about summary refs log tree commit diff stats
path: root/html/apps/pack.subx.html
Commit message (Expand)AuthorAgeFilesLines
* 6091Kartik Agaram2020-03-061-3/+3
* 6084Kartik Agaram2020-03-061-22/+22
* 6015Kartik Agaram2020-02-171-44/+44
* 6001Kartik Agaram2020-02-091-84/+84
* 5925Kartik Agaram2020-01-271-61/+61
* 5901Kartik Agaram2020-01-191-11/+11
* 5897 - rename comparison instructionsKartik Agaram2020-01-161-49/+49
* 5884Kartik Agaram2020-01-121-18/+18
* 5876 - address -> addrKartik Agaram2020-01-031-21/+21
* 5806Kartik Agaram2019-12-091-408/+408
* 5783Kartik Agaram2019-11-301-1/+1
* 5716Kartik Agaram2019-10-261-4440/+4346
* 5701Kartik Agaram2019-10-171-114/+114
* 5684Kartik Agaram2019-09-211-44/+44
* 5683Kartik Agaram2019-09-201-180/+180
* 5661Kartik Agaram2019-09-151-1/+1
* 5607Kartik Agaram2019-09-011-3/+3
* 5600Kartik Agaram2019-08-311-4729/+4732
* 5592 - switch register names to lowercaseKartik Agaram2019-08-261-1261/+1261
* 5589Kartik Agaram2019-08-251-1/+1
* 5582Kartik Agaram2019-08-251-357/+357
* 5499Kartik Agaram2019-07-311-1/+1
* 5490Kartik Agaram2019-07-271-2/+2
* 5485 - promote SubX to top-levelKartik Agaram2019-07-271-0/+4833
a id='n257' href='#n257'>257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
<!DOCTYPE html>
<html>
<head>
	<title>Broughlike tutorial - Stage 4</title>
	<meta charset="utf-8">
