like2d 2.7.4 → 2.9.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 (77) hide show
  1. package/README.md +34 -35
  2. package/dist/core/audio.d.ts +12 -9
  3. package/dist/core/audio.d.ts.map +1 -1
  4. package/dist/core/audio.js +7 -4
  5. package/dist/core/canvas.d.ts +58 -0
  6. package/dist/core/canvas.d.ts.map +1 -0
  7. package/dist/core/canvas.js +209 -0
  8. package/dist/core/events.d.ts +92 -7
  9. package/dist/core/events.d.ts.map +1 -1
  10. package/dist/core/events.js +20 -0
  11. package/dist/core/gamepad-mapping.d.ts +57 -18
  12. package/dist/core/gamepad-mapping.d.ts.map +1 -1
  13. package/dist/core/gamepad-mapping.js +23 -223
  14. package/dist/core/gamepad.d.ts +34 -58
  15. package/dist/core/gamepad.d.ts.map +1 -1
  16. package/dist/core/gamepad.js +79 -213
  17. package/dist/core/graphics.d.ts +175 -64
  18. package/dist/core/graphics.d.ts.map +1 -1
  19. package/dist/core/graphics.js +294 -198
  20. package/dist/core/input-state.d.ts +2 -2
  21. package/dist/core/input-state.d.ts.map +1 -1
  22. package/dist/core/input.d.ts +22 -15
  23. package/dist/core/input.d.ts.map +1 -1
  24. package/dist/core/input.js +25 -37
  25. package/dist/core/keyboard.d.ts +7 -7
  26. package/dist/core/keyboard.d.ts.map +1 -1
  27. package/dist/core/keyboard.js +24 -31
  28. package/dist/core/like.d.ts +98 -44
  29. package/dist/core/like.d.ts.map +1 -1
  30. package/dist/core/like.js +8 -0
  31. package/dist/core/mouse.d.ts +45 -22
  32. package/dist/core/mouse.d.ts.map +1 -1
  33. package/dist/core/mouse.js +90 -78
  34. package/dist/core/timer.d.ts +2 -5
  35. package/dist/core/timer.d.ts.map +1 -1
  36. package/dist/core/timer.js +2 -14
  37. package/dist/engine.d.ts +61 -11
  38. package/dist/engine.d.ts.map +1 -1
  39. package/dist/engine.js +119 -102
  40. package/dist/index.d.ts +42 -21
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +35 -14
  43. package/dist/math/index.d.ts +2 -0
  44. package/dist/math/index.d.ts.map +1 -0
  45. package/dist/math/index.js +1 -0
  46. package/dist/math/rect.d.ts +71 -0
  47. package/dist/math/rect.d.ts.map +1 -0
  48. package/dist/{core → math}/rect.js +8 -0
  49. package/dist/math/vector2.d.ts +78 -0
  50. package/dist/math/vector2.d.ts.map +1 -0
  51. package/dist/{core → math}/vector2.js +24 -0
  52. package/dist/prefab-scenes/index.d.ts +7 -0
  53. package/dist/prefab-scenes/index.d.ts.map +1 -0
  54. package/dist/prefab-scenes/index.js +6 -0
  55. package/dist/prefab-scenes/startScreen.d.ts +59 -0
  56. package/dist/prefab-scenes/startScreen.d.ts.map +1 -0
  57. package/dist/{scenes/startup.js → prefab-scenes/startScreen.js} +47 -7
  58. package/dist/scene.d.ts +103 -5
  59. package/dist/scene.d.ts.map +1 -1
  60. package/dist/scene.js +28 -1
  61. package/package.json +18 -2
  62. package/dist/core/canvas-config.d.ts +0 -22
  63. package/dist/core/canvas-config.d.ts.map +0 -1
  64. package/dist/core/canvas-config.js +0 -14
  65. package/dist/core/canvas-manager.d.ts +0 -25
  66. package/dist/core/canvas-manager.d.ts.map +0 -1
  67. package/dist/core/canvas-manager.js +0 -178
  68. package/dist/core/gamepad-buttons.d.ts +0 -23
  69. package/dist/core/gamepad-buttons.d.ts.map +0 -1
  70. package/dist/core/gamepad-buttons.js +0 -36
  71. package/dist/core/rect.d.ts +0 -26
  72. package/dist/core/rect.d.ts.map +0 -1
  73. package/dist/core/vector2.d.ts +0 -26
  74. package/dist/core/vector2.d.ts.map +0 -1
  75. package/dist/gamecontrollerdb.txt +0 -2221
  76. package/dist/scenes/startup.d.ts +0 -18
  77. package/dist/scenes/startup.d.ts.map +0 -1
