214 lines
10 KiB
JavaScript
214 lines
10 KiB
JavaScript
// How belts, filters and screw conveyors move what is on or in them.
|
|
// What rests on a belt or filter rides along: the whole stack standing on the carrying surface
|
|
// steps one cell toward d together (belts run front of the line first, so the way ahead is
|
|
// 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), carrier = (sy >> 2) * BW + ((a % W) >> 2);
|
|
let blocked = false;
|
|
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
|
|
if (a < 0) return;
|
|
const m = mat[a];
|
|
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
|
|
if (PH[m] !== PH_POWDER || stamp[a] === tickStamp || occ[a]) continue;
|
|
if (pass && pass[m]) {
|
|
if (enter(m, PH_POWDER, a + W, 0, 1)) continue; // falls through on its own
|
|
// the rollers shake the load: what the filter passes works its way down through the rest
|
|
const u = mat[a + W];
|
|
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); continue; }
|
|
}
|
|
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 + 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, 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 + step, d * 0.5, step * 0.5);
|
|
else move(a, j, x, y, nx, y + step);
|
|
stats.moved++;
|
|
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
|
|
}
|
|
}
|
|
// 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;
|
|
let d = b;
|
|
for (let k = 0; k < 16 && d < N; k++) { const m = mat[d]; if (m !== BELTL && m !== BELTR) break; d += W; }
|
|
if (d >= N || mat[d] === BELTL || mat[d] === BELTR) return;
|
|
if (!passable(mat[a], PH_LIQUID, d, 0, 1)) return;
|
|
move(a, d, x, (a / W) | 0, x, (d / W) | 0);
|
|
}
|
|
// Take a grain out of cell i and send it flying from (x, y): what leaves a belt or a screw mouth
|
|
function launchGrain(i, x, y, vx, vy) {
|
|
const m = mat[i], T = temp[i];
|
|
mat[i] = EMPTY; flags[i] = 0; life[i] = 0; stamp[i] = tickStamp;
|
|
wakeXY(i % W, (i / W) | 0);
|
|
flyers.push({ x: x + 0.5, y: y + 0.5, vx, vy, age: 0, m, T, free: true, run: 0 });
|
|
}
|
|
let filterR = [], filterL = [], filtersDirty = true;
|
|
function rebuildFilters() {
|
|
filtersDirty = false; filterR = []; filterL = [];
|
|
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, 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 = beltFaceY(code, bx, by, x) * W + x;
|
|
if (part[top] === P_FILTER) {
|
|
if (isSlopeBelt(code)) screenNudge(top, d, pass);
|
|
else if (carrying) carryColumn(top - W, d, 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);
|
|
// uphill, the cleats do the carrying
|
|
if (!isSlopeBelt(code) || !slopeUphill(code, blockV[(y >> 2) * BW + (x >> 2)])) carryColumn(b - W, d, null);
|
|
}
|
|
}
|
|
}
|
|
// Change what a filter does; grains inside it that no longer pass are pushed out on top.
|
|
function setFilter(b, cfg) {
|
|
renderDirtyXY((b % BW) * BS, ((b / BW) | 0) * BS);
|
|
if (!isFilter(blockP[b])) return false;
|
|
const rec = filterRec(cfg);
|
|
filterCfg.set(b, rec);
|
|
const bx = b % BW, by = (b / BW) | 0;
|
|
const cells = blockCellList(bx, by).filter(i => PH[mat[i]] === PH_POWDER && !rec.pass[mat[i]]);
|
|
for (let y = by * BS + BS - 1; y >= by * BS; y--) for (const i of cells) if (((i / W) | 0) === y) evictCells([i], by * BS);
|
|
return true;
|
|
}
|
|
// The run of filters in a row that touch block b and do the same thing it does
|
|
function filterRun(b) {
|
|
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 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);
|
|
}
|
|
|
|
// A cell a grain can be pushed into: empty, or liquid or gas that simply trades places with it
|
|
function roomFor(j) { const t = mat[j]; return t === EMPTY || PH[t] === PH_GAS || PH[t] === PH_LIQUID; }
|
|
// Cross one screw block, far lanes first; corners retain their liquid/gas swaps.
|
|
function screwSpread(c, x, y, sx, sy, intake = false) {
|
|
const p = part[c], bx = x >> 2, by = y >> 2;
|
|
for (;;) {
|
|
const nx = x + sx, ny = y + sy;
|
|
if (nx < 0 || ny < 0 || nx >= W || ny >= H || (nx >> 2) !== bx || (ny >> 2) !== by) break;
|
|
const j = ny * W + nx;
|
|
if (part[j] !== p || occ[j] || (intake ? mat[j] !== EMPTY || stamp[j] === tickStamp : !roomFor(j))) break;
|
|
move(c, j, x, y, nx, ny); c = j; x = nx; y = ny;
|
|
}
|
|
return c;
|
|
}
|
|
// An empty screw cell draws in loose material from behind it or from its sides
|
|
// (not from the cell it pushes into), so a screw end sitting in a pile feeds itself.
|
|
function screwIntake(i, p, dx, dy) {
|
|
if (occ[i]) return;
|
|
const x = i % W, y = (i / W) | 0;
|
|
const start = (tick + i) % 3;
|
|
for (let c = 0; c < 3; c++) {
|
|
const k = (start + c) % 3, diagonal = dx && dy;
|
|
const ox = diagonal ? (k === 1 ? 0 : -dx) : k === 0 ? -dx : k === 1 ? dy : -dy;
|
|
const oy = diagonal ? (k === 0 ? 0 : -dy) : k === 0 ? -dy : k === 1 ? dx : -dx;
|
|
const nx = x + ox, ny = y + oy;
|
|
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
|
if (ox && oy && PH[mat[y * W + nx]] === PH_SOLID && PH[mat[ny * W + x]] === PH_SOLID) continue;
|
|
const j = ny * W + nx, m = mat[j];
|
|
if (m === EMPTY || occ[j] || stamp[j] === tickStamp) continue;
|
|
if (PH[m] !== PH_POWDER) continue; // the screw only takes loose material; liquids just flow through
|
|
if (stamp[i] === tickStamp) return;
|
|
const pj = part[j];
|
|
if (IS_SCREW[pj]) continue; // other screw cells move their own contents
|
|
if (pj === P_GRATE || pj === P_FILTER) continue;
|
|
move(j, i, nx, ny, x, y);
|
|
if (diagonal) screwSpread(i, x, y, 0, (y & 3) < 2 ? 1 : -1, true);
|
|
else if (ox !== -dx || oy !== -dy) screwSpread(i, x, y, -ox, -oy, true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Powder may enter any screw except one pushing exactly against it.
|
|
function screwAccepts(p, dx, dy) {
|
|
return SCREW_DX[p] !== -dx || SCREW_DY[p] !== -dy;
|
|
}
|
|
// Straight mouths retain side-feeding into lifts; only diagonal transfers need a join.
|
|
function straightScrewNext(i, p) {
|
|
const dx = SCREW_DX[p], dy = SCREW_DY[p], x = i % W, y = (i / W) | 0;
|
|
const xx = x + dx, yy = y + dy, next = yy * W + xx;
|
|
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
|
|
const q = part[next];
|
|
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
|
|
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
|
|
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
|
|
}
|
|
// Four lanes cross a block in four beats; only a mutual geometric join can receive them.
|
|
function diagonalScrewNext(i, p) {
|
|
const x = i % W, y = (i / W) | 0, dx = SCREW_DX[p], dy = SCREW_DY[p];
|
|
const xx = x + dx;
|
|
if (xx < 0 || xx >= W) return -1;
|
|
if ((xx >> 2) === (x >> 2)) return i + dx;
|
|
const yy = y + dy * BS, join = endJoin(x >> 2, y >> 2, dx > 0 ? 1 : 0);
|
|
if (join >= 0) {
|
|
const nb = join >> 1, nx = nb % BW * BS, ny = ((nb / BW) | 0) * BS;
|
|
if (blockP[nb] === BP_BELT || blockP[nb] === BP_FILTER) return (ny - 1) * W + xx;
|
|
if (blockP[nb] === BP_SCREW) {
|
|
const v = blockV[nb];
|
|
if (v >= 4) return yy * W + xx;
|
|
if (v & 1) return (ny + (y & 3)) * W + xx;
|
|
return (ny + (dy > 0 ? 0 : 3)) * W + nx + (y & 3);
|
|
}
|
|
}
|
|
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
|
|
}
|