@dloizides/game-input 1.0.0 → 1.3.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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `@dloizides/game-input` are documented here.
4
4
 
5
+ ## 1.3.0
6
+
7
+ Standard Gamepad action poller (first consumer: NextGame console-dashboard UI).
8
+
9
+ - `stepPad(prev, next, repeat, now)` — pure per-frame reducer: face/shoulder buttons fire on the
10
+ rising edge by W3C Standard POSITION; D-pad or left stick (0.5 dead-zone, dominant axis) fires
11
+ once, repeats after 380 ms, then every 110 ms; release resets the repeat.
12
+ - `createGamepadPoller({ getPads, requestFrame, cancelFrame, onAction })` — injectable rAF loop over
13
+ the first connected pad; a disappearing pad resets state; `stop()` is safe from inside `onAction`.
14
+ - `PAD_ACTIONS`, `STANDARD_BUTTON`, `IDLE_REPEAT` + types `PadAction`, `PadSnapshot`, `RepeatState`,
15
+ `PadLike`, `GamepadPollerOptions`.
16
+
5
17
  ## 1.0.0
6
18
 
7
19
  Initial release — the haptics primitive of the C4 games shared kit.
package/README.md CHANGED
@@ -61,6 +61,13 @@ feel instead of re-picking magic durations:
61
61
  - `options.enableConfirmation` (default a `15`ms tick; pass `null` to disable) — fired when turning ON.
62
62
  - `options.navigator` / `options.storage` — inject for tests/RN; default to `globalThis`.
63
63
  - `HAPTIC_PATTERNS`, `DEFAULT_HAPTICS_STORAGE_KEY`, `DEFAULT_ENABLE_CONFIRMATION`.
64
+ - `createGamepadPoller({ getPads, requestFrame, cancelFrame, onAction })` → `{ start, stop }` — polls the
65
+ first connected W3C Standard Gamepad each frame and calls `onAction(PadAction)`. Pass
66
+ `() => navigator.getGamepads()`, `requestAnimationFrame`, `cancelAnimationFrame` in the browser.
67
+ - Buttons by POSITION: south → `confirm`, east → `back`, west → `view`, north → `mood`,
68
+ L/R shoulder → `pagePrev`/`pageNext` (rising edge only).
69
+ - D-pad or left stick (0.5 dead-zone) → `up`/`down`/`left`/`right`: fires once, repeats after 380 ms, then every 110 ms.
70
+ - `stepPad(prev, next, repeat, now)` — the pure per-frame reducer behind the poller; `IDLE_REPEAT`, `PAD_ACTIONS`, `STANDARD_BUTTON`.
64
71
 
65
72
  ## License
66
73
 
package/dist/index.d.mts CHANGED
@@ -1,3 +1,75 @@
1
+ /** The semantic actions a Standard Gamepad produces, independent of the device's labels. */
2
+ declare const PAD_ACTIONS: {
3
+ readonly up: "up";
4
+ readonly down: "down";
5
+ readonly left: "left";
6
+ readonly right: "right";
7
+ readonly confirm: "confirm";
8
+ readonly back: "back";
9
+ readonly mood: "mood";
10
+ readonly view: "view";
11
+ readonly pagePrev: "pagePrev";
12
+ readonly pageNext: "pageNext";
13
+ };
14
+ type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];
15
+ /** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */
16
+ declare const STANDARD_BUTTON: {
17
+ readonly south: 0;
18
+ readonly east: 1;
19
+ readonly west: 2;
20
+ readonly north: 3;
21
+ readonly leftShoulder: 4;
22
+ readonly rightShoulder: 5;
23
+ readonly dpadUp: 12;
24
+ readonly dpadDown: 13;
25
+ readonly dpadLeft: 14;
26
+ readonly dpadRight: 15;
27
+ };
28
+
29
+ /** The structural subset of the DOM `Gamepad` the poller reads. */
30
+ interface PadLike {
31
+ connected: boolean;
32
+ buttons: ReadonlyArray<{
33
+ pressed: boolean;
34
+ }>;
35
+ axes: readonly number[];
36
+ }
37
+ interface GamepadPollerOptions {
38
+ /** Usually `() => navigator.getGamepads()`. */
39
+ getPads: () => ReadonlyArray<PadLike | null>;
40
+ /** Usually `requestAnimationFrame`. */
41
+ requestFrame: (cb: (now: number) => void) => number;
42
+ /** Usually `cancelAnimationFrame`. */
43
+ cancelFrame: (id: number) => void;
44
+ onAction: (action: PadAction) => void;
45
+ }
46
+ /**
47
+ * rAF-driven poller over the first connected pad. Starts idle; call start().
48
+ * Safe to stop() (or stop() then start()) from inside `onAction`.
49
+ */
50
+ declare function createGamepadPoller(opts: GamepadPollerOptions): {
51
+ start: () => void;
52
+ stop: () => void;
53
+ };
54
+
55
+ /** One frame of pad state: pressed flags by Standard button index, and raw axes. */
56
+ interface PadSnapshot {
57
+ buttons: readonly boolean[];
58
+ axes: readonly number[];
59
+ }
60
+ /** Which direction is held, when it started, and when it last fired. */
61
+ interface RepeatState {
62
+ held: PadAction | null;
63
+ since: number;
64
+ lastFire: number;
65
+ }
66
+ declare const IDLE_REPEAT: RepeatState;
67
+ /** One poll frame → the actions to dispatch and the next repeat state. Pure. */
68
+ declare function stepPad(prev: PadSnapshot | null, next: PadSnapshot, repeat: RepeatState, now: number): {
69
+ actions: PadAction[];
70
+ repeat: RepeatState;
71
+ };
72
+
1
73
  /**
2
74
  * A vibration pattern: a single duration in ms, or an on/off pattern
3
75
  * (`[wait, on, off, on, ...]`) exactly as the Web Vibration API expects.
@@ -70,4 +142,4 @@ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success'
70
142
  */
71
143
  declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
72
144
 
