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,482 @@
1
+ import type { GameContext } from "../../shared/game-context";
2
+ import { Enemy } from "./point-objects/enemies/enemy";
3
+ import { Flag } from "./set-pieces/flag";
4
+ import { FireStache } from "./point-objects/items/fire-stache";
5
+ import { Item } from "./point-objects/items/item";
6
+ import { Stacheroom } from "./point-objects/items/stacheroom";
7
+ import { EnemyProjectile } from "./projectiles/enemy-projectiles/enemy-projectile";
8
+ import { FallingFloor } from "./set-pieces/obstacles/blocks/falling-floor";
9
+ import { WarpPipe } from "./set-pieces/obstacles/warp-pipe";
10
+ import { Obstacle } from "./set-pieces/obstacles/obstacle";
11
+ import { SetPiece } from "./set-pieces/set-piece";
12
+ import { Player } from "../../shared/player";
13
+ import { direction, type collision } from "../../shared/types";
14
+ import { StacheBall } from "./projectiles/stache-ball";
15
+ import { BLOCK_SIZE } from "../../shared/constants";
16
+ import { FireBar } from "./projectiles/enemy-projectiles/fire-bar";
17
+ import { PunchableBlock } from "./set-pieces/obstacles/blocks/punchable-blockS/punchable-block";
18
+ import { ItemBlock } from "./set-pieces/obstacles/blocks/punchable-blockS/item-block";
19
+ import { Brick } from "./set-pieces/obstacles/blocks/punchable-blockS/brick";
20
+ import { Floor } from "./set-pieces/obstacles/floor";
21
+ import { StacheSeed } from "./point-objects/enemies/stache-seed";
22
+ import { UpdatingGameObject } from "../../shared/game-objects/updating-game-object";
23
+ import { CaveWall } from "./set-pieces/obstacles/blocks/cave-wall";
24
+ import mustachio from "../../assets/Mustachio.webp";
25
+ import mustachioFacingLeft from "../../assets/Mustachio_FacingLeft.webp";
26
+ import mustachioFacingRight from "../../assets/Mustachio_FacingRight.webp";
27
+ import mustachioFire from "../../assets/Mustachio_Fire.webp";
28
+ import mustachioFacingLeftFire from "../../assets/Mustachio_FacingLeft_Fire.webp";
29
+ import mustachioFacingRightFire from "../../assets/Mustachio_FacingRight_Fire.webp";
30
+
31
+ export class Mustachio extends Player {
32
+ private readonly image = new Image();
33
+ private isFire = false;
34
+ private isBig = false;
35
+ private hitTimer: number | null = null;
36
+ private warpPipe: WarpPipe | null = null;
37
+ private canFire = true;
38
+ private crouched = false;
39
+ protected ignoreUpdate = false;
40
+
41
+ constructor(gameContext: GameContext, x: number, y: number) {
42
+ super(gameContext, {
43
+ x,
44
+ y,
45
+ width: BLOCK_SIZE * 0.66,
46
+ height: BLOCK_SIZE * 0.66,
47
+ });
48
+
49
+ this.image.src = mustachio;
50
+ }
51
+
52
+ draw(ctx: CanvasRenderingContext2D) {
53
+ ctx.drawImage(
54
+ this.image,
55
+ this.rect.x,
56
+ this.rect.y,
57
+ this.rect.width,
58
+ this.rect.height,
59
+ );
60
+ }
61
+
62
+ update(collisions: collision[]): void {
63
+ if (this.ignoreUpdate) {
64
+ return; // Let the goDownPipe method handle the update
65
+ }
66
+
67
+ this.blockedDirHor = direction.NONE;
68
+ this.blockedDirVert = direction.NONE;
69
+ this.warpPipe = null;
70
+
71
+ for (const collision of collisions) {
72
+ this.handleCollision(collision);
73
+ }
74
+
75
+ this.handleGravity();
76
+ }
77
+
78
+ goDownPipe() {
79
+ if (this.warpPipe === null) {
80
+ return;
81
+ }
82
+
83
+ this.ignoreUpdate = true;
84
+ this.rect.x =
85
+ this.warpPipe.rect.x +
86
+ this.warpPipe.rect.width / 2 -
87
+ this.rect.width / 2 +
88
+ this.gameContext.xOffset;
89
+ this.rect.y = this.warpPipe.rect.y - this.rect.height;
90
+
91
+ let downAnimationID: number | null = null;
92
+ const downAnimation = () => {
93
+ if (!this.warpPipe) {
94
+ if (downAnimationID) {
95
+ clearInterval(downAnimationID);
96
+ }
97
+ return;
98
+ }
99
+
100
+ this.rect.y += 5;
101
+
102
+ if (this.rect.y >= this.warpPipe.rect.y) {
103
+ if (downAnimationID) {
104
+ clearInterval(downAnimationID);
105
+ }
106
+
107
+ this.warpPipe.enter();
108
+ this.warpPipe = null;
109
+ this.ignoreUpdate = false;
110
+ }
111
+ };
112
+
113
+ downAnimationID = setInterval(downAnimation, 50);
114
+ }
115
+
116
+ reset(x?: number, y?: number) {
117
+ this.rect.x = x ?? BLOCK_SIZE * 4.5;
118
+ this.rect.y = y ?? BLOCK_SIZE * 5;
119
+ }
120
+
121
+ private changeSize(isBig: boolean) {
122
+ if (this.isBig === isBig) {
123
+ return;
124
+ }
125
+
126
+ this.isBig = isBig;
127
+
128
+ let runCount = 0;
129
+ let changeID: number | null = null;
130
+ const change = () => {
131
+ if (++runCount > 3 && changeID) {
132
+ clearInterval(changeID);
133
+ }
134
+
135
+ if (this.isBig) {
136
+ this.rect.height += 10;
137
+ this.rect.y -= 10;
138
+ this.rect.width += 3;
139
+ this.rect.x -= 1.5;
140
+ } else {
141
+ this.rect.height -= 10;
142
+ this.rect.y += 10;
143
+ this.rect.width -= 3;
144
+ this.rect.x += 1.5;
145
+ }
146
+ };
147
+
148
+ changeID = setInterval(change, 115);
149
+ }
150
+
151
+ private changeFire(isFire: boolean) {
152
+ if (this.isFire === isFire) {
153
+ return;
154
+ }
155
+ this.isFire = isFire;
156
+
157
+ if (this.isFire && !this.isBig) {
158
+ this.changeSize(true);
159
+ }
160
+
161
+ let runCount = 0;
162
+ let changeID: number | null = null;
163
+ const change = () => {
164
+ if (runCount < 6) {
165
+ runCount++;
166
+ this.isFire = !this.isFire;
167
+ return;
168
+ }
169
+
170
+ if (changeID) {
171
+ clearInterval(changeID);
172
+ }
173
+ };
174
+
175
+ changeID = setInterval(change, 115);
176
+ }
177
+
178
+ protected handleCollision(collision: collision) {
179
+ if (
180
+ collision.gameObject instanceof UpdatingGameObject &&
181
+ !collision.gameObject.acceptsCollision
182
+ ) {
183
+ return;
184
+ }
185
+
186
+ this.handleCollisionGameObject(collision);
187
+ this.handleCollisionDirection(collision);
188
+ }
189
+
190
+ private handleCollisionDirection(collision: collision) {
191
+ if (
192
+ collision.gameObject instanceof ItemBlock &&
193
+ collision.gameObject.hidden
194
+ ) {
195
+ return;
196
+ }
197
+
198
+ if (collision.gameObject instanceof Item) {
199
+ return;
200
+ }
201
+ const cRect = collision.gameObject.rect;
202
+
203
+ if (
204
+ collision.gameObject instanceof Floor ||
205
+ (collision.gameObject instanceof CaveWall && this.rect.y < cRect.y)
206
+ ) {
207
+ if (this.rect.x + this.rect.width < cRect.x + this.gameContext.xOffset) {
208
+ collision.collisionDirection = direction.LEFT;
209
+ } else if (
210
+ this.rect.x >
211
+ cRect.x + cRect.width + this.gameContext.xOffset
212
+ ) {
213
+ collision.collisionDirection = direction.RIGHT;
214
+ } else {
215
+ collision.collisionDirection = direction.DOWN;
216
+ }
217
+ }
218
+
219
+ let allowVert =
220
+ this.rect.x + this.rect.width > cRect.x + 10 + this.gameContext.xOffset;
221
+ allowVert =
222
+ allowVert &&
223
+ this.rect.x < cRect.x + cRect.width - 10 + this.gameContext.xOffset;
224
+
225
+ if (allowVert && collision.collisionDirection === direction.DOWN) {
226
+ this.blockedDirVert = direction.DOWN;
227
+ this.landOnGameObject(collision.gameObject);
228
+ } else if (allowVert && collision.collisionDirection === direction.UP) {
229
+ this.speedY = 1;
230
+ this.rect.y = cRect.y + cRect.height - 1;
231
+ this.blockedDirVert = direction.UP;
232
+ } else if (
233
+ collision.collisionDirection === direction.LEFT &&
234
+ this.gameContext.currentDir === direction.LEFT
235
+ ) {
236
+ this.speedX = 0;
237
+ this.rect.x = cRect.x - this.rect.width + 1 + this.gameContext.xOffset;
238
+ this.blockedDirHor = direction.LEFT;
239
+ } else if (
240
+ collision.collisionDirection === direction.RIGHT &&
241
+ this.gameContext.currentDir === direction.RIGHT
242
+ ) {
243
+ this.speedX = 0;
244
+ this.rect.x = cRect.x + cRect.width - 1 + this.gameContext.xOffset;
245
+ this.blockedDirHor = direction.RIGHT;
246
+ }
247
+ }
248
+
249
+ private handleCollisionGameObject(collision: collision) {
250
+ const gameObject = collision.gameObject;
251
+
252
+ // FireBar is a special case since it rotates
253
+ // so it isnt an EnemyProjectile technically
254
+ if (
255
+ gameObject instanceof FireBar ||
256
+ gameObject instanceof EnemyProjectile
257
+ ) {
258
+ this.playerHit();
259
+ return;
260
+ }
261
+
262
+ if (gameObject instanceof SetPiece) {
263
+ this.handleCollisionSetPiece(collision.collisionDirection, gameObject);
264
+ return;
265
+ }
266
+
267
+ // If the gameObject is an enemy
268
+ if (gameObject instanceof Enemy) {
269
+ this.handleCollisionEnemy(collision.collisionDirection, gameObject);
270
+ return;
271
+ }
272
+
273
+ // If the gameobject is an item, collect it and act accordingly
274
+ if (gameObject instanceof Item) {
275
+ this.handleCollisionItem(gameObject);
276
+ return;
277
+ }
278
+ }
279
+
280
+ private handleCollisionItem(item: Item) {
281
+ item.collect();
282
+
283
+ if (item instanceof Stacheroom) {
284
+ this.changeSize(true);
285
+ } else if (item instanceof FireStache) {
286
+ this.changeFire(true);
287
+ }
288
+ }
289
+
290
+ private handleCollisionEnemy(dir: number, enemy: Enemy) {
291
+ if (enemy instanceof StacheSeed) {
292
+ if (!enemy.isDead && !enemy.inPipe) {
293
+ this.playerHit();
294
+ }
295
+ } else if (dir !== direction.DOWN) {
296
+ if (!enemy.isDead) {
297
+ this.playerHit();
298
+ }
299
+
300
+ return;
301
+ } else if (dir === direction.DOWN) {
302
+ this.landOnGameObject(enemy);
303
+ enemy.enemyHit();
304
+ }
305
+ }
306
+
307
+ private handleCollisionSetPiece(dir: number, setPiece: SetPiece) {
308
+ // The Flag is the goal
309
+ if (setPiece instanceof Flag) {
310
+ this.winGame(setPiece);
311
+ return;
312
+ }
313
+
314
+ // If you are on top of a setPiece, set speedY to 0
315
+ // and set the rect.y to the top of the setPiece
316
+ if (dir === direction.DOWN && setPiece instanceof Obstacle) {
317
+ this.handleCollisionObstacle(setPiece);
318
+ } else if (
319
+ this.speedY <= 0 &&
320
+ dir === direction.UP &&
321
+ setPiece instanceof PunchableBlock
322
+ ) {
323
+ if (!(setPiece instanceof Brick) || this.isBig) {
324
+ setPiece.punch();
325
+ }
326
+
327
+ this.speedY = 1;
328
+ }
329
+ }
330
+
331
+ private handleCollisionObstacle(obstacle: Obstacle) {
332
+ if (obstacle instanceof FallingFloor) {
333
+ obstacle.startFall();
334
+ }
335
+
336
+ if (obstacle instanceof WarpPipe) {
337
+ this.warpPipe = obstacle;
338
+ }
339
+ }
340
+
341
+ private toggleCrouch(crouch: boolean) {
342
+ if (this.crouched === crouch) {
343
+ return;
344
+ }
345
+
346
+ this.crouched = crouch;
347
+ let modifier: number;
348
+ if (crouch) {
349
+ modifier = -(this.rect.height / 2);
350
+ } else {
351
+ modifier = this.rect.height;
352
+ }
353
+
354
+ this.rect.y -= modifier;
355
+ this.rect.height += modifier;
356
+ }
357
+
358
+ playerHit() {
359
+ if (this.hitTimer !== null) {
360
+ return;
361
+ }
362
+
363
+ if (this.isFire) {
364
+ this.changeFire(false);
365
+ } else if (this.isBig) {
366
+ this.toggleCrouch(false);
367
+ this.changeSize(false);
368
+ } else {
369
+ this.playerKill();
370
+ }
371
+
372
+ this.hitTimer = setTimeout(() => {
373
+ this.hitTimer = null;
374
+ }, 1000);
375
+ }
376
+
377
+ playerKill() {
378
+ this.gameContext.setGameOver();
379
+ this.ignoreUpdate = true;
380
+ this.image.src = mustachio;
381
+
382
+ setTimeout(() => {
383
+ this.speedY = -5;
384
+
385
+ const deathAnimationTimeout = setInterval(() => {
386
+ this.rect.y += this.speedY;
387
+ this.speedY += 0.06;
388
+
389
+ if (this.rect.y > this.gameContext.gameArea.height) {
390
+ clearInterval(deathAnimationTimeout);
391
+ this.gameContext.removeGameObject(this);
392
+ this.gameContext.stopMainLoop();
393
+ }
394
+ });
395
+ }, 1000);
396
+ }
397
+
398
+ customKeyDown(key: string): void {
399
+ if (key === "") {
400
+ if (!this.isFire || !this.canFire) {
401
+ return;
402
+ }
403
+
404
+ this.canFire = false;
405
+ setTimeout(() => {
406
+ this.canFire = true;
407
+ }, 250);
408
+
409
+ const fire = new StacheBall(
410
+ this.gameContext,
411
+ this.rect.x + this.rect.width + 5,
412
+ this.rect.y + this.rect.height / 2,
413
+ );
414
+
415
+ fire.speedX = this.gameContext.currentDir === direction.LEFT ? -5 : 5;
416
+ fire.speedY = 0;
417
+
418
+ this.gameContext.addGameObject(fire);
419
+ } else if (key === "arrowdown" || key === "s") {
420
+ if (this.warpPipe) {
421
+ if (this.isFire) {
422
+ this.image.src = mustachioFire;
423
+ } else {
424
+ this.image.src = mustachio;
425
+ }
426
+
427
+ this.goDownPipe();
428
+ } else {
429
+ this.toggleCrouch(true);
430
+ }
431
+ } else if (key === "arrowleft" || key === "a") {
432
+ if (this.isFire) {
433
+ this.image.src = mustachioFacingLeftFire;
434
+ } else {
435
+ this.image.src = mustachioFacingLeft;
436
+ }
437
+ } else if (key === "arrowright" || key === "d") {
438
+ if (this.isFire) {
439
+ this.image.src = mustachioFacingRightFire;
440
+ } else {
441
+ this.image.src = mustachioFacingRight;
442
+ }
443
+ }
444
+ }
445
+
446
+ customKeyUp(key: string): void {
447
+ if (key === "arrowdown" || key === "s") {
448
+ this.toggleCrouch(false);
449
+ }
450
+ }
451
+
452
+ winGame(flag: Flag) {
453
+ this.ignoreUpdate = true;
454
+ this.gameContext.setGameOver();
455
+ this.image.src = mustachioFacingRight;
456
+
457
+ const targetX =
458
+ flag.rect.x +
459
+ flag.rect.width / 2 -
460
+ this.rect.width / 1.5 +
461
+ this.gameContext.xOffset;
462
+
463
+ const winAnimation = setInterval(() => {
464
+ if (this.rect.y + this.rect.height < BLOCK_SIZE * 17) {
465
+ this.rect.y += this.speedY;
466
+ this.speedY += this.gameContext.gravity;
467
+ }
468
+
469
+ if (this.rect.x > targetX) {
470
+ clearInterval(winAnimation);
471
+ this.image.src = mustachio;
472
+ setTimeout(() => {
473
+ flag.closeDoor();
474
+ this.gameContext.removeGameObject(this);
475
+ this.gameContext.win();
476
+ }, 1000);
477
+ }
478
+
479
+ this.rect.x += 1.5;
480
+ }, 5);
481
+ }
482
+ }
@@ -0,0 +1,64 @@
1
+ import type { rectangle } from "../../../../shared/types";
2
+ import { PointObject } from "../point-item";
3
+ import type { GameContext } from "../../../../shared/game-context";
4
+
5
+ export abstract class Enemy extends PointObject {
6
+ isDead: boolean = false;
7
+ protected image: HTMLImageElement = new Image();
8
+ protected imageSources: string[] = [];
9
+ protected imageSourceIndex: number = 0;
10
+ protected shotTimer: number | null = null;
11
+ private imageTimer: number | null = null;
12
+
13
+ constructor(
14
+ gameContext: GameContext,
15
+ rect: rectangle,
16
+ useBothImages: boolean = true,
17
+ ) {
18
+ super(gameContext, rect);
19
+
20
+ if (useBothImages) {
21
+ this.imageTimer = setInterval(() => {
22
+ this.setNextImage();
23
+ }, 250);
24
+ }
25
+ }
26
+
27
+ setNextImage() {
28
+ this.imageSourceIndex++;
29
+ if (this.imageSourceIndex >= this.imageSources.length) {
30
+ this.imageSourceIndex = 0;
31
+ }
32
+ this.image.src = this.imageSources[this.imageSourceIndex];
33
+ }
34
+
35
+ enemyHit() {
36
+ if (this.isDead) {
37
+ return;
38
+ }
39
+
40
+ this.isDead = true;
41
+ this.speedX = 0;
42
+ this.speedY = 0;
43
+ this.rect.y += this.rect.height / 2;
44
+ this.rect.height /= 2;
45
+ this.gameContext.addScore(this.pointValue);
46
+
47
+ setTimeout(() => {
48
+ this.gameContext.removeGameObject(this);
49
+ }, 500);
50
+ }
51
+
52
+ dispose() {
53
+ if (this.shotTimer) {
54
+ clearTimeout(this.shotTimer);
55
+ clearInterval(this.shotTimer);
56
+ this.shotTimer = null;
57
+ }
58
+
59
+ if (this.imageTimer) {
60
+ clearInterval(this.imageTimer);
61
+ this.imageTimer = null;
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,124 @@
1
+ import { direction, type collision } from "../../../../shared/types";
2
+ import { Enemy } from "./enemy";
3
+ import type { GameContext } from "../../../../shared/game-context";
4
+ import type { GameObject } from "../../../../shared/game-objects/game-object";
5
+ import { BLOCK_SIZE } from "../../../../shared/constants";
6
+ import stacheSeed1 from "../../../../assets/stacheSeed1.webp";
7
+ import stacheSeed2 from "../../../../assets/stacheSeed2.webp";
8
+ import stacheSeedReversed1 from "../../../../assets/stacheSeedReversed1.webp";
9
+ import stacheSeedReversed2 from "../../../../assets/stacheSeedReversed2.webp";
10
+
11
+ export class StacheSeed extends Enemy {
12
+ readonly pointValue: number = 100;
13
+ inPipe: boolean = false;
14
+
15
+ private direction: number;
16
+ private readonly parent: GameObject;
17
+ private readonly waitTime: number = 2500;
18
+ private readonly reversed: boolean;
19
+
20
+ constructor(gameContext: GameContext, parent: GameObject, reversed: boolean) {
21
+ const width = BLOCK_SIZE * 1;
22
+ const height = BLOCK_SIZE * 3;
23
+
24
+ let x: number;
25
+ let y: number;
26
+
27
+ if (reversed) {
28
+ x = parent.rect.x + parent.rect.width / 2 - width / 2;
29
+ y = parent.rect.y + parent.rect.height - height;
30
+ } else {
31
+ x = parent.rect.x + parent.rect.width / 2 - width / 2;
32
+ y = parent.rect.y;
33
+ }
34
+
35
+ super(gameContext, {
36
+ x,
37
+ y,
38
+ width,
39
+ height,
40
+ });
41
+
42
+ this.reversed = reversed;
43
+ this.parent = parent;
44
+ this.speedY = 1.5;
45
+ if (reversed) {
46
+ this.direction = direction.DOWN;
47
+ this.imageSources.push(stacheSeedReversed1, stacheSeedReversed2);
48
+ } else {
49
+ this.direction = direction.UP;
50
+ this.imageSources.push(stacheSeed1, stacheSeed2);
51
+ }
52
+ }
53
+
54
+ // collisions with this enemy are handled by the player class
55
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
56
+ update(_: collision[]): void {
57
+ if (this.direction === direction.UP) {
58
+ this.rect.y -= this.speedY;
59
+
60
+ if (this.shouldChangeDirection()) {
61
+ this.direction = direction.NONE;
62
+ if (this.reversed) {
63
+ this.inPipe = true;
64
+ }
65
+
66
+ setTimeout(() => {
67
+ this.inPipe = false;
68
+ this.direction = direction.DOWN;
69
+ }, this.waitTime);
70
+ }
71
+ } else if (this.direction === direction.DOWN) {
72
+ this.rect.y += this.speedY;
73
+
74
+ if (this.shouldChangeDirection()) {
75
+ this.direction = direction.NONE;
76
+ if (!this.reversed) {
77
+ this.inPipe = true;
78
+ }
79
+
80
+ setTimeout(() => {
81
+ this.inPipe = false;
82
+ this.direction = direction.UP;
83
+ }, this.waitTime);
84
+ }
85
+ }
86
+
87
+ if (this.rect.y < 0) {
88
+ this.isDead = true;
89
+ }
90
+ }
91
+
92
+ draw(ctx: CanvasRenderingContext2D) {
93
+ ctx.drawImage(
94
+ this.image,
95
+ this.rect.x + this.gameContext.xOffset,
96
+ this.rect.y,
97
+ this.rect.width,
98
+ this.rect.height,
99
+ );
100
+ }
101
+
102
+ private shouldChangeDirection(): boolean {
103
+ if (this.reversed) {
104
+ if (this.direction === direction.UP) {
105
+ return (
106
+ this.rect.y + this.rect.height <
107
+ this.parent.rect.y + this.parent.rect.height
108
+ );
109
+ }
110
+ if (this.direction === direction.DOWN) {
111
+ return this.rect.y > this.parent.rect.y + this.parent.rect.height;
112
+ }
113
+ } else {
114
+ if (this.direction === direction.UP) {
115
+ return this.rect.y + this.rect.height < this.parent.rect.y;
116
+ }
117
+ if (this.direction === direction.DOWN) {
118
+ return this.rect.y > this.parent.rect.y;
119
+ }
120
+ }
121
+
122
+ return false;
123
+ }
124
+ }