Every transport piece moves material at belt speed and capacity (t105)

This commit is contained in:
2026-10-30 03:34:39 -04:00
parent e605953661
commit 1740f598eb
20 changed files with 413 additions and 126 deletions
+10
View File
@@ -168,9 +168,19 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
const t = mat[b];
if (PH[t] === PH_POWDER && (!barriers || (!cleatMask[i] && !cleatMask[b])) && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); chunkWake((y >> 5) * CW + (x >> 5), 40); return; }
// Selected grains above a screen sift down; they do not slide onto its outlet belt.
if ((y + SLOPE_CARRY_DEPTH < H && isFilter(blockP[((y + SLOPE_CARRY_DEPTH) >> 2) * BW + (x >> 2)])) ||
isFilter(blockP[(y >> 2) * BW + (x >> 2)])) {
const face = transportBed(i);
if (face >= 0 && part[face] === P_FILTER && filterPassAt(m, face)) return;
}
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
if ((t === BELTL || t === BELTR || part[b] === P_FILTER) &&
!isSlopeBelt(blockP[((y + 1) >> 2) * BW + (x >> 2)])) return;
if(t===BELTL||t===BELTR||part[b]===P_FILTER) {
const owner=((y+1)>>2)*BW+(x>>2),code=blockP[owner];
if(isSlopeBelt(code)&&(isFilter(code)||!slopeUphill(code,blockV[owner])))return;
}
// Feed a receiving screw mouth sideways instead of falling under its bottom casing.
if (t === BELTL || t === BELTR) {
const b = ((y + 1) >> 2) * BW + (x >> 2);
+138 -9
View File
@@ -3,7 +3,8 @@
// 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.
const CARRY_DEPTH = 4;
// Six vertical grains give a 45° face roughly the same depth across its path.
const CARRY_DEPTH = 4, SLOPE_CARRY_DEPTH = 6;
// Find the next carrying face at the same height or one grain above/below it.
function conveyorStep(x, y, d) {
const nx = x + d;
@@ -23,7 +24,8 @@ function carryColumn(a, d, pass) {
if (a < 0 || mat[a] === EMPTY) return;
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d), carrier = (sy >> 2) * BW + ((a % W) >> 2);
let blocked = false;
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
const depth=isSlopeBelt(blockP[carrier])?SLOPE_CARRY_DEPTH:CARRY_DEPTH;
for (let L = 0; L < 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
@@ -35,10 +37,36 @@ function carryColumn(a, d, pass) {
const u = mat[a + W];
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
}
if(pass && pass[m])continue;
if (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d;
if (nx < 0 || nx >= W) return;
const j = a + d + step * W;
let j = a + d + step * W;
// Turn the incoming bed across a screw mouth without squeezing it into one lane.
let sp=part[j];
const down=sy<H?part[sy*W+nx]:0;
if(!sp && IS_SCREW[down] && (SCREW_DY[down]>0 || SCREW_DX[down]) && screwAccepts(down,d,0)) {
sp=down;
j=(sy+(SCREW_DX[sp]?((a/W)|0)&3:0))*W+nx;
}
if(IS_SCREW[sp] && !SCREW_DX[sp]) {
const row=((j/W)|0),lane=d>0?(((a/W)|0)&3):3-(((a/W)|0)&3);
j=((row&~3)+(SCREW_DY[sp]<0?3:0))*W+(nx&~3)+lane;
}
if(IS_SCREW[sp] && SCREW_DX[sp] && SCREW_DY[sp]) {
const by=((j/W)|0)&~3,xx=nx;
for(let yy=by+3;yy>=by;yy--){const target=yy*W+xx;if(roomFor(target)&&!occ[target]){j=target;break;}}
} else if(isSlopeBelt(blockP[((sy+step)>>2)*BW+(nx>>2)]) && !isSlopeBelt(blockP[carrier])) {
const face=(sy+step)*W+nx;
for(let h=1;h<=SLOPE_CARRY_DEPTH;h++){const target=face-h*W;if(target>=0&&roomFor(target)&&!occ[target]){j=target;break;}}
}
const receiver=blockP[((sy+step)>>2)*BW+(nx>>2)];
if(isSlopeBelt(blockP[carrier]) && (receiver===BP_BELT || receiver===BP_FILTER)) {
const face=(sy+step)*W+nx;
let target=-1;for(let h=1;h<=CARRY_DEPTH;h++){const q=face-h*W;if(roomFor(q)&&!occ[q]){target=q;break;}}
if(target<0)return;
j=target;
}
if (j < 0 || j >= N) return;
const tj = mat[j];
// grains on a wet belt push through the water rather than stopping at it
@@ -50,7 +78,10 @@ function carryColumn(a, d, pass) {
const y = (a / W) | 0, bj = j + W;
// carried off the 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, nx, y + step, d * 0.5, step * 0.5);
else move(a, j, x, y, nx, y + step);
else {
move(a,j,x,y,j%W,(j/W)|0);
if(IS_SCREW[part[j]] && SCREW_DX[part[j]])screwSpread(j,j%W,(j/W)|0,d,step);
}
transportNote(carrier, true);
stats.moved++;
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
@@ -81,6 +112,7 @@ function rebuildFilters() {
}
function filterCarry(b, d, carrying = true) {
if (!isFilter(blockP[b])) return;
if(isSlopeBelt(blockP[b]) && !diagonalBeat())return;
const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
if (y < 1) return;
if (!carrying && !isSlopeBelt(code)) return;
@@ -93,16 +125,21 @@ function filterCarry(b, d, carrying = true) {
}
}
}
// Belts and filters share one front-first order, including their flat/slope joins.
// Belts, filters and screws share a front-first order, so receivers clear before feeders.
let screwCarryR=[], screwCarryL=[];
function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
let k = 0, f = 0;
while (k < belts.length || f < filters.length) {
const screws=d>0?screwCarryR:screwCarryL;
let k = 0, f = 0, s=0;
while (k < belts.length || f < filters.length || (carrying && s<screws.length)) {
const bx=k<belts.length?belts[k]%W:(d>0?-1:W);
const fx0=f<filters.length?(filters[f]%BW)*BS+(d>0?BS-1:0):(d>0?-1:W);
if(carrying && s<screws.length && (((screws[s]%W)>>2)-(bx>>2))*d>=0 && (((screws[s]%W)>>2)-(fx0>>2))*d>=0){screwCarry(screws[s++]);continue;}
const fx = f < filters.length ? (filters[f] % BW) * BS + (d > 0 ? BS - 1 : 0) : 0;
if (f < filters.length && (k >= belts.length || (fx - belts[k] % W) * d > 0)) { filterCarry(filters[f++], d, carrying); continue; }
const b = belts[k++];
if (mat[b] !== bm || b < W) continue;
const x = b % W, y = (b / W) | 0, code = blockP[(y >> 2) * BW + (x >> 2)];
if (!carrying && !isSlopeBelt(code)) continue;
if (isSlopeBelt(code) ? !diagonalBeat() : !carrying) continue;
if (isSlopeBelt(code) && beltFaceY(code, x >> 2, y >> 2, x) !== y) continue;
if (slopeSealOwner(b) >= 0) continue;
const above = mat[b - W];
@@ -153,6 +190,15 @@ function screwSpread(c, x, y, sx, sy, intake = false) {
}
return c;
}
// Carrying face below a grain, including the deeper vertical bed on a slope.
function transportBed(i) {
const x=i%W, y=(i/W)|0;
for(let h=1;h<=SLOPE_CARRY_DEPTH && y+h<H;h++) {
const j=i+h*W,b=((y+h)>>2)*BW+(x>>2),c=blockP[b];
if((isSlopeBelt(c)||h<=CARRY_DEPTH)&&(isBelt(c)||isFilter(c))&&beltFaceY(c,x>>2,(y+h)>>2,x)===y+h)return j;
}
return -1;
}
// An empty screw cell draws in loose material from behind it or from its sides
// (not from the cell it pushes into), so a screw end sitting in a pile feeds itself.
function screwIntake(i, p, dx, dy) {
@@ -172,7 +218,7 @@ function screwIntake(i, p, dx, dy) {
if (stamp[i] === tickStamp) return;
const pj = part[j];
if (IS_SCREW[pj]) continue; // other screw cells move their own contents
if (pj === P_GRATE || pj === P_FILTER) continue;
if (pj === P_GRATE || pj === P_FILTER || transportBed(j)>=0) continue;
move(j, i, nx, ny, x, y);
if (diagonal) screwSpread(i, x, y, 0, (y & 3) < 2 ? 1 : -1, true);
else if (ox !== -dx || oy !== -dy) screwSpread(i, x, y, -ox, -oy, true);
@@ -190,6 +236,10 @@ function straightScrewNext(i, p) {
const xx = x + dx, yy = y + dy, next = yy * W + xx;
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
const q = part[next];
if(IS_SCREW[q] && !(SCREW_DX[q] && SCREW_DY[q]) && !!dx !== !!SCREW_DX[q]) {
const bx=xx&~3,by=yy&~3;
return dx ? (by+(SCREW_DY[q]>0?0:3))*W+bx+(y&3) : (by+(x&3))*W+bx+(SCREW_DX[q]>0?0:3);
}
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
@@ -213,3 +263,82 @@ function diagonalScrewNext(i, p) {
}
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
}
// Move one screw cell after the cells ahead of it, including adjoining belts.
function screwCarry(i) {
const p=part[i];
if(!IS_SCREW[p] || occ[i])return;
const dx=SCREW_DX[p], dy=SCREW_DY[p], flat=dy===0;
const m = mat[i];
if (m === EMPTY || PH[m] !== PH_POWDER) { if ((!dx || !dy || diagonalBeat()) && (m === EMPTY || PH[m] !== PH_SOLID)) screwIntake(i, p, dx, dy); return; }
if (stamp[i] === tickStamp) return;
const carrier = (Math.floor(i / W) >> 2) * BW + ((i % W) >> 2);
transportNote(carrier);
if (temp[i] > 1000 && rnd() < 0.02) {
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
return;
}
if(dx && dy && !diagonalBeat())return;
const s = netSat(i);
if (s <= 0 || rnd() > s) return;
const x = i % W, y = (i / W) | 0;
if(flat && !(y&3) && y>0 && PH[mat[i-W]]===PH_POWDER && !part[i-W] && roomFor(i+W) && !occ[i+W]) {
move(i,i+W,x,y,x,y+1);transportNote(carrier,true);return;
}
if(flat && x+dx>=0 && x+dx<W && !roomFor(i+dx) && y+1<H && part[i+W]===p && roomFor(i+W) && !occ[i+W]) {
move(i,i+W,x,y,x,y+1);transportNote(carrier,true);return;
}
if (!flat && !dx) {
let own = 0;
const end = dy < 0 ? (y & ~3) : (y | 3);
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
const side = ((tick >> 1) + i) & 1 ? 1 : -1;
for (let c = 0; c < 2; c++) {
const sx = c ? -side : side, xx = x + sx, j = i + sx;
if ((xx >> 2) !== (x >> 2) || part[j] !== p || !roomFor(j) || mat[j] !== EMPTY || occ[j] || stamp[j] === tickStamp) continue;
let load = 0;
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
// Include this grain: a strict gap prevents a sideways step being undone next beat.
if (own <= load + 1) continue;
move(i,j,x,y,xx,y);stats.moved++;transportNote(carrier,true);return;
}
}
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) return;
let dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
if (dest < 0) return;
const bed=transportBed(dest);
if(bed>=0 && !IS_SCREW[part[dest]]) {
for(let j=bed-W;j>=bed-CARRY_DEPTH*W;j-=W)if(roomFor(j)&&!occ[j]){dest=j;break;}
}
const nx = dest % W, ny = (dest / W) | 0;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) return;
const j = dest;
if (occ[j]) return;
const pj = part[j], screw = IS_SCREW[pj];
if (screw && !screwAccepts(pj, dx, dy)) return;
const t = mat[j];
if (!roomFor(j)) return;
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) return;
if (!screw && !pj && t === EMPTY && (bed < 0 || dx && dy < 0)) {
if (bed >= 0) {
// Clear the raised belt's open edge; landing on its lip lets the load tumble underneath.
const mouth=bed-W-(y&3)*W;
if(!roomFor(mouth)||occ[mouth])return;
launchGrain(i,nx,(mouth/W)|0,dx*0.9,-0.5);
} else {
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
}
stats.moved++; transportNote(carrier, true); if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); return;
}
move(i, j, x, y, nx, ny); stats.moved++; transportNote(carrier, true);
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
if (screw && pj !== p && (dx && dy || SCREW_DX[pj] && SCREW_DY[pj])) {
// turning a corner: keep going across the new screw's width so every lane fills
screwSpread(j, nx, ny, dx, dy);
}
}
+6 -6
View File
@@ -120,9 +120,9 @@ function entScrewTick(e, p) {
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
if (entScrewMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
}
// A horizontal bar stays whole: one diagonal block per four screw beats.
// A horizontal bar stays whole; its block steps follow the same distance clock as grains.
function entDiagonalScrewTick(e, p) {
if (tick & 7) return;
if (!diagonalBeat(tick,4)) return;
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
const edge = (by + lane) * W + bx + (dx > 0 ? BS - 1 : 0);
const dest = diagonalScrewNext(edge, p);
@@ -163,12 +163,12 @@ function entBeltScrewEnter(e) {
function entSlopeBeltTick(e) {
const bottom = e.iy + BAR_H;
if (bottom >= H) return false;
let d = 0, slope = false, screen = false;
let d = 0, slope = false;
for (let x = e.ix; x < e.ix + BAR_W; x++) {
const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
if (isSlopeBelt(c)) slope = true;
}
}
if (!d) return false;
@@ -179,12 +179,12 @@ function entSlopeBeltTick(e) {
const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
if (isSlopeBelt(c)) slope = true;
face = Math.min(face, y);
}
if (!slope) return false;
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
if (screen ? tick % SCREEN_NUDGE_TICKS : tick & 1) return true;
if (!diagonalBeat()) return true;
const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
else if (!entScrewEnter(e, d, 0)) entScrewEnter(e, d, Math.sign(y - e.iy), true);
-64
View File
@@ -94,68 +94,4 @@ function machinesTick() {
slopeCleatsTick();
carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
// 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++) {
if (!IS_SCREW[p]) continue;
const list = screwLists[p], dx = SCREW_DX[p], dy = SCREW_DY[p], flat = dy === 0;
for (let k = 0; k < list.length; k++) {
const i = list[k];
if (part[i] !== p || occ[i]) continue;
const m = mat[i];
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) screwIntake(i, p, dx, dy); continue; }
if (stamp[i] === tickStamp) continue;
const carrier = (Math.floor(i / W) >> 2) * BW + ((i % W) >> 2);
transportNote(carrier);
if (temp[i] > 1000 && rnd() < 0.02) {
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
continue;
}
const s = netSat(i);
if (s <= 0 || rnd() > s) continue;
const x = i % W, y = (i / W) | 0;
// a sideways screw carries its load along the bottom: grains settle into empty lanes below
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
if (!flat && !dx) {
let own = 0, shifted = false;
const end = dy < 0 ? (y & ~3) : (y | 3);
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
const side = ((tick >> 1) + i) & 1 ? 1 : -1;
for (let c = 0; c < 2; c++) {
const sx = c ? -side : side, xx = x + sx, j = i + sx;
if ((xx >> 2) !== (x >> 2) || part[j] !== p || !roomFor(j) || mat[j] !== EMPTY || occ[j] || stamp[j] === tickStamp) continue;
let load = 0;
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
// Include this grain: a strict gap prevents a sideways step being undone next beat.
if (own <= load + 1) continue;
move(i, j, x, y, xx, y); stats.moved++; transportNote(carrier, true); shifted = true; break;
}
if (shifted) continue;
}
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue;
const dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
if (dest < 0) continue;
const nx = dest % W, ny = (dest / W) | 0;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = dest;
if (occ[j]) continue;
const pj = part[j], screw = IS_SCREW[pj];
if (screw && !screwAccepts(pj, dx, dy)) continue;
const t = mat[j];
if (!roomFor(j)) continue;
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue;
if (!screw && !pj && t === EMPTY) {
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
stats.moved++; transportNote(carrier, true); if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); continue;
}
move(i, j, x, y, nx, ny); stats.moved++; transportNote(carrier, true);
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
if (screw && pj !== p) {
// turning a corner: keep going across the new screw's width so every lane fills
screwSpread(j, nx, ny, dx, dy);
}
}
}
}
+22
View File
@@ -44,6 +44,28 @@ function rebuildPower() {
if (!net) continue;
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
}
screwCarryR=[];screwCarryL=[];
for(const p of SCREW_PARTS)for(const i of screwLists[p]) {
let d=SCREW_DX[p];
if(!d){
const x=i%W,y=(i/W)|0,by=y>>2;
// A vertical run follows its outlet corner, or the belt feeding its side.
for(let yy=by;yy>=0&&yy<BH;yy+=SCREW_DY[p]) {
const b=yy*BW+(x>>2);
if(blockP[b]!==BP_SCREW)break;
const v=blockV[b];if(SCREW_DIRS[v][0]){d=SCREW_DIRS[v][0];break;}
if((x>>2)>0 && isBelt(blockP[b-1]) && (blockV[b-1]&1))d=1;
if((x>>2)+1<BW && isBelt(blockP[b+1]) && !(blockV[b+1]&1))d=-1;
}
}
(d<0?screwCarryL:screwCarryR).push(i);
}
const vertical=new Int8Array(BW);
for(const p of SCREW_PARTS)if(!SCREW_DX[p])for(const i of screwLists[p])vertical[(i%W)>>2]=SCREW_DY[p];
const order=(a,b,d)=>(((a%W)>>2)-((b%W)>>2))*d ||
(((a/W)>>2)-((b/W)>>2))*(vertical[(a%W)>>2]||1)*-1 ||
((a%W)-(b%W))*d || (SCREW_DY[part[a]]<0?a-b:b-a);
screwCarryR.sort((a,b)=>order(a,b,-1));screwCarryL.sort((a,b)=>order(a,b,1));
beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a);
for (const p of SCREW_PARTS) {
screwLists[p].sort((a, b) => {
+3 -3
View File
@@ -73,11 +73,11 @@ const PIECES = [
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally for a sidewall belt. Moving cleats lift pockets of grains; extra grains roll back.' },
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally for a sidewall belt. Moving cleats lift pockets of grains at belt pace; extra grains roll back.' },
{ id: 'screw', code: BP_SCREW, name: 'Screw conveyor', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
desc: 'A turning screw inside a tube that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single screw conveyor. Join either end to screw conveyors or belts. Copper softens around 1000°C.' },
desc: 'A turning screw inside a tube that draws in loose grains and whole bars and pushes them along at belt pace, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single screw conveyor. Join either end to screw conveyors or belts. Copper softens around 1000°C.' },
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
desc: 'A shaking mesh that joins belts. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally for a downhill screen. Grains slide down while it shakes; R keeps a picked slope downhill.' },
desc: 'A shaking mesh that joins belts. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally for a downhill screen. Grains and bars travel downhill at belt pace while it shakes; R keeps a picked slope downhill.' },
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
+12 -17
View File
@@ -4,9 +4,12 @@
// 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.
const CLEAT_SPACING = 8, CLEAT_HEIGHT = 6; // spacing in face cells, height in grains
const CLEAT_SPACING = 8, CLEAT_HEIGHT = 10; // 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
// Three diagonal face steps per eight ticks: 31.8 cells/s along a 45° path.
function diagonalBeat(t = tick, blocks = 1) {
return !(t & 1) && Math.floor(t*3/(8*blocks)) !== Math.floor((t-2)*3/(8*blocks));
}
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
@@ -55,9 +58,9 @@ function pushRow(p, dx, dy) {
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.
// feeding belt behind it, about 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++) {
for (let k = 1; k <= CLEAT_SPACING + 1; 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--) {
@@ -137,7 +140,7 @@ function slopeCleatsTick(advance = true) {
if (slopePhase[b] !== p) { slopePhase[b] = p; resynced = true; }
}
if (resynced) rebuildCleatMask();
if (advance && !(tick & 1)) {
if (advance && diagonalBeat()) {
// 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.
@@ -171,7 +174,7 @@ function slopeCleatsTick(advance = true) {
}
}
rebuildCleatMask();
if (advance && !(tick & 1)) {
if (advance && diagonalBeat()) {
// 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) {
@@ -215,17 +218,9 @@ function expelFromRib(i) {
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;
const i = top - W, m = mat[i];
if (i < 0 || PH[m] !== PH_POWDER || stamp[i] === tickStamp || occ[i]) return;
const carrier = ((top / W) >> 2) * BW + ((top % W) >> 2);
transportNote(carrier);
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) { transportNote(carrier, true); return; }
const j = i + d + W;
if (i % W + d < 0 || i % W + d >= W || j >= N) return;
if (enter(m, PH_POWDER, j, d, 1)) { move(i, j, i % W, (i / W) | 0, j % W, (j / W) | 0); transportNote(carrier, true); if (flowOn) flowCross(carrier, i, j); }
// A screen shakes a finite bed, on the same distance clock as the other diagonal carriers.
function screenNudge(top,d,pass) {
if(diagonalBeat())carryColumn(top-W,d,pass);
}
// Only loose load can react to a barrier appearing or disappearing.
+4 -3
View File
@@ -12,14 +12,15 @@ function transportClock(b) { return transportState.get(b)?.clock ?? tick; }
function transportBegin() {
for (const [b, s] of transportState) {
if (blockP[b] !== s.code || blockV[b] !== s.v || !pieceEnds(s.code, s.v).length) { transportState.delete(b); continue; }
if (isFilter(s.code) && isSlopeBelt(s.code) && tick % SCREEN_NUDGE_TICKS) continue;
if ((isSlopeBelt(s.code)||s.code===BP_SCREW&&s.v>=4) && !diagonalBeat()) continue;
s.loaded = s.moved = s.jam = false;
}
}
function transportEnd() {
for (const s of transportState.values()) {
const cadence = isFilter(s.code) && isSlopeBelt(s.code) ? SCREEN_NUDGE_TICKS : 2;
if (tick % cadence) continue;
const diagonal=isSlopeBelt(s.code)||s.code===BP_SCREW&&s.v>=4;
if (diagonal ? !diagonalBeat() : tick&1)continue;
const cadence=diagonal&&!diagonalBeat(tick-2)?4:2;
s.age = s.jam || (s.loaded && !s.moved) ? Math.min(STALL_TICKS, s.age + cadence) : 0;
if (s.age < STALL_TICKS) s.clock += cadence;
}