@codexo/exojs 0.7.12 → 0.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +334 -0
  2. package/dist/esm/core/Application.d.ts +3 -1
  3. package/dist/esm/core/Application.js +7 -6
  4. package/dist/esm/core/Application.js.map +1 -1
  5. package/dist/esm/core/Scene.d.ts +30 -0
  6. package/dist/esm/core/Scene.js +56 -0
  7. package/dist/esm/core/Scene.js.map +1 -1
  8. package/dist/esm/core/SceneManager.js +2 -2
  9. package/dist/esm/core/SceneManager.js.map +1 -1
  10. package/dist/esm/debug/DebugOverlay.js +2 -2
  11. package/dist/esm/debug/DebugOverlay.js.map +1 -1
  12. package/dist/esm/debug/PointerStackLayer.js +1 -1
  13. package/dist/esm/debug/PointerStackLayer.js.map +1 -1
  14. package/dist/esm/index.js +4 -3
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/input/ArcadeStickGamepadMapping.js +18 -19
  17. package/dist/esm/input/ArcadeStickGamepadMapping.js.map +1 -1
  18. package/dist/esm/input/Gamepad.d.ts +164 -62
  19. package/dist/esm/input/Gamepad.js +290 -134
  20. package/dist/esm/input/Gamepad.js.map +1 -1
  21. package/dist/esm/input/GamepadAxis.d.ts +120 -0
  22. package/dist/esm/input/GamepadAxis.js +106 -0
  23. package/dist/esm/input/GamepadAxis.js.map +1 -0
  24. package/dist/esm/input/GamepadButton.d.ts +110 -0
  25. package/dist/esm/input/GamepadButton.js +99 -0
  26. package/dist/esm/input/GamepadButton.js.map +1 -0
  27. package/dist/esm/input/GamepadDefinitions.js +4 -0
  28. package/dist/esm/input/GamepadDefinitions.js.map +1 -1
  29. package/dist/esm/input/GamepadMapping.d.ts +28 -24
  30. package/dist/esm/input/GamepadMapping.js +33 -16
  31. package/dist/esm/input/GamepadMapping.js.map +1 -1
  32. package/dist/esm/input/GamepadPromptLayouts.d.ts +10 -8
  33. package/dist/esm/input/GamepadPromptLayouts.js +21 -20
  34. package/dist/esm/input/GamepadPromptLayouts.js.map +1 -1
  35. package/dist/esm/input/GenericDualAnalogGamepadMapping.d.ts +6 -3
  36. package/dist/esm/input/GenericDualAnalogGamepadMapping.js +55 -46
  37. package/dist/esm/input/GenericDualAnalogGamepadMapping.js.map +1 -1
  38. package/dist/esm/input/InputBinding.d.ts +74 -0
  39. package/dist/esm/input/InputBinding.js +100 -0
  40. package/dist/esm/input/InputBinding.js.map +1 -0
  41. package/dist/esm/input/InputManager.d.ts +79 -33
  42. package/dist/esm/input/InputManager.js +229 -104
  43. package/dist/esm/input/InputManager.js.map +1 -1
  44. package/dist/esm/input/InteractionManager.d.ts +1 -1
  45. package/dist/esm/input/InteractionManager.js +13 -13
  46. package/dist/esm/input/InteractionManager.js.map +1 -1
  47. package/dist/esm/input/JoyConLeftGamepadMapping.d.ts +14 -9
  48. package/dist/esm/input/JoyConLeftGamepadMapping.js +39 -9
  49. package/dist/esm/input/JoyConLeftGamepadMapping.js.map +1 -1
  50. package/dist/esm/input/JoyConRightGamepadMapping.d.ts +14 -9
  51. package/dist/esm/input/JoyConRightGamepadMapping.js +35 -9
  52. package/dist/esm/input/JoyConRightGamepadMapping.js.map +1 -1
  53. package/dist/esm/input/Pointer.d.ts +84 -71
  54. package/dist/esm/input/Pointer.js +71 -71
  55. package/dist/esm/input/Pointer.js.map +1 -1
  56. package/dist/esm/input/SteamDeckGamepadMapping.d.ts +18 -0
  57. package/dist/esm/input/SteamDeckGamepadMapping.js +76 -0
  58. package/dist/esm/input/SteamDeckGamepadMapping.js.map +1 -0
  59. package/dist/esm/input/index.d.ts +7 -4
  60. package/dist/esm/input/types.d.ts +0 -76
  61. package/dist/esm/input/types.js +1 -80
  62. package/dist/esm/input/types.js.map +1 -1
  63. package/dist/esm/resources/CacheFirstStrategy.d.ts +7 -4
  64. package/dist/esm/resources/CacheFirstStrategy.js +11 -8
  65. package/dist/esm/resources/CacheFirstStrategy.js.map +1 -1
  66. package/dist/esm/resources/CacheStrategy.d.ts +14 -6
  67. package/dist/esm/resources/Loader.d.ts +8 -3
  68. package/dist/esm/resources/Loader.js +19 -37
  69. package/dist/esm/resources/Loader.js.map +1 -1
  70. package/dist/esm/resources/NetworkOnlyStrategy.d.ts +3 -0
  71. package/dist/esm/resources/NetworkOnlyStrategy.js +8 -3
  72. package/dist/esm/resources/NetworkOnlyStrategy.js.map +1 -1
  73. package/dist/esm/resources/factories/ImageFactory.d.ts +2 -2
  74. package/dist/esm/resources/factories/ImageFactory.js.map +1 -1
  75. package/dist/esm/resources/factories/TextureFactory.d.ts +2 -2
  76. package/dist/esm/resources/factories/TextureFactory.js.map +1 -1
  77. package/dist/esm/resources/factories/VttFactory.d.ts +3 -3
  78. package/dist/esm/resources/factories/VttFactory.js +83 -6
  79. package/dist/esm/resources/factories/VttFactory.js.map +1 -1
  80. package/dist/exo.esm.js +1390 -795
  81. package/dist/exo.esm.js.map +1 -1
  82. package/package.json +2 -1
  83. package/dist/esm/input/GamepadChannels.d.ts +0 -47
  84. package/dist/esm/input/GamepadChannels.js +0 -53
  85. package/dist/esm/input/GamepadChannels.js.map +0 -1
  86. package/dist/esm/input/GamepadControl.d.ts +0 -33
  87. package/dist/esm/input/GamepadControl.js +0 -42
  88. package/dist/esm/input/GamepadControl.js.map +0 -1
  89. package/dist/esm/input/Input.d.ts +0 -52
  90. package/dist/esm/input/Input.js +0 -90
  91. package/dist/esm/input/Input.js.map +0 -1
package/dist/exo.esm.js CHANGED
@@ -5592,7 +5592,7 @@ class SceneManager {
5592
5592
  return { updateScenes, drawScenes };
5593
5593
  }
