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, } }; // ═══ STATE ═══ let camera, scene, renderer, composer; let clock, playerPos, playerRot; let keys = {}; let mouseDown = false; let batcaveTerminals = []; let portalMesh, portalGlow; let particles, dustParticles; let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; let loadProgress = 0; // ═══ INIT ═══ function init() { clock = new THREE.Clock(); playerPos = new THREE.Vector3(0, 2, 12); playerRot = new THREE.Euler(0, 0, 0, 'YXZ'); // Renderer const canvas = document.getElementById('nexus-canvas'); renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; updateLoad(20); // Scene scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x050510, 0.012); // Camera camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.copy(playerPos); updateLoad(30); // Build world createSkybox(); updateLoad(40); createLighting(); updateLoad(50); createFloor(); updateLoad(55); createBatcaveTerminal(); updateLoad(70); createPortal(); updateLoad(80); createParticles(); createDustParticles(); updateLoad(85); createAmbientStructures(); updateLoad(90); // Post-processing 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); // Events setupControls(); window.addEventListener('resize', onResize); // Debug overlay ref debugOverlay = document.getElementById('debug-overlay'); updateLoad(100); // Transition from loading to enter screen 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); // Start loop requestAnimationFrame(gameLoop); } function updateLoad(pct) { loadProgress = pct; const fill = document.getElementById('load-progress'); if (fill) fill.style.width = pct + '%'; } // ═══ SKYBOX ═══ function createSkybox() { // Procedural nebula skybox using shader 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; // Hash and noise 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); // Nebula clouds 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); // Nebula glow regions 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); // Stars float starField = hash(dir * 800.0); float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0)); // Twinkling float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28); col += vec3(stars * twinkle); // Big bright stars 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() { // Ambient const ambient = new THREE.AmbientLight(0x1a1a3a, 0.4); scene.add(ambient); // Main directional (moonlight feel) const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6); dirLight.position.set(10, 20, 10); dirLight.castShadow = true; dirLight.shadow.mapSize.set(1024, 1024); dirLight.shadow.camera.near = 0.5; dirLight.shadow.camera.far = 80; dirLight.shadow.camera.left = -30; dirLight.shadow.camera.right = 30; dirLight.shadow.camera.top = 30; dirLight.shadow.camera.bottom = -30; scene.add(dirLight); // Teal accent from below terminal const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5); tealLight.position.set(0, 1, -5); scene.add(tealLight); // Purple accent const purpleLight = new THREE.PointLight(NEXUS.colors.secondary, 1.5, 25, 1.5); purpleLight.position.set(-8, 3, -8); scene.add(purpleLight); // Portal glow light const portalLight = new THREE.PointLight(0xff6600, 2, 20, 1.5); portalLight.position.set(15, 4, -10); scene.add(portalLight); } // ═══ FLOOR ═══ function createFloor() { // Main hexagonal-feel platform using a flat circle 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); // Grid lines on the floor 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); // Glowing edge ring const ringGeo = new THREE.RingGeometry(24.5, 25.2, 6); const ringMat = new THREE.MeshBasicMaterial({ color: NEXUS.colors.primary, transparent: true, opacity: 0.15, side: THREE.DoubleSide, }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = -Math.PI / 2; ring.position.y = 0.05; scene.add(ring); // Inner ring const innerRingGeo = new THREE.RingGeometry(14.5, 15, 32); const innerRingMat = new THREE.MeshBasicMaterial({ color: NEXUS.colors.secondary, transparent: true, opacity: 0.08, side: THREE.DoubleSide, }); const innerRing = new THREE.Mesh(innerRingGeo, innerRingMat); innerRing.rotation.x = -Math.PI / 2; innerRing.position.y = 0.03; scene.add(innerRing); } // ═══ BATCAVE TERMINAL ═══ function createBatcaveTerminal() { const termGroup = new THREE.Group(); termGroup.position.set(0, 0, -8); // Main large screen createHoloPanel(termGroup, { x: 0, y: 4, z: 0, w: 8, h: 5, title: '◈ NEXUS COMMAND', lines: [ '┌─────────────────────────────────┐', '│ SYSTEM STATUS NOMINAL │', '│ HERMES HARNESS ACTIVE │', '│ AGENT LOOPS 3/3 RUN │', '│ MEMORY BANKS 2.4 GB │', '│ THOUGHT CYCLES 14,892 │', '├─────────────────────────────────┤', '│ ACTIVE PROCESSES │', '│ ▸ triage-daemon ● RUNNING │', '│ ▸ code-review-loop ● RUNNING │', '│ ▸ world-builder ○ STANDBY │', '│ ▸ matrix-renderer ● RUNNING │', '└─────────────────────────────────┘', ], color: NEXUS.colors.primary, }); // Left panel — Dev Items createHoloPanel(termGroup, { x: -6, y: 3.5, z: 1, w: 4, h: 4, rotY: 0.3, title: '⚡ DEV QUEUE', lines: [ '#1090 Nexus v1 Build', '#1079 Code Hygiene Epic', '#1080 Showcase Epic', '#864 PR Pending Merge', '#1076 Deep Triage Gov.', '', 'Open Issues: 293', 'Closed Today: 19', ], color: NEXUS.colors.secondary, }); // Right panel — Metrics createHoloPanel(termGroup, { x: 6, y: 3.5, z: 1, w: 4, h: 4, rotY: -0.3, title: '📊 METRICS', lines: [ 'Uptime: 23d 14h 22m', 'Commits: 1,847', 'Agents: 5 active', 'Worlds: 1 (Nexus)', 'Portals: 1 staging', '', 'CPU: ████████░░ 78%', 'MEM: ██████░░░░ 62%', ], color: 0x44aaff, }); // Far left — Thought Stream createHoloPanel(termGroup, { x: -10, y: 2.5, z: 3, w: 3.5, h: 3, rotY: 0.5, title: '💭 THOUGHTS', lines: [ 'Considering portal arch.', 'Morrowind integration is', 'next priority after the', 'Nexus shell is stable.', '', 'The harness is the core', 'product. Focus there.', ], color: NEXUS.colors.gold, }); // Far right — Agents createHoloPanel(termGroup, { x: 10, y: 2.5, z: 3, w: 3.5, h: 3, rotY: -0.5, title: '🤖 AGENTS', lines: [ 'Claude Code ● ACTIVE', 'Kimi ● ACTIVE', 'Gemini ○ STANDBY', 'Hermes ● ACTIVE', 'Perplexity ● ACTIVE', ], color: 0xff8844, }); scene.add(termGroup); } function createHoloPanel(parent, opts) { const { x, y, z, w, h, title, lines, color, rotY } = opts; const group = new THREE.Group(); group.position.set(x, y, z); if (rotY) group.rotation.y = rotY; // Background panel const panelGeo = new THREE.PlaneGeometry(w, h); const panelMat = new THREE.MeshBasicMaterial({ color: 0x000815, transparent: true, opacity: 0.7, side: THREE.DoubleSide, }); const panel = new THREE.Mesh(panelGeo, panelMat); group.add(panel); // Border frame const borderGeo = new THREE.EdgesGeometry(panelGeo); const borderMat = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.6, }); const border = new THREE.LineSegments(borderGeo, borderMat); group.add(border); // Text content via CanvasTexture const textCanvas = document.createElement('canvas'); const ctx = textCanvas.getContext('2d'); const res = 512; textCanvas.width = res * (w / h); textCanvas.height = res; ctx.fillStyle = 'transparent'; ctx.clearRect(0, 0, textCanvas.width, textCanvas.height); // Title const cHex = '#' + new THREE.Color(color).getHexString(); ctx.font = 'bold 28px "JetBrains Mono", monospace'; ctx.fillStyle = cHex; ctx.fillText(title, 20, 40); // Separator ctx.strokeStyle = cHex; ctx.globalAlpha = 0.3; ctx.beginPath(); ctx.moveTo(20, 52); ctx.lineTo(textCanvas.width - 20, 52); ctx.stroke(); ctx.globalAlpha = 1; // Lines ctx.font = '20px "JetBrains Mono", monospace'; ctx.fillStyle = '#a0b8d0'; lines.forEach((line, i) => { // Color active indicators 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, 80 + i * 30); }); 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); // Scanline effect overlay 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); // Glow behind panel const glowGeo = new THREE.PlaneGeometry(w + 0.5, h + 0.5); const glowMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.06, side: THREE.DoubleSide, }); const glowMesh = new THREE.Mesh(glowGeo, glowMat); glowMesh.position.z = -0.05; group.add(glowMesh); parent.add(group); batcaveTerminals.push({ group, scanMat, borderMat }); } // ═══ PORTAL ═══ function createPortal() { const portalGroup = new THREE.Group(); portalGroup.position.set(15, 0, -10); portalGroup.rotation.y = -0.5; // Portal ring const torusGeo = new THREE.TorusGeometry(3, 0.15, 16, 64); const torusMat = new THREE.MeshStandardMaterial({ color: 0xff6600, emissive: 0xff4400, emissiveIntensity: 1.5, roughness: 0.2, metalness: 0.8, }); portalMesh = new THREE.Mesh(torusGeo, torusMat); portalMesh.position.y = 3.5; portalGroup.add(portalMesh); // Inner swirl const swirlGeo = new THREE.CircleGeometry(2.8, 64); const swirlMat = new THREE.ShaderMaterial({ transparent: true, side: THREE.DoubleSide, uniforms: { uTime: { value: 0 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float uTime; varying vec2 vUv; void main() { 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(vec3(1.0, 0.3, 0.0), vec3(1.0, 0.6, 0.1), swirl); col = mix(col, vec3(1.0, 0.8, 0.3), swirl2 * 0.3); float alpha = mask * (0.5 + 0.3 * swirl); gl_FragColor = vec4(col, alpha); } `, }); portalGlow = new THREE.Mesh(swirlGeo, swirlMat); portalGlow.position.y = 3.5; portalGroup.add(portalGlow); // 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 = '#ff8844'; lctx.textAlign = 'center'; lctx.fillText('◈ MORROWIND', 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; portalGroup.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: 0xff4400, emissiveIntensity: 0.1, }); const pillar = new THREE.Mesh(pillarGeo, pillarMat); pillar.position.set(side * 3, 3.5, 0); pillar.castShadow = true; portalGroup.add(pillar); } scene.add(portalGroup); } // ═══ PARTICLES ═══ function createParticles() { const count = 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 = 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() { // Crystal formations around the edges 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); }); // Floating rune stones 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, roughness: 0.3, metalness: 0.7, }); 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); } // Central pedestal / nexus core 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); // Core pedestal 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); } // ═══ 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(); } }); document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; }); // Mouse look const canvas = document.getElementById('nexus-canvas'); canvas.addEventListener('mousedown', (e) => { if (e.target === canvas) mouseDown = true; }); document.addEventListener('mouseup', () => { mouseDown = false; }); document.addEventListener('mousemove', (e) => { if (!mouseDown) return; if (document.activeElement === document.getElementById('chat-input')) return; 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)); }); // Chat toggle document.getElementById('chat-toggle').addEventListener('click', () => { chatOpen = !chatOpen; document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen); }); document.getElementById('chat-header')?.addEventListener('click', () => { chatOpen = !chatOpen; document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen); }); // Chat send document.getElementById('chat-send').addEventListener('click', sendChatMessage); } function sendChatMessage() { const input = document.getElementById('chat-input'); const text = input.value.trim(); if (!text) return; addChatMessage('user', text); input.value = ''; // Simulate Timmy response 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 = `${prefixes[type] || '[???]'} ${text}`; container.appendChild(div); container.scrollTop = container.scrollHeight; } // ═══ GAME LOOP ═══ function gameLoop() { requestAnimationFrame(gameLoop); const delta = Math.min(clock.getDelta(), 0.1); const elapsed = clock.elapsedTime; // Movement if (document.activeElement !== document.getElementById('chat-input')) { 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); // Clamp to platform 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; } } } camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ'); // Animate skybox const sky = scene.getObjectByName('skybox'); if (sky) sky.material.uniforms.uTime.value = elapsed; // Animate terminal scanlines batcaveTerminals.forEach(t => { if (t.scanMat?.uniforms) t.scanMat.uniforms.uTime.value = elapsed; }); // Animate portal if (portalMesh) { portalMesh.rotation.z = elapsed * 0.3; portalMesh.rotation.x = Math.sin(elapsed * 0.5) * 0.1; } if (portalGlow?.material?.uniforms) { portalGlow.material.uniforms.uTime.value = elapsed; } // Animate particles if (particles?.material?.uniforms) { particles.material.uniforms.uTime.value = elapsed; } // Animate dust if (dustParticles) { dustParticles.rotation.y = elapsed * 0.01; } // Animate runestones 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; } } // Animate nexus core 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; } // Render composer.render(); // Debug overlay (read AFTER render so counts are populated) 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}\n` + `Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)}`; } renderer.info.reset(); } // ═══ RESIZE ═══ function onResize() { const w = window.innerWidth; const h = window.innerHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); composer.setSize(w, h); } // ═══ START ═══ init();