Files
the-nexus/app.js
perplexity 95793222ce
Some checks failed
CI / validate (pull_request) Failing after 5s
feat: WebSocket bridge to Timmy + trim fat (2211→1181 lines)
Adds real-time WebSocket connection to Timmy's backend:
- Agent behaviors driven by live cognitive state
- Chat routed to real Timmy (not fake responses)
- Dual-brain panel updates from WS
- Graceful degradation when offline

Trimmed 1030 lines (47% reduction):
- Simplified glass floor (removed 6-band edge system)
- Compacted dual-brain panel (removed per-frame scan canvas)
- Removed simulateAgentThought() (WS replaces it)
- Removed fake chat responses
- Compacted all functions

Refs #8
2026-03-25 16:30:08 +00:00

1182 lines
57 KiB
JavaScript

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';
// ═══════════════════════════════════════════
// 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';
// ═══ 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);
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 = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${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 = `<span class="agent-log-tag tag-${agentId}">[${agentId.toUpperCase()}]</span><span class="agent-log-text">${text}</span>`;
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();