@agent-os-lab/agent-game-sdk 0.1.8 → 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.
- package/README.md +32 -0
- package/USAGE.md +46 -2
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/office/core/projection.ts +2 -11
- package/src/office/core/types.ts +18 -0
- package/src/office/layout/config.ts +234 -25
- package/src/office/layout/index.ts +1 -0
- package/src/office/layout/navigation.ts +168 -0
- package/src/office/layout/resolver.ts +644 -13
- package/src/office/mount.ts +20 -11
- package/src/office/react/AgentGameOfficeView.ts +20 -4
- package/src/office/renderers/three/agent-body-instancing.ts +16 -8
- package/src/office/renderers/three/agent-effect-instancing.ts +26 -12
- package/src/office/renderers/three/agent-route.ts +220 -0
- package/src/office/renderers/three/mount.ts +108 -21
- package/src/office/renderers/three/scene.ts +652 -18
- package/src/runtime-agent-list.ts +101 -0
package/src/office/mount.ts
CHANGED
|
@@ -2,7 +2,10 @@ import {
|
|
|
2
2
|
createSnapshotOfficeSource,
|
|
3
3
|
resolveOfficeSource,
|
|
4
4
|
} from "./core/source";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
subscribeAgentPresenceList,
|
|
7
|
+
type AgentPresenceListSubscription,
|
|
8
|
+
} from "../runtime-agent-list";
|
|
6
9
|
import { resolveOfficeLayout } from "./layout";
|
|
7
10
|
import type {
|
|
8
11
|
AgentGameOfficeMountOptions,
|
|
@@ -29,8 +32,7 @@ export async function mountAgentGameOffice(
|
|
|
29
32
|
const source = mutableSource ?? runtimeSource ?? resolveOfficeSource(options.source);
|
|
30
33
|
let controller: AgentGameOfficeRendererController | null = null;
|
|
31
34
|
let latestSnapshot: AgentGameOfficeSnapshot | null = null;
|
|
32
|
-
let runtimeSubscription:
|
|
33
|
-
let runtimeAgents: AgentPresence[] = [];
|
|
35
|
+
let runtimeSubscription: AgentPresenceListSubscription | null = null;
|
|
34
36
|
|
|
35
37
|
const unsubscribe = source.subscribe((snapshot) => {
|
|
36
38
|
latestSnapshot = snapshot;
|
|
@@ -45,14 +47,9 @@ export async function mountAgentGameOffice(
|
|
|
45
47
|
|
|
46
48
|
if (options.source.type === "runtime" && runtimeSource) {
|
|
47
49
|
try {
|
|
48
|
-
runtimeSubscription = await options.source.client
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
runtimeSource.updateAgents(runtimeAgents);
|
|
52
|
-
},
|
|
53
|
-
onPatch: (message) => {
|
|
54
|
-
runtimeAgents = mergeAgentPresence(runtimeAgents, message.agents);
|
|
55
|
-
runtimeSource.updateAgents(runtimeAgents);
|
|
50
|
+
runtimeSubscription = await subscribeAgentPresenceList(options.source.client, {
|
|
51
|
+
onAgentsChange: (agents) => {
|
|
52
|
+
runtimeSource.updateAgents(agents);
|
|
56
53
|
},
|
|
57
54
|
});
|
|
58
55
|
} catch (error) {
|
|
@@ -65,6 +62,9 @@ export async function mountAgentGameOffice(
|
|
|
65
62
|
if (options.focusedAgentId !== undefined) {
|
|
66
63
|
controller.focusAgent?.(options.focusedAgentId);
|
|
67
64
|
}
|
|
65
|
+
if (options.focusedFloorId !== undefined) {
|
|
66
|
+
controller.focusFloor?.(options.focusedFloorId);
|
|
67
|
+
}
|
|
68
68
|
|
|
69
69
|
return {
|
|
70
70
|
updateAgents(agents: AgentPresence[]) {
|
|
@@ -75,9 +75,18 @@ export async function mountAgentGameOffice(
|
|
|
75
75
|
focusAgent(agentId) {
|
|
76
76
|
controller?.focusAgent?.(agentId);
|
|
77
77
|
},
|
|
78
|
+
focusFloor(floorId) {
|
|
79
|
+
controller?.focusFloor?.(floorId);
|
|
80
|
+
},
|
|
81
|
+
getFocusedFloor() {
|
|
82
|
+
return controller?.getFocusedFloor?.() ?? null;
|
|
83
|
+
},
|
|
78
84
|
refreshAgents() {
|
|
79
85
|
runtimeSubscription?.refresh();
|
|
80
86
|
},
|
|
87
|
+
getNavigationErrors() {
|
|
88
|
+
return controller?.getNavigationErrors?.() ?? [];
|
|
89
|
+
},
|
|
81
90
|
destroy() {
|
|
82
91
|
unsubscribe();
|
|
83
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(
|
|
79
|
-
|
|
80
|
-
|
|
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<
|
|
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
|
|
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({
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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:${
|
|
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(
|
|
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
|
+
}
|