@idle-screens/saver-metaquarium 0.5.1 → 0.7.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 +4 -1
- package/dist/{chunk-23P73MYY.js → chunk-RTUYWDDZ.js} +267 -51
- package/dist/chunk-RTUYWDDZ.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{manifest-Bk9LNZb9.d.ts → manifest-C25MXGwc.d.ts} +296 -144
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +15 -1
- package/dist/{tank-QQMOLZUJ.js → tank-HZJV2GD7.js} +314 -49
- package/dist/tank-HZJV2GD7.js.map +1 -0
- package/package.json +3 -3
- package/dist/chunk-23P73MYY.js.map +0 -1
- package/dist/tank-QQMOLZUJ.js.map +0 -1
package/README.md
CHANGED
|
@@ -58,12 +58,15 @@ frame loop (rAF or renderFrame(t)):
|
|
|
58
58
|
| swimSpeed | number | 1 | Swim time-scale multiplier |
|
|
59
59
|
| fogColor | color | #030009 | Atmosphere / background |
|
|
60
60
|
| fishUrl | string | ipfs://…/fish_257_….glb | GLB model URL, single-breed mode (playground can override to a local asset) |
|
|
61
|
-
| fishMix | string | "" | Mixed population DSL: `id[:count]` comma-separated, catalog ids or breed aliases (`"257:2,100:1"`). Non-empty overrides fishUrl + fishCount; counts absolute, tier-capped |
|
|
61
|
+
| fishMix | string | "" | Mixed population DSL: `id[:count][@style]` comma-separated, catalog ids or breed aliases (`"257:2,100:1"`, `"457:3@hover,257:6@school"` — per-token swim style; untagged tokens follow `swimStyle`). Non-empty overrides fishUrl + fishCount; counts absolute, tier-capped |
|
|
62
62
|
| fogNear | number | 60 | Fog start distance (the old hardcoded Fog near) |
|
|
63
63
|
| fogFar | number | 500 | Fog full-opacity distance; tank enforces > near + 20 |
|
|
64
64
|
| moteDensity | number | 0 | Plankton motes, 0–1 of the tier budget (400/250/120). 0 = off |
|
|
65
65
|
| moteColor | color | #7fd6ff | Mote tint |
|
|
66
66
|
| floorColor | color | #0a1d33 | Floor disc color (the old hardcoded navy) |
|
|
67
|
+
| swimStyle | enum | loop | `loop` (pre-style), `school`, `drift`, `hover`, `patrol`, `bottom`, `surface`; relationship styles `follow` / `pair` / `chase` bond a fish to the nearest preceding unbonded fish in the mix (and swim in its depth band); `auto` gives each untagged token its breed's default |
|
|
68
|
+
| lightSeek | number | 0 | Free fish drawn toward the room's light shafts, each to its own pool (needs rays) |
|
|
69
|
+
| formationBreathe | number | 0 | The school relaxes outward and back on a ~15 s cycle; only ever expands |
|
|
67
70
|
|
|
68
71
|
## File map
|
|
69
72
|
|
|
@@ -1,55 +1,69 @@
|
|
|
1
1
|
// src/environments.ts
|
|
2
2
|
var FLOOR_KINDS = ["flat", "dunes", "ridges", "basin"];
|
|
3
3
|
var ENVIRONMENTS = [
|
|
4
|
-
{ name: "void", label: "Void", water: null, floor: "flat", rays: null },
|
|
4
|
+
{ name: "void", label: "Void", water: null, floor: "flat", rays: null, seedSalt: 0 },
|
|
5
5
|
{
|
|
6
6
|
name: "abyss",
|
|
7
7
|
label: "Abyss",
|
|
8
8
|
water: null,
|
|
9
9
|
floor: "basin",
|
|
10
|
-
|
|
10
|
+
seedSalt: 161,
|
|
11
|
+
rays: { strength: 0.35, color: "#0e4a6e", y: -620 },
|
|
12
|
+
palette: { fog: "#02030a", floor: "#05070f", mote: "#4fd4a8" }
|
|
11
13
|
},
|
|
12
14
|
{
|
|
13
15
|
name: "reef",
|
|
14
16
|
label: "Reef",
|
|
15
17
|
water: { y: 150, color: "#2ad4ff", opacity: 0.18 },
|
|
16
18
|
floor: "dunes",
|
|
17
|
-
rays: { strength: 0.5, color: "#bfefff", y: 900 }
|
|
19
|
+
rays: { strength: 0.5, color: "#bfefff", y: 900 },
|
|
20
|
+
seedSalt: 162,
|
|
21
|
+
palette: { fog: "#0a3d5c", floor: "#123c50", mote: "#bfe8ff" }
|
|
18
22
|
},
|
|
19
23
|
{
|
|
20
24
|
name: "kelp",
|
|
21
25
|
label: "Kelp forest",
|
|
22
26
|
water: { y: 170, color: "#1e6f5c", opacity: 0.2 },
|
|
23
27
|
floor: "ridges",
|
|
24
|
-
rays: { strength: 0.4, color: "#9ef5d0", y: 820 }
|
|
28
|
+
rays: { strength: 0.4, color: "#9ef5d0", y: 820 },
|
|
29
|
+
seedSalt: 163,
|
|
30
|
+
palette: { fog: "#07271e", floor: "#0c2f22", mote: "#a5f2c6" }
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
name: "ice",
|
|
28
34
|
label: "Under ice",
|
|
29
35
|
water: { y: 132, color: "#cfefff", opacity: 0.32 },
|
|
30
36
|
floor: "flat",
|
|
31
|
-
rays: { strength: 0.6, color: "#eaf8ff", y: 1e3 }
|
|
37
|
+
rays: { strength: 0.6, color: "#eaf8ff", y: 1e3 },
|
|
38
|
+
seedSalt: 164,
|
|
39
|
+
palette: { fog: "#12283c", floor: "#1b3d59", mote: "#d8f2ff" }
|
|
32
40
|
},
|
|
33
41
|
{
|
|
34
42
|
name: "vent",
|
|
35
43
|
label: "Hydrothermal vent",
|
|
36
44
|
water: null,
|
|
37
45
|
floor: "ridges",
|
|
38
|
-
|
|
46
|
+
seedSalt: 165,
|
|
47
|
+
rays: { strength: 0.45, color: "#ff7a3c", y: -520 },
|
|
48
|
+
palette: { fog: "#170502", floor: "#4a1a0a", mote: "#ff8a3c" }
|
|
39
49
|
},
|
|
40
50
|
{
|
|
41
51
|
name: "lagoon",
|
|
42
52
|
label: "Lagoon",
|
|
43
53
|
water: { y: 118, color: "#7ff3d0", opacity: 0.22 },
|
|
44
54
|
floor: "dunes",
|
|
45
|
-
rays: { strength: 0.55, color: "#fff3b0", y: 950 }
|
|
55
|
+
rays: { strength: 0.55, color: "#fff3b0", y: 950 },
|
|
56
|
+
seedSalt: 166,
|
|
57
|
+
palette: { fog: "#0d453f", floor: "#6e4457", mote: "#eafff4" }
|
|
46
58
|
},
|
|
47
59
|
{
|
|
48
60
|
name: "universe",
|
|
49
61
|
label: "Universe",
|
|
50
62
|
water: null,
|
|
51
63
|
floor: "ridges",
|
|
52
|
-
|
|
64
|
+
seedSalt: 167,
|
|
65
|
+
rays: { strength: 0.3, color: "#c39bff", y: 1200 },
|
|
66
|
+
palette: { fog: "#0d0618", floor: "#171030", mote: "#cfa8ff" }
|
|
53
67
|
}
|
|
54
68
|
];
|
|
55
69
|
var ENVIRONMENT_NAMES = ENVIRONMENTS.map((e) => e.name);
|
|
@@ -74,11 +88,50 @@ var SWIM_STYLES = [
|
|
|
74
88
|
{ name: "loop", label: "Loop", speedMul: 1, band: "free", bobAmp: 0, bobHz: 0, formation: false, travel: 1 },
|
|
75
89
|
{ name: "school", label: "School", speedMul: 1, band: "free", bobAmp: 1.5, bobHz: 0.5, formation: true, travel: 1 },
|
|
76
90
|
{ name: "drift", label: "Drift", speedMul: 0.35, band: "mid", bobAmp: 5, bobHz: 0.13, formation: false, travel: 0.6 },
|
|
77
|
-
|
|
91
|
+
// speedMul 0.55, not 0.2: QA measured hover at swimSpeed 0.5 pinned to a
|
|
92
|
+
// fixed screen x for 8+ seconds — frozen, not station-keeping. The two
|
|
93
|
+
// multiplied slowdowns (speedMul x travel) were the freeze.
|
|
94
|
+
{ name: "hover", label: "Hover", speedMul: 0.55, band: "free", bobAmp: 7, bobHz: 0.3, formation: false, travel: 0.12 },
|
|
78
95
|
{ name: "patrol", label: "Patrol", speedMul: 0.55, band: "mid", bobAmp: 1, bobHz: 0.09, formation: false, travel: 1 },
|
|
79
96
|
{ 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 }
|
|
97
|
+
{ name: "surface", label: "Surface-skimmer", speedMul: 0.9, band: "ceiling", bobAmp: 3, bobHz: 0.55, formation: false, travel: 1 },
|
|
98
|
+
// Relationships (MQ31). A bonded fish has no route of its own: it rides
|
|
99
|
+
// its leader's plan at a lag, so a `@follow` trio behind a turtle is a
|
|
100
|
+
// file, a `@pair` couple orbits a shared point, and a `@chase` closes on
|
|
101
|
+
// its leader and falls back. All three stay pure in t because the leader
|
|
102
|
+
// is — the follower samples the SAME closed form at `d - lag`.
|
|
103
|
+
{ name: "follow", label: "Follower", speedMul: 1, band: "free", bobAmp: 1.2, bobHz: 0.5, formation: false, travel: 1, bond: "follow" },
|
|
104
|
+
{ name: "pair", label: "Pair", speedMul: 0.8, band: "free", bobAmp: 2.5, bobHz: 0.35, formation: false, travel: 1, bond: "pair" },
|
|
105
|
+
{ name: "chase", label: "Chaser", speedMul: 1, band: "free", bobAmp: 1, bobHz: 0.6, formation: false, travel: 1, bond: "chase" }
|
|
81
106
|
];
|
|
107
|
+
var AUTO_STYLE_BY_BREED = {
|
|
108
|
+
angelfish: "school",
|
|
109
|
+
betafish: "drift",
|
|
110
|
+
seahorse: "hover",
|
|
111
|
+
seaturtle: "surface",
|
|
112
|
+
// The unminted NPC set.
|
|
113
|
+
blowfish: "hover",
|
|
114
|
+
hackerfish: "loop",
|
|
115
|
+
glowfish: "drift",
|
|
116
|
+
babyfish: "school",
|
|
117
|
+
shark: "patrol",
|
|
118
|
+
crab: "bottom",
|
|
119
|
+
jellyfish: "drift",
|
|
120
|
+
dori: "school"
|
|
121
|
+
};
|
|
122
|
+
function autoStyleFor(breed) {
|
|
123
|
+
const name = breed ? AUTO_STYLE_BY_BREED[breed.toLowerCase()] : void 0;
|
|
124
|
+
return swimStyleOf(name ?? "loop");
|
|
125
|
+
}
|
|
126
|
+
function formationBreathe(tSec, amount) {
|
|
127
|
+
const a = Math.max(0, Math.min(1, amount));
|
|
128
|
+
if (a <= 0) return 1;
|
|
129
|
+
return 1 + a * 0.22 * (0.5 + 0.5 * Math.sin(tSec * 0.42));
|
|
130
|
+
}
|
|
131
|
+
function idleSway(style, tSec, phase) {
|
|
132
|
+
if (style.travel >= 1 || style.formation) return 0;
|
|
133
|
+
return (1 - style.travel) * 0.5 * Math.sin(tSec * 0.45 + phase);
|
|
134
|
+
}
|
|
82
135
|
var SWIM_STYLE_NAMES = SWIM_STYLES.map((s) => s.name);
|
|
83
136
|
function swimStyleOf(name) {
|
|
84
137
|
return SWIM_STYLES.find((s) => s.name === name) ?? SWIM_STYLES[0];
|
|
@@ -105,9 +158,13 @@ function fishVariation(index, variance) {
|
|
|
105
158
|
anchor: fishHash(index, 7)
|
|
106
159
|
};
|
|
107
160
|
}
|
|
161
|
+
function anchorFraction(style, index, variance) {
|
|
162
|
+
if (style.name === "loop") return 0;
|
|
163
|
+
return fishVariation(index, variance).anchor;
|
|
164
|
+
}
|
|
108
165
|
var FISH_LENGTH = 18;
|
|
109
166
|
var FORMATION_HALF_WIDTH = 62;
|
|
110
|
-
var FORMATION_SHAPES = ["phalanx", "line", "ring", "wedge", "ball"];
|
|
167
|
+
var FORMATION_SHAPES = ["phalanx", "line", "ring", "wedge", "ball", "wheel"];
|
|
111
168
|
function formationSlot(index, count, variance, halfWidth = FORMATION_HALF_WIDTH, shape = "phalanx") {
|
|
112
169
|
const j = 0.35 + 0.65 * Math.max(0, Math.min(1, variance));
|
|
113
170
|
if (shape === "line") {
|
|
@@ -131,6 +188,15 @@ function formationSlot(index, count, variance, halfWidth = FORMATION_HALF_WIDTH,
|
|
|
131
188
|
back: Math.sin(a) * r
|
|
132
189
|
};
|
|
133
190
|
}
|
|
191
|
+
if (shape === "wheel") {
|
|
192
|
+
const r = Math.max(FISH_LENGTH * 1.6, count * FISH_LENGTH * 1.35 / (Math.PI * 2));
|
|
193
|
+
const a = index / Math.max(1, count) * Math.PI * 2;
|
|
194
|
+
return {
|
|
195
|
+
side: Math.cos(a) * r,
|
|
196
|
+
up: Math.sin(a) * Math.min(r * 0.55, 24) + (fishHash(index, 5) - 0.5) * FISH_LENGTH * 0.3 * j,
|
|
197
|
+
back: Math.sin(a) * r
|
|
198
|
+
};
|
|
199
|
+
}
|
|
134
200
|
if (shape === "wedge") {
|
|
135
201
|
const VSIZE = 9;
|
|
136
202
|
const layers = Math.ceil(Math.max(1, count) / VSIZE);
|
|
@@ -138,12 +204,12 @@ function formationSlot(index, count, variance, halfWidth = FORMATION_HALF_WIDTH,
|
|
|
138
204
|
const within = index % VSIZE;
|
|
139
205
|
const rank = within + 1 >> 1;
|
|
140
206
|
const wing = within === 0 ? 0 : within % 2 === 1 ? -1 : 1;
|
|
207
|
+
const droop = (rank - 2) * FISH_LENGTH * (layers === 1 ? 0.24 : 0.12);
|
|
208
|
+
const pitch2 = layers === 1 ? 1.5 : 1.2;
|
|
209
|
+
const jit2 = layers === 1 ? 0.2 : 0.15;
|
|
141
210
|
return {
|
|
142
211
|
side: wing * rank * FISH_LENGTH * 1.25 + (fishHash(index, 4) - 0.5) * FISH_LENGTH * 0.3 * j,
|
|
143
|
-
|
|
144
|
-
// the same rank directly above each other, so pitch minus worst-case
|
|
145
|
-
// jitter closure must clear one body length.
|
|
146
|
-
up: (layer - (layers - 1) / 2) * FISH_LENGTH * 1.5 + (fishHash(index, 5) - 0.5) * FISH_LENGTH * 0.2 * j,
|
|
212
|
+
up: (layer - (layers - 1) / 2) * FISH_LENGTH * pitch2 + (fishHash(index, 5) - 0.5) * FISH_LENGTH * jit2 * j - droop,
|
|
147
213
|
back: (rank - 2) * FISH_LENGTH * 1.35
|
|
148
214
|
};
|
|
149
215
|
}
|
|
@@ -189,6 +255,12 @@ function formationExtent(count, variance, shape = "phalanx") {
|
|
|
189
255
|
}
|
|
190
256
|
return { side, up, back, reach };
|
|
191
257
|
}
|
|
258
|
+
function fitBreath(extent, lattice, bounds, margin = FISH_LENGTH * 0.5) {
|
|
259
|
+
let fit = lattice;
|
|
260
|
+
if (extent.up > 0) fit = Math.min(fit, Math.max(1, (bounds.yRange / 2 - margin) / extent.up));
|
|
261
|
+
if (extent.reach > 0) fit = Math.min(fit, Math.max(1, (bounds.radius - margin) / extent.reach));
|
|
262
|
+
return fit;
|
|
263
|
+
}
|
|
192
264
|
function bandRange(band) {
|
|
193
265
|
switch (band) {
|
|
194
266
|
// Tight, because 0..0.3 of a 57-unit volume is nearly two body lengths of
|
|
@@ -226,10 +298,32 @@ function splineAt(points, g) {
|
|
|
226
298
|
catmullRom(p0[2], p1[2], p2[2], p3[2], u)
|
|
227
299
|
];
|
|
228
300
|
}
|
|
229
|
-
var PATH_SHAPES = ["wander", "orbit", "eight", "helix", "canyon"];
|
|
230
|
-
function shapeWaypoints(shape, rng, bounds) {
|
|
301
|
+
var PATH_SHAPES = ["wander", "orbit", "eight", "helix", "canyon", "crossing"];
|
|
302
|
+
function shapeWaypoints(shape, rng, bounds, opts = {}) {
|
|
231
303
|
const points = [];
|
|
232
304
|
const ySpan = bounds.yMax - bounds.yMin;
|
|
305
|
+
if (shape === "crossing") {
|
|
306
|
+
const n2 = 16;
|
|
307
|
+
const az = (opts.cameraAzimuthDeg ?? 0) * Math.PI / 180;
|
|
308
|
+
const rx = Math.cos(az), rz = -Math.sin(az);
|
|
309
|
+
const dx = Math.sin(az), dz = Math.cos(az);
|
|
310
|
+
const len = bounds.radius * 0.9;
|
|
311
|
+
const depth = bounds.radius * rng.range(0.18, 0.3);
|
|
312
|
+
const y0 = bounds.yMin + ySpan * rng.range(0.35, 0.6);
|
|
313
|
+
const lift = ySpan * rng.range(0.04, 0.1);
|
|
314
|
+
for (let i = 0; i < n2; i++) {
|
|
315
|
+
const u = i / n2 * Math.PI * 2;
|
|
316
|
+
const along = Math.sin(u) * len;
|
|
317
|
+
const toward = Math.cos(u) * depth;
|
|
318
|
+
points.push([
|
|
319
|
+
rx * along + dx * toward,
|
|
320
|
+
// The far leg rides a little higher so it never hides behind the near one.
|
|
321
|
+
y0 + (toward < 0 ? lift : 0) + ySpan * rng.range(0, 0.04),
|
|
322
|
+
rz * along + dz * toward
|
|
323
|
+
]);
|
|
324
|
+
}
|
|
325
|
+
return points;
|
|
326
|
+
}
|
|
233
327
|
if (shape === "orbit") {
|
|
234
328
|
const n2 = 14 + rng.int(0, 4);
|
|
235
329
|
const r0 = bounds.radius * rng.range(0.45, 0.8);
|
|
@@ -304,8 +398,8 @@ function shapeWaypoints(shape, rng, bounds) {
|
|
|
304
398
|
}
|
|
305
399
|
return points;
|
|
306
400
|
}
|
|
307
|
-
function compileSwimPlan(rng, bounds, shape = "wander") {
|
|
308
|
-
const points = shapeWaypoints(shape, rng, bounds);
|
|
401
|
+
function compileSwimPlan(rng, bounds, shape = "wander", opts = {}) {
|
|
402
|
+
const points = shapeWaypoints(shape, rng, bounds, opts);
|
|
309
403
|
const n = points.length;
|
|
310
404
|
const arc = new Float32Array(ARC_SAMPLES + 1);
|
|
311
405
|
let prev = splineAt(points, 0);
|
|
@@ -371,11 +465,13 @@ function swimPoseAtDistance(plan, dist) {
|
|
|
371
465
|
// src/maneuver.ts
|
|
372
466
|
var MANEUVERS = ["none", "dart", "startle", "graze", "curious", "zoomies"];
|
|
373
467
|
var SPECS = {
|
|
374
|
-
dart: { name: "dart", dur: 2.4, interval: 14, advance: 4.5, kick: 0.5, lift: 0, flurry: 1.1, bursts: 1 },
|
|
375
|
-
startle: { name: "startle", dur: 1.8, interval: 18, advance: 1.2, kick: 2.8, lift: 0.4, flurry: 1.3, bursts: 1 },
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
468
|
+
dart: { name: "dart", dur: 2.4, interval: 14, advance: 4.5, kick: 0.5, lift: 0, flurry: 1.1, bursts: 1, pitch: 0, contagious: false },
|
|
469
|
+
startle: { name: "startle", dur: 1.8, interval: 18, advance: 1.2, kick: 2.8, lift: 0.4, flurry: 1.3, bursts: 1, pitch: 0.18, contagious: true },
|
|
470
|
+
// Graze sinks for real now: lift beyond ±1 is deliberate — QA measured the
|
|
471
|
+
// old -0.7 × 0.6 at 7.6u of drop, indistinguishable from hover's idle bob.
|
|
472
|
+
graze: { name: "graze", dur: 5.5, interval: 16, advance: -3.2, kick: 0.6, lift: -2.1, flurry: 0.3, bursts: 1, pitch: -0.55, contagious: false },
|
|
473
|
+
curious: { name: "curious", dur: 4, interval: 15, advance: 0.8, kick: 2, lift: 0.2, flurry: 0.5, bursts: 1, pitch: 0.12, contagious: false },
|
|
474
|
+
zoomies: { name: "zoomies", dur: 3.2, interval: 20, advance: 7.5, kick: 0.9, lift: 0.15, flurry: 1.5, bursts: 3, pitch: 0, contagious: false }
|
|
379
475
|
};
|
|
380
476
|
function maneuverSpecOf(name) {
|
|
381
477
|
return Object.hasOwn(SPECS, name) ? SPECS[name] : null;
|
|
@@ -390,38 +486,44 @@ function surges(u, bursts) {
|
|
|
390
486
|
if (u <= 0) return 0;
|
|
391
487
|
if (u >= 1) return 1;
|
|
392
488
|
const x = u * bursts;
|
|
393
|
-
|
|
489
|
+
const frac = x - Math.floor(x);
|
|
490
|
+
return (Math.floor(x) + smooth(frac / 0.55)) / bursts;
|
|
394
491
|
}
|
|
395
|
-
var IDLE = { along: 0, alongBump: 0, side: 0, up: 0, flurry: 0 };
|
|
396
|
-
function maneuverAt(spec, index, tSec, rate, intensity) {
|
|
492
|
+
var IDLE = { along: 0, alongBump: 0, side: 0, up: 0, flurry: 0, pitch: 0 };
|
|
493
|
+
function maneuverAt(spec, index, tSec, rate, intensity, seatDelay = null) {
|
|
397
494
|
if (!spec || rate <= 0 || intensity <= 0 || tSec < 0) return IDLE;
|
|
398
|
-
const interval = spec.interval * (3 - 2 * Math.
|
|
495
|
+
const interval = rate <= 1 ? spec.interval * (3 - 2 * rate) : Math.max(spec.dur * 1.15, spec.interval / rate);
|
|
399
496
|
const k0 = Math.floor(tSec / interval);
|
|
400
497
|
let along = 0;
|
|
401
498
|
let side = 0;
|
|
402
499
|
let up = 0;
|
|
403
500
|
let flurry = 0;
|
|
501
|
+
let pitch = 0;
|
|
404
502
|
let alongBump = 0;
|
|
405
503
|
const done = Math.max(0, k0 - 1);
|
|
406
|
-
|
|
504
|
+
const perm = 0.6 + 0.8 * fishHash(index, 37);
|
|
505
|
+
along += done * spec.advance * intensity * perm;
|
|
407
506
|
for (const k of [k0 - 1, k0, k0 + 1]) {
|
|
408
507
|
if (k < 0) continue;
|
|
409
|
-
const
|
|
508
|
+
const wave = spec.contagious && seatDelay !== null;
|
|
509
|
+
const jitter = wave ? fishHash(1013, 11 + k) : fishHash(index, 11 + k);
|
|
510
|
+
const start = (k + jitter) * interval + (wave ? seatDelay : 0);
|
|
410
511
|
const u = (tSec - start) / spec.dur;
|
|
411
512
|
if (u <= 0) continue;
|
|
412
513
|
if (u >= 1) {
|
|
413
|
-
if (k >= done) along += spec.advance * intensity;
|
|
514
|
+
if (k >= done) along += spec.advance * intensity * perm;
|
|
414
515
|
continue;
|
|
415
516
|
}
|
|
416
517
|
const sign = fishHash(index, 23 + k) < 0.5 ? -1 : 1;
|
|
417
518
|
const env = bump(u) * intensity;
|
|
418
|
-
along += spec.advance * surges(u, spec.bursts) * intensity;
|
|
519
|
+
along += spec.advance * surges(u, spec.bursts) * intensity * perm;
|
|
419
520
|
alongBump += spec.advance * bump(u) * intensity;
|
|
420
521
|
side += sign * spec.kick * env;
|
|
421
522
|
up += spec.lift * spec.kick * env;
|
|
422
523
|
flurry += spec.flurry * env;
|
|
524
|
+
pitch += spec.pitch * env;
|
|
423
525
|
}
|
|
424
|
-
return { along, alongBump, side, up, flurry };
|
|
526
|
+
return { along, alongBump, side, up, flurry, pitch };
|
|
425
527
|
}
|
|
426
528
|
|
|
427
529
|
// src/asset-cids.ts
|
|
@@ -1095,10 +1197,59 @@ var NPC_CATALOG = [
|
|
|
1095
1197
|
function parseFishMix(mix, catalog = FISH_CATALOG) {
|
|
1096
1198
|
const entries = [];
|
|
1097
1199
|
const problems = [];
|
|
1200
|
+
const unique = catalog === FISH_CATALOG;
|
|
1201
|
+
const used = /* @__PURE__ */ new Set();
|
|
1202
|
+
const resolvesAsToken = (core) => {
|
|
1203
|
+
const [idRaw, countRaw, ...extra] = core.split(":");
|
|
1204
|
+
if (extra.length > 0) return false;
|
|
1205
|
+
if (countRaw !== void 0) {
|
|
1206
|
+
const count = Number(countRaw.trim());
|
|
1207
|
+
if (!Number.isInteger(count) || count < 1) return false;
|
|
1208
|
+
}
|
|
1209
|
+
const key = (idRaw ?? "").trim().toLowerCase();
|
|
1210
|
+
if (/^\d+$/.test(key)) {
|
|
1211
|
+
if (catalog.some((fish) => fish.id === Number(key))) return true;
|
|
1212
|
+
return catalog === FISH_CATALOG && fishAsset(Number(key), "3d") !== null && breedOf(Number(key)) !== null;
|
|
1213
|
+
}
|
|
1214
|
+
return catalog.some((fish) => fish.breed.toLowerCase() === key);
|
|
1215
|
+
};
|
|
1216
|
+
const allocate = (breed, want) => {
|
|
1217
|
+
const b = BREEDS.find((x) => x.breed === breed);
|
|
1218
|
+
if (!b?.range) return null;
|
|
1219
|
+
const [lo, hi] = b.range;
|
|
1220
|
+
if (!used.has(want) && want >= lo && want <= hi) {
|
|
1221
|
+
used.add(want);
|
|
1222
|
+
return want;
|
|
1223
|
+
}
|
|
1224
|
+
for (let d = 1; d <= hi - lo; d += 1) {
|
|
1225
|
+
for (const cand of [want + d, want - d]) {
|
|
1226
|
+
if (cand >= lo && cand <= hi && !used.has(cand)) {
|
|
1227
|
+
used.add(cand);
|
|
1228
|
+
return cand;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
};
|
|
1098
1234
|
for (const rawToken of mix.split(",")) {
|
|
1099
1235
|
const token = rawToken.trim();
|
|
1100
1236
|
if (token === "") continue;
|
|
1101
|
-
|
|
1237
|
+
let style;
|
|
1238
|
+
let core = token;
|
|
1239
|
+
const at = token.lastIndexOf("@");
|
|
1240
|
+
if (at >= 0) {
|
|
1241
|
+
const styleRaw = token.slice(at + 1).trim().toLowerCase();
|
|
1242
|
+
const styledCore = token.slice(0, at).trim();
|
|
1243
|
+
if (resolvesAsToken(token)) {
|
|
1244
|
+
} else if (SWIM_STYLE_NAMES.includes(styleRaw)) {
|
|
1245
|
+
core = styledCore;
|
|
1246
|
+
style = styleRaw;
|
|
1247
|
+
} else if (resolvesAsToken(styledCore)) {
|
|
1248
|
+
core = styledCore;
|
|
1249
|
+
problems.push(`"${token}": unknown style "${styleRaw}" (${SWIM_STYLE_NAMES.join(", ")}) \u2014 swimming with the scene's swimStyle`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const [idRaw, countRaw, ...extra] = core.split(":");
|
|
1102
1253
|
if (extra.length > 0) {
|
|
1103
1254
|
problems.push(`"${token}": too many ':' \u2014 expected id[:count]`);
|
|
1104
1255
|
continue;
|
|
@@ -1108,8 +1259,8 @@ function parseFishMix(mix, catalog = FISH_CATALOG) {
|
|
|
1108
1259
|
if (!fish && catalog === FISH_CATALOG && /^\d+$/.test(key)) {
|
|
1109
1260
|
const n = Number(key);
|
|
1110
1261
|
const url2 = fishAsset(n, "3d");
|
|
1111
|
-
const
|
|
1112
|
-
if (url2 &&
|
|
1262
|
+
const breed2 = breedOf(n);
|
|
1263
|
+
if (url2 && breed2) fish = { id: n, name: `Fish ${n}`, breed: breed2, ipfs3d: url2, localGlb: "" };
|
|
1113
1264
|
}
|
|
1114
1265
|
if (!fish) {
|
|
1115
1266
|
const breeds = [...new Set(catalog.map((f) => f.breed))].join(", ");
|
|
@@ -1137,16 +1288,48 @@ function parseFishMix(mix, catalog = FISH_CATALOG) {
|
|
|
1137
1288
|
count = n;
|
|
1138
1289
|
}
|
|
1139
1290
|
}
|
|
1140
|
-
|
|
1291
|
+
if (!unique || fish.id > TOTAL_SUPPLY) {
|
|
1292
|
+
entries.push({ id: fish.id, breed: fish.breed, url, count, ...style ? { style } : {} });
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
const breed = breedOf(fish.id);
|
|
1296
|
+
if (!breed) {
|
|
1297
|
+
entries.push({ id: fish.id, breed: fish.breed, url, count, ...style ? { style } : {} });
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const isAlias = !/^\d+$/.test(key);
|
|
1301
|
+
const b = BREEDS.find((x) => x.breed === breed);
|
|
1302
|
+
for (let i = 0; i < count; i += 1) {
|
|
1303
|
+
const want = isAlias && b?.range ? b.range[0] + Math.floor((b.range[1] - b.range[0]) * i / Math.max(1, count)) : fish.id;
|
|
1304
|
+
const got = allocate(breed, want);
|
|
1305
|
+
if (got === null) {
|
|
1306
|
+
problems.push(`"${token}": only ${i} distinct ${breed} left \u2014 a minted fish appears once per scene`);
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
if (!isAlias && i === 0 && got !== fish.id) {
|
|
1310
|
+
problems.push(`fish ${fish.id} already cast \u2014 fish ${got} swims instead`);
|
|
1311
|
+
}
|
|
1312
|
+
const gotUrl = fishAsset(got, "3d");
|
|
1313
|
+
if (!gotUrl) {
|
|
1314
|
+
problems.push(`fish ${got}: no asset URL`);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
entries.push({ id: got, breed: fish.breed, url: gotUrl, count: 1, ...style ? { style } : {} });
|
|
1318
|
+
}
|
|
1141
1319
|
}
|
|
1142
1320
|
return { entries, problems };
|
|
1143
1321
|
}
|
|
1144
1322
|
function expandFishMix(entries, cap) {
|
|
1145
|
-
|
|
1323
|
+
return expandFishMixSlots(entries, cap).map((s) => s.url);
|
|
1324
|
+
}
|
|
1325
|
+
function expandFishMixSlots(entries, cap) {
|
|
1326
|
+
const slots = [];
|
|
1146
1327
|
for (const e of entries) {
|
|
1147
|
-
for (let i = 0; i < e.count &&
|
|
1328
|
+
for (let i = 0; i < e.count && slots.length < cap; i++) {
|
|
1329
|
+
slots.push(e.style ? { url: e.url, breed: e.breed, style: e.style } : { url: e.url, breed: e.breed });
|
|
1330
|
+
}
|
|
1148
1331
|
}
|
|
1149
|
-
return
|
|
1332
|
+
return slots;
|
|
1150
1333
|
}
|
|
1151
1334
|
|
|
1152
1335
|
// src/manifest.ts
|
|
@@ -1173,9 +1356,15 @@ var METAQUARIUM_PARAMS = {
|
|
|
1173
1356
|
fishUrl: { type: "string", default: "ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb" },
|
|
1174
1357
|
/** Mixed population DSL: comma-separated `id[:count]` of a minted token id
|
|
1175
1358
|
* (1-512, default catalog) or breed name — e.g. "257:3,100:2,seaturtle:1".
|
|
1176
|
-
* A
|
|
1359
|
+
* A minted fish is an INDIVIDUAL: no id appears twice in a scene, and a
|
|
1360
|
+
* count casts that many DISTINCT fish of the breed ("300:12" is twelve
|
|
1361
|
+
* different angelfish). A custom catalog is a closed world (no farm/IPFS
|
|
1362
|
+
* fallback, no uniqueness — NPC entries are species). Counts are
|
|
1177
1363
|
* absolute; the expanded total clamps to the device tier's fish cap.
|
|
1178
|
-
*
|
|
1364
|
+
* A token may carry its own swim style — `id[:count]@style`, e.g.
|
|
1365
|
+
* "457:3@hover,257:6@school,497:1@surface" — so one tank holds several
|
|
1366
|
+
* behaviours; untagged tokens follow `swimStyle`. Non-empty mix
|
|
1367
|
+
* OVERRIDES fishUrl and fishCount; empty string = single-
|
|
1179
1368
|
* breed mode. Unknown ids/bad counts degrade (good tokens still parse);
|
|
1180
1369
|
* raw URLs are not accepted here — use fishUrl for a custom single-breed GLB. */
|
|
1181
1370
|
fishMix: { type: "string", default: "", ease: "step" },
|
|
@@ -1217,15 +1406,23 @@ var METAQUARIUM_PARAMS = {
|
|
|
1217
1406
|
rayStrength: { type: "number", default: -1, min: -1, max: 1, ease: "step" },
|
|
1218
1407
|
/** How the fish move. `loop` is exactly the pre-style behaviour, so the
|
|
1219
1408
|
* default changes nothing. A small named set on purpose: a silhouette of
|
|
1220
|
-
* movement you can name is one you can choose from.
|
|
1221
|
-
|
|
1409
|
+
* movement you can name is one you can choose from. `auto` lets each
|
|
1410
|
+
* untagged fishMix token swim the way its breed does (seahorse hover,
|
|
1411
|
+
* turtle skim the surface, angelfish school, betafish drift); a token's
|
|
1412
|
+
* `@style` still wins. The relationship styles — `follow`, `pair`,
|
|
1413
|
+
* `chase` — bond a fish to the nearest preceding unbonded fish in the mix
|
|
1414
|
+
* (`seaturtle:1, 257:3@follow` is a turtle with an escort). */
|
|
1415
|
+
swimStyle: { type: "enum", default: "loop", options: [...SWIM_STYLE_NAMES, "auto"], ease: "step" },
|
|
1222
1416
|
/** The shape a fish's loop is drawn on. `wander` is the original roaming
|
|
1223
1417
|
* itinerary, so the default changes nothing. `orbit` laps, `eight` crosses
|
|
1224
|
-
* the middle, `helix` tours the water column, `canyon` sweeps low
|
|
1225
|
-
*
|
|
1418
|
+
* the middle, `helix` tours the water column, `canyon` sweeps low,
|
|
1419
|
+
* `crossing` is a camera-relative parade lane (across the frame in front,
|
|
1420
|
+
* back the other way behind; laid against cameraAzimuth when chosen) —
|
|
1421
|
+
* same spline engine, different itinerary. */
|
|
1226
1422
|
pathShape: { type: "enum", default: "wander", options: [...PATH_SHAPES], ease: "step" },
|
|
1227
1423
|
/** How a `school` holds together: the original lattice, single file, a
|
|
1228
|
-
* carousel ring, the migratory V,
|
|
1424
|
+
* carousel ring, the migratory V, a bait-ball, or the ring tilted into a
|
|
1425
|
+
* `wheel` that reads from a side camera. Ignored by every
|
|
1229
1426
|
* non-formation style; the same no-pair-inside-a-body-length law holds
|
|
1230
1427
|
* for all of them. */
|
|
1231
1428
|
formationShape: { type: "enum", default: "phalanx", options: [...FORMATION_SHAPES], ease: "step" },
|
|
@@ -1239,8 +1436,9 @@ var METAQUARIUM_PARAMS = {
|
|
|
1239
1436
|
* addressable. `none` (default) changes nothing. */
|
|
1240
1437
|
maneuver: { type: "enum", default: "none", options: [...MANEUVERS], ease: "step" },
|
|
1241
1438
|
/** How often events fire: 0 never, 1 the maneuver's own tempo (roughly
|
|
1242
|
-
* every 14-20s per fish, desynchronised)
|
|
1243
|
-
|
|
1439
|
+
* every 14-20s per fish, desynchronised), above 1 faster — 3 packs events
|
|
1440
|
+
* nearly back to back. Values <=1 mean exactly what they always did. */
|
|
1441
|
+
maneuverRate: { type: "number", default: 0.5, min: 0, max: 3, ease: "smooth" },
|
|
1244
1442
|
/** How hard: scales the surge, the kick, and the tail flurry together. */
|
|
1245
1443
|
maneuverIntensity: { type: "number", default: 0.7, min: 0, max: 1, ease: "smooth" },
|
|
1246
1444
|
/** Procedural body yaw for models that carry NO animation clip — most of
|
|
@@ -1253,7 +1451,18 @@ var METAQUARIUM_PARAMS = {
|
|
|
1253
1451
|
* behaviour, and a scene already on someone's wall should not start moving
|
|
1254
1452
|
* differently because a dependency was bumped. 0.3–0.4 is the recommended
|
|
1255
1453
|
* value for a clip-less cast — the studio swim variants all set it. */
|
|
1256
|
-
bodyWiggle: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" }
|
|
1454
|
+
bodyWiggle: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
|
|
1455
|
+
/** Light-seeking: free-swimming fish are drawn toward the room's light
|
|
1456
|
+
* shafts, each fish to its own pool. 0 (default) leaves every route
|
|
1457
|
+
* where it was; 1 pulls a fish most of the way into its shaft, so the
|
|
1458
|
+
* cast gathers in the light and a `rayStrength` dial becomes a staging
|
|
1459
|
+
* dial too. Needs a room with rays; formations are steered by their
|
|
1460
|
+
* carrier and ignore it. */
|
|
1461
|
+
lightSeek: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
|
|
1462
|
+
/** Formation breathing: the school relaxes outward and draws back in on a
|
|
1463
|
+
* slow (~15 s) cycle. 0 (default) is the rigid lattice; 1 opens it by up
|
|
1464
|
+
* to a fifth. Only ever expands, so the spacing guarantee holds. */
|
|
1465
|
+
formationBreathe: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" }
|
|
1257
1466
|
};
|
|
1258
1467
|
var MIAMI_VICE_COLORS = [
|
|
1259
1468
|
"#ff6ad5",
|
|
@@ -1351,14 +1560,20 @@ export {
|
|
|
1351
1560
|
RAY_COUNT,
|
|
1352
1561
|
affordableLayers,
|
|
1353
1562
|
SWIM_STYLES,
|
|
1563
|
+
AUTO_STYLE_BY_BREED,
|
|
1564
|
+
autoStyleFor,
|
|
1565
|
+
formationBreathe,
|
|
1566
|
+
idleSway,
|
|
1354
1567
|
SWIM_STYLE_NAMES,
|
|
1355
1568
|
swimStyleOf,
|
|
1356
1569
|
fishHash,
|
|
1357
1570
|
fishVariation,
|
|
1571
|
+
anchorFraction,
|
|
1358
1572
|
FISH_LENGTH,
|
|
1359
1573
|
FORMATION_SHAPES,
|
|
1360
1574
|
formationSlot,
|
|
1361
1575
|
formationExtent,
|
|
1576
|
+
fitBreath,
|
|
1362
1577
|
bandRange,
|
|
1363
1578
|
PATH_SHAPES,
|
|
1364
1579
|
compileSwimPlan,
|
|
@@ -1386,6 +1601,7 @@ export {
|
|
|
1386
1601
|
NPC_CATALOG,
|
|
1387
1602
|
parseFishMix,
|
|
1388
1603
|
expandFishMix,
|
|
1604
|
+
expandFishMixSlots,
|
|
1389
1605
|
METAQUARIUM_PARAMS,
|
|
1390
1606
|
MIAMI_VICE_COLORS,
|
|
1391
1607
|
BLOOM_COLORS,
|
|
@@ -1394,4 +1610,4 @@ export {
|
|
|
1394
1610
|
paramSpaceWith,
|
|
1395
1611
|
coerceNum
|
|
1396
1612
|
};
|
|
1397
|
-
//# sourceMappingURL=chunk-
|
|
1613
|
+
//# sourceMappingURL=chunk-RTUYWDDZ.js.map
|