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,244 @@
|
||||
// What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame.
|
||||
// Which overlays are on, shown as a coloured frame round the screen and a named badge
|
||||
let ovShown = '';
|
||||
function updateOverlayUI() {
|
||||
const on = (GAME.running && R.lens ? 'lens ' : '') + (GAME.running && GAME.infoOverlay ? 'info' : '');
|
||||
if (on + SET.thermalPalette === ovShown) return;
|
||||
ovShown = on + SET.thermalPalette;
|
||||
const f = $('ovframe'), b = $('ovbadges');
|
||||
if (!f || !b) return;
|
||||
f.className = on ? 'on ' + on : '';
|
||||
let h = '';
|
||||
if (R.lens) {
|
||||
// a little key of the lens colours, from freezing to white hot
|
||||
const tl = THERM[SET.thermalPalette] || THERM.inferno, stops = [0, 20, 150, 700, 1600].map(T => { const j = thermIdx(T); return `rgb(${tl[j]},${tl[j + 1]},${tl[j + 2]})`; });
|
||||
h += `<span class="ovbadge lens"><span class="dot"></span>Thermal lens<span class="scale" style="background:linear-gradient(90deg,${stops.join(',')})" title="0°C to 1600°C"></span><kbd>H</kbd></span>`;
|
||||
}
|
||||
if (GAME.infoOverlay) h += `<span class="ovbadge info"><span class="dot"></span>Info overlay<kbd>I</kbd></span>`;
|
||||
b.innerHTML = h;
|
||||
}
|
||||
function toggleLens() {
|
||||
if (!player.lens) { setStatus('The thermal lens unlocks at your first fire.'); playSfx('error'); return; }
|
||||
R.lens = !R.lens; playSfx('ui');
|
||||
}
|
||||
function doInteract() {
|
||||
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||
const fb = filterBlockAt(ax, ay);
|
||||
if (fb >= 0) {
|
||||
if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
||||
const run = filterRun(fb);
|
||||
openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0 }), f => {
|
||||
applyFilterRun(run, f);
|
||||
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir;
|
||||
setStatus(`${run.length === 1 ? 'This filter' : `These ${run.length} filters`} now ${filterSentence(f)}, ${f.dir ? 'right' : 'left'}.`, 'info');
|
||||
}, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
|
||||
GAME.editRun = run;
|
||||
return;
|
||||
}
|
||||
if (toolId() === 'build' && piece().filter) { openFilterPicker(Object.assign({}, GAME.filterCfg, { dir: GAME.pieceDir.filter || 0 }), f => { GAME.filterCfg = { m: f.m, inv: f.inv }; GAME.pieceDir.filter = f.dir; }, 'New filters'); return; }
|
||||
let msg = '';
|
||||
outer: for (let r = 0; r <= 2; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
||||
if (!inReach(ax + dx, ay + dy)) continue;
|
||||
msg = interactAt(ax + dx, ay + dy);
|
||||
if (msg) break outer;
|
||||
}
|
||||
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); }
|
||||
else { setStatus('Nothing to use there. F works on gates, molds, fans and filters.', 'info'); }
|
||||
}
|
||||
// Pick (G or middle mouse): the piece under the cursor becomes the one you build, with its
|
||||
// direction, wall material and filter settings. On loose material it picks that to blow out.
|
||||
function pickUnderCursor() {
|
||||
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return;
|
||||
const i = ay * W + ax, m = mat[i], bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
|
||||
let pid = null, note = '';
|
||||
const ghost = ghostBlocks.get(bi);
|
||||
if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; }
|
||||
else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; note = MD[WALL_OF[mo.wall]].name.toLowerCase(); }
|
||||
else if (blockP[bi] || ghost) {
|
||||
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
|
||||
const P = PIECE_BY_CODE[code]; pid = P.id;
|
||||
if (P.code === BP_WALL) { GAME.wallMat = v; note = MD[WALL_OF[v]].name.toLowerCase(); }
|
||||
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; note = filterSentence(GAME.filterCfg); }
|
||||
else if (P.dirs) GAME.pieceDir[P.id] = v & 3;
|
||||
if (P.dirs && !P.filter) note = P.dirs[GAME.pieceDir[P.id] || 0];
|
||||
else if (P.filter) note = `${P.dirs[GAME.pieceDir[P.id] || 0]}, ${note}`;
|
||||
}
|
||||
if (pid) {
|
||||
const P = PIECE_BY_ID[pid];
|
||||
if (P.hidden) { setStatus(`${P.name} cannot be built.`); playSfx('error'); return; }
|
||||
GAME.pieceIdx = P.index; GAME.buildCat = catOf(P.id); GAME.paste = false; selectTool(2); hudDirty = true; playSfx('ui');
|
||||
setStatus(`Picked: ${P.name}${note ? ` (${note})` : ''}.`, 'info');
|
||||
return;
|
||||
}
|
||||
const t = PH[m] === PH_POWDER || PH[m] === PH_LIQUID ? m : 0;
|
||||
if (t && bag[t] > 0) { GAME.dropMat = t; GAME.matChosen = true; GAME.blowMix = false; selectTool(1); hudDirty = true; playSfx('ui'); setStatus(`Picked: ${MD[t].name} to blow out.`, 'info'); }
|
||||
else setStatus('Nothing built here to pick.', 'info');
|
||||
}
|
||||
function aimBlock() { return blockOf(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)); }
|
||||
function stampOrigin(bw, bh) { const { bx, by } = aimBlock(); return { bx: bx - ((bw - 1) >> 1), by: by - ((bh - 1) >> 1) }; }
|
||||
|
||||
// Copy and cut: hold C or X and drag a box with the left button; letting go does it.
|
||||
function selModeHeld() { return GAME.running && !anyModal() ? (keys.has('x') ? 'cut' : keys.has('c') ? 'copy' : null) : null; }
|
||||
function startPaste() {
|
||||
if (!GAME.clip) { setStatus('Nothing copied yet. Hold C and drag a box over something you built.', 'info'); playSfx('error'); return; }
|
||||
GAME.paste = true; GAME.drag = null; hudDirty = true; playSfx('ui');
|
||||
}
|
||||
function stopSelectAndPaste() { GAME.selDrag = null; GAME.paste = false; hudDirty = true; playSfx('ui'); }
|
||||
function plural(n, word) { return `${n} ${word}${n === 1 ? '' : 's'}`; }
|
||||
function piecesIn(rec, tag) { return rec.ops.filter(o => o.t === tag).length; }
|
||||
function doUndo() {
|
||||
GAME.drag = null; GAME.selDrag = null;
|
||||
const msg = undoLast();
|
||||
setStatus(msg, /could not|Nothing/.test(msg) ? 'err' : 'info');
|
||||
playSfx(/Nothing/.test(msg) ? 'error' : 'decon'); hudDirty = true;
|
||||
}
|
||||
// right-click take-backs while the button is held count as one thing to undo
|
||||
function endRmbRecord() { if (GAME.rmbRec) { const r = GAME.rmbRec; GAME.rmbRec = null; const n = r.ops.length; r.label = `taking back ${plural(n, 'piece')}`; undoPush(r); } }
|
||||
// Belts, filters and tubes run the way you drag them; a single click uses the direction R sets
|
||||
function dragDirFor(P, a, b) {
|
||||
if (!a || !P.dirs || !(P.id === 'belt' || P.id === 'tube' || P.filter)) return -1;
|
||||
const dx = b.bx - a.bx, dy = b.by - a.by;
|
||||
if (!dx && !dy) return -1;
|
||||
if (P.id === 'tube') return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);
|
||||
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
|
||||
}
|
||||
function blockLabel(P, rec) {
|
||||
const put = piecesIn(rec, 'block+'), took = piecesIn(rec, 'block-') + piecesIn(rec, 'filter');
|
||||
return took ? `replacing ${plural(took, 'piece')}` : `placing ${plural(put, P.name.toLowerCase())}`;
|
||||
}
|
||||
|
||||
// press / release actions (one-shot)
|
||||
function primaryPress(shift) {
|
||||
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||
const sel = selModeHeld();
|
||||
if (sel) { GAME.selDrag = { x: ax, y: ay, mode: sel }; GAME.drag = null; hudDirty = true; return; }
|
||||
if (GAME.paste && GAME.clip) {
|
||||
const { bx, by } = stampOrigin(GAME.clip.bw, GAME.clip.bh);
|
||||
undoRecord('pasting a blueprint', () => pasteGhost(GAME.clip, bx, by)); playSfx('place'); hudDirty = true;
|
||||
return;
|
||||
}
|
||||
if (t === 'vacuum') {
|
||||
// with anything ticked, the vacuum takes only the ticked materials, wherever you start;
|
||||
// with nothing ticked, it takes what you start on
|
||||
const g = GAME.collect.size ? 0 : grainAt(ax, ay);
|
||||
GAME.vacFilter = shift ? -1 : g;
|
||||
GAME.vacLocked = shift || !!g || GAME.collect.size > 0;
|
||||
hudDirty = true;
|
||||
} else if (t === 'build') {
|
||||
const P = piece();
|
||||
if (P.kind === 'block') { GAME.drag = aimBlock(); }
|
||||
else {
|
||||
const bw = P.kind === 'mold' ? MOLDS[P.mold].bw : P.bw, bh = P.kind === 'mold' ? MOLDS[P.mold].bh : P.bh;
|
||||
const { bx, by } = stampOrigin(bw, bh);
|
||||
if (!inReach(bx * BS + bw * BS / 2, by * BS + bh * BS / 2)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
||||
const place = () => P.kind === 'mold' ? placeMoldAt(P.mold, bx, by, GAME.wallMat) : placeMachineAt(P.id, GAME.pieceDir[P.id] || 0, bx, by);
|
||||
const check = () => P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
||||
let err = '';
|
||||
undoRecord(rec => piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-') ? `replacing with a ${P.name.toLowerCase()}` : `placing a ${P.name.toLowerCase()}`, () => {
|
||||
err = check() || '';
|
||||
// Ctrl: take down what you built in the way (refunded) and put this in its place
|
||||
if (err && GAME.ctrl && /already/.test(err)) {
|
||||
err = footprintBlocker(bx, by, bw, bh);
|
||||
if (!err && !canAfford(pieceCostNow(P), 1)) err = needText(pieceCostNow(P), 1);
|
||||
if (!err) err = clearFootprint(bx, by, bw, bh);
|
||||
}
|
||||
if (!err) err = place();
|
||||
});
|
||||
if (err) { setStatus(err + (/already/.test(err) ? `${/[.!]$/.test(err) ? '' : '.'} Hold Ctrl to replace it.` : '')); playSfx('error'); } else { playSfx('place'); hudDirty = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
function primaryRelease(shift) {
|
||||
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||
if (GAME.selDrag) {
|
||||
const sd = GAME.selDrag; GAME.selDrag = null; hudDirty = true;
|
||||
if (sd.mode === 'cut') {
|
||||
let bp = null;
|
||||
undoRecord(rec => `cutting ${plural(piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-'), 'piece')}`, () => { bp = cutRegion(sd.x, sd.y, ax, ay); });
|
||||
if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('decon'); setStatus(`Cut ${bp.bw}×${bp.bh} blocks into your bag. Click to paste it somewhere; Ctrl+Z puts it back.`, 'info'); }
|
||||
else { setStatus('Nothing you built is inside that box.'); playSfx('error'); }
|
||||
} else {
|
||||
const bp = copyRegion(sd.x, sd.y, ax, ay);
|
||||
if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('cast'); setStatus(`Copied ${bp.bw}×${bp.bh} blocks. Click to place it.`, 'info'); }
|
||||
else { setStatus('Nothing you built is inside that box.'); playSfx('error'); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (t === 'build' && GAME.drag) {
|
||||
const P = piece(), b = aimBlock();
|
||||
const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by);
|
||||
const dd = dragDirFor(P, GAME.drag, b);
|
||||
if (dd >= 0) GAME.pieceDir[P.id] = dd;
|
||||
const v = pieceV(P), ex = pieceExtra(P);
|
||||
// blocks past the bot's reach become ghosts it builds when it gets close, so a line can be any length
|
||||
const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by));
|
||||
let ghosts = 0;
|
||||
const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => {
|
||||
const res = placeBlocks(P.code, v, near, ex, GAME.ctrl);
|
||||
const g = { t: 'paste', blocks: [], machines: [], molds: [] };
|
||||
for (const [bx, by] of far) {
|
||||
const b = by * BW + bx;
|
||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, P.code, v, ex) || ghostBlocks.has(b)) continue;
|
||||
ghostBlocks.set(b, ex ? [P.code, v, ex] : [P.code, v]); g.blocks.push([bx, by, P.code]); ghosts++;
|
||||
}
|
||||
if (ghosts) undoNote(g);
|
||||
return res;
|
||||
});
|
||||
const hint = /already built/.test(r.status) ? `${/[.!]$/.test(r.status) ? '' : '.'} Hold Ctrl to replace it.` : '';
|
||||
const gtxt = ghosts ? `${r.n ? `Placed ${plural(r.n, P.name.toLowerCase())}; ` : ''}${plural(ghosts, 'more')} out of reach ${ghosts === 1 ? 'is a ghost' : 'are ghosts'} your bot builds when it gets close.` : '';
|
||||
if (r.n || ghosts) { playSfx('place'); setStatus(gtxt || (r.status ? r.status + hint : ''), 'info'); } else { setStatus((r.status || 'Nothing placed.') + hint); playSfx('error'); }
|
||||
GAME.drag = null; GAME.dragDir = -1; hudDirty = true;
|
||||
}
|
||||
if (t === 'vacuum') { GAME.vacFilter = 0; GAME.vacLocked = false; hudDirty = true; }
|
||||
}
|
||||
function secondaryPress() {
|
||||
const t = toolId();
|
||||
GAME.rmbSeen = new Set();
|
||||
if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; GAME.rmbConsumed = true; }
|
||||
else if (GAME.paste) { GAME.paste = false; hudDirty = true; playSfx('ui'); GAME.rmbConsumed = true; }
|
||||
else if (t === 'build' && GAME.drag) { GAME.drag = null; GAME.dragDir = -1; GAME.rmbConsumed = true; }
|
||||
else { GAME.rmbConsumed = false; if (t === 'build') { endRmbRecord(); GAME.rmbRec = { label: '', ops: [] }; } }
|
||||
}
|
||||
|
||||
// continuous actions, once per simulation tick
|
||||
function toolTick() {
|
||||
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||
if (GAME.lmb && !GAME.selDrag && !GAME.paste && !selModeHeld()) {
|
||||
if (t === 'drill') {
|
||||
const r = toolPulse(ax, ay, GAME.drillW, GAME.drillTune);
|
||||
if (r.fired) { playSfx('pulse', GAME.drillW); if (r.status) setStatus(r.status); }
|
||||
} else if (t === 'vacuum') {
|
||||
// started on empty space: the first loose thing you sweep over (that is not ticked) becomes the filter
|
||||
// clicked on empty space: the first loose thing that comes under the cursor sets the filter, once
|
||||
if (!GAME.vacLocked && !GAME.collect.size) { const g = grainAt(ax, ay); if (g) { GAME.vacFilter = g; GAME.vacLocked = true; hudDirty = true; } }
|
||||
const f = GAME.vacFilter, list = GAME.collect;
|
||||
if (f || list.size) {
|
||||
const r = toolVacuum(ax, ay, GAME.brush[1], f < 0 ? () => true : list.size ? m => list.has(m) : m => m === f);
|
||||
if (r.got) { playSfx('suck'); hudDirty = true; }
|
||||
if (r.status) setStatus(r.status);
|
||||
}
|
||||
} else if (t === 'ignite') {
|
||||
if (inReach(ax, ay)) { const r = toolIgnite(ax, ay); playSfx('spark'); if (Math.random() < 0.6) spawnSpark(ax + 0.5, ay + 0.5); if (!r.fuel) setStatus('Nothing here burns. Aim at coal or wood that touches air.'); }
|
||||
else setStatus('Out of reach.');
|
||||
}
|
||||
}
|
||||
if (GAME.rmb && !GAME.rmbConsumed) {
|
||||
if (t === 'vacuum') {
|
||||
ensureDropMat();
|
||||
// what right click blows is only ever what is selected (and shown by the cursor): one material, or the ticked mix
|
||||
const mix = GAME.blowMix && GAME.collect.size ? [...GAME.collect] : null;
|
||||
const r = toolBlow(ax, ay, GAME.brush[1], GAME.dropMat, mix);
|
||||
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
||||
} else if (t === 'build') {
|
||||
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
||||
if (!GAME.rmbSeen.has(key) && inReach(ax, ay)) {
|
||||
GAME.rmbSeen.add(key);
|
||||
const keep = undoRec; undoRec = GAME.rmbRec;
|
||||
const msg = removePieceAt(ax, ay);
|
||||
undoRec = keep;
|
||||
if (msg) { playSfx('decon'); setStatus(msg, 'info'); hudDirty = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user