// 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; }