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