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