// Machines: the power network, footprints, placing and removing machines. // ---------- Power & machines ---------- function markPow(i, on) { if (on) { if (!isPow[i]) { isPow[i] = 1; pcells.add(i); } } else if (isPow[i]) { isPow[i] = 0; pcells.delete(i); netId[i] = 0; } powerDirty = true; } function partLost(i) { if (chunkWork) chunkEvent('lost', i); else markPow(i, false); } let nextPowerBuild = 0; function rebuildPower() { powerDirty = false; for (const i of pcells) netId[i] = 0; nets = [null]; beltL = []; beltR = []; screwLists = Array.from({ length: IS_SCREW.length }, () => []); const stack = []; for (const s of pcells) { if (netId[s]) continue; const id = nets.length; if (id >= 65535) break; const net = { id, nb: 0, nt: 0, fans: [], prods: [], supply: 0, demand: 0, sat: 0 }; nets.push(net); netId[s] = id; stack.push(s); while (stack.length) { const i = stack.pop(); const m = mat[i], p = part[i]; if (m === BELTL) { net.nb++; beltL.push(i); } else if (m === BELTR) { net.nb++; beltR.push(i); } if (IS_SCREW[p]) { net.nt++; screwLists[p].push(i); } const x = i % W, y = (i / W) | 0; // Corner-touching screws share power, without connecting unrelated diagonal machinery. if (IS_SCREW[p] && SCREW_DX[p] && SCREW_DY[p]) for (let d = -1; d <= 1; d += 2) { const xx = x + d * SCREW_DX[p], yy = y + d * SCREW_DY[p], j = yy * W + xx; if (xx >= 0 && xx < W && yy >= 0 && yy < H && part[j] === p && isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } } let j; if (x > 0) { j = i - 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } } if (x < W - 1) { j = i + 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } } if (i >= W) { j = i - W; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } } if (i < N - W) { j = i + W; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } } } } for (const mm of machines) { if (!mm) continue; mm.net = netId[mm.cell0] || 0; const net = nets[mm.net]; if (!net) continue; if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm); } beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a); for (const p of SCREW_PARTS) { screwLists[p].sort((a, b) => { if (SCREW_DX[p] && SCREW_DY[p]) return ((b % W) - (a % W)) * SCREW_DX[p]; return (SCREW_DY[p] || SCREW_DX[p]) < 0 ? a - b : b - a; }); } } function netSat(i) { if (POWER_FREE) return 1; const n = nets[netId[i]]; return n ? n.sat : 0; } // Move loose grains out of the given cells, to the nearest open cell above // the footprint in the same column (they then fall back on top). Gas just vanishes. function evictCells(cells, topY) { for (const i of cells) { bgm[i] = 0; renderDirtyIdx(i); } for (const i of cells) { const m = mat[i], ph = PH[m]; if (m === EMPTY || ph === PH_SOLID) continue; if (ph === PH_GAS) { mat[i] = EMPTY; life[i] = 0; continue; } const x = i % W; let moved = false; for (let y = topY - 1; y >= Math.max(0, topY - 80); y--) { const j = y * W + x; if (mat[j] === EMPTY && !part[j] && !occ[j] && !mach[j]) { swapCells(i, j); wakeXY(x, y); moved = true; break; } if (PH[mat[j]] === PH_SOLID) break; } if (!moved) { mat[i] = EMPTY; life[i] = 0; flags[i] = 0; } wakeIdx(i); } } function cellBuildable(i) { return PH[mat[i]] !== PH_SOLID && !part[i] && !mach[i] && !occ[i]; } function machineFootprint(type, x, y) { // [cx, cy, kind] where kind is 'hull', 'void' (inside the machine's area but open) or a part code const out = []; if (type === 'wheel') { for (let dy = 0; dy < 12; dy++) for (let dx = 0; dx < 12; dx++) { const ex = dx - 5.5, ey = dy - 5.5; out.push([x + dx, y + dy, ex * ex + ey * ey <= 36.5 ? P_WHEEL : 'void']); } } else if (type === 'fan') { for (let dy = 0; dy < 4; dy++) for (let dx = 0; dx < 4; dx++) out.push([x + dx, y + dy, 'hull']); } else if (type === 'silo') { // roof with a hatch, the body, a cone narrowing to the gate, and two legs; the gaps beside // the cone are left open for (let dy = 0; dy < 24; dy++) for (let dx = 0; dx < 12; dx++) { let k = null; if (dy < 2) k = dx >= 3 && dx <= 8 ? 'void' : 'hull'; else if (dy < 20) k = 'hull'; else if (dx === 0 || dx === 11) k = 'hull'; // legs else if (dy === 20) k = dx >= 1 && dx <= 10 ? 'hull' : null; else if (dy === 21) k = dx >= 2 && dx <= 9 ? 'hull' : null; else k = dx >= 4 && dx <= 7 ? 'void' : dx === 3 || dx === 8 ? 'hull' : null; // the gate if (k) out.push([x + dx, y + dy, k]); } } else if (type === 'dock') { // an open-topped hopper: 2-grain walls and floor around a 20x14 bin for (let dy = 0; dy < 16; dy++) for (let dx = 0; dx < 24; dx++) out.push([x + dx, y + dy, dx < 2 || dx >= 22 || dy >= 14 ? 'hull' : 'void']); } else if (type === 'engine') { for (let dy = 0; dy < 8; dy++) for (let dx = 0; dx < 12; dx++) { if (dy === 7 && dx >= 4 && dx <= 7) out.push([x + dx, y + dy, P_INTAKE]); else out.push([x + dx, y + dy, 'hull']); } } return out; } function canPlaceMachine(type, bx, by) { const P = PIECE_BY_ID[type]; if (bx < 0 || by < 0 || bx + P.bw > BW || by + P.bh > BH) return 'Out of bounds'; for (const [cx, cy] of machineFootprint(type, bx * BS, by * BS)) { const i = cy * W + cx; if (mach[i] || part[i]) return 'Something is already built there'; if (PH[mat[i]] === PH_SOLID) return (flags[i] & F_NAT) ? 'Needs open space; clear the rock first.' : 'Something is already built there'; if (occ[i]) return 'A bar is in the way'; } for (let y = by; y < by + P.bh; y++) for (let x = bx; x < bx + P.bw; x++) if (blockP[y * BW + x]) return 'Something is already built there'; return null; } function addMachine(type, bx, by, dir) { let id = machines.indexOf(null, 1); if (id < 0) id = machines.length; const P = PIECE_BY_ID[type]; const x = bx * BS, y = by * BS; const mm = { id, type, x, y, w: P.w, h: P.h, dir: dir | 0, on: true, out: 0, flow: 0, energy: 0, angle: 0, net: 0, cell0: y * W + x, intake: [] }; machines[id] = mm; const fp = machineFootprint(type, x, y); evictCells(fp.filter(f => f[2] === 'hull').map(([cx, cy]) => cy * W + cx), y); for (const [cx, cy, k] of fp) { const i = cy * W + cx; if (k === 'hull') { mat[i] = type === 'dock' ? DOCKW : HULL; flags[i] = 0; life[i] = 0; temp[i] = AMB[cy]; if (type !== 'dock') markPow(i, true); } else if (k !== 'void') { part[i] = k; markPow(i, true); if (k === P_INTAKE) mm.intake.push(i); } mach[i] = id; wakeXY(cx, cy); } if (type === 'wheel') mm.cell0 = (y + 6) * W + x + 6; return mm; } function removeMachine(id, refund, scrap) { const mm = machines[id]; if (!mm) return; for (const [cx, cy, k] of machineFootprint(mm.type, mm.x, mm.y)) { const i = cy * W + cx; if (mach[i] !== id) continue; mach[i] = 0; markPow(i, false); if (k === 'hull') { if (mat[i] === HULL || mat[i] === DOCKW) { mat[i] = scrap ? SCRCU : EMPTY; flags[i] = 0; } } else if (k !== 'void') part[i] = 0; wakeXY(cx, cy); } machines[id] = null; if (refund) { refundAt(PIECE_BY_ID[mm.type].cost, 1, mm.x + mm.w / 2, mm.y + mm.h / 2); undoNote({ t: 'mach-', type: mm.type, bx: mm.x / BS, by: mm.y / BS, dir: mm.dir, on: mm.on }); } if (scrap) emit({ t: 'warn', x: mm.x + mm.w / 2, y: mm.y, text: `${PIECE_BY_ID[mm.type].name} destroyed` }); }