Files
timmy-tower/the-matrix/js/agents.js

238 lines
6.7 KiB
JavaScript

import * as THREE from 'three';
import { AGENT_DEFS, colorToCss } from './agent-defs.js';
const agents = new Map();
let scene;
let connectionLines = [];
class Agent {
constructor(def) {
this.id = def.id;
this.label = def.label;
this.color = def.color;
this.role = def.role;
this.position = new THREE.Vector3(def.x, 0, def.z);
this.state = 'idle';
this.pulsePhase = Math.random() * Math.PI * 2;
this.group = new THREE.Group();
this.group.position.copy(this.position);
this._buildMeshes();
this._buildLabel();
}
_buildMeshes() {
const mat = new THREE.MeshStandardMaterial({
color: this.color,
emissive: this.color,
emissiveIntensity: 0.4,
roughness: 0.3,
metalness: 0.8,
});
const geo = new THREE.IcosahedronGeometry(0.7, 1);
this.core = new THREE.Mesh(geo, mat);
this.group.add(this.core);
const ringGeo = new THREE.TorusGeometry(1.1, 0.04, 8, 32);
const ringMat = new THREE.MeshBasicMaterial({ color: this.color, transparent: true, opacity: 0.5 });
this.ring = new THREE.Mesh(ringGeo, ringMat);
this.ring.rotation.x = Math.PI / 2;
this.group.add(this.ring);
const glowGeo = new THREE.SphereGeometry(1.3, 16, 16);
const glowMat = new THREE.MeshBasicMaterial({
color: this.color,
transparent: true,
opacity: 0.05,
side: THREE.BackSide,
});
this.glow = new THREE.Mesh(glowGeo, glowMat);
this.group.add(this.glow);
const light = new THREE.PointLight(this.color, 1.5, 10);
this.group.add(light);
this.light = light;
}
_buildLabel() {
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0, 0, 256, 64);
ctx.font = 'bold 22px Courier New';
ctx.fillStyle = colorToCss(this.color);
ctx.textAlign = 'center';
ctx.fillText(this.label, 128, 28);
ctx.font = '14px Courier New';
ctx.fillStyle = '#007722';
ctx.fillText(this.role.toUpperCase(), 128, 50);
const tex = new THREE.CanvasTexture(canvas);
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
this.sprite = new THREE.Sprite(spriteMat);
this.sprite.scale.set(2.4, 0.6, 1);
this.sprite.position.y = 2;
this.group.add(this.sprite);
}
update(time) {
const pulse = Math.sin(time * 0.002 + this.pulsePhase);
const pulse2 = Math.sin(time * 0.005 + this.pulsePhase * 1.3);
let intensity, lightIntensity, ringSpeed, glowOpacity, scaleAmp;
switch (this.state) {
case 'working':
intensity = 1.0 + pulse * 0.3;
lightIntensity = 3.5 + pulse2 * 0.8;
ringSpeed = 0.07;
glowOpacity = 0.18 + pulse * 0.08;
scaleAmp = 0.14;
break;
case 'thinking':
intensity = 0.7 + pulse2 * 0.5;
lightIntensity = 2.2 + pulse * 0.5;
ringSpeed = 0.045;
glowOpacity = 0.12 + pulse2 * 0.06;
scaleAmp = 0.10;
break;
case 'active':
intensity = 0.6 + pulse * 0.4;
lightIntensity = 2.0 + pulse;
ringSpeed = 0.03;
glowOpacity = 0.08 + pulse * 0.04;
scaleAmp = 0.08;
break;
default:
intensity = 0.2 + pulse * 0.1;
lightIntensity = 0.8 + pulse * 0.3;
ringSpeed = 0.008;
glowOpacity = 0.03 + pulse * 0.02;
scaleAmp = 0.03;
}
this.core.material.emissiveIntensity = intensity;
this.light.intensity = lightIntensity;
this.core.scale.setScalar(1 + pulse * scaleAmp);
this.ring.rotation.y += ringSpeed;
this.ring.material.opacity = 0.3 + pulse * 0.2;
this.glow.material.opacity = glowOpacity;
this.group.position.y = this.position.y + Math.sin(time * 0.001 + this.pulsePhase) * 0.15;
}
setState(state) {
this.state = state;
}
dispose() {
this.core.geometry.dispose();
this.core.material.dispose();
this.ring.geometry.dispose();
this.ring.material.dispose();
this.glow.geometry.dispose();
this.glow.material.dispose();
if (this.sprite.material.map) this.sprite.material.map.dispose();
this.sprite.material.dispose();
}
}
export function initAgents(sceneRef) {
scene = sceneRef;
AGENT_DEFS.forEach(def => {
const agent = new Agent(def);
agents.set(def.id, agent);
scene.add(agent.group);
});
buildConnectionLines();
}
function buildConnectionLines() {
connectionLines.forEach(l => scene.remove(l));
connectionLines = [];
const agentList = [...agents.values()];
const lineMat = new THREE.LineBasicMaterial({
color: 0x003300,
transparent: true,
opacity: 0.4,
});
for (let i = 0; i < agentList.length; i++) {
for (let j = i + 1; j < agentList.length; j++) {
const a = agentList[i];
const b = agentList[j];
if (a.position.distanceTo(b.position) <= 8) {
const points = [a.position.clone(), b.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geo, lineMat.clone());
connectionLines.push(line);
scene.add(line);
}
}
}
}
export function updateAgents(time) {
agents.forEach(agent => agent.update(time));
}
export function getAgentCount() {
return agents.size;
}
export function setAgentState(agentId, state) {
const agent = agents.get(agentId);
if (agent) agent.setState(state);
}
export function getAgentDefs() {
return [...agents.values()].map(a => ({
id: a.id, label: a.label, role: a.role, color: a.color, state: a.state,
}));
}
/**
* Return a snapshot of each agent's current runtime state.
* Call before teardown so the state can be reapplied after reinit.
* @returns {Object.<string, string>} — e.g. { alpha: 'active', beta: 'idle' }
*/
export function getAgentStates() {
const snapshot = {};
agents.forEach((agent, id) => { snapshot[id] = agent.state; });
return snapshot;
}
/**
* Apply a previously captured state snapshot to freshly-created agents.
* Call immediately after initAgents() during context-restore reinit.
* @param {Object.<string, string>} snapshot
*/
export function applyAgentStates(snapshot) {
if (!snapshot) return;
for (const [id, state] of Object.entries(snapshot)) {
const agent = agents.get(id);
if (agent) agent.setState(state);
}
}
/**
* Dispose all agent GPU resources (geometries, materials, textures).
* Called before context-loss teardown.
*/
export function disposeAgents() {
agents.forEach(agent => agent.dispose());
agents.clear();
connectionLines.forEach(l => {
l.geometry.dispose();
l.material.dispose();
});
connectionLines = [];
scene = null;
}