diff --git a/app.js b/app.js index c63dc48..ca55a81 100644 --- a/app.js +++ b/app.js @@ -19,6 +19,7 @@ const NEXUS = { gold: 0xffd700, danger: 0xff4466, gridLine: 0x1a2a4a, + memory: 0x00ffff, } }; @@ -45,10 +46,20 @@ const STATE = { 'WAITING FOR INPUT', 'SOUL ON BITCOIN' ], + selectedMemory: null, lastUpdate: 0, pulseRate: 1.0 // Hz }; +// ═══ MEMORY STORE (The Vault) ═══ +const MEMORY_VAULT = [ + { id: 1, title: 'ORIGIN', date: '2026-03-14', summary: 'Timmy initialized in the Nexus.', tags: ['core', 'origin'] }, + { id: 2, title: 'HERMES LINK', date: '2026-03-18', summary: 'Established stable bridge to Bannerlord.', tags: ['harness', 'bridge'] }, + { id: 3, title: 'SOVEREIGNTY', date: '2026-03-22', summary: 'First autonomous task assignment successful.', tags: ['agentic', 'freedom'] }, + { id: 4, title: 'NEXUS CORE', date: '2026-03-23', summary: 'Three.js foundation implemented.', tags: ['visual', 'home'] }, + { id: 5, title: 'HEARTBEAT', date: '2026-03-24', summary: 'Real-time state broadcasting active.', tags: ['infrastructure', 'live'] }, +]; + // ═══ STATE BROADCASTER ═══ const Broadcaster = { listeners: [], @@ -59,62 +70,37 @@ const Broadcaster = { // ═══ STATE UPDATER ═══ function updateSovereignState(elapsed) { STATE.metrics.uptime = elapsed; - - // Simulate some jitter/activity if (Math.random() > 0.95) { STATE.metrics.cpu = 10 + Math.floor(Math.random() * 15); STATE.metrics.activeLoops = 4 + Math.floor(Math.random() * 3); - - // Random thought shift if (Math.random() > 0.7) { - const newThoughts = [ - 'DECENTRALIZING COGNITION', - 'ZAPPING CONTRIBUTORS', - 'MAPPING SPATIAL LOOPS', - 'REFINING LORA WEIGHTS', - 'OBSERVING ALEXANDER', - 'NEXUS INTEGRITY: 100%', - 'HERMES LINK STABLE' - ]; + const newThoughts = ['DECENTRALIZING COGNITION', 'ZAPPING CONTRIBUTORS', 'MAPPING SPATIAL LOOPS', 'REFINING LORA WEIGHTS', 'OBSERVING ALEXANDER', 'NEXUS INTEGRITY: 100%', 'HERMES LINK STABLE']; STATE.thoughts.shift(); STATE.thoughts.push(newThoughts[Math.floor(Math.random() * newThoughts.length)]); } - Broadcaster.broadcast(); } } -// ═══ STATE ═══ +// ═══ GLOBAL REFS ═══ let camera, scene, renderer, composer; let clock, playerPos, playerRot; let keys = {}; let mouseDown = false; let batcaveTerminals = []; +let memoryCrystals = []; let portalMesh, portalGlow; let particles, dustParticles; let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; -let chatOpen = true; -let loadProgress = 0; -let performanceTier = 'high'; // 'high' | 'medium' | 'low' +let performanceTier = 'high'; +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); // ═══ NAVIGATION SYSTEM ═══ const NAV_MODES = ['walk', 'orbit', 'fly']; -let navModeIdx = 0; // default: walk - -// Orbit state -const orbitState = { - target: new THREE.Vector3(0, 2, 0), - radius: 14, - theta: Math.PI, // azimuthal (horizontal rotation) - phi: Math.PI / 6, // polar (vertical tilt, 0=top) - minR: 3, - maxR: 40, - lastX: 0, - lastY: 0, -}; - -// Fly state — separate Y so walk and fly share XZ history +let navModeIdx = 0; +const orbitState = { target: new THREE.Vector3(0, 2, 0), radius: 14, theta: Math.PI, phi: Math.PI / 6, minR: 3, maxR: 40, lastX: 0, lastY: 0 }; let flyY = 2; // ═══ INIT ═══ @@ -123,7 +109,6 @@ function init() { playerPos = new THREE.Vector3(0, 2, 12); playerRot = new THREE.Euler(0, 0, 0, 'YXZ'); - // Renderer const canvas = document.getElementById('nexus-canvas'); renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); @@ -132,199 +117,69 @@ function init() { renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; - // Performance budget — must run before scene objects are created performanceTier = detectPerformanceTier(); - updateLoad(20); - - // Scene scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x050510, 0.012); - // Camera camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.copy(playerPos); - updateLoad(30); - - // Build world createSkybox(); - updateLoad(40); createLighting(); - updateLoad(50); createFloor(); - updateLoad(55); createBatcaveTerminal(); - updateLoad(70); createPortal(); - updateLoad(80); createParticles(); createDustParticles(); - updateLoad(85); createAmbientStructures(); - updateLoad(90); + createMemoryVault(); - // Post-processing 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 - ); + const bloom = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.6, 0.4, 0.85); composer.addPass(bloom); composer.addPass(new SMAAPass(window.innerWidth, window.innerHeight)); - updateLoad(95); - - // Events setupControls(); window.addEventListener('resize', onResize); - - // Debug overlay ref debugOverlay = document.getElementById('debug-overlay'); - updateLoad(100); - - // Transition from loading to enter screen + // Fade out loading setTimeout(() => { - document.getElementById('loading-screen').classList.add('fade-out'); + document.getElementById('loading-screen')?.classList.add('fade-out'); const enterPrompt = document.getElementById('enter-prompt'); - enterPrompt.style.display = 'flex'; - - enterPrompt.addEventListener('click', () => { - enterPrompt.classList.add('fade-out'); - document.getElementById('hud').style.display = 'block'; - setTimeout(() => { enterPrompt.remove(); }, 600); - }, { once: true }); - - setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900); + if (enterPrompt) { + enterPrompt.style.display = 'flex'; + enterPrompt.addEventListener('click', () => { + enterPrompt.classList.add('fade-out'); + document.getElementById('hud').style.display = 'block'; + setTimeout(() => { enterPrompt.remove(); }, 600); + }, { once: true }); + } }, 600); - // Start loop requestAnimationFrame(gameLoop); } -function updateLoad(pct) { - loadProgress = pct; - const fill = document.getElementById('load-progress'); - if (fill) fill.style.width = pct + '%'; -} - -// ═══ PERFORMANCE BUDGET ═══ 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; - renderer.toneMappingExposure = 1.0; - return 'low'; - } else if (cores < 8) { - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); - renderer.shadowMap.type = THREE.BasicShadowMap; - return 'medium'; - } else { - // M3 Max / high-end desktop — full quality - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - return 'high'; - } + if (isMobile) { renderer.setPixelRatio(1); renderer.shadowMap.enabled = false; return 'low'; } + 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); + if (performanceTier === 'low') return Math.floor(base * 0.25); return base; } // ═══ SKYBOX ═══ function createSkybox() { - // Procedural nebula skybox using shader - const skyGeo = new THREE.SphereGeometry(400, 64, 64); + const skyGeo = new THREE.SphereGeometry(400, 32, 32); const skyMat = new THREE.ShaderMaterial({ - uniforms: { - uTime: { value: 0 }, - uColor1: { value: new THREE.Color(0x0a0520) }, - uColor2: { value: new THREE.Color(0x1a0a3e) }, - uColor3: { value: new THREE.Color(0x0a1a3e) }, - uStarDensity: { value: 0.97 }, - }, - vertexShader: ` - varying vec3 vPos; - void main() { - vPos = position; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } - `, - fragmentShader: ` - uniform float uTime; - uniform vec3 uColor1; - uniform vec3 uColor2; - uniform vec3 uColor3; - uniform float uStarDensity; - varying vec3 vPos; - - // Hash and noise - 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 - ); - } - - 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; - } - - void main() { - vec3 dir = normalize(vPos); - - // Nebula clouds - 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); - - // Nebula glow regions - 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); - - // Stars - float starField = hash(dir * 800.0); - float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0)); - // Twinkling - float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28); - col += vec3(stars * twinkle); - - // Big bright stars - float bigStar = step(0.998, starField); - col += vec3(0.8, 0.9, 1.0) * bigStar * twinkle; - - gl_FragColor = vec4(col, 1.0); - } - `, + uniforms: { uTime: { value: 0 }, uColor1: { value: new THREE.Color(0x0a0520) }, uColor2: { value: new THREE.Color(0x1a0a3e) }, uColor3: { value: new THREE.Color(0x0a1a3e) }, uStarDensity: { value: 0.97 } }, + vertexShader: `varying vec3 vPos; void main() { vPos = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, + fragmentShader: `uniform float uTime; uniform vec3 uColor1, uColor2, uColor3; uniform float uStarDensity; varying vec3 vPos; float hash(vec3 p) { p = fract(p * vec3(443.897, 441.423, 437.195)); p += dot(p, p.yzx + 19.19); return fract((p.x + p.y) * p.z); } float noise(vec3 p) { vec3 i = floor(p); 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); } float fbm(vec3 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += a * noise(p); p *= 2.0; a *= 0.5; } return v; } void main() { vec3 dir = normalize(vPos); float n1 = fbm(dir * 3.0 + uTime * 0.02), n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0); vec3 col = mix(uColor1, uColor2, smoothstep(0.3, 0.7, n1)); col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5); 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); gl_FragColor = vec4(col, 1.0); }`, side: THREE.BackSide, }); const sky = new THREE.Mesh(skyGeo, skyMat); @@ -334,92 +189,43 @@ function createSkybox() { // ═══ LIGHTING ═══ function createLighting() { - // Ambient - const ambient = new THREE.AmbientLight(0x1a1a3a, 0.4); - scene.add(ambient); - - // Main directional (moonlight feel) + 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.camera.near = 0.5; - dirLight.shadow.camera.far = 80; - dirLight.shadow.camera.left = -30; - dirLight.shadow.camera.right = 30; - dirLight.shadow.camera.top = 30; - dirLight.shadow.camera.bottom = -30; scene.add(dirLight); - - // Teal accent from below terminal const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5); tealLight.position.set(0, 1, -5); scene.add(tealLight); - - // Purple accent - const purpleLight = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5); - purpleLight.position.set(-8, 3, -8); - scene.add(purpleLight); - - // Portal glow light - const portalLight = new THREE.PointLight(0xff6600, 2, 20, 1.5); - portalLight.position.set(15, 4, -10); - scene.add(portalLight); } // ═══ FLOOR ═══ function createFloor() { - // Main hexagonal-feel platform using a flat circle const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6); - const platMat = new THREE.MeshStandardMaterial({ - color: 0x0a0f1a, - roughness: 0.8, - metalness: 0.3, - }); + const platMat = new THREE.MeshStandardMaterial({ color: 0x0a0f1a, roughness: 0.8, metalness: 0.3 }); const platform = new THREE.Mesh(platGeo, platMat); platform.position.y = -0.15; platform.receiveShadow = true; scene.add(platform); - - // Grid lines on the floor const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine); gridHelper.material.opacity = 0.15; gridHelper.material.transparent = true; gridHelper.position.y = 0.02; scene.add(gridHelper); - - // Glowing edge ring - const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6); - const ringMat = new THREE.MeshBasicMaterial({ - color: NEXUS.colors.primary, - transparent: true, - opacity: 0.4, - side: THREE.DoubleSide, - }); - const ring = new THREE.Mesh(ringGeo, ringMat); - ring.rotation.x = Math.PI / 2; - ring.position.y = 0.05; - scene.add(ring); } // ═══ BATCAVE TERMINAL ═══ function createBatcaveTerminal() { const terminalGroup = new THREE.Group(); terminalGroup.position.set(0, 0, -8); - const panels = [ - { id: 'command', title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3 }, - { id: 'queue', title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3 }, - { id: 'metrics', title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3 }, - { id: 'thoughts',title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3 }, - { id: 'agents', title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3 }, + { id: 'command', title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3 }, + { id: 'metrics', title: 'METRICS', color: NEXUS.colors.secondary, rot: -0.2, x: -3, y: 3 }, + { id: 'thoughts', title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0, x: 0, y: 3 }, + { id: 'vault', title: 'MEMORY VAULT', color: NEXUS.colors.memory, rot: 0.2, x: 3, y: 3 }, + { id: 'agents', title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3 }, ]; - - panels.forEach(data => { - createTerminalPanel(terminalGroup, data); - }); - + panels.forEach(data => createTerminalPanel(terminalGroup, data)); scene.add(terminalGroup); } @@ -429,131 +235,82 @@ function createTerminalPanel(parent, data) { const group = new THREE.Group(); group.position.set(x, y, 0); group.rotation.y = rot; - - // Panel background (glassy) - 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); - group.add(bg); - - // Border - 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); - - // Canvas for text + const bgMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.panelBg, transparent: true, opacity: 0.6, roughness: 0.1, metalness: 0.5, side: THREE.DoubleSide }); + group.add(new THREE.Mesh(new THREE.PlaneGeometry(w, h), bgMat)); const textCanvas = document.createElement('canvas'); - textCanvas.width = 512; - textCanvas.height = 640; + textCanvas.width = 512; textCanvas.height = 640; const ctx = textCanvas.getContext('2d'); - 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 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); - // Update function for this specific panel const updatePanel = (state) => { ctx.clearRect(0, 0, 512, 640); - - // Header ctx.fillStyle = '#' + new THREE.Color(color).getHexString(); ctx.font = 'bold 32px "Orbitron", sans-serif'; ctx.fillText(title, 20, 45); ctx.fillRect(20, 55, 472, 2); - ctx.font = '20px "JetBrains Mono", monospace'; ctx.fillStyle = '#a0b8d0'; - let lines = []; - if (id === 'command') { - lines = [ - `> STATUS: NOMINAL`, - `> UPTIME: ${state.metrics.uptime.toFixed(1)}s`, - `> HARNESS: STABLE`, - `> MODE: SOVEREIGN` - ]; - } else if (id === 'queue') { - lines = ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #39: HEART']; - } else if (id === 'metrics') { - lines = [ - `> CPU: ${state.metrics.cpu}%`, - `> MEM: ${state.metrics.mem}GB`, - `> LOOPS: ${state.metrics.activeLoops}`, - `> FPS: ${state.metrics.fps}` - ]; - } else if (id === 'thoughts') { - lines = state.thoughts.map(t => `> ${t}`); - } else if (id === 'agents') { - lines = Object.entries(state.agents).map(([name, status]) => `> ${name.toUpperCase()}: ${status}`); + if (id === 'command') lines = [`> STATUS: NOMINAL`, `> UPTIME: ${state.metrics.uptime.toFixed(1)}s`, `> MODE: SOVEREIGN` ]; + else if (id === 'metrics') lines = [`> CPU: ${state.metrics.cpu}%`, `> MEM: ${state.metrics.mem}GB`, `> FPS: ${state.metrics.fps}`]; + else if (id === 'thoughts') lines = state.thoughts.map(t => `> ${t}`); + else if (id === 'agents') lines = Object.entries(state.agents).map(([name, status]) => `> ${name.toUpperCase()}: ${status}`); + else if (id === 'vault') { + const mem = state.selectedMemory || MEMORY_VAULT[0]; + lines = [`> ID: ${mem.id}`, `> TITLE: ${mem.title}`, `> DATE: ${mem.date}`, `> TAGS: ${mem.tags.join(', ')}`, `> SUMMARY:`, mem.summary]; } - lines.forEach((line, i) => { - let fillColor = '#a0b8d0'; - if (line.includes('RUNNING') || line.includes('ACTIVE')) fillColor = '#4af0c0'; - ctx.fillStyle = fillColor; + ctx.fillStyle = (line.includes('RUNNING') || line.includes('ACTIVE')) ? '#4af0c0' : '#a0b8d0'; ctx.fillText(line, 20, 100 + i * 40); }); - textTexture.needsUpdate = true; }; - - // Initial draw updatePanel(STATE); Broadcaster.subscribe(updatePanel); - - // Scanline effect overlay - const scanGeo = new THREE.PlaneGeometry(w, h); - const scanMat = new THREE.ShaderMaterial({ - transparent: true, - depthWrite: false, - uniforms: { - uTime: { value: 0 }, - uColor: { value: new THREE.Color(color) }, - }, - vertexShader: ` - varying vec2 vUv; - void main() { - vUv = uv; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } - `, - fragmentShader: ` - uniform float uTime; - uniform vec3 uColor; - varying vec2 vUv; - void main() { - float 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); - } - `, - side: THREE.DoubleSide, - }); - const scanMesh = new THREE.Mesh(scanGeo, scanMat); - scanMesh.position.z = 0.02; - group.add(scanMesh); - parent.add(group); - batcaveTerminals.push({ group, scanMat, borderMat }); + batcaveTerminals.push({ group, id }); +} + +// ═══ MEMORY VAULT ═══ +function createMemoryVault() { + const vaultGroup = new THREE.Group(); + vaultGroup.position.set(-15, 0, -10); + vaultGroup.rotation.y = 0.5; + + const pedestalGeo = new THREE.CylinderGeometry(4, 4.5, 0.5, 6); + const pedestalMat = new THREE.MeshStandardMaterial({ color: 0x0a1a2e, roughness: 0.4, metalness: 0.8 }); + const pedestal = new THREE.Mesh(pedestalGeo, pedestalMat); + pedestal.position.y = 0.25; + vaultGroup.add(pedestal); + + 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 = '#00ffff'; lctx.textAlign = 'center'; + lctx.fillText('◈ MEMORY VAULT', 256, 42); + const labelTex = new THREE.CanvasTexture(labelCanvas); + const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(5, 0.6), new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide })); + labelMesh.position.y = 5; + vaultGroup.add(labelMesh); + + MEMORY_VAULT.forEach((mem, i) => { + const angle = (i / MEMORY_VAULT.length) * Math.PI * 2; + const r = 2.5; + const crystalGeo = new THREE.OctahedronGeometry(0.5, 0); + const crystalMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.memory, emissive: NEXUS.colors.memory, emissiveIntensity: 0.5, roughness: 0, metalness: 0.5, transmission: 0.8, thickness: 1 }); + const crystal = new THREE.Mesh(crystalGeo, crystalMat); + crystal.position.set(Math.cos(angle) * r, 2, Math.sin(angle) * r); + crystal.userData = { memory: mem, originalPos: crystal.position.clone() }; + crystal.name = 'memory_crystal'; + vaultGroup.add(crystal); + memoryCrystals.push(crystal); + }); + + scene.add(vaultGroup); } // ═══ PORTAL ═══ @@ -561,559 +318,103 @@ function createPortal() { const portalGroup = new THREE.Group(); portalGroup.position.set(15, 0, -10); portalGroup.rotation.y = -0.5; - - // Portal ring - const torusGeo = new THREE.TorusGeometry(3, 0.15, 16, 64); - const torusMat = new THREE.MeshStandardMaterial({ - color: 0xff6600, - emissive: 0xff4400, - emissiveIntensity: 1.5, - roughness: 0.2, - metalness: 0.8, - }); - portalMesh = new THREE.Mesh(torusGeo, torusMat); + portalMesh = new THREE.Mesh(new THREE.TorusGeometry(3, 0.15, 16, 64), new THREE.MeshStandardMaterial({ color: 0xff6600, emissive: 0xff4400, emissiveIntensity: 1.5 })); portalMesh.position.y = 3.5; portalGroup.add(portalMesh); - - // Inner swirl - const swirlGeo = new THREE.CircleGeometry(2.8, 64); - const swirlMat = new THREE.ShaderMaterial({ - transparent: true, - side: THREE.DoubleSide, - uniforms: { - uTime: { value: 0 }, - }, - vertexShader: ` - varying vec2 vUv; - void main() { - vUv = uv; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } - `, - fragmentShader: ` - uniform float uTime; - 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(vec3(1.0, 0.3, 0.0), vec3(1.0, 0.6, 0.1), swirl); - col = mix(col, vec3(1.0, 0.8, 0.3), swirl2 * 0.3); - float alpha = mask * (0.5 + 0.3 * swirl); - gl_FragColor = vec4(col, alpha); - } - `, - }); - portalGlow = new THREE.Mesh(swirlGeo, swirlMat); - portalGlow.position.y = 3.5; - portalGroup.add(portalGlow); - - // 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 = '#ff8844'; - lctx.textAlign = 'center'; - lctx.fillText('◈ MORROWIND', 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; - portalGroup.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: 0xff4400, - emissiveIntensity: 0.1, - }); - const pillar = new THREE.Mesh(pillarGeo, pillarMat); - pillar.position.set(side * 3, 3.5, 0); - pillar.castShadow = true; - portalGroup.add(pillar); - } - scene.add(portalGroup); } // ═══ PARTICLES ═══ function createParticles() { - const count = particleCount(1500); + const count = particleCount(1000); 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); - - 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; - - sizes[i] = 0.02 + Math.random() * 0.06; - } - - 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({ - 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); + const pos = new Float32Array(count * 3); + for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5)*60; pos[i*3+1] = Math.random()*20; pos[i*3+2] = (Math.random()-0.5)*60; } + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + particles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x4af0c0, size: 0.05, transparent: true, opacity: 0.4 })); scene.add(particles); } function createDustParticles() { - const count = particleCount(500); + const count = particleCount(300); const geo = new THREE.BufferGeometry(); - const 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; - } - - 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); + const pos = new Float32Array(count * 3); + for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5)*40; pos[i*3+1] = Math.random()*15; pos[i*3+2] = (Math.random()-0.5)*40; } + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + dustParticles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x8899bb, size: 0.02, transparent: true, opacity: 0.2 })); scene.add(dustParticles); } -// ═══ AMBIENT STRUCTURES ═══ function createAmbientStructures() { - // Crystal formations around the edges - const crystalMat = new THREE.MeshPhysicalMaterial({ - color: 0x3355aa, - roughness: 0.1, - metalness: 0.2, - transmission: 0.6, - thickness: 2, - emissive: 0x1122aa, - emissiveIntensity: 0.3, - }); - - 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 }, - ]; - - 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); - }); - - // Floating rune stones - for (let i = 0; i < 5; i++) { - const angle = (i / 5) * Math.PI * 2; - const r = 10; - const geo = new THREE.OctahedronGeometry(0.4, 0); - const mat = new THREE.MeshStandardMaterial({ - color: NEXUS.colors.primary, - emissive: NEXUS.colors.primary, - emissiveIntensity: 0.5, - }); - const stone = new THREE.Mesh(geo, mat); - stone.position.set(Math.cos(angle) * r, 5 + Math.sin(i * 1.3) * 1.5, Math.sin(angle) * r); - stone.name = 'runestone_' + i; - scene.add(stone); - } - - // Central pedestal / nexus core - 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'; + const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.6, 2), new THREE.MeshPhysicalMaterial({ color: 0x4af0c0, emissive: 0x4af0c0, emissiveIntensity: 2 })); + core.position.set(0, 2.5, 0); core.name = 'nexus-core'; scene.add(core); - - // Core pedestal - 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); -} - -// ═══ NAVIGATION MODE ═══ -function cycleNavMode() { - navModeIdx = (navModeIdx + 1) % NAV_MODES.length; - const mode = NAV_MODES[navModeIdx]; - - // Sync orbit target/radius from current camera when switching into orbit - if (mode === 'orbit') { - const dir = new THREE.Vector3(0, 0, -1).applyEuler(playerRot); - orbitState.target.copy(playerPos).addScaledVector(dir, orbitState.radius); - orbitState.target.y = Math.max(0, orbitState.target.y); - // Recompute angles from current camera → target vector - const toCamera = new THREE.Vector3().subVectors(playerPos, orbitState.target); - orbitState.radius = toCamera.length(); - orbitState.theta = Math.atan2(toCamera.x, toCamera.z); - orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius))); - } - // Sync fly Y from current walk position - if (mode === 'fly') flyY = playerPos.y; - - updateNavModeUI(mode); -} - -function updateNavModeUI(mode) { - const el = document.getElementById('nav-mode-label'); - if (el) el.textContent = mode.toUpperCase(); } // ═══ CONTROLS ═══ function setupControls() { - 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(); - } - // V cycles navigation modes - if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) { - cycleNavMode(); - } - }); - document.addEventListener('keyup', (e) => { - keys[e.key.toLowerCase()] = false; - }); - - // Mouse look / orbit drag + document.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; if (e.key.toLowerCase() === 'v') cycleNavMode(); }); + 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; + mouseDown = true; orbitState.lastX = e.clientX; orbitState.lastY = e.clientY; + // Raycasting for memory crystals + mouse.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(memoryCrystals); + if (intersects.length > 0) { + STATE.selectedMemory = intersects[0].object.userData.memory; + Broadcaster.broadcast(); } }); 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)); - } else { - // Walk and Fly: mouse look - 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)); - } - }); - - // Scroll to zoom in orbit mode - canvas.addEventListener('wheel', (e) => { if (NAV_MODES[navModeIdx] === 'orbit') { - orbitState.radius = Math.max(orbitState.minR, Math.min(orbitState.maxR, orbitState.radius + e.deltaY * 0.02)); - } - }, { passive: true }); - - // Chat toggle - document.getElementById('chat-toggle').addEventListener('click', () => { - chatOpen = !chatOpen; - document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen); + 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; } }); - document.getElementById('chat-header')?.addEventListener('click', () => { - chatOpen = !chatOpen; - document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen); - }); - - // Chat send - document.getElementById('chat-send').addEventListener('click', sendChatMessage); } -function sendChatMessage() { - const input = document.getElementById('chat-input'); - const text = input.value.trim(); - if (!text) return; - - addChatMessage('user', text); - input.value = ''; - - // Simulate Timmy response - 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); - }, 500 + Math.random() * 1000); - - input.blur(); -} - -function addChatMessage(type, text) { - const container = document.getElementById('chat-messages'); - const div = document.createElement('div'); - div.className = `chat-msg chat-msg-${type}`; - const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' }; - div.innerHTML = `${prefixes[type] || '[???]'} ${text}`; - container.appendChild(div); - container.scrollTop = container.scrollHeight; -} +function cycleNavMode() { navModeIdx = (navModeIdx + 1) % NAV_MODES.length; document.getElementById('nav-mode-label').textContent = NAV_MODES[navModeIdx].toUpperCase(); } // ═══ GAME LOOP ═══ function gameLoop() { requestAnimationFrame(gameLoop); - const delta = Math.min(clock.getDelta(), 0.1); - const elapsed = clock.elapsedTime; - - // ─── Sovereign State Update ───────────────────────────────────── + const delta = Math.min(clock.getDelta(), 0.1), elapsed = clock.elapsedTime; updateSovereignState(elapsed); - // ─── Navigation update ─────────────────────────────────────────── const mode = NAV_MODES[navModeIdx]; - const chatActive = document.activeElement === document.getElementById('chat-input'); - if (mode === 'walk') { - if (!chatActive) { - 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); - // Clamp to platform - 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; } - } - } - playerPos.y = 2; // fixed eye height in walk mode - camera.position.copy(playerPos); - camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ'); - + 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) playerPos.add(dir.normalize().multiplyScalar(6 * delta).applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y)); + playerPos.y = 2; camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ'); } else if (mode === 'orbit') { - // Pan target with WASD - if (!chatActive) { - 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)); - } - } - // Position camera on sphere around target - 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.position.set(orbitState.target.x + orbitState.radius * Math.sin(orbitState.phi) * Math.sin(orbitState.theta), orbitState.target.y + orbitState.radius * Math.cos(orbitState.phi), orbitState.target.z + orbitState.radius * Math.sin(orbitState.phi) * Math.cos(orbitState.theta)); camera.lookAt(orbitState.target); - // Keep playerPos in sync so switching back to walk is smooth - playerPos.copy(camera.position); - playerRot.y = orbitState.theta; - - } else if (mode === 'fly') { - if (!chatActive) { - 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; - } - camera.position.copy(playerPos); - camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ'); } - // Animate skybox - const sky = scene.getObjectByName('skybox'); - if (sky) sky.material.uniforms.uTime.value = elapsed; - - // Animate terminal scanlines - batcaveTerminals.forEach(t => { - if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed; + memoryCrystals.forEach((c, i) => { + c.position.y = c.userData.originalPos.y + Math.sin(elapsed * 1.5 + i) * 0.2; + c.rotation.y = elapsed * 0.5; + const isSelected = STATE.selectedMemory && STATE.selectedMemory.id === c.userData.memory.id; + c.material.emissiveIntensity = isSelected ? 2.0 : 0.5 + Math.sin(elapsed * 2 + i) * 0.2; + c.scale.setScalar(isSelected ? 1.3 : 1.0); }); - // Animate portal - if (portalMesh) { - portalMesh.rotation.z = elapsed * 0.3; - portalMesh.rotation.x = Math.sin(elapsed * 0.5) * 0.1; - } - if (portalGlow?.material?.uniforms) { - portalGlow.material.uniforms.uTime.value = elapsed; - } - - // Animate particles - if (particles?.material?.uniforms) { - particles.material.uniforms.uTime.value = elapsed; - } - - // Animate dust - if (dustParticles) { - dustParticles.rotation.y = elapsed * 0.01; - } - - // Animate runestones - for (let i = 0; i < 5; i++) { - const stone = scene.getObjectByName('runestone_' + i); - if (stone) { - stone.position.y = 5 + Math.sin(elapsed * 0.8 + i * 1.3) * 0.8; - stone.rotation.y = elapsed * 0.5 + i; - stone.rotation.x = elapsed * 0.3 + i * 0.7; - } - } - - // Animate nexus core 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 pulses in sync with state updates (simulated heartbeat) - core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5; - } + if (core) core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5; - // Render composer.render(); - - // Debug overlay (read AFTER render so counts are populated) frameCount++; - const now = performance.now(); - if (now - lastFPSTime >= 1000) { - fps = frameCount; - frameCount = 0; - lastFPSTime = now; - // Update state metrics - STATE.metrics.fps = fps; - STATE.metrics.drawCalls = renderer.info.render.calls; - STATE.metrics.triangles = renderer.info.render.triangles; - } - if (debugOverlay) { - debugOverlay.textContent = - `FPS: ${fps} Draw: ${renderer.info.render.calls} Tri: ${renderer.info.render.triangles} [${performanceTier}]\n` + - `Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`; - } - renderer.info.reset(); + if (performance.now() - lastFPSTime >= 1000) { fps = frameCount; frameCount = 0; lastFPSTime = performance.now(); STATE.metrics.fps = fps; } + if (debugOverlay) debugOverlay.textContent = `FPS: ${fps} [${performanceTier}] Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`; } -// ═══ RESIZE ═══ -function onResize() { - const w = window.innerWidth; - const h = window.innerHeight; - camera.aspect = w / h; - camera.updateProjectionMatrix(); - renderer.setSize(w, h); - composer.setSize(w, h); -} +function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); } -// ═══ START ═══ init();