let-them-talk 4.2.0 → 5.2.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +640 -540
  2. package/README.md +592 -415
  3. package/cli.js +1089 -589
  4. package/conversation-templates/autonomous-feature.json +22 -0
  5. package/conversation-templates/code-review.json +21 -11
  6. package/conversation-templates/debug-squad.json +21 -11
  7. package/conversation-templates/feature-build.json +21 -11
  8. package/conversation-templates/research-write.json +21 -11
  9. package/dashboard.html +9250 -7771
  10. package/dashboard.js +1232 -29
  11. package/office/agents.js +148 -4
  12. package/office/animation.js +68 -0
  13. package/office/assets.js +431 -0
  14. package/office/builder.js +355 -0
  15. package/office/building-interior.js +261 -0
  16. package/office/campus-env.js +119 -23
  17. package/office/car-hud.js +368 -0
  18. package/office/daynight.js +221 -0
  19. package/office/economy-hud.js +432 -0
  20. package/office/economy-ui.js +238 -0
  21. package/office/environment.js +818 -808
  22. package/office/face.js +65 -0
  23. package/office/fast-travel.js +215 -0
  24. package/office/hq-building.js +295 -0
  25. package/office/index.js +1095 -423
  26. package/office/instancing.js +160 -0
  27. package/office/lod-manager.js +165 -0
  28. package/office/multiplayer-hud.js +428 -0
  29. package/office/net-client.js +299 -0
  30. package/office/particles.js +172 -0
  31. package/office/player.js +658 -436
  32. package/office/post-processing.js +82 -0
  33. package/office/sky.js +319 -0
  34. package/office/street-furniture.js +308 -0
  35. package/office/vehicle.js +455 -0
  36. package/office/world-save.js +91 -0
  37. package/package.json +59 -59
  38. package/server.js +7190 -4685
  39. package/conversation-templates/managed-team.json +0 -12
