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:
+1
-1
@@ -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)?
|
||||
// 0 = no, 1 = into empty, 2 = by displacing what is there.
|
||||
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);
|
||||
}
|
||||
// Powder already knows whether its chunk has barriers; other callers use enter.
|
||||
|
||||
@@ -19,11 +19,11 @@ function conveyorStep(x, y, d) {
|
||||
const b = (y >> 2) * BW + (x >> 2);
|
||||
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;
|
||||
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d);
|
||||
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;
|
||||
const m = mat[a];
|
||||
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 (carrying) {
|
||||
if (PH[above] === PH_LIQUID) beltDrain(b);
|
||||
if (isSlopeBelt(code)) {
|
||||
cleatCarrying = true;
|
||||
carryColumn(b - W, d, null, pocketDepth(x, (y >> 2) * BW + (x >> 2)));
|
||||
cleatCarrying = false;
|
||||
} else carryColumn(b - W, d, null);
|
||||
if (!isSlopeBelt(code)) carryColumn(b - W, d, null); // on a slope, the cleats do the carrying
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,9 +89,10 @@ function machinesTick() {
|
||||
}
|
||||
// Belts step every other tick; sloped belts carry only their finite pockets.
|
||||
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(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
|
||||
// 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++) {
|
||||
|
||||
+178
-24
@@ -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_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
|
||||
let cleatCarrying = false;
|
||||
let cleatChunks = 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]) {
|
||||
const d = blockV[b] & 1 ? 1 : -1;
|
||||
return ((x - d * phase) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING === 0;
|
||||
}
|
||||
function pocketDepth(x, b) {
|
||||
const g = beltGrade(blockP[b]), d = blockV[b] & 1 ? 1 : -1;
|
||||
// A periodic pocket envelope, clipped to two vertical projections of the cleat.
|
||||
// End pockets keep moving while their cleat rounds the end of the band.
|
||||
const distance = ((g * (d * slopePhase[b] - x)) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING;
|
||||
return Math.max(0, CLEAT_REACH * 2 - distance);
|
||||
// The rib standing on face cell (x, y) of a slope with grade g, foot first. Where its end would
|
||||
// reach into something built (a screw conveyor, the next belt, a wall), the rib stops short: -1.
|
||||
function ribCells(x, y, g) {
|
||||
const out = [];
|
||||
let open = true;
|
||||
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) {
|
||||
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); }
|
||||
cleatChunks.fill(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) {
|
||||
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;
|
||||
const d = blockV[b] & 1 ? 1 : -1, g = beltGrade(c), running = netSat(i) > 0;
|
||||
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;
|
||||
if (!isBelt(c) || !isSlopeBelt(c) || 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) {
|
||||
cleatMask[i] = 1; wakeCleatLoad(i);
|
||||
if (PH[mat[i]] === PH_POWDER && !occ[i]) expelFromRib(i);
|
||||
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 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.
|
||||
function screenNudge(top, d, pass) {
|
||||
if (tick % SCREEN_NUDGE_TICKS) return;
|
||||
|
||||
Reference in New Issue
Block a user