Bar rack blocks: store 50 bars each, F lets them out oldest first (t1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 19:53:09 -04:00
co-authored by Claude Opus 5.5
parent f34c34aa73
commit 99d04b4972
20 changed files with 272 additions and 22 deletions
+5
View File
@@ -76,6 +76,11 @@ function interactAt(x, y) {
// ingot molds empty themselves: F passes them by
if (mo && mo.kind !== 'ingot') { const r = collectMold(mo); if (!r.startsWith('New') && !r.startsWith('Knocked')) emit({ t: 'sfx', s: 'ui' }); return r; }
const pb = ((y / BS) | 0) * BW + ((x / BS) | 0);
if (blockP[pb] === BP_RACK) {
const top = rackTop(pb), open = !rackState.get(top).open;
for (let b = top; b < NB && blockP[b] === BP_RACK; b += BW) { const s = rackState.get(b); s.open = open; s.wait = 0; }
return open ? 'Rack open.' : 'Rack closed.';
}
if (blockP[pb] === BP_PUMP || blockP[pb] === BP_OUTLET) {
blockV[pb] ^= 4; emit({ t: 'sfx', s: 'ui' });
return blockP[pb] === BP_PUMP ? (blockV[pb] & 4 ? 'Pump off.' : 'Pump on.') : (blockV[pb] & 4 ? 'Outlet closed.' : 'Outlet open.');
+15 -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 (code === BP_RACK) setCell(i, RACKB, AMB[y], 0);
else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : 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); }
@@ -38,6 +39,12 @@ function placeBlock(code, v, bx, by, free, extra) {
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;
if (code === BP_RACK) {
const b = by * BW + bx, s = newRack();
s.open = !!(rackState.get(b - BW)?.open || rackState.get(b + BW)?.open);
rackState.set(b, s);
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open;
}
stats.placed++;
undoNote({ t: 'block+', bx, by, code });
return '';
@@ -53,6 +60,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 (code === BP_RACK) { if (m === RACKB) n++; }
else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB)) n++; }
else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; }
else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
@@ -82,8 +90,8 @@ 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 || isProcCode(code)) {
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB || m === PRESSB) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; }
} else if (code === BP_BELT || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) {
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) { 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++; }
}
@@ -91,6 +99,11 @@ function removeBlock(bx, by) {
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); }
if (code === BP_RACK) {
const s = rackState.get(b);
rackState.delete(b);
if (s) for (const [m, count] of s.q) giveOrDrop(m, count, bot.x, bot.y);
}
blockP[b] = 0; blockV[b] = 0;
if (isPipeCode(code)) {
pipesDirty = true;
+1
View File
@@ -153,6 +153,7 @@ function entTick() {
}
e.ix = cx; e.iy = cy;
}
if (rackIntake(e)) { ents.splice(k, 1); continue; }
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)];
+1
View File
@@ -46,6 +46,7 @@ function fanBlow(mm, sat) {
}
function machinesTick() {
procBlocksTick();
rackTick();
if (powerDirty && tick >= nextPowerBuild) { rebuildPower(); nextPowerBuild = tick + 6; }
if (brokenMachines.length) {
const ids = brokenMachines; brokenMachines = [];
+3 -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, GRINDER = 95, SAWB = 96, PLANK = 97, PRESSB = 98, GREENB = 99, BARBR = 100;
const NMAT = 101;
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, PLANK = 97, PRESSB = 98, GREENB = 99, BARBR = 100, RACKB = 101;
const NMAT = 102;
const MD = [];
function def(id, o) {
@@ -64,6 +64,7 @@ def(SHAFT, { name: 'Shaft', col: [150, 110, 64], v: 6, k: 0.005, hard: 2, ign: 3
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(RACKB, { name: 'Bar rack', col: [74, 80, 90], v: 4, k: 0.035, hard: 6, melt: 1450, meltTo: MST, partMat: true });
def(PRESSB, { name: 'Brick press', 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.' });
+3 -1
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, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13;
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, BP_PRESS = 13, BP_RACK = 14;
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
@@ -61,6 +61,8 @@ const PIECES = [
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: 'press', code: BP_PRESS, name: 'Brick press', kind: 'block', cost: [[CASH, 60]],
desc: 'Presses clay lumps resting on top into a green brick for every 8. They drop out underneath; put a belt or an open drop under it. Fire green bricks in a fire hotter than about 900°C to make Bricks. Wider rows press faster.' },
{ id: 'rack', name: 'Bar rack', kind: 'block', code: BP_RACK, cost: [[CASH, 40]],
desc: 'Stores bars of any kind, 50 per block. Stack blocks into a column for more room. Bars that land on top go in; F opens the gate at the bottom and lets them out one at a time, oldest first, onto a belt below.' },
{ id: 'saw', code: BP_SAW, name: 'Saw', kind: 'block', cost: [[CASH, 90]],
desc: 'Cuts stone rubble into stone blocks and wood chunks into planks, one from every 8 grains resting on top. They drop out underneath and pile up there: put a belt or an open drop under it to carry them away. 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,
+56
View File
@@ -0,0 +1,56 @@
// Each rack block owns its bars; only the bottom of a column releases them.
const RACK_CAP = 50;
let rackState = new Map();
function newRack() { return { q: [], n: 0, open: false, wait: 0 }; }
function rackTop(b) { while (b >= BW && blockP[b - BW] === BP_RACK) b -= BW; return b; }
function rackBottom(b) { while (b + BW < NB && blockP[b + BW] === BP_RACK) b += BW; return b; }
function rackPush(s, m, n) {
const last = s.q[s.q.length - 1];
if (last && last[0] === m) last[1] += n; else s.q.push([m, n]);
s.n += n;
}
function rackShift(s, n) {
s.q[0][1] -= n; s.n -= n;
if (!s.q[0][1]) s.q.shift();
}
// Called with the moving bar unmarked, after its ordinary collision handling.
function rackIntake(e) {
const y = e.iy + BAR_H;
if (e.vy !== 0 || Math.abs(e.vx) >= 0.05 || y >= H || y % BS) return false;
for (let x = Math.max(0, e.ix); x < Math.min(W, e.ix + BAR_W); x += BS - x % BS) {
const b = (y / BS) * BW + Math.floor(x / BS), left = (b % BW) * BS;
if (blockP[b] !== BP_RACK || b >= BW && blockP[b - BW] === BP_RACK || Math.min(e.ix + BAR_W, left + BS) - Math.max(e.ix, left) < BAR_W / 2) continue;
for (let q = rackBottom(b); q >= b; q -= BW) {
const s = rackState.get(q);
if (s.n < RACK_CAP) { rackPush(s, e.m, 1); return true; }
}
}
return false;
}
function rackRefill(b) {
const top = rackTop(b);
for (let q = b; q > top; q -= BW) {
const lower = rackState.get(q);
for (let above = q - BW; above >= top && lower.n < RACK_CAP; above -= BW) {
const upper = rackState.get(above);
while (lower.n < RACK_CAP && upper.n) {
const n = Math.min(RACK_CAP - lower.n, upper.q[0][1]);
rackPush(lower, upper.q[0][0], n); rackShift(upper, n);
}
}
}
}
function rackTick() {
for (const [b, s] of rackState) {
if (b + BW < NB && blockP[b + BW] === BP_RACK) continue;
if (!s.open) continue;
if (!s.n) rackRefill(b);
if (!s.n) continue;
if (s.wait < 20) s.wait++;
if (s.wait < 20) continue;
const x = (b % BW) * BS, y = (((b / BW) | 0) + 1) * BS;
if (!entFreeAt(x, y) || !spawnBar(s.q[0][0], x, y, 0, 0)) continue;
rackShift(s, 1); s.wait = 0;
rackRefill(b);
}
}
+15
View File
@@ -12,6 +12,7 @@ function serializeWorld() {
v: SAVE_VERSION, seed: worldSeed, W, H, tick, surfBase, underD, shallowD, lapse, tOff: TOFF,
saplings: saplings.map(s => [s.x, s.y, s.age, s.th, s.seed]),
economy: { money, debt, earned, recent: Array.from(mkRecent, v => Math.round(v)), log: shipLog, orders, ordersDone, nextOrderT, orderSeq },
racks: [...rackState].map(([b, s]) => [b, s.q.map(r => r.slice()), s.open]),
filters: [...filterCfg].map(([b, c]) => [b, c.m, c.inv]),
weather: { moist: (() => { const m = Float32Array.from(weather.moist); for (const f of flyers) if (f.rain) m[Math.min(m.length - 1, Math.max(0, (f.x / WBAND) | 0))] += 1; return Array.from(m, v => Math.round(v)); })(), raining: Array.from(weather.raining), shower: weather.shower, next: weather.next, wind: weather.wind, seen: weather.seen, base: weather.base },
bot: { x: bot.x, y: bot.y, lamp: bot.lamp },
@@ -38,6 +39,20 @@ function deserializeWorld(meta, buffers) {
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; rebuildProcessors();
for (let b = 0; b < NB; b++) if (blockP[b] === BP_RACK) rackState.set(b, newRack());
for (const entry of Array.isArray(meta.racks) ? meta.racks : []) {
if (!Array.isArray(entry)) continue;
const [b, q, open] = entry;
if (!Number.isInteger(b) || !rackState.has(b) || !Array.isArray(q)) continue;
const s = newRack();
for (const r of q) {
if (!Array.isArray(r) || r.length !== 2) continue;
const [m, n] = r;
if (!Number.isInteger(m) || m <= 0 || m >= NMAT || PH[m] !== PH_ITEM || !Number.isInteger(n) || n <= 0 || n > RACK_CAP - s.n) continue;
rackPush(s, m, n);
}
s.open = open === true; rackState.set(b, s);
}
// 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(); procDirty = true; procState.clear();
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(); rackState.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);