commit d17452dc2502edfd6061044932f039fd5047d979 Author: Perplexity Date: Mon Mar 23 16:04:24 2026 +0000 Initial Nexus v1 scaffold diff --git a/app.js b/app.js new file mode 100644 index 0000000..045e5b0 --- /dev/null +++ b/app.js @@ -0,0 +1,983 @@ +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 + renderer.info.reset(); + composer.render(); + + // Debug overlay + frameCount++; + if (performance.now() - lastFPSTime >= 1000) { + fps = frameCount; + frameCount = 0; + lastFPSTime = performance.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)}`; + } +} + +// ═══ 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(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..3a2c6ea --- /dev/null +++ b/index.html @@ -0,0 +1,122 @@ + + + + + + + + + + + +The Nexus — Timmy's Sovereign Home + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+

THE NEXUS

+

Initializing Sovereign Space...

+
+
+
+ + + + + + + + + + + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..519b05e --- /dev/null +++ b/style.css @@ -0,0 +1,361 @@ +/* === NEXUS DESIGN SYSTEM === */ +:root { + --font-display: 'Orbitron', sans-serif; + --font-body: 'JetBrains Mono', monospace; + + --color-bg: #050510; + --color-surface: rgba(10, 15, 40, 0.85); + --color-border: rgba(74, 240, 192, 0.2); + --color-border-bright: rgba(74, 240, 192, 0.5); + + --color-text: #c8d8e8; + --color-text-muted: #5a6a8a; + --color-text-bright: #e0f0ff; + + --color-primary: #4af0c0; + --color-primary-dim: rgba(74, 240, 192, 0.3); + --color-secondary: #7b5cff; + --color-danger: #ff4466; + --color-warning: #ffaa22; + --color-gold: #ffd700; + + --text-xs: 11px; + --text-sm: 13px; + --text-base: 15px; + --text-lg: 18px; + --text-xl: 24px; + --text-2xl: 36px; + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-6: 24px; + --space-8: 32px; + + --panel-blur: 16px; + --panel-radius: 8px; + --transition-ui: 200ms cubic-bezier(0.16, 1, 0.3, 1); +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + width: 100%; + height: 100%; + overflow: hidden; + background: var(--color-bg); + font-family: var(--font-body); + color: var(--color-text); + -webkit-font-smoothing: antialiased; +} + +canvas#nexus-canvas { + display: block; + width: 100vw; + height: 100vh; + position: fixed; + top: 0; + left: 0; +} + +/* === LOADING SCREEN === */ +#loading-screen { + position: fixed; + inset: 0; + z-index: 1000; + background: var(--color-bg); + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.8s ease; +} +#loading-screen.fade-out { + opacity: 0; + pointer-events: none; +} +.loader-content { + text-align: center; +} +.loader-sigil { + margin-bottom: var(--space-6); +} +.loader-title { + font-family: var(--font-display); + font-size: var(--text-2xl); + font-weight: 700; + letter-spacing: 0.3em; + color: var(--color-primary); + text-shadow: 0 0 30px rgba(74, 240, 192, 0.4); + margin-bottom: var(--space-2); +} +.loader-subtitle { + font-size: var(--text-sm); + color: var(--color-text-muted); + letter-spacing: 0.1em; + margin-bottom: var(--space-6); +} +.loader-bar { + width: 200px; + height: 2px; + background: rgba(74, 240, 192, 0.15); + border-radius: 1px; + margin: 0 auto; + overflow: hidden; +} +.loader-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, var(--color-primary), var(--color-secondary)); + border-radius: 1px; + transition: width 0.3s ease; +} + +/* === ENTER PROMPT === */ +#enter-prompt { + position: fixed; + inset: 0; + z-index: 500; + background: rgba(5, 5, 16, 0.7); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: opacity 0.5s ease; +} +#enter-prompt.fade-out { + opacity: 0; + pointer-events: none; +} +.enter-content { + text-align: center; +} +.enter-content h2 { + font-family: var(--font-display); + font-size: var(--text-xl); + color: var(--color-primary); + letter-spacing: 0.2em; + text-shadow: 0 0 20px rgba(74, 240, 192, 0.3); + margin-bottom: var(--space-2); +} +.enter-content p { + font-size: var(--text-sm); + color: var(--color-text-muted); + animation: pulse-text 2s ease-in-out infinite; +} +@keyframes pulse-text { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +/* === GAME UI (HUD) === */ +.game-ui { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 10; + font-family: var(--font-body); + color: var(--color-text); +} +.game-ui button, .game-ui input, .game-ui [data-interactive] { + pointer-events: auto; +} + +/* Debug overlay */ +.hud-debug { + position: absolute; + top: var(--space-3); + left: var(--space-3); + background: rgba(0, 0, 0, 0.7); + color: #0f0; + font-size: var(--text-xs); + line-height: 1.5; + padding: var(--space-2) var(--space-3); + border-radius: 4px; + white-space: pre; + pointer-events: none; + font-variant-numeric: tabular-nums lining-nums; +} + +/* Location indicator */ +.hud-location { + position: absolute; + top: var(--space-3); + left: 50%; + transform: translateX(-50%); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 500; + letter-spacing: 0.15em; + color: var(--color-primary); + text-shadow: 0 0 10px rgba(74, 240, 192, 0.3); + display: flex; + align-items: center; + gap: var(--space-2); +} +.hud-location-icon { + font-size: 16px; + animation: spin-slow 10s linear infinite; +} +@keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Controls hint */ +.hud-controls { + position: absolute; + bottom: var(--space-3); + left: var(--space-3); + font-size: var(--text-xs); + color: var(--color-text-muted); + pointer-events: none; +} +.hud-controls span { + color: var(--color-primary); + font-weight: 600; +} + +/* === CHAT PANEL === */ +.chat-panel { + position: absolute; + bottom: var(--space-4); + right: var(--space-4); + width: 380px; + max-height: 400px; + background: var(--color-surface); + backdrop-filter: blur(var(--panel-blur)); + border: 1px solid var(--color-border); + border-radius: var(--panel-radius); + display: flex; + flex-direction: column; + overflow: hidden; + pointer-events: auto; + transition: max-height var(--transition-ui); +} +.chat-panel.collapsed { + max-height: 42px; +} +.chat-header { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border); + font-family: var(--font-display); + font-size: var(--text-xs); + letter-spacing: 0.1em; + font-weight: 500; + color: var(--color-text-bright); + cursor: pointer; + flex-shrink: 0; +} +.chat-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-primary); + box-shadow: 0 0 6px var(--color-primary); + animation: dot-pulse 2s ease-in-out infinite; +} +@keyframes dot-pulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} +.chat-toggle-btn { + margin-left: auto; + background: none; + border: none; + color: var(--color-text-muted); + font-size: 14px; + cursor: pointer; + transition: transform var(--transition-ui); +} +.chat-panel.collapsed .chat-toggle-btn { + transform: rotate(180deg); +} +.chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--space-3) var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-2); + max-height: 280px; + scrollbar-width: thin; + scrollbar-color: rgba(74,240,192,0.2) transparent; +} +.chat-msg { + font-size: var(--text-xs); + line-height: 1.6; + padding: var(--space-1) 0; +} +.chat-msg-prefix { + font-weight: 700; +} +.chat-msg-system .chat-msg-prefix { color: var(--color-text-muted); } +.chat-msg-timmy .chat-msg-prefix { color: var(--color-primary); } +.chat-msg-user .chat-msg-prefix { color: var(--color-gold); } +.chat-msg-error .chat-msg-prefix { color: var(--color-danger); } + +.chat-input-row { + display: flex; + border-top: 1px solid var(--color-border); + flex-shrink: 0; +} +.chat-input { + flex: 1; + background: transparent; + border: none; + padding: var(--space-3) var(--space-4); + font-family: var(--font-body); + font-size: var(--text-xs); + color: var(--color-text-bright); + outline: none; +} +.chat-input::placeholder { + color: var(--color-text-muted); +} +.chat-send-btn { + background: none; + border: none; + border-left: 1px solid var(--color-border); + padding: var(--space-3) var(--space-4); + color: var(--color-primary); + font-size: 16px; + cursor: pointer; + transition: background var(--transition-ui); +} +.chat-send-btn:hover { + background: rgba(74, 240, 192, 0.1); +} + +/* === FOOTER === */ +.nexus-footer { + position: fixed; + bottom: var(--space-1); + left: 50%; + transform: translateX(-50%); + z-index: 5; + font-size: 10px; + opacity: 0.3; +} +.nexus-footer a { + color: var(--color-text-muted); + text-decoration: none; +} +.nexus-footer a:hover { + color: var(--color-primary); +} + +/* Mobile adjustments */ +@media (max-width: 480px) { + .chat-panel { + width: calc(100vw - 32px); + right: var(--space-4); + bottom: var(--space-4); + } + .hud-controls { + display: none; + } +}