@gjsify/gamepad 0.1.9

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.
@@ -0,0 +1,16 @@
1
+ import type Manette from '@girs/manette-0.2';
2
+ import type { GamepadHapticActuator, GamepadHapticEffectType, GamepadEffectParameters, GamepadHapticsResult } from './gamepad.js';
3
+ /**
4
+ * Wraps libmanette's rumble support as a W3C GamepadHapticActuator.
5
+ *
6
+ * libmanette supports dual-rumble with strong/weak magnitude control
7
+ * via `Device.rumble(strong_magnitude, weak_magnitude, milliseconds)`.
8
+ * Magnitudes are in the range 0–65535 (uint16).
9
+ */
10
+ export declare class ManetteHapticActuator implements GamepadHapticActuator {
11
+ readonly effects: readonly GamepadHapticEffectType[];
12
+ private _device;
13
+ constructor(device: Manette.Device);
14
+ playEffect(type: GamepadHapticEffectType, params?: GamepadEffectParameters): Promise<GamepadHapticsResult>;
15
+ reset(): Promise<GamepadHapticsResult>;
16
+ }
@@ -0,0 +1,9 @@
1
+ export { GamepadButton } from './gamepad-button.js';
2
+ export { Gamepad } from './gamepad.js';
3
+ export type { GamepadMappingType, GamepadHapticActuator, GamepadHapticEffectType, GamepadHapticsResult, GamepadEffectParameters, } from './gamepad.js';
4
+ export { GamepadEvent } from './gamepad-event.js';
5
+ export type { GamepadEventInit } from './gamepad-event.js';
6
+ export { GamepadManager } from './gamepad-manager.js';
7
+ export { ManetteHapticActuator } from './haptic-actuator.js';
8
+ export { MANETTE_TO_W3C_BUTTON, ManetteButton, W3CButton, W3C_BUTTON_COUNT } from './button-mapping.js';
9
+ export { MANETTE_TO_W3C_AXIS, ManetteAxis, W3CAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@gjsify/gamepad",
3
+ "version": "0.1.9",
4
+ "description": "Gamepad Web API for GJS using libmanette as backend",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ },
13
+ "./register": {
14
+ "types": "./lib/types/register.d.ts",
15
+ "default": "./lib/esm/register.js"
16
+ }
17
+ },
18
+ "sideEffects": [
19
+ "./lib/esm/register.js"
20
+ ],
21
+ "scripts": {
22
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs || exit 0",
23
+ "check": "tsc --noEmit",
24
+ "build": "yarn build:gjsify && yarn build:types",
25
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
26
+ "build:types": "tsc",
27
+ "build:test": "yarn build:test:gjs",
28
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
29
+ "test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
30
+ "test:gjs": "gjs -m test.gjs.mjs"
31
+ },
32
+ "keywords": [
33
+ "gjs",
34
+ "gamepad",
35
+ "libmanette",
36
+ "controller"
37
+ ],
38
+ "dependencies": {
39
+ "@gjsify/dom-events": "^0.1.9"
40
+ },
41
+ "devDependencies": {
42
+ "@girs/gjs": "^4.0.0-rc.2",
43
+ "@girs/glib-2.0": "^2.88.0-4.0.0-rc.2",
44
+ "@girs/manette-0.2": "^0.2.13-4.0.0-rc.2",
45
+ "@gjsify/cli": "^0.1.9",
46
+ "@gjsify/unit": "^0.1.9",
47
+ "@types/node": "^25.6.0",
48
+ "typescript": "^6.0.2"
49
+ }
50
+ }
@@ -0,0 +1,50 @@
1
+ // Gamepad Web API — axis mapping from libmanette to W3C standard gamepad layout
2
+ // Reference: https://w3c.github.io/gamepad/#remapping
3
+ //
4
+ // NOTE: Unlike buttons (which use Linux BTN_* hardware codes), libmanette 0.2
5
+ // axis events use SDL logical indices: 0=leftx, 1=lefty, 2=rightx, 3=righty,
6
+ // 4=lefttrigger, 5=righttrigger. The hardware ABS_* code is available via
7
+ // event.get_hardware_code() but the main axis index from get_absolute() is
8
+ // the SDL-mapped logical index.
9
+
10
+ /**
11
+ * SDL logical axis indices as reported by Manette.Event.get_absolute().
12
+ * These are the SDL gamepad mapping indices, NOT Linux ABS_* hardware codes.
13
+ */
14
+ export const ManetteAxis = {
15
+ LEFT_X: 0, // SDL leftx
16
+ LEFT_Y: 1, // SDL lefty
17
+ RIGHT_X: 2, // SDL rightx
18
+ RIGHT_Y: 3, // SDL righty
19
+ LEFT_TRIGGER: 4, // SDL lefttrigger
20
+ RIGHT_TRIGGER: 5, // SDL righttrigger
21
+ } as const;
22
+
23
+ /**
24
+ * W3C standard gamepad axis indices.
25
+ * https://w3c.github.io/gamepad/#remapping
26
+ */
27
+ export const W3CAxis = {
28
+ LEFT_STICK_X: 0,
29
+ LEFT_STICK_Y: 1,
30
+ RIGHT_STICK_X: 2,
31
+ RIGHT_STICK_Y: 3,
32
+ } as const;
33
+
34
+ /** Total number of axes in the W3C standard mapping. */
35
+ export const W3C_AXIS_COUNT = 4;
36
+
37
+ /**
38
+ * Maps SDL logical axis → W3C axis index.
39
+ * Stick axes (0–3) map 1:1. Trigger axes (4–5) are NOT in this map —
40
+ * they are handled separately in gamepad-manager as buttons[6]/buttons[7].
41
+ */
42
+ export const MANETTE_TO_W3C_AXIS: ReadonlyMap<number, number> = new Map([
43
+ [ManetteAxis.LEFT_X, W3CAxis.LEFT_STICK_X],
44
+ [ManetteAxis.LEFT_Y, W3CAxis.LEFT_STICK_Y],
45
+ [ManetteAxis.RIGHT_X, W3CAxis.RIGHT_STICK_X],
46
+ [ManetteAxis.RIGHT_Y, W3CAxis.RIGHT_STICK_Y],
47
+ ]);
48
+
49
+ /** Threshold above which an analog trigger is considered "pressed". */
50
+ export const TRIGGER_PRESS_THRESHOLD = 0.5;
@@ -0,0 +1,87 @@
1
+ // Gamepad Web API — button mapping from libmanette to W3C standard gamepad layout
2
+ // Reference: https://w3c.github.io/gamepad/#remapping
3
+ //
4
+ // IMPORTANT: libmanette 0.2 delivers Linux input event codes (BTN_*) in
5
+ // Event.get_button(), NOT the Manette.Button enum values from the docs.
6
+ // The enum (SOUTH=5, EAST=7, …) describes the semantic meaning, but the
7
+ // actual values transmitted over signals are the kernel BTN_* constants.
8
+ // Reference: linux/input-event-codes.h
9
+
10
+ /**
11
+ * Linux BTN_* input event codes as reported by Manette.Event.get_button().
12
+ * These are the actual values libmanette 0.2 passes in button-press/release signals.
13
+ */
14
+ export const LinuxButton = {
15
+ BTN_SOUTH: 304, // 0x130 — A (Xbox), B (Nintendo), Cross (PlayStation)
16
+ BTN_EAST: 305, // 0x131 — B (Xbox), A (Nintendo), Circle (PlayStation)
17
+ BTN_C: 306, // 0x132
18
+ BTN_NORTH: 307, // 0x133 — Y (Xbox), X (Nintendo), Triangle (PlayStation)
19
+ BTN_WEST: 308, // 0x134 — X (Xbox), Y (Nintendo), Square (PlayStation)
20
+ BTN_Z: 309, // 0x135
21
+ BTN_TL: 310, // 0x136 — Left shoulder (L, LB)
22
+ BTN_TR: 311, // 0x137 — Right shoulder (R, RB)
23
+ BTN_TL2: 312, // 0x138 — Left trigger (LT, L2)
24
+ BTN_TR2: 313, // 0x139 — Right trigger (RT, R2)
25
+ BTN_SELECT: 314, // 0x13a — Select / Back / Share
26
+ BTN_START: 315, // 0x13b — Start / Menu / Options
27
+ BTN_MODE: 316, // 0x13c — Home / Guide / PS
28
+ BTN_THUMBL: 317, // 0x13d — Left stick click (L3)
29
+ BTN_THUMBR: 318, // 0x13e — Right stick click (R3)
30
+ BTN_DPAD_UP: 544, // 0x220
31
+ BTN_DPAD_DOWN: 545, // 0x221
32
+ BTN_DPAD_LEFT: 546, // 0x222
33
+ BTN_DPAD_RIGHT:547, // 0x223
34
+ } as const;
35
+
36
+ /**
37
+ * W3C standard gamepad button indices.
38
+ * https://w3c.github.io/gamepad/#remapping
39
+ */
40
+ export const W3CButton = {
41
+ FACE_1: 0, // A (Xbox), B (Nintendo), Cross (PlayStation)
42
+ FACE_2: 1, // B (Xbox), A (Nintendo), Circle (PlayStation)
43
+ FACE_3: 2, // X (Xbox), Y (Nintendo), Square (PlayStation)
44
+ FACE_4: 3, // Y (Xbox), X (Nintendo), Triangle (PlayStation)
45
+ LEFT_BUMPER: 4,
46
+ RIGHT_BUMPER: 5,
47
+ LEFT_TRIGGER: 6, // Analog trigger — also populated from axis events
48
+ RIGHT_TRIGGER: 7, // Analog trigger — also populated from axis events
49
+ SELECT: 8,
50
+ START: 9,
51
+ LEFT_STICK: 10,
52
+ RIGHT_STICK: 11,
53
+ DPAD_UP: 12,
54
+ DPAD_DOWN: 13,
55
+ DPAD_LEFT: 14,
56
+ DPAD_RIGHT: 15,
57
+ HOME: 16,
58
+ } as const;
59
+
60
+ /** Total number of buttons in the W3C standard mapping. */
61
+ export const W3C_BUTTON_COUNT = 17;
62
+
63
+ /**
64
+ * Maps Linux BTN_* code → W3C standard button index.
65
+ */
66
+ export const MANETTE_TO_W3C_BUTTON: ReadonlyMap<number, number> = new Map([
67
+ [LinuxButton.BTN_SOUTH, W3CButton.FACE_1],
68
+ [LinuxButton.BTN_EAST, W3CButton.FACE_2],
69
+ [LinuxButton.BTN_WEST, W3CButton.FACE_3],
70
+ [LinuxButton.BTN_NORTH, W3CButton.FACE_4],
71
+ [LinuxButton.BTN_TL, W3CButton.LEFT_BUMPER],
72
+ [LinuxButton.BTN_TR, W3CButton.RIGHT_BUMPER],
73
+ [LinuxButton.BTN_TL2, W3CButton.LEFT_TRIGGER],
74
+ [LinuxButton.BTN_TR2, W3CButton.RIGHT_TRIGGER],
75
+ [LinuxButton.BTN_SELECT, W3CButton.SELECT],
76
+ [LinuxButton.BTN_START, W3CButton.START],
77
+ [LinuxButton.BTN_THUMBL, W3CButton.LEFT_STICK],
78
+ [LinuxButton.BTN_THUMBR, W3CButton.RIGHT_STICK],
79
+ [LinuxButton.BTN_DPAD_UP, W3CButton.DPAD_UP],
80
+ [LinuxButton.BTN_DPAD_DOWN, W3CButton.DPAD_DOWN],
81
+ [LinuxButton.BTN_DPAD_LEFT, W3CButton.DPAD_LEFT],
82
+ [LinuxButton.BTN_DPAD_RIGHT, W3CButton.DPAD_RIGHT],
83
+ [LinuxButton.BTN_MODE, W3CButton.HOME],
84
+ ]);
85
+
86
+ // Keep the old name exported for backward compat in tests/examples
87
+ export { LinuxButton as ManetteButton };
@@ -0,0 +1,22 @@
1
+ // Gamepad Web API — GamepadButton
2
+ // Reference: https://w3c.github.io/gamepad/#dom-gamepadbutton
3
+
4
+ /**
5
+ * Represents the state of a single button on a gamepad.
6
+ * https://w3c.github.io/gamepad/#dom-gamepadbutton
7
+ */
8
+ export class GamepadButton {
9
+ pressed: boolean;
10
+ touched: boolean;
11
+ value: number;
12
+
13
+ constructor(pressed = false, touched = false, value = 0) {
14
+ this.pressed = pressed;
15
+ this.touched = touched;
16
+ this.value = value;
17
+ }
18
+
19
+ get [Symbol.toStringTag]() {
20
+ return 'GamepadButton';
21
+ }
22
+ }
@@ -0,0 +1,29 @@
1
+ // Gamepad Web API — GamepadEvent
2
+ // Reference: https://w3c.github.io/gamepad/#dom-gamepadevent
3
+
4
+ import { Event } from '@gjsify/dom-events';
5
+ import type { Gamepad } from './gamepad.js';
6
+
7
+ export interface GamepadEventInit {
8
+ gamepad: Gamepad;
9
+ bubbles?: boolean;
10
+ cancelable?: boolean;
11
+ composed?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Fired on the Window when a gamepad is connected or disconnected.
16
+ * https://w3c.github.io/gamepad/#dom-gamepadevent
17
+ */
18
+ export class GamepadEvent extends Event {
19
+ readonly gamepad: Gamepad;
20
+
21
+ constructor(type: string, eventInitDict: GamepadEventInit) {
22
+ super(type, eventInitDict);
23
+ this.gamepad = eventInitDict.gamepad;
24
+ }
25
+
26
+ get [Symbol.toStringTag]() {
27
+ return 'GamepadEvent';
28
+ }
29
+ }
@@ -0,0 +1,299 @@
1
+ // Gamepad Web API — GamepadManager
2
+ // Bridges libmanette's event-driven model to the W3C polling-based Gamepad API.
3
+ // Reference: https://w3c.github.io/gamepad/
4
+ // Reimplemented for GJS using libmanette (gi://Manette)
5
+
6
+ import type Manette from '@girs/manette-0.2';
7
+ import { GamepadButton } from './gamepad-button.js';
8
+ import { Gamepad } from './gamepad.js';
9
+ import { GamepadEvent } from './gamepad-event.js';
10
+ import { ManetteHapticActuator } from './haptic-actuator.js';
11
+ import { MANETTE_TO_W3C_BUTTON, W3C_BUTTON_COUNT } from './button-mapping.js';
12
+ import { MANETTE_TO_W3C_AXIS, ManetteAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
13
+ import { W3CButton } from './button-mapping.js';
14
+
15
+ /** Internal mutable state for a single connected gamepad. */
16
+ interface DeviceState {
17
+ device: Manette.Device;
18
+ index: number;
19
+ connected: boolean;
20
+ timestamp: number;
21
+ buttons: Float64Array;
22
+ buttonsPressed: boolean[];
23
+ axes: Float64Array;
24
+ hapticActuator: ManetteHapticActuator;
25
+ signalIds: number[];
26
+ }
27
+
28
+ const MAX_GAMEPADS = 4;
29
+
30
+ /**
31
+ * Singleton manager that wraps Manette.Monitor and maintains gamepad state.
32
+ *
33
+ * libmanette fires GObject signals on button/axis changes. This manager
34
+ * caches the latest state so that `getGamepads()` can return a snapshot
35
+ * matching the W3C Gamepad API's polling model.
36
+ */
37
+ export class GamepadManager {
38
+ private _monitor: Manette.Monitor | null = null;
39
+ private _slots: (DeviceState | null)[] = new Array(MAX_GAMEPADS).fill(null);
40
+ private _monitorSignalIds: number[] = [];
41
+ private _ManetteModule: typeof Manette | null = null;
42
+ private _initPromise: Promise<void> | null = null;
43
+ private _initialized = false;
44
+
45
+ /**
46
+ * Lazily initialize the Manette.Monitor.
47
+ * Called on first `getGamepads()` invocation.
48
+ */
49
+ private _ensureInit(): void {
50
+ if (this._initialized) return;
51
+ if (this._initPromise) return;
52
+
53
+ this._initPromise = this._init();
54
+ }
55
+
56
+ private async _init(): Promise<void> {
57
+ try {
58
+ const mod = await import('gi://Manette');
59
+ this._ManetteModule = mod.default;
60
+ } catch {
61
+ // libmanette not available — getGamepads() will return empty array
62
+ this._initialized = true;
63
+ return;
64
+ }
65
+
66
+ const monitor = new this._ManetteModule.Monitor();
67
+ this._monitor = monitor;
68
+
69
+ // Enumerate already-connected devices
70
+ const iter = monitor.iterate();
71
+ let result = iter.next();
72
+ while (result[0]) {
73
+ const device = result[1];
74
+ if (device) {
75
+ this._onDeviceConnected(device);
76
+ }
77
+ result = iter.next();
78
+ }
79
+
80
+ // Listen for future connect/disconnect
81
+ this._monitorSignalIds.push(
82
+ monitor.connect('device-connected', (_monitor: Manette.Monitor, device: Manette.Device) => {
83
+ this._onDeviceConnected(device);
84
+ }),
85
+ monitor.connect('device-disconnected', (_monitor: Manette.Monitor, device: Manette.Device) => {
86
+ this._onDeviceDisconnected(device);
87
+ }),
88
+ );
89
+
90
+ this._initialized = true;
91
+ }
92
+
93
+ private _onDeviceConnected(device: Manette.Device): void {
94
+ // Find a free slot
95
+ let slotIndex = -1;
96
+ for (let i = 0; i < MAX_GAMEPADS; i++) {
97
+ if (this._slots[i] === null) {
98
+ slotIndex = i;
99
+ break;
100
+ }
101
+ }
102
+ if (slotIndex === -1) return; // All slots occupied
103
+
104
+ const state: DeviceState = {
105
+ device,
106
+ index: slotIndex,
107
+ connected: true,
108
+ timestamp: performance.now(),
109
+ buttons: new Float64Array(W3C_BUTTON_COUNT),
110
+ buttonsPressed: new Array(W3C_BUTTON_COUNT).fill(false),
111
+ axes: new Float64Array(W3C_AXIS_COUNT),
112
+ hapticActuator: new ManetteHapticActuator(device),
113
+ signalIds: [],
114
+ };
115
+
116
+ // Wire up device signals
117
+ state.signalIds.push(
118
+ device.connect('button-press-event', (_device: Manette.Device, event: Manette.Event) => {
119
+ this._onButtonPress(state, event);
120
+ }),
121
+ device.connect('button-release-event', (_device: Manette.Device, event: Manette.Event) => {
122
+ this._onButtonRelease(state, event);
123
+ }),
124
+ device.connect('absolute-axis-event', (_device: Manette.Device, event: Manette.Event) => {
125
+ this._onAxisChange(state, event);
126
+ }),
127
+ device.connect('hat-axis-event', (_device: Manette.Device, event: Manette.Event) => {
128
+ this._onHatChange(state, event);
129
+ }),
130
+ device.connect('disconnected', () => {
131
+ this._onDeviceDisconnected(device);
132
+ }),
133
+ );
134
+
135
+ this._slots[slotIndex] = state;
136
+
137
+ // Dispatch gamepadconnected event
138
+ const snapshot = this._createSnapshot(state);
139
+ if (snapshot) {
140
+ globalThis.dispatchEvent?.(new GamepadEvent('gamepadconnected', { gamepad: snapshot }) as unknown as Event);
141
+ }
142
+ }
143
+
144
+ private _onDeviceDisconnected(device: Manette.Device): void {
145
+ const state = this._findStateByDevice(device);
146
+ if (!state) return;
147
+
148
+ // Disconnect all device signals
149
+ for (const id of state.signalIds) {
150
+ device.disconnect(id);
151
+ }
152
+
153
+ state.connected = false;
154
+ const snapshot = this._createSnapshot(state);
155
+ this._slots[state.index] = null;
156
+
157
+ // Dispatch gamepaddisconnected event
158
+ if (snapshot) {
159
+ globalThis.dispatchEvent?.(new GamepadEvent('gamepaddisconnected', { gamepad: snapshot }) as unknown as Event);
160
+ }
161
+ }
162
+
163
+ private _onButtonPress(state: DeviceState, event: Manette.Event): void {
164
+ const [ok, button] = event.get_button();
165
+ if (!ok) return;
166
+
167
+ const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
168
+ if (w3cIdx === undefined) return;
169
+
170
+ state.buttons[w3cIdx] = 1.0;
171
+ state.buttonsPressed[w3cIdx] = true;
172
+ state.timestamp = performance.now();
173
+ }
174
+
175
+ private _onButtonRelease(state: DeviceState, event: Manette.Event): void {
176
+ const [ok, button] = event.get_button();
177
+ if (!ok) return;
178
+
179
+ const w3cIdx = MANETTE_TO_W3C_BUTTON.get(button);
180
+ if (w3cIdx === undefined) return;
181
+
182
+ state.buttons[w3cIdx] = 0.0;
183
+ state.buttonsPressed[w3cIdx] = false;
184
+ state.timestamp = performance.now();
185
+ }
186
+
187
+ private _onAxisChange(state: DeviceState, event: Manette.Event): void {
188
+ const [ok, axis, value] = event.get_absolute();
189
+ if (!ok) return;
190
+
191
+ const w3cAxisIdx = MANETTE_TO_W3C_AXIS.get(axis);
192
+ if (w3cAxisIdx !== undefined) {
193
+ // Stick axis → axes array
194
+ state.axes[w3cAxisIdx] = value;
195
+ } else if (axis === ManetteAxis.LEFT_TRIGGER) {
196
+ // Left trigger (SDL idx 4) → buttons[6] with analog value
197
+ const normalized = (value + 1) / 2; // libmanette: -1..1 → 0..1
198
+ state.buttons[W3CButton.LEFT_TRIGGER] = normalized;
199
+ state.buttonsPressed[W3CButton.LEFT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
200
+ } else if (axis === ManetteAxis.RIGHT_TRIGGER) {
201
+ // Right trigger (SDL idx 5) → buttons[7] with analog value
202
+ const normalized = (value + 1) / 2;
203
+ state.buttons[W3CButton.RIGHT_TRIGGER] = normalized;
204
+ state.buttonsPressed[W3CButton.RIGHT_TRIGGER] = normalized > TRIGGER_PRESS_THRESHOLD;
205
+ }
206
+
207
+ state.timestamp = performance.now();
208
+ }
209
+
210
+ private _onHatChange(state: DeviceState, event: Manette.Event): void {
211
+ const [ok, hatAxis, hatValue] = event.get_hat();
212
+ if (!ok) return;
213
+
214
+ // Hat axes: 0 = horizontal (left/right), 1 = vertical (up/down)
215
+ // Values: -1, 0, 1
216
+ if (hatAxis === 0) {
217
+ // Horizontal: negative = left, positive = right
218
+ state.buttonsPressed[W3CButton.DPAD_LEFT] = hatValue < 0;
219
+ state.buttons[W3CButton.DPAD_LEFT] = hatValue < 0 ? 1.0 : 0.0;
220
+ state.buttonsPressed[W3CButton.DPAD_RIGHT] = hatValue > 0;
221
+ state.buttons[W3CButton.DPAD_RIGHT] = hatValue > 0 ? 1.0 : 0.0;
222
+ } else if (hatAxis === 1) {
223
+ // Vertical: negative = up, positive = down
224
+ state.buttonsPressed[W3CButton.DPAD_UP] = hatValue < 0;
225
+ state.buttons[W3CButton.DPAD_UP] = hatValue < 0 ? 1.0 : 0.0;
226
+ state.buttonsPressed[W3CButton.DPAD_DOWN] = hatValue > 0;
227
+ state.buttons[W3CButton.DPAD_DOWN] = hatValue > 0 ? 1.0 : 0.0;
228
+ }
229
+
230
+ state.timestamp = performance.now();
231
+ }
232
+
233
+ private _findStateByDevice(device: Manette.Device): DeviceState | null {
234
+ for (const state of this._slots) {
235
+ if (state && state.device === device) return state;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ private _createSnapshot(state: DeviceState): Gamepad {
241
+ const buttons: GamepadButton[] = [];
242
+ for (let i = 0; i < W3C_BUTTON_COUNT; i++) {
243
+ buttons.push(new GamepadButton(
244
+ state.buttonsPressed[i],
245
+ state.buttonsPressed[i] || state.buttons[i] > 0,
246
+ state.buttons[i],
247
+ ));
248
+ }
249
+
250
+ return new Gamepad({
251
+ id: state.device.get_name() ?? `Gamepad (${state.device.get_guid()})`,
252
+ index: state.index,
253
+ connected: state.connected,
254
+ timestamp: state.timestamp,
255
+ mapping: 'standard',
256
+ axes: Array.from(state.axes),
257
+ buttons,
258
+ vibrationActuator: state.hapticActuator,
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Returns a snapshot array matching the W3C `navigator.getGamepads()` contract.
264
+ * Each non-null entry is a frozen Gamepad object with current state.
265
+ */
266
+ getGamepads(): (Gamepad | null)[] {
267
+ this._ensureInit();
268
+
269
+ const result: (Gamepad | null)[] = [];
270
+ for (let i = 0; i < MAX_GAMEPADS; i++) {
271
+ const state = this._slots[i];
272
+ result.push(state ? this._createSnapshot(state) : null);
273
+ }
274
+ return result;
275
+ }
276
+
277
+ /** Cleanup — disconnect all signal handlers. */
278
+ dispose(): void {
279
+ for (const state of this._slots) {
280
+ if (state) {
281
+ for (const id of state.signalIds) {
282
+ state.device.disconnect(id);
283
+ }
284
+ }
285
+ }
286
+ this._slots.fill(null);
287
+
288
+ if (this._monitor) {
289
+ for (const id of this._monitorSignalIds) {
290
+ this._monitor.disconnect(id);
291
+ }
292
+ this._monitorSignalIds = [];
293
+ this._monitor = null;
294
+ }
295
+
296
+ this._initialized = false;
297
+ this._initPromise = null;
298
+ }
299
+ }