// Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick). function engineIntake(mm) { for (const i of mm.intake) { if (mat[i] === STEAM) { const T = temp[i]; mm.energy += 1 + Math.max(0, T - 100) / 120; mat[i] = EMPTY; life[i] = 0; temp[i] = AMB[(i / W) | 0]; wakeIdx(i); const ex = mm.dir === 0 ? mm.x + mm.w : mm.x - 1, ey = mm.y + mm.h - 1; if (ex >= 0 && ex < W) { const j = ey * W + ex; if (mat[j] === EMPTY && !part[j]) { mat[j] = WATER; temp[j] = 92; life[j] = 0; flags[j] = 0; wakeXY(ex, ey); } } } } } function fanBlow(mm, sat) { const dir = mm.dir; // 0 right, 1 down, 2 left, 3 up const dx = [1, 0, -1, 0][dir], dy = [0, 1, 0, -1][dir]; const cur = curAir(); for (let lane = 0; lane < 4; lane++) { let sx, sy; if (dir === 0) { sx = mm.x + 4; sy = mm.y + lane; } else if (dir === 2) { sx = mm.x - 1; sy = mm.y + lane; } else if (dir === 1) { sx = mm.x + lane; sy = mm.y + 4; } else { sx = mm.x + lane; sy = mm.y - 1; } let powderDepth = 0; const range = 28; for (let k = 0; k < range; k++) { const x = sx + dx * k, y = sy + dy * k; if (x < 0 || y < 0 || x >= W || y >= H) break; const i = y * W + x, m = mat[i], ph = PH[m]; if (ph === PH_SOLID && part[i] !== P_GRATE) break; airS[i] = cur; if (ph === PH_POWDER) { if (++powderDepth > 16) break; } else if (ph === PH_GAS && k < range - 1 && rnd() < sat * 0.7 && stamp[i] !== tickStamp) { const nx = x + dx, ny = y + dy; if (nx >= 0 && ny >= 0 && nx < W && ny < H) { const j = ny * W + nx; if (mat[j] === EMPTY && passable(m, PH_GAS, j, dx, dy)) move(i, j, x, y, nx, ny); } } } wakeHeatXY(Math.min(W - 1, Math.max(0, sx + dx * 4)), Math.min(H - 1, Math.max(0, sy + dy * 4))); } } function machinesTick() { if (powerDirty && tick >= nextPowerBuild) { rebuildPower(); nextPowerBuild = tick + 6; } if (brokenMachines.length) { const ids = brokenMachines; brokenMachines = []; for (const id of ids) if (machines[id]) removeMachine(id, false, true); } for (const mm of machines) { if (!mm) continue; if (mm.type === 'wheel') { const inst = mm.flow * WHEEL_K; mm.out += (inst - mm.out) * 0.04; mm.flow = 0; mm.angle += Math.min(0.35, mm.out * 0.012); } else if (mm.type === 'refiner') { refinerTick(mm); } else if (mm.type === 'cutter') { cutterTick(mm); } else if (mm.type === 'silo') { siloTick(mm); } else if (mm.type === 'engine') { engineIntake(mm); mm.out += (mm.energy * ENGINE_K - mm.out) * 0.03; mm.energy = 0; mm.angle += Math.min(0.4, mm.out * 0.01); } } for (let n = 1; n < nets.length; n++) { const net = nets[n]; let s = 0; for (const p of net.prods) s += p.out; let d = net.nb * BELT_POWER + net.nt * TUBE_POWER; for (const f of net.fans) if (f.on) d += FAN_POWER; net.supply = s; net.demand = d; net.sat = POWER_FREE ? 1 : d > 0 ? Math.min(1, s / d) : 0; } for (const mm of machines) { if (!mm || mm.type !== 'fan') continue; const net = nets[mm.net]; const sat = POWER_FREE ? 1 : net ? net.sat : 0; mm.angle += sat * 0.5 * (mm.on ? 1 : 0); if (mm.on && sat > 0.05) fanBlow(mm, sat); // Keep the measured heat for the marker and hover panel. if ((tick + mm.id) % 30 === 0) { let hotT = 0; for (let y = mm.y; y < mm.y + 4; y++) for (let x = mm.x; x < mm.x + 4; x++) { const tt = temp[y * W + x]; if (tt > hotT) hotT = tt; } mm.hot = Math.round(hotT); } } // belts and filters move what rests on them every other tick: half a grain per tick if ((tick & 1) === 0) { for (let pass = 0; pass < 2; pass++) { const list = pass ? beltR : beltL, d = pass ? 1 : -1, bm = pass ? BELTR : BELTL; for (let k = 0; k < list.length; k++) { const b = list[k]; if (mat[b] !== bm || b < W) continue; const above = mat[b - W]; if (above === BELTL || above === BELTR) continue; // only the top surface carries if (netSat(b) <= 0) continue; if (PH[above] === PH_LIQUID) beltDrain(b); carryColumn(b - W, d, null); } } if (filtersDirty) rebuildFilters(); for (const b of filterR) filterCarry(b, 1); for (const b of filterL) filterCarry(b, -1); } // tubes: every loose grain inside steps one cell along, front of the line first, on the same // beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt if ((tick & 1) === 0) for (let p = P_TU; p <= P_TR; p++) { const list = tubeLists[p], dx = TDX[p], dy = TDY[p], dj = dy * W + dx, flat = dy === 0; for (let k = 0; k < list.length; k++) { const i = list[k]; if (part[i] !== p) continue; const m = mat[i]; if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) tubeIntake(i, p, dx, dy); continue; } if (stamp[i] === tickStamp) continue; if (temp[i] > 1000 && rnd() < 0.02) { part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i); continue; } const s = netSat(i); if (s <= 0 || rnd() > s) continue; const x = i % W, y = (i / W) | 0; // a sideways screw carries its load along the bottom: grains settle into empty lanes below if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); continue; } if (!flat) { let own = 0, shifted = false; const end = dy < 0 ? (y & ~3) : (y | 3); for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++; const side = ((tick >> 1) + i) & 1 ? 1 : -1; for (let c = 0; c < 2; c++) { const sx = c ? -side : side, xx = x + sx, j = i + sx; if ((xx >> 2) !== (x >> 2) || part[j] !== p || !roomFor(j) || mat[j] !== EMPTY || occ[j] || stamp[j] === tickStamp) continue; let load = 0; for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++; // Include this grain: a strict gap prevents a sideways step being undone next beat. if (own <= load + 1) continue; move(i, j, x, y, xx, y); stats.moved++; shifted = true; break; } if (shifted) continue; } const nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue; const j = i + dj; if (occ[j]) continue; const pj = part[j], tube = pj >= P_TU && pj <= P_TR; if (tube && TDX[pj] === -dx && TDY[pj] === -dy) continue; const t = mat[j]; if (!roomFor(j)) continue; if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue; if (!tube && !pj && t === EMPTY) { // out of the mouth: the screw spits it clear, so the next grain is never blocked by the last launchGrain(i, nx, ny, dy ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08); stats.moved++; continue; } move(i, j, x, y, nx, ny); stats.moved++; if (tube && pj !== p) { // turning a corner: keep going across the new tube's width so every lane fills tubeSpread(j, nx, ny, dx, dy); } } } }