@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.
- package/README.md +4 -0
- package/USAGE.md +8 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/office/core/types.ts +20 -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 +645 -14
- package/src/office/mount.ts +15 -0
- package/src/office/react/AgentGameOfficeView.ts +20 -4
- package/src/office/renderers/three/agent-body-instancing.ts +38 -13
- package/src/office/renderers/three/agent-effect-instancing.ts +26 -12
- package/src/office/renderers/three/agent-label.ts +3 -1
- package/src/office/renderers/three/agent-mesh.ts +15 -2
- package/src/office/renderers/three/agent-route.ts +220 -0
- package/src/office/renderers/three/mount.ts +118 -21
- package/src/office/renderers/three/scene.ts +652 -18
- package/src/runtime-agent-list.ts +15 -0
package/src/office/mount.ts
CHANGED
|
@@ -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[]) {
|
|
@@ -69,12 +72,24 @@ export async function mountAgentGameOffice(
|
|
|
69
72
|
mutableSource.updateAgents(agents);
|
|
70
73
|
}
|
|
71
74
|
},
|
|
75
|
+
resetCamera() {
|
|
76
|
+
controller?.resetCamera?.();
|
|
77
|
+
},
|
|
72
78
|
focusAgent(agentId) {
|
|
73
79
|
controller?.focusAgent?.(agentId);
|
|
74
80
|
},
|
|
81
|
+
focusFloor(floorId) {
|
|
82
|
+
controller?.focusFloor?.(floorId);
|
|
83
|
+
},
|
|
84
|
+
getFocusedFloor() {
|
|
85
|
+
return controller?.getFocusedFloor?.() ?? null;
|
|
86
|
+
},
|
|
75
87
|
refreshAgents() {
|
|
76
88
|
runtimeSubscription?.refresh();
|
|
77
89
|
},
|
|
90
|
+
getNavigationErrors() {
|
|
91
|
+
return controller?.getNavigationErrors?.() ?? [];
|
|
92
|
+
},
|
|
78
93
|
destroy() {
|
|
79
94
|
unsubscribe();
|
|
80
95
|
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 = {
|
|
@@ -17,8 +18,8 @@ type AgentBodyBatch = {
|
|
|
17
18
|
count: number;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
const geometryCache = new Map<string, THREE.
|
|
21
|
-
const materialCache = new Map<
|
|
21
|
+
const geometryCache = new Map<string, THREE.BufferGeometry>();
|
|
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,10 @@ 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
|
|
73
|
+
const shape = part.shape ?? "box";
|
|
74
|
+
const key = `${shape}:${part.width}:${part.height}:${part.depth}:${part.color}:${opacity}`;
|
|
72
75
|
const existing = this.batches.get(key);
|
|
73
76
|
if (existing && existing.capacity >= requiredCapacity) {
|
|
74
77
|
return existing;
|
|
@@ -78,8 +81,8 @@ export class AgentBodyInstancedLayer {
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
const mesh = new THREE.InstancedMesh(
|
|
81
|
-
getGeometry(part
|
|
82
|
-
getMaterial(part.color),
|
|
84
|
+
getGeometry(part),
|
|
85
|
+
getMaterial(part.color, opacity),
|
|
83
86
|
Math.max(requiredCapacity, 1),
|
|
84
87
|
);
|
|
85
88
|
mesh.castShadow = false;
|
|
@@ -98,23 +101,45 @@ export function createAgentBodyInstancedLayer(scene: THREE.Scene): AgentBodyInst
|
|
|
98
101
|
return new AgentBodyInstancedLayer(scene);
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
function getGeometry(
|
|
102
|
-
const
|
|
104
|
+
function getGeometry(part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number]): THREE.BufferGeometry {
|
|
105
|
+
const shape = part.shape ?? "box";
|
|
106
|
+
const key = `${shape}:${part.width}:${part.height}:${part.depth}`;
|
|
103
107
|
const cached = geometryCache.get(key);
|
|
104
108
|
if (cached) {
|
|
105
109
|
return cached;
|
|
106
110
|
}
|
|
107
|
-
const geometry =
|
|
111
|
+
const geometry = createGeometry(part);
|
|
108
112
|
geometryCache.set(key, geometry);
|
|
109
113
|
return geometry;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
function
|
|
113
|
-
|
|
116
|
+
function createGeometry(part: ReturnType<typeof resolveAgentBodyPartRenderSpecs>[number]): THREE.BufferGeometry {
|
|
117
|
+
switch (part.shape) {
|
|
118
|
+
case "frontPlane":
|
|
119
|
+
return new THREE.PlaneGeometry(part.width, part.height);
|
|
120
|
+
case "backPlane": {
|
|
121
|
+
const geometry = new THREE.PlaneGeometry(part.width, part.height);
|
|
122
|
+
geometry.rotateY(Math.PI);
|
|
123
|
+
return geometry;
|
|
124
|
+
}
|
|
125
|
+
case "box":
|
|
126
|
+
case undefined:
|
|
127
|
+
return new THREE.BoxGeometry(part.width, part.height, part.depth);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getMaterial(color: number, opacity: number): THREE.MeshLambertMaterial {
|
|
132
|
+
const key = `${color}:${opacity}`;
|
|
133
|
+
const cached = materialCache.get(key);
|
|
114
134
|
if (cached) {
|
|
115
135
|
return cached;
|
|
116
136
|
}
|
|
117
|
-
const material = new THREE.MeshLambertMaterial({
|
|
118
|
-
|
|
137
|
+
const material = new THREE.MeshLambertMaterial({
|
|
138
|
+
color,
|
|
139
|
+
transparent: opacity < 1,
|
|
140
|
+
opacity,
|
|
141
|
+
depthWrite: opacity >= 1,
|
|
142
|
+
});
|
|
143
|
+
materialCache.set(key, material);
|
|
119
144
|
return material;
|
|
120
145
|
}
|
|
@@ -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
|
}
|
|
@@ -2,6 +2,8 @@ import * as THREE from "three";
|
|
|
2
2
|
|
|
3
3
|
import type { AgentGameOfficeAgent } from "../../core/types";
|
|
4
4
|
|
|
5
|
+
export const AGENT_LABEL_WORLD_Y_OFFSET = 3.05;
|
|
6
|
+
|
|
5
7
|
export type AgentLabelRecord = {
|
|
6
8
|
root: HTMLDivElement;
|
|
7
9
|
title: HTMLDivElement;
|
|
@@ -69,7 +71,7 @@ export function updateAgentLabelPosition(
|
|
|
69
71
|
viewport: HTMLElement,
|
|
70
72
|
): void {
|
|
71
73
|
const projected = position.clone();
|
|
72
|
-
projected.y +=
|
|
74
|
+
projected.y += AGENT_LABEL_WORLD_Y_OFFSET;
|
|
73
75
|
projected.project(camera);
|
|
74
76
|
const x = (projected.x * 0.5 + 0.5) * viewport.clientWidth;
|
|
75
77
|
const y = (-projected.y * 0.5 + 0.5) * viewport.clientHeight;
|
|
@@ -13,6 +13,9 @@ export type AgentMeshParts = {
|
|
|
13
13
|
head: THREE.Object3D;
|
|
14
14
|
leftEye: THREE.Object3D;
|
|
15
15
|
rightEye: THREE.Object3D;
|
|
16
|
+
mouth: THREE.Object3D;
|
|
17
|
+
chestFront: THREE.Object3D;
|
|
18
|
+
backPanel: THREE.Object3D;
|
|
16
19
|
hairTop: THREE.Object3D;
|
|
17
20
|
hairBack: THREE.Object3D;
|
|
18
21
|
leftArm: THREE.Object3D;
|
|
@@ -38,6 +41,9 @@ export type AgentBodyPartKey =
|
|
|
38
41
|
| "head"
|
|
39
42
|
| "leftEye"
|
|
40
43
|
| "rightEye"
|
|
44
|
+
| "mouth"
|
|
45
|
+
| "chestFront"
|
|
46
|
+
| "backPanel"
|
|
41
47
|
| "hairTop"
|
|
42
48
|
| "hairBack"
|
|
43
49
|
| "leftArm"
|
|
@@ -45,6 +51,7 @@ export type AgentBodyPartKey =
|
|
|
45
51
|
|
|
46
52
|
export type AgentBodyPartRenderSpec = {
|
|
47
53
|
key: AgentBodyPartKey;
|
|
54
|
+
shape?: "box" | "frontPlane" | "backPlane";
|
|
48
55
|
width: number;
|
|
49
56
|
height: number;
|
|
50
57
|
depth: number;
|
|
@@ -66,8 +73,11 @@ export function resolveAgentBodyPartRenderSpecs(index: number): AgentBodyPartRen
|
|
|
66
73
|
{ key: "leftFoot", width: 0.24, height: 0.12, depth: 0.34, color: 0x111827, x: -0.14, y: 0.06, z: 0.08 },
|
|
67
74
|
{ key: "rightFoot", width: 0.24, height: 0.12, depth: 0.34, color: 0x111827, x: 0.14, y: 0.06, z: 0.08 },
|
|
68
75
|
{ key: "head", width: 0.5, height: 0.48, depth: 0.42, color: skin, x: 0, y: 1.48, z: 0 },
|
|
69
|
-
{ key: "leftEye", width: 0.06, height: 0.07, depth: 0
|
|
70
|
-
{ key: "rightEye", width: 0.06, height: 0.07, depth: 0
|
|
76
|
+
{ key: "leftEye", shape: "frontPlane", width: 0.06, height: 0.07, depth: 0, color: 0x111827, x: -0.11, y: 1.5, z: 0.235 },
|
|
77
|
+
{ key: "rightEye", shape: "frontPlane", width: 0.06, height: 0.07, depth: 0, color: 0x111827, x: 0.11, y: 1.5, z: 0.235 },
|
|
78
|
+
{ key: "mouth", shape: "frontPlane", width: 0.16, height: 0.035, depth: 0, color: 0x7f1d1d, x: 0, y: 1.38, z: 0.245 },
|
|
79
|
+
{ key: "chestFront", shape: "frontPlane", width: 0.28, height: 0.12, depth: 0, color: 0xe0f2fe, x: 0, y: 1.02, z: 0.185 },
|
|
80
|
+
{ key: "backPanel", shape: "backPlane", width: 0.36, height: 0.44, depth: 0, color: 0x0f172a, x: 0, y: 0.92, z: -0.185 },
|
|
71
81
|
{ key: "hairTop", width: 0.54, height: 0.14, depth: 0.46, color: hair, x: 0, y: 1.78, z: -0.01 },
|
|
72
82
|
{ key: "hairBack", width: 0.52, height: 0.28, depth: 0.09, color: hair, x: 0, y: 1.61, z: -0.23 },
|
|
73
83
|
{ key: "leftArm", width: 0.15, height: 0.56, depth: 0.17, color: shirt, x: -0.43, y: 0.88, z: 0 },
|
|
@@ -99,6 +109,9 @@ export function createAgentMesh(agent: AgentGameOfficeAgent, index: number): Age
|
|
|
99
109
|
head: parts.head,
|
|
100
110
|
leftEye: parts.leftEye,
|
|
101
111
|
rightEye: parts.rightEye,
|
|
112
|
+
mouth: parts.mouth,
|
|
113
|
+
chestFront: parts.chestFront,
|
|
114
|
+
backPanel: parts.backPanel,
|
|
102
115
|
hairTop: parts.hairTop,
|
|
103
116
|
hairBack: parts.hairBack,
|
|
104
117
|
leftArm: parts.leftArm,
|
|
@@ -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
|
+
}
|