@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10
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 +163 -0
- package/LICENSE +21 -21
- package/README.md +203 -139
- package/dist/auto.d.ts +8 -8
- package/dist/auto.js +3 -3
- package/dist/avatar-pixi.d.ts +165 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +299 -0
- package/dist/avatar-pixi.js.map +1 -0
- package/dist/avatar-svg.d.ts +4 -0
- package/dist/avatar-svg.js +5 -0
- package/dist/avatar-three.d.ts +69 -0
- package/dist/avatar-three.d.ts.map +1 -0
- package/dist/avatar-three.js +49 -0
- package/dist/avatar-three.js.map +1 -0
- package/dist/avatar.d.ts +7 -0
- package/dist/avatar.js +500 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +119 -28
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/input.d.ts +2 -2
- package/dist/input.js +2 -2
- package/dist/mp.d.ts +3 -3
- package/dist/race.d.ts +1 -1
- package/dist/rooms.d.ts +3 -3
- package/dist/rooms.js +1 -1
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1946 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +138 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +123 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +18 -4
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +145 -19
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +138 -25
- package/dist/shared/daily.js.map +1 -1
- package/dist/shared/defineProperty.js +39 -0
- package/dist/shared/describe.d.ts +32 -0
- package/dist/shared/describe.d.ts.map +1 -0
- package/dist/shared/describe.js +109 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +9 -42
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1122 -46
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +74 -14
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +179 -25
- package/dist/shared/input.js.map +1 -1
- package/dist/shared/json-guards.js +48 -0
- package/dist/shared/json-guards.js.map +1 -0
- package/dist/shared/latest-state.js +1 -1
- package/dist/shared/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +10 -2
- package/dist/shared/mp.js.map +1 -1
- package/dist/shared/multiplayer-types.d.ts +15 -2
- package/dist/shared/multiplayer-types.d.ts.map +1 -1
- package/dist/shared/overlays.js +800 -0
- package/dist/shared/overlays.js.map +1 -0
- package/dist/shared/page-stack.js +2 -1
- package/dist/shared/page-stack.js.map +1 -1
- package/dist/shared/pose-key.js +249 -0
- package/dist/shared/pose-key.js.map +1 -0
- package/dist/shared/race-types.d.ts +1 -1
- package/dist/shared/race.js +1 -1
- package/dist/shared/room-types.d.ts +28 -3
- package/dist/shared/room-types.d.ts.map +1 -1
- package/dist/shared/rooms.d.ts.map +1 -1
- package/dist/shared/rooms.js +23 -4
- package/dist/shared/rooms.js.map +1 -1
- package/dist/shared/runtime-opener.js +1 -1
- package/dist/shared/scene.d.ts +90 -0
- package/dist/shared/scene.d.ts.map +1 -0
- package/dist/shared/social.d.ts +90 -9
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/social.js +6 -1
- package/dist/shared/social.js.map +1 -1
- package/dist/shared/src.js +4563 -667
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +2354 -0
- package/dist/shared/svg-markup.js.map +1 -0
- package/dist/shared/svg.js +27 -0
- package/dist/shared/svg.js.map +1 -0
- package/dist/shared/types.d.ts +174 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/wire-types.d.ts +6 -2
- package/dist/shared/wire-types.d.ts.map +1 -1
- package/dist/social.d.ts +5 -5
- package/dist/social.js +2 -2
- package/dist/turns-rules.d.ts +2 -0
- package/dist/turns-rules.js +1 -0
- package/dist/turns.d.ts +61 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +183 -0
- package/dist/turns.js.map +1 -0
- package/package.json +125 -89
- package/src/auto.ts +39 -39
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +329 -0
- package/src/avatar/adapters/pixi-types.ts +84 -0
- package/src/avatar/adapters/three.ts +126 -0
- package/src/avatar/animator/animator.ts +285 -0
- package/src/avatar/animator/reactions.ts +44 -0
- package/src/avatar/animator/schedule.ts +73 -0
- package/src/avatar/art/beard.ts +49 -0
- package/src/avatar/art/glasses.ts +67 -0
- package/src/avatar/art/hair.ts +254 -0
- package/src/avatar/art/headwear.ts +75 -0
- package/src/avatar/art/index.ts +21 -0
- package/src/avatar/art/shape.ts +87 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +246 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +79 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +173 -0
- package/src/avatar/layout.ts +47 -0
- package/src/avatar/look-like.ts +52 -0
- package/src/avatar/outputs/bitmap.ts +75 -0
- package/src/avatar/outputs/canvas-paint.ts +177 -0
- package/src/avatar/outputs/canvas.ts +62 -0
- package/src/avatar/outputs/custom-element.ts +140 -0
- package/src/avatar/outputs/element.ts +58 -0
- package/src/avatar/outputs/live-canvas.ts +57 -0
- package/src/avatar/outputs/overlay-svg.ts +22 -0
- package/src/avatar/outputs/pose-key.ts +30 -0
- package/src/avatar/outputs/svg-markup.ts +113 -0
- package/src/avatar/outputs/svg.ts +41 -0
- package/src/avatar/overlays.ts +128 -0
- package/src/avatar/render/body.ts +116 -0
- package/src/avatar/render/colours.ts +81 -0
- package/src/avatar/render/compose.ts +320 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +118 -0
- package/src/avatar/render/frame.ts +73 -0
- package/src/avatar/render/head.ts +81 -0
- package/src/avatar/render/mouth.ts +86 -0
- package/src/avatar/render/props.ts +91 -0
- package/src/avatar/render/types.ts +93 -0
- package/src/avatar/render/walker-parts.ts +121 -0
- package/src/avatar/render/walker.ts +199 -0
- package/src/avatar/scene.ts +119 -0
- package/src/avatar-pixi.ts +20 -0
- package/src/avatar-svg.ts +21 -0
- package/src/avatar-three.ts +18 -0
- package/src/avatar.ts +63 -0
- package/src/core/avatar/avatar-cache.ts +95 -0
- package/src/core/avatar/avatar-ref.ts +34 -0
- package/src/core/avatar/avatar-types.ts +43 -0
- package/src/core/avatar/hub-avatar.ts +308 -0
- package/src/core/connection.ts +408 -299
- package/src/core/errors.ts +160 -147
- package/src/core/game-data.ts +40 -38
- package/src/core/hub-economy.ts +216 -179
- package/src/core/hub-progress.ts +269 -205
- package/src/core/hub.ts +352 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/cloud-saves.ts +62 -0
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
- package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
- package/src/core/offline/hub-kinds/game-fields.ts +2 -0
- package/src/core/offline/hub-kinds/index.ts +52 -0
- package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
- package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
- package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/offline/kind-adapters.ts +48 -0
- package/src/core/offline/local-inventory.ts +64 -0
- package/src/core/offline/local-store.ts +304 -261
- package/src/core/offline/offline-api.ts +65 -0
- package/src/core/offline/outbox-entry.ts +59 -0
- package/src/core/offline/outbox-kinds.ts +78 -0
- package/src/core/offline/outbox-merge.ts +39 -0
- package/src/core/offline/outbox-replayer.ts +141 -0
- package/src/core/offline/outbox-store.ts +85 -0
- package/src/core/offline/outbox.ts +232 -0
- package/src/core/offline/queue.ts +126 -171
- package/src/core/offline/replay-lease.ts +87 -0
- package/src/core/offline/replay-policy.ts +52 -0
- package/src/core/page-account.ts +121 -0
- package/src/core/page.ts +87 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +163 -152
- package/src/core/rest/routes.gen.ts +144 -88
- package/src/core/resume/hub-resume.ts +286 -0
- package/src/core/resume/local-resume.ts +130 -0
- package/src/core/resume/resume-list.ts +65 -0
- package/src/core/resume/resume-request.ts +52 -0
- package/src/core/resume/resume-types.ts +64 -0
- package/src/core/saved/daily-saves.ts +196 -0
- package/src/core/saved/daily-seed.ts +16 -0
- package/src/core/saved/hub-saved.ts +221 -0
- package/src/core/saved/local-saved.ts +122 -0
- package/src/core/saved/saved-list.ts +129 -0
- package/src/core/saved/saved-open.ts +104 -0
- package/src/core/saved/saved-order.ts +49 -0
- package/src/core/saved/saved-types.ts +24 -0
- package/src/core/storage/json-guards.ts +80 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/turns/hub-turns.ts +161 -0
- package/src/core/turns/turn-game.ts +510 -0
- package/src/core/turns/your-turn-buzz.ts +69 -0
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +276 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +105 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +86 -76
- package/src/doctor/cli.ts +101 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/doctor/world-check.ts +61 -0
- package/src/globals.ts +115 -110
- package/src/index.ts +68 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/focus-ring.ts +13 -0
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +171 -164
- package/src/input/input-system.ts +406 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/modality.ts +122 -0
- package/src/input/nav-state.ts +200 -191
- package/src/input/navigator.ts +182 -171
- package/src/input/on-screen-controls.ts +101 -101
- package/src/input/page-control.ts +43 -43
- package/src/input/page-input.ts +193 -193
- package/src/input/player-remaps.ts +77 -77
- package/src/input/source-values.ts +116 -116
- package/src/input/spatial.ts +47 -47
- package/src/input/types.ts +331 -331
- package/src/input/zero-config.ts +164 -177
- package/src/input.ts +26 -22
- package/src/mp.ts +10 -10
- package/src/overlay/controls-panel.ts +163 -163
- package/src/overlay/dom.ts +46 -46
- package/src/overlay/layout-clusters.ts +162 -162
- package/src/overlay/layout-geometry.ts +136 -136
- package/src/overlay/layout.ts +203 -203
- package/src/overlay/listener-scope.ts +23 -23
- package/src/overlay/on-screen-keyboard.ts +137 -137
- package/src/overlay/overlay.ts +234 -208
- package/src/overlay/styles.ts +13 -12
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +126 -121
- package/src/realtime/cross-bundle.ts +29 -29
- package/src/realtime/fallback-notice.ts +48 -48
- package/src/realtime/hub-link.ts +65 -65
- package/src/realtime/latest-state.ts +33 -33
- package/src/realtime/multiplayer-forwarder.ts +40 -28
- package/src/realtime/multiplayer-types.ts +66 -52
- package/src/realtime/page-runtime.ts +60 -60
- package/src/realtime/race-forwarder.ts +59 -59
- package/src/realtime/race-types.ts +132 -132
- package/src/realtime/room-types.ts +151 -125
- package/src/realtime/rooms-forwarder.ts +104 -79
- package/src/realtime/runtime-api.ts +74 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +77 -72
- package/src/realtime/social-types.ts +86 -78
- package/src/realtime/types.ts +54 -53
- package/src/realtime/wire-types.ts +79 -72
- package/src/realtime/world-types.ts +20 -0
- package/src/rooms.ts +10 -10
- package/src/social.ts +19 -18
- package/src/turns-kit/check-rules.ts +281 -0
- package/src/turns-rules.ts +12 -0
- package/src/turns.ts +14 -0
- package/src/types.ts +13 -13
- package/src/uistack/page-stack.ts +45 -45
- package/src/uistack/play-suspension.ts +82 -82
- package/src/uistack/types.ts +76 -76
- package/src/uistack/ui-stack.ts +144 -144
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The small, stable slice of the input system that other bundles on the page
|
|
3
|
-
* use (plan §4.4): the host menu (attach itself, `game.controls`, the
|
|
4
|
-
* Controls button), the social runtime's dialogs (navigate them), the
|
|
5
|
-
* overlay stack (suspend `play`), and a handover between two client copies.
|
|
6
|
-
* Every client version keeps these methods.
|
|
7
|
-
*/
|
|
8
|
-
import { hasMethods } from '../realtime/cross-bundle';
|
|
9
|
-
import type { MenuBridge, NavTarget } from './types';
|
|
10
|
-
|
|
11
|
-
/** The cross-bundle input surface. */
|
|
12
|
-
export interface PageInputControl {
|
|
13
|
-
/** Whether the game declared its inputs. */
|
|
14
|
-
readonly isDefined: boolean;
|
|
15
|
-
/** The host menu attaches itself. */
|
|
16
|
-
attachMenu(bridge: MenuBridge | null): void;
|
|
17
|
-
/** The host menu passes `game.controls`. */
|
|
18
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: game.controls from the page, parsed by the implementation
|
|
19
|
-
setLegacyControls(controls: unknown): void;
|
|
20
|
-
/** The menu's Controls button. */
|
|
21
|
-
openControls(): void;
|
|
22
|
-
/** Whether a group is on. */
|
|
23
|
-
isEnabled(group: string): boolean;
|
|
24
|
-
/** Turn a group on. */
|
|
25
|
-
enable(group: string): void;
|
|
26
|
-
/** Turn a group off. */
|
|
27
|
-
disable(group: string): void;
|
|
28
|
-
/** Make elements gamepad-navigable (the runtime's dialogs). */
|
|
29
|
-
autoNavigate(selector: string | (() => NavTarget[]), options?: { back?: () => void }): void;
|
|
30
|
-
/** Stop navigating them. */
|
|
31
|
-
clearAutoNavigate(): void;
|
|
32
|
-
/** Stop driving the page and hand over to `owner`. Absent on v1 copies. */
|
|
33
|
-
yieldPageTo?(owner: PageInputControl): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The methods every input system on the page has, v1 copies included. */
|
|
37
|
-
const PAGE_INPUT_METHODS = ['attachMenu', 'setLegacyControls', 'openControls', 'isEnabled', 'enable', 'disable', 'autoNavigate', 'clearAutoNavigate', 'define', 'value'] as const;
|
|
38
|
-
|
|
39
|
-
/** Whether `candidate` is some bundle's input system. */
|
|
40
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
|
41
|
-
export function isPageInputControl(candidate: unknown): candidate is PageInputControl {
|
|
42
|
-
return hasMethods(candidate, PAGE_INPUT_METHODS);
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The small, stable slice of the input system that other bundles on the page
|
|
3
|
+
* use (plan §4.4): the host menu (attach itself, `game.controls`, the
|
|
4
|
+
* Controls button), the social runtime's dialogs (navigate them), the
|
|
5
|
+
* overlay stack (suspend `play`), and a handover between two client copies.
|
|
6
|
+
* Every client version keeps these methods.
|
|
7
|
+
*/
|
|
8
|
+
import { hasMethods } from '../realtime/cross-bundle';
|
|
9
|
+
import type { MenuBridge, NavTarget } from './types';
|
|
10
|
+
|
|
11
|
+
/** The cross-bundle input surface. */
|
|
12
|
+
export interface PageInputControl {
|
|
13
|
+
/** Whether the game declared its inputs. */
|
|
14
|
+
readonly isDefined: boolean;
|
|
15
|
+
/** The host menu attaches itself. */
|
|
16
|
+
attachMenu(bridge: MenuBridge | null): void;
|
|
17
|
+
/** The host menu passes `game.controls`. */
|
|
18
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: game.controls from the page, parsed by the implementation
|
|
19
|
+
setLegacyControls(controls: unknown): void;
|
|
20
|
+
/** The menu's Controls button. */
|
|
21
|
+
openControls(): void;
|
|
22
|
+
/** Whether a group is on. */
|
|
23
|
+
isEnabled(group: string): boolean;
|
|
24
|
+
/** Turn a group on. */
|
|
25
|
+
enable(group: string): void;
|
|
26
|
+
/** Turn a group off. */
|
|
27
|
+
disable(group: string): void;
|
|
28
|
+
/** Make elements gamepad-navigable (the runtime's dialogs). */
|
|
29
|
+
autoNavigate(selector: string | (() => NavTarget[]), options?: { back?: () => void }): void;
|
|
30
|
+
/** Stop navigating them. */
|
|
31
|
+
clearAutoNavigate(): void;
|
|
32
|
+
/** Stop driving the page and hand over to `owner`. Absent on v1 copies. */
|
|
33
|
+
yieldPageTo?(owner: PageInputControl): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The methods every input system on the page has, v1 copies included. */
|
|
37
|
+
const PAGE_INPUT_METHODS = ['attachMenu', 'setLegacyControls', 'openControls', 'isEnabled', 'enable', 'disable', 'autoNavigate', 'clearAutoNavigate', 'define', 'value'] as const;
|
|
38
|
+
|
|
39
|
+
/** Whether `candidate` is some bundle's input system. */
|
|
40
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
|
41
|
+
export function isPageInputControl(candidate: unknown): candidate is PageInputControl {
|
|
42
|
+
return hasMethods(candidate, PAGE_INPUT_METHODS);
|
|
43
|
+
}
|
package/src/input/page-input.ts
CHANGED
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The page's one input system (plan §4.4). The game's copy of the client owns
|
|
3
|
-
* the page because the game defines the controls; the host menu reaches it
|
|
4
|
-
* through the stable {@link PageInputControl} slice. Remaps persist to
|
|
5
|
-
* localStorage (offline-safe) and, through the core client, to a reserved
|
|
6
|
-
* cloud save slot.
|
|
7
|
-
*/
|
|
8
|
-
import { HUB_CLIENT_VERSION } from '../core/version/client-version.gen';
|
|
9
|
-
import { claimSingleton, LEGACY_GLOBAL_NAMES } from '../globals';
|
|
10
|
-
import { hasMethods, isObject, readField } from '../realtime/cross-bundle';
|
|
11
|
-
import { InputSystem } from './input-system';
|
|
12
|
-
import { isPageInputControl } from './page-control';
|
|
13
|
-
import type { PageInputControl } from './page-control';
|
|
14
|
-
import type { InputDef, InputHost, InputOverrides, VirtualOverride } from './types';
|
|
15
|
-
|
|
16
|
-
/** The cross-bundle shape number of the `input` singleton. Bump only on an incompatible change. */
|
|
17
|
-
export const INPUT_SINGLETON_API = 1;
|
|
18
|
-
|
|
19
|
-
/** The public methods of every input system, v1 copies included. */
|
|
20
|
-
const INPUT_SYSTEM_METHODS = [
|
|
21
|
-
'define', 'enable', 'disable', 'isEnabled', 'value', 'get', 'down', 'up', 'pressed', 'axis', 'vector', 'on',
|
|
22
|
-
'setVirtualHtml', 'showVirtual', 'setInsets', 'autoNavigate', 'clearAutoNavigate', 'attachMenu', 'setLegacyControls',
|
|
23
|
-
'openControls', 'rebind', 'setVirtual',
|
|
24
|
-
] as const;
|
|
25
|
-
|
|
26
|
-
/** Whether `candidate` is some bundle's input system (v1 copies included). */
|
|
27
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
|
28
|
-
export function isInputSystem(candidate: unknown): candidate is InputSystem {
|
|
29
|
-
return hasMethods(candidate, INPUT_SYSTEM_METHODS);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** How the core client connects the input system to its game. */
|
|
33
|
-
export interface InputOptions {
|
|
34
|
-
/** This page's game slug (default: `window.__HUB__.slug`). */
|
|
35
|
-
slug?: string | null;
|
|
36
|
-
/** Mirror remaps to the cloud (the core client writes the `__controls` save slot). */
|
|
37
|
-
saveToCloud?: (overrides: InputOverrides) => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** A finite number, or `undefined`. */
|
|
41
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
42
|
-
function finiteNumber(candidate: unknown): number | undefined {
|
|
43
|
-
return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** A saved rebind: the keys or gamepad button the player chose. */
|
|
47
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
48
|
-
function parseBinding(saved: unknown): Partial<InputDef> {
|
|
49
|
-
const binding: Partial<InputDef> = {};
|
|
50
|
-
const keys = readField(saved, 'keys');
|
|
51
|
-
if (Array.isArray(keys) && keys.every((key) => typeof key === 'string')) {
|
|
52
|
-
binding.keys = keys.filter((key): key is string => typeof key === 'string');
|
|
53
|
-
}
|
|
54
|
-
const button = finiteNumber(readField(readField(saved, 'gamepad'), 'button'));
|
|
55
|
-
if (button !== undefined) {
|
|
56
|
-
binding.gamepad = { button };
|
|
57
|
-
}
|
|
58
|
-
return binding;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A saved dragged spot. */
|
|
62
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
63
|
-
function parseVirtualOverride(saved: unknown): VirtualOverride {
|
|
64
|
-
const spot: VirtualOverride = {};
|
|
65
|
-
const left = finiteNumber(readField(saved, 'x'));
|
|
66
|
-
const top = finiteNumber(readField(saved, 'y'));
|
|
67
|
-
if (left !== undefined) {
|
|
68
|
-
spot.x = left;
|
|
69
|
-
}
|
|
70
|
-
if (top !== undefined) {
|
|
71
|
-
spot.y = top;
|
|
72
|
-
}
|
|
73
|
-
return spot;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Entries of a saved record, parsed. */
|
|
77
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
78
|
-
function parseRecord<Parsed>(saved: unknown, parseEntry: (entry: unknown) => Parsed): Record<string, Parsed> | undefined {
|
|
79
|
-
if (!isObject(saved)) {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
return Object.fromEntries(Object.entries(saved).map(([key, entry]) => [key, parseEntry(entry)]));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Saved overrides from JSON, keeping only well-formed fields. */
|
|
86
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: JSON read back from localStorage
|
|
87
|
-
export function parseInputOverrides(saved: unknown): InputOverrides {
|
|
88
|
-
const overrides: InputOverrides = {};
|
|
89
|
-
const bindings = parseRecord(readField(saved, 'bindings'), parseBinding);
|
|
90
|
-
const virtual = parseRecord(readField(saved, 'virtual'), parseVirtualOverride);
|
|
91
|
-
const showVirtual = readField(saved, 'showVirtual');
|
|
92
|
-
if (bindings !== undefined) {
|
|
93
|
-
overrides.bindings = bindings;
|
|
94
|
-
}
|
|
95
|
-
if (virtual !== undefined) {
|
|
96
|
-
overrides.virtual = virtual;
|
|
97
|
-
}
|
|
98
|
-
if (typeof showVirtual === 'boolean') {
|
|
99
|
-
overrides.showVirtual = showVirtual;
|
|
100
|
-
}
|
|
101
|
-
return overrides;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** A field of the host's injected `window.__HUB__`. */
|
|
105
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the host page's injected config, checked by the caller
|
|
106
|
-
function injectedField(key: string): unknown {
|
|
107
|
-
return typeof window === 'undefined' ? undefined : readField(readField(window, '__HUB__'), key);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The slug the host injected, or `null`. */
|
|
111
|
-
function injectedSlug(): string | null {
|
|
112
|
-
const slug = injectedField('slug');
|
|
113
|
-
return typeof slug === 'string' ? slug : null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** A new input system, starting from the host's injected `game.controls` (the menu may pass them again later). */
|
|
117
|
-
function createInputSystem(options: InputOptions, isPageOwner: boolean): InputSystem {
|
|
118
|
-
const system = new InputSystem(createInputHost(options), { isPageOwner, claimPage: registerAsPageInput });
|
|
119
|
-
system.setLegacyControls(injectedField('controls') ?? null);
|
|
120
|
-
return system;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Persistence for remaps: localStorage under `hub:<slug>:input`, mirrored to the cloud when given. */
|
|
124
|
-
function createInputHost(options: InputOptions): InputHost {
|
|
125
|
-
const slug = options.slug === undefined ? injectedSlug() : options.slug;
|
|
126
|
-
const storageKey = `hub:${slug ?? 'null'}:input`;
|
|
127
|
-
return {
|
|
128
|
-
slug,
|
|
129
|
-
loadOverrides() {
|
|
130
|
-
try {
|
|
131
|
-
const saved = localStorage.getItem(storageKey);
|
|
132
|
-
if (saved === null) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: JSON from localStorage, parsed straight away
|
|
136
|
-
const parsed: unknown = JSON.parse(saved);
|
|
137
|
-
return parseInputOverrides(parsed);
|
|
138
|
-
} catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
saveOverrides(overrides) {
|
|
143
|
-
try {
|
|
144
|
-
localStorage.setItem(storageKey, JSON.stringify(overrides));
|
|
145
|
-
} catch {
|
|
146
|
-
// Storage full or blocked: the cloud copy still saves.
|
|
147
|
-
}
|
|
148
|
-
if (slug !== null) {
|
|
149
|
-
options.saveToCloud?.(overrides);
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Make `system` the page's registered input system; returns the one it replaces. */
|
|
156
|
-
function registerAsPageInput(system: InputSystem): PageInputControl | null {
|
|
157
|
-
const registry = window.__DG__ ?? {};
|
|
158
|
-
window.__DG__ = registry;
|
|
159
|
-
const previous = registry.input?.instance ?? window[LEGACY_GLOBAL_NAMES.input];
|
|
160
|
-
registry.input = { version: HUB_CLIENT_VERSION, api: INPUT_SINGLETON_API, instance: system };
|
|
161
|
-
window[LEGACY_GLOBAL_NAMES.input] = system;
|
|
162
|
-
return previous !== system && isPageInputControl(previous) ? previous : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Whether another bundle registered an input system with a different shape number. */
|
|
166
|
-
function hasMismatchedPageInput(): boolean {
|
|
167
|
-
const registered = typeof window === 'undefined' ? undefined : window.__DG__?.input;
|
|
168
|
-
return registered !== undefined && registered.api !== INPUT_SINGLETON_API && isInputSystem(registered.instance);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let claimedInput: InputSystem | null = null;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* The page-wide input system, created on first use. `options` connect it to
|
|
175
|
-
* the core client (its slug, the cloud save); the first call's options win.
|
|
176
|
-
* When another client copy with a different shape owns the page, this copy
|
|
177
|
-
* gets a private instance that takes the page over once its game defines
|
|
178
|
-
* inputs.
|
|
179
|
-
*/
|
|
180
|
-
export function getInput(options: InputOptions = {}): InputSystem {
|
|
181
|
-
if (claimedInput !== null) {
|
|
182
|
-
return claimedInput;
|
|
183
|
-
}
|
|
184
|
-
const isPrivate = hasMismatchedPageInput();
|
|
185
|
-
claimedInput = claimSingleton({
|
|
186
|
-
name: 'input',
|
|
187
|
-
version: HUB_CLIENT_VERSION,
|
|
188
|
-
api: INPUT_SINGLETON_API,
|
|
189
|
-
create: () => createInputSystem(options, !isPrivate),
|
|
190
|
-
isInstance: isInputSystem,
|
|
191
|
-
});
|
|
192
|
-
return claimedInput;
|
|
193
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The page's one input system (plan §4.4). The game's copy of the client owns
|
|
3
|
+
* the page because the game defines the controls; the host menu reaches it
|
|
4
|
+
* through the stable {@link PageInputControl} slice. Remaps persist to
|
|
5
|
+
* localStorage (offline-safe) and, through the core client, to a reserved
|
|
6
|
+
* cloud save slot.
|
|
7
|
+
*/
|
|
8
|
+
import { HUB_CLIENT_VERSION } from '../core/version/client-version.gen';
|
|
9
|
+
import { claimSingleton, LEGACY_GLOBAL_NAMES } from '../globals';
|
|
10
|
+
import { hasMethods, isObject, readField } from '../realtime/cross-bundle';
|
|
11
|
+
import { InputSystem } from './input-system';
|
|
12
|
+
import { isPageInputControl } from './page-control';
|
|
13
|
+
import type { PageInputControl } from './page-control';
|
|
14
|
+
import type { InputDef, InputHost, InputOverrides, VirtualOverride } from './types';
|
|
15
|
+
|
|
16
|
+
/** The cross-bundle shape number of the `input` singleton. Bump only on an incompatible change. */
|
|
17
|
+
export const INPUT_SINGLETON_API = 1;
|
|
18
|
+
|
|
19
|
+
/** The public methods of every input system, v1 copies included. */
|
|
20
|
+
const INPUT_SYSTEM_METHODS = [
|
|
21
|
+
'define', 'enable', 'disable', 'isEnabled', 'value', 'get', 'down', 'up', 'pressed', 'axis', 'vector', 'on',
|
|
22
|
+
'setVirtualHtml', 'showVirtual', 'setInsets', 'autoNavigate', 'clearAutoNavigate', 'attachMenu', 'setLegacyControls',
|
|
23
|
+
'openControls', 'rebind', 'setVirtual',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
/** Whether `candidate` is some bundle's input system (v1 copies included). */
|
|
27
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
|
28
|
+
export function isInputSystem(candidate: unknown): candidate is InputSystem {
|
|
29
|
+
return hasMethods(candidate, INPUT_SYSTEM_METHODS);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** How the core client connects the input system to its game. */
|
|
33
|
+
export interface InputOptions {
|
|
34
|
+
/** This page's game slug (default: `window.__HUB__.slug`). */
|
|
35
|
+
slug?: string | null;
|
|
36
|
+
/** Mirror remaps to the cloud (the core client writes the `__controls` save slot). */
|
|
37
|
+
saveToCloud?: (overrides: InputOverrides) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A finite number, or `undefined`. */
|
|
41
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
42
|
+
function finiteNumber(candidate: unknown): number | undefined {
|
|
43
|
+
return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A saved rebind: the keys or gamepad button the player chose. */
|
|
47
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
48
|
+
function parseBinding(saved: unknown): Partial<InputDef> {
|
|
49
|
+
const binding: Partial<InputDef> = {};
|
|
50
|
+
const keys = readField(saved, 'keys');
|
|
51
|
+
if (Array.isArray(keys) && keys.every((key) => typeof key === 'string')) {
|
|
52
|
+
binding.keys = keys.filter((key): key is string => typeof key === 'string');
|
|
53
|
+
}
|
|
54
|
+
const button = finiteNumber(readField(readField(saved, 'gamepad'), 'button'));
|
|
55
|
+
if (button !== undefined) {
|
|
56
|
+
binding.gamepad = { button };
|
|
57
|
+
}
|
|
58
|
+
return binding;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A saved dragged spot. */
|
|
62
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
63
|
+
function parseVirtualOverride(saved: unknown): VirtualOverride {
|
|
64
|
+
const spot: VirtualOverride = {};
|
|
65
|
+
const left = finiteNumber(readField(saved, 'x'));
|
|
66
|
+
const top = finiteNumber(readField(saved, 'y'));
|
|
67
|
+
if (left !== undefined) {
|
|
68
|
+
spot.x = left;
|
|
69
|
+
}
|
|
70
|
+
if (top !== undefined) {
|
|
71
|
+
spot.y = top;
|
|
72
|
+
}
|
|
73
|
+
return spot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Entries of a saved record, parsed. */
|
|
77
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: saved JSON
|
|
78
|
+
function parseRecord<Parsed>(saved: unknown, parseEntry: (entry: unknown) => Parsed): Record<string, Parsed> | undefined {
|
|
79
|
+
if (!isObject(saved)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return Object.fromEntries(Object.entries(saved).map(([key, entry]) => [key, parseEntry(entry)]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Saved overrides from JSON, keeping only well-formed fields. */
|
|
86
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: JSON read back from localStorage
|
|
87
|
+
export function parseInputOverrides(saved: unknown): InputOverrides {
|
|
88
|
+
const overrides: InputOverrides = {};
|
|
89
|
+
const bindings = parseRecord(readField(saved, 'bindings'), parseBinding);
|
|
90
|
+
const virtual = parseRecord(readField(saved, 'virtual'), parseVirtualOverride);
|
|
91
|
+
const showVirtual = readField(saved, 'showVirtual');
|
|
92
|
+
if (bindings !== undefined) {
|
|
93
|
+
overrides.bindings = bindings;
|
|
94
|
+
}
|
|
95
|
+
if (virtual !== undefined) {
|
|
96
|
+
overrides.virtual = virtual;
|
|
97
|
+
}
|
|
98
|
+
if (typeof showVirtual === 'boolean') {
|
|
99
|
+
overrides.showVirtual = showVirtual;
|
|
100
|
+
}
|
|
101
|
+
return overrides;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A field of the host's injected `window.__HUB__`. */
|
|
105
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the host page's injected config, checked by the caller
|
|
106
|
+
function injectedField(key: string): unknown {
|
|
107
|
+
return typeof window === 'undefined' ? undefined : readField(readField(window, '__HUB__'), key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The slug the host injected, or `null`. */
|
|
111
|
+
function injectedSlug(): string | null {
|
|
112
|
+
const slug = injectedField('slug');
|
|
113
|
+
return typeof slug === 'string' ? slug : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A new input system, starting from the host's injected `game.controls` (the menu may pass them again later). */
|
|
117
|
+
function createInputSystem(options: InputOptions, isPageOwner: boolean): InputSystem {
|
|
118
|
+
const system = new InputSystem(createInputHost(options), { isPageOwner, claimPage: registerAsPageInput });
|
|
119
|
+
system.setLegacyControls(injectedField('controls') ?? null);
|
|
120
|
+
return system;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Persistence for remaps: localStorage under `hub:<slug>:input`, mirrored to the cloud when given. */
|
|
124
|
+
function createInputHost(options: InputOptions): InputHost {
|
|
125
|
+
const slug = options.slug === undefined ? injectedSlug() : options.slug;
|
|
126
|
+
const storageKey = `hub:${slug ?? 'null'}:input`;
|
|
127
|
+
return {
|
|
128
|
+
slug,
|
|
129
|
+
loadOverrides() {
|
|
130
|
+
try {
|
|
131
|
+
const saved = localStorage.getItem(storageKey);
|
|
132
|
+
if (saved === null) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: JSON from localStorage, parsed straight away
|
|
136
|
+
const parsed: unknown = JSON.parse(saved);
|
|
137
|
+
return parseInputOverrides(parsed);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
saveOverrides(overrides) {
|
|
143
|
+
try {
|
|
144
|
+
localStorage.setItem(storageKey, JSON.stringify(overrides));
|
|
145
|
+
} catch {
|
|
146
|
+
// Storage full or blocked: the cloud copy still saves.
|
|
147
|
+
}
|
|
148
|
+
if (slug !== null) {
|
|
149
|
+
options.saveToCloud?.(overrides);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Make `system` the page's registered input system; returns the one it replaces. */
|
|
156
|
+
function registerAsPageInput(system: InputSystem): PageInputControl | null {
|
|
157
|
+
const registry = window.__DG__ ?? {};
|
|
158
|
+
window.__DG__ = registry;
|
|
159
|
+
const previous = registry.input?.instance ?? window[LEGACY_GLOBAL_NAMES.input];
|
|
160
|
+
registry.input = { version: HUB_CLIENT_VERSION, api: INPUT_SINGLETON_API, instance: system };
|
|
161
|
+
window[LEGACY_GLOBAL_NAMES.input] = system;
|
|
162
|
+
return previous !== system && isPageInputControl(previous) ? previous : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Whether another bundle registered an input system with a different shape number. */
|
|
166
|
+
function hasMismatchedPageInput(): boolean {
|
|
167
|
+
const registered = typeof window === 'undefined' ? undefined : window.__DG__?.input;
|
|
168
|
+
return registered !== undefined && registered.api !== INPUT_SINGLETON_API && isInputSystem(registered.instance);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let claimedInput: InputSystem | null = null;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The page-wide input system, created on first use. `options` connect it to
|
|
175
|
+
* the core client (its slug, the cloud save); the first call's options win.
|
|
176
|
+
* When another client copy with a different shape owns the page, this copy
|
|
177
|
+
* gets a private instance that takes the page over once its game defines
|
|
178
|
+
* inputs.
|
|
179
|
+
*/
|
|
180
|
+
export function getInput(options: InputOptions = {}): InputSystem {
|
|
181
|
+
if (claimedInput !== null) {
|
|
182
|
+
return claimedInput;
|
|
183
|
+
}
|
|
184
|
+
const isPrivate = hasMismatchedPageInput();
|
|
185
|
+
claimedInput = claimSingleton({
|
|
186
|
+
name: 'input',
|
|
187
|
+
version: HUB_CLIENT_VERSION,
|
|
188
|
+
api: INPUT_SINGLETON_API,
|
|
189
|
+
create: () => createInputSystem(options, !isPrivate),
|
|
190
|
+
isInstance: isInputSystem,
|
|
191
|
+
});
|
|
192
|
+
return claimedInput;
|
|
193
|
+
}
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The player's remaps and on-screen control customisations for one game,
|
|
3
|
-
* persisted through the input host after every change.
|
|
4
|
-
*/
|
|
5
|
-
import type { InputDef, InputHost, InputOverrides, VirtualOverride } from './types';
|
|
6
|
-
|
|
7
|
-
/** A readable label of an input's binding (`w/ArrowUp · Pad 0`). */
|
|
8
|
-
export function describeBinding(definition: InputDef | undefined): string {
|
|
9
|
-
const parts: string[] = [];
|
|
10
|
-
if (definition?.keys !== undefined && definition.keys.length > 0) {
|
|
11
|
-
parts.push(definition.keys.join('/'));
|
|
12
|
-
}
|
|
13
|
-
if (definition?.gamepad?.button !== undefined) {
|
|
14
|
-
parts.push(`Pad ${String(definition.gamepad.button)}`);
|
|
15
|
-
}
|
|
16
|
-
if (definition?.gamepad?.axis !== undefined) {
|
|
17
|
-
parts.push(`Axis ${String(definition.gamepad.axis[0])}${definition.gamepad.axis[1]}`);
|
|
18
|
-
}
|
|
19
|
-
return parts.length > 0 ? parts.join(' · ') : '—';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The player's overrides, saved on every change. */
|
|
23
|
-
export class PlayerRemaps {
|
|
24
|
-
private readonly host: InputHost;
|
|
25
|
-
private saved: InputOverrides;
|
|
26
|
-
|
|
27
|
-
constructor(host: InputHost) {
|
|
28
|
-
this.host = host;
|
|
29
|
-
this.saved = host.loadOverrides() ?? {};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The overrides as they stand. */
|
|
33
|
-
get overrides(): InputOverrides {
|
|
34
|
-
return this.saved;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A declared input with the player's rebind applied. */
|
|
38
|
-
withBinding(name: string, declared: InputDef): InputDef {
|
|
39
|
-
return { ...declared, ...this.saved.bindings?.[name] };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Rebind an input. */
|
|
43
|
-
rebind(inputName: string, patch: Partial<InputDef>): void {
|
|
44
|
-
const bindings = this.saved.bindings ?? {};
|
|
45
|
-
bindings[inputName] = { ...bindings[inputName], ...patch };
|
|
46
|
-
this.saved.bindings = bindings;
|
|
47
|
-
this.persist();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Move or restyle an on-screen control. */
|
|
51
|
-
customiseControl(id: string, patch: VirtualOverride): void {
|
|
52
|
-
const virtual = this.saved.virtual ?? {};
|
|
53
|
-
virtual[id] = { ...virtual[id], ...patch };
|
|
54
|
-
this.saved.virtual = virtual;
|
|
55
|
-
this.persist();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Record the player's show/hide choice. */
|
|
59
|
-
setShowVirtual(isVisible: boolean): void {
|
|
60
|
-
this.saved.showVirtual = isVisible;
|
|
61
|
-
this.persist();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Forget everything. */
|
|
65
|
-
reset(): void {
|
|
66
|
-
this.saved = {};
|
|
67
|
-
this.persist();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private persist(): void {
|
|
71
|
-
try {
|
|
72
|
-
this.host.saveOverrides(this.saved);
|
|
73
|
-
} catch {
|
|
74
|
-
// Storage can be full or blocked; remaps still apply for this visit.
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The player's remaps and on-screen control customisations for one game,
|
|
3
|
+
* persisted through the input host after every change.
|
|
4
|
+
*/
|
|
5
|
+
import type { InputDef, InputHost, InputOverrides, VirtualOverride } from './types';
|
|
6
|
+
|
|
7
|
+
/** A readable label of an input's binding (`w/ArrowUp · Pad 0`). */
|
|
8
|
+
export function describeBinding(definition: InputDef | undefined): string {
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
if (definition?.keys !== undefined && definition.keys.length > 0) {
|
|
11
|
+
parts.push(definition.keys.join('/'));
|
|
12
|
+
}
|
|
13
|
+
if (definition?.gamepad?.button !== undefined) {
|
|
14
|
+
parts.push(`Pad ${String(definition.gamepad.button)}`);
|
|
15
|
+
}
|
|
16
|
+
if (definition?.gamepad?.axis !== undefined) {
|
|
17
|
+
parts.push(`Axis ${String(definition.gamepad.axis[0])}${definition.gamepad.axis[1]}`);
|
|
18
|
+
}
|
|
19
|
+
return parts.length > 0 ? parts.join(' · ') : '—';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The player's overrides, saved on every change. */
|
|
23
|
+
export class PlayerRemaps {
|
|
24
|
+
private readonly host: InputHost;
|
|
25
|
+
private saved: InputOverrides;
|
|
26
|
+
|
|
27
|
+
constructor(host: InputHost) {
|
|
28
|
+
this.host = host;
|
|
29
|
+
this.saved = host.loadOverrides() ?? {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The overrides as they stand. */
|
|
33
|
+
get overrides(): InputOverrides {
|
|
34
|
+
return this.saved;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A declared input with the player's rebind applied. */
|
|
38
|
+
withBinding(name: string, declared: InputDef): InputDef {
|
|
39
|
+
return { ...declared, ...this.saved.bindings?.[name] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Rebind an input. */
|
|
43
|
+
rebind(inputName: string, patch: Partial<InputDef>): void {
|
|
44
|
+
const bindings = this.saved.bindings ?? {};
|
|
45
|
+
bindings[inputName] = { ...bindings[inputName], ...patch };
|
|
46
|
+
this.saved.bindings = bindings;
|
|
47
|
+
this.persist();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Move or restyle an on-screen control. */
|
|
51
|
+
customiseControl(id: string, patch: VirtualOverride): void {
|
|
52
|
+
const virtual = this.saved.virtual ?? {};
|
|
53
|
+
virtual[id] = { ...virtual[id], ...patch };
|
|
54
|
+
this.saved.virtual = virtual;
|
|
55
|
+
this.persist();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Record the player's show/hide choice. */
|
|
59
|
+
setShowVirtual(isVisible: boolean): void {
|
|
60
|
+
this.saved.showVirtual = isVisible;
|
|
61
|
+
this.persist();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Forget everything. */
|
|
65
|
+
reset(): void {
|
|
66
|
+
this.saved = {};
|
|
67
|
+
this.persist();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private persist(): void {
|
|
71
|
+
try {
|
|
72
|
+
this.host.saveOverrides(this.saved);
|
|
73
|
+
} catch {
|
|
74
|
+
// Storage can be full or blocked; remaps still apply for this visit.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|