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,87 @@
|
||||
// Undo (Ctrl+Z) for building, and the bot building ghosts.
|
||||
// ---------- Undo ----------
|
||||
// Building actions are recorded while undoRec is open (the game wraps each click, drag, cut or
|
||||
// paste in one record). Ctrl+Z plays the last record backwards: what was placed is taken back
|
||||
// and refunded, what was taken back is built again (paid for again), ghosts are cleared.
|
||||
const UNDO_MAX = 80;
|
||||
let undoStack = [], undoRec = null;
|
||||
function undoNote(op) { if (undoRec) undoRec.ops.push(op); }
|
||||
function undoPush(rec) { if (rec && rec.ops.length) { undoStack.push(rec); if (undoStack.length > UNDO_MAX) undoStack.shift(); } }
|
||||
// label: text for the message when it is undone, or a function of the record that makes it
|
||||
function undoRecord(label, fn) {
|
||||
const prev = undoRec, rec = { label: '', ops: [] };
|
||||
undoRec = rec;
|
||||
try { return fn(); } finally { undoRec = prev; rec.label = typeof label === 'function' ? label(rec) : label; undoPush(rec); }
|
||||
}
|
||||
function undoLast() {
|
||||
const rec = undoStack.pop();
|
||||
if (!rec) return 'Nothing to undo.';
|
||||
const prev = undoRec; undoRec = null;
|
||||
let fails = 0, why = '';
|
||||
const fail = (r) => { fails++; if (r && !why) why = r; };
|
||||
for (let k = rec.ops.length - 1; k >= 0; k--) {
|
||||
const o = rec.ops[k];
|
||||
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
|
||||
else if (o.t === 'block-') { const r = placeBlock(o.code, o.code === BP_FILTER ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
|
||||
else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (blockP[b] === BP_FILTER) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
|
||||
else if (o.t === 'mach+') { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm && siloFull(mm)) fail('empty the silo first'); else if (mm) removeMachine(mm.id, true, false); }
|
||||
else if (o.t === 'mach-') { const r = placeMachineAt(o.type, o.dir, o.bx, o.by); if (r) fail(r); else { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm) mm.on = o.on !== false; } }
|
||||
else if (o.t === 'mold+') { const mo = molds.find(m => m.kind === o.kind && m.x === o.bx * BS && m.y === o.by * BS); if (mo) removeMold(mo); }
|
||||
else if (o.t === 'mold-') { const r = placeMoldAt(o.kind, o.bx, o.by, o.wall); if (r) fail(r); }
|
||||
else if (o.t === 'paste') {
|
||||
// ghosts still waiting are cleared; anything already built from them is taken back
|
||||
for (const [bx, by, code] of o.blocks) { const b = by * BW + bx; if (ghostBlocks.has(b)) { ghostBlocks.delete(b); ghostBlocked.delete(b); } else if (blockP[b] === code) removeBlock(bx, by); }
|
||||
for (const g of o.machines) { const k = ghostMachines.indexOf(g); if (k >= 0) ghostMachines.splice(k, 1); else { const mm = machines.find(m => m && m.type === g.type && m.x === g.bx * BS && m.y === g.by * BS); if (mm) removeMachine(mm.id, true, false); } }
|
||||
for (const g of o.molds) { const k = ghostMolds.indexOf(g); if (k >= 0) ghostMolds.splice(k, 1); else { const mo = molds.find(m => m.kind === g.kind && m.x === g.bx * BS && m.y === g.by * BS); if (mo) removeMold(mo); } }
|
||||
}
|
||||
}
|
||||
undoRec = prev;
|
||||
powerDirty = true;
|
||||
const n = rec.ops.length;
|
||||
return fails ? `Undid ${rec.label}, but ${fails === n ? 'none' : fails} of it could not be put back${why ? ': ' + why.replace(/\.$/, '').toLowerCase() : ''}.` : `Undid ${rec.label}.`;
|
||||
}
|
||||
function clearAllGhosts() { ghostBlocks.clear(); ghostMachines = []; ghostMolds = []; ghostBlocked.clear(); }
|
||||
let ghostMissing = 0, ghostIter = null;
|
||||
function ghostTick() {
|
||||
if (!ghostBlocks.size && !ghostMachines.length && !ghostMolds.length) return;
|
||||
const keepRec = undoRec; undoRec = null;
|
||||
try { ghostTick0(); } finally { undoRec = keepRec; }
|
||||
}
|
||||
function ghostTick0() {
|
||||
let budget = 2, checks = 200;
|
||||
ghostMissing = 0;
|
||||
if (!ghostIter) ghostIter = ghostBlocks.entries();
|
||||
while (budget > 0 && checks-- > 0 && ghostBlocks.size) {
|
||||
let nx = ghostIter.next();
|
||||
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
|
||||
const [b, [code, v, e]] = nx.value;
|
||||
const bx = b % BW, by = (b / BW) | 0;
|
||||
if (blockP[b] === code && blockV[b] === (code === BP_FILTER ? v & 128 : v)) {
|
||||
if (code === BP_FILTER && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
|
||||
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
|
||||
}
|
||||
if (!blockInReach(bx, by)) continue;
|
||||
const err = canPlaceBlock(bx, by);
|
||||
if (err) { if (err.startsWith('Needs')) ghostBlocked.add(b); continue; }
|
||||
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
||||
placeBlock(code, v, bx, by, false, e);
|
||||
ghostBlocks.delete(b); ghostBlocked.delete(b); budget--;
|
||||
emit({ t: 'sfx', s: 'build' });
|
||||
}
|
||||
for (let k = ghostMachines.length - 1; k >= 0 && budget > 0; k--) {
|
||||
const g = ghostMachines[k], P = PIECE_BY_ID[g.type];
|
||||
if (!inReach(g.bx * BS + P.w / 2, g.by * BS + P.h / 2)) continue;
|
||||
if (canPlaceMachine(g.type, g.bx, g.by)) continue;
|
||||
if (!canAfford(P.cost, 1)) { ghostMissing++; continue; }
|
||||
placeMachineAt(g.type, g.dir, g.bx, g.by); ghostMachines.splice(k, 1); budget--;
|
||||
emit({ t: 'sfx', s: 'build' });
|
||||
}
|
||||
for (let k = ghostMolds.length - 1; k >= 0 && budget > 0; k--) {
|
||||
const g = ghostMolds[k], md = MOLDS[g.kind];
|
||||
if (!inReach(g.bx * BS + md.w / 2, g.by * BS + md.h / 2)) continue;
|
||||
if (canPlaceMold(g.kind, g.bx, g.by)) continue;
|
||||
if (!canAfford(wallCost(g.wall, md.walls), 1)) { ghostMissing++; continue; }
|
||||
addMold(g.kind, g.bx, g.by, g.wall, false); ghostMolds.splice(k, 1); budget--;
|
||||
emit({ t: 'sfx', s: 'build' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user