Files
the-nexus/frontend/js/main.js
Alexander Whitestone cec0781d95
Some checks failed
CI / test (pull_request) Failing after 11s
CI / validate (pull_request) Failing after 11s
Review Approval Gate / verify-review (pull_request) Failing after 9s
feat: restore frontend shell and implement Project Mnemosyne visual memory bridge
2026-04-08 21:24:32 -04:00

181 lines
5.2 KiB
JavaScript

import { initWorld, onWindowResize, disposeWorld } from './world.js';
import {
initAgents, updateAgents, getAgentCount,
disposeAgents, getAgentStates, applyAgentStates,
} from './agents.js';
import { initEffects, updateEffects, disposeEffects, feedFps } from './effects.js';
import { initUI, updateUI } from './ui.js';
import { initInteraction, updateControls, updateInteraction, disposeInteraction } from './interaction.js';
import { initAmbient, updateAmbient, disposeAmbient } from './ambient.js';
import { initSatFlow, updateSatFlow, disposeSatFlow } from './satflow.js';
import { initEconomy, disposeEconomy } from './economy.js';
import { initWebSocket, getConnectionState, getJobCount } from './websocket.js';
import { initVisitor } from './visitor.js';
import { initPresence, disposePresence } from './presence.js';
import { initTranscript } from './transcript.js';
import { initAvatar, updateAvatar, getAvatarMainCamera, renderAvatarPiP, disposeAvatar } from './avatar.js';
import { initSceneObjects, updateSceneObjects, clearSceneObjects } from './scene-objects.js';
import { updateZones } from './zones.js';
import { initBehaviors, updateBehaviors, disposeBehaviors } from './behaviors.js';
let running = false;
let canvas = null;
/**
* Build (or rebuild) the Three.js world.
*
* @param {boolean} firstInit
* true — first page load: also starts UI, WebSocket, and visitor
* false — context-restore reinit: skips UI/WS (they survive context loss)
* @param {Object.<string,string>|null} stateSnapshot
* Agent state map captured just before teardown; reapplied after initAgents.
*/
function buildWorld(firstInit, stateSnapshot) {
const { scene, camera, renderer } = initWorld(canvas);
canvas = renderer.domElement;
initEffects(scene);
initAgents(scene);
if (stateSnapshot) {
applyAgentStates(stateSnapshot);
}
initSceneObjects(scene);
initBehaviors(); // autonomous agent behaviors (#68)
initAvatar(scene, camera, renderer);
initInteraction(camera, renderer, scene);
initAmbient(scene);
initSatFlow(scene);
if (firstInit) {
initUI();
initEconomy();
initWebSocket(scene);
initVisitor();
initPresence();
initTranscript();
// Dismiss loading screen
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) loadingScreen.classList.add('hidden');
}
// Debounce resize to 1 call per frame
const ac = new AbortController();
let resizeFrame = null;
window.addEventListener('resize', () => {
if (resizeFrame) cancelAnimationFrame(resizeFrame);
resizeFrame = requestAnimationFrame(() => onWindowResize(camera, renderer));
}, { signal: ac.signal });
let frameCount = 0;
let lastFpsTime = performance.now();
let currentFps = 0;
let rafId = null;
let lastTime = performance.now();
running = true;
function animate() {
if (!running) return;
rafId = requestAnimationFrame(animate);
const now = performance.now();
const delta = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
frameCount++;
if (now - lastFpsTime >= 1000) {
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
frameCount = 0;
lastFpsTime = now;
}
updateControls();
updateInteraction();
updateAmbient(delta);
updateSatFlow(delta);
feedFps(currentFps);
updateEffects(now);
updateAgents(now, delta);
updateBehaviors(delta);
updateSceneObjects(now, delta);
updateZones(null); // portal handler wired via loadWorld in websocket.js
updateAvatar(delta);
updateUI({
fps: currentFps,
agentCount: getAgentCount(),
jobCount: getJobCount(),
connectionState: getConnectionState(),
});
renderer.render(scene, getAvatarMainCamera());
renderAvatarPiP(scene);
}
// Pause rendering when tab is backgrounded (saves battery on iPad PWA)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
running = false;
}
} else {
if (!running) {
running = true;
animate();
}
}
});
animate();
return { scene, renderer, ac };
}
function teardown({ scene, renderer, ac }) {
running = false;
ac.abort();
disposeAvatar();
disposeInteraction();
disposeAmbient();
disposeSatFlow();
disposeEconomy();
disposeEffects();
disposePresence();
clearSceneObjects();
disposeBehaviors();
disposeAgents();
disposeWorld(renderer, scene);
}
function main() {
const $overlay = document.getElementById('webgl-recovery-overlay');
let handle = buildWorld(true, null);
// WebGL context loss recovery (iPad PWA, GPU driver reset, etc.)
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();
// Register service worker only in production builds
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}