Deepgrain Foundry: move from a claude.ai artifact to a local repository
The game as of artifact version 24, ready for local development with coding agents. - Source split from three large files into 52 small ones under src/sim, src/render and src/ui, joined in the order in src/manifest.json by build.py. The joined code is unchanged apart from comments; every headless suite gives the same output, and screenshots in both looks are pixel-identical to before. - Light tidying with no change in behaviour: one movement scan loop, shared hazard-stripe and belt shader helpers, unused code removed, a shared check helper for the test suites. - World files can be exported as an ordinary download outside claude.ai. - Tests: broken old scripts removed, experiments moved to tools/, test/run_all.py added. Tools: shot.py (repeatable screenshots), where.py, map.py. - Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox), docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the owner's requests and an agent log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user