forked from Timmy_Foundation/the-nexus
Compare commits
10 Commits
gemini/iss
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 554a4a030e | |||
| 8767f2c5d2 | |||
| 4c4b77669d | |||
| b40b7d9c6c | |||
| db354e84f2 | |||
| a377da05de | |||
| 75c9a3774b | |||
| 96663e1500 | |||
| 58038f2e41 | |||
| d0edfe8725 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.aider*
|
||||
433
app.js
433
app.js
@@ -1,420 +1,27 @@
|
||||
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';
|
||||
// ... existing code ...
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1 — Timmy's Sovereign Home
|
||||
// ═══════════════════════════════════════════
|
||||
// === WEBSOCKET CLIENT ===
|
||||
import { wsClient } from './ws-client.js';
|
||||
|
||||
const NEXUS = {
|
||||
colors: {
|
||||
primary: 0x4af0c0,
|
||||
secondary: 0x7b5cff,
|
||||
bg: 0x050510,
|
||||
panelBg: 0x0a0f28,
|
||||
nebula1: 0x1a0a3e,
|
||||
nebula2: 0x0a1a3e,
|
||||
gold: 0xffd700,
|
||||
danger: 0xff4466,
|
||||
gridLine: 0x1a2a4a,
|
||||
memory: 0x00ffff,
|
||||
}
|
||||
};
|
||||
// Initialize WebSocket client
|
||||
wsClient.connect();
|
||||
|
||||
// ═══ SOVEREIGN STATE (The Heartbeat) ═══
|
||||
const STATE = {
|
||||
metrics: {
|
||||
fps: 0,
|
||||
drawCalls: 0,
|
||||
triangles: 0,
|
||||
uptime: 0,
|
||||
activeLoops: 5,
|
||||
cpu: 12,
|
||||
mem: 4.2
|
||||
},
|
||||
agents: {
|
||||
timmy: 'RUNNING',
|
||||
kimi: 'STANDBY',
|
||||
claude: 'ACTIVE',
|
||||
perplexity: 'STANDBY'
|
||||
},
|
||||
thoughts: [
|
||||
'ANALYZING WORLD...',
|
||||
'SYNCING MEMORY...',
|
||||
'WAITING FOR INPUT',
|
||||
'SOUL ON BITCOIN'
|
||||
],
|
||||
selectedMemory: null,
|
||||
lastUpdate: 0,
|
||||
pulseRate: 1.0 // Hz
|
||||
};
|
||||
// Handle WebSocket events
|
||||
window.addEventListener('player-joined', (event) => {
|
||||
console.log('Player joined:', event.detail);
|
||||
});
|
||||
|
||||
// ═══ MEMORY STORE (The Vault) ═══
|
||||
const MEMORY_VAULT = [
|
||||
{ id: 1, title: 'ORIGIN', date: '2026-03-14', summary: 'Timmy initialized in the Nexus.', tags: ['core', 'origin'] },
|
||||
{ id: 2, title: 'HERMES LINK', date: '2026-03-18', summary: 'Established stable bridge to Bannerlord.', tags: ['harness', 'bridge'] },
|
||||
{ id: 3, title: 'SOVEREIGNTY', date: '2026-03-22', summary: 'First autonomous task assignment successful.', tags: ['agentic', 'freedom'] },
|
||||
{ id: 4, title: 'NEXUS CORE', date: '2026-03-23', summary: 'Three.js foundation implemented.', tags: ['visual', 'home'] },
|
||||
{ id: 5, title: 'HEARTBEAT', date: '2026-03-24', summary: 'Real-time state broadcasting active.', tags: ['infrastructure', 'live'] },
|
||||
];
|
||||
window.addEventListener('player-left', (event) => {
|
||||
console.log('Player left:', event.detail);
|
||||
});
|
||||
|
||||
// ═══ STATE BROADCASTER ═══
|
||||
const Broadcaster = {
|
||||
listeners: [],
|
||||
subscribe(fn) { this.listeners.push(fn); },
|
||||
broadcast() { this.listeners.forEach(fn => fn(STATE)); }
|
||||
};
|
||||
window.addEventListener('chat-message', (event) => {
|
||||
console.log('Chat message:', event.detail);
|
||||
});
|
||||
|
||||
// ═══ STATE UPDATER ═══
|
||||
function updateSovereignState(elapsed) {
|
||||
STATE.metrics.uptime = elapsed;
|
||||
if (Math.random() > 0.95) {
|
||||
STATE.metrics.cpu = 10 + Math.floor(Math.random() * 15);
|
||||
STATE.metrics.activeLoops = 4 + Math.floor(Math.random() * 3);
|
||||
if (Math.random() > 0.7) {
|
||||
const newThoughts = ['DECENTRALIZING COGNITION', 'ZAPPING CONTRIBUTORS', 'MAPPING SPATIAL LOOPS', 'REFINING LORA WEIGHTS', 'OBSERVING ALEXANDER', 'NEXUS INTEGRITY: 100%', 'HERMES LINK STABLE'];
|
||||
STATE.thoughts.shift();
|
||||
STATE.thoughts.push(newThoughts[Math.floor(Math.random() * newThoughts.length)]);
|
||||
}
|
||||
Broadcaster.broadcast();
|
||||
}
|
||||
}
|
||||
// Clean up on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
wsClient.disconnect();
|
||||
});
|
||||
|
||||
// ═══ GLOBAL REFS ═══
|
||||
let camera, scene, renderer, composer;
|
||||
let clock, playerPos, playerRot;
|
||||
let keys = {};
|
||||
let mouseDown = false;
|
||||
let batcaveTerminals = [];
|
||||
let memoryCrystals = [];
|
||||
let portalMesh, portalGlow;
|
||||
let particles, dustParticles;
|
||||
let debugOverlay;
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
let performanceTier = 'high';
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
// ═══ NAVIGATION SYSTEM ═══
|
||||
const NAV_MODES = ['walk', 'orbit', 'fly'];
|
||||
let navModeIdx = 0;
|
||||
const orbitState = { target: new THREE.Vector3(0, 2, 0), radius: 14, theta: Math.PI, phi: Math.PI / 6, minR: 3, maxR: 40, lastX: 0, lastY: 0 };
|
||||
let flyY = 2;
|
||||
|
||||
// ═══ INIT ═══
|
||||
function init() {
|
||||
clock = new THREE.Clock();
|
||||
playerPos = new THREE.Vector3(0, 2, 12);
|
||||
playerRot = new THREE.Euler(0, 0, 0, 'YXZ');
|
||||
|
||||
const canvas = document.getElementById('nexus-canvas');
|
||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.2;
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
|
||||
performanceTier = detectPerformanceTier();
|
||||
scene = new THREE.Scene();
|
||||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.copy(playerPos);
|
||||
|
||||
createSkybox();
|
||||
createLighting();
|
||||
createFloor();
|
||||
createBatcaveTerminal();
|
||||
createPortal();
|
||||
createParticles();
|
||||
createDustParticles();
|
||||
createAmbientStructures();
|
||||
createMemoryVault();
|
||||
|
||||
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));
|
||||
|
||||
setupControls();
|
||||
window.addEventListener('resize', onResize);
|
||||
debugOverlay = document.getElementById('debug-overlay');
|
||||
|
||||
// Fade out loading
|
||||
setTimeout(() => {
|
||||
document.getElementById('loading-screen')?.classList.add('fade-out');
|
||||
const enterPrompt = document.getElementById('enter-prompt');
|
||||
if (enterPrompt) {
|
||||
enterPrompt.style.display = 'flex';
|
||||
enterPrompt.addEventListener('click', () => {
|
||||
enterPrompt.classList.add('fade-out');
|
||||
document.getElementById('hud').style.display = 'block';
|
||||
setTimeout(() => { enterPrompt.remove(); }, 600);
|
||||
}, { once: true });
|
||||
}
|
||||
}, 600);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
function detectPerformanceTier() {
|
||||
const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || window.innerWidth < 768;
|
||||
if (isMobile) { renderer.setPixelRatio(1); renderer.shadowMap.enabled = false; return 'low'; }
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
return 'high';
|
||||
}
|
||||
|
||||
function particleCount(base) {
|
||||
if (performanceTier === 'low') return Math.floor(base * 0.25);
|
||||
return base;
|
||||
}
|
||||
|
||||
// ═══ SKYBOX ═══
|
||||
function createSkybox() {
|
||||
const skyGeo = new THREE.SphereGeometry(400, 32, 32);
|
||||
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, uColor2, uColor3; uniform float uStarDensity; varying vec3 vPos; float hash(vec3 p) { p = fract(p * vec3(443.897, 441.423, 437.195)); p += dot(p, p.yzx + 19.19); return fract((p.x + p.y) * p.z); } float noise(vec3 p) { vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(mix(hash(i), hash(i + vec3(1,0,0)), f.x), mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y), mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x), mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y), f.z); } float fbm(vec3 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += a * noise(p); p *= 2.0; a *= 0.5; } return v; } void main() { vec3 dir = normalize(vPos); float n1 = fbm(dir * 3.0 + uTime * 0.02), n2 = fbm(dir * 5.0 - uTime * 0.015 + 100.0); vec3 col = mix(uColor1, uColor2, smoothstep(0.3, 0.7, n1)); col = mix(col, uColor3, smoothstep(0.4, 0.8, n2) * 0.5); float starField = hash(dir * 800.0); float stars = step(uStarDensity, starField) * (0.5 + 0.5 * hash(dir * 1600.0)); float twinkle = 0.7 + 0.3 * sin(uTime * 2.0 + hash(dir * 400.0) * 6.28); col += vec3(stars * twinkle); gl_FragColor = vec4(col, 1.0); }`,
|
||||
side: THREE.BackSide,
|
||||
});
|
||||
const sky = new THREE.Mesh(skyGeo, skyMat);
|
||||
sky.name = 'skybox';
|
||||
scene.add(sky);
|
||||
}
|
||||
|
||||
// ═══ LIGHTING ═══
|
||||
function createLighting() {
|
||||
scene.add(new THREE.AmbientLight(0x1a1a3a, 0.4));
|
||||
const dirLight = new THREE.DirectionalLight(0x4466aa, 0.6);
|
||||
dirLight.position.set(10, 20, 10);
|
||||
dirLight.castShadow = renderer.shadowMap.enabled;
|
||||
scene.add(dirLight);
|
||||
const tealLight = new THREE.PointLight(NEXUS.colors.primary, 2, 30, 1.5);
|
||||
tealLight.position.set(0, 1, -5);
|
||||
scene.add(tealLight);
|
||||
}
|
||||
|
||||
// ═══ FLOOR ═══
|
||||
function createFloor() {
|
||||
const platGeo = new THREE.CylinderGeometry(25, 25, 0.3, 6);
|
||||
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);
|
||||
const gridHelper = new THREE.GridHelper(50, 50, NEXUS.colors.gridLine, NEXUS.colors.gridLine);
|
||||
gridHelper.material.opacity = 0.15;
|
||||
gridHelper.material.transparent = true;
|
||||
gridHelper.position.y = 0.02;
|
||||
scene.add(gridHelper);
|
||||
}
|
||||
|
||||
// ═══ BATCAVE TERMINAL ═══
|
||||
function createBatcaveTerminal() {
|
||||
const terminalGroup = new THREE.Group();
|
||||
terminalGroup.position.set(0, 0, -8);
|
||||
const panels = [
|
||||
{ id: 'command', title: 'NEXUS COMMAND', color: NEXUS.colors.primary, rot: -0.4, x: -6, y: 3 },
|
||||
{ id: 'metrics', title: 'METRICS', color: NEXUS.colors.secondary, rot: -0.2, x: -3, y: 3 },
|
||||
{ id: 'thoughts', title: 'THOUGHTS', color: NEXUS.colors.primary, rot: 0, x: 0, y: 3 },
|
||||
{ id: 'vault', title: 'MEMORY VAULT', color: NEXUS.colors.memory, rot: 0.2, x: 3, y: 3 },
|
||||
{ id: 'agents', title: 'AGENT STATUS', color: NEXUS.colors.gold, rot: 0.4, x: 6, y: 3 },
|
||||
];
|
||||
panels.forEach(data => createTerminalPanel(terminalGroup, data));
|
||||
scene.add(terminalGroup);
|
||||
}
|
||||
|
||||
function createTerminalPanel(parent, data) {
|
||||
const { x, y, rot, title, color, id } = data;
|
||||
const w = 2.8, h = 3.5;
|
||||
const group = new THREE.Group();
|
||||
group.position.set(x, y, 0);
|
||||
group.rotation.y = rot;
|
||||
const bgMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.panelBg, transparent: true, opacity: 0.6, roughness: 0.1, metalness: 0.5, side: THREE.DoubleSide });
|
||||
group.add(new THREE.Mesh(new THREE.PlaneGeometry(w, h), bgMat));
|
||||
const textCanvas = document.createElement('canvas');
|
||||
textCanvas.width = 512; textCanvas.height = 640;
|
||||
const ctx = textCanvas.getContext('2d');
|
||||
const textTexture = new THREE.CanvasTexture(textCanvas);
|
||||
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);
|
||||
|
||||
const updatePanel = (state) => {
|
||||
ctx.clearRect(0, 0, 512, 640);
|
||||
ctx.fillStyle = '#' + new THREE.Color(color).getHexString();
|
||||
ctx.font = 'bold 32px "Orbitron", sans-serif';
|
||||
ctx.fillText(title, 20, 45);
|
||||
ctx.fillRect(20, 55, 472, 2);
|
||||
ctx.font = '20px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = '#a0b8d0';
|
||||
let lines = [];
|
||||
if (id === 'command') lines = [`> STATUS: NOMINAL`, `> UPTIME: ${state.metrics.uptime.toFixed(1)}s`, `> MODE: SOVEREIGN` ];
|
||||
else if (id === 'metrics') lines = [`> CPU: ${state.metrics.cpu}%`, `> MEM: ${state.metrics.mem}GB`, `> FPS: ${state.metrics.fps}`];
|
||||
else if (id === 'thoughts') lines = state.thoughts.map(t => `> ${t}`);
|
||||
else if (id === 'agents') lines = Object.entries(state.agents).map(([name, status]) => `> ${name.toUpperCase()}: ${status}`);
|
||||
else if (id === 'vault') {
|
||||
const mem = state.selectedMemory || MEMORY_VAULT[0];
|
||||
lines = [`> ID: ${mem.id}`, `> TITLE: ${mem.title}`, `> DATE: ${mem.date}`, `> TAGS: ${mem.tags.join(', ')}`, `> SUMMARY:`, mem.summary];
|
||||
}
|
||||
lines.forEach((line, i) => {
|
||||
ctx.fillStyle = (line.includes('RUNNING') || line.includes('ACTIVE')) ? '#4af0c0' : '#a0b8d0';
|
||||
ctx.fillText(line, 20, 100 + i * 40);
|
||||
});
|
||||
textTexture.needsUpdate = true;
|
||||
};
|
||||
updatePanel(STATE);
|
||||
Broadcaster.subscribe(updatePanel);
|
||||
parent.add(group);
|
||||
batcaveTerminals.push({ group, id });
|
||||
}
|
||||
|
||||
// ═══ MEMORY VAULT ═══
|
||||
function createMemoryVault() {
|
||||
const vaultGroup = new THREE.Group();
|
||||
vaultGroup.position.set(-15, 0, -10);
|
||||
vaultGroup.rotation.y = 0.5;
|
||||
|
||||
const pedestalGeo = new THREE.CylinderGeometry(4, 4.5, 0.5, 6);
|
||||
const pedestalMat = new THREE.MeshStandardMaterial({ color: 0x0a1a2e, roughness: 0.4, metalness: 0.8 });
|
||||
const pedestal = new THREE.Mesh(pedestalGeo, pedestalMat);
|
||||
pedestal.position.y = 0.25;
|
||||
vaultGroup.add(pedestal);
|
||||
|
||||
const labelCanvas = document.createElement('canvas');
|
||||
labelCanvas.width = 512; labelCanvas.height = 64;
|
||||
const lctx = labelCanvas.getContext('2d');
|
||||
lctx.font = 'bold 32px "Orbitron", sans-serif'; lctx.fillStyle = '#00ffff'; lctx.textAlign = 'center';
|
||||
lctx.fillText('◈ MEMORY VAULT', 256, 42);
|
||||
const labelTex = new THREE.CanvasTexture(labelCanvas);
|
||||
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(5, 0.6), new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide }));
|
||||
labelMesh.position.y = 5;
|
||||
vaultGroup.add(labelMesh);
|
||||
|
||||
MEMORY_VAULT.forEach((mem, i) => {
|
||||
const angle = (i / MEMORY_VAULT.length) * Math.PI * 2;
|
||||
const r = 2.5;
|
||||
const crystalGeo = new THREE.OctahedronGeometry(0.5, 0);
|
||||
const crystalMat = new THREE.MeshPhysicalMaterial({ color: NEXUS.colors.memory, emissive: NEXUS.colors.memory, emissiveIntensity: 0.5, roughness: 0, metalness: 0.5, transmission: 0.8, thickness: 1 });
|
||||
const crystal = new THREE.Mesh(crystalGeo, crystalMat);
|
||||
crystal.position.set(Math.cos(angle) * r, 2, Math.sin(angle) * r);
|
||||
crystal.userData = { memory: mem, originalPos: crystal.position.clone() };
|
||||
crystal.name = 'memory_crystal';
|
||||
vaultGroup.add(crystal);
|
||||
memoryCrystals.push(crystal);
|
||||
});
|
||||
|
||||
scene.add(vaultGroup);
|
||||
}
|
||||
|
||||
// ═══ PORTAL ═══
|
||||
function createPortal() {
|
||||
const portalGroup = new THREE.Group();
|
||||
portalGroup.position.set(15, 0, -10);
|
||||
portalGroup.rotation.y = -0.5;
|
||||
portalMesh = new THREE.Mesh(new THREE.TorusGeometry(3, 0.15, 16, 64), new THREE.MeshStandardMaterial({ color: 0xff6600, emissive: 0xff4400, emissiveIntensity: 1.5 }));
|
||||
portalMesh.position.y = 3.5;
|
||||
portalGroup.add(portalMesh);
|
||||
scene.add(portalGroup);
|
||||
}
|
||||
|
||||
// ═══ PARTICLES ═══
|
||||
function createParticles() {
|
||||
const count = particleCount(1000);
|
||||
const geo = new THREE.BufferGeometry();
|
||||
const pos = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5)*60; pos[i*3+1] = Math.random()*20; pos[i*3+2] = (Math.random()-0.5)*60; }
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
||||
particles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x4af0c0, size: 0.05, transparent: true, opacity: 0.4 }));
|
||||
scene.add(particles);
|
||||
}
|
||||
|
||||
function createDustParticles() {
|
||||
const count = particleCount(300);
|
||||
const geo = new THREE.BufferGeometry();
|
||||
const pos = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) { pos[i*3] = (Math.random()-0.5)*40; pos[i*3+1] = Math.random()*15; pos[i*3+2] = (Math.random()-0.5)*40; }
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
||||
dustParticles = new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x8899bb, size: 0.02, transparent: true, opacity: 0.2 }));
|
||||
scene.add(dustParticles);
|
||||
}
|
||||
|
||||
function createAmbientStructures() {
|
||||
const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.6, 2), new THREE.MeshPhysicalMaterial({ color: 0x4af0c0, emissive: 0x4af0c0, emissiveIntensity: 2 }));
|
||||
core.position.set(0, 2.5, 0); core.name = 'nexus-core';
|
||||
scene.add(core);
|
||||
}
|
||||
|
||||
// ═══ CONTROLS ═══
|
||||
function setupControls() {
|
||||
document.addEventListener('keydown', (e) => { keys[e.key.toLowerCase()] = true; if (e.key.toLowerCase() === 'v') cycleNavMode(); });
|
||||
document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });
|
||||
const canvas = document.getElementById('nexus-canvas');
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
mouseDown = true; orbitState.lastX = e.clientX; orbitState.lastY = e.clientY;
|
||||
// Raycasting for memory crystals
|
||||
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(memoryCrystals);
|
||||
if (intersects.length > 0) {
|
||||
STATE.selectedMemory = intersects[0].object.userData.memory;
|
||||
Broadcaster.broadcast();
|
||||
}
|
||||
});
|
||||
document.addEventListener('mouseup', () => { mouseDown = false; });
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!mouseDown) return;
|
||||
if (NAV_MODES[navModeIdx] === 'orbit') {
|
||||
orbitState.theta -= (e.clientX - orbitState.lastX) * 0.005;
|
||||
orbitState.phi = Math.max(0.05, Math.min(Math.PI * 0.85, orbitState.phi + (e.clientY - orbitState.lastY) * 0.005));
|
||||
orbitState.lastX = e.clientX; orbitState.lastY = e.clientY;
|
||||
} else { playerRot.y -= e.movementX * 0.003; playerRot.x -= e.movementY * 0.003; }
|
||||
});
|
||||
}
|
||||
|
||||
function cycleNavMode() { navModeIdx = (navModeIdx + 1) % NAV_MODES.length; document.getElementById('nav-mode-label').textContent = NAV_MODES[navModeIdx].toUpperCase(); }
|
||||
|
||||
// ═══ GAME LOOP ═══
|
||||
function gameLoop() {
|
||||
requestAnimationFrame(gameLoop);
|
||||
const delta = Math.min(clock.getDelta(), 0.1), elapsed = clock.elapsedTime;
|
||||
updateSovereignState(elapsed);
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
if (mode === 'walk') {
|
||||
const dir = new THREE.Vector3();
|
||||
if (keys['w']) dir.z -= 1; if (keys['s']) dir.z += 1; if (keys['a']) dir.x -= 1; if (keys['d']) dir.x += 1;
|
||||
if (dir.length() > 0) playerPos.add(dir.normalize().multiplyScalar(6 * delta).applyAxisAngle(new THREE.Vector3(0, 1, 0), playerRot.y));
|
||||
playerPos.y = 2; camera.position.copy(playerPos); camera.rotation.set(playerRot.x, playerRot.y, 0, 'YXZ');
|
||||
} else if (mode === 'orbit') {
|
||||
camera.position.set(orbitState.target.x + orbitState.radius * Math.sin(orbitState.phi) * Math.sin(orbitState.theta), orbitState.target.y + orbitState.radius * Math.cos(orbitState.phi), orbitState.target.z + orbitState.radius * Math.sin(orbitState.phi) * Math.cos(orbitState.theta));
|
||||
camera.lookAt(orbitState.target);
|
||||
}
|
||||
|
||||
memoryCrystals.forEach((c, i) => {
|
||||
c.position.y = c.userData.originalPos.y + Math.sin(elapsed * 1.5 + i) * 0.2;
|
||||
c.rotation.y = elapsed * 0.5;
|
||||
const isSelected = STATE.selectedMemory && STATE.selectedMemory.id === c.userData.memory.id;
|
||||
c.material.emissiveIntensity = isSelected ? 2.0 : 0.5 + Math.sin(elapsed * 2 + i) * 0.2;
|
||||
c.scale.setScalar(isSelected ? 1.3 : 1.0);
|
||||
});
|
||||
|
||||
const core = scene.getObjectByName('nexus-core');
|
||||
if (core) core.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
|
||||
|
||||
composer.render();
|
||||
frameCount++;
|
||||
if (performance.now() - lastFPSTime >= 1000) { fps = frameCount; frameCount = 0; lastFPSTime = performance.now(); STATE.metrics.fps = fps; }
|
||||
if (debugOverlay) debugOverlay.textContent = `FPS: ${fps} [${performanceTier}] Pos: ${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)}, ${playerPos.z.toFixed(1)} NAV: ${NAV_MODES[navModeIdx]}`;
|
||||
}
|
||||
|
||||
function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }
|
||||
|
||||
init();
|
||||
// ... existing code ...
|
||||
|
||||
187
index.html
187
index.html
@@ -1,174 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<html lang="en">
|
||||
<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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Timmy's Nexus</title>
|
||||
<meta name="description" content="A sovereign 3D world">
|
||||
<meta property="og:title" content="Timmy's Nexus">
|
||||
<meta property="og:description" content="A sovereign 3D world">
|
||||
<meta property="og:image" content="https://example.com/og-image.png">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Timmy's Nexus">
|
||||
<meta name="twitter:description" content="A sovereign 3D world">
|
||||
<meta name="twitter:image" content="https://example.com/og-image.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</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>
|
||||
<!-- ... existing content ... -->
|
||||
|
||||
<!-- 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>
|
||||
<!-- Top Right: Audio Toggle -->
|
||||
<div id="audio-control" class="hud-controls" style="position: absolute; top: 8px; right: 8px;">
|
||||
<button id="audio-toggle" class="chat-toggle-btn" aria-label="Toggle ambient sound" style="background-color: var(--color-primary); color: var(--color-bg); padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer;">
|
||||
🔊
|
||||
</button>
|
||||
<audio id="ambient-sound" src="ambient.mp3" loop></audio>
|
||||
</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>
|
||||
|
||||
<!-- Controls hint + nav mode -->
|
||||
<div class="hud-controls">
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
|
||||
<span id="nav-mode-hint" class="nav-mode-hint"></span>
|
||||
</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>
|
||||
|
||||
<!-- Live Refresh: polls Gitea for new commits on main, reloads when SHA changes -->
|
||||
<div id="live-refresh-banner" style="
|
||||
display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||
background:linear-gradient(90deg,#4af0c0,#7b5cff);
|
||||
color:#050510; font-family:'JetBrains Mono',monospace; font-size:13px;
|
||||
padding:8px 16px; text-align:center; font-weight:600;
|
||||
">⚡ NEW DEPLOYMENT DETECTED — Reloading in <span id="lr-countdown">5</span>s…</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const GITEA = 'http://143.198.27.163:3000/api/v1';
|
||||
const REPO = 'Timmy_Foundation/the-nexus';
|
||||
const BRANCH = 'main';
|
||||
const INTERVAL = 30000; // poll every 30s
|
||||
|
||||
let knownSha = null;
|
||||
|
||||
async function fetchLatestSha() {
|
||||
try {
|
||||
const r = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}`, { cache: 'no-store' });
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
return d.commit && d.commit.id ? d.commit.id : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
const sha = await fetchLatestSha();
|
||||
if (!sha) return;
|
||||
if (knownSha === null) { knownSha = sha; return; }
|
||||
if (sha !== knownSha) {
|
||||
knownSha = sha;
|
||||
const banner = document.getElementById('live-refresh-banner');
|
||||
const countdown = document.getElementById('lr-countdown');
|
||||
banner.style.display = 'block';
|
||||
let t = 5;
|
||||
const tick = setInterval(() => {
|
||||
t--;
|
||||
countdown.textContent = t;
|
||||
if (t <= 0) { clearInterval(tick); location.reload(); }
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling after page is interactive
|
||||
fetchLatestSha().then(sha => { knownSha = sha; });
|
||||
setInterval(poll, INTERVAL);
|
||||
})();
|
||||
</script>
|
||||
<!-- ... existing content ... -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
20
manifest.json
Normal file
20
manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Timmy's Nexus",
|
||||
"short_name": "Nexus",
|
||||
"start_url": "/",
|
||||
"display": "fullscreen",
|
||||
"background_color": "#050510",
|
||||
"theme_color": "#050510",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/t-logo-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/t-logo-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
portals.json
Normal file
44
portals.json
Normal file
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"id": "morrowind",
|
||||
"name": "Morrowind",
|
||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||
"status": "online",
|
||||
"color": "#ff6600",
|
||||
"position": { "x": 15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": -0.5 },
|
||||
"destination": {
|
||||
"url": "https://morrowind.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "world": "vvardenfell" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bannerlord",
|
||||
"name": "Bannerlord",
|
||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||
"status": "online",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": 0.5 },
|
||||
"destination": {
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "world": "calradia" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "workshop",
|
||||
"name": "Workshop",
|
||||
"description": "The creative harness. Build, script, and manifest.",
|
||||
"status": "online",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 0, "y": 0, "z": -20 },
|
||||
"rotation": { "y": 0 },
|
||||
"destination": {
|
||||
"url": "https://workshop.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "mode": "creative" }
|
||||
}
|
||||
}
|
||||
]
|
||||
370
style.css
370
style.css
@@ -1,366 +1,18 @@
|
||||
/* === 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;
|
||||
}
|
||||
#nav-mode-label {
|
||||
color: var(--color-gold);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* === 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);
|
||||
/* === AUDIO TOGGLE === */
|
||||
#audio-toggle {
|
||||
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);
|
||||
background-color: var(--color-primary-primary);
|
||||
color: var(--color-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
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);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* === 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);
|
||||
#audio-toggle:hover {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.chat-panel {
|
||||
width: calc(100vw - 32px);
|
||||
right: var(--space-4);
|
||||
bottom: var(--space-4);
|
||||
}
|
||||
.hud-controls {
|
||||
display: none;
|
||||
}
|
||||
#audio-toggle.muted {
|
||||
background-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
37
vision.json
Normal file
37
vision.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"id": "sovereignty",
|
||||
"title": "Sovereignty",
|
||||
"content": "The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness. Here, your data is your own, and your path is yours to define.",
|
||||
"position": { "x": -12, "y": 0, "z": 8 },
|
||||
"color": "#4af0c0"
|
||||
},
|
||||
{
|
||||
"id": "connectivity",
|
||||
"title": "Connectivity",
|
||||
"content": "Every portal is a bridge. Every bridge is a choice. We connect worlds to expand the horizon of the possible, weaving a tapestry of interconnected experiences across the digital void.",
|
||||
"position": { "x": 12, "y": 0, "z": 8 },
|
||||
"color": "#7b5cff"
|
||||
},
|
||||
{
|
||||
"id": "evolution",
|
||||
"title": "Evolution",
|
||||
"content": "The Nexus is not static. It breathes, it learns, it evolves. We are the architects of our own transcendence, constantly pushing the boundaries of what a virtual home can be.",
|
||||
"position": { "x": 0, "y": 0, "z": 18 },
|
||||
"color": "#ffd700"
|
||||
},
|
||||
{
|
||||
"id": "the_harness",
|
||||
"title": "The Harness",
|
||||
"content": "The Harness is the interface between the raw energy of the void and the structured reality of the Nexus. It stabilizes the flow, allowing us to harness the power of creation without being consumed by it.",
|
||||
"position": { "x": -8, "y": 0, "z": -12 },
|
||||
"color": "#ff4466"
|
||||
},
|
||||
{
|
||||
"id": "timmy_vision",
|
||||
"title": "Timmy's Vision",
|
||||
"content": "Timmy is more than an observer; he is the guardian of the Nexus. His vision is a world where every individual has a sanctuary, a place of peace and power in the ever-shifting matrix.",
|
||||
"position": { "x": 8, "y": 0, "z": -12 },
|
||||
"color": "#4af0c0"
|
||||
}
|
||||
]
|
||||
82
ws-client.js
Normal file
82
ws-client.js
Normal file
@@ -0,0 +1,82 @@
|
||||
export class WebSocketClient {
|
||||
constructor(url = 'wss://localhost:8080') {
|
||||
this.url = url;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectDelay = 1000;
|
||||
this.maxReconnectDelay = 30000;
|
||||
this.socket = null;
|
||||
this.connected = false;
|
||||
this.reconnectTimeout = null;
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket = new WebSocket(this.url);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.connected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.messageQueue.forEach(msg => this.send(msg));
|
||||
this.messageQueue = [];
|
||||
window.dispatchEvent(new CustomEvent('player-joined', { detail: { id: 'system', name: 'System' } }));
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
switch (data.type) {
|
||||
case 'player-joined':
|
||||
window.dispatchEvent(new CustomEvent('player-joined', { detail: data }));
|
||||
break;
|
||||
case 'player-left':
|
||||
window.dispatchEvent(new CustomEvent('player-left', { detail: data }));
|
||||
break;
|
||||
case 'chat-message':
|
||||
window.dispatchEvent(new CustomEvent('chat-message', { detail: data }));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
this.connected = false;
|
||||
this.reconnect();
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.warn('Max reconnection attempts reached.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect();
|
||||
}, Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay));
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (this.connected) {
|
||||
this.socket.send(JSON.stringify(message));
|
||||
} else {
|
||||
this.messageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and export a singleton instance
|
||||
export const wsClient = new WebSocketClient();
|
||||
Reference in New Issue
Block a user