@diffenderfer-games/hub 2.0.0-next.5 → 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 (170) hide show
  1. package/CHANGELOG.md +65 -19
  2. package/README.md +3 -6
  3. package/dist/auto.d.ts +5 -5
  4. package/dist/auto.js +2 -2
  5. package/dist/avatar-pixi.d.ts +147 -0
  6. package/dist/avatar-pixi.d.ts.map +1 -0
  7. package/dist/avatar-pixi.js +280 -0
  8. package/dist/avatar-pixi.js.map +1 -0
  9. package/dist/avatar-svg.d.ts +4 -0
  10. package/dist/avatar-svg.js +5 -0
  11. package/dist/avatar-three.d.ts +69 -0
  12. package/dist/avatar-three.d.ts.map +1 -0
  13. package/dist/avatar-three.js +49 -0
  14. package/dist/avatar-three.js.map +1 -0
  15. package/dist/avatar.d.ts +7 -0
  16. package/dist/avatar.js +487 -0
  17. package/dist/avatar.js.map +1 -0
  18. package/dist/daily.d.ts +2 -2
  19. package/dist/doctor/cli.mjs +1 -1
  20. package/dist/index.d.ts +5 -5
  21. package/dist/index.js +2 -2
  22. package/dist/input.d.ts +2 -2
  23. package/dist/shared/animator.d.ts +104 -0
  24. package/dist/shared/animator.d.ts.map +1 -0
  25. package/dist/shared/art.js +1946 -0
  26. package/dist/shared/art.js.map +1 -0
  27. package/dist/shared/atlas.d.ts +134 -0
  28. package/dist/shared/atlas.d.ts.map +1 -0
  29. package/dist/shared/avatar.d.ts +117 -0
  30. package/dist/shared/avatar.d.ts.map +1 -0
  31. package/dist/shared/bridge.js +13 -5
  32. package/dist/shared/bridge.js.map +1 -1
  33. package/dist/shared/client-version.gen.js +1 -1
  34. package/dist/shared/daily.d.ts +129 -18
  35. package/dist/shared/daily.d.ts.map +1 -1
  36. package/dist/shared/daily.js +120 -26
  37. package/dist/shared/daily.js.map +1 -1
  38. package/dist/shared/defineProperty.js +39 -0
  39. package/dist/shared/describe.d.ts +32 -0
  40. package/dist/shared/describe.d.ts.map +1 -0
  41. package/dist/shared/describe.js +109 -0
  42. package/dist/shared/describe.js.map +1 -0
  43. package/dist/shared/globals.js +1 -39
  44. package/dist/shared/globals.js.map +1 -1
  45. package/dist/shared/index.d.ts +503 -14
  46. package/dist/shared/index.d.ts.map +1 -1
  47. package/dist/shared/input.d.ts +31 -13
  48. package/dist/shared/input.d.ts.map +1 -1
  49. package/dist/shared/input.js +40 -2
  50. package/dist/shared/input.js.map +1 -1
  51. package/dist/shared/json-guards.js +48 -0
  52. package/dist/shared/json-guards.js.map +1 -0
  53. package/dist/shared/live-canvas.js +62 -0
  54. package/dist/shared/live-canvas.js.map +1 -0
  55. package/dist/shared/mp.js +1 -1
  56. package/dist/shared/overlays.js +800 -0
  57. package/dist/shared/overlays.js.map +1 -0
  58. package/dist/shared/page-stack.js +2 -1
  59. package/dist/shared/page-stack.js.map +1 -1
  60. package/dist/shared/pose-key.js +249 -0
  61. package/dist/shared/pose-key.js.map +1 -0
  62. package/dist/shared/race.js +1 -1
  63. package/dist/shared/rooms.js +1 -1
  64. package/dist/shared/scene.d.ts +90 -0
  65. package/dist/shared/scene.d.ts.map +1 -0
  66. package/dist/shared/social.d.ts +55 -4
  67. package/dist/shared/social.d.ts.map +1 -1
  68. package/dist/shared/src.js +2020 -144
  69. package/dist/shared/src.js.map +1 -1
  70. package/dist/shared/svg-markup.js +1945 -0
  71. package/dist/shared/svg-markup.js.map +1 -0
  72. package/dist/shared/svg.js +27 -0
  73. package/dist/shared/svg.js.map +1 -0
  74. package/dist/shared/types.d.ts +166 -0
  75. package/dist/shared/types.d.ts.map +1 -0
  76. package/dist/social.d.ts +2 -2
  77. package/dist/social.js +2 -2
  78. package/dist/turns.d.ts +39 -0
  79. package/dist/turns.d.ts.map +1 -0
  80. package/dist/turns.js +117 -0
  81. package/dist/turns.js.map +1 -0
  82. package/package.json +33 -3
  83. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  84. package/src/avatar/adapters/pixi-crowd.ts +302 -0
  85. package/src/avatar/adapters/pixi-types.ts +84 -0
  86. package/src/avatar/adapters/three.ts +126 -0
  87. package/src/avatar/animator/animator.ts +285 -0
  88. package/src/avatar/animator/reactions.ts +44 -0
  89. package/src/avatar/animator/schedule.ts +73 -0
  90. package/src/avatar/art/beard.ts +49 -0
  91. package/src/avatar/art/glasses.ts +67 -0
  92. package/src/avatar/art/hair.ts +254 -0
  93. package/src/avatar/art/headwear.ts +75 -0
  94. package/src/avatar/art/index.ts +21 -0
  95. package/src/avatar/art/shape.ts +87 -0
  96. package/src/avatar/art-store.ts +36 -0
  97. package/src/avatar/atlas.ts +236 -0
  98. package/src/avatar/catalog-view.ts +14 -0
  99. package/src/avatar/colour.ts +109 -0
  100. package/src/avatar/describe.ts +79 -0
  101. package/src/avatar/face.ts +187 -0
  102. package/src/avatar/geom.ts +173 -0
  103. package/src/avatar/layout.ts +47 -0
  104. package/src/avatar/look-like.ts +52 -0
  105. package/src/avatar/outputs/bitmap.ts +75 -0
  106. package/src/avatar/outputs/canvas-paint.ts +177 -0
  107. package/src/avatar/outputs/canvas.ts +62 -0
  108. package/src/avatar/outputs/custom-element.ts +140 -0
  109. package/src/avatar/outputs/element.ts +58 -0
  110. package/src/avatar/outputs/live-canvas.ts +57 -0
  111. package/src/avatar/outputs/overlay-svg.ts +22 -0
  112. package/src/avatar/outputs/pose-key.ts +30 -0
  113. package/src/avatar/outputs/svg-markup.ts +113 -0
  114. package/src/avatar/outputs/svg.ts +41 -0
  115. package/src/avatar/overlays.ts +128 -0
  116. package/src/avatar/render/body.ts +116 -0
  117. package/src/avatar/render/colours.ts +81 -0
  118. package/src/avatar/render/compose.ts +303 -0
  119. package/src/avatar/render/context.ts +117 -0
  120. package/src/avatar/render/eyes.ts +78 -0
  121. package/src/avatar/render/eyewear.ts +118 -0
  122. package/src/avatar/render/frame.ts +60 -0
  123. package/src/avatar/render/head.ts +81 -0
  124. package/src/avatar/render/mouth.ts +86 -0
  125. package/src/avatar/render/props.ts +91 -0
  126. package/src/avatar/render/types.ts +84 -0
  127. package/src/avatar/scene.ts +119 -0
  128. package/src/avatar-pixi.ts +20 -0
  129. package/src/avatar-svg.ts +21 -0
  130. package/src/avatar-three.ts +18 -0
  131. package/src/avatar.ts +62 -0
  132. package/src/core/avatar/avatar-cache.ts +95 -0
  133. package/src/core/avatar/avatar-ref.ts +34 -0
  134. package/src/core/avatar/avatar-types.ts +43 -0
  135. package/src/core/avatar/hub-avatar.ts +308 -0
  136. package/src/core/connection.ts +8 -0
  137. package/src/core/errors.ts +7 -1
  138. package/src/core/hub-progress.ts +39 -8
  139. package/src/core/hub.ts +174 -6
  140. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  141. package/src/core/offline/hub-kinds/index.ts +9 -0
  142. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  143. package/src/core/page-account.ts +121 -0
  144. package/src/core/rest/routes.gen.ts +31 -1
  145. package/src/core/resume/hub-resume.ts +39 -5
  146. package/src/core/resume/resume-types.ts +2 -2
  147. package/src/core/saved/daily-saves.ts +196 -0
  148. package/src/core/saved/daily-seed.ts +16 -0
  149. package/src/core/saved/hub-saved.ts +221 -0
  150. package/src/core/saved/local-saved.ts +122 -0
  151. package/src/core/saved/saved-list.ts +129 -0
  152. package/src/core/saved/saved-open.ts +104 -0
  153. package/src/core/saved/saved-order.ts +49 -0
  154. package/src/core/saved/saved-types.ts +24 -0
  155. package/src/core/turns/hub-turns.ts +140 -0
  156. package/src/core/turns/turn-game.ts +388 -0
  157. package/src/core/version/client-version.gen.ts +2 -2
  158. package/src/daily/daily-system.ts +112 -13
  159. package/src/daily/page-daily.ts +2 -2
  160. package/src/daily/types.ts +46 -9
  161. package/src/doctor/hub-version-fetch.ts +43 -43
  162. package/src/index.ts +10 -1
  163. package/src/input/input-system.ts +10 -1
  164. package/src/input/navigator.ts +11 -0
  165. package/src/input.ts +1 -0
  166. package/src/overlay/overlay.ts +25 -1
  167. package/src/realtime/runtime-api.ts +14 -3
  168. package/src/social.ts +1 -0
  169. package/src/turns-kit/check-rules.ts +182 -0
  170. package/src/turns.ts +13 -0
