The game as of artifact version 24, ready for local development with coding agents. - Source split from three large files into 52 small ones under src/sim, src/render and src/ui, joined in the order in src/manifest.json by build.py. The joined code is unchanged apart from comments; every headless suite gives the same output, and screenshots in both looks are pixel-identical to before. - Light tidying with no change in behaviour: one movement scan loop, shared hazard-stripe and belt shader helpers, unused code removed, a shared check helper for the test suites. - World files can be exported as an ordinary download outside claude.ai. - Tests: broken old scripts removed, experiments moved to tools/, test/run_all.py added. Tools: shot.py (repeatable screenshots), where.py, map.py. - Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox), docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the owner's requests and an agent log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
159 lines
7.0 KiB
JavaScript
159 lines
7.0 KiB
JavaScript
// Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
|
|
function engineIntake(mm) {
|
|
for (const i of mm.intake) {
|
|
if (mat[i] === STEAM) {
|
|
const T = temp[i];
|
|
mm.energy += 1 + Math.max(0, T - 100) / 120;
|
|
mat[i] = EMPTY; life[i] = 0; temp[i] = AMB[(i / W) | 0];
|
|
wakeIdx(i);
|
|
const ex = mm.dir === 0 ? mm.x + mm.w : mm.x - 1, ey = mm.y + mm.h - 1;
|
|
if (ex >= 0 && ex < W) {
|
|
const j = ey * W + ex;
|
|
if (mat[j] === EMPTY && !part[j]) { mat[j] = WATER; temp[j] = 92; life[j] = 0; flags[j] = 0; wakeXY(ex, ey); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function fanBlow(mm, sat) {
|
|
const dir = mm.dir; // 0 right, 1 down, 2 left, 3 up
|
|
const dx = [1, 0, -1, 0][dir], dy = [0, 1, 0, -1][dir];
|
|
const cur = curAir();
|
|
for (let lane = 0; lane < 4; lane++) {
|
|
let sx, sy;
|
|
if (dir === 0) { sx = mm.x + 4; sy = mm.y + lane; }
|
|
else if (dir === 2) { sx = mm.x - 1; sy = mm.y + lane; }
|
|
else if (dir === 1) { sx = mm.x + lane; sy = mm.y + 4; }
|
|
else { sx = mm.x + lane; sy = mm.y - 1; }
|
|
let powderDepth = 0;
|
|
const range = 28;
|
|
for (let k = 0; k < range; k++) {
|
|
const x = sx + dx * k, y = sy + dy * k;
|
|
if (x < 0 || y < 0 || x >= W || y >= H) break;
|
|
const i = y * W + x, m = mat[i], ph = PH[m];
|
|
if (ph === PH_SOLID && part[i] !== P_GRATE) break;
|
|
airS[i] = cur;
|
|
if (ph === PH_POWDER) { if (++powderDepth > 16) break; }
|
|
else if (ph === PH_GAS && k < range - 1 && rnd() < sat * 0.7 && stamp[i] !== tickStamp) {
|
|
const nx = x + dx, ny = y + dy;
|
|
if (nx >= 0 && ny >= 0 && nx < W && ny < H) {
|
|
const j = ny * W + nx;
|
|
if (mat[j] === EMPTY && passable(m, PH_GAS, j, dx, dy)) move(i, j, x, y, nx, ny);
|
|
}
|
|
}
|
|
}
|
|
wakeHeatXY(Math.min(W - 1, Math.max(0, sx + dx * 4)), Math.min(H - 1, Math.max(0, sy + dy * 4)));
|
|
}
|
|
}
|
|
function machinesTick() {
|
|
if (powerDirty && tick >= nextPowerBuild) { rebuildPower(); nextPowerBuild = tick + 6; }
|
|
if (brokenMachines.length) {
|
|
const ids = brokenMachines; brokenMachines = [];
|
|
for (const id of ids) if (machines[id]) removeMachine(id, false, true);
|
|
}
|
|
for (const mm of machines) {
|
|
if (!mm) continue;
|
|
if (mm.type === 'wheel') {
|
|
const inst = mm.flow * WHEEL_K;
|
|
mm.out += (inst - mm.out) * 0.04; mm.flow = 0;
|
|
mm.angle += Math.min(0.35, mm.out * 0.012);
|
|
} else if (mm.type === 'refiner') {
|
|
refinerTick(mm);
|
|
} else if (mm.type === 'cutter') {
|
|
cutterTick(mm);
|
|
} else if (mm.type === 'silo') {
|
|
siloTick(mm);
|
|
} else if (mm.type === 'engine') {
|
|
engineIntake(mm);
|
|
mm.out += (mm.energy * ENGINE_K - mm.out) * 0.03; mm.energy = 0;
|
|
mm.angle += Math.min(0.4, mm.out * 0.01);
|
|
}
|
|
}
|
|
for (let n = 1; n < nets.length; n++) {
|
|
const net = nets[n];
|
|
let s = 0; for (const p of net.prods) s += p.out;
|
|
let d = net.nb * BELT_POWER + net.nt * TUBE_POWER;
|
|
for (const f of net.fans) if (f.on) d += FAN_POWER;
|
|
net.supply = s; net.demand = d;
|
|
net.sat = POWER_FREE ? 1 : d > 0 ? Math.min(1, s / d) : 0;
|
|
}
|
|
for (const mm of machines) {
|
|
if (!mm || mm.type !== 'fan') continue;
|
|
const net = nets[mm.net];
|
|
const sat = POWER_FREE ? 1 : net ? net.sat : 0;
|
|
mm.angle += sat * 0.5 * (mm.on ? 1 : 0);
|
|
if (mm.on && sat > 0.05) fanBlow(mm, sat);
|
|
// a fan getting close to melting says so (once every few seconds)
|
|
if ((tick + mm.id) % 30 === 0) {
|
|
let hotT = 0;
|
|
for (let y = mm.y; y < mm.y + 4; y++) for (let x = mm.x; x < mm.x + 4; x++) { const tt = temp[y * W + x]; if (tt > hotT) hotT = tt; }
|
|
if (hotT > 760 && (!mm.warnT || tick - mm.warnT > 600)) { mm.warnT = tick; emit({ t: 'toast', text: `A fan is at ${Math.round(hotT)}°C and melts at 1085°C. Move it out of the heat: outside the furnace wall, blowing in through a grate.` }); }
|
|
}
|
|
}
|
|
// belts and filters move what rests on them every other tick: half a grain per tick
|
|
if ((tick & 1) === 0) {
|
|
for (let pass = 0; pass < 2; pass++) {
|
|
const list = pass ? beltR : beltL, d = pass ? 1 : -1, bm = pass ? BELTR : BELTL;
|
|
for (let k = 0; k < list.length; k++) {
|
|
const b = list[k];
|
|
if (mat[b] !== bm || b < W) continue;
|
|
const above = mat[b - W];
|
|
if (above === BELTL || above === BELTR) continue; // only the top surface carries
|
|
if (netSat(b) <= 0) continue;
|
|
if (PH[above] === PH_LIQUID) beltDrain(b);
|
|
carryColumn(b - W, d, null);
|
|
}
|
|
}
|
|
if (filtersDirty) rebuildFilters();
|
|
for (const b of filterR) filterCarry(b, 1);
|
|
for (const b of filterL) filterCarry(b, -1);
|
|
}
|
|
// tubes: 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 = P_TU; p <= P_TR; p++) {
|
|
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;
|
|
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;
|
|
if (temp[i] > 1000 && rnd() < 0.02) {
|
|
part[i] = 0; markPow(i, false); emit({ t: 'toast', text: 'A screw tube melted. Copper softens around 1000°C.' }); wakeIdx(i);
|
|
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); continue; }
|
|
const nx = x + dx, ny = y + dy;
|
|
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
|
const j = i + dj;
|
|
if (occ[j]) continue;
|
|
const pj = part[j], tube = pj >= P_TU && pj <= P_TR;
|
|
if (tube && TDX[pj] === -dx && TDY[pj] === -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 (!tube && !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 ? (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++; continue;
|
|
}
|
|
move(i, j, x, y, nx, ny); stats.moved++;
|
|
if (tube && pj !== p) {
|
|
// turning a corner: keep going across the new tube's width so every lane fills
|
|
let c = j, cx = nx, cy = ny;
|
|
const bxj = nx >> 2, byj = ny >> 2;
|
|
for (;;) {
|
|
const ex = cx + dx, ey = cy + dy;
|
|
if (ex < 0 || ey < 0 || ex >= W || ey >= H || (ex >> 2) !== bxj || (ey >> 2) !== byj) break;
|
|
const e = ey * W + ex;
|
|
if (part[e] !== pj || !roomFor(e) || occ[e]) break;
|
|
move(c, e, cx, cy, ex, ey); c = e; cx = ex; cy = ey;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|