mustachio-game 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pr.yaml +21 -0
- package/.github/workflows/push.yaml +26 -0
- package/eslint.config.ts +17 -0
- package/index.html +22 -0
- package/package.json +35 -0
- package/src/assets/Mustachio.webp +0 -0
- package/src/assets/Mustachio_FacingLeft.webp +0 -0
- package/src/assets/Mustachio_FacingLeft_Fire.webp +0 -0
- package/src/assets/Mustachio_FacingRight.webp +0 -0
- package/src/assets/Mustachio_FacingRight_Fire.webp +0 -0
- package/src/assets/Mustachio_Fire.webp +0 -0
- package/src/assets/brick.webp +0 -0
- package/src/assets/cannonDown.webp +0 -0
- package/src/assets/cannonLeft.webp +0 -0
- package/src/assets/cannonRight.webp +0 -0
- package/src/assets/cannonUp.webp +0 -0
- package/src/assets/fallingFloor.webp +0 -0
- package/src/assets/homestead.webp +0 -0
- package/src/assets/homesteadClosed.webp +0 -0
- package/src/assets/itemBlock.webp +0 -0
- package/src/assets/obstacleBrick.webp +0 -0
- package/src/assets/punchedBlock.webp +0 -0
- package/src/assets/stacheSeed1.webp +0 -0
- package/src/assets/stacheSeed2.webp +0 -0
- package/src/assets/stacheSeedReversed1.webp +0 -0
- package/src/assets/stacheSeedReversed2.webp +0 -0
- package/src/assets/stacheShotDown.webp +0 -0
- package/src/assets/stacheShotLeft.webp +0 -0
- package/src/assets/stacheShotRight.webp +0 -0
- package/src/assets/stacheShotUp.webp +0 -0
- package/src/assets/stacheSlinger1.webp +0 -0
- package/src/assets/stacheSlinger2.webp +0 -0
- package/src/assets/stacheStalker.webp +0 -0
- package/src/assets/stacheStalkerReversed.webp +0 -0
- package/src/assets/stacheStreaker1.webp +0 -0
- package/src/assets/stacheStreaker2.webp +0 -0
- package/src/classes/game-objects/bg-objects/background.ts +18 -0
- package/src/classes/game-objects/bg-objects/cloud.ts +37 -0
- package/src/classes/game-objects/mustachio.ts +482 -0
- package/src/classes/game-objects/point-objects/enemies/enemy.ts +64 -0
- package/src/classes/game-objects/point-objects/enemies/stache-seed.ts +124 -0
- package/src/classes/game-objects/point-objects/enemies/stache-shot.ts +68 -0
- package/src/classes/game-objects/point-objects/enemies/stache-slinger.ts +63 -0
- package/src/classes/game-objects/point-objects/enemies/stache-stalker.ts +41 -0
- package/src/classes/game-objects/point-objects/enemies/stache-streaker.ts +78 -0
- package/src/classes/game-objects/point-objects/items/coin.ts +72 -0
- package/src/classes/game-objects/point-objects/items/fire-stache.ts +48 -0
- package/src/classes/game-objects/point-objects/items/item.ts +27 -0
- package/src/classes/game-objects/point-objects/items/stacheroom.ts +48 -0
- package/src/classes/game-objects/point-objects/point-item.ts +10 -0
- package/src/classes/game-objects/projectiles/brick-debris.ts +44 -0
- package/src/classes/game-objects/projectiles/enemy-projectiles/enemy-projectile.ts +3 -0
- package/src/classes/game-objects/projectiles/enemy-projectiles/fire-ball.ts +87 -0
- package/src/classes/game-objects/projectiles/enemy-projectiles/fire-bar.ts +65 -0
- package/src/classes/game-objects/projectiles/enemy-projectiles/fire-cross.ts +67 -0
- package/src/classes/game-objects/projectiles/enemy-projectiles/laser.ts +41 -0
- package/src/classes/game-objects/projectiles/projectile.ts +3 -0
- package/src/classes/game-objects/projectiles/stache-ball.ts +57 -0
- package/src/classes/game-objects/set-pieces/flag.ts +34 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/block.ts +17 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/cave-wall.ts +21 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/falling-floor.ts +65 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/fire-bar-block.ts +31 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/fire-cross-block.ts +28 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/punchable-blockS/brick.ts +44 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/punchable-blockS/item-block.ts +82 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/punchable-blockS/punchable-block.ts +6 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/stache-cannon.ts +54 -0
- package/src/classes/game-objects/set-pieces/obstacles/blocks/wall.ts +22 -0
- package/src/classes/game-objects/set-pieces/obstacles/floor.ts +27 -0
- package/src/classes/game-objects/set-pieces/obstacles/obstacle-types.ts +14 -0
- package/src/classes/game-objects/set-pieces/obstacles/obstacle.ts +3 -0
- package/src/classes/game-objects/set-pieces/obstacles/pipe.ts +35 -0
- package/src/classes/game-objects/set-pieces/obstacles/warp-pipe.ts +17 -0
- package/src/classes/game-objects/set-pieces/set-piece.ts +3 -0
- package/src/classes/game-objects/ui-objects/score-display.ts +10 -0
- package/src/classes/game-objects/ui-objects/timer-display.ts +15 -0
- package/src/classes/game-objects/ui-objects/ui-object.ts +16 -0
- package/src/classes/game-objects/ui-objects/win-display.ts +25 -0
- package/src/dev.ts +5 -0
- package/src/index.ts +3 -0
- package/src/levels/caves/cave-one.ts +90 -0
- package/src/levels/level-helpers.ts +101 -0
- package/src/levels/level-one.ts +379 -0
- package/src/levels/test-levels/blocks-and-items.ts +77 -0
- package/src/levels/test-levels/cannon-and-cross.ts +75 -0
- package/src/levels/test-levels/caves-and-enemies.ts +73 -0
- package/src/levels/test-levels/win-game.ts +24 -0
- package/src/main.ts +6 -0
- package/src/mustachi-game-context.ts +35 -0
- package/src/shared/app-code.ts +106 -0
- package/src/shared/constants.ts +1 -0
- package/src/shared/game-context.ts +547 -0
- package/src/shared/game-objects/game-object.ts +28 -0
- package/src/shared/game-objects/moving-game-object.ts +46 -0
- package/src/shared/game-objects/rotating-game-object.ts +58 -0
- package/src/shared/game-objects/updating-game-object.ts +7 -0
- package/src/shared/player.ts +73 -0
- package/src/shared/types.ts +21 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +13 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCollisionDirection,
|
|
3
|
+
getReverseDirection,
|
|
4
|
+
outOfBounds,
|
|
5
|
+
} from "./app-code";
|
|
6
|
+
import type { GameObject } from "./game-objects/game-object";
|
|
7
|
+
import { Player } from "./player";
|
|
8
|
+
import { RotatingGameObject } from "./game-objects/rotating-game-object";
|
|
9
|
+
import { direction, type collision } from "./types";
|
|
10
|
+
import { UpdatingGameObject } from "./game-objects/updating-game-object";
|
|
11
|
+
import type { ScoreDisplay } from "../classes/game-objects/ui-objects/score-display";
|
|
12
|
+
import type { TimerDisplay } from "../classes/game-objects/ui-objects/timer-display";
|
|
13
|
+
import { WinDisplay } from "../classes/game-objects/ui-objects/win-display";
|
|
14
|
+
|
|
15
|
+
export abstract class GameContext {
|
|
16
|
+
score: number = 0;
|
|
17
|
+
currentDir: number = direction.NONE;
|
|
18
|
+
time: number = 300; // 5 minutes
|
|
19
|
+
xOffset: number = 0;
|
|
20
|
+
|
|
21
|
+
readonly gravity: number = 0.6;
|
|
22
|
+
readonly gameArea: HTMLCanvasElement;
|
|
23
|
+
readonly ui: HTMLCanvasElement;
|
|
24
|
+
readonly bg: HTMLCanvasElement;
|
|
25
|
+
readonly uiContext: CanvasRenderingContext2D;
|
|
26
|
+
readonly gameContext: CanvasRenderingContext2D;
|
|
27
|
+
readonly bgContext: CanvasRenderingContext2D;
|
|
28
|
+
|
|
29
|
+
private xSpeed: number;
|
|
30
|
+
private readonly walkSpeed;
|
|
31
|
+
private readonly sprintSpeed;
|
|
32
|
+
private mainLoop: number | null = null;
|
|
33
|
+
private timerLoop: number | null = null;
|
|
34
|
+
private isStatic: boolean = false;
|
|
35
|
+
private gameOver: boolean = false;
|
|
36
|
+
|
|
37
|
+
private readonly pressedKeys: string[] = [];
|
|
38
|
+
private readonly gameObjects: GameObject[] = [];
|
|
39
|
+
private readonly uiObjects: GameObject[] = [];
|
|
40
|
+
private readonly bgObjects: GameObject[] = [];
|
|
41
|
+
protected abstract readonly gameName: string;
|
|
42
|
+
|
|
43
|
+
// Needs to be initialized in the implementation
|
|
44
|
+
protected abstract readonly player: Player;
|
|
45
|
+
protected abstract readonly scoreDisplay: ScoreDisplay;
|
|
46
|
+
protected abstract readonly timeDisplay: TimerDisplay;
|
|
47
|
+
|
|
48
|
+
constructor(containerId: string) {
|
|
49
|
+
if (window.innerWidth <= 1000) {
|
|
50
|
+
this.walkSpeed = 14;
|
|
51
|
+
} else {
|
|
52
|
+
this.walkSpeed = 7;
|
|
53
|
+
}
|
|
54
|
+
this.xSpeed = this.walkSpeed;
|
|
55
|
+
this.sprintSpeed = 14;
|
|
56
|
+
|
|
57
|
+
const { gameCanvas, bgCanvas, uiCanvas } = this.createGameArea(containerId);
|
|
58
|
+
let result = this.setupCanvas(gameCanvas);
|
|
59
|
+
this.gameArea = result.canvas;
|
|
60
|
+
this.gameContext = result.context;
|
|
61
|
+
|
|
62
|
+
result = this.setupCanvas(uiCanvas);
|
|
63
|
+
this.ui = result.canvas;
|
|
64
|
+
this.uiContext = result.context;
|
|
65
|
+
|
|
66
|
+
result = this.setupCanvas(bgCanvas);
|
|
67
|
+
this.bg = result.canvas;
|
|
68
|
+
this.bgContext = result.context;
|
|
69
|
+
|
|
70
|
+
window.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
71
|
+
window.addEventListener("keyup", (event) => this.onKeyUp(event));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getPlayer() {
|
|
75
|
+
return this.player;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
addScore(score: number) {
|
|
79
|
+
this.score += score;
|
|
80
|
+
this.scoreDisplay.draw(this.uiContext);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setPlayerLocation(x: number, y: number) {
|
|
84
|
+
if (this.isStatic) {
|
|
85
|
+
this.player.reset(x, y);
|
|
86
|
+
} else {
|
|
87
|
+
this.player.reset(undefined, y);
|
|
88
|
+
this.xOffset = -x;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
startMainLoop() {
|
|
93
|
+
if (this.mainLoop) {
|
|
94
|
+
clearInterval(this.mainLoop);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.mainLoop = setInterval(() => {
|
|
98
|
+
this.updateGameArea();
|
|
99
|
+
}, 20);
|
|
100
|
+
|
|
101
|
+
if (this.timerLoop) {
|
|
102
|
+
clearInterval(this.timerLoop);
|
|
103
|
+
}
|
|
104
|
+
this.timerLoop = setInterval(() => {
|
|
105
|
+
this.timerTick();
|
|
106
|
+
}, 1000);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stopMainLoop() {
|
|
110
|
+
if (this.mainLoop) {
|
|
111
|
+
clearInterval(this.mainLoop);
|
|
112
|
+
this.mainLoop = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.timerLoop) {
|
|
116
|
+
clearInterval(this.timerLoop);
|
|
117
|
+
this.timerLoop = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
clearLevel() {
|
|
122
|
+
this.isStatic = false;
|
|
123
|
+
this.xOffset = 0;
|
|
124
|
+
this.stopMainLoop();
|
|
125
|
+
|
|
126
|
+
for (const gameObject of this.gameObjects) {
|
|
127
|
+
// JS doesn't support interfaces
|
|
128
|
+
// so we have to check if the object has a dispose method
|
|
129
|
+
if ("dispose" in gameObject && typeof gameObject.dispose === "function") {
|
|
130
|
+
gameObject.dispose();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.gameObjects.splice(0, this.gameObjects.length);
|
|
135
|
+
this.addGameObject(this.player);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setStatic(isStatic: boolean) {
|
|
139
|
+
this.isStatic = isStatic;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setGameOver() {
|
|
143
|
+
this.currentDir = direction.NONE;
|
|
144
|
+
this.gameOver = true;
|
|
145
|
+
this.stopTimer();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Assign a unique ID to the game object and add it to the gameObjects array
|
|
149
|
+
addGameObject(gameObject: GameObject, beginning: boolean = false) {
|
|
150
|
+
const gameObjectInList = this.gameObjects.find(
|
|
151
|
+
(go) => go.objectId === gameObject.objectId,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (gameObjectInList) {
|
|
155
|
+
// If the game object is already in the list, we don't need to add it again
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (beginning) {
|
|
160
|
+
this.gameObjects.unshift(gameObject);
|
|
161
|
+
} else {
|
|
162
|
+
this.gameObjects.push(gameObject);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
addUIObject(gameObject: GameObject) {
|
|
167
|
+
this.uiObjects.push(gameObject);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
addBgObject(gameObject: GameObject) {
|
|
171
|
+
this.bgObjects.push(gameObject);
|
|
172
|
+
gameObject.draw(this.bgContext);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
removeGameObject(gameObject: GameObject) {
|
|
176
|
+
const index = this.gameObjects.findIndex(
|
|
177
|
+
(go) => go.objectId === gameObject.objectId,
|
|
178
|
+
);
|
|
179
|
+
if (index > -1) {
|
|
180
|
+
this.gameObjects.splice(index, 1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
removeUIObject(gameObject: GameObject) {
|
|
185
|
+
const index = this.uiObjects.findIndex(
|
|
186
|
+
(go) => go.objectId === gameObject.objectId,
|
|
187
|
+
);
|
|
188
|
+
if (index > -1) {
|
|
189
|
+
this.uiObjects.splice(index, 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
removeBgObject(gameObject: GameObject) {
|
|
194
|
+
const index = this.bgObjects.findIndex(
|
|
195
|
+
(go) => go.objectId === gameObject.objectId,
|
|
196
|
+
);
|
|
197
|
+
if (index > -1) {
|
|
198
|
+
this.bgObjects.splice(index, 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
restart(levelFunc: (gc: GameContext) => void) {
|
|
203
|
+
this.stopMainLoop();
|
|
204
|
+
this.gameObjects.splice(0, this.gameObjects.length);
|
|
205
|
+
levelFunc(this);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
validateNewObjectId(objectId: number) {
|
|
209
|
+
const gameObject = this.gameObjects.find((go) => go.objectId === objectId);
|
|
210
|
+
return gameObject === undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
stopTimer() {
|
|
214
|
+
if (this.timerLoop) {
|
|
215
|
+
clearInterval(this.timerLoop);
|
|
216
|
+
this.timerLoop = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private clear() {
|
|
221
|
+
this.gameContext.clearRect(0, 0, this.gameArea.width, this.gameArea.height);
|
|
222
|
+
// this.uiContext.clearRect(0, 0, this.ui.width, this.ui.height)
|
|
223
|
+
// this.bgContext.clearRect(0, 0, this.bg.width, this.bg.height)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private updateGameArea() {
|
|
227
|
+
this.clear();
|
|
228
|
+
const gameObjectsInUpdateArea = this.getGameObjectsToUpdate();
|
|
229
|
+
const gameObjectCollisions = this.getGameObjectsWithCollisions(
|
|
230
|
+
gameObjectsInUpdateArea,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (this.gameOver) {
|
|
234
|
+
this.player.update([]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Update the game objects in the game area
|
|
238
|
+
for (const gameObject of gameObjectsInUpdateArea) {
|
|
239
|
+
if (!this.gameOver && gameObject instanceof UpdatingGameObject) {
|
|
240
|
+
const collisions = gameObjectCollisions.get(gameObject.objectId);
|
|
241
|
+
gameObject.update(collisions ?? []);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
gameObject.draw(this.gameContext);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The ui layer is never out of bounds
|
|
248
|
+
for (const uiObject of this.uiObjects) {
|
|
249
|
+
uiObject.draw(this.uiContext);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const canMove = this.player.canMove(this.currentDir);
|
|
253
|
+
if (this.gameOver || !canMove) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!this.isStatic) {
|
|
258
|
+
// We move the game object opposite to the player
|
|
259
|
+
// to simulate the player moving
|
|
260
|
+
if (this.currentDir === direction.RIGHT) {
|
|
261
|
+
this.xOffset += this.xSpeed;
|
|
262
|
+
} else if (this.currentDir === direction.LEFT) {
|
|
263
|
+
this.xOffset -= this.xSpeed;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
if (this.currentDir === direction.RIGHT) {
|
|
267
|
+
this.player.rect.x -= this.xSpeed;
|
|
268
|
+
} else if (this.currentDir === direction.LEFT) {
|
|
269
|
+
this.player.rect.x += this.xSpeed;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private onKeyDown(event: KeyboardEvent) {
|
|
275
|
+
if (this.gameOver || event.repeat) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const key = event.key.toLocaleLowerCase().trim();
|
|
280
|
+
|
|
281
|
+
if (key === "p") {
|
|
282
|
+
if (this.mainLoop) {
|
|
283
|
+
this.stopMainLoop();
|
|
284
|
+
} else {
|
|
285
|
+
this.startMainLoop();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.mainLoop === null) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (this.pressedKeys.includes(key)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.pressedKeys.push(key);
|
|
298
|
+
this.player.customKeyDown(key);
|
|
299
|
+
|
|
300
|
+
if (key === "arrowleft" || key === "a") {
|
|
301
|
+
this.currentDir = direction.RIGHT;
|
|
302
|
+
} else if (key === "arrowright" || key === "d") {
|
|
303
|
+
this.currentDir = direction.LEFT;
|
|
304
|
+
} else if (key === "arrowup" || key === "w") {
|
|
305
|
+
this.player.jump();
|
|
306
|
+
} else if (key === "shift") {
|
|
307
|
+
this.xSpeed = this.sprintSpeed;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private onKeyUp(event: KeyboardEvent) {
|
|
312
|
+
if (this.gameOver) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const key = event.key.toLocaleLowerCase();
|
|
317
|
+
|
|
318
|
+
this.pressedKeys.splice(
|
|
319
|
+
this.pressedKeys.findIndex((pressedKey) => pressedKey === key),
|
|
320
|
+
1,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
this.player.customKeyUp(key);
|
|
324
|
+
|
|
325
|
+
if (
|
|
326
|
+
key === "arrowleft" ||
|
|
327
|
+
key === "a" ||
|
|
328
|
+
key === "arrowright" ||
|
|
329
|
+
key === "d"
|
|
330
|
+
) {
|
|
331
|
+
if (
|
|
332
|
+
this.pressedKeys.includes("arrowleft") ||
|
|
333
|
+
this.pressedKeys.includes("a")
|
|
334
|
+
) {
|
|
335
|
+
this.currentDir = direction.RIGHT;
|
|
336
|
+
} else if (
|
|
337
|
+
this.pressedKeys.includes("arrowright") ||
|
|
338
|
+
this.pressedKeys.includes("d")
|
|
339
|
+
) {
|
|
340
|
+
this.currentDir = direction.LEFT;
|
|
341
|
+
} else {
|
|
342
|
+
this.currentDir = direction.NONE;
|
|
343
|
+
}
|
|
344
|
+
} else if (key === "shift") {
|
|
345
|
+
this.xSpeed = this.walkSpeed;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private createGameArea(containerId: string) {
|
|
350
|
+
const container = document.getElementById(containerId);
|
|
351
|
+
if (!container) {
|
|
352
|
+
throw new Error(`Container with id ${containerId} not found`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Clear the container
|
|
356
|
+
container.innerHTML = "";
|
|
357
|
+
container.style.position = "relative";
|
|
358
|
+
|
|
359
|
+
const { width, height } = container.getBoundingClientRect();
|
|
360
|
+
|
|
361
|
+
const bgCanvas = document.createElement("canvas");
|
|
362
|
+
bgCanvas.id = "background-layer";
|
|
363
|
+
bgCanvas.style.position = "absolute";
|
|
364
|
+
bgCanvas.style.inset = "0";
|
|
365
|
+
bgCanvas.style.width = `${width}px`;
|
|
366
|
+
bgCanvas.style.height = `${height}px`;
|
|
367
|
+
container.appendChild(bgCanvas);
|
|
368
|
+
|
|
369
|
+
const gameCanvas = document.createElement("canvas");
|
|
370
|
+
gameCanvas.id = "game-layer";
|
|
371
|
+
gameCanvas.style.position = "absolute";
|
|
372
|
+
gameCanvas.style.inset = "0";
|
|
373
|
+
gameCanvas.style.width = `${width}px`;
|
|
374
|
+
gameCanvas.style.height = `${height}px`;
|
|
375
|
+
container.appendChild(gameCanvas);
|
|
376
|
+
|
|
377
|
+
const uiCanvas = document.createElement("canvas");
|
|
378
|
+
uiCanvas.id = "ui-layer";
|
|
379
|
+
uiCanvas.style.position = "absolute";
|
|
380
|
+
uiCanvas.style.inset = "0";
|
|
381
|
+
uiCanvas.style.width = `${width}px`;
|
|
382
|
+
uiCanvas.style.height = `${height}px`;
|
|
383
|
+
container.appendChild(uiCanvas);
|
|
384
|
+
|
|
385
|
+
return { gameCanvas, bgCanvas, uiCanvas };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private setupCanvas(canvas: HTMLCanvasElement) {
|
|
389
|
+
canvas.width = 1920;
|
|
390
|
+
canvas.height = 1080;
|
|
391
|
+
const context = canvas.getContext("2d") as CanvasRenderingContext2D;
|
|
392
|
+
return { canvas, context };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private timerTick() {
|
|
396
|
+
this.time--;
|
|
397
|
+
this.timeDisplay.draw(this.uiContext);
|
|
398
|
+
if (this.time <= 0) {
|
|
399
|
+
if (this.timerLoop) {
|
|
400
|
+
clearInterval(this.timerLoop);
|
|
401
|
+
this.timerLoop = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.isStatic = true;
|
|
405
|
+
this.player.playerKill();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// This function returns an array of game objects that are
|
|
410
|
+
// within the bounds of the game area
|
|
411
|
+
private getGameObjectsToUpdate() {
|
|
412
|
+
return this.gameObjects.filter((gameObject) => {
|
|
413
|
+
return !outOfBounds(gameObject, this);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// This function checks for collisions between game objects
|
|
418
|
+
// and returns an array of collision objects
|
|
419
|
+
// Each collision object contains the two game objects involved in the collision
|
|
420
|
+
// and the direction of the collision
|
|
421
|
+
// This allows us to handle collisions between game objects without
|
|
422
|
+
// having to check for collisions more than once
|
|
423
|
+
private getGameObjectsWithCollisions(gameObjects: GameObject[]) {
|
|
424
|
+
const collisons: Map<number, collision[]> = new Map();
|
|
425
|
+
for (const gameObject of gameObjects) {
|
|
426
|
+
if (!gameObject.acceptsCollision) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this.getCollisionForGameObject(gameObject, gameObjects, collisons);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return collisons;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private getCollisionForGameObject(
|
|
437
|
+
gameObject: GameObject,
|
|
438
|
+
gameObjects: GameObject[],
|
|
439
|
+
collisons: Map<number, collision[]>,
|
|
440
|
+
) {
|
|
441
|
+
for (const otherGameObject of gameObjects) {
|
|
442
|
+
if (!otherGameObject.acceptsCollision) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.getCollisionForGameObjects(gameObject, otherGameObject, collisons);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private getCollisionForGameObjects(
|
|
451
|
+
gameObject: GameObject,
|
|
452
|
+
otherGameObject: GameObject,
|
|
453
|
+
collisions: Map<number, collision[]>,
|
|
454
|
+
) {
|
|
455
|
+
if (gameObject.objectId === otherGameObject.objectId) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!collisions.has(gameObject.objectId)) {
|
|
460
|
+
collisions.set(gameObject.objectId, []);
|
|
461
|
+
} else {
|
|
462
|
+
// If the game object has already been added to the collisions map
|
|
463
|
+
// we don't need to check for collisions again
|
|
464
|
+
const goCollisions = collisions.get(gameObject.objectId);
|
|
465
|
+
if (goCollisions) {
|
|
466
|
+
const existingCollision = goCollisions.find(
|
|
467
|
+
(collision) =>
|
|
468
|
+
collision.gameObject.objectId === otherGameObject.objectId,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (existingCollision) {
|
|
472
|
+
// If the collision already exists, we don't need to check for collisions again
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const collisionDirection = this.getCollisionDirectionForGameObjects(
|
|
479
|
+
gameObject,
|
|
480
|
+
otherGameObject,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
if (collisionDirection !== null) {
|
|
484
|
+
// Add 2 collision objects to the array
|
|
485
|
+
// one for each game object
|
|
486
|
+
const collision = {
|
|
487
|
+
gameObject: otherGameObject,
|
|
488
|
+
collisionDirection,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const reversedCollision = {
|
|
492
|
+
gameObject: gameObject,
|
|
493
|
+
collisionDirection: getReverseDirection(collisionDirection),
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const goCollisions = collisions.get(gameObject.objectId);
|
|
497
|
+
if (goCollisions) {
|
|
498
|
+
goCollisions.push(collision);
|
|
499
|
+
collisions.set(gameObject.objectId, goCollisions);
|
|
500
|
+
} else {
|
|
501
|
+
collisions.set(gameObject.objectId, [collision]);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const otherGoCollisions = collisions.get(otherGameObject.objectId);
|
|
505
|
+
if (otherGoCollisions) {
|
|
506
|
+
otherGoCollisions.push(reversedCollision);
|
|
507
|
+
collisions.set(otherGameObject.objectId, otherGoCollisions);
|
|
508
|
+
} else {
|
|
509
|
+
collisions.set(otherGameObject.objectId, [reversedCollision]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private getCollisionDirectionForGameObjects(
|
|
515
|
+
gameObject: GameObject,
|
|
516
|
+
otherGameObject: GameObject,
|
|
517
|
+
) {
|
|
518
|
+
let collisionDirection: number | null = null;
|
|
519
|
+
if (gameObject instanceof RotatingGameObject) {
|
|
520
|
+
if (
|
|
521
|
+
otherGameObject instanceof Player &&
|
|
522
|
+
gameObject.hitDetection(otherGameObject.rect.x, otherGameObject.rect.y)
|
|
523
|
+
) {
|
|
524
|
+
collisionDirection = direction.NONE;
|
|
525
|
+
}
|
|
526
|
+
} else if (otherGameObject instanceof RotatingGameObject) {
|
|
527
|
+
if (
|
|
528
|
+
gameObject instanceof Player &&
|
|
529
|
+
otherGameObject.hitDetection(gameObject.rect.x, gameObject.rect.y)
|
|
530
|
+
) {
|
|
531
|
+
collisionDirection = direction.NONE;
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
collisionDirection = getCollisionDirection(
|
|
535
|
+
gameObject,
|
|
536
|
+
otherGameObject,
|
|
537
|
+
this.xOffset,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return collisionDirection;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
win() {
|
|
545
|
+
this.addUIObject(new WinDisplay(this));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GameContext } from "../game-context";
|
|
2
|
+
import type { rectangle } from "../types";
|
|
3
|
+
|
|
4
|
+
export abstract class GameObject {
|
|
5
|
+
acceptsCollision = true;
|
|
6
|
+
|
|
7
|
+
readonly rect: rectangle;
|
|
8
|
+
readonly objectId: number;
|
|
9
|
+
|
|
10
|
+
protected readonly gameContext: GameContext;
|
|
11
|
+
|
|
12
|
+
constructor(gameContext: GameContext, rect: rectangle) {
|
|
13
|
+
this.gameContext = gameContext;
|
|
14
|
+
this.objectId = this.generateUniqueId();
|
|
15
|
+
this.rect = rect;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
abstract draw(ctx: CanvasRenderingContext2D): void;
|
|
19
|
+
|
|
20
|
+
private generateUniqueId() {
|
|
21
|
+
let id = Math.floor(Math.random() * 10000);
|
|
22
|
+
while (!this.gameContext.validateNewObjectId(id)) {
|
|
23
|
+
id = Math.floor(Math.random() * 10000);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { outOfBounds } from "../app-code";
|
|
2
|
+
import { direction, type collision } from "../types";
|
|
3
|
+
import { UpdatingGameObject } from "./updating-game-object";
|
|
4
|
+
|
|
5
|
+
export abstract class MovingGameObject extends UpdatingGameObject {
|
|
6
|
+
speedX: number = 0;
|
|
7
|
+
speedY: number = 0;
|
|
8
|
+
onGround: boolean = false;
|
|
9
|
+
|
|
10
|
+
leftRightMovement(collisions: collision[]) {
|
|
11
|
+
// The floor is never outside of the canvas
|
|
12
|
+
// so if the object is outside of the canvas
|
|
13
|
+
// we dont use gravity
|
|
14
|
+
|
|
15
|
+
this.onGround = outOfBounds(this, this.gameContext);
|
|
16
|
+
for (const collision of collisions) {
|
|
17
|
+
this.handleCollision(collision);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.rect.x += this.speedX;
|
|
21
|
+
|
|
22
|
+
if (!this.onGround) {
|
|
23
|
+
this.rect.y += this.speedY;
|
|
24
|
+
this.speedY += this.gameContext.gravity;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected handleCollision(collision: collision) {
|
|
29
|
+
if (collision.collisionDirection === direction.DOWN) {
|
|
30
|
+
this.onGround = true;
|
|
31
|
+
this.rect.y = collision.gameObject.rect.y - this.rect.height;
|
|
32
|
+
this.speedY = 0;
|
|
33
|
+
} else if (collision.collisionDirection === direction.UP) {
|
|
34
|
+
this.speedY = 1;
|
|
35
|
+
this.rect.y =
|
|
36
|
+
collision.gameObject.rect.y + collision.gameObject.rect.height;
|
|
37
|
+
} else if (collision.collisionDirection === direction.LEFT) {
|
|
38
|
+
this.rect.x = collision.gameObject.rect.x - this.rect.width;
|
|
39
|
+
this.speedX = -Math.abs(this.speedX);
|
|
40
|
+
} else if (collision.collisionDirection === direction.RIGHT) {
|
|
41
|
+
this.rect.x =
|
|
42
|
+
collision.gameObject.rect.x + collision.gameObject.rect.width;
|
|
43
|
+
this.speedX = Math.abs(this.speedX);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { direction } from "../types";
|
|
2
|
+
import { UpdatingGameObject } from "./updating-game-object";
|
|
3
|
+
|
|
4
|
+
export abstract class RotatingGameObject extends UpdatingGameObject {
|
|
5
|
+
protected rotation: number = 0;
|
|
6
|
+
protected readonly rotationSpeed: number = 0.01;
|
|
7
|
+
|
|
8
|
+
hitDetection(playerX: number, playerY: number) {
|
|
9
|
+
// 1. Move player point into firebar's local space (rotated frame)
|
|
10
|
+
const pivotX = this.rect.x + this.rect.width / 2 + this.gameContext.xOffset;
|
|
11
|
+
const pivotY = this.rect.y + this.rect.height;
|
|
12
|
+
|
|
13
|
+
// Translate to pivot
|
|
14
|
+
const dx = playerX - pivotX;
|
|
15
|
+
const dy = playerY - pivotY;
|
|
16
|
+
|
|
17
|
+
// Unrotate the point (rotate opposite direction)
|
|
18
|
+
const sin = Math.sin(-this.rotation);
|
|
19
|
+
const cos = Math.cos(-this.rotation);
|
|
20
|
+
|
|
21
|
+
const localX = dx * cos - dy * sin;
|
|
22
|
+
const localY = dx * sin + dy * cos;
|
|
23
|
+
|
|
24
|
+
// 2. Check against firebar's axis-aligned box in local space
|
|
25
|
+
const halfW = this.rect.width / 2;
|
|
26
|
+
const height = this.rect.height;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
localX >= -halfW && localX <= halfW && localY >= -height && localY <= 0
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
hitDirection(playerX: number, playerY: number) {
|
|
34
|
+
const pivotX = this.rect.x + this.rect.width / 2;
|
|
35
|
+
const pivotY = this.rect.y + this.rect.height;
|
|
36
|
+
|
|
37
|
+
// Translate to pivot
|
|
38
|
+
const dx = playerX - pivotX;
|
|
39
|
+
const dy = playerY - pivotY;
|
|
40
|
+
|
|
41
|
+
// Unrotate the point (rotate opposite direction)
|
|
42
|
+
const sin = Math.sin(-this.rotation);
|
|
43
|
+
const cos = Math.cos(-this.rotation);
|
|
44
|
+
|
|
45
|
+
const localX = dx * cos - dy * sin;
|
|
46
|
+
const localY = dx * sin + dy * cos;
|
|
47
|
+
|
|
48
|
+
if (localY < 0) {
|
|
49
|
+
return direction.UP;
|
|
50
|
+
} else if (localX < 0) {
|
|
51
|
+
return direction.LEFT;
|
|
52
|
+
} else if (localX > 0) {
|
|
53
|
+
return direction.RIGHT;
|
|
54
|
+
} else {
|
|
55
|
+
return direction.DOWN;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|