From 99d04b497207caae76d0981cc7e32a149b993451 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Sat, 24 Oct 2026 19:53:09 -0400 Subject: [PATCH] Bar rack blocks: store 50 bars each, F lets them out oldest first (t1) Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN.md | 6 ++++ docs/PROJECT_MAP.md | 30 +++++++++------- docs/agent-log.md | 10 ++++++ src/manifest.json | 1 + src/render/machines.js | 30 ++++++++++++++++ src/render/world.js | 2 +- src/sim/actions.js | 5 +++ src/sim/blocks.js | 17 +++++++-- src/sim/items.js | 1 + src/sim/machine-tick.js | 1 + src/sim/materials.js | 5 +-- src/sim/pieces.js | 4 ++- src/sim/racks.js | 56 ++++++++++++++++++++++++++++++ src/sim/save.js | 15 ++++++++ src/sim/world.js | 2 +- src/ui/build-menu.js | 3 +- src/ui/info.js | 12 +++++++ src/ui/notebook.js | 2 +- test/play.py | 16 ++++++++- test/t_racks.js | 76 +++++++++++++++++++++++++++++++++++++++++ 20 files changed, 272 insertions(+), 22 deletions(-) create mode 100644 src/sim/racks.js create mode 100644 test/t_racks.js diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f6d9182..3b680e2 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -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 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. + 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 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 diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index a486efd..6946a95 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -196,15 +196,15 @@ Deepgrain Foundry: simulation core (src/sim/). No DOM anywhere in src/sim/: plai - functions: rnd, mulberry32, hash2 - 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. - 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 -### 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. - 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) 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 - 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) 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 - 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). - 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 - 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. - 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 - 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. - 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 @@ -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 - 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. - 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 - 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. - functions: serializeWorld, deserializeWorld - variables: SAVE_VERSION, TOFF @@ -348,9 +353,9 @@ Scale colour without changing the smooth-look class. - functions: fogColor, drawWorld - 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. -- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel +- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawRackBlocks ### src/render/overlays.js (229 lines) 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. - 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. - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - 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 - variables: lastBagKey -### src/ui/info.js (174 lines) +### src/ui/info.js (186 lines) Discovery cards, hover info (I) and the cursor label. - 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_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_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_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. diff --git a/docs/agent-log.md b/docs/agent-log.md index a4e8e17..c05b9d7 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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 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) - On the owner's request: t12 (exotic ores) and t31 (choosing claims) merged into t13, now diff --git a/src/manifest.json b/src/manifest.json index c578ac8..4979fff 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -15,6 +15,7 @@ "sim/heat.js", "sim/machines.js", "sim/processors.js", + "sim/racks.js", "sim/pipes.js", "sim/machine-tick.js", "sim/conveyors.js", diff --git a/src/render/machines.js b/src/render/machines.js index a0fd3af..9bf7879 100644 --- a/src/render/machines.js +++ b/src/render/machines.js @@ -76,6 +76,7 @@ function drawMachines() { ctx.restore(); } drawProcBlocks(); + drawRackBlocks(); } // Crushing rollers and saw blades sit over the shared steel housings in both looks. function drawProcBlocks() { @@ -210,3 +211,32 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) { 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(); + } +} diff --git a/src/render/world.js b/src/render/world.js index efa1e95..eba9b20 100644 --- a/src/render/world.js +++ b/src/render/world.js @@ -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 === 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; r = frame ? 96 : 38; g = frame ? 102 : 42; b = frame ? 112 : 50; } diff --git a/src/sim/actions.js b/src/sim/actions.js index d40cd08..9d632a0 100644 --- a/src/sim/actions.js +++ b/src/sim/actions.js @@ -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.'); diff --git a/src/sim/blocks.js b/src/sim/blocks.js index 4689f22..acd8597 100644 --- a/src/sim/blocks.js +++ b/src/sim/blocks.js @@ -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; diff --git a/src/sim/items.js b/src/sim/items.js index 1bec43d..add7d55 100644 --- a/src/sim/items.js +++ b/src/sim/items.js @@ -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)]; diff --git a/src/sim/machine-tick.js b/src/sim/machine-tick.js index 8abe615..a185f04 100644 --- a/src/sim/machine-tick.js +++ b/src/sim/machine-tick.js @@ -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 = []; diff --git a/src/sim/materials.js b/src/sim/materials.js index 221c5fb..30b2119 100644 --- a/src/sim/materials.js +++ b/src/sim/materials.js @@ -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.' }); diff --git a/src/sim/pieces.js b/src/sim/pieces.js index f7acd4c..8d9523a 100644 --- a/src/sim/pieces.js +++ b/src/sim/pieces.js @@ -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, diff --git a/src/sim/racks.js b/src/sim/racks.js new file mode 100644 index 0000000..498f1fd --- /dev/null +++ b/src/sim/racks.js @@ -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); + } +} diff --git a/src/sim/save.js b/src/sim/save.js index 9ebadb8..716ff90 100644 --- a/src/sim/save.js +++ b/src/sim/save.js @@ -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 })); diff --git a/src/sim/world.js b/src/sim/world.js index 20a769f..1d4ea68 100644 --- a/src/sim/world.js +++ b/src/sim/world.js @@ -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); diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js index 1b20951..edf1a18 100644 --- a/src/ui/build-menu.js +++ b/src/ui/build-menu.js @@ -4,7 +4,7 @@ const BUILD_CATS = [ ['Structure', ['wall', 'grate', 'gate']], ['Moving', ['belt', 'filter', 'tube']], ['Liquids', ['pipe', 'pump', 'outlet']], - ['Machines', ['fan', 'grinder', 'saw', 'press', 'silo']], + ['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']], ['Molds', ['ingotmold', 'bitmold', 'tankmold']], ]; const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]); @@ -22,6 +22,7 @@ const PIECE_ICONS = { grinder: '', press: '', saw: '', + rack: '', silo: '', ingotmold: '', bitmold: '', diff --git a/src/ui/info.js b/src/ui/info.js index e444d0c..c57ed00 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -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 === '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'); + } 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)) { const s = procState.get((ay >> 2) * BW + (ax >> 2)), saw = b === BP_SAW, press = b === BP_PRESS; t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder'; diff --git a/src/ui/notebook.js b/src/ui/notebook.js index 30b65fa..2366769 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -184,7 +184,7 @@ function controlsHtml() { 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.'], ['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.'], ['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.'], ]; diff --git a/test/play.py b/test/play.py index a8f3408..6c06133 100644 --- a/test/play.py +++ b/test/play.py @@ -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')) # 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') + 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'") 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)) @@ -802,6 +803,19 @@ with sync_playwright() as p: }} 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 ev('seen[CUORE] = 1; seen[FEORE] = 1; seen[SAND] = 1; money = 1000; GAME.filterCfg = { m: [], inv: 0 };') ok = select('3', "piece().id === 'filter'") diff --git a/test/t_racks.js b/test/t_racks.js new file mode 100644 index 0000000..5d91004 --- /dev/null +++ b/test/t_racks.js @@ -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();