diff --git a/app.js b/app.js index 60689a0..04f95a5 100644 --- a/app.js +++ b/app.js @@ -34,6 +34,8 @@ let debugOverlay; let frameCount = 0, lastFPSTime = 0, fps = 0; let chatOpen = true; let loadProgress = 0; +let memoryNodes = []; +let sovereigntyGroup, sovereigntyPulse, sovereigntyPulseT = 0; // ═══ INIT ═══ function init() { @@ -77,6 +79,8 @@ function init() { createDustParticles(); updateLoad(85); createAmbientStructures(); + createMemoryGraph(); + createSovereigntyLoop(); updateLoad(90); // Post-processing @@ -787,6 +791,214 @@ function createAmbientStructures() { scene.add(pedestal); } +// ═══ MEMORY GRAPH ═══ +function createMemoryGraph() { + const group = new THREE.Group(); + group.position.set(-15, 0, -10); + group.name = 'memory-graph'; + scene.add(group); + + // Memory type color map + const typeColors = { + user: 0x4af0c0, + feedback: 0xff8844, + project: 0x7b5cff, + reference: 0xffd700, + }; + + // Node definitions: label, type, local position + const nodeDefs = [ + { label: 'role', type: 'user', pos: [-2.5, 4, 0.5] }, + { label: 'prefs', type: 'user', pos: [-1, 5.5, -1 ] }, + { label: 'expertise', type: 'user', pos: [-3, 3, -1.5] }, + { label: 'corrections', type: 'feedback', pos: [ 1.5, 4, -0.5] }, + { label: 'confirmed', type: 'feedback', pos: [ 2.5, 5.5, 1 ] }, + { label: 'milestone', type: 'project', pos: [ 0.5, 2.5, -2 ] }, + { label: 'decisions', type: 'project', pos: [-1, 2, 1 ] }, + { label: 'gitea', type: 'reference', pos: [ 2, 2.8, 2 ] }, + { label: 'grafana', type: 'reference', pos: [-2, 5, 2 ] }, + ]; + + // Edges (index pairs) + const edges = [ + [0, 1], [0, 2], [1, 2], + [3, 4], + [5, 6], + [7, 8], + [0, 3], [2, 6], [4, 5], [6, 7], + ]; + + // Create node spheres + nodeDefs.forEach((def, i) => { + const color = typeColors[def.type]; + const geo = new THREE.SphereGeometry(0.22, 16, 16); + const mat = new THREE.MeshStandardMaterial({ + color, + emissive: color, + emissiveIntensity: 1.2, + roughness: 0.2, + metalness: 0.5, + }); + const sphere = new THREE.Mesh(geo, mat); + sphere.position.set(...def.pos); + sphere.userData = { baseY: def.pos[1], phase: i * 0.7 }; + sphere.name = 'memnode_' + i; + group.add(sphere); + + // Halo ring + const haloGeo = new THREE.TorusGeometry(0.32, 0.02, 8, 32); + const haloMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 }); + const halo = new THREE.Mesh(haloGeo, haloMat); + halo.rotation.x = Math.PI / 2; + sphere.add(halo); + + // Canvas label + const lc = document.createElement('canvas'); + lc.width = 256; lc.height = 48; + const lctx = lc.getContext('2d'); + lctx.font = 'bold 22px "JetBrains Mono", monospace'; + lctx.fillStyle = '#' + new THREE.Color(color).getHexString(); + lctx.textAlign = 'center'; + lctx.fillText(def.label, 128, 32); + const ltex = new THREE.CanvasTexture(lc); + const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide, depthWrite: false }); + const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(1.0, 0.2), lmat); + lmesh.position.y = 0.42; + sphere.add(lmesh); + + memoryNodes.push(sphere); + }); + + // Create edge lines + edges.forEach(([a, b]) => { + const posA = new THREE.Vector3(...nodeDefs[a].pos); + const posB = new THREE.Vector3(...nodeDefs[b].pos); + const points = [posA, posB]; + const lineGeo = new THREE.BufferGeometry().setFromPoints(points); + const lineMat = new THREE.LineBasicMaterial({ + color: 0x334466, + transparent: true, + opacity: 0.4, + }); + group.add(new THREE.Line(lineGeo, lineMat)); + }); + + // Section label + const lc = document.createElement('canvas'); + lc.width = 512; lc.height = 56; + const lctx = lc.getContext('2d'); + lctx.font = 'bold 28px "Orbitron", sans-serif'; + lctx.fillStyle = '#4af0c0'; + lctx.textAlign = 'center'; + lctx.fillText('◈ MEMORY BANKS', 256, 38); + const ltex = new THREE.CanvasTexture(lc); + const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide }); + const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.45), lmat); + lmesh.position.set(0, 7, 0); + group.add(lmesh); +} + +// ═══ SOVEREIGNTY LOOP ═══ +function createSovereigntyLoop() { + sovereigntyGroup = new THREE.Group(); + sovereigntyGroup.position.set(0, 8, -22); + sovereigntyGroup.name = 'sovereignty-loop'; + scene.add(sovereigntyGroup); + + const stages = [ + { label: 'EXPORT', color: 0x4af0c0, angle: 0 }, + { label: 'COMPRESS', color: 0x7b5cff, angle: Math.PI / 2 }, + { label: 'TRAIN', color: 0xff8844, angle: Math.PI }, + { label: 'EVAL', color: 0xffd700, angle: Math.PI * 3 / 2 }, + ]; + + const ringRadius = 3.5; + + stages.forEach((s, i) => { + const x = Math.cos(s.angle) * ringRadius; + const z = Math.sin(s.angle) * ringRadius; + + // Stage sphere + const geo = new THREE.SphereGeometry(0.38, 20, 20); + const mat = new THREE.MeshStandardMaterial({ + color: s.color, + emissive: s.color, + emissiveIntensity: 1.5, + roughness: 0.1, + metalness: 0.7, + }); + const sphere = new THREE.Mesh(geo, mat); + sphere.position.set(x, 0, z); + sphere.name = 'stage_' + i; + sovereigntyGroup.add(sphere); + + // Stage label + const lc = document.createElement('canvas'); + lc.width = 256; lc.height = 48; + const lctx = lc.getContext('2d'); + lctx.font = 'bold 24px "Orbitron", sans-serif'; + lctx.fillStyle = '#' + new THREE.Color(s.color).getHexString(); + lctx.textAlign = 'center'; + lctx.fillText(s.label, 128, 32); + const ltex = new THREE.CanvasTexture(lc); + const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide, depthWrite: false }); + const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 0.26), lmat); + lmesh.position.set(x, 0.7, z); + sovereigntyGroup.add(lmesh); + + // Arc segment to next stage + const nextAngle = stages[(i + 1) % stages.length].angle; + const pts = []; + const steps = 24; + for (let k = 0; k <= steps; k++) { + const a = s.angle + (nextAngle - s.angle + (i === 3 ? Math.PI * 2 : 0)) * (k / steps); + pts.push(new THREE.Vector3(Math.cos(a) * ringRadius, 0, Math.sin(a) * ringRadius)); + } + const arcGeo = new THREE.BufferGeometry().setFromPoints(pts); + const arcMat = new THREE.LineBasicMaterial({ color: s.color, transparent: true, opacity: 0.35 }); + sovereigntyGroup.add(new THREE.Line(arcGeo, arcMat)); + }); + + // Central soul sphere + const soulGeo = new THREE.IcosahedronGeometry(0.55, 2); + const soulMat = new THREE.MeshPhysicalMaterial({ + color: 0xffffff, + emissive: 0x4af0c0, + emissiveIntensity: 2, + roughness: 0, + metalness: 1, + transmission: 0.4, + }); + const soul = new THREE.Mesh(soulGeo, soulMat); + soul.name = 'soul-core'; + sovereigntyGroup.add(soul); + + // Traveling pulse sphere + const pulseGeo = new THREE.SphereGeometry(0.15, 12, 12); + const pulseMat = new THREE.MeshStandardMaterial({ + color: 0x4af0c0, + emissive: 0x4af0c0, + emissiveIntensity: 3, + roughness: 0, + }); + sovereigntyPulse = new THREE.Mesh(pulseGeo, pulseMat); + sovereigntyGroup.add(sovereigntyPulse); + + // Section label + const lc = document.createElement('canvas'); + lc.width = 640; lc.height = 56; + const lctx = lc.getContext('2d'); + lctx.font = 'bold 28px "Orbitron", sans-serif'; + lctx.fillStyle = '#ffd700'; + lctx.textAlign = 'center'; + lctx.fillText('◈ SOVEREIGNTY LOOP', 320, 38); + const ltex = new THREE.CanvasTexture(lc); + const lmat = new THREE.MeshBasicMaterial({ map: ltex, transparent: true, side: THREE.DoubleSide }); + const lmesh = new THREE.Mesh(new THREE.PlaneGeometry(5, 0.45), lmat); + lmesh.position.set(0, 2.2, 0); + sovereigntyGroup.add(lmesh); +} + // ═══ CONTROLS ═══ function setupControls() { document.addEventListener('keydown', (e) => { @@ -941,6 +1153,47 @@ function gameLoop() { } } + // Animate memory nodes (float & pulse) + memoryNodes.forEach((node, i) => { + node.position.y = node.userData.baseY + Math.sin(elapsed * 0.9 + node.userData.phase) * 0.25; + node.material.emissiveIntensity = 1.0 + Math.sin(elapsed * 1.5 + node.userData.phase) * 0.4; + }); + + // Animate sovereignty loop + if (sovereigntyGroup) { + sovereigntyGroup.rotation.y = elapsed * 0.12; + + // Soul core pulse + const soul = scene.getObjectByName('soul-core'); + if (soul) { + soul.rotation.y = elapsed * 0.8; + soul.rotation.x = elapsed * 0.4; + soul.material.emissiveIntensity = 1.5 + Math.sin(elapsed * 2.5) * 0.5; + } + + // Traveling pulse around the ring + if (sovereigntyPulse) { + sovereigntyPulseT = (elapsed * 0.4) % 1; // full lap period ~2.5s + const stageAngles = [0, Math.PI / 2, Math.PI, Math.PI * 3 / 2]; + const stageColors = [0x4af0c0, 0x7b5cff, 0xff8844, 0xffd700]; + const seg = sovereigntyPulseT * 4; + const segIdx = Math.floor(seg) % 4; + const segT = seg - Math.floor(seg); + const aFrom = stageAngles[segIdx]; + const aTo = stageAngles[(segIdx + 1) % 4]; + // handle wrap-around from EVAL (270°) back to EXPORT (0°/360°) + let diff = aTo - aFrom; + if (diff < 0) diff += Math.PI * 2; + const angle = aFrom + diff * segT; + const ringRadius = 3.5; + sovereigntyPulse.position.set(Math.cos(angle) * ringRadius, 0, Math.sin(angle) * ringRadius); + const col = new THREE.Color(stageColors[segIdx]); + sovereigntyPulse.material.color.copy(col); + sovereigntyPulse.material.emissive.copy(col); + sovereigntyPulse.material.emissiveIntensity = 2.5 + Math.sin(elapsed * 8) * 0.5; + } + } + // Animate nexus core const core = scene.getObjectByName('nexus-core'); if (core) { diff --git a/index.html b/index.html index 3a2c6ea..e3f0852 100644 --- a/index.html +++ b/index.html @@ -95,6 +95,20 @@ + +
+
MEMORY BANKS
+
USER
+
FEEDBACK
+
PROJECT
+
REFERENCE
+
SOVEREIGNTY LOOP
+
EXPORT
+
COMPRESS
+
TRAIN
+
EVAL
+
+
WASD move   Mouse look   Enter chat diff --git a/style.css b/style.css index 519b05e..36ac63f 100644 --- a/style.css +++ b/style.css @@ -359,3 +359,42 @@ canvas#nexus-canvas { display: none; } } + +/* === MEMORY LEGEND === */ +.hud-memory-legend { + position: fixed; + top: 80px; + right: var(--space-4); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--panel-radius); + padding: var(--space-3); + backdrop-filter: blur(var(--panel-blur)); + min-width: 140px; + font-size: var(--text-xs); + font-family: var(--font-body); + color: var(--color-text-muted); +} +.legend-title { + font-family: var(--font-display); + font-size: 9px; + letter-spacing: 0.1em; + color: var(--color-primary); + margin-bottom: var(--space-1); + text-transform: uppercase; +} +.legend-row { + display: flex; + align-items: center; + gap: var(--space-2); + margin: 3px 0; + font-size: var(--text-xs); + letter-spacing: 0.05em; +} +.legend-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + box-shadow: 0 0 6px currentColor; +}