opencroc 1.8.2 → 1.8.4
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 +383 -417
- package/package.json +1 -1
- package/dist/web/index.html +0 -12
- package/dist/web/public/botreview/char_0.png +0 -0
- package/dist/web/public/botreview/char_1.png +0 -0
- package/dist/web/public/botreview/char_2.png +0 -0
- package/dist/web/public/botreview/coffee-machine.gif +0 -0
- package/dist/web/public/botreview/server.gif +0 -0
- package/dist/web/public/botreview/walls.png +0 -0
- package/dist/web/public/star/desk-v3.webp +0 -0
- package/dist/web/public/star/office_bg_small.webp +0 -0
- package/dist/web/public/star/star-idle-v5.png +0 -0
- package/dist/web/public/star/star-working-spritesheet-grid.webp +0 -0
- package/dist/web/src/app/AppLayout.tsx +0 -34
- package/dist/web/src/app/AppRouter.tsx +0 -46
- package/dist/web/src/app/bootstrap.tsx +0 -22
- package/dist/web/src/app/routes.tsx +0 -52
- package/dist/web/src/features/office/runtime/index.ts +0 -1
- package/dist/web/src/features/office/runtime/mount.ts +0 -809
- package/dist/web/src/features/pixel/runtime/index.ts +0 -1
- package/dist/web/src/features/pixel/runtime/mount.ts +0 -728
- package/dist/web/src/features/studio/runtime/index.ts +0 -1
- package/dist/web/src/features/studio/runtime/mount.ts +0 -664
- package/dist/web/src/features/three/engine/index.ts +0 -1
- package/dist/web/src/main.tsx +0 -7
- package/dist/web/src/pages/office/index.ts +0 -1
- package/dist/web/src/pages/office/page.tsx +0 -283
- package/dist/web/src/pages/pixel/index.ts +0 -1
- package/dist/web/src/pages/pixel/page.tsx +0 -564
- package/dist/web/src/pages/studio/index.ts +0 -1
- package/dist/web/src/pages/studio/page.tsx +0 -446
- package/dist/web/src/runtime/agents.ts +0 -738
- package/dist/web/src/runtime/camera.ts +0 -132
- package/dist/web/src/runtime/dataviz.ts +0 -312
- package/dist/web/src/runtime/effects.ts +0 -482
- package/dist/web/src/runtime/engine.ts +0 -528
- package/dist/web/src/runtime/office.ts +0 -932
- package/dist/web/src/runtime/state.ts +0 -37
- package/dist/web/src/runtime/ui.ts +0 -388
- package/dist/web/src/shared/assets.ts +0 -4
- package/dist/web/src/shared/navigation.ts +0 -47
- package/dist/web/src/styles/app-layout.css +0 -19
- package/dist/web/src/styles/office.css +0 -268
- package/dist/web/tsconfig.json +0 -28
- 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
|
-
}
|