@idle-screens/saver-metaquarium 0.17.0 → 0.18.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 },
@@ -2897,12 +2930,12 @@ var PARAM_DOCS = {
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
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.",
2900
- autoRotate: "Turntable speed, degrees/second. 0 holds the shot (stages, vignettes); 1\u20132 is a slow reveal.",
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.",
2901
2934
  // cast
2902
2935
  fishCount: "How many fish when `fishMix` is empty (all are `fishUrl`).",
2903
2936
  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
2937
  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.",
2938
+ 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
2939
  dracoPath: "Where the Draco decoder lives; leave empty on a channel.",
2907
2940
  swimSpeed: "Global swim speed multiplier. 0.4 is contemplative, 1 lively.",
2908
2941
  swimStyle: "How the cast swims unless a fishMix entry says otherwise (`@style`). `auto` picks by breed.",
@@ -2911,7 +2944,7 @@ var PARAM_DOCS = {
2911
2944
  swimVariance: "How differently individual fish swim from one another (speed, phase).",
2912
2945
  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
2946
  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.",
2947
+ maneuver: "Which set-piece moves the cast may break into. Vignette actors (slots 0\u20132) keep their marks: the script owns them.",
2915
2948
  maneuverRate: "How often maneuvers happen.",
2916
2949
  maneuverIntensity: "How big they are.",
2917
2950
  bodyWiggle: "Body yaw while swimming, for models without an animation clip. A fish the swim wave bends (`swimWave` > 0) ignores it.",
@@ -2925,8 +2958,8 @@ var PARAM_DOCS = {
2925
2958
  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
2959
  fishGlow: "How strongly GLOW- parts of a fish bloom and light their neighbours and the floor.",
2927
2960
  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.",
2961
+ 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.",
2962
+ 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
2963
  danceTempo: "Beats per minute for the starfish dance (60\u2013180, default 128). Glides on the beat.",
2931
2964
  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
2965
  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 +2993,9 @@ var PARAM_DOCS = {
2960
2993
  crystalTint: "How much nearby light sources colour a passing fish. 0.5\u20130.7 ties the cast to the place.",
2961
2994
  rockDensity: "Faceted boulders, an arch and a back ridge around the crystals.",
2962
2995
  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.",
2996
+ 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
2997
  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).",
2998
+ 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
2999
  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
3000
  geodeMix: "Which geodes: `kind[:weight]`, comma-separated. geode cathedral cluster cavern (the cavern is one at most).",
2968
3001
  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 +3027,12 @@ var PARAM_DOCS = {
2994
3027
  landmark: "One thing bigger than everything else. `castle`: voxel walls, crystal-spired towers, gate, road, plaza. `citadel`: two storeys. Marks: `gate`, `plaza`, `courtyard`.",
2995
3028
  // stage
2996
3029
  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.",
3030
+ 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
3031
  spotColor: "Colour of the single `followSpot`.",
2999
3032
  spotShadow: "The spotted fish's shadow in its pool. 0 none, 1 true size.",
3000
3033
  spotRig: "Up to three spots: `slot[/color][*radius]`. `0/#ff8ad0*26, 1/#7fdcff`. They are a, b, c in order. Overrides followSpot.",
3001
3034
  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."
3035
+ 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
3036
  };
3004
3037
  var GRAMMAR = `
3005
3038
  fishMix id[:count][@style][*size], \u2026 100:2@drift, 257:1, seahorse:3, shark:1@patrol*1.5
@@ -3028,14 +3061,16 @@ signs kind[@place][>target][/#color][*size][:text], \u2026 plank@gate:"Wel
3028
3061
  kinds plank arrow ring porthole neon led \xB7 places gate plaza courtyard hub fountain home1 home2 home3 centre left right front back
3029
3062
  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
3063
  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
3064
+ [<sec>s:] actor =mark be there, no travel (a later beat's = snaps at its start) actor >mark swim there
3065
+ 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
3066
+ actor follow actor gestures: talk nod shake hop spin wiggle rest peek bow \xB7 ink (octopus only: a cloud of ink)
3034
3067
  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
3068
  Give every beat a duration and end where you began and a spotCues sheet stays in step forever.
3036
3069
  Marks outdoors: ${Object.keys(OPEN_MARKS).join(" ")} \u2014 plus the world's own:
3037
3070
  home1 home1in \u2026 (geodeHomes), gate plaza courtyard (landmark), hub (paths), fountain (fountain).
3038
3071
  Marks indoors (interior: geode): ${Object.keys(INTERIOR_MARKS).join(" ")}
3072
+ rug centre: middle; chandelier: overhead; others bearing\xB0/distance/height (0\xB0=+x, 90\xB0=+z), \xD7max(crystalScale, 0.85):
3073
+ ${Object.entries(INTERIOR_MARK_POLAR).map(([n, p]) => `${n} ${p.deg}/${p.r}/${p.y}`).join(" \xB7 ")}
3039
3074
  `.trim();
3040
3075
  var NIGHT = { fogColor: "#0a0a1a", fogNear: 120, fogFar: 700 };
3041
3076
  var STAGE = { fogColor: "#01030a", floorColor: "#0a1322", fogNear: 120, fogFar: 650, autoRotate: 0 };
@@ -3470,8 +3505,17 @@ function dependencyProblems(params) {
3470
3505
  else if (spot2 >= cast) out.push({ path: "followSpot", also: castParams, message: noFish(spot2) });
3471
3506
  for (const s of rig) if (s.slot >= cast) out.push({ path: "spotRig", also: castParams, message: noFish(s.slot) });
3472
3507
  if (str("vignette")) {
3473
- const { actors } = parseVignette(resolveVignette(str("vignette")), { ...OPEN_MARKS, ...INTERIOR_MARKS });
3508
+ const v = parseVignette(resolveVignette(str("vignette")), { ...OPEN_MARKS, ...INTERIOR_MARKS });
3509
+ const { actors } = v;
3474
3510
  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}` });
3511
+ const slotBreeds = expandFishMixSlots(mix, 3).map((sl) => sl.breed);
3512
+ for (const [g, breeds] of Object.entries(BREED_GESTURES)) {
3513
+ for (let a = 0; a < actors; a++) {
3514
+ if (!v.beats.some((b) => b.actors[a]?.gesture === g)) continue;
3515
+ const breed = slotBreeds[a] ?? "fish";
3516
+ 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}` });
3517
+ }
3518
+ }
3475
3519
  }
