diff --git a/app.js b/app.js
deleted file mode 100644
index 080d628..0000000
--- a/app.js
+++ /dev/null
@@ -1,1200 +0,0 @@
-import * as THREE from 'three';
-import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
-import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
-import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
-import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
-import { ArchonAssembler } from './archon_assembler.js';
-
-// ═══════════════════════════════════════════
-// NEXUS v2.0 — WebSocket Bridge to Timmy
-// ═══════════════════════════════════════════
-
-const NEXUS = {
- colors: {
- primary: 0x4af0c0,
- secondary: 0x7b5cff,
- bg: 0x050510,
- panelBg: 0x0a0f28,
- nebula1: 0x1a0a3e,
- nebula2: 0x0a1a3e,
- gold: 0xffd700,
- danger: 0xff4466,
- gridLine: 0x1a2a4a,
- }
-};
-
-// ═══ STATE ═══
-let camera, scene, renderer, composer;
-let clock, playerPos, playerRot;
-let keys = {};
-let mouseDown = false;
-let batcaveTerminals = [];
-let portals = [];
-let visionPoints = [];
-let agents = [];
-let activePortal = null;
-let activeVisionPoint = null;
-let portalOverlayActive = false;
-let visionOverlayActive = false;
-let thoughtStreamMesh;
-let harnessPulseMesh;
-let powerMeterBars = [];
-let particles, dustParticles;
-let dualBrainGroup, dualBrainPanelTexture, dualBrainPanelCanvas;
-let cloudOrb, localOrb, dualBrainLight;
-let debugOverlay;
-let voidLight = null;
-let frameCount = 0, lastFPSTime = 0, fps = 0;
-let chatOpen = true;
-let loadProgress = 0;
-let performanceTier = 'high';
-let archonAssembler;
-
-// ═══ COMMIT HEATMAP ═══
-let heatmapMesh = null, heatmapMat = null, heatmapTexture = null;
-const _heatmapCanvas = document.createElement('canvas');
-_heatmapCanvas.width = 512;
-_heatmapCanvas.height = 512;
-const HEATMAP_ZONES = [
- { name: 'Claude', color: [255, 100, 60], authorMatch: /^claude$/i, angleDeg: 0 },
- { name: 'Timmy', color: [ 60, 160, 255], authorMatch: /^timmy/i, angleDeg: 90 },
- { name: 'Kimi', color: [ 60, 255, 140], authorMatch: /^kimi/i, angleDeg: 180 },
- { name: 'Perplexity', color: [200, 60, 255], authorMatch: /^perplexity/i, angleDeg: 270 },
-];
-const _heatZoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
-
-// ═══ NAVIGATION ═══
-const NAV_MODES = ['walk', 'orbit', 'fly'];
-let navModeIdx = 0;
-const orbitState = {
- target: new THREE.Vector3(0, 2, 0), radius: 14,
- theta: Math.PI, phi: Math.PI / 6,
- minR: 3, maxR: 40, lastX: 0, lastY: 0,
-};
-let flyY = 2;
-
-// ═══ WEBSOCKET BRIDGE ═══
-const WS_URL = new URLSearchParams(location.search).get('ws') || 'ws://localhost:8765';
-let ws = null;
-let wsConnected = false;
-let wsReconnectTimer = null;
-
-function initWebSocket() {
- if (ws) { try { ws.close(); } catch (_) {} }
- try {
- ws = new WebSocket(WS_URL);
- ws.onopen = () => {
- wsConnected = true;
- addChatMessage('system', `Connected to Timmy @ ${WS_URL}`);
- wsSend({ type: 'presence', event: 'join' });
- if (wsReconnectTimer) { clearInterval(wsReconnectTimer); wsReconnectTimer = null; }
- };
- ws.onmessage = (e) => {
- try { onWsMessage(JSON.parse(e.data)); } catch (_) {}
- };
- ws.onclose = () => {
- wsConnected = false;
- addChatMessage('system', 'Connection to Timmy lost. Reconnecting...');
- scheduleReconnect();
- };
- ws.onerror = () => {
- wsConnected = false;
- scheduleReconnect();
- };
- } catch (_) {
- wsConnected = false;
- scheduleReconnect();
- }
-}
-
-function scheduleReconnect() {
- if (wsReconnectTimer) return;
- wsReconnectTimer = setInterval(() => {
- if (!wsConnected) initWebSocket();
- }, 5000);
-}
-
-function wsSend(msg) {
- if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
-}
-
-function onWsMessage(data) {
- switch (data.type) {
- case 'agent_state': {
- const agent = agents.find(a => a.id === data.agent);
- if (!agent) break;
- if (data.state === 'thinking') {
- setAgentActivity(agent, ACTIVITY_STATES.THINKING);
- agent.activityLocked = true;
- } else if (data.state === 'processing') {
- setAgentActivity(agent, ACTIVITY_STATES.PROCESSING);
- agent.activityLocked = true;
- } else if (data.state === 'waiting') {
- setAgentActivity(agent, ACTIVITY_STATES.WAITING);
- agent.activityLocked = true;
- } else {
- setAgentActivity(agent, ACTIVITY_STATES.NONE);
- agent.activityLocked = false;
- }
- if (data.thought) addAgentLog(data.agent, data.thought);
- break;
- }
- case 'agent_move': {
- const agent = agents.find(a => a.id === data.agent);
- if (agent) agent.targetPos.set(data.x, 0, data.z);
- break;
- }
- case 'chat_response':
- addChatMessage(data.agent || 'timmy', data.text);
- // Clear Timmy's thinking state
- const timmy = agents.find(a => a.id === 'timmy');
- if (timmy) { setAgentActivity(timmy, ACTIVITY_STATES.NONE); timmy.activityLocked = false; }
- break;
- case 'system_metrics':
- updateTerminalMetrics(data);
- break;
- case 'dual_brain':
- updateDualBrainFromWS(data);
- break;
- case 'heartbeat':
- break;
- }
-}
-
-// ═══ INIT ═══
-async function init() {
- clock = new THREE.Clock();
- playerPos = new THREE.Vector3(0, 2, 12);
- playerRot = new THREE.Euler(0, 0, 0, 'YXZ');
-
- const canvas = document.getElementById('nexus-canvas');
- renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
- renderer.setSize(window.innerWidth, window.innerHeight);
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
- renderer.toneMappingExposure = 1.2;
- renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
-
- performanceTier = detectPerformanceTier();
- updateLoad(10);
-
- scene = new THREE.Scene();
- scene.fog = new THREE.FogExp2(0x050510, 0.012);
- camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
- camera.position.copy(playerPos);
- updateLoad(20);
-
- createSkybox(); updateLoad(30);
- createLighting(); updateLoad(40);
- createFloor();
- createCommitHeatmap(); updateLoad(50);
- createBatcaveTerminal(); updateLoad(60);
-
- try {
- const portalData = await (await fetch('./portals.json')).json();
- createPortals(portalData);
- } catch (e) { addChatMessage('error', 'Portal registry offline.'); }
-
- try {
- const visionData = await (await fetch('./vision.json')).json();
- createVisionPoints(visionData);
- } catch (_) {}
-
- updateLoad(80);
- createParticles();
- createDustParticles();
- updateLoad(85);
- createAmbientStructures();
- createAgentPresences();
- createThoughtStream();
- createHarnessPulse();
- createSessionPowerMeter();
- createDualBrainPanel();
- updateLoad(90);
-
- // Test Archon Assembler
- const testManifest = {
- head: true,
- torso: true,
- arms: true,
- legs: true,
- hands: true,
- eyes: true,
- mouth: true,
- wings: true,
- aura: true,
- crown: true,
- };
- archonAssembler = new ArchonAssembler(scene, testManifest);
- archonAssembler.assemble();
- archonAssembler.spawn(new THREE.Vector3(0, 0, -15));
-
- composer = new EffectComposer(renderer);
- composer.addPass(new RenderPass(scene, camera));
- composer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.6, 0.4, 0.85));
- composer.addPass(new SMAAPass(window.innerWidth, window.innerHeight));
- updateLoad(95);
-
- setupControls();
- window.addEventListener('resize', onResize);
- debugOverlay = document.getElementById('debug-overlay');
- updateLoad(100);
-
- setTimeout(() => {
- document.getElementById('loading-screen').classList.add('fade-out');
- const enterPrompt = document.getElementById('enter-prompt');
- enterPrompt.style.display = 'flex';
- enterPrompt.addEventListener('click', () => {
- enterPrompt.classList.add('fade-out');
- document.getElementById('hud').style.display = 'block';
- setTimeout(() => enterPrompt.remove(), 600);
- }, { once: true });
- setTimeout(() => document.getElementById('loading-screen').remove(), 900);
- }, 600);
-
- initWebSocket();
- requestAnimationFrame(gameLoop);
-}
-
-function updateLoad(pct) {
- loadProgress = pct;
- const fill = document.getElementById('load-progress');
- if (fill) fill.style.width = pct + '%';
-}
-
-// ═══ PERFORMANCE ═══
-function detectPerformanceTier() {
- const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
- const cores = navigator.hardwareConcurrency || 4;
- if (isMobile) { renderer.setPixelRatio(1); renderer.shadowMap.enabled = false; return 'low'; }
- if (cores < 8) { renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); renderer.shadowMap.type = THREE.BasicShadowMap; return 'medium'; }
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- return 'high';
-}
-
-function particleCount(base) {
- return Math.floor(base * (performanceTier === 'low' ? 0.25 : performanceTier === 'medium' ? 0.6 : 1));
-}
-
-// ═══ SKYBOX ═══
-function createSkybox() {
- const skyGeo = new THREE.SphereGeometry(400, 64, 64);
- const skyMat = new THREE.ShaderMaterial({
- uniforms: {
- uTime: { value: 0 },
- uColor1: { value: new THREE.Color(0x0a0520) },
- uColor2: { value: new THREE.Color(0x1a0a3e) },
- uColor3: { value: new THREE.Color(0x0a1a3e) },
- uStarDensity: { value: 0.97 },
- },
- vertexShader: `
- varying vec3 vPos;
- void main() { vPos = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
- `,
- fragmentShader: `
- uniform float uTime; uniform vec3 uColor1, uColor2, uColor3; uniform float uStarDensity;
- varying vec3 vPos;
- float hash(vec3 p) { p = fract(p * vec3(443.897,441.423,437.195)); p += dot(p,p.yzx+19.19); return fract((p.x+p.y)*p.z); }
- float noise(vec3 p) {
- vec3 i = floor(p), f = fract(p); f = f*f*(3.0-2.0*f);
- return mix(mix(mix(hash(i),hash(i+vec3(1,0,0)),f.x),mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y),
- mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x),mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z);
- }
- float fbm(vec3 p) { float v=0.0,a=0.5; for(int i=0;i<5;i++){v+=a*noise(p);p*=2.0;a*=0.5;} return v; }
- void main() {
- vec3 dir = normalize(vPos);
- float n1=fbm(dir*3.0+uTime*0.02), n2=fbm(dir*5.0-uTime*0.015+100.0), n3=fbm(dir*2.0+uTime*0.01+200.0);
- vec3 col = mix(mix(uColor1,uColor2,smoothstep(0.3,0.7,n1)),uColor3,smoothstep(0.4,0.8,n2)*0.5);
- col += vec3(0.15,0.05,0.25)*pow(n1*n2,2.0)*1.5 + vec3(0.05,0.15,0.25)*pow(n3,3.0);
- float sf=hash(dir*800.0), stars=step(uStarDensity,sf)*(0.5+0.5*hash(dir*1600.0));
- float tw=0.7+0.3*sin(uTime*2.0+hash(dir*400.0)*6.28);
- col += vec3(stars*tw) + vec3(0.8,0.9,1.0)*step(0.998,sf)*tw;
- gl_FragColor = vec4(col, 1.0);
- }
- `,
- side: THREE.BackSide,
- });
- const sky = new THREE.Mesh(skyGeo, skyMat);
- sky.name = 'skybox';
- scene.add(sky);
-}
-
-// ═══ LIGHTING ═══
-function createLighting() {
- scene.add(new THREE.AmbientLight(0x1a1a3a, 0.4));
- const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6);
- dirLight.position.set(10, 20, 10);
- dirLight.castShadow = renderer.shadowMap.enabled;
- dirLight.shadow.mapSize.set(performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512,
- performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512);
- scene.add(dirLight);
- const tl = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5); tl.position.set(0, 1, -5); scene.add(tl);
- const pl = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5); pl.position.set(-8, 3, -8); scene.add(pl);
-}
-
-// ═══ FLOOR ═══
-function createFloor() {
- const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
- const platMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.bg, transparent: true, opacity: 0.2, transmission: 0.9, roughness: 0.1, metalness: 0.2 });
- const platform = new THREE.Mesh(platGeo, platMat);
- platform.position.y = -0.15; platform.receiveShadow = true;
- scene.add(platform);
-
- const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine);
- gridHelper.material.opacity = 0.15; gridHelper.material.transparent = true; gridHelper.position.y = 0.02;
- scene.add(gridHelper);
-
- const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6);
- const ringMat = new THREE.MeshBasicMaterial({ color: NEXUS.colors.primary, transparent: true, opacity: 0.4, side: THREE.DoubleSide });
- const ring = new THREE.Mesh(ringGeo, ringMat);
- ring.rotation.x = Math.PI / 2; ring.position.y = 0.05;
- scene.add(ring);
-
- _buildGlassFloor();
-}
-
-function _buildGlassFloor() {
- const TILE = 0.85, GAP = 0.14, STEP = TILE + GAP, RADIUS = 4.55;
- const group = new THREE.Group();
-
- // Frame ring
- const frameMat = new THREE.MeshStandardMaterial({ color: 0x0a1828, metalness: 0.9, roughness: 0.1, emissive: new THREE.Color(NEXUS.colors.primary).multiplyScalar(0.06) });
- const rim = new THREE.Mesh(new THREE.RingGeometry(4.7, 5.3, 64), frameMat);
- rim.rotation.x = -Math.PI / 2; rim.position.y = 0.01; group.add(rim);
- const torus = new THREE.Mesh(new THREE.TorusGeometry(5.0, 0.1, 6, 64), frameMat);
- torus.rotation.x = Math.PI / 2; torus.position.y = 0.01; group.add(torus);
-
- // Collect tile positions
- const slots = [];
- for (let r = -5; r <= 5; r++) for (let c = -5; c <= 5; c++) {
- const x = c * STEP, z = r * STEP;
- if (Math.sqrt(x * x + z * z) <= RADIUS) slots.push({ x, z });
- }
-
- // Instanced glass tiles
- const tileMat = new THREE.MeshPhysicalMaterial({
- color: new THREE.Color(NEXUS.colors.primary), transparent: true, opacity: 0.09,
- roughness: 0, metalness: 0, transmission: 0.92, thickness: 0.06, side: THREE.DoubleSide, depthWrite: false,
- });
- const tileMesh = new THREE.InstancedMesh(new THREE.PlaneGeometry(TILE, TILE), tileMat, slots.length);
- tileMesh.instanceMatrix.setUsage(THREE.StaticDrawUsage);
- const dummy = new THREE.Object3D();
- dummy.rotation.x = -Math.PI / 2;
- slots.forEach((s, i) => { dummy.position.set(s.x, 0.005, s.z); dummy.updateMatrix(); tileMesh.setMatrixAt(i, dummy.matrix); });
- tileMesh.instanceMatrix.needsUpdate = true;
- group.add(tileMesh);
-
- // Edge lines (single draw call)
- const HS = TILE / 2;
- const verts = new Float32Array(slots.length * 24);
- let vi = 0;
- for (const { x, z } of slots) {
- const y = 0.008;
- verts[vi++]=x-HS;verts[vi++]=y;verts[vi++]=z-HS; verts[vi++]=x+HS;verts[vi++]=y;verts[vi++]=z-HS;
- verts[vi++]=x+HS;verts[vi++]=y;verts[vi++]=z-HS; verts[vi++]=x+HS;verts[vi++]=y;verts[vi++]=z+HS;
- verts[vi++]=x+HS;verts[vi++]=y;verts[vi++]=z+HS; verts[vi++]=x-HS;verts[vi++]=y;verts[vi++]=z+HS;
- verts[vi++]=x-HS;verts[vi++]=y;verts[vi++]=z+HS; verts[vi++]=x-HS;verts[vi++]=y;verts[vi++]=z-HS;
- }
- const edgeGeo = new THREE.BufferGeometry();
- edgeGeo.setAttribute('position', new THREE.BufferAttribute(verts, 3));
- const edgeMat = new THREE.LineBasicMaterial({ color: NEXUS.colors.primary, transparent: true, opacity: 0.55 });
- group.add(new THREE.LineSegments(edgeGeo, edgeMat));
-
- // Void light
- voidLight = new THREE.PointLight(NEXUS.colors.primary, 0.5, 14);
- voidLight.position.set(0, -3.5, 0);
- group.add(voidLight);
-
- scene.add(group);
-}
-
-// ═══ COMMIT HEATMAP ═══
-function createCommitHeatmap() {
- heatmapTexture = new THREE.CanvasTexture(_heatmapCanvas);
- heatmapMat = new THREE.MeshBasicMaterial({ map: heatmapTexture, transparent: true, opacity: 0.9, depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide });
- heatmapMesh = new THREE.Mesh(new THREE.CircleGeometry(24, 64), heatmapMat);
- heatmapMesh.rotation.x = -Math.PI / 2; heatmapMesh.position.y = 0.005;
- scene.add(heatmapMesh);
- updateHeatmap();
- setInterval(updateHeatmap, 5 * 60 * 1000);
-}
-
-function drawHeatmap() {
- const ctx = _heatmapCanvas.getContext('2d'), cx = 256, cy = 256, r = 246;
- ctx.clearRect(0, 0, 512, 512);
- ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.clip();
- for (const zone of HEATMAP_ZONES) {
- const intensity = _heatZoneIntensity[zone.name] || 0;
- if (intensity < 0.01) continue;
- const [rr, gg, bb] = zone.color;
- const baseRad = zone.angleDeg * (Math.PI / 180);
- const gx = cx + Math.cos(baseRad) * r * 0.55, gy = cy + Math.sin(baseRad) * r * 0.55;
- const grad = ctx.createRadialGradient(gx, gy, 0, gx, gy, r * 0.75);
- grad.addColorStop(0, `rgba(${rr},${gg},${bb},${0.65 * intensity})`);
- grad.addColorStop(0.45, `rgba(${rr},${gg},${bb},${0.25 * intensity})`);
- grad.addColorStop(1, `rgba(${rr},${gg},${bb},0)`);
- ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, baseRad - Math.PI / 4, baseRad + Math.PI / 4); ctx.closePath();
- ctx.fillStyle = grad; ctx.fill();
- if (intensity > 0.05) {
- ctx.font = `bold ${Math.round(13 * intensity + 7)}px "Courier New",monospace`;
- ctx.fillStyle = `rgba(${rr},${gg},${bb},${Math.min(intensity * 1.2, 0.9)})`;
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
- ctx.fillText(zone.name, cx + Math.cos(baseRad) * r * 0.62, cy + Math.sin(baseRad) * r * 0.62);
- }
- }
- ctx.restore();
- if (heatmapTexture) heatmapTexture.needsUpdate = true;
-}
-
-async function updateHeatmap() {
- let commits = [];
- try { const res = await fetch('/api/commits'); if (res.ok) commits = await res.json(); } catch {}
- const DECAY_MS = 24 * 60 * 60 * 1000, now = Date.now();
- const raw = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0]));
- for (const c of commits) {
- const author = c.commit?.author?.name || c.author?.login || '';
- const age = now - new Date(c.commit?.author?.date || 0).getTime();
- if (age > DECAY_MS) continue;
- const weight = 1 - age / DECAY_MS;
- for (const z of HEATMAP_ZONES) { if (z.authorMatch.test(author)) { raw[z.name] += weight; break; } }
- }
- for (const z of HEATMAP_ZONES) _heatZoneIntensity[z.name] = Math.min(raw[z.name] / 8, 1.0);
- drawHeatmap();
-}
-
-// ═══ BATCAVE TERMINAL ═══
-function createBatcaveTerminal() {
- const group = new THREE.Group();
- group.position.set(0, 0, -8);
- const panels = [
- { title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: —', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] },
- { title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> AWAITING WS...', '> CONNECT TIMMY', '> TO SEE LIVE', '> ISSUE DATA'] },
- { title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: —', '> MEM: —', '> COMMITS: —', '> AGENTS: —'] },
- { title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> AWAITING', '> WEBSOCKET', '> CONNECTION', '> TO TIMMY'] },
- { title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ○ OFFLINE', '> KIMI: ○ OFFLINE', '> CLAUDE: ○ OFFLINE', '> PERPLEXITY: ○'] },
- ];
- panels.forEach(d => createTerminalPanel(group, d.x, d.y, d.rot, d.title, d.color, d.lines));
- scene.add(group);
-}
-
-function createTerminalPanel(parent, x, y, rot, title, color, lines) {
- const w = 2.8, h = 3.5, group = new THREE.Group();
- group.position.set(x, y, 0); group.rotation.y = rot;
-
- const bg = new THREE.Mesh(new THREE.PlaneGeometry(w, h), new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.panelBg, transparent: true, opacity: 0.6, roughness: 0.1, metalness: 0.5, side: THREE.DoubleSide }));
- group.add(bg);
-
- const border = new THREE.Mesh(new THREE.PlaneGeometry(w + 0.05, h + 0.05), new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.3, side: THREE.DoubleSide }));
- border.position.z = -0.01; group.add(border);
-
- const tc = document.createElement('canvas'); tc.width = 512; tc.height = 640;
- const ctx = tc.getContext('2d');
- ctx.fillStyle = '#' + new THREE.Color(color).getHexString();
- ctx.font = 'bold 32px "Orbitron",sans-serif'; ctx.fillText(title, 20, 45);
- ctx.fillRect(20, 55, 472, 2);
- ctx.font = '20px "JetBrains Mono",monospace';
- lines.forEach((line, i) => {
- ctx.fillStyle = line.includes('●') ? '#4af0c0' : line.includes('○') ? '#5a6a8a' : '#a0b8d0';
- ctx.fillText(line, 20, 100 + i * 40);
- });
- const tex = new THREE.CanvasTexture(tc); tex.minFilter = THREE.LinearFilter;
- const textMesh = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.95, h * 0.95), new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide, depthWrite: false }));
- textMesh.position.z = 0.01; group.add(textMesh);
-
- const scanMat = new THREE.ShaderMaterial({
- transparent: true, depthWrite: false,
- uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(color) } },
- vertexShader: `varying vec2 vUv; void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
- fragmentShader: `uniform float uTime;uniform vec3 uColor;varying vec2 vUv;void main(){float s=pow(sin(vUv.y*200.0+uTime*2.0)*0.5+0.5,8.0);float w=smoothstep(0.0,0.02,abs(fract(vUv.y-uTime*0.1)-0.5));float a=s*0.04+(1.0-w)*0.08;gl_FragColor=vec4(uColor,a);}`,
- side: THREE.DoubleSide,
- });
- const scan = new THREE.Mesh(new THREE.PlaneGeometry(w, h), scanMat);
- scan.position.z = 0.02; group.add(scan);
-
- parent.add(group);
- batcaveTerminals.push({ group, scanMat, borderMat: border.material });
-}
-
-function updateTerminalMetrics(data) {
- // WS-driven terminal update — could redraw canvases in the future
- // For now, metrics come through and are visible in the thought stream
-}
-
-// ═══ AGENT SYSTEM ═══
-const AGENT_STATES = { IDLE: 'IDLE', PACING: 'PACING', LOOKING: 'LOOKING', READING: 'READING' };
-const ACTIVITY_STATES = { NONE: 'NONE', WAITING: 'WAITING', THINKING: 'THINKING', PROCESSING: 'PROCESSING' };
-
-function createActivityIndicator(color) {
- const group = new THREE.Group(); group.position.y = 4.2; group.visible = false;
- const waitMesh = new THREE.Mesh(new THREE.SphereGeometry(0.18, 16, 16), new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.85 }));
- waitMesh.name = 'indicator_waiting'; waitMesh.visible = false; group.add(waitMesh);
- const thinkMesh = new THREE.Mesh(new THREE.OctahedronGeometry(0.2, 0), new THREE.MeshBasicMaterial({ color, wireframe: true }));
- thinkMesh.name = 'indicator_thinking'; thinkMesh.visible = false; group.add(thinkMesh);
- const procMesh = new THREE.Mesh(new THREE.TorusGeometry(0.18, 0.04, 8, 32), new THREE.MeshBasicMaterial({ color }));
- procMesh.name = 'indicator_processing'; procMesh.visible = false; group.add(procMesh);
- return { group, waitMesh, thinkMesh, procMesh };
-}
-
-function setAgentActivity(agent, state) {
- agent.activityState = state;
- agent.indicator.group.visible = (state !== ACTIVITY_STATES.NONE);
- agent.indicator.waitMesh.visible = (state === ACTIVITY_STATES.WAITING);
- agent.indicator.thinkMesh.visible = (state === ACTIVITY_STATES.THINKING);
- agent.indicator.procMesh.visible = (state === ACTIVITY_STATES.PROCESSING);
-}
-
-function buildPacingPath(station) {
- const r = 1.8;
- return [
- new THREE.Vector3(station.x - r, 0, station.z),
- new THREE.Vector3(station.x, 0, station.z + r),
- new THREE.Vector3(station.x + r, 0, station.z - r * 0.5),
- ];
-}
-
-function pickNextState() {
- const w = { IDLE: 40, PACING: 25, LOOKING: 20, READING: 15 };
- let r = Math.random() * 100;
- for (const [s, wt] of Object.entries(w)) { r -= wt; if (r <= 0) return s; }
- return 'IDLE';
-}
-
-function createAgentPresences() {
- const agentData = [
- { id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 } },
- { id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 } },
- { id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 } },
- { id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 } },
- ];
- agentData.forEach(data => {
- const group = new THREE.Group();
- group.position.set(data.pos.x, 0, data.pos.z);
- const color = new THREE.Color(data.color);
-
- const orb = new THREE.Mesh(new THREE.SphereGeometry(0.4, 32, 32), new THREE.MeshPhysicalMaterial({
- color, emissive: color, emissiveIntensity: 2, roughness: 0, metalness: 1, transmission: 0.8, thickness: 0.5,
- }));
- orb.position.y = 3; group.add(orb);
-
- const halo = new THREE.Mesh(new THREE.TorusGeometry(0.6, 0.02, 16, 64), new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 }));
- halo.position.y = 3; halo.rotation.x = Math.PI / 2; group.add(halo);
-
- const lc = document.createElement('canvas'); lc.width = 256; lc.height = 64;
- const lctx = lc.getContext('2d');
- lctx.font = 'bold 24px "Orbitron",sans-serif'; lctx.fillStyle = '#' + color.getHexString();
- lctx.textAlign = 'center'; lctx.fillText(data.name, 128, 40);
- const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(lc), transparent: true, side: THREE.DoubleSide }));
- label.position.y = 3.8; group.add(label);
-
- const indicator = createActivityIndicator(color);
- group.add(indicator.group);
- scene.add(group);
-
- agents.push({
- id: data.id, group, orb, halo, color,
- station: data.pos,
- targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
- state: AGENT_STATES.IDLE, stateTimer: 2 + Math.random() * 4,
- lookAngle: 0, lookSpeed: 0.4 + Math.random() * 0.3,
- pacingPath: buildPacingPath(data.pos), pacingIdx: 0,
- indicator, activityState: ACTIVITY_STATES.NONE, activityLocked: false,
- });
- });
-}
-
-// ═══ THOUGHT STREAM ═══
-function createThoughtStream() {
- const mat = new THREE.ShaderMaterial({
- transparent: true, side: THREE.BackSide, depthWrite: false,
- uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(NEXUS.colors.primary) } },
- vertexShader: `varying vec2 vUv; void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
- fragmentShader: `uniform float uTime;uniform vec3 uColor;varying vec2 vUv;
- float hash(vec2 p){return fract(sin(dot(p,vec2(12.9898,78.233)))*43758.5453);}
- void main(){float flow=fract(vUv.y-uTime*0.1);float lines=step(0.98,fract(vUv.x*20.0+uTime*0.05));float dots=step(0.99,hash(vUv*50.0+floor(uTime*10.0)*0.01));
- float a=(lines*0.1+dots*0.5)*smoothstep(0.0,0.2,vUv.y)*smoothstep(1.0,0.8,vUv.y);gl_FragColor=vec4(uColor,a*0.3);}`,
- });
- thoughtStreamMesh = new THREE.Mesh(new THREE.CylinderGeometry(8, 8, 12, 32, 1, true), mat);
- thoughtStreamMesh.position.y = 6;
- scene.add(thoughtStreamMesh);
-}
-
-function createHarnessPulse() {
- const mat = new THREE.MeshBasicMaterial({ color: NEXUS.colors.primary, transparent: true, opacity: 0, side: THREE.DoubleSide });
- harnessPulseMesh = new THREE.Mesh(new THREE.RingGeometry(0.1, 0.2, 64), mat);
- harnessPulseMesh.rotation.x = -Math.PI / 2; harnessPulseMesh.position.y = 0.1;
- scene.add(harnessPulseMesh);
-}
-
-function createSessionPowerMeter() {
- const group = new THREE.Group(); group.position.set(0, 0, 3);
- const barGeo = new THREE.BoxGeometry(0.2, 0.1, 0.1);
- for (let i = 0; i < 12; i++) {
- const mat = new THREE.MeshStandardMaterial({ color: NEXUS.colors.primary, emissive: NEXUS.colors.primary, emissiveIntensity: 0.2, transparent: true, opacity: 0.6 });
- const bar = new THREE.Mesh(barGeo, mat); bar.position.y = 0.2 + i * 0.2;
- group.add(bar); powerMeterBars.push(bar);
- }
- const lc = document.createElement('canvas'); lc.width = 256; lc.height = 64;
- const ctx = lc.getContext('2d');
- ctx.font = 'bold 24px "Orbitron",sans-serif'; ctx.fillStyle = '#4af0c0'; ctx.textAlign = 'center'; ctx.fillText('POWER LEVEL', 128, 40);
- const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(lc), transparent: true, side: THREE.DoubleSide }));
- label.position.y = 3; group.add(label);
- scene.add(group);
-}
-
-// ═══ DUAL-BRAIN PANEL ═══
-function createDualBrainPanel() {
- dualBrainGroup = new THREE.Group();
- dualBrainGroup.position.set(10, 3, -8);
- dualBrainGroup.lookAt(0, 3, 0);
- scene.add(dualBrainGroup);
-
- dualBrainPanelCanvas = document.createElement('canvas');
- dualBrainPanelCanvas.width = 512; dualBrainPanelCanvas.height = 512;
- drawDualBrainTexture(null);
- dualBrainPanelTexture = new THREE.CanvasTexture(dualBrainPanelCanvas);
-
- const panelSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: dualBrainPanelTexture, transparent: true, opacity: 0.92, depthWrite: false }));
- panelSprite.scale.set(5, 5, 1);
- dualBrainGroup.add(panelSprite);
-
- dualBrainLight = new THREE.PointLight(0x4488ff, 0.6, 10);
- dualBrainLight.position.set(0, 0.5, 1);
- dualBrainGroup.add(dualBrainLight);
-
- const orbMat = (c) => new THREE.MeshStandardMaterial({ color: c, emissive: new THREE.Color(c), emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2, transparent: true, opacity: 0.85 });
- cloudOrb = new THREE.Mesh(new THREE.SphereGeometry(0.35, 32, 32), orbMat(0x334466));
- cloudOrb.position.set(-2, 3, 0); dualBrainGroup.add(cloudOrb);
- localOrb = new THREE.Mesh(new THREE.SphereGeometry(0.35, 32, 32), orbMat(0x334466));
- localOrb.position.set(2, 3, 0); dualBrainGroup.add(localOrb);
-}
-
-function drawDualBrainTexture(data) {
- const W = 512, H = 512, ctx = dualBrainPanelCanvas.getContext('2d');
- ctx.fillStyle = 'rgba(0,6,20,0.90)'; ctx.fillRect(0, 0, W, H);
- ctx.strokeStyle = '#4488ff'; ctx.lineWidth = 2; ctx.strokeRect(1, 1, W - 2, H - 2);
- ctx.font = 'bold 22px "Courier New",monospace'; ctx.fillStyle = '#88ccff'; ctx.textAlign = 'center';
- ctx.fillText('\u25C8 DUAL-BRAIN STATUS', W / 2, 40);
- ctx.strokeStyle = '#1a3a6a'; ctx.lineWidth = 1;
- ctx.beginPath(); ctx.moveTo(20, 52); ctx.lineTo(W - 20, 52); ctx.stroke();
-
- if (!data) {
- ctx.font = 'bold 18px "Courier New",monospace'; ctx.fillStyle = '#334466';
- ctx.fillText('AWAITING CONNECTION', W / 2, H / 2);
- ctx.font = '11px "Courier New",monospace'; ctx.fillStyle = '#223344';
- ctx.fillText('Connect Timmy via WebSocket', W / 2, H / 2 + 28);
- } else {
- const y = 74;
- ctx.font = '13px "Courier New",monospace'; ctx.textAlign = 'left';
- ctx.fillStyle = data.cloud?.status === 'connected' ? '#4af0c0' : '#ff4466';
- ctx.fillText(`CLOUD: ${data.cloud?.model || '—'} [${data.cloud?.status || 'offline'}]`, 20, y);
- ctx.fillStyle = data.local?.status === 'connected' ? '#4af0c0' : '#ff4466';
- ctx.fillText(`LOCAL: ${data.local?.model || '—'} [${data.local?.status || 'offline'}]`, 20, y + 24);
- }
- if (dualBrainPanelTexture) dualBrainPanelTexture.needsUpdate = true;
-}
-
-function updateDualBrainFromWS(data) {
- drawDualBrainTexture(data);
- // Activate orbs based on connection status
- if (cloudOrb && data.cloud?.status === 'connected') {
- cloudOrb.material.color.setHex(0x4488ff); cloudOrb.material.emissive.setHex(0x4488ff); cloudOrb.material.emissiveIntensity = 1.5;
- }
- if (localOrb && data.local?.status === 'connected') {
- localOrb.material.color.setHex(0x4af0c0); localOrb.material.emissive.setHex(0x4af0c0); localOrb.material.emissiveIntensity = 1.5;
- }
-}
-
-// ═══ VISION SYSTEM ═══
-function createVisionPoints(data) { data.forEach(c => visionPoints.push(createVisionPoint(c))); }
-
-function createVisionPoint(config) {
- const group = new THREE.Group();
- group.position.set(config.position.x, config.position.y, config.position.z);
- const color = new THREE.Color(config.color);
- const crystal = new THREE.Mesh(new THREE.OctahedronGeometry(0.6, 0), new THREE.MeshPhysicalMaterial({ color, emissive: color, emissiveIntensity: 1, roughness: 0, metalness: 1, transmission: 0.5, thickness: 1 }));
- crystal.position.y = 2.5; group.add(crystal);
- const ring = new THREE.Mesh(new THREE.TorusGeometry(0.8, 0.02, 16, 64), new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 }));
- ring.position.y = 2.5; ring.rotation.x = Math.PI / 2; group.add(ring);
- const light = new THREE.PointLight(color, 1, 10); light.position.set(0, 2.5, 0); group.add(light);
- scene.add(group);
- return { config, group, crystal, ring, light };
-}
-
-// ═══ PORTAL SYSTEM ═══
-function createPortals(data) { data.forEach(c => portals.push(createPortal(c))); }
-
-function createPortal(config) {
- const group = new THREE.Group();
- group.position.set(config.position.x, config.position.y, config.position.z);
- if (config.rotation) group.rotation.y = config.rotation.y;
- const pc = new THREE.Color(config.color);
-
- // Ring
- const ring = new THREE.Mesh(new THREE.TorusGeometry(3, 0.15, 16, 64), new THREE.MeshStandardMaterial({ color: pc, emissive: pc, emissiveIntensity: 1.5, roughness: 0.2, metalness: 0.8 }));
- ring.position.y = 3.5; ring.name = `portal_ring_${config.id}`; group.add(ring);
-
- // Swirl
- const swirlMat = new THREE.ShaderMaterial({
- transparent: true, side: THREE.DoubleSide,
- uniforms: { uTime: { value: 0 }, uColor: { value: pc } },
- vertexShader: `varying vec2 vUv; void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
- fragmentShader: `uniform float uTime;uniform vec3 uColor;varying vec2 vUv;void main(){vec2 c=vUv-0.5;float r=length(c),a=atan(c.y,c.x);float s1=sin(a*3.0+r*10.0-uTime*3.0)*0.5+0.5,s2=sin(a*5.0-r*8.0+uTime*2.0)*0.5+0.5;float mask=smoothstep(0.5,0.1,r);vec3 col=mix(mix(uColor,vec3(1),s1*0.3),vec3(1),s2*0.2);gl_FragColor=vec4(col,mask*(0.5+0.3*s1));}`,
- });
- const swirl = new THREE.Mesh(new THREE.CircleGeometry(2.8, 64), swirlMat);
- swirl.position.y = 3.5; group.add(swirl);
-
- // Particles
- const pCount = 120, pPos = new Float32Array(pCount * 3);
- for (let i = 0; i < pCount; i++) {
- const angle = Math.random() * Math.PI * 2, r = 3.2 + Math.random() * 0.5;
- pPos[i*3] = Math.cos(angle)*r; pPos[i*3+1] = 3.5 + (Math.random()-0.5)*6; pPos[i*3+2] = (Math.random()-0.5)*0.5;
- }
- const pGeo = new THREE.BufferGeometry();
- pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
- const pSystem = new THREE.Points(pGeo, new THREE.PointsMaterial({ color: pc, size: 0.08, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending, depthWrite: false }));
- group.add(pSystem);
-
- // Light
- const light = new THREE.PointLight(pc, 2, 15, 1.5); light.position.set(0, 3.5, 1); group.add(light);
-
- // Runes
- const runes = [];
- for (let i = 0; i < 8; i++) {
- const angle = (i / 8) * Math.PI * 2;
- const rune = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.8, 0.1), new THREE.MeshStandardMaterial({ color: pc, emissive: pc, emissiveIntensity: 0.8, transparent: true, opacity: 0.7, roughness: 0.2, metalness: 0.5 }));
- rune.position.set(Math.cos(angle) * 4.5, 4, Math.sin(angle) * 4.5);
- rune.rotation.y = angle + Math.PI / 2; group.add(rune); runes.push(rune);
- }
-
- // Label
- const lc = document.createElement('canvas'); lc.width = 512; lc.height = 64;
- const lctx = lc.getContext('2d');
- lctx.font = 'bold 32px "Orbitron",sans-serif'; lctx.fillStyle = '#' + pc.getHexString();
- lctx.textAlign = 'center'; lctx.fillText(`\u25C8 ${config.name.toUpperCase()}`, 256, 42);
- const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.5), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(lc), transparent: true, side: THREE.DoubleSide }));
- labelMesh.position.y = 7.5; group.add(labelMesh);
-
- // Pillars
- for (const side of [-1, 1]) {
- const pillar = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.3, 7, 8), new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.5, metalness: 0.7, emissive: pc, emissiveIntensity: 0.1 }));
- pillar.position.set(side * 3, 3.5, 0); pillar.castShadow = true; group.add(pillar);
- }
-
- scene.add(group);
- return { config, group, ring, swirl, pSystem, light, runes };
-}
-
-// ═══ PARTICLES ═══
-function createParticles() {
- const count = particleCount(1500);
- const positions = new Float32Array(count * 3), colors = new Float32Array(count * 3), sizes = new Float32Array(count);
- const c1 = new THREE.Color(NEXUS.colors.primary), c2 = new THREE.Color(NEXUS.colors.secondary), c3 = new THREE.Color(NEXUS.colors.gold);
- for (let i = 0; i < count; i++) {
- positions[i*3] = (Math.random()-0.5)*60; positions[i*3+1] = Math.random()*20; positions[i*3+2] = (Math.random()-0.5)*60;
- const t = Math.random(), col = t < 0.5 ? c1.clone().lerp(c2, t*2) : c2.clone().lerp(c3, (t-0.5)*2);
- colors[i*3] = col.r; colors[i*3+1] = col.g; colors[i*3+2] = col.b;
- sizes[i] = 0.02 + Math.random() * 0.06;
- }
- const geo = new THREE.BufferGeometry();
- geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
- geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
- geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
- particles = new THREE.Points(geo, new THREE.ShaderMaterial({
- uniforms: { uTime: { value: 0 } },
- vertexShader: `attribute float size;attribute vec3 color;varying vec3 vColor;uniform float uTime;void main(){vColor=color;vec3 p=position;p.y+=sin(uTime*0.5+position.x*0.5)*0.3;p.x+=sin(uTime*0.3+position.z*0.4)*0.2;vec4 mv=modelViewMatrix*vec4(p,1.0);gl_PointSize=size*300.0/-mv.z;gl_Position=projectionMatrix*mv;}`,
- fragmentShader: `varying vec3 vColor;void main(){float d=length(gl_PointCoord-0.5);if(d>0.5)discard;gl_FragColor=vec4(vColor,smoothstep(0.5,0.1,d)*0.7);}`,
- transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
- }));
- scene.add(particles);
-}
-
-function createDustParticles() {
- const count = particleCount(500), positions = new Float32Array(count * 3);
- for (let i = 0; i < count; i++) {
- positions[i*3] = (Math.random()-0.5)*40; positions[i*3+1] = Math.random()*15; positions[i*3+2] = (Math.random()-0.5)*40;
- }
- const geo = new THREE.BufferGeometry();
- geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
- dustParticles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x8899bb, size: 0.03, transparent: true, opacity: 0.3, depthWrite: false }));
- scene.add(dustParticles);
-}
-
-// ═══ AMBIENT STRUCTURES ═══
-function createAmbientStructures() {
- const crystalMat = new THREE.MeshPhysicalMaterial({ color: 0x3355aa, roughness: 0.1, metalness: 0.2, transmission: 0.6, thickness: 2, emissive: 0x1122aa, emissiveIntensity: 0.3 });
- [{ x:-18,z:-15,s:1.5,ry:0.3 },{x:-20,z:-10,s:1,ry:0.8},{x:-15,z:-18,s:2,ry:1.2},{x:18,z:-15,s:1.8,ry:2.1},{x:20,z:-12,s:1.2,ry:0.5},{x:-12,z:18,s:1.3,ry:1.8},{x:14,z:16,s:1.6,ry:0.9}]
- .forEach(p => {
- const c = new THREE.Mesh(new THREE.ConeGeometry(0.4*p.s, 2.5*p.s, 5), crystalMat.clone());
- c.position.set(p.x, 1.25*p.s, p.z); c.rotation.y = p.ry; c.rotation.z = (Math.random()-0.5)*0.3; c.castShadow = true;
- scene.add(c);
- });
-
- const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.6, 2), new THREE.MeshPhysicalMaterial({ color: 0x4af0c0, emissive: 0x4af0c0, emissiveIntensity: 2, roughness: 0, metalness: 1, transmission: 0.3, thickness: 1 }));
- core.position.set(0, 2.5, 0); core.name = 'nexus-core'; scene.add(core);
-
- const pedestal = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 1.2, 1.5, 8), new THREE.MeshStandardMaterial({ color: 0x0a0f1a, roughness: 0.4, metalness: 0.8, emissive: 0x1a2a4a, emissiveIntensity: 0.3 }));
- pedestal.position.set(0, 0.75, 0); pedestal.castShadow = true; scene.add(pedestal);
-}
-
-// ═══ NAVIGATION ═══
-function cycleNavMode() {
- navModeIdx = (navModeIdx + 1) % NAV_MODES.length;
- const mode = NAV_MODES[navModeIdx];
- if (mode === 'orbit') {
- const dir = new THREE.Vector3(0, 0, -1).applyEuler(playerRot);
- orbitState.target.copy(playerPos).addScaledVector(dir, orbitState.radius);
- orbitState.target.y = Math.max(0, orbitState.target.y);
- const toCamera = new THREE.Vector3().subVectors(playerPos, orbitState.target);
- orbitState.radius = toCamera.length();
- orbitState.theta = Math.atan2(toCamera.x, toCamera.z);
- orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius)));
- }
- if (mode === 'fly') flyY = playerPos.y;
- const el = document.getElementById('nav-mode-label'); if (el) el.textContent = mode.toUpperCase();
-}
-
-// ═══ CONTROLS ═══
-function setupControls() {
- const chatInput = document.getElementById('chat-input');
- document.addEventListener('keydown', (e) => {
- keys[e.key.toLowerCase()] = true;
- if (e.key === 'Enter') { e.preventDefault(); document.activeElement === chatInput ? sendChatMessage() : chatInput.focus(); }
- if (e.key === 'Escape') { chatInput.blur(); if (portalOverlayActive) closePortalOverlay(); if (visionOverlayActive) closeVisionOverlay(); }
- if (e.key.toLowerCase() === 'v' && document.activeElement !== chatInput) cycleNavMode();
- if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) activatePortal(activePortal);
- if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) activateVisionPoint(activeVisionPoint);
- });
- document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });
-
- const canvas = document.getElementById('nexus-canvas');
- canvas.addEventListener('mousedown', (e) => {
- if (e.target !== canvas) return;
- mouseDown = true; orbitState.lastX = e.clientX; orbitState.lastY = e.clientY;
- if (!portalOverlayActive) {
- const mouse = new THREE.Vector2((e.clientX/window.innerWidth)*2-1, -(e.clientY/window.innerHeight)*2+1);
- const rc = new THREE.Raycaster(); rc.setFromCamera(mouse, camera);
- const hit = rc.intersectObjects(portals.map(p => p.ring));
- if (hit.length) { const p = portals.find(p => p.ring === hit[0].object); if (p) activatePortal(p); }
- }
- });
- document.addEventListener('mouseup', () => { mouseDown = false; });
- document.addEventListener('mousemove', (e) => {
- if (!mouseDown || document.activeElement === chatInput) return;
- if (NAV_MODES[navModeIdx] === 'orbit') {
- orbitState.theta -= (e.clientX - orbitState.lastX) * 0.005;
- orbitState.phi = Math.max(0.05, Math.min(Math.PI * 0.85, orbitState.phi + (e.clientY - orbitState.lastY) * 0.005));
- orbitState.lastX = e.clientX; orbitState.lastY = e.clientY;
- } else {
- playerRot.y -= e.movementX * 0.003;
- playerRot.x = Math.max(-Math.PI/3, Math.min(Math.PI/3, playerRot.x - e.movementY * 0.003));
- }
- });
- canvas.addEventListener('wheel', (e) => {
- if (NAV_MODES[navModeIdx] === 'orbit') orbitState.radius = Math.max(orbitState.minR, Math.min(orbitState.maxR, orbitState.radius + e.deltaY * 0.02));
- }, { passive: true });
-
- document.getElementById('chat-toggle').addEventListener('click', () => { chatOpen = !chatOpen; document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen); });
- document.getElementById('chat-send').addEventListener('click', sendChatMessage);
- document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
- document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
-}
-
-// ═══ CHAT ═══
-function sendChatMessage() {
- const input = document.getElementById('chat-input');
- const text = input.value.trim();
- if (!text) return;
- addChatMessage('user', text);
- input.value = '';
-
- const timmy = agents.find(a => a.id === 'timmy');
- if (wsConnected) {
- wsSend({ type: 'chat_message', text, sender: 'Alexander' });
- if (timmy) { timmy.activityLocked = true; setAgentActivity(timmy, ACTIVITY_STATES.THINKING); }
- } else {
- addChatMessage('system', 'OFFLINE — Timmy is not connected. Start gateway: ws://hermes:8765');
- if (timmy) { setAgentActivity(timmy, ACTIVITY_STATES.NONE); timmy.activityLocked = false; }
- }
- input.blur();
-}
-
-function addChatMessage(type, text) {
- const container = document.getElementById('chat-messages');
- const div = document.createElement('div');
- div.className = `chat-msg chat-msg-${type}`;
- const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
- div.innerHTML = `${prefixes[type] || '[???]'} ${text}`;
- container.appendChild(div);
- container.scrollTop = container.scrollHeight;
-}
-
-// ═══ PORTAL INTERACTION ═══
-function checkPortalProximity() {
- if (portalOverlayActive) return;
- let closest = null, minDist = Infinity;
- portals.forEach(p => { const d = playerPos.distanceTo(p.group.position); if (d < 4.5 && d < minDist) { minDist = d; closest = p; } });
- activePortal = closest;
- const hint = document.getElementById('portal-hint');
- if (activePortal) { document.getElementById('portal-hint-name').textContent = activePortal.config.name; hint.style.display = 'flex'; }
- else hint.style.display = 'none';
-}
-
-function activatePortal(portal) {
- portalOverlayActive = true;
- document.getElementById('portal-name-display').textContent = portal.config.name.toUpperCase();
- document.getElementById('portal-desc-display').textContent = portal.config.description;
- const dot = document.getElementById('portal-status-dot');
- dot.style.background = portal.config.color; dot.style.boxShadow = `0 0 10px ${portal.config.color}`;
- document.getElementById('portal-overlay').style.display = 'flex';
-
- if (portal.config.destination?.url) {
- document.getElementById('portal-redirect-box').style.display = 'block';
- document.getElementById('portal-error-box').style.display = 'none';
- let count = 5;
- document.getElementById('portal-timer').textContent = count;
- const iv = setInterval(() => {
- if (--count <= 0) { clearInterval(iv); if (portalOverlayActive) window.location.href = portal.config.destination.url; }
- if (!portalOverlayActive) clearInterval(iv);
- document.getElementById('portal-timer').textContent = count;
- }, 1000);
- } else {
- document.getElementById('portal-redirect-box').style.display = 'none';
- document.getElementById('portal-error-box').style.display = 'block';
- }
-}
-
-function closePortalOverlay() { portalOverlayActive = false; document.getElementById('portal-overlay').style.display = 'none'; }
-
-// ═══ VISION INTERACTION ═══
-function checkVisionProximity() {
- if (visionOverlayActive) return;
- let closest = null, minDist = Infinity;
- visionPoints.forEach(vp => { const d = playerPos.distanceTo(vp.group.position); if (d < 3.5 && d < minDist) { minDist = d; closest = vp; } });
- activeVisionPoint = closest;
- const hint = document.getElementById('vision-hint');
- if (activeVisionPoint) { document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title; hint.style.display = 'flex'; }
- else hint.style.display = 'none';
-}
-
-function activateVisionPoint(vp) {
- visionOverlayActive = true;
- document.getElementById('vision-title-display').textContent = vp.config.title.toUpperCase();
- document.getElementById('vision-content-display').textContent = vp.config.content;
- const dot = document.getElementById('vision-status-dot');
- dot.style.background = vp.config.color; dot.style.boxShadow = `0 0 10px ${vp.config.color}`;
- document.getElementById('vision-overlay').style.display = 'flex';
-}
-
-function closeVisionOverlay() { visionOverlayActive = false; document.getElementById('vision-overlay').style.display = 'none'; }
-
-// ═══ GAME LOOP ═══
-let pulseTimer = 0;
-
-function gameLoop() {
- requestAnimationFrame(gameLoop);
- const delta = Math.min(clock.getDelta(), 0.1), elapsed = clock.elapsedTime;
-
- // Harness Pulse
- pulseTimer += delta;
- if (pulseTimer > 8) { pulseTimer = 0; triggerHarnessPulse(); }
- if (harnessPulseMesh) { harnessPulseMesh.scale.addScalar(delta * 15); harnessPulseMesh.material.opacity = Math.max(0, harnessPulseMesh.material.opacity - delta * 0.5); }
-
- // Navigation
- const mode = NAV_MODES[navModeIdx];
- const chatActive = document.activeElement === document.getElementById('chat-input');
-
- if (mode === 'walk') {
- if (!chatActive && !portalOverlayActive) {
- const speed = 6 * delta, dir = new THREE.Vector3();
- if (keys['w']) dir.z -= 1; if (keys['s']) dir.z += 1; if (keys['a']) dir.x -= 1; if (keys['d']) dir.x += 1;
- if (dir.length() > 0) { dir.normalize().multiplyScalar(speed).applyAxisAngle(new THREE.Vector3(0,1,0), playerRot.y); playerPos.add(dir); const d = Math.sqrt(playerPos.x**2+playerPos.z**2); if (d > 24) { playerPos.x *= 24/d; playerPos.z *= 24/d; } }
- }
- playerPos.y = 2; camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
- } else if (mode === 'orbit') {
- if (!chatActive && !portalOverlayActive) {
- const speed = 8 * delta, pan = new THREE.Vector3();
- if (keys['w']) pan.z -= 1; if (keys['s']) pan.z += 1; if (keys['a']) pan.x -= 1; if (keys['d']) pan.x += 1;
- if (pan.length() > 0) { pan.normalize().multiplyScalar(speed).applyAxisAngle(new THREE.Vector3(0,1,0), orbitState.theta); orbitState.target.add(pan); orbitState.target.y = Math.max(0, Math.min(20, orbitState.target.y)); }
- }
- const r = orbitState.radius;
- camera.position.set(orbitState.target.x+r*Math.sin(orbitState.phi)*Math.sin(orbitState.theta), orbitState.target.y+r*Math.cos(orbitState.phi), orbitState.target.z+r*Math.sin(orbitState.phi)*Math.cos(orbitState.theta));
- camera.lookAt(orbitState.target); playerPos.copy(camera.position); playerRot.y = orbitState.theta;
- } else if (mode === 'fly') {
- if (!chatActive && !portalOverlayActive) {
- const speed = 8 * delta;
- const fwd = new THREE.Vector3(-Math.sin(playerRot.y), 0, -Math.cos(playerRot.y));
- const right = new THREE.Vector3(Math.cos(playerRot.y), 0, -Math.sin(playerRot.y));
- if (keys['w']) playerPos.addScaledVector(fwd, speed); if (keys['s']) playerPos.addScaledVector(fwd, -speed);
- if (keys['a']) playerPos.addScaledVector(right, -speed); if (keys['d']) playerPos.addScaledVector(right, speed);
- if (keys['q'] || keys[' ']) flyY += speed; if (keys['e'] || keys['shift']) flyY -= speed;
- flyY = Math.max(0.5, Math.min(30, flyY)); playerPos.y = flyY;
- }
- camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
- }
-
- checkPortalProximity();
- checkVisionProximity();
-
- // Animate scene
- const sky = scene.getObjectByName('skybox');
- if (sky) sky.material.uniforms.uTime.value = elapsed;
- if (heatmapMat) heatmapMat.opacity = 0.75 + Math.sin(elapsed * 0.6) * 0.2;
- batcaveTerminals.forEach(t => { if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed; });
-
- // Portals
- portals.forEach(p => {
- p.ring.rotation.z = elapsed * 0.3; p.ring.rotation.x = Math.sin(elapsed * 0.5) * 0.1;
- if (p.swirl.material.uniforms) p.swirl.material.uniforms.uTime.value = elapsed;
- p.light.intensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
- const pos = p.pSystem.geometry.attributes.position.array;
- for (let i = 0; i < pos.length / 3; i++) pos[i*3+1] += Math.sin(elapsed + i) * 0.002;
- p.pSystem.geometry.attributes.position.needsUpdate = true;
- p.runes.forEach((rune, i) => { rune.position.y = 4 + Math.sin(elapsed*2+i*0.5)*0.2; rune.rotation.z = elapsed*0.8+i; });
- });
-
- // Vision points
- visionPoints.forEach(vp => {
- vp.crystal.rotation.y = elapsed * 0.8; vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2;
- vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2;
- vp.ring.rotation.z = elapsed * 0.5; vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05);
- vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3;
- });
-
- // Agents
- updateAgents(elapsed, delta);
-
- // Dual-brain float
- if (dualBrainGroup) {
- dualBrainGroup.position.y = 3 + Math.sin(elapsed * 0.22) * 0.15;
- if (cloudOrb) { cloudOrb.position.y = 3 + Math.sin(elapsed*1.3)*0.15; cloudOrb.rotation.y = elapsed*0.4; }
- if (localOrb) { localOrb.position.y = 3 + Math.sin(elapsed*1.3+Math.PI)*0.15; localOrb.rotation.y = -elapsed*0.4; }
- if (dualBrainLight) dualBrainLight.intensity = 0.4 + Math.sin(elapsed*1.5)*0.2;
- }
-
- // Power meter
- powerMeterBars.forEach((bar, i) => {
- const level = Math.sin(elapsed*2+i*0.5)*0.5+0.5, active = level > i/powerMeterBars.length;
- bar.material.emissiveIntensity = active ? 2 : 0.2; bar.material.opacity = active ? 0.9 : 0.3; bar.scale.x = active ? 1.2 : 1;
- });
-
- if (voidLight) voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2;
- if (thoughtStreamMesh) { thoughtStreamMesh.material.uniforms.uTime.value = elapsed; thoughtStreamMesh.rotation.y = elapsed * 0.05; }
- if (particles?.material?.uniforms) particles.material.uniforms.uTime.value = elapsed;
- if (dustParticles) dustParticles.rotation.y = elapsed * 0.01;
-
- const core = scene.getObjectByName('nexus-core');
- if (core) { core.position.y = 2.5 + Math.sin(elapsed*1.2)*0.3; core.rotation.y = elapsed*0.4; core.rotation.x = elapsed*0.2; core.material.emissiveIntensity = 1.5 + Math.sin(elapsed*2)*0.5; }
-
- composer.render();
-
- frameCount++;
- const now = performance.now();
- if (now - lastFPSTime >= 1000) { fps = frameCount; frameCount = 0; lastFPSTime = now; }
- if (debugOverlay) {
- const info = renderer.info;
- debugOverlay.textContent = `FPS: ${fps} Draw: ${info.render?.calls} Tri: ${info.render?.triangles} [${performanceTier}] WS: ${wsConnected ? 'ON' : 'OFF'}\nPos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`;
- }
- renderer.info.reset();
-}
-
-function onResize() {
- const w = window.innerWidth, h = window.innerHeight;
- camera.aspect = w / h; camera.updateProjectionMatrix();
- renderer.setSize(w, h); composer.setSize(w, h);
-}
-
-// ═══ AGENT ANIMATION ═══
-function updateAgents(elapsed, delta) {
- const ATTENTION_RADIUS = 7;
- agents.forEach((agent, i) => {
- const station = new THREE.Vector3(agent.station.x, 0, agent.station.z);
- const toPlayer = new THREE.Vector3(playerPos.x - agent.group.position.x, 0, playerPos.z - agent.group.position.z);
- const playerNearby = toPlayer.length() < ATTENTION_RADIUS && !agent.activityLocked;
-
- if (playerNearby) {
- const ta = Math.atan2(toPlayer.x, toPlayer.z);
- agent.group.rotation.y += ((ta - agent.group.rotation.y + Math.PI*3) % (Math.PI*2) - Math.PI) * Math.min(delta*3, 1);
- }
-
- if (!playerNearby && !agent.activityLocked) {
- agent.stateTimer -= delta;
- if (agent.stateTimer <= 0) {
- agent.state = pickNextState();
- agent.stateTimer = agent.state === 'IDLE' ? 4+Math.random()*6 : agent.state === 'PACING' ? 8+Math.random()*6 : 4+Math.random()*4;
- if (agent.state === 'PACING') agent.pacingIdx = 0;
- if (agent.state === 'LOOKING') agent.lookAngle = agent.group.rotation.y;
- if (agent.state !== 'PACING') agent.targetPos.copy(station);
- }
- if (agent.state === 'PACING') {
- const wp = agent.pacingPath[agent.pacingIdx];
- const toWp = new THREE.Vector3(wp.x - agent.group.position.x, 0, wp.z - agent.group.position.z);
- if (toWp.length() < 0.3) agent.pacingIdx = (agent.pacingIdx + 1) % agent.pacingPath.length;
- else { agent.group.position.addScaledVector(toWp.normalize(), delta*1.2); agent.group.rotation.y += (Math.atan2(toWp.x,toWp.z)-agent.group.rotation.y)*Math.min(delta*4,1); }
- } else if (agent.state === 'READING') {
- const tt = new THREE.Vector3(-agent.group.position.x, 0, -8-agent.group.position.z);
- agent.group.rotation.y += (Math.atan2(tt.x,tt.z)-agent.group.rotation.y)*Math.min(delta*2,1);
- agent.group.position.lerp(agent.targetPos, delta*0.4);
- } else if (agent.state === 'LOOKING') {
- agent.lookAngle += Math.sin(elapsed*agent.lookSpeed+i)*delta*0.8;
- agent.group.rotation.y += (agent.lookAngle-agent.group.rotation.y)*Math.min(delta*1.5,1);
- agent.group.position.lerp(agent.targetPos, delta*0.3);
- } else {
- agent.group.position.lerp(agent.targetPos, delta*0.3);
- }
- }
-
- // Orb & halo
- const bob = agent.activityState === ACTIVITY_STATES.THINKING ? 0.25 : 0.15;
- agent.orb.position.y = 3 + Math.sin(elapsed*2+i)*bob;
- agent.halo.rotation.z = elapsed * 0.5; agent.halo.scale.setScalar(1 + Math.sin(elapsed*3+i)*0.1);
- agent.orb.material.emissiveIntensity = (agent.activityState === ACTIVITY_STATES.NONE ? 2 : 3) + Math.sin(elapsed*4+i);
-
- // Activity indicators
- if (agent.activityState !== ACTIVITY_STATES.NONE) {
- agent.indicator.group.position.y = 4.2 + Math.sin(elapsed*2+i*1.3)*0.1;
- if (agent.activityState === ACTIVITY_STATES.WAITING) { const p = 0.7+Math.sin(elapsed*4+i)*0.3; agent.indicator.waitMesh.scale.setScalar(p); agent.indicator.waitMesh.material.opacity = 0.5+p*0.35; }
- else if (agent.activityState === ACTIVITY_STATES.THINKING) { agent.indicator.thinkMesh.rotation.y = elapsed*2.5; agent.indicator.thinkMesh.rotation.x = elapsed*1.5; }
- else if (agent.activityState === ACTIVITY_STATES.PROCESSING) { agent.indicator.procMesh.rotation.z = elapsed*4; agent.indicator.procMesh.rotation.x = Math.sin(elapsed*1.2)*0.5; }
- const tc = new THREE.Vector3(camera.position.x-agent.group.position.x, 0, camera.position.z-agent.group.position.z);
- if (tc.length() > 0.01) agent.indicator.group.rotation.y = Math.atan2(tc.x, tc.z);
- }
- });
-}
-
-function addAgentLog(agentId, text) {
- const container = document.getElementById('agent-log-content');
- if (!container) return;
- const entry = document.createElement('div');
- entry.className = 'agent-log-entry';
- entry.innerHTML = `[${agentId.toUpperCase()}]${text}`;
- container.prepend(entry);
- if (container.children.length > 6) container.lastElementChild.remove();
-}
-
-function triggerHarnessPulse() {
- if (!harnessPulseMesh) return;
- harnessPulseMesh.scale.setScalar(0.1); harnessPulseMesh.material.opacity = 0.8;
- const core = scene.getObjectByName('nexus-core');
- if (core) { core.material.emissiveIntensity = 10; setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 200); }
-}
-
-// ═══ BITCOIN BLOCK HEIGHT ═══
-(function initBitcoin() {
- const display = document.getElementById('block-height-display');
- const value = document.getElementById('block-height-value');
- if (!display || !value) return;
- let lastHeight = null;
- async function fetch_() {
- try {
- const res = await fetch('https://blockstream.info/api/blocks/tip/height');
- if (!res.ok) return;
- const h = parseInt(await res.text(), 10);
- if (isNaN(h)) return;
- if (lastHeight !== null && h !== lastHeight) { display.classList.remove('fresh'); void display.offsetWidth; display.classList.add('fresh'); }
- lastHeight = h; value.textContent = h.toLocaleString();
- } catch (_) {}
- }
- fetch_(); setInterval(fetch_, 60000);
-})();
-
-init();