@idle-screens/saver-metaquarium 0.3.0 → 0.4.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.
@@ -1,3 +1,152 @@
1
+ // src/environments.ts
2
+ var FLOOR_KINDS = ["flat", "dunes", "ridges", "basin"];
3
+ var ENVIRONMENTS = [
4
+ { name: "void", label: "Void", water: null, floor: "flat", rays: null },
5
+ {
6
+ name: "abyss",
7
+ label: "Abyss",
8
+ water: null,
9
+ floor: "basin",
10
+ rays: { strength: 0.35, color: "#0e4a6e", y: -620 }
11
+ },
12
+ {
13
+ name: "reef",
14
+ label: "Reef",
15
+ water: { y: 150, color: "#2ad4ff", opacity: 0.18 },
16
+ floor: "dunes",
17
+ rays: { strength: 0.5, color: "#bfefff", y: 900 }
18
+ },
19
+ {
20
+ name: "kelp",
21
+ label: "Kelp forest",
22
+ water: { y: 170, color: "#1e6f5c", opacity: 0.2 },
23
+ floor: "ridges",
24
+ rays: { strength: 0.4, color: "#9ef5d0", y: 820 }
25
+ },
26
+ {
27
+ name: "ice",
28
+ label: "Under ice",
29
+ water: { y: 132, color: "#cfefff", opacity: 0.32 },
30
+ floor: "flat",
31
+ rays: { strength: 0.6, color: "#eaf8ff", y: 1e3 }
32
+ },
33
+ {
34
+ name: "vent",
35
+ label: "Hydrothermal vent",
36
+ water: null,
37
+ floor: "ridges",
38
+ rays: { strength: 0.45, color: "#ff7a3c", y: -520 }
39
+ },
40
+ {
41
+ name: "lagoon",
42
+ label: "Lagoon",
43
+ water: { y: 118, color: "#7ff3d0", opacity: 0.22 },
44
+ floor: "dunes",
45
+ rays: { strength: 0.55, color: "#fff3b0", y: 950 }
46
+ },
47
+ {
48
+ name: "universe",
49
+ label: "Universe",
50
+ water: null,
51
+ floor: "ridges",
52
+ rays: { strength: 0.3, color: "#c39bff", y: 1200 }
53
+ }
54
+ ];
55
+ var ENVIRONMENT_NAMES = ENVIRONMENTS.map((e) => e.name);
56
+ function environmentOf(name) {
57
+ return ENVIRONMENTS.find((e) => e.name === name) ?? ENVIRONMENTS[0];
58
+ }
59
+ var LAYER_COST = { floor: 0, water: 1, rays: 2 };
60
+ var RAY_COUNT = { full: 5, reduced: 2 };
61
+ function affordableLayers(budget, preset) {
62
+ let left = budget;
63
+ const water = preset.water !== null && left >= LAYER_COST.water;
64
+ if (water) left -= LAYER_COST.water;
65
+ let rayCount = 0;
66
+ if (preset.rays !== null && left > 0) {
67
+ rayCount = left >= LAYER_COST.rays ? RAY_COUNT.full : RAY_COUNT.reduced;
68
+ }
69
+ return { floor: true, water, rayCount };
70
+ }
71
+
72
+ // src/swim.ts
73
+ var SWIM_STYLES = [
74
+ { name: "loop", label: "Loop", speedMul: 1, band: "free", bobAmp: 0, bobHz: 0, formation: false, travel: 1 },
75
+ { name: "school", label: "School", speedMul: 1, band: "free", bobAmp: 1.5, bobHz: 0.5, formation: true, travel: 1 },
76
+ { name: "drift", label: "Drift", speedMul: 0.35, band: "mid", bobAmp: 5, bobHz: 0.13, formation: false, travel: 0.6 },
77
+ { name: "hover", label: "Hover", speedMul: 0.2, band: "free", bobAmp: 7, bobHz: 0.3, formation: false, travel: 0.12 },
78
+ { name: "patrol", label: "Patrol", speedMul: 0.55, band: "mid", bobAmp: 1, bobHz: 0.09, formation: false, travel: 1 },
79
+ { name: "bottom", label: "Bottom-hugger", speedMul: 0.7, band: "floor", bobAmp: 2, bobHz: 0.4, formation: false, travel: 1 },
80
+ { name: "surface", label: "Surface-skimmer", speedMul: 0.9, band: "ceiling", bobAmp: 3, bobHz: 0.55, formation: false, travel: 1 }
81
+ ];
82
+ var SWIM_STYLE_NAMES = SWIM_STYLES.map((s) => s.name);
83
+ function swimStyleOf(name) {
84
+ return SWIM_STYLES.find((s) => s.name === name) ?? SWIM_STYLES[0];
85
+ }
86
+ function fishHash(index, salt) {
87
+ const x = Math.sin((index + 1) * 127.1 + salt * 311.7) * 43758.5453;
88
+ return x - Math.floor(x);
89
+ }
90
+ function fishVariation(index, variance) {
91
+ const v = Math.max(0, Math.min(1, variance));
92
+ return {
93
+ // ±40% speed and ±25% size at full variance — enough to read as a mixed
94
+ // population, not so much that one fish looks broken.
95
+ speedMul: 1 + (fishHash(index, 1) - 0.5) * 0.8 * v,
96
+ scaleMul: 1 + (fishHash(index, 2) - 0.5) * 0.5 * v,
97
+ // Independent of `variance` — see the note above; synchronised bobbing is
98
+ // a machine, not a shoal.
99
+ phase: fishHash(index, 3) * Math.PI * 2,
100
+ // Where along its own route a fish holds station, 0..1. Styles that only
101
+ // work a patch of water (hover, drift) start every fish at distance 0
102
+ // without this, so the whole shoal knots up in one corner — which is
103
+ // exactly what it did the first time I looked at `hover` on screen.
104
+ // Independent of `variance`: spreading is correctness, not flavour.
105
+ anchor: fishHash(index, 7)
106
+ };
107
+ }
108
+ var FISH_LENGTH = 18;
109
+ var FORMATION_HALF_WIDTH = 62;
110
+ function formationSlot(index, count, variance, halfWidth = FORMATION_HALF_WIDTH) {
111
+ const cols = Math.max(1, Math.ceil(Math.sqrt(Math.max(1, count))));
112
+ const col = index % cols;
113
+ const row = Math.floor(index / cols);
114
+ const j = 0.35 + 0.65 * Math.max(0, Math.min(1, variance));
115
+ const span = Math.max(1, cols - 1);
116
+ const pitch = Math.min(FISH_LENGTH * 1.9, halfWidth * 2 / span);
117
+ const jit = pitch * 0.35;
118
+ return {
119
+ side: (col - (cols - 1) / 2) * pitch + (fishHash(index, 4) - 0.5) * jit * j,
120
+ up: (fishHash(index, 5) - 0.5) * FISH_LENGTH * j,
121
+ back: row * FISH_LENGTH * 1.6 + fishHash(index, 6) * FISH_LENGTH * 0.4 * j
122
+ };
123
+ }
124
+ function formationExtent(count, variance) {
125
+ let side = 0, up = 0, back = 0;
126
+ for (let i = 0; i < count; i += 1) {
127
+ const s = formationSlot(i, count, variance);
128
+ side = Math.max(side, Math.abs(s.side));
129
+ up = Math.max(up, Math.abs(s.up));
130
+ back = Math.max(back, Math.abs(s.back));
131
+ }
132
+ return { side, up, back };
133
+ }
134
+ function bandRange(band) {
135
+ switch (band) {
136
+ // Tight, because 0..0.3 of a 57-unit volume is nearly two body lengths of
137
+ // headroom — measured on screen it read as hovering in the lower third
138
+ // rather than hugging anything.
139
+ case "floor":
140
+ return { lo: 0, hi: 0.14 };
141
+ case "ceiling":
142
+ return { lo: 0.7, hi: 1 };
143
+ case "mid":
144
+ return { lo: 0.3, hi: 0.7 };
145
+ default:
146
+ return null;
147
+ }
148
+ }
149
+
1
150
  // src/asset-cids.ts
