@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.
@@ -62,6 +62,9 @@ export async function mountAgentGameOffice(
62
62
  if (options.focusedAgentId !== undefined) {
63
63
  controller.focusAgent?.(options.focusedAgentId);
64
64
  }
65
+ if (options.focusedFloorId !== undefined) {
66
+ controller.focusFloor?.(options.focusedFloorId);
67
+ }
65
68
 
66
69
  return {
67
70
  updateAgents(agents: AgentPresence[]) {
@@ -72,9 +75,18 @@ export async function mountAgentGameOffice(
72
75
  focusAgent(agentId) {
73
76
  controller?.focusAgent?.(agentId);
74
77
  },
78
+ focusFloor(floorId) {
79
+ controller?.focusFloor?.(floorId);
80
+ },
81
+ getFocusedFloor() {
82
+ return controller?.getFocusedFloor?.() ?? null;
83
+ },
75
84
  refreshAgents() {
76
85
  runtimeSubscription?.refresh();
77
86
  },
87
+ getNavigationErrors() {
88
+ return controller?.getNavigationErrors?.() ?? [];
89
+ },
78
90
  destroy() {
79
91
  unsubscribe();
80
92
  runtimeSubscription?.close();
@@ -59,6 +59,10 @@ export function AgentGameOfficeView({
59
59
  viewRef.current?.focusAgent(options.focusedAgentId ?? null);
60
60
  }, [options.focusedAgentId]);
61
61
 
62
+ useEffect(() => {
63
+ viewRef.current?.focusFloor(options.focusedFloorId ?? null);
64
+ }, [options.focusedFloorId]);
65
+
62
66
  return React.createElement("div", {
63
67
  ref: containerRef,
64
68
  className,
@@ -75,8 +79,20 @@ function createOfficeConfigKey(office: AgentGameOfficeMountOptions["office"]): s
75
79
  if (!office) {
76
80
  return "default";
77
81
  }
78
- return JSON.stringify(office.rooms.map((room) => ({
79
- type: room.type,
80
- capacity: room.capacity ?? null,
81
- })));
82
+ return JSON.stringify({
83
+ floors: office.building.floors.map((floor) => ({
84
+ id: floor.id,
85
+ name: floor.name,
86
+ rooms: floor.rooms.map((room) => ({
87
+ type: room.type,
88
+ capacity: room.capacity ?? null,
89
+ })),
90
+ })),
91
+ connectors: office.building.connectors.map((connector) => ({
92
+ id: connector.id,
93
+ name: connector.name,
94
+ type: connector.type,
95
+ serves: connector.serves,
96
+ })),
97
+ });
82
98
  }
@@ -9,6 +9,7 @@ export type AgentBodyInstanceRecord = {
9
9
  mesh: AgentMeshParts;
10
10
  renderIndex: number;
11
11
  visible: boolean;
12
+ opacity?: number;
12
13
  };
13
14
 
14
15
  type AgentBodyBatch = {
@@ -18,7 +19,7 @@ type AgentBodyBatch = {
18
19
  };
19
20
 
20
21
  const geometryCache = new Map<string, THREE.BoxGeometry>();
21
- const materialCache = new Map<number, THREE.MeshLambertMaterial>();
22
+ const materialCache = new Map<string, THREE.MeshLambertMaterial>();
22
23
  const matrixHelper = new THREE.Matrix4();
23
24
 
24
25
  export class AgentBodyInstancedLayer {
@@ -41,7 +42,7 @@ export class AgentBodyInstancedLayer {
41
42
  }
42
43
  record.mesh.group.updateMatrixWorld(true);
43
44
  resolveAgentBodyPartRenderSpecs(record.renderIndex).forEach((part) => {
44
- const batch = this.ensureBatch(part, records.length * 2);
45
+ const batch = this.ensureBatch(part, records.length * 2, record.opacity ?? 1);
45
46
  const object = record.mesh[part.key];
46
47
  object.updateMatrixWorld(true);
47
48
  matrixHelper.copy(object.matrixWorld);
@@ -67,8 +68,9 @@ export class AgentBodyInstancedLayer {
67
68
  private ensureBatch(
68
69
  part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number],
69
70
  requiredCapacity: number,
71
+ opacity: number,
70
72
  ): AgentBodyBatch {
71
- const key = `${part.width}:${part.height}:${part.depth}:${part.color}`;
73
+ const key = `${part.width}:${part.height}:${part.depth}:${part.color}:${opacity}`;
72
74
  const existing = this.batches.get(key);
73
75
  if (existing && existing.capacity >= requiredCapacity) {
74
76
  return existing;
@@ -79,7 +81,7 @@ export class AgentBodyInstancedLayer {
79
81
 
80
82
  const mesh = new THREE.InstancedMesh(
81
83
  getGeometry(part.width, part.height, part.depth),
82
- getMaterial(part.color),
84
+ getMaterial(part.color, opacity),
83
85
  Math.max(requiredCapacity, 1),
84
86
  );
85
87
  mesh.castShadow = false;
@@ -109,12 +111,18 @@ function getGeometry(width: number, height: number, depth: number): THREE.BoxGeo
109
111
  return geometry;
110
112
  }
111
113
 
112
- function getMaterial(color: number): THREE.MeshLambertMaterial {
113
- const cached = materialCache.get(color);
114
+ function getMaterial(color: number, opacity: number): THREE.MeshLambertMaterial {
115
+ const key = `${color}:${opacity}`;
116
+ const cached = materialCache.get(key);
114
117
  if (cached) {
115
118
  return cached;
116
119
  }
117
- const material = new THREE.MeshLambertMaterial({ color });
118
- materialCache.set(color, material);
120
+ const material = new THREE.MeshLambertMaterial({
121
+ color,
122
+ transparent: opacity < 1,
123
+ opacity,
124
+ depthWrite: opacity >= 1,
125
+ });
126
+ materialCache.set(key, material);
119
127
  return material;
120
128
  }
@@ -5,6 +5,7 @@ import type { AgentMeshParts } from "./agent-mesh";
5
5
  export type AgentEffectInstanceRecord = {
6
6
  mesh: AgentMeshParts;
7
7
  visible: boolean;
8
+ opacity?: number;
8
9
  };
9
10
 
10
11
  type EffectBatch = {
@@ -34,13 +35,14 @@ export class AgentEffectInstancedLayer {
34
35
  return;
35
36
  }
36
37
  record.mesh.group.updateMatrixWorld(true);
37
- this.addObject("glow", record.mesh.activityGlow, records.length);
38
+ const opacity = record.opacity ?? 1;
39
+ this.addObject("glow", record.mesh.activityGlow, records.length, opacity);
38
40
  record.mesh.activityEffects.typingDots.forEach((dot) => {
39
- this.addObject("typingDot", dot, records.length * 3);
41
+ this.addObject("typingDot", dot, records.length * 3, opacity);
40
42
  });
41
- this.addObject("toolBlock", record.mesh.activityEffects.toolBlock, records.length);
42
- this.addObject("runningRing", record.mesh.activityEffects.runningRing, records.length);
43
- this.addObject("completionRing", record.mesh.activityEffects.completionRing, records.length);
43
+ this.addObject("toolBlock", record.mesh.activityEffects.toolBlock, records.length, opacity);
44
+ this.addObject("runningRing", record.mesh.activityEffects.runningRing, records.length, opacity);
45
+ this.addObject("completionRing", record.mesh.activityEffects.completionRing, records.length, opacity);
44
46
  });
45
47
 
46
48
  this.batches.forEach((batch) => {
@@ -57,19 +59,20 @@ export class AgentEffectInstancedLayer {
57
59
  this.batches.clear();
58
60
  }
59
61
 
60
- private addObject(key: EffectKey, object: THREE.Object3D, capacity: number): void {
62
+ private addObject(key: EffectKey, object: THREE.Object3D, capacity: number, opacity: number): void {
61
63
  if (!object.visible) {
62
64
  return;
63
65
  }
64
- const batch = this.ensureBatch(key, capacity);
66
+ const batch = this.ensureBatch(key, capacity, opacity);
65
67
  object.updateMatrixWorld(true);
66
68
  matrixHelper.copy(object.matrixWorld);
67
69
  batch.mesh.setMatrixAt(batch.count, matrixHelper);
68
70
  batch.count += 1;
69
71
  }
70
72
 
71
- private ensureBatch(key: EffectKey, requiredCapacity: number): EffectBatch {
72
- const existing = this.batches.get(key);
73
+ private ensureBatch(key: EffectKey, requiredCapacity: number, opacity: number): EffectBatch {
74
+ const batchKey = `${key}:${opacity}`;
75
+ const existing = this.batches.get(batchKey);
73
76
  if (existing && existing.capacity >= requiredCapacity) {
74
77
  return existing;
75
78
  }
@@ -80,21 +83,32 @@ export class AgentEffectInstancedLayer {
80
83
  const spec = effectSpecs[key];
81
84
  const mesh = new THREE.InstancedMesh(
82
85
  spec.geometry,
83
- spec.material,
86
+ resolveEffectMaterial(spec.material, opacity),
84
87
  Math.max(requiredCapacity, 1),
85
88
  );
86
89
  mesh.castShadow = false;
87
90
  mesh.receiveShadow = false;
88
91
  mesh.frustumCulled = false;
89
- mesh.name = `agentEffect:${key}`;
92
+ mesh.name = `agentEffect:${batchKey}`;
90
93
  this.scene.add(mesh);
91
94
 
92
95
  const batch = { mesh, capacity: Math.max(requiredCapacity, 1), count: 0 };
93
- this.batches.set(key, batch);
96
+ this.batches.set(batchKey, batch);
94
97
  return batch;
95
98
  }
96
99
  }
97
100
 
101
+ function resolveEffectMaterial(material: THREE.Material, opacity: number): THREE.Material {
102
+ if (opacity >= 1) {
103
+ return material;
104
+ }
105
+ const clone = material.clone();
106
+ clone.transparent = true;
107
+ clone.opacity *= opacity;
108
+ clone.depthWrite = false;
109
+ return clone;
110
+ }
111
+
98
112
  export function createAgentEffectInstancedLayer(scene: THREE.Scene): AgentEffectInstancedLayer {
99
113
  return new AgentEffectInstancedLayer(scene);
100
114
  }
@@ -0,0 +1,220 @@
1
+ import type {
2
+ AgentGameOfficeAgent,
3
+ AgentNavigationError,
4
+ } from "../../core/types";
5
+ import {
6
+ NoBuildingRouteError,
7
+ planAgentRoute,
8
+ type AgentRoute,
9
+ type Vector3Like,
10
+ } from "../../layout";
11
+ import type {
12
+ ResolvedOffstageAnchor,
13
+ ResolvedOfficeLayout,
14
+ ResolvedSeatAnchor,
15
+ } from "../../layout";
16
+
17
+ export type RenderAgentLocationState = {
18
+ currentNodeId: string;
19
+ currentPosition: Vector3Like;
20
+ destinationAnchorId: string | null;
21
+ activeRoute: AgentRoute | null;
22
+ activeStepIndex: number;
23
+ error?: AgentNavigationError;
24
+ };
25
+
26
+ type TargetAnchorResult =
27
+ | { anchor: ResolvedSeatAnchor | ResolvedOffstageAnchor; error?: undefined }
28
+ | { anchor?: undefined; error: AgentNavigationError };
29
+
30
+ export function createAgentRouteState(
31
+ layout: ResolvedOfficeLayout,
32
+ agent: AgentGameOfficeAgent,
33
+ previous: RenderAgentLocationState | undefined,
34
+ ): RenderAgentLocationState {
35
+ if (agent.targetPosition) {
36
+ return {
37
+ currentNodeId: previous?.currentNodeId ?? `explicit:${agent.id}`,
38
+ currentPosition: {
39
+ x: agent.targetPosition.x,
40
+ y: previous?.currentPosition.y ?? 0,
41
+ z: agent.targetPosition.z,
42
+ },
43
+ destinationAnchorId: null,
44
+ activeRoute: null,
45
+ activeStepIndex: 0,
46
+ };
47
+ }
48
+
49
+ const target = resolveAgentRouteTargetAnchor(layout, agent);
50
+ if (target.error) {
51
+ return createErrorState(agent, previous, target.error);
52
+ }
53
+
54
+ const targetNodeId = target.anchor.id;
55
+ const targetPosition = resolveAnchorPosition(layout, target.anchor);
56
+ if (!previous) {
57
+ return {
58
+ currentNodeId: targetNodeId,
59
+ currentPosition: targetPosition,
60
+ destinationAnchorId: targetNodeId,
61
+ activeRoute: null,
62
+ activeStepIndex: 0,
63
+ };
64
+ }
65
+
66
+ if (!layout.building.navigation.nodes.some((node) => node.id === previous.currentNodeId)) {
67
+ return createErrorState(agent, previous, {
68
+ agentId: agent.id,
69
+ code: "stale-location-node",
70
+ message: `Agent ${agent.id} references missing building navigation node ${previous.currentNodeId}`,
71
+ fromNodeId: previous.currentNodeId,
72
+ toNodeId: targetNodeId,
73
+ });
74
+ }
75
+
76
+ if (previous.destinationAnchorId === targetNodeId && !previous.error) {
77
+ return previous;
78
+ }
79
+
80
+ try {
81
+ const route = planAgentRoute(layout.building.navigation, previous.currentNodeId, targetNodeId);
82
+ return {
83
+ currentNodeId: previous.currentNodeId,
84
+ currentPosition: previous.currentPosition,
85
+ destinationAnchorId: targetNodeId,
86
+ activeRoute: route,
87
+ activeStepIndex: 0,
88
+ };
89
+ } catch (error) {
90
+ if (error instanceof NoBuildingRouteError) {
91
+ return createErrorState(agent, previous, {
92
+ agentId: agent.id,
93
+ code: "missing-route",
94
+ message: error.message,
95
+ zoneId: agent.zoneId,
96
+ fromNodeId: previous.currentNodeId,
97
+ toNodeId: targetNodeId,
98
+ });
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ export function resolveAgentRouteTargetAnchor(
105
+ layout: ResolvedOfficeLayout,
106
+ agent: AgentGameOfficeAgent,
107
+ ): TargetAnchorResult {
108
+ if (agent.zoneId === "offstage") {
109
+ return { anchor: layout.anchors.offstage };
110
+ }
111
+
112
+ const floorOrder = new Map(layout.building.floors.map((floor, index) => [floor.id, index]));
113
+ const roomsById = new Map(layout.rooms.map((room) => [room.id, room]));
114
+ const candidates = layout.anchors.seats
115
+ .filter((anchor) => anchor.zoneId === agent.zoneId && anchor.floorId !== "offstage")
116
+ .sort((left, right) => {
117
+ if (agent.zoneId === "meeting-room") {
118
+ const leftIsAuditorium = roomsById.get(left.roomId)?.type === "auditorium";
119
+ const rightIsAuditorium = roomsById.get(right.roomId)?.type === "auditorium";
120
+ if (leftIsAuditorium !== rightIsAuditorium) {
121
+ return leftIsAuditorium ? -1 : 1;
122
+ }
123
+ }
124
+ const floorDelta = (floorOrder.get(left.floorId) ?? 0) - (floorOrder.get(right.floorId) ?? 0);
125
+ if (floorDelta !== 0) {
126
+ return floorDelta;
127
+ }
128
+ return left.id.localeCompare(right.id);
129
+ });
130
+
131
+ const anchor = candidates[0];
132
+ if (!anchor) {
133
+ return {
134
+ error: {
135
+ agentId: agent.id,
136
+ code: "missing-zone-anchor",
137
+ message: `No building navigation anchor for agent ${agent.id} zone ${agent.zoneId}`,
138
+ zoneId: agent.zoneId,
139
+ },
140
+ };
141
+ }
142
+ return { anchor };
143
+ }
144
+
145
+ export function resolveRouteTargetPosition(state: RenderAgentLocationState): Vector3Like {
146
+ const step = state.activeRoute?.steps[state.activeStepIndex];
147
+ return step?.to ?? state.currentPosition;
148
+ }
149
+
150
+ export function advanceAgentRouteState(
151
+ state: RenderAgentLocationState,
152
+ currentPosition: Vector3Like,
153
+ ): RenderAgentLocationState {
154
+ if (!state.activeRoute) {
155
+ return { ...state, currentPosition };
156
+ }
157
+ const step = state.activeRoute.steps[state.activeStepIndex];
158
+ if (!step) {
159
+ return {
160
+ ...state,
161
+ currentPosition,
162
+ currentNodeId: state.activeRoute.toNodeId,
163
+ activeRoute: null,
164
+ activeStepIndex: 0,
165
+ };
166
+ }
167
+ if (state.activeStepIndex < state.activeRoute.steps.length - 1) {
168
+ return {
169
+ ...state,
170
+ currentPosition: step.to,
171
+ currentNodeId: step.toNodeId,
172
+ activeStepIndex: state.activeStepIndex + 1,
173
+ };
174
+ }
175
+ return {
176
+ ...state,
177
+ currentPosition: step.to,
178
+ currentNodeId: step.toNodeId,
179
+ activeRoute: null,
180
+ activeStepIndex: 0,
181
+ };
182
+ }
183
+
184
+ export function resolveAgentRouteFloorId(
185
+ layout: ResolvedOfficeLayout,
186
+ state: RenderAgentLocationState,
187
+ ): string | null {
188
+ const node = layout.building.navigation.nodes.find((item) => item.id === state.currentNodeId);
189
+ return node?.floorId ?? null;
190
+ }
191
+
192
+ function createErrorState(
193
+ agent: AgentGameOfficeAgent,
194
+ previous: RenderAgentLocationState | undefined,
195
+ error: AgentNavigationError,
196
+ ): RenderAgentLocationState {
197
+ return {
198
+ currentNodeId: previous?.currentNodeId ?? "offstage",
199
+ currentPosition: previous?.currentPosition ?? { x: 0, y: 0, z: 0 },
200
+ destinationAnchorId: previous?.destinationAnchorId ?? null,
201
+ activeRoute: null,
202
+ activeStepIndex: 0,
203
+ error: { ...error, agentId: agent.id },
204
+ };
205
+ }
206
+
207
+ function resolveAnchorPosition(
208
+ layout: ResolvedOfficeLayout,
209
+ anchor: ResolvedSeatAnchor | ResolvedOffstageAnchor,
210
+ ): Vector3Like {
211
+ if (anchor.floorId === null) {
212
+ return anchor.position;
213
+ }
214
+ const floor = layout.building.floors.find((item) => item.id === anchor.floorId);
215
+ return {
216
+ x: anchor.position.x,
217
+ y: floor?.elevation ?? 0,
218
+ z: anchor.position.z,
219
+ };
220
+ }