diff --git a/.gitignore b/.gitignore index 7ad9698..620adeb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Generated files src/frontend/styles.css +#DS_Store files +*.DS_Store + # Logs logs *.log diff --git a/src/frontend/app.js b/src/frontend/app.js index 1f7f94a..e5d3291 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -1,5 +1,5 @@ import Game from './utilities/Game.js'; -import Player from './utilities/Player.js'; +import { PacMan, Blinky, Clyde, Inky, Pinky } from './utilities/Players/index.js'; const foregroundCanvas = document.getElementById('foreground-layer'); const playerCanvas = document.getElementById('player-layer'); @@ -7,13 +7,62 @@ const playerCanvas = document.getElementById('player-layer'); const movementKeys = ['w', 'a', 's', 'd', 'ArrowUp', 'ArrowLeft', 'ArrowDown', 'ArrowRight']; const game = new Game(foregroundCanvas, playerCanvas); -const player = new Player(); +const player = new PacMan(); +const blinky = new Blinky(); +const clyde = new Clyde(); +const inky = new Inky(); +const pinky = new Pinky(); await game.loadGameBoard('./assets/map.json'); // load the gameboard/map from json file game.addPlayer(player); +game.addPlayer(blinky); +game.addPlayer(clyde); +game.addPlayer(inky); +game.addPlayer(pinky); game.start(); +// TODO: remove this +// This is temporary to show many players moving at once. +// AI logic will be implemented in player classes +setInterval(() => { + if (Math.floor(Math.random() * 2)) { + if (Math.floor(Math.random() * 2)) { + blinky.setMovement({ x: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + else { + blinky.setMovement({ y: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + } + + if (Math.floor(Math.random() * 2)) { + if (Math.floor(Math.random() * 2)) { + clyde.setMovement({ x: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + else { + clyde.setMovement({ y: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + } + + if (Math.floor(Math.random() * 2)) { + if (Math.floor(Math.random() * 2)) { + inky.setMovement({ x: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + else { + inky.setMovement({ y: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + } + + if (Math.floor(Math.random() * 2)) { + if (Math.floor(Math.random() * 2)) { + pinky.setMovement({ x: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + else { + pinky.setMovement({ y: Math.floor(Math.random() * 2) ? -1 : 1 }); + } + } +}, 200); + document.addEventListener('keydown', (event) => { if (movementKeys.includes(event.key)) { event.preventDefault(); diff --git a/src/frontend/assets/map-hd.png b/src/frontend/assets/map-hd.png new file mode 100644 index 0000000..bc98b50 Binary files /dev/null and b/src/frontend/assets/map-hd.png differ diff --git a/src/frontend/assets/map.json b/src/frontend/assets/map.json index 4f210d6..f4d8429 100644 --- a/src/frontend/assets/map.json +++ b/src/frontend/assets/map.json @@ -562,5 +562,1047 @@ "y": 656 } ] - ] + ], + "lairPaths": [ + [ + { + "x": 376, + "y": 464 + }, + { + "x": 412, + "y": 464 + } + ], + [ + { + "x": 412, + "y": 464 + }, + { + "x": 448, + "y": 464 + } + ], + [ + { + "x": 448, + "y": 368 + }, + { + "x": 448, + "y": 464 + } + ], + [ + { + "x": 448, + "y": 464 + }, + { + "x": 484, + "y": 464 + } + ], + [ + { + "x": 484, + "y": 464 + }, + { + "x": 520, + "y": 464 + } + ] + ], + "items": { + "dots": [ + { + "x": 48, + "y": 48 + }, + { + "x": 80, + "y": 48 + }, + { + "x": 112, + "y": 48 + }, + { + "x": 144, + "y": 48 + }, + { + "x": 176, + "y": 48 + }, + { + "x": 208, + "y": 48 + }, + { + "x": 240, + "y": 48 + }, + { + "x": 272, + "y": 48 + }, + { + "x": 304, + "y": 48 + }, + { + "x": 336, + "y": 48 + }, + { + "x": 368, + "y": 48 + }, + { + "x": 400, + "y": 48 + }, + { + "x": 496, + "y": 48 + }, + { + "x": 528, + "y": 48 + }, + { + "x": 560, + "y": 48 + }, + { + "x": 592, + "y": 48 + }, + { + "x": 624, + "y": 48 + }, + { + "x": 656, + "y": 48 + }, + { + "x": 688, + "y": 48 + }, + { + "x": 720, + "y": 48 + }, + { + "x": 752, + "y": 48 + }, + { + "x": 784, + "y": 48 + }, + { + "x": 816, + "y": 48 + }, + { + "x": 848, + "y": 48 + }, + { + "x": 48, + "y": 80 + }, + { + "x": 208, + "y": 80 + }, + { + "x": 400, + "y": 80 + }, + { + "x": 496, + "y": 80 + }, + { + "x": 688, + "y": 80 + }, + { + "x": 848, + "y": 80 + }, + { + "x": 208, + "y": 112 + }, + { + "x": 400, + "y": 112 + }, + { + "x": 496, + "y": 112 + }, + { + "x": 688, + "y": 112 + }, + { + "x": 48, + "y": 144 + }, + { + "x": 208, + "y": 144 + }, + { + "x": 400, + "y": 144 + }, + { + "x": 496, + "y": 144 + }, + { + "x": 688, + "y": 144 + }, + { + "x": 848, + "y": 144 + }, + { + "x": 48, + "y": 176 + }, + { + "x": 80, + "y": 176 + }, + { + "x": 112, + "y": 176 + }, + { + "x": 144, + "y": 176 + }, + { + "x": 176, + "y": 176 + }, + { + "x": 208, + "y": 176 + }, + { + "x": 240, + "y": 176 + }, + { + "x": 272, + "y": 176 + }, + { + "x": 304, + "y": 176 + }, + { + "x": 336, + "y": 176 + }, + { + "x": 368, + "y": 176 + }, + { + "x": 400, + "y": 176 + }, + { + "x": 432, + "y": 176 + }, + { + "x": 464, + "y": 176 + }, + { + "x": 496, + "y": 176 + }, + { + "x": 528, + "y": 176 + }, + { + "x": 560, + "y": 176 + }, + { + "x": 592, + "y": 176 + }, + { + "x": 624, + "y": 176 + }, + { + "x": 656, + "y": 176 + }, + { + "x": 688, + "y": 176 + }, + { + "x": 720, + "y": 176 + }, + { + "x": 752, + "y": 176 + }, + { + "x": 784, + "y": 176 + }, + { + "x": 816, + "y": 176 + }, + { + "x": 848, + "y": 176 + }, + { + "x": 48, + "y": 208 + }, + { + "x": 208, + "y": 208 + }, + { + "x": 304, + "y": 208 + }, + { + "x": 592, + "y": 208 + }, + { + "x": 688, + "y": 208 + }, + { + "x": 848, + "y": 208 + }, + { + "x": 48, + "y": 240 + }, + { + "x": 208, + "y": 240 + }, + { + "x": 304, + "y": 240 + }, + { + "x": 592, + "y": 240 + }, + { + "x": 688, + "y": 240 + }, + { + "x": 848, + "y": 240 + }, + { + "x": 48, + "y": 272 + }, + { + "x": 80, + "y": 272 + }, + { + "x": 112, + "y": 272 + }, + { + "x": 144, + "y": 272 + }, + { + "x": 176, + "y": 272 + }, + { + "x": 208, + "y": 272 + }, + { + "x": 304, + "y": 272 + }, + { + "x": 336, + "y": 272 + }, + { + "x": 368, + "y": 272 + }, + { + "x": 400, + "y": 272 + }, + { + "x": 496, + "y": 272 + }, + { + "x": 528, + "y": 272 + }, + { + "x": 560, + "y": 272 + }, + { + "x": 592, + "y": 272 + }, + { + "x": 688, + "y": 272 + }, + { + "x": 720, + "y": 272 + }, + { + "x": 752, + "y": 272 + }, + { + "x": 784, + "y": 272 + }, + { + "x": 816, + "y": 272 + }, + { + "x": 848, + "y": 272 + }, + { + "x": 208, + "y": 304 + }, + { + "x": 688, + "y": 304 + }, + { + "x": 208, + "y": 336 + }, + { + "x": 688, + "y": 336 + }, + { + "x": 208, + "y": 368 + }, + { + "x": 688, + "y": 368 + }, + { + "x": 208, + "y": 400 + }, + { + "x": 688, + "y": 400 + }, + { + "x": 208, + "y": 432 + }, + { + "x": 688, + "y": 432 + }, + { + "x": 208, + "y": 464 + }, + { + "x": 688, + "y": 464 + }, + { + "x": 208, + "y": 496 + }, + { + "x": 688, + "y": 496 + }, + { + "x": 208, + "y": 528 + }, + { + "x": 688, + "y": 528 + }, + { + "x": 208, + "y": 560 + }, + { + "x": 688, + "y": 560 + }, + { + "x": 208, + "y": 592 + }, + { + "x": 688, + "y": 592 + }, + { + "x": 208, + "y": 624 + }, + { + "x": 688, + "y": 624 + }, + { + "x": 48, + "y": 656 + }, + { + "x": 80, + "y": 656 + }, + { + "x": 112, + "y": 656 + }, + { + "x": 144, + "y": 656 + }, + { + "x": 176, + "y": 656 + }, + { + "x": 208, + "y": 656 + }, + { + "x": 240, + "y": 656 + }, + { + "x": 272, + "y": 656 + }, + { + "x": 304, + "y": 656 + }, + { + "x": 336, + "y": 656 + }, + { + "x": 368, + "y": 656 + }, + { + "x": 400, + "y": 656 + }, + { + "x": 496, + "y": 656 + }, + { + "x": 528, + "y": 656 + }, + { + "x": 560, + "y": 656 + }, + { + "x": 592, + "y": 656 + }, + { + "x": 624, + "y": 656 + }, + { + "x": 656, + "y": 656 + }, + { + "x": 688, + "y": 656 + }, + { + "x": 720, + "y": 656 + }, + { + "x": 752, + "y": 656 + }, + { + "x": 784, + "y": 656 + }, + { + "x": 816, + "y": 656 + }, + { + "x": 848, + "y": 656 + }, + { + "x": 48, + "y": 688 + }, + { + "x": 208, + "y": 688 + }, + { + "x": 400, + "y": 688 + }, + { + "x": 496, + "y": 688 + }, + { + "x": 688, + "y": 688 + }, + { + "x": 848, + "y": 688 + }, + { + "x": 48, + "y": 720 + }, + { + "x": 208, + "y": 720 + }, + { + "x": 400, + "y": 720 + }, + { + "x": 496, + "y": 720 + }, + { + "x": 688, + "y": 720 + }, + { + "x": 848, + "y": 720 + }, + { + "x": 80, + "y": 752 + }, + { + "x": 112, + "y": 752 + }, + { + "x": 208, + "y": 752 + }, + { + "x": 240, + "y": 752 + }, + { + "x": 272, + "y": 752 + }, + { + "x": 304, + "y": 752 + }, + { + "x": 336, + "y": 752 + }, + { + "x": 368, + "y": 752 + }, + { + "x": 400, + "y": 752 + }, + { + "x": 432, + "y": 752 + }, + { + "x": 464, + "y": 752 + }, + { + "x": 496, + "y": 752 + }, + { + "x": 528, + "y": 752 + }, + { + "x": 560, + "y": 752 + }, + { + "x": 592, + "y": 752 + }, + { + "x": 624, + "y": 752 + }, + { + "x": 656, + "y": 752 + }, + { + "x": 688, + "y": 752 + }, + { + "x": 784, + "y": 752 + }, + { + "x": 816, + "y": 752 + }, + { + "x": 112, + "y": 784 + }, + { + "x": 208, + "y": 784 + }, + { + "x": 304, + "y": 784 + }, + { + "x": 592, + "y": 784 + }, + { + "x": 688, + "y": 784 + }, + { + "x": 784, + "y": 784 + }, + { + "x": 112, + "y": 816 + }, + { + "x": 208, + "y": 816 + }, + { + "x": 304, + "y": 816 + }, + { + "x": 592, + "y": 816 + }, + { + "x": 688, + "y": 816 + }, + { + "x": 784, + "y": 816 + }, + { + "x": 48, + "y": 848 + }, + { + "x": 80, + "y": 848 + }, + { + "x": 112, + "y": 848 + }, + { + "x": 144, + "y": 848 + }, + { + "x": 176, + "y": 848 + }, + { + "x": 208, + "y": 848 + }, + { + "x": 304, + "y": 848 + }, + { + "x": 336, + "y": 848 + }, + { + "x": 368, + "y": 848 + }, + { + "x": 400, + "y": 848 + }, + { + "x": 496, + "y": 848 + }, + { + "x": 528, + "y": 848 + }, + { + "x": 560, + "y": 848 + }, + { + "x": 592, + "y": 848 + }, + { + "x": 688, + "y": 848 + }, + { + "x": 720, + "y": 848 + }, + { + "x": 752, + "y": 848 + }, + { + "x": 784, + "y": 848 + }, + { + "x": 816, + "y": 848 + }, + { + "x": 848, + "y": 848 + }, + { + "x": 48, + "y": 880 + }, + { + "x": 400, + "y": 880 + }, + { + "x": 496, + "y": 880 + }, + { + "x": 848, + "y": 880 + }, + { + "x": 48, + "y": 912 + }, + { + "x": 400, + "y": 912 + }, + { + "x": 496, + "y": 912 + }, + { + "x": 848, + "y": 912 + }, + { + "x": 48, + "y": 944 + }, + { + "x": 80, + "y": 944 + }, + { + "x": 112, + "y": 944 + }, + { + "x": 144, + "y": 944 + }, + { + "x": 176, + "y": 944 + }, + { + "x": 208, + "y": 944 + }, + { + "x": 240, + "y": 944 + }, + { + "x": 272, + "y": 944 + }, + { + "x": 304, + "y": 944 + }, + { + "x": 336, + "y": 944 + }, + { + "x": 368, + "y": 944 + }, + { + "x": 400, + "y": 944 + }, + { + "x": 432, + "y": 944 + }, + { + "x": 464, + "y": 944 + }, + { + "x": 496, + "y": 944 + }, + { + "x": 528, + "y": 944 + }, + { + "x": 560, + "y": 944 + }, + { + "x": 592, + "y": 944 + }, + { + "x": 624, + "y": 944 + }, + { + "x": 656, + "y": 944 + }, + { + "x": 688, + "y": 944 + }, + { + "x": 720, + "y": 944 + }, + { + "x": 752, + "y": 944 + }, + { + "x": 784, + "y": 944 + }, + { + "x": 816, + "y": 944 + }, + { + "x": 848, + "y": 944 + } + ], + "powerPills": [ + { + "x": 48, + "y": 112 + }, + { + "x": 848, + "y": 112 + }, + { + "x": 48, + "y": 752 + }, + { + "x": 848, + "y": 752 + } + ] + } } diff --git a/src/frontend/index.html b/src/frontend/index.html index 464ff25..e2a367d 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -7,10 +7,10 @@ Multiplayer Pac-Man - +

