Sloped belts and filters, laid by a 45° drag (t40)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 23:17:55 -04:00
co-authored by Claude Opus 5.5
parent 24eb9559f8
commit e19cb784d2
26 changed files with 531 additions and 110 deletions
+32 -21
View File
@@ -3,33 +3,42 @@
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;
for (const by of [(y >> 2) - 1, (y >> 2) + 1]) {
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) && slopeSeal(c, bx, by) === i) return 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) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3));
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]) return 'Something is already built there';
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)) {
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.';
}
@@ -41,25 +50,25 @@ function placeBlock(code, v, bx, by, free, extra) {
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) ? wallCells(code, bx, by) : blockCellList(bx, by);
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 (code === BP_BELT) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
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_TUBE) { part[i] = TUBE_PARTS[v & 7]; markPow(i, true); wakeXY(x, y); }
else if (code === BP_FILTER) { part[i] = P_FILTER; 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 (code === BP_FILTER) { blockV[by * BW + bx] = v & 128; filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
if (isFilter(code)) { blockV[by * BW + bx] = v & 128; 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();
@@ -67,9 +76,9 @@ function placeBlock(code, v, bx, by, free, extra) {
rackState.set(b, s);
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open;
}
if (isSlope(code)) {
if (isSlope(code) || isSlopeBelt(code)) {
const i = slopeSeal(code, bx, by);
if (i >= 0 && PH[mat[i]] !== PH_SOLID) { evictCells([i], by * BS); setCell(i, WALL_OF[v], AMB[(i / W) | 0], 0); }
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 });
@@ -84,14 +93,14 @@ function blockIntegrity(b) {
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 (code === BP_BELT) { if (m === BELTL || m === BELTR) 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_TUBE) { if (IS_TUBE[p]) n++; }
else if (code === BP_FILTER) { if (p === P_FILTER) n++; }
else if (isFilter(code)) { if (p === P_FILTER) n++; }
else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; }
}
void v;
@@ -109,7 +118,7 @@ function removeWallCell(i, wall) {
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: code === BP_FILTER ? filterInfo(b) : undefined });
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;
@@ -125,7 +134,7 @@ function removeBlock(bx, by) {
} 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 (code === BP_BELT || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) {
} 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++; }
@@ -137,7 +146,7 @@ function removeBlock(bx, by) {
}
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 (code === BP_FILTER) { filtersDirty = true; filterCfg.delete(b); }
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);
@@ -146,17 +155,19 @@ function removeBlock(bx, by) {
}
blockP[b] = 0; blockV[b] = 0;
const cleared = blockCellList(bx, by);
if (isSlope(code)) {
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] === WALL_OF[v] && !(flags[i] & F_NAT)) setCell(i, EMPTY, temp[i], 0);
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) setCell(i, WALL_OF[blockV[owner]], AMB[(i / W) | 0], 0);
if (owner >= 0 && mat[i] === EMPTY && !part[i]) {
setSlopeSeal(i, blockP[owner], blockV[owner]);
}
}
if (isPipeCode(code)) {
pipesDirty = true;
@@ -186,7 +197,7 @@ function blockInReach(bx, by) { return inReach(bx * BS + BS / 2, by * BS + BS /
// 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 (code === BP_FILTER) return (blockV[b] & 128) === (v & 128) && sameFilter(filterInfo(b), cleanFilterCfg(extra));
if (isFilter(code)) return (blockV[b] & 128) === (v & 128) && 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
@@ -197,7 +208,7 @@ function placeBlocks(code, v, list, extra, replace) {
const b = by * BW + bx;
if (replace && blockP[b]) {
if (blockIs(b, code, v, extra)) continue;
if (blockP[b] === BP_FILTER && code === BP_FILTER) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = v & 128; setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it
if (blockP[b] === code && isFilter(code)) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = v & 128; 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);
}
+3 -3
View File
@@ -7,7 +7,7 @@ function copyRegion(x0, y0, x1, y1) {
const blocks = [], ms = [], ols = [];
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
const b = by * BW + bx;
if (blockP[b] && blockIntegrity(b) >= 8) blocks.push(blockP[b] === BP_FILTER ? [bx - bx0, by - by0, blockP[b], blockV[b], filterInfo(b)] : [bx - bx0, by - by0, blockP[b], blockV[b]]);
if (blockP[b] && blockIntegrity(b) >= 8) blocks.push(isFilter(blockP[b]) ? [bx - bx0, by - by0, blockP[b], blockV[b], filterInfo(b)] : [bx - bx0, by - by0, blockP[b], blockV[b]]);
}
for (const mm of machines) {
if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
@@ -23,11 +23,11 @@ function copyRegion(x0, y0, x1, y1) {
}
function flipBlueprint(bp) {
const bw = bp.bw;
const flipV = (code, v) => code === BP_BELT ? 1 - v : code === BP_FILTER ? v ^ 128 : code === BP_TUBE ? [0, 3, 2, 1, 7, 6, 5, 4][v & 7]
const flipV = (code, v) => isBelt(code) ? 1 - v : isFilter(code) ? v ^ 128 : code === BP_TUBE ? [0, 3, 2, 1, 7, 6, 5, 4][v & 7]
: code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
return {
v: 2, bw, bh: bp.bh, name: bp.name,
blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v), e] : [bw - 1 - x, y, isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v)]),
blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, isSlopeBelt(c) ? c + (c & 1 ? 1 : -1) : isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v), e] : [bw - 1 - x, y, isSlopeBelt(c) ? c + (c & 1 ? 1 : -1) : isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v)]),
machines: bp.machines.map(([t, x, y, d]) => {
const P = PIECE_BY_ID[t];
let nd = d;
+68 -14
View File
@@ -4,7 +4,24 @@
// clear). If a powder grain is blocked, the load above it stands still and the belt slips
// underneath. A filter's own material drops through it instead, unless it is already full.
const CARRY_DEPTH = 96;
// Find the next carrying face at the same height or one grain above/below it.
function conveyorStep(x, y, d) {
const nx = x + d;
if (nx < 0 || nx >= W) return 0;
if ((x >> 2) === (nx >> 2)) return beltGrade(blockP[(y >> 2) * BW + (x >> 2)]) * d;
for (let dy = -1; dy <= 1; dy++) {
const yy = y + dy;
if (yy < 0 || yy >= H) continue;
const b = (yy >> 2) * BW + (nx >> 2), c = blockP[b], i = yy * W + nx;
if ((isBelt(c) || isFilter(c)) && beltFaceY(c, nx >> 2, yy >> 2, nx) === yy &&
(mat[i] === BELTL || mat[i] === BELTR || part[i] === P_FILTER)) return dy;
}
const b = (y >> 2) * BW + (x >> 2);
return beltGrade(blockP[b]) * d;
}
function carryColumn(a, d, pass) {
if (a < 0 || mat[a] === EMPTY) return;
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d);
let blocked = false;
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
if (a < 0) return;
@@ -20,20 +37,30 @@ function carryColumn(a, d, pass) {
if (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d;
if (nx < 0 || nx >= W) return;
const j = a + d, tj = mat[j];
const j = a + d + step * W;
if (j < 0 || j >= N) return;
const tj = mat[j];
// grains on a wet belt push through the water rather than stopping at it
const wet = PH[tj] === PH_LIQUID && !occ[j] && stamp[j] !== tickStamp && (!part[j] || part[j] === P_FILTER || part[j] === P_GOPEN);
if (!wet && !enter(m, PH_POWDER, j, d, 0)) {
if (!wet && !enter(m, PH_POWDER, j, d, step)) {
if (!pass) return;
blocked = true; continue;
}
const y = (a / W) | 0, bj = j + W;
// carried off the end with nothing under it: it leaves at belt speed and falls
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y, d * 0.5, 0);
else move(a, j, x, y, nx, y);
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y + step, d * 0.5, step * 0.5);
else move(a, j, x, y, nx, y + step);
stats.moved++;
}
}
// The running face grips its whole supported load, including on the ticks between steps.
function holdSlopeColumn(a, pass) {
for (let depth = 0; depth < CARRY_DEPTH && a >= 0; depth++, a -= W) {
const m = mat[a];
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return;
if (PH[m] === PH_POWDER && !occ[a] && !(pass && pass[m])) stamp[a] = tickStamp;
}
}
// Belts are perforated: liquid resting on one drips through to whatever open space is under it.
function beltDrain(b) {
const a = b - W, x = b % W;
@@ -53,23 +80,48 @@ function launchGrain(i, x, y, vx, vy) {
let filterR = [], filterL = [], filtersDirty = true;
function rebuildFilters() {
filtersDirty = false; filterR = []; filterL = [];
for (let b = 0; b < NB; b++) if (blockP[b] === BP_FILTER) (blockV[b] & 128 ? filterR : filterL).push(b);
for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) (blockV[b] & 128 ? filterR : filterL).push(b);
// front of the line first, so a full run moves as one
filterR.sort((a, c) => (c % BW) - (a % BW)); filterL.sort((a, c) => (a % BW) - (c % BW));
}
function filterCarry(b, d) {
if (blockP[b] !== BP_FILTER) return;
const bx = b % BW, y = ((b / BW) | 0) * BS;
function filterCarry(b, d, carrying = true) {
if (!isFilter(blockP[b])) return;
const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
if (y < 1) return;
if (!carrying && !isSlopeBelt(code)) return;
const c = filterCfg.get(b), pass = c ? c.pass : null;
for (let k = 0; k < BS; k++) {
const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = y * W + x;
if (part[top] === P_FILTER) carryColumn(top - W, d, pass);
const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = beltFaceY(code, bx, by, x) * W + x;
if (part[top] === P_FILTER) {
if (carrying) carryColumn(top - W, d, pass);
if (isSlopeBelt(code)) holdSlopeColumn(top - W, pass);
}
}
}
// Belts and filters share one front-first order, including their flat/slope joins.
function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
let k = 0, f = 0;
while (k < belts.length || f < filters.length) {
const fx = f < filters.length ? (filters[f] % BW) * BS + (d > 0 ? BS - 1 : 0) : 0;
if (f < filters.length && (k >= belts.length || (fx - belts[k] % W) * d > 0)) { filterCarry(filters[f++], d, carrying); continue; }
const b = belts[k++];
if (mat[b] !== bm || b < W) continue;
const x = b % W, y = (b / W) | 0, code = blockP[(y >> 2) * BW + (x >> 2)];
if (!carrying && !isSlopeBelt(code)) continue;
if (isSlopeBelt(code) && beltFaceY(code, x >> 2, y >> 2, x) !== y) continue;
if (slopeSealOwner(b) >= 0) continue;
const above = mat[b - W];
if (above === BELTL || above === BELTR || netSat(b) <= 0) continue;
if (carrying) {
if (PH[above] === PH_LIQUID) beltDrain(b);
carryColumn(b - W, d, null);
}
if (isSlopeBelt(code)) holdSlopeColumn(b - W, null);
}
}
// Change what a filter does; grains inside it that no longer pass are pushed out on top.
function setFilter(b, cfg) {
if (blockP[b] !== BP_FILTER) return false;
if (!isFilter(blockP[b])) return false;
const rec = filterRec(cfg);
filterCfg.set(b, rec);
const bx = b % BW, by = (b / BW) | 0;
@@ -79,11 +131,13 @@ function setFilter(b, cfg) {
}
// The run of filters in a row that touch block b and do the same thing it does
function filterRun(b) {
if (blockP[b] !== BP_FILTER) return [];
if (!isFilter(blockP[b])) return [];
const f = filterInfo(b), dir = blockV[b] & 128, by = (b / BW) | 0, out = [b];
for (const s of [-1, 1]) for (let bx = (b % BW) + s; bx >= 0 && bx < BW; bx += s) {
const c = by * BW + bx;
if (blockP[c] !== BP_FILTER || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break;
const yy = by + beltGrade(blockP[b]) * (bx - (b % BW));
if (yy < 0 || yy >= BH) break;
const c = yy * BW + bx;
if (blockP[c] !== blockP[b] || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break;
out.push(c);
}
return out.sort((a, c) => a - c);
+38 -6
View File
@@ -59,9 +59,10 @@ function entTubeFree(x, y, tubeOnly = false) {
function entTubeMove(e, x, y, tubeOnly = false, mouth = false) {
if (!entTubeFree(x, y, tubeOnly)) return false;
if (mouth) {
// Lift clear of the receiving belt before sliding over it; check both legs before moving.
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entTubeFree(e.ix, yy)) return false;
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, y)) return false;
// Go around the receiving corner, checking both legs before moving.
const acrossY = y > e.iy ? e.iy : y, downX = y > e.iy ? x : e.ix;
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entTubeFree(downX, yy)) return false;
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, acrossY)) return false;
} else for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
@@ -71,7 +72,7 @@ function entTubeEdge(e, dx, dy, k) {
const x = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), y = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
}
function entTubeEnter(e, dx, dy) {
function entTubeEnter(e, dx, dy, mouth = false) {
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entTubeEdge(e, dx, dy, k);
if (i < 0) continue;
@@ -79,7 +80,7 @@ function entTubeEnter(e, dx, dy) {
const p = part[i]; if (!IS_TUBE[p] || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
const x = (xx & ~3), y = entTubeLane(yy, e.iy);
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true, mouth)) return true;
}
return false;
}
@@ -129,6 +130,37 @@ function entDiagonalTubeTick(e, p) {
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
if (entTubeMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; }
}
// Bars stay level; their highest contact follows the face one grain per belt beat.
function entSlopeBeltTick(e) {
const bottom = e.iy + BAR_H;
if (bottom >= H) return false;
let d = 0, slope = false;
for (let x = e.ix; x < e.ix + BAR_W; x++) {
const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
if (isSlopeBelt(c)) slope = true;
}
}
if (!d) return false;
let face = H;
for (let x = e.ix + d; x < e.ix + d + BAR_W; x++) for (let dy = -1; dy <= 1; dy++) {
const y = bottom + dy;
if (x < 0 || x >= W || y < 0 || y >= H) continue;
const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
if (isSlopeBelt(c)) slope = true;
face = Math.min(face, y);
}
if (!slope) return false;
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
if (tick & 1) return true;
const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
else if (!entTubeEnter(e, d, 0)) entTubeEnter(e, d, Math.sign(y - e.iy), true);
return true;
}
function entTick() {
for (let k = ents.length - 1; k >= 0; k--) {
const e = ents[k];
@@ -137,7 +169,7 @@ function entTick() {
let ix = e.ix; const iy = e.iy;
const tube = entTubeAt(ix, iy);
if (tube) entTubeTick(e, tube);
else {
else if (!entSlopeBeltTick(e)) {
// what is underneath?
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
if (!support) {
+4 -18
View File
@@ -87,24 +87,10 @@ function machinesTick() {
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);
}
// Half a grain per tick; sloped faces grip their load on the intervening ticks too.
if (filtersDirty) rebuildFilters();
carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
// 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 = 1; p < IS_TUBE.length; p++) {
+1 -1
View File
@@ -44,7 +44,7 @@ function rebuildPower() {
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 - b); beltR.sort((a, b) => b - a);
beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a);
for (const p of TUBE_PARTS) {
tubeLists[p].sort((a, b) => {
if (TDX[p] && TDY[p]) return ((b % W) - (a % W)) * TDX[p];
+23 -3
View File
@@ -6,9 +6,22 @@
const BS = 4;
const BP_WALL = 1, BP_BELT = 2, BP_TUBE = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13, BP_RACK = 14;
const BP_SLOPE_BL = 15, BP_SLOPE_BR = 16, BP_SLOPE_TL = 17, BP_SLOPE_TR = 18;
// The shape is independent of the direction: reversing a belt never flips its face.
const BP_BELT_UP = 19, BP_BELT_DOWN = 20, BP_FILTER_UP = 21, BP_FILTER_DOWN = 22;
function isSlopeBelt(c) { return c >= BP_BELT_UP && c <= BP_FILTER_DOWN; }
function isFilter(c) { return c === BP_FILTER || c === BP_FILTER_UP || c === BP_FILTER_DOWN; }
function isBelt(c) { return c === BP_BELT || c === BP_BELT_UP || c === BP_BELT_DOWN; }
function beltGrade(c) { return c === BP_BELT_UP || c === BP_FILTER_UP ? -1 : c === BP_BELT_DOWN || c === BP_FILTER_DOWN ? 1 : 0; }
function slopeShape(c) { return isSlopeBelt(c) ? (beltGrade(c) < 0 ? BP_SLOPE_BR : BP_SLOPE_BL) : c; }
function beltFaceY(c, bx, by, x) { return by * BS + (beltGrade(c) < 0 ? 3 - (x - bx * BS) : beltGrade(c) > 0 ? x - bx * BS : 0); }
function beltDirection(c, v) {
const d = isFilter(c) ? (v & 128 ? 1 : -1) : (v ? 1 : -1), g = beltGrade(c);
return g ? (g * d < 0 ? 'up-' : 'down-') + (d > 0 ? 'right' : 'left') : d > 0 ? 'right' : 'left';
}
function isSlope(c) { return c >= BP_SLOPE_BL && c <= BP_SLOPE_TR; }
function isWall(c) { return c === BP_WALL || isSlope(c); }
function slopeCell(c, x, y) {
c = slopeShape(c);
if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x;
if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y;
return y >= x;
@@ -30,7 +43,12 @@ function filterRec(c) {
for (let m = 1; m < NMAT; m++) if (filterable(m)) pass[m] = (k.m.includes(m) ? 1 : 0) ^ k.inv;
return { m: k.m, inv: k.inv, pass };
}
function filterPassAt(m, j) { const c = filterCfg.get((((j / W) | 0) >> 2) * BW + ((j % W) >> 2)); return c ? c.pass[m] : 0; }
function filterPassAt(m, j) {
let b = (((j / W) | 0) >> 2) * BW + ((j % W) >> 2);
if (!isFilter(blockP[b])) { const owner = slopeSealOwner(j); if (owner >= 0) b = owner; }
const c = filterCfg.get(b);
return c ? c.pass[m] : 0;
}
function filterInfo(b) { const c = filterCfg.get(b); return c ? { m: c.m.slice(), inv: c.inv } : { m: [], inv: 0 }; }
function sameFilter(a, b) { return a.inv === b.inv && a.m.length === b.m.length && a.m.every((m, k) => m === b.m[k]); }
// loose material -> the solid it becomes in a wall block
@@ -52,11 +70,11 @@ const PIECES = [
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it.' },
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally to make a sloped belt that carries up or down.' },
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
desc: 'A turning screw that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single tube. Join either end to tubes or belts. Copper softens around 1000°C.' },
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through.' },
desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally to make a sloped filter that carries up or down.' },
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
@@ -97,6 +115,8 @@ const PIECES = [
const PIECE_BY_ID = {}, PIECE_BY_CODE = [];
PIECES.forEach((p, i) => { p.index = i; PIECE_BY_ID[p.id] = p; if (p.code) PIECE_BY_CODE[p.code] = p; if (p.kind === 'machine') { p.w = p.bw * BS; p.h = p.bh * BS; } });
for (let c = BP_SLOPE_BL; c <= BP_SLOPE_TR; c++) PIECE_BY_CODE[c] = PIECE_BY_ID.wall;
for (const c of [BP_BELT_UP, BP_BELT_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.belt;
for (const c of [BP_FILTER_UP, BP_FILTER_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.filter;
// power per grain of belt / tube (a block is 16 grains)
const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6;
const WHEEL_K = 0.32, ENGINE_K = 40;
+2 -2
View File
@@ -55,8 +55,8 @@ function deserializeWorld(meta, buffers) {
}
// pipes used to be solid copper blocks; now they are thin and let things past
for (let b = 0; b < NB; b++) if (blockP[b] === BP_PIPE) for (const i of blockCellList(b % BW, (b / BW) | 0)) if (mat[i] === PIPE) { mat[i] = EMPTY; part[i] = P_PIPE; }
for (const [b, m, inv] of meta.filters || []) if (blockP[b] === BP_FILTER) filterCfg.set(b, filterRec({ m, inv }));
for (let b = 0; b < NB; b++) if (blockP[b] === BP_FILTER) {
for (const [b, m, inv] of meta.filters || []) if (isFilter(blockP[b])) filterCfg.set(b, filterRec({ m, inv }));
for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) {
// the first filters kept one material in their block variant
if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 }));
blockV[b] &= 128;
+4 -4
View File
@@ -22,8 +22,8 @@ function undoLast() {
for (let k = rec.ops.length - 1; k >= 0; k--) {
const o = rec.ops[k];
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
else if (o.t === 'block-') { const r = placeBlock(o.code, o.code === BP_FILTER ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (blockP[b] === BP_FILTER) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
else if (o.t === 'block-') { const r = placeBlock(o.code, isFilter(o.code) ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (isFilter(blockP[b])) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
else if (o.t === 'mach+') { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm && siloFull(mm)) fail('empty the silo first'); else if (mm) removeMachine(mm.id, true, false); }
else if (o.t === 'mach-') { const r = placeMachineAt(o.type, o.dir, o.bx, o.by); if (r) fail(r); else { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm) mm.on = o.on !== false; } }
else if (o.t === 'mold+') { const mo = molds.find(m => m.kind === o.kind && m.x === o.bx * BS && m.y === o.by * BS); if (mo) removeMold(mo); }
@@ -56,8 +56,8 @@ function ghostTick0() {
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
const [b, [code, v, e]] = nx.value;
const bx = b % BW, by = (b / BW) | 0;
if (blockP[b] === code && blockV[b] === (code === BP_FILTER ? v & 128 : v)) {
if (code === BP_FILTER && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
}
if (!blockInReach(bx, by)) continue;