@idle-screens/saver-metaquarium 0.17.0 → 0.19.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.
package/README.md CHANGED
@@ -185,11 +185,11 @@ Every one of them defaults to off — 0, `none`, −1 for `followSpot`, empty fo
185
185
  | --- | --- | --- |
186
186
  | `rockDensity` | 0–1 | Crystal-root boulders, sparse satellite rocks, a distant ridge and an open arch. Any positive value provides foundations; density adds satellites. |
187
187
  | `rockVeins` | 0–1 (0.7) | How hard the crystals broke out of the stone. Each rock's crown is heaved into a pit with chips of stone on the rim and a few short dark fractures, and a colony of real crystals (druse or spire, the same shards, material, pulse and halo as `propMix`) grows out of it, rooted inside the rock. A rock under a `propMix` cluster is broken open under that cluster instead. More veins, more rocks split, and bigger colonies. The colonies add no floor light. 0 is plain stone. |
188
- | `geodeHomes` | 0–3 | Geode homes: a broken boulder with an agate rind and a throat of crystal teeth, and a voxel house recessed inside — round door, lit window, lamp, steps, a chimney that vents bubbles. Habits cycle cottage / hall / tower; each home lights the floor at its door. Weak devices retain at most two. |
188
+ | `geodeHomes` | 0–3 | Geode homes: a broken boulder with an agate rind and a throat of crystal teeth, and a voxel house recessed inside — round door, lit window, lamp, steps, a chimney that vents bubbles. Habits cycle cottage / hall / tower; each home lights the floor at its door. Weak devices retain at most two: geodeHomes is capped at 2 on basic and minimal tiers (3 on standard and high); a house that is dropped takes its `homeN`/`homeNin` marks with it, and indoors (`interior: geode`) there are no `homeN` marks. |
189
189
  | `interior` | `none` · `geode` | Sets the scene INSIDE a geode home: crystal-lined dome with agate strata, plank floor and rug, voxel furniture, a chandelier / lamp / stove / window that light the floor and the fish. Use with the `void` environment; the default orbit camera stays indoors (keep `cameraDistance` ≲ 130). |
190
190
  | `followSpot` | -1…23 | A follow-spot on one fish (its cast slot; -1 off): a beam from the rig, a pool of moving caustics on the floor beneath it, a light that rides with the fish, and the house lights down — a performer on a stage. `spotStrength` 0–1, `spotColor`. Closed-form: it follows the fish's own deterministic path. |
