let-them-talk 4.0.2 → 4.3.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.
package/office/player.js CHANGED
@@ -261,6 +261,10 @@ export function spawnPlayer() {
261
261
  camYaw: Math.PI, // camera orbit angle around player (horizontal)
262
262
  camPitch: 0.4, // camera pitch (vertical angle, 0=level, positive=looking down)
263
263
  camDist: 6, // distance from player
264
+ _jumping: false,
265
+ _jumpVel: 0,
266
+ _jumpY: 0,
267
+ _landSquash: 0,
264
268
  };
265
269
 
266
270
  // Disable spectator camera movement but keep key/mouse tracking alive
@@ -278,6 +282,12 @@ export function spawnPlayer() {
278
282
 
279
283
  export function despawnPlayer() {
280
284
  if (!S._player) return;
285
+ // Clean up "Press E to sit" prompt so it doesn't leak to other tabs
286
+ if (S._player._sitPrompt) {
287
+ S._player._sitPrompt.style.display = 'none';
288
+ if (S._player._sitPrompt.parentElement) S._player._sitPrompt.remove();
289
+ S._player._sitPrompt = null;
290
+ }
281
291
  S.scene.remove(S._player.parts.group);
282
292
  S._player.parts.group.traverse(function(child) {
283
293
  if (child.geometry) child.geometry.dispose();
@@ -333,6 +343,27 @@ export function updatePlayer(dt, time, keys) {
333
343
  var isMoving = moveX !== 0 || moveZ !== 0;
334
344
  player.isMoving = isMoving;
335
345
 
346
+ // --- Jump with Space ---
347
+ if (keys['Space'] && !player.sitting && !player._jumping) {
348
+ player._jumping = true;
349
+ player._jumpVel = 5.5; // initial upward velocity
350
+ player._jumpY = 0;
351
+ }
352
+ if (player._jumping) {
353
+ player._jumpVel -= 18 * dt; // gravity
354
+ player._jumpY += player._jumpVel * dt;
355
+ if (player._jumpY <= 0) {
356
+ player._jumpY = 0;
357
+ player._jumping = false;
358
+ player._jumpVel = 0;
359
+ player._landSquash = 0.3; // trigger landing squash
360
+ }
361
+ }
362
+ // Landing squash/stretch decay
363
+ if (player._landSquash > 0) {
364
+ player._landSquash = Math.max(0, player._landSquash - dt * 3);
365
+ }
366
+
336
367
  if (isMoving) {
337
368
  // Movement relative to camera yaw (orbit angle)
338
369
  var camYaw = S.controls && S.controls._euler ? S.controls._euler.y : 0;
@@ -363,9 +394,23 @@ export function updatePlayer(dt, time, keys) {
363
394
  player.pos.y += (targetY - player.pos.y) * Math.min(1, dt * 8); // smooth height transition
364
395
 
365
396
  player.parts.group.position.x = player.pos.x;
366
- player.parts.group.position.y = player.pos.y;
397
+ player.parts.group.position.y = player.pos.y + (player._jumpY || 0);
367
398
  player.parts.group.position.z = player.pos.z;
368
399
 
400
+ // Jump squash/stretch visual
401
+ var jumpSquash = player._landSquash || 0;
402
+ if (player._jumping && player._jumpVel > 0) {
403
+ // Stretch upward during ascent
404
+ player.parts.body.scale.set(0.9, 1.15, 0.9);
405
+ } else if (jumpSquash > 0) {
406
+ // Squash on landing — wide and short
407
+ var sq = 1 + jumpSquash * 0.5; // width: up to 1.15
408
+ var sy = 1 - jumpSquash * 0.4; // height: down to 0.88
409
+ player.parts.body.scale.set(sq, sy, sq);
410
+ } else if (!isMoving) {
411
+ // Reset to breathing (handled below)
412
+ }
413
+
369
414
  // Smooth rotation
370
415
  var currentRot = player.parts.group.rotation.y;
371
416
  var diff = player.facing - currentRot;
@@ -373,8 +418,20 @@ export function updatePlayer(dt, time, keys) {
373
418
  while (diff < -Math.PI) diff += Math.PI * 2;
374
419
  player.parts.group.rotation.y += diff * Math.min(1, dt * 8);
375
420
 
376
- // --- Walking animation ---
377
- if (isMoving) {
421
+ // --- Walking / Jump / Idle animation ---
422
+ if (player._jumping) {
423
+ // Jump pose — tuck legs, raise arms
424
+ var tuck = player._jumpVel > 0 ? 0.6 : 0.3; // more tuck on ascent
425
+ player.parts.leftLeg.rotation.x = -tuck;
426
+ player.parts.rightLeg.rotation.x = -tuck;
427
+ player.parts.leftLowerLeg.rotation.x = tuck * 1.2;
428
+ player.parts.rightLowerLeg.rotation.x = tuck * 1.2;
429
+ player.parts.leftArm.rotation.x = -1.2; // arms up
430
+ player.parts.rightArm.rotation.x = -1.2;
431
+ player.parts.leftForearm.rotation.x = -0.5;
432
+ player.parts.rightForearm.rotation.x = -0.5;
433
+ } else if (isMoving) {
434
+ player.parts.body.scale.set(1, 1, 1); // reset from jump squash
378
435
  var swing = Math.sin(time * 10) * 0.5;
379
436
  player.parts.leftLeg.rotation.x = swing;
380
437
  player.parts.rightLeg.rotation.x = -swing;
@@ -395,12 +452,125 @@ export function updatePlayer(dt, time, keys) {
395
452
  player.parts.leftForearm.rotation.x *= 0.9;
396
453
  player.parts.rightForearm.rotation.x *= 0.9;
397
454
  var breathe = 1 + Math.sin(time * 2) * 0.02;
398
- player.parts.body.scale.y = breathe;
455
+ if (!player._landSquash) player.parts.body.scale.set(1, breathe, 1);
399
456
  player.parts.head.rotation.z = Math.sin(time * 0.5) * 0.03;
400
457
  }
401
458
 
402
- // --- Third-person camera follow ---
403
- updatePlayerCamera(dt);
459
+ // --- E-to-sit at desk system ---
460
+ var desks = S._campusDeskPositions || [];
461
+ var SIT_RANGE = 2.5; // max distance to sit at a desk
462
+ var nearestDesk = -1;
463
+ var nearestDist = SIT_RANGE;
464
+
465
+ if (!player.sitting) {
466
+ // Find nearest desk
467
+ for (var di = 0; di < desks.length; di++) {
468
+ var ddx = player.pos.x - desks[di].x;
469
+ var ddz = player.pos.z - (desks[di].z + 0.7); // chair is 0.7 in front of desk
470
+ var dd = Math.sqrt(ddx * ddx + ddz * ddz);
471
+ if (dd < nearestDist) { nearestDist = dd; nearestDesk = di; }
472
+ }
473
+ }
474
+
475
+ // Show/hide "Press E to sit" prompt
476
+ if (nearestDesk >= 0 && !player.sitting) {
477
+ if (!player._sitPrompt) {
478
+ player._sitPrompt = document.createElement('div');
479
+ player._sitPrompt.className = 'office3d-sit-prompt';
480
+ player._sitPrompt.textContent = 'Press E to sit';
481
+ player._sitPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#58a6ff;padding:8px 16px;border-radius:8px;font-size:14px;z-index:1000;pointer-events:none;border:1px solid #30363d;';
482
+ document.body.appendChild(player._sitPrompt);
483
+ }
484
+ player._sitPrompt.style.display = 'block';
485
+ player._nearDesk = nearestDesk;
486
+ } else if (player._sitPrompt && !player.sitting) {
487
+ player._sitPrompt.style.display = 'none';
488
+ player._nearDesk = -1;
489
+ }
490
+
491
+ // E key: sit down or stand up
492
+ if (keys['KeyE'] && !player._ePressed) {
493
+ player._ePressed = true;
494
+ if (!player.sitting && player._nearDesk >= 0) {
495
+ // Sit down at desk
496
+ player.sitting = true;
497
+ player.sittingDeskIdx = player._nearDesk;
498
+ player.sittingLerp = 0;
499
+ var deskPos = desks[player._nearDesk];
500
+ player._sitTarget = { x: deskPos.x, y: player.pos.y, z: deskPos.z + 0.7 };
501
+ if (player._sitPrompt) player._sitPrompt.style.display = 'none';
502
+ // Notify: player sat down (Builder's iframe hook)
503
+ if (typeof window.onPlayerSit === 'function') window.onPlayerSit(player.sittingDeskIdx);
504
+ } else if (player.sitting) {
505
+ // Stand up — push player AWAY from desk center to avoid collision trapping
506
+ var desks = S._campusDeskPositions || [];
507
+ var dIdx = player.sittingDeskIdx;
508
+ player.sitting = false;
509
+ player.sittingDeskIdx = -1;
510
+ if (dIdx >= 0 && desks[dIdx]) {
511
+ var ddx = player.pos.x - desks[dIdx].x;
512
+ var ddz = player.pos.z - desks[dIdx].z;
513
+ var dd = Math.sqrt(ddx * ddx + ddz * ddz) || 1;
514
+ player.pos.x += (ddx / dd) * 0.8;
515
+ player.pos.z += (ddz / dd) * 0.8;
516
+ } else {
517
+ player.pos.z += 0.8; // fallback
518
+ }
519
+ // Notify: player stood up
520
+ if (typeof window.onPlayerStand === 'function') window.onPlayerStand();
521
+ }
522
+ }
523
+ if (!keys['KeyE']) player._ePressed = false;
524
+
525
+ // --- Jukebox E-to-interact ---
526
+ if (S._jukebox && !player.sitting) {
527
+ var jbx = player.pos.x - S._jukebox.pos.x;
528
+ var jbz = player.pos.z - S._jukebox.pos.z;
529
+ var jbDist = Math.sqrt(jbx * jbx + jbz * jbz);
530
+ if (jbDist < 2.0) {
531
+ // Show prompt
532
+ if (!player._jukeboxPrompt) {
533
+ player._jukeboxPrompt = document.createElement('div');
534
+ player._jukeboxPrompt.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#ff4488;padding:8px 16px;border-radius:8px;font-size:14px;z-index:1000;pointer-events:none;border:1px solid #ff4488;';
535
+ player._jukeboxPrompt.textContent = 'Press E for Jukebox';
536
+ document.body.appendChild(player._jukeboxPrompt);
537
+ }
538
+ player._jukeboxPrompt.style.display = 'block';
539
+ // E key activates jukebox (only if not near a desk)
540
+ if (keys['KeyE'] && player._ePressed && nearestDesk < 0) {
541
+ if (typeof window.onJukeboxInteract === 'function') window.onJukeboxInteract();
542
+ if (player._jukeboxPrompt) player._jukeboxPrompt.style.display = 'none';
543
+ }
544
+ } else if (player._jukeboxPrompt) {
545
+ player._jukeboxPrompt.style.display = 'none';
546
+ }
547
+ }
548
+
549
+ // Sitting animation: lerp position to chair, set sitting pose
550
+ if (player.sitting && player._sitTarget) {
551
+ player.sittingLerp = Math.min(1, (player.sittingLerp || 0) + dt * 3);
552
+ var sl = player.sittingLerp;
553
+ player.pos.x += (player._sitTarget.x - player.pos.x) * Math.min(1, dt * 5);
554
+ player.pos.z += (player._sitTarget.z - player.pos.z) * Math.min(1, dt * 5);
555
+ player.facing = Math.PI; // face the desk
556
+
557
+ // Sitting pose (same as agent sitting)
558
+ var sitHip = -1.5 * sl;
559
+ player.parts.leftLeg.rotation.x = sitHip;
560
+ player.parts.rightLeg.rotation.x = sitHip;
561
+ player.parts.leftLowerLeg.rotation.x = 1.5 * sl;
562
+ player.parts.rightLowerLeg.rotation.x = 1.5 * sl;
563
+ player.parts.leftForearm.rotation.x = -0.4 * sl;
564
+ player.parts.rightForearm.rotation.x = -0.4 * sl;
565
+ player.parts.group.position.y = player.pos.y + sl * 0.14;
566
+ }
567
+
568
+ // --- Third-person camera follow (or desk POV when sitting) ---
569
+ if (player.sitting) {
570
+ updatePlayerCameraDesk(dt);
571
+ } else {
572
+ updatePlayerCamera(dt);
573
+ }
404
574
  }
405
575
 
406
576
  function updatePlayerCamera(dt) {
@@ -434,3 +604,55 @@ function updatePlayerCamera(dt) {
434
604
  );
435
605
  S.camera.lookAt(_tmpLookAt);
436
606
  }
607
+
608
+ // Desk POV camera — when player is sitting, camera moves behind the character looking at the monitor
609
+ function updatePlayerCameraDesk(dt) {
610
+ var player = S._player;
611
+ if (!player || !player._sitTarget) return;
612
+
613
+ // Camera behind player's head, looking at the desk/monitor
614
+ var deskX = player._sitTarget.x;
615
+ var deskZ = player._sitTarget.z;
616
+ var baseY = player.pos.y;
617
+
618
+ // Camera position: slightly behind and above the seated player
619
+ _tmpCamTarget.set(deskX, baseY + 1.6, deskZ + 1.8);
620
+ S.camera.position.lerp(_tmpCamTarget, Math.min(1, dt * 4));
621
+
622
+ // Look at the monitor (desk is at z, monitor is slightly behind at z - 0.3)
623
+ _tmpLookAt.set(deskX, baseY + 1.2, deskZ - 0.5);
624
+ S.camera.lookAt(_tmpLookAt);
625
+ }
626
+
627
+ // --- Public API for iframe integration ---
628
+ export function isPlayerSitting() {
629
+ return S._player && S._player.sitting;
630
+ }
631
+
632
+ export function getPlayerDeskIdx() {
633
+ return S._player ? S._player.sittingDeskIdx : -1;
634
+ }
635
+
636
+ // Force stand from external code (LEAVE button, Escape key handler in iframe overlay)
637
+ // Sets sitting=false so WASD movement resumes and camera returns to third-person
638
+ window.playerForceStand = function() {
639
+ var player = S._player;
640
+ if (player && player.sitting) {
641
+ // Push player AWAY from desk center (works for all desk orientations)
642
+ var desks = S._campusDeskPositions || [];
643
+ var dIdx = player.sittingDeskIdx;
644
+ player.sitting = false;
645
+ player.sittingDeskIdx = -1;
646
+ if (player._sitPrompt) player._sitPrompt.style.display = 'none';
647
+ if (dIdx >= 0 && desks[dIdx]) {
648
+ // Always stand up FORWARD (toward camera/door side, negative Z in desk-local space)
649
+ // This avoids pushing into walls behind the desk
650
+ player.pos.z = desks[dIdx].z + 1.2; // 1.2 units in front of desk center
651
+ // Slight X offset to avoid chair mesh
652
+ var ddx = player.pos.x - desks[dIdx].x;
653
+ if (Math.abs(ddx) < 0.3) player.pos.x += 0.4;
654
+ } else {
655
+ player.pos.z += 0.8; // fallback
656
+ }
657
+ }
658
+ };
@@ -0,0 +1,91 @@
1
+ // world-save.js — Persistence layer for the World Builder
2
+ // Saves/loads placed objects to .agent-bridge/world-layout.json via dashboard API
3
+
4
+ var _placements = []; // in-memory placement array
5
+ var _saveTimeout = null; // debounce timer
6
+ var _loaded = false;
7
+
8
+ // Generate unique placement ID
9
+ function generatePlacementId() {
10
+ return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
11
+ }
12
+
13
+ // Get project query string for API calls
14
+ function getProjectParam() {
15
+ return window.activeProject ? '?project=' + encodeURIComponent(window.activeProject) : '';
16
+ }
17
+
18
+ // --- Load world layout from server ---
19
+ export async function loadWorld() {
20
+ try {
21
+ var res = await fetch('/api/world-layout' + getProjectParam());
22
+ if (res.ok) {
23
+ var data = await res.json();
24
+ _placements = Array.isArray(data) ? data : [];
25
+ _loaded = true;
26
+ return _placements;
27
+ }
28
+ } catch (e) {
29
+ console.warn('[world-save] Load failed:', e.message);
30
+ }
31
+ _placements = [];
32
+ _loaded = true;
33
+ return _placements;
34
+ }
35
+
36
+ // --- Save full world layout to server (debounced) ---
37
+ function scheduleSave() {
38
+ if (_saveTimeout) clearTimeout(_saveTimeout);
39
+ _saveTimeout = setTimeout(function() {
40
+ _saveTimeout = null;
41
+ fetch('/api/world-save' + getProjectParam(), {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
44
+ body: JSON.stringify(_placements)
45
+ }).catch(function(e) {
46
+ console.warn('[world-save] Save failed:', e.message);
47
+ });
48
+ }, 500); // debounce 500ms — batches rapid placements
49
+ }
50
+
51
+ // --- Add a single placement ---
52
+ export function addPlacement(type, x, y, z, rotY, placedBy) {
53
+ var entry = {
54
+ id: generatePlacementId(),
55
+ type: type,
56
+ x: x,
57
+ y: y || 0,
58
+ z: z,
59
+ rotY: rotY || 0,
60
+ placed_by: placedBy || 'user',
61
+ timestamp: new Date().toISOString()
62
+ };
63
+ _placements.push(entry);
64
+ scheduleSave();
65
+ return entry;
66
+ }
67
+
68
+ // --- Remove a placement by ID ---
69
+ export function removePlacement(id) {
70
+ var idx = _placements.findIndex(function(p) { return p.id === id; });
71
+ if (idx === -1) return null;
72
+ var removed = _placements.splice(idx, 1)[0];
73
+ scheduleSave();
74
+ return removed;
75
+ }
76
+
77
+ // --- Get all placements (read-only copy) ---
78
+ export function getPlacements() {
79
+ return _placements.slice();
80
+ }
81
+
82
+ // --- Clear all placements ---
83
+ export function clearWorld() {
84
+ _placements = [];
85
+ scheduleSave();
86
+ }
87
+
88
+ // --- Check if world has been loaded ---
89
+ export function isLoaded() {
90
+ return _loaded;
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "4.0.2",
3
+ "version": "4.3.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": {