Screw tubes carry bars, one whole bar at a time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 00:09:33 -04:00
co-authored by Claude Opus 5.5
parent 9340bc132b
commit 9df161046d
10 changed files with 201 additions and 33 deletions
+1
View File
@@ -106,6 +106,7 @@ function tubeSpread(c, x, y, sx, sy, intake = false) {
// An empty screw cell draws in loose material from behind it or from its sides
// (not from the cell it pushes into), so a tube end sitting in a pile feeds itself.
function tubeIntake(i, p, dx, dy) {
if (occ[i]) return;
const x = i % W, y = (i / W) | 0;
const cands = [[-dx, -dy], [dy, dx], [-dy, -dx]];
const start = (tick + i) % 3;
+93 -27
View File
@@ -32,41 +32,107 @@ function spawnBar(m, x, y, vx, vy, T) {
return e;
}
function removeEnt(e) { entMark(e, 0); const k = ents.indexOf(e); if (k >= 0) ents.splice(k, 1); }
// No transient tube state: even a loaded bar derives its ride from its eight cells.
function entTubeAt(x, y) {
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return 0;
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
const p = part[yy * W + xx]; if (p < P_TU || p > P_TR) return 0;
}
return part[(y + 1) * W + x + 1];
}
function entTubeFree(x, y, tubeOnly = false) {
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
const j = yy * W + xx, p = part[j], tube = p >= P_TU && p <= P_TR;
// gas and liquid don't stop a bar, in a tube or in the open
if (occ[j] || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS && PH[mat[j]] !== PH_LIQUID) || (tubeOnly ? !tube : !tube && entSolidCell(j))) return false;
}
return true;
}
// Check the swept rectangle too: a snap must never jump through a grain or another bar.
function entTubeMove(e, x, y, tubeOnly = false) {
if (!entTubeFree(x, y, tubeOnly)) return false;
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
}
function entTubeEnter(e, dx, dy) {
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue;
const p = part[yy * W + xx]; if (p < P_TU || p > P_TR || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
const x = (xx & ~3), y = Math.max(yy & ~3, Math.min((yy & ~3) + 2, e.iy));
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
}
return false;
}
function entTubeTick(e, p) {
e.vx = e.vy = 0; e.x = e.ix; e.y = e.iy;
const ci = (e.iy + 1) * W + e.ix + 1;
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
const dx = TDX[p], dy = TDY[p];
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
if (xx < 0 || yy < 0 || xx >= W || yy >= H) return;
const q = part[yy * W + xx];
if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; }
}
if (next) {
if (TDX[next] === -dx && TDY[next] === -dy) return;
x = ax & ~3; y = Math.max(ay & ~3, Math.min((ay & ~3) + 2, e.iy));
if (entTubeMove(e, x, y, true)) stats.moved++;
return;
}
if (entTubeAt(x, y)) { if (entTubeMove(e, x, y, true)) stats.moved++; return; }
// Clear the mouth in one move so the next tick can derive ordinary falling from position.
x = e.ix + dx * BAR_W; y = e.iy + dy * BAR_H;
if (!entTubeFree(x, y) || entTubeAt(x, y)) return;
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 (entTubeMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
}
function entTick() {
for (let k = ents.length - 1; k >= 0; k--) {
const e = ents[k];
entMark(e, 0);
entCurM = e.m;
const ix = e.ix, iy = e.iy;
// what is underneath?
let belt = 0, bsat = 0, support = iy + BAR_H >= H;
if (!support) {
const yb = iy + BAR_H;
for (let dx = 0; dx < BAR_W; dx++) {
const x = ix + dx; if (x < 0 || x >= W) continue;
const j = yb * W + x, m = mat[j];
if (m === BELTL || m === BELTR) { const s = netSat(j); if (s > bsat) { bsat = s; belt = m === BELTR ? 1 : -1; } }
else if (part[j] === P_FILTER && !filterPassAt(e.m, j) && (yb & 3) === 0) { bsat = 1; belt = blockV[(yb >> 2) * BW + (x >> 2)] & 128 ? 1 : -1; }
if (entSolidCell(j)) support = true;
const tube = entTubeAt(ix, iy);
if (tube) entTubeTick(e, tube);
else {
// what is underneath?
let belt = 0, bsat = 0, support = iy + BAR_H >= H;
if (!support) {
const yb = iy + BAR_H;
for (let dx = 0; dx < BAR_W; dx++) {
const x = ix + dx; if (x < 0 || x >= W) continue;
const j = yb * W + x, m = mat[j];
if (m === BELTL || m === BELTR) { const s = netSat(j); if (s > bsat) { bsat = s; belt = m === BELTR ? 1 : -1; } }
else if (part[j] === P_FILTER && !filterPassAt(e.m, j) && (yb & 3) === 0) { bsat = 1; belt = blockV[(yb >> 2) * BW + (x >> 2)] & 128 ? 1 : -1; }
if (entSolidCell(j)) support = true;
}
}
const ci = Math.min(H - 1, iy + 1) * W + Math.min(W - 1, ix + 1);
const inLiq = PH[mat[ci]] === PH_LIQUID;
if (support) { if (e.vy > 0) e.vy = 0; e.vx = belt ? belt * 0.5 * bsat : e.vx * 0.55; }
else { e.vy = Math.min(inLiq ? 0.35 : 1.3, e.vy + 0.07); e.vx *= inLiq ? 0.8 : 0.985; }
if (belt && bsat > 0) stats.moved += 0.05;
e.x += e.vx; e.y += e.vy;
// step toward the new rounded position one cell at a time
let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy;
while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else if (belt && entTubeEnter(e, s, 0)) { cx = e.ix; cy = e.iy; e.x = cx; e.y = cy; break; } else { e.vx = 0; e.x = cx; break; } }
while (cy !== ty) { const s = Math.sign(ty - cy); if (entTubeAt(cx, cy)) break; if (entFreeAt(cx, cy + s)) cy += s; else if (entTubeEnter(e, 0, s)) { cx = e.ix; cy = e.iy; break; } else { e.vy = 0; e.y = cy; break; } }
if (support && !belt && entTubeEnter(e, 0, 1)) { cx = e.ix; cy = e.iy; }
if (!entTubeAt(cx, cy) && !entFreeAt(cx, cy)) {
// something moved into us (a grain fell in): try to step up out of it
let freed = false;
for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; }
if (!freed) e.y = cy;
}
e.ix = cx; e.iy = cy;
}
const ci = Math.min(H - 1, iy + 1) * W + Math.min(W - 1, ix + 1);
const inLiq = PH[mat[ci]] === PH_LIQUID;
if (support) { if (e.vy > 0) e.vy = 0; e.vx = belt ? belt * 0.5 * bsat : e.vx * 0.55; }
else { e.vy = Math.min(inLiq ? 0.35 : 1.3, e.vy + 0.07); e.vx *= inLiq ? 0.8 : 0.985; }
if (belt && bsat > 0) stats.moved += 0.05;
e.x += e.vx; e.y += e.vy;
// step toward the new rounded position one cell at a time
let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy;
while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else { e.vx = 0; e.x = cx; break; } }
while (cy !== ty) { const s = Math.sign(ty - cy); if (entFreeAt(cx, cy + s)) cy += s; else { e.vy = 0; e.y = cy; break; } }
if (!entFreeAt(cx, cy)) {
// something moved into us (a grain fell in): try to step up out of it
let freed = false;
for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; }
if (!freed) e.y = cy;
}
e.ix = cx; e.iy = cy;
const cx = e.ix, cy = e.iy;
// heat: the bar tends toward the temperature around it
const T = temp[Math.min(H - 1, cy + 1) * W + Math.min(W - 1, cx + 1)];
e.T += (T - e.T) * 0.04;
+1 -1
View File
@@ -113,7 +113,7 @@ function machinesTick() {
const list = tubeLists[p], dx = TDX[p], dy = TDY[p], dj = dy * W + dx, flat = dy === 0;
for (let k = 0; k < list.length; k++) {
const i = list[k];
if (part[i] !== p) continue;
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) tubeIntake(i, p, dx, dy); continue; }
if (stamp[i] === tickStamp) continue;
+1 -1
View File
@@ -42,7 +42,7 @@ const PIECES = [
{ 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.' },
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left'],
desc: 'A turning screw that draws in loose material and pushes it along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Stack them pointing up to lift ore out of a mine, and turn the top one sideways to unload. Copper softens around 1000°C.' },
desc: 'A turning screw 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. Stack them pointing up to lift ore out of a mine, and turn the top one sideways to unload. Copper softens around 1000°C.' },
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through.' },
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,