Belts carry 4 grains deep; stalled belts warn and stop animating (t45)

This commit is contained in:
2026-10-29 23:21:27 -04:00
parent 99aae0aae0
commit ac5f5a06d1
27 changed files with 335 additions and 51 deletions
+1
View File
@@ -23,6 +23,7 @@
"sim/joins.js",
"sim/conveyors.js",
"sim/flow.js",
"sim/transport-status.js",
"sim/slope-conveyors.js",
"sim/blocks.js",
"sim/molds.js",
+10 -2
View File
@@ -15,7 +15,8 @@ function cellPixel(x, y) {
return;
}
const useFog = SET.fog, grain = SET.grain, incand = SET.incandescence;
const anim = R.frame, lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
let anim = R.frame;
const lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
const glowOn = SET.glow !== 'off' && !lens, gpx = R.glowPx, gw = R.glowW;
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
const yw = y * W;
@@ -36,6 +37,8 @@ function cellPixel(x, y) {
}
}
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
const ownBlock = (y >> 2) * BW + (x >> 2);
if (isBelt(blockP[ownBlock]) || isFilter(blockP[ownBlock]) || blockP[ownBlock] === BP_SCREW) anim = transportClock(ownBlock);
const T = temp[i];
// Also used by the thermal lens, which always draws in pixels.
let looseShade = 1;
@@ -138,6 +141,7 @@ function cellPixel(x, y) {
const db = diagonalScrewBlock(x, y, sm);
let diagonal = false;
if (db >= 0) {
anim = transportClock(db);
const v = blockV[db], joins = diagonalJoins[db];
const sd = diagonalScrewShape(x + 0.5, y + 0.5, db, v, joins);
p = sm || sd <= 0 ? SCREW_PARTS[v] : 0;
@@ -193,7 +197,7 @@ function cellPixel(x, y) {
}
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
if (slopeArt && !sm) {
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
const color = slopeBeltColor(sb, x, y, transportClock(sb), br, bgc, bb);
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
}
const slopeDetail = slopeDetailAt(i, x, y, sm);
@@ -272,5 +276,9 @@ function cellPixel(x, y) {
bpx[row + bx] = fogColor(c, k); tpx[row + bx] = fogColor(t, k);
}
}
if (sm && GPU.motionData) {
const owner = slopeDetail && slopeWallOwner >= 0 ? slopeWallOwner : sb >= 0 ? sb : db >= 0 ? db : ownBlock;
GPU.motionData[row + bx] = transportClock(owner) / 60;
}
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
}
+2
View File
@@ -7,6 +7,8 @@ function gpuOverrides() {
if (R.smoothOn) {
if (!GPU.overrideMask || GPU.overrideMask.length !== R.px.length) GPU.overrideMask = new Uint8Array(R.px.length);
GPU.overrideMask.fill(0);
if (!GPU.motionData || GPU.motionData.length !== R.px.length) GPU.motionData = new Float32Array(R.px.length);
else GPU.motionData.fill(0);
}
if (!GPU.blocks || GPU.blocks.length !== NB) GPU.blocks = new Uint8Array(NB);
const marked = GPU.blocks; marked.fill(0);
+8 -6
View File
@@ -10,6 +10,7 @@ struct Params { view: vec4f, world: vec4f, style: vec4f, effects: vec4f, camera:
@group(0) @binding(6) var tGlow: texture_2d<f32>;
@group(0) @binding(7) var linear: sampler;
@group(0) @binding(8) var<uniform> su: vec4f;
@group(0) @binding(9) var tMotion: texture_2d<f32>;
fn sampleLevel(t: texture_2d<f32>, uv: vec2f, level: f32) -> vec4f { return textureSampleLevel(t,linear,uv,level); }
fn modulo(x: f32,y: f32) -> f32 { return x-y*floor(x/y); }
fn h21(inputP: vec2f) -> f32 { var p=inputP; p = fract(p * vec2f(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
@@ -51,9 +52,9 @@ fn beltLineSD(c: vec2i, lb: vec2f) -> f32 {
return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
}
fn bandRidge(w: vec2f, lb: vec2f, dir: f32) -> f32 {
fn bandRidge(w: vec2f, lb: vec2f, dir: f32, time: f32) -> f32 {
var run: f32 = select( -1.0, 1.0 , lb.y < 2.0 );
return step(0.72, fract((w.x - dir * run * (su.x) * 30.0) * 0.5));
return step(0.72, fract((w.x - dir * run * time * 30.0) * 0.5));
}
fn bandColor(lb: vec2f, rg: f32) -> vec3f { return mix(vec3f(0.2, 0.21, 0.24), vec3f(0.34, 0.35, 0.39), rg) * (select( 0.72, 1.0 ,lb.y < 2.0 )); }
@@ -72,6 +73,7 @@ ${GPU_SMOOTH_SLOPE_HELPERS}
var qq: vec2f = q - 0.5; var c0: vec2i = vec2i(floor(qq)); var t: vec2f = qq - floor(qq);
var k0: vec4f = cellAt(tCol, c0); var k1: vec4f = cellAt(tCol, c0 + vec2i(1, 0)); var k2: vec4f = cellAt(tCol, c0 + vec2i(0, 1)); var k3: vec4f = cellAt(tCol, c0 + vec2i(1, 1));
var n0: i32 = cls(k0); var n1: i32 = cls(k1); var n2: i32 = cls(k2); var n3: i32 = cls(k3);
let motionTime = cellAt(tMotion, c).r;
var own: vec4f = cellAt(tCol, c); var nc: i32 = cls(own);
var s: vec2f = smoothstep(vec2f(0.1), vec2f(0.9), t);
var wq: vec4f = vec4f((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
@@ -228,14 +230,14 @@ ${GPU_SMOOTH_SLOPE_DRAW}
var edge: f32 = -sd;
var bc: vec3f;
if (edge < 0.8) {
bc = bandColor(lb, bandRidge(w, lb, dir));
bc = bandColor(lb, bandRidge(w, lb, dir, motionTime));
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
} else {
var rc: vec2f = vec2f(select( 3.0, 1.0 ,lb.x < 2.0 ), 2.0); var dv: vec2f = lb - rc;
var rd: f32 = length(dv);
bc = vec3f(0.15, 0.16, 0.18);
if (rd < 0.95) {
var ang: f32 = atan2(dv.y, dv.x) - dir * (su.x) * 30.0 / 0.95;
var ang: f32 = atan2(dv.y, dv.x) - dir * motionTime * 30.0 / 0.95;
var spoke: f32 = smoothstep(0.55, 0.85, cos(ang * 3.0));
bc = mix(vec3f(0.5, 0.36, 0.25), vec3f(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2f(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
@@ -294,7 +296,7 @@ ${GPU_SMOOTH_SLOPE_DRAW}
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
} else {
var full: bool = own.r + own.g + own.b > 0.0;
var fl: f32 = fract((along + a * 0.9 - sp2 * (su.x) * 14.0) * 0.5);
var fl: f32 = fract((along + a * 0.9 - sp2 * motionTime * 14.0) * 0.5);
var flight: f32 = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
var bore: vec3f = vec3f(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
tc = select( bore, own.rgb * (0.9 + 0.1 * vn(w * 1.3)) ,full );
@@ -343,7 +345,7 @@ ${GPU_SMOOTH_SLOPE_DRAW}
var hole: f32 = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
var fc: vec3f;
if (edge < 0.8) {
var rg: f32 = bandRidge(w, lb, dir);
var rg: f32 = bandRidge(w, lb, dir, motionTime);
var band: vec3f = bandColor(lb, rg);
if (tinted && (carry == (lb.y < 2.0))) { band = mix(band, tag.rgb * (select( 0.8, 1.0 ,lb.y < 2.0 )), 0.75 * rg); }
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
+1 -1
View File
@@ -76,7 +76,7 @@ const GPU_SMOOTH_SLOPE_DRAW = ` if (nc == 31 || nc == 32) {
var local: vec2f = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
var shapePos: vec3f = slopeShape(local, w - local, grade, joins);
var sd: f32 = shapePos.x; var axis: f32 = shapePos.y; var depth: f32 = shapePos.z;
var time: f32 = select( 0.0, (su.x) , !mesh && (joins & 16) != 0 );
var time: f32 = select( 0.0, motionTime , !mesh && (joins & 16) != 0 );
var lb: vec2f = vec2f(axis, depth); var alongWorld: vec2f = vec2f(axis, depth);
var rg: f32 = step(0.72, fract((axis - dir * (select( -1.0, 1.0 ,depth < 2.0 )) * time * 30.0) * 0.5));
var belt: vec3f;
+4 -2
View File
@@ -19,16 +19,17 @@ async function gpuSmoothInit() {
function gpuSmoothTextures() {
const w=R.bw,h=R.bh;
GPU.smoothOver=Array.from({length:3},()=>gpuTexture(w,h));
GPU.motion=gpuTexture(w,h,'r32float');
GPU.smoothMask=gpuTexture(w,h,'r8uint');
GPU.backTex=gpuTexture(w,h,'rgba8unorm',true);
GPU.treeTex=gpuTexture(w,h,'rgba8unorm',true);
GPU.fieldTex=GPU.device.createTexture({size:[w,h],format:'rgba8unorm',mipLevelCount:Math.min(4,1+Math.floor(Math.log2(Math.max(w,h)))),usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING|GPUTextureUsage.RENDER_ATTACHMENT});
GPU.viewTextures.push(...GPU.smoothOver,GPU.smoothMask,GPU.backTex,GPU.treeTex,GPU.fieldTex);
GPU.viewTextures.push(...GPU.smoothOver,GPU.smoothMask,GPU.motion,GPU.backTex,GPU.treeTex,GPU.fieldTex);
}
function gpuSmoothBindings(group,view,buffer) {
const colourResources=[buffer(GPU.uniform),buffer(GPU.lut),buffer(GPU.land),...GPU.worldTextures.slice(0,9).map(view),view(GPU.override),view(GPU.overrideGlow),view(GPU.colourTex),null,view(GPU.backTex),view(GPU.treeTex),GPU.fieldTex.createView({baseMipLevel:0,mipLevelCount:1}),...GPU.smoothOver.map(view),view(GPU.smoothMask)];
GPU.bindColourSmooth=GPU.device.createBindGroup({layout:GPU.colourSmooth.getBindGroupLayout(0),entries:colourResources.map((resource,binding)=>({binding,resource})).filter(e=>e.resource)});
GPU.bindSmooth=group(GPU.smooth,[buffer(GPU.uniform),view(GPU.colourTex),view(GPU.backTex),view(GPU.treeTex),view(GPU.fieldTex),view(GPU.lightTex),view(GPU.glowTex),GPU.sampler,buffer(GPU.smoothUniform)]);
GPU.bindSmooth=group(GPU.smooth,[buffer(GPU.uniform),view(GPU.colourTex),view(GPU.backTex),view(GPU.treeTex),view(GPU.fieldTex),view(GPU.lightTex),view(GPU.glowTex),GPU.sampler,buffer(GPU.smoothUniform),view(GPU.motion)]);
GPU.fieldMips=[];
for(let i=1;i<GPU.fieldTex.mipLevelCount;i++) GPU.fieldMips.push({
bind:group(GPU.fieldMip,[GPU.fieldTex.createView({baseMipLevel:i-1,mipLevelCount:1}),GPU.sampler]),
@@ -36,6 +37,7 @@ function gpuSmoothBindings(group,view,buffer) {
});
}
function gpuSmoothUpload() {
gpuWrite(GPU.motion,GPU.motionData,R.bw,0,0,R.bw,R.bh);
for(const [i,a] of [R.backPx,R.treePx,R.fieldPx].entries()) gpuWrite(GPU.smoothOver[i],a,R.bw,0,0,R.bw,R.bh);
gpuWrite(GPU.smoothMask,GPU.overrideMask,R.bw,0,0,R.bw,R.bh);
GPU.device.queue.writeBuffer(GPU.smoothUniform,0,new Float32Array([(performance.now()-SM.t0)/1000,smoothSurface(),SET.glow==='off'?0:SET.glow==='high'?0.9:0.55,0]));
+18
View File
@@ -368,3 +368,21 @@ function drawParticles() {
ctx.fillRect(s.x, s.y, sz, sz);
}
}
// Always visible, in both looks: amber warning on each stopped transport block.
function drawTransportStalls() {
const ctx = R.ctx, v = viewCells(), s = BS * R.zp;
ctx.save();
for (const [b] of transportState) {
if (!transportStalled(b)) continue;
const x = b % BW * BS, y = Math.floor(b / BW) * BS;
if (x + BS < R.camX || x > R.camX + v.w || y + BS < R.camY || y > R.camY + v.h || (SET.fog && fogSeen(x + 2, y + 2) < 64)) continue;
const p = worldToScreenPx(x, y);
ctx.fillStyle = 'rgba(255,164,48,0.25)'; ctx.fillRect(p.x, p.y, s, s);
ctx.strokeStyle = '#ffbd59'; ctx.lineWidth = Math.max(1, R.zp * 0.25); ctx.strokeRect(p.x, p.y, s, s);
ctx.fillStyle = '#ffdc85';
ctx.fillRect(p.x + s * 0.45, p.y + s * 0.15, s * 0.1, s * 0.4);
ctx.fillRect(p.x + s * 0.45, p.y + s * 0.7, s * 0.1, s * 0.1);
}
ctx.restore();
}
+1 -1
View File
@@ -33,7 +33,7 @@ function prepareSlopeBelts(x0, y0, x1, y1) {
if (c === BP_BELT || c === BP_FILTER) beltCaps[b] = flatBeltCaps(b);
if (!isSlopeBelt(c)) continue;
const face = beltFaceY(c, bx, by, bx * BS) * W + bx * BS;
slopeBeltJoins[b] = slopeBeltConnections(bx, by, c) | (netSat(face) > 0 ? 16 : 0) | (filterCfg.get(b)?.inv ? 32 : 0) | (isFilter(c) && netSat(face) > 0 && screenLoaded(b) && (tick & 2) ? 64 : 0);
slopeBeltJoins[b] = slopeBeltConnections(bx, by, c) | (netSat(face) > 0 ? 16 : 0) | (filterCfg.get(b)?.inv ? 32 : 0) | (isFilter(c) && netSat(face) > 0 && screenLoaded(b) && !transportStalled(b) && (tick & 2) ? 64 : 0);
for (let yy = Math.max(0, by - 2); yy <= Math.min(BH - 1, by + 1); yy++)
for (let xx = Math.max(0, bx - 1); xx <= Math.min(BW - 1, bx + 1); xx++) slopeBeltHalo[yy * BW + xx] = 1;
}
+6 -4
View File
@@ -1,9 +1,9 @@
// 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
// What rests on a belt or filter rides along: the bottom four grains 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.
const CARRY_DEPTH = 96;
const CARRY_DEPTH = 4;
// Find the next carrying face at the same height or one grain above/below it.
function conveyorStep(x, y, d) {
const nx = x + d;
@@ -28,11 +28,12 @@ function carryColumn(a, d, pass) {
const m = mat[a];
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
if (PH[m] !== PH_POWDER || stamp[a] === tickStamp || occ[a]) continue;
transportNote(carrier);
if (pass && pass[m]) {
if (enter(m, PH_POWDER, a + W, 0, 1)) continue; // falls through on its own
if (enter(m, PH_POWDER, a + W, 0, 1)) { transportNote(carrier, true); continue; } // falls through on its own
// the rollers shake the load: what the filter passes works its way down through the rest
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); continue; }
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 (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d;
@@ -50,6 +51,7 @@ function carryColumn(a, d, pass) {
// 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);
transportNote(carrier, true);
stats.moved++;
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
}
+2 -1
View File
@@ -192,7 +192,7 @@ function entSlopeBeltTick(e) {
}
function entTick() {
for (let k = ents.length - 1; k >= 0; k--) {
const e = ents[k];
const e = ents[k], oldX = e.ix, oldY = e.iy;
entMark(e, 0);
entCurM = e.m;
let ix = e.ix; const iy = e.iy;
@@ -264,6 +264,7 @@ function entTick() {
continue;
}
}
if (!(tick & 1)) transportBar(oldX, oldY, e.ix !== oldX || e.iy !== oldY);
entMark(e, 1);
}
entCurM = 0;
+6 -4
View File
@@ -104,6 +104,8 @@ function machinesTick() {
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' }); }
@@ -113,7 +115,7 @@ function machinesTick() {
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); continue; }
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);
@@ -126,7 +128,7 @@ function machinesTick() {
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++; shifted = true; break;
move(i, j, x, y, xx, y); stats.moved++; transportNote(carrier, true); shifted = true; break;
}
if (shifted) continue;
}
@@ -145,9 +147,9 @@ function machinesTick() {
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++; if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); continue;
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++;
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
+11 -5
View File
@@ -39,7 +39,8 @@ function slopeFaceAt(x, y, g) {
function pushRow(p, dx, dy) {
const row = [];
let q = p;
for (let k = 0; k < CARRY_DEPTH; k++) {
// A pushed chain can cross pockets; its length is independent of the flat carrying depth.
for (let k = 0; k < 96; 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;
@@ -162,8 +163,11 @@ function slopeCleatsTick(advance = true) {
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;
if ((x & 3) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c, x >> 2, y >> 2, x) === y && netSat(i) > 0) {
const jam = stalled.has(slopeRunKey(b, memo));
if (slopeUphill(c, blockV[b])) transportCleat(b, jam);
if (!jam) slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
}
}
}
rebuildCleatMask();
@@ -216,10 +220,12 @@ 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;
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) 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); if (flowOn) flowCross(((top / W) >> 2) * BW + ((top % W) >> 2), i, j); }
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); }
}
// Only loose load can react to a barrier appearing or disappearing.
+2
View File
@@ -6,10 +6,12 @@ function simTick() {
if (loadWakeOrder) loadWakeTick();
if (tick % 20 === 1) updateDirtyRoof();
weatherTick();
if (!(tick & 1)) transportBegin();
machinesTick();
pipesTick();
pulseTick();
entTick();
if (!(tick & 1)) transportEnd();
if (flyers.length) flyTick();
if (tick % 8 === 0) wakeBotPowder();
simMove();
+38
View File
@@ -0,0 +1,38 @@
// Local backpressure: only loaded transport that cannot advance warns after half a second.
const STALL_TICKS = 30;
const transportState = new Map();
function transportNote(b, moved = false) {
if (b < 0 || b >= NB) return;
let s = transportState.get(b);
if (!s) { s = { age: 0, clock: tick, loaded: false, moved: false, jam: false, code: blockP[b], v: blockV[b] }; transportState.set(b, s); }
s.loaded = true; s.moved ||= moved;
}
function transportStalled(b) { return (transportState.get(b)?.age || 0) >= STALL_TICKS; }
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;
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;
s.age = s.jam || (s.loaded && !s.moved) ? Math.min(STALL_TICKS, s.age + cadence) : 0;
if (s.age < STALL_TICKS) s.clock += cadence;
}
}
// A whole cleated run rolls back together; friction or a bar must not hide that failure.
function transportCleat(b, jam) {
transportNote(b, !jam);
transportState.get(b).jam = jam;
}
function transportBar(x, y, moved) {
for (let yy = y; yy <= y + BAR_H && yy < H; yy++) for (let xx = x; xx < x + BAR_W && xx < W; xx++) {
if (xx < 0 || yy < 0) continue;
const b = (yy >> 2) * BW + (xx >> 2), c = blockP[b];
if (c === BP_SCREW || isBelt(c) || isFilter(c)) transportNote(b, moved);
}
}
+1
View File
@@ -46,6 +46,7 @@ function sharedArray(Type, length) {
}
function alloc(w, h) {
resetDevConsole();
transportState.clear();
botTrailHead = botTrailCount = 0; botEscape = false;
W = w; H = h; N = w * h;
fogAlloc();
+5
View File
@@ -142,6 +142,11 @@ function hoverInfo(ax, ay) {
warn = 'near heat limit'; t2.push(`${Math.round(screwLoadHeat(jb))}°C load · melts above 1000°C`);
}
}
const stalledBlock = slopeSealOwner(i) >= 0 ? slopeSealOwner(i) : jb;
if (transportStalled(stalledBlock)) {
warn = 'stalled';
t2.push('Stalled: load cannot move. Clear the outlet or the top of the heap.');
}
if (GAME.infoOverlay && !id && !bar && pieceEnds(blockP[jb], blockV[jb]).length) t2.push(`moving ${(flowRate[jb] || 0).toFixed(1)} grains/s`);
for (let end = 0; end < 2 && !id && !bar; end++) {
const miss = nearMiss(jb % BW, (jb / BW) | 0, end);
+1
View File
@@ -80,6 +80,7 @@ function frame(now) {
if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); }
drawJoinMarks();
drawScrewHeat();
drawTransportStalls();
drawPing();
drawHoveredVein();
drawPingRecharge();