@agent-os-lab/agent-game-sdk 0.1.9 → 0.1.11

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.
@@ -8,12 +8,20 @@ import type {
8
8
  AgentGameOfficeResolvedOptions,
9
9
  AgentGameOfficeSceneState,
10
10
  AgentGameOfficeSnapshot,
11
+ AgentNavigationError,
11
12
  } from "../../core/types";
12
13
  import type { ResolvedOfficeLayout } from "../../layout";
13
14
  import { applyAgentPose, updateAgentMotion } from "./agent-animation";
14
15
  import { createAgentBodyInstancedLayer } from "./agent-body-instancing";
15
16
  import { createAgentEffectInstancedLayer } from "./agent-effect-instancing";
16
- import { resolveAgentFacingTarget, resolveAgentPosition } from "./agent-layout";
17
+ import { resolveAgentFacingTarget } from "./agent-layout";
18
+ import {
19
+ advanceAgentRouteState,
20
+ createAgentRouteState,
21
+ resolveAgentRouteFloorId,
22
+ resolveRouteTargetPosition,
23
+ type RenderAgentLocationState,
24
+ } from "./agent-route";
17
25
  import {
18
26
  type AgentLabelRecord,
19
27
  createAgentLabel,
@@ -22,7 +30,13 @@ import {
22
30
  updateAgentLabelPosition,
23
31
  } from "./agent-label";
24
32
  import { type AgentMeshParts, createAgentMesh } from "./agent-mesh";
25
- import { addLights, buildOfficeScene, disposeObject } from "./scene";
33
+ import {
34
+ OFFICE_FLOOR_FOCUS_DIM_OPACITY,
35
+ addLights,
36
+ applyOfficeFloorFocus,
37
+ buildOfficeScene,
38
+ disposeObject,
39
+ } from "./scene";
26
40
 
27
41
  type AgentMeshRecord = {
28
42
  id: string;
@@ -31,18 +45,20 @@ type AgentMeshRecord = {
31
45
  mesh: AgentMeshParts;
32
46
  label: AgentLabelRecord;
33
47
  target: THREE.Vector3;
48
+ routeState: RenderAgentLocationState;
34
49
  };
35
50
 
36
51
  export const THREE_RENDERER_PIXEL_RATIO_LIMIT = 1.5;
37
52
  export const THREE_RENDERER_TARGET_FPS = 30;
38
53
  export const THREE_RENDERER_MIN_CAMERA_DISTANCE = 8;
39
- export const THREE_RENDERER_MAX_CAMERA_DISTANCE = 150;
40
- export const THREE_RENDERER_INITIAL_CAMERA_ROTATION = { x: -0.93, y: -0.23, z: -0.29 } as const;
54
+ export const THREE_RENDERER_MAX_CAMERA_DISTANCE = 180;
55
+ export const THREE_RENDERER_INITIAL_CAMERA_ROTATION = { x: -0.57, y: 0.52, z: 0.31 } as const;
56
+ export const THREE_RENDERER_INITIAL_CAMERA_POSITION = { x: 130.17, y: 100.75, z: 123.18 } as const;
41
57
  const THREE_RENDERER_CAMERA_PADDING = 1.16;
42
- const OFFICE_CAMERA_BOUNDS_HEIGHT = 3.2;
58
+ const OFFICE_CAMERA_MIN_BOUNDS_HEIGHT = 3.2;
43
59
  const WORKING_FOLLOW_RESET_DELAY_MS = 30_000;
44
60
  const WORK_EXIT_WAIT_MS = 30_000;
45
- const WORKING_FOLLOW_CAMERA_OFFSET = new THREE.Vector3(9, 12, 12);
61
+ const WORKING_FOLLOW_CAMERA_OFFSET = new THREE.Vector3(14, 18, 20);
46
62
  const OFFICE_CAMERA_TRANSITION_DURATION_MS = 600;
47
63
  const OFFICE_CAMERA_UP = new THREE.Vector3(0, 1, 0);
48
64
 
@@ -238,6 +254,8 @@ export function mountThreeAgentGameOffice(
238
254
  let previousFrameTime = performance.now();
239
255
  let previousRenderTime = previousFrameTime;
240
256
  let cameraTransition: OfficeCameraTransition | null = null;
257
+ let focusedFloorId = normalizeFocusedFloorId(_options.officeLayout, _options.focusedFloorId ?? null);
258
+ setFocusedFloor(focusedFloorId, false);
241
259
 
242
260
  function update(snapshot: AgentGameOfficeSnapshot) {
243
261
  latestSourceAgents = snapshot.agents;
@@ -262,11 +280,14 @@ export function mountThreeAgentGameOffice(
262
280
  latestAgents.forEach((agent, index) => {
263
281
  const record = agents.get(agent.id) ?? createAgentRecord(agent, index);
264
282
  agents.set(agent.id, record);
265
- const target = resolveAgentPosition(agent, index, latestAgents.length, _options.officeLayout);
283
+ record.routeState = createAgentRouteState(_options.officeLayout, agent, record.routeState);
284
+ const target = vectorFromLike(resolveRouteTargetPosition(record.routeState));
266
285
  record.agent = agent;
267
286
  record.renderIndex = index;
268
287
  record.target.copy(target);
269
288
  record.mesh.group.visible = agent.sceneState !== "offline";
289
+ record.mesh.group.userData.officeFloorId = resolveAgentRouteFloorId(_options.officeLayout, record.routeState);
290
+ record.label.root.style.opacity = String(resolveFocusedAgentOpacity(record));
270
291
  updateAgentLabel(record.label, agent);
271
292
  updateAgentLabelPosition(record.label, record.mesh.group.position, camera, renderer.domElement);
272
293
  });
@@ -274,13 +295,16 @@ export function mountThreeAgentGameOffice(
274
295
 
275
296
  function createAgentRecord(agent: AgentGameOfficeAgent, index: number): AgentMeshRecord {
276
297
  const mesh = createAgentMesh(agent, index);
277
- const position = resolveAgentPosition(agent, index, initialSnapshot.agents.length, _options.officeLayout);
298
+ void index;
299
+ const routeState = createAgentRouteState(_options.officeLayout, agent, undefined);
300
+ const position = vectorFromLike(routeState.currentPosition);
278
301
  mesh.group.position.copy(position);
279
302
  scene.add(mesh.group);
280
303
 
281
304
  const label = createAgentLabel(overlay);
305
+ label.root.style.opacity = String(resolveFocusedAgentOpacity({ routeState } as AgentMeshRecord));
282
306
 
283
- return { id: agent.id, agent, renderIndex: index, mesh, label, target: position.clone() };
307
+ return { id: agent.id, agent, renderIndex: index, mesh, label, target: position.clone(), routeState };
284
308
  }
285
309
 
286
310
  function animate() {
@@ -312,7 +336,17 @@ export function mountThreeAgentGameOffice(
312
336
  elapsedSeconds,
313
337
  deltaSeconds,
314
338
  });
339
+ record.routeState = {
340
+ ...record.routeState,
341
+ currentPosition: vectorToLike(record.mesh.group.position),
342
+ };
343
+ if (!moving && record.routeState.activeRoute) {
344
+ record.routeState = advanceAgentRouteState(record.routeState, vectorToLike(record.mesh.group.position));
345
+ record.target.copy(vectorFromLike(resolveRouteTargetPosition(record.routeState)));
346
+ }
315
347
  record.mesh.group.visible = record.agent.sceneState !== "offline";
348
+ record.mesh.group.userData.officeFloorId = resolveAgentRouteFloorId(_options.officeLayout, record.routeState);
349
+ record.label.root.style.opacity = String(resolveFocusedAgentOpacity(record));
316
350
  applyAgentPose(record.mesh, record.agent, moving, elapsedSeconds);
317
351
  if (moving || shouldUpdateLabels) {
318
352
  updateAgentLabelPosition(record.label, record.mesh.group.position, camera, renderer.domElement);
@@ -324,10 +358,12 @@ export function mountThreeAgentGameOffice(
324
358
  mesh: record.mesh,
325
359
  renderIndex: record.renderIndex,
326
360
  visible: record.agent.sceneState !== "offline",
361
+ opacity: resolveFocusedAgentOpacity(record),
327
362
  })));
328
363
  agentEffectLayer.update(Array.from(agents.values()).map((record) => ({
329
364
  mesh: record.mesh,
330
365
  visible: record.agent.sceneState !== "offline",
366
+ opacity: resolveFocusedAgentOpacity(record),
331
367
  })));
332
368
  renderer.render(scene, camera);
333
369
  labelsDirty = false;
@@ -351,6 +387,16 @@ export function mountThreeAgentGameOffice(
351
387
 
352
388
  return {
353
389
  update,
390
+ resetCamera() {
391
+ workingFollowState = {
392
+ ...workingFollowState,
393
+ selectedAgentId: null,
394
+ resetAtMs: null,
395
+ };
396
+ camera.zoom = 1;
397
+ camera.updateProjectionMatrix();
398
+ transitionOfficeCameraTo(createInitialOfficeCameraView(_options.officeLayout, camera.aspect, camera.fov));
399
+ },
354
400
  focusAgent(agentId) {
355
401
  if (!agentId) {
356
402
  transitionOfficeCameraTo(createInitialOfficeCameraView(_options.officeLayout, camera.aspect, camera.fov));
@@ -361,6 +407,16 @@ export function mountThreeAgentGameOffice(
361
407
  return;
362
408
  }
363
409
  transitionOfficeCameraTo(createFollowOfficeCameraView(record.mesh.group.position));
410
+ setFocusedFloor(resolveAgentRouteFloorId(_options.officeLayout, record.routeState));
411
+ },
412
+ focusFloor(floorId) {
413
+ setFocusedFloor(floorId);
414
+ },
415
+ getFocusedFloor() {
416
+ return focusedFloorId;
417
+ },
418
+ getNavigationErrors() {
419
+ return getCurrentNavigationErrors();
364
420
  },
365
421
  destroy() {
366
422
  disposed = true;
@@ -387,6 +443,30 @@ export function mountThreeAgentGameOffice(
387
443
  _options.onCameraChange?.(createOfficeCameraState(camera, controls));
388
444
  }
389
445
 
446
+ function getCurrentNavigationErrors(): AgentNavigationError[] {
447
+ return Array.from(agents.values())
448
+ .map((record) => record.routeState.error)
449
+ .filter((error): error is AgentNavigationError => Boolean(error));
450
+ }
451
+
452
+ function resolveFocusedAgentOpacity(record: Pick<AgentMeshRecord, "routeState">): number {
453
+ if (!focusedFloorId) {
454
+ return 1;
455
+ }
456
+ const agentFloorId = resolveAgentRouteFloorId(_options.officeLayout, record.routeState);
457
+ return agentFloorId === focusedFloorId ? 1 : OFFICE_FLOOR_FOCUS_DIM_OPACITY;
458
+ }
459
+
460
+ function setFocusedFloor(floorId: string | null, emit = true) {
461
+ const nextFloorId = normalizeFocusedFloorId(_options.officeLayout, floorId);
462
+ focusedFloorId = nextFloorId;
463
+ applyOfficeFloorFocus(scene, _options.officeLayout, focusedFloorId);
464
+ labelsDirty = true;
465
+ if (emit) {
466
+ _options.onFocusedFloorChange?.(focusedFloorId);
467
+ }
468
+ }
469
+
390
470
  function syncWorkingFollowState(nowMs: number) {
391
471
  const previousSelectedAgentId = workingFollowState.selectedAgentId;
392
472
  workingFollowState = updateWorkingAgentFollowState(workingFollowState, latestAgents, nowMs);
@@ -432,6 +512,21 @@ export function mountThreeAgentGameOffice(
432
512
  }
433
513
  }
434
514
 
515
+ function vectorFromLike(value: { x: number; y: number; z: number }): THREE.Vector3 {
516
+ return new THREE.Vector3(value.x, value.y, value.z);
517
+ }
518
+
519
+ function vectorToLike(value: THREE.Vector3): { x: number; y: number; z: number } {
520
+ return { x: value.x, y: value.y, z: value.z };
521
+ }
522
+
523
+ function normalizeFocusedFloorId(layout: ResolvedOfficeLayout, floorId: string | null): string | null {
524
+ if (!floorId) {
525
+ return null;
526
+ }
527
+ return layout.building.floors.some((floor) => floor.id === floorId) ? floorId : null;
528
+ }
529
+
435
530
  export type OfficeCameraView = {
436
531
  position: THREE.Vector3;
437
532
  rotation: THREE.Euler;
@@ -496,21 +591,19 @@ export function createInitialOfficeCameraView(
496
591
  aspect: number,
497
592
  fovDegrees: number,
498
593
  ): OfficeCameraView {
499
- const rotation = new THREE.Euler(
500
- THREE_RENDERER_INITIAL_CAMERA_ROTATION.x,
501
- THREE_RENDERER_INITIAL_CAMERA_ROTATION.y,
502
- THREE_RENDERER_INITIAL_CAMERA_ROTATION.z,
594
+ const boundsHeight = resolveOfficeCameraBoundsHeight(layout);
595
+ const target = new THREE.Vector3(layout.scene.center.x, boundsHeight / 2, layout.scene.center.z);
596
+ const position = new THREE.Vector3(
597
+ THREE_RENDERER_INITIAL_CAMERA_POSITION.x,
598
+ THREE_RENDERER_INITIAL_CAMERA_POSITION.y,
599
+ THREE_RENDERER_INITIAL_CAMERA_POSITION.z,
503
600
  );
504
- const quaternion = new THREE.Quaternion().setFromEuler(rotation);
505
- const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(quaternion).normalize();
506
- const target = new THREE.Vector3(layout.scene.center.x, OFFICE_CAMERA_BOUNDS_HEIGHT / 2, layout.scene.center.z);
507
- const preliminaryPosition = target.clone().sub(forward.clone());
508
601
  const orbitRotation = new THREE.Euler();
509
- const matrix = new THREE.Matrix4().lookAt(preliminaryPosition, target, OFFICE_CAMERA_UP);
602
+ const matrix = new THREE.Matrix4().lookAt(position, target, OFFICE_CAMERA_UP);
510
603
  orbitRotation.setFromRotationMatrix(matrix);
511
604
  const orbitQuaternion = new THREE.Quaternion().setFromEuler(orbitRotation);
512
- const distance = resolveOfficeCameraDistance(layout, target, orbitQuaternion, aspect, fovDegrees);
513
- const position = target.clone().sub(forward.multiplyScalar(distance));
605
+ const fittedDistance = resolveOfficeCameraDistance(layout, target, orbitQuaternion, aspect, fovDegrees);
606
+ const distance = Math.max(position.distanceTo(target), fittedDistance);
514
607
 
515
608
  return {
516
609
  position,
@@ -587,7 +680,7 @@ function resolveOfficeCameraDistance(
587
680
  const corners: THREE.Vector3[] = [];
588
681
 
589
682
  for (const x of [layout.scene.center.x - halfWidth, layout.scene.center.x + halfWidth]) {
590
- for (const y of [0, OFFICE_CAMERA_BOUNDS_HEIGHT]) {
683
+ for (const y of [0, resolveOfficeCameraBoundsHeight(layout)]) {
591
684
  for (const z of [layout.scene.center.z - halfDepth, layout.scene.center.z + halfDepth]) {
592
685
  corners.push(new THREE.Vector3(x, y, z));
593
686
  }
@@ -610,6 +703,10 @@ function resolveOfficeCameraDistance(
610
703
  );
611
704
  }
612
705
 
706
+ function resolveOfficeCameraBoundsHeight(layout: ResolvedOfficeLayout): number {
707
+ return Math.max(OFFICE_CAMERA_MIN_BOUNDS_HEIGHT, layout.scene.height);
708
+ }
709
+
613
710
  export function createOfficeCameraState(
614
711
  camera: THREE.PerspectiveCamera,
615
712
  controls: OrbitControls,