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
153 lines
11 KiB
JavaScript
153 lines
11 KiB
JavaScript
// Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world.
|
|
// ---------- Overlays ----------
|
|
function cellRect(x, y, w, h) { const p = worldToScreenPx(x, y); return [p.x, p.y, w * R.zp, h * R.zp]; }
|
|
// Yellow and black hazard stripes on both top corners of a hopper
|
|
function drawHazardRims(ctx, sx, sy, sw, zp) {
|
|
for (let k = 0; k < 2; k++) {
|
|
const rx = k ? sx + sw - 2 * zp : sx;
|
|
for (let j = 0; j < 4; j++) { ctx.fillStyle = j & 1 ? '#1d1b18' : '#e0b43c'; ctx.fillRect(rx, sy + j * zp * 0.5, 2 * zp, zp * 0.5); }
|
|
}
|
|
}
|
|
function drawMachines() {
|
|
const ctx = R.ctx, zp = R.zp;
|
|
for (const mm of machines) {
|
|
if (!mm) continue;
|
|
const [sx, sy, sw, sh] = cellRect(mm.x, mm.y, mm.w, mm.h);
|
|
if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sh < 0) continue;
|
|
ctx.save();
|
|
if (mm.type === 'dock') {
|
|
// hazard stripes along the hopper rim and a sign on a post
|
|
drawHazardRims(ctx, sx, sy, sw, zp);
|
|
// the grate in its floor
|
|
ctx.fillStyle = 'rgba(30,28,26,0.85)'; ctx.fillRect(sx + 2 * zp, sy + 13 * zp, 20 * zp, zp);
|
|
ctx.fillStyle = 'rgba(150,160,170,0.8)';
|
|
for (let k = 0; k < 20; k += 2) ctx.fillRect(sx + (2 + k) * zp, sy + 13 * zp, zp * 0.5, zp);
|
|
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
|
const fs = Math.max(9, Math.round(zp * 2.6));
|
|
ctx.font = `700 ${fs}px ${R.monoFont}`;
|
|
const label = 'TRADE DOCK', tw = ctx.measureText(label).width + fs;
|
|
const px = sx + zp * 0.5, py = sy - zp * 7;
|
|
ctx.fillStyle = '#4a4036'; ctx.fillRect(px + zp * 0.5, py + fs * 1.4, zp * 0.8, sy - py - fs * 1.4);
|
|
ctx.fillStyle = '#2b4a3f'; ctx.fillRect(px, py, tw, fs * 1.5);
|
|
ctx.strokeStyle = '#e7dccb'; ctx.lineWidth = Math.max(1, zp * 0.2); ctx.strokeRect(px + 1, py + 1, tw - 2, fs * 1.5 - 2);
|
|
ctx.fillStyle = '#efe6d8'; ctx.textBaseline = 'middle'; ctx.fillText(label, px + fs * 0.5, py + fs * 0.78);
|
|
} else if (mm.type === 'wheel') {
|
|
const cx = sx + sw / 2, cy = sy + sh / 2, rad = sw / 2 - zp * 0.3;
|
|
ctx.strokeStyle = '#8a6a44'; ctx.lineWidth = Math.max(1, zp * 0.7);
|
|
ctx.beginPath(); ctx.arc(cx, cy, rad, 0, Math.PI * 2); ctx.stroke();
|
|
ctx.lineWidth = Math.max(1, zp * 0.5);
|
|
for (let k = 0; k < 8; k++) {
|
|
const a = mm.angle + k * Math.PI / 4;
|
|
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(a) * rad, cy + Math.sin(a) * rad); ctx.stroke();
|
|
ctx.fillStyle = '#6e5232';
|
|
ctx.fillRect(cx + Math.cos(a) * rad - zp * 0.6, cy + Math.sin(a) * rad - zp * 0.6, zp * 1.2, zp * 1.2);
|
|
}
|
|
ctx.fillStyle = '#4a3a28'; ctx.beginPath(); ctx.arc(cx, cy, zp * 1.1, 0, Math.PI * 2); ctx.fill();
|
|
} else if (mm.type === 'fan') {
|
|
const cx = sx + sw / 2, cy = sy + sh / 2;
|
|
const net = nets[mm.net], sat = net ? net.sat : 0;
|
|
ctx.fillStyle = 'rgba(20,22,26,0.55)'; ctx.beginPath(); ctx.arc(cx, cy, sw * 0.38, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = mm.on ? (sat > 0.05 ? '#b9c4cc' : '#7d868e') : '#5a5f66';
|
|
for (let k = 0; k < 3; k++) {
|
|
const a = mm.angle + k * Math.PI * 2 / 3;
|
|
ctx.beginPath(); ctx.ellipse(cx + Math.cos(a) * sw * 0.18, cy + Math.sin(a) * sw * 0.18, sw * 0.17, sw * 0.07, a, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
// direction arrow
|
|
const dx = [1, 0, -1, 0][mm.dir], dy = [0, 1, 0, -1][mm.dir];
|
|
ctx.strokeStyle = sat > 0.05 && mm.on ? 'rgba(160,220,255,0.7)' : 'rgba(160,170,180,0.35)'; ctx.lineWidth = Math.max(1, zp * 0.35);
|
|
ctx.beginPath(); ctx.moveTo(cx + dx * sw * 0.55, cy + dy * sh * 0.55); ctx.lineTo(cx + dx * sw * 0.9, cy + dy * sh * 0.9); ctx.stroke();
|
|
} else if (mm.type === 'silo') {
|
|
drawSilo(ctx, mm, sx, sy, zp);
|
|
} else if (mm.type === 'cutter') {
|
|
// hazard rims on the hopper, and a toothed saw blade in the floor that spins while it cuts
|
|
const busy = tick - (mm.busyT || -99) < 12;
|
|
drawHazardRims(ctx, sx, sy, sw, zp);
|
|
const cx = sx + 4 * zp, cy = sy + 6.5 * zp, rr = zp * 1.4, a0 = busy ? R.frame * 0.35 : 0;
|
|
ctx.fillStyle = '#c9cfd4'; ctx.beginPath();
|
|
for (let t = 0; t < 16; t++) { const a = a0 + t * Math.PI / 8, r2 = t & 1 ? rr : rr * 0.78; ctx.lineTo(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2); }
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#3a4048'; ctx.beginPath(); ctx.arc(cx, cy, rr * 0.3, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = mm.blocked ? '#c4553f' : busy ? '#e0b43c' : '#4a5058';
|
|
ctx.fillRect(sx + 2 * zp, sy + 7.6 * zp, 4 * zp, zp * 0.4);
|
|
} else if (mm.type === 'refiner') {
|
|
// hopper rim, two toothed grinding rollers in the floor (they turn while it works), the chute
|
|
const busy = tick - (mm.busyT || -99) < 12;
|
|
drawHazardRims(ctx, sx, sy, sw, zp);
|
|
for (let k = 0; k < 2; k++) {
|
|
const cx = sx + (k ? 7.5 : 4.5) * zp, cy = sy + 11 * zp, rr = zp * 1.15, a0 = (busy ? R.frame * 0.25 : 0) * (k ? -1 : 1);
|
|
ctx.fillStyle = '#2c3036'; ctx.beginPath(); ctx.arc(cx, cy, rr, 0, Math.PI * 2); ctx.fill();
|
|
ctx.strokeStyle = '#9aa4ae'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
|
ctx.beginPath(); for (let t = 0; t < 6; t++) { const a = a0 + t * Math.PI / 3; ctx.moveTo(cx + Math.cos(a) * rr * 0.35, cy + Math.sin(a) * rr * 0.35); ctx.lineTo(cx + Math.cos(a) * rr, cy + Math.sin(a) * rr); } ctx.stroke();
|
|
}
|
|
ctx.fillStyle = mm.blocked ? '#c4553f' : busy ? '#e0b43c' : '#4a5058';
|
|
ctx.beginPath(); ctx.moveTo(sx + 4 * zp, sy + 12 * zp); ctx.lineTo(sx + 8 * zp, sy + 12 * zp); ctx.lineTo(sx + 7 * zp, sy + 12.8 * zp); ctx.lineTo(sx + 5 * zp, sy + 12.8 * zp); ctx.closePath(); ctx.fill();
|
|
} else if (mm.type === 'engine') {
|
|
ctx.fillStyle = 'rgba(16,18,22,0.4)'; ctx.fillRect(sx + zp, sy + zp, sw - 2 * zp, sh - 3 * zp);
|
|
const fx = sx + sw * (mm.dir ? 0.3 : 0.7), fy = sy + sh * 0.42, fr = sh * 0.3;
|
|
ctx.strokeStyle = '#c3a06a'; ctx.lineWidth = Math.max(1, zp * 0.5);
|
|
ctx.beginPath(); ctx.arc(fx, fy, fr, 0, Math.PI * 2); ctx.stroke();
|
|
const ax = fx + Math.cos(mm.angle) * fr * 0.8, ay = fy + Math.sin(mm.angle) * fr * 0.8;
|
|
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(sx + sw * (mm.dir ? 0.75 : 0.25), fy); ctx.stroke();
|
|
ctx.fillStyle = '#d6b27a'; ctx.fillRect(sx + sw * (mm.dir ? 0.75 : 0.25) - zp, fy - zp * 0.8, zp * 2, zp * 1.6);
|
|
if (mm.out > 0.3) { ctx.fillStyle = 'rgba(230,236,244,0.5)'; ctx.fillRect(mm.dir ? sx - zp * 1.5 : sx + sw + zp * 0.5, sy + sh - zp * (1 + (R.frame >> 3) % 2), zp, zp); }
|
|
}
|
|
ctx.restore();
|
|
}
|
|
}
|
|
// The silo: legs and braces, a cone to a slide gate, a ribbed steel drum with a ladder and a sight
|
|
// glass that shows how full it is, a roof with the loading hatch. All in grain units from its corner.
|
|
function drawSilo(ctx, mm, sx, sy, zp) {
|
|
const X = u => sx + u * zp, Y = u => sy + u * zp, full = Math.min(1, (mm.sn || 0) / SILO_CAP);
|
|
const steel = (x0, x1) => { const g = ctx.createLinearGradient(X(x0), 0, X(x1), 0); g.addColorStop(0, '#3b4249'); g.addColorStop(0.22, '#a9b3bb'); g.addColorStop(0.5, '#87919a'); g.addColorStop(1, '#353c43'); return g; };
|
|
ctx.save();
|
|
// legs, with short struts to the cone
|
|
ctx.fillStyle = '#2f353b';
|
|
ctx.fillRect(X(0.15), Y(19.5), zp * 0.75, zp * 4.5); ctx.fillRect(X(11.1), Y(19.5), zp * 0.75, zp * 4.5);
|
|
ctx.strokeStyle = '#2f353b'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
|
ctx.beginPath(); ctx.moveTo(X(0.8), Y(23)); ctx.lineTo(X(2.6), Y(21)); ctx.moveTo(X(11.2), Y(23)); ctx.lineTo(X(9.4), Y(21)); ctx.stroke();
|
|
// the cone down to the gate
|
|
ctx.fillStyle = steel(1, 11); ctx.beginPath(); ctx.moveTo(X(0.8), Y(19.8)); ctx.lineTo(X(11.2), Y(19.8)); ctx.lineTo(X(8.2), Y(23)); ctx.lineTo(X(3.8), Y(23)); ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.35)'; ctx.lineWidth = Math.max(1, zp * 0.12); ctx.stroke();
|
|
// the slide gate: across the opening when shut, pulled aside when open
|
|
ctx.fillStyle = '#1c1f23'; ctx.fillRect(X(4), Y(23), zp * 4, zp);
|
|
ctx.fillStyle = mm.open ? '#c99a3c' : '#6b737b';
|
|
if (mm.open) ctx.fillRect(X(7.6), Y(23.15), zp * 2.4, zp * 0.6); else ctx.fillRect(X(3.6), Y(23.15), zp * 4.8, zp * 0.6);
|
|
// the drum
|
|
ctx.fillStyle = steel(0.5, 11.5); ctx.fillRect(X(0.5), Y(1.8), zp * 11, zp * 18.2);
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.28)'; ctx.lineWidth = Math.max(1, zp * 0.14);
|
|
for (let k = 1; k < 4; k++) { const yy = Y(1.8 + k * 4.55); ctx.beginPath(); ctx.moveTo(X(0.5), yy); ctx.lineTo(X(11.5), yy); ctx.stroke(); }
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.12)';
|
|
for (let k = 1; k < 6; k++) { const xx = X(0.5 + k * 11 / 6); ctx.beginPath(); ctx.moveTo(xx, Y(1.8)); ctx.lineTo(xx, Y(20)); ctx.stroke(); }
|
|
if (zp >= 4) { ctx.fillStyle = 'rgba(255,255,255,0.35)'; for (let k = 1; k < 4; k++) for (let j = 0; j < 8; j++) { ctx.beginPath(); ctx.arc(X(1 + j * 1.43), Y(1.8 + k * 4.55) - zp * 0.25, Math.max(0.6, zp * 0.09), 0, Math.PI * 2); ctx.fill(); } }
|
|
// a ladder up the left side
|
|
ctx.strokeStyle = '#2b3036'; ctx.lineWidth = Math.max(1, zp * 0.14);
|
|
ctx.beginPath(); ctx.moveTo(X(1.3), Y(2)); ctx.lineTo(X(1.3), Y(19.8)); ctx.moveTo(X(2.3), Y(2)); ctx.lineTo(X(2.3), Y(19.8));
|
|
for (let yy = 2.6; yy < 19.8; yy += 1.1) { ctx.moveTo(X(1.3), Y(yy)); ctx.lineTo(X(2.3), Y(yy)); }
|
|
ctx.stroke();
|
|
// the sight glass: fills from the bottom in the colour of what is inside
|
|
const gx = 9.1, gy0 = 3, gy1 = 18.8, gh = gy1 - gy0;
|
|
ctx.fillStyle = '#14181c'; ctx.fillRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
|
|
if (mm.sn > 0) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(gx + 0.15), Y(gy1 - gh * full), zp * 0.8, zp * gh * full); }
|
|
ctx.strokeStyle = '#c9d1d8'; ctx.lineWidth = Math.max(1, zp * 0.1);
|
|
ctx.strokeRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
|
|
for (let k = 1; k < 4; k++) { const yy = Y(gy1 - gh * k / 4); ctx.beginPath(); ctx.moveTo(X(gx - 0.35), yy); ctx.lineTo(X(gx), yy); ctx.stroke(); }
|
|
// what it holds, stencilled on the drum
|
|
if (zp >= 3) {
|
|
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
|
const fs = Math.max(8, Math.round(zp * 1.15));
|
|
ctx.font = `700 ${fs}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = 'rgba(20,24,28,0.75)';
|
|
ctx.fillText('SILO', X(5.6), Y(5));
|
|
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
|
|
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', X(5.6), Y(16.8));
|
|
if (mm.sn) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(4.4), Y(17.9), zp * 2.4, zp * 0.55); }
|
|
}
|
|
// roof with a rim, and the loading hatch (its lid stands open)
|
|
ctx.fillStyle = '#5b646c'; ctx.fillRect(X(0.2), Y(1.2), zp * 11.6, zp * 0.8);
|
|
ctx.fillStyle = '#7d878f'; ctx.fillRect(X(0.6), Y(0.6), zp * 2.4, zp * 0.7); ctx.fillRect(X(9), Y(0.6), zp * 2.4, zp * 0.7);
|
|
ctx.fillStyle = '#15181b'; ctx.fillRect(X(3), Y(0.9), zp * 6, zp * 1.1);
|
|
ctx.strokeStyle = tick - (mm.fillT || -99) < 10 ? '#e0b43c' : '#9aa4ae'; ctx.lineWidth = Math.max(1, zp * 0.3);
|
|
ctx.beginPath(); ctx.moveTo(X(8.9), Y(0.9)); ctx.lineTo(X(10.2), Y(-1.4)); ctx.stroke();
|
|
ctx.restore();
|
|
}
|