@dryanovski/gamefoo 0.2.3 → 0.2.5

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 (84) hide show
  1. package/dist/core/animate.js +147 -0
  2. package/dist/core/asset.js +74 -0
  3. package/dist/core/behaviour.js +88 -0
  4. package/dist/core/behaviours/collidable.js +186 -0
  5. package/dist/core/behaviours/control.js +75 -0
  6. package/dist/core/behaviours/healtkit.js +153 -0
  7. package/dist/core/behaviours/sprite_render.js +193 -0
  8. package/dist/core/camera.js +134 -0
  9. package/dist/core/engine.d.ts +1 -1
  10. package/dist/core/engine.d.ts.map +1 -1
  11. package/dist/core/engine.js +527 -0
  12. package/dist/core/fonts/font_bitmap.js +205 -0
  13. package/dist/core/fonts/font_bitmap_prebuild.js +137 -0
  14. package/dist/core/fonts/internal/font_3x5.js +169 -0
  15. package/dist/core/fonts/internal/font_4x6.js +171 -0
  16. package/dist/core/fonts/internal/font_5x5.js +129 -0
  17. package/dist/core/fonts/internal/font_6x8.js +171 -0
  18. package/dist/core/fonts/internal/font_8x13.js +171 -0
  19. package/dist/core/fonts/internal/font_8x8.js +171 -0
  20. package/dist/core/game_object_register.js +134 -0
  21. package/dist/core/input.js +170 -0
  22. package/dist/core/sprite.js +222 -0
  23. package/dist/core/utils/perlin_noise.js +183 -0
  24. package/dist/core/world.js +304 -0
  25. package/dist/debug/monitor.js +47 -0
  26. package/dist/decorators/index.js +1 -0
  27. package/dist/decorators/log.js +42 -0
  28. package/dist/entities/dynamic_entity.js +99 -0
  29. package/dist/entities/entity.js +283 -0
  30. package/dist/entities/player.js +93 -0
  31. package/dist/entities/text.js +62 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +47 -29
  35. package/dist/subsystems/camera_system.d.ts +2 -2
  36. package/dist/subsystems/camera_system.d.ts.map +1 -1
  37. package/dist/subsystems/camera_system.js +41 -0
  38. package/dist/subsystems/collision_system.js +17 -0
  39. package/dist/subsystems/monitor_system.js +20 -0
  40. package/dist/subsystems/object_system.d.ts +1 -1
  41. package/dist/subsystems/object_system.d.ts.map +1 -1
  42. package/dist/subsystems/object_system.js +29 -0
  43. package/dist/subsystems/types.d.ts +1 -1
  44. package/dist/subsystems/types.d.ts.map +1 -1
  45. package/dist/subsystems/types.js +0 -0
  46. package/dist/types.js +10 -0
  47. package/package.json +17 -6
  48. package/src/core/animate.ts +159 -0
  49. package/src/core/asset.ts +76 -0
  50. package/src/core/behaviour.ts +145 -0
  51. package/src/core/behaviours/collidable.ts +296 -0
  52. package/src/core/behaviours/control.ts +80 -0
  53. package/src/core/behaviours/healtkit.ts +166 -0
  54. package/src/core/behaviours/sprite_render.ts +216 -0
  55. package/src/core/camera.ts +145 -0
  56. package/src/core/engine.ts +607 -0
  57. package/src/core/fonts/font_bitmap.ts +232 -0
  58. package/src/core/fonts/font_bitmap_prebuild.ts +141 -0
  59. package/src/core/fonts/internal/font_3x5.ts +178 -0
  60. package/src/core/fonts/internal/font_4x6.ts +180 -0
  61. package/src/core/fonts/internal/font_5x5.ts +137 -0
  62. package/src/core/fonts/internal/font_6x8.ts +180 -0
  63. package/src/core/fonts/internal/font_8x13.ts +180 -0
  64. package/src/core/fonts/internal/font_8x8.ts +180 -0
  65. package/src/core/game_object_register.ts +146 -0
  66. package/src/core/input.ts +182 -0
  67. package/src/core/sprite.ts +339 -0
  68. package/src/core/utils/perlin_noise.ts +196 -0
  69. package/src/core/world.ts +331 -0
  70. package/src/debug/monitor.ts +60 -0
  71. package/src/decorators/index.ts +1 -0
  72. package/src/decorators/log.ts +45 -0
  73. package/src/entities/dynamic_entity.ts +106 -0
  74. package/src/entities/entity.ts +322 -0
  75. package/src/entities/player.ts +99 -0
  76. package/src/entities/text.ts +72 -0
  77. package/src/index.ts +51 -0
  78. package/src/subsystems/camera_system.ts +52 -0
  79. package/src/subsystems/collision_system.ts +21 -0
  80. package/src/subsystems/monitor_system.ts +26 -0
  81. package/src/subsystems/object_system.ts +37 -0
  82. package/src/subsystems/types.ts +46 -0
  83. package/src/types.ts +178 -0
  84. package/dist/index.js.map +0 -9
