// Molds: cavities that fill with molten metal (or glass) and are knocked out as bars. // ---------- Molds ---------- function moldCellsAt(kind, x, y) { const md = MOLDS[kind]; const walls = []; for (let r = 0; r < md.h; r++) for (let c = 0; c < md.w; c++) if (md.rows[r][c] === '#') walls.push((y + r) * W + x + c); const cavs = md.cavs.map(comp => comp.map(([c, r]) => (y + r) * W + x + c)); return { walls, cavs }; } function canPlaceMold(kind, bx, by) { const md = MOLDS[kind]; if (bx < 0 || by < 0 || bx + md.bw > BW || by + md.bh > BH) return 'Out of bounds'; for (let y = by; y < by + md.bh; y++) for (let x = bx; x < bx + md.bw; x++) if (blockP[y * BW + x]) return 'Something is already built there'; const { walls, cavs } = moldCellsAt(kind, bx * BS, by * BS); for (const i of walls.concat(...cavs)) if (!cellBuildable(i)) return PH[mat[i]] === PH_SOLID && (flags[i] & F_NAT) ? 'Needs open space. Clear the rock first.' : 'Something is already built there'; if (moldAt(bx * BS + 1, by * BS + 1)) return 'There is already a mold there'; return null; } function newMoldRecord(kind, x, y, wall) { const md = MOLDS[kind]; return { id: (molds.length ? molds[molds.length - 1].id : 0) + 1, kind, x, y, w: md.w, h: md.h, wall, cavs: moldCellsAt(kind, x, y).cavs, ready: [], state: 'empty', metal: 0, fill: 0, flaw: '' }; } function addMold(kind, bx, by, wallLoose, free) { const md = MOLDS[kind]; if (!WALL_OF[wallLoose]) return 'Pick a wall material with R'; const cost = wallCost(wallLoose, md.walls); if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); } const x = bx * BS, y = by * BS; const { walls, cavs } = moldCellsAt(kind, x, y); evictCells(walls.concat(...cavs), y); for (const i of walls) setCell(i, WALL_OF[wallLoose], AMB[(i / W) | 0], 0); const mo = newMoldRecord(kind, x, y, wallLoose); molds.push(mo); stats.placed++; 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. 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. function castMix(a, b) { return a === b ? a : (a === IRON && b === STEEL) || (a === STEEL && b === IRON) ? IRON : -1; } function cavityState(cav) { let solid = 0, liq = 0, metal = -1, mixed = false, dirty = false, uncast = false, dross = 0; for (const i of cav) { const m = mat[i]; if (SOLIDMETAL[m]) { solid++; if (!(flags[i] & F_CAST)) uncast = true; if (metal < 0) metal = m; else if (metal !== m) { const c = castMix(metal, m); if (c < 0) mixed = true; else metal = c; } } else if (CASTMETAL[m]) { liq++; const sm = FRZTO[m]; if (metal < 0) metal = sm; else if (metal !== sm) { const c = castMix(metal, sm); if (c < 0) mixed = true; else metal = c; } } else if (DROSS[m]) { dross++; if (i >= W && SOLIDMETAL[mat[i - W]]) dirty = true; // metal set on top of it: it is stuck inside the casting } else if (m !== EMPTY && PH[m] !== PH_GAS) dirty = true; } const n = cav.length; let st; if (liq > 0) st = 'filling'; else if (solid === 0) st = 'empty'; else if (solid === n && !mixed && !uncast) st = 'ready'; else if (dirty || mixed || uncast) st = 'flawed'; else st = 'short'; const flaw = dirty ? 'Slag or grit got stuck inside the casting. Press F to knock it out as scrap and start again.' : mixed ? 'Two different metals mixed in the casting. Press F to knock it out.' : uncast ? 'That metal was not cast here. Pour molten metal in.' : st === 'short' ? `Not full: ${solid} of ${n} grains${dross ? ', with slag floating on top' : ''}. Fire the furnace again to top it up, or press F to knock it out as scrap.` : ''; 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. function troughState(mo) { let solid = 0, liq = 0, junk = 0, metal = -1, mixed = false; for (const i of mo.cavs[0]) { const m = mat[i]; if (SOLIDMETAL[m]) { solid++; if (metal < 0) metal = m; else if (metal !== m) { const c = castMix(metal, m); if (c < 0) mixed = true; else metal = c; } } else if (CASTMETAL[m]) { liq++; if (metal < 0) metal = FRZTO[m]; } else if (m !== EMPTY && PH[m] !== PH_GAS && !DROSS[m]) junk++; } 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.` : ''; 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 // grains (ore, coal) as they are, and slag and ash are thrown away. Molten metal is left alone. // Returns what went to the bag, and how much dross was thrown away as got.dross. function clearCavity(cav) { const got = new Map(); let dross = 0; for (const i of cav) { const m = mat[i]; if (m === EMPTY || PH[m] === PH_GAS || CASTMETAL[m]) continue; if (DROSS[m]) dross++; else { const loose = SOLIDMETAL[m] ? (YLD[m] || SCRCU) : PH[m] === PH_SOLID ? YLD[m] : m; if (loose) got.set(loose, (got.get(loose) || 0) + 1); } setCell(i, EMPTY, AMB[(i / W) | 0], 0); } for (const [m, n] of got) giveOrDrop(m, n, bot.x + bot.w / 2, bot.y + bot.h / 2); got.dross = dross; return got; } // Metal that set where it should not have: the open part of the mold above the cavity (overflow // that sets as a plate over it) and the grate cells just above the mold (late drips that froze // on the way down). Left there, it plugs the way down for the next firing. Returns the cells. function moldPlugCells(mo) { const { walls } = moldCellsAt(mo.kind, mo.x, mo.y), wall = new Set(walls), cav = new Set(mo.cavs.flat()), out = []; for (let y = mo.y; y < mo.y + mo.h; y++) for (let x = mo.x; x < mo.x + mo.w; x++) { const i = y * W + x, m = mat[i]; if (wall.has(i) || cav.has(i) || m === EMPTY || PH[m] === PH_GAS || CASTMETAL[m]) continue; if (SOLIDMETAL[m] || PH[m] === PH_POWDER || DROSS[m]) out.push(i); } for (let y = Math.max(0, mo.y - 8); y < mo.y; y++) for (let x = mo.x; x < mo.x + mo.w; x++) { const i = y * W + x; if (part[i] === P_GRATE && SOLIDMETAL[mat[i]]) out.push(i); } return out; } // Knocking a mold 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) { let n = 0; for (let x = mo.x; x < mo.x + mo.w; x++) for (let y = mo.y - 1, k = 0; y >= 0 && k < 48; y--, k++) { const i = y * W + x, m = mat[i]; if (part[i] !== P_GRATE && (occ[i] || (PH[m] === PH_SOLID && !DROSS[m]))) break; // the roof, a wall or a machine if (DROSS[m] && !(flags[i] & F_BURN)) { setCell(i, EMPTY, AMB[y], 0); n++; } } if (n) wakeXY(mo.x + (mo.w >> 1), mo.y - 8); return n; } // After a knock-out: if the furnace over the mold is badly loaded, say what to change function furnaceJunkHint(mo) { let ore = 0, fuel = 0, open = 0; for (let x = mo.x; x < mo.x + mo.w; x++) for (let y = mo.y - 1, k = 0; y >= 0 && k < 48; y--, k++) { const i = y * W + x, m = mat[i]; if (part[i] === P_GRATE) continue; if (occ[i] || PH[m] === PH_SOLID) break; if (SMELTP[m] && PH[m] === PH_POWDER) ore++; else if (FUEL[m] && PH[m] === PH_POWDER) fuel++; else if (m === EMPTY || PH[m] === PH_GAS) open++; } if (ore > 40 && fuel < ore * 0.3) return ' A lot of ore is left and little fuel: add mostly coal next time.'; if (ore + fuel > 60 && open < (ore + fuel) * 0.5) return ' The furnace is packed too full to burn well. Leave the top third empty so air gets in.'; if (ore > 60) return ' A lot of ore did not melt: mix the next charge evenly with coal.'; return ''; } // Knocking a mold also clears metal stuck over and above it, and rakes the furnace. // Returns { metal: grains of metal that went to your bag, chipped: slag thrown away from the // mold, raked: slag and ash raked out of the furnace }. function unplugMold(mo) { const plug = moldPlugCells(mo); const got = plug.length ? clearCavity(plug) : new Map(); let metal = 0; for (const [k, c] of got) if (PH[k] === PH_POWDER && MELTTO[k] && CASTMETAL[MELTTO[k]] && !SMELTP[k]) metal += c; return { metal, chipped: got.dross || 0, raked: rakeAbove(mo) }; } const grains = n => `${n} ${n === 1 ? 'grain' : 'grains'}`; 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; continue; } const res = mo.cavs.map(cavityState); mo.ready = []; res.forEach((r, k) => { if (r.st === 'ready') mo.ready.push(k); }); let st; if (mo.ready.length) st = 'ready'; else if (res.some(r => r.st === 'filling')) st = 'filling'; else if (res.some(r => r.st === 'flawed')) st = 'flawed'; else if (res.some(r => r.st === 'short')) st = 'short'; else st = 'empty'; mo.fill = res.reduce((a, r) => a + r.fill, 0) / res.length; const ref = mo.ready.length ? res[mo.ready[0]] : res.find(r => r.metal >= 0) || res[0]; mo.flaw = (res.find(r => r.flaw) || {}).flaw || ''; if (st === 'ready' && mo.state !== 'ready') emit({ t: 'cast_ready', mold: mo.id, kind: mo.kind, metal: ref.metal, x: mo.x + (mo.w >> 1), y: mo.y }); mo.state = st; mo.metal = ref.metal; } } function moldAt(x, y) { for (const mo of molds) if (x >= mo.x && y >= mo.y && x < mo.x + mo.w && y < mo.y + mo.h) return mo; return null; } // What a knock-out sent where, as one short sentence after the main message function knockNote(scrap, other, chipped, raked) { let s = ''; if (scrap) s += ` The leftover ${grains(scrap)} of scrap went into your bag${other ? ', with the grit that got in' : ''}.`; else if (other) s += ' The grit that got in went into your bag.'; if (chipped && raked) s += ' The slag was chipped off and the grate raked clean.'; else if (chipped) s += ' The slag was chipped off.'; else if (raked) s += ' Slag and ash were raked off the grate.'; return s; } function tally(got) { let scrap = 0, other = 0; for (const [k, c] of got) { if (PH[k] === PH_POWDER && MELTTO[k] && CASTMETAL[MELTTO[k]] && !SMELTP[k]) scrap += c; else other += c; } return { scrap, other, dross: got.dross || 0 }; } function collectMold(mo) { 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); const scrap = g.scrap + u.metal; if (!scrap && !g.other && !g.dross && !u.chipped && !u.raked) return 'The mold is empty.'; emit({ t: 'sfx', s: 'ui' }); return 'Knocked the mold clean.' + knockNote(scrap, g.other, g.dross + u.chipped, u.raked); } let msg = ''; 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); } 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; return (mo.flaw || 'The casting is not full yet.').replace(', or press F to knock it out as scrap.', '.') + ' Press F again to knock it out as scrap.'; } if (mo.state === 'flawed' || mo.state === 'short') { // a spoiled or unfinished casting is knocked out as scrap, so the mold is ready for another go const short = mo.state === 'short', g = tally(clearCavity(mo.cavs.flat())), u = unplugMold(mo); mo.state = 'empty'; mo.ready = []; emit({ t: 'sfx', s: 'ui' }); const what = mo.kind === 'bit' ? 'bit' : 'casting'; return (short ? `The ${what} was not full, so it was knocked out as scrap.` : `The ${what} was spoiled, so it was knocked out as scrap.`) + knockNote(g.scrap + u.metal, g.other, g.dross + u.chipped, u.raked) + furnaceJunkHint(mo); } const tier = METAL_TIER[mo.metal]; const cav = mo.cavs[0]; if (tier <= 0) { msg = `${MD[mo.metal] ? MD[mo.metal].name : 'That metal'} is too ${mo.metal === GLASS ? 'brittle' : 'soft'} for a ${mo.kind === 'bit' ? 'drill bit' : 'tank'}. The casting broke into scrap, which went into your bag.`; } else if (mo.kind === 'bit') { if (tier > player.bitTier) { player.bitTier = tier; msg = `New drill bit: ${BIT_TIERS[tier].name}.`; emit({ t: 'upgrade', kind: 'bit', tier }); } else { msg = `You already have a ${BIT_TIERS[player.bitTier].name.toLowerCase()}. The casting broke into scrap, which went into your bag.`; } } else { if (tier > player.tankTier) { player.tankTier = tier; msg = `New bag: ${TANK_TIERS[tier].name}, holds ${TANK_TIERS[tier].cap.toLocaleString('en-US')}.`; emit({ t: 'upgrade', kind: 'tank', tier }); } else { msg = 'Your tank is already as good. The casting broke into scrap, which went into your bag.'; } } // a casting you could not use breaks into scrap, which goes to your bag if (msg.startsWith('New')) for (const i of cav) setCell(i, EMPTY, AMB[(i / W) | 0], 0); else clearCavity(cav); const u = unplugMold(mo); mo.state = 'empty'; stats.cast++; return msg + knockNote(0, 0, u.chipped, u.raked) + furnaceJunkHint(mo); } function removeMold(mo) { undoNote({ t: 'mold-', kind: mo.kind, bx: mo.x / BS, by: mo.y / BS, wall: mo.wall }); const { walls } = moldCellsAt(mo.kind, mo.x, mo.y); let metal = 0; for (const i of walls) { const m = mat[i]; if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) { if (BAR_OF[m] && PH[mo.wall] === PH_ITEM) { metal++; setCell(i, EMPTY, temp[i], 0); } else if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; } else setCell(i, EMPTY, temp[i], 0); } } if (metal) giveOrDrop(BAR_OF[WALL_OF[mo.wall]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), mo.x + 8, mo.y); molds = molds.filter(m => m !== mo); }