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:
@@ -0,0 +1,230 @@
|
||||
// Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
||||
function spawnFloater(x, y, text) { R.floaters = R.floaters || []; if (R.floaters.length < 30) R.floaters.push({ x, y, text, life: 70 }); }
|
||||
// Rain as thin falling streaks, snow as soft flakes
|
||||
function drawRain() {
|
||||
if (!flyers.length) return;
|
||||
const ctx = R.ctx, zp = R.zp, v = viewCells();
|
||||
const x0 = R.camX - 2, y0 = R.camY - 4, x1 = R.camX + v.w + 2, y1 = R.camY + v.h + 2;
|
||||
ctx.save();
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = 'rgba(176,204,236,0.5)'; ctx.lineWidth = Math.max(1, zp * 0.22);
|
||||
ctx.beginPath();
|
||||
let snow = false;
|
||||
for (const f of flyers) {
|
||||
if (!f.rain || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
||||
if (f.m !== WATER) { snow = true; continue; }
|
||||
const a = worldToScreenPx(f.x, f.y), len = Math.max(1.5, Math.hypot(f.vx, f.vy) * 3.2);
|
||||
ctx.moveTo(a.x, a.y); ctx.lineTo(a.x - f.vx / Math.max(0.2, f.vy) * len * zp * 0.5, a.y - len * zp);
|
||||
}
|
||||
ctx.stroke();
|
||||
if (snow) {
|
||||
ctx.fillStyle = 'rgba(244,247,252,0.9)';
|
||||
for (const f of flyers) {
|
||||
if (!f.rain || f.m === WATER || f.x < x0 || f.x > x1 || f.y < y0 || f.y > y1) continue;
|
||||
const a = worldToScreenPx(f.x + Math.sin(f.age * 0.07 + f.x) * 0.6, f.y), r = Math.max(1, zp * 0.42);
|
||||
ctx.beginPath(); ctx.arc(a.x, a.y, r, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
// Info overlay: a small arrow on every belt and screw tube block on screen, the way it moves things
|
||||
function drawFlowArrows() {
|
||||
const ctx = R.ctx, zp = R.zp, v = viewCells();
|
||||
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
||||
const by0 = Math.max(0, Math.floor(R.camY / BS)), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS));
|
||||
const s = BS * zp, r = Math.max(3, s * 0.22);
|
||||
ctx.save();
|
||||
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||
const b = by * BW + bx, code = blockP[b];
|
||||
if (code !== BP_BELT && code !== BP_TUBE && code !== BP_PUMP && code !== BP_OUTLET) continue;
|
||||
const vv = blockV[b];
|
||||
const [dx, dy] = code === BP_BELT ? (vv ? [1, 0] : [-1, 0]) : code === BP_PUMP ? PUMP_V[vv & 3] : code === BP_OUTLET ? OUTLET_V[vv & 3] : [[0, -1], [1, 0], [0, 1], [-1, 0]][vv & 3];
|
||||
const c = worldToScreenPx(bx * BS + BS / 2, by * BS + BS / 2);
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.55)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.35, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = code === BP_TUBE ? '#ffc58a' : '#e7dccb'; ctx.lineWidth = Math.max(1.2, r * 0.32);
|
||||
// a chevron pointing (dx, dy)
|
||||
const px = -dy, py = dx;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c.x - dx * r * 0.45 + px * r * 0.7, c.y - dy * r * 0.45 + py * r * 0.7);
|
||||
ctx.lineTo(c.x + dx * r * 0.55, c.y + dy * r * 0.55);
|
||||
ctx.lineTo(c.x - dx * r * 0.45 - px * r * 0.7, c.y - dy * r * 0.45 - py * r * 0.7);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
// Outline a run of blocks: strong while it is being edited, faint when pointed at
|
||||
function drawRunOutline(run, strong) {
|
||||
if (!run || !run.length) return;
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
const x0 = (run[0] % BW) * BS, x1 = (run[run.length - 1] % BW + 1) * BS, y = ((run[0] / BW) | 0) * BS;
|
||||
const a = worldToScreenPx(x0, y), b = worldToScreenPx(x1, y + BS);
|
||||
ctx.save();
|
||||
ctx.strokeStyle = strong ? 'rgba(255,190,90,0.95)' : 'rgba(236,226,210,0.45)'; ctx.lineWidth = Math.max(1, zp * (strong ? 0.5 : 0.25));
|
||||
if (!strong) ctx.setLineDash([zp, zp * 0.7]);
|
||||
ctx.strokeRect(a.x - zp * 0.3, a.y - zp * 0.3, b.x - a.x + zp * 0.6, b.y - a.y + zp * 0.6);
|
||||
ctx.restore();
|
||||
}
|
||||
// Tags over filters: what falls through (↓) and what rides along, and which way.
|
||||
// showAll tags every run of matching filters on screen; otherwise only the run under hoverB.
|
||||
function drawFilterTags(showAll, hoverB) {
|
||||
if (!filterCfg.size || (!showAll && hoverB < 0)) return;
|
||||
const ctx = R.ctx, u = Math.max(1, R.canvas.width / window.innerWidth);
|
||||
const v = viewCells();
|
||||
const bx0 = Math.max(0, Math.floor(R.camX / BS) - 1), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS) + 1);
|
||||
const by0 = Math.max(0, Math.floor(R.camY / BS) - 1), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS) + 1);
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
ctx.save();
|
||||
ctx.font = `600 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle';
|
||||
const sw = Math.round(9 * u), gap = Math.round(3 * u), pad = Math.round(5 * u), th = Math.round(17 * u);
|
||||
const runs = showAll ? [] : [filterRun(hoverB)];
|
||||
if (showAll) for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||
const b = by * BW + bx;
|
||||
if (blockP[b] !== BP_FILTER) continue;
|
||||
const run = filterRun(b);
|
||||
if (run[0] === b || bx === bx0) runs.push(run);
|
||||
bx = Math.max(bx, (run[run.length - 1] % BW));
|
||||
}
|
||||
for (const run of runs) {
|
||||
if (!run.length) continue;
|
||||
const b = run[0], c = filterCfg.get(b), ms = c ? c.m : [], inv = c ? c.inv : 0, right = blockV[b] & 128;
|
||||
// segment contents: swatches, or a word when it is "the rest"
|
||||
const segs = [
|
||||
{ icon: '↓', col: '#7fd6c2', mats: inv ? null : ms, word: inv ? (ms.length ? 'rest' : 'all') : 'none' },
|
||||
{ icon: right ? '→' : '←', col: '#ffd27a', mats: inv ? ms : null, word: inv ? 'none' : (ms.length ? 'rest' : 'all') },
|
||||
];
|
||||
let w = pad;
|
||||
for (const sg of segs) {
|
||||
sg.iw = ctx.measureText(sg.icon).width;
|
||||
sg.show = sg.mats && sg.mats.length ? sg.mats.slice(0, 5) : null;
|
||||
sg.more = sg.mats && sg.mats.length > 5 ? `+${sg.mats.length - 5}` : '';
|
||||
sg.cw = sg.show ? sg.show.length * (sw + 2) - 2 + (sg.more ? gap + ctx.measureText(sg.more).width : 0) : ctx.measureText(sg.word).width;
|
||||
w += sg.iw + gap + sg.cw + pad;
|
||||
}
|
||||
w += gap;
|
||||
const x0 = (run[0] % BW) * BS, x1 = (run[run.length - 1] % BW + 1) * BS, y = ((b / BW) | 0) * BS;
|
||||
const p = worldToScreenPx((x0 + x1) / 2, y);
|
||||
let tx = Math.round(p.x - w / 2), ty = Math.round(p.y - th - 4 * u);
|
||||
if (tx + w < 0 || tx > R.canvas.width || ty + th < 0 || ty > R.canvas.height) continue;
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.86)'; ctx.fillRect(tx, ty, w, th);
|
||||
ctx.strokeStyle = 'rgba(236,226,210,0.22)'; ctx.lineWidth = u; ctx.strokeRect(tx + u / 2, ty + u / 2, w - u, th - u);
|
||||
let cx = tx + pad;
|
||||
segs.forEach((sg, k) => {
|
||||
if (k) { ctx.fillStyle = 'rgba(236,226,210,0.25)'; ctx.fillRect(cx - pad / 2 - gap / 2, ty + 3 * u, u, th - 6 * u); cx += gap; }
|
||||
ctx.fillStyle = sg.col; ctx.fillText(sg.icon, cx, ty + th / 2 + u * 0.5); cx += sg.iw + gap;
|
||||
if (sg.show) {
|
||||
for (const m of sg.show) { ctx.fillStyle = `rgb(${COLR[m]},${COLG[m]},${COLB[m]})`; ctx.fillRect(cx, ty + (th - sw) / 2, sw, sw); ctx.strokeStyle = 'rgba(0,0,0,0.55)'; ctx.strokeRect(cx + 0.5, ty + (th - sw) / 2 + 0.5, sw - 1, sw - 1); cx += sw + 2; }
|
||||
cx -= 2;
|
||||
if (sg.more) { cx += gap; ctx.fillStyle = '#e7dccb'; ctx.fillText(sg.more, cx, ty + th / 2 + u * 0.5); cx += ctx.measureText(sg.more).width; }
|
||||
} else { ctx.fillStyle = 'rgba(231,220,203,0.85)'; ctx.fillText(sg.word, cx, ty + th / 2 + u * 0.5); cx += sg.cw; }
|
||||
cx += pad;
|
||||
});
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawFloaters() {
|
||||
const fl = R.floaters; if (!fl || !fl.length) return;
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
ctx.save();
|
||||
ctx.font = `700 ${Math.max(11, Math.round(zp * 3))}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||
for (let k = fl.length - 1; k >= 0; k--) {
|
||||
const f = fl[k]; f.life--; f.y -= 0.12;
|
||||
if (f.life <= 0) { fl.splice(k, 1); continue; }
|
||||
const s = worldToScreenPx(f.x, f.y), a = Math.min(1, f.life / 25);
|
||||
ctx.fillStyle = `rgba(14,12,11,${0.6 * a})`; ctx.fillText(f.text, s.x + 1, s.y + 1);
|
||||
ctx.fillStyle = `rgba(255,210,122,${a})`; ctx.fillText(f.text, s.x, s.y);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawMolds(hoverMold) {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
ctx.save();
|
||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||
ctx.font = `600 ${Math.max(10, Math.round(zp * 3.4))}px ${R.monoFont}`;
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||
for (const mo of molds) {
|
||||
const [sx, sy, sw, sh] = cellRect(mo.x, mo.y, mo.w, mo.h);
|
||||
if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sh < 0) continue;
|
||||
const ready = mo.state === 'ready', flawed = mo.state === 'flawed';
|
||||
ctx.setLineDash([zp, zp]);
|
||||
ctx.strokeStyle = ready ? 'rgba(255,200,90,0.95)' : flawed ? 'rgba(230,90,70,0.8)' : 'rgba(200,190,170,0.4)';
|
||||
ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp);
|
||||
ctx.setLineDash([]);
|
||||
if (ready || flawed || mo === hoverMold || mo.state === 'filling' || mo.state === 'short') {
|
||||
const label = ready ? `${mo.kind === 'ingot' ? (mo.ready.length === 1 ? 'Bar' : `${mo.ready.length} bars`) : mo.kind === 'bit' ? 'Bit' : 'Tank'} set · F` : flawed ? 'Flawed casting' : mo.state === 'short' ? 'Needs more metal' : mo.state === 'filling' ? `Filling ${Math.round(mo.fill * 100)}%` : MOLDS[mo.kind].name;
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.75)';
|
||||
const tw = ctx.measureText(label).width + zp * 2;
|
||||
ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp * 5.2, tw, zp * 4.2);
|
||||
ctx.fillStyle = ready ? '#ffcf7a' : flawed ? '#ff8a72' : '#e7dccb';
|
||||
ctx.fillText(label, sx + sw / 2, sy - zp * 1.4);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawBot() {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
const [sx, sy, sw, sh] = cellRect(bot.x, bot.y, bot.w, bot.h);
|
||||
const cx = sx + sw / 2, cy = sy + sh / 2;
|
||||
const bob = Math.sin(R.frame * 0.12) * zp * 0.25;
|
||||
ctx.save();
|
||||
// thruster glow
|
||||
if (bot.thrust > 0.1) {
|
||||
const fl = (0.6 + Math.random() * 0.4) * bot.thrust;
|
||||
const grd = ctx.createRadialGradient(cx, sy + sh + bob, 0, cx, sy + sh + bob, zp * 3.2 * fl);
|
||||
grd.addColorStop(0, 'rgba(255,214,140,0.9)'); grd.addColorStop(1, 'rgba(255,120,40,0)');
|
||||
ctx.fillStyle = grd; ctx.fillRect(cx - zp * 4, sy + sh - zp + bob, zp * 8, zp * 5);
|
||||
}
|
||||
// body
|
||||
ctx.fillStyle = '#c8bfb2';
|
||||
ctx.beginPath(); ctx.roundRect(sx + zp * 0.2, sy + zp * 0.6 + bob, sw - zp * 0.4, sh - zp * 1.4, zp * 1.4); ctx.fill();
|
||||
ctx.fillStyle = '#8c8378';
|
||||
ctx.fillRect(sx + zp * 0.2, sy + sh - zp * 1.6 + bob, sw - zp * 0.4, zp * 0.8);
|
||||
// visor
|
||||
ctx.fillStyle = '#1b1715';
|
||||
const vx = cx + bot.face * zp * 0.5;
|
||||
ctx.beginPath(); ctx.roundRect(vx - zp * 1.5, sy + zp * 1.4 + bob, zp * 3, zp * 1.6, zp * 0.6); ctx.fill();
|
||||
ctx.fillStyle = bot.lamp ? '#ffb45a' : '#6b5a48';
|
||||
ctx.fillRect(vx + bot.face * zp * 0.5 - zp * 0.45, sy + zp * 1.8 + bob, zp * 0.9, zp * 0.8);
|
||||
// antenna
|
||||
ctx.strokeStyle = '#8c8378'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||
ctx.beginPath(); ctx.moveTo(cx - bot.face * zp, sy + zp * 0.6 + bob); ctx.lineTo(cx - bot.face * zp * 1.6, sy - zp * 0.6 + bob); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
function spawnDebris(x, y, m) {
|
||||
if (!SET.particles || R.particles.length > 400) return;
|
||||
for (let k = 0; k < 2; k++) R.particles.push({ x: x + Math.random(), y: y + Math.random(), vx: (Math.random() - 0.5) * 0.5, vy: -Math.random() * 0.5, life: 20 + Math.random() * 20, r: COLR[m], g: COLG[m], b: COLB[m] });
|
||||
}
|
||||
function spawnSpark(x, y) {
|
||||
if (!SET.particles || R.particles.length > 400) return;
|
||||
R.particles.push({ x, y, vx: (Math.random() - 0.5) * 0.9, vy: -Math.random() * 0.8, life: 10 + Math.random() * 12, r: 255, g: 190 + Math.random() * 60, b: 90, spark: true });
|
||||
}
|
||||
// grains flying into the vacuum nozzle
|
||||
function spawnSuck(x, y, m) {
|
||||
if (!SET.particles || R.particles.length > 500) return;
|
||||
const v = (Math.random() - 0.5) * 2 * CVAR[m];
|
||||
R.particles.push({ x: x + 0.5, y: y + 0.5, vx: 0, vy: 0, life: 40, r: COLR[m] + v, g: COLG[m] + v, b: COLB[m] + v, suck: true });
|
||||
}
|
||||
function drawParticles() {
|
||||
const ctx = R.ctx, zp = R.zp;
|
||||
const ps = R.particles;
|
||||
const tx = bot.x + bot.w / 2, ty = bot.y + bot.h / 2;
|
||||
for (let k = ps.length - 1; k >= 0; k--) {
|
||||
const p = ps[k];
|
||||
if (p.suck) {
|
||||
const dx = tx - p.x, dy = ty - p.y, d = Math.hypot(dx, dy);
|
||||
if (d < 2.5) { ps.splice(k, 1); continue; }
|
||||
const sp = Math.min(d, 0.6 + (40 - p.life) * 0.28);
|
||||
p.x += dx / d * sp; p.y += dy / d * sp; p.life--;
|
||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||
} else {
|
||||
p.x += p.vx; p.y += p.vy; p.vy += 0.04; p.life--;
|
||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||
}
|
||||
const s = worldToScreenPx(p.x, p.y);
|
||||
ctx.fillStyle = `rgba(${p.r | 0},${p.g | 0},${p.b | 0},${Math.min(1, p.life / 10)})`;
|
||||
const sz = p.spark ? Math.max(1, zp * 0.5) : p.suck ? zp : Math.max(1, zp * 0.7);
|
||||
ctx.fillRect(s.x, s.y, sz, sz);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user