opencroc 1.8.2 → 1.8.3

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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/dist/web/index.html +0 -12
  3. package/dist/web/public/botreview/char_0.png +0 -0
  4. package/dist/web/public/botreview/char_1.png +0 -0
  5. package/dist/web/public/botreview/char_2.png +0 -0
  6. package/dist/web/public/botreview/coffee-machine.gif +0 -0
  7. package/dist/web/public/botreview/server.gif +0 -0
  8. package/dist/web/public/botreview/walls.png +0 -0
  9. package/dist/web/public/star/desk-v3.webp +0 -0
  10. package/dist/web/public/star/office_bg_small.webp +0 -0
  11. package/dist/web/public/star/star-idle-v5.png +0 -0
  12. package/dist/web/public/star/star-working-spritesheet-grid.webp +0 -0
  13. package/dist/web/src/app/AppLayout.tsx +0 -34
  14. package/dist/web/src/app/AppRouter.tsx +0 -46
  15. package/dist/web/src/app/bootstrap.tsx +0 -22
  16. package/dist/web/src/app/routes.tsx +0 -52
  17. package/dist/web/src/features/office/runtime/index.ts +0 -1
  18. package/dist/web/src/features/office/runtime/mount.ts +0 -809
  19. package/dist/web/src/features/pixel/runtime/index.ts +0 -1
  20. package/dist/web/src/features/pixel/runtime/mount.ts +0 -728
  21. package/dist/web/src/features/studio/runtime/index.ts +0 -1
  22. package/dist/web/src/features/studio/runtime/mount.ts +0 -664
  23. package/dist/web/src/features/three/engine/index.ts +0 -1
  24. package/dist/web/src/main.tsx +0 -7
  25. package/dist/web/src/pages/office/index.ts +0 -1
  26. package/dist/web/src/pages/office/page.tsx +0 -283
  27. package/dist/web/src/pages/pixel/index.ts +0 -1
  28. package/dist/web/src/pages/pixel/page.tsx +0 -564
  29. package/dist/web/src/pages/studio/index.ts +0 -1
  30. package/dist/web/src/pages/studio/page.tsx +0 -446
  31. package/dist/web/src/runtime/agents.ts +0 -738
  32. package/dist/web/src/runtime/camera.ts +0 -132
  33. package/dist/web/src/runtime/dataviz.ts +0 -312
  34. package/dist/web/src/runtime/effects.ts +0 -482
  35. package/dist/web/src/runtime/engine.ts +0 -528
  36. package/dist/web/src/runtime/office.ts +0 -932
  37. package/dist/web/src/runtime/state.ts +0 -37
  38. package/dist/web/src/runtime/ui.ts +0 -388
  39. package/dist/web/src/shared/assets.ts +0 -4
  40. package/dist/web/src/shared/navigation.ts +0 -47
  41. package/dist/web/src/styles/app-layout.css +0 -19
  42. package/dist/web/src/styles/office.css +0 -268
  43. package/dist/web/tsconfig.json +0 -28
  44. package/dist/web/vite.config.ts +0 -93
