@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
package/src/core/hub.ts CHANGED
@@ -1,131 +1,352 @@
1
- /**
2
- * The `Hub` client: every REST method (in `hub-progress.ts` and
3
- * `hub-economy.ts`), the hub's version, and the page-wide namespaces as lazy
4
- * getters (`hub.input`, `hub.daily`, `hub.social`, `hub.mp`, `hub.rooms`,
5
- * `hub.race`, ...), so v1 code such as `hub.input.define(...)` works unchanged.
6
- */
7
- import type { HubVersion } from '@diffenderfer-games/hub-contract';
8
- import { getDaily, type DailyHost, type DailySystem } from '../daily';
9
- import { getInput, getUiStack, type InputSystem } from '../input';
10
- import { mp } from '../mp';
11
- import { race } from '../race';
12
- import { rooms } from '../rooms';
13
- import { linkRealtimeToHub, notify, presence, social } from '../social';
14
- import { OfflineError, toHubError } from './errors';
15
- import { HubEconomy } from './hub-economy';
16
- import { hubMenu, type HubMenuControl } from './menu-control';
17
- import { emitWindowEvent } from './page';
18
- import { fetchGameServerTicket } from './ticket';
19
- import { clientSupport, readHubVersion, type ClientSupport } from './version/hub-version';
20
- import { HUB_CLIENT_VERSION } from './version/client-version.gen';
21
- import type { HubOptions } from './types';
22
-
23
- /** The hub client. Use the ready-made {@link hub}, or `createHub()` for another slug or API base. */
24
- export class Hub extends HubEconomy {
25
- /** This client's package version (sent as `x-hub-client`). */
26
- readonly clientVersion: string = HUB_CLIENT_VERSION;
27
-
28
- /**
29
- * The hub's version and the client versions it supports (`GET /_api/version`),
30
- * read once per page. `null` for a hub from before versioning.
31
- */
32
- hubVersion(): Promise<HubVersion | null> {
33
- return readHubVersion(this.rest);
34
- }
35
-
36
- /** How the hub regards this client: `supported`, `deprecated`, `unsupported`, or `unknown` (an older hub). */
37
- async clientSupport(): Promise<ClientSupport> {
38
- return clientSupport(await this.hubVersion());
39
- }
40
-
41
- /**
42
- * The input system (keyboard, touch and gamepad): `hub.input.define({ ... })`,
43
- * `hub.input.value('jump')`. Page-wide; remaps are saved locally and to the
44
- * reserved `__controls` save slot.
45
- */
46
- get input(): InputSystem {
47
- return getInput({
48
- slug: this.slug,
49
- saveToCloud: (overrides) => {
50
- void this.putSave('__controls', overrides).catch(() => undefined);
51
- },
52
- });
53
- }
54
-
55
- /** Hub UI covering the game (menu, dialogs, chat): open/close events and `autoPause`. Page-wide. */
56
- get overlay(): ReturnType<typeof getUiStack> {
57
- return getUiStack();
58
- }
59
-
60
- /** Show or hide the injected hub menu button (page-wide). */
61
- get menu(): HubMenuControl {
62
- return hubMenu;
63
- }
64
-
65
- /**
66
- * Daily challenges: call `hub.daily.define({ play })` once at load and
67
- * `hub.daily.complete({ ... })` on finish. Page-wide, shared with the
68
- * injected menu, which can start a clicked day on the same instance.
69
- */
70
- get daily(): DailySystem {
71
- return getDaily(this.dailyHost());
72
- }
73
-
74
- /** Tell friends what the player is doing in this game. */
75
- get presence(): typeof presence {
76
- return presence;
77
- }
78
-
79
- /** Friends, players and the hub's social dialogs. */
80
- get social(): typeof social {
81
- return social;
82
- }
83
-
84
- /** Invites, launches, the leave guard and own-server tickets. */
85
- get mp(): typeof mp {
86
- return mp;
87
- }
88
-
89
- /** Hub relay rooms, for games without a server. */
90
- get rooms(): typeof rooms {
91
- return rooms;
92
- }
93
-
94
- /** Toast behaviour during intense play. */
95
- get notify(): typeof notify {
96
- return notify;
97
- }
98
-
99
- /** Races: a single-player game becomes multiplayer by racing. */
100
- get race(): typeof race {
101
- return race;
102
- }
103
-
104
- /** What the daily system needs from this client: its REST calls and window events. */
105
- private dailyHost(): DailyHost {
106
- return {
107
- slug: this.slug,
108
- status: () => this.rest.call('getGameDailyChallenges', this.gameFields()),
109
- all: () => this.rest.call('listDailyChallengesForAllGames'),
110
- start: (day) => this.rest.call('startDailyChallenge', { ...this.gameFields(), day }),
111
- complete: (day, token, meta) => this.rest.call('completeDailyChallenge', { ...this.gameFields(), day, token, meta }),
112
- emit: (eventName, detail) => {
113
- emitWindowEvent(eventName, detail);
114
- },
115
- };
116
- }
117
- }
118
-
119
- linkRealtimeToHub({
120
- toHubError,
121
- createOfflineError: (message) => new OfflineError(message),
122
- getGameServerTicket: fetchGameServerTicket,
123
- });
124
-
125
- /** A ready-to-use client, with the slug and API base detected from the page. */
126
- export const hub: Hub = new Hub();
127
-
128
- /** Create a client for another slug or API base, or with other offline behaviour. */
129
- export function createHub(options: HubOptions = {}): Hub {
130
- return new Hub(options);
131
- }
1
+ /**
2
+ * The `Hub` client: every REST method (in `hub-progress.ts` and
3
+ * `hub-economy.ts`), the hub's version, and the page-wide namespaces as lazy
4
+ * getters (`hub.input`, `hub.daily`, `hub.social`, `hub.mp`, `hub.rooms`,
5
+ * `hub.race`, ...), so v1 code such as `hub.input.define(...)` works unchanged.
6
+ */
7
+ import type { HubVersion, ResumeSlot } from '@diffenderfer-games/hub-contract';
8
+ import { getDaily, type DailyCompleteResponse, type DailyHost, type DailySystem } from '../daily';
9
+ import { getInput, getUiStack, type InputSystem } from '../input';
10
+ import { mp } from '../mp';
11
+ import { race } from '../race';
12
+ import { rooms } from '../rooms';
13
+ import { pageBridge } from '../realtime/bridge';
14
+ import { isInHubRoom } from '../realtime/rooms-forwarder';
15
+ import { linkRealtimeToHub, notify, presence, social } from '../social';
16
+ import { HubAvatar } from './avatar/hub-avatar';
17
+ import { HubError, OfflineError, toHubError } from './errors';
18
+ import { HubEconomy } from './hub-economy';
19
+ import { AVATAR_KIND, DAILY_KIND, RESUME_KIND, SAVED_KIND } from './offline/hub-kinds';
20
+ import { readDailyCompletion } from './offline/hub-kinds/daily-kind';
21
+ import { hubMenu, type HubMenuControl } from './menu-control';
22
+ import { detectGameTitle, emitWindowEvent } from './page';
23
+ import { HubResume, type ResumeHost } from './resume/hub-resume';
24
+ import { isResumeRequested } from './resume/resume-request';
25
+ import { DailySaves } from './saved/daily-saves';
26
+ import { HubSaved, type SavedHost } from './saved/hub-saved';
27
+ import { HubTurns, type TurnsHost } from './turns/hub-turns';
28
+ import { savedOpenedOnPage } from './saved/saved-open';
29
+ import type { SavedEntry } from './saved/saved-types';
30
+ import { fetchGameServerTicket } from './ticket';
31
+ import { clientSupport, readHubVersion, type ClientSupport } from './version/hub-version';
32
+ import { HUB_CLIENT_VERSION } from './version/client-version.gen';
33
+ import type { HubOptions } from './types';
34
+
35
+ /** The hub client. Use the ready-made {@link hub}, or `createHub()` for another slug or API base. */
36
+ export class Hub extends HubEconomy {
37
+ /** This client's package version (sent as `x-hub-client`). */
38
+ readonly clientVersion: string = HUB_CLIENT_VERSION;
39
+
40
+ private resumeApi: HubResume | undefined;
41
+ private avatarApi: HubAvatar | undefined;
42
+ private savedApi: HubSaved | undefined;
43
+ private dailySavesApi: DailySaves | undefined;
44
+ private turnsApi: HubTurns | undefined;
45
+ /** The online games open on this page (a run isn't saved while one is). */
46
+ private readonly openTurnGames = new Set<object>();
47
+
48
+ /**
49
+ * The hub's version and the client versions it supports (`GET /_api/version`),
50
+ * read once per page. `null` for a hub from before versioning.
51
+ */
52
+ hubVersion(): Promise<HubVersion | null> {
53
+ return readHubVersion(this.rest);
54
+ }
55
+
56
+ /** How the hub regards this client: `supported`, `deprecated`, `unsupported`, or `unknown` (an older hub). */
57
+ async clientSupport(): Promise<ClientSupport> {
58
+ return clientSupport(await this.hubVersion());
59
+ }
60
+
61
+ /**
62
+ * The input system (keyboard, touch and gamepad): `hub.input.define({ ... })`,
63
+ * `hub.input.value('jump')`. Page-wide; remaps are saved locally and to the
64
+ * reserved `__controls` save slot.
65
+ */
66
+ get input(): InputSystem {
67
+ return getInput({
68
+ slug: this.slug,
69
+ saveToCloud: (overrides) => {
70
+ void this.putSave('__controls', overrides).catch(() => undefined);
71
+ },
72
+ });
73
+ }
74
+
75
+ /** Hub UI covering the game (menu, dialogs, chat): open/close events and `autoPause`. Page-wide. */
76
+ get overlay(): ReturnType<typeof getUiStack> {
77
+ return getUiStack();
78
+ }
79
+
80
+ /** Show or hide the injected hub menu button (page-wide). */
81
+ get menu(): HubMenuControl {
82
+ return hubMenu;
83
+ }
84
+
85
+ /**
86
+ * Daily challenges: call `hub.daily.define({ play })` once at load and
87
+ * `hub.daily.complete({ ... })` on finish. Page-wide, shared with the
88
+ * injected menu, which can start a clicked day on the same instance.
89
+ */
90
+ get daily(): DailySystem {
91
+ return getDaily(this.dailyHost());
92
+ }
93
+
94
+ /** Tell friends what the player is doing in this game. */
95
+ get presence(): typeof presence {
96
+ return presence;
97
+ }
98
+
99
+ /** Friends, players and the hub's social dialogs. */
100
+ get social(): typeof social {
101
+ return social;
102
+ }
103
+
104
+ /** Invites, launches, the leave guard and own-server tickets. */
105
+ get mp(): typeof mp {
106
+ return mp;
107
+ }
108
+
109
+ /** Hub relay rooms, for games without a server. */
110
+ get rooms(): typeof rooms {
111
+ return rooms;
112
+ }
113
+
114
+ /** Toast behaviour during intense play. */
115
+ get notify(): typeof notify {
116
+ return notify;
117
+ }
118
+
119
+ /** Races: a single-player game becomes multiplayer by racing. */
120
+ get race(): typeof race {
121
+ return race;
122
+ }
123
+
124
+ /**
125
+ * Resume a saved game: `hub.resume.save({ state, progress })` during a long
126
+ * single-player run, `clear()` when it ends, `load()` and `requested` to
127
+ * pick it up again (docs: "Resume saved games"). Needs `game.resumable: true`.
128
+ */
129
+ get resume(): HubResume {
130
+ this.resumeApi ??= new HubResume(this.resumeHost());
131
+ return this.resumeApi;
132
+ }
133
+
134
+ /**
135
+ * Players' avatars: `hub.avatar.mine()`, `of(userId)`, `from(seat)`,
136
+ * `bot(seed)`, `on('change')`, `openDesigner()` and `load()` for the renderer
137
+ * (docs: "Avatars"). No art or renderer until `load()`.
138
+ */
139
+ get avatar(): HubAvatar {
140
+ this.avatarApi ??= new HubAvatar({
141
+ currentUserId: () => this.userId(),
142
+ onAccount: (listener) => this.account.on(listener),
143
+ fetchMine: () => this.rest.call('getMyAvatar'),
144
+ fetchCodes: async (ids) => (await this.rest.call('getAvatars', { ids: [...ids] })).codes,
145
+ queueSave: async (code, savedAt) => {
146
+ await this.mutateOffline(AVATAR_KIND, { code, savedAt }, () => undefined);
147
+ },
148
+ });
149
+ return this.avatarApi;
150
+ }
151
+
152
+ /**
153
+ * Every game the player has in progress, of every kind: `hub.saved.list()`
154
+ * for this game's "Continue" list, `opened` and `onOpen` to open the one
155
+ * the player picked (docs: "Saved games").
156
+ */
157
+ get saved(): HubSaved {
158
+ this.savedApi ??= new HubSaved(this.savedHost());
159
+ return this.savedApi;
160
+ }
161
+
162
+ /**
163
+ * Online games played over minutes or days, run by the hub from the
164
+ * game's rules module (`game.turns`): `hub.turns.create`, `join`, `open`
165
+ * (docs/multiplayer.md "Turn games").
166
+ */
167
+ get turns(): HubTurns {
168
+ this.turnsApi ??= new HubTurns(this.turnsHost());
169
+ return this.turnsApi;
170
+ }
171
+
172
+ /** What `hub.turns` needs from this client: its REST calls. */
173
+ private turnsHost(): TurnsHost {
174
+ const { rest } = this;
175
+ return {
176
+ slug: this.slug,
177
+ create: (body) => rest.call('createTurnGame', body),
178
+ join: (id) => rest.call('joinTurnGame', { id }),
179
+ joinByLink: (key) => rest.call('joinTurnGameByLink', { key }),
180
+ get: (id) => rest.call('getTurnGame', { id }),
181
+ start: (id) => rest.call('startTurnGame', { id }),
182
+ move: (id, seq, move) => rest.call('moveTurnGame', { id, seq, move }),
183
+ history: async (id, since) => (await rest.call('getTurnGameHistory', { id, since })).moves,
184
+ leave: async (id) => { await rest.call('leaveTurnGame', { id }); },
185
+ nudge: async (id) => { await rest.call('nudgeTurnGame', { id }); },
186
+ setNotify: async (id, mode) => { await rest.call('setTurnGameNotify', { id, mode }); },
187
+ rematch: (id) => rest.call('rematchTurnGame', { id }),
188
+ invite: (id, userIds) => rest.call('inviteToTurnGame', { id, userIds: [...userIds] }),
189
+ share: (id, share) => rest.call('shareTurnGame', { id, ...share }),
190
+ autoPlay: (id) => rest.call('autoPlayTurn', { id }),
191
+ chat: (id, send) => rest.call('chatInTurnGame', { id, ...send }),
192
+ listOpen: async (game) => (await rest.call('listOpenTurnGames', game === null ? {} : { game })).games,
193
+ list: () => this.saved.list({ kinds: ['turns'] }),
194
+ opened: (game) => { this.openTurnGames.add(game); },
195
+ closed: (game) => { this.openTurnGames.delete(game); },
196
+ isLive: () => pageBridge.current() !== null,
197
+ };
198
+ }
199
+
200
+ /** What `hub.saved` needs from this client. */
201
+ private savedHost(): SavedHost {
202
+ return {
203
+ slug: this.slug,
204
+ fetchList: (game) => this.withFallback<readonly SavedEntry[] | undefined>(
205
+ async () => (await this.rest.call('listMySavedGames', game === null ? {} : { game })).games,
206
+ () => undefined,
207
+ ),
208
+ deviceCopies: () => {
209
+ const userId = this.store.getUser()?.id ?? null;
210
+ return {
211
+ runs: new Map([...this.resumeStore.all()].filter(([, copy]) => copy.userId === userId)),
212
+ dailies: this.dailyStore.all(userId),
213
+ };
214
+ },
215
+ discard: (entry) => this.discardSaved(entry),
216
+ navigate: (url) => {
217
+ location.assign(url);
218
+ },
219
+ };
220
+ }
221
+
222
+ /** Discard a daily or run: this game's run through `hub.resume` (works offline), others on the hub. */
223
+ private async discardSaved(entry: SavedEntry): Promise<void> {
224
+ if (entry.kind === 'run' && entry.game === this.slug) {
225
+ await this.resume.clear();
226
+ return;
227
+ }
228
+ if (!entry.id.startsWith('local:')) {
229
+ await this.rest.call('discardSavedGame', { id: entry.id });
230
+ }
231
+ if (entry.kind === 'run') {
232
+ this.resumeStore.remove(entry.game);
233
+ } else if (entry.daily !== undefined) {
234
+ this.dailyStore.remove(entry.game, entry.daily.day);
235
+ }
236
+ }
237
+
238
+ /** What `hub.resume` needs from this client. */
239
+ private resumeHost(): ResumeHost {
240
+ return {
241
+ slug: this.slug,
242
+ local: this.resumeStore,
243
+ currentUserId: () => this.store.getUser()?.id ?? null,
244
+ gameTitle: () => detectGameTitle(this.slug ?? ''),
245
+ blocker: () => {
246
+ if (race.current() !== null) {
247
+ return 'race';
248
+ }
249
+ if (this.openTurnGames.size > 0) {
250
+ return 'turns';
251
+ }
252
+ return isInHubRoom() ? 'room' : null;
253
+ },
254
+ daily: () => {
255
+ const daily = this.daily;
256
+ return daily.active === null ? null : daily;
257
+ },
258
+ send: async (payload) => {
259
+ await this.mutateOffline(RESUME_KIND, payload, () => undefined);
260
+ },
261
+ fetchSlot: () => this.withFallback<ResumeSlot | null | undefined>(async () => {
262
+ try {
263
+ return (await this.rest.call('getResumeSlot', this.gameFields())).resume;
264
+ } catch (failure) {
265
+ if (failure instanceof HubError && failure.status === 401) {
266
+ return null;
267
+ }
268
+ throw failure;
269
+ }
270
+ }, () => undefined),
271
+ fetchList: () => this.withFallback(async () => (await this.rest.call('listMyResumeSlots')).games, () => undefined),
272
+ clearOther: async (slug) => {
273
+ await this.rest.call('clearResumeSlot', { slug });
274
+ },
275
+ onChange: () => {
276
+ this.savedApi?.changed();
277
+ },
278
+ };
279
+ }
280
+
281
+ /** This game's daily saves (device copies and their sends). */
282
+ private dailySaves(): DailySaves {
283
+ const slug = this.slug;
284
+ if (slug === null) {
285
+ throw new HubError('hub.daily: could not determine the game slug.');
286
+ }
287
+ this.dailySavesApi ??= new DailySaves({
288
+ slug,
289
+ local: this.dailyStore,
290
+ currentUserId: () => this.store.getUser()?.id ?? null,
291
+ gameTitle: () => detectGameTitle(slug),
292
+ send: async (payload) => {
293
+ await this.mutateOffline(SAVED_KIND, payload, () => undefined);
294
+ this.savedApi?.changed();
295
+ },
296
+ });
297
+ return this.dailySavesApi;
298
+ }
299
+
300
+ /** What the daily system needs from this client: its REST calls, the device copies and window events. */
301
+ private dailyHost(): DailyHost {
302
+ return {
303
+ slug: this.slug,
304
+ status: () => this.rest.call('getGameDailyChallenges', this.gameFields()),
305
+ all: () => this.rest.call('listDailyChallengesForAllGames'),
306
+ start: (day) => this.withFallback(
307
+ async () => this.dailySaves().remember(await this.rest.call('startDailyChallenge', { ...this.gameFields(), day })),
308
+ () => this.dailySaves().startLocally(day),
309
+ ),
310
+ resume: async (instanceId) => this.dailySaves().remember(await this.rest.call('resumeDailyChallenge', { id: instanceId })),
311
+ complete: async (day, token, meta) => {
312
+ this.dailySaves().finished(day);
313
+ const completed = await this.mutateOffline<DailyCompleteResponse | null>(DAILY_KIND, { day, token, meta }, () => null, (result) => readDailyCompletion(result));
314
+ if (completed !== null) {
315
+ this.dailySaves().completed(day);
316
+ }
317
+ this.savedApi?.changed();
318
+ return completed;
319
+ },
320
+ save: (challenge, input) => {
321
+ const isSaved = this.dailySaves().save(challenge, input);
322
+ this.savedApi?.changed();
323
+ return isSaved;
324
+ },
325
+ flush: () => this.dailySaves().flush(),
326
+ discard: (challenge) => this.dailySaves().discard(challenge),
327
+ inProgress: () => this.saved.list({ kinds: ['daily'] }),
328
+ emit: (eventName, detail) => {
329
+ emitWindowEvent(eventName, detail);
330
+ },
331
+ };
332
+ }
333
+ }
334
+
335
+ linkRealtimeToHub({
336
+ toHubError,
337
+ createOfflineError: (message) => new OfflineError(message),
338
+ getGameServerTicket: fetchGameServerTicket,
339
+ });
340
+
341
+ /** A ready-to-use client, with the slug and API base detected from the page. */
342
+ export const hub: Hub = new Hub();
343
+
344
+ // Take `?hub_resume`, `?hub_open` and `?hub_kind` out of the address bar before the game's router sees them
345
+ // (the answers stay on `hub.resume.requested` and `hub.saved.opened`).
346
+ isResumeRequested();
347
+ savedOpenedOnPage();
348
+
349
+ /** Create a client for another slug or API base, or with other offline behaviour. */
350
+ export function createHub(options: HubOptions = {}): Hub {
351
+ return new Hub(options);
352
+ }
@@ -1,61 +1,61 @@
1
- /**
2
- * `hub.menu`: show or hide the injected hub menu button. The menu
3
- * (`/_hub/menu.js`) may load before or after the game, and the game may
4
- * bundle its own client copy, so the shared state is a window global and a
5
- * window event rather than an object:
6
- *
7
- * - `window.__HUB_MENU_VISIBLE__` (boolean; absent means visible);
8
- * - `hub:menu-visibility` (`CustomEvent`, detail `{ visible }`).
9
- *
10
- * The menu reads the global on boot and listens for the event.
11
- */
12
- import { emitWindowEvent, isBrowserPage } from './page';
13
-
14
- declare global {
15
- interface Window {
16
- /** The last requested menu-button visibility (absent: visible). */
17
- __HUB_MENU_VISIBLE__?: boolean;
18
- }
19
- }
20
-
21
- /** Control over the injected hub menu button. */
22
- export interface HubMenuControl {
23
- /**
24
- * Show (`true`) or hide (`false`) the menu button. Hidden means faded out,
25
- * not focusable and `aria-hidden`; an open drawer stays open until closed,
26
- * and the gamepad Select button still opens it. A small corner hotspot
27
- * peeks it back for a few seconds on hover or tap, so players can always
28
- * reach it.
29
- */
30
- setVisible(isVisible: boolean): void;
31
- /** Show the menu button. */
32
- show(): void;
33
- /** Hide the menu button. */
34
- hide(): void;
35
- /** The last requested visibility (`true` unless a game hid it). */
36
- readonly visible: boolean;
37
- }
38
-
39
- /** The page's menu control (one per page; every client copy shares the window state). */
40
- export const hubMenu: HubMenuControl = {
41
- setVisible(isVisible: boolean): void {
42
- if (!isBrowserPage()) {
43
- return;
44
- }
45
- const wasVisible = window.__HUB_MENU_VISIBLE__;
46
- if (wasVisible === isVisible || (wasVisible === undefined && isVisible)) {
47
- return;
48
- }
49
- window.__HUB_MENU_VISIBLE__ = isVisible;
50
- emitWindowEvent('hub:menu-visibility', { visible: isVisible });
51
- },
52
- show(): void {
53
- hubMenu.setVisible(true);
54
- },
55
- hide(): void {
56
- hubMenu.setVisible(false);
57
- },
58
- get visible(): boolean {
59
- return !isBrowserPage() || window.__HUB_MENU_VISIBLE__ !== false;
60
- },
61
- };
1
+ /**
2
+ * `hub.menu`: show or hide the injected hub menu button. The menu
3
+ * (`/_hub/menu.js`) may load before or after the game, and the game may
4
+ * bundle its own client copy, so the shared state is a window global and a
5
+ * window event rather than an object:
6
+ *
7
+ * - `window.__HUB_MENU_VISIBLE__` (boolean; absent means visible);
8
+ * - `hub:menu-visibility` (`CustomEvent`, detail `{ visible }`).
9
+ *
10
+ * The menu reads the global on boot and listens for the event.
11
+ */
12
+ import { emitWindowEvent, isBrowserPage } from './page';
13
+
14
+ declare global {
15
+ interface Window {
16
+ /** The last requested menu-button visibility (absent: visible). */
17
+ __HUB_MENU_VISIBLE__?: boolean;
18
+ }
19
+ }
20
+
21
+ /** Control over the injected hub menu button. */
22
+ export interface HubMenuControl {
23
+ /**
24
+ * Show (`true`) or hide (`false`) the menu button. Hidden means faded out,
25
+ * not focusable and `aria-hidden`; an open drawer stays open until closed,
26
+ * and the gamepad Select button still opens it. A small corner hotspot
27
+ * peeks it back for a few seconds on hover or tap, so players can always
28
+ * reach it.
29
+ */
30
+ setVisible(isVisible: boolean): void;
31
+ /** Show the menu button. */
32
+ show(): void;
33
+ /** Hide the menu button. */
34
+ hide(): void;
35
+ /** The last requested visibility (`true` unless a game hid it). */
36
+ readonly visible: boolean;
37
+ }
38
+
39
+ /** The page's menu control (one per page; every client copy shares the window state). */
40
+ export const hubMenu: HubMenuControl = {
41
+ setVisible(isVisible: boolean): void {
42
+ if (!isBrowserPage()) {
43
+ return;
44
+ }
45
+ const wasVisible = window.__HUB_MENU_VISIBLE__;
46
+ if (wasVisible === isVisible || (wasVisible === undefined && isVisible)) {
47
+ return;
48
+ }
49
+ window.__HUB_MENU_VISIBLE__ = isVisible;
50
+ emitWindowEvent('hub:menu-visibility', { visible: isVisible });
51
+ },
52
+ show(): void {
53
+ hubMenu.setVisible(true);
54
+ },
55
+ hide(): void {
56
+ hubMenu.setVisible(false);
57
+ },
58
+ get visible(): boolean {
59
+ return !isBrowserPage() || window.__HUB_MENU_VISIBLE__ !== false;
60
+ },
61
+ };