From 43eb4e23fdc14347057505f947d0b9f39508c4cb Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Fri, 23 Oct 2026 22:53:38 -0400 Subject: [PATCH] Ingot molds empty themselves (no F); removing a mold returns its set metal Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN.md | 11 +++-- docs/PROJECT_MAP.md | 4 +- docs/agent-log.md | 20 +++++++++ docs/systems/stone-and-liquids.md | 3 ++ src/render/overlays.js | 2 +- src/sim/actions.js | 2 +- src/sim/molds.js | 62 ++++++++++++++++++++------- src/sim/pieces.js | 2 +- src/sim/progress.js | 4 +- src/ui/info.js | 2 +- src/ui/notebook.js | 2 +- test/furnace_play.js | 4 +- test/t_molds.js | 71 +++++++++++++++++++++++++------ 13 files changed, 146 insertions(+), 43 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1f7bdbc..54a0208 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -78,9 +78,10 @@ Details and numbers live in the code and in `docs/systems/`. pipes, pumps, outlets. - Machines: fan, Refiner, Stone cutter, Silo. - 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. + leave one open block below and a belt under that carries them off. Ingot molds empty themselves: + junk and mixed scrap drop below, slag is thrown away and the grate above is raked. Short stubs + stay for the next firing. Removing any mold returns its set metal to the bag: whole bars as + bars, the rest as scrap. - Build actions: drag for lines, Shift-drag for rectangles, hold G 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. @@ -137,7 +138,7 @@ above, including: **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. + short stubs stay for the next firing. The mold cleans itself and rakes the grate after dropping bars or clearing junk. - **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. If a grain is blocked, @@ -172,6 +173,8 @@ above, including: - **The build tool has a compact top-left card** like the other tools. The owner asked for it back after it was hidden; the long description stays in the hover popup. +- **Ingot molds need no F.** They push junk and mixed metal out of the bottom as loose grains with the bars, throw slag away and rake the grate. Removing any mold returns its set metal: whole bars as bars, the rest as scrap. The owner asked what F was for once bars dropped on their own, and chose this over keeping F for cleaning. + ## Ideas not built yet - **Concrete**, as a stronger wall material from rubble. diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 5316df8..ff831c5 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -257,8 +257,8 @@ 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 (298 lines) -Molds: cavities for castings; ingot troughs drop bars out of the bottom. +### src/sim/molds.js (328 lines) +Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal. - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold - variables: DROSS, grains, barWord diff --git a/docs/agent-log.md b/docs/agent-log.md index 816645f..e41dd7e 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,26 @@ 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: ingot molds need no F; removing a mold returns its metal (Claude Code, with Codex) + +Note: "now that the ingot molds drop the ingots, what is the purpose of F…". The owner chose no F +on ingot molds. + +- `src/sim/molds.js` `ejectIngot`: when nothing over or in the mold is molten, junk and mixed + metal (as scrap) and plugs over the trough or frozen in the grate are written as loose grains + into the 4 rows under the mold; what doesn't fit waits. Slag and ash are thrown away. After a + drop or a clean-out the grate above is raked. Bars and the short stub work as before. +- `collectMold` on an ingot mold only says "Ingot molds empty themselves." Bit and tank molds + keep F. `removeMold` (every kind) gives the set metal to the bag: whole bars as bars, the rest + as scrap; molten metal is left alone. +- Text: `troughState` flaws, `info.js`, `overlays.js` (no "· F" on ingots), `pieces.js`, + `progress.js`, `notebook.js`, `docs/systems/stone-and-liquids.md`, `docs/DESIGN.md`. +- Tests: `t_molds` 60 checks (F does nothing on ingots; plate, slag and rake cases now + automatic; junk out, mixed metal out as scrap, junk waits when blocked and while molten; + removal returns bars and scrap, leaves molten metal). `furnace_play.js` adapted. All 11 + headless suites and `play.py` (122) pass. +- Open: undoing a mold removal brings the mold back empty (its metal stays in the bag). + ## 2026-10-23: compact build card top-left (Claude Code, with Codex) Note: "build tool has no info box like the other tools." The owner chose a compact card. diff --git a/docs/systems/stone-and-liquids.md b/docs/systems/stone-and-liquids.md index c005249..b8a6e95 100644 --- a/docs/systems/stone-and-liquids.md +++ b/docs/systems/stone-and-liquids.md @@ -71,6 +71,9 @@ through another refiner. - **Glass bricks.** - Sand melts to molten glass in a fan furnace (peak about 2000°C measured). - Cast in an Ingot mold, it drops out of the bottom as glass bricks, worth $14 each. + - Ingot molds empty themselves: bars, junk and mixed scrap drop below; slag is thrown away + and the grate above is raked. Removing any mold returns its set metal to the bag: whole + bars as bars, the rest as scrap. - 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/render/overlays.js b/src/render/overlays.js index 704e647..e126547 100644 --- a/src/render/overlays.js +++ b/src/render/overlays.js @@ -153,7 +153,7 @@ function drawMolds(hoverMold) { ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp); ctx.setLineDash([]); if (ready || flawed || mo === hoverMold || mo.state === 'filling' || mo.state === 'short') { - const label = ready ? `${mo.kind === 'ingot' ? (mo.ready.length === 1 ? 'Bar' : `${mo.ready.length} bars`) : mo.kind === 'bit' ? 'Bit' : 'Tank'} set · F` : flawed ? 'Flawed casting' : mo.state === 'short' ? 'Needs more metal' : mo.state === 'filling' ? `Filling ${Math.round(mo.fill * 100)}%` : MOLDS[mo.kind].name; + const label = ready ? `${mo.kind === 'ingot' ? (mo.ready.length === 1 ? 'Bar' : `${mo.ready.length} bars`) : mo.kind === 'bit' ? 'Bit' : 'Tank'} set${mo.kind === 'ingot' ? '' : ' · F'}` : flawed ? 'Flawed casting' : mo.state === 'short' ? 'Needs more metal' : mo.state === 'filling' ? `Filling ${Math.round(mo.fill * 100)}%` : MOLDS[mo.kind].name; ctx.fillStyle = 'rgba(14,12,11,0.75)'; const tw = ctx.measureText(label).width + zp * 2; ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp * 5.2, tw, zp * 4.2); diff --git a/src/sim/actions.js b/src/sim/actions.js index a801480..7501c0c 100644 --- a/src/sim/actions.js +++ b/src/sim/actions.js @@ -73,7 +73,7 @@ function interactAt(x, y) { const g = toggleGateAt(x, y); if (g) { emit({ t: 'sfx', s: 'gate' }); return g === 'open' ? 'Gate opened.' : 'Gate closed.'; } const mo = moldAt(x, y); - if (mo) { const r = collectMold(mo); if (!r.startsWith('New') && !r.startsWith('Knocked')) emit({ t: 'sfx', s: 'ui' }); return r; } + if (mo) { const r = collectMold(mo); if (mo.kind !== 'ingot' && !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_PUMP || blockP[pb] === BP_OUTLET) { blockV[pb] ^= 4; emit({ t: 'sfx', s: 'ui' }); diff --git a/src/sim/molds.js b/src/sim/molds.js index b4939aa..7bfaef9 100644 --- a/src/sim/molds.js +++ b/src/sim/molds.js @@ -1,4 +1,4 @@ -// Molds: cavities for castings; ingot troughs drop bars out of the bottom. +// Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal. // ---------- Molds ---------- function moldCellsAt(kind, x, y) { const md = MOLDS[kind]; const walls = []; @@ -34,7 +34,7 @@ function addMold(kind, bx, by, wallLoose, free) { return mo; } // Slag and ash that reach a mold float on the metal. They are chipped off and thrown -// away when the casting is knocked out, so they never spoil it unless metal set over them. +// away automatically in ingot troughs, or when other castings are knocked out. const DROSS = new Uint8Array(NMAT); DROSS[SLAG] = 1; DROSS[MSLAG] = 1; DROSS[ASH] = 1; // Two metals in one casting: iron with some steel in it (air got at it) casts as plain iron; // anything else is a mix that spoils it. Returns the metal the casting counts as, or -1. @@ -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. -// Bars drop automatically; the short stub stays on the floor. F cleans out slag and grit. +// Bars, junk and mixed scrap drop automatically; slag is discarded. Short stubs stay on the floor. function troughState(mo) { let solid = 0, liq = 0, junk = 0, metal = -1, mixed = false; for (const i of mo.cavs[0]) { @@ -79,8 +79,8 @@ function troughState(mo) { } const bars = mixed ? 0 : Math.floor(solid / GRAINS_PER_BAR), n = mo.cavs[0].length; const st = liq ? 'filling' : bars ? 'ready' : mixed ? 'flawed' : solid ? 'short' : junk ? 'flawed' : 'empty'; - const flaw = st === 'flawed' ? (mixed ? 'Two different metals mixed in the ingot. Press F to knock it out as scrap.' : 'Only junk in the trough. Press F to knock it out.') - : st === 'short' ? `Only ${solid} grains set. A bar takes ${GRAINS_PER_BAR}. Fire again to top it up, or press F to knock it out as scrap.` : ''; + const flaw = st === 'flawed' ? (mixed ? 'Two metals mixed. It drops out below as scrap.' : 'Junk in the trough. It drops out below when there is room.') + : st === 'short' ? `Only ${solid} grains set. A bar takes ${GRAINS_PER_BAR}; the next firing tops it up.` : ''; return { st, bars, fill: (solid + liq) / n, metal, flaw }; } // Empties cells: metal goes to your bag (or beside you if the bag is full) as scrap, other loose @@ -119,7 +119,7 @@ function moldPlugCells(mo) { } return out; } -// Knocking a mold out also rakes the furnace over it: slag and ash lying on the grate and in +// Ingot ejection or knocking a casting out also rakes the furnace over it: slag and ash lying on the grate and in // the charge above (up to the furnace roof) are cleared away, so the next charge sits on a // clean grate. Unburnt coal and ore stay for the next firing. Returns how many grains it raked. function rakeAbove(mo) { @@ -184,6 +184,32 @@ function ejectIngot(mo) { r = troughState(mo); } else if (!free && !mo.blockedSaid) { mo.blockedSaid = true; emit({ t: 'ingot_blocked', mold: mo.id, x, y }); } } + let molten = mo.cavs[0].some(i => CASTMETAL[mat[i]]), pushed = 0; + for (let yy = Math.max(0, mo.y - 8); yy < mo.y + mo.h && !molten; yy++) for (let xx = mo.x; xx < mo.x + mo.w; xx++) if (CASTMETAL[mat[yy * W + xx]]) { molten = true; break; } + if (!molten) { + const mixed = r.st === 'flawed' && r.metal >= 0; + const plug = moldPlugCells(mo), cavity = mo.cavs[0]; + let slot = 0; + for (const i of cavity.concat(plug)) { + const m = mat[i]; + if (m === EMPTY || PH[m] === PH_GAS || CASTMETAL[m] || (SOLIDMETAL[m] && !mixed && !plug.includes(i))) continue; + if (!DROSS[m]) { + const loose = SOLIDMETAL[m] ? (YLD[m] || SCRCU) : PH[m] === PH_SOLID ? YLD[m] : m; + if (!loose) continue; + let placed = false; + for (; slot < mo.w * Math.min(4, H - y); slot++) { + const dest = (y + ((slot / mo.w) | 0)) * W + mo.x + slot % mo.w; + if ((mat[dest] === EMPTY || PH[mat[dest]] === PH_GAS) && !occ[dest] && !part[dest]) { + setCell(dest, loose, temp[i], 0); slot++; placed = true; break; + } + } + if (!placed) continue; + } + setCell(i, EMPTY, AMB[(i / W) | 0], 0); pushed++; + } + } + if (n || pushed) rakeAbove(mo); + r = troughState(mo); 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; @@ -231,6 +257,7 @@ function tally(got) { return { scrap, other, dross: got.dross || 0 }; } function collectMold(mo) { + if (mo.kind === 'ingot') return 'Ingot molds empty themselves.'; if (mo.state === 'filling') return 'The metal has not fully set yet.'; if (mo.state === 'empty') { const g = tally(clearCavity(mo.cavs.flat())), u = unplugMold(mo); @@ -240,16 +267,6 @@ 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 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 mo.knockAsk = tick; @@ -283,6 +300,19 @@ function collectMold(mo) { } function removeMold(mo) { undoNote({ t: 'mold-', kind: mo.kind, bx: mo.x / BS, by: mo.y / BS, wall: mo.wall }); + const cavity = mo.cavs.flat(), set = cavity.filter(i => SOLIDMETAL[mat[i]]); + let cast = -1, mixed = false; + for (const i of set) { if (cast < 0) cast = mat[i]; else { const c = castMix(cast, mat[i]); if (c < 0) mixed = true; else cast = c; } } + const cx = mo.x + mo.w / 2, cy = mo.y + mo.h / 2; + const bars = mixed ? 0 : Math.floor(set.length / GRAINS_PER_BAR); + const scrap = new Map(); + for (let k = 0; k < set.length; k++) { + const i = set[k], m = mixed ? mat[i] : cast; + if (k >= bars * GRAINS_PER_BAR) { const loose = YLD[m] || SCRCU; scrap.set(loose, (scrap.get(loose) || 0) + 1); } + setCell(i, EMPTY, AMB[(i / W) | 0], 0); + } + if (bars) giveOrDrop(BAR_OF[cast], bars, cx, cy); + for (const [m, n] of scrap) giveOrDrop(m, n, cx, cy); const { walls } = moldCellsAt(mo.kind, mo.x, mo.y); let metal = 0; for (const i of walls) { diff --git a/src/sim/pieces.js b/src/sim/pieces.js index 2b886f2..ba9ef36 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: 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.' }, + 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. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.' }, { 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', diff --git a/src/sim/progress.js b/src/sim/progress.js index 4cf992a..c55e636 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. 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.' }, + 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. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.' }, { 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, 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: '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. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap. 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/info.js b/src/ui/info.js index 02434d5..6f4a4dd 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 · 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']; + t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set · drop when there is room 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 c6442fe..18c8e7f 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, 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.
+ 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. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.
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 9ab133c..529ed5f 100644 --- a/test/furnace_play.js +++ b/test/furnace_play.js @@ -65,11 +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. + // Vacuum earlier drops, open the outlet, let the ingot molds empty themselves. 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 msgs = JSON.parse(run(`JSON.stringify(molds.map(m => m.kind === "ingot" ? "Ingot molds empty themselves." : collectMold(m)))`)); 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/t_molds.js b/test/t_molds.js index fdf350a..33647ed 100644 --- a/test/t_molds.js +++ b/test/t_molds.js @@ -22,10 +22,10 @@ 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); +check('F on a blocked ingot mold changes nothing', run('ents.length === 0') && cavCount(0, run('COPPER')) === 28 && /empty themselves/.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('checkMolds()'); +check('the next check drops bars only below it', run('ents.length === 3 && ents.every(e => e.iy === molds[0].y + molds[0].h) && !molds[0].blockedSaid')); 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();'); @@ -40,22 +40,20 @@ check('remaining bars drop when mouths open', run('ents.length === 2 && molds[0] 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 +// a plate of copper and frozen grate drips are pushed out without F fresh(); run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); run('for (let bx = 30; bx < 34; bx++) placeBlock(BP_GRATE, 0, bx, 21, true);'); run('{ const mo = molds[0]; for (let x = mo.x + 1; x < mo.x + 15; x++) setCell(mo.y * W + x, COPPER, 30, F_CAST); for (let x = mo.x + 2; x < mo.x + 10; x++) setCell((mo.y - 1) * W + x, COPPER, 30, F_CAST); } checkMolds();'); check('a mold with only a plate over it reads as empty', run('molds[0].state') === 'empty'); -msg = run('collectMold(molds[0])'); -check('pressing F knocks the plate and the frozen drips loose', run(`(() => { const mo = molds[0]; for (let y = mo.y - 4; y <= mo.y; y++) for (let x = mo.x; x < mo.x + 16; x++) if (mat[y*W+x] === COPPER) return false; return true; })()`) && run('bag[SCRCU]') === 22, `${msg} | scrap ${run('bag[SCRCU]')}`); +check('a check pushes the plate and frozen drips out below', run(`(() => { const mo = molds[0]; for (let y = mo.y - 4; y <= mo.y; y++) for (let x = mo.x; x < mo.x + 16; x++) if (mat[y*W+x] === COPPER) return false; return true; })()`) && count(run, run('SCRCU'), 120, 92, 135, 95) === 22 && run('bagTotal === 0')); check('the grate itself is still there', run('part[(molds[0].y - 1) * W + molds[0].x + 3]') === run('P_GRATE')); // slag in an ingot is skimmed off; the bars still come out 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('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('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); +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')) === 0 && run('bagTotal === 0')); +check('slag is thrown away without putting it in the bag or below', run('bag[SLAG] === 0 && molds[0].cavs[0].every(i => mat[i] === EMPTY)') && count(run, run('SLAG')) === 0); // 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;'); @@ -103,13 +101,12 @@ check('ore does not sift down through unlit coal', Math.abs(y1 - y0) < 0.3, `${y run('for (let y = 90; y <= 99; y++) for (let x = 102; x <= 113; x++) { const i = y * W + x; if (mat[i] === COAL) { flags[i] |= F_BURN; life[i] = 250; temp[i] = 900; } wakeXY(x, y); }'); run('for (let k = 0; k < 900; k++) { if (k % 8 === 0) wakeXY(107, 95); simTick(); }'); const y2 = oreY(); check('but sinks into a burning bed', y2 > y1 + 0.3, `${y1.toFixed(2)} -> ${y2.toFixed(2)}`); -// knocking out the mold rakes slag and ash off the grate above it, and leaves the charge +// dropping bars rakes slag and ash off the grate above it, and leaves the charge fresh(); run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); run('for (let bx = 30; bx < 34; bx++) placeBlock(BP_GRATE, 0, bx, 21, true); for (let bx = 29; bx < 35; bx++) placeBlock(BP_WALL, RSTONE, bx, 17, true);'); run('{ const mo = molds[0]; for (let x = mo.x; x < mo.x + 16; x++) { setCell((mo.y - 5) * W + x, SLAG, 30, 0); setCell((mo.y - 6) * W + x, x & 1 ? ASH : SLAG, 30, 0); setCell((mo.y - 7) * W + x, x & 1 ? CUORE : COAL, 30, 0); setCell(70 * W + x, SLAG, 30, 0); } const c = molds[0].cavs[0]; for (let k = 0; k < 16; k++) setCell(c[k], COPPER, 30, F_CAST); } checkMolds();'); -msg = run('collectMold(molds[0])'); -check('F rakes the slag and ash off the grate', /raked/.test(msg) && count(run, run('SLAG'), 120, 72, 136, 88) + count(run, run('ASH'), 120, 72, 136, 88) === 0 && run('bag[SLAG] + bag[ASH]') === 0, msg); +check('an automatic drop rakes the slag and ash off the grate', run('ents.length === 2') && count(run, run('SLAG'), 120, 72, 136, 88) + count(run, run('ASH'), 120, 72, 136, 88) === 0 && run('bag[SLAG] + bag[ASH]') === 0); check('but leaves the unburnt charge, and what is outside the furnace roof', count(run, run('CUORE'), 120, 72, 136, 88) + count(run, run('COAL'), 120, 72, 136, 88) === 16 && count(run, run('SLAG'), 120, 70, 136, 70) === 16); // slag floating on top of a part-filled bit is not a flaw: fire again, or knock it out fresh(); @@ -137,4 +134,54 @@ msg = run('collectMold(molds[0])'); check('and knocks out as an iron bit', run('player.bitTier') === 2, msg); run('{ const c = molds[0].cavs[0]; c.forEach((i, k) => setCell(i, k % 5 === 0 ? COPPER : IRON, 30, F_CAST)); } checkMolds();'); check('copper mixed with iron still spoils it', run('molds[0].state') === 'flawed', run('molds[0].flaw')); +// self-cleaning waits for pours and space, and preserves loose material and temperature +function ingot() { fresh(); run('addMold("ingot", 30, 22, RSTONE, true);'); } +const under = m => count(run, run(m), 120, 92, 135, 95); +ingot(); +run('setCell(molds[0].cavs[0][0], SAND, 123, 0); checkMolds();'); +check('junk drops below as sand at its original temperature', under('SAND') === 1 && run('molds[0].state === "empty" && temp[92 * W + 120] === 123 && bagTotal === 0')); +ingot(); +run('molds[0].cavs[0].forEach((i, k) => setCell(i, k & 1 ? COPPER : IRON, 30, F_CAST)); checkMolds();'); +check('mixed metal all drops as scrap without bars', under('SCRCU') === 14 && under('SCRFE') === 14 && run('ents.length === 0 && molds[0].state === "empty" && bagTotal === 0')); +ingot(); +run('setCell(molds[0].cavs[0][0], SAND, 30, 0); for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, STONE, 30, 0); checkMolds();'); +check('blocked junk stays in the trough', cavCount(0, run('SAND')) === 1 && under('SAND') === 0); +run('for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, EMPTY, 30, 0); checkMolds();'); +check('junk drops once there is room', cavCount(0, run('SAND')) === 0 && under('SAND') === 1); +ingot(); +run('setCell(molds[0].cavs[0][0], SAND, 30, 0); setCell(molds[0].cavs[0][1], MCU, 1200, 0); checkMolds();'); +check('cleaning waits for molten metal in the cavity', cavCount(0, run('SAND')) === 1 && under('SAND') === 0); +run('setCell(molds[0].cavs[0][1], EMPTY, 30, 0); setCell((molds[0].y - 8) * W + molds[0].x, MCU, 1200, 0); checkMolds();'); +check('cleaning also waits for molten metal above the mold', cavCount(0, run('SAND')) === 1); +ingot(); +run('setCell(molds[0].cavs[0][0], SAND, 30, 0); for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) part[y * W + x] = P_PIPE; checkMolds();'); +check('cleaning never writes into built parts', cavCount(0, run('SAND')) === 1 && under('SAND') === 0); +// removal returns set cavity metal for every mold, separately from wall refunds +for (const kind of ['ingot', 'bit', 'tank']) { + fresh(); run(`addMold("${kind}", 30, 20, RSTONE, true); molds[0].cavs.flat().slice(0, 20).forEach(i => setCell(i, COPPER, 30, F_CAST)); removeMold(molds[0]);`); + check(`removing a ${kind} returns two bars and four scrap`, run('bag[BARCU] === 2 && bag[SCRCU] === 4') && count(run, run('COPPER')) === 0); +} +ingot(); +run('molds[0].cavs[0].slice(0, 5).forEach(i => setCell(i, MCU, 1200, 0)); removeMold(molds[0]);'); +check('removal leaves molten copper alone', count(run, run('MCU')) === 5 && run('bag[BARCU] === 0 && bag[SCRCU] === 0')); +fresh(); +run('addMold("bit", 30, 20, RSTONE, true); molds[0].cavs[0].forEach((i, k) => setCell(i, k & 1 ? COPPER : IRON, 30, F_CAST)); removeMold(molds[0]);'); +check('removing a mixed bit returns all metal as scrap', run('bag[BARCU] === 0 && bag[BARFE] === 0 && bag[SCRCU] === 15 && bag[SCRFE] === 15')); +ingot(); +run('for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, STONE, 30, 0); setCell(92 * W + 120, EMPTY, 30, 0); molds[0].cavs[0].slice(0, 3).forEach(i => setCell(i, SAND, 77, 0)); checkMolds();'); +check('only grains that fit are cleared', under('SAND') === 1 && cavCount(0, run('SAND')) === 2); +ingot(); +run('spawnBar(BARCU, 121, 92); setCell(molds[0].cavs[0][0], SAND, 30, 0); checkMolds();'); +check('loose junk never overlaps a dropped bar', under('SAND') === 1 && run('!mat.some((m, i) => m === SAND && occ[i])')); +for (const state of ['empty', 'short', 'flawed', 'ready', 'filling']) { + ingot(); run(`molds[0].state = "${state}"; setCell(molds[0].cavs[0][0], SAND, 30, 0); EV.length = 0;`); + msg = run('interactAt(molds[0].x + 1, molds[0].y + 1)'); + check(`F does nothing to a ${state} ingot mold and makes no sound`, /empty themselves/.test(msg) && cavCount(0, run('SAND')) === 1 && run('EV.length === 0 && bagTotal === 0 && ents.length === 0')); +} +fresh(); +run('addMold("ingot", 30, 20, BARCU, true); molds[0].cavs[0].slice(0, 20).forEach(i => setCell(i, COPPER, 30, F_CAST)); removeMold(molds[0]);'); +check('metal wall refund does not count cavity metal twice', run('bag[BARCU] === 2 + Math.floor(MOLDS.ingot.walls / GRAINS_PER_BAR) && bag[SCRCU] === 4')); +fresh(); +run('addMold("bit", 30, 20, RSTONE, true); molds[0].cavs[0].slice(0, 20).forEach((i, k) => setCell(i, k & 1 ? IRON : STEEL, 30, F_CAST)); removeMold(molds[0]);'); +check('compatible iron and steel return as iron bars and scrap', run('bag[BARFE] === 2 && bag[SCRFE] === 4 && bag[SCRST] === 0')); done();