@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,163 +1,163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Controls screen: rebind inputs, show or hide the on-screen controls,
|
|
3
|
-
* edit the touch layout, reset to defaults.
|
|
4
|
-
*/
|
|
5
|
-
import { readGamepads } from '../input/gamepad';
|
|
6
|
-
import type { InputConfig, InputDef } from '../input/types';
|
|
7
|
-
import { escapeHtml, findElements, requireElement } from './dom';
|
|
8
|
-
|
|
9
|
-
/** What the Controls screen drives (the input system). */
|
|
10
|
-
export interface ControlsPanelHost {
|
|
11
|
-
/** The player's show/hide state of the on-screen controls. */
|
|
12
|
-
readonly showingVirtual: boolean;
|
|
13
|
-
/** Show or hide the on-screen controls (persisted). */
|
|
14
|
-
showVirtual(isVisible: boolean): void;
|
|
15
|
-
/** Force the controls visible while editing the layout. */
|
|
16
|
-
setVirtualEditing(isEditing: boolean): void;
|
|
17
|
-
/** Rebind an input (persisted). */
|
|
18
|
-
rebind(inputName: string, patch: Partial<InputDef>): void;
|
|
19
|
-
/** Forget every remap. */
|
|
20
|
-
resetControls(): void;
|
|
21
|
-
/** A readable label of an input's current binding. */
|
|
22
|
-
bindingLabel(inputName: string): string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** How long a rebind waits for a key or button. */
|
|
26
|
-
const REBIND_TIMEOUT_MS = 8000;
|
|
27
|
-
|
|
28
|
-
/** The Controls screen markup for `config`. */
|
|
29
|
-
function panelHtml(config: InputConfig, host: ControlsPanelHost): string {
|
|
30
|
-
let rows = '';
|
|
31
|
-
for (const [group, groupDefinition] of Object.entries(config.groups)) {
|
|
32
|
-
rows += `<h4>${escapeHtml(group)}</h4>`;
|
|
33
|
-
for (const name of Object.keys(groupDefinition.inputs)) {
|
|
34
|
-
const escapedName = escapeHtml(name);
|
|
35
|
-
rows += `<div class="cr"><span>${escapedName}</span><b class="bind" data-i="${escapedName}">${escapeHtml(host.bindingLabel(name))}</b><button class="rb" data-i="${escapedName}">Rebind</button></div>`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return `
|
|
39
|
-
<div class="panel-card">
|
|
40
|
-
<h3>Controls</h3>
|
|
41
|
-
<label class="chk"><input type="checkbox" class="showv"> Show on-screen controls</label>
|
|
42
|
-
<button class="btn editlayout">Edit touch layout</button>
|
|
43
|
-
<div class="crs">${rows}</div>
|
|
44
|
-
<div class="panel-actions">
|
|
45
|
-
<button class="btn reset">Reset to defaults</button>
|
|
46
|
-
<button class="btn done">Done</button>
|
|
47
|
-
</div>
|
|
48
|
-
<p class="hint"></p>
|
|
49
|
-
<div class="edit-bar"><span>Drag a control to move it</span><button class="btn editdone">Done</button></div>
|
|
50
|
-
</div>`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The Controls screen (one at a time). */
|
|
54
|
-
export class ControlsPanel {
|
|
55
|
-
private readonly root: ShadowRoot;
|
|
56
|
-
private readonly shadowHost: HTMLElement;
|
|
57
|
-
private panel: HTMLDivElement | null = null;
|
|
58
|
-
private isEditingLayout = false;
|
|
59
|
-
|
|
60
|
-
constructor(root: ShadowRoot, shadowHost: HTMLElement) {
|
|
61
|
-
this.root = root;
|
|
62
|
-
this.shadowHost = shadowHost;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Whether the player is dragging controls around. */
|
|
66
|
-
get isEditing(): boolean {
|
|
67
|
-
return this.isEditingLayout;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Open the screen (or close it when already open). */
|
|
71
|
-
toggle(config: InputConfig | null, host: ControlsPanelHost): void {
|
|
72
|
-
if (config === null) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (this.panel !== null) {
|
|
76
|
-
this.close();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const panel = document.createElement('div');
|
|
80
|
-
panel.className = 'panel';
|
|
81
|
-
panel.innerHTML = panelHtml(config, host);
|
|
82
|
-
this.root.appendChild(panel);
|
|
83
|
-
this.panel = panel;
|
|
84
|
-
const showToggle = requireElement(panel, '.showv', HTMLInputElement);
|
|
85
|
-
showToggle.checked = host.showingVirtual;
|
|
86
|
-
showToggle.addEventListener('change', () => {
|
|
87
|
-
host.showVirtual(showToggle.checked);
|
|
88
|
-
});
|
|
89
|
-
requireElement(panel, '.editlayout', HTMLButtonElement).addEventListener('click', () => {
|
|
90
|
-
this.setEditing(!this.isEditingLayout, host);
|
|
91
|
-
});
|
|
92
|
-
requireElement(panel, '.reset', HTMLButtonElement).addEventListener('click', () => {
|
|
93
|
-
host.resetControls();
|
|
94
|
-
this.close();
|
|
95
|
-
});
|
|
96
|
-
requireElement(panel, '.done', HTMLButtonElement).addEventListener('click', () => {
|
|
97
|
-
this.setEditing(false, host);
|
|
98
|
-
this.close();
|
|
99
|
-
});
|
|
100
|
-
requireElement(panel, '.editdone', HTMLButtonElement).addEventListener('click', () => {
|
|
101
|
-
this.setEditing(false, host);
|
|
102
|
-
});
|
|
103
|
-
for (const button of findElements(panel, '.rb', HTMLButtonElement)) {
|
|
104
|
-
button.addEventListener('click', () => {
|
|
105
|
-
this.captureRebind(button.dataset['i'] ?? '', host, panel);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private close(): void {
|
|
111
|
-
this.panel?.remove();
|
|
112
|
-
this.panel = null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private setEditing(isEditing: boolean, host: ControlsPanelHost): void {
|
|
116
|
-
this.isEditingLayout = isEditing;
|
|
117
|
-
this.shadowHost.classList.toggle('editing', isEditing);
|
|
118
|
-
// Forces the controls visible without touching the player's show/hide choice.
|
|
119
|
-
host.setVirtualEditing(isEditing);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Wait for the next key or gamepad button and bind `inputName` to it. */
|
|
123
|
-
private captureRebind(inputName: string, host: ControlsPanelHost, panel: HTMLElement): void {
|
|
124
|
-
const hint = requireElement(panel, '.hint', HTMLElement);
|
|
125
|
-
hint.textContent = `Press a key or gamepad button for "${inputName}"…`;
|
|
126
|
-
let frameRequest = 0;
|
|
127
|
-
const startedAt = performance.now();
|
|
128
|
-
const cleanUp = (): void => {
|
|
129
|
-
window.removeEventListener('keydown', onKey, true);
|
|
130
|
-
cancelAnimationFrame(frameRequest);
|
|
131
|
-
};
|
|
132
|
-
const finish = (patch: Partial<InputDef>, label: string): void => {
|
|
133
|
-
host.rebind(inputName, patch);
|
|
134
|
-
const binding = findElements(panel, '.bind', HTMLElement).find((element) => element.dataset['i'] === inputName);
|
|
135
|
-
if (binding !== undefined) {
|
|
136
|
-
binding.textContent = label;
|
|
137
|
-
}
|
|
138
|
-
hint.textContent = '';
|
|
139
|
-
cleanUp();
|
|
140
|
-
};
|
|
141
|
-
function onKey(event: KeyboardEvent): void {
|
|
142
|
-
event.preventDefault();
|
|
143
|
-
finish({ keys: [event.key] }, event.key);
|
|
144
|
-
}
|
|
145
|
-
const pollPads = (): void => {
|
|
146
|
-
for (const pad of readGamepads()) {
|
|
147
|
-
const pressed = pad.buttons.findIndex((button) => button.pressed);
|
|
148
|
-
if (pressed >= 0) {
|
|
149
|
-
finish({ gamepad: { button: pressed } }, `Pad ${String(pressed)}`);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (performance.now() - startedAt > REBIND_TIMEOUT_MS) {
|
|
154
|
-
hint.textContent = '';
|
|
155
|
-
cleanUp();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
frameRequest = requestAnimationFrame(pollPads);
|
|
159
|
-
};
|
|
160
|
-
window.addEventListener('keydown', onKey, true);
|
|
161
|
-
frameRequest = requestAnimationFrame(pollPads);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The Controls screen: rebind inputs, show or hide the on-screen controls,
|
|
3
|
+
* edit the touch layout, reset to defaults.
|
|
4
|
+
*/
|
|
5
|
+
import { readGamepads } from '../input/gamepad';
|
|
6
|
+
import type { InputConfig, InputDef } from '../input/types';
|
|
7
|
+
import { escapeHtml, findElements, requireElement } from './dom';
|
|
8
|
+
|
|
9
|
+
/** What the Controls screen drives (the input system). */
|
|
10
|
+
export interface ControlsPanelHost {
|
|
11
|
+
/** The player's show/hide state of the on-screen controls. */
|
|
12
|
+
readonly showingVirtual: boolean;
|
|
13
|
+
/** Show or hide the on-screen controls (persisted). */
|
|
14
|
+
showVirtual(isVisible: boolean): void;
|
|
15
|
+
/** Force the controls visible while editing the layout. */
|
|
16
|
+
setVirtualEditing(isEditing: boolean): void;
|
|
17
|
+
/** Rebind an input (persisted). */
|
|
18
|
+
rebind(inputName: string, patch: Partial<InputDef>): void;
|
|
19
|
+
/** Forget every remap. */
|
|
20
|
+
resetControls(): void;
|
|
21
|
+
/** A readable label of an input's current binding. */
|
|
22
|
+
bindingLabel(inputName: string): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** How long a rebind waits for a key or button. */
|
|
26
|
+
const REBIND_TIMEOUT_MS = 8000;
|
|
27
|
+
|
|
28
|
+
/** The Controls screen markup for `config`. */
|
|
29
|
+
function panelHtml(config: InputConfig, host: ControlsPanelHost): string {
|
|
30
|
+
let rows = '';
|
|
31
|
+
for (const [group, groupDefinition] of Object.entries(config.groups)) {
|
|
32
|
+
rows += `<h4>${escapeHtml(group)}</h4>`;
|
|
33
|
+
for (const name of Object.keys(groupDefinition.inputs)) {
|
|
34
|
+
const escapedName = escapeHtml(name);
|
|
35
|
+
rows += `<div class="cr"><span>${escapedName}</span><b class="bind" data-i="${escapedName}">${escapeHtml(host.bindingLabel(name))}</b><button class="rb" data-i="${escapedName}">Rebind</button></div>`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return `
|
|
39
|
+
<div class="panel-card">
|
|
40
|
+
<h3>Controls</h3>
|
|
41
|
+
<label class="chk"><input type="checkbox" class="showv"> Show on-screen controls</label>
|
|
42
|
+
<button class="btn editlayout">Edit touch layout</button>
|
|
43
|
+
<div class="crs">${rows}</div>
|
|
44
|
+
<div class="panel-actions">
|
|
45
|
+
<button class="btn reset">Reset to defaults</button>
|
|
46
|
+
<button class="btn done">Done</button>
|
|
47
|
+
</div>
|
|
48
|
+
<p class="hint"></p>
|
|
49
|
+
<div class="edit-bar"><span>Drag a control to move it</span><button class="btn editdone">Done</button></div>
|
|
50
|
+
</div>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The Controls screen (one at a time). */
|
|
54
|
+
export class ControlsPanel {
|
|
55
|
+
private readonly root: ShadowRoot;
|
|
56
|
+
private readonly shadowHost: HTMLElement;
|
|
57
|
+
private panel: HTMLDivElement | null = null;
|
|
58
|
+
private isEditingLayout = false;
|
|
59
|
+
|
|
60
|
+
constructor(root: ShadowRoot, shadowHost: HTMLElement) {
|
|
61
|
+
this.root = root;
|
|
62
|
+
this.shadowHost = shadowHost;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether the player is dragging controls around. */
|
|
66
|
+
get isEditing(): boolean {
|
|
67
|
+
return this.isEditingLayout;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Open the screen (or close it when already open). */
|
|
71
|
+
toggle(config: InputConfig | null, host: ControlsPanelHost): void {
|
|
72
|
+
if (config === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (this.panel !== null) {
|
|
76
|
+
this.close();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const panel = document.createElement('div');
|
|
80
|
+
panel.className = 'panel';
|
|
81
|
+
panel.innerHTML = panelHtml(config, host);
|
|
82
|
+
this.root.appendChild(panel);
|
|
83
|
+
this.panel = panel;
|
|
84
|
+
const showToggle = requireElement(panel, '.showv', HTMLInputElement);
|
|
85
|
+
showToggle.checked = host.showingVirtual;
|
|
86
|
+
showToggle.addEventListener('change', () => {
|
|
87
|
+
host.showVirtual(showToggle.checked);
|
|
88
|
+
});
|
|
89
|
+
requireElement(panel, '.editlayout', HTMLButtonElement).addEventListener('click', () => {
|
|
90
|
+
this.setEditing(!this.isEditingLayout, host);
|
|
91
|
+
});
|
|
92
|
+
requireElement(panel, '.reset', HTMLButtonElement).addEventListener('click', () => {
|
|
93
|
+
host.resetControls();
|
|
94
|
+
this.close();
|
|
95
|
+
});
|
|
96
|
+
requireElement(panel, '.done', HTMLButtonElement).addEventListener('click', () => {
|
|
97
|
+
this.setEditing(false, host);
|
|
98
|
+
this.close();
|
|
99
|
+
});
|
|
100
|
+
requireElement(panel, '.editdone', HTMLButtonElement).addEventListener('click', () => {
|
|
101
|
+
this.setEditing(false, host);
|
|
102
|
+
});
|
|
103
|
+
for (const button of findElements(panel, '.rb', HTMLButtonElement)) {
|
|
104
|
+
button.addEventListener('click', () => {
|
|
105
|
+
this.captureRebind(button.dataset['i'] ?? '', host, panel);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private close(): void {
|
|
111
|
+
this.panel?.remove();
|
|
112
|
+
this.panel = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private setEditing(isEditing: boolean, host: ControlsPanelHost): void {
|
|
116
|
+
this.isEditingLayout = isEditing;
|
|
117
|
+
this.shadowHost.classList.toggle('editing', isEditing);
|
|
118
|
+
// Forces the controls visible without touching the player's show/hide choice.
|
|
119
|
+
host.setVirtualEditing(isEditing);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Wait for the next key or gamepad button and bind `inputName` to it. */
|
|
123
|
+
private captureRebind(inputName: string, host: ControlsPanelHost, panel: HTMLElement): void {
|
|
124
|
+
const hint = requireElement(panel, '.hint', HTMLElement);
|
|
125
|
+
hint.textContent = `Press a key or gamepad button for "${inputName}"…`;
|
|
126
|
+
let frameRequest = 0;
|
|
127
|
+
const startedAt = performance.now();
|
|
128
|
+
const cleanUp = (): void => {
|
|
129
|
+
window.removeEventListener('keydown', onKey, true);
|
|
130
|
+
cancelAnimationFrame(frameRequest);
|
|
131
|
+
};
|
|
132
|
+
const finish = (patch: Partial<InputDef>, label: string): void => {
|
|
133
|
+
host.rebind(inputName, patch);
|
|
134
|
+
const binding = findElements(panel, '.bind', HTMLElement).find((element) => element.dataset['i'] === inputName);
|
|
135
|
+
if (binding !== undefined) {
|
|
136
|
+
binding.textContent = label;
|
|
137
|
+
}
|
|
138
|
+
hint.textContent = '';
|
|
139
|
+
cleanUp();
|
|
140
|
+
};
|
|
141
|
+
function onKey(event: KeyboardEvent): void {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
finish({ keys: [event.key] }, event.key);
|
|
144
|
+
}
|
|
145
|
+
const pollPads = (): void => {
|
|
146
|
+
for (const pad of readGamepads()) {
|
|
147
|
+
const pressed = pad.buttons.findIndex((button) => button.pressed);
|
|
148
|
+
if (pressed >= 0) {
|
|
149
|
+
finish({ gamepad: { button: pressed } }, `Pad ${String(pressed)}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (performance.now() - startedAt > REBIND_TIMEOUT_MS) {
|
|
154
|
+
hint.textContent = '';
|
|
155
|
+
cleanUp();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
frameRequest = requestAnimationFrame(pollPads);
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener('keydown', onKey, true);
|
|
161
|
+
frameRequest = requestAnimationFrame(pollPads);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/overlay/dom.ts
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed DOM helpers for the overlay: element lookups that check the element
|
|
3
|
-
* class instead of asserting it, and small escapes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** The first element under `root` matching `selector` that is an instance of `elementClass`; throws if missing. */
|
|
7
|
-
export function requireElement<ElementType extends Element>(root: ParentNode, selector: string, elementClass: new () => ElementType): ElementType {
|
|
8
|
-
const found = root.querySelector(selector);
|
|
9
|
-
if (!(found instanceof elementClass)) {
|
|
10
|
-
throw new Error(`[hub] overlay element ${selector} is missing`);
|
|
11
|
-
}
|
|
12
|
-
return found;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Every element under `root` matching `selector` that is an instance of `elementClass`. */
|
|
16
|
-
export function findElements<ElementType extends Element>(root: ParentNode, selector: string, elementClass: new () => ElementType): ElementType[] {
|
|
17
|
-
return [...root.querySelectorAll(selector)].filter((found): found is ElementType => found instanceof elementClass);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** `document.body`, which is `null` while the page is still parsing its head (whatever lib.dom says). */
|
|
21
|
-
export function pageBody(): HTMLElement | null {
|
|
22
|
-
return document.body;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const HTML_ESCAPES: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
26
|
-
|
|
27
|
-
/** Escape text for HTML. */
|
|
28
|
-
export function escapeHtml(text: string): string {
|
|
29
|
-
return text.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character] ?? character);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Clear an element's position styles. */
|
|
33
|
-
export function clearPosition(element: HTMLElement): void {
|
|
34
|
-
element.style.left = '';
|
|
35
|
-
element.style.right = '';
|
|
36
|
-
element.style.top = '';
|
|
37
|
-
element.style.bottom = '';
|
|
38
|
-
element.style.transform = '';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Position an element's top-left corner in viewport px. */
|
|
42
|
-
export function placeAt(element: HTMLElement, left: number, top: number): void {
|
|
43
|
-
clearPosition(element);
|
|
44
|
-
element.style.left = `${String(left)}px`;
|
|
45
|
-
element.style.top = `${String(top)}px`;
|
|
46
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Typed DOM helpers for the overlay: element lookups that check the element
|
|
3
|
+
* class instead of asserting it, and small escapes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The first element under `root` matching `selector` that is an instance of `elementClass`; throws if missing. */
|
|
7
|
+
export function requireElement<ElementType extends Element>(root: ParentNode, selector: string, elementClass: new () => ElementType): ElementType {
|
|
8
|
+
const found = root.querySelector(selector);
|
|
9
|
+
if (!(found instanceof elementClass)) {
|
|
10
|
+
throw new Error(`[hub] overlay element ${selector} is missing`);
|
|
11
|
+
}
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Every element under `root` matching `selector` that is an instance of `elementClass`. */
|
|
16
|
+
export function findElements<ElementType extends Element>(root: ParentNode, selector: string, elementClass: new () => ElementType): ElementType[] {
|
|
17
|
+
return [...root.querySelectorAll(selector)].filter((found): found is ElementType => found instanceof elementClass);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `document.body`, which is `null` while the page is still parsing its head (whatever lib.dom says). */
|
|
21
|
+
export function pageBody(): HTMLElement | null {
|
|
22
|
+
return document.body;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const HTML_ESCAPES: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
26
|
+
|
|
27
|
+
/** Escape text for HTML. */
|
|
28
|
+
export function escapeHtml(text: string): string {
|
|
29
|
+
return text.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character] ?? character);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Clear an element's position styles. */
|
|
33
|
+
export function clearPosition(element: HTMLElement): void {
|
|
34
|
+
element.style.left = '';
|
|
35
|
+
element.style.right = '';
|
|
36
|
+
element.style.top = '';
|
|
37
|
+
element.style.bottom = '';
|
|
38
|
+
element.style.transform = '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Position an element's top-left corner in viewport px. */
|
|
42
|
+
export function placeAt(element: HTMLElement, left: number, top: number): void {
|
|
43
|
+
clearPosition(element);
|
|
44
|
+
element.style.left = `${String(left)}px`;
|
|
45
|
+
element.style.top = `${String(top)}px`;
|
|
46
|
+
}
|