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
+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});");