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.
- package/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- 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
|
+
}
|