Cleats really hold and push their pockets (t42 feedback)

Nothing carries the load on a slope but the ribs: each belt step every
rib moves one face cell and pushes the grains in its way, the layer on
the belt is only dragged by friction that gravity beats on 45 degrees,
and heaped grains roll back. Ribs never pass through grains or into
machines; a run that can't push stalls. The foot of an uphill run
scoops the feed into pockets. Ribs drawn as rubber bars in both looks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 20:56:51 -04:00
co-authored by Claude Opus 5.5
parent 3ee85429fd
commit 8cdc47fd33
12 changed files with 276 additions and 52 deletions
+14 -1
View File
@@ -417,7 +417,7 @@ above, including:
only loaded screens shake. Empty moving cleats must let chunks sleep. Keep pocket transport, only loaded screens shake. Empty moving cleats must let chunks sleep. Keep pocket transport,
uphill throughput near 71 grains/s and exact sorting unchanged. uphill throughput near 71 grains/s and exact sorting unchanged.
- **Sidewall belts with moving cleats (t42, 2026-10-26).** To replace the old full-column - **Sidewall belts with moving cleats (t42, 2026-10-26; physics replaced by the entry below).** To replace the old full-column
grip, use moving barrier lines rather than material cells: the barrier mask blocks normal grip, use moving barrier lines rather than material cells: the barrier mask blocks normal
powder movement without overwriting grains, terrain or parts. Cleats stand perpendicular powder movement without overwriting grains, terrain or parts. Cleats stand perpendicular
to the face, spaced 8 horizontal face cells apart and 6 grains tall (integer normal reach to the face, spaced 8 horizontal face cells apart and 6 grains tall (integer normal reach
@@ -449,6 +449,19 @@ above, including:
belt and the animation stopped on everything affected, and belts limited to a realistic belt and the animation stopped on everything affected, and belts limited to a realistic
load depth instead of carrying 96-grain stacks. Not built yet. load depth instead of carrying 96-grain stacks. Not built yet.
- **Cleats really push their pockets (t42 feedback, 2026-10-26).** The owner found the first
cleats passed through heavy loads while the material climbed as before, and wants cleats that
hold the material and drag it up. Now nothing carries the load on a slope but the ribs and a
friction drag on the layer touching the belt (which gravity beats on 45°, so loose grains slide
back until a rib holds them). Each belt step every rib moves one face cell and pushes the
grains in its way ahead of it; whatever is heaped above the rib is left to roll back. A rib
never moves into a grain, never crosses a machine, and stops short of anything built. If any
rib of a run can't push its way, the whole run stalls that step and every move is undone (the
stall t45 will show). At the foot of an uphill run, each rib coming up around the end roller
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.
## Ideas not built yet ## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble. - **Concrete**, as a stronger wall material from rubble.
+9 -8
View File
@@ -260,19 +260,19 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick - functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty - variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
### src/sim/machine-tick.js (155 lines) ### src/sim/machine-tick.js (156 lines)
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 (197 lines) ### src/sim/conveyors.js (193 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 (66 lines) ### src/sim/slope-conveyors.js (220 lines)
Cleats are moving barriers, not material cells. A cleat supports a finite pocket; the bare face never holds grains. 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: cleatFoot, pocketDepth, slopeCleatsTick, screenNudge, wakeCleatLoad - functions: ribMove, cleatFoot, ribCells, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatCarrying, cleatChunks, cleatMask, cleatCells, slopePhase - 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)
Building blocks: placing, integrity, removing, lines and rectangles of blocks. Building blocks: placing, integrity, removing, lines and rectangles of blocks.
@@ -366,7 +366,7 @@ Drawing only: the face stays on its simulation cells; the band hangs below it.
- functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat - functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner - variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner
### src/render/world.js (456 lines) ### src/render/world.js (458 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock, drawWorld - functions: fogColor, diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock, drawWorld
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
@@ -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 (99 lines) ### src/render/slope-belt-shader.js (109 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
@@ -566,6 +566,7 @@ The frame loop and boot.
- `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge. - `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
- `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen). - `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen).
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7 - `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
- `tools/scenes/t-screen-joint.js`: Downhill screen whose outlet belt sits level with its last block (the jam the owner saw).
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket. - `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
- `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt. - `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect. - `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
+18
View File
@@ -4,6 +4,24 @@ 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: 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
physics in `src/sim/slope-conveyors.js`: no column carry on slopes; each step every rib moves
one face cell and pushes the rows of grains in its way (`pushRow`, front first, never between
two rib cells or into a machine); a contact-layer friction drag that gravity beats on 45°;
a boot-section scoop at the foot of uphill runs; a run stalls (all its moves undone) if a rib
can't push; grains under a newly placed rib are lifted out. Ribs stop short of built cells.
Slopes now step before flat belts (`machine-tick.js`), so a feeding belt finds the room.
- Drawing: ribs drawn by the smooth wall shader at the simulation phase (rubber bars with a
light cap); pixel ribs lighter with a cap; far wall a little lighter (`render/world.js`,
`slope-belt-shader.js`, `slope-belts.js`).
- Rates: uphill 78.7 (suite) to 86 grains/s (heavy probe), downhill 38, flat 90. Perf: 576
loaded slopes 1.03 ms/tick; 50 slopes in a generated world 2.19 vs 1.83 ms/tick without.
- Tests: `t_slope_belts.js` adds "no grain ever inside a rib under heavy feed", "a grain on the
bare face slides back to a rib, then rides up with it"; downhill rate floor lowered to 30.
`python3 test/run_all.py --browser`: ALL PASSED. Open: downhill rate; t58 (low-end joins).
## 2026-10-26: t42/t43 drawing and performance follow-up (Codex) ## 2026-10-26: t42/t43 drawing and performance follow-up (Codex)
- Filled the far sidewall down to the belt, behind grains, with two-tone pixel corrugation - Filled the far sidewall down to the belt, behind grains, with two-tone pixel corrugation
+11 -1
View File
@@ -51,8 +51,18 @@ const SM_SLOPE_DRAW = `
float sd = max(ends * 1.41421356, max(-depth, depth - ${CLEAT_HEIGHT.toFixed(1)})); float sd = max(ends * 1.41421356, max(-depth, depth - ${CLEAT_HEIGHT.toFixed(1)}));
float phase = float(wallFlags & 7); float phase = float(wallFlags & 7);
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.16,0.18,0.21), vec3(0.21,0.23,0.27), 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,
// one every ${CLEAT_SPACING} face cells, moved along by the belt's phase
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;
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)};
float rd = length(vec2(off, depth - clamp(depth, 0.0, ribTop))) - 0.5;
vec3 rib = mix(vec3(0.50,0.52,0.56), vec3(0.70,0.72,0.76), smoothstep(ribTop - 1.2, ribTop, depth));
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.
+1 -1
View File
@@ -160,7 +160,7 @@ function slopeDetailAt(i, x, y, smooth = false) {
slopeWallOwner = -1; slopeWallOwner = -1;
if (!slopeBeltHalo[(y >> 2) * BW + (x >> 2)] && !cleatMask[i]) return 0; if (!slopeBeltHalo[(y >> 2) * BW + (x >> 2)] && !cleatMask[i]) return 0;
if (occ[i] || mach[i] || part[i]) return 0; if (occ[i] || mach[i] || part[i]) return 0;
if (cleatMask[i] && PH[mat[i]] !== PH_SOLID) return 1; if (cleatMask[i] && PH[mat[i]] !== PH_SOLID) { slopeWallAt(x, y, smooth); return 1; }
if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return 0; if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return 0;
return slopeWallAt(x, y, smooth) >= 0 ? 2 : 0; return slopeWallAt(x, y, smooth) >= 0 ? 2 : 0;
} }
+8 -6
View File
@@ -292,9 +292,11 @@ function drawWorld() {
const slopeDetail = slopeDetailAt(i, x, y, sm); const slopeDetail = slopeDetailAt(i, x, y, sm);
if (slopeDetail) { if (slopeDetail) {
const wave = slopeDetail === 2 && ((x + beltGrade(blockP[slopeWallOwner]) * y - slopePhase[slopeWallOwner]) & 3) < 2; const wave = slopeDetail === 2 && ((x + beltGrade(blockP[slopeWallOwner]) * y - slopePhase[slopeWallOwner]) & 3) < 2;
r = slopeDetail === 1 ? 100 : wave ? 51 : 43; // ribs: light rubber with a pale cap; the far wall behind them: muted, corrugated rubber
g = slopeDetail === 1 ? 104 : wave ? 55 : 47; const cap = slopeDetail === 1 && slopeWallOwner >= 0 && !cleatMask[i - W + beltGrade(blockP[slopeWallOwner])];
b = slopeDetail === 1 ? 112 : wave ? 63 : 55; r = slopeDetail === 1 ? (cap ? 178 : 146) : wave ? 64 : 56;
g = slopeDetail === 1 ? (cap ? 182 : 150) : wave ? 68 : 60;
b = slopeDetail === 1 ? (cap ? 190 : 158) : wave ? 76 : 68;
} }
// incandescence // incandescence
if (T > 450 && m !== EMPTY && incand) { if (T > 450 && m !== EMPTY && incand) {
@@ -346,11 +348,11 @@ function drawWorld() {
((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8); ((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8);
if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; } if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; }
} }
if (slopeDetail === 1) cl = 4; if (slopeDetail && slopeWallOwner >= 0) {
else if (slopeDetail === 2) { // ribs and the far wall are both drawn by the wall shader, ribs from the same phase as the simulation
const wb = slopeWallOwner; const wb = slopeWallOwner;
cl = beltGrade(blockP[wb]) < 0 ? 31 : 32; cl = beltGrade(blockP[wb]) < 0 ? 31 : 32;
tpx[row + bx] = ((128 | slopePhase[wb] | ((slopeBeltJoins[wb] & 12) << 1)) << 24) | cl | tpx[row + bx] = ((128 | slopePhase[wb] | ((slopeBeltJoins[wb] & 12) << 1) | (blockV[wb] & 1 ? 32 : 0)) << 24) | cl |
((y - ((wb / BW) | 0) * BS + 8) << 16) | ((x - wb % BW * BS + 8) << 8); ((y - ((wb / BW) | 0) * BS + 8) << 16) | ((x - wb % BW * BS + 8) << 8);
} }
top = cl << 24; top = cl << 24;
+1 -1
View File
@@ -36,7 +36,7 @@ function move(i, j, x, y, nx, ny) {
// Can a grain of material m (phase ph) enter cell j moving by (dx,dy)? // Can a grain of material m (phase ph) enter cell j moving by (dx,dy)?
// 0 = no, 1 = into empty, 2 = by displacing what is there. // 0 = no, 1 = into empty, 2 = by displacing what is there.
function enter(m, ph, j, dx, dy) { function enter(m, ph, j, dx, dy) {
if (ph === PH_POWDER && !cleatCarrying && cleatCells.length && (cleatMask[j] || (dy > 0 && cleatMask[j - dx - dy * W]))) return 0; if (ph === PH_POWDER && cleatCells.length && (cleatMask[j] || (dy > 0 && cleatMask[j - dx - dy * W]))) return 0;
return enterFree(m, ph, j, dx, dy); return enterFree(m, ph, j, dx, dy);
} }
// Powder already knows whether its chunk has barriers; other callers use enter. // Powder already knows whether its chunk has barriers; other callers use enter.
+3 -7
View File
@@ -19,11 +19,11 @@ function conveyorStep(x, y, d) {
const b = (y >> 2) * BW + (x >> 2); const b = (y >> 2) * BW + (x >> 2);
return beltGrade(blockP[b]) * d; return beltGrade(blockP[b]) * d;
} }
function carryColumn(a, d, pass, depth = CARRY_DEPTH) { function carryColumn(a, d, pass) {
if (a < 0 || mat[a] === EMPTY) return; if (a < 0 || mat[a] === EMPTY) return;
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d); const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d);
let blocked = false; let blocked = false;
for (let L = 0; L < depth; L++, a -= W) { for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
if (a < 0) return; if (a < 0) return;
const m = mat[a]; const m = mat[a];
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
@@ -106,11 +106,7 @@ 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)) { if (!isSlopeBelt(code)) carryColumn(b - W, d, null); // on a slope, the cleats do the carrying
cleatCarrying = true;
carryColumn(b - W, d, null, pocketDepth(x, (y >> 2) * BW + (x >> 2)));
cleatCarrying = false;
} else carryColumn(b - W, d, null);
} }
} }
} }
+2 -1
View File
@@ -89,9 +89,10 @@ function machinesTick() {
} }
// Belts step every other tick; sloped belts carry only their finite pockets. // Belts step every other tick; sloped belts carry only their finite pockets.
if (filtersDirty) rebuildFilters(); if (filtersDirty) rebuildFilters();
// slopes first, so a flat belt feeding one finds the room the slope has just made
slopeCleatsTick();
carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0); carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0); carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
slopeCleatsTick();
// screws: every loose grain inside steps one cell along, front of the line first, on the same // screws: every loose grain inside steps one cell along, front of the line first, on the same
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt // beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
if ((tick & 1) === 0) for (let p = 1; p < IS_SCREW.length; p++) { if ((tick & 1) === 0) for (let p = 1; p < IS_SCREW.length; p++) {
+178 -24
View File
@@ -1,51 +1,205 @@
// Cleats are moving barriers, not material cells. A cleat supports a finite pocket; the bare face never holds grains. // 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.
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
let cleatCarrying = false;
let cleatChunks = new Uint8Array(0); let cleatChunks = new Uint8Array(0);
let cleatMask = new Uint8Array(0), cleatCells = [], slopePhase = new Uint8Array(0); let cleatMask = new Uint8Array(0), cleatCells = [], slopePhase = new Uint8Array(0);
let ribLog = []; // the swaps one run's ribs made this step, undone if the run stalls
function ribMove(a, b) { move(a, b, a % W, (a / W) | 0, b % W, (b / W) | 0); ribLog.push(a, b); }
function cleatFoot(x, b, phase = slopePhase[b]) { function cleatFoot(x, b, phase = slopePhase[b]) {
const d = blockV[b] & 1 ? 1 : -1; const d = blockV[b] & 1 ? 1 : -1;
return ((x - d * phase) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING === 0; return ((x - d * phase) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING === 0;
} }
function pocketDepth(x, b) { // The rib standing on face cell (x, y) of a slope with grade g, foot first. Where its end would
const g = beltGrade(blockP[b]), d = blockV[b] & 1 ? 1 : -1; // reach into something built (a screw conveyor, the next belt, a wall), the rib stops short: -1.
// A periodic pocket envelope, clipped to two vertical projections of the cleat. function ribCells(x, y, g) {
// End pockets keep moving while their cleat rounds the end of the band. const out = [];
const distance = ((g * (d * slopePhase[b] - x)) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING; let open = true;
return Math.max(0, CLEAT_REACH * 2 - distance); for (let h = 0; h <= CLEAT_REACH; h++) {
const xx = x + g * h, yy = y - h - 1, j = yy * W + xx;
if (xx < 0 || xx >= W || yy < 0 || yy >= H || part[j] || mach[j] || PH[mat[j]] === PH_SOLID) open = false;
out.push(open ? j : -1);
}
return out;
}
// 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;
const c = blockP[(y >> 2) * BW + (x >> 2)];
return isBelt(c) && isSlopeBelt(c) && beltGrade(c) === g && beltFaceY(c, x >> 2, y >> 2, x) === y;
}
// Push the row of loose grains starting at p one cell along (dx, dy), front first. False if the
// row runs into something it can't enter.
function pushRow(p, dx, dy) {
const row = [];
let q = p;
for (let k = 0; k < CARRY_DEPTH; k++) {
if (PH[mat[q]] !== PH_POWDER || occ[q] || part[q] || (k && cleatMask[q])) break; // a machine moves its own contents
row.push(q);
const x = q % W + dx, y = ((q / W) | 0) + dy;
if (x < 0 || x >= W || y < 0 || y >= H) return false;
if (cleatMask[q + dx] && cleatMask[q + dy * W]) return false; // never between two cells of a rib
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);
for (let k = row.length - 2; k >= 0; k--) ribMove(row[k], row[k] + dx + dy * W);
return true;
}
// At the foot of an uphill run, a rib coming up around the end roller scoops what lies on the
// feeding belt behind it, one pocket long and as high as the rib, into the pocket in front of it.
function scoop(x, y, d, rib) {
for (let k = 1; k < CLEAT_SPACING; k++) {
const xx = x - d * k;
if (xx < 0 || xx >= W) break;
for (let yy = y; yy >= y - Math.min(k, CLEAT_REACH) && yy >= 0; yy--) {
const a = yy * W + xx, m = mat[a];
if (PH[m] !== PH_POWDER || occ[a] || cleatMask[a]) continue;
const to = pocketRoom(rib, d, m);
if (to < 0) return;
ribMove(a, to);
}
}
}
// The lowest open cell in the pocket just in front of a rib, or -1 if it is full.
function pocketRoom(rib, d, m) {
for (let h = 0; h < rib.length; h++) {
if (rib[h] < 0) continue;
for (let t = 1; t < CLEAT_SPACING; t++) {
const j = rib[h] + d * t, jx = rib[h] % W + d * t;
if (jx < 0 || jx >= W) break;
if (PH[mat[j]] === PH_SOLID || cleatMask[j]) break;
if (passable(m, PH_POWDER, j, d, 0)) return j;
}
}
return -1;
}
// Move the rib that now stands on face cell (x - d) to (x, y), pushing what is in its way.
function advanceCleat(x, y, g, d) {
const st = g * d, ox = x - d, oy = y - st;
const fresh = ribCells(x, y, g), old = slopeFaceAt(ox, oy, g) ? ribCells(ox, oy, g) : null;
// the cells the rib sweeps: those between the old and the new rib, and the new rib itself
const sweep = [];
if (old) {
for (const j of old) if (j >= 0) cleatMask[j] = 0;
for (let h = 0; h < old.length; h++) {
const j = old[h];
if (j >= 0) {
sweep.push(j + d);
if (h < old.length - 1) sweep.push(j + st * W);
}
if (fresh[h] >= 0) sweep.push(fresh[h]);
}
} else {
for (const j of fresh) if (j >= 0) sweep.push(j);
if (st < 0) scoop(x, y, d, fresh);
}
let clear = true;
for (const s of sweep) {
if (s < 0 || s >= N || PH[mat[s]] !== PH_POWDER || occ[s] || part[s] || pushRow(s, d, st)) continue;
clear = false;
break;
}
for (const j of fresh) if (j >= 0) { cleatMask[j] = 1; cleatCells.push(j); } // the rebuild clears it if this run stalls
return clear;
}
// 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;
} }
function slopeCleatsTick(advance = true) { function slopeCleatsTick(advance = true) {
const memo = new Map(), stalled = new Set();
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
// ahead of it has just made.
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;
// 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;
const key = slopeRunKey(b, memo);
if (!runs.has(key)) runs.set(key, []);
runs.get(key).push(x, y, beltGrade(c), blockV[b] & 1 ? 1 : -1);
}
for (const [key, feet] of runs) {
ribLog.length = 0;
let ok = true;
for (let k = 0; k < feet.length && ok; k += 4) ok = advanceCleat(feet[k], feet[k + 1], feet[k + 2], feet[k + 3]);
if (ok) continue;
stalled.add(key);
for (let k = ribLog.length - 2; k >= 0; k -= 2) { swapCells(ribLog[k], ribLog[k + 1]); wakeIdx(ribLog[k]); wakeIdx(ribLog[k + 1]); }
}
ribLog.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 ((x & 3) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c, x >> 2, y >> 2, x) === y && netSat(i) > 0 && !stalled.has(slopeRunKey(b, memo)))
slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
}
}
rebuildCleatMask();
if (advance && !(tick & 1)) {
// the running face drags the grains lying right on it; unless something below holds them,
// 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 (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;
const bj = j + W;
// dragged off an open end with nothing under it: it leaves at belt speed and falls
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, x + d, y - 1 + st, d * 0.5, st * 0.5);
else move(a, j, x, y - 1, x + d, y - 1 + st);
}
}
}
// The barriers, rebuilt from the phases: ribs that left the top of a slope are gone.
function rebuildCleatMask() {
for (const i of cleatCells) { cleatMask[i] = 0; wakeCleatLoad(i); } for (const i of cleatCells) { cleatMask[i] = 0; wakeCleatLoad(i); }
cleatChunks.fill(0); cleatChunks.fill(0);
cleatCells.length = 0; cleatCells.length = 0;
// Front first, just like flat transport. Each foot belongs to exactly one face cell.
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) continue; if (!isBelt(c) || !isSlopeBelt(c) || beltFaceY(c, x >> 2, y >> 2, x) !== y || !cleatFoot(x, b)) continue;
const d = blockV[b] & 1 ? 1 : -1, g = beltGrade(c), running = netSat(i) > 0; for (const j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatCells.push(j);
const phase = (slopePhase[b] + (advance && running && !(tick & 1) ? 1 : 0)) % CLEAT_SPACING;
if (!cleatFoot(x, b, phase)) continue;
for (let h = CLEAT_REACH; h >= 0; h--) {
const xx = x + g * h, yy = y - h - 1;
if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue;
const j = yy * W + xx;
cleatCells.push(j);
}
}
if (advance && !(tick & 1)) 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) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c,x>>2,y>>2,x)===y && netSat(i)>0)
slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
} }
for (const i of cleatCells) { for (const i of cleatCells) {
cleatMask[i] = 1; wakeCleatLoad(i); cleatMask[i] = 1; wakeCleatLoad(i);
if (PH[mat[i]] === PH_POWDER && !occ[i]) expelFromRib(i);
const x = i % W, y = (i / W) | 0; const x = i % W, y = (i / W) | 0;
for (let cy = Math.max(0, (y - 1) >> 5); cy <= Math.min(CH - 1, (y + 1) >> 5); cy++) for (let cy = Math.max(0, (y - 1) >> 5); cy <= Math.min(CH - 1, (y + 1) >> 5); cy++)
for (let cx = Math.max(0, (x - 1) >> 5); cx <= Math.min(CW - 1, (x + 1) >> 5); cx++) cleatChunks[cy * CW + cx] = 1; for (let cx = Math.max(0, (x - 1) >> 5); cx <= Math.min(CW - 1, (x + 1) >> 5); cx++) cleatChunks[cy * CW + cx] = 1;
} }
} }
// A rib that appears where grains already lie (a belt built under a heap, a loaded save) lifts
// each one to the nearest open cell above it.
function expelFromRib(i) {
for (let q = i - W, k = 0; q >= 0 && k < 24; q -= W, k++) {
if (cleatMask[q] || part[q] || occ[q]) continue;
if (mat[q] === EMPTY || PH[mat[q]] === PH_GAS) { move(i, q, i % W, (i / W) | 0, q % W, (q / W) | 0); return; }
if (PH[mat[q]] !== PH_POWDER) return;
}
}
// A screen shakes only the grains touching its mesh, never the whole column above it. // A screen shakes only the grains touching its mesh, never the whole column above it.
function screenNudge(top, d, pass) { function screenNudge(top, d, pass) {
if (tick % SCREEN_NUDGE_TICKS) return; if (tick % SCREEN_NUDGE_TICKS) return;
+20 -2
View File
@@ -32,7 +32,10 @@ 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);
check(`grade ${grade} carries a generous feed at least 60 grains/s`,r.out>0 && r.out/10>=60,JSON.stringify(r)); // 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;
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));
} }
@@ -141,7 +144,22 @@ for(const grade of [-1,1]) {
} }
fresh(); fresh();
run('placeBlock(BP_BELT_UP,1,20,32,true);'); run('placeBlock(BP_BELT_UP,1,20,32,true);');
check('pocket capacity is bounded by the real cleat height',run('Array.from({length:CLEAT_SPACING},(_,k)=>pocketDepth(80+k,32*BW+20)).every(d=>d>=0 && d<=CLEAT_REACH*2)')); // The ribs hold the load: under a heavy feed no grain is ever inside a rib, and a grain on the bare
// face slides down until a rib stops it, then rides up with that rib.
{
const s=line(-1);
const inRib=run(`(()=>{let worst=0;for(let k=0;k<600;k++){for(let l=1;l<=12;l++){const i=(${s.inlet}*4-l)*W+44;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
simTick();let n=0;for(const c of cleatCells)if(PH[mat[c]]===PH_POWDER)n++;if(n>worst)worst=n;}return worst;})()`);
check('under a heavy feed no grain is ever inside a rib',inRib===0,inRib);
line(-1);
// a free face cell high on the run, halfway between two ribs
const g=run(`(()=>{for(let x=90;x<100;x++){const b=((32-((x>>2)-16))*BW)+(x>>2),y=beltFaceY(blockP[b],x>>2,32-((x>>2)-16),x);
let near=99;for(let h=0;h<CLEAT_SPACING;h++){if(cleatFoot(x-h,b)||cleatFoot(x+h,b)){near=h;break;}}
if(near>=3){setCell((y-1)*W+x,SAND,20,0);return x;}}return -1;})()`);
const path=run(`(()=>{const xs=[];for(let k=0;k<40;k++){simTick();for(let i=0;i<N;i++)if(mat[i]===SAND){xs.push(i%W);break;}}return xs;})()`);
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 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);
+11
View File
@@ -0,0 +1,11 @@
// Downhill screen whose outlet belt sits level with its last block (the jam the owner saw).
globalThis.jointScene = function(level = true) {
const bx = Math.floor((SX - 100) / BS), by = Math.floor((SY + 12) / BS) - 8;
for(let y=by*BS-64;y<by*BS+96;y++)for(let x=(bx-12)*BS;x<(bx+24)*BS;x++){const i=y*W+x;bgm[i]=0;setCell(i,y>=by*BS+88?STONE:EMPTY,20,0);}
money=1e6;weather.next=1e15;
for(let k=0;k<5;k++)placeBlock(BP_FILTER_DOWN,128,bx+k,by+k,true,{m:[GRAVEL],inv:0});
for(let k=1;k<=5;k++)placeBlock(BP_BELT,1,bx-k,by,true);
for(let k=5;k<10;k++)placeBlock(BP_BELT,1,bx+k,by+4+(level?0:1),true);
for(let t=0;t<900;t++){for(let h=1;h<=6;h++){const i=(by*BS-h)*W+(bx-4)*BS+(t%3);if(mat[i]===EMPTY)setCell(i,COAL,20,0);}simTick();}
CAM=[(bx+4)*BS,(by+3)*BS];updateRoof();
};