@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
@@ -0,0 +1,329 @@
1
+ /**
2
+ * `createCrowd` (§3.11): the world's whole avatar layer in Pixi 8. One
3
+ * y-sorted container; most avatars are atlas frames (shared page textures,
4
+ * so Pixi batches them), and up to `liveMax` flagged ones (nearest, hovered,
5
+ * talking, friends; the world decides) are live vector avatars with full
6
+ * expressions. Name tags and bubbles are cached textures, re-rendered only
7
+ * when their text changes. Baking is time-sliced; an avatar without a baked
8
+ * frame yet shows the silhouette.
9
+ */
10
+ import { animator, type Animator } from '../animator/animator';
11
+ import type { AvatarAtlas } from '../atlas';
12
+ import { expressionFromEmote } from '../face';
13
+ import { layout } from '../layout';
14
+ import { toCode } from '../look-like';
15
+ import { defaultLayer } from '../outputs/canvas';
16
+ import { paintNodes } from '../outputs/canvas-paint';
17
+ import { overlays, type Overlay, type ReactionGlyph } from '../overlays';
18
+ import { frameBox } from '../render/frame';
19
+ import type { BodyPose, Facing, Figure, LookLike, StatusProp } from '../render/types';
20
+ import { pixiAvatar, type PixiAvatar } from './pixi-avatar';
21
+ import { refresh, type PixiContainer, type PixiNs, type PixiRectangle, type PixiSprite, type PixiTexture } from './pixi-types';
22
+
23
+ /** One avatar's state in the crowd (the world's own protocol fills it). */
24
+ export interface CrowdMember {
25
+ readonly look: LookLike;
26
+ readonly x: number;
27
+ readonly y: number;
28
+ readonly facing?: Facing;
29
+ readonly body?: BodyPose;
30
+ readonly status?: StatusProp | null;
31
+ /** The username for the name tag (omit for no tag). */
32
+ readonly name?: string;
33
+ readonly friend?: boolean;
34
+ readonly you?: boolean;
35
+ /** "Playing Uno": the game's catalog title. */
36
+ readonly activity?: string;
37
+ /** A quick-chat emote to pop (shown for 2.5 s from when it's set). */
38
+ readonly emote?: string;
39
+ readonly reaction?: ReactionGlyph;
40
+ /** Ask for live vector rendering (granted to at most `liveMax`). */
41
+ readonly live?: boolean;
42
+ /** Hide the tag (the world fades tags by distance and hides them when crowded). */
43
+ readonly tagHidden?: boolean;
44
+ /** Moves the tag and bubble up by this much (container units), so a world can stack tags that would overlap. */
45
+ readonly tagLift?: number;
46
+ }
47
+
48
+ /** Crowd options; `container` is the game's own pixi `Container`. */
49
+ export interface CrowdOptions<TSprite extends PixiSprite = PixiSprite> {
50
+ readonly atlas: AvatarAtlas;
51
+ /** The container avatars go into (it is made y-sortable). */
52
+ readonly container: PixiContainer<TSprite>;
53
+ /** Live vector avatars at most (default 6; 2 on low-end devices). */
54
+ readonly liveMax?: number;
55
+ /** Atlas baking budget per update, ms (default 4). */
56
+ readonly bakeMs?: number;
57
+ /**
58
+ * Where name tags and bubbles go (default `container`). A world puts them in
59
+ * a container above its lighting so tags stay readable at night.
60
+ */
61
+ readonly overlays?: PixiContainer<TSprite>;
62
+ /**
63
+ * Draws atlas avatars at this scale (default 1). Bake the atlas at
64
+ * `scale × devicePixelRatio` and pass `1 / devicePixelRatio` for crisp
65
+ * avatars on high-density screens; positions stay in the container's units.
66
+ */
67
+ readonly displayScale?: number;
68
+ /**
69
+ * The figure the atlas draws (default `blob`). Pass the atlas's own
70
+ * figure, so name tags sit on the right heads and walks step in time.
71
+ */
72
+ readonly figure?: Figure;
73
+ }
74
+
75
+ /** The crowd. */
76
+ export interface AvatarCrowd {
77
+ set(id: string | number, member: CrowdMember): void;
78
+ remove(id: string | number): void;
79
+ /** Advances animation to `t` (ms), bakes, and lays everyone out. Call once per frame. */
80
+ update(t: number): void;
81
+ destroy(): void;
82
+ }
83
+
84
+ /** A cached overlay sprite and the key it shows. */
85
+ interface OverlaySlot<TSprite extends PixiSprite> {
86
+ sprite: TSprite | null;
87
+ key: string;
88
+ }
89
+
90
+ interface Entry<TSprite extends PixiSprite> {
91
+ member: CrowdMember;
92
+ readonly sprite: TSprite;
93
+ readonly tag: OverlaySlot<TSprite>;
94
+ readonly bubble: OverlaySlot<TSprite>;
95
+ emoteAt: number;
96
+ /** A baked frame has been shown (while a new look bakes, the old one stays instead of the silhouette). */
97
+ hasFrame: boolean;
98
+ live: PixiAvatar<TSprite> | null;
99
+ body: BodyPose | null;
100
+ readonly anim: Animator;
101
+ }
102
+
103
+ const EMOTE_MS = 2500;
104
+ const CYCLE: Readonly<Partial<Record<BodyPose, number>>> = { walk: 520, run: 320, dance: 900, jump: 700, cheer: 600, idle: 2400 };
105
+ /** The walker's shorter legs step quicker. */
106
+ const WALKER_CYCLE: Readonly<Partial<Record<BodyPose, number>>> = { ...CYCLE, walk: 440, run: 300, idle: 2800 };
107
+
108
+ /** A crowd over an atlas: `createCrowd(pixi, { atlas, container })`. */
109
+ export function createCrowd<TTexture extends PixiTexture, TSprite extends PixiSprite<TTexture>, TRectangle extends PixiRectangle>(
110
+ pixi: PixiNs<TTexture, TSprite, TRectangle>,
111
+ opts: CrowdOptions<NoInfer<TSprite>>,
112
+ ): AvatarCrowd {
113
+ const { atlas, container } = opts;
114
+ const overlayLayer = opts.overlays ?? container;
115
+ const displayScale = opts.displayScale ?? 1;
116
+ container.sortableChildren = true;
117
+ overlayLayer.sortableChildren = true;
118
+ const liveMax = opts.liveMax ?? 6;
119
+ const figure = opts.figure ?? 'blob';
120
+ const cycles = figure === 'walker' ? WALKER_CYCLE : CYCLE;
121
+ const [, liveTop, , liveHeight] = frameBox({ frame: 'full', figure });
122
+ const entries = new Map<string, Entry<TSprite>>();
123
+ const pageTextures: TTexture[] = [];
124
+ const frameTextures = new Map<string, TTexture>();
125
+ const overlayTextures = new Map<string, { texture: TTexture; ax: number; ay: number }>();
126
+
127
+ /** Where the name plate sits above the feet (rig units), cached per look, pose and prop. */
128
+ const nameplates = new Map<string, number>();
129
+ const nameplateY = (look: LookLike, body: BodyPose, status: StatusProp | null): number => {
130
+ const key = `${toCode(look)}|${body}|${status ?? ''}`;
131
+ let plateY = nameplates.get(key);
132
+ if (plateY === undefined) {
133
+ plateY = layout(look, { figure, pose: { body, status } }).nameplate.y;
134
+ nameplates.set(key, plateY);
135
+ }
136
+ return plateY;
137
+ };
138
+ const overlayTexture = (key: string, overlay: Overlay): { texture: TTexture; ax: number; ay: number } | null => {
139
+ const hit = overlayTextures.get(key);
140
+ if (hit) {
141
+ return hit;
142
+ }
143
+ const [left, top, w, h] = overlay.box;
144
+ const resolution = 2;
145
+ const made = defaultLayer(Math.ceil(w * resolution), Math.ceil(h * resolution));
146
+ if (!made || !('getContext' in made.canvas)) {
147
+ return null;
148
+ }
149
+ made.ctx.scale(resolution, resolution);
150
+ made.ctx.translate(-left, -top);
151
+ paintNodes(made.ctx, overlay.nodes);
152
+ const texture = pixi.Texture.from(made.canvas);
153
+ const out = { texture, ax: -left / w, ay: -top / h };
154
+ overlayTextures.set(key, out);
155
+ return out;
156
+ };
157
+
158
+ const textureFor = (page: number, left: number, top: number, w: number, h: number): TTexture | null => {
159
+ const sources = atlas.sources;
160
+ while (pageTextures.length < sources.length) {
161
+ const source = sources[pageTextures.length];
162
+ if (!source || !('getContext' in source)) {
163
+ return null;
164
+ }
165
+ pageTextures.push(pixi.Texture.from(source));
166
+ }
167
+ const base = pageTextures[page]?.source;
168
+ if (!base) {
169
+ return null;
170
+ }
171
+ const key = `${String(page)}:${String(left)}:${String(top)}`;
172
+ let tex = frameTextures.get(key);
173
+ if (!tex) {
174
+ tex = new pixi.Texture({ source: base, frame: new pixi.Rectangle(left, top, w, h) });
175
+ frameTextures.set(key, tex);
176
+ }
177
+ return tex;
178
+ };
179
+
180
+ const placeOverlay = (slot: OverlaySlot<TSprite>, key: string, make: () => Overlay | null, atX: number, atY: number): void => {
181
+ if (key === '') {
182
+ if (slot.sprite) {
183
+ slot.sprite.visible = false;
184
+ }
185
+ return;
186
+ }
187
+ let sprite = slot.sprite;
188
+ if (slot.key !== key || !sprite) {
189
+ const overlay = make();
190
+ const made = overlay ? overlayTexture(key, overlay) : null;
191
+ if (!made) {
192
+ return;
193
+ }
194
+ if (!sprite) {
195
+ sprite = new pixi.Sprite(made.texture);
196
+ overlayLayer.addChild(sprite);
197
+ }
198
+ slot.sprite = sprite;
199
+ sprite.texture = made.texture;
200
+ sprite.anchor.set(made.ax, made.ay);
201
+ sprite.scale.set(0.5);
202
+ slot.key = key;
203
+ }
204
+ sprite.visible = true;
205
+ sprite.x = atX;
206
+ sprite.y = atY;
207
+ sprite.zIndex = 1e6 + atY;
208
+ };
209
+
210
+ return {
211
+ set(id, member) {
212
+ const key = String(id);
213
+ const entry = entries.get(key);
214
+ if (entry) {
215
+ if (member.emote && member.emote !== entry.member.emote) {
216
+ entry.emoteAt = -1;
217
+ const face = expressionFromEmote(member.emote);
218
+ if (face) {
219
+ entry.anim.express(face);
220
+ }
221
+ }
222
+ if (member.look !== entry.member.look) {
223
+ entry.live?.setLook(member.look);
224
+ }
225
+ entry.member = member;
226
+ return;
227
+ }
228
+ const sprite = new pixi.Sprite();
229
+ container.addChild(sprite);
230
+ entries.set(key, {
231
+ member, sprite, tag: { sprite: null, key: '' }, bubble: { sprite: null, key: '' }, emoteAt: member.emote ? -1 : 0, hasFrame: false, live: null, body: null,
232
+ anim: animator(member.look, { seed: key }),
233
+ });
234
+ },
235
+ remove(id) {
236
+ const entry = entries.get(String(id));
237
+ if (!entry) {
238
+ return;
239
+ }
240
+ entry.sprite.destroy();
241
+ entry.tag.sprite?.destroy();
242
+ entry.bubble.sprite?.destroy();
243
+ entry.live?.destroy();
244
+ entries.delete(String(id));
245
+ },
246
+ update(t) {
247
+ if (atlas.bake(opts.bakeMs ?? 4) > 0) {
248
+ pageTextures.forEach(refresh);
249
+ }
250
+ const wantsLive = [...entries.values()].filter((e) => e.member.live === true).slice(0, liveMax);
251
+ const liveSet = new Set(wantsLive);
252
+ for (const entry of entries.values()) {
253
+ const m = entry.member;
254
+ const body = m.body ?? 'idle';
255
+ if (entry.emoteAt === -1) {
256
+ entry.emoteAt = t;
257
+ }
258
+ const isEmoting = m.emote !== undefined && t - entry.emoteAt < EMOTE_MS;
259
+ if (liveSet.has(entry)) {
260
+ if (!entry.live) {
261
+ entry.live = pixiAvatar(pixi, m.look, { frame: 'full', figure, size: 64, rim: true, shadow: false, animator: entry.anim });
262
+ container.addChild(entry.live.view);
263
+ }
264
+ entry.sprite.visible = false;
265
+ const view = entry.live.view;
266
+ if (entry.body !== body) {
267
+ entry.body = body;
268
+ entry.anim.setBody(body, { at: t });
269
+ }
270
+ entry.live.update({ ...entry.anim.pose(t), body, status: m.status ?? null, phase: (t / (cycles[body] ?? 600)) % 1 });
271
+ view.x = m.x;
272
+ view.y = m.y + (figure === 'walker' ? (liveTop + liveHeight / 2) * (64 / liveHeight) : -32);
273
+ view.zIndex = m.y;
274
+ view.visible = true;
275
+ } else {
276
+ if (entry.live) {
277
+ entry.live.view.visible = false;
278
+ }
279
+ const frame = atlas.frame({
280
+ look: m.look, facing: m.facing ?? 'S', body, status: m.status ?? null,
281
+ phase: (t / (cycles[body] ?? 600)) % 1, face: isEmoting ? 'happy' : 'neutral',
282
+ });
283
+ // Until its first frame bakes, a member shows the silhouette, anchored and scaled like a frame.
284
+ const shown = frame ?? (entry.hasFrame ? null : atlas.frame({ look: null }));
285
+ entry.hasFrame ||= frame !== null;
286
+ const tex = shown ? textureFor(shown.page, shown.x, shown.y, shown.w, shown.h) : null;
287
+ if (tex) {
288
+ entry.sprite.texture = tex;
289
+ entry.sprite.visible = true;
290
+ }
291
+ if (shown) {
292
+ entry.sprite.anchor.set(shown.footX / shown.w, shown.footY / shown.h);
293
+ entry.sprite.scale.set(shown.mirror ? -displayScale : displayScale, displayScale);
294
+ }
295
+ entry.sprite.x = m.x;
296
+ entry.sprite.y = m.y;
297
+ entry.sprite.zIndex = m.y;
298
+ }
299
+ const top = m.y - (m.tagLift ?? 0) + nameplateY(m.look, body, m.status ?? null) * (entry.live?.view.visible === true ? 64 / liveHeight : atlas.scale * displayScale);
300
+ const tagKey = m.name && !m.tagHidden ? `tag:${m.name}:${m.friend ? 1 : 0}:${m.you ? 1 : 0}` : '';
301
+ placeOverlay(entry.tag, tagKey, () => (m.name ? overlays.nameTag({ name: m.name, friend: m.friend === true, you: m.you === true }) : null), m.x, top);
302
+ const bubbleKey = isEmoting && m.emote ? `emote:${m.emote}` : m.reaction ? `react:${m.reaction}` : m.activity ? `act:${m.activity}` : '';
303
+ placeOverlay(entry.bubble, bubbleKey, () => {
304
+ if (isEmoting && m.emote) {
305
+ return overlays.emoteBubble(m.emote);
306
+ }
307
+ if (m.reaction) {
308
+ return overlays.reactionBubble(m.reaction);
309
+ }
310
+ return m.activity ? overlays.activityBubble({ game: m.activity }) : null;
311
+ }, m.x, top - 12);
312
+ }
313
+ },
314
+ destroy() {
315
+ for (const id of [...entries.keys()]) {
316
+ this.remove(id);
317
+ }
318
+ for (const tex of frameTextures.values()) {
319
+ tex.destroy(false);
320
+ }
321
+ for (const tex of pageTextures) {
322
+ tex.destroy(true);
323
+ }
324
+ for (const o of overlayTextures.values()) {
325
+ o.texture.destroy(true);
326
+ }
327
+ },
328
+ };
329
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The slice of PixiJS the adapters use, typed structurally so the hub has no
3
+ * dependency on pixi. Pixi 8 is the target; Pixi 7 works through the same
4
+ * calls where they exist (`Texture.from(canvas)`, `texture.update()`).
5
+ */
6
+
7
+ /** A canvas-like source Pixi can upload. */
8
+ export type PixiSource = HTMLCanvasElement | OffscreenCanvas;
9
+
10
+ /** A texture's uploadable source (Pixi 8). */
11
+ export interface PixiTextureSource {
12
+ update(): void;
13
+ }
14
+
15
+ /** A texture (Pixi 8: `source.update()`; Pixi 7: `update()`). */
16
+ export interface PixiTexture {
17
+ readonly source?: PixiTextureSource;
18
+ update?(): void;
19
+ destroy(destroySource?: boolean): void;
20
+ }
21
+
22
+ /** A display object's shared fields. */
23
+ export interface PixiDisplay {
24
+ x: number;
25
+ y: number;
26
+ visible: boolean;
27
+ alpha: number;
28
+ zIndex: number;
29
+ readonly scale: { x: number; y: number; set(x: number, y?: number): void };
30
+ destroy(options?: { children?: boolean }): void;
31
+ }
32
+
33
+ /** A sprite showing a `TTexture`. */
34
+ export interface PixiSprite<TTexture extends PixiTexture = PixiTexture> extends PixiDisplay {
35
+ texture: TTexture;
36
+ readonly anchor: { set(x: number, y?: number): void };
37
+ }
38
+
39
+ /** A container that takes `TChild` sprites. */
40
+ export interface PixiContainer<TChild extends PixiDisplay = PixiSprite> extends PixiDisplay {
41
+ sortableChildren: boolean;
42
+ addChild(child: TChild): void;
43
+ removeChild(child: TChild): void;
44
+ }
45
+
46
+ /** A frame rectangle. */
47
+ export interface PixiRectangle {
48
+ readonly x: number;
49
+ readonly y: number;
50
+ readonly width: number;
51
+ readonly height: number;
52
+ }
53
+
54
+ /**
55
+ * The PIXI namespace, as passed in by the game (`import * as PIXI from
56
+ * 'pixi.js'`, or an object of the named imports). It is generic over the
57
+ * game's own classes, inferred from what is passed, so the sprites and
58
+ * textures that come back are pixi's own types and go straight into a
59
+ * `Container` with no cast.
60
+ */
61
+ export interface PixiNs<
62
+ TTexture extends PixiTexture = PixiTexture,
63
+ TSprite extends PixiSprite<TTexture> = PixiSprite<TTexture>,
64
+ TRectangle extends PixiRectangle = PixiRectangle,
65
+ > {
66
+ readonly Texture: {
67
+ from(source: PixiSource): TTexture;
68
+ new (options: { source: NonNullable<TTexture['source']>; frame: TRectangle }): TTexture;
69
+ };
70
+ readonly Sprite: new (texture?: TTexture) => TSprite;
71
+ /** Not used by the adapters; accepted so an object of the named imports can include it. */
72
+ readonly Container?: new () => PixiDisplay;
73
+ readonly Rectangle: new (x: number, y: number, width: number, height: number) => TRectangle;
74
+ }
75
+
76
+ /** Re-uploads a texture after its canvas changed (Pixi 8 and 7). */
77
+ export function refresh(texture: PixiTexture): void {
78
+ if (texture.source) {
79
+ texture.source.update();
80
+ } else {
81
+ texture.update?.();
82
+ }
83
+ }
84
+
@@ -0,0 +1,126 @@
1
+ /**
2
+ * three.js helpers (§3.8): an avatar on a `CanvasTexture` (redrawn only when
3
+ * the pose changes, at most 30 times a second) and a ready-made `Sprite`.
4
+ * Pass the game's three namespace; the hub never imports three. A 3D head is
5
+ * out of scope (§14.4, Q11).
6
+ */
7
+ import type { Animator } from '../animator/animator';
8
+ import { liveCanvas } from '../outputs/live-canvas';
9
+ import type { LookLike, Pose, RenderOptions } from '../render/types';
10
+
11
+ /**
12
+ * The slice of three.js {@link avatarCanvasTexture} uses: `import * as THREE
13
+ * from 'three'`, or an object of the named imports. Generic over the game's
14
+ * own `CanvasTexture`, inferred from what is passed, so the texture that comes
15
+ * back is three's own type.
16
+ */
17
+ export interface ThreeTextureNs<TTexture extends ThreeTexture = ThreeTexture> {
18
+ readonly CanvasTexture: new (canvas: HTMLCanvasElement | OffscreenCanvas) => TTexture;
19
+ readonly SRGBColorSpace?: string;
20
+ }
21
+
22
+ /**
23
+ * The slice of three.js {@link avatarSprite} uses (the namespace, or the named
24
+ * imports). The sprite that comes back is three's own `Sprite`, so
25
+ * `scene.add(sprite)` needs no cast. `TMap` is what the game's
26
+ * `SpriteMaterial` takes as its `map` (three's `Texture`); it is inferred on
27
+ * its own so an inline object of the named imports infers too.
28
+ */
29
+ export interface ThreeNs<
30
+ TTexture extends ThreeTexture = ThreeTexture,
31
+ TMaterial extends ThreeMaterial = ThreeMaterial,
32
+ TSprite extends ThreeSprite = ThreeSprite,
33
+ TMap = TTexture,
34
+ > extends ThreeTextureNs<TTexture> {
35
+ readonly SpriteMaterial: new (params: { map: TMap; transparent: boolean }) => TMaterial;
36
+ readonly Sprite: new (material: TMaterial) => TSprite;
37
+ }
38
+
39
+ /** A texture. */
40
+ export interface ThreeTexture {
41
+ needsUpdate: boolean;
42
+ colorSpace?: string;
43
+ dispose(): void;
44
+ }
45
+
46
+ /** A material. */
47
+ export interface ThreeMaterial {
48
+ dispose(): void;
49
+ }
50
+
51
+ /** A sprite. */
52
+ export interface ThreeSprite {
53
+ readonly scale: { set(x: number, y: number, z: number): void };
54
+ }
55
+
56
+ /** Options for the three.js helpers. */
57
+ export interface ThreeAvatarOptions extends RenderOptions {
58
+ readonly animator?: Animator;
59
+ /** Texture pixels per `size` pixel (default 2). */
60
+ readonly resolution?: number;
61
+ }
62
+
63
+ /** A live avatar texture; `texture` is the game's own `CanvasTexture`. */
64
+ export interface ThreeAvatarTexture<TTexture extends ThreeTexture = ThreeTexture> {
65
+ readonly texture: TTexture;
66
+ /** Redraws for the animator's pose at `t` (or a given pose), at most 30 times a second. */
67
+ update(t: number | Pose): void;
68
+ setLook(like: LookLike): void;
69
+ dispose(): void;
70
+ }
71
+
72
+ const MIN_FRAME_MS = 1000 / 30;
73
+
74
+ /** A `CanvasTexture` showing the avatar. */
75
+ export function avatarCanvasTexture<TTexture extends ThreeTexture>(
76
+ three: ThreeTextureNs<TTexture>,
77
+ like: LookLike,
78
+ opts: ThreeAvatarOptions = {},
79
+ ): ThreeAvatarTexture<TTexture> {
80
+ const live = liveCanvas(like, opts, opts.resolution ?? 2);
81
+ live.draw(opts.pose);
82
+ const texture = new three.CanvasTexture(live.canvas);
83
+ if (three.SRGBColorSpace) {
84
+ texture.colorSpace = three.SRGBColorSpace;
85
+ }
86
+ let last = -Infinity;
87
+ return {
88
+ texture,
89
+ update(t) {
90
+ if (typeof t === 'number') {
91
+ if (t - last < MIN_FRAME_MS) {
92
+ return;
93
+ }
94
+ last = t;
95
+ }
96
+ const pose = typeof t === 'number' ? opts.animator?.pose(t) : t;
97
+ if (live.draw(pose ?? opts.pose)) {
98
+ texture.needsUpdate = true;
99
+ }
100
+ },
101
+ setLook(next) {
102
+ live.setLook(next);
103
+ opts.animator?.setLook(next);
104
+ if (live.draw(opts.pose)) {
105
+ texture.needsUpdate = true;
106
+ }
107
+ },
108
+ dispose() {
109
+ texture.dispose();
110
+ },
111
+ };
112
+ }
113
+
114
+ /** A billboard `Sprite` of the avatar, `height` world units tall (default 1). */
115
+ export function avatarSprite<TTexture extends ThreeTexture & TMap, TMaterial extends ThreeMaterial, TSprite extends ThreeSprite, TMap>(
116
+ three: ThreeNs<TTexture, TMaterial, TSprite, TMap>,
117
+ like: LookLike,
118
+ opts: ThreeAvatarOptions & { readonly height?: number } = {},
119
+ ): { readonly sprite: TSprite; readonly avatar: ThreeAvatarTexture<TTexture> } {
120
+ const avatar = avatarCanvasTexture(three, like, opts);
121
+ const sprite = new three.Sprite(new three.SpriteMaterial({ map: avatar.texture, transparent: true }));
122
+ const h = opts.height ?? 1;
123
+ const aspect = opts.frame === 'full' ? 172 / 206 : 1;
124
+ sprite.scale.set(h * aspect, h, 1);
125
+ return { sprite, avatar };
126
+ }