Initial Nexus v1 scaffold
This commit is contained in:
983
app.js
Normal file
983
app.js
Normal 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
122
index.html
Normal 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 <span>Mouse</span> look <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
361
style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user