watercooler 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import * as THREE from 'three';
2
2
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
4
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
5
+ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
3
6
 
4
7
  // State
5
8
  let config = { user: '', mailbox: '', avatar: null };
6
9
  let messages = []; // Messages TO user (for main panel)
7
- let allMessages = []; // All messages involving user (for house dialogs)
10
+ let allMessages = []; // All messages involving user (for desk dialogs)
8
11
  let recipients = [];
9
12
  let avatarStates = {}; // Map of name -> {tool_name, timestamp}
10
13
  let scene, camera, renderer, controls;
@@ -12,10 +15,10 @@ let agentMeshes = new Map();
12
15
  let connectionLines = [];
13
16
  let raycaster, mouse;
14
17
 
15
- // Color palette for agents
18
+ // Color palette for agents - modern muted tones
16
19
  const agentColors = [
17
- 0xFF6B6B, 0x4ECDC4, 0x45B7D1, 0xFFA07A, 0x98D8C8,
18
- 0xF7DC6F, 0xBB8FCE, 0x85C1E2, 0xF8B500, 0x6C5CE7
20
+ 0x5EEAD4, 0x6EE7B7, 0x7DD3FC, 0xA78BFA, 0xFBBF24,
21
+ 0xF9A8D4, 0x86EFAC, 0x93C5FD, 0xC4B5FD, 0x67E8F9
19
22
  ];
20
23
 
21
24
  function getAgentColor(name) {
@@ -26,81 +29,130 @@ function getAgentColor(name) {
26
29
  return agentColors[Math.abs(hash) % agentColors.length];
27
30
  }
28
31
 
32
+ // Platform dimensions
33
+ const PLATFORM_SIZE = 60;
34
+ const PLATFORM_HEIGHT = 2;
35
+ const WALL_HEIGHT = 18;
36
+
37
+ // Animated objects
38
+ let holoSphere = null;
39
+ let holoParticles = null;
40
+ let glowLights = [];
41
+ let floatingParticles = [];
42
+ let composer = null;
43
+ let waterMesh = null;
44
+
29
45
  // Initialize Three.js
30
46
  function init() {
31
47
  const container = document.getElementById('canvas-container');
32
48
 
33
49
  scene = new THREE.Scene();
34
- scene.background = new THREE.Color(0x667eea);
50
+ scene.background = new THREE.Color(0x1a3a3a);
51
+ scene.fog = new THREE.FogExp2(0x1a3a3a, 0.003);
35
52
 
36
- camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
37
- camera.position.set(0, 30, 40);
53
+ camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
54
+ camera.position.set(55, 45, 55);
38
55
 
39
56
  renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
40
57
  renderer.setSize(window.innerWidth, window.innerHeight);
58
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
41
59
  renderer.shadowMap.enabled = true;
42
60
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
61
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
62
+ renderer.toneMappingExposure = 1.2;
43
63
  container.appendChild(renderer.domElement);
44
64
 
45
65
  controls = new OrbitControls(camera, renderer.domElement);
46
66
  controls.enableDamping = true;
47
67
  controls.dampingFactor = 0.05;
48
- controls.maxPolarAngle = Math.PI / 2 - 0.1;
49
- controls.minDistance = 20;
50
- controls.maxDistance = 80;
68
+ controls.maxPolarAngle = Math.PI / 2 - 0.05;
69
+ controls.minDistance = 25;
70
+ controls.maxDistance = 120;
51
71
  controls.enableZoom = true;
52
72
  controls.zoomSpeed = 0.8;
53
- controls.enablePan = false; // Disable pan on touch for better mobile UX
73
+ controls.enablePan = false;
74
+ controls.target.set(0, 5, 0);
54
75
  controls.touches = {
55
76
  ONE: THREE.TOUCH.ROTATE,
56
77
  TWO: THREE.TOUCH.DOLLY_PAN
57
78
  };
58
79
 
59
- // Lighting
60
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
80
+ // === Lighting ===
81
+ // Soft ambient
82
+ const ambientLight = new THREE.AmbientLight(0x2d5a5a, 0.8);
61
83
  scene.add(ambientLight);
62
84
 
63
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
64
- dirLight.position.set(50, 100, 50);
85
+ // Main directional light (warm)
86
+ const dirLight = new THREE.DirectionalLight(0xfff5e6, 0.6);
87
+ dirLight.position.set(40, 80, 30);
65
88
  dirLight.castShadow = true;
66
- dirLight.shadow.camera.left = -50;
67
- dirLight.shadow.camera.right = 50;
68
- dirLight.shadow.camera.top = 50;
69
- dirLight.shadow.camera.bottom = -50;
89
+ dirLight.shadow.camera.left = -40;
90
+ dirLight.shadow.camera.right = 40;
91
+ dirLight.shadow.camera.top = 40;
92
+ dirLight.shadow.camera.bottom = -40;
70
93
  dirLight.shadow.mapSize.width = 2048;
71
94
  dirLight.shadow.mapSize.height = 2048;
95
+ dirLight.shadow.bias = -0.001;
72
96
  scene.add(dirLight);
73
97
 
74
- // Ground
75
- const groundGeo = new THREE.PlaneGeometry(200, 200);
76
- const groundMat = new THREE.MeshStandardMaterial({
77
- color: 0x7dd3c0,
78
- roughness: 0.8
79
- });
80
- const ground = new THREE.Mesh(groundGeo, groundMat);
81
- ground.rotation.x = -Math.PI / 2;
82
- ground.receiveShadow = true;
83
- scene.add(ground);
98
+ // Fill light from below (teal tint)
99
+ const fillLight = new THREE.DirectionalLight(0x4fd1c5, 0.3);
100
+ fillLight.position.set(-20, 5, -20);
101
+ scene.add(fillLight);
102
+
103
+ // Hemisphere light for natural ambient
104
+ const hemiLight = new THREE.HemisphereLight(0x4fd1c5, 0x1a3a3a, 0.4);
105
+ scene.add(hemiLight);
106
+
107
+ // Additional accent lights for bloom effect
108
+ // Center glow from holographic sphere area
109
+ const centerGlow = new THREE.PointLight(0x4fd1c5, 0.6, 50);
110
+ centerGlow.position.set(0, PLATFORM_HEIGHT + 15, 0);
111
+ scene.add(centerGlow);
112
+
113
+ // Edge accent lights
114
+ const edgeLight1 = new THREE.PointLight(0x88ffdd, 0.4, 30);
115
+ edgeLight1.position.set(30, PLATFORM_HEIGHT + 10, 30);
116
+ scene.add(edgeLight1);
117
+
118
+ const edgeLight2 = new THREE.PointLight(0x88ffdd, 0.4, 30);
119
+ edgeLight2.position.set(-30, PLATFORM_HEIGHT + 10, -30);
120
+ scene.add(edgeLight2);
84
121
 
85
- // Grid helper
86
- const grid = new THREE.GridHelper(200, 50, 0xffffff, 0xffffff);
87
- grid.material.opacity = 0.2;
88
- grid.material.transparent = true;
89
- scene.add(grid);
122
+ // === Platform ===
123
+ createPlatform();
90
124
 
91
- // Trees
92
- createTrees();
125
+ // === Reflective Water Surface ===
126
+ createReflectiveWater();
127
+
128
+ // === Glass Walls ===
129
+ createGlassWalls();
130
+
131
+ // === Decorative Plants ===
132
+ createPlants();
133
+
134
+ // === Holographic Sphere ===
135
+ createHolographicSphere();
136
+
137
+ // === Ambient Glow Lights ===
138
+ createGlowLights();
139
+
140
+ // === Floating Particles ===
141
+ createFloatingParticles();
142
+
143
+ // === Background Stars ===
144
+ createBackgroundStars();
93
145
 
94
146
  window.addEventListener('resize', onWindowResize);
95
147
 
96
- // Raycaster for house clicks
148
+ // Raycaster for desk clicks
97
149
  raycaster = new THREE.Raycaster();
98
150
  mouse = new THREE.Vector2();
99
- renderer.domElement.addEventListener('click', onHouseClick);
151
+ renderer.domElement.addEventListener('click', onDeskClick);
100
152
 
101
153
  // Add touch support for mobile
102
- renderer.domElement.addEventListener('touchstart', onHouseTouchStart, { passive: false });
103
- renderer.domElement.addEventListener('touchend', onHouseTouchEnd, { passive: false });
154
+ renderer.domElement.addEventListener('touchstart', onDeskTouchStart, { passive: false });
155
+ renderer.domElement.addEventListener('touchend', onDeskTouchEnd, { passive: false });
104
156
 
105
157
  // Disable context menu on mobile for better UX
106
158
  renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
@@ -110,105 +162,649 @@ function init() {
110
162
  setTimeout(onWindowResize, 100);
111
163
  });
112
164
 
165
+ // === Post Processing ===
166
+ setupPostProcessing();
167
+
113
168
  animate();
114
169
  }
