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
-
+
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';