diff options
Diffstat (limited to 'centipede')
26 files changed, 983 insertions, 0 deletions
diff --git a/centipede/.DS_Store b/centipede/.DS_Store Binary files differnew file mode 100644 index 0000000..0342a22 --- /dev/null +++ b/centipede/.DS_Store diff --git a/centipede/assets/.DS_Store b/centipede/assets/.DS_Store Binary files differnew file mode 100644 index 0000000..12856bc --- /dev/null +++ b/centipede/assets/.DS_Store diff --git a/centipede/assets/images/.DS_Store b/centipede/assets/images/.DS_Store Binary files differnew file mode 100644 index 0000000..b7bf291 --- /dev/null +++ b/centipede/assets/images/.DS_Store diff --git a/centipede/assets/images/centipede-assets.png b/centipede/assets/images/centipede-assets.png Binary files differnew file mode 100644 index 0000000..20b9eac --- /dev/null +++ b/centipede/assets/images/centipede-assets.png diff --git a/centipede/assets/sounds/.DS_Store b/centipede/assets/sounds/.DS_Store Binary files differnew file mode 100644 index 0000000..34bc351 --- /dev/null +++ b/centipede/assets/sounds/.DS_Store diff --git a/centipede/assets/sounds/enemy_hit.wav b/centipede/assets/sounds/enemy_hit.wav Binary files differnew file mode 100644 index 0000000..1b9c53f --- /dev/null +++ b/centipede/assets/sounds/enemy_hit.wav diff --git a/centipede/assets/sounds/laser.wav b/centipede/assets/sounds/laser.wav Binary files differnew file mode 100644 index 0000000..16159a4 --- /dev/null +++ b/centipede/assets/sounds/laser.wav diff --git a/centipede/assets/sounds/mushroom_hit.wav b/centipede/assets/sounds/mushroom_hit.wav Binary files differnew file mode 100644 index 0000000..ef26432 --- /dev/null +++ b/centipede/assets/sounds/mushroom_hit.wav diff --git a/centipede/css/style.css b/centipede/css/style.css new file mode 100644 index 0000000..7142e28 --- /dev/null +++ b/centipede/css/style.css @@ -0,0 +1,63 @@ +body { + margin: 0; + font-family: Lucida Sans Typewriter,Lucida Console,monaco,Bitstream Vera Sans Mono,monospace; +} + +.canvas-holder canvas { + padding: 0; + margin: auto; + display: block; + height: 100vh; + width: auto; + max-width: 100%; + background-color: black; +} + +.game-hud { + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -5px); + z-index: 1; + + background-color: rgba(255,255,255,0.5); + padding-left: 10px; + padding-right: 10px; + padding-top: 3px; + border: 1px solid white; + border-radius: 5px; +} + +.game-hud p { + margin: 0; + padding: 0; + font-size: 3vh; + text-align: center; +} + +.menu { + text-align:center; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + position:absolute; + + background-color: rgba(255,255,255,0.75); + border: 1px solid #fff; + border-radius: 5px; + padding: 12px; + min-width: 400px; +} + +.menu-button { + background-color: #fff; + border-radius: 5px; + padding: 12px; + margin-bottom: 8px; + cursor: pointer; +} + +.menu-button:hover { + background-color: #d0d0d0; +}
\ No newline at end of file diff --git a/centipede/index.html b/centipede/index.html new file mode 100644 index 0000000..3d73470 --- /dev/null +++ b/centipede/index.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Centipede</title> + <link rel="stylesheet" href="css/style.css"> + </head> + + <body> + <div style="text-align:center"> + <div class="canvas-holder"> + <canvas id="game-canvas" width="1400" height="1000"></canvas> + <div class="game-hud" id="hud"> + <p>Hello, world!</p> + </div> + <div class="menu" style="display: none" id="menu"> + </div> + </div> + </div> + + <script src="js/game/game.js"></script> + <script src="js/game/input.js"></script> + <script src="js/game/menu.js"></script> + <script src="js/game/graphics.js"></script> + <script src="js/game/sprites.js"></script> + <script src="js/game/sounds.js"></script> + <script src="js/game/object.js"></script> + <script src="js/game/objects/player.js"></script> + <script src="js/game/objects/mushroom.js"></script> + <script src="js/game/objects/bullet.js"></script> + <script src="js/game/objects/centipede.js"></script> + <script src="js/game/objects/explosion.js"></script> + <script src="js/game/objects/flea.js"></script> + <script src="js/game/objects/spider.js"></script> + <script src="js/game/objects/scorpion.js"></script> + + <script src="js/main.js"></script> + </body> +</html>
\ No newline at end of file diff --git a/centipede/js/game/game.js b/centipede/js/game/game.js new file mode 100644 index 0000000..30454f9 --- /dev/null +++ b/centipede/js/game/game.js @@ -0,0 +1,39 @@ +const game = { + stopped: false, + width: document.getElementById('game-canvas').width, + height: document.getElementById('game-canvas').height, + level: 1, +}; + +game.resume = () => { + game.stopped = false; + game.lastTimeStamp = performance.now(); + menu.reRegisterKeys(); + requestAnimationFrame(gameLoop); +} + +game.resetObjects = () => { + game.player.x = game.width/2; + game.player.y = game.height/2; + game.bullets = []; + game.explosions = []; + game.mushroomDims = {width: 40, height: 40}; + game.mushrooms = game.Mushroom.generateMushrooms(game.mushroomDims); + game.centipede = game.Centipede({segments: Math.min(game.level*5 + 5, 15), startX: game.width/2, startY: 0, rot: 180, width: 40, height: 40, dx: 0.2, dy: 0}); + game.spiders = []; + game.fleas = []; + game.scorpions = []; +} + +game.gameOver = () => { + menu.showMenu(); + menu.setState('game-over'); + menu.addScore(game.score); + + menu.onHide = initialize; +} + +game.getObjects = () => [game.player, ...game.bullets, ...game.mushrooms, ...game.spiders, ...game.fleas, ...game.scorpions, game.centipede, ...game.explosions]; +game.getBulletCollidableObjects = () => [...game.mushrooms, ...game.spiders, ...game.fleas, ...game.scorpions, game.centipede]; +game.getMushroomCollidableObjects = () => [game.player, ...game.scorpions, game.centipede]; +game.getPlayerCollidableObjects = () => [...game.spiders, ...game.fleas, ...game.scorpions, game.centipede]
\ No newline at end of file diff --git a/centipede/js/game/graphics.js b/centipede/js/game/graphics.js new file mode 100644 index 0000000..3a0d7f9 --- /dev/null +++ b/centipede/js/game/graphics.js @@ -0,0 +1,55 @@ +game.graphics = ( + (context) => { + context.imageSmoothingEnabled = false; + const clear = () => { + context.clearRect(0, 0, game.width, game.height); + }; + + const Sprite = ({sheetSrc, spriteX, spriteY, spriteWidth, spriteHeight, timePerFrame, cols, rows, numFrames, drawFunction}) => { + timePerFrame = timePerFrame ?? 100; + numFrames = numFrames ?? 1; + cols = cols ?? numFrames; + rows = rows ?? 1; + + let ready = false; + + let image; + if (sheetSrc) { + image = new Image(); + image.src = sheetSrc; + image.onload = () => { ready = true; }; + } + + let currentFrame = 0; + let lastTime = performance.now(); + + let draw; + if (!drawFunction) { + draw = (_elapsedTime, {x, y, rot, width, height}) => { + + if (ready) { + if (numFrames > 1) { + if (performance.now()-lastTime > timePerFrame) { + lastTime = performance.now(); + currentFrame = (currentFrame + 1) % numFrames; + } + } + context.save(); + context.translate(x+width/2, y+height/2); + context.rotate(rot * Math.PI / 180); + context.translate(-x-width/2, -y-height/2); + const row = currentFrame % rows; + const col = Math.floor(currentFrame / rows); + context.drawImage(image, spriteX+col*spriteWidth, spriteY+row*spriteHeight, spriteWidth, spriteHeight, x, y, width, height); + context.restore(); + } + }; + } else { + draw = (elapsedTime, drawSpec) => drawFunction(elapsedTime, drawSpec, context); + } + return { draw, timePerFrame, numFrames }; + } + + return { clear, Sprite }; + } +)(document.getElementById("game-canvas").getContext("2d"));
\ No newline at end of file diff --git a/centipede/js/game/input.js b/centipede/js/game/input.js new file mode 100644 index 0000000..6ceff13 --- /dev/null +++ b/centipede/js/game/input.js @@ -0,0 +1,32 @@ +game.input = (() => { + "use strict"; + const Keyboard = () => { + const keys = {}; + const handlers = {}; + const keyPress = (event) => { + keys[event.key] = event.timeStamp; + }; + const keyRelease = (event) => { + delete keys[event.key]; + }; + const registerCommand = (key, handler) => { + handlers[key] = handler; + }; + const unregisterCommand = (key) => { + delete handlers[key]; + } + const update = (elapsedTime) => { + for (let key in keys) { + if (keys.hasOwnProperty(key)) { + if (handlers[key]) { + handlers[key](elapsedTime); + } + } + } + }; + window.addEventListener("keydown", keyPress); + window.addEventListener("keyup", keyRelease); + return {keys, handlers, registerCommand, unregisterCommand, update}; + } + return { Keyboard }; +})();
\ No newline at end of file diff --git a/centipede/js/game/menu.js b/centipede/js/game/menu.js new file mode 100644 index 0000000..d4a7fe9 --- /dev/null +++ b/centipede/js/game/menu.js @@ -0,0 +1,143 @@ +const menu = {}; +menu.initialize = () => { + menu.scores = localStorage.getItem("scores") ? JSON.parse(localStorage.getItem("scores")) : []; + menu.state = "main"; + + menu.controls = localStorage.getItem("controls") ? JSON.parse(localStorage.getItem("controls")) : { + "moveUp": "ArrowUp", + "moveDown": "ArrowDown", + "moveLeft": "ArrowLeft", + "moveRight": "ArrowRight", + "shoot": " ", + }; +} + +menu.setState = (state) => { + menu.state = state; + menu.draw(); +} + +menu.escapeEventListener = (e) => { + if (e.key == "Escape") { + menu.setState('main'); + menu.draw(); + } +} + +menu.showMenu = () => { + menu.draw(); + game.stopped = true; + window.addEventListener("keydown", menu.escapeEventListener); +} + +menu.reRegisterKeys = () => { + Object.keys(game.keyboard.handlers).map(key => game.keyboard.unregisterCommand(key)); + game.keyboard.registerCommand(menu.controls.moveUp, game.player.moveUp); + game.keyboard.registerCommand(menu.controls.moveDown, game.player.moveDown); + game.keyboard.registerCommand(menu.controls.moveLeft, game.player.moveLeft); + game.keyboard.registerCommand(menu.controls.moveRight, game.player.moveRight); + game.keyboard.registerCommand(menu.controls.shoot, game.player.shoot); + game.keyboard.registerCommand("Escape", menu.showMenu); + localStorage.setItem("controls", JSON.stringify(menu.controls)); +} + +menu.addScore = (score) => { + menu.scores.push(score); + menu.scores.sort((a, b) => b - a); + localStorage.setItem("scores", JSON.stringify(menu.scores)); +} + +menu.hide = () => { + const menuElement = document.getElementById("menu"); + menuElement.style.display = "none"; + menu.reRegisterKeys(); + window.removeEventListener("keydown", menu.escapeEventListener); + if (menu.onHide) { + menu.onHide(); + menu.onHide = null; + } + game.resume(); +} + +menu.listenFor = (action, elementId) => { + const element = document.getElementById(elementId); + element.innerHTML = "Listening..."; + const handleKey = (event) => { + window.removeEventListener("keydown", handleKey); + if (event.key == "Escape") { + element.innerHTML = menu.controls[action]; + return; + } + menu.controls[action] = event.key; + element.innerHTML = event.key; + } + window.addEventListener("keydown", handleKey); +} + +menu.draw = () => { + const menuElement = document.getElementById("menu"); + menuElement.style.display = "block"; + menuElement.innerHTML = `<h1>Centipede</h1>`; + if (menu.state == "main") { + menuElement.innerHTML += ` + <div class='menu-button' onclick='menu.setState("controls")'>Change Controls</div> + <div class='menu-button' onclick='menu.setState("credits")'>Credits</div> + <div class='menu-button' onclick='menu.setState("scores")'>High Scores</div> + `; + } + else if (menu.state == "controls") { + menuElement.innerHTML += ` + <div> + <p> + Move left: <button id="moveLeft" onfocus='menu.listenFor("moveLeft", "moveLeft")'>${menu.controls.moveLeft}</button> + <br> + Move right: <button id="moveRight" onfocus='menu.listenFor("moveRight", "moveRight")'>${menu.controls.moveRight}</button> + <br> + Move up: <button id="moveUp" onfocus='menu.listenFor("moveUp", "moveUp")'>${menu.controls.moveUp}</button> + <br> + Move down: <button id="moveDown" onfocus='menu.listenFor("moveDown", "moveDown")'>${menu.controls.moveDown}</button> + <br> + Shoot: <button id="shoot" onfocus='menu.listenFor("shoot", "shoot")'>${menu.controls.shoot}</button> + </p> + </div> + ` + } else if (menu.state == "credits") { + menuElement.innerHTML += ` + <div> + <p> + Sounds from <a href="https://opengameart.org/content/laser-fire">dklon</a> + <br> + Sprites from <a href="https://www.pngkit.com/view/u2w7r5u2e6u2a9r5_general-sprites-centipede-arcade-game-sprites/">PNGKit</a> + <br> + Some code from <a href="https://www.usu.edu/directory/?person=56DB0BFCCAEECEC8D5">Dr. Mathias</a> + <br> + Developed by Logan Hunt + </p> + </div> + ` + } else if (menu.state == "scores") { + menuElement.innerHTML += ` + <div> + <p> + ${menu.scores.map((score, index) => `${index + 1}: ${score}<br>`).join("")} + </p> + </div> + ` + } else if (menu.state == "game-over") { + menuElement.innerHTML += ` + <div> + <p> + Game Over + <br> + Your final score was: ${game.score} + </div> + ` + } + + menuElement.innerHTML += "<div class='menu-button' onclick='menu.hide()'>Resume Game</div>" + if (menu.state !== "main") { + menuElement.innerHTML += "<div class='menu-button' onclick='menu.setState(\"main\")'>Back</div>" + } +} + +menu.initialize();
\ No newline at end of file diff --git a/centipede/js/game/object.js b/centipede/js/game/object.js new file mode 100644 index 0000000..1e22ec6 --- /dev/null +++ b/centipede/js/game/object.js @@ -0,0 +1,51 @@ +game.Object = (object) => { + object.dx = object.dx ?? 0; + object.dy = object.dy ?? 0; + object.rot = object.rot ?? 0; + object.drot = object.drot ?? 0; + object.alive = object.alive ?? true; + + object.poisonedTimer = object.poisonedTimer ?? 4000; + object.poisoned = false; + object.elapsedPoisonedTimer = 0; + object.poison = () => { + object.poisoned = true; + object.elapsedPoisonedTimer = 0; + } + + object.intersects = (other) => { + if (object.x + object.width <= other.x) { + return false; + } + if (object.x >= other.x + other.width) { + return false; + } + if (object.y + object.height <= other.y) { + return false; + } + if (object.y >= other.y + other.height) { + return false; + } + return true; + } + + object.update = (elapsedTime) => { + if (object.poisoned && object.y >= game.height - object.height) { + object.elapsedPoisonedTimer += elapsedTime; + if (object.elapsedPoisonedTimer > object.poisonedTimer) { + object.poisoned = false; + object.elapsedPoisonedTimer = 0; + } + } + + object.x += (object.poisoned ? 0 : object.dx)*elapsedTime; + object.y += (object.poisoned ? 0.2 : object.dy)*elapsedTime; + object.rot += object.drot*elapsedTime; + }; + + object.draw = (elapsedTime) => { + object.sprite.draw(elapsedTime, object); + }; + + return object; +}; diff --git a/centipede/js/game/objects/bullet.js b/centipede/js/game/objects/bullet.js new file mode 100644 index 0000000..5791aca --- /dev/null +++ b/centipede/js/game/objects/bullet.js @@ -0,0 +1,11 @@ +game.Bullet = (spec) => { + const object = game.Object(spec); + const parentUpdate = object.update; + object.update = (elapsedTime) => { + parentUpdate(elapsedTime); + if (object.y < 0) { + object.alive = false; + } + }; + return object; +}
\ No newline at end of file diff --git a/centipede/js/game/objects/centipede.js b/centipede/js/game/objects/centipede.js new file mode 100644 index 0000000..7e4c8f6 --- /dev/null +++ b/centipede/js/game/objects/centipede.js @@ -0,0 +1,105 @@ +game.CentipedePiece = (spec) => { + const object = game.Object(spec); + object.poisonedTimer = 1000; + const parentUpdate = object.update; + object.turningState = { + turning: false, + turnDirectionY: 1, + objectStateBeforeTurn: null, + }; + object.turn = () => { + object.turningState.objectStateBeforeTurn = {dx: object.dx, dy: object.dy, x: object.x, y: object.y}; + object.turningState.turning = true; + if (object.y >= game.height - object.height) { + object.turningState.turnDirectionY = -1; + } + if (object.y <= 0) { + object.turningState.turnDirectionY = 1; + } + }; + object.update = (elapsedTime) => { + if (object.poisoned) { + object.turningState.turning = false; + } + else if ((object.x+object.width > game.width || object.x < 0) && !object.turningState.turning) { + object.x = Math.min(Math.max(object.x, 0), game.width - object.width); + object.turn(); + } + if (object.turningState.turning) { + object.dx = 0; + object.dy = Math.abs(object.turningState.objectStateBeforeTurn.dx) * object.turningState.turnDirectionY; + object.rot = object.dy > 0 ? -90 : 90; + if (Math.abs(object.turningState.objectStateBeforeTurn.y - object.y) >= object.height) { + object.y = object.turningState.objectStateBeforeTurn.y + object.height * object.turningState.turnDirectionY; + object.dx = -object.turningState.objectStateBeforeTurn.dx; + object.rot = object.dx > 0 ? 180 : 0; + object.dy = 0; + object.turningState.turning = false; + } + } + parentUpdate(elapsedTime); + object.y = Math.min(Math.max(object.y, 0), game.height - object.height); + }; + object.onMushroomCollision = (mushroom) => { + if (mushroom.poisoned && object.dy === 0) { + object.poison(); + return; + } + if (!object.turningState.turning) { + if (mushroom.x < object.x && object.dx > 0) { + return; + } + object.turn(); + } + } + return object; +} + +game.Centipede = (spec) => { + const segments = [ + ...Array(spec.segments).fill(0).map((_, i) => game.CentipedePiece({...spec, x: spec.startX - spec.width*(i+1), y: spec.startY, sprite: game.sprites.centipedeBody})), + game.CentipedePiece({...spec, x: spec.startX, y: spec.startY, sprite: game.sprites.centipedeHead}), + ]; + + const update = (elapsedTime) => { + segments.map((segment) => segment.update(elapsedTime)); + } + + const draw = (elapsedTime) => { + segments.map((segment) => segment.draw(elapsedTime)); + } + + const intersects = (object) => { + return segments.filter((segment) => segment.intersects(object)).length; + } + + const onBulletCollision = (bullet) => { + if (bullet.alive) { + const segment = segments.find((segment) => segment.intersects(bullet)); + const segmentIndex = segments.indexOf(segment); + + const {mushX, mushY} = game.Mushroom.toMushCoords(segment); + game.explosions.push(game.Explosion({...game.Mushroom.toGameCoords({mushX, mushY}), width: segment.width, height: segment.height, sprite: game.sprites.explosionSmall})); + if (!game.mushrooms.find((mushroom) => mushroom.mushX === mushX && mushroom.mushY === mushY)) { + game.mushrooms.push(game.Mushroom({mushX, mushY, ...game.mushroomDims})); + } + game.score += segment.sprite === game.sprites.centipedeHead ? 20 : 5; + game.sounds.enemy_hit.load(); + game.sounds.enemy_hit.play(); + segments.splice(segmentIndex, 1); + } + bullet.alive = false; + } + + const onMushroomCollision = (mushroom) => { + segments.find((segment) => segment.intersects(mushroom)).onMushroomCollision(mushroom); + } + + const onPlayerCollision = (player) => { + player.kill(); + } + + const alive = () => segments.length ? true : false; + + return {update, draw, segments, intersects, onBulletCollision, onMushroomCollision, onPlayerCollision, alive}; +}
\ No newline at end of file diff --git a/centipede/js/game/objects/explosion.js b/centipede/js/game/objects/explosion.js new file mode 100644 index 0000000..f38d820 --- /dev/null +++ b/centipede/js/game/objects/explosion.js @@ -0,0 +1,14 @@ +game.Explosion = (spec) => { + const object = game.Object(spec); + let explosionTime = 0; + const parentUpdate = object.update; + object.update = (elapsedTime) => { + parentUpdate(elapsedTime); + explosionTime += elapsedTime; + + if (explosionTime > (object.sprite.numFrames * object.sprite.timePerFrame)) { + object.alive = false; + } + } + return object; +}
\ No newline at end of file diff --git a/centipede/js/game/objects/flea.js b/centipede/js/game/objects/flea.js new file mode 100644 index 0000000..23479cc --- /dev/null +++ b/centipede/js/game/objects/flea.js @@ -0,0 +1,49 @@ +game.Flea = (spec) => { + const object = game.Object(spec); + const parentUpdate = object.update; + object.mushroomCoords = game.Mushroom.toMushCoords(object); + object.update = (elapsedTime) => { + const newMushroomCoords = game.Mushroom.toMushCoords(object); + if (newMushroomCoords.mushY !== object.mushroomCoords.mushY || newMushroomCoords.mushX !== object.mushroomCoords.mushX) { + if (Math.random() < Math.min(0.15 + 0.05*game.level, 0.7)) { + if (!game.mushrooms.find((mushroom) => mushroom.mushX === newMushroomCoords.mushX && mushroom.mushY === newMushroomCoords.mushY)) { + game.mushrooms.push(game.Mushroom({...newMushroomCoords, ...game.mushroomDims})); + } + } + object.mushroomCoords = newMushroomCoords; + } + parentUpdate(elapsedTime); + }; + + object.onMushroomCollision = (mushroom) => { + if (mushroom.poisoned) { + mushroom.state = 0; + object.poison(); + } + } + + object.explode = () => { + game.explosions.push(game.Explosion({x: object.x, y: object.y, width: object.width, height: object.height, sprite: game.sprites.explosionSmall})); + game.sounds.enemy_hit.load(); + game.sounds.enemy_hit.play(); + } + + object.onBulletCollision = (bullet) => { + game.score += 20; + object.alive = false; + object.explode(); + } + + object.onPlayerCollision = (player) => { + object.alive = false; + player.kill(); + object.explode(); + } + + object.onBulletCollision = (bullet) => { + object.explode(); + object.alive = false; + } + + return object; +}
\ No newline at end of file diff --git a/centipede/js/game/objects/mushroom.js b/centipede/js/game/objects/mushroom.js new file mode 100644 index 0000000..b1f4787 --- /dev/null +++ b/centipede/js/game/objects/mushroom.js @@ -0,0 +1,43 @@ +game.Mushroom = (spec) => { + spec.state = spec.state ?? 4; + const {mushX, mushY} = spec; + const objectSpec = {...spec}; + objectSpec.x = mushX * objectSpec.width; + objectSpec.y = mushY * objectSpec.height; + const object = {...spec, ...game.Object(objectSpec)}; + object.onBulletCollision = (bullet) => { + if (bullet.alive) { + object.state--; + game.score += 5; + game.sounds.mushroom_hit.load(); + game.sounds.mushroom_hit.play(); + } + bullet.alive = false; + }; + object.draw = (elapsedTime) => { + if (object.state) { + object.sprite = object.poisoned ? game.sprites.poisonMushrooms[object.state-1] : game.sprites.regularMushrooms[object.state-1]; + object.sprite.draw(elapsedTime, object); + } + } + return object; +}; + +game.Mushroom.toMushCoords = (coords) => { + return {mushX: Math.ceil(coords.x / game.mushroomDims.width), mushY: Math.ceil(coords.y / game.mushroomDims.height)}; +} + +game.Mushroom.toGameCoords = (mushCoords) => { + return {x: mushCoords.mushX * game.mushroomDims.width, y: mushCoords.mushY * game.mushroomDims.height}; +} + +game.Mushroom.generateMushrooms = (mushroomSpec) => { + const mushPositions = new Set(); + for (let i = 0; i < Math.max(Math.random(), 0.05) * game.height / mushroomSpec.height * game.width / mushroomSpec.width * game.level * 0.5; i++) { + mushPositions.add(JSON.stringify([Math.floor(Math.random() * game.width / mushroomSpec.width), Math.max(1, Math.floor(Math.random() * (game.height / mushroomSpec.height - 3)))])); + } + return Array.from(mushPositions).map((pos) => { + const [mushX, mushY] = JSON.parse(pos); + return game.Mushroom({...mushroomSpec, mushX, mushY}); + }); +} diff --git a/centipede/js/game/objects/player.js b/centipede/js/game/objects/player.js new file mode 100644 index 0000000..a1f6ea0 --- /dev/null +++ b/centipede/js/game/objects/player.js @@ -0,0 +1,63 @@ +game.Player = (spec) => { + const object = game.Object(spec); + object.poisonedTimer = 4000; + object.elapsedPoisonedTimer = 0; + object.poisoned = false; + object.bulletTimer = spec.bulletTimer ?? 150; + object.maxPlayerHeight = spec.maxPlayerHeight ?? game.height - object.height*6; + object.elapsedBulletTimer = 0; + object.lives = spec.lives ?? 3; + + const parentUpdate = object.update; + object.update = (elapsedTime) => { + parentUpdate(elapsedTime); + object.x = Math.max(0, Math.min(object.x, game.width - object.width)); + object.y = Math.max(object.maxPlayerHeight, Math.min(object.y, game.height - object.height)); + object.dx = object.dy = 0; + object.elapsedBulletTimer += elapsedTime; + }; + object.moveUp = () => { + object.dy = -0.75; + } + object.moveDown = () => { + object.dy = 0.75; + } + object.moveLeft = () => { + object.dx = -0.5; + } + object.moveRight = () => { + object.dx = 0.5; + } + + object.shoot = () => { + if (object.elapsedBulletTimer > object.bulletTimer) { + object.elapsedBulletTimer = 0; + game.bullets.push(game.Bullet({x: object.x + object.width/2 - 5, y: object.y-object.height/2, dx: 0, dy: -1.5, width: 5, height: 50, sprite: game.sprites.bullet})); + game.sounds.laser.load(); + game.sounds.laser.play(); + } + } + + object.onMushroomCollision = (mushroom) => { + if (mushroom.poisoned) { + mushroom.state = 0; + object.poison(); + } + if (mushroom.x > object.x) { + object.x = mushroom.x - mushroom.width; + } + if (mushroom.x < object.x) { + object.x = mushroom.x + mushroom.width; + } + } + + object.kill = () => { + object.lives--; + game.resetObjects(); + if (object.lives == 0) { + game.gameOver(); + } + } + + return object; +}
\ No newline at end of file diff --git a/centipede/js/game/objects/scorpion.js b/centipede/js/game/objects/scorpion.js new file mode 100644 index 0000000..036db14 --- /dev/null +++ b/centipede/js/game/objects/scorpion.js @@ -0,0 +1,32 @@ +game.Scorpion = (spec) => { + const object = game.Object(spec); + + const parentUpdate = object.update; + object.update = (elapsedTime) => { + parentUpdate(elapsedTime); + }; + + object.explode = () => { + game.explosions.push(game.Explosion({x: object.x, y: object.y, width: object.width, height: object.height, sprite: game.sprites.explosionBig})); + game.sounds.enemy_hit.load(); + game.sounds.enemy_hit.play(); + } + + object.onBulletCollision = (bullet) => { + game.score += 100; + object.alive = false; + object.explode(); + } + + object.onPlayerCollision = (player) => { + object.alive = false; + player.kill(); + object.explode(); + } + + object.onMushroomCollision = (mushroom) => { + mushroom.poisoned = true; + } + + return object; +}
\ No newline at end of file diff --git a/centipede/js/game/objects/spider.js b/centipede/js/game/objects/spider.js new file mode 100644 index 0000000..732c0a3 --- /dev/null +++ b/centipede/js/game/objects/spider.js @@ -0,0 +1,45 @@ +game.Spider = (spec) => { + const object = game.Object(spec); + + const parentUpdate = object.update; + + object.randomizeVel = () => { + object.dx = Math.min(Math.random(), 0.25 + 0.05*game.level) * (Math.random() > 0.5 ? 1 : -1); + object.dy = Math.min(Math.random(), 0.25 + 0.05*game.level) * (Math.random() > 0.5 ? 1 : -1); + } + + object.update = (elapsedTime) => { + if (Math.random() < 0.01*game.level) { + object.randomizeVel(); + } + if (object.x < 0 || object.x > game.width - object.width) { + object.dx = -object.dx; + } + if (object.y < 0 || object.y > game.height - object.height) { + object.dy = -object.dy; + } + object.x = Math.max(0, Math.min(game.width - object.width, object.x)); + object.y = Math.max(0, Math.min(game.height - object.height, object.y)); + parentUpdate(elapsedTime); + }; + + object.explode = () => { + game.explosions.push(game.Explosion({x: object.x, y: object.y, width: object.width, height: object.height, sprite: game.sprites.explosionBig})); + game.sounds.enemy_hit.load(); + game.sounds.enemy_hit.play(); + } + + object.onBulletCollision = (bullet) => { + game.score += 150; + object.alive = false; + object.explode(); + } + + object.onPlayerCollision = (player) => { + object.alive = false; + player.kill(); + object.explode(); + } + + return object; +}
\ No newline at end of file diff --git a/centipede/js/game/sounds.js b/centipede/js/game/sounds.js new file mode 100644 index 0000000..584dbd3 --- /dev/null +++ b/centipede/js/game/sounds.js @@ -0,0 +1,5 @@ +game.sounds = { + mushroom_hit: new Audio("assets/sounds/mushroom_hit.wav"), + enemy_hit: new Audio("assets/sounds/enemy_hit.wav"), + laser: new Audio("assets/sounds/laser.wav"), +}
\ No newline at end of file diff --git a/centipede/js/game/sprites.js b/centipede/js/game/sprites.js new file mode 100644 index 0000000..3cdd77f --- /dev/null +++ b/centipede/js/game/sprites.js @@ -0,0 +1,120 @@ +game.sprites = { + centipedeHead: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 0, + spriteY: 0, + spriteWidth: 40, + spriteHeight: 40, + numFrames: 4, + timePerFrame: 100, + }), + centipedeBody: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 0, + spriteY: 80, + spriteWidth: 40, + spriteHeight: 40, + numFrames: 4, + timePerFrame: 100, + }), + spider: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 0, + spriteY: 160, + spriteWidth: 80, + spriteHeight: 40, + numFrames: 8, + timePerFrame: 100, + cols: 4, + rows: 2, + }), + flea: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 320, + spriteY: 160, + spriteWidth: 45, + spriteHeight: 40, + numFrames: 4, + timePerFrame: 500, + cols: 2, + rows: 2, + }), + scorpion: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 0, + spriteY: 280, + spriteWidth: 80, + spriteHeight: 40, + numFrames: 4, + timePerFrame: 500, + cols: 4, + }), + ship: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 0, + spriteY: 400, + spriteWidth: 40, + spriteHeight: 40, + numFrames: 1, + timePerFrame: 0, + cols: 1, + rows: 1 + }), + bullet: game.graphics.Sprite({ + drawFunction: (_elapsedTime, {x, y, rot, width, height}, context) => { + context.save(); + context.translate(x+width/2, y+height/2); + context.rotate(rot * Math.PI / 180); + context.translate(-x-width/2, -y-height/2); + const fillStyle = context.fillStyle; + context.fillStyle = "#FF0000"; + context.fillRect(x, y, width, height); + context.fillStyle = fillStyle; + context.restore(); + } + }), + explosionBig: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 0, + spriteY: 320, + numFrames: 8, + spriteWidth: 80, + spriteHeight: 40, + cols: 4, + rows: 2, + timePerFrame: 30, + }), + explosionSmall: game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 320, + spriteY: 320, + numFrames: 6, + spriteWidth: 40, + spriteHeight: 40, + cols: 3, + rows: 2, + timePerFrame: 30, + }), + regularMushrooms: [3,2,1,0].map(i => + game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 320 + i*40, + spriteY: 0, + numFrames: 1, + spriteWidth: 40, + spriteHeight: 40, + timePerFrame: 0, + }) + ), + poisonMushrooms: [3,2,1,0].map(i => + game.graphics.Sprite({ + sheetSrc: "assets/images/centipede-assets.png", + spriteX: 320 + i*40, + spriteY: 40, + numFrames: 1, + spriteWidth: 40, + spriteHeight: 40, + timePerFrame: 0, + }) + ) +};
\ No newline at end of file diff --git a/centipede/js/main.js b/centipede/js/main.js new file mode 100644 index 0000000..f4c954d --- /dev/null +++ b/centipede/js/main.js @@ -0,0 +1,75 @@ +let handleInput; +const initialize = () => { + game.level = 1; + game.score = 0; + game.totalTime = 0; + + game.keyboard = game.input.Keyboard(); + handleInput = game.keyboard.update; + + game.lastTimeStamp = performance.now(); + + game.player = game.Player({x: game.width/2 - 20, y: game.height-40, width: 40, height: 40, sprite: game.sprites.ship}); + + game.resetObjects(); +}; + +const update = (elapsedTime) => { + game.totalTime += elapsedTime; + + game.mushrooms.map((mushroom) => game.getMushroomCollidableObjects().filter((object) => object.intersects(mushroom))).map((objects, i) => { + objects.map((object => object.onMushroomCollision ? object.onMushroomCollision(game.mushrooms[i]) : null)); + }); + game.bullets.map((bullet) => game.getBulletCollidableObjects().filter((object) => object.intersects(bullet))).map((objects, i) => { + objects.map((object) => object.onBulletCollision ? object.onBulletCollision(game.bullets[i]) : null) + }) + game.getPlayerCollidableObjects().map((object) => object.intersects(game.player) ? object.onPlayerCollision(game.player) : null); + + game.bullets = game.bullets.filter((bullet) => bullet.alive); + game.spiders = game.spiders.filter((spider) => spider.alive); + game.explosions = game.explosions.filter((explosion) => explosion.alive); + game.mushrooms = game.mushrooms.filter((mushroom) => mushroom.state > 0); + game.scorpions = game.scorpions.filter((scorpion) => scorpion.x >= 0 && scorpion.x <= game.width - scorpion.width && scorpion.alive); + game.fleas = game.fleas.filter((flea) => flea.y < game.height - flea.height && flea.alive); + game.getObjects().map((object) => object.update(elapsedTime)); + + if (Math.random() < 0.002 * game.level && game.spiders.length < game.level) { + game.spiders.push(game.Spider({x: game.width/2, y: 0, width: 80, height: 40, rot: 0, dx: -0.2, dy: 0, sprite: game.sprites.spider})); + } + if (Math.random() < 0.001 * game.level && game.fleas.length < game.level) { + game.fleas.push(game.Flea({x: Math.floor(Math.random() * game.width / game.mushroomDims.width) * game.mushroomDims.width, y: 0, width: 40, height: 40, rot: 0, dx: 0, dy: 0.2, sprite: game.sprites.flea})); + } + if (Math.random() < 0.001 * game.level && game.scorpions.length < game.level) { + game.scorpions.push(game.Scorpion({y: Math.floor(Math.random() * game.height / game.mushroomDims.height) * game.mushroomDims.height, x: 0, width: 80, height: 40, rot: 0, dx: 0.2, dy: 0, sprite: game.sprites.scorpion})); + } + + if (!game.centipede.alive()) { + game.resetObjects(); + game.level++; + game.score += game.level*400; + } +}; + +const render = (elapsedTime) => { + game.graphics.clear(); + game.getObjects().map((object) => object.draw(elapsedTime)); + + document.getElementById("hud").innerHTML = `Level: ${game.level} Lives: ${game.player.lives} Score: ${game.score} ${game.player.poisoned ? "<span style='color:green'>POISONED<span>" : ""}`; +}; + +const gameLoop = (time) => { + const elapsedTime = time - game.lastTimeStamp; + game.lastTimeStamp = time; + + handleInput(elapsedTime); + update(elapsedTime); + render(elapsedTime); + + if (!game.stopped) { + requestAnimationFrame(gameLoop); + } +}; + +initialize(); +menu.reRegisterKeys(); +requestAnimationFrame(gameLoop);
\ No newline at end of file |