115
170
 
116
- function createTrees() {
117
- for (let i = 0; i < 30; i++) {
118
- const x = (Math.random() - 0.5) * 150;
119
- const z = (Math.random() - 0.5) * 150;
171
+ function createPlatform() {
172
+ // Main platform - dark concrete slab
173
+ const platformGeo = new THREE.BoxGeometry(PLATFORM_SIZE, PLATFORM_HEIGHT, PLATFORM_SIZE);
174
+ const platformMat = new THREE.MeshStandardMaterial({
175
+ color: 0x3a3a3a,
176
+ roughness: 0.4,
177
+ metalness: 0.1
178
+ });
179
+ const platform = new THREE.Mesh(platformGeo, platformMat);
180
+ platform.position.y = PLATFORM_HEIGHT / 2;
181
+ platform.receiveShadow = true;
182
+ platform.castShadow = true;
183
+ scene.add(platform);
184
+
185
+ // Edge trim - lighter accent
186
+ const trimGeo = new THREE.BoxGeometry(PLATFORM_SIZE + 0.5, 0.3, PLATFORM_SIZE + 0.5);
187
+ const trimMat = new THREE.MeshStandardMaterial({
188
+ color: 0x5a5a5a,
189
+ roughness: 0.3,
190
+ metalness: 0.3
191
+ });
192
+ const trim = new THREE.Mesh(trimGeo, trimMat);
193
+ trim.position.y = PLATFORM_HEIGHT + 0.15;
194
+ scene.add(trim);
195
+
196
+ // Floor surface - polished concrete with subtle grid
197
+ const floorGeo = new THREE.PlaneGeometry(PLATFORM_SIZE - 2, PLATFORM_SIZE - 2);
198
+ const floorMat = new THREE.MeshStandardMaterial({
199
+ color: 0x4a4a4a,
200
+ roughness: 0.2,
201
+ metalness: 0.15
202
+ });
203
+ const floor = new THREE.Mesh(floorGeo, floorMat);
204
+ floor.rotation.x = -Math.PI / 2;
205
+ floor.position.y = PLATFORM_HEIGHT + 0.02;
206
+ floor.receiveShadow = true;
207
+ scene.add(floor);
208
+
209
+ // Subtle grid on floor
210
+ const gridHelper = new THREE.GridHelper(PLATFORM_SIZE - 4, 20, 0x555555, 0x444444);
211
+ gridHelper.position.y = PLATFORM_HEIGHT + 0.05;
212
+ gridHelper.material.opacity = 0.15;
213
+ gridHelper.material.transparent = true;
214
+ scene.add(gridHelper);
215
+
216
+ // Ground below platform (dark reflection surface)
217
+ const groundGeo = new THREE.PlaneGeometry(300, 300);
218
+ const groundMat = new THREE.MeshStandardMaterial({
219
+ color: 0x1a3a3a,
220
+ roughness: 0.6,
221
+ metalness: 0.2
222
+ });
223
+ const ground = new THREE.Mesh(groundGeo, groundMat);
224
+ ground.rotation.x = -Math.PI / 2;
225
+ ground.position.y = -0.1;
226
+ ground.receiveShadow = true;
227
+ scene.add(ground);
228
+ }
229
+
230
+ function createReflectiveWater() {
231
+ // Reflective water surface below the platform
232
+ const waterSize = PLATFORM_SIZE * 1.5;
233
+ const waterGeo = new THREE.PlaneGeometry(waterSize, waterSize, 64, 64);
234
+
235
+ // Create a custom shader material for reflective water effect
236
+ const waterMat = new THREE.MeshPhysicalMaterial({
237
+ color: 0x0d3333,
238
+ metalness: 0.9,
239
+ roughness: 0.1,
240
+ transparent: true,
241
+ opacity: 0.85,
242
+ transmission: 0.3,
243
+ thickness: 0.5,
244
+ clearcoat: 1.0,
245
+ clearcoatRoughness: 0.1,
246
+ side: THREE.DoubleSide
247
+ });
248
+
249
+ waterMesh = new THREE.Mesh(waterGeo, waterMat);
250
+ waterMesh.rotation.x = -Math.PI / 2;
251
+ waterMesh.position.y = -0.5;
252
+ waterMesh.receiveShadow = true;
253
+ scene.add(waterMesh);
254
+
255
+ // Add subtle ripple effect using vertex displacement
256
+ const positions = waterMesh.geometry.attributes.position;
257
+ const initialPositions = positions.array.slice();
258
+ waterMesh.userData.initialPositions = initialPositions;
259
+ waterMesh.userData.ripplePhase = 0;
260
+ }
261
+
262
+ function setupPostProcessing() {
263
+ // Setup EffectComposer for bloom
264
+ composer = new EffectComposer(renderer);
265
+
266
+ // Add render pass
267
+ const renderPass = new RenderPass(scene, camera);
268
+ composer.addPass(renderPass);
269
+
270
+ // Add bloom pass
271
+ const bloomPass = new UnrealBloomPass(
272
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
273
+ 0.8, // strength
274
+ 0.4, // radius
275
+ 0.75 // threshold
276
+ );
277
+ composer.addPass(bloomPass);
278
+ }
279
+
280
+ function createGlassWalls() {
281
+ const glassMat = new THREE.MeshPhysicalMaterial({
282
+ color: 0x88cccc,
283
+ transparent: true,
284
+ opacity: 0.08,
285
+ roughness: 0.05,
286
+ metalness: 0.0,
287
+ transmission: 0.95,
288
+ thickness: 0.5,
289
+ side: THREE.DoubleSide
290
+ });
291
+
292
+ const wallHeight = WALL_HEIGHT;
293
+ const wallY = PLATFORM_HEIGHT + wallHeight / 2;
294
+ const halfSize = PLATFORM_SIZE / 2;
295
+
296
+ // Back wall
297
+ const backWall = new THREE.Mesh(
298
+ new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
299
+ glassMat
300
+ );
301
+ backWall.position.set(0, wallY, -halfSize);
302
+ scene.add(backWall);
303
+
304
+ // Left wall
305
+ const leftWall = new THREE.Mesh(
306
+ new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
307
+ glassMat
308
+ );
309
+ leftWall.position.set(-halfSize, wallY, 0);
310
+ leftWall.rotation.y = Math.PI / 2;
311
+ scene.add(leftWall);
312
+
313
+ // Right wall (partial, for openness)
314
+ const rightWall = new THREE.Mesh(
315
+ new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
316
+ glassMat
317
+ );
318
+ rightWall.position.set(halfSize, wallY, 0);
319
+ rightWall.rotation.y = -Math.PI / 2;
320
+ scene.add(rightWall);
321
+
322
+ // Glass edge frames (vertical pillars at corners)
323
+ const pillarGeo = new THREE.BoxGeometry(0.5, wallHeight, 0.5);
324
+ const pillarMat = new THREE.MeshStandardMaterial({
325
+ color: 0x777777,
326
+ roughness: 0.2,
327
+ metalness: 0.6
328
+ });
329
+
330
+ const corners = [
331
+ [-halfSize, wallY, -halfSize],
332
+ [halfSize, wallY, -halfSize],
333
+ [-halfSize, wallY, halfSize],
334
+ [halfSize, wallY, halfSize]
335
+ ];
336
+
337
+ corners.forEach(pos => {
338
+ const pillar = new THREE.Mesh(pillarGeo, pillarMat);
339
+ pillar.position.set(...pos);
340
+ pillar.castShadow = true;
341
+ scene.add(pillar);
342
+ });
343
+
344
+ // Top edge frame
345
+ const topFrameMat = new THREE.MeshStandardMaterial({
346
+ color: 0x666666,
347
+ roughness: 0.2,
348
+ metalness: 0.5
349
+ });
350
+
351
+ const frameY = PLATFORM_HEIGHT + wallHeight;
352
+
353
+ // Back top frame
354
+ const backFrame = new THREE.Mesh(new THREE.BoxGeometry(PLATFORM_SIZE, 0.3, 0.3), topFrameMat);
355
+ backFrame.position.set(0, frameY, -halfSize);
356
+ scene.add(backFrame);
357
+
358
+ // Left top frame
359
+ const leftFrame = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, PLATFORM_SIZE), topFrameMat);
360
+ leftFrame.position.set(-halfSize, frameY, 0);
361
+ scene.add(leftFrame);
362
+
363
+ // Right top frame
364
+ const rightFrame = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, PLATFORM_SIZE), topFrameMat);
365
+ rightFrame.position.set(halfSize, frameY, 0);
366
+ scene.add(rightFrame);
367
+ }
368
+
369
+ function createPlants() {
370
+ const plantPositions = [
371
+ // Corner clusters
372
+ [-25, PLATFORM_HEIGHT, -25],
373
+ [25, PLATFORM_HEIGHT, -25],
374
+ [-25, PLATFORM_HEIGHT, 25],
375
+ [25, PLATFORM_HEIGHT, 25],
376
+ // Edge accents
377
+ [-20, PLATFORM_HEIGHT, 27],
378
+ [20, PLATFORM_HEIGHT, 27],
379
+ [-27, PLATFORM_HEIGHT, 0],
380
+ [27, PLATFORM_HEIGHT, -15],
381
+ ];
382
+
383
+ plantPositions.forEach(pos => {
384
+ createPlantCluster(pos[0], pos[1], pos[2]);
385
+ });
386
+ }
387
+
388
+ function createPlantCluster(x, y, z) {
389
+ const group = new THREE.Group();
390
+
391
+ // Planter box
392
+ const planterGeo = new THREE.BoxGeometry(3, 1.5, 3);
393
+ const planterMat = new THREE.MeshStandardMaterial({
394
+ color: 0x2a2a2a,
395
+ roughness: 0.6,
396
+ metalness: 0.1
397
+ });
398
+ const planter = new THREE.Mesh(planterGeo, planterMat);
399
+ planter.position.y = 0.75;
400
+ planter.castShadow = true;
401
+ planter.receiveShadow = true;
402
+ group.add(planter);
403
+
404
+ // Soil
405
+ const soilGeo = new THREE.BoxGeometry(2.6, 0.2, 2.6);
406
+ const soilMat = new THREE.MeshStandardMaterial({ color: 0x3d2817 });
407
+ const soil = new THREE.Mesh(soilGeo, soilMat);
408
+ soil.position.y = 1.5;
409
+ group.add(soil);
410
+
411
+ // Foliage - multiple spheres for bush look
412
+ const leafColors = [0x1a6b3a, 0x228B22, 0x2d8b4e, 0x1f7a3f];
413
+
414
+ for (let i = 0; i < 5; i++) {
415
+ const size = 0.6 + Math.random() * 0.8;
416
+ const leafGeo = new THREE.SphereGeometry(size, 8, 8);
417
+ const leafMat = new THREE.MeshStandardMaterial({
418
+ color: leafColors[Math.floor(Math.random() * leafColors.length)],
419
+ roughness: 0.8
420
+ });
421
+ const leaf = new THREE.Mesh(leafGeo, leafMat);
422
+ leaf.position.set(
423
+ (Math.random() - 0.5) * 1.5,
424
+ 1.8 + Math.random() * 1.5,
425
+ (Math.random() - 0.5) * 1.5
426
+ );
427
+ leaf.castShadow = true;
428
+ group.add(leaf);
429
+ }
430
+
431
+ // Tall fern-like elements (cone shapes)
432
+ for (let i = 0; i < 3; i++) {
433
+ const fernGeo = new THREE.ConeGeometry(0.3, 2 + Math.random() * 2, 6);
434
+ const fernMat = new THREE.MeshStandardMaterial({
435
+ color: 0x1a5c2e,
436
+ roughness: 0.7
437
+ });
438
+ const fern = new THREE.Mesh(fernGeo, fernMat);
439
+ fern.position.set(
440
+ (Math.random() - 0.5) * 1.5,
441
+ 2.5 + Math.random() * 1.5,
442
+ (Math.random() - 0.5) * 1.5
443
+ );
444
+ fern.castShadow = true;
445
+ group.add(fern);
446
+ }
447
+
448
+ group.position.set(x, y, z);
449
+ scene.add(group);
450
+ }
451
+
452
+ function createHolographicSphere() {
453
+ // Wireframe sphere
454
+ const sphereGeo = new THREE.IcosahedronGeometry(6, 3);
455
+ const sphereMat = new THREE.MeshBasicMaterial({
456
+ color: 0x4fd1c5,
457
+ wireframe: true,
458
+ transparent: true,
459
+ opacity: 0.3
460
+ });
461
+ holoSphere = new THREE.Mesh(sphereGeo, sphereMat);
462
+ holoSphere.position.set(0, PLATFORM_HEIGHT + 12, 0);
463
+ scene.add(holoSphere);
464
+
465
+ // Inner glow sphere
466
+ const innerGeo = new THREE.SphereGeometry(4, 32, 32);
467
+ const innerMat = new THREE.MeshBasicMaterial({
468
+ color: 0x4fd1c5,
469
+ transparent: true,
470
+ opacity: 0.05
471
+ });
472
+ const innerSphere = new THREE.Mesh(innerGeo, innerMat);
473
+ holoSphere.add(innerSphere);
474
+
475
+ // Point cloud on sphere surface
476
+ const particleCount = 300;
477
+ const positions = new Float32Array(particleCount * 3);
478
+ for (let i = 0; i < particleCount; i++) {
479
+ const theta = Math.random() * Math.PI * 2;
480
+ const phi = Math.acos(2 * Math.random() - 1);
481
+ const r = 5.5 + Math.random() * 0.5;
482
+ positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
483
+ positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
484
+ positions[i * 3 + 2] = r * Math.cos(phi);
485
+ }
486
+
487
+ const particleGeo = new THREE.BufferGeometry();
488
+ particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
489
+ const particleMat = new THREE.PointsMaterial({
490
+ color: 0x88ffee,
491
+ size: 0.15,
492
+ transparent: true,
493
+ opacity: 0.6,
494
+ blending: THREE.AdditiveBlending
495
+ });
496
+ holoParticles = new THREE.Points(particleGeo, particleMat);
497
+ holoSphere.add(holoParticles);
498
+
499
+ // Point light from the sphere
500
+ const sphereLight = new THREE.PointLight(0x4fd1c5, 0.8, 35);
501
+ sphereLight.position.copy(holoSphere.position);
502
+ scene.add(sphereLight);
503
+ }
504
+
505
+ function createGlowLights() {
506
+ // Floor-standing lamp posts
507
+ const lampPositions = [
508
+ [20, PLATFORM_HEIGHT, 15],
509
+ [-20, PLATFORM_HEIGHT, 15],
510
+ [20, PLATFORM_HEIGHT, -20],
511
+ [-20, PLATFORM_HEIGHT, -20],
512
+ ];
513
+
514
+ lampPositions.forEach(pos => {
515
+ // Lamp post
516
+ const postGeo = new THREE.CylinderGeometry(0.15, 0.15, 6, 8);
517
+ const postMat = new THREE.MeshStandardMaterial({
518
+ color: 0x555555,
519
+ roughness: 0.3,
520
+ metalness: 0.7
521
+ });
522
+ const post = new THREE.Mesh(postGeo, postMat);
523
+ post.position.set(pos[0], pos[1] + 3, pos[2]);
524
+ post.castShadow = true;
525
+ scene.add(post);
120
526
 
121
- // Don't place trees too close to center
122
- if (Math.sqrt(x*x + z*z) < 30) continue;
527
+ // Lamp bulb
528
+ const bulbGeo = new THREE.SphereGeometry(0.4, 16, 16);
529
+ const bulbMat = new THREE.MeshBasicMaterial({
530
+ color: 0xffcc66,
531
+ transparent: true,
532
+ opacity: 0.9
533
+ });
534
+ const bulb = new THREE.Mesh(bulbGeo, bulbMat);
535
+ bulb.position.set(pos[0], pos[1] + 6.2, pos[2]);
536
+ scene.add(bulb);
123
537
 
124
- const trunkGeo = new THREE.CylinderGeometry(0.5, 0.8, 3, 8);
125
- const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
126
- const trunk = new THREE.Mesh(trunkGeo, trunkMat);
127
- trunk.position.set(x, 1.5, z);
128
- trunk.castShadow = true;
538
+ // Point light
539
+ const light = new THREE.PointLight(0xffcc66, 0.5, 18);
540
+ light.position.set(pos[0], pos[1] + 6.2, pos[2]);
541
+ light.castShadow = false;
542
+ scene.add(light);
543
+ glowLights.push({ bulb, light, baseIntensity: 0.5 });
544
+ });
545
+ }
546
+
547
+ function createFloatingParticles() {
548
+ const particleCount = 80;
549
+ const positions = new Float32Array(particleCount * 3);
550
+
551
+ for (let i = 0; i < particleCount; i++) {
552
+ positions[i * 3] = (Math.random() - 0.5) * PLATFORM_SIZE;
553
+ positions[i * 3 + 1] = PLATFORM_HEIGHT + 2 + Math.random() * WALL_HEIGHT;
554
+ positions[i * 3 + 2] = (Math.random() - 0.5) * PLATFORM_SIZE;
555
+ }
556
+
557
+ const geometry = new THREE.BufferGeometry();
558
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
559
+
560
+ const material = new THREE.PointsMaterial({
561
+ color: 0x88ffdd,
562
+ size: 0.12,
563
+ transparent: true,
564
+ opacity: 0.4,
565
+ blending: THREE.AdditiveBlending
566
+ });
567
+
568
+ const particles = new THREE.Points(geometry, material);
569
+ scene.add(particles);
570
+ floatingParticles.push(particles);
571
+ }
572
+
573
+ function createBackgroundStars() {
574
+ // Distant stars/sparkles in the background
575
+ const starCount = 200;
576
+ const positions = new Float32Array(starCount * 3);
577
+ const sizes = new Float32Array(starCount);
578
+
579
+ for (let i = 0; i < starCount; i++) {
580
+ // Place stars far outside the platform
581
+ const theta = Math.random() * Math.PI * 2;
582
+ const phi = Math.acos(2 * Math.random() - 1);
583
+ const radius = 100 + Math.random() * 150;
129
584
 
130
- const leavesGeo = new THREE.ConeGeometry(3, 8, 8);
131
- const leavesMat = new THREE.MeshStandardMaterial({ color: 0x228B22 });
132
- const leaves = new THREE.Mesh(leavesGeo, leavesMat);
133
- leaves.position.set(x, 6, z);
134
- leaves.castShadow = true;
585
+ positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
586
+ positions[i * 3 + 1] = 20 + Math.random() * 100;
587
+ positions[i * 3 + 2] = radius * Math.sin(phi) * Math.sin(theta);
135
588
 
136
- scene.add(trunk);
137
- scene.add(leaves);
589
+ sizes[i] = 0.5 + Math.random() * 1.5;
138
590
  }
591
+
592
+ const geometry = new THREE.BufferGeometry();
593
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
594
+ geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
595
+
596
+ const material = new THREE.PointsMaterial({
597
+ color: 0xaaddff,
598
+ size: 1.0,
599
+ transparent: true,
600
+ opacity: 0.6,
601
+ blending: THREE.AdditiveBlending,
602
+ sizeAttenuation: true
603
+ });
604
+
605
+ const stars = new THREE.Points(geometry, material);
606
+ scene.add(stars);
607
+
608
+ // Animate stars with twinkle effect
609
+ stars.userData.twinklePhase = Math.random() * Math.PI * 2;
610
+
611
+ // Add to floatingParticles for animation
612
+ floatingParticles.push(stars);
139
613
  }
