Files
the-nexus/app.js
Perplexity Computer 675b61d65e
All checks were successful
CI / validate (pull_request) Successful in 14s
CI / auto-merge (pull_request) Successful in 0s
refactor: modularize app.js into ES module architecture
Split the monolithic 5393-line app.js into 32 focused ES modules under
modules/ with a thin ~330-line orchestrator. No bundler required — runs
in-browser via import maps.

Module structure:
  core/     — scene, ticker, state, theme, audio
  data/     — gitea, weather, bitcoin, loaders
  terrain/  — stars, clouds, island
  effects/  — matrix-rain, energy-beam, lightning, shockwave, rune-ring, gravity-zones
  panels/   — heatmap, sigil, sovereignty, dual-brain, batcave, earth, agent-board, lora-panel
  portals/  — portal-system, commit-banners
  narrative/ — bookshelves, oath, chat
  utils/    — perlin

All files pass node --check. No new dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 18:12:53 +00:00

333 lines
12 KiB
JavaScript

// app.js — Nexus orchestrator (thin shell)
// All logic lives in modules/. This file wires them together.
import * as THREE from 'three';
// --- Core ---
import { initScene, scene, camera, renderer, composer, orbitControls, bokehPass,
raycaster, forwardVector, clock, ambientLight, overheadLight, warpPass } from './modules/core/scene.js';
import { subscribe, setRenderTarget, start as startTicker } from './modules/core/ticker.js';
import { state } from './modules/core/state.js';
import * as audio from './modules/core/audio.js';
// --- Terrain ---
import * as stars from './modules/terrain/stars.js';
import * as clouds from './modules/terrain/clouds.js';
import * as island from './modules/terrain/island.js';
// --- Effects ---
import * as matrixRain from './modules/effects/matrix-rain.js';
import * as energyBeam from './modules/effects/energy-beam.js';
import * as lightning from './modules/effects/lightning.js';
import * as shockwave from './modules/effects/shockwave.js';
import * as runeRing from './modules/effects/rune-ring.js';
import * as gravityZones from './modules/effects/gravity-zones.js';
// --- Panels ---
import * as heatmap from './modules/panels/heatmap.js';
import * as sigil from './modules/panels/sigil.js';
import * as sovereignty from './modules/panels/sovereignty.js';
import * as dualBrain from './modules/panels/dual-brain.js';
import * as batcave from './modules/panels/batcave.js';
import * as earth from './modules/panels/earth.js';
import * as agentBoard from './modules/panels/agent-board.js';
import * as loraPanel from './modules/panels/lora-panel.js';
// --- Portals ---
import * as portalSystem from './modules/portals/portal-system.js';
import * as commitBanners from './modules/portals/commit-banners.js';
// --- Narrative ---
import * as bookshelves from './modules/narrative/bookshelves.js';
import * as oath from './modules/narrative/oath.js';
import * as chat from './modules/narrative/chat.js';
// --- Data ---
import { fetchCommits } from './modules/data/gitea.js';
import { startWeatherPolling, updateWeatherParticles } from './modules/data/weather.js';
import { startBlockPolling } from './modules/data/bitcoin.js';
import { loadSovereigntyStatus } from './modules/data/loaders.js';
import { cloudMaterial } from './modules/terrain/clouds.js';
import { startPortalHums } from './modules/core/audio.js';
// --- WebSocket ---
import { wsClient } from './ws-client.js';
// ─── Mouse-driven rotation ───
let mouseX = 0, mouseY = 0, targetRotX = 0, targetRotY = 0;
document.addEventListener('mousemove', (e) => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
});
// ─── Overview mode (Tab) ───
let overviewMode = false, overviewT = 0;
const NORMAL_CAM = new THREE.Vector3(0, 6, 11);
const OVERVIEW_CAM = new THREE.Vector3(0, 200, 0.1);
const overviewIndicator = document.getElementById('overview-indicator');
// ─── Zoom-to-object (dblclick) ───
const _zoomRaycaster = new THREE.Raycaster();
const _zoomMouse = new THREE.Vector2();
const _zoomCamTarget = new THREE.Vector3();
const _zoomLookTarget = new THREE.Vector3();
let zoomT = 0, zoomTargetT = 0, zoomActive = false;
const zoomIndicator = document.getElementById('zoom-indicator');
const zoomLabelEl = document.getElementById('zoom-label');
function getZoomLabel(obj) {
let o = obj;
while (o) { if (o.userData && o.userData.zoomLabel) return o.userData.zoomLabel; o = o.parent; }
return 'Object';
}
function exitZoom() {
zoomTargetT = 0; zoomActive = false;
if (zoomIndicator) zoomIndicator.classList.remove('visible');
}
// ─── Photo mode (P) ───
let photoMode = false;
const photoIndicator = document.getElementById('photo-indicator');
const photoFocusDisplay = document.getElementById('photo-focus');
function updateFocusDisplay() {
if (photoFocusDisplay) photoFocusDisplay.textContent = bokehPass.uniforms['focus'].value.toFixed(1);
}
// ─── Sovereignty cheat code ───
const SOVEREIGNTY_WORD = 'sovereignty';
let sovereigntyBuffer = '';
let sovereigntyBufferTimer = null;
// ─── Debug mode ───
let debugMode = false;
// ─── Podcast toggle ───
function initPodcastToggle() {
const btn = document.getElementById('podcast-toggle');
if (!btn) return;
btn.addEventListener('click', () => {
if (btn.textContent === '\uD83C\uDFA7') {
fetch('SOUL.md').then(r => { if (!r.ok) throw new Error('fail'); return r.text(); }).then(text => {
const paragraphs = text.split('\n\n').filter(p => p.trim());
if (!paragraphs.length) throw new Error('empty');
let index = 0;
const speakNext = () => {
if (index >= paragraphs.length) return;
const u = new SpeechSynthesisUtterance(paragraphs[index++]);
u.lang = 'en-US'; u.rate = 0.9; u.pitch = 1.1;
u.onend = () => setTimeout(speakNext, 800);
speechSynthesis.speak(u);
};
btn.textContent = '\u23F9'; btn.classList.add('active'); speakNext();
}).catch(() => { btn.textContent = '\uD83C\uDFA7'; });
} else { speechSynthesis.cancel(); btn.textContent = '\uD83C\uDFA7'; btn.classList.remove('active'); }
});
}
function initSoulToggle() {
const btn = document.getElementById('soul-toggle');
if (!btn) return;
btn.addEventListener('click', () => {
if (btn.textContent === '\uD83D\uDCDC') {
fetch('SOUL.md').then(r => { if (!r.ok) throw new Error('fail'); return r.text(); }).then(text => {
const lines = text.split('\n').slice(1).map(l => l.replace(/^#+\s*/, ''));
let index = 0;
const speakLine = () => {
if (index >= lines.length) return;
const line = lines[index++];
if (!line.trim()) { setTimeout(speakLine, 400); return; }
const u = new SpeechSynthesisUtterance(line);
u.lang = 'en-US'; u.rate = 0.85; u.pitch = 1.0;
u.onend = () => setTimeout(speakLine, 600);
speechSynthesis.speak(u);
};
btn.textContent = '\u23F9'; speakLine();
}).catch(() => {});
} else { speechSynthesis.cancel(); btn.textContent = '\uD83D\uDCDC'; }
});
}
function initDebugToggle() {
const btn = document.getElementById('debug-toggle');
if (!btn) return;
btn.addEventListener('click', () => {
debugMode = !debugMode;
btn.style.backgroundColor = debugMode ? 'var(--color-text-muted)' : 'var(--color-secondary)';
});
}
// ─── Keyboard bindings ───
function initKeyboardBindings() {
document.addEventListener('keydown', (e) => {
// Tab — overview toggle
if (e.key === 'Tab') {
e.preventDefault();
overviewMode = !overviewMode;
if (overviewIndicator) overviewIndicator.classList.toggle('visible', overviewMode);
}
// Escape — exit zoom
if (e.key === 'Escape') exitZoom();
// P — photo mode
if (e.key === 'p' || e.key === 'P') {
photoMode = !photoMode;
document.body.classList.toggle('photo-mode', photoMode);
orbitControls.enabled = photoMode;
if (photoIndicator) photoIndicator.classList.toggle('visible', photoMode);
if (photoMode) {
bokehPass.uniforms['aperture'].value = 0.0003;
bokehPass.uniforms['maxblur'].value = 0.008;
orbitControls.target.set(0, 0, 0); orbitControls.update();
updateFocusDisplay();
} else {
bokehPass.uniforms['aperture'].value = 0.00015;
bokehPass.uniforms['maxblur'].value = 0.004;
}
}
// [ ] — adjust focus in photo mode
if (photoMode) {
if (e.key === '[') { bokehPass.uniforms['focus'].value = Math.max(0.5, bokehPass.uniforms['focus'].value - 0.5); updateFocusDisplay(); }
if (e.key === ']') { bokehPass.uniforms['focus'].value = Math.min(200, bokehPass.uniforms['focus'].value + 0.5); updateFocusDisplay(); }
}
// Sovereignty cheat code
if (e.key.length === 1) {
sovereigntyBuffer += e.key.toLowerCase();
if (sovereigntyBufferTimer) clearTimeout(sovereigntyBufferTimer);
sovereigntyBufferTimer = setTimeout(() => { sovereigntyBuffer = ''; }, 2000);
if (sovereigntyBuffer.includes(SOVEREIGNTY_WORD)) {
sovereigntyBuffer = '';
shockwave.triggerSovereigntyEasterEgg();
}
}
});
}
// ─── Zoom-to-object (dblclick on renderer) ───
function initZoomInteraction() {
renderer.domElement.addEventListener('dblclick', (e) => {
if (overviewMode || photoMode) return;
_zoomMouse.x = (e.clientX / window.innerWidth) * 2 - 1;
_zoomMouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
_zoomRaycaster.setFromCamera(_zoomMouse, camera);
const hits = _zoomRaycaster.intersectObjects(scene.children, true)
.filter(h => !(h.object instanceof THREE.Points) && !(h.object instanceof THREE.Line));
if (!hits.length) { exitZoom(); return; }
const hit = hits[0];
const label = getZoomLabel(hit.object);
const dir = new THREE.Vector3().subVectors(camera.position, hit.point).normalize();
const flyDist = Math.max(1.5, Math.min(5, hit.distance * 0.45));
_zoomCamTarget.copy(hit.point).addScaledVector(dir, flyDist);
_zoomLookTarget.copy(hit.point);
zoomT = 0; zoomTargetT = 1; zoomActive = true;
if (zoomLabelEl) zoomLabelEl.textContent = label;
if (zoomIndicator) zoomIndicator.classList.add('visible');
});
}
// ─── Main per-frame update (subscribed to ticker) ───
function onFrame(elapsed, delta) {
// Camera — overview blend
const targetT = overviewMode ? 1 : 0;
overviewT += (targetT - overviewT) * 0.04;
const basePos = new THREE.Vector3().lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT);
// Camera — zoom blend
if (!photoMode) zoomT += (zoomTargetT - zoomT) * 0.07;
if (zoomT > 0.001 && !photoMode && !overviewMode) {
camera.position.lerpVectors(basePos, _zoomCamTarget, zoomT);
camera.lookAt(new THREE.Vector3(0, 0, 0).lerp(_zoomLookTarget, zoomT));
} else {
camera.position.copy(basePos);
camera.lookAt(0, 0, 0);
}
// Mouse-driven rotation
const rotScale = photoMode ? 0 : (1 - overviewT);
targetRotX += (mouseY * 0.3 - targetRotX) * 0.02;
targetRotY += (mouseX * 0.3 - targetRotY) * 0.02;
if (photoMode) orbitControls.update();
// Module updates
stars.update(elapsed, delta, mouseX, mouseY, overviewT, photoMode);
clouds.update(elapsed);
island.update(elapsed);
energyBeam.update(elapsed);
lightning.update(elapsed);
shockwave.update(elapsed);
runeRing.update(elapsed);
gravityZones.update(elapsed);
heatmap.update(elapsed);
sigil.update(elapsed);
sovereignty.update(elapsed);
dualBrain.update(elapsed);
batcave.updateProbe(elapsed, renderer, scene);
earth.update(elapsed);
agentBoard.update(elapsed);
loraPanel.update(elapsed);
portalSystem.update(elapsed, camera, raycaster, forwardVector);
commitBanners.update(elapsed);
bookshelves.update(elapsed);
oath.update(elapsed);
chat.update(elapsed);
updateWeatherParticles(elapsed);
audio.updateAudioListener();
}
// ─── Data loading after scene is ready ───
async function loadData() {
fetchCommits();
startWeatherPolling(ambientLight, cloudMaterial);
startBlockPolling();
const sovData = await loadSovereigntyStatus();
if (sovData) sovereignty.updateFromData(sovData);
portalSystem.loadPortals(startPortalHums);
}
// ─── Boot ───
initScene(() => {
// Init all modules
matrixRain.init();
stars.init(scene);
clouds.init(scene);
island.init(scene);
energyBeam.init(scene);
lightning.init(scene);
shockwave.init(scene, clock);
runeRing.init(scene);
gravityZones.init(scene);
heatmap.init(scene);
heatmap.drawHeatmap();
sigil.init(scene);
sovereignty.init(scene);
dualBrain.init(scene);
batcave.init(scene);
earth.init(scene);
agentBoard.init(scene);
loraPanel.init(scene);
portalSystem.init(scene, clock, warpPass);
commitBanners.init(scene);
bookshelves.init(scene);
oath.init(scene, ambientLight, overheadLight, renderer, camera);
chat.init(scene, clock);
audio.init(camera);
// Interactions & bindings
initKeyboardBindings();
initZoomInteraction();
initPodcastToggle();
initSoulToggle();
initDebugToggle();
// WebSocket
wsClient.connect();
window.addEventListener('player-joined', (e) => console.log('Player joined:', e.detail));
window.addEventListener('player-left', (e) => console.log('Player left:', e.detail));
// Wire up ticker and start
subscribe(onFrame);
setRenderTarget(renderer, scene, camera, composer);
startTicker();
// Kick off async data loading
loadData();
});