action-engine-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +348 -0
  3. package/actionengine/3rdparty/goblin/goblin.js +9609 -0
  4. package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
  5. package/actionengine/camera/actioncamera.js +90 -0
  6. package/actionengine/camera/cameracollisionhandler.js +69 -0
  7. package/actionengine/character/actioncharacter.js +360 -0
  8. package/actionengine/character/actioncharacter3D.js +61 -0
  9. package/actionengine/core/app.js +430 -0
  10. package/actionengine/debug/basedebugpanel.js +858 -0
  11. package/actionengine/display/canvasmanager.js +75 -0
  12. package/actionengine/display/gl/programmanager.js +570 -0
  13. package/actionengine/display/gl/shaders/lineshader.js +118 -0
  14. package/actionengine/display/gl/shaders/objectshader.js +1756 -0
  15. package/actionengine/display/gl/shaders/particleshader.js +43 -0
  16. package/actionengine/display/gl/shaders/shadowshader.js +319 -0
  17. package/actionengine/display/gl/shaders/spriteshader.js +100 -0
  18. package/actionengine/display/gl/shaders/watershader.js +67 -0
  19. package/actionengine/display/graphics/actionmodel3D.js +191 -0
  20. package/actionengine/display/graphics/actionsprite3D.js +230 -0
  21. package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
  22. package/actionengine/display/graphics/lighting/actionlight.js +211 -0
  23. package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
  24. package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
  25. package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
  26. package/actionengine/display/graphics/renderableobject.js +44 -0
  27. package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
  28. package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
  29. package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
  30. package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
  31. package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
  32. package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
  33. package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
  34. package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
  35. package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
  36. package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
  37. package/actionengine/display/graphics/texture/texturemanager.js +242 -0
  38. package/actionengine/display/graphics/texture/textureregistry.js +177 -0
  39. package/actionengine/input/actionscrollablearea.js +1405 -0
  40. package/actionengine/input/inputhandler.js +1647 -0
  41. package/actionengine/math/geometry/geometrybuilder.js +161 -0
  42. package/actionengine/math/geometry/glbexporter.js +364 -0
  43. package/actionengine/math/geometry/glbloader.js +722 -0
  44. package/actionengine/math/geometry/modelcodegenerator.js +97 -0
  45. package/actionengine/math/geometry/triangle.js +33 -0
  46. package/actionengine/math/geometry/triangleutils.js +34 -0
  47. package/actionengine/math/mathutils.js +25 -0
  48. package/actionengine/math/matrix4.js +785 -0
  49. package/actionengine/math/physics/actionphysics.js +108 -0
  50. package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
  51. package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
  52. package/actionengine/math/physics/actionraycast.js +129 -0
  53. package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
  54. package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
  55. package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
  56. package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
  57. package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
  58. package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
  59. package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
  60. package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
  61. package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
  62. package/actionengine/math/quaternion.js +61 -0
  63. package/actionengine/math/vector2.js +277 -0
  64. package/actionengine/math/vector3.js +318 -0
  65. package/actionengine/math/viewfrustum.js +136 -0
  66. package/actionengine/network/ACTIONNETREADME.md +810 -0
  67. package/actionengine/network/client/ActionNetManager.js +802 -0
  68. package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
  69. package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
  70. package/actionengine/network/client/SyncSystem.js +422 -0
  71. package/actionengine/network/p2p/ActionNetPeer.js +142 -0
  72. package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
  73. package/actionengine/network/p2p/DataConnection.js +282 -0
  74. package/actionengine/network/p2p/README.md +510 -0
  75. package/actionengine/network/p2p/example.html +502 -0
  76. package/actionengine/network/server/ActionNetServer.js +577 -0
  77. package/actionengine/network/server/ActionNetServerSSL.js +579 -0
  78. package/actionengine/network/server/ActionNetServerUtils.js +458 -0
  79. package/actionengine/network/server/SERVERREADME.md +314 -0
  80. package/actionengine/network/server/package-lock.json +35 -0
  81. package/actionengine/network/server/package.json +13 -0
  82. package/actionengine/network/server/start.bat +27 -0
  83. package/actionengine/network/server/start.sh +25 -0
  84. package/actionengine/network/server/startwss.bat +27 -0
  85. package/actionengine/sound/audiomanager.js +1589 -0
  86. package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
  87. package/actionengine/sound/soundfont/actionparser.js +718 -0
  88. package/actionengine/sound/soundfont/actionreverb.js +252 -0
  89. package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
  90. package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
  91. package/actionengine/sound/soundfont/soundfont.js +2 -0
  92. package/dist/action-engine.min.js +328 -0
  93. package/package.json +35 -0
