Ingot molds empty themselves (no F); removing a mold returns its set metal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 22:53:38 -04:00
co-authored by Claude Opus 5.5
parent 9addc43be1
commit 43eb4e23fd
13 changed files with 146 additions and 43 deletions
+7 -4
View File
@@ -78,9 +78,10 @@ Details and numbers live in the code and in `docs/systems/`.
pipes, pumps, outlets. pipes, pumps, outlets.
- Machines: fan, Refiner, Stone cutter, Silo. - Machines: fan, Refiner, Stone cutter, Silo.
- Molds: ingot, bit, tank. Ingot bars drop out of the bottom onto whatever is under the mold; - 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 leave one open block below and a belt under that carries them off. Ingot molds empty themselves:
cleans the trough, chips off slag and rakes the furnace grate. Short stubs stay for the next junk and mixed scrap drop below, slag is thrown away and the grate above is raked. Short stubs
firing. 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 - 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, drag to copy / cut, V to paste, Ctrl+Z to undo, middle mouse to pick,
Shift+Q/E to switch build tabs. Shift+Q/E to switch build tabs.
@@ -137,7 +138,7 @@ above, including:
**2026-10-23:** **2026-10-23:**
- **Ingot molds drop bars below automatically.** The owner reported bars being thrown into the - **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; 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, - **Screw tubes carry no more than belts:** they move every other tick. For more throughput,
build more lines. 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, - **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 - **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. 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 ## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble. - **Concrete**, as a stronger wall material from rubble.
+2 -2
View File
@@ -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. Building blocks: placing, integrity, removing, lines and rectangles of blocks.
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks - functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
### src/sim/molds.js (298 lines) ### src/sim/molds.js (328 lines)
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.
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - variables: DROSS, grains, barWord
+20
View File
@@ -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 note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-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) ## 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. Note: "build tool has no info box like the other tools." The owner chose a compact card.
+3
View File
@@ -71,6 +71,9 @@ through another refiner.
- **Glass bricks.** - **Glass bricks.**
- Sand melts to molten glass in a fan furnace (peak about 2000°C measured). - 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. - 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. - Glass bricks build glass wall blocks. Glass is too brittle for a bit mold.
- **Stone cutter** (machine, 2×2, $90). - **Stone cutter** (machine, 2×2, $90).
- A saw under a hopper. Every 8 grains of stone rubble become one stone brick, which drops out - A saw under a hopper. Every 8 grains of stone rubble become one stone brick, which drops out
+1 -1
View File
@@ -153,7 +153,7 @@ function drawMolds(hoverMold) {
ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp); ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp);
ctx.setLineDash([]); ctx.setLineDash([]);
if (ready || flawed || mo === hoverMold || mo.state === 'filling' || mo.state === 'short') { 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)'; ctx.fillStyle = 'rgba(14,12,11,0.75)';
const tw = ctx.measureText(label).width + zp * 2; const tw = ctx.measureText(label).width + zp * 2;
ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp * 5.2, tw, zp * 4.2); ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp * 5.2, tw, zp * 4.2);
+1 -1
View File
@@ -73,7 +73,7 @@ function interactAt(x, y) {
const g = toggleGateAt(x, y); const g = toggleGateAt(x, y);
if (g) { emit({ t: 'sfx', s: 'gate' }); return g === 'open' ? 'Gate opened.' : 'Gate closed.'; } if (g) { emit({ t: 'sfx', s: 'gate' }); return g === 'open' ? 'Gate opened.' : 'Gate closed.'; }
const mo = moldAt(x, y); 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); const pb = ((y / BS) | 0) * BW + ((x / BS) | 0);
if (blockP[pb] === BP_PUMP || blockP[pb] === BP_OUTLET) { if (blockP[pb] === BP_PUMP || blockP[pb] === BP_OUTLET) {
blockV[pb] ^= 4; emit({ t: 'sfx', s: 'ui' }); blockV[pb] ^= 4; emit({ t: 'sfx', s: 'ui' });
+46 -16
View File
@@ -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 ---------- // ---------- Molds ----------
function moldCellsAt(kind, x, y) { function moldCellsAt(kind, x, y) {
const md = MOLDS[kind]; const walls = []; const md = MOLDS[kind]; const walls = [];
@@ -34,7 +34,7 @@ function addMold(kind, bx, by, wallLoose, free) {
return mo; return mo;
} }
// Slag and ash that reach a mold float on the metal. They are chipped off and thrown // 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; 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; // 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. // 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 }; 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. // 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) { function troughState(mo) {
let solid = 0, liq = 0, junk = 0, metal = -1, mixed = false; let solid = 0, liq = 0, junk = 0, metal = -1, mixed = false;
for (const i of mo.cavs[0]) { 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 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 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.') 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}. Fire again to top it up, or press F to knock it out as scrap.` : ''; : 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 }; 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 // 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; 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 // 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. // clean grate. Unburnt coal and ore stay for the next firing. Returns how many grains it raked.
function rakeAbove(mo) { function rakeAbove(mo) {
@@ -184,6 +184,32 @@ function ejectIngot(mo) {
r = troughState(mo); r = troughState(mo);
} else if (!free && !mo.blockedSaid) { mo.blockedSaid = true; emit({ t: 'ingot_blocked', mold: mo.id, x, y }); } } 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; 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.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; 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 }; return { scrap, other, dross: got.dross || 0 };
} }
function collectMold(mo) { 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 === 'filling') return 'The metal has not fully set yet.';
if (mo.state === 'empty') { if (mo.state === 'empty') {
const g = tally(clearCavity(mo.cavs.flat())), u = unplugMold(mo); 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); return 'Knocked the mold clean.' + knockNote(scrap, g.other, g.dross + u.chipped, u.raked);
} }
let msg = ''; 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)) { 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 // an unfinished casting can still be topped up, so the first press only says so
mo.knockAsk = tick; mo.knockAsk = tick;
@@ -283,6 +300,19 @@ function collectMold(mo) {
} }
function removeMold(mo) { function removeMold(mo) {
undoNote({ t: 'mold-', kind: mo.kind, bx: mo.x / BS, by: mo.y / BS, wall: mo.wall }); 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); const { walls } = moldCellsAt(mo.kind, mo.x, mo.y);
let metal = 0; let metal = 0;
for (const i of walls) { for (const i of walls) {
+1 -1
View File
@@ -72,7 +72,7 @@ const PIECES = [
{ id: 'dock', name: 'Trade dock', kind: 'machine', bw: 6, bh: 4, cost: [], hidden: true, fixed: true, { 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.' }, 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', { 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', { 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.' }, 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', { id: 'tankmold', name: 'Tank mold', kind: 'mold', mold: 'tank', variants: 'wall',
+2 -2
View File
@@ -15,7 +15,7 @@ function smelterBlueprint(kind, count, ch, wall) {
} }
const STARTER_DESIGNS = [ const STARTER_DESIGNS = [
{ id: 'smelter1', name: 'Copper smelter', bp: () => smelterBlueprint('ingot', 1, 3, RSTONE), { 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), { 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.' }, 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: '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: '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: '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: '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: '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.' }, { 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.' },
+1 -1
View File
@@ -123,7 +123,7 @@ function hoverInfo(ax, ay) {
const mo = moldAt(ax, ay); const mo = moldAt(ax, ay);
if (mo) { if (mo) {
t1 = MOLDS[mo.kind].name; 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])); if (player.lens) t2.push(fmtT(temp[i]));
} }
const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay); const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay);
+1 -1
View File
@@ -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.<br> 2. With the Vacuum, click on coal to suck up only coal, then the copper ore, rubble and clay you want.<br>
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.<br> 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.<br>
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.<br> 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.<br>
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.<br> 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.<br>
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.<br> 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.<br>
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.</p> 7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.</p>
`; `;
+2 -2
View File
@@ -65,11 +65,11 @@ for (let f = 0; f < firings; f++) {
} else ticks(run, 5400); } else ticks(run, 5400);
const st = run(`JSON.stringify(molds.map(m => m.state + (m.flaw ? ':' + m.flaw : '')))`); 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(); } 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'); 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();'); 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; 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'); if (run('player.bitTier') > bits) bits = run('player.bitTier');
run('for (const e of ents.slice()) removeEnt(e);'); 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) }; 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) };
+59 -12
View File
@@ -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();'); 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); 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])'); 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); }'); 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])'); run('checkMolds()');
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); 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();'); 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')); 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();'); 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(); 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;'); 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')); 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(); fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); 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('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();'); 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'); check('a mold with only a plate over it reads as empty', run('molds[0].state') === 'empty');
msg = run('collectMold(molds[0])'); 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('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('the grate itself is still there', run('part[(molds[0].y - 1) * W + molds[0].x + 3]') === run('P_GRATE')); 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 // slag in an ingot is skimmed off; the bars still come out
fresh(); fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); 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();'); 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')); 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'));
msg = run('collectMold(molds[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);
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 // a bit casting with slag in it is spoiled, but F knocks it out instead of asking you to drill
fresh(); fresh();
run('bag[RSTONE] = 200; bagTotal = 200; placeMoldAt("bit", 30, 20, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); 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 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(); 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)}`); 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(); fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0;'); 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('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();'); 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('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('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('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); 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 // slag floating on top of a part-filled bit is not a flaw: fire again, or knock it out
fresh(); fresh();
@@ -137,4 +134,54 @@ msg = run('collectMold(molds[0])');
check('and knocks out as an iron bit', run('player.bitTier') === 2, msg); 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();'); 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')); 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(); done();