5594
5594
  _subscribeInputRouting() {
5595
- const inputManager = this._app.inputManager;
5595
+ const inputManager = this._app.input;
5596
5596
  inputManager?.onKeyDown?.add?.(this._handleKeyDown);
5597
5597
  inputManager?.onKeyUp?.add?.(this._handleKeyUp);
5598
5598
  inputManager?.onPointerEnter?.add?.(this._handlePointerEnter);
@@ -5606,7 +5606,7 @@ class SceneManager {
5606
5606
  inputManager?.onMouseWheel?.add?.(this._handleMouseWheel);
5607
5607
  }
5608
5608
  _unsubscribeInputRouting() {
5609
- const inputManager = this._app.inputManager;
5609
+ const inputManager = this._app.input;
5610
5610
  inputManager?.onKeyDown?.remove?.(this._handleKeyDown);
5611
5611
  inputManager?.onKeyUp?.remove?.(this._handleKeyUp);
5612
5612
  inputManager?.onPointerEnter?.remove?.(this._handlePointerEnter);
@@ -12919,85 +12919,6 @@ var ChannelOffset;
12919
12919
  const maxPointers = 16;
12920
12920
  /** Number of channel slots reserved per pointer. 16 pointers × 16 slots = 256 (fills the Pointers category exactly). */
12921
12921
  const pointerSlotSize = 16;
12922
- /**
12923
- * Channel offsets for unified pointer (mouse / touch / pen) state.
12924
- *
12925
- * The un-prefixed aliases (Active, X, Y, …) are identical to Slot0Active, Slot0X, Slot0Y, …
12926
- * and address the primary pointer (slot 0). Use Slot{N}Active / Slot{N}X / Slot{N}Y for
12927
- * multi-pointer (e.g. pinch) access. Other per-slot channels beyond Active/X/Y are reachable
12928
- * via arithmetic: `Pointer.X + slotIndex * pointerSlotSize + channelOffset` (Pointer.X = PointerChannel.X).
12929
- *
12930
- * @internal — accessed publicly via the `Pointer` class namespace (see Pointer.ts).
12931
- */
12932
- var PointerChannel;
12933
- (function (PointerChannel) {
12934
- // --- Convenience aliases for the primary pointer (slot 0) ---
12935
- PointerChannel[PointerChannel["Active"] = 256] = "Active";
12936
- PointerChannel[PointerChannel["X"] = 257] = "X";
12937
- PointerChannel[PointerChannel["Y"] = 258] = "Y";
12938
- PointerChannel[PointerChannel["Pressure"] = 259] = "Pressure";
12939
- PointerChannel[PointerChannel["Width"] = 260] = "Width";
12940
- PointerChannel[PointerChannel["Height"] = 261] = "Height";
12941
- PointerChannel[PointerChannel["Twist"] = 262] = "Twist";
12942
- PointerChannel[PointerChannel["TiltX"] = 263] = "TiltX";
12943
- PointerChannel[PointerChannel["TiltY"] = 264] = "TiltY";
12944
- PointerChannel[PointerChannel["Left"] = 265] = "Left";
12945
- PointerChannel[PointerChannel["Right"] = 266] = "Right";
12946
- PointerChannel[PointerChannel["Middle"] = 267] = "Middle";
12947
- PointerChannel[PointerChannel["IsMouse"] = 268] = "IsMouse";
12948
- PointerChannel[PointerChannel["IsTouch"] = 269] = "IsTouch";
12949
- PointerChannel[PointerChannel["IsPen"] = 270] = "IsPen";
12950
- PointerChannel[PointerChannel["IsPrimary"] = 271] = "IsPrimary";
12951
- // --- Per-slot Active/X/Y for multi-pointer access ---
12952
- PointerChannel[PointerChannel["Slot0Active"] = 256] = "Slot0Active";
12953
- PointerChannel[PointerChannel["Slot0X"] = 257] = "Slot0X";
12954
- PointerChannel[PointerChannel["Slot0Y"] = 258] = "Slot0Y";
12955
- PointerChannel[PointerChannel["Slot1Active"] = 272] = "Slot1Active";
12956
- PointerChannel[PointerChannel["Slot1X"] = 273] = "Slot1X";
12957
- PointerChannel[PointerChannel["Slot1Y"] = 274] = "Slot1Y";
12958
- PointerChannel[PointerChannel["Slot2Active"] = 288] = "Slot2Active";
12959
- PointerChannel[PointerChannel["Slot2X"] = 289] = "Slot2X";
12960
- PointerChannel[PointerChannel["Slot2Y"] = 290] = "Slot2Y";
12961
- PointerChannel[PointerChannel["Slot3Active"] = 304] = "Slot3Active";
12962
- PointerChannel[PointerChannel["Slot3X"] = 305] = "Slot3X";
12963
- PointerChannel[PointerChannel["Slot3Y"] = 306] = "Slot3Y";
12964
- PointerChannel[PointerChannel["Slot4Active"] = 320] = "Slot4Active";
12965
- PointerChannel[PointerChannel["Slot4X"] = 321] = "Slot4X";
12966
- PointerChannel[PointerChannel["Slot4Y"] = 322] = "Slot4Y";
12967
- PointerChannel[PointerChannel["Slot5Active"] = 336] = "Slot5Active";
12968
- PointerChannel[PointerChannel["Slot5X"] = 337] = "Slot5X";
12969
- PointerChannel[PointerChannel["Slot5Y"] = 338] = "Slot5Y";
12970
- PointerChannel[PointerChannel["Slot6Active"] = 352] = "Slot6Active";
12971
- PointerChannel[PointerChannel["Slot6X"] = 353] = "Slot6X";
12972
- PointerChannel[PointerChannel["Slot6Y"] = 354] = "Slot6Y";
12973
- PointerChannel[PointerChannel["Slot7Active"] = 368] = "Slot7Active";
12974
- PointerChannel[PointerChannel["Slot7X"] = 369] = "Slot7X";
12975
- PointerChannel[PointerChannel["Slot7Y"] = 370] = "Slot7Y";
12976
- PointerChannel[PointerChannel["Slot8Active"] = 384] = "Slot8Active";
12977
- PointerChannel[PointerChannel["Slot8X"] = 385] = "Slot8X";
12978
- PointerChannel[PointerChannel["Slot8Y"] = 386] = "Slot8Y";
12979
- PointerChannel[PointerChannel["Slot9Active"] = 400] = "Slot9Active";
12980
- PointerChannel[PointerChannel["Slot9X"] = 401] = "Slot9X";
12981
- PointerChannel[PointerChannel["Slot9Y"] = 402] = "Slot9Y";
12982
- PointerChannel[PointerChannel["Slot10Active"] = 416] = "Slot10Active";
12983
- PointerChannel[PointerChannel["Slot10X"] = 417] = "Slot10X";
12984
- PointerChannel[PointerChannel["Slot10Y"] = 418] = "Slot10Y";
12985
- PointerChannel[PointerChannel["Slot11Active"] = 432] = "Slot11Active";
12986
- PointerChannel[PointerChannel["Slot11X"] = 433] = "Slot11X";
12987
- PointerChannel[PointerChannel["Slot11Y"] = 434] = "Slot11Y";
12988
- PointerChannel[PointerChannel["Slot12Active"] = 448] = "Slot12Active";
12989
- PointerChannel[PointerChannel["Slot12X"] = 449] = "Slot12X";
12990
- PointerChannel[PointerChannel["Slot12Y"] = 450] = "Slot12Y";
12991
- PointerChannel[PointerChannel["Slot13Active"] = 464] = "Slot13Active";
12992
- PointerChannel[PointerChannel["Slot13X"] = 465] = "Slot13X";
12993
- PointerChannel[PointerChannel["Slot13Y"] = 466] = "Slot13Y";
12994
- PointerChannel[PointerChannel["Slot14Active"] = 480] = "Slot14Active";
12995
- PointerChannel[PointerChannel["Slot14X"] = 481] = "Slot14X";
12996
- PointerChannel[PointerChannel["Slot14Y"] = 482] = "Slot14Y";
12997
- PointerChannel[PointerChannel["Slot15Active"] = 496] = "Slot15Active";
12998
- PointerChannel[PointerChannel["Slot15X"] = 497] = "Slot15X";
12999
- PointerChannel[PointerChannel["Slot15Y"] = 498] = "Slot15Y";
13000
- })(PointerChannel || (PointerChannel = {}));
13001
12922
  /**
13002
12923
  * Channel indices for keyboard keys, derived from the legacy `KeyboardEvent.keyCode`
13003
12924
  * map. Pass any value to the {@link Input} constructor to react to that key.
@@ -13111,197 +13032,484 @@ var Keyboard;
13111
13032
  })(Keyboard || (Keyboard = {}));
13112
13033
 
13113
13034
  /**
13114
- * Runtime wrapper for a single browser gamepad slot.
13035
+ * {@link Clock} variant with a fixed limit. Inherits start/stop/reset/restart
13036
+ * semantics; adds {@link Timer.expired} (true once `elapsedTime >= limit`)
13037
+ * and remaining-time accessors. Useful for cooldowns, delays, and any timed
13038
+ * gating logic where you want to ask "is the duration up?" each frame.
13039
+ */
13040
+ class Timer extends Clock {
13041
+ _limit;
13042
+ constructor(limit, autoStart = false) {
13043
+ super();
13044
+ this._limit = limit.clone();
13045
+ if (autoStart) {
13046
+ this.restart();
13047
+ }
13048
+ }
13049
+ set limit(limit) {
13050
+ this._limit.copy(limit);
13051
+ }
13052
+ /** `true` once the elapsed time has reached or exceeded the configured limit. */
13053
+ get expired() {
13054
+ return this.elapsedMilliseconds >= this._limit.milliseconds;
13055
+ }
13056
+ get remainingMilliseconds() {
13057
+ return Math.max(0, this._limit.milliseconds - this.elapsedMilliseconds);
13058
+ }
13059
+ get remainingSeconds() {
13060
+ return this.remainingMilliseconds / Time.seconds;
13061
+ }
13062
+ get remainingMinutes() {
13063
+ return this.remainingMilliseconds / Time.minutes;
13064
+ }
13065
+ get remainingHours() {
13066
+ return this.remainingMilliseconds / Time.hours;
13067
+ }
13068
+ }
13069
+
13070
+ /**
13071
+ * One subscription to one or more input channels. Tracks active state, fires
13072
+ * the {@link onStart} / {@link onActive} / {@link onStop} / {@link onTrigger}
13073
+ * Signals each frame, and registers itself with whichever owner created it
13074
+ * (typically an {@link InputManager}, {@link Gamepad}, or scene-bound
13075
+ * proxy).
13076
+ *
13077
+ * Construct via the owner's `onStart` / `onActive` / `onStop` /
13078
+ * `onTrigger` factory methods rather than `new InputBinding(...)` directly.
13115
13079
  *
13116
- * Owns the slot's {@link GamepadMapping}, reads raw button and axis values from
13117
- * the browser's Gamepad API each frame via {@link update}, and writes transformed
13118
- * values into the shared `Float32Array` channel buffer. Emits {@link onConnect},
13119
- * {@link onDisconnect}, and per-channel {@link onUpdate} signals so consumers can
13120
- * react without polling.
13080
+ * Lifecycle: a binding lives until {@link unbind} is called, the owner
13081
+ * disposes it, or for scene-bound bindings the scene unloads.
13082
+ *
13083
+ * @internal
13084
+ */
13085
+ class InputBinding {
13086
+ /**
13087
+ * Default tap-window for `onTrigger`. Override per binding via the
13088
+ * `threshold` option. Mutating this static affects only newly created
13089
+ * bindings.
13090
+ */
13091
+ static defaultTriggerThreshold = 300;
13092
+ channels;
13093
+ onStart = new Signal();
13094
+ onActive = new Signal();
13095
+ onStop = new Signal();
13096
+ onTrigger = new Signal();
13097
+ _triggerTimer;
13098
+ _detacher;
13099
+ _value = 0;
13100
+ _unbound = false;
13101
+ constructor(channels, options = {}, detacher = null) {
13102
+ this.channels = channels;
13103
+ this._triggerTimer = new Timer(milliseconds(options.threshold ?? InputBinding.defaultTriggerThreshold));
13104
+ this._detacher = detacher;
13105
+ }
13106
+ /** Last value sampled this frame. 0 when inactive. */
13107
+ get value() {
13108
+ return this._value;
13109
+ }
13110
+ /** `true` when the last sampled value exceeded the channel's threshold. */
13111
+ get active() {
13112
+ return this._value > 0;
13113
+ }
13114
+ /**
13115
+ * Read the latest values from the unified channel buffer and dispatch
13116
+ * the appropriate Signals. Called once per frame by the owning manager.
13117
+ *
13118
+ * @internal
13119
+ */
13120
+ update(channels) {
13121
+ if (this._unbound) {
13122
+ return;
13123
+ }
13124
+ let value = 0;
13125
+ for (const channel of this.channels) {
13126
+ const sample = channels[channel];
13127
+ if (Math.abs(sample) > Math.abs(value)) {
13128
+ value = sample;
13129
+ }
13130
+ }
13131
+ this._value = value;
13132
+ if (value !== 0) {
13133
+ if (!this._triggerTimer.running) {
13134
+ this._triggerTimer.restart();
13135
+ this.onStart.dispatch(value);
13136
+ }
13137
+ this.onActive.dispatch(value);
13138
+ }
13139
+ else if (this._triggerTimer.running) {
13140
+ this.onStop.dispatch(0);
13141
+ if (!this._triggerTimer.expired) {
13142
+ this.onTrigger.dispatch(0);
13143
+ }
13144
+ this._triggerTimer.stop();
13145
+ }
13146
+ }
13147
+ /**
13148
+ * Detach this binding from its owner and release its Signals. Idempotent.
13149
+ */
13150
+ unbind() {
13151
+ if (this._unbound) {
13152
+ return;
13153
+ }
13154
+ this._unbound = true;
13155
+ this._detacher?.detach(this);
13156
+ this._triggerTimer.destroy();
13157
+ this.onStart.destroy();
13158
+ this.onActive.destroy();
13159
+ this.onStop.destroy();
13160
+ this.onTrigger.destroy();
13161
+ }
13162
+ }
13163
+
13164
+ /**
13165
+ * One of four stable gamepad slots. Lives for the entire `Application`
13166
+ * lifetime even when no physical pad is attached — a "mailbox" that
13167
+ * physical hardware moves into and out of.
13168
+ *
13169
+ * Subscribe to {@link onConnect} / {@link onDisconnect} for hardware
13170
+ * lifecycle, {@link onButtonDown} / {@link onButtonUp} / {@link onAxisChange}
13171
+ * for granular per-event notifications, or call {@link onTrigger} /
13172
+ * {@link onActive} / {@link onStart} / {@link onStop} to register
13173
+ * stateful {@link InputBinding}s pinned to this slot.
13174
+ *
13175
+ * Listeners survive disconnect/reconnect cycles — a binding registered when
13176
+ * the slot was empty will automatically activate when a pad connects.
13121
13177
  */
13122
13178
  class Gamepad {
13179
+ /** Fires when a physical pad connects to this slot. */
13123
13180
  onConnect = new Signal();
13181
+ /** Fires when the physical pad in this slot disconnects. */
13124
13182
  onDisconnect = new Signal();
13125
- onUpdate = new Signal();
13126
- indexValue;
13127
- channelsValue;
13128
- channelOffset;
13129
- mappingValue;
13130
- browserGamepad = null;
13131
- info = {
13132
- name: 'Generic Gamepad',
13133
- label: 'Generic Gamepad',
13134
- vendorId: null,
13135
- productId: null,
13136
- productKey: null,
13137
- };
13138
- constructor(indexOrGamepad, channels, mappingOrDefinition) {
13139
- const isBrowserGamepad = typeof indexOrGamepad !== 'number';
13140
- const gamepad = isBrowserGamepad ? indexOrGamepad : null;
13141
- const index = isBrowserGamepad ? indexOrGamepad.index : indexOrGamepad;
13142
- this.indexValue = index;
13143
- this.channelsValue = channels;
13144
- this.channelOffset = ChannelOffset.Gamepads + (index * ChannelSize.Gamepad);
13145
- this.mappingValue = gamepad
13146
- ? mappingOrDefinition.mapping
13147
- : mappingOrDefinition;
13148
- if (gamepad) {
13149
- const definition = mappingOrDefinition;
13150
- this.setInfo({
13151
- name: definition.name,
13152
- label: definition.descriptor.label,
13153
- vendorId: definition.descriptor.vendorId,
13154
- productId: definition.descriptor.productId,
13155
- productKey: definition.descriptor.productKey,
13156
- });
13157
- this.connect(gamepad);
13158
- }
13183
+ /** Fires for every button transition from inactive to active. */
13184
+ onButtonDown = new Signal();
13185
+ /** Fires for every button transition from active to inactive. */
13186
+ onButtonUp = new Signal();
13187
+ /** Fires whenever an axis crosses its activation threshold. */
13188
+ onAxisChange = new Signal();
13189
+ /**
13190
+ * Fires when this slot's physical pad has been replaced by a previously
13191
+ * higher-numbered slot's pad, after a `'compact'`-strategy disconnect.
13192
+ * Dispatched with the source slot index the pad came from. Listeners
13193
+ * remain attached and the channel buffer is preserved across the move.
13194
+ */
13195
+ onPadReassigned = new Signal();
13196
+ _slot;
13197
+ _channels;
13198
+ _bindings = new Set();
13199
+ _detacher = { detach: (binding) => { this._bindings.delete(binding); } };
13200
+ _channelOffset;
13201
+ _mapping = null;
13202
+ _info = null;
13203
+ _browserGamepad = null;
13204
+ constructor(slot, channels) {
13205
+ this._slot = slot;
13206
+ this._channels = channels;
13207
+ this._channelOffset = ChannelOffset.Gamepads + (slot * ChannelSize.Gamepad);
13159
13208
  }
13160
- get mapping() {
13161
- return this.mappingValue;
13209
+ /** This pad's stable slot (0..3). */
13210
+ get slot() {
13211
+ return this._slot;
13212
+ }
13213
+ /** `true` while a physical pad is attached to this slot. */
13214
+ get connected() {
13215
+ return this._browserGamepad !== null;
13162
13216
  }
13163
- set mapping(mapping) {
13164
- this.mappingValue = mapping;
13217
+ /** The active mapping, or `null` when disconnected. */
13218
+ get mapping() {
13219
+ return this._mapping;
13165
13220
  }
13166
- /** The {@link GamepadMappingFamily} of the currently active mapping. */
13221
+ /** The {@link GamepadMappingFamily}, or `null` when disconnected. */
13167
13222
  get mappingFamily() {
13168
- return this.mappingValue.family;
13223
+ return this._mapping?.family ?? null;
13169
13224
  }
13170
- get channels() {
13171
- return this.channelsValue;
13225
+ /** Identity metadata, or `null` when disconnected. */
13226
+ get info() {
13227
+ return this._info;
13172
13228
  }
13173
- get gamepad() {
13174
- return this.browserGamepad;
13229
+ /** The underlying browser gamepad object, or `null` when disconnected. */
13230
+ get browserGamepad() {
13231
+ return this._browserGamepad;
13175
13232
  }
13176
- get index() {
13177
- return this.indexValue;
13233
+ /**
13234
+ * Browser-assigned hardware index from `navigator.getGamepads()` (i.e.
13235
+ * `Gamepad.index`), or `null` when no pad is attached. Stable for the
13236
+ * lifetime of a single physical connection but may change across
13237
+ * disconnect/reconnect. Low-level escape hatch for advanced consumers
13238
+ * that need to correlate slots with the raw browser API; prefer
13239
+ * {@link slot} for stable per-application pad identity.
13240
+ */
13241
+ get internalIndex() {
13242
+ return this._browserGamepad?.index ?? null;
13178
13243
  }
13179
- /** Whether a browser gamepad is currently attached to this slot. */
13180
- get connected() {
13181
- return this.browserGamepad !== null;
13244
+ /** `true` when the connected pad supports rumble via the Web Gamepad API. */
13245
+ get canVibrate() {
13246
+ return this._browserGamepad?.vibrationActuator != null;
13247
+ }
13248
+ /**
13249
+ * Returns `true` when this pad's mapping declares the requested channel.
13250
+ * Use to gate listener registration on optional hardware (e.g. Joy-Con
13251
+ * solo lacks a right stick — `pad.hasChannel(GamepadAxis.RightStickX)`
13252
+ * returns `false`).
13253
+ */
13254
+ hasChannel(channel) {
13255
+ return this._mapping?.hasChannel(channel) ?? false;
13256
+ }
13257
+ /**
13258
+ * Trigger a rumble effect on this pad. Resolves when the effect finishes.
13259
+ * Silent no-op when the pad is disconnected or the platform does not
13260
+ * support haptic feedback. Use {@link canVibrate} to detect support.
13261
+ */
13262
+ async vibrate(options) {
13263
+ const actuator = this._browserGamepad?.vibrationActuator;
13264
+ if (!actuator?.playEffect) {
13265
+ return;
13266
+ }
13267
+ await actuator.playEffect('dual-rumble', {
13268
+ duration: options.duration,
13269
+ weakMagnitude: options.weakMagnitude ?? 1,
13270
+ strongMagnitude: options.strongMagnitude ?? 1,
13271
+ startDelay: options.startDelay ?? 0,
13272
+ });
13182
13273
  }
13183
- get name() {
13184
- return this.info.name;
13274
+ /** Stop any active rumble on this pad. Silent no-op when unsupported. */
13275
+ stopVibration() {
13276
+ this._browserGamepad?.vibrationActuator?.reset?.();
13185
13277
  }
13186
- get label() {
13187
- return this.info.label;
13278
+ /**
13279
+ * Register a callback fired once when any of `channels` becomes active.
13280
+ * Listener survives disconnect/reconnect; call `.unbind()` on the
13281
+ * returned {@link InputBinding} to detach.
13282
+ */
13283
+ onStart(channel, callback, options) {
13284
+ const binding = this._createBinding(channel, options);
13285
+ binding.onStart.add(callback);
13286
+ return binding;
13188
13287
  }
13189
- get vendorId() {
13190
- return this.info.vendorId;
13288
+ /**
13289
+ * Register a callback fired every frame while any of `channels` is active.
13290
+ * Receives the channel value (0..1 for buttons, -1..1 for bipolar axes).
13291
+ */
13292
+ onActive(channel, callback, options) {
13293
+ const binding = this._createBinding(channel, options);
13294
+ binding.onActive.add(callback);
13295
+ return binding;
13191
13296
  }
13192
- get productId() {
13193
- return this.info.productId;
13297
+ /** Register a callback fired once when all of `channels` become inactive. */
13298
+ onStop(channel, callback, options) {
13299
+ const binding = this._createBinding(channel, options);
13300
+ binding.onStop.add(callback);
13301
+ return binding;
13194
13302
  }
13195
- get productKey() {
13196
- return this.info.productKey;
13303
+ /**
13304
+ * Register a callback fired when the input is released within
13305
+ * {@link InputBindingOptions.threshold} ms of activation (a "tap").
13306
+ */
13307
+ onTrigger(channel, callback, options) {
13308
+ const binding = this._createBinding(channel, options);
13309
+ binding.onTrigger.add(callback);
13310
+ return binding;
13197
13311
  }
13198
13312
  /**
13199
- * Replaces the gamepad's identity metadata.
13200
- * Called automatically during construction when a {@link BrowserGamepad} is
13201
- * provided; exposed publicly to allow runtime overrides.
13313
+ * Attach a physical browser gamepad to this slot. Called by
13314
+ * {@link InputManager} on connect.
13315
+ *
13316
+ * @internal
13202
13317
  */
13203
- setInfo(info) {
13204
- this.info = info;
13205
- return this;
13318
+ _bind(gamepad, definition) {
13319
+ this._browserGamepad = gamepad;
13320
+ this._mapping = definition.mapping;
13321
+ this._info = {
13322
+ name: definition.name,
13323
+ label: definition.descriptor.label,
13324
+ vendorId: definition.descriptor.vendorId,
13325
+ productId: definition.descriptor.productId,
13326
+ productKey: definition.descriptor.productKey,
13327
+ };
13328
+ this.onConnect.dispatch();
13206
13329
  }
13207
13330
  /**
13208
- * Attaches a live browser gamepad to this slot and dispatches {@link onConnect}
13209
- * if the slot was previously disconnected.
13331
+ * Detach the physical gamepad and clear its channels.
13332
+ *
13333
+ * @internal
13210
13334
  */
13211
- connect(gamepad) {
13212
- const wasConnected = this.connected;
13213
- this.browserGamepad = gamepad;
13214
- if (!wasConnected) {
13215
- this.onConnect.dispatch(this);
13335
+ _unbind() {
13336
+ if (this._browserGamepad === null) {
13337
+ return;
13216
13338
  }
13217
- return this;
13339
+ this._clearMappedChannels();
13340
+ this._browserGamepad = null;
13341
+ this._mapping = null;
13342
+ this._info = null;
13343
+ this.onDisconnect.dispatch();
13218
13344
  }
13219
13345
  /**
13220
- * Detaches the browser gamepad, zeros all mapped channels, and dispatches
13221
- * {@link onDisconnect}. No-op when already disconnected.
13346
+ * Detach the physical gamepad and clear channels without firing
13347
+ * {@link onDisconnect}. Used by {@link InputManager} during the compact
13348
+ * slot-shift to silently vacate a slot before another pad shifts into
13349
+ * its place; the disconnect signal is fired separately on the slot that
13350
+ * ends up empty after compaction.
13351
+ *
13352
+ * @internal
13222
13353
  */
13223
- disconnect() {
13224
- if (this.connected) {
13225
- this.browserGamepad = null;
13226
- this.clearMappedChannels();
13227
- this.onDisconnect.dispatch(this);
13354
+ _silentUnbind() {
13355
+ if (this._browserGamepad === null) {
13356
+ return;
13228
13357
  }
13229
- return this;
13358
+ this._clearMappedChannels();
13359
+ this._browserGamepad = null;
13360
+ this._mapping = null;
13361
+ this._info = null;
13230
13362
  }
13231
13363
  /**
13232
- * Samples the browser gamepad's current state and writes transformed values
13233
- * into the shared channel buffer, dispatching {@link onUpdate} for each channel
13234
- * whose value changed. Should be called once per frame by the engine's input loop.
13235
- * No-op when disconnected.
13364
+ * Dispatch this slot's {@link onDisconnect} signal without altering its
13365
+ * state. Used by {@link InputManager} after a compact-mode shift, when a
13366
+ * slot has already been emptied by {@link _rebindFrom} and now needs to
13367
+ * notify subscribers that its mailbox is no longer occupied.
13368
+ *
13369
+ * @internal
13370
+ */
13371
+ _dispatchDisconnect() {
13372
+ this.onDisconnect.dispatch();
13373
+ }
13374
+ /**
13375
+ * Reassign this slot to take over another slot's physical gamepad without
13376
+ * firing the full disconnect / connect cycle. Used by {@link InputManager}
13377
+ * when the `'compact'` slot strategy shuffles pads after a disconnect.
13378
+ *
13379
+ * @internal
13380
+ */
13381
+ _rebindFrom(other) {
13382
+ const gamepad = other._browserGamepad;
13383
+ const mapping = other._mapping;
13384
+ const info = other._info;
13385
+ this._clearMappedChannels();
13386
+ other._clearMappedChannels();
13387
+ other._browserGamepad = null;
13388
+ other._mapping = null;
13389
+ other._info = null;
13390
+ this._browserGamepad = gamepad;
13391
+ this._mapping = mapping;
13392
+ this._info = info;
13393
+ }
13394
+ /**
13395
+ * Sample the browser gamepad's current state and write transformed values
13396
+ * into the shared channel buffer, dispatching transition events when
13397
+ * channel activity crosses thresholds. Called once per frame by the
13398
+ * engine's input loop. No-op when disconnected.
13399
+ *
13400
+ * @internal
13236
13401
  */
13237
13402
  update() {
13238
- if (this.browserGamepad === null) {
13239
- return this;
13403
+ if (this._browserGamepad === null || this._mapping === null) {
13404
+ this._updateBindings();
13405
+ return;
13240
13406
  }
13241
- const channels = this.channelsValue;
13242
- const { buttons: gamepadButtons, axes: gamepadAxes } = this.browserGamepad;
13243
- for (const control of this.mappingValue.buttons) {
13244
- const offsetChannel = this.resolveChannelOffset(control.channel);
13245
- if (control.index < gamepadButtons.length) {
13246
- const value = control.transformValue(gamepadButtons[control.index].value) || 0;
13247
- if (channels[offsetChannel] !== value) {
13248
- channels[offsetChannel] = value;
13249
- this.onUpdate.dispatch(control.channel, value, this);
13250
- }
13407
+ const channels = this._channels;
13408
+ const { buttons: rawButtons, axes: rawAxes } = this._browserGamepad;
13409
+ for (const button of this._mapping.buttons) {
13410
+ if (button.index >= rawButtons.length) {
13411
+ continue;
13412
+ }
13413
+ const offset = this._resolveOffset(button.channel);
13414
+ const previous = channels[offset];
13415
+ const value = button.transformValue(rawButtons[button.index].value) || 0;
13416
+ if (previous === value) {
13417
+ continue;
13418
+ }
13419
+ channels[offset] = value;
13420
+ if (previous === 0 && value !== 0) {
13421
+ this.onButtonDown.dispatch(button, value);
13422
+ }
13423
+ else if (previous !== 0 && value === 0) {
13424
+ this.onButtonUp.dispatch(button, value);
13251
13425
  }
13252
13426
  }
13253
- for (const control of this.mappingValue.axes) {
13254
- const offsetChannel = this.resolveChannelOffset(control.channel);
13255
- if (control.index < gamepadAxes.length) {
13256
- const value = control.transformValue(gamepadAxes[control.index]) || 0;
13257
- if (channels[offsetChannel] !== value) {
13258
- channels[offsetChannel] = value;
13259
- this.onUpdate.dispatch(control.channel, value, this);
13260
- }
13427
+ for (const axis of this._mapping.axes) {
13428
+ if (axis.index >= rawAxes.length) {
13429
+ continue;
13261
13430
  }
13431
+ const offset = this._resolveOffset(axis.channel);
13432
+ const previous = channels[offset];
13433
+ const value = axis.transformValue(rawAxes[axis.index]) || 0;
13434
+ if (previous === value) {
13435
+ continue;
13436
+ }
13437
+ channels[offset] = value;
13438
+ this.onAxisChange.dispatch(axis, value);
13262
13439
  }
13263
- return this;
13264
- }
13265
- /** Zeroes all channel buffer entries that belong to this gamepad's mapping. */
13266
- clearChannels() {
13267
- this.clearMappedChannels();
13268
- return this;
13440
+ this._updateBindings();
13269
13441
  }
13270
13442
  /**
13271
- * Disconnects the gamepad, clears its channels, and destroys all signals.
13272
- * The instance must not be used after this call.
13443
+ * Tear down this gamepad slot. Called on application shutdown.
13273
13444
  */
13274
13445
  destroy() {
13275
- this.disconnect();
13276
- this.clearMappedChannels();
13446
+ for (const binding of Array.from(this._bindings)) {
13447
+ binding.unbind();
13448
+ }
13449
+ this._bindings.clear();
13450
+ this._unbind();
13277
13451
  this.onConnect.destroy();
13278
13452
  this.onDisconnect.destroy();
13279
- this.onUpdate.destroy();
13453
+ this.onButtonDown.destroy();
13454
+ this.onButtonUp.destroy();
13455
+ this.onAxisChange.destroy();
13456
+ this.onPadReassigned.destroy();
13280
13457
  }
13281
13458
  /**
13282
- * Converts a {@link GamepadChannel} to its absolute index in the shared channel
13283
- * buffer for this gamepad instance.
13459
+ * Convert a slot-relative channel value to its absolute index in the
13460
+ * shared channel buffer for this slot.
13284
13461
  */
13285
13462
  resolveChannelOffset(channel) {
13286
- return this.channelOffset + (channel ^ ChannelOffset.Gamepads);
13463
+ return this._resolveOffset(channel);
13287
13464
  }
13288
13465
  /**
13289
- * Converts a gamepad slot index and {@link GamepadChannel} to an absolute
13290
- * channel buffer offset without requiring a {@link Gamepad} instance.
13466
+ * Static counterpart to {@link Gamepad.resolveChannelOffset} resolves
13467
+ * an absolute channel-buffer offset for a given slot index without
13468
+ * requiring a Gamepad instance.
13291
13469
  */
13292
- static resolveChannelOffset(gamepadIndex, channel) {
13293
- return ChannelOffset.Gamepads + (gamepadIndex * ChannelSize.Gamepad) + (channel ^ ChannelOffset.Gamepads);
13470
+ static resolveChannelOffset(slot, channel) {
13471
+ return ChannelOffset.Gamepads + (slot * ChannelSize.Gamepad) + (channel ^ ChannelOffset.Gamepads);
13294
13472
  }
13295
- clearMappedChannels() {
13296
- for (const control of this.mappingValue.buttons) {
13297
- this.channelsValue[this.resolveChannelOffset(control.channel)] = 0;
13473
+ _resolveOffset(channel) {
13474
+ return this._channelOffset + (channel ^ ChannelOffset.Gamepads);
13475
+ }
13476
+ _clearMappedChannels() {
13477
+ if (this._mapping === null) {
13478
+ return;
13298
13479
  }
13299
- for (const control of this.mappingValue.axes) {
13300
- this.channelsValue[this.resolveChannelOffset(control.channel)] = 0;
13480
+ for (const button of this._mapping.buttons) {
13481
+ this._channels[this._resolveOffset(button.channel)] = 0;
13482
+ }
13483
+ for (const axis of this._mapping.axes) {
13484
+ this._channels[this._resolveOffset(axis.channel)] = 0;
13485
+ }
13486
+ }
13487
+ _createBinding(channel, options = {}) {
13488
+ const list = Array.isArray(channel) ? channel : [channel];
13489
+ const resolved = list.map((c) => this._resolveExternalChannel(c));
13490
+ const binding = new InputBinding(resolved, options, this._detacher);
13491
+ this._bindings.add(binding);
13492
+ return binding;
13493
+ }
13494
+ _resolveExternalChannel(channel) {
13495
+ // Keyboard channels are global (no slot offset). Gamepad channels
13496
+ // need slot-aware translation. Any channel value within the
13497
+ // gamepad-section maps through this pad's slot offset; others
13498
+ // (Keyboard, Pointer) pass through as-is.
13499
+ if (channel >= ChannelOffset.Gamepads && channel < ChannelOffset.Gamepads + ChannelSize.Category) {
13500
+ return this._resolveOffset(channel);
13501
+ }
13502
+ return channel;
13503
+ }
13504
+ _updateBindings() {
13505
+ for (const binding of this._bindings) {
13506
+ binding.update(this._channels);
13301
13507
  }
13302
13508
  }
13303
13509
  }
13304
13510
 
13511
+ const pointerCh = (offset) => (ChannelOffset.Pointers + offset);
13512
+ const slot = (s, field) => pointerCh(s * pointerSlotSize + field);
13305
13513
  /**
13306
13514
  * Bit flags accumulated on a {@link Pointer} between frames so consumers can
13307
13515
  * detect transient events (entered the canvas, was released, was cancelled)
@@ -13525,82 +13733,80 @@ class Pointer {
13525
13733
  }
13526
13734
  }
13527
13735
  /**
13528
- * Namespace merged onto the `Pointer` class to expose channel-offset constants.
13529
- * All members mirror `PointerChannel` so callers can write `Pointer.Active`, `Pointer.X`, etc.
13530
- *
13531
- * The un-prefixed members (Active, X, Y, …) address slot 0 (the primary pointer).
13532
- * For multi-touch access use `Pointer.Slot{N}Active / Slot{N}X / Slot{N}Y`, or compute:
13533
- * `Pointer.X + slotIndex * pointerSlotSize + channelOffset`.
13736
+ * Channel-identifier constants merged onto the `Pointer` class. The
13737
+ * un-prefixed members (Active, X, Y, …) address slot 0 (the primary
13738
+ * pointer). For multi-touch access use `Pointer.Slot{N}Active /
13739
+ * Slot{N}X / Slot{N}Y`.
13534
13740
  */
13535
13741
  // eslint-disable-next-line @typescript-eslint/no-namespace
13536
13742
  (function (Pointer) {
13537
13743
  /* eslint-disable @typescript-eslint/naming-convention */
13538
13744
  // --- Primary-pointer convenience aliases (slot 0) ---
13539
- Pointer.Active = PointerChannel.Active;
13540
- Pointer.X = PointerChannel.X;
13541
- Pointer.Y = PointerChannel.Y;
13542
- Pointer.Pressure = PointerChannel.Pressure;
13543
- Pointer.Width = PointerChannel.Width;
13544
- Pointer.Height = PointerChannel.Height;
13545
- Pointer.Twist = PointerChannel.Twist;
13546
- Pointer.TiltX = PointerChannel.TiltX;
13547
- Pointer.TiltY = PointerChannel.TiltY;
13548
- Pointer.Left = PointerChannel.Left;
13549
- Pointer.Right = PointerChannel.Right;
13550
- Pointer.Middle = PointerChannel.Middle;
13551
- Pointer.IsMouse = PointerChannel.IsMouse;
13552
- Pointer.IsTouch = PointerChannel.IsTouch;
13553
- Pointer.IsPen = PointerChannel.IsPen;
13554
- Pointer.IsPrimary = PointerChannel.IsPrimary;
13745
+ Pointer.Active = pointerCh(0);
13746
+ Pointer.X = pointerCh(1);
13747
+ Pointer.Y = pointerCh(2);
13748
+ Pointer.Pressure = pointerCh(3);
13749
+ Pointer.Width = pointerCh(4);
13750
+ Pointer.Height = pointerCh(5);
13751
+ Pointer.Twist = pointerCh(6);
13752
+ Pointer.TiltX = pointerCh(7);
13753
+ Pointer.TiltY = pointerCh(8);
13754
+ Pointer.Left = pointerCh(9);
13755
+ Pointer.Right = pointerCh(10);
13756
+ Pointer.Middle = pointerCh(11);
13757
+ Pointer.IsMouse = pointerCh(12);
13758
+ Pointer.IsTouch = pointerCh(13);
13759
+ Pointer.IsPen = pointerCh(14);
13760
+ Pointer.IsPrimary = pointerCh(15);
13555
13761
  // --- Per-slot Active/X/Y for multi-pointer access ---
13556
- Pointer.Slot0Active = PointerChannel.Slot0Active;
13557
- Pointer.Slot0X = PointerChannel.Slot0X;
13558
- Pointer.Slot0Y = PointerChannel.Slot0Y;
13559
- Pointer.Slot1Active = PointerChannel.Slot1Active;
13560
- Pointer.Slot1X = PointerChannel.Slot1X;
13561
- Pointer.Slot1Y = PointerChannel.Slot1Y;
13562
- Pointer.Slot2Active = PointerChannel.Slot2Active;
13563
- Pointer.Slot2X = PointerChannel.Slot2X;
13564
- Pointer.Slot2Y = PointerChannel.Slot2Y;
13565
- Pointer.Slot3Active = PointerChannel.Slot3Active;
13566
- Pointer.Slot3X = PointerChannel.Slot3X;
13567
- Pointer.Slot3Y = PointerChannel.Slot3Y;
13568
- Pointer.Slot4Active = PointerChannel.Slot4Active;
13569
- Pointer.Slot4X = PointerChannel.Slot4X;
13570
- Pointer.Slot4Y = PointerChannel.Slot4Y;
13571
- Pointer.Slot5Active = PointerChannel.Slot5Active;
13572
- Pointer.Slot5X = PointerChannel.Slot5X;
13573
- Pointer.Slot5Y = PointerChannel.Slot5Y;
13574
- Pointer.Slot6Active = PointerChannel.Slot6Active;
13575
- Pointer.Slot6X = PointerChannel.Slot6X;
13576
- Pointer.Slot6Y = PointerChannel.Slot6Y;
13577
- Pointer.Slot7Active = PointerChannel.Slot7Active;
13578
- Pointer.Slot7X = PointerChannel.Slot7X;
13579
- Pointer.Slot7Y = PointerChannel.Slot7Y;
13580
- Pointer.Slot8Active = PointerChannel.Slot8Active;
13581
- Pointer.Slot8X = PointerChannel.Slot8X;
13582
- Pointer.Slot8Y = PointerChannel.Slot8Y;
13583
- Pointer.Slot9Active = PointerChannel.Slot9Active;
13584
- Pointer.Slot9X = PointerChannel.Slot9X;
13585
- Pointer.Slot9Y = PointerChannel.Slot9Y;
13586
- Pointer.Slot10Active = PointerChannel.Slot10Active;
13587
- Pointer.Slot10X = PointerChannel.Slot10X;
13588
- Pointer.Slot10Y = PointerChannel.Slot10Y;
13589
- Pointer.Slot11Active = PointerChannel.Slot11Active;
13590
- Pointer.Slot11X = PointerChannel.Slot11X;
13591
- Pointer.Slot11Y = PointerChannel.Slot11Y;
13592
- Pointer.Slot12Active = PointerChannel.Slot12Active;
13593
- Pointer.Slot12X = PointerChannel.Slot12X;
13594
- Pointer.Slot12Y = PointerChannel.Slot12Y;
13595
- Pointer.Slot13Active = PointerChannel.Slot13Active;
13596
- Pointer.Slot13X = PointerChannel.Slot13X;
13597
- Pointer.Slot13Y = PointerChannel.Slot13Y;
13598
- Pointer.Slot14Active = PointerChannel.Slot14Active;
13599
- Pointer.Slot14X = PointerChannel.Slot14X;
13600
- Pointer.Slot14Y = PointerChannel.Slot14Y;
13601
- Pointer.Slot15Active = PointerChannel.Slot15Active;
13602
- Pointer.Slot15X = PointerChannel.Slot15X;
13603
- Pointer.Slot15Y = PointerChannel.Slot15Y;
13762
+ Pointer.Slot0Active = slot(0, 0);
13763
+ Pointer.Slot0X = slot(0, 1);
13764
+ Pointer.Slot0Y = slot(0, 2);
13765
+ Pointer.Slot1Active = slot(1, 0);
13766
+ Pointer.Slot1X = slot(1, 1);
13767
+ Pointer.Slot1Y = slot(1, 2);
13768
+ Pointer.Slot2Active = slot(2, 0);
13769
+ Pointer.Slot2X = slot(2, 1);
13770
+ Pointer.Slot2Y = slot(2, 2);
13771
+ Pointer.Slot3Active = slot(3, 0);
13772
+ Pointer.Slot3X = slot(3, 1);
13773
+ Pointer.Slot3Y = slot(3, 2);
13774
+ Pointer.Slot4Active = slot(4, 0);
13775
+ Pointer.Slot4X = slot(4, 1);
13776
+ Pointer.Slot4Y = slot(4, 2);
13777
+ Pointer.Slot5Active = slot(5, 0);
13778
+ Pointer.Slot5X = slot(5, 1);
13779
+ Pointer.Slot5Y = slot(5, 2);
13780
+ Pointer.Slot6Active = slot(6, 0);
13781
+ Pointer.Slot6X = slot(6, 1);
13782
+ Pointer.Slot6Y = slot(6, 2);
13783
+ Pointer.Slot7Active = slot(7, 0);
13784
+ Pointer.Slot7X = slot(7, 1);
13785
+ Pointer.Slot7Y = slot(7, 2);
13786
+ Pointer.Slot8Active = slot(8, 0);
13787
+ Pointer.Slot8X = slot(8, 1);
13788
+ Pointer.Slot8Y = slot(8, 2);
13789
+ Pointer.Slot9Active = slot(9, 0);
13790
+ Pointer.Slot9X = slot(9, 1);
13791
+ Pointer.Slot9Y = slot(9, 2);
13792
+ Pointer.Slot10Active = slot(10, 0);
13793
+ Pointer.Slot10X = slot(10, 1);
13794
+ Pointer.Slot10Y = slot(10, 2);
13795
+ Pointer.Slot11Active = slot(11, 0);
13796
+ Pointer.Slot11X = slot(11, 1);
13797
+ Pointer.Slot11Y = slot(11, 2);
13798
+ Pointer.Slot12Active = slot(12, 0);
13799
+ Pointer.Slot12X = slot(12, 1);
13800
+ Pointer.Slot12Y = slot(12, 2);
13801
+ Pointer.Slot13Active = slot(13, 0);
13802
+ Pointer.Slot13X = slot(13, 1);
13803
+ Pointer.Slot13Y = slot(13, 2);
13804
+ Pointer.Slot14Active = slot(14, 0);
13805
+ Pointer.Slot14X = slot(14, 1);
13806
+ Pointer.Slot14Y = slot(14, 2);
13807
+ Pointer.Slot15Active = slot(15, 0);
13808
+ Pointer.Slot15X = slot(15, 1);
13809
+ Pointer.Slot15Y = slot(15, 2);
13604
13810
  /* eslint-enable @typescript-eslint/naming-convention */
13605
13811
  })(Pointer || (Pointer = {}));
13606
13812
 
@@ -13742,91 +13948,98 @@ class GestureRecognizer {
13742
13948
  }
13743
13949
 
13744
13950
  /**
13745
- * Canonical channel identifiers for every control a standard gamepad can expose.
13746
- *
13747
- * Values are absolute offsets into the engine's shared {@link Float32Array} input
13748
- * channel buffer — each member equals `ChannelOffset.Gamepads + localIndex`.
13749
- * Use {@link Gamepad.resolveChannelOffset} to account for per-slot skew when
13750
- * reading a specific gamepad's data.
13951
+ * Single mappable button on a physical gamepad. Holds the raw browser
13952
+ * `Gamepad.buttons[]` index, the canonical channel the value is written to,
13953
+ * and the deadzone/inversion transform applied each frame by
13954
+ * {@link transformValue}.
13955
+ *
13956
+ * Used by concrete {@link GamepadMapping} subclasses to declare a device's
13957
+ * button layout. User code typically constructs these via
13958
+ * `new GamepadButton(rawIndex, GamepadButton.South)` only when authoring a
13959
+ * custom mapping.
13960
+ *
13961
+ * The static namespace exports (`GamepadButton.South`, `.East`, ...) carry
13962
+ * the canonical channel offsets used to address each button.
13751
13963
  */
13752
- var GamepadChannel;
13753
- (function (GamepadChannel) {
13754
- GamepadChannel[GamepadChannel["ButtonSouth"] = 512] = "ButtonSouth";
13755
- GamepadChannel[GamepadChannel["ButtonWest"] = 513] = "ButtonWest";
13756
- GamepadChannel[GamepadChannel["ButtonEast"] = 514] = "ButtonEast";
13757
- GamepadChannel[GamepadChannel["ButtonNorth"] = 515] = "ButtonNorth";
13758
- GamepadChannel[GamepadChannel["LeftShoulder"] = 516] = "LeftShoulder";
13759
- GamepadChannel[GamepadChannel["RightShoulder"] = 517] = "RightShoulder";
13760
- GamepadChannel[GamepadChannel["LeftTrigger"] = 518] = "LeftTrigger";
13761
- GamepadChannel[GamepadChannel["RightTrigger"] = 519] = "RightTrigger";
13762
- GamepadChannel[GamepadChannel["Select"] = 520] = "Select";
13763
- GamepadChannel[GamepadChannel["Start"] = 521] = "Start";
13764
- GamepadChannel[GamepadChannel["LeftStick"] = 522] = "LeftStick";
13765
- GamepadChannel[GamepadChannel["RightStick"] = 523] = "RightStick";
13766
- GamepadChannel[GamepadChannel["DPadUp"] = 524] = "DPadUp";
13767
- GamepadChannel[GamepadChannel["DPadDown"] = 525] = "DPadDown";
13768
- GamepadChannel[GamepadChannel["DPadLeft"] = 526] = "DPadLeft";
13769
- GamepadChannel[GamepadChannel["DPadRight"] = 527] = "DPadRight";
13770
- GamepadChannel[GamepadChannel["Guide"] = 528] = "Guide";
13771
- GamepadChannel[GamepadChannel["Share"] = 529] = "Share";
13772
- GamepadChannel[GamepadChannel["Capture"] = 530] = "Capture";
13773
- GamepadChannel[GamepadChannel["Touchpad"] = 531] = "Touchpad";
13774
- GamepadChannel[GamepadChannel["Paddle1"] = 532] = "Paddle1";
13775
- GamepadChannel[GamepadChannel["LeftStickLeft"] = 533] = "LeftStickLeft";
13776
- GamepadChannel[GamepadChannel["LeftStickRight"] = 534] = "LeftStickRight";
13777
- GamepadChannel[GamepadChannel["LeftStickUp"] = 535] = "LeftStickUp";
13778
- GamepadChannel[GamepadChannel["LeftStickDown"] = 536] = "LeftStickDown";
13779
- GamepadChannel[GamepadChannel["RightStickLeft"] = 537] = "RightStickLeft";
13780
- GamepadChannel[GamepadChannel["RightStickRight"] = 538] = "RightStickRight";
13781
- GamepadChannel[GamepadChannel["RightStickUp"] = 539] = "RightStickUp";
13782
- GamepadChannel[GamepadChannel["RightStickDown"] = 540] = "RightStickDown";
13783
- GamepadChannel[GamepadChannel["AuxiliaryAxis0Negative"] = 541] = "AuxiliaryAxis0Negative";
13784
- GamepadChannel[GamepadChannel["AuxiliaryAxis0Positive"] = 542] = "AuxiliaryAxis0Positive";
13785
- GamepadChannel[GamepadChannel["AuxiliaryAxis1Negative"] = 543] = "AuxiliaryAxis1Negative";
13786
- GamepadChannel[GamepadChannel["AuxiliaryAxis1Positive"] = 544] = "AuxiliaryAxis1Positive";
13787
- GamepadChannel[GamepadChannel["AuxiliaryAxis2Negative"] = 545] = "AuxiliaryAxis2Negative";
13788
- GamepadChannel[GamepadChannel["AuxiliaryAxis2Positive"] = 546] = "AuxiliaryAxis2Positive";
13789
- GamepadChannel[GamepadChannel["AuxiliaryAxis3Negative"] = 547] = "AuxiliaryAxis3Negative";
13790
- GamepadChannel[GamepadChannel["AuxiliaryAxis3Positive"] = 548] = "AuxiliaryAxis3Positive";
13791
- })(GamepadChannel || (GamepadChannel = {}));
13792
-
13793
- /**
13794
- * Represents a single mappable control — one button or one axis — on a physical gamepad.
13795
- *
13796
- * Stores the raw Gamepad API `index`, the target {@link GamepadChannel}, and the
13797
- * transform parameters (`invert`, `normalize`, `threshold`) applied each frame
13798
- * by {@link transformValue} before the value is written to the channel buffer.
13799
- */
13800
- class GamepadControl {
13964
+ class GamepadButton {
13801
13965
  index;
13802
13966
  channel;
13803
13967
  invert;
13804
- normalize;
13805
13968
  threshold;
13806
13969
  constructor(index, channel, options = {}) {
13807
13970
  this.index = index;
13808
13971
  this.channel = channel;
13809
13972
  this.invert = options.invert ?? false;
13810
- this.normalize = options.normalize ?? false;
13811
13973
  this.threshold = clamp(options.threshold ?? 0.2, 0, 1);
13812
13974
  }
13813
13975
  /**
13814
- * Applies the control's transform pipeline to a raw browser value.
13976
+ * Apply the button's transform pipeline to a raw browser button value
13977
+ * (typically `Gamepad.buttons[i].value`, in 0..1).
13815
13978
  *
13816
- * Pipeline: clamp to [-1, 1] → optional invert → optional normalize to [0, 1]
13817
- * → dead-zone (returns 0 when the absolute result is at or below `threshold`).
13979
+ * Pipeline: clamp to [0, 1] → optional invert → deadzone (returns 0 when
13980
+ * the result is at or below `threshold`).
13818
13981
  */
13819
13982
  transformValue(value) {
13820
- let result = clamp(value, -1, 1);
13983
+ let result = clamp(value, 0, 1);
13821
13984
  if (this.invert) {
13822
- result *= -1;
13823
- }
13824
- if (this.normalize) {
13825
- result = (result + 1) / 2;
13985
+ result = 1 - result;
13826
13986
  }
13827
13987
  return result > this.threshold ? result : 0;
13828
13988
  }
13829
13989
  }
13990
+ const button = (offset) => (ChannelOffset.Gamepads + offset);
13991
+ /* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/naming-convention */
13992
+ /**
13993
+ * Channel-identifier constants — same convention as `Pointer.X` /
13994
+ * `Keyboard.Space`. The first 32 slots of each gamepad sub-buffer are
13995
+ * reserved for buttons (24 named, 8 buffer for future / custom mappings).
13996
+ */
13997
+ (function (GamepadButton) {
13998
+ /** Bottom face button. Xbox=A, PlayStation=✕, Switch (horizontal Joy-Con)=B. Conventional usage: confirm / primary action / jump. */
13999
+ GamepadButton.South = button(0);
14000
+ /** Right face button. Xbox=B, PlayStation=○, Switch=A. Conventional usage: cancel / back / secondary. */
14001
+ GamepadButton.East = button(1);
14002
+ /** Left face button. Xbox=X, PlayStation=□, Switch=Y. Conventional usage: tertiary action. */
14003
+ GamepadButton.West = button(2);
14004
+ /** Top face button. Xbox=Y, PlayStation=△, Switch=X. Conventional usage: quaternary action. */
14005
+ GamepadButton.North = button(3);
14006
+ GamepadButton.LeftShoulder = button(4);
14007
+ GamepadButton.RightShoulder = button(5);
14008
+ /** Left trigger as a button (analog 0..1 reported through the same channel). */
14009
+ GamepadButton.LeftTrigger = button(6);
14010
+ /** Right trigger as a button. */
14011
+ GamepadButton.RightTrigger = button(7);
14012
+ /** Select / Back / Minus button. */
14013
+ GamepadButton.Select = button(8);
14014
+ /** Start / Options / Plus button. */
14015
+ GamepadButton.Start = button(9);
14016
+ /** Left analog stick click (L3). */
14017
+ GamepadButton.LeftStick = button(10);
14018
+ /** Right analog stick click (R3). */
14019
+ GamepadButton.RightStick = button(11);
14020
+ GamepadButton.DPadUp = button(12);
14021
+ GamepadButton.DPadDown = button(13);
14022
+ GamepadButton.DPadLeft = button(14);
14023
+ GamepadButton.DPadRight = button(15);
14024
+ /** Home / Guide / PS button. */
14025
+ GamepadButton.Guide = button(16);
14026
+ /** Share / Create button (PS4/PS5, Xbox Series). */
14027
+ GamepadButton.Share = button(17);
14028
+ /** Capture / Screenshot button (Switch, Xbox Series). */
14029
+ GamepadButton.Capture = button(18);
14030
+ /** Touchpad click (PlayStation). */
14031
+ GamepadButton.Touchpad = button(19);
14032
+ /** First paddle / extra button (Xbox Elite, Steam Controller, PS5 Edge, Steam Deck L4). */
14033
+ GamepadButton.Paddle1 = button(20);
14034
+ /** Second paddle / extra button (Xbox Elite, Steam Deck R4, PS5 Edge). */
14035
+ GamepadButton.Paddle2 = button(21);
14036
+ /** Third paddle / extra button (Xbox Elite, Steam Deck L5). */
14037
+ GamepadButton.Paddle3 = button(22);
14038
+ /** Fourth paddle / extra button (Xbox Elite, Steam Deck R5). */
14039
+ GamepadButton.Paddle4 = button(23);
14040
+ // Offsets 24..31 reserved for future named buttons / custom mapping use.
14041
+ })(GamepadButton || (GamepadButton = {}));
14042
+ /* eslint-enable @typescript-eslint/no-namespace, @typescript-eslint/naming-convention */
13830
14043
 
13831
14044
  /**
13832
14045
  * Discriminant tag identifying which device family a {@link GamepadMapping} belongs to.
@@ -13842,26 +14055,53 @@ var GamepadMappingFamily;
13842
14055
  GamepadMappingFamily["JoyConRight"] = "joyConRight";
13843
14056
  GamepadMappingFamily["GameCube"] = "gameCube";
13844
14057
  GamepadMappingFamily["SteamController"] = "steamController";
14058
+ GamepadMappingFamily["SteamDeck"] = "steamDeck";
13845
14059
  GamepadMappingFamily["ArcadeStick"] = "arcadeStick";
13846
14060
  })(GamepadMappingFamily || (GamepadMappingFamily = {}));
13847
14061
  /**
13848
14062
  * Abstract translation layer between the browser's raw {@link https://developer.mozilla.org/en-US/docs/Web/API/Gamepad Gamepad API}
13849
- * indices and ExoJS-canonical {@link GamepadChannel} controls.
14063
+ * indices and ExoJS-canonical channel buffers.
13850
14064
  *
13851
14065
  * Each concrete subclass encodes one device family's button/axis layout as
13852
- * ordered arrays of {@link GamepadControl} objects. The engine selects the
13853
- * appropriate mapping when a gamepad connects and uses it to route raw
13854
- * values to the correct input channels every frame.
14066
+ * ordered arrays of {@link GamepadButton} / {@link GamepadAxis} instances.
14067
+ * The engine selects the appropriate mapping when a gamepad connects and
14068
+ * uses it to route raw values to the correct input channels every frame.
13855
14069
  */
13856
14070
  class GamepadMapping {
13857
- /** Ordered list of button controls, indexed by the Gamepad API button index. */
14071
+ /** Ordered list of buttons, indexed by the Gamepad API button index. */
13858
14072
  buttons;
13859
- /** Ordered list of axis controls, indexed by the Gamepad API axis index. */
14073
+ /** Ordered list of axes, indexed by the Gamepad API axis index. */
13860
14074
  axes;
13861
14075
  constructor(buttons, axes) {
13862
14076
  this.buttons = buttons;
13863
14077
  this.axes = axes;
13864
14078
  }
14079
+ /**
14080
+ * Returns `true` when this mapping declares at least one button or axis
14081
+ * control that writes to `channel`. Use to detect device-specific
14082
+ * capabilities at runtime — e.g. before binding an input to a
14083
+ * right-stick channel that may not exist on a single Joy-Con.
14084
+ *
14085
+ * @example
14086
+ * ```ts
14087
+ * if (gamepad.mapping?.hasChannel(GamepadAxis.RightStickX)) {
14088
+ * pad.onActive(GamepadAxis.RightStickX, (v) => crosshair.x += v * 8);
14089
+ * }
14090
+ * ```
14091
+ */
14092
+ hasChannel(channel) {
14093
+ for (const button of this.buttons) {
14094
+ if (button.channel === channel) {
14095
+ return true;
14096
+ }
14097
+ }
14098
+ for (const axis of this.axes) {
14099
+ if (axis.channel === channel) {
14100
+ return true;
14101
+ }
14102
+ }
14103
+ return false;
14104
+ }
13865
14105
  /**
13866
14106
  * Releases all button and axis control references held by this mapping.
13867
14107
  * Call when the associated gamepad disconnects to allow garbage collection.
@@ -13870,33 +14110,8 @@ class GamepadMapping {
13870
14110
  this.buttons.length = 0;
13871
14111
  this.axes.length = 0;
13872
14112
  }
13873
- /**
13874
- * Converts an array of {@link GamepadControlDefinition} tuples into fully
13875
- * constructed {@link GamepadControl} instances.
13876
- * Shared factory used by all concrete mapping constructors.
13877
- */
13878
- static createControls(definitions) {
13879
- return definitions.map(([index, channel, options]) => new GamepadControl(index, channel, options));
13880
- }
13881
14113
  }
13882
14114
 
13883
- const arcadeStickButtonDefinitions = [
13884
- [0, GamepadChannel.ButtonSouth],
13885
- [1, GamepadChannel.ButtonEast],
13886
- [2, GamepadChannel.ButtonWest],
13887
- [3, GamepadChannel.ButtonNorth],
13888
- [4, GamepadChannel.LeftShoulder],
13889
- [5, GamepadChannel.RightShoulder],
13890
- [6, GamepadChannel.LeftTrigger],
13891
- [7, GamepadChannel.RightTrigger],
13892
- [8, GamepadChannel.Select],
13893
- [9, GamepadChannel.Start],
13894
- [12, GamepadChannel.DPadUp],
13895
- [13, GamepadChannel.DPadDown],
13896
- [14, GamepadChannel.DPadLeft],
13897
- [15, GamepadChannel.DPadRight],
13898
- [16, GamepadChannel.Guide],
13899
- ];
13900
14115
  /**
13901
14116
  * Mapping for generic arcade-stick controllers.
13902
14117
  *
@@ -13907,65 +14122,190 @@ const arcadeStickButtonDefinitions = [
13907
14122
  class ArcadeStickGamepadMapping extends GamepadMapping {
13908
14123
  family = GamepadMappingFamily.ArcadeStick;
13909
14124
  constructor() {
13910
- super(GamepadMapping.createControls(arcadeStickButtonDefinitions), []);
14125
+ super([
14126
+ new GamepadButton(0, GamepadButton.South),
14127
+ new GamepadButton(1, GamepadButton.East),
14128
+ new GamepadButton(2, GamepadButton.West),
14129
+ new GamepadButton(3, GamepadButton.North),
14130
+ new GamepadButton(4, GamepadButton.LeftShoulder),
14131
+ new GamepadButton(5, GamepadButton.RightShoulder),
14132
+ new GamepadButton(6, GamepadButton.LeftTrigger),
14133
+ new GamepadButton(7, GamepadButton.RightTrigger),
14134
+ new GamepadButton(8, GamepadButton.Select),
14135
+ new GamepadButton(9, GamepadButton.Start),
14136
+ new GamepadButton(12, GamepadButton.DPadUp),
14137
+ new GamepadButton(13, GamepadButton.DPadDown),
14138
+ new GamepadButton(14, GamepadButton.DPadLeft),
14139
+ new GamepadButton(15, GamepadButton.DPadRight),
14140
+ new GamepadButton(16, GamepadButton.Guide),
14141
+ ], []);
13911
14142
  }
13912
14143
  }
13913
14144
 
13914
- const genericDualAnalogButtonDefinitions = [
13915
- [0, GamepadChannel.ButtonSouth],
13916
- [1, GamepadChannel.ButtonEast],
13917
- [2, GamepadChannel.ButtonWest],
13918
- [3, GamepadChannel.ButtonNorth],
13919
- [4, GamepadChannel.LeftShoulder],
13920
- [5, GamepadChannel.RightShoulder],
13921
- [6, GamepadChannel.LeftTrigger],
13922
- [7, GamepadChannel.RightTrigger],
13923
- [8, GamepadChannel.Select],
13924
- [9, GamepadChannel.Start],
13925
- [10, GamepadChannel.LeftStick],
13926
- [11, GamepadChannel.RightStick],
13927
- [12, GamepadChannel.DPadUp],
13928
- [13, GamepadChannel.DPadDown],
13929
- [14, GamepadChannel.DPadLeft],
13930
- [15, GamepadChannel.DPadRight],
13931
- [16, GamepadChannel.Guide],
13932
- [17, GamepadChannel.Share],
13933
- [18, GamepadChannel.Capture],
13934
- [19, GamepadChannel.Touchpad],
13935
- [20, GamepadChannel.Paddle1],
13936
- ];
13937
- const genericDualAnalogAxisDefinitions = [
13938
- [0, GamepadChannel.LeftStickLeft, { invert: true }],
13939
- [0, GamepadChannel.LeftStickRight],
13940
- [1, GamepadChannel.LeftStickUp, { invert: true }],
13941
- [1, GamepadChannel.LeftStickDown],
13942
- [2, GamepadChannel.RightStickLeft, { invert: true }],
13943
- [2, GamepadChannel.RightStickRight],
13944
- [3, GamepadChannel.RightStickUp, { invert: true }],
13945
- [3, GamepadChannel.RightStickDown],
13946
- [4, GamepadChannel.AuxiliaryAxis0Negative, { invert: true }],
13947
- [4, GamepadChannel.AuxiliaryAxis0Positive],
13948
- [5, GamepadChannel.AuxiliaryAxis1Negative, { invert: true }],
13949
- [5, GamepadChannel.AuxiliaryAxis1Positive],
13950
- [6, GamepadChannel.AuxiliaryAxis2Negative, { invert: true }],
13951
- [6, GamepadChannel.AuxiliaryAxis2Positive],
13952
- [7, GamepadChannel.AuxiliaryAxis3Negative, { invert: true }],
13953
- [7, GamepadChannel.AuxiliaryAxis3Positive],
13954
- ];
14145
+ /**
14146
+ * Single mappable analog axis on a physical gamepad. Holds the raw browser
14147
+ * `Gamepad.axes[]` index, the canonical channel the value is written to, and
14148
+ * the transform pipeline applied each frame by {@link transformValue}.
14149
+ *
14150
+ * Direction-split axis channels (e.g. `LeftStickLeft`, `LeftStickRight`)
14151
+ * live in the 0..1 range — set `invert: true` on the negative half so it
14152
+ * reads positive when pushed in its direction.
14153
+ *
14154
+ * Aggregate channels (e.g. `LeftStickX`, `LeftStickY`) live in the full
14155
+ * -1..1 range — set `bipolar: true` to preserve sign through the pipeline.
14156
+ *
14157
+ * The static namespace exports (`GamepadAxis.LeftStickLeft`,
14158
+ * `.LeftStickX`, ...) carry the canonical channel offsets used to address
14159
+ * each axis.
14160
+ */
14161
+ class GamepadAxis {
14162
+ index;
14163
+ channel;
14164
+ invert;
14165
+ normalize;
14166
+ threshold;
14167
+ bipolar;
14168
+ constructor(index, channel, options = {}) {
14169
+ this.index = index;
14170
+ this.channel = channel;
14171
+ this.invert = options.invert ?? false;
14172
+ this.normalize = options.normalize ?? false;
14173
+ this.threshold = clamp(options.threshold ?? 0.2, 0, 1);
14174
+ this.bipolar = options.bipolar ?? false;
14175
+ }
14176
+ /**
14177
+ * Apply the axis transform pipeline to a raw browser axis value
14178
+ * (typically `Gamepad.axes[i]`, in -1..1).
14179
+ *
14180
+ * Pipeline: clamp to [-1, 1] → optional invert → optional normalize to
14181
+ * [0, 1] bipolar passthrough OR deadzone (returns 0 when the absolute
14182
+ * value is at or below `threshold`).
14183
+ */
14184
+ transformValue(value) {
14185
+ let result = clamp(value, -1, 1);
14186
+ if (this.invert) {
14187
+ result *= -1;
14188
+ }
14189
+ if (this.normalize) {
14190
+ result = (result + 1) / 2;
14191
+ }
14192
+ if (this.bipolar) {
14193
+ return Math.abs(result) > this.threshold ? result : 0;
14194
+ }
14195
+ return result > this.threshold ? result : 0;
14196
+ }
14197
+ }
14198
+ const axis = (offset) => (ChannelOffset.Gamepads + offset);
14199
+ /* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/naming-convention */
14200
+ /**
14201
+ * Channel-identifier constants. The axis section starts after the 32-slot
14202
+ * button block: 24 named axes (offsets 32..55) plus 8 reserved slots
14203
+ * (offsets 56..63).
14204
+ */
14205
+ (function (GamepadAxis) {
14206
+ // Direction-split (0..1, "buttons-style").
14207
+ GamepadAxis.LeftStickLeft = axis(32);
14208
+ GamepadAxis.LeftStickRight = axis(33);
14209
+ GamepadAxis.LeftStickUp = axis(34);
14210
+ GamepadAxis.LeftStickDown = axis(35);
14211
+ GamepadAxis.RightStickLeft = axis(36);
14212
+ GamepadAxis.RightStickRight = axis(37);
14213
+ GamepadAxis.RightStickUp = axis(38);
14214
+ GamepadAxis.RightStickDown = axis(39);
14215
+ // Aggregate (-1..1, "stick-style").
14216
+ /** Signed left-stick X axis (-1..1). Negative = left, positive = right. */
14217
+ GamepadAxis.LeftStickX = axis(40);
14218
+ /** Signed left-stick Y axis (-1..1). Negative = up (screen-up), positive = down. */
14219
+ GamepadAxis.LeftStickY = axis(41);
14220
+ /** Signed right-stick X axis (-1..1). */
14221
+ GamepadAxis.RightStickX = axis(42);
14222
+ /** Signed right-stick Y axis (-1..1). */
14223
+ GamepadAxis.RightStickY = axis(43);
14224
+ // Touchpad XY (PlayStation 4/5, Steam Deck, dual-touchpad Steam-class hardware).
14225
+ /** Primary touchpad X (0..1, left to right). PlayStation, Steam Deck (left pad), Steam Controller. */
14226
+ GamepadAxis.TouchpadX = axis(44);
14227
+ /** Primary touchpad Y (0..1, top to bottom). */
14228
+ GamepadAxis.TouchpadY = axis(45);
14229
+ /** Secondary touchpad X (0..1). Steam Deck (right pad), other dual-touchpad hardware. */
14230
+ GamepadAxis.Touchpad2X = axis(46);
14231
+ /** Secondary touchpad Y (0..1). */
14232
+ GamepadAxis.Touchpad2Y = axis(47);
14233
+ // Auxiliary axes (4 bipolar axes split into 8 non-negative channels).
14234
+ GamepadAxis.AuxiliaryAxis0Negative = axis(48);
14235
+ GamepadAxis.AuxiliaryAxis0Positive = axis(49);
14236
+ GamepadAxis.AuxiliaryAxis1Negative = axis(50);
14237
+ GamepadAxis.AuxiliaryAxis1Positive = axis(51);
14238
+ GamepadAxis.AuxiliaryAxis2Negative = axis(52);
14239
+ GamepadAxis.AuxiliaryAxis2Positive = axis(53);
14240
+ GamepadAxis.AuxiliaryAxis3Negative = axis(54);
14241
+ GamepadAxis.AuxiliaryAxis3Positive = axis(55);
14242
+ // Offsets 56..63 reserved for future named axes / custom mapping use.
14243
+ })(GamepadAxis || (GamepadAxis = {}));
14244
+ /* eslint-enable @typescript-eslint/no-namespace, @typescript-eslint/naming-convention */
14245
+
13955
14246
  /**
13956
14247
  * Baseline mapping for dual-analog controllers that follow the standard
13957
14248
  * W3C Gamepad API layout (axes 0–3 for both sticks, axes 4–7 auxiliary).
13958
14249
  *
13959
- * Each signed axis is split into two directional channels — one for the
13960
- * negative half and one for the positive half — with the negative channel
13961
- * inverted so all channel values are non-negative during normal use.
14250
+ * Each signed stick axis is exposed three ways for ergonomic binding:
14251
+ * - Two direction-split, non-negative channels (e.g. `LeftStickLeft` /
14252
+ * `LeftStickRight`) for "buttons-style" subscriptions.
14253
+ * - One signed aggregate channel (e.g. `LeftStickX`) for direct -1..1
14254
+ * consumption — useful for movement or aiming.
14255
+ *
13962
14256
  * Device-specific subclasses (Xbox, PlayStation, Switch Pro, etc.) inherit
13963
14257
  * this layout and override only {@link GamepadMapping.family}.
13964
14258
  */
13965
14259
  class GenericDualAnalogGamepadMapping extends GamepadMapping {
13966
14260
  family = GamepadMappingFamily.GenericDualAnalog;
13967
14261
  constructor() {
13968
- super(GamepadMapping.createControls(genericDualAnalogButtonDefinitions), GamepadMapping.createControls(genericDualAnalogAxisDefinitions));
14262
+ super([
14263
+ new GamepadButton(0, GamepadButton.South),
14264
+ new GamepadButton(1, GamepadButton.East),
14265
+ new GamepadButton(2, GamepadButton.West),
14266
+ new GamepadButton(3, GamepadButton.North),
14267
+ new GamepadButton(4, GamepadButton.LeftShoulder),
14268
+ new GamepadButton(5, GamepadButton.RightShoulder),
14269
+ new GamepadButton(6, GamepadButton.LeftTrigger),
14270
+ new GamepadButton(7, GamepadButton.RightTrigger),
14271
+ new GamepadButton(8, GamepadButton.Select),
14272
+ new GamepadButton(9, GamepadButton.Start),
14273
+ new GamepadButton(10, GamepadButton.LeftStick),
14274
+ new GamepadButton(11, GamepadButton.RightStick),
14275
+ new GamepadButton(12, GamepadButton.DPadUp),
14276
+ new GamepadButton(13, GamepadButton.DPadDown),
14277
+ new GamepadButton(14, GamepadButton.DPadLeft),
14278
+ new GamepadButton(15, GamepadButton.DPadRight),
14279
+ new GamepadButton(16, GamepadButton.Guide),
14280
+ new GamepadButton(17, GamepadButton.Share),
14281
+ new GamepadButton(18, GamepadButton.Capture),
14282
+ new GamepadButton(19, GamepadButton.Touchpad),
14283
+ new GamepadButton(20, GamepadButton.Paddle1),
14284
+ ], [
14285
+ // Direction-split (0..1).
14286
+ new GamepadAxis(0, GamepadAxis.LeftStickLeft, { invert: true }),
14287
+ new GamepadAxis(0, GamepadAxis.LeftStickRight),
14288
+ new GamepadAxis(1, GamepadAxis.LeftStickUp, { invert: true }),
14289
+ new GamepadAxis(1, GamepadAxis.LeftStickDown),
14290
+ new GamepadAxis(2, GamepadAxis.RightStickLeft, { invert: true }),
14291
+ new GamepadAxis(2, GamepadAxis.RightStickRight),
14292
+ new GamepadAxis(3, GamepadAxis.RightStickUp, { invert: true }),
14293
+ new GamepadAxis(3, GamepadAxis.RightStickDown),
14294
+ // Aggregate signed channels (-1..1).
14295
+ new GamepadAxis(0, GamepadAxis.LeftStickX, { bipolar: true }),
14296
+ new GamepadAxis(1, GamepadAxis.LeftStickY, { bipolar: true }),
14297
+ new GamepadAxis(2, GamepadAxis.RightStickX, { bipolar: true }),
14298
+ new GamepadAxis(3, GamepadAxis.RightStickY, { bipolar: true }),
14299
+ // Auxiliary axes (4 bipolar physical axes split into 8 half-channels).
14300
+ new GamepadAxis(4, GamepadAxis.AuxiliaryAxis0Negative, { invert: true }),
14301
+ new GamepadAxis(4, GamepadAxis.AuxiliaryAxis0Positive),
14302
+ new GamepadAxis(5, GamepadAxis.AuxiliaryAxis1Negative, { invert: true }),
14303
+ new GamepadAxis(5, GamepadAxis.AuxiliaryAxis1Positive),
14304
+ new GamepadAxis(6, GamepadAxis.AuxiliaryAxis2Negative, { invert: true }),
14305
+ new GamepadAxis(6, GamepadAxis.AuxiliaryAxis2Positive),
14306
+ new GamepadAxis(7, GamepadAxis.AuxiliaryAxis3Negative, { invert: true }),
14307
+ new GamepadAxis(7, GamepadAxis.AuxiliaryAxis3Positive),
14308
+ ]);
13969
14309
  }
13970
14310
  }
13971
14311
 
@@ -13983,29 +14323,83 @@ class GameCubeGamepadMapping extends GenericDualAnalogGamepadMapping {
13983
14323
  }
13984
14324
 
13985
14325
  /**
13986
- * Mapping for the Nintendo Switch Joy-Con (L) held horizontally or used as
13987
- * a standalone controller.
13988
- *
13989
- * Inherits the {@link GenericDualAnalogGamepadMapping} layout. Because the
13990
- * Joy-Con Left has only one physical stick, right-stick channels will never
13991
- * receive input when this mapping is active. The SL/SR shoulder buttons are
13992
- * surfaced through the standard LeftShoulder/RightShoulder channels.
14326
+ * Mapping for the Nintendo Joy-Con (L) held horizontally as a solo controller.
14327
+ *
14328
+ * Declares only channels that physically exist on the device — one stick
14329
+ * (mapped to {@link GamepadAxis.LeftStickX} / `LeftStickY` and the
14330
+ * direction-split equivalents), four face buttons, the SL/SR inner shoulders
14331
+ * (routed through the standard shoulder channels), Minus, the Capture
14332
+ * button, and the stick-click.
14333
+ *
14334
+ * Right-stick channels, triggers, Plus/Home, Touchpad, paddles, and
14335
+ * auxiliary axes are intentionally absent. Use
14336
+ * {@link GamepadMapping.hasChannel} to detect availability before binding
14337
+ * inputs that may not exist on every device family.
13993
14338
  */
13994
- class JoyConLeftGamepadMapping extends GenericDualAnalogGamepadMapping {
14339
+ class JoyConLeftGamepadMapping extends GamepadMapping {
13995
14340
  family = GamepadMappingFamily.JoyConLeft;
14341
+ constructor() {
14342
+ super([
14343
+ new GamepadButton(0, GamepadButton.South),
14344
+ new GamepadButton(1, GamepadButton.East),
14345
+ new GamepadButton(2, GamepadButton.West),
14346
+ new GamepadButton(3, GamepadButton.North),
14347
+ // Inner SL/SR shoulders — routed through the standard shoulder channels.
14348
+ new GamepadButton(4, GamepadButton.LeftShoulder),
14349
+ new GamepadButton(5, GamepadButton.RightShoulder),
14350
+ new GamepadButton(8, GamepadButton.Select), // Minus
14351
+ new GamepadButton(10, GamepadButton.LeftStick), // stick click
14352
+ new GamepadButton(16, GamepadButton.Capture),
14353
+ ], [
14354
+ // Single physical stick — surfaced through the LeftStick channels so
14355
+ // gamepad-agnostic code that binds to "the stick" works regardless of
14356
+ // which Joy-Con is held.
14357
+ new GamepadAxis(0, GamepadAxis.LeftStickLeft, { invert: true }),
14358
+ new GamepadAxis(0, GamepadAxis.LeftStickRight),
14359
+ new GamepadAxis(1, GamepadAxis.LeftStickUp, { invert: true }),
14360
+ new GamepadAxis(1, GamepadAxis.LeftStickDown),
14361
+ new GamepadAxis(0, GamepadAxis.LeftStickX, { bipolar: true }),
14362
+ new GamepadAxis(1, GamepadAxis.LeftStickY, { bipolar: true }),
14363
+ ]);
14364
+ }
13996
14365
  }
13997
14366
 
13998
14367
  /**
13999
- * Mapping for the Nintendo Switch Joy-Con (R) held horizontally or used as
14000
- * a standalone controller.
14001
- *
14002
- * Inherits the {@link GenericDualAnalogGamepadMapping} layout. Because the
14003
- * Joy-Con Right has only one physical stick, left-stick channels will never
14004
- * receive input when this mapping is active. The SL/SR shoulder buttons are
14005
- * surfaced through the standard LeftShoulder/RightShoulder channels.
14368
+ * Mapping for the Nintendo Joy-Con (R) held horizontally as a solo controller.
14369
+ *
14370
+ * Declares only channels that physically exist on the device — one stick
14371
+ * (mapped to the LeftStick channels to match the W3C standard layout for the
14372
+ * lone reported stick regardless of which Joy-Con reports it), four face
14373
+ * buttons, the SL/SR inner shoulders (routed through the standard shoulder
14374
+ * channels), Plus, the Home button, and the stick-click.
14375
+ *
14376
+ * Right-stick channels, triggers, Minus/Capture, Touchpad, paddles, and
14377
+ * auxiliary axes are intentionally absent. Use
14378
+ * {@link GamepadMapping.hasChannel} to detect availability before binding
14379
+ * inputs that may not exist on every device family.
14006
14380
  */
14007
- class JoyConRightGamepadMapping extends GenericDualAnalogGamepadMapping {
14381
+ class JoyConRightGamepadMapping extends GamepadMapping {
14008
14382
  family = GamepadMappingFamily.JoyConRight;
14383
+ constructor() {
14384
+ super([
14385
+ new GamepadButton(0, GamepadButton.South),
14386
+ new GamepadButton(1, GamepadButton.East),
14387
+ new GamepadButton(2, GamepadButton.West),
14388
+ new GamepadButton(3, GamepadButton.North),
14389
+ new GamepadButton(4, GamepadButton.LeftShoulder),
14390
+ new GamepadButton(5, GamepadButton.RightShoulder),
14391
+ new GamepadButton(9, GamepadButton.Start), // Plus
14392
+ new GamepadButton(10, GamepadButton.LeftStick), // stick click
14393
+ new GamepadButton(16, GamepadButton.Guide), // Home
14394
+ ], [
14395
+ new GamepadAxis(0, GamepadAxis.LeftStickLeft, { invert: true }),
14396
+ new GamepadAxis(0, GamepadAxis.LeftStickRight),
14397
+ new GamepadAxis(1, GamepadAxis.LeftStickUp, { invert: true }),
14398
+ new GamepadAxis(1, GamepadAxis.LeftStickDown),
14399
+ new GamepadAxis(0, GamepadAxis.LeftStickX, { bipolar: true }),
14400
+ new GamepadAxis(1, GamepadAxis.LeftStickY, { bipolar: true }),
14401
+ ]);
14402
+ }
14009
14403
  }
14010
14404
 
14011
14405
  /**
@@ -14032,6 +14426,76 @@ class SteamControllerGamepadMapping extends GenericDualAnalogGamepadMapping {
14032
14426
  family = GamepadMappingFamily.SteamController;
14033
14427
  }
14034
14428
 
14429
+ /**
14430
+ * Mapping for the Valve Steam Deck (and the new Valve Controller via vendor
14431
+ * fallback) when its raw HID gamepad is exposed directly to the browser —
14432
+ * i.e. when Steam Input is *not* intercepting the device. With Steam Input
14433
+ * intercepting, the device appears as `28de:11ff` "Steam Virtual Gamepad"
14434
+ * with a standard W3C layout instead, and is routed to
14435
+ * {@link GenericDualAnalogGamepadMapping}.
14436
+ *
14437
+ * The raw layout is non-standard: face buttons live at indices 3-6 (not the
14438
+ * W3C-standard 0-3), the D-pad lives at indices 16-19, paddles at 20-23, and
14439
+ * triggers report as analog axes 8/9 rather than buttons 6/7. Indices are
14440
+ * derived from the Linux SDL_GameControllerDB entry for `Valve Steam Deck`.
14441
+ */
14442
+ class SteamDeckGamepadMapping extends GamepadMapping {
14443
+ family = GamepadMappingFamily.SteamDeck;
14444
+ constructor() {
14445
+ super([
14446
+ // Quick Access (Steam Deck "..." button) → mapped to Capture as
14447
+ // the closest semantic match in the canonical channel set.
14448
+ new GamepadButton(2, GamepadButton.Capture),
14449
+ // Face cluster — non-standard offsets.
14450
+ new GamepadButton(3, GamepadButton.South),
14451
+ new GamepadButton(4, GamepadButton.East),
14452
+ new GamepadButton(5, GamepadButton.West),
14453
+ new GamepadButton(6, GamepadButton.North),
14454
+ new GamepadButton(7, GamepadButton.LeftShoulder),
14455
+ new GamepadButton(8, GamepadButton.RightShoulder),
14456
+ // View / Menu / Steam buttons.
14457
+ new GamepadButton(11, GamepadButton.Select),
14458
+ new GamepadButton(12, GamepadButton.Start),
14459
+ new GamepadButton(13, GamepadButton.Guide),
14460
+ // Stick clicks.
14461
+ new GamepadButton(14, GamepadButton.LeftStick),
14462
+ new GamepadButton(15, GamepadButton.RightStick),
14463
+ // D-pad.
14464
+ new GamepadButton(16, GamepadButton.DPadUp),
14465
+ new GamepadButton(17, GamepadButton.DPadDown),
14466
+ new GamepadButton(18, GamepadButton.DPadLeft),
14467
+ new GamepadButton(19, GamepadButton.DPadRight),
14468
+ // Back paddles. SDL labels them paddle1=R4, paddle2=L4,
14469
+ // paddle3=R5, paddle4=L5; we expose them in canonical
14470
+ // L4/R4/L5/R5 order via Paddle1..Paddle4.
14471
+ new GamepadButton(20, GamepadButton.Paddle2),
14472
+ new GamepadButton(21, GamepadButton.Paddle1),
14473
+ new GamepadButton(22, GamepadButton.Paddle4),
14474
+ new GamepadButton(23, GamepadButton.Paddle3),
14475
+ ], [
14476
+ // Sticks — direction-split (0..1).
14477
+ new GamepadAxis(0, GamepadAxis.LeftStickLeft, { invert: true }),
14478
+ new GamepadAxis(0, GamepadAxis.LeftStickRight),
14479
+ new GamepadAxis(1, GamepadAxis.LeftStickUp, { invert: true }),
14480
+ new GamepadAxis(1, GamepadAxis.LeftStickDown),
14481
+ new GamepadAxis(2, GamepadAxis.RightStickLeft, { invert: true }),
14482
+ new GamepadAxis(2, GamepadAxis.RightStickRight),
14483
+ new GamepadAxis(3, GamepadAxis.RightStickUp, { invert: true }),
14484
+ new GamepadAxis(3, GamepadAxis.RightStickDown),
14485
+ // Sticks — aggregate signed (-1..1).
14486
+ new GamepadAxis(0, GamepadAxis.LeftStickX, { bipolar: true }),
14487
+ new GamepadAxis(1, GamepadAxis.LeftStickY, { bipolar: true }),
14488
+ new GamepadAxis(2, GamepadAxis.RightStickX, { bipolar: true }),
14489
+ new GamepadAxis(3, GamepadAxis.RightStickY, { bipolar: true }),
14490
+ // Triggers as analog axes (Steam Deck reports them as a8/a9,
14491
+ // not buttons). Browsers expose -1..+1; normalize to 0..1
14492
+ // for the canonical trigger channels.
14493
+ new GamepadAxis(8, GamepadAxis.AuxiliaryAxis0Positive, { normalize: true }),
14494
+ new GamepadAxis(9, GamepadAxis.AuxiliaryAxis1Positive, { normalize: true }),
14495
+ ]);
14496
+ }
14497
+ }
14498
+
14035
14499
  /**
14036
14500
  * Mapping for the Nintendo Switch Pro Controller connected via USB or
14037
14501
  * Bluetooth.
@@ -14222,6 +14686,8 @@ const exactDeviceDefinitions = [
14222
14686
  createStaticGamepadDefinition('Switch 2 Pro Controller', () => new SwitchProGamepadMapping(), '057e:2069'),
14223
14687
  createStaticGamepadDefinition('Switch 2 GameCube Controller', () => new GameCubeGamepadMapping(), '057e:2073'),
14224
14688
  createStaticGamepadDefinition('Steam Controller', () => new SteamControllerGamepadMapping(), ['28de:1102', '28de:1142']),
14689
+ createStaticGamepadDefinition('Steam Virtual Gamepad', () => new GenericDualAnalogGamepadMapping(), '28de:11ff'),
14690
+ createStaticGamepadDefinition('Steam Deck', () => new SteamDeckGamepadMapping(), '28de:1205'),
14225
14691
  createStaticGamepadDefinition('F310 Gamepad', () => new GenericDualAnalogGamepadMapping(), '046d:c216'),
14226
14692
  createStaticGamepadDefinition('F710 Gamepad', () => new GenericDualAnalogGamepadMapping(), ['046d:c219', '046d:c21f']),
14227
14693
  createStaticGamepadDefinition('8BitDo P30 Controller', () => new GenericDualAnalogGamepadMapping(), ['2dc8:5107', '2dc8:5108']),
@@ -14244,6 +14710,7 @@ const exactDeviceDefinitions = [
14244
14710
  const vendorFallbackDefinitions = [
14245
14711
  createStaticGamepadDefinition('Microsoft Controller', () => new XboxGamepadMapping(), '045e'),
14246
14712
  createStaticGamepadDefinition('Sony Controller', () => new PlayStationGamepadMapping(), '054c'),
14713
+ createStaticGamepadDefinition('Valve Controller', () => new SteamDeckGamepadMapping(), '28de'),
14247
14714
  ];
14248
14715
  const genericFallbackDefinition = createStaticGamepadDefinition('Generic Gamepad', () => new GenericDualAnalogGamepadMapping());
14249
14716
  /**
@@ -14259,6 +14726,7 @@ const builtInGamepadDefinitions = [
14259
14726
  genericFallbackDefinition,
14260
14727
  ];
14261
14728
 
14729
+ const gamepadSlots = 4;
14262
14730
  var InputManagerFlag;
14263
14731
  (function (InputManagerFlag) {
14264
14732
  InputManagerFlag[InputManagerFlag["None"] = 0] = "None";
@@ -14275,10 +14743,11 @@ var InputManagerFlag;
14275
14743
  * rotate / long-press).
14276
14744
  *
14277
14745
  * All raw inputs are written into a shared `Float32Array` channel buffer.
14278
- * Bind {@link Input} instances to channel indices to react to specific
14279
- * keys, gamepad controls, or pointer slots without rolling your own event
14280
- * routing. Direct subscribers can use the per-event Signals
14281
- * (`onKeyDown`, `onPointerDown`, `onGamepadConnected`, `onPinch`, …).
14746
+ * Bind input listeners via the {@link onTrigger} / {@link onActive} /
14747
+ * {@link onStart} / {@link onStop} factory methods (or via
14748
+ * {@link Gamepad.onTrigger}-style methods on individual pads), or
14749
+ * subscribe to the signal-style notifications
14750
+ * (`onKeyDown`, `onPointerDown`, `onGamepadConnected`, `onAnyGamepadButtonDown`, …).
14282
14751
  *
14283
14752
  * Driven each frame by {@link Application.update}; constructed
14284
14753
  * automatically — you do not instantiate this class yourself.
@@ -14286,18 +14755,19 @@ var InputManagerFlag;
14286
14755
  class InputManager {
14287
14756
  canvas;
14288
14757
  channels = new Float32Array(ChannelSize.Container);
14289
- inputs = new Set();
14290
14758
  pointers = {};
14291
- gamepadsValue = [];
14292
- gamepadsByIndex = new Map();
14293
- gamepadSlotsActive = new Uint8Array(ChannelSize.Category / ChannelSize.Gamepad);
14759
+ _gamepads;
14760
+ gamepadsByBrowserIndex = new Map();
14761
+ bindings = new Set();
14762
+ bindingDetacher = { detach: (binding) => { this.bindings.delete(binding); } };
14294
14763
  wheelOffset = new Vector();
14295
14764
  flags = new Flags();
14296
14765
  channelsPressed = [];
14297
14766
  channelsReleased = [];
14298
14767
  gamepadDefinitions;
14768
+ slotStrategy;
14299
14769
  // Slot allocation for unified pointer tracking (mouse / touch / pen).
14300
- pointerSlots = new Map(); // pointerId → slotIndex
14770
+ pointerSlots = new Map();
14301
14771
  freeSlots = Array.from({ length: maxPointers }, (_, i) => i);
14302
14772
  gestureRecognizer;
14303
14773
  canvasFocusedValue;
@@ -14326,9 +14796,22 @@ class InputManager {
14326
14796
  onMouseWheel = new Signal();
14327
14797
  onKeyDown = new Signal();
14328
14798
  onKeyUp = new Signal();
14799
+ /** Fires when a physical pad connects to any slot. */
14329
14800
  onGamepadConnected = new Signal();
14801
+ /** Fires when a physical pad disconnects from any slot. */
14330
14802
  onGamepadDisconnected = new Signal();
14331
- onGamepadUpdated = new Signal();
14803
+ /**
14804
+ * Fires when a `'compact'`-strategy disconnect shifts a higher-numbered
14805
+ * slot's pad into a lower one. Dispatched once per moved pad with the
14806
+ * destination slot and the slot index it came from.
14807
+ */
14808
+ onAnyGamepadReassigned = new Signal();
14809
+ /** Fires whenever any pad reports a button press transition. */
14810
+ onAnyGamepadButtonDown = new Signal();
14811
+ /** Fires whenever any pad reports a button release transition. */
14812
+ onAnyGamepadButtonUp = new Signal();
14813
+ /** Fires whenever any pad reports an axis value change. */
14814
+ onAnyGamepadAxisChange = new Signal();
14332
14815
  /** Fires on every two-touch-pointer move where the distance between them changed. `scale` > 1 = spreading, < 1 = pinching. */
14333
14816
  onPinch = new Signal();
14334
14817
  /** Fires on every two-touch-pointer move where the angle between them changed. `angleDelta` is in radians. */
@@ -14336,16 +14819,25 @@ class InputManager {
14336
14819
  /** Fires when a pointer has been held without significant movement for ≥ 500 ms. */
14337
14820
  onLongPress = new Signal();
14338
14821
  constructor(app) {
14339
- const { gamepadDefinitions = [], pointerDistanceThreshold } = app.options;
14822
+ const { gamepadDefinitions = [], pointerDistanceThreshold, gamepadSlotStrategy = 'sticky', } = app.options;
14340
14823
  this.canvas = app.canvas;
14341
14824
  this.canvasFocusedValue = document.activeElement === this.canvas;
14342
14825
  this.pointerDistanceThreshold = pointerDistanceThreshold;
14343
14826
  this.gamepadDefinitions = [...gamepadDefinitions, ...builtInGamepadDefinitions];
14827
+ this.slotStrategy = gamepadSlotStrategy;
14344
14828
  // Disable the browser's default pan/zoom/double-tap-zoom on touch devices so
14345
14829
  // pointer events reach the canvas without being swallowed by the browser's
14346
14830
  // native touch gestures.
14347
14831
  this.canvas.style.touchAction = 'none';
14348
14832
  this.gestureRecognizer = new GestureRecognizer(pointerDistanceThreshold, this.onPinch, this.onRotate, this.onLongPress);
14833
+ const slot0 = new Gamepad(0, this.channels);
14834
+ const slot1 = new Gamepad(1, this.channels);
14835
+ const slot2 = new Gamepad(2, this.channels);
14836
+ const slot3 = new Gamepad(3, this.channels);
14837
+ this._gamepads = [slot0, slot1, slot2, slot3];
14838
+ for (const pad of this._gamepads) {
14839
+ this.wireGamepadEvents(pad);
14840
+ }
14349
14841
  this.addEventListeners();
14350
14842
  }
14351
14843
  /**
@@ -14359,7 +14851,6 @@ class InputManager {
14359
14851
  return { x: pointer.x, y: pointer.y };
14360
14852
  }
14361
14853
  }
14362
- // Fall back to first non-cancelled pointer.
14363
14854
  for (const pointer of Object.values(this.pointers)) {
14364
14855
  if (pointer.currentState !== PointerState.Cancelled) {
14365
14856
  return { x: pointer.x, y: pointer.y };
@@ -14374,58 +14865,103 @@ class InputManager {
14374
14865
  get canvasFocused() {
14375
14866
  return this.canvasFocusedValue;
14376
14867
  }
14868
+ /**
14869
+ * Always-4 array of {@link Gamepad} slot mailboxes. Each entry exists for
14870
+ * the application's full lifetime; check `pad.connected` for hardware
14871
+ * presence. Listeners attached to a slot survive disconnect/reconnect.
14872
+ */
14377
14873
  get gamepads() {
14378
- return this.gamepadsValue;
14874
+ return this._gamepads;
14379
14875
  }
14380
- getGamepad(index) {
14381
- return this.gamepadsByIndex.get(index) ?? null;
14876
+ /** The slot strategy active for this `InputManager`. */
14877
+ get gamepadSlotStrategy() {
14878
+ return this.slotStrategy;
14382
14879
  }
14383
14880
  /**
14384
- * Register one or more {@link Input} bindings so they participate in the
14385
- * per-frame channel-buffer reads. Idempotent for already-registered
14386
- * inputs; chainable.
14881
+ * Direct accessor for a single gamepad slot. Equivalent to
14882
+ * `app.input.gamepads[slot]` but reads more clearly at call sites.
14387
14883
  */
14388
- add(inputs) {
14389
- if (Array.isArray(inputs)) {
14390
- inputs.forEach((input) => this.add(input));
14391
- return this;
14884
+ getGamepad(slot) {
14885
+ return this._gamepads[slot];
14886
+ }
14887
+ /** Subset of {@link gamepads} containing only currently connected pads, in slot order. */
14888
+ get connectedGamepads() {
14889
+ const result = [];
14890
+ for (const pad of this._gamepads) {
14891
+ if (pad.connected) {
14892
+ result.push(pad);
14893
+ }
14392
14894
  }
14393
- this.inputs.add(inputs);
14394
- return this;
14895
+ return result;
14896
+ }
14897
+ /** Number of slots currently occupied by a physical gamepad. */
14898
+ get connectedGamepadCount() {
14899
+ let count = 0;
14900
+ for (const pad of this._gamepads) {
14901
+ if (pad.connected) {
14902
+ count++;
14903
+ }
14904
+ }
14905
+ return count;
14906
+ }
14907
+ /** First connected gamepad in slot order, or `null` when no pads are attached. */
14908
+ get firstConnectedGamepad() {
14909
+ for (const pad of this._gamepads) {
14910
+ if (pad.connected) {
14911
+ return pad;
14912
+ }
14913
+ }
14914
+ return null;
14915
+ }
14916
+ /** `true` when at least one slot is occupied by a physical gamepad. */
14917
+ get hasGamepad() {
14918
+ for (const pad of this._gamepads) {
14919
+ if (pad.connected) {
14920
+ return true;
14921
+ }
14922
+ }
14923
+ return false;
14924
+ }
14925
+ /**
14926
+ * Register a callback fired once when any of `channels` becomes active.
14927
+ * Manual lifecycle — call `.unbind()` on the returned binding to detach.
14928
+ */
14929
+ onStart(channel, callback, options) {
14930
+ const binding = this.createBinding(channel, options);
14931
+ binding.onStart.add(callback);
14932
+ return binding;
14933
+ }
14934
+ /** Register a callback fired every frame while any of `channels` is active. */
14935
+ onActive(channel, callback, options) {
14936
+ const binding = this.createBinding(channel, options);
14937
+ binding.onActive.add(callback);
14938
+ return binding;
14395
14939
  }
14396
- /** Unregister one or more {@link Input} bindings. */
14397
- remove(inputs) {
14398
- if (Array.isArray(inputs)) {
14399
- inputs.forEach((input) => this.remove(input));
14400
- return this;
14401
- }
14402
- this.inputs.delete(inputs);
14403
- return this;
14940
+ /** Register a callback fired once when all of `channels` become inactive. */
14941
+ onStop(channel, callback, options) {
14942
+ const binding = this.createBinding(channel, options);
14943
+ binding.onStop.add(callback);
14944
+ return binding;
14404
14945
  }
14405
14946
  /**
14406
- * Drop every registered {@link Input}. Pass `destroyInputs = true` to
14407
- * also call `.destroy()` on each one (releases its Signals); default
14408
- * `false` leaves them intact for re-registration.
14947
+ * Register a callback fired when the input is released within
14948
+ * {@link InputBindingOptions.threshold} ms of activation (a "tap").
14409
14949
  */
14410
- clear(destroyInputs = false) {
14411
- if (destroyInputs) {
14412
- for (const input of this.inputs) {
14413
- input.destroy();
14414
- }
14415
- }
14416
- this.inputs.clear();
14417
- return this;
14950
+ onTrigger(channel, callback, options) {
14951
+ const binding = this.createBinding(channel, options);
14952
+ binding.onTrigger.add(callback);
14953
+ return binding;
14418
14954
  }
14419
14955
  /**
14420
14956
  * Per-frame entry point invoked by {@link Application.update}. Polls
14421
14957
  * the gamepad API, drains queued keyboard/pointer/wheel deltas into
14422
14958
  * the channel buffer, fires the corresponding Signals, then evaluates
14423
- * each registered {@link Input}.
14959
+ * each registered binding.
14424
14960
  */
14425
14961
  update() {
14426
14962
  this.updateGamepads();
14427
- for (const input of this.inputs) {
14428
- input.update(this.channels);
14963
+ for (const binding of this.bindings) {
14964
+ binding.update(this.channels);
14429
14965
  }
14430
14966
  if (this.flags.value !== InputManagerFlag.None) {
14431
14967
  this.updateEvents();
@@ -14438,12 +14974,14 @@ class InputManager {
14438
14974
  for (const pointer of Object.values(this.pointers)) {
14439
14975
  pointer.destroy();
14440
14976
  }
14441
- for (const gamepad of this.gamepadsValue) {
14442
- gamepad.destroy();
14977
+ for (const pad of this._gamepads) {
14978
+ pad.destroy();
14443
14979
  }
14444
- this.inputs.clear();
14445
- this.gamepadsByIndex.clear();
14446
- this.gamepadsValue.length = 0;
14980
+ for (const binding of Array.from(this.bindings)) {
14981
+ binding.unbind();
14982
+ }
14983
+ this.bindings.clear();
14984
+ this.gamepadsByBrowserIndex.clear();
14447
14985
  this.channelsPressed.length = 0;
14448
14986
  this.channelsReleased.length = 0;
14449
14987
  this.pointerSlots.clear();
@@ -14463,18 +15001,40 @@ class InputManager {
14463
15001
  this.onKeyUp.destroy();
14464
15002
  this.onGamepadConnected.destroy();
14465
15003
  this.onGamepadDisconnected.destroy();
14466
- this.onGamepadUpdated.destroy();
15004
+ this.onAnyGamepadReassigned.destroy();
15005
+ this.onAnyGamepadButtonDown.destroy();
15006
+ this.onAnyGamepadButtonUp.destroy();
15007
+ this.onAnyGamepadAxisChange.destroy();
14467
15008
  this.onPinch.destroy();
14468
15009
  this.onRotate.destroy();
14469
15010
  this.onLongPress.destroy();
14470
15011
  this.onCanvasFocusChange.destroy();
14471
15012
  }
15013
+ createBinding(channel, options = {}) {
15014
+ const list = Array.isArray(channel) ? channel : [channel];
15015
+ const slot = options.gamepadSlot ?? 0;
15016
+ const resolved = list.map((c) => this.resolveExternalChannel(c, slot));
15017
+ const binding = new InputBinding(resolved, options, this.bindingDetacher);
15018
+ this.bindings.add(binding);
15019
+ return binding;
15020
+ }
15021
+ resolveExternalChannel(channel, slot) {
15022
+ if (channel >= ChannelOffset.Gamepads && channel < ChannelOffset.Gamepads + ChannelSize.Category) {
15023
+ return ChannelOffset.Gamepads + (slot * ChannelSize.Gamepad) + (channel ^ ChannelOffset.Gamepads);
15024
+ }
15025
+ return channel;
15026
+ }
15027
+ wireGamepadEvents(pad) {
15028
+ pad.onButtonDown.add((button, value) => { this.onAnyGamepadButtonDown.dispatch(pad, button, value); });
15029
+ pad.onButtonUp.add((button, value) => { this.onAnyGamepadButtonUp.dispatch(pad, button, value); });
15030
+ pad.onAxisChange.add((axis, value) => { this.onAnyGamepadAxisChange.dispatch(pad, axis, value); });
15031
+ }
14472
15032
  _assignSlot(pointerId) {
14473
15033
  if (this.pointerSlots.has(pointerId)) {
14474
15034
  return this.pointerSlots.get(pointerId);
14475
15035
  }
14476
15036
  if (this.freeSlots.length === 0) {
14477
- return null; // All 16 slots occupied — silently drop.
15037
+ return null;
14478
15038
  }
14479
15039
  const slot = this.freeSlots.shift();
14480
15040
  this.pointerSlots.set(pointerId, slot);
@@ -14484,14 +15044,10 @@ class InputManager {
14484
15044
  const slot = this.pointerSlots.get(pointerId);
14485
15045
  if (slot !== undefined) {
14486
15046
  this.pointerSlots.delete(pointerId);
14487
- // Push to the front so slot 0 is recovered first, keeping allocation predictable.
14488
15047
  this.freeSlots.unshift(slot);
14489
15048
  }
14490
15049
  }
14491
15050
  handleKeyDown(event) {
14492
- // Game-engine convention: keys only register while the canvas
14493
- // owns focus. Otherwise typing into adjacent <input> fields would
14494
- // also drive game state, which is never what users want.
14495
15051
  if (!this.canvasFocusedValue) {
14496
15052
  return;
14497
15053
  }
@@ -14499,9 +15055,6 @@ class InputManager {
14499
15055
  this.channels[channel] = 1;
14500
15056
  this.channelsPressed.push(channel);
14501
15057
  this.flags.push(InputManagerFlag.KeyDown);
14502
- // Consume the event: stop default browser actions (page scroll on
14503
- // arrow/space, find-as-you-type on /, etc.) and stop propagation
14504
- // so other listeners on the page don't double-handle.
14505
15058
  stopEvent(event);
14506
15059
  }
14507
15060
  handleKeyUp(event) {
@@ -14517,7 +15070,7 @@ class InputManager {
14517
15070
  handlePointerOver(event) {
14518
15071
  const slot = this._assignSlot(event.pointerId);
14519
15072
  if (slot === null) {
14520
- return; // 17th+ simultaneous pointer — silently drop.
15073
+ return;
14521
15074
  }
14522
15075
  this.pointers[event.pointerId] = new Pointer(event, this.canvas, this.channels, slot);
14523
15076
  this.flags.push(InputManagerFlag.PointerUpdate);
@@ -14542,10 +15095,6 @@ class InputManager {
14542
15095
  pointer.handlePress(event);
14543
15096
  this.gestureRecognizer.onPointerDown(pointer);
14544
15097
  this.flags.push(InputManagerFlag.PointerUpdate);
14545
- // preventDefault stops native drag / text-selection;
14546
- // stopImmediatePropagation prevents bubbling to host-page click
14547
- // handlers so an embedded canvas doesn't accidentally trigger UI
14548
- // outside its bounds.
14549
15098
  stopEvent(event);
14550
15099
  }
14551
15100
  handlePointerMove(event) {
@@ -14605,13 +15154,6 @@ class InputManager {
14605
15154
  this.onCanvasFocusChange.dispatch(false);
14606
15155
  }
14607
15156
  }
14608
- /**
14609
- * Force every currently-held keyboard channel back to zero and emit
14610
- * onKeyUp for each. Called on canvas/window blur so keys held when
14611
- * focus leaves don't stay stuck "down" forever — without this, a user
14612
- * who alt-tabs while pressing W would have W register as held until
14613
- * they manually release while focus is back.
14614
- */
14615
15157
  releaseAllKeyboardChannels() {
14616
15158
  for (let offset = 0; offset < ChannelSize.Category; offset++) {
14617
15159
  const channel = ChannelOffset.Keyboard + offset;
@@ -14660,49 +15202,98 @@ class InputManager {
14660
15202
  this.canvas.removeEventListener('selectstart', stopEvent, activeListenerOption);
14661
15203
  }
14662
15204
  updateGamepads() {
14663
- const activeGamepads = window.navigator.getGamepads();
14664
- this.gamepadSlotsActive.fill(0);
14665
- for (const activeGamepad of activeGamepads) {
14666
- if (!activeGamepad) {
15205
+ const browserGamepads = window.navigator.getGamepads();
15206
+ const seenBrowserIndices = new Set();
15207
+ for (const browserGamepad of browserGamepads) {
15208
+ if (!browserGamepad) {
14667
15209
  continue;
14668
15210
  }
14669
- const activeIndex = activeGamepad.index;
14670
- if (activeIndex < 0 || activeIndex >= this.gamepadSlotsActive.length) {
15211
+ const browserIndex = browserGamepad.index;
15212
+ if (browserIndex < 0) {
14671
15213
  continue;
14672
15214
  }
14673
- this.gamepadSlotsActive[activeIndex] = 1;
14674
- let gamepad = this.gamepadsByIndex.get(activeIndex);
14675
- if (!gamepad) {
14676
- const definition = resolveGamepadDefinition(activeGamepad, this.gamepadDefinitions);
14677
- gamepad = new Gamepad(activeGamepad, this.channels, definition);
14678
- this.gamepadsByIndex.set(activeIndex, gamepad);
14679
- this.insertGamepadByIndex(gamepad);
14680
- this.onGamepadConnected.dispatch(gamepad, this.gamepadsValue);
14681
- }
14682
- else {
14683
- gamepad.connect(activeGamepad);
15215
+ seenBrowserIndices.add(browserIndex);
15216
+ const existing = this.gamepadsByBrowserIndex.get(browserIndex);
15217
+ if (existing === undefined) {
15218
+ const pad = this.assignSlotForNewPad(browserGamepad);
15219
+ if (pad === null) {
15220
+ continue;
15221
+ }
15222
+ this.gamepadsByBrowserIndex.set(browserIndex, pad);
15223
+ this.onGamepadConnected.dispatch(pad);
14684
15224
  }
14685
- gamepad.update();
14686
- this.onGamepadUpdated.dispatch(gamepad, this.gamepadsValue);
14687
15225
  }
14688
- for (let index = this.gamepadsValue.length - 1; index >= 0; index -= 1) {
14689
- const gamepad = this.gamepadsValue[index];
14690
- if (this.gamepadSlotsActive[gamepad.index] === 0) {
14691
- gamepad.disconnect();
14692
- this.gamepadsValue.splice(index, 1);
14693
- this.gamepadsByIndex.delete(gamepad.index);
14694
- this.onGamepadDisconnected.dispatch(gamepad, this.gamepadsValue);
14695
- gamepad.destroy();
15226
+ for (const [browserIndex, pad] of Array.from(this.gamepadsByBrowserIndex.entries())) {
15227
+ if (!seenBrowserIndices.has(browserIndex)) {
15228
+ this.gamepadsByBrowserIndex.delete(browserIndex);
15229
+ this.handleGamepadDisconnect(pad);
14696
15230
  }
14697
15231
  }
15232
+ for (const pad of this._gamepads) {
15233
+ pad.update();
15234
+ }
14698
15235
  return this;
14699
15236
  }
14700
- insertGamepadByIndex(gamepad) {
14701
- let insertIndex = 0;
14702
- while (insertIndex < this.gamepadsValue.length && this.gamepadsValue[insertIndex].index < gamepad.index) {
14703
- insertIndex += 1;
15237
+ assignSlotForNewPad(browserGamepad) {
15238
+ const definition = resolveGamepadDefinition(browserGamepad, this.gamepadDefinitions);
15239
+ for (const pad of this._gamepads) {
15240
+ if (!pad.connected) {
15241
+ pad._bind(browserGamepad, definition);
15242
+ return pad;
15243
+ }
15244
+ }
15245
+ return null;
15246
+ }
15247
+ handleGamepadDisconnect(pad) {
15248
+ if (this.slotStrategy !== 'compact') {
15249
+ // Sticky: pad's slot becomes empty in place; fire onDisconnect
15250
+ // on that slot directly.
15251
+ pad._unbind();
15252
+ this.onGamepadDisconnected.dispatch(pad);
15253
+ return;
15254
+ }
15255
+ // Compact: in semantic terms the user lost a player, and the trailing
15256
+ // (highest-numbered) occupied slot is the one that becomes empty.
15257
+ // 1. Snapshot the highest occupied slot before any state change.
15258
+ // 2. Silently vacate the disconnecting pad (no onDisconnect yet).
15259
+ // 3. Shift higher-numbered occupied slots down to fill any gaps,
15260
+ // firing onPadReassigned for each slot that received a new pad.
15261
+ // 4. Fire onDisconnect on the slot that ended up empty (the one
15262
+ // snapshotted in step 1).
15263
+ let lastOccupiedSlot = -1;
15264
+ for (let i = gamepadSlots - 1; i >= 0; i--) {
15265
+ if (this._gamepads[i].connected) {
15266
+ lastOccupiedSlot = i;
15267
+ break;
15268
+ }
15269
+ }
15270
+ pad._silentUnbind();
15271
+ for (let target = 0; target < gamepadSlots; target++) {
15272
+ if (this._gamepads[target].connected) {
15273
+ continue;
15274
+ }
15275
+ for (let source = target + 1; source < gamepadSlots; source++) {
15276
+ const sourcePad = this._gamepads[source];
15277
+ if (!sourcePad.connected) {
15278
+ continue;
15279
+ }
15280
+ const browserIndex = sourcePad.browserGamepad?.index;
15281
+ const targetPad = this._gamepads[target];
15282
+ const sourceSlot = sourcePad.slot;
15283
+ targetPad._rebindFrom(sourcePad);
15284
+ if (browserIndex !== undefined) {
15285
+ this.gamepadsByBrowserIndex.set(browserIndex, targetPad);
15286
+ }
15287
+ targetPad.onPadReassigned.dispatch(sourceSlot);
15288
+ this.onAnyGamepadReassigned.dispatch(targetPad, sourceSlot);
15289
+ break;
15290
+ }
15291
+ }
15292
+ if (lastOccupiedSlot >= 0) {
15293
+ const emptiedSlot = this._gamepads[lastOccupiedSlot];
15294
+ emptiedSlot._dispatchDisconnect();
15295
+ this.onGamepadDisconnected.dispatch(emptiedSlot);
14704
15296
  }
14705
- this.gamepadsValue.splice(insertIndex, 0, gamepad);
14706
15297
  }
14707
15298
  updateEvents() {
14708
15299
  if (this.flags.pop(InputManagerFlag.KeyDown)) {
@@ -15324,12 +15915,12 @@ class InteractionManager {
15324
15915
  this._onPointerTapHandler = this._handlePointerTap.bind(this);
15325
15916
  this._onPointerCancelHandler = this._handlePointerCancel.bind(this);
15326
15917
  this._onPointerLeaveHandler = this._handlePointerLeave.bind(this);
15327
- app.inputManager.onPointerDown.add(this._onPointerDownHandler);
15328
- app.inputManager.onPointerMove.add(this._onPointerMoveHandler);
15329
- app.inputManager.onPointerUp.add(this._onPointerUpHandler);
15330
- app.inputManager.onPointerTap.add(this._onPointerTapHandler);
15331
- app.inputManager.onPointerCancel.add(this._onPointerCancelHandler);
15332
- app.inputManager.onPointerLeave.add(this._onPointerLeaveHandler);
15918
+ app.input.onPointerDown.add(this._onPointerDownHandler);
15919
+ app.input.onPointerMove.add(this._onPointerMoveHandler);
15920
+ app.input.onPointerUp.add(this._onPointerUpHandler);
15921
+ app.input.onPointerTap.add(this._onPointerTapHandler);
15922
+ app.input.onPointerCancel.add(this._onPointerCancelHandler);
15923
+ app.input.onPointerLeave.add(this._onPointerLeaveHandler);
15333
15924
  }
15334
15925
  /**
15335
15926
  * Returns the RenderNode currently hovered by the given pointer, or null.
@@ -15373,12 +15964,12 @@ class InteractionManager {
15373
15964
  return this._quadtree;
15374
15965
  }
15375
15966
  destroy() {
15376
- this._app.inputManager.onPointerDown.remove(this._onPointerDownHandler);
15377
- this._app.inputManager.onPointerMove.remove(this._onPointerMoveHandler);
15378
- this._app.inputManager.onPointerUp.remove(this._onPointerUpHandler);
15379
- this._app.inputManager.onPointerTap.remove(this._onPointerTapHandler);
15380
- this._app.inputManager.onPointerCancel.remove(this._onPointerCancelHandler);
15381
- this._app.inputManager.onPointerLeave.remove(this._onPointerLeaveHandler);
15967
+ this._app.input.onPointerDown.remove(this._onPointerDownHandler);
15968
+ this._app.input.onPointerMove.remove(this._onPointerMoveHandler);
15969
+ this._app.input.onPointerUp.remove(this._onPointerUpHandler);
15970
+ this._app.input.onPointerTap.remove(this._onPointerTapHandler);
15971
+ this._app.input.onPointerCancel.remove(this._onPointerCancelHandler);
15972
+ this._app.input.onPointerLeave.remove(this._onPointerLeaveHandler);
15382
15973
  this._lastHit.clear();
15383
15974
  this._pending.clear();
15384
15975
  this._capturedPointers.clear();
@@ -15396,7 +15987,7 @@ class InteractionManager {
15396
15987
  /**
15397
15988
  * Process all pending pointer events accumulated since the last frame.
15398
15989
  * Must be called once per frame from {@link Application.update}, after
15399
- * `inputManager.update()` has run (so signals are already dispatched and
15990
+ * `input.update()` has run (so signals are already dispatched and
15400
15991
  * queued here) and before game-state updates so that user listeners on
15401
15992
  * `onPointerDown` etc. fire before per-frame logic mutates state.
15402
15993
  *
@@ -18469,14 +19060,86 @@ const parseTimestamp = (value) => {
18469
19060
  }
18470
19061
  return seconds;
18471
19062
  };
19063
+ const validAlignValues = new Set(['start', 'center', 'end', 'left', 'right']);
19064
+ const validLineAlignValues = new Set(['start', 'center', 'end']);
19065
+ const validPositionAlignValues = new Set(['auto', 'line-left', 'center', 'line-right']);
19066
+ /**
19067
+ * Parses the WebVTT cue-settings tail (`align:center line:80% position:50%`)
19068
+ * and applies each recognized setting to the supplied {@link VTTCue}.
19069
+ *
19070
+ * Unknown keys, malformed values, and unknown enum members are silently
19071
+ * skipped so that one bad token does not invalidate an otherwise valid cue.
19072
+ * Percent signs on numeric values are tolerated and stripped.
19073
+ *
19074
+ * @internal
19075
+ */
19076
+ const applyCueSettings = (cue, settings) => {
19077
+ if (!settings) {
19078
+ return;
19079
+ }
19080
+ for (const token of settings.split(/\s+/)) {
19081
+ const colonIndex = token.indexOf(':');
19082
+ if (colonIndex === -1) {
19083
+ continue;
19084
+ }
19085
+ const name = token.slice(0, colonIndex);
19086
+ const value = token.slice(colonIndex + 1);
19087
+ switch (name) {
19088
+ case 'vertical':
19089
+ if (value === 'rl' || value === 'lr' || value === '') {
19090
+ cue.vertical = value;
19091
+ }
19092
+ break;
19093
+ case 'line': {
19094
+ if (value === 'auto') {
19095
+ cue.line = 'auto';
19096
+ }
19097
+ else {
19098
+ const [linePart, alignPart] = value.split(',');
19099
+ const num = parseFloat(linePart);
19100
+ if (!Number.isNaN(num)) {
19101
+ cue.line = num;
19102
+ }
19103
+ if (alignPart !== undefined && validLineAlignValues.has(alignPart)) {
19104
+ cue.lineAlign = alignPart;
19105
+ }
19106
+ }
19107
+ break;
19108
+ }
19109
+ case 'position': {
19110
+ const [posPart, alignPart] = value.split(',');
19111
+ const num = parseFloat(posPart);
19112
+ if (!Number.isNaN(num)) {
19113
+ cue.position = num;
19114
+ }
19115
+ if (alignPart !== undefined && validPositionAlignValues.has(alignPart)) {
19116
+ cue.positionAlign = alignPart;
19117
+ }
19118
+ break;
19119
+ }
19120
+ case 'size': {
19121
+ const num = parseFloat(value);
19122
+ if (!Number.isNaN(num)) {
19123
+ cue.size = num;
19124
+ }
19125
+ break;
19126
+ }
19127
+ case 'align':
19128
+ if (validAlignValues.has(value)) {
19129
+ cue.align = value;
19130
+ }
19131
+ break;
19132
+ }
19133
+ }
19134
+ };
18472
19135
  /**
18473
19136
  * {@link AssetFactory} implementation that parses WebVTT (`.vtt`) subtitle and
18474
19137
  * caption files and produces an array of {@link VTTCue} instances.
18475
19138
  *
18476
19139
  * The parser handles CRLF and CR line endings, skips the `WEBVTT` header and
18477
- * any metadata blocks, and supports optional cue settings on the timestamp
18478
- * line. Inline cue settings are stripped; only start time, end time, and cue
18479
- * text are preserved.
19140
+ * any metadata blocks, and applies optional cue settings (`align`, `line`,
19141
+ * `position`, `size`, `vertical`) on the timestamp line directly to the
19142
+ * resulting {@link VTTCue}.
18480
19143
  */
18481
19144
  class VttFactory extends AbstractAssetFactory {
18482
19145
  storageName = 'vtt';
@@ -18506,8 +19169,11 @@ class VttFactory extends AbstractAssetFactory {
18506
19169
  const arrowIndex = line.indexOf('-->');
18507
19170
  const startStr = line.slice(0, arrowIndex).trim();
18508
19171
  const rest = line.slice(arrowIndex + 3).trim();
18509
- // rest may have cue settings after the end timestamp
18510
- const endStr = rest.split(/\s+/)[0];
19172
+ // rest contains the end timestamp followed by optional cue
19173
+ // settings (align, line, position, size, vertical).
19174
+ const restTokens = rest.split(/\s+/);
19175
+ const endStr = restTokens[0];
19176
+ const settingsString = restTokens.slice(1).join(' ');
18511
19177
  const start = parseTimestamp(startStr);
18512
19178
  const end = parseTimestamp(endStr);
18513
19179
  i++;
@@ -18516,7 +19182,9 @@ class VttFactory extends AbstractAssetFactory {
18516
19182
  textLines.push(lines[i]);
18517
19183
  i++;
18518
19184
  }
18519
- cues.push(new VTTCue(start, end, textLines.join('\n')));
19185
+ const cue = new VTTCue(start, end, textLines.join('\n'));
19186
+ applyCueSettings(cue, settingsString);
19187
+ cues.push(cue);
18520
19188
  }
18521
19189
  else {
18522
19190
  i++;
@@ -18615,6 +19283,53 @@ function describeType(type) {
18615
19283
  return type.name.length > 0 ? type.name : '(anonymous type)';
18616
19284
  }
18617
19285
 
19286
+ /**
19287
+ * {@link CacheStrategy} that checks every provided {@link CacheStore} before
19288
+ * falling back to the network.
19289
+ *
19290
+ * On a cache hit the stored value is fed directly to
19291
+ * {@link AssetFactory.create | factory.create}; if that throws (stale or
19292
+ * corrupt entry) the entry is deleted and the next store is tried. Only once
19293
+ * all stores miss does the strategy fetch from the network and write the
19294
+ * processed source back to every store. Quota or serialisation errors during
19295
+ * write are swallowed silently so that a full storage can never prevent an
19296
+ * asset from loading.
19297
+ *
19298
+ * Returns the fully constructed resource — callers do not need to call
19299
+ * {@link AssetFactory.create} again.
19300
+ */
19301
+ class CacheFirstStrategy {
19302
+ async resolve(request, stores) {
19303
+ const { storageName, key, url, requestOptions, factory, options } = request;
19304
+ for (const store of stores) {
19305
+ const cached = await store.load(storageName, key);
19306
+ if (cached !== null && cached !== undefined) {
19307
+ try {
19308
+ return await factory.create(cached, options);
19309
+ }
19310
+ catch {
19311
+ await store.delete(storageName, key);
19312
+ }
19313
+ }
19314
+ }
19315
+ const response = await fetch(url, requestOptions);
19316
+ if (!response.ok) {
19317
+ throw new Error(`Failed to fetch "${url}" (${response.status} ${response.statusText}).`);
19318
+ }
19319
+ const source = await factory.process(response);
19320
+ const resource = await factory.create(source, options);
19321
+ for (const store of stores) {
19322
+ try {
19323
+ await store.save(storageName, key, source);
19324
+ }
19325
+ catch {
19326
+ // Quota exceeded or non-cloneable value — continue without caching.
19327
+ }
19328
+ }
19329
+ return resource;
19330
+ }
19331
+ }
19332
+
18618
19333
  // ---------------------------------------------------------------------------
18619
19334
  // Loader
18620
19335
  // ---------------------------------------------------------------------------
@@ -18653,6 +19368,7 @@ class Loader {
18653
19368
  _typeIds = new WeakMap();
18654
19369
  _preventStoreKeys = new Set();
18655
19370
  _stores;
19371
+ _cacheStrategy;
18656
19372
  _resourcePath;
18657
19373
  _requestOptions;
18658
19374
  _concurrency;
@@ -18677,6 +19393,7 @@ class Loader {
18677
19393
  this._stores = options.cache
18678
19394
  ? (Array.isArray(options.cache) ? options.cache : [options.cache])
18679
19395
  : [];
19396
+ this._cacheStrategy = options.cacheStrategy ?? new CacheFirstStrategy();
18680
19397
  this._registerBuiltinFactories();
18681
19398
  }
18682
19399
  // -----------------------------------------------------------------------
@@ -19037,45 +19754,24 @@ class Loader {
19037
19754
  async _fetch(type, alias, path, options) {
19038
19755
  const factory = this._registry.resolve(type);
19039
19756
  const url = this._resolveUrl(path);
19040
- let source = null;
19041
- // Check caches
19042
- for (const store of this._stores) {
19043
- source = await store.load(factory.storageName, alias);
19044
- if (source !== null && source !== undefined) {
19045
- try {
19046
- const resource = await factory.create(source, options);
19047
- this._storeResource(type, alias, resource);
19048
- return resource;
19049
- }
19050
- catch {
19051
- await store.delete(factory.storageName, alias);
19052
- source = null;
19053
- }
19054
- }
19055
- }
19056
- // Network fetch
19057
- const response = await fetch(url, this._requestOptions);
19058
- if (!response.ok) {
19059
- throw new Error(`Failed to fetch "${alias}" from "${url}" (${response.status} ${response.statusText}).`);
19757
+ try {
19758
+ const resource = await this._cacheStrategy.resolve({
19759
+ storageName: factory.storageName,
19760
+ key: alias,
19761
+ url,
19762
+ requestOptions: this._requestOptions,
19763
+ factory,
19764
+ options,
19765
+ }, this._stores);
19766
+ this._storeResource(type, alias, resource);
19767
+ return resource;
19060
19768
  }
19061
- source = await factory.process(response);
19062
- const resource = await factory.create(source, options).catch((error) => {
19063
- const cause = error instanceof Error ? error : new Error(String(error));
19064
- throw new Error(`Failed to create "${alias}" from "${url}": ${cause.message}`, {
19065
- cause,
19769
+ catch (error) {
19770
+ const message = error instanceof Error ? error.message : String(error);
19771
+ throw new Error(`Failed to load "${alias}" from "${url}": ${message}`, {
19772
+ cause: error,
19066
19773
  });
19067
- });
19068
- // Write to caches
19069
- for (const store of this._stores) {
19070
- try {
19071
- await store.save(factory.storageName, alias, source);
19072
- }
19073
- catch {
19074
- // Quota exceeded or non-cloneable — continue without caching.
19075
- }
19076
19774
  }
19077
- this._storeResource(type, alias, resource);
19078
- return resource;
19079
19775
  }
19080
19776
  // -----------------------------------------------------------------------
19081
19777
  // Internal — background queue
@@ -19322,6 +20018,7 @@ const defaultAppSettings = {
19322
20018
  spriteRendererBatchSize: 4096, // ~ 262kb
19323
20019
  particleRendererBatchSize: 8192, // ~ 1.18mb
19324
20020
  gamepadDefinitions: [],
20021
+ gamepadSlotStrategy: 'sticky',
19325
20022
  pointerDistanceThreshold: 10,
19326
20023
  webglAttributes: {
19327
20024
  alpha: false,
@@ -19367,7 +20064,7 @@ class Application {
19367
20064
  options;
19368
20065
  canvas;
19369
20066
  loader;
19370
- inputManager;
20067
+ input;
19371
20068
  interaction;
19372
20069
  sceneManager;
19373
20070
  tweens = new TweenManager();
@@ -19409,7 +20106,7 @@ class Application {
19409
20106
  });
19410
20107
  this._backendType = this.resolveInitialBackendType();
19411
20108
  this._backend = this.createBackend(this._backendType);
19412
- this.inputManager = new InputManager(this);
20109
+ this.input = new InputManager(this);
19413
20110
  this.interaction = new InteractionManager(this);
19414
20111
  this.sceneManager = new SceneManager(this);
19415
20112
  this._updateHandler = this.update.bind(this);
@@ -19418,7 +20115,7 @@ class Application {
19418
20115
  this._documentVisible = document.visibilityState === 'visible';
19419
20116
  document.addEventListener('visibilitychange', this._visibilityChangeHandler);
19420
20117
  }
19421
- this.inputManager.onCanvasFocusChange.add((focused) => {
20118
+ this.input.onCanvasFocusChange.add((focused) => {
19422
20119
  this.onCanvasFocusChange.dispatch(focused);
19423
20120
  });
19424
20121
  this.onVisibilityChange.add((visible) => {
@@ -19455,7 +20152,7 @@ class Application {
19455
20152
  return this._capabilities;
19456
20153
  }
19457
20154
  get canvasFocused() {
19458
- return this.inputManager.canvasFocused;
20155
+ return this.input.canvasFocused;
19459
20156
  }
19460
20157
  get documentVisible() {
19461
20158
  return this._documentVisible;
@@ -19513,7 +20210,7 @@ class Application {
19513
20210
  const frameDelta = this._frameClock.elapsedTime;
19514
20211
  const frameStart = performance.now();
19515
20212
  this.backend.resetStats();
19516
- this.inputManager.update();
20213
+ this.input.update();
19517
20214
  this.interaction.update();
19518
20215
  getAudioManager().update();
19519
20216
  this.tweens.update(frameDelta.seconds);
@@ -19586,7 +20283,7 @@ class Application {
19586
20283
  this.stop();
19587
20284
  this.loader.destroy();
19588
20285
  this.interaction.destroy();
19589
- this.inputManager.destroy();
20286
+ this.input.destroy();
19590
20287
  this.tweens.destroy();
19591
20288
  this._backend.destroy();
19592
20289
  this.sceneManager.destroy();
@@ -19649,6 +20346,41 @@ class Application {
19649
20346
  }
19650
20347
  }
19651
20348
 
20349
+ /**
20350
+ * Scene-bound input proxy that automatically disposes its bindings when
20351
+ * the owning scene unloads. Created lazily on first access via
20352
+ * {@link Scene.inputs}; do not instantiate directly.
20353
+ */
20354
+ class SceneInputs {
20355
+ _scene;
20356
+ _bindings = new Set();
20357
+ constructor(_scene) {
20358
+ this._scene = _scene;
20359
+ }
20360
+ onStart(channel, callback, options) {
20361
+ return this._track(this._scene.app.input.onStart(channel, callback, options));
20362
+ }
20363
+ onActive(channel, callback, options) {
20364
+ return this._track(this._scene.app.input.onActive(channel, callback, options));
20365
+ }
20366
+ onStop(channel, callback, options) {
20367
+ return this._track(this._scene.app.input.onStop(channel, callback, options));
20368
+ }
20369
+ onTrigger(channel, callback, options) {
20370
+ return this._track(this._scene.app.input.onTrigger(channel, callback, options));
20371
+ }
20372
+ /** @internal Called by Scene.destroy. */
20373
+ _disposeAll() {
20374
+ for (const binding of Array.from(this._bindings)) {
20375
+ binding.unbind();
20376
+ }
20377
+ this._bindings.clear();
20378
+ }
20379
+ _track(binding) {
20380
+ this._bindings.add(binding);
20381
+ return binding;
20382
+ }
20383
+ }
19652
20384
  /**
19653
20385
  * A scene's lifecycle host. Subclass to define scene behavior:
19654
20386
  *
@@ -19672,6 +20404,7 @@ class Scene {
19672
20404
  _root = new Container();
19673
20405
  _stackMode = 'overlay';
19674
20406
  _inputMode = 'capture';
20407
+ _inputs = null;
19675
20408
  get app() {
19676
20409
  return this._app;
19677
20410
  }
@@ -19694,6 +20427,24 @@ class Scene {
19694
20427
  get root() {
19695
20428
  return this._root;
19696
20429
  }
20430
+ /**
20431
+ * Scene-bound input registry. Bindings created via
20432
+ * `this.inputs.onTrigger(...)` etc. are automatically disposed when the
20433
+ * scene unloads — no manual cleanup required.
20434
+ *
20435
+ * Lazily instantiated on first access; throws if accessed before
20436
+ * {@link Scene.app} is set (i.e. before the scene is registered with
20437
+ * a {@link SceneManager}).
20438
+ */
20439
+ get inputs() {
20440
+ if (this._inputs === null) {
20441
+ if (this._app === null) {
20442
+ throw new Error('Scene.inputs is unavailable before the scene is attached to an Application.');
20443
+ }
20444
+ this._inputs = new SceneInputs(this);
20445
+ }
20446
+ return this._inputs;
20447
+ }
19697
20448
  get stackMode() {
19698
20449
  return this._stackMode;
19699
20450
  }
@@ -19790,47 +20541,13 @@ class Scene {
19790
20541
  // override in subclass
19791
20542
  }
19792
20543
  destroy() {
20544
+ this._inputs?._disposeAll();
20545
+ this._inputs = null;
19793
20546
  this._root.destroy();
19794
20547
  this._app = null;
19795
20548
  }
19796
20549
  }
19797
20550
 
19798
- /**
19799
- * {@link Clock} variant with a fixed limit. Inherits start/stop/reset/restart
19800
- * semantics; adds {@link Timer.expired} (true once `elapsedTime >= limit`)
19801
- * and remaining-time accessors. Useful for cooldowns, delays, and any timed
19802
- * gating logic where you want to ask "is the duration up?" each frame.
19803
- */
19804
- class Timer extends Clock {
19805
- _limit;
19806
- constructor(limit, autoStart = false) {
19807
- super();
19808
- this._limit = limit.clone();
19809
- if (autoStart) {
19810
- this.restart();
19811
- }
19812
- }
19813
- set limit(limit) {
19814
- this._limit.copy(limit);
19815
- }
19816
- /** `true` once the elapsed time has reached or exceeded the configured limit. */
19817
- get expired() {
19818
- return this.elapsedMilliseconds >= this._limit.milliseconds;
19819
- }
19820
- get remainingMilliseconds() {
19821
- return Math.max(0, this._limit.milliseconds - this.elapsedMilliseconds);
19822
- }
19823
- get remainingSeconds() {
19824
- return this.remainingMilliseconds / Time.seconds;
19825
- }
19826
- get remainingMinutes() {
19827
- return this.remainingMilliseconds / Time.minutes;
19828
- }
19829
- get remainingHours() {
19830
- return this.remainingMilliseconds / Time.hours;
19831
- }
19832
- }
19833
-
19834
20551
  /**
19835
20552
  * Lightweight visualisation analyser backed by a Web Audio AnalyserNode.
19836
20553
  *
@@ -23027,22 +23744,22 @@ const basePositions = new Map([
23027
23744
  ['RightStick', [0.62, 0.66]],
23028
23745
  ]);
23029
23746
  const channelMap = new Map([
23030
- ['ButtonNorth', GamepadChannel.ButtonNorth],
23031
- ['ButtonWest', GamepadChannel.ButtonWest],
23032
- ['ButtonEast', GamepadChannel.ButtonEast],
23033
- ['ButtonSouth', GamepadChannel.ButtonSouth],
23034
- ['LeftShoulder', GamepadChannel.LeftShoulder],
23035
- ['RightShoulder', GamepadChannel.RightShoulder],
23036
- ['LeftTrigger', GamepadChannel.LeftTrigger],
23037
- ['RightTrigger', GamepadChannel.RightTrigger],
23038
- ['Select', GamepadChannel.Select],
23039
- ['Start', GamepadChannel.Start],
23040
- ['LeftStick', GamepadChannel.LeftStick],
23041
- ['RightStick', GamepadChannel.RightStick],
23042
- ['DPadUp', GamepadChannel.DPadUp],
23043
- ['DPadDown', GamepadChannel.DPadDown],
23044
- ['DPadLeft', GamepadChannel.DPadLeft],
23045
- ['DPadRight', GamepadChannel.DPadRight],
23747
+ ['ButtonNorth', GamepadButton.North],
23748
+ ['ButtonWest', GamepadButton.West],
23749
+ ['ButtonEast', GamepadButton.East],
23750
+ ['ButtonSouth', GamepadButton.South],
23751
+ ['LeftShoulder', GamepadButton.LeftShoulder],
23752
+ ['RightShoulder', GamepadButton.RightShoulder],
23753
+ ['LeftTrigger', GamepadButton.LeftTrigger],
23754
+ ['RightTrigger', GamepadButton.RightTrigger],
23755
+ ['Select', GamepadButton.Select],
23756
+ ['Start', GamepadButton.Start],
23757
+ ['LeftStick', GamepadButton.LeftStick],
23758
+ ['RightStick', GamepadButton.RightStick],
23759
+ ['DPadUp', GamepadButton.DPadUp],
23760
+ ['DPadDown', GamepadButton.DPadDown],
23761
+ ['DPadLeft', GamepadButton.DPadLeft],
23762
+ ['DPadRight', GamepadButton.DPadRight],
23046
23763
  ]);
23047
23764
  const genericLabels = new Map([
23048
23765
  ['ButtonNorth', 'North'],
@@ -23109,6 +23826,7 @@ const promptLabelsByFamily = new Map([
23109
23826
  [GamepadMappingFamily.JoyConRight, switchLabels],
23110
23827
  [GamepadMappingFamily.GameCube, genericLabels],
23111
23828
  [GamepadMappingFamily.SteamController, genericLabels],
23829
+ [GamepadMappingFamily.SteamDeck, genericLabels],
23112
23830
  [GamepadMappingFamily.ArcadeStick, genericLabels],
23113
23831
  ]);
23114
23832
  /**
@@ -23117,7 +23835,7 @@ const promptLabelsByFamily = new Map([
23117
23835
  * Provides the canonical set of prompt controls, their normalised [x, y] positions
23118
23836
  * on a generic controller silhouette, device-family label strings (e.g. "A" for
23119
23837
  * Xbox, "Cross" for PlayStation, "B" for Switch), and the mapping from prompt
23120
- * control names to {@link GamepadChannel} values.
23838
+ * control names to {@link GamepadButton} channel values.
23121
23839
  */
23122
23840
  class GamepadPromptLayouts {
23123
23841
  /** Complete ordered list of every {@link GamepadPromptControl} token. */
@@ -23158,98 +23876,14 @@ class GamepadPromptLayouts {
23158
23876
  }
23159
23877
  /**
23160
23878
  * Returns the static mapping from each {@link GamepadPromptControl} to its
23161
- * corresponding {@link GamepadChannel}. The composite `'DPad'` control has no
23162
- * channel entry and is absent from the returned map.
23879
+ * corresponding {@link GamepadButton} channel. The composite `'DPad'`
23880
+ * control has no channel entry and is absent from the returned map.
23163
23881
  */
23164
23882
  static getControlChannelMap() {
23165
23883
  return channelMap;
23166
23884
  }
23167
23885
  }
23168
23886
 
23169
- /**
23170
- * Bind one or more input channels (keyboard keys, gamepad buttons, gamepad
23171
- * axes) to a set of high-level events: `onStart` (became active),
23172
- * `onStop` (became inactive), `onActive` (per-frame while active), and
23173
- * `onTrigger` (released within the threshold window — a "tap"). The current
23174
- * raw value is the max across all subscribed channels.
23175
- *
23176
- * Construct ad-hoc, or via {@link InputManager.add}. Driven by the
23177
- * {@link InputManager} update loop which feeds the unified channel buffer.
23178
- *
23179
- * @example
23180
- * ```ts
23181
- * const jump = new Input([Keyboard.Space, GamepadChannel.FaceA], {
23182
- * onTrigger: () => player.jump(),
23183
- * });
23184
- * ```
23185
- */
23186
- class Input {
23187
- static triggerThreshold = 300;
23188
- channels = new Set();
23189
- triggerTimer;
23190
- valueState = 0;
23191
- onStart = new Signal();
23192
- onStop = new Signal();
23193
- onActive = new Signal();
23194
- onTrigger = new Signal();
23195
- constructor(channels, { onStart, onStop, onActive, onTrigger, context, threshold } = {}) {
23196
- this.channels = new Set(Array.isArray(channels) ? channels : [channels]);
23197
- this.triggerTimer = new Timer(milliseconds(threshold ?? Input.triggerThreshold));
23198
- if (onStart) {
23199
- this.onStart.add(onStart, context);
23200
- }
23201
- if (onStop) {
23202
- this.onStop.add(onStop, context);
23203
- }
23204
- if (onActive) {
23205
- this.onActive.add(onActive, context);
23206
- }
23207
- if (onTrigger) {
23208
- this.onTrigger.add(onTrigger, context);
23209
- }
23210
- }
23211
- get activeChannels() {
23212
- return this.channels;
23213
- }
23214
- get value() {
23215
- return this.valueState;
23216
- }
23217
- /**
23218
- * Read the latest values from the unified channel buffer and dispatch
23219
- * the appropriate Signals. Called once per frame by {@link InputManager}.
23220
- * No-op for inputs not bound to any channel.
23221
- */
23222
- update(channels) {
23223
- this.valueState = 0;
23224
- for (const channel of this.channels) {
23225
- this.valueState = Math.max(channels[channel], this.valueState);
23226
- }
23227
- if (this.valueState) {
23228
- if (!this.triggerTimer.running) {
23229
- this.triggerTimer.restart();
23230
- this.onStart.dispatch(this.valueState);
23231
- }
23232
- this.onActive.dispatch(this.valueState);
23233
- }
23234
- else if (this.triggerTimer.running) {
23235
- this.onStop.dispatch(this.valueState);
23236
- if (!this.triggerTimer.expired) {
23237
- this.onTrigger.dispatch(this.valueState);
23238
- }
23239
- this.triggerTimer.stop();
23240
- }
23241
- return this;
23242
- }
23243
- destroy() {
23244
- this.channels.clear();
23245
- this.triggerTimer.destroy();
23246
- this.onStart.destroy();
23247
- this.onStop.destroy();
23248
- this.onActive.destroy();
23249
- this.onTrigger.destroy();
23250
- }
23251
- }
23252
-
23253
23887
  /**
23254
23888
  * Triangulate a simple 2D polygon by ear-clipping.
23255
23889
  *
@@ -27370,50 +28004,6 @@ class WebGpuShaderFilter extends Filter {
27370
28004
  }
27371
28005
  }
27372
28006
 
27373
- /**
27374
- * {@link CacheStrategy} that checks every provided {@link CacheStore} before
27375
- * falling back to the network.
27376
- *
27377
- * On a cache hit the stored value is validated by calling
27378
- * {@link AssetFactory.create | factory.create}; if that throws (stale or
27379
- * corrupt entry) the entry is deleted and the next store is tried. Only once
27380
- * all stores miss does the strategy fetch from the network and write the
27381
- * result back to every store. Quota or serialisation errors during write are
27382
- * swallowed silently so that a full storage can never prevent an asset from
27383
- * loading.
27384
- */
27385
- class CacheFirstStrategy {
27386
- async resolve(request, stores) {
27387
- const { storageName, key, url, requestOptions, factory } = request;
27388
- for (const store of stores) {
27389
- const cached = await store.load(storageName, key);
27390
- if (cached !== null && cached !== undefined) {
27391
- try {
27392
- await factory.create(cached);
27393
- return cached;
27394
- }
27395
- catch {
27396
- await store.delete(storageName, key);
27397
- }
27398
- }
27399
- }
27400
- const response = await fetch(url, requestOptions);
27401
- if (!response.ok) {
27402
- throw new Error(`Failed to fetch "${url}" (${response.status} ${response.statusText}).`);
27403
- }
27404
- const source = await factory.process(response);
27405
- for (const store of stores) {
27406
- try {
27407
- await store.save(storageName, key, source);
27408
- }
27409
- catch {
27410
- // Quota exceeded or non-cloneable value — continue without caching.
27411
- }
27412
- }
27413
- return source;
27414
- }
27415
- }
27416
-
27417
28007
  /**
27418
28008
  * {@link CacheStrategy} that always fetches from the network and never reads
27419
28009
  * from or writes to any {@link CacheStore}.
@@ -27421,14 +28011,19 @@ class CacheFirstStrategy {
27421
28011
  * Useful for assets that must always be fresh (e.g. live configuration files)
27422
28012
  * or for environments where persistent storage is unavailable. The `stores`
27423
28013
  * argument is accepted but intentionally ignored.
28014
+ *
28015
+ * Returns the fully constructed resource — callers do not need to call
28016
+ * {@link AssetFactory.create} again.
27424
28017
  */
27425
28018
  class NetworkOnlyStrategy {
27426
28019
  async resolve(request, _stores) {
27427
- const response = await fetch(request.url, request.requestOptions);
28020
+ const { url, requestOptions, factory, options } = request;
28021
+ const response = await fetch(url, requestOptions);
27428
28022
  if (!response.ok) {
27429
- throw new Error(`Failed to fetch "${request.url}" (${response.status} ${response.statusText}).`);
28023
+ throw new Error(`Failed to fetch "${url}" (${response.status} ${response.statusText}).`);
27430
28024
  }
27431
- return request.factory.process(response);
28025
+ const source = await factory.process(response);
28026
+ return factory.create(source, options);
27432
28027
  }
27433
28028
  }
27434
28029
 
@@ -27632,5 +28227,5 @@ class IndexedDbStore {
27632
28227
  }
27633
28228
  }
27634
28229
 
27635
- export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, AudioBus, AudioFilter, AudioListener, AudioManager, BeatDetector, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, ChorusFilter, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, CompressorFilter, Container, DelayFilter, Drawable, DuckingFilter, DynamicGlyphAtlas, Ease, Ellipse, Envelope, EqualizerFilter, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadChannel, GamepadControl, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, GranularFilter, Graphics, HighpassFilter, ImageFactory, IndexedDbDatabase, IndexedDbStore, Input, InputManager, InteractionEvent, InteractionManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, LowpassFilter, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, OscillatorSound, Particle, ParticleOptions, ParticleSystem, PitchShiftFilter, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, ReverbFilter, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, SoundPoolStrategy, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, Tween, TweenManager, TweenState, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VocoderFilter, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2ShaderFilter, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuShaderFilter, WebGpuSpriteRenderer, WorkletFilter, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, crossFade, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getAudioManager, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionEllipseCircle, getCollisionEllipseRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, milliseconds, minutes, noop$1 as noop, onAudioContextReady, parseGamepadDescriptor, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, registerWorkletProcessor, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, substepSweep, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst, tau, trimRotation, upgradeFragmentShaderToGl300, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
28230
+ export { AbstractAssetFactory, AbstractMedia, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, AudioAnalyser, AudioBus, AudioFilter, AudioListener, AudioManager, BeatDetector, BinaryFactory, BlendModes, BlurFilter, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Capabilities, ChannelOffset, ChannelSize, ChorusFilter, Circle, Clock, CollisionType, Color, ColorAffector, ColorFilter, CompressorFilter, Container, DelayFilter, Drawable, DuckingFilter, DynamicGlyphAtlas, Ease, Ellipse, Envelope, EqualizerFilter, FactoryRegistry, Filter, Flags, FontFactory, ForceAffector, GameCubeGamepadMapping, Gamepad, GamepadAxis, GamepadButton, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, GranularFilter, Graphics, HighpassFilter, ImageFactory, IndexedDbDatabase, IndexedDbStore, InputBinding, InputManager, InteractionEvent, InteractionManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, Keyboard, Line, Loader, LowpassFilter, Matrix, Mesh, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, OscillatorSound, Particle, ParticleOptions, ParticleSystem, PitchShiftFilter, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, Random, Rectangle, RenderBackendType, RenderNode, RenderTarget, RenderTargetPass, RenderTexture, RendererRegistry, RenderingPrimitives, ReverbFilter, Sampler, ScaleAffector, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderUniform, Signal, Size, Sound, SoundFactory, SoundPoolStrategy, Sprite, SpriteFlags, Spritesheet, SteamControllerGamepadMapping, SteamDeckGamepadMapping, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, TorqueAffector, Tween, TweenManager, TweenState, UniversalEmitter, Vector, Video, VideoFactory, View, ViewFlags, VocoderFilter, VoronoiRegion, VttAsset, VttFactory, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2ParticleRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2ShaderFilter, WebGl2SpriteRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuMeshRenderer, WebGpuParticleRenderer, WebGpuShaderFilter, WebGpuSpriteRenderer, WorkletFilter, WrapModes, XboxGamepadMapping, bezierCurveTo, buildCircle, buildEllipse, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, crossFade, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, emptyArrayBuffer, getAudioContext, getAudioManager, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionEllipseCircle, getCollisionEllipseRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, milliseconds, minutes, noop$1 as noop, onAudioContextReady, parseGamepadDescriptor, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, registerWorkletProcessor, removeArrayItems, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, substepSweep, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst, tau, trimRotation, upgradeFragmentShaderToGl300, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
27636
28231
  //# sourceMappingURL=exo.esm.js.map