@@ -0,0 +1,607 @@
1
+ import type { SubSystem } from "../subsystems/types";
2
+
3
+ const DEFAULT_GAME_SCALE = 1;
4
+
5
+ /**
6
+ * Configuration options for the {@link Engine}.
7
+ *
8
+ * Passed to the `Engine` constructor to customise visual defaults.
9
+ * Every property is optional; sensible defaults are applied internally.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const config: EngineConfig = {
14
+ * backgroundColor: "#1a1a2e",
15
+ * };
16
+ *
17
+ * const engine = new Engine("game", 800, 600, config);
18
+ * ```
19
+ */
20
+ interface EngineConfig {
21
+ /**
22
+ * The CSS colour string used to clear the canvas each frame.
23
+ *
24
+ * Accepts any value valid for {@link CanvasRenderingContext2D.fillStyle}
25
+ * (hex, `rgb()`, `hsl()`, named colours, etc.).
26
+ *
27
+ * @defaultValue `"#000000"`
28
+ */
29
+ backgroundColor?: string;
30
+
31
+ /**
32
+ * Global scale factor applied to the canvas via CSS transforms.
33
+ *
34
+ * @defaultValue `1` (no scaling)
35
+ */
36
+ gameScale?: number;
37
+ }
38
+
39
+ /**
40
+ * Core game engine responsible for the game loop, rendering pipeline,
41
+ * entity management, collision detection, and camera tracking.
42
+ *
43
+ * `Engine` owns a single HTML `<canvas>` element and drives a
44
+ * `requestAnimationFrame`-based loop that, on every tick:
45
+ *
46
+ * 1. Computes **deltaTime** (seconds since the previous frame).
47
+ * 2. Calls {@link Engine.update | update} — advances all entities and runs
48
+ * collision detection via the internal {@link World}.
49
+ * 3. Calls {@link Engine.render | render} — clears the canvas and draws every
50
+ * registered entity.
51
+ *
52
+ * ---
53
+ *
54
+ * ### Lifecycle
55
+ *
56
+ * ```text
57
+ * new Engine() — creates canvas context, camera, object register, world
58
+ * │
59
+ * ▼
60
+ * setup(fn) — runs the user-supplied initialiser, then starts the loop
61
+ * │
62
+ * ▼
63
+ * ┌─► loop(timestamp) — called every frame via requestAnimationFrame
64
+ * │ ├─ update(dt)
65
+ * │ └─ render()
66
+ * │ │
67
+ * └─────────┘
68
+ * │
69
+ * pause() / clear() / destroy() — stops or tears down the engine
70
+ * ```
71
+ *
72
+ * ---
73
+ *
74
+ * ### Minimal example
75
+ *
76
+ * ```ts
77
+ * import { Engine, Player } from "gamefoo";
78
+ *
79
+ * const engine = new Engine("game", 800, 600, {
80
+ * backgroundColor: "#1a1a2e",
81
+ * });
82
+ *
83
+ * const player = new Player("hero", 400, 300, 50, 50);
84
+ * engine.player = player;
85
+ *
86
+ * engine.setup(() => {
87
+ * console.log("Game started!");
88
+ * });
89
+ * ```
90
+ *
91
+ * ### Adding game objects
92
+ *
93
+ * ```ts
94
+ * import { Engine, DynamicEntity } from "gamefoo";
95
+ *
96
+ * const engine = new Engine("game", 800, 600, {});
97
+ *
98
+ * class Crate extends DynamicEntity {
99
+ * constructor(x: number, y: number) {
100
+ * super("crate", x, y, 32, 32);
101
+ * }
102
+ * override update(_dt: number) {}
103
+ * override render(ctx: CanvasRenderingContext2D) {
104
+ * ctx.fillStyle = "#8B4513";
105
+ * ctx.fillRect(this.x, this.y, 32, 32);
106
+ * }
107
+ * }
108
+ *
109
+ * engine.attachObjects(new Crate(200, 150));
110
+ *
111
+ * engine.setup(() => {
112
+ * console.log("Crate placed!");
113
+ * });
114
+ * ```
115
+ *
116
+ * @see {@link Camera} — viewport tracking
117
+ * @see {@link GameObjectRegister} — entity storage
118
+ * @see {@link World} — collision detection
119
+ */
120
+ export default class Engine {
121
+ /**
122
+ * The underlying `<canvas>` DOM element retrieved by its `id` during
123
+ * construction.
124
+ */
125
+ private canvas: HTMLCanvasElement;
126
+
127
+ /**
128
+ * The 2-D rendering context obtained from {@link Engine.canvas}.
129
+ * Used by {@link Engine.render} and exposed indirectly to game objects.
130
+ */
131
+ private ctx: CanvasRenderingContext2D;
132
+
133
+ /**
134
+ * Timestamp (in milliseconds) of the previous animation frame.
135
+ * Used internally to calculate `deltaTime` between frames.
136
+ *
137
+ * Reset to `0` when the engine is set up via {@link Engine.setup}.
138
+ */
139
+ private lastTime: number = 0;
140
+
141
+ /**
142
+ * The logical width of the game world (and the canvas), in pixels.
143
+ *
144
+ * Set once during construction and also used by the {@link Camera}.
145
+ */
146
+ private width: number;
147
+
148
+ /**
149
+ * The logical height of the game world (and the canvas), in pixels.
150
+ *
151
+ * Set once during construction and also used by the {@link Camera}.
152
+ */
153
+ private height: number;
154
+
155
+ /**
156
+ * Guards against calling {@link Engine.setup} more than once.
157
+ * Flipped to `true` after the first successful setup invocation.
158
+ */
159
+ private _initialized: boolean = false;
160
+
161
+ /**
162
+ * Whether the game loop is actively requesting new frames.
163
+ *
164
+ * - Set to `true` inside {@link Engine.setup}.
165
+ * - Set to `false` by {@link Engine.pause} or {@link Engine.clear}.
166
+ */
167
+ private running: boolean = false;
168
+
169
+ /**
170
+ * Merged engine configuration. Combines user-supplied values with internal
171
+ * defaults (`backgroundColor: "#000000"`).
172
+ */
173
+ private cnf: EngineConfig = {
174
+ backgroundColor: "#000000",
175
+
176
+ /**
177
+ * Global scale factor applied to the canvas via CSS transforms.
178
+ */
179
+ gameScale: 1,
180
+ };
181
+
182
+ /**
183
+ * Global scale factor applied to the canvas via CSS transforms.
184
+ *
185
+ * This does not affect the internal resolution (`width` / `height`) or the
186
+ * camera viewport, but simply scales the rendered output for display.
187
+ *
188
+ */
189
+ public gameScale: number = DEFAULT_GAME_SCALE;
190
+
191
+ /**
192
+ * The main subsystems of the engine, responsible for core functionalities
193
+ * like rendering, object management, collision detection, and monitoring.
194
+ *
195
+ * Subsystems are executed in a specific order determined by their `order` property
196
+ */
197
+ private subsystems: SubSystem[] = [];
198
+
199
+ /**
200
+ * Creates a new `Engine` instance, binds it to a `<canvas>` element,
201
+ * and initialises the camera, object register, and collision world.
202
+ *
203
+ * @param canvasId - The DOM `id` attribute of the `<canvas>` element to
204
+ * render into. Must already exist in the document.
205
+ * @param width - Logical width of the game area in pixels. Sets both
206
+ * `canvas.width` and the camera viewport width.
207
+ * @param height - Logical height of the game area in pixels. Sets both
208
+ * `canvas.height` and the camera viewport height.
209
+ * @param config - Optional overrides for engine-level settings.
210
+ * See {@link EngineConfig}.
211
+ *
212
+ * @throws {Error} If no 2-D rendering context can be obtained from the
213
+ * canvas (e.g. the browser does not support Canvas 2D).
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * const engine = new Engine("game", 800, 600, {
218
+ * backgroundColor: "#1a1a2e",
219
+ * });
220
+ * ```
221
+ */
222
+ constructor(canvasId: string, width: number, height: number, config: EngineConfig) {
223
+ this.canvas = document.getElementById(canvasId) as HTMLCanvasElement;
224
+ this.height = height;
225
+ this.width = width;
226
+ this.canvas.width = width;
227
+ this.canvas.height = height;
228
+ const context = this.canvas.getContext("2d");
229
+ if (!context) {
230
+ throw new Error("Failed to get 2D context");
231
+ }
232
+ this.ctx = context;
233
+
234
+ this.cnf = { ...this.cnf, ...config };
235
+
236
+ this.gameScale = this.cnf.gameScale || DEFAULT_GAME_SCALE;
237
+
238
+ /**
239
+ * Make sure to always pixelate the canvas to preserve crisp edges for pixel art.
240
+ */
241
+ this.canvas.style.imageRendering = "pixelated";
242
+ this.canvas.style.imageRendering = "-moz-crisp-edges";
243
+ this.canvas.style.imageRendering = "crisp-edges";
244
+
245
+ this.canvas.style.width = `${this.width * this.gameScale}px`;
246
+ this.canvas.style.height = `${this.height * this.gameScale}px`;
247
+ }
248
+
249
+ /**
250
+ * Attaches a subsystem to the engine, making it part of the update and render cycles.
251
+ *
252
+ * Subsystems are executed in a specific order determined by their `order` property (lower values run first).
253
+ *
254
+ * @since 0.2.0
255
+ *
256
+ * @param subsystem - The subsystem instance to attach. Must implement the {@link SubSystem} interface.
257
+ *
258
+ * @returns The engine instance, allowing for method chaining.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * engine.use(new ObjectSystem());
263
+ * engine.use(new CollisionSystem());
264
+ * ```
265
+ */
266
+ use(subsystem: SubSystem): this {
267
+ this.subsystems.push(subsystem);
268
+ /**
269
+ * Sort subsystems by their `order` property (defaulting to `100` if not specified) to ensure they run in the correct sequence.
270
+ */
271
+ this.subsystems.sort((a, b) => (a.order || 100) - (b.order || 100));
272
+
273
+ /**
274
+ * Call the `init` method of the subsystem if it exists, passing the engine instance.
275
+ * This allows subsystems to perform any necessary setup that depends on the engine
276
+ * being available.
277
+ */
278
+ subsystem.init?.(this);
279
+ return this;
280
+ }
281
+
282
+ /**
283
+ * Runs a specific lifecycle hook on all enabled subsystems that implement it.
284
+ *
285
+ * This method is used internally by the game loop to delegate update and render calls to subsystems.
286
+ *
287
+ * @param hook - The name of the lifecycle method to invoke (e.g. "update", "render").
288
+ * @param args - Arguments to pass to the subsystem methods (e.g. `deltaTime` for updates, `ctx` for rendering).
289
+ *
290
+ * @remarks
291
+ * The method iterates over all subsystems, checks if they are enabled, and if they implement the specified hook. If so, it calls the hook method with the provided arguments.
292
+ * This allows for a flexible and modular architecture where subsystems can independently implement the lifecycle methods they need without being tightly coupled to the engine's core logic.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * // During the update phase of the game loop:
297
+ * this.run("update", deltaTime);
298
+ *
299
+ * // During the render phase of the game loop:
300
+ * this.run("render", ctx);
301
+ * ```
302
+ */
303
+ private run<K extends keyof SubSystem>(hook: K, ...args: any[]): void {
304
+ for (const subsystem of this.subsystems) {
305
+ /**
306
+ * Check if the subsystem is enabled before attempting to call the hook method. If `enabled` is explicitly set to `false`, skip this subsystem.
307
+ */
308
+ if (subsystem.enabled === false) continue;
309
+ const fun = subsystem[hook];
310
+
311
+ if (typeof fun === "function") {
312
+ // @ts-expect-error
313
+ fun.apply(subsystem, args);
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Resizes the canvas via CSS transforms so it fits within its parent
320
+ * container while preserving the original aspect ratio.
321
+ *
322
+ * Call this in a `window` `resize` event listener to keep the game
323
+ * centred and properly scaled in responsive layouts.
324
+ *
325
+ * @remarks
326
+ * The method calculates the uniform scale factor from the container
327
+ * dimensions and centres the canvas with a CSS `translate + scale`
328
+ * transform. The internal resolution (`width` / `height`) remains
329
+ * unchanged.
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * window.addEventListener("resize", () => engine.handleResize());
334
+ * ```
335
+ */
336
+ handleResize() {
337
+ if (!this.canvas) return;
338
+
339
+ const container = this.canvas.parentElement;
340
+ if (!container) return;
341
+
342
+ const containerWidth = container.clientWidth;
343
+ const containerHeight = container.clientHeight;
344
+
345
+ const scaleX = containerWidth / this.width;
346
+ const scaleY = containerHeight / this.height;
347
+ const scale = Math.min(scaleX, scaleY);
348
+ const offsetX = (containerWidth - this.width * scale) / 2;
349
+ const offsetY = (containerHeight - this.height * scale) / 2;
350
+
351
+ this.canvas.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
352
+ }
353
+
354
+ /**
355
+ * Directly sets the canvas pixel dimensions.
356
+ *
357
+ * Unlike {@link Engine.handleResize}, this changes the **actual**
358
+ * resolution of the canvas (clearing its contents in the process).
359
+ *
360
+ * @param width - New canvas width in pixels.
361
+ * @param height - New canvas height in pixels.
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * engine.resize(1024, 768);
366
+ * ```
367
+ */
368
+ resize(width: number, height: number): void {
369
+ this.canvas.width = width;
370
+ this.canvas.height = height;
371
+ }
372
+
373
+ /**
374
+ * Binds the `loop` method to the current `this` context so it can be passed directly to
375
+ * `requestAnimationFrame` without losing the correct reference to the engine instance.
376
+ *
377
+ * Also prevent the need of GC to create a new function on each frame, which could lead to performance issues over time.
378
+ */
379
+ private boundLoop = this.loop.bind(this);
380
+
381
+ /**
382
+ * The core animation loop driven by `requestAnimationFrame`.
383
+ *
384
+ * Each iteration:
385
+ * 1. Computes **deltaTime** (seconds since the last frame).
386
+ * 2. Delegates to {@link Engine.update} and {@link Engine.render}.
387
+ * 3. Schedules itself for the next frame (unless `running` is `false`).
388
+ *
389
+ * @param timestamp - The high-resolution timestamp provided by
390
+ * `requestAnimationFrame`, in milliseconds.
391
+ *
392
+ * @internal
393
+ */
394
+ private loop(timestamp: number) {
395
+ /**
396
+ * Prevent the loop from running if the engine is paused or cleared. The loop
397
+ * is only restarted by calling {@link Engine.setup} on a new engine instance.
398
+ */
399
+ if (!this.running) {
400
+ return;
401
+ }
402
+
403
+ /**
404
+ * Calculate deltaTime (in seconds) since the last frame. On the first frame,
405
+ * `lastTime` is `0`, so we set it to the current timestamp to avoid a large
406
+ * delta on the first update.
407
+ */
408
+ if (this.lastTime === 0) {
409
+ this.lastTime = timestamp;
410
+ }
411
+
412
+ /**
413
+ * @remarks
414
+ * - `timestamp` is provided by `requestAnimationFrame` and is in milliseconds. Dividing by `1000`
415
+ * converts it to seconds, which is a more common unit for game logic.
416
+ * - The first frame is handled specially to avoid a large deltaTime value that would occur if we calculated it as `(timestamp - 0) / 1000`.
417
+ * - This allows the game loop to adapt to varying frame rates, ensuring that game logic runs at a consistent pace regardless of how fast or slow the frames are rendered.
418
+ * - In summary, this block of code is crucial for maintaining smooth and consistent game updates by accurately tracking the time elapsed between frames and providing that information to the update logic.
419
+ */
420
+ const deltaTime = (timestamp - this.lastTime) / 1000;
421
+ this.lastTime = timestamp;
422
+
423
+ this.run("preUpdate", deltaTime);
424
+ this.run("update", deltaTime);
425
+ this.run("postUpdate", deltaTime);
426
+
427
+ /**
428
+ * Call the main `update` method of the engine, which can be overridden by subclasses to
429
+ * implement custom update logic that runs every frame.
430
+ * This is where you would put any global game logic that needs to run independently of
431
+ * individual game objects.
432
+ */
433
+ this.update(deltaTime);
434
+
435
+ /**
436
+ * Clear the canvas at the start of each frame to prepare for fresh rendering.
437
+ */
438
+ this.clearScrean();
439
+
440
+ /**
441
+ * Call the main `render` method of the engine, which can be overridden by subclasses to
442
+ * implement custom rendering logic that runs every frame.
443
+ */
444
+ this.render(this.ctx);
445
+
446
+ this.run("preRender", this.ctx);
447
+ this.run("render", this.ctx);
448
+ this.run("postRender", this.ctx);
449
+
450
+ /**
451
+ * Schedule the next frame by calling `requestAnimationFrame` with the `loop` method as the callback.
452
+ * This creates a continuous animation loop that keeps the game running until `running` is set to `false`.
453
+ */
454
+ requestAnimationFrame(this.boundLoop);
455
+ }
456
+
457
+ /**
458
+ * Initialises the engine and starts the game loop.
459
+ *
460
+ * The supplied `setupFn` callback is invoked **synchronously** before the
461
+ * first frame. Use it to perform any last-minute setup that depends on
462
+ * the engine being ready (e.g. spawning initial entities, binding UI).
463
+ *
464
+ * Calling `setup` more than once is a no-op — a warning is logged to the
465
+ * console and the method returns immediately.
466
+ *
467
+ * @param setupFn - (optional) A synchronous callback executed once before the loop
468
+ * begins. Typically used for scene initialisation.
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * engine.setup(() => {
473
+ * console.log("Engine initialised — first frame incoming!");
474
+ * });
475
+ * ```
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * // Attempting a second setup is safely ignored:
480
+ * engine.setup(() => {}); // warns: "Engine is already initialized."
481
+ * ```
482
+ */
483
+ public async setup(setupFn?: () => void) {
484
+ if (this._initialized) {
485
+ console.warn("Engine is already initialized.");
486
+ return;
487
+ }
488
+
489
+ /**
490
+ * Handle automatically the resize of the viewport
491
+ */
492
+ if (window && typeof window.addEventListener === "function") {
493
+ window.addEventListener("resize", () => this.handleResize());
494
+ }
495
+
496
+ this.lastTime = 0;
497
+ this.clearScrean();
498
+
499
+ /**
500
+ * Setup function is optional and can be used
501
+ * to perform any last-minute initialisation that depends on the
502
+ * engine being ready (e.g. spawning initial entities, binding UI).
503
+ */
504
+ if (typeof setupFn === "function") {
505
+ setupFn();
506
+ }
507
+
508
+ this._initialized = true;
509
+ this.running = true;
510
+ requestAnimationFrame(this.boundLoop);
511
+ }
512
+
513
+ /**
514
+ * Advances the game state by one tick.
515
+ *
516
+ * Called automatically by the game loop, but is `public` so it can be
517
+ * invoked manually for deterministic / test-driven updates.
518
+ *
519
+ *
520
+ * @param deltaTime - Time elapsed since the last frame, **in seconds**.
521
+ *
522
+ * @example
523
+ * ```ts
524
+ * // Manual update (useful for unit testing):
525
+ * engine.update(1 / 60); // simulate a single 60 FPS tick
526
+ * ```
527
+ */
528
+ public update(_deltaTime: number) {}
529
+
530
+ /**
531
+ * Draws one complete frame to the canvas.
532
+ *
533
+ * Called automatically after {@link Engine.update} in the game loop, but
534
+ * is `public` for manual / debug rendering.
535
+ *
536
+ * @example
537
+ * ```ts
538
+ * // Force a single frame repaint:
539
+ * engine.render((ctx) => {
540
+ * ctx.fillStyle = "red";
541
+ * });
542
+ * ```
543
+ */
544
+ public render(_ctx: CanvasRenderingContext2D) {}
545
+
546
+ /**
547
+ * Pauses the game loop.
548
+ *
549
+ * The current frame finishes, but no further frames are scheduled.
550
+ * The canvas retains its last rendered state.
551
+ *
552
+ * Resume by calling {@link Engine.setup} on a **new** engine instance
553
+ * (the current instance cannot be restarted after pausing because
554
+ * `_initialized` remains `true`).
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * document.addEventListener("visibilitychange", () => {
559
+ * if (document.hidden) engine.pause();
560
+ * });
561
+ * ```
562
+ */
563
+ public pause() {
564
+ this.running = false;
565
+ }
566
+
567
+ /**
568
+ * Clear the screen and set default background colour.
569
+ *
570
+ * @example
571
+ * ```ts
572
+ * engine.clearScrean();
573
+ * // Canvas is now blank; loop is stopped.
574
+ * ```
575
+ */
576
+ public clearScrean() {
577
+ this.ctx.clearRect(0, 0, this.width, this.height);
578
+ this.ctx.fillStyle = this.cnf.backgroundColor || "#000000";
579
+ this.ctx.fillRect(0, 0, this.width, this.height);
580
+ }
581
+
582
+ /**
583
+ * Tears down the engine and releases resources.
584
+ *
585
+ * Use this to perform any cleanup such as removing event listeners or
586
+ * releasing external references when the engine is no longer needed.
587
+ *
588
+ * @remarks
589
+ * Currently a no-op placeholder. Extend this method when the engine
590
+ * acquires resources that need explicit cleanup (e.g. `ResizeObserver`,
591
+ * `WebSocket`, audio contexts).
592
+ *
593
+ * @example
594
+ * ```ts
595
+ * // When leaving the game screen:
596
+ * engine.destroy();
597
+ * ```
598
+ */
599
+ public destroy() {
600
+ // Clean up resources, event listeners, etc. if needed.
601
+ this.pause();
602
+
603
+ for (let i = this.subsystems.length - 1; i >= 0; i--) {
604
+ this.subsystems[i]?.destroy?.();
605
+ }
606
+ }
607
+ }