2026-03-23 16:04:24 +00:00
|
|
|
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 — Timmy's Sovereign Home
|
|
|
|
|
// ═══════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
const NEXUS = {
|
|
|
|
|
colors: {
|
|
|
|
|
primary: 0x4af0c0,
|
|
|
|
|
secondary: 0x7b5cff,
|
|
|
|
|
bg: 0x050510,
|
|
|
|
|
panelBg: 0x0a0f28,
|
|
|
|
|
nebula1: 0x1a0a3e,
|
|
|
|
|
nebula2: 0x0a1a3e,
|
|
|
|
|
gold: 0xffd700,
|
|
|
|
|
danger: 0xff4466,
|
|
|
|
|
gridLine: 0x1a2a4a,
|
2026-03-24 02:21:36 +00:00
|
|
|
memory: 0x00ffff,
|
2026-03-23 16:04:24 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-24 02:17:19 +00:00
|
|
|
// ═══ SOVEREIGN STATE (The Heartbeat) ═══
|
|
|
|
|
const STATE = {
|
|
|
|
|
metrics: {
|
|
|
|
|
fps: 0,
|
|
|
|
|
drawCalls: 0,
|
|
|
|
|
triangles: 0,
|
|
|
|
|
uptime: 0,
|
|
|
|
|
activeLoops: 5,
|
|
|
|
|
cpu: 12,
|
|
|
|
|
mem: 4.2
|
|
|
|
|
},
|
|
|
|
|
agents: {
|
|
|
|
|
timmy: 'RUNNING',
|
|
|
|
|
kimi: 'STANDBY',
|
|
|
|
|
claude: 'ACTIVE',
|
|
|
|
|
perplexity: 'STANDBY'
|
|
|
|
|
},
|
|
|
|
|
thoughts: [
|
|
|
|
|
'ANALYZING WORLD...',
|
|
|
|
|
'SYNCING MEMORY...',
|
|
|
|
|
'WAITING FOR INPUT',
|
|
|
|
|
'SOUL ON BITCOIN'
|
|
|
|
|
],
|
2026-03-24 02:21:36 +00:00
|
|
|
selectedMemory: null,
|
2026-03-24 02:17:19 +00:00
|
|
|
lastUpdate: 0,
|
|
|
|
|
pulseRate: 1.0 // Hz
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
// ═══ MEMORY STORE (The Vault) ═══
|
|
|
|
|
const MEMORY_VAULT = [
|
|
|
|
|
{ id: 1, title: 'ORIGIN', date: '2026-03-14', summary: 'Timmy initialized in the Nexus.', tags: ['core', 'origin'] },
|
|
|
|
|
{ id: 2, title: 'HERMES LINK', date: '2026-03-18', summary: 'Established stable bridge to Bannerlord.', tags: ['harness', 'bridge'] },
|
|
|
|
|
{ id: 3, title: 'SOVEREIGNTY', date: '2026-03-22', summary: 'First autonomous task assignment successful.', tags: ['agentic', 'freedom'] },
|
|
|
|
|
{ id: 4, title: 'NEXUS CORE', date: '2026-03-23', summary: 'Three.js foundation implemented.', tags: ['visual', 'home'] },
|
|
|
|
|
{ id: 5, title: 'HEARTBEAT', date: '2026-03-24', summary: 'Real-time state broadcasting active.', tags: ['infrastructure', 'live'] },
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-24 02:17:19 +00:00
|
|
|
// ═══ STATE BROADCASTER ═══
|
|
|
|
|
const Broadcaster = {
|
|
|
|
|
listeners: [],
|
|
|
|
|
subscribe(fn) { this.listeners.push(fn); },
|
|
|
|
|
broadcast() { this.listeners.forEach(fn => fn(STATE)); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ═══ STATE UPDATER ═══
|
|
|
|
|
function updateSovereignState(elapsed) {
|
|
|
|
|
STATE.metrics.uptime = elapsed;
|
|
|
|
|
if (Math.random() > 0.95) {
|
|
|
|
|
STATE.metrics.cpu = 10 + Math.floor(Math.random() * 15);
|
|
|
|
|
STATE.metrics.activeLoops = 4 + Math.floor(Math.random() * 3);
|
|
|
|
|
if (Math.random() > 0.7) {
|
2026-03-24 02:21:36 +00:00
|
|
|
const newThoughts = ['DECENTRALIZING COGNITION', 'ZAPPING CONTRIBUTORS', 'MAPPING SPATIAL LOOPS', 'REFINING LORA WEIGHTS', 'OBSERVING ALEXANDER', 'NEXUS INTEGRITY: 100%', 'HERMES LINK STABLE'];
|
2026-03-24 02:17:19 +00:00
|
|
|
STATE.thoughts.shift();
|
|
|
|
|
STATE.thoughts.push(newThoughts[Math.floor(Math.random() * newThoughts.length)]);
|
|
|
|
|
}
|
|
|
|
|
Broadcaster.broadcast();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
// ═══ GLOBAL REFS ═══
|
2026-03-23 16:04:24 +00:00
|
|
|
let camera, scene, renderer, composer;
|
|
|
|
|
let clock, playerPos, playerRot;
|
|
|
|
|
let keys = {};
|
|
|
|
|
let mouseDown = false;
|
|
|
|
|
let batcaveTerminals = [];
|
2026-03-24 02:21:36 +00:00
|
|
|
let memoryCrystals = [];
|
2026-03-23 16:04:24 +00:00
|
|
|
let portalMesh, portalGlow;
|
|
|
|
|
let particles, dustParticles;
|
|
|
|
|
let debugOverlay;
|
|
|
|
|
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
2026-03-24 02:21:36 +00:00
|
|
|
let performanceTier = 'high';
|
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
|
|
|
const mouse = new THREE.Vector2();
|
2026-03-24 01:30:25 +00:00
|
|
|
|
|
|
|
|
// ═══ NAVIGATION SYSTEM ═══
|
|
|
|
|
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
2026-03-24 02:21:36 +00:00
|
|
|
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 };
|
2026-03-24 01:30:25 +00:00
|
|
|
let flyY = 2;
|
2026-03-23 16:04:24 +00:00
|
|
|
|
|
|
|
|
// ═══ INIT ═══
|
|
|
|
|
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;
|
2026-03-24 01:30:25 +00:00
|
|
|
|
|
|
|
|
performanceTier = detectPerformanceTier();
|
2026-03-23 16:04:24 +00:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
createSkybox();
|
|
|
|
|
createLighting();
|
|
|
|
|
createFloor();
|
|
|
|
|
createBatcaveTerminal();
|
|
|
|
|
createPortal();
|
|
|
|
|
createParticles();
|
|
|
|
|
createDustParticles();
|
|
|
|
|
createAmbientStructures();
|
2026-03-24 02:21:36 +00:00
|
|
|
createMemoryVault();
|
2026-03-23 16:04:24 +00:00
|
|
|
|
|
|
|
|
composer = new EffectComposer(renderer);
|
|
|
|
|
composer.addPass(new RenderPass(scene, camera));
|
2026-03-24 02:21:36 +00:00
|
|
|
const bloom = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.6, 0.4, 0.85);
|
2026-03-23 16:04:24 +00:00
|
|
|
composer.addPass(bloom);
|
|
|
|
|
composer.addPass(new SMAAPass(window.innerWidth, window.innerHeight));
|
|
|
|
|
|
|
|
|
|
setupControls();
|
|
|
|
|
window.addEventListener('resize', onResize);
|
|
|
|
|
debugOverlay = document.getElementById('debug-overlay');
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
// Fade out loading
|
2026-03-23 16:04:24 +00:00
|
|
|
setTimeout(() => {
|
2026-03-24 02:21:36 +00:00
|
|
|
document.getElementById('loading-screen')?.classList.add('fade-out');
|
2026-03-23 16:04:24 +00:00
|
|
|
const enterPrompt = document.getElementById('enter-prompt');
|
2026-03-24 02:21:36 +00:00
|
|
|
if (enterPrompt) {
|
|
|
|
|
enterPrompt.style.display = 'flex';
|
|
|
|
|
enterPrompt.addEventListener('click', () => {
|
|
|
|
|
enterPrompt.classList.add('fade-out');
|
|
|
|
|
document.getElementById('hud').style.display = 'block';
|
|
|
|
|
setTimeout(() => { enterPrompt.remove(); }, 600);
|
|
|
|
|
}, { once: true });
|
|
|
|
|
}
|
2026-03-23 16:04:24 +00:00
|
|
|
}, 600);
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 01:30:25 +00:00
|
|
|
function detectPerformanceTier() {
|
|
|
|
|
const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
|
2026-03-24 02:21:36 +00:00
|
|
|
if (isMobile) { renderer.setPixelRatio(1); renderer.shadowMap.enabled = false; return 'low'; }
|
|
|
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
|
|
|
return 'high';
|
2026-03-24 01:30:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function particleCount(base) {
|
2026-03-24 02:21:36 +00:00
|
|
|
if (performanceTier === 'low') return Math.floor(base * 0.25);
|
2026-03-24 01:30:25 +00:00
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 16:04:24 +00:00
|
|
|
// ═══ SKYBOX ═══
|
|
|
|
|
function createSkybox() {
|
2026-03-24 02:21:36 +00:00
|
|
|
const skyGeo = new THREE.SphereGeometry(400, 32, 32);
|
2026-03-23 16:04:24 +00:00
|
|
|
const skyMat = new THREE.ShaderMaterial({
|
2026-03-24 02:21:36 +00:00
|
|
|
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, 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 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, 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), n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0); vec3 col = mix(uColor1, uColor2, smoothstep(0.3, 0.7, n1)); col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5); 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); gl_FragColor = vec4(col, 1.0); }`,
|
2026-03-23 16:04:24 +00:00
|
|
|
side: THREE.BackSide,
|
|
|
|
|
});
|
|
|
|
|
const sky = new THREE.Mesh(skyGeo, skyMat);
|
|
|
|
|
sky.name = 'skybox';
|
|
|
|
|
scene.add(sky);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══ LIGHTING ═══
|
|
|
|
|
function createLighting() {
|
2026-03-24 02:21:36 +00:00
|
|
|
scene.add(new THREE.AmbientLight(0x1a1a3a, 0.4));
|
2026-03-23 16:04:24 +00:00
|
|
|
const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6);
|
|
|
|
|
dirLight.position.set(10, 20, 10);
|
2026-03-24 01:30:25 +00:00
|
|
|
dirLight.castShadow = renderer.shadowMap.enabled;
|
2026-03-23 16:04:24 +00:00
|
|
|
scene.add(dirLight);
|
|
|
|
|
const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5);
|
|
|
|
|
tealLight.position.set(0, 1, -5);
|
|
|
|
|
scene.add(tealLight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══ FLOOR ═══
|
|
|
|
|
function createFloor() {
|
|
|
|
|
const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
|
2026-03-24 02:21:36 +00:00
|
|
|
const platMat = new THREE.MeshStandardMaterial({ color: 0x0a0f1a, roughness: 0.8, metalness: 0.3 });
|
2026-03-23 16:04:24 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══ BATCAVE TERMINAL ═══
|
|
|
|
|
function createBatcaveTerminal() {
|
2026-03-24 02:01:00 +00:00
|
|
|
const terminalGroup = new THREE.Group();
|
|
|
|
|
terminalGroup.position.set(0, 0, -8);
|
2026-03-24 02:17:19 +00:00
|
|
|
const panels = [
|
2026-03-24 02:21:36 +00:00
|
|
|
{ id: 'command', title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3 },
|
|
|
|
|
{ id: 'metrics', title: 'METRICS', color: NEXUS.colors.secondary, rot: -0.2, x: -3, y: 3 },
|
|
|
|
|
{ id: 'thoughts', title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0, x: 0, y: 3 },
|
|
|
|
|
{ id: 'vault', title: 'MEMORY VAULT', color: NEXUS.colors.memory, rot: 0.2, x: 3, y: 3 },
|
|
|
|
|
{ id: 'agents', title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3 },
|
2026-03-24 02:01:00 +00:00
|
|
|
];
|
2026-03-24 02:21:36 +00:00
|
|
|
panels.forEach(data => createTerminalPanel(terminalGroup, data));
|
2026-03-24 02:01:00 +00:00
|
|
|
scene.add(terminalGroup);
|
2026-03-23 16:04:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:17:19 +00:00
|
|
|
function createTerminalPanel(parent, data) {
|
|
|
|
|
const { x, y, rot, title, color, id } = data;
|
2026-03-24 02:01:00 +00:00
|
|
|
const w = 2.8, h = 3.5;
|
2026-03-23 16:04:24 +00:00
|
|
|
const group = new THREE.Group();
|
2026-03-24 02:01:00 +00:00
|
|
|
group.position.set(x, y, 0);
|
|
|
|
|
group.rotation.y = rot;
|
2026-03-24 02:21:36 +00:00
|
|
|
const bgMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.panelBg, transparent: true, opacity: 0.6, roughness: 0.1, metalness: 0.5, side: THREE.DoubleSide });
|
|
|
|
|
group.add(new THREE.Mesh(new THREE.PlaneGeometry(w, h), bgMat));
|
2026-03-23 16:04:24 +00:00
|
|
|
const textCanvas = document.createElement('canvas');
|
2026-03-24 02:21:36 +00:00
|
|
|
textCanvas.width = 512; textCanvas.height = 640;
|
2026-03-23 16:04:24 +00:00
|
|
|
const ctx = textCanvas.getContext('2d');
|
|
|
|
|
const textTexture = new THREE.CanvasTexture(textCanvas);
|
2026-03-24 02:21:36 +00:00
|
|
|
const textMat = new THREE.MeshBasicMaterial({ map: textTexture, transparent: true, side: THREE.DoubleSide, depthWrite: false });
|
2026-03-23 16:04:24 +00:00
|
|
|
const textMesh = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.95, h * 0.95), textMat);
|
|
|
|
|
textMesh.position.z = 0.01;
|
|
|
|
|
group.add(textMesh);
|
|
|
|
|
|
2026-03-24 02:17:19 +00:00
|
|
|
const updatePanel = (state) => {
|
|
|
|
|
ctx.clearRect(0, 0, 512, 640);
|
|
|
|
|
ctx.fillStyle = '#' + new THREE.Color(color).getHexString();
|
|
|
|
|
ctx.font = 'bold 32px "Orbitron", sans-serif';
|
|
|
|
|
ctx.fillText(title, 20, 45);
|
|
|
|
|
ctx.fillRect(20, 55, 472, 2);
|
|
|
|
|
ctx.font = '20px "JetBrains Mono", monospace';
|
|
|
|
|
ctx.fillStyle = '#a0b8d0';
|
|
|
|
|
let lines = [];
|
2026-03-24 02:21:36 +00:00
|
|
|
if (id === 'command') lines = [`> STATUS: NOMINAL`, `> UPTIME: ${state.metrics.uptime.toFixed(1)}s`, `> MODE: SOVEREIGN` ];
|
|
|
|
|
else if (id === 'metrics') lines = [`> CPU: ${state.metrics.cpu}%`, `> MEM: ${state.metrics.mem}GB`, `> FPS: ${state.metrics.fps}`];
|
|
|
|
|
else if (id === 'thoughts') lines = state.thoughts.map(t => `> ${t}`);
|
|
|
|
|
else if (id === 'agents') lines = Object.entries(state.agents).map(([name, status]) => `> ${name.toUpperCase()}: ${status}`);
|
|
|
|
|
else if (id === 'vault') {
|
|
|
|
|
const mem = state.selectedMemory || MEMORY_VAULT[0];
|
|
|
|
|
lines = [`> ID: ${mem.id}`, `> TITLE: ${mem.title}`, `> DATE: ${mem.date}`, `> TAGS: ${mem.tags.join(', ')}`, `> SUMMARY:`, mem.summary];
|
2026-03-24 02:17:19 +00:00
|
|
|
}
|
|
|
|
|
lines.forEach((line, i) => {
|
2026-03-24 02:21:36 +00:00
|
|
|
ctx.fillStyle = (line.includes('RUNNING') || line.includes('ACTIVE')) ? '#4af0c0' : '#a0b8d0';
|
2026-03-24 02:17:19 +00:00
|
|
|
ctx.fillText(line, 20, 100 + i * 40);
|
|
|
|
|
});
|
|
|
|
|
textTexture.needsUpdate = true;
|
|
|
|
|
};
|
|
|
|
|
updatePanel(STATE);
|
|
|
|
|
Broadcaster.subscribe(updatePanel);
|
2026-03-24 02:21:36 +00:00
|
|
|
parent.add(group);
|
|
|
|
|
batcaveTerminals.push({ group, id });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══ MEMORY VAULT ═══
|
|
|
|
|
function createMemoryVault() {
|
|
|
|
|
const vaultGroup = new THREE.Group();
|
|
|
|
|
vaultGroup.position.set(-15, 0, -10);
|
|
|
|
|
vaultGroup.rotation.y = 0.5;
|
|
|
|
|
|
|
|
|
|
const pedestalGeo = new THREE.CylinderGeometry(4, 4.5, 0.5, 6);
|
|
|
|
|
const pedestalMat = new THREE.MeshStandardMaterial({ color: 0x0a1a2e, roughness: 0.4, metalness: 0.8 });
|
|
|
|
|
const pedestal = new THREE.Mesh(pedestalGeo, pedestalMat);
|
|
|
|
|
pedestal.position.y = 0.25;
|
|
|
|
|
vaultGroup.add(pedestal);
|
2026-03-24 02:17:19 +00:00
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
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 = '#00ffff'; lctx.textAlign = 'center';
|
|
|
|
|
lctx.fillText('◈ MEMORY VAULT', 256, 42);
|
|
|
|
|
const labelTex = new THREE.CanvasTexture(labelCanvas);
|
|
|
|
|
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(5, 0.6), new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide }));
|
|
|
|
|
labelMesh.position.y = 5;
|
|
|
|
|
vaultGroup.add(labelMesh);
|
|
|
|
|
|
|
|
|
|
MEMORY_VAULT.forEach((mem, i) => {
|
|
|
|
|
const angle = (i / MEMORY_VAULT.length) * Math.PI * 2;
|
|
|
|
|
const r = 2.5;
|
|
|
|
|
const crystalGeo = new THREE.OctahedronGeometry(0.5, 0);
|
|
|
|
|
const crystalMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.memory, emissive: NEXUS.colors.memory, emissiveIntensity: 0.5, roughness: 0, metalness: 0.5, transmission: 0.8, thickness: 1 });
|
|
|
|
|
const crystal = new THREE.Mesh(crystalGeo, crystalMat);
|
|
|
|
|
crystal.position.set(Math.cos(angle) * r, 2, Math.sin(angle) * r);
|
|
|
|
|
crystal.userData = { memory: mem, originalPos: crystal.position.clone() };
|
|
|
|
|
crystal.name = 'memory_crystal';
|
|
|
|
|
vaultGroup.add(crystal);
|
|
|
|
|
memoryCrystals.push(crystal);
|
2026-03-23 16:04:24 +00:00
|
|
|
});
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
scene.add(vaultGroup);
|
2026-03-23 16:04:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══ PORTAL ═══
|
|
|
|
|
function createPortal() {
|
|
|
|
|
const portalGroup = new THREE.Group();
|
|
|
|
|
portalGroup.position.set(15, 0, -10);
|
|
|
|
|
portalGroup.rotation.y = -0.5;
|
2026-03-24 02:21:36 +00:00
|
|
|
portalMesh = new THREE.Mesh(new THREE.TorusGeometry(3, 0.15, 16, 64), new THREE.MeshStandardMaterial({ color: 0xff6600, emissive: 0xff4400, emissiveIntensity: 1.5 }));
|
2026-03-23 16:04:24 +00:00
|
|
|
portalMesh.position.y = 3.5;
|
|
|
|
|
portalGroup.add(portalMesh);
|
|
|
|
|
scene.add(portalGroup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══ PARTICLES ═══
|
|
|
|
|
function createParticles() {
|
2026-03-24 02:21:36 +00:00
|
|
|
const count = particleCount(1000);
|
2026-03-23 16:04:24 +00:00
|
|
|
const geo = new THREE.BufferGeometry();
|
2026-03-24 02:21:36 +00:00
|
|
|
const pos = new Float32Array(count * 3);
|
|
|
|
|
for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5)*60; pos[i*3+1] = Math.random()*20; pos[i*3+2] = (Math.random()-0.5)*60; }
|
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
|
|
|
particles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x4af0c0, size: 0.05, transparent: true, opacity: 0.4 }));
|
2026-03-23 16:04:24 +00:00
|
|
|
scene.add(particles);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createDustParticles() {
|
2026-03-24 02:21:36 +00:00
|
|
|
const count = particleCount(300);
|
2026-03-23 16:04:24 +00:00
|
|
|
const geo = new THREE.BufferGeometry();
|
2026-03-24 02:21:36 +00:00
|
|
|
const pos = new Float32Array(count * 3);
|
|
|
|
|
for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5)*40; pos[i*3+1] = Math.random()*15; pos[i*3+2] = (Math.random()-0.5)*40; }
|
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
|
|
|
dustParticles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x8899bb, size: 0.02, transparent: true, opacity: 0.2 }));
|
2026-03-23 16:04:24 +00:00
|
|
|
scene.add(dustParticles);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createAmbientStructures() {
|
2026-03-24 02:21:36 +00:00
|
|
|
const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.6, 2), new THREE.MeshPhysicalMaterial({ color: 0x4af0c0, emissive: 0x4af0c0, emissiveIntensity: 2 }));
|
|
|
|
|
core.position.set(0, 2.5, 0); core.name = 'nexus-core';
|
2026-03-23 16:04:24 +00:00
|
|
|
scene.add(core);
|
2026-03-24 01:30:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 16:04:24 +00:00
|
|
|
// ═══ CONTROLS ═══
|
|
|
|
|
function setupControls() {
|
2026-03-24 02:21:36 +00:00
|
|
|
document.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; if (e.key.toLowerCase() === 'v') cycleNavMode(); });
|
|
|
|
|
document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });
|
2026-03-23 16:04:24 +00:00
|
|
|
const canvas = document.getElementById('nexus-canvas');
|
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
2026-03-24 02:21:36 +00:00
|
|
|
mouseDown = true; orbitState.lastX = e.clientX; orbitState.lastY = e.clientY;
|
|
|
|
|
// Raycasting for memory crystals
|
|
|
|
|
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
|
|
|
|
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
|
|
const intersects = raycaster.intersectObjects(memoryCrystals);
|
|
|
|
|
if (intersects.length > 0) {
|
|
|
|
|
STATE.selectedMemory = intersects[0].object.userData.memory;
|
|
|
|
|
Broadcaster.broadcast();
|
2026-03-24 01:30:25 +00:00
|
|
|
}
|
2026-03-23 16:04:24 +00:00
|
|
|
});
|
|
|
|
|
document.addEventListener('mouseup', () => { mouseDown = false; });
|
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
|
|
|
if (!mouseDown) return;
|
2026-03-24 01:30:25 +00:00
|
|
|
if (NAV_MODES[navModeIdx] === 'orbit') {
|
2026-03-24 02:21:36 +00:00
|
|
|
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; }
|
2026-03-23 16:04:24 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
function cycleNavMode() { navModeIdx = (navModeIdx + 1) % NAV_MODES.length; document.getElementById('nav-mode-label').textContent = NAV_MODES[navModeIdx].toUpperCase(); }
|
2026-03-23 16:04:24 +00:00
|
|
|
|
|
|
|
|
// ═══ GAME LOOP ═══
|
|
|
|
|
function gameLoop() {
|
|
|
|
|
requestAnimationFrame(gameLoop);
|
2026-03-24 02:21:36 +00:00
|
|
|
const delta = Math.min(clock.getDelta(), 0.1), elapsed = clock.elapsedTime;
|
2026-03-24 02:17:19 +00:00
|
|
|
updateSovereignState(elapsed);
|
|
|
|
|
|
2026-03-24 01:30:25 +00:00
|
|
|
const mode = NAV_MODES[navModeIdx];
|
|
|
|
|
if (mode === 'walk') {
|
2026-03-24 02:21:36 +00:00
|
|
|
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) playerPos.add(dir.normalize().multiplyScalar(6 * delta).applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y));
|
|
|
|
|
playerPos.y = 2; camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
|
2026-03-24 01:30:25 +00:00
|
|
|
} else if (mode === 'orbit') {
|
2026-03-24 02:21:36 +00:00
|
|
|
camera.position.set(orbitState.target.x + orbitState.radius * Math.sin(orbitState.phi) * Math.sin(orbitState.theta), orbitState.target.y + orbitState.radius * Math.cos(orbitState.phi), orbitState.target.z + orbitState.radius * Math.sin(orbitState.phi) * Math.cos(orbitState.theta));
|
2026-03-24 01:30:25 +00:00
|
|
|
camera.lookAt(orbitState.target);
|
2026-03-23 16:04:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
memoryCrystals.forEach((c, i) => {
|
|
|
|
|
c.position.y = c.userData.originalPos.y + Math.sin(elapsed * 1.5 + i) * 0.2;
|
|
|
|
|
c.rotation.y = elapsed * 0.5;
|
|
|
|
|
const isSelected = STATE.selectedMemory && STATE.selectedMemory.id === c.userData.memory.id;
|
|
|
|
|
c.material.emissiveIntensity = isSelected ? 2.0 : 0.5 + Math.sin(elapsed * 2 + i) * 0.2;
|
|
|
|
|
c.scale.setScalar(isSelected ? 1.3 : 1.0);
|
2026-03-23 16:04:24 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const core = scene.getObjectByName('nexus-core');
|
2026-03-24 02:21:36 +00:00
|
|
|
if (core) core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
|
2026-03-23 16:04:24 +00:00
|
|
|
|
|
|
|
|
composer.render();
|
|
|
|
|
frameCount++;
|
2026-03-24 02:21:36 +00:00
|
|
|
if (performance.now() - lastFPSTime >= 1000) { fps = frameCount; frameCount = 0; lastFPSTime = performance.now(); STATE.metrics.fps = fps; }
|
|
|
|
|
if (debugOverlay) debugOverlay.textContent = `FPS: ${fps} [${performanceTier}] Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`;
|
2026-03-23 16:04:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 02:21:36 +00:00
|
|
|
function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }
|
2026-03-23 16:04:24 +00:00
|
|
|
|
|
|
|
|
init();
|