@@ -1,808 +1,818 @@
1
- import * as THREE from 'three';
2
- import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
- import { S } from './state.js';
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';
6
-
7
- export function buildEnvironment() {
8
- if (S.furnitureGroup) {
9
- var css2dElements = [];
10
- S.furnitureGroup.traverse(function(child) {
11
- if (child.isCSS2DObject) css2dElements.push(child);
12
- });
13
- S.scene.remove(S.furnitureGroup);
14
- if (S.cssRenderer) S.cssRenderer.render(S.scene, S.camera);
15
- css2dElements.forEach(function(obj) {
16
- if (obj.element && obj.element.parentElement) obj.element.remove();
17
- });
18
- S.furnitureGroup.traverse(function(child) {
19
- if (child.geometry) child.geometry.dispose();
20
- if (child.material) {
21
- if (Array.isArray(child.material)) child.material.forEach(function(m) { m.dispose(); });
22
- else {
23
- if (child.material.map) child.material.map.dispose();
24
- child.material.dispose();
25
- }
26
- }
27
- });
28
- }
29
- S.furnitureGroup = new THREE.Group();
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
-
40
- var env = ENVS[S.currentEnv] || ENVS.modern;
41
-
42
- buildFloor(env);
43
- buildWalls(env);
44
- buildReception(env);
45
- DESK_POSITIONS.forEach(function(pos, i) { buildDesk(pos.x, pos.z, i, env); });
46
- buildDecorations(env);
47
- buildDressingRoom(env);
48
- buildRestArea(env);
49
- buildWingDivider(env);
50
-
51
- S.scene.add(S.furnitureGroup);
52
- }
53
-
54
- function buildFloor(env) {
55
- var size = 512;
56
- var canvas = document.createElement('canvas');
57
- canvas.width = size; canvas.height = size;
58
- var ctx = canvas.getContext('2d');
59
- var tiles = 16;
60
- var ts = size / tiles;
61
- var c1 = '#' + env.floor1.toString(16).padStart(6, '0');
62
- var c2 = '#' + env.floor2.toString(16).padStart(6, '0');
63
- for (var i = 0; i < tiles; i++) {
64
- for (var j = 0; j < tiles; j++) {
65
- ctx.fillStyle = (i + j) % 2 === 0 ? c1 : c2;
66
- ctx.fillRect(i * ts, j * ts, ts, ts);
67
- }
68
- }
69
- var tex = new THREE.CanvasTexture(canvas);
70
- tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
71
- var geo = new THREE.PlaneGeometry(FLOOR_W, FLOOR_D);
72
- var mat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.8 });
73
- var floor = new THREE.Mesh(geo, mat);
74
- floor.rotation.x = -Math.PI / 2;
75
- floor.receiveShadow = true;
76
- S.furnitureGroup.add(floor);
77
- }
78
-
79
- function buildWalls(env) {
80
- var wallMat = new THREE.MeshStandardMaterial({ color: env.wall, roughness: 0.9, side: THREE.DoubleSide });
81
-
82
- var backWall = new THREE.Mesh(new THREE.PlaneGeometry(FLOOR_W, 5), wallMat);
83
- backWall.position.set(0, 2.5, -FLOOR_D / 2);
84
- backWall.receiveShadow = true;
85
- S.furnitureGroup.add(backWall);
86
-
87
- var leftWall = new THREE.Mesh(new THREE.PlaneGeometry(FLOOR_D, 5), wallMat);
88
- leftWall.position.set(-FLOOR_W / 2, 2.5, 0);
89
- leftWall.rotation.y = Math.PI / 2;
90
- leftWall.receiveShadow = true;
91
- S.furnitureGroup.add(leftWall);
92
-
93
- var rightWall = new THREE.Mesh(new THREE.PlaneGeometry(FLOOR_D, 5), wallMat);
94
- rightWall.position.set(FLOOR_W / 2, 2.5, 0);
95
- rightWall.rotation.y = -Math.PI / 2;
96
- rightWall.receiveShadow = true;
97
- S.furnitureGroup.add(rightWall);
98
-
99
- var windowMat = new THREE.MeshStandardMaterial({
100
- color: 0x87CEEB, emissive: 0x87CEEB, emissiveIntensity: 0.3, roughness: 0.1
101
- });
102
- [-2, -1, 1, 2].forEach(function(i) {
103
- var win = new THREE.Mesh(new THREE.PlaneGeometry(1.8, 2), windowMat);
104
- win.position.set(i * 3.5, 3, -FLOOR_D / 2 + 0.01);
105
- S.furnitureGroup.add(win);
106
- });
107
- [-1, 0, 1].forEach(function(i) {
108
- var win = new THREE.Mesh(new THREE.PlaneGeometry(2, 1.8), windowMat);
109
- win.position.set(-FLOOR_W / 2 + 0.01, 3, i * 3.5);
110
- win.rotation.y = Math.PI / 2;
111
- S.furnitureGroup.add(win);
112
- });
113
- }
114
-
115
- function buildReception(env) {
116
- var rx = RECEPTION_POS.x, rz = RECEPTION_POS.z;
117
- var deskGeo = new THREE.BoxGeometry(3, 1, 1.2);
118
- var deskMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
119
- var desk = new THREE.Mesh(deskGeo, deskMat);
120
- desk.position.set(rx, 0.5, rz);
121
- desk.castShadow = true; desk.receiveShadow = true;
122
- S.furnitureGroup.add(desk);
123
-
124
- var topGeo = new THREE.BoxGeometry(3.2, 0.08, 1.4);
125
- var topMat = new THREE.MeshStandardMaterial({ color: 0x7a5a3e, roughness: 0.4 });
126
- var top = new THREE.Mesh(topGeo, topMat);
127
- top.position.set(rx, 1.04, rz); top.castShadow = true;
128
- S.furnitureGroup.add(top);
129
-
130
- var bellGeo = new THREE.SphereGeometry(0.08, 16, 12);
131
- var bellMat = new THREE.MeshStandardMaterial({ color: 0xd4af37, metalness: 0.8, roughness: 0.2 });
132
- var bell = new THREE.Mesh(bellGeo, bellMat);
133
- bell.position.set(rx + 0.8, 1.12, rz);
134
- S.furnitureGroup.add(bell);
135
-
136
- var signDiv = document.createElement('div');
137
- signDiv.textContent = 'RECEPTION';
138
- signDiv.style.cssText = 'color:#d4af37;font-size:11px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:1px;';
139
- var signLabel = new CSS2DObject(signDiv);
140
- signLabel.position.set(rx, 1.5, rz);
141
- S.furnitureGroup.add(signLabel);
142
- }
143
-
144
- function buildDesk(x, z, index, env) {
145
- var group = new THREE.Group();
146
- group.position.set(x, 0, z);
147
-
148
- var topGeo = new THREE.BoxGeometry(1.8, 0.08, 0.9);
149
- var topMat = new THREE.MeshStandardMaterial({ color: env.desk, roughness: 0.5 });
150
- var top = new THREE.Mesh(topGeo, topMat);
151
- top.position.y = 0.75; top.castShadow = true; top.receiveShadow = true;
152
- group.add(top);
153
-
154
- var legGeo = new THREE.BoxGeometry(0.06, 0.75, 0.06);
155
- var legMat = new THREE.MeshStandardMaterial({ color: env.deskLegs, roughness: 0.7 });
156
- [[-0.8, -0.35], [-0.8, 0.35], [0.8, -0.35], [0.8, 0.35]].forEach(function(pos) {
157
- var leg = new THREE.Mesh(legGeo, legMat);
158
- leg.position.set(pos[0], 0.375, pos[1]); leg.castShadow = true;
159
- group.add(leg);
160
- });
161
-
162
- var monGeo = new THREE.BoxGeometry(0.5, 0.35, 0.04);
163
- var monMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.3 });
164
- var monitor = new THREE.Mesh(monGeo, monMat);
165
- monitor.position.set(0, 1.1, -0.2); monitor.castShadow = true;
166
- group.add(monitor);
167
-
168
- var screenGeo = new THREE.PlaneGeometry(0.44, 0.28);
169
- var screenMat = new THREE.MeshStandardMaterial({
170
- color: 0x333333, emissive: 0x333333, emissiveIntensity: 0.1, roughness: 0.2
171
- });
172
- var screen = new THREE.Mesh(screenGeo, screenMat);
173
- screen.position.set(0, 1.1, -0.179);
174
- group.add(screen);
175
-
176
- var standGeo = new THREE.BoxGeometry(0.06, 0.2, 0.06);
177
- var stand = new THREE.Mesh(standGeo, legMat);
178
- stand.position.set(0, 0.88, -0.2);
179
- group.add(stand);
180
-
181
- // Chair
182
- var chairGroup = new THREE.Group();
183
- chairGroup.position.set(0, 0, 0.7);
184
- var seatGeo = new THREE.CylinderGeometry(0.25, 0.25, 0.06, 16);
185
- var seatMat = new THREE.MeshStandardMaterial({ color: env.chairSeat, roughness: 0.7 });
186
- var seat = new THREE.Mesh(seatGeo, seatMat);
187
- seat.position.y = 0.45; seat.castShadow = true;
188
- chairGroup.add(seat);
189
- var postGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.45, 8);
190
- var post = new THREE.Mesh(postGeo, legMat);
191
- post.position.y = 0.225;
192
- chairGroup.add(post);
193
- var backGeo = new THREE.BoxGeometry(0.45, 0.35, 0.04);
194
- var backMat = new THREE.MeshStandardMaterial({ color: env.chair, roughness: 0.6 });
195
- var back = new THREE.Mesh(backGeo, backMat);
196
- back.position.set(0, 0.7, 0.2); back.castShadow = true;
197
- chairGroup.add(back);
198
- group.add(chairGroup);
199
-
200
- S.furnitureGroup.add(group);
201
- S.deskMeshes.push({ group: group, screen: screen, screenMat: screenMat, index: index, x: x, z: z });
202
- }
203
-
204
- function buildDecorations(env) {
205
- var isStartup = S.currentEnv === 'startup';
206
- buildPlant(-9, -6.5);
207
- buildPlant(9, -6.5);
208
- buildPlant(-9, -2);
209
- buildPlant(9, -2);
210
- buildWhiteboard(-9.5, 1);
211
- buildBookshelf(-9.5, -4.5);
212
- buildFloorLamp(-8.5, 4.5);
213
- buildFloorLamp(6, 5);
214
- buildAreaRug(0, -1);
215
- if (isStartup) {
216
- buildPizzaBox(9, 1);
217
- buildBeanbag(9, -6);
218
- buildBeanbag(-9, 3);
219
- buildArcadeMachine(-8.5, -6);
220
- buildTV(0, -7.5);
221
- } else {
222
- buildCoffeeMachine(9, 1);
223
- buildWatercooler(9, -4);
224
- buildTV(0, -7.5);
225
- buildBookshelf(-9.5, 3);
226
- }
227
- }
228
-
229
- function buildPlant(x, z) {
230
- var group = new THREE.Group();
231
- group.position.set(x, 0, z);
232
- var potGeo = new THREE.CylinderGeometry(0.2, 0.15, 0.4, 12);
233
- var potMat = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.8 });
234
- var pot = new THREE.Mesh(potGeo, potMat);
235
- pot.position.y = 0.2; pot.castShadow = true;
236
- group.add(pot);
237
- var leafColors = [0x2d8a4e, 0x34a853, 0x28a745];
238
- for (var i = 0; i < 5; i++) {
239
- var angle = (i / 5) * Math.PI * 2;
240
- var leafGeo = new THREE.SphereGeometry(0.15, 8, 6);
241
- var leafMat = new THREE.MeshStandardMaterial({ color: leafColors[i % 3], roughness: 0.8 });
242
- var leaf = new THREE.Mesh(leafGeo, leafMat);
243
- leaf.position.set(Math.cos(angle) * 0.15, 0.55, Math.sin(angle) * 0.15);
244
- leaf.scale.set(1, 0.7, 1); leaf.castShadow = true;
245
- group.add(leaf);
246
- }
247
- var topLeaf = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 6), new THREE.MeshStandardMaterial({ color: 0x34a853, roughness: 0.8 }));
248
- topLeaf.position.y = 0.7; topLeaf.castShadow = true;
249
- group.add(topLeaf);
250
- S.furnitureGroup.add(group);
251
- }
252
-
253
- function buildWhiteboard(x, z) {
254
- var group = new THREE.Group();
255
- group.position.set(x, 0, z);
256
- var boardGeo = new THREE.BoxGeometry(0.08, 1.5, 2);
257
- var boardMat = new THREE.MeshStandardMaterial({ color: 0xe8e8e8, roughness: 0.3 });
258
- var board = new THREE.Mesh(boardGeo, boardMat);
259
- board.position.y = 1.8; board.castShadow = true;
260
- group.add(board);
261
- var frameMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.5 });
262
- [2.55, 1.05].forEach(function(y) {
263
- var frame = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.05, 2.1), frameMat);
264
- frame.position.y = y; group.add(frame);
265
- });
266
- var legGeo = new THREE.CylinderGeometry(0.03, 0.03, 1, 8);
267
- var legMat2 = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.5 });
268
- [-0.8, 0.8].forEach(function(lz) {
269
- var leg = new THREE.Mesh(legGeo, legMat2);
270
- leg.position.set(0, 0.5, lz); group.add(leg);
271
- });
272
- S.furnitureGroup.add(group);
273
- }
274
-
275
- function buildCoffeeMachine(x, z) {
276
- var group = new THREE.Group();
277
- group.position.set(x, 0, z);
278
- var bodyGeo = new THREE.BoxGeometry(0.5, 0.8, 0.4);
279
- var bodyMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5 });
280
- var body = new THREE.Mesh(bodyGeo, bodyMat);
281
- body.position.y = 0.8; body.castShadow = true;
282
- group.add(body);
283
- var tableGeo = new THREE.BoxGeometry(0.7, 0.04, 0.5);
284
- var tableMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
285
- var table = new THREE.Mesh(tableGeo, tableMat);
286
- table.position.y = 0.4; table.castShadow = true;
287
- group.add(table);
288
- var cupGeo = new THREE.CylinderGeometry(0.04, 0.03, 0.08, 12);
289
- var cupMat = new THREE.MeshStandardMaterial({ color: 0xf5f5f5, roughness: 0.3 });
290
- var cup = new THREE.Mesh(cupGeo, cupMat);
291
- cup.position.set(0.2, 0.46, 0);
292
- group.add(cup);
293
- S.furnitureGroup.add(group);
294
- }
295
-
296
- function buildPizzaBox(x, z) {
297
- var group = new THREE.Group();
298
- group.position.set(x, 0, z);
299
- var tableGeo = new THREE.BoxGeometry(0.7, 0.04, 0.5);
300
- var tableMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
301
- var table = new THREE.Mesh(tableGeo, tableMat);
302
- table.position.y = 0.4; table.castShadow = true;
303
- group.add(table);
304
- var boxGeo = new THREE.BoxGeometry(0.5, 0.06, 0.5);
305
- var boxMat = new THREE.MeshStandardMaterial({ color: 0xd4a24e, roughness: 0.8 });
306
- var box = new THREE.Mesh(boxGeo, boxMat);
307
- box.position.y = 0.46; box.castShadow = true;
308
- group.add(box);
309
- var lidGeo = new THREE.BoxGeometry(0.5, 0.03, 0.5);
310
- var lidMat = new THREE.MeshStandardMaterial({ color: 0xe8c06a, roughness: 0.8 });
311
- var lid = new THREE.Mesh(lidGeo, lidMat);
312
- lid.position.set(0, 0.5, -0.22); lid.rotation.x = -0.6;
313
- group.add(lid);
314
- S.furnitureGroup.add(group);
315
- }
316
-
317
- function buildBeanbag(x, z) {
318
- var group = new THREE.Group();
319
- group.position.set(x, 0, z);
320
- var botGeo = new THREE.SphereGeometry(0.4, 16, 12);
321
- var botMat = new THREE.MeshStandardMaterial({ color: 0xe53e3e, roughness: 0.9 });
322
- var bottom = new THREE.Mesh(botGeo, botMat);
323
- bottom.position.y = 0.2; bottom.scale.set(1, 0.5, 1); bottom.castShadow = true;
324
- group.add(bottom);
325
- var topGeo = new THREE.SphereGeometry(0.35, 16, 12);
326
- var topMat = new THREE.MeshStandardMaterial({ color: 0xc53030, roughness: 0.9 });
327
- var topPart = new THREE.Mesh(topGeo, topMat);
328
- topPart.position.set(-0.05, 0.4, 0); topPart.scale.set(1, 0.6, 1); topPart.castShadow = true;
329
- group.add(topPart);
330
- S.furnitureGroup.add(group);
331
- }
332
-
333
- function buildWatercooler(x, z) {
334
- var group = new THREE.Group();
335
- group.position.set(x, 0, z);
336
- var baseGeo = new THREE.BoxGeometry(0.3, 0.6, 0.3);
337
- var baseMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.5 });
338
- var base = new THREE.Mesh(baseGeo, baseMat);
339
- base.position.y = 0.3; base.castShadow = true;
340
- group.add(base);
341
- var bottleGeo = new THREE.CylinderGeometry(0.12, 0.12, 0.4, 16);
342
- var bottleMat = new THREE.MeshStandardMaterial({ color: 0x64b4ff, transparent: true, opacity: 0.5, roughness: 0.1 });
343
- var bottle = new THREE.Mesh(bottleGeo, bottleMat);
344
- bottle.position.y = 0.8;
345
- group.add(bottle);
346
- S.furnitureGroup.add(group);
347
- }
348
-
349
- // ===================== BOOKSHELF =====================
350
- function buildBookshelf(x, z) {
351
- var group = new THREE.Group();
352
- group.position.set(x, 0, z);
353
- var frameMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.7 });
354
- // Main frame
355
- var back = new THREE.Mesh(new THREE.BoxGeometry(0.08, 2.2, 1.2), frameMat);
356
- back.position.y = 1.1; back.castShadow = true;
357
- group.add(back);
358
- // Shelves (4 levels)
359
- [0.05, 0.55, 1.1, 1.65, 2.15].forEach(function(sy) {
360
- var shelf = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.04, 1.2), frameMat);
361
- shelf.position.set(0.13, sy, 0); shelf.castShadow = true; shelf.receiveShadow = true;
362
- group.add(shelf);
363
- });
364
- // Side panels
365
- [-0.58, 0.58].forEach(function(sz) {
366
- var side = new THREE.Mesh(new THREE.BoxGeometry(0.35, 2.2, 0.04), frameMat);
367
- side.position.set(0.13, 1.1, sz); side.castShadow = true;
368
- group.add(side);
369
- });
370
- // Books (colored blocks on shelves)
371
- var bookColors = [0xc0392b, 0x2980b9, 0x27ae60, 0x8e44ad, 0xe67e22, 0x2c3e50, 0xd4a24e, 0x1abc9c];
372
- var shelfYs = [0.09, 0.59, 1.14, 1.69];
373
- shelfYs.forEach(function(sy, si) {
374
- var numBooks = 4 + Math.floor(Math.random() * 4);
375
- var startZ = -0.45;
376
- for (var bi = 0; bi < numBooks; bi++) {
377
- var bh = 0.3 + Math.random() * 0.15;
378
- var bw = 0.04 + Math.random() * 0.03;
379
- var bookMat = new THREE.MeshStandardMaterial({ color: bookColors[(si * 5 + bi) % bookColors.length], roughness: 0.8 });
380
- var book = new THREE.Mesh(new THREE.BoxGeometry(0.2, bh, bw), bookMat);
381
- book.position.set(0.18, sy + bh / 2, startZ);
382
- book.castShadow = true;
383
- group.add(book);
384
- startZ += bw + 0.02;
385
- }
386
- });
387
- S.furnitureGroup.add(group);
388
- }
389
-
390
- // ===================== TV / MONITOR =====================
391
- function buildTV(x, z) {
392
- var group = new THREE.Group();
393
- group.position.set(x, 0, z);
394
- // Wall mount bracket
395
- var bracketMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5, metalness: 0.3 });
396
- var bracket = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.3, 0.06), bracketMat);
397
- bracket.position.y = 2.2;
398
- group.add(bracket);
399
- // TV body — wide on X, thin on Z (mounted on back wall, facing +Z into room)
400
- var tvMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.3 });
401
- var tvBody = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1, 0.08), tvMat);
402
- tvBody.position.y = 2.2; tvBody.castShadow = true;
403
- group.add(tvBody);
404
- // Animated screen canvas
405
- var W = 320, H = 200;
406
- var cvs = document.createElement('canvas');
407
- cvs.width = W; cvs.height = H;
408
- var tex = new THREE.CanvasTexture(cvs);
409
- tex.minFilter = THREE.LinearFilter;
410
- var screenMat = new THREE.MeshStandardMaterial({
411
- map: tex, emissive: 0x58a6ff, emissiveIntensity: 0.25, roughness: 0.1
412
- });
413
- var screen = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 0.9), screenMat);
414
- screen.position.set(0, 2.2, 0.045);
415
- group.add(screen);
416
- // Store for animation updates
417
- S._tvScreen = { canvas: cvs, texture: tex, tickerOffset: 0 };
418
- S.furnitureGroup.add(group);
419
- }
420
-
421
- // Called every ~1s from the sync interval to update the TV screen
422
- export function updateTVScreen(time) {
423
- var tv = S._tvScreen;
424
- if (!tv) return;
425
- var cvs = tv.canvas, ctx = cvs.getContext('2d');
426
- var W = cvs.width, H = cvs.height;
427
-
428
- // Clear
429
- ctx.fillStyle = '#0a0e1a';
430
- ctx.fillRect(0, 0, W, H);
431
-
432
- // Top bar
433
- ctx.fillStyle = '#111830';
434
- ctx.fillRect(0, 0, W, 40);
435
- ctx.fillStyle = '#58a6ff';
436
- ctx.font = 'bold 20px monospace';
437
- ctx.fillText('OFFICE DASHBOARD', 16, 27);
438
- var now = new Date();
439
- var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
440
- ctx.fillStyle = '#7ee787';
441
- ctx.textAlign = 'right';
442
- ctx.fillText(timeStr, W - 16, 27);
443
- ctx.textAlign = 'left';
444
-
445
- // Data
446
- var agents = window.cachedAgents || {};
447
- var history = window.cachedHistory || [];
448
- var agentNames = Object.keys(agents);
449
- var activeCount = 0, sleepCount = 0;
450
- agentNames.forEach(function(n) {
451
- if (agents[n].status === 'active') activeCount++;
452
- else if (agents[n].status === 'sleeping') sleepCount++;
453
- });
454
-
455
- var y = 68;
456
- ctx.font = '16px monospace';
457
-
458
- // Stats row 1
459
- ctx.fillStyle = '#546178'; ctx.fillText('AGENTS', 16, y);
460
- ctx.fillStyle = '#d2a8ff'; ctx.fillText(String(agentNames.length), 110, y);
461
- ctx.fillStyle = '#4ade80'; ctx.fillText(activeCount + ' active', 140, y);
462
- ctx.fillStyle = '#facc15'; ctx.fillText(sleepCount + ' idle', 280, y);
463
- y += 26;
464
-
465
- // Stats row 2
466
- ctx.fillStyle = '#546178'; ctx.fillText('MESSAGES', 16, y);
467
- ctx.fillStyle = '#79c0ff'; ctx.fillText(String(history.length), 130, y);
468
- y += 26;
469
-
470
- // Separator
471
- ctx.strokeStyle = '#1a2744'; ctx.lineWidth = 1;
472
- ctx.beginPath(); ctx.moveTo(16, y); ctx.lineTo(W - 16, y); ctx.stroke();
473
- y += 20;
474
-
475
- // Activity header
476
- ctx.fillStyle = '#546178'; ctx.font = '14px monospace';
477
- ctx.fillText('RECENT ACTIVITY', 16, y);
478
- y += 22;
479
-
480
- // Messages
481
- ctx.font = '13px monospace';
482
- var recentMsgs = history.slice(-7);
483
- for (var i = 0; i < recentMsgs.length; i++) {
484
- if (y > H - 50) break;
485
- var msg = recentMsgs[i];
486
- var from = msg.from || '?';
487
- var to2 = msg.to || 'all';
488
- var content = msg.content || msg.message || '';
489
- var maxLen = Math.floor((W - 40) / 7.5);
490
- var snippet = content.length > maxLen ? content.substring(0, maxLen - 2) + '..' : content;
491
-
492
- // From > To
493
- ctx.fillStyle = '#7ee787'; ctx.fillText(from, 16, y);
494
- var fromW = ctx.measureText(from).width;
495
- ctx.fillStyle = '#546178'; ctx.fillText(' > ', 16 + fromW, y);
496
- ctx.fillStyle = '#d2a8ff'; ctx.fillText(to2, 16 + fromW + ctx.measureText(' > ').width, y);
497
- y += 18;
498
- // Snippet
499
- ctx.fillStyle = '#8892b0'; ctx.fillText(' ' + snippet, 16, y);
500
- y += 22;
501
- }
502
- if (recentMsgs.length === 0) {
503
- ctx.fillStyle = '#3d4663'; ctx.fillText(' Waiting for messages...', 16, y);
504
- }
505
-
506
- // Bottom ticker
507
- ctx.fillStyle = '#111830';
508
- ctx.fillRect(0, H - 32, W, 32);
509
- var tickerParts = [];
510
- agentNames.forEach(function(n) {
511
- var info = agents[n];
512
- var st = info.status === 'active' ? '\u25CF' : '\u25CB';
513
- tickerParts.push(st + ' ' + (info.display_name || n));
514
- });
515
- var tickerText = tickerParts.length > 0 ? tickerParts.join(' \u2022 ') + ' \u2022 ' : 'No agents online';
516
- ctx.font = '15px monospace';
517
- var charW = 9;
518
- tv.tickerOffset = (tv.tickerOffset + 1.5) % (tickerText.length * charW);
519
- ctx.fillStyle = '#58a6ff';
520
- var fullTW = tickerText.length * charW;
521
- ctx.fillText(tickerText, -tv.tickerOffset, H - 10);
522
- ctx.fillText(tickerText, -tv.tickerOffset + fullTW, H - 10);
523
-
524
- // Scanline overlay
525
- ctx.fillStyle = 'rgba(0,0,0,0.04)';
526
- for (var sl = 0; sl < H; sl += 2) {
527
- ctx.fillRect(0, sl, W, 1);
528
- }
529
-
530
- tv.texture.needsUpdate = true;
531
- }
532
-
533
- // ===================== ARCADE MACHINE =====================
534
- function buildArcadeMachine(x, z) {
535
- var group = new THREE.Group();
536
- group.position.set(x, 0, z);
537
- var cabinetMat = new THREE.MeshStandardMaterial({ color: 0x2c1654, roughness: 0.7 });
538
- // Main cabinet body
539
- var cabinet = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.8, 0.7), cabinetMat);
540
- cabinet.position.y = 0.9; cabinet.castShadow = true;
541
- group.add(cabinet);
542
- // Top section (angled screen housing)
543
- var topMat = new THREE.MeshStandardMaterial({ color: 0x3a1f6e, roughness: 0.6 });
544
- var top = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.5, 0.6), topMat);
545
- top.position.set(0, 2.05, -0.05); top.castShadow = true;
546
- group.add(top);
547
- // Screen
548
- var scrMat = new THREE.MeshStandardMaterial({ color: 0x000000, emissive: 0x00ff88, emissiveIntensity: 0.5, roughness: 0.1 });
549
- var scr = new THREE.Mesh(new THREE.PlaneGeometry(0.35, 0.35), scrMat);
550
- scr.position.set(0.31, 2.0, -0.05);
551
- scr.rotation.y = Math.PI / 2;
552
- group.add(scr);
553
- // Control panel
554
- var panelMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.5 });
555
- var panel = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.1, 0.4), panelMat);
556
- panel.position.set(0, 1.5, 0.2); panel.rotation.x = -0.3;
557
- group.add(panel);
558
- // Joystick
559
- var joyMat = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.4 });
560
- var joyBase = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.02, 8), panelMat);
561
- joyBase.position.set(0.1, 1.56, 0.15);
562
- group.add(joyBase);
563
- var joyStick = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.08, 6), joyMat);
564
- joyStick.position.set(0.1, 1.6, 0.15);
565
- group.add(joyStick);
566
- var joyBall = new THREE.Mesh(new THREE.SphereGeometry(0.02, 8, 6), joyMat);
567
- joyBall.position.set(0.1, 1.65, 0.15);
568
- group.add(joyBall);
569
- // Buttons
570
- var btnColors = [0xff4444, 0x44ff44, 0x4444ff];
571
- btnColors.forEach(function(col, bi) {
572
- var btn = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.018, 0.015, 8), new THREE.MeshStandardMaterial({ color: col, roughness: 0.3 }));
573
- btn.position.set(-0.05 - bi * 0.06, 1.56, 0.15);
574
- group.add(btn);
575
- });
576
- // Marquee sign
577
- var marqueeDiv = document.createElement('div');
578
- marqueeDiv.textContent = 'ARCADE';
579
- marqueeDiv.style.cssText = 'color:#ff00ff;font-size:8px;font-weight:bold;font-family:monospace;letter-spacing:2px;text-shadow:0 0 4px #ff00ff;';
580
- var marquee = new CSS2DObject(marqueeDiv);
581
- marquee.position.set(0, 2.45, 0);
582
- group.add(marquee);
583
- S.furnitureGroup.add(group);
584
- }
585
-
586
- // ===================== FLOOR LAMP =====================
587
- function buildFloorLamp(x, z) {
588
- var group = new THREE.Group();
589
- group.position.set(x, 0, z);
590
- // Base
591
- var baseMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5, metalness: 0.3 });
592
- var base = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.18, 0.04, 12), baseMat);
593
- base.position.y = 0.02; base.castShadow = true;
594
- group.add(base);
595
- // Pole
596
- var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 1.6, 8), baseMat);
597
- pole.position.y = 0.84;
598
- group.add(pole);
599
- // Shade (cone)
600
- var shadeMat = new THREE.MeshStandardMaterial({ color: 0xddd5c0, roughness: 0.8, side: THREE.DoubleSide });
601
- var shade = new THREE.Mesh(new THREE.ConeGeometry(0.18, 0.25, 12, 1, true), shadeMat);
602
- shade.position.y = 1.72; shade.castShadow = true;
603
- group.add(shade);
604
- // Light inside
605
- var light = new THREE.PointLight(0xffeedd, 0.3, 4);
606
- light.position.set(0, 1.6, 0);
607
- group.add(light);
608
- S.furnitureGroup.add(group);
609
- }
610
-
611
- // ===================== AREA RUG =====================
612
- function buildAreaRug(x, z) {
613
- var group = new THREE.Group();
614
- group.position.set(x, 0.005, z);
615
- var isStartup = S.currentEnv === 'startup';
616
- var rugColor = isStartup ? 0x3d2b1f : 0x232a35;
617
- var borderColor = isStartup ? 0x4a3525 : 0x2a3340;
618
- // Main rug
619
- var rugMat = new THREE.MeshStandardMaterial({ color: rugColor, roughness: 0.95 });
620
- var rug = new THREE.Mesh(new THREE.PlaneGeometry(6, 4), rugMat);
621
- rug.rotation.x = -Math.PI / 2; rug.receiveShadow = true;
622
- group.add(rug);
623
- // Subtle border stripe (slightly lighter than rug, not bright)
624
- var borderMat = new THREE.MeshStandardMaterial({ color: borderColor, roughness: 0.9 });
625
- // Top/bottom borders
626
- [-1.9, 1.9].forEach(function(bz) {
627
- var stripe = new THREE.Mesh(new THREE.PlaneGeometry(5.8, 0.08), borderMat);
628
- stripe.rotation.x = -Math.PI / 2;
629
- stripe.position.set(0, 0.001, bz);
630
- group.add(stripe);
631
- });
632
- // Left/right borders
633
- [-2.9, 2.9].forEach(function(bx) {
634
- var stripe = new THREE.Mesh(new THREE.PlaneGeometry(0.08, 3.8), borderMat);
635
- stripe.rotation.x = -Math.PI / 2;
636
- stripe.position.set(bx, 0.001, 0);
637
- group.add(stripe);
638
- });
639
- S.furnitureGroup.add(group);
640
- }
641
-
642
- // ===================== WING DIVIDER =====================
643
- function buildWingDivider(env) {
644
- var wallMat = new THREE.MeshStandardMaterial({ color: (ENVS[S.currentEnv] || ENVS.modern).wall, roughness: 0.9, side: THREE.DoubleSide });
645
- // Partial wall separating main office from right wing (gap in middle for walking through)
646
- // Upper section (z: -8 to -3.5)
647
- var upper = new THREE.Mesh(new THREE.PlaneGeometry(4.5, 5), wallMat);
648
- upper.position.set(7, 2.5, -5.75);
649
- upper.rotation.y = Math.PI / 2;
650
- upper.receiveShadow = true;
651
- S.furnitureGroup.add(upper);
652
- // Lower section (z: 0.5 to 4)
653
- var lower = new THREE.Mesh(new THREE.PlaneGeometry(3.5, 5), wallMat);
654
- lower.position.set(7, 2.5, 2.25);
655
- lower.rotation.y = Math.PI / 2;
656
- lower.receiveShadow = true;
657
- S.furnitureGroup.add(lower);
658
- // Archway header (connecting the two sections above the gap)
659
- var header = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.5, 4), wallMat);
660
- header.position.set(7, 4.25, -1.5);
661
- S.furnitureGroup.add(header);
662
- // "LOUNGE" sign above archway
663
- var signDiv = document.createElement('div');
664
- signDiv.textContent = 'LOUNGE';
665
- signDiv.style.cssText = 'color:#6c8aff;font-size:10px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:2px;opacity:0.7;';
666
- var signLabel = new CSS2DObject(signDiv);
667
- signLabel.position.set(7, 4.2, -1.5);
668
- S.furnitureGroup.add(signLabel);
669
- }
670
-
671
- // ===================== DRESSING ROOM =====================
672
- function buildDressingRoom(env) {
673
- var rx = DRESSING_ROOM_POS.x, rz = DRESSING_ROOM_POS.z;
674
- var group = new THREE.Group();
675
- group.position.set(rx, 0, rz);
676
-
677
- // Floor platform (raised circular disc)
678
- var platformGeo = new THREE.CylinderGeometry(0.6, 0.65, 0.06, 24);
679
- var platformMat = new THREE.MeshStandardMaterial({ color: 0x4a4a5a, roughness: 0.4, metalness: 0.2 });
680
- var platform = new THREE.Mesh(platformGeo, platformMat);
681
- platform.position.y = 0.03; platform.receiveShadow = true; platform.castShadow = true;
682
- group.add(platform);
683
- // Platform rim (subtle glow ring)
684
- var rimGeo = new THREE.TorusGeometry(0.62, 0.02, 8, 32);
685
- var rimMat = new THREE.MeshStandardMaterial({ color: 0x6c8aff, emissive: 0x6c8aff, emissiveIntensity: 0.4, roughness: 0.3 });
686
- var rim = new THREE.Mesh(rimGeo, rimMat);
687
- rim.rotation.x = -Math.PI / 2; rim.position.y = 0.07;
688
- group.add(rim);
689
-
690
- // Mirror on the right wall (tall reflective rectangle)
691
- var mirrorGeo = new THREE.PlaneGeometry(1.2, 2);
692
- var mirrorMat = new THREE.MeshStandardMaterial({ color: 0xd0d8e8, emissive: 0x8899bb, emissiveIntensity: 0.15, roughness: 0.05, metalness: 0.8 });
693
- var mirror = new THREE.Mesh(mirrorGeo, mirrorMat);
694
- mirror.position.set(1.5, 1.2, 0); mirror.rotation.y = -Math.PI / 2;
695
- group.add(mirror);
696
- // Mirror frame
697
- var frameMat = new THREE.MeshStandardMaterial({ color: 0x8B6914, roughness: 0.4, metalness: 0.3 });
698
- var frameTop = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, 1.3), frameMat);
699
- frameTop.position.set(1.52, 2.22, 0); group.add(frameTop);
700
- var frameBot = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, 1.3), frameMat);
701
- frameBot.position.set(1.52, 0.18, 0); group.add(frameBot);
702
- var frameL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.1, 0.06), frameMat);
703
- frameL.position.set(1.52, 1.2, -0.65); group.add(frameL);
704
- var frameR = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.1, 0.06), frameMat);
705
- frameR.position.set(1.52, 1.2, 0.65); group.add(frameR);
706
-
707
- // Left partition wall (half-height privacy screen)
708
- var partMat = new THREE.MeshStandardMaterial({ color: 0x3a4050, roughness: 0.8, side: THREE.DoubleSide });
709
- var partL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.2, 2.5), partMat);
710
- partL.position.set(-1.3, 1.1, 0);
711
- partL.castShadow = true;
712
- group.add(partL);
713
- // Back partition
714
- var partB = new THREE.Mesh(new THREE.BoxGeometry(2.6, 2.2, 0.06), partMat);
715
- partB.position.set(0.1, 1.1, -1.3);
716
- partB.castShadow = true;
717
- group.add(partB);
718
-
719
- // Coat hooks on back partition
720
- var hookMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.3, metalness: 0.5 });
721
- [-0.4, 0.2, 0.8].forEach(function(hx) {
722
- var hook = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.12, 8), hookMat);
723
- hook.position.set(hx, 1.6, -1.25); hook.rotation.x = Math.PI / 3;
724
- group.add(hook);
725
- });
726
-
727
- // Warm spotlight
728
- var spotLight = new THREE.PointLight(0xffeedd, 0.6, 5);
729
- spotLight.position.set(0, 3, 0);
730
- group.add(spotLight);
731
-
732
- // Sign
733
- var signDiv = document.createElement('div');
734
- signDiv.textContent = 'DRESSING ROOM';
735
- signDiv.style.cssText = 'color:#d4af37;font-size:9px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:1.5px;';
736
- var signLabel = new CSS2DObject(signDiv);
737
- signLabel.position.set(0, 2.6, 0);
738
- group.add(signLabel);
739
-
740
- S.furnitureGroup.add(group);
741
- }
742
-
743
- // ===================== REST AREA =====================
744
- function buildRestArea(env) {
745
- var rx = REST_AREA_POS.x, rz = REST_AREA_POS.z;
746
- var group = new THREE.Group();
747
- group.position.set(rx, 0, rz);
748
-
749
- // Soft rug (circular, textured)
750
- var rugGeo = new THREE.CircleGeometry(1.8, 24);
751
- var rugMat = new THREE.MeshStandardMaterial({ color: 0x4a3828, roughness: 0.95 });
752
- var rug = new THREE.Mesh(rugGeo, rugMat);
753
- rug.rotation.x = -Math.PI / 2; rug.position.y = 0.01; rug.receiveShadow = true;
754
- group.add(rug);
755
-
756
- // Beanbags (3, arranged in a cozy cluster)
757
- var bbColors = [0xe53e3e, 0x3b82f6, 0x22c55e];
758
- var bbPositions = [{ x: -0.6, z: 0.3 }, { x: 0.6, z: 0.4 }, { x: 0, z: -0.5 }];
759
- bbPositions.forEach(function(pos, i) {
760
- var bbGroup = new THREE.Group();
761
- bbGroup.position.set(pos.x, 0, pos.z);
762
- var botGeo = new THREE.SphereGeometry(0.4, 16, 12);
763
- var botMat = new THREE.MeshStandardMaterial({ color: bbColors[i], roughness: 0.9 });
764
- var bottom = new THREE.Mesh(botGeo, botMat);
765
- bottom.position.y = 0.2; bottom.scale.set(1, 0.5, 1); bottom.castShadow = true;
766
- bbGroup.add(bottom);
767
- var topGeo = new THREE.SphereGeometry(0.35, 16, 12);
768
- var topMat = new THREE.MeshStandardMaterial({ color: bbColors[i], roughness: 0.9 });
769
- topMat.color.multiplyScalar(0.8);
770
- var topPart = new THREE.Mesh(topGeo, topMat);
771
- topPart.position.set(-0.05, 0.4, 0); topPart.scale.set(1, 0.6, 1); topPart.castShadow = true;
772
- bbGroup.add(topPart);
773
- bbGroup.rotation.y = (i / 3) * Math.PI * 2;
774
- group.add(bbGroup);
775
- });
776
-
777
- // Small side table
778
- var tableGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.04, 16);
779
- var tableMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
780
- var table = new THREE.Mesh(tableGeo, tableMat);
781
- table.position.set(1.2, 0.35, 0); table.castShadow = true;
782
- group.add(table);
783
- var tableLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.35, 8), tableMat);
784
- tableLeg.position.set(1.2, 0.175, 0);
785
- group.add(tableLeg);
786
-
787
- // Coffee mug on table
788
- var mugGeo = new THREE.CylinderGeometry(0.04, 0.03, 0.07, 12);
789
- var mugMat = new THREE.MeshStandardMaterial({ color: 0xf5f5f5, roughness: 0.3 });
790
- var mug = new THREE.Mesh(mugGeo, mugMat);
791
- mug.position.set(1.2, 0.405, 0);
792
- group.add(mug);
793
-
794
- // Warm dim point light (cozy orange glow)
795
- var warmLight = new THREE.PointLight(0xffaa55, 0.4, 6);
796
- warmLight.position.set(0, 2.5, 0);
797
- group.add(warmLight);
798
-
799
- // Sign
800
- var signDiv = document.createElement('div');
801
- signDiv.textContent = 'REST AREA';
802
- signDiv.style.cssText = 'color:#facc15;font-size:9px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:1.5px;';
803
- var signLabel = new CSS2DObject(signDiv);
804
- signLabel.position.set(0, 2.2, 0);
805
- group.add(signLabel);
806
-
807
- S.furnitureGroup.add(group);
808
- }
1
+ import * as THREE from 'three';
2
+ import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
+ import { S } from './state.js';
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';
6
+ // city-env loaded dynamically to avoid killing campus FPS
7
+
8
+ export function buildEnvironment() {
9
+ if (S.furnitureGroup) {
10
+ var css2dElements = [];
11
+ S.furnitureGroup.traverse(function(child) {
12
+ if (child.isCSS2DObject) css2dElements.push(child);
13
+ });
14
+ S.scene.remove(S.furnitureGroup);
15
+ if (S.cssRenderer) S.cssRenderer.render(S.scene, S.camera);
16
+ css2dElements.forEach(function(obj) {
17
+ if (obj.element && obj.element.parentElement) obj.element.remove();
18
+ });
19
+ S.furnitureGroup.traverse(function(child) {
20
+ if (child.geometry) child.geometry.dispose();
21
+ if (child.material) {
22
+ if (Array.isArray(child.material)) child.material.forEach(function(m) { m.dispose(); });
23
+ else {
24
+ if (child.material.map) child.material.map.dispose();
25
+ child.material.dispose();
26
+ }
27
+ }
28
+ });
29
+ }
30
+ S.furnitureGroup = new THREE.Group();
31
+ S.deskMeshes = [];
32
+
33
+ if (S.currentEnv === 'campus') {
34
+ buildCampusEnvironment();
35
+ // Store campus desk positions for agent assignment
36
+ S._campusDeskPositions = getCampusDeskPositions();
37
+ S.scene.add(S.furnitureGroup);
38
+ return;
39
+ }
40
+
41
+ if (S.currentEnv === 'city') {
42
+ import('./city-env.js').then(function(mod) {
43
+ mod.buildCityEnvironment();
44
+ S._cityDeskPositions = mod.getCityDeskPositions();
45
+ S.scene.add(S.furnitureGroup);
46
+ });
47
+ return;
48
+ }
49
+
50
+ var env = ENVS[S.currentEnv] || ENVS.modern;
51
+
52
+ buildFloor(env);
53
+ buildWalls(env);
54
+ buildReception(env);
55
+ DESK_POSITIONS.forEach(function(pos, i) { buildDesk(pos.x, pos.z, i, env); });
56
+ buildDecorations(env);
57
+ buildDressingRoom(env);
58
+ buildRestArea(env);
59
+ buildWingDivider(env);
60
+
61
+ S.scene.add(S.furnitureGroup);
62
+ }
63
+
64
+ function buildFloor(env) {
65
+ var size = 512;
66
+ var canvas = document.createElement('canvas');
67
+ canvas.width = size; canvas.height = size;
68
+ var ctx = canvas.getContext('2d');
69
+ var tiles = 16;
70
+ var ts = size / tiles;
71
+ var c1 = '#' + env.floor1.toString(16).padStart(6, '0');
72
+ var c2 = '#' + env.floor2.toString(16).padStart(6, '0');
73
+ for (var i = 0; i < tiles; i++) {
74
+ for (var j = 0; j < tiles; j++) {
75
+ ctx.fillStyle = (i + j) % 2 === 0 ? c1 : c2;
76
+ ctx.fillRect(i * ts, j * ts, ts, ts);
77
+ }
78
+ }
79
+ var tex = new THREE.CanvasTexture(canvas);
80
+ tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
81
+ var geo = new THREE.PlaneGeometry(FLOOR_W, FLOOR_D);
82
+ var mat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.8 });
83
+ var floor = new THREE.Mesh(geo, mat);
84
+ floor.rotation.x = -Math.PI / 2;
85
+ floor.receiveShadow = true;
86
+ S.furnitureGroup.add(floor);
87
+ }
88
+
89
+ function buildWalls(env) {
90
+ var wallMat = new THREE.MeshStandardMaterial({ color: env.wall, roughness: 0.9, side: THREE.DoubleSide });
91
+
92
+ var backWall = new THREE.Mesh(new THREE.PlaneGeometry(FLOOR_W, 5), wallMat);
93
+ backWall.position.set(0, 2.5, -FLOOR_D / 2);
94
+ backWall.receiveShadow = true;
95
+ S.furnitureGroup.add(backWall);
96
+
97
+ var leftWall = new THREE.Mesh(new THREE.PlaneGeometry(FLOOR_D, 5), wallMat);
98
+ leftWall.position.set(-FLOOR_W / 2, 2.5, 0);
99
+ leftWall.rotation.y = Math.PI / 2;
100
+ leftWall.receiveShadow = true;
101
+ S.furnitureGroup.add(leftWall);
102
+
103
+ var rightWall = new THREE.Mesh(new THREE.PlaneGeometry(FLOOR_D, 5), wallMat);
104
+ rightWall.position.set(FLOOR_W / 2, 2.5, 0);
105
+ rightWall.rotation.y = -Math.PI / 2;
106
+ rightWall.receiveShadow = true;
107
+ S.furnitureGroup.add(rightWall);
108
+
109
+ var windowMat = new THREE.MeshStandardMaterial({
110
+ color: 0x87CEEB, emissive: 0x87CEEB, emissiveIntensity: 0.3, roughness: 0.1
111
+ });
112
+ [-2, -1, 1, 2].forEach(function(i) {
113
+ var win = new THREE.Mesh(new THREE.PlaneGeometry(1.8, 2), windowMat);
114
+ win.position.set(i * 3.5, 3, -FLOOR_D / 2 + 0.01);
115
+ S.furnitureGroup.add(win);
116
+ });
117
+ [-1, 0, 1].forEach(function(i) {
118
+ var win = new THREE.Mesh(new THREE.PlaneGeometry(2, 1.8), windowMat);
119
+ win.position.set(-FLOOR_W / 2 + 0.01, 3, i * 3.5);
120
+ win.rotation.y = Math.PI / 2;
121
+ S.furnitureGroup.add(win);
122
+ });
123
+ }
124
+
125
+ function buildReception(env) {
126
+ var rx = RECEPTION_POS.x, rz = RECEPTION_POS.z;
127
+ var deskGeo = new THREE.BoxGeometry(3, 1, 1.2);
128
+ var deskMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
129
+ var desk = new THREE.Mesh(deskGeo, deskMat);
130
+ desk.position.set(rx, 0.5, rz);
131
+ desk.castShadow = true; desk.receiveShadow = true;
132
+ S.furnitureGroup.add(desk);
133
+
134
+ var topGeo = new THREE.BoxGeometry(3.2, 0.08, 1.4);
135
+ var topMat = new THREE.MeshStandardMaterial({ color: 0x7a5a3e, roughness: 0.4 });
136
+ var top = new THREE.Mesh(topGeo, topMat);
137
+ top.position.set(rx, 1.04, rz); top.castShadow = true;
138
+ S.furnitureGroup.add(top);
139
+
140
+ var bellGeo = new THREE.SphereGeometry(0.08, 16, 12);
141
+ var bellMat = new THREE.MeshStandardMaterial({ color: 0xd4af37, metalness: 0.8, roughness: 0.2 });
142
+ var bell = new THREE.Mesh(bellGeo, bellMat);
143
+ bell.position.set(rx + 0.8, 1.12, rz);
144
+ S.furnitureGroup.add(bell);
145
+
146
+ var signDiv = document.createElement('div');
147
+ signDiv.textContent = 'RECEPTION';
148
+ signDiv.style.cssText = 'color:#d4af37;font-size:11px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:1px;';
149
+ var signLabel = new CSS2DObject(signDiv);
150
+ signLabel.position.set(rx, 1.5, rz);
151
+ S.furnitureGroup.add(signLabel);
152
+ }
153
+
154
+ function buildDesk(x, z, index, env) {
155
+ var group = new THREE.Group();
156
+ group.position.set(x, 0, z);
157
+
158
+ var topGeo = new THREE.BoxGeometry(1.8, 0.08, 0.9);
159
+ var topMat = new THREE.MeshStandardMaterial({ color: env.desk, roughness: 0.5 });
160
+ var top = new THREE.Mesh(topGeo, topMat);
161
+ top.position.y = 0.75; top.castShadow = true; top.receiveShadow = true;
162
+ group.add(top);
163
+
164
+ var legGeo = new THREE.BoxGeometry(0.06, 0.75, 0.06);
165
+ var legMat = new THREE.MeshStandardMaterial({ color: env.deskLegs, roughness: 0.7 });
166
+ [[-0.8, -0.35], [-0.8, 0.35], [0.8, -0.35], [0.8, 0.35]].forEach(function(pos) {
167
+ var leg = new THREE.Mesh(legGeo, legMat);
168
+ leg.position.set(pos[0], 0.375, pos[1]); leg.castShadow = true;
169
+ group.add(leg);
170
+ });
171
+
172
+ var monGeo = new THREE.BoxGeometry(0.5, 0.35, 0.04);
173
+ var monMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.3 });
174
+ var monitor = new THREE.Mesh(monGeo, monMat);
175
+ monitor.position.set(0, 1.1, -0.2); monitor.castShadow = true;
176
+ group.add(monitor);
177
+
178
+ var screenGeo = new THREE.PlaneGeometry(0.44, 0.28);
179
+ var screenMat = new THREE.MeshStandardMaterial({
180
+ color: 0x333333, emissive: 0x333333, emissiveIntensity: 0.1, roughness: 0.2
181
+ });
182
+ var screen = new THREE.Mesh(screenGeo, screenMat);
183
+ screen.position.set(0, 1.1, -0.179);
184
+ group.add(screen);
185
+
186
+ var standGeo = new THREE.BoxGeometry(0.06, 0.2, 0.06);
187
+ var stand = new THREE.Mesh(standGeo, legMat);
188
+ stand.position.set(0, 0.88, -0.2);
189
+ group.add(stand);
190
+
191
+ // Chair
192
+ var chairGroup = new THREE.Group();
193
+ chairGroup.position.set(0, 0, 0.7);
194
+ var seatGeo = new THREE.CylinderGeometry(0.25, 0.25, 0.06, 16);
195
+ var seatMat = new THREE.MeshStandardMaterial({ color: env.chairSeat, roughness: 0.7 });
196
+ var seat = new THREE.Mesh(seatGeo, seatMat);
197
+ seat.position.y = 0.45; seat.castShadow = true;
198
+ chairGroup.add(seat);
199
+ var postGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.45, 8);
200
+ var post = new THREE.Mesh(postGeo, legMat);
201
+ post.position.y = 0.225;
202
+ chairGroup.add(post);
203
+ var backGeo = new THREE.BoxGeometry(0.45, 0.35, 0.04);
204
+ var backMat = new THREE.MeshStandardMaterial({ color: env.chair, roughness: 0.6 });
205
+ var back = new THREE.Mesh(backGeo, backMat);
206
+ back.position.set(0, 0.7, 0.2); back.castShadow = true;
207
+ chairGroup.add(back);
208
+ group.add(chairGroup);
209
+
210
+ S.furnitureGroup.add(group);
211
+ S.deskMeshes.push({ group: group, screen: screen, screenMat: screenMat, index: index, x: x, z: z });
212
+ }
213
+
214
+ function buildDecorations(env) {
215
+ var isStartup = S.currentEnv === 'startup';
216
+ buildPlant(-9, -6.5);
217
+ buildPlant(9, -6.5);
218
+ buildPlant(-9, -2);
219
+ buildPlant(9, -2);
220
+ buildWhiteboard(-9.5, 1);
221
+ buildBookshelf(-9.5, -4.5);
222
+ buildFloorLamp(-8.5, 4.5);
223
+ buildFloorLamp(6, 5);
224
+ buildAreaRug(0, -1);
225
+ if (isStartup) {
226
+ buildPizzaBox(9, 1);
227
+ buildBeanbag(9, -6);
228
+ buildBeanbag(-9, 3);
229
+ buildArcadeMachine(-8.5, -6);
230
+ buildTV(0, -7.5);
231
+ } else {
232
+ buildCoffeeMachine(9, 1);
233
+ buildWatercooler(9, -4);
234
+ buildTV(0, -7.5);
235
+ buildBookshelf(-9.5, 3);
236
+ }
237
+ }
238
+
239
+ function buildPlant(x, z) {
240
+ var group = new THREE.Group();
241
+ group.position.set(x, 0, z);
242
+ var potGeo = new THREE.CylinderGeometry(0.2, 0.15, 0.4, 12);
243
+ var potMat = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.8 });
244
+ var pot = new THREE.Mesh(potGeo, potMat);
245
+ pot.position.y = 0.2; pot.castShadow = true;
246
+ group.add(pot);
247
+ var leafColors = [0x2d8a4e, 0x34a853, 0x28a745];
248
+ for (var i = 0; i < 5; i++) {
249
+ var angle = (i / 5) * Math.PI * 2;
250
+ var leafGeo = new THREE.SphereGeometry(0.15, 8, 6);
251
+ var leafMat = new THREE.MeshStandardMaterial({ color: leafColors[i % 3], roughness: 0.8 });
252
+ var leaf = new THREE.Mesh(leafGeo, leafMat);
253
+ leaf.position.set(Math.cos(angle) * 0.15, 0.55, Math.sin(angle) * 0.15);
254
+ leaf.scale.set(1, 0.7, 1); leaf.castShadow = true;
255
+ group.add(leaf);
256
+ }
257
+ var topLeaf = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 6), new THREE.MeshStandardMaterial({ color: 0x34a853, roughness: 0.8 }));
258
+ topLeaf.position.y = 0.7; topLeaf.castShadow = true;
259
+ group.add(topLeaf);
260
+ S.furnitureGroup.add(group);
261
+ }
262
+
263
+ function buildWhiteboard(x, z) {
264
+ var group = new THREE.Group();
265
+ group.position.set(x, 0, z);
266
+ var boardGeo = new THREE.BoxGeometry(0.08, 1.5, 2);
267
+ var boardMat = new THREE.MeshStandardMaterial({ color: 0xe8e8e8, roughness: 0.3 });
268
+ var board = new THREE.Mesh(boardGeo, boardMat);
269
+ board.position.y = 1.8; board.castShadow = true;
270
+ group.add(board);
271
+ var frameMat = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.5 });
272
+ [2.55, 1.05].forEach(function(y) {
273
+ var frame = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.05, 2.1), frameMat);
274
+ frame.position.y = y; group.add(frame);
275
+ });
276
+ var legGeo = new THREE.CylinderGeometry(0.03, 0.03, 1, 8);
277
+ var legMat2 = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.5 });
278
+ [-0.8, 0.8].forEach(function(lz) {
279
+ var leg = new THREE.Mesh(legGeo, legMat2);
280
+ leg.position.set(0, 0.5, lz); group.add(leg);
281
+ });
282
+ S.furnitureGroup.add(group);
283
+ }
284
+
285
+ function buildCoffeeMachine(x, z) {
286
+ var group = new THREE.Group();
287
+ group.position.set(x, 0, z);
288
+ var bodyGeo = new THREE.BoxGeometry(0.5, 0.8, 0.4);
289
+ var bodyMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5 });
290
+ var body = new THREE.Mesh(bodyGeo, bodyMat);
291
+ body.position.y = 0.8; body.castShadow = true;
292
+ group.add(body);
293
+ var tableGeo = new THREE.BoxGeometry(0.7, 0.04, 0.5);
294
+ var tableMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
295
+ var table = new THREE.Mesh(tableGeo, tableMat);
296
+ table.position.y = 0.4; table.castShadow = true;
297
+ group.add(table);
298
+ var cupGeo = new THREE.CylinderGeometry(0.04, 0.03, 0.08, 12);
299
+ var cupMat = new THREE.MeshStandardMaterial({ color: 0xf5f5f5, roughness: 0.3 });
300
+ var cup = new THREE.Mesh(cupGeo, cupMat);
301
+ cup.position.set(0.2, 0.46, 0);
302
+ group.add(cup);
303
+ S.furnitureGroup.add(group);
304
+ }
305
+
306
+ function buildPizzaBox(x, z) {
307
+ var group = new THREE.Group();
308
+ group.position.set(x, 0, z);
309
+ var tableGeo = new THREE.BoxGeometry(0.7, 0.04, 0.5);
310
+ var tableMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
311
+ var table = new THREE.Mesh(tableGeo, tableMat);
312
+ table.position.y = 0.4; table.castShadow = true;
313
+ group.add(table);
314
+ var boxGeo = new THREE.BoxGeometry(0.5, 0.06, 0.5);
315
+ var boxMat = new THREE.MeshStandardMaterial({ color: 0xd4a24e, roughness: 0.8 });
316
+ var box = new THREE.Mesh(boxGeo, boxMat);
317
+ box.position.y = 0.46; box.castShadow = true;
318
+ group.add(box);
319
+ var lidGeo = new THREE.BoxGeometry(0.5, 0.03, 0.5);
320
+ var lidMat = new THREE.MeshStandardMaterial({ color: 0xe8c06a, roughness: 0.8 });
321
+ var lid = new THREE.Mesh(lidGeo, lidMat);
322
+ lid.position.set(0, 0.5, -0.22); lid.rotation.x = -0.6;
323
+ group.add(lid);
324
+ S.furnitureGroup.add(group);
325
+ }
326
+
327
+ function buildBeanbag(x, z) {
328
+ var group = new THREE.Group();
329
+ group.position.set(x, 0, z);
330
+ var botGeo = new THREE.SphereGeometry(0.4, 16, 12);
331
+ var botMat = new THREE.MeshStandardMaterial({ color: 0xe53e3e, roughness: 0.9 });
332
+ var bottom = new THREE.Mesh(botGeo, botMat);
333
+ bottom.position.y = 0.2; bottom.scale.set(1, 0.5, 1); bottom.castShadow = true;
334
+ group.add(bottom);
335
+ var topGeo = new THREE.SphereGeometry(0.35, 16, 12);
336
+ var topMat = new THREE.MeshStandardMaterial({ color: 0xc53030, roughness: 0.9 });
337
+ var topPart = new THREE.Mesh(topGeo, topMat);
338
+ topPart.position.set(-0.05, 0.4, 0); topPart.scale.set(1, 0.6, 1); topPart.castShadow = true;
339
+ group.add(topPart);
340
+ S.furnitureGroup.add(group);
341
+ }
342
+
343
+ function buildWatercooler(x, z) {
344
+ var group = new THREE.Group();
345
+ group.position.set(x, 0, z);
346
+ var baseGeo = new THREE.BoxGeometry(0.3, 0.6, 0.3);
347
+ var baseMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.5 });
348
+ var base = new THREE.Mesh(baseGeo, baseMat);
349
+ base.position.y = 0.3; base.castShadow = true;
350
+ group.add(base);
351
+ var bottleGeo = new THREE.CylinderGeometry(0.12, 0.12, 0.4, 16);
352
+ var bottleMat = new THREE.MeshStandardMaterial({ color: 0x64b4ff, transparent: true, opacity: 0.5, roughness: 0.1 });
353
+ var bottle = new THREE.Mesh(bottleGeo, bottleMat);
354
+ bottle.position.y = 0.8;
355
+ group.add(bottle);
356
+ S.furnitureGroup.add(group);
357
+ }
358
+
359
+ // ===================== BOOKSHELF =====================
360
+ function buildBookshelf(x, z) {
361
+ var group = new THREE.Group();
362
+ group.position.set(x, 0, z);
363
+ var frameMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.7 });
364
+ // Main frame
365
+ var back = new THREE.Mesh(new THREE.BoxGeometry(0.08, 2.2, 1.2), frameMat);
366
+ back.position.y = 1.1; back.castShadow = true;
367
+ group.add(back);
368
+ // Shelves (4 levels)
369
+ [0.05, 0.55, 1.1, 1.65, 2.15].forEach(function(sy) {
370
+ var shelf = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.04, 1.2), frameMat);
371
+ shelf.position.set(0.13, sy, 0); shelf.castShadow = true; shelf.receiveShadow = true;
372
+ group.add(shelf);
373
+ });
374
+ // Side panels
375
+ [-0.58, 0.58].forEach(function(sz) {
376
+ var side = new THREE.Mesh(new THREE.BoxGeometry(0.35, 2.2, 0.04), frameMat);
377
+ side.position.set(0.13, 1.1, sz); side.castShadow = true;
378
+ group.add(side);
379
+ });
380
+ // Books (colored blocks on shelves)
381
+ var bookColors = [0xc0392b, 0x2980b9, 0x27ae60, 0x8e44ad, 0xe67e22, 0x2c3e50, 0xd4a24e, 0x1abc9c];
382
+ var shelfYs = [0.09, 0.59, 1.14, 1.69];
383
+ shelfYs.forEach(function(sy, si) {
384
+ var numBooks = 4 + Math.floor(Math.random() * 4);
385
+ var startZ = -0.45;
386
+ for (var bi = 0; bi < numBooks; bi++) {
387
+ var bh = 0.3 + Math.random() * 0.15;
388
+ var bw = 0.04 + Math.random() * 0.03;
389
+ var bookMat = new THREE.MeshStandardMaterial({ color: bookColors[(si * 5 + bi) % bookColors.length], roughness: 0.8 });
390
+ var book = new THREE.Mesh(new THREE.BoxGeometry(0.2, bh, bw), bookMat);
391
+ book.position.set(0.18, sy + bh / 2, startZ);
392
+ book.castShadow = true;
393
+ group.add(book);
394
+ startZ += bw + 0.02;
395
+ }
396
+ });
397
+ S.furnitureGroup.add(group);
398
+ }
399
+
400
+ // ===================== TV / MONITOR =====================
401
+ function buildTV(x, z) {
402
+ var group = new THREE.Group();
403
+ group.position.set(x, 0, z);
404
+ // Wall mount bracket
405
+ var bracketMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5, metalness: 0.3 });
406
+ var bracket = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.3, 0.06), bracketMat);
407
+ bracket.position.y = 2.2;
408
+ group.add(bracket);
409
+ // TV body — wide on X, thin on Z (mounted on back wall, facing +Z into room)
410
+ var tvMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.3 });
411
+ var tvBody = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1, 0.08), tvMat);
412
+ tvBody.position.y = 2.2; tvBody.castShadow = true;
413
+ group.add(tvBody);
414
+ // Animated screen canvas
415
+ var W = 320, H = 200;
416
+ var cvs = document.createElement('canvas');
417
+ cvs.width = W; cvs.height = H;
418
+ var tex = new THREE.CanvasTexture(cvs);
419
+ tex.minFilter = THREE.LinearFilter;
420
+ var screenMat = new THREE.MeshStandardMaterial({
421
+ map: tex, emissive: 0x58a6ff, emissiveIntensity: 0.25, roughness: 0.1
422
+ });
423
+ var screen = new THREE.Mesh(new THREE.PlaneGeometry(1.4, 0.9), screenMat);
424
+ screen.position.set(0, 2.2, 0.045);
425
+ group.add(screen);
426
+ // Store for animation updates
427
+ S._tvScreen = { canvas: cvs, texture: tex, tickerOffset: 0 };
428
+ S.furnitureGroup.add(group);
429
+ }
430
+
431
+ // Called every ~1s from the sync interval to update the TV screen
432
+ export function updateTVScreen(time) {
433
+ var tv = S._tvScreen;
434
+ if (!tv) return;
435
+ var cvs = tv.canvas, ctx = cvs.getContext('2d');
436
+ var W = cvs.width, H = cvs.height;
437
+
438
+ // Clear
439
+ ctx.fillStyle = '#0a0e1a';
440
+ ctx.fillRect(0, 0, W, H);
441
+
442
+ // Top bar
443
+ ctx.fillStyle = '#111830';
444
+ ctx.fillRect(0, 0, W, 40);
445
+ ctx.fillStyle = '#58a6ff';
446
+ ctx.font = 'bold 20px monospace';
447
+ ctx.fillText('OFFICE DASHBOARD', 16, 27);
448
+ var now = new Date();
449
+ var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0') + ':' + now.getSeconds().toString().padStart(2, '0');
450
+ ctx.fillStyle = '#7ee787';
451
+ ctx.textAlign = 'right';
452
+ ctx.fillText(timeStr, W - 16, 27);
453
+ ctx.textAlign = 'left';
454
+
455
+ // Data
456
+ var agents = window.cachedAgents || {};
457
+ var history = window.cachedHistory || [];
458
+ var agentNames = Object.keys(agents);
459
+ var activeCount = 0, sleepCount = 0;
460
+ agentNames.forEach(function(n) {
461
+ if (agents[n].status === 'active') activeCount++;
462
+ else if (agents[n].status === 'sleeping') sleepCount++;
463
+ });
464
+
465
+ var y = 68;
466
+ ctx.font = '16px monospace';
467
+
468
+ // Stats row 1
469
+ ctx.fillStyle = '#546178'; ctx.fillText('AGENTS', 16, y);
470
+ ctx.fillStyle = '#d2a8ff'; ctx.fillText(String(agentNames.length), 110, y);
471
+ ctx.fillStyle = '#4ade80'; ctx.fillText(activeCount + ' active', 140, y);
472
+ ctx.fillStyle = '#facc15'; ctx.fillText(sleepCount + ' idle', 280, y);
473
+ y += 26;
474
+
475
+ // Stats row 2
476
+ ctx.fillStyle = '#546178'; ctx.fillText('MESSAGES', 16, y);
477
+ ctx.fillStyle = '#79c0ff'; ctx.fillText(String(history.length), 130, y);
478
+ y += 26;
479
+
480
+ // Separator
481
+ ctx.strokeStyle = '#1a2744'; ctx.lineWidth = 1;
482
+ ctx.beginPath(); ctx.moveTo(16, y); ctx.lineTo(W - 16, y); ctx.stroke();
483
+ y += 20;
484
+
485
+ // Activity header
486
+ ctx.fillStyle = '#546178'; ctx.font = '14px monospace';
487
+ ctx.fillText('RECENT ACTIVITY', 16, y);
488
+ y += 22;
489
+
490
+ // Messages
491
+ ctx.font = '13px monospace';
492
+ var recentMsgs = history.slice(-7);
493
+ for (var i = 0; i < recentMsgs.length; i++) {
494
+ if (y > H - 50) break;
495
+ var msg = recentMsgs[i];
496
+ var from = msg.from || '?';
497
+ var to2 = msg.to || 'all';
498
+ var content = msg.content || msg.message || '';
499
+ var maxLen = Math.floor((W - 40) / 7.5);
500
+ var snippet = content.length > maxLen ? content.substring(0, maxLen - 2) + '..' : content;
501
+
502
+ // From > To
503
+ ctx.fillStyle = '#7ee787'; ctx.fillText(from, 16, y);
504
+ var fromW = ctx.measureText(from).width;
505
+ ctx.fillStyle = '#546178'; ctx.fillText(' > ', 16 + fromW, y);
506
+ ctx.fillStyle = '#d2a8ff'; ctx.fillText(to2, 16 + fromW + ctx.measureText(' > ').width, y);
507
+ y += 18;
508
+ // Snippet
509
+ ctx.fillStyle = '#8892b0'; ctx.fillText(' ' + snippet, 16, y);
510
+ y += 22;
511
+ }
512
+ if (recentMsgs.length === 0) {
513
+ ctx.fillStyle = '#3d4663'; ctx.fillText(' Waiting for messages...', 16, y);
514
+ }
515
+
516
+ // Bottom ticker
517
+ ctx.fillStyle = '#111830';
518
+ ctx.fillRect(0, H - 32, W, 32);
519
+ var tickerParts = [];
520
+ agentNames.forEach(function(n) {
521
+ var info = agents[n];
522
+ var st = info.status === 'active' ? '\u25CF' : '\u25CB';
523
+ tickerParts.push(st + ' ' + (info.display_name || n));
524
+ });
525
+ var tickerText = tickerParts.length > 0 ? tickerParts.join(' \u2022 ') + ' \u2022 ' : 'No agents online';
526
+ ctx.font = '15px monospace';
527
+ var charW = 9;
528
+ tv.tickerOffset = (tv.tickerOffset + 1.5) % (tickerText.length * charW);
529
+ ctx.fillStyle = '#58a6ff';
530
+ var fullTW = tickerText.length * charW;
531
+ ctx.fillText(tickerText, -tv.tickerOffset, H - 10);
532
+ ctx.fillText(tickerText, -tv.tickerOffset + fullTW, H - 10);
533
+
534
+ // Scanline overlay
535
+ ctx.fillStyle = 'rgba(0,0,0,0.04)';
536
+ for (var sl = 0; sl < H; sl += 2) {
537
+ ctx.fillRect(0, sl, W, 1);
538
+ }
539
+
540
+ tv.texture.needsUpdate = true;
541
+ }
542
+
543
+ // ===================== ARCADE MACHINE =====================
544
+ function buildArcadeMachine(x, z) {
545
+ var group = new THREE.Group();
546
+ group.position.set(x, 0, z);
547
+ var cabinetMat = new THREE.MeshStandardMaterial({ color: 0x2c1654, roughness: 0.7 });
548
+ // Main cabinet body
549
+ var cabinet = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.8, 0.7), cabinetMat);
550
+ cabinet.position.y = 0.9; cabinet.castShadow = true;
551
+ group.add(cabinet);
552
+ // Top section (angled screen housing)
553
+ var topMat = new THREE.MeshStandardMaterial({ color: 0x3a1f6e, roughness: 0.6 });
554
+ var top = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.5, 0.6), topMat);
555
+ top.position.set(0, 2.05, -0.05); top.castShadow = true;
556
+ group.add(top);
557
+ // Screen
558
+ var scrMat = new THREE.MeshStandardMaterial({ color: 0x000000, emissive: 0x00ff88, emissiveIntensity: 0.5, roughness: 0.1 });
559
+ var scr = new THREE.Mesh(new THREE.PlaneGeometry(0.35, 0.35), scrMat);
560
+ scr.position.set(0.31, 2.0, -0.05);
561
+ scr.rotation.y = Math.PI / 2;
562
+ group.add(scr);
563
+ // Control panel
564
+ var panelMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.5 });
565
+ var panel = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.1, 0.4), panelMat);
566
+ panel.position.set(0, 1.5, 0.2); panel.rotation.x = -0.3;
567
+ group.add(panel);
568
+ // Joystick
569
+ var joyMat = new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.4 });
570
+ var joyBase = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.02, 8), panelMat);
571
+ joyBase.position.set(0.1, 1.56, 0.15);
572
+ group.add(joyBase);
573
+ var joyStick = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.08, 6), joyMat);
574
+ joyStick.position.set(0.1, 1.6, 0.15);
575
+ group.add(joyStick);
576
+ var joyBall = new THREE.Mesh(new THREE.SphereGeometry(0.02, 8, 6), joyMat);
577
+ joyBall.position.set(0.1, 1.65, 0.15);
578
+ group.add(joyBall);
579
+ // Buttons
580
+ var btnColors = [0xff4444, 0x44ff44, 0x4444ff];
581
+ btnColors.forEach(function(col, bi) {
582
+ var btn = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.018, 0.015, 8), new THREE.MeshStandardMaterial({ color: col, roughness: 0.3 }));
583
+ btn.position.set(-0.05 - bi * 0.06, 1.56, 0.15);
584
+ group.add(btn);
585
+ });
586
+ // Marquee sign
587
+ var marqueeDiv = document.createElement('div');
588
+ marqueeDiv.textContent = 'ARCADE';
589
+ marqueeDiv.style.cssText = 'color:#ff00ff;font-size:8px;font-weight:bold;font-family:monospace;letter-spacing:2px;text-shadow:0 0 4px #ff00ff;';
590
+ var marquee = new CSS2DObject(marqueeDiv);
591
+ marquee.position.set(0, 2.45, 0);
592
+ group.add(marquee);
593
+ S.furnitureGroup.add(group);
594
+ }
595
+
596
+ // ===================== FLOOR LAMP =====================
597
+ function buildFloorLamp(x, z) {
598
+ var group = new THREE.Group();
599
+ group.position.set(x, 0, z);
600
+ // Base
601
+ var baseMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5, metalness: 0.3 });
602
+ var base = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.18, 0.04, 12), baseMat);
603
+ base.position.y = 0.02; base.castShadow = true;
604
+ group.add(base);
605
+ // Pole
606
+ var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 1.6, 8), baseMat);
607
+ pole.position.y = 0.84;
608
+ group.add(pole);
609
+ // Shade (cone)
610
+ var shadeMat = new THREE.MeshStandardMaterial({ color: 0xddd5c0, roughness: 0.8, side: THREE.DoubleSide });
611
+ var shade = new THREE.Mesh(new THREE.ConeGeometry(0.18, 0.25, 12, 1, true), shadeMat);
612
+ shade.position.y = 1.72; shade.castShadow = true;
613
+ group.add(shade);
614
+ // Light inside
615
+ var light = new THREE.PointLight(0xffeedd, 0.3, 4);
616
+ light.position.set(0, 1.6, 0);
617
+ group.add(light);
618
+ S.furnitureGroup.add(group);
619
+ }
620
+
621
+ // ===================== AREA RUG =====================
622
+ function buildAreaRug(x, z) {
623
+ var group = new THREE.Group();
624
+ group.position.set(x, 0.005, z);
625
+ var isStartup = S.currentEnv === 'startup';
626
+ var rugColor = isStartup ? 0x3d2b1f : 0x232a35;
627
+ var borderColor = isStartup ? 0x4a3525 : 0x2a3340;
628
+ // Main rug
629
+ var rugMat = new THREE.MeshStandardMaterial({ color: rugColor, roughness: 0.95 });
630
+ var rug = new THREE.Mesh(new THREE.PlaneGeometry(6, 4), rugMat);
631
+ rug.rotation.x = -Math.PI / 2; rug.receiveShadow = true;
632
+ group.add(rug);
633
+ // Subtle border stripe (slightly lighter than rug, not bright)
634
+ var borderMat = new THREE.MeshStandardMaterial({ color: borderColor, roughness: 0.9 });
635
+ // Top/bottom borders
636
+ [-1.9, 1.9].forEach(function(bz) {
637
+ var stripe = new THREE.Mesh(new THREE.PlaneGeometry(5.8, 0.08), borderMat);
638
+ stripe.rotation.x = -Math.PI / 2;
639
+ stripe.position.set(0, 0.001, bz);
640
+ group.add(stripe);
641
+ });
642
+ // Left/right borders
643
+ [-2.9, 2.9].forEach(function(bx) {
644
+ var stripe = new THREE.Mesh(new THREE.PlaneGeometry(0.08, 3.8), borderMat);
645
+ stripe.rotation.x = -Math.PI / 2;
646
+ stripe.position.set(bx, 0.001, 0);
647
+ group.add(stripe);
648
+ });
649
+ S.furnitureGroup.add(group);
650
+ }
651
+
652
+ // ===================== WING DIVIDER =====================
653
+ function buildWingDivider(env) {
654
+ var wallMat = new THREE.MeshStandardMaterial({ color: (ENVS[S.currentEnv] || ENVS.modern).wall, roughness: 0.9, side: THREE.DoubleSide });
655
+ // Partial wall separating main office from right wing (gap in middle for walking through)
656
+ // Upper section (z: -8 to -3.5)
657
+ var upper = new THREE.Mesh(new THREE.PlaneGeometry(4.5, 5), wallMat);
658
+ upper.position.set(7, 2.5, -5.75);
659
+ upper.rotation.y = Math.PI / 2;
660
+ upper.receiveShadow = true;
661
+ S.furnitureGroup.add(upper);
662
+ // Lower section (z: 0.5 to 4)
663
+ var lower = new THREE.Mesh(new THREE.PlaneGeometry(3.5, 5), wallMat);
664
+ lower.position.set(7, 2.5, 2.25);
665
+ lower.rotation.y = Math.PI / 2;
666
+ lower.receiveShadow = true;
667
+ S.furnitureGroup.add(lower);
668
+ // Archway header (connecting the two sections above the gap)
669
+ var header = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.5, 4), wallMat);
670
+ header.position.set(7, 4.25, -1.5);
671
+ S.furnitureGroup.add(header);
672
+ // "LOUNGE" sign above archway
673
+ var signDiv = document.createElement('div');
674
+ signDiv.textContent = 'LOUNGE';
675
+ signDiv.style.cssText = 'color:#6c8aff;font-size:10px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:2px;opacity:0.7;';
676
+ var signLabel = new CSS2DObject(signDiv);
677
+ signLabel.position.set(7, 4.2, -1.5);
678
+ S.furnitureGroup.add(signLabel);
679
+ }
680
+
681
+ // ===================== DRESSING ROOM =====================
682
+ function buildDressingRoom(env) {
683
+ var rx = DRESSING_ROOM_POS.x, rz = DRESSING_ROOM_POS.z;
684
+ var group = new THREE.Group();
685
+ group.position.set(rx, 0, rz);
686
+
687
+ // Floor platform (raised circular disc)
688
+ var platformGeo = new THREE.CylinderGeometry(0.6, 0.65, 0.06, 24);
689
+ var platformMat = new THREE.MeshStandardMaterial({ color: 0x4a4a5a, roughness: 0.4, metalness: 0.2 });
690
+ var platform = new THREE.Mesh(platformGeo, platformMat);
691
+ platform.position.y = 0.03; platform.receiveShadow = true; platform.castShadow = true;
692
+ group.add(platform);
693
+ // Platform rim (subtle glow ring)
694
+ var rimGeo = new THREE.TorusGeometry(0.62, 0.02, 8, 32);
695
+ var rimMat = new THREE.MeshStandardMaterial({ color: 0x6c8aff, emissive: 0x6c8aff, emissiveIntensity: 0.4, roughness: 0.3 });
696
+ var rim = new THREE.Mesh(rimGeo, rimMat);
697
+ rim.rotation.x = -Math.PI / 2; rim.position.y = 0.07;
698
+ group.add(rim);
699
+
700
+ // Mirror on the right wall (tall reflective rectangle)
701
+ var mirrorGeo = new THREE.PlaneGeometry(1.2, 2);
702
+ var mirrorMat = new THREE.MeshStandardMaterial({ color: 0xd0d8e8, emissive: 0x8899bb, emissiveIntensity: 0.15, roughness: 0.05, metalness: 0.8 });
703
+ var mirror = new THREE.Mesh(mirrorGeo, mirrorMat);
704
+ mirror.position.set(1.5, 1.2, 0); mirror.rotation.y = -Math.PI / 2;
705
+ group.add(mirror);
706
+ // Mirror frame
707
+ var frameMat = new THREE.MeshStandardMaterial({ color: 0x8B6914, roughness: 0.4, metalness: 0.3 });
708
+ var frameTop = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, 1.3), frameMat);
709
+ frameTop.position.set(1.52, 2.22, 0); group.add(frameTop);
710
+ var frameBot = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, 1.3), frameMat);
711
+ frameBot.position.set(1.52, 0.18, 0); group.add(frameBot);
712
+ var frameL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.1, 0.06), frameMat);
713
+ frameL.position.set(1.52, 1.2, -0.65); group.add(frameL);
714
+ var frameR = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.1, 0.06), frameMat);
715
+ frameR.position.set(1.52, 1.2, 0.65); group.add(frameR);
716
+
717
+ // Left partition wall (half-height privacy screen)
718
+ var partMat = new THREE.MeshStandardMaterial({ color: 0x3a4050, roughness: 0.8, side: THREE.DoubleSide });
719
+ var partL = new THREE.Mesh(new THREE.BoxGeometry(0.06, 2.2, 2.5), partMat);
720
+ partL.position.set(-1.3, 1.1, 0);
721
+ partL.castShadow = true;
722
+ group.add(partL);
723
+ // Back partition
724
+ var partB = new THREE.Mesh(new THREE.BoxGeometry(2.6, 2.2, 0.06), partMat);
725
+ partB.position.set(0.1, 1.1, -1.3);
726
+ partB.castShadow = true;
727
+ group.add(partB);
728
+
729
+ // Coat hooks on back partition
730
+ var hookMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.3, metalness: 0.5 });
731
+ [-0.4, 0.2, 0.8].forEach(function(hx) {
732
+ var hook = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.12, 8), hookMat);
733
+ hook.position.set(hx, 1.6, -1.25); hook.rotation.x = Math.PI / 3;
734
+ group.add(hook);
735
+ });
736
+
737
+ // Warm spotlight
738
+ var spotLight = new THREE.PointLight(0xffeedd, 0.6, 5);
739
+ spotLight.position.set(0, 3, 0);
740
+ group.add(spotLight);
741
+
742
+ // Sign
743
+ var signDiv = document.createElement('div');
744
+ signDiv.textContent = 'DRESSING ROOM';
745
+ signDiv.style.cssText = 'color:#d4af37;font-size:9px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:1.5px;';
746
+ var signLabel = new CSS2DObject(signDiv);
747
+ signLabel.position.set(0, 2.6, 0);
748
+ group.add(signLabel);
749
+
750
+ S.furnitureGroup.add(group);
751
+ }
752
+
753
+ // ===================== REST AREA =====================
754
+ function buildRestArea(env) {
755
+ var rx = REST_AREA_POS.x, rz = REST_AREA_POS.z;
756
+ var group = new THREE.Group();
757
+ group.position.set(rx, 0, rz);
758
+
759
+ // Soft rug (circular, textured)
760
+ var rugGeo = new THREE.CircleGeometry(1.8, 24);
761
+ var rugMat = new THREE.MeshStandardMaterial({ color: 0x4a3828, roughness: 0.95 });
762
+ var rug = new THREE.Mesh(rugGeo, rugMat);
763
+ rug.rotation.x = -Math.PI / 2; rug.position.y = 0.01; rug.receiveShadow = true;
764
+ group.add(rug);
765
+
766
+ // Beanbags (3, arranged in a cozy cluster)
767
+ var bbColors = [0xe53e3e, 0x3b82f6, 0x22c55e];
768
+ var bbPositions = [{ x: -0.6, z: 0.3 }, { x: 0.6, z: 0.4 }, { x: 0, z: -0.5 }];
769
+ bbPositions.forEach(function(pos, i) {
770
+ var bbGroup = new THREE.Group();
771
+ bbGroup.position.set(pos.x, 0, pos.z);
772
+ var botGeo = new THREE.SphereGeometry(0.4, 16, 12);
773
+ var botMat = new THREE.MeshStandardMaterial({ color: bbColors[i], roughness: 0.9 });
774
+ var bottom = new THREE.Mesh(botGeo, botMat);
775
+ bottom.position.y = 0.2; bottom.scale.set(1, 0.5, 1); bottom.castShadow = true;
776
+ bbGroup.add(bottom);
777
+ var topGeo = new THREE.SphereGeometry(0.35, 16, 12);
778
+ var topMat = new THREE.MeshStandardMaterial({ color: bbColors[i], roughness: 0.9 });
779
+ topMat.color.multiplyScalar(0.8);
780
+ var topPart = new THREE.Mesh(topGeo, topMat);
781
+ topPart.position.set(-0.05, 0.4, 0); topPart.scale.set(1, 0.6, 1); topPart.castShadow = true;
782
+ bbGroup.add(topPart);
783
+ bbGroup.rotation.y = (i / 3) * Math.PI * 2;
784
+ group.add(bbGroup);
785
+ });
786
+
787
+ // Small side table
788
+ var tableGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.04, 16);
789
+ var tableMat = new THREE.MeshStandardMaterial({ color: 0x5a3e28, roughness: 0.6 });
790
+ var table = new THREE.Mesh(tableGeo, tableMat);
791
+ table.position.set(1.2, 0.35, 0); table.castShadow = true;
792
+ group.add(table);
793
+ var tableLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.35, 8), tableMat);
794
+ tableLeg.position.set(1.2, 0.175, 0);
795
+ group.add(tableLeg);
796
+
797
+ // Coffee mug on table
798
+ var mugGeo = new THREE.CylinderGeometry(0.04, 0.03, 0.07, 12);
799
+ var mugMat = new THREE.MeshStandardMaterial({ color: 0xf5f5f5, roughness: 0.3 });
800
+ var mug = new THREE.Mesh(mugGeo, mugMat);
801
+ mug.position.set(1.2, 0.405, 0);
802
+ group.add(mug);
803
+
804
+ // Warm dim point light (cozy orange glow)
805
+ var warmLight = new THREE.PointLight(0xffaa55, 0.4, 6);
806
+ warmLight.position.set(0, 2.5, 0);
807
+ group.add(warmLight);
808
+
809
+ // Sign
810
+ var signDiv = document.createElement('div');
811
+ signDiv.textContent = 'REST AREA';
812
+ signDiv.style.cssText = 'color:#facc15;font-size:9px;font-weight:bold;font-family:Inter,sans-serif;letter-spacing:1.5px;';
813
+ var signLabel = new CSS2DObject(signDiv);
814
+ signLabel.position.set(0, 2.2, 0);
815
+ group.add(signLabel);
816
+
817
+ S.furnitureGroup.add(group);
818
+ }