@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.
@@ -2,8 +2,18 @@ import type { AgentGameOfficeAgent, AgentGameOfficeZoneId } from "../core/types"
2
2
  import {
3
3
  type AgentGameOfficeConfig,
4
4
  type AgentGameOfficeRoomType,
5
+ type NormalizedOfficeConnectorConfig,
6
+ type NormalizedOfficeFloorConfig,
7
+ type NormalizedOfficeRoomConfig,
5
8
  normalizeOfficeConfig,
6
9
  } from "./config";
10
+ import type {
11
+ NavigationEdge,
12
+ NavigationNode,
13
+ ResolvedBuildingNavigationGraph,
14
+ } from "./navigation";
15
+
16
+ export const FLOOR_ELEVATION_STEP = 20;
7
17
 
8
18
  const OFFICE_LAYOUT_SCALE = 1.2;
9
19
  const OFFICE_FLOOR_CENTER_X = 16;
@@ -18,12 +28,25 @@ const LARGE_MEETING_CHAIR_DISTANCE_Z = 2.55 * OFFICE_LAYOUT_SCALE;
18
28
  const ROOMS_PER_ROW = 3;
19
29
  const ROOM_ROW_GAP = 0;
20
30
  const NORTH_WALL_BOOKCASE_Z_OFFSET = 0.32;
31
+ const ELEVATOR_CORE_MARGIN_X = 3.2;
32
+ const ELEVATOR_CORE_MARGIN_Z = 3.2;
33
+ const ELEVATOR_ENTRY_OFFSET_Z = 1.6;
21
34
 
22
35
  export type OfficeComponentKind =
23
36
  | "desk"
24
37
  | "officeChair"
25
38
  | "monitor"
26
39
  | "meetingTable"
40
+ | "diningTable"
41
+ | "cafeCounter"
42
+ | "cafeTable"
43
+ | "kitchenCounter"
44
+ | "kitchenStove"
45
+ | "kitchenFridge"
46
+ | "gardenPlanter"
47
+ | "gardenTree"
48
+ | "gardenBench"
49
+ | "gardenPath"
27
50
  | "meetingGlassRoom"
28
51
  | "largeMeetingTable"
29
52
  | "tableCup"
@@ -46,11 +69,15 @@ export type OfficeComponentKind =
46
69
  | "dumbbellRack"
47
70
  | "weightBench"
48
71
  | "yogaMat"
49
- | "gymMirror";
72
+ | "gymMirror"
73
+ | "elevatorDoor"
74
+ | "elevatorFrame"
75
+ | "elevatorIndicator";
50
76
 