@@ -0,0 +1,800 @@
1
+ import { t as _defineProperty } from "./defineProperty.js";
2
+ import { A as tr, C as pixelSize, D as path, E as line, F as toLook, O as rect, P as toCode, T as g, _ as mix, a as composeAvatar, f as EXPRESSIONS, h as faceOf, k as rot, w as ellipse } from "./svg-markup.js";
3
+ import { r as renderCanvas, t as renderKey } from "./pose-key.js";
4
+ import { n as renderSvg } from "./svg.js";
5
+ import { hashSeed, mulberry32 } from "@diffenderfer-games/hub-contract/avatar";
6
+ //#region src/avatar/outputs/bitmap.ts
7
+ /**
8
+ * `render.bitmap` and `render.dataUrl`: cached images of an avatar, keyed by
9
+ * code, pose bucket, size and view (an LRU of 256 entries).
10
+ */
11
+ const CACHE_SIZE = 256;
12
+ const bitmaps = /* @__PURE__ */ new Map();
13
+ function remember(cache, key, make) {
14
+ const hit = cache.get(key);
15
+ if (hit !== void 0) {
16
+ cache.delete(key);
17
+ cache.set(key, hit);
18
+ return hit;
19
+ }
20
+ const value = make();
21
+ cache.set(key, value);
22
+ if (cache.size > CACHE_SIZE) {
23
+ const oldest = cache.keys().next().value;
24
+ if (oldest !== void 0) cache.delete(oldest);
25
+ }
26
+ return value;
27
+ }
28
+ /** A canvas of the avatar's frame size (an `OffscreenCanvas` where available). */
29
+ function drawToCanvas(like, opts) {
30
+ const { width, height } = pixelSize(opts);
31
+ const w = Math.max(1, Math.ceil(width));
32
+ const h = Math.max(1, Math.ceil(height));
33
+ if (typeof OffscreenCanvas === "undefined") {
34
+ const canvas = Object.assign(document.createElement("canvas"), {
35
+ width: w,
36
+ height: h
37
+ });
38
+ const ctx = canvas.getContext("2d");
39
+ if (ctx) renderCanvas(ctx, like, {
40
+ ...opts,
41
+ x: 0,
42
+ y: 0
43
+ });
44
+ return canvas;
45
+ }
46
+ const canvas = new OffscreenCanvas(w, h);
47
+ const ctx = canvas.getContext("2d");
48
+ if (ctx) renderCanvas(ctx, like, {
49
+ ...opts,
50
+ x: 0,
51
+ y: 0
52
+ });
53
+ return canvas;
54
+ }
55
+ /** An `ImageBitmap` of the avatar (cached). */
56
+ function renderBitmap(like, opts = {}) {
57
+ return remember(bitmaps, renderKey(toCode(like), opts), () => createImageBitmap(drawToCanvas(like, opts)));
58
+ }
59
+ /** A data URL: `svg` (synchronous inside, the default) or `png`. */
60
+ async function renderDataUrl(like, opts = {}, type = "svg") {
61
+ if (type === "svg") return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(renderSvg(like, opts))}`;
62
+ const canvas = drawToCanvas(like, opts);
63
+ if ("toDataURL" in canvas) return canvas.toDataURL("image/png");
64
+ const blob = await canvas.convertToBlob({ type: "image/png" });
65
+ const bytes = new Uint8Array(await blob.arrayBuffer());
66
+ let binary = "";
67
+ for (const byte of bytes) binary += String.fromCharCode(byte);
68
+ return `data:image/png;base64,${btoa(binary)}`;
69
+ }
70
+ //#endregion
71
+ //#region src/avatar/animator/reactions.ts
72
+ const still = {
73
+ bob: {
74
+ x: 0,
75
+ y: 0
76
+ },
77
+ tilt: 0,
78
+ squash: 0
79
+ };
80
+ const ease = (u) => Math.sin(u * Math.PI);
81
+ /** Every reaction's track; `u` runs 0 … 1 over `ms`. */
82
+ const REACTIONS = {
83
+ nod: {
84
+ ms: 700,
85
+ at: (u) => ({
86
+ bob: {
87
+ x: 0,
88
+ y: Math.sin(u * Math.PI * 2) * 3 * (1 - u)
89
+ },
90
+ tilt: 0,
91
+ squash: 0
92
+ })
93
+ },
94
+ shake: {
95
+ ms: 800,
96
+ at: (u) => ({
97
+ bob: {
98
+ x: Math.sin(u * Math.PI * 4) * 4 * (1 - u),
99
+ y: 0
100
+ },
101
+ tilt: 0,
102
+ squash: 0
103
+ })
104
+ },
105
+ bounce: {
106
+ ms: 600,
107
+ expression: "happy",
108
+ at: (u) => ({
109
+ bob: {
110
+ x: 0,
111
+ y: -ease(u) * 8
112
+ },
113
+ tilt: 0,
114
+ squash: -ease(u) * .04
115
+ })
116
+ },
117
+ cheer: {
118
+ ms: 1400,
119
+ expression: "laugh",
120
+ body: "cheer",
121
+ at: (u) => ({
122
+ bob: {
123
+ x: 0,
124
+ y: -Math.abs(Math.sin(u * Math.PI * 3)) * 6
125
+ },
126
+ tilt: 0,
127
+ squash: 0
128
+ })
129
+ },
130
+ gasp: {
131
+ ms: 900,
132
+ expression: "surprised",
133
+ at: (u) => ({
134
+ bob: {
135
+ x: 0,
136
+ y: -ease(Math.min(1, u * 2)) * 3
137
+ },
138
+ tilt: 0,
139
+ squash: -ease(u) * .05
140
+ })
141
+ },
142
+ flinch: {
143
+ ms: 600,
144
+ expression: "shocked",
145
+ at: (u) => ({
146
+ bob: {
147
+ x: -ease(u) * 5,
148
+ y: ease(u) * 2
149
+ },
150
+ tilt: -ease(u) * 6,
151
+ squash: ease(u) * .04
152
+ })
153
+ },
154
+ shrug: {
155
+ ms: 900,
156
+ expression: "smug",
157
+ at: (u) => ({
158
+ bob: {
159
+ x: 0,
160
+ y: -ease(u) * 3
161
+ },
162
+ tilt: Math.sin(u * Math.PI * 2) * 5,
163
+ squash: 0
164
+ })
165
+ },
166
+ sleep: {
167
+ ms: 3e3,
168
+ expression: "sleepy",
169
+ body: "away",
170
+ at: () => still
171
+ },
172
+ wave: {
173
+ ms: 1500,
174
+ expression: "happy",
175
+ body: "wave",
176
+ at: (u) => ({
177
+ bob: {
178
+ x: 0,
179
+ y: 0
180
+ },
181
+ tilt: Math.sin(u * Math.PI * 4) * 3,
182
+ squash: 0
183
+ })
184
+ }
185
+ };
186
+ /** No motion. */
187
+ const STILL = still;
188
+ const BLINK_BUCKET = 4e3;
189
+ /** A deterministic random in [0, 1) for (seed, channel, bucket). */
190
+ function cell(seed, channel, bucket) {
191
+ return mulberry32(hashSeed(`${String(seed)}:${String(channel)}:${String(bucket)}`));
192
+ }
193
+ /** One blink's closure at offset `dt` from its start: up then down. */
194
+ function closure(dt) {
195
+ if (dt < 0 || dt > 150) return 0;
196
+ const half = 75;
197
+ return dt < half ? dt / half : 1 - (dt - half) / half;
198
+ }
199
+ /**
200
+ * Lid closure from blinking, 0 … 1: one blink per 4 s bucket at a seeded
201
+ * offset (so intervals fall between about 2 and 6 s), 10% of them doubled.
202
+ */
203
+ function blinkAt(seed, t) {
204
+ const bucket = Math.floor(t / BLINK_BUCKET);
205
+ let out = 0;
206
+ for (const b of [bucket - 1, bucket]) {
207
+ const r = cell(seed, 1, b);
208
+ const start = b * BLINK_BUCKET + 1e3 + r() * 2e3;
209
+ const isDouble = r() < .1;
210
+ out = Math.max(out, closure(t - start), isDouble ? closure(t - start - 150 - 90) : 0);
211
+ }
212
+ return out;
213
+ }
214
+ /** An idle wandering gaze target: holds for 3–8 s, small amplitude. */
215
+ function wanderAt(seed, t) {
216
+ const r = cell(seed, 2, Math.floor(t / 5500));
217
+ if (r() < .45) return {
218
+ x: 0,
219
+ y: 0
220
+ };
221
+ return {
222
+ x: (r() * 2 - 1) * .6,
223
+ y: (r() * 2 - 1) * .3
224
+ };
225
+ }
226
+ /** Breathing bob (rig units) and a slow sway (degrees). */
227
+ function breathAt(seed, t) {
228
+ const phase = seed % 1e3 / 1e3;
229
+ return {
230
+ bob: Math.sin((t / 2400 + phase) * Math.PI * 2) * .6,
231
+ sway: Math.sin((t / 5200 + phase) * Math.PI * 2) * 1.2
232
+ };
233
+ }
234
+ /** An occasional micro smile while idle: its weight at t (0 most of the time). */
235
+ function microSmileAt(seed, t) {
236
+ const bucket = Math.floor(t / 11e3);
237
+ const r = cell(seed, 3, bucket);
238
+ if (r() < .6) return 0;
239
+ const dt = t - (bucket * 11e3 + 2e3 + r() * 6e3);
240
+ if (dt < 0 || dt > 900) return 0;
241
+ return Math.sin(dt / 900 * Math.PI) * .35;
242
+ }
243
+ //#endregion
244
+ //#region src/avatar/animator/animator.ts
245
+ /**
246
+ * The animator (avatars.md §3.7): a pure, time-based face and body driver.
247
+ * Every call (`express`, `lookAt`, `react`, …) appends to a timestamped
248
+ * schedule and `pose(t)` evaluates it, with no clock or animation frame of
249
+ * its own: the same schedule and `t` always give the same pose, so it suits
250
+ * game loops, Pixi tickers, three.js, CSS-driven DOM, tests and replays.
251
+ */
252
+ const KEEP = 48;
253
+ const push = (list, item) => {
254
+ list.push(item);
255
+ if (list.length > KEEP) list.shift();
256
+ };
257
+ /** The last event at or before t. */
258
+ const latest = (list, t) => {
259
+ for (let i = list.length - 1; i >= 0; i -= 1) {
260
+ const e = list[i];
261
+ if (e && e.t <= t) return e;
262
+ }
263
+ };
264
+ const easeOut = (u) => 1 - (1 - Math.min(1, Math.max(0, u))) ** 2;
265
+ function prefersReducedMotion() {
266
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
267
+ }
268
+ /** A face and body driver for one avatar. */
269
+ var Animator = class {
270
+ constructor(look, opts = {}) {
271
+ _defineProperty(this, "reducedMotion", void 0);
272
+ _defineProperty(this, "seed", void 0);
273
+ _defineProperty(this, "idle", void 0);
274
+ _defineProperty(this, "clock", void 0);
275
+ _defineProperty(this, "bases", []);
276
+ _defineProperty(this, "expresses", []);
277
+ _defineProperty(this, "looks", []);
278
+ _defineProperty(this, "talks", []);
279
+ _defineProperty(this, "reacts", []);
280
+ _defineProperty(this, "bodies", []);
281
+ _defineProperty(this, "statuses", []);
282
+ _defineProperty(this, "current", void 0);
283
+ this.current = look;
284
+ this.seed = hashSeed(opts.seed ?? (typeof look === "string" ? look : "dg"));
285
+ this.idle = opts.idle ?? true;
286
+ this.reducedMotion = opts.reducedMotion === "auto" || opts.reducedMotion === void 0 ? prefersReducedMotion() : opts.reducedMotion;
287
+ this.clock = opts.clock ?? (() => typeof performance === "undefined" ? Date.now() : performance.now());
288
+ this.bases.push({
289
+ t: -Infinity,
290
+ face: EXPRESSIONS[opts.base ?? "neutral"],
291
+ fade: 0
292
+ });
293
+ }
294
+ /** The look this animator drives (adapters render it with each pose). */
295
+ get look() {
296
+ return this.current;
297
+ }
298
+ /** Swaps the look (an `ev:avatar` change) without resetting the schedule. */
299
+ setLook(look) {
300
+ this.current = look;
301
+ return this;
302
+ }
303
+ now(o) {
304
+ return o?.at ?? this.clock();
305
+ }
306
+ fade(ms) {
307
+ return this.reducedMotion ? Math.min(ms, 120) : ms;
308
+ }
309
+ /** Shows an expression (or raw parameters), then returns to the base. */
310
+ express(e, o = {}) {
311
+ push(this.expresses, {
312
+ t: this.now(o),
313
+ face: faceOf(e),
314
+ fade: this.fade(o.fadeMs ?? 180),
315
+ hold: o.holdMs ?? 1500
316
+ });
317
+ return this;
318
+ }
319
+ /** Sets the resting expression. */
320
+ setBase(e, o = {}) {
321
+ push(this.bases, {
322
+ t: this.now(o),
323
+ face: faceOf(e),
324
+ fade: this.fade(o.fadeMs ?? 250)
325
+ });
326
+ return this;
327
+ }
328
+ /** Eases the gaze toward a direction; `null` goes back to wandering (or the centre with `idle: false`). */
329
+ lookAt(target, o = {}) {
330
+ push(this.looks, {
331
+ t: this.now(o),
332
+ target,
333
+ speed: o.speed ?? 6
334
+ });
335
+ return this;
336
+ }
337
+ /** Flaps the mouth until `talk(false)`. */
338
+ talk(isOn, o = {}) {
339
+ const t = this.now(o);
340
+ push(this.talks, {
341
+ t,
342
+ level: isOn ? 1 : 0,
343
+ until: Infinity
344
+ });
345
+ return this;
346
+ }
347
+ /** Talks for a duration. */
348
+ talkFor(ms, o = {}) {
349
+ const t = this.now(o);
350
+ push(this.talks, {
351
+ t,
352
+ level: 1,
353
+ until: t + ms
354
+ });
355
+ return this;
356
+ }
357
+ /** Drives the mouth from audio amplitude (0 … 1). */
358
+ talkLevel(level, o = {}) {
359
+ push(this.talks, {
360
+ t: this.now(o),
361
+ level: Math.max(0, Math.min(1, level)),
362
+ until: Infinity
363
+ });
364
+ return this;
365
+ }
366
+ /** Plays a one-shot reaction. */
367
+ react(reaction, o = {}) {
368
+ push(this.reacts, {
369
+ t: this.now(o),
370
+ reaction
371
+ });
372
+ return this;
373
+ }
374
+ /** Sets the body pose; cycle poses (`walk`, `run`, `dance`) loop every `cycleMs`. */
375
+ setBody(body, o = {}) {
376
+ push(this.bodies, {
377
+ t: this.now(o),
378
+ body,
379
+ cycleMs: o.cycleMs ?? (body === "run" ? 320 : 520)
380
+ });
381
+ return this;
382
+ }
383
+ /** Sets or clears the status prop. */
384
+ setStatus(status, o = {}) {
385
+ push(this.statuses, {
386
+ t: this.now(o),
387
+ status
388
+ });
389
+ return this;
390
+ }
391
+ /** Plays the tuned face and reaction for a game event. */
392
+ feel(feeling, o = {}) {
393
+ const at = { at: this.now(o) };
394
+ switch (feeling) {
395
+ case "win": return this.express("laugh", {
396
+ ...at,
397
+ holdMs: 1800
398
+ }).react("cheer", at);
399
+ case "lose": return this.express("sad", {
400
+ ...at,
401
+ holdMs: 1800
402
+ });
403
+ case "bigPlay": return this.express("grin", at).react("bounce", at);
404
+ case "victim": return this.express("shocked", {
405
+ ...at,
406
+ holdMs: 900
407
+ }).react("flinch", at);
408
+ case "waiting": return this.setBase("thinking", at);
409
+ case "yourTurn": return this.express("determined", at).react("nod", at);
410
+ case "idle": return this.setBase("neutral", at);
411
+ }
412
+ }
413
+ faceAt(t) {
414
+ const base = latest(this.bases, t);
415
+ const prior = base ? latest(this.bases.filter((b) => b.t < base.t), t) : void 0;
416
+ let face = base ? prior && base.fade > 0 ? mix(prior.face, base.face, easeOut((t - base.t) / base.fade)) : base.face : EXPRESSIONS.neutral;
417
+ if (this.idle) {
418
+ const smile = microSmileAt(this.seed, t);
419
+ if (smile > 0) face = mix(face, "happy", smile);
420
+ }
421
+ const e = latest(this.expresses, t);
422
+ if (e) {
423
+ const dt = t - e.t;
424
+ if (dt < e.fade * 2 + e.hold) {
425
+ const w = dt < e.fade ? easeOut(dt / e.fade) : dt < e.fade + e.hold ? 1 : 1 - easeOut((dt - e.fade - e.hold) / e.fade);
426
+ face = mix(face, e.face, w);
427
+ }
428
+ }
429
+ const r = latest(this.reacts, t);
430
+ const track = r ? REACTIONS[r.reaction] : void 0;
431
+ if (r && track?.expression && t - r.t < track.ms) face = mix(face, track.expression, Math.min(1, (t - r.t) / 120));
432
+ return face;
433
+ }
434
+ gazeAt(t) {
435
+ let from = {
436
+ x: 0,
437
+ y: 0
438
+ };
439
+ let current;
440
+ for (const e of this.looks) {
441
+ if (e.t > t) break;
442
+ if (current) from = this.approach(from, current, e.t);
443
+ current = e;
444
+ }
445
+ if (!current || current.target === null) {
446
+ const wander = this.idle && !this.reducedMotion ? wanderAt(this.seed, t) : {
447
+ x: 0,
448
+ y: 0
449
+ };
450
+ return current ? this.approach(from, {
451
+ ...current,
452
+ target: wander
453
+ }, t) : wander;
454
+ }
455
+ return this.approach(from, current, t);
456
+ }
457
+ approach(from, e, t) {
458
+ const target = e.target ?? {
459
+ x: 0,
460
+ y: 0
461
+ };
462
+ const k = this.reducedMotion ? 1 : 1 - Math.exp(-e.speed * Math.max(0, t - e.t) / 1e3);
463
+ return {
464
+ x: from.x + (target.x - from.x) * k,
465
+ y: from.y + (target.y - from.y) * k
466
+ };
467
+ }
468
+ talkAt(t) {
469
+ const e = latest(this.talks, t);
470
+ if (!e || e.level === 0 || t > e.until) return 0;
471
+ if (this.reducedMotion) return .5 * e.level;
472
+ const flap = Math.abs(Math.sin((t - e.t) / 1e3 * Math.PI * 5.5)) * (.65 + .35 * Math.sin((t - e.t) / 1e3 * 3.1));
473
+ return e.level * flap;
474
+ }
475
+ reactionAt(t) {
476
+ const r = latest(this.reacts, t);
477
+ if (!r) return { frame: STILL };
478
+ const track = REACTIONS[r.reaction];
479
+ const u = (t - r.t) / track.ms;
480
+ if (u >= 1) return { frame: STILL };
481
+ return {
482
+ frame: this.reducedMotion ? STILL : track.at(u),
483
+ ...track.body ? { body: track.body } : {}
484
+ };
485
+ }
486
+ /** The pose at time `t` (same clock as the calls). Pure: the same schedule and `t` give the same pose. */
487
+ pose(t = this.clock()) {
488
+ const face = this.faceAt(t);
489
+ const reaction = this.reactionAt(t);
490
+ const bodyEvent = latest(this.bodies, t);
491
+ const body = reaction.body ?? bodyEvent?.body ?? "idle";
492
+ const cycle = bodyEvent ? (t - bodyEvent.t) / bodyEvent.cycleMs % 1 : 0;
493
+ const breath = this.idle && !this.reducedMotion ? breathAt(this.seed, t) : {
494
+ bob: 0,
495
+ sway: 0
496
+ };
497
+ const status = latest(this.statuses, t)?.status ?? null;
498
+ return {
499
+ face,
500
+ look: this.gazeAt(t),
501
+ blink: blinkAt(this.seed, t),
502
+ talk: this.talkAt(t),
503
+ body,
504
+ phase: reaction.body === "cheer" ? t / 467 % 1 : cycle,
505
+ status,
506
+ bob: {
507
+ x: reaction.frame.bob.x,
508
+ y: reaction.frame.bob.y + breath.bob
509
+ },
510
+ tilt: reaction.frame.tilt + breath.sway,
511
+ squash: reaction.frame.squash
512
+ };
513
+ }
514
+ };
515
+ /** A new animator for a look: `animator(look, { seed: userId })`. */
516
+ function animator(look, opts = {}) {
517
+ return new Animator(look, opts);
518
+ }
519
+ //#endregion
520
+ //#region src/avatar/layout.ts
521
+ /** Anchors, facings from movement, and the look-at helper (§3.2, §3.3, §3.6). */
522
+ /**
523
+ * The rig anchors for a look in a facing and pose: place name plates,
524
+ * bubbles, props and world objects with these, never with magic numbers.
525
+ * Rig units (a 100-unit head), feet at (0, 0), y down.
526
+ */
527
+ function layout(like, opts = {}) {
528
+ return composeAvatar(toLook(like), {
529
+ frame: "full",
530
+ ...opts
531
+ }).anchors;
532
+ }
533
+ const EIGHT = [
534
+ "E",
535
+ "SE",
536
+ "S",
537
+ "SW",
538
+ "W",
539
+ "NW",
540
+ "N",
541
+ "NE"
542
+ ];
543
+ const FOUR = [
544
+ "E",
545
+ "S",
546
+ "W",
547
+ "N"
548
+ ];
549
+ /** The facing for a movement vector (screen space, y down), in 8 or 4 directions. */
550
+ function facingFrom(dx, dy, dirs = 8) {
551
+ if (dx === 0 && dy === 0) return "S";
552
+ const angle = Math.atan2(dy, dx);
553
+ const list = dirs === 8 ? EIGHT : FOUR;
554
+ const step = Math.PI * 2 / list.length;
555
+ return list[(Math.round(angle / step) % list.length + list.length) % list.length] ?? "S";
556
+ }
557
+ /**
558
+ * A look direction from one screen point toward another, for card tables
559
+ * where each seat looks at the active player. `strength` scales it.
560
+ */
561
+ function lookToward(from, to, strength = 1) {
562
+ const dx = to.x - from.x;
563
+ const dy = to.y - from.y;
564
+ const len = Math.hypot(dx, dy);
565
+ if (len < 1e-6) return {
566
+ x: 0,
567
+ y: 0
568
+ };
569
+ return {
570
+ x: dx / len * strength,
571
+ y: dy / len * strength * .7
572
+ };
573
+ }
574
+ //#endregion
575
+ //#region src/avatar/overlays.ts
576
+ /**
577
+ * Overlays drawn above an avatar (§3.11, §14.3), as scene nodes in screen
578
+ * pixels with the origin at the anchor they sit on (`nameplate`, `bubble`,
579
+ * `shadow`). Every surface draws them the same way. Text is a username or a
580
+ * catalog title, never player-typed bubble text, and is escaped on output.
581
+ */
582
+ const INK = "#0d0f14";
583
+ const PAPER = "#e8e6df";
584
+ const MONO_ADVANCE = .6;
585
+ const textWidth = (text, size) => Array.from(text).length * size * MONO_ADVANCE;
586
+ /** A name tag: the username in a pill; friends get a green dot and a ★ (never colour alone), you get an accent outline. */
587
+ function nameTag(o) {
588
+ const size = 12;
589
+ const lead = o.friend ? 22 : 0;
590
+ const w = textWidth(o.name, size) + 16 + lead;
591
+ const h = 19;
592
+ const nodes = [rect(-w / 2, -19, w, h, {
593
+ fill: INK,
594
+ fillOpacity: .86,
595
+ stroke: o.you ? "#ff5f3b" : "#2a3142",
596
+ sw: o.you ? 2 : 1.2
597
+ }, h / 2)];
598
+ if (o.friend) nodes.push(ellipse(-w / 2 + 10, -19 / 2, 3.6, 3.6, { fill: "#4ade80" }), {
599
+ k: "text",
600
+ x: -w / 2 + 19,
601
+ y: -5,
602
+ text: "★",
603
+ size: 11,
604
+ font: "mono",
605
+ anchor: "middle",
606
+ fill: "#ffd23f"
607
+ });
608
+ nodes.push({
609
+ k: "text",
610
+ x: lead / 2,
611
+ y: -5.5,
612
+ text: o.name,
613
+ size,
614
+ font: "mono",
615
+ bold: true,
616
+ anchor: "middle",
617
+ fill: o.muted ? "#6f7787" : PAPER
618
+ });
619
+ return {
620
+ nodes,
621
+ width: w,
622
+ height: h,
623
+ box: [
624
+ -w / 2,
625
+ -19,
626
+ w,
627
+ h
628
+ ]
629
+ };
630
+ }
631
+ function icon(kind) {
632
+ switch (kind) {
633
+ case "cards": return g([rect(-6, -8, 12, 16, {
634
+ fill: "#ff5f3b",
635
+ stroke: INK,
636
+ sw: 1.6
637
+ }, 2.5), g([rect(-1, -11, 12, 16, {
638
+ fill: "#3d7bff",
639
+ stroke: INK,
640
+ sw: 1.6
641
+ }, 2.5)], { tf: [rot(18)] })], { tf: [rot(-10)] });
642
+ case "controller": return path("M-9 -2C-9 -7 9 -7 9 -2C11 6 6 8 3 4L-3 4C-6 8 -11 6 -9 -2Z", {
643
+ fill: "#2a3142",
644
+ stroke: INK,
645
+ sw: 1.6
646
+ });
647
+ case "puzzle": return path("M-7 -7H-1C-1 -11 4 -11 4 -7H7V-1C11 -1 11 4 7 4V7H-7Z", {
648
+ fill: "#4ade80",
649
+ stroke: INK,
650
+ sw: 1.6
651
+ });
652
+ case "race": return path("M-7 -8V8M-7 -8H7L4 -4L7 0H-7", {
653
+ fill: "#ffd23f",
654
+ stroke: INK,
655
+ sw: 1.6
656
+ });
657
+ }
658
+ }
659
+ /** "Playing Uno": the game's catalog title in a light bubble with a tail. */
660
+ function activityBubble(o) {
661
+ const text = `${o.verb ?? "Playing"} ${o.game}`;
662
+ const w = textWidth(text, 12) + 40;
663
+ return {
664
+ nodes: [
665
+ rect(-w / 2, -33, w, 26, {
666
+ fill: PAPER,
667
+ stroke: INK,
668
+ sw: 2
669
+ }, 8),
670
+ path("M-6 -7.5L0 0L6 -7.5Z", {
671
+ fill: PAPER,
672
+ stroke: INK,
673
+ sw: 2
674
+ }),
675
+ rect(-7, -10, 14, 4, { fill: PAPER }),
676
+ g([icon(o.icon ?? "cards")], { tf: [tr(-w / 2 + 16, -20)] }),
677
+ {
678
+ k: "text",
679
+ x: 10,
680
+ y: -15.5,
681
+ text,
682
+ size: 12,
683
+ font: "mono",
684
+ bold: true,
685
+ anchor: "middle",
686
+ fill: INK
687
+ }
688
+ ],
689
+ width: w,
690
+ height: 33,
691
+ box: [
692
+ -w / 2,
693
+ -33,
694
+ w,
695
+ 33
696
+ ]
697
+ };
698
+ }
699
+ /** An emote popping above the head (the hub's quick-chat emotes). */
700
+ function emoteBubble(emote) {
701
+ const glyph = emote.startsWith("e:") ? emote.slice(2) : emote;
702
+ return {
703
+ nodes: [
704
+ ellipse(0, -24, 20, 17, {
705
+ fill: PAPER,
706
+ stroke: INK,
707
+ sw: 2
708
+ }),
709
+ path("M-5 -9L0 0L5 -9Z", {
710
+ fill: PAPER,
711
+ stroke: INK,
712
+ sw: 2
713
+ }),
714
+ ellipse(0, -10, 5, 2.4, { fill: PAPER }),
715
+ {
716
+ k: "text",
717
+ x: 0,
718
+ y: -17,
719
+ text: glyph,
720
+ size: 20,
721
+ font: "mono",
722
+ anchor: "middle",
723
+ fill: INK
724
+ }
725
+ ],
726
+ width: 40,
727
+ height: 42,
728
+ box: [
729
+ -21,
730
+ -42,
731
+ 42,
732
+ 42
733
+ ]
734
+ };
735
+ }
736
+ /** A reaction glyph in a small bubble. */
737
+ function reactionBubble(glyph) {
738
+ const colour = glyph === "♥" ? "#ff5f8f" : glyph === "!" ? "#ff5f3b" : INK;
739
+ return {
740
+ nodes: [
741
+ rect(-15, -30, 30, 24, {
742
+ fill: PAPER,
743
+ stroke: INK,
744
+ sw: 2
745
+ }, 9),
746
+ path("M-4 -6.5L0 0L4 -6.5Z", {
747
+ fill: PAPER,
748
+ stroke: INK,
749
+ sw: 2
750
+ }),
751
+ rect(-5, -9, 10, 4, { fill: PAPER }),
752
+ {
753
+ k: "text",
754
+ x: 0,
755
+ y: -12,
756
+ text: glyph,
757
+ size: glyph === "zzz" ? 12 : 17,
758
+ font: "mono",
759
+ bold: true,
760
+ anchor: "middle",
761
+ fill: colour
762
+ }
763
+ ],
764
+ width: 30,
765
+ height: 30,
766
+ box: [
767
+ -16,
768
+ -31,
769
+ 32,
770
+ 31
771
+ ]
772
+ };
773
+ }
774
+ /** The hover or selection ring at the feet (`radius` in px). */
775
+ function selectionRing(o = {}) {
776
+ const r = o.radius ?? 18;
777
+ return {
778
+ nodes: [ellipse(0, 0, r, r * .3, line(o.selected ? "#ff5f3b" : "#e8e6df", o.selected ? 2.4 : 1.6, "round", .85))],
779
+ width: r * 2,
780
+ height: r * .6,
781
+ box: [
782
+ -r - 2,
783
+ -r * .3 - 2,
784
+ r * 2 + 4,
785
+ r * .6 + 4
786
+ ]
787
+ };
788
+ }
789
+ /** Every overlay builder, as the `overlays` namespace. */
790
+ const overlays = {
791
+ nameTag,
792
+ activityBubble,
793
+ emoteBubble,
794
+ reactionBubble,
795
+ selectionRing
796
+ };
797
+ //#endregion
798
+ export { Animator as a, drawToCanvas as c, lookToward as i, renderBitmap as l, facingFrom as n, animator as o, layout as r, REACTIONS as s, overlays as t, renderDataUrl as u };
799
+
800
+ //# sourceMappingURL=overlays.js.map