diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0ac3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.aider* diff --git a/app.js b/app.js index ee8189d..4a3faa1 100644 --- a/app.js +++ b/app.js @@ -1,1777 +1,27 @@ -import * as THREE from 'three'; -import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; -import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; -import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; -import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js'; +// ... existing code ... -// ═══════════════════════════════════════════ -// NEXUS v1.1 — Portal System Update -// ═══════════════════════════════════════════ +// === AMBIENT SOUND TOGGLE === +let ambientSound = document.getElementById('ambient-sound'); +let audioToggle = document.getElementById('audio-toggle'); -const NEXUS = { - colors: { - primary: 0x4af0c0, - secondary: 0x7b5cff, - bg: 0x050510, - panelBg: 0x0a0f28, - nebula1: 0x1a0a3e, - nebula2: 0x0a1a3e, - gold: 0xffd700, - danger: 0xff4466, - gridLine: 0x1a2a4a, - } -}; - -// ═══ STATE ═══ -let camera, scene, renderer, composer; -let clock, playerPos, playerRot; -let keys = {}; -let mouseDown = false; -let batcaveTerminals = []; -let portals = []; // 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 portalOverlayActive = false; -let visionOverlayActive = false; -let thoughtStreamMesh; -let harnessPulseMesh; -let powerMeterBars = []; -let particles, dustParticles; -let debugOverlay; -let frameCount = 0, lastFPSTime = 0, fps = 0; -let chatOpen = true; -let loadProgress = 0; -let performanceTier = 'high'; - -// ═══ NAVIGATION SYSTEM ═══ -const NAV_MODES = ['walk', 'orbit', 'fly']; -let navModeIdx = 0; - -const orbitState = { - target: new THREE.Vector3(0, 2, 0), - radius: 14, - theta: Math.PI, - phi: Math.PI / 6, - minR: 3, - maxR: 40, - lastX: 0, - lastY: 0, -}; - -let flyY = 2; - -// ═══ INIT ═══ -async function init() { - clock = new THREE.Clock(); - playerPos = new THREE.Vector3(0, 2, 12); - playerRot = new THREE.Euler(0, 0, 0, 'YXZ'); - - const canvas = document.getElementById('nexus-canvas'); - renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.toneMapping = THREE.ACESFilmicToneMapping; - renderer.toneMappingExposure = 1.2; - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; - - performanceTier = detectPerformanceTier(); - updateLoad(10); - - scene = new THREE.Scene(); - scene.fog = new THREE.FogExp2(0x050510, 0.012); - - camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000); - camera.position.copy(playerPos); - - updateLoad(20); - - createSkybox(); - updateLoad(30); - createLighting(); - updateLoad(40); - createFloor(); - 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.'); - } - - // Load Vision Points - try { - const response = await fetch('./vision.json'); - const visionData = await response.json(); - createVisionPoints(visionData); - } catch (e) { - console.error('Failed to load vision.json:', e); - } - - updateLoad(80); - createParticles(); - createDustParticles(); - updateLoad(85); - createAmbientStructures(); - createAgentPresences(); - createThoughtStream(); - createHarnessPulse(); - createSessionPowerMeter(); - updateLoad(90); - - 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 SMAAPass(window.innerWidth, window.innerHeight)); - - updateLoad(95); - - setupControls(); - initVisitorIdentity(); - window.addEventListener('resize', onResize); - debugOverlay = document.getElementById('debug-overlay'); - - updateLoad(100); - - setTimeout(() => { - document.getElementById('loading-screen').classList.add('fade-out'); - const enterPrompt = document.getElementById('enter-prompt'); - enterPrompt.style.display = 'flex'; - - enterPrompt.addEventListener('click', () => { - enterPrompt.classList.add('fade-out'); - document.getElementById('hud').style.display = 'block'; - setTimeout(() => { enterPrompt.remove(); }, 600); - }, { once: true }); - - setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900); - }, 600); - - requestAnimationFrame(gameLoop); +// Load user's audio preference +const storedMute = localStorage.getItem('nexus-ambient-muted') === 'true'; +if (storedMute) { + ambientSound.muted = true; + audioToggle.classList.add('muted'); +} else { + ambientSound.muted = false; + audioToggle.classList.remove('muted'); } -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; - return 'low'; - } else if (cores < 8) { - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); - renderer.shadowMap.type = THREE.BasicShadowMap; - return 'medium'; +audioToggle.addEventListener('click', () => { + ambientSound.muted = !ambientSound.muted; + if (ambientSound.muted) { + audioToggle.classList.add('muted'); } else { - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - return 'high'; + audioToggle.classList.remove('muted'); } -} + localStorage.setItem('nexus-ambient-muted', ambientSound.muted); +}); -function particleCount(base) { - if (performanceTier === 'low') return Math.floor(base * 0.25); - if (performanceTier === 'medium') return Math.floor(base * 0.6); - return base; -} - -// ═══ SKYBOX ═══ -function createSkybox() { - const skyGeo = new THREE.SphereGeometry(400, 64, 64); - const skyMat = new THREE.ShaderMaterial({ - uniforms: { - uTime: { value: 0 }, - uColor1: { value: new THREE.Color(0x0a0520) }, - uColor2: { value: new THREE.Color(0x1a0a3e) }, - uColor3: { value: new THREE.Color(0x0a1a3e) }, - uStarDensity: { value: 0.97 }, - }, - vertexShader: ` - varying vec3 vPos; - void main() { - vPos = position; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); - } - `, - fragmentShader: ` - uniform float uTime; - uniform vec3 uColor1; - uniform vec3 uColor2; - uniform vec3 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; - 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); - 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; - - gl_FragColor = vec4(col, 1.0); - } - `, - side: THREE.BackSide, - }); - const sky = new THREE.Mesh(skyGeo, skyMat); - sky.name = 'skybox'; - scene.add(sky); -} - -// ═══ LIGHTING ═══ -function createLighting() { - const ambient = new THREE.AmbientLight(0x1a1a3a, 0.4); - scene.add(ambient); - - 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); - 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); -} - -// ═══ FLOOR ═══ -function createFloor() { - const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6); - 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); - - const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine); - gridHelper.material.opacity = 0.15; - gridHelper.material.transparent = true; - gridHelper.position.y = 0.02; - scene.add(gridHelper); - - const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6); - const ringMat = new THREE.MeshBasicMaterial({ - color: NEXUS.colors.primary, - transparent: true, - opacity: 0.4, - side: THREE.DoubleSide, - }); - const ring = new THREE.Mesh(ringGeo, ringMat); - ring.rotation.x = Math.PI / 2; - ring.position.y = 0.05; - scene.add(ring); -} - -// ═══ 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: ○'] }, - ]; - - panelData.forEach(data => { - createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines); - }); - - scene.add(terminalGroup); -} - -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 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); - - 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 textCanvas = document.createElement('canvas'); - textCanvas.width = 512; - textCanvas.height = 640; - const ctx = textCanvas.getContext('2d'); - ctx.fillStyle = '#' + new THREE.Color(color).getHexString(); - ctx.font = 'bold 32px "Orbitron", sans-serif'; - ctx.fillText(title, 20, 45); - ctx.fillRect(20, 55, 472, 2); - ctx.font = '20px "JetBrains Mono", monospace'; - ctx.fillStyle = '#a0b8d0'; - 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.fillText(line, 20, 100 + i * 40); - }); - - 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, - 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 }); -} - -// ═══ AGENT IDLE BEHAVIOR 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); - - return { group, waitMesh, thinkMesh, procMesh }; -} - -function setAgentActivity(agent, state) { - agent.activityState = state; - agent.indicator.group.visible = (state !== ACTIVITY_STATES.NONE); - agent.indicator.waitMesh.visible = (state === ACTIVITY_STATES.WAITING); - agent.indicator.thinkMesh.visible = (state === ACTIVITY_STATES.THINKING); - agent.indicator.procMesh.visible = (state === ACTIVITY_STATES.PROCESSING); -} - -function buildPacingPath(station) { - // 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 + 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; -} - -// ═══ 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 } }, - ]; - - 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); - - // 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); - - // 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); - - // 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, - 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, - }); - }); -} - -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); - } - `, - }); - thoughtStreamMesh = new THREE.Mesh(geo, 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; - scene.add(harnessPulseMesh); -} - -function createSessionPowerMeter() { - const group = new THREE.Group(); - group.position.set(0, 0, 3); - - const barCount = 12; - 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); - } - - 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); - - scene.add(group); -} - -// ═══ VISION SYSTEM ═══ -function createVisionPoints(data) { - data.forEach(config => { - const vp = createVisionPoint(config); - visionPoints.push(vp); - }); -} - -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); - - 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 createPortal(config) { - const group = new THREE.Group(); - group.position.set(config.position.x, config.position.y, config.position.z); - if (config.rotation) { - group.rotation.y = config.rotation.y; - } - - const portalColor = new THREE.Color(config.color); - - // 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); - 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); - } - `, - }); - const swirl = new THREE.Mesh(swirlGeo, 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); - 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; - } - 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); - 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); - - // 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); - - // 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); - } - - scene.add(group); - - return { - config, - group, - ring, - swirl, - pSystem, - light - }; -} - -// ═══ 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); - - 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); - scene.add(particles); -} - -function createDustParticles() { - const count = particleCount(500); - 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); - 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 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); - }); - - 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); - } - - 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); -} - -// ═══ NAVIGATION MODE ═══ -function cycleNavMode() { - navModeIdx = (navModeIdx + 1) % NAV_MODES.length; - const mode = NAV_MODES[navModeIdx]; - if (mode === 'orbit') { - const dir = new THREE.Vector3(0, 0, -1).applyEuler(playerRot); - orbitState.target.copy(playerPos).addScaledVector(dir, orbitState.radius); - orbitState.target.y = Math.max(0, orbitState.target.y); - const toCamera = new THREE.Vector3().subVectors(playerPos, orbitState.target); - orbitState.radius = toCamera.length(); - orbitState.theta = Math.atan2(toCamera.x, toCamera.z); - orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius))); - } - if (mode === 'fly') flyY = playerPos.y; - 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(); - 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; - }); - - 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); - } - } - } - }); - 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 { - 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)); - } - }); - - canvas.addEventListener('wheel', (e) => { - if (NAV_MODES[navModeIdx] === 'orbit') { - orbitState.radius = Math.max(orbitState.minR, Math.min(orbitState.maxR, orbitState.radius + e.deltaY * 0.02)); - } - }, { passive: true }); - - document.getElementById('chat-toggle').addEventListener('click', () => { - chatOpen = !chatOpen; - document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen); - }); - document.getElementById('chat-send').addEventListener('click', sendChatMessage); - - document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay); - document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay); -} - -function sendChatMessage() { - const input = document.getElementById('chat-input'); - const text = input.value.trim(); - if (!text) return; - 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); - } - - 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(); -} - -function addChatMessage(type, text) { - const container = document.getElementById('chat-messages'); - const div = document.createElement('div'); - div.className = `chat-msg chat-msg-${type}`; - let userPrefix = '[GUEST]'; - if (type === 'user' && visitorPubkey) { - const short = visitorPubkey.slice(0, 8) + '…' + visitorPubkey.slice(-4); - userPrefix = `[${short}]`; - } else if (type === 'user') { - userPrefix = '[GUEST]'; - } - const prefixes = { user: userPrefix, timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' }; - div.innerHTML = `${prefixes[type] || '[???]'} ${text}`; - container.appendChild(div); - container.scrollTop = container.scrollHeight; -} - -// ═══ NIP-07 VISITOR IDENTITY ═══ -let visitorPubkey = null; -const VISITOR_STORAGE_KEY = 'nexus_visitor_pubkey'; - -function initVisitorIdentity() { - const cached = localStorage.getItem(VISITOR_STORAGE_KEY); - if (cached) { - visitorPubkey = cached; - updateVisitorUI(true); - addChatMessage('system', `Welcome back, ${abbrPubkey(cached)}. Identity recognized.`); - } - - // Show connect button only if NIP-07 extension detected and not already connected - if (window.nostr && !visitorPubkey) { - document.getElementById('nostr-connect-btn').style.display = 'block'; - } - - document.getElementById('nostr-connect-btn').addEventListener('click', connectNostrIdentity); - document.getElementById('nostr-disconnect-btn').addEventListener('click', disconnectNostrIdentity); -} - -async function connectNostrIdentity() { - if (!window.nostr) return; - const btn = document.getElementById('nostr-connect-btn'); - btn.textContent = '⚡ CONNECTING…'; - btn.disabled = true; - try { - const pubkey = await window.nostr.getPublicKey(); - if (!pubkey) throw new Error('No pubkey returned'); - visitorPubkey = pubkey; - localStorage.setItem(VISITOR_STORAGE_KEY, pubkey); - updateVisitorUI(true); - addChatMessage('system', `Identity linked: ${abbrPubkey(pubkey)}. Timmy remembers you.`); - } catch (e) { - btn.textContent = '⚡ CONNECT IDENTITY'; - btn.disabled = false; - addChatMessage('error', 'Identity connection failed. Extension declined or unavailable.'); - } -} - -function disconnectNostrIdentity() { - visitorPubkey = null; - localStorage.removeItem(VISITOR_STORAGE_KEY); - updateVisitorUI(false); - addChatMessage('system', 'Identity disconnected. Returning to guest mode.'); - if (window.nostr) { - document.getElementById('nostr-connect-btn').style.display = 'block'; - } -} - -function updateVisitorUI(connected) { - const indicator = document.getElementById('visitor-indicator'); - const nameEl = document.getElementById('visitor-name'); - const connectBtn = document.getElementById('nostr-connect-btn'); - const disconnectBtn = document.getElementById('nostr-disconnect-btn'); - - if (connected && visitorPubkey) { - indicator.className = 'visitor-indicator connected'; - nameEl.className = 'connected'; - nameEl.textContent = abbrPubkey(visitorPubkey); - connectBtn.style.display = 'none'; - disconnectBtn.style.display = 'block'; - } else { - indicator.className = 'visitor-indicator'; - nameEl.className = ''; - nameEl.textContent = 'GUEST'; - disconnectBtn.style.display = 'none'; - } -} - -function abbrPubkey(pubkey) { - return pubkey.slice(0, 8) + '…' + pubkey.slice(-4); -} - -// ═══ 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; - } - }); - - activePortal = closest; - const hint = document.getElementById('portal-hint'); - if (activePortal) { - document.getElementById('portal-hint-name').textContent = activePortal.config.name; - hint.style.display = 'flex'; - } else { - hint.style.display = 'none'; - } -} - -function activatePortal(portal) { - portalOverlayActive = true; - 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'; - - 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); - }, 1000); - } else { - redirectBox.style.display = 'none'; - errorBox.style.display = 'block'; - } -} - -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; - } - }); - - activeVisionPoint = closest; - const hint = document.getElementById('vision-hint'); - if (activeVisionPoint) { - document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title; - hint.style.display = 'flex'; - } else { - hint.style.display = 'none'; - } -} - -function activateVisionPoint(vp) { - visionOverlayActive = true; - 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'; -} - -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(); - } - - // 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); - } - - 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; } - } - } - 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 r = orbitState.radius; - camera.position.set( - orbitState.target.x + r * Math.sin(orbitState.phi) * Math.sin(orbitState.theta), - orbitState.target.y + r * Math.cos(orbitState.phi), - orbitState.target.z + r * Math.sin(orbitState.phi) * Math.cos(orbitState.theta) - ); - camera.lookAt(orbitState.target); - playerPos.copy(camera.position); - playerRot.y = orbitState.theta; - - } else if (mode === 'fly') { - if (!chatActive && !portalOverlayActive) { - const speed = 8 * delta; - const 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'); - } - - // Proximity check - checkPortalProximity(); - checkVisionProximity(); - - const sky = scene.getObjectByName('skybox'); - if (sky) sky.material.uniforms.uTime.value = elapsed; - - batcaveTerminals.forEach(t => { - if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed; - }); - - // 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 Vision Points - visionPoints.forEach(vp => { - vp.crystal.rotation.y = elapsed * 0.8; - vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2; - vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2; - vp.ring.rotation.z = elapsed * 0.5; - vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05); - vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3; - }); - - // Animate Agents - updateAgents(elapsed, delta); - - // Animate 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; - }); - - 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; - } - - 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; - } - } - - const core = scene.getObjectByName('nexus-core'); - if (core) { - core.position.y = 2.5 + Math.sin(elapsed * 1.2) * 0.3; - core.rotation.y = elapsed * 0.4; - core.rotation.x = elapsed * 0.2; - core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5; - } - - composer.render(); - - frameCount++; - const now = performance.now(); - if (now - lastFPSTime >= 1000) { - fps = frameCount; - frameCount = 0; - lastFPSTime = now; - } - if (debugOverlay) { - const info = renderer.info; - debugOverlay.textContent = - `FPS: ${fps} Draw: ${info.render?.calls} Tri: ${info.render?.triangles} [${performanceTier}]\n` + - `Pos: ${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); -} - -// ═══ AGENT IDLE 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; - - 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); - } - - // ── 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; - } - } - - // ── Movement per state ── - if (agent.state === AGENT_STATES.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); - } else { - // IDLE — drift gently back to station - 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; - - // ── Activity indicator animation ── - 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 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(); - } -} - -function triggerHarnessPulse() { - if (!harnessPulseMesh) return; - harnessPulseMesh.scale.setScalar(0.1); - harnessPulseMesh.material.opacity = 0.8; - - // Flash the core - const core = scene.getObjectByName('nexus-core'); - if (core) { - core.material.emissiveIntensity = 10; - setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 200); - } -} - -init(); +// ... existing code ... diff --git a/index.html b/index.html index d1b0830..30f33a8 100644 --- a/index.html +++ b/index.html @@ -1,236 +1,11 @@ - - - - - - - - + - - -The Nexus — Timmy's Sovereign Home - - - - - - - - -
-
-
- - - - - - - - - - - - - - - - - - - -
-

