diff --git a/NOTES.md b/NOTES.md index dbedd9c..0ead966 100644 --- a/NOTES.md +++ b/NOTES.md @@ -6,14 +6,18 @@ and stay there until the owner has checked them in the game and deleted them. ## To do -- copper bars go into the furnace. also they shouldnt require the player to press F to collect. instead it should automatically eject ingots below and the player can have a belt there. - stacking ingots is a little odd, they dont stack neatly. so putting them all in the same place means it will get full but have lots of air space. what can we do about this? WHat in real life might be the solution for storing ingots? -- i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. +- i have noticed that belts with a big pile on top only move the grains on the top of the stack. the belt should move the whole stack. if this is an issue for performance, maybe a nice solution is to stall the belt when there is too much on it. then the player must build in a way to control the flow to that belt. update: i found that its because when there is a screw tube at the end of the belt, it stops the lower grains and only the ones at the top move. the screw tube shouldnt block them, it should suck them in. - (agent) Written after version 24, which already tried to fix this: carryColumn in src/sim/conveyors.js moves the whole stack, up to 96 grains deep. A one-off headless check of a tall heap moving together passed then (it was not kept as a suite test), so the real game must differ from that check somehow. Reproduce it in the game first (tools/shot.py, or play it), then add a suite test that fails, then fix. - instead of full description on to left for build menu, mouse hover reviels a new info popup. organize the popup look so its nice and easy to read the information. - dont need the short description in the build menu if we have a hover info popup. - lets think about how best to design the factory products. basics are normal smelting ore, refining, etc. but the actual export products could be like real construction materials. materials that a real construction site might need. - the world still seems quite small. i dont know if thats because the bot moves so fast, the bot can mine too fast, or the grains should be smaller... i would like to make the worl seem huge to the player and give them loads of resources to mine for a long game. as is, you could mine all the resources from the map pretty quickly and end up having to start a new world and new game as a result. or maybe there is some other solution that i havent thought of yet. need to brainstorm this. +- there is a sound effect that seems to happen randomly but constantly. sounds like a circuit breaker flipping on or off or something. maybe like a ruller snapping on a desk. what is it? its not new ive just been ignoring it. actually it may be a part of the background music. but it seems out of place. not sure if its the music or not. it folows the player wherever it goes. but maybe the sound effects are not local? +- for buildings that need a material selection, lets make a new material selection menu +- lets rework the vacuum/blower tool. the selecting, ticking, etc of the items for filtering and otherwise. its a little confusing and difficult to understand. lets ocme up with a simpler ui/ux, intuitive while still containing the same core functions like filtering what you pickup and drop and allowing pickup/drop all or filtered. +- the silo art looks the same for both pixelated graphiocs and smooth. + ## Future features and ideas @@ -23,7 +27,6 @@ and stay there until the owner has checked them in the game and deleted them. ## Done, please check -- for the build menu, add shift + q/e to move tabs. - - (agent) Shift+Q and Shift+E switch build-menu tabs, wrapping at the ends; each tab remembers the piece you last chose in it. Plain Q/E still cycle pieces. The keys are in the menu hint and the notebook's controls list. -- in the build menu, having the "g pick" there is confusing. im not sure if it should be there or not. also middle mouse the default for pick is better, g shouldnt even be used at all, its too far from the normal keys people use around wasd. maybe later for something less used. - - (agent) G does nothing now and is free for something later. Middle mouse picks, and the menu hint, build card and notebook controls list say so. +- copper bars go into the furnace. also they shouldnt require the player to press F to collect. instead it should automatically eject ingots below and the player can have a belt there. + - (agent) Bars now drop out of the bottom of an ingot mold by themselves as soon as they set, never up into the furnace. Leave one open block under the mold and put a belt below that: the bars land on it and ride off (a belt touching the mold's underside counts as blocked). If there is no room, the bars wait in the mold and a message says so once. The bit of metal too short for a bar stays in the trough for the next firing. F on the mold is now optional upkeep: it empties the trough, chips off slag and rakes the furnace grate. To check: run a furnace over a mold with a belt a block below. + diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 16ad374..558261f 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -46,7 +46,7 @@ decides something new, add it under "Decisions" with the date. 2. Vacuum up what you want (tick materials to take only those), and blow it out where you need it. 3. Build a furnace from wall blocks, with a grate floor over a mold. Charge it with ore and coal, light it, and let the molten metal run into the mold. -4. Knock out bars and sell them at the trade dock, or fill buyers' orders, which pay more. +4. Bars drop out of the bottom of the mold; collect them and sell them at the trade dock, or fill buyers' orders, which pay more. 5. Better bits (copper, then iron, then steel) open harder rock: granite, then basalt. Deeper ores give tin, zinc, silver and gold, and alloys (bronze, brass) pay more. 6. Automate with belts, screw tubes, filters, fans, pipes and machines. Pay off the claim. @@ -77,7 +77,10 @@ Details and numbers live in the code and in `docs/systems/`. - Blocks: walls of any of several materials, belts, screw tubes, filters, grates, gates, pipes, pumps, outlets. - Machines: fan, Refiner, Stone cutter, Silo. - - Molds: ingot, bit, tank. + - Molds: ingot, bit, tank. Ingot bars drop out of the bottom onto whatever is under the mold; + leave one open block below and a belt under that carries them off. F is optional upkeep: it + cleans the trough, chips off slag and rakes the furnace grate. Short stubs stay for the next + firing. - Build actions: drag for lines, Shift-drag for rectangles, Ctrl to replace, hold C / X and drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse to pick, Shift+Q/E to switch build tabs. @@ -132,6 +135,9 @@ above, including: does not replace it. **2026-10-23:** +- **Ingot molds drop bars below automatically.** The owner reported bars being thrown into the + furnace above and wants belts underneath to carry them off. Blocked bars wait in the trough; + short stubs stay for the next firing. F stays available for cleaning and raking. - **Screw tubes carry no more than belts:** they move every other tick. For more throughput, build more lines. - **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. The owner diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 48c9271..58c8285 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -199,7 +199,7 @@ Material ids, the material table (def), per-material property arrays, cell flags - functions: def, metalFamily - variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, 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 (140 lines) +### src/sim/pieces.js (139 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, 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 @@ -257,9 +257,9 @@ How belts, filters and screw tubes move what is on or in them. What rests on a b Building blocks: placing, integrity, removing, lines and rectangles of blocks. - functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks -### src/sim/molds.js (284 lines) -Molds: cavities that fill with molten metal (or glass) and are knocked out as bars. -- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, checkMolds, moldAt, knockNote, tally, collectMold, removeMold +### src/sim/molds.js (297 lines) +Molds: cavities for castings; ingot troughs drop bars out of the bottom. +- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold - variables: DROSS, grains ### src/sim/items.js (134 lines) @@ -426,7 +426,7 @@ Previews drawn under the cursor: build ghosts, drill aim and pulses, selections, - functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawPreviews, drawTag, drawSelection, drawPastePreview - variables: CRACK_N, CRACK_LV, SEL_COL -### src/ui/events.js (44 lines) +### src/ui/events.js (50 lines) Events from the simulation (emit) turned into sounds, toasts and cards. - functions: drainEvents @@ -447,7 +447,7 @@ The frame loop and boot. - `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks. - `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys. - `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: knock-outs leave the mold clean, plugs over the mold 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 Refiner grinds waste rock for trace ore. - `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 stone cutter, the silo and builders' orders. diff --git a/docs/agent-log.md b/docs/agent-log.md index 1467911..8e38baf 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,27 @@ 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-23: ingots drop out of the bottom of the mold (Claude Code, with Codex) + +Note: "copper bars go into the furnace. also they shouldnt require the player to press F…". +The owner confirmed the first part was a bug: the knock-out threw bars up into the furnace. + +- `src/sim/molds.js`: `ejectIngot` runs from `checkMolds` (every 15 ticks). Bars spawn directly + under the mold, only where `entFreeAt` says there is room; the rest wait in the trough. Metal + is taken from the top so the short stub stays on the floor. `ingot_drop` and `ingot_blocked` + events (blocked is said once per spell via `mo.blockedSaid`, not saved). F on a ready ingot + never launches bars upward; F on an empty, short or spoiled mold still cleans and rakes. +- `src/ui/events.js`: first drop gets a card, later drops a toast; blocked gets a toast. Ingots + no longer emit `cast_ready`. +- Text: `pieces.js`, `progress.js`, `notebook.js`, `info.js`, `docs/DESIGN.md` (with the + decision), `docs/systems/stone-and-liquids.md`, `docs/PROJECT_MAP.md`. +- Tests: `t_molds` (38 checks: downward drops, stub kept, top-up, blocked and warned once, + partial drops, full entity pool, slag), `t_basics`, `t_stone`, `furnace_play.js` and + `play.py` section 6 changed to the new behaviour. All 11 headless suites, `play.py` (109) and + `t_smooth.py` (8) pass. A shot of a mold with a belt one block below shows the bars riding off. +- Open: a belt touching the mold's underside counts as blocked; bars need one open block. The + owner may want a belt directly under the mold to work. + ## 2026-10-23: Shift+Q/E build tabs, G pick key removed (Claude Code, with Codex) Notes: "add shift + q/e to move tabs" and "having the "g pick" there is confusing". diff --git a/docs/systems/stone-and-liquids.md b/docs/systems/stone-and-liquids.md index 04c732d..c005249 100644 --- a/docs/systems/stone-and-liquids.md +++ b/docs/systems/stone-and-liquids.md @@ -70,7 +70,7 @@ through another refiner. ## Uses for stone and sand - **Glass bricks.** - Sand melts to molten glass in a fan furnace (peak about 2000°C measured). - - Cast in an Ingot mold, it knocks out as glass bricks, worth $14 each. + - Cast in an Ingot mold, it drops out of the bottom as glass bricks, worth $14 each. - Glass bricks build glass wall blocks. Glass is too brittle for a bit mold. - **Stone cutter** (machine, 2×2, $90). - A saw under a hopper. Every 8 grains of stone rubble become one stone brick, which drops out diff --git a/src/sim/molds.js b/src/sim/molds.js index 856239f..b4939aa 100644 --- a/src/sim/molds.js +++ b/src/sim/molds.js @@ -1,4 +1,4 @@ -// Molds: cavities that fill with molten metal (or glass) and are knocked out as bars. +// Molds: cavities for castings; ingot troughs drop bars out of the bottom. // ---------- Molds ---------- function moldCellsAt(kind, x, y) { const md = MOLDS[kind]; const walls = []; @@ -68,7 +68,7 @@ function cavityState(cav) { return { st, fill: (solid + liq) / n, metal, flaw }; } // The ingot trough: ready as soon as at least one bar's worth has set and nothing is still molten. -// It is forgiving: slag, ash or ore that got in is skimmed off when you knock the ingot out. +// Bars drop automatically; the short stub stays on the floor. F cleans out slag and grit. function troughState(mo) { let solid = 0, liq = 0, junk = 0, metal = -1, mixed = false; for (const i of mo.cavs[0]) { @@ -159,13 +159,40 @@ function unplugMold(mo) { return { metal, chipped: got.dross || 0, raked: rakeAbove(mo) }; } const grains = n => `${n} ${n === 1 ? 'grain' : 'grains'}`; +const barWord = (m, n) => m === GLASS ? (n === 1 ? 'glass brick' : 'glass bricks') : n === 1 ? 'bar' : 'bars'; +function ejectIngot(mo) { + let r = troughState(mo), n = 0; + const metal = r.metal, x = mo.x + (mo.w >> 1), y = mo.y + mo.h; + if (r.st === 'ready') { + let T = 0, solid = 0, free = false; + for (const i of mo.cavs[0]) if (SOLIDMETAL[mat[i]] && castMix(metal, mat[i]) === metal) { T += temp[i]; solid++; } + T /= Math.max(1, solid); + for (let k = 0; k < 3 && n < r.bars; k++) { + const sx = mo.x + 1 + k * 4; + if (!entFreeAt(sx, y)) continue; + free = true; + if (spawnBar(BAR_OF[metal], sx, y, (rnd() - 0.5) * 0.15, 0.2, T)) n++; + } + if (n) { + // Take from the top so the short stub stays on the trough floor. + let take = n * GRAINS_PER_BAR; + for (const i of mo.cavs[0].slice().sort((a, b) => a - b)) if (take > 0 && SOLIDMETAL[mat[i]] && castMix(metal, mat[i]) === metal) { setCell(i, EMPTY, AMB[(i / W) | 0], 0); take--; } + const first = stats.cast === 0; + stats.bars += n; stats.cast++; mo.blockedSaid = false; + emit({ t: 'sfx', s: 'cast' }); + emit({ t: 'ingot_drop', mold: mo.id, metal, bars: n, x, y, first }); + r = troughState(mo); + } else if (!free && !mo.blockedSaid) { mo.blockedSaid = true; emit({ t: 'ingot_blocked', mold: mo.id, x, y }); } + } + if (r.st === 'empty') mo.blockedSaid = false; + mo.ready = []; if (r.st === 'ready') for (let k = 0; k < r.bars; k++) mo.ready.push(k); + mo.state = r.st; mo.fill = r.fill; mo.metal = r.metal; mo.flaw = r.flaw; + return n; +} function checkMolds() { for (const mo of molds) { if (mo.kind === 'ingot') { - const r = troughState(mo); - mo.ready = []; if (r.st === 'ready') for (let k = 0; k < r.bars; k++) mo.ready.push(k); - if (r.st === 'ready' && mo.state !== 'ready') emit({ t: 'cast_ready', mold: mo.id, kind: mo.kind, metal: r.metal, bars: r.bars, x: mo.x + (mo.w >> 1), y: mo.y }); - mo.state = r.st; mo.fill = r.fill; mo.metal = r.metal; mo.flaw = r.flaw; + ejectIngot(mo); continue; } const res = mo.cavs.map(cavityState); @@ -213,28 +240,15 @@ function collectMold(mo) { return 'Knocked the mold clean.' + knockNote(scrap, g.other, g.dross + u.chipped, u.raked); } let msg = ''; + if (mo.kind === 'ingot' && mo.state === 'ready') { + const n = ejectIngot(mo); + return n ? `Dropped ${n} ${barWord(mo.metal, n)} out of the bottom of the mold.` : 'Bars drop out of the bottom of the mold. Leave one open block under it, with a belt below that to carry them off.'; + } if (mo.kind === 'ingot') { - const cav = mo.cavs[0]; - let solid = 0, T = 0; - const m = mo.metal; - if (mo.state === 'ready') for (const i of cav) if (SOLIDMETAL[mat[i]]) { solid++; T += temp[i]; } - T /= Math.max(1, solid); - const bars = Math.floor(solid / GRAINS_PER_BAR); - // the ingot snaps into bars that pop up and out of the trough; take the metal for them first - let take = bars * GRAINS_PER_BAR; - for (const i of cav.slice().sort((a, b) => b - a)) if (take > 0 && SOLIDMETAL[mat[i]] && castMix(m, mat[i]) === m) { setCell(i, EMPTY, AMB[(i / W) | 0], 0); take--; } - let n = 0; - for (let k = 0; k < bars; k++) { - const e = spawnBar(BAR_OF[m], mo.x + 1 + k * 4, mo.y - 3, (k - (bars - 1) / 2) * 0.3 + (rnd() - 0.5) * 0.15, -0.55, T); - if (e) n++; else bagAdd(BAR_OF[m], 1, true); - } - // the stub too short for a bar, and anything else that got in, are cleared out: the trough is left clean - const u = unplugMold(mo), g = tally(clearCavity(cav)); - stats.bars += n; if (n) stats.cast++; - mo.state = 'empty'; mo.ready = []; - emit({ t: 'sfx', s: n ? 'cast' : 'ui' }); - const word = m === GLASS ? (n === 1 ? 'glass brick' : 'glass bricks') : n === 1 ? 'bar' : 'bars'; - return (n ? `Knocked out ${n} ${word}. Vacuum them up.` : 'Knocked the trough clean.') + knockNote(g.scrap + u.metal, g.other, g.dross + u.chipped, u.raked) + furnaceJunkHint(mo); + const u = unplugMold(mo), g = tally(clearCavity(mo.cavs[0])); + mo.state = 'empty'; mo.ready = []; mo.blockedSaid = false; + emit({ t: 'sfx', s: 'ui' }); + return 'Knocked the trough clean.' + knockNote(g.scrap + u.metal, g.other, g.dross + u.chipped, u.raked) + furnaceJunkHint(mo); } if (mo.state === 'short' && !(mo.knockAsk != null && tick - mo.knockAsk >= 0 && tick - mo.knockAsk < 300)) { // an unfinished casting can still be topped up, so the first press only says so diff --git a/src/sim/pieces.js b/src/sim/pieces.js index b3483d7..2b886f2 100644 --- a/src/sim/pieces.js +++ b/src/sim/pieces.js @@ -72,7 +72,7 @@ const PIECES = [ { id: 'dock', name: 'Trade dock', kind: 'machine', bw: 6, bh: 4, cost: [], hidden: true, fixed: true, desc: 'Owned by the claim office. Anything that drops through the grate in its floor is sold on the spot. Build walls around it to funnel more in.' }, { id: 'ingotmold', name: 'Ingot mold', kind: 'mold', mold: 'ingot', variants: 'wall', - desc: 'A long trough. Pour molten metal in, let it set, press F: it snaps into bars of 8 grains, up to 3.' }, + desc: 'A long trough. Pour molten metal in: bars of 8 grains drop out of the bottom when set, up to 3. Leave one open block below it; a belt under that carries them off. F cleans the trough, chips off slag and rakes the furnace grate.' }, { id: 'bitmold', name: 'Bit mold', kind: 'mold', mold: 'bit', variants: 'wall', desc: 'A drill-bit-shaped cavity. Cast a harder metal in it to upgrade your drill.' }, { id: 'tankmold', name: 'Tank mold', kind: 'mold', mold: 'tank', variants: 'wall', @@ -86,8 +86,7 @@ const WHEEL_K = 0.32, ENGINE_K = 40; const MOLDS = { ingot: { - // one long trough: metal runs along it and sets level; knocking it out - // snaps the ingot into bars of 8 grains each + // one long trough: metal sets level and drops bars of 8 grains below name: 'Ingot mold', rows: [ '................', '#oooooooooooooo#', diff --git a/src/sim/progress.js b/src/sim/progress.js index 6f0e5f1..4cf992a 100644 --- a/src/sim/progress.js +++ b/src/sim/progress.js @@ -15,7 +15,7 @@ function smelterBlueprint(kind, count, ch, wall) { } const STARTER_DESIGNS = [ { id: 'smelter1', name: 'Copper smelter', bp: () => smelterBlueprint('ingot', 1, 3, RSTONE), - desc: 'Stone furnace over an ingot mold. Charge it with about 50 copper ore and 50 coal, mixed (or the ore first with the coal on top), and light the coal. Makes 3 bars a firing. Press F on the mold to knock the bars out: that also rakes the slag off the grate, so it is ready to charge again. Leftover copper goes to your bag as scrap; throw it back in with the next charge.' }, + desc: 'Stone furnace over an ingot mold. Charge it with about 50 copper ore and 50 coal, mixed (or the ore first with the coal on top), and light the coal. Makes 3 bars a firing. Bars drop out of the bottom of the mold onto whatever is under it; leave one open block under it, and a belt below that carries them off. Leftover copper stays for the next firing. F on the mold is optional: it cleans the trough, chips off slag and rakes the furnace grate.' }, { id: 'bitfurnace', name: 'Bit casting furnace', bp: () => smelterBlueprint('bit', 1, 3, RSTONE), desc: 'Stone furnace over a bit mold. Charge it with about 70 copper ore and 70 coal, mixed (or the coal on top), and light the coal. Press F once the bit has set. For iron, build it from brick and blow air in with a fan.' }, ]; @@ -28,7 +28,7 @@ const MILESTONES = [ { id: 'charcoal', title: 'Charcoal', hint: 'Wood that gets hot without air turns into charcoal instead of burning. Bury a burning pile of wood chunks under dirt, or heat a sealed box of wood chunks from outside.', reward: 'A fuel you can grow: nearly as hot as coal.' }, { id: 'grow', title: 'Plant a tree', msg: 'Wood is renewable. Plant rows of acorns on dirt to farm it.', hint: 'Cutting down a tree drops acorns. Leave one on dirt or grass under open sky and it sprouts, then grows into a tree in about a minute and a half.' }, { id: 'copper', title: 'Molten copper', msg: 'Molten copper sinks to the bottom of the charge. Get it into a mold before it sets.', hint: 'Copper ore melts at 1085°C. An open fire loses too much heat, so wall coal and ore in with blocks and leave a small vent at the top for smoke. Mix ore and coal about half and half: tick both in your bag and pick Ticked mix with Q/E, and right click blows an even mix.' }, - { id: 'bars', title: 'Copper bars', msg: 'Bars sell for far more than the ore in them, and make metal wall blocks.', hint: 'Build an Ingot mold (Build, 3) right under the furnace with a floor of clay Grates between them: molten copper drips through, while the charge and the thick slag stay up. When the bars set, press F on the mold: the bars pop out and the grate is raked clean of slag. Then vacuum up the bars. Copper that sets in the furnace melts again next firing.' }, + { id: 'bars', title: 'Copper bars', msg: 'Bars sell for far more than the ore in them, and make metal wall blocks.', hint: 'Build an Ingot mold (Build, 3) right under the furnace with a floor of clay Grates between them: molten copper drips through, while the charge and the thick slag stay up. When the bars set, they drop out of the bottom of the mold onto whatever is under it; a belt carries them off. Leave space below, or vacuum up the bars. F on the mold is optional: it cleans the trough, chips off slag and rakes the furnace grate. Copper that sets in the furnace melts again next firing.' }, { id: 'bit1', title: 'Copper drill bit', hint: 'Cast copper in a Bit mold. Building the furnace right on top works. A Grate over the funnel keeps the charge out. Let it set, then press F.', reward: 'Copper bit. You can now dig iron ore.' }, { id: 'auto', title: 'Moving parts', msg: 'Machines can now haul for you. Feed furnaces automatically, and lift ore out of your mines.', hint: 'Belts carry grains and bars resting on them. Screw tubes push loose material along (never liquids); stacked pointing up they lift it out of a mine. A Filter lets one material fall through and carries the rest along, so you can sort ore from rubble. Move 300 grains with machines.' }, { id: 'glass', title: 'Glass', hint: 'Sand melts near 1300°C. Still air will not get a fire that hot: point a Fan into the fuel. Molten glass runs through a grate like metal, so cast it in an Ingot mold for glass bricks, which sell well.', reward: 'Flask unlocked. The Vacuum can suck up and blow out liquids.' }, diff --git a/src/ui/events.js b/src/ui/events.js index f9d78d4..1b40527 100644 --- a/src/ui/events.js +++ b/src/ui/events.js @@ -12,9 +12,15 @@ function drainEvents() { if (m && !['bit1', 'bit2', 'bit3', 'tank'].includes(m.id)) { showCard({ kind: 'Milestone', title: m.title, body: [m.msg, m.reward], accent: 'milestone' }); playSfx('milestone'); } } else if (e.t === 'cast_ready') { const metal = MD[e.metal] ? MD[e.metal].name : 'Metal'; - const body = e.kind === 'ingot' ? `${e.bars} ${e.bars === 1 ? 'bar' : 'bars'} worth has set. Press F on the mold to knock ${e.bars === 1 ? 'it' : 'them'} out.` : `The ${e.kind} casting has set. Press F on the mold to knock it out.`; - showCard({ kind: 'Casting ready', title: `${metal} ${e.kind === 'ingot' ? 'ingot' : e.kind}`, swatch: e.metal > 0 ? e.metal : 0, body: [body], accent: 'milestone' }); + const body = `The ${e.kind} casting has set. Press F on the mold to knock it out.`; + showCard({ kind: 'Casting ready', title: `${metal} ${e.kind}`, swatch: e.metal > 0 ? e.metal : 0, body: [body], accent: 'milestone' }); playSfx('cast'); + } else if (e.t === 'ingot_drop') { + const metal = MD[e.metal] ? MD[e.metal].name : 'Metal', body = `${e.bars} ${barWord(e.metal, e.bars)} dropped out of the mold.`; + if (e.first) showCard({ kind: 'Casting', title: e.metal === GLASS ? 'Glass bricks' : `${metal} bars`, swatch: e.metal, body: [body], accent: 'milestone' }); + else toast(body); + } else if (e.t === 'ingot_blocked') { + toast('Bars are ready in a mold but there is no room under it. Leave one open block under the mold: bars drop out of the bottom.'); } else if (e.t === 'upgrade') { playSfx('upgrade'); if (SET.shake) R.shake = 1.4; showCard({ kind: e.kind === 'bit' ? 'Drill bit upgraded' : 'Bag upgraded', title: e.kind === 'bit' ? BIT_TIERS[e.tier].name : TANK_TIERS[e.tier].name, accent: 'milestone', diff --git a/src/ui/info.js b/src/ui/info.js index 6cd7c61..02434d5 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -123,7 +123,7 @@ function hoverInfo(ax, ay) { const mo = moldAt(ax, ay); if (mo) { t1 = MOLDS[mo.kind].name; - t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set · press F` : `${MD[mo.metal].name} casting set · press F`) : mo.state === 'flawed' || mo.state === 'short' ? mo.flaw : mo.state === 'filling' ? `filling ${Math.round(mo.fill * 100)}%` : 'empty']; + t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set · leave space below` : `${MD[mo.metal].name} casting set · press F`) : mo.state === 'flawed' || mo.state === 'short' ? mo.flaw : mo.state === 'filling' ? `filling ${Math.round(mo.fill * 100)}%` : 'empty']; if (player.lens) t2.push(fmtT(temp[i])); } const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay); diff --git a/src/ui/notebook.js b/src/ui/notebook.js index 40b82fb..e86ce72 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -194,7 +194,7 @@ function controlsHtml() { 2. With the Vacuum, click on coal to suck up only coal, then the copper ore, rubble and clay you want.
3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.
4. Tick coal and copper ore in your bag and pick Ticked mix (Q/E, or click it at the bottom of the bag). Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.
- 5. When the ingot sets, press F on the mold and vacuum up the bars. That also rakes the slag and ash off the grate, so the furnace is ready for the next charge. Scrap goes to your bag: throw it in with the next charge.
+ 5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or vacuum them up. Leftover metal stays for the next firing. F on the mold is optional: it cleans the trough, chips off slag and rakes the furnace grate. Scrap goes to your bag: throw it in with the next charge.
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw tubes and fans; pay down the claim there too.
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.

