Downhill sloped belts draw as plain belts, ghosts and previews too

Downhill runs have no cleats, so they no longer draw the sidewall or
ribs either: placed, as ghosts, or in the drag preview, which now knows
the belt's direction. Uphill runs keep the wall and ribs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:46:34 -04:00
co-authored by Claude Opus 5.5
parent 2e5ad0f1a1
commit be93ad1251
9 changed files with 32 additions and 15 deletions
+4 -3
View File
@@ -462,9 +462,10 @@ above, including:
Measured: about 80-86 grains/s uphill under a heavy feed (flat 90); downhill about 38, since 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. 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 - **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 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 no ribs and no sidewall, that carries at belt speed like a flat one (90 grains/s); only uphill
ribs. A whole run also shares one cleat phase, so pieces added later line up with it. runs are sidewall belts. A whole run also shares one cleat phase, so pieces added later line up
with it.
## Ideas not built yet ## Ideas not built yet
+13
View File
@@ -4,6 +4,19 @@ 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 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. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-27: t42 feedback: downhill belts drop the sidewall too; new tasks (Claude Code)
- Downhill sloped belts now draw as plain belts: no far wall and no ribs, placed or as ghosts or
drag previews (`slopeWallAt`, ghost drawing in `render/world.js`, `previewBlock` now gets the
belt direction from `previews.js` and `preview-selection.js`). `slopeUphill` takes the
direction bits instead of a block.
- Test: `t_slope_belt_art.js` checks a downhill run draws no wall or ribs. Drag preview checked
headlessly with a counting canvas (downhill draws the band only); placed belts and ghosts
checked in screenshots. `python3 test/run_all.py --browser`: ALL PASSED. Rebuilt dist.
- Filed t59 (world longevity audit), t60 (renewable resources, discussion), t61 (sloped walls
sticky after removing a piece mid-flow: not reproduced headless, see its report), t62 (screw
conveyor horizontal/diagonal joins), t63 (vacuum falling grains).
## 2026-10-26: t42 feedback: uphill stall at a seam, no cleats downhill, slope pick (Claude Code) ## 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 - The owner's long uphill run stalled with a few grains, with a flashing seam between pieces 9
+1 -1
View File
@@ -149,7 +149,7 @@ function slopeWallAt(x, y, smooth = false) {
for (let by = Math.max(0, (y >> 2) - 1); by <= Math.min(BH - 1, (y >> 2) + 3); by++) for (let by = Math.max(0, (y >> 2) - 1); by <= Math.min(BH - 1, (y >> 2) + 3); by++)
for (let bx = Math.max(0, (x >> 2) - 2); bx <= Math.min(BW - 1, (x >> 2) + 2); bx++) { for (let bx = Math.max(0, (x >> 2) - 2); bx <= Math.min(BW - 1, (x >> 2) + 2); bx++) {
const b = by * BW + bx; const b = by * BW + bx;
if (!isBelt(blockP[b]) || !isSlopeBelt(blockP[b])) continue; if (!isBelt(blockP[b]) || !isSlopeBelt(blockP[b]) || !slopeUphill(blockP[b], blockV[b])) continue; // downhill: a plain belt
const sd = slopeWallShape(x + 0.5, y + 0.5, b); const sd = slopeWallShape(x + 0.5, y + 0.5, b);
if (sd <= best) { best = sd; slopeWallOwner = b; } if (sd <= best) { best = sd; slopeWallOwner = b; }
} }
+2 -2
View File
@@ -413,8 +413,8 @@ function drawWorld() {
const i = y * W + x, n = (y >> 2) * BW + (x >> 2); const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue; if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code); sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code);
const rib = !isFilter(code) && slopePreviewCleat(x, y, b, code); const up = !isFilter(code) && slopeUphill(code, v), rib = up && slopePreviewCleat(x, y, b, code);
if (sd > 0 && !rib && (isFilter(code) || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue; if (sd > 0 && !rib && (!up || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue;
if (rib) sd = -0.1; if (rib) sd = -0.1;
} }
if (isSlope(code) && !slopeCell(code, dx, dy)) continue; if (isSlope(code) && !slopeCell(code, dx, dy)) continue;
+1 -1
View File
@@ -107,7 +107,7 @@ function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
if (carrying) { if (carrying) {
if (PH[above] === PH_LIQUID) beltDrain(b); if (PH[above] === PH_LIQUID) beltDrain(b);
// uphill, 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); if (!isSlopeBelt(code) || !slopeUphill(code, blockV[(y >> 2) * BW + (x >> 2)])) carryColumn(b - W, d, null);
} }
} }
} }
+4 -4
View File
@@ -27,7 +27,7 @@ function ribCells(x, y, g) {
} }
return out; return out;
} }
function slopeUphill(c, b) { return beltGrade(c) * (blockV[b] & 1 ? 1 : -1) < 0; } function slopeUphill(c, v) { return beltGrade(c) * (v & 1 ? 1 : -1) < 0; } // v: the belt's direction
// Is (x, y) a face cell of a sloped belt with grade g? // Is (x, y) a face cell of a sloped belt with grade g?
function slopeFaceAt(x, y, g) { function slopeFaceAt(x, y, g) {
if (x < 0 || x >= W || y < 0 || y >= H) return false; if (x < 0 || x >= W || y < 0 || y >= H) return false;
@@ -143,7 +143,7 @@ function slopeCleatsTick(advance = true) {
const runs = new Map(); const runs = new Map();
for (const list of [beltR, beltL]) for (const i of list) { 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]; const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, b) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue; if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, blockV[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 // 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)) 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; if (!cleatFoot(x, b, (slopePhase[b] + 1) % CLEAT_SPACING)) continue;
@@ -172,7 +172,7 @@ function slopeCleatsTick(advance = true) {
// they slide back down between steps, which on 45° is faster than the belt runs // 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) { 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; 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) || !slopeUphill(c, b) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue; if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, blockV[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; 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; 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; if (x + d < 0 || x + d >= W || j < 0 || cleatMask[j] || !passable(mat[a], PH_POWDER, j, d, st)) continue;
@@ -190,7 +190,7 @@ function rebuildCleatMask() {
cleatCells.length = 0; cleatCells.length = 0;
for (const list of [beltR, beltL]) for (const i of list) { 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]; const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, b) || beltFaceY(c, x >> 2, y >> 2, x) !== y || !cleatFoot(x, b)) continue; if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, blockV[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 j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatCells.push(j);
} }
for (const i of cleatCells) { for (const i of cleatCells) {
+1 -1
View File
@@ -73,7 +73,7 @@ function drawPastePreview() {
ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3); ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3);
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); ctx.strokeRect(x, y, w, h); ctx.setLineDash([]);
const planned = new Map(bp.blocks.map(([dx, dy, code, v]) => [(oy + dy) * BW + ox + dx, [code, v]])); const planned = new Map(bp.blocks.map(([dx, dy, code, v]) => [(oy + dy) * BW + ox + dx, [code, v]]));
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; previewBlock(code, bx2, by2, bw2, bh2, false, code === BP_SCREW ? v : -1, ox + dx, oy + dy, planned); } for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; previewBlock(code, bx2, by2, bw2, bh2, false, code === BP_SCREW || isBelt(code) ? v : -1, ox + dx, oy + dy, planned); }
for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); } for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); } for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
ctx.restore(); ctx.restore();
+3 -3
View File
@@ -118,10 +118,10 @@ function drawPreviews() {
const ok = (!err || swap) && !ghost && afford; const ok = (!err || swap) && !ghost && afford;
const [x, y, w, h] = blockRectPx(bx, by, 1, 1); const [x, y, w, h] = blockRectPx(bx, by, 1, 1);
ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)'; ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)';
previewBlock(code, x, y, w, h, false, P.id === 'screw' ? dIdx : -1, bx, by, planned); previewBlock(code, x, y, w, h, false, P.id === 'screw' || P.id === 'belt' ? dIdx : -1, bx, by, planned);
ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25); ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25);
if (ghost) ctx.setLineDash([zp, zp * 0.7]); if (ghost) ctx.setLineDash([zp, zp * 0.7]);
previewBlock(code, x, y, w, h, true, P.id === 'screw' ? dIdx : -1, bx, by, planned); ctx.setLineDash([]); previewBlock(code, x, y, w, h, true, P.id === 'screw' || P.id === 'belt' ? dIdx : -1, bx, by, planned); ctx.setLineDash([]);
if (P.dirs && (P.id === 'belt' || P.id === 'screw' || P.filter || P.id === 'pump' || P.id === 'outlet')) { if (P.dirs && (P.id === 'belt' || P.id === 'screw' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
// a chevron on every block, pointing the way it will carry (or pump, or pour) // a chevron on every block, pointing the way it will carry (or pump, or pour)
const [dx, dy] = isSlopeBelt(code) ? [dIdx ? 1 : -1, beltGrade(code) * (dIdx ? 1 : -1)] : P.id === 'screw' ? SCREW_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx]; const [dx, dy] = isSlopeBelt(code) ? [dIdx ? 1 : -1, beltGrade(code) * (dIdx ? 1 : -1)] : P.id === 'screw' ? SCREW_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
@@ -222,7 +222,7 @@ function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1,
if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; } if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
else ctx.fill(); else ctx.fill();
// Use the same filled wall profile as placed slopes; ribs stay on the physics feet. // Use the same filled wall profile as placed slopes; ribs stay on the physics feet.
if (!isFilter(code)) { if (!isFilter(code) && (direction < 0 || slopeUphill(code, direction))) { // downhill: a plain belt
ctx.beginPath(); ctx.beginPath();
for (let yy = -9; yy < 8; yy += step) for (let xx = -5; xx < 9; xx += step) for (let yy = -9; yy < 8; yy += step) for (let xx = -5; xx < 9; xx += step)
if (slopeWallShape(ox + xx + step / 2, oy + yy + step / 2, b, code) <= 0) if (slopeWallShape(ox + xx + step / 2, oy + yy + step / 2, b, code) <= 0)
+3
View File
@@ -137,6 +137,9 @@ check('far wall is filled down to the belt and grains cover it',run(`(()=>{
} }
return wall>20; return wall>20;
})()`)); })()`));
flatWorld(run,160,120,112);
run('for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,0,12+k,20-k,true);rebuildPower();slopeCleatsTick(false);prepareSlopeBelts(0,0,W,H);');
check('a downhill belt draws no wall and no ribs',run('(()=>{let n=0;for(let i=0;i<N;i++)if(slopeDetailAt(i,i%W,(i/W)|0))n++;return n===0 && cleatCells.length===0;})()'));
flatWorld(run,320,240,232); flatWorld(run,320,240,232);
run('for(let k=0;k<8;k++)placeBlock(BP_BELT_UP,1,12+k,30-k,true);for(let t=0;t<120;t++)simTick();'); run('for(let k=0;k<8;k++)placeBlock(BP_BELT_UP,1,12+k,30-k,true);for(let t=0;t<120;t++)simTick();');
check('idle empty slope lets every moving area sleep',run('awake.every(v=>v===0)'),run('awake.reduce((n,v)=>n+(v>0),0)')); check('idle empty slope lets every moving area sleep',run('awake.every(v=>v===0)'),run('awake.reduce((n,v)=>n+(v>0),0)'));