@diffenderfer-games/hub 2.0.0-next.6 → 2.0.0-next.7

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 (38) hide show
  1. package/CHANGELOG.md +39 -28
  2. package/dist/avatar-pixi.js +1 -1
  3. package/dist/avatar-svg.js +1 -1
  4. package/dist/avatar.d.ts +2 -2
  5. package/dist/avatar.js +3 -3
  6. package/dist/doctor/cli.mjs +1 -1
  7. package/dist/shared/art.js +145 -3
  8. package/dist/shared/art.js.map +1 -1
  9. package/dist/shared/avatar.d.ts +3 -3
  10. package/dist/shared/client-version.gen.js +1 -1
  11. package/dist/shared/daily.js +1 -1
  12. package/dist/shared/describe.d.ts.map +1 -1
  13. package/dist/shared/describe.js +9 -1
  14. package/dist/shared/describe.js.map +1 -1
  15. package/dist/shared/index.d.ts +2 -2
  16. package/dist/shared/live-canvas.js +1 -1
  17. package/dist/shared/overlays.js +1 -1
  18. package/dist/shared/pose-key.js +1 -1
  19. package/dist/shared/svg-markup.js +112 -27
  20. package/dist/shared/svg-markup.js.map +1 -1
  21. package/dist/shared/svg.js +1 -1
  22. package/package.json +2 -2
  23. package/src/avatar/adapters/pixi-crowd.ts +302 -302
  24. package/src/avatar/adapters/pixi-types.ts +84 -84
  25. package/src/avatar/adapters/three.ts +126 -126
  26. package/src/avatar/art/glasses.ts +61 -3
  27. package/src/avatar/art/shape.ts +22 -3
  28. package/src/avatar/describe.ts +4 -1
  29. package/src/avatar/geom.ts +5 -0
  30. package/src/avatar/render/compose.ts +67 -22
  31. package/src/avatar/render/eyewear.ts +75 -13
  32. package/src/avatar-three.ts +18 -18
  33. package/src/avatar.ts +2 -2
  34. package/src/core/avatar/hub-avatar.ts +308 -308
  35. package/src/core/hub-progress.ts +269 -269
  36. package/src/core/page-account.ts +121 -121
  37. package/src/core/version/client-version.gen.ts +1 -1
  38. package/src/overlay/overlay.ts +3 -3
