128 lines
8.1 KiB
JavaScript
128 lines
8.1 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.loading) { acc = 0; return; }
|
||
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 (SET.cameraLead && GAME.usingPad) { lookX = GAME.padAim.x * 0.5; lookY = GAME.padAim.y * 0.5; }
|
||
else if (SET.cameraLead && 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.replace !== GAME.replaceShown) { GAME.replaceShown = GAME.replace; 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 = async () => {
|
||
auInit(); playSfx('ui'); $('newworld').hidden = !$('newworld').hidden;
|
||
if ($('newworld').hidden) return;
|
||
try { $('nw-name').value = `World ${(await listWorlds()).length + 1}`; }
|
||
catch (e) { $('nw-name').value = 'World 1'; }
|
||
$('nw-name').focus(); $('nw-name').select();
|
||
};
|
||
for (const id of ['nw-name', 'nw-seed']) $(id).onkeydown = e => { if (e.key === 'Enter') $('btn-generate').click(); };
|
||
$('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, $('nw-name').value);
|
||
};
|
||
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); await loadSavedWorld(); };
|
||
$('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;
|
||
$('title').hidden = true;
|
||
migrateWorlds().then(() => refreshContinue()).then(async () => {
|
||
$('title').hidden = false; if (!TITLE.raf) drawTitle();
|
||
if (hotData && hotData.resume) { await loadSavedWorld(); }
|
||
}).catch(() => { $('title').hidden = false; if (!TITLE.raf) drawTitle(); });
|
||
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) || {});
|