@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,76 +1,76 @@
1
- /**
2
- * The overlay stack's public shapes: "is the player looking at hub UI right
3
- * now?" (`hub.overlay`, docs/multiplayer.md §5.7).
4
- */
5
-
6
- /** Why the overlay opened, so a game can word its pause screen. */
7
- export type OverlayReason = 'menu' | 'chat' | 'dialog' | 'keyboard' | 'invite' | 'notify';
8
-
9
- /** An overlay edge. */
10
- export type OverlayEdge = 'open' | 'close';
11
-
12
- /** Payload of `open` and `close` (and of the `hub:overlay-open` / `hub:overlay-close` window events). */
13
- export interface OverlayEvent {
14
- /** The reason of the entry that opened (for `open`) or closed last (for `close`). */
15
- reason: OverlayReason;
16
- /**
17
- * False during a live online match (`hub.mp.setBusy(true)`): the game must
18
- * keep simulating, so it should not pause.
19
- */
20
- canPause: boolean;
21
- }
22
-
23
- /** Options for {@link UiStack.autoPause}. */
24
- export interface AutoPauseOptions {
25
- /** Pause exactly the way the game's own pause does. */
26
- pause(): void;
27
- /** Resume play. Only called when `resumeOnClose` is true. */
28
- resume?(): void;
29
- /**
30
- * Resume automatically when the hub UI closes. Default false: action games
31
- * should show their pause screen so the player resumes deliberately;
32
- * turn-based and idle games may pass true.
33
- */
34
- resumeOnClose?: boolean;
35
- }
36
-
37
- /** A listener for overlay edges. */
38
- export type OverlayListener = (event: OverlayEvent) => void;
39
-
40
- /** What an older, idle stack hands to a newer one replacing it. */
41
- export interface UiStackHandoff {
42
- /** Its listeners, by edge. */
43
- listeners: { open: OverlayListener[]; close: OverlayListener[] };
44
- /** Its pause permission. */
45
- canPause: boolean;
46
- }
47
-
48
- /**
49
- * The page-wide overlay stack. Every piece of hub UI that covers the game
50
- * (the menu drawer, a dialog, the chat sheet…) pushes an entry while it shows
51
- * and pops it when it goes away; the game sees one `open` / `close` pair for
52
- * the whole visit, however the UI nests.
53
- */
54
- export interface UiStack {
55
- /** Implementation version; a newer bundle replaces an older, idle instance. */
56
- readonly version: number;
57
- /** Register a covering piece of UI. Returns its idempotent `pop()`. */
58
- push(reason: OverlayReason): () => void;
59
- /** True while any hub UI covers the game. */
60
- readonly isOpen: boolean;
61
- /** Number of open entries (nesting depth). */
62
- readonly depth: number;
63
- /** Whether the game may pause right now (false during an online match). */
64
- readonly canPause: boolean;
65
- /** Subscribe to an edge. Returns the unsubscribe function. */
66
- on(edge: OverlayEdge, listener: OverlayListener): () => void;
67
- /**
68
- * One-line adoption for games: pause on open (unless `canPause` is false),
69
- * optionally resume on close. Returns a function that detaches it.
70
- */
71
- autoPause(options: AutoPauseOptions): () => void;
72
- /** Driven by `hub.mp.setBusy`: false while a live match can't pause. */
73
- setCanPause(canPause: boolean): void;
74
- /** Hand listeners to a newer version replacing this (idle) instance. */
75
- handoff(): UiStackHandoff;
76
- }
1
+ /**
2
+ * The overlay stack's public shapes: "is the player looking at hub UI right
3
+ * now?" (`hub.overlay`, docs/multiplayer.md §5.7).
4
+ */
5
+
6
+ /** Why the overlay opened, so a game can word its pause screen. */
7
+ export type OverlayReason = 'menu' | 'chat' | 'dialog' | 'keyboard' | 'invite' | 'notify';
8
+
9
+ /** An overlay edge. */
10
+ export type OverlayEdge = 'open' | 'close';
11
+
12
+ /** Payload of `open` and `close` (and of the `hub:overlay-open` / `hub:overlay-close` window events). */
13
+ export interface OverlayEvent {
14
+ /** The reason of the entry that opened (for `open`) or closed last (for `close`). */
15
+ reason: OverlayReason;
16
+ /**
17
+ * False during a live online match (`hub.mp.setBusy(true)`): the game must
18
+ * keep simulating, so it should not pause.
19
+ */
20
+ canPause: boolean;
21
+ }
22
+
23
+ /** Options for {@link UiStack.autoPause}. */
24
+ export interface AutoPauseOptions {
25
+ /** Pause exactly the way the game's own pause does. */
26
+ pause(): void;
27
+ /** Resume play. Only called when `resumeOnClose` is true. */
28
+ resume?(): void;
29
+ /**
30
+ * Resume automatically when the hub UI closes. Default false: action games
31
+ * should show their pause screen so the player resumes deliberately;
32
+ * turn-based and idle games may pass true.
33
+ */
34
+ resumeOnClose?: boolean;
35
+ }
36
+
37
+ /** A listener for overlay edges. */
38
+ export type OverlayListener = (event: OverlayEvent) => void;
39
+
40
+ /** What an older, idle stack hands to a newer one replacing it. */
41
+ export interface UiStackHandoff {
42
+ /** Its listeners, by edge. */
43
+ listeners: { open: OverlayListener[]; close: OverlayListener[] };
44
+ /** Its pause permission. */
45
+ canPause: boolean;
46
+ }
47
+
48
+ /**
49
+ * The page-wide overlay stack. Every piece of hub UI that covers the game
50
+ * (the menu drawer, a dialog, the chat sheet…) pushes an entry while it shows
51
+ * and pops it when it goes away; the game sees one `open` / `close` pair for
52
+ * the whole visit, however the UI nests.
53
+ */
54
+ export interface UiStack {
55
+ /** Implementation version; a newer bundle replaces an older, idle instance. */
56
+ readonly version: number;
57
+ /** Register a covering piece of UI. Returns its idempotent `pop()`. */
58
+ push(reason: OverlayReason): () => void;
59
+ /** True while any hub UI covers the game. */
60
+ readonly isOpen: boolean;
61
+ /** Number of open entries (nesting depth). */
62
+ readonly depth: number;
63
+ /** Whether the game may pause right now (false during an online match). */
64
+ readonly canPause: boolean;
65
+ /** Subscribe to an edge. Returns the unsubscribe function. */
66
+ on(edge: OverlayEdge, listener: OverlayListener): () => void;
67
+ /**
68
+ * One-line adoption for games: pause on open (unless `canPause` is false),
69
+ * optionally resume on close. Returns a function that detaches it.
70
+ */
71
+ autoPause(options: AutoPauseOptions): () => void;
72
+ /** Driven by `hub.mp.setBusy`: false while a live match can't pause. */
73
+ setCanPause(canPause: boolean): void;
74
+ /** Hand listeners to a newer version replacing this (idle) instance. */
75
+ handoff(): UiStackHandoff;
76
+ }
@@ -1,144 +1,144 @@
1
- /**
2
- * The overlay stack implementation: two edges (`open` when depth goes
3
- * 0 → 1, `close` when it returns to 0) mirrored as window events, the
4
- * `autoPause` helper, and the `play` suspension.
5
- */
6
- import { LEGACY_GLOBAL_NAMES } from '../globals';
7
- import { isGroupSwitch, PlaySuspension } from './play-suspension';
8
- import type { AutoPauseOptions, OverlayEdge, OverlayEvent, OverlayListener, OverlayReason, UiStack, UiStackHandoff } from './types';
9
-
10
- /** Bump when the UiStack contract gains behaviour; idle older instances are replaced. */
11
- export const UI_STACK_VERSION = 1;
12
-
13
- /** One open entry (its identity is what `pop` removes). */
14
- interface StackEntry {
15
- readonly reason: OverlayReason;
16
- }
17
-
18
- /** The page-wide overlay stack (see {@link UiStack}). */
19
- export class UiStackImpl implements UiStack {
20
- readonly version = UI_STACK_VERSION;
21
- private readonly entries: StackEntry[] = [];
22
- private readonly listeners: Record<OverlayEdge, Set<OverlayListener>> = { open: new Set(), close: new Set() };
23
- private isPausable = true;
24
- private suspension: PlaySuspension | null = null;
25
-
26
- constructor(handoff?: UiStackHandoff) {
27
- if (handoff === undefined) {
28
- return;
29
- }
30
- handoff.listeners.open.forEach((listener) => this.listeners.open.add(listener));
31
- handoff.listeners.close.forEach((listener) => this.listeners.close.add(listener));
32
- this.isPausable = handoff.canPause;
33
- }
34
-
35
- get isOpen(): boolean {
36
- return this.entries.length > 0;
37
- }
38
-
39
- get depth(): number {
40
- return this.entries.length;
41
- }
42
-
43
- get canPause(): boolean {
44
- return this.isPausable;
45
- }
46
-
47
- push(reason: OverlayReason): () => void {
48
- const entry: StackEntry = { reason };
49
- this.entries.push(entry);
50
- if (this.entries.length === 1) {
51
- this.suspendPlay();
52
- this.emit('open', { reason, canPause: this.isPausable });
53
- }
54
- let wasPopped = false;
55
- return () => {
56
- if (wasPopped) {
57
- return;
58
- }
59
- wasPopped = true;
60
- const index = this.entries.indexOf(entry);
61
- if (index < 0) {
62
- return;
63
- }
64
- this.entries.splice(index, 1);
65
- if (this.entries.length === 0) {
66
- // Listeners run before `play` is restored, so a game that pauses in its close handler is honoured.
67
- this.emit('close', { reason, canPause: this.isPausable });
68
- this.suspension?.release();
69
- this.suspension = null;
70
- }
71
- };
72
- }
73
-
74
- on(edge: OverlayEdge, listener: OverlayListener): () => void {
75
- this.listeners[edge].add(listener);
76
- return () => {
77
- this.listeners[edge].delete(listener);
78
- };
79
- }
80
-
81
- autoPause(options: AutoPauseOptions): () => void {
82
- let wasPausedByUs = false;
83
- const pauseOnOpen = (event: OverlayEvent): void => {
84
- if (!event.canPause) {
85
- return;
86
- }
87
- wasPausedByUs = true;
88
- options.pause();
89
- };
90
- const resumeOnClose = (): void => {
91
- if (wasPausedByUs && options.resumeOnClose === true) {
92
- options.resume?.();
93
- }
94
- wasPausedByUs = false;
95
- };
96
- const stopOpen = this.on('open', pauseOnOpen);
97
- const stopClose = this.on('close', resumeOnClose);
98
- // Registered while hub UI is already up: pause now, as if it just opened.
99
- const firstEntry = this.entries[0];
100
- if (firstEntry !== undefined) {
101
- pauseOnOpen({ reason: firstEntry.reason, canPause: this.isPausable });
102
- }
103
- return () => {
104
- stopOpen();
105
- stopClose();
106
- };
107
- }
108
-
109
- setCanPause(canPause: boolean): void {
110
- this.isPausable = canPause;
111
- }
112
-
113
- handoff(): UiStackHandoff {
114
- const handoff: UiStackHandoff = {
115
- listeners: { open: [...this.listeners.open], close: [...this.listeners.close] },
116
- canPause: this.isPausable,
117
- };
118
- this.listeners.open.clear();
119
- this.listeners.close.clear();
120
- return handoff;
121
- }
122
-
123
- private suspendPlay(): void {
124
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: the page input system may be another bundle's
125
- const input: unknown = typeof window === 'undefined' ? undefined : window[LEGACY_GLOBAL_NAMES.input];
126
- if (this.suspension === null && isGroupSwitch(input)) {
127
- this.suspension = new PlaySuspension(input);
128
- }
129
- }
130
-
131
- private emit(edge: OverlayEdge, event: OverlayEvent): void {
132
- for (const listener of [...this.listeners[edge]]) {
133
- try {
134
- listener(event);
135
- } catch (error) {
136
- // eslint-disable-next-line no-console -- a game's listener failing must not break the hub UI; surface it
137
- console.error('[hub] overlay listener failed', error);
138
- }
139
- }
140
- if (typeof window !== 'undefined') {
141
- window.dispatchEvent(new CustomEvent(`hub:overlay-${edge}`, { detail: event }));
142
- }
143
- }
144
- }
1
+ /**
2
+ * The overlay stack implementation: two edges (`open` when depth goes
3
+ * 0 → 1, `close` when it returns to 0) mirrored as window events, the
4
+ * `autoPause` helper, and the `play` suspension.
5
+ */
6
+ import { LEGACY_GLOBAL_NAMES } from '../globals';
7
+ import { isGroupSwitch, PlaySuspension } from './play-suspension';
8
+ import type { AutoPauseOptions, OverlayEdge, OverlayEvent, OverlayListener, OverlayReason, UiStack, UiStackHandoff } from './types';
9
+
10
+ /** Bump when the UiStack contract gains behaviour; idle older instances are replaced. */
11
+ export const UI_STACK_VERSION = 1;
12
+
13
+ /** One open entry (its identity is what `pop` removes). */
14
+ interface StackEntry {
15
+ readonly reason: OverlayReason;
16
+ }
17
+
18
+ /** The page-wide overlay stack (see {@link UiStack}). */
19
+ export class UiStackImpl implements UiStack {
20
+ readonly version = UI_STACK_VERSION;
21
+ private readonly entries: StackEntry[] = [];
22
+ private readonly listeners: Record<OverlayEdge, Set<OverlayListener>> = { open: new Set(), close: new Set() };
23
+ private isPausable = true;
24
+ private suspension: PlaySuspension | null = null;
25
+
26
+ constructor(handoff?: UiStackHandoff) {
27
+ if (handoff === undefined) {
28
+ return;
29
+ }
30
+ handoff.listeners.open.forEach((listener) => this.listeners.open.add(listener));
31
+ handoff.listeners.close.forEach((listener) => this.listeners.close.add(listener));
32
+ this.isPausable = handoff.canPause;
33
+ }
34
+
35
+ get isOpen(): boolean {
36
+ return this.entries.length > 0;
37
+ }
38
+
39
+ get depth(): number {
40
+ return this.entries.length;
41
+ }
42
+
43
+ get canPause(): boolean {
44
+ return this.isPausable;
45
+ }
46
+
47
+ push(reason: OverlayReason): () => void {
48
+ const entry: StackEntry = { reason };
49
+ this.entries.push(entry);
50
+ if (this.entries.length === 1) {
51
+ this.suspendPlay();
52
+ this.emit('open', { reason, canPause: this.isPausable });
53
+ }
54
+ let wasPopped = false;
55
+ return () => {
56
+ if (wasPopped) {
57
+ return;
58
+ }
59
+ wasPopped = true;
60
+ const index = this.entries.indexOf(entry);
61
+ if (index < 0) {
62
+ return;
63
+ }
64
+ this.entries.splice(index, 1);
65
+ if (this.entries.length === 0) {
66
+ // Listeners run before `play` is restored, so a game that pauses in its close handler is honoured.
67
+ this.emit('close', { reason, canPause: this.isPausable });
68
+ this.suspension?.release();
69
+ this.suspension = null;
70
+ }
71
+ };
72
+ }
73
+
74
+ on(edge: OverlayEdge, listener: OverlayListener): () => void {
75
+ this.listeners[edge].add(listener);
76
+ return () => {
77
+ this.listeners[edge].delete(listener);
78
+ };
79
+ }
80
+
81
+ autoPause(options: AutoPauseOptions): () => void {
82
+ let wasPausedByUs = false;
83
+ const pauseOnOpen = (event: OverlayEvent): void => {
84
+ if (!event.canPause) {
85
+ return;
86
+ }
87
+ wasPausedByUs = true;
88
+ options.pause();
89
+ };
90
+ const resumeOnClose = (): void => {
91
+ if (wasPausedByUs && options.resumeOnClose === true) {
92
+ options.resume?.();
93
+ }
94
+ wasPausedByUs = false;
95
+ };
96
+ const stopOpen = this.on('open', pauseOnOpen);
97
+ const stopClose = this.on('close', resumeOnClose);
98
+ // Registered while hub UI is already up: pause now, as if it just opened.
99
+ const firstEntry = this.entries[0];
100
+ if (firstEntry !== undefined) {
101
+ pauseOnOpen({ reason: firstEntry.reason, canPause: this.isPausable });
102
+ }
103
+ return () => {
104
+ stopOpen();
105
+ stopClose();
106
+ };
107
+ }
108
+
109
+ setCanPause(canPause: boolean): void {
110
+ this.isPausable = canPause;
111
+ }
112
+
113
+ handoff(): UiStackHandoff {
114
+ const handoff: UiStackHandoff = {
115
+ listeners: { open: [...this.listeners.open], close: [...this.listeners.close] },
116
+ canPause: this.isPausable,
117
+ };
118
+ this.listeners.open.clear();
119
+ this.listeners.close.clear();
120
+ return handoff;
121
+ }
122
+
123
+ private suspendPlay(): void {
124
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the page input system may be another bundle's
125
+ const input: unknown = typeof window === 'undefined' ? undefined : window[LEGACY_GLOBAL_NAMES.input];
126
+ if (this.suspension === null && isGroupSwitch(input)) {
127
+ this.suspension = new PlaySuspension(input);
128
+ }
129
+ }
130
+
131
+ private emit(edge: OverlayEdge, event: OverlayEvent): void {
132
+ for (const listener of [...this.listeners[edge]]) {
133
+ try {
134
+ listener(event);
135
+ } catch (error) {
136
+ // eslint-disable-next-line no-console -- a game's listener failing must not break the hub UI; surface it
137
+ console.error('[hub] overlay listener failed', error);
138
+ }
139
+ }
140
+ if (typeof window !== 'undefined') {
141
+ window.dispatchEvent(new CustomEvent(`hub:overlay-${edge}`, { detail: event }));
142
+ }
143
+ }
144
+ }