73
- export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, type VibrationCapable, type VibrationPattern, createHaptics };
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 };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,75 @@
1
+ /** The semantic actions a Standard Gamepad produces, independent of the device's labels. */
2
+ declare const PAD_ACTIONS: {
3
+ readonly up: "up";
4
+ readonly down: "down";
5
+ readonly left: "left";
6
+ readonly right: "right";
7
+ readonly confirm: "confirm";
8
+ readonly back: "back";
9
+ readonly mood: "mood";
10
+ readonly view: "view";
11
+ readonly pagePrev: "pagePrev";
12
+ readonly pageNext: "pageNext";
13
+ };
14
+ type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];
15
+ /** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */
16
+ declare const STANDARD_BUTTON: {
17
+ readonly south: 0;
18
+ readonly east: 1;
19
+ readonly west: 2;
20
+ readonly north: 3;
21
+ readonly leftShoulder: 4;
22
+ readonly rightShoulder: 5;
23
+ readonly dpadUp: 12;
24
+ readonly dpadDown: 13;
25
+ readonly dpadLeft: 14;
26
+ readonly dpadRight: 15;
27
+ };
28
+
29
+ /** The structural subset of the DOM `Gamepad` the poller reads. */
30
+ interface PadLike {
31
+ connected: boolean;
32
+ buttons: ReadonlyArray<{
33
+ pressed: boolean;
34
+ }>;
35
+ axes: readonly number[];
36
+ }
37
+ interface GamepadPollerOptions {
38
+ /** Usually `() => navigator.getGamepads()`. */
39
+ getPads: () => ReadonlyArray<PadLike | null>;
40
+ /** Usually `requestAnimationFrame`. */
41
+ requestFrame: (cb: (now: number) => void) => number;
42
+ /** Usually `cancelAnimationFrame`. */
43
+ cancelFrame: (id: number) => void;
44
+ onAction: (action: PadAction) => void;
45
+ }
46
+ /**
47
+ * rAF-driven poller over the first connected pad. Starts idle; call start().
48
+ * Safe to stop() (or stop() then start()) from inside `onAction`.
49
+ */
50
+ declare function createGamepadPoller(opts: GamepadPollerOptions): {
51
+ start: () => void;
52
+ stop: () => void;
53
+ };
54
+
55
+ /** One frame of pad state: pressed flags by Standard button index, and raw axes. */
56
+ interface PadSnapshot {
57
+ buttons: readonly boolean[];
58
+ axes: readonly number[];
59
+ }
60
+ /** Which direction is held, when it started, and when it last fired. */
61
+ interface RepeatState {
62
+ held: PadAction | null;
63
+ since: number;
64
+ lastFire: number;
65
+ }
66
+ declare const IDLE_REPEAT: RepeatState;
67
+ /** One poll frame → the actions to dispatch and the next repeat state. Pure. */
68
+ declare function stepPad(prev: PadSnapshot | null, next: PadSnapshot, repeat: RepeatState, now: number): {
69
+ actions: PadAction[];
70
+ repeat: RepeatState;
71
+ };
72
+
1
73
  /**
2
74
  * A vibration pattern: a single duration in ms, or an on/off pattern
3
75
  * (`[wait, on, off, on, ...]`) exactly as the Web Vibration API expects.
@@ -70,4 +142,4 @@ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success'
70
142
  */
71
143
  declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
72
144
 
73
- export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, type VibrationCapable, type VibrationPattern, createHaptics };
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,127 @@
1
1
  'use strict';
2
2
 
3
+ // src/gamepad/padMap.ts
4
+ var PAD_ACTIONS = {
5
+ up: "up",
6
+ down: "down",
7
+ left: "left",
8
+ right: "right",
9
+ confirm: "confirm",
10
+ back: "back",
11
+ mood: "mood",
12
+ view: "view",
13
+ pagePrev: "pagePrev",
14
+ pageNext: "pageNext"
15
+ };
16
+ var STANDARD_BUTTON = {
17
+ south: 0,
18
+ east: 1,
19
+ west: 2,
20
+ north: 3,
21
+ leftShoulder: 4,
22
+ rightShoulder: 5,
23
+ dpadUp: 12,
24
+ dpadDown: 13,
25
+ dpadLeft: 14,
26
+ dpadRight: 15
27
+ };
28
+ var FACE_BUTTON_ACTIONS = [
29
+ [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],
30
+ [STANDARD_BUTTON.east, PAD_ACTIONS.back],
31
+ [STANDARD_BUTTON.west, PAD_ACTIONS.view],
32
+ [STANDARD_BUTTON.north, PAD_ACTIONS.mood],
33
+ [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],
34
+ [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext]
35
+ ];
36
+ var DPAD_ACTIONS = [
37
+ [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],
38
+ [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],
39
+ [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],
40
+ [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right]
41
+ ];
42
+ var STICK_DEAD_ZONE = 0.5;
43
+ var REPEAT_DELAY_MS = 380;
44
+ var REPEAT_INTERVAL_MS = 110;
45
+
46
+ // src/gamepad/stepPad.ts
47
+ var IDLE_REPEAT = { held: null, since: 0, lastFire: 0 };
48
+ var STICK_X = 0;
49
+ var STICK_Y = 1;
50
+ function stickDirection(axes) {
51
+ const x = axes[STICK_X] ?? 0;
52
+ const y = axes[STICK_Y] ?? 0;
53
+ if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {
54
+ return null;
55
+ }
56
+ if (Math.abs(x) > Math.abs(y)) {
57
+ return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;
58
+ }
59
+ return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;
60
+ }
61
+ function heldDirection(snapshot) {
62
+ const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);
63
+ return dpad === void 0 ? stickDirection(snapshot.axes) : dpad[1];
64
+ }
65
+ function stepPad(prev, next, repeat, now) {
66
+ const actions = FACE_BUTTON_ACTIONS.filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true).map(([, action]) => action);
67
+ const direction = heldDirection(next);
68
+ if (direction === null) {
69
+ return { actions, repeat: IDLE_REPEAT };
70
+ }
71
+ if (direction !== repeat.held) {
72
+ return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };
73
+ }
74
+ const firstRepeat = repeat.lastFire === repeat.since;
75
+ const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;
76
+ if (now - repeat.lastFire < wait) {
77
+ return { actions, repeat };
78
+ }
79
+ return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };
80
+ }
81
+
82
+ // src/gamepad/createGamepadPoller.ts
83
+ function createGamepadPoller(opts) {
84
+ let prev = null;
85
+ let repeat = IDLE_REPEAT;
86
+ let running = false;
87
+ let frame = null;
88
+ const tick = (now) => {
89
+ frame = null;
90
+ const pad = opts.getPads().find((p) => p !== null && p.connected);
91
+ if (pad === void 0) {
92
+ prev = null;
93
+ repeat = IDLE_REPEAT;
94
+ } else {
95
+ const snapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };
96
+ const step = stepPad(prev, snapshot, repeat, now);
97
+ prev = snapshot;
98
+ repeat = step.repeat;
99
+ step.actions.forEach((action) => opts.onAction(action));
100
+ }
101
+ if (running && frame === null) {
102
+ frame = opts.requestFrame(tick);
103
+ }
104
+ };
105
+ return {
106
+ start: () => {
107
+ if (running) {
108
+ return;
109
+ }
110
+ running = true;
111
+ if (frame === null) {
112
+ frame = opts.requestFrame(tick);
113
+ }
114
+ },
115
+ stop: () => {
116
+ running = false;
117
+ if (frame !== null) {
118
+ opts.cancelFrame(frame);
119
+ frame = null;
120
+ }
121
+ }
122
+ };
123
+ }
124
+
3
125
  // src/constants.ts
