@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.1
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 +9 -0
- package/LICENSE +21 -21
- package/README.md +191 -139
- package/dist/doctor/cli.mjs +49 -19
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.js +1 -1
- package/dist/shared/globals.js +8 -3
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/package.json +89 -89
- package/src/auto.ts +39 -39
- package/src/core/connection.ts +299 -299
- package/src/core/errors.ts +147 -147
- package/src/core/game-data.ts +38 -38
- package/src/core/hub-economy.ts +179 -179
- package/src/core/hub-progress.ts +205 -205
- package/src/core/hub.ts +131 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/local-store.ts +261 -261
- package/src/core/offline/queue.ts +171 -171
- package/src/core/page.ts +74 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +152 -152
- package/src/core/storage/json-guards.ts +74 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +1 -1
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +161 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +67 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +78 -76
- package/src/doctor/cli.ts +98 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/globals.ts +115 -110
- package/src/index.ts +50 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +164 -164
- package/src/input/input-system.ts +393 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/nav-state.ts +191 -191
- package/src/input/navigator.ts +171 -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 +177 -177
- package/src/input.ts +22 -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 +208 -208
- package/src/overlay/styles.ts +9 -9
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +121 -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 +28 -28
- package/src/realtime/multiplayer-types.ts +52 -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 +125 -125
- package/src/realtime/rooms-forwarder.ts +79 -79
- package/src/realtime/runtime-api.ts +49 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +72 -72
- package/src/realtime/social-types.ts +78 -78
- package/src/realtime/types.ts +53 -53
- package/src/realtime/wire-types.ts +72 -72
- package/src/rooms.ts +10 -10
- package/src/social.ts +18 -18
- 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
package/src/overlay/overlay.ts
CHANGED
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The input system's DOM overlay: the on-screen touch controls, the
|
|
3
|
-
* navigation highlight ring, the on-screen keyboard and the Controls screen.
|
|
4
|
-
* Everything lives in one style-isolated shadow root layered over the game,
|
|
5
|
-
* so it works the same for canvas and HTML games.
|
|
6
|
-
*/
|
|
7
|
-
import type { NavHighlighter } from '../input/nav-state';
|
|
8
|
-
import type { InputConfig, InputOverrides, Rect, VirtualDef } from '../input/types';
|
|
9
|
-
import { readField } from '../realtime/cross-bundle';
|
|
10
|
-
import { ControlsPanel } from './controls-panel';
|
|
11
|
-
import type { ControlsPanelHost } from './controls-panel';
|
|
12
|
-
import { pageBody, placeAt, requireElement } from './dom';
|
|
13
|
-
import { layoutControls } from './layout';
|
|
14
|
-
import { controlSize } from './layout-geometry';
|
|
15
|
-
import type { Insets, LayoutItem } from './layout-geometry';
|
|
16
|
-
import { ListenerScope } from './listener-scope';
|
|
17
|
-
import { OnScreenKeyboard } from './on-screen-keyboard';
|
|
18
|
-
import { OVERLAY_CSS } from './styles';
|
|
19
|
-
import { createVirtualControl } from './virtual-controls';
|
|
20
|
-
import type { VirtualControlCallbacks } from './virtual-controls';
|
|
21
|
-
|
|
22
|
-
/** One built on-screen control. */
|
|
23
|
-
interface BuiltControl {
|
|
24
|
-
readonly definition: VirtualDef;
|
|
25
|
-
readonly group: string;
|
|
26
|
-
readonly element: HTMLElement;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The window event the race HUD fires with the bands it covers. */
|
|
30
|
-
const RACE_HUD_EVENT = 'hub:race-hud';
|
|
31
|
-
/** Wait for the browser to settle after a rotation before re-laying out. */
|
|
32
|
-
const ORIENTATION_SETTLE_MS = 200;
|
|
33
|
-
|
|
34
|
-
/** A number field of an event detail, or 0. */
|
|
35
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: a window event detail from another bundle (the race HUD)
|
|
36
|
-
function numberField(detail: unknown, key: string): number {
|
|
37
|
-
const field = Number(readField(detail, key));
|
|
38
|
-
return Number.isFinite(field) ? field : 0;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** The overlay (one per input system). */
|
|
42
|
-
export class Overlay implements NavHighlighter {
|
|
43
|
-
private readonly shadowHost: HTMLDivElement;
|
|
44
|
-
private readonly root: ShadowRoot;
|
|
45
|
-
private readonly controlsLayer: HTMLDivElement;
|
|
46
|
-
private readonly ring: HTMLDivElement;
|
|
47
|
-
private readonly keyboard: OnScreenKeyboard;
|
|
48
|
-
private readonly controlsPanel: ControlsPanel;
|
|
49
|
-
private controls: BuiltControl[] = [];
|
|
50
|
-
private controlScope = new ListenerScope();
|
|
51
|
-
private overrides: InputOverrides = {};
|
|
52
|
-
private gameInsets: Partial<Insets> = {};
|
|
53
|
-
private raceHudBands = { top: 0, bottom: 0 };
|
|
54
|
-
private safeAreaProbe: HTMLDivElement | null = null;
|
|
55
|
-
|
|
56
|
-
constructor() {
|
|
57
|
-
this.shadowHost = document.createElement('div');
|
|
58
|
-
this.shadowHost.id = 'hub-input-root';
|
|
59
|
-
this.root = this.shadowHost.attachShadow({ mode: 'open' });
|
|
60
|
-
this.root.innerHTML = `<style>${OVERLAY_CSS}</style>
|
|
61
|
-
<div class="layer controls"></div>
|
|
62
|
-
<div class="ring" hidden></div>`;
|
|
63
|
-
this.controlsLayer = requireElement(this.root, '.controls', HTMLDivElement);
|
|
64
|
-
this.ring = requireElement(this.root, '.ring', HTMLDivElement);
|
|
65
|
-
this.keyboard = new OnScreenKeyboard(this.root);
|
|
66
|
-
this.controlsPanel = new ControlsPanel(this.root, this.shadowHost);
|
|
67
|
-
(pageBody() ?? document.documentElement).appendChild(this.shadowHost);
|
|
68
|
-
window.addEventListener('resize', () => {
|
|
69
|
-
this.layout();
|
|
70
|
-
});
|
|
71
|
-
window.addEventListener('orientationchange', () => {
|
|
72
|
-
setTimeout(() => {
|
|
73
|
-
this.layout();
|
|
74
|
-
}, ORIENTATION_SETTLE_MS);
|
|
75
|
-
});
|
|
76
|
-
// Keep top and bottom controls clear of the hub's race HUD while it shows.
|
|
77
|
-
window.addEventListener(RACE_HUD_EVENT, (event) => {
|
|
78
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the race HUD's event detail, read field by field
|
|
79
|
-
const detail: unknown = event instanceof CustomEvent ? event.detail : null;
|
|
80
|
-
const isVisible = readField(detail, 'visible') === true;
|
|
81
|
-
const bands = { top: isVisible ? numberField(detail, 'top') : 0, bottom: isVisible ? numberField(detail, 'bottom') : 0 };
|
|
82
|
-
if (bands.top !== this.raceHudBands.top || bands.bottom !== this.raceHudBands.bottom) {
|
|
83
|
-
this.raceHudBands = bands;
|
|
84
|
-
this.layout();
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** (Re)build every on-screen control from the config and the player's overrides. */
|
|
90
|
-
build(config: InputConfig, overrides: InputOverrides, callbacks: VirtualControlCallbacks): void {
|
|
91
|
-
this.overrides = overrides;
|
|
92
|
-
this.controlScope.dispose();
|
|
93
|
-
this.controlScope = new ListenerScope();
|
|
94
|
-
this.controlsLayer.innerHTML = '';
|
|
95
|
-
this.controls = [];
|
|
96
|
-
const environment = { callbacks, scope: this.controlScope, isEditing: () => this.controlsPanel.isEditing };
|
|
97
|
-
for (const [group, groupDefinition] of Object.entries(config.groups)) {
|
|
98
|
-
for (const declared of groupDefinition.virtual ?? []) {
|
|
99
|
-
const definition: VirtualDef = { ...declared, ...overrides.virtual?.[declared.id] };
|
|
100
|
-
const element = createVirtualControl(definition, environment);
|
|
101
|
-
Reflect.set(element, '__group', group);
|
|
102
|
-
this.controlsLayer.appendChild(element);
|
|
103
|
-
this.controls.push({ definition, group, element });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
this.layout();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Replace a button or tap control's content at runtime. Listeners live on the wrapper, so they survive. */
|
|
110
|
-
setControlHtml(id: string, html: string): void {
|
|
111
|
-
const control = this.controls.find((candidate) => candidate.definition.id === id);
|
|
112
|
-
if (control !== undefined && (control.definition.type === 'button' || control.definition.type === 'tap')) {
|
|
113
|
-
control.element.innerHTML = html;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Show controls of the enabled groups (or of every group, with `null`, while editing). */
|
|
118
|
-
setVirtualVisible(isVisible: boolean, enabledGroups: ReadonlySet<string> | null): void {
|
|
119
|
-
for (const control of this.controls) {
|
|
120
|
-
const isShown = isVisible && (enabledGroups === null || enabledGroups.has(control.group));
|
|
121
|
-
control.element.style.display = isShown ? '' : 'none';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Game insets (px) auto-placed controls keep clear of, e.g. a top HUD bar. Merged with earlier calls. */
|
|
126
|
-
setInsets(insets: Partial<Insets>): void {
|
|
127
|
-
this.gameInsets = { ...this.gameInsets, ...insets };
|
|
128
|
-
this.layout();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Re-place every control for the current viewport, keeping the player's dragged spots. */
|
|
132
|
-
layout(): void {
|
|
133
|
-
if (this.controls.length === 0) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const insets: Insets = {
|
|
137
|
-
top: (this.gameInsets.top ?? 0) + this.raceHudBands.top,
|
|
138
|
-
right: this.gameInsets.right ?? 0,
|
|
139
|
-
bottom: (this.gameInsets.bottom ?? 0) + this.raceHudBands.bottom,
|
|
140
|
-
left: this.gameInsets.left ?? 0,
|
|
141
|
-
};
|
|
142
|
-
const items: LayoutItem[] = this.controls.map(({ definition }) => {
|
|
143
|
-
const dragged = this.overrides.virtual?.[definition.id];
|
|
144
|
-
return {
|
|
145
|
-
id: definition.id,
|
|
146
|
-
type: definition.type,
|
|
147
|
-
place: definition.place,
|
|
148
|
-
...controlSize(definition),
|
|
149
|
-
pos: definition.pos,
|
|
150
|
-
offset: definition.offset,
|
|
151
|
-
override: dragged?.x !== undefined && dragged.y !== undefined ? { x: dragged.x, y: dragged.y } : undefined,
|
|
152
|
-
};
|
|
153
|
-
});
|
|
154
|
-
const boxes = layoutControls(items, window.innerWidth, window.innerHeight, { insets, safe: this.readSafeArea() });
|
|
155
|
-
for (const control of this.controls) {
|
|
156
|
-
const box = boxes[control.definition.id];
|
|
157
|
-
if (box !== undefined) {
|
|
158
|
-
placeAt(control.element, box.x, box.y);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Ring the highlighted element. */
|
|
164
|
-
showHighlight(bounds: Rect): void {
|
|
165
|
-
this.ring.hidden = false;
|
|
166
|
-
this.ring.style.left = `${String(bounds.x)}px`;
|
|
167
|
-
this.ring.style.top = `${String(bounds.y)}px`;
|
|
168
|
-
this.ring.style.width = `${String(bounds.width)}px`;
|
|
169
|
-
this.ring.style.height = `${String(bounds.height)}px`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Remove the ring. */
|
|
173
|
-
hideHighlight(): void {
|
|
174
|
-
this.ring.hidden = true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Open the on-screen keyboard for a text field. */
|
|
178
|
-
openKeyboard(target: HTMLInputElement | HTMLTextAreaElement | null): void {
|
|
179
|
-
this.keyboard.open(target);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Whether the on-screen keyboard is showing. */
|
|
183
|
-
get isKeyboardOpen(): boolean {
|
|
184
|
-
return this.keyboard.isOpen;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Open (or close) the Controls screen. */
|
|
188
|
-
toggleControls(config: InputConfig | null, host: ControlsPanelHost): void {
|
|
189
|
-
this.controlsPanel.toggle(config, host);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Per-frame work: gamepad navigation of the keyboard. */
|
|
193
|
-
afterFrame(): void {
|
|
194
|
-
this.keyboard.pollGamepads();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** The device safe-area insets (notch, home bar), via a probe element. */
|
|
198
|
-
private readSafeArea(): Insets {
|
|
199
|
-
if (this.safeAreaProbe === null) {
|
|
200
|
-
this.safeAreaProbe = document.createElement('div');
|
|
201
|
-
this.safeAreaProbe.className = 'safe-probe';
|
|
202
|
-
this.root.appendChild(this.safeAreaProbe);
|
|
203
|
-
}
|
|
204
|
-
const style = getComputedStyle(this.safeAreaProbe);
|
|
205
|
-
const pixels = (value: string): number => parseFloat(value) || 0;
|
|
206
|
-
return { top: pixels(style.paddingTop), right: pixels(style.paddingRight), bottom: pixels(style.paddingBottom), left: pixels(style.paddingLeft) };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The input system's DOM overlay: the on-screen touch controls, the
|
|
3
|
+
* navigation highlight ring, the on-screen keyboard and the Controls screen.
|
|
4
|
+
* Everything lives in one style-isolated shadow root layered over the game,
|
|
5
|
+
* so it works the same for canvas and HTML games.
|
|
6
|
+
*/
|
|
7
|
+
import type { NavHighlighter } from '../input/nav-state';
|
|
8
|
+
import type { InputConfig, InputOverrides, Rect, VirtualDef } from '../input/types';
|
|
9
|
+
import { readField } from '../realtime/cross-bundle';
|
|
10
|
+
import { ControlsPanel } from './controls-panel';
|
|
11
|
+
import type { ControlsPanelHost } from './controls-panel';
|
|
12
|
+
import { pageBody, placeAt, requireElement } from './dom';
|
|
13
|
+
import { layoutControls } from './layout';
|
|
14
|
+
import { controlSize } from './layout-geometry';
|
|
15
|
+
import type { Insets, LayoutItem } from './layout-geometry';
|
|
16
|
+
import { ListenerScope } from './listener-scope';
|
|
17
|
+
import { OnScreenKeyboard } from './on-screen-keyboard';
|
|
18
|
+
import { OVERLAY_CSS } from './styles';
|
|
19
|
+
import { createVirtualControl } from './virtual-controls';
|
|
20
|
+
import type { VirtualControlCallbacks } from './virtual-controls';
|
|
21
|
+
|
|
22
|
+
/** One built on-screen control. */
|
|
23
|
+
interface BuiltControl {
|
|
24
|
+
readonly definition: VirtualDef;
|
|
25
|
+
readonly group: string;
|
|
26
|
+
readonly element: HTMLElement;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The window event the race HUD fires with the bands it covers. */
|
|
30
|
+
const RACE_HUD_EVENT = 'hub:race-hud';
|
|
31
|
+
/** Wait for the browser to settle after a rotation before re-laying out. */
|
|
32
|
+
const ORIENTATION_SETTLE_MS = 200;
|
|
33
|
+
|
|
34
|
+
/** A number field of an event detail, or 0. */
|
|
35
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: a window event detail from another bundle (the race HUD)
|
|
36
|
+
function numberField(detail: unknown, key: string): number {
|
|
37
|
+
const field = Number(readField(detail, key));
|
|
38
|
+
return Number.isFinite(field) ? field : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The overlay (one per input system). */
|
|
42
|
+
export class Overlay implements NavHighlighter {
|
|
43
|
+
private readonly shadowHost: HTMLDivElement;
|
|
44
|
+
private readonly root: ShadowRoot;
|
|
45
|
+
private readonly controlsLayer: HTMLDivElement;
|
|
46
|
+
private readonly ring: HTMLDivElement;
|
|
47
|
+
private readonly keyboard: OnScreenKeyboard;
|
|
48
|
+
private readonly controlsPanel: ControlsPanel;
|
|
49
|
+
private controls: BuiltControl[] = [];
|
|
50
|
+
private controlScope = new ListenerScope();
|
|
51
|
+
private overrides: InputOverrides = {};
|
|
52
|
+
private gameInsets: Partial<Insets> = {};
|
|
53
|
+
private raceHudBands = { top: 0, bottom: 0 };
|
|
54
|
+
private safeAreaProbe: HTMLDivElement | null = null;
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
this.shadowHost = document.createElement('div');
|
|
58
|
+
this.shadowHost.id = 'hub-input-root';
|
|
59
|
+
this.root = this.shadowHost.attachShadow({ mode: 'open' });
|
|
60
|
+
this.root.innerHTML = `<style>${OVERLAY_CSS}</style>
|
|
61
|
+
<div class="layer controls"></div>
|
|
62
|
+
<div class="ring" hidden></div>`;
|
|
63
|
+
this.controlsLayer = requireElement(this.root, '.controls', HTMLDivElement);
|
|
64
|
+
this.ring = requireElement(this.root, '.ring', HTMLDivElement);
|
|
65
|
+
this.keyboard = new OnScreenKeyboard(this.root);
|
|
66
|
+
this.controlsPanel = new ControlsPanel(this.root, this.shadowHost);
|
|
67
|
+
(pageBody() ?? document.documentElement).appendChild(this.shadowHost);
|
|
68
|
+
window.addEventListener('resize', () => {
|
|
69
|
+
this.layout();
|
|
70
|
+
});
|
|
71
|
+
window.addEventListener('orientationchange', () => {
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
this.layout();
|
|
74
|
+
}, ORIENTATION_SETTLE_MS);
|
|
75
|
+
});
|
|
76
|
+
// Keep top and bottom controls clear of the hub's race HUD while it shows.
|
|
77
|
+
window.addEventListener(RACE_HUD_EVENT, (event) => {
|
|
78
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the race HUD's event detail, read field by field
|
|
79
|
+
const detail: unknown = event instanceof CustomEvent ? event.detail : null;
|
|
80
|
+
const isVisible = readField(detail, 'visible') === true;
|
|
81
|
+
const bands = { top: isVisible ? numberField(detail, 'top') : 0, bottom: isVisible ? numberField(detail, 'bottom') : 0 };
|
|
82
|
+
if (bands.top !== this.raceHudBands.top || bands.bottom !== this.raceHudBands.bottom) {
|
|
83
|
+
this.raceHudBands = bands;
|
|
84
|
+
this.layout();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** (Re)build every on-screen control from the config and the player's overrides. */
|
|
90
|
+
build(config: InputConfig, overrides: InputOverrides, callbacks: VirtualControlCallbacks): void {
|
|
91
|
+
this.overrides = overrides;
|
|
92
|
+
this.controlScope.dispose();
|
|
93
|
+
this.controlScope = new ListenerScope();
|
|
94
|
+
this.controlsLayer.innerHTML = '';
|
|
95
|
+
this.controls = [];
|
|
96
|
+
const environment = { callbacks, scope: this.controlScope, isEditing: () => this.controlsPanel.isEditing };
|
|
97
|
+
for (const [group, groupDefinition] of Object.entries(config.groups)) {
|
|
98
|
+
for (const declared of groupDefinition.virtual ?? []) {
|
|
99
|
+
const definition: VirtualDef = { ...declared, ...overrides.virtual?.[declared.id] };
|
|
100
|
+
const element = createVirtualControl(definition, environment);
|
|
101
|
+
Reflect.set(element, '__group', group);
|
|
102
|
+
this.controlsLayer.appendChild(element);
|
|
103
|
+
this.controls.push({ definition, group, element });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
this.layout();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Replace a button or tap control's content at runtime. Listeners live on the wrapper, so they survive. */
|
|
110
|
+
setControlHtml(id: string, html: string): void {
|
|
111
|
+
const control = this.controls.find((candidate) => candidate.definition.id === id);
|
|
112
|
+
if (control !== undefined && (control.definition.type === 'button' || control.definition.type === 'tap')) {
|
|
113
|
+
control.element.innerHTML = html;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Show controls of the enabled groups (or of every group, with `null`, while editing). */
|
|
118
|
+
setVirtualVisible(isVisible: boolean, enabledGroups: ReadonlySet<string> | null): void {
|
|
119
|
+
for (const control of this.controls) {
|
|
120
|
+
const isShown = isVisible && (enabledGroups === null || enabledGroups.has(control.group));
|
|
121
|
+
control.element.style.display = isShown ? '' : 'none';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Game insets (px) auto-placed controls keep clear of, e.g. a top HUD bar. Merged with earlier calls. */
|
|
126
|
+
setInsets(insets: Partial<Insets>): void {
|
|
127
|
+
this.gameInsets = { ...this.gameInsets, ...insets };
|
|
128
|
+
this.layout();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Re-place every control for the current viewport, keeping the player's dragged spots. */
|
|
132
|
+
layout(): void {
|
|
133
|
+
if (this.controls.length === 0) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const insets: Insets = {
|
|
137
|
+
top: (this.gameInsets.top ?? 0) + this.raceHudBands.top,
|
|
138
|
+
right: this.gameInsets.right ?? 0,
|
|
139
|
+
bottom: (this.gameInsets.bottom ?? 0) + this.raceHudBands.bottom,
|
|
140
|
+
left: this.gameInsets.left ?? 0,
|
|
141
|
+
};
|
|
142
|
+
const items: LayoutItem[] = this.controls.map(({ definition }) => {
|
|
143
|
+
const dragged = this.overrides.virtual?.[definition.id];
|
|
144
|
+
return {
|
|
145
|
+
id: definition.id,
|
|
146
|
+
type: definition.type,
|
|
147
|
+
place: definition.place,
|
|
148
|
+
...controlSize(definition),
|
|
149
|
+
pos: definition.pos,
|
|
150
|
+
offset: definition.offset,
|
|
151
|
+
override: dragged?.x !== undefined && dragged.y !== undefined ? { x: dragged.x, y: dragged.y } : undefined,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
const boxes = layoutControls(items, window.innerWidth, window.innerHeight, { insets, safe: this.readSafeArea() });
|
|
155
|
+
for (const control of this.controls) {
|
|
156
|
+
const box = boxes[control.definition.id];
|
|
157
|
+
if (box !== undefined) {
|
|
158
|
+
placeAt(control.element, box.x, box.y);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Ring the highlighted element. */
|
|
164
|
+
showHighlight(bounds: Rect): void {
|
|
165
|
+
this.ring.hidden = false;
|
|
166
|
+
this.ring.style.left = `${String(bounds.x)}px`;
|
|
167
|
+
this.ring.style.top = `${String(bounds.y)}px`;
|
|
168
|
+
this.ring.style.width = `${String(bounds.width)}px`;
|
|
169
|
+
this.ring.style.height = `${String(bounds.height)}px`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Remove the ring. */
|
|
173
|
+
hideHighlight(): void {
|
|
174
|
+
this.ring.hidden = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Open the on-screen keyboard for a text field. */
|
|
178
|
+
openKeyboard(target: HTMLInputElement | HTMLTextAreaElement | null): void {
|
|
179
|
+
this.keyboard.open(target);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether the on-screen keyboard is showing. */
|
|
183
|
+
get isKeyboardOpen(): boolean {
|
|
184
|
+
return this.keyboard.isOpen;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Open (or close) the Controls screen. */
|
|
188
|
+
toggleControls(config: InputConfig | null, host: ControlsPanelHost): void {
|
|
189
|
+
this.controlsPanel.toggle(config, host);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Per-frame work: gamepad navigation of the keyboard. */
|
|
193
|
+
afterFrame(): void {
|
|
194
|
+
this.keyboard.pollGamepads();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The device safe-area insets (notch, home bar), via a probe element. */
|
|
198
|
+
private readSafeArea(): Insets {
|
|
199
|
+
if (this.safeAreaProbe === null) {
|
|
200
|
+
this.safeAreaProbe = document.createElement('div');
|
|
201
|
+
this.safeAreaProbe.className = 'safe-probe';
|
|
202
|
+
this.root.appendChild(this.safeAreaProbe);
|
|
203
|
+
}
|
|
204
|
+
const style = getComputedStyle(this.safeAreaProbe);
|
|
205
|
+
const pixels = (value: string): number => parseFloat(value) || 0;
|
|
206
|
+
return { top: pixels(style.paddingTop), right: pixels(style.paddingRight), bottom: pixels(style.paddingBottom), left: pixels(style.paddingLeft) };
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/overlay/styles.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The overlay's styles (inside its shadow root, so games can't restyle it).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/** The hub accent colour. */
|
|
6
|
-
export const ACCENT_COLOR = '#ff5f3b';
|
|
7
|
-
|
|
8
|
-
/** The overlay stylesheet. */
|
|
1
|
+
/**
|
|
2
|
+
* The overlay's styles (inside its shadow root, so games can't restyle it).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** The hub accent colour. */
|
|
6
|
+
export const ACCENT_COLOR = '#ff5f3b';
|
|
7
|
+
|
|
8
|
+
/** The overlay stylesheet. */
|
|
9
9
|
export const OVERLAY_CSS = `
|
|
10
10
|
:host { all: initial; }
|
|
11
11
|
.layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483000; font-family: 'Space Mono', ui-monospace, monospace; }
|
|
@@ -51,4 +51,4 @@ export const OVERLAY_CSS = `
|
|
|
51
51
|
.btn.editlayout, .btn.reset { background: transparent; color: #e8e6df; border: 1px solid #2a3142; }
|
|
52
52
|
.panel-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
53
53
|
.hint { color: #38d6ff; font-size: 12px; min-height: 1em; }
|
|
54
|
-
`;
|
|
54
|
+
`;
|