Grinder and Saw hopper blocks replace the Refiner and Stone cutter in the menu (t4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 16:06:51 -04:00
co-authored by Claude Opus 5.5
parent 01403bb95e
commit 8d9e9c546b
16 changed files with 176 additions and 25 deletions
+6 -2
View File
@@ -26,6 +26,7 @@ function placeBlock(code, v, bx, by, free, extra) {
if (code === BP_WALL) setCell(i, WALL_OF[v], AMB[y], 0);
else if (code === BP_BELT) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
else if (code === BP_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); }
else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : SAWB, AMB[y], 0);
else if (code === BP_GATE) setCell(i, GATE, AMB[y], 0);
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); }
else if (code === BP_TUBE) { part[i] = [P_TU, P_TR, P_TD, P_TL][v & 3]; markPow(i, true); wakeXY(x, y); }
@@ -36,6 +37,7 @@ function placeBlock(code, v, bx, by, free, extra) {
if (isPipeCode(code)) pipesDirty = true;
blockP[by * BW + bx] = code; blockV[by * BW + bx] = v;
if (code === BP_FILTER) { blockV[by * BW + bx] = v & 128; filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
if (isProcCode(code)) procDirty = true;
stats.placed++;
undoNote({ t: 'block+', bx, by, code });
return '';
@@ -51,6 +53,7 @@ function blockIntegrity(b) {
if (code === BP_WALL) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; }
else if (code === BP_BELT) { if (m === BELTL || m === BELTR) n++; }
else if (code === BP_SHAFT) { if (m === SHAFT) n++; }
else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : SAWB)) n++; }
else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; }
else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
else if (code === BP_TUBE) { if (p >= P_TU && p <= P_TR) n++; }
@@ -79,14 +82,15 @@ function removeBlock(bx, by) {
} else if (isPipeCode(code)) {
if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; }
else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); n++; }
} else if (code === BP_BELT || code === BP_SHAFT || code === BP_GATE) {
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; }
} else if (code === BP_BELT || code === BP_SHAFT || code === BP_GATE || isProcCode(code)) {
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; }
if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; }
}
if (code === BP_WALL && metal) giveOrDrop(BAR_OF[WALL_OF[v]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), bx * BS + 2, by * BS + 2);
else if (code !== BP_WALL && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2);
if (code === BP_FILTER) { filtersDirty = true; filterCfg.delete(b); }
if (isProcCode(code)) { procDirty = true; procState.delete(b); }
blockP[b] = 0; blockV[b] = 0;
if (isPipeCode(code)) {
pipesDirty = true;
+1
View File
@@ -45,6 +45,7 @@ function fanBlow(mm, sat) {
}
}
function machinesTick() {
procBlocksTick();
if (powerDirty && tick >= nextPowerBuild) { rebuildPower(); nextPowerBuild = tick + 6; }
if (brokenMachines.length) {
const ids = brokenMachines; brokenMachines = [];
+4 -2
View File
@@ -11,8 +11,8 @@ const EMPTY = 0, GRASS = 1, DIRT = 2, LDIRT = 3, SAND = 4, GRAVEL = 5, CLAY = 6,
ZNV = 63, ZNORE = 64, ZINC = 65, MZN = 66, SCRZN = 67, BARZN = 68,
AGV = 69, AGORE = 70, SILVER = 71, MAG = 72, SCRAG = 73, BARAG = 74,
AUV = 75, AUORE = 76, GOLD = 77, MAU = 78, SCRAU = 79, BARAU = 80,
BRONZE = 81, MBZ = 82, SCRBZ = 83, BARBZ = 84, BRASS = 85, MBS = 86, SCRBS = 87, BARBS = 88, DOCKW = 89, MSLAG = 90, PIPE = 91, BARGL = 92, CUTST = 93, BARSB = 94;
const NMAT = 95;
BRONZE = 81, MBZ = 82, SCRBZ = 83, BARBZ = 84, BRASS = 85, MBS = 86, SCRBS = 87, BARBS = 88, DOCKW = 89, MSLAG = 90, PIPE = 91, BARGL = 92, CUTST = 93, BARSB = 94, GRINDER = 95, SAWB = 96;
const NMAT = 97;
const MD = [];
function def(id, o) {
@@ -62,6 +62,8 @@ def(BELTL, { name: 'Belt', col: [66, 68, 74], v: 4, k: 0.03, hard: 2, melt: 1085
def(BELTR, { name: 'Belt', col: [66, 68, 74], v: 4, k: 0.03, hard: 2, melt: 1085, meltTo: MCU, partMat: true });
def(SHAFT, { name: 'Shaft', col: [150, 110, 64], v: 6, k: 0.005, hard: 2, ign: 340, flame: 800, flameF: 1000, fuel: 140, burnTo: ASH, ashP: 0.3, partMat: true });
def(GATE, { name: 'Gate', col: [146, 74, 58], v: 6, k: 0.004, hard: 3, melt: 1750, meltTo: MAGMA, partMat: true });
def(GRINDER, { name: 'Grinder', col: [64, 70, 80], v: 4, k: 0.035, hard: 6, melt: 1450, meltTo: MST, partMat: true });
def(SAWB, { name: 'Saw', col: [74, 80, 90], v: 4, k: 0.035, hard: 6, melt: 1450, meltTo: MST, partMat: true });
def(SPRING, { name: 'Spring', col: [92, 112, 128], v: 30, k: 0.012, hard: 9, yld: 0, desc: 'Groundwater seeps out of this rock whenever there is room next to it.' });
def(RSTONE, { name: 'Stone rubble', ph: PH_POWDER, col: [126, 125, 130], v: 30, dens: 1.8, k: 0.01, hard: 0, melt: 1250, meltTo: MAGMA, bag: true, desc: 'Broken stone. 16 grains make a stone block.' });
def(RGRAN, { name: 'Granite rubble', ph: PH_POWDER, col: [164, 128, 124], v: 32, dens: 1.9, k: 0.01, hard: 0, melt: 1260, meltTo: MAGMA, bag: true, desc: 'Broken granite. Makes granite blocks.' });
+7 -3
View File
@@ -4,7 +4,7 @@
// Everything you build snaps to a grid of 4x4-grain blocks. The world itself
// stays grain-level; only built pieces snap.
const BS = 4;
const BP_WALL = 1, BP_BELT = 2, BP_TUBE = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10;
const BP_WALL = 1, BP_BELT = 2, BP_TUBE = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12;
function isPipeCode(c) { return c === BP_PIPE || c === BP_PUMP || c === BP_OUTLET; }
// Filters: each filter block has a set of materials and a mode. In drop mode (inv 0)
// the chosen materials fall through and everything else rides along on top; in carry
@@ -57,15 +57,19 @@ const PIECES = [
desc: 'Draws liquid in at the tail of its arrow (keep that side under water) and pushes it into the pipes on its other sides, about 60 grains a second while the line has room. It takes water and anything else cooler than 500°C, one liquid at a time per line. Press F to switch it on or off.' },
{ id: 'outlet', code: BP_OUTLET, name: 'Outlet', kind: 'block', cost: [[CASH, 15]], dirs: ['down', 'left', 'up', 'right'],
desc: 'Lets a pipe line\'s liquid out of the side its arrow points to, wherever there is room for it. Press F to open or close it.' },
{ id: 'grinder', code: BP_GRIND, name: 'Grinder', kind: 'block', cost: [[CASH, 60]],
desc: 'Grinds rubble, gravel, sand and dirt resting on top into sand and the odd grain of ore. Results drop out below; keep that space clear. Wider rows grind faster. Stack under a Saw to cut stone and grind the rest.' },
{ id: 'saw', code: BP_SAW, name: 'Saw', kind: 'block', cost: [[CASH, 90]],
desc: 'Cuts stone rubble resting on top into a brick for every 8 grains. Results drop out below; keep that space clear. Wider rows cut faster. Stack over a Grinder to grind everything else.' },
{ id: 'wheel', name: 'Water wheel', kind: 'machine', bw: 3, bh: 3, cost: [[LWOOD, 48]], hidden: POWER_FREE,
desc: 'Makes power when liquid falls through it. More flow, more power.' },
{ id: 'fan', name: 'Fan', kind: 'machine', bw: 1, bh: 1, cost: [[CASH, 60]], dirs: ['right', 'down', 'left', 'up'],
desc: 'Blows gas and forces air into fires, making them far hotter: its stream reaches 28 grains and passes through grates but not walls. Its copper body melts at 1085°C, so keep it outside the furnace, a block clear of the wall, blowing in through a grate. Press F to switch it on or off.' },
{ id: 'cutter', name: 'Stone cutter', kind: 'machine', bw: 2, bh: 2, cost: [[CASH, 90]],
{ id: 'cutter', hidden: true, name: 'Stone cutter', kind: 'machine', bw: 2, bh: 2, cost: [[CASH, 90]],
desc: 'A saw under a hopper. Stone rubble dropped in is cut into stone bricks, one from every 8 grains, which drop out underneath. Builders buy stone bricks, and they make neat cut stone walls that stand more heat than rough stone. Anything else falls straight through.' },
{ id: 'silo', name: 'Silo', kind: 'machine', bw: 3, bh: 6, cost: [[CASH, 180]],
desc: 'A steel bin on legs for one loose material: up to 4,000 grains go in through the hatch in its roof (drop them in from a belt or tube). Its gauge shows how full it is. Press F to open the gate in its cone and let it out underneath, about 60 grains a second; F again shuts it. The first material in decides what it holds until it is empty.' },
{ id: 'refiner', name: 'Refiner', kind: 'machine', bw: 3, bh: 3, cost: [[CASH, 120]],
{ id: 'refiner', hidden: true, name: 'Refiner', kind: 'machine', bw: 3, bh: 3, cost: [[CASH, 120]],
desc: 'A crushing sieve. Drop stone, granite or basalt rubble, gravel, sand or dirt into its hopper: it grinds each grain, keeps the odd grain of ore it finds (clay, from dirt) and lets about half as much fine sand out of the chute underneath. Ore and anything else it cannot grind falls straight through. Keep the space under the chute clear, or a belt running under it.' },
{ id: 'engine', name: 'Steam engine', kind: 'machine', bw: 3, bh: 2, cost: [[BARCU, 10]], dirs: ['exhaust right', 'exhaust left'], hidden: POWER_FREE,
desc: 'Steam rising into the intake slots on its underside drives it. Condensate drips out the exhaust side.' },
+66
View File
@@ -74,6 +74,72 @@ function cutterTick(mm) {
jobs--; mm.busyT = tick;
}
}
// Block hoppers share an outlet down a vertical stack; only its top spends work.
let procState = new Map(), procBlocks = [], procDirty = true;
function isProcCode(c) { return c === BP_GRIND || c === BP_SAW; }
function rebuildProcessors() {
procBlocks = [];
for (let b = 0; b < NB; b++) if (isProcCode(blockP[b])) {
procBlocks.push(b);
if (!procState.has(b)) procState.set(b, { work: 0, cut: 0, busyT: -999, blocked: false });
}
procDirty = false;
}
function procBottom(b) { while (b + BW < NB && isProcCode(blockP[b + BW])) b += BW; return b; }
function procSlot(b) {
const y = (((b / BW) | 0) + 1) * BS, x = (b % BW) * BS;
if (y >= H) return -1;
for (let k = 0; k < 4; k++) { const j = y * W + x + [1, 2, 0, 3][k]; if (mat[j] === EMPTY && !part[j] && !occ[j] && !mach[j]) return j; }
return -1;
}
function procFeed(b, m, T) {
const end = procBottom(b), x = (b % BW) * BS, y = ((b / BW) | 0) * BS - 1;
for (;;) {
const s = procState.get(b); s.busyT = tick;
if (blockP[b] === BP_GRIND && PH[m] === PH_POWDER && REFINE[m]) {
const from = m; let got = 0;
for (const [chance, o] of REFINE[m]) if (rnd() < chance) { got = o; break; }
if (got) { stats.refinedOre = (stats.refinedOre || 0) + 1; discover(got, x, y, from, 'refine'); }
else if (rnd() < REFINE_SAND) got = SAND;
stats.refined = (stats.refined || 0) + 1; m = got;
if (!m) return;
} else if (blockP[b] === BP_SAW && m === RSTONE) {
if (++s.cut < CUT_PER_BRICK) return;
s.cut = 0; m = BARSB;
}
if (b === end) break;
b += BW;
}
if (m === BARSB) {
const e = spawnBar(BARSB, x, (((end / BW) | 0) + 1) * BS, (rnd() - 0.5) * 0.1, 0.05);
if (e) { e.T = T; discover(BARSB, x, y, RSTONE, 'cut'); stats.bricks = (stats.bricks || 0) + 1; }
} else setCell(procSlot(end), m, T, 0);
}
function procBlocksTick() {
if (procDirty) rebuildProcessors();
for (const b of procBlocks) {
if (b >= BW && isProcCode(blockP[b - BW])) continue;
const s = procState.get(b), end = procBottom(b), y = ((b / BW) | 0) * BS - 1, x = (b % BW) * BS;
s.work += blockP[b] === BP_GRIND ? 0.25 : 0.5;
let jobs = Math.floor(s.work); s.work -= jobs;
if (!jobs || y < 0) continue; // a tick without work keeps the last blocked state, so it doesn't flicker
for (let q = b; q <= end; q += BW) procState.get(q).blocked = false;
for (let k = 0; k < 4 && jobs > 0; k++) {
const i = y * W + x + ((k + tick) & 3), m = mat[i];
if (m === EMPTY || PH[m] === PH_GAS || PH[m] === PH_SOLID || stamp[i] === tickStamp) continue;
let brick = false;
if (m === RSTONE) for (let q = b; q <= end; q += BW) {
if (blockP[q] === BP_GRIND) break;
if (blockP[q] === BP_SAW) { brick = procState.get(q).cut + 1 >= CUT_PER_BRICK; break; }
}
if (procSlot(end) < 0 || brick && !entFreeAt(x, (((end / BW) | 0) + 1) * BS)) {
for (let q = b; q <= end; q += BW) procState.get(q).blocked = true;
break;
}
const T = temp[i]; setCell(i, EMPTY, AMB[y], 0); procFeed(b, m, T); jobs--;
}
}
}
// ---------- Silo ----------
// One loose material, counted (not simulated grain by grain inside): grains in the roof hatch
// go in, and with the gate open they come out of the bottom of the cone.
+1 -1
View File
@@ -37,7 +37,7 @@ function deserializeWorld(meta, buffers) {
computeAmbient(); fillNoise();
mat.set(buffers.mat); life.set(buffers.life); flags.set(buffers.flags);
for (let i = 0; i < N; i++) if (mat[i] === SPRING) mat[i] = STONE; // springs are gone: older worlds keep plain rock there
blockP.set(buffers.blockP); blockV.set(buffers.blockV); filtersDirty = true;
blockP.set(buffers.blockP); blockV.set(buffers.blockV); filtersDirty = true; rebuildProcessors();
// pipes used to be solid copper blocks; now they are thin and let things past
for (let b = 0; b < NB; b++) if (blockP[b] === BP_PIPE) for (const i of blockCellList(b % BW, (b / BW) | 0)) if (mat[i] === PIPE) { mat[i] = EMPTY; part[i] = P_PIPE; }
for (const [b, m, inv] of meta.filters || []) if (blockP[b] === BP_FILTER) filterCfg.set(b, filterRec({ m, inv }));
+1 -1
View File
@@ -44,7 +44,7 @@ function alloc(w, h) {
mat = new Uint8Array(N); temp = new Float32Array(N); life = new Uint8Array(N); flags = new Uint8Array(N);
stamp = new Uint8Array(N); part = new Uint8Array(N); mach = new Uint16Array(N); digp = new Uint8Array(N);
noiseB = new Uint8Array(N); bgm = new Uint8Array(N); airS = new Uint16Array(N); netId = new Uint16Array(N); isPow = new Uint8Array(N); occ = new Uint8Array(N);
BW = Math.ceil(w / BS); BH = Math.ceil(h / BS); NB = BW * BH; blockP = new Uint8Array(NB); blockV = new Uint8Array(NB); filtersDirty = true; filterCfg = new Map();
BW = Math.ceil(w / BS); BH = Math.ceil(h / BS); NB = BW * BH; blockP = new Uint8Array(NB); blockV = new Uint8Array(NB); filtersDirty = true; filterCfg = new Map(); procDirty = true; procState.clear();
ents = []; flyers = []; ghostBlocks = new Map(); ghostMolds = []; treeBases = []; saplings = [];
surfBase = Math.round(Math.min(h * 0.27, 175)); underD = h - surfBase; shallowD = 0; lapse = 0.36;
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);