chore: update app.js from workspace

This commit is contained in:
2026-03-30 16:07:20 +00:00
parent 981d95a720
commit eeeed16a9b

796
app.js
View File

@@ -35,7 +35,6 @@ let activePortal = null; // Portal currently in proximity
let activeVisionPoint = null; // Vision point currently in proximity
let portalOverlayActive = false;
let visionOverlayActive = false;
let atlasOverlayActive = false;
let thoughtStreamMesh;
let harnessPulseMesh;
let powerMeterBars = [];
@@ -76,569 +75,6 @@ const orbitState = {
let flyY = 2;
// ═══ INIT ═══
// ═══ SOVEREIGN SYMBOLIC ENGINE (GOFAI) ═══
class SymbolicEngine {
constructor() {
this.facts = new Map();
this.factIndices = new Map();
this.factMask = 0n;
this.rules = [];
this.reasoningLog = [];
}
addFact(key, value) {
this.facts.set(key, value);
if (!this.factIndices.has(key)) {
this.factIndices.set(key, BigInt(this.factIndices.size));
}
const bitIndex = this.factIndices.get(key);
if (value) {
this.factMask |= (1n << bitIndex);
} else {
this.factMask &= ~(1n << bitIndex);
}
}
addRule(condition, action, description) {
this.rules.push({ condition, action, description });
}
reason() {
this.rules.forEach(rule => {
if (rule.condition(this.facts)) {
const result = rule.action(this.facts);
if (result) {
this.logReasoning(rule.description, result);
}
}
});
}
logReasoning(ruleDesc, outcome) {
const entry = { timestamp: Date.now(), rule: ruleDesc, outcome: outcome };
this.reasoningLog.unshift(entry);
if (this.reasoningLog.length > 5) this.reasoningLog.pop();
const container = document.getElementById('symbolic-log-content');
if (container) {
const logDiv = document.createElement('div');
logDiv.className = 'symbolic-log-entry';
logDiv.innerHTML = `<span class="symbolic-rule">[RULE] ${ruleDesc}</span><span class="symbolic-outcome">→ ${outcome}</span>`;
container.prepend(logDiv);
if (container.children.length > 5) container.lastElementChild.remove();
}
}
}
class AgentFSM {
constructor(agentId, initialState) {
this.agentId = agentId;
this.state = initialState;
this.transitions = {};
}
addTransition(fromState, toState, condition) {
if (!this.transitions[fromState]) this.transitions[fromState] = [];
this.transitions[fromState].push({ toState, condition });
}
update(facts) {
const possibleTransitions = this.transitions[this.state] || [];
for (const transition of possibleTransitions) {
if (transition.condition(facts)) {
console.log(`[FSM] Agent ${this.agentId} transitioning: ${this.state} -> ${transition.toState}`);
this.state = transition.toState;
return true;
}
}
return false;
}
}
class KnowledgeGraph {
constructor() {
this.nodes = new Map();
this.edges = [];
}
addNode(id, type, metadata = {}) {
this.nodes.set(id, { id, type, ...metadata });
}
addEdge(from, to, relation) {
this.edges.push({ from, to, relation });
}
query(from, relation) {
return this.edges
.filter(e => e.from === from && e.relation === relation)
.map(e => this.nodes.get(e.to));
}
}
class Blackboard {
constructor() {
this.data = {};
this.subscribers = [];
}
write(key, value, source) {
const oldValue = this.data[key];
this.data[key] = value;
this.notify(key, value, oldValue, source);
}
read(key) { return this.data[key]; }
subscribe(callback) { this.subscribers.push(callback); }
notify(key, value, oldValue, source) {
this.subscribers.forEach(sub => sub(key, value, oldValue, source));
const container = document.getElementById('blackboard-log-content');
if (container) {
const entry = document.createElement('div');
entry.className = 'blackboard-entry';
entry.innerHTML = `<span class="bb-source">[${source}]</span> <span class="bb-key">${key}</span>: <span class="bb-value">${JSON.stringify(value)}</span>`;
container.prepend(entry);
if (container.children.length > 8) container.lastElementChild.remove();
}
}
}
class SymbolicPlanner {
constructor() {
this.actions = [];
this.currentPlan = [];
}
addAction(name, preconditions, effects) {
this.actions.push({ name, preconditions, effects });
}
heuristic(state, goal) {
let h = 0;
for (let key in goal) {
if (state[key] !== goal[key]) {
h += Math.abs((state[key] || 0) - (goal[key] || 0));
}
}
return h;
}
findPlan(initialState, goalState) {
let openSet = [{ state: initialState, plan: [], g: 0, h: this.heuristic(initialState, goalState) }];
let visited = new Map();
visited.set(JSON.stringify(initialState), 0);
while (openSet.length > 0) {
openSet.sort((a, b) => (a.g + a.h) - (b.g + b.h));
let { state, plan, g } = openSet.shift();
if (this.isGoalReached(state, goalState)) return plan;
for (let action of this.actions) {
if (this.arePreconditionsMet(state, action.preconditions)) {
let nextState = { ...state, ...action.effects };
let stateStr = JSON.stringify(nextState);
let nextG = g + 1;
if (!visited.has(stateStr) || nextG < visited.get(stateStr)) {
visited.set(stateStr, nextG);
openSet.push({
state: nextState,
plan: [...plan, action.name],
g: nextG,
h: this.heuristic(nextState, goalState)
});
}
}
}
}
return null;
}
isGoalReached(state, goal) {
for (let key in goal) {
if (state[key] !== goal[key]) return false;
}
return true;
}
arePreconditionsMet(state, preconditions) {
for (let key in preconditions) {
if (state[key] < preconditions[key]) return false;
}
return true;
}
logPlan(plan) {
this.currentPlan = plan;
const container = document.getElementById('planner-log-content');
if (container) {
container.innerHTML = '';
if (!plan || plan.length === 0) {
container.innerHTML = '<div class="planner-empty">NO ACTIVE PLAN</div>';
return;
}
plan.forEach((step, i) => {
const div = document.createElement('div');
div.className = 'planner-step';
div.innerHTML = `<span class="step-num">${i+1}.</span> ${step}`;
container.appendChild(div);
});
}
}
}
class HTNPlanner {
constructor() {
this.methods = {};
this.primitiveTasks = {};
}
addMethod(taskName, preconditions, subtasks) {
if (!this.methods[taskName]) this.methods[taskName] = [];
this.methods[taskName].push({ preconditions, subtasks });
}
addPrimitiveTask(taskName, preconditions, effects) {
this.primitiveTasks[taskName] = { preconditions, effects };
}
findPlan(initialState, tasks) {
return this.decompose(initialState, tasks, []);
}
decompose(state, tasks, plan) {
if (tasks.length === 0) return plan;
const [task, ...remainingTasks] = tasks;
if (this.primitiveTasks[task]) {
const { preconditions, effects } = this.primitiveTasks[task];
if (this.arePreconditionsMet(state, preconditions)) {
const nextState = { ...state, ...effects };
return this.decompose(nextState, remainingTasks, [...plan, task]);
}
return null;
}
const methods = this.methods[task] || [];
for (const method of methods) {
if (this.arePreconditionsMet(state, method.preconditions)) {
const result = this.decompose(state, [...method.subtasks, ...remainingTasks], plan);
if (result) return result;
}
}
return null;
}
arePreconditionsMet(state, preconditions) {
for (const key in preconditions) {
if (state[key] < (preconditions[key] || 0)) return false;
}
return true;
}
}
class CaseBasedReasoner {
constructor() {
this.caseLibrary = [];
}
addCase(situation, action, outcome) {
this.caseLibrary.push({ situation, action, outcome, timestamp: Date.now() });
}
findSimilarCase(currentSituation) {
let bestMatch = null;
let maxSimilarity = -1;
this.caseLibrary.forEach(c => {
let similarity = this.calculateSimilarity(currentSituation, c.situation);
if (similarity > maxSimilarity) {
maxSimilarity = similarity;
bestMatch = c;
}
});
return maxSimilarity > 0.7 ? bestMatch : null;
}
calculateSimilarity(s1, s2) {
let score = 0, total = 0;
for (let key in s1) {
if (s2[key] !== undefined) {
score += 1 - Math.abs(s1[key] - s2[key]);
total += 1;
}
}
return total > 0 ? score / total : 0;
}
logCase(c) {
const container = document.getElementById('cbr-log-content');
if (container) {
const div = document.createElement('div');
div.className = 'cbr-entry';
div.innerHTML = `
<div class="cbr-match">SIMILAR CASE FOUND (${(this.calculateSimilarity(symbolicEngine.facts, c.situation) * 100).toFixed(0)}%)</div>
<div class="cbr-action">SUGGESTED: ${c.action}</div>
<div class="cbr-outcome">PREVIOUS OUTCOME: ${c.outcome}</div>
`;
container.prepend(div);
if (container.children.length > 3) container.lastElementChild.remove();
}
}
}
class NeuroSymbolicBridge {
constructor(symbolicEngine, blackboard) {
this.engine = symbolicEngine;
this.blackboard = blackboard;
this.perceptionLog = [];
}
perceive(rawState) {
const concepts = [];
if (rawState.stability < 0.4 && rawState.energy > 60) concepts.push('UNSTABLE_OSCILLATION');
if (rawState.energy < 30 && rawState.activePortals > 2) concepts.push('CRITICAL_DRAIN_PATTERN');
concepts.forEach(concept => {
this.engine.addFact(concept, true);
this.logPerception(concept);
});
return concepts;
}
logPerception(concept) {
const container = document.getElementById('neuro-bridge-log-content');
if (container) {
const div = document.createElement('div');
div.className = 'neuro-bridge-entry';
div.innerHTML = `<span class="neuro-icon">🧠</span> <span class="neuro-concept">${concept}</span>`;
container.prepend(div);
if (container.children.length > 5) container.lastElementChild.remove();
}
}
}
class MetaReasoningLayer {
constructor(planner, blackboard) {
this.planner = planner;
this.blackboard = blackboard;
this.reasoningCache = new Map();
this.performanceMetrics = { totalReasoningTime: 0, calls: 0 };
}
getCachedPlan(stateKey) {
const cached = this.reasoningCache.get(stateKey);
if (cached && (Date.now() - cached.timestamp < 10000)) return cached.plan;
return null;
}
cachePlan(stateKey, plan) {
this.reasoningCache.set(stateKey, { plan, timestamp: Date.now() });
}
reflect() {
const avgTime = this.performanceMetrics.totalReasoningTime / (this.performanceMetrics.calls || 1);
const container = document.getElementById('meta-log-content');
if (container) {
container.innerHTML = `
<div class="meta-stat">CACHE SIZE: ${this.reasoningCache.size}</div>
<div class="meta-stat">AVG LATENCY: ${avgTime.toFixed(2)}ms</div>
<div class="meta-stat">STATUS: ${avgTime > 50 ? 'OPTIMIZING' : 'NOMINAL'}</div>
`;
}
}
track(startTime) {
const duration = performance.now() - startTime;
this.performanceMetrics.totalReasoningTime += duration;
this.performanceMetrics.calls++;
}
}
// ═══ ADAPTIVE CALIBRATOR (LOCAL EFFICIENCY) ═══
class AdaptiveCalibrator {
constructor(modelId, initialParams) {
this.model = modelId;
this.weights = {
'input_tokens': 0.0,
'complexity_score': 0.0,
'task_type_indicator': 0.0,
'bias': initialParams.base_rate || 0.0
};
this.learningRate = 0.01;
this.history = [];
}
predict(features) {
let prediction = this.weights['bias'];
for (let feature in features) {
if (this.weights[feature] !== undefined) {
prediction += this.weights[feature] * features[feature];
}
}
return Math.max(0, prediction);
}
update(features, actualCost) {
const predicted = this.predict(features);
const error = actualCost - predicted;
for (let feature in features) {
if (this.weights[feature] !== undefined) {
this.weights[feature] += this.learningRate * error * features[feature];
}
}
this.history.push({ predicted, actual: actualCost, timestamp: Date.now() });
const container = document.getElementById('calibrator-log-content');
if (container) {
const div = document.createElement('div');
div.className = 'calibrator-entry';
div.innerHTML = `<span class="cal-label">CALIBRATED:</span> <span class="cal-val">${predicted.toFixed(4)}</span> <span class="cal-err">ERR: ${error.toFixed(4)}</span>`;
container.prepend(div);
if (container.children.length > 5) container.lastElementChild.remove();
}
}
}
// ═══ NOSTR AGENT REGISTRATION ═══
class NostrAgent {
constructor(pubkey) {
this.pubkey = pubkey;
this.relays = ['wss://relay.damus.io', 'wss://nos.lol'];
}
async announce(metadata) {
console.log(`[NOSTR] Announcing agent ${this.pubkey}...`);
const event = {
kind: 0,
pubkey: this.pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: JSON.stringify(metadata),
id: 'mock_id',
sig: 'mock_sig'
};
this.relays.forEach(url => {
console.log(`[NOSTR] Publishing to ${url}: `, event);
});
const container = document.getElementById('nostr-log-content');
if (container) {
const div = document.createElement('div');
div.className = 'nostr-entry';
div.innerHTML = `<span class="nostr-pubkey">[${this.pubkey.substring(0,8)}...]</span> <span class="nostr-status">ANNOUNCED</span>`;
container.prepend(div);
}
}
}
// ═══ L402 CLIENT LOGIC ═══
class L402Client {
async fetchWithL402(url) {
console.log(`[L402] Fetching ${url}...`);
const response = await fetch(url);
if (response.status === 402) {
const authHeader = response.headers.get('WWW-Authenticate');
console.log(`[L402] Challenge received: ${authHeader}`);
const container = document.getElementById('l402-log-content');
if (container) {
const div = document.createElement('div');
div.className = 'l402-entry';
div.innerHTML = `<span class="l402-status">CHALLENGE</span> <span class="l402-msg">Payment Required</span>`;
container.prepend(div);
}
return { status: 402, challenge: authHeader };
}
return response.json();
}
}
let nostrAgent, l402Client;
// ═══ PARALLEL SYMBOLIC EXECUTION (PSE) ═══
class PSELayer {
constructor() {
this.worker = new Worker('gofai_worker.js');
this.worker.onmessage = (e) => this.handleWorkerMessage(e);
this.pendingRequests = new Map();
}
handleWorkerMessage(e) {
const { type, results, plan } = e.data;
if (type === 'REASON_RESULT') {
results.forEach(res => symbolicEngine.logReasoning(res.rule, res.outcome));
} else if (type === 'PLAN_RESULT') {
symbolicPlanner.logPlan(plan);
}
}
offloadReasoning(facts, rules) {
this.worker.postMessage({ type: 'REASON', data: { facts, rules } });
}
offloadPlanning(initialState, goalState, actions) {
this.worker.postMessage({ type: 'PLAN', data: { initialState, goalState, actions } });
}
}
let pseLayer;
let metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
let agentFSMs = {};
function setupGOFAI() {
knowledgeGraph = new KnowledgeGraph();
blackboard = new Blackboard();
symbolicEngine = new SymbolicEngine();
symbolicPlanner = new SymbolicPlanner();
cbr = new CaseBasedReasoner();
neuroBridge = new NeuroSymbolicBridge(symbolicEngine, blackboard);
metaLayer = new MetaReasoningLayer(symbolicPlanner, blackboard);
nostrAgent = new NostrAgent("npub1...");
l402Client = new L402Client();
nostrAgent.announce({ name: "Timmy Nexus Agent", capabilities: ["GOFAI", "L402"] });
pseLayer = new PSELayer();
calibrator = new AdaptiveCalibrator('nexus-v1', { base_rate: 0.05 });
// Setup initial facts
symbolicEngine.addFact('energy', 100);
symbolicEngine.addFact('stability', 1.0);
// Setup FSM
agentFSMs['timmy'] = new AgentFSM('timmy', 'IDLE');
agentFSMs['timmy'].addTransition('IDLE', 'ANALYZING', (facts) => facts.get('activePortals') > 0);
// Setup Planner
symbolicPlanner.addAction('Stabilize Matrix', { energy: 50 }, { stability: 1.0 });
}
function updateGOFAI(delta, elapsed) {
const startTime = performance.now();
// Simulate perception
neuroBridge.perceive({ stability: 0.3, energy: 80, activePortals: 1 });
// Run reasoning
if (Math.floor(elapsed * 2) > Math.floor((elapsed - delta) * 2)) {
symbolicEngine.reason();
pseLayer.offloadReasoning(Array.from(symbolicEngine.facts.entries()), symbolicEngine.rules.map(r => ({ description: r.description })));
document.getElementById("pse-task-count").innerText = parseInt(document.getElementById("pse-task-count").innerText) + 1;
metaLayer.reflect();
// Simulate calibration update
calibrator.update({ input_tokens: 100, complexity_score: 0.5 }, 0.06);
if (Math.random() > 0.95) l402Client.fetchWithL402("http://localhost:8080/api/cost-estimate");
}
metaLayer.track(startTime);
}
async function init() {
clock = new THREE.Clock();
playerPos = new THREE.Vector3(0, 2, 12);
@@ -658,7 +94,6 @@ async function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050510, 0.012);
setupGOFAI();
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.copy(playerPos);
@@ -1512,91 +947,14 @@ function createPortal(config) {
scene.add(group);
const portalObj = {
return {
config,
group,
ring,
swirl,
pSystem,
light,
customElements: {}
light
};
// ═══ DISTINCT VISUAL IDENTITIES ═══
if (config.id === 'archive') {
// Floating Data Cubes
const cubes = [];
for (let i = 0; i < 6; i++) {
const cubeGeo = new THREE.BoxGeometry(0.4, 0.4, 0.4);
const cubeMat = new THREE.MeshStandardMaterial({
color: portalColor,
emissive: portalColor,
emissiveIntensity: 1.5,
transparent: true,
opacity: 0.8
});
const cube = new THREE.Mesh(cubeGeo, cubeMat);
group.add(cube);
cubes.push(cube);
}
portalObj.customElements.cubes = cubes;
} else if (config.id === 'chapel') {
// Glowing Core + Halo
const coreGeo = new THREE.SphereGeometry(1.2, 32, 32);
const coreMat = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
emissive: portalColor,
emissiveIntensity: 2,
transparent: true,
opacity: 0.4,
transmission: 0.9,
thickness: 2
});
const core = new THREE.Mesh(coreGeo, coreMat);
core.position.y = 3.5;
group.add(core);
portalObj.customElements.core = core;
const haloGeo = new THREE.TorusGeometry(3.5, 0.05, 16, 100);
const haloMat = new THREE.MeshBasicMaterial({ color: portalColor, transparent: true, opacity: 0.3 });
const halo = new THREE.Mesh(haloGeo, haloMat);
halo.position.y = 3.5;
group.add(halo);
portalObj.customElements.halo = halo;
} else if (config.id === 'courtyard') {
// Double Rotating Rings
const outerRingGeo = new THREE.TorusGeometry(4.2, 0.1, 16, 80);
const outerRingMat = new THREE.MeshStandardMaterial({
color: portalColor,
emissive: portalColor,
emissiveIntensity: 0.8,
transparent: true,
opacity: 0.5
});
const outerRing = new THREE.Mesh(outerRingGeo, outerRingMat);
outerRing.position.y = 3.5;
group.add(outerRing);
portalObj.customElements.outerRing = outerRing;
} else if (config.id === 'gate') {
// Spiky Monoliths
const spikes = [];
for (let i = 0; i < 8; i++) {
const spikeGeo = new THREE.ConeGeometry(0.2, 1.5, 4);
const spikeMat = new THREE.MeshStandardMaterial({ color: 0x111111, emissive: portalColor, emissiveIntensity: 0.5 });
const spike = new THREE.Mesh(spikeGeo, spikeMat);
const angle = (i / 8) * Math.PI * 2;
spike.position.set(Math.cos(angle) * 3.5, 3.5 + Math.sin(angle) * 3.5, 0);
spike.rotation.z = angle + Math.PI / 2;
group.add(spike);
spikes.push(spike);
}
portalObj.customElements.spikes = spikes;
// Darker Swirl
swirl.material.uniforms.uColor.value = new THREE.Color(0x220000);
}
return portalObj;
}
// ═══ PARTICLES ═══
@@ -1800,14 +1158,10 @@ function setupControls() {
input.focus();
}
}
if (e.key.toLowerCase() === 'm' && document.activeElement !== document.getElementById('chat-input')) {
openPortalAtlas();
}
if (e.key === 'Escape') {
document.getElementById('chat-input').blur();
if (portalOverlayActive) closePortalOverlay();
if (visionOverlayActive) closeVisionOverlay();
if (atlasOverlayActive) closePortalAtlas();
}
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
cycleNavMode();
@@ -1876,44 +1230,18 @@ function setupControls() {
chatOpen = !chatOpen;
document.getElementById('chat-panel').classList.toggle('collapsed', !chatOpen);
});
document.getElementById('chat-send').addEventListener('click', () => sendChatMessage());
// Chat quick actions
document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
const btn = e.target.closest('.quick-action-btn');
if (!btn) return;
const action = btn.dataset.action;
switch(action) {
case 'status':
sendChatMessage("Timmy, what is the current system status?");
break;
case 'agents':
sendChatMessage("Timmy, check on all active agents.");
break;
case 'portals':
openPortalAtlas();
break;
case 'help':
sendChatMessage("Timmy, I need assistance with Nexus navigation.");
break;
}
});
document.getElementById('chat-send').addEventListener('click', sendChatMessage);
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
}
function sendChatMessage(overrideText = null) {
function sendChatMessage() {
const input = document.getElementById('chat-input');
const text = overrideText || input.value.trim();
const text = input.value.trim();
if (!text) return;
addChatMessage('user', text);
if (!overrideText) input.value = '';
input.value = '';
setTimeout(() => {
const responses = [
'Processing your request through the harness...',
@@ -2192,86 +1520,6 @@ function closeVisionOverlay() {
document.getElementById('vision-overlay').style.display = 'none';
}
// ═══ PORTAL ATLAS ═══
function openPortalAtlas() {
atlasOverlayActive = true;
document.getElementById('atlas-overlay').style.display = 'flex';
populateAtlas();
}
function closePortalAtlas() {
atlasOverlayActive = false;
document.getElementById('atlas-overlay').style.display = 'none';
}
function populateAtlas() {
const grid = document.getElementById('atlas-grid');
grid.innerHTML = '';
let onlineCount = 0;
let standbyCount = 0;
portals.forEach(portal => {
const config = portal.config;
if (config.status === 'online') onlineCount++;
if (config.status === 'standby') standbyCount++;
const card = document.createElement('div');
card.className = 'atlas-card';
card.style.setProperty('--portal-color', config.color);
const statusClass = `status-${config.status || 'online'}`;
card.innerHTML = `
<div class="atlas-card-header">
<div class="atlas-card-name">${config.name}</div>
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
</div>
<div class="atlas-card-desc">${config.description}</div>
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
</div>
`;
card.addEventListener('click', () => {
focusPortal(portal);
closePortalAtlas();
});
grid.appendChild(card);
});
document.getElementById('atlas-online-count').textContent = onlineCount;
document.getElementById('atlas-standby-count').textContent = standbyCount;
// Update Bannerlord HUD status
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
if (bannerlord) {
const statusEl = document.getElementById('bannerlord-status');
statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline');
}
}
function focusPortal(portal) {
// Teleport player to a position in front of the portal
const offset = new THREE.Vector3(0, 0, 6).applyEuler(new THREE.Euler(0, portal.config.rotation?.y || 0, 0));
playerPos.copy(portal.group.position).add(offset);
playerPos.y = 2; // Keep at eye level
// Rotate player to face the portal
playerRot.y = (portal.config.rotation?.y || 0) + Math.PI;
playerRot.x = 0;
addChatMessage('system', `Navigation focus: ${portal.config.name}`);
// If in orbit mode, reset target
if (NAV_MODES[navModeIdx] === 'orbit') {
orbitState.target.copy(portal.group.position);
orbitState.target.y = 3.5;
}
}
// ═══ GAME LOOP ═══
let lastThoughtTime = 0;
let pulseTimer = 0;
@@ -2387,38 +1635,6 @@ function gameLoop() {
}
// Pulse light
portal.light.intensity = 1.5 + Math.sin(elapsed * 2) * 0.5;
// Custom animations for distinct identities
if (portal.config.id === 'archive' && portal.customElements.cubes) {
portal.customElements.cubes.forEach((cube, i) => {
cube.rotation.x += delta * (0.5 + i * 0.1);
cube.rotation.y += delta * (0.3 + i * 0.1);
const orbitSpeed = 0.5 + i * 0.2;
const orbitRadius = 4 + Math.sin(elapsed * 0.5 + i) * 0.5;
cube.position.x = Math.cos(elapsed * orbitSpeed + i) * orbitRadius;
cube.position.z = Math.sin(elapsed * orbitSpeed + i) * orbitRadius;
cube.position.y = 3.5 + Math.sin(elapsed * 1.2 + i) * 1.5;
});
}
if (portal.config.id === 'chapel' && portal.customElements.halo) {
portal.customElements.halo.rotation.z -= delta * 0.2;
portal.customElements.halo.scale.setScalar(1 + Math.sin(elapsed * 0.8) * 0.05);
portal.customElements.core.material.emissiveIntensity = 2 + Math.sin(elapsed * 3) * 1;
}
if (portal.config.id === 'courtyard' && portal.customElements.outerRing) {
portal.customElements.outerRing.rotation.z -= delta * 0.5;
portal.customElements.outerRing.rotation.y = Math.cos(elapsed * 0.4) * 0.2;
}
if (portal.config.id === 'gate' && portal.customElements.spikes) {
portal.customElements.spikes.forEach((spike, i) => {
const s = 1 + Math.sin(elapsed * 2 + i) * 0.2;
spike.scale.set(s, s, s);
});
}
// Animate particles
const positions = portal.pSystem.geometry.attributes.position.array;
for (let i = 0; i < positions.length / 3; i++) {