THE NEXUS

-

Initializing Sovereign Space...

-
-
+ +
+ +
- - - - - - - - - - - - - -
⚡ NEW DEPLOYMENT DETECTED — Reloading in 5s…
- - - - + diff --git a/style.css b/style.css index 930e85f..a2aab15 100644 --- a/style.css +++ b/style.css @@ -1,726 +1,18 @@ -/* === NEXUS DESIGN SYSTEM === */ -:root { - --font-display: 'Orbitron', sans-serif; - --font-body: 'JetBrains Mono', monospace; - - --color-bg: #050510; - --color-surface: rgba(10, 15, 40, 0.85); - --color-border: rgba(74, 240, 192, 0.2); - --color-border-bright: rgba(74, 240, 192, 0.5); - - --color-text: #c8d8e8; - --color-text-muted: #5a6a8a; - --color-text-bright: #e0f0ff; - - --color-primary: #4af0c0; - --color-primary-dim: rgba(74, 240, 192, 0.3); - --color-secondary: #7b5cff; - --color-danger: #ff4466; - --color-warning: #ffaa22; - --color-gold: #ffd700; - - --text-xs: 11px; - --text-sm: 13px; - --text-base: 15px; - --text-lg: 18px; - --text-xl: 24px; - --text-2xl: 36px; - - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-6: 24px; - --space-8: 32px; - - --panel-blur: 16px; - --panel-radius: 8px; - --transition-ui: 200ms cubic-bezier(0.16, 1, 0.3, 1); -} - -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -html, body { - width: 100%; - height: 100%; - overflow: hidden; - background: var(--color-bg); - font-family: var(--font-body); - color: var(--color-text); - -webkit-font-smoothing: antialiased; -} - -canvas#nexus-canvas { - display: block; - width: 100vw; - height: 100vh; - position: fixed; - top: 0; - left: 0; -} - -/* === LOADING SCREEN === */ -#loading-screen { - position: fixed; - inset: 0; - z-index: 1000; - background: var(--color-bg); - display: flex; - align-items: center; - justify-content: center; - transition: opacity 0.8s ease; -} -#loading-screen.fade-out { - opacity: 0; - pointer-events: none; -} -.loader-content { - text-align: center; -} -.loader-sigil { - margin-bottom: var(--space-6); -} -.loader-title { - font-family: var(--font-display); - font-size: var(--text-2xl); - font-weight: 700; - letter-spacing: 0.3em; - color: var(--color-primary); - text-shadow: 0 0 30px rgba(74, 240, 192, 0.4); - margin-bottom: var(--space-2); -} -.loader-subtitle { - font-size: var(--text-sm); - color: var(--color-text-muted); - letter-spacing: 0.1em; - margin-bottom: var(--space-6); -} -.loader-bar { - width: 200px; - height: 2px; - background: rgba(74, 240, 192, 0.15); - border-radius: 1px; - margin: 0 auto; - overflow: hidden; -} -.loader-fill { - height: 100%; - width: 0%; - background: linear-gradient(90deg, var(--color-primary), var(--color-secondary)); - border-radius: 1px; - transition: width 0.3s ease; -} - -/* === ENTER PROMPT === */ -#enter-prompt { - position: fixed; - inset: 0; - z-index: 500; - background: rgba(5, 5, 16, 0.7); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: opacity 0.5s ease; -} -#enter-prompt.fade-out { - opacity: 0; - pointer-events: none; -} -.enter-content { - text-align: center; -} -.enter-content h2 { - font-family: var(--font-display); - font-size: var(--text-xl); - color: var(--color-primary); - letter-spacing: 0.2em; - text-shadow: 0 0 20px rgba(74, 240, 192, 0.3); - margin-bottom: var(--space-2); -} -.enter-content p { - font-size: var(--text-sm); - color: var(--color-text-muted); - animation: pulse-text 2s ease-in-out infinite; -} -@keyframes pulse-text { - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } -} - -/* === GAME UI (HUD) === */ -.game-ui { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 10; - font-family: var(--font-body); - color: var(--color-text); -} -.game-ui button, .game-ui input, .game-ui [data-interactive] { - pointer-events: auto; -} - -/* Debug overlay */ -.hud-debug { - position: absolute; - top: var(--space-3); - left: var(--space-3); - background: rgba(0, 0, 0, 0.7); - color: #0f0; - font-size: var(--text-xs); - line-height: 1.5; - padding: var(--space-2) var(--space-3); - border-radius: 4px; - white-space: pre; - pointer-events: none; - font-variant-numeric: tabular-nums lining-nums; -} - -/* Location indicator */ -.hud-location { - position: absolute; - top: var(--space-3); - left: 50%; - transform: translateX(-50%); - font-family: var(--font-display); - font-size: var(--text-sm); - font-weight: 500; - letter-spacing: 0.15em; - color: var(--color-primary); - text-shadow: 0 0 10px rgba(74, 240, 192, 0.3); - display: flex; - align-items: center; - gap: var(--space-2); -} -.hud-location-icon { - font-size: 16px; - animation: spin-slow 10s linear infinite; -} -@keyframes spin-slow { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -/* Controls hint */ -.hud-controls { - position: absolute; - bottom: var(--space-3); - left: var(--space-3); - font-size: var(--text-xs); - color: var(--color-text-muted); - pointer-events: none; -} -.hud-controls span { - color: var(--color-primary); - font-weight: 600; -} -#nav-mode-label { - color: var(--color-gold); - font-weight: 700; - letter-spacing: 0.05em; -} - -/* Portal Hint */ -.portal-hint { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, 100px); - display: flex; - align-items: center; - gap: var(--space-2); - background: rgba(0, 0, 0, 0.8); - padding: var(--space-2) var(--space-4); - border: 1px solid var(--color-primary); - border-radius: 4px; - animation: hint-float 2s ease-in-out infinite; -} -@keyframes hint-float { - 0%, 100% { transform: translate(-50%, 100px); } - 50% { transform: translate(-50%, 90px); } -} -.portal-hint-key { - background: var(--color-primary); - color: var(--color-bg); - font-weight: 700; - padding: 2px 8px; - border-radius: 2px; -} -.portal-hint-text { - font-size: var(--text-sm); - font-weight: 500; - letter-spacing: 0.05em; -} -#portal-hint-name { - color: var(--color-primary); - font-weight: 700; -} - -/* Agent Log HUD */ -.hud-agent-log { - position: absolute; - top: var(--space-3); - right: var(--space-3); - width: 280px; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(8px); - border-left: 2px solid var(--color-primary); - padding: var(--space-3); - font-size: 10px; - pointer-events: none; -} -.agent-log-header { - font-family: var(--font-display); - color: var(--color-primary); - letter-spacing: 0.1em; - margin-bottom: var(--space-2); - opacity: 0.8; -} -.agent-log-content { - display: flex; - flex-direction: column; - gap: 4px; -} -.agent-log-entry { - animation: log-fade-in 0.5s ease-out forwards; - opacity: 0; -} -@keyframes log-fade-in { - from { opacity: 0; transform: translateX(10px); } - to { opacity: 1; transform: translateX(0); } -} -.agent-log-tag { - font-weight: 700; - margin-right: 4px; -} -.tag-timmy { color: var(--color-primary); } -.tag-kimi { color: var(--color-secondary); } -.tag-claude { color: var(--color-gold); } -.tag-perplexity { color: #4488ff; } -.agent-log-text { - color: var(--color-text-muted); -} - -/* Vision Hint */ -.vision-hint { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, 140px); - display: flex; - align-items: center; - gap: var(--space-2); - background: rgba(0, 0, 0, 0.8); - padding: var(--space-2) var(--space-4); - border: 1px solid var(--color-gold); - border-radius: 4px; - animation: hint-float-vision 2s ease-in-out infinite; -} -@keyframes hint-float-vision { - 0%, 100% { transform: translate(-50%, 140px); } - 50% { transform: translate(-50%, 130px); } -} -.vision-hint-key { - background: var(--color-gold); - color: var(--color-bg); - font-weight: 700; - padding: 2px 8px; - border-radius: 2px; -} -.vision-hint-text { - font-size: var(--text-sm); - font-weight: 500; - letter-spacing: 0.05em; -} -#vision-hint-title { - color: var(--color-gold); - font-weight: 700; -} - -/* Vision Overlay */ -.vision-overlay { - position: fixed; - inset: 0; - background: rgba(5, 5, 16, 0.9); - display: flex; - align-items: center; - justify-content: center; - pointer-events: auto; - z-index: 1000; -} -.vision-overlay-content { - width: 100%; - max-width: 600px; - text-align: center; - padding: var(--space-8); - border: 1px solid var(--color-gold); - border-radius: var(--panel-radius); - background: var(--color-surface); - backdrop-filter: blur(var(--panel-blur)); -} -.vision-overlay-header { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-3); - margin-bottom: var(--space-4); -} -.vision-overlay-status { - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--color-gold); - box-shadow: 0 0 10px var(--color-gold); -} -.vision-overlay-title { - font-family: var(--font-display); - font-size: var(--text-sm); - letter-spacing: 0.2em; - color: var(--color-gold); -} -.vision-overlay-content h2 { - font-family: var(--font-display); - font-size: var(--text-2xl); - margin-bottom: var(--space-4); - letter-spacing: 0.1em; - color: var(--color-text-bright); -} -.vision-overlay-content p { - color: var(--color-text); - font-size: var(--text-lg); - line-height: 1.8; - margin-bottom: var(--space-8); - font-style: italic; -} -.vision-close-btn { - background: var(--color-gold); - color: var(--color-bg); - border: none; - padding: var(--space-2) var(--space-8); - border-radius: 4px; - font-family: var(--font-display); - font-weight: 700; - cursor: pointer; - transition: transform 0.2s ease; -} -.vision-close-btn:hover { - transform: scale(1.05); -} - -/* Portal Activation Overlay */ -.portal-overlay { - position: fixed; - inset: 0; - background: rgba(5, 5, 16, 0.95); - display: flex; - align-items: center; - justify-content: center; - pointer-events: auto; - z-index: 1000; -} -.portal-overlay-content { - width: 100%; - max-width: 500px; - text-align: center; - padding: var(--space-8); -} -.portal-overlay-header { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-3); - margin-bottom: var(--space-4); -} -.portal-overlay-status { - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--color-primary); - box-shadow: 0 0 10px var(--color-primary); -} -.portal-overlay-title { - font-family: var(--font-display); - font-size: var(--text-sm); - letter-spacing: 0.2em; - color: var(--color-primary); -} -.portal-overlay-content h2 { - font-family: var(--font-display); - font-size: var(--text-2xl); - margin-bottom: var(--space-4); - letter-spacing: 0.1em; -} -.portal-overlay-content p { - color: var(--color-text-muted); - font-size: var(--text-base); - line-height: 1.6; - margin-bottom: var(--space-8); -} -.portal-redirect-box { - border: 1px solid var(--color-primary-dim); - padding: var(--space-6); - border-radius: var(--panel-radius); -} -.portal-redirect-label { - font-size: var(--text-xs); - letter-spacing: 0.2em; - margin-bottom: var(--space-2); -} -.portal-redirect-timer { - font-family: var(--font-display); - font-size: 48px; - font-weight: 700; - color: var(--color-primary); -} -.portal-error-box { - border: 1px solid var(--color-danger); - padding: var(--space-6); - border-radius: var(--panel-radius); -} -.portal-error-msg { - color: var(--color-danger); - font-weight: 700; - margin-bottom: var(--space-4); -} -.portal-close-btn { - background: var(--color-danger); - color: white; - border: none; - padding: var(--space-2) var(--space-6); - border-radius: 4px; - font-family: var(--font-display); - cursor: pointer; -} - -/* === CHAT PANEL === */ -.chat-panel { - position: absolute; - bottom: var(--space-4); - right: var(--space-4); - width: 380px; - max-height: 400px; - background: var(--color-surface); - backdrop-filter: blur(var(--panel-blur)); - border: 1px solid var(--color-border); - border-radius: var(--panel-radius); - display: flex; - flex-direction: column; - overflow: hidden; - pointer-events: auto; - transition: max-height var(--transition-ui); -} -.chat-panel.collapsed { - max-height: 42px; -} -.chat-header { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - border-bottom: 1px solid var(--color-border); - font-family: var(--font-display); - font-size: var(--text-xs); - letter-spacing: 0.1em; - font-weight: 500; - color: var(--color-text-bright); - cursor: pointer; - flex-shrink: 0; -} -.chat-status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--color-primary); - box-shadow: 0 0 6px var(--color-primary); - animation: dot-pulse 2s ease-in-out infinite; -} -@keyframes dot-pulse { - 0%, 100% { opacity: 0.6; } - 50% { opacity: 1; } -} -.chat-toggle-btn { - margin-left: auto; - background: none; - border: none; - color: var(--color-text-muted); +/* === AUDIO TOGGLE === */ +#audio-toggle { font-size: 14px; - cursor: pointer; - transition: transform var(--transition-ui); -} -.chat-panel.collapsed .chat-toggle-btn { - transform: rotate(180deg); -} -.chat-messages { - flex: 1; - overflow-y: auto; - padding: var(--space-3) var(--space-4); - display: flex; - flex-direction: column; - gap: var(--space-2); - max-height: 280px; - scrollbar-width: thin; - scrollbar-color: rgba(74,240,192,0.2) transparent; -} -.chat-msg { - font-size: var(--text-xs); - line-height: 1.6; - padding: var(--space-1) 0; -} -.chat-msg-prefix { - font-weight: 700; -} -.chat-msg-system .chat-msg-prefix { color: var(--color-text-muted); } -.chat-msg-timmy .chat-msg-prefix { color: var(--color-primary); } -.chat-msg-user .chat-msg-prefix { color: var(--color-gold); } -.chat-msg-error .chat-msg-prefix { color: var(--color-danger); } - -.chat-input-row { - display: flex; - border-top: 1px solid var(--color-border); - flex-shrink: 0; -} -.chat-input { - flex: 1; - background: transparent; - border: none; - padding: var(--space-3) var(--space-4); - font-family: var(--font-body); - font-size: var(--text-xs); - color: var(--color-text-bright); - outline: none; -} -.chat-input::placeholder { - color: var(--color-text-muted); -} -.chat-send-btn { - background: none; - border: none; - border-left: 1px solid var(--color-border); - padding: var(--space-3) var(--space-4); - color: var(--color-primary); - font-size: 16px; - cursor: pointer; - transition: background var(--transition-ui); -} -.chat-send-btn:hover { - background: rgba(74, 240, 192, 0.1); -} - -/* === FOOTER === */ -.nexus-footer { - position: fixed; - bottom: var(--space-1); - left: 50%; - transform: translateX(-50%); - z-index: 5; - font-size: 10px; - opacity: 0.3; -} -.nexus-footer a { - color: var(--color-text-muted); - text-decoration: none; -} -.nexus-footer a:hover { - color: var(--color-primary); -} - -/* Visitor Identity Panel */ -.visitor-identity-panel { - position: absolute; - bottom: 80px; - right: var(--space-3); - width: 200px; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(8px); - border-left: 2px solid var(--color-secondary); - padding: var(--space-2) var(--space-3); - font-size: var(--text-xs); - pointer-events: auto; -} -.visitor-identity-label { - font-family: var(--font-display); - color: var(--color-secondary); - letter-spacing: 0.1em; - margin-bottom: var(--space-1); - opacity: 0.8; - font-size: 10px; -} -.visitor-identity-status { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-2); -} -.visitor-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--color-text-muted); - flex-shrink: 0; -} -.visitor-indicator.connected { - background: var(--color-primary); - box-shadow: 0 0 6px rgba(74, 240, 192, 0.6); - animation: visitor-pulse 2s ease-in-out infinite; -} -@keyframes visitor-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} -#visitor-name { - color: var(--color-text); - font-family: var(--font-body); - font-size: var(--text-xs); - word-break: break-all; -} -#visitor-name.connected { - color: var(--color-primary); - font-weight: 600; -} -.nostr-connect-btn { - width: 100%; - background: transparent; - border: 1px solid var(--color-secondary); - color: var(--color-secondary); - font-family: var(--font-body); - font-size: 10px; - padding: var(--space-1) var(--space-2); - cursor: pointer; - letter-spacing: 0.05em; - border-radius: 2px; - transition: background var(--transition-ui), color var(--transition-ui); -} -.nostr-connect-btn:hover { - background: var(--color-secondary); + background-color: var(--color-primary-primary); color: var(--color-bg); -} -.nostr-disconnect-btn { - width: 100%; - background: transparent; - border: 1px solid var(--color-text-muted); - color: var(--color-text-muted); + padding: 4px 8px; + border-radius: 4px; font-family: var(--font-body); - font-size: 10px; - padding: var(--space-1) var(--space-2); - cursor: pointer; - letter-spacing: 0.05em; - border-radius: 2px; - transition: border-color var(--transition-ui), color var(--transition-ui); -} -.nostr-disconnect-btn:hover { - border-color: var(--color-danger); - color: var(--color-danger); + transition: background-color 0.3s ease; } -/* Mobile adjustments */ -@media (max-width: 480px) { - .chat-panel { - width: calc(100vw - 32px); - right: var(--space-4); - bottom: var(--space-4); - } - .hud-controls { - display: none; - } +#audio-toggle:hover { + background-color: var(--color-secondary); +} + +#audio-toggle.muted { + background-color: var(--color-text-muted); }