From be93ad12519b3028771ef8aa9fac8234db10aa25 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Mon, 26 Oct 2026 22:46:34 -0400 Subject: [PATCH] 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 --- docs/DESIGN.md | 7 ++++--- docs/agent-log.md | 13 +++++++++++++ src/render/slope-belts.js | 2 +- src/render/world.js | 4 ++-- src/sim/conveyors.js | 2 +- src/sim/slope-conveyors.js | 8 ++++---- src/ui/preview-selection.js | 2 +- src/ui/previews.js | 6 +++--- test/t_slope_belt_art.js | 3 +++ 9 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 8b4396b..21c4b28 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -462,9 +462,10 @@ above, including: 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. + belt has cleats: on 45° the load slides anyway. A downhill sloped belt is now a plain belt, with + no ribs and no sidewall, that carries at belt speed like a flat one (90 grains/s); only uphill + runs are sidewall belts. A whole run also shares one cleat phase, so pieces added later line up + with it. ## Ideas not built yet diff --git a/docs/agent-log.md b/docs/agent-log.md index 64be5cf..3950284 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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 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) - The owner's long uphill run stalled with a few grains, with a flashing seam between pieces 9 diff --git a/src/render/slope-belts.js b/src/render/slope-belts.js index ceade75..b5703f0 100644 --- a/src/render/slope-belts.js +++ b/src/render/slope-belts.js @@ -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 bx = Math.max(0, (x >> 2) - 2); bx <= Math.min(BW - 1, (x >> 2) + 2); 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); if (sd <= best) { best = sd; slopeWallOwner = b; } } diff --git a/src/render/world.js b/src/render/world.js index 8f9b0a6..265244e 100644 --- a/src/render/world.js +++ b/src/render/world.js @@ -413,8 +413,8 @@ function drawWorld() { 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; sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code); - const rib = !isFilter(code) && slopePreviewCleat(x, y, b, code); - if (sd > 0 && !rib && (isFilter(code) || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue; + const up = !isFilter(code) && slopeUphill(code, v), rib = up && slopePreviewCleat(x, y, b, code); + if (sd > 0 && !rib && (!up || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue; if (rib) sd = -0.1; } if (isSlope(code) && !slopeCell(code, dx, dy)) continue; diff --git a/src/sim/conveyors.js b/src/sim/conveyors.js index e79e332..90d2403 100644 --- a/src/sim/conveyors.js +++ b/src/sim/conveyors.js @@ -107,7 +107,7 @@ function carryBeltsAndFilters(belts, filters, d, bm, carrying) { if (carrying) { if (PH[above] === PH_LIQUID) beltDrain(b); // 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); } } } diff --git a/src/sim/slope-conveyors.js b/src/sim/slope-conveyors.js index a377b40..f2cfe50 100644 --- a/src/sim/slope-conveyors.js +++ b/src/sim/slope-conveyors.js @@ -27,7 +27,7 @@ function ribCells(x, y, g) { } 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? function slopeFaceAt(x, y, g) { if (x < 0 || x >= W || y < 0 || y >= H) return false; @@ -143,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) || !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 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; @@ -172,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) || !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; 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; @@ -190,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) || !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 i of cleatCells) { diff --git a/src/ui/preview-selection.js b/src/ui/preview-selection.js index 5eb5561..6496815 100644 --- a/src/ui/preview-selection.js +++ b/src/ui/preview-selection.js @@ -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.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]])); - 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 [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(); diff --git a/src/ui/previews.js b/src/ui/previews.js index 04447fb..538b684 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -118,10 +118,10 @@ function drawPreviews() { const ok = (!err || swap) && !ghost && afford; 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)'; - 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); 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')) { // 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]; @@ -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; } else ctx.fill(); // 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(); 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) diff --git a/test/t_slope_belt_art.js b/test/t_slope_belt_art.js index 0c49196..6a54ac0 100644 --- a/test/t_slope_belt_art.js +++ b/test/t_slope_belt_art.js @@ -137,6 +137,9 @@ check('far wall is filled down to the belt and grains cover it',run(`(()=>{ } 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;iv===0)'),run('awake.reduce((n,v)=>n+(v>0),0)'));