140
614
 
141
- function createAgentHouse(name, position, toolName = null) {
615
+ function createAgentDesk(name, position, toolName = null) {
142
616
  const color = getAgentColor(name);
143
617
  const group = new THREE.Group();
144
618
  group.position.copy(position);
619
+ group.position.y = PLATFORM_HEIGHT;
620
+
621
+ // Modern desk - white top with thin legs
622
+ const deskTopGeo = new THREE.BoxGeometry(5, 0.2, 3);
623
+ const deskMat = new THREE.MeshStandardMaterial({
624
+ color: 0xe8e8e8,
625
+ roughness: 0.3,
626
+ metalness: 0.1
627
+ });
628
+ const deskTop = new THREE.Mesh(deskTopGeo, deskMat);
629
+ deskTop.position.y = 2.5;
630
+ deskTop.castShadow = true;
631
+ deskTop.receiveShadow = true;
632
+ group.add(deskTop);
633
+
634
+ // Desk legs - thin metal
635
+ const legGeo = new THREE.CylinderGeometry(0.08, 0.08, 2.4, 8);
636
+ const legMat = new THREE.MeshStandardMaterial({
637
+ color: 0x999999,
638
+ roughness: 0.2,
639
+ metalness: 0.7
640
+ });
641
+ const legPositions = [
642
+ [-2.2, 1.2, -1.2],
643
+ [2.2, 1.2, -1.2],
644
+ [-2.2, 1.2, 1.2],
645
+ [2.2, 1.2, 1.2]
646
+ ];
647
+ legPositions.forEach(pos => {
648
+ const leg = new THREE.Mesh(legGeo, legMat);
649
+ leg.position.set(...pos);
650
+ group.add(leg);
651
+ });
145
652
 
146
- // House base
147
- const baseGeo = new THREE.BoxGeometry(6, 4, 6);
148
- const baseMat = new THREE.MeshStandardMaterial({ color: color });
149
- const base = new THREE.Mesh(baseGeo, baseMat);
150
- base.position.y = 2;
151
- base.castShadow = true;
152
- base.receiveShadow = true;
153
- group.add(base);
154
-
155
- // Roof
156
- const roofGeo = new THREE.ConeGeometry(5, 3, 4);
157
- const roofMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
158
- const roof = new THREE.Mesh(roofGeo, roofMat);
159
- roof.position.y = 5.5;
160
- roof.rotation.y = Math.PI / 4;
161
- roof.castShadow = true;
162
- group.add(roof);
163
-
164
- // Door
165
- const doorGeo = new THREE.BoxGeometry(1.5, 2.5, 0.2);
166
- const doorMat = new THREE.MeshStandardMaterial({ color: 0x4a3c28 });
167
- const door = new THREE.Mesh(doorGeo, doorMat);
168
- door.position.set(0, 1.25, 3.1);
169
- group.add(door);
170
-
171
- // Windows
172
- const windowGeo = new THREE.BoxGeometry(1.2, 1.2, 0.2);
173
- const windowMat = new THREE.MeshStandardMaterial({
174
- color: 0xFFFF99,
175
- emissive: 0xFFFF99,
176
- emissiveIntensity: 0.3
177
- });
178
-
179
- const window1 = new THREE.Mesh(windowGeo, windowMat);
180
- window1.position.set(-1.8, 2.5, 3.1);
181
- group.add(window1);
182
-
183
- const window2 = new THREE.Mesh(windowGeo, windowMat);
184
- window2.position.set(1.8, 2.5, 3.1);
185
- group.add(window2);
186
-
187
- // Name label sprite (with optional tool name)
653
+ // Modern chair - sleek
654
+ const chairSeatGeo = new THREE.BoxGeometry(1.8, 0.15, 1.8);
655
+ const chairMat = new THREE.MeshStandardMaterial({
656
+ color: 0x2a2a2a,
657
+ roughness: 0.5,
658
+ metalness: 0.2
659
+ });
660
+ const chairSeat = new THREE.Mesh(chairSeatGeo, chairMat);
661
+ chairSeat.position.set(0, 1.6, 3.2);
662
+ chairSeat.castShadow = true;
663
+ group.add(chairSeat);
664
+
665
+ // Chair back - curved look (box approximation)
666
+ const chairBackGeo = new THREE.BoxGeometry(1.8, 2.2, 0.15);
667
+ const chairBack = new THREE.Mesh(chairBackGeo, chairMat);
668
+ chairBack.position.set(0, 2.7, 4.1);
669
+ chairBack.castShadow = true;
670
+ group.add(chairBack);
671
+
672
+ // Chair post
673
+ const chairPostGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.2, 8);
674
+ const chairPost = new THREE.Mesh(chairPostGeo, legMat);
675
+ chairPost.position.set(0, 0.9, 3.2);
676
+ group.add(chairPost);
677
+
678
+ // Chair base star
679
+ for (let i = 0; i < 5; i++) {
680
+ const armGeo = new THREE.CylinderGeometry(0.06, 0.06, 1.2, 6);
681
+ const arm = new THREE.Mesh(armGeo, legMat);
682
+ const angle = (i / 5) * Math.PI * 2;
683
+ arm.rotation.z = Math.PI / 2;
684
+ arm.position.set(
685
+ Math.cos(angle) * 0.5,
686
+ 0.3,
687
+ 3.2 + Math.sin(angle) * 0.5
688
+ );
689
+ arm.rotation.y = angle;
690
+ group.add(arm);
691
+ }
692
+
693
+ // Person - Body (sitting, modern look)
694
+ const bodyGeo = new THREE.CylinderGeometry(0.6, 0.5, 2, 8);
695
+ const bodyMat = new THREE.MeshStandardMaterial({
696
+ color: color,
697
+ roughness: 0.6,
698
+ metalness: 0.05
699
+ });
700
+ const body = new THREE.Mesh(bodyGeo, bodyMat);
701
+ body.position.set(0, 2.7, 3.2);
702
+ body.castShadow = true;
703
+ group.add(body);
704
+
705
+ // Person - Head
706
+ const headGeo = new THREE.SphereGeometry(0.5, 16, 16);
707
+ const headMat = new THREE.MeshStandardMaterial({
708
+ color: 0xf5d0b0,
709
+ roughness: 0.7
710
+ });
711
+ const head = new THREE.Mesh(headGeo, headMat);
712
+ head.position.set(0, 4.0, 3.2);
713
+ head.castShadow = true;
714
+ group.add(head);
715
+
716
+ // Person - Arms on desk
717
+ const armObjGeo = new THREE.CylinderGeometry(0.12, 0.12, 1.8, 6);
718
+ const armMat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.6 });
719
+
720
+ const leftArm = new THREE.Mesh(armObjGeo, armMat);
721
+ leftArm.rotation.z = Math.PI / 2;
722
+ leftArm.rotation.y = 0.3;
723
+ leftArm.position.set(-0.8, 2.8, 2);
724
+ group.add(leftArm);
725
+
726
+ const rightArm = new THREE.Mesh(armObjGeo, armMat);
727
+ rightArm.rotation.z = Math.PI / 2;
728
+ rightArm.rotation.y = -0.3;
729
+ rightArm.position.set(0.8, 2.8, 2);
730
+ group.add(rightArm);
731
+
732
+ // Monitor (modern flat screen)
733
+ const monitorStandGeo = new THREE.CylinderGeometry(0.5, 0.6, 0.1, 16);
734
+ const monitorMat = new THREE.MeshStandardMaterial({
735
+ color: 0x333333,
736
+ roughness: 0.3,
737
+ metalness: 0.5
738
+ });
739
+ const monitorStand = new THREE.Mesh(monitorStandGeo, monitorMat);
740
+ monitorStand.position.set(0, 2.65, 0.8);
741
+ group.add(monitorStand);
742
+
743
+ const monitorNeckGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.2, 8);
744
+ const monitorNeck = new THREE.Mesh(monitorNeckGeo, monitorMat);
745
+ monitorNeck.position.set(0, 3.2, 0.8);
746
+ group.add(monitorNeck);
747
+
748
+ // Screen
749
+ const screenFrameGeo = new THREE.BoxGeometry(3, 1.8, 0.12);
750
+ const screenFrame = new THREE.Mesh(screenFrameGeo, monitorMat);
751
+ screenFrame.position.set(0, 4.0, 0.8);
752
+ screenFrame.castShadow = true;
753
+ group.add(screenFrame);
754
+
755
+ // Screen display (glowing)
756
+ const screenDisplayGeo = new THREE.PlaneGeometry(2.7, 1.5);
757
+ const screenDisplayMat = new THREE.MeshBasicMaterial({
758
+ color: 0x2a6b5e,
759
+ });
760
+ const screenDisplay = new THREE.Mesh(screenDisplayGeo, screenDisplayMat);
761
+ screenDisplay.position.set(0, 4.0, 0.87);
762
+ group.add(screenDisplay);
763
+
764
+ // Screen glow light
765
+ const screenLight = new THREE.PointLight(0x4fd1c5, 0.3, 6);
766
+ screenLight.position.set(0, 4.0, 1.5);
767
+ group.add(screenLight);
768
+
769
+ // Keyboard
770
+ const kbGeo = new THREE.BoxGeometry(1.6, 0.05, 0.5);
771
+ const kbMat = new THREE.MeshStandardMaterial({
772
+ color: 0x444444,
773
+ roughness: 0.5,
774
+ metalness: 0.3
775
+ });
776
+ const keyboard = new THREE.Mesh(kbGeo, kbMat);
777
+ keyboard.position.set(0, 2.63, 2);
778
+ group.add(keyboard);
779
+
780
+ // Name label sprite
188
781
  const canvas = document.createElement('canvas');
