// Building blocks: placing, integrity, removing, lines and rectangles of blocks. // ---------- Blocks ---------- function blockOf(x, y) { return { bx: Math.floor(x / BS), by: Math.floor(y / BS) }; } function blockCellList(bx, by) { const out = []; for (let y = by * BS; y < by * BS + BS; y++) for (let x = bx * BS; x < bx * BS + BS; x++) out.push(y * W + x); return out; } function pieceCost(code, v) { return isWall(code) ? wallCost(v, isSlope(code) ? 8 : BS * BS) : PIECE_BY_CODE[code].cost; } function setSlopeSeal(i, code, v) { if (isFilter(code)) { part[i] = P_FILTER; wakeIdx(i); } else { setCell(i, isBelt(code) ? (v ? BELTR : BELTL) : WALL_OF[v], AMB[(i / W) | 0], 0); if (isBelt(code)) markPow(i, true); } } // One extra cell on the solid side seals diagonal crossings between successive triangles. function slopeSeal(code, bx, by) { code = slopeShape(code); const x = bx * BS + (code === BP_SLOPE_BR || code === BP_SLOPE_TR ? 0 : 3); const y = by * BS + (code >= BP_SLOPE_TL ? -1 : 4); return y < 0 || y >= H ? -1 : y * W + x; } function slopeSealOwner(i) { const x = i % W, y = (i / W) | 0, bx = x >> 2; if (((x & 3) !== 0 && (x & 3) !== 3) || ((y & 3) !== 0 && (y & 3) !== 3)) return -1; for (let by = (y >> 2) - 1; by <= (y >> 2) + 1; by += 2) { if (by < 0 || by >= BH) continue; const b = by * BW + bx, c = blockP[b]; if ((isSlope(c) || isSlopeBelt(c)) && slopeSeal(c, bx, by) === i) return b; } return -1; } function wallCells(code, bx, by) { return blockCellList(bx, by).filter(i => !(isSlope(code) || isSlopeBelt(code)) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3)); } function canPlaceBlock(bx, by, code = BP_WALL) { if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds'; if (blockP[by * BW + bx]) return 'Something is already built there'; for (const i of blockCellList(bx, by)) { if (mach[i] || (part[i] && slopeSealOwner(i) < 0)) return 'Something is already built there'; if (PH[mat[i]] === PH_SOLID && ((flags[i] & F_NAT) || slopeSealOwner(i) < 0)) return 'Needs open space; clear the rock first.'; if (occ[i]) return 'A bar is in the way'; } if (isSlope(code) || isSlopeBelt(code)) { const i = slopeSeal(code, bx, by); if (i >= 0 && PH[mat[i]] !== PH_SOLID && (mach[i] || part[i] || occ[i])) return 'The slope corner is blocked.'; } return ''; } // v: wall material (loose id) for walls, direction for belts and screws function placeBlock(code, v, bx, by, free, extra) { const err = canPlaceBlock(bx, by, code); if (err) return err; if (isWall(code) && !WALL_OF[v]) return 'Pick a wall material with R'; const cost = pieceCost(code, v); if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); } const cells = isSlope(code) || isSlopeBelt(code) ? wallCells(code, bx, by) : blockCellList(bx, by); if (code !== BP_PIPE) evictCells(cells, by * BS); // a pipe is thin: what is there stays and flows past it for (const i of cells) { const y = (i / W) | 0, x = i % W; if (isWall(code)) setCell(i, WALL_OF[v], AMB[y], 0); else if (isBelt(code)) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); } else if (code === BP_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); } else if (code === BP_RACK) setCell(i, RACKB, AMB[y], 0); else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB, AMB[y], 0); else if (code === BP_GATE) setCell(i, GATE, AMB[y], 0); else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); } else if (code === BP_SCREW) { part[i] = SCREW_PARTS[v & 7]; markPow(i, true); wakeXY(x, y); } else if (isFilter(code)) { part[i] = P_FILTER; wakeXY(x, y); } else if (code === BP_PIPE) { part[i] = P_PIPE; wakeXY(x, y); } else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0); } if (isPipeCode(code)) pipesDirty = true; blockP[by * BW + bx] = code; blockV[by * BW + bx] = v; if (isFilter(code)) { blockV[by * BW + bx] = filterVariant(code, v); filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; } if (isProcCode(code)) procDirty = true; if (code === BP_RACK) { const b = by * BW + bx, s = newRack(); s.open = !!(rackState.get(b - BW)?.open || rackState.get(b + BW)?.open); rackState.set(b, s); for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open; } if (isSlope(code) || isSlopeBelt(code)) { const i = slopeSeal(code, bx, by); if (i >= 0 && PH[mat[i]] !== PH_SOLID && !part[i]) { evictCells([i], by * BS); setSlopeSeal(i, code, v); } } stats.placed++; undoNote({ t: 'block+', bx, by, code }); return ''; } // How many of a block's cells still look like the piece the registry says is there function blockIntegrity(b) { const code = blockP[b], v = blockV[b]; if (!code) return 0; const bx = b % BW, by = (b / BW) | 0; let n = 0; for (const i of blockCellList(bx, by)) { const m = mat[i], p = part[i]; if (isWall(code) && (!isSlope(code) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3))) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; } else if (isBelt(code)) { if (m === BELTL || m === BELTR) n++; } else if (code === BP_SHAFT) { if (m === SHAFT) n++; } else if (code === BP_RACK) { if (m === RACKB) n++; } else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB)) n++; } else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; } else if (code === BP_GRATE) { if (p === P_GRATE) n++; } else if (code === BP_SCREW) { if (IS_SCREW[p]) n++; } else if (isFilter(code)) { if (p === P_FILTER) n++; } else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; } } void v; return n; } // One cell of a wall or mold being taken back: metal is counted toward bars (returns 1), other // walls give their rubble back. function removeWallCell(i, wall) { const m = mat[i]; if (BAR_OF[m] && PH[wall] === PH_ITEM) { setCell(i, EMPTY, temp[i], 0); return 1; } if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; } // bag full: it crumbles in place else setCell(i, EMPTY, temp[i], 0); return 0; } function removeBlock(bx, by) { const b = by * BW + bx, code = blockP[b], v = blockV[b]; if (!code) return 0; undoNote({ t: 'block-', bx, by, code, v, extra: isFilter(code) ? filterInfo(b) : undefined }); // a pipe taken out spills its share of what the line holds const spill = isPipeCode(code) ? pipeTakeShare(b) : null; let n = 0, metal = 0; for (const i of blockCellList(bx, by)) { const m = mat[i], p = part[i]; if (isWall(code)) { if (isSlope(code) && !slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3)) continue; if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m] && m !== SPRING) { if (isSlope(code)) setCell(i, EMPTY, temp[i], 0); else metal += removeWallCell(i, v); n++; } } else if (isPipeCode(code)) { if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; } else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); n++; } } else if (isBelt(code) || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) { if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; } if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); } } else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; } } if (isSlope(code)) { const cells = Math.floor(n * 8 / 10), refund = PH[v] === PH_ITEM ? Math.floor(cells / GRAINS_PER_BAR) : cells; giveOrDrop(v, refund, bx * BS + 2, by * BS + 2); } if (code === BP_WALL && metal) giveOrDrop(BAR_OF[WALL_OF[v]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), bx * BS + 2, by * BS + 2); else if (!isWall(code) && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2); if (isFilter(code)) { filtersDirty = true; filterCfg.delete(b); } if (isProcCode(code)) { procDirty = true; procState.delete(b); } if (code === BP_RACK) { const s = rackState.get(b); rackState.delete(b); if (s) for (const [m, count] of s.q) giveOrDrop(m, count, bot.x, bot.y); } blockP[b] = 0; blockV[b] = 0; const cleared = blockCellList(bx, by); if (isSlope(code) || isSlopeBelt(code)) { const i = slopeSeal(code, bx, by); if (i >= 0) { const nb = (((i / W) | 0) >> 2) * BW + ((i % W) >> 2), nc = blockP[nb]; if ((!nc || (isSlope(nc) && !slopeCell(nc, (i % W) & 3, ((i / W) | 0) & 3))) && (mat[i] === (isBelt(code) ? (v ? BELTR : BELTL) : WALL_OF[v]) || (isFilter(code) && part[i] === P_FILTER)) && !(flags[i] & F_NAT)) { if (isFilter(code)) { part[i] = 0; wakeIdx(i); } else { setCell(i, EMPTY, temp[i], 0); if (isBelt(code)) markPow(i, false); } } cleared.push(i); } } for (const i of cleared) { const owner = slopeSealOwner(i); if (owner >= 0 && mat[i] === EMPTY && !part[i]) { setSlopeSeal(i, blockP[owner], blockV[owner]); } } if (isPipeCode(code)) { pipesDirty = true; if (spill) { let k = spill.n; for (let y = by * BS + BS - 1; y >= by * BS && k > 0; y--) for (let x = bx * BS; x < bx * BS + BS && k > 0; x++) { const i = y * W + x; if (mat[i] === EMPTY) { setCell(i, spill.m, spill.T, 0); k--; } } } } return n; } // Wall drags near 45° snap to a diagonal; other lines follow the dominant axis. function diagonalDrag(dx, dy) { return dx !== 0 && dy !== 0 && Math.min(Math.abs(dx), Math.abs(dy)) >= Math.max(Math.abs(dx), Math.abs(dy)) * 0.7; } function blockLine(bx0, by0, bx1, by1, diagonal = false) { const out = []; const dx = bx1 - bx0, dy = by1 - by0; if (diagonal && diagonalDrag(dx, dy)) { for (let k = 0; k <= Math.max(Math.abs(dx), Math.abs(dy)); k++) out.push([bx0 + k * Math.sign(dx), by0 + k * Math.sign(dy)]); return out; } if (Math.abs(bx1 - bx0) >= Math.abs(by1 - by0)) { const s = bx1 >= bx0 ? 1 : -1; for (let x = bx0; ; x += s) { out.push([x, by0]); if (x === bx1) break; } } else { const s = by1 >= by0 ? 1 : -1; for (let y = by0; ; y += s) { out.push([bx0, y]); if (y === by1) break; } } return out; } function blockRect(bx0, by0, bx1, by1) { const out = []; for (let y = Math.min(by0, by1); y <= Math.max(by0, by1); y++) for (let x = Math.min(bx0, bx1); x <= Math.max(bx0, bx1); x++) out.push([x, y]); return out; } function blockInReach(bx, by) { return inReach(bx * BS + BS / 2, by * BS + BS / 2); } // Is block b already exactly this piece (same kind, variant and, for a filter, settings)? function blockIs(b, code, v, extra) { if (blockP[b] !== code) return false; if (isFilter(code)) return (blockV[b] & 128) === filterVariant(code, v) && sameFilter(filterInfo(b), cleanFilterCfg(extra)); return blockV[b] === v; } // replace: a block already built in the way is taken down (and refunded) to make room function placeBlocks(code, v, list, extra, replace) { let n = 0, last = ''; for (const [bx, by] of list) { if (!blockInReach(bx, by)) { last = 'Out of reach'; continue; } const b = by * BW + bx; if (replace && blockP[b]) { if (blockIs(b, code, v, extra)) continue; if (blockP[b] === code && isFilter(code)) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = filterVariant(code, v); setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it if (!canAfford(pieceCost(code, v), 1)) { last = needText(pieceCost(code, v), 1); break; } removeBlock(bx, by); } const r = placeBlock(code, v, bx, by, false, extra); if (r) { last = r; if (r.startsWith('Not enough') || r.startsWith('Pick')) break; } else n++; } return { n, status: n ? '' : last }; }