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
118 lines
7.6 KiB
JavaScript
118 lines
7.6 KiB
JavaScript
// The frame loop and boot.
|
||
// ---------- Main loop ----------
|
||
let lastT = performance.now(), acc = 0, lastRender = 0, frameMs = 0, simMs = 0, fpsSm = 60;
|
||
function scanVisible() {
|
||
let fire = 0, power = 0;
|
||
const x0 = Math.max(0, R.vx0), y0 = Math.max(0, R.vy0), x1 = Math.min(W, R.vx0 + R.bw), y1 = Math.min(H, R.vy0 + R.bh);
|
||
for (let y = y0; y < y1; y += 2) for (let x = x0; x < x1; x += 2) { const i = y * W + x; if (mat[i] === FIRE || (flags[i] & F_BURN)) fire++; }
|
||
for (const mm of machines) if (mm && mm.x + mm.w > x0 && mm.x < x1 && mm.y + mm.h > y0 && mm.y < y1) power += mm.type === 'fan' ? (nets[mm.net] ? nets[mm.net].sat * 6 : 0) : mm.out;
|
||
for (let n = 1; n < nets.length; n++) power += nets[n].demand * nets[n].sat * 0.2;
|
||
const cx = Math.max(0, Math.min(W - 1, Math.floor(bot.x)));
|
||
return { fire: fire * 4, power, surface: bot.y < surfY[cx] + 4, rain: R.rain || 0 };
|
||
}
|
||
let ambientT = 0;
|
||
function frame(now) {
|
||
requestAnimationFrame(frame);
|
||
const dtReal = Math.min(0.25, (now - lastT) / 1000); lastT = now;
|
||
if (SET.fpsCap && now - lastRender < 1000 / SET.fpsCap - 1) return;
|
||
const dt = Math.min(0.1, (now - (lastRender || now)) / 1000) || 1 / 60; lastRender = now;
|
||
if (!GAME.running) { pollPad(); return; }
|
||
const t0 = performance.now();
|
||
const pad = pollPad();
|
||
if (GAME.usingPad) { GAME.aim.x = bot.x + bot.w / 2 + GAME.padAim.x; GAME.aim.y = bot.y + bot.h / 2 + GAME.padAim.y; }
|
||
else { const w = screenToWorld(GAME.mouse.x, GAME.mouse.y); GAME.aim.x = w.x; GAME.aim.y = w.y; }
|
||
if (!GAME.paused) {
|
||
acc += dtReal * SET.simSpeed;
|
||
let steps = 0;
|
||
const step = 1 / 60;
|
||
while (acc >= step && steps < SET.maxSteps) {
|
||
let ix = 0, iy = 0;
|
||
if (keys.has('a') || keys.has('ArrowLeft')) ix -= 1; if (keys.has('d') || keys.has('ArrowRight')) ix += 1;
|
||
if (keys.has('w') || keys.has('ArrowUp') || keys.has(' ')) iy -= 1; if (keys.has('s') || keys.has('ArrowDown')) iy += 1;
|
||
ix += pad.mx; iy += pad.my;
|
||
const mag = Math.hypot(ix, iy); if (mag > 1) { ix /= mag; iy /= mag; }
|
||
// Shift boosts flight with every tool; as a tool modifier it only matters when you click
|
||
const pm = Math.hypot(pad.mx, pad.my);
|
||
GAME.padRun = pm > 0.92 ? GAME.padRun + 1 : 0;
|
||
botStep(ix, iy, keys.has('Shift') || GAME.padRun > 24);
|
||
toolTick();
|
||
const s0 = performance.now(); simTick(); simMs = simMs * 0.9 + (performance.now() - s0) * 0.1;
|
||
acc -= step; steps++;
|
||
GAME.playTime += step;
|
||
}
|
||
if (steps >= SET.maxSteps) acc = 0;
|
||
if (SET.autosave && performance.now() - GAME.lastAutosave > SET.autosave * 60000) { GAME.lastAutosave = performance.now(); saveWorld('auto', true); }
|
||
} else acc = 0;
|
||
drainEvents();
|
||
let lookX = 0, lookY = 0;
|
||
if (GAME.usingPad) { lookX = GAME.padAim.x * 0.5; lookY = GAME.padAim.y * 0.5; }
|
||
else if (GAME.mouse.in && toolId() !== 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld()) { const dpr = R.canvas.width / window.innerWidth; lookX = (GAME.mouse.x - window.innerWidth / 2) * dpr / R.zp; lookY = (GAME.mouse.y - window.innerHeight / 2) * dpr / R.zp; }
|
||
// hold the camera's look-ahead still while dragging out a line, box or blueprint
|
||
// While dragging, the camera stays put unless the bot itself is flying, so the
|
||
// grid under the cursor does not slide around mid-line.
|
||
if (GAME.drag) { lookX = GAME.lookHold.x; lookY = GAME.lookHold.y; } else { GAME.lookHold.x = lookX; GAME.lookHold.y = lookY; }
|
||
if (!GAME.drag || Math.hypot(bot.vx, bot.vy) > 0.08) updateCamera(dt, lookX, lookY);
|
||
R.frame++;
|
||
drawWorld();
|
||
drawCracks();
|
||
if (!weather.seen && (R.rain || 0) > 0.12) {
|
||
weather.seen = true;
|
||
showCard({ kind: 'Weather', title: 'Rain', swatch: WATER, body: ['Steam that rises into the open sky gathers into clouds and comes back down as rain nearby. On freezing heights it falls as snow.', 'Water is never used up, only moved around. If some leaves the world (dropped into the dock, say), a shower from beyond the claim makes it up.'] });
|
||
}
|
||
if (SET.look === 'smooth' && SM.tried && !SM.ok) {
|
||
SET.look = 'pixel'; saveSettings();
|
||
toast(SM.why === 'slow' ? 'The smooth look was too slow on this computer, so the pixel look is on. You can try it again in Settings.' : SM.why === 'software' ? 'This browser is drawing without the graphics card, so the smooth look would crawl. The pixel look is on.' : 'This browser cannot draw the smooth look, so the pixel look is on.');
|
||
}
|
||
drawMachines();
|
||
drawMolds(moldAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)));
|
||
drawParticles();
|
||
const hb = GAME.mouse.in || GAME.usingPad ? filterBlockAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) : -1;
|
||
if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false);
|
||
if (GAME.infoOverlay) drawFlowArrows();
|
||
drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb);
|
||
drawFloaters();
|
||
drawRain();
|
||
drawBot();
|
||
if (!GAME.hidePreviews) drawPreviews(); // tools/shot.py hides them for clean screenshots
|
||
if (GAME.ctrl !== GAME.ctrlShown) { GAME.ctrlShown = GAME.ctrl; hudDirty = true; }
|
||
renderBuildBar();
|
||
if (hudDirty || R.frame % 6 === 0) { renderSelCard(); renderBag(); renderWallet(); hudDirty = false; }
|
||
if (R.frame % 4 === 0) renderInfo();
|
||
if (document.body.classList.contains('paused') !== GAME.paused) document.body.classList.toggle('paused', GAME.paused);
|
||
updateOverlayUI();
|
||
if (now > ambientT) { ambientT = now + 200; auAmbient(scanVisible()); }
|
||
frameMs = frameMs * 0.9 + (performance.now() - t0) * 0.1;
|
||
fpsSm = fpsSm * 0.95 + (1 / Math.max(0.001, dt)) * 0.05;
|
||
if (SET.showPerf && R.frame % 15 === 0) {
|
||
let aw = 0, ht = 0; for (let c = 0; c < NCH; c++) { if (awake[c]) aw++; if (hot[c]) ht++; }
|
||
$('perf').textContent = `${fpsSm.toFixed(0)} fps · frame ${frameMs.toFixed(1)} ms · sim ${simMs.toFixed(1)} ms/tick\nmoving areas ${aw} · hot areas ${ht} / ${NCH} · bars ${ents.length} · view ${R.bw}×${R.bh}`;
|
||
}
|
||
}
|
||
|
||
// ---------- Boot ----------
|
||
function boot(hotData) {
|
||
document.documentElement.style.setProperty('--ui', SET.uiScale);
|
||
initRender(canvas);
|
||
setupBuildBar();
|
||
buildTitleCells(); drawTitle();
|
||
$('nw-seed').value = String(Math.floor(Math.random() * 900000) + 100000);
|
||
$('wallet').onclick = () => { playSfx('ui'); openNotebook('Market'); };
|
||
$('btn-new').onclick = () => { auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden; if (!$('newworld').hidden) $('btn-generate').focus(); };
|
||
$('btn-generate').onclick = () => {
|
||
const seed = parseInt(String($('nw-seed').value).replace(/\D/g, ''), 10) || Math.floor(Math.random() * 1e6);
|
||
const [w, h] = $('nw-size').value.split('x').map(Number);
|
||
startNewWorld(seed, w, h);
|
||
};
|
||
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); const sv = await newestSave(); if (sv) startFromSave(sv); else toast('No saved world found.'); };
|
||
$('btn-title-settings').onclick = () => { auInit(); playSfx('ui'); openSettings('Display'); };
|
||
$('btn-title-help').onclick = () => { auInit(); playSfx('ui'); openNotebook('Controls'); };
|
||
const touchOnly = matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches;
|
||
$('touchnote').hidden = !touchOnly;
|
||
refreshContinue().then(async () => {
|
||
if (hotData && hotData.resume) { const sv = await newestSave(); if (sv) startFromSave(sv); }
|
||
});
|
||
requestAnimationFrame(frame);
|
||
}
|
||
try { if (window.claude && window.claude.hot && window.claude.hot.snapshot) window.claude.hot.snapshot(() => { if (GAME.running) saveWorld('auto', true); return { resume: GAME.running }; }); } catch (e) { /* not in a viewer */ }
|
||
if (window.claude && window.claude.hot && window.claude.hot.ready) window.claude.hot.ready(boot); else boot((window.claude && window.claude.hot && window.claude.hot.data) || {});
|