Initial Nexus v1 scaffold

This commit is contained in:
Perplexity
2026-03-23 16:04:24 +00:00
commit d17452dc25
3 changed files with 1466 additions and 0 deletions

983
app.js Normal file
View File

@@ -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 = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${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();

122
index.html Normal file
View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<!--
______ __
/ ____/___ ____ ___ ____ __ __/ /____ _____
/ / / __ \/ __ `__ \/ __ \/ / / / __/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ / /_/ __/ /
\____/\____/_/ /_/ /_/ .___/\__,_/\__/\___/_/
/_/
Created with Perplexity Computer
https://www.perplexity.ai/computer
-->
<meta name="generator" content="Perplexity Computer">
<meta name="author" content="Perplexity Computer">
<meta property="og:see_also" content="https://www.perplexity.ai/computer">
<link rel="author" href="https://www.perplexity.ai/computer">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Nexus — Timmy's Sovereign Home</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./style.css">
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
}
}
</script>
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen">
<div class="loader-content">
<div class="loader-sigil">
<svg viewBox="0 0 120 120" width="120" height="120">
<defs>
<linearGradient id="sigil-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#4af0c0"/>
<stop offset="100%" stop-color="#7b5cff"/>
</linearGradient>
</defs>
<circle cx="60" cy="60" r="55" fill="none" stroke="url(#sigil-grad)" stroke-width="1.5" opacity="0.4"/>
<circle cx="60" cy="60" r="45" fill="none" stroke="url(#sigil-grad)" stroke-width="1" opacity="0.3">
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="360 60 60" dur="8s" repeatCount="indefinite"/>
</circle>
<polygon points="60,15 95,80 25,80" fill="none" stroke="#4af0c0" stroke-width="1.5" opacity="0.6">
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="-360 60 60" dur="12s" repeatCount="indefinite"/>
</polygon>
<circle cx="60" cy="60" r="8" fill="#4af0c0" opacity="0.8">
<animate attributeName="r" values="6;10;6" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
</circle>
</svg>
</div>
<h1 class="loader-title">THE NEXUS</h1>
<p class="loader-subtitle">Initializing Sovereign Space...</p>
<div class="loader-bar"><div class="loader-fill" id="load-progress"></div></div>
</div>
</div>
<!-- HUD Overlay -->
<div id="hud" class="game-ui" style="display:none;">
<!-- Top Left: Debug -->
<div id="debug-overlay" class="hud-debug"></div>
<!-- Top Center: Location -->
<div class="hud-location">
<span class="hud-location-icon"></span>
<span id="hud-location-text">The Nexus</span>
</div>
<!-- Bottom: Chat Interface -->
<div id="chat-panel" class="chat-panel">
<div class="chat-header">
<span class="chat-status-dot"></span>
<span>Timmy Terminal</span>
<button id="chat-toggle" class="chat-toggle-btn" aria-label="Toggle chat"></button>
</div>
<div id="chat-messages" class="chat-messages">
<div class="chat-msg chat-msg-system">
<span class="chat-msg-prefix">[NEXUS]</span> Sovereign space initialized. Timmy is observing.
</div>
<div class="chat-msg chat-msg-timmy">
<span class="chat-msg-prefix">[TIMMY]</span> Welcome to the Nexus, Alexander. All systems nominal.
</div>
</div>
<div class="chat-input-row">
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">
<button id="chat-send" class="chat-send-btn" aria-label="Send message"></button>
</div>
</div>
<!-- Minimap / Controls hint -->
<div class="hud-controls">
<span>WASD</span> move &nbsp; <span>Mouse</span> look &nbsp; <span>Enter</span> chat
</div>
</div>
<!-- Click to Enter -->
<div id="enter-prompt" style="display:none;">
<div class="enter-content">
<h2>Enter The Nexus</h2>
<p>Click anywhere to begin</p>
</div>
</div>
<canvas id="nexus-canvas"></canvas>
<footer class="nexus-footer">
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
Created with Perplexity Computer
</a>
</footer>
<script type="module" src="./app.js"></script>
</body>
</html>

361
style.css Normal file
View File

@@ -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;
}
}