let-them-talk 3.6.1 → 3.7.0

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.
@@ -2,6 +2,7 @@ import * as THREE from 'three';
2
2
  import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
3
  import { S } from './state.js';
4
4
  import { FLOOR_W, FLOOR_D, DESK_POSITIONS, RECEPTION_POS, ENVS, DRESSING_ROOM_POS, REST_AREA_POS } from './constants.js';
5
+ import { buildCampusEnvironment, getCampusDeskPositions } from './campus-env.js';
5
6
 
6
7
  export function buildEnvironment() {
7
8
  if (S.furnitureGroup) {
@@ -27,6 +28,15 @@ export function buildEnvironment() {
27
28
  }
28
29
  S.furnitureGroup = new THREE.Group();
29
30
  S.deskMeshes = [];
31
+
32
+ if (S.currentEnv === 'campus') {
33
+ buildCampusEnvironment();
34
+ // Store campus desk positions for agent assignment
35
+ S._campusDeskPositions = getCampusDeskPositions();
36
+ S.scene.add(S.furnitureGroup);
37
+ return;
38
+ }
39
+
30
40
  var env = ENVS[S.currentEnv] || ENVS.modern;
31
41
 
32
42
  buildFloor(env);
package/office/index.js CHANGED
@@ -5,7 +5,7 @@ import { DESK_POSITIONS, DRESSING_ROOM_POS, DRESSING_ROOM_ENTRANCE, REST_AREA_PO
5
5
  import { initScene } from './scene.js';
6
6
  import { buildEnvironment, updateTVScreen } from './environment.js';
7
7
  import { updateAgent } from './animation.js';
8
- import { syncAgents, processMessages, walkTo, showBubble } from './agents.js';
8
+ import { syncAgents, processMessages, walkTo, navigateTo, showBubble } from './agents.js';
9
9
  // Side-effect: registers window.officeGetAppearance
10
10
  import './appearance.js';
11
11
 
@@ -141,8 +141,8 @@ function executeCommand(agentName, action) {
141
141
  agent.location = 'walking';
142
142
  agent.isSitting = false;
143
143
  showBubble(agent, 'Going to change!');
144
- walkTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
145
- walkTo(agent, DRESSING_ROOM_POS.x, DRESSING_ROOM_POS.z, function() {
144
+ navigateTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
145
+ navigateTo(agent, DRESSING_ROOM_POS.x, DRESSING_ROOM_POS.z, function() {
146
146
  agent.location = 'dressing_room';
147
147
  agent.isSitting = false;
148
148
  showBubble(agent, 'Time for a new look!');
@@ -161,8 +161,8 @@ function executeCommand(agentName, action) {
161
161
  agent.location = 'walking';
162
162
  agent.isSitting = false;
163
163
  showBubble(agent, 'Need a break...');
164
- walkTo(agent, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
165
- walkTo(agent, REST_AREA_POS.x, REST_AREA_POS.z, function() {
164
+ navigateTo(agent, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
165
+ navigateTo(agent, REST_AREA_POS.x, REST_AREA_POS.z, function() {
166
166
  agent.location = 'rest';
167
167
  agent.state = 'sleeping';
168
168
  agent.isSitting = false;
@@ -176,7 +176,7 @@ function executeCommand(agentName, action) {
176
176
  agent.state = 'active';
177
177
  agent.isSitting = false;
178
178
  showBubble(agent, 'Back to work!');
179
- walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
179
+ navigateTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
180
180
  agent.location = 'desk';
181
181
  agent.registered = true;
182
182
  });
@@ -201,8 +201,8 @@ function waitForEditorClose(agent) {
201
201
  if (agent.location === 'dressing_room') {
202
202
  agent.location = 'walking';
203
203
  showBubble(agent, 'Looking good!');
204
- walkTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
205
- walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
204
+ navigateTo(agent, DRESSING_ROOM_ENTRANCE.x, DRESSING_ROOM_ENTRANCE.z, function() {
205
+ navigateTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7, function() {
206
206
  agent.location = 'desk';
207
207
  agent.registered = true;
208
208
  });
@@ -226,6 +226,29 @@ function animate() {
226
226
  updateAgent(S.agents3d[name], dt, time);
227
227
  }
228
228
 
229
+ // Hide roof when camera is above ceiling height
230
+ if (S._roofGroup) {
231
+ S._roofGroup.visible = S.camera.position.y < 6.5;
232
+ }
233
+
234
+ // Manager office door animation — opens when agent is near the door
235
+ if (S._managerDoor && S._managerOfficePos) {
236
+ var doorX = S._managerOfficePos.x;
237
+ var doorZ = S._managerOfficePos.z - 3.5; // front of office
238
+ var shouldOpen = false;
239
+ for (var an in S.agents3d) {
240
+ var ag = S.agents3d[an];
241
+ if (ag.target || ag.location === 'walking') {
242
+ var adx = ag.pos.x - doorX;
243
+ var adz = ag.pos.z - doorZ;
244
+ if (Math.sqrt(adx * adx + adz * adz) < 3) { shouldOpen = true; break; }
245
+ }
246
+ }
247
+ S._managerDoorOpen = shouldOpen ? 1 : 0;
248
+ S._managerDoorLerp += (S._managerDoorOpen - S._managerDoorLerp) * Math.min(1, dt * 4);
249
+ S._managerDoor.position.x = S._managerDoorLerp * 1.3; // slide open to the right
250
+ }
251
+
229
252
  // Update TV screen every ~0.5s for smooth ticker
230
253
  if (!S._tvTimer) S._tvTimer = 0;
231
254
  S._tvTimer += dt;
@@ -309,17 +332,24 @@ window.office3dSetEnvironment = function(env) {
309
332
  if (env === S.currentEnv) return;
310
333
  S.currentEnv = env;
311
334
  if (S.scene) {
312
- buildEnvironment();
313
- var i = 0;
335
+ // Remove all existing agents so they get recreated with proper desk assignments
314
336
  for (var name in S.agents3d) {
315
337
  var agent = S.agents3d[name];
316
- if (i < DESK_POSITIONS.length) {
317
- agent.deskIdx = i;
318
- agent.deskPos = { x: DESK_POSITIONS[i].x, z: DESK_POSITIONS[i].z };
319
- walkTo(agent, agent.deskPos.x, agent.deskPos.z + 0.7);
320
- }
321
- i++;
338
+ S.scene.remove(agent.parts.group);
339
+ agent.parts.group.traverse(function(child) {
340
+ if (child.geometry) child.geometry.dispose();
341
+ if (child.material) {
342
+ if (child.material.map) child.material.map.dispose();
343
+ child.material.dispose();
344
+ }
345
+ });
322
346
  }
347
+ S.agents3d = {};
348
+ S.lastProcessedMsg = 0;
349
+ buildEnvironment();
350
+ // syncAgents will recreate all agents with correct desk assignments
351
+ syncAgents();
352
+ processMessages();
323
353
  }
324
354
  };
325
355
 
@@ -37,7 +37,16 @@ export function updateMonitorScreen(deskIdx, agentName, time) {
37
37
  var agentInfo = (window.cachedAgents || {})[agentName] || {};
38
38
  var lines = [];
39
39
 
40
- var statusColor = agentInfo.status === 'active' ? '#28c840' : '#ffbd2e';
40
+ // Prominent warning when agent is NOT listening
41
+ if (agentInfo.status === 'active' && !agentInfo.is_listening) {
42
+ ctx.fillStyle = '#1a0808';
43
+ ctx.fillRect(0, 14, W, 14);
44
+ ctx.fillStyle = '#ef4444';
45
+ ctx.font = 'bold 10px monospace';
46
+ ctx.fillText('\u26A0 NOT LISTENING', 6, 25);
47
+ }
48
+
49
+ var statusColor = agentInfo.is_listening ? '#28c840' : agentInfo.status === 'active' ? '#ef4444' : '#ffbd2e';
41
50
  lines.push({ color: '#546178', text: '$ agent status' });
42
51
  lines.push({ color: statusColor, text: ' ' + (agentInfo.status || 'unknown').toUpperCase() + (agentInfo.is_listening ? ' (listening)' : ' (working)') });
43
52
  lines.push({ color: '#546178', text: '' });
@@ -0,0 +1,263 @@
1
+ import { S } from './state.js';
2
+
3
+ // ============================================================
4
+ // NAVIGATION SYSTEM — Waypoint graph pathfinding
5
+ // Agents walk along connected waypoints to avoid walls/objects
6
+ // ============================================================
7
+
8
+ // Manager office geometry reference:
9
+ // Office group at (12, 5), size 8x7, walls at:
10
+ // Front (door): z = 5 - 3.5 = 1.5 (door at center x=12)
11
+ // Back: z = 5 + 3.5 = 8.5
12
+ // Left: x = 12 - 4 = 8
13
+ // Right: x = 12 + 4 = 16
14
+ // Glass partition at z = -7 (between workspace and rec)
15
+ // Glass partition at x = -8 (between designer and main)
16
+
17
+ var CAMPUS_WAYPOINTS = [
18
+ // === LOBBY / ENTRANCE ===
19
+ { id: 'spawn', x: 0, z: 14 },
20
+ { id: 'lobby', x: 0, z: 10 },
21
+ { id: 'lobby_left', x: -6, z: 10 },
22
+ { id: 'lobby_right', x: 6, z: 10 },
23
+
24
+ // === MAIN CORRIDOR (runs along z=6, above workspace) ===
25
+ { id: 'corr_L', x: -8, z: 7 },
26
+ { id: 'corr_CL', x: -3, z: 7 },
27
+ { id: 'corr_C', x: 0, z: 7 },
28
+ { id: 'corr_CR', x: 3, z: 7 },
29
+ { id: 'corr_R', x: 7, z: 7 },
30
+
31
+ // === WORKSPACE ZONE (center area, between glass partitions) ===
32
+ { id: 'work_N', x: 0, z: 4 }, // north end
33
+ { id: 'work_NW', x: -5, z: 4 },
34
+ { id: 'work_NE', x: 5, z: 4 },
35
+ { id: 'work_W', x: -5, z: 0 },
36
+ { id: 'work_C', x: 0, z: 0 },
37
+ { id: 'work_E', x: 5, z: 0 },
38
+ { id: 'work_SW', x: -5, z: -3 },
39
+ { id: 'work_S', x: 0, z: -5 },
40
+ { id: 'work_SE', x: 5, z: -3 },
41
+
42
+ // === DESIGNER WING (left of glass partition x=-8) ===
43
+ { id: 'design_gate', x: -8, z: 3 }, // gap in partition
44
+ { id: 'design_N', x: -12, z: 3 },
45
+ { id: 'design_C', x: -12.5, z: 0 },
46
+ { id: 'design_S', x: -12, z: -3 },
47
+
48
+ // === MANAGER OFFICE (right side, enclosed glass room) ===
49
+ // Office walls: left x=8, right x=16, front z=1.5, back z=8.5
50
+ // Door at front wall center (x=12, z=1.5)
51
+ // Path must go AROUND the left-front corner, then to door from outside
52
+ { id: 'mgr_hallway', x: 7, z: 3 }, // south of corridor, OUTSIDE left wall (x<8)
53
+ { id: 'mgr_corner', x: 7, z: 0 }, // past the front-left corner (x<8, z<1.5)
54
+ { id: 'mgr_outside', x: 12, z: 0 }, // in front of door, OUTSIDE front wall (z<1.5)
55
+ { id: 'mgr_doorstep', x: 12, z: 1.5 }, // at the door threshold (triggers door open)
56
+ { id: 'mgr_entry', x: 12, z: 3 }, // just inside the door
57
+ { id: 'mgr_center', x: 12, z: 5 }, // middle of office
58
+ { id: 'mgr_desk', x: 12, z: 7 }, // at the desk/chair
59
+
60
+ // === BACK ZONE CORRIDOR (runs along z=-7 to z=-8, south of glass partition) ===
61
+ { id: 'back_gate', x: 0, z: -6.5 }, // gap in glass partition
62
+ { id: 'back_L', x: -8, z: -8 },
63
+ { id: 'back_C', x: 0, z: -8 },
64
+ { id: 'back_R', x: 8, z: -8 },
65
+
66
+ // === BAR (back left) ===
67
+ { id: 'bar_entry', x: -10, z: -10 },
68
+ { id: 'bar_center', x: -14, z: -12 },
69
+
70
+ // === REC CENTER (back center) ===
71
+ { id: 'rec_entry', x: 0, z: -10 },
72
+ { id: 'rec_center', x: 0, z: -12 },
73
+
74
+ // === GYM (back right) ===
75
+ { id: 'gym_entry', x: 10, z: -10 },
76
+ { id: 'gym_center', x: 14, z: -12 },
77
+
78
+ // === MEZZANINE / STAIRS ===
79
+ { id: 'stairs_bot', x: 20, z: -5 },
80
+ { id: 'stairs_top', x: 20, z: -8 },
81
+ { id: 'mezz_C', x: 0, z: -13 },
82
+
83
+ // === REST / DRESSING (right wing, for old office compat) ===
84
+ { id: 'rest_entry', x: 7.5, z: -5.5 },
85
+ { id: 'dress_entry', x: 7.5, z: -1.5 },
86
+ ];
87
+
88
+ var CAMPUS_CONNECTIONS = [
89
+ // Lobby connections
90
+ ['spawn', 'lobby'],
91
+ ['lobby', 'lobby_left'],
92
+ ['lobby', 'lobby_right'],
93
+ ['lobby', 'corr_C'],
94
+ ['lobby_left', 'corr_L'],
95
+ ['lobby_right', 'corr_R'],
96
+
97
+ // Main corridor (horizontal)
98
+ ['corr_L', 'corr_CL'],
99
+ ['corr_CL', 'corr_C'],
100
+ ['corr_C', 'corr_CR'],
101
+ ['corr_CR', 'corr_R'],
102
+
103
+ // Corridor → workspace
104
+ ['corr_C', 'work_N'],
105
+ ['corr_CL', 'work_NW'],
106
+ ['corr_CR', 'work_NE'],
107
+
108
+ // Workspace grid
109
+ ['work_N', 'work_NW'],
110
+ ['work_N', 'work_NE'],
111
+ ['work_N', 'work_C'],
112
+ ['work_NW', 'work_W'],
113
+ ['work_NE', 'work_E'],
114
+ ['work_W', 'work_C'],
115
+ ['work_C', 'work_E'],
116
+ ['work_W', 'work_SW'],
117
+ ['work_C', 'work_S'],
118
+ ['work_E', 'work_SE'],
119
+ ['work_SW', 'work_S'],
120
+ ['work_S', 'work_SE'],
121
+
122
+ // Designer wing (through gap in glass partition)
123
+ ['work_NW', 'design_gate'],
124
+ ['corr_L', 'design_gate'],
125
+ ['design_gate', 'design_N'],
126
+ ['design_N', 'design_C'],
127
+ ['design_C', 'design_S'],
128
+
129
+ // Manager office — path goes AROUND the corner then through door
130
+ // corr_R(7,7) → mgr_hallway(7,3) → mgr_corner(7,0) → mgr_outside(12,0) → door → inside
131
+ ['corr_R', 'mgr_hallway'], // walk south, outside left wall (x=7 < wall x=8)
132
+ ['mgr_hallway', 'mgr_corner'], // walk further south past front-left corner (z=0 < wall z=1.5)
133
+ ['mgr_corner', 'mgr_outside'], // walk east to front of door (z=0, safely below front wall z=1.5)
134
+ ['mgr_outside', 'mgr_doorstep'], // step to door threshold (triggers open)
135
+ ['mgr_doorstep', 'mgr_entry'], // walk through open door into office
136
+ ['mgr_entry', 'mgr_center'], // walk deeper inside
137
+ ['mgr_center', 'mgr_desk'], // walk to desk
138
+
139
+ // Workspace → back zone (through gap in glass partition at z=-7)
140
+ ['work_S', 'back_gate'],
141
+ ['back_gate', 'back_C'],
142
+
143
+ // Back corridor
144
+ ['back_L', 'back_C'],
145
+ ['back_C', 'back_R'],
146
+ ['work_SW', 'back_L'],
147
+ ['work_SE', 'back_R'],
148
+
149
+ // Back zones
150
+ ['back_L', 'bar_entry'],
151
+ ['bar_entry', 'bar_center'],
152
+ ['back_C', 'rec_entry'],
153
+ ['rec_entry', 'rec_center'],
154
+ ['back_R', 'gym_entry'],
155
+ ['gym_entry', 'gym_center'],
156
+
157
+ // Stairs / mezzanine
158
+ ['back_R', 'stairs_bot'],
159
+ ['stairs_bot', 'stairs_top'],
160
+ ['stairs_top', 'mezz_C'],
161
+
162
+ // Rest/dressing (legacy)
163
+ ['work_SE', 'rest_entry'],
164
+ ['work_E', 'dress_entry'],
165
+ ];
166
+
167
+ // === BUILD GRAPH ===
168
+ var adjacency = {};
169
+ var waypointMap = {};
170
+
171
+ function buildGraph() {
172
+ adjacency = {};
173
+ waypointMap = {};
174
+ CAMPUS_WAYPOINTS.forEach(function(wp) {
175
+ adjacency[wp.id] = [];
176
+ waypointMap[wp.id] = wp;
177
+ });
178
+ CAMPUS_CONNECTIONS.forEach(function(conn) {
179
+ if (adjacency[conn[0]] && adjacency[conn[1]]) {
180
+ adjacency[conn[0]].push(conn[1]);
181
+ adjacency[conn[1]].push(conn[0]);
182
+ }
183
+ });
184
+ }
185
+ buildGraph();
186
+
187
+ // Find nearest waypoint to a world position
188
+ function nearestWaypoint(x, z) {
189
+ var best = null, bestDist = Infinity;
190
+ CAMPUS_WAYPOINTS.forEach(function(wp) {
191
+ var dx = wp.x - x, dz = wp.z - z;
192
+ var d = dx * dx + dz * dz;
193
+ if (d < bestDist) { bestDist = d; best = wp.id; }
194
+ });
195
+ return best;
196
+ }
197
+
198
+ // BFS shortest path
199
+ function findPath(startId, endId) {
200
+ if (startId === endId) return [startId];
201
+ var visited = {};
202
+ var queue = [[startId]];
203
+ visited[startId] = true;
204
+ while (queue.length > 0) {
205
+ var path = queue.shift();
206
+ var current = path[path.length - 1];
207
+ var neighbors = adjacency[current] || [];
208
+ for (var i = 0; i < neighbors.length; i++) {
209
+ var next = neighbors[i];
210
+ if (visited[next]) continue;
211
+ var newPath = path.concat([next]);
212
+ if (next === endId) return newPath;
213
+ visited[next] = true;
214
+ queue.push(newPath);
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+
220
+ // ==================== PUBLIC API ====================
221
+
222
+ export function getNavigationPath(fromX, fromZ, toX, toZ) {
223
+ if (S.currentEnv !== 'campus') {
224
+ return [{ x: toX, z: toZ }];
225
+ }
226
+
227
+ var startWP = nearestWaypoint(fromX, fromZ);
228
+ var endWP = nearestWaypoint(toX, toZ);
229
+
230
+ if (!startWP || !endWP || startWP === endWP) {
231
+ return [{ x: toX, z: toZ }];
232
+ }
233
+
234
+ var wpPath = findPath(startWP, endWP);
235
+ if (!wpPath || wpPath.length === 0) {
236
+ return [{ x: toX, z: toZ }];
237
+ }
238
+
239
+ var result = [];
240
+ // Skip first waypoint if very close to current position
241
+ var firstWP = waypointMap[wpPath[0]];
242
+ var dx0 = firstWP.x - fromX, dz0 = firstWP.z - fromZ;
243
+ var startIdx = (dx0 * dx0 + dz0 * dz0 < 4) ? 1 : 0;
244
+
245
+ for (var i = startIdx; i < wpPath.length; i++) {
246
+ var wp = waypointMap[wpPath[i]];
247
+ var point = { x: wp.x, z: wp.z };
248
+ // Flag door waypoint
249
+ if (wpPath[i] === 'mgr_doorstep') {
250
+ point.triggerDoor = 'open';
251
+ }
252
+ result.push(point);
253
+ }
254
+
255
+ // Add final walk to exact destination if far from last waypoint
256
+ var lastWP = waypointMap[wpPath[wpPath.length - 1]];
257
+ var dxEnd = lastWP.x - toX, dzEnd = lastWP.z - toZ;
258
+ if (dxEnd * dxEnd + dzEnd * dzEnd > 1) {
259
+ result.push({ x: toX, z: toZ });
260
+ }
261
+
262
+ return result;
263
+ }
package/office/scene.js CHANGED
@@ -9,10 +9,10 @@ export function initScene() {
9
9
 
10
10
  S.scene = new THREE.Scene();
11
11
  S.scene.background = new THREE.Color(0x0d1117);
12
- S.scene.fog = new THREE.Fog(0x0d1117, 25, 55);
12
+ S.scene.fog = new THREE.Fog(0x0d1117, 30, 80);
13
13
 
14
- S.camera = new THREE.PerspectiveCamera(50, S.container.clientWidth / S.container.clientHeight, 0.1, 200);
15
- S.camera.position.set(0, 12, 16);
14
+ S.camera = new THREE.PerspectiveCamera(50, S.container.clientWidth / S.container.clientHeight, 0.1, 300);
15
+ S.camera.position.set(0, 15, 22);
16
16
  S.camera.lookAt(0, 0, 0);
17
17
 
18
18
  S.renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -131,7 +131,7 @@ export function SpectatorCamera(camera, domElement) {
131
131
  if (keys['KeyA'] || keys['ArrowLeft']) moveDir.x -= 1;
132
132
  if (keys['KeyD'] || keys['ArrowRight']) moveDir.x += 1;
133
133
  if (keys['KeyE'] || keys['Space']) moveDir.y += 1;
134
- if (keys['KeyQ'] || keys['ControlLeft']) moveDir.y -= 1;
134
+ if (keys['KeyQ']) moveDir.y -= 1;
135
135
 
136
136
  if (moveDir.lengthSq() > 0) {
137
137
  moveDir.normalize();
package/office/state.js CHANGED
@@ -12,7 +12,7 @@ export const S = {
12
12
  clock: null,
13
13
  agents3d: {},
14
14
  lastProcessedMsg: 0,
15
- currentEnv: 'modern',
15
+ currentEnv: 'campus',
16
16
  furnitureGroup: null,
17
17
  deskMeshes: [],
18
18
  syncInterval: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
4
4
  "description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
5
5
  "main": "server.js",
6
6
  "bin": {