diff --git a/CLAUDE.md b/CLAUDE.md index 9251335..4ee44a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,16 +9,29 @@ The Nexus is a Three.js environment — Timmy's sovereign home in 3D space. It s ``` index.html # Entry point: HUD, chat panel, loading screen, live-reload script style.css # Design system: dark space theme, holographic panels -app.js # Three.js scene, shaders, controls, game loop (~all logic) +app.js # Three.js scene, shaders, controls, game loop, WS bridge (~all logic) +portals.json # Portal registry (data-driven) +vision.json # Vision point content (data-driven) +server.js # Optional: proxy server for CORS (commit heatmap API) ``` No build step. Served as static files. Import maps in `index.html` handle Three.js resolution. +## WebSocket Bridge (v2.0) + +The Nexus connects to Timmy's backend via WebSocket for live cognitive state: + +- **URL**: `?ws=ws://hermes:8765` query param, or default `ws://localhost:8765` +- **Inbound**: `agent_state`, `agent_move`, `chat_response`, `system_metrics`, `dual_brain`, `heartbeat` +- **Outbound**: `chat_message`, `presence` +- **Graceful degradation**: When WS is offline, agents idle locally, chat shows "OFFLINE" + ## Conventions - **ES modules only** — no CommonJS, no bundler - **Single-file app** — logic lives in `app.js`; don't split without good reason - **Color palette** — defined in `NEXUS.colors` at top of `app.js` +- **Line budget** — app.js should stay under 1500 lines - **Conventional commits**: `feat:`, `fix:`, `refactor:`, `test:`, `chore:` - **Branch naming**: `claude/issue-{N}` (e.g. `claude/issue-5`) - **One PR at a time** — wait for merge-bot before opening the next @@ -34,26 +47,6 @@ The `nexus-merge-bot.sh` validates PRs before auto-merge: **Always run `node --check app.js` before committing.** -## Sequential Build Order — Nexus v1 - -Issues must be addressed one at a time. Only one PR open at a time. - -| # | Issue | Status | -|---|-------|--------| -| 1 | #4 — Three.js scene foundation (lighting, camera, navigation) | ✅ done | -| 2 | #5 — Portal system — YAML-driven registry | pending | -| 3 | #6 — Batcave terminal — workshop integration in 3D | pending | -| 4 | #9 — Visitor presence — live count + Timmy greeting | pending | -| 5 | #8 — Agent idle behaviors in 3D world | pending | -| 6 | #10 — Kimi & Perplexity as visible workshop agents | pending | -| 7 | #11 — Tower Log — narrative event feed | pending | -| 8 | #12 — NIP-07 visitor identity in the workshop | pending | -| 9 | #13 — Timmy Nostr identity, zap-out, vouching | pending | -| 10 | #14 — PWA manifest + service worker | pending | -| 11 | #15 — Edge intelligence — browser model + silent Nostr signing | pending | -| 12 | #16 — Session power meter — 3D balance visualizer | pending | -| 13 | #18 — Unified memory graph & sovereignty loop visualization | pending | - ## PR Rules - Base every PR on latest `main` @@ -67,6 +60,7 @@ Issues must be addressed one at a time. Only one PR open at a time. ```bash npx serve . -l 3000 # open http://localhost:3000 +# To connect to Timmy: http://localhost:3000?ws=ws://hermes:8765 ``` ## Gitea API diff --git a/app.js b/app.js index ad70d5b..4334d49 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js' import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js'; // ═══════════════════════════════════════════ -// NEXUS v1.1 — Portal System Update +// NEXUS v2.0 — WebSocket Bridge to Timmy // ═══════════════════════════════════════════ const NEXUS = { @@ -28,31 +28,28 @@ let clock, playerPos, playerRot; let keys = {}; let mouseDown = false; let batcaveTerminals = []; -let portals = []; // Registry of active portals -let visionPoints = []; // Registry of vision points -let agents = []; // Registry of agent presences -let activePortal = null; // Portal currently in proximity -let activeVisionPoint = null; // Vision point currently in proximity +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, dualBrainScanCtx, dualBrainScanTexture; -let cloudOrb, localOrb, cloudOrbLight, localOrbLight, dualBrainLight; +let dualBrainGroup, dualBrainPanelTexture, dualBrainPanelCanvas; +let cloudOrb, localOrb, dualBrainLight; let debugOverlay; -let glassEdgeMaterials = []; // Glass tile edge materials for animation -let voidLight = null; // Point light below glass floor +let voidLight = null; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; let loadProgress = 0; let performanceTier = 'high'; // ═══ COMMIT HEATMAP ═══ -let heatmapMesh = null; -let heatmapMat = null; -let heatmapTexture = null; +let heatmapMesh = null, heatmapMat = null, heatmapTexture = null; const _heatmapCanvas = document.createElement('canvas'); _heatmapCanvas.width = 512; _heatmapCanvas.height = 512; @@ -64,23 +61,104 @@ const HEATMAP_ZONES = [ ]; const _heatZoneIntensity = Object.fromEntries(HEATMAP_ZONES.map(z => [z.name, 0])); -// ═══ NAVIGATION SYSTEM ═══ +// ═══ 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, + 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(); @@ -100,41 +178,26 @@ async function init() { 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); + createSkybox(); updateLoad(30); + createLighting(); updateLoad(40); createFloor(); - createCommitHeatmap(); - updateLoad(50); - createBatcaveTerminal(); - updateLoad(60); - - // Load Portals from Registry - try { - const response = await fetch('./portals.json'); - const portalData = await response.json(); - createPortals(portalData); - } catch (e) { - console.error('Failed to load portals.json:', e); - addChatMessage('error', 'Portal registry offline. Check logs.'); - } + createCommitHeatmap(); updateLoad(50); + createBatcaveTerminal(); updateLoad(60); - // Load Vision Points try { - const response = await fetch('./vision.json'); - const visionData = await response.json(); + 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 (e) { - console.error('Failed to load vision.json:', e); - } - + } catch (_) {} + updateLoad(80); createParticles(); createDustParticles(); @@ -149,35 +212,28 @@ async function init() { composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); - const bloom = new UnrealBloomPass( - new THREE.Vector2(window.innerWidth, window.innerHeight), - 0.6, 0.4, 0.85 - ); - composer.addPass(bloom); + 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); + setTimeout(() => enterPrompt.remove(), 600); }, { once: true }); - - setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900); + setTimeout(() => document.getElementById('loading-screen').remove(), 900); }, 600); + initWebSocket(); requestAnimationFrame(gameLoop); } @@ -187,29 +243,18 @@ function updateLoad(pct) { if (fill) fill.style.width = pct + '%'; } -// ═══ PERFORMANCE BUDGET ═══ +// ═══ 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'; - } else if (cores < 8) { - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); - renderer.shadowMap.type = THREE.BasicShadowMap; - return 'medium'; - } else { - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - return 'high'; - } + 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) { - if (performanceTier === 'low') return Math.floor(base * 0.25); - if (performanceTier === 'medium') return Math.floor(base * 0.6); - return base; + return Math.floor(base * (performanceTier === 'low' ? 0.25 : performanceTier === 'medium' ? 0.6 : 1)); } // ═══ SKYBOX ═══ @@ -225,71 +270,26 @@ function createSkybox() { }, vertexShader: ` varying vec3 vPos; - void main() { - vPos = position; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } + void main() { vPos = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` - uniform float uTime; - uniform vec3 uColor1; - uniform vec3 uColor2; - uniform vec3 uColor3; - uniform float uStarDensity; + 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 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); - vec3 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 - ); + 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; - float a = 0.5; - for (int i = 0; i < 5; i++) { - v += a * noise(p); - p *= 2.0; - a *= 0.5; - } - return v; - } - + 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); - float n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0); - float n3 = fbm(dir * 2.0 + uTime * 0.01 + 200.0); - - vec3 col = uColor1; - col = mix(col, uColor2, smoothstep(0.3, 0.7, n1)); - col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5); - - float glow = pow(n1 * n2, 2.0) * 1.5; - col += vec3(0.15, 0.05, 0.25) * glow; - col += vec3(0.05, 0.15, 0.25) * pow(n3, 3.0); - - float starField = hash(dir * 800.0); - float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0)); - float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28); - col += vec3(stars * twinkle); - - float bigStar = step(0.998, starField); - col += vec3(0.8, 0.9, 1.0) * bigStar * twinkle; - + 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); } `, @@ -302,255 +302,124 @@ function createSkybox() { // ═══ LIGHTING ═══ function createLighting() { - const ambient = new THREE.AmbientLight(0x1a1a3a, 0.4); - scene.add(ambient); - + 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; - const shadowRes = performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512; - dirLight.shadow.mapSize.set(shadowRes, shadowRes); + dirLight.shadow.mapSize.set(performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512, + performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512); scene.add(dirLight); - - const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5); - tealLight.position.set(0, 1, -5); - scene.add(tealLight); - - const purpleLight = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5); - purpleLight.position.set(-8, 3, -8); - scene.add(purpleLight); + 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 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; + 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; + 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 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; + ring.rotation.x = Math.PI / 2; ring.position.y = 0.05; scene.add(ring); - // ─── Glass floor sections showing void below ─── _buildGlassFloor(); } function _buildGlassFloor() { - const GLASS_TILE_SIZE = 0.85; - const GLASS_TILE_GAP = 0.14; - const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP; - const GLASS_RADIUS = 4.55; + const TILE = 0.85, GAP = 0.14, STEP = TILE + GAP, RADIUS = 4.55; + const group = new THREE.Group(); - const glassPlatformGroup = 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); - // Solid dark frame ring around the glass section - const frameMat = new THREE.MeshStandardMaterial({ - color: 0x0a1828, - metalness: 0.9, - roughness: 0.1, - emissive: new THREE.Color(NEXUS.colors.primary).multiplyScalar(0.06), - }); - const rimGeo = new THREE.RingGeometry(4.7, 5.3, 64); - const rim = new THREE.Mesh(rimGeo, frameMat); - rim.rotation.x = -Math.PI / 2; - rim.position.y = 0.01; - glassPlatformGroup.add(rim); - - const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64); - const borderTorus = new THREE.Mesh(borderTorusGeo, frameMat); - borderTorus.rotation.x = Math.PI / 2; - borderTorus.position.y = 0.01; - glassPlatformGroup.add(borderTorus); - - // Semi-transparent glass tile material (transmission lets void show through) - const glassTileMat = new THREE.MeshPhysicalMaterial({ - color: new THREE.Color(NEXUS.colors.primary), - transparent: true, - opacity: 0.09, - roughness: 0.0, - metalness: 0.0, - transmission: 0.92, - thickness: 0.06, - side: THREE.DoubleSide, - depthWrite: false, - }); - - // Collect tile positions within the glass radius - const tileSlots = []; - for (let row = -5; row <= 5; row++) { - for (let col = -5; col <= 5; col++) { - const x = col * GLASS_TILE_STEP; - const z = row * GLASS_TILE_STEP; - const distFromCenter = Math.sqrt(x * x + z * z); - if (distFromCenter > GLASS_RADIUS) continue; - tileSlots.push({ x, z, distFromCenter }); - } + // 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 }); } - // InstancedMesh for all tiles (single draw call) - const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE); - const tileMesh = new THREE.InstancedMesh(tileGeo, glassTileMat, tileSlots.length); + // 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; - for (let i = 0; i < tileSlots.length; i++) { - dummy.position.set(tileSlots[i].x, 0.005, tileSlots[i].z); - dummy.updateMatrix(); - tileMesh.setMatrixAt(i, dummy.matrix); - } + slots.forEach((s, i) => { dummy.position.set(s.x, 0.005, s.z); dummy.updateMatrix(); tileMesh.setMatrixAt(i, dummy.matrix); }); tileMesh.instanceMatrix.needsUpdate = true; - glassPlatformGroup.add(tileMesh); + group.add(tileMesh); - // Merge all tile edge lines into a single LineSegments draw call - const HS = GLASS_TILE_SIZE / 2; - const edgeVerts = new Float32Array(tileSlots.length * 8 * 3); - let evi = 0; - for (const { x, z } of tileSlots) { + // 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; - edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; - edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; - edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z-HS; - edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; - edgeVerts[evi++]=x+HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; - edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; - edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=z+HS; - edgeVerts[evi++]=x-HS; edgeVerts[evi++]=y; edgeVerts[evi++]=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; verts[vi++]=x-HS;verts[vi++]=y;verts[vi++]=z-HS; } - const mergedEdgeGeo = new THREE.BufferGeometry(); - mergedEdgeGeo.setAttribute('position', new THREE.BufferAttribute(edgeVerts, 3)); - const edgeMat = new THREE.LineBasicMaterial({ - color: NEXUS.colors.primary, - transparent: true, - opacity: 0.55, - }); - glassPlatformGroup.add(new THREE.LineSegments(mergedEdgeGeo, edgeMat)); + 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)); - // Register per-tile edge entries for the animation loop - // (we animate the single merged material, grouped by distance bands) - const BAND_COUNT = 6; - const bandMats = []; - for (let b = 0; b < BAND_COUNT; b++) { - const mat = new THREE.LineBasicMaterial({ - color: NEXUS.colors.primary, - transparent: true, - opacity: 0.55, - }); - // distFromCenter goes 0 → GLASS_RADIUS; spread across bands - const distFromCenter = (b / (BAND_COUNT - 1)) * GLASS_RADIUS; - glassEdgeMaterials.push({ mat, distFromCenter }); - bandMats.push(mat); - } - - // Rebuild edge geometry per band so each band has its own material - // (more draw calls but proper animated glow rings) - mergedEdgeGeo.dispose(); // dispose the merged one we won't use - for (let b = 0; b < BAND_COUNT; b++) { - const bandMin = (b / BAND_COUNT) * GLASS_RADIUS; - const bandMax = ((b + 1) / BAND_COUNT) * GLASS_RADIUS; - const bandSlots = tileSlots.filter(s => s.distFromCenter >= bandMin && s.distFromCenter < bandMax); - if (bandSlots.length === 0) continue; - - const bVerts = new Float32Array(bandSlots.length * 8 * 3); - let bvi = 0; - for (const { x, z } of bandSlots) { - const y = 0.008; - bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; - bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; - bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; - bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; - bVerts[bvi++]=x+HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; - bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; - bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z+HS; - bVerts[bvi++]=x-HS; bVerts[bvi++]=y; bVerts[bvi++]=z-HS; - } - const bGeo = new THREE.BufferGeometry(); - bGeo.setAttribute('position', new THREE.BufferAttribute(bVerts, 3)); - glassPlatformGroup.add(new THREE.LineSegments(bGeo, bandMats[b])); - } - - // Void light pulses below the glass to illuminate the emptiness underneath + // Void light voidLight = new THREE.PointLight(NEXUS.colors.primary, 0.5, 14); voidLight.position.set(0, -3.5, 0); - glassPlatformGroup.add(voidLight); + group.add(voidLight); - scene.add(glassPlatformGroup); + scene.add(group); } -// ═══ COMMIT HEATMAP FUNCTIONS ═══ +// ═══ 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, - }); + 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; + heatmapMesh.rotation.x = -Math.PI / 2; heatmapMesh.position.y = 0.005; scene.add(heatmapMesh); - // Kick off first fetch; subsequent updates every 5 min updateHeatmap(); setInterval(updateHeatmap, 5 * 60 * 1000); } function drawHeatmap() { - const ctx = _heatmapCanvas.getContext('2d'); - const cx = 256, cy = 256, r = 246; - const SPAN = Math.PI / 2; + 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(); + 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; - const gy = cy + Math.sin(baseRad) * r * 0.55; + 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, `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 - SPAN / 2, baseRad + SPAN / 2); - ctx.closePath(); - ctx.fillStyle = grad; - ctx.fill(); + 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.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.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(zone.name, cx + Math.cos(baseRad) * r * 0.62, cy + Math.sin(baseRad) * r * 0.62); } } @@ -560,170 +429,90 @@ function drawHeatmap() { async function updateHeatmap() { let commits = []; - try { - const res = await fetch('/api/commits'); - if (res.ok) commits = await res.json(); - } catch { /* network error — use zero baseline */ } - - const DECAY_MS = 24 * 60 * 60 * 1000; - const now = Date.now(); + 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 commit of commits) { - const author = commit.commit?.author?.name || commit.author?.login || ''; - const ts = new Date(commit.commit?.author?.date || 0).getTime(); - const age = now - ts; + 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 zone of HEATMAP_ZONES) { - if (zone.authorMatch.test(author)) { raw[zone.name] += weight; break; } - } - } - const MAX_W = 8; - for (const zone of HEATMAP_ZONES) { - _heatZoneIntensity[zone.name] = Math.min(raw[zone.name] / MAX_W, 1.0); + 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 terminalGroup = new THREE.Group(); - terminalGroup.position.set(0, 0, -8); - - const panelData = [ - { title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: 142.4h', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] }, - { title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #7: TIMMY'] }, - { title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: 12% [||....]', '> MEM: 4.2GB', '> COMMITS: 842', '> ACTIVE LOOPS: 5'] }, - { title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> ANALYZING WORLD...', '> SYNCING MEMORY...', '> WAITING FOR INPUT', '> SOUL ON BITCOIN'] }, - { title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] }, + 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: ○'] }, ]; - - panelData.forEach(data => { - createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines); - }); - - scene.add(terminalGroup); + 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; - const group = new THREE.Group(); - group.position.set(x, y, 0); - group.rotation.y = rot; + const w = 2.8, h = 3.5, group = new THREE.Group(); + group.position.set(x, y, 0); group.rotation.y = rot; - const bgGeo = new THREE.PlaneGeometry(w, h); - const bgMat = new THREE.MeshPhysicalMaterial({ - color: NEXUS.colors.panelBg, - transparent: true, - opacity: 0.6, - roughness: 0.1, - metalness: 0.5, - side: THREE.DoubleSide, - }); - const bg = new THREE.Mesh(bgGeo, bgMat); + 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 borderMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); - const border = new THREE.Mesh(new THREE.PlaneGeometry(w + 0.05, h + 0.05), borderMat); - border.position.z = -0.01; - group.add(border); + 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 textCanvas = document.createElement('canvas'); - textCanvas.width = 512; - textCanvas.height = 640; - const ctx = textCanvas.getContext('2d'); + 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.font = 'bold 32px "Orbitron",sans-serif'; ctx.fillText(title, 20, 45); ctx.fillRect(20, 55, 472, 2); - ctx.font = '20px "JetBrains Mono", monospace'; - ctx.fillStyle = '#a0b8d0'; + ctx.font = '20px "JetBrains Mono",monospace'; lines.forEach((line, i) => { - let fillColor = '#a0b8d0'; - if (line.includes('● RUNNING') || line.includes('● ACTIVE')) fillColor = '#4af0c0'; - else if (line.includes('○ STANDBY')) fillColor = '#5a6a8a'; - else if (line.includes('NOMINAL')) fillColor = '#4af0c0'; - ctx.fillStyle = fillColor; + 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 textTexture = new THREE.CanvasTexture(textCanvas); - textTexture.minFilter = THREE.LinearFilter; - const textMat = new THREE.MeshBasicMaterial({ - map: textTexture, - transparent: true, - side: THREE.DoubleSide, - depthWrite: false, - }); - const textMesh = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.95, h * 0.95), textMat); - textMesh.position.z = 0.01; - group.add(textMesh); - - const scanGeo = new THREE.PlaneGeometry(w, h); const scanMat = new THREE.ShaderMaterial({ - transparent: true, - depthWrite: false, + 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 scanline = sin(vUv.y * 200.0 + uTime * 2.0) * 0.5 + 0.5; - scanline = pow(scanline, 8.0); - float sweep = smoothstep(0.0, 0.02, abs(fract(vUv.y - uTime * 0.1) - 0.5)); - sweep = 1.0 - (1.0 - sweep) * 0.3; - float alpha = scanline * 0.04 + (1.0 - sweep) * 0.08; - gl_FragColor = vec4(uColor, alpha); - } - `, + 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 scanMesh = new THREE.Mesh(scanGeo, scanMat); - scanMesh.position.z = 0.02; - group.add(scanMesh); + 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 }); + batcaveTerminals.push({ group, scanMat, borderMat: border.material }); } -// ═══ AGENT IDLE BEHAVIOR SYSTEM ═══ +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; - - // WAITING — pulsing sphere - const waitGeo = new THREE.SphereGeometry(0.18, 16, 16); - const waitMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.85 }); - const waitMesh = new THREE.Mesh(waitGeo, waitMat); - waitMesh.name = 'indicator_waiting'; - waitMesh.visible = false; - group.add(waitMesh); - - // THINKING — wireframe octahedron - const thinkGeo = new THREE.OctahedronGeometry(0.2, 0); - const thinkMat = new THREE.MeshBasicMaterial({ color, wireframe: true }); - const thinkMesh = new THREE.Mesh(thinkGeo, thinkMat); - thinkMesh.name = 'indicator_thinking'; - thinkMesh.visible = false; - group.add(thinkMesh); - - // PROCESSING — spinning torus ring - const procGeo = new THREE.TorusGeometry(0.18, 0.04, 8, 32); - const procMat = new THREE.MeshBasicMaterial({ color }); - const procMesh = new THREE.Mesh(procGeo, procMat); - procMesh.name = 'indicator_processing'; - procMesh.visible = false; - group.add(procMesh); - + 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 }; } @@ -736,746 +525,299 @@ function setAgentActivity(agent, state) { } function buildPacingPath(station) { - // Small 3-waypoint circuit around the 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, 0, station.z + r), new THREE.Vector3(station.x + r, 0, station.z - r * 0.5), ]; } -function pickNextState(agent) { - const weights = { - [AGENT_STATES.IDLE]: 40, - [AGENT_STATES.PACING]: 25, - [AGENT_STATES.LOOKING]: 20, - [AGENT_STATES.READING]: 15, - }; - const total = Object.values(weights).reduce((a, b) => a + b, 0); - let r = Math.random() * total; - for (const [state, w] of Object.entries(weights)) { - r -= w; - if (r <= 0) return state; - } - return AGENT_STATES.IDLE; +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'; } -// ═══ AGENT PRESENCE SYSTEM ═══ function createAgentPresences() { const agentData = [ - { id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 }, station: { x: -4, z: -4 } }, - { id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 }, station: { x: 4, z: -4 } }, - { id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 }, station: { x: 0, z: -6 } }, - { id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 }, station: { x: -6, z: -2 } }, + { 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); - // Agent Orb - const orbGeo = new THREE.SphereGeometry(0.4, 32, 32); - const orbMat = new THREE.MeshPhysicalMaterial({ - color: color, - emissive: color, - emissiveIntensity: 2, - roughness: 0, - metalness: 1, - transmission: 0.8, - thickness: 0.5, - }); - const orb = new THREE.Mesh(orbGeo, orbMat); - orb.position.y = 3; - group.add(orb); + 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); - // Halo - const haloGeo = new THREE.TorusGeometry(0.6, 0.02, 16, 64); - const haloMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 }); - const halo = new THREE.Mesh(haloGeo, haloMat); - halo.position.y = 3; - halo.rotation.x = Math.PI / 2; - group.add(halo); + 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); - // Label - const canvas = document.createElement('canvas'); - canvas.width = 256; - canvas.height = 64; - const ctx = canvas.getContext('2d'); - ctx.font = 'bold 24px "Orbitron", sans-serif'; - ctx.fillStyle = '#' + color.getHexString(); - ctx.textAlign = 'center'; - ctx.fillText(data.name, 128, 40); - const tex = new THREE.CanvasTexture(canvas); - const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide }); - const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), mat); - label.position.y = 3.8; - group.add(label); + 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); - // Activity Indicator const indicator = createActivityIndicator(color); group.add(indicator.group); - scene.add(group); + agents.push({ - id: data.id, - group, - orb, - halo, - color, - station: data.station, + id: data.id, group, orb, halo, color, + station: data.pos, targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z), - // Idle state machine - state: AGENT_STATES.IDLE, - stateTimer: 2 + Math.random() * 4, - lookAngle: 0, - lookSpeed: 0.4 + Math.random() * 0.3, - pacingPath: buildPacingPath(data.station), - pacingIdx: 0, - // Activity indicators - indicator, - activityState: ACTIVITY_STATES.NONE, - activityLocked: false, + 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 geo = new THREE.CylinderGeometry(8, 8, 12, 32, 1, true); 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 alpha = (lines * 0.1 + dots * 0.5) * smoothstep(0.0, 0.2, vUv.y) * smoothstep(1.0, 0.8, vUv.y); - gl_FragColor = vec4(uColor, alpha * 0.3); - } - `, + 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(geo, mat); + thoughtStreamMesh = new THREE.Mesh(new THREE.CylinderGeometry(8, 8, 12, 32, 1, true), mat); thoughtStreamMesh.position.y = 6; scene.add(thoughtStreamMesh); } function createHarnessPulse() { - const geo = new THREE.RingGeometry(0.1, 0.2, 64); - const mat = new THREE.MeshBasicMaterial({ - color: NEXUS.colors.primary, - transparent: true, - opacity: 0, - side: THREE.DoubleSide, - }); - harnessPulseMesh = new THREE.Mesh(geo, mat); - harnessPulseMesh.rotation.x = -Math.PI / 2; - harnessPulseMesh.position.y = 0.1; + 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 barCount = 12; + 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 < barCount; 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); + 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 labelCanvas = document.createElement('canvas'); - labelCanvas.width = 256; - labelCanvas.height = 64; - const ctx = labelCanvas.getContext('2d'); - ctx.font = 'bold 24px "Orbitron", sans-serif'; - ctx.fillStyle = '#4af0c0'; - ctx.textAlign = 'center'; - ctx.fillText('POWER LEVEL', 128, 40); - const tex = new THREE.CanvasTexture(labelCanvas); - const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide }); - const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), labelMat); - label.position.y = 3; - group.add(label); - + 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 createDualBrainTexture() { - const W = 512, H = 512; - const canvas = document.createElement('canvas'); - canvas.width = W; - canvas.height = H; - const ctx = canvas.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.strokeStyle = '#223366'; - ctx.lineWidth = 1; - ctx.strokeRect(5, 5, W - 10, H - 10); - - 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(); - - ctx.font = '11px "Courier New", monospace'; - ctx.fillStyle = '#556688'; - ctx.textAlign = 'left'; - ctx.fillText('BRAIN GAP SCORECARD', 20, 74); - - const categories = [ - { name: 'Triage' }, - { name: 'Tool Use' }, - { name: 'Code Gen' }, - { name: 'Planning' }, - { name: 'Communication' }, - { name: 'Reasoning' }, - ]; - - const barX = 20; - const barW = W - 130; - const barH = 20; - let y = 90; - - for (const cat of categories) { - ctx.font = '13px "Courier New", monospace'; - ctx.fillStyle = '#445566'; - ctx.textAlign = 'left'; - ctx.fillText(cat.name, barX, y + 14); - - ctx.font = 'bold 13px "Courier New", monospace'; - ctx.fillStyle = '#334466'; - ctx.textAlign = 'right'; - ctx.fillText('\u2014', W - 20, y + 14); - - y += 22; - - ctx.fillStyle = 'rgba(255, 255, 255, 0.06)'; - ctx.fillRect(barX, y, barW, barH); - - y += barH + 12; - } - - ctx.strokeStyle = '#1a3a6a'; - ctx.beginPath(); - ctx.moveTo(20, y + 4); - ctx.lineTo(W - 20, y + 4); - ctx.stroke(); - - y += 22; - - ctx.font = 'bold 18px "Courier New", monospace'; - ctx.fillStyle = '#334466'; - ctx.textAlign = 'center'; - ctx.fillText('AWAITING DEPLOYMENT', W / 2, y + 10); - - ctx.font = '11px "Courier New", monospace'; - ctx.fillStyle = '#223344'; - ctx.fillText('Dual-brain system not yet connected', W / 2, y + 32); - - y += 52; - ctx.beginPath(); - ctx.arc(W / 2 - 60, y + 8, 6, 0, Math.PI * 2); - ctx.fillStyle = '#334466'; - ctx.fill(); - ctx.font = '11px "Courier New", monospace'; - ctx.fillStyle = '#334466'; - ctx.textAlign = 'left'; - ctx.fillText('CLOUD', W / 2 - 48, y + 12); - - ctx.beginPath(); - ctx.arc(W / 2 + 30, y + 8, 6, 0, Math.PI * 2); - ctx.fillStyle = '#334466'; - ctx.fill(); - ctx.fillStyle = '#334466'; - ctx.fillText('LOCAL', W / 2 + 42, y + 12); - - return new THREE.CanvasTexture(canvas); -} - function createDualBrainPanel() { dualBrainGroup = new THREE.Group(); dualBrainGroup.position.set(10, 3, -8); dualBrainGroup.lookAt(0, 3, 0); scene.add(dualBrainGroup); - // Main panel sprite - const panelTexture = createDualBrainTexture(); - const panelMat = new THREE.SpriteMaterial({ - map: panelTexture, transparent: true, opacity: 0.92, depthWrite: false, - }); - const panelSprite = new THREE.Sprite(panelMat); - panelSprite.scale.set(5.0, 5.0, 1); - panelSprite.position.set(0, 0, 0); - panelSprite.userData = { baseY: 0, floatPhase: 0, floatSpeed: 0.22, zoomLabel: 'Dual-Brain Status' }; + 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); - // Panel glow light dualBrainLight = new THREE.PointLight(0x4488ff, 0.6, 10); dualBrainLight.position.set(0, 0.5, 1); dualBrainGroup.add(dualBrainLight); - // Cloud Brain Orb - const cloudOrbGeo = new THREE.SphereGeometry(0.35, 32, 32); - const cloudOrbMat = new THREE.MeshStandardMaterial({ - color: 0x334466, - emissive: new THREE.Color(0x334466), - emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2, - transparent: true, opacity: 0.85, - }); - cloudOrb = new THREE.Mesh(cloudOrbGeo, cloudOrbMat); - cloudOrb.position.set(-2.0, 3.0, 0); - cloudOrb.userData.zoomLabel = 'Cloud Brain'; - dualBrainGroup.add(cloudOrb); + 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); +} - cloudOrbLight = new THREE.PointLight(0x334466, 0.15, 5); - cloudOrbLight.position.copy(cloudOrb.position); - dualBrainGroup.add(cloudOrbLight); +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(); - // Local Brain Orb - const localOrbGeo = new THREE.SphereGeometry(0.35, 32, 32); - const localOrbMat = new THREE.MeshStandardMaterial({ - color: 0x334466, - emissive: new THREE.Color(0x334466), - emissiveIntensity: 0.1, metalness: 0.3, roughness: 0.2, - transparent: true, opacity: 0.85, - }); - localOrb = new THREE.Mesh(localOrbGeo, localOrbMat); - localOrb.position.set(2.0, 3.0, 0); - localOrb.userData.zoomLabel = 'Local Brain'; - dualBrainGroup.add(localOrb); + 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; +} - localOrbLight = new THREE.PointLight(0x334466, 0.15, 5); - localOrbLight.position.copy(localOrb.position); - dualBrainGroup.add(localOrbLight); - - // Scan line overlay - const scanCanvas = document.createElement('canvas'); - scanCanvas.width = 512; - scanCanvas.height = 512; - dualBrainScanCtx = scanCanvas.getContext('2d'); - dualBrainScanTexture = new THREE.CanvasTexture(scanCanvas); - const scanMat = new THREE.SpriteMaterial({ - map: dualBrainScanTexture, transparent: true, opacity: 0.18, depthWrite: false, - }); - const scanSprite = new THREE.Sprite(scanMat); - scanSprite.scale.set(5.0, 5.0, 1); - scanSprite.position.set(0, 0, 0.01); - dualBrainGroup.add(scanSprite); +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(config => { - const vp = createVisionPoint(config); - visionPoints.push(vp); - }); -} +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); - - // Floating Crystal - const crystalGeo = new THREE.OctahedronGeometry(0.6, 0); - const crystalMat = new THREE.MeshPhysicalMaterial({ - color: color, - emissive: color, - emissiveIntensity: 1, - roughness: 0, - metalness: 1, - transmission: 0.5, - thickness: 1, - }); - const crystal = new THREE.Mesh(crystalGeo, crystalMat); - crystal.position.y = 2.5; - group.add(crystal); - - // Glow Ring - const ringGeo = new THREE.TorusGeometry(0.8, 0.02, 16, 64); - const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 }); - const ring = new THREE.Mesh(ringGeo, ringMat); - ring.position.y = 2.5; - ring.rotation.x = Math.PI / 2; - group.add(ring); - - // Light - const light = new THREE.PointLight(color, 1, 10); - light.position.set(0, 2.5, 0); - group.add(light); - + 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(config => { - const portal = createPortal(config); - portals.push(portal); - }); -} +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; - } + if (config.rotation) group.rotation.y = config.rotation.y; + const pc = new THREE.Color(config.color); - const portalColor = 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); - // Torus Ring - const torusGeo = new THREE.TorusGeometry(3, 0.15, 16, 64); - const torusMat = new THREE.MeshStandardMaterial({ - color: portalColor, - emissive: portalColor, - emissiveIntensity: 1.5, - roughness: 0.2, - metalness: 0.8, - }); - const ring = new THREE.Mesh(torusGeo, torusMat); - ring.position.y = 3.5; - ring.name = `portal_ring_${config.id}`; - group.add(ring); - - // Swirl Disc - const swirlGeo = new THREE.CircleGeometry(2.8, 64); + // Swirl const swirlMat = new THREE.ShaderMaterial({ - transparent: true, - side: THREE.DoubleSide, - uniforms: { - uTime: { value: 0 }, - uColor: { value: portalColor }, - }, - 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); - float a = atan(c.y, c.x); - float swirl = sin(a * 3.0 + r * 10.0 - uTime * 3.0) * 0.5 + 0.5; - float swirl2 = 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(uColor, vec3(1.0, 1.0, 1.0), swirl * 0.3); - col = mix(col, vec3(1.0, 1.0, 1.0), swirl2 * 0.2); - float alpha = mask * (0.5 + 0.3 * swirl); - gl_FragColor = vec4(col, alpha); - } - `, + 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(swirlGeo, swirlMat); - swirl.position.y = 3.5; - group.add(swirl); + const swirl = new THREE.Mesh(new THREE.CircleGeometry(2.8, 64), swirlMat); + swirl.position.y = 3.5; group.add(swirl); - // Orbital Particles - const pCount = 120; - const pGeo = new THREE.BufferGeometry(); - const pPos = new Float32Array(pCount * 3); - const pSizes = new Float32Array(pCount); + // Particles + const pCount = 120, pPos = new Float32Array(pCount * 3); for (let i = 0; i < pCount; i++) { - const angle = Math.random() * Math.PI * 2; - const 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; - pSizes[i] = 0.05 + Math.random() * 0.1; + 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)); - pGeo.setAttribute('size', new THREE.BufferAttribute(pSizes, 1)); - const pMat = new THREE.PointsMaterial({ - color: portalColor, - size: 0.08, - transparent: true, - opacity: 0.6, - blending: THREE.AdditiveBlending, - depthWrite: false, - }); - const pSystem = new THREE.Points(pGeo, pMat); + 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); - // Pulsing Point Light - const light = new THREE.PointLight(portalColor, 2, 15, 1.5); - light.position.set(0, 3.5, 1); - group.add(light); + // Light + const light = new THREE.PointLight(pc, 2, 15, 1.5); light.position.set(0, 3.5, 1); group.add(light); - // Rune Ring (Portal-tethered) - const runeCount = 8; - const runeRingRadius = 4.5; + // Runes const runes = []; - for (let i = 0; i < runeCount; i++) { - const angle = (i / runeCount) * Math.PI * 2; - const runeGeo = new THREE.BoxGeometry(0.3, 0.8, 0.1); - const runeMat = new THREE.MeshStandardMaterial({ - color: portalColor, - emissive: portalColor, - emissiveIntensity: 0.8, - transparent: true, - opacity: 0.7, - roughness: 0.2, - metalness: 0.5, - }); - const rune = new THREE.Mesh(runeGeo, runeMat); - rune.position.set( - Math.cos(angle) * runeRingRadius, - 4, - Math.sin(angle) * runeRingRadius - ); - rune.rotation.y = angle + Math.PI / 2; - group.add(rune); - runes.push(rune); + 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 labelCanvas = document.createElement('canvas'); - labelCanvas.width = 512; - labelCanvas.height = 64; - const lctx = labelCanvas.getContext('2d'); - lctx.font = 'bold 32px "Orbitron", sans-serif'; - lctx.fillStyle = '#' + portalColor.getHexString(); - lctx.textAlign = 'center'; - lctx.fillText(`◈ ${config.name.toUpperCase()}`, 256, 42); - const labelTex = new THREE.CanvasTexture(labelCanvas); - const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide }); - const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.5), labelMat); - labelMesh.position.y = 7.5; - group.add(labelMesh); + 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); - // Base Pillars - for (let side of [-1, 1]) { - const pillarGeo = new THREE.CylinderGeometry(0.2, 0.3, 7, 8); - const pillarMat = new THREE.MeshStandardMaterial({ - color: 0x1a1a2e, - roughness: 0.5, - metalness: 0.7, - emissive: portalColor, - emissiveIntensity: 0.1, - }); - const pillar = new THREE.Mesh(pillarGeo, pillarMat); - pillar.position.set(side * 3, 3.5, 0); - pillar.castShadow = true; - group.add(pillar); + // 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 - }; + return { config, group, ring, swirl, pSystem, light, runes }; } // ═══ PARTICLES ═══ function createParticles() { const count = particleCount(1500); - const geo = new THREE.BufferGeometry(); - const positions = new Float32Array(count * 3); - const colors = new Float32Array(count * 3); - const sizes = new Float32Array(count); - - const c1 = new THREE.Color(NEXUS.colors.primary); - const c2 = new THREE.Color(NEXUS.colors.secondary); - const c3 = new THREE.Color(NEXUS.colors.gold); - + 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(); - const 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; - + 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)); - - const mat = new THREE.ShaderMaterial({ + 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 pos = position; - pos.y += sin(uTime * 0.5 + position.x * 0.5) * 0.3; - pos.x += sin(uTime * 0.3 + position.z * 0.4) * 0.2; - vec4 mv = modelViewMatrix * vec4(pos, 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; - float alpha = smoothstep(0.5, 0.1, d); - gl_FragColor = vec4(vColor, alpha * 0.7); - } - `, - transparent: true, - depthWrite: false, - blending: THREE.AdditiveBlending, - }); - - particles = new THREE.Points(geo, mat); + 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); - const geo = new THREE.BufferGeometry(); - const positions = new Float32Array(count * 3); - + 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; + 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)); - - const mat = new THREE.PointsMaterial({ - color: 0x8899bb, - size: 0.03, - transparent: true, - opacity: 0.3, - depthWrite: false, - }); - - dustParticles = new THREE.Points(geo, mat); + 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, - }); + 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 positions = [ - { 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 }, - ]; + 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); - positions.forEach(p => { - const geo = new THREE.ConeGeometry(0.4 * p.s, 2.5 * p.s, 5); - const crystal = new THREE.Mesh(geo, crystalMat.clone()); - crystal.position.set(p.x, 1.25 * p.s, p.z); - crystal.rotation.y = p.ry; - crystal.rotation.z = (Math.random() - 0.5) * 0.3; - crystal.castShadow = true; - scene.add(crystal); - }); - - - - const coreGeo = new THREE.IcosahedronGeometry(0.6, 2); - const coreMat = new THREE.MeshPhysicalMaterial({ - color: 0x4af0c0, - emissive: 0x4af0c0, - emissiveIntensity: 2, - roughness: 0, - metalness: 1, - transmission: 0.3, - thickness: 1, - }); - const core = new THREE.Mesh(coreGeo, coreMat); - core.position.set(0, 2.5, 0); - core.name = 'nexus-core'; - scene.add(core); - - const pedGeo = new THREE.CylinderGeometry(0.8, 1.2, 1.5, 8); - const pedMat = new THREE.MeshStandardMaterial({ - color: 0x0a0f1a, - roughness: 0.4, - metalness: 0.8, - emissive: 0x1a2a4a, - emissiveIntensity: 0.3, - }); - const pedestal = new THREE.Mesh(pedGeo, pedMat); - pedestal.position.set(0, 0.75, 0); - pedestal.castShadow = true; - scene.add(pedestal); + 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 MODE ═══ +// ═══ NAVIGATION ═══ function cycleNavMode() { navModeIdx = (navModeIdx + 1) % NAV_MODES.length; const mode = NAV_MODES[navModeIdx]; @@ -1489,105 +831,56 @@ function cycleNavMode() { orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius))); } if (mode === 'fly') flyY = playerPos.y; - updateNavModeUI(mode); -} - -function updateNavModeUI(mode) { - const el = document.getElementById('nav-mode-label'); - if (el) el.textContent = mode.toUpperCase(); + 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(); - const input = document.getElementById('chat-input'); - if (document.activeElement === input) { - sendChatMessage(); - } else { - input.focus(); - } - } - if (e.key === 'Escape') { - document.getElementById('chat-input').blur(); - if (portalOverlayActive) closePortalOverlay(); - if (visionOverlayActive) closeVisionOverlay(); - } - if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) { - 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; + 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) { - mouseDown = true; - orbitState.lastX = e.clientX; - orbitState.lastY = e.clientY; - - // Raycasting for portals - if (!portalOverlayActive) { - const mouse = new THREE.Vector2( - (e.clientX / window.innerWidth) * 2 - 1, - -(e.clientY / window.innerHeight) * 2 + 1 - ); - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(mouse, camera); - const intersects = raycaster.intersectObjects(portals.map(p => p.ring)); - if (intersects.length > 0) { - const clickedRing = intersects[0].object; - const portal = portals.find(p => p.ring === clickedRing); - if (portal) activatePortal(portal); - } - } + 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) return; - if (document.activeElement === document.getElementById('chat-input')) return; - const mode = NAV_MODES[navModeIdx]; - if (mode === 'orbit') { - const dx = e.clientX - orbitState.lastX; - const dy = e.clientY - orbitState.lastY; - orbitState.lastX = e.clientX; - orbitState.lastY = e.clientY; - orbitState.theta -= dx * 0.005; - orbitState.phi = Math.max(0.05, Math.min(Math.PI * 0.85, orbitState.phi + dy * 0.005)); + 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 -= e.movementY * 0.003; - playerRot.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, playerRot.x)); + 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)); - } + 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-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(); @@ -1595,38 +888,14 @@ function sendChatMessage() { addChatMessage('user', text); input.value = ''; - // Drive Timmy activity indicators const timmy = agents.find(a => a.id === 'timmy'); - if (timmy) { - timmy.activityLocked = true; - setAgentActivity(timmy, ACTIVITY_STATES.THINKING); + 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; } } - - const delay = 500 + Math.random() * 1000; - if (timmy) { - setTimeout(() => setAgentActivity(timmy, ACTIVITY_STATES.PROCESSING), delay * 0.4); - } - - setTimeout(() => { - const responses = [ - 'Processing your request through the harness...', - 'I have noted this in my thought stream.', - 'Acknowledged. Routing to appropriate agent loop.', - 'The sovereign space recognizes your command.', - 'Running analysis. Results will appear on the main terminal.', - 'My crystal ball says... yes. Implementing.', - 'Understood, Alexander. Adjusting priorities.', - ]; - const resp = responses[Math.floor(Math.random() * responses.length)]; - addChatMessage('timmy', resp); - if (timmy) { - setAgentActivity(timmy, ACTIVITY_STATES.WAITING); - setTimeout(() => { - setAgentActivity(timmy, ACTIVITY_STATES.NONE); - timmy.activityLocked = false; - }, 2000); - } - }, delay); input.blur(); } @@ -1643,569 +912,270 @@ function addChatMessage(type, text) { // ═══ PORTAL INTERACTION ═══ function checkPortalProximity() { if (portalOverlayActive) return; - - let closest = null; - let minDist = Infinity; - - portals.forEach(portal => { - const dist = playerPos.distanceTo(portal.group.position); - if (dist < 4.5 && dist < minDist) { - minDist = dist; - closest = portal; - } - }); - + 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'; - } + 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; - const overlay = document.getElementById('portal-overlay'); - const nameDisplay = document.getElementById('portal-name-display'); - const descDisplay = document.getElementById('portal-desc-display'); - const redirectBox = document.getElementById('portal-redirect-box'); - const errorBox = document.getElementById('portal-error-box'); - const timerDisplay = document.getElementById('portal-timer'); - const statusDot = document.getElementById('portal-status-dot'); - - nameDisplay.textContent = portal.config.name.toUpperCase(); - descDisplay.textContent = portal.config.description; - statusDot.style.background = portal.config.color; - statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`; - - overlay.style.display = 'flex'; - - if (portal.config.destination && portal.config.destination.url) { - redirectBox.style.display = 'block'; - errorBox.style.display = 'none'; - + 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; - timerDisplay.textContent = count; - const interval = setInterval(() => { - count--; - timerDisplay.textContent = count; - if (count <= 0) { - clearInterval(interval); - if (portalOverlayActive) window.location.href = portal.config.destination.url; - } - if (!portalOverlayActive) clearInterval(interval); + 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 { - redirectBox.style.display = 'none'; - errorBox.style.display = 'block'; + 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'; -} +function closePortalOverlay() { portalOverlayActive = false; document.getElementById('portal-overlay').style.display = 'none'; } // ═══ VISION INTERACTION ═══ function checkVisionProximity() { if (visionOverlayActive) return; - - let closest = null; - let minDist = Infinity; - - visionPoints.forEach(vp => { - const dist = playerPos.distanceTo(vp.group.position); - if (dist < 3.5 && dist < minDist) { - minDist = dist; - closest = vp; - } - }); - + 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'; - } + 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; - const overlay = document.getElementById('vision-overlay'); - const titleDisplay = document.getElementById('vision-title-display'); - const contentDisplay = document.getElementById('vision-content-display'); - const statusDot = document.getElementById('vision-status-dot'); - - titleDisplay.textContent = vp.config.title.toUpperCase(); - contentDisplay.textContent = vp.config.content; - statusDot.style.background = vp.config.color; - statusDot.style.boxShadow = `0 0 10px ${vp.config.color}`; - - overlay.style.display = 'flex'; + 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'; -} +function closeVisionOverlay() { visionOverlayActive = false; document.getElementById('vision-overlay').style.display = 'none'; } // ═══ GAME LOOP ═══ -let lastThoughtTime = 0; let pulseTimer = 0; function gameLoop() { requestAnimationFrame(gameLoop); - const delta = Math.min(clock.getDelta(), 0.1); - const elapsed = clock.elapsedTime; - - // Agent Thought Simulation - if (elapsed - lastThoughtTime > 4) { - lastThoughtTime = elapsed; - simulateAgentThought(); - } + 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); - } + 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; - const 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); - dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y); - playerPos.add(dir); - const maxR = 24; - const dist = Math.sqrt(playerPos.x * playerPos.x + playerPos.z * playerPos.z); - if (dist > maxR) { playerPos.x *= maxR / dist; playerPos.z *= maxR / dist; } - } + 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'); - + 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; - const 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); - pan.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 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; - + 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 forward = 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(forward, speed); - if (keys['s']) playerPos.addScaledVector(forward, -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; + 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'); + camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ'); } - // Proximity check checkPortalProximity(); checkVisionProximity(); + // Animate scene const sky = scene.getObjectByName('skybox'); if (sky) sky.material.uniforms.uTime.value = elapsed; - - // Pulse heatmap opacity 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; }); - 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; }); }); - // Animate Portals - portals.forEach(portal => { - portal.ring.rotation.z = elapsed * 0.3; - portal.ring.rotation.x = Math.sin(elapsed * 0.5) * 0.1; - if (portal.swirl.material.uniforms) { - portal.swirl.material.uniforms.uTime.value = elapsed; - } - // Pulse light - portal.light.intensity = 1.5 + Math.sin(elapsed * 2) * 0.5; - // Animate particles - const positions = portal.pSystem.geometry.attributes.position.array; - for (let i = 0; i < positions.length / 3; i++) { - positions[i * 3 + 1] += Math.sin(elapsed + i) * 0.002; - } - portal.pSystem.geometry.attributes.position.needsUpdate = true; - - // Animate runes - portal.runes.forEach((rune, i) => { - rune.position.y = 4 + Math.sin(elapsed * 2 + i * 0.5) * 0.2; - rune.rotation.z = elapsed * 0.8 + i; - }); - }); - - // Animate Vision Points + // 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.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.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; }); - // Animate Agents + // Agents updateAgents(elapsed, delta); - // Animate Dual-Brain Panel + // 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; - } - if (dualBrainScanCtx && dualBrainScanTexture) { - const W = 512, H = 512; - dualBrainScanCtx.clearRect(0, 0, W, H); - const scanY = ((elapsed * 80) % H); - const grad = dualBrainScanCtx.createLinearGradient(0, scanY - 20, 0, scanY + 20); - grad.addColorStop(0, 'rgba(68, 136, 255, 0)'); - grad.addColorStop(0.5, 'rgba(68, 136, 255, 0.6)'); - grad.addColorStop(1, 'rgba(68, 136, 255, 0)'); - dualBrainScanCtx.fillStyle = grad; - dualBrainScanCtx.fillRect(0, scanY - 20, W, 40); - dualBrainScanTexture.needsUpdate = true; - } + 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; } - // Animate Power Meter + // Power meter powerMeterBars.forEach((bar, i) => { - const level = (Math.sin(elapsed * 2 + i * 0.5) * 0.5 + 0.5); - const 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.0; + 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; }); - // Animate glass floor edge glow (ripple outward from center) - for (const { mat, distFromCenter } of glassEdgeMaterials) { - const phase = elapsed * 1.1 - distFromCenter * 0.18; - mat.opacity = 0.25 + Math.sin(phase) * 0.22; - } - 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; - } - - + 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; - } + 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 (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}]\n` + - `Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`; + 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; - const h = window.innerHeight; - camera.aspect = w / h; - camera.updateProjectionMatrix(); - renderer.setSize(w, h); - composer.setSize(w, h); + const w = window.innerWidth, h = window.innerHeight; + camera.aspect = w / h; camera.updateProjectionMatrix(); + renderer.setSize(w, h); composer.setSize(w, h); } -// ═══ AGENT IDLE ANIMATION ═══ +// ═══ AGENT ANIMATION ═══ function updateAgents(elapsed, delta) { const ATTENTION_RADIUS = 7; - const terminalFacing = new THREE.Vector3(0, 0, -8); // batcave terminal bank Z - agents.forEach((agent, i) => { - const stationWorld = new THREE.Vector3(agent.station.x, 0, agent.station.z); - - // ── Attention system: face player when close ── - const toPlayer = new THREE.Vector3( - playerPos.x - agent.group.position.x, - 0, - playerPos.z - agent.group.position.z - ); - const playerDist = toPlayer.length(); - const playerNearby = playerDist < ATTENTION_RADIUS && !agent.activityLocked; + 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 targetAngle = Math.atan2(toPlayer.x, toPlayer.z); - const currentAngle = agent.group.rotation.y; - const diff = ((targetAngle - currentAngle + Math.PI * 3) % (Math.PI * 2)) - Math.PI; - agent.group.rotation.y += diff * Math.min(delta * 3, 1); + 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); } - // ── State machine (skip if activity locked or player nearby) ── if (!playerNearby && !agent.activityLocked) { agent.stateTimer -= delta; - if (agent.stateTimer <= 0) { - agent.state = pickNextState(agent); - switch (agent.state) { - case AGENT_STATES.IDLE: - agent.stateTimer = 4 + Math.random() * 6; - agent.targetPos.copy(stationWorld); - break; - case AGENT_STATES.PACING: - agent.stateTimer = 8 + Math.random() * 6; - agent.pacingIdx = 0; - break; - case AGENT_STATES.LOOKING: - agent.stateTimer = 4 + Math.random() * 4; - agent.lookAngle = agent.group.rotation.y; - break; - case AGENT_STATES.READING: - agent.stateTimer = 5 + Math.random() * 5; - agent.targetPos.copy(stationWorld); - break; - } + 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); } - - // ── Movement per state ── - if (agent.state === AGENT_STATES.PACING) { + 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 === AGENT_STATES.READING) { - // Face the terminal bank - const toTerminal = new THREE.Vector3( - terminalFacing.x - agent.group.position.x, - 0, - terminalFacing.z - agent.group.position.z - ); - const targetAngle = Math.atan2(toTerminal.x, toTerminal.z); - agent.group.rotation.y += (targetAngle - agent.group.rotation.y) * Math.min(delta * 2, 1); - agent.group.position.lerp(agent.targetPos, delta * 0.4); - } else if (agent.state === AGENT_STATES.LOOKING) { - // Slow environmental scan left/right - 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); + 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 { - // IDLE — drift gently back to station - agent.group.position.lerp(agent.targetPos, delta * 0.3); + agent.group.position.lerp(agent.targetPos, delta*0.3); } } - // ── Orb & halo animation ── - const bobAmt = agent.activityState === ACTIVITY_STATES.THINKING ? 0.25 : 0.15; - agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * bobAmt; - agent.halo.rotation.z = elapsed * 0.5; - agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1); - const baseEmissive = agent.activityState === ACTIVITY_STATES.NONE ? 2 : 3; - agent.orb.material.emissiveIntensity = baseEmissive + Math.sin(elapsed * 4 + i) * 1; + // 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 indicator animation ── + // Activity indicators if (agent.activityState !== ACTIVITY_STATES.NONE) { - // Floating bob - agent.indicator.group.position.y = 4.2 + Math.sin(elapsed * 2 + i * 1.3) * 0.1; - - if (agent.activityState === ACTIVITY_STATES.WAITING) { - const pulse = 0.7 + Math.sin(elapsed * 4 + i) * 0.3; - agent.indicator.waitMesh.scale.setScalar(pulse); - agent.indicator.waitMesh.material.opacity = 0.5 + pulse * 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; - } - - // Billboard — indicator faces camera - const toCamera = new THREE.Vector3( - camera.position.x - agent.group.position.x, - 0, - camera.position.z - agent.group.position.z - ); - if (toCamera.length() > 0.01) { - agent.indicator.group.rotation.y = Math.atan2(toCamera.x, toCamera.z); - } + 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); } }); } -// ═══ AGENT SIMULATION ═══ -function simulateAgentThought() { - const agentIds = ['timmy', 'kimi', 'claude', 'perplexity']; - const agentId = agentIds[Math.floor(Math.random() * agentIds.length)]; - const thoughts = { - timmy: [ - 'Analyzing portal stability...', - 'Sovereign nodes synchronized.', - 'Memory stream optimization complete.', - 'Scanning for external interference...', - 'The harness is humming beautifully.', - ], - kimi: [ - 'Processing linguistic patterns...', - 'Context window expanded.', - 'Synthesizing creative output...', - 'Awaiting user prompt sequence.', - 'Neural weights adjusted.', - ], - claude: [ - 'Reasoning through complex logic...', - 'Ethical guardrails verified.', - 'Refining thought architecture...', - 'Connecting disparate data points.', - 'Deep analysis in progress.', - ], - perplexity: [ - 'Searching global knowledge graph...', - 'Verifying source citations...', - 'Synthesizing real-time data...', - 'Mapping information topology...', - 'Fact-checking active streams.', - ] - }; - - const thought = thoughts[agentId][Math.floor(Math.random() * thoughts[agentId].length)]; - addAgentLog(agentId, thought); -} - 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(); - } + if (container.children.length > 6) container.lastElementChild.remove(); } function triggerHarnessPulse() { if (!harnessPulseMesh) return; - harnessPulseMesh.scale.setScalar(0.1); - harnessPulseMesh.material.opacity = 0.8; - - // Flash the core + 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); - } + if (core) { core.material.emissiveIntensity = 10; setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 200); } } -// === BITCOIN BLOCK HEIGHT === +// ═══ BITCOIN BLOCK HEIGHT ═══ (function initBitcoin() { - const blockHeightDisplay = document.getElementById('block-height-display'); - const blockHeightValue = document.getElementById('block-height-value'); - if (!blockHeightDisplay || !blockHeightValue) return; - - let lastKnownBlockHeight = null; - - async function fetchBlockHeight() { + 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 height = parseInt(await res.text(), 10); - if (isNaN(height)) return; - - if (lastKnownBlockHeight !== null && height !== lastKnownBlockHeight) { - blockHeightDisplay.classList.remove('fresh'); - void blockHeightDisplay.offsetWidth; - blockHeightDisplay.classList.add('fresh'); - } - - lastKnownBlockHeight = height; - blockHeightValue.textContent = height.toLocaleString(); - } catch (_) { - // Network unavailable - } + 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 (_) {} } - - fetchBlockHeight(); - setInterval(fetchBlockHeight, 60000); + fetch_(); setInterval(fetch_, 60000); })(); init();