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:
+246
@@ -0,0 +1,246 @@
|
||||
// Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
||||
// ---------- Heat ----------
|
||||
function curAir() { return (tick & 0x7fff) + 1; }
|
||||
function airflowAt(i) { const a = airS[i]; return a !== 0 && ((curAir() - a) & 0x7fff) < 8; }
|
||||
// Air reaches a fuel grain if an open cell touches it, or sits just behind a
|
||||
// porous neighbor (ash or loose coal), so a fuel bed burns a couple of grains deep.
|
||||
const POROUS = new Uint8Array(NMAT); POROUS[ASH] = 1; POROUS[SLAG] = 1; POROUS[COAL] = 1; POROUS[CHARCOAL] = 1;
|
||||
const SOIL = new Uint8Array(NMAT); SOIL[DIRT] = 1; SOIL[GRASS] = 1; SOIL[LDIRT] = 1;
|
||||
const HOTLIQ = new Uint8Array(NMAT); for (const m of [MAGMA, MCU, MFE, MST, MGL, MAG, MAU, MBZ, MBS, MSLAG]) HOTLIQ[m] = 1;
|
||||
// Alloys: a molten additive grain turns neighbouring molten copper into the alloy,
|
||||
// and is itself used up after about this many (the ratio: bronze ~1:7 tin, brass ~1:2 zinc).
|
||||
const ALLOY = { [MSN]: { into: MBZ, per: 7 }, [MZN]: { into: MBS, per: 2 } };
|
||||
function alloyMix(i, x, y, m) {
|
||||
const a = ALLOY[m];
|
||||
for (let d = 0; d < 4; d++) {
|
||||
const j = neighborDir(i, x, y, (d + tick) & 3);
|
||||
if (j < 0 || mat[j] !== MCU || rnd() > 0.25) continue;
|
||||
mat[j] = a.into; wakeIdx(j); discover(a.into, j % W, (j / W) | 0, m, 'alloy');
|
||||
if (rnd() < 1 / a.per) { mat[i] = a.into; wakeXY(x, y); return; }
|
||||
}
|
||||
}
|
||||
function airy(m) { return m === EMPTY || m === FIRE; }
|
||||
function hasO2(i, x, y) {
|
||||
let m;
|
||||
if (x > 0) { m = mat[i - 1]; if (airy(m) || (POROUS[m] && (m === ASH || m === SLAG || (x > 1 && airy(mat[i - 2]))))) return true; }
|
||||
if (x < W - 1) { m = mat[i + 1]; if (airy(m) || (POROUS[m] && (m === ASH || m === SLAG || (x < W - 2 && airy(mat[i + 2]))))) return true; }
|
||||
if (y > 0) { m = mat[i - W]; if (airy(m) || (POROUS[m] && (m === ASH || m === SLAG || (y > 1 && airy(mat[i - 2 * W]))))) return true; }
|
||||
if (y < H - 1) { m = mat[i + W]; if (airy(m) || (POROUS[m] && (m === ASH || m === SLAG || (y < H - 2 && airy(mat[i + 2 * W]))))) return true; }
|
||||
return false;
|
||||
}
|
||||
// Conducts heat between a and b. `da` = AMB(a) - AMB(b): only the excess over
|
||||
// the local ambient flows, so the built-in geothermal gradient stays put.
|
||||
function condPair(a, b, sc, nc, da) {
|
||||
const ma = mat[a], mb = mat[b];
|
||||
const d = temp[a] - temp[b] - da;
|
||||
if (d < 0.05 && d > -0.05) return;
|
||||
const k = KC[ma] < KC[mb] ? KC[ma] : KC[mb];
|
||||
const q = d * k * sc;
|
||||
temp[a] -= q * ICAP[ma];
|
||||
temp[b] += q * ICAP[mb];
|
||||
if (nc >= 0 && (q > 0.15 || q < -0.15)) { if (!hot[nc]) hotLvl[nc] = 2; hot[nc] = 6; }
|
||||
}
|
||||
function heatPass() {
|
||||
for (let c = 0; c < NCH; c++) {
|
||||
if (!hot[c]) continue;
|
||||
let sc = 1;
|
||||
if (hotLvl[c] === 1) { if (((tick + c) & 3) !== 0) continue; sc = 4; }
|
||||
heatChunk(c, sc);
|
||||
}
|
||||
}
|
||||
function heatChunk(c, sc) {
|
||||
const cx = c % CW, cy = (c / CW) | 0;
|
||||
const x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
|
||||
const leftCold = cx > 0 && !hot[c - 1], upCold = cy > 0 && !hot[c - CW];
|
||||
let maxDev = 0, burning = false;
|
||||
for (let y = y0; y < y1; y++) {
|
||||
const amb = AMB[y], yw = y * W;
|
||||
const dDown = y + 1 < H ? amb - AMB[y + 1] : 0, dUp = y > 0 ? AMB[y - 1] - amb : 0;
|
||||
for (let x = x0; x < x1; x++) {
|
||||
const i = yw + x;
|
||||
if (x + 1 < W) condPair(i, i + 1, sc, x + 1 === x1 ? c + 1 : -1, 0);
|
||||
if (y + 1 < H) condPair(i, i + W, sc, y + 1 === y1 ? c + CW : -1, dDown);
|
||||
if (x === x0 && leftCold) condPair(i - 1, i, sc, c - 1, 0);
|
||||
if (y === y0 && upCold) condPair(i - W, i, sc, c - CW, dUp);
|
||||
let m = mat[i];
|
||||
const ph = PH[m];
|
||||
let t = temp[i];
|
||||
let rel;
|
||||
if (ph === PH_EMPTY || ph === PH_GAS) {
|
||||
if (y > 0) {
|
||||
const u = i - W, pu = PH[mat[u]];
|
||||
const ex = (t - amb) - (temp[u] - AMB[y - 1]);
|
||||
if ((pu === PH_EMPTY || pu === PH_GAS) && ex > 2) {
|
||||
const a = ex * (sc > 1 ? 0.45 : 0.25);
|
||||
t -= a; temp[u] += a;
|
||||
if (y === y0 && upCold && a > 0.5) { hot[c - CW] = 6; hotLvl[c - CW] = 2; }
|
||||
}
|
||||
}
|
||||
rel = y < roofY[x] ? 0.02 : 0.002;
|
||||
} else rel = RELAX[m];
|
||||
t += (amb - t) * rel * sc;
|
||||
temp[i] = t;
|
||||
if (m !== EMPTY) { react(i, x, y, m); m = mat[i]; t = temp[i]; }
|
||||
const dv = t > amb ? t - amb : amb - t;
|
||||
if (dv > maxDev) maxDev = dv;
|
||||
if (flags[i] & F_BURN) burning = true;
|
||||
}
|
||||
}
|
||||
if (burning || maxDev > 1.5) { hot[c] = 6; hotLvl[c] = (burning || maxDev > 60) ? 2 : 1; }
|
||||
else hot[c]--;
|
||||
}
|
||||
function neighborDir(i, x, y, d) {
|
||||
if (d === 0) return y > 0 ? i - W : -1;
|
||||
if (d === 1) return x < W - 1 ? i + 1 : -1;
|
||||
if (d === 2) return y < H - 1 ? i + W : -1;
|
||||
return x > 0 ? i - 1 : -1;
|
||||
}
|
||||
function react(i, x, y, m) {
|
||||
const t = temp[i];
|
||||
const fl = flags[i];
|
||||
if (fl & F_BURN) { burn(i, x, y, m, t); return; }
|
||||
if (IGN[m] !== 0 && t >= IGN[m] && (hasO2(i, x, y) || burningNear(i, x, y))) {
|
||||
flags[i] = fl | F_BURN; life[i] = FUEL[m];
|
||||
stats.ignitions++; discover(FIRE, x, y, m, 'ignite', t);
|
||||
return;
|
||||
}
|
||||
const ph = PH[m];
|
||||
if (ph === PH_LIQUID) {
|
||||
if (m === WATER && t < -0.5) {
|
||||
// freezes: into ice on something solid, into snowflakes in open air
|
||||
if (rnd() < 0.02 - t * 0.004) {
|
||||
const below = y < H - 1 ? PH[mat[i + W]] : PH_SOLID;
|
||||
const to = below === PH_EMPTY || below === PH_GAS ? SNOW : ICE;
|
||||
mat[i] = to; life[i] = 0; flags[i] = fl & F_NAT; wakeXY(x, y); discover(to, x, y, WATER, 'freeze', 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (HOTLIQ[m] && t > 600 && rnd() < 0.5) quench(i, x, y, m);
|
||||
if (ALLOY[m]) alloyMix(i, x, y, m);
|
||||
const fz = FRZ[m];
|
||||
if (fz !== 0) {
|
||||
if (t < fz) {
|
||||
const L = life[i] - (fz - t) * 0.28;
|
||||
if (L <= 0) {
|
||||
const to = FRZTO[m];
|
||||
mat[i] = to; life[i] = 0; temp[i] = fz - 1;
|
||||
flags[i] = (fl & F_NAT) | (CASTMETAL[m] ? F_CAST : 0); // natural magma freezes into natural rock
|
||||
wakeXY(x, y); discover(to, x, y, m, 'freeze', fz);
|
||||
} else { life[i] = L; temp[i] = fz; }
|
||||
return;
|
||||
}
|
||||
if (life[i] < 255 && t > fz + 1) {
|
||||
const add = Math.min(255 - life[i], (t - fz) * 0.28);
|
||||
life[i] += add; temp[i] = t - add / 0.28;
|
||||
}
|
||||
if (m === MFE && t >= 1530) steelCheck(i, x, y);
|
||||
return;
|
||||
}
|
||||
const bp = BOIL[m];
|
||||
if (bp !== 0) {
|
||||
if (t > bp) {
|
||||
const L = life[i] + (t - bp) * 2.0;
|
||||
if (L >= 255) {
|
||||
mat[i] = BOILTO[m]; life[i] = 0; temp[i] = bp + 3; flags[i] = 0;
|
||||
wakeXY(x, y); discover(BOILTO[m], x, y, m, 'boil', bp);
|
||||
if (rnd() < 0.02) emit({ t: 'sfx', s: 'sizzle', x, y });
|
||||
} else { life[i] = L; temp[i] = bp; }
|
||||
} else if (life[i] > 0) life[i] = life[i] > 2 ? life[i] - 2 : 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ph === PH_GAS) {
|
||||
if (CONDT[m] !== 0 && t < CONDT[m] && rnd() < 0.05 && !(m === STEAM && y < roofY[x])) { mat[i] = CONDTO[m]; life[i] = 0; wakeXY(x, y); discover(CONDTO[m], x, y, m, 'condense', CONDT[m]); }
|
||||
return;
|
||||
}
|
||||
const mp = MELT[m];
|
||||
if (mp !== 0 && t >= mp) {
|
||||
if (rnd() < 0.03 + (t - mp) * 0.002) {
|
||||
let to = MELTTO[m];
|
||||
if (SMELTP[m] !== 0 && rnd() >= SMELTP[m]) to = t >= FRZ[MSLAG] ? MSLAG : SLAG; // the waste runs off molten when it is hot enough
|
||||
if (PARTMAT[m]) { partLost(i); if (m === HULL && mach[i]) brokenMachines.push(mach[i]); }
|
||||
mat[i] = to; flags[i] = 0; life[i] = PH[to] === PH_LIQUID ? 255 : 0; digp[i] = 0;
|
||||
wakeXY(x, y); discover(to, x, y, m, SMELTP[m] !== 0 ? 'smelt' : 'melt', mp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m === CLAY) {
|
||||
if (t >= 880 && rnd() < 0.004 + (t - 880) * 0.00012) { mat[i] = BRICK; discover(BRICK, x, y, CLAY, 'fire', 880); }
|
||||
} else if (m === LCLAY) {
|
||||
if (t >= 880 && rnd() < 0.004 + (t - 880) * 0.00012) { mat[i] = RBRICK; discover(RBRICK, x, y, LCLAY, 'fire', 880); }
|
||||
} else if ((m === LWOOD || m === WOOD) && t >= 380) {
|
||||
// baked hot with no air to burn in: wood turns into charcoal and gives off its volatiles
|
||||
if (rnd() < 0.006 + (t - 380) * 0.00002) char(i, x, y, m);
|
||||
} else if (m === GRASS) {
|
||||
if (t >= 260) { mat[i] = DIRT; }
|
||||
}
|
||||
}
|
||||
// Wood with heat but no air chars instead of burning (a kiln, or a smothered pile).
|
||||
function char(i, x, y, m) {
|
||||
const keep = rnd() < 0.6;
|
||||
mat[i] = keep ? CHARCOAL : EMPTY; flags[i] = 0; life[i] = 0; digp[i] = 0;
|
||||
wakeXY(x, y);
|
||||
if (keep) discover(CHARCOAL, x, y, m, 'char');
|
||||
}
|
||||
// A very hot liquid meeting water flashes it to steam and is chilled hard;
|
||||
// snow and ice next to it melt at once.
|
||||
function quench(i, x, y, m) {
|
||||
for (let d = 0; d < 4; d++) {
|
||||
const j = neighborDir(i, x, y, d);
|
||||
if (j < 0) continue;
|
||||
const n = mat[j];
|
||||
if (n === WATER) {
|
||||
mat[j] = STEAM; temp[j] = 110; life[j] = 0; flags[j] = 0; wakeIdx(j); wakeHeatIdx(j);
|
||||
temp[i] -= 40; discover(STEAM, j % W, (j / W) | 0, WATER, 'boil', 100);
|
||||
if (rnd() < 0.05) emit({ t: 'sfx', s: 'sizzle', x, y });
|
||||
} else if (n === SNOW || n === ICE) {
|
||||
mat[j] = WATER; temp[j] = 20; life[j] = 0; flags[j] = 0; wakeIdx(j); wakeHeatIdx(j); temp[i] -= 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
function burningNear(i, x, y) {
|
||||
return (x > 0 && (flags[i - 1] & F_BURN)) || (x < W - 1 && (flags[i + 1] & F_BURN)) ||
|
||||
(y > 0 && (flags[i - W] & F_BURN)) || (y < H - 1 && (flags[i + W] & F_BURN));
|
||||
}
|
||||
function burn(i, x, y, m, t) {
|
||||
const forced = airflowAt(i);
|
||||
const open = forced || hasO2(i, x, y);
|
||||
// smothered wood chars into charcoal (a covered pile, like a charcoal clamp)
|
||||
if (!open && (m === LWOOD || m === WOOD) && rnd() < 0.006) { char(i, x, y, m); return; }
|
||||
// buried fuel next to burning fuel smolders: less heat, slower
|
||||
if (!open && !burningNear(i, x, y)) {
|
||||
if (rnd() < 0.004) flags[i] &= ~F_BURN;
|
||||
return;
|
||||
}
|
||||
const target = forced ? FLAMEF[m] : open ? FLAME[m] : FLAME[m] * 0.95;
|
||||
if (t < target) temp[i] = t + (target - t) * (forced ? 0.2 : open ? 0.07 : 0.035);
|
||||
if (rnd() < (forced ? 0.5 : open ? 0.22 : 0.1)) {
|
||||
const l = life[i] - 1;
|
||||
if (l <= 0) {
|
||||
if (PARTMAT[m]) partLost(i);
|
||||
const to = BURNTO[m];
|
||||
mat[i] = (to === ASH && rnd() < ASHP[m]) ? ASH : EMPTY;
|
||||
flags[i] = 0; life[i] = 0; wakeXY(x, y);
|
||||
if (mat[i] === ASH) discover(ASH, x, y, m, 'burn');
|
||||
return;
|
||||
}
|
||||
life[i] = l;
|
||||
}
|
||||
if (open && rnd() < 0.3) {
|
||||
const j = neighborDir(i, x, y, (rnd() * 4) | 0);
|
||||
if (j >= 0 && mat[j] === EMPTY && part[j] !== P_INTAKE) {
|
||||
mat[j] = FIRE; temp[j] = temp[i]; life[j] = 8 + ((rnd() * 18) | 0); flags[j] = 0;
|
||||
wakeIdx(j);
|
||||
}
|
||||
}
|
||||
if (rnd() < 0.012) {
|
||||
const j = neighborDir(i, x, y, (rnd() * 4) | 0);
|
||||
if (j >= 0 && mat[j] === EMPTY) { mat[j] = SMOKE; temp[j] = temp[i] * 0.8; life[j] = 200 + ((rnd() * 55) | 0); flags[j] = 0; wakeIdx(j); }
|
||||
}
|
||||
}
|
||||
// Bessemer: air blown through molten iron burns off excess carbon (and gives off heat).
|
||||
function steelCheck(i, x, y) {
|
||||
if (airflowAt(i) && rnd() < 0.025) {
|
||||
mat[i] = MST; life[i] = 255; temp[i] += 45; discover(MST, x, y, MFE, 'blow');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user