tiny-essentials 1.20.3 → 1.21.1

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