189
782
  const context = canvas.getContext('2d');
190
- // High DPI canvas for crisp text
191
783
  const scale = 2;
192
784
  canvas.width = 512;
193
- canvas.height = toolName ? 160 : 128; // Taller if showing tool
785
+ canvas.height = toolName ? 160 : 128;
194
786
  context.scale(scale, scale);
195
787
 
196
- // Background
197
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
788
+ // Frosted glass background
789
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
198
790
  context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
199
791
  context.fill();
200
792
 
201
- // Name text
202
- context.font = 'bold 24px Arial';
203
- context.fillStyle = 'white';
793
+ // Subtle border
794
+ context.strokeStyle = 'rgba(79, 209, 197, 0.4)';
795
+ context.lineWidth = 1;
796
+ context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
797
+ context.stroke();
798
+
799
+ context.font = 'bold 22px Arial';
800
+ context.fillStyle = '#e0f5f0';
204
801
  context.textAlign = 'center';
205
802
  context.textBaseline = 'middle';
206
803
  context.fillText(name, 128, 24);
207
804
 
208
- // Tool name text (if available)
209
805
  if (toolName) {
210
- context.font = 'italic 16px Arial';
211
- context.fillStyle = '#FFD700'; // Gold color for tool name
806
+ context.font = 'italic 14px Arial';
807
+ context.fillStyle = '#4fd1c5';
212
808
  context.fillText(toolName, 128, 56);
213
809
  }