51
77
  export type ResolvedOfficeComponentInstance = {
52
78
  id: string;
53
79
  kind: OfficeComponentKind;
80
+ floorId: string;
54
81
  roomId: string | null;
55
82
  position: { x: number; y?: number; z: number };
56
83
  rotation?: number;
@@ -75,6 +102,7 @@ export type ResolvedOfficeComponentInstance = {
75
102
 
76
103
  export type ResolvedOfficeRoom = {
77
104
  id: string;
105
+ floorId: string;
78
106
  type: AgentGameOfficeRoomType;
79
107
  bounds: { x: number; z: number; width: number; depth: number };
80
108
  capacity: number;
@@ -82,6 +110,7 @@ export type ResolvedOfficeRoom = {
82
110
 
83
111
  export type ResolvedFloorTile = {
84
112
  id: string;
113
+ floorId: string;
85
114
  color: number;
86
115
  x: number;
87
116
  z: number;
@@ -93,6 +122,7 @@ export type ResolvedWallSide = "north" | "south" | "west" | "east";
93
122
 
94
123
  export type ResolvedWall = {
95
124
  id: string;
125
+ floorId: string;
96
126
  roomId: string;
97
127
  side: ResolvedWallSide;
98
128
  x: number;
@@ -114,6 +144,7 @@ export type ResolvedLight = {
114
144
  export type ResolvedSeatAnchor = {
115
145
  id: string;
116
146
  roomId: string;
147
+ floorId: string;
117
148
  zoneId: AgentGameOfficeZoneId;
118
149
  position: { x: number; z: number };
119
150
  facing?: { x: number; z: number };
@@ -125,14 +156,56 @@ export type ResolvedZoneAnchor = ResolvedSeatAnchor;
125
156
  export type ResolvedWallAnchor = {
126
157
  id: string;
127
158
  roomId: string;
159
+ floorId: string;
128
160
  position: { x: number; z: number };
129
161
  rotation?: number;
130
162
  faceDirection: -1 | 1;
131
163
  };
132
164
 
165
+ export type ResolvedOffstageAnchor = {
166
+ id: "offstage";
167
+ roomId: null;
168
+ floorId: null;
169
+ zoneId: "offstage";
170
+ position: { x: number; y: number; z: number };
171
+ };
172
+
173
+ export type ResolvedOfficeFloor = {
174
+ id: string;
175
+ name: string;
176
+ level: number;
177
+ elevation: number;
178
+ rooms: ResolvedOfficeRoom[];
179
+ floorTiles: ResolvedFloorTile[];
180
+ walls: ResolvedWall[];
181
+ components: ResolvedOfficeComponentInstance[];
182
+ };
183
+
184
+ export type ResolvedElevatorStop = {
185
+ floorId: string;
186
+ entryNodeId: string;
187
+ cabinNodeId: string;
188
+ entryPosition: { x: number; y: number; z: number };
189
+ cabinPosition: { x: number; y: number; z: number };
190
+ };
191
+
192
+ export type ResolvedBuildingConnector = {
193
+ id: string;
194
+ type: "elevator";
195
+ name: string;
196
+ serves: string[];
197
+ stops: ResolvedElevatorStop[];
198
+ };
199
+
133
200
  export type ResolvedOfficeLayout = {
201
+ building: {
202
+ floors: ResolvedOfficeFloor[];
203
+ connectors: ResolvedBuildingConnector[];
204
+ navigation: ResolvedBuildingNavigationGraph;
205
+ };
134
206
  scene: {
135
207
  width: number;
208
+ height: number;
136
209
  depth: number;
137
210
  center: { x: number; z: number };
138
211
  floorTileSize: number;
@@ -146,7 +219,7 @@ export type ResolvedOfficeLayout = {
146
219
  seats: ResolvedSeatAnchor[];
147
220
  zones: ResolvedZoneAnchor[];
148
221
  wallDecorations: ResolvedWallAnchor[];
149
- offstage: ResolvedSeatAnchor;
222
+ offstage: ResolvedOffstageAnchor;
150
223
  };
151
224
  };
152
225
 
@@ -159,6 +232,15 @@ type MutableResolvedLayout = ResolvedOfficeLayout & {
159
232
  };
160
233
  };
161
234
 
235
+ type FloorResolvedLayout = ResolvedOfficeFloor & {
236
+ scene: Pick<ResolvedOfficeLayout["scene"], "width" | "depth" | "center" | "floorTileSize">;
237
+ anchors: {
238
+ seats: ResolvedSeatAnchor[];
239
+ zones: ResolvedZoneAnchor[];
240
+ wallDecorations: ResolvedWallAnchor[];
241
+ };
242
+ };
243
+
162
244
  const DESK_TEMPLATE = [
163
245
  { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(-8) } },
164
246
  { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(-8) } },
@@ -220,13 +302,30 @@ const LOUNGE_CENTER = { x: scaleOfficeX(4.8), z: scaleOfficeZ(8.2) } as const;
220
302
 
221
303
  export function resolveOfficeLayout(config?: AgentGameOfficeConfig): ResolvedOfficeLayout {
222
304
  const normalized = normalizeOfficeConfig(config);
223
- const offstage = createAnchor("offstage", "offstage-1", "offstage", "offstage", scaleOfficeX(-13), scaleOfficeZ(8), {
224
- x: scaleOfficeX(-13),
225
- z: scaleOfficeZ(8),
226
- });
305
+ const floors = normalized.building.floors.map((floor) =>
306
+ resolveOfficeFloor(floor, floor.level * FLOOR_ELEVATION_STEP, normalized.building.floors.length === 1)
307
+ );
308
+ const connectors = resolveBuildingConnectors(normalized.building.connectors, floors);
309
+ const offstage = createOffstageAnchor();
310
+ return assembleBuildingLayout(floors, connectors, offstage);
311
+ }
312
+
313
+ function resolveOfficeFloor(
314
+ floor: NormalizedOfficeFloorConfig,
315
+ elevation: number,
316
+ useLegacyRoomIds: boolean,
317
+ ): FloorResolvedLayout {
318
+ const offstage = createOffstageAnchor();
319
+ const rooms = floor.rooms.map((room, index) => createRoom(resolveRoomId(room, useLegacyRoomIds), floor.id, room.type, room.capacity, index));
227
320
  const layout: MutableResolvedLayout = {
321
+ building: {
322
+ floors: [],
323
+ connectors: [],
324
+ navigation: { nodes: [], edges: [] },
325
+ },
228
326
  scene: {
229
327
  width: OFFICE_FLOOR_WIDTH,
328
+ height: 0,
230
329
  depth: OFFICE_FLOOR_DEPTH,
231
330
  center: { x: OFFICE_FLOOR_CENTER_X, z: 0 },
232
331
  floorTileSize: OFFICE_FLOOR_TILE_SIZE,
@@ -237,7 +336,7 @@ export function resolveOfficeLayout(config?: AgentGameOfficeConfig): ResolvedOff
237
336
  { id: "directional", kind: "directional", color: 0xffffff, intensity: 1.2, position: { x: 10, y: 18, z: 8 } },
238
337
  ],
239
338
  },
240
- rooms: normalized.rooms.map((room, index) => createRoom(room.id, room.type, room.capacity, index)),
339
+ rooms,
241
340
  components: [],
242
341
  anchors: {
243
342
  seats: [],
@@ -252,21 +351,320 @@ export function resolveOfficeLayout(config?: AgentGameOfficeConfig): ResolvedOff
252
351
  addOfficePreset(layout, room);
253
352
  } else if (room.type === "auditorium") {
254
353
  addAuditoriumPreset(layout, room);
255
- } else {
354
+ } else if (room.type === "gym") {
256
355
  addGymPreset(layout, room);
356
+ } else if (room.type === "dining") {
357
+ addDiningPreset(layout, room);
358
+ } else if (room.type === "cafe") {
359
+ addCafePreset(layout, room);
360
+ } else {
361
+ addSkyGardenPreset(layout, room);
257
362
  }
258
363
  }
259
364
 
260
365
  applyAdaptiveSceneBounds(layout);
261
- layout.anchors.seats.push(offstage);
262
- layout.anchors.zones.push(offstage);
263
- return layout;
366
+ return {
367
+ id: floor.id,
368
+ name: floor.name,
369
+ level: floor.level,
370
+ elevation,
371
+ rooms: layout.rooms,
372
+ floorTiles: layout.scene.floorTiles,
373
+ walls: layout.scene.walls,
374
+ components: layout.components,
375
+ scene: {
376
+ width: layout.scene.width,
377
+ depth: layout.scene.depth,
378
+ center: layout.scene.center,
379
+ floorTileSize: layout.scene.floorTileSize,
380
+ },
381
+ anchors: {
382
+ seats: layout.anchors.seats,
383
+ zones: layout.anchors.zones,
384
+ wallDecorations: layout.anchors.wallDecorations,
385
+ },
386
+ };
387
+ }
388
+
389
+ function resolveBuildingConnectors(
390
+ connectors: NormalizedOfficeConnectorConfig[],
391
+ floors: FloorResolvedLayout[],
392
+ ): ResolvedBuildingConnector[] {
393
+ const floorsById = new Map(floors.map((floor) => [floor.id, floor]));
394
+ return connectors.map((connector) => {
395
+ const stops = connector.serves.map((floorId) => {
396
+ const floor = floorsById.get(floorId);
397
+ if (!floor) {
398
+ throw new Error(`Agent game office connector ${connector.id} serves unresolved floor: ${floorId}`);
399
+ }
400
+ const stop = resolveElevatorStop(connector.id, floor);
401
+ addElevatorStopComponents(floor, connector.id, stop);
402
+ return stop;
403
+ });
404
+
405
+ return {
406
+ id: connector.id,
407
+ type: connector.type,
408
+ name: connector.name,
409
+ serves: connector.serves,
410
+ stops,
411
+ };
412
+ });
413
+ }
414
+
415
+ function resolveElevatorStop(
416
+ connectorId: string,
417
+ floor: ResolvedOfficeFloor,
418
+ ): ResolvedElevatorStop {
419
+ const bounds = getFloorBounds(floor);
420
+ const x = bounds.minX + ELEVATOR_CORE_MARGIN_X;
421
+ const z = bounds.minZ + ELEVATOR_CORE_MARGIN_Z;
422
+ return {
423
+ floorId: floor.id,
424
+ entryNodeId: `${connectorId}:${floor.id}:entry`,
425
+ cabinNodeId: `${connectorId}:${floor.id}:cabin`,
426
+ entryPosition: { x, y: floor.elevation, z: z + ELEVATOR_ENTRY_OFFSET_Z },
427
+ cabinPosition: { x, y: floor.elevation, z },
428
+ };
429
+ }
430
+
431
+ function addElevatorStopComponents(
432
+ floor: FloorResolvedLayout,
433
+ connectorId: string,
434
+ stop: ResolvedElevatorStop,
435
+ ) {
436
+ floor.components.push(
437
+ {
438
+ id: `${connectorId}:${floor.id}:door`,
439
+ kind: "elevatorDoor",
440
+ floorId: floor.id,
441
+ roomId: null,
442
+ position: { x: stop.cabinPosition.x, z: stop.cabinPosition.z },
443
+ props: { width: 1.5, depth: 0.18, height: 2.4 },
444
+ },
445
+ {
446
+ id: `${connectorId}:${floor.id}:frame`,
447
+ kind: "elevatorFrame",
448
+ floorId: floor.id,
449
+ roomId: null,
450
+ position: { x: stop.cabinPosition.x, z: stop.cabinPosition.z },
451
+ props: { width: 2.2, depth: 1.1, height: 2.8 },
452
+ },
453
+ {
454
+ id: `${connectorId}:${floor.id}:indicator`,
455
+ kind: "elevatorIndicator",
456
+ floorId: floor.id,
457
+ roomId: null,
458
+ position: { x: stop.cabinPosition.x - 1.35, y: 2.6, z: stop.cabinPosition.z },
459
+ props: { width: 0.45, depth: 0.08, height: 0.3 },
460
+ },
461
+ );
462
+ }
463
+
464
+ function getFloorBounds(floor: ResolvedOfficeFloor): { minX: number; maxX: number; minZ: number; maxZ: number } {
465
+ const bounds = createEmptyBounds();
466
+ for (const room of floor.rooms) {
467
+ const edges = getRoomEdges(room);
468
+ includeBounds(bounds, edges.minX, edges.minZ);
469
+ includeBounds(bounds, edges.maxX, edges.maxZ);
470
+ }
471
+ if (!Number.isFinite(bounds.minX)) {
472
+ return {
473
+ minX: OFFICE_FLOOR_CENTER_X - OFFICE_FLOOR_WIDTH / 2,
474
+ maxX: OFFICE_FLOOR_CENTER_X + OFFICE_FLOOR_WIDTH / 2,
475
+ minZ: -OFFICE_FLOOR_DEPTH / 2,
476
+ maxZ: OFFICE_FLOOR_DEPTH / 2,
477
+ };
478
+ }
479
+ return bounds;
480
+ }
481
+
482
+ function buildNavigationGraph(
483
+ floors: FloorResolvedLayout[],
484
+ connectors: ResolvedBuildingConnector[],
485
+ offstage: ResolvedOffstageAnchor,
486
+ ): ResolvedBuildingNavigationGraph {
487
+ const nodes: NavigationNode[] = [];
488
+ const edges: NavigationEdge[] = [];
489
+
490
+ for (const floor of floors) {
491
+ for (const anchor of floor.anchors.seats) {
492
+ nodes.push({
493
+ id: anchor.id,
494
+ floorId: anchor.floorId,
495
+ kind: "seat",
496
+ anchorId: anchor.id,
497
+ position: { x: anchor.position.x, y: floor.elevation, z: anchor.position.z },
498
+ });
499
+ }
500
+ for (const anchor of floor.anchors.zones) {
501
+ nodes.push({
502
+ id: `zone:${anchor.id}`,
503
+ floorId: anchor.floorId,
504
+ kind: "zone",
505
+ anchorId: anchor.id,
506
+ position: { x: anchor.position.x, y: floor.elevation, z: anchor.position.z },
507
+ });
508
+ }
509
+ }
510
+
511
+ nodes.push({
512
+ id: offstage.id,
513
+ floorId: null,
514
+ kind: "offstage",
515
+ anchorId: offstage.id,
516
+ position: offstage.position,
517
+ });
518
+
519
+ for (const connector of connectors) {
520
+ for (const stop of connector.stops) {
521
+ nodes.push(
522
+ {
523
+ id: stop.entryNodeId,
524
+ floorId: stop.floorId,
525
+ kind: "connector-entry",
526
+ connectorId: connector.id,
527
+ position: stop.entryPosition,
528
+ },
529
+ {
530
+ id: stop.cabinNodeId,
531
+ floorId: stop.floorId,
532
+ kind: "connector-cabin",
533
+ connectorId: connector.id,
534
+ position: stop.cabinPosition,
535
+ },
536
+ );
537
+ pushBidirectionalWalkEdge(edges, stop.entryNodeId, stop.cabinNodeId, stop.floorId);
538
+ }
539
+
540
+ for (let leftIndex = 0; leftIndex < connector.stops.length; leftIndex += 1) {
541
+ for (let rightIndex = leftIndex + 1; rightIndex < connector.stops.length; rightIndex += 1) {
542
+ const left = connector.stops[leftIndex]!;
543
+ const right = connector.stops[rightIndex]!;
544
+ pushBidirectionalElevatorEdge(edges, connector.id, left, right);
545
+ }
546
+ }
547
+ }
548
+
549
+ for (const floor of floors) {
550
+ const floorNodes = nodes.filter((node) =>
551
+ node.floorId === floor.id &&
552
+ (node.kind === "seat" || node.kind === "zone" || node.kind === "connector-entry")
553
+ );
554
+ for (let leftIndex = 0; leftIndex < floorNodes.length; leftIndex += 1) {
555
+ for (let rightIndex = leftIndex + 1; rightIndex < floorNodes.length; rightIndex += 1) {
556
+ pushBidirectionalWalkEdge(edges, floorNodes[leftIndex]!.id, floorNodes[rightIndex]!.id, floor.id);
557
+ }
558
+ }
559
+ }
560
+
561
+ return { nodes, edges };
562
+ }
563
+
564
+ function pushBidirectionalWalkEdge(
565
+ edges: NavigationEdge[],
566
+ left: string,
567
+ right: string,
568
+ floorId: string,
569
+ ) {
570
+ edges.push(
571
+ { kind: "walk", from: left, to: right, floorId },
572
+ { kind: "walk", from: right, to: left, floorId },
573
+ );
574
+ }
575
+
576
+ function pushBidirectionalElevatorEdge(
577
+ edges: NavigationEdge[],
578
+ connectorId: string,
579
+ left: ResolvedElevatorStop,
580
+ right: ResolvedElevatorStop,
581
+ ) {
582
+ edges.push(
583
+ {
584
+ kind: "elevator",
585
+ from: left.cabinNodeId,
586
+ to: right.cabinNodeId,
587
+ connectorId,
588
+ fromFloorId: left.floorId,
589
+ toFloorId: right.floorId,
590
+ },
591
+ {
592
+ kind: "elevator",
593
+ from: right.cabinNodeId,
594
+ to: left.cabinNodeId,
595
+ connectorId,
596
+ fromFloorId: right.floorId,
597
+ toFloorId: left.floorId,
598
+ },
599
+ );
600
+ }
601
+
602
+ function assembleBuildingLayout(
603
+ floors: FloorResolvedLayout[],
604
+ connectors: ResolvedBuildingConnector[],
605
+ offstage: ResolvedOffstageAnchor,
606
+ ): ResolvedOfficeLayout {
607
+ const rooms = floors.flatMap((floor) => floor.rooms);
608
+ const components = floors.flatMap((floor) => floor.components);
609
+ const floorTiles = floors.flatMap((floor) => floor.floorTiles);
610
+ const walls = floors.flatMap((floor) => floor.walls);
611
+ const aggregateOffstage = createAggregateOffstageAnchor(offstage);
612
+ const seats = [...floors.flatMap((floor) => floor.anchors.seats), aggregateOffstage];
613
+ const zones = [...floors.flatMap((floor) => floor.anchors.zones), aggregateOffstage];
614
+ const wallDecorations = floors.flatMap((floor) => floor.anchors.wallDecorations);
615
+ const bounds = createEmptyBounds();
616
+
617
+ for (const room of rooms) {
618
+ const edges = getRoomEdges(room);
619
+ includeBounds(bounds, edges.minX, edges.minZ);
620
+ includeBounds(bounds, edges.maxX, edges.maxZ);
621
+ }
622
+
623
+ const width = Number.isFinite(bounds.minX) ? bounds.maxX - bounds.minX : OFFICE_FLOOR_WIDTH;
624
+ const depth = Number.isFinite(bounds.minZ) ? bounds.maxZ - bounds.minZ : OFFICE_FLOOR_DEPTH;
625
+ const center = Number.isFinite(bounds.minX)
626
+ ? { x: (bounds.minX + bounds.maxX) / 2, z: (bounds.minZ + bounds.maxZ) / 2 }
627
+ : { x: OFFICE_FLOOR_CENTER_X, z: 0 };
628
+ const maxElevation = Math.max(0, ...floors.map((floor) => floor.elevation));
629
+
630
+ return {
631
+ building: {
632
+ floors,
633
+ connectors,
634
+ navigation: buildNavigationGraph(floors, connectors, offstage),
635
+ },
636
+ scene: {
637
+ width,
638
+ height: maxElevation + FLOOR_ELEVATION_STEP,
639
+ depth,
640
+ center,
641
+ floorTileSize: OFFICE_FLOOR_TILE_SIZE,
642
+ floorTiles,
643
+ walls,
644
+ lights: [
645
+ { id: "ambient", kind: "ambient", color: 0xffffff, intensity: 0.7 },
646
+ { id: "directional", kind: "directional", color: 0xffffff, intensity: 1.2, position: { x: 10, y: 18 + maxElevation, z: 8 } },
647
+ ],
648
+ },
649
+ rooms,
650
+ components,
651
+ anchors: {
652
+ seats,
653
+ zones,
654
+ wallDecorations,
655
+ offstage,
656
+ },
657
+ };
264
658
  }
265
659
 
266
660
  export function resolveOfficeAgentAnchor(
267
661
  layout: ResolvedOfficeLayout,
268
662
  agent: AgentGameOfficeAgent,
269
663
  ): ResolvedSeatAnchor {
664
+ if (agent.zoneId === "offstage") {
665
+ return layout.anchors.offstage as unknown as ResolvedSeatAnchor;
666
+ }
667
+
270
668
  const candidates = sortPreferredAnchors(layout, layout.anchors.seats
271
669
  .filter((anchor) => anchor.zoneId === agent.zoneId)
272
670
  .sort(compareAnchorId), agent.zoneId);
@@ -281,7 +679,7 @@ export function resolveOfficeAgentAnchor(
281
679
  return available[stableHash(agent.id) % available.length]!;
282
680
  }
283
681
 
284
- return layout.anchors.offstage;
682
+ return layout.anchors.offstage as unknown as ResolvedSeatAnchor;
285
683
  }
286
684
 
287
685
  function sortPreferredAnchors(
@@ -307,7 +705,7 @@ function addOfficePreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom
307
705
  addComponent(layout, room.id, "desk", `desk-${index + 1}`, slot.desk.x, slot.desk.z);
308
706
  addComponent(layout, room.id, "officeChair", `desk-chair-${index + 1}`, slot.seat.x, slot.seat.z, { rotation: slot.rotation });
309
707
  addComponent(layout, room.id, "monitor", `monitor-${index + 1}`, slot.desk.x, slot.desk.z - 0.4);
310
- addSeat(layout, room.id, `desk-${index + 1}`, "desk", slot.seat.x, slot.seat.z, { x: scaleOfficeX(-7), z: scaleOfficeZ(-5) });
708
+ addSeat(layout, room.id, `desk-${index + 1}`, "desk", slot.seat.x, slot.seat.z, { x: slot.desk.x, z: slot.desk.z - 0.4 });
311
709
  });
312
710
 
313
711
  MEETING_ROOMS.forEach((meetingRoom, roomIndex) => {
@@ -462,8 +860,164 @@ function addGymPreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
462
860
  }
463
861
  }
464
862
 
863
+ function addDiningPreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
864
+ const tableCenters = [
865
+ { x: scaleOfficeX(42), z: scaleOfficeZ(-5.2) },
866
+ { x: scaleOfficeX(50), z: scaleOfficeZ(-5.2) },
867
+ { x: scaleOfficeX(42), z: scaleOfficeZ(2.8) },
868
+ { x: scaleOfficeX(50), z: scaleOfficeZ(2.8) },
869
+ ];
870
+ const seatsPerTable = 4;
871
+ const maxSeats = Math.min(room.capacity, tableCenters.length * seatsPerTable);
872
+
873
+ tableCenters.forEach((center, tableIndex) => {
874
+ addComponent(layout, room.id, "diningTable", `dining-table-${tableIndex + 1}`, center.x, center.z);
875
+ addComponent(layout, room.id, "tableCup", `dining-cup-${tableIndex + 1}-1`, center.x - 0.45, center.z - 0.28);
876
+ addComponent(layout, room.id, "tableCup", `dining-cup-${tableIndex + 1}-2`, center.x + 0.45, center.z + 0.28);
877
+
878
+ [
879
+ { x: center.x - 1.55, z: center.z, rotation: -Math.PI / 2 },
880
+ { x: center.x + 1.55, z: center.z, rotation: Math.PI / 2 },
881
+ { x: center.x, z: center.z - 1.25, rotation: 0 },
882
+ { x: center.x, z: center.z + 1.25, rotation: Math.PI },
883
+ ].forEach((seat, seatIndex) => {
884
+ const seatNumber = tableIndex * seatsPerTable + seatIndex + 1;
885
+ if (seatNumber > maxSeats) {
886
+ return;
887
+ }
888
+ addComponent(layout, room.id, "officeChair", `dining-chair-${seatNumber}`, seat.x, seat.z, {
889
+ rotation: seat.rotation,
890
+ });
891
+ addSeat(layout, room.id, `dining-${seatNumber}`, seatNumber % 3 === 0 ? "lounge" : "pantry", seat.x, seat.z, center);
892
+ });
893
+ });
894
+
895
+ addComponent(layout, room.id, "teaCounter", "serving-counter", scaleOfficeX(58.5), scaleOfficeZ(-8.8));
896
+ addComponent(layout, room.id, "waterDispenser", "dining-water-dispenser", scaleOfficeX(61.2), scaleOfficeZ(-8.7));
897
+ addComponent(layout, room.id, "kitchenCounter", "kitchen-counter", scaleOfficeX(57.8), scaleOfficeZ(8.8));
898
+ addComponent(layout, room.id, "kitchenStove", "kitchen-stove", scaleOfficeX(61.0), scaleOfficeZ(8.7));
899
+ addComponent(layout, room.id, "kitchenFridge", "kitchen-fridge", scaleOfficeX(53.9), scaleOfficeZ(8.8));
900
+ addSeat(layout, room.id, "serving-queue-1", "pantry", scaleOfficeX(57.5), scaleOfficeZ(-6.6), {
901
+ x: scaleOfficeX(58.5),
902
+ z: scaleOfficeZ(-8.8),
903
+ });
904
+ addSeat(layout, room.id, "serving-queue-2", "pantry", scaleOfficeX(60.2), scaleOfficeZ(-6.6), {
905
+ x: scaleOfficeX(58.5),
906
+ z: scaleOfficeZ(-8.8),
907
+ });
908
+ addSeat(layout, room.id, "kitchen-prep-1", "pantry", scaleOfficeX(57.2), scaleOfficeZ(6.8), {
909
+ x: scaleOfficeX(57.8),
910
+ z: scaleOfficeZ(8.8),
911
+ });
912
+ addSeat(layout, room.id, "kitchen-prep-2", "pantry", scaleOfficeX(60.4), scaleOfficeZ(6.8), {
913
+ x: scaleOfficeX(61.0),
914
+ z: scaleOfficeZ(8.7),
915
+ });
916
+ }
917
+
918
+ function addCafePreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
919
+ const counterX = scaleOfficeX(55.7);
920
+ const counterZ = scaleOfficeZ(-8.8);
921
+ const tableCenters = [
922
+ { x: scaleOfficeX(40.5), z: scaleOfficeZ(-4.9) },
923
+ { x: scaleOfficeX(47.5), z: scaleOfficeZ(-4.9) },
924
+ { x: scaleOfficeX(40.5), z: scaleOfficeZ(3.1) },
925
+ { x: scaleOfficeX(47.5), z: scaleOfficeZ(3.1) },
926
+ ];
927
+ const seatsPerTable = 3;
928
+ const maxTableSeats = Math.min(room.capacity, tableCenters.length * seatsPerTable);
929
+
930
+ addComponent(layout, room.id, "cafeCounter", "coffee-counter", counterX, counterZ);
931
+ addComponent(layout, room.id, "waterDispenser", "cafe-water-dispenser", scaleOfficeX(60.2), scaleOfficeZ(-8.7));
932
+ addComponent(layout, room.id, "tableCup", "counter-cup-1", counterX - 0.8, counterZ - 0.15);
933
+ addComponent(layout, room.id, "tableCup", "counter-cup-2", counterX - 0.35, counterZ + 0.1);
934
+ addSeat(layout, room.id, "barista-1", "pantry", counterX - 0.4, scaleOfficeZ(-7.35), { x: counterX, z: counterZ });
935
+ addSeat(layout, room.id, "pickup-1", "pantry", counterX + 1.25, scaleOfficeZ(-6.7), { x: counterX, z: counterZ });
936
+
937
+ tableCenters.forEach((center, tableIndex) => {
938
+ addComponent(layout, room.id, "cafeTable", `cafe-table-${tableIndex + 1}`, center.x, center.z);
939
+ addComponent(layout, room.id, "tableCup", `cafe-cup-${tableIndex + 1}-1`, center.x - 0.28, center.z - 0.18);
940
+ addComponent(layout, room.id, "tableCup", `cafe-cup-${tableIndex + 1}-2`, center.x + 0.25, center.z + 0.2);
941
+
942
+ [
943
+ { x: center.x - 1.25, z: center.z, rotation: -Math.PI / 2 },
944
+ { x: center.x + 1.25, z: center.z, rotation: Math.PI / 2 },
945
+ { x: center.x, z: center.z + 1.08, rotation: Math.PI },
946
+ ].forEach((seat, seatIndex) => {
947
+ const seatNumber = tableIndex * seatsPerTable + seatIndex + 1;
948
+ if (seatNumber > maxTableSeats) {
949
+ return;
950
+ }
951
+ addComponent(layout, room.id, "officeChair", `cafe-chair-${seatNumber}`, seat.x, seat.z, {
952
+ rotation: seat.rotation,
953
+ });
954
+ addSeat(layout, room.id, `cafe-seat-${seatNumber}`, "lounge", seat.x, seat.z, center);
955
+ });
956
+ });
957
+ }
958
+
959
+ function addSkyGardenPreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
960
+ const centerX = scaleOfficeX(16);
961
+ const centerZ = scaleOfficeZ(0);
962
+ addComponent(layout, room.id, "gardenPath", "main-path", centerX, centerZ, {
963
+ props: { width: 31, depth: 3.2 },
964
+ });
965
+ addComponent(layout, room.id, "gardenPath", "cross-path", centerX, centerZ, {
966
+ rotation: Math.PI / 2,
967
+ props: { width: 14, depth: 2.6 },
968
+ });
969
+
970
+ [
971
+ { id: "north-west", x: scaleOfficeX(-7), z: scaleOfficeZ(-7.6) },
972
+ { id: "north-east", x: scaleOfficeX(39), z: scaleOfficeZ(-7.6) },
973
+ { id: "south-west", x: scaleOfficeX(-7), z: scaleOfficeZ(7.6) },
974
+ { id: "south-east", x: scaleOfficeX(39), z: scaleOfficeZ(7.6) },
975
+ ].forEach((planter) => {
976
+ addComponent(layout, room.id, "gardenPlanter", `planter-${planter.id}`, planter.x, planter.z);
977
+ });
978
+
979
+ [
980
+ { id: "tree-1", x: scaleOfficeX(2), z: scaleOfficeZ(-5.4) },
981
+ { id: "tree-2", x: scaleOfficeX(30), z: scaleOfficeZ(-5.4) },
982
+ { id: "tree-3", x: scaleOfficeX(2), z: scaleOfficeZ(5.4) },
983
+ { id: "tree-4", x: scaleOfficeX(30), z: scaleOfficeZ(5.4) },
984
+ ].forEach((tree) => {
985
+ addComponent(layout, room.id, "gardenTree", tree.id, tree.x, tree.z);
986
+ });
987
+
988
+ [
989
+ { id: "bench-north", x: centerX, z: scaleOfficeZ(-5.7), rotation: 0 },
990
+ { id: "bench-south", x: centerX, z: scaleOfficeZ(5.7), rotation: Math.PI },
991
+ { id: "bench-west", x: scaleOfficeX(5.5), z: centerZ, rotation: -Math.PI / 2 },
992
+ { id: "bench-east", x: scaleOfficeX(26.5), z: centerZ, rotation: Math.PI / 2 },
993
+ ].forEach((bench, index) => {
994
+ addComponent(layout, room.id, "gardenBench", bench.id, bench.x, bench.z, {
995
+ rotation: bench.rotation,
996
+ });
997
+ addSeat(layout, room.id, `garden-seat-${index + 1}`, index < 2 ? "reading" : "lounge", bench.x, bench.z, {
998
+ x: centerX,
999
+ z: centerZ,
1000
+ });
1001
+ });
1002
+
1003
+ const extraCount = Math.max(0, Math.min(room.capacity, 12) - 4);
1004
+ for (let index = 0; index < extraCount; index += 1) {
1005
+ const angle = (index / Math.max(1, extraCount)) * Math.PI * 2;
1006
+ addSeat(
1007
+ layout,
1008
+ room.id,
1009
+ `garden-open-${index + 1}`,
1010
+ index % 2 === 0 ? "lounge" : "reading",
1011
+ centerX + Math.cos(angle) * 4.2,
1012
+ centerZ + Math.sin(angle) * 3.6,
1013
+ { x: centerX, z: centerZ },
1014
+ );
1015
+ }
1016
+ }
1017
+
465
1018
  function createRoom(
466
1019
  id: string,
1020
+ floorId: string,
467
1021
  type: AgentGameOfficeRoomType,
468
1022
  capacity: number,
469
1023
  index: number,
@@ -474,6 +1028,7 @@ function createRoom(
474
1028
  const isOffice = type === "office";
475
1029
  return {
476
1030
  id,
1031
+ floorId,
477
1032
  type,
478
1033
  capacity,
479
1034
  bounds: {
@@ -519,6 +1074,7 @@ function applyAdaptiveSceneBounds(layout: MutableResolvedLayout) {
519
1074
  if (!bounds) {
520
1075
  return {
521
1076
  id: `floor-${room.id}`,
1077
+ floorId: room.floorId,
522
1078
  color: getRoomFloorColor(room.type),
523
1079
  x: center.x,
524
1080
  z: center.z,
@@ -539,6 +1095,7 @@ function applyAdaptiveSceneBounds(layout: MutableResolvedLayout) {
539
1095
  };
540
1096
  return {
541
1097
  id: `floor-${room.id}`,
1098
+ floorId: room.floorId,
542
1099
  color: getRoomFloorColor(room.type),
543
1100
  x: room.bounds.x,
544
1101
  z: room.bounds.z,
@@ -563,6 +1120,12 @@ function getRoomFloorColor(type: AgentGameOfficeRoomType): number {
563
1120
  return 0xdbe4ec;
564
1121
  case "gym":
565
1122
  return 0xd7e3d2;
1123
+ case "dining":
1124
+ return 0xeadfc8;
1125
+ case "cafe":
1126
+ return 0xe8ddcf;
1127
+ case "skyGarden":
1128
+ return 0xd9ead2;
566
1129
  }
567
1130
  }
568
1131
 
@@ -808,6 +1371,7 @@ function createRoomWall(
808
1371
  const isHorizontal = side === "north" || side === "south";
809
1372
  return {
810
1373
  id: `${room.id}:${side}-wall`,
1374
+ floorId: room.floorId,
811
1375
  roomId: room.id,
812
1376
  side,
813
1377
  color: 0xe8e0d0,
@@ -883,6 +1447,26 @@ function getComponentFootprint(component: ResolvedOfficeComponentInstance): { wi
883
1447
  return { width: 1.4, depth: 0.4 };
884
1448
  case "meetingTable":
885
1449
  return { width: 5, depth: component.props?.depth ?? 3 };
1450
+ case "diningTable":
1451
+ return { width: 2.6, depth: 1.8 };
1452
+ case "cafeCounter":
1453
+ return { width: 4.2, depth: 0.9 };
1454
+ case "cafeTable":
1455
+ return { width: 1.7, depth: 1.7 };
1456
+ case "kitchenCounter":
1457
+ return { width: 4.8, depth: 0.9 };
1458
+ case "kitchenStove":
1459
+ return { width: 1.5, depth: 0.9 };
1460
+ case "kitchenFridge":
1461
+ return { width: 1.1, depth: 0.9 };
1462
+ case "gardenPlanter":
1463
+ return { width: 4.4, depth: 1.2 };
1464
+ case "gardenTree":
1465
+ return { width: 1.8, depth: 1.8 };
1466
+ case "gardenBench":
1467
+ return { width: 3.2, depth: 1 };
1468
+ case "gardenPath":
1469
+ return { width: component.props?.width ?? 4, depth: component.props?.depth ?? 1 };
886
1470
  case "meetingGlassRoom":
887
1471
  return { width: 11.7 * OFFICE_LAYOUT_SCALE, depth: component.props?.depth ?? 8 * OFFICE_LAYOUT_SCALE };
888
1472
  case "largeMeetingTable":
@@ -925,6 +1509,12 @@ function getComponentFootprint(component: ResolvedOfficeComponentInstance): { wi
925
1509
  return { width: 1.4, depth: 3.2 };
926
1510
  case "gymMirror":
927
1511
  return { width: component.props?.width ?? 5.2, depth: 0.2 };
1512
+ case "elevatorDoor":
1513
+ return { width: component.props?.width ?? 1.5, depth: component.props?.depth ?? 0.18 };
1514
+ case "elevatorFrame":
1515
+ return { width: component.props?.width ?? 2.2, depth: component.props?.depth ?? 1.1 };
1516
+ case "elevatorIndicator":
1517
+ return { width: component.props?.width ?? 0.45, depth: component.props?.depth ?? 0.08 };
928
1518
  case "glassWall":
929
1519
  case "glassDoor":
930
1520
  return { width: component.props?.width ?? 1, depth: component.props?.depth ?? 1 };
@@ -948,6 +1538,7 @@ function addComponent(
948
1538
  layout.components.push({
949
1539
  id: `${roomId}:${id}`,
950
1540
  kind,
1541
+ floorId: resolveRoomFloorId(layout, roomId),
951
1542
  roomId,
952
1543
  position: { x, y: options.y, z },
953
1544
  rotation: options.rotation,
@@ -970,6 +1561,7 @@ function addWallDecoration(
970
1561
  layout.anchors.wallDecorations.push({
971
1562
  id: `${room.id}:${id}`,
972
1563
  roomId: room.id,
1564
+ floorId: room.floorId,
973
1565
  position: { x, z },
974
1566
  rotation,
975
1567
  faceDirection,
@@ -985,12 +1577,13 @@ function addSeat(
985
1577
  z: number,
986
1578
  facing: { x: number; z: number },
987
1579
  ) {
988
- const anchor = createAnchor(roomId, id, zoneId, roomId, x, z, facing);
1580
+ const anchor = createAnchor(resolveRoomFloorId(layout, roomId), roomId, id, zoneId, roomId, x, z, facing);
989
1581
  layout.anchors.seats.push(anchor);
990
1582
  layout.anchors.zones.push(anchor);
991
1583
  }
992
1584
 
993
1585
  function createAnchor(
1586
+ floorId: string,
994
1587
  roomId: string,
995
1588
  id: string,
996
1589
  zoneId: AgentGameOfficeZoneId,
@@ -1002,12 +1595,50 @@ function createAnchor(
1002
1595
  return {
1003
1596
  id: `${roomId}:${id}`,
1004
1597
  roomId: anchorRoomId,
1598
+ floorId,
1005
1599
  zoneId,
1006
1600
  position: { x, z },
1007
1601
  facing,
1008
1602
  };
1009
1603
  }
1010
1604
 
1605
+ function createOffstageAnchor(): ResolvedOffstageAnchor {
1606
+ return {
1607
+ id: "offstage",
1608
+ roomId: null,
1609
+ floorId: null,
1610
+ zoneId: "offstage",
1611
+ position: { x: scaleOfficeX(-13), y: 0, z: scaleOfficeZ(8) },
1612
+ };
1613
+ }
1614
+
1615
+ function createAggregateOffstageAnchor(offstage: ResolvedOffstageAnchor): ResolvedSeatAnchor {
1616
+ return {
1617
+ id: offstage.id,
1618
+ roomId: "offstage",
1619
+ floorId: "offstage",
1620
+ zoneId: offstage.zoneId,
1621
+ position: { x: offstage.position.x, z: offstage.position.z },
1622
+ facing: { x: offstage.position.x, z: offstage.position.z },
1623
+ };
1624
+ }
1625
+
1626
+ function resolveRoomId(room: NormalizedOfficeRoomConfig, useLegacyRoomIds: boolean): string {
1627
+ if (!useLegacyRoomIds) {
1628
+ return room.id;
1629
+ }
1630
+ const separatorIndex = room.id.indexOf(":");
1631
+ return separatorIndex === -1 ? room.id : room.id.slice(separatorIndex + 1);
1632
+ }
1633
+
1634
+ function resolveRoomFloorId(layout: MutableResolvedLayout, roomId: string): string {
1635
+ const floorId = layout.rooms.find((room) => room.id === roomId)?.floorId;
1636
+ if (!floorId) {
1637
+ throw new Error(`Unknown office room for floor-owned layout content: ${roomId}`);
1638
+ }
1639
+ return floorId;
1640
+ }
1641
+
1011
1642
  function compareAnchorId(left: ResolvedSeatAnchor, right: ResolvedSeatAnchor): number {
1012
1643
  return left.id.localeCompare(right.id);
1013
1644
  }