three-gamepad-controls 0.24.3 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,7 @@ pn test:coverage
92
92
  | --- | --- |
93
93
  | `0.184.x` | `<=0.22.x` |
94
94
  | `0.185.x` | `0.23.x` |
95
- | `0.186.x` | `0.24.x` |
95
+ | `0.186.x` | `0.24.x`, `0.25.x` |
96
96
 
97
97
  ## 📖 Documentation
98
98
 
@@ -63,7 +63,7 @@ var GamepadControls = class extends EventDispatcher {
63
63
  */
64
64
  #handleGamepadConnected(event) {
65
65
  this.gamepad = this.#gamepadInput.gamepad;
66
- this.onGamepadConnected(event.gamepad);
66
+ this.onGamepadConnected(event.detail.gamepad);
67
67
  }
68
68
  /**
69
69
  * Forwards an input disconnection event to the overridable lifecycle hook.
@@ -72,7 +72,7 @@ var GamepadControls = class extends EventDispatcher {
72
72
  */
73
73
  #handleGamepadDisconnected(event) {
74
74
  this.gamepad = this.#gamepadInput.gamepad;
75
- this.onGamepadDisconnected(event.gamepad);
75
+ this.onGamepadDisconnected(event.detail.gamepad);
76
76
  }
77
77
  /**
78
78
  * Advances the controller by one frame. Call this inside your render loop.
@@ -19,7 +19,7 @@ export type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
19
19
  */
20
20
  lookSpeed: number;
21
21
  /**
22
- * Movement stick axes and processing pipeline.
22
+ * Axes and processing pipeline for yaw-based movement in the XZ plane.
23
23
  */
24
24
  moveStick: GamepadStickBindingOptions;
25
25
  /**
@@ -32,12 +32,12 @@ export type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
32
32
  */
33
33
  buttonDeadzone: number;
34
34
  /**
35
- * Button index for **moving up** (analog trigger value used for proportional speed).
35
+ * Button index for **moving up** along Y (analog value used for proportional speed).
36
36
  * @default 6 - Left trigger
37
37
  */
38
38
  buttonMoveUp: number;
39
39
  /**
40
- * Button index for **moving down** (analog trigger value used for proportional speed).
40
+ * Button index for **moving down** along Y (analog value used for proportional speed).
41
41
  * @default 7 - Right trigger
42
42
  */
43
43
  buttonMoveDown: number;
