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

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,15 +45,17 @@ 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
61
  const WORKING_FOLLOW_CAMERA_OFFSET = new THREE.Vector3(9, 12, 12);
@@ -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() {
@@ -308,11 +332,21 @@ export function mountThreeAgentGameOffice(
308
332
  mesh: record.mesh,
309
333
  agent: record.agent,
310
334
  target: record.target,
311
- facingTarget: resolveAgentFacingTarget(record.agent, _options.officeLayout),
335
+ facingTarget: record.routeState.activeRoute ? record.target : resolveAgentFacingTarget(record.agent, _options.officeLayout),
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;
@@ -361,6 +397,16 @@ export function mountThreeAgentGameOffice(
361
397
  return;
362
398
  }
363
399
  transitionOfficeCameraTo(createFollowOfficeCameraView(record.mesh.group.position));
400
+ setFocusedFloor(resolveAgentRouteFloorId(_options.officeLayout, record.routeState));
401
+ },
402
+ focusFloor(floorId) {
403
+ setFocusedFloor(floorId);
404
+ },
405
+ getFocusedFloor() {
406
+ return focusedFloorId;
407
+ },
408
+ getNavigationErrors() {
409
+ return getCurrentNavigationErrors();
364
410
  },
365
411
  destroy() {
366
412
  disposed = true;
@@ -387,6 +433,30 @@ export function mountThreeAgentGameOffice(
387
433
  _options.onCameraChange?.(createOfficeCameraState(camera, controls));
388
434
  }
389
435
 
436
+ function getCurrentNavigationErrors(): AgentNavigationError[] {
437
+ return Array.from(agents.values())
438
+ .map((record) => record.routeState.error)
439
+ .filter((error): error is AgentNavigationError => Boolean(error));
440
+ }
441
+
442
+ function resolveFocusedAgentOpacity(record: Pick<AgentMeshRecord, "routeState">): number {
443
+ if (!focusedFloorId) {
444
+ return 1;
445
+ }
446
+ const agentFloorId = resolveAgentRouteFloorId(_options.officeLayout, record.routeState);
447
+ return agentFloorId === focusedFloorId ? 1 : OFFICE_FLOOR_FOCUS_DIM_OPACITY;
448
+ }
449
+
450
+ function setFocusedFloor(floorId: string | null, emit = true) {
451
+ const nextFloorId = normalizeFocusedFloorId(_options.officeLayout, floorId);
452
+ focusedFloorId = nextFloorId;
453
+ applyOfficeFloorFocus(scene, _options.officeLayout, focusedFloorId);
454
+ labelsDirty = true;
455
+ if (emit) {
456
+ _options.onFocusedFloorChange?.(focusedFloorId);
457
+ }
458
+ }
459
+
390
460
  function syncWorkingFollowState(nowMs: number) {
391
461
  const previousSelectedAgentId = workingFollowState.selectedAgentId;
392
462
  workingFollowState = updateWorkingAgentFollowState(workingFollowState, latestAgents, nowMs);
@@ -432,6 +502,21 @@ export function mountThreeAgentGameOffice(
432
502
  }
433
503
  }
434
504
 
505
+ function vectorFromLike(value: { x: number; y: number; z: number }): THREE.Vector3 {
506
+ return new THREE.Vector3(value.x, value.y, value.z);
507
+ }
508
+
509
+ function vectorToLike(value: THREE.Vector3): { x: number; y: number; z: number } {
510
+ return { x: value.x, y: value.y, z: value.z };
511
+ }
512
+
513
+ function normalizeFocusedFloorId(layout: ResolvedOfficeLayout, floorId: string | null): string | null {
514
+ if (!floorId) {
515
+ return null;
516
+ }
517
+ return layout.building.floors.some((floor) => floor.id === floorId) ? floorId : null;
518
+ }
519
+
435
520
  export type OfficeCameraView = {
436
521
  position: THREE.Vector3;
437
522
  rotation: THREE.Euler;
@@ -496,21 +581,19 @@ export function createInitialOfficeCameraView(
496
581
  aspect: number,
497
582
  fovDegrees: number,
498
583
  ): 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,
584
+ const boundsHeight = resolveOfficeCameraBoundsHeight(layout);
585
+ const target = new THREE.Vector3(layout.scene.center.x, boundsHeight / 2, layout.scene.center.z);
586
+ const position = new THREE.Vector3(
587
+ THREE_RENDERER_INITIAL_CAMERA_POSITION.x,
588
+ THREE_RENDERER_INITIAL_CAMERA_POSITION.y,
589
+ THREE_RENDERER_INITIAL_CAMERA_POSITION.z,
503
590
  );
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
591
  const orbitRotation = new THREE.Euler();
509
- const matrix = new THREE.Matrix4().lookAt(preliminaryPosition, target, OFFICE_CAMERA_UP);
592
+ const matrix = new THREE.Matrix4().lookAt(position, target, OFFICE_CAMERA_UP);
510
593
  orbitRotation.setFromRotationMatrix(matrix);
511
594
  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));
595
+ const fittedDistance = resolveOfficeCameraDistance(layout, target, orbitQuaternion, aspect, fovDegrees);
596
+ const distance = Math.max(position.distanceTo(target), fittedDistance);
514
597
 
515
598
  return {
516
599
  position,
@@ -587,7 +670,7 @@ function resolveOfficeCameraDistance(
587
670
  const corners: THREE.Vector3[] = [];
588
671
 
589
672
  for (const x of [layout.scene.center.x - halfWidth, layout.scene.center.x + halfWidth]) {
590
- for (const y of [0, OFFICE_CAMERA_BOUNDS_HEIGHT]) {
673
+ for (const y of [0, resolveOfficeCameraBoundsHeight(layout)]) {
591
674
  for (const z of [layout.scene.center.z - halfDepth, layout.scene.center.z + halfDepth]) {
592
675
  corners.push(new THREE.Vector3(x, y, z));
593
676
  }
@@ -610,6 +693,10 @@ function resolveOfficeCameraDistance(
610
693
  );
611
694
  }
612
695
 
696
+ function resolveOfficeCameraBoundsHeight(layout: ResolvedOfficeLayout): number {
697
+ return Math.max(OFFICE_CAMERA_MIN_BOUNDS_HEIGHT, layout.scene.height);
698
+ }
699
+
613
700
  export function createOfficeCameraState(
614
701
  camera: THREE.PerspectiveCamera,
615
702
  controls: OrbitControls,