3476
3520
  const dance = str("starfishDance", "off") || "off";
3477
3521
  const starfishSwims = mix.some((e, i) => e.breed === "starfish" && mix.slice(0, i).reduce((n, p) => n + p.count, 0) < CAST_CAP);
@@ -3510,6 +3554,7 @@ var BREED_NOUN = {
3510
3554
  octopus: ["octopus", "octopuses"],
3511
3555
  snail: ["snail", "snails"],
3512
3556
  dot: ["Dot", "Dots"],
3557
+ dolphin: ["dolphin", "dolphins"],
3513
3558
  starfish: ["starfish", "starfish"],
3514
3559
  crab: ["crab", "crabs"],
3515
3560
  shark: ["shark", "sharks"],
@@ -4317,6 +4362,8 @@ export {
4317
4362
  GESTURES,
4318
4363
  OPEN_MARKS,
4319
4364
  parseVignette,
4365
+ snapBetween,
4366
+ poseAcross,
4320
4367
  poseOf,
4321
4368
  INTERIOR_MARKS,
4322
4369
  VIGNETTES,
@@ -4338,4 +4385,4 @@ export {
4338
4385
  paramSpaceWith,
4339
4386
  coerceNum
4340
4387
  };
4341
- //# sourceMappingURL=chunk-3RP7VMWG.js.map
4388
+ //# sourceMappingURL=chunk-G2ZD33CA.js.map