let-them-talk 5.3.0 → 5.4.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 (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
package/office/player.js CHANGED
@@ -2,6 +2,7 @@ import * as THREE from 'three';
2
2
  import { S } from './state.js';
3
3
  import { createCharacter } from './character.js';
4
4
  import { resolveAppearance } from './appearance.js';
5
+ import { galleryNavigate } from './gallery.js';
5
6
  // ============================================================
6
7
  // PLAYER AVATAR — Walk around the 3D world as a character
7
8
  // Not an agent — no MCP, no messages — just visual presence
@@ -16,6 +17,8 @@ var PLAYER_RADIUS = 0.35; // collision radius
16
17
  var _tmpForward = new THREE.Vector3();
17
18
  var _tmpRight = new THREE.Vector3();
18
19
  var _tmpDir = new THREE.Vector3();
20
+ var _screenRaycaster = new THREE.Raycaster();
21
+ _screenRaycaster.far = 8; // only detect screens within 8 units
19
22
  var _tmpCamTarget = new THREE.Vector3();
20
23
  var _tmpLookAt = new THREE.Vector3();
21
24
  var _tmpTargetPos = new THREE.Vector3();
@@ -25,63 +28,49 @@ var _tmpTargetPos = new THREE.Vector3();
25
28
  // Thin walls as boxes with small thickness
26
29
 
27
30
  function getCampusColliders() {
28
- var W = 50, D = 35;
29
31
  var colliders = [
30
- // Building walls (0.3 thick)
31
- { minX: -W/2 - 0.3, maxX: -W/2, minZ: -D/2, maxZ: D/2 }, // left wall
32
- { minX: W/2, maxX: W/2 + 0.3, minZ: -D/2, maxZ: D/2 }, // right wall
33
- { minX: -W/2, maxX: W/2, minZ: -D/2 - 0.3, maxZ: -D/2 }, // back wall
34
- // Front wall with entrance gap (gap at x: -4 to 4)
35
- { minX: -W/2, maxX: -4, minZ: D/2, maxZ: D/2 + 0.3 },
36
- { minX: 4, maxX: W/2, minZ: D/2, maxZ: D/2 + 0.3 },
37
-
38
- // Manager office walls: group at (12,5), offW=8, offD=7
39
- // Left wall (x=8): z from 1.5 to 8.5
40
- { minX: 7.85, maxX: 8.15, minZ: 1.5, maxZ: 8.5 },
41
- // Right wall (x=16): z from 1.5 to 8.5
42
- { minX: 15.85, maxX: 16.15, minZ: 1.5, maxZ: 8.5 },
43
- // Back wall (z=8.5): x from 8 to 16
44
- { minX: 8, maxX: 16, minZ: 8.35, maxZ: 8.65 },
45
- // Front wall left of door (z=1.5, x from 8 to ~11.4)
46
- { minX: 8, maxX: 11.4, minZ: 1.35, maxZ: 1.65 },
47
- // Front wall right of door (z=1.5, x from ~12.6 to 16)
48
- { minX: 12.6, maxX: 16, minZ: 1.35, maxZ: 1.65 },
32
+ // Building walls (0.4 thick) — campus 90W x 60D (X: -45 to +45, Z: -30 to +30)
33
+ { minX: -45.4, maxX: -45, minZ: -30, maxZ: 30 }, // left wall
34
+ { minX: 45, maxX: 45.4, minZ: -30, maxZ: 30 }, // right wall (solid)
35
+ { minX: -45, maxX: 45, minZ: -30.4, maxZ: -30 }, // back wall
36
+ // Front wall with 6-unit entrance gap at center (X: -3 to +3)
37
+ { minX: -45, maxX: -3, minZ: 30, maxZ: 30.4 },
38
+ { minX: 3, maxX: 45, minZ: 30, maxZ: 30.4 },
39
+
40
+ // Manager office walls: center (30, 10), size 10x10
41
+ // Left wall (X=25): Z from 5 to 15
42
+ { minX: 24.8, maxX: 25.2, minZ: 5, maxZ: 15 },
43
+ // Right wall (X=35): Z from 5 to 15
44
+ { minX: 34.8, maxX: 35.2, minZ: 5, maxZ: 15 },
45
+ // Back wall (Z=15): X from 25 to 35
46
+ { minX: 25, maxX: 35, minZ: 14.8, maxZ: 15.2 },
47
+ // Front wall left of door (Z=5, X from 25 to 29.25)
48
+ { minX: 25, maxX: 29.25, minZ: 4.85, maxZ: 5.15 },
49
+ // Front wall right of door (Z=5, X from 30.75 to 35)
50
+ { minX: 30.75, maxX: 35, minZ: 4.85, maxZ: 5.15 },
49
51
  // Door collider (only active when closed) — handled dynamically below
50
52
 
51
- // Glass partition between workspace and rec (z=-7, gap at x=-1 to 1)
52
- { minX: -7, maxX: -1, minZ: -7.15, maxZ: -6.85 },
53
- { minX: 1, maxX: 7, minZ: -7.15, maxZ: -6.85 },
53
+ // Reception desk (ground floor)
54
+ { minX: -2.5, maxX: 2.5, minZ: 24, maxZ: 26, floor: 'ground' },
55
+ // Water feature (ground floor)
56
+ { minX: -2.5, maxX: 2.5, minZ: 20, maxZ: 22, floor: 'ground' },
54
57
 
55
- // Glass partition designer/main (x=-8, gap at z=2 to 4)
56
- { minX: -8.15, maxX: -7.85, minZ: -5, maxZ: 2 },
57
- { minX: -8.15, maxX: -7.85, minZ: 4, maxZ: 7 },
58
+ // Bar area counter (ground floor, center -28,-18, approx)
59
+ { minX: -34, maxX: -22, minZ: -22, maxZ: -20, floor: 'ground' },
58
60
 
59
- // Reception desk (ground floor)
60
- { minX: -2.2, maxX: 2.2, minZ: 13.5, maxZ: 15, floor: 'ground' },
61
- // Reception logo wall
62
- { minX: -3, maxX: 3, minZ: 15.5, maxZ: 16, floor: 'ground' },
63
- // Water feature
64
- { minX: -1.5, maxX: 1.5, minZ: 9.5, maxZ: 10.5, floor: 'ground' },
65
-
66
- // Bar counter (ground floor)
67
- { minX: -17, maxX: -11, minZ: -12.7, maxZ: -11.3, floor: 'ground' },
68
-
69
- // Pool table (ground floor)
70
- { minX: -3.3, maxX: -0.7, minZ: -12.7, maxZ: -11.3, floor: 'ground' },
71
- // Foosball (ground floor)
72
- { minX: 1.8, maxX: 3.2, minZ: -12.4, maxZ: -11.6, floor: 'ground' },
73
-
74
- // Mezzanine support columns (thin cylinders, approximate as small boxes)
75
- // Columns at x: -18,-9,0,9,18 z: -CAMPUS_D/2 + MEZZ_DEPTH = -17.5+12 = -5.5
61
+ // Rec Center (ground floor, center 0,-18)
62
+ { minX: -5, maxX: 5, minZ: -22, maxZ: -20, floor: 'ground' },
63
+
64
+ // Gym equipment (ground floor, center 22,-18)
65
+ { minX: 16, maxX: 28, minZ: -22, maxZ: -20, floor: 'ground' },
76
66
  ];
77
67
 
78
- // Desk colliders (gaming desks)
68
+ // Desk colliders (20 regular desks)
79
69
  var CAMPUS_DESKS = [
80
- { x: -4.5, z: 2 }, { x: -1.5, z: 2 }, { x: 1.5, z: 2 }, { x: 4.5, z: 2 },
81
- { x: -4.5, z: -1 }, { x: -1.5, z: -1 }, { x: 1.5, z: -1 }, { x: 4.5, z: -1 },
82
- { x: -4.5, z: -4 }, { x: -1.5, z: -4 }, { x: 1.5, z: -4 }, { x: 4.5, z: -4 },
83
- { x: -14, z: 1 }, { x: -11, z: 1 },
84
- { x: -14, z: -2 }, { x: -11, z: -2 },
70
+ { x: -8, z: 6 }, { x: -4, z: 6 }, { x: 0, z: 6 }, { x: 4, z: 6 }, { x: 8, z: 6 },
71
+ { x: -8, z: 10 }, { x: -4, z: 10 }, { x: 0, z: 10 }, { x: 4, z: 10 }, { x: 8, z: 10 },
72
+ { x: -8, z: 14 }, { x: -4, z: 14 }, { x: 0, z: 14 }, { x: 4, z: 14 }, { x: 8, z: 14 },
73
+ { x: -8, z: 18 }, { x: -4, z: 18 }, { x: 0, z: 18 }, { x: 4, z: 18 }, { x: 8, z: 18 },
85
74
  ];
86
75
  CAMPUS_DESKS.forEach(function(d) {
87
76
  // Desk body only — chair area excluded so player can stand up without getting stuck
@@ -89,13 +78,23 @@ function getCampusColliders() {
89
78
  });
90
79
 
91
80
  // Manager's desk inside office — chair side excluded (ground floor)
92
- colliders.push({ minX: 10.5, maxX: 14.5, minZ: 5.5, maxZ: 6.8, floor: 'ground' });
93
-
94
- // Bar counter (ground floor)
95
- // Pool table, foosball (ground floor) — already added above without tag, let me not duplicate
96
-
97
- // Reception area (ground floor)
98
- // Already added above
81
+ colliders.push({ minX: 27, maxX: 33, minZ: 8.5, maxZ: 10, floor: 'ground' });
82
+
83
+ // Gallery Wing colliders (inside campus, center at X:-36, Z:10, 14W x 12D)
84
+ // West wall (X=-43)
85
+ colliders.push({ minX: -43.2, maxX: -42.8, minZ: 4, maxZ: 16 });
86
+ // South wall (Z=4)
87
+ colliders.push({ minX: -43, maxX: -29, minZ: 3.8, maxZ: 4.2 });
88
+ // North wall (Z=16)
89
+ colliders.push({ minX: -43, maxX: -29, minZ: 15.8, maxZ: 16.2 });
90
+ // East wall (X=-29) — glass facade with door gap at center (Z: 8.75 to 11.25)
91
+ colliders.push({ minX: -29.2, maxX: -28.8, minZ: 4, maxZ: 8.75 });
92
+ colliders.push({ minX: -29.2, maxX: -28.8, minZ: 11.25, maxZ: 16 });
93
+ // Gallery furniture
94
+ colliders.push({ minX: -39.4, maxX: -36.6, minZ: 11.5, maxZ: 12.5, floor: 'ground' }); // robot desk
95
+ colliders.push({ minX: -37.5, maxX: -34.5, minZ: 10.2, maxZ: 10.8, floor: 'ground' }); // bench
96
+ colliders.push({ minX: -33.3, maxX: -32.7, minZ: 13.5, maxZ: 14.5, floor: 'ground' }); // pedestal
97
+ colliders.push({ minX: -39.3, maxX: -38.7, minZ: 13.5, maxZ: 14.5, floor: 'ground' }); // pedestal
99
98
 
100
99
  return colliders;
101
100
  }
@@ -146,15 +145,15 @@ function checkCollision(x, z, r) {
146
145
 
147
146
  // Mezzanine railing — blocks walking off the edge (only when on mezzanine)
148
147
  if (onMezzanine && !onStairs) {
149
- // Front edge of mezzanine at z = -5.5 (except staircase gap at x 18.75-21.25)
150
- if (z > -5.7 && z < -5.3 && !(x >= STAIR_X_MIN - 0.5 && x <= STAIR_X_MAX + 0.5)) {
148
+ // Front edge of mezzanine at z = -18 (except staircase gap at x 33.5-36.5)
149
+ if (z > -18.2 && z < -17.8 && !(x >= STAIR_X_MIN - 0.5 && x <= STAIR_X_MAX + 0.5)) {
151
150
  return true;
152
151
  }
153
152
  }
154
153
 
155
154
  // Dynamic: manager door (closed = collider, open = passable)
156
155
  if (S.currentEnv === 'campus' && S._managerDoorLerp < 0.5 && !onMezzanine) {
157
- var doorBox = { minX: 11.4, maxX: 12.6, minZ: 1.35, maxZ: 1.65 };
156
+ var doorBox = { minX: 29.25, maxX: 30.75, minZ: 4.85, maxZ: 5.15 };
158
157
  var dcx = Math.max(doorBox.minX, Math.min(x, doorBox.maxX));
159
158
  var dcz = Math.max(doorBox.minZ, Math.min(z, doorBox.maxZ));
160
159
  var ddx = x - dcx, ddz = z - dcz;
@@ -193,12 +192,12 @@ function resolveMovement(oldX, oldZ, newX, newZ, r) {
193
192
  }
194
193
 
195
194
  // ==================== HEIGHT SYSTEM ====================
196
- // Staircase: x 18.75-21.25, z -3.5 (bottom, y=0) to -9.5 (top, y=3.2)
197
- // Mezzanine: y=3.2, z from -17.5 to -5.5, full width
198
- var STAIR_X_MIN = 18.75, STAIR_X_MAX = 21.25;
199
- var STAIR_Z_BOTTOM = -3.5, STAIR_Z_TOP = -9.5;
195
+ // Staircase: switchback at X=35, X range 33.5 to 36.5, Z -14 (bottom) to -22 (top, y=3.2)
196
+ // Mezzanine: y=3.2, z from -30 to -18, full width (X: -43 to +43)
197
+ var STAIR_X_MIN = 33.5, STAIR_X_MAX = 36.5;
198
+ var STAIR_Z_BOTTOM = -14, STAIR_Z_TOP = -22;
200
199
  var MEZZ_HEIGHT = 3.2;
201
- var MEZZ_Z_BACK = -17.5, MEZZ_Z_FRONT = -5.5;
200
+ var MEZZ_Z_BACK = -30, MEZZ_Z_FRONT = -18;
202
201
 
203
202
  function getGroundHeight(x, z, currentY) {
204
203
  if (S.currentEnv !== 'campus') return 0;
@@ -237,7 +236,7 @@ export function spawnPlayer() {
237
236
  } catch (e) {}
238
237
 
239
238
  var parts = createCharacter('Player', appearance);
240
- parts.group.position.set(0, 0, 12); // spawn at lobby
239
+ parts.group.position.set(0, 0, 4); // spawn at main corridor, facing workspace
241
240
 
242
241
  // Remove typing dots and task indicator (player doesn't need them)
243
242
  parts.typingLabel.visible = false;
@@ -253,7 +252,7 @@ export function spawnPlayer() {
253
252
 
254
253
  S._player = {
255
254
  parts: parts,
256
- pos: { x: 0, y: 0, z: 12 },
255
+ pos: { x: 0, y: 0, z: 4 },
257
256
  facing: 0, // radians, 0 = +z direction
258
257
  velocity: { x: 0, z: 0 },
259
258
  isMoving: false,
@@ -265,6 +264,8 @@ export function spawnPlayer() {
265
264
  _jumpVel: 0,
266
265
  _jumpY: 0,
267
266
  _landSquash: 0,
267
+ firstPerson: false, // V key toggle
268
+ _vPressed: false,
268
269
  };
269
270
 
270
271
  // Disable spectator camera movement but keep key/mouse tracking alive
@@ -288,6 +289,14 @@ export function despawnPlayer() {
288
289
  if (S._player._sitPrompt.parentElement) S._player._sitPrompt.remove();
289
290
  S._player._sitPrompt = null;
290
291
  }
292
+ if (S._player._fpPrompt) {
293
+ if (S._player._fpPrompt.parentElement) S._player._fpPrompt.remove();
294
+ S._player._fpPrompt = null;
295
+ }
296
+ if (S._player._screenPrompt) {
297
+ if (S._player._screenPrompt.parentElement) S._player._screenPrompt.remove();
298
+ S._player._screenPrompt = null;
299
+ }
291
300
  S.scene.remove(S._player.parts.group);
292
301
  S._player.parts.group.traverse(function(child) {
293
302
  if (child.geometry) child.geometry.dispose();
@@ -298,6 +307,9 @@ export function despawnPlayer() {
298
307
  });
299
308
  S._player = null;
300
309
 
310
+ // Release pointer lock if active
311
+ if (document.pointerLockElement) document.exitPointerLock();
312
+
301
313
  // Re-enable spectator camera
302
314
  if (S.controls) {
303
315
  S.controls.enabled = true;
@@ -562,12 +574,79 @@ export function updatePlayer(dt, time, keys) {
562
574
  player.parts.rightLowerLeg.rotation.x = 1.5 * sl;
563
575
  player.parts.leftForearm.rotation.x = -0.4 * sl;
564
576
  player.parts.rightForearm.rotation.x = -0.4 * sl;
565
- player.parts.group.position.y = player.pos.y + sl * 0.14;
577
+ player.parts.group.position.y = player.pos.y + sl * 0.08;
566
578
  }
567
579
 
568
- // --- Third-person camera follow (or desk POV when sitting) ---
580
+ // --- E/Q: navigate gallery screens via line trace from camera ---
581
+ // Runs BEFORE sit system — if looking at a screen, E navigates instead of sitting
582
+ var _lookingAtScreen = null;
583
+ if (S._galleryScreenMeshes && S._galleryScreenMeshes.length > 0 && !player.sitting) {
584
+ _screenRaycaster.setFromCamera({ x: 0, y: 0 }, S.camera);
585
+ var screenHits = _screenRaycaster.intersectObjects(S._galleryScreenMeshes, false);
586
+ _lookingAtScreen = screenHits.length > 0 ? screenHits[0].object.userData._galleryScreen : null;
587
+
588
+ if (_lookingAtScreen) {
589
+ if (!player._screenPrompt) {
590
+ player._screenPrompt = document.createElement('div');
591
+ player._screenPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#06b6d4;padding:8px 16px;border-radius:8px;font-size:13px;z-index:1000;pointer-events:none;border:1px solid #06b6d4;';
592
+ document.body.appendChild(player._screenPrompt);
593
+ }
594
+ player._screenPrompt.textContent = _lookingAtScreen.toUpperCase() + ' — E: Next Q: Prev';
595
+ player._screenPrompt.style.display = 'block';
596
+
597
+ // E = next, Q = previous (with cooldown to prevent skipping)
598
+ var now = performance.now();
599
+ var galleryCooldown = 300; // ms between navigations
600
+ if (!player._galleryLastNav) player._galleryLastNav = 0;
601
+
602
+ if (keys['KeyE'] && !player._screenEPressed && now - player._galleryLastNav > galleryCooldown) {
603
+ player._screenEPressed = true;
604
+ player._galleryLastNav = now;
605
+ galleryNavigate(_lookingAtScreen, 1);
606
+ }
607
+ if (!keys['KeyE']) player._screenEPressed = false;
608
+
609
+ if (keys['KeyQ'] && !player._screenQPressed && now - player._galleryLastNav > galleryCooldown) {
610
+ player._screenQPressed = true;
611
+ player._galleryLastNav = now;
612
+ galleryNavigate(_lookingAtScreen, -1);
613
+ }
614
+ if (!keys['KeyQ']) player._screenQPressed = false;
615
+ } else {
616
+ if (player._screenPrompt) player._screenPrompt.style.display = 'none';
617
+ player._screenEPressed = false;
618
+ player._screenQPressed = false;
619
+ }
620
+ }
621
+
622
+ // --- V key: toggle first-person / third-person ---
623
+ if (keys['KeyV'] && !player._vPressed) {
624
+ player._vPressed = true;
625
+ player.firstPerson = !player.firstPerson;
626
+ // Show/hide character mesh in first-person
627
+ player.parts.group.traverse(function(child) {
628
+ if (child.isMesh) child.visible = !player.firstPerson;
629
+ });
630
+ // Show FP indicator
631
+ if (!player._fpPrompt) {
632
+ player._fpPrompt = document.createElement('div');
633
+ player._fpPrompt.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.6);color:#58a6ff;padding:6px 14px;border-radius:6px;font-size:12px;z-index:1000;pointer-events:none;transition:opacity 0.5s;';
634
+ document.body.appendChild(player._fpPrompt);
635
+ }
636
+ player._fpPrompt.textContent = player.firstPerson ? 'First Person (V to switch)' : 'Third Person (V to switch)';
637
+ player._fpPrompt.style.opacity = '1';
638
+ clearTimeout(player._fpFadeTimer);
639
+ player._fpFadeTimer = setTimeout(function() {
640
+ if (player._fpPrompt) player._fpPrompt.style.opacity = '0';
641
+ }, 2000);
642
+ }
643
+ if (!keys['KeyV']) player._vPressed = false;
644
+
645
+ // --- Camera follow ---
569
646
  if (player.sitting) {
570
647
  updatePlayerCameraDesk(dt);
648
+ } else if (player.firstPerson) {
649
+ updatePlayerCameraFP(dt);
571
650
  } else {
572
651
  updatePlayerCamera(dt);
573
652
  }
@@ -624,6 +703,37 @@ function updatePlayerCameraDesk(dt) {
624
703
  S.camera.lookAt(_tmpLookAt);
625
704
  }
626
705
 
706
+ // First-person camera — at head height, looks where mouse points
707
+ function updatePlayerCameraFP(dt) {
708
+ var player = S._player;
709
+ if (!player) return;
710
+
711
+ var yaw = 0, pitch = 0;
712
+ if (S.controls && S.controls._euler) {
713
+ yaw = S.controls._euler.y;
714
+ pitch = -S.controls._euler.x; // flip: mouse up = look up
715
+ }
716
+
717
+ // Clamp pitch to prevent flipping
718
+ pitch = Math.max(-1.2, Math.min(1.2, pitch));
719
+
720
+ // Camera at standing eye level (higher than chibi head for immersive FP view)
721
+ var headY = player.pos.y + 1.65;
722
+ S.camera.position.set(player.pos.x, headY, player.pos.z);
723
+
724
+ // Look direction from yaw/pitch
725
+ var lookDist = 5;
726
+ var lookX = player.pos.x - Math.sin(yaw) * Math.cos(pitch) * lookDist;
727
+ var lookZ = player.pos.z - Math.cos(yaw) * Math.cos(pitch) * lookDist;
728
+ var lookY = headY - Math.sin(pitch) * lookDist;
729
+
730
+ _tmpLookAt.set(lookX, lookY, lookZ);
731
+ S.camera.lookAt(_tmpLookAt);
732
+
733
+ // Also face the character model in the look direction (for shadow/collision)
734
+ player.facing = yaw + Math.PI;
735
+ }
736
+
627
737
  // --- Public API for iframe integration ---
628
738
  export function isPlayerSitting() {
629
739
  return S._player && S._player.sitting;
@@ -0,0 +1,272 @@
1
+ import * as THREE from 'three';
2
+ import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
3
+
4
+ // ============================================================
5
+ // ROBOT CHARACTER — Boxy/mechanical design for API agents
6
+ // Distinct from chibi characters: metallic, glowing eyes, antenna
7
+ // ============================================================
8
+
9
+ export function createRobotCharacter(name, providerColor) {
10
+ var group = new THREE.Group();
11
+ var color = new THREE.Color(providerColor || '#0ea5e9');
12
+ var colorHex = color.getHex();
13
+
14
+ // Materials
15
+ var bodyMat = new THREE.MeshStandardMaterial({ color: 0x2a2d3a, roughness: 0.3, metalness: 0.7 });
16
+ var chromeMat = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.1, metalness: 0.9 });
17
+ var darkMat = new THREE.MeshStandardMaterial({ color: 0x111118, roughness: 0.4, metalness: 0.5 });
18
+ var glowMat = new THREE.MeshStandardMaterial({ color: colorHex, emissive: colorHex, emissiveIntensity: 0.8, roughness: 0.2 });
19
+ var eyeGlowMat = new THREE.MeshStandardMaterial({ color: colorHex, emissive: colorHex, emissiveIntensity: 1.2, roughness: 0.1 });
20
+ var screenMat = new THREE.MeshStandardMaterial({ color: 0x333333, emissive: 0x333333, emissiveIntensity: 0.1, roughness: 0.2 });
21
+
22
+ // Shadow
23
+ var shadow = new THREE.Mesh(
24
+ new THREE.PlaneGeometry(0.6, 0.6),
25
+ new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.3, depthWrite: false })
26
+ );
27
+ shadow.rotation.x = -Math.PI / 2;
28
+ shadow.position.y = 0.01;
29
+ shadow.userData.isShadow = true;
30
+ group.add(shadow);
31
+
32
+ // ===== BODY (boxy torso) =====
33
+ var body = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.35, 0.25), bodyMat);
34
+ body.position.y = 0.55;
35
+ body.castShadow = true;
36
+ group.add(body);
37
+
38
+ // Chest panel (darker inset)
39
+ var chestPanel = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.2, 0.01), darkMat);
40
+ chestPanel.position.set(0, 0.55, 0.131);
41
+ group.add(chestPanel);
42
+
43
+ // Status LED on chest
44
+ var statusLed = new THREE.Mesh(new THREE.SphereGeometry(0.02, 8, 8), glowMat);
45
+ statusLed.position.set(0, 0.6, 0.14);
46
+ group.add(statusLed);
47
+
48
+ // Accent stripes on body
49
+ var stripe1 = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.015, 0.01), glowMat);
50
+ stripe1.position.set(0, 0.68, 0.131);
51
+ group.add(stripe1);
52
+ var stripe2 = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.015, 0.01), glowMat);
53
+ stripe2.position.set(0, 0.42, 0.131);
54
+ group.add(stripe2);
55
+
56
+ // ===== LEGS (cylindrical, mechanical) =====
57
+ var legGeo = new THREE.CylinderGeometry(0.06, 0.05, 0.35, 8);
58
+ var leftLeg = new THREE.Mesh(legGeo, chromeMat);
59
+ leftLeg.position.set(-0.1, 0.2, 0);
60
+ leftLeg.castShadow = true;
61
+ group.add(leftLeg);
62
+
63
+ var rightLeg = new THREE.Mesh(legGeo, chromeMat);
64
+ rightLeg.position.set(0.1, 0.2, 0);
65
+ rightLeg.castShadow = true;
66
+ group.add(rightLeg);
67
+
68
+ // Joint rings
69
+ var jointGeo = new THREE.TorusGeometry(0.065, 0.01, 8, 12);
70
+ [-0.1, 0.1].forEach(function(lx) {
71
+ var joint = new THREE.Mesh(jointGeo, glowMat);
72
+ joint.position.set(lx, 0.37, 0);
73
+ joint.rotation.x = Math.PI / 2;
74
+ group.add(joint);
75
+ });
76
+
77
+ // Feet (flat boxes)
78
+ [-0.1, 0.1].forEach(function(fx) {
79
+ var foot = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.03, 0.12), darkMat);
80
+ foot.position.set(fx, 0.02, 0.01);
81
+ group.add(foot);
82
+ });
83
+
84
+ // ===== ARMS (jointed, mechanical) =====
85
+ var armGeo = new THREE.CylinderGeometry(0.04, 0.035, 0.25, 8);
86
+
87
+ var leftArm = new THREE.Mesh(armGeo, chromeMat);
88
+ leftArm.position.set(-0.28, 0.55, 0);
89
+ leftArm.rotation.z = 0.15;
90
+ leftArm.castShadow = true;
91
+ group.add(leftArm);
92
+
93
+ var rightArm = new THREE.Mesh(armGeo, chromeMat);
94
+ rightArm.position.set(0.28, 0.55, 0);
95
+ rightArm.rotation.z = -0.15;
96
+ rightArm.castShadow = true;
97
+ group.add(rightArm);
98
+
99
+ // Hands (sphere clamps)
100
+ var handGeo = new THREE.SphereGeometry(0.04, 8, 8);
101
+ var leftHand = new THREE.Mesh(handGeo, bodyMat);
102
+ leftHand.position.set(-0.3, 0.42, 0);
103
+ group.add(leftHand);
104
+
105
+ var rightHand = new THREE.Mesh(handGeo, bodyMat);
106
+ rightHand.position.set(0.3, 0.42, 0);
107
+ group.add(rightHand);
108
+
109
+ // ===== HEAD (boxy with rounded edges) =====
110
+ var headGeo = new THREE.BoxGeometry(0.3, 0.25, 0.22);
111
+ var head = new THREE.Mesh(headGeo, bodyMat);
112
+ head.position.y = 0.87;
113
+ head.castShadow = true;
114
+ group.add(head);
115
+
116
+ // Face plate (visor)
117
+ var visor = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.12, 0.01), darkMat);
118
+ visor.position.set(0, 0.88, 0.115);
119
+ group.add(visor);
120
+
121
+ // Glowing eyes (2 dots on visor)
122
+ var eyeGeo = new THREE.SphereGeometry(0.025, 8, 8);
123
+ var leftEye = new THREE.Mesh(eyeGeo, eyeGlowMat);
124
+ leftEye.position.set(-0.06, 0.89, 0.12);
125
+ group.add(leftEye);
126
+
127
+ var rightEye = new THREE.Mesh(eyeGeo, eyeGlowMat);
128
+ rightEye.position.set(0.06, 0.89, 0.12);
129
+ group.add(rightEye);
130
+
131
+ // Mouth (small LED strip)
132
+ var mouthStrip = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.015, 0.01), glowMat);
133
+ mouthStrip.position.set(0, 0.84, 0.12);
134
+ group.add(mouthStrip);
135
+
136
+ // ===== ANTENNA (satellite dish style) =====
137
+ var antennaBase = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.12, 6), chromeMat);
138
+ antennaBase.position.set(0.08, 1.06, 0);
139
+ group.add(antennaBase);
140
+
141
+ var antennaDish = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2), glowMat);
142
+ antennaDish.position.set(0.08, 1.12, 0);
143
+ antennaDish.rotation.x = Math.PI;
144
+ group.add(antennaDish);
145
+
146
+ var antennaTip = new THREE.Mesh(new THREE.SphereGeometry(0.012, 6, 6), eyeGlowMat);
147
+ antennaTip.position.set(0.08, 1.14, 0);
148
+ group.add(antennaTip);
149
+
150
+ // ===== LABELS =====
151
+ // Name label
152
+ var labelDiv = document.createElement('div');
153
+ labelDiv.className = 'office3d-label';
154
+ labelDiv.innerHTML = '<span class="office3d-label-dot" style="background:#4ade80"></span><span class="office3d-label-name">' + name + '</span><span class="office3d-robot-badge" style="background:' + providerColor + ';color:#fff;font-size:7px;padding:1px 4px;border-radius:3px;margin-left:4px">BOT</span>';
155
+ labelDiv.style.cssText = 'display:flex;align-items:center;gap:4px;background:rgba(0,0,0,0.65);padding:3px 8px;border-radius:6px;font-size:10px;font-family:monospace;color:#c9d1d9;white-space:nowrap;pointer-events:none;border:1px solid ' + providerColor + '44;';
156
+ var label = new CSS2DObject(labelDiv);
157
+ label.position.set(0, 1.35, 0);
158
+ group.add(label);
159
+
160
+ // Speech bubble
161
+ var bubbleDiv = document.createElement('div');
162
+ bubbleDiv.className = 'office3d-bubble';
163
+ bubbleDiv.style.cssText = 'display:none;opacity:0;background:rgba(10,10,20,0.85);color:#e0e0e0;padding:6px 12px;border-radius:10px;font-size:11px;max-width:220px;white-space:pre-wrap;word-wrap:break-word;font-family:monospace;pointer-events:none;border:1px solid ' + providerColor + '66;box-shadow:0 0 10px ' + providerColor + '33;';
164
+ var bubble = new CSS2DObject(bubbleDiv);
165
+ bubble.position.set(0, 1.55, 0);
166
+ group.add(bubble);
167
+
168
+ // Task indicator
169
+ var taskDiv = document.createElement('div');
170
+ taskDiv.className = 'office3d-task-indicator working';
171
+ var taskLabel = new CSS2DObject(taskDiv);
172
+ taskLabel.position.set(0, 1.7, 0);
173
+ taskLabel.visible = false;
174
+ group.add(taskLabel);
175
+
176
+ // Typing dots
177
+ var typingDiv = document.createElement('div');
178
+ typingDiv.className = 'office3d-typing';
179
+ typingDiv.innerHTML = '<span class="office3d-typing-dot"></span><span class="office3d-typing-dot"></span><span class="office3d-typing-dot"></span>';
180
+ var typingLabel = new CSS2DObject(typingDiv);
181
+ typingLabel.position.set(0, 1.65, 0);
182
+ typingLabel.visible = false;
183
+ group.add(typingLabel);
184
+
185
+ // ZZZ sprites
186
+ var zzzObjects = [];
187
+ for (var zi = 0; zi < 3; zi++) {
188
+ var zDiv = document.createElement('div');
189
+ zDiv.textContent = 'Z';
190
+ zDiv.style.cssText = 'color:#facc15;font-size:' + (10 + zi * 4) + 'px;font-weight:bold;font-family:monospace;opacity:0;pointer-events:none;';
191
+ var zObj = new CSS2DObject(zDiv);
192
+ zObj.position.set(0.2 + zi * 0.1, 1.2 + zi * 0.15, 0);
193
+ group.add(zObj);
194
+ zzzObjects.push({ obj: zObj, div: zDiv });
195
+ }
196
+
197
+ return {
198
+ group: group,
199
+ body: body,
200
+ head: head,
201
+ leftLeg: leftLeg,
202
+ rightLeg: rightLeg,
203
+ leftArm: leftArm,
204
+ rightArm: rightArm,
205
+ leftHand: leftHand,
206
+ rightHand: rightHand,
207
+ statusLed: statusLed,
208
+ antennaTip: antennaTip,
209
+ leftEye: leftEye,
210
+ rightEye: rightEye,
211
+ mouthStrip: mouthStrip,
212
+ label: label,
213
+ labelDiv: labelDiv,
214
+ bubble: bubble,
215
+ bubbleDiv: bubbleDiv,
216
+ zzzObjects: zzzObjects,
217
+ taskDiv: taskDiv, taskLabel: taskLabel,
218
+ typingDiv: typingDiv, typingLabel: typingLabel,
219
+ bodyMat: bodyMat,
220
+ glowMat: glowMat,
221
+ eyeGlowMat: eyeGlowMat,
222
+ screenMat: screenMat,
223
+ isRobot: true,
224
+ // Compatibility with chibi character parts for animation system
225
+ // These dummy objects prevent crashes in animation.js which expects chibi parts
226
+ leftLowerLeg: new THREE.Object3D(),
227
+ rightLowerLeg: new THREE.Object3D(),
228
+ leftForearm: new THREE.Object3D(),
229
+ rightForearm: new THREE.Object3D(),
230
+ faceSprite: null,
231
+ hairGroup: new THREE.Group(),
232
+ headMat: bodyMat,
233
+ legMat: chromeMat,
234
+ shoeMat: darkMat,
235
+ armMat: chromeMat,
236
+ handMat: bodyMat,
237
+ outfitGroup: null,
238
+ };
239
+ }
240
+
241
+ // Robot-specific animations
242
+ export function updateRobotAnimation(agent, dt, time) {
243
+ if (!agent.parts || !agent.parts.isRobot) return;
244
+
245
+ // Antenna tip pulse
246
+ if (agent.parts.antennaTip) {
247
+ var pulse = (Math.sin(time * 3) + 1) / 2;
248
+ agent.parts.antennaTip.scale.setScalar(0.8 + pulse * 0.4);
249
+ }
250
+
251
+ // Eye glow pulse when processing
252
+ if (agent._processing && agent.parts.leftEye) {
253
+ var eyePulse = (Math.sin(time * 8) + 1) / 2;
254
+ agent.parts.eyeGlowMat.emissiveIntensity = 0.6 + eyePulse * 0.8;
255
+ }
256
+
257
+ // Status LED blink when idle
258
+ if (agent.parts.statusLed && !agent._processing) {
259
+ var ledPulse = Math.sin(time * 2) > 0.7 ? 1 : 0.3;
260
+ agent.parts.glowMat.emissiveIntensity = ledPulse;
261
+ }
262
+
263
+ // Idle head bob (subtle)
264
+ if (agent.parts.head && agent.isSitting && !agent._processing) {
265
+ agent.parts.head.rotation.y = Math.sin(time * 0.5) * 0.05;
266
+ }
267
+
268
+ // Processing spin — body slight rotation oscillation
269
+ if (agent._processing && agent.parts.body) {
270
+ agent.parts.body.rotation.y = Math.sin(time * 4) * 0.03;
271
+ }
272
+ }