@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.2

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.
Files changed (154) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -21
  3. package/README.md +206 -139
  4. package/dist/auto.d.ts +6 -6
  5. package/dist/auto.js +2 -2
  6. package/dist/doctor/cli.mjs +62 -25
  7. package/dist/doctor/cli.mjs.map +1 -1
  8. package/dist/index.d.ts +6 -6
  9. package/dist/index.js +2 -2
  10. package/dist/mp.d.ts +3 -3
  11. package/dist/race.d.ts +1 -1
  12. package/dist/rooms.d.ts +3 -3
  13. package/dist/shared/bridge.js +9 -3
  14. package/dist/shared/bridge.js.map +1 -1
  15. package/dist/shared/client-version.gen.js +1 -1
  16. package/dist/shared/daily.d.ts +10 -4
  17. package/dist/shared/daily.d.ts.map +1 -1
  18. package/dist/shared/daily.js +12 -2
  19. package/dist/shared/daily.js.map +1 -1
  20. package/dist/shared/globals.js +8 -3
  21. package/dist/shared/globals.js.map +1 -1
  22. package/dist/shared/index.d.ts +314 -35
  23. package/dist/shared/index.d.ts.map +1 -1
  24. package/dist/shared/input.js +7 -2
  25. package/dist/shared/input.js.map +1 -1
  26. package/dist/shared/latest-state.js +1 -1
  27. package/dist/shared/mp.js +10 -2
  28. package/dist/shared/mp.js.map +1 -1
  29. package/dist/shared/multiplayer-types.d.ts +15 -2
  30. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  31. package/dist/shared/race-types.d.ts +1 -1
  32. package/dist/shared/race.js +1 -1
  33. package/dist/shared/room-types.d.ts +28 -3
  34. package/dist/shared/room-types.d.ts.map +1 -1
  35. package/dist/shared/rooms.d.ts.map +1 -1
  36. package/dist/shared/rooms.js +10 -2
  37. package/dist/shared/rooms.js.map +1 -1
  38. package/dist/shared/runtime-opener.js +1 -1
  39. package/dist/shared/social.d.ts +23 -8
  40. package/dist/shared/social.d.ts.map +1 -1
  41. package/dist/shared/social.js +6 -1
  42. package/dist/shared/social.js.map +1 -1
  43. package/dist/shared/src.js +1588 -388
  44. package/dist/shared/src.js.map +1 -1
  45. package/dist/shared/wire-types.d.ts +6 -2
  46. package/dist/shared/wire-types.d.ts.map +1 -1
  47. package/dist/social.d.ts +5 -5
  48. package/dist/social.js +1 -1
  49. package/package.json +89 -89
  50. package/src/auto.ts +39 -39
  51. package/src/core/connection.ts +394 -299
  52. package/src/core/errors.ts +147 -147
  53. package/src/core/game-data.ts +40 -38
  54. package/src/core/hub-economy.ts +216 -179
  55. package/src/core/hub-progress.ts +202 -205
  56. package/src/core/hub.ts +133 -131
  57. package/src/core/menu-control.ts +61 -61
  58. package/src/core/offline/events.ts +50 -50
  59. package/src/core/offline/favorites.ts +21 -21
  60. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  61. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  62. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  63. package/src/core/offline/hub-kinds/index.ts +35 -0
  64. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  65. package/src/core/offline/hub-kinds/progress-kinds.ts +148 -0
  66. package/src/core/offline/kind-adapters.ts +48 -0
  67. package/src/core/offline/local-inventory.ts +64 -0
  68. package/src/core/offline/local-store.ts +259 -261
  69. package/src/core/offline/offline-api.ts +65 -0
  70. package/src/core/offline/outbox-entry.ts +59 -0
  71. package/src/core/offline/outbox-kinds.ts +78 -0
  72. package/src/core/offline/outbox-merge.ts +39 -0
  73. package/src/core/offline/outbox-replayer.ts +141 -0
  74. package/src/core/offline/outbox-store.ts +85 -0
  75. package/src/core/offline/outbox.ts +232 -0
  76. package/src/core/offline/queue.ts +126 -171
  77. package/src/core/offline/replay-lease.ts +87 -0
  78. package/src/core/offline/replay-policy.ts +52 -0
  79. package/src/core/page.ts +74 -74
  80. package/src/core/rest/endpoint-types.ts +22 -22
  81. package/src/core/rest/rest-client.ts +163 -152
  82. package/src/core/rest/routes.gen.ts +108 -87
  83. package/src/core/storage/json-guards.ts +80 -74
  84. package/src/core/storage/key-value.ts +66 -66
  85. package/src/core/ticket.ts +43 -43
  86. package/src/core/types.ts +134 -134
  87. package/src/core/version/client-version.gen.ts +1 -1
  88. package/src/core/version/hub-version.ts +62 -62
  89. package/src/core/version/semver.ts +97 -97
  90. package/src/daily/daily-system.ts +168 -161
  91. package/src/daily/page-daily.ts +36 -36
  92. package/src/daily/types.ts +68 -67
  93. package/src/daily.ts +10 -10
  94. package/src/doctor/checks.ts +86 -76
  95. package/src/doctor/cli.ts +98 -106
  96. package/src/doctor/hub-version-fetch.ts +43 -0
  97. package/src/doctor/tsconfig.json +12 -12
  98. package/src/globals.ts +115 -110
  99. package/src/index.ts +55 -50
  100. package/src/input/device-state.ts +146 -146
  101. package/src/input/edges.ts +55 -55
  102. package/src/input/gamepad.ts +61 -61
  103. package/src/input/input-events.ts +32 -32
  104. package/src/input/input-registry.ts +171 -164
  105. package/src/input/input-system.ts +393 -393
  106. package/src/input/legacy-controls.ts +64 -64
  107. package/src/input/nav-state.ts +191 -191
  108. package/src/input/navigator.ts +171 -171
  109. package/src/input/on-screen-controls.ts +101 -101
  110. package/src/input/page-control.ts +43 -43
  111. package/src/input/page-input.ts +193 -193
  112. package/src/input/player-remaps.ts +77 -77
  113. package/src/input/source-values.ts +116 -116
  114. package/src/input/spatial.ts +47 -47
  115. package/src/input/types.ts +331 -331
  116. package/src/input/zero-config.ts +177 -177
  117. package/src/input.ts +22 -22
  118. package/src/mp.ts +10 -10
  119. package/src/overlay/controls-panel.ts +163 -163
  120. package/src/overlay/dom.ts +46 -46
  121. package/src/overlay/layout-clusters.ts +162 -162
  122. package/src/overlay/layout-geometry.ts +136 -136
  123. package/src/overlay/layout.ts +203 -203
  124. package/src/overlay/listener-scope.ts +23 -23
  125. package/src/overlay/on-screen-keyboard.ts +137 -137
  126. package/src/overlay/overlay.ts +208 -208
  127. package/src/overlay/styles.ts +9 -9
  128. package/src/overlay/virtual-controls.ts +262 -262
  129. package/src/race.ts +10 -10
  130. package/src/realtime/bridge.ts +126 -121
  131. package/src/realtime/cross-bundle.ts +29 -29
  132. package/src/realtime/fallback-notice.ts +48 -48
  133. package/src/realtime/hub-link.ts +65 -65
  134. package/src/realtime/latest-state.ts +33 -33
  135. package/src/realtime/multiplayer-forwarder.ts +40 -28
  136. package/src/realtime/multiplayer-types.ts +66 -52
  137. package/src/realtime/page-runtime.ts +60 -60
  138. package/src/realtime/race-forwarder.ts +59 -59
  139. package/src/realtime/race-types.ts +132 -132
  140. package/src/realtime/room-types.ts +151 -125
  141. package/src/realtime/rooms-forwarder.ts +91 -79
  142. package/src/realtime/runtime-api.ts +60 -49
  143. package/src/realtime/runtime-opener.ts +61 -61
  144. package/src/realtime/social-forwarder.ts +77 -72
  145. package/src/realtime/social-types.ts +86 -78
  146. package/src/realtime/types.ts +53 -53
  147. package/src/realtime/wire-types.ts +79 -72
  148. package/src/rooms.ts +10 -10
  149. package/src/social.ts +18 -18
  150. package/src/types.ts +13 -13
  151. package/src/uistack/page-stack.ts +45 -45
  152. package/src/uistack/play-suspension.ts +82 -82
  153. package/src/uistack/types.ts +76 -76
  154. package/src/uistack/ui-stack.ts +144 -144
@@ -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
+ }
@@ -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
+ `;