@@ -1,130 +1,142 @@
1
- export class Mouse {
2
- constructor(transformFn) {
3
- Object.defineProperty(this, "x", {
1
+ import { Vec2 } from '../math/vector2';
2
+ const mouseButtons = ["left", "middle", "right"];
3
+ const numToButton = (i) => mouseButtons[i];
4
+ /**
5
+ * Mouse input handling. Bound to canvas. Emits relative movement when pointer locked.
6
+ * Buttons: 1 = left, 2 = middle, 3 = right.
7
+ */
8
+ export class MouseInternal {
9
+ constructor(canvas, dispatch) {
10
+ Object.defineProperty(this, "canvas", {
4
11
  enumerable: true,
5
12
  configurable: true,
6
13
  writable: true,
7
- value: 0
14
+ value: canvas
8
15
  });
9
- Object.defineProperty(this, "y", {
16
+ Object.defineProperty(this, "dispatch", {
10
17
  enumerable: true,
11
18
  configurable: true,
12
19
  writable: true,
13
- value: 0
20
+ value: dispatch
14
21
  });
15
- Object.defineProperty(this, "buttons", {
16
- enumerable: true,
17
- configurable: true,
18
- writable: true,
19
- value: new Set()
20
- });
21
- Object.defineProperty(this, "onMouseEvent", {
22
+ Object.defineProperty(this, "pos", {
22
23
  enumerable: true,
23
24
  configurable: true,
24
25
  writable: true,
25
- value: void 0
26
+ value: [0, 0]
26
27
  });
27
- Object.defineProperty(this, "transformFn", {
28
+ Object.defineProperty(this, "lastPos", {
28
29
  enumerable: true,
29
30
  configurable: true,
30
31
  writable: true,
31
- value: void 0
32
+ value: [0, 0]
32
33
  });
33
- // Event handler references for cleanup
34
- Object.defineProperty(this, "mousemoveHandler", {
35
- enumerable: true,
36
- configurable: true,
37
- writable: true,
38
- value: void 0
39
- });
40
- Object.defineProperty(this, "mousedownHandler", {
34
+ Object.defineProperty(this, "buttons", {
41
35
  enumerable: true,
42
36
  configurable: true,
43
37
  writable: true,
44
- value: void 0
38
+ value: new Set()
45
39
  });
46
- Object.defineProperty(this, "mouseupHandler", {
40
+ Object.defineProperty(this, "cursorVisible", {
47
41
  enumerable: true,
48
42
  configurable: true,
49
43
  writable: true,
50
- value: void 0
44
+ value: true
51
45
  });
52
- Object.defineProperty(this, "blurHandler", {
46
+ Object.defineProperty(this, "abort", {
53
47
  enumerable: true,
54
48
  configurable: true,
55
49
  writable: true,
56
- value: void 0
50
+ value: new AbortController()
57
51
  });
58
- this.transformFn = transformFn;
59
- // Bind event handlers
60
- this.mousemoveHandler = this.handleMouseMove.bind(this);
61
- this.mousedownHandler = this.handleMouseDown.bind(this);
62
- this.mouseupHandler = this.handleMouseUp.bind(this);
63
- this.blurHandler = this.handleBlur.bind(this);
64
- // Register event listeners
65
- window.addEventListener('mousemove', this.mousemoveHandler);
66
- window.addEventListener('mousedown', this.mousedownHandler);
67
- window.addEventListener('mouseup', this.mouseupHandler);
68
- window.addEventListener('blur', this.blurHandler);
69
- }
70
- setTransform(transformFn) {
71
- this.transformFn = transformFn;
52
+ this.canvas.addEventListener('like:mousemoved', this.handleMouseMove.bind(this), { signal: this.abort.signal });
53
+ this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this), { signal: this.abort.signal });
54
+ window.addEventListener('mouseup', this.handleMouseUp.bind(this), { signal: this.abort.signal });
55
+ this.canvas.addEventListener('wheel', this.handleWheel.bind(this), { passive: false, signal: this.abort.signal });
56
+ this.canvas.addEventListener('mouseleave', () => this.buttons.clear(), { signal: this.abort.signal });
72
57
  }
73
58
  handleMouseMove(e) {
74
- // Store raw CSS coordinates - transformation to game coordinates
75
- // should be done by the consumer using engine.transformMousePosition()
76
- this.x = e.clientX;
77
- this.y = e.clientY;
78
- this.onMouseEvent?.(e.clientX, e.clientY, undefined, 'mousemove');
59
+ if (this.isPointerLocked()) {
60
+ /** In pointer-lock mode, simulate a real cursor bounded by the canvas. */
61
+ this.pos = Vec2.clamp(Vec2.add(this.pos, e.detail.delta), [0, 0], e.detail.renderSize);
62
+ this.dispatch('mousemoved', [this.pos, e.detail.delta]);
63
+ }
64
+ else {
65
+ /** In non-pointer locked mode, calculate deltas ourselves. */
66
+ this.pos = e.detail.pos;
67
+ this.dispatch('mousemoved', [this.pos, Vec2.sub(this.pos, this.lastPos)]);
68
+ }
69
+ this.lastPos = this.pos;
79
70
  }
80
71
  handleMouseDown(e) {
81
- this.buttons.add(e.button + 1);
82
- this.onMouseEvent?.(e.clientX, e.clientY, e.button, 'mousedown');
72
+ // hack: ignore right clicks because they cause a refocus
73
+ if (!this.isPointerLocked() && e.button == 2)
74
+ return;
75
+ this.buttons.add(numToButton(e.button));
76
+ this.dispatch('mousepressed', [[e.offsetX, e.offsetY], numToButton(e.button)]);
77
+ this.canvas?.focus();
83
78
  }
84
79
  handleMouseUp(e) {
85
- this.buttons.delete(e.button + 1);
86
- this.onMouseEvent?.(e.clientX, e.clientY, e.button, 'mouseup');
80
+ this.buttons.delete(numToButton(e.button));
81
+ this.dispatch('mousereleased', [[e.offsetX, e.offsetY], numToButton(e.button)]);
87
82
  }
88
- handleBlur() {
89
- this.buttons.clear();
83
+ handleWheel(e) {
84
+ e.preventDefault();
90
85
  }
91
- dispose() {
92
- window.removeEventListener('mousemove', this.mousemoveHandler);
93
- window.removeEventListener('mousedown', this.mousedownHandler);
94
- window.removeEventListener('mouseup', this.mouseupHandler);
95
- window.removeEventListener('blur', this.blurHandler);
96
- this.buttons.clear();
86
+ _dispose() {
87
+ this.abort.abort();
97
88
  }
89
+ /** Mouse position, transformed to canvas pixels. */
98
90
  getPosition() {
99
- if (this.transformFn) {
100
- return this.transformFn(this.x, this.y);
101
- }
102
- return [this.x, this.y];
103
- }
104
- getX() {
105
- return this.x;
106
- }
107
- getY() {
108
- return this.y;
91
+ return this.pos;
109
92
  }
93
+ /** Check if button is held. Button 1 = left, 2 = middle, 3 = right. */
110
94
  isDown(button) {
111
95
  return this.buttons.has(button);
112
96
  }
97
+ /** All currently held buttons. */
113
98
  getPressedButtons() {
114
99
  return new Set(this.buttons);
115
100
  }
116
- isVisible() {
117
- return document.pointerLockElement === null;
101
+ /** True when pointer is locked to canvas.
102
+ * {@link lockPointer}
103
+ */
104
+ isPointerLocked() {
105
+ return document.pointerLockElement !== null;
118
106
  }
119
- setVisible(visible, canvas) {
120
- if (!visible && canvas) {
121
- canvas.requestPointerLock();
107
+ /**
108
+ * Whether to lock (capture) the pointer.
109
+ *
110
+ * In locked mode, the cursor cannot escape the canvas.
111
+ * It also becomes invisible.
112
+ *
113
+ * Note that event {@link mousemoved} passes both
114
+ * `pos` and `delta`.
115
+ *
116
+ * Though the emulated cursor in locked mode
117
+ * (locked mode doesn't natively track absolute position)
118
+ * may be stuck on canvas edges, the `delta` field always
119
+ * represents mouse movement.
120
+ */
121
+ lockPointer(locked) {
122
+ if (!this.canvas)
123
+ return;
124
+ if (locked && document.pointerLockElement !== this.canvas) {
125
+ this.canvas.requestPointerLock();
122
126
  }
123
- else if (visible && canvas && document.pointerLockElement === canvas) {
127
+ else if (!locked && document.pointerLockElement === this.canvas) {
124
128
  document.exitPointerLock();
125
129
  }
126
130
  }
127
- getRelativeMode() {
128
- return document.pointerLockElement !== null;
131
+ /** Show or hide cursor. Unlike pointer lock, cursor can still leave canvas. */
132
+ showCursor(visible) {
133
+ this.cursorVisible = visible;
134
+ if (this.canvas) {
135
+ this.canvas.style.cursor = visible ? 'auto' : 'none';
136
+ }
137
+ }
138
+ /** Current cursor visibility state. */
139
+ isCursorVisible() {
140
+ return this.cursorVisible;
129
141
  }
130
142
  }
@@ -1,17 +1,14 @@
1
- export declare class Timer {
1
+ export declare class TimerInternal {
2
2
  private currentDelta;
3
3
  private totalTime;
4
4
  private frameCount;
5
5
  private fps;
6
6
  private fpsAccumulator;
7
7
  private sleepUntil;
8
- private sceneStartTime;
9
- update(dt: number): void;
10
- resetSceneTime(): void;
8
+ _update(dt: number): void;
11
9
  getDelta(): number;
12
10
  getFPS(): number;
13
11
  getTime(): number;
14
- getSceneTime(): number;
15
12
  isSleeping(): boolean;
16
13
  sleep(duration: number): void;
17
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["../../src/core/timer.ts"],"names":[],"mappings":"AAAA,qBAAa,KAAK;IAChB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,cAAc,CAAK;IAE3B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAaxB,cAAc,IAAI,IAAI;IAItB,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;IAIhB,OAAO,IAAI,MAAM;IAIjB,YAAY,IAAI,MAAM;IAItB,UAAU,IAAI,OAAO;IAUrB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAG9B"}
1
+ {"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["../../src/core/timer.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAa;IACxB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,UAAU,CAAuB;IAEzC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAazB,QAAQ,IAAI,MAAM;IAIlB,MAAM,IAAI,MAAM;IAIhB,OAAO,IAAI,MAAM;IAIjB,UAAU,IAAI,OAAO;IAUrB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAG9B"}
@@ -1,4 +1,4 @@
1
- export class Timer {
1
+ export class TimerInternal {
2
2
  constructor() {
3
3
  Object.defineProperty(this, "currentDelta", {
4
4
  enumerable: true,
@@ -36,14 +36,8 @@ export class Timer {
36
36
  writable: true,
37
37
  value: null
38
38
  });
39
- Object.defineProperty(this, "sceneStartTime", {
40
- enumerable: true,
41
- configurable: true,
42
- writable: true,
43
- value: 0
44
- });
45
39
  }
46
- update(dt) {
40
+ _update(dt) {
47
41
  this.currentDelta = dt;
48
42
  this.totalTime += dt;
49
43
  this.frameCount++;
@@ -54,9 +48,6 @@ export class Timer {
54
48
  this.fpsAccumulator = 0;
55
49
  }
56
50
  }
57
- resetSceneTime() {
58
- this.sceneStartTime = this.totalTime;
59
- }
60
51
  getDelta() {
61
52
  return this.currentDelta;
62
53
  }
@@ -66,9 +57,6 @@ export class Timer {
66
57
  getTime() {
67
58
  return this.totalTime;
68
59
  }
69
- getSceneTime() {
70
- return this.totalTime - this.sceneStartTime;
71
- }
72
60
  isSleeping() {
73
61
  if (this.sleepUntil === null)
74
62
  return false;
package/dist/engine.d.ts CHANGED
@@ -1,20 +1,70 @@
1
- import type { Like2DEvent } from './core/events';
2
- import type { PartialCanvasMode } from './core/canvas-config';
3
- import type { Like } from './core/like';
1
+ /**
2
+ * @module engine
3
+ * @description Core game engine - lifecycle management and event dispatch.
4
+ *
5
+ * You've reached the most evil part of the codebase -- the man
6
+ * behind the curtain.
7
+ *
8
+ * The secret force gluing everything together.
9
+ *
10
+ * If you want to use modules independently, look here first.
11
+ *
12
+ * ## Memory Management
13
+ *
14
+ * Always call `dispose()` when destroying an engine instance:
15
+ * - Removes all event listeners
16
+ * - Stops the game loop
17
+ * - Removes canvas from DOM
18
+ * - Cleans up canvas manager resources
19
+ *
20
+ */
21
+ import type { LikeInternal } from './core/like';
22
+ export type EngineDispatch = Engine["dispatch"];
23
+ /**
24
+ * Core game engine managing the event loop and subsystems.
25
+ *
26
+ * Normally you don't instantiate this directly - use {@link createLike} instead.
27
+ * The Engine class is exposed for advanced use cases like testing or
28
+ * custom initialization sequences.
29
+ *
30
+ * All subsystems are accessible via the {@link like} property.
31
+ */
4
32
  export declare class Engine {
33
+ private container;
5
34
  private canvas;
6
- private ctx;
7
35
  private isRunning;
8
36
  private lastTime;
9
- private container;
10
- private canvasManager;
11
- private handleEvent;
12
- private currentScene;
13
- readonly like: Like;
37
+ private abort;
38
+ /**
39
+ * The Like interface providing access to all engine subsystems.
40
+ * This object is passed to all scene callbacks and game code.
41
+ */
42
+ readonly like: LikeInternal;
14
43
  constructor(container: HTMLElement);
15
44
  private dispatch;
16
- setMode(mode: PartialCanvasMode): void;
17
- start(handleEvent: (event: Like2DEvent) => void): Promise<void>;
45
+ /**
46
+ * Start the game loop.
47
+ *
48
+ * @remarks
49
+ * This method:
50
+ * 1. Dispatches the initial `load` event
51
+ * 2. Starts the requestAnimationFrame loop
52
+ *
53
+ * The engine runs until dispose() is called.
54
+ */
55
+ start(): Promise<void>;
56
+ /**
57
+ * Clean up all resources and stop the engine.
58
+ *
59
+ * @remarks
60
+ * This method:
61
+ * - Stops the game loop
62
+ * - Removes all event listeners (keyboard, mouse, window, fullscreen)
63
+ * - Disposes canvas manager (removes resize observer)
64
+ * - Removes the canvas element from the DOM
65
+ *
66
+ * The engine cannot be restarted after disposal - create a new instance.
67
+ */
18
68
  dispose(): void;
19
69
  }
20
70
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAIxC,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,YAAY,CAAsB;IAE1C,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;gBAER,SAAS,EAAE,WAAW;IAkElC,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI;IAWhC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BrE,OAAO,IAAI,IAAI;CAUhB"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;AAEhD;;;;;;;;GAQG;AACH,qBAAa,MAAM;IAaL,OAAO,CAAC,SAAS;IAZ7B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAK;IAErB,OAAO,CAAC,KAAK,CAAyB;IAEtC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;gBAER,SAAS,EAAE,WAAW;IAmD1C,OAAO,CAAC,QAAQ;IAShB;;;;;;;;;OASG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B5B;;;;;;;;;;;OAWG;IACH,OAAO,IAAI,IAAI;CAahB"}
package/dist/engine.js CHANGED
@@ -1,20 +1,50 @@
1
- import { Audio } from './core/audio';
2
- import { Input } from './core/input';
3
- import { Timer } from './core/timer';
4
- import { Keyboard } from './core/keyboard';
5
- import { Mouse } from './core/mouse';
6
- import { Gamepad } from './core/gamepad';
7
- import { newState, bindGraphics } from './core/graphics';
8
- import { CanvasManager } from './core/canvas-manager';
1
+ /**
2
+ * @module engine
3
+ * @description Core game engine - lifecycle management and event dispatch.
4
+ *
5
+ * You've reached the most evil part of the codebase -- the man
6
+ * behind the curtain.
7
+ *
8
+ * The secret force gluing everything together.
9
+ *
10
+ * If you want to use modules independently, look here first.
11
+ *
12
+ * ## Memory Management
13
+ *
14
+ * Always call `dispose()` when destroying an engine instance:
15
+ * - Removes all event listeners
16
+ * - Stops the game loop
17
+ * - Removes canvas from DOM
18
+ * - Cleans up canvas manager resources
19
+ *
20
+ */
21
+ import { AudioInternal } from './core/audio';
22
+ import { InputInternal } from './core/input';
23
+ import { TimerInternal } from './core/timer';
24
+ import { KeyboardInternal } from './core/keyboard';
25
+ import { MouseInternal } from './core/mouse';
26
+ import { GamepadInternal } from './core/gamepad';
27
+ import { bindGraphics } from './core/graphics';
28
+ import { CanvasInternal } from './core/canvas';
29
+ import { sceneDispatch } from './scene';
30
+ /**
31
+ * Core game engine managing the event loop and subsystems.
32
+ *
33
+ * Normally you don't instantiate this directly - use {@link createLike} instead.
34
+ * The Engine class is exposed for advanced use cases like testing or
35
+ * custom initialization sequences.
36
+ *
37
+ * All subsystems are accessible via the {@link like} property.
38
+ */
9
39
  export class Engine {
10
40
  constructor(container) {
11
- Object.defineProperty(this, "canvas", {
41
+ Object.defineProperty(this, "container", {
12
42
  enumerable: true,
13
43
  configurable: true,
14
44
  writable: true,
15
- value: void 0
45
+ value: container
16
46
  });
17
- Object.defineProperty(this, "ctx", {
47
+ Object.defineProperty(this, "canvas", {
18
48
  enumerable: true,
19
49
  configurable: true,
20
50
  writable: true,
@@ -32,54 +62,38 @@ export class Engine {
32
62
  writable: true,
33
63
  value: 0
34
64
  });
35
- Object.defineProperty(this, "container", {
36
- enumerable: true,
37
- configurable: true,
38
- writable: true,
39
- value: void 0
40
- });
41
- Object.defineProperty(this, "canvasManager", {
42
- enumerable: true,
43
- configurable: true,
44
- writable: true,
45
- value: void 0
46
- });
47
- Object.defineProperty(this, "handleEvent", {
65
+ Object.defineProperty(this, "abort", {
48
66
  enumerable: true,
49
67
  configurable: true,
50
68
  writable: true,
51
- value: null
52
- });
53
- Object.defineProperty(this, "currentScene", {
54
- enumerable: true,
55
- configurable: true,
56
- writable: true,
57
- value: null
69
+ value: new AbortController()
58
70
  });
71
+ /**
72
+ * The Like interface providing access to all engine subsystems.
73
+ * This object is passed to all scene callbacks and game code.
74
+ */
59
75
  Object.defineProperty(this, "like", {
60
76
  enumerable: true,
61
77
  configurable: true,
62
78
  writable: true,
63
79
  value: void 0
64
80
  });
65
- this.canvas = document.createElement('canvas');
66
- this.canvas.style.border = '1px solid #ccc';
67
- this.canvas.style.display = 'block';
68
- const ctx = this.canvas.getContext('2d');
69
- if (!ctx)
70
- throw new Error('Failed to get 2D context');
71
- this.ctx = ctx;
72
- this.container = container;
73
- this.container.appendChild(this.canvas);
74
- this.canvasManager = new CanvasManager(this.canvas, this.container, this.ctx, { pixelResolution: null, fullscreen: false });
75
- const gfxState = newState(this.ctx);
76
- const gfx = bindGraphics(gfxState);
77
- const audio = new Audio();
78
- const timer = new Timer();
79
- const keyboard = new Keyboard();
80
- const mouse = new Mouse((cssX, cssY) => this.canvasManager.transformMousePosition(cssX, cssY));
81
- const gamepad = new Gamepad();
82
- const input = new Input({ keyboard, mouse, gamepad });
81
+ this.canvas = new CanvasInternal(this.dispatch.bind(this));
82
+ const canvas = this.canvas._displayCanvas;
83
+ canvas.addEventListener("like:updateRenderTarget", (event) => {
84
+ if (!(event instanceof CustomEvent))
85
+ return;
86
+ this.like.gfx = bindGraphics(event.detail.target.getContext('2d'));
87
+ });
88
+ this.container.appendChild(canvas);
89
+ let gfx = bindGraphics(canvas.getContext('2d'));
90
+ const dispatch = this.dispatch.bind(this);
91
+ const audio = new AudioInternal();
92
+ const timer = new TimerInternal();
93
+ const keyboard = new KeyboardInternal(canvas, dispatch);
94
+ const mouse = new MouseInternal(canvas, dispatch);
95
+ const gamepad = new GamepadInternal(dispatch);
96
+ const input = new InputInternal({ keyboard, mouse, gamepad });
83
97
  this.like = {
84
98
  audio,
85
99
  timer,
@@ -88,61 +102,50 @@ export class Engine {
88
102
  mouse,
89
103
  gamepad,
90
104
  gfx,
91
- setMode: (m) => this.setMode(m),
92
- getMode: () => this.canvasManager.getMode(),
93
- getCanvasSize: () => [this.canvas.width, this.canvas.height],
105
+ canvas: this.canvas,
106
+ start: this.start.bind(this),
107
+ dispose: this.dispose.bind(this),
94
108
  setScene: (scene) => {
95
- this.currentScene = scene;
96
- scene?.load?.(this.like);
109
+ if (scene) {
110
+ this.like.handleEvent = (event) => sceneDispatch(scene, this.like, event);
111
+ this.dispatch("load", []);
112
+ }
113
+ else {
114
+ this.like.handleEvent = undefined;
115
+ }
97
116
  },
98
- };
99
- keyboard.onKeyEvent = (scancode, keycode, type) => {
100
- this.dispatch(type === 'keydown' ? 'keypressed' : 'keyreleased', [scancode, keycode]);
101
- };
102
- mouse.onMouseEvent = (clientX, clientY, button, type) => {
103
- const [x, y] = this.canvasManager.transformMousePosition(clientX, clientY);
104
- this.dispatch(type === 'mousedown' ? 'mousepressed' : 'mousereleased', [x, y, (button ?? 0) + 1]);
105
- };
106
- gamepad.onButtonEvent = (gpIndex, buttonIndex, buttonName, pressed) => {
107
- this.dispatch(pressed ? 'gamepadpressed' : 'gamepadreleased', [gpIndex, buttonIndex, buttonName]);
108
- };
109
- this.canvasManager.onResize = (size, pixelSize, fullscreen) => {
110
- this.dispatch('resize', [size, pixelSize, fullscreen]);
111
- };
112
- document.addEventListener('fullscreenchange', () => {
113
- const mode = this.canvasManager.getMode();
114
- const isFullscreen = !!document.fullscreenElement;
115
- if (mode.fullscreen !== isFullscreen) {
116
- this.canvasManager.setMode({ ...mode, fullscreen: isFullscreen });
117
+ callOwnHandlers: (event) => {
118
+ if (event.type in this.like)
119
+ this.like[event.type](...event.args);
117
120
  }
118
- });
121
+ };
122
+ window.addEventListener('focus', () => this.dispatch('focus', ['tab']));
123
+ window.addEventListener('blur', () => this.dispatch('blur', ['tab']));
124
+ canvas.addEventListener('focus', () => this.dispatch('focus', ['canvas']));
125
+ canvas.addEventListener('focus', () => this.dispatch('focus', ['canvas']));
119
126
  }
120
127
  dispatch(type, args) {
121
- if (!this.handleEvent)
122
- return;
123
- const event = { type, args, timestamp: performance.now() };
124
- if (this.currentScene) {
125
- this.currentScene.handleEvent?.(this.like, event);
126
- const method = this.currentScene[event.type];
127
- method?.call(this.currentScene, this.like, ...args);
128
+ const event = { type, args, timestamp: this.like.timer.getTime() };
129
+ if (this.like.handleEvent) {
130
+ this.like.handleEvent(event);
128
131
  }
129
132
  else {
130
- this.handleEvent(event);
131
- }
132
- }
133
- setMode(mode) {
134
- const currentMode = this.canvasManager.getMode();
135
- const mergedMode = { ...currentMode, ...mode };
136
- if (mode.fullscreen !== undefined && mode.fullscreen !== currentMode.fullscreen) {
137
- mergedMode.fullscreen ? this.container.requestFullscreen().catch(console.error) : document.exitFullscreen();
133
+ this.like.callOwnHandlers(event);
138
134
  }
139
- this.canvasManager.setMode(mode);
140
135
  }
141
- async start(handleEvent) {
142
- this.handleEvent = handleEvent;
136
+ /**
137
+ * Start the game loop.
138
+ *
139
+ * @remarks
140
+ * This method:
141
+ * 1. Dispatches the initial `load` event
142
+ * 2. Starts the requestAnimationFrame loop
143
+ *
144
+ * The engine runs until dispose() is called.
145
+ */
146
+ async start() {
143
147
  this.isRunning = true;
144
148
  this.lastTime = performance.now();
145
- await this.like.gamepad.init();
146
149
  const loop = () => {
147
150
  if (!this.isRunning)
148
151
  return;
@@ -150,27 +153,41 @@ export class Engine {
150
153
  const dt = (now - this.lastTime) / 1000;
151
154
  this.lastTime = now;
152
155
  if (!this.like.timer.isSleeping()) {
153
- this.like.timer.update(dt);
154
- const { pressed, released } = this.like.input.update();
156
+ this.like.timer._update(dt);
157
+ const { pressed, released } = this.like.input._update();
155
158
  pressed.forEach(action => this.dispatch('actionpressed', [action]));
156
159
  released.forEach(action => this.dispatch('actionreleased', [action]));
157
160
  this.dispatch('update', [dt]);
158
161
  }
159
162
  this.dispatch('draw', []);
160
- this.canvasManager.present();
163
+ this.canvas._present();
161
164
  requestAnimationFrame(loop);
162
165
  };
163
166
  this.dispatch('load', []);
164
167
  requestAnimationFrame(loop);
165
168
  }
169
+ /**
170
+ * Clean up all resources and stop the engine.
171
+ *
172
+ * @remarks
173
+ * This method:
174
+ * - Stops the game loop
175
+ * - Removes all event listeners (keyboard, mouse, window, fullscreen)
176
+ * - Disposes canvas manager (removes resize observer)
177
+ * - Removes the canvas element from the DOM
178
+ *
179
+ * The engine cannot be restarted after disposal - create a new instance.
180
+ */
166
181
  dispose() {
182
+ const canvas = this.canvas._displayCanvas;
167
183
  this.isRunning = false;
168
- this.like.keyboard.dispose();
169
- this.like.mouse.dispose();
170
- this.like.gamepad.dispose();
171
- this.canvasManager.dispose();
172
- if (this.canvas.parentNode === this.container) {
173
- this.container.removeChild(this.canvas);
184
+ this.like.keyboard._dispose();
185
+ this.like.mouse._dispose();
186
+ this.like.gamepad._dispose();
187
+ this.canvas._dispose();
188
+ this.abort.abort();
189
+ if (canvas.parentNode === this.container) {
190
+ this.container.removeChild(canvas);
174
191
  }
175
192
  }
176
193
  }