@@ -47,6 +47,8 @@ export type GamepadFirstPersonControlsOptions = GamepadControlsOptions & {
47
47
  *
48
48
  * Gamepad input is additive with keyboard/mouse input - call
49
49
  * `gamepadControls.update(delta)` before `controls.update(delta)` each frame.
50
+ * Movement follows the native keyboard frame: yaw-based translation in XZ
51
+ * and climbing along Y, independent of camera pitch and roll.
50
52
  * Bindings and speeds are configurable via {@link GamepadFirstPersonControlsOptions}.
51
53
  */
52
54
  export declare class GamepadFirstPersonControls extends GamepadControls {
@@ -26,6 +26,8 @@ const LOOK_SPEED_SCALE = 36e3;
26
26
  *
27
27
  * Gamepad input is additive with keyboard/mouse input - call
28
28
  * `gamepadControls.update(delta)` before `controls.update(delta)` each frame.
29
+ * Movement follows the native keyboard frame: yaw-based translation in XZ
30
+ * and climbing along Y, independent of camera pitch and roll.
29
31
  * Bindings and speeds are configurable via {@link GamepadFirstPersonControlsOptions}.
30
32
  */
31
33
  var GamepadFirstPersonControls = class extends GamepadControls {
@@ -64,7 +66,9 @@ var GamepadFirstPersonControls = class extends GamepadControls {
64
66
  this.#applyLook(deltaTime, lookSpeed, lookStick);
65
67
  }
66
68
  /**
67
- * Applies local translation input to FirstPersonControls' object.
69
+ * Applies translation in the native keyboard frame, using yaw before look.
70
+ * Forward height gain uses the initial Y position; triggers are filtered
71
+ * independently before combining their proportional Y displacement.
68
72
  *
69
73
  * @param deltaTime - Seconds since the last frame.
70
74
  * @param moveSpeed - User-configured movement speed multiplier.
@@ -78,21 +82,24 @@ var GamepadFirstPersonControls = class extends GamepadControls {
78
82
  const input = this.gamepadInput;
79
83
  const moveMult = deltaTime * controls.movementSpeed * moveSpeed;
80
84
  const move = input.stick(moveStick.xAxis, moveStick.yAxis, moveStick.pipeline);
81
- const forward = move.y;
82
- if (forward !== 0) {
83
- let distance = forward * moveMult;
84
- if (forward < 0 && controls.heightSpeed) {
85
+ if (move.x !== 0 || move.y !== 0) {
86
+ const yaw = MathUtils.degToRad(this.#getOrientation().lon);
87
+ const sinYaw = Math.sin(yaw);
88
+ const cosYaw = Math.cos(yaw);
89
+ const forward = -move.y;
90
+ let forwardDistance = forward * moveMult;
91
+ if (forward > 0 && controls.heightSpeed) {
85
92
  const heightDelta = MathUtils.clamp(controls.object.position.y, controls.heightMin, controls.heightMax) - controls.heightMin;
86
- distance -= -forward * deltaTime * heightDelta * controls.heightCoef * moveSpeed;
93
+ forwardDistance += forward * deltaTime * heightDelta * controls.heightCoef * moveSpeed;
87
94
  }
88
- controls.object.translateZ(distance);
95
+ const strafeDistance = move.x * moveMult;
96
+ controls.object.position.x += sinYaw * forwardDistance - cosYaw * strafeDistance;
97
+ controls.object.position.z += cosYaw * forwardDistance + sinYaw * strafeDistance;
89
98
  }
90
- const strafe = move.x;
91
- if (strafe !== 0) controls.object.translateX(strafe * moveMult);
92
99
  const up = input.buttonValue(buttonMoveUp);
93
100
  const down = input.buttonValue(buttonMoveDown);
94
- if (up > buttonDeadzone) controls.object.translateY(up * moveMult);
95
- if (down > buttonDeadzone) controls.object.translateY(-down * moveMult);
101
+ const climb = (up > buttonDeadzone ? up : 0) - (down > buttonDeadzone ? down : 0);
102
+ if (climb !== 0) controls.object.position.y += climb * moveMult;
96
103
  }
97
104
  /**
98
105
  * Applies camera look input while keeping FirstPersonControls state in sync.
@@ -1,30 +1,33 @@
1
1
  import { GamepadStick, GamepadStickPipeline } from "./gamepad-stick-processing.js";
2
- import { EventDispatcher } from "three";
3
2
  //#region src/gamepad-input.d.ts
4
3
  /**
5
- * Event map for {@link GamepadInput}.
4
+ * Native custom event map for {@link GamepadInput}.
5
+ * Gamepad snapshots are available through `event.detail.gamepad`.
6
6
  */
7
7
  export type GamepadInputEventMap = {
8
8
  /**
9
9
  * Fired when a gamepad is connected and becomes active.
10
10
  */
11
- connected: {
11
+ connected: CustomEvent<{
12
12
  /**
13
13
  * Gamepad snapshot that became active.
14
14
  */
15
15
  gamepad: Gamepad;
16
- };
16
+ }>;
17
17
  /**
18
18
  * Fired on a matching browser disconnection event or when polling observes
19
19
  * the active slot missing or disconnected. Continuous snapshots in the same
20
20
  * slot do not establish a physical-device identity or signal replacement.
21
21
  */
22
- disconnected: {
22
+ disconnected: CustomEvent<{
23
23
  /**
24
24
  * Gamepad snapshot that was active before disconnection.
25
25
  */
26
26
  gamepad: Gamepad;
27
- };
27
+ }>;
28
+ };
29
+ type GamepadInputEventListener<K extends keyof GamepadInputEventMap> = ((this: GamepadInput, event: GamepadInputEventMap[K]) => void) | {
30
+ handleEvent(event: GamepadInputEventMap[K]): void;
28
31
  };
29
32
  /**
30
33
  * Configuration for {@link GamepadInput}.
@@ -66,8 +69,13 @@ export type GamepadAxisOptions = {
66
69
  * Gamepad input state reader for gameplay, menus, and custom actions.
67
70
  *
68
71
  * Call {@link update} once per frame before reading button transitions or axes.
72
+ * Connection notifications are synchronous native `CustomEvent` objects with
73
+ * the gamepad snapshot in `event.detail.gamepad`. Input state is updated before
74
+ * listeners run. Inherited `dispatchEvent()` requires an `Event` instance;
75
+ * listener exceptions are reported by the browser instead of propagating to
76
+ * the dispatch caller.
69
77
  */
70
- export declare class GamepadInput extends EventDispatcher<GamepadInputEventMap> {
78
+ export declare class GamepadInput extends EventTarget {
71
79
  #private;
72
80
  /**
73
81
  * When `false`, polling through `update()` is paused and the last observed
@@ -84,6 +92,67 @@ export declare class GamepadInput extends EventDispatcher<GamepadInputEventMap>
84
92
  * `MIN_GAMEPAD_INDEX` through `MAX_GAMEPAD_INDEX`.
85
93
  */
86
94
  constructor(options?: Partial<GamepadInputOptions>);
95
+ /**
96
+ * Adds a native event listener with typed connection event details.
97
+ *
98
+ * Connection callbacks receive a `CustomEvent` with `detail.gamepad`.
99
+ * Supports callback functions and objects with `handleEvent`. Registering
100
+ * the same event type, listener, and capture flag again does not duplicate it.
101
+ * A regular callback's `this` is this input; a listener object's `this` is
102
+ * the listener object. Manage subscriptions separately from {@link dispose}.
103
+ *
104
+ * @param type - Event name to observe.
105
+ * @param listener - Callback or listener object, or `null` for a no-op.
106
+ * @param options - Capture flag (default `false`) or native options including
107
+ * `capture`, `once`, `passive`, and `signal`.
108
+ * @example
109
+ * ```ts
110
+ * const subscriptions = new AbortController();
111
+ * input.addEventListener("connected", (event) => {
112
+ * console.log(event.detail.gamepad.id);
113
+ * }, { signal: subscriptions.signal });
114
+ * subscriptions.abort();
115
+ * ```
116
+ */
117
+ addEventListener<K extends keyof GamepadInputEventMap>(type: K, listener: GamepadInputEventListener<K> | null, options?: boolean | AddEventListenerOptions): void;
118
+ /**
119
+ * Adds a listener using standard `EventTarget` types for any event name.
120
+ *
121
+ * @param type - Event name to observe.
122
+ * @param listener - Native callback or listener object, or `null` for a no-op.
123
+ * @param options - Capture flag or native event listener options.
124
+ */
125
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
126
+ /**
127
+ * Removes a native event listener with the matching capture option.
128
+ *
129
+ * The event type, callback or object identity, and capture flag must match
130
+ * registration. Other registration options do not affect removal. A missing
131
+ * listener or `null` is a no-op. Removal during dispatch prevents a pending
132
+ * invocation of that listener.
133
+ *
134
+ * @param type - Event name being observed.
135
+ * @param listener - Previously registered callback or listener object, or `null`.
136
+ * @param options - Capture flag (default `false`) or an object with `capture`.
137
+ * @example
138
+ * ```ts
139
+ * const onConnected = (event: GamepadInputEventMap["connected"]) => {
140
+ * console.log(event.detail.gamepad.id);
141
+ * };
142
+ * input.addEventListener("connected", onConnected);
143
+ * input.removeEventListener("connected", onConnected);
144
+ * ```
145
+ */
146
+ removeEventListener<K extends keyof GamepadInputEventMap>(type: K, listener: GamepadInputEventListener<K> | null, options?: boolean | EventListenerOptions): void;
147
+ /**
148
+ * Removes a listener using standard `EventTarget` types for any event name.
149
+ *
150
+ * @param type - Event name being observed.
151
+ * @param listener - Previously registered native callback or listener object,
152
+ * or `null` for a no-op.
153
+ * @param options - Capture flag or an object with the matching `capture` flag.
154
+ */
155
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
87
156
  /**
88
157
  * The currently active gamepad, or `null` if no gamepad is connected.
89
158
  *
@@ -1,7 +1,6 @@
1
1
  import { isGamepadVibrationSupported, playGamepadVibrationEffect, resetGamepadVibration } from "./gamepad-haptics.js";
2
2
  import { GamepadManager } from "./gamepad-manager.js";
3
3
  import { DEFAULT_GAMEPAD_STICK_PIPELINE } from "./gamepad-stick-processing.js";
4
- import { EventDispatcher } from "three";
5
4
  //#region src/gamepad-input.ts
6
5
  const DEFAULT_GAMEPAD_INPUT_OPTIONS = {
7
6
  axisDeadzone: .1,
@@ -49,8 +48,13 @@ const getGamepadButtonValue = (gamepad, button) => {
49
48
  * Gamepad input state reader for gameplay, menus, and custom actions.
50
49
  *
51
50
  * Call {@link update} once per frame before reading button transitions or axes.
51
+ * Connection notifications are synchronous native `CustomEvent` objects with
52
+ * the gamepad snapshot in `event.detail.gamepad`. Input state is updated before
53
+ * listeners run. Inherited `dispatchEvent()` requires an `Event` instance;
54
+ * listener exceptions are reported by the browser instead of propagating to
55
+ * the dispatch caller.
52
56
  */
53
- var GamepadInput = class extends EventDispatcher {
57
+ var GamepadInput = class extends EventTarget {
54
58
  /**
55
59
  * When `false`, polling through `update()` is paused and the last observed
56
60
  * state, including button transitions, is retained. Browser listeners remain
@@ -86,6 +90,12 @@ var GamepadInput = class extends EventDispatcher {
86
90
  window.addEventListener("gamepadconnected", this.#onGamepadConnected);
87
91
  window.addEventListener("gamepaddisconnected", this.#onGamepadDisconnected);
88
92
  }
93
+ addEventListener(type, listener, options) {
94
+ super.addEventListener(type, listener, options);
95
+ }
96
+ removeEventListener(type, listener, options) {
97
+ super.removeEventListener(type, listener, options);
98
+ }
89
99
  /**
90
100
  * Forwards a browser connection event to the active-gamepad adoption logic.
91
101
  *
@@ -156,19 +166,13 @@ var GamepadInput = class extends EventDispatcher {
156
166
  if (connected !== null) {
157
167
  this.#gamepad = gamepad;
158
168
  this.#syncButtonState({ seedPrevious: true });
159
- this.dispatchEvent({
160
- type: "connected",
161
- gamepad: connected
162
- });
169
+ this.dispatchEvent(new CustomEvent("connected", { detail: { gamepad: connected } }));
163
170
  return;
164
171
  }
165
172
  if (disconnected !== null) {
166
173
  this.#gamepad = null;
167
174
  this.#clearButtonState();
168
- this.dispatchEvent({
169
- type: "disconnected",
170
- gamepad: disconnected
171
- });
175
+ this.dispatchEvent(new CustomEvent("disconnected", { detail: { gamepad: disconnected } }));
172
176
  return;
173
177
  }
174
178
  this.#gamepad = gamepad;
@@ -291,10 +295,7 @@ var GamepadInput = class extends EventDispatcher {
291
295
  if (connectedGamepad === null) return;
292
296
  this.#gamepad = connectedGamepad;
293
297
  this.#syncButtonState({ seedPrevious: true });
294
- this.dispatchEvent({
295
- type: "connected",
296
- gamepad: connectedGamepad
297
- });
298
+ this.dispatchEvent(new CustomEvent("connected", { detail: { gamepad: connectedGamepad } }));
298
299
  }
299
300
  /**
300
301
  * Handles a browser disconnection event for the active gamepad.
@@ -309,10 +310,7 @@ var GamepadInput = class extends EventDispatcher {
309
310
  if (disconnectedGamepad === null) return;
310
311
  this.#gamepad = null;
311
312
  this.#clearButtonState();
312
- this.dispatchEvent({
313
- type: "disconnected",
314
- gamepad: disconnectedGamepad
315
- });
313
+ this.dispatchEvent(new CustomEvent("disconnected", { detail: { gamepad: disconnectedGamepad } }));
316
314
  }
317
315
  /**
318
316
  * Refreshes current and previous pressed-button sets from the active snapshot.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "three-gamepad-controls",
3
3
  "description": "Gamepad support for Three.js controls.",
4
- "version": "0.24.3",
4
+ "version": "0.25.1",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/luckasnix/three-gamepad-controls#readme",
7
7
  "author": {