chore: update app.js from workspace
This commit is contained in:
796
app.js
796
app.js
@@ -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++) {
|
||||
|
||||
Reference in New Issue
Block a user