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:
2026-10-23 16:05:31 -04:00
co-authored by Claude Opus 5.5
commit b270c4e818
97 changed files with 11710 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
/* Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers,
toasts and the status line. */
// ---------- Settings ----------
const SET_DEFAULT = {
zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7,
look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: true, sv: 2,
};
const SET = Object.assign({}, SET_DEFAULT);
// sv: settings version. 2: the default zoom became fully zoomed in, so older saved settings move there once.
function loadSettings() {
try {
const s = JSON.parse(localStorage.getItem('deepgrain.settings') || '{}'); Object.assign(SET, s);
if (!(s.sv >= 2)) { SET.zoom = SET_DEFAULT.zoom; SET.sv = 2; if (Object.keys(s).length) saveSettings(); }
} catch (e) { /* storage unavailable */ }
}
function saveSettings() { try { localStorage.setItem('deepgrain.settings', JSON.stringify(SET)); } catch (e) { /* storage unavailable */ } }
loadSettings();
// ---------- Game state ----------
const TOOLS = [
{ id: 'drill', name: 'Drill', key: '1' },
{ id: 'vacuum', name: 'Vacuum', key: '2' },
{ id: 'build', name: 'Build', key: '3' },
{ id: 'ignite', name: 'Igniter', key: '4' },
];
const ICONS = {
drill: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M4 9h9l3 3-3 3H4z"/><path d="M16 12h5M17 9.5l3 2.5-3 2.5"/><path d="M7 9v6M10 9v6"/></svg>',
vacuum: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M3 15l6-6h4l2 2v4l-6 6z"/><path d="M15 11l5-5M17 4l3 3"/><circle cx="6" cy="19" r="0.8" fill="currentColor"/><circle cx="4" cy="21" r="0.6" fill="currentColor"/></svg>',
build: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="6" width="18" height="12" rx="1"/><path d="M3 12h18M9 6v6M15 12v6"/></svg>',
ignite: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M12 21c-4 0-6-2.6-6-5.6 0-3.4 3-5 3.6-8.4C12.5 9 14 11 14 13c1-.6 1.6-1.8 1.6-3 2 1.6 2.4 3.6 2.4 5.4 0 3-2 5.6-6 5.6z"/></svg>',
blueprint: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="11" height="11" rx="1" stroke-dasharray="2.5 2"/><rect x="10" y="10" width="11" height="11" rx="1"/></svg>',
};
const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN, BARGL, BARSB, AUORE, AGORE, ZNORE, SNORE, RSTONE, LCLAY, RBRICK, LDIRT, LWOOD, ACORN, COAL, CHARCOAL, CUORE, FEORE, SAND, GRAVEL, RGRAN, RBAS, CULLET,
SCRCU, SCRFE, SCRST, SCRSN, SCRZN, SCRAG, SCRAU, SCRBZ, SCRBS, ASH, SLAG, SNOW, WATER, MAGMA, MCU, MFE, MST, MGL, MSN, MZN, MAG, MAU, MBZ, MBS];
function loadCollect() { try { const a = JSON.parse(localStorage.getItem('deepgrain.collect') || '[]'); return new Set(a.filter(m => m > 0 && m < NMAT && BAGOK[m])); } catch (e) { return new Set(); } }
const GAME = {
running: false, paused: false, tool: 0, dropMat: 0, matChosen: false, wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {},
brush: [3, 2, 0, 0, 0], collect: loadCollect(), tipsOpen: new Set(), cards: [], vacFilter: 0, vacLocked: false, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 },
paste: false, selDrag: null, rmbRec: null, dragDir: -1, ctrlShown: false, clip: null, drag: null, aim: { x: 0, y: 0 }, mouse: { x: 0, y: 0, in: false },
lmb: false, rmb: false, blowMix: false, drillW: 1, drillTune: 0, lastDug: 0, padRun: 0, status: '', statusKind: 'err', statusT: 0, usingPad: false, padAim: { x: 12, y: 0 },
lastAutosave: 0, playTime: 0, lastHud: 0, worldName: '',
};
const keys = new Set();
let hudDirty = true;
// ---------- DOM helpers ----------
const $ = id => document.getElementById(id);
function el(tag, attrs, html) { const e = document.createElement(tag); if (attrs) for (const k in attrs) e.setAttribute(k, attrs[k]); if (html !== undefined) e.innerHTML = html; return e; }
function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
function rgbOf(m) { return `rgb(${COLR[m]},${COLG[m]},${COLB[m]})`; }
function fmtT(t) { return t >= 999.5 ? `${Math.round(t).toLocaleString('en-US')}°C` : `${Math.round(t)}°C`; }
// ---------- Toasts ----------
function toast(text, opts = {}) {
const box = $('toasts');
while (box.children.length > 4) box.firstChild.remove();
const t = el('div', { class: 'toast plate' + (opts.big ? ' big' : '') });
t.innerHTML = opts.title ? `<b>${esc(opts.title)}</b>${esc(text)}` : esc(text);
box.appendChild(t);
const life = opts.big ? 5200 : 2600;
setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 450); }, life);
}
function setStatus(s, kind) { if (s) { GAME.status = s; GAME.statusKind = kind || 'err'; GAME.statusT = performance.now(); hudDirty = true; } }