Sloped belts hold their load with moving cleats (t42); sloped filters are downhill shaking screens (t43)

The magic grip is gone: cleats every 8 face cells, 6 grains tall, carry
pockets uphill (about 71 grains/s against 90 flat) and overflow rolls
back. Sloped filters always run downhill; grains travel by gravity and
a shake, and sorting stays exact. Drawing needs another pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 19:47:59 -04:00
co-authored by Claude Opus 5.5
parent 1f8a89db6a
commit 578b55316b
28 changed files with 425 additions and 89 deletions
+51
View File
@@ -0,0 +1,51 @@
// Cleats are moving barriers, not material cells. A cleat supports a finite pocket; the bare face never holds grains.
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
let cleatCarrying = false;
let cleatMask = new Uint8Array(0), cleatCells = [], slopePhase = new Uint8Array(0);
function cleatFoot(x, b, phase = slopePhase[b]) {
const d = blockV[b] & 1 ? 1 : -1;
return ((x - d * phase) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING === 0;
}
function pocketDepth(x, b) {
const g = beltGrade(blockP[b]), d = blockV[b] & 1 ? 1 : -1;
// A periodic pocket envelope, clipped to two vertical projections of the cleat.
// End pockets keep moving while their cleat rounds the end of the band.
const distance = ((g * (d * slopePhase[b] - x)) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING;
return Math.max(0, CLEAT_REACH * 2 - distance);
}
function slopeCleatsTick(advance = true) {
for (const i of cleatCells) { cleatMask[i] = 0; wakeIdx(i); }
cleatCells.length = 0;
// Front first, just like flat transport. Each foot belongs to exactly one face cell.
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) continue;
const d = blockV[b] & 1 ? 1 : -1, g = beltGrade(c), running = netSat(i) > 0;
const phase = (slopePhase[b] + (advance && running && !(tick & 1) ? 1 : 0)) % CLEAT_SPACING;
if (!cleatFoot(x, b, phase)) continue;
for (let h = CLEAT_REACH; h >= 0; h--) {
const xx = x + g * h, yy = y - h - 1;
if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue;
const j = yy * W + xx;
cleatCells.push(j);
}
}
if (advance && !(tick & 1)) 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) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c,x>>2,y>>2,x)===y && netSat(i)>0)
slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
}
for (const i of cleatCells) { cleatMask[i] = 1; wakeIdx(i); }
}
// A screen shakes only the grains touching its mesh, never the whole column above it.
function screenNudge(top, d, pass) {
if (tick % SCREEN_NUDGE_TICKS) return;
const i = top - W, m = mat[i];
if (i < 0 || PH[m] !== PH_POWDER || stamp[i] === tickStamp || occ[i]) return;
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) return;
const j = i + d + W;
if (i % W + d < 0 || i % W + d >= W || j >= N) return;
if (enter(m, PH_POWDER, j, d, 1)) move(i, j, i % W, (i / W) | 0, j % W, (j / W) | 0);
}