agents-dojo 0.1.6 → 0.1.7
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/monitor/.env.development +1 -0
- package/monitor/.env.production +1 -0
- package/monitor/ANIMATION_REQUIREMENTS.md +118 -0
- package/monitor/index.html +12 -0
- package/monitor/package-lock.json +2160 -0
- package/monitor/package.json +25 -0
- package/monitor/public/bg.png +0 -0
- package/monitor/public/bg_clean.png +0 -0
- package/monitor/public/positions.json +215 -0
- package/monitor/public/sprites/agent_default.png +0 -0
- package/monitor/public/sprites/cushion_0.png +0 -0
- package/monitor/public/sprites/cushion_1.png +0 -0
- package/monitor/public/sprites/cushion_10.png +0 -0
- package/monitor/public/sprites/cushion_2.png +0 -0
- package/monitor/public/sprites/cushion_3.png +0 -0
- package/monitor/public/sprites/cushion_4.png +0 -0
- package/monitor/public/sprites/cushion_5.png +0 -0
- package/monitor/public/sprites/cushion_6.png +0 -0
- package/monitor/public/sprites/cushion_7.png +0 -0
- package/monitor/public/sprites/cushion_8.png +0 -0
- package/monitor/public/sprites/cushion_9.png +0 -0
- package/monitor/public/sprites/master.png +0 -0
- package/monitor/public/sprites/stake_0.png +0 -0
- package/monitor/public/sprites/stake_1.png +0 -0
- package/monitor/public/sprites/stake_10.png +0 -0
- package/monitor/public/sprites/stake_2.png +0 -0
- package/monitor/public/sprites/stake_3.png +0 -0
- package/monitor/public/sprites/stake_4.png +0 -0
- package/monitor/public/sprites/stake_5.png +0 -0
- package/monitor/public/sprites/stake_6.png +0 -0
- package/monitor/public/sprites/stake_7.png +0 -0
- package/monitor/public/sprites/stake_8.png +0 -0
- package/monitor/public/sprites/stake_9.png +0 -0
- package/monitor/scripts/record-gif.py +53 -0
- package/monitor/src/App.tsx +22 -0
- package/monitor/src/components/AgentMenu.tsx +67 -0
- package/monitor/src/components/ChatPanel.tsx +214 -0
- package/monitor/src/components/LogPage.tsx +173 -0
- package/monitor/src/components/Stage.tsx +39 -0
- package/monitor/src/components/StatusBar.tsx +50 -0
- package/monitor/src/lib/dojo-app.ts +799 -0
- package/monitor/src/lib/interactables.ts +162 -0
- package/monitor/src/lib/store.ts +352 -0
- package/monitor/src/lib/types.ts +72 -0
- package/monitor/src/lib/ws-client.ts +66 -0
- package/monitor/src/main.tsx +9 -0
- package/monitor/src/vite-env.d.ts +1 -0
- package/monitor/tsconfig.json +14 -0
- package/monitor/vite.config.ts +13 -0
- package/package.json +2 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DojoApp — Pure Pixi.js application that manages all dojo sprites.
|
|
3
|
+
* Single Ticker loop drives all animations. No React re-renders for movement.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
Application, Container, Sprite, Texture, Assets, Rectangle,
|
|
7
|
+
Graphics, Text, TextStyle, type FederatedPointerEvent,
|
|
8
|
+
} from 'pixi.js';
|
|
9
|
+
import { useMonitorStore, getChatPosition } from './store.js';
|
|
10
|
+
import { sendCommand } from './ws-client.js';
|
|
11
|
+
import { INTERACTABLES, getObstacles } from './interactables.js';
|
|
12
|
+
import type { Agent, AgentAnim } from './types.js';
|
|
13
|
+
|
|
14
|
+
// ── Constants ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const DESIGN_W = 960;
|
|
17
|
+
const DESIGN_H = 600;
|
|
18
|
+
const FRAME_SIZE = 256;
|
|
19
|
+
const TOTAL_FRAMES = 21;
|
|
20
|
+
const SPRITE_SCALE = 64 / 256;
|
|
21
|
+
const MOVE_SPEED = 1.2;
|
|
22
|
+
const STAKE_ATTACK_OFFSET_X = 25;
|
|
23
|
+
const DS = 960 / 2400;
|
|
24
|
+
const OBSTACLES = getObstacles();
|
|
25
|
+
|
|
26
|
+
const MASTER_POS = { x: 480, y: 460 };
|
|
27
|
+
|
|
28
|
+
const ANIMS: Record<string, number[]> = {
|
|
29
|
+
idle: [0, 1], walk: [2, 3, 4, 5], sit: [6, 7, 8], attack: [9, 10, 11],
|
|
30
|
+
daydream: [12, 13], chat: [19, 20],
|
|
31
|
+
shake_hand: [0, 1], // reuse idle frames; wobble effect applied in tick
|
|
32
|
+
head_shake: [0, 1], // reuse idle frames; wobble effect applied in tick
|
|
33
|
+
};
|
|
34
|
+
const ANIM_SPEED: Record<string, number> = {
|
|
35
|
+
idle: 800, walk: 150, sit: 600, attack: 200, daydream: 1200, chat: 400,
|
|
36
|
+
shake_hand: 200, head_shake: 150,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FLOOR = { minX: 40, maxX: 920, minY: 310, maxY: 550 };
|
|
40
|
+
const BUBBLE_CHUNK_SIZE = 40;
|
|
41
|
+
const BUBBLE_CHUNK_INTERVAL = 1500; // ms per chunk
|
|
42
|
+
|
|
43
|
+
/** Split text into display chunks of ~BUBBLE_CHUNK_SIZE chars, breaking at word boundaries. */
|
|
44
|
+
function splitIntoChunks(text: string): string[] {
|
|
45
|
+
if (text.length <= BUBBLE_CHUNK_SIZE) return [text];
|
|
46
|
+
const chunks: string[] = [];
|
|
47
|
+
let pos = 0;
|
|
48
|
+
while (pos < text.length) {
|
|
49
|
+
let end = pos + BUBBLE_CHUNK_SIZE;
|
|
50
|
+
if (end >= text.length) { chunks.push(text.slice(pos)); break; }
|
|
51
|
+
// Find a word boundary near the end
|
|
52
|
+
const spaceIdx = text.lastIndexOf(' ', end);
|
|
53
|
+
if (spaceIdx > pos) end = spaceIdx;
|
|
54
|
+
chunks.push(text.slice(pos, end));
|
|
55
|
+
pos = end;
|
|
56
|
+
// Skip leading space on next chunk
|
|
57
|
+
if (text[pos] === ' ') pos++;
|
|
58
|
+
}
|
|
59
|
+
return chunks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function randomFloorPos() {
|
|
63
|
+
for (let i = 0; i < 30; i++) {
|
|
64
|
+
const x = FLOOR.minX + Math.random() * (FLOOR.maxX - FLOOR.minX);
|
|
65
|
+
const y = FLOOR.minY + Math.random() * (FLOOR.maxY - FLOOR.minY);
|
|
66
|
+
if (!OBSTACLES.some(o => Math.hypot(x - o.x, y - o.y) < o.r + 20)) return { x, y };
|
|
67
|
+
}
|
|
68
|
+
return { x: 480, y: 450 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Agent Sprite State ─────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
interface AgentState {
|
|
74
|
+
sprite: Sprite;
|
|
75
|
+
nameLabel: Text;
|
|
76
|
+
nameBg: Graphics;
|
|
77
|
+
bubble: Container;
|
|
78
|
+
bubbleText: Text;
|
|
79
|
+
bubbleBg: Graphics;
|
|
80
|
+
x: number; y: number;
|
|
81
|
+
targetX: number; targetY: number;
|
|
82
|
+
animKey: string;
|
|
83
|
+
animTime: number;
|
|
84
|
+
frameIdx: number;
|
|
85
|
+
facingLeft: boolean;
|
|
86
|
+
arrived: boolean;
|
|
87
|
+
autoAction: { kind: string; target?: { x: number; y: number }; duration?: number; timer: number };
|
|
88
|
+
visitedMaster: boolean;
|
|
89
|
+
failTimer?: number; // ms counter for shake_hand animation on failed
|
|
90
|
+
rejectTimer?: number; // ms counter for head_shake animation on rejected
|
|
91
|
+
dragging: boolean; // true while user is dragging this agent
|
|
92
|
+
wobbleTime: number; // ticker-synced wobble accumulator
|
|
93
|
+
prevStoreState: string; // previous store state (for detecting transitions)
|
|
94
|
+
idleGrace: number; // ms grace period after task→idle (delays auto-behavior)
|
|
95
|
+
pendingSync?: Agent; // buffered store agent snapshot, applied at start of tick
|
|
96
|
+
// Chunked bubble display
|
|
97
|
+
bubbleChunks: string[];
|
|
98
|
+
bubbleChunkIdx: number;
|
|
99
|
+
bubbleChunkTimer: number;
|
|
100
|
+
bubbleFullText: string; // track to detect when message changes
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Interactable State ─────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
interface InteractableState {
|
|
106
|
+
sprite: Sprite;
|
|
107
|
+
shakeX: number;
|
|
108
|
+
scaleX: number;
|
|
109
|
+
deform: number;
|
|
110
|
+
type: string;
|
|
111
|
+
anchorX: number;
|
|
112
|
+
spriteY: number;
|
|
113
|
+
agentX: number;
|
|
114
|
+
agentY: number;
|
|
115
|
+
designW: number;
|
|
116
|
+
designH: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Main App ───────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export class DojoApp {
|
|
122
|
+
app: Application;
|
|
123
|
+
private scene!: Container;
|
|
124
|
+
private agentFrames: Texture[] = [];
|
|
125
|
+
private masterFrames: Texture[] = [];
|
|
126
|
+
private agents = new Map<string, AgentState>();
|
|
127
|
+
private interactables: InteractableState[] = [];
|
|
128
|
+
private ready = false;
|
|
129
|
+
// Master bubble state
|
|
130
|
+
private masterBubble!: Container;
|
|
131
|
+
private masterBubbleBg!: Graphics;
|
|
132
|
+
private masterBubbleText!: Text;
|
|
133
|
+
private masterBubbleChunks: string[] = [];
|
|
134
|
+
private masterBubbleChunkIdx = 0;
|
|
135
|
+
private masterBubbleChunkTimer = 0;
|
|
136
|
+
private masterBubbleFullText = '';
|
|
137
|
+
|
|
138
|
+
constructor(private canvas: HTMLCanvasElement) {
|
|
139
|
+
this.app = new Application();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async init() {
|
|
143
|
+
await this.app.init({
|
|
144
|
+
canvas: this.canvas,
|
|
145
|
+
width: window.innerWidth,
|
|
146
|
+
height: window.innerHeight,
|
|
147
|
+
background: 0x2a1a0a,
|
|
148
|
+
antialias: false,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.scene = new Container();
|
|
152
|
+
this.app.stage.addChild(this.scene);
|
|
153
|
+
this.app.stage.eventMode = 'static';
|
|
154
|
+
this.app.stage.on('pointerdown', () => {
|
|
155
|
+
useMonitorStore.getState().closeMenu();
|
|
156
|
+
});
|
|
157
|
+
this.updateScale();
|
|
158
|
+
|
|
159
|
+
// Load assets
|
|
160
|
+
const [bgTex, agentTex, masterTex] = await Promise.all([
|
|
161
|
+
Assets.load('/bg_clean.png'),
|
|
162
|
+
Assets.load('/sprites/agent_default.png'),
|
|
163
|
+
Assets.load('/sprites/master.png'),
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
// Background
|
|
167
|
+
const bg = new Sprite(bgTex);
|
|
168
|
+
bg.width = DESIGN_W;
|
|
169
|
+
bg.height = DESIGN_H;
|
|
170
|
+
this.scene.addChild(bg);
|
|
171
|
+
|
|
172
|
+
// Cut agent frames
|
|
173
|
+
for (let i = 0; i < TOTAL_FRAMES; i++) {
|
|
174
|
+
this.agentFrames.push(new Texture({ source: agentTex.source, frame: new Rectangle(i * FRAME_SIZE, 0, FRAME_SIZE, FRAME_SIZE) }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Cut master frames
|
|
178
|
+
for (let i = 0; i < 2; i++) {
|
|
179
|
+
this.masterFrames.push(new Texture({ source: masterTex.source, frame: new Rectangle(i * FRAME_SIZE, 0, FRAME_SIZE, FRAME_SIZE) }));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Load interactables
|
|
183
|
+
await this.loadInteractables();
|
|
184
|
+
|
|
185
|
+
// Master sprite
|
|
186
|
+
this.createMaster();
|
|
187
|
+
|
|
188
|
+
// Start ticker
|
|
189
|
+
this.app.ticker.add(this.tick.bind(this));
|
|
190
|
+
|
|
191
|
+
// Subscribe to store — buffer agent snapshots for next tick (avoids race condition)
|
|
192
|
+
useMonitorStore.subscribe((state) => {
|
|
193
|
+
for (const [id, agent] of state.agents) {
|
|
194
|
+
if (!this.agents.has(id)) this.createAgent(id, agent);
|
|
195
|
+
const s = this.agents.get(id);
|
|
196
|
+
if (s) s.pendingSync = agent;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Also process any agents already in the store (loaded before init)
|
|
201
|
+
const initialState = useMonitorStore.getState();
|
|
202
|
+
for (const [id, agent] of initialState.agents) {
|
|
203
|
+
if (!this.agents.has(id)) this.createAgent(id, agent);
|
|
204
|
+
this.syncAgentFromStore(id, agent);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.ready = true;
|
|
208
|
+
|
|
209
|
+
// Handle resize
|
|
210
|
+
window.addEventListener('resize', () => this.updateScale());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private updateScale() {
|
|
214
|
+
const vw = window.innerWidth;
|
|
215
|
+
const vh = window.innerHeight;
|
|
216
|
+
const scale = Math.min(vw / DESIGN_W, vh / DESIGN_H);
|
|
217
|
+
this.scene.x = (vw - DESIGN_W * scale) / 2;
|
|
218
|
+
this.scene.y = (vh - DESIGN_H * scale) / 2;
|
|
219
|
+
this.scene.scale.set(scale);
|
|
220
|
+
this.app.renderer.resize(vw, vh);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async loadInteractables() {
|
|
224
|
+
for (const def of INTERACTABLES) {
|
|
225
|
+
const tex = await Assets.load(def.spriteFile);
|
|
226
|
+
const spr = new Sprite(tex);
|
|
227
|
+
spr.anchor.set(def.spriteAnchorX, def.spriteAnchorY);
|
|
228
|
+
spr.x = def.anchorX;
|
|
229
|
+
spr.y = def.spriteY;
|
|
230
|
+
spr.width = def.origW * DS;
|
|
231
|
+
spr.height = def.origH * DS;
|
|
232
|
+
// Cushions behind agents, stakes in front
|
|
233
|
+
if (def.type === 'cushion') {
|
|
234
|
+
this.scene.addChild(spr);
|
|
235
|
+
}
|
|
236
|
+
this.interactables.push({
|
|
237
|
+
sprite: spr, shakeX: 0, scaleX: 1, deform: 0,
|
|
238
|
+
type: def.type, anchorX: def.anchorX, spriteY: def.spriteY,
|
|
239
|
+
agentX: def.agentX, agentY: def.agentY,
|
|
240
|
+
designW: def.origW * DS, designH: def.origH * DS,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Stakes added later (on top)
|
|
244
|
+
for (const ist of this.interactables) {
|
|
245
|
+
if (ist.type === 'stake') this.scene.addChild(ist.sprite);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private createMaster() {
|
|
250
|
+
const masterContainer = new Container();
|
|
251
|
+
masterContainer.x = MASTER_POS.x;
|
|
252
|
+
masterContainer.y = MASTER_POS.y;
|
|
253
|
+
|
|
254
|
+
const spr = new Sprite(this.masterFrames[0]);
|
|
255
|
+
spr.anchor.set(0.5, 1);
|
|
256
|
+
spr.scale.set(SPRITE_SCALE);
|
|
257
|
+
|
|
258
|
+
const nameBg = new Graphics();
|
|
259
|
+
nameBg.roundRect(-28, -FRAME_SIZE * SPRITE_SCALE - 14, 56, 14, 3);
|
|
260
|
+
nameBg.fill({ color: 0x8b0000, alpha: 0.6 });
|
|
261
|
+
|
|
262
|
+
const label = new Text({ text: 'Master', style: new TextStyle({ fontFamily: 'monospace', fontSize: 11, fill: 0xffd700, fontWeight: 'bold' }) });
|
|
263
|
+
label.anchor.set(0.5, 0);
|
|
264
|
+
label.y = -FRAME_SIZE * SPRITE_SCALE - 13;
|
|
265
|
+
|
|
266
|
+
// Speech bubble (hidden by default)
|
|
267
|
+
this.masterBubble = new Container();
|
|
268
|
+
this.masterBubble.visible = false;
|
|
269
|
+
this.masterBubbleBg = new Graphics();
|
|
270
|
+
this.masterBubbleText = new Text({ text: '', style: new TextStyle({ fontFamily: 'monospace', fontSize: 8, fill: 0x333333, wordWrap: true, wordWrapWidth: 140 }) });
|
|
271
|
+
this.masterBubbleText.anchor.set(0.5, 0);
|
|
272
|
+
this.masterBubble.addChild(this.masterBubbleBg, this.masterBubbleText);
|
|
273
|
+
|
|
274
|
+
masterContainer.addChild(spr, nameBg, label, this.masterBubble);
|
|
275
|
+
this.scene.addChild(masterContainer);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private createAgent(id: string, agent: Agent) {
|
|
279
|
+
const container = new Container();
|
|
280
|
+
|
|
281
|
+
const spr = new Sprite(this.agentFrames[0]);
|
|
282
|
+
spr.anchor.set(0.5, 1);
|
|
283
|
+
spr.scale.set(SPRITE_SCALE);
|
|
284
|
+
|
|
285
|
+
const nameBg = new Graphics();
|
|
286
|
+
nameBg.roundRect(-28, -FRAME_SIZE * SPRITE_SCALE - 14, 56, 14, 3);
|
|
287
|
+
nameBg.fill({ color: 0x000000, alpha: 0.45 });
|
|
288
|
+
|
|
289
|
+
const nameLabel = new Text({ text: id, style: new TextStyle({ fontFamily: 'monospace', fontSize: 11, fill: 0xffffff }) });
|
|
290
|
+
nameLabel.anchor.set(0.5, 0);
|
|
291
|
+
nameLabel.y = -FRAME_SIZE * SPRITE_SCALE - 13;
|
|
292
|
+
|
|
293
|
+
// Speech bubble (hidden by default)
|
|
294
|
+
const bubble = new Container();
|
|
295
|
+
bubble.visible = false;
|
|
296
|
+
const bubbleBg = new Graphics();
|
|
297
|
+
const bubbleText = new Text({ text: '', style: new TextStyle({ fontFamily: 'monospace', fontSize: 8, fill: 0x333333, wordWrap: true, wordWrapWidth: 140 }) });
|
|
298
|
+
bubbleText.anchor.set(0.5, 0);
|
|
299
|
+
bubble.addChild(bubbleBg, bubbleText);
|
|
300
|
+
|
|
301
|
+
// Hit area for easier clicking/dragging
|
|
302
|
+
const hitArea = new Graphics();
|
|
303
|
+
hitArea.rect(-32, -FRAME_SIZE * SPRITE_SCALE, 64, FRAME_SIZE * SPRITE_SCALE);
|
|
304
|
+
hitArea.fill({ color: 0, alpha: 0.001 });
|
|
305
|
+
|
|
306
|
+
container.addChild(hitArea, spr, nameBg, nameLabel, bubble);
|
|
307
|
+
// Insert before stakes
|
|
308
|
+
const stakeIdx = this.scene.children.findIndex(c => this.interactables.some(i => i.type === 'stake' && i.sprite === c));
|
|
309
|
+
if (stakeIdx >= 0) this.scene.addChildAt(container, stakeIdx);
|
|
310
|
+
else this.scene.addChild(container);
|
|
311
|
+
|
|
312
|
+
const state: AgentState = {
|
|
313
|
+
sprite: spr, nameLabel, nameBg, bubble, bubbleText, bubbleBg,
|
|
314
|
+
x: agent.position.x, y: agent.position.y,
|
|
315
|
+
targetX: agent.position.x, targetY: agent.position.y,
|
|
316
|
+
animKey: 'idle', animTime: 0, frameIdx: 0, facingLeft: false,
|
|
317
|
+
arrived: false,
|
|
318
|
+
autoAction: { kind: 'pause', duration: 500 + Math.random() * 2000, timer: 0 },
|
|
319
|
+
visitedMaster: false,
|
|
320
|
+
dragging: false,
|
|
321
|
+
wobbleTime: 0,
|
|
322
|
+
prevStoreState: 'idle',
|
|
323
|
+
idleGrace: 0,
|
|
324
|
+
bubbleChunks: [],
|
|
325
|
+
bubbleChunkIdx: 0,
|
|
326
|
+
bubbleChunkTimer: 0,
|
|
327
|
+
bubbleFullText: '',
|
|
328
|
+
};
|
|
329
|
+
container.x = state.x;
|
|
330
|
+
container.y = state.y;
|
|
331
|
+
this.agents.set(id, state);
|
|
332
|
+
|
|
333
|
+
// ── Drag + Click interaction ──
|
|
334
|
+
container.eventMode = 'static';
|
|
335
|
+
container.cursor = 'pointer';
|
|
336
|
+
let dragStart: { x: number; y: number } | null = null;
|
|
337
|
+
let dragMoved = false;
|
|
338
|
+
const DRAG_THRESHOLD = 4;
|
|
339
|
+
|
|
340
|
+
container.on('pointerdown', (e: FederatedPointerEvent) => {
|
|
341
|
+
e.stopPropagation();
|
|
342
|
+
dragStart = { x: e.globalX, y: e.globalY };
|
|
343
|
+
dragMoved = false;
|
|
344
|
+
state.dragging = false;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
container.on('globalpointermove', (e: FederatedPointerEvent) => {
|
|
348
|
+
if (!dragStart) return;
|
|
349
|
+
const dx = e.globalX - dragStart.x;
|
|
350
|
+
const dy = e.globalY - dragStart.y;
|
|
351
|
+
if (!dragMoved && Math.hypot(dx, dy) > DRAG_THRESHOLD) {
|
|
352
|
+
dragMoved = true;
|
|
353
|
+
state.dragging = true;
|
|
354
|
+
useMonitorStore.getState().closeMenu();
|
|
355
|
+
}
|
|
356
|
+
if (dragMoved) {
|
|
357
|
+
// Convert screen coords to design coords via scene transform
|
|
358
|
+
const sceneScale = this.scene.scale.x;
|
|
359
|
+
const designX = (e.globalX - this.scene.x) / sceneScale;
|
|
360
|
+
const designY = (e.globalY - this.scene.y) / sceneScale;
|
|
361
|
+
state.x = Math.max(FLOOR.minX, Math.min(FLOOR.maxX, designX));
|
|
362
|
+
state.y = Math.max(FLOOR.minY, Math.min(FLOOR.maxY, designY));
|
|
363
|
+
state.targetX = state.x;
|
|
364
|
+
state.targetY = state.y;
|
|
365
|
+
state.arrived = true;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const onPointerEnd = (e: FederatedPointerEvent) => {
|
|
370
|
+
if (!dragStart) return;
|
|
371
|
+
if (dragMoved) {
|
|
372
|
+
// Drag ended — persist position
|
|
373
|
+
sendCommand({ type: 'set_position', agentId: id, x: state.x, y: state.y });
|
|
374
|
+
state.autoAction = { kind: 'pause', duration: 2000, timer: 0 };
|
|
375
|
+
state.arrived = true;
|
|
376
|
+
} else {
|
|
377
|
+
// Click — open context menu
|
|
378
|
+
useMonitorStore.getState().openMenu(id, e.globalX, e.globalY);
|
|
379
|
+
}
|
|
380
|
+
dragStart = null;
|
|
381
|
+
dragMoved = false;
|
|
382
|
+
state.dragging = false;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
container.on('pointerup', onPointerEnd);
|
|
386
|
+
container.on('pointerupoutside', onPointerEnd);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Sync animation targets from store state.
|
|
391
|
+
* State machine per ANIMATION_REQUIREMENTS.md §10:
|
|
392
|
+
*
|
|
393
|
+
* 10.1 Client → Agent:
|
|
394
|
+
* submitted → walk to Master
|
|
395
|
+
* working → walk to stake, attack
|
|
396
|
+
* input-required → walk back to Master, wait
|
|
397
|
+
* auth-required → same as input-required
|
|
398
|
+
* completed → walk home
|
|
399
|
+
* failed → shake_hand at stake, then walk home
|
|
400
|
+
* canceled → walk home immediately
|
|
401
|
+
* rejected → head_shake at Master, then walk home
|
|
402
|
+
*
|
|
403
|
+
* 10.2 Agent → Agent:
|
|
404
|
+
* submitted/working → walk to peer, chat animation
|
|
405
|
+
* completed etc. → walk home
|
|
406
|
+
*/
|
|
407
|
+
private syncAgentFromStore(id: string, agent: Agent) {
|
|
408
|
+
const s = this.agents.get(id);
|
|
409
|
+
if (!s) return;
|
|
410
|
+
|
|
411
|
+
const store = useMonitorStore.getState();
|
|
412
|
+
const peerSession = agent.chatSessionId ? store.chatSessions.get(agent.chatSessionId) : undefined;
|
|
413
|
+
const isInPeerChat = !!peerSession;
|
|
414
|
+
const isAgentCall = agent.taskFrom && agent.taskFrom !== 'user';
|
|
415
|
+
const state = agent.state;
|
|
416
|
+
|
|
417
|
+
// Priority 1: Peer chat (agent-to-agent interaction)
|
|
418
|
+
if (isInPeerChat && peerSession) {
|
|
419
|
+
const idx = peerSession.memberIds.indexOf(id);
|
|
420
|
+
const pos = getChatPosition(peerSession.meetPoint, idx, peerSession.memberIds.length);
|
|
421
|
+
s.targetX = pos.x; s.targetY = pos.y;
|
|
422
|
+
s.animKey = s.arrived ? 'chat' : 'walk';
|
|
423
|
+
}
|
|
424
|
+
// Priority 2: Agent-to-agent call (no peer session yet, B walking to A)
|
|
425
|
+
else if (isAgentCall && (state === 'submitted' || state === 'working' || state === 'tool_call')) {
|
|
426
|
+
// Will be handled by peer chat session once created; for now walk toward caller
|
|
427
|
+
s.animKey = 'walk';
|
|
428
|
+
}
|
|
429
|
+
// Priority 3: Client task states
|
|
430
|
+
else if (state === 'submitted') {
|
|
431
|
+
// Walk to Master
|
|
432
|
+
s.animKey = 'walk';
|
|
433
|
+
s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
|
|
434
|
+
s.visitedMaster = false;
|
|
435
|
+
}
|
|
436
|
+
else if (state === 'working' || state === 'tool_call') {
|
|
437
|
+
if (!s.visitedMaster) {
|
|
438
|
+
// First visit master, then go to stake
|
|
439
|
+
s.animKey = 'walk';
|
|
440
|
+
s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
|
|
441
|
+
} else {
|
|
442
|
+
// At stake, attack
|
|
443
|
+
s.animKey = s.arrived ? 'attack' : 'walk';
|
|
444
|
+
s.targetX = agent.stakePosition.x + STAKE_ATTACK_OFFSET_X;
|
|
445
|
+
s.targetY = agent.stakePosition.y;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (state === 'input-required' || state === 'auth-required') {
|
|
449
|
+
// Walk back to Master and wait
|
|
450
|
+
s.animKey = s.arrived ? 'idle' : 'walk';
|
|
451
|
+
s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
|
|
452
|
+
}
|
|
453
|
+
else if (state === 'completed' || state === 'canceled') {
|
|
454
|
+
// Walk home
|
|
455
|
+
s.animKey = s.arrived ? 'idle' : 'walk';
|
|
456
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
457
|
+
}
|
|
458
|
+
else if (state === 'failed') {
|
|
459
|
+
// Shake hand at stake, then walk home
|
|
460
|
+
if (!s.arrived) {
|
|
461
|
+
// If still walking somewhere, keep going to stake first
|
|
462
|
+
s.animKey = 'walk';
|
|
463
|
+
s.targetX = agent.stakePosition.x + STAKE_ATTACK_OFFSET_X;
|
|
464
|
+
s.targetY = agent.stakePosition.y;
|
|
465
|
+
} else if (s.failTimer === undefined) {
|
|
466
|
+
// Just arrived at stake — start shake_hand animation
|
|
467
|
+
s.animKey = 'shake_hand';
|
|
468
|
+
s.failTimer = 0;
|
|
469
|
+
} else if (s.failTimer < 1500) {
|
|
470
|
+
// Shake hand for 1.5 seconds
|
|
471
|
+
s.animKey = 'shake_hand';
|
|
472
|
+
} else {
|
|
473
|
+
// Done shaking, walk home
|
|
474
|
+
s.animKey = 'walk';
|
|
475
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
476
|
+
s.arrived = false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else if (state === 'rejected') {
|
|
480
|
+
// Head shake at Master, then walk home
|
|
481
|
+
if (!s.arrived && Math.hypot(s.x - MASTER_POS.x - 30, s.y - MASTER_POS.y) > MOVE_SPEED * 3) {
|
|
482
|
+
s.animKey = 'walk';
|
|
483
|
+
s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
|
|
484
|
+
} else if (s.rejectTimer === undefined) {
|
|
485
|
+
s.animKey = 'head_shake';
|
|
486
|
+
s.rejectTimer = 0;
|
|
487
|
+
} else if (s.rejectTimer < 1500) {
|
|
488
|
+
s.animKey = 'head_shake';
|
|
489
|
+
} else {
|
|
490
|
+
s.animKey = 'walk';
|
|
491
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
492
|
+
s.arrived = false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// else: idle — handled by auto-behavior in tick
|
|
496
|
+
|
|
497
|
+
// Reset timers when leaving failed/rejected states
|
|
498
|
+
if (state !== 'failed') s.failTimer = undefined;
|
|
499
|
+
if (state !== 'rejected') s.rejectTimer = undefined;
|
|
500
|
+
|
|
501
|
+
// Bubble — split full message into chunks, cycled in tick()
|
|
502
|
+
const showBubble = !!agent.lastMessage && state !== 'idle';
|
|
503
|
+
s.bubble.visible = showBubble;
|
|
504
|
+
if (showBubble && agent.lastMessage) {
|
|
505
|
+
// Recompute chunks if message changed
|
|
506
|
+
if (agent.lastMessage !== s.bubbleFullText) {
|
|
507
|
+
s.bubbleFullText = agent.lastMessage;
|
|
508
|
+
s.bubbleChunks = splitIntoChunks(agent.lastMessage);
|
|
509
|
+
s.bubbleChunkIdx = 0;
|
|
510
|
+
s.bubbleChunkTimer = 0;
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
s.bubbleFullText = '';
|
|
514
|
+
s.bubbleChunks = [];
|
|
515
|
+
s.bubbleChunkIdx = 0;
|
|
516
|
+
s.bubbleChunkTimer = 0;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── Single Ticker Loop ───────────────────────────────────
|
|
521
|
+
|
|
522
|
+
private tick(ticker: any) {
|
|
523
|
+
const dt = ticker.deltaMS;
|
|
524
|
+
|
|
525
|
+
for (const [id, s] of this.agents) {
|
|
526
|
+
// Apply buffered store sync at the start of each tick (avoids mid-frame mutation)
|
|
527
|
+
if (s.pendingSync) {
|
|
528
|
+
this.syncAgentFromStore(id, s.pendingSync);
|
|
529
|
+
s.pendingSync = undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const agent = useMonitorStore.getState().agents.get(id);
|
|
533
|
+
if (!agent) continue;
|
|
534
|
+
|
|
535
|
+
const state = agent.state;
|
|
536
|
+
const isIdle = state === 'idle';
|
|
537
|
+
const isInPeerChat = !!agent.chatSessionId;
|
|
538
|
+
|
|
539
|
+
// Track state transitions for idle grace period
|
|
540
|
+
if (s.prevStoreState !== state) {
|
|
541
|
+
if (state === 'idle' && s.prevStoreState !== 'idle') {
|
|
542
|
+
// Just transitioned to idle — set grace period to prevent auto-behavior flicker
|
|
543
|
+
s.idleGrace = 500;
|
|
544
|
+
}
|
|
545
|
+
s.prevStoreState = state;
|
|
546
|
+
}
|
|
547
|
+
if (s.idleGrace > 0) s.idleGrace -= dt;
|
|
548
|
+
|
|
549
|
+
// Auto-behavior when idle (sit on cushion, occasionally wander)
|
|
550
|
+
// Skip during grace period after task completion
|
|
551
|
+
if (isIdle && !isInPeerChat && s.idleGrace <= 0) {
|
|
552
|
+
const action = s.autoAction;
|
|
553
|
+
if (action.kind === 'sit') {
|
|
554
|
+
// Sit on cushion (home position)
|
|
555
|
+
s.animKey = 'sit';
|
|
556
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
557
|
+
action.timer += dt;
|
|
558
|
+
if (action.timer >= (action.duration ?? 5000)) {
|
|
559
|
+
s.autoAction = this.pickAutoAction();
|
|
560
|
+
s.arrived = false;
|
|
561
|
+
}
|
|
562
|
+
} else if (action.kind === 'wander') {
|
|
563
|
+
s.animKey = 'walk';
|
|
564
|
+
s.targetX = action.target!.x;
|
|
565
|
+
s.targetY = action.target!.y;
|
|
566
|
+
if (s.arrived) {
|
|
567
|
+
s.autoAction = this.pickAutoAction();
|
|
568
|
+
s.arrived = false;
|
|
569
|
+
}
|
|
570
|
+
} else if (action.kind === 'daydream' || action.kind === 'pause') {
|
|
571
|
+
s.animKey = action.kind === 'daydream' ? 'daydream' : 'idle';
|
|
572
|
+
s.targetX = s.x; s.targetY = s.y;
|
|
573
|
+
action.timer += dt;
|
|
574
|
+
if (action.timer >= (action.duration ?? 3000)) {
|
|
575
|
+
s.autoAction = this.pickAutoAction();
|
|
576
|
+
s.arrived = false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Advance fail/reject timers
|
|
582
|
+
if (s.failTimer !== undefined) s.failTimer += dt;
|
|
583
|
+
if (s.rejectTimer !== undefined) s.rejectTimer += dt;
|
|
584
|
+
|
|
585
|
+
// Movement (skip while being dragged)
|
|
586
|
+
if (s.dragging) {
|
|
587
|
+
// Position is updated directly by drag handler, skip movement logic
|
|
588
|
+
} else {
|
|
589
|
+
|
|
590
|
+
const dx = s.targetX - s.x;
|
|
591
|
+
const dy = s.targetY - s.y;
|
|
592
|
+
const dist = Math.hypot(dx, dy);
|
|
593
|
+
if (dist > MOVE_SPEED * 2) {
|
|
594
|
+
s.arrived = false;
|
|
595
|
+
if (Math.abs(dx) > 0.5) s.facingLeft = dx < 0;
|
|
596
|
+
let nx = s.x + (dx / dist) * MOVE_SPEED;
|
|
597
|
+
let ny = s.y + (dy / dist) * MOVE_SPEED;
|
|
598
|
+
// Obstacle avoidance when idle wandering
|
|
599
|
+
if (isIdle && !isInPeerChat) {
|
|
600
|
+
for (const o of OBSTACLES) {
|
|
601
|
+
const od = Math.hypot(nx - o.x, ny - o.y);
|
|
602
|
+
if (od < o.r + 8 && od > 0.1) {
|
|
603
|
+
nx = o.x + ((nx - o.x) / od) * (o.r + 8);
|
|
604
|
+
ny = o.y + ((ny - o.y) / od) * (o.r + 8);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
s.x = nx; s.y = ny;
|
|
609
|
+
} else {
|
|
610
|
+
if (!s.arrived) { s.x = s.targetX; s.y = s.targetY; }
|
|
611
|
+
s.arrived = true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Master visit: when agent arrives at Master during working state
|
|
615
|
+
if ((state === 'working' || state === 'tool_call') && !s.visitedMaster && s.arrived
|
|
616
|
+
&& Math.hypot(s.x - MASTER_POS.x - 30, s.y - MASTER_POS.y) < MOVE_SPEED * 3) {
|
|
617
|
+
s.visitedMaster = true;
|
|
618
|
+
s.arrived = false;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Peer chat: face toward partner when arrived
|
|
622
|
+
if (isInPeerChat && s.arrived) {
|
|
623
|
+
const session = useMonitorStore.getState().chatSessions.get(agent.chatSessionId!);
|
|
624
|
+
if (session) {
|
|
625
|
+
s.facingLeft = session.meetPoint.x < s.x;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
} // end of !dragging else block
|
|
630
|
+
|
|
631
|
+
// Animation frame
|
|
632
|
+
const frames = ANIMS[s.animKey] ?? ANIMS.idle;
|
|
633
|
+
const spd = ANIM_SPEED[s.animKey] ?? 800;
|
|
634
|
+
s.animTime += dt;
|
|
635
|
+
if (s.animTime >= spd) {
|
|
636
|
+
s.animTime = 0;
|
|
637
|
+
const idx = frames.indexOf(s.frameIdx);
|
|
638
|
+
s.frameIdx = idx === -1 ? frames[0] : frames[(idx + 1) % frames.length];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Apply to Pixi sprite
|
|
642
|
+
const container = s.sprite.parent as Container;
|
|
643
|
+
container.x = s.x;
|
|
644
|
+
container.y = s.y;
|
|
645
|
+
s.sprite.texture = this.agentFrames[s.frameIdx] ?? this.agentFrames[0];
|
|
646
|
+
const facing = s.animKey === 'attack' ? -1 : (s.facingLeft ? -1 : 1);
|
|
647
|
+
s.sprite.scale.x = facing * SPRITE_SCALE;
|
|
648
|
+
|
|
649
|
+
// Wobble effects for shake_hand / head_shake (ticker-synced)
|
|
650
|
+
if (s.animKey === 'shake_hand' || s.animKey === 'head_shake') {
|
|
651
|
+
s.wobbleTime += dt;
|
|
652
|
+
const freq = s.animKey === 'shake_hand' ? 0.02 : 0.03;
|
|
653
|
+
const amp = s.animKey === 'shake_hand' ? 3 : 4;
|
|
654
|
+
container.x = s.x + Math.sin(s.wobbleTime * freq) * amp;
|
|
655
|
+
} else {
|
|
656
|
+
s.wobbleTime = 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Bubble chunk cycling
|
|
660
|
+
if (s.bubble.visible && s.bubbleChunks.length > 0) {
|
|
661
|
+
s.bubbleChunkTimer += dt;
|
|
662
|
+
if (s.bubbleChunkTimer >= BUBBLE_CHUNK_INTERVAL && s.bubbleChunks.length > 1) {
|
|
663
|
+
s.bubbleChunkTimer = 0;
|
|
664
|
+
s.bubbleChunkIdx = (s.bubbleChunkIdx + 1) % s.bubbleChunks.length;
|
|
665
|
+
}
|
|
666
|
+
const text = s.bubbleChunks[s.bubbleChunkIdx] ?? '';
|
|
667
|
+
s.bubbleText.text = text;
|
|
668
|
+
const bw = Math.min(Math.max(text.length * 5 + 16, 50), 160);
|
|
669
|
+
const bh = 22;
|
|
670
|
+
const by = -FRAME_SIZE * SPRITE_SCALE - 16 - bh;
|
|
671
|
+
s.bubbleBg.clear();
|
|
672
|
+
s.bubbleBg.roundRect(-bw / 2, by, bw, bh, 6);
|
|
673
|
+
s.bubbleBg.fill({ color: 0xffffff, alpha: 0.92 });
|
|
674
|
+
s.bubbleText.y = by + 4;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Sync position to store (throttled)
|
|
678
|
+
const storeAgent = useMonitorStore.getState().agents.get(id);
|
|
679
|
+
if (storeAgent && Math.hypot(storeAgent.position.x - s.x, storeAgent.position.y - s.y) > 1) {
|
|
680
|
+
useMonitorStore.getState().setPosition(id, s.x, s.y);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Sync anim
|
|
684
|
+
const storeAnim = this.animToStoreAnim(s.animKey);
|
|
685
|
+
if (storeAgent && storeAgent.currentAnim !== storeAnim) {
|
|
686
|
+
useMonitorStore.getState().setAnim(id, storeAnim);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Master bubble: show client's message with chunked display
|
|
691
|
+
const masterMsg = useMonitorStore.getState().masterMessage;
|
|
692
|
+
if (masterMsg) {
|
|
693
|
+
this.masterBubble.visible = true;
|
|
694
|
+
if (masterMsg !== this.masterBubbleFullText) {
|
|
695
|
+
this.masterBubbleFullText = masterMsg;
|
|
696
|
+
this.masterBubbleChunks = splitIntoChunks(masterMsg);
|
|
697
|
+
this.masterBubbleChunkIdx = 0;
|
|
698
|
+
this.masterBubbleChunkTimer = 0;
|
|
699
|
+
}
|
|
700
|
+
this.masterBubbleChunkTimer += dt;
|
|
701
|
+
if (this.masterBubbleChunkTimer >= BUBBLE_CHUNK_INTERVAL && this.masterBubbleChunks.length > 1) {
|
|
702
|
+
this.masterBubbleChunkTimer = 0;
|
|
703
|
+
this.masterBubbleChunkIdx = (this.masterBubbleChunkIdx + 1) % this.masterBubbleChunks.length;
|
|
704
|
+
}
|
|
705
|
+
const text = this.masterBubbleChunks[this.masterBubbleChunkIdx] ?? '';
|
|
706
|
+
this.masterBubbleText.text = text;
|
|
707
|
+
const bw = Math.min(Math.max(text.length * 5 + 16, 50), 160);
|
|
708
|
+
const bh = 22;
|
|
709
|
+
const by = -FRAME_SIZE * SPRITE_SCALE - 16 - bh;
|
|
710
|
+
this.masterBubbleBg.clear();
|
|
711
|
+
this.masterBubbleBg.roundRect(-bw / 2, by, bw, bh, 6);
|
|
712
|
+
this.masterBubbleBg.fill({ color: 0xfff8dc, alpha: 0.95 });
|
|
713
|
+
this.masterBubbleText.y = by + 4;
|
|
714
|
+
} else {
|
|
715
|
+
this.masterBubble.visible = false;
|
|
716
|
+
this.masterBubbleFullText = '';
|
|
717
|
+
this.masterBubbleChunks = [];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Depth sort: objects with higher y should render in front
|
|
721
|
+
this.scene.children.sort((a, b) => (a.y || 0) - (b.y || 0));
|
|
722
|
+
|
|
723
|
+
// Update interactables
|
|
724
|
+
for (const ist of this.interactables) {
|
|
725
|
+
const agents = useMonitorStore.getState().agents;
|
|
726
|
+
const isActive = this.checkInteractableActive(ist, agents);
|
|
727
|
+
|
|
728
|
+
if (ist.type === 'stake') {
|
|
729
|
+
if (isActive) {
|
|
730
|
+
const t = performance.now() / 1000;
|
|
731
|
+
const decay = Math.exp(-((t % 0.4) * 5));
|
|
732
|
+
ist.shakeX = Math.sin(t * 20) * 3 * decay;
|
|
733
|
+
const hp = (t * 5) % 1;
|
|
734
|
+
ist.scaleX = hp < 0.2 ? 1 + (0.2 - hp) * 0.15 : 1;
|
|
735
|
+
} else {
|
|
736
|
+
ist.shakeX *= 0.85;
|
|
737
|
+
ist.scaleX += (1 - ist.scaleX) * 0.15;
|
|
738
|
+
if (Math.abs(ist.shakeX) < 0.1) ist.shakeX = 0;
|
|
739
|
+
}
|
|
740
|
+
ist.sprite.x = ist.anchorX + ist.shakeX;
|
|
741
|
+
ist.sprite.width = ist.designW * ist.scaleX;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (ist.type === 'cushion') {
|
|
745
|
+
const target = isActive ? 1 : 0;
|
|
746
|
+
const diff = target - ist.deform;
|
|
747
|
+
if (Math.abs(diff) > 0.005) ist.deform += diff * (isActive ? 0.12 : 0.06);
|
|
748
|
+
else ist.deform = target;
|
|
749
|
+
ist.sprite.width = ist.designW * (1 + ist.deform * 0.12);
|
|
750
|
+
ist.sprite.height = ist.designH * (1 - ist.deform * 0.25);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private checkInteractableActive(ist: InteractableState, agents: Map<string, Agent>): boolean {
|
|
756
|
+
for (const a of agents.values()) {
|
|
757
|
+
if (ist.type === 'stake') {
|
|
758
|
+
if (a.currentAnim !== 'attack') continue;
|
|
759
|
+
if (Math.abs(a.position.x - ist.agentX) < 30 && Math.abs(a.position.y - ist.agentY) < 30) return true;
|
|
760
|
+
}
|
|
761
|
+
if (ist.type === 'cushion') {
|
|
762
|
+
if (a.currentAnim !== 'sit') continue;
|
|
763
|
+
if (Math.abs(a.homePosition.x - ist.agentX) < 5 && Math.abs(a.homePosition.y - ist.agentY) < 5 &&
|
|
764
|
+
Math.abs(a.position.x - a.homePosition.x) < 20 && Math.abs(a.position.y - a.homePosition.y) < 20) return true;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private pickAutoAction() {
|
|
771
|
+
const r = Math.random();
|
|
772
|
+
// Prefer sitting on cushion (idle default per R10: "坐在蒲团上发呆")
|
|
773
|
+
if (r < 0.45) return { kind: 'sit', duration: 5000 + Math.random() * 8000, timer: 0 };
|
|
774
|
+
if (r < 0.70) return { kind: 'wander', target: randomFloorPos(), timer: 0 };
|
|
775
|
+
if (r < 0.85) return { kind: 'daydream', duration: 3000 + Math.random() * 5000, timer: 0 };
|
|
776
|
+
return { kind: 'pause', duration: 1000 + Math.random() * 2000, timer: 0 };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private animToStoreAnim(key: string): AgentAnim {
|
|
780
|
+
switch (key) {
|
|
781
|
+
case 'attack': return 'attack';
|
|
782
|
+
case 'sit': return 'sit';
|
|
783
|
+
case 'walk': return 'walk';
|
|
784
|
+
case 'chat': return 'chat';
|
|
785
|
+
case 'daydream': return 'daydream';
|
|
786
|
+
case 'shake_hand': return 'shake_hand';
|
|
787
|
+
case 'head_shake': return 'head_shake';
|
|
788
|
+
default: return 'idle';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
destroy() {
|
|
793
|
+
try {
|
|
794
|
+
this.app.ticker.stop();
|
|
795
|
+
this.app.stage.removeChildren();
|
|
796
|
+
this.app.destroy(true, { children: true });
|
|
797
|
+
} catch { /* ignore cleanup errors */ }
|
|
798
|
+
}
|
|
799
|
+
}
|