about summary refs log tree commit diff stats
path: root/js/games/nluqo.github.io/broughlike-tutorial/stage4.html
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2023-08-23 07:52:19 -0400
committerelioat <elioat@tilde.institute>2023-08-23 07:52:19 -0400
commit562a9a52d599d9a05f871404050968a5fd282640 (patch)
tree7d3305c1252c043bfe246ccc7deff0056aa6b5ab /js/games/nluqo.github.io/broughlike-tutorial/stage4.html
parent5d012c6c011a9dedf7d0a098e456206244eb5a0f (diff)
downloadtour-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.html472
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