`; diff --git a/test/furnace_play.js b/test/furnace_play.js index 7f403ba..9ab133c 100644 --- a/test/furnace_play.js +++ b/test/furnace_play.js @@ -65,8 +65,11 @@ for (let f = 0; f < firings; f++) { } else ticks(run, 5400); const st = run(`JSON.stringify(molds.map(m => m.state + (m.flaw ? ':' + m.flaw : '')))`); if (process.env.DUMP === 'each') { console.log('-- before knock-out, firing', f + 1, st); dump(); } + // Vacuum earlier drops, open the outlet, then do the optional F upkeep. + let bars = run('ents.length'); + run('for (const e of ents.slice()) removeEnt(e); for (const mo of molds) if (mo.kind === "ingot") for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, AMB[y], 0); checkMolds();'); + bars += run('ents.length'); totalBars += bars; const msgs = JSON.parse(run(`JSON.stringify(molds.map(m => collectMold(m)))`)); - const bars = run('ents.length'); totalBars += bars; if (run('player.bitTier') > bits) bits = run('player.bitTier'); run('for (const e of ents.slice()) removeEnt(e);'); const left = { ore: count(run('CUORE'), false), coal: count(run('COAL'), false), slag: count(run('SLAG'), false), ash: count(run('ASH'), false), copperOut: count(run('COPPER'), false) + count(run('SCRCU'), false) }; diff --git a/test/play.py b/test/play.py index 4bfa6ce..28b3231 100644 --- a/test/play.py +++ b/test/play.py @@ -254,21 +254,19 @@ with sync_playwright() as p: check('bot can sit inside a built wall', not ev(f'botOverlapSolid({wx_ - 2}, {wy_ - 2})')) check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})')) - # 6. ingot mold: click to place, pour, knock out bars, vacuum them + # 6. ingot mold: click to place, pour, bars drop below, vacuum them ok = select('3', "piece().id === 'ingotmold'") check('E cycles to the ingot mold', ok) - spot = ev(f'''(() => {{ for (let dx = -2; dx <= 10; dx++) for (let dy = 6; dy < 14; dy++) {{ const ox = {bbx} + dx, oy = {bby} - dy; if (!canPlaceMold('ingot', ox - 1, oy) && inReach(ox * 4 + 8, oy * 4 + 2)) return [ox, oy]; }} return null; }})()''') + spot = ev(f'''(() => {{ for (let dx = -2; dx <= 10; dx++) for (let dy = 6; dy < 14; dy++) {{ const ox = {bbx} + dx, oy = {bby} - dy; if (!canPlaceMold('ingot', ox - 1, oy) && [0, 1, 2].every(k => entFreeAt((ox - 1) * 4 + 1 + k * 4, oy * 4 + MOLDS.ingot.h)) && inReach(ox * 4 + 8, oy * 4 + 2)) return [ox, oy]; }} return null; }})()''') check('found a spot for the mold', spot is not None) mx, my = blk(spot[0], spot[1]) pg.mouse.move(mx, my); settle(100); pg.mouse.down(); pg.mouse.up(); settle(100) check('mold placed by click', ev('molds.length') == 1, ev('GAME.status')) ev('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)') - pg.wait_for_function("molds[0].state === 'ready'", timeout=30000) - check('bars set in the mold', ev("molds[0].state === 'ready' && molds[0].ready.length === 3"), ev('[molds[0].state, molds[0].ready.length]')) + pg.wait_for_function("ents.filter(e => e.m === BARCU).length === 3", timeout=30000) + check('bars set and drop on their own', ev("ents.length === 3 && molds[0].state === 'short' && molds[0].ready.length === 0"), ev('[molds[0].state, ents.length]')) pg.screenshot(path=str(out / '05-mold-ready.png')) - mo = ev('[molds[0].x + 8, molds[0].y + 2]') - pg.mouse.move(*cell_to_screen(*mo)); settle(60); pg.keyboard.press('f'); settle(300) - check('F knocks out three bars', ev('ents.length') == 3, ev('GAME.status')) + check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])')) settle(1500) pg.screenshot(path=str(out / '06-bars.png')) pg.keyboard.press('2'); settle(60) diff --git a/test/t_basics.js b/test/t_basics.js index 70ede24..56e152e 100644 --- a/test/t_basics.js +++ b/test/t_basics.js @@ -44,15 +44,14 @@ check('stone wall line placed', JSON.parse(r1).n === 5 && run('bag[RSTONE]') === check('wall cells are solid stone at grid positions', run('mat[80*W+100]') === run('STONE') && run('mat[83*W+119]') === run('STONE')); const rm = run("removePieceAt(101, 81)"); check('taking a wall block back refunds its rubble', run('bag[RSTONE]') === 200 - 64, rm + ' ' + run('bag[RSTONE]')); -// 5. ingot mold: pour molten copper, knock out bars +// 5. ingot mold: pour molten copper, bars drop below run('bag[RSTONE] = 200; bagTotal = 0; for (let k=0;k e.m === BARCU && e.iy >= molds[0].y + molds[0].h)')); ticks(run, 200); check('bars fall and come to rest', run('ents.every(e => Math.abs(e.vy) < 0.01)'), run('JSON.stringify(ents.map(e=>[e.ix,e.iy]))')); check('bars count as built-up copper', run('stats.bars') === 3); diff --git a/test/t_molds.js b/test/t_molds.js index 29c3b77..fdf350a 100644 --- a/test/t_molds.js +++ b/test/t_molds.js @@ -1,4 +1,4 @@ -// Molds and furnaces stay usable: knock-outs leave the mold clean, plugs over the mold are +// 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. const { load, suite, flatWorld, rect, ticks, count } = require('./harness'); @@ -7,13 +7,39 @@ const { check, done } = suite(); function fresh() { flatWorld(run, 240, 160, 100); run('bot.x = 110; bot.y = 70; bag.fill(0); bagTotal = 0; for (const e of ents.slice()) removeEnt(e);'); } const cavCount = (k, m) => run(`molds[${k}].cavs[0].filter(i => mat[i] === ${m}).length`); -// an ingot with 27 grains of copper: 3 bars, and the 3 left over go to the bag, not back in the trough +// automatic drops leave the short stub on the trough floor for the next firing fresh(); -run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); -run('{ const c = molds[0].cavs[0].slice().sort((a, b) => b - a); for (let k = 0; k < 27; k++) { setCell(c[k], COPPER, 30, F_CAST); } } checkMolds();'); -let msg = run('collectMold(molds[0])'); -check('knocking out an ingot gives the bars', run('ents.length') === 3 && run('ents.every(e => e.m === BARCU)'), msg); -check('and leaves the trough empty, the scrap in your bag', run('molds[0].cavs[0].every(i => mat[i] === EMPTY)') && run('bag[SCRCU]') === 3, `scrap in bag ${run('bag[SCRCU]')}`); +run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; EV.length = 0;'); +run('{ const c = molds[0].cavs[0].slice().sort((a, b) => b - a); for (let k = 0; k < 27; k++) setCell(c[k], COPPER, 30, F_CAST); } checkMolds();'); +let msg; +check('an ingot drops three copper bars below the mold', run('ents.length === 3 && ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h && e.vy > 0 && e.T === 30)')); +check('three grains stay on the trough floor, nothing goes to the bag', cavCount(0, run('COPPER')) === 3 && run('bagTotal === 0 && molds[0].cavs[0].filter(i => mat[i] === COPPER).every(i => (i / W | 0) === molds[0].y + molds[0].h - 2)')); +check('ingots report drops instead of casting ready', run('EV.filter(e => e.t === "ingot_drop").length === 1 && !EV.some(e => e.t === "cast_ready") && stats.bars === 3 && stats.cast === 1')); +run('for (const e of ents.slice()) removeEnt(e); { const c = molds[0].cavs[0].filter(i => mat[i] === EMPTY).sort((a, b) => b - a); for (let k = 0; k < 5; k++) setCell(c[k], COPPER, 30, F_CAST); } checkMolds();'); +check('five more grains top up the stub into another bar', run('ents.length === 1 && ents[0].m === BARCU && molds[0].state === "empty"') && cavCount(0, run('COPPER')) === 0); +// blocked mouths retain their metal and warn once, including molds from old saves +fresh(); +run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; EV.length = 0; { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, STONE, 30, 0); } checkMolds(); checkMolds();'); +check('blocked bars stay put and warn only once', run('ents.length === 0 && molds[0].state === "ready" && EV.filter(e => e.t === "ingot_blocked").length === 1') && cavCount(0, run('COPPER')) === 28); +msg = run('collectMold(molds[0])'); +check('F on a blocked mold never launches bars upwards', run('ents.length === 0') && /open block under/.test(msg), msg); +run('{ const mo = molds[0]; for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, 30, 0); }'); +msg = run('collectMold(molds[0])'); +check('F on a ready mold drops bars only below it', run('ents.length === 3 && ents.every(e => e.iy === molds[0].y + molds[0].h) && !molds[0].blockedSaid') && /Dropped 3 bars/.test(msg), msg); +run('for (const e of ents.slice()) removeEnt(e); { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let x = mo.x; x < mo.x + mo.w; x++) setCell((mo.y + mo.h) * W + x, STONE, 30, 0); } checkMolds();'); +check('a new blocked spell warns again', run('EV.filter(e => e.t === "ingot_blocked").length === 2')); +run('{ const mo = molds[0]; for (let x = mo.x; x < mo.x + mo.w; x++) setCell((mo.y + mo.h) * W + x, EMPTY, 30, 0); } checkMolds();'); +check('clearing the obstruction lets the next check drop bars', run('ents.length === 3 && !molds[0].blockedSaid') && cavCount(0, run('COPPER')) === 4); +// one free mouth drops one bar; occupied mouths keep the rest for later +fresh(); +run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); spawnBar(BARCU, mo.x + 1, mo.y + mo.h); for (let x = mo.x + 9; x < mo.x + 13; x++) setCell((mo.y + mo.h) * W + x, STONE, 30, 0); } checkMolds();'); +check('partial ejection uses the free middle mouth and retains two bars', run('ents.length === 2 && ents[1].ix === molds[0].x + 5 && ents[1].iy === molds[0].y + molds[0].h && molds[0].ready.length === 2') && cavCount(0, run('COPPER')) === 20); +run('for (const e of ents.slice()) removeEnt(e); { const mo = molds[0]; for (let x = mo.x + 9; x < mo.x + 13; x++) setCell((mo.y + mo.h) * W + x, EMPTY, 30, 0); } checkMolds();'); +check('remaining bars drop when mouths open', run('ents.length === 2 && molds[0].state === "short"') && cavCount(0, run('COPPER')) === 4); +// a full loose-object pool must not consume the casting or put bars in the bag +fresh(); +run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; for (const i of molds[0].cavs[0]) setCell(i, COPPER, 30, F_CAST); ents.length = 4000; checkMolds(); ents.length = 0;'); +check('failed spawns preserve all metal and leave the bag alone', cavCount(0, run('COPPER')) === 28 && run('bagTotal === 0 && molds[0].ready.length === 3')); // a plate of copper set over the trough, and drips frozen in the grate above: knocked loose too fresh(); run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); @@ -27,9 +53,9 @@ check('the grate itself is still there', run('part[(molds[0].y - 1) * W + molds[ fresh(); run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); run('{ const c = molds[0].cavs[0].slice().sort((a, b) => b - a); for (let k = 0; k < 16; k++) setCell(c[k], COPPER, 30, F_CAST); for (let k = 16; k < 20; k++) setCell(c[k], SLAG, 30, 0); } checkMolds();'); -check('an ingot with slag on top is still ready', run('molds[0].state') === 'ready', run('molds[0].state')); +check('slag on top does not stop automatic bars', run('ents.length === 2 && ents.every(e => e.iy >= molds[0].y + molds[0].h)') && cavCount(0, run('SLAG')) === 4 && run('bagTotal === 0')); msg = run('collectMold(molds[0])'); -check('the slag is chipped off and thrown away, and two bars come out', run('ents.length') === 2 && run('bag[SLAG]') === 0 && /chipped off/.test(msg) && run('molds[0].cavs[0].every(i => mat[i] === EMPTY)'), msg); +check('optional F chips the slag off without putting it in the bag', run('bag[SLAG] === 0 && molds[0].cavs[0].every(i => mat[i] === EMPTY)') && /chipped off/.test(msg), msg); // a bit casting with slag in it is spoiled, but F knocks it out instead of asking you to drill fresh(); run('bag[RSTONE] = 200; bagTotal = 200; placeMoldAt("bit", 30, 20, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); diff --git a/test/t_stone.js b/test/t_stone.js index ed22c7c..62aae0a 100644 --- a/test/t_stone.js +++ b/test/t_stone.js @@ -26,8 +26,8 @@ let peak = 0; for (let t = 0; t < 6; t++) { run('for (let k = 0; k < 600; k++) simTick();'); peak = Math.max(peak, run(`(() => { let p = 0; for (let y = ${cy0}; y <= ${cy1}; y++) for (let x = ${cx0}; x <= ${cx1}; x++) p = Math.max(p, temp[y * W + x]); return Math.round(p); })()`)); } const st = run('molds[0].state'), glassIn = run('molds[0].cavs.flat().filter(i => mat[i] === GLASS).length'); check('a fan furnace melts the sand and the molten glass runs into the ingot mold', glassIn >= 8, `peak ${peak}°C, ${sand0} sand -> ${count(run, run('SAND'))}, ${glassIn} glass in the mold, state ${st}`); -const msg = run('collectMold(molds[0])'); -check('knocking it out gives glass bricks', bars(run('BARGL')) >= 2, `${bars(run('BARGL'))} bricks: ${msg}`); +run('{ const mo = molds[0]; for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, AMB[y], 0); } checkMolds();'); +check('opening space below drops glass bricks', bars(run('BARGL')) >= 2 && run('ents.every(e => e.iy >= molds[0].y + molds[0].h)'), `${bars(run('BARGL'))} bricks`); check('glass bricks sell for much more than the sand in them', run('PRICE[BARGL]') >= 8 * run('PRICE[SAND]') * 10, run('PRICE[BARGL]')); check('glass in a bit mold is too brittle to make a bit', run("METAL_TIER[GLASS]") === 0);