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
+85
View File
@@ -0,0 +1,85 @@
// Weather: clouds from steam, rain and snow, and the world water count.
// ---------- Weather ----------
// Steam that rises into the open sky is gathered into clouds above where it rose. The clouds
// drift slowly with the wind and rain it back out, or snow where the ground is below freezing.
// Water is only ever moved around, never made, so the world neither floods nor runs dry. If
// some does leave (dropped into the trade dock, say), a shower blows in from beyond the claim
// now and then with just enough to make it up.
const WBAND = 64;
let WS = 0x1b873593 | 0;
function wrnd() { WS ^= WS << 13; WS ^= WS >>> 17; WS ^= WS << 5; return (WS >>> 0) * 2.3283064365386963e-10; }
let weather = { moist: new Float32Array(1), raining: new Uint8Array(1), shower: null, next: 0, wind: 0.3, seen: false, skyY: 1 };
function resetWeather() {
let top = H; for (let x = 0; x < W; x++) if (surfY[x] < top) top = surfY[x];
weather = { moist: new Float32Array(Math.ceil(W / WBAND)), raining: new Uint8Array(Math.ceil(W / WBAND)), shower: null, next: tick + rainGap(), wind: wrnd() < 0.5 ? -0.3 : 0.3, seen: false, skyY: Math.max(1, top - 40), base: 0 };
}
function rainGap() { return 3600 * (5 + wrnd() * 7); } // 5 to 12 minutes between checks on the world's water
// All the world's water: everything that is water or will be again (ice, snow, steam, clouds,
// rain in the air, what is in your bag). weather.base is how much a world started with.
function worldWater() {
let n = 0;
for (let i = 0; i < N; i++) { const m = mat[i]; if (m === WATER || m === ICE || m === SNOW || m === STEAM) n++; }
for (let b = 0; b < weather.moist.length; b++) n += weather.moist[b];
for (const f of flyers) if (f.m === WATER || f.m === SNOW) n++;
for (const pn of pipeNets) if (pn.m === WATER) n += pn.n; // water in pipes still counts
for (const mm of machines) if (mm && mm.type === 'silo' && mm.sm === SNOW) n += mm.sn; // and snow in silos
return Math.round(n + bag[WATER] + bag[SNOW]);
}
function absorbSteam(x) { const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)); weather.moist[b] += 1; }
// Send about k drops (snowflakes where it is freezing) down from the clouds over x0..x1; returns how many fell
function spawnRain(x0, x1, k) {
if (flyers.length > 2500) return 0;
let n = Math.floor(k) + (wrnd() < k - Math.floor(k) ? 1 : 0), made = 0;
for (; n > 0; n--) {
const x = Math.floor(x0 + wrnd() * (x1 - x0));
if (x < 0 || x >= W) continue;
const y = Math.min(weather.skyY, Math.max(1, roofY[x] - 30));
const ground = Math.min(H - 1, roofY[x]), cold = AMB[ground] < 0;
flyers.push({ x: x + wrnd(), y: y + wrnd() * 6, vx: weather.wind * (cold ? 0.35 : 0.12), vy: cold ? 0.15 : 0.5, age: 0, m: cold ? SNOW : WATER, T: cold ? Math.min(AMB[ground], -2) : Math.max(4, Math.min(18, AMB[ground])), free: true, run: 0, rain: true });
made++;
}
return made;
}
// How hard it is raining over column x, 0 (dry) to 1 (a downpour)
function rainAt(x) {
let r = 0;
const sh = weather.shower;
if (sh && x >= sh.x0 - 40 && x <= sh.x1 + 40) r += sh.rate * sh.ramp * (x >= sh.x0 && x <= sh.x1 ? 1 : 0.5);
const b = Math.min(weather.moist.length - 1, Math.max(0, (x / WBAND) | 0)), mo = weather.moist[b];
if (weather.raining[b]) r += Math.min(3, 0.5 + mo / 100);
return Math.min(1, r / 2);
}
function weatherTick() {
const w = weather, n = w.moist.length;
if (!w.shower && tick >= w.next) {
// a shower from beyond the claim only when the world is short of the water it started with
const short = w.base ? w.base - worldWater() : 0;
if (short > Math.max(60, w.base * 0.03)) {
const len = Math.min(W, 160 + wrnd() * 380), x0 = wrnd() * Math.max(1, W - len);
w.shower = { x0, x1: x0 + len, rate: 0.6 + wrnd() * 1.6, until: tick + 60 * (40 + Math.floor(wrnd() * 80)), ramp: 0, left: short };
w.wind = (wrnd() - 0.5) * 0.8;
emit({ t: 'weather', kind: 'shower', x0: Math.round(x0), x1: Math.round(x0 + len) });
} else w.next = tick + rainGap();
}
if (w.shower) {
const sh = w.shower;
sh.ramp = tick < sh.until ? Math.min(1, sh.ramp + 1 / 300) : sh.ramp - 1 / 300; // fades in and out over 5 s
sh.x0 += w.wind * 0.03; sh.x1 += w.wind * 0.03;
if (sh.ramp <= 0) { w.shower = null; w.next = tick + rainGap(); }
else if (!(sh.left <= 0)) {
const made = spawnRain(sh.x0, sh.x1, Math.min(sh.left === undefined ? 1e9 : sh.left, sh.rate * sh.ramp * (sh.x1 - sh.x0) / 300));
if (sh.left !== undefined) { sh.left -= made; if (sh.left <= 0 && sh.until > tick) sh.until = tick; } // made up the shortfall: it passes
}
}
// clouds made of your steam build up, then rain it back out where they hang
for (let b = 0; b < n; b++) {
if (!w.raining[b] && w.moist[b] > 150) w.raining[b] = 1;
if (w.raining[b]) { w.moist[b] -= spawnRain(b * WBAND, (b + 1) * WBAND, Math.min(3, 0.5 + w.moist[b] / 100)); if (w.moist[b] < 12) w.raining[b] = 0; }
}
// and drift a little downwind every second; at the edge of the world they pile up instead
if (tick % 60 === 0) {
const out = new Float32Array(n), d = w.wind > 0 ? 1 : -1, f = Math.min(0.12, Math.abs(w.wind) * 0.1);
for (let b = 0; b < n; b++) { const mv = w.raining[b] ? 0 : w.moist[b] * f; out[b] += w.moist[b] - mv; if (b + d >= 0 && b + d < n) out[b + d] += mv; else out[b] += mv; }
w.moist = out;
}
}