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
+168
View File
@@ -0,0 +1,168 @@
// Trees (background layer), acorns, saplings, and waking freezing water.
// ---------- Trees (background layer) ----------
// Trunk cells drill into wood chunks; leaves just fall apart. A tree whose trunk
// no longer reaches the ground comes down: every trunk cell drops as wood chunks.
function cutTree(i, x, y, tier, amt = 1) {
const b = bgm[i];
const p = digp[i] + amt * tier.speed * 70 / (1 + HARD[b]);
if (p < 255) { digp[i] = p; return false; }
digp[i] = 0; bgm[i] = 0;
if (b === WOOD && woodRoom(i)) { setCell(i, LWOOD, temp[i], 0); discover(LWOOD, x, y, WOOD, 'dig'); }
else if (b === LEAVES && mat[i] === EMPTY && rnd() < 0.03) { setCell(i, ACORN, temp[i], 0); discover(ACORN, x, y, LEAVES, 'dig'); }
else { wakeXY(x, y); emit({ t: 'debris', x, y, m: b }); }
if (b === WOOD) for (const [dx, dy] of [[1, 0], [-1, 0], [0, -1], [0, 1], [1, -1], [-1, -1]]) {
const nx = x + dx, ny = y + dy;
if (nx >= 0 && ny >= 0 && nx < W && ny < H && bgm[ny * W + nx]) treeFall(ny * W + nx);
}
return true;
}
// A trunk cell is held up by solid ground or soil directly beneath it.
function treeHeld(i) {
if (i + W >= N) return true;
const b = mat[i + W];
return PH[b] === PH_SOLID || SOIL[b] === 1;
}
// Base cells of every tree (the lowest trunk cells). Checked now and then: a tree
// whose ground has been dug or washed away comes down.
let treeBases = [];
function findTreeBases() {
treeBases = [];
for (let i = 0; i < N - W; i++) if (bgm[i] === WOOD && bgm[i + W] !== WOOD) treeBases.push(i);
}
function treeSupportTick() {
for (let k = treeBases.length - 1; k >= 0; k--) {
const i = treeBases[k];
if (bgm[i] !== WOOD) { treeBases.splice(k, 1); continue; }
if (!treeHeld(i)) { treeBases.splice(k, 1); treeFall(i); }
}
}
// Make room for a wood chunk where a trunk cell was: air is fine; water there is
// pushed to the nearest open cell (the chunk then floats up).
function woodRoom(i) {
const m = mat[i];
if (m === EMPTY || PH[m] === PH_GAS) return true;
if (PH[m] !== PH_LIQUID) return false;
const T = temp[i];
mat[i] = EMPTY;
depositNear(m, T, i % W, (i / W) | 0, i); // if there is nowhere at all, that bit of water is lost
return true;
}
function treeFall(start) {
if (bgm[start] !== WOOD) return;
// follow the wood (trunk and branches) to see whether any of it still stands on something
const seenT = new Set([start]), st = [start], woodCells = [];
let grounded = false, bx0 = W, bx1 = 0, by0 = H, by1 = 0;
while (st.length && woodCells.length < 2000) {
const i = st.pop(); woodCells.push(i);
const x = i % W, y = (i / W) | 0;
if (treeHeld(i)) { grounded = true; break; }
if (x < bx0) bx0 = x; if (x > bx1) bx1 = x; if (y < by0) by0 = y; if (y > by1) by1 = y;
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = ny * W + nx; if (bgm[j] === WOOD && !seenT.has(j)) { seenT.add(j); st.push(j); }
}
}
if (grounded) return;
// its crown: leaves connected to it, not straying far from its own wood
const cells = woodCells.slice();
for (const i of woodCells) st.push(i);
bx0 -= 9; bx1 += 9; by0 -= 12; by1 += 4;
while (st.length && cells.length < 4000) {
const i = st.pop(), x = i % W, y = (i / W) | 0;
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
const nx = x + dx, ny = y + dy; if (nx < bx0 || ny < by0 || nx > bx1 || ny > by1 || nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = ny * W + nx; if (bgm[j] === LEAVES && !seenT.has(j)) { seenT.add(j); st.push(j); cells.push(j); }
}
}
let wood = 0;
// a felled tree drops a few acorns from its crown
const crown = cells.filter(i => bgm[i] === LEAVES && mat[i] === EMPTY);
const nAcorn = Math.min(crown.length, 1 + (crown.length > 60 ? 1 : 0) + (rnd() < 0.5 ? 1 : 0));
for (let k = 0; k < nAcorn; k++) { const i = crown[(rnd() * crown.length) | 0]; if (mat[i] === EMPTY) { setCell(i, ACORN, AMB[(i / W) | 0], 0); bgm[i] = 0; discover(ACORN, i % W, (i / W) | 0, LEAVES, 'dig'); } }
for (const i of cells) {
const x = i % W, y = (i / W) | 0;
if (bgm[i] === WOOD && woodRoom(i)) { setCell(i, LWOOD, temp[i], 0); wood++; }
else if (bgm[i] && (x + y) % 3 === 0) emit({ t: 'debris', x, y, m: bgm[i] });
bgm[i] = 0; wakeXY(x, y);
}
if (wood) { discover(LWOOD, cells[0] % W, (cells[0] / W) | 0, WOOD, 'dig'); emit({ t: 'sfx', s: 'decon' }); }
}
// Acorns resting on soil under open sky sprout after a while; saplings grow
// into full trees in the background layer.
let saplings = [];
// Heat only runs where something is off ambient, so water resting in freezing air
// would never be looked at. Wake its area now and then so it can freeze.
function coldTick() {
for (let y = 0; y < H && AMB[y] < -0.5; y++) {
const yw = y * W;
for (let x = 0; x < W; x++) if (mat[yw + x] === WATER) { wakeHeatXY(x, y); x |= 31; }
}
}
function plantTick() {
for (let x = 0; x < W; x++) {
const y = roofY[x];
if (y <= 6 || y >= H - 1) continue;
const i = y * W + x;
if (mat[i] !== ACORN || (flags[i] & F_BURN)) continue;
if (!SOIL[mat[i + W]] || temp[i] < 2 || temp[i] > 45) { life[i] = 0; continue; }
let room = true;
for (let k = 1; k <= 6 && room; k++) { const j = i - k * W; if (PH[mat[j]] !== PH_EMPTY && PH[mat[j]] !== PH_GAS || bgm[j] === WOOD) room = false; }
if (!room) continue;
if (++life[i] < 14) continue;
setCell(i, EMPTY, AMB[y], 0);
saplings.push({ x, y: y + 1, age: 0, th: 8 + ((rnd() * 7) | 0), seed: (rnd() * 1e9) | 0 });
if (!stats.sprouted) { stats.sprouted = 1; emit({ t: 'sprout', x, y }); }
}
}
let treeDraw = null; // cells the sapling being drawn has set, so the next stage can redraw it
function setTreeCell(x, y, m) {
if (x < 0 || y < 1 || x >= W || y >= H) return;
const i = y * W + x;
if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return;
if (m === LEAVES && bgm[i]) return;
bgm[i] = m;
if (treeDraw) treeDraw.push(i);
}
function saplingTick() {
for (let k = saplings.length - 1; k >= 0; k--) {
const sp = saplings[k], base = sp.y * W + sp.x;
// the soil it grows from must still be there
if (sp.y >= H || !(SOIL[mat[base]] || PH[mat[base]] === PH_SOLID)) { saplings.splice(k, 1); continue; }
if (sp.age > 2 && bgm[base - W] !== WOOD) { saplings.splice(k, 1); continue; } // cut down or burned while young
// if any trunk it grew is gone (cut or burned), it stops growing and is left as it is
if (sp.wood && sp.wood.some(i => bgm[i] !== WOOD)) { saplings.splice(k, 1); continue; }
sp.age++;
const s = Math.min(1, sp.age / 180);
// wipe the previous stage, then draw this one
if (sp.cells) for (const i of sp.cells) if (bgm[i] === LEAVES || (bgm[i] === WOOD && i !== base - W)) bgm[i] = 0;
treeDraw = sp.cells = [];
const th = Math.max(1, Math.round(sp.th * s)), wide = s > 0.5 ? 2 : 1;
for (let j = 1; j <= th; j++) for (let dx = 0; dx < wide; dx++) setTreeCell(sp.x + dx, sp.y - j, WOOD);
if (s > 0.2) {
// the same lumpy crown as wild trees, scaled with age
const q = s, h0 = sp.seed;
if (s >= 1) for (const side of [-1, 1]) { const by = sp.y - th + 3 + (h0 >> (side > 0 ? 3 : 5)) % 3; for (let j = 1; j <= Math.round(3 * q); j++) setTreeCell(sp.x + (side < 0 ? -j : 1 + j), by - (j >> 1), WOOD); }
const blobs = [[0.5, -th - 1, (6 + (h0 % 25) / 10) * q], [-3.5 * q, -th + 1, (4 + (h0 >> 4) % 20 / 10) * q], [4.5 * q, -th + 1, (4 + (h0 >> 8) % 20 / 10) * q], [0.5, -th - 5 * q, (4 + (h0 >> 12) % 20 / 10) * q]];
for (const [bx, by, r] of blobs) {
if (r < 1) continue;
const cx = sp.x + bx, cy = sp.y + by;
for (let yy = Math.floor(cy - r); yy <= cy + r; yy++) for (let xx = Math.floor(cx - r); xx <= cx + r; xx++) {
const ex = (xx - cx) / r, ey = (yy - cy) / (r * 0.85);
if (ex * ex + ey * ey <= 1 && hash2(xx, yy, worldSeed + 7) % 9 !== 0) setTreeCell(xx, yy, LEAVES);
}
}
}
treeDraw = null;
sp.wood = sp.cells.filter(i => bgm[i] === WOOD);
for (const i of sp.wood) if (bgm[i + W] !== WOOD && !treeBases.includes(i)) treeBases.push(i);
if (s >= 1) { saplings.splice(k, 1); stats.grown = (stats.grown || 0) + 1; }
}
}
// A flame next to a tree sets that part of it alight: it moves into the
// foreground as burning wood or leaves and burns like any other fuel.
function igniteTree(j) {
const b = bgm[j]; bgm[j] = 0;
mat[j] = b; flags[j] = F_BURN; life[j] = FUEL[b]; temp[j] = Math.max(temp[j], IGN[b] + 40);
const x = j % W, y = (j / W) | 0;
wakeXY(x, y); wakeHeatXY(x, y);
}