@idle-screens/saver-metaquarium 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
- rays: { strength: 0.35, color: "#0e4a6e", y: -620 }
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
- rays: { strength: 0.45, color: "#ff7a3c", y: -520 }
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
- rays: { strength: 0.3, color: "#c39bff", y: 1200 }
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,7 +88,10 @@ 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
- { name: "hover", label: "Hover", speedMul: 0.2, band: "free", bobAmp: 7, bobHz: 0.3, formation: false, travel: 0.12 },
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
97
  { name: "surface", label: "Surface-skimmer", speedMul: 0.9, band: "ceiling", bobAmp: 3, bobHz: 0.55, formation: false, travel: 1 }
@@ -105,6 +122,10 @@ function fishVariation(index, variance) {
105
122
  anchor: fishHash(index, 7)
106
123
  };
107
124
  }
125
+ function anchorFraction(style, index, variance) {
126
+ if (style.name === "loop") return 0;
127
+ return fishVariation(index, variance).anchor;
128
+ }
108
129
  var FISH_LENGTH = 18;
109
130
  var FORMATION_HALF_WIDTH = 62;
110
131
  var FORMATION_SHAPES = ["phalanx", "line", "ring", "wedge", "ball"];
@@ -138,12 +159,12 @@ function formationSlot(index, count, variance, halfWidth = FORMATION_HALF_WIDTH,
138
159
  const within = index % VSIZE;
139
160
  const rank = within + 1 >> 1;
140
161
  const wing = within === 0 ? 0 : within % 2 === 1 ? -1 : 1;
162
+ const droop = (rank - 2) * FISH_LENGTH * (layers === 1 ? 0.24 : 0.12);
163
+ const pitch2 = layers === 1 ? 1.5 : 1.2;
164
+ const jit2 = layers === 1 ? 0.2 : 0.15;
141
165
  return {
142
166
  side: wing * rank * FISH_LENGTH * 1.25 + (fishHash(index, 4) - 0.5) * FISH_LENGTH * 0.3 * j,
143
- // 1.5 body lengths per layer with a small jitter: adjacent layers seat
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,
167
+ up: (layer - (layers - 1) / 2) * FISH_LENGTH * pitch2 + (fishHash(index, 5) - 0.5) * FISH_LENGTH * jit2 * j - droop,
147
168
  back: (rank - 2) * FISH_LENGTH * 1.35
148
169
  };
149
170
  }
@@ -371,11 +392,13 @@ function swimPoseAtDistance(plan, dist) {
371
392
  // src/maneuver.ts
372
393
  var MANEUVERS = ["none", "dart", "startle", "graze", "curious", "zoomies"];
373
394
  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
- graze: { name: "graze", dur: 5.5, interval: 16, advance: -3.2, kick: 0.6, lift: -0.7, flurry: 0.3, bursts: 1 },
377
- curious: { name: "curious", dur: 4, interval: 15, advance: 0.8, kick: 2, lift: 0.2, flurry: 0.5, bursts: 1 },
378
- zoomies: { name: "zoomies", dur: 3.2, interval: 20, advance: 7.5, kick: 0.9, lift: 0.15, flurry: 1.5, bursts: 3 }
395
+ 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 },
396
+ 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 },
397
+ // Graze sinks for real now: lift beyond ±1 is deliberate — QA measured the
398
+ // old -0.7 × 0.6 at 7.6u of drop, indistinguishable from hover's idle bob.
399
+ 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 },
400
+ 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 },
401
+ 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
402
  };
