diff --git a/public/tide-pool-world.js b/public/tide-pool-world.js index a4f1d6b..32ba44a 100644 --- a/public/tide-pool-world.js +++ b/public/tide-pool-world.js @@ -1662,6 +1662,15 @@ function moveWalker(world, c, dt) { } // Swimming: a loose school that keeps to water, heads for deeper water as the tide drains, and scatters from ripples. +// A slow current shared by the whole shelf: the curl of a drifting field, so it swirls without gathering anywhere. +// Fish that feel it together turn together, which is what keeps a school moving as one shape. +export function drift(x, z, t) { + const e = .01; + const psi = (a, b) => Math.sin(a * .23 + t * .045) * Math.cos(b * .19 - t * .031) + .6 * Math.sin((a + b) * .13 - t * .027); + const vx = (psi(x, z + e) - psi(x, z - e)) / (2 * e), vz = -(psi(x + e, z) - psi(x - e, z)) / (2 * e); + const len = Math.hypot(vx, vz) || 1; + return { x: vx / len, z: vz / len }; +} function moveSwimmer(world, c, dt) { const S = SPECIES.tab; const depth = depthAt(world, c.x, c.z); @@ -1669,11 +1678,11 @@ function moveSwimmer(world, c, dt) { for (const o of world.creatures) { if (o === c || o.sp !== 'tab') continue; const dx = o.x - c.x, dz = o.z - c.z, d = Math.hypot(dx, dz); - if (d > 1.4 || d < 1e-4) continue; + if (d > 2 || d < 1e-4) continue; n++; cx += dx; cz += dz; ux += o.vx; uz += o.vz; if (d < .35) { ax -= dx / d * (.35 - d) * 6; az -= dz / d * (.35 - d) * 6; } } - if (n) { ax += cx / n * .6 + (ux / n - c.vx) * .9; az += cz / n * .6 + (uz / n - c.vz) * .9; } + if (n) { ax += cx / n * .5 + (ux / n - c.vx) * 1.4; az += cz / n * .5 + (uz / n - c.vz) * 1.4; } for (const o of world.creatures) { if (o.sp !== 'pylon' && o.sp !== 'breaker') continue; const dx = c.x - o.x, dz = c.z - o.z, d = Math.hypot(dx, dz); @@ -1701,8 +1710,24 @@ function moveSwimmer(world, c, dt) { } if (best > depth) { ax += bx * 2.5 * worry; az += bz * 2.5 * worry; } } + // A hungry fish follows the plankton, leaning toward richer water nearby; the school turns with it, so a school that + // has grazed its water out moves on to fresh water instead of starving where it is. + const hunger = clamp((.75 - c.energy) / .5, 0, 1); + if (hunger > .05) { + let best = world.plankton[cellAt(c.x, c.z)] + .02, bx = 0, bz = 0; + for (let k = 0; k < 6; k++) { + const a = k / 6 * Math.PI * 2 + c.phase * .1, x = c.x + Math.cos(a) * 1.6, z = c.z + Math.sin(a) * 1.6; + if (depthAt(world, x, z) < .12) continue; + const pl = world.plankton[cellAt(x, z)]; + if (pl > best) { best = pl; bx = Math.cos(a); bz = Math.sin(a); } + } + // Smoothed, so a fish turns toward food rather than twitching between samples. + c.fx = (c.fx || 0) + (bx - (c.fx || 0)) * Math.min(1, dt * .8); + c.fz = (c.fz || 0) + (bz - (c.fz || 0)) * Math.min(1, dt * .8); + ax += c.fx * 1.8 * hunger; az += c.fz * 1.8 * hunger; + } // Most of the school takes a tower as home once one stands: they find their way back to it from anywhere, - // then circle it in a slow orbit, drifting in and out through its frames. + // then circle it in a slow orbit, drifting in and out through its frames. Hunger loosens the hold. let home = null, homeD = Infinity; if (c.id % 4) for (const r of world.structures) { if (r.n < 8) continue; @@ -1711,10 +1736,12 @@ function moveSwimmer(world, c, dt) { } if (home && homeD > 1e-3) { const dx = home.x - c.x, dz = home.z - c.z, d = homeD, ring = .4 + home.reach * .8; - if (d > 7) { ax += dx / d * 1.3; az += dz / d * 1.3; } + const hold = 1 - hunger * .75; + if (d > 7) { ax += dx / d * 1.3 * hold; az += dz / d * 1.3 * hold; } else { - const pull = (d - ring) * .9, swirl = 1.1 * (c.id % 2 ? 1 : -1); - ax += dx / d * pull + (-dz / d) * swirl; az += dz / d * pull + (dx / d) * swirl; + // Every fish around one structure circles it the same way, so they go round as one. + const pull = (d - ring) * .9, swirl = 1.1 * (Math.floor(home.x * 3 + home.z * 7) % 2 ? 1 : -1); + ax += (dx / d * pull + (-dz / d) * swirl) * hold; az += (dz / d * pull + (dx / d) * swirl) * hold; } } // At night a lit beacon draws the school toward its light. @@ -1723,15 +1750,12 @@ function moveSwimmer(world, c, dt) { const dx = m.x - c.x, dz = m.z - c.z, d = Math.hypot(dx, dz); if (d < 9 && d > 1.2) { ax += dx / d * m.lit * 1.2; az += dz / d * m.lit * 1.2; } } - // At high water the school roams the flooded shelf along a slowly turning heading. - const high = tideOf(world.time); - if (high > .55 && depth > .15) { - c.heading += (world.random() - .5) * dt * 1.2; - const roam = 1.4 * (high - .55) * 2.2 * (home ? .3 : 1); - ax += Math.cos(c.heading) * roam; az += Math.sin(c.heading) * roam; - } + // The shared current always nudges a little, and at high water carries the school out across the flooded shelf. + const high = tideOf(world.time), flow = drift(c.x, c.z, world.time); + const roam = .35 + (high > .55 && depth > .15 ? 1.4 * (high - .55) * 2.2 * (home ? .3 : 1) : 0); + ax += flow.x * roam; az += flow.z * roam; const a = (world.random() - .5) * 2.2; - ax += Math.cos(c.phase * .3 + a) * .5; az += Math.sin(c.phase * .3 + a) * .5; + ax += Math.cos(c.phase * .3 + a) * .15; az += Math.sin(c.phase * .3 + a) * .15; c.vx += ax * dt; c.vz += az * dt; const sp = Math.hypot(c.vx, c.vz), max = S.speed * (depth < .1 ? .3 : 1), min = .25; if (sp > max) { c.vx *= max / sp; c.vz *= max / sp; } else if (sp < min && sp > 1e-5) { c.vx *= min / sp; c.vz *= min / sp; } diff --git a/public/tide-pool.js b/public/tide-pool.js index 60789d8..0c4745b 100644 --- a/public/tide-pool.js +++ b/public/tide-pool.js @@ -1,7 +1,7 @@ import { createWorld, advanceWorld, offerObject, SPECIES, SPECIES_ORDER, CELL, COLS, ROWS, X0, Z0, X1, Z1, START_POOL, waterLevel, daylight, heightAt, surfaceAt, depthAt, onShelf, bodyAt, tideOf, tideRising, label, goalText, describeWorld, creatureById, MACHINES, BLOCK, BLOCK_Y0, blockKey, footAt, debrisAt, footing, -} from './tide-pool-world.js?v=26'; +} from './tide-pool-world.js?v=28'; const root = document.querySelector('[data-tide-pool]'); const status = document.querySelector('#tide-status'); diff --git a/src/components/tide-pool.tsx b/src/components/tide-pool.tsx index feffe89..75d1f88 100644 --- a/src/components/tide-pool.tsx +++ b/src/components/tide-pool.tsx @@ -141,7 +141,7 @@ export function TidePoolContent() { {/* Lets the three.js add-ons resolve the same pinned module the page already uses. */} + ); }