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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -21
  3. package/README.md +191 -139
  4. package/dist/doctor/cli.mjs +49 -19
  5. package/dist/doctor/cli.mjs.map +1 -1
  6. package/dist/shared/client-version.gen.js +1 -1
  7. package/dist/shared/daily.js +1 -1
  8. package/dist/shared/globals.js +8 -3
  9. package/dist/shared/globals.js.map +1 -1
  10. package/dist/shared/index.d.ts +1 -1
  11. package/package.json +89 -89
  12. package/src/auto.ts +39 -39
  13. package/src/core/connection.ts +299 -299
  14. package/src/core/errors.ts +147 -147
  15. package/src/core/game-data.ts +38 -38
  16. package/src/core/hub-economy.ts +179 -179
  17. package/src/core/hub-progress.ts +205 -205
  18. package/src/core/hub.ts +131 -131
  19. package/src/core/menu-control.ts +61 -61
  20. package/src/core/offline/events.ts +50 -50
  21. package/src/core/offline/favorites.ts +21 -21
  22. package/src/core/offline/local-store.ts +261 -261
  23. package/src/core/offline/queue.ts +171 -171
  24. package/src/core/page.ts +74 -74
  25. package/src/core/rest/endpoint-types.ts +22 -22
  26. package/src/core/rest/rest-client.ts +152 -152
  27. package/src/core/storage/json-guards.ts +74 -74
  28. package/src/core/storage/key-value.ts +66 -66
  29. package/src/core/ticket.ts +43 -43
  30. package/src/core/types.ts +134 -134
  31. package/src/core/version/client-version.gen.ts +1 -1
  32. package/src/core/version/hub-version.ts +62 -62
  33. package/src/core/version/semver.ts +97 -97
  34. package/src/daily/daily-system.ts +161 -161
  35. package/src/daily/page-daily.ts +36 -36
  36. package/src/daily/types.ts +67 -67
  37. package/src/daily.ts +10 -10
  38. package/src/doctor/checks.ts +78 -76
  39. package/src/doctor/cli.ts +98 -106
  40. package/src/doctor/hub-version-fetch.ts +43 -0
  41. package/src/doctor/tsconfig.json +12 -12
  42. package/src/globals.ts +115 -110
  43. package/src/index.ts +50 -50
  44. package/src/input/device-state.ts +146 -146
  45. package/src/input/edges.ts +55 -55
  46. package/src/input/gamepad.ts +61 -61
  47. package/src/input/input-events.ts +32 -32
  48. package/src/input/input-registry.ts +164 -164
  49. package/src/input/input-system.ts +393 -393
  50. package/src/input/legacy-controls.ts +64 -64
  51. package/src/input/nav-state.ts +191 -191
  52. package/src/input/navigator.ts +171 -171
  53. package/src/input/on-screen-controls.ts +101 -101
  54. package/src/input/page-control.ts +43 -43
  55. package/src/input/page-input.ts +193 -193
  56. package/src/input/player-remaps.ts +77 -77
  57. package/src/input/source-values.ts +116 -116
  58. package/src/input/spatial.ts +47 -47
  59. package/src/input/types.ts +331 -331
  60. package/src/input/zero-config.ts +177 -177
  61. package/src/input.ts +22 -22
  62. package/src/mp.ts +10 -10
  63. package/src/overlay/controls-panel.ts +163 -163
  64. package/src/overlay/dom.ts +46 -46
  65. package/src/overlay/layout-clusters.ts +162 -162
  66. package/src/overlay/layout-geometry.ts +136 -136
  67. package/src/overlay/layout.ts +203 -203
  68. package/src/overlay/listener-scope.ts +23 -23
  69. package/src/overlay/on-screen-keyboard.ts +137 -137
  70. package/src/overlay/overlay.ts +208 -208
  71. package/src/overlay/styles.ts +9 -9
  72. package/src/overlay/virtual-controls.ts +262 -262
  73. package/src/race.ts +10 -10
  74. package/src/realtime/bridge.ts +121 -121
  75. package/src/realtime/cross-bundle.ts +29 -29
  76. package/src/realtime/fallback-notice.ts +48 -48
  77. package/src/realtime/hub-link.ts +65 -65
  78. package/src/realtime/latest-state.ts +33 -33
  79. package/src/realtime/multiplayer-forwarder.ts +28 -28
  80. package/src/realtime/multiplayer-types.ts +52 -52
  81. package/src/realtime/page-runtime.ts +60 -60
  82. package/src/realtime/race-forwarder.ts +59 -59
  83. package/src/realtime/race-types.ts +132 -132
  84. package/src/realtime/room-types.ts +125 -125
  85. package/src/realtime/rooms-forwarder.ts +79 -79
  86. package/src/realtime/runtime-api.ts +49 -49
  87. package/src/realtime/runtime-opener.ts +61 -61
  88. package/src/realtime/social-forwarder.ts +72 -72
  89. package/src/realtime/social-types.ts +78 -78
  90. package/src/realtime/types.ts +53 -53
  91. package/src/realtime/wire-types.ts +72 -72
  92. package/src/rooms.ts +10 -10
  93. package/src/social.ts +18 -18
  94. package/src/types.ts +13 -13
  95. package/src/uistack/page-stack.ts +45 -45
  96. package/src/uistack/play-suspension.ts +82 -82
  97. package/src/uistack/types.ts +76 -76
  98. 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
+ }