@fabi-play/sdk 0.3.0 → 0.5.0

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.
@@ -2,6 +2,35 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
+ // ../protocol/src/picture.ts
6
+ var MAX_PICTURE_BYTES = 3 * 1024 * 1024;
7
+ var MAX_PICTURE_SIDE = 1920;
8
+ var PICTURE_JPEG_QUALITY = 0.85;
9
+ var MAX_PICTURE_TITLE = 60;
10
+ var PICTURE_RESULTS = ["sent", "unavailable", "too-big", "error"];
11
+ var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
12
+ var MAGIC = { "image/jpeg": "/9j/", "image/png": "iVBORw0KGg" };
13
+ var PREFIX = /^data:(image\/(?:jpeg|png));base64,/;
14
+ var decodedSize = (b64) => b64.length / 4 * 3 - (b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0);
15
+ var MAX_BASE64 = Math.ceil(MAX_PICTURE_BYTES / 3) * 4;
16
+ var isImage = (mime, b64) => b64.length % 4 === 0 && b64.length > 0 && b64.startsWith(MAGIC[mime]) && BASE64.test(b64);
17
+ function checkPicture(v) {
18
+ if (typeof v !== "string") return { error: "invalid" };
19
+ const m = PREFIX.exec(v);
20
+ if (!m) return { error: "invalid" };
21
+ const mime = m[1];
22
+ const base64 = v.slice(m[0].length);
23
+ if (base64.length > MAX_BASE64) return { error: "too-big" };
24
+ if (!isImage(mime, base64)) return { error: "invalid" };
25
+ const size = decodedSize(base64);
26
+ return size > MAX_PICTURE_BYTES ? { error: "too-big" } : { ok: { mime, base64, size } };
27
+ }
28
+ function parsePictureTitle(v) {
29
+ if (typeof v !== "string") return void 0;
30
+ const t = Array.from(v.replace(/[\p{Cc}\p{Cf}]/gu, "").trim()).slice(0, MAX_PICTURE_TITLE).join("").trim();
31
+ return t || void 0;
32
+ }
33
+
5
34
  // ../protocol/src/index.ts
6
35
  var PROTOCOL_VERSION = 1;
7
36
  var LANDMARKS = 33;
@@ -57,6 +86,15 @@ function parseGameStats(v) {
57
86
  if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
58
87
  return out;
59
88
  }
89
+ function parseCursor(v) {
90
+ if (!isObject(v) || !isNumber(v.x) || !isNumber(v.y) || typeof v.down !== "boolean") return void 0;
91
+ const c = (n) => Math.min(Math.max(n, 0), 1);
92
+ return { type: "cursor", x: c(v.x), y: c(v.y), down: v.down };
93
+ }
94
+ function parsePictureResult(v) {
95
+ if (!isObject(v) || !PICTURE_RESULTS.includes(v.result)) return void 0;
96
+ return { type: "picture-result", ...Number.isInteger(v.id) ? { id: v.id } : {}, result: v.result };
97
+ }
60
98
  var isPlayerId = (id) => Number.isInteger(id) && id >= 1 && id <= MAX_PLAYERS;
