diff options
author | elioat <elioat@tilde.institute> | 2023-08-23 07:52:19 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2023-08-23 07:52:19 -0400 |
commit | 562a9a52d599d9a05f871404050968a5fd282640 (patch) | |
tree | 7d3305c1252c043bfe246ccc7deff0056aa6b5ab /js/games/nluqo.github.io/broughlike-tutorial/stage4.html | |
parent | 5d012c6c011a9dedf7d0a098e456206244eb5a0f (diff) | |
download | tour-562a9a52d599d9a05f871404050968a5fd282640.tar.gz |
*
Diffstat (limited to 'js/games/nluqo.github.io/broughlike-tutorial/stage4.html')
-rw-r--r-- | js/games/nluqo.github.io/broughlike-tutorial/stage4.html | 472 |
1 files changed, 472 insertions, 0 deletions
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/stage4.html b/js/games/nluqo.github.io/broughlike-tutorial/stage4.html new file mode 100644 index 0000000..e055cda --- /dev/null +++ b/js/games/nluqo.github.io/broughlike-tutorial/stage4.html @@ -0,0 +1,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> \ No newline at end of file |