Files
deepgrain-foundry/src/sim/cells.js
T
44r0n7andClaude Opus 5.5 f678165e1b Physics test pass for sand, molten metal and gases; stuck gas finishes, steam is not deleted (t21)
- New test/t_physics.js: heaps, sorting, sinking, world edges; molten copper levelling, setting,
  quenching, slag over copper, cavity filling; smoke, steam, fire, sealed pockets, the top row.
- Gas in a sealed pocket kept its area asleep and never faded or condensed: gas() keeps its area
  awake while it lasts.
- A rain drop or blown grain landing on steam deleted the steam's water: it goes to the clouds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:55:04 -04:00

190 lines
8.8 KiB
JavaScript

// Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids
// and gases (simMove).
// ---------- Cell helpers ----------
function setCell(i, m, t, fl) {
const old = mat[i];
if (PARTMAT[old] && old !== m) partLost(i);
mat[i] = m; temp[i] = t; flags[i] = fl || 0; life[i] = (PH[m] === PH_LIQUID && FRZ[m]) ? 255 : 0; digp[i] = 0;
const x = i % W, y = (i / W) | 0;
wakeXY(x, y);
// far off the local air temperature, or something that cannot last in it (snow in warm air,
// water in freezing air): let the heat pass see it
if (t - AMB[y] > 20 || AMB[y] - t > 20 || (MELT[m] !== 0 && AMB[y] >= MELT[m]) || (m === WATER && AMB[y] < -0.5)) wakeHeatXY(x, y);
}
function swapCells(i, j) {
let a = mat[i]; mat[i] = mat[j]; mat[j] = a;
const f = temp[i]; temp[i] = temp[j]; temp[j] = f;
a = life[i]; life[i] = life[j]; life[j] = a;
a = flags[i]; flags[i] = flags[j]; flags[j] = a;
stamp[i] = tickStamp; stamp[j] = tickStamp;
}
function move(i, j, x, y, nx, ny) {
swapCells(i, j);
wakeXY(x, y); wakeXY(nx, ny);
if (ny === y && LFILM > 1 && PH[mat[j]] === PH_LIQUID && (y === 0 || PH[mat[i - W]] !== PH_LIQUID)) {
// a top-layer grain stepping sideways may clear the path to a drop for surface grains far
// along the row, so wake them; moves inside a body of water do not (settled lakes must sleep)
const reach = SPREAD[mat[j]] * LFILM, row = (y >> 5) * CW;
const first = Math.max(0, x - reach) >> 5, last = Math.min(W - 1, x + reach) >> 5;
for (let cx = first; cx <= last; cx++) awake[row + cx] = 3;
}
if (part[j] === P_WHEEL) { const mm = machines[mach[j]]; if (mm) mm.flow += PH[mat[j]] === PH_LIQUID ? 1 : 0.5; }
const d = temp[j] - AMB[ny];
if (d > 25 || d < -25) wakeHeatXY(nx, ny);
}
// Can a grain of material m (phase ph) enter cell j moving by (dx,dy)?
// 0 = no, 1 = into empty, 2 = by displacing what is there.
function enter(m, ph, j, dx, dy) {
if (occ[j]) return 0;
const p = part[j];
if (p !== 0) {
if (p <= P_TR) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return 0; } // liquids and gases flow through freely
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return 0; } // thick slag does not run through a grate
else if (p === P_INTAKE) { if (ph !== PH_GAS) return 0; }
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return 0; }
}
const t = mat[j];
if (t === EMPTY) return 1;
const tp = PH[t];
if (tp === PH_SOLID) return 0;
if (stamp[j] === tickStamp && tp !== PH_GAS) return 0;
if (ph === PH_GAS) {
if (dy < 0 && (tp === PH_LIQUID || (tp === PH_GAS && DENS[t] > DENS[m]))) return 2;
return 0;
}
if (tp === PH_GAS) return 2;
if (dy > 0 && DENS[m] > DENS[t] && (tp === PH_LIQUID || ph === PH_LIQUID)) return rnd() < (DENS[m] > DENS[t] * 2 ? 0.55 : 0.3) ? 2 : 0;
return 0;
}
// ---------- Movement ----------
// Heavy grains settle through a loose pile of much lighter ones, slowly. A sorting
// step keeps its area awake for a while so a pile finishes sorting even at rest.
let SORTP = 0.003;
// Liquids step up to LSTEP cells toward a drop in one tick (fewer the thicker they are), and a top-layer grain looks LFILM
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
// World generation settles with both at 1, so every seed keeps making the same world.
let LSTEP = 8, LFILM = 4;
function powder(i, x, y, m) {
if (y >= H - 1) return;
const b = i + W;
if (enter(m, PH_POWDER, b, 0, 1)) { move(i, b, x, y, x, y + 1); return; }
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
const t = mat[b];
if (PH[t] === PH_POWDER && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < SORTP) { move(i, b, x, y, x, y + 1); awake[(y >> 5) * CW + (x >> 5)] = 40; return; }
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
if (t === BELTL || t === BELTR || part[b] === P_FILTER) return;
if (rnd() < STICK[m]) {
const d = (RS & 2) ? 1 : -1;
let nx = x + d;
if (nx >= 0 && nx < W && enter(m, PH_POWDER, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
nx = x - d;
if (nx >= 0 && nx < W && enter(m, PH_POWDER, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
}
}
function passable(m, ph, j, dx, dy) {
// pure check (no randomness): cell is empty or gas and part rules allow
if (occ[j]) return false;
const p = part[j];
if (p !== 0) {
if (p <= P_TR) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return false; }
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return false; }
else if (p === P_INTAKE) { if (ph !== PH_GAS) return false; }
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return false; }
}
const t = mat[j];
return t === EMPTY || PH[t] === PH_GAS;
}
function liquid(i, x, y, m) {
if (y < H - 1) {
const b = i + W;
if (enter(m, PH_LIQUID, b, 0, 1)) { move(i, b, x, y, x, y + 1); return; }
const d = (RS & 2) ? 1 : -1;
let nx = x + d;
if (nx >= 0 && nx < W && enter(m, PH_LIQUID, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
nx = x - d;
if (nx >= 0 && nx < W && enter(m, PH_LIQUID, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
}
if (rnd() > FLUID[m]) return;
const sp = SPREAD[m] * (y === 0 || PH[mat[i - W]] !== PH_LIQUID ? LFILM : 1);
const d0 = (RS & 4) ? 1 : -1;
for (let pass = 0; pass < 2; pass++) {
const d = pass ? -d0 : d0;
for (let s = 1; s <= sp; s++) {
const nx = x + d * s;
if (nx < 0 || nx >= W) break;
const j = i + d * s;
if (!passable(m, PH_LIQUID, j, d, 0)) break;
if (y < H - 1 && passable(m, PH_LIQUID, j + W, 0, 1)) {
const k = Math.min(s, Math.max(1, (LSTEP * FLUID[m]) | 0)); // thick liquids take shorter steps
move(i, i + d * k, x, y, x + d * k, y);
return;
}
}
}
}
function gas(i, x, y, m) {
// Even blocked gas must keep aging or condensing until it is gone.
awake[(y >> 5) * CW + (x >> 5)] = 3;
if (m === FIRE) {
const l = life[i];
if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; }
life[i] = l - 1;
// flames lick at trees behind them
if (rnd() < 0.25) { const j = neighborDir(i, x, y, (rnd() * 4) | 0); if (j >= 0 && bgm[j] && mat[j] === EMPTY && temp[i] > IGN[bgm[j]] && rnd() < 0.3) igniteTree(j); }
} else if (m === SMOKE) {
if (rnd() < (y < roofY[x] ? 0.5 : 0.05)) {
const l = life[i];
if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; }
life[i] = l - 1;
}
} else if (m === STEAM && temp[i] < CONDT[STEAM]) wakeHeatXY(x, y); // cooled steam drifting under a roof needs the heat pass to condense it
if (y === 0 || (m === STEAM && y <= weather.skyY + 4 && y < roofY[x])) {
// steam that has risen to cloud height in the open sky joins the clouds
if (m === STEAM) absorbSteam(x);
mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return;
}
const u = i - W;
if (enter(m, PH_GAS, u, 0, -1)) { move(i, u, x, y, x, y - 1); return; }
const d = (RS & 2) ? 1 : -1;
let nx = x + d;
if (nx >= 0 && nx < W && enter(m, PH_GAS, u + d, d, -1)) { move(i, u + d, x, y, nx, y - 1); return; }
nx = x - d;
if (nx >= 0 && nx < W && enter(m, PH_GAS, u - d, -d, -1)) { move(i, u - d, x, y, nx, y - 1); return; }
if (rnd() < 0.6) {
nx = x + d;
if (nx >= 0 && nx < W && mat[i + d] === EMPTY && passable(m, PH_GAS, i + d, d, 0)) { move(i, i + d, x, y, nx, y); }
}
}
function simMove() {
for (let cy = CH - 1; cy >= 0; cy--) {
const rowBase = cy * CW;
let any = false;
for (let cx = 0; cx < CW; cx++) if (awake[rowBase + cx]) { any = true; break; }
if (!any) continue;
const y0 = cy << 5, y1 = Math.min(H, y0 + 32);
for (let y = y1 - 1; y >= y0; y--) {
const ltr = ((y + tick) & 1) === 0;
const yw = y * W;
for (let k = 0; k < CW; k++) {
const cx = ltr ? k : CW - 1 - k;
if (!awake[rowBase + cx]) continue;
const x0 = cx << 5, x1 = Math.min(W, x0 + 32);
// alternate rows scan left to right and right to left, so nothing drifts one way
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) {
const i = yw + x, m = mat[i];
if (m === EMPTY || stamp[i] === tickStamp) continue;
const ph = PH[m];
if (ph === PH_SOLID) continue;
const p = part[i];
if (p >= P_TU && p <= P_TR && ph === PH_POWDER) continue; // a screw tube moves its own grains
if (ph === PH_POWDER) powder(i, x, y, m); else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
}
}
}
}
}