61
99
  function parseTurnPlayers(v) {
62
100
  return Array.isArray(v) && v.length === 1 && isPlayerId(v[0]) ? [v[0]] : void 0;
@@ -65,6 +103,7 @@ function parseTurnOrder(v) {
65
103
  if (!Array.isArray(v) || v.length < 1 || v.length > MAX_PLAYERS || !v.every(isPlayerId) || new Set(v).size !== v.length) return void 0;
66
104
  return [...v];
67
105
  }
106
+ var COVER_ASPECT = 16 / 9;
68
107
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
69
108
  var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
70
109
  var POSE_HEADER = 12;
@@ -305,6 +344,7 @@ function headShapes(def, ow) {
305
344
  if (has("locks")) {
306
345
  out.push({ k: "path", d: smoothClosed(pts(-1.14, 0.58, -1.24, -0.2, -0.86, -0.98, 0, -1.2, 0.86, -0.98, 1.24, -0.2, 1.14, 0.58, 0.8, 0.66, 0, 0.2, -0.8, 0.66)), fill: p.hair ?? p.outline, ...ol });
307
346
  }
347
+ if (has("bun")) out.push({ k: "circle", x: 0.04, y: -1.14, r: 0.4, fill: p.hair ?? p.outline, ...ol });
308
348
  if (has("ninja-hood")) {
309
349
  for (const [rot, len2] of [
310
350
  [0.35, 0.5],
@@ -342,7 +382,15 @@ function headShapes(def, ow) {
342
382
  if (muzzle === "disc") for (const s of SIDES) out.push({ k: "circle", x: s * 0.4, y: 0.02, r: 0.44, fill: mc });
343
383
  if (muzzle === "cheeks") for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.4, y: 0.44, rx: 0.44, ry: 0.32, fill: mc });
344
384
  if (muzzle === "band") out.push({ k: "path", d: lowerFace(def, ow), fill: mc });
345
- if (has("stripes")) {
385
+ if (has("stripes") && def.head === "snout-long") {
386
+ const c = extra(0, p.outline);
387
+ for (const [x0, y0, x1, y1] of [
388
+ [0.46, -0.94, 0.36, -0.66],
389
+ [0.8, -0.72, 0.62, -0.5],
390
+ [1, -0.36, 0.78, -0.24]
391
+ ])
392
+ out.push({ k: "line", pts: [x0, y0, x1, y1], stroke: c, lw: 0.11 });
393
+ } else if (has("stripes")) {
346
394
  const c = extra(0, p.outline);
347
395
  out.push({ k: "line", pts: [0, -0.97, 0, -0.66], stroke: c, lw: 0.13 });
348
396
  for (const s of SIDES) {
@@ -357,6 +405,13 @@ function headShapes(def, ow) {
357
405
  out.push({ k: "circle", x: 0.58, y: -0.42, r: 0.11, fill: c });
358
406
  out.push({ k: "circle", x: 0.22, y: -0.76, r: 0.09, fill: c });
359
407
  }
408
+ if (def.head === "snout-long") {
409
+ for (const [x, y, r] of [
410
+ [-1.38, -0.14, 0.06],
411
+ [-1.14, -0.22, 0.055]
412
+ ])
413
+ out.push({ k: "ellipse", x, y, rx: r, ry: r * 0.8, rot: -0.3, fill: p.outline, alpha: 0.85 });
414
+ }
360
415
  if (has("forehead")) out.push({ k: "path", d: smoothClosed(pts(-0.62, -0.66, 0, -0.86, 0.62, -0.66, 0.26, -0.36, 0, -0.1, -0.26, -0.36), 0.9), fill: extra(0, p.accent) });
361
416
  if (has("eye-spot")) out.push({ k: "ellipse", x: 0.42, y: -0.06, rx: 0.3, ry: 0.33, rot: 0.3, fill: p.ears ?? p.accent });
362
417
  if (has("gills")) {
@@ -365,6 +420,7 @@ function headShapes(def, ow) {
365
420
  const hair = p.hair ?? p.outline;
366
421
  if (has("hair")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.06, -1.02, -0.58, -0.62, -1.04, 0.08, -1.14, 0.76, -0.92, 1.05, -0.42, 1, -0.06, 0.84, -0.4, 0.42, -0.5, 0.06, -0.4, -0.34, -0.56, -0.74, -0.38)), fill: hair, ...ol });
367
422
  if (has("locks")) out.push({ k: "path", d: smoothClosed(pts(-1, -0.08, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1, -0.08, 0.8, -0.38, 0.36, -0.44, 0, -0.4, -0.36, -0.44, -0.8, -0.38)), fill: hair, ...ol });
423
+ if (has("bun")) out.push({ k: "path", d: smoothClosed(pts(-1.02, -0.1, -0.98, -0.62, -0.56, -1.02, 0, -1.1, 0.56, -1.02, 0.98, -0.62, 1.02, -0.1, 0.84, -0.36, 0.5, -0.56, 0.12, -0.48, -0.22, -0.6, -0.6, -0.5, -0.86, -0.36)), fill: hair, ...ol });
368
424
  if (has("curls")) {
369
425
  for (let i = 0; i < 7; i++) {
370
426
  const c = polar(-1.3 + i / 6 * 2.6, 0.86);
@@ -386,7 +442,7 @@ function headShapes(def, ow) {
386
442
  }
387
443
  if (def.face.brows) for (const s of SIDES) out.push({ k: "line", pts: [s * 0.2, -0.2, s * 0.54, -0.28], stroke: def.face.brows, lw: 0.09 });
388
444
  out.push(...mouth(def, ow));
389
- if (def.face.cheeks) for (const s of SIDES) out.push({ k: "ellipse", x: s * 0.64, y: 0.3, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
445
+ if (def.face.cheeks) for (const c of cheekSpots(def)) out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.15, ry: 0.1, fill: def.face.cheeks, alpha: 0.8 });
390
446
  if (has("whiskers")) {
391
447
  for (const s of SIDES)
392
448
  for (const dy of [-0.1, 0.06]) out.push({ k: "line", pts: [s * 0.62, 0.38 + dy, s * 1.06, 0.3 + dy * 2.2], stroke: p.outline, lw: ow * 0.7, alpha: 0.75 });
@@ -408,6 +464,24 @@ function headShapes(def, ow) {
408
464
  out.push({ k: "line", pts: [-w, y + 0.04, w, y - 0.04], stroke: p.outline, lw: ow * 0.7, alpha: 0.6 });
409
465
  }
410
466
  }
467
+ if (has("flower-crown")) {
468
+ const cols = [extra(1, "#f7a8cf"), extra(2, "#fff3a0"), extra(3, WHITE)];
469
+ for (const a of [-0.84, -0.28, 0.28, 0.84]) {
470
+ const c = polar(a, 0.9);
471
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: 0.14, ry: 0.07, rot: a + 0.5, fill: "#7cc76a", stroke: p.outline, lw: ow * 0.7 });
472
+ }
473
+ ;
474
+ [-1.12, -0.56, 0, 0.56, 1.12].forEach((a, i) => {
475
+ const c = polar(a, 0.9);
476
+ const r = i === 2 ? 0.13 : 0.11;
477
+ for (let k = 0; k < 5; k++) {
478
+ const t = a + k / 5 * Math.PI * 2;
479
+ out.push({ k: "circle", x: c.x + Math.sin(t) * r, y: c.y - Math.cos(t) * r, r: r * 0.82, fill: cols[i % cols.length], stroke: p.outline, lw: ow * 0.7 });
480
+ }
481
+ out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.62, fill: cols[i % cols.length] });
482
+ out.push({ k: "circle", x: c.x, y: c.y, r: r * 0.42, fill: p.accent });
483
+ });
484
+ }
411
485
  if (has("helmet")) {
412
486
  for (const s of SIDES) out.push({ k: "rect", x: s * 1, y: 0.05, w: 0.26, h: 0.56, r: 0.12, fill: p.accent, ...ol });
413
487
  out.push({ k: "path", d: "M-0.62 -0.62 Q-0.3 -0.92 0.12 -0.9", stroke: WHITE, lw: 0.1, alpha: 0.9 });
@@ -472,6 +546,8 @@ function headShape(def, ol) {
472
546
  return { k: "rect", x: 0, y: 0, w: 2, h: 1.8, r: 0.46, fill, ...ol };
473
547
  case "bucket":
474
548
  return { k: "path", d: roundedPolygon(pts(-0.94, 0.96, -0.98, -0.36, -0.62, -1.04, 0.62, -1.04, 0.98, -0.36, 0.94, 0.96), 0.42), fill, ...ol };
549
+ case "snout-long":
550
+ return { k: "path", d: smoothClosed(SNOUT_HEAD, 0.95), fill, ...ol };
475
551
  case "bulb":
476
552
  return {
477
553
  k: "path",
@@ -490,6 +566,19 @@ function headShape(def, ol) {
490
566
  };
491
567
  }
492
568
  }
569
+ var SNOUT_HEAD = pts(0.7, -0.88, 0.12, -1.04, -0.3, -0.98, -0.72, -0.74, -1.2, -0.56, -1.5, -0.32, -1.6, 0.04, -1.44, 0.42, -0.9, 0.72, -0.2, 0.92, 0.5, 0.88, 0.96, 0.56, 1.08, 0, 0.98, -0.5);
570
+ function eyeSpots(def, ex, ey) {
571
+ if (def.head === "snout-long")
572
+ return [
573
+ { x: -0.36, y: -0.5, k: 0.95 },
574
+ { x: 0.26, y: -0.48, k: 1.1 }
575
+ ];
576
+ return SIDES.map((s) => ({ x: s * ex, y: ey, k: 1 }));
577
+ }
578
+ function cheekSpots(def) {
579
+ if (def.head === "snout-long") return pts(-0.66, -0.14, 0.66, 0.1);
580
+ return SIDES.map((s) => ({ x: s * 0.64, y: 0.3 }));
581
+ }
493
582
  function headEllipse(def) {
494
583
  if (def.head === "round") return { cy: 0, rx: 1, ry: 1 };
495
584
  if (def.head === "tall") return { cy: -0.02, rx: 0.94, ry: 1.04 };
@@ -570,6 +659,10 @@ function ears(def, ow) {
570
659
  out.push({ k: "ellipse", x: s * 1.04, y: -0.22, rx: 0.4, ry: 0.18, rot: s * 0.32, fill: p.skin ?? p.body, ...ol });
571
660
  out.push({ k: "ellipse", x: s * 1.08, y: -0.2, rx: 0.24, ry: 0.08, rot: s * 0.32, fill: p.accent });
572
661
  break;
662
+ case "pixie":
663
+ out.push({ k: "path", d: roundedPolygon(pts(s * 0.86, -0.22, s * 1.36, -0.5, s * 1.02, 0.26), 0.28), fill: p.skin ?? p.body, ...ol });
664
+ out.push({ k: "line", pts: [s * 0.98, -0.12, s * 1.16, -0.3], stroke: p.accent, lw: ow * 0.8, alpha: 0.5 });
665
+ break;
573
666
  case "human":
574
667
  out.push({ k: "ellipse", x: s * 0.98, y: 0.06, rx: 0.17, ry: 0.23, fill: p.skin ?? p.body, ...ol });
575
668
  break;
@@ -602,15 +695,15 @@ function eyes(def, ow) {
602
695
  const shine = (x, y, r) => ({ k: "circle", x, y, r, fill: WHITE });
603
696
  switch (f2.eyes) {
604
697
  case "dot":
605
- for (const s of SIDES) {
606
- out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.12, ry: 0.155, fill: ink });
607
- out.push(shine(s * ex + 0.04, ey - 0.06, 0.045));
698
+ for (const e of eyeSpots(def, ex, ey)) {
699
+ out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.12 * e.k, ry: 0.155 * e.k, fill: ink });
700
+ out.push(shine(e.x + 0.04 * e.k, e.y - 0.06 * e.k, 0.045 * e.k));
608
701
  }
609
702
  break;
610
703
  case "bead":
611
- for (const s of SIDES) {
612
- out.push({ k: "ellipse", x: s * ex, y: ey, rx: 0.16, ry: 0.22, fill: ink });
613
- out.push(shine(s * ex + 0.05, ey - 0.08, 0.065));
704
+ for (const e of eyeSpots(def, ex, ey)) {
705
+ out.push({ k: "ellipse", x: e.x, y: e.y, rx: 0.16 * e.k, ry: 0.22 * e.k, fill: ink });
706
+ out.push(shine(e.x + 0.05 * e.k, e.y - 0.08 * e.k, 0.065 * e.k));
614
707
  }
615
708
  break;
616
709
  case "big":
@@ -719,6 +812,21 @@ function mouth(def, ow) {
719
812
  out.push({ k: "ellipse", x: 0, y: 0.48, rx: 0.16, ry: 0.11, fill: MOUTH });
720
813
  out.push({ k: "path", d: "M-0.3 0.12 Q0 -0.02 0.3 0.12 Q0.36 0.52 0.04 0.76 Q0.08 0.52 -0.08 0.46 Q-0.28 0.38 -0.3 0.12 Z", fill: p.accent, stroke: p.outline, lw: ow * 0.8 });
721
814
  break;
815
+ case "toothy": {
816
+ out.push({ k: "path", d: "M-1.4 0.1 Q-0.5 0.36 0.5 0.2 Q0.64 0.16 0.68 0.02 Q0.78 0.28 0.52 0.38 Q-0.4 0.78 -1.3 0.26 Z", fill: dark, stroke: dark, lw: ow * 0.6 });
817
+ out.push({ k: "ellipse", x: -0.38, y: 0.5, rx: 0.24, ry: 0.08, fill: TONGUE });
818
+ for (const t of [0.12, 0.27, 0.42, 0.57, 0.72, 0.86]) {
819
+ const x = (1 - t) * (1 - t) * -1.4 + 2 * (1 - t) * t * -0.5 + t * t * 0.5;
820
+ const y = (1 - t) * (1 - t) * 0.1 + 2 * (1 - t) * t * 0.36 + t * t * 0.2 - 0.01;
821
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.08, y, x + 0.08, y, x, y + 0.14), 0.3), fill: WHITE });
822
+ }
823
+ for (const t of [0.3, 0.7]) {
824
+ const x = (1 - t) * (1 - t) * 0.52 + 2 * (1 - t) * t * -0.4 + t * t * -1.3;
825
+ const y = (1 - t) * (1 - t) * 0.38 + 2 * (1 - t) * t * 0.78 + t * t * 0.26 + 0.01;
826
+ out.push({ k: "path", d: roundedPolygon(pts(x - 0.07, y, x + 0.07, y, x, y - 0.12), 0.3), fill: WHITE });
827
+ }
828
+ break;
829
+ }
722
830
  case "jaws": {
723
831
  out.push({ k: "path", d: "M-0.5 0.3 Q0 0.5 0.5 0.3 Q0.38 0.72 0 0.72 Q-0.38 0.72 -0.5 0.3 Z", fill: dark, stroke: dark, lw: ow * 0.5 });
724
832
  for (const x of [-0.34, -0.17, 0, 0.17, 0.34]) {
@@ -741,7 +849,10 @@ var BAND_BODY = {
741
849
  "9-12": { head: 0.5, neck: 0.92, arm: 0.17, leg: 0.21, hand: 0.125, foot: 0.9, shoulderMin: 0.36, shoulderPad: 0.07, hipMin: 0.32, hipPad: 0.08, bottom: 0.1, bulge: 1, armsOverHead: false }
742
850
  };
743
851
  var DEFAULT_BAND = "6-8";
744
- var bodyBand = (def) => def.band in BAND_BODY ? def.band : DEFAULT_BAND;
852
+ var bodyBand = (def, shown) => {
853
+ if (shown !== void 0 && def.band !== "all" && def.alsoIn?.includes(shown) && shown in BAND_BODY) return shown;
854
+ return def.band in BAND_BODY ? def.band : DEFAULT_BAND;
855
+ };
745
856
  var HEAD = BAND_BODY[DEFAULT_BAND].head;
746
857
  var MAX_TILT = 0.3;
747
858
  var TILT_DEAD_ZONE = 0.12;
@@ -835,7 +946,7 @@ function headTilt(body, at, angle, U) {
835
946
  return 0;
836
947
  }
837
948
  function avatarShapes(body, def, box, opts = {}) {
838
- const band = bodyBand(def);
949
+ const band = bodyBand(def, opts.band);
839
950
  const bb = BAND_BODY[band];
840
951
  const F = frameOf(body, box, { ...opts, band });
841
952
  if (!F) return [];
@@ -867,8 +978,8 @@ function avatarShapes(body, def, box, opts = {}) {
867
978
  out.push({ k: "line", pts: flat, stroke: p.outline, lw: w + ow * 2 });
868
979
  out.push({ k: "line", pts: flat, stroke: color, lw: w });
869
980
  };
870
- const legW = bb.leg * U;
871
- const armW = bb.arm * U;
981
+ const legW = bb.leg * U * (has("strong-legs") ? 1.3 : 1);
982
+ const armW = bb.arm * U * (has("tiny-arms") ? 0.6 : 1);
872
983
  const pad = Math.max(bb.arm / 0.22, 0.9);
873
984
  if (F.hipsOk) {
874
985
  for (const [hip, kneeI, ankleI, side] of [
@@ -882,7 +993,7 @@ function avatarShapes(body, def, box, opts = {}) {
882
993
  const pts2 = ankle.ok ? [top, knee, ankle] : [top, knee];
883
994
  limb(legs, pts2, legW, p.limbs);
884
995
  if (has("joints")) legs.push({ k: "circle", x: knee.x, y: knee.y, r: 0.15 * U * pad, fill: p.accent, ...ol });
885
- if (ankle.ok) legs.push(foot(def, ankle, F, side, ow, bb.foot));
996
+ if (ankle.ok) legs.push(...feet(def, ankle, F, side, ow, bb.foot));
886
997
  }
887
998
  }
888
999
  for (const [sh, elbowI, wristI, side] of [
@@ -915,7 +1026,20 @@ function avatarShapes(body, def, box, opts = {}) {
915
1026
  out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.2 * U, ry: 0.09 * U, rot: Math.atan2(fd.y, fd.x), fill: p.hands ?? p.accent, ...ol });
916
1027
  }
917
1028
  }
918
- out.push({ k: "circle", x: hand.x, y: hand.y, r: bb.hand * U, fill: p.hands ?? p.limbs, ...ol });
1029
+ if (has("wand") && side === 1 && !opts.portrait) out.push(...wand(def, hand, norm(add(mul(dir, 0.4), sub(mul(r, 0.6), d))), U, ow));
1030
+ const handR = bb.hand * U * (has("tiny-arms") ? 0.72 : 1);
1031
+ if (has("tiny-arms")) {
1032
+ for (const a of [-0.45, 0.45]) {
1033
+ const c = Math.cos(a);
1034
+ const s = Math.sin(a);
1035
+ const cd = { x: dir.x * c - dir.y * s, y: dir.x * s + dir.y * c };
1036
+ const base = add(hand, mul(cd, handR * 0.75));
1037
+ const tip = add(hand, mul(cd, handR * 1.6));
1038
+ const nrm = { x: -cd.y * handR * 0.32, y: cd.x * handR * 0.32 };
1039
+ out.push({ k: "path", d: roundedPolygon([add(base, nrm), tip, sub(base, nrm)], 0.2), fill: p.belly, ...ol });
1040
+ }
1041
+ }
1042
+ out.push({ k: "circle", x: hand.x, y: hand.y, r: handR, fill: p.hands ?? p.limbs, ...ol });
919
1043
  }
920
1044
  if (has("wings")) {
921
1045
  for (const s of [-1, 1]) {
@@ -930,6 +1054,7 @@ function avatarShapes(body, def, box, opts = {}) {
930
1054
  back.push({ k: "path", d: roundedPolygon(w, 0.18), fill: p.accent, ...ol });
931
1055
  }
932
1056
  }
1057
+ if (has("wings-fairy")) back.push(...fairyWings(def, T, ht, U, ow));
933
1058
  if (has("cape")) {
934
1059
  const yb = L + 0.8 * U;
935
1060
  const wb = hb + 0.36 * U;
@@ -1147,6 +1272,7 @@ function avatarShapes(body, def, box, opts = {}) {
1147
1272
  const k = 0.15 * U;
1148
1273
  torso.push({ k: "group", x: c.x, y: c.y, rot: F.angle - 0.2, s: k, items: [{ k: "path", d: STAR, fill: def.extraColors?.[0] ?? "#ffd84a", stroke: p.outline, lw: ow * 0.8 / k }] });
1149
1274
  }
1275
+ if (has("petals")) torso.push(...petals(def, T, ht, hb, bulge, L, y1, U, ow, bb));
1150
1276
  if (has("scarf")) {
1151
1277
  const y = Math.max(0.04, bb.head * (1 - bb.neck) - 0.06) * U;
1152
1278
  const c = T(0, y);
@@ -1171,6 +1297,18 @@ function cachedHead(def, ow) {
1171
1297
  }
1172
1298
  return shapes;
1173
1299
  }
1300
+ function feet(def, ankle, F, side, ow, k) {
1301
+ const shoe = foot(def, ankle, F, side, ow, k);
1302
+ if (!def.bodyExtras.includes("strong-legs")) return [shoe];
1303
+ const U = F.U * k;
1304
+ const c = add(add(ankle, mul(F.d, 0.07 * U)), mul(F.r, side * 0.06 * U));
1305
+ const out = [shoe];
1306
+ for (const t of [-1, 0, 1]) {
1307
+ const q = add(add(c, mul(F.r, t * 0.12 * U)), mul(F.d, 0.09 * U));
1308
+ out.push({ k: "ellipse", x: q.x, y: q.y, rx: 0.05 * U, ry: 0.045 * U, rot: F.angle, fill: def.palette.belly, stroke: def.palette.outline, lw: ow * 0.8 });
1309
+ }
1310
+ return out;
1311
+ }
1174
1312
  function foot(def, ankle, F, side, ow, k) {
1175
1313
  const p = def.palette;
1176
1314
  const { d, r } = F;
@@ -1324,6 +1462,30 @@ function tail(def, F, hb, ow) {
1324
1462
  });
1325
1463
  break;
1326
1464
  }
1465
+ case "thick": {
1466
+ const pts2 = TP([
1467
+ [hb * 0.3, L * 0.42],
1468
+ [hb + 0.42 * U, L * 0.62],
1469
+ [hb + 0.92 * U, L * 0.96],
1470
+ [hb + 1.32 * U, L * 1.08],
1471
+ [hb + 1.44 * U, L * 0.98],
1472
+ [hb + 0.98 * U, L * 1.2],
1473
+ [hb + 0.4 * U, L * 1.22],
1474
+ [hb * 0.3, L * 1.12]
1475
+ ]);
1476
+ out.push({ k: "path", d: smoothClosed(pts2, 0.8), fill: p.body, ...ol });
1477
+ const c = def.extraColors?.[0] ?? p.accent;
1478
+ for (const [x, y, l] of [
1479
+ [0.28, 0.58, 0.26],
1480
+ [0.62, 0.8, 0.22],
1481
+ [0.96, 1, 0.16]
1482
+ ]) {
1483
+ const a = T(hb + x * U, L * y + 0.03 * U);
1484
+ const b = T(hb + (x - 0.1) * U, L * y + l * U);
1485
+ out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: c, lw: 0.09 * U });
1486
+ }
1487
+ break;
1488
+ }
1327
1489
  case "spiky":
1328
1490
  case "arrow": {
1329
1491
  const pts2 = TP([
@@ -1356,6 +1518,81 @@ function tail(def, F, hb, ow) {
1356
1518
  }
1357
1519
  return out;
1358
1520
  }
1521
+ function wand(def, hand, wd, U, ow) {
1522
+ const p = def.palette;
1523
+ const from = add(hand, mul(wd, -0.08 * U));
1524
+ const tip = add(hand, mul(wd, 0.52 * U));
1525
+ const k = 0.2 * U;
1526
+ return [
1527
+ { k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: p.outline, lw: 0.06 * U + ow * 2 },
1528
+ { k: "line", pts: [from.x, from.y, tip.x, tip.y], stroke: "#fff3d6", lw: 0.06 * U },
1529
+ { k: "circle", x: tip.x, y: tip.y, r: 0.24 * U, fill: p.accent, alpha: 0.28 },
1530
+ { k: "group", x: tip.x, y: tip.y, rot: Math.atan2(wd.y, wd.x) + Math.PI / 2, s: k, items: [{ k: "path", d: STAR, fill: p.accent, stroke: p.outline, lw: ow * 0.9 / k }] }
1531
+ ];
1532
+ }
1533
+ function fairyWings(def, T, ht, U, ow) {
1534
+ const p = def.palette;
1535
+ const fill = def.extraColors?.[0] ?? "#cdeeff";
1536
+ const out = [];
1537
+ for (const s of [-1, 1]) {
1538
+ const lobes = [
1539
+ // Upper: big, up and out.
1540
+ [
1541
+ [s * ht * 0.3, 0.12 * U],
1542
+ [s * (ht + 0.42 * U), -0.66 * U],
1543
+ [s * (ht + 1 * U), -0.74 * U],
1544
+ [s * (ht + 1.02 * U), -0.18 * U],
1545
+ [s * (ht + 0.5 * U), 0.26 * U]
1546
+ ],
1547
+ // Lower: smaller, down and out.
1548
+ [
1549
+ [s * ht * 0.4, 0.3 * U],
1550
+ [s * (ht + 0.6 * U), 0.36 * U],
1551
+ [s * (ht + 0.74 * U), 0.78 * U],
1552
+ [s * (ht + 0.3 * U), 0.9 * U]
1553
+ ]
1554
+ ];
1555
+ for (const lobe of lobes) {
1556
+ const d = smoothClosed(lobe.map(([x, y]) => T(x, y)), 0.9);
1557
+ out.push({ k: "path", d, fill, alpha: 0.6 });
1558
+ out.push({ k: "path", d, stroke: p.outline, lw: ow * 0.9 });
1559
+ const inner = lobe[Math.floor(lobe.length / 2)];
1560
+ const a = T(lobe[0][0], lobe[0][1]);
1561
+ const b = T(lobe[0][0] + (inner[0] - lobe[0][0]) * 0.75, lobe[0][1] + (inner[1] - lobe[0][1]) * 0.75);
1562
+ out.push({ k: "line", pts: [a.x, a.y, b.x, b.y], stroke: "#ffffff", lw: ow * 0.9, alpha: 0.85 });
1563
+ }
1564
+ for (const [x, y, r] of [
1565
+ [ht + 0.72 * U, -0.42 * U, 0.075 * U],
1566
+ [ht + 0.44 * U, -0.16 * U, 0.05 * U],
1567
+ [ht + 0.5 * U, 0.6 * U, 0.055 * U]
1568
+ ]) {
1569
+ const q = T(s * x, y);
1570
+ out.push({ k: "group", x: q.x, y: q.y, s: r, items: [{ k: "path", d: STAR, fill: "#ffffff" }] });
1571
+ }
1572
+ }
1573
+ return out;
1574
+ }
1575
+ function petals(def, T, ht, hb, bulge, L, y1, U, ow, bb) {
1576
+ const p = def.palette;
1577
+ const second = def.extraColors?.[1] ?? p.belly;
1578
+ const out = [];
1579
+ const ang = Math.atan2(T(1, 0).y - T(0, 0).y, T(1, 0).x - T(0, 0).x);
1580
+ const n = 5;
1581
+ const w = Math.max(hb, bulge * 0.92) * 1.08;
1582
+ const y = (L * 0.8 + y1) / 2;
1583
+ const order = [0, 4, 1, 3, 2];
1584
+ for (const i of order) {
1585
+ const t = i / (n - 1) - 0.5;
1586
+ const c = T(t * 2 * w, y + (0.5 - Math.abs(t)) * 0.1 * U);
1587
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: w * 0.3, ry: 0.34 * U, rot: ang - t * 0.6, fill: i % 2 ? second : p.body, stroke: p.outline, lw: ow });
1588
+ }
1589
+ const cy = Math.max(0.04, bb.head * (1 - bb.neck) - 0.04) * U;
1590
+ for (const t of [-1, 1, 0]) {
1591
+ const c = T(t * ht * 0.32, cy + (t ? 0 : 0.05 * U));
1592
+ out.push({ k: "ellipse", x: c.x, y: c.y, rx: ht * 0.26, ry: 0.12 * U, rot: ang + t * 0.5, fill: t ? second : p.body, stroke: p.outline, lw: ow * 0.9 });
1593
+ }
1594
+ return out;
1595
+ }
1359
1596
 
1360
1597
  // ../avatars/src/defs.ts
1361
1598
  var SOFT = "#4a382c";
@@ -1472,6 +1709,7 @@ var AVATARS = [
1472
1709
  id: "lion",
1473
1710
  name: { en: "Lion", it: "Leoncino" },
1474
1711
  band: "6-8",
1712
+ alsoIn: ["3-5"],
1475
1713
  palette: { body: "#f6c454", limbs: "#f6c454", belly: "#fde7b0", accent: "#fde7b0", outline: INK },
1476
1714
  head: "round",
1477
1715
  ears: "round",
@@ -1838,6 +2076,39 @@ var AVATARS = [
1838
2076
  headExtras: ["hair", "domino"],
1839
2077
  bodyExtras: ["cape", "emblem", "boots"],
1840
2078
  extraColors: ["#ffd84a"]
2079
+ },
2080
+ // ── In both 3–5 and 6–8 (home 6–8, alsoIn 3–5: drawn with the proportions of the band shown) ──
2081
+ {
2082
+ // Next to the round 'dinosaur': a T-rex at a glance, a big head turned three-quarters with a long snout and a
2083
+ // toothy grin, tiny arms, strong legs, a thick striped tail.
2084
+ id: "trex",
2085
+ name: { en: "T-Rex", it: "T-Rex" },
2086
+ band: "6-8",
2087
+ alsoIn: ["3-5"],
2088
+ palette: { body: "#6cb84e", limbs: "#6cb84e", belly: "#f4e8a8", accent: "#f4e8a8", outline: INK },
2089
+ head: "snout-long",
2090
+ ears: "none",
2091
+ face: { eyes: "dot", mouth: "toothy", muzzle: "none", cheeks: "#f59b8f" },
2092
+ tail: "thick",
2093
+ headExtras: ["stripes"],
2094
+ bodyExtras: ["tiny-arms", "strong-legs"],
2095
+ extraColors: ["#4a9235", "#58a23f"]
2096
+ },
2097
+ {
2098
+ // An original child fairy: a bun with a flower crown, little pointed ears, a petal tunic, see-through wings and
2099
+ // a star wand in one hand.
2100
+ id: "fairy",
2101
+ name: { en: "Fairy", it: "Fatina" },
2102
+ band: "6-8",
2103
+ alsoIn: ["3-5"],
2104
+ palette: { body: "#b99af0", limbs: "#a8693f", belly: "#b99af0", accent: "#ffd84a", outline: INK, skin: "#a8693f", hair: "#2e1d17", hands: "#a8693f", feet: "#f58cbc" },
2105
+ head: "round",
2106
+ ears: "pixie",
2107
+ face: { eyes: "dot", mouth: "smile", muzzle: "none", cheeks: "#f0857a" },
2108
+ tail: "none",
2109
+ headExtras: ["bun", "flower-crown"],
2110
+ bodyExtras: ["wings-fairy", "petals", "wand"],
2111
+ extraColors: ["#c4ecff", "#f7a8cf", "#fff3a0", "#ffffff"]
1841
2112
  }
1842
2113
  ];
1843
2114
 
@@ -1983,7 +2254,7 @@ var isAvatarId = (v) => typeof v === "string" && BY_ID.has(v);
1983
2254
  var avatarById = (id) => BY_ID.get(id);
1984
2255
  var resolve = (def) => typeof def === "string" ? BY_ID.get(def) ?? DEFAULT_AVATAR : def;
1985
2256
  function drawAvatar(ctx, body, def, box, opts = {}) {
1986
- const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored });
2257
+ const shapes = avatarShapes(body, resolve(def), box, { mirrored: opts.mirrored, band: opts.band });
1987
2258
  if (!shapes.length) return false;
1988
2259
  ctx.save();
1989
2260
  if (opts.alpha !== void 0) ctx.globalAlpha *= Math.min(Math.max(opts.alpha, 0), 1);
@@ -1991,17 +2262,19 @@ function drawAvatar(ctx, body, def, box, opts = {}) {
1991
2262
  ctx.restore();
1992
2263
  return true;
1993
2264
  }
1994
- function iconShapes(def, size) {
2265
+ function iconShapes(def, size, band) {
1995
2266
  const d = resolve(def);
1996
2267
  const body = neutralPose();
1997
- const band = bodyBand(d);
1998
- const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band });
1999
- const top = -bounds(headShapes(d, 0.05)).minY;
2268
+ const shown = bodyBand(d, band);
2269
+ const unit = frameOf(body, { x: 0, y: 0, w: 1, h: 1 }, { band: shown });
2270
+ const hb = bounds(headShapes(d, 0.05));
2271
+ const top = -hb.minY;
2000
2272
  const headR = Math.min(0.27, 0.43 / Math.max(top, 1)) * size;
2001
- const k = headR / BAND_BODY[band].head / unit.U;
2273
+ const dx = d.head === "snout-long" ? (hb.minX + hb.maxX) / 2 * headR : 0;
2274
+ const k = headR / BAND_BODY[shown].head / unit.U;
2002
2275
  const cy = 0.5 * size + Math.min(0, (top - 1.6) * 0.06 * size);
2003
- const box = { x: size / 2 - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2004
- return avatarShapes(body, d, box, { portrait: true });
2276
+ const box = { x: size / 2 - dx - unit.head.x * k, y: cy - unit.head.y * k, w: k, h: k };
2277
+ return avatarShapes(body, d, box, { portrait: true, band: shown });
2005
2278
  }
2006
2279
  function smoothBody(prev, next, amount = 0.5) {
2007
2280
  if (!prev || prev.length !== next.length) return next.slice();
@@ -2102,6 +2375,10 @@ function bounds(shapes) {
2102
2375
 
2103
2376
  export {
2104
2377
  __publicField,
2378
+ MAX_PICTURE_SIDE,
2379
+ PICTURE_JPEG_QUALITY,
2380
+ checkPicture,
2381
+ parsePictureTitle,
2105
2382
  PROTOCOL_VERSION,
2106
2383
  defaultAvatarFor,
2107
2384
  NS,
@@ -2110,6 +2387,8 @@ export {
2110
2387
  toScreen,
2111
2388
  parseDeviceHint,
2112
2389
  parseGameStats,
2390
+ parseCursor,
2391
+ parsePictureResult,
2113
2392
  parseTurnPlayers,
2114
2393
  parseTurnOrder,
2115
2394
  drawShapes,