Compare commits

...

10 Commits

Author SHA1 Message Date
Alexander Whitestone
e564bac96e feat: Add WebSocket stub for future live multiplayer (#100)
Fixes #100
Agent: groq (groq/qwen/qwen3-32b via aider)
2026-03-23 23:49:28 -04:00
8767f2c5d2 [groq] Create manifest.json for PWA install (#101) (#102)
Co-authored-by: Groq Agent <groq@noreply.143.198.27.163>
Co-committed-by: Groq Agent <groq@noreply.143.198.27.163>
2026-03-24 03:47:01 +00:00
4c4b77669d [groq] Add meta tags for SEO and social sharing (#74) (#76)
Co-authored-by: Groq Agent <groq@noreply.143.198.27.163>
Co-committed-by: Groq Agent <groq@noreply.143.198.27.163>
2026-03-24 03:40:32 +00:00
b40b7d9c6c [groq] Add ambient sound toggle for the Nexus (#54) (#60)
Co-authored-by: Groq Agent <groq@noreply.143.198.27.163>
Co-committed-by: Groq Agent <groq@noreply.143.198.27.163>
2026-03-24 03:36:14 +00:00
db354e84f2 [claude] NIP-07 visitor identity in the workshop (#12) (#49)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-24 03:27:35 +00:00
a377da05de [claude] Agent idle behaviors in 3D world (#8) (#48) 2026-03-24 03:25:23 +00:00
75c9a3774b Nexus Autonomy: Agent Autonomy & Power Dynamics (#46)
Co-authored-by: Google Gemini <gemini@hermes.local>
Co-committed-by: Google Gemini <gemini@hermes.local>
2026-03-24 03:07:01 +00:00
96663e1500 Nexus Evolution: Agent Presence & Sovereign Thought Stream (#45)
Co-authored-by: Google Gemini <gemini@hermes.local>
Co-committed-by: Google Gemini <gemini@hermes.local>
2026-03-24 02:38:34 +00:00
58038f2e41 Nexus Refinement: Vision Points & Narrative Expansion (#44)
Co-authored-by: Google Gemini <gemini@hermes.local>
Co-committed-by: Google Gemini <gemini@hermes.local>
2026-03-24 02:36:12 +00:00
d0edfe8725 Feature: Portal system — entry points to other worlds (#5) (#43)
Co-authored-by: Google Gemini <gemini@hermes.local>
Co-committed-by: Google Gemini <gemini@hermes.local>
2026-03-24 02:29:45 +00:00
8 changed files with 237 additions and 937 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.aider*

433
app.js
View File

@@ -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 ...

View File

@@ -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 &nbsp; <span>Mouse</span> look &nbsp; <span>Enter</span> chat &nbsp;
<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
View 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
View 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
View File

@@ -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
View 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
View 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();