@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,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
+ }
@@ -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> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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
+ }