let-them-talk 3.7.0 → 3.8.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.
@@ -0,0 +1,436 @@
1
+ import * as THREE from 'three';
2
+ import { S } from './state.js';
3
+ import { createCharacter } from './character.js';
4
+ import { resolveAppearance } from './appearance.js';
5
+ // ============================================================
6
+ // PLAYER AVATAR — Walk around the 3D world as a character
7
+ // Not an agent — no MCP, no messages — just visual presence
8
+ // ============================================================
9
+
10
+ var PLAYER_SPEED = 3;
11
+ var CAMERA_OFFSET = new THREE.Vector3(0, 3, 5); // behind + above
12
+ var CAMERA_LOOK_OFFSET = new THREE.Vector3(0, 1.2, 0); // look at chest height
13
+ var PLAYER_RADIUS = 0.35; // collision radius
14
+
15
+ // Reusable Vector3s (avoid per-frame allocation)
16
+ var _tmpForward = new THREE.Vector3();
17
+ var _tmpRight = new THREE.Vector3();
18
+ var _tmpDir = new THREE.Vector3();
19
+ var _tmpCamTarget = new THREE.Vector3();
20
+ var _tmpLookAt = new THREE.Vector3();
21
+ var _tmpTargetPos = new THREE.Vector3();
22
+
23
+ // ==================== COLLISION SYSTEM ====================
24
+ // AABB boxes: { minX, maxX, minZ, maxZ }
25
+ // Thin walls as boxes with small thickness
26
+
27
+ function getCampusColliders() {
28
+ var W = 50, D = 35;
29
+ 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 },
49
+ // Door collider (only active when closed) — handled dynamically below
50
+
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 },
54
+
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
+
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
76
+ ];
77
+
78
+ // Desk colliders (gaming desks)
79
+ 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 },
85
+ ];
86
+ CAMPUS_DESKS.forEach(function(d) {
87
+ // Desk body + chair: box around the desk area (ground floor)
88
+ colliders.push({ minX: d.x - 1.1, maxX: d.x + 1.1, minZ: d.z - 0.5, maxZ: d.z + 1, floor: 'ground' });
89
+ });
90
+
91
+ // Manager's desk inside office (ground floor)
92
+ colliders.push({ minX: 10.5, maxX: 14.5, minZ: 5.5, maxZ: 7.5, 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
99
+
100
+ return colliders;
101
+ }
102
+
103
+ function getModernColliders() {
104
+ // Simple colliders for the old modern/startup office
105
+ var colliders = [
106
+ { minX: -14.3, maxX: -14, minZ: -8, maxZ: 8 }, // left wall
107
+ { minX: 14, maxX: 14.3, minZ: -8, maxZ: 8 }, // right wall
108
+ { minX: -14, maxX: 14, minZ: -8.3, maxZ: -8 }, // back wall
109
+ { minX: -14, maxX: 14, minZ: 8, maxZ: 8.3 }, // front wall
110
+ ];
111
+ return colliders;
112
+ }
113
+
114
+ var _cachedColliders = null;
115
+ var _cachedCollidersEnv = null;
116
+
117
+ function getColliders() {
118
+ if (_cachedCollidersEnv === S.currentEnv && _cachedColliders) return _cachedColliders;
119
+ _cachedCollidersEnv = S.currentEnv;
120
+ if (S.currentEnv === 'campus') {
121
+ _cachedColliders = getCampusColliders();
122
+ } else {
123
+ _cachedColliders = getModernColliders();
124
+ }
125
+ return _cachedColliders;
126
+ }
127
+
128
+ // Check if a circle (player) at (x,z) with radius r collides with any AABB
129
+ function checkCollision(x, z, r) {
130
+ var playerY = S._player ? S._player.pos.y : 0;
131
+ var onMezzanine = playerY > MEZZ_HEIGHT * 0.4;
132
+ var onStairs = S._player && x >= STAIR_X_MIN && x <= STAIR_X_MAX && z <= STAIR_Z_BOTTOM && z >= STAIR_Z_TOP;
133
+
134
+ var colliders = getColliders();
135
+ for (var i = 0; i < colliders.length; i++) {
136
+ var c = colliders[i];
137
+ // Skip ground-floor furniture colliders when on mezzanine (desks etc)
138
+ if (onMezzanine && c.floor === 'ground') continue;
139
+ // Skip mezzanine colliders when on ground floor
140
+ if (!onMezzanine && !onStairs && c.floor === 'upper') continue;
141
+ var cx = Math.max(c.minX, Math.min(x, c.maxX));
142
+ var cz = Math.max(c.minZ, Math.min(z, c.maxZ));
143
+ var dx = x - cx, dz = z - cz;
144
+ if (dx * dx + dz * dz < r * r) return true;
145
+ }
146
+
147
+ // Mezzanine railing — blocks walking off the edge (only when on mezzanine)
148
+ 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)) {
151
+ return true;
152
+ }
153
+ }
154
+
155
+ // Dynamic: manager door (closed = collider, open = passable)
156
+ if (S.currentEnv === 'campus' && S._managerDoorLerp < 0.5 && !onMezzanine) {
157
+ var doorBox = { minX: 11.4, maxX: 12.6, minZ: 1.35, maxZ: 1.65 };
158
+ var dcx = Math.max(doorBox.minX, Math.min(x, doorBox.maxX));
159
+ var dcz = Math.max(doorBox.minZ, Math.min(z, doorBox.maxZ));
160
+ var ddx = x - dcx, ddz = z - dcz;
161
+ if (ddx * ddx + ddz * ddz < r * r) return true;
162
+ }
163
+ return false;
164
+ }
165
+
166
+ // Check collision with agents (circle vs circle)
167
+ function checkAgentCollision(x, z, r) {
168
+ for (var name in S.agents3d) {
169
+ var ag = S.agents3d[name];
170
+ if (!ag.registered || ag.dying) continue;
171
+ var dx = x - ag.pos.x, dz = z - ag.pos.z;
172
+ if (dx * dx + dz * dz < (r + 0.3) * (r + 0.3)) return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ // Resolve movement with sliding collision (try X and Z independently)
178
+ function resolveMovement(oldX, oldZ, newX, newZ, r) {
179
+ // Try full movement
180
+ if (!checkCollision(newX, newZ, r) && !checkAgentCollision(newX, newZ, r)) {
181
+ return { x: newX, z: newZ };
182
+ }
183
+ // Try sliding along X
184
+ if (!checkCollision(newX, oldZ, r) && !checkAgentCollision(newX, oldZ, r)) {
185
+ return { x: newX, z: oldZ };
186
+ }
187
+ // Try sliding along Z
188
+ if (!checkCollision(oldX, newZ, r) && !checkAgentCollision(oldX, newZ, r)) {
189
+ return { x: oldX, z: newZ };
190
+ }
191
+ // Stuck — don't move
192
+ return { x: oldX, z: oldZ };
193
+ }
194
+
195
+ // ==================== 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;
200
+ var MEZZ_HEIGHT = 3.2;
201
+ var MEZZ_Z_BACK = -17.5, MEZZ_Z_FRONT = -5.5;
202
+
203
+ function getGroundHeight(x, z, currentY) {
204
+ if (S.currentEnv !== 'campus') return 0;
205
+
206
+ // On the staircase?
207
+ if (x >= STAIR_X_MIN && x <= STAIR_X_MAX && z <= STAIR_Z_BOTTOM && z >= STAIR_Z_TOP) {
208
+ // Interpolate height: bottom (z=-3.5, y=0) to top (z=-9.5, y=3.2)
209
+ var t = (STAIR_Z_BOTTOM - z) / (STAIR_Z_BOTTOM - STAIR_Z_TOP);
210
+ t = Math.max(0, Math.min(1, t));
211
+ return t * MEZZ_HEIGHT;
212
+ }
213
+
214
+ // On the mezzanine? (must have come from stairs — check if currentY > halfway)
215
+ if (currentY > MEZZ_HEIGHT * 0.4 && z <= MEZZ_Z_FRONT && z >= MEZZ_Z_BACK) {
216
+ return MEZZ_HEIGHT;
217
+ }
218
+
219
+ // Ground level
220
+ return 0;
221
+ }
222
+
223
+ // Invalidate collider cache on env switch
224
+ export function invalidateColliders() {
225
+ _cachedColliders = null;
226
+ _cachedCollidersEnv = null;
227
+ }
228
+
229
+ export function spawnPlayer() {
230
+ if (S._player) despawnPlayer();
231
+
232
+ // Load appearance from localStorage
233
+ var appearance = {};
234
+ try {
235
+ var stored = localStorage.getItem('ltt_player_appearance');
236
+ if (stored) appearance = JSON.parse(stored);
237
+ } catch (e) {}
238
+
239
+ var parts = createCharacter('Player', appearance);
240
+ parts.group.position.set(0, 0, 12); // spawn at lobby
241
+
242
+ // Remove typing dots and task indicator (player doesn't need them)
243
+ parts.typingLabel.visible = false;
244
+ parts.taskLabel.visible = false;
245
+
246
+ // Update name label
247
+ var nameEl = parts.labelDiv.querySelector('.office3d-label-name');
248
+ var dotEl = parts.labelDiv.querySelector('.office3d-label-dot');
249
+ if (nameEl) nameEl.textContent = 'You';
250
+ if (dotEl) dotEl.style.background = '#58a6ff';
251
+
252
+ S.scene.add(parts.group);
253
+
254
+ S._player = {
255
+ parts: parts,
256
+ pos: { x: 0, y: 0, z: 12 },
257
+ facing: 0, // radians, 0 = +z direction
258
+ velocity: { x: 0, z: 0 },
259
+ isMoving: false,
260
+ appearance: appearance,
261
+ camYaw: Math.PI, // camera orbit angle around player (horizontal)
262
+ camPitch: 0.4, // camera pitch (vertical angle, 0=level, positive=looking down)
263
+ camDist: 6, // distance from player
264
+ };
265
+
266
+ // Disable spectator camera movement but keep key/mouse tracking alive
267
+ if (S.controls) {
268
+ S.controls.enabled = false;
269
+ S.controls._playerZoomCb = function(deltaY) {
270
+ if (S._player) {
271
+ S._player.camDist = Math.max(2, Math.min(15, S._player.camDist + deltaY * 0.01));
272
+ }
273
+ };
274
+ }
275
+
276
+ return S._player;
277
+ }
278
+
279
+ export function despawnPlayer() {
280
+ if (!S._player) return;
281
+ S.scene.remove(S._player.parts.group);
282
+ S._player.parts.group.traverse(function(child) {
283
+ if (child.geometry) child.geometry.dispose();
284
+ if (child.material) {
285
+ if (child.material.map) child.material.map.dispose();
286
+ child.material.dispose();
287
+ }
288
+ });
289
+ S._player = null;
290
+
291
+ // Re-enable spectator camera
292
+ if (S.controls) {
293
+ S.controls.enabled = true;
294
+ S.controls._playerZoomCb = null;
295
+ }
296
+ }
297
+
298
+ export function isPlayerMode() {
299
+ return !!S._player;
300
+ }
301
+
302
+ export function getPlayer() {
303
+ return S._player;
304
+ }
305
+
306
+ export function savePlayerAppearance(appearance) {
307
+ try {
308
+ localStorage.setItem('ltt_player_appearance', JSON.stringify(appearance));
309
+ } catch (e) {}
310
+ if (S._player) S._player.appearance = appearance;
311
+ }
312
+
313
+ export function getPlayerAppearance() {
314
+ try {
315
+ var stored = localStorage.getItem('ltt_player_appearance');
316
+ if (stored) return JSON.parse(stored);
317
+ } catch (e) {}
318
+ return {};
319
+ }
320
+
321
+ // Called every frame from the animation loop
322
+ export function updatePlayer(dt, time, keys) {
323
+ var player = S._player;
324
+ if (!player) return;
325
+
326
+ // --- Movement from keyboard ---
327
+ var moveX = 0, moveZ = 0;
328
+ if (keys['KeyW'] || keys['ArrowUp']) moveZ -= 1;
329
+ if (keys['KeyS'] || keys['ArrowDown']) moveZ += 1;
330
+ if (keys['KeyA'] || keys['ArrowLeft']) moveX -= 1;
331
+ if (keys['KeyD'] || keys['ArrowRight']) moveX += 1;
332
+
333
+ var isMoving = moveX !== 0 || moveZ !== 0;
334
+ player.isMoving = isMoving;
335
+
336
+ if (isMoving) {
337
+ // Movement relative to camera yaw (orbit angle)
338
+ var camYaw = S.controls && S.controls._euler ? S.controls._euler.y : 0;
339
+ _tmpForward.set(-Math.sin(camYaw), 0, -Math.cos(camYaw)).normalize();
340
+ _tmpRight.set(-_tmpForward.z, 0, _tmpForward.x);
341
+
342
+ _tmpDir.set(0, 0, 0);
343
+ _tmpDir.addScaledVector(_tmpForward, -moveZ);
344
+ _tmpDir.addScaledVector(_tmpRight, moveX);
345
+ _tmpDir.normalize();
346
+
347
+ var speed = PLAYER_SPEED * (keys['ShiftLeft'] || keys['ShiftRight'] ? 2 : 1);
348
+ var newX = player.pos.x + _tmpDir.x * speed * dt;
349
+ var newZ = player.pos.z + _tmpDir.z * speed * dt;
350
+
351
+ // Collision resolution (sliding)
352
+ var resolved = resolveMovement(player.pos.x, player.pos.z, newX, newZ, PLAYER_RADIUS);
353
+ player.pos.x = resolved.x;
354
+ player.pos.z = resolved.z;
355
+
356
+ // Face movement direction
357
+ player.facing = Math.atan2(_tmpDir.x, _tmpDir.z);
358
+ }
359
+
360
+ // Update character position
361
+ // Update height based on position (stairs/mezzanine)
362
+ var targetY = getGroundHeight(player.pos.x, player.pos.z, player.pos.y);
363
+ player.pos.y += (targetY - player.pos.y) * Math.min(1, dt * 8); // smooth height transition
364
+
365
+ player.parts.group.position.x = player.pos.x;
366
+ player.parts.group.position.y = player.pos.y;
367
+ player.parts.group.position.z = player.pos.z;
368
+
369
+ // Smooth rotation
370
+ var currentRot = player.parts.group.rotation.y;
371
+ var diff = player.facing - currentRot;
372
+ while (diff > Math.PI) diff -= Math.PI * 2;
373
+ while (diff < -Math.PI) diff += Math.PI * 2;
374
+ player.parts.group.rotation.y += diff * Math.min(1, dt * 8);
375
+
376
+ // --- Walking animation ---
377
+ if (isMoving) {
378
+ var swing = Math.sin(time * 10) * 0.5;
379
+ player.parts.leftLeg.rotation.x = swing;
380
+ player.parts.rightLeg.rotation.x = -swing;
381
+ player.parts.leftLowerLeg.rotation.x = Math.max(0, -swing) * 0.8;
382
+ player.parts.rightLowerLeg.rotation.x = Math.max(0, swing) * 0.8;
383
+ player.parts.leftArm.rotation.x = -swing * 0.7;
384
+ player.parts.rightArm.rotation.x = swing * 0.7;
385
+ player.parts.leftForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
386
+ player.parts.rightForearm.rotation.x = -0.3 - Math.abs(swing) * 0.3;
387
+ } else {
388
+ // Idle — breathing + slight head bob
389
+ player.parts.leftLeg.rotation.x *= 0.9;
390
+ player.parts.rightLeg.rotation.x *= 0.9;
391
+ player.parts.leftArm.rotation.x *= 0.9;
392
+ player.parts.rightArm.rotation.x *= 0.9;
393
+ player.parts.leftLowerLeg.rotation.x *= 0.9;
394
+ player.parts.rightLowerLeg.rotation.x *= 0.9;
395
+ player.parts.leftForearm.rotation.x *= 0.9;
396
+ player.parts.rightForearm.rotation.x *= 0.9;
397
+ var breathe = 1 + Math.sin(time * 2) * 0.02;
398
+ player.parts.body.scale.y = breathe;
399
+ player.parts.head.rotation.z = Math.sin(time * 0.5) * 0.03;
400
+ }
401
+
402
+ // --- Third-person camera follow ---
403
+ updatePlayerCamera(dt);
404
+ }
405
+
406
+ function updatePlayerCamera(dt) {
407
+ var player = S._player;
408
+ if (!player) return;
409
+
410
+ // Use spectator camera's euler for orbit angle (from mouse right-drag)
411
+ var yaw = 0, pitch = 0.4;
412
+ if (S.controls && S.controls._euler) {
413
+ yaw = S.controls._euler.y;
414
+ pitch = Math.max(0.05, Math.min(1.3, -S.controls._euler.x));
415
+ }
416
+
417
+ // Orbit camera around the player using yaw/pitch (+ player height)
418
+ var dist = player.camDist;
419
+ var baseY = player.pos.y;
420
+ var camX = player.pos.x + Math.sin(yaw) * Math.cos(pitch) * dist;
421
+ var camZ = player.pos.z + Math.cos(yaw) * Math.cos(pitch) * dist;
422
+ var camY = baseY + 1 + Math.sin(pitch) * dist;
423
+
424
+ _tmpCamTarget.set(camX, camY, camZ);
425
+
426
+ // Smooth follow
427
+ S.camera.position.lerp(_tmpCamTarget, Math.min(1, dt * 6));
428
+
429
+ // Look at player chest (offset by player height)
430
+ _tmpLookAt.set(
431
+ player.pos.x,
432
+ baseY + CAMERA_LOOK_OFFSET.y,
433
+ player.pos.z + CAMERA_LOOK_OFFSET.z
434
+ );
435
+ S.camera.lookAt(_tmpLookAt);
436
+ }
@@ -1,5 +1,9 @@
1
1
  import * as THREE from 'three';
2
2
 
3
+ // Reusable Vector3s for onMouseMove (avoid per-frame allocation)
4
+ var _panRight = new THREE.Vector3();
5
+ var _panUp = new THREE.Vector3();
6
+
3
7
  // Spectator / fly camera — Unreal Engine style free movement
4
8
  // Left-drag: look around (rotate)
5
9
  // Right-drag: pan (strafe)
@@ -28,6 +32,7 @@ export function SpectatorCamera(camera, domElement) {
28
32
  var velocity = new THREE.Vector3();
29
33
  var moveDir = new THREE.Vector3();
30
34
  var keys = {};
35
+ self.keys = keys; // expose for player mode
31
36
  var isLeftDrag = false;
32
37
  var isRightDrag = false;
33
38
  var isMiddleDrag = false;
@@ -35,11 +40,11 @@ export function SpectatorCamera(camera, domElement) {
35
40
 
36
41
  // Initialize euler from camera
37
42
  euler.setFromQuaternion(camera.quaternion, 'YXZ');
43
+ self._euler = euler; // expose for player mode camera orbit
38
44
 
39
45
  // --- Event handlers ---
40
46
  function onMouseDown(e) {
41
- if (!self.enabled) return;
42
- // Only capture if click is on the 3D canvas area
47
+ // Allow mouse input in both spectator and player modes
43
48
  if (e.target !== domElement) return;
44
49
  // Take focus away from any text input so WASD works
45
50
  if (document.activeElement && document.activeElement !== document.body) {
@@ -60,42 +65,45 @@ export function SpectatorCamera(camera, domElement) {
60
65
  }
61
66
 
62
67
  function onMouseMove(e) {
63
- if (!self.enabled) return;
64
68
  var dx = e.clientX - lastMouse.x;
65
69
  var dy = e.clientY - lastMouse.y;
66
70
  lastMouse.x = e.clientX;
67
71
  lastMouse.y = e.clientY;
68
72
 
69
73
  if (isRightDrag) {
70
- // Look around (rotate)
74
+ // Look around (rotate) — always update euler, but only apply to camera in spectator mode
71
75
  euler.y -= dx * self.lookSpeed;
72
76
  euler.x -= dy * self.lookSpeed;
73
77
  euler.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, euler.x));
74
- camera.quaternion.setFromEuler(euler);
78
+ if (self.enabled) {
79
+ camera.quaternion.setFromEuler(euler);
80
+ }
81
+ // In player mode, euler is read by player.js for orbit camera
75
82
  }
76
83
 
77
- if (isLeftDrag || isMiddleDrag) {
78
- // Pan (strafe)
79
- var right = new THREE.Vector3();
80
- var up = new THREE.Vector3();
81
- camera.getWorldDirection(new THREE.Vector3());
82
- right.setFromMatrixColumn(camera.matrixWorld, 0);
83
- up.setFromMatrixColumn(camera.matrixWorld, 1);
84
- camera.position.addScaledVector(right, -dx * self.panSpeed);
85
- camera.position.addScaledVector(up, dy * self.panSpeed);
84
+ if ((isLeftDrag || isMiddleDrag) && self.enabled) {
85
+ // Pan (strafe) — only in spectator mode
86
+ _panRight.setFromMatrixColumn(camera.matrixWorld, 0);
87
+ _panUp.setFromMatrixColumn(camera.matrixWorld, 1);
88
+ camera.position.addScaledVector(_panRight, -dx * self.panSpeed);
89
+ camera.position.addScaledVector(_panUp, dy * self.panSpeed);
86
90
  }
87
91
  }
88
92
 
89
93
  function onWheel(e) {
90
- if (!self.enabled) return;
91
- // Check if scroll is over the 3D container
92
94
  var rect = domElement.getBoundingClientRect();
93
95
  if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
94
- var forward = new THREE.Vector3();
95
- camera.getWorldDirection(forward);
96
- var amount = -e.deltaY * 0.01 * self.scrollSpeed;
97
- camera.position.addScaledVector(forward, amount);
98
96
  e.preventDefault();
97
+ if (self.enabled) {
98
+ // Spectator mode: dolly forward/back
99
+ var forward = new THREE.Vector3();
100
+ camera.getWorldDirection(forward);
101
+ var amount = -e.deltaY * 0.01 * self.scrollSpeed;
102
+ camera.position.addScaledVector(forward, amount);
103
+ } else if (self._playerZoomCb) {
104
+ // Player mode: adjust orbit distance
105
+ self._playerZoomCb(e.deltaY);
106
+ }
99
107
  }
100
108
 
101
109
  function isTyping() {
@@ -106,7 +114,8 @@ export function SpectatorCamera(camera, domElement) {
106
114
  }
107
115
 
108
116
  function onKeyDown(e) {
109
- if (!self.enabled || isTyping()) return;
117
+ // Always track keys (player mode reads them) — only block when typing in inputs
118
+ if (isTyping()) return;
110
119
  keys[e.code] = true;
111
120
  }
112
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
5
5
  "main": "server.js",
6
6
  "bin": {