@@ -0,0 +1,1647 @@
1
+ class ActionInputHandler {
2
+ constructor(audio, canvases) {
3
+ this.audio = audio;
4
+ this.canvases = canvases;
5
+ this.virtualControls = false;
6
+ this.isPaused = false;
7
+
8
+ // Track which context we're in (update or fixed_update)
9
+ this.currentContext = 'update';
10
+
11
+ // Create containers
12
+ this.virtualControlsContainer = this.createVirtualControlsContainer();
13
+ this.uiControlsContainer = document.getElementById("UIControlsContainer");
14
+
15
+ // Setup action mappings
16
+ this.setupActionMap();
17
+ this.setupGamepadActionMap();
18
+
19
+ // Gamepad state
20
+ this.gamepads = new Map(); // Store gamepad states by index
21
+ this.gamepadDeadzone = 0.15; // Default deadzone for analog sticks
22
+ this.gamepadConnected = false;
23
+ this.gamepadKeyboardMirroring = true; // Default: gamepad inputs map to keyboard actions
24
+
25
+ // Raw state - continuously updated by events
26
+ this.rawState = {
27
+ keys: new Map(),
28
+ pointer: {
29
+ x: 0,
30
+ y: 0,
31
+ movementX: 0,
32
+ movementY: 0,
33
+ isDown: false,
34
+ downTimestamp: null,
35
+ buttons: {
36
+ left: false,
37
+ right: false,
38
+ middle: false
39
+ }
40
+ },
41
+ elements: {
42
+ gui: new Map(),
43
+ game: new Map(),
44
+ debug: new Map()
45
+ },
46
+ uiButtons: new Map([
47
+ ["soundToggle", { isPressed: false }],
48
+ ["controlsToggle", { isPressed: false }],
49
+ ["fullscreenToggle", { isPressed: false }],
50
+ ["pauseButton", { isPressed: false }]
51
+ ]),
52
+ virtualControlsVisible: false
53
+ };
54
+
55
+ // Frame snapshots - updated at frame boundaries
56
+ this.currentSnapshot = {
57
+ keys: new Map(),
58
+ mouseButtons: {
59
+ left: false,
60
+ right: false,
61
+ middle: false
62
+ },
63
+ pointer: {
64
+ isDown: false
65
+ },
66
+ elements: {
67
+ gui: new Map(),
68
+ game: new Map(),
69
+ debug: new Map()
70
+ },
71
+ elementsHovered: {
72
+ gui: new Map(),
73
+ game: new Map(),
74
+ debug: new Map()
75
+ },
76
+ uiButtons: new Map()
77
+ };
78
+
79
+ this.previousSnapshot = {
80
+ keys: new Map(),
81
+ mouseButtons: {
82
+ left: false,
83
+ right: false,
84
+ middle: false
85
+ },
86
+ pointer: {
87
+ isDown: false
88
+ },
89
+ elements: {
90
+ gui: new Map(),
91
+ game: new Map(),
92
+ debug: new Map()
93
+ },
94
+ elementsHovered: {
95
+ gui: new Map(),
96
+ game: new Map(),
97
+ debug: new Map()
98
+ },
99
+ uiButtons: new Map()
100
+ };
101
+
102
+ // Fixed snapshots - updated at fixed timesteps
103
+ this.currentFixedSnapshot = {
104
+ keys: new Map(),
105
+ mouseButtons: {
106
+ left: false,
107
+ right: false,
108
+ middle: false
109
+ },
110
+ pointer: {
111
+ isDown: false
112
+ },
113
+ elements: {
114
+ gui: new Map(),
115
+ game: new Map(),
116
+ debug: new Map()
117
+ },
118
+ elementsHovered: {
119
+ gui: new Map(),
120
+ game: new Map(),
121
+ debug: new Map()
122
+ },
123
+ uiButtons: new Map()
124
+ };
125
+
126
+ this.previousFixedSnapshot = {
127
+ keys: new Map(),
128
+ mouseButtons: {
129
+ left: false,
130
+ right: false,
131
+ middle: false
132
+ },
133
+ pointer: {
134
+ isDown: false
135
+ },
136
+ elements: {
137
+ gui: new Map(),
138
+ game: new Map(),
139
+ debug: new Map()
140
+ },
141
+ elementsHovered: {
142
+ gui: new Map(),
143
+ game: new Map(),
144
+ debug: new Map()
145
+ },
146
+ uiButtons: new Map()
147
+ };
148
+
149
+ // Setup keyboard event listeners
150
+ this.setupEventListeners();
151
+
152
+ // Setup UI elements
153
+ this.createUIControls();
154
+ this.createVirtualControls();
155
+
156
+ // Setup input listeners
157
+ this.setupPointerListeners();
158
+ this.setupVirtualButtons();
159
+ this.setupUIButtons();
160
+ this.setupGamepadListeners();
161
+
162
+ // Make game canvas focusable
163
+ if (this.canvases.gameCanvas) {
164
+ this.canvases.gameCanvas.tabIndex = 1;
165
+ this.canvases.gameCanvas.focus();
166
+ }
167
+ }
168
+
169
+ // Set the current execution context (update or fixed_update)
170
+ setContext(context) {
171
+ this.currentContext = context;
172
+ }
173
+
174
+ setupGamepadActionMap() {
175
+ // Standard gamepad button mapping (based on standard gamepad layout)
176
+ // Button indices follow the W3C Gamepad API standard mapping
177
+ this.gamepadActionMap = new Map([
178
+ // Face buttons (Xbox layout: A=0, B=1, X=2, Y=3)
179
+ [0, ["Action1"]], // A / Cross - Primary action
180
+ [1, ["Action2"]], // B / Circle - Secondary action
181
+ [2, ["Action3"]], // X / Square
182
+ [3, ["Action4"]], // Y / Triangle
183
+
184
+ // Shoulder buttons
185
+ [4, ["Action5"]], // Left Bumper (LB)
186
+ [5, ["Action6"]], // Right Bumper (RB)
187
+ [6, ["Action7"]], // Left Trigger (LT) when pressed as button
188
+ [7, ["Action8"]], // Right Trigger (RT) when pressed as button
189
+
190
+ // Menu buttons
191
+ [8, ["Action7"]], // Back/Select
192
+ [9, ["Action8"]], // Start
193
+
194
+ // Stick clicks
195
+ [10, ["Action9"]], // Left stick click (L3)
196
+ [11, ["Action10"]], // Right stick click (R3)
197
+
198
+ // D-pad
199
+ [12, ["DirUp"]],
200
+ [13, ["DirDown"]],
201
+ [14, ["DirLeft"]],
202
+ [15, ["DirRight"]]
203
+ ]);
204
+
205
+ // Axis mapping for analog sticks
206
+ // Standard gamepad axes: 0-1 = left stick (x, y), 2-3 = right stick (x, y)
207
+ this.gamepadAxisMap = new Map([
208
+ [0, { action: "AxisLeftX", inverted: false }],
209
+ [1, { action: "AxisLeftY", inverted: false }],
210
+ [2, { action: "AxisRightX", inverted: false }],
211
+ [3, { action: "AxisRightY", inverted: false }]
212
+ ]);
213
+ }
214
+
215
+ setupActionMap() {
216
+ this.actionMap = new Map([
217
+ ["KeyW", ["DirUp"]],
218
+ ["KeyS", ["DirDown"]],
219
+ ["KeyA", ["DirLeft"]],
220
+ ["KeyD", ["DirRight"]],
221
+ ["ArrowUp", ["DirUp"]],
222
+ ["ArrowDown", ["DirDown"]],
223
+ ["ArrowLeft", ["DirLeft"]],
224
+ ["ArrowRight", ["DirRight"]],
225
+ ["Space", ["Action1"]], // face button left
226
+ ["ShiftLeft", ["Action2"]], // face button down
227
+ ["KeyE", ["Action3"]], // face button right
228
+ ["KeyQ", ["Action4"]], // face button up
229
+ ["KeyZ", ["Action5"]], // Left Bumper
230
+ ["KeyX", ["Action6"]], // Right Bumper
231
+ ["KeyC", ["Action7"]], // Back Button
232
+ ["KeyF", ["Action8"]], // Start Button
233
+ ["F9", ["ActionDebugToggle"]],
234
+ ["F3", ["ActionDebugToggle"]],
235
+ ["Tab", ["ActionDebugToggle"]],
236
+
237
+ // Numpad keys
238
+ ["Numpad0", ["Numpad0"]],
239
+ ["Numpad1", ["Numpad1"]],
240
+ ["Numpad2", ["Numpad2"]],
241
+ ["Numpad3", ["Numpad3"]],
242
+ ["Numpad4", ["Numpad4"]],
243
+ ["Numpad5", ["Numpad5"]],
244
+ ["Numpad6", ["Numpad6"]],
245
+ ["Numpad7", ["Numpad7"]],
246
+ ["Numpad8", ["Numpad8"]],
247
+ ["Numpad9", ["Numpad9"]],
248
+ ["NumpadDecimal", ["NumpadDecimal"]], // Numpad period/del
249
+ ["NumpadEnter", ["NumpadEnter"]], // Numpad enter
250
+ ["NumpadAdd", ["NumpadAdd"]], // Numpad plus
251
+ ["NumpadSubtract", ["NumpadSubtract"]] // Numpad minus
252
+ ]);
253
+
254
+ // Extract all key codes the game uses from actionMap
255
+ this.gameKeyCodes = new Set();
256
+ for (const [keyCode, _] of this.actionMap) {
257
+ this.gameKeyCodes.add(keyCode);
258
+ }
259
+
260
+ // Add additional browser keys we want to block
261
+ const additionalBlockedKeys = ['F5'];
262
+ additionalBlockedKeys.forEach(key => this.gameKeyCodes.add(key));
263
+ }
264
+
265
+ setupGamepadListeners() {
266
+ // Listen for gamepad connection events
267
+ window.addEventListener("gamepadconnected", (e) => {
268
+ console.log(`[ActionInputHandler] Gamepad connected: ${e.gamepad.id} (index: ${e.gamepad.index})`);
269
+ this.gamepadConnected = true;
270
+ this.initializeGamepad(e.gamepad.index);
271
+ });
272
+
273
+ window.addEventListener("gamepaddisconnected", (e) => {
274
+ console.log(`[ActionInputHandler] Gamepad disconnected: ${e.gamepad.id} (index: ${e.gamepad.index})`);
275
+ this.gamepads.delete(e.gamepad.index);
276
+ if (this.gamepads.size === 0) {
277
+ this.gamepadConnected = false;
278
+ }
279
+ });
280
+ }
281
+
282
+ initializeGamepad(index) {
283
+ this.gamepads.set(index, {
284
+ buttons: new Map(),
285
+ axes: new Map(),
286
+ previousButtons: new Map(),
287
+ previousAxes: new Map()
288
+ });
289
+ }
290
+
291
+ pollGamepads() {
292
+ // Get current gamepad states from browser
293
+ const gamepads = navigator.getGamepads();
294
+
295
+ for (let i = 0; i < gamepads.length; i++) {
296
+ const gamepad = gamepads[i];
297
+ if (!gamepad) continue;
298
+
299
+ // Initialize if this is a new gamepad
300
+ if (!this.gamepads.has(i)) {
301
+ this.initializeGamepad(i);
302
+ }
303
+
304
+ const state = this.gamepads.get(i);
305
+
306
+ // Store previous state
307
+ state.previousButtons = new Map(state.buttons);
308
+ state.previousAxes = new Map(state.axes);
309
+
310
+ // Update button states AND inject into rawState.keys
311
+ gamepad.buttons.forEach((button, index) => {
312
+ state.buttons.set(index, {
313
+ pressed: button.pressed,
314
+ value: button.value
315
+ });
316
+
317
+ // Create a unique key for this gamepad button
318
+ const gamepadKey = `Gamepad${i}_Button${index}`;
319
+
320
+ // Update rawState.keys so it goes through snapshot system
321
+ if (button.pressed) {
322
+ this.rawState.keys.set(gamepadKey, true);
323
+ } else {
324
+ this.rawState.keys.delete(gamepadKey);
325
+ }
326
+ });
327
+
328
+ // Update axis states with deadzone
329
+ gamepad.axes.forEach((value, index) => {
330
+ const processedValue = Math.abs(value) < this.gamepadDeadzone ? 0 : value;
331
+ state.axes.set(index, processedValue);
332
+ });
333
+
334
+ // Map analog sticks to directional keys in rawState
335
+ const leftStick = {
336
+ x: state.axes.get(0) || 0,
337
+ y: state.axes.get(1) || 0
338
+ };
339
+
340
+ const threshold = 0.5;
341
+ const stickUpKey = `Gamepad${i}_StickUp`;
342
+ const stickDownKey = `Gamepad${i}_StickDown`;
343
+ const stickLeftKey = `Gamepad${i}_StickLeft`;
344
+ const stickRightKey = `Gamepad${i}_StickRight`;
345
+
346
+ // Update rawState based on stick position
347
+ if (leftStick.y < -threshold) {
348
+ this.rawState.keys.set(stickUpKey, true);
349
+ } else {
350
+ this.rawState.keys.delete(stickUpKey);
351
+ }
352
+
353
+ if (leftStick.y > threshold) {
354
+ this.rawState.keys.set(stickDownKey, true);
355
+ } else {
356
+ this.rawState.keys.delete(stickDownKey);
357
+ }
358
+
359
+ if (leftStick.x < -threshold) {
360
+ this.rawState.keys.set(stickLeftKey, true);
361
+ } else {
362
+ this.rawState.keys.delete(stickLeftKey);
363
+ }
364
+
365
+ if (leftStick.x > threshold) {
366
+ this.rawState.keys.set(stickRightKey, true);
367
+ } else {
368
+ this.rawState.keys.delete(stickRightKey);
369
+ }
370
+ }
371
+ }
372
+
373
+ setupEventListeners() {
374
+ // Keyboard event listeners
375
+ window.addEventListener("keydown", (e) => {
376
+ // Update raw state immediately
377
+ this.rawState.keys.set(e.code, true);
378
+
379
+ // Conditionally prevent default based on context
380
+ if (this.shouldPreventDefault(e)) {
381
+ e.preventDefault();
382
+ }
383
+ }, false);
384
+
385
+ window.addEventListener("keyup", (e) => {
386
+ // Update raw state immediately
387
+ this.rawState.keys.set(e.code, false);
388
+
389
+ // Conditionally prevent default based on context
390
+ if (this.shouldPreventDefault(e)) {
391
+ e.preventDefault();
392
+ }
393
+ }, false);
394
+
395
+ // Block context menu when we want to use right click
396
+ document.addEventListener('contextmenu', (e) => e.preventDefault());
397
+ }
398
+
399
+ shouldPreventDefault(event) {
400
+ // If ANY standard text input is focused, don't capture ANYTHING
401
+ const textInputFocused = document.activeElement?.matches(
402
+ 'input[type="text"], input[type="password"], input[type="search"], input[type="email"], input[type="url"], textarea, [contenteditable="true"]'
403
+ );
404
+
405
+ if (textInputFocused) {
406
+ return false; // Let ALL keys through to text input
407
+ }
408
+
409
+ // Otherwise, prevent default for game keys and special browser keys
410
+ return this.actionMap.has(event.code) ||
411
+ event.code === 'F5' ||
412
+ (event.ctrlKey && (event.code === 'KeyS' || event.code === 'KeyP' || event.code === 'KeyR')) ||
413
+ (event.altKey && event.code === 'ArrowLeft');
414
+ }
415
+
416
+ // Called by the engine at the start of each frame
417
+ captureKeyState() {
418
+ // Poll gamepads first
419
+ this.pollGamepads();
420
+ // Save current as previous - properly preserving Map objects
421
+ // Create deep copies of each component
422
+
423
+ // Copy key maps
424
+ this.previousSnapshot.keys = new Map(this.currentSnapshot.keys);
425
+
426
+ // Copy mouse button state
427
+ this.previousSnapshot.mouseButtons.left = this.currentSnapshot.mouseButtons.left;
428
+ this.previousSnapshot.mouseButtons.right = this.currentSnapshot.mouseButtons.right;
429
+ this.previousSnapshot.mouseButtons.middle = this.currentSnapshot.mouseButtons.middle;
430
+
431
+ // Copy pointer state
432
+ this.previousSnapshot.pointer.isDown = this.currentSnapshot.pointer.isDown;
433
+
434
+ // Copy element maps
435
+ for (const layer of Object.keys(this.currentSnapshot.elements)) {
436
+ this.previousSnapshot.elements[layer] = new Map(this.currentSnapshot.elements[layer]);
437
+ this.previousSnapshot.elementsHovered[layer] = new Map(this.currentSnapshot.elementsHovered[layer]);
438
+ }
439
+
440
+ // Copy UI button map
441
+ this.previousSnapshot.uiButtons = new Map(this.currentSnapshot.uiButtons);
442
+
443
+ // Capture current raw key state
444
+ this.currentSnapshot.keys = new Map();
445
+ for (const [key, isPressed] of this.rawState.keys.entries()) {
446
+ if (isPressed) {
447
+ this.currentSnapshot.keys.set(key, true);
448
+ }
449
+ }
450
+
451
+ // Capture current mouse state
452
+ this.currentSnapshot.pointer.isDown = this.rawState.pointer.isDown;
453
+ this.currentSnapshot.mouseButtons.left = this.rawState.pointer.buttons.left;
454
+ this.currentSnapshot.mouseButtons.right = this.rawState.pointer.buttons.right;
455
+ this.currentSnapshot.mouseButtons.middle = this.rawState.pointer.buttons.middle;
456
+
457
+ // Capture elements state
458
+ for (const layer of Object.keys(this.rawState.elements)) {
459
+ // Pressed state
460
+ this.currentSnapshot.elements[layer] = new Map();
461
+ this.rawState.elements[layer].forEach((element, id) => {
462
+ if (element.isPressed) {
463
+ this.currentSnapshot.elements[layer].set(id, true);
464
+ }
465
+ });
466
+
467
+ // Hover state
468
+ this.currentSnapshot.elementsHovered[layer] = new Map();
469
+ this.rawState.elements[layer].forEach((element, id) => {
470
+ if (element.isHovered) {
471
+ this.currentSnapshot.elementsHovered[layer].set(id, true);
472
+ }
473
+ });
474
+ }
475
+
476
+ // Capture UI button state
477
+ this.currentSnapshot.uiButtons = new Map();
478
+ for (const [id, buttonState] of this.rawState.uiButtons.entries()) {
479
+ if (buttonState.isPressed) {
480
+ this.currentSnapshot.uiButtons.set(id, true);
481
+ }
482
+ }
483
+ }
484
+
485
+ // Called by the engine before fixed updates begin
486
+ captureFixedKeyState() {
487
+ // Poll gamepads for fixed update as well
488
+ this.pollGamepads();
489
+ // Save current fixed state as previous fixed state - properly preserving Map objects
490
+
491
+ // Copy key maps
492
+ this.previousFixedSnapshot.keys = new Map(this.currentFixedSnapshot.keys);
493
+
494
+ // Copy mouse button state
495
+ this.previousFixedSnapshot.mouseButtons.left = this.currentFixedSnapshot.mouseButtons.left;
496
+ this.previousFixedSnapshot.mouseButtons.right = this.currentFixedSnapshot.mouseButtons.right;
497
+ this.previousFixedSnapshot.mouseButtons.middle = this.currentFixedSnapshot.mouseButtons.middle;
498
+
499
+ // Copy pointer state
500
+ this.previousFixedSnapshot.pointer.isDown = this.currentFixedSnapshot.pointer.isDown;
501
+
502
+ // Copy element maps
503
+ for (const layer of Object.keys(this.currentFixedSnapshot.elements)) {
504
+ this.previousFixedSnapshot.elements[layer] = new Map(this.currentFixedSnapshot.elements[layer]);
505
+ this.previousFixedSnapshot.elementsHovered[layer] = new Map(this.currentFixedSnapshot.elementsHovered[layer]);
506
+ }
507
+
508
+ // Copy UI button map
509
+ this.previousFixedSnapshot.uiButtons = new Map(this.currentFixedSnapshot.uiButtons);
510
+
511
+ // Capture current raw key state at this fixed frame
512
+ this.currentFixedSnapshot.keys = new Map();
513
+ for (const [key, isPressed] of this.rawState.keys.entries()) {
514
+ if (isPressed) {
515
+ this.currentFixedSnapshot.keys.set(key, true);
516
+ }
517
+ }
518
+
519
+ // Capture current mouse state at this fixed frame
520
+ this.currentFixedSnapshot.pointer.isDown = this.rawState.pointer.isDown;
521
+ this.currentFixedSnapshot.mouseButtons.left = this.rawState.pointer.buttons.left;
522
+ this.currentFixedSnapshot.mouseButtons.right = this.rawState.pointer.buttons.right;
523
+ this.currentFixedSnapshot.mouseButtons.middle = this.rawState.pointer.buttons.middle;
524
+
525
+ // Capture elements state at this fixed frame
526
+ for (const layer of Object.keys(this.rawState.elements)) {
527
+ // Pressed state
528
+ this.currentFixedSnapshot.elements[layer] = new Map();
529
+ this.rawState.elements[layer].forEach((element, id) => {
530
+ if (element.isPressed) {
531
+ this.currentFixedSnapshot.elements[layer].set(id, true);
532
+ }
533
+ });
534
+
535
+ // Hover state
536
+ this.currentFixedSnapshot.elementsHovered[layer] = new Map();
537
+ this.rawState.elements[layer].forEach((element, id) => {
538
+ if (element.isHovered) {
539
+ this.currentFixedSnapshot.elementsHovered[layer].set(id, true);
540
+ }
541
+ });
542
+ }
543
+
544
+ // Capture UI button state at this fixed frame
545
+ this.currentFixedSnapshot.uiButtons = new Map();
546
+ for (const [id, buttonState] of this.rawState.uiButtons.entries()) {
547
+ if (buttonState.isPressed) {
548
+ this.currentFixedSnapshot.uiButtons.set(id, true);
549
+ }
550
+ }
551
+ }
552
+
553
+ // Helper method to get the right snapshots based on context
554
+ getSnapshots() {
555
+ if (this.currentContext === 'fixed_update') {
556
+ return {
557
+ current: this.currentFixedSnapshot,
558
+ previous: this.previousFixedSnapshot
559
+ };
560
+ } else {
561
+ return {
562
+ current: this.currentSnapshot,
563
+ previous: this.previousSnapshot
564
+ };
565
+ }
566
+ }
567
+
568
+ createVirtualControlsContainer() {
569
+ const container = document.createElement("div");
570
+ container.id = "virtualControls";
571
+ container.classList.add("hidden");
572
+ document.getElementById("appContainer").appendChild(container);
573
+ return container;
574
+ }
575
+
576
+ createUIControls() {
577
+ const controlsToggleContainer = document.createElement("div");
578
+ controlsToggleContainer.id = "controlsToggleContainer";
579
+ const controlsToggle = document.createElement("button");
580
+ controlsToggle.id = "controlsToggle";
581
+ controlsToggle.className = "ui-button";
582
+ controlsToggle.setAttribute("aria-label", "Toggle Virtual Controls");
583
+ controlsToggle.textContent = "🖐️";
584
+ controlsToggleContainer.appendChild(controlsToggle);
585
+
586
+ const soundToggleContainer = document.createElement("div");
587
+ soundToggleContainer.id = "soundToggleContainer";
588
+ const soundToggle = document.createElement("button");
589
+ soundToggle.id = "soundToggle";
590
+ soundToggle.className = "ui-button";
591
+ soundToggle.setAttribute("aria-label", "Toggle Sound");
592
+ soundToggle.textContent = "🔊";
593
+ soundToggleContainer.appendChild(soundToggle);
594
+
595
+ const fullscreenToggleContainer = document.createElement("div");
596
+ fullscreenToggleContainer.id = "fullscreenToggleContainer";
597
+ const fullscreenToggle = document.createElement("button");
598
+ fullscreenToggle.id = "fullscreenToggle";
599
+ fullscreenToggle.className = "ui-button";
600
+ fullscreenToggle.setAttribute("aria-label", "Toggle Fullscreen");
601
+ fullscreenToggle.textContent = "↔️";
602
+ fullscreenToggleContainer.appendChild(fullscreenToggle);
603
+
604
+ const pauseButtonContainer = document.createElement("div");
605
+ pauseButtonContainer.id = "pauseButtonContainer";
606
+ const pauseButton = document.createElement("button");
607
+ pauseButton.id = "pauseButton";
608
+ pauseButton.className = "ui-button";
609
+ pauseButton.setAttribute("aria-label", "Pause");
610
+ pauseButton.textContent = "⏸️";
611
+ pauseButtonContainer.appendChild(pauseButton);
612
+
613
+ this.uiControlsContainer.appendChild(controlsToggleContainer);
614
+ this.uiControlsContainer.appendChild(soundToggleContainer);
615
+ this.uiControlsContainer.appendChild(fullscreenToggleContainer);
616
+ this.uiControlsContainer.appendChild(pauseButtonContainer);
617
+ }
618
+
619
+ createVirtualControls() {
620
+ const buttons = [
621
+ { id: "dpadUp", class: "dpad-button", key: "KeyW", text: "↑" },
622
+ { id: "dpadDown", class: "dpad-button", key: "KeyS", text: "↓" },
623
+ { id: "dpadLeft", class: "dpad-button", key: "KeyA", text: "←" },
624
+ { id: "dpadRight", class: "dpad-button", key: "KeyD", text: "→" },
625
+ { id: "button1", class: "action-button", key: "Space", text: "1" },
626
+ { id: "button2", class: "action-button", key: "ShiftLeft", text: "2" },
627
+ { id: "button3", class: "action-button", key: "KeyE", text: "3" },
628
+ { id: "button4", class: "action-button", key: "KeyQ", text: "4" }
629
+ ];
630
+
631
+ buttons.forEach((btn) => {
632
+ const container = document.createElement("div");
633
+ container.id = `${btn.id}Container`;
634
+
635
+ const button = document.createElement("button");
636
+ button.id = btn.id;
637
+ button.className = btn.class;
638
+ button.dataset.key = btn.key;
639
+ button.textContent = btn.text;
640
+
641
+ container.appendChild(button);
642
+ this.virtualControlsContainer.appendChild(container);
643
+ });
644
+ }
645
+
646
+ setupUIButtons() {
647
+ const buttons = {
648
+ soundToggle: {
649
+ element: document.getElementById("soundToggle"),
650
+ upCallback: () => {
651
+ const enabled = this.audio.toggle();
652
+ document.getElementById("soundToggle").textContent = enabled ? "🔊" : "🔇";
653
+ }
654
+ },
655
+ controlsToggle: {
656
+ element: document.getElementById("controlsToggle"),
657
+ upCallback: () => {
658
+ const enabled = this.toggleVirtualControls();
659
+ document.getElementById("controlsToggle").textContent = enabled ? "⬆️" : "🖐️";
660
+ }
661
+ },
662
+ fullscreenToggle: {
663
+ element: document.getElementById("fullscreenToggle"),
664
+ upCallback: () => {
665
+ const willBeEnabled = !document.fullscreenElement;
666
+ if (willBeEnabled) {
667
+ document.documentElement.requestFullscreen();
668
+ } else {
669
+ document.exitFullscreen();
670
+ }
671
+ }
672
+ },
673
+ pauseButton: {
674
+ element: document.getElementById("pauseButton"),
675
+ upCallback: () => {
676
+ const isPaused = this.togglePause();
677
+ document.getElementById("pauseButton").textContent = isPaused ? "▶️" : "⏸️";
678
+ }
679
+ }
680
+ };
681
+
682
+ Object.entries(buttons).forEach(([id, config]) => {
683
+ const handleStart = (e) => {
684
+ e.preventDefault();
685
+ this.rawState.uiButtons.set(id, { isPressed: true });
686
+ };
687
+
688
+ const handleEnd = (e) => {
689
+ e.preventDefault();
690
+ this.rawState.uiButtons.set(id, { isPressed: false });
691
+ config.upCallback();
692
+ };
693
+
694
+ config.element.addEventListener("touchstart", handleStart, { passive: false });
695
+ config.element.addEventListener("touchend", handleEnd, { passive: false });
696
+ config.element.addEventListener("mousedown", handleStart);
697
+ config.element.addEventListener("mouseup", handleEnd);
698
+ });
699
+ }
700
+
701
+ setupPointerListeners() {
702
+ // DEBUG LAYER
703
+ this.canvases.debugCanvas.addEventListener("mousemove", (e) => {
704
+ const pos = this.getCanvasPosition(e);
705
+ this.rawState.pointer.x = pos.x;
706
+ this.rawState.pointer.y = pos.y;
707
+ this.rawState.pointer.movementX = e.movementX || 0;
708
+ this.rawState.pointer.movementY = e.movementY || 0;
709
+
710
+ let handledByDebug = false;
711
+ this.rawState.elements.debug.forEach((element) => {
712
+ const wasHovered = element.isHovered;
713
+ element.isHovered = this.isPointInBounds(pos.x, pos.y, element.bounds());
714
+
715
+ if (!wasHovered && element.isHovered) {
716
+ element.hoverTimestamp = performance.now();
717
+ handledByDebug = true;
718
+ }
719
+ });
720
+
721
+ if (!handledByDebug) {
722
+ const newEvent = new MouseEvent("mousemove", e);
723
+ this.canvases.guiCanvas.dispatchEvent(newEvent);
724
+ }
725
+ });
726
+
727
+ this.canvases.debugCanvas.addEventListener("mousedown", (e) => {
728
+ const pos = this.getCanvasPosition(e);
729
+ this.rawState.pointer.x = pos.x;
730
+ this.rawState.pointer.y = pos.y;
731
+
732
+ // Track the specific button pressed
733
+ const button = e.button; // 0: left, 1: middle, 2: right
734
+
735
+ // Update button-specific state
736
+ if (button === 0) {
737
+ this.rawState.pointer.buttons.left = true;
738
+ // Maintain backward compatibility
739
+ this.rawState.pointer.isDown = true;
740
+ this.rawState.pointer.downTimestamp = performance.now();
741
+ }
742
+ if (button === 1) this.rawState.pointer.buttons.middle = true;
743
+ if (button === 2) this.rawState.pointer.buttons.right = true;
744
+
745
+ let handledByDebug = false;
746
+ this.rawState.elements.debug.forEach((element) => {
747
+ if (element.isHovered) {
748
+ element.isPressed = true;
749
+ handledByDebug = true;
750
+ }
751
+ });
752
+
753
+ if (!handledByDebug) {
754
+ const newEvent = new MouseEvent("mousedown", e);
755
+ this.canvases.guiCanvas.dispatchEvent(newEvent);
756
+ }
757
+ });
758
+
759
+ this.canvases.debugCanvas.addEventListener("mouseup", (e) => {
760
+ const pos = this.getCanvasPosition(e);
761
+ this.rawState.pointer.x = pos.x;
762
+ this.rawState.pointer.y = pos.y;
763
+
764
+ // Track the specific button released
765
+ const button = e.button; // 0: left, 1: middle, 2: right
766
+
767
+ // Update button-specific state
768
+ if (button === 0) {
769
+ this.rawState.pointer.buttons.left = false;
770
+ // Maintain backward compatibility
771
+ this.rawState.pointer.isDown = false;
772
+ this.rawState.pointer.downTimestamp = null;
773
+ }
774
+ if (button === 1) this.rawState.pointer.buttons.middle = false;
775
+ if (button === 2) this.rawState.pointer.buttons.right = false;
776
+
777
+ let handledByDebug = false;
778
+ this.rawState.elements.debug.forEach((element) => {
779
+ if (element.isPressed) {
780
+ element.isPressed = false;
781
+ handledByDebug = true;
782
+ }
783
+ });
784
+
785
+ if (!handledByDebug) {
786
+ const newEvent = new MouseEvent("mouseup", e);
787
+ this.canvases.guiCanvas.dispatchEvent(newEvent);
788
+ }
789
+ });
790
+
791
+ this.canvases.debugCanvas.addEventListener(
792
+ "touchstart",
793
+ (e) => {
794
+ e.preventDefault();
795
+ const pos = this.getCanvasPosition(e.touches[0]);
796
+ this.rawState.pointer.x = pos.x;
797
+ this.rawState.pointer.y = pos.y;
798
+
799
+ // For touch, always treat as left button
800
+ this.rawState.pointer.buttons.left = true;
801
+ this.rawState.pointer.isDown = true;
802
+ this.rawState.pointer.downTimestamp = performance.now();
803
+
804
+ let handledByDebug = false;
805
+ this.rawState.elements.debug.forEach((element) => {
806
+ if (this.isPointInBounds(pos.x, pos.y, element.bounds())) {
807
+ element.isPressed = true;
808
+ handledByDebug = true;
809
+ }
810
+ });
811
+
812
+ if (!handledByDebug) {
813
+ const newEvent = new TouchEvent("touchstart", e);
814
+ this.canvases.guiCanvas.dispatchEvent(newEvent);
815
+ }
816
+ },
817
+ { passive: false }
818
+ );
819
+
820
+ this.canvases.debugCanvas.addEventListener(
821
+ "touchend",
822
+ (e) => {
823
+ e.preventDefault();
824
+
825
+ // For touch, always treat as left button
826
+ this.rawState.pointer.buttons.left = false;
827
+ this.rawState.pointer.isDown = false;
828
+ this.rawState.pointer.downTimestamp = null;
829
+
830
+ let handledByDebug = false;
831
+ this.rawState.elements.debug.forEach((element) => {
832
+ if (element.isPressed) {
833
+ element.isPressed = false;
834
+ handledByDebug = true;
835
+ }
836
+ });
837
+
838
+ if (!handledByDebug) {
839
+ const newEvent = new TouchEvent("touchend", e);
840
+ this.canvases.guiCanvas.dispatchEvent(newEvent);
841
+ }
842
+ },
843
+ { passive: false }
844
+ );
845
+
846
+ this.canvases.debugCanvas.addEventListener(
847
+ "touchmove",
848
+ (e) => {
849
+ e.preventDefault();
850
+ const pos = this.getCanvasPosition(e.touches[0]);
851
+ this.rawState.pointer.x = pos.x;
852
+ this.rawState.pointer.y = pos.y;
853
+
854
+ let handledByDebug = false;
855
+ this.rawState.elements.debug.forEach((element) => {
856
+ const wasHovered = element.isHovered;
857
+ element.isHovered = this.isPointInBounds(pos.x, pos.y, element.bounds());
858
+
859
+ if (!wasHovered && element.isHovered) {
860
+ element.hoverTimestamp = performance.now();
861
+ handledByDebug = true;
862
+ }
863
+ });
864
+
865
+ if (!handledByDebug) {
866
+ const newEvent = new TouchEvent("touchmove", e);
867
+ this.canvases.guiCanvas.dispatchEvent(newEvent);
868
+ }
869
+ },
870
+ { passive: false }
871
+ );
872
+
873
+ // GUI LAYER
874
+ this.canvases.guiCanvas.addEventListener("mousemove", (e) => {
875
+ const pos = this.getCanvasPosition(e);
876
+ this.rawState.pointer.x = pos.x;
877
+ this.rawState.pointer.y = pos.y;
878
+ this.rawState.pointer.movementX = e.movementX || 0;
879
+ this.rawState.pointer.movementY = e.movementY || 0;
880
+
881
+ let handledByGui = false;
882
+ this.rawState.elements.gui.forEach((element) => {
883
+ const wasHovered = element.isHovered;
884
+ element.isHovered = this.isPointInBounds(pos.x, pos.y, element.bounds());
885
+
886
+ if (!wasHovered && element.isHovered) {
887
+ element.hoverTimestamp = performance.now();
888
+ handledByGui = true;
889
+ }
890
+ });
891
+
892
+ if (!handledByGui) {
893
+ const newEvent = new MouseEvent("mousemove", e);
894
+ this.canvases.gameCanvas.dispatchEvent(newEvent);
895
+ }
896
+ });
897
+
898
+ this.canvases.guiCanvas.addEventListener("mousedown", (e) => {
899
+ const pos = this.getCanvasPosition(e);
900
+ this.rawState.pointer.x = pos.x;
901
+ this.rawState.pointer.y = pos.y;
902
+
903
+ // Track the specific button pressed
904
+ const button = e.button; // 0: left, 1: middle, 2: right
905
+
906
+ // Update button-specific state
907
+ if (button === 0) {
908
+ this.rawState.pointer.buttons.left = true;
909
+ // Maintain backward compatibility
910
+ this.rawState.pointer.isDown = true;
911
+ this.rawState.pointer.downTimestamp = performance.now();
912
+ }
913
+ if (button === 1) this.rawState.pointer.buttons.middle = true;
914
+ if (button === 2) this.rawState.pointer.buttons.right = true;
915
+
916
+ let handledByGui = false;
917
+ this.rawState.elements.gui.forEach((element) => {
918
+ if (element.isHovered) {
919
+ element.isPressed = true;
920
+ handledByGui = true;
921
+ }
922
+ });
923
+
924
+ if (!handledByGui) {
925
+ const newEvent = new MouseEvent("mousedown", e);
926
+ this.canvases.gameCanvas.dispatchEvent(newEvent);
927
+ }
928
+ });
929
+
930
+ this.canvases.guiCanvas.addEventListener("mouseup", (e) => {
931
+ const pos = this.getCanvasPosition(e);
932
+ this.rawState.pointer.x = pos.x;
933
+ this.rawState.pointer.y = pos.y;
934
+
935
+ // Track the specific button released
936
+ const button = e.button; // 0: left, 1: middle, 2: right
937
+
938
+ // Update button-specific state
939
+ if (button === 0) {
940
+ this.rawState.pointer.buttons.left = false;
941
+ // Maintain backward compatibility
942
+ this.rawState.pointer.isDown = false;
943
+ this.rawState.pointer.downTimestamp = null;
944
+ }
945
+ if (button === 1) this.rawState.pointer.buttons.middle = false;
946
+ if (button === 2) this.rawState.pointer.buttons.right = false;
947
+
948
+ let handledByGui = false;
949
+ this.rawState.elements.gui.forEach((element) => {
950
+ if (element.isPressed) {
951
+ element.isPressed = false;
952
+ handledByGui = true;
953
+ }
954
+ });
955
+
956
+ if (!handledByGui) {
957
+ const newEvent = new MouseEvent("mouseup", e);
958
+ this.canvases.gameCanvas.dispatchEvent(newEvent);
959
+ }
960
+ });
961
+
962
+ this.canvases.guiCanvas.addEventListener(
963
+ "touchstart",
964
+ (e) => {
965
+ e.preventDefault();
966
+ const pos = this.getCanvasPosition(e.touches[0]);
967
+ this.rawState.pointer.x = pos.x;
968
+ this.rawState.pointer.y = pos.y;
969
+
970
+ // For touch, always treat as left button
971
+ this.rawState.pointer.buttons.left = true;
972
+ this.rawState.pointer.isDown = true;
973
+ this.rawState.pointer.downTimestamp = performance.now();
974
+
975
+ let handledByGui = false;
976
+ this.rawState.elements.gui.forEach((element) => {
977
+ if (this.isPointInBounds(pos.x, pos.y, element.bounds())) {
978
+ element.isPressed = true;
979
+ handledByGui = true;
980
+ }
981
+ });
982
+
983
+ if (!handledByGui) {
984
+ const newEvent = new TouchEvent("touchstart", e);
985
+ this.canvases.gameCanvas.dispatchEvent(newEvent);
986
+ }
987
+ },
988
+ { passive: false }
989
+ );
990
+
991
+ this.canvases.guiCanvas.addEventListener(
992
+ "touchend",
993
+ (e) => {
994
+ e.preventDefault();
995
+
996
+ // For touch, always treat as left button
997
+ this.rawState.pointer.buttons.left = false;
998
+ this.rawState.pointer.isDown = false;
999
+ this.rawState.pointer.downTimestamp = null;
1000
+
1001
+ let handledByGui = false;
1002
+ this.rawState.elements.gui.forEach((element) => {
1003
+ if (element.isPressed) {
1004
+ element.isPressed = false;
1005
+ handledByGui = true;
1006
+ }
1007
+ });
1008
+
1009
+ if (!handledByGui) {
1010
+ const newEvent = new TouchEvent("touchend", e);
1011
+ this.canvases.gameCanvas.dispatchEvent(newEvent);
1012
+ }
1013
+ },
1014
+ { passive: false }
1015
+ );
1016
+
1017
+ this.canvases.guiCanvas.addEventListener(
1018
+ "touchmove",
1019
+ (e) => {
1020
+ e.preventDefault();
1021
+ const pos = this.getCanvasPosition(e.touches[0]);
1022
+ this.rawState.pointer.x = pos.x;
1023
+ this.rawState.pointer.y = pos.y;
1024
+
1025
+ let handledByGui = false;
1026
+ this.rawState.elements.gui.forEach((element) => {
1027
+ const wasHovered = element.isHovered;
1028
+ element.isHovered = this.isPointInBounds(pos.x, pos.y, element.bounds());
1029
+
1030
+ if (!wasHovered && element.isHovered) {
1031
+ element.hoverTimestamp = performance.now();
1032
+ handledByGui = true;
1033
+ }
1034
+ });
1035
+
1036
+ if (!handledByGui) {
1037
+ const newEvent = new TouchEvent("touchmove", e);
1038
+ this.canvases.gameCanvas.dispatchEvent(newEvent);
1039
+ }
1040
+ },
1041
+ { passive: false }
1042
+ );
1043
+
1044
+ // GAME LAYER
1045
+ this.canvases.gameCanvas.addEventListener("mousemove", (e) => {
1046
+ const pos = this.getCanvasPosition(e);
1047
+ this.rawState.pointer.x = pos.x;
1048
+ this.rawState.pointer.y = pos.y;
1049
+ this.rawState.pointer.movementX = e.movementX || 0;
1050
+ this.rawState.pointer.movementY = e.movementY || 0;
1051
+
1052
+ this.rawState.elements.game.forEach((element) => {
1053
+ const wasHovered = element.isHovered;
1054
+ element.isHovered = this.isPointInBounds(pos.x, pos.y, element.bounds());
1055
+
1056
+ if (!wasHovered && element.isHovered) {
1057
+ element.hoverTimestamp = performance.now();
1058
+ }
1059
+ });
1060
+ });
1061
+
1062
+ this.canvases.gameCanvas.addEventListener("mousedown", (e) => {
1063
+ const pos = this.getCanvasPosition(e);
1064
+ this.rawState.pointer.x = pos.x;
1065
+ this.rawState.pointer.y = pos.y;
1066
+
1067
+ // Track the specific button pressed
1068
+ const button = e.button; // 0: left, 1: middle, 2: right
1069
+
1070
+ // Update button-specific state
1071
+ if (button === 0) {
1072
+ this.rawState.pointer.buttons.left = true;
1073
+ // Maintain backward compatibility
1074
+ this.rawState.pointer.isDown = true;
1075
+ this.rawState.pointer.downTimestamp = performance.now();
1076
+ }
1077
+ if (button === 1) this.rawState.pointer.buttons.middle = true;
1078
+ if (button === 2) this.rawState.pointer.buttons.right = true;
1079
+
1080
+ this.rawState.elements.game.forEach((element) => {
1081
+ if (element.isHovered) {
1082
+ element.isPressed = true;
1083
+ }
1084
+ });
1085
+ });
1086
+
1087
+ this.canvases.gameCanvas.addEventListener("mouseup", (e) => {
1088
+ const pos = this.getCanvasPosition(e);
1089
+ this.rawState.pointer.x = pos.x;
1090
+ this.rawState.pointer.y = pos.y;
1091
+
1092
+ // Track the specific button released
1093
+ const button = e.button; // 0: left, 1: middle, 2: right
1094
+
1095
+ // Update button-specific state
1096
+ if (button === 0) {
1097
+ this.rawState.pointer.buttons.left = false;
1098
+ // Maintain backward compatibility
1099
+ this.rawState.pointer.isDown = false;
1100
+ this.rawState.pointer.downTimestamp = null;
1101
+ }
1102
+ if (button === 1) this.rawState.pointer.buttons.middle = false;
1103
+ if (button === 2) this.rawState.pointer.buttons.right = false;
1104
+
1105
+ this.rawState.elements.game.forEach((element) => {
1106
+ if (element.isPressed) {
1107
+ element.isPressed = false;
1108
+ }
1109
+ });
1110
+ });
1111
+
1112
+ this.canvases.gameCanvas.addEventListener(
1113
+ "touchstart",
1114
+ (e) => {
1115
+ e.preventDefault();
1116
+ const pos = this.getCanvasPosition(e.touches[0]);
1117
+ this.rawState.pointer.x = pos.x;
1118
+ this.rawState.pointer.y = pos.y;
1119
+
1120
+ // For touch, always treat as left button
1121
+ this.rawState.pointer.buttons.left = true;
1122
+ this.rawState.pointer.isDown = true;
1123
+ this.rawState.pointer.downTimestamp = performance.now();
1124
+
1125
+ this.rawState.elements.game.forEach((element) => {
1126
+ if (this.isPointInBounds(pos.x, pos.y, element.bounds())) {
1127
+ element.isPressed = true;
1128
+ }
1129
+ });
1130
+ },
1131
+ { passive: false }
1132
+ );
1133
+
1134
+ this.canvases.gameCanvas.addEventListener(
1135
+ "touchend",
1136
+ (e) => {
1137
+ e.preventDefault();
1138
+
1139
+ // For touch, always treat as left button
1140
+ this.rawState.pointer.buttons.left = false;
1141
+ this.rawState.pointer.isDown = false;
1142
+ this.rawState.pointer.downTimestamp = null;
1143
+
1144
+ this.rawState.elements.game.forEach((element) => {
1145
+ if (element.isPressed) {
1146
+ element.isPressed = false;
1147
+ }
1148
+ });
1149
+ },
1150
+ { passive: false }
1151
+ );
1152
+
1153
+ this.canvases.gameCanvas.addEventListener(
1154
+ "touchmove",
1155
+ (e) => {
1156
+ e.preventDefault();
1157
+ const pos = this.getCanvasPosition(e.touches[0]);
1158
+ this.rawState.pointer.x = pos.x;
1159
+ this.rawState.pointer.y = pos.y;
1160
+
1161
+ this.rawState.elements.game.forEach((element) => {
1162
+ const wasHovered = element.isHovered;
1163
+ element.isHovered = this.isPointInBounds(pos.x, pos.y, element.bounds());
1164
+
1165
+ if (!wasHovered && element.isHovered) {
1166
+ element.hoverTimestamp = performance.now();
1167
+ }
1168
+ });
1169
+ },
1170
+ { passive: false }
1171
+ );
1172
+
1173
+ document.addEventListener("mousemove", (e) => {
1174
+ if (document.pointerLockElement) {
1175
+ this.rawState.pointer.movementX = e.movementX;
1176
+ this.rawState.pointer.movementY = e.movementY;
1177
+ }
1178
+ });
1179
+ }
1180
+
1181
+ getLockedPointerMovement() {
1182
+ if (!document.pointerLockElement) {
1183
+ return { x: 0, y: 0 };
1184
+ }
1185
+ // Return the raw movement values
1186
+ return {
1187
+ x: this.rawState.pointer.movementX,
1188
+ y: this.rawState.pointer.movementY
1189
+ };
1190
+ }
1191
+
1192
+ getCanvasPosition(e) {
1193
+ const canvas = document.getElementById("gameCanvas");
1194
+ const rect = canvas.getBoundingClientRect();
1195
+ const scaleX = canvas.width / rect.width;
1196
+ const scaleY = canvas.height / rect.height;
1197
+
1198
+ return {
1199
+ x: (e.clientX - rect.left) * scaleX,
1200
+ y: (e.clientY - rect.top) * scaleY
1201
+ };
1202
+ }
1203
+
1204
+ isPointInBounds(x, y, bounds) {
1205
+ // Use simple top-left based collision detection
1206
+ return x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height;
1207
+ }
1208
+
1209
+ setupVirtualButtons() {
1210
+ const buttons = document.querySelectorAll(".dpad-button, .action-button");
1211
+
1212
+ buttons.forEach((button) => {
1213
+ const key = button.dataset.key;
1214
+
1215
+ const handleStart = (e) => {
1216
+ e.preventDefault();
1217
+ this.rawState.keys.set(key, true);
1218
+ };
1219
+
1220
+ const handleEnd = (e) => {
1221
+ e.preventDefault();
1222
+ this.rawState.keys.set(key, false);
1223
+ };
1224
+
1225
+ button.addEventListener("touchstart", handleStart, { passive: false });
1226
+ button.addEventListener("touchend", handleEnd, { passive: false });
1227
+ button.addEventListener("mousedown", handleStart);
1228
+ button.addEventListener("mouseup", handleEnd);
1229
+ button.addEventListener("mouseleave", handleEnd);
1230
+ });
1231
+ }
1232
+
1233
+ registerElement(id, element, layer = "gui") {
1234
+ if (!this.rawState.elements[layer]) {
1235
+ console.warn(`[ActionInputHandler] Layer ${layer} doesn't exist, defaulting to gui`);
1236
+ layer = "gui";
1237
+ }
1238
+
1239
+ this.rawState.elements[layer].set(id, {
1240
+ bounds: element.bounds,
1241
+ isHovered: false,
1242
+ hoverTimestamp: null, // Keep for compatibility
1243
+ isPressed: false,
1244
+ isActive: false,
1245
+ activeTimestamp: null
1246
+ });
1247
+ }
1248
+
1249
+ // CONTEXT-AWARE API METHODS FOR GAME CODE
1250
+
1251
+ setElementActive(id, layer, isActive) {
1252
+ const element = this.rawState.elements[layer]?.get(id);
1253
+ if (element) {
1254
+ element.isActive = isActive;
1255
+ }
1256
+ }
1257
+
1258
+ isElementJustPressed(id, layer = "gui") {
1259
+ const { current, previous } = this.getSnapshots();
1260
+
1261
+ const isCurrentlyPressed = current.elements[layer]?.has(id);
1262
+ const wasPreviouslyPressed = previous.elements[layer]?.has(id);
1263
+
1264
+ // Element is pressed now but wasn't in the previous frame/fixed frame step
1265
+ return isCurrentlyPressed && !wasPreviouslyPressed;
1266
+ }
1267
+
1268
+ isElementPressed(id, layer = "gui") {
1269
+ const { current } = this.getSnapshots();
1270
+ return current.elements[layer]?.has(id) || false;
1271
+ }
1272
+
1273
+ isElementJustHovered(id, layer = "gui") {
1274
+ const { current, previous } = this.getSnapshots();
1275
+
1276
+ const isCurrentlyHovered = current.elementsHovered[layer]?.has(id);
1277
+ const wasPreviouslyHovered = previous.elementsHovered[layer]?.has(id);
1278
+
1279
+ // Element is hovered now but wasn't in the previous frame/fixed frame step
1280
+ return isCurrentlyHovered && !wasPreviouslyHovered;
1281
+ }
1282
+
1283
+ isElementHovered(id, layer = "gui") {
1284
+ const { current } = this.getSnapshots();
1285
+ return current.elementsHovered[layer]?.has(id) || false;
1286
+ }
1287
+
1288
+ isElementActive(id, layer = "gui") {
1289
+ const element = this.rawState.elements[layer]?.get(id);
1290
+ return element ? element.isActive : false;
1291
+ }
1292
+
1293
+ // Legacy pointer methods for backward compatibility
1294
+ isPointerDown() {
1295
+ const { current } = this.getSnapshots();
1296
+ return current.pointer.isDown;
1297
+ }
1298
+
1299
+ isPointerJustDown() {
1300
+ const { current, previous } = this.getSnapshots();
1301
+ // Pointer is down now but wasn't in the previous frame/fixed frame step
1302
+ return current.pointer.isDown && !previous.pointer.isDown;
1303
+ }
1304
+
1305
+ // Mouse button methods
1306
+ isLeftMouseButtonDown() {
1307
+ const { current } = this.getSnapshots();
1308
+ return current.mouseButtons.left;
1309
+ }
1310
+
1311
+ isRightMouseButtonDown() {
1312
+ const { current } = this.getSnapshots();
1313
+ return current.mouseButtons.right;
1314
+ }
1315
+
1316
+ isMiddleMouseButtonDown() {
1317
+ const { current } = this.getSnapshots();
1318
+ return current.mouseButtons.middle;
1319
+ }
1320
+
1321
+ isLeftMouseButtonJustPressed() {
1322
+ const { current, previous } = this.getSnapshots();
1323
+ return current.mouseButtons.left && !previous.mouseButtons.left;
1324
+ }
1325
+
1326
+ isRightMouseButtonJustPressed() {
1327
+ const { current, previous } = this.getSnapshots();
1328
+ return current.mouseButtons.right && !previous.mouseButtons.right;
1329
+ }
1330
+
1331
+ isMiddleMouseButtonJustPressed() {
1332
+ const { current, previous } = this.getSnapshots();
1333
+ return current.mouseButtons.middle && !previous.mouseButtons.middle;
1334
+ }
1335
+
1336
+ // Generic mouse button method
1337
+ isMouseButtonDown(button) {
1338
+ const { current } = this.getSnapshots();
1339
+ // button: 0=left, 1=middle, 2=right
1340
+ if (button === 0) return current.mouseButtons.left;
1341
+ if (button === 1) return current.mouseButtons.middle;
1342
+ if (button === 2) return current.mouseButtons.right;
1343
+ return false;
1344
+ }
1345
+
1346
+ isMouseButtonJustPressed(button) {
1347
+ const { current, previous } = this.getSnapshots();
1348
+ // button: 0=left, 1=middle, 2=right
1349
+ if (button === 0) return current.mouseButtons.left && !previous.mouseButtons.left;
1350
+ if (button === 1) return current.mouseButtons.middle && !previous.mouseButtons.middle;
1351
+ if (button === 2) return current.mouseButtons.right && !previous.mouseButtons.right;
1352
+ return false;
1353
+ }
1354
+
1355
+ // UI Button methods
1356
+ isUIButtonPressed(buttonId) {
1357
+ const { current } = this.getSnapshots();
1358
+ return current.uiButtons.has(buttonId);
1359
+ }
1360
+
1361
+ isUIButtonJustPressed(buttonId) {
1362
+ const { current, previous } = this.getSnapshots();
1363
+
1364
+ const isCurrentlyPressed = current.uiButtons.has(buttonId);
1365
+ const wasPreviouslyPressed = previous.uiButtons.has(buttonId);
1366
+
1367
+ // Button is pressed now but wasn't in the previous frame/fixed frame step
1368
+ return isCurrentlyPressed && !wasPreviouslyPressed;
1369
+ }
1370
+
1371
+ // Game state toggle methods
1372
+ togglePause() {
1373
+ this.isPaused = !this.isPaused;
1374
+ return this.isPaused;
1375
+ }
1376
+
1377
+ toggleVirtualControls() {
1378
+ this.rawState.virtualControlsVisible = !this.rawState.virtualControlsVisible;
1379
+ this.virtualControlsContainer.classList.toggle("hidden", !this.rawState.virtualControlsVisible);
1380
+ return this.rawState.virtualControlsVisible;
1381
+ }
1382
+
1383
+ // Gamepad Methods - Direct per-gamepad access
1384
+
1385
+ isGamepadButtonPressed(buttonIndex, gamepadIndex = 0) {
1386
+ const { current } = this.getSnapshots();
1387
+
1388
+ // Check if this gamepad exists
1389
+ if (!this.gamepads.has(gamepadIndex)) return false;
1390
+
1391
+ // Check snapshot for this specific gamepad button
1392
+ const gamepadKey = `Gamepad${gamepadIndex}_Button${buttonIndex}`;
1393
+ return current.keys.has(gamepadKey);
1394
+ }
1395
+
1396
+ isGamepadButtonJustPressed(buttonIndex, gamepadIndex = 0) {
1397
+ const { current, previous } = this.getSnapshots();
1398
+
1399
+ // Check if this gamepad exists
1400
+ if (!this.gamepads.has(gamepadIndex)) return false;
1401
+
1402
+ // Check snapshot for just pressed on this specific gamepad
1403
+ const gamepadKey = `Gamepad${gamepadIndex}_Button${buttonIndex}`;
1404
+ const isCurrentlyPressed = current.keys.has(gamepadKey);
1405
+ const wasPreviouslyPressed = previous.keys.has(gamepadKey);
1406
+
1407
+ return isCurrentlyPressed && !wasPreviouslyPressed;
1408
+ }
1409
+
1410
+ getGamepadAxis(axisIndex, gamepadIndex = 0) {
1411
+ const gamepad = this.gamepads.get(gamepadIndex);
1412
+ if (!gamepad) return 0;
1413
+
1414
+ return gamepad.axes.get(axisIndex) || 0;
1415
+ }
1416
+
1417
+ getGamepadLeftStick(gamepadIndex = 0) {
1418
+ return {
1419
+ x: this.getGamepadAxis(0, gamepadIndex),
1420
+ y: this.getGamepadAxis(1, gamepadIndex)
1421
+ };
1422
+ }
1423
+
1424
+ getGamepadRightStick(gamepadIndex = 0) {
1425
+ return {
1426
+ x: this.getGamepadAxis(2, gamepadIndex),
1427
+ y: this.getGamepadAxis(3, gamepadIndex)
1428
+ };
1429
+ }
1430
+
1431
+ isGamepadConnected(gamepadIndex = 0) {
1432
+ return this.gamepads.has(gamepadIndex);
1433
+ }
1434
+
1435
+ getConnectedGamepads() {
1436
+ return Array.from(this.gamepads.keys());
1437
+ }
1438
+
1439
+ setGamepadDeadzone(deadzone) {
1440
+ this.gamepadDeadzone = Math.max(0, Math.min(1, deadzone));
1441
+ }
1442
+
1443
+ setGamepadKeyboardMirroring(enabled) {
1444
+ this.gamepadKeyboardMirroring = enabled;
1445
+ }
1446
+
1447
+ isGamepadKeyboardMirroringEnabled() {
1448
+ return this.gamepadKeyboardMirroring;
1449
+ }
1450
+
1451
+ // Map gamepad button to custom action
1452
+ mapGamepadButton(buttonIndex, action) {
1453
+ if (!this.gamepadActionMap.has(buttonIndex)) {
1454
+ this.gamepadActionMap.set(buttonIndex, []);
1455
+ }
1456
+ const actions = this.gamepadActionMap.get(buttonIndex);
1457
+ if (!actions.includes(action)) {
1458
+ actions.push(action);
1459
+ }
1460
+ }
1461
+
1462
+ // Remove gamepad button mapping
1463
+ unmapGamepadButton(buttonIndex, action) {
1464
+ if (!this.gamepadActionMap.has(buttonIndex)) return;
1465
+
1466
+ const actions = this.gamepadActionMap.get(buttonIndex);
1467
+ const index = actions.indexOf(action);
1468
+ if (index !== -1) {
1469
+ actions.splice(index, 1);
1470
+ if (actions.length === 0) {
1471
+ this.gamepadActionMap.delete(buttonIndex);
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ // Key check methods (now includes gamepad support)
1477
+ isKeyPressed(action) {
1478
+ const { current } = this.getSnapshots();
1479
+
1480
+ // Check keyboard
1481
+ for (const [key, actions] of this.actionMap) {
1482
+ if (actions.includes(action)) {
1483
+ if (current.keys.has(key)) return true;
1484
+ }
1485
+ }
1486
+
1487
+ // Only check gamepad if mirroring is enabled
1488
+ if (!this.gamepadKeyboardMirroring) {
1489
+ return false;
1490
+ }
1491
+
1492
+ // Check gamepad buttons via the snapshot system
1493
+ for (const [buttonIndex, actions] of this.gamepadActionMap) {
1494
+ if (actions.includes(action)) {
1495
+ // Check all connected gamepads
1496
+ for (const gamepadIndex of this.gamepads.keys()) {
1497
+ const gamepadKey = `Gamepad${gamepadIndex}_Button${buttonIndex}`;
1498
+ if (current.keys.has(gamepadKey)) {
1499
+ return true;
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ // Check analog stick as directional input via snapshot system
1506
+ if (action === "DirUp" || action === "DirDown" || action === "DirLeft" || action === "DirRight") {
1507
+ for (const gamepadIndex of this.gamepads.keys()) {
1508
+ let stickKey;
1509
+ if (action === "DirUp") stickKey = `Gamepad${gamepadIndex}_StickUp`;
1510
+ if (action === "DirDown") stickKey = `Gamepad${gamepadIndex}_StickDown`;
1511
+ if (action === "DirLeft") stickKey = `Gamepad${gamepadIndex}_StickLeft`;
1512
+ if (action === "DirRight") stickKey = `Gamepad${gamepadIndex}_StickRight`;
1513
+
1514
+ if (current.keys.has(stickKey)) {
1515
+ return true;
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ return false;
1521
+ }
1522
+
1523
+ isKeyJustPressed(action) {
1524
+ const { current, previous } = this.getSnapshots();
1525
+
1526
+ // Check keyboard
1527
+ for (const [key, actions] of this.actionMap) {
1528
+ if (actions.includes(action)) {
1529
+ const isCurrentlyPressed = current.keys.has(key);
1530
+ const wasPreviouslyPressed = previous.keys.has(key);
1531
+
1532
+ if (isCurrentlyPressed && !wasPreviouslyPressed) {
1533
+ return true;
1534
+ }
1535
+ }
1536
+ }
1537
+
1538
+ // Only check gamepad if mirroring is enabled
1539
+ if (!this.gamepadKeyboardMirroring) {
1540
+ return false;
1541
+ }
1542
+
1543
+ // Check gamepad buttons via snapshot system
1544
+ for (const [buttonIndex, actions] of this.gamepadActionMap) {
1545
+ if (actions.includes(action)) {
1546
+ for (const gamepadIndex of this.gamepads.keys()) {
1547
+ const gamepadKey = `Gamepad${gamepadIndex}_Button${buttonIndex}`;
1548
+ const isCurrentlyPressed = current.keys.has(gamepadKey);
1549
+ const wasPreviouslyPressed = previous.keys.has(gamepadKey);
1550
+
1551
+ if (isCurrentlyPressed && !wasPreviouslyPressed) {
1552
+ return true;
1553
+ }
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ return false;
1559
+ }
1560
+
1561
+ getPointerPosition() {
1562
+ return {
1563
+ x: this.rawState.pointer.x,
1564
+ y: this.rawState.pointer.y,
1565
+ movementX: this.rawState.pointer.movementX,
1566
+ movementY: this.rawState.pointer.movementY
1567
+ };
1568
+ }
1569
+
1570
+ removeElement(id, layer = "gui") {
1571
+ if (!this.rawState.elements[layer]) {
1572
+ console.warn(`[ActionInputHandler] Layer ${layer} doesn't exist`);
1573
+ return false;
1574
+ }
1575
+ return this.rawState.elements[layer].delete(id);
1576
+ }
1577
+
1578
+ clearLayerElements(layer = "gui") {
1579
+ if (!this.rawState.elements[layer]) {
1580
+ console.warn(`[ActionInputHandler] Layer ${layer} doesn't exist`);
1581
+ return false;
1582
+ }
1583
+ this.rawState.elements[layer].clear();
1584
+ return true;
1585
+ }
1586
+
1587
+ clearAllElements() {
1588
+ Object.keys(this.rawState.elements).forEach((layer) => {
1589
+ this.rawState.elements[layer].clear();
1590
+ });
1591
+ }
1592
+
1593
+ // Method to get all registered actions
1594
+ getRegisteredActions() {
1595
+ const actions = new Set();
1596
+ for (const [_, actionsList] of this.actionMap) {
1597
+ actionsList.forEach(action => actions.add(action));
1598
+ }
1599
+ return Array.from(actions);
1600
+ }
1601
+
1602
+ // Raw key access methods
1603
+ isRawKeyPressed(keyCode) {
1604
+ const { current } = this.getSnapshots();
1605
+ return current.keys.has(keyCode);
1606
+ }
1607
+
1608
+ isRawKeyJustPressed(keyCode) {
1609
+ const { current, previous } = this.getSnapshots();
1610
+
1611
+ const isCurrentlyPressed = current.keys.has(keyCode);
1612
+ const wasPreviouslyPressed = previous.keys.has(keyCode);
1613
+
1614
+ // Key is pressed now but wasn't in the previous frame/fixed frame step
1615
+ return isCurrentlyPressed && !wasPreviouslyPressed;
1616
+ }
1617
+
1618
+ // Dynamic action registration
1619
+ registerAction(actionName, keyCodes) {
1620
+ // Allow developers to register new actions dynamically
1621
+ if (typeof keyCodes === 'string') keyCodes = [keyCodes];
1622
+
1623
+ for (const keyCode of keyCodes) {
1624
+ if (!this.actionMap.has(keyCode)) {
1625
+ this.actionMap.set(keyCode, []);
1626
+ }
1627
+ this.actionMap.get(keyCode).push(actionName);
1628
+ this.gameKeyCodes.add(keyCode); // Add to blocked keys
1629
+ }
1630
+ }
1631
+
1632
+ unregisterAction(actionName) {
1633
+ // Remove an action from all key mappings
1634
+ for (const [keyCode, actions] of this.actionMap) {
1635
+ const index = actions.indexOf(actionName);
1636
+ if (index !== -1) {
1637
+ actions.splice(index, 1);
1638
+ if (actions.length === 0) {
1639
+ this.actionMap.delete(keyCode);
1640
+ this.gameKeyCodes.delete(keyCode);
1641
+ }
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+
1647
+ }