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