about summary refs log tree commit diff stats
path: root/js/games/nluqo.github.io/broughlike-tutorial/completed
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/completed
parent5d012c6c011a9dedf7d0a098e456206244eb5a0f (diff)
downloadtour-562a9a52d599d9a05f871404050968a5fd282640.tar.gz
*
Diffstat (limited to 'js/games/nluqo.github.io/broughlike-tutorial/completed')
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/index.html63
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/game.js205
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/map.js66
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/monster.js223
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/spell.js139
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/tile.js112
-rw-r--r--js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/util.js36
7 files changed, 844 insertions, 0 deletions
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/index.html b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/index.html
new file mode 100644
index 0000000..a4d4d57
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/index.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<title>AWESOME BROUGHLIKE</title>
+
+<style>
+    canvas{
+        outline: 1px solid white;
+    }
+
+    body{
+        background-color: indigo;
+        text-align: center;
+        margin-top: 50px;
+    }
+</style>
+
+<canvas></canvas>
+<script src="js/game.js"></script>
+<script src="js/map.js"></script>
+<script src="js/tile.js"></script>
+<script src="js/monster.js"></script>
+<script src="js/util.js"></script>
+<script src="js/spell.js"></script>
+<script>
+    tileSize = 64;
+    numTiles = 9;
+    uiWidth = 4;
+    level = 1;
+    maxHp = 6;
+
+    spritesheet = new Image();
+    spritesheet.src = 'spritesheet.png';
+    spritesheet.onload = showTitle;
+                             
+    gameState = "loading";  
+
+    startingHp = 3; 
+    numLevels = 6;      
+
+    shakeAmount = 0;       
+    shakeX = 0;                 
+    shakeY = 0;      
+
+    document.querySelector("html").onkeypress = function(e){
+        if(gameState == "title"){                              
+            startGame();                
+        }else if(gameState == "dead"){                             
+            showTitle();                                        
+        }else if(gameState == "running"){            
+            if(e.key=="w") player.tryMove(0, -1);
+            if(e.key=="s") player.tryMove(0, 1);
+            if(e.key=="a") player.tryMove(-1, 0);
+            if(e.key=="d") player.tryMove(1, 0);
+
+            if(e.key>=1 && e.key<=9) player.castSpell(e.key-1);
+        }
+    };
+
+    setInterval(draw, 15);
+
+    setupCanvas();
+
+    initSounds();
+</script>
\ No newline at end of file
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/game.js b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/game.js
new file mode 100644
index 0000000..d11c64d
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/game.js
@@ -0,0 +1,205 @@
+function setupCanvas(){
+    canvas = document.querySelector("canvas");
+    ctx = canvas.getContext("2d");
+
+    canvas.width = tileSize*(numTiles+uiWidth);
+    canvas.height = tileSize*numTiles;
+    canvas.style.width = canvas.width + 'px';
+    canvas.style.height = canvas.height + 'px';
+    ctx.imageSmoothingEnabled = false;
+}
+
+function drawSprite(sprite, x, y){
+    ctx.drawImage(
+        spritesheet,
+        sprite*16,
+        0,
+        16,
+        16,
+        x*tileSize + shakeX,
+        y*tileSize + shakeY,
+        tileSize,
+        tileSize
+    );
+}
+
+function draw(){
+    if(gameState == "running" || gameState == "dead"){  
+        ctx.clearRect(0,0,canvas.width,canvas.height);
+
+        screenshake();
+
+        for(let i=0;i<numTiles;i++){
+            for(let j=0;j<numTiles;j++){
+                getTile(i,j).draw();
+            }
+        }
+
+        for(let i=0;i<monsters.length;i++){
+            monsters[i].draw();
+        }
+
+        player.draw();
+
+        drawText("Level: "+level, 30, false, 40, "violet");
+        drawText("Score: "+score, 30, false, 70, "violet");
+
+        for(let i=0; i<player.spells.length; i++){
+            let spellText = (i+1) + ") " + (player.spells[i] || "");                        
+            drawText(spellText, 20, false, 110+i*40, "aqua");        
+        }
+    }
+}
+
+function tick(){
+    for(let k=monsters.length-1;k>=0;k--){
+        if(!monsters[k].dead){
+            monsters[k].update();
+        }else{
+            monsters.splice(k,1);
+        }
+    }
+
+    player.update();
+
+    if(player.dead){    
+        addScore(score, false);
+        gameState = "dead";
+    }
+
+    spawnCounter--;
+    if(spawnCounter <= 0){  
+        spawnMonster();
+        spawnCounter = spawnRate;
+        spawnRate--;
+    }
+}
+
+function showTitle(){                                          
+    ctx.fillStyle = 'rgba(0,0,0,.75)';
+    ctx.fillRect(0,0,canvas.width, canvas.height);
+
+    gameState = "title";
+
+    drawText("SUPER", 40, true, canvas.height/2 - 110, "white");
+    drawText("BROUGH BROS.", 70, true, canvas.height/2 - 50, "white"); 
+
+    drawScores(); 
+}
+
+function startGame(){                                           
+    level = 1;
+    score = 0;
+    numSpells = 1;
+    startLevel(startingHp);
+
+    gameState = "running";
+}
+
+function startLevel(playerHp, playerSpells){  
+    spawnRate = 15;              
+    spawnCounter = spawnRate;  
+
+    generateLevel();
+
+    player = new Player(randomPassableTile());
+    player.hp = playerHp;
+    if(playerSpells){
+        player.spells = playerSpells;
+    } 
+
+    randomPassableTile().replace(Exit); 
+}
+
+function drawText(text, size, centered, textY, color){
+    ctx.fillStyle = color;
+    ctx.font = size + "px monospace";
+    let textX;
+    if(centered){
+        textX = (canvas.width-ctx.measureText(text).width)/2;
+    }else{
+        textX = canvas.width-uiWidth*tileSize+25;
+    }
+
+    ctx.fillText(text, textX, textY);
+}
+
+function getScores(){
+    if(localStorage["scores"]){
+        return JSON.parse(localStorage["scores"]);
+    }else{
+        return [];
+    }
+}
+
+function addScore(score, won){
+    let scores = getScores();
+    let scoreObject = {score: score, run: 1, totalScore: score, active: won};
+    let lastScore = scores.pop();
+
+    if(lastScore){
+        if(lastScore.active){
+            scoreObject.run = lastScore.run+1;
+            scoreObject.totalScore += lastScore.totalScore;
+        }else{
+            scores.push(lastScore);
+        }
+    }
+    scores.push(scoreObject);
+
+    localStorage["scores"] = JSON.stringify(scores);
+}
+
+function drawScores(){
+    let scores = getScores();
+    if(scores.length){
+        drawText(
+            rightPad(["RUN","SCORE","TOTAL"]),
+            18,
+            true,
+            canvas.height/2,
+            "white"
+        );
+
+        let newestScore = scores.pop();
+        scores.sort(function(a,b){
+            return b.totalScore - a.totalScore;
+        });
+        scores.unshift(newestScore);
+
+        for(let i=0;i<Math.min(10,scores.length);i++){
+            let scoreText = rightPad([scores[i].run, scores[i].score, scores[i].totalScore]);
+            drawText(
+                scoreText,
+                18,
+                true,
+                canvas.height/2 + 24+i*24,
+                i == 0 ? "aqua" : "violet"
+            );
+        }
+    }
+}
+
+function screenshake(){
+    if(shakeAmount){
+        shakeAmount--;
+    }
+    let shakeAngle = Math.random()*Math.PI*2;
+    shakeX = Math.round(Math.cos(shakeAngle)*shakeAmount);
+    shakeY = Math.round(Math.sin(shakeAngle)*shakeAmount);
+}
+
+function initSounds(){          
+    sounds = {
+        hit1: new Audio('sounds/hit1.wav'),
+        hit2: new Audio('sounds/hit2.wav'),
+        treasure: new Audio('sounds/treasure.wav'),
+        newLevel: new Audio('sounds/newLevel.wav'),
+        spell: new Audio('sounds/spell.wav'),
+    };
+}
+
+function playSound(soundName){                       
+    sounds[soundName].currentTime = 0;  
+    sounds[soundName].play();
+}
\ No newline at end of file
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/map.js b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/map.js
new file mode 100644
index 0000000..41a1aab
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/map.js
@@ -0,0 +1,66 @@
+function generateLevel(){
+    tryTo('generate map', function(){
+        return generateTiles() == randomPassableTile().getConnectedTiles().length;
+    });
+
+    generateMonsters();
+                                           
+    for(let i=0;i<3;i++){                                         
+        randomPassableTile().treasure = true;                            
+    }
+}
+
+function generateTiles(){
+    let passableTiles=0;
+    tiles = [];
+    for(let i=0;i<numTiles;i++){
+        tiles[i] = [];
+        for(let j=0;j<numTiles;j++){
+            if(Math.random() < 0.3 || !inBounds(i,j)){
+                tiles[i][j] = new Wall(i,j);
+            }else{
+                tiles[i][j] = new Floor(i,j);
+                passableTiles++;
+            }
+        }
+    }
+    return passableTiles;
+}
+
+function inBounds(x,y){
+    return x>0 && y>0 && x<numTiles-1 && y<numTiles-1;
+}
+
+
+function getTile(x, y){
+    if(inBounds(x,y)){
+        return tiles[x][y];
+    }else{
+        return new Wall(x,y);
+    }
+}
+
+function randomPassableTile(){
+    let tile;
+    tryTo('get random passable tile', function(){
+        let x = randomRange(0,numTiles-1);
+        let y = randomRange(0,numTiles-1);
+        tile = getTile(x, y);
+        return tile.passable && !tile.monster;
+    });
+    return tile;
+}
+
+function generateMonsters(){
+    monsters = [];
+    let numMonsters = level+1;
+    for(let i=0;i<numMonsters;i++){
+        spawnMonster();
+    }
+}
+
+function spawnMonster(){
+    let monsterType = shuffle([Bird, Snake, Tank, Eater, Jester])[0];
+    let monster = new monsterType(randomPassableTile());
+    monsters.push(monster);
+}
\ No newline at end of file
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/monster.js b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/monster.js
new file mode 100644
index 0000000..7e36257
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/monster.js
@@ -0,0 +1,223 @@
+class Monster{
+    constructor(tile, sprite, hp){
+        this.move(tile);
+        this.sprite = sprite;
+        this.hp = hp;
+        this.teleportCounter = 2;
+        this.offsetX = 0;                                                   
+        this.offsetY = 0;      
+        this.lastMove = [-1,0];    
+        this.bonusAttack = 0;
+    }
+
+    heal(damage){
+        this.hp = Math.min(maxHp, this.hp+damage);
+    }
+
+    update(){
+        this.teleportCounter--;
+        if(this.stunned || this.teleportCounter > 0){ 
+            this.stunned = false;
+            return;
+        }
+
+        this.doStuff();
+    }
+
+    doStuff(){
+       let neighbors = this.tile.getAdjacentPassableNeighbors();
+       
+       neighbors = neighbors.filter(t => !t.monster || t.monster.isPlayer);
+
+       if(neighbors.length){
+           neighbors.sort((a,b) => a.dist(player.tile) - b.dist(player.tile));
+           let newTile = neighbors[0];
+           this.tryMove(newTile.x - this.tile.x, newTile.y - this.tile.y);
+       }
+    }
+
+    getDisplayX(){                     
+        return this.tile.x + this.offsetX;
+    }
+
+    getDisplayY(){                                                                  
+        return this.tile.y + this.offsetY;
+    }
+
+    draw(){
+        if(this.teleportCounter > 0){                  
+            drawSprite(10, this.getDisplayX(),  this.getDisplayY());                 
+        }else{
+            drawSprite(this.sprite, this.getDisplayX(),  this.getDisplayY());
+            this.drawHp();
+        }
+
+        this.offsetX -= Math.sign(this.offsetX)*(1/8);     
+        this.offsetY -= Math.sign(this.offsetY)*(1/8); 
+    }
+
+    drawHp(){
+        for(let i=0; i<this.hp; i++){
+            drawSprite(
+                9,
+                this.getDisplayX() + (i%3)*(5/16),   
+                this.getDisplayY() - Math.floor(i/3)*(5/16)
+            );
+        }
+    }   
+
+    tryMove(dx, dy){
+        let newTile = this.tile.getNeighbor(dx,dy);
+        if(newTile.passable){
+            this.lastMove = [dx,dy];
+            if(!newTile.monster){
+                this.move(newTile);
+            }else{
+                if(this.isPlayer != newTile.monster.isPlayer){
+                    this.attackedThisTurn = true;
+                    newTile.monster.stunned = true;
+                    newTile.monster.hit(1 + this.bonusAttack);
+                    this.bonusAttack = 0;
+
+                    shakeAmount = 5;
+
+                    this.offsetX = (newTile.x - this.tile.x)/2;         
+                    this.offsetY = (newTile.y - this.tile.y)/2;   
+                }
+            }
+            return true;
+        }
+    }
+
+    hit(damage){            
+        if(this.shield>0){           
+            return;                                                             
+        }
+
+        this.hp -= damage;
+        if(this.hp <= 0){
+            this.die();
+        }
+
+        if(this.isPlayer){                                                     
+            playSound("hit1");                                              
+        }else{                                                       
+            playSound("hit2");                                              
+        }
+    }
+
+    die(){
+        this.dead = true;
+        this.tile.monster = null;
+        this.sprite = 1;
+    }
+
+    move(tile){
+        if(this.tile){
+            this.tile.monster = null;
+            this.offsetX = this.tile.x - tile.x;    
+            this.offsetY = this.tile.y - tile.y;
+        }
+        this.tile = tile;
+        tile.monster = this;                                           
+        tile.stepOn(this);   
+    }
+}
+
+class Player extends Monster{
+    constructor(tile){
+        super(tile, 0, 3);
+        this.isPlayer = true;
+        this.teleportCounter = 0;
+        this.spells = shuffle(Object.keys(spells)).splice(0,numSpells);
+    }
+
+    update(){          
+        this.shield--;                                                      
+    }
+
+    tryMove(dx, dy){
+        if(super.tryMove(dx,dy)){
+            tick();
+        }
+    }
+
+    addSpell(){                                                       
+        let newSpell = shuffle(Object.keys(spells))[0];
+        this.spells.push(newSpell);
+    }
+
+    castSpell(index){                                                   
+        let spellName = this.spells[index];
+        if(spellName){
+            delete this.spells[index];
+            spells[spellName]();
+            playSound("spell");
+            tick();
+        }
+    }
+}
+
+class Bird extends Monster{
+    constructor(tile){
+        super(tile, 4, 3);
+    }
+}
+
+class Snake extends Monster{
+    constructor(tile){
+        super(tile, 5, 1);
+    }
+
+    doStuff(){
+        this.attackedThisTurn = false;
+        super.doStuff();
+
+        if(!this.attackedThisTurn){
+            super.doStuff();
+        }
+    }
+}
+
+class Tank extends Monster{
+    constructor(tile){
+        super(tile, 6, 2);
+    }
+
+    update(){
+        let startedStunned = this.stunned;
+        super.update();
+        if(!startedStunned){
+            this.stunned = true;
+        }
+    }
+}
+
+class Eater extends Monster{
+    constructor(tile){
+        super(tile, 7, 1);
+    }
+
+    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();
+        }
+    }
+}
+
+class Jester extends Monster{
+    constructor(tile){
+        super(tile, 8, 2);
+    }
+
+    doStuff(){
+        let neighbors = this.tile.getAdjacentPassableNeighbors();
+        if(neighbors.length){
+            this.tryMove(neighbors[0].x - this.tile.x, neighbors[0].y - this.tile.y);
+        }
+    }
+}
\ No newline at end of file
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/spell.js b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/spell.js
new file mode 100644
index 0000000..0b33e56
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/spell.js
@@ -0,0 +1,139 @@
+spells = {
+    WOOP: function(){
+        player.move(randomPassableTile());
+    },
+    QUAKE: function(){                  
+        for(let i=0; i<numTiles; i++){
+            for(let j=0; j<numTiles; j++){
+                let tile = getTile(i,j);
+                if(tile.monster){
+                    let numWalls = 4 - tile.getAdjacentPassableNeighbors().length;
+                    tile.monster.hit(numWalls*2);
+                }
+            }
+        }
+        shakeAmount = 20;
+    },
+    MAELSTROM: function(){
+        for(let i=0;i<monsters.length;i++){
+            monsters[i].move(randomPassableTile());
+            monsters[i].teleportCounter = 2;
+        }
+    },
+    MULLIGAN: function(){
+        startLevel(1, player.spells);
+    },
+    AURA: function(){
+        player.tile.getAdjacentNeighbors().forEach(function(t){
+            t.setEffect(13);
+            if(t.monster){
+                t.monster.heal(1);
+            }
+        });
+        player.tile.setEffect(13);
+        player.heal(1);
+    },
+    DASH: function(){
+        let newTile = player.tile;
+        while(true){
+            let testTile = newTile.getNeighbor(player.lastMove[0],player.lastMove[1]);
+            if(testTile.passable && !testTile.monster){
+                newTile = testTile;
+            }else{
+                break;
+            }
+        }
+        if(player.tile != newTile){
+            player.move(newTile);
+            newTile.getAdjacentNeighbors().forEach(t => {
+                if(t.monster){
+                    t.setEffect(14);
+                    t.monster.stunned = true;
+                    t.monster.hit(1);
+                }
+            });
+        }
+    },
+    DIG: function(){
+        for(let i=1;i<numTiles-1;i++){
+            for(let j=1;j<numTiles-1;j++){
+                let tile = getTile(i,j);
+                if(!tile.passable){
+                    tile.replace(Floor);
+                }
+            }
+        }
+        player.tile.setEffect(13);
+        player.heal(2);
+    },
+    KINGMAKER: function(){
+        for(let i=0;i<monsters.length;i++){
+            monsters[i].heal(1);
+            monsters[i].tile.treasure = true;
+        }
+    },
+    ALCHEMY: function(){
+        player.tile.getAdjacentNeighbors().forEach(function(t){
+            if(!t.passable && inBounds(t.x, t.y)){
+                t.replace(Floor).treasure = true;
+            }
+        });
+    },
+    POWER: function(){
+        player.bonusAttack=5;
+    },
+    BUBBLE: function(){
+        for(let i=player.spells.length-1;i>0;i--){
+            if(!player.spells[i]){
+                player.spells[i] = player.spells[i-1];
+            }
+        }
+    },
+    BRAVERY: function(){
+        player.shield = 2;
+        for(let i=0;i<monsters.length;i++){
+            monsters[i].stunned = true;
+        }
+    },
+    BOLT: function(){
+        boltTravel(player.lastMove, 15 + Math.abs(player.lastMove[1]), 4);
+    },
+    CROSS: function(){
+        let directions = [
+            [0, -1],
+            [0, 1],
+            [-1, 0],
+            [1, 0]
+        ];
+        for(let k=0;k<directions.length;k++){
+            boltTravel(directions[k], 15 + Math.abs(directions[k][1]), 2);
+        }
+    },
+    EX: function(){
+        let directions = [
+            [-1, -1],
+            [-1, 1],
+            [1, -1],
+            [1, 1]
+        ];
+        for(let k=0;k<directions.length;k++){
+            boltTravel(directions[k], 14, 3);
+        }
+    }
+};
+
+function boltTravel(direction, effect, damage){
+    let newTile = player.tile;
+    while(true){
+        let testTile = newTile.getNeighbor(direction[0], direction[1]);
+        if(testTile.passable){
+            newTile = testTile;
+            if(newTile.monster){
+                newTile.monster.hit(damage);
+            }
+            newTile.setEffect(effect);
+        }else{
+            break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/tile.js b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/tile.js
new file mode 100644
index 0000000..3cc40e5
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/tile.js
@@ -0,0 +1,112 @@
+class Tile{
+    constructor(x, y, sprite, passable){
+        this.x = x;
+        this.y = y;
+        this.sprite = sprite;
+        this.passable = passable;
+    }
+
+    replace(newTileType){
+        tiles[this.x][this.y] = new newTileType(this.x, this.y);
+        return tiles[this.x][this.y];
+    }
+
+    //manhattan distance
+    dist(other){
+        return Math.abs(this.x-other.x)+Math.abs(this.y-other.y);
+    }
+
+    getNeighbor(dx, dy){
+        return getTile(this.x + dx, this.y + dy)
+    }
+
+    getAdjacentNeighbors(){
+        return shuffle([
+            this.getNeighbor(0, -1),
+            this.getNeighbor(0, 1),
+            this.getNeighbor(-1, 0),
+            this.getNeighbor(1, 0)
+        ]);
+    }
+
+    getAdjacentPassableNeighbors(){
+        return this.getAdjacentNeighbors().filter(t => t.passable);
+    }
+
+    getConnectedTiles(){
+        let connectedTiles = [this];
+        let frontier = [this];
+        while(frontier.length){
+            let neighbors = frontier.pop()
+                                .getAdjacentPassableNeighbors()
+                                .filter(t => !connectedTiles.includes(t));
+            connectedTiles = connectedTiles.concat(neighbors);
+            frontier = frontier.concat(neighbors);
+        }
+        return connectedTiles;
+    }
+
+    draw(){
+        drawSprite(this.sprite, this.x, this.y);
+
+        if(this.treasure){                      
+            drawSprite(12, this.x, this.y);                                             
+        }
+
+        if(this.effectCounter){                    
+            this.effectCounter--;
+            ctx.globalAlpha = this.effectCounter/30;
+            drawSprite(this.effect, this.x, this.y);
+            ctx.globalAlpha = 1;
+        }
+    }
+
+    setEffect(effectSprite){                                  
+        this.effect = effectSprite;
+        this.effectCounter = 30;
+    }
+}
+
+class Floor extends Tile{
+    constructor(x,y){
+        super(x, y, 2, true);
+    };
+
+    stepOn(monster){
+        if(monster.isPlayer && this.treasure){   
+            score++;   
+            if(score % 3 == 0 && numSpells < 9){                         
+                numSpells++;                
+                player.addSpell();            
+            }  
+            playSound("treasure");            
+            this.treasure = false;
+            spawnMonster();
+        }
+    }
+}
+
+class Wall extends Tile{
+    constructor(x, y){
+        super(x, y, 3, false);
+    }
+}
+
+class Exit extends Tile{
+    constructor(x, y){
+        super(x, y, 11, true);
+    }
+
+    stepOn(monster){
+        if(monster.isPlayer){
+            playSound("newLevel"); 
+            if(level == numLevels){
+                addScore(score, true); 
+                showTitle();
+            }else{
+                level++;
+                startLevel(Math.min(maxHp, player.hp+1));
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/util.js b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/util.js
new file mode 100644
index 0000000..a56c6a9
--- /dev/null
+++ b/js/games/nluqo.github.io/broughlike-tutorial/completed/stage8/js/util.js
@@ -0,0 +1,36 @@
+function tryTo(description, callback){
+    for(let timeout=1000;timeout>0;timeout--){
+        if(callback()){
+            return;
+        }
+    }
+    throw 'Timeout while trying to '+description;
+}
+
+
+function randomRange(min, max){
+    return Math.floor(Math.random()*(max-min+1))+min;
+}
+
+function shuffle(arr){
+    let temp, r;
+    for (let i = 1; i < arr.length; i++) {
+        r = randomRange(0,i);
+        temp = arr[i];
+        arr[i] = arr[r];
+        arr[r] = temp;
+    }
+    return arr;
+}
+
+function rightPad(textArray){
+    let finalText = "";
+    textArray.forEach(text => {
+        text+="";
+        for(let i=text.length;i<10;i++){
+            text+=" ";
+        }
+        finalText += text;
+    });
+    return finalText;
+}
\ No newline at end of file