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.
Files changed (101) hide show
  1. package/.github/workflows/pr.yaml +21 -0
  2. package/.github/workflows/push.yaml +26 -0
  3. package/eslint.config.ts +17 -0
  4. package/index.html +22 -0
  5. package/package.json +35 -0
  6. package/src/assets/Mustachio.webp +0 -0
  7. package/src/assets/Mustachio_FacingLeft.webp +0 -0
  8. package/src/assets/Mustachio_FacingLeft_Fire.webp +0 -0
  9. package/src/assets/Mustachio_FacingRight.webp +0 -0
  10. package/src/assets/Mustachio_FacingRight_Fire.webp +0 -0
  11. package/src/assets/Mustachio_Fire.webp +0 -0
  12. package/src/assets/brick.webp +0 -0
  13. package/src/assets/cannonDown.webp +0 -0
  14. package/src/assets/cannonLeft.webp +0 -0
  15. package/src/assets/cannonRight.webp +0 -0
  16. package/src/assets/cannonUp.webp +0 -0
  17. package/src/assets/fallingFloor.webp +0 -0
  18. package/src/assets/homestead.webp +0 -0
  19. package/src/assets/homesteadClosed.webp +0 -0
  20. package/src/assets/itemBlock.webp +0 -0
  21. package/src/assets/obstacleBrick.webp +0 -0
  22. package/src/assets/punchedBlock.webp +0 -0
  23. package/src/assets/stacheSeed1.webp +0 -0
  24. package/src/assets/stacheSeed2.webp +0 -0
  25. package/src/assets/stacheSeedReversed1.webp +0 -0
  26. package/src/assets/stacheSeedReversed2.webp +0 -0
  27. package/src/assets/stacheShotDown.webp +0 -0
  28. package/src/assets/stacheShotLeft.webp +0 -0
  29. package/src/assets/stacheShotRight.webp +0 -0
  30. package/src/assets/stacheShotUp.webp +0 -0
  31. package/src/assets/stacheSlinger1.webp +0 -0
  32. package/src/assets/stacheSlinger2.webp +0 -0
  33. package/src/assets/stacheStalker.webp +0 -0
  34. package/src/assets/stacheStalkerReversed.webp +0 -0
  35. package/src/assets/stacheStreaker1.webp +0 -0
  36. package/src/assets/stacheStreaker2.webp +0 -0
  37. package/src/classes/game-objects/bg-objects/background.ts +18 -0
  38. package/src/classes/game-objects/bg-objects/cloud.ts +37 -0
  39. package/src/classes/game-objects/mustachio.ts +482 -0
  40. package/src/classes/game-objects/point-objects/enemies/enemy.ts +64 -0
  41. package/src/classes/game-objects/point-objects/enemies/stache-seed.ts +124 -0
  42. package/src/classes/game-objects/point-objects/enemies/stache-shot.ts +68 -0
  43. package/src/classes/game-objects/point-objects/enemies/stache-slinger.ts +63 -0
  44. package/src/classes/game-objects/point-objects/enemies/stache-stalker.ts +41 -0
  45. package/src/classes/game-objects/point-objects/enemies/stache-streaker.ts +78 -0
  46. package/src/classes/game-objects/point-objects/items/coin.ts +72 -0
  47. package/src/classes/game-objects/point-objects/items/fire-stache.ts +48 -0
  48. package/src/classes/game-objects/point-objects/items/item.ts +27 -0
  49. package/src/classes/game-objects/point-objects/items/stacheroom.ts +48 -0
  50. package/src/classes/game-objects/point-objects/point-item.ts +10 -0
  51. package/src/classes/game-objects/projectiles/brick-debris.ts +44 -0
  52. package/src/classes/game-objects/projectiles/enemy-projectiles/enemy-projectile.ts +3 -0
  53. package/src/classes/game-objects/projectiles/enemy-projectiles/fire-ball.ts +87 -0
  54. package/src/classes/game-objects/projectiles/enemy-projectiles/fire-bar.ts +65 -0
  55. package/src/classes/game-objects/projectiles/enemy-projectiles/fire-cross.ts +67 -0
  56. package/src/classes/game-objects/projectiles/enemy-projectiles/laser.ts +41 -0
  57. package/src/classes/game-objects/projectiles/projectile.ts +3 -0
  58. package/src/classes/game-objects/projectiles/stache-ball.ts +57 -0
  59. package/src/classes/game-objects/set-pieces/flag.ts +34 -0
  60. package/src/classes/game-objects/set-pieces/obstacles/blocks/block.ts +17 -0
  61. package/src/classes/game-objects/set-pieces/obstacles/blocks/cave-wall.ts +21 -0
  62. package/src/classes/game-objects/set-pieces/obstacles/blocks/falling-floor.ts +65 -0
  63. package/src/classes/game-objects/set-pieces/obstacles/blocks/fire-bar-block.ts +31 -0
  64. package/src/classes/game-objects/set-pieces/obstacles/blocks/fire-cross-block.ts +28 -0
  65. package/src/classes/game-objects/set-pieces/obstacles/blocks/punchable-blockS/brick.ts +44 -0
  66. package/src/classes/game-objects/set-pieces/obstacles/blocks/punchable-blockS/item-block.ts +82 -0
  67. package/src/classes/game-objects/set-pieces/obstacles/blocks/punchable-blockS/punchable-block.ts +6 -0
  68. package/src/classes/game-objects/set-pieces/obstacles/blocks/stache-cannon.ts +54 -0
  69. package/src/classes/game-objects/set-pieces/obstacles/blocks/wall.ts +22 -0
  70. package/src/classes/game-objects/set-pieces/obstacles/floor.ts +27 -0
  71. package/src/classes/game-objects/set-pieces/obstacles/obstacle-types.ts +14 -0
  72. package/src/classes/game-objects/set-pieces/obstacles/obstacle.ts +3 -0
  73. package/src/classes/game-objects/set-pieces/obstacles/pipe.ts +35 -0
  74. package/src/classes/game-objects/set-pieces/obstacles/warp-pipe.ts +17 -0
  75. package/src/classes/game-objects/set-pieces/set-piece.ts +3 -0
  76. package/src/classes/game-objects/ui-objects/score-display.ts +10 -0
  77. package/src/classes/game-objects/ui-objects/timer-display.ts +15 -0
  78. package/src/classes/game-objects/ui-objects/ui-object.ts +16 -0
  79. package/src/classes/game-objects/ui-objects/win-display.ts +25 -0
  80. package/src/dev.ts +5 -0
  81. package/src/index.ts +3 -0
  82. package/src/levels/caves/cave-one.ts +90 -0
  83. package/src/levels/level-helpers.ts +101 -0
  84. package/src/levels/level-one.ts +379 -0
  85. package/src/levels/test-levels/blocks-and-items.ts +77 -0
  86. package/src/levels/test-levels/cannon-and-cross.ts +75 -0
  87. package/src/levels/test-levels/caves-and-enemies.ts +73 -0
  88. package/src/levels/test-levels/win-game.ts +24 -0
  89. package/src/main.ts +6 -0
  90. package/src/mustachi-game-context.ts +35 -0
  91. package/src/shared/app-code.ts +106 -0
  92. package/src/shared/constants.ts +1 -0
  93. package/src/shared/game-context.ts +547 -0
  94. package/src/shared/game-objects/game-object.ts +28 -0
  95. package/src/shared/game-objects/moving-game-object.ts +46 -0
  96. package/src/shared/game-objects/rotating-game-object.ts +58 -0
  97. package/src/shared/game-objects/updating-game-object.ts +7 -0
  98. package/src/shared/player.ts +73 -0
  99. package/src/shared/types.ts +21 -0
  100. package/tsconfig.json +26 -0
  101. 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
+ }
@@ -0,0 +1,7 @@
1
+ import type { collision } from "../types";
2
+ import { GameObject } from "./game-object";
3
+
4
+ export abstract class UpdatingGameObject extends GameObject {
5
+ acceptsCollision = true;
6
+ abstract update(collisions: collision[]): void;
7
+ }