emeraldengine 2.2.1 → 3.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 (162) hide show
  1. package/README.md +2359 -968
  2. package/dist/types/index.d.ts +72 -32
  3. package/dist/types/src/Animator.d.ts +50 -0
  4. package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
  5. package/dist/types/src/Camera.d.ts +122 -0
  6. package/dist/types/src/CameraController.d.ts +107 -0
  7. package/dist/types/src/CanvasText.d.ts +91 -0
  8. package/dist/types/src/CollisionLayers.d.ts +58 -0
  9. package/dist/types/{Color.d.ts → src/Color.d.ts} +5 -6
  10. package/dist/types/src/Coroutine.d.ts +111 -0
  11. package/dist/types/src/DebugOverlay.d.ts +86 -0
  12. package/dist/types/src/Drawable.d.ts +271 -0
  13. package/dist/types/src/Easing.d.ts +22 -0
  14. package/dist/types/src/Emerald.d.ts +420 -0
  15. package/dist/types/src/EmeraldDB.d.ts +159 -0
  16. package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
  17. package/dist/types/src/GLUtils.d.ts +4 -0
  18. package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
  19. package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +60 -23
  20. package/dist/types/src/Interpolator.d.ts +66 -0
  21. package/dist/types/src/Material.d.ts +87 -0
  22. package/dist/types/src/MathUtils.d.ts +80 -0
  23. package/dist/types/src/ParticleEmitter.d.ts +131 -0
  24. package/dist/types/{Physics.d.ts → src/Physics.d.ts} +88 -20
  25. package/dist/types/src/Pool.d.ts +53 -0
  26. package/dist/types/src/PostEffects.d.ts +68 -0
  27. package/dist/types/src/PostProcessor.d.ts +124 -0
  28. package/dist/types/src/RenderTarget.d.ts +56 -0
  29. package/dist/types/src/Scene.d.ts +62 -0
  30. package/dist/types/src/ScreenEffects.d.ts +111 -0
  31. package/dist/types/src/Serializer.d.ts +86 -0
  32. package/dist/types/src/Shaders.d.ts +2 -0
  33. package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
  34. package/dist/types/src/SpatialGrid.d.ts +50 -0
  35. package/dist/types/src/SpriteBatch.d.ts +89 -0
  36. package/dist/types/src/StateMachine.d.ts +59 -0
  37. package/dist/types/src/Storage.d.ts +89 -0
  38. package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
  39. package/dist/types/src/TextureAtlas.d.ts +55 -0
  40. package/dist/types/src/Tilemap.d.ts +91 -0
  41. package/dist/types/src/Time.d.ts +53 -0
  42. package/dist/types/src/Timer.d.ts +54 -0
  43. package/dist/types/src/Transform.d.ts +94 -0
  44. package/dist/types/src/Tween.d.ts +81 -0
  45. package/dist/types/src/UI.d.ts +121 -0
  46. package/dist/types/src/components/Behaviour.d.ts +71 -0
  47. package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
  48. package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
  49. package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
  50. package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
  51. package/dist/types/src/components/Collider.d.ts +96 -0
  52. package/dist/types/src/components/GameObject.d.ts +135 -0
  53. package/dist/types/src/components/RigidBody.d.ts +183 -0
  54. package/dist/types/src/importers/Aseprite.d.ts +79 -0
  55. package/dist/types/src/importers/TiledMap.d.ts +62 -0
  56. package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
  57. package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
  58. package/dist/types/src/managers/AssetManager.d.ts +116 -0
  59. package/dist/types/src/managers/AudioManager.d.ts +259 -0
  60. package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
  61. package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
  62. package/dist/types/src/managers/GLManager.d.ts +84 -0
  63. package/dist/types/src/managers/GLState.d.ts +37 -0
  64. package/dist/types/src/managers/IDManager.d.ts +31 -0
  65. package/dist/types/src/managers/InputManager.d.ts +290 -0
  66. package/dist/types/src/managers/NetworkManager.d.ts +93 -0
  67. package/dist/types/src/managers/RenderStats.d.ts +34 -0
  68. package/dist/types/src/managers/SceneManager.d.ts +44 -0
  69. package/dist/types/src/managers/ShaderManager.d.ts +55 -0
  70. package/dist/types/src/managers/TextureManager.d.ts +102 -0
  71. package/dist/types/src/particlesystem/Particle.d.ts +64 -0
  72. package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
  73. package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
  74. package/index.js +72 -0
  75. package/package.json +74 -60
  76. package/src/Animator.js +95 -0
  77. package/src/BitmapText.js +6 -5
  78. package/src/Camera.js +183 -0
  79. package/src/CameraController.js +192 -0
  80. package/src/CanvasText.js +281 -0
  81. package/src/CollisionLayers.js +86 -0
  82. package/src/Color.js +18 -18
  83. package/src/Coroutine.js +259 -0
  84. package/src/DebugOverlay.js +246 -0
  85. package/src/Drawable.js +842 -582
  86. package/src/Easing.js +57 -0
  87. package/src/Emerald.js +1150 -459
  88. package/src/EmeraldDB.js +328 -0
  89. package/src/FPSCounter.js +43 -43
  90. package/src/GLUtils.js +60 -67
  91. package/src/Instance.js +41 -5
  92. package/src/InstancedTexture.js +251 -118
  93. package/src/Interpolator.js +124 -0
  94. package/src/Material.js +202 -0
  95. package/src/MathUtils.js +133 -0
  96. package/src/ParticleEmitter.js +284 -0
  97. package/src/Physics.js +186 -5
  98. package/src/Pool.js +85 -0
  99. package/src/PostEffects.js +296 -0
  100. package/src/PostProcessor.js +304 -0
  101. package/src/RenderTarget.js +134 -0
  102. package/src/Scene.js +115 -83
  103. package/src/ScreenEffects.js +266 -0
  104. package/src/Serializer.js +131 -0
  105. package/src/Shaders.js +150 -165
  106. package/src/Shapes.js +118 -129
  107. package/src/SpatialGrid.js +111 -0
  108. package/src/SpriteBatch.js +299 -0
  109. package/src/StateMachine.js +82 -0
  110. package/src/Storage.js +175 -47
  111. package/src/Texture.js +58 -67
  112. package/src/TextureAtlas.js +96 -0
  113. package/src/Tilemap.js +274 -0
  114. package/src/Time.js +51 -6
  115. package/src/Timer.js +99 -0
  116. package/src/Transform.js +100 -7
  117. package/src/Tween.js +160 -0
  118. package/src/UI.js +394 -0
  119. package/src/components/Behaviour.js +90 -0
  120. package/src/components/BoxCollider.js +22 -3
  121. package/src/components/CircleCollider.js +19 -3
  122. package/src/components/CircleColliderDebug.js +24 -24
  123. package/src/components/Collider.js +140 -34
  124. package/src/components/GameObject.js +130 -21
  125. package/src/components/RigidBody.js +123 -2
  126. package/src/importers/Aseprite.js +142 -0
  127. package/src/importers/TiledMap.js +158 -0
  128. package/src/lights/DirectionalLight.js +6 -15
  129. package/src/lights/PointLight.js +4 -4
  130. package/src/managers/AssetManager.js +239 -0
  131. package/src/managers/AudioManager.js +565 -146
  132. package/src/managers/EventManager.js +488 -477
  133. package/src/managers/GLManager.js +57 -0
  134. package/src/managers/GLState.js +70 -0
  135. package/src/managers/IDManager.js +24 -2
  136. package/src/managers/InputManager.js +779 -0
  137. package/src/managers/NetworkManager.js +178 -0
  138. package/src/managers/RenderStats.js +34 -0
  139. package/src/managers/SceneManager.js +30 -0
  140. package/src/managers/ShaderManager.js +0 -2
  141. package/src/managers/TextureManager.js +218 -0
  142. package/src/particlesystem/Particle.js +82 -7
  143. package/src/particlesystem/ParticleSettings.js +21 -3
  144. package/src/particlesystem/Particles.js +80 -31
  145. package/dist/types/Drawable.d.ts +0 -157
  146. package/dist/types/Emerald.d.ts +0 -73
  147. package/dist/types/GLUtils.d.ts +0 -6
  148. package/dist/types/Scene.d.ts +0 -39
  149. package/dist/types/Shaders.d.ts +0 -4
  150. package/dist/types/Storage.d.ts +0 -46
  151. package/dist/types/Time.d.ts +0 -22
  152. package/dist/types/Transform.d.ts +0 -41
  153. package/dist/types/components/BoxColliderDebug.d.ts +0 -19
  154. package/dist/types/components/CircleColliderDebug.d.ts +0 -19
  155. package/dist/types/components/Collider.d.ts +0 -53
  156. package/dist/types/components/GameObject.d.ts +0 -72
  157. package/dist/types/components/RigidBody.d.ts +0 -104
  158. package/dist/types/managers/AudioManager.d.ts +0 -60
  159. package/dist/types/managers/GLManager.d.ts +0 -47
  160. package/dist/types/managers/IDManager.d.ts +0 -21
  161. package/dist/types/managers/SceneManager.d.ts +0 -22
  162. package/dist/types/particlesystem/Particle.d.ts +0 -42
