forked from Timmy_Foundation/the-nexus
Compare commits
11 Commits
gemini/iss
...
kimi/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d2d8a12bc | ||
|
|
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 ...
|
||||
|
||||
60
icons/nexus-icon.svg
Normal file
60
icons/nexus-icon.svg
Normal file
@@ -0,0 +1,60 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4af0c0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0a1628;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" fill="#0a1628"/>
|
||||
|
||||
<!-- Outer glow circle -->
|
||||
<circle cx="256" cy="256" r="200" fill="none" stroke="#4af0c0" stroke-width="2" opacity="0.3"/>
|
||||
<circle cx="256" cy="256" r="180" fill="none" stroke="#4af0c0" stroke-width="1" opacity="0.2"/>
|
||||
|
||||
<!-- Icosahedron / Crystal shape -->
|
||||
<g transform="translate(256, 256)" filter="url(#glow)">
|
||||
<!-- Main crystal body -->
|
||||
<path d="M0,-120 L103.9,-60 L103.9,60 L0,120 L-103.9,60 L-103.9,-60 Z"
|
||||
fill="none" stroke="#4af0c0" stroke-width="3" opacity="0.9"/>
|
||||
|
||||
<!-- Inner geometric lines -->
|
||||
<path d="M0,-120 L0,120 M-103.9,-60 L103.9,60 M103.9,-60 L-103.9,60"
|
||||
fill="none" stroke="#4af0c0" stroke-width="2" opacity="0.6"/>
|
||||
|
||||
<!-- Center point -->
|
||||
<circle cx="0" cy="0" r="15" fill="#4af0c0" opacity="0.8"/>
|
||||
|
||||
<!-- Top crystal point -->
|
||||
<path d="M0,-120 L0,-150 L15,-120 Z" fill="#4af0c0" opacity="0.7"/>
|
||||
<path d="M0,-120 L0,-150 L-15,-120 Z" fill="#2dd4a8" opacity="0.7"/>
|
||||
|
||||
<!-- Bottom crystal point -->
|
||||
<path d="M0,120 L0,150 L15,120 Z" fill="#4af0c0" opacity="0.7"/>
|
||||
<path d="M0,120 L0,150 L-15,120 Z" fill="#2dd4a8" opacity="0.7"/>
|
||||
|
||||
<!-- Side crystal points -->
|
||||
<path d="M103.9,-60 L130,-45 L103.9,-30 Z" fill="#4af0c0" opacity="0.6"/>
|
||||
<path d="M103.9,60 L130,45 L103.9,30 Z" fill="#4af0c0" opacity="0.6"/>
|
||||
<path d="M-103.9,-60 L-130,-45 L-103.9,-30 Z" fill="#4af0c0" opacity="0.6"/>
|
||||
<path d="M-103.9,60 L-130,45 L-103.9,30 Z" fill="#4af0c0" opacity="0.6"/>
|
||||
</g>
|
||||
|
||||
<!-- Small orbiting particles -->
|
||||
<circle cx="380" cy="150" r="6" fill="#4af0c0" opacity="0.8">
|
||||
<animate attributeName="opacity" values="0.8;0.3;0.8" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="130" cy="380" r="4" fill="#4af0c0" opacity="0.6">
|
||||
<animate attributeName="opacity" values="0.6;0.2;0.6" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="400" cy="320" r="5" fill="#4af0c0" opacity="0.7">
|
||||
<animate attributeName="opacity" values="0.7;0.3;0.7" dur="2.5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
32
icons/nexus-maskable.svg
Normal file
32
icons/nexus-maskable.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#0d1f35;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0a1628;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Background with safe zone for maskable icons -->
|
||||
<rect width="512" height="512" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Main icon content within safe zone (center 70% = ~358px) -->
|
||||
<g transform="translate(256, 256)">
|
||||
<!-- Icosahedron outline -->
|
||||
<path d="M0,-100 L86.6,-50 L86.6,50 L0,100 L-86.6,50 L-86.6,-50 Z"
|
||||
fill="none" stroke="#4af0c0" stroke-width="4"/>
|
||||
|
||||
<!-- Inner star pattern -->
|
||||
<path d="M0,-100 L0,100 M-86.6,-50 L86.6,50 M86.6,-50 L-86.6,50"
|
||||
fill="none" stroke="#4af0c0" stroke-width="3" opacity="0.7"/>
|
||||
|
||||
<!-- Center crystal -->
|
||||
<circle cx="0" cy="0" r="20" fill="#4af0c0"/>
|
||||
|
||||
<!-- Corner accents -->
|
||||
<circle cx="0" cy="-100" r="8" fill="#4af0c0"/>
|
||||
<circle cx="86.6" cy="-50" r="8" fill="#4af0c0"/>
|
||||
<circle cx="86.6" cy="50" r="8" fill="#4af0c0"/>
|
||||
<circle cx="0" cy="100" r="8" fill="#4af0c0"/>
|
||||
<circle cx="-86.6" cy="50" r="8" fill="#4af0c0"/>
|
||||
<circle cx="-86.6" cy="-50" r="8" fill="#4af0c0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
257
index.html
257
index.html
@@ -1,174 +1,101 @@
|
||||
<!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">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<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">
|
||||
|
||||
<!-- Twitter -->
|
||||
<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">
|
||||
|
||||
<!-- PWA: Web App Manifest -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
<!-- PWA: Theme Color -->
|
||||
<meta name="theme-color" content="#4af0c0">
|
||||
<meta name="background-color" content="#0a1628">
|
||||
|
||||
<!-- PWA: Apple iOS Support -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="The Nexus">
|
||||
<link rel="apple-touch-icon" href="/icons/nexus-icon.svg">
|
||||
|
||||
<!-- PWA: Microsoft Windows -->
|
||||
<meta name="msapplication-TileColor" content="#0a1628">
|
||||
<meta name="msapplication-config" content="none">
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</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 ... -->
|
||||
|
||||
<!-- Application Script -->
|
||||
<script src="app.js"></script>
|
||||
|
||||
<!-- PWA: Service Worker Registration -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Only register service worker in production (not in development with file://)
|
||||
if ('serviceWorker' in navigator && window.location.protocol === 'https:' || window.location.hostname === 'localhost') {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(function(registration) {
|
||||
console.log('[Nexus] Service Worker registered:', registration.scope);
|
||||
|
||||
// Handle updates
|
||||
registration.addEventListener('updatefound', function() {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', function() {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New version available
|
||||
console.log('[Nexus] New version available, refreshing...');
|
||||
// Optionally show update notification to user
|
||||
if (confirm('A new version of The Nexus is available. Refresh to update?')) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log('[Nexus] Service Worker registration failed:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for messages from service worker
|
||||
navigator.serviceWorker.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'OFFLINE_READY') {
|
||||
console.log('[Nexus] App is ready for offline use');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[Nexus] Service Worker not supported or not in secure context');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
61
manifest.json
Normal file
61
manifest.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "The Nexus",
|
||||
"short_name": "Nexus",
|
||||
"description": "Timmy's sovereign 3D world — a Three.js environment serving as the central hub for all portals",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["fullscreen", "minimal-ui"],
|
||||
"orientation": "any",
|
||||
"background_color": "#0a1628",
|
||||
"theme_color": "#4af0c0",
|
||||
"categories": ["entertainment", "games"],
|
||||
"lang": "en",
|
||||
"dir": "ltr",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/nexus-icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "icons/nexus-maskable.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "screenshots/nexus-wide.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "The Nexus 3D environment"
|
||||
},
|
||||
{
|
||||
"src": "screenshots/nexus-narrow.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow",
|
||||
"label": "The Nexus on mobile"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Enter Nexus",
|
||||
"short_name": "Enter",
|
||||
"description": "Jump directly into the Nexus world",
|
||||
"url": "/?action=enter",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/nexus-icon.svg",
|
||||
"sizes": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"related_applications": [],
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
198
offline.html
Normal file
198
offline.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Offline — The Nexus</title>
|
||||
<meta name="description" content="The Nexus is currently offline">
|
||||
<style>
|
||||
:root {
|
||||
--color-bg: #0a1628;
|
||||
--color-bg-secondary: #0d1f35;
|
||||
--color-primary: #4af0c0;
|
||||
--color-primary-dim: #2dd4a8;
|
||||
--color-text: #e6f1ff;
|
||||
--color-text-muted: #8b9bb4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: radial-gradient(ellipse at center, var(--color-bg-secondary) 0%, var(--color-bg) 70%);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 2rem;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.7; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.nexus-title {
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
border: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.status::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: blink 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--color-primary-dim);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Crystal animation */
|
||||
.crystal {
|
||||
fill: none;
|
||||
stroke: var(--color-primary);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.crystal-center {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<svg class="icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Crystal icosahedron representation -->
|
||||
<path class="crystal" d="M50,15 L84.6,42.5 L84.6,72.5 L50,85 L15.4,72.5 L15.4,42.5 Z" opacity="0.8"/>
|
||||
<path class="crystal" d="M50,15 L50,85 M15.4,42.5 L84.6,72.5 M84.6,42.5 L15.4,72.5" opacity="0.5"/>
|
||||
<circle class="crystal-center" cx="50" cy="55" r="8" opacity="0.9"/>
|
||||
<!-- Offline indicator -->
|
||||
<circle cx="75" cy="25" r="12" fill="#ff6b6b" opacity="0.9"/>
|
||||
<path d="M69,25 L81,25" stroke="white" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
<h1>The <span class="nexus-title">Nexus</span> is Dormant</h1>
|
||||
|
||||
<div class="status">
|
||||
<span>You're offline</span>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The crystalline pathways cannot form without a connection to the sovereign network.
|
||||
Check your connection and try again to enter the 3D realm.
|
||||
</p>
|
||||
|
||||
<button class="btn" onclick="window.location.reload()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
Reconnect
|
||||
</button>
|
||||
|
||||
<p class="hint">
|
||||
Core assets are cached for offline use. Some features may be limited without connectivity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-retry when connection comes back
|
||||
window.addEventListener('online', () => {
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
// Check if we're actually back online
|
||||
setInterval(() => {
|
||||
if (navigator.onLine) {
|
||||
fetch('/', { method: 'HEAD', cache: 'no-store' })
|
||||
.then(() => {
|
||||
window.location.href = '/';
|
||||
})
|
||||
.catch(() => {
|
||||
// Still unreachable, stay on offline page
|
||||
});
|
||||
}
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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);
|
||||
}
|
||||
|
||||
218
sw.js
Normal file
218
sw.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* The Nexus Service Worker
|
||||
* Provides offline capability and home screen install support
|
||||
* Strategy: Cache-first for local assets, stale-while-revalidate for CDN
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = 'nexus-v1';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const CDN_CACHE = `${CACHE_VERSION}-cdn`;
|
||||
const OFFLINE_PAGE = '/offline.html';
|
||||
|
||||
// Core local assets that must be cached
|
||||
const CORE_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/style.css',
|
||||
'/app.js',
|
||||
'/manifest.json',
|
||||
'/icons/nexus-icon.svg',
|
||||
'/icons/nexus-maskable.svg',
|
||||
OFFLINE_PAGE
|
||||
];
|
||||
|
||||
// CDN resources that benefit from caching but can be stale
|
||||
const CDN_PATTERNS = [
|
||||
/^https:\/\/unpkg\.com/,
|
||||
/^https:\/\/cdn\.jsdelivr\.net/,
|
||||
/^https:\/\/fonts\.googleapis\.com/,
|
||||
/^https:\/\/fonts\.gstatic\.com/,
|
||||
/^https:\/\/cdn\.threejs\.org/
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a URL matches any CDN pattern
|
||||
*/
|
||||
function isCdnResource(url) {
|
||||
return CDN_PATTERNS.some(pattern => pattern.test(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install event - cache core assets
|
||||
*/
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[Nexus SW] Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then(cache => {
|
||||
console.log('[Nexus SW] Caching core assets');
|
||||
return cache.addAll(CORE_ASSETS);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[Nexus SW] Core assets cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[Nexus SW] Cache failed:', err);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Activate event - clean up old caches
|
||||
*/
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[Nexus SW] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(name => name.startsWith('nexus-') && name !== STATIC_CACHE && name !== CDN_CACHE)
|
||||
.map(name => {
|
||||
console.log('[Nexus SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[Nexus SW] Activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch event - handle requests with appropriate strategy
|
||||
*/
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip cross-origin requests that aren't CDN resources
|
||||
if (url.origin !== self.location.origin && !isCdnResource(url.href)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle CDN resources with stale-while-revalidate
|
||||
if (isCdnResource(url.href)) {
|
||||
event.respondWith(handleCdnRequest(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle local assets with cache-first strategy
|
||||
event.respondWith(handleLocalRequest(request));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cache-first strategy for local assets
|
||||
* Fastest response, updates cache in background
|
||||
*/
|
||||
async function handleLocalRequest(request) {
|
||||
try {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
// Return cached version immediately if available
|
||||
if (cached) {
|
||||
// Revalidate in background for next time
|
||||
fetch(request)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Network failed, cached version is already being used
|
||||
});
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Not in cache - fetch from network
|
||||
const response = await fetch(request);
|
||||
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[Nexus SW] Local request failed:', error);
|
||||
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
const cache = await caches.open(STATIC_CACHE);
|
||||
const offlinePage = await cache.match(OFFLINE_PAGE);
|
||||
if (offlinePage) {
|
||||
return offlinePage;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stale-while-revalidate strategy for CDN resources
|
||||
* Serves stale content while updating in background
|
||||
*/
|
||||
async function handleCdnRequest(request) {
|
||||
const cache = await caches.open(CDN_CACHE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
// Always try to fetch fresh version
|
||||
const fetchPromise = fetch(request)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[Nexus SW] CDN fetch failed:', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Return cached version immediately if available, otherwise wait for network
|
||||
if (cached) {
|
||||
// Return stale but revalidate for next time
|
||||
fetchPromise.catch(() => {}); // Swallow errors, we have cached version
|
||||
return cached;
|
||||
}
|
||||
|
||||
// No cache - must wait for network
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message handler for runtime cache updates
|
||||
*/
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'GET_VERSION') {
|
||||
event.ports[0].postMessage({ version: CACHE_VERSION });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Background sync for deferred actions (future enhancement)
|
||||
*/
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'nexus-sync') {
|
||||
console.log('[Nexus SW] Background sync triggered');
|
||||
// Future: sync chat messages, state updates, etc.
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Nexus SW] Service worker loaded');
|
||||
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