@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,146 +1,146 @@
1
- /**
2
- * The raw state of every input device, updated by DOM events and sampled once
3
- * per frame: held keys and mouse buttons, the pointer, movement and wheel
4
- * accumulators, one-frame pulses, on-screen sticks and buttons, and gamepads.
5
- */
6
- import { AXIS_DEADZONE, readGamepads } from './gamepad';
7
-
8
- /** An on-screen stick's output, -1..1 per axis. */
9
- export interface StickOutput {
10
- outX: number;
11
- outY: number;
12
- }
13
-
14
- /** A 2D accumulator. */
15
- interface Delta {
16
- dx: number;
17
- dy: number;
18
- }
19
-
20
- /** Keys that scroll the page; cancelled during play (not while typing). */
21
- const SCROLL_KEYS = new Set([' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright']);
22
-
23
- /** Input types that don't take text (buttons, checkboxes, sliders…). */
24
- const NON_TEXT_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'range', 'color', 'file', 'image']);
25
-
26
- /** Whether typing in `target` owns Space and the arrow keys. */
27
- export function isTypingTarget(target: EventTarget | null): boolean {
28
- if (!(target instanceof HTMLElement)) {
29
- return false;
30
- }
31
- if (target.isContentEditable || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
32
- return true;
33
- }
34
- return target instanceof HTMLInputElement && !NON_TEXT_INPUT_TYPES.has(target.type);
35
- }
36
-
37
- /** What the device state reports back to the input system. */
38
- export interface DeviceStateEvents {
39
- /** The mouse moved or clicked: it is the active pointer. */
40
- onMouseActivity(): void;
41
- /** A gamepad connected (`true`) or disconnected (`false`). */
42
- onGamepadConnection(isConnected: boolean): void;
43
- /** Whether keydown should cancel scrolling keys (a game defined its inputs). */
44
- shouldCancelScrollKeys(): boolean;
45
- }
46
-
47
- /** Raw device state for one page. */
48
- export class DeviceState {
49
- readonly heldKeys = new Set<string>();
50
- readonly heldMouseButtons = new Set<number>();
51
- readonly pressedVirtualButtons = new Set<string>();
52
- readonly sticks = new Map<string, StickOutput>();
53
- pointerX = 0;
54
- pointerY = 0;
55
- readonly movement: Delta = { dx: 0, dy: 0 };
56
- readonly wheel: Delta = { dx: 0, dy: 0 };
57
- gamepads: Gamepad[] = [];
58
- frame = 0;
59
- private readonly pulseFrames = new Map<string, number>();
60
-
61
- /** Fire a one-frame pulse (`tap`, `mouse:dbl`). */
62
- pulse(name: string): void {
63
- this.pulseFrames.set(name, this.frame);
64
- }
65
-
66
- /** Whether a pulse fired this frame or the last. */
67
- isPulseActive(name: string): boolean {
68
- const firedAt = this.pulseFrames.get(name);
69
- return firedAt === this.frame || firedAt === this.frame - 1;
70
- }
71
-
72
- /** Poll the gamepads. Returns whether any are connected and whether any moved. */
73
- pollGamepads(): { isConnected: boolean; hasActivity: boolean } {
74
- this.gamepads = readGamepads();
75
- const hasActivity = this.gamepads.some((pad) => pad.buttons.some((button) => button.pressed || button.value > 0.5)
76
- || pad.axes.some((axis) => Math.abs(axis) > AXIS_DEADZONE));
77
- return { isConnected: this.gamepads.length > 0, hasActivity };
78
- }
79
-
80
- /** End-of-frame decay of the relative accumulators. */
81
- decay(): void {
82
- this.movement.dx = 0;
83
- this.movement.dy = 0;
84
- this.wheel.dx *= 0.6;
85
- this.wheel.dy *= 0.6;
86
- }
87
-
88
- /** Release everything held (focus loss, hidden tab), so nothing sticks. */
89
- releaseAll(): void {
90
- this.heldKeys.clear();
91
- this.heldMouseButtons.clear();
92
- this.pressedVirtualButtons.clear();
93
- for (const stick of this.sticks.values()) {
94
- stick.outX = 0;
95
- stick.outY = 0;
96
- }
97
- }
98
-
99
- /** Wire the DOM listeners that feed this state. */
100
- listen(events: DeviceStateEvents): void {
101
- if (typeof window === 'undefined') {
102
- return;
103
- }
104
- window.addEventListener('keydown', (event) => {
105
- const key = event.key.toLowerCase();
106
- this.heldKeys.add(key);
107
- if (events.shouldCancelScrollKeys() && SCROLL_KEYS.has(key) && !isTypingTarget(event.target)) {
108
- event.preventDefault();
109
- }
110
- });
111
- window.addEventListener('keyup', (event) => this.heldKeys.delete(event.key.toLowerCase()));
112
- window.addEventListener('mousemove', (event) => {
113
- this.movement.dx += event.movementX;
114
- this.movement.dy += event.movementY;
115
- this.pointerX = event.clientX;
116
- this.pointerY = event.clientY;
117
- events.onMouseActivity();
118
- });
119
- window.addEventListener('mousedown', (event) => {
120
- this.heldMouseButtons.add(event.button);
121
- events.onMouseActivity();
122
- });
123
- window.addEventListener('mouseup', (event) => this.heldMouseButtons.delete(event.button));
124
- window.addEventListener('dblclick', () => {
125
- this.pulse('mouse:dbl');
126
- });
127
- window.addEventListener('wheel', (event) => {
128
- this.wheel.dx += Math.sign(event.deltaX);
129
- this.wheel.dy += Math.sign(event.deltaY);
130
- }, { passive: true });
131
- window.addEventListener('blur', () => {
132
- this.releaseAll();
133
- });
134
- document.addEventListener('visibilitychange', () => {
135
- if (document.hidden) {
136
- this.releaseAll();
137
- }
138
- });
139
- window.addEventListener('gamepadconnected', () => {
140
- events.onGamepadConnection(true);
141
- });
142
- window.addEventListener('gamepaddisconnected', () => {
143
- events.onGamepadConnection(false);
144
- });
145
- }
146
- }
1
+ /**
2
+ * The raw state of every input device, updated by DOM events and sampled once
3
+ * per frame: held keys and mouse buttons, the pointer, movement and wheel
4
+ * accumulators, one-frame pulses, on-screen sticks and buttons, and gamepads.
5
+ */
6
+ import { AXIS_DEADZONE, readGamepads } from './gamepad';
7
+
8
+ /** An on-screen stick's output, -1..1 per axis. */
9
+ export interface StickOutput {
10
+ outX: number;
11
+ outY: number;
12
+ }
13
+
14
+ /** A 2D accumulator. */
15
+ interface Delta {
16
+ dx: number;
17
+ dy: number;
18
+ }
19
+
20
+ /** Keys that scroll the page; cancelled during play (not while typing). */
21
+ const SCROLL_KEYS = new Set([' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright']);
22
+
23
+ /** Input types that don't take text (buttons, checkboxes, sliders…). */
24
+ const NON_TEXT_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'range', 'color', 'file', 'image']);
25
+
26
+ /** Whether typing in `target` owns Space and the arrow keys. */
27
+ export function isTypingTarget(target: EventTarget | null): boolean {
28
+ if (!(target instanceof HTMLElement)) {
29
+ return false;
30
+ }
31
+ if (target.isContentEditable || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
32
+ return true;
33
+ }
34
+ return target instanceof HTMLInputElement && !NON_TEXT_INPUT_TYPES.has(target.type);
35
+ }
36
+
37
+ /** What the device state reports back to the input system. */
38
+ export interface DeviceStateEvents {
39
+ /** The mouse moved or clicked: it is the active pointer. */
40
+ onMouseActivity(): void;
41
+ /** A gamepad connected (`true`) or disconnected (`false`). */
42
+ onGamepadConnection(isConnected: boolean): void;
43
+ /** Whether keydown should cancel scrolling keys (a game defined its inputs). */
44
+ shouldCancelScrollKeys(): boolean;
45
+ }
46
+
47
+ /** Raw device state for one page. */
48
+ export class DeviceState {
49
+ readonly heldKeys = new Set<string>();
50
+ readonly heldMouseButtons = new Set<number>();
51
+ readonly pressedVirtualButtons = new Set<string>();
52
+ readonly sticks = new Map<string, StickOutput>();
53
+ pointerX = 0;
54
+ pointerY = 0;
55
+ readonly movement: Delta = { dx: 0, dy: 0 };
56
+ readonly wheel: Delta = { dx: 0, dy: 0 };
57
+ gamepads: Gamepad[] = [];
58
+ frame = 0;
59
+ private readonly pulseFrames = new Map<string, number>();
60
+
61
+ /** Fire a one-frame pulse (`tap`, `mouse:dbl`). */
62
+ pulse(name: string): void {
63
+ this.pulseFrames.set(name, this.frame);
64
+ }
65
+
66
+ /** Whether a pulse fired this frame or the last. */
67
+ isPulseActive(name: string): boolean {
68
+ const firedAt = this.pulseFrames.get(name);
69
+ return firedAt === this.frame || firedAt === this.frame - 1;
70
+ }
71
+
72
+ /** Poll the gamepads. Returns whether any are connected and whether any moved. */
73
+ pollGamepads(): { isConnected: boolean; hasActivity: boolean } {
74
+ this.gamepads = readGamepads();
75
+ const hasActivity = this.gamepads.some((pad) => pad.buttons.some((button) => button.pressed || button.value > 0.5)
76
+ || pad.axes.some((axis) => Math.abs(axis) > AXIS_DEADZONE));
77
+ return { isConnected: this.gamepads.length > 0, hasActivity };
78
+ }
79
+
80
+ /** End-of-frame decay of the relative accumulators. */
81
+ decay(): void {
82
+ this.movement.dx = 0;
83
+ this.movement.dy = 0;
84
+ this.wheel.dx *= 0.6;
85
+ this.wheel.dy *= 0.6;
86
+ }
87
+
88
+ /** Release everything held (focus loss, hidden tab), so nothing sticks. */
89
+ releaseAll(): void {
90
+ this.heldKeys.clear();
91
+ this.heldMouseButtons.clear();
92
+ this.pressedVirtualButtons.clear();
93
+ for (const stick of this.sticks.values()) {
94
+ stick.outX = 0;
95
+ stick.outY = 0;
96
+ }
97
+ }
98
+
99
+ /** Wire the DOM listeners that feed this state. */
100
+ listen(events: DeviceStateEvents): void {
101
+ if (typeof window === 'undefined') {
102
+ return;
103
+ }
104
+ window.addEventListener('keydown', (event) => {
105
+ const key = event.key.toLowerCase();
106
+ this.heldKeys.add(key);
107
+ if (events.shouldCancelScrollKeys() && SCROLL_KEYS.has(key) && !isTypingTarget(event.target)) {
108
+ event.preventDefault();
109
+ }
110
+ });
111
+ window.addEventListener('keyup', (event) => this.heldKeys.delete(event.key.toLowerCase()));
112
+ window.addEventListener('mousemove', (event) => {
113
+ this.movement.dx += event.movementX;
114
+ this.movement.dy += event.movementY;
115
+ this.pointerX = event.clientX;
116
+ this.pointerY = event.clientY;
117
+ events.onMouseActivity();
118
+ });
119
+ window.addEventListener('mousedown', (event) => {
120
+ this.heldMouseButtons.add(event.button);
121
+ events.onMouseActivity();
122
+ });
123
+ window.addEventListener('mouseup', (event) => this.heldMouseButtons.delete(event.button));
124
+ window.addEventListener('dblclick', () => {
125
+ this.pulse('mouse:dbl');
126
+ });
127
+ window.addEventListener('wheel', (event) => {
128
+ this.wheel.dx += Math.sign(event.deltaX);
129
+ this.wheel.dy += Math.sign(event.deltaY);
130
+ }, { passive: true });
131
+ window.addEventListener('blur', () => {
132
+ this.releaseAll();
133
+ });
134
+ document.addEventListener('visibilitychange', () => {
135
+ if (document.hidden) {
136
+ this.releaseAll();
137
+ }
138
+ });
139
+ window.addEventListener('gamepadconnected', () => {
140
+ events.onGamepadConnection(true);
141
+ });
142
+ window.addEventListener('gamepaddisconnected', () => {
143
+ events.onGamepadConnection(false);
144
+ });
145
+ }
146
+ }
@@ -1,55 +1,55 @@
1
- /**
2
- * Edge detection for polled buttons: a plain rising edge, and a rising edge
3
- * that repeats while held (menu navigation: a first step, a pause, then steady
4
- * steps).
5
- */
6
-
7
- /** Delay before a held direction starts repeating. */
8
- const FIRST_REPEAT_DELAY_MS = 320;
9
- /** Interval between repeats while held. */
10
- const REPEAT_INTERVAL_MS = 130;
11
-
12
- /** A monotonic clock in ms. */
13
- export function nowMs(): number {
14
- return typeof performance === 'undefined' ? Date.now() : performance.now();
15
- }
16
-
17
- /** Rising edges of named polled buttons. */
18
- export class RisingEdges {
19
- private readonly wasPressed = new Map<string, boolean>();
20
-
21
- /** Whether `key` went from released to pressed since the last call for it. */
22
- rose(key: string, isPressed: boolean): boolean {
23
- const wasPressedBefore = this.wasPressed.get(key) === true;
24
- this.wasPressed.set(key, isPressed);
25
- return isPressed && !wasPressedBefore;
26
- }
27
- }
28
-
29
- /** Rising edges that repeat while held. */
30
- export class RepeatingEdges {
31
- private readonly timers = new Map<string, { isHeld: boolean; nextAt: number }>();
32
-
33
- /** Whether `key` fires now: on the press, then after a delay, then steadily while held. */
34
- fires(key: string, isPressed: boolean, at: number = nowMs()): boolean {
35
- let timer = this.timers.get(key);
36
- if (timer === undefined) {
37
- timer = { isHeld: false, nextAt: 0 };
38
- this.timers.set(key, timer);
39
- }
40
- if (!isPressed) {
41
- timer.isHeld = false;
42
- return false;
43
- }
44
- if (!timer.isHeld) {
45
- timer.isHeld = true;
46
- timer.nextAt = at + FIRST_REPEAT_DELAY_MS;
47
- return true;
48
- }
49
- if (at >= timer.nextAt) {
50
- timer.nextAt = at + REPEAT_INTERVAL_MS;
51
- return true;
52
- }
53
- return false;
54
- }
55
- }
1
+ /**
2
+ * Edge detection for polled buttons: a plain rising edge, and a rising edge
3
+ * that repeats while held (menu navigation: a first step, a pause, then steady
4
+ * steps).
5
+ */
6
+
7
+ /** Delay before a held direction starts repeating. */
8
+ const FIRST_REPEAT_DELAY_MS = 320;
9
+ /** Interval between repeats while held. */
10
+ const REPEAT_INTERVAL_MS = 130;
11
+
12
+ /** A monotonic clock in ms. */
13
+ export function nowMs(): number {
14
+ return typeof performance === 'undefined' ? Date.now() : performance.now();
15
+ }
16
+
17
+ /** Rising edges of named polled buttons. */
18
+ export class RisingEdges {
19
+ private readonly wasPressed = new Map<string, boolean>();
20
+
21
+ /** Whether `key` went from released to pressed since the last call for it. */
22
+ rose(key: string, isPressed: boolean): boolean {
23
+ const wasPressedBefore = this.wasPressed.get(key) === true;
24
+ this.wasPressed.set(key, isPressed);
25
+ return isPressed && !wasPressedBefore;
26
+ }
27
+ }
28
+
29
+ /** Rising edges that repeat while held. */
30
+ export class RepeatingEdges {
31
+ private readonly timers = new Map<string, { isHeld: boolean; nextAt: number }>();
32
+
33
+ /** Whether `key` fires now: on the press, then after a delay, then steadily while held. */
34
+ fires(key: string, isPressed: boolean, at: number = nowMs()): boolean {
35
+ let timer = this.timers.get(key);
36
+ if (timer === undefined) {
37
+ timer = { isHeld: false, nextAt: 0 };
38
+ this.timers.set(key, timer);
39
+ }
40
+ if (!isPressed) {
41
+ timer.isHeld = false;
42
+ return false;
43
+ }
44
+ if (!timer.isHeld) {
45
+ timer.isHeld = true;
46
+ timer.nextAt = at + FIRST_REPEAT_DELAY_MS;
47
+ return true;
48
+ }
49
+ if (at >= timer.nextAt) {
50
+ timer.nextAt = at + REPEAT_INTERVAL_MS;
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The hub's focus ring: a two-tone box-shadow (a dark gap, then a light ring)
3
+ * that follows the element's own border-radius and shows on light and dark
4
+ * buttons alike. Pages tune it with CSS variables:
5
+ * `--hub-focus-color` (the ring), `--hub-focus-inner` (the gap),
6
+ * `--hub-focus-width` and `--hub-focus-gap`.
7
+ */
8
+
9
+ const GAP = 'var(--hub-focus-gap, 2px)';
10
+ const WIDTH = 'var(--hub-focus-width, 3px)';
11
+
12
+ /** The ring as a `box-shadow` value. */
13
+ export const FOCUS_RING_SHADOW = `0 0 0 ${GAP} var(--hub-focus-inner, rgba(13, 15, 20, 0.85)), 0 0 0 calc(${GAP} + ${WIDTH}) var(--hub-focus-color, #fff)`;
@@ -1,61 +1,61 @@
1
- /**
2
- * Reading standard gamepads: button indices, the stick deadzone and the
3
- * directions a pad is pointing.
4
- */
5
-
6
- /** Standard-gamepad button indices. */
7
- export const PAD_BUTTON = {
8
- A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, SELECT: 8, START: 9, UP: 12, DOWN: 13, LEFT: 14, RIGHT: 15,
9
- } as const;
10
-
11
- /** Stick deflection below this is ignored. */
12
- export const AXIS_DEADZONE = 0.35;
13
-
14
- /** A screen direction. */
15
- export type Direction = 'up' | 'down' | 'left' | 'right';
16
-
17
- /** Which directions are held. */
18
- export type HeldDirections = Record<Direction, boolean>;
19
-
20
- /** The connected gamepads (the browser's list has holes). */
21
- export function readGamepads(): Gamepad[] {
22
- if (typeof navigator === 'undefined' || typeof navigator.getGamepads !== 'function') {
23
- return [];
24
- }
25
- return navigator.getGamepads().filter((pad): pad is Gamepad => pad !== null);
26
- }
27
-
28
- /** Whether `pad` holds button `index`. */
29
- export function isPadButtonPressed(pad: Gamepad, index: number): boolean {
30
- return pad.buttons[index]?.pressed === true;
31
- }
32
-
33
- /** Whether any pad holds button `index`. */
34
- export function isAnyPadButtonPressed(pads: readonly Gamepad[], index: number): boolean {
35
- return pads.some((pad) => isPadButtonPressed(pad, index));
36
- }
37
-
38
- /** The directions one pad points (left stick past `deadzone`, or the d-pad). */
39
- export function padDirections(pad: Gamepad, deadzone: number): HeldDirections {
40
- const horizontal = pad.axes[0] ?? 0;
41
- const vertical = pad.axes[1] ?? 0;
42
- return {
43
- up: vertical < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.UP),
44
- down: vertical > deadzone || isPadButtonPressed(pad, PAD_BUTTON.DOWN),
45
- left: horizontal < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.LEFT),
46
- right: horizontal > deadzone || isPadButtonPressed(pad, PAD_BUTTON.RIGHT),
47
- };
48
- }
49
-
50
- /** The directions any pad points. */
51
- export function anyPadDirections(pads: readonly Gamepad[], deadzone: number): HeldDirections {
52
- const held: HeldDirections = { up: false, down: false, left: false, right: false };
53
- for (const pad of pads) {
54
- const directions = padDirections(pad, deadzone);
55
- held.up ||= directions.up;
56
- held.down ||= directions.down;
57
- held.left ||= directions.left;
58
- held.right ||= directions.right;
59
- }
60
- return held;
61
- }
1
+ /**
2
+ * Reading standard gamepads: button indices, the stick deadzone and the
3
+ * directions a pad is pointing.
4
+ */
5
+
6
+ /** Standard-gamepad button indices. */
7
+ export const PAD_BUTTON = {
8
+ A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, SELECT: 8, START: 9, UP: 12, DOWN: 13, LEFT: 14, RIGHT: 15,
9
+ } as const;
10
+
11
+ /** Stick deflection below this is ignored. */
12
+ export const AXIS_DEADZONE = 0.35;
13
+
14
+ /** A screen direction. */
15
+ export type Direction = 'up' | 'down' | 'left' | 'right';
16
+
17
+ /** Which directions are held. */
18
+ export type HeldDirections = Record<Direction, boolean>;
19
+
20
+ /** The connected gamepads (the browser's list has holes). */
21
+ export function readGamepads(): Gamepad[] {
22
+ if (typeof navigator === 'undefined' || typeof navigator.getGamepads !== 'function') {
23
+ return [];
24
+ }
25
+ return navigator.getGamepads().filter((pad): pad is Gamepad => pad !== null);
26
+ }
27
+
28
+ /** Whether `pad` holds button `index`. */
29
+ export function isPadButtonPressed(pad: Gamepad, index: number): boolean {
30
+ return pad.buttons[index]?.pressed === true;
31
+ }
32
+
33
+ /** Whether any pad holds button `index`. */
34
+ export function isAnyPadButtonPressed(pads: readonly Gamepad[], index: number): boolean {
35
+ return pads.some((pad) => isPadButtonPressed(pad, index));
36
+ }
37
+
38
+ /** The directions one pad points (left stick past `deadzone`, or the d-pad). */
39
+ export function padDirections(pad: Gamepad, deadzone: number): HeldDirections {
40
+ const horizontal = pad.axes[0] ?? 0;
41
+ const vertical = pad.axes[1] ?? 0;
42
+ return {
43
+ up: vertical < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.UP),
44
+ down: vertical > deadzone || isPadButtonPressed(pad, PAD_BUTTON.DOWN),
45
+ left: horizontal < -deadzone || isPadButtonPressed(pad, PAD_BUTTON.LEFT),
46
+ right: horizontal > deadzone || isPadButtonPressed(pad, PAD_BUTTON.RIGHT),
47
+ };
48
+ }
49
+
50
+ /** The directions any pad points. */
51
+ export function anyPadDirections(pads: readonly Gamepad[], deadzone: number): HeldDirections {
52
+ const held: HeldDirections = { up: false, down: false, left: false, right: false };
53
+ for (const pad of pads) {
54
+ const directions = padDirections(pad, deadzone);
55
+ held.up ||= directions.up;
56
+ held.down ||= directions.down;
57
+ held.left ||= directions.left;
58
+ held.right ||= directions.right;
59
+ }
60
+ return held;
61
+ }
@@ -1,32 +1,32 @@
1
- /**
2
- * The input system's events (`hub.input.on`): typed listeners per event,
3
- * isolated so one failing listener can't stop the frame loop.
4
- */
5
- import type { InputEvents } from './types';
6
-
7
- /** Listeners by event name. */
8
- export class InputEventHub {
9
- private readonly listeners: { [Name in keyof InputEvents]: Set<(payload: InputEvents[Name]) => void> } = {
10
- focus: new Set(), blur: new Set(), act: new Set(), sourcechange: new Set(), navigate: new Set(),
11
- };
12
-
13
- /** Subscribe. Returns the unsubscribe function. */
14
- on<Name extends keyof InputEvents>(event: Name, listener: (payload: InputEvents[Name]) => void): () => void {
15
- const registered = this.listeners[event];
16
- registered.add(listener);
17
- return () => {
18
- registered.delete(listener);
19
- };
20
- }
21
-
22
- /** Deliver an event to every listener. */
23
- emit<Name extends keyof InputEvents>(event: Name, payload: InputEvents[Name]): void {
24
- for (const listener of [...this.listeners[event]]) {
25
- try {
26
- listener(payload);
27
- } catch {
28
- // A game's listener failing must not stop the frame loop.
29
- }
30
- }
31
- }
32
- }
1
+ /**
2
+ * The input system's events (`hub.input.on`): typed listeners per event,
3
+ * isolated so one failing listener can't stop the frame loop.
4
+ */
5
+ import type { InputEvents } from './types';
6
+
7
+ /** Listeners by event name. */
8
+ export class InputEventHub {
9
+ private readonly listeners: { [Name in keyof InputEvents]: Set<(payload: InputEvents[Name]) => void> } = {
10
+ focus: new Set(), blur: new Set(), act: new Set(), sourcechange: new Set(), navigate: new Set(),
11
+ };
12
+
13
+ /** Subscribe. Returns the unsubscribe function. */
14
+ on<Name extends keyof InputEvents>(event: Name, listener: (payload: InputEvents[Name]) => void): () => void {
15
+ const registered = this.listeners[event];
16
+ registered.add(listener);
17
+ return () => {
18
+ registered.delete(listener);
19
+ };
20
+ }
21
+
22
+ /** Deliver an event to every listener. */
23
+ emit<Name extends keyof InputEvents>(event: Name, payload: InputEvents[Name]): void {
24
+ for (const listener of [...this.listeners[event]]) {
25
+ try {
26
+ listener(payload);
27
+ } catch {
28
+ // A game's listener failing must not stop the frame loop.
29
+ }
30
+ }
31
+ }
32
+ }