@@ -0,0 +1,779 @@
1
+ /**
2
+ * Canonical W3C "standard gamepad" button layout, with vendor-neutral
3
+ * (south/east/west/north) and vendor (Xbox a/b/x/y, PlayStation
4
+ * cross/circle/square/triangle) aliases all pointing at the same index. This is
5
+ * the table used when a pad reports `mapping === "standard"` (Xbox pads, most
6
+ * controllers, and anything routed through Steam Input / XInput).
7
+ */
8
+ const STANDARD_BUTTONS = {
9
+ south: 0,
10
+ a: 0,
11
+ cross: 0,
12
+ east: 1,
13
+ b: 1,
14
+ circle: 1,
15
+ west: 2,
16
+ x: 2,
17
+ square: 2,
18
+ north: 3,
19
+ y: 3,
20
+ triangle: 3,
21
+ l1: 4,
22
+ lb: 4,
23
+ leftShoulder: 4,
24
+ r1: 5,
25
+ rb: 5,
26
+ rightShoulder: 5,
27
+ l2: 6,
28
+ lt: 6,
29
+ leftTrigger: 6,
30
+ r2: 7,
31
+ rt: 7,
32
+ rightTrigger: 7,
33
+ select: 8,
34
+ back: 8,
35
+ view: 8,
36
+ share: 8,
37
+ start: 9,
38
+ menu: 9,
39
+ options: 9,
40
+ l3: 10,
41
+ leftStick: 10,
42
+ r3: 11,
43
+ rightStick: 11,
44
+ up: 12,
45
+ dpadUp: 12,
46
+ down: 13,
47
+ dpadDown: 13,
48
+ left: 14,
49
+ dpadLeft: 14,
50
+ right: 15,
51
+ dpadRight: 15,
52
+ home: 16,
53
+ guide: 16,
54
+ };
55
+
56
+ const STANDARD_AXES = { leftX: 0, leftY: 1, rightX: 2, rightY: 3 };
57
+
58
+ const CUSTOM_MAPPINGS = [];
59
+
60
+ const BUILTIN_MAPPINGS = [
61
+ {
62
+ match: "logitech dual action",
63
+ buttons: {
64
+ west: 0,
65
+ x: 0,
66
+ square: 0,
67
+ south: 1,
68
+ a: 1,
69
+ cross: 1,
70
+ east: 2,
71
+ b: 2,
72
+ circle: 2,
73
+ north: 3,
74
+ y: 3,
75
+ triangle: 3,
76
+ l1: 4,
77
+ lb: 4,
78
+ r1: 5,
79
+ rb: 5,
80
+ l2: 6,
81
+ lt: 6,
82
+ r2: 7,
83
+ rt: 7,
84
+ select: 8,
85
+ back: 8,
86
+ start: 9,
87
+ l3: 10,
88
+ r3: 11,
89
+ },
90
+ },
91
+ {
92
+ match: "twin usb",
93
+ buttons: {
94
+ north: 0,
95
+ y: 0,
96
+ triangle: 0,
97
+ east: 1,
98
+ b: 1,
99
+ circle: 1,
100
+ south: 2,
101
+ a: 2,
102
+ cross: 2,
103
+ west: 3,
104
+ x: 3,
105
+ square: 3,
106
+ l2: 4,
107
+ lt: 4,
108
+ r2: 5,
109
+ rt: 5,
110
+ l1: 6,
111
+ lb: 6,
112
+ r1: 7,
113
+ rb: 7,
114
+ select: 8,
115
+ back: 8,
116
+ start: 9,
117
+ l3: 10,
118
+ r3: 11,
119
+ },
120
+ },
121
+ {
122
+ match: "8bitdo",
123
+ buttons: {
124
+ east: 0,
125
+ b: 0,
126
+ circle: 0,
127
+ south: 1,
128
+ a: 1,
129
+ cross: 1,
130
+ north: 3,
131
+ y: 3,
132
+ triangle: 3,
133
+ west: 4,
134
+ x: 4,
135
+ square: 4,
136
+ l1: 6,
137
+ lb: 6,
138
+ r1: 7,
139
+ rb: 7,
140
+ l2: 8,
141
+ lt: 8,
142
+ r2: 9,
143
+ rt: 9,
144
+ select: 10,
145
+ back: 10,
146
+ start: 11,
147
+ },
148
+ },
149
+ ];
150
+
151
+ const STICK_DIRECTIONS = {
152
+ leftStickLeft: ["leftX", -1],
153
+ leftStickRight: ["leftX", 1],
154
+ leftStickUp: ["leftY", -1],
155
+ leftStickDown: ["leftY", 1],
156
+ rightStickLeft: ["rightX", -1],
157
+ rightStickRight: ["rightX", 1],
158
+ rightStickUp: ["rightY", -1],
159
+ rightStickDown: ["rightY", 1],
160
+ };
161
+
162
+ for (const b of BUILTIN_MAPPINGS) {
163
+ CUSTOM_MAPPINGS.push({
164
+ test: (id) => (id || "").toLowerCase().includes(b.match),
165
+ buttons: { ...STANDARD_BUTTONS, ...b.buttons },
166
+ axes: { ...STANDARD_AXES, ...(b.axes || {}) },
167
+ });
168
+ }
169
+
170
+ /**
171
+ * @class InputManager
172
+ * @description Unified, pollable input: keyboard, mouse, touch, and full
173
+ * gamepad support (analog sticks/triggers, semantic button names, rumble,
174
+ * connect events, and per-controller mapping), with rebindable named actions.
175
+ * Call `update()` once per frame so `justPressed`/`justReleased` edge queries
176
+ * work for every device — including the gamepad.
177
+ *
178
+ * Gamepad tokens (usable anywhere a key token is, including in mapAction and
179
+ * justPressed) — `<i>` is the pad index:
180
+ * "pad:<i>:south" / "pad:<i>:a" - face buttons (also east/b, west/x, north/y)
181
+ * "pad:<i>:l1" / "pad:<i>:r2" ... - shoulders / triggers
182
+ * "pad:<i>:start" / "pad:<i>:select" - center buttons
183
+ * "pad:<i>:dpadLeft" ... - d-pad (works via buttons or a hat axis)
184
+ * "pad:<i>:<n>" - raw button index (mapping-independent)
185
+ * "pad:<i>:axis<n>+" / "axis<n>-" - analog axis past the deadzone
186
+ * "pad:<i>:leftStickUp" / "Down" / "Left" / "Right" - stick as a d-pad
187
+ * "pad:<i>:rightStickUp" ... - (threshold: stickPressThreshold)
188
+ * "gamepad:<n>" - legacy: raw button <n> on pad 0
189
+ * Names resolve through the active mapping, so "pad:0:south" is the bottom face
190
+ * button regardless of whether the pad reports Xbox or PlayStation ordering.
191
+ *
192
+ * @example
193
+ * const input = new InputManager();
194
+ * input.mapAction("jump", ["Space", " ", "pad:0:south"]);
195
+ * input.mapAction("left", ["a", "ArrowLeft", "pad:0:dpadLeft"]);
196
+ * input.onGamepadConnected((info) => console.log("pad:", info.id, info.mapping));
197
+ * // in the loop:
198
+ * if (input.justPressed("jump")) { player.jump(); input.rumble(0, { duration: 120 }); }
199
+ * const { x } = input.getGamepadStick("left"); // analog, deadzoned
200
+ * input.update();
201
+ */
202
+ class InputManager {
203
+ /**
204
+ * @method registerGamepadMapping
205
+ * @description Registers a custom button/axis mapping for controllers whose
206
+ * `mapping` is not "standard" (so their raw button indices differ). Match by
207
+ * a substring of `gamepad.id` (case-insensitive), a RegExp, or a predicate.
208
+ * @param {string|RegExp|Function} match - id matcher
209
+ * @param {Object} mapping - { buttons:{name:index}, axes:{name:index} };
210
+ * merged over the standard table, so you only specify what differs.
211
+ */
212
+ static registerGamepadMapping(match, mapping = {}) {
213
+ const test =
214
+ typeof match === "function"
215
+ ? match
216
+ : match instanceof RegExp
217
+ ? (id) => match.test(id)
218
+ : (id) =>
219
+ (id || "").toLowerCase().includes(String(match).toLowerCase());
220
+ CUSTOM_MAPPINGS.unshift({
221
+ test,
222
+ buttons: { ...STANDARD_BUTTONS, ...(mapping.buttons || {}) },
223
+ axes: { ...STANDARD_AXES, ...(mapping.axes || {}) },
224
+ });
225
+ }
226
+
227
+ constructor(options = {}) {
228
+ this.target =
229
+ options.target || (typeof window !== "undefined" ? window : null);
230
+ this.actions = new Map();
231
+ this.down = new Set();
232
+ this.prevDown = new Set();
233
+ this.mouse = { x: 0, y: 0, buttons: new Set() };
234
+ this.touches = [];
235
+ this.gamepads = [];
236
+
237
+ this.gamepadDeadzone = options.gamepadDeadzone ?? 0.3;
238
+ this.gamepadCurve = options.gamepadCurve ?? 1;
239
+ this.stickPressThreshold = options.stickPressThreshold ?? 0.5;
240
+ /** @private */
241
+ this._gamepadTokens = new Set();
242
+ /** @private */
243
+ this._connectHandlers = [];
244
+ /** @private */
245
+ this._disconnectHandlers = [];
246
+ /** @private */
247
+ this._connected = new Map();
248
+
249
+ /** @private */
250
+ this._onKeyDown = (e) => this.down.add(this._normKey(e.key));
251
+ /** @private */
252
+ this._onKeyUp = (e) => this.down.delete(this._normKey(e.key));
253
+ /** @private */
254
+ this._onMouseDown = (e) => {
255
+ this.down.add(`mouse:${e.button}`);
256
+ this.mouse.buttons.add(e.button);
257
+ };
258
+ /** @private */
259
+ this._onMouseUp = (e) => {
260
+ this.down.delete(`mouse:${e.button}`);
261
+ this.mouse.buttons.delete(e.button);
262
+ };
263
+ /** @private */
264
+ this._onMouseMove = (e) => {
265
+ this.mouse.x = e.clientX;
266
+ this.mouse.y = e.clientY;
267
+ };
268
+ /** @private */
269
+ this._onTouch = (e) => {
270
+ this.touches = Array.from(e.touches || []).map((t) => ({
271
+ x: t.clientX,
272
+ y: t.clientY,
273
+ id: t.identifier,
274
+ }));
275
+ };
276
+ /** @private */
277
+ this._onGamepadConnected = (e) => {
278
+ const gp = e.gamepad;
279
+ if (!gp) return;
280
+ const info = this._infoFromPad(gp);
281
+ this._connected.set(gp.index, info);
282
+ for (const h of this._connectHandlers) h(info);
283
+ };
284
+ /** @private */
285
+ this._onGamepadDisconnected = (e) => {
286
+ const gp = e.gamepad;
287
+ if (!gp) return;
288
+ const info = this._connected.get(gp.index) || this._infoFromPad(gp);
289
+ this._connected.delete(gp.index);
290
+ for (const h of this._disconnectHandlers) h(info);
291
+ };
292
+
293
+ if (this.target) {
294
+ this.target.addEventListener("keydown", this._onKeyDown);
295
+ this.target.addEventListener("keyup", this._onKeyUp);
296
+ this.target.addEventListener("mousedown", this._onMouseDown);
297
+ this.target.addEventListener("mouseup", this._onMouseUp);
298
+ this.target.addEventListener("mousemove", this._onMouseMove);
299
+ this.target.addEventListener("touchstart", this._onTouch);
300
+ this.target.addEventListener("touchmove", this._onTouch);
301
+ this.target.addEventListener("touchend", this._onTouch);
302
+ this.target.addEventListener(
303
+ "gamepadconnected",
304
+ this._onGamepadConnected
305
+ );
306
+ this.target.addEventListener(
307
+ "gamepaddisconnected",
308
+ this._onGamepadDisconnected
309
+ );
310
+ }
311
+ }
312
+
313
+ /** @private */
314
+ _normKey(key) {
315
+ return key && key.length <= 1 ? key.toLowerCase() : key;
316
+ }
317
+
318
+ /**
319
+ * @method mapAction
320
+ * @description Binds an action name to one or more input tokens (keys,
321
+ * "mouse:0", or any gamepad token above).
322
+ * @param {string} name - Action name
323
+ * @param {string[]} tokens - Input tokens
324
+ * @returns {InputManager} - this
325
+ */
326
+ mapAction(name, tokens) {
327
+ this.actions.set(
328
+ name,
329
+ tokens.map((t) => this._normKey(t))
330
+ );
331
+ return this;
332
+ }
333
+
334
+ /** @private */
335
+ _tokensFor(actionOrToken) {
336
+ return this.actions.get(actionOrToken) || [this._normKey(actionOrToken)];
337
+ }
338
+
339
+ /** @private */
340
+ _tokenDown(token) {
341
+ if (this.down.has(token)) return true;
342
+ if (this._gamepadTokens.has(token)) return true;
343
+ if (token.startsWith("gamepad:")) {
344
+ const idx = parseInt(token.slice(8), 10);
345
+ const gp = this.gamepads[0];
346
+ return !!(gp && gp.buttons[idx] && gp.buttons[idx].pressed);
347
+ }
348
+ return false;
349
+ }
350
+
351
+ /**
352
+ * @method isDown
353
+ * @description Whether the action (or raw token) is currently held.
354
+ */
355
+ isDown(actionOrToken) {
356
+ return this._tokensFor(actionOrToken).some((t) => this._tokenDown(t));
357
+ }
358
+
359
+ /**
360
+ * @method justPressed
361
+ * @description Whether the action became pressed this frame.
362
+ */
363
+ justPressed(actionOrToken) {
364
+ return this._tokensFor(actionOrToken).some(
365
+ (t) => this.down.has(t) && !this.prevDown.has(t)
366
+ );
367
+ }
368
+
369
+ /**
370
+ * @method justReleased
371
+ * @description Whether the action was released this frame.
372
+ */
373
+ justReleased(actionOrToken) {
374
+ return this._tokensFor(actionOrToken).some(
375
+ (t) => !this.down.has(t) && this.prevDown.has(t)
376
+ );
377
+ }
378
+
379
+ /**
380
+ * @method getAxis
381
+ * @description Returns -1/0/+1 based on two opposing actions.
382
+ */
383
+ getAxis(negative, positive) {
384
+ return (this.isDown(positive) ? 1 : 0) - (this.isDown(negative) ? 1 : 0);
385
+ }
386
+
387
+ /**
388
+ * @method getGamepad
389
+ * @description Returns the polled gamepad at an index (or null).
390
+ */
391
+ getGamepad(index = 0) {
392
+ return this.gamepads[index] || null;
393
+ }
394
+
395
+ /**
396
+ * @method setGamepadDeadzone
397
+ * @description Sets the analog deadzone (0..1) for axes and trigger buttons.
398
+ * @returns {InputManager} - this
399
+ */
400
+ setGamepadDeadzone(dz) {
401
+ this.gamepadDeadzone = dz;
402
+ return this;
403
+ }
404
+
405
+ /**
406
+ * @method setGamepadCurve
407
+ * @description Sets the response-curve exponent applied to deadzoned analog
408
+ * values: 1 = linear (default), >1 = softer near center for fine aiming.
409
+ * @returns {InputManager} - this
410
+ */
411
+ setGamepadCurve(exp) {
412
+ this.gamepadCurve = exp;
413
+ return this;
414
+ }
415
+
416
+ /**
417
+ * @method _rescaleAxis
418
+ * @description Deadzone + rescale + curve for a single axis value. Unlike a
419
+ * hard cutoff, the output ramps smoothly from 0 at the deadzone edge to ±1
420
+ * at full deflection (no jump, and the full range stays reachable).
421
+ * @private
422
+ */
423
+ _rescaleAxis(v, dz = this.gamepadDeadzone, curve = this.gamepadCurve) {
424
+ const a = Math.abs(v);
425
+ if (a < dz) return 0;
426
+ let t = Math.min(1, (a - dz) / (1 - dz || 1));
427
+ if (curve !== 1) t = Math.pow(t, curve);
428
+ return v < 0 ? -t : t;
429
+ }
430
+
431
+ /**
432
+ * @method _buttonMapFor
433
+ * @description Resolves the button-name → index table for a gamepad: a
434
+ * registered custom mapping (matched by id) wins; otherwise the standard
435
+ * table is used (also as the best-effort fallback for unknown non-standard
436
+ * pads).
437
+ * @private
438
+ */
439
+ _buttonMapFor(gp) {
440
+ if (gp.mapping !== "standard") {
441
+ const id = gp.id || "";
442
+ for (const m of CUSTOM_MAPPINGS) {
443
+ if (m.test(id)) return m.buttons;
444
+ }
445
+ }
446
+ return STANDARD_BUTTONS;
447
+ }
448
+
449
+ /**
450
+ * @method getGamepadAxis
451
+ * @description Returns a deadzoned analog axis value (-1..1). The value is
452
+ * RESCALED past the deadzone (0 at the edge, ±1 at full deflection) and runs
453
+ * through the response curve, so there is no jump at the threshold. Standard
454
+ * mapping: axis 0/1 = left stick X/Y, 2/3 = right stick X/Y.
455
+ * @param {number} axisIndex
456
+ * @param {number} [padIndex=0]
457
+ * @returns {number}
458
+ */
459
+ getGamepadAxis(axisIndex, padIndex = 0) {
460
+ const gp = this.gamepads[padIndex];
461
+ if (!gp || !gp.axes) return 0;
462
+ return this._rescaleAxis(gp.axes[axisIndex] || 0);
463
+ }
464
+
465
+ /**
466
+ * @method getGamepadStick
467
+ * @description Returns the left or right stick with a RADIAL deadzone: the
468
+ * deadzone applies to the stick's distance from center (not per axis), so
469
+ * diagonals aren't clipped square and direction is preserved exactly. The
470
+ * magnitude is rescaled to use the full 0..1 range and shaped by the
471
+ * response curve.
472
+ * @param {"left"|"right"} [side="left"]
473
+ * @param {number} [padIndex=0]
474
+ * @param {Object} [opts] - { deadzone, curve, invertY } overrides. invertY
475
+ * flips the browser's y-down convention so up = +1.
476
+ * @returns {{x:number, y:number, magnitude:number, angle:number}}
477
+ */
478
+ getGamepadStick(side = "left", padIndex = 0, opts = {}) {
479
+ const gp = this.gamepads[padIndex];
480
+ if (!gp || !gp.axes) return { x: 0, y: 0, magnitude: 0, angle: 0 };
481
+ const map = this._axisMapFor(gp);
482
+ const ax = side === "right" ? map.rightX : map.leftX;
483
+ const ay = side === "right" ? map.rightY : map.leftY;
484
+ const rx = gp.axes[ax] || 0;
485
+ const ry = gp.axes[ay] || 0;
486
+ const dz = opts.deadzone ?? this.gamepadDeadzone;
487
+ const curve = opts.curve ?? this.gamepadCurve;
488
+
489
+ const mag = Math.hypot(rx, ry);
490
+ if (mag < dz) return { x: 0, y: 0, magnitude: 0, angle: 0 };
491
+ let t = Math.min(1, (mag - dz) / (1 - dz || 1));
492
+ if (curve !== 1) t = Math.pow(t, curve);
493
+ const s = t / mag;
494
+ const x = rx * s;
495
+ const y = (opts.invertY ? -ry : ry) * s;
496
+ return { x, y, magnitude: t, angle: Math.atan2(y, x) };
497
+ }
498
+
499
+ /**
500
+ * @method _axisMapFor
501
+ * @description Axis-name table for a pad (custom/built-in mapping wins for
502
+ * non-standard pads, else the standard table).
503
+ * @private
504
+ */
505
+ _axisMapFor(gp) {
506
+ if (gp.mapping !== "standard") {
507
+ const id = gp.id || "";
508
+ for (const m of CUSTOM_MAPPINGS) {
509
+ if (m.test(id)) return m.axes;
510
+ }
511
+ }
512
+ return STANDARD_AXES;
513
+ }
514
+
515
+ /**
516
+ * @method getGamepadButton
517
+ * @description Looks up a button by semantic name (e.g. "south", "a", "r1",
518
+ * "start", "dpadLeft") or raw index through the pad's active mapping.
519
+ * @param {string|number} name
520
+ * @param {number} [padIndex=0]
521
+ * @returns {{pressed:boolean, value:number, index:number}}
522
+ */
523
+ getGamepadButton(name, padIndex = 0) {
524
+ const gp = this.gamepads[padIndex];
525
+ if (!gp || !gp.buttons) return { pressed: false, value: 0, index: -1 };
526
+ const map = this._buttonMapFor(gp);
527
+ const idx = typeof name === "number" ? name : map[name];
528
+ if (idx == null) return { pressed: false, value: 0, index: -1 };
529
+ const btn = gp.buttons[idx];
530
+ const value = btn ? btn.value || 0 : 0;
531
+ return {
532
+ pressed: !!(btn && (btn.pressed || value > this.gamepadDeadzone)),
533
+ value,
534
+ index: idx,
535
+ };
536
+ }
537
+
538
+ /**
539
+ * @method getGamepadTrigger
540
+ * @description Returns the analog value (0..1) of a trigger.
541
+ * @param {"left"|"right"} [side="left"]
542
+ * @param {number} [padIndex=0]
543
+ * @returns {number}
544
+ */
545
+ getGamepadTrigger(side = "left", padIndex = 0) {
546
+ return this.getGamepadButton(side === "right" ? "r2" : "l2", padIndex)
547
+ .value;
548
+ }
549
+
550
+ /**
551
+ * @method rumble
552
+ * @description Plays a vibration effect on a pad (where supported).
553
+ * @param {number} [padIndex=0]
554
+ * @param {Object} [opts] - { duration=200, strong=1, weak=1 }
555
+ * @returns {Promise|undefined}
556
+ */
557
+ rumble(padIndex = 0, opts = {}) {
558
+ const gp = this.gamepads[padIndex];
559
+ const act =
560
+ gp &&
561
+ (gp.vibrationActuator || (gp.hapticActuators && gp.hapticActuators[0]));
562
+ if (!act) return;
563
+ const { duration = 200, strong = 1, weak = 1 } = opts;
564
+ if (typeof act.playEffect === "function") {
565
+ return act.playEffect("dual-rumble", {
566
+ duration,
567
+ strongMagnitude: strong,
568
+ weakMagnitude: weak,
569
+ startDelay: 0,
570
+ });
571
+ }
572
+ if (typeof act.pulse === "function")
573
+ return act.pulse(Math.max(strong, weak), duration);
574
+ }
575
+
576
+ /**
577
+ * @method onGamepadConnected
578
+ * @description Registers a callback fired when a pad connects (also called for
579
+ * pads already connected at registration time). Receives an info object.
580
+ * @param {Function} cb
581
+ * @returns {InputManager} - this
582
+ */
583
+ onGamepadConnected(cb) {
584
+ this._connectHandlers.push(cb);
585
+ for (const info of this.getConnectedGamepads()) cb(info);
586
+ return this;
587
+ }
588
+
589
+ /**
590
+ * @method onGamepadDisconnected
591
+ * @description Registers a callback fired when a pad disconnects.
592
+ * @returns {InputManager} - this
593
+ */
594
+ onGamepadDisconnected(cb) {
595
+ this._disconnectHandlers.push(cb);
596
+ return this;
597
+ }
598
+
599
+ /** @private */
600
+ _infoFromPad(gp) {
601
+ return {
602
+ index: gp.index,
603
+ id: gp.id || "",
604
+ mapping: gp.mapping || "",
605
+ buttonCount: (gp.buttons || []).length,
606
+ axesCount: (gp.axes || []).length,
607
+ standard: gp.mapping === "standard",
608
+ };
609
+ }
610
+
611
+ /**
612
+ * @method getGamepadInfo
613
+ * @description Diagnostic info for a connected pad (id, mapping, counts).
614
+ * @param {number} [padIndex=0]
615
+ * @returns {Object|null}
616
+ */
617
+ getGamepadInfo(padIndex = 0) {
618
+ const gp = this.gamepads[padIndex];
619
+ return gp ? this._infoFromPad(gp) : null;
620
+ }
621
+
622
+ /**
623
+ * @method getConnectedGamepads
624
+ * @description Info for every currently polled pad.
625
+ * @returns {Object[]}
626
+ */
627
+ getConnectedGamepads() {
628
+ return this.gamepads.filter(Boolean).map((gp) => this._infoFromPad(gp));
629
+ }
630
+
631
+ /**
632
+ * @method isGamepadConnected
633
+ * @returns {boolean}
634
+ */
635
+ isGamepadConnected(padIndex = 0) {
636
+ return !!this.gamepads[padIndex];
637
+ }
638
+
639
+ /**
640
+ * @method getPressedButtons
641
+ * @description Diagnostic: raw indices of all currently pressed buttons on a
642
+ * pad. Handy for discovering an unknown controller's layout.
643
+ * @param {number} [padIndex=0]
644
+ * @returns {number[]}
645
+ */
646
+ getPressedButtons(padIndex = 0) {
647
+ const gp = this.gamepads[padIndex];
648
+ if (!gp || !gp.buttons) return [];
649
+ const out = [];
650
+ for (let b = 0; b < gp.buttons.length; b++) {
651
+ const btn = gp.buttons[b];
652
+ if (btn && (btn.pressed || (btn.value || 0) > this.gamepadDeadzone))
653
+ out.push(b);
654
+ }
655
+ return out;
656
+ }
657
+
658
+ /**
659
+ * @method _decodeHat
660
+ * @description Some non-standard pads report the d-pad as an 8-way "hat" on a
661
+ * single axis instead of buttons 12-15. When a pad is non-standard and exposes
662
+ * an extra (odd) trailing axis, decode it into d-pad direction tokens.
663
+ * @private
664
+ */
665
+ _decodeHat(gp, i, set) {
666
+ if (gp.mapping === "standard") return;
667
+ const axes = gp.axes || [];
668
+ if (axes.length < 5 || axes.length % 2 === 0) return;
669
+ const v = axes[axes.length - 1];
670
+ if (v == null || v > 1.2 || v < -1.2) return;
671
+ const dirs = [
672
+ ["dpadUp"],
673
+ ["dpadUp", "dpadRight"],
674
+ ["dpadRight"],
675
+ ["dpadDown", "dpadRight"],
676
+ ["dpadDown"],
677
+ ["dpadDown", "dpadLeft"],
678
+ ["dpadLeft"],
679
+ ["dpadUp", "dpadLeft"],
680
+ ];
681
+ const bucket = Math.round((v + 1) * 3.5);
682
+ const names = dirs[Math.max(0, Math.min(7, bucket))];
683
+ for (const n of names) set.add(`pad:${i}:${n}`);
684
+ }
685
+
686
+ /**
687
+ * @method _collectGamepadTokens
688
+ * @description Rebuilds the active gamepad token set (raw indices, semantic
689
+ * names via the pad's mapping, d-pad, and axis directions) for every connected
690
+ * pad, so gamepad input flows through the same down/justPressed machinery as
691
+ * the keyboard.
692
+ * @private
693
+ */
694
+ _collectGamepadTokens() {
695
+ const set = this._gamepadTokens;
696
+ set.clear();
697
+ const dz = this.gamepadDeadzone;
698
+
699
+ for (let i = 0; i < this.gamepads.length; i++) {
700
+ const gp = this.gamepads[i];
701
+ if (!gp) continue;
702
+ const map = this._buttonMapFor(gp);
703
+ const buttons = gp.buttons || [];
704
+
705
+ for (let b = 0; b < buttons.length; b++) {
706
+ const btn = buttons[b];
707
+ if (btn && (btn.pressed || (btn.value || 0) > dz)) {
708
+ set.add(`pad:${i}:${b}`);
709
+ if (i === 0) set.add(`gamepad:${b}`);
710
+ }
711
+ }
712
+ for (const name in map) {
713
+ const btn = buttons[map[name]];
714
+ if (btn && (btn.pressed || (btn.value || 0) > dz))
715
+ set.add(`pad:${i}:${name}`);
716
+ }
717
+ this._decodeHat(gp, i, set);
718
+
719
+ const axes = gp.axes || [];
720
+ for (let a = 0; a < axes.length; a++) {
721
+ const v = axes[a] || 0;
722
+ if (v <= -dz) set.add(`pad:${i}:axis${a}-`);
723
+ else if (v >= dz) set.add(`pad:${i}:axis${a}+`);
724
+ }
725
+
726
+ const axisMap = this._axisMapFor(gp);
727
+ const press = this.stickPressThreshold;
728
+ for (const name in STICK_DIRECTIONS) {
729
+ const [axisName, sign] = STICK_DIRECTIONS[name];
730
+ const idx = axisMap[axisName];
731
+ if (idx == null || axes[idx] == null) continue;
732
+ const v = this._rescaleAxis(axes[idx]);
733
+ if (sign * v >= press) set.add(`pad:${i}:${name}`);
734
+ }
735
+ }
736
+ return set;
737
+ }
738
+
739
+ /**
740
+ * @method update
741
+ * @description Polls gamepads and advances edge-detection state. Call once
742
+ * per frame after reading input.
743
+ */
744
+ update() {
745
+ if (typeof navigator !== "undefined" && navigator.getGamepads) {
746
+ this.gamepads = Array.from(navigator.getGamepads()).filter(Boolean);
747
+ }
748
+ this.prevDown = new Set(this.down);
749
+ for (const t of this._gamepadTokens) this.down.delete(t);
750
+ this._collectGamepadTokens();
751
+ for (const t of this._gamepadTokens) this.down.add(t);
752
+ }
753
+
754
+ /**
755
+ * @method destroy
756
+ * @description Removes all event listeners.
757
+ */
758
+ destroy() {
759
+ if (!this.target) return;
760
+ this.target.removeEventListener("keydown", this._onKeyDown);
761
+ this.target.removeEventListener("keyup", this._onKeyUp);
762
+ this.target.removeEventListener("mousedown", this._onMouseDown);
763
+ this.target.removeEventListener("mouseup", this._onMouseUp);
764
+ this.target.removeEventListener("mousemove", this._onMouseMove);
765
+ this.target.removeEventListener("touchstart", this._onTouch);
766
+ this.target.removeEventListener("touchmove", this._onTouch);
767
+ this.target.removeEventListener("touchend", this._onTouch);
768
+ this.target.removeEventListener(
769
+ "gamepadconnected",
770
+ this._onGamepadConnected
771
+ );
772
+ this.target.removeEventListener(
773
+ "gamepaddisconnected",
774
+ this._onGamepadDisconnected
775
+ );
776
+ }
777
+ }
778
+
779
+ export default InputManager;