Files
the-nexus/public/nexus/app.js

2551 lines
81 KiB
JavaScript

import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
// ═══════════════════════════════════════════
// NEXUS 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 = `<span class="symbolic-rule">[RULE] ${ruleDesc}</span><span class="symbolic-outcome">→ ${outcome}</span>`;
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 = `<span class="bb-source">[${source}]</span> <span class="bb-key">${key}</span>: <span class="bb-value">${JSON.stringify(value)}</span>`;
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 = '<div class="planner-empty">NO ACTIVE PLAN</div>';
return;
}
plan.forEach((step, i) => {
const div = document.createElement('div');
div.className = 'planner-step';
div.innerHTML = `<span class="step-num">${i+1}.</span> ${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 = `
<div class="cbr-match">SIMILAR CASE FOUND (${(this.calculateSimilarity(symbolicEngine.facts, c.situation) * 100).toFixed(0)}%)</div>
<div class="cbr-action">SUGGESTED: ${c.action}</div>
<div class="cbr-outcome">PREVIOUS OUTCOME: ${c.outcome}</div>
`;
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 = `
<div class="atlas-card-header">
<div class="atlas-card-name">${config.name}</div>
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
</div>
<div class="atlas-card-desc">${config.description}</div>
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
</div>
`;
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 = `<span class="agent-log-tag tag-${agentId}">[${agentId.toUpperCase()}]</span><span class="agent-log-text">${text}</span>`;
container.prepend(entry);
if (container.children.length > 6) {
container.lastElementChild.remove();
}
}
function triggerHarnessPulse() {
if (!harnessPulseMesh) return;
harnessPulseMesh.scale.setScalar(0.1);
harnessPulseMesh.material.opacity = 0.8;
// 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);
});