Metabolism M2.9: flora islands, gardens, walls, farmer strain, aimed work (VM AIM actions), food-limited ecology rebalance
This commit is contained in:
+368
-76
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user