Far sidewall drawn behind the load with cleat ribs at their physics positions; sloped screens get a tinted mesh between rails and shake only when loaded. Powder skips cleat checks outside chunks that have cleats, and empty slopes let their chunks sleep. Record the screw conveyor and stall/carry-depth decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
3.5 KiB
JavaScript
67 lines
3.5 KiB
JavaScript
// 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 cleatChunks = new Uint8Array(0);
|
|
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; wakeCleatLoad(i); }
|
|
cleatChunks.fill(0);
|
|
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; wakeCleatLoad(i);
|
|
const x = i % W, y = (i / W) | 0;
|
|
for (let cy = Math.max(0, (y - 1) >> 5); cy <= Math.min(CH - 1, (y + 1) >> 5); cy++)
|
|
for (let cx = Math.max(0, (x - 1) >> 5); cx <= Math.min(CW - 1, (x + 1) >> 5); cx++) cleatChunks[cy * CW + cx] = 1;
|
|
}
|
|
}
|
|
// 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);
|
|
}
|
|
|
|
// Only loose load can react to a barrier appearing or disappearing.
|
|
function wakeCleatLoad(i) {
|
|
const x = i % W, y = (i / W) | 0;
|
|
for (let yy = Math.max(0, y - 1); yy <= Math.min(H - 1, y + 1); yy++)
|
|
for (let xx = Math.max(0, x - 1); xx <= Math.min(W - 1, x + 1); xx++)
|
|
if (PH[mat[yy * W + xx]] === PH_POWDER || occ[yy * W + xx]) wakeXY(xx, yy);
|
|
}
|