import { initWorld, onWindowResize, disposeWorld } from './world.js'; import { initAgents, updateAgents, getAgentCount, disposeAgents, getAgentStates, applyAgentStates, getTimmyGroup, applySlap, getCameraShakeStrength, } from './agents.js'; import { initEffects, updateEffects, disposeEffects } from './effects.js'; import { initUI, updateUI } from './ui.js'; import { initInteraction, disposeInteraction, registerSlapTarget } from './interaction.js'; import { initWebSocket, getConnectionState, getJobCount } from './websocket.js'; import { initPaymentPanel } from './payment.js'; import { initSessionPanel } from './session.js'; let running = false; let canvas = null; function buildWorld(firstInit, stateSnapshot) { const { scene, camera, renderer } = initWorld(canvas); canvas = renderer.domElement; initEffects(scene); initAgents(scene); if (stateSnapshot) applyAgentStates(stateSnapshot); initInteraction(camera, renderer); registerSlapTarget(getTimmyGroup(), applySlap); if (firstInit) { initUI(); initWebSocket(scene); initPaymentPanel(); initSessionPanel(); } const ac = new AbortController(); window.addEventListener('resize', () => onWindowResize(camera, renderer), { signal: ac.signal }); let frameCount = 0; let lastFpsTime = performance.now(); let currentFps = 0; running = true; function animate() { if (!running) return; requestAnimationFrame(animate); const now = performance.now(); frameCount++; if (now - lastFpsTime >= 1000) { currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime)); frameCount = 0; lastFpsTime = now; } updateEffects(now); updateAgents(now); updateUI({ fps: currentFps, agentCount: getAgentCount(), jobCount: getJobCount(), connectionState: getConnectionState(), }); // Camera shake — apply transient offset, render, then restore (no drift) const shakeStr = getCameraShakeStrength(); let sx = 0, sy = 0; if (shakeStr > 0) { const mag = shakeStr * 0.22; sx = (Math.random() - 0.5) * mag; sy = (Math.random() - 0.5) * mag * 0.45; camera.position.x += sx; camera.position.y += sy; } renderer.render(scene, camera); if (shakeStr > 0) { camera.position.x -= sx; camera.position.y -= sy; } } animate(); return { scene, renderer, ac }; } function teardown({ scene, renderer, ac }) { running = false; ac.abort(); disposeInteraction(); disposeEffects(); disposeAgents(); disposeWorld(renderer, scene); } function main() { const $overlay = document.getElementById('webgl-recovery-overlay'); let handle = buildWorld(true, null); canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); running = false; if ($overlay) $overlay.style.display = 'flex'; }); canvas.addEventListener('webglcontextrestored', () => { const snapshot = getAgentStates(); teardown(handle); handle = buildWorld(false, snapshot); if ($overlay) $overlay.style.display = 'none'; }); } main(); if (import.meta.env.PROD && 'serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register(import.meta.env.BASE_URL + 'sw.js').catch(() => {}); }); }