2
151
  var ASSET_CIDS = [
3
152
  "QmZCzKVLJ1MYRzaZEFyuMCVmrPR3urQYMFR9RWerJYpCEr",
@@ -744,7 +893,44 @@ var METAQUARIUM_PARAMS = {
744
893
  * Draco-compressed. Empty = the copy shipped beside this package (no CDN,
745
894
  * no network beyond your own host). Override when a host serves the
746
895
  * decoder from its own static path. */
747
- dracoPath: { type: "string", default: "", ease: "step" }
896
+ dracoPath: { type: "string", default: "", ease: "step" },
897
+ /** The ROOM: a named place rather than thirty numbers. `void` is exactly
898
+ * the pre-environment scene, so the default changes nothing. Each other
899
+ * value adds a water ceiling, a terrain silhouette and light shafts,
900
+ * tier-budgeted. Palette params (fog, floor colour, motes) stay yours —
901
+ * an environment never overrides them. */
902
+ environment: { type: "enum", default: "void", options: [...ENVIRONMENT_NAMES], ease: "step" },
903
+ /** Override the environment's terrain. `auto` follows the environment. */
904
+ floorKind: { type: "enum", default: "auto", options: ["auto", "flat", "dunes", "ridges", "basin"], ease: "step" },
905
+ /** Water-ceiling height. -1 = follow the environment; fish swim to y=72.
906
+ * STEP, not smooth: -1 is a sentinel, so a ramp from -1 to a real height
907
+ * passes through negatives that read as "auto" — the ceiling would jump
908
+ * rather than glide. A value you cannot interpolate through must not
909
+ * advertise that it can. */
910
+ waterY: { type: "number", default: -1, min: -1, max: 220, ease: "step" },
911
+ /** Light-shaft strength. -1 = follow the environment, 0 = off. Step for the
912
+ * same sentinel reason: ramping -1 → 0 would read as FULL strength until it
913
+ * snapped off. */
914
+ rayStrength: { type: "number", default: -1, min: -1, max: 1, ease: "step" },
915
+ /** How the fish move. `loop` is exactly the pre-style behaviour, so the
916
+ * default changes nothing. A small named set on purpose: a silhouette of
917
+ * movement you can name is one you can choose from. */
918
+ swimStyle: { type: "enum", default: "loop", options: [...SWIM_STYLE_NAMES], ease: "step" },
919
+ /** Per-fish spread: 0 a uniform shoal, 1 every fish visibly its own animal
920
+ * (±40% speed, ±25% size, own phase). The uniqueness dial — one number
921
+ * instead of per-fish values nobody wants to author. */
922
+ swimVariance: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
923
+ /** Procedural body yaw for models that carry NO animation clip — most of
924
+ * the breed library. Distance-driven like the tail beat, so it speeds up
925
+ * with the fish and stays frame-addressable. Clipped models ignore it:
926
+ * their own clip is the better animation.
927
+ *
928
+ * Defaults to 0, off, even though a rigidly gliding fish is the worse
929
+ * look. Every param this saver has added defaults to the previous
930
+ * behaviour, and a scene already on someone's wall should not start moving
931
+ * differently because a dependency was bumped. 0.3–0.4 is the recommended
932
+ * value for a clip-less cast — the studio swim variants all set it. */
933
+ bodyWiggle: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" }
748
934
  };
749
935
  var MIAMI_VICE_COLORS = [
750
936
  "#ff6ad5",
@@ -834,6 +1020,22 @@ function coerceNum(def, v) {
834
1020
  }
835
1021
 
836
1022
  export {
1023
+ FLOOR_KINDS,
1024
+ ENVIRONMENTS,
1025
+ ENVIRONMENT_NAMES,
1026
+ environmentOf,
1027
+ LAYER_COST,
1028
+ RAY_COUNT,
1029
+ affordableLayers,
1030
+ SWIM_STYLES,
1031
+ SWIM_STYLE_NAMES,
1032
+ swimStyleOf,
1033
+ fishHash,
1034
+ fishVariation,
1035
+ FISH_LENGTH,
1036
+ formationSlot,
1037
+ formationExtent,
1038
+ bandRange,
837
1039
  ASSET_CIDS,
838
1040
  TOTAL_SUPPLY,
839
1041
  isMintedId,
@@ -861,4 +1063,4 @@ export {
861
1063
  paramSpaceWith,
862
1064
  coerceNum
863
1065
  };
864
- //# sourceMappingURL=chunk-ZGYVELA4.js.map
1066
+ //# sourceMappingURL=chunk-BWIJDJPQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/environments.ts","../src/swim.ts","../src/asset-cids.ts","../src/farm.ts","../src/ipfs.ts","../src/manifest.ts"],"sourcesContent":["/**\n * Environments — the tank as a room.\n *\n * The original aquarium switches whole VENUES, not backdrops: turning its\n * EXHIBIT knob moves from open shader terrain under a water ceiling to an\n * indoor hall with walls and a striped floor. Our tank has one room — a disc\n * under fog — so the gap was never \"more knobs\", it was that there is no room.\n *\n * This is that room, as data. An `environment` names a PLACE; an agent picks\n * one instead of dialling thirty numbers, and the values below are the taste.\n * Design and the browser session behind it:\n * `idle-mono/docs/metaquarium-environment-spec.md`.\n *\n * Zero-dep on purpose — the server validates through `./manifest` without\n * pulling three.js.\n */\n\nexport type EnvironmentName =\n | 'void' | 'abyss' | 'reef' | 'kelp' | 'ice' | 'vent' | 'lagoon' | 'universe';\n\n/** Terrain silhouette. `flat` is the original disc; the rest are generated\n * from the mount seed — never fetched, so determinism and offline both hold. */\nexport type FloorKind = 'flat' | 'dunes' | 'ridges' | 'basin';\n\n/** Runtime guard for the same set — the steering lane is unvalidated, so a\n * bad value must fall back rather than build an invisible floor. */\nexport const FLOOR_KINDS: readonly FloorKind[] = ['flat', 'dunes', 'ridges', 'basin'];\n\n/** A translucent plane ABOVE the fish. The single strongest identity cue in\n * the original: it is what makes a scene read as *under* something. */\nexport interface WaterCeiling {\n /** World Y. The tank's fish swim between y=15 and y=72. */\n y: number;\n color: string;\n opacity: number;\n}\n\n/** Volumetric shafts. `y` may be NEGATIVE — the original's raysLightY swings\n * ±2000, and light from below is the difference between a sunlit surface and\n * an abyssal glow. */\nexport interface Rays {\n strength: number;\n color: string;\n y: number;\n}\n\nexport interface EnvironmentPreset {\n name: EnvironmentName;\n label: string;\n water: WaterCeiling | null;\n floor: FloorKind;\n rays: Rays | null;\n}\n\n/**\n * The catalogue. `void` is EXACTLY today's scene — no ceiling, flat floor, no\n * rays — so the default is a no-op and every already-published scene renders\n * unchanged. That is the whole reason the enum is safe to ship first.\n *\n * Deliberately NOT set here: fog, floor colour, motes. Those are already\n * steerable params, and an environment that quietly overrode them would make\n * \"did the author choose this, or did the preset?\" unanswerable. An\n * environment supplies the ROOM; the palette stays the author's.\n */\nexport const ENVIRONMENTS: readonly EnvironmentPreset[] = [\n { name: 'void', label: 'Void', water: null, floor: 'flat', rays: null },\n { name: 'abyss', label: 'Abyss', water: null, floor: 'basin',\n rays: { strength: 0.35, color: '#0e4a6e', y: -620 } },\n { name: 'reef', label: 'Reef', water: { y: 150, color: '#2ad4ff', opacity: 0.18 },\n floor: 'dunes', rays: { strength: 0.5, color: '#bfefff', y: 900 } },\n { name: 'kelp', label: 'Kelp forest', water: { y: 170, color: '#1e6f5c', opacity: 0.2 },\n floor: 'ridges', rays: { strength: 0.4, color: '#9ef5d0', y: 820 } },\n { name: 'ice', label: 'Under ice', water: { y: 132, color: '#cfefff', opacity: 0.32 },\n floor: 'flat', rays: { strength: 0.6, color: '#eaf8ff', y: 1000 } },\n { name: 'vent', label: 'Hydrothermal vent', water: null, floor: 'ridges',\n rays: { strength: 0.45, color: '#ff7a3c', y: -520 } },\n { name: 'lagoon', label: 'Lagoon', water: { y: 118, color: '#7ff3d0', opacity: 0.22 },\n floor: 'dunes', rays: { strength: 0.55, color: '#fff3b0', y: 950 } },\n { name: 'universe', label: 'Universe', water: null, floor: 'ridges',\n rays: { strength: 0.3, color: '#c39bff', y: 1200 } },\n];\n\nexport const ENVIRONMENT_NAMES: readonly EnvironmentName[] =\n ENVIRONMENTS.map((e) => e.name);\n\nexport function environmentOf(name: string): EnvironmentPreset {\n return ENVIRONMENTS.find((e) => e.name === name) ?? ENVIRONMENTS[0]!;\n}\n\n// ---------------------------------------------------------------------------\n// Cost budget\n// ---------------------------------------------------------------------------\n\n/**\n * What each layer costs, and what a tier can afford.\n *\n * The arch research asked for a cost model \"before rich scenes ship\", and this\n * is that, scoped to one saver. The floor is free because it is generated once\n * at mount and never touched per frame; the ceiling is one extra draw call;\n * rays are the only per-frame work worth rationing.\n */\nexport const LAYER_COST = { floor: 0, water: 1, rays: 2 } as const;\n\n/** Shafts at full and at reduced count. A rays-only place keeps its identity\n * on a weak device by running fewer shafts rather than none. */\nexport const RAY_COUNT = { full: 5, reduced: 2 } as const;\n\n/**\n * Layers in priority order until the budget runs out: a device that can only\n * afford one thing keeps the ceiling, the cue that carries most of the read.\n *\n * But `abyss`, `vent` and `universe` have NO ceiling — their whole identity is\n * the glow — so spending ceiling-first would silently leave them as bare\n * terrain on exactly the devices that most need a recognisable scene. When\n * nothing higher-priority claims the budget, rays run at a reduced shaft count\n * instead of not at all.\n */\nexport function affordableLayers(budget: number, preset: EnvironmentPreset): {\n floor: boolean; water: boolean; rayCount: number;\n} {\n let left = budget;\n const water = preset.water !== null && left >= LAYER_COST.water;\n if (water) left -= LAYER_COST.water;\n let rayCount = 0;\n if (preset.rays !== null && left > 0) {\n rayCount = left >= LAYER_COST.rays ? RAY_COUNT.full : RAY_COUNT.reduced;\n }\n return { floor: true, water, rayCount };\n}\n","/**\n * Swim styles — a few places a fish can be, not a pile of motion knobs.\n *\n * Same shape as environments, for the same reason: a curated set is how you\n * ship taste, and an agent picks a BEHAVIOUR rather than tuning six numbers\n * until it looks accidental. The uniqueness lives in two dials\n * (`swimVariance`, `bodyWiggle`) that spread a school out without anyone\n * having to author per-fish values.\n *\n * Everything here is pure and closed-form in distance/time, so\n * `renderFrame(t, seed)` stays frame-addressable — a style that needed\n * accumulated state would not be shippable at all.\n *\n * Zero-dep: the server validates through `./manifest` without three.js.\n */\n\nexport type SwimStyle =\n | 'loop' | 'school' | 'drift' | 'hover' | 'patrol' | 'bottom' | 'surface';\n\n/** Where in the water column a style lives. */\nexport type DepthBand = 'free' | 'floor' | 'ceiling' | 'mid';\n\nexport interface SwimStyleSpec {\n name: SwimStyle;\n label: string;\n /** Tempo baked into the style — a patrolling shark is not a darting shoal.\n * Multiplies the author's swimSpeed rather than replacing it. */\n speedMul: number;\n band: DepthBand;\n /** Vertical bob, the thing that separates \"alive but holding station\" from\n * \"parked\". Amplitude in world units, rate in Hz. */\n bobAmp: number;\n bobHz: number;\n /** Ride one shared route in formation (the carrier school) instead of each\n * fish owning a loop. */\n formation: boolean;\n /** How FAST a fish works its own loop, relative to how hard it is swimming.\n * Below 1 it stays in a patch of water instead of touring the tank — what\n * makes hovering read as hovering without any new path maths.\n *\n * Not a leash: a hover fish still gets all the way round eventually (about\n * 80 minutes). At any timescale anyone is watching it reads as staying\n * put, which is the effect being bought — but the loop is not fenced. */\n travel: number;\n}\n\n/**\n * The catalogue. `loop` is exactly the pre-style behaviour, so the default\n * changes nothing and every published scene renders as before. That holds for\n * every param in this file's orbit, `bodyWiggle` included: each one defaults\n * to the old look and has to be asked for.\n *\n * Deliberately small. Each entry is a silhouette of movement a viewer can\n * name — patrol, hover, drift — because a set you can name is a set you can\n * choose from; twelve variations on \"swim\" would just be noise.\n */\nexport const SWIM_STYLES: readonly SwimStyleSpec[] = [\n { name: 'loop', label: 'Loop', speedMul: 1, band: 'free', bobAmp: 0, bobHz: 0, formation: false, travel: 1 },\n { name: 'school', label: 'School', speedMul: 1, band: 'free', bobAmp: 1.5, bobHz: 0.5, formation: true, travel: 1 },\n { name: 'drift', label: 'Drift', speedMul: 0.35, band: 'mid', bobAmp: 5, bobHz: 0.13, formation: false, travel: 0.6 },\n { name: 'hover', label: 'Hover', speedMul: 0.2, band: 'free', bobAmp: 7, bobHz: 0.3, formation: false, travel: 0.12 },\n { name: 'patrol', label: 'Patrol', speedMul: 0.55, band: 'mid', bobAmp: 1, bobHz: 0.09, formation: false, travel: 1 },\n { name: 'bottom', label: 'Bottom-hugger', speedMul: 0.7, band: 'floor', bobAmp: 2, bobHz: 0.4, formation: false, travel: 1 },\n { name: 'surface', label: 'Surface-skimmer', speedMul: 0.9, band: 'ceiling', bobAmp: 3, bobHz: 0.55, formation: false, travel: 1 },\n];\n\nexport const SWIM_STYLE_NAMES: readonly SwimStyle[] = SWIM_STYLES.map((s) => s.name);\n\nexport function swimStyleOf(name: string): SwimStyleSpec {\n return SWIM_STYLES.find((s) => s.name === name) ?? SWIM_STYLES[0]!;\n}\n\n// ---------------------------------------------------------------------------\n// Per-fish uniqueness\n// ---------------------------------------------------------------------------\n\n/** Deterministic 0..1 hash of a fish index and a salt. Not an rng: a fish's\n * variation must be recoverable from its index alone, so nothing depends on\n * spawn order or on how many times the tank has been rebuilt. */\nexport function fishHash(index: number, salt: number): number {\n const x = Math.sin((index + 1) * 127.1 + salt * 311.7) * 43758.5453;\n return x - Math.floor(x);\n}\n\n/**\n * Per-fish spread. `variance` 0 means a uniform shoal in the things an author\n * is asking about — size and pace; 1 means every fish is visibly its own\n * animal. This is the \"expandable uniqueness\" dial: one number an author\n * turns up, rather than per-fish values nobody wants to write.\n *\n * Two fields deliberately IGNORE `variance` — `phase` and `anchor`. Both are\n * desynchronisation, not flavour: a shoal that bobs and beats its tail in\n * perfect unison reads as one animation played N times, and a station-keeping\n * style with a shared anchor knots every fish into the same corner. Turning\n * variance to 0 should give you a uniform population, not a chorus line.\n */\nexport function fishVariation(index: number, variance: number): {\n speedMul: number; scaleMul: number; phase: number; anchor: number;\n} {\n const v = Math.max(0, Math.min(1, variance));\n return {\n // ±40% speed and ±25% size at full variance — enough to read as a mixed\n // population, not so much that one fish looks broken.\n speedMul: 1 + (fishHash(index, 1) - 0.5) * 0.8 * v,\n scaleMul: 1 + (fishHash(index, 2) - 0.5) * 0.5 * v,\n // Independent of `variance` — see the note above; synchronised bobbing is\n // a machine, not a shoal.\n phase: fishHash(index, 3) * Math.PI * 2,\n // Where along its own route a fish holds station, 0..1. Styles that only\n // work a patch of water (hover, drift) start every fish at distance 0\n // without this, so the whole shoal knots up in one corner — which is\n // exactly what it did the first time I looked at `hover` on screen.\n // Independent of `variance`: spreading is correctness, not flavour.\n anchor: fishHash(index, 7),\n };\n}\n\n/** Nose-to-tail length of a fish at scale 1, in tank units. Formation spacing\n * is quoted in these so the lattice cannot silently start interpenetrating\n * when the fish change size — a review measured 36% of fish-frames with a\n * neighbour inside one body length when the pitch was a bare number. */\nexport const FISH_LENGTH = 18;\n\n/** Half-width the formation lattice is allowed to occupy, in tank units. The\n * tank still clamps the final position — this keeps the shape sane, the\n * clamp keeps it legal. */\nconst FORMATION_HALF_WIDTH = 62;\n\n/**\n * Formation slot for the carrier school, in the carrier's local frame.\n *\n * A jittered lattice, NOT rejection sampling: the spike's sampler could\n * exhaust its candidate box and fail to place a fish, which is a crash\n * wearing a costume. A lattice always terminates, and the jitter keeps it\n * from looking like a parade ground.\n */\nexport function formationSlot(\n index: number, count: number, variance: number, halfWidth = FORMATION_HALF_WIDTH,\n): { side: number; up: number; back: number } {\n const cols = Math.max(1, Math.ceil(Math.sqrt(Math.max(1, count))));\n const col = index % cols;\n const row = Math.floor(index / cols);\n const j = 0.35 + 0.65 * Math.max(0, Math.min(1, variance));\n // Spacing shrinks as the cast grows so the formation's own width stays\n // inside `halfWidth`. At 24 fish a fixed 26-unit column pitch spread the\n // shoal wider than the tank, and the outer ranks swam through the glass.\n const span = Math.max(1, cols - 1);\n // Spacing is quoted in body lengths, and the jitter is a FRACTION of the\n // pitch rather than a fixed number of units. Both matter: a bare 26/22 was\n // narrower than a fish is long once jitter ate into it, so the shoal\n // interpenetrated at every variance including 0.\n const pitch = Math.min(FISH_LENGTH * 1.9, (halfWidth * 2) / span);\n // Jitter is bounded so the CLOSEST possible pair still clears a body\n // length: adjacent columns are `pitch` apart and jitter can close at most\n // `jit`, so pitch - jit must stay above FISH_LENGTH. That is the whole\n // reason the spacing is quoted in body lengths at all.\n const jit = pitch * 0.35;\n return {\n side: (col - (cols - 1) / 2) * pitch + (fishHash(index, 4) - 0.5) * jit * j,\n up: (fishHash(index, 5) - 0.5) * FISH_LENGTH * j,\n back: row * FISH_LENGTH * 1.6 + fishHash(index, 6) * FISH_LENGTH * 0.4 * j,\n };\n}\n\n/**\n * How far the formation reaches from its own centre, per axis. The tank uses\n * it to pull the WHOLE shoal inside the glass by moving its centre, instead of\n * squashing individual fish toward the middle — squashing is what turned a\n * legal lattice back into a pile.\n */\nexport function formationExtent(count: number, variance: number): {\n side: number; up: number; back: number;\n} {\n let side = 0, up = 0, back = 0;\n for (let i = 0; i < count; i += 1) {\n const s = formationSlot(i, count, variance);\n side = Math.max(side, Math.abs(s.side));\n up = Math.max(up, Math.abs(s.up));\n back = Math.max(back, Math.abs(s.back));\n }\n return { side, up, back };\n}\n\n/** Depth band as a fraction of the tank's vertical extent — the tank owns the\n * actual bounds, this owns the intent. */\nexport function bandRange(band: DepthBand): { lo: number; hi: number } | null {\n switch (band) {\n // Tight, because 0..0.3 of a 57-unit volume is nearly two body lengths of\n // headroom — measured on screen it read as hovering in the lower third\n // rather than hugging anything.\n case 'floor': return { lo: 0, hi: 0.14 };\n case 'ceiling': return { lo: 0.7, hi: 1 };\n case 'mid': return { lo: 0.3, hi: 0.7 };\n default: return null;\n }\n}\n","/**\n * ASSET_CIDS — the per-fish IPFS asset directory CID for all 512 minted fish,\n * indexed by `tokenId - 1`.\n *\n * Baked deliberately. Each fish's asset directory hash is unguessable, so the\n * alternative is a metadata fetch per fish before a single frame can render —\n * an intermediary (the AWS farm API, or a gateway round-trip) on the critical\n * path of a screensaver. This table makes asset resolution PURE: id in, URL\n * out, offline, no network, no CORS question, no third party.\n *\n * Provenance: the farm's `/y2k/stream/cache` (512 entries, complete, no gaps),\n * cross-checked against the on-chain metadata directories 2026-08-15. The\n * collection is closed at 512 by `totalSupply()`, so this table is final —\n * it can only change if assets are re-pinned under new CIDs, which is a\n * deliberate migration, not drift.\n *\n * Regenerate: scripts/fetch-farm-manifest.mjs (documented in\n * idle-mono/docs/metaquarium-farm-api.md).\n */\nexport const ASSET_CIDS: readonly string[] = [\n 'QmZCzKVLJ1MYRzaZEFyuMCVmrPR3urQYMFR9RWerJYpCEr', 'Qma79eBTFDb2i1CtF96R6coxtnEnTu8qgW8xTTt9wdKdsW', 'QmZrjPzPrkHWNfYJWCKadGxqrhwVJu3zsovCAuYRw8EzAt', 'QmRzCWYvpRMuGX7MUrXFDQ6SEzxvfiJy2XCc2KFT9nZRwH',\n 'QmYz6VAVyJb4XdhNkcByQsTMedyLUPuy13q835XBqBvFnN', 'QmNadqTMC59SV41Ljqs3wftQRMnKzP5HfHhJhNUGaop187', 'QmVo9GNRMZRhdykEfsasiKCkSxJ3NZMnJW2teu3ydM3j3C', 'QmNY8rACjywo9MCnq5ZRLqDovCeD3rThQkquVhBNZpDx5G',\n 'QmYSmYXaGe9sjyccj3BDfaWpjrRHfQut2KUj4PPqfNP2JQ', 'QmeXJpHtuo2xdyLLHVjYn4DnkmAmYQChfwYMNgH738342g', 'QmXQ8sWSv8wVzpizPy7XAEg4fkGULegqhYmmmNxQxFhURF', 'QmSGLEWtsJfzRDtyg6qirHjVAWmojFu4DTNMjgkgGYhaQX',\n 'QmQz6mraCUgznbC2gAhv4xfN94baioDXvSwjxUY9HFdcUc', 'QmRNkt5myZfwJ4ja9s6mxkSvcAUaFmmKQT6EpVVU9EpxXJ', 'QmUNQ4m28QSNadjS78XjufrY6tXBsdoAJXV3wP25Mw8ECE', 'QmYy8J3L34p6db2qpqsxC8fMS635KretGDoGTh7Ro8t36N',\n 'QmZPL8gWr26b2CNNo6LGFJ8dn1sQMcpb1bnrmjJN3mRBPP', 'Qmbsuivz8SHZS89ss8PeCuaeZBv1m5Vwe6b841YkSt57Gr', 'QmYcF95XchfmViqJsHgzKTP33gVxCUHzq7dmxnWuh4JbbE', 'QmVuzAQutncvQBCGY43tapphaCNNoawVydEfHo48nuQG53',\n 'QmWL4zmX8QnSwcEhrxb6Ek9FWnodT75fK5jQE9G3nhrTHT', 'QmWdgysw3gxrJ3Hy129Ej7NcGW5D2utvWTMie2CA4tXjf5', 'QmW84KhqabL5niFwknUvU2dx5KCgkW9gEU4513m82fguSK', 'QmYtfYGfPRQdd2v2e2mVezgiY9qZGXmLUn4TLnhBUMfsk3',\n 'QmTgu5xQ7a2ZDnSbSeDZwVWYJpoi2LHSEA7Q172ykHgEcn', 'QmbUSdfjzd8EVsJtzt8iXmCwLPeDC84ScriwtEau6W5WUz', 'Qmd3y3u9kzHtKPvrsBraQX9VYfWKHzRgTE2Wchhrm9ub8M', 'QmbxDsnC14zmzKShpRTscTaKJayTSaCaSxW2gKRc7g8Xfq',\n 'QmeKFPkwPTUU8GFVpNfFjsBKr3nfsRMo138EiDBYyoi5iB', 'QmUT7B543xj7TYkgbSspBDrHsJHG2yJTvWPy6iVEcP54HM', 'QmNzFPTar1xFa5QJVAuksyDpJTwK9mARRbNWGZ456Ja3sE', 'QmPiYEDAJPUA7pjqjBz3xTCFcVEFa57DJoZnFaoU49FGZ2',\n 'QmTsngNyu277DGD3qg1u89nTSBHp6QpeyNjoQPNEaNaqtU', 'QmSK68pRT68B4oBxP2LjVCNJBYmwsSpkykGmgW5Fgc9HAz', 'QmTVjYdCrexSTbMkmzBC6rNbYQF8AddQi3gPbGQqwz8iqU', 'QmdFFcK9hHWKyShCX8gHLUdrWf4aKMtJonfmKGDac3BdrX',\n 'QmeYJm1xn5hnkpZSfrYcMbpmT4u8XmaSmehhnyoDttLdFB', 'QmNbydhwhNHemQwebQvhDihnc3n1QiNFPueR46ZWZSsxYa', 'QmYy1uXan36v31H4znXmL855XrStjxtyjsxEaeUngdRzRt', 'QmW7yf2JMmpM3bAauiomXhWm75ur4Lu5GASywFanSS7yp4',\n 'QmPJYvY9bEpEhDoyH5CfGkpMGgk9cppPe22vD2yechR2Pv', 'QmYEbrDdFnfqvnkfnUwDC6tXoC1zwFmaAfsJmjM3bUd8ST', 'QmSBQYv4Gbd3SykKyieHcNwT4uaRE6GPsaJkB4RH7Z5QMT', 'QmTcsXsMTqfgad6VgQxqgR5htFPhRovdB4CrNoF8RjVR9v',\n 'QmdK65oNZpQibD2TP9QurwtuHMjfQTQBEgTx3v8ywy1q6q', 'Qma23MMRv6Jv95QKg1SuSgUcmKiBbrgLQYcTGonT3FVmLP', 'QmU38DaWwsKvxh69YrrKpLwnYBuNd5RMxE4MLVUjZDj5ov', 'QmNRASdujvSTCJYHNUVfqSDWeR8DPqdbXsz9PmNTAkzZVR',\n 'QmfAMeBwHY4RXqSNwiqmv85nKNcfMxn6gg5gouhfjeqsnF', 'QmbcpM2J9kCJ8kEaN7avcum6BpcebHSML9f4iEUmwDQkGf', 'QmTrmBe8r7F6mEaUscVuzDg7WS2iP6rmDLhEx5BprrSMRT', 'QmdRWCzJuza5Eq6n8f8YPcFPxbbDBUxn9CF5NASTQrkzzv',\n 'QmSRCQUAE4L8aUTB3kxp7SLjKwk5WkCocRJc6GEUjtMqVg', 'QmdvomiXzh97Jx7MHhbb2b2y8Fr5UrUMybdiWcGCJ8gCKM', 'QmNpRThE4ors6zBb68gz98pJUneDzNN3hdVo43hLzQaSCf', 'QmfSGJm4RqUVvYsrbS5txWypPb3rRoQQS8kpcZnbf5drqm',\n 'QmUH8DBSDSev99CpiYNxd4wc7xPfFYZ2SQ1gb5zdcry3gX', 'QmZc5WQo9vvMHhUNwLg9nXCrynXMPu2L9KYAV6LD3NDm4y', 'QmYtDyPEd2BcgJLgZjj6JfpVhy3ZYA23GEGRGB4zRqrP2R', 'QmZeG9tvLDPRbBVJMZs76caJBWfHcCY599vA7PeY9yWk3n',\n 'QmdpQFyGFRgdK5FH8tQZFWcCb5YS27BG2nYH2jkfrP9c6x', 'QmaWVfUWr41XpGrhzU8EZA6khTeMVgSZ5FU1sQACRGzw6v', 'QmW8Nanwbwoe5ZWaSAN5w6SbHGYAzE1Uje5wNJySkULWZF', 'QmThgMpfZXYxrneAzYeUJwBa5gAjG3rGYXKLyx6kJSEi8D',\n 'QmQbdofeVFNqsMfYrJxoQbobyz8PXX3ghdtTKK2NtTv6kQ', 'QmbmNr5NUKCfxxPrkCwQhpYMyvYCWsMEfFmYJLUf8MWVzM', 'QmWtDac1r2sNyUUx2LcN7VeJwcdxno6x4iRp2VD7zHRhyo', 'QmTDxjfns7kPpQVWzDbhJfdczUb4RZ5jiu7KetmhhjNo2Z',\n 'QmcMVUXcDjQgEm9YJsAQykmnnyYDFowqiLaEkUyuD43rKr', 'QmWQudrmXa9TvPe4jC6JYCyUuogCmficYDytbis1dQTK8A', 'QmazJBD9H1dJeM6SjxpKAEoJqL9QY7ie2v8p3Qk5sHc77M', 'QmexHy2WQbF2w2bToTa8V2JTZZxG1f8C7TjfxVFjvvLCY9',\n 'QmXq23WvtUdbNGRAzHJen2JLGGsTkMQx5CcyJjw6BAoscS', 'QmRRgSeYAwBBhTMBCUTEVPRMH3B9if16oiuntaQBjNSser', 'QmTB273BcmsW4Gkm3MyUEkK4zH8QFkQvnTnDdapyGzYXMv', 'QmZok97rbnUDoEKiwJ1gvwBzKhhFeqWbmw92KfppvmcwJA',\n 'QmUDPUMg946iVUZM7iA7xENaP7VYp7KHfXBM9EkiWLcZU5', 'QmZXqUUaRDDFwG2HVZWvXSoa4j3eVBHLWdtTEMe9meD1c5', 'QmRTiH27NJzrRnsQw8kNTSc9iUG813HweZN4HznQCgkqry', 'Qmc3K2yyS5tG7yhDVaFVUjPRnuP7MahjA8LQJDqv9ZhN1s',\n 'QmYVdyMo7izizLFMk4FEb9WUqRuL39Wi6Xw8h6aaPqe65f', 'QmVwt4bK9Ycj7VL4hLnbHojLYuGvm4FXbGeduwk5hWwFzX', 'QmcQhyTqt6cGj6UJx1dEGqPHjdncY6Jpy5DXE9ZuNJ9uY7', 'QmV6gJiqNJpk6JuTYuA2y78TMRKrGRWD4wmVyriqocgyvn',\n 'QmUxej92evQZ5GPX8Uk9tRDnDEAPuPaXL6Sym1Yyo2jjRP', 'QmXqxKWJ98KQHB5Awze5Jye7oj6Axy5o9DxfMiXuNWu2VX', 'QmQkwjEDJy28EmU7mmpTCTY2hddZMy4NWvpFcRLAhqCr6n', 'QmYyLKD2FNxBUgHYnCStcfLEjphsi6ih6h9SiMtZp7wpHW',\n 'QmS4V15bkLER8ewFD1Q4FtmbneXTyGBmmCMQGxiXKes4vC', 'QmTFbhX5V7QrJANzPQEG9JViYBZXSoFB2ADHX4dSecRgv3', 'QmQEz3G568uXLwdVMQ8coYrWC4RefJk2c62oMmcBHr8dnV', 'QmSgJhpyvrjpWHH941JXtmTXC3pMkraMpm5s8PTUPkYPcg',\n 'QmeToFndkRpgGyApV3R9Vq9KLNnhLv9Qxw78suGA7vpZ1T', 'QmX2PU9gusQtiaKJqfbmPmDQQAgm24T8gromYuApo8tBTj', 'Qmcg8NCsqR3YwU1PCqwMXW81f4CXRKnXjEMBesgTJTEKgQ', 'QmVeYwMzpmr73zeU35UifpjkKGFT1BZ3DPz1F1rMfCxLrz',\n 'QmaN7WkmRZsMtygd2pJkedYdpYsRAAYRD78Q8Wi3VbXeqz', 'QmNUfD91TkPwdo5eakkSmTZYyymdce74XAfK5BjLNbGs4T', 'QmVgupkRbxhFvy4QJySzaZrrkQLiTTNYKPod8zDbWnMqre', 'Qmb5Uu8u154QTzoGpB6ypwVfPZ8NUsU519tQmrgE8yQrWV',\n 'QmYZkLdy6tF6gwcfUZeqoHPBjCYuA1cYfxwRnxaXciEJTM', 'QmVpfmBfjCwqEtPf6pbva9eTohrVHafsFd7toVpjFLqE99', 'Qmf8RFSgbSdoYCSqkJqFHZy8hktR1LsoPvVg8DZHe9m7f4', 'QmepjQCNSbbEoVGrQMbQdtsrwhELikDpieaY1DvSjUhGWD',\n 'QmZDDPcyzGVuMSB3ys6kpvvqKEFnWPrsFW8k58sieBaczS', 'QmbfFcEEeKLkeFtH32N2DSi6UaHJdRhTwLxXx3RkN1Ds2q', 'QmaZ1ha9dbtr3Dq76sDHS5UGyb6BXAGeVfbDriQjZCt3MX', 'QmWDPX4dKPi17AwzBua723Ui3kKj6KY8SKcSiajhQNNzb2',\n 'Qmdq5ncejjNb8uUN8658A7wtDCwpN2KCrmzvsYSDhVDLQK', 'QmTu8KgApeJEt2H8heoT9t39GR1wChQgKXQ6EQLrLuYhy3', 'Qmd2C7Xpf8mMJt8TfbGzq1UyjdBrkKyTviP7k1LCmdGY4y', 'QmVmakJTh7jMEfwzxy8jBQAeLqqhM2ujHo4K6PVtWGs68S',\n 'QmaPCMjMXeHrwU99M9bsqmTstXfVunytEjLzX9HXHq2sQm', 'QmS4VYuTajrsp2DJjVFvnKqPjUvfC1k2hA7BM7raSDpNah', 'QmNwcJ5GaXG669jNs4pdnkEHunKJX6DF1dQCaBhobZJWVK', 'QmZG9d2rygrkSiPnJVCLBsjyAD2JakTcDh8bSZbau9UXGd',\n 'QmdLsuo5siCvG5KduhizhPVQmSyK3jGVmAXkZdkxLxiypv', 'Qmed2hoAfDto8cRd1XL5C91ZeDCWSVd3K55u574tstx2sr', 'Qme2K2z3nNQ1MDPY2ejbcWczetGrwv6WVyLBtDdRCBBx4R', 'QmatpBQ8KwYUtHpEYdd67Vd5Q4WDuFs5ooNSG1C8eiSyAB',\n 'QmZ6cPQGycdfrwTwxfdSkE43cQBDjSoCZfGETtie9L2JKN', 'QmaVSjydLiF9Bno1FRyUj4KQvpHPmwEuBitpdCHMmLDYSQ', 'QmdqCKe5ANxPxQAsbMLW4JKxFndh1Rx7W653qQVVhh2R52', 'QmVPwhrB17kgB99iUPMbCT8sqadxrctX3WG4bg5twtQzUy',\n 'QmWYpdLh4mqBZC4wBBzrQLZyM3mfehzyx8jd5vU5KjK7ZD', 'QmUtWPrCifM4FM3Wbd4u1JMjfqQ8AsRzgUEF2L7vaC2bpv', 'QmYuBEuU8cpdg7Udb9wN7ChHcimqqG9F2BogjxX8NM48BF', 'QmVHFng7aLG2zAHeqnKUVoJdTohwZnBMsSeFUPsPbx586b',\n 'QmVRdRQuHZLwe1T9EzKdTt5ZFfv8vcr1kjiJ17scnwzXv7', 'Qma29scmx6K4NkMDXnRSuBGUwctShHhMdMaz9CFkz1Bj15', 'QmRC6oC1MV4tJ3WKBpTW3vvWi8Lw1VnjWQthjXrqgifdYd', 'QmX3BTbCb4snjaKH1apuopkY3ZBvCqnyh2682zTPrqCAsV',\n 'QmdQy5FVneE6kqgA2LCS3k3FMsPLCyKauWhCnuFyyZxa6E', 'QmfCoMLjGJvDE6v5M45LU3zLbXtoYWXRzqzFFvrQ3QU41A', 'QmVidfqfiJuRimwk5V2XUi1cQHR4hBMThXQZ2SME5S3Wrc', 'QmVbg3iagkRapLoctTZonA5rLfbo2m4w5m4RUaNtbtuwmB',\n 'QmerFeVE5Z6ThcgxRrDSPRtawnZo6a7QPPQbT4pG91AwsX', 'Qmc3vudn5pZkbD1ALfLgtU5UQQfaY1AucuwMWFa38yN9LH', 'QmU2mnSibwokwZSp6aXoAf1htdWvrBSzvpMwnTmnkyRTah', 'QmeqML1nzTEBAHzEXxyY6EB6mQpm1MTTSS349XfpW4A2Lx',\n 'Qmd99kaKTB2sCqQhaBVF62KjvXtz7rpR1sNJQ2bDBP1zCC', 'QmV3Jr4bKhQLmHZDeZu5xfD8JJKxvpnr3nnom27McRfCSe', 'Qmc4Vqnmy8aeMdgdGMNT8PH7zkbF5arvFxn6DxZdFYhiPm', 'QmRRkLXR53fkbrkwtpxJLvjsaxi89FNUSokLphezLwTfaS',\n 'QmNN7c1SzYAi2gMfbQq7BZdUgKbwcF8kLxQCuXo1ax5a6m', 'QmWZGWfTZWAA8iQK8XKSsRBe2ZzGFySu84Dawy2Mp9SZsG', 'QmXT6UzMd6JNMrAJBMpPesXiL3cXrAo8cF7uXAr8n6UFbk', 'QmfUXVC95xRcrVK3TNDubMM6VbvQ9TXoVJUAc6bKdi7RY9',\n 'QmaodAiK9cSVrouFiNS6Mp5tukUs7BHekc1NCH2FQV9RAc', 'QmPgdA4MnCtDUE21aZBV6Djvwrw4DxhbkY899TwAxCQ8q6', 'QmWZkMRH3iGpaPwJknobWreN69wzLojDCQZDSpxZP3VFGa', 'QmahGxWxRG4qhzUvD7b87efTYFDWKugJDuwkHDezcmCz2M',\n 'QmRd22d8hvns8H7rMhD4Xy8w8cCZgRbBar6gSWxqh9ZR59', 'Qmayv6CkimPPn3ZWTZFQEvTNh4FTRyk2HEvp1rXgaYwKpG', 'QmcPBk58JNwU293Der8QewpDDtQExSgRCXWQRYCwJ66SzL', 'QmR2NLzG5orqxMXXwAu7Z77vkqGSeHwBdsR91FVjzdsNdC',\n 'QmRTfSCzSKofkLLmoYae4Q92wpGmWjuEJniyZbHkMH7bbP', 'QmemaoPwdn2HcQ8h7A2QRgHPj3ADjfdvFFk9NyS1yQBQQi', 'QmYcNg1nTVdtVNqg1KSGTWwruUT4NgrjDH8hyJnxbND67i', 'QmXL4drp3Gz9XjDvhGPtMGNhasrHFMY5bNhqZuvJny1si3',\n 'QmRcRCYgbPwfXn7Po9Ve1GqsfYtSLHLfBnve8SvxTUPkHv', 'QmTMrkwMcMzytnPWLFCZ8r8RLStYWw96EjKmDHLc4UQ5dm', 'QmWyqnRMYgZbhJ99eyg49PgJo34eNkWWnY5U7CfNgZmPvG', 'Qmer4aFfhMMTD5mEY7sfWKzwdao8hj7qvCTVHbBacF35Zr',\n 'QmTaxfrktGTf3AfZaPZdBiTfPMBBamshM6o2qterQAhny7', 'QmUsdqReKyfnFQi7p6cQZWh2khq1QVR136JJ7717N7aUGy', 'QmfPYW9neLP8PnmbN28RkWHBX9THiEf74LrL12Epu1vG3v', 'QmQQSJFdRv2qEzmCbYDjUFxTnKgGr5PB4EQjVpyYXGD7yq',\n 'QmRjSVLsAFPnZpKS2RhCUo9fbCrniyr6NyRNUHWkoFd1C3', 'QmZ7N3xFgNUk6jzhEvPFwaUw6No9sgTxKcCyMeUSQQ3LFy', 'QmQfrqNdEyYDpzqncKBUMFVX315HjNtWt1B6qCx5tHkq1T', 'QmeAMQ2L89NkjzWpfTvCJ8pL8ik9iZGYu2nVWsnKwcxhU6',\n 'QmZEeDCLDT1bTKwma5CRVGA1n5aN9c6hD1KPKV6jAGsiKC', 'QmVkaXTC16mSLP3cNc1Kcd4cfjeb2DSHevf6FBcPs3ck3Y', 'QmdmQhqoGUwvrdMvcshpa88UWUSMWf26NJactVxun3rFvv', 'QmPhGekoLzQyYjh6dcFVNVtQdJJgtx5HncdW7YYMMnQkYi',\n 'Qmafd51KbHsTC8rHZTuHHD5FCUgj7swgDLQ3gexxCJev1H', 'QmNfjQLmNpGDBejVJTo8DZFjQpdjQyhE2ooe2DTzXig8Cs', 'Qmc54nYmDWddHahn1y9aFbxpZMJ19mN1iT7S3W5NvYJKat', 'QmNnNN5am3rajEqYAzND7EaXbn7hQPtDGrEo4hnoapFpuf',\n 'QmZMnjzmNBkNBMVcjJoFNfn2Aj932P8UQ39hH33y3VyeUp', 'QmbNiSMfRQ2HZ7nsi95EJdjLth2mdGbHwB2c6Jjiwv9jQm', 'QmQ2PpMuQSTWKxxeonx1xdsgCVRNnUsBrqqFLMCR8Ki8G9', 'QmVrUe4YQra5ywChr3LW8n26mmuH3ff98p7DPqzJtxdYYa',\n 'Qmbq4LqYwbgY8w3oWug7EYuUTeNGzQU6BwprtPe4rVSxhh', 'QmbcJnZsgBFyZLgF1CoN4BpAYXzgv5fxeCPhyTokJLqwov', 'QmPe5SeU1eK7qh9VuCKeWx9v8tdC4XguMeQ1Ep4VmG2uSh', 'QmQzVJzJowHoMpq3wuUQATJ2Vh3ketQSeJ6u1wvU8zXbzF',\n 'QmegVuYAB4eFLUeE7rYmFXWudMrZMM7oJ5WhuKfZhwsMfp', 'QmYQshiPnL7CmyJJEuyRSMLTS41mkFnPnAbbGfwtSuJY4d', 'QmPJbq1oFrTQrQDs5iUc9egwzxdiYRZ1PFfxemK993pjqe', 'QmWZKsea1nQ1yxCJXq3CAn6Zv9gT5MiHn5xepxyPdkbjRr',\n 'QmPFzAe3VzxX7MnpCmHjtGapR9PaPkTaGFU9fR8NG9Vofv', 'QmW9p5NyXUoU3ASKizFf9VkGsGjTyKC889AFeJRp9hQRxv', 'QmWQob2X7mL3t5xcF1GDnN9hZ6U27khYoDMf8k44QZ3Asu', 'QmeQoqz1v258i3YqjD94ji3abY7svB5WoV8qnBDR15Znzu',\n 'QmW9LzFyD9sEtw4QPQ1NfiSUksXDB7w2EJbbtjYkENcPx7', 'QmUvFmCDXpSYjDQRuUZ1cQejiGd3nR2qNuPdxCSA2R54Fx', 'QmPgYampLSWCmaqiZnGaf2sa9WpjbGrTUiz8mEBR9nLmxW', 'QmPcjfQkv7heSQ7g3ivfuzNorFRMJhk6tGtSAVGEwsKtqU',\n 'QmZPLWj97VidVYocCpyjco86FvF9ihxMDzm1269bXB2mXg', 'QmVru99h1EaLvQhdhnfZBGsH366fm7MsK8ukqio7cTLKQE', 'QmYcbGS1NZgfDQZE5Uz2u3qp2ypxJF8bknuKRxFU1FZLHG', 'QmYdhTFHpZB1Fgc4WEg3Ku6SGzVwxgDku3ngqJBuCiPtxT',\n 'QmS4QYGY89HvTYeNA9uMPPctXw1xC4L6ZoggDuKCqGtzjx', 'QmXnwXhkf9z5XFdTxqaCK5gLQE7RLSZ7W9GvB4gGwWyiQX', 'QmVpZ2jKCtJRhfapuRTy1JsGrkj4PKJHUMs2AAAjPhBu1J', 'QmZdqfWkT1JCefp5KmMiTokuaq5zYQ289ienRAa1V6Ekhq',\n 'QmV4nkJz6o1SyhnyfjNYdksR2T6WcaQuVJGpiaJGkv6isr', 'QmeKDp5F2g5qziF91FvP7utftj2vYAjtJTHpDYZ2mFCEDs', 'QmaPow2mt4tf37JeG15TjLbJSZJ2yngtugK8nrAjPZaaJj', 'QmNx96iE8gjP4F4HwzkFN55faUetx9ZVj5ETYWY4SrkNjT',\n 'QmVE8U8Nq1kpGKheEk2wW8MQhohY5kuvzu3eP3bnmdjuP2', 'Qme4h3KizSaqthbXRevpC6VVj77cpfHLYAFYVq44Ugd5z5', 'QmZDxCPGDuYSN2HnEcjVF1FUgx29brci1s6AYnmxfbTtc5', 'QmfYKPSuAiNyXDkE3NQ7J3ADV93j81mVbAVCpf1xF6wo5f',\n 'QmTyrAkEup6TZuYihiZCDfYjtnEdDSrcNzZ77uvmha4mR8', 'Qmcdt69vR8kSap9gZUtdTT91JUo5ePD5H9FwRhTRZjeZhU', 'QmWt6dUfMs3qMPYJgSCyjLUjSzJHj2pAdQDriPP1eDkfuZ', 'QmUA9AJR9gfBqXUSV2z3CXk1DGjDtorvLV8CwPGw1SZCvo',\n 'QmPHoQuKDqh2c5uW8utNrbERTXg3jUdoMXpHcrjmWtdoVk', 'QmYsGmRm92LHxRK2kcXep5kLdruiCjsXzra5boRy4nAUs4', 'QmebzhY566vN8n9h1P6jdrMQPoKcnFdm9UxC6BbWc3puWV', 'QmRst9PxEpfXTadfSGbTE2zxBQdVA676VmSVBPs5b3kDjU',\n 'QmdxEMY7vm5Ga4DBCL4eCttBsZLLQXyaUga583H243KFJ4', 'QmPMYtjAesq8aGDQfwPCgvDeLrMbh8mNZvqweuhZvtqomT', 'QmWkxMNGdTuvgdgFgY9KkkiADBJ466sCXfwr6Z67vxeFbj', 'QmQPZntLdCCT5PdYRqgqdfDe2mrWqpRYTXRL5rGKQFSHF2',\n 'QmSoQR2AbwtXXpp9qwJwTt4ywkXBt339RU5FUWWHt7NJcN', 'QmdAoQqgkENgHXAas3bCvDyYCJf247uTj8WjEqHc21JXST', 'QmbJueSqMWqV7xKXukXSxyHj5TPXxjBK6oYMJdBNJaGSFh', 'QmUjvGyGssPE3r6Mjuz3xNKPevC4Nq9tTrbzdjDrUCBtoh',\n 'QmUwomacb9wepczydDRuKWc8Pwf7cKPF8q7NbmkXXmkyec', 'QmcAwhhDMNEnq2GXmR5ZiHPniTCYEfikVCW8oaA7qJ79Ww', 'QmdkEUeuRfQJjBWbvvKgNFRNzjSJq1s5tofNrrUmAqnV6a', 'QmWUzo5o9ki26XC4dicECwvNKvKpMsQ7wr2fz4bcGp4w4Q',\n 'QmNnbd2F4WLRuAKPEEi4trTxiNVeYn7z2ZctroUcirSszf', 'QmTxUZJoJktDCkg5mdU7qA27sJo1gs2kWyhxA4xZED3aVj', 'Qmew6YW3ttjCDMfpvZz4WjCTJTx86hBxZvV1NB6MHNENtA', 'QmShVcWV9eR2zkBriASQWfcd6Ue35SU4TSvLAF2Cz7caKA',\n 'QmYY6cixMbkYv64jssd2sSbq9vzXwAXFJwjYXw6J4X8pn9', 'QmYE1sBR8FbzGz8BWx72sV3c9wq4Mm276nYBXAiASBGGDj', 'QmVzPtCUZt9TtNu7e4P7g6FfxMr5Z3CCb5DXQ3KpiRUCCZ', 'QmYqkhEm1XXp2WqKpejvC7BjNXM1FMc2uGH2pVucacaxtg',\n 'QmTwdq6xAaeJ62i9Vr4AvJsmhoNpq6AiF5tHwTpo5zCqNC', 'QmXjTMRXXpqRYcf3qm8NbNFUKN9yEGEvDjy8McLaGcRfHT', 'QmdjfULZ9vMwxd8kQrTNjWQ43h1NZ5KCHCEmau5KguTcid', 'QmfDTne3NS2hgnJZjwEpMPfcWns4TcGrVsJck3JJcwrDdZ',\n 'QmfGuMepibrSLCztNy4TNGPXj34wWv5zWFFDxSh1E2LzqL', 'QmQ45eU49MEzvikt1iX4YzSa7NvyPV8qhz8Cp3cPbtp98g', 'QmTkMujFKq2s1qFXvQPG7gUnV7UxRr83Y4ARQRu2oTL3xw', 'QmW647YTF5b4FvTzM4vGYmU1dQYNfiwFgPFmHHApfe6YP7',\n 'Qmd4Awoyx9DAvDWNyYPYnH8cZy79KDHBQCd2Uvgs2jXv3P', 'QmYK7UQHrXLF6kd7LrX8YZcvR8V6W9fCYYPPW6YwJv1zVL', 'QmXtu363znTk86aVuKBL3414s2eMvTjdnhhSbdRWgJPg6D', 'QmWtxrWBwwdzLgVDPCT37hao6p9tDUyYHcq2wD8289prjp',\n 'QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp', 'QmUZGF3ge3d9rzrtxrD6V4qx2gLtGeeNLuCb8fQeNyUkwJ', 'QmfBBnNrVrkffMKoESvq3cB6nAWGpfMPjduTgw1unahvPf', 'QmXsFMnKWaXJ7r4tnunZrehsTcdXeesbkz4Ck7ynZzCTDj',\n 'QmbB8wwtq1w76YrMueifMuvUymzzC7chgcu4Zp76HSZpRw', 'QmW3UwAySQQEGeXY7TNKcmSaGWWpDsNhrSySVRdYnBnfgE', 'Qmf5FYbchHe2gCHFsmVKmDPSKDfR3RWruu1Kwy7gUX5Jhd', 'QmdwubNVvqAUQV5Rft616cox4HP1bAe99hchJ4n9gKZ2vA',\n 'QmUF2n8omGU5ko3XjsWZMt5dwS1ZoPnCf3RJyXXobYCJJW', 'QmdDogp7wJMLwHG2JYV3YTCFtLagceR5ANkeC4caf8CAMR', 'QmRpnJN76aw2E8rbBxEQsDccW7KDT9wLBQF7ogDgtScgg7', 'QmSomBzNykXFfZaHcAed6gxgs4HcNkjyMGbk8LMXWLsh8i',\n 'Qmf8cGMJGFBiws7nSJ4yrF9yHtxWeJJEzCvpehLomNxzgm', 'QmY7UjDL2qz3JvgVwpoWkmtTNQzV6YAdkGDe6Ammt8f5tQ', 'QmWbrqL55LSfjXRewWBf37yKToJRBdrmCrRnc8PeBr2bae', 'QmYxjjBtYW3ppcCAai5t8UekS6nuknRgwH2xKUhFK8Tnrj',\n 'QmTVo65272gTQvTrYY2ECidpW6yHAMFMkZngzsu3y4pYhY', 'QmcNxGZBV3BXQEgPFa2FgoYCkWcGeakB41ysMXQoKtG3qe', 'QmQJ2y13JEmSA6nDDLWbGnibP5Mzuwdb5G6PfRdkYgT9DT', 'QmTqVinHaSUWBZTJ3fUpSTisWWn9n11Go4AtN3nMc3T3Um',\n 'QmaSV69r69MtNyXUSgzrdxQpbFtcw6KBLCb8WCHXypkX9D', 'QmbjEQ1QpUAYcYZhyZWNHqjHa25e6a7kwrAZVYbfYtkQ3u', 'QmW6r3iPj4b1mqGDRB13LiKEFaGn7e7MZpUgjAv2RG27WY', 'Qmaa6DgT6CYQEMnzaA12yhgssV6WERGcztsqWQCJTCPtm3',\n 'QmYfgJvNM9LZPkSxMbhGjGgSoFXtU8z8r3t8omg8Kaau2d', 'QmVV3omLAKReiKEF69pXhZgTdsoPbyVR2ugoevv1x9CAgo', 'QmX9o8vNPFV54LETBYdnAwWLTfjw4cminZtnXcK9jm7zPU', 'QmPNhKGc1sb3PydoGxUaxQUrHELp1AwStCJ4WVYkJkfJAP',\n 'QmXX2ANymzA8d3pzghY5Z9aBJZVHqN4xpchhywFNUAf21w', 'QmVEuGnPZE1PAwcZeXqcoJuoc2P1A2nytxag59o9GauDTm', 'Qmc96xroeXHSYf1S5bBDjY3S1PAB4kXwCBQgLRsgyp8VZt', 'QmRoYGay8SnJWVwqzJ43vYtVJd6q7GD773kXmsLbdhmLZD',\n 'QmR1BHc59QoeCnEjjLrynnAH5wpu9nRWX2e6diArvp5Cse', 'QmbRDNBHtZA6Bqs7mpLAzdQd1HrNkZw2TujFNjEUZ16dUU', 'QmTTHRx4KLEDa2W4skQPhARjSfKPQpopAJUhqubn3azyKS', 'QmbFeX3MjvZituGhnTq1jQTCWnGqtNyVs5TGpKt3vBucpb',\n 'Qmd4CbU4aeSbBzq5mk3533GhbwVV3C5oVNp15B6saVEDCZ', 'QmWTDFP9CrdByzLoqzr4Pn3QcxCyKnyhFnfWHLX2JxQ2kf', 'QmXW1ou24MdJq9BHCi1T45TaAbhHouWGNpsmL2XzYkY5if', 'Qmami4ifqxEDmDFSK6hPJX6FQ7bJ9b38Nz2aMg8tkAPRvc',\n 'QmRnf5bkjXSQmWPTLBDXRyCPj1SxL5QDZq5LGR1zL1fShP', 'QmRyQZAhTomP4ir9b1sFuZRivoone2ukwHtV48DXwA32ko', 'QmbDsXyUiBzMA52vmHhfY6qCGk2NSbHRAyGBCXN5bMm5Yx', 'QmZwhZyzjrc7mgMHLQH98xBbqn2CgSMt9yJ6cH2ZKjJ5yy',\n 'QmduL2hm9bV5GN9U2oxLByqjiEQ5rXEARLNnGvf9sbmRdn', 'QmcxLTpyAknVj5QZgsxdgrg7NSqChD93ut2RPBFT1uUTdr', 'QmdvaG9MiBm85djwjYrPJg3wmh38dne72YDcSzXaPgBNDE', 'QmXF9A1poQfKR1mkfwv6wUNk42S3v4a7K4AtCaVmoJcHHZ',\n 'QmVebS69oAGwJJMRHUtTxbNgCjW4RZg8CgMbW67bndqP2z', 'Qmbz2ADm3f3asYgKwUArUBWTB2Grtbe2fuaQxGF7pfjDJY', 'QmYr5qFAnmTKSeVegzjn3Nt6SUAyMLJ8hpZhK5qKwFe3bt', 'QmdsAdwp8xaJF2Qc6shZPwd4P4kiT5qK5H1awxZU6MdiyS',\n 'QmRkqCuvbnyqzKSjY5ozN2oEGT2E59Zw8iHhd3pETfemiS', 'QmVhXYiLs5AMSnqGLrYC7kUAzVTTiyu9qDphb8TFH7biqk', 'QmNycssFwkGPSsukKwA34GjFoKWbef1e1guVkWHUdvTgWd', 'QmTmuRdadBzwCsdHDMXAqXvDcGkCQDfvoMBsqonJNRpSiB',\n 'QmRoZV32mXZmFwwqvX5ykMoyNjyjZvjVT3NkLVJcLCj1Rn', 'QmeiLPe3djTfMmAKdNCnMQBdes4vFaxbzPH5k26rnjkTCs', 'QmY3FVhPNBxjgKZYi9mSiqAB1GXaKDMpRPoFLoHaW9UFBc', 'QmWyhhKhuB9zwURUCaF5RssuKT2uZi4gPcXLwHRPkfs6zN',\n 'QmW7DzVmvSKYLWXzoSvF5UFoy9z8ATGUcs49VyvH8EsBZn', 'QmTv4wHn3pjs23pt1ESBpK533TwDmepSBse2ULvf5DzBiC', 'QmTnbkeTsXAruaPkSrmGVBTyeBcztWRTAfmQJa1FDDu5Zo', 'QmR4Kw7wjEuC6j7XgwWKSJYUb8fqq4CPnUBNC4ojaS2Eib',\n 'QmRLVSNbfXdCJ83i4L6d52A4XoSqteb4UEF5qNXb1A5YBf', 'QmejCD17oPmHGhnLxohaToQqUnThGE1qy7fu9XoHT8JkXR', 'QmQn5eTB4VSNBY4PPhBx1qqULMdEdnBDkmU7ZRVsQPupPK', 'QmYwSY81XmqvToA5nHWnCZYTzS3eShnrEV7XmrVXFRqLfb',\n 'QmRmYSraWyDCuXtBgitfPgZj5wpGr11dfrDg8QzdfEevpM', 'QmYhLgHZFQxGyPegdKn3TYoEYRuhYzrmfhRh4nHH2m1VL6', 'QmX1cN8i2chRMsLxJxbLs1u72gZoSEdPLensxFN6WaeGp7', 'QmTv6mimaoDKCPNBhyQZTzwT719K18S63FkU8oMKbtsSgc',\n 'QmQmfsRBNXHidS3oUnpxkkTwWKnZ6z8vreaujAfY2W1P9L', 'QmZLF9SyRjgPtcnixyiEUGQFyVRtKaTzjR477bKBTDmvLp', 'QmeKWJuHuynrbYsBmjrcu9N4Kmd1xi93X8AfNEbVN2vvHk', 'QmWLDLsXWUetW24eXrbfPrWVdXhzoADKZi9hKvcLPQvnZA',\n 'QmZfhbN1UELRfmQtGRxFJPtUhJpsF2wwBnacw7EdC12kvg', 'Qmf75aHP7UuHkA9QU49dqzU1iVBHq9kryQVYwaBPGkM9WF', 'QmTvHegn544abqaYaxDhhv7pZ8DEo6nZVyEQ3XmPrtXamD', 'QmbSE9wMdyLtqSpJcp5WHHQAzMJjWDyZqBGMxx1hCBFTTt',\n 'QmcJurMQ5LmRT8P2g92i7dLmMp7h28P8kkPJ3pvLsUGUuT', 'QmPdDu9c5i4A9XhRXMWZaWw1uBPdPGU4FmSXRRRzBrcvCH', 'QmaGhZoJhufXUb26RFy8VNXfSnrTFd9PVkzSL2ENtYETFB', 'QmPseR1x9WKsocnXqbacTvEZyQwVcdYLbLsFd78U44TT7x',\n 'QmSZmmipeHyH9E1oCyZ7XMB5q6L8t3iCo7em5VS12tqrME', 'QmdUs3DUxrJBtKc2QU7Uu1hcw4cZsKDifDb43NsMsmMWYU', 'QmXsxzWCc3bEtytUgGHx9eorMUAeCGsudFhsR6v1TbqvfB', 'QmQ5AAH23EUGpShhnsmWhQt44DnTAzZ5rCy5ZXNwM7ys7w',\n 'QmPjk1HGXEvhzmJBED9CL5TC4QzBju8rGc33EeKdBNbBFZ', 'QmXfhKkGZSKU2a8Zb1z3JsaykCLeGCaeMA2ipydq6KdWCw', 'QmNy8pmg5hE9m9rHCZtmZgNDeYwpDpNk6GEYGjjCjHkHZJ', 'QmUFxK9QdQbRMbQfUxv3nv66XRdoEePaKazVjXuzwCdrMV',\n 'QmUMJkRPkQoFt3dAHPVAu2ogGFvswUQAXH38bzXLDFkc7Z', 'QmWrhGtxZUXKDSJsuoCkBLFfSNNy6pHD5gRnHLGNKhZGi8', 'Qmb7YAP3xcytesVbvezovg48R9E5nJ8XgAPzATf8sFQNkf', 'QmRdWrcxmqdsyZTnpRzBt3fPDnYycArqv1jJM87FEqJeay',\n 'QmSvMoy3SeAREVFTcr3LRzw2vHAq159nTjCrcXNLRSmAyk', 'QmSvof6bsEDAuMvvQgeCXFLhATfewz6uA9QwcUhEYy2AvY', 'QmYgqwNStNFuQBeXsjybtrgCswdzApm5Rm1s23JyEU9WfW', 'QmPvQCbgwBsgzagm7w13G4rxai1DXJhKrh14HKd5PQw72F',\n 'Qmb9QQCRuoRnu2YieCGM8H5Zf9XKdLGEmSJM8oCdvPqqrN', 'QmcTXbXZrwwt5KSVYHb9ffsJfc5D1LQb7KzLvHoatQy7tj', 'QmRWspnUxueYQqAEvSVb9JWPorGiMHKFB8aXGKLQJepjnM', 'QmeXdubuRVN3T4kh5QjjDWXDPegrChgS2DCXNLaxUYndsH',\n 'QmQtLz5n14mz2vHncQxQgtYFBjZR27vy3GtARaXRphuW2u', 'QmeBmXdDQpqwJaJhN7B4RYoQ5fJwC6mtQDK6hj4yghyVm1', 'Qmchuv1DxrQ1byEx1s2isumNnQoe1uDoq5RVRKPyFbRqMh', 'QmRWKEjcQQo73De2nHqJfz6hTkixaeiTcUkkCvxNoziTpv',\n 'QmcNhE5LGJ3okZKpsmbWHRsMrwooEGoTD5j3UCq592ep9N', 'QmezYGsfsN8UE98PPAafQHdRbVtAd5bMW8xHGmR9XeWXDC', 'QmYVzAikao1frNNH9yphk3w43p8yeo3vVyEEmYReeFYM2J', 'QmUqVej93jpSMDMAnEH6SVSZ6uTWzVn8uE3zZe2besb7g5',\n 'QmVrkEL7jTsnJVhSjHgpXQqf1tVZNBWhXqVHgKeGsP9nMC', 'Qmabyk6rg9YiaZor7XSAWy288H6Zmkj9m4crE6wQqo1XSJ', 'QmcMrXvn2VJQjh2HbKxeswDv6bXX4F1PKDRkbZ2xAzvpjv', 'QmaJ1SBMs9WW3Xfzcq8hgs5MY8NvSQGtGU8jGLGK2uMnAE',\n 'QmTqVH42fgCKHg6Fe3jq7WyT4A2hFTqmbucoqXeRviRGVH', 'QmZTenfCMdDDFZKDx4kDrgWQ7ZQjQ6oQkzSp6U4dfH3x7V', 'QmRFE81ssXNF5oFfFMhb2cZu27ZrSuSokzduWy4XEFzyZ4', 'QmVpqFAddYg7D1LxvJeW7rMhV9pTDc5WXZEmRUkmkfobe1',\n 'QmVnC6KpkunQKyUwJNgUzpE2F7sqhA6bV7jSkEBStGcz6p', 'QmTjd8GAaafuwybbwTRECkuhc6rM3WLg4qrodLdqc8RWAz', 'QmZPjBexGowyGDYLAFF7LjQuwYfTSi25XAY6rPeTDUvhDE', 'QmeCuEHVfYMr8ChxydaTnAQhPP21SfRNmhbrrnrmFujNP1',\n 'QmeQk3tUaABArcy4BF6LidJccKSUPsanSq1C5BUmKxwGSi', 'QmVGseX7vWj6HH3vybSSWLbUY2yeKgNwgnUYwriFHpZGsx', 'Qmeoc99jqk7TdazUBvP7VxZL6EP5AjjWxk3Ug8oJHapxhZ', 'QmZpVWbcMi78uSHLmwwdKmje35CkQ9sTWhUssEF5qR91LR',\n 'QmWUzizz8BdqphbYQtwTz9ah5RD3m3GqoALDHqc21SuCgt', 'QmXohnh1V9Bp5vDBwrjLZoHcfEdkqfR6CwXZDnfmwZ32mb', 'QmPbRi5FhBtsQa1xPAixKdpw3PeFeszYyUDvqw7W6kFmqL', 'QmS33cEacDz6iLr3JAqQXNHoPz21sdvWShxfWKJUtVJFuY',\n 'QmXjbU7kKPFeQHvMNwAr2D5DTaUJ1WVeELgqM3ATSZ4Ksr', 'QmRrDSDwrryprbm71xKAxTtP6iWH6aFf9KTDRRGRPiuAx2', 'QmWuZzVQULPWbPMgnc2uvoNicW2tA8YQ4Z9s1RXH2xmPPX', 'QmVDmCvVZ9me6bwVs9C329ibY9JgmBzZFnrkSyy2E4xtpd',\n 'QmahqoEozVJF62G6vYihJLEa1AMEbmfve9k2d7TWoErdcy', 'QmZLWPy49X96iHZSfg3peGC3CmP7s8c9CMC6mTJ3wmmdQA', 'Qmcfi34hZ2avTeB3rVPCpC5X1eayvYyiVv7m2TAijMBmb9', 'QmNtiB3EqUf1379cD6NVBv3J4VxeRsUWs2482ZyZxjY9gK',\n 'QmNdoAVYna3P8Mej5tQj7bo5CWQRUXwxAQgmnEwFHTqSV2', 'QmPsepvYGHSqR4oZoD5tjQgGRWcc1HE2kDBJgMhwxCSFdW', 'QmbmRRWKoV11bayoaRXCGgK16vTEd4KVnRSZqcyuoARCxW', 'QmbQg3RiG36gKxqDX64qveewm9iSo1RvL8p2CK9WbupWhX',\n 'QmcF2EBQrW9HUpYQ8i7mZ6bSGzSTxRgznjQpyPpo5U2T2m', 'QmUhu79Q9Gv3bMEtvqmyrC2R3eRHfVfg32zLTjrgqkqv6B', 'QmP8ij2oqUKZbVm1zvUiGYfd6tatgSz74Y63vuWtAWhksa', 'QmcrKzdYMReMDoAX2v69U2yPRYN1VSJyTqYLNVicQtcJkV',\n 'QmdQkSp8woSGtDEJx6xmfCpQc4dmpmC9ogxRggctfiusyu', 'QmZzchmmu9n6L7K59yTLvFnjU5LTyPF4BwgxjT2HYquNXW', 'QmdzuRTgi5bnGXaTmb3RYGcThA1qNR8Whx1tmtqrQDR3KK', 'QmPG37vSFgCmUcEbxdQDhdExb8JgZBXdUu7wuWH76gt3mX',\n 'QmQjHzyms2nvVissqcQdJ1h1NVJbqJcWWvMccEecxhqdTN', 'QmSPCZnFqQtPuu2wgHGqPqDP5M49TLEtL6x7atTadsGifx', 'QmTM1BwXakX9YkYxSaLdtyxdGYfc5mDYYERGphURZqHsuL', 'QmbYBQaxcv1yDnszX2RcZtLRK4rW2vJLeoyC7NY5hv9dcf',\n 'QmWg5TrRuvZeougTbNvcw8CGzvFUTBTVZbVr1mAFnpUoqs', 'QmcSyeHikrbSi4bje2JU79FepKS9mrTGHyfdgQNLrNcR9x', 'QmSycpuBiHdXaLmYRT6srCZcW8mcCHHevjyK36nY6q5jGm', 'QmaDXEGRGqQ6AiD61s4e56qcveaWHRobK447aNYd6Fciru',\n 'Qmd8h9crMyMwxheB8aZnbsCx9FQkVaV7Psd1NDvLCMRGWT', 'QmXRbBgvXhK7qNPZsmNsBnMpzwZTi5PFvHxZbPuR79hkrb', 'QmT4tMC1rDNHJY3h2fXhrPM6zb2MHWrhpWrusKP76p6KSQ', 'QmPxDqqghekjUxCbo3mz2KdxHk9z3BsyJKMcJFxpikNCk3',\n 'QmfSJiWBj1JVWYqi59USbJJwBZ9SAahvqu1iXSF7Pr2JVR', 'QmcDEJorb2v5JAFjpDVyM8twgFXdh9qBZFJ3uHKUZ2eMqK', 'QmbHqJWd5GPLiCA7UJedGzLzzkjMNm4WG7RVY7im911SAQ', 'QmarmJnvsaBnXqyS2piyY57VyYXjDYbhMHVomjeBzRV2K3',\n 'QmbYCnSCyokDrm82MVSmfZe6bumJ9PaNZWpPioX7uxKmbo', 'QmPgFMCk6EgKqEc3Hbcrmef9dHyk4fBe6uX3Sh6of67GZV', 'QmeZPLoLqAta24hnhfBxMSTTUzd6mxgEmHGBVJqZMxmsVr', 'QmTJSKFa22b5ecUXEkcgrXnKSEA6WtjpfDafWtpTQFAGGF',\n 'QmeHUBJZ3xJDPpDLJ7PMGubnuR9Fh5M3N1JMmnL22RUY42', 'QmYbk4GneMQHqftuDXvKh3xBL6qZiorpLeTrNsb3rbD9XR', 'QmbButr18q9TRDXkm3mcq1iRL2fYgvB8dimx6jCcZv9Pjq', 'QmbiVK9BKuVuEYQzJpuD1a1bBU9c6C7CgmJsTrjXbwgsWR',\n 'QmT6veKQdJdP8QjvqRFhAeuqr3aMgkkuWzRn81THxyMydW', 'QmPwE7CZDQ52ofjcGuMqP74VWNXWRbY2havDRUFd49LCnV', 'QmWqsrtgeSPY6jmxPNiuZ3589odmpR9omXZJ1cGWeLxFd5', 'QmUXMSfzLncfosqrwx5P7m8UDE4nqQ5RciZf9YXrcdoJvs',\n 'QmWHr8JZcBytZYHyhP123pTFUPyaK5ZwEAA64uj2y2WLzF', 'QmXrLBqEkTUH5K8zpp6z96tL8tC3uQvkYzSpWHawJDWyat', 'QmbbCEUjF3iQJDRyfireVh9bUq4EACveM67tJAwpr8MU3v', 'QmcKbbrrJSRh7QNzdg1y8GVZNNotr1ZgeMY6EmhwS4v1pC',\n 'QmdDqBiN3z626Qu5jA4St3vpbLScobEYhuQG4RCtXHzyif', 'QmRxab77NQPzSgLFLFE6BeaN36zPDgeL8gPSe2VdfkA6P6', 'Qma7TTNV7B8udte8YY1H4WD6nTBiiw1SAzMktWDEkkjDWu', 'QmVEz4JWsDWpQrvtZnVbyPVWfHaudCkXZwC1zcs1vQKWGC',\n 'QmWpJWo9QZkCr1i5BJvqSfrwemjaNeZ8hAR8oQ5WvNh5Cu', 'QmfPoCoNeXdEuknusGvEhvYSL5wUmsmFP19b1xQBqRjecf', 'QmPRL9h9m8EZDRn6QzMpP97CRHEnyuToHYyBa9Br7nrH18', 'QmVEAsxcZ1h2fqsXLCWNui2CJEAwkSb8LoerCLGN7wFQ8y',\n 'QmPDPFoeLzbPHamsVAPtSPy1wcBHggxj48iFbobAUp4gme', 'Qmckvd9arXsXxDRTarHu2KUBnNJqHmh4tnW3S1RZ1VT89t', 'QmPVJEh2LeJyF3PUJxmYAC3YLNDNQJ1CQ32fEzkxRsdiko', 'QmaqVzchifsjNx6Hy1RD6wb2Z36BiJNPzgSyisEBzh8T6Y',\n 'QmaBx9PXB2nRkDmKhFz1QJjRsuZshgcpzMMmhLt5T4qSyK', 'QmNPoTBjpm7UQkUWxyqb6B131qQkvCMqGyh3FU8rTyytk8', 'QmX8zsBdjvjNHXTXmNdSLBjMLceiQgA7xKn5HKUTdR1HRw', 'QmZxTrZPtwryMoZSUByFoLxMJ1hnAfLeXwesLsqQzUZzyV',\n 'QmVvEaCa6zRp8Z9YkkZVYBn2owSdwZxupEQacjfd1b2HA2', 'QmdjQEZzEhigLyfij6ZtJPPHUQR1TdsMRnGPgbxBRmw8op', 'QmYF7w26Fjufgr1DC85dM2bycqckSFdMENHnZMmPhQDtVi', 'QmVnxHS4rty2k9esBVjM54iBKj5EhroE62TBpFCQ7ZfSMB',\n 'QmbBUkh7bQdmH92NmaycDakjb6jU5mYJooU8nTDCwNEJzu', 'QmfJYmwnagstpwJWTc33AZsLoFVsy64TjXAhdaWjwys9Eu', 'QmSL23H5bR3JiqYzAYiXfbN6Yu7TH8QhE2dsggbeef4VBR', 'Qmbgzd2gfdpgBTEEHUjJdcJAtHzuuQxSVC13fDZPwQ3knx',\n 'QmeQBNX3PQvQMrj3VnbeELzbdWc5wGfQSAF8fXSNYoWrAg', 'Qmcm1bx64SHmiECXYs5TGTEp1gHeyEsBzp12y5Wr9Tduee', 'QmSzgefWsFPS3qwDvmyTLZV8P6ZBSEnW7yEejaooDKXXUi', 'QmaELnqmjdMPZ8HqsfW3tDuz4uGHUn9txJtJkkJHYSR9Lq',\n 'QmXwR5VjTSrKzjHzo2FrXnWS3GSo76oPVrd5WhV32sLvub', 'QmZVP8g8tSNDriRH32jKFrD34RfGro2xY29zwqtNkTqVz5', 'QmP211gF8mXzCGExNTNPr4a3Jn4MH7iZ5rr5oavpsYhx6Y', 'QmWE4zzqoXS9yRWFYokMb3pRdTeehRqBgEuqwMgRYka51R',\n 'QmUSyyUqCZnTQmFYwNUhWGYm4CSJCuGgxFCbnD1T8mRZrM', 'Qmb7jwtvCbPxfUZAhhqE5ng6et2ViYspQiArLqf6bUX1Nq', 'QmfSyDYeshn4Y5JyP3EzDEgsgL5Z8o4AouZWDfToJRJSmH', 'QmPobNHxbr6w2nmdTxFfoVH8FP6QTbgp8wUb6UtxAv8S1R',\n 'QmPXF8ffhF3zdS1Tng3rVN6FZfq3hxMch5BZcWWfnfEhSN', 'Qmc581Phidr2Jx2AYwtgjaYeDUrC2fWx7bowfq5hN8QRyz', 'QmZcMEbiiaTaHizVzJMEFhBHmwVpBt7RFcy4r62BAuJiga', 'Qmek8PjkwBA8x9zBLsrJPY1quKobbNLRXgkXN8F3iaHMx6',\n 'QmaCGKbEog5LmnVrNGj1QqauAqmYmtHLPdCgaw793JgvTv', 'QmQ6XBjWGQpep3NFhWeHn3p4KVvQxE4wG9vVRka8HNbnQr', 'Qme8RgJ2sp7qWNqTHbFHgbRejLqugjcbzG2LPjMVaEF5dD', 'QmeE6VgtuYCsMAmkoKzeeTpcDuhJnzBpL8yf4pKRGJB81g',\n 'QmPoREZCTBDWa8c14fyLTB2S8hVoZq2a7V7gH4cte4ES9D', 'QmfQuoGraszAniiXM3CF4ut78w6u73bowUZJDeT4ukrsZb', 'QmeMyKGcYLq7XLrQes42rSiWufA49x6DsWh8SMk8QZCXR6', 'QmcF7MsVaTtrGfeM9ZSq9PUSCoTT67k5B7J46L7CH6EhZR',\n 'QmZaqVVqLKD4SGFEDMYHCrPTcSVuduAwHN6Yjs6CMQhous', 'QmUwBcPAkyw2arFBD8Mqj6pJe1wgpSHE9NLSXFiEVQqcu9', 'QmaN7ky7s6zBdqWodEEqD4844ajqpobXCChwxccqS8rjWx', 'QmZECsRwqvdjbz7DYrZcZ9wno9RoBuNXX1GDWQNaC4icYq',\n 'QmY3xWVqdSm6XoHMvPYLamkHLC6VVRnupcp7FmZGqFwCL4', 'QmbGFhAS7YFp2ir2VXQBmcagB5nYfcbempcrj7U3ZS5ySx', 'QmczBQnqVcBs4tnY6nw8H6T2JNGMuCCtezWjbCY99Wokz7', 'QmadwMSsweUH26bAbrWvDPQGQHDy31ds2GCAU2KsasMZgQ',\n 'QmTBNvoUiwPw9HSUmy1qKCWPBKkBRAensgooYVqMmsviaE', 'QmRR7sPfkzuhYATFjGiLBVyo9QkBF35V9i8vk8exthuUoa', 'QmedEbGk9ZMD3pecvy5mc5S41cXxQb83cfkE7JQjNbJUb3', 'QmZaHP72PQ6UQ4BqW8rjLDN6BFmPX4U8S3P3PtMCjVDudP',\n 'Qmam4H1YnqinKNpwVMqFtVmtjCVm4YTvHNA1gucUoonrz5', 'Qmdgg92V3KbEuiJ1PLG7Huyph8SoYZNgonTCdbvvteERyF', 'Qmezb3CHKTtcxc7qxrFKkgsQKypC2pEiBw1M5jgD657tah', 'QmW6ZHWgtFpeY6P6BGvmQRNLbV5oCYBsK8z3pQju8L8AMQ',\n 'QmRj9wPp9dPxbDuUjStKArQB26X56qpXuUS2vAwLTMGTR1', 'QmNPJotqYDKtNG8XgHkbHamxm2QMdMn2FtFRrKy8t3fBhd', 'QmSXV1k82avNPDXYHRufH6RzBfyUSjnDmQoQ9idjx9bGHX', 'QmPGq6baCU7n5oq6c1wKfP4Scqg23Dfs7fwS3T8SDPpGTW',\n 'QmTeNQtppRWfQB2PQNohdx2PP9DAos1rKBqg7ypCrs6rXs', 'QmW9iAGca5Nst3Ck78RWs7PePgSB35mMvRKhFDdHM6CaZ6', 'QmcoEw3jgjT18PT6E4Jhf6AF2jWaqFK14T7cSZmbE5imTi', 'QmRgprite9rBeUfFCiCta42BvcjfqdpkAsXpguG5huz6RR',\n];\n","/**\n * The Metaquarium farm, in-house.\n *\n * Everything needed to turn a fish token id into asset URLs lives here as\n * DATA — no AWS API, no metadata round-trip, no third-party resolver on the\n * path to a rendered frame. `fishAssets(85)` is a pure function.\n *\n * Why this is not a client: the farm's AWS endpoint requires an allowlisted\n * `Origin` header that does not include idlescreens.com, and its media lives\n * behind gateways that have proven unreliable. Depending on either at render\n * time makes an ambient screen only as good as someone else's uptime. The\n * collection is closed (512, `totalSupply()`), so the mapping is a constant.\n *\n * Zero-dep on purpose — servers can validate fish references through the\n * `./manifest` subpath without pulling three.js.\n *\n * Source of truth and provenance: `idle-mono/docs/metaquarium-farm-api.md`.\n */\n\nimport { ASSET_CIDS } from './asset-cids';\n\nexport { ASSET_CIDS };\n\nexport const TOTAL_SUPPLY = 512;\n\nexport function isMintedId(id: number): boolean {\n return Number.isInteger(id) && id >= 1 && id <= TOTAL_SUPPLY;\n}\n\n/** ERC-721, Ethereum mainnet. Metadata is per-token but collapses to the four\n * directory CIDs in {@link BREEDS}, so `tokenURI` is never needed at runtime. */\nexport const CONTRACT = '0x680cCc4fE7aa62172D20899Ab87C5304545431CB';\n\n// ---------------------------------------------------------------------------\n// Breeds\n// ---------------------------------------------------------------------------\n\n/** Every breed the Metaquarium was designed around. Four were minted as the\n * 512-fish collection; the rest exist as finished models in the original\n * aquarium and are the natural stock for future tank life (props, NPCs,\n * wildlife that isn't a token). Kept here so the knowledge isn't stranded\n * in the source project. */\nexport type Breed =\n | 'betafish' | 'angelfish' | 'seahorse' | 'seaturtle' // minted\n | 'blowfish' | 'hackerfish' | 'glowfish' | 'babyfish' // designed, unminted\n | 'shark' | 'crab' | 'jellyfish' | 'dori';\n\nexport interface BreedInfo {\n breed: Breed;\n minted: boolean;\n /** Inclusive token range, minted breeds only. Ranges are contiguous and\n * in mint order — this is what makes id → metadata a pure function. */\n range?: readonly [number, number];\n count: number;\n /** Metadata directory CID (one per minted breed, holding every\n * `fish_<n>_of_the_metaquarium.json` in its range). */\n metadataCid?: string;\n /** Model basename in the original aquarium's NPC set, for unminted breeds\n * whose art exists but has no token behind it. */\n model?: string;\n}\n\nexport const BREEDS: readonly BreedInfo[] = [\n { breed: 'betafish', minted: true, range: [1, 256], count: 256,\n metadataCid: 'QmZSs1ZHsW4B4ZLip8N8EqdUaXCv5ra5M5bWz1RsFSkKb4', model: 'beta-fish' },\n { breed: 'angelfish', minted: true, range: [257, 456], count: 200,\n metadataCid: 'QmaQXcFof5qb6cigk4eo59zyMUdP4CV6pVAupPZys4jquT', model: 'angel-fish' },\n { breed: 'seahorse', minted: true, range: [457, 496], count: 40,\n metadataCid: 'Qme8G5eXaAkk1dzVr5VDt8sgauVez9kmxxDNaYbSQLyggx', model: 'sea-horse' },\n { breed: 'seaturtle', minted: true, range: [497, 512], count: 16,\n metadataCid: 'QmZTntNpvwxe8Mii7s1XTpTrbaci4ZgYUXs9sEbsBodtaF', model: 'sea-turtle' },\n { breed: 'blowfish', minted: false, count: 0, model: 'blow-fish' },\n { breed: 'hackerfish', minted: false, count: 0, model: 'hacker-fish' },\n { breed: 'glowfish', minted: false, count: 0, model: 'glow-fish' },\n { breed: 'babyfish', minted: false, count: 0, model: 'baby-fish' },\n { breed: 'shark', minted: false, count: 0, model: 'shark' },\n { breed: 'crab', minted: false, count: 0, model: 'crab' },\n { breed: 'jellyfish', minted: false, count: 0, model: 'jellyfish' },\n { breed: 'dori', minted: false, count: 0, model: 'dori' },\n];\n\n/** Breed of a minted token, or null when the id is outside 1..512. */\nexport function breedOf(id: number): Breed | null {\n if (!isMintedId(id)) return null;\n for (const b of BREEDS) {\n if (b.range && id >= b.range[0] && id <= b.range[1]) return b.breed;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Traits\n// ---------------------------------------------------------------------------\n\n/**\n * The twelve trait types every fish carries, in the order the metadata emits\n * them. Documented because several are steering-relevant and would otherwise\n * be rediscovered by reading JSON: `primary color`/`secondary color` are the\n * fish's own hexes (an authored palette the tank currently overrides with its\n * seeded coats), `eyes` is a 9-bit pixel mask, `track`/`voice` name the audio\n * the original aquarium played for that fish.\n */\nexport const TRAIT_TYPES = [\n 'breed', 'track', 'primary color', 'secondary color', 'eyes', 'voice',\n 'credit score', 'astrological sign', 'mystery digit', 'attack power',\n 'birthday', 'left finned',\n] as const;\nexport type TraitType = (typeof TRAIT_TYPES)[number];\n\n/** A fish's metadata as the farm publishes it. `token_id` is deliberately\n * absent — the live API does not return it; derive from `name`. */\nexport interface FishMetadata {\n name: string;\n description?: string;\n external_url?: string;\n image?: string;\n favicon?: string;\n transparent_icon?: string;\n /** GLB. Note the filename suffix differs by metadata generation — see\n * {@link fishAssets}, which builds the current one. */\n '3d'?: string;\n animation_url?: string;\n video_512?: string;\n born_in?: string;\n background_color?: string;\n attributes?: Array<{ trait_type: TraitType | string; value: string | number }>;\n}\n\n/** Token id from a metadata `name` (\"Fish 85 of the Metaquarium\" → 85). */\nexport function idFromName(name: string): number | null {\n const m = /Fish\\s+(\\d+)\\s+of the Metaquarium/i.exec(name);\n return m ? Number(m[1]) : null;\n}\n\n// ---------------------------------------------------------------------------\n// URL construction\n// ---------------------------------------------------------------------------\n\nexport type AssetKind = '3d' | 'image' | 'transparent_icon' | 'favicon' | 'video';\n\n/** Filename suffix per asset kind, current generation. (An earlier metadata\n * batch named the model `_glb.glb`; the directories themselves hold\n * `_3d.glb`, which is what these build.) */\nconst SUFFIX: Record<AssetKind, string> = {\n '3d': '_3d.glb',\n image: '_image.jpg',\n transparent_icon: '_transparent_icon.png',\n favicon: '_favicon.png',\n video: '_video.mp4',\n};\n\n/** The asset directory CID for a token, or null when the id isn't minted. */\nexport function assetCid(id: number): string | null {\n return isMintedId(id) ? (ASSET_CIDS[id - 1] ?? null) : null;\n}\n\n/** `ipfs://<cid>/<file>` for one asset — the scheme the tank's gateway ladder\n * resolves. Pure: no fetch, no metadata lookup. */\nexport function fishAsset(id: number, kind: AssetKind = '3d'): string | null {\n const cid = assetCid(id);\n return cid ? `ipfs://${cid}/fish_${id}_of_the_metaquarium${SUFFIX[kind]}` : null;\n}\n\n/** Every asset URL for a fish, plus its breed — one call, no network. */\nexport function fishAssets(id: number): {\n id: number; breed: Breed; cid: string;\n model: string; image: string; icon: string; favicon: string; video: string;\n} | null {\n const cid = assetCid(id);\n const breed = breedOf(id);\n if (!cid || !breed) return null;\n return {\n id, breed, cid,\n model: fishAsset(id, '3d')!,\n image: fishAsset(id, 'image')!,\n icon: fishAsset(id, 'transparent_icon')!,\n favicon: fishAsset(id, 'favicon')!,\n video: fishAsset(id, 'video')!,\n };\n}\n\n/** Canonical metadata URL for a token — the range formula, no RPC. Useful for\n * traits/description; never needed just to render. */\nexport function fishMetadataUrl(id: number): string | null {\n const b = BREEDS.find((x) => x.breed === breedOf(id));\n return b?.metadataCid\n ? `ipfs://${b.metadataCid}/fish_${id}_of_the_metaquarium.json`\n : null;\n}\n\n/** All minted ids of a breed — the seam for \"a tank of seahorses\". */\nexport function idsOfBreed(breed: Breed): number[] {\n const b = BREEDS.find((x) => x.breed === breed);\n if (!b?.range) return [];\n const out: number[] = [];\n for (let i = b.range[0]; i <= b.range[1]; i++) out.push(i);\n return out;\n}\n","/** Ordered gateway candidates. dweb.link stays primary (flaky but usually\n * fast); our own node at hermosaai.com is second — flip it to the front\n * once the tunnel is verified serving /ipfs/ (it timed out on the\n * 2026-08-15 check); ipfs.io is the last resort. Each is tried in order\n * with a per-gateway timeout by the tank's template loader (MQ21). */\nexport const IPFS_GATEWAYS = [\n 'https://dweb.link/ipfs/',\n 'https://hermosaai.com/ipfs/',\n 'https://ipfs.io/ipfs/',\n];\n\nexport function resolveIpfsUrl(url: string): string {\n return resolveIpfsUrls(url)[0]!;\n}\n\n/** All gateway candidates for a URL — one entry for non-ipfs URLs. */\nexport function resolveIpfsUrls(url: string): string[] {\n if (!url.startsWith('ipfs://')) return [url];\n const stripped = url.slice('ipfs://'.length);\n return IPFS_GATEWAYS.map((g) => `${g}${stripped}`);\n}\n\nexport interface FishEntry {\n id: number;\n name: string;\n breed: string;\n ipfs3d: string;\n localGlb: string;\n}\n\nexport const FISH_CATALOG: FishEntry[] = [\n { id: 257, name: 'Fish 257', breed: 'angelfish', ipfs3d: 'ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb', localGlb: '/assets/metaquarium/fish-257-angelfish.glb' },\n { id: 258, name: 'Fish 258', breed: 'angelfish', ipfs3d: 'ipfs://QmUZGF3ge3d9rzrtxrD6V4qx2gLtGeeNLuCb8fQeNyUkwJ/fish_258_of_the_metaquarium_3d.glb', localGlb: '' },\n { id: 259, name: 'Fish 259', breed: 'angelfish', ipfs3d: 'ipfs://QmfBBnNrVrkffMKoESvq3cB6nAWGpfMPjduTgw1unahvPf/fish_259_of_the_metaquarium_3d.glb', localGlb: '' },\n { id: 100, name: 'Fish 100', breed: 'betafish', ipfs3d: 'ipfs://Qmb5Uu8u154QTzoGpB6ypwVfPZ8NUsU519tQmrgE8yQrWV/fish_100_of_the_metaquarium_3d.glb', localGlb: '/assets/metaquarium/fish-100-betafish.glb' },\n { id: 457, name: 'Fish 457', breed: 'seahorse', ipfs3d: 'ipfs://QmVvEaCa6zRp8Z9YkkZVYBn2owSdwZxupEQacjfd1b2HA2/fish_457_of_the_metaquarium_3d.glb', localGlb: '' },\n { id: 497, name: 'Fish 497', breed: 'seaturtle', ipfs3d: 'ipfs://QmTBNvoUiwPw9HSUmy1qKCWPBKkBRAensgooYVqMmsviaE/fish_497_of_the_metaquarium_3d.glb', localGlb: '' },\n];\n\nexport const DEFAULT_FISH = FISH_CATALOG[0]!;\n\nimport { BREEDS, breedOf, fishAsset, TOTAL_SUPPLY } from './farm';\n\nexport interface FishMixEntry {\n id: number;\n url: string;\n count: number;\n}\n\nexport interface FishMixResult {\n entries: FishMixEntry[];\n problems: string[];\n}\n\n/**\n * Parse the `fishMix` DSL: comma-separated `id[:count]` where `id` is a\n * catalog token id (`257`) or breed alias (`betafish`; picks the breed's\n * first entry). Against the default catalog, any minted id 1–512 also\n * resolves via the in-house farm table. A caller-supplied catalog is a\n * closed world — ids not in it are problems, not IPFS fallbacks. Counts\n * are absolute; the tank clamps the expanded total to its tier cap. Raw\n * URLs are deliberately NOT accepted — `:` and `,` stay unambiguous, the\n * validation surface stays finite, and custom URLs remain `fishUrl`'s job\n * (single-breed mode).\n *\n * Zero-dep and pure, so the Worker (via the manifest subpath), the\n * playground, and the tank all validate with the same code. Never throws:\n * bad tokens land in `problems` and good ones still parse, so one typo\n * degrades a mix instead of blanking the tank.\n */\nexport function parseFishMix(\n mix: string,\n catalog: FishEntry[] = FISH_CATALOG,\n): FishMixResult {\n const entries: FishMixEntry[] = [];\n const problems: string[] = [];\n for (const rawToken of mix.split(',')) {\n const token = rawToken.trim();\n if (token === '') continue;\n const [idRaw, countRaw, ...extra] = token.split(':');\n if (extra.length > 0) {\n problems.push(`\"${token}\": too many ':' — expected id[:count]`);\n continue;\n }\n const key = (idRaw ?? '').trim().toLowerCase();\n let fish = /^\\d+$/.test(key)\n ? catalog.find((f) => f.id === Number(key))\n : catalog.find((f) => f.breed.toLowerCase() === key);\n // Farm fallback is only for the default catalog. A custom catalog is a\n // closed world (playground offline e2e, a future pack) — leaking a minted\n // id out to IPFS would silently undo that constraint.\n if (!fish && catalog === FISH_CATALOG && /^\\d+$/.test(key)) {\n const n = Number(key);\n const url = fishAsset(n, '3d');\n const breed = breedOf(n);\n if (url && breed) fish = { id: n, name: `Fish ${n}`, breed, ipfs3d: url, localGlb: '' };\n }\n if (!fish) {\n problems.push(\n /^\\d+$/.test(key)\n ? catalog === FISH_CATALOG\n ? `\"${key}\": not a minted token id (1-${TOTAL_SUPPLY})`\n : `\"${key}\": not a catalog id`\n : `\"${key}\": not a breed (${BREEDS.filter((b) => b.minted).map((b) => b.breed).join(', ')})`,\n );\n continue;\n }\n let count = 1;\n if (countRaw !== undefined) {\n const n = Number(countRaw.trim());\n if (!Number.isInteger(n) || n < 1 || n > 24) {\n problems.push(`\"${token}\": count must be an integer 1-24`);\n continue;\n }\n count = n;\n }\n entries.push({ id: fish.id, url: fish.ipfs3d, count });\n }\n return { entries, problems };\n}\n\n/** Expand a parsed mix into the ordered per-slot URL list the tank spawns\n * from: `\"257:2,100:1\"` → [url257, url257, url100]. Slot order IS the\n * DSL order, so fish 0..N are stable for a given string. */\nexport function expandFishMix(entries: FishMixEntry[], cap: number): string[] {\n const urls: string[] = [];\n for (const e of entries) {\n for (let i = 0; i < e.count && urls.length < cap; i++) urls.push(e.url);\n }\n return urls;\n}\n","import type { ParamSpace, ParamValue, SaverManifest } from '@idle-screens/core';\nimport { ENVIRONMENT_NAMES } from './environments';\nimport { SWIM_STYLE_NAMES } from './swim';\n\n/**\n * Zero-dependency manifest module (type-only imports). The channel server\n * imports `@idle-screens/saver-metaquarium/manifest` to validate published\n * params against this paramSpace without pulling three.js into the Worker.\n */\nexport const METAQUARIUM_PARAMS = {\n /** Camera orbit angle, degrees. */\n cameraAzimuth: { type: 'number', default: 35, min: 0, max: 360, ease: 'smooth' },\n /** Camera height angle, degrees above the waterline plane. */\n cameraElevation: { type: 'number', default: 15, min: -5, max: 60, ease: 'smooth' },\n /** Camera distance from tank center, world units. */\n cameraDistance: { type: 'number', default: 110, min: 80, max: 400, ease: 'smooth' },\n /** Continuous orbit speed, degrees/second. Zero by default: the tank is\n * still, letting the fish movement carry the scene. Steer up for an orbit. */\n autoRotate: { type: 'number', default: 0, min: 0, max: 12, ease: 'smooth' },\n /** Visible fish. Default 1 = hero mode: one fish center-stage in its own\n * tank; the pool grows on demand when steered up (never shrinks). */\n fishCount: { type: 'number', default: 1, min: 1, max: 24, ease: 'step' },\n /** Swim time-scale multiplier. */\n swimSpeed: { type: 'number', default: 1, min: 0.2, max: 3, ease: 'smooth' },\n /** Water/atmosphere color (background + fog). */\n fogColor: { type: 'color', default: '#030009', ease: 'smooth' },\n /** GLB model URL for the fish (supports ipfs:// scheme). Package default\n * is the IPFS hero fish so npm / channel hosts work without playground\n * static assets; the playground overrides this to a local GLB. */\n fishUrl: { type: 'string', default: 'ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb' },\n /** Mixed population DSL: comma-separated `id[:count]` of a minted token id\n * (1-512, default catalog) or breed name — e.g. \"257:3,100:2,seaturtle:1\".\n * A custom catalog is a closed world (no farm/IPFS fallback). Counts are\n * absolute; the expanded total clamps to the device tier's fish cap.\n * Non-empty mix OVERRIDES fishUrl and fishCount; empty string = single-\n * breed mode. Unknown ids/bad counts degrade (good tokens still parse);\n * raw URLs are not accepted here — use fishUrl for a custom single-breed GLB. */\n fishMix: { type: 'string', default: '', ease: 'step' },\n /** Fog start distance, world units. Default matches the tank's original\n * hardcoded Fog(60, 500) — steering below pulls the murk close. */\n fogNear: { type: 'number', default: 60, min: 20, max: 200, ease: 'smooth' },\n /** Fog full-opacity distance. Kept under the camera far plane (1200);\n * the tank enforces far > near + 20. */\n fogFar: { type: 'number', default: 500, min: 120, max: 1100, ease: 'smooth' },\n /** Plankton mote density, 0-1 of the device tier's mote budget. Default 0\n * = off, so the baseline look is untouched until steered. */\n moteDensity: { type: 'number', default: 0, min: 0, max: 1, ease: 'smooth' },\n /** Mote tint. */\n moteColor: { type: 'color', default: '#7fd6ff', ease: 'smooth' },\n /** Floor disc color. Default is the original hardcoded navy. */\n floorColor: { type: 'color', default: '#0a1d33', ease: 'smooth' },\n /** Where the Draco decoder lives, for the many Metaquarium models that are\n * Draco-compressed. Empty = the copy shipped beside this package (no CDN,\n * no network beyond your own host). Override when a host serves the\n * decoder from its own static path. */\n dracoPath: { type: 'string', default: '', ease: 'step' },\n /** The ROOM: a named place rather than thirty numbers. `void` is exactly\n * the pre-environment scene, so the default changes nothing. Each other\n * value adds a water ceiling, a terrain silhouette and light shafts,\n * tier-budgeted. Palette params (fog, floor colour, motes) stay yours —\n * an environment never overrides them. */\n environment: { type: 'enum', default: 'void', options: [...ENVIRONMENT_NAMES], ease: 'step' },\n /** Override the environment's terrain. `auto` follows the environment. */\n floorKind: { type: 'enum', default: 'auto', options: ['auto', 'flat', 'dunes', 'ridges', 'basin'], ease: 'step' },\n /** Water-ceiling height. -1 = follow the environment; fish swim to y=72.\n * STEP, not smooth: -1 is a sentinel, so a ramp from -1 to a real height\n * passes through negatives that read as \"auto\" — the ceiling would jump\n * rather than glide. A value you cannot interpolate through must not\n * advertise that it can. */\n waterY: { type: 'number', default: -1, min: -1, max: 220, ease: 'step' },\n /** Light-shaft strength. -1 = follow the environment, 0 = off. Step for the\n * same sentinel reason: ramping -1 → 0 would read as FULL strength until it\n * snapped off. */\n rayStrength: { type: 'number', default: -1, min: -1, max: 1, ease: 'step' },\n /** How the fish move. `loop` is exactly the pre-style behaviour, so the\n * default changes nothing. A small named set on purpose: a silhouette of\n * movement you can name is one you can choose from. */\n swimStyle: { type: 'enum', default: 'loop', options: [...SWIM_STYLE_NAMES], ease: 'step' },\n /** Per-fish spread: 0 a uniform shoal, 1 every fish visibly its own animal\n * (±40% speed, ±25% size, own phase). The uniqueness dial — one number\n * instead of per-fish values nobody wants to author. */\n swimVariance: { type: 'number', default: 0, min: 0, max: 1, ease: 'smooth' },\n /** Procedural body yaw for models that carry NO animation clip — most of\n * the breed library. Distance-driven like the tail beat, so it speeds up\n * with the fish and stays frame-addressable. Clipped models ignore it:\n * their own clip is the better animation.\n *\n * Defaults to 0, off, even though a rigidly gliding fish is the worse\n * look. Every param this saver has added defaults to the previous\n * behaviour, and a scene already on someone's wall should not start moving\n * differently because a dependency was bumped. 0.3–0.4 is the recommended\n * value for a clip-less cast — the studio swim variants all set it. */\n bodyWiggle: { type: 'number', default: 0, min: 0, max: 1, ease: 'smooth' },\n} satisfies ParamSpace;\n\n/** The original's Miami-Vice body palette (scss-variables.ts) — seeded fish\n * coats draw from this. Zero-dep so the server/manifest lane can surface it. */\nexport const MIAMI_VICE_COLORS = [\n '#ff6ad5', // Hot Pink\n '#c774e8', // Light Purple\n '#ad8cff', // Lavender\n '#8795e8', // Periwinkle Blue\n '#94d0ff', // Light Sky Blue\n '#4fb4f4', // Azure\n '#00a6fb', // Bright Blue\n '#0085a1', // Cerulean\n '#0b3d91', // Yale Blue\n '#1c1c1c', // Off Black\n];\n\n/** The original's emissive/glow palette (`GLOW-*` parts, bloom layer). */\nexport const BLOOM_COLORS = [\n '#FF69B4', // Hot Pink\n '#FFFF00', // Yellow\n '#00FFFF', // Cyan\n '#00FF00', // Lime\n '#FF4500', // OrangeRed\n '#7FFF00', // Chartreuse\n];\n\nexport const metaquariumManifest: SaverManifest = {\n id: 'metaquarium',\n label: 'Metaquarium',\n description:\n 'A living fish tank: skinned GLB fish swim seeded Catmull-Rom paths through a dark, fogged aquarium.',\n minBackend: 'webgl2',\n costTier: 'medium',\n motionIntensity: 'calm',\n reducedMotionFallback: 'static',\n timeModel: 'closed-form',\n palette: ['#030009', '#ff6ad5', '#94d0ff', '#00a6fb', '#00FFFF'],\n paramSpace: METAQUARIUM_PARAMS,\n attribution: {\n source: 'Metaquarium (metaquarium.xyz) — original artwork and fish models by Shawn Partridge',\n license: 'MIT port of first-party artwork',\n url: 'https://metaquarium.xyz',\n },\n a11y: {\n flashSafe: true,\n notes: 'Slow ambient swim in a dark fogged tank; no strobing.',\n },\n};\n\n/** Options for {@link createMetaquarium}-style variants: a distinct id/label and\n * overridden param defaults (e.g. a farm-connected tank). */\nexport interface MetaquariumOptions {\n /** Override the fish catalog fishMix ids resolve against — the seam the\n * playground uses to point ids at bundled local GLBs (offline e2e), and\n * the future farm/pack extension point. */\n catalog?: import('./ipfs').FishEntry[];\n id?: string;\n label?: string;\n params?: Partial<Record<keyof typeof METAQUARIUM_PARAMS, ParamValue>>;\n}\n\n/** Clone a paramSpace with default overrides; unknown keys are ignored. */\nexport function withDefaults(\n space: ParamSpace,\n overrides?: Record<string, ParamValue>,\n): ParamSpace {\n if (!overrides) return space;\n const out: ParamSpace = {};\n for (const [k, def] of Object.entries(space)) {\n const o = overrides[k];\n out[k] = o === undefined ? def : { ...def, default: o };\n }\n return out;\n}\n\n/** Clone the metaquarium paramSpace with per-variant default overrides. */\nexport function paramSpaceWith(overrides: MetaquariumOptions['params']): ParamSpace {\n return withDefaults(METAQUARIUM_PARAMS, overrides as Record<string, ParamValue> | undefined);\n}\n\n/**\n * Defensive read of a tracked number param. The server validates nothing on\n * the classic steering lane (any path, any value reaches the saver), and MCP\n * harnesses with untyped `value` params stringify numbers — so \"22\" arrives\n * where 22 was meant. Finite numbers pass; finite numeric strings coerce;\n * anything else falls back to the def's default; the result clamps to the\n * def's min/max. Zero-dep on purpose: hosts can reuse it to sanitize.\n */\nexport function coerceNum(\n def: { default?: ParamValue; min?: number; max?: number } | undefined,\n v: ParamValue | undefined,\n): number {\n let n: number | null = null;\n if (typeof v === 'number' && Number.isFinite(v)) n = v;\n else if (typeof v === 'string' && v.trim() !== '') {\n const parsed = Number(v);\n if (Number.isFinite(parsed)) n = parsed;\n }\n if (n === null) {\n const d = def?.default;\n if (typeof d === 'number' && Number.isFinite(d)) n = d;\n else if (typeof d === 'string' && d.trim() !== '' && Number.isFinite(Number(d))) n = Number(d);\n else n = 0;\n }\n if (def?.min !== undefined && n < def.min) n = def.min;\n if (def?.max !== undefined && n > def.max) n = def.max;\n return n;\n}\n\nexport * from './farm';\nexport { parseFishMix, expandFishMix, type FishMixEntry, type FishMixResult, type FishEntry, FISH_CATALOG } from './ipfs';\n\nexport * from './environments';\nexport * from './swim';\n"],"mappings":";AA0BO,IAAM,cAAoC,CAAC,QAAQ,SAAS,UAAU,OAAO;AAsC7E,IAAM,eAA6C;AAAA,EACxD,EAAE,MAAM,QAAQ,OAAO,QAAQ,OAAO,MAAM,OAAO,QAAQ,MAAM,KAAK;AAAA,EACtE;AAAA,IAAE,MAAM;AAAA,IAAS,OAAO;AAAA,IAAS,OAAO;AAAA,IAAM,OAAO;AAAA,IACnD,MAAM,EAAE,UAAU,MAAM,OAAO,WAAW,GAAG,KAAK;AAAA,EAAE;AAAA,EACtD;AAAA,IAAE,MAAM;AAAA,IAAQ,OAAO;AAAA,IAAQ,OAAO,EAAE,GAAG,KAAK,OAAO,WAAW,SAAS,KAAK;AAAA,IAC9E,OAAO;AAAA,IAAS,MAAM,EAAE,UAAU,KAAK,OAAO,WAAW,GAAG,IAAI;AAAA,EAAE;AAAA,EACpE;AAAA,IAAE,MAAM;AAAA,IAAQ,OAAO;AAAA,IAAe,OAAO,EAAE,GAAG,KAAK,OAAO,WAAW,SAAS,IAAI;AAAA,IACpF,OAAO;AAAA,IAAU,MAAM,EAAE,UAAU,KAAK,OAAO,WAAW,GAAG,IAAI;AAAA,EAAE;AAAA,EACrE;AAAA,IAAE,MAAM;AAAA,IAAO,OAAO;AAAA,IAAa,OAAO,EAAE,GAAG,KAAK,OAAO,WAAW,SAAS,KAAK;AAAA,IAClF,OAAO;AAAA,IAAQ,MAAM,EAAE,UAAU,KAAK,OAAO,WAAW,GAAG,IAAK;AAAA,EAAE;AAAA,EACpE;AAAA,IAAE,MAAM;AAAA,IAAQ,OAAO;AAAA,IAAqB,OAAO;AAAA,IAAM,OAAO;AAAA,IAC9D,MAAM,EAAE,UAAU,MAAM,OAAO,WAAW,GAAG,KAAK;AAAA,EAAE;AAAA,EACtD;AAAA,IAAE,MAAM;AAAA,IAAU,OAAO;AAAA,IAAU,OAAO,EAAE,GAAG,KAAK,OAAO,WAAW,SAAS,KAAK;AAAA,IAClF,OAAO;AAAA,IAAS,MAAM,EAAE,UAAU,MAAM,OAAO,WAAW,GAAG,IAAI;AAAA,EAAE;AAAA,EACrE;AAAA,IAAE,MAAM;AAAA,IAAY,OAAO;AAAA,IAAY,OAAO;AAAA,IAAM,OAAO;AAAA,IACzD,MAAM,EAAE,UAAU,KAAK,OAAO,WAAW,GAAG,KAAK;AAAA,EAAE;AACvD;AAEO,IAAM,oBACX,aAAa,IAAI,CAAC,MAAM,EAAE,IAAI;AAEzB,SAAS,cAAc,MAAiC;AAC7D,SAAO,aAAa,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,KAAK,aAAa,CAAC;AACpE;AAcO,IAAM,aAAa,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,EAAE;AAIjD,IAAM,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE;AAYxC,SAAS,iBAAiB,QAAgB,QAE/C;AACA,MAAI,OAAO;AACX,QAAM,QAAQ,OAAO,UAAU,QAAQ,QAAQ,WAAW;AAC1D,MAAI,MAAO,SAAQ,WAAW;AAC9B,MAAI,WAAW;AACf,MAAI,OAAO,SAAS,QAAQ,OAAO,GAAG;AACpC,eAAW,QAAQ,WAAW,OAAO,UAAU,OAAO,UAAU;AAAA,EAClE;AACA,SAAO,EAAE,OAAO,MAAM,OAAO,SAAS;AACxC;;;ACxEO,IAAM,cAAwC;AAAA,EACnD,EAAE,MAAM,QAAQ,OAAO,QAAQ,UAAU,GAAG,MAAM,QAAQ,QAAQ,GAAG,OAAO,GAAG,WAAW,OAAO,QAAQ,EAAE;AAAA,EAC3G,EAAE,MAAM,UAAU,OAAO,UAAU,UAAU,GAAG,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK,WAAW,MAAM,QAAQ,EAAE;AAAA,EAClH,EAAE,MAAM,SAAS,OAAO,SAAS,UAAU,MAAM,MAAM,OAAO,QAAQ,GAAG,OAAO,MAAM,WAAW,OAAO,QAAQ,IAAI;AAAA,EACpH,EAAE,MAAM,SAAS,OAAO,SAAS,UAAU,KAAK,MAAM,QAAQ,QAAQ,GAAG,OAAO,KAAK,WAAW,OAAO,QAAQ,KAAK;AAAA,EACpH,EAAE,MAAM,UAAU,OAAO,UAAU,UAAU,MAAM,MAAM,OAAO,QAAQ,GAAG,OAAO,MAAM,WAAW,OAAO,QAAQ,EAAE;AAAA,EACpH,EAAE,MAAM,UAAU,OAAO,iBAAiB,UAAU,KAAK,MAAM,SAAS,QAAQ,GAAG,OAAO,KAAK,WAAW,OAAO,QAAQ,EAAE;AAAA,EAC3H,EAAE,MAAM,WAAW,OAAO,mBAAmB,UAAU,KAAK,MAAM,WAAW,QAAQ,GAAG,OAAO,MAAM,WAAW,OAAO,QAAQ,EAAE;AACnI;AAEO,IAAM,mBAAyC,YAAY,IAAI,CAAC,MAAM,EAAE,IAAI;AAE5E,SAAS,YAAY,MAA6B;AACvD,SAAO,YAAY,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,KAAK,YAAY,CAAC;AAClE;AASO,SAAS,SAAS,OAAe,MAAsB;AAC5D,QAAM,IAAI,KAAK,KAAK,QAAQ,KAAK,QAAQ,OAAO,KAAK,IAAI;AACzD,SAAO,IAAI,KAAK,MAAM,CAAC;AACzB;AAcO,SAAS,cAAc,OAAe,UAE3C;AACA,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,UAAU,KAAK,SAAS,OAAO,CAAC,IAAI,OAAO,MAAM;AAAA,IACjD,UAAU,KAAK,SAAS,OAAO,CAAC,IAAI,OAAO,MAAM;AAAA;AAAA;AAAA,IAGjD,OAAO,SAAS,OAAO,CAAC,IAAI,KAAK,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMtC,QAAQ,SAAS,OAAO,CAAC;AAAA,EAC3B;AACF;AAMO,IAAM,cAAc;AAK3B,IAAM,uBAAuB;AAUtB,SAAS,cACd,OAAe,OAAe,UAAkB,YAAY,sBAChB;AAC5C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC;AACjE,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,KAAK,MAAM,QAAQ,IAAI;AACnC,QAAM,IAAI,OAAO,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;AAIzD,QAAM,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC;AAKjC,QAAM,QAAQ,KAAK,IAAI,cAAc,KAAM,YAAY,IAAK,IAAI;AAKhE,QAAM,MAAM,QAAQ;AACpB,SAAO;AAAA,IACL,OAAO,OAAO,OAAO,KAAK,KAAK,SAAS,SAAS,OAAO,CAAC,IAAI,OAAO,MAAM;AAAA,IAC1E,KAAK,SAAS,OAAO,CAAC,IAAI,OAAO,cAAc;AAAA,IAC/C,MAAM,MAAM,cAAc,MAAM,SAAS,OAAO,CAAC,IAAI,cAAc,MAAM;AAAA,EAC3E;AACF;AAQO,SAAS,gBAAgB,OAAe,UAE7C;AACA,MAAI,OAAO,GAAG,KAAK,GAAG,OAAO;AAC7B,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;AACjC,UAAM,IAAI,cAAc,GAAG,OAAO,QAAQ;AAC1C,WAAO,KAAK,IAAI,MAAM,KAAK,IAAI,EAAE,IAAI,CAAC;AACtC,SAAK,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;AAChC,WAAO,KAAK,IAAI,MAAM,KAAK,IAAI,EAAE,IAAI,CAAC;AAAA,EACxC;AACA,SAAO,EAAE,MAAM,IAAI,KAAK;AAC1B;AAIO,SAAS,UAAU,MAAoD;AAC5E,UAAQ,MAAM;AAAA;AAAA;AAAA;AAAA,IAIZ,KAAK;AAAS,aAAO,EAAE,IAAI,GAAG,IAAI,KAAK;AAAA,IACvC,KAAK;AAAW,aAAO,EAAE,IAAI,KAAK,IAAI,EAAE;AAAA,IACxC,KAAK;AAAO,aAAO,EAAE,IAAI,KAAK,IAAI,IAAI;AAAA,IACtC;AAAS,aAAO;AAAA,EAClB;AACF;;;AChLO,IAAM,aAAgC;AAAA,EAC3C;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAAkD;AAAA,EAAkD;AAAA,EAAkD;AAAA,EACtJ;AAAA,EAA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","names":[]}
@@ -96,11 +96,11 @@ function swimPoseAtDistance(plan, dist) {
96
96
  function qualityFor(tier) {
97
97
  switch (tier) {
98
98
  case "high":
99
- return { maxPixelRatio: 1.25, antialias: true, fishCap: 24, pixelBudget: 18e5, moteCap: 400 };
99
+ return { maxPixelRatio: 1.25, antialias: true, fishCap: 24, pixelBudget: 18e5, moteCap: 400, envBudget: 3 };
100
100
  case "standard":
101
- return { maxPixelRatio: 1, antialias: true, fishCap: 16, pixelBudget: 12e5, moteCap: 250 };
101
+ return { maxPixelRatio: 1, antialias: true, fishCap: 16, pixelBudget: 12e5, moteCap: 250, envBudget: 2 };
102
102
  default:
103
- return { maxPixelRatio: 1, antialias: false, fishCap: 8, pixelBudget: 9e5, moteCap: 120 };
103
+ return { maxPixelRatio: 1, antialias: false, fishCap: 8, pixelBudget: 9e5, moteCap: 120, envBudget: 1 };
104
104
  }
105
105
  }
106
106
  function effectivePixelRatio(width, height, dpr, q) {
@@ -130,4 +130,4 @@ export {
130
130
  effectivePixelRatio,
131
131
  probeSoftwareGL
132
132
  };
133
- //# sourceMappingURL=chunk-F74UWBMY.js.map
133
+ //# sourceMappingURL=chunk-UXMO57ND.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plan.ts","../src/quality.ts"],"sourcesContent":["import type { Rng } from '@idle-screens/core';\n\nexport interface TankBounds {\n radius: number;\n yMin: number;\n yMax: number;\n}\n\n/**\n * Catmull-Rom spline swim: a seeded closed loop of waypoints with arc-length\n * parameterization. Pose is a pure function of (plan, distance), so\n * `renderFrame(t, seed)` stays frame-addressable and scrubbing works.\n */\n\nexport interface SwimPlan {\n /** Closed-loop control points. */\n points: Array<[number, number, number]>;\n /** Arc-length lookup: cumulative length at N uniform spline params. */\n arc: Float32Array;\n totalLength: number;\n /** Cruise speed, units/sec, before the swimSpeed param scales it. */\n cruise: number;\n /** Speed-wobble harmonics (closed-form integral → distance). */\n wobble: Array<{ amp: number; w: number; phase: number }>;\n}\n\nexport interface SwimPose {\n x: number;\n y: number;\n z: number;\n /** Unit forward tangent — the fish noses along this. */\n fx: number;\n fy: number;\n fz: number;\n /** Bank roll (radians): lean into horizontal turns. */\n roll: number;\n /** Distance travelled (drives the tail-beat clip phase). */\n dist: number;\n}\n\nconst ARC_SAMPLES = 1024;\n\nfunction catmullRom(\n p0: number,\n p1: number,\n p2: number,\n p3: number,\n u: number,\n): number {\n const u2 = u * u;\n const u3 = u2 * u;\n return (\n 0.5 *\n (2 * p1 +\n (-p0 + p2) * u +\n (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 +\n (-p0 + 3 * p1 - 3 * p2 + p3) * u3)\n );\n}\n\n/** Point on the closed loop at global param `g` ∈ [0, n). */\nfunction splineAt(points: SwimPlan['points'], g: number): [number, number, number] {\n const n = points.length;\n const i = Math.floor(g) % n;\n const u = g - Math.floor(g);\n const p0 = points[(i - 1 + n) % n]!;\n const p1 = points[i]!;\n const p2 = points[(i + 1) % n]!;\n const p3 = points[(i + 2) % n]!;\n return [\n catmullRom(p0[0], p1[0], p2[0], p3[0], u),\n catmullRom(p0[1], p1[1], p2[1], p3[1], u),\n catmullRom(p0[2], p1[2], p2[2], p3[2], u),\n ];\n}\n\n/**\n * Compile a seeded itinerary through the tank volume. Waypoints alternate\n * around the ring (angle jitter, radius breathing, depth changes) so the\n * loop reads as purposeful wandering, never a circle.\n */\nexport function compileSwimPlan(rng: Rng, bounds: TankBounds): SwimPlan {\n const n = 16 + rng.int(0, 4);\n const points: SwimPlan['points'] = [];\n const ySpan = bounds.yMax - bounds.yMin;\n for (let i = 0; i < n; i++) {\n const baseAngle = (i / n) * Math.PI * 2;\n const angle = baseAngle + rng.range(-0.25, 0.25);\n const inner = i % 2 === 0;\n const radius = inner\n ? bounds.radius * rng.range(0.15, 0.45)\n : bounds.radius * rng.range(0.55, 0.85);\n const y = bounds.yMin + ySpan * rng.range(0.2, 0.8);\n points.push([Math.cos(angle) * radius, y, Math.sin(angle) * radius]);\n }\n\n // Arc-length table over the closed loop.\n const arc = new Float32Array(ARC_SAMPLES + 1);\n let prev = splineAt(points, 0);\n let total = 0;\n for (let s = 1; s <= ARC_SAMPLES; s++) {\n const g = (s / ARC_SAMPLES) * n;\n const p = splineAt(points, g);\n total += Math.hypot(p[0] - prev[0], p[1] - prev[1], p[2] - prev[2]);\n arc[s] = total;\n prev = p;\n }\n\n // Cruise + gentle speed wobble: sometimes dawdling, sometimes keen — the\n // integral of each harmonic is closed-form, so distance(t) never drifts.\n const cruise = 8 + rng.range(0, 4);\n const wobble = [\n { amp: cruise * 0.18, w: 0.11 + rng.range(0, 0.05), phase: rng.range(0, Math.PI * 2) },\n { amp: cruise * 0.1, w: 0.043 + rng.range(0, 0.02), phase: rng.range(0, Math.PI * 2) },\n ];\n\n return { points, arc, totalLength: total, cruise, wobble };\n}\n\n/** Distance travelled by time `tSec` at `speed` scale — pure closed form. */\nexport function distanceAt(plan: SwimPlan, tSec: number, speed: number): number {\n let d = plan.cruise * tSec;\n for (const h of plan.wobble) {\n // ∫ amp·sin(w·t + φ) dt = −(amp/w)·cos(w·t + φ); anchored so d(0) = 0.\n d += (h.amp / h.w) * (Math.cos(h.phase) - Math.cos(h.w * tSec + h.phase));\n }\n return d * speed;\n}\n\n/** Global spline param for a given arc distance (binary search + lerp). */\nfunction paramForDistance(plan: SwimPlan, dist: number): number {\n const n = plan.points.length;\n const d = ((dist % plan.totalLength) + plan.totalLength) % plan.totalLength;\n const arc = plan.arc;\n let lo = 0;\n let hi = ARC_SAMPLES;\n while (lo + 1 < hi) {\n const mid = (lo + hi) >> 1;\n if (arc[mid]! <= d) lo = mid;\n else hi = mid;\n }\n const seg = arc[hi]! - arc[lo]! || 1;\n const frac = (d - arc[lo]!) / seg;\n return ((lo + frac) / ARC_SAMPLES) * n;\n}\n\n/** Pose at `tSec` — position, unit forward tangent, bank roll, distance. */\nexport function swimPoseAt(plan: SwimPlan, tSec: number, speed = 1): SwimPose {\n return swimPoseAtDistance(plan, distanceAt(plan, tSec, speed));\n}\n\n/** Pose at an explicit arc distance — the seam for behavior-modulated speed.\n * When the track steers swimSpeed, the tank feeds this the closed-form\n * integral of the speed curve (core's `integrateParam`) so speed changes\n * glide instead of teleporting, while staying a pure function of (t, track)\n * — scrubbing and renderFrame(t) keep working. */\nexport function swimPoseAtDistance(plan: SwimPlan, dist: number): SwimPose {\n const g = paramForDistance(plan, dist);\n const [x, y, z] = splineAt(plan.points, g);\n\n const step = (plan.points.length / ARC_SAMPLES) * 8;\n const ahead = splineAt(plan.points, g + step);\n const behind = splineAt(plan.points, g - step + plan.points.length);\n let fx = ahead[0] - behind[0];\n let fy = ahead[1] - behind[1];\n let fz = ahead[2] - behind[2];\n const m = Math.hypot(fx, fy, fz) || 1;\n fx /= m;\n fy /= m;\n fz /= m;\n\n // Bank: signed change of horizontal heading over the step → lean into it.\n const h0 = Math.atan2(z - behind[2], x - behind[0]);\n const h1 = Math.atan2(ahead[2] - z, ahead[0] - x);\n let turn = h1 - h0;\n if (turn > Math.PI) turn -= Math.PI * 2;\n if (turn < -Math.PI) turn += Math.PI * 2;\n const roll = Math.max(-0.35, Math.min(0.35, turn * 3.5));\n\n return { x, y, z, fx, fy, fz, roll, dist };\n}\n","import type { CapabilityTier } from '@idle-screens/capabilities';\n\nexport interface TankQuality {\n maxPixelRatio: number;\n antialias: boolean;\n fishCap: number;\n pixelBudget: number;\n /** Plankton motes at moteDensity=1 — scaled down by the param, zero cost\n * at the default 0. Vertex-shader driven, so the per-frame cost is GPU\n * vertex work only. */\n moteCap: number;\n}\n\nexport function qualityFor(tier: CapabilityTier): TankQuality {\n switch (tier) {\n case 'high':\n return { maxPixelRatio: 1.25, antialias: true, fishCap: 24, pixelBudget: 1_800_000, moteCap: 400 };\n case 'standard':\n return { maxPixelRatio: 1, antialias: true, fishCap: 16, pixelBudget: 1_200_000, moteCap: 250 };\n default:\n return { maxPixelRatio: 1, antialias: false, fishCap: 8, pixelBudget: 900_000, moteCap: 120 };\n }\n}\n\nexport function effectivePixelRatio(\n width: number,\n height: number,\n dpr: number,\n q: TankQuality,\n): number {\n const area = Math.max(1, width * height);\n return Math.max(0.5, Math.min(dpr, q.maxPixelRatio, Math.sqrt(q.pixelBudget / area)));\n}\n\nexport function isSoftwareGL(renderer: string): boolean {\n return /swiftshader|llvmpipe|software|swangle/i.test(renderer);\n}\n\n/** Probe the default GL context for a software renderer before creating a\n * WebGLRenderer so antialias/quality tier match from the first frame. */\nexport function probeSoftwareGL(): boolean {\n if (typeof document === 'undefined') return false;\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');\n if (!gl) return false;\n const dbg = gl.getExtension('WEBGL_debug_renderer_info');\n const renderer = String(\n dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),\n );\n return isSoftwareGL(renderer);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/plan.ts","../src/quality.ts"],"sourcesContent":["import type { Rng } from '@idle-screens/core';\n\nexport interface TankBounds {\n radius: number;\n yMin: number;\n yMax: number;\n}\n\n/**\n * Catmull-Rom spline swim: a seeded closed loop of waypoints with arc-length\n * parameterization. Pose is a pure function of (plan, distance), so\n * `renderFrame(t, seed)` stays frame-addressable and scrubbing works.\n */\n\nexport interface SwimPlan {\n /** Closed-loop control points. */\n points: Array<[number, number, number]>;\n /** Arc-length lookup: cumulative length at N uniform spline params. */\n arc: Float32Array;\n totalLength: number;\n /** Cruise speed, units/sec, before the swimSpeed param scales it. */\n cruise: number;\n /** Speed-wobble harmonics (closed-form integral → distance). */\n wobble: Array<{ amp: number; w: number; phase: number }>;\n}\n\nexport interface SwimPose {\n x: number;\n y: number;\n z: number;\n /** Unit forward tangent — the fish noses along this. */\n fx: number;\n fy: number;\n fz: number;\n /** Bank roll (radians): lean into horizontal turns. */\n roll: number;\n /** Distance travelled (drives the tail-beat clip phase). */\n dist: number;\n}\n\nconst ARC_SAMPLES = 1024;\n\nfunction catmullRom(\n p0: number,\n p1: number,\n p2: number,\n p3: number,\n u: number,\n): number {\n const u2 = u * u;\n const u3 = u2 * u;\n return (\n 0.5 *\n (2 * p1 +\n (-p0 + p2) * u +\n (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 +\n (-p0 + 3 * p1 - 3 * p2 + p3) * u3)\n );\n}\n\n/** Point on the closed loop at global param `g` ∈ [0, n). */\nfunction splineAt(points: SwimPlan['points'], g: number): [number, number, number] {\n const n = points.length;\n const i = Math.floor(g) % n;\n const u = g - Math.floor(g);\n const p0 = points[(i - 1 + n) % n]!;\n const p1 = points[i]!;\n const p2 = points[(i + 1) % n]!;\n const p3 = points[(i + 2) % n]!;\n return [\n catmullRom(p0[0], p1[0], p2[0], p3[0], u),\n catmullRom(p0[1], p1[1], p2[1], p3[1], u),\n catmullRom(p0[2], p1[2], p2[2], p3[2], u),\n ];\n}\n\n/**\n * Compile a seeded itinerary through the tank volume. Waypoints alternate\n * around the ring (angle jitter, radius breathing, depth changes) so the\n * loop reads as purposeful wandering, never a circle.\n */\nexport function compileSwimPlan(rng: Rng, bounds: TankBounds): SwimPlan {\n const n = 16 + rng.int(0, 4);\n const points: SwimPlan['points'] = [];\n const ySpan = bounds.yMax - bounds.yMin;\n for (let i = 0; i < n; i++) {\n const baseAngle = (i / n) * Math.PI * 2;\n const angle = baseAngle + rng.range(-0.25, 0.25);\n const inner = i % 2 === 0;\n const radius = inner\n ? bounds.radius * rng.range(0.15, 0.45)\n : bounds.radius * rng.range(0.55, 0.85);\n const y = bounds.yMin + ySpan * rng.range(0.2, 0.8);\n points.push([Math.cos(angle) * radius, y, Math.sin(angle) * radius]);\n }\n\n // Arc-length table over the closed loop.\n const arc = new Float32Array(ARC_SAMPLES + 1);\n let prev = splineAt(points, 0);\n let total = 0;\n for (let s = 1; s <= ARC_SAMPLES; s++) {\n const g = (s / ARC_SAMPLES) * n;\n const p = splineAt(points, g);\n total += Math.hypot(p[0] - prev[0], p[1] - prev[1], p[2] - prev[2]);\n arc[s] = total;\n prev = p;\n }\n\n // Cruise + gentle speed wobble: sometimes dawdling, sometimes keen — the\n // integral of each harmonic is closed-form, so distance(t) never drifts.\n const cruise = 8 + rng.range(0, 4);\n const wobble = [\n { amp: cruise * 0.18, w: 0.11 + rng.range(0, 0.05), phase: rng.range(0, Math.PI * 2) },\n { amp: cruise * 0.1, w: 0.043 + rng.range(0, 0.02), phase: rng.range(0, Math.PI * 2) },\n ];\n\n return { points, arc, totalLength: total, cruise, wobble };\n}\n\n/** Distance travelled by time `tSec` at `speed` scale — pure closed form. */\nexport function distanceAt(plan: SwimPlan, tSec: number, speed: number): number {\n let d = plan.cruise * tSec;\n for (const h of plan.wobble) {\n // ∫ amp·sin(w·t + φ) dt = −(amp/w)·cos(w·t + φ); anchored so d(0) = 0.\n d += (h.amp / h.w) * (Math.cos(h.phase) - Math.cos(h.w * tSec + h.phase));\n }\n return d * speed;\n}\n\n/** Global spline param for a given arc distance (binary search + lerp). */\nfunction paramForDistance(plan: SwimPlan, dist: number): number {\n const n = plan.points.length;\n const d = ((dist % plan.totalLength) + plan.totalLength) % plan.totalLength;\n const arc = plan.arc;\n let lo = 0;\n let hi = ARC_SAMPLES;\n while (lo + 1 < hi) {\n const mid = (lo + hi) >> 1;\n if (arc[mid]! <= d) lo = mid;\n else hi = mid;\n }\n const seg = arc[hi]! - arc[lo]! || 1;\n const frac = (d - arc[lo]!) / seg;\n return ((lo + frac) / ARC_SAMPLES) * n;\n}\n\n/** Pose at `tSec` — position, unit forward tangent, bank roll, distance. */\nexport function swimPoseAt(plan: SwimPlan, tSec: number, speed = 1): SwimPose {\n return swimPoseAtDistance(plan, distanceAt(plan, tSec, speed));\n}\n\n/** Pose at an explicit arc distance — the seam for behavior-modulated speed.\n * When the track steers swimSpeed, the tank feeds this the closed-form\n * integral of the speed curve (core's `integrateParam`) so speed changes\n * glide instead of teleporting, while staying a pure function of (t, track)\n * — scrubbing and renderFrame(t) keep working. */\nexport function swimPoseAtDistance(plan: SwimPlan, dist: number): SwimPose {\n const g = paramForDistance(plan, dist);\n const [x, y, z] = splineAt(plan.points, g);\n\n const step = (plan.points.length / ARC_SAMPLES) * 8;\n const ahead = splineAt(plan.points, g + step);\n const behind = splineAt(plan.points, g - step + plan.points.length);\n let fx = ahead[0] - behind[0];\n let fy = ahead[1] - behind[1];\n let fz = ahead[2] - behind[2];\n const m = Math.hypot(fx, fy, fz) || 1;\n fx /= m;\n fy /= m;\n fz /= m;\n\n // Bank: signed change of horizontal heading over the step → lean into it.\n const h0 = Math.atan2(z - behind[2], x - behind[0]);\n const h1 = Math.atan2(ahead[2] - z, ahead[0] - x);\n let turn = h1 - h0;\n if (turn > Math.PI) turn -= Math.PI * 2;\n if (turn < -Math.PI) turn += Math.PI * 2;\n const roll = Math.max(-0.35, Math.min(0.35, turn * 3.5));\n\n return { x, y, z, fx, fy, fz, roll, dist };\n}\n","import type { CapabilityTier } from '@idle-screens/capabilities';\n\nexport interface TankQuality {\n maxPixelRatio: number;\n antialias: boolean;\n fishCap: number;\n pixelBudget: number;\n /** Plankton motes at moteDensity=1 — scaled down by the param, zero cost\n * at the default 0. Vertex-shader driven, so the per-frame cost is GPU\n * vertex work only. */\n moteCap: number;\n /** Budget for environment layers (see environments.ts LAYER_COST): the\n * ceiling costs 1, rays 2, terrain is free. A weak device keeps the\n * ceiling and loses the rays. */\n envBudget: number;\n}\n\nexport function qualityFor(tier: CapabilityTier): TankQuality {\n switch (tier) {\n case 'high':\n return { maxPixelRatio: 1.25, antialias: true, fishCap: 24, pixelBudget: 1_800_000, moteCap: 400, envBudget: 3 };\n case 'standard':\n return { maxPixelRatio: 1, antialias: true, fishCap: 16, pixelBudget: 1_200_000, moteCap: 250, envBudget: 2 };\n default:\n return { maxPixelRatio: 1, antialias: false, fishCap: 8, pixelBudget: 900_000, moteCap: 120, envBudget: 1 };\n }\n}\n\nexport function effectivePixelRatio(\n width: number,\n height: number,\n dpr: number,\n q: TankQuality,\n): number {\n const area = Math.max(1, width * height);\n return Math.max(0.5, Math.min(dpr, q.maxPixelRatio, Math.sqrt(q.pixelBudget / area)));\n}\n\nexport function isSoftwareGL(renderer: string): boolean {\n return /swiftshader|llvmpipe|software|swangle/i.test(renderer);\n}\n\n/** Probe the default GL context for a software renderer before creating a\n * WebGLRenderer so antialias/quality tier match from the first frame. */\nexport function probeSoftwareGL(): boolean {\n if (typeof document === 'undefined') return false;\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');\n if (!gl) return false;\n const dbg = gl.getExtension('WEBGL_debug_renderer_info');\n const renderer = String(\n dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),\n );\n return isSoftwareGL(renderer);\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SaverPlugin, ControlTrack } from '@idle-screens/core';
2
- import { M as MetaquariumOptions } from './manifest-BYQUyBxU.js';
3
- export { B as BLOOM_COLORS, D as DEFAULT_FISH, F as FISH_CATALOG, a as FishEntry, b as METAQUARIUM_PARAMS, c as MIAMI_VICE_COLORS, m as metaquariumManifest, p as paramSpaceWith, r as resolveIpfsUrl, w as withDefaults } from './manifest-BYQUyBxU.js';
2
+ import { M as MetaquariumOptions } from './manifest--GHWtt5W.js';
3
+ export { B as BLOOM_COLORS, D as DEFAULT_FISH, F as FISH_CATALOG, a as FishEntry, b as METAQUARIUM_PARAMS, c as MIAMI_VICE_COLORS, m as metaquariumManifest, p as paramSpaceWith, r as resolveIpfsUrl, w as withDefaults } from './manifest--GHWtt5W.js';
4
4
  import { CapabilityTier } from '@idle-screens/capabilities';
5
5
 
6
6
  declare function createMetaquarium(opts?: MetaquariumOptions): SaverPlugin;
@@ -21,6 +21,10 @@ interface TankQuality {
21
21
  * at the default 0. Vertex-shader driven, so the per-frame cost is GPU
22
22
  * vertex work only. */
23
23
  moteCap: number;
24
+ /** Budget for environment layers (see environments.ts LAYER_COST): the
25
+ * ceiling costs 1, rays 2, terrain is free. A weak device keeps the
26
+ * ceiling and loses the rays. */
27
+ envBudget: number;
24
28
  }
25
29
  declare function qualityFor(tier: CapabilityTier): TankQuality;
26
30
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  qualityFor
3
- } from "./chunk-F74UWBMY.js";
3
+ } from "./chunk-UXMO57ND.js";
4
4
  import {
5
5
  BLOOM_COLORS,
6
6
  DEFAULT_FISH,
@@ -11,7 +11,7 @@ import {
11
11
  paramSpaceWith,
12
12
  resolveIpfsUrl,
13
13
  withDefaults
14
- } from "./chunk-ZGYVELA4.js";
14
+ } from "./chunk-BWIJDJPQ.js";
15
15
 
16
16
  // src/metaquarium.ts
17
17
  import { detectCapabilities, computeTier } from "@idle-screens/capabilities";
@@ -26,7 +26,7 @@ function createMetaquarium(opts = {}) {
26
26
  },
27
27
  async mount(ctx) {
28
28
  const [{ mountTank }, caps] = await Promise.all([
29
- import("./tank-NLSUKS2L.js"),
29
+ import("./tank-2TH4D2EE.js"),
30
30
  detectCapabilities()
31
31
  ]);
32
32
  return mountTank(ctx, space, computeTier(caps), opts.catalog);