@@ -1,302 +1,302 @@
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 type { BodyPose, Facing, LookLike, StatusProp } from '../render/types';
19
- import { pixiAvatar, type PixiAvatar } from './pixi-avatar';
20
- import { refresh, type PixiContainer, type PixiNs, type PixiRectangle, type PixiSprite, type PixiTexture } from './pixi-types';
21
-
22
- /** One avatar's state in the crowd (the world's own protocol fills it). */
23
- export interface CrowdMember {
24
- readonly look: LookLike;
25
- readonly x: number;
26
- readonly y: number;
27
- readonly facing?: Facing;
28
- readonly body?: BodyPose;
29
- readonly status?: StatusProp | null;
30
- /** The username for the name tag (omit for no tag). */
31
- readonly name?: string;
32
- readonly friend?: boolean;
33
- readonly you?: boolean;
34
- /** "Playing Uno": the game's catalog title. */
35
- readonly activity?: string;
36
- /** A quick-chat emote to pop (shown for 2.5 s from when it's set). */
37
- readonly emote?: string;
38
- readonly reaction?: ReactionGlyph;
39
- /** Ask for live vector rendering (granted to at most `liveMax`). */
40
- readonly live?: boolean;
41
- /** Hide the tag (the world fades tags by distance and hides them when crowded). */
42
- readonly tagHidden?: boolean;
43
- }
44
-
45
- /** Crowd options; `container` is the game's own pixi `Container`. */
46
- export interface CrowdOptions<TSprite extends PixiSprite = PixiSprite> {
47
- readonly atlas: AvatarAtlas;
48
- /** The container avatars go into (it is made y-sortable). */
49
- readonly container: PixiContainer<TSprite>;
50
- /** Live vector avatars at most (default 6; 2 on low-end devices). */
51
- readonly liveMax?: number;
52
- /** Atlas baking budget per update, ms (default 4). */
53
- readonly bakeMs?: number;
54
- }
55
-
56
- /** The crowd. */
57
- export interface AvatarCrowd {
58
- set(id: string | number, member: CrowdMember): void;
59
- remove(id: string | number): void;
60
- /** Advances animation to `t` (ms), bakes, and lays everyone out. Call once per frame. */
61
- update(t: number): void;
62
- destroy(): void;
63
- }
64
-
65
- /** A cached overlay sprite and the key it shows. */
66
- interface OverlaySlot<TSprite extends PixiSprite> {
67
- sprite: TSprite | null;
68
- key: string;
69
- }
70
-
71
- interface Entry<TSprite extends PixiSprite> {
72
- member: CrowdMember;
73
- readonly sprite: TSprite;
74
- readonly tag: OverlaySlot<TSprite>;
75
- readonly bubble: OverlaySlot<TSprite>;
76
- emoteAt: number;
77
- live: PixiAvatar<TSprite> | null;
78
- body: BodyPose | null;
79
- readonly anim: Animator;
80
- }
81
-
82
- const EMOTE_MS = 2500;
83
- const CYCLE: Readonly<Partial<Record<BodyPose, number>>> = { walk: 520, run: 320, dance: 900, jump: 700, cheer: 600, idle: 2400 };
84
-
85
- /** A crowd over an atlas: `createCrowd(pixi, { atlas, container })`. */
86
- export function createCrowd<TTexture extends PixiTexture, TSprite extends PixiSprite<TTexture>, TRectangle extends PixiRectangle>(
87
- pixi: PixiNs<TTexture, TSprite, TRectangle>,
88
- opts: CrowdOptions<NoInfer<TSprite>>,
89
- ): AvatarCrowd {
90
- const { atlas, container } = opts;
91
- container.sortableChildren = true;
92
- const liveMax = opts.liveMax ?? 6;
93
- const entries = new Map<string, Entry<TSprite>>();
94
- const pageTextures: TTexture[] = [];
95
- const frameTextures = new Map<string, TTexture>();
96
- const overlayTextures = new Map<string, { texture: TTexture; ax: number; ay: number }>();
97
-
98
- /** Where the name plate sits above the feet (rig units), cached per look, pose and prop. */
99
- const nameplates = new Map<string, number>();
100
- const nameplateY = (look: LookLike, body: BodyPose, status: StatusProp | null): number => {
101
- const key = `${toCode(look)}|${body}|${status ?? ''}`;
102
- let plateY = nameplates.get(key);
103
- if (plateY === undefined) {
104
- plateY = layout(look, { pose: { body, status } }).nameplate.y;
105
- nameplates.set(key, plateY);
106
- }
107
- return plateY;
108
- };
109
- const overlayTexture = (key: string, overlay: Overlay): { texture: TTexture; ax: number; ay: number } | null => {
110
- const hit = overlayTextures.get(key);
111
- if (hit) {
112
- return hit;
113
- }
114
- const [left, top, w, h] = overlay.box;
115
- const resolution = 2;
116
- const made = defaultLayer(Math.ceil(w * resolution), Math.ceil(h * resolution));
117
- if (!made || !('getContext' in made.canvas)) {
118
- return null;
119
- }
120
- made.ctx.scale(resolution, resolution);
121
- made.ctx.translate(-left, -top);
122
- paintNodes(made.ctx, overlay.nodes);
123
- const texture = pixi.Texture.from(made.canvas);
124
- const out = { texture, ax: -left / w, ay: -top / h };
125
- overlayTextures.set(key, out);
126
- return out;
127
- };
128
-
129
- const silhouette = (): TTexture | null => {
130
- const frame = atlas.frame({ look: null });
131
- return frame ? textureFor(frame.page, frame.x, frame.y, frame.w, frame.h) : null;
132
- };
133
-
134
- const textureFor = (page: number, left: number, top: number, w: number, h: number): TTexture | null => {
135
- const sources = atlas.sources;
136
- while (pageTextures.length < sources.length) {
137
- const source = sources[pageTextures.length];
138
- if (!source || !('getContext' in source)) {
139
- return null;
140
- }
141
- pageTextures.push(pixi.Texture.from(source));
142
- }
143
- const base = pageTextures[page]?.source;
144
- if (!base) {
145
- return null;
146
- }
147
- const key = `${String(page)}:${String(left)}:${String(top)}`;
148
- let tex = frameTextures.get(key);
149
- if (!tex) {
150
- tex = new pixi.Texture({ source: base, frame: new pixi.Rectangle(left, top, w, h) });
151
- frameTextures.set(key, tex);
152
- }
153
- return tex;
154
- };
155
-
156
- const placeOverlay = (slot: OverlaySlot<TSprite>, key: string, make: () => Overlay | null, atX: number, atY: number): void => {
157
- if (key === '') {
158
- if (slot.sprite) {
159
- slot.sprite.visible = false;
160
- }
161
- return;
162
- }
163
- let sprite = slot.sprite;
164
- if (slot.key !== key || !sprite) {
165
- const overlay = make();
166
- const made = overlay ? overlayTexture(key, overlay) : null;
167
- if (!made) {
168
- return;
169
- }
170
- if (!sprite) {
171
- sprite = new pixi.Sprite(made.texture);
172
- container.addChild(sprite);
173
- }
174
- slot.sprite = sprite;
175
- sprite.texture = made.texture;
176
- sprite.anchor.set(made.ax, made.ay);
177
- sprite.scale.set(0.5);
178
- slot.key = key;
179
- }
180
- sprite.visible = true;
181
- sprite.x = atX;
182
- sprite.y = atY;
183
- sprite.zIndex = 1e6 + atY;
184
- };
185
-
186
- return {
187
- set(id, member) {
188
- const key = String(id);
189
- const entry = entries.get(key);
190
- if (entry) {
191
- if (member.emote && member.emote !== entry.member.emote) {
192
- entry.emoteAt = -1;
193
- const face = expressionFromEmote(member.emote);
194
- if (face) {
195
- entry.anim.express(face);
196
- }
197
- }
198
- if (member.look !== entry.member.look) {
199
- entry.live?.setLook(member.look);
200
- }
201
- entry.member = member;
202
- return;
203
- }
204
- const sprite = new pixi.Sprite();
205
- container.addChild(sprite);
206
- entries.set(key, {
207
- member, sprite, tag: { sprite: null, key: '' }, bubble: { sprite: null, key: '' }, emoteAt: member.emote ? -1 : 0, live: null, body: null,
208
- anim: animator(member.look, { seed: key }),
209
- });
210
- },
211
- remove(id) {
212
- const entry = entries.get(String(id));
213
- if (!entry) {
214
- return;
215
- }
216
- entry.sprite.destroy();
217
- entry.tag.sprite?.destroy();
218
- entry.bubble.sprite?.destroy();
219
- entry.live?.destroy();
220
- entries.delete(String(id));
221
- },
222
- update(t) {
223
- if (atlas.bake(opts.bakeMs ?? 4) > 0) {
224
- pageTextures.forEach(refresh);
225
- }
226
- const wantsLive = [...entries.values()].filter((e) => e.member.live === true).slice(0, liveMax);
227
- const liveSet = new Set(wantsLive);
228
- for (const entry of entries.values()) {
229
- const m = entry.member;
230
- const body = m.body ?? 'idle';
231
- if (entry.emoteAt === -1) {
232
- entry.emoteAt = t;
233
- }
234
- const isEmoting = m.emote !== undefined && t - entry.emoteAt < EMOTE_MS;
235
- if (liveSet.has(entry)) {
236
- if (!entry.live) {
237
- entry.live = pixiAvatar(pixi, m.look, { frame: 'full', size: 64, rim: true, shadow: false, animator: entry.anim });
238
- container.addChild(entry.live.view);
239
- }
240
- entry.sprite.visible = false;
241
- const view = entry.live.view;
242
- if (entry.body !== body) {
243
- entry.body = body;
244
- entry.anim.setBody(body, { at: t });
245
- }
246
- entry.live.update({ ...entry.anim.pose(t), body, status: m.status ?? null, phase: (t / (CYCLE[body] ?? 600)) % 1 });
247
- view.x = m.x;
248
- view.y = m.y - 32;
249
- view.zIndex = m.y;
250
- view.visible = true;
251
- } else {
252
- if (entry.live) {
253
- entry.live.view.visible = false;
254
- }
255
- const frame = atlas.frame({
256
- look: m.look, facing: m.facing ?? 'S', body, status: m.status ?? null,
257
- phase: (t / (CYCLE[body] ?? 600)) % 1, face: isEmoting ? 'happy' : 'neutral',
258
- });
259
- const tex = frame ? textureFor(frame.page, frame.x, frame.y, frame.w, frame.h) : silhouette();
260
- if (tex) {
261
- entry.sprite.texture = tex;
262
- entry.sprite.visible = true;
263
- }
264
- if (frame) {
265
- entry.sprite.anchor.set(frame.footX / frame.w, frame.footY / frame.h);
266
- entry.sprite.scale.x = frame.mirror ? -1 : 1;
267
- }
268
- entry.sprite.x = m.x;
269
- entry.sprite.y = m.y;
270
- entry.sprite.zIndex = m.y;
271
- }
272
- const top = m.y + nameplateY(m.look, body, m.status ?? null) * (entry.live?.view.visible === true ? 64 / 206 : atlas.scale);
273
- const tagKey = m.name && !m.tagHidden ? `tag:${m.name}:${m.friend ? 1 : 0}:${m.you ? 1 : 0}` : '';
274
- placeOverlay(entry.tag, tagKey, () => (m.name ? overlays.nameTag({ name: m.name, friend: m.friend === true, you: m.you === true }) : null), m.x, top);
275
- const bubbleKey = isEmoting && m.emote ? `emote:${m.emote}` : m.reaction ? `react:${m.reaction}` : m.activity ? `act:${m.activity}` : '';
276
- placeOverlay(entry.bubble, bubbleKey, () => {
277
- if (isEmoting && m.emote) {
278
- return overlays.emoteBubble(m.emote);
279
- }
280
- if (m.reaction) {
281
- return overlays.reactionBubble(m.reaction);
282
- }
283
- return m.activity ? overlays.activityBubble({ game: m.activity }) : null;
284
- }, m.x, top - 12);
285
- }
286
- },
287
- destroy() {
288
- for (const id of [...entries.keys()]) {
289
- this.remove(id);
290
- }
291
- for (const tex of frameTextures.values()) {
292
- tex.destroy(false);
293
- }
294
- for (const tex of pageTextures) {
295
- tex.destroy(true);
296
- }
297
- for (const o of overlayTextures.values()) {
298
- o.texture.destroy(true);
299
- }
300
- },
301
- };
302
- }
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 type { BodyPose, Facing, LookLike, StatusProp } from '../render/types';
19
+ import { pixiAvatar, type PixiAvatar } from './pixi-avatar';
20
+ import { refresh, type PixiContainer, type PixiNs, type PixiRectangle, type PixiSprite, type PixiTexture } from './pixi-types';
21
+
22
+ /** One avatar's state in the crowd (the world's own protocol fills it). */
23
+ export interface CrowdMember {
24
+ readonly look: LookLike;
25
+ readonly x: number;
26
+ readonly y: number;
27
+ readonly facing?: Facing;
28
+ readonly body?: BodyPose;
29
+ readonly status?: StatusProp | null;
30
+ /** The username for the name tag (omit for no tag). */
31
+ readonly name?: string;
32
+ readonly friend?: boolean;
33
+ readonly you?: boolean;
34
+ /** "Playing Uno": the game's catalog title. */
35
+ readonly activity?: string;
36
+ /** A quick-chat emote to pop (shown for 2.5 s from when it's set). */
37
+ readonly emote?: string;
38
+ readonly reaction?: ReactionGlyph;
39
+ /** Ask for live vector rendering (granted to at most `liveMax`). */
40
+ readonly live?: boolean;
41
+ /** Hide the tag (the world fades tags by distance and hides them when crowded). */
42
+ readonly tagHidden?: boolean;
43
+ }
44
+
45
+ /** Crowd options; `container` is the game's own pixi `Container`. */
46
+ export interface CrowdOptions<TSprite extends PixiSprite = PixiSprite> {
47
+ readonly atlas: AvatarAtlas;
48
+ /** The container avatars go into (it is made y-sortable). */
49
+ readonly container: PixiContainer<TSprite>;
50
+ /** Live vector avatars at most (default 6; 2 on low-end devices). */
51
+ readonly liveMax?: number;
52
+ /** Atlas baking budget per update, ms (default 4). */
53
+ readonly bakeMs?: number;
54
+ }
55
+
56
+ /** The crowd. */
57
+ export interface AvatarCrowd {
58
+ set(id: string | number, member: CrowdMember): void;
59
+ remove(id: string | number): void;
60
+ /** Advances animation to `t` (ms), bakes, and lays everyone out. Call once per frame. */
61
+ update(t: number): void;
62
+ destroy(): void;
63
+ }
64
+
65
+ /** A cached overlay sprite and the key it shows. */
66
+ interface OverlaySlot<TSprite extends PixiSprite> {
67
+ sprite: TSprite | null;
68
+ key: string;
69
+ }
70
+
71
+ interface Entry<TSprite extends PixiSprite> {
72
+ member: CrowdMember;
73
+ readonly sprite: TSprite;
74
+ readonly tag: OverlaySlot<TSprite>;
75
+ readonly bubble: OverlaySlot<TSprite>;
76
+ emoteAt: number;
77
+ live: PixiAvatar<TSprite> | null;
78
+ body: BodyPose | null;
79
+ readonly anim: Animator;
80
+ }
81
+
82
+ const EMOTE_MS = 2500;
83
+ const CYCLE: Readonly<Partial<Record<BodyPose, number>>> = { walk: 520, run: 320, dance: 900, jump: 700, cheer: 600, idle: 2400 };
84
+
85
+ /** A crowd over an atlas: `createCrowd(pixi, { atlas, container })`. */
86
+ export function createCrowd<TTexture extends PixiTexture, TSprite extends PixiSprite<TTexture>, TRectangle extends PixiRectangle>(
87
+ pixi: PixiNs<TTexture, TSprite, TRectangle>,
88
+ opts: CrowdOptions<NoInfer<TSprite>>,
89
+ ): AvatarCrowd {
90
+ const { atlas, container } = opts;
91
+ container.sortableChildren = true;
92
+ const liveMax = opts.liveMax ?? 6;
93
+ const entries = new Map<string, Entry<TSprite>>();
94
+ const pageTextures: TTexture[] = [];
95
+ const frameTextures = new Map<string, TTexture>();
96
+ const overlayTextures = new Map<string, { texture: TTexture; ax: number; ay: number }>();
97
+
98
+ /** Where the name plate sits above the feet (rig units), cached per look, pose and prop. */
99
+ const nameplates = new Map<string, number>();
100
+ const nameplateY = (look: LookLike, body: BodyPose, status: StatusProp | null): number => {
101
+ const key = `${toCode(look)}|${body}|${status ?? ''}`;
102
+ let plateY = nameplates.get(key);
103
+ if (plateY === undefined) {
104
+ plateY = layout(look, { pose: { body, status } }).nameplate.y;
105
+ nameplates.set(key, plateY);
106
+ }
107
+ return plateY;
108
+ };
109
+ const overlayTexture = (key: string, overlay: Overlay): { texture: TTexture; ax: number; ay: number } | null => {
110
+ const hit = overlayTextures.get(key);
111
+ if (hit) {
112
+ return hit;
113
+ }
114
+ const [left, top, w, h] = overlay.box;
115
+ const resolution = 2;
116
+ const made = defaultLayer(Math.ceil(w * resolution), Math.ceil(h * resolution));
117
+ if (!made || !('getContext' in made.canvas)) {
118
+ return null;
119
+ }
120
+ made.ctx.scale(resolution, resolution);
121
+ made.ctx.translate(-left, -top);
122
+ paintNodes(made.ctx, overlay.nodes);
123
+ const texture = pixi.Texture.from(made.canvas);
124
+ const out = { texture, ax: -left / w, ay: -top / h };
125
+ overlayTextures.set(key, out);
126
+ return out;
127
+ };
128
+
129
+ const silhouette = (): TTexture | null => {
130
+ const frame = atlas.frame({ look: null });
131
+ return frame ? textureFor(frame.page, frame.x, frame.y, frame.w, frame.h) : null;
132
+ };
133
+
134
+ const textureFor = (page: number, left: number, top: number, w: number, h: number): TTexture | null => {
135
+ const sources = atlas.sources;
136
+ while (pageTextures.length < sources.length) {
137
+ const source = sources[pageTextures.length];
138
+ if (!source || !('getContext' in source)) {
139
+ return null;
140
+ }
141
+ pageTextures.push(pixi.Texture.from(source));
142
+ }
143
+ const base = pageTextures[page]?.source;
144
+ if (!base) {
145
+ return null;
146
+ }
147
+ const key = `${String(page)}:${String(left)}:${String(top)}`;
148
+ let tex = frameTextures.get(key);
149
+ if (!tex) {
150
+ tex = new pixi.Texture({ source: base, frame: new pixi.Rectangle(left, top, w, h) });
151
+ frameTextures.set(key, tex);
152
+ }
153
+ return tex;
154
+ };
155
+
156
+ const placeOverlay = (slot: OverlaySlot<TSprite>, key: string, make: () => Overlay | null, atX: number, atY: number): void => {
157
+ if (key === '') {
158
+ if (slot.sprite) {
159
+ slot.sprite.visible = false;
160
+ }
161
+ return;
162
+ }
163
+ let sprite = slot.sprite;
164
+ if (slot.key !== key || !sprite) {
165
+ const overlay = make();
166
+ const made = overlay ? overlayTexture(key, overlay) : null;
167
+ if (!made) {
168
+ return;
169
+ }
170
+ if (!sprite) {
171
+ sprite = new pixi.Sprite(made.texture);
172
+ container.addChild(sprite);
173
+ }
174
+ slot.sprite = sprite;
175
+ sprite.texture = made.texture;
176
+ sprite.anchor.set(made.ax, made.ay);
177
+ sprite.scale.set(0.5);
178
+ slot.key = key;
179
+ }
180
+ sprite.visible = true;
181
+ sprite.x = atX;
182
+ sprite.y = atY;
183
+ sprite.zIndex = 1e6 + atY;
184
+ };
185
+
186
+ return {
187
+ set(id, member) {
188
+ const key = String(id);
189
+ const entry = entries.get(key);
190
+ if (entry) {
191
+ if (member.emote && member.emote !== entry.member.emote) {
192
+ entry.emoteAt = -1;
193
+ const face = expressionFromEmote(member.emote);
194
+ if (face) {
195
+ entry.anim.express(face);
196
+ }
197
+ }
198
+ if (member.look !== entry.member.look) {
199
+ entry.live?.setLook(member.look);
200
+ }
201
+ entry.member = member;
202
+ return;
203
+ }
204
+ const sprite = new pixi.Sprite();
205
+ container.addChild(sprite);
206
+ entries.set(key, {
207
+ member, sprite, tag: { sprite: null, key: '' }, bubble: { sprite: null, key: '' }, emoteAt: member.emote ? -1 : 0, live: null, body: null,
208
+ anim: animator(member.look, { seed: key }),
209
+ });
210
+ },
211
+ remove(id) {
212
+ const entry = entries.get(String(id));
213
+ if (!entry) {
214
+ return;
215
+ }
216
+ entry.sprite.destroy();
217
+ entry.tag.sprite?.destroy();
218
+ entry.bubble.sprite?.destroy();
219
+ entry.live?.destroy();
220
+ entries.delete(String(id));
221
+ },
222
+ update(t) {
223
+ if (atlas.bake(opts.bakeMs ?? 4) > 0) {
224
+ pageTextures.forEach(refresh);
225
+ }
226
+ const wantsLive = [...entries.values()].filter((e) => e.member.live === true).slice(0, liveMax);
227
+ const liveSet = new Set(wantsLive);
228
+ for (const entry of entries.values()) {
229
+ const m = entry.member;
230
+ const body = m.body ?? 'idle';
231
+ if (entry.emoteAt === -1) {
232
+ entry.emoteAt = t;
233
+ }
234
+ const isEmoting = m.emote !== undefined && t - entry.emoteAt < EMOTE_MS;
235
+ if (liveSet.has(entry)) {
236
+ if (!entry.live) {
237
+ entry.live = pixiAvatar(pixi, m.look, { frame: 'full', size: 64, rim: true, shadow: false, animator: entry.anim });
238
+ container.addChild(entry.live.view);
239
+ }
240
+ entry.sprite.visible = false;
241
+ const view = entry.live.view;
242
+ if (entry.body !== body) {
243
+ entry.body = body;
244
+ entry.anim.setBody(body, { at: t });
245
+ }
246
+ entry.live.update({ ...entry.anim.pose(t), body, status: m.status ?? null, phase: (t / (CYCLE[body] ?? 600)) % 1 });
247
+ view.x = m.x;
248
+ view.y = m.y - 32;
249
+ view.zIndex = m.y;
250
+ view.visible = true;
251
+ } else {
252
+ if (entry.live) {
253
+ entry.live.view.visible = false;
254
+ }
255
+ const frame = atlas.frame({
256
+ look: m.look, facing: m.facing ?? 'S', body, status: m.status ?? null,
257
+ phase: (t / (CYCLE[body] ?? 600)) % 1, face: isEmoting ? 'happy' : 'neutral',
258
+ });
259
+ const tex = frame ? textureFor(frame.page, frame.x, frame.y, frame.w, frame.h) : silhouette();
260
+ if (tex) {
261
+ entry.sprite.texture = tex;
262
+ entry.sprite.visible = true;
263
+ }
264
+ if (frame) {
265
+ entry.sprite.anchor.set(frame.footX / frame.w, frame.footY / frame.h);
266
+ entry.sprite.scale.x = frame.mirror ? -1 : 1;
267
+ }
268
+ entry.sprite.x = m.x;
269
+ entry.sprite.y = m.y;
270
+ entry.sprite.zIndex = m.y;
271
+ }
272
+ const top = m.y + nameplateY(m.look, body, m.status ?? null) * (entry.live?.view.visible === true ? 64 / 206 : atlas.scale);
273
+ const tagKey = m.name && !m.tagHidden ? `tag:${m.name}:${m.friend ? 1 : 0}:${m.you ? 1 : 0}` : '';
274
+ placeOverlay(entry.tag, tagKey, () => (m.name ? overlays.nameTag({ name: m.name, friend: m.friend === true, you: m.you === true }) : null), m.x, top);
275
+ const bubbleKey = isEmoting && m.emote ? `emote:${m.emote}` : m.reaction ? `react:${m.reaction}` : m.activity ? `act:${m.activity}` : '';
276
+ placeOverlay(entry.bubble, bubbleKey, () => {
277
+ if (isEmoting && m.emote) {
278
+ return overlays.emoteBubble(m.emote);
279
+ }
280
+ if (m.reaction) {
281
+ return overlays.reactionBubble(m.reaction);
282
+ }
283
+ return m.activity ? overlays.activityBubble({ game: m.activity }) : null;
284
+ }, m.x, top - 12);
285
+ }
286
+ },
287
+ destroy() {
288
+ for (const id of [...entries.keys()]) {
289
+ this.remove(id);
290
+ }
291
+ for (const tex of frameTextures.values()) {
292
+ tex.destroy(false);
293
+ }
294
+ for (const tex of pageTextures) {
295
+ tex.destroy(true);
296
+ }
297
+ for (const o of overlayTextures.values()) {
298
+ o.texture.destroy(true);
299
+ }
300
+ },
301
+ };
302
+ }