Metabolism M2.9: flora islands, gardens, walls, farmer strain, aimed work (VM AIM actions), food-limited ecology rebalance

This commit is contained in:
Dejvino
2026-10-28 12:49:17 +01:00
parent 1d497e556e
commit 476fa1833a
11 changed files with 675 additions and 120 deletions
+368 -76
View File
@@ -32,8 +32,9 @@ import { Rng } from '../rng.js';
import { wellPos, wellEnv, buildKernel } from './wells.js';
import {
runProgram, SENSE_COUNT, GRAD_COUNT, ACTION_COUNT, REGS,
S_FOOD, S_SELF, S_KIN, S_FOE, S_TRAIL, S_LAW, S_HUNGER, S_EDGE, S_TIME, S_TOTAL, S_WELL, S_PEACE, S_ROCK,
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL,
S_FOOD, S_SELF, S_KIN, S_FOE, S_TRAIL, S_LAW, S_HUNGER, S_EDGE, S_TIME, S_TOTAL, S_WELL, S_PEACE, S_ROCK, S_GARDEN,
G_GARDEN,
G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL, G_ROCK,
} from '../song/strains.js';
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
@@ -51,6 +52,38 @@ const DEPOSIT_SCALE = 0.012;
// bodies are founded once and persist; war kills, it does not convert.
const COLONIZE_EVERY = 60; // ~2 s: a blessed frontier settlement may found
/** A normalised Gaussian footprint (cell indices + weights) per coarse block. */
function buildBlockKernels(F, FC) {
const kernels = [];
const bs = F / FC;
for (let b = 0; b < FC * FC; b++) {
const cx = ((b % FC) + 0.5) * bs - 0.5;
const cy = (((b / FC) | 0) + 0.5) * bs - 0.5;
const sig = Math.max(1.4, bs * 0.9);
const r = Math.ceil(sig * 2.2);
const denom = 1 / (2 * sig * sig);
const idx = [];
const w = [];
let total = 0;
for (let dy = -r; dy <= r; dy++) {
const py = Math.round(cy) + dy;
if (py < 0 || py >= F) continue;
for (let dx = -r; dx <= r; dx++) {
const px = Math.round(cx) + dx;
if (px < 0 || px >= F) continue;
const g = Math.exp(-(dx * dx + dy * dy) * denom);
if (g < 0.02) continue;
idx.push(py * F + px);
w.push(g);
total += g;
}
}
const k = total > 0 ? 1 / total : 0;
kernels.push({ idx: Int32Array.from(idx), w: Float32Array.from(w, (v) => v * k) });
}
return kernels;
}
export class World {
constructor(song, { fps = 30, field = FIELD, seed = song.seed, substeps = SUBSTEPS, genesis } = {}) {
this.song = song;
@@ -118,6 +151,24 @@ export class World {
// carry and wall with. rockMask marks the impassable cells.
this.rock = new Float32Array(n);
this.rockMask = new Uint8Array(n);
this.rockFallen = 0; // total stone the song has dropped
// Flora (M2.9): the wild plants — the world's pioneers, living in
// ISLANDS scattered at birth. They ARE food, they regrow on their
// own, and they BLOOM when the world is starving or dwindling
// (seeding it back to life), then recede once the population is
// strong and feeding itself again. Never enough to sustain a
// large world, and never anywhere but on their own seed ground.
this.flora = new Float32Array(n);
this.floraSeed = new Uint8Array(n); // the islands: where flora may live
this.famine = 0; // smoothed mean hunger of the population
this.peakPop = 0; // recent population high-water mark
this.hungerMass = 0; // per-frame accumulator (the reaction fills it)
// Cultivated ground: worked stone that slowly yields food, then
// petrifies back to rock. garden[i] is the fertility left.
this.garden = new Float32Array(n);
this.gardenSrc = new Float32Array(n);
// Recent tending, per cell: a visited garden yields far more.
this.tend = new Float32Array(n);
// Deterministic jitter: static signed noise fields from the seeded RNG,
// read every substep. The track's flatness sets how loud it is.
@@ -155,6 +206,7 @@ export class World {
this.blkBody = Array.from({ length: this.B }, () => new Float32Array(fc));
this.blkTotal = new Float32Array(fc);
this.blkRock = new Float32Array(fc);
this.blkGarden = new Float32Array(fc);
this.domB = new Int8Array(fc).fill(-1);
this.subB = new Int8Array(fc).fill(-1);
this.fxX = Array.from({ length: this.m }, () => new Float32Array(fc));
@@ -162,12 +214,18 @@ export class World {
this.velX = new Float32Array(fc);
this.velY = new Float32Array(fc);
this.wellField = new Float32Array(fc);
this.rockField = new Float32Array(fc); // smoothed stone: the far smell
this.ancX = Array.from({ length: this.m }, () => new Float32Array(fc));
this.ancY = Array.from({ length: this.m }, () => new Float32Array(fc));
this.synergy = new Float32Array(this.m * fc);
this.valScratch = new Float64Array(this.m);
this.growScratch = new Float64Array(this.m);
this.hostility = new Float32Array(fc);
this.hitAt = new Float32Array(fc);
// The last aim per (strain, block): the senses of WORK look where
// the work points, one substep back — sight follows intent.
this.aimX = new Int8Array(this.m * fc);
this.aimY = new Int8Array(this.m * fc);
this.cUp = new Int32Array(fc);
this.cDown = new Int32Array(fc);
this.cLeft = new Int32Array(fc);
@@ -191,7 +249,14 @@ export class World {
}
}
this.carry = new Float32Array(this.m * fc); // quarried burden per block
// Quarried burden per strain, a fine field advected with the
// population flux — stone is genuinely HAULED: dug at the quarry,
// carried with the crowd, walled at home.
this.carry = Array.from({ length: this.m }, () => new Float32Array(n));
this.carryNext = Array.from({ length: this.m }, () => new Float32Array(n));
// Rounded build footprints: a Gaussian kernel per coarse block, so
// walls rise organically instead of as square tiles.
this.buildKernel = buildBlockKernels(field, Math.max(16, Math.round(field / 2)));
// Strain programs run on this decision grid: registers are the
// strains' persistent memory, the action slots carry each decision.
@@ -240,6 +305,7 @@ export class World {
this.deathsThisFrame = 0;
this.seedRocks();
this.seedFlora();
this.refreshRockMask();
this.foundOpeningBodies();
this.seedInitial();
@@ -398,6 +464,7 @@ export class World {
this.warmth = Math.max(-1, Math.min(1,
this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1)));
const living = this.framePrep(regime);
this.updateRockField();
this.inject(this.frame, regime);
this.refreshRockMask();
for (let s = 0; s < this.substeps; s++) this.substep(regime);
@@ -483,8 +550,8 @@ export class World {
for (let k = 0; k < n; k++) {
const x = this.rng.next();
const y = this.rng.next();
const sigma = 0.022 + 0.030 * this.rng.next();
const amount = 3.5 + 6.5 * this.rng.next();
const sigma = 0.030 + 0.045 * this.rng.next();
const amount = 5.0 + 9.0 * this.rng.next();
this.rockSplat(x, y, sigma, amount);
}
}
@@ -538,11 +605,40 @@ export class World {
return { idx, w, k };
}
/** Scatter the wild flora: small patches, deterministic from the seed. */
seedFlora() {
const n = 9 + Math.round(6 * clamp01(this.song.summary.onsetDensity / 6));
for (let k = 0; k < n; k++) {
const x = this.rng.next();
const y = this.rng.next();
const sigma = 0.010 + 0.011 * this.rng.next();
const F = this.field;
const cx = Math.round(x * (F - 1));
const cy = Math.round(y * (F - 1));
const sig = Math.max(1.2, sigma * F);
const r = Math.ceil(sig * 2.2);
const denom = 1 / (2 * sig * sig);
for (let dy = -r; dy <= r; dy++) {
const py = cy + dy;
if (py < 0 || py >= F) continue;
for (let dx = -r; dx <= r; dx++) {
const px = cx + dx;
if (px < 0 || px >= F) continue;
const i = py * F + px;
if (this.rockMask[i]) continue;
const g = Math.exp(-(dx * dx + dy * dy) * denom);
this.floraSeed[i] = 1;
this.flora[i] = Math.min(0.75, this.flora[i] + g * (0.35 + 0.3 * this.rng.next()));
}
}
}
}
/** The impassable frontier, recomputed after any rock change. */
refreshRockMask() {
const rock = this.rock;
const mask = this.rockMask;
for (let i = 0; i < rock.length; i++) mask[i] = rock[i] > 0.55 ? 1 : 0;
for (let i = 0; i < rock.length; i++) mask[i] = rock[i] > 0.25 ? 1 : 0;
}
// ---- wells ---------------------------------------------------------------
@@ -623,8 +719,10 @@ export class World {
this.birthsThisFrame = 0;
this.deathsThisFrame = 0;
this.moveAccum = 0;
this.hungerMass = 0;
const living = this.framePrep(regime);
this.updateRockField();
this.inject(f, regime);
this.refreshRockMask();
@@ -660,6 +758,28 @@ export class World {
}
}
/**
* The far smell of stone: the rock field, box-blurred twice on the
* coarse grid. Adjacent-block gradients only pull at the rim; this is
* what draws a quarry crew across the map.
*/
updateRockField() {
const FC = this.FC;
const fc = FC * FC;
const f = this.rockField;
f.fill(0);
const rock = this.rock;
const coarseOf = this.coarseOf;
for (let i = 0; i < this.n; i++) f[coarseOf[i]] += rock[i];
for (let pass = 0; pass < 2; pass++) {
const prev = Float32Array.from(f);
for (let b = 0; b < fc; b++) {
f[b] = (prev[b] * 4 + prev[this.cUp[b]] + prev[this.cDown[b]] +
prev[this.cLeft[b]] + prev[this.cRight[b]]) / 8;
}
}
}
/** Home anchors: each strain is pulled to its nearest living well. */
updateAnchors(living) {
const FC = this.FC;
@@ -730,7 +850,8 @@ export class World {
if (e.voice === 2) {
this.rockSplat(e.x, e.y, e.spread * 1.4, -e.strength * 2.4 * rockPol.shatter * ops.erode);
} else {
this.rockSplat(e.x, e.y, e.spread * 1.2, e.strength * 3.2 * ops.rockfall * rockPol.drop);
this.rockFallen += e.strength * 2.8 * ops.rockfall * rockPol.drop;
this.rockSplat(e.x, e.y, e.spread * 1.3, e.strength * 2.8 * ops.rockfall * rockPol.drop);
}
this.activity[ay * 32 + ax] += 1.5;
}
@@ -751,6 +872,7 @@ export class World {
runStrains(regime) {
const FC = this.FC;
const blkRock = this.blkRock;
const blkGarden = this.blkGarden;
const fc = FC * FC;
const m = this.m;
const blkPop = this.blkPop;
@@ -820,7 +942,17 @@ export class World {
ss[S_TOTAL] = clamp01(tot * 0.005);
ss[S_WELL] = clamp01(wellField[b]);
ss[S_PEACE] = peace;
ss[S_ROCK] = clamp01(blkRock[b] * 0.35);
// The work-senses look where the work points: the block the
// strain aimed at last substep (its own feet when it stood).
const aimIdx = s * fc + b;
const axn = this.aimX[aimIdx];
const ayn = this.aimY[aimIdx];
const wb = (axn || ayn)
? Math.min(FC - 1, Math.max(0, (b % FC) + axn)) +
FC * Math.min(FC - 1, Math.max(0, ((b / FC) | 0) + ayn))
: b;
ss[S_ROCK] = clamp01(blkRock[wb] * 0.45);
ss[S_GARDEN] = clamp01(blkGarden[wb] * 0.5);
// ---- gradients (sign per field × axis) ---------------------
gg[G_FOOD * 2] = Math.sign(gRx);
@@ -847,8 +979,17 @@ export class World {
}
gg[G_WELL * 2] = Math.sign(wellField[rg] - wellField[lg]);
gg[G_WELL * 2 + 1] = Math.sign(wellField[dg] - wellField[ug]);
gg[G_ROCK * 2] = Math.sign(this.rockField[rg] - this.rockField[lg]);
gg[G_ROCK * 2 + 1] = Math.sign(this.rockField[dg] - this.rockField[ug]);
gg[G_GARDEN * 2] = Math.sign(blkGarden[rg] - blkGarden[lg]);
gg[G_GARDEN * 2 + 1] = Math.sign(blkGarden[dg] - blkGarden[ug]);
runProgram(this.stMem, s * REGS * FC + b, ss, gg, act, strains[s].code);
// Remember the aim: the next substep's work-senses look there.
let nx = act[12] !== 0 ? Math.sign(act[12]) : Math.sign(act[0]);
let ny = act[13] !== 0 ? Math.sign(act[13]) : Math.sign(act[1]);
this.aimX[aimIdx] = nx;
this.aimY[aimIdx] = ny;
// ---- actions → forces + reaction mods ----------------------
fxX[s][b] = act[0] + this.ancX[s][b] * 0.12 + gRx * 0.02 - dgx * pressure;
@@ -860,6 +1001,10 @@ export class World {
actBuf[off + 5 * actFC] = act[5]; // SPAWN
actBuf[off + 6 * actFC] = act[6]; // DIG
actBuf[off + 7 * actFC] = act[7]; // BUILD
actBuf[off + 8 * actFC] = act[8]; // TILL
actBuf[off + 9 * actFC] = act[9]; // TEND
actBuf[off + 12 * actFC] = act[12]; // AIM_X
actBuf[off + 13 * actFC] = act[13]; // AIM_Y
// Synergy: this strain breeds fastest in a mixed, same-body
// crowd — the share of the block's population that is kin.
@@ -920,16 +1065,19 @@ export class World {
const blkBody = this.blkBody;
const blkTotal = this.blkTotal;
const blkRock = this.blkRock;
const blkGarden = this.blkGarden;
blkR.fill(0);
blkTotal.fill(0);
blkRock.fill(0);
blkGarden.fill(0);
for (let s = 0; s < m; s++) blkPop[s].fill(0);
for (let b = 0; b < this.B; b++) { blkTrail[b].fill(0); blkBody[b].fill(0); }
for (let i = 0; i < n; i++) {
const b = coarseOf[i];
const d = density[i];
blkR[b] += res[i];
blkR[b] += res[i] + this.flora[i] * 0.5;
blkRock[b] += rock[i];
blkGarden[b] += this.garden[i];
if (d > 1e-6) {
blkTotal[b] += d;
for (let s = 0; s < m; s++) blkPop[s][b] += pop[s][i];
@@ -962,22 +1110,43 @@ export class World {
// ---- 2. the strain programs decide the forces --------------------------
this.runStrains(regime);
// The per-block war pressure: hits × hostile presence. A body strikes
// its neighbour only where the two actually touch.
// The per-block war pressure: hits land at their AIM target — where
// the striker is heading, i.e. where the foe actually stands — and
// siege saps the stone there. War has a front line now.
const actBuf = this.stAct;
const actFC = FC * FC;
const hostility = this.hostility;
hostility.fill(0);
const hitAt = this.hitAt;
hitAt.fill(0);
const aimTarget = (b, s) => {
const base = s * ACTION_COUNT * actFC + b;
let ax = actBuf[base + 12 * actFC];
let ay = actBuf[base + 13 * actFC];
if (ax === 0) ax = actBuf[base + 0 * actFC]; // default: where we walk
if (ay === 0) ay = actBuf[base + 1 * actFC];
const sx = Math.sign(ax);
const sy = Math.sign(ay);
const bx = b % FC;
const by = (b / FC) | 0;
const nx = Math.min(FC - 1, Math.max(0, bx + sx));
const ny = Math.min(FC - 1, Math.max(0, by + sy));
return ny * FC + nx;
};
for (let b = 0; b < fc; b++) {
for (let s = 0; s < m; s++) {
const hits = actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b];
if (hits > 1e-9) hitAt[aimTarget(b, s)] += hits;
}
}
for (let b = 0; b < fc; b++) {
const dom = domB[b];
const other = subB[b];
if (dom < 0 || other < 0) continue;
const dip = this.diplomacy(dom, other);
if (dip >= 0) continue;
let hits = 0;
for (let s = 0; s < m; s++) hits += actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b];
const foeShare = clamp01(blkBody[other][b] / Math.max(1e-6, blkTotal[b]));
hostility[b] = Math.min(1, hits * 8) * (-dip) * foeShare;
hostility[b] = Math.min(1, hitAt[b] * 8) * (-dip) * foeShare;
}
// ---- 3. advection + diffusion ------------------------------------------
@@ -1004,11 +1173,16 @@ export class World {
this.advect(pop[s], next[s], this.fxX[s], this.fxY[s],
advFrac, jitterAmt, diff, sub, true);
}
const carry = this.carry;
const allegDiff = diff; // allegiance is frozen-in: it travels exactly
// with the population's own spreading
for (let c = 0; c < this.B; c++) {
this.advect(alleg[c], allegNext[c], velX, velY, advFrac, 0, allegDiff, sub, false);
}
// The quarried burden rides the same flux — this is the hauling.
for (let s = 0; s < m; s++) {
this.advect(carry[s], this.carryNext[s], velX, velY, advFrac, 0, diff * 0.15, sub, false);
}
// ---- 4. react -----------------------------------------------------------
const societyCeil = prog.society.ceiling;
@@ -1030,6 +1204,7 @@ export class World {
// Saturated food factor (rational approximation, no transcendentals).
const foodFactor = (r0 * 5) / (1 + r0 * 5);
this.hungerMass += density[i] * (1 - foodFactor);
// The gate is nearly shut on bare ground: food is the substrate, not a
// catalyst. A colony that outgrows its ground starves back and must
// wander — the world tracks its wells instead of congealing.
@@ -1041,15 +1216,24 @@ export class World {
if (host > 1e-9 && dom >= 0) {
const other = subB[b] >= 0 ? subB[b] : dom;
const loseShare = dom !== other
? allegNext[other][i] / Math.max(1e-6, density[i])
? clamp01(allegNext[other][i] / Math.max(1e-6, density[i]))
: 0;
casualty = host * 2.5 * sub * loseShare;
casualty = Math.min(0.35, host * 2.5 * sub * loseShare);
}
let demand = 0;
for (let s = 0; s < m; s++) demand += vals[s] * this.eatR[s] * actBuf[s * ACTION_COUNT * actFC + 3 * actFC + b];
demand *= sub;
res[i] = demand > 0 ? Math.max(0, r0 - Math.min(r0, demand)) : r0;
// The store runs out → the crowd mows the wild flora (half a
// stand at a time; it regrows if left alone).
const fromRes = Math.min(r0, demand);
// Grazing can only take a whisker of the stand per bite — a
// crowd mows a meadow over seconds, never in an instant.
const bite = Math.min(this.flora[i] * 0.004, demand - fromRes);
this.flora[i] -= bite;
res[i] = demand > 0
? Math.max(0, r0 - fromRes) + bite * 0.55
: r0;
const starve = (1 - foodFactor) * (0.28 + 0.9 * ops.spawn) * sub;
const society = Math.max(0, 1 - density[i] / societyCeil);
@@ -1067,7 +1251,7 @@ export class World {
// eating.)
g = Math.max(0, p + g * society) - p * starve * this.eatR[s];
g = Math.max(0, g);
if (casualty > 0) g -= g * casualty * 6; // war kills; it does not convert
if (casualty > 0) g = Math.max(0, g - g * casualty * 6); // war kills; it does not convert
grown[s] = g;
total += g;
}
@@ -1109,59 +1293,6 @@ export class World {
}
}
// ---- rock economy: quarries and walls -----------------------------------
// Digging takes effort (a slow bite out of the stone) and yields
// soil; the burden rides in the block's carry until the program
// builds with it. This is how the world moves its own ground around.
const rockPol = prog.rock;
const carry = this.carry;
const mask = this.rockMask;
const bs = F / FC;
for (let b = 0; b < fc; b++) {
const bx = (b % FC) * bs;
const by = ((b / FC) | 0) * bs;
for (let s = 0; s < m; s++) {
const base = s * ACTION_COUNT * actFC + b;
const dig = actBuf[base + 6 * actFC];
const build = actBuf[base + 7 * actFC];
if (dig > 1e-6) {
let want = dig * rockPol.dig * 1.1 * sub;
let taken = 0;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (want <= 1e-9) break;
const i = y * F + x;
const t = Math.min(rock[i], want / (bs * bs));
if (t <= 0) continue;
rock[i] -= t;
taken += t;
want -= t;
res[i] += t * 0.35; // ground-down rock feeds the ground
}
}
carry[s * fc + b] += taken * 0.85;
}
const c = carry[s * fc + b];
if (build > 1e-6 && c > 1e-6) {
const rate = build * rockPol.build * 1.1 * sub;
let deposit = Math.min(c, rate + c * 0.04);
let left = deposit;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (left <= 1e-9) break;
const i = y * F + x;
if (mask[i]) continue;
const t = Math.min(left / (bs * bs), 2.2 - rock[i]);
if (t <= 0) continue;
rock[i] += t;
left -= t;
}
}
carry[s * fc + b] = c - (deposit - left);
}
}
}
// ---- swap ---------------------------------------------------------------
let totalPop = 0;
for (let s = 0; s < m; s++) {
@@ -1176,7 +1307,106 @@ export class World {
alleg[c] = allegNext[c];
allegNext[c] = t;
}
for (let s = 0; s < m; s++) {
const t = carry[s];
carry[s] = this.carryNext[s];
this.carryNext[s] = t;
}
this.totalPop = totalPop;
// ---- rock economy: quarries and walls -----------------------------------
// Digging takes effort (a slow bite out of the stone) and yields
// soil; the burden rides in the block's carry until the program
// builds with it. This is how the world moves its own ground around.
const rockPol = prog.rock;
const mask = this.rockMask;
const bs = F / FC;
const cells = bs * bs;
for (let b = 0; b < fc; b++) {
for (let s = 0; s < m; s++) {
const base = s * ACTION_COUNT * actFC + b;
const dig = actBuf[base + 6 * actFC];
const build = actBuf[base + 7 * actFC];
const till = actBuf[base + 8 * actFC];
const tend = actBuf[base + 9 * actFC];
if (dig <= 1e-6 && build <= 1e-6 && till <= 1e-6 && tend <= 1e-6) continue;
// The work lands AHEAD: at the block the aim points at.
const tb = aimTarget(b, s);
const bx = (tb % FC) * bs;
const by = ((tb / FC) | 0) * bs;
if (till > 1e-6) {
// Work the stone into beds — one cell brought to
// maturity before the next is started (a scattered
// dusting of fertility would exhaust as fast as it is
// laid and mint stone from nothing). The garden's own
// production is the payoff; work yields no food.
let want = till * rockPol.till * 3.0 * sub;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (want <= 1e-9) break;
const i = y * F + x;
if (this.garden[i] >= 0.2) continue; // already a bed
const room = Math.min(rock[i], 0.6 - this.garden[i]);
if (room <= 1e-6) continue;
const t = Math.min(room, want);
rock[i] -= t;
this.garden[i] += t;
want -= t;
}
}
}
if (dig > 1e-6) {
let want = dig * rockPol.dig * 2.0 * sub;
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
if (want <= 1e-9) break;
const i = y * F + x;
const t = Math.min(rock[i], want / cells);
if (t <= 0) continue;
rock[i] -= t;
want -= t;
carry[s][i] += t * 0.9; // shouldered burden
}
}
}
if (tend > 1e-6) {
// Tended beds yield far more: the husbandman's visit
// raises the garden's vigour for a while.
for (let y = by; y < by + bs; y++) {
for (let x = bx; x < bx + bs; x++) {
const i = y * F + x;
if (this.garden[i] < 0.02) continue;
this.tend[i] = Math.min(1, this.tend[i] + tend * sub * 2.5);
}
}
}
if (build > 1e-6) {
// The block's burden is pooled and mortared along a
// rounded footprint: walls rise organically.
const kernel = this.buildKernel[tb];
let avail = 0;
let room = 0;
for (let k = 0; k < kernel.idx.length; k++) {
const i = kernel.idx[k];
avail += carry[s][i];
room += kernel.w[k] * Math.max(0, 2.4 - rock[i]);
}
if (avail > 1e-9 && room > 1e-9) {
const deposit = Math.min(avail, build * rockPol.build * 4.0 * sub, room);
const takeK = deposit / avail;
for (let k = 0; k < kernel.idx.length; k++) {
const i = kernel.idx[k];
carry[s][i] *= 1 - takeK;
// each cell receives its weighted share of the
// room it still has
const roomK = kernel.w[k] * Math.max(0, 2.4 - rock[i]);
if (roomK > 0) rock[i] += deposit * roomK / room;
}
}
}
}
}
}
/**
@@ -1278,8 +1508,10 @@ export class World {
}
if (target > 1e-6) {
if (S > 1e-4) {
const k = target / S;
for (const b of this.aliveBodies) alleg[b][i] *= k;
const k = Math.min(1e4, target / S);
for (const b of this.aliveBodies) {
alleg[b][i] = Math.min(target, alleg[b][i] * k);
}
} else {
const x = (i % F) / F;
const y = ((i / F) | 0) / F;
@@ -1425,16 +1657,69 @@ export class World {
for (let i = 0; i < act.length; i++) act[i] *= actK;
// Erosion: the section's erode operator sets how fast the stone
// weathers away. Breakdowns clear the ground; drops raise it.
// weathers away. Breakdowns clear the ground; drops raise it. Even
// walls weather — unmaintained works crumble, so the masons are
// never done.
const rock = this.rock;
const rockHalf = 60 / (0.4 + 0.8 * ops.erode);
if (this.dbgRock) {
let before = 0; for (let i = 0; i < rock.length; i++) before += rock[i];
this.dbgBefore = before;
}
const rockHalf = 80 / (0.3 + 0.7 * ops.erode);
const rockK = Math.pow(0.5, dt / rockHalf);
for (let i = 0; i < rock.length; i++) rock[i] *= rockK;
// The flora is the world's pioneer, rooted in ISLANDS: it can only
// ever live on its own seed ground. Its vigour tracks the famine —
// a starving or dwindling world blooms with it (the seed-rain is
// the lifeline); a strong, well-fed world grazes it back to a
// whisper. The map keeps its green archipelago either way.
const famine = this.famine;
const popWeak = clamp01(1 - this.totalPop / Math.max(1, this.peakPop * 0.7));
const bloom = clamp01(famine * 0.6 + popWeak * 0.6);
const flora = this.flora;
const mask = this.rockMask;
const seeds = this.floraSeed;
for (let i = 0; i < flora.length; i++) {
if (mask[i] || !seeds[i]) { flora[i] = 0; continue; }
const f = flora[i];
flora[i] = Math.min(0.22, f + (0.008 * bloom + f * (1 - f / 0.22)) * (0.05 + 0.9 * bloom) * dt);
res[i] = Math.min(1.6, res[i] + f * 0.02 * dt);
}
// The gardens feed the world: cultivated ground yields a few units
// over its life, and spent ground petrifies back to rock.
const garden = this.garden;
const tendF = this.tend;
const tendK = Math.pow(0.5, dt / 4);
for (let i = 0; i < garden.length; i++) {
tendF[i] *= tendK;
const g = garden[i];
if (g < 0.01) continue;
const make = g * 0.045 * (1 + 2 * tendF[i]) * dt;
res[i] = Math.min(1.6, res[i] + make * 1.2);
garden[i] = g - make;
if (garden[i] < 0.03) {
// The bed is spent: the worked soil sets back into stone —
// almost all of it. Agriculture slowly eats the world's rock.
const back = garden[i] * 0.85 + 0.15;
rock[i] += back;
garden[i] = 0;
if (this.gardenSrc) this.gardenSrc[i] += back;
}
}
if (this.dbgRock) {
let after = 0; for (let i = 0; i < rock.length; i++) after += rock[i];
let g2r = 0;
for (let i = 0; i < this.garden.length; i++) if (this.gardenSrc[i] > 0) g2r += this.gardenSrc[i];
console.log('rockΔ this frame:', (after - this.dbgBefore).toFixed(1), '(garden-petrify portion:', g2r.toFixed(1), ')');
this.gardenSrc.fill(0);
}
// Wells recharge to a ceiling — but not through stone: rock capped
// ground stays barren until someone quarries it open.
const scale = (0.22 + 0.7 * regime.energy) * dt;
const mask = this.rockMask;
for (const { well, env } of living) {
const add = well.rate * scale * env / well.peak;
const idx = well.kernel.idx;
@@ -1448,6 +1733,13 @@ export class World {
}
record(f) {
// The famine signal: mean hunger of the living, smoothed; and the
// population's high-water mark (slowly forgotten, so a crash can
// ask the pioneers for help again).
const meanHunger = this.totalPop > 1 ? Math.min(1, this.hungerMass / this.totalPop) : 1;
this.famine += (meanHunger - this.famine) * 0.03;
this.peakPop = Math.max(this.totalPop, this.peakPop * 0.9995);
if (f < 0 || f >= this.history.alive.length) return;
const h = this.history;
h.alive[f] = this.totalPop;