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