Far sidewall drawn behind the load with cleat ribs at their physics positions; sloped screens get a tinted mesh between rails and shake only when loaded. Powder skips cleat checks outside chunks that have cleats, and empty slopes let their chunks sleep. Record the screw conveyor and stall/carry-depth decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
224 lines
12 KiB
JavaScript
224 lines
12 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 (ph === PH_POWDER && !cleatCarrying && cleatCells.length && (cleatMask[j] || (dy > 0 && cleatMask[j - dx - dy * W]))) return 0;
|
|
return enterFree(m, ph, j, dx, dy);
|
|
}
|
|
// Powder already knows whether its chunk has barriers; other callers use enter.
|
|
function enterFree(m, ph, j, dx, dy) {
|
|
if (occ[j]) return 0;
|
|
const p = part[j];
|
|
if (p !== 0) {
|
|
if (IS_SCREW[p]) { if (ph === PH_POWDER && SCREW_DX[p] === -dx && SCREW_DY[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. A sorting
|
|
// step keeps its area awake for a while so a pile finishes sorting even at rest.
|
|
// Through burning fuel it stays slow: a furnace bed is tuned to it, and dirt heaped on a burning
|
|
// pile has to smother it into charcoal, not sink through.
|
|
let SORTP = 0.06;
|
|
const SORTP_FUEL = 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 and corner sealing off, so seeds keep the same world.
|
|
let LSTEP = 8, LFILM = 4;
|
|
let CORNER_SEAL = true;
|
|
function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
|
if (y >= H - 1) return;
|
|
const b = i + W;
|
|
if ((!barriers || (!cleatMask[i] && !cleatMask[b])) && enterFree(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 && (!barriers || (!cleatMask[i] && !cleatMask[b])) && 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() < (FUEL[t] ? SORTP_FUEL : 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) &&
|
|
!isSlopeBelt(blockP[((y + 1) >> 2) * BW + (x >> 2)])) return;
|
|
// Feed a receiving screw mouth sideways instead of falling under its bottom casing.
|
|
if (t === BELTL || t === BELTR) {
|
|
const b = ((y + 1) >> 2) * BW + (x >> 2);
|
|
const d = isSlopeBelt(blockP[b]) ? (blockV[b] & 1 ? 1 : -1) : 0;
|
|
if (d && x + d >= 0 && x + d < W && IS_SCREW[part[i + d]]) {
|
|
if ((!barriers || !cleatMask[i + d]) && enterFree(m, PH_POWDER, i + d, d, 0)) move(i, i + d, x, y, x + d, y);
|
|
return;
|
|
}
|
|
}
|
|
if (rnd() < STICK[m]) {
|
|
const d = (RS & 2) ? 1 : -1;
|
|
let nx = x + d;
|
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || (PH[mat[b]] !== PH_SOLID && (!barriers || !cleatMask[b])) || (PH[mat[i + d]] !== PH_SOLID && (!barriers || !cleatMask[i + d]))) && (!barriers || (!cleatMask[i] && !cleatMask[b + d])) && enterFree(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 && (!CORNER_SEAL || (PH[mat[b]] !== PH_SOLID && (!barriers || !cleatMask[b])) || (PH[mat[i - d]] !== PH_SOLID && (!barriers || !cleatMask[i - d]))) && (!barriers || (!cleatMask[i] && !cleatMask[b - d])) && enterFree(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 (IS_SCREW[p]) { if (ph === PH_POWDER && SCREW_DX[p] === -dx && SCREW_DY[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 && (!CORNER_SEAL || PH[mat[b]] !== PH_SOLID || PH[mat[i + d]] !== PH_SOLID) && 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 && (!CORNER_SEAL || PH[mat[b]] !== PH_SOLID || PH[mat[i - d]] !== PH_SOLID) && 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 && (!CORNER_SEAL || PH[mat[u]] !== PH_SOLID || PH[mat[i + d]] !== PH_SOLID) && 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 && (!CORNER_SEAL || PH[mat[u]] !== PH_SOLID || PH[mat[i - d]] !== PH_SOLID) && 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 wakeBotPowder() {
|
|
const x = bot.x + bot.w / 2, y = bot.y + bot.h / 2;
|
|
const x0 = Math.max(0, Math.floor((x - 100) / 32)), x1 = Math.min(CW - 1, Math.floor((x + 100) / 32));
|
|
const y0 = Math.max(0, Math.floor((y - 100) / 32)), y1 = Math.min(CH - 1, Math.floor((y + 100) / 32));
|
|
for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) {
|
|
const c = cy * CW + cx;
|
|
if (!powderArea[c] || awake[c]) continue;
|
|
const dx = Math.max(cx * 32 - x, 0, x - (cx * 32 + 31));
|
|
const dy = Math.max(cy * 32 - y, 0, y - (cy * 32 + 31));
|
|
if (dx * dx + dy * dy <= 10000) awake[c] = 1;
|
|
}
|
|
}
|
|
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; powderArea[rowBase + cx] = 0; }
|
|
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;
|
|
roofDirty[cx] = 1;
|
|
const x0 = cx << 5, x1 = Math.min(W, x0 + 32), barriers = cleatChunks[rowBase + cx];
|
|
// 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) continue;
|
|
const ph = PH[m];
|
|
if (ph === PH_POWDER) powderArea[rowBase + cx] = 1;
|
|
if (stamp[i] === tickStamp) continue;
|
|
if (ph === PH_SOLID) continue;
|
|
const p = part[i];
|
|
if (IS_SCREW[p] && ph === PH_POWDER) continue; // a screw conveyor moves its own grains
|
|
if (ph === PH_POWDER) powder(i, x, y, m, barriers); else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|