diff --git a/app.js b/app.js index 085ed8f..9b0bf04 100644 --- a/app.js +++ b/app.js @@ -2573,6 +2573,12 @@ function gameLoop() { updateAshStorm(delta, elapsed); + // Project Mnemosyne - Memory Orb Animation + if (typeof animateMemoryOrbs === 'function') { + animateMemoryOrbs(delta); + } + + const mode = NAV_MODES[navModeIdx]; const chatActive = document.activeElement === document.getElementById('chat-input'); @@ -2771,6 +2777,12 @@ function gameLoop() { composer.render(); updateAshStorm(delta, elapsed); + + // Project Mnemosyne - Memory Orb Animation + if (typeof animateMemoryOrbs === 'function') { + animateMemoryOrbs(delta); + } + updatePortalTunnel(delta, elapsed); if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime(); @@ -2933,6 +2945,151 @@ function updateAshStorm(delta, elapsed) { } } + +// ═══════════════════════════════════════════ +// PROJECT MNEMOSYNE — HOLOGRAPHIC MEMORY ORBS +// ═══════════════════════════════════════════ + +// Memory orbs registry for animation loop +const memoryOrbs = []; + +/** + * Spawn a glowing memory orb at the given position. + * Used to visualize RAG retrievals and memory recalls in the Nexus. + * + * @param {THREE.Vector3} position - World position for the orb + * @param {number} color - Hex color (default: 0x4af0c0 - cyan) + * @param {number} size - Radius of the orb (default: 0.5) + * @param {object} metadata - Optional metadata for the memory (source, timestamp, etc.) + * @returns {THREE.Mesh} The created orb mesh + */ +function spawnMemoryOrb(position, color = 0x4af0c0, size = 0.5, metadata = {}) { + if (typeof THREE === 'undefined' || typeof scene === 'undefined') { + console.warn('[Mnemosyne] THREE/scene not available for orb spawn'); + return null; + } + + const geometry = new THREE.SphereGeometry(size, 32, 32); + const material = new THREE.MeshStandardMaterial({ + color: color, + emissive: color, + emissiveIntensity: 2.5, + metalness: 0.3, + roughness: 0.2, + transparent: true, + opacity: 0.85, + envMapIntensity: 1.5 + }); + + const orb = new THREE.Mesh(geometry, material); + orb.position.copy(position); + orb.castShadow = true; + orb.receiveShadow = true; + + orb.userData = { + type: 'memory_orb', + pulse: Math.random() * Math.PI * 2, // Random phase offset + pulseSpeed: 0.002 + Math.random() * 0.001, + originalScale: size, + metadata: metadata, + createdAt: Date.now() + }; + + // Point light for local illumination + const light = new THREE.PointLight(color, 1.5, 8); + orb.add(light); + + scene.add(orb); + memoryOrbs.push(orb); + + console.info('[Mnemosyne] Memory orb spawned:', metadata.source || 'unknown'); + return orb; +} + +/** + * Remove a memory orb from the scene and dispose resources. + * @param {THREE.Mesh} orb - The orb to remove + */ +function removeMemoryOrb(orb) { + if (!orb) return; + + if (orb.parent) orb.parent.remove(orb); + if (orb.geometry) orb.geometry.dispose(); + if (orb.material) orb.material.dispose(); + + const idx = memoryOrbs.indexOf(orb); + if (idx > -1) memoryOrbs.splice(idx, 1); +} + +/** + * Animate all memory orbs — pulse, rotate, and fade. + * Called from gameLoop() every frame. + * @param {number} delta - Time since last frame + */ +function animateMemoryOrbs(delta) { + for (let i = memoryOrbs.length - 1; i >= 0; i--) { + const orb = memoryOrbs[i]; + if (!orb || !orb.userData) continue; + + // Pulse animation + orb.userData.pulse += orb.userData.pulseSpeed * delta * 1000; + const pulseFactor = 1 + Math.sin(orb.userData.pulse) * 0.1; + orb.scale.setScalar(pulseFactor * orb.userData.originalScale); + + // Gentle rotation + orb.rotation.y += delta * 0.5; + + // Fade after 30 seconds + const age = (Date.now() - orb.userData.createdAt) / 1000; + if (age > 30) { + const fadeDuration = 10; + const fadeProgress = Math.min(1, (age - 30) / fadeDuration); + orb.material.opacity = 0.85 * (1 - fadeProgress); + + if (fadeProgress >= 1) { + removeMemoryOrb(orb); + i--; // Adjust index after removal + } + } + } +} + +/** + * Spawn memory orbs arranged in a spiral for RAG retrieval results. + * @param {Array} results - Array of {content, score, source} + * @param {THREE.Vector3} center - Center position (default: above avatar) + */ +function spawnRetrievalOrbs(results, center) { + if (!results || !Array.isArray(results) || results.length === 0) return; + + if (!center) { + center = new THREE.Vector3(0, 2, 0); + } + + const colors = [0x4af0c0, 0x7b5cff, 0xffd700, 0xff4466, 0x00ff88]; + const radius = 3; + + results.forEach((result, i) => { + const angle = (i / results.length) * Math.PI * 2; + const height = (i / results.length) * 2 - 1; + + const position = new THREE.Vector3( + center.x + Math.cos(angle) * radius, + center.y + height, + center.z + Math.sin(angle) * radius + ); + + const colorIdx = Math.min(colors.length - 1, Math.floor((result.score || 0.5) * colors.length)); + const size = 0.3 + (result.score || 0.5) * 0.4; + + spawnMemoryOrb(position, colors[colorIdx], size, { + source: result.source || 'unknown', + score: result.score || 0, + contentPreview: (result.content || '').substring(0, 100) + }); + }); +} + init().then(() => { createAshStorm(); createPortalTunnel();