191
- | `vignette` | string | A small scene for the first 2–3 fish: a preset (`tea`, `bedtime`, `seek`) or a script of beats — `a =table, b =door \| b >table @a \| a @b talk, b @a nod \| a b circle rug`. `=mark` start there, `>mark` go there, `@x` face a mark or actor, `circle x`, `follow x`, gestures `talk nod shake hop spin wiggle bow peek rest`, leading `6s:` sets a beat's length. Marks indoors: rug table bed shelf stove lamp armchair door window chest chandelier; outdoors: centre left right front back high low. Closed-form, looping; other fish swim as usual. |
192
- | `floraDensity` | 0–1 | Voxel plants — seventeen species (grass, tube worms, lantern bulbs, fans, kelp, anemone, staghorn, brain coral, sea whip, barrel sponge, glow caps, sea pen, bubble algae, fiddlehead curls, orb pods, and two specimens: the giant clam and the elder blossom tree). They grow in colonies that share a colour, ramp from base to tip, and one colony in thirty is a rare nacreous morph; they lean toward the nearest crystal and, on mid/high tiers, are lit by it and by follow-spots. Stony coral stands still, kelp and whips sway, tentacles writhe; a gust travels across the field, tips breathe. With `floraMix` empty the room picks the garden (coral on a reef, kelp and whips in the kelp forest, glow caps in the abyss). Plants answer passing fish: anemone crowns fold, worms duck, sea pens pull down, pods swell, lamps flare. |
191
+ | `vignette` | string | A small scene for the first 2–3 fish: a preset (`tea`, `bedtime`, `seek`) or a script of beats — `a =table, b =door \| b >table @a \| a @b talk, b @a nod \| a b circle rug`. `=mark` places an actor with no travel (in a later beat it snaps at the start of the beat), `>mark` swims there, `@x` face a mark or actor, `circle x` (each named actor from wherever it stands onto a ring round the mark and back to its own spot by the end of the beat; actors not named stay put), `follow x`, gestures `talk nod shake hop spin wiggle bow peek rest`, and `ink` (an octopus's own: it squirts a cloud of ink on its mark; other breeds ignore it, with a warning), leading `6s:` sets a beat's length. Marks indoors: rug table bed shelf stove lamp armchair door window chest chandelier (bearings and distances in `GRAMMAR`; they scale with the room, by `max(crystalScale, 0.85)`); outdoors: centre left right front back high low. A floor creature cast as an actor keeps its feet on the floor at its marks, and maneuvers leave actors alone. Closed-form, looping; other fish swim as usual. |
192
+ | `floraDensity` | 0–1 | Voxel plants — seventeen species (grass, tube worms, lantern bulbs, fans, kelp, anemone, staghorn, brain coral, sea whip, barrel sponge, glow caps, sea pen, bubble algae, fiddlehead curls, orb pods, and two specimens: the giant clam and the elder blossom tree). They grow in colonies that share a colour, ramp from base to tip, and one colony in thirty is a rare nacreous morph; they lean toward the nearest crystal and, on mid/high tiers, are lit by it and by follow-spots. Stony coral stands still, kelp and whips sway, tentacles writhe; a gust travels across the field, tips breathe. With `floraMix` empty the room picks the garden (coral on a reef, kelp and whips in the kelp forest, glow caps in the abyss). Plants answer passing fish: anemone crowns fold, worms duck, sea pens pull down, pods swell, lamps flare. The density sets one tank-wide plant budget (density × 36 / 72 / 108 by tier), shared round-robin among the crystals and geodes (or six bare spots when there are none): more crystals means sparser gardens, not more plants. |
193
193
  | `floraPalette` | string | One colour scheme for the garden: `world` (seeded) or up to six comma-separated `#rrggbb` values. Empty = each species' own colours. |
194
194
  | `geodes` | 0–1 | Wild geodes, few and made properly: split geodes (one in three a thunder egg), amethyst cathedrals, crystal clusters and one mega cavern geode. Agate banding drawn per pixel, crystals in real quartz habit and colour-zoned root to tip, twinkling druse, self-lit so they read in a flat tank; one in twenty is an iridescent aura morph, and their crystals wake when a fish comes close. |
195
195
  | `geodeMix` | string | Which kinds: `kind[:weight]`, comma-separated — `geode, cathedral, cluster, cavern`. |
@@ -125,6 +125,7 @@ var AUTO_STYLE_BY_BREED = {
125
125
  octopus: "bottom",
126
126
  snail: "bottom",
127
127
  dot: "bottom",
128
+ dolphin: "loop",
128
129
  dori: "school"
129
130
  };
130
131
  function autoStyleFor(breed) {
@@ -185,7 +186,8 @@ var BREED_SIZE = {
185
186
  starfish: 1,
186
187
  octopus: 1.2,
187
188
  snail: 0.8,
188
- dot: 1.2
189
+ dot: 1.2,
190
+ dolphin: 1.8
189
191
  };
190
192
  function breedSize(breed) {
191
193
  return breed && BREED_SIZE[breed.toLowerCase()] || 1;
@@ -1249,7 +1251,9 @@ var NPC_CATALOG = [
1249
1251
  // The third, a floor creature (breeds/rig/snail-model.mjs).
1250
1252
  { id: 611, name: "Snail", breed: "snail", ipfs3d: `${BUNDLED_BREED_SCHEME}snail`, localGlb: "" },
1251
1253
  // The first character (experimental): Dot, the courier (breeds/rig/dot-model.mjs).
1252
- { id: 612, name: "Dot", breed: "dot", ipfs3d: `${BUNDLED_BREED_SCHEME}dot`, localGlb: "" }
1254
+ { id: 612, name: "Dot", breed: "dot", ipfs3d: `${BUNDLED_BREED_SCHEME}dot`, localGlb: "" },
1255
+ // Dot's friend (experimental): a dolphin (breeds/rig/dolphin-model.mjs).
1256
+ { id: 613, name: "Dolphin", breed: "dolphin", ipfs3d: `${BUNDLED_BREED_SCHEME}dolphin`, localGlb: "" }
1253
1257
  ];
1254
1258
  var findInDefault = (key) => /^\d+$/.test(key) ? FISH_CATALOG.find((f) => f.id === Number(key)) ?? NPC_CATALOG.find((f) => f.id === Number(key)) : FISH_CATALOG.find((f) => f.breed.toLowerCase() === key) ?? NPC_CATALOG.find((f) => f.breed.toLowerCase() === key);
1255
1259
  function parseFishMix(mix, catalog = FISH_CATALOG) {
@@ -2493,7 +2497,8 @@ function parseSignMix(value) {
2493
2497
 
2494
2498
  // src/vignette.ts
2495
2499
  var DANCES = ["toprock", "footwork", "windmill", "headspin", "freeze", "drop"];
2496
- var GESTURES = ["talk", "nod", "shake", "hop", "spin", "wiggle", "rest", "peek", "bow", ...DANCES];
2500
+ var GESTURES = ["talk", "nod", "shake", "hop", "spin", "wiggle", "rest", "peek", "bow", "ink", ...DANCES];
2501
+ var BREED_GESTURES = { ink: ["octopus"] };
2497
2502
  var OPEN_MARKS = {
2498
2503
  centre: { x: 0, y: 36, z: 0 },
2499
2504
  left: { x: -70, y: 34, z: 0 },
@@ -2567,6 +2572,7 @@ function parseVignette(script, marks) {
2567
2572
  if (word[0] === "=") at[i] = dest;
2568
2573
  else longest = Math.max(longest, Math.hypot(dest.x - at[i].x, dest.y - at[i].y, dest.z - at[i].z) / CRUISE);
2569
2574
  state[i] = { ...state[i], to: dest, moved: word[0] === ">", facePoint: state[i].facePoint ?? mk };
2575
+ if (word[0] === "=" && bi > 0) state[i] = { ...state[i], from: dest, placed: true };
2570
2576
  }
2571
2577
  } else if (word[0] === "@") {
2572
2578
  const tg = target(word.slice(1));
@@ -2634,7 +2640,7 @@ function placeOf(v, actor, tSec, depth = 0) {
2634
2640
  if (bi < 0) bi = v.beats.length - 1;
2635
2641
  const beat = v.beats[bi], me = beat.actors[actor];
2636
2642
  const prev = v.beats[(bi + v.beats.length - 1) % v.beats.length].actors[actor];
2637
- const from = bi === 0 ? me.from ?? me.to : prev.to;
2643
+ const from = me.from ?? (bi === 0 ? me.to : prev.to);
2638
2644
  const u = (t - beat.t0) / beat.dur;
2639
2645
  if (me.follow !== null && depth < 2) {
2640
2646
  const lead = placeOf(v, me.follow, tSec - 1.7, depth + 1);
@@ -2662,6 +2668,19 @@ function placeOf(v, actor, tSec, depth = 0) {
2662
2668
  z: from.z + dz * w + dx / (dist || 1) * arc
2663
2669
  };
2664
2670
  }
2671
+ function snapBetween(v, actor, t1, t2) {
2672
+ if (!v.duration || actor >= v.actors) return false;
2673
+ const lo = Math.min(t1, t2), span = Math.abs(t2 - t1);
2674
+ const a = (lo % v.duration + v.duration) % v.duration;
2675
+ return v.beats.some((b) => {
2676
+ if (!b.actors[actor]?.placed) return false;
2677
+ const d = ((b.t0 - a) % v.duration + v.duration) % v.duration;
2678
+ return d > 0 && d <= span;
2679
+ });
2680
+ }
2681
+ function poseAcross(v, actor, tSec, dt, own) {
2682
+ return poseOf(v, actor, snapBetween(v, actor, tSec, tSec + dt) ? tSec : tSec + dt, own);
2683
+ }
2665
2684
  function poseOf(v, actor, tSec, own) {
2666
2685
  if (actor >= v.actors || !v.duration) return null;
2667
2686
  const t = (tSec % v.duration + v.duration) % v.duration;
@@ -2670,8 +2689,9 @@ function poseOf(v, actor, tSec, own) {
2670
2689
  const beat = v.beats[bi], me = beat.actors[actor];
2671
2690
  const u = (t - beat.t0) / beat.dur;
2672
2691
  const p = placeOf(v, actor, tSec);
2673
- const ahead = placeOf(v, actor, tSec + 0.12);
2674
- const tx = ahead.x - p.x, ty = ahead.y - p.y, tz = ahead.z - p.z;
2692
+ const cut = snapBetween(v, actor, tSec, tSec + 0.12);
2693
+ const ahead = placeOf(v, actor, tSec + (cut ? -0.12 : 0.12));
2694
+ const tx = (ahead.x - p.x) * (cut ? -1 : 1), ty = (ahead.y - p.y) * (cut ? -1 : 1), tz = (ahead.z - p.z) * (cut ? -1 : 1);
2675
2695
  const speed = Math.hypot(tx, ty, tz) / 0.12;
2676
2696
  let fx = tx, fy = ty * 0.5, fz = tz;
2677
2697
  const target = me.faceActor !== null ? placeOf(v, me.faceActor, tSec) : me.facePoint;
@@ -2689,7 +2709,8 @@ function poseOf(v, actor, tSec, own) {
2689
2709
  fz = fz / ml * (1 - settled) + gz / gl * settled;
2690
2710
  }
2691
2711
  } else if (Math.hypot(fx, fz) < 1e-4) {
2692
- const back = placeOf(v, actor, beat.t0 - 0.2), here = placeOf(v, actor, beat.t0);
2712
+ const end = me.placed ? beat.t0 - 1e-3 : beat.t0;
2713
+ const back = placeOf(v, actor, end - 0.2), here = placeOf(v, actor, end);
2693
2714
  fx = here.x - back.x;
2694
2715
  fz = here.z - back.z;
2695
2716
  fy = 0;
@@ -2754,6 +2775,7 @@ function poseOf(v, actor, tSec, own) {
2754
2775
  roll += 0.22 * e;
2755
2776
  pitch = -0.08 * e;
2756
2777
  break;
2778
+ // ink: the body holds still; the octopus squirts its cloud (the tank's InkLayer) and pales.
2757
2779
  // The dances, for a fish with no clips for them: the whole body does what it can.
2758
2780
  case "toprock":
2759
2781
  roll += Math.sin(bt * Math.PI * 2) * 0.3 * e;
@@ -2798,6 +2820,17 @@ function poseOf(v, actor, tSec, own) {
2798
2820
  return { x, y: Math.max(9, y), z, fx, fy, fz, roll, doing, lookAt: target ?? null, ...gesture ? { gesture } : {} };
2799
2821
  }
2800
2822
  var polar = (deg, r, y) => ({ x: Math.cos(deg * Math.PI / 180) * r, y, z: Math.sin(deg * Math.PI / 180) * r });
2823
+ var INTERIOR_MARK_POLAR = {
2824
+ door: { deg: 28, r: 124, y: 22 },
2825
+ stove: { deg: 88, r: 106, y: 33 },
2826
+ window: { deg: 128, r: 124, y: 46 },
2827
+ table: { deg: 150, r: 46, y: 27, note: "on the rug" },
2828
+ bed: { deg: 205, r: 104, y: 21 },
2829
+ chest: { deg: 232, r: 110, y: 24 },
2830
+ lamp: { deg: 262, r: 102, y: 32 },
2831
+ armchair: { deg: 271, r: 100, y: 22 },
2832
+ shelf: { deg: 330, r: 110, y: 30 }
2833
+ };
2801
2834
  var INTERIOR_MARKS = {
2802
2835
  rug: { x: 6, y: 26, z: -4 },
2803
2836
  centre: { x: 6, y: 30, z: -4 },
@@ -2896,13 +2929,14 @@ var PARAM_DOCS = {
2896
2929
  shot: "A named framing to cut to: `orbit` (default), `hero` (three-quarter establishing), `front` (level, long lens), `low` (among the plants looking up), `top` (steep, the floor as a map), `surface` (looking up at the water's underside), `macro` (close on the landmark nearest the front). Azimuth and autoRotate turn it; distance scales it.",
2897
2930
  cameraFollow: "Ride with one fish (cast slot, 0 = first); -1 is the orbit camera. Orbit params are ignored while following. Pair with `followSpot` on the same slot for a spotlit chase.",
2898
2931
  followAngle: "Where round the followed fish the chase camera stands, degrees: 0 behind (default), 90 beside, 180 in front looking back at its face.",
2899
- followDistance: "How far behind the followed fish the camera rides, along its own path (40\u201370 a chase). Under ~18 it is the fish's eye and the fish is hidden.",
2900
- autoRotate: "Turntable speed, degrees/second. 0 holds the shot (stages, vignettes); 1\u20132 is a slow reveal.",
2932
+ followDistance: "How far behind the followed fish the camera rides, along its own path (40\u201370 a chase). Under ~18 it is the fish's eye and the fish is hidden; a floor walker (Dot, crab, octopus, snail, starfish) sees from its own head height, pitched with the ground ahead.",
2933
+ autoRotate: "Turntable speed, degrees/second. 0 holds the shot (stages, vignettes); 1\u20132 is a slow reveal. frame.state.camera.bearing: where it stands now.",
2934
+ followAvoid: "Keep the followed fish in view, 0\u20131 (default 0): with something in the way (a home, a wall, a lamp, a kelp stand) the chase camera rises a little over low clutter and otherwise pulls in toward the fish \u2014 eased, like a game camera's spring arm. Use it for long walking takes through a village. No effect in the fish's own eye (followDistance under about 16).",
2901
2935
  // cast
2902
2936
  fishCount: "How many fish when `fishMix` is empty (all are `fishUrl`).",
2903
2937
  finish: "A final full-screen pass: gentle grade, soft vignette, dither against banding, and restrained bloom on high-end devices (0 = none; never on the low tier).",
2904
2938
  fishUrl: "The single model used when `fishMix` is empty. ipfs:// or https.",
2905
- fishMix: "The cast: `id[:count][@style]`, comma-separated. `100:2@drift, 257:1, seahorse:3, shark:1@patrol`. Minted breeds plus bundled creatures (shark crab dori glowfish babyfish hackerfish blowfish starfish octopus snail dot). A hackerfish's face is a screen (faces, code rain, crash and reboot); a glowfish fishes with its lure. A crab walks the seabed on its own legs at its own pace (swimSpeed does not hurry it), climbs the rocks, and stops to forage, pinch, wave and cheer. A starfish crawls the floor too, walks some of the way upright on its front arms, and stops to curl up, wave, or stand facing you; with `starfishDance` it dances. A snail glides the floor very slowly and stops to look about, look at you, graze, stretch up tall, or pull into its shell and peek back out. `dot` is a character (experimental): Dot, the town courier, a box on two legs whose face is a screen; it walks, runs and sneaks the floor, stops to wave, cheer, think, tip its cap, deliver a letter, dance, laugh and more, and as a vignette actor plays the gestures with its own body. Overrides fishCount/fishUrl.",
2939
+ fishMix: "The cast: `id[:count][@style]`, comma-separated. `100:2@drift, 257:1, seahorse:3, shark:1@patrol`. Minted breeds plus bundled creatures (shark crab dori glowfish babyfish hackerfish blowfish starfish octopus snail dot dolphin). A hackerfish's face is a screen (faces, code rain, crash and reboot); a glowfish fishes with its lure. A crab walks the seabed on its own legs at its own pace (swimSpeed does not hurry it), climbs the rocks, and stops to forage, pinch, wave and cheer. A starfish crawls the floor too, walks some of the way upright on its front arms, and stops to curl up, wave, or stand facing you; with `starfishDance` it dances. A snail glides the floor very slowly and stops to look about, look at you, graze, stretch up tall, or pull into its shell and peek back out. `dot` is a character (experimental): Dot, the town courier, a box on two legs whose face is a screen; it walks, runs and sneaks the floor, stops to wave, cheer, think, tip its cap, deliver a letter, dance, laugh and more, and as a vignette actor plays the gestures with its own body. `dolphin` (experimental) is Dot's friend: a bottlenose with flat flukes that strokes up and down, coasts, and every so often chatters (its laugh), nods, waves a flipper, blows a bubble, spins or flips \u2014 turning an eye to you for the friendly ones; in a vignette it chatters for talk, flips for hop, waves for wiggle and blows a bubble for peek. Overrides fishCount/fishUrl.",
2906
2940
  dracoPath: "Where the Draco decoder lives; leave empty on a channel.",
2907
2941
  swimSpeed: "Global swim speed multiplier. 0.4 is contemplative, 1 lively.",
2908
2942
  swimStyle: "How the cast swims unless a fishMix entry says otherwise (`@style`). `auto` picks by breed.",
@@ -2911,7 +2945,7 @@ var PARAM_DOCS = {
2911
2945
  swimVariance: "How differently individual fish swim from one another (speed, phase).",
2912
2946
  fishSize: "Every fish bigger or smaller, over each breed's own size (a shark is 2.2\xD7 a minted fish, a babyfish 0.45\xD7).",
2913
2947
  sizeVariance: "How much fish of one breed differ in size: 0 all alike, 1 from about half to nearly double.",
2914
- maneuver: "Which set-piece moves the cast may break into.",
2948
+ maneuver: "Which set-piece moves the cast may break into. Vignette actors (slots 0\u20132) keep their marks: the script owns them.",
2915
2949
  maneuverRate: "How often maneuvers happen.",
2916
2950
  maneuverIntensity: "How big they are.",
2917
2951
  bodyWiggle: "Body yaw while swimming, for models without an animation clip. A fish the swim wave bends (`swimWave` > 0) ignores it.",
@@ -2925,8 +2959,8 @@ var PARAM_DOCS = {
2925
2959
  fishAmbient: "The water's light on the fish in lit mode: the sunlit water on their backs, the water around on their flanks, the floor under their bellies, mirrored in metal plates. Fixes fish reading dark; 0.6\u20131 in bright tanks. Dims with a follow-spot's house lights.",
2926
2960
  fishGlow: "How strongly GLOW- parts of a fish bloom and light their neighbours and the floor.",
2927
2961
  fishMetal: "`on` (default) gives METAL- parts a polished, reflective finish.",
2928
- snailShell: "What the snails' shells are made of: `banded` (the coat with a spiral, as drawn), `pearl` (iridescent nacre: a rainbow sheen that slides as it turns), `gold` (polished metal), `glow` (a dark shell whose spiral breathes light \u2014 best at night). `mix` (default): each snail its own. Lit fish only: flat lighting and the neon look keep the banded coat.",
2929
- starfishDance: "Starfish dance: `aerobics` stands every starfish up in a class facing the camera (an instructor in front) dancing a routine in unison \u2014 march, jacks, reaches, kicks, twists, arm circles, disco, a spin; `freestyle`, each its own move every eight counts; `duet`, the partner dance (Dirty Dancing): couples face to face \u2014 the basic, a sway, a twirl, the dip, and the lift \u2014 best with `spotRig: 0/#ffd27a*26, 1/#ff8ad0*26`. Cast a few: `fishMix: starfish:7`. Best with `shot: front`, `autoRotate: 0`, a bright room. `off` (default): they crawl, walk upright and wave on their own.",
2962
+ snailShell: "What the snails' shells are made of: `banded` (the coat with a spiral, as drawn), `pearl` (iridescent nacre: a rainbow sheen that slides as it turns), `gold` (polished metal), `glow` (a dark shell whose spiral breathes light \u2014 best at night). `mix` (default): each snail its own. Lit fish only: flat lighting and the neon look keep the banded coat. Glow keeps each coat's hue; frame.state.fish[i].shell says which.",
2963
+ starfishDance: "Starfish dance: `aerobics` stands every starfish up in a class facing the camera (an instructor in front) dancing a routine in unison \u2014 march, jacks, reaches, kicks, twists, arm circles, disco, a spin; `freestyle`, each its own move every eight counts; `duet`, the partner dance (Dirty Dancing): couples face to face \u2014 the basic, a sway, a twirl, the dip, and the lift \u2014 best with `spotRig: 0/#ffd27a*26, 1/#ff8ad0*26`. Cast a few: `fishMix: starfish:7`. Best with `shot: front`, `autoRotate: 0`, a bright room. `off` (default): they crawl, walk upright and wave on their own. The class re-faces the live camera every frame: under autoRotate it stays put on screen while the room turns behind it (a follow camera uses cameraAzimuth).",
2930
2964
  danceTempo: "Beats per minute for the starfish dance (60\u2013180, default 128). Glides on the beat.",
2931
2965
  fishLook: "`neon` dresses the bundled creatures (crab, glowfish, shark\u2026) for blacklight: near-black coats, eyes and a crab's mouth glowing a neon of their own. Best in a dark room (abyss, void) with `finish`. `natural` is the default.",
2932
2966
  swimWave: "Fish bend as they swim: a wave runs nose to tail and the body curls into turns (0 = rigid wiggle). Skips the turtle, crab and fish that already have a skeleton; the seahorse gets its own motion instead \u2014 fin, tail and nod.",
@@ -2960,9 +2994,9 @@ var PARAM_DOCS = {
2960
2994
  crystalTint: "How much nearby light sources colour a passing fish. 0.5\u20130.7 ties the cast to the place.",
2961
2995
  rockDensity: "Faceted boulders, an arch and a back ridge around the crystals.",
2962
2996
  rockVeins: "Crystals bursting out of the rocks: each crown broken open, chips on the rim, a colony of the same crystal growing from the breach. 0 is plain stone.",
2963
- geodeHomes: "Geode houses (0\u20133) in a crescent: a village. Their doors are vignette marks `home1`\u2026, `home1in`\u2026.",
2997
+ geodeHomes: "Geode houses (0\u20133) in a crescent: a village. Their doors are vignette marks `home1`\u2026, `home1in`\u2026. Capped at 2 on basic/minimal tiers (3 on standard/high); a dropped house takes its homeN/homeNin marks, and indoors there are none.",
2964
2998
  interior: "`geode` sets the whole scene INSIDE a geode home: furniture, chandelier, marks like `table`, `bed`, `door`.",
2965
- floraDensity: "Voxel plants around each crystal, swaying, lit and shedding spores. What grows is `floraMix`; with it empty the room picks (coral on a reef, kelp in the kelp forest, glow caps in the abyss).",
2999
+ floraDensity: "Voxel plants, swaying, lit and shedding spores: one tank-wide budget (density \xD7 36/72/108 by tier) shared round-robin among crystals and geodes (or six bare spots), so more crystals means sparser gardens. What grows is `floraMix`; with it empty the room picks (coral on a reef, kelp in the kelp forest, glow caps in the abyss).",
2966
3000
  geodes: "Wild geodes about the floor (0 = none): split geodes (one in three a thunder egg), amethyst cathedrals, crystal clusters, one mega cavern geode. They follow the world's rules \u2014 slate crust lit by the same key as the rocks, crystals that glow and pulse like the crystals (crystalGlow, crystalPulse), the mineral of the crystal nearest them, the crystals' light on their stone \u2014 and flora grows round them as round a crystal. Agate banding is drawn per pixel; one in twenty is an iridescent aura morph; their crystals wake when a fish comes close.",
2967
3001
  geodeMix: "Which geodes: `kind[:weight]`, comma-separated. geode cathedral cluster cavern (the cavern is one at most).",
2968
3002
  geodeMineral: "What they are made of: amethyst agate celestine citrine carnelian rose emerald quartz smoky. Empty = the mineral nearest in colour to the crystal nearest each geode (it belongs to its world, as flora and homes do); `world` = two from the seed; or a list.",
@@ -2994,12 +3028,12 @@ var PARAM_DOCS = {
2994
3028
  landmark: "One thing bigger than everything else. `castle`: voxel walls, crystal-spired towers, gate, road, plaza. `citadel`: two storeys. Marks: `gate`, `plaza`, `courtyard`.",
2995
3029
  // stage
2996
3030
  followSpot: "A follow-spot on one fish slot (0 = first of the cast); -1 off. House lights drop with it.",
2997
- spotStrength: "How hard the spots are on and how far the house lights fall.",
3031
+ spotStrength: "How hard the spots are on and how far the house lights fall. House lights dim only lit fish's key/fill (to 40% at full) and fishAmbient; reflections, glow, flat fish and the unlit floor/scenery keep theirs, and a `-` blackout only puts out the spots.",
2998
3032
  spotColor: "Colour of the single `followSpot`.",
2999
3033
  spotShadow: "The spotted fish's shadow in its pool. 0 none, 1 true size.",
3000
3034
  spotRig: "Up to three spots: `slot[/color][*radius]`. `0/#ff8ad0*26, 1/#7fdcff`. They are a, b, c in order. Overrides followSpot.",
3001
3035
  spotCues: "A looping cue sheet for the rig: `8s:a, 8s:b, 12s:a+b, 4s:-` (`-` = blackout). Spots cross-fade at each cue.",
3002
- vignette: "A small scene for the first 2\u20133 fish: a preset (tea\xB7bedtime\xB7seek indoors; duet\xB7trio on the open stage) or a script of beats."
3036
+ vignette: "A small scene for the first 2\u20133 fish: a preset (tea\xB7bedtime\xB7seek indoors; duet\xB7trio on the open stage) or a script of beats. A floor creature cast as an actor stays on the floor. Grammar below."
3003
3037
  };
3004
3038
  var GRAMMAR = `
3005
3039
  fishMix id[:count][@style][*size], \u2026 100:2@drift, 257:1, seahorse:3, shark:1@patrol*1.5
@@ -3028,14 +3062,16 @@ signs kind[@place][>target][/#color][*size][:text], \u2026 plank@gate:"Wel
3028
3062
  kinds plank arrow ring porthole neon led \xB7 places gate plaza courtyard hub fountain home1 home2 home3 centre left right front back
3029
3063
  an arrow points at >target \xB7 text \u2264 plank 24, arrow 16, ring 10, porthole 4 (empty = a fish), neon 14, led 64 (scrolls) \xB7 \u2264 8 signs
3030
3064
  vignette beats separated by |, cues by comma; actors a b c are fish slots 0 1 2
3031
- [<sec>s:] actor =mark start there actor >mark swim there
3032
- actor @mark | @actor face it a b circle mark circle it together
3033
- actor follow actor gestures: talk nod shake hop spin wiggle rest peek bow
3065
+ [<sec>s:] actor =mark be there, no travel (a later beat's = snaps at its start) actor >mark swim there
3066
+ actor @mark | @actor face it a b circle mark each named actor rings it, back to its spot by the beat's end; others stay put
3067
+ actor follow actor gestures: talk nod shake hop spin wiggle rest peek bow \xB7 ink (octopus only: a cloud of ink)
3034
3068
  dances: toprock footwork windmill headspin freeze drop (a betafish dances them with its own body; 2 s beats keep them on a 120 bpm bar)
3035
3069
  Give every beat a duration and end where you began and a spotCues sheet stays in step forever.
3036
3070
  Marks outdoors: ${Object.keys(OPEN_MARKS).join(" ")} \u2014 plus the world's own:
3037
3071
  home1 home1in \u2026 (geodeHomes), gate plaza courtyard (landmark), hub (paths), fountain (fountain).
3038
3072
  Marks indoors (interior: geode): ${Object.keys(INTERIOR_MARKS).join(" ")}
3073
+ rug centre: middle; chandelier: overhead; others bearing\xB0/distance/height (0\xB0=+x, 90\xB0=+z), \xD7max(crystalScale, 0.85):
3074
+ ${Object.entries(INTERIOR_MARK_POLAR).map(([n, p]) => `${n} ${p.deg}/${p.r}/${p.y}`).join(" \xB7 ")}
3039
3075
  `.trim();
3040
3076
  var NIGHT = { fogColor: "#0a0a1a", fogNear: 120, fogFar: 700 };
3041
3077
  var STAGE = { fogColor: "#01030a", floorColor: "#0a1322", fogNear: 120, fogFar: 650, autoRotate: 0 };
@@ -3470,8 +3506,17 @@ function dependencyProblems(params) {
3470
3506
  else if (spot2 >= cast) out.push({ path: "followSpot", also: castParams, message: noFish(spot2) });
3471
3507
  for (const s of rig) if (s.slot >= cast) out.push({ path: "spotRig", also: castParams, message: noFish(s.slot) });
3472
3508
  if (str("vignette")) {
3473
- const { actors } = parseVignette(resolveVignette(str("vignette")), { ...OPEN_MARKS, ...INTERIOR_MARKS });
3509
+ const v = parseVignette(resolveVignette(str("vignette")), { ...OPEN_MARKS, ...INTERIOR_MARKS });
3510
+ const { actors } = v;
3474
3511
  if (actors > cast) out.push({ path: "vignette", also: castParams, message: `needs ${actors} fish (actors a\u2013${"abc"[actors - 1]} are slots 0\u2013${actors - 1}), but ${castWhy}` });
3512
+ const slotBreeds = expandFishMixSlots(mix, 3).map((sl) => sl.breed);
3513
+ for (const [g, breeds] of Object.entries(BREED_GESTURES)) {
3514
+ for (let a = 0; a < actors; a++) {
3515
+ if (!v.beats.some((b) => b.actors[a]?.gesture === g)) continue;
3516
+ const breed = slotBreeds[a] ?? "fish";
3517
+ if (!breeds.includes(breed)) out.push({ path: "vignette", also: ["fishMix"], message: `"${"abc"[a]} ${g}" does nothing: ${g} is the ${breeds.join("/")}'s own, and actor ${"abc"[a]} (slot ${a}) is a ${breed}` });
3518
+ }
3519
+ }
3475
3520
  }
3476
3521
  const dance = str("starfishDance", "off") || "off";
3477
3522
  const starfishSwims = mix.some((e, i) => e.breed === "starfish" && mix.slice(0, i).reduce((n, p) => n + p.count, 0) < CAST_CAP);
@@ -3480,7 +3525,7 @@ function dependencyProblems(params) {
3480
3525
  }
3481
3526
  if (set("danceTempo") && dance === "off") out.push({ path: "danceTempo", also: ["starfishDance"], message: "does nothing while starfishDance is off" });
3482
3527
  if (follow < 0) {
3483
- for (const k of ["followDistance", "followAngle"]) if (set(k)) out.push({ path: k, also: ["cameraFollow"], message: "does nothing while cameraFollow is -1 (the orbit camera)" });
3528
+ for (const k of ["followDistance", "followAngle", "followAvoid"]) if (set(k)) out.push({ path: k, also: ["cameraFollow"], message: "does nothing while cameraFollow is -1 (the orbit camera)" });
3484
3529
  } else if (follow < cast) {
3485
3530
  for (const k of ORBIT_PARAMS) if (set(k)) out.push({ path: k, also: ["cameraFollow"], message: `ignored while cameraFollow rides with slot ${follow} \u2014 they return when it is -1` });
3486
3531
  }
@@ -3510,6 +3555,7 @@ var BREED_NOUN = {
3510
3555
  octopus: ["octopus", "octopuses"],
3511
3556
  snail: ["snail", "snails"],
3512
3557
  dot: ["Dot", "Dots"],
3558
+ dolphin: ["dolphin", "dolphins"],
3513
3559
  starfish: ["starfish", "starfish"],
3514
3560
  crab: ["crab", "crabs"],
3515
3561
  shark: ["shark", "sharks"],
@@ -3708,6 +3754,12 @@ var METAQUARIUM_PARAMS = {
3708
3754
  * it (the default), 90 at its side, 180 in front looking back at its face
3709
3755
  * — a hackerfish's screen, a crab's smile. */
3710
3756
  followAngle: { type: "number", default: 0, min: -180, max: 180, ease: "smooth" },
3757
+ /** Keep the followed fish in view (sightline.ts): when a home, a wall, a lamp
3758
+ * or a kelp stand comes between it and the chase camera, the camera rises a
3759
+ * little to look over low clutter and otherwise pulls in toward the fish, as
3760
+ * a game camera's spring arm does — eased, never a snap. 0 (default) is the
3761
+ * plain chase camera; 1 the full arm. Not the fish's-eye view (it has none). */
3762
+ followAvoid: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
3711
3763
  /** Continuous orbit speed, degrees/second. Zero by default: the tank is
3712
3764
  * still, letting the fish movement carry the scene. Steer up for an orbit. */
3713
3765
  autoRotate: { type: "number", default: 0, min: 0, max: 12, ease: "smooth" },
@@ -4317,6 +4369,8 @@ export {
4317
4369
  GESTURES,
4318
4370
  OPEN_MARKS,
4319
4371
  parseVignette,
4372
+ snapBetween,
4373
+ poseAcross,
4320
4374
  poseOf,
4321
4375
  INTERIOR_MARKS,
4322
4376
  VIGNETTES,
@@ -4338,4 +4392,4 @@ export {
4338
4392
  paramSpaceWith,
4339
4393
  coerceNum
4340
4394
  };
4341
- //# sourceMappingURL=chunk-3RP7VMWG.js.map
4395
+ //# sourceMappingURL=chunk-7ZYKA2WS.js.map