@idle-screens/saver-metaquarium 0.14.0 → 0.15.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 +20 -1
- package/dist/{chunk-XLPOK4TE.js → chunk-XPNWX4OU.js} +266 -4
- package/dist/chunk-XPNWX4OU.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/{lofi-tank-CYX6HD25.js → lofi-tank-C3G5GDYY.js} +2 -2
- package/dist/{manifest-8ZSA9JzK.d.ts → manifest-VELV2chO.d.ts} +102 -2
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +5 -1
- package/dist/octopus-KWO7IBFE.js +6 -0
- package/dist/octopus-KWO7IBFE.js.map +1 -0
- package/dist/{tank-FKPMBCUY.js → tank-QQUJMA66.js} +587 -341
- package/dist/tank-QQUJMA66.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-XLPOK4TE.js.map +0 -1
- package/dist/octopus-SXLCCN76.js +0 -6
- package/dist/octopus-SXLCCN76.js.map +0 -1
- package/dist/tank-FKPMBCUY.js.map +0 -1
- /package/dist/{lofi-tank-CYX6HD25.js.map → lofi-tank-C3G5GDYY.js.map} +0 -0
package/README.md
CHANGED
|
@@ -232,6 +232,24 @@ textures, downloads, shadow maps or additional lights. Flora, shards, snow and
|
|
|
232
232
|
bubbles scale with the existing device prop budget. `inspect().props.scenery`
|
|
233
233
|
reports actual populations; prop draw-call/triangle totals include the world.
|
|
234
234
|
|
|
235
|
+
## Reading a scene back
|
|
236
|
+
|
|
237
|
+
A SaverSpec explains itself; a metaquarium scene is `{id: "metaquarium"}` plus a control track. `describeMetaquarium` reads that back as parts, using the tank's own parsers and room presets, for a viewer's scene card, a channel's state, or an agent with no eyes:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { describeMetaquarium, metaquariumParamsFromTrack } from '@idle-screens/saver-metaquarium/manifest';
|
|
241
|
+
|
|
242
|
+
const { params, timed } = metaquariumParamsFromTrack(scene.track.deltas); // in force at mount, plus later choreography
|
|
243
|
+
const a = describeMetaquarium(params, timed);
|
|
244
|
+
a.summary; // '3 octopuses, a starfish, a crab and 4 baby fish in the reef; slow orbit.'
|
|
245
|
+
a.cast; // [{ slots: [0, 2], count: 3, breed: 'octopus', label: 'octopuses', motion: 'floor', … }, …]
|
|
246
|
+
a.sections; // [{ name: 'Room', items: ['Reef', 'dunes floor', 'water overhead'] }, { name: 'Camera', … }, …]
|
|
247
|
+
a.palette; // the author's colours, else the room's own
|
|
248
|
+
a.problems; // params set but doing nothing (validateMetaquariumParams)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
It reports only what the params say, and only what the author set (plus the room and camera, which every scene has).
|
|
252
|
+
|
|
235
253
|
## For agents (MCP)
|
|
236
254
|
|
|
237
255
|
Everything an agent needs to author a scene is data, importable without three.js:
|
|
@@ -242,6 +260,7 @@ import { RECIPES, recipeTrack, PARAM_DOCS, GRAMMAR, validateMetaquariumParams }
|
|
|
242
260
|
const scene = RECIPES.find((r) => r.id === 'stage-duet')!;
|
|
243
261
|
publishScene({ spec: { id: 'metaquarium' }, track: recipeTrack(scene.params) });
|
|
244
262
|
validateMetaquariumParams({ vignette: 'a >home1', geodeHomes: 0 }); // → [{ path: 'vignette', message: 'beat 1: no mark "home1" …' }]
|
|
263
|
+
validateMetaquariumParams({ fishMix: 'dori:3,blowfish:2', fishCount: 14 }); // → [{ path: 'fishCount', also: ['fishMix'], message: 'ignored: a non-empty fishMix sets the cast (5 fish) …' }]
|
|
245
264
|
```
|
|
246
265
|
|
|
247
|
-
`PARAM_DOCS` has one line per param (a test keeps it complete), `GRAMMAR` documents the five small DSLs and the marks a vignette can name, and the playground mounts every recipe unchanged on its "recipes" shelf.
|
|
266
|
+
`PARAM_DOCS` has one line per param (a test keeps it complete), `GRAMMAR` documents the five small DSLs and the marks a vignette can name, and the playground mounts every recipe unchanged on its "recipes" shelf. `validateMetaquariumParams` also names the params that would do nothing: one another param overrides (`fishCount` beside a `fishMix`), a slot past the end of the cast, or a dial on something switched off (`shoalKind` with no shoal); a problem that involves other params lists them in `also`.
|
|
@@ -1898,7 +1898,7 @@ function parseFloraMix(src) {
|
|
|
1898
1898
|
for (const raw of src.split(",")) {
|
|
1899
1899
|
const part = raw.trim();
|
|
1900
1900
|
if (!part) continue;
|
|
1901
|
-
const m = /^([a-z]+)\s*(?::\s*([0-9]
|
|
1901
|
+
const m = /^([a-z]+)\s*(?::\s*([0-9]*\.?[0-9]+))?$/i.exec(part);
|
|
1902
1902
|
const name = m?.[1]?.toLowerCase();
|
|
1903
1903
|
if (!m || !name || !FLORA_SPECIES.includes(name)) {
|
|
1904
1904
|
problems.push(`"${part}": expected species[:weight] with species one of ${FLORA_SPECIES.join(", ")}`);
|
|
@@ -1946,7 +1946,7 @@ function parseGeodeMix(src) {
|
|
|
1946
1946
|
for (const raw of src.split(",")) {
|
|
1947
1947
|
const part = raw.trim();
|
|
1948
1948
|
if (!part) continue;
|
|
1949
|
-
const m = /^([a-z]+)\s*(?::\s*([0-9]
|
|
1949
|
+
const m = /^([a-z]+)\s*(?::\s*([0-9]*\.?[0-9]+))?$/i.exec(part);
|
|
1950
1950
|
const name = m?.[1]?.toLowerCase();
|
|
1951
1951
|
if (!m || !name || !GEODE_KINDS.includes(name)) {
|
|
1952
1952
|
problems.push(`"${part}": expected kind[:weight] with kind one of ${GEODE_KINDS.join(", ")}`);
|
|
@@ -2390,6 +2390,7 @@ var PARAM_DOCS = {
|
|
|
2390
2390
|
danceTempo: "Beats per minute for the starfish dance (60\u2013180, default 128). Glides on the beat.",
|
|
2391
2391
|
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.",
|
|
2392
2392
|
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.",
|
|
2393
|
+
fishAvoid: "Fish make way for each other instead of swimming through one another: a pair about to meet parts a second ahead, the smaller fish giving more, over or under before round. 1 (default) is on, 0 is the old pass-through. `inspect().crowding` counts the pairs still touching.",
|
|
2393
2394
|
eyeLife: "Eyes blink, look, and emote (0 = painted on). Redraws each token's own pixel-grid eye; black and white only.",
|
|
2394
2395
|
// water and room
|
|
2395
2396
|
fogColor: "The water colour; also the background. Dark blues and purples make light sources read.",
|
|
@@ -2709,6 +2710,30 @@ var RECIPES = [
|
|
|
2709
2710
|
cameraAzimuth: 0,
|
|
2710
2711
|
autoRotate: 1
|
|
2711
2712
|
}
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
id: "reef-characters",
|
|
2716
|
+
label: "Reef characters",
|
|
2717
|
+
what: "The bundled characters on a coral reef: tangs that look back at you, blowfish that puff up and flirt, an octopus that changes colour and inks, a crab on the floor. A slow orbit.",
|
|
2718
|
+
params: {
|
|
2719
|
+
...ALIVE,
|
|
2720
|
+
environment: "reef",
|
|
2721
|
+
fishMix: "dori:3,blowfish:2,octopus:1,crab:1",
|
|
2722
|
+
floraDensity: 0.6,
|
|
2723
|
+
marineSnow: 0.3,
|
|
2724
|
+
finish: 0.5,
|
|
2725
|
+
swimSpeed: 0.7,
|
|
2726
|
+
cameraDistance: 160,
|
|
2727
|
+
cameraElevation: 13,
|
|
2728
|
+
cameraAzimuth: 20,
|
|
2729
|
+
autoRotate: 0.6
|
|
2730
|
+
}
|
|
2731
|
+
},
|
|
2732
|
+
{
|
|
2733
|
+
id: "starfish-class",
|
|
2734
|
+
label: "Starfish aerobics",
|
|
2735
|
+
what: "Seven starfish stand up on a pale ice floor, face the camera and dance an aerobics routine in unison, an instructor in front.",
|
|
2736
|
+
params: { environment: "ice", fishMix: "starfish:7", starfishDance: "aerobics", danceTempo: 120, shot: "front", autoRotate: 0 }
|
|
2712
2737
|
}
|
|
2713
2738
|
];
|
|
2714
2739
|
var recipe = (id) => RECIPES.find((r) => r.id === id);
|
|
@@ -2748,8 +2773,238 @@ function validateMetaquariumParams(params) {
|
|
|
2748
2773
|
if (params.fountain === "vent" || params.fountain === "geode") world.fountain = at;
|
|
2749
2774
|
push("vignette", parseVignette(resolveVignette(str("vignette")), indoors ? INTERIOR_MARKS : { ...OPEN_MARKS, ...world }).problems);
|
|
2750
2775
|
}
|
|
2776
|
+
out.push(...dependencyProblems(params));
|
|
2751
2777
|
return out;
|
|
2752
2778
|
}
|
|
2779
|
+
var CAST_CAP = 24;
|
|
2780
|
+
var ORBIT_PARAMS = ["cameraElevation", "cameraDistance"];
|
|
2781
|
+
function dependencyProblems(params) {
|
|
2782
|
+
const out = [];
|
|
2783
|
+
const set = (k) => params[k] !== void 0 && params[k] !== null;
|
|
2784
|
+
const num = (k, fallback) => {
|
|
2785
|
+
const v = Number(params[k]);
|
|
2786
|
+
return set(k) && Number.isFinite(v) ? v : fallback;
|
|
2787
|
+
};
|
|
2788
|
+
const str = (k, fallback = "") => typeof params[k] === "string" ? params[k].trim() : fallback;
|
|
2789
|
+
const mix = str("fishMix") ? parseFishMix(str("fishMix")).entries : [];
|
|
2790
|
+
const mixTotal = mix.reduce((n, e) => n + e.count, 0);
|
|
2791
|
+
const cast = mix.length ? Math.min(mixTotal, CAST_CAP) : Math.min(Math.max(1, Math.round(num("fishCount", 1))), CAST_CAP);
|
|
2792
|
+
const castWhy = mix.length ? `fishMix casts ${cast}` : `the cast is fishCount ${cast}`;
|
|
2793
|
+
const castParams = mix.length ? ["fishMix"] : ["fishCount"];
|
|
2794
|
+
if (mix.length) {
|
|
2795
|
+
for (const k of ["fishCount", "fishUrl"]) {
|
|
2796
|
+
if (set(k)) out.push({ path: k, also: ["fishMix"], message: `ignored: a non-empty fishMix sets the cast (${mixTotal} fish) \u2014 put the counts in fishMix` });
|
|
2797
|
+
}
|
|
2798
|
+
if (mixTotal > CAST_CAP) out.push({ path: "fishMix", message: `casts ${mixTotal} fish, but at most ${CAST_CAP} swim (fewer on smaller devices): slots ${CAST_CAP}+ never appear` });
|
|
2799
|
+
}
|
|
2800
|
+
const noFish = (slot) => `slot ${slot} has no fish \u2014 ${castWhy} (slots 0\u2013${cast - 1})`;
|
|
2801
|
+
const follow = Math.round(num("cameraFollow", -1));
|
|
2802
|
+
if (follow >= cast) out.push({ path: "cameraFollow", also: castParams, message: noFish(follow) });
|
|
2803
|
+
const rig = parseSpotRig(str("spotRig")).spots;
|
|
2804
|
+
const spot2 = Math.round(num("followSpot", -1));
|
|
2805
|
+
if (rig.length && set("followSpot")) out.push({ path: "followSpot", also: ["spotRig"], message: "ignored: a spotRig replaces the single followSpot" });
|
|
2806
|
+
else if (spot2 >= cast) out.push({ path: "followSpot", also: castParams, message: noFish(spot2) });
|
|
2807
|
+
for (const s of rig) if (s.slot >= cast) out.push({ path: "spotRig", also: castParams, message: noFish(s.slot) });
|
|
2808
|
+
if (str("vignette")) {
|
|
2809
|
+
const { actors } = parseVignette(resolveVignette(str("vignette")), { ...OPEN_MARKS, ...INTERIOR_MARKS });
|
|
2810
|
+
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}` });
|
|
2811
|
+
}
|
|
2812
|
+
const dance = str("starfishDance", "off") || "off";
|
|
2813
|
+
const starfishSwims = mix.some((e, i) => e.breed === "starfish" && mix.slice(0, i).reduce((n, p) => n + p.count, 0) < CAST_CAP);
|
|
2814
|
+
if (dance !== "off" && !starfishSwims) {
|
|
2815
|
+
out.push({ path: "starfishDance", also: ["fishMix"], message: "no starfish in the cast to dance \u2014 add e.g. starfish:7 (slots past the cap never swim)" });
|
|
2816
|
+
}
|
|
2817
|
+
if (set("danceTempo") && dance === "off") out.push({ path: "danceTempo", also: ["starfishDance"], message: "does nothing while starfishDance is off" });
|
|
2818
|
+
if (follow < 0) {
|
|
2819
|
+
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)" });
|
|
2820
|
+
} else if (follow < cast) {
|
|
2821
|
+
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` });
|
|
2822
|
+
}
|
|
2823
|
+
const spotOn = rig.length > 0 || spot2 >= 0;
|
|
2824
|
+
if (!spotOn) {
|
|
2825
|
+
for (const k of ["spotStrength", "spotShadow", "spotColor"]) if (set(k)) out.push({ path: k, also: ["followSpot", "spotRig"], message: "does nothing with no spot on (followSpot is -1 and spotRig is empty)" });
|
|
2826
|
+
} else if (rig.length && set("spotColor")) {
|
|
2827
|
+
out.push({ path: "spotColor", also: ["spotRig"], message: "colours the single followSpot; a spotRig gives each of its spots its own colour (slot/#rrggbb)" });
|
|
2828
|
+
}
|
|
2829
|
+
if (Math.round(num("shoal", 0) * CAST_CAP * 2.5) < 3) {
|
|
2830
|
+
for (const k of ["shoalKind", "shoalSpeed"]) if (set(k)) out.push({ path: k, also: ["shoal"], message: "does nothing while shoal is too low to make a school (under 3 fish; 0 is off)" });
|
|
2831
|
+
}
|
|
2832
|
+
if (num("floraDensity", 0) <= 0) {
|
|
2833
|
+
for (const k of ["floraMix", "floraPalette", "floraLayout"]) if (set(k)) out.push({ path: k, also: ["floraDensity"], message: "does nothing while floraDensity is 0 (no plants)" });
|
|
2834
|
+
}
|
|
2835
|
+
return out;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/anatomy.ts
|
|
2839
|
+
var CAST_CAP2 = 24;
|
|
2840
|
+
var FLOOR_BREEDS = /* @__PURE__ */ new Set(["crab", "starfish", "octopus"]);
|
|
2841
|
+
var BREED_NOUN = {
|
|
2842
|
+
dori: ["tang", "tangs"],
|
|
2843
|
+
babyfish: ["baby fish", "baby fish"],
|
|
2844
|
+
blowfish: ["blowfish", "blowfish"],
|
|
2845
|
+
octopus: ["octopus", "octopuses"],
|
|
2846
|
+
starfish: ["starfish", "starfish"],
|
|
2847
|
+
crab: ["crab", "crabs"],
|
|
2848
|
+
shark: ["shark", "sharks"],
|
|
2849
|
+
glowfish: ["glowfish", "glowfish"],
|
|
2850
|
+
hackerfish: ["hackerfish", "hackerfish"],
|
|
2851
|
+
betafish: ["betafish", "betafish"],
|
|
2852
|
+
angelfish: ["angelfish", "angelfish"],
|
|
2853
|
+
seahorse: ["seahorse", "seahorses"],
|
|
2854
|
+
seaturtle: ["sea turtle", "sea turtles"]
|
|
2855
|
+
};
|
|
2856
|
+
var noun = (breed, n) => (BREED_NOUN[breed] ?? [breed, `${breed}s`])[n === 1 ? 0 : 1];
|
|
2857
|
+
function metaquariumParamsFromTrack(deltas) {
|
|
2858
|
+
const params = {};
|
|
2859
|
+
const timed = [];
|
|
2860
|
+
for (const d of deltas ?? []) {
|
|
2861
|
+
if (!d || typeof d.path !== "string") continue;
|
|
2862
|
+
const t = typeof d.t === "number" && Number.isFinite(d.t) ? d.t : 0;
|
|
2863
|
+
if (t <= 0 || typeof d.liveAt === "number") params[d.path] = d.value;
|
|
2864
|
+
else timed.push({ atMs: t, path: d.path, value: d.value });
|
|
2865
|
+
}
|
|
2866
|
+
timed.sort((a, b) => a.atMs - b.atMs);
|
|
2867
|
+
return { params, timed };
|
|
2868
|
+
}
|
|
2869
|
+
var defaultOf = (k) => METAQUARIUM_PARAMS[k]?.default;
|
|
2870
|
+
function describeMetaquarium(params, timed = []) {
|
|
2871
|
+
const has = (k) => params[k] !== void 0 && params[k] !== null && params[k] !== "";
|
|
2872
|
+
const val = (k) => has(k) ? params[k] : defaultOf(k);
|
|
2873
|
+
const num = (k) => {
|
|
2874
|
+
const n = Number(val(k));
|
|
2875
|
+
return Number.isFinite(n) ? n : 0;
|
|
2876
|
+
};
|
|
2877
|
+
const str = (k) => String(val(k) ?? "").trim();
|
|
2878
|
+
const pct = (k) => `${Math.round(num(k) * 100)}%`;
|
|
2879
|
+
const cast = [];
|
|
2880
|
+
const swimStyle = str("swimStyle");
|
|
2881
|
+
const motionOf = (breed, style) => FLOOR_BREEDS.has(breed) ? "floor" : style ?? (has("swimStyle") ? swimStyle : "swims");
|
|
2882
|
+
const mix = has("fishMix") ? parseFishMix(str("fishMix")).entries : [];
|
|
2883
|
+
if (mix.length) {
|
|
2884
|
+
const slots = expandFishMixSlots(mix, Number.MAX_SAFE_INTEGER);
|
|
2885
|
+
let at = 0;
|
|
2886
|
+
for (const e of mix) {
|
|
2887
|
+
const minted = isMintedId(e.id);
|
|
2888
|
+
cast.push({
|
|
2889
|
+
slots: [at, at + e.count - 1],
|
|
2890
|
+
count: e.count,
|
|
2891
|
+
visible: Math.max(0, Math.min(e.count, CAST_CAP2 - at)),
|
|
2892
|
+
breed: e.breed,
|
|
2893
|
+
label: minted ? e.count > 1 ? `${e.breed} from #${e.id}` : `${e.breed} #${e.id}` : noun(e.breed, e.count),
|
|
2894
|
+
motion: motionOf(e.breed, e.style),
|
|
2895
|
+
size: slots[at]?.size ?? null,
|
|
2896
|
+
offstage: at >= CAST_CAP2
|
|
2897
|
+
});
|
|
2898
|
+
at += e.count;
|
|
2899
|
+
}
|
|
2900
|
+
} else {
|
|
2901
|
+
const n = Math.min(Math.max(1, Math.round(num("fishCount"))), CAST_CAP2);
|
|
2902
|
+
const id = Number(/fish_(\d+)_/.exec(str("fishUrl"))?.[1] ?? NaN);
|
|
2903
|
+
const breed = Number.isFinite(id) && breedOf(id) || "fish";
|
|
2904
|
+
cast.push({ slots: [0, n - 1], count: n, visible: n, breed, label: Number.isFinite(id) ? `${breed} #${id}` : breed, motion: motionOf(breed), size: null, offstage: false });
|
|
2905
|
+
}
|
|
2906
|
+
const castTotal = Math.min(cast.reduce((n, r) => n + r.count, 0), CAST_CAP2);
|
|
2907
|
+
const slotName = (slot) => {
|
|
2908
|
+
const row = cast.find((r) => slot >= r.slots[0] && slot <= r.slots[1]);
|
|
2909
|
+
return row ? `slot ${slot} (${row.breed === "fish" ? "fish" : noun(row.breed, 1)})` : `slot ${slot} (empty)`;
|
|
2910
|
+
};
|
|
2911
|
+
const env = environmentOf(str("environment"));
|
|
2912
|
+
const room = [env.label];
|
|
2913
|
+
if (has("floorKind") && str("floorKind") !== "auto") room.push(`${str("floorKind")} floor`);
|
|
2914
|
+
else room.push(`${env.floor} floor`);
|
|
2915
|
+
if (env.water) room.push("water overhead");
|
|
2916
|
+
if (num("water") > 0) room.push(`water ${pct("water")}${has("waterClarity") ? `, clarity ${pct("waterClarity")}` : ""}`);
|
|
2917
|
+
if (num("caustics") > 0) room.push(`caustics ${pct("caustics")}`);
|
|
2918
|
+
if (num("surfaceMirror") > 0) room.push(`surface mirror ${pct("surfaceMirror")}`);
|
|
2919
|
+
const rays = has("rayStrength") && num("rayStrength") >= 0 ? num("rayStrength") : env.rays?.strength ?? null;
|
|
2920
|
+
if (rays !== null) room.push(rays === 0 ? "no light shafts" : `light shafts ${Math.round(rays * 100)}%`);
|
|
2921
|
+
if (has("fogNear") || has("fogFar")) room.push(`fog ${num("fogNear")}\u2013${num("fogFar")}`);
|
|
2922
|
+
if (str("interior") === "geode") room.push("inside a geode home");
|
|
2923
|
+
const world = [];
|
|
2924
|
+
if (str("landmark") !== "none") world.push(`the ${str("landmark")}`);
|
|
2925
|
+
if (num("geodeHomes") > 0) world.push(`${num("geodeHomes")} geode home${num("geodeHomes") === 1 ? "" : "s"}`);
|
|
2926
|
+
if (num("paths") > 0) world.push(`paths${has("pathMaterial") && str("pathMaterial") !== "auto" ? ` (${str("pathMaterial")})` : ""}`);
|
|
2927
|
+
if (str("fountain") !== "none") world.push(`${str("fountain")} fountain`);
|
|
2928
|
+
if (num("streetLamps") > 0) world.push("street lamps");
|
|
2929
|
+
if (num("floraDensity") > 0) world.push(`plants ${pct("floraDensity")}${has("floraMix") ? `: ${str("floraMix")}` : ""}`);
|
|
2930
|
+
if (num("rockDensity") > 0) world.push(`rocks ${pct("rockDensity")}`);
|
|
2931
|
+
if (num("geodes") > 0) world.push(`wild geodes ${pct("geodes")}`);
|
|
2932
|
+
if (has("propMix")) {
|
|
2933
|
+
const props = parsePropMix(str("propMix")).entries;
|
|
2934
|
+
const n = props.reduce((k, e) => k + e.count, 0);
|
|
2935
|
+
if (n) world.push(`${n} crystal${n === 1 ? "" : "s"}: ${props.map((e) => `${e.count > 1 ? `${e.count} ` : ""}${e.palette === "env" ? "" : `${e.palette} `}${e.habit}${e.size > 1 ? ` \xD7${e.size}` : ""}`).join(", ")}`);
|
|
2936
|
+
}
|
|
2937
|
+
if (num("skyLanterns") > 0) world.push("jellyfish lanterns overhead");
|
|
2938
|
+
if (num("horizon") > 0) world.push("a far horizon");
|
|
2939
|
+
if (num("bubbleVents") > 0) world.push("bubble vents");
|
|
2940
|
+
if (num("marineSnow") > 0) world.push("marine snow");
|
|
2941
|
+
if (num("co2Mist") > 0) world.push("CO\u2082 mist");
|
|
2942
|
+
if (num("pearling") > 0) world.push("pearling plants");
|
|
2943
|
+
if (Math.round(num("shoal") * CAST_CAP2 * 2.5) >= 3) world.push(`a school of ${str("shoalKind")}`);
|
|
2944
|
+
const stage = [];
|
|
2945
|
+
const rig = parseSpotRig(str("spotRig")).spots;
|
|
2946
|
+
if (rig.length) stage.push(`${rig.length} spotlight${rig.length === 1 ? "" : "s"} on ${rig.map((s) => slotName(s.slot)).join(", ")}`);
|
|
2947
|
+
else if (num("followSpot") >= 0) stage.push(`follow-spot on ${slotName(num("followSpot"))}`);
|
|
2948
|
+
if (has("spotCues") && (rig.length || num("followSpot") >= 0)) {
|
|
2949
|
+
const sheet = parseSpotCues(str("spotCues"), Math.max(1, rig.length));
|
|
2950
|
+
stage.push(`${sheet.cues.length} light cues, ${Math.round(sheet.duration)}s loop`);
|
|
2951
|
+
}
|
|
2952
|
+
if (has("vignette")) {
|
|
2953
|
+
const v = str("vignette");
|
|
2954
|
+
stage.push(VIGNETTES[v] ? `vignette: ${v}` : `vignette: ${v.split(/[|\n]/).filter((b) => b.trim()).length} beats`);
|
|
2955
|
+
}
|
|
2956
|
+
if (str("starfishDance") !== "off") stage.push(`starfish ${str("starfishDance")}${has("danceTempo") ? ` at ${num("danceTempo")} bpm` : ""}`);
|
|
2957
|
+
const camera = [];
|
|
2958
|
+
const follow = Math.round(num("cameraFollow"));
|
|
2959
|
+
if (follow >= 0) {
|
|
2960
|
+
camera.push(`rides with ${slotName(follow)}`);
|
|
2961
|
+
if (has("followDistance")) camera.push(`${num("followDistance")} behind`);
|
|
2962
|
+
if (has("followAngle") && num("followAngle") !== 0) camera.push(`${num("followAngle")}\xB0 round`);
|
|
2963
|
+
} else {
|
|
2964
|
+
if (str("shot") !== "orbit") camera.push(`${str("shot")} shot`);
|
|
2965
|
+
camera.push(`distance ${num("cameraDistance")}`, `elevation ${num("cameraElevation")}\xB0`);
|
|
2966
|
+
const spin = num("autoRotate");
|
|
2967
|
+
camera.push(spin > 0 ? `orbits ${spin}\xB0/s` : "holds still");
|
|
2968
|
+
}
|
|
2969
|
+
const motion = [];
|
|
2970
|
+
if (has("swimSpeed")) motion.push(`speed \xD7${num("swimSpeed")}`);
|
|
2971
|
+
if (has("pathShape")) motion.push(`${str("pathShape")} paths`);
|
|
2972
|
+
if (has("formationShape")) motion.push(`${str("formationShape")} formation`);
|
|
2973
|
+
if (str("maneuver") !== "none") motion.push(`${str("maneuver")} maneuvers`);
|
|
2974
|
+
if (num("bodyWiggle") > 0) motion.push(`wiggle ${pct("bodyWiggle")}`);
|
|
2975
|
+
if (has("fishSize") && num("fishSize") !== 1) motion.push(`fish \xD7${num("fishSize")} size`);
|
|
2976
|
+
const look = [];
|
|
2977
|
+
if (str("fishLighting") === "flat") look.push("flat-lit fish");
|
|
2978
|
+
if (str("fishLook") === "neon") look.push("neon (blacklight) coats");
|
|
2979
|
+
if (num("eyeLife") > 0) look.push("living eyes");
|
|
2980
|
+
if (num("finish") > 0) look.push(`finish ${pct("finish")}`);
|
|
2981
|
+
if (has("fishGlow")) look.push(`glow ${pct("fishGlow")}`);
|
|
2982
|
+
if (num("fishAmbient") > 0) look.push(`fish ambient ${pct("fishAmbient")}`);
|
|
2983
|
+
if (str("dither") === "on") look.push("dither");
|
|
2984
|
+
const sections = [["Room", room], ["World", world], ["Stage", stage], ["Camera", camera], ["Motion", motion], ["Look", look]].filter(([, items]) => items.length > 0).map(([name, items]) => ({ name, items: [...items] }));
|
|
2985
|
+
const palette = [];
|
|
2986
|
+
const add = (c) => {
|
|
2987
|
+
if (typeof c === "string" && /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(c) && !palette.includes(c.toLowerCase())) palette.push(c.toLowerCase());
|
|
2988
|
+
};
|
|
2989
|
+
const roomOr = (k, room2) => add(has(k) ? params[k] : room2 ?? defaultOf(k));
|
|
2990
|
+
roomOr("fogColor", env.palette?.fog);
|
|
2991
|
+
roomOr("floorColor", env.palette?.floor);
|
|
2992
|
+
if (num("moteDensity") > 0 || num("marineSnow") > 0 || env.palette) roomOr("moteColor", env.palette?.mote);
|
|
2993
|
+
if (num("water") > 0) roomOr("waterTint", env.palette?.tint);
|
|
2994
|
+
for (const s of rig) add(s.color);
|
|
2995
|
+
if (!rig.length && num("followSpot") >= 0) add(str("spotColor"));
|
|
2996
|
+
if (has("floraPalette")) for (const c of str("floraPalette").split(",")) add(c.trim());
|
|
2997
|
+
const swimming = cast.filter((r) => r.visible > 0);
|
|
2998
|
+
const nameOf = (r) => r.visible < r.count && !r.label.includes("#") ? noun(r.breed, r.visible) : r.label;
|
|
2999
|
+
const names = swimming.map((r) => r.visible === 1 ? `${/^[aeiou]/.test(nameOf(r)) ? "an" : "a"} ${nameOf(r)}` : `${r.visible} ${nameOf(r)}`);
|
|
3000
|
+
const list = names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}` : names[0] ?? "no fish";
|
|
3001
|
+
const where = str("interior") === "geode" ? "inside a geode home" : `in the ${env.label.toLowerCase()}`;
|
|
3002
|
+
const act = stage.find((x) => x.startsWith("starfish ")) ?? stage.find((x) => x.startsWith("vignette")) ?? stage[0];
|
|
3003
|
+
const extra = act ?? (str("landmark") !== "none" ? `around the ${str("landmark")}` : num("geodeHomes") > 0 ? "in a village" : null);
|
|
3004
|
+
const cam = follow >= 0 ? `following ${slotName(follow)}` : num("autoRotate") > 0 ? "slow orbit" : "still camera";
|
|
3005
|
+
const summary = `${list} ${where}${extra ? `, ${extra}` : ""}; ${cam}.`;
|
|
3006
|
+
return { summary, cast, castTotal, sections, palette, timed, problems: validateMetaquariumParams(params) };
|
|
3007
|
+
}
|
|
2753
3008
|
|
|
2754
3009
|
// src/manifest.ts
|
|
2755
3010
|
var METAQUARIUM_PARAMS = {
|
|
@@ -3003,6 +3258,11 @@ var METAQUARIUM_PARAMS = {
|
|
|
3003
3258
|
/** A body wave from nose to tail, beating with distance swum and curling
|
|
3004
3259
|
* into turns. 0 keeps the legacy rigid wiggle and the stock programs. */
|
|
3005
3260
|
swimWave: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
|
|
3261
|
+
/** Elbow room, 0..1: fish see each other coming and make way — the smaller
|
|
3262
|
+
* gives more, over or under before round. Pure in t, like every motion here.
|
|
3263
|
+
* On by default, the one exception to "defaults keep the old look":
|
|
3264
|
+
* fish swimming through fish was a bug, not a look. 0 is the old tank. */
|
|
3265
|
+
fishAvoid: { type: "number", default: 1, min: 0, max: 1, ease: "smooth" },
|
|
3006
3266
|
/** Independent mineral-world layers. Zero preserves legacy scenes; counts
|
|
3007
3267
|
* are reduced by the device's existing prop budget. All motion is analytic. */
|
|
3008
3268
|
rockDensity: { type: "number", default: 0, min: 0, max: 1, ease: "step" },
|
|
@@ -3180,7 +3440,7 @@ var BLOOM_COLORS = [
|
|
|
3180
3440
|
var metaquariumManifest = {
|
|
3181
3441
|
id: "metaquarium",
|
|
3182
3442
|
label: "Metaquarium",
|
|
3183
|
-
description: "A
|
|
3443
|
+
description: "A voxel fish tank you configure by params: a cast of fish and characters (tangs, blowfish, an octopus, crabs, starfish, minted fish) in rooms, gardens, villages and castles, with stage spotlights and small scripted scenes.",
|
|
3184
3444
|
minBackend: "webgl2",
|
|
3185
3445
|
costTier: "medium",
|
|
3186
3446
|
motionIntensity: "calm",
|
|
@@ -3325,6 +3585,8 @@ export {
|
|
|
3325
3585
|
recipe,
|
|
3326
3586
|
recipeTrack,
|
|
3327
3587
|
validateMetaquariumParams,
|
|
3588
|
+
metaquariumParamsFromTrack,
|
|
3589
|
+
describeMetaquarium,
|
|
3328
3590
|
METAQUARIUM_PARAMS,
|
|
3329
3591
|
MIAMI_VICE_COLORS,
|
|
3330
3592
|
BLOOM_COLORS,
|
|
@@ -3333,4 +3595,4 @@ export {
|
|
|
3333
3595
|
paramSpaceWith,
|
|
3334
3596
|
coerceNum
|
|
3335
3597
|
};
|
|
3336
|
-
//# sourceMappingURL=chunk-
|
|
3598
|
+
//# sourceMappingURL=chunk-XPNWX4OU.js.map
|