tiny-essentials 1.20.3 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/dist/_/numbers.cjs +136 -0
- package/dist/_/numbers.d.mts +24 -0
- package/dist/_/numbers.mjs +102 -0
- package/dist/v1/TinyAdvancedRaffle.min.js +1 -0
- package/dist/v1/TinyArrayPaginator.min.js +1 -0
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyCookieConsent.min.js +1 -0
- package/dist/v1/TinyDayNightCycle.min.js +1 -0
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyGamepad.min.js +1 -0
- package/dist/v1/TinyI18.min.js +1 -0
- package/dist/v1/TinyInventory.min.js +1 -0
- package/dist/v1/TinyInventoryTrader.min.js +1 -0
- package/dist/v1/TinyLocalStorage.min.js +1 -1
- package/dist/v1/TinyRateLimiter.min.js +1 -1
- package/dist/v1/TinySmartScroller.min.js +1 -1
- package/dist/v1/TinyToastNotify.min.js +1 -1
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/basics/array.cjs +21 -0
- package/dist/v1/basics/array.d.mts +8 -0
- package/dist/v1/basics/array.mjs +18 -0
- package/dist/v1/basics/index.cjs +2 -8
- package/dist/v1/basics/index.d.mts +3 -8
- package/dist/v1/basics/index.mjs +2 -3
- package/dist/v1/basics/simpleMath.cjs +46 -0
- package/dist/v1/basics/simpleMath.d.mts +27 -0
- package/dist/v1/basics/simpleMath.mjs +42 -0
- package/dist/v1/build/TinyAdvancedRaffle.cjs +7 -0
- package/dist/v1/build/TinyAdvancedRaffle.d.mts +3 -0
- package/dist/v1/build/TinyAdvancedRaffle.mjs +2 -0
- package/dist/v1/build/TinyArrayPaginator.cjs +7 -0
- package/dist/v1/build/TinyArrayPaginator.d.mts +3 -0
- package/dist/v1/build/TinyArrayPaginator.mjs +2 -0
- package/dist/v1/build/TinyCookieConsent.cjs +7 -0
- package/dist/v1/build/TinyCookieConsent.d.mts +3 -0
- package/dist/v1/build/TinyCookieConsent.mjs +2 -0
- package/dist/v1/build/TinyDayNightCycle.cjs +7 -0
- package/dist/v1/build/TinyDayNightCycle.d.mts +3 -0
- package/dist/v1/build/TinyDayNightCycle.mjs +2 -0
- package/dist/v1/build/TinyGamepad.cjs +7 -0
- package/dist/v1/build/TinyGamepad.d.mts +3 -0
- package/dist/v1/build/TinyGamepad.mjs +2 -0
- package/dist/v1/build/TinyI18.cjs +7 -0
- package/dist/v1/build/TinyI18.d.mts +3 -0
- package/dist/v1/build/TinyI18.mjs +2 -0
- package/dist/v1/build/TinyInventory.cjs +7 -0
- package/dist/v1/build/TinyInventory.d.mts +3 -0
- package/dist/v1/build/TinyInventory.mjs +2 -0
- package/dist/v1/build/TinyInventoryTrader.cjs +7 -0
- package/dist/v1/build/TinyInventoryTrader.d.mts +3 -0
- package/dist/v1/build/TinyInventoryTrader.mjs +2 -0
- package/dist/v1/css/TinyCookieConsent.min.css +1 -0
- package/dist/v1/index.cjs +18 -8
- package/dist/v1/index.d.mts +11 -8
- package/dist/v1/index.mjs +10 -3
- package/dist/v1/libs/TinyAdvancedRaffle.cjs +1654 -0
- package/dist/v1/libs/TinyAdvancedRaffle.d.mts +851 -0
- package/dist/v1/libs/TinyAdvancedRaffle.mjs +1477 -0
- package/dist/v1/libs/TinyArrayPaginator.cjs +132 -0
- package/dist/v1/libs/TinyArrayPaginator.d.mts +78 -0
- package/dist/v1/libs/TinyArrayPaginator.mjs +119 -0
- package/dist/v1/libs/TinyCookieConsent.cjs +278 -0
- package/dist/v1/libs/TinyCookieConsent.d.mts +142 -0
- package/dist/v1/libs/TinyCookieConsent.mjs +248 -0
- package/dist/v1/libs/TinyDayNightCycle.cjs +1389 -0
- package/dist/v1/libs/TinyDayNightCycle.d.mts +673 -0
- package/dist/v1/libs/TinyDayNightCycle.mjs +1232 -0
- package/dist/v1/libs/TinyGamepad.cjs +2690 -0
- package/dist/v1/libs/TinyGamepad.d.mts +1286 -0
- package/dist/v1/libs/TinyGamepad.mjs +2386 -0
- package/dist/v1/libs/TinyHtml.d.mts +2 -2
- package/dist/v1/libs/TinyI18.cjs +946 -0
- package/dist/v1/libs/TinyI18.d.mts +418 -0
- package/dist/v1/libs/TinyI18.mjs +873 -0
- package/dist/v1/libs/TinyInventory.cjs +1788 -0
- package/dist/v1/libs/TinyInventory.d.mts +871 -0
- package/dist/v1/libs/TinyInventory.mjs +1650 -0
- package/dist/v1/libs/TinyInventoryTrader.cjs +242 -0
- package/dist/v1/libs/TinyInventoryTrader.d.mts +95 -0
- package/dist/v1/libs/TinyInventoryTrader.mjs +212 -0
- package/dist/v1/libs/TinyLocalStorage.cjs +14 -1
- package/dist/v1/libs/TinyLocalStorage.mjs +8 -1
- package/dist/v1/libs/TinyRateLimiter.cjs +0 -13
- package/dist/v1/libs/TinyRateLimiter.d.mts +0 -8
- package/dist/v1/libs/TinyRateLimiter.mjs +0 -12
- package/dist/v1/libs/TinySmartScroller.cjs +0 -51
- package/dist/v1/libs/TinySmartScroller.d.mts +0 -36
- package/dist/v1/libs/TinySmartScroller.mjs +0 -46
- package/dist/v1/libs/TinyToastNotify.cjs +2 -2
- package/dist/v1/libs/TinyToastNotify.mjs +2 -2
- package/dist/v1/libs/TinyUploadClicker.cjs +1 -1
- package/docs/v1/Ai-Tips.md +51 -0
- package/docs/v1/Personal-Ai-Prompts(portuguese).md +134 -0
- package/docs/v1/Personal-Ai-Prompts.md +113 -0
- package/docs/v1/README.md +23 -0
- package/docs/v1/basics/array.md +17 -0
- package/docs/v1/basics/simpleMath.md +73 -0
- package/docs/v1/libs/TinyAdvancedRaffle.md +674 -0
- package/docs/v1/libs/TinyArrayPaginator.md +150 -0
- package/docs/v1/libs/TinyCookieConsent.md +152 -0
- package/docs/v1/libs/TinyDayNightCycle.md +711 -0
- package/docs/v1/libs/TinyGamepad.md +980 -0
- package/docs/v1/libs/TinyI18.md +515 -0
- package/docs/v1/libs/TinyInventory.md +777 -0
- package/docs/v1/libs/TinyInventoryTrader.md +114 -0
- package/package.json +7 -5
- package/dist/v1/basics/html_deprecated.cjs +0 -124
- package/dist/v1/basics/html_deprecated.d.mts +0 -40
- package/dist/v1/basics/html_deprecated.mjs +0 -97
- package/dist/v1/css/TinyDraggerExample.css +0 -21
- package/dist/v1/css/TinyNotify.css +0 -350
- package/dist/v1/css/aiMarker.css +0 -4
- 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;
|