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/CHANGELOG.md +86 -0
- package/cli.js +1 -1
- package/dashboard.html +497 -13
- package/dashboard.js +318 -3
- package/office/agents.js +148 -4
- package/office/animation.js +68 -0
- package/office/assets.js +431 -0
- package/office/builder.js +355 -0
- package/office/campus-env.js +119 -23
- package/office/face.js +65 -0
- package/office/index.js +623 -0
- package/office/player.js +228 -6
- package/office/world-save.js +91 -0
- package/package.json +1 -1
- package/server.js +512 -83
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 (
|
|
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.
|
|
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
|
-
// ---
|
|
403
|
-
|
|
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
|
+
}
|