</head>
<body>
	<div id="outer">
        <div class="header">
            <a href="index.html">JavaScript Broughlike Tutorial</a>
            <a href="stage3.html">Previously: Monsters</a>
        </div>
		<h1>Stage 4 - Monsters Part 2</h1>

		Since the monsters will be attacking the player and vice versa, let's first draw the HP for each.
		
		<div class="filename">monster.js</div>
        <div class="code-container">
			<pre><code id="contentDRAWHP1" class="javascript"></code></pre>
			<pre><code id="contentDRAWHP2" class="javascript add"></code></pre>
			<pre><code id="contentDRAWHP3" class="javascript"></code></pre>
			<pre><code id="contentDRAWHP4" class="javascript add"></code></pre>
        </div>
        Monster HP now displays nicely.
        <br><br>
        The idea here is to draw one HP pip sprite for each unit of HP the monster has. We can't just draw each pip in the same spot; you would only see the top one in that case. We've got a bit of funky math to layout all the pips:
        <br><br>
        <ul>
            <li>
                <strong>
                    <div class="code-container-inline inline"><pre><code class="javascript">5/16</code></pre></div>
                </strong> - Since 
                <div class="code-container-inline inline"><pre><code class="javascript">drawSprite</code></pre></div>
                operates on a sprite index we normally pass in whole numbers representing 16 pixel sprites. However, we can instead work in individual pixels by using fractions. So  
                <div class="code-container-inline inline"><pre><code class="javascript">5/16</code></pre></div>
                means 5 pixels within a 16 pixel sprite.
            </li>
            <li>
                <strong>
                    <div class="code-container-inline inline"><pre><code class="javascript">i%3</code></pre></div>
                </strong> - this resets to 0 every 3 pips.
            </li>
            <li>
                <strong>
                    <div class="code-container-inline inline"><pre><code class="javascript">Math.floor(i/3)</code></pre></div>
                </strong> - this increases by one every 3 pips.
            </li>
        </ul>
        The result means pips are drawn first left to right offset by 5 pixels each and then stacked vertically offset by 5 pixels for each row.
        <img src="screens/hp.png">
        <h2>Attacking</h2>
        Now onto attacking. We can let the monsters attack the player and vice versa with a small addition to
        <div class="code-container-inline inline"><pre><code class="javascript">tryMove</code></pre></div>.

        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentATTACK1" class="javascript"></code></pre>
            <pre><code id="contentATTACK2" class="javascript add"></code></pre>
            <pre><code id="contentATTACK3" class="javascript"></code></pre>
            <pre><code id="contentATTACK4" class="javascript add"></code></pre>
        </div>
        We're comparing 
        <div class="code-container-inline inline"><pre><code class="javascript">isPlayer</code></pre></div>
        flags to make sure monsters don't attack each other.
        <br><br>
        When an attack is successful, that triggers
        <div class="code-container-inline inline"><pre><code class="javascript">hit</code></pre></div>,
        applying damage to the target monster's HP and if they run out of HP, they
        <div class="code-container-inline inline"><pre><code class="javascript">die</code></pre></div>. When dying, the monster sprite is set to index 
        <div class="code-container-inline inline"><pre><code class="javascript">1</code></pre></div>
        (our player corpse). This will only apply to the player since we earlier wrote code to delete monsters as soon as they are 
        <div class="code-container-inline inline"><pre><code class="javascript">dead</code></pre></div>.
        <br><br>
        Cool beans! Attacking is in the game. If you let the monsters kill you, you'll notice that you're still able to move around the map as a corpse. We'll tackle that later.
        <br><br>
        Monsters are working as expected, but with identical behavior. Here's the plan for making each one unique:

        <ul>
            <li><strong>Bird:</strong> our basic monster with no special behavior</li>
            <li><strong>Snake:</strong> moves twice (yes, basically copied from 868-HACK's Virus)</li>
            <li><strong>Tank:</strong> moves every other turn</li>
            <li><strong>Eater:</strong> destroys walls and heals by doing so</li>
            <li><strong>Jester:</strong> moves randomly</li>
        </ul>
        <h2>Snake</h2>
        Since 
        <div class="code-container-inline inline"><pre><code class="javascript">Bird</code></pre></div>
        is already done, let's start with 
        <div class="code-container-inline inline"><pre><code class="javascript">Snake</code></pre></div>. Make sure to test out each monster after updating their code. While testing this code, it may be easier to temporarily modify the 
        <div class="code-container-inline inline"><pre><code class="javascript">spawnMonster</code></pre></div>
        code to only generate a specific kind of monster (left as an exercise for the reader). Or you can just refresh a bunch of times.

        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentSNAKE" class="javascript"></code></pre>
            <pre><code id="contentSNAKEADD" class="javascript add"></code></pre>
            <pre><code id="contentCLOSINGBRACE" class="javascript"></code></pre>
        </div>

        Rather simple. The Snake can move twice, move and attack, but not attack twice (that's overpowered!).

        We need one tie-in within tryMove to set 
        <div class="code-container-inline inline"><pre><code class="javascript">attackedThisTurn</code></pre></div>
        to true upon attacking.

        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentPREATTACKEDTHISTURN" class="javascript"></code></pre>
            <pre><code id="contentATTACKEDTHISTURN" class="javascript add"></code></pre>
            <pre><code id="contentPOSTATTACKEDTHISTURN" class="javascript"></code></pre>
        </div>

        <h2>Tank</h2>
        While working on the Tank, we'll introduce a 
        <div class="code-container-inline inline"><pre><code class="javascript">stunned</code></pre></div>
        flag. When a monster is stunned, they'll be unable to react until the next turn.
        <br><br>
        We'll be able to use this flag in multiple ways: to stun monsters whenever they are hit by the player or hit by certain spells and to pause the action of monsters like the Tank.

        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentPREATTACKEDTHISTURN" class="javascript"></code></pre>
            <pre><code id="contentATTACKEDTHISTURN" class="javascript"></code></pre>
            <pre><code id="contentSTUNFLAG" class="javascript add"></code></pre>
            <pre><code id="contentPOSTATTACKEDTHISTURN" class="javascript"></code></pre>
        </div>
        When monsters are attacked, they get 
        <div class="code-container-inline inline"><pre><code class="javascript">stunned</code></pre></div>,
        making it easier  for the player to take on tough monsters.
        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentPREUPDATEDUPDATE" class="javascript"></code></pre>
            <pre><code id="contentUPDATEDUPDATE" class="javascript add"></code></pre>
            <pre><code id="contentPOSTUPDATEDUPDATE" class="javascript"></code></pre>
        </div>
        If the
        <div class="code-container-inline inline"><pre><code class="javascript">stunned</code></pre></div>
        flag is true, we reset it to false and do a 
        <div class="code-container-inline inline"><pre><code class="javascript">return</code></pre></div>
        which exits the function and prevents the monster from doing anything until next turn.
        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentTANK" class="javascript"></code></pre>
            <pre><code id="contentTANKADD" class="javascript add"></code></pre>
            <pre><code id="contentCLOSINGBRACE" class="javascript"></code></pre>
        </div>
        Here, the 
        <div class="code-container-inline inline"><pre><code class="javascript">Tank</code></pre></div>
        monster stuns itself if it wasn't already
        <div class="code-container-inline inline"><pre><code class="javascript">stunned</code></pre></div>
        at the beginning of the turn. Effectively, this results in action only every other turn.

        <h2>Eater</h2>
        Then comes the 
        <div class="code-container-inline inline"><pre><code class="javascript">Eater</code></pre></div>.
        Before doing normal monster behavior, this guy is going to check for any nearby walls and eat them for health! Each wall will grant half a health point (our 
        <div class="code-container-inline inline"><pre><code class="javascript">drawHp</code></pre></div>
        method only draws whole points though).

        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentEATER" class="javascript"></code></pre>
            <pre><code id="contentEATERADD" class="javascript add"></code></pre>
            <pre><code id="contentCLOSINGBRACE" class="javascript"></code></pre>
        </div>
        First, we need to get all the nearby walls using 
        <div class="code-container-inline inline"><pre><code class="javascript">getAdjacentNeighbors</code></pre></div>
        and only include tiles that are not 
        <div class="code-container-inline inline"><pre><code class="javascript">passable</code></pre></div>
        (indicating a wall)
        and are also 
        <div class="code-container-inline inline"><pre><code class="javascript">inBounds</code></pre></div>
        (so the outer wall doesn't get destroyed).
        <br><br>
        If walls are found, we're going to call two new methods. The 
        <div class="code-container-inline inline"><pre><code class="javascript">replace</code></pre></div>
        method is replacing a
        <div class="code-container-inline inline"><pre><code class="javascript">Wall</code></pre></div>
        tile with a 
        <div class="code-container-inline inline"><pre><code class="javascript">Floor</code></pre></div> tile.
        The 
        <div class="code-container-inline inline"><pre><code class="javascript">heal</code></pre></div>
        method adds half a hitpoint to the monster.  If no walls are found, we'll simply do the normal monster behavior.
        <br><br>
        Now let's implement those methods.
        <div class="filename">monster.js</div>
        <div class="code-container">
            <pre><code id="contentPREHEAL" class="javascript"></code></pre>
            <pre><code id="contentHEAL" class="javascript add"></code></pre>
        </div>
        This method 
        <div class="code-container-inline inline"><pre><code class="javascript">heal</code></pre></div>
        is a one-liner. Add some amount of healing "damage" without going over some global 
        <div class="code-container-inline inline"><pre><code class="javascript">maxHp</code></pre></div>, which we'll need to define next. We don't want our monsters to gain infinite health!
        <div class="filename">index.html</div>
        <div class="code-container margin-bottom">
            <pre><code id="contentPREMAXHP" class="javascript"></code></pre>
            <pre><code id="contentMAXHP" class="javascript add"></code></pre>
        </div>
        Next up is <div class="code-container-inline inline"><pre><code class="javascript">replace</code></pre></div>.
        <div class="filename">tile.js</div>
        <div class="code-container">
            <pre><code id="contentPREREPLACE" class="javascript"></code></pre>
            <pre><code id="contentREPLACE" class="javascript add"></code></pre>
        </div>
        You can use
        <div class="code-container-inline inline"><pre><code class="javascript">replace</code></pre></div>
        any time one tile type changes into another type. Here it's a wall replacing a floor, but imagine if a water tile replaced a floor!
        <br><br>One thing that's not coded here is to copy over monsters and items present on the old tile to the new tile. Keep that in mind for future additions.


        <h2>Jester</h2>
			The last monster is the 
            <div class="code-container-inline inline"><pre><code class="javascript">Jester</code></pre></div>
            and it's able to move randomly simply by trying to move to the first neighbor returned by the (pre-shuffled)
            <div class="code-container-inline inline"><pre><code class="javascript">getAdjacentPassableNeighbors</code></pre></div>.
            <div class="filename">tile.js</div>
            <div class="code-container margin-bottom">
                <pre><code id="contentJESTER" class="javascript"></code></pre>
                <pre><code id="contentJESTERADD" class="javascript add"></code></pre>
                <pre><code id="contentCLOSINGBRACE" class="javascript"></code></pre>
            </div>

            With those enemy behaviors in place, our little broughlike is starting to feel like... a game. 😍
            <img src="screens/behavior.gif">
			In the <a href="stage5.html">next section</a>, we'll turn this thing into a proper game with a title screen, multiple levels, and victory and failure conditions.
	</div>

	<script>
		let content = {
			DRAWHP1:
			`
    draw(){
        drawSprite(this.sprite, this.tile.x, this.tile.y);
    			`,
    			DRAWHP2:
    			`
        this.drawHp();
			`,
			DRAWHP3:
			`
    }

			`,
			DRAWHP4:
			`
    drawHp(){
        for(let i=0; i<this.hp; i++){
            drawSprite(
                9,
                this.tile.x + (i%3)*(5/16),
                this.tile.y - Math.floor(i/3)*(5/16)
            );
        }
    }		
			`,
            ATTACK1:
            `
    tryMove(dx, dy){
        let newTile = this.tile.getNeighbor(dx,dy);
        if(newTile.passable){
            if(!newTile.monster){
                this.move(newTile);
            `,
            ATTACK2:
            `
            }else{
                if(this.isPlayer != newTile.monster.isPlayer){
                    newTile.monster.hit(1);
                }
            `,
            ATTACK3:
            `
            }
            return true;
        }
    }

            `,
            ATTACK4:
            `
    hit(damage){
        this.hp -= damage;
        if(this.hp <= 0){
            this.die();
        }
    }

    die(){
        this.dead = true;
        this.tile.monster = null;
        this.sprite = 1;
    }
            `,
            SNAKE:
            `
class Snake extends Monster{
    constructor(tile){
        super(tile, 5, 1);
    }

            `,
            SNAKEADD:
            `
    doStuff(){
        this.attackedThisTurn = false;
        super.doStuff();

        if(!this.attackedThisTurn){
            super.doStuff();
        }
    }
            `,
            TANK:
            `
class Tank extends Monster{
    constructor(tile){
        super(tile, 6, 2);
    }

            `,
            TANKADD:
            `
    update(){
        let startedStunned = this.stunned;
        super.update();
        if(!startedStunned){
            this.stunned = true;
        }
    }
            `,
            EATER:
            `
class Eater extends Monster{
    constructor(tile){
        super(tile, 7, 1);
    }

            `,
            EATERADD:
            `
    doStuff(){
        let neighbors = this.tile.getAdjacentNeighbors().filter(t => !t.passable && inBounds(t.x,t.y));
        if(neighbors.length){
            neighbors[0].replace(Floor);
            this.heal(0.5);
        }else{
            super.doStuff();
        }
    }
            `,
            JESTER:
            `
class Jester extends Monster{
    constructor(tile){
        super(tile, 8, 2);
    }

            `,
            JESTERADD:
            `
    doStuff(){
        let neighbors = this.tile.getAdjacentPassableNeighbors();
        if(neighbors.length){
            this.tryMove(neighbors[0].x - this.tile.x, neighbors[0].y - this.tile.y);
        }
    }
            `,
            CLOSINGBRACE:
            `
}

            `,
            PREATTACKEDTHISTURN:
            `
    tryMove(dx, dy){
        let newTile = this.tile.getNeighbor(dx,dy);
        if(newTile.passable){
            if(!newTile.monster){
                this.move(newTile);
            }else{
                if(this.isPlayer != newTile.monster.isPlayer){       
            `,
            ATTACKEDTHISTURN:
            `
                    this.attackedThisTurn = true;
            `,
            POSTATTACKEDTHISTURN:
            `
                    newTile.monster.hit(1);
                }
            }
            return true;
        }
    }
            `,
            STUNFLAG:
            `
                    newTile.monster.stunned = true;
            `,
            PREUPDATEDUPDATE:
            `
    update(){
            `,
            UPDATEDUPDATE:
            `
        if(this.stunned){
            this.stunned = false;
            return;
        }
            `,
            POSTUPDATEDUPDATE:
            `

        this.doStuff();
    }
            `,
            PREHEAL:
            `
class Monster{
    constructor(tile, sprite, hp){
        this.move(tile);
        this.sprite = sprite;
        this.hp = hp;
    }

            `,
            HEAL:
            `
    heal(damage){
        this.hp = Math.min(maxHp, this.hp+damage);
    }
            `,
            PREMAXHP:
            `
<script>
    tileSize = 64;
    numTiles = 9;
    uiWidth = 4;
    level = 1;
            `,
            MAXHP:
            `
    maxHp = 6; 
            `,
            PREREPLACE:
            `
class Tile{
    constructor(x, y, sprite, passable){
        this.x = x;
        this.y = y;
        this.sprite = sprite;
        this.passable = passable;
    }

            `,
            REPLACE:
            `
    replace(newTileType){
        tiles[this.x][this.y] = new newTileType(this.x, this.y);
        return tiles[this.x][this.y];
    }
            `
		};
	</script>

	<link rel="stylesheet" href="highlight.min.css">
	<link rel="stylesheet" href="style.css">
	<script src="highlight.min.js"></script>
	<script src="diff.js"></script>
</body>
</html>