Multiplayer Pacman

- Your browser does not support HTML5. + Your browser does not support HTML5. Your browser does not support HTML5. Your browser does not support HTML5.
diff --git a/src/frontend/utilities/Game.js b/src/frontend/utilities/Game.js index b8d4238..b6e2903 100644 --- a/src/frontend/utilities/Game.js +++ b/src/frontend/utilities/Game.js @@ -1,7 +1,11 @@ import Intersection from './Intersection.js'; +import { Dot, PowerPill } from './Items/index.js'; +import { Ghost, PacMan } from './Players/index.js'; import Path from './Path.js'; import Portal from './Portal.js'; +const POWER_UP_DURATION = 7500; + export default class Game { constructor(foregroundCanvas, playerCanvas) { this.foregroundCtx = foregroundCanvas.getContext('2d'); @@ -9,16 +13,16 @@ export default class Game { this.players = []; this.intersections = []; this.paths = []; + this.items = []; this.interval = undefined; + this.powerUpInterval = undefined; this.board = { width: foregroundCanvas.width, height: foregroundCanvas.height }; - // TODO: change this this.foregroundCtx.fillStyle = '#FFFFFF'; - this.playerCtx.fillStyle = '#FFFFFF'; } async loadGameBoard(path) { @@ -30,6 +34,7 @@ export default class Game { } this.#generatePaths(map); + this.#generateItems(map); // draw intersections (dev purposes only, will change) for (const intersection of this.intersections) { @@ -40,9 +45,14 @@ export default class Game { for (const path of this.paths) { path.draw(this.foregroundCtx); } + + // draw items + for (const item of this.items) { + item.draw(this.foregroundCtx); + } } - #generatePaths({ inaccessiblePaths, portals }) { + #generatePaths({ inaccessiblePaths, portals, lairPaths }) { for (const start of this.intersections) { for (const end of this.intersections) { if (start === end || start.position.x > end.position.x || start.position.y > end.position.y) continue; @@ -62,7 +72,7 @@ export default class Game { const isPortal = portals.find((intersections) => { const startPortal = (intersections[0].x === start.position.x && intersections[0].y === start.position.y); - let endPortal = (intersections[1].x === end.position.x && intersections[1].y === end.position.y); + const endPortal = (intersections[1].x === end.position.x && intersections[1].y === end.position.y); return startPortal && endPortal; }); @@ -79,8 +89,14 @@ export default class Game { return containsStart && containsEnd; }); + const isLairPath = lairPaths.find((intersections) => { + const startIntersection = (intersections[0].x === start.position.x && intersections[0].y === start.position.y); + const endIntersection = (intersections[1].x === end.position.x && intersections[1].y === end.position.y); + return startIntersection && endIntersection; + }); + if (!isInaccessiblePath) { - this.paths.push(new Path(start, end)); + this.paths.push(new Path(start, end, isLairPath)); } } @@ -91,12 +107,24 @@ export default class Game { } } + #generateItems({ items }) { + for (const position of items.dots) { + this.items.push(new Dot(position)); + } + + for (const position of items.powerPills) { + this.items.push(new PowerPill(position)); + } + } + addPlayer(player) { this.players.push(player); - // Spawn the player at the first safe path for (const path of this.paths) { - if (path.isSafe) { + const isMatchingStart = path.start.position.x === player.spawnPath[0].x && path.start.position.y === player.spawnPath[0].y; + const isMatchingEnd = path.end.position.x === player.spawnPath[1].x && path.end.position.y === player.spawnPath[1].y; + + if (isMatchingStart && isMatchingEnd) { player.spawn(path); } } @@ -121,11 +149,90 @@ export default class Game { update() { this.playerCtx.clearRect(0, 0, this.board.width, this.board.height); - // move and draw players + // move each player for (let i = 0; i < this.players.length; i++) { const player = this.players[i]; player.move(); + } + + // handle collisions + const { isRedrawingItems } = this.#collisionHandler(); + + // redraw items if necessary + if (isRedrawingItems) { + this.foregroundCtx.clearRect(0, 0, this.board.width, this.board.height); + for (const item of this.items) { + item.draw(this.foregroundCtx); + } + } + + // draw each player + for (const player of this.players) { player.draw(this.playerCtx); } } + + #collisionHandler() { + // TODO: handle collisions between players and players. + // NOTE: be sure to handle collisions between players before collisions for items + + let isRedrawingItems = false; + + for (const player of this.players) { + for (const item of this.items) { + if (player.position.x === item.position.x && player.position.y === item.position.y) { + const itemWasUsed = item.use(player); + + if (itemWasUsed) { + // remove this item from the game's items + this.items = this.items.filter(({ position }) => { + return !(position.x === item.position.x && position.y === item.position.y); + }); + + if (item instanceof PowerPill) { + this.#triggerPowerUp(); + } + + isRedrawingItems = true; + } + break; + } + } + } + + return { isRedrawingItems }; + } + + #triggerPowerUp() { + for (const player of this.players) { + if (player instanceof PacMan) { + player.isPoweredUp = true; + } + + if (player instanceof Ghost) { + player.isScared = true; + } + } + + // clear and reset the timeout if there is already one + if (this.powerUpInterval) { + clearTimeout(this.powerUpInterval); + this.powerUpInterval = undefined; + } + + this.powerUpInterval = setTimeout(() => { + // update each player's status property + for (const player of this.players) { + if (player instanceof PacMan) { + player.isPoweredUp = false; + } + + if (player instanceof Ghost) { + player.isScared = false; + } + } + + this.powerUpInterval = undefined; + }, POWER_UP_DURATION); + } } diff --git a/src/frontend/utilities/Items/Dot.js b/src/frontend/utilities/Items/Dot.js new file mode 100644 index 0000000..c03b87b --- /dev/null +++ b/src/frontend/utilities/Items/Dot.js @@ -0,0 +1,22 @@ +import Item from './Item.js'; +import { PacMan } from '../Players/index.js'; + +export default class Dot extends Item { + constructor(position) { + super({ + position, + size: 5, + points: 1 + }); + } + + use(player) { + if (player instanceof PacMan) { + player.incrementScore(this.points); + + return true; + } + + return false; + } +} diff --git a/src/frontend/utilities/Items/Item.js b/src/frontend/utilities/Items/Item.js new file mode 100644 index 0000000..4e308f8 --- /dev/null +++ b/src/frontend/utilities/Items/Item.js @@ -0,0 +1,18 @@ +export default class Item { + constructor({ position, points, lifespan, size }) { + this.position = position; + this.points = points; + this.lifespan = lifespan; + this.size = size; + } + + // override this function for more advanced items (i.e. fruits) + draw(ctx) { + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(this.position.x, this.position.y, this.size, 0, 2 * Math.PI); + ctx.fill(); + + ctx.stroke(); + } +} diff --git a/src/frontend/utilities/Items/PowerPill.js b/src/frontend/utilities/Items/PowerPill.js new file mode 100644 index 0000000..890cc83 --- /dev/null +++ b/src/frontend/utilities/Items/PowerPill.js @@ -0,0 +1,16 @@ +import Item from './Item.js'; +import { PacMan } from '../Players/index.js'; + +export default class PowerPill extends Item { + constructor(position) { + super({ + position, + size: 12, + points: 0 + }); + } + + use(player) { + return player instanceof PacMan; + } +} diff --git a/src/frontend/utilities/Items/index.js b/src/frontend/utilities/Items/index.js new file mode 100644 index 0000000..c475af9 --- /dev/null +++ b/src/frontend/utilities/Items/index.js @@ -0,0 +1,9 @@ +import Dot from './Dot.js'; +import Item from './Item.js'; +import PowerPill from './PowerPill.js'; + +export { + Dot, + Item, + PowerPill +}; diff --git a/src/frontend/utilities/Path.js b/src/frontend/utilities/Path.js index db62561..aa1bcbb 100644 --- a/src/frontend/utilities/Path.js +++ b/src/frontend/utilities/Path.js @@ -1,8 +1,9 @@ export default class Path { - constructor(start, end) { + constructor(start, end, isLair=false) { this.isSafe = true; this.start = start; this.end = end; + this.isLair = isLair; this.isHorizontal = this.start.position.y === this.end.position.y; this.isVertical = !this.isHorizontal; diff --git a/src/frontend/utilities/Players/Blinky.js b/src/frontend/utilities/Players/Blinky.js new file mode 100644 index 0000000..e40a0ae --- /dev/null +++ b/src/frontend/utilities/Players/Blinky.js @@ -0,0 +1,22 @@ +import Ghost from './Ghost.js'; + +export default class Blinky extends Ghost { + constructor() { + super(); + this.spawnPath = [ + { x: 376, y: 464 }, + { x: 412, y: 464 } + ]; + } + + draw(ctx) { + if (this.isScared) { + super.drawScared(ctx); + } + else { + // TODO: change to draw Blinky + ctx.fillStyle = '#FF0000'; + ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); + } + } +} diff --git a/src/frontend/utilities/Players/Clyde.js b/src/frontend/utilities/Players/Clyde.js new file mode 100644 index 0000000..cada3b7 --- /dev/null +++ b/src/frontend/utilities/Players/Clyde.js @@ -0,0 +1,22 @@ +import Ghost from './Ghost.js'; + +export default class Clyde extends Ghost { + constructor() { + super(); + this.spawnPath = [ + { x: 484, y: 464 }, + { x: 520, y: 464 } + ]; + } + + draw(ctx) { + if (this.isScared) { + super.drawScared(ctx); + } + else { + // TODO: change to draw Clyde + ctx.fillStyle = '#FFA500'; + ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); + } + } +} diff --git a/src/frontend/utilities/Players/Ghost.js b/src/frontend/utilities/Players/Ghost.js new file mode 100644 index 0000000..43edd50 --- /dev/null +++ b/src/frontend/utilities/Players/Ghost.js @@ -0,0 +1,15 @@ +import Player from './Player.js'; + +export default class Ghost extends Player { + constructor() { + super(); + this.isScared = false; + } + + // in subclasses (Clyde, Inky, Pinky, etc.) call `super.drawScared()` whenever the `isScared` is true + drawScared(ctx) { + // This is to be envoked when the ghost is scared + ctx.fillStyle = '#0000FF'; + ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); + } +} diff --git a/src/frontend/utilities/Players/Inky.js b/src/frontend/utilities/Players/Inky.js new file mode 100644 index 0000000..44ef116 --- /dev/null +++ b/src/frontend/utilities/Players/Inky.js @@ -0,0 +1,22 @@ +import Ghost from './Ghost.js'; + +export default class Inky extends Ghost { + constructor() { + super(); + this.spawnPath = [ + { x: 412, y: 464 }, + { x: 448, y: 464 } + ]; + } + + draw(ctx) { + if (this.isScared) { + super.drawScared(ctx); + } + else { + // TODO: change to draw Inky + ctx.fillStyle = '#ADD8E6'; + ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); + } + } +} diff --git a/src/frontend/utilities/Players/PacMan.js b/src/frontend/utilities/Players/PacMan.js new file mode 100644 index 0000000..1d709dc --- /dev/null +++ b/src/frontend/utilities/Players/PacMan.js @@ -0,0 +1,20 @@ +import Player from './Player.js'; + +export default class PacMan extends Player { + constructor() { + super(); + this.isPoweredUp = false; + + this.spawnPath = [ + { x: 304, y: 560 }, + { x: 592, y: 560 }, + ]; + } + + // TODO: override draw method + // when `isPoweredUp`, draw PacMan with teeth + draw(ctx) { + ctx.fillStyle = '#FFFF00'; + ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); + } +} diff --git a/src/frontend/utilities/Players/Pinky.js b/src/frontend/utilities/Players/Pinky.js new file mode 100644 index 0000000..175d9f8 --- /dev/null +++ b/src/frontend/utilities/Players/Pinky.js @@ -0,0 +1,22 @@ +import Ghost from './Ghost.js'; + +export default class Pinky extends Ghost { + constructor() { + super(); + this.spawnPath = [ + { x: 448, y: 464 }, + { x: 484, y: 464 } + ]; + } + + draw(ctx) { + if (this.isScared) { + super.drawScared(ctx); + } + else { + // TODO: change to draw Pinky + ctx.fillStyle = '#FFC0CB'; + ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); + } + } +} diff --git a/src/frontend/utilities/Player.js b/src/frontend/utilities/Players/Player.js similarity index 90% rename from src/frontend/utilities/Player.js rename to src/frontend/utilities/Players/Player.js index ef1bf40..3f1df00 100644 --- a/src/frontend/utilities/Player.js +++ b/src/frontend/utilities/Players/Player.js @@ -1,14 +1,16 @@ -import Path from './Path.js'; -import Intersection from './Intersection.js'; -import Portal from './Portal.js'; +import Path from '../Path.js'; +import Intersection from '../Intersection.js'; +import Portal from '../Portal.js'; export default class Player { constructor() { + // Find an alternative to 'id'. This is not random enough :( this.id = Math.floor(Math.random() * 100); - this.width = 20; - this.height = 20; + this.width = 30; + this.height = 30; this.isSpawned = false; this.movement = { x: 0, y: 0 }; + this.score = 0; } spawn(path) { @@ -106,6 +108,10 @@ export default class Player { this.movement = { x: 0, y: 0 }; } + incrementScore(points) { + this.score += points; + } + draw(ctx) { ctx.fillStyle = '#FFFFFF'; ctx.fillRect(this.position.x - (this.width / 2), this.position.y - (this.height / 2), this.width, this.height); diff --git a/src/frontend/utilities/Players/index.js b/src/frontend/utilities/Players/index.js new file mode 100644 index 0000000..89804d9 --- /dev/null +++ b/src/frontend/utilities/Players/index.js @@ -0,0 +1,17 @@ +import Blinky from './Blinky.js'; +import Clyde from './Clyde.js'; +import Ghost from './Ghost.js'; +import Inky from './Inky.js'; +import PacMan from './PacMan.js'; +import Pinky from './Pinky.js'; +import Player from './Player.js'; + +export { + Blinky, + Clyde, + Ghost, + Inky, + PacMan, + Pinky, + Player +}; diff --git a/test/frontend/utilities/Game.test.js b/test/frontend/utilities/Game.test.js index f6f12d0..6cc3c4c 100644 --- a/test/frontend/utilities/Game.test.js +++ b/test/frontend/utilities/Game.test.js @@ -1,5 +1,5 @@ import Game from '@/frontend/utilities/Game.js'; -import Player from '@/frontend/utilities/Player.js'; +import { Player } from '@/frontend/utilities/Players'; import Chance from 'chance'; const chance = new Chance(); diff --git a/test/frontend/utilities/Items/Dot.test.js b/test/frontend/utilities/Items/Dot.test.js new file mode 100644 index 0000000..94d6a05 --- /dev/null +++ b/test/frontend/utilities/Items/Dot.test.js @@ -0,0 +1,61 @@ +import { Dot } from '@/frontend/utilities/Items'; +import { PacMan, Player } from '@/frontend/utilities/Players'; +import Chance from 'chance'; + +const chance = new Chance(); + +describe('Dot', () => { + let dot, position; + + beforeEach(() => { + position = { + x: chance.integer(), + y: chance.integer() + }; + + dot = new Dot(position); + }); + + it('creates a dot correctly', () => { + expect(dot.position).toMatchObject(position); + expect(dot.points).toEqual(1); + expect(dot.lifespan).toBeUndefined(); + expect(dot.size).toEqual(5); + }); + + describe('use()', () => { + let player; + + describe('given the player is PacMan', () => { + beforeEach(() => { + player = new PacMan(); + }); + + it('returns truthy', () => { + const response = dot.use(player); + expect(response).toBeTruthy(); + }); + + it('increments the player score', () => { + dot.use(player); + expect(player.score).toEqual(1); + }); + }); + + describe('given the player is not PacMan', () => { + beforeEach(() => { + player = new Player(); + }); + + it('returns falsy', () => { + const response = dot.use(player); + expect(response).not.toBeTruthy(); + }); + + it('does not increment the player score', () => { + dot.use(player); + expect(player.score).toEqual(0); + }); + }); + }); +}); diff --git a/test/frontend/utilities/Items/Item.test.js b/test/frontend/utilities/Items/Item.test.js new file mode 100644 index 0000000..ef366e6 --- /dev/null +++ b/test/frontend/utilities/Items/Item.test.js @@ -0,0 +1,65 @@ +import { Item } from '@/frontend/utilities/Items'; +import Chance from 'chance'; + +const chance = new Chance(); + +describe('Item', () => { + let item, position, points, size; + + beforeEach(() => { + position = { + x: chance.integer(), + y: chance.integer() + }; + points = chance.integer({ min: 0, max: 5000 }); + size = chance.integer({ min: 1, max: 20 }); + + item = new Item({ position, points, size }); + }); + + it('creates an item correctly', () => { + expect(item.position).toMatchObject(position); + expect(item.points).toEqual(points); + expect(item.lifespan).toBeUndefined(); + expect(item.size).toEqual(size); + }); + + describe('draw()', () => { + let ctxMock; + + beforeEach(() => { + item.position = { + x: chance.integer(), + y: chance.integer() + }; + + ctxMock = { + beginPath: jest.fn(), + arc: jest.fn(), + fill: jest.fn(), + stroke: jest.fn() + }; + + item.draw(ctxMock); + }); + + it('calls the ctx beginPath() method', () => { + expect(ctxMock.beginPath).toBeCalled(); + }); + + it('calls the ctx arc() method correctly', () => { + expect(ctxMock.arc).toBeCalled(); + expect(ctxMock.arc).toBeCalledWith( + item.position.x, + item.position.y, + item.size, + 0, + 2 * Math.PI + ); + }); + + it('calls the ctx fill() method', () => { + expect(ctxMock.fill).toBeCalled(); + }); + }); +}); diff --git a/test/frontend/utilities/Items/PowerPill.test.js b/test/frontend/utilities/Items/PowerPill.test.js new file mode 100644 index 0000000..2cb3bd9 --- /dev/null +++ b/test/frontend/utilities/Items/PowerPill.test.js @@ -0,0 +1,51 @@ +import { PowerPill } from '@/frontend/utilities/Items'; +import { Player, PacMan } from '@/frontend/utilities/Players'; +import Chance from 'chance'; + +const chance = new Chance(); + +describe('PowerPill', () => { + let pill, position; + + beforeEach(() => { + position = { + x: chance.integer(), + y: chance.integer() + }; + + pill = new PowerPill(position); + }); + + it('creates a PowerPill correctly', () => { + expect(pill.position).toMatchObject(position); + expect(pill.points).toEqual(0); + expect(pill.lifespan).toBeUndefined(); + expect(pill.size).toEqual(12); + }); + + describe('use()', () => { + let player; + + describe('given the player is PacMan', () => { + beforeEach(() => { + player = new PacMan(); + }); + + it('returns truthy', () => { + const response = pill.use(player); + expect(response).toBeTruthy(); + }); + }); + + describe('given the player is not PacMan', () => { + beforeEach(() => { + player = new Player(); + }); + + it('returns falsy', () => { + const response = pill.use(player); + expect(response).not.toBeTruthy(); + }); + }); + }); +}); diff --git a/test/frontend/utilities/Path.test.js b/test/frontend/utilities/Path.test.js index 1d6b608..63d40aa 100644 --- a/test/frontend/utilities/Path.test.js +++ b/test/frontend/utilities/Path.test.js @@ -24,6 +24,7 @@ describe('Path', () => { expect(path.isSafe).toBeTruthy(); expect(path.start).toBe(start); expect(path.end).toBe(end); + expect(path.isLair).not.toBeTruthy(); }); it('adds itself to both intersections list of paths', () => { diff --git a/test/frontend/utilities/Player.test.js b/test/frontend/utilities/Players/Player.test.js similarity index 92% rename from test/frontend/utilities/Player.test.js rename to test/frontend/utilities/Players/Player.test.js index e2a549e..d9d4736 100644 --- a/test/frontend/utilities/Player.test.js +++ b/test/frontend/utilities/Players/Player.test.js @@ -1,6 +1,6 @@ import Intersection from '@/frontend/utilities/Intersection'; import Path from '@/frontend/utilities/Path.js'; -import Player from '@/frontend/utilities/Player.js'; +import Player from '@/frontend/utilities/Players/Player.js'; import Chance from 'chance'; const chance = new Chance(); @@ -159,6 +159,21 @@ describe('Player', () => { }); }); + describe('incrementScore()', () => { + let points; + + beforeEach(() => { + points = chance.integer({ min: 1 }); + + player.score = 0; + player.incrementScore(points); + }); + + it('increments the player score correctly', () => { + expect(player.score).toEqual(points); + }); + }); + describe('draw()', () => { let ctxMock; diff --git a/test/frontend/utilities/Portal.test.js b/test/frontend/utilities/Portal.test.js index cb5ae3c..f411f11 100644 --- a/test/frontend/utilities/Portal.test.js +++ b/test/frontend/utilities/Portal.test.js @@ -1,4 +1,4 @@ -import Player from '@/frontend/utilities/Player.js'; +import { Player } from '@/frontend/utilities/Players'; import Portal from '@/frontend/utilities/Portal.js'; import Intersection from '@/frontend/utilities/Intersection.js'; import Chance from 'chance';