Initial commit

This commit is contained in:
LZH-YS1998
2026-07-01 17:56:31 +08:00
commit d78931979d
731 changed files with 311088 additions and 0 deletions
@@ -0,0 +1,548 @@
import { AgentState, Direction } from '../types'
import {
CELEBRATE_DURATION, COFFEE_DURATION_MIN, COFFEE_DURATION_MAX,
CHAT_DURATION_MIN, CHAT_DURATION_MAX,
WANDER_PAUSE_MIN, WANDER_PAUSE_MAX,
WANDER_MOVES_BEFORE_REST_MIN, WANDER_MOVES_BEFORE_REST_MAX,
SEAT_REST_MIN, SEAT_REST_MAX,
INACTIVE_SEAT_TIMER_MIN, INACTIVE_SEAT_TIMER_RANGE,
TILE_SIZE,
} from '../config'
import type { OfficeScene } from '../scenes/OfficeScene'
import type { Agent } from '../entities/Agent'
import { ZONES, randomTileInZone, getOfficeLobbyDoorways } from '../map/InteractionZones'
import type { VisualEvent } from '../../types/visual'
function randomRange(min: number, max: number) { return min + Math.random() * (max - min) }
function randomInt(min: number, max: number) { return Math.floor(randomRange(min, max + 1)) }
function trimPreview(s: string, maxLen: number) { return s.length > maxLen ? s.slice(0, maxLen) + '...' : s }
const READING_TOOLS = new Set([
'read', 'read_file', 'browse', 'list', 'list_dir',
'glob', 'grep', 'search', 'fetch', 'web_fetch',
'webfetch', 'web_search', 'websearch',
])
function mapToolToState(toolName: string | null): AgentState {
if (!toolName) return AgentState.TYPE
const t = toolName.toLowerCase().trim()
if (t === 'reflect') return AgentState.REFLECT
if (t === 'practice') return AgentState.PRACTICE
if (t === 'synthesize') return AgentState.PRESENT
if (READING_TOOLS.has(t)) return AgentState.THINK
return AgentState.TYPE
}
function mapToolDisplay(toolName: string | null): string {
if (!toolName) return 'Tool'
const t = toolName.toLowerCase().trim()
if (t === 'shell') return 'Shell'
if (t === 'write_file' || t === 'write') return 'Write'
if (t === 'edit_file' || t === 'edit') return 'Edit'
if (t === 'read_file' || t === 'read') return 'Read'
if (t === 'grep' || t === 'search') return 'Search'
if (t === 'web_search' || t === 'websearch') return 'WebSearch'
if (t === 'web_fetch' || t === 'webfetch') return 'Fetch'
return toolName.slice(0, 12)
}
export class BehaviorController {
private scene: OfficeScene
private pendingDespawn = new Set<string>()
constructor(scene: OfficeScene) {
this.scene = scene
}
// ── Event dispatch ────────────────────────────────────
applyEvent(evt: VisualEvent) {
const data = evt.data ?? {}
const agentId = evt.agent_id || 'openopc-main'
if (agentId === 'user') return
const isSub = agentId.startsWith('subagent-')
const parentId = isSub
? (typeof data.parent_agent_id === 'string' ? data.parent_agent_id : 'openopc-main')
: null
const displayName = isSub ? `Sub ${agentId.slice(-4)}` : undefined
const agent = this.scene.ensureAgent(agentId, displayName, isSub, parentId)
agent.lastEventAt = Date.now()
if (typeof data.task_preview === 'string') agent.taskSummary = data.task_preview
if (typeof data.result_preview === 'string') agent.taskSummary = data.result_preview
switch (evt.type) {
case 'tool_start': this.onToolStart(agent, data); break
case 'tool_done': this.onToolDone(agent, data); break
case 'agent_active': this.onAgentActive(agent); break
case 'waiting': this.onWaiting(agent); break
case 'reflect_start': this.onReflectStart(agent); break
case 'reflect_done': this.onReflectDone(agent); break
case 'skill_synthesized': this.onSkillSynthesized(agent, data); break
case 'subagent_spawn': this.onSubagentSpawn(agent, agentId, parentId); break
case 'subagent_done': this.onSubagentDone(agent, agentId); break
case 'message_in': this.onMessageIn(agent, data); break
case 'message_out': this.onMessageOut(agent, data); break
case 'practice_start': this.onPracticeStart(agent, data); break
case 'practice_done': this.onPracticeDone(agent); break
case 'task_routed': this.onTaskRouted(agent, data); break
case 'task_delegated': this.onTaskDelegated(agent, agentId, data); break
case 'delegation_done': this.onDelegationDone(agent, data); break
case 'agent_spawned': this.onAgentSpawned(agent, agentId, data); break
case 'agent_removed': this.onAgentRemoved(agent, agentId); break
case 'collab_started': this.onCollabStarted(agent); break
case 'collab_ended': this.onCollabEnded(agent); break
case 'skill_published': this.onSkillPublished(agent, data); break
case 'skill_adopted': this.onSkillAdopted(agent, data); break
case 'mycelium_transport': this.onMyceliumTransport(agent, agentId, data); break
case 'mycelium_crystallize': this.onMyceliumCrystallize(agent, agentId, data); break
case 'mycelium_spore': this.onMyceliumSpore(agent, agentId, data); break
case 'mycelium_decompose': this.onMyceliumDecompose(agent, agentId, data); break
case 'mycelium_unit_created': this.onMyceliumUnitCreated(agent, data); break
case 'mycelium_germinate': this.onMyceliumGerminate(agent, agentId, data); break
case 'hyphal_strengthen': this.onHyphalStrengthen(agentId, data); break
case 'hyphal_weaken': this.onHyphalWeaken(agentId, data); break
}
}
// ── Individual event handlers ─────────────────────────
private onToolStart(agent: Agent, data: Record<string, unknown>) {
const toolName = typeof data.tool_name === 'string' ? data.tool_name : 'tool'
const label = mapToolDisplay(toolName)
agent.urgency = 'urgent'
agent.currentTool = toolName
agent.isActive = true
agent.showBubble(`${label}...`)
this.sendToSeat(agent)
}
private onToolDone(agent: Agent, data: Record<string, unknown>) {
const toolName = typeof data.tool_name === 'string' ? data.tool_name : agent.currentTool
const label = mapToolDisplay(toolName)
agent.currentTool = null
agent.setAgentState(AgentState.CELEBRATE)
agent.stateTimer = CELEBRATE_DURATION
agent.isActive = false
agent.showBubble(`${label} done`)
}
private onAgentActive(agent: Agent) {
agent.urgency = 'urgent'
agent.isActive = true
if (!agent.currentTool) {
this.sendToSeat(agent)
}
}
private onWaiting(agent: Agent) {
agent.urgency = 'relaxed'
agent.isActive = false
agent.currentTool = null
agent.showBubble('Waiting')
const moved = this.moveToZone(agent, 'breakRoom', AgentState.COFFEE)
if (!moved) agent.setAgentState(AgentState.IDLE)
if (moved) agent.stateTimer = randomRange(COFFEE_DURATION_MIN, COFFEE_DURATION_MAX)
}
private onReflectStart(agent: Agent) {
agent.urgency = 'normal'
agent.isActive = false
agent.currentTool = 'Reflect'
const moved = this.moveToZone(agent, 'meetingRoom', AgentState.REFLECT)
if (!moved) agent.setAgentState(AgentState.REFLECT)
agent.stateTimer = 30.0
agent.showBubble('Reflecting...')
}
private onReflectDone(agent: Agent) {
agent.currentTool = null
agent.setAgentState(AgentState.CELEBRATE)
agent.stateTimer = CELEBRATE_DURATION
agent.showBubble('Insight!')
}
private onSkillSynthesized(agent: Agent, data: Record<string, unknown>) {
const name = trimPreview(String(data.skill_name ?? 'new'), 20)
agent.setAgentState(AgentState.PRESENT)
agent.showBubble(`New Skill: ${name}`)
}
private onSubagentSpawn(agent: Agent, agentId: string, parentId: string | null) {
this.placeAtDoorway(agent)
agent.urgency = 'urgent'
agent.isActive = true
agent.showBubble('Spawned')
this.sendToSeat(agent)
}
private onSubagentDone(agent: Agent, agentId: string) {
agent.urgency = 'relaxed'
agent.currentTool = null
agent.isActive = false
agent.showBubble('Finished')
const moved = this.moveToDoorway(agent)
if (moved) {
this.pendingDespawn.add(agentId)
} else {
this.scene.removeAgent(agentId)
}
}
private onMessageIn(agent: Agent, data: Record<string, unknown>) {
agent.urgency = 'urgent'
const content = typeof data.content_preview === 'string' ? data.content_preview : ''
if (content) agent.taskSummary = content
const preview = content ? trimPreview(content, 30) : 'New task'
agent.showBubble(preview)
agent.isActive = true
this.sendToSeat(agent)
}
private onMessageOut(agent: Agent, data: Record<string, unknown>) {
const content = typeof data.content_preview === 'string' ? data.content_preview : ''
if (content) agent.taskSummary = content
const preview = content ? trimPreview(content, 30) : 'Reply sent'
agent.showBubble(`Reply: ${preview}`)
}
private onPracticeStart(agent: Agent, data: Record<string, unknown>) {
agent.urgency = 'normal'
const domain = typeof data.target_domain === 'string' ? data.target_domain : 'Practice'
agent.showBubble(`Practicing: ${trimPreview(domain, 20)}`)
agent.isActive = false
agent.currentTool = 'Practice'
const moved = this.moveToZone(agent, 'meetingRoom', AgentState.PRACTICE)
if (!moved) agent.setAgentState(AgentState.PRACTICE)
}
private onPracticeDone(agent: Agent) {
agent.currentTool = null
agent.setAgentState(AgentState.CELEBRATE)
agent.stateTimer = CELEBRATE_DURATION
agent.showBubble('Practice done!')
}
private onTaskRouted(agent: Agent, data: Record<string, unknown>) {
agent.urgency = 'urgent'
const method = typeof data.method === 'string' ? data.method : 'auto'
agent.isActive = true
this.sendToSeat(agent)
agent.showBubble(`Assigned (${method})`)
}
private onTaskDelegated(agent: Agent, agentId: string, data: Record<string, unknown>) {
agent.urgency = 'normal'
agent.stateTimer = randomRange(CHAT_DURATION_MIN, CHAT_DURATION_MAX)
const target = typeof data.target === 'string' ? data.target : '?'
agent.setAgentState(AgentState.CHAT)
agent.showBubble(`Delegating to ${target}...`)
const targetAgent = this.scene.getAgent(target)
if (targetAgent) {
targetAgent.parentAgentId = agentId
targetAgent.showBubble('Receiving task...')
}
}
private onDelegationDone(agent: Agent, data: Record<string, unknown>) {
const target = typeof data.target === 'string' ? data.target : ''
agent.setAgentState(AgentState.IDLE)
agent.showBubble('Delegation complete')
if (target) {
const targetAgent = this.scene.getAgent(target)
if (targetAgent) targetAgent.parentAgentId = null
}
}
private onAgentSpawned(agent: Agent, agentId: string, data: Record<string, unknown>) {
const roleName = typeof data.role_name === 'string' ? data.role_name : agentId
agent.displayName = roleName
this.placeAtDoorway(agent)
this.sendToSeat(agent)
agent.showBubble(`${roleName} joined`)
}
private onAgentRemoved(agent: Agent, agentId: string) {
agent.showBubble('Leaving...')
const moved = this.moveToDoorway(agent)
if (!moved) {
this.scene.removeAgent(agentId)
} else {
this.pendingDespawn.add(agentId)
}
}
private onCollabStarted(agent: Agent) {
agent.urgency = 'normal'
agent.isActive = false
agent.stateTimer = randomRange(CHAT_DURATION_MIN, CHAT_DURATION_MAX)
this.moveToZone(agent, 'meetingRoom', AgentState.CHAT)
agent.showBubble('Collaborating...')
}
private onCollabEnded(agent: Agent) {
agent.stateTimer = CELEBRATE_DURATION
agent.setAgentState(AgentState.CELEBRATE)
agent.showBubble('Collaboration done!')
}
private onSkillPublished(agent: Agent, data: Record<string, unknown>) {
const name = trimPreview(String(data.skill_name ?? 'skill'), 20)
agent.setAgentState(AgentState.CELEBRATE)
agent.stateTimer = CELEBRATE_DURATION
agent.showBubble(`Published: ${name}`)
}
private onSkillAdopted(agent: Agent, data: Record<string, unknown>) {
const name = trimPreview(String(data.skill_name ?? 'skill'), 20)
agent.showBubble(`Adopted: ${name}`)
}
// ── Mycelium events ───────────────────────────────────
private onMyceliumTransport(agent: Agent, agentId: string, data: Record<string, unknown>) {
const source = typeof data.source === 'string' ? data.source : agentId
const target = typeof data.target === 'string' ? data.target : null
const domain = typeof data.domain === 'string' ? trimPreview(data.domain, 15) : '?'
const srcAgent = this.scene.ensureAgent(source)
srcAgent.myceliumEffect = 'transport_send'
srcAgent.myceliumEffectTimer = 3.0
srcAgent.showBubble(`-> [${domain}]`)
if (target) {
const tgtAgent = this.scene.ensureAgent(target)
tgtAgent.myceliumEffect = 'transport_recv'
tgtAgent.myceliumEffectTimer = 2.0
tgtAgent.showBubble('Receiving...')
const tgtPos = tgtAgent.getTilePos()
srcAgent.walkTo(tgtPos.x, tgtPos.y + 1)
}
}
private onMyceliumCrystallize(agent: Agent, agentId: string, data: Record<string, unknown>) {
const corrobAgents: string[] = Array.isArray(data.corroborating_agents)
? data.corroborating_agents : []
const contentPreview = trimPreview(String(data.content_preview ?? 'Knowledge'), 22)
const allParticipants = Array.from(new Set([agentId, ...corrobAgents]))
const meetingZoneKey = `${agent.officeId}-meetingRoom`
const meetingSeats = ZONES[meetingZoneKey]?.seats ?? []
for (let i = 0; i < allParticipants.length; i++) {
const pid = allParticipants[i]
const pAgent = this.scene.ensureAgent(pid)
pAgent.myceliumEffect = 'crystal'
pAgent.myceliumEffectTimer = 15.0
pAgent.isActive = false
const seatIdx = i % meetingSeats.length
const seat = meetingSeats[seatIdx]
pAgent.walkTo(seat.tileX, seat.tileY, () => {
pAgent.setDirection(seat.facing)
pAgent.setAgentState(AgentState.CHAT)
})
pAgent.showBubble(pid === agentId ? `Crystal: ${contentPreview}` : 'Crystal!')
}
}
private onMyceliumSpore(agent: Agent, agentId: string, data: Record<string, unknown>) {
const preview = trimPreview(String(data.content_preview ?? 'Breakthrough'), 20)
agent.myceliumEffect = 'spore_send'
agent.myceliumEffectTimer = 4.0
agent.setAgentState(AgentState.PRESENT)
agent.showBubble(`Breakthrough! ${preview}`)
// Move to break room for broadcast
this.moveToZone(agent, 'breakRoom')
}
private onMyceliumDecompose(agent: Agent, agentId: string, data: Record<string, unknown>) {
const preview = trimPreview(String(data.humus_preview ?? 'Lesson learned'), 22)
agent.myceliumEffect = 'decompose'
agent.myceliumEffectTimer = 4.0
agent.setAgentState(AgentState.REFLECT)
agent.showBubble(`Learning: ${preview}`)
this.moveToZone(agent, 'meetingRoom')
}
private onMyceliumUnitCreated(agent: Agent, data: Record<string, unknown>) {
const nutrientType = String(data.nutrient_type ?? 'insight').slice(0, 1).toUpperCase()
agent.showBubble(`[${nutrientType}]...`, 2.0)
}
private onMyceliumGerminate(agent: Agent, agentId: string, data: Record<string, unknown>) {
const targetAgent = typeof data.target_agent === 'string' ? data.target_agent : agentId
const tgt = this.scene.ensureAgent(targetAgent)
tgt.myceliumEffect = null
tgt.setAgentState(AgentState.CELEBRATE)
tgt.stateTimer = CELEBRATE_DURATION
tgt.showBubble('Insight took root!')
}
private onHyphalStrengthen(_agentId: string, _data: Record<string, unknown>) {
// Visual connection tracking could be added here
}
private onHyphalWeaken(_agentId: string, _data: Record<string, unknown>) {
// Visual connection tracking could be added here
}
// ── Idle behavior (called each frame) ─────────────────
updateIdle(dt: number) {
for (const agent of this.scene.agents.values()) {
// Check pending despawn
if (this.pendingDespawn.has(agent.agentId) && !agent.isMoving) {
this.pendingDespawn.delete(agent.agentId)
this.scene.removeAgent(agent.agentId)
continue
}
// Mycelium effect timer
if (agent.myceliumEffectTimer > 0) {
agent.myceliumEffectTimer -= dt
if (agent.myceliumEffectTimer <= 0) {
agent.myceliumEffect = null
agent.myceliumEffectTimer = 0
agent.myceliumSession = null
}
}
// Non-active agents sitting at desk: count down seatTimer then transition to IDLE
if (agent.agentState === AgentState.TYPE && !agent.isActive) {
if (agent.seatTimer > 0) {
agent.seatTimer -= dt
if (agent.seatTimer <= 0) {
agent.seatTimer = 0
agent.setAgentState(AgentState.IDLE)
agent.wanderCount = 0
agent.wanderLimit = randomInt(WANDER_MOVES_BEFORE_REST_MIN, WANDER_MOVES_BEFORE_REST_MAX)
agent.wanderTimer = randomRange(WANDER_PAUSE_MIN, WANDER_PAUSE_MAX)
}
} else {
agent.setAgentState(AgentState.IDLE)
}
continue
}
if (agent.agentState !== AgentState.IDLE) continue
// Active agents should go to seat
if (agent.isActive) {
agent.urgency = 'urgent'
if (!agent.seatId) {
agent.setAgentState(AgentState.TYPE)
continue
}
this.sendToSeat(agent)
continue
}
// Idle wander logic
agent.wanderTimer -= dt
if (agent.wanderTimer <= 0) {
// Wander limit reached => go back to seat and rest
if (agent.wanderCount >= agent.wanderLimit && agent.seatId) {
agent.urgency = 'relaxed'
this.sendToSeat(agent)
agent.wanderTimer = randomRange(WANDER_PAUSE_MIN, WANDER_PAUSE_MAX)
continue
}
// Random wander (confined to agent's office)
const tiles = this.scene.getWalkableTilesForOffice(agent.officeId)
if (tiles.length > 0) {
agent.urgency = 'relaxed'
const target = tiles[Math.floor(Math.random() * tiles.length)]
agent.walkTo(target.x, target.y)
agent.wanderCount++
}
agent.wanderTimer = randomRange(WANDER_PAUSE_MIN, WANDER_PAUSE_MAX)
}
}
}
// ── Movement helpers ──────────────────────────────────
sendToSeat(agent: Agent) {
if (!agent.seatId) return
const seat = this.scene.getSeatById(agent.seatId)
if (!seat) return
agent.walkTo(seat.tileX, seat.tileY, () => {
agent.setAgentState(AgentState.TYPE)
agent.setDirection(seat.facing)
if (!agent.isActive) {
agent.seatTimer = INACTIVE_SEAT_TIMER_MIN + Math.random() * INACTIVE_SEAT_TIMER_RANGE
}
}).then(moved => {
if (!moved) {
agent.setAgentState(AgentState.TYPE)
agent.setDirection(seat.facing)
if (!agent.isActive) {
agent.seatTimer = INACTIVE_SEAT_TIMER_MIN + Math.random() * INACTIVE_SEAT_TIMER_RANGE
}
}
})
}
moveToZone(agent: Agent, zoneName: string, arrivalState?: string): boolean {
const zoneKey = `${agent.officeId}-${zoneName}`
const zone = ZONES[zoneKey]
if (!zone) return false
if (zone.seats.length > 0) {
const occupiedTiles = new Set<string>()
for (const other of this.scene.agents.values()) {
if (other === agent) continue
if (other.isMoving) continue
const pos = other.getTilePos()
occupiedTiles.add(`${pos.x},${pos.y}`)
}
for (const other of this.scene.agents.values()) {
if (other === agent) continue
if (!other.isMoving) continue
const path = (other as any).currentPath as { x: number; y: number }[]
if (path?.length) {
const dest = path[path.length - 1]
occupiedTiles.add(`${dest.x},${dest.y}`)
}
}
const freeSeat = zone.seats.find(s => !occupiedTiles.has(`${s.tileX},${s.tileY}`))
if (freeSeat) {
agent.walkTo(freeSeat.tileX, freeSeat.tileY, () => {
agent.setDirection(freeSeat.facing)
if (arrivalState) agent.setAgentState(arrivalState as any)
else agent.setAgentState(AgentState.IDLE)
})
return true
}
}
const pos = randomTileInZone(zoneKey)
if (!pos) return false
agent.walkTo(pos.x, pos.y, () => {
if (arrivalState) agent.setAgentState(arrivalState as any)
else agent.setAgentState(AgentState.IDLE)
})
return true
}
moveToDoorway(agent: Agent): boolean {
const doorways = getOfficeLobbyDoorways(agent.officeId)
if (doorways.length === 0) return false
const target = doorways[Math.floor(Math.random() * doorways.length)]
agent.walkTo(target.x, target.y)
return true
}
placeAtDoorway(agent: Agent) {
const doorways = getOfficeLobbyDoorways(agent.officeId)
if (doorways.length > 0) {
const d = doorways[Math.floor(Math.random() * doorways.length)]
agent.setPosition(d.x * TILE_SIZE + TILE_SIZE / 2, d.y * TILE_SIZE + TILE_SIZE / 2)
}
}
}
@@ -0,0 +1,97 @@
import * as EasyStar from 'easystarjs'
export class PathfindingManager {
private easystar: EasyStar.js
private grid: number[][]
private gridCols: number
private gridRows: number
constructor(collisionGrid: number[][]) {
this.grid = collisionGrid.map(row => [...row])
this.gridRows = this.grid.length
this.gridCols = this.gridRows > 0 ? this.grid[0].length : 0
this.easystar = new EasyStar.js()
this.easystar.setGrid(this.grid)
this.easystar.setAcceptableTiles([0])
this.easystar.setIterationsPerCalculation(800)
}
findPath(
from: { x: number; y: number },
to: { x: number; y: number },
): Promise<{ x: number; y: number }[]> {
return new Promise((resolve) => {
if (
from.x < 0 || from.x >= this.gridCols ||
from.y < 0 || from.y >= this.gridRows ||
to.x < 0 || to.x >= this.gridCols ||
to.y < 0 || to.y >= this.gridRows
) {
resolve([])
return
}
if (this.grid[to.y]?.[to.x] === 1) {
const alt = this.findNearestWalkable(to.x, to.y)
if (!alt) { resolve([]); return }
to = alt
}
if (this.grid[from.y]?.[from.x] === 1) {
const alt = this.findNearestWalkable(from.x, from.y)
if (!alt) { resolve([]); return }
from = alt
}
this.easystar.findPath(from.x, from.y, to.x, to.y, (path) => {
resolve(path ?? [])
})
this.easystar.calculate()
})
}
blockTile(x: number, y: number) {
if (y >= 0 && y < this.grid.length && x >= 0 && x < this.grid[0].length) {
this.grid[y][x] = 1
this.easystar.setGrid(this.grid)
}
}
unblockTile(x: number, y: number) {
if (y >= 0 && y < this.grid.length && x >= 0 && x < this.grid[0].length) {
this.grid[y][x] = 0
this.easystar.setGrid(this.grid)
}
}
isWalkable(x: number, y: number): boolean {
if (y < 0 || y >= this.grid.length || x < 0 || x < 0 || x >= this.grid[0].length) return false
return this.grid[y][x] === 0
}
getWalkableTiles(): { x: number; y: number }[] {
const tiles: { x: number; y: number }[] = []
for (let r = 0; r < this.grid.length; r++) {
for (let c = 0; c < this.grid[r].length; c++) {
if (this.grid[r][c] === 0) {
tiles.push({ x: c, y: r })
}
}
}
return tiles
}
private findNearestWalkable(x: number, y: number): { x: number; y: number } | null {
for (let radius = 1; radius <= 5; radius++) {
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue
const nx = x + dx
const ny = y + dy
if (this.isWalkable(nx, ny)) return { x: nx, y: ny }
}
}
}
return null
}
}