Compare commits
4 Commits
feat/mnemo
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cfbef970e | |||
| b1f5b1b859 | |||
| 6f949698fe | |||
| 6cf1f4d078 |
580
app.js
580
app.js
@@ -41,7 +41,6 @@ let harnessPulseMesh;
|
||||
let powerMeterBars = [];
|
||||
let particles, dustParticles;
|
||||
let debugOverlay;
|
||||
let spatialSchema; // Project Mnemosyne — Spatial Memory Schema
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
let chatOpen = true;
|
||||
let loadProgress = 0;
|
||||
@@ -593,192 +592,6 @@ let pseLayer;
|
||||
let metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||||
let agentFSMs = {};
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// PROJECT MNEMOSYNE — SPATIAL MEMORY SCHEMA
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* SpatialMemorySchema — Maps memory categories to persistent 3D world regions.
|
||||
* Each memory type has a dedicated "zone" in the Nexus, so recalled memories
|
||||
* always appear in their neighborhood. Zones persist via localStorage.
|
||||
*/
|
||||
class SpatialMemorySchema {
|
||||
constructor() {
|
||||
this.zones = new Map();
|
||||
this.zoneVisuals = [];
|
||||
this.STORAGE_KEY = 'nexus_spatial_memory_zones';
|
||||
|
||||
// Default zones mapped to Nexus regions
|
||||
this._defineDefaultZones();
|
||||
this._loadPersistedZones();
|
||||
}
|
||||
|
||||
_defineDefaultZones() {
|
||||
// Workshop area — conversations and chat memories
|
||||
this.registerZone('conversations', new THREE.Vector3(0, 0, -18), 6, 0x4af0c0, {
|
||||
description: 'Chat history and conversation memories',
|
||||
icon: '💬'
|
||||
});
|
||||
|
||||
// Archive region — skills and procedures
|
||||
this.registerZone('skills', new THREE.Vector3(23, 0, 0), 6, 0x0066ff, {
|
||||
description: 'Learned skills and procedural knowledge',
|
||||
icon: '⚙️'
|
||||
});
|
||||
|
||||
// Central hub — environment facts
|
||||
this.registerZone('facts', new THREE.Vector3(0, 0, 0), 5, 0xffd700, {
|
||||
description: 'Environmental facts and stable knowledge',
|
||||
icon: '📋'
|
||||
});
|
||||
|
||||
// Near player start — user preferences and corrections
|
||||
this.registerZone('preferences', new THREE.Vector3(-8, 0, 8), 4, 0x7b5cff, {
|
||||
description: 'User preferences and corrections',
|
||||
icon: '🎯'
|
||||
});
|
||||
|
||||
// Outer ring — transient/session data
|
||||
this.registerZone('transient', new THREE.Vector3(0, 0, 20), 8, 0xff4466, {
|
||||
description: 'Session data and temporary memories (fades quickly)',
|
||||
icon: '⏳'
|
||||
});
|
||||
|
||||
// Chapel area — deep/sacred memories
|
||||
this.registerZone('deep', new THREE.Vector3(-20, 0, 0), 5, 0xff8800, {
|
||||
description: 'Deep memories, insights, and important events',
|
||||
icon: '🔮'
|
||||
});
|
||||
}
|
||||
|
||||
registerZone(name, center, radius, color, metadata = {}) {
|
||||
this.zones.set(name, {
|
||||
name,
|
||||
center: center.clone(),
|
||||
radius,
|
||||
color,
|
||||
metadata,
|
||||
orbCount: 0,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a memory object to its zone based on type.
|
||||
* @param {object} memoryObj — { type, source, content, score, ... }
|
||||
* @returns {{ zone: string, position: THREE.Vector3, color: number }}
|
||||
*/
|
||||
assignMemory(memoryObj) {
|
||||
const type = (memoryObj.type || 'facts').toLowerCase();
|
||||
const zone = this.zones.get(type) || this.zones.get('facts');
|
||||
zone.orbCount++;
|
||||
|
||||
// Jittered position within zone — spread orbs so they don't stack
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const dist = Math.random() * zone.radius * 0.8;
|
||||
const position = new THREE.Vector3(
|
||||
zone.center.x + Math.cos(angle) * dist,
|
||||
1.5 + Math.random() * 2,
|
||||
zone.center.z + Math.sin(angle) * dist
|
||||
);
|
||||
|
||||
return { zone: zone.name, position, color: zone.color };
|
||||
}
|
||||
|
||||
getZonePosition(type) {
|
||||
const zone = this.zones.get(type) || this.zones.get('facts');
|
||||
return zone.center.clone();
|
||||
}
|
||||
|
||||
listZones() {
|
||||
const result = [];
|
||||
this.zones.forEach((z, name) => {
|
||||
result.push({
|
||||
name,
|
||||
center: { x: z.center.x, y: z.center.y, z: z.center.z },
|
||||
radius: z.radius,
|
||||
color: '#' + z.color.toString(16).padStart(6, '0'),
|
||||
orbCount: z.orbCount,
|
||||
description: z.metadata.description
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw subtle ground rings at each zone boundary.
|
||||
* Call once after scene is initialized.
|
||||
*/
|
||||
visualizeZones() {
|
||||
if (typeof scene === 'undefined') return;
|
||||
|
||||
this.zones.forEach((zone, name) => {
|
||||
// Ground ring
|
||||
const ringGeo = new THREE.RingGeometry(zone.radius - 0.15, zone.radius + 0.15, 64);
|
||||
const ringMat = new THREE.MeshBasicMaterial({
|
||||
color: zone.color,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.rotation.x = -Math.PI / 2;
|
||||
ring.position.copy(zone.center);
|
||||
ring.position.y = 0.02;
|
||||
scene.add(ring);
|
||||
|
||||
// Inner glow disc
|
||||
const discGeo = new THREE.CircleGeometry(zone.radius, 64);
|
||||
const discMat = new THREE.MeshBasicMaterial({
|
||||
color: zone.color,
|
||||
transparent: true,
|
||||
opacity: 0.04,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const disc = new THREE.Mesh(discGeo, discMat);
|
||||
disc.rotation.x = -Math.PI / 2;
|
||||
disc.position.copy(zone.center);
|
||||
disc.position.y = 0.01;
|
||||
scene.add(disc);
|
||||
|
||||
this.zoneVisuals.push(ring, disc);
|
||||
});
|
||||
|
||||
console.info('[Mnemosyne] Zone visualization created for', this.zones.size, 'zones');
|
||||
}
|
||||
|
||||
_persistZones() {
|
||||
try {
|
||||
const data = {};
|
||||
this.zones.forEach((z, name) => {
|
||||
data[name] = {
|
||||
center: { x: z.center.x, y: z.center.y, z: z.center.z },
|
||||
radius: z.radius,
|
||||
orbCount: z.orbCount
|
||||
};
|
||||
});
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (e) { /* storage full or unavailable */ }
|
||||
}
|
||||
|
||||
_loadPersistedZones() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (!raw) return;
|
||||
const data = JSON.parse(raw);
|
||||
Object.entries(data).forEach(([name, saved]) => {
|
||||
const zone = this.zones.get(name);
|
||||
if (zone && saved.orbCount) {
|
||||
zone.orbCount = saved.orbCount;
|
||||
}
|
||||
});
|
||||
} catch (e) { /* corrupt or missing */ }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function setupGOFAI() {
|
||||
knowledgeGraph = new KnowledgeGraph();
|
||||
blackboard = new Blackboard();
|
||||
@@ -793,10 +606,6 @@ function setupGOFAI() {
|
||||
pseLayer = new PSELayer();
|
||||
calibrator = new AdaptiveCalibrator('nexus-v1', { base_rate: 0.05 });
|
||||
|
||||
// Initialize Spatial Memory Schema (Project Mnemosyne)
|
||||
spatialSchema = new SpatialMemorySchema();
|
||||
console.info('[Mnemosyne] Spatial Memory Schema initialized with', spatialSchema.zones.size, 'zones');
|
||||
|
||||
// Setup initial facts
|
||||
symbolicEngine.addFact('energy', 100);
|
||||
symbolicEngine.addFact('stability', 1.0);
|
||||
@@ -2764,6 +2573,14 @@ function gameLoop() {
|
||||
|
||||
updateAshStorm(delta, elapsed);
|
||||
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
if (typeof animateMemoryOrbs === 'function') {
|
||||
animateMemoryOrbs(delta);
|
||||
animateHolographicThreads(delta);
|
||||
animateRoomTransitions(delta);
|
||||
}
|
||||
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||
|
||||
@@ -2962,6 +2779,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();
|
||||
@@ -3124,14 +2947,379 @@ 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)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// MNEMOSYNE — SPATIAL MEMORY INTEGRATION
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// Spatial memory schema state (loaded async)
|
||||
let spatialMemorySchema = null;
|
||||
const holographicThreads = []; // Active thread meshes
|
||||
|
||||
/**
|
||||
* Load the spatial memory schema and store it for room mapping.
|
||||
* Called during init. Falls back gracefully if schema unavailable.
|
||||
*/
|
||||
async function loadSpatialMemorySchema() {
|
||||
try {
|
||||
const resp = await fetch('/spatial-memory-schema.json');
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
spatialMemorySchema = await resp.json();
|
||||
console.info('[Mnemosyne] Spatial memory schema loaded:',
|
||||
Object.keys(spatialMemorySchema.rooms).length, 'rooms');
|
||||
} catch (err) {
|
||||
console.warn('[Mnemosyne] Could not load spatial schema, using defaults:', err.message);
|
||||
spatialMemorySchema = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room definition for a memory category.
|
||||
* @param {string} category - Memory category (user_pref, project, tool, general)
|
||||
* @returns {object|null} Room definition with spatial_bounds and visual_theme
|
||||
*/
|
||||
function getRoomForCategory(category) {
|
||||
if (!spatialMemorySchema) return null;
|
||||
for (const [roomId, room] of Object.entries(spatialMemorySchema.rooms)) {
|
||||
if (room.category === category) return { id: roomId, ...room };
|
||||
}
|
||||
// Fallback to commons for unknown categories
|
||||
if (spatialMemorySchema.rooms.commons) {
|
||||
return { id: 'commons', ...spatialMemorySchema.rooms.commons };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a random position within a room's spatial bounds.
|
||||
* @param {object} room - Room definition with spatial_bounds
|
||||
* @returns {THREE.Vector3} Position within room bounds
|
||||
*/
|
||||
function getPositionInRoom(room) {
|
||||
const bounds = room.spatial_bounds;
|
||||
const dims = bounds.dimensions;
|
||||
const center = bounds.center;
|
||||
|
||||
return new THREE.Vector3(
|
||||
center[0] + (Math.random() - 0.5) * dims[0] * 0.8,
|
||||
center[1] + (Math.random() - 0.5) * dims[1] * 0.6 + 1.5, // Float above floor
|
||||
center[2] + (Math.random() - 0.5) * dims[2] * 0.8
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a categorized memory orb placed in its corresponding room.
|
||||
* Extends spawnMemoryOrb with spatial placement based on category.
|
||||
*
|
||||
* @param {string} category - Memory category (user_pref, project, tool, general)
|
||||
* @param {object} metadata - Memory metadata (source, content, score, etc.)
|
||||
* @param {number} importance - 0-1 importance score (affects size/glow)
|
||||
* @returns {THREE.Mesh} The spawned orb
|
||||
*/
|
||||
function spawnCategorizedOrb(category, metadata = {}, importance = 0.5) {
|
||||
const room = getRoomForCategory(category);
|
||||
const position = room ? getPositionInRoom(room) : new THREE.Vector3(0, 2, 0);
|
||||
|
||||
// Color from schema trust mapping or room theme
|
||||
let color = 0x4af0c0; // Default cyan
|
||||
if (room && room.visual_theme) {
|
||||
const accent = room.visual_theme.colors?.accent;
|
||||
if (accent) color = parseInt(accent.replace('#', ''), 16);
|
||||
}
|
||||
|
||||
// Size scales with importance
|
||||
const size = 0.2 + importance * 0.5;
|
||||
|
||||
const orb = spawnMemoryOrb(position, color, size, {
|
||||
...metadata,
|
||||
category: category,
|
||||
room: room ? room.id : 'unknown'
|
||||
});
|
||||
|
||||
return orb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a holographic thread connecting two memory orbs.
|
||||
* @param {THREE.Mesh} orbA - First orb
|
||||
* @param {THREE.Mesh} orbB - Second orb
|
||||
* @param {number} color - Thread color (default: 0x4af0c0)
|
||||
* @returns {THREE.Line} The thread mesh
|
||||
*/
|
||||
function drawHolographicThread(orbA, orbB, color = 0x4af0c0) {
|
||||
if (typeof THREE === 'undefined' || !orbA || !orbB) return null;
|
||||
|
||||
const points = [orbA.position.clone(), orbB.position.clone()];
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: color,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
linewidth: 1
|
||||
});
|
||||
|
||||
const thread = new THREE.Line(geometry, material);
|
||||
thread.userData = {
|
||||
type: 'holographic_thread',
|
||||
orbA: orbA,
|
||||
orbB: orbB,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
scene.add(thread);
|
||||
holographicThreads.push(thread);
|
||||
return thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update holographic threads to follow their connected orbs.
|
||||
* Called from the animation loop.
|
||||
* @param {number} delta - Time since last frame
|
||||
*/
|
||||
function animateHolographicThreads(delta) {
|
||||
for (let i = holographicThreads.length - 1; i >= 0; i--) {
|
||||
const thread = holographicThreads[i];
|
||||
if (!thread || !thread.userData) continue;
|
||||
|
||||
const { orbA, orbB } = thread.userData;
|
||||
|
||||
// Remove thread if either orb is gone
|
||||
if (!orbA || !orbA.parent || !orbB || !orbB.parent) {
|
||||
scene.remove(thread);
|
||||
thread.geometry.dispose();
|
||||
thread.material.dispose();
|
||||
holographicThreads.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update line positions to follow orbs
|
||||
const positions = thread.geometry.attributes.position;
|
||||
positions.setXYZ(0, orbA.position.x, orbA.position.y, orbA.position.z);
|
||||
positions.setXYZ(1, orbB.position.x, orbB.position.y, orbB.position.z);
|
||||
positions.needsUpdate = true;
|
||||
|
||||
// Pulse opacity
|
||||
const age = (Date.now() - thread.userData.createdAt) / 1000;
|
||||
thread.material.opacity = 0.3 + Math.sin(age * 2) * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a memory orb and animate it transitioning to its room.
|
||||
* @param {string} category - Memory category
|
||||
* @param {object} metadata - Memory metadata
|
||||
* @param {number} importance - 0-1 importance score
|
||||
* @param {THREE.Vector3} startPos - Starting position (default: above avatar)
|
||||
* @returns {THREE.Mesh} The orb (already in transit)
|
||||
*/
|
||||
function spawnWithRoomTransition(category, metadata = {}, importance = 0.5, startPos = null) {
|
||||
if (!startPos) startPos = new THREE.Vector3(0, 2, 0);
|
||||
|
||||
const room = getRoomForCategory(category);
|
||||
const endPos = room ? getPositionInRoom(room) : new THREE.Vector3(0, 2, 0);
|
||||
|
||||
let color = 0x4af0c0;
|
||||
if (room && room.visual_theme) {
|
||||
const accent = room.visual_theme.colors?.accent;
|
||||
if (accent) color = parseInt(accent.replace('#', ''), 16);
|
||||
}
|
||||
|
||||
const size = 0.2 + importance * 0.5;
|
||||
|
||||
// Spawn at start position
|
||||
const orb = spawnMemoryOrb(startPos, color, size, {
|
||||
...metadata,
|
||||
category: category,
|
||||
room: room ? room.id : 'unknown',
|
||||
transitioning: true,
|
||||
targetPos: endPos,
|
||||
transitionStart: Date.now(),
|
||||
transitionDuration: 2000 + Math.random() * 1000
|
||||
});
|
||||
|
||||
return orb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate room transitions for orbs that are in transit.
|
||||
* @param {number} delta - Time since last frame
|
||||
*/
|
||||
function animateRoomTransitions(delta) {
|
||||
for (const orb of memoryOrbs) {
|
||||
if (!orb.userData?.transitioning || !orb.userData?.targetPos) continue;
|
||||
|
||||
const elapsed = Date.now() - orb.userData.transitionStart;
|
||||
const duration = orb.userData.transitionDuration;
|
||||
const progress = Math.min(1, elapsed / duration);
|
||||
|
||||
// Ease-out cubic
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
orb.position.lerpVectors(
|
||||
orb.position, // Current (already partially moved)
|
||||
orb.userData.targetPos,
|
||||
eased * 0.05 // Smooth interpolation factor per frame
|
||||
);
|
||||
|
||||
if (progress >= 1) {
|
||||
orb.position.copy(orb.userData.targetPos);
|
||||
orb.userData.transitioning = false;
|
||||
delete orb.userData.targetPos;
|
||||
delete orb.userData.transitionStart;
|
||||
delete orb.userData.transitionDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init().then(() => {
|
||||
loadSpatialMemorySchema();
|
||||
createAshStorm();
|
||||
createPortalTunnel();
|
||||
|
||||
// Visualize memory zones if schema is ready
|
||||
if (spatialSchema) {
|
||||
spatialSchema.visualizeZones();
|
||||
}
|
||||
fetchGiteaData();
|
||||
setInterval(fetchGiteaData, 30000);
|
||||
runWeeklyAudit();
|
||||
|
||||
Reference in New Issue
Block a user