@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,177 +1,164 @@
1
- /**
2
- * The zero-config fallback for games that never call `hub.input.define()`:
3
- * on the catalog page a gamepad moves a highlight across the game tiles; in
4
- * a game it presses the keys `game.controls` maps its buttons to. Runs each
5
- * frame only while the menu is closed and no game has taken over input.
6
- */
7
- import type { RepeatingEdges } from './edges';
8
- import { anyPadDirections, isAnyPadButtonPressed, isPadButtonPressed, PAD_BUTTON, padDirections } from './gamepad';
9
- import type { Direction } from './gamepad';
10
- import type { LegacyControls } from './legacy-controls';
11
- import { controlMapFor } from './legacy-controls';
12
- import { findNearestInDirection, rectOf } from './spatial';
13
- import { isTypingTarget } from './device-state';
14
-
15
- /** Synthetic keys stay pressed past half deflection. */
16
- const SYNTH_DEADZONE = 0.5;
17
- const FOCUS_CLASS = 'gp-focus';
18
- const FOCUS_STYLE_ID = 'gp-focus-style';
19
- const FOCUS_CSS = '.gp-focus{outline:3px solid #ff5f3b !important;outline-offset:2px;border-radius:4px;}';
20
-
21
- /** `KeyboardEvent.code` → its `key` and legacy `keyCode`. */
22
- const KEY_FOR_CODE = new Map<string, readonly [string, number]>([
23
- ['ArrowUp', ['ArrowUp', 38]], ['ArrowDown', ['ArrowDown', 40]], ['ArrowLeft', ['ArrowLeft', 37]], ['ArrowRight', ['ArrowRight', 39]],
24
- ['Space', [' ', 32]], ['Enter', ['Enter', 13]], ['Escape', ['Escape', 27]], ['Tab', ['Tab', 9]], ['Backspace', ['Backspace', 8]],
25
- ['ShiftLeft', ['Shift', 16]], ['ControlLeft', ['Control', 17]], ['AltLeft', ['Alt', 18]],
26
- ]);
27
- for (let code = 65; code <= 90; code += 1) {
28
- const letter = String.fromCharCode(code);
29
- KEY_FOR_CODE.set(`Key${letter}`, [letter.toLowerCase(), code]);
30
- }
31
- for (let digit = 0; digit <= 9; digit += 1) {
32
- KEY_FOR_CODE.set(`Digit${String(digit)}`, [String(digit), 48 + digit]);
33
- }
34
-
35
- /** What the fallback reads from the input system each frame. */
36
- export interface ZeroConfigFrame {
37
- readonly gamepads: readonly Gamepad[];
38
- readonly slug: string | null;
39
- readonly controls: LegacyControls;
40
- readonly navEdges: RepeatingEdges;
41
- }
42
-
43
- /** Dispatch a synthetic key event (with the legacy `keyCode`/`which` old games read) on the focused element. */
44
- function dispatchKey(type: 'keydown' | 'keyup', code: string): void {
45
- const [key, keyCode] = KEY_FOR_CODE.get(code) ?? [code, 0];
46
- const active = document.activeElement;
47
- const target: EventTarget = active !== null && active !== document.body ? active : document.body;
48
- const event = new KeyboardEvent(type, { key, code, bubbles: true, cancelable: true });
49
- Object.defineProperty(event, 'keyCode', { get: () => keyCode });
50
- Object.defineProperty(event, 'which', { get: () => keyCode });
51
- target.dispatchEvent(event);
52
- }
53
-
54
- /** Add the highlight style to the page once. */
55
- function ensureFocusStyle(): void {
56
- if (document.getElementById(FOCUS_STYLE_ID) !== null) {
57
- return;
58
- }
59
- const style = document.createElement('style');
60
- style.id = FOCUS_STYLE_ID;
61
- style.textContent = FOCUS_CSS;
62
- document.head.appendChild(style);
63
- }
64
-
65
- /** The keys each pad holds down, by pad index. */
66
- const heldCodes = new Map<number, Set<string>>();
67
-
68
- function releaseAllKeys(): void {
69
- for (const [padIndex, codes] of heldCodes) {
70
- for (const code of codes) {
71
- dispatchKey('keyup', code);
72
- }
73
- heldCodes.set(padIndex, new Set());
74
- }
75
- }
76
-
77
- /** In a game: press and release the mapped keys to match each pad. */
78
- function synthesizeKeys(frame: ZeroConfigFrame): void {
79
- if (isTypingTarget(document.activeElement)) {
80
- releaseAllKeys();
81
- return;
82
- }
83
- frame.gamepads.forEach((pad, padIndex) => {
84
- const map = controlMapFor(frame.controls, padIndex);
85
- const directions = padDirections(pad, SYNTH_DEADZONE);
86
- const wanted = new Set<string>();
87
- for (const direction of ['up', 'down', 'left', 'right'] as const) {
88
- if (directions[direction]) {
89
- wanted.add(map[direction]);
90
- }
91
- }
92
- for (const [buttonIndex, code] of map.buttons) {
93
- if (isPadButtonPressed(pad, buttonIndex)) {
94
- wanted.add(code);
95
- }
96
- }
97
- const held = heldCodes.get(padIndex) ?? new Set<string>();
98
- wanted.forEach((code) => {
99
- if (!held.has(code)) {
100
- dispatchKey('keydown', code);
101
- }
102
- });
103
- held.forEach((code) => {
104
- if (!wanted.has(code)) {
105
- dispatchKey('keyup', code);
106
- }
107
- });
108
- heldCodes.set(padIndex, wanted);
109
- });
110
- }
111
-
112
- /** The visible game tiles on the catalog page. */
113
- function visibleTiles(): HTMLElement[] {
114
- return [...document.querySelectorAll<HTMLElement>('.grid .card')].filter((card) => card.offsetParent !== null);
115
- }
116
-
117
- function highlightTile(tiles: readonly HTMLElement[], index: number): void {
118
- tiles.forEach((tile) => {
119
- tile.classList.remove(FOCUS_CLASS);
120
- });
121
- const tile = tiles[index];
122
- tile?.classList.add(FOCUS_CLASS);
123
- tile?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
124
- }
125
-
126
- /** On the catalog page: move the highlight across the tiles; A or Start opens one. */
127
- function navigateCatalog(frame: ZeroConfigFrame): void {
128
- const held = anyPadDirections(frame.gamepads, SYNTH_DEADZONE);
129
- const isAccepting = isAnyPadButtonPressed(frame.gamepads, PAD_BUTTON.A) || isAnyPadButtonPressed(frame.gamepads, PAD_BUTTON.START);
130
- const fired: Record<Direction, boolean> = {
131
- up: frame.navEdges.fires('lu', held.up),
132
- down: frame.navEdges.fires('ld', held.down),
133
- left: frame.navEdges.fires('ll', held.left),
134
- right: frame.navEdges.fires('lr', held.right),
135
- };
136
- const shouldAccept = frame.navEdges.fires('la', isAccepting) && isAccepting;
137
- const tiles = visibleTiles();
138
- if (tiles.length === 0) {
139
- return;
140
- }
141
- const focused = tiles.findIndex((tile) => tile.classList.contains(FOCUS_CLASS));
142
- const focusedTile = tiles[focused];
143
- if (focusedTile === undefined) {
144
- highlightTile(tiles, 0);
145
- return;
146
- }
147
- if (shouldAccept) {
148
- const href = focusedTile.getAttribute('href');
149
- if (href === null) {
150
- focusedTile.click();
151
- } else {
152
- location.href = href;
153
- }
154
- return;
155
- }
156
- const direction = (['up', 'down', 'left', 'right'] as const).find((candidate) => fired[candidate]);
157
- if (direction === undefined) {
158
- return;
159
- }
160
- const next = findNearestInDirection(tiles.map(rectOf), focused, direction);
161
- if (next >= 0) {
162
- highlightTile(tiles, next);
163
- }
164
- }
165
-
166
- /** One frame of the zero-config fallback. */
167
- export function runZeroConfigFrame(frame: ZeroConfigFrame): void {
168
- if (frame.gamepads.length === 0) {
169
- return;
170
- }
171
- ensureFocusStyle();
172
- if (frame.slug === null) {
173
- navigateCatalog(frame);
174
- } else {
175
- synthesizeKeys(frame);
176
- }
177
- }
1
+ /**
2
+ * The zero-config fallback for games that never call `hub.input.define()`:
3
+ * on the catalog page a gamepad moves a highlight across the game tiles; in
4
+ * a game it presses the keys `game.controls` maps its buttons to. Runs each
5
+ * frame only while the menu is closed and no game has taken over input.
6
+ */
7
+ import type { RepeatingEdges } from './edges';
8
+ import { anyPadDirections, isAnyPadButtonPressed, isPadButtonPressed, PAD_BUTTON, padDirections } from './gamepad';
9
+ import type { Direction } from './gamepad';
10
+ import type { LegacyControls } from './legacy-controls';
11
+ import { controlMapFor } from './legacy-controls';
12
+ import { findNearestInDirection, rectOf } from './spatial';
13
+ import { isTypingTarget } from './device-state';
14
+
15
+ /** Synthetic keys stay pressed past half deflection. */
16
+ const SYNTH_DEADZONE = 0.5;
17
+ /** The catalog tile highlight; its style (keyboard and gamepad players only) is in `modality.ts`. */
18
+ const FOCUS_CLASS = 'gp-focus';
19
+
20
+ /** `KeyboardEvent.code` → its `key` and legacy `keyCode`. */
21
+ const KEY_FOR_CODE = new Map<string, readonly [string, number]>([
22
+ ['ArrowUp', ['ArrowUp', 38]], ['ArrowDown', ['ArrowDown', 40]], ['ArrowLeft', ['ArrowLeft', 37]], ['ArrowRight', ['ArrowRight', 39]],
23
+ ['Space', [' ', 32]], ['Enter', ['Enter', 13]], ['Escape', ['Escape', 27]], ['Tab', ['Tab', 9]], ['Backspace', ['Backspace', 8]],
24
+ ['ShiftLeft', ['Shift', 16]], ['ControlLeft', ['Control', 17]], ['AltLeft', ['Alt', 18]],
25
+ ]);
26
+ for (let code = 65; code <= 90; code += 1) {
27
+ const letter = String.fromCharCode(code);
28
+ KEY_FOR_CODE.set(`Key${letter}`, [letter.toLowerCase(), code]);
29
+ }
30
+ for (let digit = 0; digit <= 9; digit += 1) {
31
+ KEY_FOR_CODE.set(`Digit${String(digit)}`, [String(digit), 48 + digit]);
32
+ }
33
+
34
+ /** What the fallback reads from the input system each frame. */
35
+ export interface ZeroConfigFrame {
36
+ readonly gamepads: readonly Gamepad[];
37
+ readonly slug: string | null;
38
+ readonly controls: LegacyControls;
39
+ readonly navEdges: RepeatingEdges;
40
+ }
41
+
42
+ /** Dispatch a synthetic key event (with the legacy `keyCode`/`which` old games read) on the focused element. */
43
+ function dispatchKey(type: 'keydown' | 'keyup', code: string): void {
44
+ const [key, keyCode] = KEY_FOR_CODE.get(code) ?? [code, 0];
45
+ const active = document.activeElement;
46
+ const target: EventTarget = active !== null && active !== document.body ? active : document.body;
47
+ const event = new KeyboardEvent(type, { key, code, bubbles: true, cancelable: true });
48
+ Object.defineProperty(event, 'keyCode', { get: () => keyCode });
49
+ Object.defineProperty(event, 'which', { get: () => keyCode });
50
+ target.dispatchEvent(event);
51
+ }
52
+
53
+ /** The keys each pad holds down, by pad index. */
54
+ const heldCodes = new Map<number, Set<string>>();
55
+
56
+ function releaseAllKeys(): void {
57
+ for (const [padIndex, codes] of heldCodes) {
58
+ for (const code of codes) {
59
+ dispatchKey('keyup', code);
60
+ }
61
+ heldCodes.set(padIndex, new Set());
62
+ }
63
+ }
64
+
65
+ /** In a game: press and release the mapped keys to match each pad. */
66
+ function synthesizeKeys(frame: ZeroConfigFrame): void {
67
+ if (isTypingTarget(document.activeElement)) {
68
+ releaseAllKeys();
69
+ return;
70
+ }
71
+ frame.gamepads.forEach((pad, padIndex) => {
72
+ const map = controlMapFor(frame.controls, padIndex);
73
+ const directions = padDirections(pad, SYNTH_DEADZONE);
74
+ const wanted = new Set<string>();
75
+ for (const direction of ['up', 'down', 'left', 'right'] as const) {
76
+ if (directions[direction]) {
77
+ wanted.add(map[direction]);
78
+ }
79
+ }
80
+ for (const [buttonIndex, code] of map.buttons) {
81
+ if (isPadButtonPressed(pad, buttonIndex)) {
82
+ wanted.add(code);
83
+ }
84
+ }
85
+ const held = heldCodes.get(padIndex) ?? new Set<string>();
86
+ wanted.forEach((code) => {
87
+ if (!held.has(code)) {
88
+ dispatchKey('keydown', code);
89
+ }
90
+ });
91
+ held.forEach((code) => {
92
+ if (!wanted.has(code)) {
93
+ dispatchKey('keyup', code);
94
+ }
95
+ });
96
+ heldCodes.set(padIndex, wanted);
97
+ });
98
+ }
99
+
100
+ /** The visible game tiles on the catalog page. */
101
+ function visibleTiles(): HTMLElement[] {
102
+ return [...document.querySelectorAll<HTMLElement>('.grid .card')].filter((card) => card.offsetParent !== null);
103
+ }
104
+
105
+ function highlightTile(tiles: readonly HTMLElement[], index: number): void {
106
+ tiles.forEach((tile) => {
107
+ tile.classList.remove(FOCUS_CLASS);
108
+ });
109
+ const tile = tiles[index];
110
+ tile?.classList.add(FOCUS_CLASS);
111
+ tile?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
112
+ }
113
+
114
+ /** On the catalog page: move the highlight across the tiles; A or Start opens one. */
115
+ function navigateCatalog(frame: ZeroConfigFrame): void {
116
+ const held = anyPadDirections(frame.gamepads, SYNTH_DEADZONE);
117
+ const isAccepting = isAnyPadButtonPressed(frame.gamepads, PAD_BUTTON.A) || isAnyPadButtonPressed(frame.gamepads, PAD_BUTTON.START);
118
+ const fired: Record<Direction, boolean> = {
119
+ up: frame.navEdges.fires('lu', held.up),
120
+ down: frame.navEdges.fires('ld', held.down),
121
+ left: frame.navEdges.fires('ll', held.left),
122
+ right: frame.navEdges.fires('lr', held.right),
123
+ };
124
+ const shouldAccept = frame.navEdges.fires('la', isAccepting) && isAccepting;
125
+ const tiles = visibleTiles();
126
+ if (tiles.length === 0) {
127
+ return;
128
+ }
129
+ const focused = tiles.findIndex((tile) => tile.classList.contains(FOCUS_CLASS));
130
+ const focusedTile = tiles[focused];
131
+ if (focusedTile === undefined) {
132
+ highlightTile(tiles, 0);
133
+ return;
134
+ }
135
+ if (shouldAccept) {
136
+ const href = focusedTile.getAttribute('href');
137
+ if (href === null) {
138
+ focusedTile.click();
139
+ } else {
140
+ location.href = href;
141
+ }
142
+ return;
143
+ }
144
+ const direction = (['up', 'down', 'left', 'right'] as const).find((candidate) => fired[candidate]);
145
+ if (direction === undefined) {
146
+ return;
147
+ }
148
+ const next = findNearestInDirection(tiles.map(rectOf), focused, direction);
149
+ if (next >= 0) {
150
+ highlightTile(tiles, next);
151
+ }
152
+ }
153
+
154
+ /** One frame of the zero-config fallback. */
155
+ export function runZeroConfigFrame(frame: ZeroConfigFrame): void {
156
+ if (frame.gamepads.length === 0) {
157
+ return;
158
+ }
159
+ if (frame.slug === null) {
160
+ navigateCatalog(frame);
161
+ } else {
162
+ synthesizeKeys(frame);
163
+ }
164
+ }
package/src/input.ts CHANGED
@@ -1,22 +1,26 @@
1
- /**
2
- * `@diffenderfer-games/hub/input`: the input system (`hub.input`): the
3
- * page-wide {@link getInput}, the {@link InputSystem} class, the on-screen
4
- * control layout, and the overlay stack behind `hub.overlay`.
5
- *
6
- * @packageDocumentation
7
- */
8
- export { InputSystem } from './input/input-system';
9
- export type { InputSystemOptions } from './input/input-system';
10
- export { getInput, INPUT_SINGLETON_API, isInputSystem, parseInputOverrides } from './input/page-input';
11
- export type { InputOptions } from './input/page-input';
12
- export { isPageInputControl } from './input/page-control';
13
- export type { PageInputControl } from './input/page-control';
14
- export type * from './input/types';
15
- export { Overlay } from './overlay/overlay';
16
- export { layoutControls } from './overlay/layout';
17
- export type { LayoutOptions } from './overlay/layout';
18
- export { boxesTouch, controlSize } from './overlay/layout-geometry';
19
- export type { Insets, LayoutBox, LayoutItem } from './overlay/layout-geometry';
20
- export { getUiStack, isUiStack } from './uistack/page-stack';
21
- export { UI_STACK_VERSION } from './uistack/ui-stack';
22
- export type * from './uistack/types';
1
+ /**
2
+ * `@diffenderfer-games/hub/input`: the input system (`hub.input`): the
3
+ * page-wide {@link getInput}, the {@link InputSystem} class, the on-screen
4
+ * control layout, and the overlay stack behind `hub.overlay`.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ export { InputSystem } from './input/input-system';
9
+ export type { InputSystemOptions } from './input/input-system';
10
+ export { getInput, INPUT_SINGLETON_API, isInputSystem, parseInputOverrides } from './input/page-input';
11
+ export type { InputOptions } from './input/page-input';
12
+ export { FOCUS_RING_SHADOW } from './input/focus-ring';
13
+ export { MODALITY_ATTRIBUTE, trackInputModality } from './input/modality';
14
+ export type { InputModality } from './input/modality';
15
+ export { isPageInputControl } from './input/page-control';
16
+ export type { PageInputControl } from './input/page-control';
17
+ export type * from './input/types';
18
+ export type { AutoNavigateTarget } from './input/navigator';
19
+ export { Overlay } from './overlay/overlay';
20
+ export { layoutControls } from './overlay/layout';
21
+ export type { LayoutOptions } from './overlay/layout';
22
+ export { boxesTouch, controlSize } from './overlay/layout-geometry';
23
+ export type { Insets, LayoutBox, LayoutItem } from './overlay/layout-geometry';
24
+ export { getUiStack, isUiStack } from './uistack/page-stack';
25
+ export { UI_STACK_VERSION } from './uistack/ui-stack';
26
+ export type * from './uistack/types';
package/src/mp.ts CHANGED
@@ -1,10 +1,10 @@
1
- /**
2
- * `@diffenderfer-games/hub/mp`: the multiplayer forwarder (`hub.mp`):
3
- * invites, launches, the leave guard and own-server tickets.
4
- *
5
- * @packageDocumentation
6
- */
7
- export { multiplayerForwarder as mp } from './realtime/multiplayer-forwarder';
8
- export type * from './realtime/multiplayer-types';
9
- export type { InviteCreated } from './realtime/wire-types';
10
- export type { Invite, JoinInfo, Launch, LaunchTicket, Party } from '@diffenderfer-games/hub-contract';
1
+ /**
2
+ * `@diffenderfer-games/hub/mp`: the multiplayer forwarder (`hub.mp`):
3
+ * invites, launches, the leave guard and own-server tickets.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ export { multiplayerForwarder as mp } from './realtime/multiplayer-forwarder';
8
+ export type * from './realtime/multiplayer-types';
9
+ export type { InviteCreated, InviteLink } from './realtime/wire-types';
10
+ export type { Invite, JoinInfo, Launch, LaunchTicket, Party } from '@diffenderfer-games/hub-contract';