// 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); } } } } }