@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
|
@@ -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
|
|
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 {
|
|
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 =
|
|
40
|
-
export const THREE_RENDERER_INITIAL_CAMERA_ROTATION = { x: -0.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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(
|
|
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
|
|
513
|
-
const
|
|
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,
|
|
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,
|