Files
the-nexus/app.js
Google Gemini 58038f2e41
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Nexus Refinement: Vision Points & Narrative Expansion (#44)
Co-authored-by: Google Gemini <gemini@hermes.local>
Co-committed-by: Google Gemini <gemini@hermes.local>
2026-03-24 02:36:12 +00:00

1207 lines
38 KiB
JavaScript

import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
// ═══════════════════════════════════════════
const NEXUS = {
colors: {
primary: 0x4af0c0,
secondary: 0x7b5cff,
bg: 0x050510,
panelBg: 0x0a0f28,
nebula1: 0x1a0a3e,
nebula2: 0x0a1a3e,
gold: 0xffd700,
danger: 0xff4466,
gridLine: 0x1a2a4a,
}
};
// ═══ STATE ═══
let camera, scene, renderer, composer;
let clock, playerPos, playerRot;
let keys = {};
let mouseDown = false;
let batcaveTerminals = [];
let portals = []; // Registry of active portals
let visionPoints = []; // Registry of vision points
let activePortal = null; // Portal currently in proximity
let activeVisionPoint = null; // Vision point currently in proximity
let portalOverlayActive = false;
let visionOverlayActive = false;
let particles, dustParticles;
let debugOverlay;
let frameCount = 0, lastFPSTime = 0, fps = 0;
let chatOpen = true;
let loadProgress = 0;
let performanceTier = 'high';
// ═══ NAVIGATION SYSTEM ═══
const NAV_MODES = ['walk', 'orbit', 'fly'];
let navModeIdx = 0;
const orbitState = {
target: new THREE.Vector3(0, 2, 0),
radius: 14,
theta: Math.PI,
phi: Math.PI / 6,
minR: 3,
maxR: 40,
lastX: 0,
lastY: 0,
};
let flyY = 2;
// ═══ INIT ═══
async function init() {
clock = new THREE.Clock();
playerPos = new THREE.Vector3(0, 2, 12);
playerRot = new THREE.Euler(0, 0, 0, 'YXZ');
const canvas = document.getElementById('nexus-canvas');
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
performanceTier = detectPerformanceTier();
updateLoad(10);
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050510, 0.012);
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.copy(playerPos);
updateLoad(20);
createSkybox();
updateLoad(30);
createLighting();
updateLoad(40);
createFloor();
updateLoad(50);
createBatcaveTerminal();
updateLoad(60);
// Load Portals from Registry
try {
const response = await fetch('./portals.json');
const portalData = await response.json();
createPortals(portalData);
} catch (e) {
console.error('Failed to load portals.json:', e);
addChatMessage('error', 'Portal registry offline. Check logs.');
}
// Load Vision Points
try {
const response = await fetch('./vision.json');
const visionData = await response.json();
createVisionPoints(visionData);
} catch (e) {
console.error('Failed to load vision.json:', e);
}
updateLoad(80);
createParticles();
createDustParticles();
updateLoad(85);
createAmbientStructures();
updateLoad(90);
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.6, 0.4, 0.85
);
composer.addPass(bloom);
composer.addPass(new SMAAPass(window.innerWidth, window.innerHeight));
updateLoad(95);
setupControls();
window.addEventListener('resize', onResize);
debugOverlay = document.getElementById('debug-overlay');
updateLoad(100);
setTimeout(() => {
document.getElementById('loading-screen').classList.add('fade-out');
const enterPrompt = document.getElementById('enter-prompt');
enterPrompt.style.display = 'flex';
enterPrompt.addEventListener('click', () => {
enterPrompt.classList.add('fade-out');
document.getElementById('hud').style.display = 'block';
setTimeout(() => { enterPrompt.remove(); }, 600);
}, { once: true });
setTimeout(() => { document.getElementById('loading-screen').remove(); }, 900);
}, 600);
requestAnimationFrame(gameLoop);
}
function updateLoad(pct) {
loadProgress = pct;
const fill = document.getElementById('load-progress');
if (fill) fill.style.width = pct + '%';
}
// ═══ PERFORMANCE BUDGET ═══
function detectPerformanceTier() {
const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
const cores = navigator.hardwareConcurrency || 4;
if (isMobile) {
renderer.setPixelRatio(1);
renderer.shadowMap.enabled = false;
return 'low';
} else if (cores < 8) {
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.shadowMap.type = THREE.BasicShadowMap;
return 'medium';
} else {
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
return 'high';
}
}
function particleCount(base) {
if (performanceTier === 'low') return Math.floor(base * 0.25);
if (performanceTier === 'medium') return Math.floor(base * 0.6);
return base;
}
// ═══ SKYBOX ═══
function createSkybox() {
const skyGeo = new THREE.SphereGeometry(400, 64, 64);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor1: { value: new THREE.Color(0x0a0520) },
uColor2: { value: new THREE.Color(0x1a0a3e) },
uColor3: { value: new THREE.Color(0x0a1a3e) },
uStarDensity: { value: 0.97 },
},
vertexShader: `
varying vec3 vPos;
void main() {
vPos = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform float uStarDensity;
varying vec3 vPos;
float hash(vec3 p) {
p = fract(p * vec3(443.897, 441.423, 437.195));
p += dot(p, p.yzx + 19.19);
return fract((p.x + p.y) * p.z);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x),
mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
f.z
);
}
float fbm(vec3 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec3 dir = normalize(vPos);
float n1 = fbm(dir * 3.0 + uTime * 0.02);
float n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0);
float n3 = fbm(dir * 2.0 + uTime * 0.01 + 200.0);
vec3 col = uColor1;
col = mix(col, uColor2, smoothstep(0.3, 0.7, n1));
col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5);
float glow = pow(n1 * n2, 2.0) * 1.5;
col += vec3(0.15, 0.05, 0.25) * glow;
col += vec3(0.05, 0.15, 0.25) * pow(n3, 3.0);
float starField = hash(dir * 800.0);
float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0));
float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28);
col += vec3(stars * twinkle);
float bigStar = step(0.998, starField);
col += vec3(0.8, 0.9, 1.0) * bigStar * twinkle;
gl_FragColor = vec4(col, 1.0);
}
`,
side: THREE.BackSide,
});
const sky = new THREE.Mesh(skyGeo, skyMat);
sky.name = 'skybox';
scene.add(sky);
}
// ═══ LIGHTING ═══
function createLighting() {
const ambient = new THREE.AmbientLight(0x1a1a3a, 0.4);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6);
dirLight.position.set(10, 20, 10);
dirLight.castShadow = renderer.shadowMap.enabled;
const shadowRes = performanceTier === 'high' ? 2048 : performanceTier === 'medium' ? 1024 : 512;
dirLight.shadow.mapSize.set(shadowRes, shadowRes);
scene.add(dirLight);
const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5);
tealLight.position.set(0, 1, -5);
scene.add(tealLight);
const purpleLight = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5);
purpleLight.position.set(-8, 3, -8);
scene.add(purpleLight);
}
// ═══ FLOOR ═══
function createFloor() {
const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
const platMat = new THREE.MeshStandardMaterial({
color: 0x0a0f1a,
roughness: 0.8,
metalness: 0.3,
});
const platform = new THREE.Mesh(platGeo, platMat);
platform.position.y = -0.15;
platform.receiveShadow = true;
scene.add(platform);
const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine);
gridHelper.material.opacity = 0.15;
gridHelper.material.transparent = true;
gridHelper.position.y = 0.02;
scene.add(gridHelper);
const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6);
const ringMat = new THREE.MeshBasicMaterial({
color: NEXUS.colors.primary,
transparent: true,
opacity: 0.4,
side: THREE.DoubleSide,
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = Math.PI / 2;
ring.position.y = 0.05;
scene.add(ring);
}
// ═══ BATCAVE TERMINAL ═══
function createBatcaveTerminal() {
const terminalGroup = new THREE.Group();
terminalGroup.position.set(0, 0, -8);
const panelData = [
{ title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3, lines: ['> STATUS: NOMINAL', '> UPTIME: 142.4h', '> HARNESS: STABLE', '> MODE: SOVEREIGN'] },
{ title: 'DEV QUEUE', color: NEXUS.colors.gold, rot: -0.2, x: -3, y: 3, lines: ['> ISSUE #4: CORE', '> ISSUE #5: PORTAL', '> ISSUE #6: TERMINAL', '> ISSUE #7: TIMMY'] },
{ title: 'METRICS', color: NEXUS.colors.secondary, rot: 0, x: 0, y: 3, lines: ['> CPU: 12% [||....]', '> MEM: 4.2GB', '> COMMITS: 842', '> ACTIVE LOOPS: 5'] },
{ title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0.2, x: 3, y: 3, lines: ['> ANALYZING WORLD...', '> SYNCING MEMORY...', '> WAITING FOR INPUT', '> SOUL ON BITCOIN'] },
{ title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3, lines: ['> TIMMY: ● RUNNING', '> KIMI: ○ STANDBY', '> CLAUDE: ● ACTIVE', '> PERPLEXITY: ○'] },
];
panelData.forEach(data => {
createTerminalPanel(terminalGroup, data.x, data.y, data.rot, data.title, data.color, data.lines);
});
scene.add(terminalGroup);
}
function createTerminalPanel(parent, x, y, rot, title, color, lines) {
const w = 2.8, h = 3.5;
const group = new THREE.Group();
group.position.set(x, y, 0);
group.rotation.y = rot;
const bgGeo = new THREE.PlaneGeometry(w, h);
const bgMat = new THREE.MeshPhysicalMaterial({
color: NEXUS.colors.panelBg,
transparent: true,
opacity: 0.6,
roughness: 0.1,
metalness: 0.5,
side: THREE.DoubleSide,
});
const bg = new THREE.Mesh(bgGeo, bgMat);
group.add(bg);
const borderMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
const border = new THREE.Mesh(new THREE.PlaneGeometry(w + 0.05, h + 0.05), borderMat);
border.position.z = -0.01;
group.add(border);
const textCanvas = document.createElement('canvas');
textCanvas.width = 512;
textCanvas.height = 640;
const ctx = textCanvas.getContext('2d');
ctx.fillStyle = '#' + new THREE.Color(color).getHexString();
ctx.font = 'bold 32px "Orbitron", sans-serif';
ctx.fillText(title, 20, 45);
ctx.fillRect(20, 55, 472, 2);
ctx.font = '20px "JetBrains Mono", monospace';
ctx.fillStyle = '#a0b8d0';
lines.forEach((line, i) => {
let fillColor = '#a0b8d0';
if (line.includes('● RUNNING') || line.includes('● ACTIVE')) fillColor = '#4af0c0';
else if (line.includes('○ STANDBY')) fillColor = '#5a6a8a';
else if (line.includes('NOMINAL')) fillColor = '#4af0c0';
ctx.fillStyle = fillColor;
ctx.fillText(line, 20, 100 + i * 40);
});
const textTexture = new THREE.CanvasTexture(textCanvas);
textTexture.minFilter = THREE.LinearFilter;
const textMat = new THREE.MeshBasicMaterial({
map: textTexture,
transparent: true,
side: THREE.DoubleSide,
depthWrite: false,
});
const textMesh = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.95, h * 0.95), textMat);
textMesh.position.z = 0.01;
group.add(textMesh);
const scanGeo = new THREE.PlaneGeometry(w, h);
const scanMat = new THREE.ShaderMaterial({
transparent: true,
depthWrite: false,
uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(color) } },
vertexShader: `
varying vec2 vUv;
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float scanline = sin(vUv.y * 200.0 + uTime * 2.0) * 0.5 + 0.5;
scanline = pow(scanline, 8.0);
float sweep = smoothstep(0.0, 0.02, abs(fract(vUv.y - uTime * 0.1) - 0.5));
sweep = 1.0 - (1.0 - sweep) * 0.3;
float alpha = scanline * 0.04 + (1.0 - sweep) * 0.08;
gl_FragColor = vec4(uColor, alpha);
}
`,
side: THREE.DoubleSide,
});
const scanMesh = new THREE.Mesh(scanGeo, scanMat);
scanMesh.position.z = 0.02;
group.add(scanMesh);
parent.add(group);
batcaveTerminals.push({ group, scanMat, borderMat });
}
// ═══ VISION SYSTEM ═══
function createVisionPoints(data) {
data.forEach(config => {
const vp = createVisionPoint(config);
visionPoints.push(vp);
});
}
function createVisionPoint(config) {
const group = new THREE.Group();
group.position.set(config.position.x, config.position.y, config.position.z);
const color = new THREE.Color(config.color);
// Floating Crystal
const crystalGeo = new THREE.OctahedronGeometry(0.6, 0);
const crystalMat = new THREE.MeshPhysicalMaterial({
color: color,
emissive: color,
emissiveIntensity: 1,
roughness: 0,
metalness: 1,
transmission: 0.5,
thickness: 1,
});
const crystal = new THREE.Mesh(crystalGeo, crystalMat);
crystal.position.y = 2.5;
group.add(crystal);
// Glow Ring
const ringGeo = new THREE.TorusGeometry(0.8, 0.02, 16, 64);
const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 });
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.position.y = 2.5;
ring.rotation.x = Math.PI / 2;
group.add(ring);
// Light
const light = new THREE.PointLight(color, 1, 10);
light.position.set(0, 2.5, 0);
group.add(light);
scene.add(group);
return { config, group, crystal, ring, light };
}
// ═══ PORTAL SYSTEM ═══
function createPortals(data) {
data.forEach(config => {
const portal = createPortal(config);
portals.push(portal);
});
}
function createPortal(config) {
const group = new THREE.Group();
group.position.set(config.position.x, config.position.y, config.position.z);
if (config.rotation) {
group.rotation.y = config.rotation.y;
}
const portalColor = new THREE.Color(config.color);
// Torus Ring
const torusGeo = new THREE.TorusGeometry(3, 0.15, 16, 64);
const torusMat = new THREE.MeshStandardMaterial({
color: portalColor,
emissive: portalColor,
emissiveIntensity: 1.5,
roughness: 0.2,
metalness: 0.8,
});
const ring = new THREE.Mesh(torusGeo, torusMat);
ring.position.y = 3.5;
ring.name = `portal_ring_${config.id}`;
group.add(ring);
// Swirl Disc
const swirlGeo = new THREE.CircleGeometry(2.8, 64);
const swirlMat = new THREE.ShaderMaterial({
transparent: true,
side: THREE.DoubleSide,
uniforms: {
uTime: { value: 0 },
uColor: { value: portalColor },
},
vertexShader: `
varying vec2 vUv;
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
vec2 c = vUv - 0.5;
float r = length(c);
float a = atan(c.y, c.x);
float swirl = sin(a * 3.0 + r * 10.0 - uTime * 3.0) * 0.5 + 0.5;
float swirl2 = sin(a * 5.0 - r * 8.0 + uTime * 2.0) * 0.5 + 0.5;
float mask = smoothstep(0.5, 0.1, r);
vec3 col = mix(uColor, vec3(1.0, 1.0, 1.0), swirl * 0.3);
col = mix(col, vec3(1.0, 1.0, 1.0), swirl2 * 0.2);
float alpha = mask * (0.5 + 0.3 * swirl);
gl_FragColor = vec4(col, alpha);
}
`,
});
const swirl = new THREE.Mesh(swirlGeo, swirlMat);
swirl.position.y = 3.5;
group.add(swirl);
// Orbital Particles
const pCount = 120;
const pGeo = new THREE.BufferGeometry();
const pPos = new Float32Array(pCount * 3);
const pSizes = new Float32Array(pCount);
for (let i = 0; i < pCount; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 3.2 + Math.random() * 0.5;
pPos[i * 3] = Math.cos(angle) * r;
pPos[i * 3 + 1] = 3.5 + (Math.random() - 0.5) * 6;
pPos[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
pSizes[i] = 0.05 + Math.random() * 0.1;
}
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
pGeo.setAttribute('size', new THREE.BufferAttribute(pSizes, 1));
const pMat = new THREE.PointsMaterial({
color: portalColor,
size: 0.08,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const pSystem = new THREE.Points(pGeo, pMat);
group.add(pSystem);
// Pulsing Point Light
const light = new THREE.PointLight(portalColor, 2, 15, 1.5);
light.position.set(0, 3.5, 1);
group.add(light);
// Label
const labelCanvas = document.createElement('canvas');
labelCanvas.width = 512;
labelCanvas.height = 64;
const lctx = labelCanvas.getContext('2d');
lctx.font = 'bold 32px "Orbitron", sans-serif';
lctx.fillStyle = '#' + portalColor.getHexString();
lctx.textAlign = 'center';
lctx.fillText(`${config.name.toUpperCase()}`, 256, 42);
const labelTex = new THREE.CanvasTexture(labelCanvas);
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide });
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.5), labelMat);
labelMesh.position.y = 7.5;
group.add(labelMesh);
// Base Pillars
for (let side of [-1, 1]) {
const pillarGeo = new THREE.CylinderGeometry(0.2, 0.3, 7, 8);
const pillarMat = new THREE.MeshStandardMaterial({
color: 0x1a1a2e,
roughness: 0.5,
metalness: 0.7,
emissive: portalColor,
emissiveIntensity: 0.1,
});
const pillar = new THREE.Mesh(pillarGeo, pillarMat);
pillar.position.set(side * 3, 3.5, 0);
pillar.castShadow = true;
group.add(pillar);
}
scene.add(group);
return {
config,
group,
ring,
swirl,
pSystem,
light
};
}
// ═══ PARTICLES ═══
function createParticles() {
const count = particleCount(1500);
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const sizes = new Float32Array(count);
const c1 = new THREE.Color(NEXUS.colors.primary);
const c2 = new THREE.Color(NEXUS.colors.secondary);
const c3 = new THREE.Color(NEXUS.colors.gold);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 60;
positions[i * 3 + 1] = Math.random() * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 60;
const t = Math.random();
const col = t < 0.5 ? c1.clone().lerp(c2, t * 2) : c2.clone().lerp(c3, (t - 0.5) * 2);
colors[i * 3] = col.r;
colors[i * 3 + 1] = col.g;
colors[i * 3 + 2] = col.b;
sizes[i] = 0.02 + Math.random() * 0.06;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 } },
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
uniform float uTime;
void main() {
vColor = color;
vec3 pos = position;
pos.y += sin(uTime * 0.5 + position.x * 0.5) * 0.3;
pos.x += sin(uTime * 0.3 + position.z * 0.4) * 0.2;
vec4 mv = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = size * 300.0 / -mv.z;
gl_Position = projectionMatrix * mv;
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
float d = length(gl_PointCoord - 0.5);
if (d > 0.5) discard;
float alpha = smoothstep(0.5, 0.1, d);
gl_FragColor = vec4(vColor, alpha * 0.7);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
particles = new THREE.Points(geo, mat);
scene.add(particles);
}
function createDustParticles() {
const count = particleCount(500);
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 40;
positions[i * 3 + 1] = Math.random() * 15;
positions[i * 3 + 2] = (Math.random() - 0.5) * 40;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0x8899bb,
size: 0.03,
transparent: true,
opacity: 0.3,
depthWrite: false,
});
dustParticles = new THREE.Points(geo, mat);
scene.add(dustParticles);
}
// ═══ AMBIENT STRUCTURES ═══
function createAmbientStructures() {
const crystalMat = new THREE.MeshPhysicalMaterial({
color: 0x3355aa,
roughness: 0.1,
metalness: 0.2,
transmission: 0.6,
thickness: 2,
emissive: 0x1122aa,
emissiveIntensity: 0.3,
});
const positions = [
{ x: -18, z: -15, s: 1.5, ry: 0.3 },
{ x: -20, z: -10, s: 1, ry: 0.8 },
{ x: -15, z: -18, s: 2, ry: 1.2 },
{ x: 18, z: -15, s: 1.8, ry: 2.1 },
{ x: 20, z: -12, s: 1.2, ry: 0.5 },
{ x: -12, z: 18, s: 1.3, ry: 1.8 },
{ x: 14, z: 16, s: 1.6, ry: 0.9 },
];
positions.forEach(p => {
const geo = new THREE.ConeGeometry(0.4 * p.s, 2.5 * p.s, 5);
const crystal = new THREE.Mesh(geo, crystalMat.clone());
crystal.position.set(p.x, 1.25 * p.s, p.z);
crystal.rotation.y = p.ry;
crystal.rotation.z = (Math.random() - 0.5) * 0.3;
crystal.castShadow = true;
scene.add(crystal);
});
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2;
const r = 10;
const geo = new THREE.OctahedronGeometry(0.4, 0);
const mat = new THREE.MeshStandardMaterial({
color: NEXUS.colors.primary,
emissive: NEXUS.colors.primary,
emissiveIntensity: 0.5,
});
const stone = new THREE.Mesh(geo, mat);
stone.position.set(Math.cos(angle) * r, 5 + Math.sin(i * 1.3) * 1.5, Math.sin(angle) * r);
stone.name = 'runestone_' + i;
scene.add(stone);
}
const coreGeo = new THREE.IcosahedronGeometry(0.6, 2);
const coreMat = new THREE.MeshPhysicalMaterial({
color: 0x4af0c0,
emissive: 0x4af0c0,
emissiveIntensity: 2,
roughness: 0,
metalness: 1,
transmission: 0.3,
thickness: 1,
});
const core = new THREE.Mesh(coreGeo, coreMat);
core.position.set(0, 2.5, 0);
core.name = 'nexus-core';
scene.add(core);
const pedGeo = new THREE.CylinderGeometry(0.8, 1.2, 1.5, 8);
const pedMat = new THREE.MeshStandardMaterial({
color: 0x0a0f1a,
roughness: 0.4,
metalness: 0.8,
emissive: 0x1a2a4a,
emissiveIntensity: 0.3,
});
const pedestal = new THREE.Mesh(pedGeo, pedMat);
pedestal.position.set(0, 0.75, 0);
pedestal.castShadow = true;
scene.add(pedestal);
}
// ═══ NAVIGATION MODE ═══
function cycleNavMode() {
navModeIdx = (navModeIdx + 1) % NAV_MODES.length;
const mode = NAV_MODES[navModeIdx];
if (mode === 'orbit') {
const dir = new THREE.Vector3(0, 0, -1).applyEuler(playerRot);
orbitState.target.copy(playerPos).addScaledVector(dir, orbitState.radius);
orbitState.target.y = Math.max(0, orbitState.target.y);
const toCamera = new THREE.Vector3().subVectors(playerPos, orbitState.target);
orbitState.radius = toCamera.length();
orbitState.theta = Math.atan2(toCamera.x, toCamera.z);
orbitState.phi = Math.acos(Math.max(-1, Math.min(1, toCamera.y / orbitState.radius)));
}
if (mode === 'fly') flyY = playerPos.y;
updateNavModeUI(mode);
}
function updateNavModeUI(mode) {
const el = document.getElementById('nav-mode-label');
if (el) el.textContent = mode.toUpperCase();
}
// ═══ CONTROLS ═══
function setupControls() {
document.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
if (e.key === 'Enter') {
e.preventDefault();
const input = document.getElementById('chat-input');
if (document.activeElement === input) {
sendChatMessage();
} else {
input.focus();
}
}
if (e.key === 'Escape') {
document.getElementById('chat-input').blur();
if (portalOverlayActive) closePortalOverlay();
if (visionOverlayActive) closeVisionOverlay();
}
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
cycleNavMode();
}
if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
activatePortal(activePortal);
}
if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) {
activateVisionPoint(activeVisionPoint);
}
});
document.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
});
const canvas = document.getElementById('nexus-canvas');
canvas.addEventListener('mousedown', (e) => {
if (e.target === canvas) {
mouseDown = true;
orbitState.lastX = e.clientX;
orbitState.lastY = e.clientY;
// Raycasting for portals
if (!portalOverlayActive) {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
if (intersects.length > 0) {
const clickedRing = intersects[0].object;
const portal = portals.find(p => p.ring === clickedRing);
if (portal) activatePortal(portal);
}
}
}
});
document.addEventListener('mouseup', () => { mouseDown = false; });
document.addEventListener('mousemove', (e) => {
if (!mouseDown) return;
if (document.activeElement === document.getElementById('chat-input')) return;
const mode = NAV_MODES[navModeIdx];
if (mode === 'orbit') {
const dx = e.clientX - orbitState.lastX;
const dy = e.clientY - orbitState.lastY;
orbitState.lastX = e.clientX;
orbitState.lastY = e.clientY;
orbitState.theta -= dx * 0.005;
orbitState.phi = Math.max(0.05, Math.min(Math.PI * 0.85, orbitState.phi + dy * 0.005));
} else {
playerRot.y -= e.movementX * 0.003;
playerRot.x -= e.movementY * 0.003;
playerRot.x = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, playerRot.x));
}
});
canvas.addEventListener('wheel', (e) => {
if (NAV_MODES[navModeIdx] === 'orbit') {
orbitState.radius = Math.max(orbitState.minR, Math.min(orbitState.maxR, orbitState.radius + e.deltaY * 0.02));
}
}, { passive: true });
document.getElementById('chat-toggle').addEventListener('click', () => {
chatOpen = !chatOpen;
document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen);
});
document.getElementById('chat-send').addEventListener('click', sendChatMessage);
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
}
function sendChatMessage() {
const input = document.getElementById('chat-input');
const text = input.value.trim();
if (!text) return;
addChatMessage('user', text);
input.value = '';
setTimeout(() => {
const responses = [
'Processing your request through the harness...',
'I have noted this in my thought stream.',
'Acknowledged. Routing to appropriate agent loop.',
'The sovereign space recognizes your command.',
'Running analysis. Results will appear on the main terminal.',
'My crystal ball says... yes. Implementing.',
'Understood, Alexander. Adjusting priorities.',
];
const resp = responses[Math.floor(Math.random() * responses.length)];
addChatMessage('timmy', resp);
}, 500 + Math.random() * 1000);
input.blur();
}
function addChatMessage(type, text) {
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = `chat-msg chat-msg-${type}`;
const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
div.innerHTML = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${text}`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
// ═══ PORTAL INTERACTION ═══
function checkPortalProximity() {
if (portalOverlayActive) return;
let closest = null;
let minDist = Infinity;
portals.forEach(portal => {
const dist = playerPos.distanceTo(portal.group.position);
if (dist < 4.5 && dist < minDist) {
minDist = dist;
closest = portal;
}
});
activePortal = closest;
const hint = document.getElementById('portal-hint');
if (activePortal) {
document.getElementById('portal-hint-name').textContent = activePortal.config.name;
hint.style.display = 'flex';
} else {
hint.style.display = 'none';
}
}
function activatePortal(portal) {
portalOverlayActive = true;
const overlay = document.getElementById('portal-overlay');
const nameDisplay = document.getElementById('portal-name-display');
const descDisplay = document.getElementById('portal-desc-display');
const redirectBox = document.getElementById('portal-redirect-box');
const errorBox = document.getElementById('portal-error-box');
const timerDisplay = document.getElementById('portal-timer');
const statusDot = document.getElementById('portal-status-dot');
nameDisplay.textContent = portal.config.name.toUpperCase();
descDisplay.textContent = portal.config.description;
statusDot.style.background = portal.config.color;
statusDot.style.boxShadow = `0 0 10px ${portal.config.color}`;
overlay.style.display = 'flex';
if (portal.config.destination && portal.config.destination.url) {
redirectBox.style.display = 'block';
errorBox.style.display = 'none';
let count = 5;
timerDisplay.textContent = count;
const interval = setInterval(() => {
count--;
timerDisplay.textContent = count;
if (count <= 0) {
clearInterval(interval);
if (portalOverlayActive) window.location.href = portal.config.destination.url;
}
if (!portalOverlayActive) clearInterval(interval);
}, 1000);
} else {
redirectBox.style.display = 'none';
errorBox.style.display = 'block';
}
}
function closePortalOverlay() {
portalOverlayActive = false;
document.getElementById('portal-overlay').style.display = 'none';
}
// ═══ VISION INTERACTION ═══
function checkVisionProximity() {
if (visionOverlayActive) return;
let closest = null;
let minDist = Infinity;
visionPoints.forEach(vp => {
const dist = playerPos.distanceTo(vp.group.position);
if (dist < 3.5 && dist < minDist) {
minDist = dist;
closest = vp;
}
});
activeVisionPoint = closest;
const hint = document.getElementById('vision-hint');
if (activeVisionPoint) {
document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title;
hint.style.display = 'flex';
} else {
hint.style.display = 'none';
}
}
function activateVisionPoint(vp) {
visionOverlayActive = true;
const overlay = document.getElementById('vision-overlay');
const titleDisplay = document.getElementById('vision-title-display');
const contentDisplay = document.getElementById('vision-content-display');
const statusDot = document.getElementById('vision-status-dot');
titleDisplay.textContent = vp.config.title.toUpperCase();
contentDisplay.textContent = vp.config.content;
statusDot.style.background = vp.config.color;
statusDot.style.boxShadow = `0 0 10px ${vp.config.color}`;
overlay.style.display = 'flex';
}
function closeVisionOverlay() {
visionOverlayActive = false;
document.getElementById('vision-overlay').style.display = 'none';
}
// ═══ GAME LOOP ═══
function gameLoop() {
requestAnimationFrame(gameLoop);
const delta = Math.min(clock.getDelta(), 0.1);
const elapsed = clock.elapsedTime;
const mode = NAV_MODES[navModeIdx];
const chatActive = document.activeElement === document.getElementById('chat-input');
if (mode === 'walk') {
if (!chatActive && !portalOverlayActive) {
const speed = 6 * delta;
const dir = new THREE.Vector3();
if (keys['w']) dir.z -= 1;
if (keys['s']) dir.z += 1;
if (keys['a']) dir.x -= 1;
if (keys['d']) dir.x += 1;
if (dir.length() > 0) {
dir.normalize().multiplyScalar(speed);
dir.applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y);
playerPos.add(dir);
const maxR = 24;
const dist = Math.sqrt(playerPos.x * playerPos.x + playerPos.z * playerPos.z);
if (dist > maxR) { playerPos.x *= maxR / dist; playerPos.z *= maxR / dist; }
}
}
playerPos.y = 2;
camera.position.copy(playerPos);
camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
} else if (mode === 'orbit') {
if (!chatActive && !portalOverlayActive) {
const speed = 8 * delta;
const pan = new THREE.Vector3();
if (keys['w']) pan.z -= 1;
if (keys['s']) pan.z += 1;
if (keys['a']) pan.x -= 1;
if (keys['d']) pan.x += 1;
if (pan.length() > 0) {
pan.normalize().multiplyScalar(speed);
pan.applyAxisAngle(new THREE.Vector3(0, 1, 0), orbitState.theta);
orbitState.target.add(pan);
orbitState.target.y = Math.max(0, Math.min(20, orbitState.target.y));
}
}
const r = orbitState.radius;
camera.position.set(
orbitState.target.x + r * Math.sin(orbitState.phi) * Math.sin(orbitState.theta),
orbitState.target.y + r * Math.cos(orbitState.phi),
orbitState.target.z + r * Math.sin(orbitState.phi) * Math.cos(orbitState.theta)
);
camera.lookAt(orbitState.target);
playerPos.copy(camera.position);
playerRot.y = orbitState.theta;
} else if (mode === 'fly') {
if (!chatActive && !portalOverlayActive) {
const speed = 8 * delta;
const forward = new THREE.Vector3(-Math.sin(playerRot.y), 0, -Math.cos(playerRot.y));
const right = new THREE.Vector3( Math.cos(playerRot.y), 0, -Math.sin(playerRot.y));
if (keys['w']) playerPos.addScaledVector(forward, speed);
if (keys['s']) playerPos.addScaledVector(forward, -speed);
if (keys['a']) playerPos.addScaledVector(right, -speed);
if (keys['d']) playerPos.addScaledVector(right, speed);
if (keys['q'] || keys[' ']) flyY += speed;
if (keys['e'] || keys['shift']) flyY -= speed;
flyY = Math.max(0.5, Math.min(30, flyY));
playerPos.y = flyY;
}
camera.position.copy(playerPos);
camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
}
// Proximity check
checkPortalProximity();
checkVisionProximity();
const sky = scene.getObjectByName('skybox');
if (sky) sky.material.uniforms.uTime.value = elapsed;
batcaveTerminals.forEach(t => {
if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed;
});
// Animate Portals
portals.forEach(portal => {
portal.ring.rotation.z = elapsed * 0.3;
portal.ring.rotation.x = Math.sin(elapsed * 0.5) * 0.1;
if (portal.swirl.material.uniforms) {
portal.swirl.material.uniforms.uTime.value = elapsed;
}
// Pulse light
portal.light.intensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
// Animate particles
const positions = portal.pSystem.geometry.attributes.position.array;
for (let i = 0; i < positions.length / 3; i++) {
positions[i * 3 + 1] += Math.sin(elapsed + i) * 0.002;
}
portal.pSystem.geometry.attributes.position.needsUpdate = true;
});
// Animate Vision Points
visionPoints.forEach(vp => {
vp.crystal.rotation.y = elapsed * 0.8;
vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2;
vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2;
vp.ring.rotation.z = elapsed * 0.5;
vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05);
vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3;
});
if (particles?.material?.uniforms) {
particles.material.uniforms.uTime.value = elapsed;
}
if (dustParticles) {
dustParticles.rotation.y = elapsed * 0.01;
}
for (let i = 0; i < 5; i++) {
const stone = scene.getObjectByName('runestone_' + i);
if (stone) {
stone.position.y = 5 + Math.sin(elapsed * 0.8 + i * 1.3) * 0.8;
stone.rotation.y = elapsed * 0.5 + i;
stone.rotation.x = elapsed * 0.3 + i * 0.7;
}
}
const core = scene.getObjectByName('nexus-core');
if (core) {
core.position.y = 2.5 + Math.sin(elapsed * 1.2) * 0.3;
core.rotation.y = elapsed * 0.4;
core.rotation.x = elapsed * 0.2;
core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
}
composer.render();
frameCount++;
const now = performance.now();
if (now - lastFPSTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastFPSTime = now;
}
if (debugOverlay) {
const info = renderer.info;
debugOverlay.textContent =
`FPS: ${fps} Draw: ${info.render?.calls} Tri: ${info.render?.triangles} [${performanceTier}]\n` +
`Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`;
}
renderer.info.reset();
}
function onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
composer.setSize(w, h);
}
init();