4
126
  var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
5
127
  var DEFAULT_ENABLE_CONFIRMATION = 15;
@@ -86,6 +208,11 @@ function createHaptics(options = {}) {
86
208
  exports.DEFAULT_ENABLE_CONFIRMATION = DEFAULT_ENABLE_CONFIRMATION;
87
209
  exports.DEFAULT_HAPTICS_STORAGE_KEY = DEFAULT_HAPTICS_STORAGE_KEY;
88
210
  exports.HAPTIC_PATTERNS = HAPTIC_PATTERNS;
211
+ exports.IDLE_REPEAT = IDLE_REPEAT;
212
+ exports.PAD_ACTIONS = PAD_ACTIONS;
213
+ exports.STANDARD_BUTTON = STANDARD_BUTTON;
214
+ exports.createGamepadPoller = createGamepadPoller;
89
215
  exports.createHaptics = createHaptics;
216
+ exports.stepPad = stepPad;
90
217
  //# sourceMappingURL=index.js.map
91
218
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":";;;AAGO,IAAM,2BAAA,GAA8B;AAGpC,IAAM,2BAAA,GAA8B;AAGpC,IAAM,cAAA,GAAiB,GAAA;AAGvB,IAAM,aAAA,GAAgB,GAAA;AAmBtB,IAAM,eAAA,GAA0D;AAAA;AAAA,EAErE,GAAA,EAAK,EAAA;AAAA;AAAA,EAEL,MAAA,EAAQ,EAAA;AAAA;AAAA,EAER,IAAA,EAAM,EAAA;AAAA;AAAA,EAEN,IAAA,EAAM,EAAA;AAAA;AAAA,EAEN,QAAA,EAAU,EAAA;AAAA;AAAA,EAEV,OAAA,EAAS,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAEvB,KAAA,EAAO,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAErB,IAAA,EAAM,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAEpB,QAAQ,CAAC,CAAA,EAAG,IAAI,EAAA,EAAI,EAAA,EAAI,IAAI,EAAE;AAChC;;;AC1CA,SAAS,iBAAiB,QAAA,EAA2D;AACnF,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAQ,UAAA,CAAgD,SAAA;AAC1D;AAEA,SAAS,eAAe,QAAA,EAAuD;AAC7E,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAQ,UAAA,CAAiD,YAAA;AAC3D;AASO,SAAS,aAAA,CAAc,OAAA,GAA0B,EAAC,EAAY;AACnE,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,2BAAA;AACzC,EAAA,MAAM,YAAA,GACJ,OAAA,CAAQ,kBAAA,KAAuB,MAAA,GAAY,8BAA8B,OAAA,CAAQ,kBAAA;AACnF,EAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,OAAA,CAAQ,SAAS,CAAA;AACpD,EAAA,MAAM,OAAA,GAAU,cAAA,CAAe,OAAA,CAAQ,OAAO,CAAA;AAE9C,EAAA,MAAM,WAAW,MAAe;AAC9B,IAAA,IAAI;AACF,MAAA,OAAO,OAAA,EAAS,OAAA,CAAQ,UAAU,CAAA,KAAM,cAAA;AAAA,IAC1C,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,IAAI,UAAU,QAAA,EAAS;AAEvB,EAAA,MAAM,OAAA,GAAU,CAAC,OAAA,KAAoC;AACnD,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,SAAA,EAAW,UAAU,OAAO,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAU,MAAY;AAC1B,IAAA,IAAI;AACF,MAAA,OAAA,EAAS,OAAA,CAAQ,UAAA,EAAY,OAAA,GAAU,aAAA,GAAgB,cAAc,CAAA;AAAA,IACvE,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAA,GAAa,CAAC,IAAA,KAA2B;AAC7C,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,OAAA,EAAQ;AAER,IAAA,IAAI,OAAA,IAAW,iBAAiB,IAAA,EAAM;AACpC,MAAA,OAAA,CAAQ,YAAY,CAAA;AAAA,IACtB;AACA,IAAA,OAAO,OAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,WAAW,MAAe,OAAA;AAAA,IAC1B,UAAA;AAAA,IACA,MAAA,EAAQ,MAAe,UAAA,CAAW,CAAC,OAAO,CAAA;AAAA,IAC1C;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":";;;AACO,IAAM,WAAA,GAAc;AAAA,EACzB,EAAA,EAAI,IAAA;AAAA,EACJ,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,OAAA,EAAS,SAAA;AAAA,EACT,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,UAAA;AAAA,EACV,QAAA,EAAU;AACZ;AAIO,IAAM,eAAA,GAAkB;AAAA,EAC7B,KAAA,EAAO,CAAA;AAAA,EACP,IAAA,EAAM,CAAA;AAAA,EACN,IAAA,EAAM,CAAA;AAAA,EACN,KAAA,EAAO,CAAA;AAAA,EACP,YAAA,EAAc,CAAA;AAAA,EACd,aAAA,EAAe,CAAA;AAAA,EACf,MAAA,EAAQ,EAAA;AAAA,EACR,QAAA,EAAU,EAAA;AAAA,EACV,QAAA,EAAU,EAAA;AAAA,EACV,SAAA,EAAW;AACb;AAGO,IAAM,mBAAA,GAAmE;AAAA,EAC9E,CAAC,eAAA,CAAgB,KAAA,EAAO,WAAA,CAAY,OAAO,CAAA;AAAA,EAC3C,CAAC,eAAA,CAAgB,IAAA,EAAM,WAAA,CAAY,IAAI,CAAA;AAAA,EACvC,CAAC,eAAA,CAAgB,IAAA,EAAM,WAAA,CAAY,IAAI,CAAA;AAAA,EACvC,CAAC,eAAA,CAAgB,KAAA,EAAO,WAAA,CAAY,IAAI,CAAA;AAAA,EACxC,CAAC,eAAA,CAAgB,YAAA,EAAc,WAAA,CAAY,QAAQ,CAAA;AAAA,EACnD,CAAC,eAAA,CAAgB,aAAA,EAAe,WAAA,CAAY,QAAQ;AACtD,CAAA;AAGO,IAAM,YAAA,GAA4D;AAAA,EACvE,CAAC,eAAA,CAAgB,MAAA,EAAQ,WAAA,CAAY,EAAE,CAAA;AAAA,EACvC,CAAC,eAAA,CAAgB,QAAA,EAAU,WAAA,CAAY,IAAI,CAAA;AAAA,EAC3C,CAAC,eAAA,CAAgB,QAAA,EAAU,WAAA,CAAY,IAAI,CAAA;AAAA,EAC3C,CAAC,eAAA,CAAgB,SAAA,EAAW,WAAA,CAAY,KAAK;AAC/C,CAAA;AAGO,IAAM,eAAA,GAAkB,GAAA;AAExB,IAAM,eAAA,GAAkB,GAAA;AAExB,IAAM,kBAAA,GAAqB,GAAA;;;AC7B3B,IAAM,cAA2B,EAAE,IAAA,EAAM,MAAM,KAAA,EAAO,CAAA,EAAG,UAAU,CAAA;AAE1E,IAAM,OAAA,GAAU,CAAA;AAChB,IAAM,OAAA,GAAU,CAAA;AAEhB,SAAS,eAAe,IAAA,EAA2C;AACjE,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAO,CAAA,IAAK,CAAA;AAC3B,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,OAAO,CAAA,IAAK,CAAA;AAC3B,EAAA,IAAI,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA,GAAI,eAAA,EAAiB;AACxD,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,KAAK,GAAA,CAAI,CAAC,IAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,EAAG;AAC7B,IAAA,OAAO,CAAA,GAAI,CAAA,GAAI,WAAA,CAAY,KAAA,GAAQ,WAAA,CAAY,IAAA;AAAA,EACjD;AACA,EAAA,OAAO,CAAA,GAAI,CAAA,GAAI,WAAA,CAAY,IAAA,GAAO,WAAA,CAAY,EAAA;AAChD;AAEA,SAAS,cAAc,QAAA,EAAyC;AAC9D,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,IAAA,CAAK,CAAC,CAAC,KAAK,CAAA,KAAM,QAAA,CAAS,OAAA,CAAQ,KAAK,CAAA,KAAM,IAAI,CAAA;AAC5E,EAAA,OAAO,SAAS,MAAA,GAAY,cAAA,CAAe,SAAS,IAAI,CAAA,GAAI,KAAK,CAAC,CAAA;AACpE;AAGO,SAAS,OAAA,CACd,IAAA,EACA,IAAA,EACA,MAAA,EACA,GAAA,EAC+C;AAC/C,EAAA,MAAM,OAAA,GAAuB,mBAAA,CAC1B,MAAA,CAAO,CAAC,CAAC,KAAK,CAAA,KAAM,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA,KAAM,IAAA,IAAQ,MAAM,OAAA,CAAQ,KAAK,CAAA,KAAM,IAAI,CAAA,CACjF,GAAA,CAAI,CAAC,GAAG,MAAM,CAAA,KAAM,MAAM,CAAA;AAC7B,EAAA,MAAM,SAAA,GAAY,cAAc,IAAI,CAAA;AACpC,EAAA,IAAI,cAAc,IAAA,EAAM;AACtB,IAAA,OAAO,EAAE,OAAA,EAAS,MAAA,EAAQ,WAAA,EAAY;AAAA,EACxC;AACA,EAAA,IAAI,SAAA,KAAc,OAAO,IAAA,EAAM;AAC7B,IAAA,OAAO,EAAE,OAAA,EAAS,CAAC,GAAG,SAAS,SAAS,CAAA,EAAG,MAAA,EAAQ,EAAE,MAAM,SAAA,EAAW,KAAA,EAAO,GAAA,EAAK,QAAA,EAAU,KAAI,EAAE;AAAA,EACpG;AACA,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,KAAa,MAAA,CAAO,KAAA;AAC/C,EAAA,MAAM,IAAA,GAAO,cAAc,eAAA,GAAkB,kBAAA;AAC7C,EAAA,IAAI,GAAA,GAAM,MAAA,CAAO,QAAA,GAAW,IAAA,EAAM;AAChC,IAAA,OAAO,EAAE,SAAS,MAAA,EAAO;AAAA,EAC3B;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,CAAC,GAAG,OAAA,EAAS,SAAS,CAAA,EAAG,MAAA,EAAQ,EAAE,GAAG,MAAA,EAAQ,QAAA,EAAU,KAAI,EAAE;AAClF;;;AC5CO,SAAS,oBAAoB,IAAA,EAAqE;AACvG,EAAA,IAAI,IAAA,GAA2B,IAAA;AAC/B,EAAA,IAAI,MAAA,GAAsB,WAAA;AAC1B,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,IAAI,KAAA,GAAuB,IAAA;AAE3B,EAAA,MAAM,IAAA,GAAO,CAAC,GAAA,KAAsB;AAClC,IAAA,KAAA,GAAQ,IAAA;AACR,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,EAAQ,CAAE,IAAA,CAAK,CAAC,CAAA,KAAoB,CAAA,KAAM,IAAA,IAAQ,CAAA,CAAE,SAAS,CAAA;AAC9E,IAAA,IAAI,QAAQ,MAAA,EAAW;AACrB,MAAA,IAAA,GAAO,IAAA;AACP,MAAA,MAAA,GAAS,WAAA;AAAA,IACX,CAAA,MAAO;AACL,MAAA,MAAM,QAAA,GAAwB,EAAE,OAAA,EAAS,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,EAAG,IAAA,EAAM,IAAI,IAAA,EAAK;AAC3F,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,EAAM,QAAA,EAAU,QAAQ,GAAG,CAAA;AAChD,MAAA,IAAA,GAAO,QAAA;AACP,MAAA,MAAA,GAAS,IAAA,CAAK,MAAA;AACd,MAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,CAAC,WAAW,IAAA,CAAK,QAAA,CAAS,MAAM,CAAC,CAAA;AAAA,IACxD;AACA,IAAA,IAAI,OAAA,IAAW,UAAU,IAAA,EAAM;AAC7B,MAAA,KAAA,GAAQ,IAAA,CAAK,aAAa,IAAI,CAAA;AAAA,IAChC;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,OAAO,MAAY;AACjB,MAAA,IAAI,OAAA,EAAS;AACX,QAAA;AAAA,MACF;AACA,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,IAAI,UAAU,IAAA,EAAM;AAClB,QAAA,KAAA,GAAQ,IAAA,CAAK,aAAa,IAAI,CAAA;AAAA,MAChC;AAAA,IACF,CAAA;AAAA,IACA,MAAM,MAAY;AAChB,MAAA,OAAA,GAAU,KAAA;AACV,MAAA,IAAI,UAAU,IAAA,EAAM;AAClB,QAAA,IAAA,CAAK,YAAY,KAAK,CAAA;AACtB,QAAA,KAAA,GAAQ,IAAA;AAAA,MACV;AAAA,IACF;AAAA,GACF;AACF;;;AC/DO,IAAM,2BAAA,GAA8B;AAGpC,IAAM,2BAAA,GAA8B;AAGpC,IAAM,cAAA,GAAiB,GAAA;AAGvB,IAAM,aAAA,GAAgB,GAAA;AAmBtB,IAAM,eAAA,GAA0D;AAAA;AAAA,EAErE,GAAA,EAAK,EAAA;AAAA;AAAA,EAEL,MAAA,EAAQ,EAAA;AAAA;AAAA,EAER,IAAA,EAAM,EAAA;AAAA;AAAA,EAEN,IAAA,EAAM,EAAA;AAAA;AAAA,EAEN,QAAA,EAAU,EAAA;AAAA;AAAA,EAEV,OAAA,EAAS,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAEvB,KAAA,EAAO,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAErB,IAAA,EAAM,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAEpB,QAAQ,CAAC,CAAA,EAAG,IAAI,EAAA,EAAI,EAAA,EAAI,IAAI,EAAE;AAChC;;;AC1CA,SAAS,iBAAiB,QAAA,EAA2D;AACnF,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAQ,UAAA,CAAgD,SAAA;AAC1D;AAEA,SAAS,eAAe,QAAA,EAAuD;AAC7E,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAQ,UAAA,CAAiD,YAAA;AAC3D;AASO,SAAS,aAAA,CAAc,OAAA,GAA0B,EAAC,EAAY;AACnE,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,2BAAA;AACzC,EAAA,MAAM,YAAA,GACJ,OAAA,CAAQ,kBAAA,KAAuB,MAAA,GAAY,8BAA8B,OAAA,CAAQ,kBAAA;AACnF,EAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,OAAA,CAAQ,SAAS,CAAA;AACpD,EAAA,MAAM,OAAA,GAAU,cAAA,CAAe,OAAA,CAAQ,OAAO,CAAA;AAE9C,EAAA,MAAM,WAAW,MAAe;AAC9B,IAAA,IAAI;AACF,MAAA,OAAO,OAAA,EAAS,OAAA,CAAQ,UAAU,CAAA,KAAM,cAAA;AAAA,IAC1C,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,IAAI,UAAU,QAAA,EAAS;AAEvB,EAAA,MAAM,OAAA,GAAU,CAAC,OAAA,KAAoC;AACnD,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,SAAA,EAAW,UAAU,OAAO,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAU,MAAY;AAC1B,IAAA,IAAI;AACF,MAAA,OAAA,EAAS,OAAA,CAAQ,UAAA,EAAY,OAAA,GAAU,aAAA,GAAgB,cAAc,CAAA;AAAA,IACvE,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAA,GAAa,CAAC,IAAA,KAA2B;AAC7C,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,OAAA,EAAQ;AAER,IAAA,IAAI,OAAA,IAAW,iBAAiB,IAAA,EAAM;AACpC,MAAA,OAAA,CAAQ,YAAY,CAAA;AAAA,IACtB;AACA,IAAA,OAAO,OAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,WAAW,MAAe,OAAA;AAAA,IAC1B,UAAA;AAAA,IACA,MAAA,EAAQ,MAAe,UAAA,CAAW,CAAC,OAAO,CAAA;AAAA,IAC1C;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
package/dist/index.mjs CHANGED
@@ -1,3 +1,125 @@
1
+ // src/gamepad/padMap.ts
2
+ var PAD_ACTIONS = {
3
+ up: "up",
4
+ down: "down",
5
+ left: "left",
6
+ right: "right",
7
+ confirm: "confirm",
8
+ back: "back",
9
+ mood: "mood",
10
+ view: "view",
11
+ pagePrev: "pagePrev",
12
+ pageNext: "pageNext"
13
+ };
14
+ var STANDARD_BUTTON = {
15
+ south: 0,
16
+ east: 1,
17
+ west: 2,
18
+ north: 3,
19
+ leftShoulder: 4,
20
+ rightShoulder: 5,
21
+ dpadUp: 12,
22
+ dpadDown: 13,
23
+ dpadLeft: 14,
24
+ dpadRight: 15
25
+ };
26
+ var FACE_BUTTON_ACTIONS = [
27
+ [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],
28
+ [STANDARD_BUTTON.east, PAD_ACTIONS.back],
29
+ [STANDARD_BUTTON.west, PAD_ACTIONS.view],
30
+ [STANDARD_BUTTON.north, PAD_ACTIONS.mood],
31
+ [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],
32
+ [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext]
33
+ ];
34
+ var DPAD_ACTIONS = [
35
+ [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],
36
+ [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],
37
+ [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],
38
+ [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right]
39
+ ];
40
+ var STICK_DEAD_ZONE = 0.5;
41
+ var REPEAT_DELAY_MS = 380;
42
+ var REPEAT_INTERVAL_MS = 110;
43
+
44
+ // src/gamepad/stepPad.ts
45
+ var IDLE_REPEAT = { held: null, since: 0, lastFire: 0 };
46
+ var STICK_X = 0;
47
+ var STICK_Y = 1;
48
+ function stickDirection(axes) {
49
+ const x = axes[STICK_X] ?? 0;
50
+ const y = axes[STICK_Y] ?? 0;
51
+ if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {
52
+ return null;
53
+ }
54
+ if (Math.abs(x) > Math.abs(y)) {
55
+ return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;
56
+ }
57
+ return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;
58
+ }
59
+ function heldDirection(snapshot) {
60
+ const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);
61
+ return dpad === void 0 ? stickDirection(snapshot.axes) : dpad[1];
62
+ }
63
+ function stepPad(prev, next, repeat, now) {
64
+ const actions = FACE_BUTTON_ACTIONS.filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true).map(([, action]) => action);
65
+ const direction = heldDirection(next);
66
+ if (direction === null) {
67
+ return { actions, repeat: IDLE_REPEAT };
68
+ }
69
+ if (direction !== repeat.held) {
70
+ return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };
71
+ }
72
+ const firstRepeat = repeat.lastFire === repeat.since;
73
+ const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;
74
+ if (now - repeat.lastFire < wait) {
75
+ return { actions, repeat };
76
+ }
77
+ return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };
78
+ }
79
+
80
+ // src/gamepad/createGamepadPoller.ts
81
+ function createGamepadPoller(opts) {
82
+ let prev = null;
83
+ let repeat = IDLE_REPEAT;
84
+ let running = false;
85
+ let frame = null;
86
+ const tick = (now) => {
87
+ frame = null;
88
+ const pad = opts.getPads().find((p) => p !== null && p.connected);
89
+ if (pad === void 0) {
90
+ prev = null;
91
+ repeat = IDLE_REPEAT;
92
+ } else {
93
+ const snapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };
94
+ const step = stepPad(prev, snapshot, repeat, now);
95
+ prev = snapshot;
96
+ repeat = step.repeat;
97
+ step.actions.forEach((action) => opts.onAction(action));
98
+ }
99
+ if (running && frame === null) {
100
+ frame = opts.requestFrame(tick);
101
+ }
102
+ };
103
+ return {
104
+ start: () => {
105
+ if (running) {
106
+ return;
107
+ }
108
+ running = true;
109
+ if (frame === null) {
110
+ frame = opts.requestFrame(tick);
111
+ }
112
+ },
113
+ stop: () => {
114
+ running = false;
115
+ if (frame !== null) {
116
+ opts.cancelFrame(frame);
117
+ frame = null;
118
+ }
119
+ }
120
+ };
121
+ }
122
+
1
123
  // src/constants.ts
