tiny-essentials 1.20.2 → 1.21.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 (123) hide show
  1. package/README.md +13 -11
  2. package/dist/_/numbers.cjs +136 -0
  3. package/dist/_/numbers.d.mts +24 -0
  4. package/dist/_/numbers.mjs +102 -0
  5. package/dist/v1/TinyAdvancedRaffle.min.js +1 -0
  6. package/dist/v1/TinyArrayPaginator.min.js +1 -0
  7. package/dist/v1/TinyBasicsEs.min.js +1 -1
  8. package/dist/v1/TinyCookieConsent.min.js +1 -0
  9. package/dist/v1/TinyDayNightCycle.min.js +1 -0
  10. package/dist/v1/TinyEssentials.min.js +1 -1
  11. package/dist/v1/TinyGamepad.min.js +1 -0
  12. package/dist/v1/TinyI18.min.js +1 -0
  13. package/dist/v1/TinyInventory.min.js +1 -0
  14. package/dist/v1/TinyInventoryTrader.min.js +1 -0
  15. package/dist/v1/TinyLocalStorage.min.js +1 -1
  16. package/dist/v1/TinyRateLimiter.min.js +1 -1
  17. package/dist/v1/TinySmartScroller.min.js +1 -1
  18. package/dist/v1/TinyTextarea.min.js +1 -0
  19. package/dist/v1/TinyToastNotify.min.js +1 -1
  20. package/dist/v1/TinyUploadClicker.min.js +1 -1
  21. package/dist/v1/basics/array.cjs +21 -0
  22. package/dist/v1/basics/array.d.mts +8 -0
  23. package/dist/v1/basics/array.mjs +18 -0
  24. package/dist/v1/basics/index.cjs +2 -8
  25. package/dist/v1/basics/index.d.mts +3 -8
  26. package/dist/v1/basics/index.mjs +2 -3
  27. package/dist/v1/basics/simpleMath.cjs +46 -0
  28. package/dist/v1/basics/simpleMath.d.mts +27 -0
  29. package/dist/v1/basics/simpleMath.mjs +42 -0
  30. package/dist/v1/build/TinyAdvancedRaffle.cjs +7 -0
  31. package/dist/v1/build/TinyAdvancedRaffle.d.mts +3 -0
  32. package/dist/v1/build/TinyAdvancedRaffle.mjs +2 -0
  33. package/dist/v1/build/TinyArrayPaginator.cjs +7 -0
  34. package/dist/v1/build/TinyArrayPaginator.d.mts +3 -0
  35. package/dist/v1/build/TinyArrayPaginator.mjs +2 -0
  36. package/dist/v1/build/TinyCookieConsent.cjs +7 -0
  37. package/dist/v1/build/TinyCookieConsent.d.mts +3 -0
  38. package/dist/v1/build/TinyCookieConsent.mjs +2 -0
  39. package/dist/v1/build/TinyDayNightCycle.cjs +7 -0
  40. package/dist/v1/build/TinyDayNightCycle.d.mts +3 -0
  41. package/dist/v1/build/TinyDayNightCycle.mjs +2 -0
  42. package/dist/v1/build/TinyGamepad.cjs +7 -0
  43. package/dist/v1/build/TinyGamepad.d.mts +3 -0
  44. package/dist/v1/build/TinyGamepad.mjs +2 -0
  45. package/dist/v1/build/TinyI18.cjs +7 -0
  46. package/dist/v1/build/TinyI18.d.mts +3 -0
  47. package/dist/v1/build/TinyI18.mjs +2 -0
  48. package/dist/v1/build/TinyInventory.cjs +7 -0
  49. package/dist/v1/build/TinyInventory.d.mts +3 -0
  50. package/dist/v1/build/TinyInventory.mjs +2 -0
  51. package/dist/v1/build/TinyInventoryTrader.cjs +7 -0
  52. package/dist/v1/build/TinyInventoryTrader.d.mts +3 -0
  53. package/dist/v1/build/TinyInventoryTrader.mjs +2 -0
  54. package/dist/v1/build/TinyTextarea.cjs +7 -0
  55. package/dist/v1/build/TinyTextarea.d.mts +3 -0
  56. package/dist/v1/build/TinyTextarea.mjs +2 -0
  57. package/dist/v1/css/TinyCookieConsent.min.css +1 -0
  58. package/dist/v1/index.cjs +20 -8
  59. package/dist/v1/index.d.mts +12 -8
  60. package/dist/v1/index.mjs +11 -3
  61. package/dist/v1/libs/TinyAdvancedRaffle.cjs +1654 -0
  62. package/dist/v1/libs/TinyAdvancedRaffle.d.mts +851 -0
  63. package/dist/v1/libs/TinyAdvancedRaffle.mjs +1477 -0
  64. package/dist/v1/libs/TinyArrayPaginator.cjs +132 -0
  65. package/dist/v1/libs/TinyArrayPaginator.d.mts +78 -0
  66. package/dist/v1/libs/TinyArrayPaginator.mjs +119 -0
  67. package/dist/v1/libs/TinyCookieConsent.cjs +278 -0
  68. package/dist/v1/libs/TinyCookieConsent.d.mts +142 -0
  69. package/dist/v1/libs/TinyCookieConsent.mjs +248 -0
  70. package/dist/v1/libs/TinyDayNightCycle.cjs +1389 -0
  71. package/dist/v1/libs/TinyDayNightCycle.d.mts +673 -0
  72. package/dist/v1/libs/TinyDayNightCycle.mjs +1232 -0
  73. package/dist/v1/libs/TinyGamepad.cjs +2690 -0
  74. package/dist/v1/libs/TinyGamepad.d.mts +1286 -0
  75. package/dist/v1/libs/TinyGamepad.mjs +2386 -0
  76. package/dist/v1/libs/TinyHtml.d.mts +2 -2
  77. package/dist/v1/libs/TinyI18.cjs +946 -0
  78. package/dist/v1/libs/TinyI18.d.mts +418 -0
  79. package/dist/v1/libs/TinyI18.mjs +873 -0
  80. package/dist/v1/libs/TinyInventory.cjs +1788 -0
  81. package/dist/v1/libs/TinyInventory.d.mts +871 -0
  82. package/dist/v1/libs/TinyInventory.mjs +1650 -0
  83. package/dist/v1/libs/TinyInventoryTrader.cjs +242 -0
  84. package/dist/v1/libs/TinyInventoryTrader.d.mts +95 -0
  85. package/dist/v1/libs/TinyInventoryTrader.mjs +212 -0
  86. package/dist/v1/libs/TinyLocalStorage.cjs +14 -1
  87. package/dist/v1/libs/TinyLocalStorage.mjs +8 -1
  88. package/dist/v1/libs/TinyRateLimiter.cjs +0 -13
  89. package/dist/v1/libs/TinyRateLimiter.d.mts +0 -8
  90. package/dist/v1/libs/TinyRateLimiter.mjs +0 -12
  91. package/dist/v1/libs/TinySmartScroller.cjs +51 -15
  92. package/dist/v1/libs/TinySmartScroller.d.mts +28 -12
  93. package/dist/v1/libs/TinySmartScroller.mjs +44 -14
  94. package/dist/v1/libs/TinyTextarea.cjs +230 -0
  95. package/dist/v1/libs/TinyTextarea.d.mts +128 -0
  96. package/dist/v1/libs/TinyTextarea.mjs +198 -0
  97. package/dist/v1/libs/TinyToastNotify.cjs +2 -2
  98. package/dist/v1/libs/TinyToastNotify.mjs +2 -2
  99. package/dist/v1/libs/TinyUploadClicker.cjs +1 -1
  100. package/docs/v1/Ai-Tips.md +51 -0
  101. package/docs/v1/Personal-Ai-Prompts(portuguese).md +134 -0
  102. package/docs/v1/Personal-Ai-Prompts.md +113 -0
  103. package/docs/v1/README.md +24 -0
  104. package/docs/v1/basics/array.md +17 -0
  105. package/docs/v1/basics/simpleMath.md +73 -0
  106. package/docs/v1/libs/TinyAdvancedRaffle.md +674 -0
  107. package/docs/v1/libs/TinyArrayPaginator.md +150 -0
  108. package/docs/v1/libs/TinyCookieConsent.md +152 -0
  109. package/docs/v1/libs/TinyDayNightCycle.md +711 -0
  110. package/docs/v1/libs/TinyGamepad.md +980 -0
  111. package/docs/v1/libs/TinyI18.md +515 -0
  112. package/docs/v1/libs/TinyInventory.md +777 -0
  113. package/docs/v1/libs/TinyInventoryTrader.md +114 -0
  114. package/docs/v1/libs/TinySmartScroller.md +4 -4
  115. package/docs/v1/libs/TinyTextarea.md +156 -0
  116. package/package.json +7 -5
  117. package/dist/v1/basics/html_deprecated.cjs +0 -124
  118. package/dist/v1/basics/html_deprecated.d.mts +0 -40
  119. package/dist/v1/basics/html_deprecated.mjs +0 -97
  120. package/dist/v1/css/TinyDraggerExample.css +0 -21
  121. package/dist/v1/css/TinyNotify.css +0 -350
  122. package/dist/v1/css/aiMarker.css +0 -4
  123. package/docs/v1/basics/html_deprecated.md +0 -127