214
810
 
@@ -217,30 +813,21 @@ function createAgentHouse(name, position, toolName = null) {
217
813
  texture.magFilter = THREE.LinearFilter;
218
814
  const spriteMat = new THREE.SpriteMaterial({ map: texture });
219
815
  const sprite = new THREE.Sprite(spriteMat);
220
- sprite.position.set(0, toolName ? 8.5 : 8, 0);
221
- sprite.scale.set(8, toolName ? 2.5 : 2, 1);
222
- sprite.name = 'label'; // Tag for easy updates
816
+ sprite.position.set(0, 6.5, 2);
817
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
818
+ sprite.name = 'label';
223
819
  group.add(sprite);
224
820
 
225
- // Path to house
226
- const pathGeo = new THREE.PlaneGeometry(2, 8);
227
- const pathMat = new THREE.MeshStandardMaterial({ color: 0xD2B48C });
228
- const path = new THREE.Mesh(pathGeo, pathMat);
229
- path.rotation.x = -Math.PI / 2;
230
- path.position.set(0, 0.02, 7);
231
- group.add(path);
232
-
233
821
  scene.add(group);
234
822
  agentMeshes.set(name, group);
235
823
 
236
824
  return group;
237
825
  }
238
826
 
239
- function updateHouseLabel(house, name, toolName = null) {
240
- const sprite = house.getObjectByName('label');
827
+ function updateDeskLabel(desk, name, toolName = null) {
828
+ const sprite = desk.getObjectByName('label');
241
829
  if (!sprite) return;
242
830
 
243
- // Create new canvas with updated text
244
831
  const canvas = document.createElement('canvas');
245
832
  const context = canvas.getContext('2d');
246
833
  const scale = 2;
@@ -248,67 +835,73 @@ function updateHouseLabel(house, name, toolName = null) {
248
835
  canvas.height = toolName ? 160 : 128;
249
836
  context.scale(scale, scale);
250
837
 
251
- // Background
252
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
838
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
253
839
  context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
254
840
  context.fill();
255
841
 
256
- // Name text
257
- context.font = 'bold 24px Arial';
258
- context.fillStyle = 'white';
842
+ context.strokeStyle = 'rgba(79, 209, 197, 0.4)';
843
+ context.lineWidth = 1;
844
+ context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
845
+ context.stroke();
846
+
847
+ context.font = 'bold 22px Arial';
848
+ context.fillStyle = '#e0f5f0';
259
849
  context.textAlign = 'center';
260
850
  context.textBaseline = 'middle';
261
851
  context.fillText(name, 128, 24);
262
852
 
263
- // Tool name text (if available)
264
853
  if (toolName) {
265
- context.font = 'italic 16px Arial';
266
- context.fillStyle = '#FFD700';
854
+ context.font = 'italic 14px Arial';
855
+ context.fillStyle = '#4fd1c5';
267
856
  context.fillText(toolName, 128, 56);
268
857
  }
269
858
 
270
- // Update texture
271
859
  const texture = new THREE.CanvasTexture(canvas);
272
860
  texture.minFilter = THREE.LinearFilter;
273
861
  texture.magFilter = THREE.LinearFilter;
274
862
  sprite.material.map = texture;
275
863
  sprite.material.needsUpdate = true;
276
864
 
277
- // Update position and scale
278
- sprite.position.set(0, toolName ? 8.5 : 8, 0);
279
- sprite.scale.set(8, toolName ? 2.5 : 2, 1);
865
+ sprite.position.set(0, 6.5, 2);
866
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
280
867
  }
281
868
 
282
869
  function createMessageParticle(fromPos, toPos) {
283
- const particleGeo = new THREE.SphereGeometry(0.3, 8, 8);
284
- const particleMat = new THREE.MeshStandardMaterial({
285
- color: 0xFFD700,
286
- emissive: 0xFFD700,
287
- emissiveIntensity: 0.5
870
+ const particleGeo = new THREE.SphereGeometry(0.35, 12, 12);
871
+ const particleMat = new THREE.MeshBasicMaterial({
872
+ color: 0xff6b6b,
873
+ transparent: true,
874
+ opacity: 0.95
288
875
  });
289
876
  const particle = new THREE.Mesh(particleGeo, particleMat);
290
877
 
291
878
  particle.position.copy(fromPos);
292
- particle.position.y += 8;
879
+ particle.position.y += 5;
880
+
881
+ // Add a glow point light that follows particle
882
+ const glow = new THREE.PointLight(0xff6b6b, 1.0, 10);
883
+ particle.add(glow);
293
884
 
294
885
  scene.add(particle);
295
886
 
296
- // Animate particle
297
887
  const startTime = Date.now();
298
- const duration = 2000;
888
+ const duration = 1500;
299
889
 
300
890
  function animateParticle() {
301
891
  const elapsed = Date.now() - startTime;
302
892
  const progress = Math.min(elapsed / duration, 1);
303
893
 
304
894
  particle.position.lerpVectors(
305
- new THREE.Vector3(fromPos.x, fromPos.y + 8, fromPos.z),
306
- new THREE.Vector3(toPos.x, toPos.y + 8, toPos.z),
895
+ new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z),
896
+ new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z),
307
897
  progress
308
898
  );
309
899
 
310
- // Add arc
311
- particle.position.y += Math.sin(progress * Math.PI) * 3;
900
+ particle.position.y += Math.sin(progress * Math.PI) * 2;
901
+
902
+ // Pulse size
903
+ const pulse = Math.sin(progress * Math.PI) * 0.2 + 1;
904
+ particle.scale.setScalar(pulse);
312
905
 
313
906
  if (progress < 1) {
314
907
  requestAnimationFrame(animateParticle);
@@ -324,48 +917,58 @@ function createConnectionLine(fromPos, toPos) {
324
917
  const startPos = new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z);
325
918
  const endPos = new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z);
326
919
 
920
+ // Create curved line with points
921
+ const mid = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5);
922
+ mid.y += 2;
923
+
924
+ const curve = new THREE.QuadraticBezierCurve3(startPos, mid, endPos);
925
+ const points = curve.getPoints(50);
926
+
927
+ // Main line - thicker, glowing red
327
928
  const material = new THREE.LineBasicMaterial({
328
929
  color: 0xff6b6b,
329
- opacity: 0.8,
930
+ opacity: 0.7,
330
931
  transparent: true,
331
932
  linewidth: 3
332
933
  });
333
934
 
334
- const points = [startPos, endPos];
335
-
336
935
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
337
936
  const line = new THREE.Line(geometry, material);
338
937
 
339
938
  scene.add(line);
340
939
  connectionLines.push(line);
341
940
 
342
- // Add arrowhead for directionality
941
+ // Add small chevron markers along the path to show direction
343
942
  const direction = new THREE.Vector3().subVectors(endPos, startPos).normalize();
344
- const arrowPos = endPos.clone().sub(direction.clone().multiplyScalar(5));
345
-
346
- const arrowGeometry = new THREE.ConeGeometry(0.5, 1.5, 8);
347
- const arrowMaterial = new THREE.MeshStandardMaterial({
348
- color: 0xff6b6b,
349
- emissive: 0xff6b6b,
350
- emissiveIntensity: 0.3
351
- });
352
- const arrowhead = new THREE.Mesh(arrowGeometry, arrowMaterial);
353
-
354
- arrowhead.position.copy(arrowPos);
355
-
356
- // Orient arrow to point along the line
357
943
  const up = new THREE.Vector3(0, 1, 0);
358
- const quaternion = new THREE.Quaternion();
359
- quaternion.setFromUnitVectors(up, direction);
360
- arrowhead.setRotationFromQuaternion(quaternion);
361
-
362
- scene.add(arrowhead);
363
- connectionLines.push(arrowhead);
944
+ const numMarkers = 4;
945
+ for (let i = 0; i < numMarkers; i++) {
946
+ const t = (i + 1) / (numMarkers + 1);
947
+ const markerPos = curve.getPoint(t);
948
+
949
+ const markerGeo = new THREE.ConeGeometry(0.25, 0.7, 8);
950
+ const markerMat = new THREE.MeshBasicMaterial({
951
+ color: 0xff6b6b,
952
+ transparent: true,
953
+ opacity: 0.5
954
+ });
955
+ const marker = new THREE.Mesh(markerGeo, markerMat);
956
+
957
+ // Get tangent at this point for direction
958
+ const tangent = curve.getTangent(t);
959
+ marker.position.copy(markerPos);
960
+
961
+ const markerQuat = new THREE.Quaternion();
962
+ markerQuat.setFromUnitVectors(up, tangent);
963
+ marker.setRotationFromQuaternion(markerQuat);
964
+
965
+ scene.add(marker);
966
+ connectionLines.push(marker);
967
+ }
364
968
 
365
- // Send particle
366
969
  setTimeout(() => {
367
970
  createMessageParticle(fromPos, toPos);
368
- }, 100);
971
+ }, 50);
369
972
  }
