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
+4
View File
@@ -461,6 +461,10 @@ above, including:
scoops what lies on the feeding belt behind it into its pocket, like a real boot section.
Measured: about 80-86 grains/s uphill under a heavy feed (flat 90); downhill about 38, since
the top only feeds a layer two grains deep onto the face.
- **No cleats on downhill belts (t42 feedback, 2026-10-26).** The owner asked why a downhill
belt has cleats: on 45° the load slides anyway. A downhill sloped belt is now a plain belt with
a sidewall that carries at belt speed like a flat one (90 grains/s); only uphill runs have
ribs. A whole run also shares one cleat phase, so pieces added later line up with it.
## Ideas not built yet
+7 -7
View File
@@ -171,7 +171,7 @@ dist/, shots/ build output and screenshots (not committed)
| Starter furnace designs | `src/sim/progress.js` (`STARTER_DESIGNS`, `smelterBlueprint`) |
| Falling sand, liquids, gases | `src/sim/cells.js` |
| Heat, melting, fire, reactions, alloys, steel | `src/sim/heat.js` |
| Belts, filters, screw conveyors | `src/sim/conveyors.js`, `src/sim/slope-conveyors.js` (cleat barriers, pocket depth and screen nudges), `src/sim/machine-tick.js` |
| Belts, filters, screw conveyors | `src/sim/conveyors.js`, `src/sim/slope-conveyors.js` (uphill cleat barriers and screen nudges), `src/sim/machine-tick.js` |
| Fans | `src/sim/machine-tick.js` (`fanBlow`) |
| Refiner, Stone cutter, Silo | `src/sim/processors.js` |
| Pipes, pumps, outlets | `src/sim/pipes.js` |
@@ -264,14 +264,14 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
- functions: engineIntake, fanBlow, machinesTick
### src/sim/conveyors.js (193 lines)
### src/sim/conveyors.js (194 lines)
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.
- functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, diagonalScrewNext
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/slope-conveyors.js (220 lines)
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it 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.
- functions: ribMove, cleatFoot, ribCells, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
### src/sim/slope-conveyors.js (230 lines)
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it 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.
- functions: ribMove, cleatFoot, ribCells, slopeUphill, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
### src/sim/blocks.js (219 lines)
@@ -384,7 +384,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes
### src/render/slope-belt-shader.js (109 lines)
### src/render/slope-belt-shader.js (111 lines)
Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
@@ -471,7 +471,7 @@ Keyboard and mouse listeners.
- functions: isTyping
- variables: canvas
### src/ui/actions.js (242 lines)
### src/ui/actions.js (244 lines)
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown
+17
View File
@@ -4,6 +4,23 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-26: t42 feedback: uphill stall at a seam, no cleats downhill, slope pick (Claude Code)
- The owner's long uphill run stalled with a few grains, with a flashing seam between pieces 9
and 10. Cause: each block kept its own cleat phase, so a piece added to a running belt was out
of step; at the seam a rib started one or two cells behind another and pushed its pocket into
that rib's back, stalling the run for good. Every block of a run now takes the phase of the
run's lower-left block (`slopeCleatsTick`, memoised `slopeRunKey`).
- Owner decision: downhill belts have no cleats. They carry like a flat belt (`carryColumn`) and
draw only the sidewall; ribs, friction drag and pushes are uphill only (`slopeUphill`).
`pushRow`'s downhill levelling is gone. Downhill now 90 grains/s (was 38).
- Middle click on a sloped belt's band, where it is drawn into the empty block below, now picks
the belt (`pickUnderCursor` falls back to `slopeBeltAt`).
- Tests: `t_slope_belts.js` adds pieces added 3/7/11/13 ticks later (13 stalled before) and the
band pick (fails without the fix); downhill floor back to 60; cleat checks uphill only.
`t_slope_belt_art.js` draws ribs on uphill runs. `python3 test/run_all.py --browser`: ALL
PASSED. Screenshots of an up-and-down run in both looks opened. Rebuilt dist.
## 2026-10-26: t42 feedback: cleats that really hold and push their load (Claude Code)
- The owner saw ribs pass through heavy loads and the load climb as before. Rewrote the slope
+3 -1
View File
@@ -53,9 +53,10 @@ const SM_SLOPE_DRAW = `
float wave = 0.5 + 0.5 * sin((foot + w.x - local.x - phase) * 3.14159265);
vec3 rubber = mix(vec3(0.20,0.22,0.25), vec3(0.25,0.27,0.31), wave);
C = mix(C, rubber, 1.0 - smoothstep(-px, px, sd));
// the ribs, where the simulation has them: rubber bars standing straight out of the belt,
// the ribs, where the simulation has them: rubber bars standing straight out of an uphill belt,
// one every ${CLEAT_SPACING} face cells, moved along by the belt's phase
float dirS = (wallFlags & 32) != 0 ? 1.0 : -1.0;
if (grade * dirS < 0.0) {
float rc = (w.x - local.x) + foot - (grade < 0.0 ? 1.0 : 0.0) - dirS * phase;
float off = (mod(rc + ${(CLEAT_SPACING / 2).toFixed(1)}, ${CLEAT_SPACING.toFixed(1)}) - ${(CLEAT_SPACING / 2).toFixed(1)}) * 1.41421356;
float ribTop = ${((2 * CLEAT_REACH + 1) * Math.SQRT1_2).toFixed(3)};
@@ -64,6 +65,7 @@ const SM_SLOPE_DRAW = `
rib *= mix(0.78, 1.0, smoothstep(-0.5, 0.2, off * -dirS * grade));
C = mix(C, rib, 1.0 - smoothstep(-px, px, rd));
}
}
// Slopes use the flat belt's band, rollers and mesh in coordinates along the run.
if ((nc == 2 || (nc >= 23 && nc <= 30)) &&
+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) {
+4 -2
View File
@@ -51,8 +51,10 @@ function doInteract() {
function pickUnderCursor() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return;
const i = ay * W + ax, m = mat[i], bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
let pid = null;
const i = ay * W + ax, m = mat[i];
let bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pid = null;
// a sloped belt's band is drawn across its block's edge, so the belt you see can lie in the next block
if (!blockP[bi] && !ghostBlocks.has(bi)) { const sb = slopeBeltAt(i, ax, ay, R.smoothOn); if (sb >= 0) bi = sb; }
const ghost = ghostBlocks.get(bi);
if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; }
else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; }
+1 -1
View File
@@ -99,7 +99,7 @@ check('drawing leaves the seed, cells and saved block data unchanged', run(`(()
})()`));
for (const grade of [-1,1]) {
flatWorld(run,160,120,112);
run(`for(let k=0;k<6;k++)placeBlock(${grade<0?'BP_BELT_UP':'BP_BELT_DOWN'},1,12+k,${grade<0?20:12}+k*${grade},true);`);
run(`for(let k=0;k<6;k++)placeBlock(${grade<0?'BP_BELT_UP':'BP_BELT_DOWN'},${grade<0?1:0},12+k,${grade<0?20:12}+k*${grade},true);`); // uphill, the cleated way
for(let phase=0;phase<8;phase++) {
run(`rebuildPower();slopePhase.fill(${phase});slopeCleatsTick(false);prepareSlopeBelts(0,0,W,H);`);
check(`grade ${grade} phase ${phase} draws exactly the physical cleat raster`,run(`(()=>{
+26 -5
View File
@@ -32,9 +32,8 @@ for(const grade of [0,-1,1]) {
else {fresh();run('for(let bx=10;bx<32;bx++)placeBlock(BP_BELT,1,bx,24,true);');s={inlet:24};}
collect(s,220,true);
const r=collect(s,600,true);rates.push(r.out/10);
// uphill pockets carry about as much as a flat belt; downhill, the top only feeds a layer two
// grains deep onto the face, which the ribs hold back to belt speed
const floor = grade > 0 ? 30 : 60;
// uphill pockets carry about as much as a flat belt, and downhill runs like one
const floor = 60;
check(`grade ${grade} carries a generous feed at least ${floor} grains/s`,r.out>0 && r.out/10>=floor,JSON.stringify(r));
const end=collect(s,600);
check(`grade ${grade} leaves no load stuck`,end.left===0,JSON.stringify(end));
@@ -114,7 +113,9 @@ for(const c of ['BP_BELT_UP','BP_BELT_DOWN','BP_FILTER_UP','BP_FILTER_DOWN'])for
check(`${c} ${d} saves its shape, direction and settings`,run(`blockP[20*BW+20]===${c} && blockV[20*BW+20]===${c.includes('FILTER')?'filterVariant('+c+','+d+'*128)':d} && blockIntegrity(20*BW+20)===10 ${c.includes('FILTER')?'&& sameFilter(filterInfo(20*BW+20),{m:[SAND,BARCU].sort((a,b)=>a-b),inv:1})':''}`));
if(c.includes('BELT')) {
run(`slopePhase[20*BW+20]=${d?1:7};rebuildPower();slopeCleatsTick(false);globalThis.savedPhase=serializeWorld();deserializeWorld(savedPhase.meta,savedPhase.buffers);`);
check(`${c} saves the moving cleat phase and immediately restores its barriers`,run(`slopePhase[20*BW+20]===${d?1:7} && cleatCells.length>0`));
// only an uphill run has cleats
const up=(c==='BP_BELT_UP')===(d===1);
check(`${c} saves the moving cleat phase and immediately restores its barriers`,run(`slopePhase[20*BW+20]===${d?1:7} && cleatCells.length${up?'>0':'===0'}`));
}
run(`globalThis.clip=copyRegion(80,80,83,83);bot.x=96;bot.y=80;undoRecord('paste',()=>pasteGhost(clip,24,20));ghostTick();`);
check(`${c} ${d} copy/paste keeps all settings`,run('blockP[20*BW+24]===blockP[20*BW+20] && blockV[20*BW+24]===blockV[20*BW+20] && sameFilter(filterInfo(20*BW+20),filterInfo(20*BW+24))'));
@@ -160,11 +161,23 @@ run('placeBlock(BP_BELT_UP,1,20,32,true);');
check('a grain on the bare face slides back down to a rib',g>0 && Math.min(...path)<g,JSON.stringify([g,path.slice(0,12)]));
check('then rides up with its rib',path[path.length-1]>Math.min(...path),JSON.stringify(path.slice(-8)));
}
// A piece added to a running belt takes up its cleat spacing, so no rib starts at the seam and
// pushes its pocket into the one ahead (that stalled the whole run).
for(const wait of [3,7,11,13]){
fresh();
run(`for(let k=0;k<9;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);for(let k=1;k<=6;k++)placeBlock(BP_BELT,1,16-k,41,true);
for(let t=0;t<${wait};t++)simTick();
for(let k=9;k<12;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);for(let k=1;k<=6;k++)placeBlock(BP_BELT,1,27+k,29,true);`);
const r=run(`(()=>{let added=0,past=0;for(let t=0;t<1500;t++){if(t<300&&t%10===0){const i=(41*4-1)*W+50;if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}simTick();}
for(let i=0;i<N;i++)if(mat[i]===SAND&&i%W>=112)past++;return {added,past,same:slopePhase[32*BW+24]===slopePhase[31*BW+25]};})()`);
check(`a piece added ${wait} ticks later shares the run's phase and the run keeps carrying`,r.same && r.added>0 && r.past===r.added,JSON.stringify(r));
}
// A stationary cleat blocks downhill crossings; the bare face still lets grains slide.
for (const grade of [-1,1]) {
const s=line(grade);
run('rebuildPower();slopeCleatsTick(false);');
check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
if(grade>0) check('a downhill run has no cleats',run('cleatCells.length===0'));
else check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
check(`grade ${grade} bare face slides between steps without stamping the load`,run(`(()=>{
for(let x=72;x<96;x++){
const by=${s.by}+((x>>2)-16)*${grade}, y=beltFaceY(blockP[by*BW+(x>>2)],x>>2,by,x)-1,i=y*W+x;
@@ -206,6 +219,14 @@ for(const id of ['belt','filter'])for(const [dx,dy] of [[3,-3],[3,3],[-3,3],[-3,
check(`${id} a single replacement keeps the selected direction`,run(`blockP[${20+dy}*BW+${20+dx}]===${c} && blockV[${20+dy}*BW+${20+dx}]===${id==='filter'?v:1-v}`));
}
// Middle click on the belt band you see, even where it is drawn into the empty block next door.
fresh();
const band=run(`(()=>{globalThis.R=globalThis.R||{smoothOn:false};
for(let k=0;k<4;k++)placeBlock(BP_BELT_UP,1,20+k,24-k,true);prepareSlopeBelts(0,0,W,H);
for(let y=60;y<110;y++)for(let x=70;x<100;x++){const i=y*W+x,own=(y>>2)*BW+(x>>2);
if(!blockP[own] && mat[i]===EMPTY && slopeBeltAt(i,x,y)>=0){GAME.pieceIdx=PIECE_BY_ID.wall.index;GAME.aim={x:x+0.5,y:y+0.5};pickUnderCursor();return GAME.pieceIdx===PIECE_BY_ID.belt.index;}}
return 'no band cell outside its block';})()`);
check('middle click on a sloped belt band outside its block picks the belt',band===true,band);
run(fs.readFileSync('src/ui/selection-pickers.js','utf8'));
const filterLine=line(-1,true);
run("applyFilterRun(filterRun(32*BW+16),{m:[GRAVEL],inv:1,dir:0});");