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. 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 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
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 ## 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`) | | Starter furnace designs | `src/sim/progress.js` (`STARTER_DESIGNS`, `smelterBlueprint`) |
| Falling sand, liquids, gases | `src/sim/cells.js` | | Falling sand, liquids, gases | `src/sim/cells.js` |
| Heat, melting, fire, reactions, alloys, steel | `src/sim/heat.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`) | | Fans | `src/sim/machine-tick.js` (`fanBlow`) |
| Refiner, Stone cutter, Silo | `src/sim/processors.js` | | Refiner, Stone cutter, Silo | `src/sim/processors.js` |
| Pipes, pumps, outlets | `src/sim/pipes.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). Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
- functions: engineIntake, fanBlow, 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. 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 - functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, diagonalScrewNext
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty - variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/slope-conveyors.js (220 lines) ### 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. 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, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad - 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 - variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
### src/sim/blocks.js (219 lines) ### 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 - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes - 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. Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW - variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
@@ -471,7 +471,7 @@ Keyboard and mouse listeners.
- functions: isTyping - functions: isTyping
- variables: canvas - 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 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 - functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown - 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 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-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) ## 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 - The owner saw ribs pass through heavy loads and the load climb as before. Rewrote the slope
+10 -8
View File
@@ -53,16 +53,18 @@ const SM_SLOPE_DRAW = `
float wave = 0.5 + 0.5 * sin((foot + w.x - local.x - phase) * 3.14159265); 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); 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)); 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 // one every ${CLEAT_SPACING} face cells, moved along by the belt's phase
float dirS = (wallFlags & 32) != 0 ? 1.0 : -1.0; float dirS = (wallFlags & 32) != 0 ? 1.0 : -1.0;
float rc = (w.x - local.x) + foot - (grade < 0.0 ? 1.0 : 0.0) - dirS * phase; if (grade * dirS < 0.0) {
float off = (mod(rc + ${(CLEAT_SPACING / 2).toFixed(1)}, ${CLEAT_SPACING.toFixed(1)}) - ${(CLEAT_SPACING / 2).toFixed(1)}) * 1.41421356; float rc = (w.x - local.x) + foot - (grade < 0.0 ? 1.0 : 0.0) - dirS * phase;
float ribTop = ${((2 * CLEAT_REACH + 1) * Math.SQRT1_2).toFixed(3)}; float off = (mod(rc + ${(CLEAT_SPACING / 2).toFixed(1)}, ${CLEAT_SPACING.toFixed(1)}) - ${(CLEAT_SPACING / 2).toFixed(1)}) * 1.41421356;
float rd = length(vec2(off, depth - clamp(depth, 0.0, ribTop))) - 0.5; float ribTop = ${((2 * CLEAT_REACH + 1) * Math.SQRT1_2).toFixed(3)};
vec3 rib = mix(vec3(0.50,0.52,0.56), vec3(0.70,0.72,0.76), smoothstep(ribTop - 1.2, ribTop, depth)); float rd = length(vec2(off, depth - clamp(depth, 0.0, ribTop))) - 0.5;
rib *= mix(0.78, 1.0, smoothstep(-0.5, 0.2, off * -dirS * grade)); vec3 rib = mix(vec3(0.50,0.52,0.56), vec3(0.70,0.72,0.76), smoothstep(ribTop - 1.2, ribTop, depth));
C = mix(C, rib, 1.0 - smoothstep(-px, px, rd)); 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. // Slopes use the flat belt's band, rollers and mesh in coordinates along the run.
+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 (above === BELTL || above === BELTR || netSat(b) <= 0) continue;
if (carrying) { if (carrying) {
if (PH[above] === PH_LIQUID) beltDrain(b); 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 // 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 // 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. // 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_SPACING = 8, CLEAT_HEIGHT = 6; // spacing in face cells, height in grains
const CLEAT_REACH = Math.floor(CLEAT_HEIGHT * Math.SQRT1_2); 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 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; 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? // 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;
@@ -46,15 +48,8 @@ function pushRow(p, dx, dy) {
q = y * W + x; q = y * W + x;
} }
const front = row[row.length - 1], m = mat[front]; 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])); if (cleatMask[q] || !(passable(m, PH_POWDER, q, dx, 0) || (PH[mat[q]] === PH_LIQUID && !occ[q] && !part[q]))) return false;
// where a downhill row meets the flat belt at the foot, its front grain levels out onto it ribMove(front, q);
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);
for (let k = row.length - 2; k >= 0; k--) ribMove(row[k], row[k] + dx + dy * W); for (let k = row.length - 2; k >= 0; k--) ribMove(row[k], row[k] + dx + dy * W);
return true; 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. // A run of sloped belt is one belt: the block at its lower-left end names it.
function slopeRunKey(b, memo) { function slopeRunKey(b, memo) {
if (memo.has(b)) return memo.get(b); if (memo.has(b)) return memo.get(b);
const c = blockP[b], g = beltGrade(c), v = blockV[b] & 1; const c = blockP[b], g = beltGrade(c), v = blockV[b] & 1, path = [b];
let bx = b % BW, by = (b / BW) | 0; 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; } while (bx > 0 && by - g >= 0 && by - g < BH && blockP[(by - g) * BW + bx - 1] === c && (blockV[(by - g) * BW + bx - 1] & 1) === v) {
memo.set(b, by * BW + bx); bx--; by -= g;
return by * BW + bx; 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) { function slopeCleatsTick(advance = true) {
const memo = new Map(), stalled = new Set(); 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)) { if (advance && !(tick & 1)) {
// each run moves all its ribs or none: if one can't clear its path, the run stalls and every // 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 // 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(); 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) || 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 // 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;
@@ -162,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) || 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; 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;
@@ -180,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) || 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 j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatCells.push(j);
} }
for (const i of cleatCells) { for (const i of cleatCells) {
+4 -2
View File
@@ -51,8 +51,10 @@ function doInteract() {
function pickUnderCursor() { function pickUnderCursor() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return; 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); const i = ay * W + ax, m = mat[i];
let pid = null; 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); 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; } 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; } 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]) { for (const grade of [-1,1]) {
flatWorld(run,160,120,112); 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++) { for(let phase=0;phase<8;phase++) {
run(`rebuildPower();slopePhase.fill(${phase});slopeCleatsTick(false);prepareSlopeBelts(0,0,W,H);`); run(`rebuildPower();slopePhase.fill(${phase});slopeCleatsTick(false);prepareSlopeBelts(0,0,W,H);`);
check(`grade ${grade} phase ${phase} draws exactly the physical cleat raster`,run(`(()=>{ 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};} else {fresh();run('for(let bx=10;bx<32;bx++)placeBlock(BP_BELT,1,bx,24,true);');s={inlet:24};}
collect(s,220,true); collect(s,220,true);
const r=collect(s,600,true);rates.push(r.out/10); 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 // uphill pockets carry about as much as a flat belt, and downhill runs like one
// grains deep onto the face, which the ribs hold back to belt speed const floor = 60;
const floor = grade > 0 ? 30 : 60;
check(`grade ${grade} carries a generous feed at least ${floor} grains/s`,r.out>0 && r.out/10>=floor,JSON.stringify(r)); 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); const end=collect(s,600);
check(`grade ${grade} leaves no load stuck`,end.left===0,JSON.stringify(end)); 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})':''}`)); 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')) { if(c.includes('BELT')) {
run(`slopePhase[20*BW+20]=${d?1:7};rebuildPower();slopeCleatsTick(false);globalThis.savedPhase=serializeWorld();deserializeWorld(savedPhase.meta,savedPhase.buffers);`); 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();`); 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))')); 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('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))); 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. // A stationary cleat blocks downhill crossings; the bare face still lets grains slide.
for (const grade of [-1,1]) { for (const grade of [-1,1]) {
const s=line(grade); const s=line(grade);
run('rebuildPower();slopeCleatsTick(false);'); 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(`(()=>{ check(`grade ${grade} bare face slides between steps without stamping the load`,run(`(()=>{
for(let x=72;x<96;x++){ 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; 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}`)); 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')); run(fs.readFileSync('src/ui/selection-pickers.js','utf8'));
const filterLine=line(-1,true); const filterLine=line(-1,true);
run("applyFilterRun(filterRun(32*BW+16),{m:[GRAVEL],inv:1,dir:0});"); run("applyFilterRun(filterRun(32*BW+16),{m:[GRAVEL],inv:1,dir:0});");