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:
+167
@@ -0,0 +1,167 @@
|
||||
// Discovery cards, hover info (I) and the cursor label.
|
||||
// ---------- Discovery cards ----------
|
||||
// Short cards in the lower right when you find or make something new. They fade
|
||||
// on their own (hover holds them); everything they say is also in the notebook.
|
||||
function showCard(c) {
|
||||
if (!SET.cardSeconds) return;
|
||||
const box = $('cards');
|
||||
while (box.children.length >= 3) box.firstChild.remove();
|
||||
const card = el('div', { class: 'card-pop plate interactive' + (c.accent ? ' ' + c.accent : ''), role: 'status' });
|
||||
const timed = SET.cardSeconds > 0;
|
||||
card.innerHTML = `<div class="top">${c.swatch ? `<span class="sw" style="background:${rgbOf(c.swatch)}"></span>` : ''}<span class="label">${esc(c.kind)}</span><button class="x" aria-label="Dismiss">×</button></div>`
|
||||
+ `<div class="title">${esc(c.title)}</div>${(c.body || []).filter(Boolean).map((t, k) => `<div class="${k ? 'p2' : 'p1'}">${esc(t)}</div>`).join('')}`
|
||||
+ (timed ? `<i class="life" style="--dur:${(SET.cardSeconds * (c.hold || 1)).toFixed(1)}s"></i>` : '');
|
||||
const close = () => { if (card.classList.contains('out')) return; card.classList.add('out'); setTimeout(() => card.remove(), 380); };
|
||||
card.querySelector('.x').onclick = () => { playSfx('ui'); close(); };
|
||||
if (timed) card.querySelector('.life').addEventListener('animationend', close);
|
||||
box.appendChild(card);
|
||||
}
|
||||
function howText(e) {
|
||||
const F = e.from ? MD[e.from].name : '', f = F.toLowerCase();
|
||||
switch (e.how) {
|
||||
case 'dig': return e.from === WOOD ? 'Cut from a tree.' : `Broken loose from ${f}.`;
|
||||
case 'melt': return `${F} melted at ${fmtT(e.T)}.`;
|
||||
case 'smelt': return e.m === SLAG || e.m === MSLAG ? `Left over when ${f} smelted.` : `${F} smelted at ${fmtT(e.T)}.`;
|
||||
case 'freeze': return e.from === WATER ? (e.m === SNOW ? 'Water froze into flakes in the cold air.' : 'Water froze in the cold air.') : `${F} set as it cooled below ${fmtT(e.T)}.`;
|
||||
case 'boil': return `${F} boiled at ${fmtT(e.T)}.`;
|
||||
case 'condense': return `${F} cooled below ${fmtT(e.T)} and condensed.`;
|
||||
case 'ignite': return `${F} caught fire at ${fmtT(e.T)}.`;
|
||||
case 'burn': return `What ${f} leaves behind when it burns.`;
|
||||
case 'fire': return `${F} fired into brick in the heat above ${fmtT(e.T)}.`;
|
||||
case 'blow': return 'Air blown through molten iron burned off its extra carbon.';
|
||||
case 'alloy': return `${F} melted into molten copper and the two mixed.`;
|
||||
case 'char': return `${F} got hot with no air to burn in, and charred.`;
|
||||
case 'refine': return `Sifted out of ${f} by a Refiner.`;
|
||||
case 'cut': return `Cut from ${f} by a Stone cutter.`;
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
function discoveryCard(e) {
|
||||
const m = e.m;
|
||||
if (m === FIRE) { showCard({ kind: 'Discovery', title: 'Fire', body: [howText(e), 'Fuel burns where it touches air. More air, more heat.'] }); return; }
|
||||
showCard({ kind: 'New material', title: MD[m].name, swatch: m, body: [howText(e), MD[m].desc] });
|
||||
}
|
||||
// Everything a material does when heated, cooled, wetted or lit, in short phrases. The hover
|
||||
// box and the notebook's Materials page both show this list.
|
||||
function reactionsOf(m, forNotebook) {
|
||||
const d = MD[m], out = [];
|
||||
if (!d || m === EMPTY || d.ph === PH_ITEM && !d.melt) return out;
|
||||
const nm = k => MD[k].name.toLowerCase();
|
||||
if (d.ign) out.push(`burns, catching at ${fmtT(d.ign)}`);
|
||||
if (m === WOOD || m === LWOOD) out.push('turns to charcoal if heated with no air');
|
||||
if (m === CLAY || m === LCLAY) out.push('fires into brick above 880°C');
|
||||
if (m === WATER) out.push('freezes below 0°C', 'boils at 100°C');
|
||||
else if (d.boil) out.push(`boils at ${fmtT(d.boil)}`);
|
||||
if (d.melt) {
|
||||
if (d.smelt) out.push(`smelts at ${fmtT(d.melt)} into ${nm(d.meltTo)} and slag`);
|
||||
else if (d.meltTo === WATER) out.push('melts above 0°C');
|
||||
else if (d.ph === PH_ITEM) out.push(`melts at ${fmtT(d.melt)}`);
|
||||
else out.push(`melts at ${fmtT(d.melt)}${d.meltTo === MAGMA ? ' into magma' : d.meltTo === MGL && m !== GLASS ? ' into glass' : d.meltTo === MSLAG && m !== SLAG ? ' into slag' : ''}`);
|
||||
}
|
||||
if (d.freeze) out.push(`sets into ${nm(d.freezeTo)} below ${fmtT(d.freeze)}`);
|
||||
if (m === STEAM) out.push('turns back to water below 97°C under a roof', 'rises into the clouds in the open');
|
||||
else if (d.condT) out.push(`turns back to ${nm(d.condTo)} below ${fmtT(d.condT)}`);
|
||||
if (HOTLIQ[m]) out.push(m === MAGMA ? 'boils water, and crusts into basalt in it' : 'boils water it touches');
|
||||
if (ALLOY[m]) out.push(`mixes into molten copper as ${nm(FRZTO[ALLOY[m].into])}`);
|
||||
if (m === MFE) out.push('turns to steel with air blown through it');
|
||||
if (m === SNOW || m === ICE) out.push('chills water, and freezes it in cold air');
|
||||
if (POROUS[m] && !d.ign) out.push('lets air through to a fire');
|
||||
if (d.smelt && d.ph === PH_POWDER) out.push('smothers a fire under it');
|
||||
if (m === ACORN) out.push('sprouts on dirt or grass under open sky');
|
||||
if ((d.ph === PH_POWDER || d.ph === PH_ITEM) && m !== SNOW) out.push(DENS[m] < 1 ? 'floats on water' : 'sinks in water');
|
||||
if (m === SNOW) out.push('floats on water');
|
||||
if (forNotebook && d.ph === PH_SOLID && d.yld && d.yld !== m && MD[d.yld]) out.push(`drills into ${nm(d.yld)}`);
|
||||
return out;
|
||||
}
|
||||
// What is under the cursor: t1 is the name, t2 the details, warn a short problem
|
||||
// (too hard, out of reach) for the label next to the cursor.
|
||||
function hoverInfo(ax, ay) {
|
||||
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
|
||||
const i = ay * W + ax, m = mat[i];
|
||||
let warn = '';
|
||||
let t1 = MD[m].name, t2 = [];
|
||||
const p = part[i], id = mach[i];
|
||||
const b = blockP[((ay / BS) | 0) * BW + ((ax / BS) | 0)];
|
||||
const bar = occ[i] ? ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H) : null;
|
||||
if (bar) { t1 = MD[bar.m].name; t2.push(fmtT(bar.T)); }
|
||||
else if (id && machines[id]) {
|
||||
const mm = machines[id], P = PIECE_BY_ID[mm.type]; t1 = P.name;
|
||||
if (mm.type === 'dock') { t2.push('sells what drops through its grate', `${orders.length} open ${orders.length === 1 ? 'order' : 'orders'}`); }
|
||||
if (mm.type === 'wheel' || mm.type === 'engine') t2.push(`making ${mm.out.toFixed(1)} power`);
|
||||
if (mm.type === 'fan') t2.push(mm.on ? 'on' : 'off · press F');
|
||||
if (mm.type === 'silo') t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it');
|
||||
if (mm.type === 'cutter') t2.push(mm.blocked ? 'stopped: clear the space under it' : tick - (mm.busyT || -999) < 30 ? 'cutting' : 'idle: drop stone rubble in', `${(stats.bricks || 0).toLocaleString('en-US')} bricks cut`);
|
||||
if (mm.type === 'refiner') t2.push(mm.blocked ? 'stopped: clear the space under its chute' : tick - (mm.busyT || -999) < 30 ? 'grinding' : 'idle: drop rubble, gravel, sand or dirt in', `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`);
|
||||
} else if (p === P_FILTER) {
|
||||
const fb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pr = filterParts(filterInfo(fb)), ar = blockV[fb] & 128 ? '→' : '←';
|
||||
t1 = 'Filter ' + ar; t2.push(`drops ${pr.drops}`, `carries ${pr.rides} ${ar}`, 'F to change · I shows all');
|
||||
} else if (p === P_PIPE) {
|
||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
|
||||
t1 = 'Pipe' + (m !== EMPTY ? ` · ${MD[m].name} flowing past` : '');
|
||||
t2.push(pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`);
|
||||
} else if (p) t1 = ({ 1: 'Screw tube ↑', 2: 'Screw tube ↓', 3: 'Screw tube ←', 4: 'Screw tube →', 5: 'Grate', 6: 'Gate (open)', 7: 'Water wheel', 8: 'Engine intake' })[p] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
|
||||
else if (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
|
||||
else if (m === PIPE && isPipeCode(b)) {
|
||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←'];
|
||||
t1 = b === BP_PUMP ? `Pump ${arrows[v & 3]}` : b === BP_OUTLET ? `Outlet ${['↓', '←', '↑', '→'][v & 3]}` : 'Pipe';
|
||||
t2.push(pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`);
|
||||
if (b === BP_PUMP) t2.push(v & 4 ? 'off · press F' : 'on · F turns it off');
|
||||
if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it');
|
||||
}
|
||||
else if (b === BP_WALL && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} block`;
|
||||
const n = nets[netId[i]];
|
||||
if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`);
|
||||
if (player.lens && !bar) t2.push(fmtT(temp[i]));
|
||||
if (!bar && m !== EMPTY && PH[m] === PH_SOLID && !PARTMAT[m] && (flags[i] & F_NAT)) {
|
||||
const ok = HARD[m] <= BIT_TIERS[player.bitTier].hard;
|
||||
t2.push(HARD[m] > 6 ? 'cannot be drilled' : ok ? `drillable (hardness ${HARD[m]})` : `too hard (${HARD[m]})`);
|
||||
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
|
||||
}
|
||||
if (!bar && PH[m] === PH_POWDER) t2.push(GAME.collect.has(m) ? 'loose · ticked' : 'loose');
|
||||
if (!bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
|
||||
if (flags[i] & F_BURN) t2.push('burning');
|
||||
const mo = moldAt(ax, ay);
|
||||
if (mo) {
|
||||
t1 = MOLDS[mo.kind].name;
|
||||
t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set · press F` : `${MD[mo.metal].name} casting set · press F`) : mo.state === 'flawed' || mo.state === 'short' ? mo.flaw : mo.state === 'filling' ? `filling ${Math.round(mo.fill * 100)}%` : 'empty'];
|
||||
if (player.lens) t2.push(fmtT(temp[i]));
|
||||
}
|
||||
const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay);
|
||||
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
|
||||
// what it does, unless the cursor is on a machine or mold, which have their own details
|
||||
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
|
||||
const t3 = what ? reactionsOf(what) : [];
|
||||
return { t1, t2, t3, warn, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
|
||||
}
|
||||
function renderInfo() {
|
||||
const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
|
||||
const box = $('info');
|
||||
const h = `${r.t1 ? `<span class="t1">${esc(r.t1)}</span>` : ''}${r.t2.length ? `<span class="t2">${esc(r.t2.join(' · '))}</span>` : ''}${r.t3 && r.t3.length ? `<span class="t3">${esc(r.t3.join(' · '))}</span>` : ''}`;
|
||||
if (box.innerHTML !== h) box.innerHTML = h;
|
||||
}
|
||||
// Name of what is under the cursor, drawn right next to it
|
||||
function drawCursorLabel(px, py, r) {
|
||||
if (!SET.cursorLabel || (!GAME.mouse.in && !GAME.usingPad)) return;
|
||||
const info = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
|
||||
if (!info.t1 || info.air) return;
|
||||
const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth;
|
||||
const fs = Math.round(12 * dpr * SET.uiScale);
|
||||
if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif';
|
||||
const main = info.t1 + (player.lens && R.lens ? '' : '');
|
||||
const sub = info.warn || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : '');
|
||||
ctx.save();
|
||||
ctx.font = `600 ${fs}px ${R.uiFont}`;
|
||||
const w1 = ctx.measureText(main).width;
|
||||
ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`;
|
||||
const w2 = sub ? ctx.measureText(sub).width + fs * 0.6 : 0;
|
||||
const padX = fs * 0.5, h = fs * 1.6;
|
||||
let x = px + (r + 1.6) * R.zp + 6 * dpr, y = py + (r + 1.2) * R.zp + 4 * dpr;
|
||||
if (x + w1 + w2 + padX * 2 > R.canvas.width) x = px - (r + 1.6) * R.zp - 6 * dpr - (w1 + w2 + padX * 2);
|
||||
if (y + h > R.canvas.height) y = py - (r + 1.2) * R.zp - h;
|
||||
ctx.fillStyle = 'rgba(20,17,15,0.82)';
|
||||
ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill();
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2);
|
||||
if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
|
||||
ctx.restore();
|
||||
}
|
||||
Reference in New Issue
Block a user