@energy8platform/game-engine 0.1.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 (80) hide show
  1. package/README.md +1134 -0
  2. package/dist/animation.cjs.js +505 -0
  3. package/dist/animation.cjs.js.map +1 -0
  4. package/dist/animation.d.ts +235 -0
  5. package/dist/animation.esm.js +500 -0
  6. package/dist/animation.esm.js.map +1 -0
  7. package/dist/assets.cjs.js +148 -0
  8. package/dist/assets.cjs.js.map +1 -0
  9. package/dist/assets.d.ts +97 -0
  10. package/dist/assets.esm.js +146 -0
  11. package/dist/assets.esm.js.map +1 -0
  12. package/dist/audio.cjs.js +345 -0
  13. package/dist/audio.cjs.js.map +1 -0
  14. package/dist/audio.d.ts +135 -0
  15. package/dist/audio.esm.js +343 -0
  16. package/dist/audio.esm.js.map +1 -0
  17. package/dist/core.cjs.js +1832 -0
  18. package/dist/core.cjs.js.map +1 -0
  19. package/dist/core.d.ts +633 -0
  20. package/dist/core.esm.js +1827 -0
  21. package/dist/core.esm.js.map +1 -0
  22. package/dist/debug.cjs.js +298 -0
  23. package/dist/debug.cjs.js.map +1 -0
  24. package/dist/debug.d.ts +114 -0
  25. package/dist/debug.esm.js +295 -0
  26. package/dist/debug.esm.js.map +1 -0
  27. package/dist/index.cjs.js +3623 -0
  28. package/dist/index.cjs.js.map +1 -0
  29. package/dist/index.d.ts +1607 -0
  30. package/dist/index.esm.js +3598 -0
  31. package/dist/index.esm.js.map +1 -0
  32. package/dist/ui.cjs.js +1081 -0
  33. package/dist/ui.cjs.js.map +1 -0
  34. package/dist/ui.d.ts +387 -0
  35. package/dist/ui.esm.js +1072 -0
  36. package/dist/ui.esm.js.map +1 -0
  37. package/dist/vite.cjs.js +125 -0
  38. package/dist/vite.cjs.js.map +1 -0
  39. package/dist/vite.d.ts +42 -0
  40. package/dist/vite.esm.js +122 -0
  41. package/dist/vite.esm.js.map +1 -0
  42. package/package.json +107 -0
  43. package/src/animation/Easing.ts +116 -0
  44. package/src/animation/SpineHelper.ts +162 -0
  45. package/src/animation/Timeline.ts +138 -0
  46. package/src/animation/Tween.ts +225 -0
  47. package/src/animation/index.ts +4 -0
  48. package/src/assets/AssetManager.ts +174 -0
  49. package/src/assets/index.ts +2 -0
  50. package/src/audio/AudioManager.ts +366 -0
  51. package/src/audio/index.ts +1 -0
  52. package/src/core/EventEmitter.ts +47 -0
  53. package/src/core/GameApplication.ts +306 -0
  54. package/src/core/Scene.ts +48 -0
  55. package/src/core/SceneManager.ts +258 -0
  56. package/src/core/index.ts +4 -0
  57. package/src/debug/DevBridge.ts +259 -0
  58. package/src/debug/FPSOverlay.ts +102 -0
  59. package/src/debug/index.ts +3 -0
  60. package/src/index.ts +71 -0
  61. package/src/input/InputManager.ts +171 -0
  62. package/src/input/index.ts +1 -0
  63. package/src/loading/CSSPreloader.ts +155 -0
  64. package/src/loading/LoadingScene.ts +356 -0
  65. package/src/loading/index.ts +2 -0
  66. package/src/state/StateMachine.ts +228 -0
  67. package/src/state/index.ts +1 -0
  68. package/src/types.ts +218 -0
  69. package/src/ui/BalanceDisplay.ts +155 -0
  70. package/src/ui/Button.ts +199 -0
  71. package/src/ui/Label.ts +111 -0
  72. package/src/ui/Modal.ts +134 -0
  73. package/src/ui/Panel.ts +125 -0
  74. package/src/ui/ProgressBar.ts +121 -0
  75. package/src/ui/Toast.ts +124 -0
  76. package/src/ui/WinDisplay.ts +133 -0
  77. package/src/ui/index.ts +16 -0
  78. package/src/viewport/ViewportManager.ts +241 -0
  79. package/src/viewport/index.ts +1 -0
  80. package/src/vite/index.ts +153 -0
