import { initWorld, onWindowResize, disposeWorld } from './world.js'; import { initAgents, updateAgents, getAgentCount, disposeAgents, getAgentStates, applyAgentStates, getTimmyGroup, applySlap, getCameraShakeStrength, TIMMY_WORLD_POS, } from './agents.js'; import { initEffects, updateEffects, disposeEffects, updateJobIndicators } 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'; import { initHistoryPanel } from './history.js'; import { initNostrIdentity } from './nostr-identity.js'; import { warmup as warmupEdgeWorker, onReady as onEdgeWorkerReady } from './edge-worker-client.js'; import { setEdgeWorkerReady } from './ui.js'; import { initTimmyId } from './timmy-id.js'; import { AGENT_DEFS } from './agent-defs.js'; import { initNavigation, updateNavigation, disposeNavigation } from './navigation.js'; import { initHudLabels, updateHudLabels, disposeHudLabels } from './hud-labels.js'; let running = false; let canvas = null; let _lastTime = performance.now(); function buildWorld(firstInit, stateSnapshot) { const { scene, camera, renderer } = initWorld(canvas); canvas = renderer.domElement; initEffects(scene); initAgents(scene); if (stateSnapshot) applyAgentStates(stateSnapshot); // Navigation replaces OrbitControls initNavigation(camera, renderer); initInteraction(camera, renderer); registerSlapTarget(getTimmyGroup(), applySlap); // AR floating labels initHudLabels(camera, AGENT_DEFS, TIMMY_WORLD_POS); if (firstInit) { initUI(); initWebSocket(scene); initPaymentPanel(); initSessionPanel(); initHistoryPanel(); void initNostrIdentity('/api'); warmupEdgeWorker(); onEdgeWorkerReady(() => setEdgeWorkerReady()); void initTimmyId(); } 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(); const deltaMs = now - _lastTime; _lastTime = now; frameCount++; if (now - lastFpsTime >= 1000) { currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime)); frameCount = 0; lastFpsTime = now; } // FPS navigation updateNavigation(deltaMs); updateEffects(now); updateAgents(now); updateJobIndicators(now); updateUI({ fps: currentFps, agentCount: getAgentCount(), jobCount: getJobCount(), connectionState: getConnectionState(), }); // Camera shake 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; } // AR label positions (after render so NDC is current) updateHudLabels(camera, renderer); } animate(); return { scene, renderer, ac }; } function teardown({ scene, renderer, ac }) { running = false; ac.abort(); disposeNavigation(); disposeInteraction(); disposeHudLabels(); 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); _lastTime = performance.now(); 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(() => {}); }); }