@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,74 +1,80 @@
1
- /**
2
- * Small type guards for values this client did not create: JSON read back
3
- * from `localStorage`, error bodies, objects from other bundles. The browser
4
- * client carries no zod (plan D16), so these replace schema parsing on the
5
- * few shapes it reads.
6
- */
7
- import type { Json } from '@diffenderfer-games/hub-contract';
8
-
9
- /** A plain object's fields, not yet checked. */
10
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: the fields of a value read from storage or another bundle
11
- export type UncheckedRecord = Readonly<Record<string, unknown>>;
12
-
13
- /** Whether `candidate` is a non-null, non-array object. */
14
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage or another bundle
15
- export function isRecord(candidate: unknown): candidate is UncheckedRecord {
16
- return typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate);
17
- }
18
-
19
- /** `candidate` when it is a string, else `undefined`. */
20
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: one field of an unchecked record
21
- export function optionalString(candidate: unknown): string | undefined {
22
- return typeof candidate === 'string' ? candidate : undefined;
23
- }
24
-
25
- /** `candidate` when it is a finite number, else `undefined`. */
26
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: one field of an unchecked record
27
- export function optionalNumber(candidate: unknown): number | undefined {
28
- return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
29
- }
30
-
31
- /** Whether `candidate` is a string or `null`. */
32
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: one field of an unchecked record
33
- export function isNullableString(candidate: unknown): candidate is string | null {
34
- return candidate === null || typeof candidate === 'string';
35
- }
36
-
37
- /** Whether `candidate` is a JSON value (what `JSON.parse` can return). */
38
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage
39
- export function isJson(candidate: unknown): candidate is Json {
40
- if (candidate === null || typeof candidate === 'string' || typeof candidate === 'boolean') {
41
- return true;
42
- }
43
- if (typeof candidate === 'number') {
44
- return Number.isFinite(candidate);
45
- }
46
- if (Array.isArray(candidate)) {
47
- return candidate.every(isJson);
48
- }
49
- return isRecord(candidate) && Object.values(candidate).every(isJson);
50
- }
51
-
52
- /** Whether `candidate` is an object whose every value is a finite number. */
53
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage
54
- export function isNumberRecord(candidate: unknown): candidate is Readonly<Record<string, number>> {
55
- return isRecord(candidate) && Object.values(candidate).every((entry) => optionalNumber(entry) !== undefined);
56
- }
57
-
58
- /** The strings in `candidate`, or an empty list when it is not an array. */
59
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: a list read from storage
60
- export function stringsIn(candidate: unknown): string[] {
61
- return Array.isArray(candidate) ? candidate.filter((entry): entry is string => typeof entry === 'string') : [];
62
- }
63
-
64
- /** Parse JSON text without throwing: `undefined` when it is not JSON. */
65
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: text read from storage, checked by the caller's guard next
66
- export function parseJsonText(text: string): unknown {
67
- try {
68
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: JSON.parse's result, checked by the caller's guard next
69
- const parsed: unknown = JSON.parse(text);
70
- return parsed;
71
- } catch {
72
- return undefined;
73
- }
74
- }
1
+ /**
2
+ * Small type guards for values this client did not create: JSON read back
3
+ * from `localStorage`, error bodies, objects from other bundles. The browser
4
+ * client carries no zod (plan D16), so these replace schema parsing on the
5
+ * few shapes it reads.
6
+ */
7
+ import type { Json } from '@diffenderfer-games/hub-contract';
8
+
9
+ /** A plain object's fields, not yet checked. */
10
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the fields of a value read from storage or another bundle
11
+ export type UncheckedRecord = Readonly<Record<string, unknown>>;
12
+
13
+ /** Whether `candidate` is a non-null, non-array object. */
14
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage or another bundle
15
+ export function isRecord(candidate: unknown): candidate is UncheckedRecord {
16
+ return typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate);
17
+ }
18
+
19
+ /** `candidate` when it is a string, else `undefined`. */
20
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: one field of an unchecked record
21
+ export function optionalString(candidate: unknown): string | undefined {
22
+ return typeof candidate === 'string' ? candidate : undefined;
23
+ }
24
+
25
+ /** `candidate` when it is a finite number, else `undefined`. */
26
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: one field of an unchecked record
27
+ export function optionalNumber(candidate: unknown): number | undefined {
28
+ return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
29
+ }
30
+
31
+ /** Whether `candidate` is a string or `null`. */
32
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: one field of an unchecked record
33
+ export function isNullableString(candidate: unknown): candidate is string | null {
34
+ return candidate === null || typeof candidate === 'string';
35
+ }
36
+
37
+ /** Whether `candidate` is a JSON value (what `JSON.parse` can return). */
38
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage
39
+ export function isJson(candidate: unknown): candidate is Json {
40
+ if (candidate === null || typeof candidate === 'string' || typeof candidate === 'boolean') {
41
+ return true;
42
+ }
43
+ if (typeof candidate === 'number') {
44
+ return Number.isFinite(candidate);
45
+ }
46
+ if (Array.isArray(candidate)) {
47
+ return candidate.every(isJson);
48
+ }
49
+ return isRecord(candidate) && Object.values(candidate).every(isJson);
50
+ }
51
+
52
+ /** Whether `candidate` is an object whose every value is a finite number. */
53
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage
54
+ export function isNumberRecord(candidate: unknown): candidate is Readonly<Record<string, number>> {
55
+ return isRecord(candidate) && Object.values(candidate).every((entry) => optionalNumber(entry) !== undefined);
56
+ }
57
+
58
+ /** The strings in `candidate`, or an empty list when it is not an array. */
59
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a list read from storage
60
+ export function stringsIn(candidate: unknown): string[] {
61
+ return Array.isArray(candidate) ? candidate.filter((entry): entry is string => typeof entry === 'string') : [];
62
+ }
63
+
64
+ /** Parse JSON text without throwing: `undefined` when it is not JSON. */
65
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: text read from storage, checked by the caller's guard next
66
+ export function parseJsonText(text: string): unknown {
67
+ try {
68
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: JSON.parse's result, checked by the caller's guard next
69
+ const parsed: unknown = JSON.parse(text);
70
+ return parsed;
71
+ } catch {
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ /** Whether `candidate` is a JSON object (not an array or `null`). */
77
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: checks a value read from storage
78
+ export function isJsonObject(candidate: unknown): candidate is { readonly [key: string]: Json } {
79
+ return isRecord(candidate) && Object.values(candidate).every(isJson);
80
+ }
@@ -1,66 +1,66 @@
1
- /**
2
- * The string store behind the offline cache and queues: `localStorage` when
3
- * the page has it, else an in-memory map (privacy mode, Node, tests). Every
4
- * key keeps the v1 client's name, so a page upgraded from a vendored copy
5
- * still reads the queue and cache that copy wrote.
6
- */
7
-
8
- /** A string key-value store with `localStorage`'s semantics. */
9
- export interface KeyValueStore {
10
- /** The stored text, or `null`. */
11
- read(key: string): string | null;
12
- /** Store text; a full or blocked storage is ignored. */
13
- write(key: string, text: string): void;
14
- /** Forget a key. */
15
- remove(key: string): void;
16
- /** Every stored key. */
17
- keys(): string[];
18
- }
19
-
20
- /** A store backed by a `Storage` (`localStorage`). */
21
- function storageBacked(storage: Storage): KeyValueStore {
22
- return {
23
- read: (key) => storage.getItem(key),
24
- write: (key, text) => {
25
- try {
26
- storage.setItem(key, text);
27
- } catch {
28
- // A full or blocked storage loses the write, as in v1.
29
- }
30
- },
31
- remove: (key) => {
32
- storage.removeItem(key);
33
- },
34
- keys: () => Object.keys(storage),
35
- };
36
- }
37
-
38
- /** A store held in memory for this page only. */
39
- export function createMemoryStore(): KeyValueStore {
40
- const entries = new Map<string, string>();
41
- return {
42
- read: (key) => entries.get(key) ?? null,
43
- write: (key, text) => {
44
- entries.set(key, text);
45
- },
46
- remove: (key) => {
47
- entries.delete(key);
48
- },
49
- keys: () => [...entries.keys()],
50
- };
51
- }
52
-
53
- /** The page's `localStorage`, or `undefined` when it is missing or throws (privacy mode). */
54
- function pageStorage(): Storage | undefined {
55
- try {
56
- return typeof localStorage === 'undefined' ? undefined : localStorage;
57
- } catch {
58
- return undefined;
59
- }
60
- }
61
-
62
- /** `localStorage` when available, else a fresh in-memory store. */
63
- export function createPageStore(): KeyValueStore {
64
- const storage = pageStorage();
65
- return storage === undefined ? createMemoryStore() : storageBacked(storage);
66
- }
1
+ /**
2
+ * The string store behind the offline cache and queues: `localStorage` when
3
+ * the page has it, else an in-memory map (privacy mode, Node, tests). Every
4
+ * key keeps the v1 client's name, so a page upgraded from a vendored copy
5
+ * still reads the queue and cache that copy wrote.
6
+ */
7
+
8
+ /** A string key-value store with `localStorage`'s semantics. */
9
+ export interface KeyValueStore {
10
+ /** The stored text, or `null`. */
11
+ read(key: string): string | null;
12
+ /** Store text; a full or blocked storage is ignored. */
13
+ write(key: string, text: string): void;
14
+ /** Forget a key. */
15
+ remove(key: string): void;
16
+ /** Every stored key. */
17
+ keys(): string[];
18
+ }
19
+
20
+ /** A store backed by a `Storage` (`localStorage`). */
21
+ function storageBacked(storage: Storage): KeyValueStore {
22
+ return {
23
+ read: (key) => storage.getItem(key),
24
+ write: (key, text) => {
25
+ try {
26
+ storage.setItem(key, text);
27
+ } catch {
28
+ // A full or blocked storage loses the write, as in v1.
29
+ }
30
+ },
31
+ remove: (key) => {
32
+ storage.removeItem(key);
33
+ },
34
+ keys: () => Object.keys(storage),
35
+ };
36
+ }
37
+
38
+ /** A store held in memory for this page only. */
39
+ export function createMemoryStore(): KeyValueStore {
40
+ const entries = new Map<string, string>();
41
+ return {
42
+ read: (key) => entries.get(key) ?? null,
43
+ write: (key, text) => {
44
+ entries.set(key, text);
45
+ },
46
+ remove: (key) => {
47
+ entries.delete(key);
48
+ },
49
+ keys: () => [...entries.keys()],
50
+ };
51
+ }
52
+
53
+ /** The page's `localStorage`, or `undefined` when it is missing or throws (privacy mode). */
54
+ function pageStorage(): Storage | undefined {
55
+ try {
56
+ return typeof localStorage === 'undefined' ? undefined : localStorage;
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ /** `localStorage` when available, else a fresh in-memory store. */
63
+ export function createPageStore(): KeyValueStore {
64
+ const storage = pageStorage();
65
+ return storage === undefined ? createMemoryStore() : storageBacked(storage);
66
+ }
@@ -1,43 +1,43 @@
1
- /**
2
- * The own-server ticket request (`hub.mp.ticket()`): a 60-second identity
3
- * ticket for this game's own server, fetched straight from the hub. It must
4
- * not depend on the social runtime: on the first visit after a deploy the
5
- * service worker can still serve an older menu that never loads it. When the
6
- * page has no session yet, a guest one is created and the request retried
7
- * once.
8
- */
9
- import { HubError, OfflineError } from './errors';
10
- import { detectApiBase, detectSlug } from './page';
11
- import { createRestClient, type RestClient } from './rest/rest-client';
12
-
13
- /** A REST client for the page's API base that uses the global fetch at call time. */
14
- function pageRestClient(): RestClient {
15
- return createRestClient({ apiBase: detectApiBase(), fetch: (input, init) => globalThis.fetch(input, init) });
16
- }
17
-
18
- /** One ticket request; a transport failure is an {@link OfflineError}. */
19
- async function requestTicket(rest: RestClient, slug: string): Promise<string> {
20
- try {
21
- return (await rest.call('getGameServerTicket', { slug })).ticket;
22
- } catch (failure) {
23
- throw failure instanceof HubError && failure.isOffline ? new OfflineError() : failure;
24
- }
25
- }
26
-
27
- /** A ticket for this page's game; signs in a guest first when the page has no session. */
28
- export async function fetchGameServerTicket(): Promise<string> {
29
- const slug = detectSlug();
30
- if (slug === null) {
31
- throw new HubError('Hub: could not determine the game slug.');
32
- }
33
- const rest = pageRestClient();
34
- try {
35
- return await requestTicket(rest, slug);
36
- } catch (failure) {
37
- if (!(failure instanceof HubError) || failure.status !== 401) {
38
- throw failure;
39
- }
40
- await rest.call('ensureGuestSession');
41
- return requestTicket(rest, slug);
42
- }
43
- }
1
+ /**
2
+ * The own-server ticket request (`hub.mp.ticket()`): a 60-second identity
3
+ * ticket for this game's own server, fetched straight from the hub. It must
4
+ * not depend on the social runtime: on the first visit after a deploy the
5
+ * service worker can still serve an older menu that never loads it. When the
6
+ * page has no session yet, a guest one is created and the request retried
7
+ * once.
8
+ */
9
+ import { HubError, OfflineError } from './errors';
10
+ import { detectApiBase, detectSlug } from './page';
11
+ import { createRestClient, type RestClient } from './rest/rest-client';
12
+
13
+ /** A REST client for the page's API base that uses the global fetch at call time. */
14
+ function pageRestClient(): RestClient {
15
+ return createRestClient({ apiBase: detectApiBase(), fetch: (input, init) => globalThis.fetch(input, init) });
16
+ }
17
+
18
+ /** One ticket request; a transport failure is an {@link OfflineError}. */
19
+ async function requestTicket(rest: RestClient, slug: string): Promise<string> {
20
+ try {
21
+ return (await rest.call('getGameServerTicket', { slug })).ticket;
22
+ } catch (failure) {
23
+ throw failure instanceof HubError && failure.isOffline ? new OfflineError() : failure;
24
+ }
25
+ }
26
+
27
+ /** A ticket for this page's game; signs in a guest first when the page has no session. */
28
+ export async function fetchGameServerTicket(): Promise<string> {
29
+ const slug = detectSlug();
30
+ if (slug === null) {
31
+ throw new HubError('Hub: could not determine the game slug.');
32
+ }
33
+ const rest = pageRestClient();
34
+ try {
35
+ return await requestTicket(rest, slug);
36
+ } catch (failure) {
37
+ if (!(failure instanceof HubError) || failure.status !== 401) {
38
+ throw failure;
39
+ }
40
+ await rest.call('ensureGuestSession');
41
+ return requestTicket(rest, slug);
42
+ }
43
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `hub.turns`: online, turn-based games the hub runs from the game's own
3
+ * rules module (`game.turns`), played over minutes or days. Make one
4
+ * (inviting friends, with a link, or with open seats anyone may take),
5
+ * join one, open one the player sits in, and list them. Players see "an
6
+ * online game", never "a turn game".
7
+ *
8
+ * ```ts
9
+ * const opened = hub.saved.opened;
10
+ * const game = opened?.kind === 'turns' ? await hub.turns.open(opened.id)
11
+ * : hub.turns.link !== null ? await hub.turns.join(hub.turns.link)
12
+ * : null;
13
+ * game?.on('move', ({ view, events }) => animate(events, view));
14
+ * ```
15
+ */
16
+ import type { CreateTurnGame, OpenTurnGame, TurnDeadline } from '@diffenderfer-games/hub-contract';
17
+ import { toStoredJson, type GamePayload } from '../game-data';
18
+ import { isBrowserPage } from '../page';
19
+ import type { SavedEntry } from '../saved/saved-types';
20
+ import { TurnGame, type TurnGameHost, type TurnGameOptions } from './turn-game';
21
+ import { readYourTurnBuzz, saveYourTurnBuzz } from './your-turn-buzz';
22
+
23
+ /** The query parameter an invite link carries (`/<slug>/?hub_turn_link=<key>`); the client strips it. */
24
+ export const TURN_LINK_PARAM = 'hub_turn_link';
25
+
26
+ /** Where the answer is kept for the page. */
27
+ const PAGE_FLAG = '__HUB_TURN_LINK__';
28
+
29
+ /** How to make a game. */
30
+ export interface TurnCreateOptions {
31
+ readonly mode?: string;
32
+ /** The game's own setup choices (at most 2 KB of JSON). */
33
+ readonly options?: GamePayload;
34
+ /** Friends to invite (their seats are held). */
35
+ readonly invite?: readonly number[];
36
+ /** Make a link others can take a seat with (`game.link`). */
37
+ readonly link?: boolean;
38
+ /** List the open seats in Play online for anyone to take. */
39
+ readonly public?: boolean;
40
+ /** Seats in all (within `game.turns.players`). */
41
+ readonly seats?: number;
42
+ /** Let the computer fill seats nobody took (default true, when the game has bots). */
43
+ readonly bots?: boolean;
44
+ /** How long a turn may wait before the others may let the computer play it (default none). */
45
+ readonly deadline?: TurnDeadline;
46
+ /** Start by itself once every seat is taken (the creator needn't come back). */
47
+ readonly autoStart?: boolean;
48
+ }
49
+
50
+ /** What `hub.turns` needs from its client. */
51
+ export interface TurnsHost extends TurnGameHost {
52
+ readonly slug: string | null;
53
+ create(body: CreateTurnGame): ReturnType<TurnGameHost['get']>;
54
+ join(id: string): ReturnType<TurnGameHost['get']>;
55
+ joinByLink(key: string): ReturnType<TurnGameHost['get']>;
56
+ listOpen(game: string | null): Promise<OpenTurnGame[]>;
57
+ list(): Promise<SavedEntry[]>;
58
+ }
59
+
60
+ /** Read and strip `hub_turn_link`, once per page. */
61
+ function linkOnPage(): string | null {
62
+ if (!isBrowserPage()) {
63
+ return null;
64
+ }
65
+ if (Reflect.has(window, PAGE_FLAG)) {
66
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a window field another bundle may have set
67
+ const recorded: unknown = Reflect.get(window, PAGE_FLAG);
68
+ return typeof recorded === 'string' ? recorded : null;
69
+ }
70
+ let key: string | null = null;
71
+ try {
72
+ const url = new URL(location.href);
73
+ key = url.searchParams.get(TURN_LINK_PARAM);
74
+ if (key !== null) {
75
+ url.searchParams.delete(TURN_LINK_PARAM);
76
+ history.replaceState(history.state, '', `${url.pathname}${url.search}${url.hash}`);
77
+ }
78
+ } catch {
79
+ // A sandboxed page keeps the parameter; the answer is still recorded.
80
+ }
81
+ Reflect.set(window, PAGE_FLAG, key);
82
+ return key;
83
+ }
84
+
85
+ /** Whether `idOrKey` is a game id (`sg_…`) rather than a link key. */
86
+ function isGameId(idOrKey: string): boolean {
87
+ return idOrKey.startsWith('sg_');
88
+ }
89
+
90
+ /** `hub.turns`. */
91
+ export class HubTurns {
92
+ private readonly host: TurnsHost;
93
+ private isBuzzOn = true;
94
+ private readonly gameOptions: TurnGameOptions = { isBuzzOn: () => this.isBuzzOn };
95
+
96
+ constructor(host: TurnsHost) {
97
+ this.host = host;
98
+ }
99
+
100
+ /**
101
+ * This page's "your turn" buzz (a short vibration when the player becomes
102
+ * due in an open game, see `TurnGame.setYourTurnBuzz`): pass `false` if
103
+ * your game gives its own signal. On by default.
104
+ */
105
+ setYourTurnBuzz(isOn: boolean): void {
106
+ this.isBuzzOn = isOn;
107
+ }
108
+
109
+ /** The player's own "Buzz when it's my turn" switch on this device (the menu's Settings show it too). */
110
+ get playerYourTurnBuzz(): boolean {
111
+ return readYourTurnBuzz();
112
+ }
113
+
114
+ set playerYourTurnBuzz(isOn: boolean) {
115
+ saveYourTurnBuzz(isOn);
116
+ }
117
+
118
+ /** The invite link key this page was opened with (`?hub_turn_link=`), taken out of the address bar; `null` when none. Synchronous. */
119
+ get link(): string | null {
120
+ return linkOnPage();
121
+ }
122
+
123
+ /** Make a game in this page's game (its lobby): the player takes the first seat. */
124
+ async create<View = GamePayload, Move extends GamePayload = GamePayload, Event = GamePayload>(options: TurnCreateOptions = {}): Promise<TurnGame<View, Move, Event>> {
125
+ const slug = this.host.slug ?? '';
126
+ const body: CreateTurnGame = {
127
+ game: slug,
128
+ ...(options.mode === undefined ? {} : { mode: options.mode }),
129
+ ...(options.options === undefined ? {} : { options: toStoredJson(options.options) }),
130
+ ...(options.invite === undefined ? {} : { invite: [...options.invite] }),
131
+ ...(options.link === undefined ? {} : { link: options.link }),
132
+ ...(options.public === undefined ? {} : { public: options.public }),
133
+ ...(options.seats === undefined ? {} : { seats: options.seats }),
134
+ ...(options.bots === undefined ? {} : { bots: options.bots }),
135
+ ...(options.deadline === undefined ? {} : { deadline: options.deadline }),
136
+ ...(options.autoStart === undefined ? {} : { autoStart: options.autoStart }),
137
+ };
138
+ return new TurnGame<View, Move, Event>(this.host, await this.host.create(body), this.gameOptions);
139
+ }
140
+
141
+ /** Take a seat: by the game's id (an invite, an open seat) or by an invite link's key. */
142
+ async join<View = GamePayload, Move extends GamePayload = GamePayload, Event = GamePayload>(idOrLinkKey: string): Promise<TurnGame<View, Move, Event>> {
143
+ const joined = isGameId(idOrLinkKey) ? await this.host.join(idOrLinkKey) : await this.host.joinByLink(idOrLinkKey);
144
+ return new TurnGame<View, Move, Event>(this.host, joined, this.gameOptions);
145
+ }
146
+
147
+ /** Open a game the player sits in (from `hub.saved.opened`, the Continue list, an invite). */
148
+ async open<View = GamePayload, Move extends GamePayload = GamePayload, Event = GamePayload>(id: string): Promise<TurnGame<View, Move, Event>> {
149
+ return new TurnGame<View, Move, Event>(this.host, await this.host.get(id), this.gameOptions);
150
+ }
151
+
152
+ /** This game's online games the player sits in (the same rows as `hub.saved.list({ kinds: ['turns'] })`). */
153
+ list(): Promise<SavedEntry[]> {
154
+ return this.host.list();
155
+ }
156
+
157
+ /** Open-seat games of this game anyone may join (none across a block). */
158
+ openLobbies(): Promise<OpenTurnGame[]> {
159
+ return this.host.listOpen(this.host.slug);
160
+ }
161
+ }