@@ -0,0 +1,259 @@
1
+ import type {
2
+ InitData,
3
+ GameConfigData,
4
+ PlayResultData,
5
+ SessionData,
6
+ } from '@energy8platform/game-sdk';
7
+
8
+ export interface DevBridgeConfig {
9
+ /** Mock initial balance */
10
+ balance?: number;
11
+ /** Mock currency */
12
+ currency?: string;
13
+ /** Game config */
14
+ gameConfig?: Partial<GameConfigData>;
15
+ /** Base URL for assets (default: '/assets/') */
16
+ assetsUrl?: string;
17
+ /** Active session to resume (null = no active session) */
18
+ session?: SessionData | null;
19
+ /** Custom play result handler — return mock result data */
20
+ onPlay?: (params: { action: string; bet: number; roundId?: string }) => Partial<PlayResultData>;
21
+ /** Simulated network delay in ms */
22
+ networkDelay?: number;
23
+ /** Enable debug logging */
24
+ debug?: boolean;
25
+ }
26
+
27
+ const DEFAULT_CONFIG: Required<DevBridgeConfig> = {
28
+ balance: 10000,
29
+ currency: 'USD',
30
+ gameConfig: {
31
+ id: 'dev-game',
32
+ type: 'slot',
33
+ version: '1.0.0',
34
+ viewport: { width: 1920, height: 1080 },
35
+ betLevels: [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50],
36
+ },
37
+ assetsUrl: '/assets/',
38
+ session: null,
39
+ onPlay: () => ({}),
40
+ networkDelay: 200,
41
+ debug: true,
42
+ };
43
+
44
+ /**
45
+ * Mock host bridge for local development.
46
+ *
47
+ * Intercepts postMessage communication from the SDK and responds
48
+ * with mock data, simulating a real casino host environment.
49
+ *
50
+ * This allows games to be developed and tested without a real backend.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * // In your dev entry point or vite plugin
55
+ * import { DevBridge } from '@energy8platform/game-engine/debug';
56
+ *
57
+ * const devBridge = new DevBridge({
58
+ * balance: 5000,
59
+ * currency: 'EUR',
60
+ * gameConfig: { id: 'my-slot', type: 'slot', betLevels: [0.2, 0.5, 1, 2] },
61
+ * onPlay: ({ action, bet }) => ({
62
+ * totalWin: Math.random() > 0.5 ? bet * (Math.random() * 20) : 0,
63
+ * data: {
64
+ * matrix: generateRandomMatrix(5, 3, 10),
65
+ * win_lines: [],
66
+ * },
67
+ * }),
68
+ * });
69
+ * devBridge.start();
70
+ * ```
71
+ */
72
+ export class DevBridge {
73
+ private _config: Required<DevBridgeConfig>;
74
+ private _balance: number;
75
+ private _roundCounter = 0;
76
+ private _listening = false;
77
+ private _handler: ((e: MessageEvent) => void) | null = null;
78
+
79
+ constructor(config: DevBridgeConfig = {}) {
80
+ this._config = { ...DEFAULT_CONFIG, ...config };
81
+ this._balance = this._config.balance;
82
+ }
83
+
84
+ /** Current mock balance */
85
+ get balance(): number {
86
+ return this._balance;
87
+ }
88
+
89
+ /** Start listening for SDK messages */
90
+ start(): void {
91
+ if (this._listening) return;
92
+
93
+ this._handler = (e: MessageEvent) => {
94
+ this.handleMessage(e);
95
+ };
96
+
97
+ window.addEventListener('message', this._handler);
98
+ this._listening = true;
99
+
100
+ if (this._config.debug) {
101
+ console.log('[DevBridge] Started — listening for SDK messages');
102
+ }
103
+ }
104
+
105
+ /** Stop listening */
106
+ stop(): void {
107
+ if (this._handler) {
108
+ window.removeEventListener('message', this._handler);
109
+ this._handler = null;
110
+ }
111
+ this._listening = false;
112
+
113
+ if (this._config.debug) {
114
+ console.log('[DevBridge] Stopped');
115
+ }
116
+ }
117
+
118
+ /** Set mock balance */
119
+ setBalance(balance: number): void {
120
+ this._balance = balance;
121
+ // Send balance update
122
+ this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
123
+ }
124
+
125
+ /** Destroy the dev bridge */
126
+ destroy(): void {
127
+ this.stop();
128
+ }
129
+
130
+ // ─── Message Handling ──────────────────────────────────
131
+
132
+ private handleMessage(e: MessageEvent): void {
133
+ const data = e.data;
134
+
135
+ // Only process bridge messages
136
+ if (!data || data.__casino_bridge !== true) return;
137
+
138
+ if (this._config.debug) {
139
+ console.log('[DevBridge] ←', data.type, data.payload);
140
+ }
141
+
142
+ switch (data.type) {
143
+ case 'GAME_READY':
144
+ this.handleGameReady(data.id);
145
+ break;
146
+ case 'PLAY_REQUEST':
147
+ this.handlePlayRequest(data.payload, data.id);
148
+ break;
149
+ case 'PLAY_RESULT_ACK':
150
+ this.handlePlayAck(data.payload, data.id);
151
+ break;
152
+ case 'GET_BALANCE':
153
+ this.handleGetBalance(data.id);
154
+ break;
155
+ case 'GET_STATE':
156
+ this.handleGetState(data.id);
157
+ break;
158
+ case 'OPEN_DEPOSIT':
159
+ this.handleOpenDeposit();
160
+ break;
161
+ default:
162
+ if (this._config.debug) {
163
+ console.log('[DevBridge] Unknown message type:', data.type);
164
+ }
165
+ }
166
+ }
167
+
168
+ private handleGameReady(id?: string): void {
169
+ const initData: InitData = {
170
+ balance: this._balance,
171
+ currency: this._config.currency,
172
+ config: this._config.gameConfig as GameConfigData,
173
+ session: this._config.session,
174
+ assetsUrl: this._config.assetsUrl,
175
+ };
176
+
177
+ this.delayedSend('INIT', initData, id);
178
+ }
179
+
180
+ private handlePlayRequest(
181
+ payload: { action: string; bet: number; roundId?: string },
182
+ id?: string,
183
+ ): void {
184
+ const { action, bet, roundId } = payload;
185
+
186
+ // Deduct bet
187
+ this._balance -= bet;
188
+ this._roundCounter++;
189
+
190
+ // Generate result
191
+ const customResult = this._config.onPlay({ action, bet, roundId });
192
+ const totalWin = customResult.totalWin ?? (Math.random() > 0.6 ? bet * (1 + Math.random() * 10) : 0);
193
+
194
+ // Credit win
195
+ this._balance += totalWin;
196
+
197
+ const result: PlayResultData = {
198
+ roundId: roundId ?? `dev-round-${this._roundCounter}`,
199
+ action,
200
+ balanceAfter: this._balance,
201
+ totalWin: Math.round(totalWin * 100) / 100,
202
+ data: customResult.data ?? {},
203
+ nextActions: customResult.nextActions ?? ['spin'],
204
+ session: customResult.session ?? null,
205
+ creditPending: false,
206
+ };
207
+
208
+ this.delayedSend('PLAY_RESULT', result, id);
209
+ }
210
+
211
+ private handlePlayAck(_payload: unknown, _id?: string): void {
212
+ if (this._config.debug) {
213
+ console.log('[DevBridge] Play acknowledged');
214
+ }
215
+ }
216
+
217
+ private handleGetBalance(id?: string): void {
218
+ this.delayedSend('BALANCE_RESPONSE', { balance: this._balance }, id);
219
+ }
220
+
221
+ private handleGetState(id?: string): void {
222
+ this.delayedSend('STATE_RESPONSE', this._config.session, id);
223
+ }
224
+
225
+ private handleOpenDeposit(): void {
226
+ if (this._config.debug) {
227
+ console.log('[DevBridge] 💰 Open deposit requested (mock: adding 1000)');
228
+ }
229
+ this._balance += 1000;
230
+ this.sendMessage('BALANCE_UPDATE', { balance: this._balance });
231
+ }
232
+
233
+ // ─── Communication ─────────────────────────────────────
234
+
235
+ private delayedSend(type: string, payload: unknown, id?: string): void {
236
+ const delay = this._config.networkDelay;
237
+ if (delay > 0) {
238
+ setTimeout(() => this.sendMessage(type, payload, id), delay);
239
+ } else {
240
+ this.sendMessage(type, payload, id);
241
+ }
242
+ }
243
+
244
+ private sendMessage(type: string, payload: unknown, id?: string): void {
245
+ const message = {
246
+ __casino_bridge: true,
247
+ type,
248
+ payload,
249
+ id,
250
+ };
251
+
252
+ if (this._config.debug) {
253
+ console.log('[DevBridge] →', type, payload);
254
+ }
255
+
256
+ // Post to the same window (SDK listens on window)
257
+ window.postMessage(message, '*');
258
+ }
259
+ }
@@ -0,0 +1,102 @@
1
+ import { Container, Text } from 'pixi.js';
2
+ import type { Application, Ticker } from 'pixi.js';
3
+
4
+ /**
5
+ * FPS overlay for debugging performance.
6
+ *
7
+ * Shows FPS, frame time, and draw call count in the corner of the screen.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const fps = new FPSOverlay(app);
12
+ * fps.show();
13
+ * ```
14
+ */
15
+ export class FPSOverlay {
16
+ private _app: Application;
17
+ private _container: Container;
18
+ private _fpsText: Text;
19
+ private _visible = false;
20
+ private _samples: number[] = [];
21
+ private _maxSamples = 60;
22
+ private _lastUpdate = 0;
23
+ private _tickFn: ((ticker: Ticker) => void) | null = null;
24
+
25
+ constructor(app: Application) {
26
+ this._app = app;
27
+
28
+ this._container = new Container();
29
+ this._container.label = 'FPSOverlay';
30
+ this._container.zIndex = 99999;
31
+
32
+ this._fpsText = new Text({
33
+ text: 'FPS: --',
34
+ style: {
35
+ fontFamily: 'monospace',
36
+ fontSize: 14,
37
+ fill: 0x00ff00,
38
+ stroke: { color: 0x000000, width: 2 },
39
+ },
40
+ });
41
+ this._fpsText.x = 8;
42
+ this._fpsText.y = 8;
43
+
44
+ this._container.addChild(this._fpsText);
45
+ }
46
+
47
+ /** Show the FPS overlay */
48
+ show(): void {
49
+ if (this._visible) return;
50
+ this._visible = true;
51
+
52
+ this._app.stage.addChild(this._container);
53
+
54
+ this._tickFn = (ticker: Ticker) => {
55
+ this._samples.push(ticker.FPS);
56
+ if (this._samples.length > this._maxSamples) {
57
+ this._samples.shift();
58
+ }
59
+
60
+ // Update display every ~500ms
61
+ const now = Date.now();
62
+ if (now - this._lastUpdate > 500) {
63
+ const avg = this._samples.reduce((a, b) => a + b, 0) / this._samples.length;
64
+ const min = Math.min(...this._samples);
65
+ this._fpsText.text = [
66
+ `FPS: ${Math.round(avg)} (min: ${Math.round(min)})`,
67
+ `Frame: ${ticker.deltaMS.toFixed(1)}ms`,
68
+ ].join('\n');
69
+ this._lastUpdate = now;
70
+ }
71
+ };
72
+
73
+ this._app.ticker.add(this._tickFn);
74
+ }
75
+
76
+ /** Hide the FPS overlay */
77
+ hide(): void {
78
+ if (!this._visible) return;
79
+ this._visible = false;
80
+
81
+ this._container.removeFromParent();
82
+ if (this._tickFn) {
83
+ this._app.ticker.remove(this._tickFn);
84
+ this._tickFn = null;
85
+ }
86
+ }
87
+
88
+ /** Toggle visibility */
89
+ toggle(): void {
90
+ if (this._visible) {
91
+ this.hide();
92
+ } else {
93
+ this.show();
94
+ }
95
+ }
96
+
97
+ /** Destroy the overlay */
98
+ destroy(): void {
99
+ this.hide();
100
+ this._container.destroy({ children: true });
101
+ }
102
+ }
@@ -0,0 +1,3 @@
1
+ export { DevBridge } from './DevBridge';
2
+ export type { DevBridgeConfig } from './DevBridge';
3
+ export { FPSOverlay } from './FPSOverlay';
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ // ─── Core ────────────────────────────────────────────────
2
+ export { GameApplication } from './core/GameApplication';
3
+ export { SceneManager } from './core/SceneManager';
4
+ export { Scene } from './core/Scene';
5
+ export { EventEmitter } from './core/EventEmitter';
6
+
7
+ // ─── Types ───────────────────────────────────────────────
8
+ export {
9
+ ScaleMode,
10
+ Orientation,
11
+ TransitionType,
12
+ } from './types';
13
+ export type {
14
+ GameApplicationConfig,
15
+ LoadingScreenConfig,
16
+ AssetManifest,
17
+ AssetBundle,
18
+ AssetEntry,
19
+ AudioConfig,
20
+ IScene,
21
+ SceneConstructor,
22
+ TransitionConfig,
23
+ GameEngineEvents,
24
+ EasingFunction,
25
+ TweenOptions,
26
+ // Re-exported SDK types
27
+ InitData,
28
+ GameConfigData,
29
+ SessionData,
30
+ PlayParams,
31
+ PlayResultData,
32
+ } from './types';
33
+
34
+ // ─── Assets ──────────────────────────────────────────────
35
+ export { AssetManager } from './assets/AssetManager';
36
+
37
+ // ─── Audio ───────────────────────────────────────────────
38
+ export { AudioManager } from './audio/AudioManager';
39
+
40
+ // ─── Viewport ────────────────────────────────────────────
41
+ export { ViewportManager } from './viewport/ViewportManager';
42
+
43
+ // ─── State Machine ───────────────────────────────────────
44
+ export { StateMachine } from './state/StateMachine';
45
+
46
+ // ─── Animation ───────────────────────────────────────────
47
+ export { Tween } from './animation/Tween';
48
+ export { Timeline } from './animation/Timeline';
49
+ export { Easing } from './animation/Easing';
50
+ export { SpineHelper } from './animation/SpineHelper';
51
+
52
+ // ─── Input ───────────────────────────────────────────────
53
+ export { InputManager } from './input/InputManager';
54
+
55
+ // ─── UI ──────────────────────────────────────────────────
56
+ export { Button } from './ui/Button';
57
+ export { ProgressBar } from './ui/ProgressBar';
58
+ export { Label } from './ui/Label';
59
+ export { Panel } from './ui/Panel';
60
+ export { BalanceDisplay } from './ui/BalanceDisplay';
61
+ export { WinDisplay } from './ui/WinDisplay';
62
+ export { Modal } from './ui/Modal';
63
+ export { Toast } from './ui/Toast';
64
+
65
+ // ─── Loading ─────────────────────────────────────────────
66
+ export { LoadingScene } from './loading/LoadingScene';
67
+
68
+ // ─── Debug ───────────────────────────────────────────────
69
+ export { DevBridge } from './debug/DevBridge';
70
+ export type { DevBridgeConfig } from './debug/DevBridge';
71
+ export { FPSOverlay } from './debug/FPSOverlay';
@@ -0,0 +1,171 @@
1
+ import { EventEmitter } from '../core/EventEmitter';
2
+
3
+ interface InputEvents {
4
+ tap: { x: number; y: number };
5
+ press: { x: number; y: number };
6
+ release: { x: number; y: number };
7
+ move: { x: number; y: number };
8
+ swipe: { direction: 'up' | 'down' | 'left' | 'right'; velocity: number };
9
+ keydown: { key: string; code: string };
10
+ keyup: { key: string; code: string };
11
+ }
12
+
13
+ /**
14
+ * Unified input manager for touch, mouse, and keyboard.
15
+ *
16
+ * Features:
17
+ * - Unified pointer events (works with touch + mouse)
18
+ * - Swipe gesture detection
19
+ * - Keyboard input with isKeyDown state
20
+ * - Input locking (block input during animations)
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const input = new InputManager(app.canvas);
25
+ *
26
+ * input.on('tap', ({ x, y }) => console.log('Tapped at', x, y));
27
+ * input.on('swipe', ({ direction }) => console.log('Swiped', direction));
28
+ * input.on('keydown', ({ key }) => {
29
+ * if (key === ' ') spin();
30
+ * });
31
+ *
32
+ * // Block input during animations
33
+ * input.lock();
34
+ * await playAnimation();
35
+ * input.unlock();
36
+ * ```
37
+ */
38
+ export class InputManager extends EventEmitter<InputEvents> {
39
+ private _canvas: HTMLCanvasElement;
40
+ private _locked = false;
41
+ private _keysDown = new Set<string>();
42
+ private _destroyed = false;
43
+
44
+ // Gesture tracking
45
+ private _pointerStart: { x: number; y: number; time: number } | null = null;
46
+ private _swipeThreshold = 50; // minimum distance in px
47
+ private _swipeMaxTime = 300; // max ms for swipe gesture
48
+
49
+ constructor(canvas: HTMLCanvasElement) {
50
+ super();
51
+ this._canvas = canvas;
52
+ this.setupPointerEvents();
53
+ this.setupKeyboardEvents();
54
+ }
55
+
56
+ /** Whether input is currently locked */
57
+ get locked(): boolean {
58
+ return this._locked;
59
+ }
60
+
61
+ /** Lock all input (e.g., during animations) */
62
+ lock(): void {
63
+ this._locked = true;
64
+ }
65
+
66
+ /** Unlock input */
67
+ unlock(): void {
68
+ this._locked = false;
69
+ }
70
+
71
+ /** Check if a key is currently pressed */
72
+ isKeyDown(key: string): boolean {
73
+ return this._keysDown.has(key.toLowerCase());
74
+ }
75
+
76
+ /** Destroy the input manager */
77
+ destroy(): void {
78
+ this._destroyed = true;
79
+ this._canvas.removeEventListener('pointerdown', this.onPointerDown);
80
+ this._canvas.removeEventListener('pointerup', this.onPointerUp);
81
+ this._canvas.removeEventListener('pointermove', this.onPointerMove);
82
+ document.removeEventListener('keydown', this.onKeyDown);
83
+ document.removeEventListener('keyup', this.onKeyUp);
84
+ this._keysDown.clear();
85
+ this.removeAllListeners();
86
+ }
87
+
88
+ // ─── Private: Pointer ──────────────────────────────────
89
+
90
+ private setupPointerEvents(): void {
91
+ this._canvas.addEventListener('pointerdown', this.onPointerDown);
92
+ this._canvas.addEventListener('pointerup', this.onPointerUp);
93
+ this._canvas.addEventListener('pointermove', this.onPointerMove);
94
+ }
95
+
96
+ private onPointerDown = (e: PointerEvent): void => {
97
+ if (this._locked || this._destroyed) return;
98
+
99
+ const pos = this.getCanvasPosition(e);
100
+ this._pointerStart = { ...pos, time: Date.now() };
101
+ this.emit('press', pos);
102
+ };
103
+
104
+ private onPointerUp = (e: PointerEvent): void => {
105
+ if (this._locked || this._destroyed) return;
106
+
107
+ const pos = this.getCanvasPosition(e);
108
+ this.emit('release', pos);
109
+
110
+ // Check for tap vs swipe
111
+ if (this._pointerStart) {
112
+ const dx = pos.x - this._pointerStart.x;
113
+ const dy = pos.y - this._pointerStart.y;
114
+ const dist = Math.sqrt(dx * dx + dy * dy);
115
+ const elapsed = Date.now() - this._pointerStart.time;
116
+
117
+ if (dist > this._swipeThreshold && elapsed < this._swipeMaxTime) {
118
+ // Swipe detected
119
+ const absDx = Math.abs(dx);
120
+ const absDy = Math.abs(dy);
121
+ let direction: 'up' | 'down' | 'left' | 'right';
122
+
123
+ if (absDx > absDy) {
124
+ direction = dx > 0 ? 'right' : 'left';
125
+ } else {
126
+ direction = dy > 0 ? 'down' : 'up';
127
+ }
128
+
129
+ this.emit('swipe', { direction, velocity: dist / elapsed });
130
+ } else if (dist < 10) {
131
+ // Tap (minimal movement)
132
+ this.emit('tap', pos);
133
+ }
134
+ }
135
+
136
+ this._pointerStart = null;
137
+ };
138
+
139
+ private onPointerMove = (e: PointerEvent): void => {
140
+ if (this._locked || this._destroyed) return;
141
+ this.emit('move', this.getCanvasPosition(e));
142
+ };
143
+
144
+ private getCanvasPosition(e: PointerEvent): { x: number; y: number } {
145
+ const rect = this._canvas.getBoundingClientRect();
146
+ return {
147
+ x: e.clientX - rect.left,
148
+ y: e.clientY - rect.top,
149
+ };
150
+ }
151
+
152
+ // ─── Private: Keyboard ─────────────────────────────────
153
+
154
+ private setupKeyboardEvents(): void {
155
+ document.addEventListener('keydown', this.onKeyDown);
156
+ document.addEventListener('keyup', this.onKeyUp);
157
+ }
158
+
159
+ private onKeyDown = (e: KeyboardEvent): void => {
160
+ if (this._locked || this._destroyed) return;
161
+ this._keysDown.add(e.key.toLowerCase());
162
+ this.emit('keydown', { key: e.key, code: e.code });
163
+ };
164
+
165
+ private onKeyUp = (e: KeyboardEvent): void => {
166
+ if (this._destroyed) return;
167
+ this._keysDown.delete(e.key.toLowerCase());
168
+ if (this._locked) return;
169
+ this.emit('keyup', { key: e.key, code: e.code });
170
+ };
171
+ }
@@ -0,0 +1 @@
1
+ export { InputManager } from './InputManager';