// 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 (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; 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]; 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)) { // step one cell toward the drop (keeps flow continuous and local) move(i, i + d, x, y, x + d, y); return; } } } } function gas(i, x, y, m) { 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; } } 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); } } } } }