2
124
  var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
3
125
  var DEFAULT_ENABLE_CONFIRMATION = 15;
@@ -81,6 +203,6 @@ function createHaptics(options = {}) {
81
203
  };
82
204
  }
83
205
 
84
- export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, createHaptics };
206
+ export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, IDLE_REPEAT, PAD_ACTIONS, STANDARD_BUTTON, createGamepadPoller, createHaptics, stepPad };
85
207
  //# sourceMappingURL=index.mjs.map
86
208
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":";AAGO,IAAM,2BAAA,GAA8B;AAGpC,IAAM,2BAAA,GAA8B;AAGpC,IAAM,cAAA,GAAiB,GAAA;AAGvB,IAAM,aAAA,GAAgB,GAAA;AAmBtB,IAAM,eAAA,GAA0D;AAAA;AAAA,EAErE,GAAA,EAAK,EAAA;AAAA;AAAA,EAEL,MAAA,EAAQ,EAAA;AAAA;AAAA,EAER,IAAA,EAAM,EAAA;AAAA;AAAA,EAEN,IAAA,EAAM,EAAA;AAAA;AAAA,EAEN,QAAA,EAAU,EAAA;AAAA;AAAA,EAEV,OAAA,EAAS,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAEvB,KAAA,EAAO,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAErB,IAAA,EAAM,CAAC,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA;AAAA,EAEpB,QAAQ,CAAC,CAAA,EAAG,IAAI,EAAA,EAAI,EAAA,EAAI,IAAI,EAAE;AAChC;;;AC1CA,SAAS,iBAAiB,QAAA,EAA2D;AACnF,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAQ,UAAA,CAAgD,SAAA;AAC1D;AAEA,SAAS,eAAe,QAAA,EAAuD;AAC7E,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAQ,UAAA,CAAiD,YAAA;AAC3D;AASO,SAAS,aAAA,CAAc,OAAA,GAA0B,EAAC,EAAY;AACnE,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,2BAAA;AACzC,EAAA,MAAM,YAAA,GACJ,OAAA,CAAQ,kBAAA,KAAuB,MAAA,GAAY,8BAA8B,OAAA,CAAQ,kBAAA;AACnF,EAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,OAAA,CAAQ,SAAS,CAAA;AACpD,EAAA,MAAM,OAAA,GAAU,cAAA,CAAe,OAAA,CAAQ,OAAO,CAAA;AAE9C,EAAA,MAAM,WAAW,MAAe;AAC9B,IAAA,IAAI;AACF,MAAA,OAAO,OAAA,EAAS,OAAA,CAAQ,UAAU,CAAA,KAAM,cAAA;AAAA,IAC1C,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,IAAI,UAAU,QAAA,EAAS;AAEvB,EAAA,MAAM,OAAA,GAAU,CAAC,OAAA,KAAoC;AACnD,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,SAAA,EAAW,UAAU,OAAO,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAU,MAAY;AAC1B,IAAA,IAAI;AACF,MAAA,OAAA,EAAS,OAAA,CAAQ,UAAA,EAAY,OAAA,GAAU,aAAA,GAAgB,cAAc,CAAA;AAAA,IACvE,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAA,GAAa,CAAC,IAAA,KAA2B;AAC7C,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,OAAA,EAAQ;AAER,IAAA,IAAI,OAAA,IAAW,iBAAiB,IAAA,EAAM;AACpC,MAAA,OAAA,CAAQ,YAAY,CAAA;AAAA,IACtB;AACA,IAAA,OAAO,OAAA;AAAA,EACT,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,WAAW,MAAe,OAAA;AAAA,IAC1B,UAAA;AAAA,IACA,MAAA,EAAQ,MAAe,UAAA,CAAW,CAAC,OAAO,CAAA;AAAA,IAC1C;AAAA,GACF;AACF","file":"index.mjs","sourcesContent":["import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/gamepad/padMap.ts","../src/gamepad/stepPad.ts","../src/gamepad/createGamepadPoller.ts","../src/constants.ts","../src/haptics.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/** The semantic actions a Standard Gamepad produces, independent of the device's labels. */\nexport const PAD_ACTIONS = {\n up: 'up',\n down: 'down',\n left: 'left',\n right: 'right',\n confirm: 'confirm',\n back: 'back',\n mood: 'mood',\n view: 'view',\n pagePrev: 'pagePrev',\n pageNext: 'pageNext',\n} as const;\nexport type PadAction = (typeof PAD_ACTIONS)[keyof typeof PAD_ACTIONS];\n\n/** W3C Standard Gamepad button indices, named by POSITION (Nintendo swaps A/B labels). */\nexport const STANDARD_BUTTON = {\n south: 0,\n east: 1,\n west: 2,\n north: 3,\n leftShoulder: 4,\n rightShoulder: 5,\n dpadUp: 12,\n dpadDown: 13,\n dpadLeft: 14,\n dpadRight: 15,\n} as const;\n\n/** Edge-triggered buttons, in dispatch order. */\nexport const FACE_BUTTON_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.south, PAD_ACTIONS.confirm],\n [STANDARD_BUTTON.east, PAD_ACTIONS.back],\n [STANDARD_BUTTON.west, PAD_ACTIONS.view],\n [STANDARD_BUTTON.north, PAD_ACTIONS.mood],\n [STANDARD_BUTTON.leftShoulder, PAD_ACTIONS.pagePrev],\n [STANDARD_BUTTON.rightShoulder, PAD_ACTIONS.pageNext],\n];\n\n/** Held (auto-repeating) buttons; the first pressed one wins. */\nexport const DPAD_ACTIONS: ReadonlyArray<readonly [number, PadAction]> = [\n [STANDARD_BUTTON.dpadUp, PAD_ACTIONS.up],\n [STANDARD_BUTTON.dpadDown, PAD_ACTIONS.down],\n [STANDARD_BUTTON.dpadLeft, PAD_ACTIONS.left],\n [STANDARD_BUTTON.dpadRight, PAD_ACTIONS.right],\n];\n\n/** Left-stick magnitude below which the stick counts as centred. */\nexport const STICK_DEAD_ZONE = 0.5;\n/** Delay before a held direction starts repeating (ms). */\nexport const REPEAT_DELAY_MS = 380;\n/** Interval between repeats once repeating (ms). */\nexport const REPEAT_INTERVAL_MS = 110;\n","import {\n DPAD_ACTIONS,\n FACE_BUTTON_ACTIONS,\n PAD_ACTIONS,\n REPEAT_DELAY_MS,\n REPEAT_INTERVAL_MS,\n STICK_DEAD_ZONE,\n type PadAction,\n} from './padMap';\n\n/** One frame of pad state: pressed flags by Standard button index, and raw axes. */\nexport interface PadSnapshot {\n buttons: readonly boolean[];\n axes: readonly number[];\n}\n\n/** Which direction is held, when it started, and when it last fired. */\nexport interface RepeatState {\n held: PadAction | null;\n since: number;\n lastFire: number;\n}\n\nexport const IDLE_REPEAT: RepeatState = { held: null, since: 0, lastFire: 0 };\n\nconst STICK_X = 0;\nconst STICK_Y = 1;\n\nfunction stickDirection(axes: readonly number[]): PadAction | null {\n const x = axes[STICK_X] ?? 0;\n const y = axes[STICK_Y] ?? 0;\n if (Math.max(Math.abs(x), Math.abs(y)) < STICK_DEAD_ZONE) {\n return null;\n }\n if (Math.abs(x) > Math.abs(y)) {\n return x > 0 ? PAD_ACTIONS.right : PAD_ACTIONS.left;\n }\n return y > 0 ? PAD_ACTIONS.down : PAD_ACTIONS.up;\n}\n\nfunction heldDirection(snapshot: PadSnapshot): PadAction | null {\n const dpad = DPAD_ACTIONS.find(([index]) => snapshot.buttons[index] === true);\n return dpad === undefined ? stickDirection(snapshot.axes) : dpad[1];\n}\n\n/** One poll frame → the actions to dispatch and the next repeat state. Pure. */\nexport function stepPad(\n prev: PadSnapshot | null,\n next: PadSnapshot,\n repeat: RepeatState,\n now: number,\n): { actions: PadAction[]; repeat: RepeatState } {\n const actions: PadAction[] = FACE_BUTTON_ACTIONS\n .filter(([index]) => next.buttons[index] === true && prev?.buttons[index] !== true)\n .map(([, action]) => action);\n const direction = heldDirection(next);\n if (direction === null) {\n return { actions, repeat: IDLE_REPEAT };\n }\n if (direction !== repeat.held) {\n return { actions: [...actions, direction], repeat: { held: direction, since: now, lastFire: now } };\n }\n const firstRepeat = repeat.lastFire === repeat.since;\n const wait = firstRepeat ? REPEAT_DELAY_MS : REPEAT_INTERVAL_MS;\n if (now - repeat.lastFire < wait) {\n return { actions, repeat };\n }\n return { actions: [...actions, direction], repeat: { ...repeat, lastFire: now } };\n}\n","import type { PadAction } from './padMap';\nimport { IDLE_REPEAT, stepPad, type PadSnapshot, type RepeatState } from './stepPad';\n\n/** The structural subset of the DOM `Gamepad` the poller reads. */\nexport interface PadLike {\n connected: boolean;\n buttons: ReadonlyArray<{ pressed: boolean }>;\n axes: readonly number[];\n}\n\nexport interface GamepadPollerOptions {\n /** Usually `() => navigator.getGamepads()`. */\n getPads: () => ReadonlyArray<PadLike | null>;\n /** Usually `requestAnimationFrame`. */\n requestFrame: (cb: (now: number) => void) => number;\n /** Usually `cancelAnimationFrame`. */\n cancelFrame: (id: number) => void;\n onAction: (action: PadAction) => void;\n}\n\n/**\n * rAF-driven poller over the first connected pad. Starts idle; call start().\n * Safe to stop() (or stop() then start()) from inside `onAction`.\n */\nexport function createGamepadPoller(opts: GamepadPollerOptions): { start: () => void; stop: () => void } {\n let prev: PadSnapshot | null = null;\n let repeat: RepeatState = IDLE_REPEAT;\n let running = false;\n let frame: number | null = null;\n\n const tick = (now: number): void => {\n frame = null;\n const pad = opts.getPads().find((p): p is PadLike => p !== null && p.connected);\n if (pad === undefined) {\n prev = null;\n repeat = IDLE_REPEAT;\n } else {\n const snapshot: PadSnapshot = { buttons: pad.buttons.map((b) => b.pressed), axes: pad.axes };\n const step = stepPad(prev, snapshot, repeat, now);\n prev = snapshot;\n repeat = step.repeat;\n step.actions.forEach((action) => opts.onAction(action));\n }\n if (running && frame === null) {\n frame = opts.requestFrame(tick);\n }\n };\n\n return {\n start: (): void => {\n if (running) {\n return;\n }\n running = true;\n if (frame === null) {\n frame = opts.requestFrame(tick);\n }\n },\n stop: (): void => {\n running = false;\n if (frame !== null) {\n opts.cancelFrame(frame);\n frame = null;\n }\n },\n };\n}\n","import type { VibrationPattern } from './types';\n\n/** Default localStorage key for the haptics on/off preference. */\nexport const DEFAULT_HAPTICS_STORAGE_KEY = 'game.haptics';\n\n/** Default confirmation tick fired when haptics are turned ON (ms). */\nexport const DEFAULT_ENABLE_CONFIRMATION = 15;\n\n/** Stored value meaning \"disabled\". Any other value (or absent) means enabled. */\nexport const DISABLED_VALUE = '0';\n\n/** Stored value meaning \"enabled\". */\nexport const ENABLED_VALUE = '1';\n\n/** The semantic intents games map their events onto. */\nexport type HapticIntent =\n | 'tap'\n | 'select'\n | 'lock'\n | 'bump'\n | 'navigate'\n | 'success'\n | 'error'\n | 'loss'\n | 'reveal';\n\n/**\n * A shared vocabulary of vibration patterns keyed by intent, so every game uses\n * the same tuned feel instead of re-picking magic durations. Pass any of these to\n * {@link Haptics.vibrate} (or supply your own pattern).\n */\nexport const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern> = {\n /** Light tick for menu navigation / selection / undo / restart. */\n tap: 10,\n /** Light pulse when a tool/charm/item is picked up or selected. */\n select: 12,\n /** Short pulse when something locks into place. */\n lock: 18,\n /** Dull buzz when an action is blocked. */\n bump: 24,\n /** Quick pulse when stepping through a portal / page turn. */\n navigate: 16,\n /** Celebratory pattern when a level/chamber is cleared. */\n success: [0, 40, 50, 90],\n /** Sharp double crack for a fatal/destructive event. */\n error: [0, 14, 24, 14],\n /** Heavier buzz when the player loses / falls. */\n loss: [0, 40, 50, 40],\n /** Rising triple pulse the instant a puzzle resolves / an exit opens. */\n reveal: [0, 14, 20, 18, 26, 24],\n};\n","import {\n DEFAULT_ENABLE_CONFIRMATION,\n DEFAULT_HAPTICS_STORAGE_KEY,\n DISABLED_VALUE,\n ENABLED_VALUE,\n} from './constants';\nimport type { Haptics, HapticsOptions, HapticsStorage, VibrationCapable, VibrationPattern } from './types';\n\nfunction resolveNavigator(explicit?: VibrationCapable): VibrationCapable | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { navigator?: VibrationCapable }).navigator;\n}\n\nfunction resolveStorage(explicit?: HapticsStorage): HapticsStorage | undefined {\n if (explicit) {\n return explicit;\n }\n return (globalThis as { localStorage?: HapticsStorage }).localStorage;\n}\n\n/**\n * Create a player-toggleable, always-safe haptics controller.\n *\n * The on/off preference is read from (and persisted to) the configured storage key\n * so it survives reloads. Every vibration routes through one gated call that no-ops\n * when disabled, when no Vibration API is present, or if the platform blocks it.\n */\nexport function createHaptics(options: HapticsOptions = {}): Haptics {\n const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;\n const confirmation =\n options.enableConfirmation === undefined ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;\n const navigator = resolveNavigator(options.navigator);\n const storage = resolveStorage(options.storage);\n\n const readPref = (): boolean => {\n try {\n return storage?.getItem(storageKey) !== DISABLED_VALUE;\n } catch {\n // Reading storage can throw (blocked cookies / sandboxed iframe) — default ON.\n return true;\n }\n };\n\n let enabled = readPref();\n\n const vibrate = (pattern: VibrationPattern): void => {\n if (!enabled) {\n return;\n }\n try {\n navigator?.vibrate?.(pattern);\n } catch {\n // Vibration can throw if blocked by the platform — never let it break a move.\n }\n };\n\n const persist = (): void => {\n try {\n storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);\n } catch {\n // Writing storage can throw (private mode / quota) — ignore.\n }\n };\n\n const setEnabled = (next: boolean): boolean => {\n enabled = next;\n persist();\n // A confirming tick when turning ON so the player feels it engage.\n if (enabled && confirmation !== null) {\n vibrate(confirmation);\n }\n return enabled;\n };\n\n return {\n isEnabled: (): boolean => enabled,\n setEnabled,\n toggle: (): boolean => setEnabled(!enabled),\n vibrate,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dloizides/game-input",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "description": "Framework-agnostic game input primitives. v1 ships safe, player-toggleable haptics (Vibration API wrapper, localStorage-gated, zero deps). Extracted from Aurora; consumed across EisaiPollis games.",
5
5
  "keywords": [
6
6
  "haptics",