@dloizides/game-input 1.0.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 ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@dloizides/game-input` are documented here.
4
+
5
+ ## 1.0.0
6
+
7
+ Initial release — the haptics primitive of the C4 games shared kit.
8
+
9
+ - `createHaptics(options)` — player-toggleable, always-safe Web Vibration API wrapper
10
+ (localStorage-gated, structural/injectable `navigator` + `storage`, never throws into the game loop).
11
+ - `HAPTIC_PATTERNS` — shared semantic vibration vocabulary (tap/select/lock/bump/navigate/success/error/loss/reveal).
12
+ - Zero runtime dependencies; isomorphic; dual CJS/ESM; 100% test coverage.
13
+
14
+ Extracted from Aurora's `src/input/haptics.ts`. First consumers: Aurora (re-export shim) and Morphe (new adoption).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 dloizides
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
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
+ - `options.storageKey` (default `game.haptics`) — localStorage key for the on/off preference.
61
+ - `options.enableConfirmation` (default a `15`ms tick; pass `null` to disable) — fired when turning ON.
62
+ - `options.navigator` / `options.storage` — inject for tests/RN; default to `globalThis`.
63
+ - `HAPTIC_PATTERNS`, `DEFAULT_HAPTICS_STORAGE_KEY`, `DEFAULT_ENABLE_CONFIRMATION`.
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A vibration pattern: a single duration in ms, or an on/off pattern
3
+ * (`[wait, on, off, on, ...]`) exactly as the Web Vibration API expects.
4
+ */
5
+ type VibrationPattern = number | number[];
6
+ /**
7
+ * The slice of `navigator` haptics needs. Structural so it works with the real
8
+ * browser `Navigator`, React Native shims, or a test double — no DOM lib required.
9
+ */
10
+ interface VibrationCapable {
11
+ vibrate?: (pattern: VibrationPattern) => boolean;
12
+ }
13
+ /**
14
+ * The slice of `localStorage` haptics needs to persist the player's toggle.
15
+ * Structural so any key/value store (or a test double) can back it.
16
+ */
17
+ interface HapticsStorage {
18
+ getItem(key: string): string | null;
19
+ setItem(key: string, value: string): void;
20
+ }
21
+ /** Options for {@link createHaptics}. All optional — sensible browser defaults apply. */
22
+ interface HapticsOptions {
23
+ /** localStorage key the on/off preference is persisted under. Default `game.haptics`. */
24
+ storageKey?: string;
25
+ /**
26
+ * Pattern fired the moment haptics are turned ON, so the player feels it engage.
27
+ * Pass `null` to disable the confirmation. Default a single 15ms tick.
28
+ */
29
+ enableConfirmation?: VibrationPattern | null;
30
+ /** Vibration source. Defaults to `globalThis.navigator`. Inject for tests/RN. */
31
+ navigator?: VibrationCapable;
32
+ /** Preference store. Defaults to `globalThis.localStorage`. Inject for tests. */
33
+ storage?: HapticsStorage;
34
+ }
35
+ /**
36
+ * A player-toggleable, always-safe haptics controller. Every call no-ops silently
37
+ * when haptics are disabled, unsupported, or blocked by the platform — so call sites
38
+ * can fire haptics unconditionally alongside their SFX/visual feedback.
39
+ */
40
+ interface Haptics {
41
+ /** Whether haptics are currently enabled. */
42
+ isEnabled(): boolean;
43
+ /** Set the enabled state, persist it, and (when enabling) fire the confirmation tick. Returns the new state. */
44
+ setEnabled(enabled: boolean): boolean;
45
+ /** Flip the enabled state (for a settings toggle). Returns the new state. */
46
+ toggle(): boolean;
47
+ /** Vibrate with the given pattern. No-ops when disabled/unsupported; never throws. */
48
+ vibrate(pattern: VibrationPattern): void;
49
+ }
50
+
51
+ /**
52
+ * Create a player-toggleable, always-safe haptics controller.
53
+ *
54
+ * The on/off preference is read from (and persisted to) the configured storage key
55
+ * so it survives reloads. Every vibration routes through one gated call that no-ops
56
+ * when disabled, when no Vibration API is present, or if the platform blocks it.
57
+ */
58
+ declare function createHaptics(options?: HapticsOptions): Haptics;
59
+
60
+ /** Default localStorage key for the haptics on/off preference. */
61
+ declare const DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
62
+ /** Default confirmation tick fired when haptics are turned ON (ms). */
63
+ declare const DEFAULT_ENABLE_CONFIRMATION = 15;
64
+ /** The semantic intents games map their events onto. */
65
+ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success' | 'error' | 'loss' | 'reveal';
66
+ /**
67
+ * A shared vocabulary of vibration patterns keyed by intent, so every game uses
68
+ * the same tuned feel instead of re-picking magic durations. Pass any of these to
69
+ * {@link Haptics.vibrate} (or supply your own pattern).
70
+ */
71
+ declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
72
+
73
+ export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, type VibrationCapable, type VibrationPattern, createHaptics };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A vibration pattern: a single duration in ms, or an on/off pattern
3
+ * (`[wait, on, off, on, ...]`) exactly as the Web Vibration API expects.
4
+ */
5
+ type VibrationPattern = number | number[];
6
+ /**
7
+ * The slice of `navigator` haptics needs. Structural so it works with the real
8
+ * browser `Navigator`, React Native shims, or a test double — no DOM lib required.
9
+ */
10
+ interface VibrationCapable {
11
+ vibrate?: (pattern: VibrationPattern) => boolean;
12
+ }
13
+ /**
14
+ * The slice of `localStorage` haptics needs to persist the player's toggle.
15
+ * Structural so any key/value store (or a test double) can back it.
16
+ */
17
+ interface HapticsStorage {
18
+ getItem(key: string): string | null;
19
+ setItem(key: string, value: string): void;
20
+ }
21
+ /** Options for {@link createHaptics}. All optional — sensible browser defaults apply. */
22
+ interface HapticsOptions {
23
+ /** localStorage key the on/off preference is persisted under. Default `game.haptics`. */
24
+ storageKey?: string;
25
+ /**
26
+ * Pattern fired the moment haptics are turned ON, so the player feels it engage.
27
+ * Pass `null` to disable the confirmation. Default a single 15ms tick.
28
+ */
29
+ enableConfirmation?: VibrationPattern | null;
30
+ /** Vibration source. Defaults to `globalThis.navigator`. Inject for tests/RN. */
31
+ navigator?: VibrationCapable;
32
+ /** Preference store. Defaults to `globalThis.localStorage`. Inject for tests. */
33
+ storage?: HapticsStorage;
34
+ }
35
+ /**
36
+ * A player-toggleable, always-safe haptics controller. Every call no-ops silently
37
+ * when haptics are disabled, unsupported, or blocked by the platform — so call sites
38
+ * can fire haptics unconditionally alongside their SFX/visual feedback.
39
+ */
40
+ interface Haptics {
41
+ /** Whether haptics are currently enabled. */
42
+ isEnabled(): boolean;
43
+ /** Set the enabled state, persist it, and (when enabling) fire the confirmation tick. Returns the new state. */
44
+ setEnabled(enabled: boolean): boolean;
45
+ /** Flip the enabled state (for a settings toggle). Returns the new state. */
46
+ toggle(): boolean;
47
+ /** Vibrate with the given pattern. No-ops when disabled/unsupported; never throws. */
48
+ vibrate(pattern: VibrationPattern): void;
49
+ }
50
+
51
+ /**
52
+ * Create a player-toggleable, always-safe haptics controller.
53
+ *
54
+ * The on/off preference is read from (and persisted to) the configured storage key
55
+ * so it survives reloads. Every vibration routes through one gated call that no-ops
56
+ * when disabled, when no Vibration API is present, or if the platform blocks it.
57
+ */
58
+ declare function createHaptics(options?: HapticsOptions): Haptics;
59
+
60
+ /** Default localStorage key for the haptics on/off preference. */
61
+ declare const DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
62
+ /** Default confirmation tick fired when haptics are turned ON (ms). */
63
+ declare const DEFAULT_ENABLE_CONFIRMATION = 15;
64
+ /** The semantic intents games map their events onto. */
65
+ type HapticIntent = 'tap' | 'select' | 'lock' | 'bump' | 'navigate' | 'success' | 'error' | 'loss' | 'reveal';
66
+ /**
67
+ * A shared vocabulary of vibration patterns keyed by intent, so every game uses
68
+ * the same tuned feel instead of re-picking magic durations. Pass any of these to
69
+ * {@link Haptics.vibrate} (or supply your own pattern).
70
+ */
71
+ declare const HAPTIC_PATTERNS: Record<HapticIntent, VibrationPattern>;
72
+
73
+ export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, type HapticIntent, type Haptics, type HapticsOptions, type HapticsStorage, type VibrationCapable, type VibrationPattern, createHaptics };
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ // src/constants.ts
4
+ var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
5
+ var DEFAULT_ENABLE_CONFIRMATION = 15;
6
+ var DISABLED_VALUE = "0";
7
+ var ENABLED_VALUE = "1";
8
+ var HAPTIC_PATTERNS = {
9
+ /** Light tick for menu navigation / selection / undo / restart. */
10
+ tap: 10,
11
+ /** Light pulse when a tool/charm/item is picked up or selected. */
12
+ select: 12,
13
+ /** Short pulse when something locks into place. */
14
+ lock: 18,
15
+ /** Dull buzz when an action is blocked. */
16
+ bump: 24,
17
+ /** Quick pulse when stepping through a portal / page turn. */
18
+ navigate: 16,
19
+ /** Celebratory pattern when a level/chamber is cleared. */
20
+ success: [0, 40, 50, 90],
21
+ /** Sharp double crack for a fatal/destructive event. */
22
+ error: [0, 14, 24, 14],
23
+ /** Heavier buzz when the player loses / falls. */
24
+ loss: [0, 40, 50, 40],
25
+ /** Rising triple pulse the instant a puzzle resolves / an exit opens. */
26
+ reveal: [0, 14, 20, 18, 26, 24]
27
+ };
28
+
29
+ // src/haptics.ts
30
+ function resolveNavigator(explicit) {
31
+ if (explicit) {
32
+ return explicit;
33
+ }
34
+ return globalThis.navigator;
35
+ }
36
+ function resolveStorage(explicit) {
37
+ if (explicit) {
38
+ return explicit;
39
+ }
40
+ return globalThis.localStorage;
41
+ }
42
+ function createHaptics(options = {}) {
43
+ const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;
44
+ const confirmation = options.enableConfirmation === void 0 ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;
45
+ const navigator = resolveNavigator(options.navigator);
46
+ const storage = resolveStorage(options.storage);
47
+ const readPref = () => {
48
+ try {
49
+ return storage?.getItem(storageKey) !== DISABLED_VALUE;
50
+ } catch {
51
+ return true;
52
+ }
53
+ };
54
+ let enabled = readPref();
55
+ const vibrate = (pattern) => {
56
+ if (!enabled) {
57
+ return;
58
+ }
59
+ try {
60
+ navigator?.vibrate?.(pattern);
61
+ } catch {
62
+ }
63
+ };
64
+ const persist = () => {
65
+ try {
66
+ storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);
67
+ } catch {
68
+ }
69
+ };
70
+ const setEnabled = (next) => {
71
+ enabled = next;
72
+ persist();
73
+ if (enabled && confirmation !== null) {
74
+ vibrate(confirmation);
75
+ }
76
+ return enabled;
77
+ };
78
+ return {
79
+ isEnabled: () => enabled,
80
+ setEnabled,
81
+ toggle: () => setEnabled(!enabled),
82
+ vibrate
83
+ };
84
+ }
85
+
86
+ exports.DEFAULT_ENABLE_CONFIRMATION = DEFAULT_ENABLE_CONFIRMATION;
87
+ exports.DEFAULT_HAPTICS_STORAGE_KEY = DEFAULT_HAPTICS_STORAGE_KEY;
88
+ exports.HAPTIC_PATTERNS = HAPTIC_PATTERNS;
89
+ exports.createHaptics = createHaptics;
90
+ //# sourceMappingURL=index.js.map
91
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,86 @@
1
+ // src/constants.ts
2
+ var DEFAULT_HAPTICS_STORAGE_KEY = "game.haptics";
3
+ var DEFAULT_ENABLE_CONFIRMATION = 15;
4
+ var DISABLED_VALUE = "0";
5
+ var ENABLED_VALUE = "1";
6
+ var HAPTIC_PATTERNS = {
7
+ /** Light tick for menu navigation / selection / undo / restart. */
8
+ tap: 10,
9
+ /** Light pulse when a tool/charm/item is picked up or selected. */
10
+ select: 12,
11
+ /** Short pulse when something locks into place. */
12
+ lock: 18,
13
+ /** Dull buzz when an action is blocked. */
14
+ bump: 24,
15
+ /** Quick pulse when stepping through a portal / page turn. */
16
+ navigate: 16,
17
+ /** Celebratory pattern when a level/chamber is cleared. */
18
+ success: [0, 40, 50, 90],
19
+ /** Sharp double crack for a fatal/destructive event. */
20
+ error: [0, 14, 24, 14],
21
+ /** Heavier buzz when the player loses / falls. */
22
+ loss: [0, 40, 50, 40],
23
+ /** Rising triple pulse the instant a puzzle resolves / an exit opens. */
24
+ reveal: [0, 14, 20, 18, 26, 24]
25
+ };
26
+
27
+ // src/haptics.ts
28
+ function resolveNavigator(explicit) {
29
+ if (explicit) {
30
+ return explicit;
31
+ }
32
+ return globalThis.navigator;
33
+ }
34
+ function resolveStorage(explicit) {
35
+ if (explicit) {
36
+ return explicit;
37
+ }
38
+ return globalThis.localStorage;
39
+ }
40
+ function createHaptics(options = {}) {
41
+ const storageKey = options.storageKey ?? DEFAULT_HAPTICS_STORAGE_KEY;
42
+ const confirmation = options.enableConfirmation === void 0 ? DEFAULT_ENABLE_CONFIRMATION : options.enableConfirmation;
43
+ const navigator = resolveNavigator(options.navigator);
44
+ const storage = resolveStorage(options.storage);
45
+ const readPref = () => {
46
+ try {
47
+ return storage?.getItem(storageKey) !== DISABLED_VALUE;
48
+ } catch {
49
+ return true;
50
+ }
51
+ };
52
+ let enabled = readPref();
53
+ const vibrate = (pattern) => {
54
+ if (!enabled) {
55
+ return;
56
+ }
57
+ try {
58
+ navigator?.vibrate?.(pattern);
59
+ } catch {
60
+ }
61
+ };
62
+ const persist = () => {
63
+ try {
64
+ storage?.setItem(storageKey, enabled ? ENABLED_VALUE : DISABLED_VALUE);
65
+ } catch {
66
+ }
67
+ };
68
+ const setEnabled = (next) => {
69
+ enabled = next;
70
+ persist();
71
+ if (enabled && confirmation !== null) {
72
+ vibrate(confirmation);
73
+ }
74
+ return enabled;
75
+ };
76
+ return {
77
+ isEnabled: () => enabled,
78
+ setEnabled,
79
+ toggle: () => setEnabled(!enabled),
80
+ vibrate
81
+ };
82
+ }
83
+
84
+ export { DEFAULT_ENABLE_CONFIRMATION, DEFAULT_HAPTICS_STORAGE_KEY, HAPTIC_PATTERNS, createHaptics };
85
+ //# sourceMappingURL=index.mjs.map
86
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@dloizides/game-input",
3
+ "version": "1.0.0",
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
+ "keywords": [
6
+ "haptics",
7
+ "vibration",
8
+ "game-input",
9
+ "phaser",
10
+ "web-game",
11
+ "eisaipollis"
12
+ ],
13
+ "author": "dloizides",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/openmindednewby/game-input.git"
18
+ },
19
+ "homepage": "https://github.com/openmindednewby/game-input#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/openmindednewby/game-input/issues"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.mjs",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "browser": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "import": {
37
+ "types": "./dist/index.d.mts",
38
+ "default": "./dist/index.mjs"
39
+ },
40
+ "default": {
41
+ "types": "./dist/index.d.ts",
42
+ "default": "./dist/index.js"
43
+ }
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "CHANGELOG.md"
50
+ ],
51
+ "sideEffects": false,
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "rimraf dist && tsup",
57
+ "build:watch": "tsup --watch",
58
+ "test": "jest",
59
+ "test:watch": "jest --watch",
60
+ "test:coverage": "jest --coverage",
61
+ "lint": "eslint src --ext .ts",
62
+ "lint:fix": "eslint src --ext .ts --fix",
63
+ "typecheck": "tsc --noEmit",
64
+ "clean": "rimraf dist",
65
+ "security:audit": "npm audit --audit-level=high",
66
+ "prepublishOnly": "npm run clean && npm run build && npm run test"
67
+ },
68
+ "devDependencies": {
69
+ "@types/jest": "^29.5.0",
70
+ "@types/node": "^20.19.32",
71
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
72
+ "@typescript-eslint/parser": "^7.0.0",
73
+ "eslint": "^8.57.0",
74
+ "jest": "^29.7.0",
75
+ "rimraf": "^5.0.0",
76
+ "ts-jest": "^29.1.0",
77
+ "tsup": "^8.0.0",
78
+ "typescript": "^5.4.0"
79
+ }
80
+ }