@@ -1,738 +0,0 @@
1
- /* ═══════════════════════════════════════════════════════════════════════════════
2
- OpenCroc Studio 3D — Agent Robot Characters
3
- Low-poly robot agents built from Three.js primitives
4
- ~2500 lines
5
- ═══════════════════════════════════════════════════════════════════════════════ */
6
-
7
- import * as THREE from 'three';
8
- import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
9
- import { DESK_POSITIONS, POND_POSITIONS, setDeskOccupied } from './office';
10
-
11
- /* ─── Module state ─────────────────────────────────────────────────────────── */
12
- const agents = new Map(); // name → { group, parts, label, bubble, anim }
13
- let scene = null;
14
- let css2dRenderer = null;
15
- let css2dResizeHandler = null;
16
-
17
- function disposeObject3D(object) {
18
- object?.traverse?.((child) => {
19
- child.geometry?.dispose?.();
20
- if (Array.isArray(child.material)) {
21
- child.material.forEach((material) => material?.dispose?.());
22
- } else {
23
- child.material?.dispose?.();
24
- }
25
- });
26
- }
27
-
28
- /* ─── Robot Colors per Role ────────────────────────────────────────────────── */
29
- const ROLE_COLORS = {
30
- parser: { body: 0x60a5fa, accent: 0x3b82f6, eye: 0xdbeafe, glow: 0x60a5fa },
31
- analyzer: { body: 0xa78bfa, accent: 0x8b5cf6, eye: 0xede9fe, glow: 0xa78bfa },
32
- tester: { body: 0x34d399, accent: 0x10b981, eye: 0xd1fae5, glow: 0x34d399 },
33
- healer: { body: 0xfbbf24, accent: 0xf59e0b, eye: 0xfef3c7, glow: 0xfbbf24 },
34
- planner: { body: 0xf472b6, accent: 0xec4899, eye: 0xfce7f3, glow: 0xf472b6 },
35
- reporter: { body: 0x22d3ee, accent: 0x06b6d4, eye: 0xcffafe, glow: 0x22d3ee },
36
- };
37
-
38
- const DEFAULT_COLORS = { body: 0x94a3b8, accent: 0x64748b, eye: 0xf1f5f9, glow: 0x94a3b8 };
39
-
40
- /* ─── Animation parameters per status ─────────────────────────────────────── */
41
- const STATUS_ANIM = {
42
- idle: { speed: 0.5, bobAmp: 0.03, rotSpeed: 0 },
43
- working: { speed: 2.0, bobAmp: 0.06, rotSpeed: 0.5 },
44
- testing: { speed: 2.5, bobAmp: 0.08, rotSpeed: 0.8 },
45
- thinking: { speed: 1.0, bobAmp: 0.02, rotSpeed: 0.3 },
46
- error: { speed: 4.0, bobAmp: 0.04, rotSpeed: 0, shake: true },
47
- failed: { speed: 4.0, bobAmp: 0.04, rotSpeed: 0, shake: true },
48
- done: { speed: 1.0, bobAmp: 0.05, rotSpeed: 0.2 },
49
- passed: { speed: 1.0, bobAmp: 0.05, rotSpeed: 0.2 },
50
- };
51
-
52
- const ACTIVE_STATUSES = new Set([
53
- 'working',
54
- 'testing',
55
- 'thinking',
56
- 'scanning',
57
- 'navigating',
58
- 'interacting',
59
- 'asserting',
60
- 'reporting',
61
- ]);
62
-
63
- /* ═══════════════════════════════════════════════════════════════════════════════
64
- AgentManager — Creates, updates, removes 3D robot agents
65
- ═══════════════════════════════════════════════════════════════════════════════ */
66
- export class AgentManager {
67
- constructor(sceneRef) {
68
- scene = sceneRef;
69
- this._time = 0;
70
- this._bubbleTimers = new Map();
71
- this._deskAssignments = new Map();
72
- this._eventAssignments = new Map();
73
- this._initCSS2D();
74
- }
75
-
76
- /** Initialize CSS2D renderer for labels and bubbles */
77
- _initCSS2D() {
78
- if (css2dRenderer?.domElement?.parentNode) {
79
- css2dRenderer.domElement.parentNode.removeChild(css2dRenderer.domElement);
80
- }
81
- css2dRenderer = new CSS2DRenderer();
82
- css2dRenderer.setSize(window.innerWidth, window.innerHeight);
83
- css2dRenderer.domElement.style.position = 'fixed';
84
- css2dRenderer.domElement.style.top = '0';
85
- css2dRenderer.domElement.style.left = '0';
86
- css2dRenderer.domElement.style.pointerEvents = 'none';
87
- css2dRenderer.domElement.style.zIndex = '5';
88
- document.body.appendChild(css2dRenderer.domElement);
89
-
90
- css2dResizeHandler = () => {
91
- css2dRenderer.setSize(window.innerWidth, window.innerHeight);
92
- };
93
- window.addEventListener('resize', css2dResizeHandler);
94
- }
95
-
96
- /** Sync agents from backend data */
97
- sync(agentData) {
98
- const current = new Set();
99
- const active = new Set();
100
-
101
- agentData.forEach((a, i) => {
102
- current.add(a.name);
103
- if (!agents.has(a.name)) {
104
- this._createRobot(a, i, agentData.length);
105
- }
106
- this._updateStatus(a.name, a.status);
107
- const eventActive = this._eventAssignments.get(a.name);
108
- const isActive = typeof eventActive === 'boolean' ? eventActive : this._isActiveStatus(a.status);
109
- if (isActive) active.add(a.name);
110
- });
111
-
112
- // Drop stale event-assignment flags for removed agents.
113
- for (const name of this._eventAssignments.keys()) {
114
- if (!current.has(name)) this._eventAssignments.delete(name);
115
- }
116
-
117
- // Release desks for idle/done agents.
118
- for (const [name, deskIdx] of this._deskAssignments) {
119
- if (!current.has(name) || !active.has(name)) {
120
- this._deskAssignments.delete(name);
121
- }
122
- }
123
-
124
- // Assign desks to active agents that don't have one yet.
125
- active.forEach((name) => {
126
- if (!this._deskAssignments.has(name)) {
127
- const deskIdx = this._nextFreeDesk();
128
- if (deskIdx >= 0) this._deskAssignments.set(name, deskIdx);
129
- }
130
- });
131
-
132
- this._syncDeskOccupancy();
133
-
134
- // Update movement targets by current allocation.
135
- current.forEach((name) => {
136
- const agent = agents.get(name);
137
- if (!agent) return;
138
- const deskIdx = this._deskAssignments.get(name);
139
- if (typeof deskIdx === 'number' && DESK_POSITIONS[deskIdx]) {
140
- const desk = DESK_POSITIONS[deskIdx];
141
- this._setTarget(agent, desk.x, desk.z + 1.2, 'desk', desk);
142
- } else {
143
- const pond = POND_POSITIONS[agent.pondSlot % Math.max(1, POND_POSITIONS.length)] || { x: -9, z: 6.2 };
144
- this._setTarget(agent, pond.x, pond.z, 'pond');
145
- }
146
- });
147
-
148
- // Remove stale agents
149
- for (const [name] of agents) {
150
- if (!current.has(name)) {
151
- this._deskAssignments.delete(name);
152
- this._removeRobot(name);
153
- }
154
- }
155
-
156
- // Schedule bubbles
157
- this._scheduleBubbles(agentData);
158
- }
159
-
160
- applyAssignmentEvent(payload) {
161
- const name = payload?.name;
162
- if (!name || !agents.has(name)) return null;
163
-
164
- this._eventAssignments.set(name, true);
165
- if (!this._deskAssignments.has(name)) {
166
- const deskIdx = this._nextFreeDesk();
167
- if (deskIdx >= 0) this._deskAssignments.set(name, deskIdx);
168
- }
169
-
170
- const agent = agents.get(name);
171
- const deskIdx = this._deskAssignments.get(name);
172
- if (!agent || typeof deskIdx !== 'number' || !DESK_POSITIONS[deskIdx]) return null;
173
-
174
- const desk = DESK_POSITIONS[deskIdx];
175
- const from = { x: agent.baseX, z: agent.baseZ };
176
- this._setTarget(agent, desk.x, desk.z + 1.2, 'desk', desk);
177
- this._syncDeskOccupancy();
178
- this._flashSummon(name);
179
-
180
- return { from, to: { x: desk.x, z: desk.z + 1.2 }, kind: 'assigned' };
181
- }
182
-
183
- applyReleaseEvent(payload) {
184
- const name = payload?.name;
185
- if (!name || !agents.has(name)) return null;
186
-
187
- this._eventAssignments.set(name, false);
188
- this._deskAssignments.delete(name);
189
-
190
- const agent = agents.get(name);
191
- if (!agent) return null;
192
-
193
- const pond = POND_POSITIONS[agent.pondSlot % Math.max(1, POND_POSITIONS.length)] || { x: -9, z: 6.2 };
194
- const from = { x: agent.baseX, z: agent.baseZ };
195
- this._setTarget(agent, pond.x, pond.z, 'pond');
196
- this._syncDeskOccupancy();
197
-
198
- return { from, to: { x: pond.x, z: pond.z }, kind: 'released' };
199
- }
200
-
201
- _flashSummon(name) {
202
- const agent = agents.get(name);
203
- if (!agent) return;
204
-
205
- // Brief glow spike
206
- if (agent.parts.glow) {
207
- const prev = agent.parts.glow.intensity;
208
- agent.parts.glow.intensity = 1.9;
209
- setTimeout(() => {
210
- const a = agents.get(name);
211
- if (a && a.parts.glow) a.parts.glow.intensity = prev;
212
- }, 700);
213
- }
214
-
215
- // Expanding ring at robot feet (blue for assignment)
216
- const ringGeo = new THREE.TorusGeometry(0.3, 0.04, 8, 20);
217
- const ringMat = new THREE.MeshBasicMaterial({
218
- color: 0x60a5fa, transparent: true, opacity: 0.88, depthWrite: false,
219
- });
220
- const ring = new THREE.Mesh(ringGeo, ringMat);
221
- ring.rotation.x = -Math.PI / 2;
222
- ring.position.set(agent.baseX, 0.24, agent.baseZ);
223
- scene.add(ring);
224
-
225
- let life = 0;
226
- const ttl = 0.78;
227
- const tick = () => {
228
- life += 0.016;
229
- ring.position.set(agent.baseX, 0.24, agent.baseZ);
230
- ring.scale.setScalar(1 + (life / ttl) * 4.5);
231
- ring.material.opacity = Math.max(0, 0.88 * (1 - life / ttl));
232
- if (life < ttl) {
233
- requestAnimationFrame(tick);
234
- } else {
235
- scene.remove(ring);
236
- ringGeo.dispose();
237
- ringMat.dispose();
238
- }
239
- };
240
- requestAnimationFrame(tick);
241
- }
242
-
243
- /** Update all agents each frame */
244
- update(dt) {
245
- this._time += dt;
246
-
247
- for (const [name, agent] of agents) {
248
- const anim = STATUS_ANIM[agent.status] || STATUS_ANIM.idle;
249
-
250
- let moveTargetX = agent.targetX;
251
- let moveTargetZ = agent.targetZ;
252
- if (agent.path.length) {
253
- moveTargetX = agent.path[0].x;
254
- moveTargetZ = agent.path[0].z;
255
- }
256
-
257
- // Move toward target zone.
258
- const dx = moveTargetX - agent.baseX;
259
- const dz = moveTargetZ - agent.baseZ;
260
- const dist = Math.hypot(dx, dz);
261
- if (dist > 0.01) {
262
- const speed = agent.zone === 'desk' ? 4.2 : 2.6;
263
- const step = Math.min(1, (dt * speed) / dist);
264
- agent.baseX += dx * step;
265
- agent.baseZ += dz * step;
266
- agent.group.lookAt(new THREE.Vector3(moveTargetX, 0.2, moveTargetZ));
267
- } else if (agent.path.length) {
268
- agent.path.shift();
269
- } else if (agent.zone === 'desk' && agent.deskPos) {
270
- agent.group.lookAt(new THREE.Vector3(agent.deskPos.x, 0.2, agent.deskPos.z));
271
- } else if (agent.zone === 'pond') {
272
- agent.group.lookAt(new THREE.Vector3(-9, 0.2, 6.2));
273
- }
274
-
275
- // Bobbing
276
- const bobY = Math.sin(this._time * anim.speed * 2) * anim.bobAmp;
277
- agent.group.position.y = agent.baseY + bobY;
278
-
279
- // Arm rotation (working animation)
280
- if (agent.parts.leftArm) {
281
- agent.parts.leftArm.rotation.x = Math.sin(this._time * anim.speed) * 0.3;
282
- }
283
- if (agent.parts.rightArm) {
284
- agent.parts.rightArm.rotation.x = -Math.sin(this._time * anim.speed) * 0.3;
285
- }
286
-
287
- // Head rotation (thinking)
288
- if (agent.parts.head && anim.rotSpeed > 0) {
289
- agent.parts.head.rotation.y = Math.sin(this._time * anim.rotSpeed) * 0.2;
290
- }
291
-
292
- // Shake effect (error)
293
- if (anim.shake) {
294
- agent.group.position.x = agent.baseX + Math.sin(this._time * 30) * 0.04;
295
- agent.group.position.z = agent.baseZ + Math.cos(this._time * 25) * 0.02;
296
- } else {
297
- agent.group.position.x = agent.baseX;
298
- agent.group.position.z = agent.baseZ;
299
- }
300
-
301
- // Eye glow pulsing
302
- if (agent.parts.leftEye && agent.parts.rightEye) {
303
- const eyePulse = 0.5 + 0.5 * Math.sin(this._time * anim.speed * 1.5);
304
- agent.parts.leftEye.material.opacity = 0.6 + eyePulse * 0.4;
305
- agent.parts.rightEye.material.opacity = 0.6 + eyePulse * 0.4;
306
- }
307
-
308
- // Antenna glow
309
- if (agent.parts.antenna) {
310
- const glow = 0.3 + 0.7 * Math.abs(Math.sin(this._time * 3));
311
- agent.parts.antenna.material.emissiveIntensity = glow;
312
- }
313
- }
314
-
315
- // Update CSS2D renderer
316
- if (css2dRenderer && scene) {
317
- // find camera from scene parent
318
- const camera = scene.userData.camera;
319
- if (camera) css2dRenderer.render(scene, camera);
320
- }
321
- }
322
-
323
- /* ═════════════════════════════════════════════════════════════════════════
324
- Robot Construction — Build a low-poly robot from primitives
325
- ═════════════════════════════════════════════════════════════════════════ */
326
- _createRobot(agentData, index, total) {
327
- const role = agentData.role || 'parser';
328
- // Support dynamic color from server (hex string like '#60a5fa')
329
- let colors = ROLE_COLORS[role] || DEFAULT_COLORS;
330
- if (agentData.color && !ROLE_COLORS[role]) {
331
- const hex = parseInt(agentData.color.replace('#', ''), 16);
332
- if (!isNaN(hex)) {
333
- const lighter = new THREE.Color(hex).lerp(new THREE.Color(0xffffff), 0.35).getHex();
334
- colors = { body: hex, accent: hex, eye: lighter, glow: hex };
335
- }
336
- }
337
- const group = new THREE.Group();
338
- group.name = `agent-${agentData.name}`;
339
-
340
- const parts = {};
341
-
342
- // Materials
343
- const bodyMat = new THREE.MeshStandardMaterial({
344
- color: colors.body, roughness: 0.4, metalness: 0.5,
345
- });
346
- const accentMat = new THREE.MeshStandardMaterial({
347
- color: colors.accent, roughness: 0.3, metalness: 0.6,
348
- });
349
- const eyeMat = new THREE.MeshBasicMaterial({
350
- color: colors.eye, transparent: true, opacity: 0.9,
351
- });
352
- const metalMat = new THREE.MeshStandardMaterial({
353
- color: 0x94a3b8, roughness: 0.2, metalness: 0.8,
354
- });
355
- const glowMat = new THREE.MeshStandardMaterial({
356
- color: colors.glow, roughness: 0.3, metalness: 0.4,
357
- emissive: colors.glow, emissiveIntensity: 0.5,
358
- });
359
-
360
- /* ── Body (torso) ────────────────────────────────────────────────────── */
361
- const bodyGeo = new THREE.BoxGeometry(0.5, 0.6, 0.35);
362
- const body = new THREE.Mesh(bodyGeo, bodyMat);
363
- body.position.y = 0.9;
364
- body.castShadow = true;
365
- group.add(body);
366
- parts.body = body;
367
-
368
- // Chest plate
369
- const chestGeo = new THREE.BoxGeometry(0.35, 0.35, 0.02);
370
- const chest = new THREE.Mesh(chestGeo, accentMat);
371
- chest.position.set(0, 0.95, 0.19);
372
- group.add(chest);
373
-
374
- // Chest LED
375
- const ledGeo = new THREE.CircleGeometry(0.04, 8);
376
- const led = new THREE.Mesh(ledGeo, glowMat);
377
- led.position.set(0, 1.0, 0.205);
378
- parts.chestLed = led;
379
- group.add(led);
380
-
381
- /* ── Head ────────────────────────────────────────────────────────────── */
382
- const headGeo = new THREE.BoxGeometry(0.4, 0.35, 0.3);
383
- const head = new THREE.Mesh(headGeo, bodyMat);
384
- head.position.y = 1.45;
385
- head.castShadow = true;
386
- group.add(head);
387
- parts.head = head;
388
-
389
- // Visor / Face plate
390
- const visorGeo = new THREE.BoxGeometry(0.35, 0.15, 0.02);
391
- const visor = new THREE.Mesh(visorGeo, new THREE.MeshStandardMaterial({
392
- color: 0x111827, roughness: 0.1, metalness: 0.9,
393
- }));
394
- visor.position.set(0, 1.48, 0.17);
395
- group.add(visor);
396
-
397
- // Eyes (glowing dots)
398
- const eyeGeo = new THREE.CircleGeometry(0.035, 8);
399
- const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
400
- leftEye.position.set(-0.08, 1.48, 0.185);
401
- group.add(leftEye);
402
- parts.leftEye = leftEye;
403
-
404
- const rightEye = new THREE.Mesh(eyeGeo, eyeMat.clone());
405
- rightEye.position.set(0.08, 1.48, 0.185);
406
- group.add(rightEye);
407
- parts.rightEye = rightEye;
408
-
409
- // Antenna
410
- const antennaGeo = new THREE.CylinderGeometry(0.015, 0.015, 0.2, 6);
411
- const antenna = new THREE.Mesh(antennaGeo, metalMat);
412
- antenna.position.set(0, 1.72, 0);
413
- group.add(antenna);
414
-
415
- // Antenna tip (glowing ball)
416
- const tipGeo = new THREE.SphereGeometry(0.035, 8, 8);
417
- const tip = new THREE.Mesh(tipGeo, glowMat);
418
- tip.position.set(0, 1.84, 0);
419
- group.add(tip);
420
- parts.antenna = tip;
421
-
422
- /* ── Arms ────────────────────────────────────────────────────────────── */
423
- // Left arm
424
- const armGeo = new THREE.BoxGeometry(0.12, 0.45, 0.12);
425
- const leftArm = new THREE.Mesh(armGeo, accentMat);
426
- leftArm.position.set(-0.36, 0.85, 0);
427
- leftArm.castShadow = true;
428
- group.add(leftArm);
429
- parts.leftArm = leftArm;
430
-
431
- // Left hand
432
- const handGeo = new THREE.SphereGeometry(0.06, 8, 8);
433
- const leftHand = new THREE.Mesh(handGeo, metalMat);
434
- leftHand.position.set(-0.36, 0.58, 0);
435
- group.add(leftHand);
436
-
437
- // Right arm
438
- const rightArm = new THREE.Mesh(armGeo, accentMat);
439
- rightArm.position.set(0.36, 0.85, 0);
440
- rightArm.castShadow = true;
441
- group.add(rightArm);
442
- parts.rightArm = rightArm;
443
-
444
- // Right hand
445
- const rightHand = new THREE.Mesh(handGeo, metalMat);
446
- rightHand.position.set(0.36, 0.58, 0);
447
- group.add(rightHand);
448
-
449
- /* ── Legs ────────────────────────────────────────────────────────────── */
450
- const legGeo = new THREE.BoxGeometry(0.14, 0.35, 0.14);
451
- const leftLeg = new THREE.Mesh(legGeo, bodyMat);
452
- leftLeg.position.set(-0.12, 0.38, 0);
453
- leftLeg.castShadow = true;
454
- group.add(leftLeg);
455
- parts.leftLeg = leftLeg;
456
-
457
- const rightLeg = new THREE.Mesh(legGeo, bodyMat);
458
- rightLeg.position.set(0.12, 0.38, 0);
459
- rightLeg.castShadow = true;
460
- group.add(rightLeg);
461
- parts.rightLeg = rightLeg;
462
-
463
- // Feet
464
- const footGeo = new THREE.BoxGeometry(0.16, 0.06, 0.2);
465
- const leftFoot = new THREE.Mesh(footGeo, accentMat);
466
- leftFoot.position.set(-0.12, 0.23, 0.03);
467
- group.add(leftFoot);
468
-
469
- const rightFoot = new THREE.Mesh(footGeo, accentMat);
470
- rightFoot.position.set(0.12, 0.23, 0.03);
471
- group.add(rightFoot);
472
-
473
- /* ── Backpack (jet-pack) ─────────────────────────────────────────────── */
474
- const backpackGeo = new THREE.BoxGeometry(0.25, 0.3, 0.15);
475
- const backpack = new THREE.Mesh(backpackGeo, accentMat);
476
- backpack.position.set(0, 0.95, -0.25);
477
- backpack.castShadow = true;
478
- group.add(backpack);
479
-
480
- // Exhaust ports
481
- const exhaustGeo = new THREE.CylinderGeometry(0.03, 0.04, 0.06, 6);
482
- const exhaust1 = new THREE.Mesh(exhaustGeo, metalMat);
483
- exhaust1.position.set(-0.06, 0.77, -0.3);
484
- group.add(exhaust1);
485
- const exhaust2 = exhaust1.clone();
486
- exhaust2.position.x = 0.06;
487
- group.add(exhaust2);
488
-
489
- /* ── Shadow blob ─────────────────────────────────────────────────────── */
490
- const shadowGeo = new THREE.CircleGeometry(0.3, 16);
491
- const shadowMat = new THREE.MeshBasicMaterial({
492
- color: 0x000000, transparent: true, opacity: 0.15,
493
- });
494
- const shadow = new THREE.Mesh(shadowGeo, shadowMat);
495
- shadow.rotation.x = -Math.PI / 2;
496
- shadow.position.y = 0.21;
497
- group.add(shadow);
498
-
499
- /* ── Position ────────────────────────────────────────────────────────── */
500
- const pondSlot = index % Math.max(1, POND_POSITIONS.length);
501
- const pond = POND_POSITIONS[pondSlot] || { x: -9, z: 6.2 };
502
- const x = pond.x;
503
- const z = pond.z;
504
-
505
- group.position.set(x, 0.2, z);
506
- group.lookAt(new THREE.Vector3(-9, 0.2, 6.2));
507
-
508
- scene.add(group);
509
-
510
- /* ── CSS2D Label ─────────────────────────────────────────────────────── */
511
- const labelDiv = document.createElement('div');
512
- labelDiv.className = 'agent-label-3d';
513
- labelDiv.innerHTML = `${agentData.name}<span class="role">${role}</span>`;
514
- const label = new CSS2DObject(labelDiv);
515
- label.position.set(0, 2.1, 0);
516
- group.add(label);
517
-
518
- /* ── Point light (glow effect around robot) ──────────────────────────── */
519
- const glow = new THREE.PointLight(colors.glow, 0.3, 4, 2);
520
- glow.position.set(0, 1.2, 0);
521
- group.add(glow);
522
- parts.glow = glow;
523
-
524
- /* ── Store ───────────────────────────────────────────────────────────── */
525
- agents.set(agentData.name, {
526
- group,
527
- parts,
528
- label,
529
- status: agentData.status || 'idle',
530
- role,
531
- baseX: x,
532
- baseY: 0.2,
533
- baseZ: z,
534
- deskPos: null,
535
- pondSlot,
536
- zone: 'pond',
537
- targetX: x,
538
- targetZ: z,
539
- path: [],
540
- });
541
- }
542
-
543
- /* ═════════════════════════════════════════════════════════════════════════
544
- Status Update
545
- ═════════════════════════════════════════════════════════════════════════ */
546
- _updateStatus(name, status) {
547
- const agent = agents.get(name);
548
- if (!agent) return;
549
- agent.status = status;
550
-
551
- // Update glow intensity based on status
552
- const intensityMap = {
553
- idle: 0.2, working: 0.6, testing: 0.7,
554
- thinking: 0.4, error: 1.0, failed: 1.0,
555
- done: 0.5, passed: 0.5,
556
- };
557
- if (agent.parts.glow) {
558
- agent.parts.glow.intensity = intensityMap[status] || 0.2;
559
- }
560
- }
561
-
562
- _isActiveStatus(status) {
563
- return ACTIVE_STATUSES.has(status || 'idle');
564
- }
565
-
566
- _nextFreeDesk() {
567
- const used = new Set(this._deskAssignments.values());
568
- for (let i = 0; i < DESK_POSITIONS.length; i++) {
569
- if (!used.has(i)) return i;
570
- }
571
- return -1;
572
- }
573
-
574
- _setTarget(agent, x, z, zone, deskPos = null) {
575
- const changed = zone !== agent.zone || Math.hypot(agent.targetX - x, agent.targetZ - z) > 0.06;
576
- if (!changed) {
577
- agent.deskPos = deskPos;
578
- return;
579
- }
580
-
581
- agent.targetX = x;
582
- agent.targetZ = z;
583
- agent.zone = zone;
584
- agent.deskPos = deskPos;
585
- agent.path = this._buildPath(agent, x, z, zone);
586
- }
587
-
588
- _buildPath(agent, targetX, targetZ, zone) {
589
- const path = [];
590
- const corridorZ = 2.6;
591
- const pondGateX = -6.4;
592
-
593
- const nearCorridor = Math.abs(agent.baseZ - corridorZ) < 0.6;
594
- if (!nearCorridor) {
595
- path.push({ x: agent.baseX, z: corridorZ });
596
- }
597
-
598
- if (zone === 'desk') {
599
- path.push({ x: targetX, z: corridorZ });
600
- path.push({ x: targetX, z: targetZ });
601
- return path;
602
- }
603
-
604
- path.push({ x: pondGateX, z: corridorZ + 1.1 });
605
- path.push({ x: targetX, z: targetZ });
606
- return path;
607
- }
608
-
609
- _syncDeskOccupancy() {
610
- for (let i = 0; i < DESK_POSITIONS.length; i++) {
611
- setDeskOccupied(i, false);
612
- }
613
- for (const deskIdx of this._deskAssignments.values()) {
614
- setDeskOccupied(deskIdx, true);
615
- }
616
- }
617
-
618
- /* ═════════════════════════════════════════════════════════════════════════
619
- Remove Robot
620
- ═════════════════════════════════════════════════════════════════════════ */
621
- _removeRobot(name) {
622
- const agent = agents.get(name);
623
- if (!agent) return;
624
- scene.remove(agent.group);
625
- disposeObject3D(agent.group);
626
- agents.delete(name);
627
- this._syncDeskOccupancy();
628
-
629
- // Clean bubble timer
630
- const bt = this._bubbleTimers.get(name);
631
- if (bt) { clearTimeout(bt); this._bubbleTimers.delete(name); }
632
- }
633
-
634
- /* ═════════════════════════════════════════════════════════════════════════
635
- Bubble System — 3D floating chat bubbles
636
- ═════════════════════════════════════════════════════════════════════════ */
637
- _scheduleBubbles(agentData) {
638
- const BUBBLE_TEXTS = {
639
- working: ['正在执行...', '快了快了', '处理中...', '加油 💪'],
640
- testing: ['跑测试中...', '验证 API...', '等结果...'],
641
- thinking: ['让我想想...', '分析中...', '推理...', '🤔'],
642
- error: ['出错了!', '修复中...', '糟糕...'],
643
- idle: ['摸鱼中~', '等任务...', '☕ 喝咖啡', 'zzZ'],
644
- done: ['搞定!', '完成 ✓', '下一个!'],
645
- passed: ['全绿 ✓', '测试通过!'],
646
- failed: ['有失败...', '需要修复'],
647
- };
648
-
649
- const current = new Set();
650
- agentData.forEach(a => {
651
- current.add(a.name);
652
- if (this._bubbleTimers.has(a.name)) return;
653
-
654
- const schedule = () => {
655
- const agent = agents.get(a.name);
656
- if (!agent) return;
657
- const status = agent.status || 'idle';
658
- const texts = BUBBLE_TEXTS[status] || BUBBLE_TEXTS.idle;
659
- const text = texts[Math.floor(Math.random() * texts.length)];
660
- this._showBubble(a.name, text);
661
- const next = 6000 + Math.random() * 8000;
662
- this._bubbleTimers.set(a.name, setTimeout(schedule, next));
663
- };
664
-
665
- const delay = 1000 + Math.random() * 3000;
666
- this._bubbleTimers.set(a.name, setTimeout(schedule, delay));
667
- });
668
-
669
- // Remove timers for removed agents
670
- for (const [name, timer] of this._bubbleTimers) {
671
- if (!current.has(name)) {
672
- clearTimeout(timer);
673
- this._bubbleTimers.delete(name);
674
- }
675
- }
676
- }
677
-
678
- _showBubble(name, text) {
679
- const agent = agents.get(name);
680
- if (!agent) return;
681
-
682
- // Remove existing bubble
683
- if (agent.bubbleObj) {
684
- agent.group.remove(agent.bubbleObj);
685
- }
686
-
687
- const div = document.createElement('div');
688
- div.className = 'bubble-3d';
689
- div.textContent = text;
690
-
691
- const bubble = new CSS2DObject(div);
692
- bubble.position.set(0.4, 2.3, 0);
693
- agent.group.add(bubble);
694
- agent.bubbleObj = bubble;
695
-
696
- // Remove after 3 seconds
697
- setTimeout(() => {
698
- if (agent.bubbleObj === bubble) {
699
- agent.group.remove(bubble);
700
- agent.bubbleObj = null;
701
- }
702
- }, 3000);
703
- }
704
-
705
- dispose() {
706
- for (const timer of this._bubbleTimers.values()) {
707
- clearTimeout(timer);
708
- }
709
- this._bubbleTimers.clear();
710
- this._deskAssignments.clear();
711
- this._eventAssignments.clear();
712
-
713
- for (const name of [...agents.keys()]) {
714
- this._removeRobot(name);
715
- }
716
-
717
- if (css2dResizeHandler) {
718
- window.removeEventListener('resize', css2dResizeHandler);
719
- css2dResizeHandler = null;
720
- }
721
-
722
- if (css2dRenderer?.domElement?.parentNode) {
723
- css2dRenderer.domElement.parentNode.removeChild(css2dRenderer.domElement);
724
- }
725
-
726
- css2dRenderer = null;
727
- scene = null;
728
- }
729
- }
730
-
731
- /* ═══════════════════════════════════════════════════════════════════════════════
732
- Helper exports
733
- ═══════════════════════════════════════════════════════════════════════════════ */
734
- export function getAgentPosition(name) {
735
- const agent = agents.get(name);
736
- if (!agent) return null;
737
- return agent.group.position.clone();
738
- }