@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.
Files changed (293) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/LICENSE +21 -21
  3. package/README.md +203 -139
  4. package/dist/auto.d.ts +8 -8
  5. package/dist/auto.js +3 -3
  6. package/dist/avatar-pixi.d.ts +165 -0
  7. package/dist/avatar-pixi.d.ts.map +1 -0
  8. package/dist/avatar-pixi.js +299 -0
  9. package/dist/avatar-pixi.js.map +1 -0
  10. package/dist/avatar-svg.d.ts +4 -0
  11. package/dist/avatar-svg.js +5 -0
  12. package/dist/avatar-three.d.ts +69 -0
  13. package/dist/avatar-three.d.ts.map +1 -0
  14. package/dist/avatar-three.js +49 -0
  15. package/dist/avatar-three.js.map +1 -0
  16. package/dist/avatar.d.ts +7 -0
  17. package/dist/avatar.js +500 -0
  18. package/dist/avatar.js.map +1 -0
  19. package/dist/daily.d.ts +2 -2
  20. package/dist/doctor/cli.mjs +119 -28
  21. package/dist/doctor/cli.mjs.map +1 -1
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +3 -3
  24. package/dist/input.d.ts +2 -2
  25. package/dist/input.js +2 -2
  26. package/dist/mp.d.ts +3 -3
  27. package/dist/race.d.ts +1 -1
  28. package/dist/rooms.d.ts +3 -3
  29. package/dist/rooms.js +1 -1
  30. package/dist/shared/animator.d.ts +104 -0
  31. package/dist/shared/animator.d.ts.map +1 -0
  32. package/dist/shared/art.js +1946 -0
  33. package/dist/shared/art.js.map +1 -0
  34. package/dist/shared/atlas.d.ts +138 -0
  35. package/dist/shared/atlas.d.ts.map +1 -0
  36. package/dist/shared/avatar.d.ts +123 -0
  37. package/dist/shared/avatar.d.ts.map +1 -0
  38. package/dist/shared/bridge.js +18 -4
  39. package/dist/shared/bridge.js.map +1 -1
  40. package/dist/shared/client-version.gen.js +1 -1
  41. package/dist/shared/daily.d.ts +145 -19
  42. package/dist/shared/daily.d.ts.map +1 -1
  43. package/dist/shared/daily.js +138 -25
  44. package/dist/shared/daily.js.map +1 -1
  45. package/dist/shared/defineProperty.js +39 -0
  46. package/dist/shared/describe.d.ts +32 -0
  47. package/dist/shared/describe.d.ts.map +1 -0
  48. package/dist/shared/describe.js +109 -0
  49. package/dist/shared/describe.js.map +1 -0
  50. package/dist/shared/globals.js +9 -42
  51. package/dist/shared/globals.js.map +1 -1
  52. package/dist/shared/index.d.ts +1122 -46
  53. package/dist/shared/index.d.ts.map +1 -1
  54. package/dist/shared/input.d.ts +74 -14
  55. package/dist/shared/input.d.ts.map +1 -1
  56. package/dist/shared/input.js +179 -25
  57. package/dist/shared/input.js.map +1 -1
  58. package/dist/shared/json-guards.js +48 -0
  59. package/dist/shared/json-guards.js.map +1 -0
  60. package/dist/shared/latest-state.js +1 -1
  61. package/dist/shared/live-canvas.js +62 -0
  62. package/dist/shared/live-canvas.js.map +1 -0
  63. package/dist/shared/mp.js +10 -2
  64. package/dist/shared/mp.js.map +1 -1
  65. package/dist/shared/multiplayer-types.d.ts +15 -2
  66. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  67. package/dist/shared/overlays.js +800 -0
  68. package/dist/shared/overlays.js.map +1 -0
  69. package/dist/shared/page-stack.js +2 -1
  70. package/dist/shared/page-stack.js.map +1 -1
  71. package/dist/shared/pose-key.js +249 -0
  72. package/dist/shared/pose-key.js.map +1 -0
  73. package/dist/shared/race-types.d.ts +1 -1
  74. package/dist/shared/race.js +1 -1
  75. package/dist/shared/room-types.d.ts +28 -3
  76. package/dist/shared/room-types.d.ts.map +1 -1
  77. package/dist/shared/rooms.d.ts.map +1 -1
  78. package/dist/shared/rooms.js +23 -4
  79. package/dist/shared/rooms.js.map +1 -1
  80. package/dist/shared/runtime-opener.js +1 -1
  81. package/dist/shared/scene.d.ts +90 -0
  82. package/dist/shared/scene.d.ts.map +1 -0
  83. package/dist/shared/social.d.ts +90 -9
  84. package/dist/shared/social.d.ts.map +1 -1
  85. package/dist/shared/social.js +6 -1
  86. package/dist/shared/social.js.map +1 -1
  87. package/dist/shared/src.js +4563 -667
  88. package/dist/shared/src.js.map +1 -1
  89. package/dist/shared/svg-markup.js +2354 -0
  90. package/dist/shared/svg-markup.js.map +1 -0
  91. package/dist/shared/svg.js +27 -0
  92. package/dist/shared/svg.js.map +1 -0
  93. package/dist/shared/types.d.ts +174 -0
  94. package/dist/shared/types.d.ts.map +1 -0
  95. package/dist/shared/wire-types.d.ts +6 -2
  96. package/dist/shared/wire-types.d.ts.map +1 -1
  97. package/dist/social.d.ts +5 -5
  98. package/dist/social.js +2 -2
  99. package/dist/turns-rules.d.ts +2 -0
  100. package/dist/turns-rules.js +1 -0
  101. package/dist/turns.d.ts +61 -0
  102. package/dist/turns.d.ts.map +1 -0
  103. package/dist/turns.js +183 -0
  104. package/dist/turns.js.map +1 -0
  105. package/package.json +125 -89
  106. package/src/auto.ts +39 -39
  107. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  108. package/src/avatar/adapters/pixi-crowd.ts +329 -0
  109. package/src/avatar/adapters/pixi-types.ts +84 -0
  110. package/src/avatar/adapters/three.ts +126 -0
  111. package/src/avatar/animator/animator.ts +285 -0
  112. package/src/avatar/animator/reactions.ts +44 -0
  113. package/src/avatar/animator/schedule.ts +73 -0
  114. package/src/avatar/art/beard.ts +49 -0
  115. package/src/avatar/art/glasses.ts +67 -0
  116. package/src/avatar/art/hair.ts +254 -0
  117. package/src/avatar/art/headwear.ts +75 -0
  118. package/src/avatar/art/index.ts +21 -0
  119. package/src/avatar/art/shape.ts +87 -0
  120. package/src/avatar/art-store.ts +36 -0
  121. package/src/avatar/atlas.ts +246 -0
  122. package/src/avatar/catalog-view.ts +14 -0
  123. package/src/avatar/colour.ts +109 -0
  124. package/src/avatar/describe.ts +79 -0
  125. package/src/avatar/face.ts +187 -0
  126. package/src/avatar/geom.ts +173 -0
  127. package/src/avatar/layout.ts +47 -0
  128. package/src/avatar/look-like.ts +52 -0
  129. package/src/avatar/outputs/bitmap.ts +75 -0
  130. package/src/avatar/outputs/canvas-paint.ts +177 -0
  131. package/src/avatar/outputs/canvas.ts +62 -0
  132. package/src/avatar/outputs/custom-element.ts +140 -0
  133. package/src/avatar/outputs/element.ts +58 -0
  134. package/src/avatar/outputs/live-canvas.ts +57 -0
  135. package/src/avatar/outputs/overlay-svg.ts +22 -0
  136. package/src/avatar/outputs/pose-key.ts +30 -0
  137. package/src/avatar/outputs/svg-markup.ts +113 -0
  138. package/src/avatar/outputs/svg.ts +41 -0
  139. package/src/avatar/overlays.ts +128 -0
  140. package/src/avatar/render/body.ts +116 -0
  141. package/src/avatar/render/colours.ts +81 -0
  142. package/src/avatar/render/compose.ts +320 -0
  143. package/src/avatar/render/context.ts +117 -0
  144. package/src/avatar/render/eyes.ts +78 -0
  145. package/src/avatar/render/eyewear.ts +118 -0
  146. package/src/avatar/render/frame.ts +73 -0
  147. package/src/avatar/render/head.ts +81 -0
  148. package/src/avatar/render/mouth.ts +86 -0
  149. package/src/avatar/render/props.ts +91 -0
  150. package/src/avatar/render/types.ts +93 -0
  151. package/src/avatar/render/walker-parts.ts +121 -0
  152. package/src/avatar/render/walker.ts +199 -0
  153. package/src/avatar/scene.ts +119 -0
  154. package/src/avatar-pixi.ts +20 -0
  155. package/src/avatar-svg.ts +21 -0
  156. package/src/avatar-three.ts +18 -0
  157. package/src/avatar.ts +63 -0
  158. package/src/core/avatar/avatar-cache.ts +95 -0
  159. package/src/core/avatar/avatar-ref.ts +34 -0
  160. package/src/core/avatar/avatar-types.ts +43 -0
  161. package/src/core/avatar/hub-avatar.ts +308 -0
  162. package/src/core/connection.ts +408 -299
  163. package/src/core/errors.ts +160 -147
  164. package/src/core/game-data.ts +40 -38
  165. package/src/core/hub-economy.ts +216 -179
  166. package/src/core/hub-progress.ts +269 -205
  167. package/src/core/hub.ts +352 -131
  168. package/src/core/menu-control.ts +61 -61
  169. package/src/core/offline/cloud-saves.ts +62 -0
  170. package/src/core/offline/events.ts +50 -50
  171. package/src/core/offline/favorites.ts +21 -21
  172. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  173. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  174. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  175. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  176. package/src/core/offline/hub-kinds/index.ts +52 -0
  177. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  178. package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
  179. package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
  180. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  181. package/src/core/offline/kind-adapters.ts +48 -0
  182. package/src/core/offline/local-inventory.ts +64 -0
  183. package/src/core/offline/local-store.ts +304 -261
  184. package/src/core/offline/offline-api.ts +65 -0
  185. package/src/core/offline/outbox-entry.ts +59 -0
  186. package/src/core/offline/outbox-kinds.ts +78 -0
  187. package/src/core/offline/outbox-merge.ts +39 -0
  188. package/src/core/offline/outbox-replayer.ts +141 -0
  189. package/src/core/offline/outbox-store.ts +85 -0
  190. package/src/core/offline/outbox.ts +232 -0
  191. package/src/core/offline/queue.ts +126 -171
  192. package/src/core/offline/replay-lease.ts +87 -0
  193. package/src/core/offline/replay-policy.ts +52 -0
  194. package/src/core/page-account.ts +121 -0
  195. package/src/core/page.ts +87 -74
  196. package/src/core/rest/endpoint-types.ts +22 -22
  197. package/src/core/rest/rest-client.ts +163 -152
  198. package/src/core/rest/routes.gen.ts +144 -88
  199. package/src/core/resume/hub-resume.ts +286 -0
  200. package/src/core/resume/local-resume.ts +130 -0
  201. package/src/core/resume/resume-list.ts +65 -0
  202. package/src/core/resume/resume-request.ts +52 -0
  203. package/src/core/resume/resume-types.ts +64 -0
  204. package/src/core/saved/daily-saves.ts +196 -0
  205. package/src/core/saved/daily-seed.ts +16 -0
  206. package/src/core/saved/hub-saved.ts +221 -0
  207. package/src/core/saved/local-saved.ts +122 -0
  208. package/src/core/saved/saved-list.ts +129 -0
  209. package/src/core/saved/saved-open.ts +104 -0
  210. package/src/core/saved/saved-order.ts +49 -0
  211. package/src/core/saved/saved-types.ts +24 -0
  212. package/src/core/storage/json-guards.ts +80 -74
  213. package/src/core/storage/key-value.ts +66 -66
  214. package/src/core/ticket.ts +43 -43
  215. package/src/core/turns/hub-turns.ts +161 -0
  216. package/src/core/turns/turn-game.ts +510 -0
  217. package/src/core/turns/your-turn-buzz.ts +69 -0
  218. package/src/core/types.ts +134 -134
  219. package/src/core/version/client-version.gen.ts +2 -2
  220. package/src/core/version/hub-version.ts +62 -62
  221. package/src/core/version/semver.ts +97 -97
  222. package/src/daily/daily-system.ts +276 -161
  223. package/src/daily/page-daily.ts +36 -36
  224. package/src/daily/types.ts +105 -67
  225. package/src/daily.ts +10 -10
  226. package/src/doctor/checks.ts +86 -76
  227. package/src/doctor/cli.ts +101 -106
  228. package/src/doctor/hub-version-fetch.ts +43 -0
  229. package/src/doctor/tsconfig.json +12 -12
  230. package/src/doctor/world-check.ts +61 -0
  231. package/src/globals.ts +115 -110
  232. package/src/index.ts +68 -50
  233. package/src/input/device-state.ts +146 -146
  234. package/src/input/edges.ts +55 -55
  235. package/src/input/focus-ring.ts +13 -0
  236. package/src/input/gamepad.ts +61 -61
  237. package/src/input/input-events.ts +32 -32
  238. package/src/input/input-registry.ts +171 -164
  239. package/src/input/input-system.ts +406 -393
  240. package/src/input/legacy-controls.ts +64 -64
  241. package/src/input/modality.ts +122 -0
  242. package/src/input/nav-state.ts +200 -191
  243. package/src/input/navigator.ts +182 -171
  244. package/src/input/on-screen-controls.ts +101 -101
  245. package/src/input/page-control.ts +43 -43
  246. package/src/input/page-input.ts +193 -193
  247. package/src/input/player-remaps.ts +77 -77
  248. package/src/input/source-values.ts +116 -116
  249. package/src/input/spatial.ts +47 -47
  250. package/src/input/types.ts +331 -331
  251. package/src/input/zero-config.ts +164 -177
  252. package/src/input.ts +26 -22
  253. package/src/mp.ts +10 -10
  254. package/src/overlay/controls-panel.ts +163 -163
  255. package/src/overlay/dom.ts +46 -46
  256. package/src/overlay/layout-clusters.ts +162 -162
  257. package/src/overlay/layout-geometry.ts +136 -136
  258. package/src/overlay/layout.ts +203 -203
  259. package/src/overlay/listener-scope.ts +23 -23
  260. package/src/overlay/on-screen-keyboard.ts +137 -137
  261. package/src/overlay/overlay.ts +234 -208
  262. package/src/overlay/styles.ts +13 -12
  263. package/src/overlay/virtual-controls.ts +262 -262
  264. package/src/race.ts +10 -10
  265. package/src/realtime/bridge.ts +126 -121
  266. package/src/realtime/cross-bundle.ts +29 -29
  267. package/src/realtime/fallback-notice.ts +48 -48
  268. package/src/realtime/hub-link.ts +65 -65
  269. package/src/realtime/latest-state.ts +33 -33
  270. package/src/realtime/multiplayer-forwarder.ts +40 -28
  271. package/src/realtime/multiplayer-types.ts +66 -52
  272. package/src/realtime/page-runtime.ts +60 -60
  273. package/src/realtime/race-forwarder.ts +59 -59
  274. package/src/realtime/race-types.ts +132 -132
  275. package/src/realtime/room-types.ts +151 -125
  276. package/src/realtime/rooms-forwarder.ts +104 -79
  277. package/src/realtime/runtime-api.ts +74 -49
  278. package/src/realtime/runtime-opener.ts +61 -61
  279. package/src/realtime/social-forwarder.ts +77 -72
  280. package/src/realtime/social-types.ts +86 -78
  281. package/src/realtime/types.ts +54 -53
  282. package/src/realtime/wire-types.ts +79 -72
  283. package/src/realtime/world-types.ts +20 -0
  284. package/src/rooms.ts +10 -10
  285. package/src/social.ts +19 -18
  286. package/src/turns-kit/check-rules.ts +281 -0
  287. package/src/turns-rules.ts +12 -0
  288. package/src/turns.ts +14 -0
  289. package/src/types.ts +13 -13
  290. package/src/uistack/page-stack.ts +45 -45
  291. package/src/uistack/play-suspension.ts +82 -82
  292. package/src/uistack/types.ts +76 -76
  293. package/src/uistack/ui-stack.ts +144 -144