@@ -0,0 +1,2690 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {Object} KeyStatus
5
+ * Describes the status of a key or button.
6
+ * @property {boolean} pressed - Whether the key is currently pressed.
7
+ * @property {number} [value] - Optional analog value associated with the key.
8
+ * @property {number} [value2] - Optional second analog value.
9
+ */
10
+
11
+ /**
12
+ * @typedef {'gamepad-only' | 'keyboard-only' | 'both'} InputMode
13
+ * Defines the available input modes.
14
+ */
15
+
16
+ /**
17
+ * A callback function that is invoked when a mapped logical input is activated or deactivated.
18
+ *
19
+ * This function receives the logical name associated with the input (e.g., "Jump", "Shoot", "Menu")
20
+ * and can be used to handle input-related actions such as triggering game mechanics or UI behavior.
21
+ *
22
+ * @typedef {(payload: { logicalName: string, activeTime: number, comboTime: number }) => void} MappedInputCallback
23
+ */
24
+
25
+ /**
26
+ * A callback function that is invoked when a mapped key is activated or deactivated.
27
+ *
28
+ * This function receives the key name associated with the input (e.g., "KeyA", "KeyB", "KeyC")
29
+ * and can be used to handle input-related actions such as triggering game mechanics or UI behavior.
30
+ *
31
+ * @typedef {(payload: { key: string, activeTime: number }) => void} MappedKeyCallback
32
+ */
33
+
34
+ /**
35
+ * @typedef {(payload: InputPayload|InputAnalogPayload) => void} PayloadCallback
36
+ * Callback for handling input events.
37
+ */
38
+
39
+ /**
40
+ * @typedef {(payload: ConnectionPayload) => void} ConnectionCallback
41
+ * Callback for handling gamepad connection events.
42
+ */
43
+
44
+ /**
45
+ * A callback function that is triggered when a registered input sequence is fully activated.
46
+ *
47
+ * @callback InputSequenceCallback
48
+ * @param {number} timestamp - The moment in milliseconds when the sequence was successfully detected.
49
+ */
50
+
51
+ /**
52
+ * A callback function that is triggered when a registered key sequence is fully activated.
53
+ *
54
+ * @callback KeySequenceCallback
55
+ * @param {number} timestamp - The moment in milliseconds when the sequence was successfully detected.
56
+ */
57
+
58
+ /**
59
+ * Represents any valid callback type used in the TinyGamepad event system.
60
+ * This is a union of all supported callback signatures.
61
+ * @typedef {ConnectionCallback | PayloadCallback | MappedInputCallback | MappedKeyCallback} CallbackList
62
+ */
63
+
64
+ /**
65
+ * Represents a specific input source from a gamepad.
66
+ * - 'gamepad-analog' refers to analog sticks or analog triggers.
67
+ * - 'gamepad-button' refers to digital buttons on the gamepad.
68
+ * @typedef {'gamepad-analog'|'gamepad-button'} GamepadDeviceSource
69
+ */
70
+
71
+ /**
72
+ * Represents any possible physical device source that can be used for input.
73
+ * - 'mouse': Input from a mouse.
74
+ * - 'keyboard': Input from a keyboard.
75
+ * - 'gamepad-analog': Analog input from a gamepad (e.g., sticks, triggers).
76
+ * - 'gamepad-button': Digital button input from a gamepad.
77
+ * @typedef {'mouse'|'keyboard'|GamepadDeviceSource} DeviceSource
78
+ */
79
+
80
+ /**
81
+ * Represents the type of input interaction detected.
82
+ * - 'up': Input was released.
83
+ * - 'down': Input was pressed.
84
+ * - 'hold': Input is being held.
85
+ * - 'change': Input value changed (e.g., pressure or axis).
86
+ * - 'move': Analog movement detected (e.g., joystick motion).
87
+ * @typedef {'up'|'down'|'hold'|'change'|'move'} DeviceInputType
88
+ */
89
+
90
+ /**
91
+ * @typedef {Object} InputPayload
92
+ * Structure for digital button input payload.
93
+ * @property {string} id - Unique identifier for the input.
94
+ * @property {Event} [event] - Optional DOM event reference.
95
+ * @property {DeviceInputType} type - Type of input event (down, up, hold).
96
+ * @property {DeviceSource} source - Input source (keyboard, mouse, gamepad).
97
+ * @property {string} key - Physical input identifier.
98
+ * @property {boolean} isPressure - Whether the input is pressure sensitive.
99
+ * @property {string} logicalName - Logical name associated with the input.
100
+ * @property {number} value - Primary analog value.
101
+ * @property {number} value2 - Secondary analog value.
102
+ * @property {boolean} pressed - Current pressed status.
103
+ * @property {boolean|null} [prevPressed] - Previous pressed status.
104
+ * @property {number} timestamp - Timestamp of the event.
105
+ * @property {Gamepad} [gp] - Reference to the originating Gamepad.
106
+ */
107
+
108
+ /**
109
+ * @typedef {Object} InputAnalogPayload
110
+ * Structure for analog input payload.
111
+ * @property {string} id - Unique identifier for the input.
112
+ * @property {Event} [event] - Optional DOM event reference.
113
+ * @property {DeviceInputType} type - Type of input event (change).
114
+ * @property {DeviceSource} source - Input source.
115
+ * @property {string} key - Physical input identifier.
116
+ * @property {string} logicalName - Logical name associated with the input.
117
+ * @property {number} value - Analog value.
118
+ * @property {number} value2 - Secondary analog value.
119
+ * @property {number} timestamp - Timestamp of the event.
120
+ * @property {Gamepad} [gp] - Reference to the originating Gamepad.
121
+ */
122
+
123
+ /**
124
+ * @typedef {Object} InputEvents
125
+ * Internal structure for digital input events.
126
+ * @property {string} id - Unique identifier for the input.
127
+ * @property {Event} [event] - Optional DOM event reference.
128
+ * @property {string} key - Input key identifier.
129
+ * @property {DeviceSource} source - Source of the input.
130
+ * @property {number} value - Value of the input.
131
+ * @property {number} value2 - Secondary value.
132
+ * @property {DeviceInputType} type - Type of input event.
133
+ * @property {boolean} isPressure - Whether it is pressure-sensitive.
134
+ * @property {boolean} pressed - Pressed status.
135
+ * @property {boolean|null} [prevPressed] - Previous pressed status.
136
+ * @property {number} timestamp - Timestamp of the event.
137
+ * @property {Gamepad} [gp] - Reference to the gamepad.
138
+ */
139
+
140
+ /**
141
+ * @typedef {Object} InputAnalogEvents
142
+ * Internal structure for analog input events.
143
+ * @property {string} id - Unique identifier for the input.
144
+ * @property {Event} [event] - Optional DOM event reference.
145
+ * @property {string} key - Analog key.
146
+ * @property {DeviceSource} source - Source of input.
147
+ * @property {number} value - Main analog value.
148
+ * @property {number} value2 - Secondary analog value.
149
+ * @property {DeviceInputType} type - Type of event.
150
+ * @property {number} timestamp - Timestamp.
151
+ * @property {Gamepad} [gp] - Gamepad reference.
152
+ */
153
+
154
+ /**
155
+ * @typedef {Object} ConnectionPayload
156
+ * Payload for connection-related events.
157
+ * @property {string} id - ID of the gamepad.
158
+ * @property {number} timestamp - Timestamp of the event.
159
+ * @property {Gamepad} gp - Gamepad instance.
160
+ */
161
+
162
+ /**
163
+ * @typedef {Object} ExportedConfig
164
+ * @property {string | null} expectedId - The expected identifier for a specific gamepad device, or null if not set.
165
+ * @property {string[]} ignoreIds - Array of device IDs to be ignored (excluded from input detection).
166
+ * @property {number} deadZone - The threshold value below which analog stick inputs are ignored (dead zone).
167
+ * @property {number} timeoutComboKeys - Time in milliseconds allowed between consecutive inputs in a combo sequence before it resets.
168
+ * @property {number} axisActiveSensitivity - Sensitivity threshold for detecting significant analog axis movement (0 = most sensitive, 1 = least sensitive).
169
+ * @property {[string, string | string[]][]} inputMap - Array of key-value pairs representing the mapping between logical input names and physical input(s).
170
+ */
171
+
172
+ /**
173
+ * TinyGamepad is a high-level and extensible input management system
174
+ * designed for professional-level control schemes in games or applications.
175
+ *
176
+ * It supports input from gamepads, keyboards, and optionally mouse devices.
177
+ * Key features include:
178
+ * - Input mapping from physical controls to logical action names
179
+ * - Pressure-sensitive trigger and button support
180
+ * - Dead zone configuration for analog inputs
181
+ * - Event-based input handling (start, hold, end)
182
+ * - Multiple logical names per input binding
183
+ * - Unified control model for both digital and analog inputs
184
+ *
185
+ * TinyGamepad allows seamless integration of fallback control types,
186
+ * detailed input feedback, and JSON-based profile serialization for saving and restoring mappings.
187
+ */
188
+ class TinyGamepad {
189
+ /** @type {boolean} Indicates whether this instance has been destroyed and is no longer usable. */
190
+ #isDestroyed = false;
191
+
192
+ /** @type {Set<string>} Currently held physical key/input identifiers. */
193
+ #heldKeys = new Set();
194
+
195
+ /** @type {Map<string, string|string[]>} Maps logical input names to one or more physical input identifiers. */
196
+ #inputMap = new Map();
197
+
198
+ /**
199
+ * @type {Map<string, CallbackList[]>}
200
+ * Stores all event callback arrays for different input-related events.
201
+ */
202
+ #callbacks = new Map();
203
+
204
+ /** @type {null|Gamepad} Holds the currently connected Gamepad instance, or null if none is connected. */
205
+ #connectedGamepad = null;
206
+
207
+ /** @type {KeyStatus[]} Stores the previous button states for the connected gamepad to detect changes. */
208
+ #lastButtonStates = [];
209
+
210
+ /** @type {Record<string|number, KeyStatus>} Tracks the previous state of each key or button (keyboard/mouse/gamepad). */
211
+ #lastKeyStates = {};
212
+
213
+ /** @type {number[]} Stores the last known values of all gamepad analog axes. */
214
+ #lastAxes = [];
215
+
216
+ /** @type {null|number} Holds the requestAnimationFrame ID for the gamepad update loop. */
217
+ #animationFrame = null;
218
+
219
+ /** @type {null|number} Holds the setInterval ID for keyboard and mouse hold tracking. */
220
+ #mouseKeyboardHoldLoop = null;
221
+
222
+ /** @type {InputMode} Defines the current input mode (keyboard, gamepad, or both). */
223
+ #inputMode;
224
+
225
+ /** @type {Set<string>} A list of controller IDs to ignore during gamepad scanning. */
226
+ #ignoreIds;
227
+
228
+ /** @type {number} Dead zone threshold for analog stick sensitivity. */
229
+ #deadZone;
230
+
231
+ /** @type {string|null} The expected gamepad ID (if filtering by specific controller model). */
232
+ #expectedId;
233
+
234
+ /** @type {boolean} Whether mouse inputs are accepted and mapped like other inputs. */
235
+ #allowMouse;
236
+
237
+ /** @type {Window|Element} The element or window that receives input events (keyboard/mouse). */
238
+ #elementBase;
239
+
240
+ /**
241
+ * @type {number}
242
+ * Time in milliseconds before resetting a combination of raw keys (used for basic key sequences).
243
+ */
244
+ #timeoutComboKeys;
245
+
246
+ /**
247
+ * Axis movement threshold to consider an axis active combo.
248
+ * Value range: 0 (most sensitive) to 1 (least sensitive).
249
+ * @type {number}
250
+ */
251
+ #axisActiveSensitivity;
252
+
253
+ /** @type {string[]} Temporarily holds the current sequence of mapped inputs being pressed. */
254
+ #comboInputs = [];
255
+
256
+ /** @type {number} Timestamp of the last mapped input combo trigger. */
257
+ #timeComboInputs = 0;
258
+
259
+ /** @type {number} Timestamp of the last mapped input. */
260
+ #timeMappedInputs = 0;
261
+
262
+ /** @type {string[]} Temporarily holds the current sequence of raw keys being pressed. */
263
+ #comboKeys = [];
264
+
265
+ /** @type {number} Timestamp of the last key combo trigger. */
266
+ #timeComboKeys = 0;
267
+
268
+ /** @type {NodeJS.Timeout|null} Timer for auto-resetting the raw key combo sequence. */
269
+ #intervalComboKeys = null;
270
+
271
+ /**
272
+ * @type {Set<string>}
273
+ * Set of currently active logical keys (used to track which mapped actions are being held).
274
+ */
275
+ #activeMappedKeys = new Set();
276
+
277
+ /**
278
+ * @type {Set<string>}
279
+ * Set of currently active logical inputs (used to track which mapped actions are being held).
280
+ */
281
+ #activeMappedInputs = new Set();
282
+
283
+ /**
284
+ * @type {Map<string, { sequence: string[], callback: InputSequenceCallback, triggered: boolean }>}
285
+ * Stores all registered logical input sequences and their associated callbacks.
286
+ */
287
+ #inputSequences = new Map();
288
+
289
+ /**
290
+ * @type {Map<string, { sequence: string[], callback: KeySequenceCallback, triggered: boolean }>}
291
+ * Stores all registered raw key sequences and their associated callbacks.
292
+ */
293
+ #keySequences = new Map();
294
+
295
+ /**
296
+ * Stores the previous values of each input key to track state changes between updates.
297
+ * @type {Map<string, { value: number; value2: number }>}
298
+ */
299
+ #keyOldValue = new Map();
300
+
301
+ /** @type {Record<string, string>} */
302
+ static #specialMap = {
303
+ ' ': 'Space',
304
+ '\n': 'Enter',
305
+ '\r': 'Enter',
306
+ '\t': 'Tab',
307
+ '-': 'Minus',
308
+ _: 'Minus',
309
+ '=': 'Equal',
310
+ '+': 'Equal',
311
+ '[': 'BracketLeft',
312
+ '{': 'BracketLeft',
313
+ ']': 'BracketRight',
314
+ '}': 'BracketRight',
315
+ '\\': 'Backslash',
316
+ '|': 'Backslash',
317
+ ';': 'Semicolon',
318
+ ':': 'Semicolon',
319
+ "'": 'Quote',
320
+ '"': 'Quote',
321
+ ',': 'Comma',
322
+ '<': 'Comma',
323
+ '.': 'Period',
324
+ '>': 'Period',
325
+ '/': 'Slash',
326
+ '?': 'Slash',
327
+ '`': 'Backquote',
328
+ '~': 'Backquote',
329
+ };
330
+
331
+ /**
332
+ * Add or update a special key mapping.
333
+ * @param {string} char - The character to map.
334
+ * @param {string} code - The corresponding key code.
335
+ */
336
+ static addSpecialKey(char, code) {
337
+ if (typeof char !== 'string')
338
+ throw new TypeError(`Invalid char type: expected string, got ${typeof char}`);
339
+ if (char.length !== 1)
340
+ throw new Error(`Invalid char length: "${char}" (must be exactly one character)`);
341
+ if (typeof code !== 'string')
342
+ throw new TypeError(`Invalid code type: expected string, got ${typeof code}`);
343
+ TinyGamepad.#specialMap[char] = code;
344
+ }
345
+
346
+ /**
347
+ * Remove a special key mapping.
348
+ * @param {string} char - The character to remove from mapping.
349
+ */
350
+ static removeSpecialKey(char) {
351
+ if (typeof char !== 'string')
352
+ throw new TypeError(`Invalid char type: expected string, got ${typeof char}`);
353
+ if (char.length !== 1)
354
+ throw new Error(`Invalid char length: "${char}" (must be exactly one character)`);
355
+ delete TinyGamepad.#specialMap[char];
356
+ }
357
+
358
+ /**
359
+ * Get the mapped code for a special character.
360
+ * @param {string} char - The character to look up.
361
+ * @returns {string | undefined} - The mapped code or undefined if not found.
362
+ */
363
+ static getSpecialKey(char) {
364
+ if (typeof char !== 'string')
365
+ throw new TypeError(`Invalid char type: expected string, got ${typeof char}`);
366
+ if (char.length !== 1)
367
+ throw new Error(`Invalid char length: "${char}" (must be exactly one character)`);
368
+ return TinyGamepad.#specialMap[char];
369
+ }
370
+
371
+ /**
372
+ * Get all current special key mappings.
373
+ * @returns {Record<string, string>}
374
+ */
375
+ static getAllSpecialKeys() {
376
+ return { ...TinyGamepad.#specialMap };
377
+ }
378
+
379
+ /**
380
+ * Converts a string into an array of TinyGamepad-style key codes.
381
+ * Example: "pudding" → ['KeyP', 'KeyU', 'KeyD', 'KeyD', 'KeyI', 'KeyN', 'KeyG']
382
+ * @param {string} text - Input text.
383
+ * @returns {string[]} Array of key codes.
384
+ */
385
+ static stringToKeys(text) {
386
+ if (typeof text !== 'string')
387
+ throw new TypeError(`Invalid text type: expected string, got ${typeof text}`);
388
+ if (!text.length) throw new Error(`Invalid text: cannot be empty`);
389
+ return Array.from(text).map((char) => {
390
+ const upper = char.toUpperCase();
391
+ if (upper >= 'A' && upper <= 'Z') {
392
+ return `Key${upper}`;
393
+ }
394
+ if (upper >= '0' && upper <= '9') {
395
+ return `Digit${upper}`;
396
+ }
397
+ const mapped = TinyGamepad.#specialMap[char];
398
+ if (mapped !== undefined) {
399
+ return mapped;
400
+ }
401
+ throw new Error(`Unsupported character: "${char}"`);
402
+ });
403
+ }
404
+
405
+ /**
406
+ * Initializes a new instance of TinyGamepad with customizable input behavior.
407
+ *
408
+ * This constructor allows configuring the expected device ID, the type of inputs to listen for
409
+ * (keyboard, gamepad, or both), analog dead zone sensitivity, and whether to allow mouse input.
410
+ * It also supports filtering out specific devices by ID.
411
+ *
412
+ * @param {Object} options - Configuration object for TinyGamepad behavior.
413
+ * @param {string | null} [options.expectedId=null] - Specific controller ID to expect.
414
+ * @param {InputMode} [options.inputMode='both'] - Mode of input to use.
415
+ * @param {string[]} [options.ignoreIds=[]] - List of device IDs to ignore.
416
+ * @param {number} [options.deadZone=0.1] - Analog stick dead zone threshold.
417
+ * @param {boolean} [options.allowMouse=false] - Whether mouse events should be treated as input triggers.
418
+ * @param {number} [options.timeoutComboKeys=500] - Maximum time (in milliseconds) allowed between inputs in a key sequence before the reset time.
419
+ * @param {number} [options.axisActiveSensitivity=0.3] - Threshold to detect meaningful axis movement (0 = most sensitive, 1 = least sensitive).
420
+ * @param {Window|Element} [options.elementBase=window] - The DOM element or window to bind keyboard and mouse events to.
421
+ */
422
+ constructor({
423
+ expectedId = null,
424
+ inputMode = 'both',
425
+ ignoreIds = [],
426
+ deadZone = 0.1,
427
+ axisActiveSensitivity = 0.3,
428
+ timeoutComboKeys = 500,
429
+ allowMouse = false,
430
+ elementBase = window,
431
+ } = {}) {
432
+ // Validate expectedId
433
+ if (expectedId !== null && typeof expectedId !== 'string')
434
+ throw new TypeError(`"expectedId" must be a string or null, received: ${typeof expectedId}`);
435
+
436
+ // Validate inputMode
437
+ if (!['keyboard-only', 'gamepad-only', 'both'].includes(inputMode))
438
+ throw new TypeError(
439
+ `"inputMode" must be 'keyboard-only', 'gamepad-only', or 'both', received: ${inputMode}`,
440
+ );
441
+
442
+ // Validate ignoreIds
443
+ if (!Array.isArray(ignoreIds) || !ignoreIds.every((id) => typeof id === 'string'))
444
+ throw new TypeError(`"ignoreIds" must be an array of strings`);
445
+
446
+ // Validate deadZone
447
+ if (typeof deadZone !== 'number' || deadZone < 0 || deadZone > 1)
448
+ throw new RangeError(`"deadZone" must be a number between 0 and 1, received: ${deadZone}`);
449
+
450
+ // Validate axisActiveSensitivity
451
+ if (
452
+ typeof axisActiveSensitivity !== 'number' ||
453
+ axisActiveSensitivity < 0 ||
454
+ axisActiveSensitivity > 1
455
+ )
456
+ throw new RangeError(
457
+ `"axisActiveSensitivity" must be a number between 0 and 1, received: ${axisActiveSensitivity}`,
458
+ );
459
+
460
+ // Validate timeoutComboKeys
461
+ if (typeof timeoutComboKeys !== 'number' || timeoutComboKeys < 0)
462
+ throw new RangeError(
463
+ `"timeoutComboKeys" must be a positive number, received: ${timeoutComboKeys}`,
464
+ );
465
+
466
+ // Validate allowMouse
467
+ if (typeof allowMouse !== 'boolean')
468
+ throw new TypeError(`"allowMouse" must be a boolean, received: ${typeof allowMouse}`);
469
+
470
+ // Validate elementBase
471
+ if (!(elementBase instanceof Window || elementBase instanceof Element))
472
+ throw new TypeError(`"elementBase" must be a Window or Element instance`);
473
+
474
+ this.#expectedId = expectedId;
475
+ this.#inputMode = inputMode;
476
+ this.#ignoreIds = new Set(ignoreIds);
477
+ this.#deadZone = deadZone;
478
+ this.#allowMouse = allowMouse;
479
+ this.#elementBase = elementBase;
480
+ this.#timeoutComboKeys = timeoutComboKeys;
481
+ this.#axisActiveSensitivity = axisActiveSensitivity;
482
+
483
+ if (['gamepad-only', 'both'].includes(this.#inputMode)) {
484
+ this.#initGamepadEvents();
485
+ }
486
+
487
+ if (['keyboard-only', 'both'].includes(this.#inputMode)) {
488
+ this.#initKeyboardMouse();
489
+ }
490
+ }
491
+
492
+ //////////////////////////////////////////
493
+
494
+ /** @type {(this: Window, ev: GamepadEvent) => any} */
495
+ #gamepadConnected = (e) => this.#onGamepadConnect(e.gamepad);
496
+
497
+ /** @type {(this: Window, ev: GamepadEvent) => any} */
498
+ #gamepadDisconnected = (e) => this.#onGamepadDisconnect(e.gamepad);
499
+
500
+ /**
501
+ * Initializes listeners for gamepad connection and disconnection.
502
+ * Automatically detects and handles supported gamepads.
503
+ */
504
+ #initGamepadEvents() {
505
+ window.addEventListener('gamepadconnected', this.#gamepadConnected);
506
+ window.addEventListener('gamepaddisconnected', this.#gamepadDisconnected);
507
+ }
508
+
509
+ /**
510
+ * Internal callback when a gamepad is connected.
511
+ * Starts polling and emits a "connected" event.
512
+ * @param {Gamepad} gamepad
513
+ */
514
+ #onGamepadConnect(gamepad) {
515
+ if (this.#isDestroyed) return;
516
+ if (this.#ignoreIds.has(gamepad.id)) return;
517
+ if (this.#expectedId && gamepad.id !== this.#expectedId) return;
518
+
519
+ if (!this.#connectedGamepad) {
520
+ this.#connectedGamepad = gamepad;
521
+ this.#expectedId = gamepad.id;
522
+
523
+ this.#startPolling();
524
+ this.#emit('connected', { id: gamepad.id, gp: gamepad, timestamp: gamepad.timestamp });
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Internal callback when a gamepad is disconnected.
530
+ * Cancels polling and emits a "disconnected" event.
531
+ * @param {Gamepad} gamepad
532
+ */
533
+ #onGamepadDisconnect(gamepad) {
534
+ if (this.#isDestroyed) return;
535
+ if (this.#connectedGamepad && gamepad.id === this.#connectedGamepad.id) {
536
+ this.#connectedGamepad = null;
537
+ if (this.#animationFrame) {
538
+ cancelAnimationFrame(this.#animationFrame);
539
+ this.#animationFrame = null;
540
+ }
541
+ this.#emit('disconnected', { id: gamepad.id, gp: gamepad, timestamp: gamepad.timestamp });
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Starts the polling loop for tracking gamepad state.
547
+ */
548
+ #startPolling() {
549
+ const loop = () => {
550
+ if (this.#isDestroyed) return;
551
+ this.#checkGamepadState();
552
+ this.#animationFrame = requestAnimationFrame(loop);
553
+ };
554
+ loop();
555
+ }
556
+
557
+ /**
558
+ * Compares and emits input changes from buttons and axes on the gamepad.
559
+ */
560
+ #checkGamepadState() {
561
+ if (this.#isDestroyed) return;
562
+ const pads = navigator.getGamepads();
563
+ const gp = Array.from(pads).find((g) => g && g.id === this.#expectedId);
564
+ if (!gp) return;
565
+
566
+ this.#connectedGamepad = gp;
567
+
568
+ gp.buttons.forEach((btn, index) => {
569
+ const key = `Button${index}`;
570
+ const prev = this.#lastButtonStates[index]?.pressed || false;
571
+
572
+ /** @type {DeviceInputType} */
573
+ let type;
574
+
575
+ const source = 'gamepad-button';
576
+ let value;
577
+ let isPressure = false;
578
+ if (btn.pressed && !prev) {
579
+ value = 1;
580
+ type = 'down';
581
+ } else if (!btn.pressed && prev) {
582
+ value = 0;
583
+ type = 'up';
584
+ } else if (btn.pressed && prev) {
585
+ value = 1;
586
+ type = 'hold';
587
+ }
588
+
589
+ if (btn.pressed && btn.value > 0 && btn.value < 1 && btn.value !== 1) {
590
+ value = btn.value;
591
+ isPressure = true;
592
+ }
593
+
594
+ // @ts-ignore
595
+ if (typeof value === 'number' && typeof type === 'string')
596
+ this.#handleInput({
597
+ key,
598
+ source,
599
+ value,
600
+ value2: NaN,
601
+ type,
602
+ gp,
603
+ isPressure,
604
+ pressed: btn.pressed,
605
+ prevPressed: prev,
606
+ timestamp: gp.timestamp,
607
+ id: gp.id,
608
+ });
609
+ this.#lastButtonStates[index] = {
610
+ pressed: btn.pressed,
611
+ value: typeof value === 'number' ? value : this.#lastButtonStates[index]?.value,
612
+ value2: NaN,
613
+ };
614
+ });
615
+
616
+ gp.axes.forEach((val, index) => {
617
+ if (Math.abs(val) < this.#deadZone) val = 0;
618
+
619
+ const key = `Axis${index}`;
620
+ const prev = this.#lastAxes[index] ?? 0;
621
+ if (val !== prev) {
622
+ this.#handleInput({
623
+ key,
624
+ source: 'gamepad-analog',
625
+ value: val,
626
+ value2: NaN,
627
+ type: 'change',
628
+ timestamp: gp.timestamp,
629
+ id: gp.id,
630
+ gp,
631
+ });
632
+ }
633
+ this.#lastAxes[index] = val;
634
+ });
635
+ }
636
+
637
+ ///////////////////////////////////
638
+
639
+ /**
640
+ * Listener for the 'keydown' event.
641
+ * Triggers when a key is pressed and marks it as held.
642
+ * Avoids duplicate presses while the key remains down.
643
+ * Reports the input as a 'keyboard' source.
644
+ *
645
+ * @type {EventListener}
646
+ */
647
+ #keydown = (e) => {
648
+ if (this.#isDestroyed) return;
649
+ if (!(e instanceof KeyboardEvent))
650
+ throw new Error('Expected KeyboardEvent in keydown listener.');
651
+ if (!this.#heldKeys.has(e.code)) {
652
+ this.#heldKeys.add(e.code);
653
+ this.#handleInput({
654
+ event: e,
655
+ key: e.code,
656
+ source: 'keyboard',
657
+ value: 1,
658
+ value2: NaN,
659
+ type: 'down',
660
+ pressed: true,
661
+ prevPressed: this.#lastKeyStates[e.code]?.pressed ?? false,
662
+ id: 'native_keyboard',
663
+ timestamp: e.timeStamp,
664
+ });
665
+ this.#lastKeyStates[e.code] = { pressed: true };
666
+ }
667
+ };
668
+
669
+ /**
670
+ * Listener for the 'keyup' event.
671
+ * Triggers when a key is released and removes it from the held list.
672
+ * Reports the input as a 'keyboard' source.
673
+ *
674
+ * @type {EventListener}
675
+ */
676
+ #keyup = (e) => {
677
+ if (this.#isDestroyed) return;
678
+ if (!(e instanceof KeyboardEvent)) throw new Error('Expected KeyboardEvent in keyup listener.');
679
+ if (this.#heldKeys.has(e.code)) {
680
+ this.#heldKeys.delete(e.code);
681
+ this.#handleInput({
682
+ event: e,
683
+ key: e.code,
684
+ source: 'keyboard',
685
+ value: 0,
686
+ value2: NaN,
687
+ type: 'up',
688
+ pressed: false,
689
+ prevPressed: this.#lastKeyStates[e.code]?.pressed ?? false,
690
+ id: 'native_keyboard',
691
+ timestamp: e.timeStamp,
692
+ });
693
+ this.#lastKeyStates[e.code] = { pressed: false };
694
+ }
695
+ };
696
+
697
+ /**
698
+ * Listener for the 'mousedown' event.
699
+ * Fires when a mouse button is pressed.
700
+ * Identifies each button as 'Mouse<button>' and tracks its held state.
701
+ *
702
+ * @type {EventListener}
703
+ */
704
+ #mousedown = (e) => {
705
+ if (this.#isDestroyed) return;
706
+ if (!(e instanceof MouseEvent)) throw new Error('Expected MouseEvent in mousedown listener.');
707
+ const key = `Mouse${e.button}`;
708
+ this.#heldKeys.add(key);
709
+ this.#handleInput({
710
+ event: e,
711
+ key,
712
+ source: 'mouse',
713
+ value: 1,
714
+ value2: NaN,
715
+ type: 'down',
716
+ pressed: true,
717
+ prevPressed: this.#lastKeyStates[key]?.pressed ?? false,
718
+ id: 'native_mouse',
719
+ timestamp: e.timeStamp,
720
+ });
721
+ this.#lastKeyStates[key] = { pressed: true };
722
+ };
723
+
724
+ /**
725
+ * Listener for the 'mouseup' event.
726
+ * Fires when a mouse button is released.
727
+ * Stops tracking the held state of the given button.
728
+ *
729
+ * @type {EventListener}
730
+ */
731
+ #mouseup = (e) => {
732
+ if (this.#isDestroyed) return;
733
+ if (!(e instanceof MouseEvent)) throw new Error('Expected MouseEvent in mouseup listener.');
734
+ const key = `Mouse${e.button}`;
735
+ this.#heldKeys.delete(key);
736
+ this.#handleInput({
737
+ event: e,
738
+ key,
739
+ source: 'mouse',
740
+ value: 0,
741
+ value2: NaN,
742
+ type: 'up',
743
+ pressed: false,
744
+ prevPressed: this.#lastKeyStates[key]?.pressed ?? false,
745
+ id: 'native_mouse',
746
+ timestamp: e.timeStamp,
747
+ });
748
+ this.#lastKeyStates[key] = { pressed: false };
749
+ };
750
+
751
+ /**
752
+ * Listener for the 'mousemove' event.
753
+ * Tracks relative movement of the mouse using movementX and movementY.
754
+ * Used to simulate analog movement via mouse input.
755
+ *
756
+ * @type {EventListener}
757
+ */
758
+ #mousemove = (e) => {
759
+ if (this.#isDestroyed) return;
760
+ if (!(e instanceof MouseEvent)) throw new Error('Expected MouseEvent in mousemove listener.');
761
+ if (e.movementX !== 0 || e.movementY !== 0) {
762
+ const key = 'MouseMove';
763
+ /** @type {KeyStatus} */
764
+ const old = this.#lastKeyStates[key] ?? { value: 0};
765
+ this.#handleInput({
766
+ event: e,
767
+ key,
768
+ source: 'mouse',
769
+ value: e.movementX + (old.value ?? 0),
770
+ value2: e.movementY + (old.value ?? 0),
771
+ id: 'native_mouse',
772
+ type: 'move',
773
+ pressed: true,
774
+ prevPressed: null,
775
+ timestamp: e.timeStamp,
776
+ });
777
+ this.#lastKeyStates[key] = { pressed: false, value: e.movementX, value2: e.movementY };
778
+ }
779
+ };
780
+
781
+ /**
782
+ * Initializes keyboard and mouse event listeners to emulate input behavior.
783
+ */
784
+ #initKeyboardMouse() {
785
+ // Keyboard
786
+ this.#elementBase.addEventListener('keydown', this.#keydown);
787
+ this.#elementBase.addEventListener('keyup', this.#keyup);
788
+ if (this.#allowMouse) {
789
+ this.#elementBase.addEventListener('mousedown', this.#mousedown);
790
+ this.#elementBase.addEventListener('mouseup', this.#mouseup);
791
+ this.#elementBase.addEventListener('mousemove', this.#mousemove);
792
+ }
793
+
794
+ // Opcional: checagem contínua para "hold"
795
+ const loop = () => {
796
+ if (this.#isDestroyed) return;
797
+ this.#heldKeys.forEach((key) => {
798
+ const source = !key.startsWith('Mouse') ? 'keyboard' : 'mouse';
799
+ this.#handleInput({
800
+ key,
801
+ source,
802
+ id: `native_${source}`,
803
+ value: 1,
804
+ value2: NaN,
805
+ type: 'hold',
806
+ pressed: true,
807
+ prevPressed: this.#lastKeyStates[key]?.pressed ?? false,
808
+ timestamp: NaN,
809
+ });
810
+ });
811
+ this.#mouseKeyboardHoldLoop = requestAnimationFrame(loop);
812
+ };
813
+ loop();
814
+ }
815
+
816
+ //////////////////////////////////
817
+
818
+ /**
819
+ * Handles an input event by dispatching to registered listeners.
820
+ * This method acts as the central hub for all input events (gamepad, keyboard, mouse, etc.),
821
+ * handling both direct physical inputs and their mapped logical equivalents.
822
+ *
823
+ * Features inside this method:
824
+ * - Wildcard callback support (global input listeners)
825
+ * - Automatic detection of axis-based controls (analog sticks, triggers)
826
+ * - Dead zone filtering for axis values (#axisActiveSensitivity)
827
+ * - Dynamic key press/release tracking for both physical keys and mapped inputs
828
+ * - Combination key sequence tracking with timeout handling
829
+ * - Separate callback systems for:
830
+ * -> Physical inputs
831
+ * -> Logical (mapped) inputs
832
+ * -> Start/End/Hold input events
833
+ * -> Combo sequences (key-based and mapped input-based)
834
+ * - Payload injection for callbacks with contextual data
835
+ *
836
+ * @param {InputEvents|InputAnalogEvents} settings - Input event data containing key, value, type, etc.
837
+ */
838
+ #handleInput(settings) {
839
+ if (this.#isDestroyed) return;
840
+
841
+ /**
842
+ * @type {PayloadCallback[]}
843
+ * List of global "input-*" listeners that will receive *all* input events
844
+ * regardless of the specific key, axis, or logical mapping.
845
+ */
846
+ // @ts-ignore
847
+ const globalCbs = this.#callbacks.get('input-*') || [];
848
+
849
+ // Extract main properties from incoming settings
850
+ // @ts-ignore
851
+ const { pressed, key } = settings;
852
+
853
+ /**
854
+ * @type {boolean}
855
+ * Detect if the incoming key belongs to an axis (e.g., 'Axis0', 'Axis1').
856
+ */
857
+ const isAxis = key.startsWith('Axis');
858
+
859
+ /**
860
+ * @type {boolean}
861
+ * Determines whether the input should be considered "active".
862
+ * - For buttons: simply uses the `pressed` flag
863
+ * - For axes: compares value to configured deadzone threshold (#axisActiveSensitivity)
864
+ */
865
+ const isPressed =
866
+ (typeof pressed === 'boolean' && pressed) ||
867
+ (isAxis &&
868
+ (settings.value > this.#axisActiveSensitivity ||
869
+ settings.value < -Math.abs(this.#axisActiveSensitivity)));
870
+
871
+ /**
872
+ * @type {string}
873
+ * The "active key" represents the directional form of the key.
874
+ * - Non-axis: same as the original key
875
+ * - Axis: adds '+' or '-' depending on value direction
876
+ */
877
+ const activeKey = !isAxis
878
+ ? key
879
+ : `${key}${settings.value > 0 ? '+' : settings.value < 0 ? '-' : ''}`;
880
+
881
+ /**
882
+ * @type {boolean|null}
883
+ * Used to track if this event results in a key press (true), release (false), or no change (null).
884
+ */
885
+ let keyResult = null;
886
+
887
+ // -------------------------
888
+ // PHYSICAL KEY TRACKING
889
+ // -------------------------
890
+ if (settings.type !== 'move' && settings.type !== 'hold') {
891
+ if (isPressed) {
892
+ // -------------------------
893
+ // NEW KEY PRESS DETECTION
894
+ // -------------------------
895
+ if (
896
+ (!isAxis && !this.#activeMappedKeys.has(key)) ||
897
+ (isAxis &&
898
+ !this.#activeMappedKeys.has(key) &&
899
+ !this.#activeMappedKeys.has(`${key}+`) &&
900
+ !this.#activeMappedKeys.has(`${key}-`))
901
+ ) {
902
+ if (this.#timeComboKeys === 0) this.#timeComboKeys = Date.now();
903
+ this.#activeMappedKeys.add(activeKey);
904
+ keyResult = true;
905
+
906
+ // Combo tracking
907
+ this.#comboKeys.push(activeKey);
908
+ if (this.#comboInputs.length < 1) {
909
+ if (this.#intervalComboKeys) clearTimeout(this.#intervalComboKeys);
910
+ this.#intervalComboKeys = setTimeout(
911
+ () => this.resetComboMapped(),
912
+ this.#timeoutComboKeys,
913
+ );
914
+ }
915
+
916
+ /**
917
+ * @type {MappedKeyCallback[]}
918
+ * Notifies all "mapped-key-start" listeners that a key has been pressed.
919
+ */
920
+ // @ts-ignore
921
+ const cbs = this.#callbacks.get('mapped-key-start') ?? [];
922
+ for (const cb of cbs)
923
+ cb({
924
+ key: activeKey,
925
+ activeTime: this.#timeComboKeys,
926
+ });
927
+ }
928
+ } else {
929
+ // -------------------------
930
+ // KEY RELEASE DETECTION
931
+ // -------------------------
932
+ if (
933
+ (!isAxis && this.#activeMappedKeys.has(key)) ||
934
+ (isAxis &&
935
+ (this.#activeMappedKeys.has(key) ||
936
+ this.#activeMappedKeys.has(`${key}+`) ||
937
+ this.#activeMappedKeys.has(`${key}-`)))
938
+ ) {
939
+ this.#activeMappedKeys.delete(key);
940
+ this.#activeMappedKeys.delete(`${key}+`);
941
+ this.#activeMappedKeys.delete(`${key}-`);
942
+ keyResult = false;
943
+
944
+ /**
945
+ * @type {MappedKeyCallback[]}
946
+ * Notifies all "mapped-key-end" listeners that a key has been released.
947
+ */
948
+ // @ts-ignore
949
+ const cbs = this.#callbacks.get('mapped-key-end') ?? [];
950
+ for (const cb of cbs)
951
+ cb({
952
+ key: activeKey,
953
+ activeTime: this.#timeComboKeys,
954
+ });
955
+ }
956
+ }
957
+
958
+ // -------------------------
959
+ // PHYSICAL KEY COMBO SEQUENCES
960
+ // -------------------------
961
+ for (const { sequence, callback, triggered } of this.#keySequences.values()) {
962
+ const keySequence = this.#keySequences.get(sequence.join('+'));
963
+ if (!keySequence) continue;
964
+
965
+ const allPressed = sequence.every((name, index) => this.#comboKeys[index] === name);
966
+ if (allPressed && !triggered) {
967
+ keySequence.triggered = true;
968
+ callback(this.#timeComboKeys);
969
+ } else if (!allPressed && triggered) {
970
+ keySequence.triggered = false;
971
+ }
972
+ }
973
+ }
974
+
975
+ // -------------------------
976
+ // LOGICAL (MAPPED) INPUTS
977
+ // -------------------------
978
+ for (const [logical, physical] of this.#inputMap.entries()) {
979
+ /**
980
+ * Checks if a given tinyKey matches the physical mapping of a logical input.
981
+ * @param {string} tinyKey
982
+ * @returns {boolean}
983
+ */
984
+ const checkPhysical = (tinyKey) =>
985
+ (typeof physical === 'string' && tinyKey === physical) ||
986
+ (Array.isArray(physical) && physical.findIndex((value, i) => tinyKey === physical[i]) > -1);
987
+
988
+ const mainKey = checkPhysical(activeKey);
989
+ const baseAxisKeyP = isAxis && checkPhysical(`${key}+`);
990
+ const baseAxisKeyN = isAxis && checkPhysical(`${key}-`);
991
+
992
+ // -------------------------
993
+ // ACTIVE MAPPED INPUT LIST
994
+ // -------------------------
995
+ if (mainKey || baseAxisKeyP || baseAxisKeyN) {
996
+ if (isPressed && mainKey) {
997
+ if (keyResult || !this.#activeMappedInputs.has(logical)) {
998
+ if (this.#timeMappedInputs === 0) this.#timeMappedInputs = Date.now();
999
+ this.#activeMappedInputs.add(logical);
1000
+
1001
+ if (this.#timeComboInputs === 0) this.#timeComboInputs = Date.now();
1002
+ if (this.#intervalComboKeys) clearTimeout(this.#intervalComboKeys);
1003
+ this.#comboInputs.push(logical);
1004
+ this.#intervalComboKeys = setTimeout(
1005
+ () => this.resetComboMapped(),
1006
+ this.#timeoutComboKeys,
1007
+ );
1008
+
1009
+ /**
1010
+ * @type {MappedInputCallback[]}
1011
+ * Notifies all "mapped-input-start" listeners that a logical input has been activated.
1012
+ */
1013
+ // @ts-ignore
1014
+ const cbs = this.#callbacks.get('mapped-input-start') ?? [];
1015
+ for (const cb of cbs)
1016
+ cb({
1017
+ logicalName: logical,
1018
+ activeTime: this.#timeMappedInputs,
1019
+ comboTime: this.#timeComboInputs,
1020
+ });
1021
+ }
1022
+ } else {
1023
+ if (!keyResult || this.#activeMappedInputs.has(logical)) {
1024
+ this.#activeMappedInputs.delete(logical);
1025
+ if (this.#activeMappedInputs.size < 1) this.#timeMappedInputs = 0;
1026
+
1027
+ /**
1028
+ * @type {MappedInputCallback[]}
1029
+ * Notifies all "mapped-input-end" listeners that a logical input has been deactivated.
1030
+ */
1031
+ // @ts-ignore
1032
+ const cbs = this.#callbacks.get('mapped-input-end') ?? [];
1033
+ for (const cb of cbs)
1034
+ cb({
1035
+ logicalName: logical,
1036
+ activeTime: this.#timeMappedInputs,
1037
+ comboTime: this.#timeComboInputs,
1038
+ });
1039
+ }
1040
+ }
1041
+
1042
+ // -------------------------
1043
+ // LOGICAL COMBO SEQUENCES
1044
+ // -------------------------
1045
+ for (const { sequence, callback, triggered } of this.#inputSequences.values()) {
1046
+ const inputSequence = this.#inputSequences.get(sequence.join('+'));
1047
+ if (!inputSequence) continue;
1048
+
1049
+ const activeSequence = Array.from(this.#activeMappedInputs);
1050
+ const allPressed = sequence.every((name, index) => activeSequence[index] === name);
1051
+ if (allPressed && !triggered) {
1052
+ inputSequence.triggered = true;
1053
+ callback(this.#timeMappedInputs);
1054
+ } else if (!allPressed && triggered) {
1055
+ inputSequence.triggered = false;
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ /** @type {string[]} */
1061
+ const keys = [];
1062
+ if (!isAxis || settings.value !== 0) keys.push(activeKey);
1063
+ else {
1064
+ const { value: valueN } = this.#keyOldValue.get(`${key}-`) ?? { value: 0};
1065
+ const { value: valueP } = this.#keyOldValue.get(`${key}+`) ?? { value: 0};
1066
+ if (settings.value !== valueN) keys.push(`${key}-`);
1067
+ if (settings.value !== valueP) keys.push(`${key}+`);
1068
+ }
1069
+
1070
+ keys.forEach((key) => {
1071
+ // -------------------------
1072
+ // MATCH CHECKER (for physical <-> logical link)
1073
+ // -------------------------
1074
+ const matches =
1075
+ physical === '*' ||
1076
+ physical === key ||
1077
+ (Array.isArray(physical) && physical.includes(key));
1078
+
1079
+ if (!matches) return;
1080
+
1081
+ // -------------------------
1082
+ // CALLBACK RETRIEVAL
1083
+ // -------------------------
1084
+ /** @type {PayloadCallback[]} */
1085
+ // @ts-ignore
1086
+ const typeCbs = this.#callbacks.get(`input-${settings.type}-${logical}`) || [];
1087
+
1088
+ /** @type {PayloadCallback[]} */
1089
+ // @ts-ignore
1090
+ const cbs = this.#callbacks.get(`input-${logical}`) || [];
1091
+
1092
+ if (cbs.length < 1 && typeCbs.length < 1 && globalCbs.length < 1) return;
1093
+
1094
+ // -------------------------
1095
+ // PAYLOAD DISPATCH
1096
+ // -------------------------
1097
+ /** @type {InputPayload|InputAnalogPayload} */
1098
+ const payload = { ...settings, key, logicalName: logical };
1099
+ for (const cb of globalCbs) cb(payload);
1100
+ for (const cb of cbs) cb(payload);
1101
+
1102
+ // ➕ Separate event type callbacks
1103
+ for (const cb of typeCbs) cb(payload);
1104
+ this.#keyOldValue.set(key, { value: settings.value, value2: settings.value2 });
1105
+ });
1106
+ }
1107
+ }
1108
+
1109
+ /**
1110
+ * Emits a custom internal event to all listeners.
1111
+ * @param {string} event - Event name.
1112
+ * @param {*} data - Payload data.
1113
+ */
1114
+ #emit(event, data) {
1115
+ const cbs = this.#callbacks.get(event) || [];
1116
+ for (const cb of cbs) cb(data);
1117
+ }
1118
+
1119
+ /**
1120
+ * Registers a callback for a logical template
1121
+ * @param {string} logicalName
1122
+ * @param {CallbackList} callback
1123
+ * @param {string} nameStart
1124
+ */
1125
+ #onTemplate(logicalName, callback, nameStart) {
1126
+ if (typeof logicalName !== 'string')
1127
+ throw new TypeError(`"logicalName" must be a string, received ${logicalName}`);
1128
+ if (typeof callback !== 'function')
1129
+ throw new TypeError(`"callback" must be a function, received ${typeof callback}`);
1130
+ const id = nameStart.replace('{logicalName}', logicalName);
1131
+
1132
+ let callbacks = this.#callbacks.get(id);
1133
+ if (!Array.isArray(callbacks)) {
1134
+ callbacks = [];
1135
+ this.#callbacks.set(id, callbacks);
1136
+ }
1137
+ callbacks.push(callback);
1138
+ }
1139
+
1140
+ /**
1141
+ * Registers a one-time callback for a logical template.
1142
+ * The callback is removed after the first invocation.
1143
+ * @param {string} logicalName
1144
+ * @param {CallbackList} callback
1145
+ * @param {string} nameStart
1146
+ */
1147
+ #onceTemplate(logicalName, callback, nameStart) {
1148
+ if (typeof logicalName !== 'string')
1149
+ throw new TypeError(`"logicalName" must be a string, received ${logicalName}`);
1150
+ if (typeof callback !== 'function')
1151
+ throw new TypeError(`"callback" must be a function, received ${typeof callback}`);
1152
+ /** @type {CallbackList} */
1153
+ // @ts-ignore
1154
+ const wrapper = (payload) => {
1155
+ this.#offTemplate(logicalName, wrapper, nameStart);
1156
+ callback(payload);
1157
+ };
1158
+ this.#onTemplate(logicalName, wrapper, nameStart);
1159
+ }
1160
+
1161
+ /**
1162
+ * Prepends a callback to the template event list.
1163
+ * @param {string} logicalName
1164
+ * @param {CallbackList} callback
1165
+ * @param {string} nameStart
1166
+ */
1167
+ #prependTemplate(logicalName, callback, nameStart) {
1168
+ if (typeof logicalName !== 'string')
1169
+ throw new TypeError(`"logicalName" must be a string, received ${logicalName}`);
1170
+ if (typeof callback !== 'function')
1171
+ throw new TypeError(`"callback" must be a function, received ${typeof callback}`);
1172
+ const id = nameStart.replace('{logicalName}', logicalName);
1173
+
1174
+ const list = this.#callbacks.get(id) ?? [];
1175
+ list.unshift(callback);
1176
+ this.#callbacks.set(id, list);
1177
+ }
1178
+
1179
+ /**
1180
+ * Removes a callback from a specific logical template event.
1181
+ * @param {string} logicalName
1182
+ * @param {CallbackList} callback
1183
+ * @param {string} nameStart
1184
+ */
1185
+ #offTemplate(logicalName, callback, nameStart) {
1186
+ if (typeof logicalName !== 'string')
1187
+ throw new TypeError(`"logicalName" must be a string, received ${logicalName}`);
1188
+ if (typeof callback !== 'function')
1189
+ throw new TypeError(`"callback" must be a function, received ${typeof callback}`);
1190
+ const id = nameStart.replace('{logicalName}', logicalName);
1191
+
1192
+ const list = this.#callbacks.get(id);
1193
+ if (Array.isArray(list))
1194
+ this.#callbacks.set(
1195
+ id,
1196
+ list.filter((cb) => cb !== callback),
1197
+ );
1198
+ }
1199
+
1200
+ ///////////////////////////////////////////////////
1201
+
1202
+ /**
1203
+ * Waits for a single input event from the user and resolves with detailed input information.
1204
+ * This is typically used in control configuration screens to allow the user to choose an input
1205
+ * (keyboard, mouse, or gamepad) that will be mapped to a logical action.
1206
+ *
1207
+ * The function listens for the first eligible input (ignores 'MouseMove') and returns the key,
1208
+ * input source, and the Gamepad object if applicable. If no input is received before the timeout,
1209
+ * the promise resolves with a `null` key and source.
1210
+ *
1211
+ * @param {object} [options] - Optional configuration for input capture behavior.
1212
+ * @param {number} [options.timeout=10000] - Timeout in milliseconds before the promise resolves automatically with null values.
1213
+ * @param {string} [options.eventName='MappingInput'] - The temporary logical event name used internally to listen for input.
1214
+ * @param {boolean} [options.canMove=false] - Whether movement-based inputs (e.g., mouse movement) are allowed.
1215
+ * @returns {Promise<{ key: string | null, source: DeviceSource | null, gp?: Gamepad }>}
1216
+ * A promise that resolves with an object containing:
1217
+ * - `key`: the identifier of the pressed input (e.g., "KeyW", "Button0", "LeftClick"),
1218
+ * - `source`: the origin of the input ("keyboard", "mouse", "gamepad-button", or "gamepad-analog"),
1219
+ * - `gp`: the Gamepad object (only if the input source is a gamepad).
1220
+ */
1221
+ awaitInputMapping({ timeout = 10000, eventName = 'MappingInput', canMove = false } = {}) {
1222
+ return new Promise((resolve, reject) => {
1223
+ // Argument validation
1224
+ if (typeof timeout !== 'number' || Number.isNaN(timeout) || timeout < 0)
1225
+ return reject(
1226
+ new TypeError(`Invalid "timeout": expected a positive number, got ${timeout}`),
1227
+ );
1228
+ if (typeof eventName !== 'string' || !eventName.trim())
1229
+ return reject(
1230
+ new TypeError(`Invalid "eventName": expected a non-empty string, got ${eventName}`),
1231
+ );
1232
+ if (typeof canMove !== 'boolean')
1233
+ return reject(new TypeError(`Invalid "canMove": expected a boolean, got ${canMove}`));
1234
+
1235
+ /** @type {{ key: string|null; source: DeviceSource|null; gp?: Gamepad; }} */
1236
+ const result = { key: null, source: null };
1237
+
1238
+ /** @type {PayloadCallback} */
1239
+ const inputCallback = ({ key, type, source, gp, value }) => {
1240
+ if (!canMove && type === 'move') return;
1241
+ result.key = key;
1242
+ result.source = source;
1243
+ result.gp = gp;
1244
+
1245
+ clearTimeout(timer);
1246
+ this.offInputStart(eventName, inputCallback);
1247
+ this.offInputChange(eventName, inputCallback);
1248
+ if (canMove) this.offInputMove(eventName, inputCallback);
1249
+ resolve(result);
1250
+ };
1251
+
1252
+ // Time limit to auto-cancel input collection
1253
+ const timer = setTimeout(() => resolve(result), timeout);
1254
+
1255
+ this.mapInput(eventName, '*');
1256
+ this.onInputStart(eventName, inputCallback);
1257
+ this.onInputChange(eventName, inputCallback);
1258
+ if (canMove) this.onInputMove(eventName, inputCallback);
1259
+ });
1260
+ }
1261
+
1262
+ /**
1263
+ * Assigns a physical input to a logical name (e.g., "Jump" => "Button1")
1264
+ * @param {string} logicalName
1265
+ * @param {string|string[]} physicalInput
1266
+ */
1267
+ mapInput(logicalName, physicalInput) {
1268
+ if (typeof logicalName !== 'string' || !logicalName.trim())
1269
+ throw new TypeError(`Invalid "logicalName": expected a non-empty string, got ${logicalName}`);
1270
+ if (
1271
+ !(
1272
+ typeof physicalInput === 'string' ||
1273
+ (Array.isArray(physicalInput) && physicalInput.every((p) => typeof p === 'string'))
1274
+ )
1275
+ )
1276
+ throw new TypeError(
1277
+ `Invalid "physicalInput": expected string or array of strings, got ${JSON.stringify(physicalInput)}`,
1278
+ );
1279
+ this.#inputMap.set(logicalName, physicalInput);
1280
+ }
1281
+
1282
+ /**
1283
+ * Removes a logical input mapping
1284
+ * @param {string} logicalName
1285
+ */
1286
+ unmapInput(logicalName) {
1287
+ if (typeof logicalName !== 'string' || !logicalName.trim())
1288
+ throw new TypeError(`Invalid "logicalName": expected a non-empty string, got ${logicalName}`);
1289
+ this.#inputMap.delete(logicalName);
1290
+ }
1291
+
1292
+ /**
1293
+ * Checks if a logical name is mapped to any physical input.
1294
+ * @param {string} logicalName
1295
+ * @returns {boolean}
1296
+ */
1297
+ hasMappedInput(logicalName) {
1298
+ if (typeof logicalName !== 'string' || !logicalName.trim())
1299
+ throw new TypeError(`Invalid "logicalName": expected a non-empty string, got ${logicalName}`);
1300
+ return this.#inputMap.has(logicalName);
1301
+ }
1302
+
1303
+ /**
1304
+ * Returns the physical input(s) mapped to a given logical name.
1305
+ * @param {string} logicalName
1306
+ * @returns {string | string[]}
1307
+ */
1308
+ getMappedInput(logicalName) {
1309
+ if (typeof logicalName !== 'string' || !logicalName.trim())
1310
+ throw new TypeError(`Invalid "logicalName": expected a non-empty string, got ${logicalName}`);
1311
+ const result = this.#inputMap.get(logicalName);
1312
+ if (!result) throw new Error(`No physical input mapped for logicalName: "${logicalName}"`);
1313
+ return result;
1314
+ }
1315
+
1316
+ /**
1317
+ * Clears all mappings for all logical inputs.
1318
+ */
1319
+ clearMapInputs() {
1320
+ this.#inputMap.clear();
1321
+ }
1322
+
1323
+ //////////////////////////////////////////////////////////////
1324
+
1325
+ /**
1326
+ * Registers a sequence of logical inputs that triggers a specific callback.
1327
+ * @param {string[]} sequence - Ordered list of logical input names (e.g., ['Jump', 'Action'])
1328
+ * @param {InputSequenceCallback} callback - Function to invoke when the sequence is fully held
1329
+ */
1330
+ registerInputSequence(sequence, callback) {
1331
+ if (!Array.isArray(sequence) || !sequence.every((s) => typeof s === 'string'))
1332
+ throw new TypeError(
1333
+ `'sequence' must be an array of strings, got: ${JSON.stringify(sequence)}`,
1334
+ );
1335
+ if (typeof callback !== 'function')
1336
+ throw new TypeError(`'callback' must be a function, got: ${typeof callback}`);
1337
+ if (sequence.length === 0) throw new Error(`'sequence' must contain at least one input name.`);
1338
+ const key = sequence.join('+');
1339
+ this.#inputSequences.set(key, { sequence, callback, triggered: false });
1340
+ }
1341
+
1342
+ /**
1343
+ * Unregisters a previously registered input sequence.
1344
+ * @param {string[]} sequence - The sequence to remove from detection
1345
+ */
1346
+ unregisterInputSequence(sequence) {
1347
+ if (!Array.isArray(sequence) || !sequence.every((s) => typeof s === 'string'))
1348
+ throw new TypeError(
1349
+ `'sequence' must be an array of strings, got: ${JSON.stringify(sequence)}`,
1350
+ );
1351
+ const key = sequence.join('+');
1352
+ this.#inputSequences.delete(key);
1353
+ }
1354
+
1355
+ /**
1356
+ * Removes all registered input sequences.
1357
+ */
1358
+ unregisterAllInputSequences() {
1359
+ this.#inputSequences.clear();
1360
+ }
1361
+
1362
+ /**
1363
+ * Checks whether a given input sequence is currently registered.
1364
+ * @param {string[]} sequence - The sequence to check
1365
+ * @returns {boolean}
1366
+ */
1367
+ hasInputSequence(sequence) {
1368
+ if (!Array.isArray(sequence) || !sequence.every((s) => typeof s === 'string'))
1369
+ throw new TypeError(
1370
+ `'sequence' must be an array of strings, got: ${JSON.stringify(sequence)}`,
1371
+ );
1372
+ const key = sequence.join('+');
1373
+ return this.#inputSequences.has(key);
1374
+ }
1375
+
1376
+ ////////////////////////////////////////////////////////
1377
+
1378
+ /**
1379
+ * Registers a sequence of logical inputs that triggers a specific callback.
1380
+ * @param {string[]} sequence - Ordered list of logical input names (e.g., ['Jump', 'Action'])
1381
+ * @param {InputSequenceCallback} callback - Function to invoke when the sequence is fully held
1382
+ */
1383
+ registerKeySequence(sequence, callback) {
1384
+ if (!Array.isArray(sequence) || !sequence.every((s) => typeof s === 'string'))
1385
+ throw new TypeError(
1386
+ `'sequence' must be an array of strings, got: ${JSON.stringify(sequence)}`,
1387
+ );
1388
+ if (typeof callback !== 'function')
1389
+ throw new TypeError(`'callback' must be a function, got: ${typeof callback}`);
1390
+ if (sequence.length === 0) throw new Error(`'sequence' must contain at least one input name.`);
1391
+ const key = sequence.join('+');
1392
+ this.#keySequences.set(key, { sequence, callback, triggered: false });
1393
+ }
1394
+
1395
+ /**
1396
+ * Unregisters a previously registered input sequence.
1397
+ * @param {string[]} sequence - The sequence to remove from detection
1398
+ */
1399
+ unregisterKeySequence(sequence) {
1400
+ if (!Array.isArray(sequence) || !sequence.every((s) => typeof s === 'string'))
1401
+ throw new TypeError(
1402
+ `'sequence' must be an array of strings, got: ${JSON.stringify(sequence)}`,
1403
+ );
1404
+ const key = sequence.join('+');
1405
+ this.#keySequences.delete(key);
1406
+ }
1407
+
1408
+ /**
1409
+ * Removes all registered input sequences.
1410
+ */
1411
+ unregisterAllKeySequences() {
1412
+ this.#keySequences.clear();
1413
+ }
1414
+
1415
+ /**
1416
+ * Checks whether a given input sequence is currently registered.
1417
+ * @param {string[]} sequence - The sequence to check
1418
+ * @returns {boolean}
1419
+ */
1420
+ hasKeySequence(sequence) {
1421
+ if (!Array.isArray(sequence) || !sequence.every((s) => typeof s === 'string'))
1422
+ throw new TypeError(
1423
+ `'sequence' must be an array of strings, got: ${JSON.stringify(sequence)}`,
1424
+ );
1425
+ const key = sequence.join('+');
1426
+ return this.#keySequences.has(key);
1427
+ }
1428
+
1429
+ ////////////////////////////////////////////////////////
1430
+
1431
+ /**
1432
+ * Renew the currently held combo logical keys.
1433
+ */
1434
+ renewComboMapped() {
1435
+ if (this.#intervalComboKeys) {
1436
+ clearTimeout(this.#intervalComboKeys);
1437
+ this.#intervalComboKeys = setTimeout(() => this.resetComboMapped(), this.#timeoutComboKeys);
1438
+ }
1439
+ }
1440
+
1441
+ /**
1442
+ * Resets the currently held key combo logical inputs.
1443
+ */
1444
+ resetComboMapped() {
1445
+ if (this.#intervalComboKeys) clearTimeout(this.#intervalComboKeys);
1446
+ this.#comboKeys = [];
1447
+ this.#intervalComboKeys = null;
1448
+ this.#timeComboKeys = 0;
1449
+ this.#comboInputs = [];
1450
+ this.#timeComboInputs = 0;
1451
+ }
1452
+
1453
+ /////////////////////////////////////////////////////////////////
1454
+
1455
+ /**
1456
+ * Registers a callback for when a mapped key is activated (pressed down)
1457
+ * @param {MappedInputCallback} callback
1458
+ */
1459
+ onMappedKeyStart(callback) {
1460
+ return this.#onTemplate('', callback, 'mapped-key-start');
1461
+ }
1462
+
1463
+ /**
1464
+ * Registers a one-time callback for the "mapped-key-start" event.
1465
+ * The callback will be automatically removed after it runs once.
1466
+ * @param {MappedInputCallback} callback
1467
+ */
1468
+ onceMappedKeyStart(callback) {
1469
+ return this.#onceTemplate('', callback, 'mapped-key-start');
1470
+ }
1471
+
1472
+ /**
1473
+ * Prepends a callback to the "mapped-key-start" event.
1474
+ * @param {MappedInputCallback} callback
1475
+ */
1476
+ prependMappedKeyStart(callback) {
1477
+ return this.#prependTemplate('', callback, 'mapped-key-start');
1478
+ }
1479
+
1480
+ /**
1481
+ * Removes a callback from the "mapped-key-start" event.
1482
+ * @param {MappedInputCallback} callback
1483
+ */
1484
+ offMappedKeyStart(callback) {
1485
+ return this.#offTemplate('', callback, 'mapped-key-start');
1486
+ }
1487
+
1488
+ /**
1489
+ * Removes all callbacks from the "mapped-key-start" event.
1490
+ */
1491
+ offAllMappedKeyStart() {
1492
+ this.#callbacks.delete('mapped-key-start');
1493
+ }
1494
+
1495
+ //////////////////////////////////////////////////////////////////
1496
+
1497
+ /**
1498
+ * Registers a callback for when a mapped key is deactivated (released)
1499
+ * @param {MappedInputCallback} callback
1500
+ */
1501
+ onMappedKeyEnd(callback) {
1502
+ return this.#onTemplate('', callback, 'mapped-key-end');
1503
+ }
1504
+
1505
+ /**
1506
+ * Registers a one-time callback for the "mapped-key-end" event.
1507
+ * The callback will be automatically removed after it runs once.
1508
+ * @param {MappedInputCallback} callback
1509
+ */
1510
+ onceMappedKeyEnd(callback) {
1511
+ return this.#onceTemplate('', callback, 'mapped-key-end');
1512
+ }
1513
+
1514
+ /**
1515
+ * Prepends a callback to the "mapped-key-end" event.
1516
+ * @param {MappedInputCallback} callback
1517
+ */
1518
+ prependMappedKeyEnd(callback) {
1519
+ return this.#prependTemplate('', callback, 'mapped-key-end');
1520
+ }
1521
+
1522
+ /**
1523
+ * Removes a callback from the "mapped-key-end" event.
1524
+ * @param {MappedInputCallback} callback
1525
+ */
1526
+ offMappedKeyEnd(callback) {
1527
+ return this.#offTemplate('', callback, 'mapped-key-end');
1528
+ }
1529
+
1530
+ /**
1531
+ * Removes all callbacks from the "mapped-key-end" event.
1532
+ */
1533
+ offAllMappedKeyEnd() {
1534
+ this.#callbacks.delete('mapped-key-end');
1535
+ }
1536
+
1537
+ /////////////////////////////////////////////////////////////////
1538
+
1539
+ /**
1540
+ * Registers a callback for when a mapped input is activated (pressed down)
1541
+ * @param {MappedInputCallback} callback
1542
+ */
1543
+ onMappedInputStart(callback) {
1544
+ return this.#onTemplate('', callback, 'mapped-input-start');
1545
+ }
1546
+
1547
+ /**
1548
+ * Registers a one-time callback for the "mapped-input-start" event.
1549
+ * The callback will be automatically removed after it runs once.
1550
+ * @param {MappedInputCallback} callback
1551
+ */
1552
+ onceMappedInputStart(callback) {
1553
+ return this.#onceTemplate('', callback, 'mapped-input-start');
1554
+ }
1555
+
1556
+ /**
1557
+ * Prepends a callback to the "mapped-input-start" event.
1558
+ * @param {MappedInputCallback} callback
1559
+ */
1560
+ prependMappedInputStart(callback) {
1561
+ return this.#prependTemplate('', callback, 'mapped-input-start');
1562
+ }
1563
+
1564
+ /**
1565
+ * Removes a callback from the "mapped-input-start" event.
1566
+ * @param {MappedInputCallback} callback
1567
+ */
1568
+ offMappedInputStart(callback) {
1569
+ return this.#offTemplate('', callback, 'mapped-input-start');
1570
+ }
1571
+
1572
+ /**
1573
+ * Removes all callbacks from the "mapped-input-start" event.
1574
+ */
1575
+ offAllMappedInputStart() {
1576
+ this.#callbacks.delete('mapped-input-start');
1577
+ }
1578
+
1579
+ //////////////////////////////////////////////////////////////////
1580
+
1581
+ /**
1582
+ * Registers a callback for when a mapped input is deactivated (released)
1583
+ * @param {MappedInputCallback} callback
1584
+ */
1585
+ onMappedInputEnd(callback) {
1586
+ return this.#onTemplate('', callback, 'mapped-input-end');
1587
+ }
1588
+
1589
+ /**
1590
+ * Registers a one-time callback for the "mapped-input-end" event.
1591
+ * The callback will be automatically removed after it runs once.
1592
+ * @param {MappedInputCallback} callback
1593
+ */
1594
+ onceMappedInputEnd(callback) {
1595
+ return this.#onceTemplate('', callback, 'mapped-input-end');
1596
+ }
1597
+
1598
+ /**
1599
+ * Prepends a callback to the "mapped-input-end" event.
1600
+ * @param {MappedInputCallback} callback
1601
+ */
1602
+ prependMappedInputEnd(callback) {
1603
+ return this.#prependTemplate('', callback, 'mapped-input-end');
1604
+ }
1605
+
1606
+ /**
1607
+ * Removes a callback from the "mapped-input-end" event.
1608
+ * @param {MappedInputCallback} callback
1609
+ */
1610
+ offMappedInputEnd(callback) {
1611
+ return this.#offTemplate('', callback, 'mapped-input-end');
1612
+ }
1613
+
1614
+ /**
1615
+ * Removes all callbacks from the "mapped-input-end" event.
1616
+ */
1617
+ offAllMappedInputEnd() {
1618
+ this.#callbacks.delete('mapped-input-end');
1619
+ }
1620
+
1621
+ /////////////////////////////////////////////////////////////
1622
+
1623
+ /**
1624
+ * Registers a callback for a logical input
1625
+ * @param {string} logicalName
1626
+ * @param {PayloadCallback} callback
1627
+ */
1628
+ onInput(logicalName, callback) {
1629
+ return this.#onTemplate(logicalName, callback, 'input-{logicalName}');
1630
+ }
1631
+
1632
+ /**
1633
+ * Registers a one-time callback for a logical input.
1634
+ * The callback is removed after the first invocation.
1635
+ * @param {string} logicalName
1636
+ * @param {PayloadCallback} callback
1637
+ */
1638
+ onceInput(logicalName, callback) {
1639
+ return this.#onceTemplate(logicalName, callback, 'input-{logicalName}');
1640
+ }
1641
+
1642
+ /**
1643
+ * Prepends a callback to the input event list.
1644
+ * @param {string} logicalName
1645
+ * @param {PayloadCallback} callback
1646
+ */
1647
+ prependInput(logicalName, callback) {
1648
+ return this.#prependTemplate(logicalName, callback, 'input-{logicalName}');
1649
+ }
1650
+
1651
+ /**
1652
+ * Removes a callback from a specific logical input event.
1653
+ * @param {string} logicalName
1654
+ * @param {PayloadCallback} callback
1655
+ */
1656
+ offInput(logicalName, callback) {
1657
+ return this.#offTemplate(logicalName, callback, 'input-{logicalName}');
1658
+ }
1659
+
1660
+ /////////////////////////////////////////////////////////
1661
+
1662
+ /**
1663
+ * Registers a callback for the "input-start" event of a logical name
1664
+ * @param {string} logicalName
1665
+ * @param {PayloadCallback} callback
1666
+ */
1667
+ onInputStart(logicalName, callback) {
1668
+ return this.#onTemplate(logicalName, callback, 'input-down-{logicalName}');
1669
+ }
1670
+
1671
+ /**
1672
+ * Registers a one-time callback for the "input-start" event.
1673
+ * @param {string} logicalName
1674
+ * @param {PayloadCallback} callback
1675
+ */
1676
+ onceInputStart(logicalName, callback) {
1677
+ return this.#onceTemplate(logicalName, callback, 'input-down-{logicalName}');
1678
+ }
1679
+
1680
+ /**
1681
+ * Prepends a callback to the "input-start" event list.
1682
+ * @param {string} logicalName
1683
+ * @param {PayloadCallback} callback
1684
+ */
1685
+ prependInputStart(logicalName, callback) {
1686
+ return this.#prependTemplate(logicalName, callback, 'input-down-{logicalName}');
1687
+ }
1688
+
1689
+ /**
1690
+ * Removes a callback from a specific logical "input-start" event.
1691
+ * @param {string} logicalName
1692
+ * @param {PayloadCallback} callback
1693
+ */
1694
+ offInputStart(logicalName, callback) {
1695
+ return this.#offTemplate(logicalName, callback, 'input-down-{logicalName}');
1696
+ }
1697
+
1698
+ /////////////////////////////////////////////////////////////////////////
1699
+
1700
+ /**
1701
+ * Registers a callback for the "input-end" event of a logical name
1702
+ * @param {string} logicalName
1703
+ * @param {PayloadCallback} callback
1704
+ */
1705
+ onInputEnd(logicalName, callback) {
1706
+ return this.#onTemplate(logicalName, callback, 'input-up-{logicalName}');
1707
+ }
1708
+
1709
+ /**
1710
+ * Registers a one-time callback for the "input-end" event.
1711
+ * @param {string} logicalName
1712
+ * @param {PayloadCallback} callback
1713
+ */
1714
+ onceInputEnd(logicalName, callback) {
1715
+ return this.#onceTemplate(logicalName, callback, 'input-up-{logicalName}');
1716
+ }
1717
+
1718
+ /**
1719
+ * Prepends a callback to the "input-end" event list.
1720
+ * @param {string} logicalName
1721
+ * @param {PayloadCallback} callback
1722
+ */
1723
+ prependInputEnd(logicalName, callback) {
1724
+ return this.#prependTemplate(logicalName, callback, 'input-up-{logicalName}');
1725
+ }
1726
+
1727
+ /**
1728
+ * Removes a callback from a specific logical "input-end" event.
1729
+ * @param {string} logicalName
1730
+ * @param {PayloadCallback} callback
1731
+ */
1732
+ offInputEnd(logicalName, callback) {
1733
+ return this.#offTemplate(logicalName, callback, 'input-up-{logicalName}');
1734
+ }
1735
+
1736
+ ///////////////////////////////////////////////////////////////////
1737
+
1738
+ /**
1739
+ * Registers a callback for the "input-hold" event of a logical name
1740
+ * @param {string} logicalName
1741
+ * @param {PayloadCallback} callback
1742
+ */
1743
+ onInputHold(logicalName, callback) {
1744
+ return this.#onTemplate(logicalName, callback, 'input-hold-{logicalName}');
1745
+ }
1746
+
1747
+ /**
1748
+ * Registers a one-time callback for the "input-hold" event.
1749
+ * @param {string} logicalName
1750
+ * @param {PayloadCallback} callback
1751
+ */
1752
+ onceInputHold(logicalName, callback) {
1753
+ return this.#onceTemplate(logicalName, callback, 'input-hold-{logicalName}');
1754
+ }
1755
+
1756
+ /**
1757
+ * Prepends a callback to the "input-hold" event list.
1758
+ * @param {string} logicalName
1759
+ * @param {PayloadCallback} callback
1760
+ */
1761
+ prependInputHold(logicalName, callback) {
1762
+ return this.#prependTemplate(logicalName, callback, 'input-hold-{logicalName}');
1763
+ }
1764
+
1765
+ /**
1766
+ * Removes a callback from a specific logical "input-hold" event.
1767
+ * @param {string} logicalName
1768
+ * @param {PayloadCallback} callback
1769
+ */
1770
+ offInputHold(logicalName, callback) {
1771
+ return this.#offTemplate(logicalName, callback, 'input-hold-{logicalName}');
1772
+ }
1773
+
1774
+ ////////////////////////////////////////////////////////////
1775
+
1776
+ /**
1777
+ * Registers a callback for the "input-change" event of a logical name
1778
+ * @param {string} logicalName
1779
+ * @param {PayloadCallback} callback
1780
+ */
1781
+ onInputChange(logicalName, callback) {
1782
+ return this.#onTemplate(logicalName, callback, 'input-change-{logicalName}');
1783
+ }
1784
+
1785
+ /**
1786
+ * Registers a one-time callback for the "input-change" event.
1787
+ * @param {string} logicalName
1788
+ * @param {PayloadCallback} callback
1789
+ */
1790
+ onceInputChange(logicalName, callback) {
1791
+ return this.#onceTemplate(logicalName, callback, 'input-change-{logicalName}');
1792
+ }
1793
+
1794
+ /**
1795
+ * Prepends a callback to the "input-change" event list.
1796
+ * @param {string} logicalName
1797
+ * @param {PayloadCallback} callback
1798
+ */
1799
+ prependInputChange(logicalName, callback) {
1800
+ return this.#prependTemplate(logicalName, callback, 'input-change-{logicalName}');
1801
+ }
1802
+
1803
+ /**
1804
+ * Removes a callback from a specific logical "input-change" event.
1805
+ * @param {string} logicalName
1806
+ * @param {PayloadCallback} callback
1807
+ */
1808
+ offInputChange(logicalName, callback) {
1809
+ return this.#offTemplate(logicalName, callback, 'input-change-{logicalName}');
1810
+ }
1811
+
1812
+ ////////////////////////////////////////////////////////////
1813
+
1814
+ /**
1815
+ * Registers a callback for the "input-move" event of a logical name
1816
+ * @param {string} logicalName
1817
+ * @param {PayloadCallback} callback
1818
+ */
1819
+ onInputMove(logicalName, callback) {
1820
+ return this.#onTemplate(logicalName, callback, 'input-move-{logicalName}');
1821
+ }
1822
+
1823
+ /**
1824
+ * Registers a one-time callback for the "input-move" event.
1825
+ * @param {string} logicalName
1826
+ * @param {PayloadCallback} callback
1827
+ */
1828
+ onceInputMove(logicalName, callback) {
1829
+ return this.#onceTemplate(logicalName, callback, 'input-move-{logicalName}');
1830
+ }
1831
+
1832
+ /**
1833
+ * Prepends a callback to the "input-move" event list.
1834
+ * @param {string} logicalName
1835
+ * @param {PayloadCallback} callback
1836
+ */
1837
+ prependInputMove(logicalName, callback) {
1838
+ return this.#prependTemplate(logicalName, callback, 'input-move-{logicalName}');
1839
+ }
1840
+
1841
+ /**
1842
+ * Removes a callback from a specific logical "input-move" event.
1843
+ * @param {string} logicalName
1844
+ * @param {PayloadCallback} callback
1845
+ */
1846
+ offInputMove(logicalName, callback) {
1847
+ return this.#offTemplate(logicalName, callback, 'input-move-{logicalName}');
1848
+ }
1849
+
1850
+ ////////////////////////////////////////////////////////////
1851
+
1852
+ /**
1853
+ * Returns a shallow clone of the callback list for a given logical input and event type.
1854
+ * @param {string} logicalName
1855
+ * @param {'all' | 'start' | 'end' | 'hold' | 'change' | 'move'} [type='all']
1856
+ * @returns {Function[]}
1857
+ */
1858
+ getCalls(logicalName, type = 'all') {
1859
+ if (typeof logicalName !== 'string' || logicalName.trim() === '')
1860
+ throw new TypeError(`"logicalName" must be a non-empty string, received ${logicalName}`);
1861
+ const validTypes = ['all', 'start', 'end', 'hold', 'change', 'move'];
1862
+ if (!validTypes.includes(type))
1863
+ throw new TypeError(`"type" must be one of ${validTypes.join(', ')}, received ${type}`);
1864
+ const prefix = {
1865
+ all: 'input-',
1866
+ start: 'input-down-',
1867
+ end: 'input-up-',
1868
+ hold: 'input-hold-',
1869
+ change: 'input-change-',
1870
+ move: 'input-move-',
1871
+ }[type];
1872
+ const key = `${prefix}${logicalName}`;
1873
+ const list = this.#callbacks.get(key);
1874
+ return Array.isArray(list) ? [...list] : [];
1875
+ }
1876
+
1877
+ /**
1878
+ * Removes all callbacks for a specific logical input event.
1879
+ * @param {string} logicalName
1880
+ * @param {'all'| 'start' | 'end' | 'hold' | 'change' | 'move'} [type='all']
1881
+ */
1882
+ offAllInputs(logicalName, type = 'all') {
1883
+ if (typeof logicalName !== 'string' || logicalName.trim() === '')
1884
+ throw new TypeError(`"logicalName" must be a non-empty string, received ${logicalName}`);
1885
+ const validTypes = ['all', 'start', 'end', 'hold', 'change', 'move'];
1886
+ if (!validTypes.includes(type))
1887
+ throw new TypeError(`"type" must be one of ${validTypes.join(', ')}, received ${type}`);
1888
+ const prefix = {
1889
+ all: 'input-',
1890
+ start: 'input-down-',
1891
+ end: 'input-up-',
1892
+ hold: 'input-hold-',
1893
+ change: 'input-change-',
1894
+ move: 'input-move-',
1895
+ }[type];
1896
+ if (prefix) this.#callbacks.delete(`${prefix}${logicalName}`);
1897
+ }
1898
+
1899
+ /**
1900
+ * Returns the number of registered callbacks for a specific logical input and type.
1901
+ * @param {string} logicalName
1902
+ * @param {'all' | 'start' | 'end' | 'hold' | 'change' | 'move'} [type='all']
1903
+ * @returns {number}
1904
+ */
1905
+ getCallSize(logicalName, type = 'all') {
1906
+ if (typeof logicalName !== 'string' || logicalName.trim() === '')
1907
+ throw new TypeError(`"logicalName" must be a non-empty string, received ${logicalName}`);
1908
+ const validTypes = ['all', 'start', 'end', 'hold', 'change', 'move'];
1909
+ if (!validTypes.includes(type))
1910
+ throw new TypeError(`"type" must be one of ${validTypes.join(', ')}, received ${type}`);
1911
+ const prefix = {
1912
+ all: 'input-',
1913
+ start: 'input-down-',
1914
+ end: 'input-up-',
1915
+ hold: 'input-hold-',
1916
+ change: 'input-change-',
1917
+ move: 'input-move-',
1918
+ }[type];
1919
+ const list = this.#callbacks.get(`${prefix}${logicalName}`);
1920
+ return Array.isArray(list) ? list.length : 0;
1921
+ }
1922
+
1923
+ ////////////////////////////////////////////////
1924
+
1925
+ /**
1926
+ * Default haptic effect settings
1927
+ */
1928
+ #defaultHapticEffect = {
1929
+ /** @type {GamepadHapticEffectType} */
1930
+ type: 'dual-rumble',
1931
+ /** @type {GamepadEffectParameters} */
1932
+ params: {
1933
+ startDelay: 0,
1934
+ duration: 200,
1935
+ weakMagnitude: 0.5,
1936
+ strongMagnitude: 1.0,
1937
+ },
1938
+ };
1939
+
1940
+ /**
1941
+ * Returns the current default haptic feedback effect configuration.
1942
+ *
1943
+ * This includes the effect type (e.g., 'dual-rumble') and its parameters
1944
+ * such as duration, delay, and vibration intensities (weak and strong).
1945
+ *
1946
+ * The returned object is a shallow clone to prevent external mutation.
1947
+ *
1948
+ * @returns {{ type: GamepadHapticEffectType; params: GamepadEffectParameters; }} The default haptic effect settings.
1949
+ */
1950
+ get defaultHapticEffect() {
1951
+ return {
1952
+ type: this.#defaultHapticEffect.type,
1953
+ params: { ...this.#defaultHapticEffect.params },
1954
+ };
1955
+ }
1956
+
1957
+ /**
1958
+ * Sets the default haptic feedback effect configuration.
1959
+ * @param {GamepadHapticEffectType} type - Type of effect.
1960
+ * @param {GamepadEffectParameters} params - Effect parameters.
1961
+ */
1962
+ setDefaultHapticEffect(type, params) {
1963
+ if (typeof type !== 'string')
1964
+ throw new TypeError(
1965
+ `"type" must be a valid GamepadHapticEffectType string, received ${type}`,
1966
+ );
1967
+ if (typeof params !== 'object' || params === null)
1968
+ throw new TypeError(`"params" must be a non-null object, received ${params}`);
1969
+ this.#defaultHapticEffect.type = type;
1970
+ this.#defaultHapticEffect.params = params;
1971
+ }
1972
+
1973
+ /**
1974
+ * Checks if the connected gamepad supports haptic feedback.
1975
+ * @returns {boolean}
1976
+ */
1977
+ hasHapticEffect() {
1978
+ const gp = this.#connectedGamepad;
1979
+ if (!gp) return false;
1980
+ const vibrationActuator = gp.vibrationActuator;
1981
+ if (!(vibrationActuator instanceof GamepadHapticActuator)) return false;
1982
+ return true;
1983
+ }
1984
+
1985
+ /**
1986
+ * Triggers a haptic feedback vibration using provided or default settings.
1987
+ * @param {GamepadEffectParameters} [params] - Custom parameters.
1988
+ * @param {GamepadHapticEffectType} [type] - Custom type.
1989
+ * @returns {Promise<GamepadHapticsResult>}
1990
+ */
1991
+ vibrate(params, type) {
1992
+ if (params !== undefined && (typeof params !== 'object' || params === null))
1993
+ throw new TypeError(`"params" must be an object if provided, received ${params}`);
1994
+ if (type !== undefined && typeof type !== 'string')
1995
+ throw new TypeError(
1996
+ `"type" must be a valid GamepadHapticEffectType string if provided, received ${type}`,
1997
+ );
1998
+ const gp = this.#connectedGamepad;
1999
+ if (!gp) return new Promise((resolve) => resolve('complete'));
2000
+ const vibrationActuator = gp.vibrationActuator;
2001
+ if (!(vibrationActuator instanceof GamepadHapticActuator))
2002
+ return new Promise((resolve) => resolve('complete'));
2003
+ return vibrationActuator.playEffect(type ?? this.#defaultHapticEffect.type, {
2004
+ ...this.#defaultHapticEffect.params,
2005
+ ...params,
2006
+ });
2007
+ }
2008
+
2009
+ ////////////////////////////////////////////
2010
+
2011
+ /**
2012
+ * Adds an ID to the ignored list
2013
+ * @param {string} id
2014
+ */
2015
+ ignoreId(id) {
2016
+ if (typeof id !== 'string' || id.trim() === '')
2017
+ throw new TypeError(`"id" must be a non-empty string, received ${id}`);
2018
+ this.#ignoreIds.add(id);
2019
+ }
2020
+
2021
+ /**
2022
+ * Removes an ID from the ignored list
2023
+ * @param {string} id
2024
+ */
2025
+ unignoreId(id) {
2026
+ if (typeof id !== 'string' || id.trim() === '')
2027
+ throw new TypeError(`"id" must be a non-empty string, received ${id}`);
2028
+ this.#ignoreIds.delete(id);
2029
+ }
2030
+
2031
+ /**
2032
+ * Registers a callback for the "connected" event
2033
+ * @param {ConnectionCallback} callback
2034
+ */
2035
+ onConnected(callback) {
2036
+ return this.#onTemplate('', callback, 'connected');
2037
+ }
2038
+
2039
+ /**
2040
+ * Registers a one-time callback for the "connected" event.
2041
+ * The callback will be automatically removed after it runs once.
2042
+ * @param {ConnectionCallback} callback
2043
+ */
2044
+ onceConnected(callback) {
2045
+ return this.#onceTemplate('', callback, 'connected');
2046
+ }
2047
+
2048
+ /**
2049
+ * Prepends a callback to the "connected" event.
2050
+ * @param {ConnectionCallback} callback
2051
+ */
2052
+ prependConnected(callback) {
2053
+ return this.#prependTemplate('', callback, 'connected');
2054
+ }
2055
+
2056
+ /**
2057
+ * Removes a callback from the "connected" event.
2058
+ * @param {ConnectionCallback} callback
2059
+ */
2060
+ offConnected(callback) {
2061
+ return this.#offTemplate('', callback, 'connected');
2062
+ }
2063
+
2064
+ /**
2065
+ * Removes all callbacks from the "connected" event.
2066
+ */
2067
+ offAllConnected() {
2068
+ this.#callbacks.delete('connected');
2069
+ }
2070
+
2071
+ /**
2072
+ * Registers a callback for the "disconnected" event
2073
+ * @param {ConnectionCallback} callback
2074
+ */
2075
+ onDisconnected(callback) {
2076
+ return this.#onTemplate('', callback, 'disconnected');
2077
+ }
2078
+
2079
+ /**
2080
+ * Registers a one-time callback for the "disconnected" event.
2081
+ * The callback will be automatically removed after it runs once.
2082
+ * @param {ConnectionCallback} callback
2083
+ */
2084
+ onceDisconnected(callback) {
2085
+ return this.#onceTemplate('', callback, 'disconnected');
2086
+ }
2087
+
2088
+ /**
2089
+ * Prepends a callback to the "disconnected" event.
2090
+ * @param {ConnectionCallback} callback
2091
+ */
2092
+ prependDisconnected(callback) {
2093
+ return this.#prependTemplate('', callback, 'disconnected');
2094
+ }
2095
+
2096
+ /**
2097
+ * Removes a callback from the "disconnected" event.
2098
+ * @param {ConnectionCallback} callback
2099
+ */
2100
+ offDisconnected(callback) {
2101
+ return this.#offTemplate('', callback, 'disconnected');
2102
+ }
2103
+
2104
+ /**
2105
+ * Removes all callbacks from the "disconnected" event.
2106
+ */
2107
+ offAllDisconnected() {
2108
+ this.#callbacks.delete('disconnected');
2109
+ }
2110
+
2111
+ //////////////////////////////////////////
2112
+
2113
+ /**
2114
+ * Returns whether a gamepad is currently connected.
2115
+ * @returns {boolean}
2116
+ */
2117
+ hasGamepad() {
2118
+ return this.#connectedGamepad instanceof Gamepad;
2119
+ }
2120
+
2121
+ /**
2122
+ * Returns the currently connected Gamepad instance.
2123
+ * Throws if no gamepad is connected.
2124
+ * @returns {Gamepad}
2125
+ */
2126
+ getGamepad() {
2127
+ if (!this.#connectedGamepad) throw new Error('No gamepad is currently connected.');
2128
+ return this.#connectedGamepad;
2129
+ }
2130
+
2131
+ /**
2132
+ * Checks if there is a recorded last state for a given button index.
2133
+ * @param {number} index - The index of the button to check.
2134
+ * @returns {boolean} True if a last state exists for the specified button index; otherwise, false.
2135
+ */
2136
+ hasLastButtonState(index) {
2137
+ if (!this.#lastButtonStates[index]) return false;
2138
+ return true;
2139
+ }
2140
+
2141
+ /**
2142
+ * Retrieves the last recorded state for a given button index.
2143
+ * Throws an error if the button index has no recorded state.
2144
+ * @param {number} index - The index of the button to retrieve.
2145
+ * @returns {KeyStatus} A copy of the last known state for the specified button index.
2146
+ * @throws {Error} If no last state exists for the specified button index.
2147
+ */
2148
+ getLastButtonState(index) {
2149
+ if (!this.#lastButtonStates[index])
2150
+ throw new Error(`No last button state found for index ${index}`);
2151
+ return { ...this.#lastButtonStates[index] };
2152
+ }
2153
+
2154
+ //////////////////////////////////////////
2155
+
2156
+ /**
2157
+ * Exports the current TinyGamepad configuration as a plain object suitable for serialization.
2158
+ * Includes device filters, input mappings, and various sensitivity settings.
2159
+ * @returns {ExportedConfig} The current configuration snapshot.
2160
+ */
2161
+ exportConfig() {
2162
+ return {
2163
+ expectedId: this.#expectedId,
2164
+ ignoreIds: Array.from(this.#ignoreIds),
2165
+ deadZone: this.#deadZone,
2166
+ timeoutComboKeys: this.#timeoutComboKeys,
2167
+ axisActiveSensitivity: this.#axisActiveSensitivity,
2168
+ inputMap: Array.from(this.#inputMap.entries()),
2169
+ };
2170
+ }
2171
+
2172
+ /**
2173
+ * Imports and applies a configuration object or JSON string to update TinyGamepad settings.
2174
+ * Validates the input to ensure proper types and throws if invalid.
2175
+ * @param {string | ExportedConfig} json - JSON string or configuration object to apply.
2176
+ * @throws {TypeError} Throws if the provided argument is not a valid JSON string or configuration object,
2177
+ * or if any property has an incorrect type.
2178
+ */
2179
+ importConfig(json) {
2180
+ if (typeof json !== 'string' && (typeof json !== 'object' || json === null))
2181
+ throw new TypeError(`"json" must be a string or a non-null object, received: ${json}`);
2182
+
2183
+ const config = typeof json === 'string' ? JSON.parse(json) : json;
2184
+
2185
+ if (
2186
+ config.expectedId !== undefined &&
2187
+ typeof config.expectedId !== 'string' &&
2188
+ config.expectedId !== null
2189
+ )
2190
+ throw new TypeError(`"expectedId" must be a string or null if provided`);
2191
+
2192
+ if (config.ignoreIds !== undefined && !Array.isArray(config.ignoreIds))
2193
+ throw new TypeError(`"ignoreIds" must be an array if provided`);
2194
+
2195
+ if (config.deadZone !== undefined && typeof config.deadZone !== 'number')
2196
+ throw new TypeError(`"deadZone" must be a number if provided`);
2197
+
2198
+ if (config.timeoutComboKeys !== undefined && typeof config.timeoutComboKeys !== 'number')
2199
+ throw new TypeError(`"timeoutComboKeys" must be a number if provided`);
2200
+
2201
+ if (
2202
+ config.axisActiveSensitivity !== undefined &&
2203
+ typeof config.axisActiveSensitivity !== 'number'
2204
+ )
2205
+ throw new TypeError(`"axisActiveSensitivity" must be a number if provided`);
2206
+
2207
+ if (config.inputMap !== undefined && !Array.isArray(config.inputMap))
2208
+ throw new TypeError(`"inputMap" must be an array if provided`);
2209
+
2210
+ if (config.expectedId !== undefined) this.#expectedId = config.expectedId;
2211
+ if (Array.isArray(config.ignoreIds)) this.#ignoreIds = new Set(config.ignoreIds);
2212
+ if (typeof config.deadZone === 'number') this.#deadZone = config.deadZone;
2213
+ if (typeof config.timeoutComboKeys === 'number')
2214
+ this.#timeoutComboKeys = config.timeoutComboKeys;
2215
+ if (typeof config.axisActiveSensitivity === 'number')
2216
+ this.#axisActiveSensitivity = config.axisActiveSensitivity;
2217
+ if (Array.isArray(config.inputMap)) this.#inputMap = new Map(config.inputMap);
2218
+ }
2219
+
2220
+ ///////////////////////////////////////////////////////////////
2221
+
2222
+ /**
2223
+ * Returns a cloned list of the "mapped-key-start" event callbacks.
2224
+ * @returns {MappedInputCallback[]}
2225
+ */
2226
+ get mappedKeyStartCalls() {
2227
+ /** @type {MappedInputCallback[]} */
2228
+ // @ts-ignore
2229
+ const list = this.#callbacks.get('mapped-key-start');
2230
+ return Array.isArray(list) ? [...list] : [];
2231
+ }
2232
+
2233
+ /**
2234
+ * Returns a cloned list of the "mapped-key-end" event callbacks.
2235
+ * @returns {MappedInputCallback[]}
2236
+ */
2237
+ get mappedKeyEndCalls() {
2238
+ /** @type {MappedInputCallback[]} */
2239
+ // @ts-ignore
2240
+ const list = this.#callbacks.get('mapped-key-end');
2241
+ return Array.isArray(list) ? [...list] : [];
2242
+ }
2243
+
2244
+ ///////////////////////////////////////////////////////////////
2245
+
2246
+ /**
2247
+ * Returns a clone of currently held key combo logical inputs.
2248
+ * @returns {string[]}
2249
+ */
2250
+ get comboMappedKeys() {
2251
+ return [...this.#comboKeys];
2252
+ }
2253
+
2254
+ /**
2255
+ * Returns a clone of currently held combo logical inputs.
2256
+ * @returns {string[]}
2257
+ */
2258
+ get comboMappedInputs() {
2259
+ return [...this.#comboInputs];
2260
+ }
2261
+
2262
+ ////////////////////////////////////
2263
+
2264
+ /**
2265
+ * Returns the number of input sequences currently registered.
2266
+ * @returns {number}
2267
+ */
2268
+ get keySequenceSize() {
2269
+ return this.#keySequences.size;
2270
+ }
2271
+
2272
+ /**
2273
+ * Returns a shallow clone of all input sequences and their associated data.
2274
+ * @returns {InputSequenceCallback[]}
2275
+ */
2276
+ get keySequences() {
2277
+ const result = [];
2278
+ for (const [, data] of this.#keySequences.entries()) result.push(data.callback);
2279
+ return result;
2280
+ }
2281
+
2282
+ /**
2283
+ * Returns a clone of currently held mapped logical keys.
2284
+ * @returns {string[]}
2285
+ */
2286
+ get activeMappedKeys() {
2287
+ return [...this.#activeMappedKeys];
2288
+ }
2289
+
2290
+ ////////////////////////////////////
2291
+
2292
+ /**
2293
+ * Returns a cloned list of the "mapped-input-start" event callbacks.
2294
+ * @returns {MappedInputCallback[]}
2295
+ */
2296
+ get mappedInputStartCalls() {
2297
+ /** @type {MappedInputCallback[]} */
2298
+ // @ts-ignore
2299
+ const list = this.#callbacks.get('mapped-input-start');
2300
+ return Array.isArray(list) ? [...list] : [];
2301
+ }
2302
+
2303
+ /**
2304
+ * Returns a cloned list of the "mapped-input-end" event callbacks.
2305
+ * @returns {MappedInputCallback[]}
2306
+ */
2307
+ get mappedInputEndCalls() {
2308
+ /** @type {MappedInputCallback[]} */
2309
+ // @ts-ignore
2310
+ const list = this.#callbacks.get('mapped-input-end');
2311
+ return Array.isArray(list) ? [...list] : [];
2312
+ }
2313
+
2314
+ ////////////////////////////////////
2315
+
2316
+ /**
2317
+ * Returns the number of input sequences currently registered.
2318
+ * @returns {number}
2319
+ */
2320
+ get inputSequenceSize() {
2321
+ return this.#inputSequences.size;
2322
+ }
2323
+
2324
+ /**
2325
+ * Returns a shallow clone of all input sequences and their associated data.
2326
+ * @returns {InputSequenceCallback[]}
2327
+ */
2328
+ get inputSequences() {
2329
+ const result = [];
2330
+ for (const [, data] of this.#inputSequences.entries()) result.push(data.callback);
2331
+ return result;
2332
+ }
2333
+
2334
+ /**
2335
+ * Returns a clone of currently held mapped logical inputs.
2336
+ * @returns {string[]}
2337
+ */
2338
+ get activeMappedInputs() {
2339
+ return [...this.#activeMappedInputs];
2340
+ }
2341
+
2342
+ ////////////////////////////////////
2343
+
2344
+ /**
2345
+ * Returns a shallow clone of all logical-to-physical input mappings as a plain object.
2346
+ * @returns {{ [logicalName: string]: string | string[] }}
2347
+ */
2348
+ get mappedInputs() {
2349
+ /** @type {{ [logicalName: string]: string | string[] }} */
2350
+ const result = {};
2351
+ for (const [logicalName, physicalInput] of this.#inputMap.entries()) {
2352
+ result[logicalName] = physicalInput;
2353
+ }
2354
+ return result;
2355
+ }
2356
+
2357
+ /**
2358
+ * Returns the number of logical inputs currently mapped.
2359
+ * @returns {number}
2360
+ */
2361
+ get mappedInputSize() {
2362
+ return this.#inputMap.size;
2363
+ }
2364
+
2365
+ ////////////////////////////////////
2366
+
2367
+ /**
2368
+ * Returns a cloned list of the "connected" event callbacks.
2369
+ * @returns {ConnectionCallback[]}
2370
+ */
2371
+ get connectedCalls() {
2372
+ /** @type {ConnectionCallback[]} */
2373
+ // @ts-ignore
2374
+ const list = this.#callbacks.get('connected');
2375
+ return Array.isArray(list) ? [...list] : [];
2376
+ }
2377
+
2378
+ /**
2379
+ * Returns a cloned list of the "disconnected" event callbacks.
2380
+ * @returns {ConnectionCallback[]}
2381
+ */
2382
+ get disconnectedCalls() {
2383
+ /** @type {ConnectionCallback[]} */
2384
+ // @ts-ignore
2385
+ const list = this.#callbacks.get('disconnected');
2386
+ return Array.isArray(list) ? [...list] : [];
2387
+ }
2388
+
2389
+ ////////////////////////////////////
2390
+
2391
+ /**
2392
+ * Returns a shallow clone of the set of ignored device IDs.
2393
+ * @returns {string[]}
2394
+ */
2395
+ get ignoredDeviceIds() {
2396
+ return [...this.#ignoreIds];
2397
+ }
2398
+
2399
+ /**
2400
+ * Returns a shallow clone of the currently held keys.
2401
+ * @returns {string[]}
2402
+ */
2403
+ get heldKeys() {
2404
+ return [...this.#heldKeys];
2405
+ }
2406
+
2407
+ ////////////////////////////////////
2408
+
2409
+ /**
2410
+ * Returns the total number of sub event keys inside all events currently registered.
2411
+ * @returns {number}
2412
+ */
2413
+ get eventsSize() {
2414
+ let total = 0;
2415
+ this.#callbacks.forEach((values) => {
2416
+ total += values.length;
2417
+ });
2418
+ return total;
2419
+ }
2420
+
2421
+ /**
2422
+ * Returns the total number of unique event keys currently registered.
2423
+ * @returns {number}
2424
+ */
2425
+ get callSize() {
2426
+ return this.#callbacks.size;
2427
+ }
2428
+
2429
+ /**
2430
+ * Returns the number of callbacks registered for the "connected" event.
2431
+ * @returns {number}
2432
+ */
2433
+ get connectedCallSize() {
2434
+ const list = this.#callbacks.get('connected');
2435
+ return Array.isArray(list) ? list.length : 0;
2436
+ }
2437
+
2438
+ /**
2439
+ * Returns the number of callbacks registered for the "disconnected" event.
2440
+ * @returns {number}
2441
+ */
2442
+ get disconnectedCallSize() {
2443
+ const list = this.#callbacks.get('disconnected');
2444
+ return Array.isArray(list) ? list.length : 0;
2445
+ }
2446
+
2447
+ /**
2448
+ * Returns the number of callbacks registered for the "mapped-key-start" event.
2449
+ * @returns {number}
2450
+ */
2451
+ get mappedKeyStartCallSize() {
2452
+ const list = this.#callbacks.get('mapped-key-start');
2453
+ return Array.isArray(list) ? list.length : 0;
2454
+ }
2455
+
2456
+ /**
2457
+ * Returns the number of callbacks registered for the "mapped-key-end" event.
2458
+ * @returns {number}
2459
+ */
2460
+ get mappedKeyEndCallSize() {
2461
+ const list = this.#callbacks.get('mapped-key-end');
2462
+ return Array.isArray(list) ? list.length : 0;
2463
+ }
2464
+
2465
+ /**
2466
+ * Returns the number of callbacks registered for the "mapped-input-start" event.
2467
+ * @returns {number}
2468
+ */
2469
+ get mappedInputStartCallSize() {
2470
+ const list = this.#callbacks.get('mapped-input-start');
2471
+ return Array.isArray(list) ? list.length : 0;
2472
+ }
2473
+
2474
+ /**
2475
+ * Returns the number of callbacks registered for the "mapped-input-end" event.
2476
+ * @returns {number}
2477
+ */
2478
+ get mappedInputEndCallSize() {
2479
+ const list = this.#callbacks.get('mapped-input-end');
2480
+ return Array.isArray(list) ? list.length : 0;
2481
+ }
2482
+
2483
+ //////////////////////////////////////
2484
+
2485
+ /**
2486
+ * Retrieves a snapshot of the most recent button states.
2487
+ * Each element represents the status of a specific button at the last update cycle.
2488
+ * @returns {KeyStatus[]} An array of button states from the last poll.
2489
+ */
2490
+ get lastButtonStates() {
2491
+ /** @type {KeyStatus[]} */
2492
+ const results = [];
2493
+ this.#lastButtonStates.forEach((value) => {
2494
+ results.push({ ...value });
2495
+ });
2496
+ return results;
2497
+ }
2498
+
2499
+ /**
2500
+ * Retrieves a snapshot of the most recent axis values.
2501
+ * Each element is a numeric value representing the position or tilt of a specific axis at the last update cycle.
2502
+ * @returns {number[]} An array of axis values from the last poll.
2503
+ */
2504
+ get lastAxes() {
2505
+ return [...this.#lastAxes];
2506
+ }
2507
+
2508
+ //////////////////////////////////////
2509
+
2510
+ /**
2511
+ * Returns the current input mode (e.g., 'keyboard-only', 'gamepad-only', or 'both').
2512
+ * @returns {InputMode}
2513
+ */
2514
+ get inputMode() {
2515
+ return this.#inputMode;
2516
+ }
2517
+
2518
+ /**
2519
+ * Returns the base element or window used for event binding.
2520
+ * @returns {Window|Element}
2521
+ */
2522
+ get elementBase() {
2523
+ return this.#elementBase;
2524
+ }
2525
+
2526
+ /**
2527
+ * Returns the timestamp of the last mapped input combo.
2528
+ * @returns {number}
2529
+ */
2530
+ get timeComboInputs() {
2531
+ return this.#timeComboInputs;
2532
+ }
2533
+
2534
+ /**
2535
+ * Returns the timestamp of the last raw key combo.
2536
+ * @returns {number}
2537
+ */
2538
+ get timeComboKeys() {
2539
+ return this.#timeComboKeys;
2540
+ }
2541
+
2542
+ /**
2543
+ * Returns the timestamp of the last mapped input activity.
2544
+ * @returns {number}
2545
+ */
2546
+ get timeMappedInputs() {
2547
+ return this.#timeMappedInputs;
2548
+ }
2549
+
2550
+ /**
2551
+ * Returns the timeout duration (in milliseconds) before raw key combos are reset.
2552
+ * @returns {number}
2553
+ */
2554
+ get timeoutComboKeys() {
2555
+ return this.#timeoutComboKeys;
2556
+ }
2557
+
2558
+ /**
2559
+ * Sets the timeout duration (in milliseconds) before raw key combos are reset.
2560
+ * Must be a positive number.
2561
+ * @param {number} value
2562
+ */
2563
+ set timeoutComboKeys(value) {
2564
+ if (typeof value !== 'number' || value < 0 || !Number.isFinite(value))
2565
+ throw new TypeError('Timeout combo keys must be a non-negative finite number.');
2566
+ this.#timeoutComboKeys = value;
2567
+ }
2568
+
2569
+ /**
2570
+ * Gets the sensitivity threshold used to detect when an axis (like a thumbstick or trigger)
2571
+ * is considered "actively moved". This helps distinguish minor noise from intentional input.
2572
+ *
2573
+ * A value of 0 means even the slightest movement is detected; a value closer to 1 means only strong input is accepted.
2574
+ *
2575
+ * @returns {number} The current axis activity sensitivity (between 0 and 1).
2576
+ */
2577
+ get axisActiveSensitivity() {
2578
+ return this.#axisActiveSensitivity;
2579
+ }
2580
+
2581
+ /**
2582
+ * Sets the sensitivity threshold used to detect active axis input.
2583
+ * This value defines how far an analog axis (such as a thumbstick or trigger) must move from its rest position
2584
+ * before being considered as a valid input.
2585
+ *
2586
+ * Values must be between 0 and 1. An error is thrown if the value is outside this range or not a number.
2587
+ *
2588
+ * @param {number} value - The new sensitivity value between 0 and 1.
2589
+ */
2590
+ set axisActiveSensitivity(value) {
2591
+ if (typeof value !== 'number' || value < 0 || value > 1)
2592
+ throw new RangeError('Axis sensitivity must be a number between 0 and 1.');
2593
+ this.#axisActiveSensitivity = value;
2594
+ }
2595
+
2596
+ /**
2597
+ * Returns the current dead zone threshold for analog inputs.
2598
+ * @returns {number}
2599
+ */
2600
+ get deadZone() {
2601
+ return this.#deadZone;
2602
+ }
2603
+
2604
+ /**
2605
+ * Sets a new dead zone threshold for analog inputs.
2606
+ * Must be a number between 0 and 1.
2607
+ * @param {number} value
2608
+ */
2609
+ set deadZone(value) {
2610
+ if (typeof value !== 'number' || value < 0 || value > 1)
2611
+ throw new RangeError('Dead zone must be a number between 0 and 1.');
2612
+ this.#deadZone = value;
2613
+ }
2614
+
2615
+ /**
2616
+ * Returns the expected device ID, or null if any is accepted.
2617
+ * @returns {string|null}
2618
+ */
2619
+ get expectedId() {
2620
+ return this.#expectedId;
2621
+ }
2622
+
2623
+ /**
2624
+ * Sets the expected device ID.
2625
+ * @returns {string}
2626
+ */
2627
+ set expectedId(value) {
2628
+ if (typeof value !== 'string') throw new TypeError('Expected device id be a string.');
2629
+ this.#expectedId = value;
2630
+ }
2631
+
2632
+ ////////////////////////////////////
2633
+
2634
+ /**
2635
+ * Returns whether the instance has been destroyed
2636
+ * @returns {boolean}
2637
+ */
2638
+ get isDestroyed() {
2639
+ return this.#isDestroyed;
2640
+ }
2641
+
2642
+ /**
2643
+ * Clears internal state and stops any ongoing input monitoring or loops.
2644
+ * Marks the instance as destroyed.
2645
+ */
2646
+ destroy() {
2647
+ if (this.#isDestroyed) return;
2648
+ this.#isDestroyed = true;
2649
+
2650
+ if (this.#animationFrame) cancelAnimationFrame(this.#animationFrame);
2651
+ if (this.#mouseKeyboardHoldLoop) cancelAnimationFrame(this.#mouseKeyboardHoldLoop);
2652
+ this.#animationFrame = null;
2653
+ this.#mouseKeyboardHoldLoop = null;
2654
+
2655
+ if (['keyboard-only', 'both'].includes(this.#inputMode)) {
2656
+ this.#elementBase.removeEventListener('keydown', this.#keydown);
2657
+ this.#elementBase.removeEventListener('keyup', this.#keyup);
2658
+ if (this.#allowMouse) {
2659
+ this.#elementBase.removeEventListener('mousedown', this.#mousedown);
2660
+ this.#elementBase.removeEventListener('mouseup', this.#mouseup);
2661
+ this.#elementBase.removeEventListener('mousemove', this.#mousemove);
2662
+ }
2663
+ }
2664
+
2665
+ if (['gamepad-only', 'both'].includes(this.#inputMode)) {
2666
+ window.removeEventListener('gamepadconnected', this.#gamepadConnected);
2667
+ window.removeEventListener('gamepaddisconnected', this.#gamepadDisconnected);
2668
+ }
2669
+
2670
+ this.resetComboMapped();
2671
+ this.#inputMap.clear();
2672
+ this.#callbacks.clear();
2673
+ this.#heldKeys.clear();
2674
+ this.#activeMappedInputs.clear();
2675
+ this.#activeMappedKeys.clear();
2676
+ this.#inputSequences.clear();
2677
+ this.#keySequences.clear();
2678
+ this.#ignoreIds.clear();
2679
+ this.#keyOldValue.clear();
2680
+ this.#lastButtonStates = [];
2681
+ this.#lastAxes = [];
2682
+ this.#lastKeyStates = {};
2683
+ this.#connectedGamepad = null;
2684
+ this.#expectedId = null;
2685
+ this.#allowMouse = false;
2686
+ this.#timeMappedInputs = 0;
2687
+ }
2688
+ }
2689
+
2690
+ module.exports = TinyGamepad;