diff --git a/public/nexus/app.js b/public/nexus/app.js new file mode 100644 index 0000000..b30690f --- /dev/null +++ b/public/nexus/app.js @@ -0,0 +1,2550 @@ +import * as THREE from 'three'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; +import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js'; + +// ═══════════════════════════════════════════ +// NEXUS v1.1 — Portal System Update +// ═══════════════════════════════════════════ + +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 atlasOverlayActive = false; +let thoughtStreamMesh; +let harnessPulseMesh; +let powerMeterBars = []; +let particles, dustParticles, ambientLight; +let debugOverlay; +let frameCount = 0, lastFPSTime = 0, fps = 0; +let chatOpen = true; +let loadProgress = 0; +let performanceTier = 'high'; + +// ═══ HERMES WS STATE ═══ +let hermesWs = null; +let wsReconnectTimer = null; +let wsConnected = false; +let recentToolOutputs = []; +let workshopPanelCtx = null; +let workshopPanelTexture = null; +let workshopPanelCanvas = null; +let workshopScanMat = null; +let workshopPanelRefreshTimer = 0; +let lastFocusedPortal = null; + +// ═══ 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; + +// ═══ SOVEREIGN SYMBOLIC ENGINE (GOFAI) ═══ +class SymbolicEngine { + constructor() { + this.facts = new Map(); + this.rules = []; + this.reasoningLog = []; + } + + addFact(key, value) { + this.facts.set(key, value); + } + + addRule(condition, action, description) { + this.rules.push({ condition, action, description }); + } + + reason() { + this.rules.forEach(rule => { + if (rule.condition(this.facts)) { + const result = rule.action(this.facts); + if (result) { + this.logReasoning(rule.description, result); + } + } + }); + } + + logReasoning(ruleDesc, outcome) { + const entry = { + timestamp: Date.now(), + rule: ruleDesc, + outcome: outcome + }; + this.reasoningLog.unshift(entry); + if (this.reasoningLog.length > 5) this.reasoningLog.pop(); + + // Update HUD if available + const container = document.getElementById('symbolic-log-content'); + if (container) { + const logDiv = document.createElement('div'); + logDiv.className = 'symbolic-log-entry'; + logDiv.innerHTML = `[RULE] ${ruleDesc}→ ${outcome}`; + container.prepend(logDiv); + if (container.children.length > 5) container.lastElementChild.remove(); + } + } +} + +class AgentFSM { + constructor(agentId, initialState) { + this.agentId = agentId; + this.state = initialState; + this.transitions = {}; + } + + addTransition(fromState, toState, condition) { + if (!this.transitions[fromState]) this.transitions[fromState] = []; + this.transitions[fromState].push({ toState, condition }); + } + + update(facts) { + const possibleTransitions = this.transitions[this.state] || []; + for (const transition of possibleTransitions) { + if (transition.condition(facts)) { + console.log(`[FSM] Agent ${this.agentId} transitioning: ${this.state} -> ${transition.toState}`); + this.state = transition.toState; + return true; + } + } + return false; + } +} + +// ═══ SOVEREIGN KNOWLEDGE GRAPH (SEMANTIC MEMORY) ═══ +class KnowledgeGraph { + constructor() { + this.nodes = new Map(); // id -> { data } + this.edges = []; // { from, to, relation } + } + + addNode(id, type, metadata = {}) { + this.nodes.set(id, { id, type, ...metadata }); + } + + addEdge(from, to, relation) { + this.edges.push({ from, to, relation }); + } + + query(from, relation) { + return this.edges + .filter(e => e.from === from && e.relation === relation) + .map(e => this.nodes.get(e.to)); + } + + getRelated(id) { + return this.edges.filter(e => e.from === id || e.to === id); + } +} + +// ═══ BLACKBOARD ARCHITECTURE (COLLABORATIVE INTELLIGENCE) ═══ +class Blackboard { + constructor() { + this.data = {}; + this.subscribers = []; + } + + write(key, value, source) { + const oldValue = this.data[key]; + this.data[key] = value; + this.notify(key, value, oldValue, source); + } + + read(key) { + return this.data[key]; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + notify(key, value, oldValue, source) { + this.subscribers.forEach(sub => sub(key, value, oldValue, source)); + + // Log to HUD + const container = document.getElementById('blackboard-log-content'); + if (container) { + const entry = document.createElement('div'); + entry.className = 'blackboard-entry'; + entry.innerHTML = `[${source}] ${key}: ${JSON.stringify(value)}`; + container.prepend(entry); + if (container.children.length > 8) container.lastElementChild.remove(); + } + } +} + +// ═══ SYMBOLIC PLANNER (STRIPS-LIKE) ═══ +class SymbolicPlanner { + constructor() { + this.actions = []; + this.currentPlan = []; + } + + addAction(name, preconditions, effects) { + this.actions.push({ name, preconditions, effects }); + } + + findPlan(initialState, goalState) { + // Simple BFS for planning (for small state spaces) + let queue = [[initialState, []]]; + let visited = new Set([JSON.stringify(initialState)]); + + while (queue.length > 0) { + let [state, plan] = queue.shift(); + + if (this.isGoalReached(state, goalState)) { + return plan; + } + + for (let action of this.actions) { + if (this.arePreconditionsMet(state, action.preconditions)) { + let nextState = { ...state, ...action.effects }; + let stateStr = JSON.stringify(nextState); + if (!visited.has(stateStr)) { + visited.add(stateStr); + queue.push([nextState, [...plan, action.name]]); + } + } + } + } + return null; + } + + isGoalReached(state, goal) { + for (let key in goal) { + if (state[key] !== goal[key]) return false; + } + return true; + } + + arePreconditionsMet(state, preconditions) { + for (let key in preconditions) { + if (state[key] < preconditions[key]) return false; + } + return true; + } + + logPlan(plan) { + this.currentPlan = plan; + const container = document.getElementById('planner-log-content'); + if (container) { + container.innerHTML = ''; + if (!plan || plan.length === 0) { + container.innerHTML = '
NO ACTIVE PLAN
'; + return; + } + plan.forEach((step, i) => { + const div = document.createElement('div'); + div.className = 'planner-step'; + div.innerHTML = `${i+1}. ${step}`; + container.appendChild(div); + }); + } + } +} + +// ═══ FUZZY LOGIC (HANDLING UNCERTAINTY) ═══ +class FuzzyLogic { + static getMembership(value, low, mid, high) { + if (value <= low) return 0; + if (value >= high) return 1; + if (value <= mid) return (value - low) / (mid - low); + return 1 - (value - mid) / (high - mid); + } + + static isLow(value) { return this.getMembership(value, 0, 20, 40); } + static isMedium(value) { return this.getMembership(value, 30, 50, 70); } + static isHigh(value) { return this.getMembership(value, 60, 80, 100); } +} + +// ═══ CASE-BASED REASONER (LEARNING FROM EXPERIENCE) ═══ +class CaseBasedReasoner { + constructor() { + this.caseLibrary = []; + } + + addCase(situation, action, outcome) { + this.caseLibrary.push({ situation, action, outcome, timestamp: Date.now() }); + } + + findSimilarCase(currentSituation) { + let bestMatch = null; + let maxSimilarity = -1; + + this.caseLibrary.forEach(c => { + let similarity = this.calculateSimilarity(currentSituation, c.situation); + if (similarity > maxSimilarity) { + maxSimilarity = similarity; + bestMatch = c; + } + }); + + return maxSimilarity > 0.7 ? bestMatch : null; + } + + calculateSimilarity(s1, s2) { + let score = 0; + let total = 0; + for (let key in s1) { + if (s2[key] !== undefined) { + score += 1 - Math.abs(s1[key] - s2[key]); + total += 1; + } + } + return total > 0 ? score / total : 0; + } + + logCase(c) { + const container = document.getElementById('cbr-log-content'); + if (container) { + const div = document.createElement('div'); + div.className = 'cbr-entry'; + div.innerHTML = ` +
SIMILAR CASE FOUND (${(this.calculateSimilarity(symbolicEngine.facts, c.situation) * 100).toFixed(0)}%)
+
SUGGESTED: ${c.action}
+
PREVIOUS OUTCOME: ${c.outcome}
+ `; + container.prepend(div); + if (container.children.length > 3) container.lastElementChild.remove(); + } + } +} + +let cbr; +let symbolicPlanner; +let knowledgeGraph; +let blackboard; +let symbolicEngine; +let agentFSMs = {}; + +// ═══ INIT ═══ +async function init() { + clock = new THREE.Clock(); + playerPos = new THREE.Vector3(0, 2, 12); + playerRot = new THREE.Euler(0, 0, 0, 'YXZ'); + + // Initialize GOFAI Stack + knowledgeGraph = new KnowledgeGraph(); + blackboard = new Blackboard(); + symbolicEngine = new SymbolicEngine(); + symbolicPlanner = new SymbolicPlanner(); + cbr = new CaseBasedReasoner(); + + setupKnowledgeBase(); + setupSymbolicRules(); + setupPlannerActions(); + setupCaseLibrary(); + + 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(); + createWorkshopTerminal(); + createAshStorm(); + updateLoad(90); + + loadSession(); + connectHermes(); + fetchGiteaData(); + setInterval(fetchGiteaData, 30000); // Refresh every 30s + + 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(); + 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); +} + +function updateLoad(pct) { + loadProgress = pct; + const fill = document.getElementById('load-progress'); + if (fill) fill.style.width = pct + '%'; +} + +function setupKnowledgeBase() { + // Define Core Entities + knowledgeGraph.addNode('nexus', 'location', { name: 'The Nexus Core' }); + knowledgeGraph.addNode('timmy', 'agent', { name: 'Timmy' }); + knowledgeGraph.addNode('evonia', 'layer', { name: 'Evonia System' }); + + // Define Relationships + knowledgeGraph.addEdge('timmy', 'nexus', 'is_at'); + knowledgeGraph.addEdge('nexus', 'evonia', 'monitored_by'); + + // Initialize Blackboard with system defaults + blackboard.write('system_status', 'INITIALIZING', 'SYSTEM'); + blackboard.write('threat_level', 0, 'SYSTEM'); +} + +function setupSymbolicRules() { + // Facts: Energy, Stability, Portal Status + symbolicEngine.addFact('energy', 100); + symbolicEngine.addFact('stability', 1.0); + symbolicEngine.addFact('activePortals', 0); + + // Rule: Low Energy Recovery + symbolicEngine.addRule( + (facts) => facts.get('energy') < 20, + (facts) => { + facts.set('mode', 'RECOVERY'); + return 'Diverting power to core systems'; + }, + 'Low Energy Protocol' + ); + + // Rule: Stability Alert + symbolicEngine.addRule( + (facts) => facts.get('stability') < 0.5, + (facts) => { + facts.set('alert', 'CRITICAL'); + return 'Initiating matrix stabilization'; + }, + 'Stability Safeguard' + ); + + // FSMs for Agents + agentFSMs['timmy'] = new AgentFSM('timmy', 'IDLE'); + agentFSMs['timmy'].addTransition('IDLE', 'ANALYZING', (facts) => facts.get('activePortals') > 0); + agentFSMs['timmy'].addTransition('ANALYZING', 'IDLE', (facts) => facts.get('activePortals') === 0); + agentFSMs['timmy'].addTransition('IDLE', 'ALERT', (facts) => facts.get('stability') < 0.7); + agentFSMs['timmy'].addTransition('ALERT', 'IDLE', (facts) => facts.get('stability') >= 0.7); +} + +function setupPlannerActions() { + symbolicPlanner.addAction('Divert Power', { energy: 20 }, { mode: 'RECOVERY' }); + symbolicPlanner.addAction('Stabilize Matrix', { energy: 50, mode: 'RECOVERY' }, { stability: 1.0 }); + symbolicPlanner.addAction('Open Portal', { energy: 80, stability: 1.0 }, { portals: 'online' }); +} + +function setupCaseLibrary() { + cbr.addCase({ energy: 15, stability: 0.4 }, 'Divert Power', 'SUCCESS'); + cbr.addCase({ energy: 85, stability: 0.9 }, 'Open Portal', 'SUCCESS'); +} + +function updateSymbolicAI(delta, elapsed) { + // Sync facts from world state + const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND'); + if (terminal && terminal.lastState) { + const state = terminal.lastState; + symbolicEngine.addFact('energy', state.tower.energy); + symbolicEngine.addFact('stability', state.matrix.stability); + symbolicEngine.addFact('activePortals', portals.filter(p => p.config.status === 'online').length); + + // Update Blackboard + blackboard.write('nexus_energy', state.tower.energy, 'NEXUS_COMMAND'); + blackboard.write('nexus_stability', state.matrix.stability, 'NEXUS_COMMAND'); + + // Run CBR for suggestions + const currentSituation = { energy: state.tower.energy, stability: state.matrix.stability }; + const matchedCase = cbr.findSimilarCase(currentSituation); + if (matchedCase) cbr.logCase(matchedCase); + + // Run Planner if stability is low + if (state.matrix.stability < 0.5 && (!symbolicPlanner.currentPlan || symbolicPlanner.currentPlan.length === 0)) { + const initialState = { energy: state.tower.energy, stability: state.matrix.stability, mode: 'NORMAL' }; + const goalState = { stability: 1.0 }; + const plan = symbolicPlanner.findPlan(initialState, goalState); + symbolicPlanner.logPlan(plan); + } + } + + // Run reasoning engine + if (Math.floor(elapsed * 2) > Math.floor((elapsed - delta) * 2)) { // Every 0.5s + // Use Fuzzy Logic in rules + const energy = symbolicEngine.facts.get('energy'); + if (FuzzyLogic.isLow(energy) > 0.8) { + symbolicEngine.logReasoning('Fuzzy Energy Check', `Energy is VERY LOW (${(FuzzyLogic.isLow(energy)*100).toFixed(0)}%)`); + } + + symbolicEngine.reason(); + Object.values(agentFSMs).forEach(fsm => fsm.update(symbolicEngine.facts)); + + // Update Knowledge Graph based on portal status + portals.forEach(p => { + const nodeId = `portal_${p.config.id}`; + if (!knowledgeGraph.nodes.has(nodeId)) { + knowledgeGraph.addNode(nodeId, 'portal', { name: p.config.name }); + } + knowledgeGraph.addEdge(nodeId, 'nexus', p.config.status === 'online' ? 'connected_to' : 'disconnected_from'); + }); + } +} + +// ═══ 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'; + } else { + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + return 'high'; + } +} + +function particleCount(base) { + if (performanceTier === 'low') return Math.floor(base * 0.25); + if (performanceTier === 'medium') return Math.floor(base * 0.6); + return base; +} + +// ═══ 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() { + ambientLight = new THREE.AmbientLight(0x1a1a3a, 0.4); + scene.add(ambientLight); + + 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: 'SOVEREIGNTY', color: NEXUS.colors.gold, rot: 0.2, x: 3, y: 3, lines: ['REPLIT: GRADE: A', 'PERPLEXITY: GRADE: A-', 'HERMES: GRADE: B+', 'KIMI: GRADE: B', 'CLAUDE: GRADE: B+'] }, + { title: 'AGENT STATUS', color: NEXUS.colors.primary, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] }, + ]; + + panelData.forEach(data => { + const terminal = createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines); + batcaveTerminals.push(terminal); + }); + + scene.add(terminalGroup); +} + +// ═══ WORKSHOP TERMINAL ═══ +function createWorkshopTerminal() { + const w = 6, h = 4; + const group = new THREE.Group(); + group.position.set(-14, 3, 0); + group.rotation.y = Math.PI / 4; + + workshopPanelCanvas = document.createElement('canvas'); + workshopPanelCanvas.width = 1024; + workshopPanelCanvas.height = 512; + workshopPanelCtx = workshopPanelCanvas.getContext('2d'); + + workshopPanelTexture = new THREE.CanvasTexture(workshopPanelCanvas); + workshopPanelTexture.minFilter = THREE.LinearFilter; + + const panelGeo = new THREE.PlaneGeometry(w, h); + const panelMat = new THREE.MeshBasicMaterial({ + map: workshopPanelTexture, + transparent: true, + opacity: 0.9, + side: THREE.DoubleSide + }); + const panel = new THREE.Mesh(panelGeo, panelMat); + group.add(panel); + + const scanGeo = new THREE.PlaneGeometry(w + 0.1, h + 0.1); + workshopScanMat = new THREE.ShaderMaterial({ + transparent: true, + 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() { + float scan = sin(vUv.y * 200.0 + uTime * 10.0) * 0.05; + float noise = fract(sin(dot(vUv, vec2(12.9898, 78.233))) * 43758.5453) * 0.05; + gl_FragColor = vec4(0.0, 0.1, 0.2, scan + noise); + } + ` + }); + const scan = new THREE.Mesh(scanGeo, workshopScanMat); + scan.position.z = 0.01; + group.add(scan); + + scene.add(group); + refreshWorkshopPanel(); +} + +function refreshWorkshopPanel() { + if (!workshopPanelCtx) return; + const ctx = workshopPanelCtx; + const w = 1024, h = 512; + + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(10, 15, 40, 0.8)'; + ctx.fillRect(0, 0, w, h); + + ctx.fillStyle = '#4af0c0'; + ctx.font = 'bold 40px "Orbitron", sans-serif'; + ctx.fillText('WORKSHOP TERMINAL v1.0', 40, 60); + ctx.fillRect(40, 80, 944, 4); + + ctx.font = '24px "JetBrains Mono", monospace'; + ctx.fillStyle = wsConnected ? '#4af0c0' : '#ff4466'; + ctx.fillText(`HERMES STATUS: ${wsConnected ? 'ONLINE' : 'OFFLINE'}`, 40, 120); + + ctx.fillStyle = '#7b5cff'; + const contextName = activePortal ? activePortal.name.toUpperCase() : 'NEXUS CORE'; + ctx.fillText(`CONTEXT: ${contextName}`, 40, 160); + + ctx.fillStyle = '#a0b8d0'; + ctx.font = 'bold 20px "Orbitron", sans-serif'; + ctx.fillText('TOOL OUTPUT STREAM', 40, 220); + ctx.fillRect(40, 230, 400, 2); + + ctx.font = '16px "JetBrains Mono", monospace'; + recentToolOutputs.slice(-10).forEach((out, i) => { + ctx.fillStyle = out.type === 'call' ? '#ffd700' : '#4af0c0'; + const text = `[${out.agent}] ${out.content.substring(0, 80)}${out.content.length > 80 ? '...' : ''}`; + ctx.fillText(text, 40, 260 + i * 24); + }); + + workshopPanelTexture.needsUpdate = true; +} + +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'); + + const textTexture = new THREE.CanvasTexture(textCanvas); + textTexture.minFilter = THREE.LinearFilter; + + function updatePanelText(newLines) { + ctx.clearRect(0, 0, 512, 640); + 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'; + const displayLines = newLines || lines; + displayLines.forEach((line, i) => { + let fillColor = '#a0b8d0'; + if (line.includes('● RUNNING') || line.includes('● ACTIVE') || line.includes('ONLINE')) fillColor = '#4af0c0'; + else if (line.includes('○ STANDBY') || line.includes('OFFLINE')) fillColor = '#5a6a8a'; + else if (line.includes('NOMINAL')) fillColor = '#4af0c0'; + ctx.fillStyle = fillColor; + ctx.fillText(line, 20, 100 + i * 40); + }); + textTexture.needsUpdate = true; + } + + updatePanelText(); + + 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); + return { group, scanMat, borderMat, updatePanelText, title }; +} + +// ═══ GITEA DATA INTEGRATION ═══ +async function fetchGiteaData() { + try { + const [issuesRes, stateRes] = await Promise.all([ + fetch('/api/gitea/repos/admin/timmy-tower/issues?state=all'), + fetch('/api/gitea/repos/admin/timmy-tower/contents/world_state.json') + ]); + + if (issuesRes.ok) { + const issues = await issuesRes.json(); + updateDevQueue(issues); + updateAgentStatus(issues); + } + + if (stateRes.ok) { + const content = await stateRes.json(); + const worldState = JSON.parse(atob(content.content)); + updateNexusCommand(worldState); + } + } catch (e) { + console.error('Failed to fetch Gitea data:', e); + } +} + +function updateAgentStatus(issues) { + const terminal = batcaveTerminals.find(t => t.title === 'AGENT STATUS'); + if (!terminal) return; + + // Check for Morrowind issues + const morrowindIssues = issues.filter(i => i.title.toLowerCase().includes('morrowind') && i.state === 'open'); + const perplexityStatus = morrowindIssues.length > 0 ? '● MORROWIND' : '○ STANDBY'; + + const lines = [ + '> TIMMY: ● RUNNING', + '> KIMI: ○ STANDBY', + '> CLAUDE: ● ACTIVE', + `> PERPLEXITY: ${perplexityStatus}` + ]; + terminal.updatePanelText(lines); +} + +function updateDevQueue(issues) { + const terminal = batcaveTerminals.find(t => t.title === 'DEV QUEUE'); + if (!terminal) return; + + const lines = issues.slice(0, 4).map(issue => `> #${issue.number}: ${issue.title.substring(0, 15)}...`); + while (lines.length < 4) lines.push('> [EMPTY SLOT]'); + terminal.updatePanelText(lines); +} + +function updateNexusCommand(state) { + const terminal = batcaveTerminals.find(t => t.title === 'NEXUS COMMAND'); + if (!terminal) return; + + terminal.lastState = state; // Store for symbolic engine + + const lines = [ + `> STATUS: ${state.tower.status.toUpperCase()}`, + `> ENERGY: ${state.tower.energy}%`, + `> STABILITY: ${(state.matrix.stability * 100).toFixed(1)}%`, + `> AGENTS: ${state.matrix.active_agents.length}` + ]; + terminal.updatePanelText(lines); +} + +// ═══ 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); + + 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), + wanderTimer: 0 + }); + }); +} + +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); + + const portalObj = { + config, + group, + ring, + swirl, + pSystem, + light, + customElements: {} + }; + + // ═══ DISTINCT VISUAL IDENTITIES ═══ + if (config.id === 'archive') { + // Floating Data Cubes + const cubes = []; + for (let i = 0; i < 6; i++) { + const cubeGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4); + const cubeMat = new THREE.MeshStandardMaterial({ + color: portalColor, + emissive: portalColor, + emissiveIntensity: 1.5, + transparent: true, + opacity: 0.8 + }); + const cube = new THREE.Mesh(cubeGeo, cubeMat); + group.add(cube); + cubes.push(cube); + } + portalObj.customElements.cubes = cubes; + } else if (config.id === 'chapel') { + // Glowing Core + Halo + const coreGeo = new THREE.SphereGeometry(1.2, 32, 32); + const coreMat = new THREE.MeshPhysicalMaterial({ + color: 0xffffff, + emissive: portalColor, + emissiveIntensity: 2, + transparent: true, + opacity: 0.4, + transmission: 0.9, + thickness: 2 + }); + const core = new THREE.Mesh(coreGeo, coreMat); + core.position.y = 3.5; + group.add(core); + portalObj.customElements.core = core; + + const haloGeo = new THREE.TorusGeometry(3.5, 0.05, 16, 100); + const haloMat = new THREE.MeshBasicMaterial({ color: portalColor, transparent: true, opacity: 0.3 }); + const halo = new THREE.Mesh(haloGeo, haloMat); + halo.position.y = 3.5; + group.add(halo); + portalObj.customElements.halo = halo; + } else if (config.id === 'courtyard') { + // Double Rotating Rings + const outerRingGeo = new THREE.TorusGeometry(4.2, 0.1, 16, 80); + const outerRingMat = new THREE.MeshStandardMaterial({ + color: portalColor, + emissive: portalColor, + emissiveIntensity: 0.8, + transparent: true, + opacity: 0.5 + }); + const outerRing = new THREE.Mesh(outerRingGeo, outerRingMat); + outerRing.position.y = 3.5; + group.add(outerRing); + portalObj.customElements.outerRing = outerRing; + } else if (config.id === 'gate') { + // Spiky Monoliths + const spikes = []; + for (let i = 0; i < 8; i++) { + const spikeGeo = new THREE.ConeGeometry(0.2, 1.5, 4); + const spikeMat = new THREE.MeshStandardMaterial({ color: 0x111111, emissive: portalColor, emissiveIntensity: 0.5 }); + const spike = new THREE.Mesh(spikeGeo, spikeMat); + const angle = (i / 8) * Math.PI * 2; + spike.position.set(Math.cos(angle) * 3.5, 3.5 + Math.sin(angle) * 3.5, 0); + spike.rotation.z = angle + Math.PI / 2; + group.add(spike); + spikes.push(spike); + } + portalObj.customElements.spikes = spikes; + + // Darker Swirl + swirl.material.uniforms.uColor.value = new THREE.Color(0x220000); + } + + return portalObj; +} + +// ═══ 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.toLowerCase() === 'm' && document.activeElement !== document.getElementById('chat-input')) { + openPortalAtlas(); + } + if (e.key === 'Escape') { + document.getElementById('chat-input').blur(); + if (portalOverlayActive) closePortalOverlay(); + if (visionOverlayActive) closeVisionOverlay(); + if (atlasOverlayActive) closePortalAtlas(); + } + 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()); + + // Chat quick actions + document.getElementById('chat-quick-actions').addEventListener('click', (e) => { + const btn = e.target.closest('.quick-action-btn'); + if (!btn) return; + + const action = btn.dataset.action; + + switch(action) { + case 'status': + sendChatMessage("Timmy, what is the current system status?"); + break; + case 'agents': + sendChatMessage("Timmy, check on all active agents."); + break; + case 'portals': + openPortalAtlas(); + break; + case 'help': + sendChatMessage("Timmy, I need assistance with Nexus navigation."); + break; + } + }); + + document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay); + document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay); + + document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas); + document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas); +} + +function sendChatMessage(overrideText = null) { + const input = document.getElementById('chat-input'); + const text = overrideText || input.value.trim(); + if (!text) return; + addChatMessage('user', text); + if (!overrideText) input.value = ''; + 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(); +} + +// ═══ HERMES WEBSOCKET ═══ +function connectHermes() { + if (hermesWs) return; + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/api/world/ws`; + + console.log(`Connecting to Hermes at ${wsUrl}...`); + hermesWs = new WebSocket(wsUrl); + + hermesWs.onopen = () => { + console.log('Hermes connected.'); + wsConnected = true; + addChatMessage('system', 'Hermes link established.'); + updateWsHudStatus(true); + refreshWorkshopPanel(); + }; + + hermesWs.onmessage = (evt) => { + try { + const data = JSON.parse(evt.data); + handleHermesMessage(data); + } catch (e) { + console.error('Failed to parse Hermes message:', e); + } + }; + + hermesWs.onclose = () => { + console.warn('Hermes disconnected. Retrying in 5s...'); + wsConnected = false; + hermesWs = null; + updateWsHudStatus(false); + refreshWorkshopPanel(); + if (wsReconnectTimer) clearTimeout(wsReconnectTimer); + wsReconnectTimer = setTimeout(connectHermes, 5000); + }; + + hermesWs.onerror = (err) => { + console.error('Hermes WS error:', err); + }; +} + +let lastHeartbeatTime = 0; +let heartbeatFrequency = 0.6; // Default 0.6Hz +let heartbeatIntensity = 1.0; +let heartbeatSource = 'local'; + +function handleHermesMessage(data) { + if (data.type === 'chat') { + addChatMessage(data.agent || 'timmy', data.text); + } else if (data.type === 'heartbeat') { + heartbeatFrequency = data.frequency || 0.6; + heartbeatIntensity = data.intensity || 1.0; + heartbeatSource = data.source || 'evonia-layer'; + lastHeartbeatTime = Date.now(); + + // Visual feedback for heartbeat sync + const pulseEl = document.querySelector('.heartbeat-pulse'); + if (pulseEl) { + pulseEl.style.borderColor = '#4af0c0'; + pulseEl.style.boxShadow = '0 0 15px #4af0c0'; + setTimeout(() => { + pulseEl.style.borderColor = ''; + pulseEl.style.boxShadow = ''; + }, 100); + } + } else if (data.type === 'tool_call') { + const content = `Calling ${data.tool}(${JSON.stringify(data.args)})`; + recentToolOutputs.push({ type: 'call', agent: data.agent || 'SYSTEM', content }); + addToolMessage(data.agent || 'SYSTEM', 'call', content); + refreshWorkshopPanel(); + } else if (data.type === 'tool_result') { + const content = `Result: ${JSON.stringify(data.result)}`; + recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content }); + addToolMessage(data.agent || 'SYSTEM', 'result', content); + refreshWorkshopPanel(); + } else if (data.type === 'history') { + const container = document.getElementById('chat-messages'); + container.innerHTML = ''; + data.messages.forEach(msg => { + if (msg.type === 'tool_call') addToolMessage(msg.agent, 'call', msg.content, false); + else if (msg.type === 'tool_result') addToolMessage(msg.agent, 'result', msg.content, false); + else addChatMessage(msg.agent, msg.text, false); + }); + } +} + +function updateWsHudStatus(connected) { + const dot = document.querySelector('.chat-status-dot'); + if (dot) { + dot.style.background = connected ? '#4af0c0' : '#ff4466'; + dot.style.boxShadow = connected ? '0 0 10px #4af0c0' : '0 0 10px #ff4466'; + } +} + +// ═══ SESSION PERSISTENCE ═══ +function saveSession() { + const msgs = Array.from(document.querySelectorAll('.chat-msg')).slice(-60).map(el => ({ + html: el.innerHTML, + className: el.className + })); + localStorage.setItem('nexus_chat_history', JSON.stringify(msgs)); +} + +function loadSession() { + const saved = localStorage.getItem('nexus_chat_history'); + if (saved) { + const msgs = JSON.parse(saved); + const container = document.getElementById('chat-messages'); + container.innerHTML = ''; + msgs.forEach(m => { + const div = document.createElement('div'); + div.className = m.className; + div.innerHTML = m.html; + container.appendChild(div); + }); + container.scrollTop = container.scrollHeight; + } +} + +function addChatMessage(agent, text, shouldSave = true) { + const container = document.getElementById('chat-messages'); + const div = document.createElement('div'); + div.className = `chat-msg chat-msg-${agent}`; + + const prefixes = { + user: '[ALEXANDER]', + timmy: '[TIMMY]', + system: '[NEXUS]', + error: '[ERROR]', + kimi: '[KIMI]', + claude: '[CLAUDE]', + perplexity: '[PERPLEXITY]' + }; + + const prefix = document.createElement('span'); + prefix.className = 'chat-msg-prefix'; + prefix.textContent = `${prefixes[agent] || '[' + agent.toUpperCase() + ']'} `; + + div.appendChild(prefix); + div.appendChild(document.createTextNode(text)); + + container.appendChild(div); + container.scrollTop = container.scrollHeight; + + if (shouldSave) saveSession(); +} + +function addToolMessage(agent, type, content, shouldSave = true) { + const container = document.getElementById('chat-messages'); + const div = document.createElement('div'); + div.className = `chat-msg chat-msg-tool tool-${type}`; + + const prefix = document.createElement('div'); + prefix.className = 'chat-msg-prefix'; + prefix.textContent = `[${agent.toUpperCase()} TOOL ${type.toUpperCase()}]`; + + const pre = document.createElement('pre'); + pre.className = 'tool-content'; + pre.textContent = content; + + div.appendChild(prefix); + div.appendChild(pre); + + container.appendChild(div); + container.scrollTop = container.scrollHeight; + + if (shouldSave) saveSession(); +} + +// ═══ 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'; +} + +// ═══ PORTAL ATLAS ═══ +function openPortalAtlas() { + atlasOverlayActive = true; + document.getElementById('atlas-overlay').style.display = 'flex'; + populateAtlas(); +} + +function closePortalAtlas() { + atlasOverlayActive = false; + document.getElementById('atlas-overlay').style.display = 'none'; +} + +function populateAtlas() { + const grid = document.getElementById('atlas-grid'); + grid.innerHTML = ''; + + let onlineCount = 0; + let standbyCount = 0; + + portals.forEach(portal => { + const config = portal.config; + if (config.status === 'online') onlineCount++; + if (config.status === 'standby') standbyCount++; + + const card = document.createElement('div'); + card.className = 'atlas-card'; + card.style.setProperty('--portal-color', config.color); + + const statusClass = `status-${config.status || 'online'}`; + + card.innerHTML = ` +
+
${config.name}
+
${config.status || 'ONLINE'}
+
+
${config.description}
+ + `; + + card.addEventListener('click', () => { + focusPortal(portal); + closePortalAtlas(); + }); + + grid.appendChild(card); + }); + + document.getElementById('atlas-online-count').textContent = onlineCount; + document.getElementById('atlas-standby-count').textContent = standbyCount; + + // Update Bannerlord HUD status + const bannerlord = portals.find(p => p.config.id === 'bannerlord'); + if (bannerlord) { + const statusEl = document.getElementById('bannerlord-status'); + statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline'); + } +} + +function focusPortal(portal) { + // Teleport player to a position in front of the portal + const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0)); + playerPos.copy(portal.group.position).add(offset); + playerPos.y = 2; // Keep at eye level + + // Rotate player to face the portal + playerRot.y = (portal.config.rotation?.y || 0) + Math.PI; + playerRot.x = 0; + + addChatMessage('system', `Navigation focus: ${portal.config.name}`); + + // If in orbit mode, reset target + if (NAV_MODES[navModeIdx] === 'orbit') { + orbitState.target.copy(portal.group.position); + orbitState.target.y = 3.5; + } +} + +// ═══ 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); + } + + updateAshStorm(delta, elapsed); + updateNexusHeartbeat(delta, elapsed); + updateSymbolicAI(delta, elapsed); + + 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; + + // Custom animations for distinct identities + if (portal.config.id === 'archive' && portal.customElements.cubes) { + portal.customElements.cubes.forEach((cube, i) => { + cube.rotation.x += delta * (0.5 + i * 0.1); + cube.rotation.y += delta * (0.3 + i * 0.1); + const orbitSpeed = 0.5 + i * 0.2; + const orbitRadius = 4 + Math.sin(elapsed * 0.5 + i) * 0.5; + cube.position.x = Math.cos(elapsed * orbitSpeed + i) * orbitRadius; + cube.position.z = Math.sin(elapsed * orbitSpeed + i) * orbitRadius; + cube.position.y = 3.5 + Math.sin(elapsed * 1.2 + i) * 1.5; + }); + } + + if (portal.config.id === 'chapel' && portal.customElements.halo) { + portal.customElements.halo.rotation.z -= delta * 0.2; + portal.customElements.halo.scale.setScalar(1 + Math.sin(elapsed * 0.8) * 0.05); + portal.customElements.core.material.emissiveIntensity = 2 + Math.sin(elapsed * 3) * 1; + } + + if (portal.config.id === 'courtyard' && portal.customElements.outerRing) { + portal.customElements.outerRing.rotation.z -= delta * 0.5; + portal.customElements.outerRing.rotation.y = Math.cos(elapsed * 0.4) * 0.2; + } + + if (portal.config.id === 'gate' && portal.customElements.spikes) { + portal.customElements.spikes.forEach((spike, i) => { + const s = 1 + Math.sin(elapsed * 2 + i) * 0.2; + spike.scale.set(s, s, s); + }); + } + + // 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 + agents.forEach((agent, i) => { + // Wander logic + agent.wanderTimer -= delta; + if (agent.wanderTimer <= 0) { + agent.wanderTimer = 3 + Math.random() * 5; + agent.targetPos.set( + agent.station.x + (Math.random() - 0.5) * 4, + 0, + agent.station.z + (Math.random() - 0.5) * 4 + ); + } + agent.group.position.lerp(agent.targetPos, delta * 0.5); + + agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * 0.15; + agent.halo.rotation.z = elapsed * 0.5; + agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1); + agent.orb.material.emissiveIntensity = 2 + Math.sin(elapsed * 4 + i) * 1; + }); + + // 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(); + + updateAshStorm(delta, elapsed); + updatePortalTunnel(delta, elapsed); + + if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime(); + if (activePortal !== lastFocusedPortal) { + lastFocusedPortal = activePortal; + refreshWorkshopPanel(); + } + + 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 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); + } +} + +// ═══ ASH STORM (MORROWIND) ═══ +let ashStormParticles; +function createAshStorm() { + const count = 1000; + const geo = new THREE.BufferGeometry(); + const pos = new Float32Array(count * 3); + const vel = new Float32Array(count * 3); + + for (let i = 0; i < count; i++) { + pos[i * 3] = (Math.random() - 0.5) * 20; + pos[i * 3 + 1] = Math.random() * 10; + pos[i * 3 + 2] = (Math.random() - 0.5) * 20; + + vel[i * 3] = -0.05 - Math.random() * 0.1; + vel[i * 3 + 1] = -0.02 - Math.random() * 0.05; + vel[i * 3 + 2] = (Math.random() - 0.5) * 0.05; + } + + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + geo.setAttribute('velocity', new THREE.BufferAttribute(vel, 3)); + + const mat = new THREE.PointsMaterial({ + color: 0x886644, + size: 0.05, + transparent: true, + opacity: 0, + depthWrite: false, + blending: THREE.AdditiveBlending + }); + + ashStormParticles = new THREE.Points(geo, mat); + ashStormParticles.position.set(15, 0, -10); // Center on Morrowind portal + scene.add(ashStormParticles); +} + +function updateAshStorm(delta, elapsed) { + if (!ashStormParticles) return; + + const morrowindPortalPos = new THREE.Vector3(15, 0, -10); + const dist = playerPos.distanceTo(morrowindPortalPos); + const intensity = Math.max(0, 1 - (dist / 12)); + + ashStormParticles.material.opacity = intensity * 0.4; + + if (intensity > 0) { + const pos = ashStormParticles.geometry.attributes.position.array; + const vel = ashStormParticles.geometry.attributes.velocity.array; + + for (let i = 0; i < pos.length / 3; i++) { + pos[i * 3] += vel[i * 3]; + pos[i * 3 + 1] += vel[i * 3 + 1]; + pos[i * 3 + 2] += vel[i * 3 + 2]; + + if (pos[i * 3 + 1] < 0 || Math.abs(pos[i * 3]) > 10 || Math.abs(pos[i * 3 + 2]) > 10) { + pos[i * 3] = (Math.random() - 0.5) * 20; + pos[i * 3 + 1] = 10; + pos[i * 3 + 2] = (Math.random() - 0.5) * 20; + } + } + ashStormParticles.geometry.attributes.position.needsUpdate = true; + } +} + +function updateNexusHeartbeat(delta, elapsed) { + const heartbeatVal = document.getElementById('heartbeat-value'); + if (heartbeatVal) { + heartbeatVal.textContent = `${heartbeatFrequency.toFixed(2)} Hz`; + heartbeatVal.style.color = heartbeatSource === 'evonia-layer' ? '#4af0c0' : '#7b5cff'; + } + + // Breathing effect for ambient light + if (ambientLight) { + const intensity = 0.3 + Math.sin(elapsed * heartbeatFrequency * Math.PI) * 0.1 * heartbeatIntensity; + ambientLight.intensity = intensity; + } + + // Update heartbeat pulse animation speed + const pulseEl = document.querySelector('.heartbeat-pulse'); + if (pulseEl) { + pulseEl.style.animationDuration = `${1 / heartbeatFrequency}s`; + pulseEl.style.opacity = 0.4 + Math.sin(elapsed * heartbeatFrequency * Math.PI) * 0.4 * heartbeatIntensity; + } +} + +init().then(() => { + createAshStorm(); + createPortalTunnel(); + fetchGiteaData(); + setInterval(fetchGiteaData, 30000); +}); diff --git a/public/nexus/index.html b/public/nexus/index.html new file mode 100644 index 0000000..b6ce465 --- /dev/null +++ b/public/nexus/index.html @@ -0,0 +1,289 @@ + + + + + + + + + + + +The Nexus — Timmy's Sovereign Home + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+

THE NEXUS

+

Initializing Sovereign Space...

+
+
+
+ + + + + + + + + + + + + + +
⚡ NEW DEPLOYMENT DETECTED — Reloading in 5s…
+ + + + diff --git a/public/nexus/style.css b/public/nexus/style.css new file mode 100644 index 0000000..d6e67a0 --- /dev/null +++ b/public/nexus/style.css @@ -0,0 +1,1063 @@ +/* === 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: #e0f0ff; + --color-text-muted: #8a9ab8; + --color-text-bright: #ffffff; + + --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; +} + +/* Top Right Container */ +.hud-top-right { + position: absolute; + top: var(--space-3); + right: var(--space-3); + display: flex; + flex-direction: column; + align-items: flex-end; + gap: var(--space-3); + pointer-events: none; +} +.hud-top-right > * { + pointer-events: auto; +} + +.hud-icon-btn { + background: rgba(10, 15, 40, 0.7); + border: 1px solid var(--color-primary); + color: var(--color-primary); + padding: 8px 12px; + font-family: var(--font-display); + font-size: 11px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all var(--transition-ui); + backdrop-filter: blur(5px); + box-shadow: 0 0 10px rgba(74, 240, 192, 0.2); + letter-spacing: 0.1em; +} + +.hud-icon-btn:hover { + background: var(--color-primary); + color: var(--color-bg); + box-shadow: 0 0 20px var(--color-primary); +} + +.hud-status-item { + display: flex; + align-items: center; + gap: 8px; + background: rgba(0, 0, 0, 0.5); + padding: 4px 12px; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.1); + font-family: var(--font-body); + font-size: 10px; + letter-spacing: 0.1em; + color: var(--color-text-muted); + margin-bottom: 8px; + pointer-events: auto; +} + +.hud-status-item .status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-danger); +} + +.hud-status-item.online .status-dot { + background: var(--color-primary); + box-shadow: 0 0 5px var(--color-primary); +} + +.hud-status-item.standby .status-dot { + background: var(--color-gold); + box-shadow: 0 0 5px var(--color-gold); +} + +.hud-status-item.online .status-label { + color: #fff; +} + +.hud-icon { + font-size: 16px; +} + +/* Portal Atlas Overlay */ +.atlas-overlay { + position: fixed; + inset: 0; + background: rgba(5, 5, 16, 0.9); + backdrop-filter: blur(15px); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + pointer-events: auto; + animation: fadeIn 0.3s ease; +} + +.atlas-content { + width: 100%; + max-width: 1000px; + max-height: 80vh; + background: var(--color-surface); + border: 1px solid var(--color-border); + display: flex; + flex-direction: column; + box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); +} + +.atlas-header { + padding: 20px 30px; + border-bottom: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.atlas-title { + display: flex; + align-items: center; + gap: 15px; +} + +.atlas-title h2 { + margin: 0; + font-family: var(--font-display); + letter-spacing: 2px; + color: var(--color-primary); + font-size: var(--text-lg); +} + +.atlas-close-btn { + background: transparent; + border: 1px solid var(--color-danger); + color: var(--color-danger); + padding: 6px 15px; + font-family: var(--font-display); + font-size: 11px; + cursor: pointer; + transition: all var(--transition-ui); +} + +.atlas-close-btn:hover { + background: var(--color-danger); + color: white; +} + +.atlas-grid { + flex: 1; + overflow-y: auto; + padding: 30px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; +} + +.atlas-card { + background: rgba(20, 30, 60, 0.4); + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 20px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.atlas-card:hover { + background: rgba(30, 45, 90, 0.6); + border-color: var(--color-primary); + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.atlas-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: var(--portal-color, var(--color-primary)); + opacity: 0.6; +} + +.atlas-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.atlas-card-name { + font-family: var(--font-display); + font-size: 16px; + font-weight: 700; + color: #fff; +} + +.atlas-card-status { + font-family: var(--font-body); + font-size: 10px; + padding: 2px 6px; + border-radius: 2px; + text-transform: uppercase; +} + +.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); } +.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); } +.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); } + +.atlas-card-desc { + font-size: 12px; + color: var(--color-text-muted); + line-height: 1.5; + margin-bottom: 15px; +} + +.atlas-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-body); + font-size: 10px; + color: rgba(160, 184, 208, 0.6); +} + +.atlas-footer { + padding: 15px 30px; + border-top: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-body); + font-size: 11px; +} + +.status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 4px; +} + +.status-indicator.online { background: var(--color-primary); box-shadow: 0 0 5px var(--color-primary); } +.status-indicator.standby { background: var(--color-gold); box-shadow: 0 0 5px var(--color-gold); } + +.atlas-hint { + color: rgba(160, 184, 208, 0.5); + font-style: italic; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +/* Responsive Atlas */ +@media (max-width: 768px) { + .atlas-grid { + grid-template-columns: 1fr; + } + .atlas-content { + max-height: 90vh; + } +} + +/* Debug overlay */ +.hud-top-left { + position: absolute; + top: var(--space-3); + left: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-2); + pointer-events: none; +} + +.hud-debug { + 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; + font-variant-numeric: tabular-nums lining-nums; +} + +.hud-heartbeat { + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + border: 1px solid rgba(74, 240, 192, 0.2); + border-radius: 4px; + padding: 8px 12px; + display: flex; + align-items: center; + gap: 10px; + pointer-events: auto; +} + +.heartbeat-pulse { + width: 10px; + height: 10px; + background: var(--color-primary); + border-radius: 50%; + box-shadow: 0 0 10px var(--color-primary); + animation: pulse-heartbeat 2s infinite ease-in-out; +} + +@keyframes pulse-heartbeat { + 0%, 100% { transform: scale(1); opacity: 0.8; } + 50% { transform: scale(1.4); opacity: 1; box-shadow: 0 0 15px var(--color-primary); } +} + +.heartbeat-label { + font-family: var(--font-display); + font-size: 9px; + letter-spacing: 0.1em; + color: var(--color-text-muted); +} + +.heartbeat-value { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-primary); + min-width: 45px; +} + +/* 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, .hud-symbolic-log, .hud-blackboard-log, .hud-planner-log, .hud-cbr-log { + position: absolute; + 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; +} + +.hud-agent-log { top: var(--space-3); } +.hud-symbolic-log { top: 160px; border-left-color: var(--color-gold); } +.hud-blackboard-log { top: 320px; border-left-color: #7b5cff; } +.hud-planner-log { top: 480px; border-left-color: #ff4a4a; } +.hud-cbr-log { top: 640px; border-left-color: #4af0f0; } + +.agent-log-header, .symbolic-log-header, .blackboard-log-header, .planner-log-header, .cbr-log-header { + font-family: var(--font-display); + color: var(--color-primary); + letter-spacing: 0.1em; + margin-bottom: var(--space-2); + opacity: 0.8; +} + +.symbolic-log-header { color: var(--color-gold); } +.blackboard-log-header { color: #7b5cff; } +.planner-log-header { color: #ff4a4a; } +.cbr-log-header { color: #4af0f0; } + +.agent-log-content, .symbolic-log-content, .blackboard-log-content, .planner-log-content, .cbr-log-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.symbolic-log-entry, .blackboard-entry, .planner-step, .cbr-entry { + animation: log-fade-in 0.5s ease-out forwards; + opacity: 0; + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px; + background: rgba(255, 255, 255, 0.03); + border-radius: 2px; +} + +.symbolic-rule { color: var(--color-gold); font-weight: bold; } +.symbolic-outcome { color: var(--color-primary); opacity: 0.8; } + +.bb-source { color: #7b5cff; font-weight: bold; font-size: 9px; } +.bb-key { color: #fff; opacity: 0.6; } +.bb-value { color: var(--color-primary); } + +.planner-step { flex-direction: row; gap: 8px; align-items: center; } +.step-num { color: #ff4a4a; font-weight: bold; } +.planner-empty { color: rgba(255, 255, 255, 0.3); font-style: italic; text-align: center; padding: 10px; } + +.cbr-match { color: #4af0f0; font-weight: bold; font-size: 9px; } +.cbr-action { color: #fff; } +.cbr-outcome { color: var(--color-primary); opacity: 0.6; font-style: italic; } +.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); + transition: all 0.3s ease; +} +@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); + 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-quick-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 8px 12px; + border-top: 1px solid var(--color-border); + background: rgba(0, 0, 0, 0.3); + pointer-events: auto; +} + +.quick-action-btn { + background: rgba(74, 240, 192, 0.1); + border: 1px solid var(--color-primary-dim); + color: var(--color-primary); + font-family: var(--font-body); + font-size: 10px; + padding: 4px 8px; + cursor: pointer; + transition: all var(--transition-ui); + white-space: nowrap; +} + +.quick-action-btn:hover { + background: var(--color-primary-dim); + border-color: var(--color-primary); + color: #fff; +} +.chat-msg { + font-size: var(--text-xs); + line-height: 1.6; + padding: var(--space-1) 0; +} +.chat-msg-prefix { + font-weight: 700; +} +.chat-msg-kimi .chat-msg-prefix { color: var(--color-secondary); } +.chat-msg-claude .chat-msg-prefix { color: var(--color-gold); } +.chat-msg-perplexity .chat-msg-prefix { color: #4488ff; } + +/* Tool Output Styling */ +.chat-msg-tool { + background: rgba(0, 0, 0, 0.3); + border-left: 2px solid #ffd700; + font-size: 11px; + padding: 8px; + margin: 4px 0; + border-radius: 4px; +} +.tool-call { border-left-color: #ffd700; } +.tool-result { border-left-color: #4af0c0; } +.tool-content { + font-family: 'JetBrains Mono', monospace; + white-space: pre-wrap; + word-break: break-all; + opacity: 0.8; + margin: 4px 0 0 0; + color: #a0b8d0; +} +.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); +} + +/* Mobile adjustments */ +@media (max-width: 1024px) { + .chat-panel { + width: 320px; + } + .hud-agent-log { + width: 220px; + } +} + +@media (max-width: 768px) { + .chat-panel { + width: 300px; + bottom: var(--space-2); + right: var(--space-2); + } + .hud-agent-log { + display: none; + } + .hud-location { + font-size: var(--text-xs); + } +} + +@media (max-width: 480px) { + .chat-panel { + width: calc(100vw - 24px); + right: 12px; + bottom: 12px; + } + .hud-controls { + display: none; + } + .loader-title { + font-size: var(--text-xl); + } +}