@@ -1,208 +1,234 @@
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 { trackInputModality } from '../input/modality';
10
+ import { readField } from '../realtime/cross-bundle';
11
+ import { ControlsPanel } from './controls-panel';
12
+ import type { ControlsPanelHost } from './controls-panel';
13
+ import { pageBody, placeAt, requireElement } from './dom';
14
+ import { layoutControls } from './layout';
15
+ import { controlSize } from './layout-geometry';
16
+ import type { Insets, LayoutItem } from './layout-geometry';
17
+ import { ListenerScope } from './listener-scope';
18
+ import { OnScreenKeyboard } from './on-screen-keyboard';
19
+ import { OVERLAY_CSS } from './styles';
20
+ import { createVirtualControl } from './virtual-controls';
21
+ import type { VirtualControlCallbacks } from './virtual-controls';
22
+
23
+ /** One built on-screen control. */
24
+ interface BuiltControl {
25
+ readonly definition: VirtualDef;
26
+ readonly group: string;
27
+ readonly element: HTMLElement;
28
+ }
29
+
30
+ /** The window event the race HUD fires with the bands it covers. */
31
+ const RACE_HUD_EVENT = 'hub:race-hud';
32
+ /** Wait for the browser to settle after a rotation before re-laying out. */
33
+ const ORIENTATION_SETTLE_MS = 200;
34
+
35
+ /** A number field of an event detail, or 0. */
36
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a window event detail from another bundle (the race HUD)
37
+ function numberField(detail: unknown, key: string): number {
38
+ const field = Number(readField(detail, key));
39
+ return Number.isFinite(field) ? field : 0;
40
+ }
41
+
42
+ /** The overlay (one per input system). */
43
+ export class Overlay implements NavHighlighter {
44
+ private readonly shadowHost: HTMLDivElement;
45
+ private readonly root: ShadowRoot;
46
+ private readonly controlsLayer: HTMLDivElement;
47
+ private readonly ring: HTMLDivElement;
48
+ private readonly keyboard: OnScreenKeyboard;
49
+ private readonly controlsPanel: ControlsPanel;
50
+ private controls: BuiltControl[] = [];
51
+ private controlScope = new ListenerScope();
52
+ private overrides: InputOverrides = {};
53
+ private gameInsets: Partial<Insets> = {};
54
+ private raceHudBands = { top: 0, bottom: 0 };
55
+ private safeAreaProbe: HTMLDivElement | null = null;
56
+
57
+ constructor() {
58
+ this.shadowHost = document.createElement('div');
59
+ this.shadowHost.id = 'hub-input-root';
60
+ this.root = this.shadowHost.attachShadow({ mode: 'open' });
61
+ this.root.innerHTML = `<style>${OVERLAY_CSS}</style>
62
+ <div class="layer controls"></div>
63
+ <div class="ring" hidden></div>`;
64
+ this.controlsLayer = requireElement(this.root, '.controls', HTMLDivElement);
65
+ this.ring = requireElement(this.root, '.ring', HTMLDivElement);
66
+ this.keyboard = new OnScreenKeyboard(this.root);
67
+ this.controlsPanel = new ControlsPanel(this.root, this.shadowHost);
68
+ (pageBody() ?? document.documentElement).appendChild(this.shadowHost);
69
+ window.addEventListener('resize', () => {
70
+ this.layout();
71
+ });
72
+ window.addEventListener('orientationchange', () => {
73
+ setTimeout(() => {
74
+ this.layout();
75
+ }, ORIENTATION_SETTLE_MS);
76
+ });
77
+ // Keep top and bottom controls clear of the hub's race HUD while it shows.
78
+ window.addEventListener(RACE_HUD_EVENT, (event) => {
79
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the race HUD's event detail, read field by field
80
+ const detail: unknown = event instanceof CustomEvent ? event.detail : null;
81
+ const isVisible = readField(detail, 'visible') === true;
82
+ const bands = { top: isVisible ? numberField(detail, 'top') : 0, bottom: isVisible ? numberField(detail, 'bottom') : 0 };
83
+ if (bands.top !== this.raceHudBands.top || bands.bottom !== this.raceHudBands.bottom) {
84
+ this.raceHudBands = bands;
85
+ this.layout();
86
+ }
87
+ });
88
+ }
89
+
90
+ /** (Re)build every on-screen control from the config and the player's overrides. */
91
+ build(config: InputConfig, overrides: InputOverrides, callbacks: VirtualControlCallbacks): void {
92
+ this.overrides = overrides;
93
+ this.controlScope.dispose();
94
+ this.controlScope = new ListenerScope();
95
+ this.controlsLayer.innerHTML = '';
96
+ this.controls = [];
97
+ const environment = { callbacks, scope: this.controlScope, isEditing: () => this.controlsPanel.isEditing };
98
+ for (const [group, groupDefinition] of Object.entries(config.groups)) {
99
+ for (const declared of groupDefinition.virtual ?? []) {
100
+ const definition: VirtualDef = { ...declared, ...overrides.virtual?.[declared.id] };
101
+ const element = createVirtualControl(definition, environment);
102
+ Reflect.set(element, '__group', group);
103
+ this.controlsLayer.appendChild(element);
104
+ this.controls.push({ definition, group, element });
105
+ }
106
+ }
107
+ this.layout();
108
+ }
109
+
110
+ /** Replace a button or tap control's content at runtime. Listeners live on the wrapper, so they survive. */
111
+ setControlHtml(id: string, html: string): void {
112
+ const control = this.controls.find((candidate) => candidate.definition.id === id);
113
+ if (control !== undefined && (control.definition.type === 'button' || control.definition.type === 'tap')) {
114
+ control.element.innerHTML = html;
115
+ }
116
+ }
117
+
118
+ /** Show controls of the enabled groups (or of every group, with `null`, while editing). */
119
+ setVirtualVisible(isVisible: boolean, enabledGroups: ReadonlySet<string> | null): void {
120
+ for (const control of this.controls) {
121
+ const isShown = isVisible && (enabledGroups === null || enabledGroups.has(control.group));
122
+ control.element.style.display = isShown ? '' : 'none';
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Game insets (px) auto-placed controls keep clear of, e.g. a top HUD bar.
128
+ * Merged with earlier calls. Also published on the page as
129
+ * `--hub-inset-top` (and right, bottom, left), so the hub's toasts sit
130
+ * below a top HUD too.
131
+ */
132
+ setInsets(insets: Partial<Insets>): void {
133
+ this.gameInsets = { ...this.gameInsets, ...insets };
134
+ publishInsets(this.gameInsets);
135
+ this.layout();
136
+ }
137
+
138
+ /** Re-place every control for the current viewport, keeping the player's dragged spots. */
139
+ layout(): void {
140
+ if (this.controls.length === 0) {
141
+ return;
142
+ }
143
+ const insets: Insets = {
144
+ top: (this.gameInsets.top ?? 0) + this.raceHudBands.top,
145
+ right: this.gameInsets.right ?? 0,
146
+ bottom: (this.gameInsets.bottom ?? 0) + this.raceHudBands.bottom,
147
+ left: this.gameInsets.left ?? 0,
148
+ };
149
+ const items: LayoutItem[] = this.controls.map(({ definition }) => {
150
+ const dragged = this.overrides.virtual?.[definition.id];
151
+ return {
152
+ id: definition.id,
153
+ type: definition.type,
154
+ place: definition.place,
155
+ ...controlSize(definition),
156
+ pos: definition.pos,
157
+ offset: definition.offset,
158
+ override: dragged?.x !== undefined && dragged.y !== undefined ? { x: dragged.x, y: dragged.y } : undefined,
159
+ };
160
+ });
161
+ const boxes = layoutControls(items, window.innerWidth, window.innerHeight, { insets, safe: this.readSafeArea() });
162
+ for (const control of this.controls) {
163
+ const box = boxes[control.definition.id];
164
+ if (box !== undefined) {
165
+ placeAt(control.element, box.x, box.y);
166
+ }
167
+ }
168
+ }
169
+
170
+ /** Ring the highlighted element, for keyboard and gamepad players only (never after a tap or click). */
171
+ showHighlight(bounds: Rect, radius: string): void {
172
+ this.ring.hidden = !trackInputModality().showsFocus;
173
+ this.ring.style.borderRadius = radius;
174
+ this.ring.style.left = `${String(bounds.x)}px`;
175
+ this.ring.style.top = `${String(bounds.y)}px`;
176
+ this.ring.style.width = `${String(bounds.width)}px`;
177
+ this.ring.style.height = `${String(bounds.height)}px`;
178
+ }
179
+
180
+ /** Remove the ring. */
181
+ hideHighlight(): void {
182
+ this.ring.hidden = true;
183
+ }
184
+
185
+ /** Open the on-screen keyboard for a text field. */
186
+ openKeyboard(target: HTMLInputElement | HTMLTextAreaElement | null): void {
187
+ this.keyboard.open(target);
188
+ }
189
+
190
+ /** Whether the on-screen keyboard is showing. */
191
+ get isKeyboardOpen(): boolean {
192
+ return this.keyboard.isOpen;
193
+ }
194
+
195
+ /** Open (or close) the Controls screen. */
196
+ toggleControls(config: InputConfig | null, host: ControlsPanelHost): void {
197
+ this.controlsPanel.toggle(config, host);
198
+ }
199
+
200
+ /** Per-frame work: gamepad navigation of the keyboard. */
201
+ afterFrame(): void {
202
+ this.keyboard.pollGamepads();
203
+ }
204
+
205
+ /** The device safe-area insets (notch, home bar), via a probe element. */
206
+ private readSafeArea(): Insets {
207
+ if (this.safeAreaProbe === null) {
208
+ this.safeAreaProbe = document.createElement('div');
209
+ this.safeAreaProbe.className = 'safe-probe';
210
+ this.root.appendChild(this.safeAreaProbe);
211
+ }
212
+ const style = getComputedStyle(this.safeAreaProbe);
213
+ const pixels = (value: string): number => parseFloat(value) || 0;
214
+ return { top: pixels(style.paddingTop), right: pixels(style.paddingRight), bottom: pixels(style.paddingBottom), left: pixels(style.paddingLeft) };
215
+ }
216
+ }
217
+
218
+ /** Every inset side. */
219
+ const INSET_SIDES: readonly (keyof Insets)[] = ['top', 'right', 'bottom', 'left'];
220
+
221
+ /** The page-wide CSS variables other hub UI (the runtime's toasts) keeps clear of. */
222
+ function publishInsets(insets: Partial<Insets>): void {
223
+ if (typeof document === 'undefined') {
224
+ return;
225
+ }
226
+ for (const side of INSET_SIDES) {
227
+ const px = insets[side];
228
+ if (px === undefined) {
229
+ document.documentElement.style.removeProperty(`--hub-inset-${side}`);
230
+ } else {
231
+ document.documentElement.style.setProperty(`--hub-inset-${side}`, `${String(Math.max(0, Math.round(px)))}px`);
232
+ }
233
+ }
234
+ }
@@ -1,11 +1,12 @@
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
+ import { FOCUS_RING_SHADOW } from '../input/focus-ring';
5
+
6
+ /** The hub accent colour. */
7
+ export const ACCENT_COLOR = '#ff5f3b';
8
+
9
+ /** The overlay stylesheet. */
9
10
  export const OVERLAY_CSS = `
10
11
  :host { all: initial; }
11
12
  .layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483000; font-family: 'Space Mono', ui-monospace, monospace; }
@@ -28,8 +29,8 @@ export const OVERLAY_CSS = `
28
29
  :host(.editing) .edit-bar .btn { margin: 0; }
29
30
  .safe-probe { position: fixed; left: 0; top: 0; width: 0; height: 0; visibility: hidden; pointer-events: none;
30
31
  padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px); }
31
- .ring { position: fixed; pointer-events: none; z-index: 2147483200; border: 3px solid ${ACCENT_COLOR}; border-radius: 8px;
32
- box-shadow: 0 0 0 2px rgba(0,0,0,.4); transition: left .08s, top .08s, width .08s, height .08s; }
32
+ .ring { position: fixed; pointer-events: none; z-index: 2147483200; box-shadow: ${FOCUS_RING_SHADOW};
33
+ transition: left .08s, top .08s, width .08s, height .08s; }
33
34
  .ring[hidden] { display: none; }
34
35
  .kb { position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483400; background: #0d0f14ee; padding: 10px;
35
36
  pointer-events: auto; border-top: 1px solid #2a3142; }
@@ -38,7 +39,7 @@ export const OVERLAY_CSS = `
38
39
  .kb .k { flex: 1; min-width: 0; max-width: 64px; height: 46px; border-radius: 8px; border: 1px solid #2a3142;
39
40
  background: #11141c; color: #e8e6df; font: 16px monospace; cursor: pointer; }
40
41
  .kb .k.space { max-width: 280px; } .kb .k.wide { max-width: 90px; }
41
- .kb .k.kfocus { border-color: ${ACCENT_COLOR}; box-shadow: 0 0 0 2px ${ACCENT_COLOR}; }
42
+ .kb .k.kfocus { box-shadow: ${FOCUS_RING_SHADOW}; }
42
43
  .panel { position: fixed; inset: 0; z-index: 2147483500; display: grid; place-items: center; background: rgba(0,0,0,.6); pointer-events: auto;
43
44
  font-family: 'Space Mono', ui-monospace, monospace; }
44
45
  .panel-card { background: #0d0f14; color: #e8e6df; border: 1px solid #2a3142; border-radius: 12px; padding: 18px;
@@ -51,4 +52,4 @@ export const OVERLAY_CSS = `
51
52
  .btn.editlayout, .btn.reset { background: transparent; color: #e8e6df; border: 1px solid #2a3142; }
52
53
  .panel-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
53
54
  .hint { color: #38d6ff; font-size: 12px; min-height: 1em; }
54
- `;
55
+ `;