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 232f5af6e4
commit eee170e3d1
21 changed files with 284 additions and 28 deletions
+6
View File
@@ -216,6 +216,12 @@ above, including:
brick pattern so piles have no air gaps; a Bar rack machine (the Silo for bars) takes them from brick pattern so piles have no air gaps; a Bar rack machine (the Silo for bars) takes them from
a belt or tube, counts them and lets them out with F. The owner wanted bar storage that doesn't a belt or tube, counts them and lets them out with F. The owner wanted bar storage that doesn't
waste space, like stacked ingots in real life. waste space, like stacked ingots in real life.
As built: a resting bar snaps to a 2-grain grid, and one with fewer than 2 of its 4 bottom cells
supported tips into the gap beside it. Storage became Rack blocks rather than a machine, like
the t4 hoppers: 50 bars of any kind per block, a column is one rack; bars landing on top go in
(into the lowest block with room), F opens the gate at the bottom of the column, and bars come
out oldest first, one every 20 ticks, onto whatever is below. Taking a block back returns its
bars.
- **Hopper blocks replace hopper machines (t4).** 1-block Grinder and Saw blocks each work what - **Hopper blocks replace hopper machines (t4).** 1-block Grinder and Saw blocks each work what
lands on them and drop it below, so a wider row is a wider, faster hopper. The Refiner and Stone lands on them and drop it below, so a wider row is a wider, faster hopper. The Refiner and Stone
cutter leave the build menu; built ones in old saves keep working. The owner wanted hoppers of cutter leave the build menu; built ones in old saves keep working. The owner wanted hoppers of
+18 -12
View File
@@ -196,15 +196,15 @@ Deepgrain Foundry: simulation core (src/sim/). No DOM anywhere in src/sim/: plai
- functions: rnd, mulberry32, hash2 - functions: rnd, mulberry32, hash2
- variables: RS, PH_EMPTY, PH_SOLID, PH_POWDER, PH_LIQUID, PH_GAS, PH_ITEM - variables: RS, PH_EMPTY, PH_SOLID, PH_POWDER, PH_LIQUID, PH_GAS, PH_ITEM
### src/sim/materials.js (167 lines) ### src/sim/materials.js (168 lines)
Material ids, the material table (def), per-material property arrays, cell flags and part ids, and drill/tank tiers. Material ids, the material table (def), per-material property arrays, cell flags and part ids, and drill/tank tiers.
- functions: def, metalFamily - functions: def, metalFamily
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO, PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_TU, P_TD, P_TL, P_TR, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH - variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO, PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_TU, P_TD, P_TL, P_TR, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH
### src/sim/pieces.js (141 lines) ### src/sim/pieces.js (143 lines)
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost. Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
- functions: isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost - functions: isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
- variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS - variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
### src/sim/economy.js (152 lines) ### src/sim/economy.js (152 lines)
Money, prices, market saturation, the trade dock, buyer and builder orders. Money, prices, market saturation, the trade dock, buyer and builder orders.
@@ -246,12 +246,17 @@ Processing: Grinder and Saw blocks, and the Silo.
- functions: isProcCode, rebuildProcessors, procBottom, procSlot, procFeed, procBlocksTick, siloTick, siloFull - functions: isProcCode, rebuildProcessors, procBottom, procSlot, procFeed, procBlocksTick, siloTick, siloFull
- variables: REFINE, REFINE_SAND, CUT_PER_BRICK, SAW_CUTS, procState, procBlocks, procDirty, SILO_CAP, SILO_RATE - variables: REFINE, REFINE_SAND, CUT_PER_BRICK, SAW_CUTS, procState, procBlocks, procDirty, SILO_CAP, SILO_RATE
### src/sim/racks.js (56 lines)
Each rack block owns its bars; only the bottom of a column releases them.
- functions: newRack, rackTop, rackBottom, rackPush, rackShift, rackIntake, rackRefill, rackTick
- variables: RACK_CAP, rackState
### src/sim/pipes.js (114 lines) ### src/sim/pipes.js (114 lines)
Pipes, pumps and outlets: each joined line holds one liquid as a count, not as grains. Pipes, pumps and outlets: each joined line holds one liquid as a count, not as grains.
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick - functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty - variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
### src/sim/machine-tick.js (163 lines) ### src/sim/machine-tick.js (164 lines)
Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick). Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
- functions: engineIntake, fanBlow, machinesTick - functions: engineIntake, fanBlow, machinesTick
@@ -260,7 +265,7 @@ How belts, filters and screw tubes move what is on or in them. What rests on a b
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake - functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty - variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/blocks.js (136 lines) ### src/sim/blocks.js (149 lines)
Building blocks: placing, integrity, removing, lines and rectangles of blocks. Building blocks: placing, integrity, removing, lines and rectangles of blocks.
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks - functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
@@ -269,7 +274,7 @@ Molds: cavities for castings; ingot troughs empty themselves below. Removal retu
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - variables: DROSS, grains, barWord
### src/sim/items.js (224 lines) ### src/sim/items.js (225 lines)
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds. Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeEnter, entTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText - functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeEnter, entTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
- variables: BAR_W, BAR_H, entCurM, droppedLast - variables: BAR_W, BAR_H, entCurM, droppedLast
@@ -293,7 +298,7 @@ The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. M
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite - functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
- variables: blowRR, FLY_G, FLY_Q - variables: blowRR, FLY_G, FLY_Q
### src/sim/actions.js (87 lines) ### src/sim/actions.js (92 lines)
Player actions on built things: take back, place a machine or mold, gates, F to interact. Take back whatever piece is at (x,y): machine, mold or block. Returns a message. Player actions on built things: take back, place a machine or mold, gates, F to interact. Take back whatever piece is at (x,y): machine, mold or block. Returns a message.
- functions: removePieceAt, removePieceAt0, placeMachineAt, placeMoldAt, toggleGateAt, interactAt - functions: removePieceAt, removePieceAt0, placeMachineAt, placeMoldAt, toggleGateAt, interactAt
@@ -329,7 +334,7 @@ World generation: noise, layers, ores, caves, water, trees, and clearing tiny ha
- functions: makeNoise, fbm, clearHardSpecks, generateWorld, generateWorldAsync - functions: makeNoise, fbm, clearHardSpecks, generateWorld, generateWorldAsync
- variables: SPECK_MIN - variables: SPECK_MIN
### src/sim/save.js (126 lines) ### src/sim/save.js (141 lines)
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration. Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
- functions: serializeWorld, deserializeWorld - functions: serializeWorld, deserializeWorld
- variables: SAVE_VERSION, TOFF - variables: SAVE_VERSION, TOFF
@@ -348,9 +353,9 @@ Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld - functions: fogColor, drawWorld
- variables: TUBE_CLASS - variables: TUBE_CLASS
### src/render/machines.js (212 lines) ### src/render/machines.js (242 lines)
Machine overlays: fans, engines, silo and the rest, drawn on top of the world. Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel - functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawRackBlocks
### src/render/overlays.js (229 lines) ### src/render/overlays.js (229 lines)
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles. Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
@@ -404,7 +409,7 @@ The notebook (N): milestones, market, wallet, blueprints, controls.
The tool hotbar and what is selected. The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece - functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece
### src/ui/build-menu.js (103 lines) ### src/ui/build-menu.js (104 lines)
The build menu along the bottom: categories, piece tiles and their icons. The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
@@ -414,7 +419,7 @@ The selected tool and piece: R / Q / E, drill width, filter picker, the selectio
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag - functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
- variables: lastBagKey - variables: lastBagKey
### src/ui/info.js (174 lines) ### src/ui/info.js (186 lines)
Discovery cards, hover info (I) and the cursor label. Discovery cards, hover info (I) and the cursor label.
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
@@ -468,6 +473,7 @@ The frame loop and boot.
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player. - `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe. - `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore. - `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works. - `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders. - `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing. - `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
+10
View File
@@ -4,6 +4,16 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-24: t1: bars stack neatly, Rack blocks store them (Claude Code, Codex)
- `232f5af`: `entTick` snap-and-tip (`entSupportCount`, `src/sim/items.js`);
`test/t_bar_stacking.js` (12 bars fill a pit's bottom six rows; fails on the old code).
- Rack blocks: `src/sim/racks.js` (`rackState` per block, FIFO runs, `rackIntake` from `entTick`,
`rackTick` release, `rackRefill`), `BP_RACK`/`RACKB`, F toggle in `interactAt`, removal returns
bars with `giveOrDrop`, saved in meta `racks` and validated on load, shelves overlay, hover,
Machines tab, notebook. `test/t_racks.js` (29 checks), play.py menu check.
- Tests: `run_all.py --browser` ALL PASSED. Screenshot `shots/t1-racks.png`.
## 2026-10-24: Backlog combined and rewritten (Claude Code) ## 2026-10-24: Backlog combined and rewritten (Claude Code)
- On the owner's request: t12 (exotic ores) and t31 (choosing claims) merged into t13, now - On the owner's request: t12 (exotic ores) and t31 (choosing claims) merged into t13, now
+1
View File
@@ -15,6 +15,7 @@
"sim/heat.js", "sim/heat.js",
"sim/machines.js", "sim/machines.js",
"sim/processors.js", "sim/processors.js",
"sim/racks.js",
"sim/pipes.js", "sim/pipes.js",
"sim/machine-tick.js", "sim/machine-tick.js",
"sim/conveyors.js", "sim/conveyors.js",
+30
View File
@@ -76,6 +76,7 @@ function drawMachines() {
ctx.restore(); ctx.restore();
} }
drawProcBlocks(); drawProcBlocks();
drawRackBlocks();
} }
// Crushing rollers and saw blades sit over the shared steel housings in both looks. // Crushing rollers and saw blades sit over the shared steel housings in both looks.
function drawProcBlocks() { function drawProcBlocks() {
@@ -210,3 +211,32 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) {
ctx.restore(); ctx.restore();
} }
} }
// Shelves show each block's fill and materials; the bottom gate belongs to the column.
function drawRackBlocks() {
const ctx = R.ctx;
for (const [b, s] of rackState) {
const x = (b % BW) * BS, y = ((b / BW) | 0) * BS;
const [sx, sy, sw] = cellRect(x, y, BS, BS);
if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sw < 0) continue;
if (SET.fog && fogSeen(x + 2, y + 2) < 64) continue;
ctx.save();
ctx.fillStyle = '#202831'; ctx.fillRect(sx + sw * 0.1, sy + sw * 0.1, sw * 0.8, sw * 0.8);
ctx.fillStyle = '#a5adb5';
for (let row = 0; row < 2; row++) ctx.fillRect(sx + sw * 0.1, sy + sw * (0.47 + row * 0.4), sw * 0.8, sw * 0.05);
const slots = Math.ceil(s.n / 5);
let run = 0, count = 0;
for (let k = 0; k < slots; k++) {
while (run + 1 < s.q.length && k * 5 >= count + s.q[run][1]) count += s.q[run++][1];
const m = s.q[run][0];
ctx.fillStyle = `rgb(${COLR[m]},${COLG[m]},${COLB[m]})`;
ctx.fillRect(sx + sw * (0.14 + k % 5 * 0.145), sy + sw * (0.71 - Math.floor(k / 5) * 0.4), sw * 0.12, sw * 0.12);
}
if (b + BW >= NB || blockP[b + BW] !== BP_RACK) {
ctx.fillStyle = s.open ? '#e0b43c' : '#c9d1d8';
ctx.fillRect(sx + sw * 0.1, sy + sw * 0.94, sw * (s.open ? 0.2 : 0.8), sw * 0.06);
if (s.open) ctx.fillRect(sx + sw * 0.7, sy + sw * 0.94, sw * 0.2, sw * 0.06);
}
ctx.restore();
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ function drawWorld() {
} }
else if (m === GATE) { if ((x & 1) === 0 && (y & 3) !== 0) { r -= 40; g -= 24; b -= 20; } } else if (m === GATE) { if ((x & 1) === 0 && (y & 3) !== 0) { r -= 40; g -= 24; b -= 20; } }
else if (m === CUTST) { const lx = x & 3, ly = y & 3; if (ly === 1 || ly === 3 || lx === (ly < 2 ? 3 : 1)) { r -= 34; g -= 33; b -= 30; } else if (ly === 0 || ly === 2) { r += 10; g += 10; b += 9; } } else if (m === CUTST) { const lx = x & 3, ly = y & 3; if (ly === 1 || ly === 3 || lx === (ly < 2 ? 3 : 1)) { r -= 34; g -= 33; b -= 30; } else if (ly === 0 || ly === 2) { r += 10; g += 10; b += 9; } }
else if (m === GRINDER || m === SAWB || m === PRESSB) { else if (m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) {
const lx = x & 3, ly = y & 3, frame = lx === 0 || lx === 3 || ly === 0 || ly === 3; const lx = x & 3, ly = y & 3, frame = lx === 0 || lx === 3 || ly === 0 || ly === 3;
r = frame ? 96 : 38; g = frame ? 102 : 42; b = frame ? 112 : 50; r = frame ? 96 : 38; g = frame ? 102 : 42; b = frame ? 112 : 50;
} }
+5
View File
@@ -76,6 +76,11 @@ function interactAt(x, y) {
// ingot molds empty themselves: F passes them by // 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; } 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); 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) { if (blockP[pb] === BP_PUMP || blockP[pb] === BP_OUTLET) {
blockV[pb] ^= 4; emit({ t: 'sfx', s: 'ui' }); 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.'); 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); 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_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_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 (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_GATE) setCell(i, GATE, AMB[y], 0);
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); } 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; 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 (code === BP_FILTER) { blockV[by * BW + bx] = v & 128; filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
if (isProcCode(code)) procDirty = 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++; stats.placed++;
undoNote({ t: 'block+', bx, by, code }); undoNote({ t: 'block+', bx, by, code });
return ''; return '';
@@ -53,6 +60,7 @@ function blockIntegrity(b) {
if (code === BP_WALL) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; } 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_BELT) { if (m === BELTL || m === BELTR) n++; }
else if (code === BP_SHAFT) { if (m === SHAFT) 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 (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_GATE) { if (m === GATE || p === P_GOPEN) n++; }
else if (code === BP_GRATE) { if (p === P_GRATE) n++; } else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
@@ -82,8 +90,8 @@ function removeBlock(bx, by) {
} else if (isPipeCode(code)) { } else if (isPipeCode(code)) {
if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; } if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; }
else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); 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)) { } 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) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; } 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); } if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; } } 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); 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 (code === BP_FILTER) { filtersDirty = true; filterCfg.delete(b); }
if (isProcCode(code)) { procDirty = true; procState.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; blockP[b] = 0; blockV[b] = 0;
if (isPipeCode(code)) { if (isPipeCode(code)) {
pipesDirty = true; pipesDirty = true;
+1
View File
@@ -153,6 +153,7 @@ function entTick() {
} }
e.ix = cx; e.iy = cy; e.ix = cx; e.iy = cy;
} }
if (rackIntake(e)) { ents.splice(k, 1); continue; }
const cx = e.ix, cy = e.iy; const cx = e.ix, cy = e.iy;
// heat: the bar tends toward the temperature around it // 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)]; 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() { function machinesTick() {
procBlocksTick(); procBlocksTick();
rackTick();
if (powerDirty && tick >= nextPowerBuild) { rebuildPower(); nextPowerBuild = tick + 6; } if (powerDirty && tick >= nextPowerBuild) { rebuildPower(); nextPowerBuild = tick + 6; }
if (brokenMachines.length) { if (brokenMachines.length) {
const ids = brokenMachines; brokenMachines = []; 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, ZNV = 63, ZNORE = 64, ZINC = 65, MZN = 66, SCRZN = 67, BARZN = 68,
AGV = 69, AGORE = 70, SILVER = 71, MAG = 72, SCRAG = 73, BARAG = 74, AGV = 69, AGORE = 70, SILVER = 71, MAG = 72, SCRAG = 73, BARAG = 74,
AUV = 75, AUORE = 76, GOLD = 77, MAU = 78, SCRAU = 79, BARAU = 80, 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; 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 = 101; const NMAT = 102;
const MD = []; const MD = [];
function def(id, o) { 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(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(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(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(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(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(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 // Everything you build snaps to a grid of 4x4-grain blocks. The world itself
// stays grain-level; only built pieces snap. // stays grain-level; only built pieces snap.
const BS = 4; 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; } 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) // 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 // 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.' }, 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]], { 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.' }, 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]], { 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.' }, 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, { 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, 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]), 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 }, 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]), 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 }, 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 }, 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); 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 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(); 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 // 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 (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 })); 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); 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); 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); 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 = []; 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; 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); surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
+2 -1
View File
@@ -4,7 +4,7 @@ const BUILD_CATS = [
['Structure', ['wall', 'grate', 'gate']], ['Structure', ['wall', 'grate', 'gate']],
['Moving', ['belt', 'filter', 'tube']], ['Moving', ['belt', 'filter', 'tube']],
['Liquids', ['pipe', 'pump', 'outlet']], ['Liquids', ['pipe', 'pump', 'outlet']],
['Machines', ['fan', 'grinder', 'saw', 'press', 'silo']], ['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']],
['Molds', ['ingotmold', 'bitmold', 'tankmold']], ['Molds', ['ingotmold', 'bitmold', 'tankmold']],
]; ];
const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]); const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]);
@@ -22,6 +22,7 @@ const PIECE_ICONS = {
grinder: '<path d="M3 4h18l-5 7H8z"/><circle cx="9" cy="15.5" r="2.8"/><circle cx="15" cy="15.5" r="2.8"/><path d="M12 20v2"/>', grinder: '<path d="M3 4h18l-5 7H8z"/><circle cx="9" cy="15.5" r="2.8"/><circle cx="15" cy="15.5" r="2.8"/><path d="M12 20v2"/>',
press: '<path d="M5 3h14l-3 5H8zM12 8v5M5 13h14v3H5z"/><rect x="8" y="18" width="8" height="3"/>', press: '<path d="M5 3h14l-3 5H8zM12 8v5M5 13h14v3H5z"/><rect x="8" y="18" width="8" height="3"/>',
saw: '<path d="M5 3h14l-3 5H8z"/><circle cx="12" cy="15" r="5"/><circle cx="12" cy="15" r="1.2"/><path d="M12 9.2v1.6M17.8 15h-1.6M12 20.8v-1.6M6.2 15h1.6"/>', saw: '<path d="M5 3h14l-3 5H8z"/><circle cx="12" cy="15" r="5"/><circle cx="12" cy="15" r="1.2"/><path d="M12 9.2v1.6M17.8 15h-1.6M12 20.8v-1.6M6.2 15h1.6"/>',
rack: '<path d="M4 3v18M20 3v18M4 11h16M4 20h16"/><path d="M7 6h10v3H7zM7 15h10v3H7z"/>',
silo: '<path d="M6 5.5h12v11H6z"/><path d="M6 5.5l1-2h10l1 2M6 16.5l4 3h4l4-3M7 16.5v5M17 16.5v5"/><path d="M15.5 8v6"/>', silo: '<path d="M6 5.5h12v11H6z"/><path d="M6 5.5l1-2h10l1 2M6 16.5l4 3h4l4-3M7 16.5v5M17 16.5v5"/><path d="M15.5 8v6"/>',
ingotmold: '<path d="M3 9h18l-2 7H5z"/><path d="M9.2 9l.8 7M14.8 9l-.8 7"/>', ingotmold: '<path d="M3 9h18l-2 7H5z"/><path d="M9.2 9l.8 7M14.8 9l-.8 7"/>',
bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>', bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>',
+12
View File
@@ -92,6 +92,18 @@ function hoverInfo(ax, ay) {
if (mm.type === 'wheel' || mm.type === 'engine') t2.push(`making ${mm.out.toFixed(1)} power`); if (mm.type === 'wheel' || mm.type === 'engine') t2.push(`making ${mm.out.toFixed(1)} power`);
if (mm.type === 'fan') { t2.push(mm.on ? 'on' : 'off · press F'); if (mm.hot > 760) { t2.push(`${mm.hot}°C · melts at 1085°C`); warn = 'too hot'; } } if (mm.type === 'fan') { t2.push(mm.on ? 'on' : 'off · press F'); if (mm.hot > 760) { t2.push(`${mm.hot}°C · melts at 1085°C`); warn = 'too hot'; } }
if (mm.type === 'silo') t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it'); if (mm.type === 'silo') t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it');
} else if (b === BP_RACK) {
const top = rackTop((ay >> 2) * BW + (ax >> 2)), counts = new Map();
let n = 0, capacity = 0;
for (let q = top; q < NB && blockP[q] === BP_RACK; q += BW) {
const s = rackState.get(q); capacity += RACK_CAP; n += s.n;
for (const [m, count] of s.q) counts.set(m, (counts.get(m) || 0) + count);
}
t1 = 'Bar rack'; t2.push(`${n} / ${capacity} bars`);
t2.push(...[...counts].slice(0, 4).map(([m, count]) => `${count} ${MD[m].name.toLowerCase()}`));
if (counts.size > 4) t2.push('+ more');
t2.push(rackState.get(top).open ? 'open: letting bars out' : 'closed: F lets bars out');
if (n === capacity) t2.push('full');
} else if (isProcCode(b)) { } else if (isProcCode(b)) {
const s = procState.get((ay >> 2) * BW + (ax >> 2)), saw = b === BP_SAW, press = b === BP_PRESS; const s = procState.get((ay >> 2) * BW + (ax >> 2)), saw = b === BP_SAW, press = b === BP_PRESS;
t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder'; t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder';
+1 -1
View File
@@ -184,7 +184,7 @@ function controlsHtml() {
const tools = [ const tools = [
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'], ['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."], ['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws and Brick presses, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. Right click takes a piece back. Your bot flies through anything you build.'], ['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. Right click takes a piece back. Your bot flies through anything you build.'],
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'], ['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'], ['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
]; ];
+12 -6
View File
@@ -1,13 +1,19 @@
--- ---
id: t1 id: t1
title: Ingot stacking and storage title: Ingot stacking and storage
status: Todo status: Review
created: 2026-10-24 created: 2026-10-24
updated: 2026-10-24T04:52:56Z updated: 2026-10-24T23:53:08Z
tags: [discussion] tags: [discussion]
order: 8 order: 0
report: | report: |
(agent) Decided 2026-10-24 (docs/DESIGN.md); not built yet. (agent) Bars now pile neatly: a bar that lands snaps into line, and one hanging mostly over a gap
tips into it, so piles have no air holes. For storage there's a new Bar rack block ($40) in the
Machines tab. Each block holds 50 bars of any kind; stack blocks in a column for more. Bars that
land on top go in, the hover shows the counts by kind, and F opens the gate at the bottom so
bars come out one at a time, oldest first, onto a belt below. Taking a block back gives its
bars to you. Please try a rack under an ingot mold's belt and see if the stacking feels right.
assignee: claude
--- ---
## Context ## Context
@@ -16,5 +22,5 @@ report: |
## Subtasks ## Subtasks
- [ ] Bars that come to rest snap to a brick pattern, so piles have no air gaps - [x] Bars that come to rest snap to a 2-grain grid, and a bar with less than half of itself supported tips into the gap beside it, so piles have no air gaps
- [ ] Bar rack machine: takes bars from a belt or tube, holds a set number, shows the count, F lets them out - [x] Rack blocks: 1-block pieces stacked into a column, 50 bars each; bars dropped on top go in (any kind, counted by kind), F opens a gate at the bottom that lets them out one at a time, oldest first
+15 -1
View File
@@ -782,8 +782,9 @@ with sync_playwright() as p:
check('and Ctrl+Z swaps the belt back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and not ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4)"), ev('GAME.status')) check('and Ctrl+Z swaps the belt back', ev(f'blockP[({bby} + 1) * BW + {bbx} - 4]') == ev('BP_BELT') and not ev(f"machines.some(m => m && m.type === 'fan' && m.x === ({bbx} - 4) * 4)"), ev('GAME.status'))
# 7a. hopper blocks: the Machines tab has Grinder and Saw instead of the Refiner and Stone cutter; drag a row # 7a. hopper blocks: the Machines tab has Grinder and Saw instead of the Refiner and Stone cutter; drag a row
check('the Machines tab has Grinder and Saw but not the Refiner or Stone cutter', ev("(() => { const ids = BUILD_CATS.find(c => c[0] === 'Machines')[1]; return ids.includes('grinder') && ids.includes('saw') && ids.includes('press') && !ids.includes('refiner') && !ids.includes('cutter'); })()")) check('the Machines tab has Grinder, Saw, Press and Bar rack', ev("(() => { const ids = BUILD_CATS.find(c => c[0] === 'Machines')[1]; return ids.includes('grinder') && ids.includes('saw') && ids.includes('press') && ids.includes('rack') && !ids.includes('refiner') && !ids.includes('cutter'); })()"))
ev('money = 1000') ev('money = 1000')
check('Bar rack has a Machines menu tile', select('3', "piece().id === 'rack'") and pg.locator('[data-piece="rack"]').count() == 1)
ok = select('3', "piece().id === 'grinder'") ok = select('3', "piece().id === 'grinder'")
ev(f"for (let y = {(bby+3)*4}; y < {(bby+4)*4}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+4)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0);") ev(f"for (let y = {(bby+3)*4}; y < {(bby+4)*4}; y++) for (let x = {(bbx-8)*4}; x < {(bbx+4)*4}; x++) if (!blockP[((y/4)|0)*BW + ((x/4)|0)]) setCell(y*W+x, EMPTY, 20, 0);")
drag(lambda: blk(bbx - 6, bby + 3), lambda: blk(bbx - 4, bby + 3)) drag(lambda: blk(bbx - 6, bby + 3), lambda: blk(bbx - 4, bby + 3))
@@ -802,6 +803,19 @@ with sync_playwright() as p:
}} finally {{ removeBlock(bx, by); removeBlock(bx + 1, by); rebuildProcessors(); }} }} finally {{ removeBlock(bx, by); removeBlock(bx + 1, by); rebuildProcessors(); }}
}})()''')) }})()'''))
check('rack hover shows stored kinds, capacity and gate', ev(f'''(() => {{
const bx = {bbx} - 6, by = {bby} + 3;
if (placeBlock(BP_RACK, 0, bx, by, true)) return false;
try {{
const s = rackState.get(by * BW + bx); rackPush(s, BARCU, 2); rackPush(s, BARFE, 1);
const before = RS; drawRackBlocks();
const h = hoverInfo(bx * BS, by * BS);
interactAt(bx * BS, by * BS);
const opened = hoverInfo(bx * BS, by * BS);
return RS === before && h.t1 === 'Bar rack' && h.t2.includes('3 / 50 bars') && h.t2.some(t => t.includes('2 copper bar')) && h.t2.some(t => t.includes('1 iron bar')) && h.t2.includes('closed: F lets bars out') && opened.t2.includes('open: letting bars out');
}} finally {{ removeBlock(bx, by); }}
}})()'''))
# 7b. filter: choose several materials with F, place a line, replace belts with G, edit the whole line # 7b. filter: choose several materials with F, place a line, replace belts with G, edit the whole line
ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };') ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };')
ok = select('3', "piece().id === 'filter'") ok = select('3', "piece().id === 'filter'")
+76
View File
@@ -0,0 +1,76 @@
// Rack intake, FIFO storage, gate, removal and saved-state validation.
const { load, suite, flatWorld } = require('./harness');
const { run } = load();
const { check, done } = suite();
function fresh(n = 1) {
flatWorld(run, 240, 160, 120);
run(`money = 5000; for (let y = 20; y < 20 + ${n}; y++) placeBlock(BP_RACK, 0, 10, y, true);`);
}
function drop(m = 'BARCU', x = 40) {
run(`spawnBar(${m}, ${x}, 72); for (let t = 0; t < 100; t++) entTick();`);
}
fresh(); drop();
check('a falling bar enters the rack', run('ents.length === 0 && rackState.get(20 * BW + 10).n === 1'));
fresh(); for (let n = 0; n < 51; n++) drop();
check('full rack leaves the 51st bar on top', run('rackState.get(20 * BW + 10).n === 50 && ents.length === 1 && ents[0].iy === 78'));
fresh(2); for (let n = 0; n < 100; n++) drop();
check('two blocks hold 100 bars', run('[...rackState.values()].every(s => s.n === 50) && !ents.length'));
check('F on the top opens the whole column', run("interactAt(40, 80) === 'Rack open.' && [...rackState.values()].every(s => s.open)"));
run('for (let t = 0; t < 20; t++) rackTick();');
check('oldest bars refill the bottom after release', run('ents.length === 1 && rackState.get(21 * BW + 10).n === 50 && rackState.get(20 * BW + 10).n === 49'));
run('for (let t = 0; t < 100; t++) rackTick();');
check('occupied outlet stops the flow', run('ents.length === 1'));
fresh(); drop('BARCU'); drop('BARFE'); drop('BARCU');
check('mixed kinds keep FIFO runs', run('JSON.stringify(rackState.get(20 * BW + 10).q) === JSON.stringify([[BARCU, 1], [BARFE, 1], [BARCU, 1]])'));
run('interactAt(40, 80);');
for (const m of ['BARCU', 'BARFE', 'BARCU']) {
run('for (let t = 0; t < 20; t++) rackTick();');
check('gate releases oldest ' + m, run(`ents.length === 1 && ents[0].m === ${m} && ents[0].iy === 84`));
run('removeEnt(ents[0]);');
}
check('empty rack stays open', run('rackState.get(20 * BW + 10).open'));
drop(); run('interactAt(40, 80); for (let t = 0; t < 40; t++) rackTick();');
check('closed gate stops output', run('ents.length === 0 && rackState.get(20 * BW + 10).n === 1'));
fresh(2); drop('BARFE'); run('rackState.get(20 * BW + 10).q = [[BARCU, 2]]; rackState.get(20 * BW + 10).n = 2;');
run('removeBlock(10, 21);');
check('removal returns contents and cash without moving upper contents', run('bag[BARFE] === 1 && money === 5040 && rackState.get(20 * BW + 10).n === 2'));
run('interactAt(40, 80); globalThis.savedRack = serializeWorld(); deserializeWorld(savedRack.meta, savedRack.buffers);');
check('save keeps queues and gate', run('rackState.get(20 * BW + 10).n === 2 && rackState.get(20 * BW + 10).open'));
run('savedRack.meta.racks = [null, {}, [-1, [[BARCU, 1]], true], [20 * BW + 10, [[BARCU, -1], [STONE, 3], [BARFE, 51], [BARCU, Infinity], null], true]]; deserializeWorld(savedRack.meta, savedRack.buffers);');
check('corrupt rack entries are ignored', run('rackState.get(20 * BW + 10).n === 0'));
fresh(); drop('BARCU', 42);
check('half of a bar over the rack is enough', run('ents.length === 0'));
fresh(); drop('BARCU', 43);
check('less than half cannot enter', run('rackState.get(20 * BW + 10).n === 0 && ents.length === 1'));
fresh(); drop();
run('interactAt(40, 80); for (let t = 0; t < 19; t++) rackTick();');
check('gate waits 20 ticks for each bar', run('ents.length === 0'));
run('setCell(84 * W + 40, STONE, 20, 0); for (let t = 0; t < 40; t++) rackTick();');
check('solid ground under the outlet also blocks it', run('ents.length === 0 && rackState.get(20 * BW + 10).n === 1'));
run('setCell(84 * W + 40, EMPTY, 20, 0); rackTick();');
check('clearing the outlet resumes output', run('ents.length === 1'));
fresh(); drop('BARFE');
run('bag.fill(0); bag[SAND] = bagCap(); bagTotal = bagCap(); removeBlock(10, 20);');
check('a full bag drops stored bars near the bot', run('bag[BARFE] + ents.filter(e => e.m === BARFE).length === 1 && !rackState.size'));
fresh();
run('interactAt(40, 80); placeBlock(BP_RACK, 0, 10, 21, true);');
check('extending an open rack keeps its gate open', run('[...rackState.values()].every(s => s.open)'));
fresh();
run('removeBlock(10, 20); undoRecord(\'rack\', () => placeBlock(BP_RACK, 0, 10, 20, false)); undoLast();');
check('rack building can be undone and refunded', run('!blockP[20 * BW + 10] && !rackState.size && money === 5040'));
fresh();
run('globalThis.oldRack = serializeWorld(); delete oldRack.meta.racks; deserializeWorld(oldRack.meta, oldRack.buffers);');
check('missing rack metadata defaults to empty and closed', run('rackState.get(20 * BW + 10).n === 0 && !rackState.get(20 * BW + 10).open'));
fresh(3);
run('rackPush(rackState.get(20 * BW + 10), BARFE, 3); rackPush(rackState.get(22 * BW + 10), BARCU, 1); interactAt(40, 84); for (let t = 0; t < 20; t++) rackTick();');
check('refilling crosses an empty middle block without losing FIFO order', run('ents[0].m === BARCU && rackState.get(22 * BW + 10).n === 3 && rackState.get(20 * BW + 10).n === 0'));
for (const delivery of ['belt', 'tube', 'thrown', 'mold']) {
fresh();
if (delivery === 'belt') run('for (let bx = 6; bx <= 9; bx++) placeBlock(BP_BELT, 1, bx, 20, true); spawnBar(BARCU, 30, 78);');
if (delivery === 'tube') run('placeBlock(BP_TUBE, 2, 10, 17, true); placeBlock(BP_TUBE, 2, 10, 18, true); spawnBar(BARCU, 40, 68);');
if (delivery === 'thrown') run('spawnBar(BARCU, 34, 66, 0.35, -0.3);');
if (delivery === 'mold') run("addMold('ingot', 10, 17, RSTONE, true); for (const i of molds[0].cavs[0].slice(0, 8)) setCell(i, COPPER, 20, F_CAST); checkMolds();");
run('for (let t = 0; t < 300; t++) simTick();');
check(delivery + ' delivery reaches storage', run('rackState.get(20 * BW + 10).n > 0'), run('JSON.stringify(ents.map(e => [e.m,e.ix,e.iy,e.vx,e.vy]))'));
}
done();