@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,137 +1,137 @@
1
- /**
2
- * The on-screen keyboard for text fields, usable by touch, mouse or gamepad
3
- * (the stick or d-pad moves between keys, A presses, B or Start closes).
4
- */
5
- import { RepeatingEdges, RisingEdges } from '../input/edges';
6
- import { isAnyPadButtonPressed, PAD_BUTTON, readGamepads } from '../input/gamepad';
7
- import type { Direction } from '../input/gamepad';
8
- import { findNearestInDirection, rectOf } from '../input/spatial';
9
- import { findElements } from './dom';
10
-
11
- const LETTER_ROWS = ['1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
12
- const STICK_THRESHOLD = 0.5;
13
-
14
- /** One key's markup. */
15
- function keyHtml(label: string, key: string, extraClass = ''): string {
16
- return `<button class="k ${extraClass}" data-k="${key}">${label}</button>`;
17
- }
18
-
19
- const KEYBOARD_HTML = `<div class="kb-rows">${LETTER_ROWS.map((row) => `<div class="kb-row">${Array.from(row).map((character) => keyHtml(character, character)).join('')}</div>`).join('')}`
20
- + `<div class="kb-row">${keyHtml('⇧', 'shift', 'wide')}${keyHtml('space', ' ', 'space')}${keyHtml('⌫', 'back', 'wide')}${keyHtml('↵', 'enter', 'wide')}${keyHtml('✕', 'close', 'wide')}</div></div>`;
21
-
22
- /** The direction the pads point, from the summed stick and d-pad. */
23
- function heldDirection(pads: readonly Gamepad[]): Direction | null {
24
- let horizontal = 0;
25
- let vertical = 0;
26
- for (const pad of pads) {
27
- horizontal += (pad.axes[0] ?? 0) - Number(pad.buttons[PAD_BUTTON.LEFT]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.RIGHT]?.pressed === true);
28
- vertical += (pad.axes[1] ?? 0) - Number(pad.buttons[PAD_BUTTON.UP]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.DOWN]?.pressed === true);
29
- }
30
- if (Math.abs(horizontal) <= STICK_THRESHOLD && Math.abs(vertical) <= STICK_THRESHOLD) {
31
- return null;
32
- }
33
- if (Math.abs(horizontal) > Math.abs(vertical)) {
34
- return horizontal > 0 ? 'right' : 'left';
35
- }
36
- return vertical > 0 ? 'down' : 'up';
37
- }
38
-
39
- /** The on-screen keyboard (one at a time, inside the overlay's shadow root). */
40
- export class OnScreenKeyboard {
41
- private readonly root: ShadowRoot;
42
- private panel: HTMLDivElement | null = null;
43
- private target: HTMLInputElement | HTMLTextAreaElement | null = null;
44
- private keys: HTMLElement[] = [];
45
- private focusedIndex = 0;
46
- private isShifted = false;
47
- private readonly repeats = new RepeatingEdges();
48
- private readonly presses = new RisingEdges();
49
-
50
- constructor(root: ShadowRoot) {
51
- this.root = root;
52
- }
53
-
54
- /** Whether the keyboard is showing. */
55
- get isOpen(): boolean {
56
- return this.panel !== null;
57
- }
58
-
59
- /** Open it for `target` (replacing any open keyboard). */
60
- open(target: HTMLInputElement | HTMLTextAreaElement | null): void {
61
- this.close();
62
- this.target = target;
63
- this.isShifted = false;
64
- this.focusedIndex = 0;
65
- const panel = document.createElement('div');
66
- panel.className = 'kb';
67
- panel.innerHTML = KEYBOARD_HTML;
68
- this.root.appendChild(panel);
69
- this.panel = panel;
70
- this.keys = findElements(panel, '.k', HTMLElement);
71
- for (const key of this.keys) {
72
- const press = (event: Event): void => {
73
- event.preventDefault();
74
- this.press(key.dataset['k'] ?? '');
75
- };
76
- key.addEventListener('mousedown', press);
77
- key.addEventListener('touchstart', press, { passive: false });
78
- }
79
- this.highlightFocusedKey();
80
- }
81
-
82
- /** Close it. */
83
- close(): void {
84
- this.panel?.remove();
85
- this.panel = null;
86
- this.target = null;
87
- this.keys = [];
88
- }
89
-
90
- /** Called every frame while open: gamepad navigation. */
91
- pollGamepads(): void {
92
- if (this.panel === null) {
93
- return;
94
- }
95
- const pads = readGamepads();
96
- const direction = heldDirection(pads);
97
- if (this.repeats.fires('move', direction !== null) && direction !== null) {
98
- this.moveFocus(direction);
99
- }
100
- if (this.presses.rose('act', isAnyPadButtonPressed(pads, PAD_BUTTON.A))) {
101
- this.press(this.keys[this.focusedIndex]?.dataset['k'] ?? '');
102
- }
103
- const isClosing = isAnyPadButtonPressed(pads, PAD_BUTTON.B) || isAnyPadButtonPressed(pads, PAD_BUTTON.START);
104
- if (this.presses.rose('close', isClosing)) {
105
- this.close();
106
- }
107
- }
108
-
109
- private press(key: string): void {
110
- if (key === 'close' || key === 'enter') {
111
- this.close();
112
- return;
113
- }
114
- if (key === 'shift') {
115
- this.isShifted = !this.isShifted;
116
- return;
117
- }
118
- const target = this.target;
119
- if (target === null || key === '') {
120
- return;
121
- }
122
- target.value = key === 'back' ? target.value.slice(0, -1) : target.value + (this.isShifted ? key.toUpperCase() : key);
123
- target.dispatchEvent(new Event('input', { bubbles: true }));
124
- }
125
-
126
- private moveFocus(direction: Direction): void {
127
- const next = findNearestInDirection(this.keys.map(rectOf), this.focusedIndex, direction);
128
- if (next >= 0) {
129
- this.focusedIndex = next;
130
- this.highlightFocusedKey();
131
- }
132
- }
133
-
134
- private highlightFocusedKey(): void {
135
- this.keys.forEach((key, index) => key.classList.toggle('kfocus', index === this.focusedIndex));
136
- }
137
- }
1
+ /**
2
+ * The on-screen keyboard for text fields, usable by touch, mouse or gamepad
3
+ * (the stick or d-pad moves between keys, A presses, B or Start closes).
4
+ */
5
+ import { RepeatingEdges, RisingEdges } from '../input/edges';
6
+ import { isAnyPadButtonPressed, PAD_BUTTON, readGamepads } from '../input/gamepad';
7
+ import type { Direction } from '../input/gamepad';
8
+ import { findNearestInDirection, rectOf } from '../input/spatial';
9
+ import { findElements } from './dom';
10
+
11
+ const LETTER_ROWS = ['1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
12
+ const STICK_THRESHOLD = 0.5;
13
+
14
+ /** One key's markup. */
15
+ function keyHtml(label: string, key: string, extraClass = ''): string {
16
+ return `<button class="k ${extraClass}" data-k="${key}">${label}</button>`;
17
+ }
18
+
19
+ const KEYBOARD_HTML = `<div class="kb-rows">${LETTER_ROWS.map((row) => `<div class="kb-row">${Array.from(row).map((character) => keyHtml(character, character)).join('')}</div>`).join('')}`
20
+ + `<div class="kb-row">${keyHtml('⇧', 'shift', 'wide')}${keyHtml('space', ' ', 'space')}${keyHtml('⌫', 'back', 'wide')}${keyHtml('↵', 'enter', 'wide')}${keyHtml('✕', 'close', 'wide')}</div></div>`;
21
+
22
+ /** The direction the pads point, from the summed stick and d-pad. */
23
+ function heldDirection(pads: readonly Gamepad[]): Direction | null {
24
+ let horizontal = 0;
25
+ let vertical = 0;
26
+ for (const pad of pads) {
27
+ horizontal += (pad.axes[0] ?? 0) - Number(pad.buttons[PAD_BUTTON.LEFT]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.RIGHT]?.pressed === true);
28
+ vertical += (pad.axes[1] ?? 0) - Number(pad.buttons[PAD_BUTTON.UP]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.DOWN]?.pressed === true);
29
+ }
30
+ if (Math.abs(horizontal) <= STICK_THRESHOLD && Math.abs(vertical) <= STICK_THRESHOLD) {
31
+ return null;
32
+ }
33
+ if (Math.abs(horizontal) > Math.abs(vertical)) {
34
+ return horizontal > 0 ? 'right' : 'left';
35
+ }
36
+ return vertical > 0 ? 'down' : 'up';
37
+ }
38
+
39
+ /** The on-screen keyboard (one at a time, inside the overlay's shadow root). */
40
+ export class OnScreenKeyboard {
41
+ private readonly root: ShadowRoot;
42
+ private panel: HTMLDivElement | null = null;
43
+ private target: HTMLInputElement | HTMLTextAreaElement | null = null;
44
+ private keys: HTMLElement[] = [];
45
+ private focusedIndex = 0;
46
+ private isShifted = false;
47
+ private readonly repeats = new RepeatingEdges();
48
+ private readonly presses = new RisingEdges();
49
+
50
+ constructor(root: ShadowRoot) {
51
+ this.root = root;
52
+ }
53
+
54
+ /** Whether the keyboard is showing. */
55
+ get isOpen(): boolean {
56
+ return this.panel !== null;
57
+ }
58
+
59
+ /** Open it for `target` (replacing any open keyboard). */
60
+ open(target: HTMLInputElement | HTMLTextAreaElement | null): void {
61
+ this.close();
62
+ this.target = target;
63
+ this.isShifted = false;
64
+ this.focusedIndex = 0;
65
+ const panel = document.createElement('div');
66
+ panel.className = 'kb';
67
+ panel.innerHTML = KEYBOARD_HTML;
68
+ this.root.appendChild(panel);
69
+ this.panel = panel;
70
+ this.keys = findElements(panel, '.k', HTMLElement);
71
+ for (const key of this.keys) {
72
+ const press = (event: Event): void => {
73
+ event.preventDefault();
74
+ this.press(key.dataset['k'] ?? '');
75
+ };
76
+ key.addEventListener('mousedown', press);
77
+ key.addEventListener('touchstart', press, { passive: false });
78
+ }
79
+ this.highlightFocusedKey();
80
+ }
81
+
82
+ /** Close it. */
83
+ close(): void {
84
+ this.panel?.remove();
85
+ this.panel = null;
86
+ this.target = null;
87
+ this.keys = [];
88
+ }
89
+
90
+ /** Called every frame while open: gamepad navigation. */
91
+ pollGamepads(): void {
92
+ if (this.panel === null) {
93
+ return;
94
+ }
95
+ const pads = readGamepads();
96
+ const direction = heldDirection(pads);
97
+ if (this.repeats.fires('move', direction !== null) && direction !== null) {
98
+ this.moveFocus(direction);
99
+ }
100
+ if (this.presses.rose('act', isAnyPadButtonPressed(pads, PAD_BUTTON.A))) {
101
+ this.press(this.keys[this.focusedIndex]?.dataset['k'] ?? '');
102
+ }
103
+ const isClosing = isAnyPadButtonPressed(pads, PAD_BUTTON.B) || isAnyPadButtonPressed(pads, PAD_BUTTON.START);
104
+ if (this.presses.rose('close', isClosing)) {
105
+ this.close();
106
+ }
107
+ }
108
+
109
+ private press(key: string): void {
110
+ if (key === 'close' || key === 'enter') {
111
+ this.close();
112
+ return;
113
+ }
114
+ if (key === 'shift') {
115
+ this.isShifted = !this.isShifted;
116
+ return;
117
+ }
118
+ const target = this.target;
119
+ if (target === null || key === '') {
120
+ return;
121
+ }
122
+ target.value = key === 'back' ? target.value.slice(0, -1) : target.value + (this.isShifted ? key.toUpperCase() : key);
123
+ target.dispatchEvent(new Event('input', { bubbles: true }));
124
+ }
125
+
126
+ private moveFocus(direction: Direction): void {
127
+ const next = findNearestInDirection(this.keys.map(rectOf), this.focusedIndex, direction);
128
+ if (next >= 0) {
129
+ this.focusedIndex = next;
130
+ this.highlightFocusedKey();
131
+ }
132
+ }
133
+
134
+ private highlightFocusedKey(): void {
135
+ this.keys.forEach((key, index) => key.classList.toggle('kfocus', index === this.focusedIndex));
136
+ }
137
+ }