380
403
  function maneuverSpecOf(name) {
381
404
  return Object.hasOwn(SPECS, name) ? SPECS[name] : null;
@@ -390,38 +413,44 @@ function surges(u, bursts) {
390
413
  if (u <= 0) return 0;
391
414
  if (u >= 1) return 1;
392
415
  const x = u * bursts;
393
- return (Math.floor(x) + smooth(x - Math.floor(x))) / bursts;
416
+ const frac = x - Math.floor(x);
417
+ return (Math.floor(x) + smooth(frac / 0.55)) / bursts;
394
418
  }
395
- var IDLE = { along: 0, alongBump: 0, side: 0, up: 0, flurry: 0 };
396
- function maneuverAt(spec, index, tSec, rate, intensity) {
419
+ var IDLE = { along: 0, alongBump: 0, side: 0, up: 0, flurry: 0, pitch: 0 };
420
+ function maneuverAt(spec, index, tSec, rate, intensity, seatDelay = null) {
397
421
  if (!spec || rate <= 0 || intensity <= 0 || tSec < 0) return IDLE;
398
- const interval = spec.interval * (3 - 2 * Math.min(1, rate));
422
+ const interval = rate <= 1 ? spec.interval * (3 - 2 * rate) : Math.max(spec.dur * 1.15, spec.interval / rate);
399
423
  const k0 = Math.floor(tSec / interval);
400
424
  let along = 0;
401
425
  let side = 0;
402
426
  let up = 0;
403
427
  let flurry = 0;
428
+ let pitch = 0;
404
429
  let alongBump = 0;
405
430
  const done = Math.max(0, k0 - 1);
406
- along += done * spec.advance * intensity;
431
+ const perm = 0.6 + 0.8 * fishHash(index, 37);
432
+ along += done * spec.advance * intensity * perm;
407
433
  for (const k of [k0 - 1, k0, k0 + 1]) {
408
434
  if (k < 0) continue;
409
- const start = (k + fishHash(index, 11 + k)) * interval;
435
+ const wave = spec.contagious && seatDelay !== null;
436
+ const jitter = wave ? fishHash(1013, 11 + k) : fishHash(index, 11 + k);
437
+ const start = (k + jitter) * interval + (wave ? seatDelay : 0);
410
438
  const u = (tSec - start) / spec.dur;
411
439
  if (u <= 0) continue;
412
440
  if (u >= 1) {
413
- if (k >= done) along += spec.advance * intensity;
441
+ if (k >= done) along += spec.advance * intensity * perm;
414
442
  continue;
415
443
  }
416
444
  const sign = fishHash(index, 23 + k) < 0.5 ? -1 : 1;
417
445
  const env = bump(u) * intensity;
418
- along += spec.advance * surges(u, spec.bursts) * intensity;
446
+ along += spec.advance * surges(u, spec.bursts) * intensity * perm;
419
447
  alongBump += spec.advance * bump(u) * intensity;
420
448
  side += sign * spec.kick * env;
421
449
  up += spec.lift * spec.kick * env;
422
450
  flurry += spec.flurry * env;
451
+ pitch += spec.pitch * env;
423
452
  }
424
- return { along, alongBump, side, up, flurry };
453
+ return { along, alongBump, side, up, flurry, pitch };
425
454
  }
426
455
 
427
456
  // src/asset-cids.ts
@@ -1095,6 +1124,26 @@ var NPC_CATALOG = [
1095
1124
  function parseFishMix(mix, catalog = FISH_CATALOG) {
1096
1125
  const entries = [];
1097
1126
  const problems = [];
1127
+ const unique = catalog === FISH_CATALOG;
1128
+ const used = /* @__PURE__ */ new Set();
1129
+ const allocate = (breed, want) => {
1130
+ const b = BREEDS.find((x) => x.breed === breed);
1131
+ if (!b?.range) return null;
1132
+ const [lo, hi] = b.range;
1133
+ if (!used.has(want) && want >= lo && want <= hi) {
1134
+ used.add(want);
1135
+ return want;
1136
+ }
1137
+ for (let d = 1; d <= hi - lo; d += 1) {
1138
+ for (const cand of [want + d, want - d]) {
1139
+ if (cand >= lo && cand <= hi && !used.has(cand)) {
1140
+ used.add(cand);
1141
+ return cand;
1142
+ }
1143
+ }
1144
+ }
1145
+ return null;
1146
+ };
1098
1147
  for (const rawToken of mix.split(",")) {
1099
1148
  const token = rawToken.trim();
1100
1149
  if (token === "") continue;
@@ -1108,8 +1157,8 @@ function parseFishMix(mix, catalog = FISH_CATALOG) {
1108
1157
  if (!fish && catalog === FISH_CATALOG && /^\d+$/.test(key)) {
1109
1158
  const n = Number(key);
1110
1159
  const url2 = fishAsset(n, "3d");
1111
- const breed = breedOf(n);
1112
- if (url2 && breed) fish = { id: n, name: `Fish ${n}`, breed, ipfs3d: url2, localGlb: "" };
1160
+ const breed2 = breedOf(n);
1161
+ if (url2 && breed2) fish = { id: n, name: `Fish ${n}`, breed: breed2, ipfs3d: url2, localGlb: "" };
1113
1162
  }
1114
1163
  if (!fish) {
1115
1164
  const breeds = [...new Set(catalog.map((f) => f.breed))].join(", ");
@@ -1137,7 +1186,34 @@ function parseFishMix(mix, catalog = FISH_CATALOG) {
1137
1186
  count = n;
1138
1187
  }
1139
1188
  }
1140
- entries.push({ id: fish.id, url, count });
1189
+ if (!unique || fish.id > TOTAL_SUPPLY) {
1190
+ entries.push({ id: fish.id, url, count });
1191
+ continue;
1192
+ }
1193
+ const breed = breedOf(fish.id);
1194
+ if (!breed) {
1195
+ entries.push({ id: fish.id, url, count });
1196
+ continue;
1197
+ }
1198
+ const isAlias = !/^\d+$/.test(key);
1199
+ const b = BREEDS.find((x) => x.breed === breed);
1200
+ for (let i = 0; i < count; i += 1) {
1201
+ const want = isAlias && b?.range ? b.range[0] + Math.floor((b.range[1] - b.range[0]) * i / Math.max(1, count)) : fish.id;
1202
+ const got = allocate(breed, want);
1203
+ if (got === null) {
1204
+ problems.push(`"${token}": only ${i} distinct ${breed} left \u2014 a minted fish appears once per scene`);
1205
+ break;
1206
+ }
1207
+ if (!isAlias && i === 0 && got !== fish.id) {
1208
+ problems.push(`fish ${fish.id} already cast \u2014 fish ${got} swims instead`);
1209
+ }
1210
+ const gotUrl = fishAsset(got, "3d");
1211
+ if (!gotUrl) {
1212
+ problems.push(`fish ${got}: no asset URL`);
1213
+ continue;
1214
+ }
1215
+ entries.push({ id: got, url: gotUrl, count: 1 });
1216
+ }
1141
1217
  }
1142
1218
  return { entries, problems };
1143
1219
  }
@@ -1173,7 +1249,10 @@ var METAQUARIUM_PARAMS = {
1173
1249
  fishUrl: { type: "string", default: "ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb" },
1174
1250
  /** Mixed population DSL: comma-separated `id[:count]` of a minted token id
1175
1251
  * (1-512, default catalog) or breed name — e.g. "257:3,100:2,seaturtle:1".
1176
- * A custom catalog is a closed world (no farm/IPFS fallback). Counts are
1252
+ * A minted fish is an INDIVIDUAL: no id appears twice in a scene, and a
1253
+ * count casts that many DISTINCT fish of the breed ("300:12" is twelve
1254
+ * different angelfish). A custom catalog is a closed world (no farm/IPFS
1255
+ * fallback, no uniqueness — NPC entries are species). Counts are
1177
1256
  * absolute; the expanded total clamps to the device tier's fish cap.
1178
1257
  * Non-empty mix OVERRIDES fishUrl and fishCount; empty string = single-
1179
1258
  * breed mode. Unknown ids/bad counts degrade (good tokens still parse);
@@ -1239,8 +1318,9 @@ var METAQUARIUM_PARAMS = {
1239
1318
  * addressable. `none` (default) changes nothing. */
1240
1319
  maneuver: { type: "enum", default: "none", options: [...MANEUVERS], ease: "step" },
1241
1320
  /** How often events fire: 0 never, 1 the maneuver's own tempo (roughly
1242
- * every 14-20s per fish, desynchronised). */
1243
- maneuverRate: { type: "number", default: 0.5, min: 0, max: 1, ease: "smooth" },
1321
+ * every 14-20s per fish, desynchronised), above 1 faster — 3 packs events
1322
+ * nearly back to back. Values <=1 mean exactly what they always did. */
1323
+ maneuverRate: { type: "number", default: 0.5, min: 0, max: 3, ease: "smooth" },
1244
1324
  /** How hard: scales the surge, the kick, and the tail flurry together. */
1245
1325
  maneuverIntensity: { type: "number", default: 0.7, min: 0, max: 1, ease: "smooth" },
1246
1326
  /** Procedural body yaw for models that carry NO animation clip — most of
@@ -1355,6 +1435,7 @@ export {
1355
1435
  swimStyleOf,
1356
1436
  fishHash,
1357
1437
  fishVariation,
1438
+ anchorFraction,
1358
1439
  FISH_LENGTH,
1359
1440
  FORMATION_SHAPES,
1360
1441
  formationSlot,
@@ -1394,4 +1475,4 @@ export {
1394
1475
  paramSpaceWith,
1395
1476
  coerceNum
1396
1477
  };
1397
- //# sourceMappingURL=chunk-23P73MYY.js.map
1478
+ //# sourceMappingURL=chunk-O676ILL6.js.map