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/completed | |
parent | 5d012c6c011a9dedf7d0a098e456206244eb5a0f (diff) | |
download | tour-562a9a52d599d9a05f871404050968a5fd282640.tar.gz |
*
Diffstat (limited to 'js/games/nluqo.github.io/broughlike-tutorial/completed')
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 |