@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,262 +1,262 @@
1
- /**
2
- * Building one on-screen control (button, tap region, joystick or dpad) and
3
- * wiring its touch and mouse handling, plus drag-to-move while the player
4
- * edits the layout.
5
- */
6
- import type { VirtualDef } from '../input/types';
7
- import { placeAt, requireElement } from './dom';
8
- import { declaredSize, isStickType } from './layout-geometry';
9
- import type { ListenerScope } from './listener-scope';
10
- import { ACCENT_COLOR } from './styles';
11
-
12
- /** What a control reports. */
13
- export interface VirtualControlCallbacks {
14
- /** A button went down. */
15
- onButtonDown(id: string): void;
16
- /** A button came up. */
17
- onButtonUp(id: string): void;
18
- /** A stick moved (-1..1 per axis; 0, 0 on release). */
19
- onStick(id: string, outX: number, outY: number): void;
20
- /** A tap region was tapped. */
21
- onTap(): void;
22
- /** The player dropped a dragged control: its top-left corner as % of the viewport. */
23
- onDragged(id: string, spot: { x: number; y: number }): void;
24
- }
25
-
26
- /** What controls need from the overlay. */
27
- export interface VirtualControlEnvironment {
28
- readonly callbacks: VirtualControlCallbacks;
29
- /** Window listeners, removed when the controls are rebuilt. */
30
- readonly scope: ListenerScope;
31
- /** Whether the player is editing the layout (controls drag instead of acting). */
32
- isEditing(): boolean;
33
- }
34
-
35
- /** A dpad snaps past this deflection. */
36
- const DPAD_THRESHOLD = 0.4;
37
-
38
- const RADIUS_BY_SHAPE = { circle: '50%', pill: '999px', square: '0', round: '16px' } as const;
39
-
40
- /** The first touch of a touch event. */
41
- function firstTouch(event: TouchEvent): Touch | undefined {
42
- return event.changedTouches[0];
43
- }
44
-
45
- function bindButton(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
46
- const { callbacks } = environment;
47
- const isTapRegion = element.classList.contains('vc-transparent');
48
- const press = (event: Event): void => {
49
- if (environment.isEditing()) {
50
- return;
51
- }
52
- event.preventDefault();
53
- element.classList.add('pressed');
54
- callbacks.onButtonDown(id);
55
- if (isTapRegion) {
56
- callbacks.onTap();
57
- }
58
- };
59
- const release = (event: Event): void => {
60
- event.preventDefault();
61
- element.classList.remove('pressed');
62
- callbacks.onButtonUp(id);
63
- };
64
- element.addEventListener('touchstart', press, { passive: false });
65
- element.addEventListener('touchend', release, { passive: false });
66
- element.addEventListener('touchcancel', release, { passive: false });
67
- element.addEventListener('mousedown', press);
68
- environment.scope.listenWindow('mouseup', release);
69
- }
70
-
71
- function bindStick(id: string, wrapper: HTMLElement, knob: HTMLElement, definition: VirtualDef, environment: VirtualControlEnvironment): void {
72
- const isDpad = definition.type === 'dpad';
73
- const axisMode = definition.axis ?? 'both';
74
- const { callbacks } = environment;
75
- let activeTouchId = -1;
76
- let centreX = 0;
77
- let centreY = 0;
78
- let travelX = 1;
79
- let travelY = 1;
80
- const measure = (): void => {
81
- const bounds = wrapper.getBoundingClientRect();
82
- centreX = bounds.left + bounds.width / 2;
83
- centreY = bounds.top + bounds.height / 2;
84
- const knobRadius = knob.getBoundingClientRect().width / 2 || Math.min(bounds.width, bounds.height) * 0.25;
85
- travelX = Math.max(1, bounds.width / 2 - knobRadius);
86
- travelY = Math.max(1, bounds.height / 2 - knobRadius);
87
- };
88
- const moveTo = (pointerX: number, pointerY: number): void => {
89
- let dx = axisMode === 'y' ? 0 : pointerX - centreX;
90
- let dy = axisMode === 'x' ? 0 : pointerY - centreY;
91
- if (axisMode === 'both') {
92
- const limit = Math.min(travelX, travelY);
93
- const distance = Math.hypot(dx, dy);
94
- if (distance > limit) {
95
- dx = (dx * limit) / distance;
96
- dy = (dy * limit) / distance;
97
- }
98
- } else {
99
- dx = Math.max(-travelX, Math.min(travelX, dx));
100
- dy = Math.max(-travelY, Math.min(travelY, dy));
101
- }
102
- let outX = dx / travelX;
103
- let outY = dy / travelY;
104
- if (isDpad) {
105
- outX = Math.abs(outX) > DPAD_THRESHOLD ? Math.sign(outX) : 0;
106
- outY = Math.abs(outY) > DPAD_THRESHOLD ? Math.sign(outY) : 0;
107
- }
108
- knob.style.transform = `translate(${String(dx)}px, ${String(dy)}px)`;
109
- callbacks.onStick(id, outX, outY);
110
- };
111
- const release = (): void => {
112
- activeTouchId = -1;
113
- knob.style.transform = 'translate(0,0)';
114
- callbacks.onStick(id, 0, 0);
115
- };
116
- wrapper.addEventListener('touchstart', (event) => {
117
- const touch = firstTouch(event);
118
- if (environment.isEditing() || touch === undefined) {
119
- return;
120
- }
121
- event.preventDefault();
122
- measure();
123
- activeTouchId = touch.identifier;
124
- moveTo(touch.clientX, touch.clientY);
125
- }, { passive: false });
126
- wrapper.addEventListener('touchmove', (event) => {
127
- event.preventDefault();
128
- for (const touch of event.changedTouches) {
129
- if (touch.identifier === activeTouchId) {
130
- moveTo(touch.clientX, touch.clientY);
131
- }
132
- }
133
- }, { passive: false });
134
- wrapper.addEventListener('touchend', release, { passive: false });
135
- wrapper.addEventListener('touchcancel', release, { passive: false });
136
- // Mouse drag (desktop testing).
137
- let isMouseHeld = false;
138
- wrapper.addEventListener('mousedown', (event) => {
139
- if (environment.isEditing()) {
140
- return;
141
- }
142
- measure();
143
- isMouseHeld = true;
144
- moveTo(event.clientX, event.clientY);
145
- });
146
- environment.scope.listenWindow('mousemove', (event) => {
147
- if (isMouseHeld) {
148
- moveTo(event.clientX, event.clientY);
149
- }
150
- });
151
- environment.scope.listenWindow('mouseup', () => {
152
- if (isMouseHeld) {
153
- isMouseHeld = false;
154
- release();
155
- }
156
- });
157
- }
158
-
159
- /** Drag-to-move while editing: the control keeps its grab point under the finger and stays fully on-screen. */
160
- function bindEditDrag(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
161
- let isDragging = false;
162
- let grabX = 0;
163
- let grabY = 0;
164
- let droppedSpot: { x: number; y: number } | null = null;
165
- const start = (pointerX: number, pointerY: number): void => {
166
- if (!environment.isEditing()) {
167
- return;
168
- }
169
- isDragging = true;
170
- const bounds = element.getBoundingClientRect();
171
- grabX = pointerX - bounds.left;
172
- grabY = pointerY - bounds.top;
173
- droppedSpot = null;
174
- };
175
- const move = (pointerX: number, pointerY: number): void => {
176
- if (!isDragging) {
177
- return;
178
- }
179
- const viewportWidth = window.innerWidth;
180
- const viewportHeight = window.innerHeight;
181
- const left = Math.max(0, Math.min(viewportWidth - element.offsetWidth, pointerX - grabX));
182
- const top = Math.max(0, Math.min(viewportHeight - element.offsetHeight, pointerY - grabY));
183
- placeAt(element, left, top);
184
- droppedSpot = { x: (left / viewportWidth) * 100, y: (top / viewportHeight) * 100 };
185
- };
186
- const end = (): void => {
187
- if (!isDragging) {
188
- return;
189
- }
190
- isDragging = false;
191
- if (droppedSpot !== null) {
192
- environment.callbacks.onDragged(id, droppedSpot);
193
- }
194
- };
195
- element.addEventListener('mousedown', (event) => {
196
- start(event.clientX, event.clientY);
197
- });
198
- element.addEventListener('touchstart', (event) => {
199
- const touch = firstTouch(event);
200
- if (touch !== undefined) {
201
- start(touch.clientX, touch.clientY);
202
- }
203
- }, { passive: true });
204
- environment.scope.listenWindow('mousemove', (event) => {
205
- move(event.clientX, event.clientY);
206
- });
207
- environment.scope.listenWindow('touchmove', (event) => {
208
- const touch = firstTouch(event);
209
- if (isDragging && touch !== undefined) {
210
- move(touch.clientX, touch.clientY);
211
- }
212
- });
213
- environment.scope.listenWindow('mouseup', end);
214
- environment.scope.listenWindow('touchend', end);
215
- }
216
-
217
- /** Build one on-screen control element, wired up. */
218
- export function createVirtualControl(definition: VirtualDef, environment: VirtualControlEnvironment): HTMLElement {
219
- const isStick = isStickType(definition.type);
220
- const { w: width, h: height } = declaredSize(definition);
221
- const wrapper = document.createElement('div');
222
- wrapper.className = `vc vc-${definition.type}`;
223
- wrapper.dataset['controlId'] = definition.id;
224
- // The v1 expando tests and tools read the control id from.
225
- Reflect.set(wrapper, '__id', definition.id);
226
- wrapper.style.width = `${String(width)}px`;
227
- wrapper.style.height = `${String(height)}px`;
228
- if (definition.opacity !== undefined) {
229
- wrapper.style.opacity = String(definition.opacity);
230
- }
231
- wrapper.style.setProperty('--vc-color', definition.color ?? ACCENT_COLOR);
232
- if (definition.bg !== undefined) {
233
- wrapper.style.setProperty('--vc-bg', definition.bg);
234
- }
235
- if (definition.text !== undefined) {
236
- wrapper.style.color = definition.text;
237
- }
238
- const radius = RADIUS_BY_SHAPE[definition.shape ?? (isStick ? 'circle' : 'round')];
239
- wrapper.style.borderRadius = radius;
240
- if (isStick) {
241
- wrapper.innerHTML = '<div class="base"></div><div class="knob"></div>';
242
- requireElement(wrapper, '.base', HTMLElement).style.borderRadius = radius;
243
- // The knob stays a circle sized to the short edge (a wide pill stick gets a
244
- // round knob that slides). Single-axis sticks get a knob that mostly fills
245
- // the short edge; 2D sticks a smaller one with room to travel.
246
- const knob = requireElement(wrapper, '.knob', HTMLElement);
247
- const isSingleAxis = definition.axis === 'x' || definition.axis === 'y';
248
- const knobDiameter = Math.round(Math.min(width, height) * (isSingleAxis ? 0.92 : 0.5));
249
- knob.style.width = `${String(knobDiameter)}px`;
250
- knob.style.height = `${String(knobDiameter)}px`;
251
- bindStick(definition.id, wrapper, knob, definition, environment);
252
- } else {
253
- // The game's own markup (an SVG icon, an entity in a label), inserted as given.
254
- wrapper.innerHTML = definition.html ?? `<span>${definition.label ?? ''}</span>`;
255
- if (definition.type === 'tap') {
256
- wrapper.classList.add('vc-transparent');
257
- }
258
- bindButton(definition.id, wrapper, environment);
259
- }
260
- bindEditDrag(definition.id, wrapper, environment);
261
- return wrapper;
262
- }
1
+ /**
2
+ * Building one on-screen control (button, tap region, joystick or dpad) and
3
+ * wiring its touch and mouse handling, plus drag-to-move while the player
4
+ * edits the layout.
5
+ */
6
+ import type { VirtualDef } from '../input/types';
7
+ import { placeAt, requireElement } from './dom';
8
+ import { declaredSize, isStickType } from './layout-geometry';
9
+ import type { ListenerScope } from './listener-scope';
10
+ import { ACCENT_COLOR } from './styles';
11
+
12
+ /** What a control reports. */
13
+ export interface VirtualControlCallbacks {
14
+ /** A button went down. */
15
+ onButtonDown(id: string): void;
16
+ /** A button came up. */
17
+ onButtonUp(id: string): void;
18
+ /** A stick moved (-1..1 per axis; 0, 0 on release). */
19
+ onStick(id: string, outX: number, outY: number): void;
20
+ /** A tap region was tapped. */
21
+ onTap(): void;
22
+ /** The player dropped a dragged control: its top-left corner as % of the viewport. */
23
+ onDragged(id: string, spot: { x: number; y: number }): void;
24
+ }
25
+
26
+ /** What controls need from the overlay. */
27
+ export interface VirtualControlEnvironment {
28
+ readonly callbacks: VirtualControlCallbacks;
29
+ /** Window listeners, removed when the controls are rebuilt. */
30
+ readonly scope: ListenerScope;
31
+ /** Whether the player is editing the layout (controls drag instead of acting). */
32
+ isEditing(): boolean;
33
+ }
34
+
35
+ /** A dpad snaps past this deflection. */
36
+ const DPAD_THRESHOLD = 0.4;
37
+
38
+ const RADIUS_BY_SHAPE = { circle: '50%', pill: '999px', square: '0', round: '16px' } as const;
39
+
40
+ /** The first touch of a touch event. */
41
+ function firstTouch(event: TouchEvent): Touch | undefined {
42
+ return event.changedTouches[0];
43
+ }
44
+
45
+ function bindButton(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
46
+ const { callbacks } = environment;
47
+ const isTapRegion = element.classList.contains('vc-transparent');
48
+ const press = (event: Event): void => {
49
+ if (environment.isEditing()) {
50
+ return;
51
+ }
52
+ event.preventDefault();
53
+ element.classList.add('pressed');
54
+ callbacks.onButtonDown(id);
55
+ if (isTapRegion) {
56
+ callbacks.onTap();
57
+ }
58
+ };
59
+ const release = (event: Event): void => {
60
+ event.preventDefault();
61
+ element.classList.remove('pressed');
62
+ callbacks.onButtonUp(id);
63
+ };
64
+ element.addEventListener('touchstart', press, { passive: false });
65
+ element.addEventListener('touchend', release, { passive: false });
66
+ element.addEventListener('touchcancel', release, { passive: false });
67
+ element.addEventListener('mousedown', press);
68
+ environment.scope.listenWindow('mouseup', release);
69
+ }
70
+
71
+ function bindStick(id: string, wrapper: HTMLElement, knob: HTMLElement, definition: VirtualDef, environment: VirtualControlEnvironment): void {
72
+ const isDpad = definition.type === 'dpad';
73
+ const axisMode = definition.axis ?? 'both';
74
+ const { callbacks } = environment;
75
+ let activeTouchId = -1;
76
+ let centreX = 0;
77
+ let centreY = 0;
78
+ let travelX = 1;
79
+ let travelY = 1;
80
+ const measure = (): void => {
81
+ const bounds = wrapper.getBoundingClientRect();
82
+ centreX = bounds.left + bounds.width / 2;
83
+ centreY = bounds.top + bounds.height / 2;
84
+ const knobRadius = knob.getBoundingClientRect().width / 2 || Math.min(bounds.width, bounds.height) * 0.25;
85
+ travelX = Math.max(1, bounds.width / 2 - knobRadius);
86
+ travelY = Math.max(1, bounds.height / 2 - knobRadius);
87
+ };
88
+ const moveTo = (pointerX: number, pointerY: number): void => {
89
+ let dx = axisMode === 'y' ? 0 : pointerX - centreX;
90
+ let dy = axisMode === 'x' ? 0 : pointerY - centreY;
91
+ if (axisMode === 'both') {
92
+ const limit = Math.min(travelX, travelY);
93
+ const distance = Math.hypot(dx, dy);
94
+ if (distance > limit) {
95
+ dx = (dx * limit) / distance;
96
+ dy = (dy * limit) / distance;
97
+ }
98
+ } else {
99
+ dx = Math.max(-travelX, Math.min(travelX, dx));
100
+ dy = Math.max(-travelY, Math.min(travelY, dy));
101
+ }
102
+ let outX = dx / travelX;
103
+ let outY = dy / travelY;
104
+ if (isDpad) {
105
+ outX = Math.abs(outX) > DPAD_THRESHOLD ? Math.sign(outX) : 0;
106
+ outY = Math.abs(outY) > DPAD_THRESHOLD ? Math.sign(outY) : 0;
107
+ }
108
+ knob.style.transform = `translate(${String(dx)}px, ${String(dy)}px)`;
109
+ callbacks.onStick(id, outX, outY);
110
+ };
111
+ const release = (): void => {
112
+ activeTouchId = -1;
113
+ knob.style.transform = 'translate(0,0)';
114
+ callbacks.onStick(id, 0, 0);
115
+ };
116
+ wrapper.addEventListener('touchstart', (event) => {
117
+ const touch = firstTouch(event);
118
+ if (environment.isEditing() || touch === undefined) {
119
+ return;
120
+ }
121
+ event.preventDefault();
122
+ measure();
123
+ activeTouchId = touch.identifier;
124
+ moveTo(touch.clientX, touch.clientY);
125
+ }, { passive: false });
126
+ wrapper.addEventListener('touchmove', (event) => {
127
+ event.preventDefault();
128
+ for (const touch of event.changedTouches) {
129
+ if (touch.identifier === activeTouchId) {
130
+ moveTo(touch.clientX, touch.clientY);
131
+ }
132
+ }
133
+ }, { passive: false });
134
+ wrapper.addEventListener('touchend', release, { passive: false });
135
+ wrapper.addEventListener('touchcancel', release, { passive: false });
136
+ // Mouse drag (desktop testing).
137
+ let isMouseHeld = false;
138
+ wrapper.addEventListener('mousedown', (event) => {
139
+ if (environment.isEditing()) {
140
+ return;
141
+ }
142
+ measure();
143
+ isMouseHeld = true;
144
+ moveTo(event.clientX, event.clientY);
145
+ });
146
+ environment.scope.listenWindow('mousemove', (event) => {
147
+ if (isMouseHeld) {
148
+ moveTo(event.clientX, event.clientY);
149
+ }
150
+ });
151
+ environment.scope.listenWindow('mouseup', () => {
152
+ if (isMouseHeld) {
153
+ isMouseHeld = false;
154
+ release();
155
+ }
156
+ });
157
+ }
158
+
159
+ /** Drag-to-move while editing: the control keeps its grab point under the finger and stays fully on-screen. */
160
+ function bindEditDrag(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
161
+ let isDragging = false;
162
+ let grabX = 0;
163
+ let grabY = 0;
164
+ let droppedSpot: { x: number; y: number } | null = null;
165
+ const start = (pointerX: number, pointerY: number): void => {
166
+ if (!environment.isEditing()) {
167
+ return;
168
+ }
169
+ isDragging = true;
170
+ const bounds = element.getBoundingClientRect();
171
+ grabX = pointerX - bounds.left;
172
+ grabY = pointerY - bounds.top;
173
+ droppedSpot = null;
174
+ };
175
+ const move = (pointerX: number, pointerY: number): void => {
176
+ if (!isDragging) {
177
+ return;
178
+ }
179
+ const viewportWidth = window.innerWidth;
180
+ const viewportHeight = window.innerHeight;
181
+ const left = Math.max(0, Math.min(viewportWidth - element.offsetWidth, pointerX - grabX));
182
+ const top = Math.max(0, Math.min(viewportHeight - element.offsetHeight, pointerY - grabY));
183
+ placeAt(element, left, top);
184
+ droppedSpot = { x: (left / viewportWidth) * 100, y: (top / viewportHeight) * 100 };
185
+ };
186
+ const end = (): void => {
187
+ if (!isDragging) {
188
+ return;
189
+ }
190
+ isDragging = false;
191
+ if (droppedSpot !== null) {
192
+ environment.callbacks.onDragged(id, droppedSpot);
193
+ }
194
+ };
195
+ element.addEventListener('mousedown', (event) => {
196
+ start(event.clientX, event.clientY);
197
+ });
198
+ element.addEventListener('touchstart', (event) => {
199
+ const touch = firstTouch(event);
200
+ if (touch !== undefined) {
201
+ start(touch.clientX, touch.clientY);
202
+ }
203
+ }, { passive: true });
204
+ environment.scope.listenWindow('mousemove', (event) => {
205
+ move(event.clientX, event.clientY);
206
+ });
207
+ environment.scope.listenWindow('touchmove', (event) => {
208
+ const touch = firstTouch(event);
209
+ if (isDragging && touch !== undefined) {
210
+ move(touch.clientX, touch.clientY);
211
+ }
212
+ });
213
+ environment.scope.listenWindow('mouseup', end);
214
+ environment.scope.listenWindow('touchend', end);
215
+ }
216
+
217
+ /** Build one on-screen control element, wired up. */
218
+ export function createVirtualControl(definition: VirtualDef, environment: VirtualControlEnvironment): HTMLElement {
219
+ const isStick = isStickType(definition.type);
220
+ const { w: width, h: height } = declaredSize(definition);
221
+ const wrapper = document.createElement('div');
222
+ wrapper.className = `vc vc-${definition.type}`;
223
+ wrapper.dataset['controlId'] = definition.id;
224
+ // The v1 expando tests and tools read the control id from.
225
+ Reflect.set(wrapper, '__id', definition.id);
226
+ wrapper.style.width = `${String(width)}px`;
227
+ wrapper.style.height = `${String(height)}px`;
228
+ if (definition.opacity !== undefined) {
229
+ wrapper.style.opacity = String(definition.opacity);
230
+ }
231
+ wrapper.style.setProperty('--vc-color', definition.color ?? ACCENT_COLOR);
232
+ if (definition.bg !== undefined) {
233
+ wrapper.style.setProperty('--vc-bg', definition.bg);
234
+ }
235
+ if (definition.text !== undefined) {
236
+ wrapper.style.color = definition.text;
237
+ }
238
+ const radius = RADIUS_BY_SHAPE[definition.shape ?? (isStick ? 'circle' : 'round')];
239
+ wrapper.style.borderRadius = radius;
240
+ if (isStick) {
241
+ wrapper.innerHTML = '<div class="base"></div><div class="knob"></div>';
242
+ requireElement(wrapper, '.base', HTMLElement).style.borderRadius = radius;
243
+ // The knob stays a circle sized to the short edge (a wide pill stick gets a
244
+ // round knob that slides). Single-axis sticks get a knob that mostly fills
245
+ // the short edge; 2D sticks a smaller one with room to travel.
246
+ const knob = requireElement(wrapper, '.knob', HTMLElement);
247
+ const isSingleAxis = definition.axis === 'x' || definition.axis === 'y';
248
+ const knobDiameter = Math.round(Math.min(width, height) * (isSingleAxis ? 0.92 : 0.5));
249
+ knob.style.width = `${String(knobDiameter)}px`;
250
+ knob.style.height = `${String(knobDiameter)}px`;
251
+ bindStick(definition.id, wrapper, knob, definition, environment);
252
+ } else {
253
+ // The game's own markup (an SVG icon, an entity in a label), inserted as given.
254
+ wrapper.innerHTML = definition.html ?? `<span>${definition.label ?? ''}</span>`;
255
+ if (definition.type === 'tap') {
256
+ wrapper.classList.add('vc-transparent');
257
+ }
258
+ bindButton(definition.id, wrapper, environment);
259
+ }
260
+ bindEditDrag(definition.id, wrapper, environment);
261
+ return wrapper;
262
+ }
package/src/race.ts CHANGED
@@ -1,10 +1,10 @@
1
- /**
2
- * `@diffenderfer-games/hub/race`: the race forwarder (`hub.race`), for games
3
- * that want only this namespace.
4
- *
5
- * @packageDocumentation
6
- */
7
- export { raceForwarder as race } from './realtime/race-forwarder';
8
- export type * from './realtime/race-types';
9
- export type { RaceHistory, RaceRecordPlayer, RaceResultPlayer, RaceStatusEvent } from './realtime/wire-types';
10
- export type { RaceOutcome, RaceParams, RaceRecord, RaceResult, RaceState, RaceStats, RaceStatValue, Racer, RacerResult, RacerState, RoomInfo } from '@diffenderfer-games/hub-contract';
1
+ /**
2
+ * `@diffenderfer-games/hub/race`: the race forwarder (`hub.race`), for games
3
+ * that want only this namespace.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ export { raceForwarder as race } from './realtime/race-forwarder';
8
+ export type * from './realtime/race-types';
9
+ export type { RaceHistory, RaceRecordPlayer, RaceResultPlayer, RaceStatusEvent } from './realtime/wire-types';
10
+ export type { RaceOutcome, RaceParams, RaceRecord, RaceResult, RaceState, RaceStats, RaceStatValue, Racer, RacerResult, RacerState, RoomInfo } from '@diffenderfer-games/hub-contract';