Files
OpenOPC/opc/plugins/office_ui/frontend_src/game/GameBridge.ts
T
2026-07-01 17:56:31 +08:00

235 lines
7.4 KiB
TypeScript

import Phaser from 'phaser'
import type { OfficeScene } from './scenes/OfficeScene'
import type { VisualEvent, VisualSnapshot } from '../types/visual'
import { getOffices, type OfficeConfig } from './map/OfficeStore'
import { AgentState } from './types'
export class GameBridge extends Phaser.Events.EventEmitter {
private scene: OfficeScene | null = null
private eventQueue: VisualEvent[] = []
private snapshotQueue: VisualSnapshot[] = []
constructor() {
super()
}
setScene(scene: OfficeScene) {
this.scene = scene
for (const snap of this.snapshotQueue) {
this.applySnapshot(snap)
}
this.snapshotQueue = []
for (const evt of this.eventQueue) {
this.applyEvent(evt)
}
this.eventQueue = []
}
getScene(): OfficeScene | null {
return this.scene
}
// ── Called from React side ────────────────────────────
pushEvent(evt: VisualEvent) {
if (!this.scene) {
this.eventQueue.push(evt)
return
}
this.applyEvent(evt)
}
pushSnapshot(snapshot: VisualSnapshot) {
const agentCount = Object.keys(snapshot.agents ?? {}).length
if (!this.scene) {
console.log(`[GameBridge] pushSnapshot queued (scene not ready) — ${agentCount} agents`)
this.snapshotQueue.push(snapshot)
return
}
console.log(`[GameBridge] pushSnapshot applying now — ${agentCount} agents`)
this.applySnapshot(snapshot)
}
sendToSeat(agentId: string) {
if (!this.scene) return
this.scene.behavior.sendToSeat(
this.scene.ensureAgent(agentId),
)
}
setAgentActive(agentId: string, active: boolean) {
if (!this.scene) return
const agent = this.scene.getAgent(agentId)
if (agent) agent.isActive = active
}
setAgentBubble(agentId: string, text: string | null) {
if (!this.scene) return
const agent = this.scene.getAgent(agentId)
if (!agent) return
if (text) agent.showBubble(text)
else agent.clearBubble()
}
ensureAgent(agentId: string, displayName?: string, officeId?: string, palette?: number, deskId?: string) {
if (!this.scene) return
this.scene.ensureAgent(agentId, displayName, false, null, officeId, palette, deskId)
}
getCharacterCards() {
if (!this.scene) return []
return this.scene.getCharacterCards()
}
// ── Office management API ─────────────────────────────
getOffices(): OfficeConfig[] {
return getOffices()
}
renameOffice(officeId: string, newName: string) {
if (!this.scene) return
this.scene.renameOffice(officeId, newName)
this.emit('officeChanged')
}
assignAgentToOffice(agentId: string, officeId: string) {
if (!this.scene) return
this.scene.reassignAgent(agentId, officeId)
this.emit('officeChanged')
}
panToOffice(officeId: string) {
if (!this.scene) return
this.scene.panToOffice(officeId)
}
resetCamera() {
if (!this.scene) return
this.scene.resetCameraView()
}
/** Re-read `isLocalDaytime()` (URL + localStorage) and refresh skyline / grass if it changed. */
syncOutdoorLighting() {
if (!this.scene) return
this.scene.syncOutdoorLighting()
}
rebuildOfficeCollision(officeId: string, mapStr: string[], seats: [number, number][]) {
if (!this.scene) return
this.scene.rebuildOfficeCollision(officeId, mapStr, seats)
}
getSeatsForOffice(officeId: string): Array<{ id: string; assigned: boolean; assignedTo: string | null }> {
if (!this.scene) return []
return this.scene.seats
.filter(s => s.id.startsWith(`${officeId}-desk-`) || s.id.startsWith(`${officeId}-leader-`))
.map(s => ({ id: s.id, assigned: s.assigned, assignedTo: s.assignedTo }))
}
changeAgentSeat(agentId: string, seatId: string) {
if (!this.scene) return
this.scene.changeAgentSeat(agentId, seatId)
this.emit('officeChanged')
}
// ── Chat + Kanban game integration ──────────────────────
notifyChannelMessage(agentIds: string[], text: string) {
if (!this.scene) return
for (const id of agentIds) {
const agent = this.scene.getAgent(id)
if (agent) {
agent.showBubble(text.slice(0, 30))
setTimeout(() => agent.clearBubble(), 4000)
}
}
}
triggerCrossOfficeMeeting(agentIds: string[]) {
if (!this.scene) return
for (const id of agentIds) {
const agent = this.scene.getAgent(id)
if (agent) {
this.scene.behavior.moveToZone(agent, 'meetingRoom')
}
}
}
triggerCelebration(agentId: string) {
if (!this.scene) return
const agent = this.scene.getAgent(agentId)
if (agent) {
agent.showBubble('🎉 Done!')
setTimeout(() => agent.clearBubble(), 3000)
}
}
// ── Internal ──────────────────────────────────────────
private applyEvent(evt: VisualEvent) {
if (!this.scene) return
this.scene.behavior.applyEvent(evt)
this.emit('eventApplied', evt)
}
private applySnapshot(snapshot: VisualSnapshot) {
if (!this.scene) {
console.warn('[GameBridge] applySnapshot called but scene is null')
return
}
// Snapshot old agents to array first (avoid mutating Map during iteration)
const oldIds = Array.from(this.scene.agents.keys())
console.log('[GameBridge] applySnapshot — clearing', oldIds.length, 'old agents:', oldIds)
for (const id of oldIds) {
this.scene.removeAgent(id)
}
const timeline = snapshot.timeline ?? []
for (const evt of timeline) {
this.scene.behavior.applyEvent(evt)
}
const agentEntries = Object.entries(snapshot.agents ?? {})
console.log('[GameBridge] applySnapshot — adding', agentEntries.length, 'agents:', agentEntries.map(([id]) => id))
for (const [id, info] of agentEntries) {
const agentData = info as {
name?: string; role_name?: string; office_id?: string
status?: string; runtime_status?: string; current_tool?: string | null
appearance?: { palette?: number; hue_shift?: number; seat_zone?: string; desk_id?: string }
}
const name = agentData.name || agentData.role_name || id
const officeId = agentData.office_id
const palette = agentData.appearance?.palette
const deskId = agentData.appearance?.desk_id
try {
const agent = this.scene.ensureAgent(id, name, false, null, officeId, palette, deskId)
const runtimeStatus = agentData.runtime_status || agentData.status
if (runtimeStatus === 'tool_active') {
agent.currentTool = agentData.current_tool ?? null
agent.isActive = true
agent.setAgentState(AgentState.TYPE)
} else if (runtimeStatus === 'reflecting') {
agent.currentTool = agentData.current_tool ?? 'Reflect'
agent.isActive = false
agent.setAgentState(AgentState.REFLECT)
}
console.log(`[GameBridge] ✓ ensured ${id} in ${officeId} palette=${palette}`)
} catch (err) {
console.error(`[GameBridge] ✗ ensureAgent failed for ${id}:`, err)
}
}
if (timeline.length === 0 && agentEntries.length === 0) {
console.warn('[GameBridge] applySnapshot — empty snapshot, creating fallback agent')
this.scene.ensureAgent('openopc-main', 'OpenOPC')
}
console.log('[GameBridge] applySnapshot done — total agents:', this.scene.agents.size)
this.emit('snapshotApplied', snapshot)
}
}