370
973
 
371
974
  function clearConnections() {
@@ -377,49 +980,47 @@ function updateVillage() {
377
980
  clearConnections();
378
981
 
379
982
  // Use recipients (from coworkers.db) as the authoritative list of agents
380
- // This ensures we only show houses for registered coworkers
381
983
  const allAgents = new Set([config.user.toLowerCase(), ...recipients.map(r => r.toLowerCase())]);
382
984
 
383
- // Also add message participants that might not be in coworker db yet
985
+ // Also add message participants
384
986
  messages.forEach(m => {
385
987
  allAgents.add(m.sender.toLowerCase());
386
988
  allAgents.add(m.recipient.toLowerCase());
387
989
  });
388
990
 
389
- // Arrange agents in a circle
991
+ // Arrange agents in a circle on the platform
390
992
  const agents = Array.from(allAgents);
391
- const radius = 25;
993
+ const radius = Math.min(20, Math.max(10, agents.length * 3));
392
994
 
393
995
  agents.forEach((agent, index) => {
394
- const angle = (index / agents.length) * Math.PI * 2;
996
+ const angle = (index / agents.length) * Math.PI * 2 - Math.PI / 2;
395
997
  const x = Math.cos(angle) * radius;
396
998
  const z = Math.sin(angle) * radius;
397
999
  const position = new THREE.Vector3(x, 0, z);
398
1000
 
399
- // Get tool name for this agent from avatar states
400
1001
  const avatarState = avatarStates[agent.toLowerCase()];
401
1002
  const toolName = avatarState?.tool_name || null;
402
1003
 
403
1004
  if (!agentMeshes.has(agent)) {
404
- createAgentHouse(agent, position, toolName);
1005
+ const group = createAgentDesk(agent, position, toolName);
1006
+ // Face desk toward center
1007
+ group.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
405
1008
  } else {
406
- // Update position if needed
407
- const house = agentMeshes.get(agent);
408
- house.position.copy(position);
409
-
410
- // Update label with current tool name
411
- updateHouseLabel(house, agent, toolName);
1009
+ const desk = agentMeshes.get(agent);
1010
+ desk.position.set(x, PLATFORM_HEIGHT, z);
1011
+ desk.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
1012
+ updateDeskLabel(desk, agent, toolName);
412
1013
  }
413
1014
  });
414
1015
 
415
- // Remove houses for coworkers that no longer exist
416
- agentMeshes.forEach((house, name) => {
1016
+ // Remove desks for coworkers that no longer exist
1017
+ agentMeshes.forEach((desk, name) => {
417
1018
  if (!allAgents.has(name)) {
418
1019
  // Remove from scene
419
- scene.remove(house);
1020
+ scene.remove(desk);
420
1021
 
421
1022
  // Dispose of geometries and materials to prevent memory leaks
422
- house.traverse((child) => {
1023
+ desk.traverse((child) => {
423
1024
  if (child.isMesh) {
424
1025
  child.geometry.dispose();
425
1026
  if (child.material) {
@@ -439,25 +1040,86 @@ function updateVillage() {
439
1040
 
440
1041
  // Create connections for unread messages only
441
1042
  allMessages.forEach(msg => {
442
- const fromHouse = agentMeshes.get(msg.sender.toLowerCase());
443
- const toHouse = agentMeshes.get(msg.recipient.toLowerCase());
1043
+ const fromDesk = agentMeshes.get(msg.sender.toLowerCase());
1044
+ const toDesk = agentMeshes.get(msg.recipient.toLowerCase());
444
1045
 
445
- if (fromHouse && toHouse && !msg.read) {
1046
+ if (fromDesk && toDesk && !msg.read) {
446
1047
  createConnectionLine(
447
- fromHouse.position,
448
- toHouse.position
1048
+ fromDesk.position,
1049
+ toDesk.position
449
1050
  );
450
1051
  }
451
1052
  });
452
1053
 
453
- // Update house labels with unread indicators
454
- updateHouseLabels();
1054
+ // Update desk labels with unread indicators
1055
+ updateDeskLabels();
455
1056
  }
456
1057
 
457
1058
  function animate() {
458
1059
  requestAnimationFrame(animate);
1060
+
1061
+ const time = Date.now() * 0.001;
1062
+
1063
+ // Rotate holographic sphere
1064
+ if (holoSphere) {
1065
+ holoSphere.rotation.y = time * 0.15;
1066
+ holoSphere.rotation.x = Math.sin(time * 0.1) * 0.1;
1067
+ }
1068
+
1069
+ // Animate floating particles
1070
+ floatingParticles.forEach((particles, index) => {
1071
+ const positions = particles.geometry.attributes.position.array;
1072
+
1073
+ if (particles.userData.twinklePhase !== undefined) {
1074
+ // Star twinkling effect
1075
+ const twinkle = Math.sin(time * 2 + particles.userData.twinklePhase) * 0.3 + 0.7;
1076
+ particles.material.opacity = 0.4 + twinkle * 0.4;
1077
+
1078
+ // Slowly rotate stars
1079
+ particles.rotation.y = time * 0.02;
1080
+ } else {
1081
+ // Regular floating particles
1082
+ for (let i = 0; i < positions.length; i += 3) {
1083
+ positions[i + 1] += Math.sin(time + positions[i] * 0.1) * 0.003;
1084
+ }
1085
+ particles.geometry.attributes.position.needsUpdate = true;
1086
+ }
1087
+ });
1088
+
1089
+ // Subtle glow pulse on lamps
1090
+ glowLights.forEach((item, i) => {
1091
+ const pulse = Math.sin(time * 1.5 + i) * 0.15 + 1;
1092
+ item.light.intensity = item.baseIntensity * pulse;
1093
+ });
1094
+
1095
+ // Animate water ripples
1096
+ if (waterMesh && waterMesh.userData.initialPositions) {
1097
+ const positions = waterMesh.geometry.attributes.position;
1098
+ const initialPositions = waterMesh.userData.initialPositions;
1099
+
1100
+ for (let i = 0; i < positions.count; i++) {
1101
+ const x = initialPositions[i * 3];
1102
+ const y = initialPositions[i * 3 + 1];
1103
+
1104
+ // Create gentle ripple effect
1105
+ const distance = Math.sqrt(x * x + y * y);
1106
+ const wave1 = Math.sin(distance * 0.3 - time * 0.8) * 0.15;
1107
+ const wave2 = Math.sin(x * 0.2 + time * 0.5) * 0.1;
1108
+ const wave3 = Math.cos(y * 0.15 + time * 0.3) * 0.08;
1109
+
1110
+ positions.setZ(i, wave1 + wave2 + wave3);
1111
+ }
1112
+ positions.needsUpdate = true;
1113
+ }
1114
+
459
1115
  controls.update();
460
- renderer.render(scene, camera);
1116
+
1117
+ // Use composer for bloom effect if available, otherwise standard renderer
1118
+ if (composer) {
1119
+ composer.render();
1120
+ } else {
1121
+ renderer.render(scene, camera);
1122
+ }
461
1123
  }
462
1124
 
463
1125
  function onWindowResize() {
@@ -467,14 +1129,17 @@ function onWindowResize() {
467
1129
  camera.updateProjectionMatrix();
468
1130
  renderer.setSize(width, height);
469
1131
 
470
- // Adjust camera position for better mobile view
1132
+ // Resize composer for bloom effect
1133
+ if (composer) {
1134
+ composer.setSize(width, height);
1135
+ }
1136
+
471
1137
  if (width < 768) {
472
- // On mobile, position camera slightly higher and further back
473
- camera.position.y = Math.max(camera.position.y, 35);
474
- camera.position.z = Math.max(camera.position.z, 45);
475
- controls.minDistance = 30; // Prevent zooming too close on mobile
1138
+ camera.position.y = Math.max(camera.position.y, 40);
1139
+ camera.position.z = Math.max(camera.position.z, 50);
1140
+ controls.minDistance = 35;
476
1141
  } else {
477
- controls.minDistance = 20;
1142
+ controls.minDistance = 25;
478
1143
  }
479
1144
  }
480
1145
 
@@ -609,9 +1274,9 @@ function updateUI() {
609
1274
  });
610
1275
  }
611
1276
 
612
- // Update house dialog if it's open
1277
+ // Update desk dialog if it's open
613
1278
  if (document.getElementById('house-dialog').classList.contains('active')) {
614
- updateHouseDialogContent();
1279
+ updateDeskDialogContent();
615
1280
  }
616
1281
  }
617
1282
 
@@ -672,23 +1337,23 @@ async function sendMessage() {
672
1337
  }
673
1338
  }
674
1339
 
675
- // House click handler
676
- function onHouseClick(event) {
677
- handleHouseInteraction(event.clientX, event.clientY);
1340
+ // Desk click handler
1341
+ function onDeskClick(event) {
1342
+ handleDeskInteraction(event.clientX, event.clientY);
678
1343
  }
679
1344
 
680
1345
  // Touch handlers for mobile
681
1346
  let touchStartX = 0;
682
1347
  let touchStartY = 0;
683
1348
 
684
- function onHouseTouchStart(event) {
1349
+ function onDeskTouchStart(event) {
685
1350
  if (event.touches.length === 1) {
686
1351
  touchStartX = event.touches[0].clientX;
687
1352
  touchStartY = event.touches[0].clientY;
688
1353
  }
689
1354
  }
690
1355
 
691
- function onHouseTouchEnd(event) {
1356
+ function onDeskTouchEnd(event) {
692
1357
  if (event.changedTouches.length === 1) {
693
1358
  const touchEndX = event.changedTouches[0].clientX;
694
1359
  const touchEndY = event.changedTouches[0].clientY;
@@ -701,13 +1366,13 @@ function onHouseTouchEnd(event) {
701
1366
 
702
1367
  // Only trigger if touch didn't move much (tap vs swipe)
703
1368
  if (moveDistance < 20) {
704
- handleHouseInteraction(touchEndX, touchEndY);
1369
+ handleDeskInteraction(touchEndX, touchEndY);
705
1370
  }
706
1371
  }
707
1372
  }
708
1373
 
709
- // Common house interaction handler
710
- function handleHouseInteraction(clientX, clientY) {
1374
+ // Common desk interaction handler
1375
+ function handleDeskInteraction(clientX, clientY) {
711
1376
  // Calculate normalized device coordinates
712
1377
  const rect = renderer.domElement.getBoundingClientRect();
713
1378
  const x = ((clientX - rect.left) / rect.width) * 2 - 1;
@@ -718,34 +1383,34 @@ function handleHouseInteraction(clientX, clientY) {
718
1383
 
719
1384
  raycaster.setFromCamera(mouse, camera);
720
1385
 
721
- // Get all house meshes
722
- const houseMeshes = [];
1386
+ // Get all desk meshes
1387
+ const deskMeshes = [];
723
1388
  agentMeshes.forEach((group, name) => {
724
1389
  group.children.forEach(child => {
725
1390
  if (child.isMesh && !child.userData.isBubble && !child.userData.isCup) {
726
1391
  child.userData.agentName = name;
727
- houseMeshes.push(child);
1392
+ deskMeshes.push(child);
728
1393
  }
729
1394
  });
730
1395
  });
731
1396
 
732
- const intersects = raycaster.intersectObjects(houseMeshes);
1397
+ const intersects = raycaster.intersectObjects(deskMeshes);
733
1398
 
734
1399
  if (intersects.length > 0) {
735
1400
  const agentName = intersects[0].object.userData.agentName;
736
1401
  if (agentName) {
737
- showHouseDialog(agentName);
1402
+ showDeskDialog(agentName);
738
1403
  }
739
1404
  }
740
1405
  }
741
1406
 
742
- // Global variable to track current agent for house dialog
743
- let currentHouseAgent = null;
1407
+ // Global variable to track current agent for desk dialog
1408
+ let currentDeskAgent = null;
744
1409
  let currentTab = 'received';
745
1410
 
746
1411
  // Show dialog with messages for a specific agent
747
- async function showHouseDialog(agentName) {
748
- currentHouseAgent = agentName.toLowerCase();
1412
+ async function showDeskDialog(agentName) {
1413
+ currentDeskAgent = agentName.toLowerCase();
749
1414
  currentTab = 'received'; // Default to received tab
750
1415
 
751
1416
  const dialog = document.getElementById('house-dialog');
@@ -785,35 +1450,35 @@ window.switchTab = function(tab) {
785
1450
  document.getElementById('tab-sent').classList.toggle('active', tab === 'sent');
786
1451
 
787
1452
  // Update content
788
- updateHouseDialogContent();
1453
+ updateDeskDialogContent();
789
1454
  };
790
1455
 
791
- function updateHouseDialogContent() {
1456
+ function updateDeskDialogContent() {
792
1457
  const content = document.getElementById('house-dialog-content');
793
1458
 
794
- if (!currentHouseAgent) return;
1459
+ if (!currentDeskAgent) return;
795
1460
 
796
1461
  // Filter messages based on current tab - FROM THE AGENT'S PERSPECTIVE
797
1462
  let filteredMessages;
798
1463
  if (currentTab === 'received') {
799
1464
  // Messages RECEIVED BY the agent (sent TO the agent)
800
1465
  filteredMessages = allMessages.filter(m =>
801
- m.recipient.toLowerCase() === currentHouseAgent
1466
+ m.recipient.toLowerCase() === currentDeskAgent
802
1467
  );
803
1468
  } else {
804
1469
  // Messages SENT BY the agent
805
1470
  filteredMessages = allMessages.filter(m =>
806
- m.sender.toLowerCase() === currentHouseAgent
1471
+ m.sender.toLowerCase() === currentDeskAgent
807
1472
  );
808
1473
  }
809
1474
 
810
1475
  // Update count badges
811
1476
  const receivedCount = allMessages.filter(m =>
812
- m.recipient.toLowerCase() === currentHouseAgent
1477
+ m.recipient.toLowerCase() === currentDeskAgent
813
1478
  ).length;
814
1479
 
815
1480
  const sentCount = allMessages.filter(m =>
816
- m.sender.toLowerCase() === currentHouseAgent
1481
+ m.sender.toLowerCase() === currentDeskAgent
817
1482
  ).length;
818
1483
 
819
1484
  const receivedBadge = document.getElementById('received-count');
@@ -843,35 +1508,30 @@ function updateHouseDialogContent() {
843
1508
  }
844
1509
  }
845
1510
 
846
- window.closeHouseDialog = function() {
1511
+ window.closeDeskDialog = function() {
847
1512
  document.getElementById('house-dialog').classList.remove('active');
848
1513
  };
849
1514
 
850
- // Update house labels to show unread indicators and tool names
851
- function updateHouseLabels() {
1515
+ // Update desk labels to show unread indicators and tool names
1516
+ function updateDeskLabels() {
852
1517
  agentMeshes.forEach((group, name) => {
853
- // Check for unread messages SENT TO this agent (messages they haven't read)
854
1518
  const unreadCount = allMessages.filter(m =>
855
1519
  m.recipient.toLowerCase() === name.toLowerCase() &&
856
1520
  m.sender.toLowerCase() === config.user.toLowerCase() &&
857
1521
  !m.read
858
1522
  ).length;
859
1523
 
860
- // Also check if this agent has sent unread messages TO user
861
1524
  const unreadFromAgent = allMessages.filter(m =>
862
1525
  m.sender.toLowerCase() === name.toLowerCase() &&
863
1526
  m.recipient.toLowerCase() === config.user.toLowerCase() &&
864
1527
  !m.read
865
1528
  ).length;
866
1529
 
867
- // Get tool name for this agent
868
1530
  const avatarState = avatarStates[name.toLowerCase()];
869
1531
  const toolName = avatarState?.tool_name || null;
870
1532
 
871
- // Find the sprite label
872
1533
  const sprite = group.children.find(c => c.isSprite);
873
1534
  if (sprite) {
874
- // Update the canvas texture - high DPI for crisp text
875
1535
  const canvas = document.createElement('canvas');
876
1536
  const context = canvas.getContext('2d');
877
1537
  const scale = 2;
@@ -879,40 +1539,43 @@ function updateHouseLabels() {
879
1539
  canvas.height = toolName ? 160 : 128;
880
1540
  context.scale(scale, scale);
881
1541
 
882
- // Background - change color if there are unread messages
1542
+ // Background - themed colors for state
883
1543
  if (unreadFromAgent > 0) {
884
- // Red background for unread messages from agent
885
- context.fillStyle = 'rgba(220, 53, 69, 0.9)';
1544
+ context.fillStyle = 'rgba(220, 80, 80, 0.85)';
886
1545
  } else if (unreadCount > 0) {
887
- // Blue background for messages sent but not read
888
- context.fillStyle = 'rgba(0, 123, 255, 0.9)';
1546
+ context.fillStyle = 'rgba(59, 130, 180, 0.85)';
889
1547
  } else {
890
- // Default black background
891
- context.fillStyle = 'rgba(0, 0, 0, 0.7)';
1548
+ context.fillStyle = 'rgba(20, 60, 60, 0.85)';
892
1549
  }
893
1550
  context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
894
1551
  context.fill();
895
1552
 
896
- // Name
897
- context.font = 'bold 24px Arial';
898
- context.fillStyle = 'white';
1553
+ // Border
1554
+ context.strokeStyle = unreadFromAgent > 0
1555
+ ? 'rgba(255, 120, 120, 0.6)'
1556
+ : unreadCount > 0
1557
+ ? 'rgba(100, 180, 255, 0.6)'
1558
+ : 'rgba(79, 209, 197, 0.3)';
1559
+ context.lineWidth = 1;
1560
+ context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
1561
+ context.stroke();
1562
+
1563
+ context.font = 'bold 22px Arial';
1564
+ context.fillStyle = '#e0f5f0';
899
1565
  context.textAlign = 'center';
900
1566
  context.textBaseline = 'middle';
901
1567
 
902
1568
  if (unreadFromAgent > 0) {
903
- // Show name with unread indicator from agent
904
- context.fillText(`${name} 🔴 ${unreadFromAgent}`, 175, 24);
1569
+ context.fillText(`${name} ${unreadFromAgent}`, 175, 24);
905
1570
  } else if (unreadCount > 0) {
906
- // Show name with sent-but-unread count
907
- context.fillText(`${name} 📤 ${unreadCount}`, 175, 24);
1571
+ context.fillText(`${name} ${unreadCount}`, 175, 24);
908
1572
  } else {
909
1573
  context.fillText(name, 175, 24);
910
1574
  }
911
1575
 
912
- // Tool name text (if available)
913
1576
  if (toolName) {
914
- context.font = 'italic 16px Arial';
915
- context.fillStyle = '#FFD700'; // Gold color for tool name
1577
+ context.font = 'italic 14px Arial';
1578
+ context.fillStyle = '#4fd1c5';
916
1579
  context.fillText(toolName, 175, 56);
917
1580
  }
918
1581
 
@@ -922,9 +1585,8 @@ function updateHouseLabels() {
922
1585
  sprite.material.map = texture;
923
1586
  sprite.material.needsUpdate = true;
924
1587
 
925
- // Update position and scale based on whether tool name is shown
926
- sprite.position.set(0, toolName ? 8.5 : 8, 0);
927
- sprite.scale.set(8, toolName ? 2.5 : 2, 1);
1588
+ sprite.position.set(0, 6.5, 2);
1589
+ sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
928
1590
  }
929
1591
  });
930
1592
  }