[claude] Agent idle behaviors in 3D world (#8) (#48)
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
This commit was merged in pull request #48.
This commit is contained in:
260
app.js
260
app.js
@@ -438,6 +438,76 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) {
|
|||||||
batcaveTerminals.push({ group, scanMat, borderMat });
|
batcaveTerminals.push({ group, scanMat, borderMat });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ AGENT IDLE BEHAVIOR SYSTEM ═══
|
||||||
|
const AGENT_STATES = { IDLE: 'IDLE', PACING: 'PACING', LOOKING: 'LOOKING', READING: 'READING' };
|
||||||
|
const ACTIVITY_STATES = { NONE: 'NONE', WAITING: 'WAITING', THINKING: 'THINKING', PROCESSING: 'PROCESSING' };
|
||||||
|
|
||||||
|
function createActivityIndicator(color) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.y = 4.2;
|
||||||
|
group.visible = false;
|
||||||
|
|
||||||
|
// WAITING — pulsing sphere
|
||||||
|
const waitGeo = new THREE.SphereGeometry(0.18, 16, 16);
|
||||||
|
const waitMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.85 });
|
||||||
|
const waitMesh = new THREE.Mesh(waitGeo, waitMat);
|
||||||
|
waitMesh.name = 'indicator_waiting';
|
||||||
|
waitMesh.visible = false;
|
||||||
|
group.add(waitMesh);
|
||||||
|
|
||||||
|
// THINKING — wireframe octahedron
|
||||||
|
const thinkGeo = new THREE.OctahedronGeometry(0.2, 0);
|
||||||
|
const thinkMat = new THREE.MeshBasicMaterial({ color, wireframe: true });
|
||||||
|
const thinkMesh = new THREE.Mesh(thinkGeo, thinkMat);
|
||||||
|
thinkMesh.name = 'indicator_thinking';
|
||||||
|
thinkMesh.visible = false;
|
||||||
|
group.add(thinkMesh);
|
||||||
|
|
||||||
|
// PROCESSING — spinning torus ring
|
||||||
|
const procGeo = new THREE.TorusGeometry(0.18, 0.04, 8, 32);
|
||||||
|
const procMat = new THREE.MeshBasicMaterial({ color });
|
||||||
|
const procMesh = new THREE.Mesh(procGeo, procMat);
|
||||||
|
procMesh.name = 'indicator_processing';
|
||||||
|
procMesh.visible = false;
|
||||||
|
group.add(procMesh);
|
||||||
|
|
||||||
|
return { group, waitMesh, thinkMesh, procMesh };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAgentActivity(agent, state) {
|
||||||
|
agent.activityState = state;
|
||||||
|
agent.indicator.group.visible = (state !== ACTIVITY_STATES.NONE);
|
||||||
|
agent.indicator.waitMesh.visible = (state === ACTIVITY_STATES.WAITING);
|
||||||
|
agent.indicator.thinkMesh.visible = (state === ACTIVITY_STATES.THINKING);
|
||||||
|
agent.indicator.procMesh.visible = (state === ACTIVITY_STATES.PROCESSING);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPacingPath(station) {
|
||||||
|
// Small 3-waypoint circuit around the station
|
||||||
|
const r = 1.8;
|
||||||
|
return [
|
||||||
|
new THREE.Vector3(station.x - r, 0, station.z),
|
||||||
|
new THREE.Vector3(station.x, 0, station.z + r),
|
||||||
|
new THREE.Vector3(station.x + r, 0, station.z - r * 0.5),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickNextState(agent) {
|
||||||
|
const weights = {
|
||||||
|
[AGENT_STATES.IDLE]: 40,
|
||||||
|
[AGENT_STATES.PACING]: 25,
|
||||||
|
[AGENT_STATES.LOOKING]: 20,
|
||||||
|
[AGENT_STATES.READING]: 15,
|
||||||
|
};
|
||||||
|
const total = Object.values(weights).reduce((a, b) => a + b, 0);
|
||||||
|
let r = Math.random() * total;
|
||||||
|
for (const [state, w] of Object.entries(weights)) {
|
||||||
|
r -= w;
|
||||||
|
if (r <= 0) return state;
|
||||||
|
}
|
||||||
|
return AGENT_STATES.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
// ═══ AGENT PRESENCE SYSTEM ═══
|
// ═══ AGENT PRESENCE SYSTEM ═══
|
||||||
function createAgentPresences() {
|
function createAgentPresences() {
|
||||||
const agentData = [
|
const agentData = [
|
||||||
@@ -491,16 +561,30 @@ function createAgentPresences() {
|
|||||||
label.position.y = 3.8;
|
label.position.y = 3.8;
|
||||||
group.add(label);
|
group.add(label);
|
||||||
|
|
||||||
|
// Activity Indicator
|
||||||
|
const indicator = createActivityIndicator(color);
|
||||||
|
group.add(indicator.group);
|
||||||
|
|
||||||
scene.add(group);
|
scene.add(group);
|
||||||
agents.push({
|
agents.push({
|
||||||
id: data.id,
|
id: data.id,
|
||||||
group,
|
group,
|
||||||
orb,
|
orb,
|
||||||
halo,
|
halo,
|
||||||
color,
|
color,
|
||||||
station: data.station,
|
station: data.station,
|
||||||
targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
|
targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
|
||||||
wanderTimer: 0
|
// Idle state machine
|
||||||
|
state: AGENT_STATES.IDLE,
|
||||||
|
stateTimer: 2 + Math.random() * 4,
|
||||||
|
lookAngle: 0,
|
||||||
|
lookSpeed: 0.4 + Math.random() * 0.3,
|
||||||
|
pacingPath: buildPacingPath(data.station),
|
||||||
|
pacingIdx: 0,
|
||||||
|
// Activity indicators
|
||||||
|
indicator,
|
||||||
|
activityState: ACTIVITY_STATES.NONE,
|
||||||
|
activityLocked: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1149,19 @@ function sendChatMessage() {
|
|||||||
if (!text) return;
|
if (!text) return;
|
||||||
addChatMessage('user', text);
|
addChatMessage('user', text);
|
||||||
input.value = '';
|
input.value = '';
|
||||||
|
|
||||||
|
// Drive Timmy activity indicators
|
||||||
|
const timmy = agents.find(a => a.id === 'timmy');
|
||||||
|
if (timmy) {
|
||||||
|
timmy.activityLocked = true;
|
||||||
|
setAgentActivity(timmy, ACTIVITY_STATES.THINKING);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = 500 + Math.random() * 1000;
|
||||||
|
if (timmy) {
|
||||||
|
setTimeout(() => setAgentActivity(timmy, ACTIVITY_STATES.PROCESSING), delay * 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const responses = [
|
const responses = [
|
||||||
'Processing your request through the harness...',
|
'Processing your request through the harness...',
|
||||||
@@ -1077,7 +1174,14 @@ function sendChatMessage() {
|
|||||||
];
|
];
|
||||||
const resp = responses[Math.floor(Math.random() * responses.length)];
|
const resp = responses[Math.floor(Math.random() * responses.length)];
|
||||||
addChatMessage('timmy', resp);
|
addChatMessage('timmy', resp);
|
||||||
}, 500 + Math.random() * 1000);
|
if (timmy) {
|
||||||
|
setAgentActivity(timmy, ACTIVITY_STATES.WAITING);
|
||||||
|
setTimeout(() => {
|
||||||
|
setAgentActivity(timmy, ACTIVITY_STATES.NONE);
|
||||||
|
timmy.activityLocked = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
input.blur();
|
input.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1336,24 +1440,7 @@ function gameLoop() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Animate Agents
|
// Animate Agents
|
||||||
agents.forEach((agent, i) => {
|
updateAgents(elapsed, delta);
|
||||||
// Wander logic
|
|
||||||
agent.wanderTimer -= delta;
|
|
||||||
if (agent.wanderTimer <= 0) {
|
|
||||||
agent.wanderTimer = 3 + Math.random() * 5;
|
|
||||||
agent.targetPos.set(
|
|
||||||
agent.station.x + (Math.random() - 0.5) * 4,
|
|
||||||
0,
|
|
||||||
agent.station.z + (Math.random() - 0.5) * 4
|
|
||||||
);
|
|
||||||
}
|
|
||||||
agent.group.position.lerp(agent.targetPos, delta * 0.5);
|
|
||||||
|
|
||||||
agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * 0.15;
|
|
||||||
agent.halo.rotation.z = elapsed * 0.5;
|
|
||||||
agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1);
|
|
||||||
agent.orb.material.emissiveIntensity = 2 + Math.sin(elapsed * 4 + i) * 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate Power Meter
|
// Animate Power Meter
|
||||||
powerMeterBars.forEach((bar, i) => {
|
powerMeterBars.forEach((bar, i) => {
|
||||||
@@ -1420,6 +1507,125 @@ function onResize() {
|
|||||||
composer.setSize(w, h);
|
composer.setSize(w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ AGENT IDLE ANIMATION ═══
|
||||||
|
function updateAgents(elapsed, delta) {
|
||||||
|
const ATTENTION_RADIUS = 7;
|
||||||
|
const terminalFacing = new THREE.Vector3(0, 0, -8); // batcave terminal bank Z
|
||||||
|
|
||||||
|
agents.forEach((agent, i) => {
|
||||||
|
const stationWorld = new THREE.Vector3(agent.station.x, 0, agent.station.z);
|
||||||
|
|
||||||
|
// ── Attention system: face player when close ──
|
||||||
|
const toPlayer = new THREE.Vector3(
|
||||||
|
playerPos.x - agent.group.position.x,
|
||||||
|
0,
|
||||||
|
playerPos.z - agent.group.position.z
|
||||||
|
);
|
||||||
|
const playerDist = toPlayer.length();
|
||||||
|
const playerNearby = playerDist < ATTENTION_RADIUS && !agent.activityLocked;
|
||||||
|
|
||||||
|
if (playerNearby) {
|
||||||
|
const targetAngle = Math.atan2(toPlayer.x, toPlayer.z);
|
||||||
|
const currentAngle = agent.group.rotation.y;
|
||||||
|
const diff = ((targetAngle - currentAngle + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
|
||||||
|
agent.group.rotation.y += diff * Math.min(delta * 3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State machine (skip if activity locked or player nearby) ──
|
||||||
|
if (!playerNearby && !agent.activityLocked) {
|
||||||
|
agent.stateTimer -= delta;
|
||||||
|
|
||||||
|
if (agent.stateTimer <= 0) {
|
||||||
|
agent.state = pickNextState(agent);
|
||||||
|
switch (agent.state) {
|
||||||
|
case AGENT_STATES.IDLE:
|
||||||
|
agent.stateTimer = 4 + Math.random() * 6;
|
||||||
|
agent.targetPos.copy(stationWorld);
|
||||||
|
break;
|
||||||
|
case AGENT_STATES.PACING:
|
||||||
|
agent.stateTimer = 8 + Math.random() * 6;
|
||||||
|
agent.pacingIdx = 0;
|
||||||
|
break;
|
||||||
|
case AGENT_STATES.LOOKING:
|
||||||
|
agent.stateTimer = 4 + Math.random() * 4;
|
||||||
|
agent.lookAngle = agent.group.rotation.y;
|
||||||
|
break;
|
||||||
|
case AGENT_STATES.READING:
|
||||||
|
agent.stateTimer = 5 + Math.random() * 5;
|
||||||
|
agent.targetPos.copy(stationWorld);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Movement per state ──
|
||||||
|
if (agent.state === AGENT_STATES.PACING) {
|
||||||
|
const wp = agent.pacingPath[agent.pacingIdx];
|
||||||
|
const toWp = new THREE.Vector3(wp.x - agent.group.position.x, 0, wp.z - agent.group.position.z);
|
||||||
|
if (toWp.length() < 0.3) {
|
||||||
|
agent.pacingIdx = (agent.pacingIdx + 1) % agent.pacingPath.length;
|
||||||
|
} else {
|
||||||
|
agent.group.position.addScaledVector(toWp.normalize(), delta * 1.2);
|
||||||
|
agent.group.rotation.y += (Math.atan2(toWp.x, toWp.z) - agent.group.rotation.y) * Math.min(delta * 4, 1);
|
||||||
|
}
|
||||||
|
} else if (agent.state === AGENT_STATES.READING) {
|
||||||
|
// Face the terminal bank
|
||||||
|
const toTerminal = new THREE.Vector3(
|
||||||
|
terminalFacing.x - agent.group.position.x,
|
||||||
|
0,
|
||||||
|
terminalFacing.z - agent.group.position.z
|
||||||
|
);
|
||||||
|
const targetAngle = Math.atan2(toTerminal.x, toTerminal.z);
|
||||||
|
agent.group.rotation.y += (targetAngle - agent.group.rotation.y) * Math.min(delta * 2, 1);
|
||||||
|
agent.group.position.lerp(agent.targetPos, delta * 0.4);
|
||||||
|
} else if (agent.state === AGENT_STATES.LOOKING) {
|
||||||
|
// Slow environmental scan left/right
|
||||||
|
agent.lookAngle += Math.sin(elapsed * agent.lookSpeed + i) * delta * 0.8;
|
||||||
|
agent.group.rotation.y += (agent.lookAngle - agent.group.rotation.y) * Math.min(delta * 1.5, 1);
|
||||||
|
agent.group.position.lerp(agent.targetPos, delta * 0.3);
|
||||||
|
} else {
|
||||||
|
// IDLE — drift gently back to station
|
||||||
|
agent.group.position.lerp(agent.targetPos, delta * 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Orb & halo animation ──
|
||||||
|
const bobAmt = agent.activityState === ACTIVITY_STATES.THINKING ? 0.25 : 0.15;
|
||||||
|
agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * bobAmt;
|
||||||
|
agent.halo.rotation.z = elapsed * 0.5;
|
||||||
|
agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1);
|
||||||
|
const baseEmissive = agent.activityState === ACTIVITY_STATES.NONE ? 2 : 3;
|
||||||
|
agent.orb.material.emissiveIntensity = baseEmissive + Math.sin(elapsed * 4 + i) * 1;
|
||||||
|
|
||||||
|
// ── Activity indicator animation ──
|
||||||
|
if (agent.activityState !== ACTIVITY_STATES.NONE) {
|
||||||
|
// Floating bob
|
||||||
|
agent.indicator.group.position.y = 4.2 + Math.sin(elapsed * 2 + i * 1.3) * 0.1;
|
||||||
|
|
||||||
|
if (agent.activityState === ACTIVITY_STATES.WAITING) {
|
||||||
|
const pulse = 0.7 + Math.sin(elapsed * 4 + i) * 0.3;
|
||||||
|
agent.indicator.waitMesh.scale.setScalar(pulse);
|
||||||
|
agent.indicator.waitMesh.material.opacity = 0.5 + pulse * 0.35;
|
||||||
|
} else if (agent.activityState === ACTIVITY_STATES.THINKING) {
|
||||||
|
agent.indicator.thinkMesh.rotation.y = elapsed * 2.5;
|
||||||
|
agent.indicator.thinkMesh.rotation.x = elapsed * 1.5;
|
||||||
|
} else if (agent.activityState === ACTIVITY_STATES.PROCESSING) {
|
||||||
|
agent.indicator.procMesh.rotation.z = elapsed * 4;
|
||||||
|
agent.indicator.procMesh.rotation.x = Math.sin(elapsed * 1.2) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Billboard — indicator faces camera
|
||||||
|
const toCamera = new THREE.Vector3(
|
||||||
|
camera.position.x - agent.group.position.x,
|
||||||
|
0,
|
||||||
|
camera.position.z - agent.group.position.z
|
||||||
|
);
|
||||||
|
if (toCamera.length() > 0.01) {
|
||||||
|
agent.indicator.group.rotation.y = Math.atan2(toCamera.x, toCamera.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ═══ AGENT SIMULATION ═══
|
// ═══ AGENT SIMULATION ═══
|
||||||
function simulateAgentThought() {
|
function simulateAgentThought() {
|
||||||
const agentIds = ['timmy', 'kimi', 'claude', 'perplexity'];
|
const agentIds = ['timmy', 'kimi', 'claude', 'perplexity'];
|
||||||
|
|||||||
Reference in New Issue
Block a user