@dloizides/game-input 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -26
- package/README.md +76 -74
- package/dist/index.d.mts +72 -6
- package/dist/index.d.ts +72 -6
- package/dist/index.js +405 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +392 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@dloizides/game-input` are documented here.
|
|
4
|
-
|
|
5
|
-
## 1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
- `
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@dloizides/game-input` are documented here.
|
|
4
|
+
|
|
5
|
+
## 1.5.0
|
|
6
|
+
|
|
7
|
+
- `createMenuNav`: one menu focus engine for arrow keys (capture phase) and pad actions; top-most dialog owns focus, focus restored on close, sliders/selects step on Left/Right, text fields keep arrows until Escape/B, `data-nav-input` on <html> (NAV-1 "Every game menu works with arrow keys and controller").
|
|
8
|
+
- `MENU_FOCUSABLE_SELECTOR`, `menuRoot`, `focusables`, `pickTarget` exported (pickTarget moved from Surge).
|
|
9
|
+
|
|
10
|
+
## 1.4.0
|
|
11
|
+
|
|
12
|
+
GI-HAPTICS-1 "Haptics vibrate only after a user activation".
|
|
13
|
+
|
|
14
|
+
- `createHaptics` no longer calls `navigator.vibrate` before a user gesture (Chrome blocks it and logs an
|
|
15
|
+
intervention). Gate: `navigator.userActivation.hasBeenActive` where present, else a capture-phase latch on
|
|
16
|
+
the first `pointerdown` / `keydown` / `touchstart` of `activationTarget` (new option, default `globalThis`).
|
|
17
|
+
- The enable-confirmation tick follows the same gate. Disabled and unsupported stay silent no-ops.
|
|
18
|
+
- New exports: `ActivationTarget` type, `USER_ACTIVATION_EVENTS`.
|
|
19
|
+
|
|
20
|
+
## 1.3.0
|
|
21
|
+
|
|
22
|
+
Standard Gamepad action poller (first consumer: NextGame console-dashboard UI).
|
|
23
|
+
|
|
24
|
+
- `stepPad(prev, next, repeat, now)` — pure per-frame reducer: face/shoulder buttons fire on the
|
|
25
|
+
rising edge by W3C Standard POSITION; D-pad or left stick (0.5 dead-zone, dominant axis) fires
|
|
26
|
+
once, repeats after 380 ms, then every 110 ms; release resets the repeat.
|
|
27
|
+
- `createGamepadPoller({ getPads, requestFrame, cancelFrame, onAction })` — injectable rAF loop over
|
|
28
|
+
the first connected pad; a disappearing pad resets state; `stop()` is safe from inside `onAction`.
|
|
29
|
+
- `PAD_ACTIONS`, `STANDARD_BUTTON`, `IDLE_REPEAT` + types `PadAction`, `PadSnapshot`, `RepeatState`,
|
|
30
|
+
`PadLike`, `GamepadPollerOptions`.
|
|
31
|
+
|
|
32
|
+
## 1.0.0
|
|
33
|
+
|
|
34
|
+
Initial release — the haptics primitive of the C4 games shared kit.
|
|
35
|
+
|
|
36
|
+
- `createHaptics(options)` — player-toggleable, always-safe Web Vibration API wrapper
|
|
37
|
+
(localStorage-gated, structural/injectable `navigator` + `storage`, never throws into the game loop).
|
|
38
|
+
- `HAPTIC_PATTERNS` — shared semantic vibration vocabulary (tap/select/lock/bump/navigate/success/error/loss/reveal).
|
|
39
|
+
- Zero runtime dependencies; isomorphic; dual CJS/ESM; 100% test coverage.
|
|
40
|
+
|
|
41
|
+
Extracted from Aurora's `src/input/haptics.ts`. First consumers: Aurora (re-export shim) and Morphe (new adoption).
|
package/README.md
CHANGED
|
@@ -1,74 +1,76 @@
|
|
|
1
|
-
# @dloizides/game-input
|
|
2
|
-
|
|
3
|
-
Framework-agnostic game input primitives. **v1 ships safe, player-toggleable haptics** — a thin wrapper
|
|
4
|
-
over the Web Vibration API that no-ops silently when disabled, unsupported, or blocked, so call sites can
|
|
5
|
-
fire haptics unconditionally alongside their SFX/visual feedback.
|
|
6
|
-
|
|
7
|
-
Zero runtime dependencies. Isomorphic (no DOM lib required — `navigator` and storage are structural and
|
|
8
|
-
injectable), so it works in browser games, React Native shells, and tests alike.
|
|
9
|
-
|
|
10
|
-
> **Scope note:** keyboard/gamepad normalization is intentionally *not* in v1. The games that were audited
|
|
11
|
-
> (Aurora, Morphe) implemented input with fundamentally different designs — there is no shared logic to
|
|
12
|
-
> extract yet. It will be added when a genuine 2nd same-stack consumer appears (extract-on-2nd-use).
|
|
13
|
-
|
|
14
|
-
## Install
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @dloizides/game-input
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Usage
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { createHaptics, HAPTIC_PATTERNS } from '@dloizides/game-input';
|
|
24
|
-
|
|
25
|
-
// One controller per game; the storage key namespaces the player's preference.
|
|
26
|
-
const haptics = createHaptics({ storageKey: 'aurora.haptics' });
|
|
27
|
-
|
|
28
|
-
// Fire unconditionally — disabled/unsupported just no-ops.
|
|
29
|
-
haptics.vibrate(HAPTIC_PATTERNS.tap); // menu navigation / selection
|
|
30
|
-
haptics.vibrate(HAPTIC_PATTERNS.success); // level cleared
|
|
31
|
-
haptics.vibrate([0, 14, 24, 14]); // or your own pattern
|
|
32
|
-
|
|
33
|
-
// Settings toggle — returns the new state, fires a confirming tick when enabling.
|
|
34
|
-
function onHapticsToggle(): boolean {
|
|
35
|
-
return haptics.toggle();
|
|
36
|
-
}
|
|
37
|
-
haptics.isEnabled();
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Semantic pattern vocabulary
|
|
41
|
-
|
|
42
|
-
`HAPTIC_PATTERNS` is a shared set of tuned vibration patterns keyed by intent, so every game uses the same
|
|
43
|
-
feel instead of re-picking magic durations:
|
|
44
|
-
|
|
45
|
-
| Intent | Pattern | Use |
|
|
46
|
-
|---|---|---|
|
|
47
|
-
| `tap` | `10` | menu nav / select / undo / restart |
|
|
48
|
-
| `select` | `12` | pick up a tool/charm/item |
|
|
49
|
-
| `lock` | `18` | something locks into place |
|
|
50
|
-
| `bump` | `24` | action blocked |
|
|
51
|
-
| `navigate` | `16` | portal / page turn |
|
|
52
|
-
| `success` | `[0,40,50,90]` | level/chamber cleared |
|
|
53
|
-
| `error` | `[0,14,24,14]` | fatal/destructive event |
|
|
54
|
-
| `loss` | `[0,40,50,40]` | player loses / falls |
|
|
55
|
-
| `reveal` | `[0,14,20,18,26,24]` | puzzle resolves / exit opens |
|
|
56
|
-
|
|
57
|
-
## API
|
|
58
|
-
|
|
59
|
-
- `createHaptics(options?)` → `Haptics` — `{ isEnabled, setEnabled, toggle, vibrate }`.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- `options.
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
# @dloizides/game-input
|
|
2
|
+
|
|
3
|
+
Framework-agnostic game input primitives. **v1 ships safe, player-toggleable haptics** — a thin wrapper
|
|
4
|
+
over the Web Vibration API that no-ops silently when disabled, unsupported, or blocked, so call sites can
|
|
5
|
+
fire haptics unconditionally alongside their SFX/visual feedback.
|
|
6
|
+
|
|
7
|
+
Zero runtime dependencies. Isomorphic (no DOM lib required — `navigator` and storage are structural and
|
|
8
|
+
injectable), so it works in browser games, React Native shells, and tests alike.
|
|
9
|
+
|
|
10
|
+
> **Scope note:** keyboard/gamepad normalization is intentionally *not* in v1. The games that were audited
|
|
11
|
+
> (Aurora, Morphe) implemented input with fundamentally different designs — there is no shared logic to
|
|
12
|
+
> extract yet. It will be added when a genuine 2nd same-stack consumer appears (extract-on-2nd-use).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @dloizides/game-input
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createHaptics, HAPTIC_PATTERNS } from '@dloizides/game-input';
|
|
24
|
+
|
|
25
|
+
// One controller per game; the storage key namespaces the player's preference.
|
|
26
|
+
const haptics = createHaptics({ storageKey: 'aurora.haptics' });
|
|
27
|
+
|
|
28
|
+
// Fire unconditionally — disabled/unsupported just no-ops.
|
|
29
|
+
haptics.vibrate(HAPTIC_PATTERNS.tap); // menu navigation / selection
|
|
30
|
+
haptics.vibrate(HAPTIC_PATTERNS.success); // level cleared
|
|
31
|
+
haptics.vibrate([0, 14, 24, 14]); // or your own pattern
|
|
32
|
+
|
|
33
|
+
// Settings toggle — returns the new state, fires a confirming tick when enabling.
|
|
34
|
+
function onHapticsToggle(): boolean {
|
|
35
|
+
return haptics.toggle();
|
|
36
|
+
}
|
|
37
|
+
haptics.isEnabled();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Semantic pattern vocabulary
|
|
41
|
+
|
|
42
|
+
`HAPTIC_PATTERNS` is a shared set of tuned vibration patterns keyed by intent, so every game uses the same
|
|
43
|
+
feel instead of re-picking magic durations:
|
|
44
|
+
|
|
45
|
+
| Intent | Pattern | Use |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `tap` | `10` | menu nav / select / undo / restart |
|
|
48
|
+
| `select` | `12` | pick up a tool/charm/item |
|
|
49
|
+
| `lock` | `18` | something locks into place |
|
|
50
|
+
| `bump` | `24` | action blocked |
|
|
51
|
+
| `navigate` | `16` | portal / page turn |
|
|
52
|
+
| `success` | `[0,40,50,90]` | level/chamber cleared |
|
|
53
|
+
| `error` | `[0,14,24,14]` | fatal/destructive event |
|
|
54
|
+
| `loss` | `[0,40,50,40]` | player loses / falls |
|
|
55
|
+
| `reveal` | `[0,14,20,18,26,24]` | puzzle resolves / exit opens |
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
- `createHaptics(options?)` → `Haptics` — `{ isEnabled, setEnabled, toggle, vibrate }`. Nothing vibrates
|
|
60
|
+
before a user gesture: `navigator.userActivation.hasBeenActive` where present, else the first
|
|
61
|
+
`pointerdown` / `keydown` / `touchstart` on `options.activationTarget` (default `globalThis`).
|
|
62
|
+
- `options.storageKey` (default `game.haptics`) — localStorage key for the on/off preference.
|
|
63
|
+
- `options.enableConfirmation` (default a `15`ms tick; pass `null` to disable) — fired when turning ON.
|
|
64
|
+
- `options.navigator` / `options.storage` — inject for tests/RN; default to `globalThis`.
|
|
65
|
+
- `HAPTIC_PATTERNS`, `DEFAULT_HAPTICS_STORAGE_KEY`, `DEFAULT_ENABLE_CONFIRMATION`.
|
|
66
|
+
- `createGamepadPoller({ getPads, requestFrame, cancelFrame, onAction })` → `{ start, stop }` — polls the
|
|
67
|
+
first connected W3C Standard Gamepad each frame and calls `onAction(PadAction)`. Pass
|
|
68
|
+
`() => navigator.getGamepads()`, `requestAnimationFrame`, `cancelAnimationFrame` in the browser.
|
|
69
|
+
- Buttons by POSITION: south → `confirm`, east → `back`, west → `view`, north → `mood`,
|
|
70
|
+
L/R shoulder → `pagePrev`/`pageNext` (rising edge only).
|
|
71
|
+
- D-pad or left stick (0.5 dead-zone) → `up`/`down`/`left`/`right`: fires once, repeats after 380 ms, then every 110 ms.
|
|
72
|
+
- `stepPad(prev, next, repeat, now)` — the pure per-frame reducer behind the poller; `IDLE_REPEAT`, `PAD_ACTIONS`, `STANDARD_BUTTON`.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -70,6 +70,63 @@ declare function stepPad(prev: PadSnapshot | null, next: PadSnapshot, repeat: Re
|
|
|
70
70
|
repeat: RepeatState;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
interface MenuNavOptions {
|
|
74
|
+
/** The game's base menu root when no dialog is open; null = no menu (e.g. during play). */
|
|
75
|
+
baseRoot: () => HTMLElement | null;
|
|
76
|
+
/** True while input must not move focus (e.g. a remap capture). Default: never. */
|
|
77
|
+
suspended?: () => boolean;
|
|
78
|
+
/** Where focus goes when a root opens. Default: [data-menu-default], else the first control. */
|
|
79
|
+
initialFocus?: (root: HTMLElement) => HTMLElement | null;
|
|
80
|
+
}
|
|
81
|
+
interface MenuNav {
|
|
82
|
+
/** Feed one pad action from stepPad (the game keeps its own poller). */
|
|
83
|
+
pad(action: PadAction): void;
|
|
84
|
+
/** Re-evaluate the root and place focus (call after the game opens a screen). */
|
|
85
|
+
refresh(): void;
|
|
86
|
+
dispose(): void;
|
|
87
|
+
}
|
|
88
|
+
/** One focus engine for every game menu: arrow keys (capture phase), pad actions, dialogs on top, sliders and text fields. */
|
|
89
|
+
declare function createMenuNav(options: MenuNavOptions): MenuNav;
|
|
90
|
+
|
|
91
|
+
/** Every control a menu walker must be able to reach (shared with @dloizides/game-e2e). */
|
|
92
|
+
declare const MENU_FOCUSABLE_SELECTOR: string;
|
|
93
|
+
declare const DIALOG_SELECTOR = "[role=\"dialog\"]";
|
|
94
|
+
declare const MENU_DEFAULT_ATTR = "data-menu-default";
|
|
95
|
+
declare const NAV_INPUT_ATTR = "data-nav-input";
|
|
96
|
+
declare const NAV_INPUT: {
|
|
97
|
+
readonly key: "key";
|
|
98
|
+
readonly pad: "pad";
|
|
99
|
+
};
|
|
100
|
+
type NavInput = (typeof NAV_INPUT)[keyof typeof NAV_INPUT];
|
|
101
|
+
|
|
102
|
+
/** True when the element has a layout box and no hidden, inert or aria-hidden ancestor. */
|
|
103
|
+
declare function isShown(el: Element): boolean;
|
|
104
|
+
/** The visible, enabled controls inside root, in DOM order. */
|
|
105
|
+
declare function focusables(root: HTMLElement): HTMLElement[];
|
|
106
|
+
/** The [data-menu-default] control, else the first control, else null. */
|
|
107
|
+
declare function defaultInitialFocus(root: HTMLElement): HTMLElement | null;
|
|
108
|
+
|
|
109
|
+
/** Top-most visible dialog (aria-modal preferred, last in DOM wins), else the visible base root, else null. */
|
|
110
|
+
declare function menuRoot(baseRoot: () => HTMLElement | null): HTMLElement | null;
|
|
111
|
+
|
|
112
|
+
interface Box {
|
|
113
|
+
readonly x: number;
|
|
114
|
+
readonly y: number;
|
|
115
|
+
readonly w: number;
|
|
116
|
+
readonly h: number;
|
|
117
|
+
}
|
|
118
|
+
declare const NAV_DIRS: {
|
|
119
|
+
readonly up: "arrowup";
|
|
120
|
+
readonly down: "arrowdown";
|
|
121
|
+
readonly left: "arrowleft";
|
|
122
|
+
readonly right: "arrowright";
|
|
123
|
+
};
|
|
124
|
+
type NavDir = (typeof NAV_DIRS)[keyof typeof NAV_DIRS];
|
|
125
|
+
/** The direction an arrow key names (case-insensitive), or null for any other key. */
|
|
126
|
+
declare const navDir: (key: string) => NavDir | null;
|
|
127
|
+
/** Index of the nearest box from `from` in `dir`; wraps in list order when none lies that way; -1 for no boxes. */
|
|
128
|
+
declare function pickTarget(boxes: readonly Box[], from: number, dir: NavDir): number;
|
|
129
|
+
|
|
73
130
|
/**
|
|
74
131
|
* A vibration pattern: a single duration in ms, or an on/off pattern
|
|
75
132
|
* (`[wait, on, off, on, ...]`) exactly as the Web Vibration API expects.
|
|
@@ -81,6 +138,14 @@ type VibrationPattern = number | number[];
|
|
|
81
138
|
*/
|
|
82
139
|
interface VibrationCapable {
|
|
83
140
|
vibrate?: (pattern: VibrationPattern) => boolean;
|
|
141
|
+
userActivation?: {
|
|
142
|
+
readonly hasBeenActive: boolean;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Event source whose first pointer/key/touch gesture unlocks haptics. Defaults to `globalThis`. */
|
|
146
|
+
interface ActivationTarget {
|
|
147
|
+
addEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
148
|
+
removeEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
84
149
|
}
|
|
85
150
|
/**
|
|
86
151
|
* The slice of `localStorage` haptics needs to persist the player's toggle.
|
|
@@ -103,6 +168,8 @@ interface HapticsOptions {
|
|
|
103
168
|
navigator?: VibrationCapable;
|
|
104
169
|
/** Preference store. Defaults to `globalThis.localStorage`. Inject for tests. */
|
|
105
170
|
storage?: HapticsStorage;
|
|
171
|
+
/** Gesture source used when `navigator.userActivation` is unavailable. Defaults to `globalThis`. */
|
|
172
|
+
activationTarget?: ActivationTarget;
|
|
106
173
|
}
|
|
107
174
|
/**
|
|
108
175
|
* A player-toggleable, always-safe haptics controller. Every call no-ops silently
|
|
@@ -121,11 +188,8 @@ interface Haptics {
|
|
|
121
188
|
}
|
|
122
189
|
|
|
123
190
|
/**
|
|
124
|
-
* Create a player-toggleable, always-safe haptics controller
|
|
125
|
-
*
|
|
126
|
-
* The on/off preference is read from (and persisted to) the configured storage key
|
|
127
|
-
* so it survives reloads. Every vibration routes through one gated call that no-ops
|
|
128
|
-
* when disabled, when no Vibration API is present, or if the platform blocks it.
|
|
191
|
+
* Create a player-toggleable, always-safe haptics controller: it never vibrates before a
|
|
192
|
+
* user gesture, while disabled, or when the Vibration API is missing or blocked.
|
|
129
193
|
*/
|
|
130
194
|
declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
131
195
|
|
|
@@ -133,6 +197,8 @@ declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
|
133
197
|
declare const DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
134
198
|
/** Default confirmation tick fired when haptics are turned ON (ms). */
|
|
135
199
|
declare const DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
200
|
+
/** DOM events that count as a user activation before `navigator.userActivation` is available. */
|
|
201
|
+
declare const USER_ACTIVATION_EVENTS: readonly ["pointerdown", "keydown", "touchstart"];
|
|
136
202
|
/** The semantic intents games map their events onto. */
|
|
137
203
|
type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success' | 'error' | 'loss' | 'reveal';
|
|
138
204
|
/**
|
|
@@ -142,4 +208,4 @@ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success'
|
|
|
142
208
|
*/
|
|
143
209
|
declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
|
|
144
210
|
|
|
145
|
-
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, stepPad };
|
|
211
|
+
export { type ActivationTarget, DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, DIALOG_SELECTOR, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, MENU_DEFAULT_ATTR, MENU_FOCUSABLE_SELECTOR, type MenuNav, type MenuNavOptions, NAV_DIRS, NAV_INPUT, NAV_INPUT_ATTR, type Box as NavBox, type NavDir, type NavInput, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, USER_ACTIVATION_EVENTS, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, createMenuNav, defaultInitialFocus, focusables, isShown, menuRoot, navDir, pickTarget, stepPad };
|
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,63 @@ declare function stepPad(prev: PadSnapshot | null, next: PadSnapshot, repeat: Re
|
|
|
70
70
|
repeat: RepeatState;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
interface MenuNavOptions {
|
|
74
|
+
/** The game's base menu root when no dialog is open; null = no menu (e.g. during play). */
|
|
75
|
+
baseRoot: () => HTMLElement | null;
|
|
76
|
+
/** True while input must not move focus (e.g. a remap capture). Default: never. */
|
|
77
|
+
suspended?: () => boolean;
|
|
78
|
+
/** Where focus goes when a root opens. Default: [data-menu-default], else the first control. */
|
|
79
|
+
initialFocus?: (root: HTMLElement) => HTMLElement | null;
|
|
80
|
+
}
|
|
81
|
+
interface MenuNav {
|
|
82
|
+
/** Feed one pad action from stepPad (the game keeps its own poller). */
|
|
83
|
+
pad(action: PadAction): void;
|
|
84
|
+
/** Re-evaluate the root and place focus (call after the game opens a screen). */
|
|
85
|
+
refresh(): void;
|
|
86
|
+
dispose(): void;
|
|
87
|
+
}
|
|
88
|
+
/** One focus engine for every game menu: arrow keys (capture phase), pad actions, dialogs on top, sliders and text fields. */
|
|
89
|
+
declare function createMenuNav(options: MenuNavOptions): MenuNav;
|
|
90
|
+
|
|
91
|
+
/** Every control a menu walker must be able to reach (shared with @dloizides/game-e2e). */
|
|
92
|
+
declare const MENU_FOCUSABLE_SELECTOR: string;
|
|
93
|
+
declare const DIALOG_SELECTOR = "[role=\"dialog\"]";
|
|
94
|
+
declare const MENU_DEFAULT_ATTR = "data-menu-default";
|
|
95
|
+
declare const NAV_INPUT_ATTR = "data-nav-input";
|
|
96
|
+
declare const NAV_INPUT: {
|
|
97
|
+
readonly key: "key";
|
|
98
|
+
readonly pad: "pad";
|
|
99
|
+
};
|
|
100
|
+
type NavInput = (typeof NAV_INPUT)[keyof typeof NAV_INPUT];
|
|
101
|
+
|
|
102
|
+
/** True when the element has a layout box and no hidden, inert or aria-hidden ancestor. */
|
|
103
|
+
declare function isShown(el: Element): boolean;
|
|
104
|
+
/** The visible, enabled controls inside root, in DOM order. */
|
|
105
|
+
declare function focusables(root: HTMLElement): HTMLElement[];
|
|
106
|
+
/** The [data-menu-default] control, else the first control, else null. */
|
|
107
|
+
declare function defaultInitialFocus(root: HTMLElement): HTMLElement | null;
|
|
108
|
+
|
|
109
|
+
/** Top-most visible dialog (aria-modal preferred, last in DOM wins), else the visible base root, else null. */
|
|
110
|
+
declare function menuRoot(baseRoot: () => HTMLElement | null): HTMLElement | null;
|
|
111
|
+
|
|
112
|
+
interface Box {
|
|
113
|
+
readonly x: number;
|
|
114
|
+
readonly y: number;
|
|
115
|
+
readonly w: number;
|
|
116
|
+
readonly h: number;
|
|
117
|
+
}
|
|
118
|
+
declare const NAV_DIRS: {
|
|
119
|
+
readonly up: "arrowup";
|
|
120
|
+
readonly down: "arrowdown";
|
|
121
|
+
readonly left: "arrowleft";
|
|
122
|
+
readonly right: "arrowright";
|
|
123
|
+
};
|
|
124
|
+
type NavDir = (typeof NAV_DIRS)[keyof typeof NAV_DIRS];
|
|
125
|
+
/** The direction an arrow key names (case-insensitive), or null for any other key. */
|
|
126
|
+
declare const navDir: (key: string) => NavDir | null;
|
|
127
|
+
/** Index of the nearest box from `from` in `dir`; wraps in list order when none lies that way; -1 for no boxes. */
|
|
128
|
+
declare function pickTarget(boxes: readonly Box[], from: number, dir: NavDir): number;
|
|
129
|
+
|
|
73
130
|
/**
|
|
74
131
|
* A vibration pattern: a single duration in ms, or an on/off pattern
|
|
75
132
|
* (`[wait, on, off, on, ...]`) exactly as the Web Vibration API expects.
|
|
@@ -81,6 +138,14 @@ type VibrationPattern = number | number[];
|
|
|
81
138
|
*/
|
|
82
139
|
interface VibrationCapable {
|
|
83
140
|
vibrate?: (pattern: VibrationPattern) => boolean;
|
|
141
|
+
userActivation?: {
|
|
142
|
+
readonly hasBeenActive: boolean;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Event source whose first pointer/key/touch gesture unlocks haptics. Defaults to `globalThis`. */
|
|
146
|
+
interface ActivationTarget {
|
|
147
|
+
addEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
148
|
+
removeEventListener(type: string, listener: () => void, capture?: boolean): void;
|
|
84
149
|
}
|
|
85
150
|
/**
|
|
86
151
|
* The slice of `localStorage` haptics needs to persist the player's toggle.
|
|
@@ -103,6 +168,8 @@ interface HapticsOptions {
|
|
|
103
168
|
navigator?: VibrationCapable;
|
|
104
169
|
/** Preference store. Defaults to `globalThis.localStorage`. Inject for tests. */
|
|
105
170
|
storage?: HapticsStorage;
|
|
171
|
+
/** Gesture source used when `navigator.userActivation` is unavailable. Defaults to `globalThis`. */
|
|
172
|
+
activationTarget?: ActivationTarget;
|
|
106
173
|
}
|
|
107
174
|
/**
|
|
108
175
|
* A player-toggleable, always-safe haptics controller. Every call no-ops silently
|
|
@@ -121,11 +188,8 @@ interface Haptics {
|
|
|
121
188
|
}
|
|
122
189
|
|
|
123
190
|
/**
|
|
124
|
-
* Create a player-toggleable, always-safe haptics controller
|
|
125
|
-
*
|
|
126
|
-
* The on/off preference is read from (and persisted to) the configured storage key
|
|
127
|
-
* so it survives reloads. Every vibration routes through one gated call that no-ops
|
|
128
|
-
* when disabled, when no Vibration API is present, or if the platform blocks it.
|
|
191
|
+
* Create a player-toggleable, always-safe haptics controller: it never vibrates before a
|
|
192
|
+
* user gesture, while disabled, or when the Vibration API is missing or blocked.
|
|
129
193
|
*/
|
|
130
194
|
declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
131
195
|
|
|
@@ -133,6 +197,8 @@ declare function createHaptics(options?: HapticsOptions): Haptics;
|
|
|
133
197
|
declare const DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
|
|
134
198
|
/** Default confirmation tick fired when haptics are turned ON (ms). */
|
|
135
199
|
declare const DEFAULT_ENABLE_CONFIRMATION = 15;
|
|
200
|
+
/** DOM events that count as a user activation before `navigator.userActivation` is available. */
|
|
201
|
+
declare const USER_ACTIVATION_EVENTS: readonly ["pointerdown", "keydown", "touchstart"];
|
|
136
202
|
/** The semantic intents games map their events onto. */
|
|
137
203
|
type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success' | 'error' | 'loss' | 'reveal';
|
|
138
204
|
/**
|
|
@@ -142,4 +208,4 @@ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success'
|
|
|
142
208
|
*/
|
|
143
209
|
declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
|
|
144
210
|
|
|
145
|
-
export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, stepPad };
|
|
211
|
+
export { type ActivationTarget, DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, DIALOG_SELECTOR, type GamepadPollerOptions, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, IDLE_REPEAT, MENU_DEFAULT_ATTR, MENU_FOCUSABLE_SELECTOR, type MenuNav, type MenuNavOptions, NAV_DIRS, NAV_INPUT, NAV_INPUT_ATTR, type Box as NavBox, type NavDir, type NavInput, PAD_ACTIONS, type PadAction, type PadLike, type PadSnapshot, type RepeatState, STANDARD_BUTTON, USER_ACTIVATION_EVENTS, type VibrationCapable, type VibrationPattern, createGamepadPoller, createHaptics, createMenuNav, defaultInitialFocus, focusables, isShown, menuRoot, navDir, pickTarget, stepPad };
|