@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,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';