Uphill runs share one cleat phase; no cleats downhill; pick slope bands

A piece added to a running sloped belt kept its own cleat phase, so at
the seam a rib could start just behind another and push its pocket into
it, stalling the whole run with only a few grains. Every block of a run
now takes the phase of the run's lower-left block.

Downhill sloped belts no longer have cleats: they carry like a flat belt
(90 grains/s, was 38) and draw only the sidewall.

Middle click on a sloped belt's band, where it is drawn into the empty
block beside it, now picks the belt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:31:01 -04:00
co-authored by Claude Opus 5.5
parent 8cdc47fd33
commit 2e5ad0f1a1
9 changed files with 98 additions and 41 deletions
+2 -1
View File
@@ -106,7 +106,8 @@ function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
if (above === BELTL || above === BELTR || netSat(b) <= 0) continue;
if (carrying) {
if (PH[above] === PH_LIQUID) beltDrain(b);
if (!isSlopeBelt(code)) carryColumn(b - W, d, null); // on a slope, the cleats do the carrying
// uphill, the cleats do the carrying
if (!isSlopeBelt(code) || !slopeUphill(code, (y >> 2) * BW + (x >> 2))) carryColumn(b - W, d, null);
}
}
}
+27 -17
View File
@@ -3,6 +3,7 @@
// until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the
// rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything
// heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls.
// Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
const CLEAT_SPACING = 8, CLEAT_HEIGHT = 6; // spacing in face cells, height in grains
const CLEAT_REACH = Math.floor(CLEAT_HEIGHT * Math.SQRT1_2);
const SCREEN_NUDGE_TICKS = 4; // one downhill contact step per four ticks (15 steps/s), plus gravity
@@ -26,6 +27,7 @@ function ribCells(x, y, g) {
}
return out;
}
function slopeUphill(c, b) { return beltGrade(c) * (blockV[b] & 1 ? 1 : -1) < 0; }
// Is (x, y) a face cell of a sloped belt with grade g?
function slopeFaceAt(x, y, g) {
if (x < 0 || x >= W || y < 0 || y >= H) return false;
@@ -46,15 +48,8 @@ function pushRow(p, dx, dy) {
q = y * W + x;
}
const front = row[row.length - 1], m = mat[front];
const open = j => !cleatMask[j] && (passable(m, PH_POWDER, j, dx, 0) || (PH[mat[j]] === PH_LIQUID && !occ[j] && !part[j]));
// where a downhill row meets the flat belt at the foot, its front grain levels out onto it
let to = q;
if (!open(q)) {
const fx = front % W + dx;
if (dy <= 0 || fx < 0 || fx >= W || !open(front + dx)) return false;
to = front + dx;
}
ribMove(front, to);
if (cleatMask[q] || !(passable(m, PH_POWDER, q, dx, 0) || (PH[mat[q]] === PH_LIQUID && !occ[q] && !part[q]))) return false;
ribMove(front, q);
for (let k = row.length - 2; k >= 0; k--) ribMove(row[k], row[k] + dx + dy * W);
return true;
}
@@ -118,14 +113,29 @@ function advanceCleat(x, y, g, d) {
// A run of sloped belt is one belt: the block at its lower-left end names it.
function slopeRunKey(b, memo) {
if (memo.has(b)) return memo.get(b);
const c = blockP[b], g = beltGrade(c), v = blockV[b] & 1;
let bx = b % BW, by = (b / BW) | 0;
while (bx > 0 && by - g >= 0 && by - g < BH && blockP[(by - g) * BW + bx - 1] === c && (blockV[(by - g) * BW + bx - 1] & 1) === v) { bx--; by -= g; }
memo.set(b, by * BW + bx);
return by * BW + bx;
const c = blockP[b], g = beltGrade(c), v = blockV[b] & 1, path = [b];
let bx = b % BW, by = (b / BW) | 0, key = -1;
while (bx > 0 && by - g >= 0 && by - g < BH && blockP[(by - g) * BW + bx - 1] === c && (blockV[(by - g) * BW + bx - 1] & 1) === v) {
bx--; by -= g;
if (memo.has(by * BW + bx)) { key = memo.get(by * BW + bx); break; }
path.push(by * BW + bx);
}
if (key < 0) key = by * BW + bx;
for (const p of path) memo.set(p, key);
return key;
}
function slopeCleatsTick(advance = true) {
const memo = new Map(), stalled = new Set();
// the whole run shares the phase of its lower-left block, so a piece added to a running belt
// takes up its cleat spacing instead of starting a rib of its own at the seam
let resynced = false;
for (const list of [beltR, beltL]) for (const i of list) {
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if ((x & 3) || !isBelt(c) || !isSlopeBelt(c) || beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
const p = slopePhase[slopeRunKey(b, memo)];
if (slopePhase[b] !== p) { slopePhase[b] = p; resynced = true; }
}
if (resynced) rebuildCleatMask();
if (advance && !(tick & 1)) {
// each run moves all its ribs or none: if one can't clear its path, the run stalls and every
// grain its ribs moved goes back. Front of the line first, so a rib pushes into room the rib
@@ -133,7 +143,7 @@ function slopeCleatsTick(advance = true) {
const runs = new Map();
for (const list of [beltR, beltL]) for (const i of list) {
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if (!isBelt(c) || !isSlopeBelt(c) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue;
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, b) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue;
// lift the run's ribs off; each is put back where it moves to, and a rib leaving the top is gone
if (cleatFoot(x, b)) for (const j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatMask[j] = 0;
if (!cleatFoot(x, b, (slopePhase[b] + 1) % CLEAT_SPACING)) continue;
@@ -162,7 +172,7 @@ function slopeCleatsTick(advance = true) {
// they slide back down between steps, which on 45° is faster than the belt runs
for (const list of [beltR, beltL]) for (const i of list) {
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b], a = i - W;
if (!isBelt(c) || !isSlopeBelt(c) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue;
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, b) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue;
if (a < 0 || PH[mat[a]] !== PH_POWDER || occ[a] || cleatMask[a] || stamp[a] === tickStamp) continue;
const d = blockV[b] & 1 ? 1 : -1, st = conveyorStep(x, y, d), j = a + d + st * W;
if (x + d < 0 || x + d >= W || j < 0 || cleatMask[j] || !passable(mat[a], PH_POWDER, j, d, st)) continue;
@@ -180,7 +190,7 @@ function rebuildCleatMask() {
cleatCells.length = 0;
for (const list of [beltR, beltL]) for (const i of list) {
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if (!isBelt(c) || !isSlopeBelt(c) || beltFaceY(c, x >> 2, y >> 2, x) !== y || !cleatFoot(x, b)) continue;
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, b) || beltFaceY(c, x >> 2, y >> 2, x) !== y || !cleatFoot(x, b)) continue;
for (const j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatCells.push(j);
}
for (const i of cleatCells) {