@agent-os-lab/agent-game-sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/package.json +38 -0
- package/src/core/agent-game-store.ts +110 -0
- package/src/core/agent-service-event-adapter.ts +20 -0
- package/src/core/assets.ts +119 -0
- package/src/core/commands.ts +42 -0
- package/src/core/errors.ts +19 -0
- package/src/core/event-adapter.ts +40 -0
- package/src/core/index.ts +23 -0
- package/src/core/life-presets.ts +54 -0
- package/src/core/movement.ts +50 -0
- package/src/core/office-building-layout.ts +376 -0
- package/src/core/office-layout.ts +152 -0
- package/src/core/pixel-character-avatar.ts +87 -0
- package/src/core/pixel-character.ts +684 -0
- package/src/core/realtime-events.ts +44 -0
- package/src/core/realtime-transport.ts +39 -0
- package/src/core/reducer.ts +105 -0
- package/src/core/scene.ts +144 -0
- package/src/core/schedule.ts +20 -0
- package/src/core/sequence.ts +48 -0
- package/src/core/state.ts +26 -0
- package/src/core/svg-pixel-avatar.ts +372 -0
- package/src/core/town-office-assets.ts +109 -0
- package/src/core/town-office-room-presets.ts +455 -0
- package/src/core/town-office-seat-layout.ts +238 -0
- package/src/graph.ts +112 -0
- package/src/index.ts +2 -0
- package/src/office/core/projection.ts +89 -0
- package/src/office/core/source.ts +46 -0
- package/src/office/core/types.ts +110 -0
- package/src/office/index.ts +4 -0
- package/src/office/mount.ts +104 -0
- package/src/office/react/AgentGameOfficeView.ts +58 -0
- package/src/office/react/index.ts +1 -0
- package/src/office/renderers/three/agent-activity-effects.ts +161 -0
- package/src/office/renderers/three/agent-animation.ts +205 -0
- package/src/office/renderers/three/agent-body-instancing.ts +119 -0
- package/src/office/renderers/three/agent-label.ts +82 -0
- package/src/office/renderers/three/agent-layout.ts +72 -0
- package/src/office/renderers/three/agent-mesh.ts +145 -0
- package/src/office/renderers/three/mount.ts +253 -0
- package/src/office/renderers/three/scene.ts +790 -0
- package/src/phaser/agent-game-scene.ts +87 -0
- package/src/phaser/anchor-debug.ts +22 -0
- package/src/phaser/avatar-registry.ts +46 -0
- package/src/phaser/camera-controls.ts +419 -0
- package/src/phaser/camera-model.ts +81 -0
- package/src/phaser/create-agent-game.ts +242 -0
- package/src/phaser/debug-overlay.ts +21 -0
- package/src/phaser/index.ts +13 -0
- package/src/phaser/movement-tween.ts +59 -0
- package/src/phaser/office-background.ts +48 -0
- package/src/phaser/office-building-renderer.ts +87 -0
- package/src/phaser/office-layout-renderer.ts +58 -0
- package/src/phaser/render-layers.ts +30 -0
- package/src/phaser/scene-reconciler.ts +614 -0
- package/src/phaser/scene-renderer.ts +138 -0
- package/src/phaser/text-style.ts +8 -0
- package/src/phaser/town-office-business-props.ts +256 -0
- package/src/phaser/town-office-environment.ts +89 -0
- package/src/phaser/town-office-furniture.ts +182 -0
- package/src/phaser/town-office-primitives.ts +53 -0
- package/src/phaser/town-office-renderer.ts +429 -0
- package/src/phaser/types.ts +67 -0
- package/src/phaser/viewport.ts +88 -0
- package/src/runtime-client.ts +435 -0
- package/src/types.ts +80 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeAgentAvatarAnimationFrames,
|
|
3
|
+
type AgentAvatarAnimationName,
|
|
4
|
+
type AgentAvatarDefinition,
|
|
5
|
+
type AgentGameActivity,
|
|
6
|
+
type AgentGameAgent,
|
|
7
|
+
type AgentGameDirection,
|
|
8
|
+
type AgentGameSnapshot,
|
|
9
|
+
} from "../core/index.js";
|
|
10
|
+
import { createMovementTweenConfig, type MovementTimingOptions } from "./movement-tween.js";
|
|
11
|
+
import { agentDepth, agentUiDepth } from "./render-layers.js";
|
|
12
|
+
import { crispTextStyle } from "./text-style.js";
|
|
13
|
+
|
|
14
|
+
const SPRITE_AGENT_SCALE = 1;
|
|
15
|
+
const SPRITE_AGENT_FOOT_ORIGIN_Y = 1;
|
|
16
|
+
const SPRITE_AGENT_LABEL_OFFSET_Y = 13;
|
|
17
|
+
const SPRITE_AGENT_BUBBLE_OFFSET_Y = -51;
|
|
18
|
+
const SPRITE_AGENT_EMOTION_OFFSET_X = 14;
|
|
19
|
+
const SPRITE_AGENT_EMOTION_OFFSET_Y = -30;
|
|
20
|
+
const PLACEHOLDER_AGENT_LABEL_OFFSET_Y = 30;
|
|
21
|
+
const PLACEHOLDER_AGENT_BUBBLE_OFFSET_Y = -45;
|
|
22
|
+
const PLACEHOLDER_AGENT_EMOTION_OFFSET_X = 18;
|
|
23
|
+
const PLACEHOLDER_AGENT_EMOTION_OFFSET_Y = -24;
|
|
24
|
+
const ARRIVAL_EPSILON = 0.5;
|
|
25
|
+
|
|
26
|
+
export type PhaserDisplayObjectLike = {
|
|
27
|
+
x?: number;
|
|
28
|
+
y?: number;
|
|
29
|
+
setPosition?: (x: number, y: number) => PhaserDisplayObjectLike;
|
|
30
|
+
setDepth?: (depth: number) => PhaserDisplayObjectLike;
|
|
31
|
+
setFillStyle?: (fillColor: number, fillAlpha?: number) => PhaserDisplayObjectLike;
|
|
32
|
+
setStrokeStyle?: (lineWidth: number, color: number) => PhaserDisplayObjectLike;
|
|
33
|
+
setScale?: (scale: number) => PhaserDisplayObjectLike;
|
|
34
|
+
setDisplaySize?: (width: number, height: number) => PhaserDisplayObjectLike;
|
|
35
|
+
setOrigin?: (x?: number, y?: number) => PhaserDisplayObjectLike;
|
|
36
|
+
setVisible?: (visible: boolean) => PhaserDisplayObjectLike;
|
|
37
|
+
play?: (animationKey: string, ignoreIfPlaying?: boolean) => PhaserDisplayObjectLike;
|
|
38
|
+
stop?: () => PhaserDisplayObjectLike;
|
|
39
|
+
setFrame?: (frame: string) => PhaserDisplayObjectLike;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type PhaserTextLike = PhaserDisplayObjectLike & {
|
|
43
|
+
setText?: (text: string) => PhaserTextLike;
|
|
44
|
+
setVisible?: (visible: boolean) => PhaserTextLike;
|
|
45
|
+
setOrigin?: (x?: number, y?: number) => PhaserTextLike;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type PhaserReconcilerSceneLike = {
|
|
49
|
+
load?: {
|
|
50
|
+
atlas: (key: string, imageUrl: string, atlasUrl: string) => unknown;
|
|
51
|
+
image?: (key: string, imageUrl: string) => unknown;
|
|
52
|
+
};
|
|
53
|
+
textures?: {
|
|
54
|
+
getFrame?: (textureKey: string, frame: string) => unknown;
|
|
55
|
+
};
|
|
56
|
+
anims?: {
|
|
57
|
+
create: (config: {
|
|
58
|
+
key: string;
|
|
59
|
+
frames: Array<{ key: string; frame: string }>;
|
|
60
|
+
frameRate: number;
|
|
61
|
+
repeat: number;
|
|
62
|
+
}) => unknown;
|
|
63
|
+
};
|
|
64
|
+
add: {
|
|
65
|
+
rectangle: (x: number, y: number, width: number, height: number, fillColor: number) => PhaserDisplayObjectLike;
|
|
66
|
+
image?: (x: number, y: number, textureKey: string, frame?: string) => PhaserDisplayObjectLike;
|
|
67
|
+
sprite?: (x: number, y: number, textureKey: string, frame?: string) => PhaserDisplayObjectLike;
|
|
68
|
+
text: (x: number, y: number, text: string, style?: Record<string, unknown>) => PhaserTextLike;
|
|
69
|
+
};
|
|
70
|
+
tweens?: {
|
|
71
|
+
add: (config: Record<string, unknown>) => unknown;
|
|
72
|
+
killTweensOf?: (target: PhaserDisplayObjectLike) => unknown;
|
|
73
|
+
};
|
|
74
|
+
cameras?: {
|
|
75
|
+
main?: {
|
|
76
|
+
zoom: number;
|
|
77
|
+
scrollX: number;
|
|
78
|
+
scrollY: number;
|
|
79
|
+
width?: number;
|
|
80
|
+
height?: number;
|
|
81
|
+
setBounds?: (x: number, y: number, width: number, height: number) => unknown;
|
|
82
|
+
setZoom?: (zoom: number) => unknown;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
input?: {
|
|
86
|
+
on: (eventName: string, handler: (...args: any[]) => void) => unknown;
|
|
87
|
+
};
|
|
88
|
+
events?: {
|
|
89
|
+
on: (eventName: string, handler: (...args: any[]) => void) => unknown;
|
|
90
|
+
};
|
|
91
|
+
children?: {
|
|
92
|
+
bringToTop?: (target: PhaserDisplayObjectLike) => unknown;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type AgentGameSceneReconciler = {
|
|
97
|
+
reconcile(snapshot: AgentGameSnapshot): void;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type AgentVisual = {
|
|
101
|
+
shadow: PhaserDisplayObjectLike;
|
|
102
|
+
body: PhaserDisplayObjectLike;
|
|
103
|
+
head?: PhaserDisplayObjectLike;
|
|
104
|
+
usesSprite: boolean;
|
|
105
|
+
label: PhaserTextLike;
|
|
106
|
+
bubble: PhaserTextLike;
|
|
107
|
+
emotion: PhaserTextLike;
|
|
108
|
+
dom?: AgentDomOverlayVisual;
|
|
109
|
+
moveToken: number;
|
|
110
|
+
isMoving: boolean;
|
|
111
|
+
targetX: number;
|
|
112
|
+
targetY: number;
|
|
113
|
+
movementDirection: AgentGameDirection | null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type SceneReconcilerOptions = {
|
|
117
|
+
avatars?: AgentAvatarDefinition[];
|
|
118
|
+
domOverlay?: {
|
|
119
|
+
root: AgentDomOverlayRoot;
|
|
120
|
+
};
|
|
121
|
+
movementDurationMs?: number;
|
|
122
|
+
movementSpeedPxPerSecond?: number;
|
|
123
|
+
movementMinDurationMs?: number;
|
|
124
|
+
movementMaxDurationMs?: number;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type AgentDomOverlayElement = {
|
|
128
|
+
className: string;
|
|
129
|
+
textContent: string | null;
|
|
130
|
+
clientWidth?: number;
|
|
131
|
+
clientHeight?: number;
|
|
132
|
+
style: Record<string, string>;
|
|
133
|
+
appendChild?: (child: AgentDomOverlayElement) => unknown;
|
|
134
|
+
remove?: () => unknown;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type AgentDomOverlayRoot = AgentDomOverlayElement & {
|
|
138
|
+
ownerDocument?: {
|
|
139
|
+
createElement: (tagName: string) => AgentDomOverlayElement;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type AgentDomOverlayVisual = {
|
|
144
|
+
root: AgentDomOverlayRoot;
|
|
145
|
+
name: AgentDomOverlayElement;
|
|
146
|
+
bubble: AgentDomOverlayElement;
|
|
147
|
+
emotion: AgentDomOverlayElement;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export function createSceneReconciler(
|
|
151
|
+
scene: PhaserReconcilerSceneLike,
|
|
152
|
+
options: SceneReconcilerOptions = {},
|
|
153
|
+
): AgentGameSceneReconciler {
|
|
154
|
+
const visuals = new Map<string, AgentVisual>();
|
|
155
|
+
const movementDurationMs = options.movementDurationMs ?? 0;
|
|
156
|
+
const movementSpeedPxPerSecond = options.movementSpeedPxPerSecond;
|
|
157
|
+
const movementMinDurationMs = options.movementMinDurationMs;
|
|
158
|
+
const movementMaxDurationMs = options.movementMaxDurationMs;
|
|
159
|
+
const avatarFrameRegistry = createAvatarFrameRegistry(options.avatars ?? []);
|
|
160
|
+
let postUpdateInstalled = false;
|
|
161
|
+
|
|
162
|
+
if (options.domOverlay?.root) {
|
|
163
|
+
installDomOverlayPostUpdate(scene, visuals, options.domOverlay.root, () => {
|
|
164
|
+
updateAllDomOverlays(scene, visuals);
|
|
165
|
+
});
|
|
166
|
+
postUpdateInstalled = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
reconcile(snapshot) {
|
|
171
|
+
for (const agent of Object.values(snapshot.agents)) {
|
|
172
|
+
const anchor = resolveAgentAnchor(snapshot, agent);
|
|
173
|
+
let visual = visuals.get(agent.agentId);
|
|
174
|
+
if (!visual) {
|
|
175
|
+
visual = createAgentVisual(scene, agent, anchor.x, anchor.y);
|
|
176
|
+
if (options.domOverlay?.root) {
|
|
177
|
+
visual.dom = createAgentDomOverlay(options.domOverlay.root);
|
|
178
|
+
hideCanvasLabels(visual);
|
|
179
|
+
}
|
|
180
|
+
visuals.set(agent.agentId, visual);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const fillColor = getActivityColor(agent.activity);
|
|
184
|
+
const movementDirection = moveVisual(scene, visual, anchor.x, anchor.y, {
|
|
185
|
+
durationMs: movementDurationMs,
|
|
186
|
+
speedPxPerSecond: movementSpeedPxPerSecond,
|
|
187
|
+
minDurationMs: movementMinDurationMs,
|
|
188
|
+
maxDurationMs: movementMaxDurationMs,
|
|
189
|
+
}, () => {
|
|
190
|
+
showStationaryFrame(visual.body, agent, avatarFrameRegistry);
|
|
191
|
+
});
|
|
192
|
+
const depth = getAgentDepth(anchor.y);
|
|
193
|
+
visual.shadow.setDepth?.(depth - 1);
|
|
194
|
+
visual.body.setFillStyle?.(fillColor);
|
|
195
|
+
visual.body.setDepth?.(depth);
|
|
196
|
+
if (movementDirection) {
|
|
197
|
+
visual.body.play?.(getWalkAnimationName(agent, movementDirection), true);
|
|
198
|
+
} else {
|
|
199
|
+
showStationaryFrame(visual.body, agent, avatarFrameRegistry);
|
|
200
|
+
}
|
|
201
|
+
visual.head?.setDepth?.(depth + 1);
|
|
202
|
+
visual.label.setText?.(agent.name).setDepth?.(depth + 2);
|
|
203
|
+
visual.bubble.setText?.(getStatusBubbleText(agent));
|
|
204
|
+
visual.bubble.setVisible?.(shouldShowStatusBubble(agent));
|
|
205
|
+
visual.bubble.setDepth?.(agentUiDepth());
|
|
206
|
+
visual.emotion.setText?.("");
|
|
207
|
+
visual.emotion.setVisible?.(false);
|
|
208
|
+
visual.emotion.setDepth?.(agentUiDepth(1));
|
|
209
|
+
updateDomOverlay(visual, scene, agent);
|
|
210
|
+
}
|
|
211
|
+
promoteAgentVisuals(scene, visuals);
|
|
212
|
+
if (options.domOverlay?.root && !postUpdateInstalled) {
|
|
213
|
+
updateAllDomOverlays(scene, visuals);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function installDomOverlayPostUpdate(
|
|
220
|
+
scene: PhaserReconcilerSceneLike,
|
|
221
|
+
visuals: Map<string, AgentVisual>,
|
|
222
|
+
root: AgentDomOverlayRoot,
|
|
223
|
+
update: () => void,
|
|
224
|
+
): void {
|
|
225
|
+
if (!root || !visuals) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
scene.events?.on?.("postupdate", update);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getAgentDepth(y: number): number {
|
|
232
|
+
return agentDepth(y);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function promoteAgentVisuals(scene: PhaserReconcilerSceneLike, visuals: Map<string, AgentVisual>): void {
|
|
236
|
+
if (!scene.children?.bringToTop) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const orderedVisuals = [...visuals.values()].sort((a, b) => (a.body.y ?? 0) - (b.body.y ?? 0));
|
|
241
|
+
for (const visual of orderedVisuals) {
|
|
242
|
+
scene.children.bringToTop(visual.shadow);
|
|
243
|
+
scene.children.bringToTop(visual.body);
|
|
244
|
+
if (visual.head) {
|
|
245
|
+
scene.children.bringToTop(visual.head);
|
|
246
|
+
}
|
|
247
|
+
scene.children.bringToTop(visual.label);
|
|
248
|
+
scene.children.bringToTop(visual.bubble);
|
|
249
|
+
scene.children.bringToTop(visual.emotion);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function moveVisual(
|
|
254
|
+
scene: PhaserReconcilerSceneLike,
|
|
255
|
+
visual: AgentVisual,
|
|
256
|
+
x: number,
|
|
257
|
+
y: number,
|
|
258
|
+
movementTiming: MovementTimingOptions,
|
|
259
|
+
onArrive: () => void,
|
|
260
|
+
): AgentGameDirection | null {
|
|
261
|
+
const current = {
|
|
262
|
+
x: visual.body.x ?? x,
|
|
263
|
+
y: visual.body.y ?? y,
|
|
264
|
+
};
|
|
265
|
+
if (isAtTarget(current, { x, y })) {
|
|
266
|
+
visual.isMoving = false;
|
|
267
|
+
visual.movementDirection = null;
|
|
268
|
+
visual.targetX = x;
|
|
269
|
+
visual.targetY = y;
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (visual.isMoving && visual.targetX === x && visual.targetY === y) {
|
|
274
|
+
return visual.movementDirection;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tweenConfig = (movementTiming.speedPxPerSecond ?? movementTiming.durationMs ?? 0) > 0
|
|
278
|
+
? createMovementTweenConfig(current, { x, y }, movementTiming)
|
|
279
|
+
: null;
|
|
280
|
+
|
|
281
|
+
if (tweenConfig && scene.tweens) {
|
|
282
|
+
killVisualTweens(scene, visual);
|
|
283
|
+
const moveToken = ++visual.moveToken;
|
|
284
|
+
const movementDirection = getMovementDirection(current, { x, y });
|
|
285
|
+
visual.isMoving = true;
|
|
286
|
+
visual.targetX = x;
|
|
287
|
+
visual.targetY = y;
|
|
288
|
+
visual.movementDirection = movementDirection;
|
|
289
|
+
scene.tweens.add({
|
|
290
|
+
targets: visual.body,
|
|
291
|
+
...tweenConfig,
|
|
292
|
+
onComplete: () => {
|
|
293
|
+
if (visual.moveToken === moveToken) {
|
|
294
|
+
visual.isMoving = false;
|
|
295
|
+
visual.movementDirection = null;
|
|
296
|
+
visual.body.setPosition?.(x, y);
|
|
297
|
+
onArrive();
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
scene.tweens.add({ targets: visual.shadow, ...tweenConfig, y: y + 10 });
|
|
302
|
+
if (visual.head) {
|
|
303
|
+
scene.tweens.add({ targets: visual.head, ...tweenConfig, y: y - 14 });
|
|
304
|
+
}
|
|
305
|
+
scene.tweens.add({ targets: visual.label, ...tweenConfig, y: y + getLabelOffsetY(visual) });
|
|
306
|
+
scene.tweens.add({ targets: visual.bubble, ...tweenConfig, y: y + getBubbleOffsetY(visual) });
|
|
307
|
+
scene.tweens.add({ targets: visual.emotion, ...tweenConfig, x: x + getEmotionOffsetX(visual), y: y + getEmotionOffsetY(visual) });
|
|
308
|
+
return movementDirection;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
killVisualTweens(scene, visual);
|
|
312
|
+
visual.moveToken++;
|
|
313
|
+
visual.isMoving = false;
|
|
314
|
+
visual.targetX = x;
|
|
315
|
+
visual.targetY = y;
|
|
316
|
+
visual.movementDirection = null;
|
|
317
|
+
visual.shadow.setPosition?.(x, y + 10);
|
|
318
|
+
visual.body.setPosition?.(x, y);
|
|
319
|
+
visual.head?.setPosition?.(x, y - 14);
|
|
320
|
+
visual.label.setPosition?.(x, y + getLabelOffsetY(visual));
|
|
321
|
+
visual.bubble.setPosition?.(x, y + getBubbleOffsetY(visual));
|
|
322
|
+
visual.emotion.setPosition?.(x + getEmotionOffsetX(visual), y + getEmotionOffsetY(visual));
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isAtTarget(current: { x: number; y: number }, target: { x: number; y: number }): boolean {
|
|
327
|
+
return Math.abs(current.x - target.x) <= ARRIVAL_EPSILON && Math.abs(current.y - target.y) <= ARRIVAL_EPSILON;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function killVisualTweens(scene: PhaserReconcilerSceneLike, visual: AgentVisual): void {
|
|
331
|
+
scene.tweens?.killTweensOf?.(visual.body);
|
|
332
|
+
scene.tweens?.killTweensOf?.(visual.shadow);
|
|
333
|
+
if (visual.head) {
|
|
334
|
+
scene.tweens?.killTweensOf?.(visual.head);
|
|
335
|
+
}
|
|
336
|
+
scene.tweens?.killTweensOf?.(visual.label);
|
|
337
|
+
scene.tweens?.killTweensOf?.(visual.bubble);
|
|
338
|
+
scene.tweens?.killTweensOf?.(visual.emotion);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getMovementDirection(current: { x: number; y: number }, target: { x: number; y: number }): AgentGameDirection {
|
|
342
|
+
const dx = target.x - current.x;
|
|
343
|
+
const dy = target.y - current.y;
|
|
344
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
345
|
+
return dx < 0 ? "left" : "right";
|
|
346
|
+
}
|
|
347
|
+
return dy < 0 ? "up" : "down";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function createAgentVisual(scene: PhaserReconcilerSceneLike, agent: AgentGameAgent, x: number, y: number): AgentVisual {
|
|
351
|
+
const shadow = scene.add.rectangle(x, y + 10, 24, 8, 0x0f172a);
|
|
352
|
+
shadow.setDepth?.(9);
|
|
353
|
+
shadow.setVisible?.(false);
|
|
354
|
+
let usesSprite = false;
|
|
355
|
+
let body: PhaserDisplayObjectLike;
|
|
356
|
+
if (scene.add.sprite) {
|
|
357
|
+
usesSprite = true;
|
|
358
|
+
body = scene.add.sprite(x, y, agent.avatarId);
|
|
359
|
+
} else {
|
|
360
|
+
body = scene.add.rectangle(x, y, 18, 24, getActivityColor(agent.activity));
|
|
361
|
+
}
|
|
362
|
+
body.setScale?.(usesSprite ? SPRITE_AGENT_SCALE : 1.5);
|
|
363
|
+
body.setOrigin?.(0.5, usesSprite ? SPRITE_AGENT_FOOT_ORIGIN_Y : 0.72);
|
|
364
|
+
body.setDepth?.(10);
|
|
365
|
+
const head = usesSprite ? undefined : scene.add.rectangle(x, y - 14, 14, 12, 0xf8c8a0);
|
|
366
|
+
head?.setDepth?.(11);
|
|
367
|
+
const label = scene.add.text(x, y + getLabelOffsetY({ usesSprite }), agent.name, crispTextStyle({
|
|
368
|
+
color: "#f8fafc",
|
|
369
|
+
fontSize: usesSprite ? "8px" : "11px",
|
|
370
|
+
fontFamily: "monospace",
|
|
371
|
+
}));
|
|
372
|
+
label.setOrigin?.(0.5, 0);
|
|
373
|
+
const bubble = scene.add.text(x, y + getBubbleOffsetY({ usesSprite }), getStatusBubbleText(agent), crispTextStyle({
|
|
374
|
+
color: "#111827",
|
|
375
|
+
backgroundColor: "#f8fafc",
|
|
376
|
+
fontSize: usesSprite ? "8px" : "12px",
|
|
377
|
+
fontFamily: "sans-serif",
|
|
378
|
+
padding: usesSprite ? { x: 4, y: 2 } : { x: 6, y: 4 },
|
|
379
|
+
}));
|
|
380
|
+
bubble.setOrigin?.(0.5, 1);
|
|
381
|
+
bubble.setVisible?.(shouldShowStatusBubble(agent));
|
|
382
|
+
const emotion = scene.add.text(x + getEmotionOffsetX({ usesSprite }), y + getEmotionOffsetY({ usesSprite }), "", crispTextStyle({
|
|
383
|
+
color: "#f8fafc",
|
|
384
|
+
fontSize: usesSprite ? "7px" : "10px",
|
|
385
|
+
fontFamily: "monospace",
|
|
386
|
+
padding: usesSprite ? { x: 3, y: 1 } : { x: 4, y: 2 },
|
|
387
|
+
}));
|
|
388
|
+
emotion.setOrigin?.(0, 0.5);
|
|
389
|
+
emotion.setVisible?.(false);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
shadow,
|
|
393
|
+
body,
|
|
394
|
+
head,
|
|
395
|
+
usesSprite,
|
|
396
|
+
label,
|
|
397
|
+
bubble,
|
|
398
|
+
emotion,
|
|
399
|
+
dom: undefined,
|
|
400
|
+
moveToken: 0,
|
|
401
|
+
isMoving: false,
|
|
402
|
+
targetX: x,
|
|
403
|
+
targetY: y,
|
|
404
|
+
movementDirection: null,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createAgentDomOverlay(root: AgentDomOverlayRoot): AgentDomOverlayVisual {
|
|
409
|
+
const name = createDomOverlayElement(root, "agent-dom-label");
|
|
410
|
+
const bubble = createDomOverlayElement(root, "agent-dom-bubble");
|
|
411
|
+
const emotion = createDomOverlayElement(root, "agent-dom-emotion");
|
|
412
|
+
root.appendChild?.(name);
|
|
413
|
+
root.appendChild?.(bubble);
|
|
414
|
+
root.appendChild?.(emotion);
|
|
415
|
+
return { root, name, bubble, emotion };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function createDomOverlayElement(root: AgentDomOverlayRoot, className: string): AgentDomOverlayElement {
|
|
419
|
+
const element = root.ownerDocument?.createElement("div") ?? globalThis.document?.createElement?.("div");
|
|
420
|
+
if (!element) {
|
|
421
|
+
throw new Error("DOM overlay requires document.createElement");
|
|
422
|
+
}
|
|
423
|
+
element.className = className;
|
|
424
|
+
element.style.position = "absolute";
|
|
425
|
+
element.style.left = "0";
|
|
426
|
+
element.style.top = "0";
|
|
427
|
+
element.style.pointerEvents = "none";
|
|
428
|
+
element.style.transform = "translate3d(-9999px, -9999px, 0)";
|
|
429
|
+
return element as AgentDomOverlayElement;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function hideCanvasLabels(visual: AgentVisual): void {
|
|
433
|
+
visual.label.setVisible?.(false);
|
|
434
|
+
visual.bubble.setVisible?.(false);
|
|
435
|
+
visual.emotion.setVisible?.(false);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function updateAllDomOverlays(scene: PhaserReconcilerSceneLike, visuals: Map<string, AgentVisual>): void {
|
|
439
|
+
for (const visual of visuals.values()) {
|
|
440
|
+
if (visual.dom) {
|
|
441
|
+
positionDomOverlay(visual, scene);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function updateDomOverlay(visual: AgentVisual, scene: PhaserReconcilerSceneLike, agent: AgentGameAgent): void {
|
|
447
|
+
if (!visual.dom) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
visual.label.setVisible?.(false);
|
|
451
|
+
visual.bubble.setVisible?.(false);
|
|
452
|
+
visual.emotion.setVisible?.(false);
|
|
453
|
+
visual.dom.name.textContent = agent.name;
|
|
454
|
+
visual.dom.bubble.textContent = getStatusBubbleText(agent);
|
|
455
|
+
visual.dom.bubble.style.display = shouldShowStatusBubble(agent) ? "block" : "none";
|
|
456
|
+
visual.dom.emotion.textContent = "";
|
|
457
|
+
visual.dom.emotion.style.display = "none";
|
|
458
|
+
positionDomOverlay(visual, scene);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function positionDomOverlay(visual: AgentVisual, scene: PhaserReconcilerSceneLike): void {
|
|
462
|
+
const camera = scene.cameras?.main;
|
|
463
|
+
if (!camera || !visual.dom) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
setDomPosition(visual.dom.root, visual.dom.name, visual, camera, 0, getLabelOffsetY(visual));
|
|
467
|
+
setDomPosition(visual.dom.root, visual.dom.bubble, visual, camera, 0, getBubbleOffsetY(visual));
|
|
468
|
+
setDomPosition(visual.dom.root, visual.dom.emotion, visual, camera, getEmotionOffsetX(visual), getEmotionOffsetY(visual));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function setDomPosition(
|
|
472
|
+
root: AgentDomOverlayRoot,
|
|
473
|
+
element: AgentDomOverlayElement,
|
|
474
|
+
visual: AgentVisual,
|
|
475
|
+
camera: NonNullable<PhaserReconcilerSceneLike["cameras"]>["main"],
|
|
476
|
+
offsetX: number,
|
|
477
|
+
offsetY: number,
|
|
478
|
+
): void {
|
|
479
|
+
if (!camera) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const scaleX = camera.width && root.clientWidth ? root.clientWidth / camera.width : 1;
|
|
483
|
+
const scaleY = camera.height && root.clientHeight ? root.clientHeight / camera.height : 1;
|
|
484
|
+
const x = Math.round(((visual.body.x ?? 0) + offsetX - camera.scrollX) * camera.zoom * scaleX);
|
|
485
|
+
const y = Math.round(((visual.body.y ?? 0) + offsetY - camera.scrollY) * camera.zoom * scaleY);
|
|
486
|
+
const scale = camera.zoom * Math.min(scaleX, scaleY);
|
|
487
|
+
element.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) scale(${formatDomScale(scale)})`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function formatDomScale(scale: number): string {
|
|
491
|
+
return Number.isInteger(scale) ? String(scale) : scale.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function getLabelOffsetY(visual: Pick<AgentVisual, "usesSprite">): number {
|
|
495
|
+
return visual.usesSprite ? SPRITE_AGENT_LABEL_OFFSET_Y : PLACEHOLDER_AGENT_LABEL_OFFSET_Y;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getBubbleOffsetY(visual: Pick<AgentVisual, "usesSprite">): number {
|
|
499
|
+
return visual.usesSprite ? SPRITE_AGENT_BUBBLE_OFFSET_Y : PLACEHOLDER_AGENT_BUBBLE_OFFSET_Y;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function getEmotionOffsetX(visual: Pick<AgentVisual, "usesSprite">): number {
|
|
503
|
+
return visual.usesSprite ? SPRITE_AGENT_EMOTION_OFFSET_X : PLACEHOLDER_AGENT_EMOTION_OFFSET_X;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getEmotionOffsetY(visual: Pick<AgentVisual, "usesSprite">): number {
|
|
507
|
+
return visual.usesSprite ? SPRITE_AGENT_EMOTION_OFFSET_Y : PLACEHOLDER_AGENT_EMOTION_OFFSET_Y;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function resolveAgentAnchor(snapshot: AgentGameSnapshot, agent: AgentGameAgent) {
|
|
511
|
+
const targetLocationId = agent.activity.kind === "moving" ? agent.activity.toLocationId : agent.locationId;
|
|
512
|
+
return snapshot.scene.anchors.find((anchor) => anchor.id === targetLocationId) ?? snapshot.scene.anchors.find((anchor) => anchor.id === agent.locationId)!;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function getActivityColor(activity: AgentGameActivity): number {
|
|
516
|
+
switch (activity.kind) {
|
|
517
|
+
case "moving":
|
|
518
|
+
return 0xfacc15;
|
|
519
|
+
case "working":
|
|
520
|
+
return 0x22c55e;
|
|
521
|
+
case "thinking":
|
|
522
|
+
return 0xa78bfa;
|
|
523
|
+
case "talking":
|
|
524
|
+
return 0x38bdf8;
|
|
525
|
+
case "tool_call":
|
|
526
|
+
return activity.status === "failed" ? 0xef4444 : activity.status === "succeeded" ? 0x14b8a6 : 0xfb923c;
|
|
527
|
+
case "idle":
|
|
528
|
+
return 0xe5e7eb;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function getStatusBubbleText(agent: AgentGameAgent): string {
|
|
533
|
+
const emotionLabel = getEmotionLabel(agent);
|
|
534
|
+
const prefix = emotionLabel ? `${emotionLabel} ` : "";
|
|
535
|
+
switch (agent.activity.kind) {
|
|
536
|
+
case "moving":
|
|
537
|
+
return `${prefix}移动中`;
|
|
538
|
+
case "working":
|
|
539
|
+
return `${prefix}工作中`;
|
|
540
|
+
case "thinking":
|
|
541
|
+
return agent.activity.text ? `${prefix}思考中:${agent.activity.text}` : `${prefix}思考中`;
|
|
542
|
+
case "talking":
|
|
543
|
+
return `${prefix}说话中:${agent.activity.text}`;
|
|
544
|
+
case "tool_call":
|
|
545
|
+
return `${prefix}${agent.activity.toolName}: ${getToolCallStatusLabel(agent.activity.status)}`;
|
|
546
|
+
case "idle":
|
|
547
|
+
return getEmotionLabel(agent);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function getToolCallStatusLabel(status: Extract<AgentGameActivity, { kind: "tool_call" }>["status"]): string {
|
|
552
|
+
switch (status) {
|
|
553
|
+
case "started":
|
|
554
|
+
return "执行中";
|
|
555
|
+
case "succeeded":
|
|
556
|
+
return "完成";
|
|
557
|
+
case "failed":
|
|
558
|
+
return "失败";
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function getWalkAnimationName(agent: AgentGameAgent, movementDirection: AgentGameDirection): string {
|
|
563
|
+
return `${agent.avatarId}:walk.${movementDirection}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function showStationaryFrame(
|
|
567
|
+
body: PhaserDisplayObjectLike,
|
|
568
|
+
agent: AgentGameAgent,
|
|
569
|
+
registry: AvatarFrameRegistry,
|
|
570
|
+
): void {
|
|
571
|
+
body.stop?.();
|
|
572
|
+
body.setFrame?.(getStationaryFrame(agent, registry));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
type AvatarFrameRegistry = Map<string, Partial<Record<AgentAvatarAnimationName, string>>>;
|
|
576
|
+
|
|
577
|
+
function createAvatarFrameRegistry(avatars: AgentAvatarDefinition[]): AvatarFrameRegistry {
|
|
578
|
+
const registry: AvatarFrameRegistry = new Map();
|
|
579
|
+
for (const avatar of avatars) {
|
|
580
|
+
const frames: Partial<Record<AgentAvatarAnimationName, string>> = {};
|
|
581
|
+
for (const [animation, animationFrames] of Object.entries(avatar.animations)) {
|
|
582
|
+
const firstFrame = normalizeAgentAvatarAnimationFrames(animationFrames)[0];
|
|
583
|
+
if (firstFrame) {
|
|
584
|
+
frames[animation as AgentAvatarAnimationName] = firstFrame;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
registry.set(avatar.id, frames);
|
|
588
|
+
}
|
|
589
|
+
return registry;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function getStationaryFrame(agent: AgentGameAgent, registry: AvatarFrameRegistry): string {
|
|
593
|
+
const animation = `idle.${agent.direction}` as AgentAvatarAnimationName;
|
|
594
|
+
return registry.get(agent.avatarId)?.[animation] ?? `idle-${agent.direction}-0`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getEmotionLabel(agent: AgentGameAgent): string {
|
|
598
|
+
switch (agent.emotion) {
|
|
599
|
+
case "focused":
|
|
600
|
+
return "🎯";
|
|
601
|
+
case "happy":
|
|
602
|
+
return "🙂";
|
|
603
|
+
case "blocked":
|
|
604
|
+
return "⛔";
|
|
605
|
+
case "tired":
|
|
606
|
+
return "😴";
|
|
607
|
+
case undefined:
|
|
608
|
+
return agent.activity.kind === "idle" ? "🙂" : "";
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function shouldShowStatusBubble(agent: AgentGameAgent): boolean {
|
|
613
|
+
return getStatusBubbleText(agent) !== "";
|
|
614
|
+
}
|