@agent-os-lab/agent-game-sdk 0.1.3 → 0.1.4

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.
@@ -0,0 +1,978 @@
1
+ import type { AgentGameOfficeAgent, AgentGameOfficeZoneId } from "../core/types";
2
+ import {
3
+ type AgentGameOfficeConfig,
4
+ type AgentGameOfficeRoomType,
5
+ normalizeOfficeConfig,
6
+ } from "./config";
7
+
8
+ const OFFICE_LAYOUT_SCALE = 1.2;
9
+ const OFFICE_FLOOR_CENTER_X = 16;
10
+ const OFFICE_FLOOR_WIDTH = 68 * OFFICE_LAYOUT_SCALE;
11
+ const OFFICE_FLOOR_DEPTH = 24 * OFFICE_LAYOUT_SCALE;
12
+ const OFFICE_FLOOR_TILE_SIZE = 3;
13
+ const DEFAULT_ROOM_DEPTH = 34.25;
14
+ const LARGE_MEETING_Z = 0;
15
+ const LARGE_MEETING_CHAIRS_PER_SIDE = 8;
16
+ const LARGE_MEETING_CHAIR_SPACING_X = 1.55 * OFFICE_LAYOUT_SCALE;
17
+ const LARGE_MEETING_CHAIR_DISTANCE_Z = 2.55 * OFFICE_LAYOUT_SCALE;
18
+ const ROOMS_PER_ROW = 3;
19
+ const ROOM_ROW_GAP = 0;
20
+
21
+ export type OfficeComponentKind =
22
+ | "desk"
23
+ | "officeChair"
24
+ | "monitor"
25
+ | "meetingTable"
26
+ | "meetingGlassRoom"
27
+ | "largeMeetingTable"
28
+ | "tableCup"
29
+ | "glassWall"
30
+ | "glassDoor"
31
+ | "dividerGlass"
32
+ | "wallWhiteboard"
33
+ | "wallWindow"
34
+ | "stickyNoteBoard"
35
+ | "wallClock"
36
+ | "sofa"
37
+ | "loungeSideTable"
38
+ | "bookcase"
39
+ | "teaCounter"
40
+ | "waterDispenser"
41
+ | "presentationBoard"
42
+ | "mobileDisplayStand"
43
+ | "treadmill"
44
+ | "stationaryBike"
45
+ | "dumbbellRack"
46
+ | "weightBench"
47
+ | "yogaMat"
48
+ | "gymMirror";
49
+
50
+ export type ResolvedOfficeComponentInstance = {
51
+ id: string;
52
+ kind: OfficeComponentKind;
53
+ roomId: string | null;
54
+ position: { x: number; y?: number; z: number };
55
+ rotation?: number;
56
+ scale?: number;
57
+ props?: {
58
+ width?: number;
59
+ depth?: number;
60
+ height?: number;
61
+ color?: number;
62
+ topColor?: number;
63
+ legColor?: number;
64
+ screenColor?: number;
65
+ opacity?: number;
66
+ faceDirection?: -1 | 1;
67
+ variant?: string;
68
+ roomIndex?: number;
69
+ wallSide?: "north" | "south";
70
+ omitGlassSide?: "north" | "south";
71
+ whiteboardSide?: "north" | "south";
72
+ };
73
+ };
74
+
75
+ export type ResolvedOfficeRoom = {
76
+ id: string;
77
+ type: AgentGameOfficeRoomType;
78
+ bounds: { x: number; z: number; width: number; depth: number };
79
+ capacity: number;
80
+ };
81
+
82
+ export type ResolvedFloorTile = {
83
+ id: string;
84
+ color: number;
85
+ x: number;
86
+ z: number;
87
+ width: number;
88
+ depth: number;
89
+ };
90
+
91
+ export type ResolvedWallSide = "north" | "south" | "west" | "east";
92
+
93
+ export type ResolvedWall = {
94
+ id: string;
95
+ roomId: string;
96
+ side: ResolvedWallSide;
97
+ x: number;
98
+ z: number;
99
+ width: number;
100
+ depth: number;
101
+ height: number;
102
+ color: number;
103
+ };
104
+
105
+ export type ResolvedLight = {
106
+ id: string;
107
+ kind: "ambient" | "directional";
108
+ color: number;
109
+ intensity: number;
110
+ position?: { x: number; y: number; z: number };
111
+ };
112
+
113
+ export type ResolvedSeatAnchor = {
114
+ id: string;
115
+ roomId: string;
116
+ zoneId: AgentGameOfficeZoneId;
117
+ position: { x: number; z: number };
118
+ facing?: { x: number; z: number };
119
+ capacityWeight?: number;
120
+ };
121
+
122
+ export type ResolvedZoneAnchor = ResolvedSeatAnchor;
123
+
124
+ export type ResolvedWallAnchor = {
125
+ id: string;
126
+ roomId: string;
127
+ position: { x: number; z: number };
128
+ rotation?: number;
129
+ faceDirection: -1 | 1;
130
+ };
131
+
132
+ export type ResolvedOfficeLayout = {
133
+ scene: {
134
+ width: number;
135
+ depth: number;
136
+ center: { x: number; z: number };
137
+ floorTileSize: number;
138
+ floorTiles: ResolvedFloorTile[];
139
+ walls: ResolvedWall[];
140
+ lights: ResolvedLight[];
141
+ };
142
+ rooms: ResolvedOfficeRoom[];
143
+ components: ResolvedOfficeComponentInstance[];
144
+ anchors: {
145
+ seats: ResolvedSeatAnchor[];
146
+ zones: ResolvedZoneAnchor[];
147
+ wallDecorations: ResolvedWallAnchor[];
148
+ offstage: ResolvedSeatAnchor;
149
+ };
150
+ };
151
+
152
+ type MutableResolvedLayout = ResolvedOfficeLayout & {
153
+ components: ResolvedOfficeComponentInstance[];
154
+ anchors: ResolvedOfficeLayout["anchors"] & {
155
+ seats: ResolvedSeatAnchor[];
156
+ zones: ResolvedZoneAnchor[];
157
+ wallDecorations: ResolvedWallAnchor[];
158
+ };
159
+ };
160
+
161
+ const DESK_TEMPLATE = [
162
+ { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(-8) } },
163
+ { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(-8) } },
164
+ { seat: { x: scaleOfficeX(-2), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-2), z: scaleOfficeZ(-8) } },
165
+ { seat: { x: scaleOfficeX(3), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(3), z: scaleOfficeZ(-8) } },
166
+ { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(-3) } },
167
+ { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(-3) } },
168
+ { seat: { x: scaleOfficeX(-2), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-2), z: scaleOfficeZ(-3) } },
169
+ { seat: { x: scaleOfficeX(3), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(3), z: scaleOfficeZ(-3) } },
170
+ { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(2) } },
171
+ { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(2) } },
172
+ { seat: { x: scaleOfficeX(-2), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(-2), z: scaleOfficeZ(2) } },
173
+ { seat: { x: scaleOfficeX(3), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(3), z: scaleOfficeZ(2) } },
174
+ ] as const;
175
+
176
+ const MEETING_ROOMS = [
177
+ {
178
+ center: { x: scaleOfficeX(16.5), z: scaleOfficeZ(3.6) },
179
+ seats: [
180
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(1.4), rotation: Math.PI / 2 },
181
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(2.8), rotation: Math.PI / 2 },
182
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(4.4), rotation: Math.PI / 2 },
183
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(5.8), rotation: Math.PI / 2 },
184
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(1.4), rotation: -Math.PI / 2 },
185
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(2.8), rotation: -Math.PI / 2 },
186
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(4.4), rotation: -Math.PI / 2 },
187
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(5.8), rotation: -Math.PI / 2 },
188
+ ],
189
+ signColor: 0x3498db,
190
+ topColor: 0xb8860b,
191
+ legColor: 0x8b6914,
192
+ tableDepth: 6.4,
193
+ roomDepth: 9.6 * OFFICE_LAYOUT_SCALE,
194
+ wallSide: undefined,
195
+ omitGlassSide: "north",
196
+ whiteboardSide: "north",
197
+ },
198
+ {
199
+ center: { x: scaleOfficeX(16.5), z: scaleOfficeZ(-7.25) },
200
+ seats: [
201
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(-8.25), rotation: Math.PI / 2 },
202
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(-6.25), rotation: Math.PI / 2 },
203
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(-8.25), rotation: -Math.PI / 2 },
204
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(-6.25), rotation: -Math.PI / 2 },
205
+ ],
206
+ signColor: 0xe67e22,
207
+ topColor: 0xa0522d,
208
+ legColor: 0x8b4513,
209
+ tableDepth: 3,
210
+ roomDepth: 7.8 * OFFICE_LAYOUT_SCALE,
211
+ wallSide: "north",
212
+ omitGlassSide: "north",
213
+ whiteboardSide: "north",
214
+ },
215
+ ] as const;
216
+
217
+ const LARGE_MEETING_X = (scaleOfficeX(24) + (OFFICE_FLOOR_CENTER_X + OFFICE_FLOOR_WIDTH / 2)) / 2;
218
+ const LOUNGE_CENTER = { x: scaleOfficeX(4.8), z: scaleOfficeZ(8.2) } as const;
219
+
220
+ export function resolveOfficeLayout(config?: AgentGameOfficeConfig): ResolvedOfficeLayout {
221
+ const normalized = normalizeOfficeConfig(config);
222
+ const offstage = createAnchor("offstage", "offstage-1", "offstage", "offstage", scaleOfficeX(-13), scaleOfficeZ(8), {
223
+ x: scaleOfficeX(-13),
224
+ z: scaleOfficeZ(8),
225
+ });
226
+ const layout: MutableResolvedLayout = {
227
+ scene: {
228
+ width: OFFICE_FLOOR_WIDTH,
229
+ depth: OFFICE_FLOOR_DEPTH,
230
+ center: { x: OFFICE_FLOOR_CENTER_X, z: 0 },
231
+ floorTileSize: OFFICE_FLOOR_TILE_SIZE,
232
+ floorTiles: [],
233
+ walls: [],
234
+ lights: [
235
+ { id: "ambient", kind: "ambient", color: 0xffffff, intensity: 0.7 },
236
+ { id: "directional", kind: "directional", color: 0xffffff, intensity: 1.2, position: { x: 10, y: 18, z: 8 } },
237
+ ],
238
+ },
239
+ rooms: normalized.rooms.map((room, index) => createRoom(room.id, room.type, room.capacity, index)),
240
+ components: [],
241
+ anchors: {
242
+ seats: [],
243
+ zones: [],
244
+ wallDecorations: [],
245
+ offstage,
246
+ },
247
+ };
248
+
249
+ for (const room of layout.rooms) {
250
+ if (room.type === "office") {
251
+ addOfficePreset(layout, room);
252
+ } else if (room.type === "auditorium") {
253
+ addAuditoriumPreset(layout, room);
254
+ } else {
255
+ addGymPreset(layout, room);
256
+ }
257
+ }
258
+
259
+ applyAdaptiveSceneBounds(layout);
260
+ layout.anchors.seats.push(offstage);
261
+ layout.anchors.zones.push(offstage);
262
+ return layout;
263
+ }
264
+
265
+ export function resolveOfficeAgentAnchor(
266
+ layout: ResolvedOfficeLayout,
267
+ agent: AgentGameOfficeAgent,
268
+ ): ResolvedSeatAnchor {
269
+ const candidates = sortPreferredAnchors(layout, layout.anchors.seats
270
+ .filter((anchor) => anchor.zoneId === agent.zoneId)
271
+ .sort(compareAnchorId), agent.zoneId);
272
+ if (candidates.length > 0) {
273
+ return candidates[stableHash(agent.id) % candidates.length]!;
274
+ }
275
+
276
+ const available = layout.anchors.seats
277
+ .filter((anchor) => anchor.zoneId !== "offstage")
278
+ .sort(compareAnchorId);
279
+ if (available.length > 0) {
280
+ return available[stableHash(agent.id) % available.length]!;
281
+ }
282
+
283
+ return layout.anchors.offstage;
284
+ }
285
+
286
+ function sortPreferredAnchors(
287
+ layout: ResolvedOfficeLayout,
288
+ anchors: ResolvedSeatAnchor[],
289
+ zoneId: AgentGameOfficeZoneId,
290
+ ): ResolvedSeatAnchor[] {
291
+ if (zoneId !== "meeting-room") {
292
+ return anchors;
293
+ }
294
+ const roomTypes = new Map(layout.rooms.map((room) => [room.id, room.type]));
295
+ const auditoriumAnchors = anchors.filter((anchor) => roomTypes.get(anchor.roomId) === "auditorium");
296
+ return auditoriumAnchors.length > 0 ? auditoriumAnchors : anchors;
297
+ }
298
+
299
+ function addOfficePreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
300
+ const northWallFaceZ = -OFFICE_FLOOR_DEPTH / 2 + 0.28;
301
+ addWallDecoration(layout, room, "wallWhiteboard", "office-whiteboard", scaleOfficeX(3.8), northWallFaceZ, 0, 1);
302
+ addWallDecoration(layout, room, "stickyNoteBoard", "sticky-notes", scaleOfficeX(-11.5), northWallFaceZ, 0, 1);
303
+ addWallDecoration(layout, room, "wallClock", "wall-clock", scaleOfficeX(-2.2), northWallFaceZ, 0, 1);
304
+
305
+ DESK_TEMPLATE.forEach((slot, index) => {
306
+ addComponent(layout, room.id, "desk", `desk-${index + 1}`, slot.desk.x, slot.desk.z);
307
+ addComponent(layout, room.id, "officeChair", `desk-chair-${index + 1}`, slot.seat.x, slot.seat.z, { rotation: slot.rotation });
308
+ addComponent(layout, room.id, "monitor", `monitor-${index + 1}`, slot.desk.x, slot.desk.z - 0.4);
309
+ addSeat(layout, room.id, `desk-${index + 1}`, "desk", slot.seat.x, slot.seat.z, { x: scaleOfficeX(-7), z: scaleOfficeZ(-5) });
310
+ });
311
+
312
+ MEETING_ROOMS.forEach((meetingRoom, roomIndex) => {
313
+ addComponent(layout, room.id, "meetingGlassRoom", `meeting-glass-room-${roomIndex + 1}`, meetingRoom.center.x, meetingRoom.center.z, {
314
+ props: {
315
+ roomIndex,
316
+ depth: meetingRoom.roomDepth,
317
+ wallSide: meetingRoom.wallSide,
318
+ omitGlassSide: meetingRoom.omitGlassSide,
319
+ whiteboardSide: meetingRoom.whiteboardSide,
320
+ },
321
+ });
322
+ addComponent(layout, room.id, "meetingTable", `meeting-table-${roomIndex + 1}`, meetingRoom.center.x, meetingRoom.center.z, {
323
+ props: {
324
+ topColor: meetingRoom.topColor,
325
+ legColor: meetingRoom.legColor,
326
+ depth: meetingRoom.tableDepth,
327
+ },
328
+ });
329
+ meetingRoom.seats.forEach((seat, seatIndex) => {
330
+ addComponent(layout, room.id, "officeChair", `meeting-chair-${roomIndex + 1}-${seatIndex + 1}`, seat.x, seat.z, {
331
+ rotation: seat.rotation,
332
+ });
333
+ addSeat(layout, room.id, `meeting-${roomIndex + 1}-${seatIndex + 1}`, "meeting-room", seat.x, seat.z, {
334
+ x: meetingRoom.center.x,
335
+ z: meetingRoom.center.z,
336
+ });
337
+ });
338
+ });
339
+ addComponent(layout, room.id, "sofa", "lounge-sofa", LOUNGE_CENTER.x, LOUNGE_CENTER.z);
340
+ addComponent(layout, room.id, "loungeSideTable", "lounge-side-table", LOUNGE_CENTER.x - 3.05, LOUNGE_CENTER.z - 0.05);
341
+ addComponent(layout, room.id, "bookcase", "lounge-bookcase", LOUNGE_CENTER.x + 3.25, LOUNGE_CENTER.z + 0.15);
342
+ for (let index = 0; index < Math.max(4, Math.min(room.capacity, 12)); index += 1) {
343
+ addSeat(
344
+ layout,
345
+ room.id,
346
+ `lounge-${index + 1}`,
347
+ "lounge",
348
+ LOUNGE_CENTER.x - 1.7 * OFFICE_LAYOUT_SCALE + (index % 4) * 1.15 * OFFICE_LAYOUT_SCALE,
349
+ LOUNGE_CENTER.z - 0.8 * OFFICE_LAYOUT_SCALE + Math.floor(index / 4) * 0.95 * OFFICE_LAYOUT_SCALE,
350
+ { x: LOUNGE_CENTER.x, z: LOUNGE_CENTER.z },
351
+ );
352
+ }
353
+
354
+ addComponent(layout, room.id, "teaCounter", "tea-counter", scaleOfficeX(-14.8), scaleOfficeZ(10.4));
355
+ addComponent(layout, room.id, "waterDispenser", "water-dispenser", scaleOfficeX(-16.9), scaleOfficeZ(10.4));
356
+ addSeat(layout, room.id, "pantry-1", "pantry", scaleOfficeX(-9.2), scaleOfficeZ(5.2), {
357
+ x: scaleOfficeX(-9.2),
358
+ z: scaleOfficeZ(5.2),
359
+ });
360
+ addSeat(layout, room.id, "review-1", "review-area", scaleOfficeX(0.5) + 2.2 * OFFICE_LAYOUT_SCALE, scaleOfficeZ(4), {
361
+ x: scaleOfficeX(0.5),
362
+ z: scaleOfficeZ(3.05),
363
+ });
364
+ }
365
+
366
+ function addAuditoriumPreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
367
+ addComponent(layout, room.id, "largeMeetingTable", "large-meeting-table", LARGE_MEETING_X, LARGE_MEETING_Z);
368
+ for (let index = 0; index < LARGE_MEETING_CHAIRS_PER_SIDE * 2; index += 1) {
369
+ const side = index < LARGE_MEETING_CHAIRS_PER_SIDE ? -1 : 1;
370
+ const offset = (index % LARGE_MEETING_CHAIRS_PER_SIDE) - (LARGE_MEETING_CHAIRS_PER_SIDE - 1) / 2;
371
+ const x = LARGE_MEETING_X + offset * LARGE_MEETING_CHAIR_SPACING_X;
372
+ const z = LARGE_MEETING_Z + side * LARGE_MEETING_CHAIR_DISTANCE_Z;
373
+ addComponent(layout, room.id, "officeChair", `auditorium-chair-${index + 1}`, x, z, {
374
+ rotation: side === -1 ? 0 : Math.PI,
375
+ });
376
+ addComponent(layout, room.id, "tableCup", `auditorium-cup-${index + 1}`, x, LARGE_MEETING_Z + side * 1.75);
377
+ addSeat(layout, room.id, `auditorium-${index + 1}`, "meeting-room", x, z, {
378
+ x: LARGE_MEETING_X,
379
+ z: LARGE_MEETING_Z,
380
+ });
381
+ }
382
+ addComponent(layout, room.id, "officeChair", "auditorium-host-chair", LARGE_MEETING_X - 9.1, LARGE_MEETING_Z, {
383
+ rotation: Math.PI / 2,
384
+ });
385
+ addComponent(layout, room.id, "tableCup", "auditorium-host-cup", LARGE_MEETING_X - 7.6, LARGE_MEETING_Z);
386
+ addComponent(layout, room.id, "mobileDisplayStand", "auditorium-mobile-display", LARGE_MEETING_X + 9.3, LARGE_MEETING_Z, {
387
+ rotation: -Math.PI / 2,
388
+ });
389
+ addSeat(layout, room.id, "auditorium-host", "meeting-room", LARGE_MEETING_X - 9.1, LARGE_MEETING_Z, {
390
+ x: LARGE_MEETING_X,
391
+ z: LARGE_MEETING_Z,
392
+ });
393
+ }
394
+
395
+ function addGymPreset(layout: MutableResolvedLayout, room: ResolvedOfficeRoom) {
396
+ const baseX = scaleOfficeX(72);
397
+ addComponent(layout, room.id, "treadmill", "treadmill-1", baseX - 5, scaleOfficeZ(-5));
398
+ addComponent(layout, room.id, "treadmill", "treadmill-2", baseX - 1.8, scaleOfficeZ(-5));
399
+ addComponent(layout, room.id, "stationaryBike", "bike-1", baseX + 2.2, scaleOfficeZ(-5));
400
+ addComponent(layout, room.id, "dumbbellRack", "dumbbell-rack", baseX - 4.2, scaleOfficeZ(4.8));
401
+ addComponent(layout, room.id, "weightBench", "weight-bench", baseX - 0.6, scaleOfficeZ(4));
402
+ addComponent(layout, room.id, "yogaMat", "yoga-mat-1", baseX + 3.2, scaleOfficeZ(2.6), {
403
+ rotation: Math.PI / 2,
404
+ });
405
+ addComponent(layout, room.id, "yogaMat", "yoga-mat-2", baseX + 3.2, scaleOfficeZ(5.1), {
406
+ rotation: Math.PI / 2,
407
+ });
408
+ addComponent(layout, room.id, "gymMirror", "mirror-wall", baseX, scaleOfficeZ(-8), {
409
+ props: { width: 7.2, faceDirection: 1 },
410
+ });
411
+
412
+ const fixedAnchors = [
413
+ { id: "treadmill-1", x: baseX - 5, z: scaleOfficeZ(-2.5), facing: { x: baseX - 5, z: scaleOfficeZ(-5) } },
414
+ { id: "treadmill-2", x: baseX - 1.8, z: scaleOfficeZ(-2.5), facing: { x: baseX - 1.8, z: scaleOfficeZ(-5) } },
415
+ { id: "bike-1", x: baseX + 2.2, z: scaleOfficeZ(-2.8), facing: { x: baseX + 2.2, z: scaleOfficeZ(-5) } },
416
+ { id: "yoga-1", x: baseX + 3.2, z: scaleOfficeZ(2.6), facing: { x: baseX + 3.2, z: scaleOfficeZ(2.6) } },
417
+ { id: "yoga-2", x: baseX + 3.2, z: scaleOfficeZ(5.1), facing: { x: baseX + 3.2, z: scaleOfficeZ(5.1) } },
418
+ { id: "weights-1", x: baseX - 2.2, z: scaleOfficeZ(4.4), facing: { x: baseX - 4.2, z: scaleOfficeZ(4.8) } },
419
+ ];
420
+
421
+ fixedAnchors.forEach((anchor) => {
422
+ addSeat(layout, room.id, anchor.id, "lounge", anchor.x, anchor.z, anchor.facing);
423
+ });
424
+
425
+ const extraCount = Math.max(0, Math.min(room.capacity, 12) - fixedAnchors.length);
426
+ for (let index = 0; index < extraCount; index += 1) {
427
+ addSeat(
428
+ layout,
429
+ room.id,
430
+ `open-${index + 1}`,
431
+ "lounge",
432
+ baseX - 3 + index * 1.15,
433
+ scaleOfficeZ(0.2),
434
+ { x: baseX, z: scaleOfficeZ(0.2) },
435
+ );
436
+ }
437
+ }
438
+
439
+ function createRoom(
440
+ id: string,
441
+ type: AgentGameOfficeRoomType,
442
+ capacity: number,
443
+ index: number,
444
+ ): ResolvedOfficeRoom {
445
+ const floorMinX = OFFICE_FLOOR_CENTER_X - OFFICE_FLOOR_WIDTH / 2;
446
+ const splitX = scaleOfficeX(24);
447
+ const floorMaxX = OFFICE_FLOOR_CENTER_X + OFFICE_FLOOR_WIDTH / 2;
448
+ const isOffice = type === "office";
449
+ return {
450
+ id,
451
+ type,
452
+ capacity,
453
+ bounds: {
454
+ x: isOffice ? (floorMinX + splitX) / 2 : (splitX + floorMaxX) / 2 + index * OFFICE_FLOOR_WIDTH,
455
+ z: 0,
456
+ width: isOffice ? splitX - floorMinX : floorMaxX - splitX,
457
+ depth: OFFICE_FLOOR_DEPTH,
458
+ },
459
+ };
460
+ }
461
+
462
+ function applyAdaptiveSceneBounds(layout: MutableResolvedLayout) {
463
+ const roomBounds = new Map<string, { minX: number; maxX: number; minZ: number; maxZ: number }>();
464
+ const allBounds = createEmptyBounds();
465
+
466
+ for (const component of layout.components) {
467
+ if (!component.roomId) {
468
+ continue;
469
+ }
470
+ const extent = getComponentFootprint(component);
471
+ const bounds = roomBounds.get(component.roomId) ?? createEmptyBounds();
472
+ includeBounds(bounds, component.position.x - extent.width / 2, component.position.z - extent.depth / 2);
473
+ includeBounds(bounds, component.position.x + extent.width / 2, component.position.z + extent.depth / 2);
474
+ includeBounds(allBounds, component.position.x - extent.width / 2, component.position.z - extent.depth / 2);
475
+ includeBounds(allBounds, component.position.x + extent.width / 2, component.position.z + extent.depth / 2);
476
+ roomBounds.set(component.roomId, bounds);
477
+ }
478
+
479
+ const padding = 3 * OFFICE_LAYOUT_SCALE;
480
+ const sceneMinX = allBounds.minX - padding;
481
+ const sceneMaxX = allBounds.maxX + padding;
482
+ const sceneMinZ = allBounds.minZ - padding;
483
+ const sceneMaxZ = allBounds.maxZ + padding;
484
+ const width = Math.max(12 * OFFICE_LAYOUT_SCALE, sceneMaxX - sceneMinX);
485
+ const depth = Math.max(DEFAULT_ROOM_DEPTH, sceneMaxZ - sceneMinZ);
486
+ const center = { x: (sceneMinX + sceneMaxX) / 2, z: (sceneMinZ + sceneMaxZ) / 2 };
487
+
488
+ layout.scene.width = width;
489
+ layout.scene.depth = depth;
490
+ layout.scene.center = center;
491
+ layout.scene.floorTiles = layout.rooms.map((room) => {
492
+ const bounds = roomBounds.get(room.id);
493
+ if (!bounds) {
494
+ return {
495
+ id: `floor-${room.id}`,
496
+ color: getRoomFloorColor(room.type),
497
+ x: center.x,
498
+ z: center.z,
499
+ width,
500
+ depth,
501
+ };
502
+ }
503
+
504
+ const roomMinX = bounds.minX - padding;
505
+ const roomMaxX = bounds.maxX + padding;
506
+ const roomMinZ = bounds.minZ - padding;
507
+ const roomMaxZ = bounds.maxZ + padding;
508
+ room.bounds = {
509
+ x: (roomMinX + roomMaxX) / 2,
510
+ z: (roomMinZ + roomMaxZ) / 2,
511
+ width: Math.max(12 * OFFICE_LAYOUT_SCALE, roomMaxX - roomMinX),
512
+ depth: Math.max(DEFAULT_ROOM_DEPTH, roomMaxZ - roomMinZ),
513
+ };
514
+ return {
515
+ id: `floor-${room.id}`,
516
+ color: getRoomFloorColor(room.type),
517
+ x: room.bounds.x,
518
+ z: room.bounds.z,
519
+ width: room.bounds.width,
520
+ depth: room.bounds.depth,
521
+ };
522
+ });
523
+ packRoomBounds(layout);
524
+ attachMeetingRoomsToOfficeWalls(layout);
525
+ attachWallDecorationsToRoomWalls(layout);
526
+ layout.scene.walls = createRoomWalls(layout.rooms);
527
+ addRoomWindows(layout);
528
+ updateSceneBoundsFromRooms(layout);
529
+ }
530
+
531
+ function getRoomFloorColor(type: AgentGameOfficeRoomType): number {
532
+ switch (type) {
533
+ case "office":
534
+ return 0xe5e7eb;
535
+ case "auditorium":
536
+ return 0xdbe4ec;
537
+ case "gym":
538
+ return 0xd7e3d2;
539
+ }
540
+ }
541
+
542
+ function packRoomBounds(layout: MutableResolvedLayout) {
543
+ const floorTilesById = new Map(layout.scene.floorTiles.map((tile) => [tile.id, tile]));
544
+ const rows: ResolvedOfficeRoom[][] = [];
545
+ layout.rooms.forEach((room, index) => {
546
+ const row = Math.floor(index / ROOMS_PER_ROW);
547
+ rows[row] ??= [];
548
+ rows[row]!.push(room);
549
+ });
550
+
551
+ let nextRowMinZ = 0;
552
+ for (const row of rows) {
553
+ const rowDepth = Math.max(...row.map((room) => room.bounds.depth));
554
+ const rowCenterZ = nextRowMinZ + rowDepth / 2;
555
+ let nextMinX = 0;
556
+ for (const room of row) {
557
+ const previousBounds = room.bounds;
558
+ const nextCenterX = nextMinX + previousBounds.width / 2;
559
+ const deltaX = nextCenterX - previousBounds.x;
560
+ const deltaZ = rowCenterZ - previousBounds.z;
561
+ room.bounds = {
562
+ ...previousBounds,
563
+ x: nextCenterX,
564
+ z: rowCenterZ,
565
+ depth: rowDepth,
566
+ };
567
+ moveRoomContents(layout, room.id, deltaX, deltaZ);
568
+
569
+ const tile = floorTilesById.get(`floor-${room.id}`);
570
+ if (tile) {
571
+ tile.x = room.bounds.x;
572
+ tile.z = room.bounds.z;
573
+ tile.width = room.bounds.width;
574
+ tile.depth = room.bounds.depth;
575
+ }
576
+ nextMinX += previousBounds.width;
577
+ }
578
+ nextRowMinZ += rowDepth + ROOM_ROW_GAP;
579
+ }
580
+ }
581
+
582
+ function moveRoomContents(layout: MutableResolvedLayout, roomId: string, deltaX: number, deltaZ: number) {
583
+ if (nearlyEqual(deltaX, 0) && nearlyEqual(deltaZ, 0)) {
584
+ return;
585
+ }
586
+ for (const component of layout.components) {
587
+ if (component.roomId !== roomId) {
588
+ continue;
589
+ }
590
+ component.position = {
591
+ ...component.position,
592
+ x: component.position.x + deltaX,
593
+ z: component.position.z + deltaZ,
594
+ };
595
+ }
596
+
597
+ const anchors = new Set([...layout.anchors.seats, ...layout.anchors.zones, ...layout.anchors.wallDecorations]);
598
+ for (const anchor of anchors) {
599
+ if (anchor.roomId !== roomId) {
600
+ continue;
601
+ }
602
+ anchor.position = {
603
+ x: anchor.position.x + deltaX,
604
+ z: anchor.position.z + deltaZ,
605
+ };
606
+ if ("facing" in anchor && anchor.facing) {
607
+ anchor.facing = {
608
+ x: anchor.facing.x + deltaX,
609
+ z: anchor.facing.z + deltaZ,
610
+ };
611
+ }
612
+ }
613
+ }
614
+
615
+ function updateSceneBoundsFromRooms(layout: MutableResolvedLayout) {
616
+ const bounds = createEmptyBounds();
617
+ for (const room of layout.rooms) {
618
+ const edges = getRoomEdges(room);
619
+ includeBounds(bounds, edges.minX, edges.minZ);
620
+ includeBounds(bounds, edges.maxX, edges.maxZ);
621
+ }
622
+ layout.scene.width = bounds.maxX - bounds.minX;
623
+ layout.scene.depth = bounds.maxZ - bounds.minZ;
624
+ layout.scene.center = {
625
+ x: (bounds.minX + bounds.maxX) / 2,
626
+ z: (bounds.minZ + bounds.maxZ) / 2,
627
+ };
628
+ }
629
+
630
+ function attachMeetingRoomsToOfficeWalls(layout: MutableResolvedLayout) {
631
+ const officeRooms = layout.rooms.filter((room) => room.type === "office");
632
+ for (const room of officeRooms) {
633
+ const roomEdges = getRoomEdges(room);
634
+ MEETING_ROOMS.forEach((meetingRoom, index) => {
635
+ const glassRoom = layout.components.find((component) => component.id === `${room.id}:meeting-glass-room-${index + 1}`);
636
+ const currentCenter = glassRoom?.position ?? meetingRoom.center;
637
+ const northMeetingDepth = MEETING_ROOMS[1]!.roomDepth;
638
+ const targetCenterZ = index === 0
639
+ ? roomEdges.minZ + northMeetingDepth + meetingRoom.roomDepth / 2
640
+ : roomEdges.minZ + meetingRoom.roomDepth / 2;
641
+ const targetCenterX = roomEdges.maxX - (11.7 * OFFICE_LAYOUT_SCALE) / 2;
642
+ const deltaX = targetCenterX - currentCenter.x;
643
+ const deltaZ = targetCenterZ - currentCenter.z;
644
+ moveComponent(layout, room.id, `meeting-glass-room-${index + 1}`, deltaX, deltaZ);
645
+ moveComponent(layout, room.id, `meeting-table-${index + 1}`, deltaX, deltaZ);
646
+ moveAnchor(layout, room.id, `meeting-${index + 1}`, deltaX, deltaZ);
647
+
648
+ const seatCount = meetingRoom.seats.length;
649
+ for (let seatIndex = 0; seatIndex < seatCount; seatIndex += 1) {
650
+ moveComponent(layout, room.id, `meeting-chair-${index + 1}-${seatIndex + 1}`, deltaX, deltaZ);
651
+ moveAnchor(layout, room.id, `meeting-${index + 1}-${seatIndex + 1}`, deltaX, deltaZ);
652
+ }
653
+ });
654
+ }
655
+ }
656
+
657
+ function moveComponent(layout: MutableResolvedLayout, roomId: string, componentId: string, deltaX: number, deltaZ: number) {
658
+ const fullId = `${roomId}:${componentId}`;
659
+ const component = layout.components.find((item) => item.id === fullId);
660
+ if (!component) {
661
+ return;
662
+ }
663
+ component.position = {
664
+ ...component.position,
665
+ x: component.position.x + deltaX,
666
+ z: component.position.z + deltaZ,
667
+ };
668
+ }
669
+
670
+ function moveAnchor(layout: MutableResolvedLayout, roomId: string, anchorId: string, deltaX: number, deltaZ: number) {
671
+ const fullId = `${roomId}:${anchorId}`;
672
+ const anchors = new Set([...layout.anchors.seats, ...layout.anchors.zones]);
673
+ for (const anchor of anchors) {
674
+ if (anchor.id !== fullId) {
675
+ continue;
676
+ }
677
+ anchor.position = {
678
+ x: anchor.position.x + deltaX,
679
+ z: anchor.position.z + deltaZ,
680
+ };
681
+ if (anchor.facing) {
682
+ anchor.facing = {
683
+ x: anchor.facing.x + deltaX,
684
+ z: anchor.facing.z + deltaZ,
685
+ };
686
+ }
687
+ }
688
+ }
689
+
690
+ function attachWallDecorationsToRoomWalls(layout: MutableResolvedLayout) {
691
+ const roomsById = new Map(layout.rooms.map((room) => [room.id, room]));
692
+ const wallDecorationIds = new Set<string>();
693
+ for (const anchor of layout.anchors.wallDecorations) {
694
+ const room = roomsById.get(anchor.roomId);
695
+ if (!room) {
696
+ continue;
697
+ }
698
+ const northWallFaceZ = getRoomEdges(room).minZ + 0.28;
699
+ anchor.position = {
700
+ ...anchor.position,
701
+ z: northWallFaceZ,
702
+ };
703
+ wallDecorationIds.add(anchor.id);
704
+ }
705
+
706
+ for (const component of layout.components) {
707
+ if (!wallDecorationIds.has(component.id)) {
708
+ continue;
709
+ }
710
+ const room = component.roomId ? roomsById.get(component.roomId) : undefined;
711
+ if (!room) {
712
+ continue;
713
+ }
714
+ component.position = {
715
+ ...component.position,
716
+ z: getRoomEdges(room).minZ + 0.28,
717
+ };
718
+ }
719
+ }
720
+
721
+ function addRoomWindows(layout: MutableResolvedLayout) {
722
+ for (const room of layout.rooms) {
723
+ const edges = getRoomEdges(room);
724
+ const southWallZ = edges.maxZ - 0.28;
725
+ const offsets = room.bounds.width > 24 ? [-room.bounds.width * 0.25, room.bounds.width * 0.25] : [0];
726
+ offsets.forEach((offset, index) => {
727
+ addComponent(layout, room.id, "wallWindow", `south-window-${index + 1}`, room.bounds.x + offset, southWallZ, {
728
+ props: {
729
+ width: 3.1,
730
+ height: 1.25,
731
+ faceDirection: -1,
732
+ },
733
+ });
734
+ });
735
+ }
736
+ }
737
+
738
+ function createRoomWalls(rooms: ResolvedOfficeRoom[]): ResolvedWall[] {
739
+ const walls: ResolvedWall[] = [];
740
+ for (const room of rooms) {
741
+ const bounds = getRoomEdges(room);
742
+ (["north", "south", "west", "east"] as const).forEach((side) => {
743
+ if (hasAdjacentRoomWall(rooms, room, side)) {
744
+ return;
745
+ }
746
+ walls.push(createRoomWall(room, bounds, side));
747
+ });
748
+ }
749
+ return walls;
750
+ }
751
+
752
+ function createRoomWall(
753
+ room: ResolvedOfficeRoom,
754
+ bounds: ReturnType<typeof getRoomEdges>,
755
+ side: ResolvedWallSide,
756
+ ): ResolvedWall {
757
+ const isHorizontal = side === "north" || side === "south";
758
+ return {
759
+ id: `${room.id}:${side}-wall`,
760
+ roomId: room.id,
761
+ side,
762
+ color: 0xe8e0d0,
763
+ width: isHorizontal ? room.bounds.width : 0.5,
764
+ height: 2.5,
765
+ depth: isHorizontal ? 0.5 : room.bounds.depth,
766
+ x: side === "west" ? bounds.minX : side === "east" ? bounds.maxX : room.bounds.x,
767
+ z: side === "north" ? bounds.minZ : side === "south" ? bounds.maxZ : room.bounds.z,
768
+ };
769
+ }
770
+
771
+ function hasAdjacentRoomWall(
772
+ rooms: ResolvedOfficeRoom[],
773
+ room: ResolvedOfficeRoom,
774
+ side: ResolvedWallSide,
775
+ ): boolean {
776
+ const current = getRoomEdges(room);
777
+ return rooms.some((other) => {
778
+ if (other.id === room.id) {
779
+ return false;
780
+ }
781
+
782
+ const next = getRoomEdges(other);
783
+ switch (side) {
784
+ case "north":
785
+ return nearlyEqual(current.minZ, next.maxZ) && nearlyEqual(current.minX, next.minX) && nearlyEqual(current.maxX, next.maxX);
786
+ case "south":
787
+ return nearlyEqual(current.maxZ, next.minZ) && nearlyEqual(current.minX, next.minX) && nearlyEqual(current.maxX, next.maxX);
788
+ case "west":
789
+ return nearlyEqual(current.minX, next.maxX) && nearlyEqual(current.minZ, next.minZ) && nearlyEqual(current.maxZ, next.maxZ);
790
+ case "east":
791
+ return nearlyEqual(current.maxX, next.minX) && nearlyEqual(current.minZ, next.minZ) && nearlyEqual(current.maxZ, next.maxZ);
792
+ }
793
+ });
794
+ }
795
+
796
+ function getRoomEdges(room: ResolvedOfficeRoom) {
797
+ return {
798
+ minX: room.bounds.x - room.bounds.width / 2,
799
+ maxX: room.bounds.x + room.bounds.width / 2,
800
+ minZ: room.bounds.z - room.bounds.depth / 2,
801
+ maxZ: room.bounds.z + room.bounds.depth / 2,
802
+ };
803
+ }
804
+
805
+ function nearlyEqual(left: number, right: number): boolean {
806
+ return Math.abs(left - right) < 0.001;
807
+ }
808
+
809
+ function createEmptyBounds() {
810
+ return {
811
+ minX: Number.POSITIVE_INFINITY,
812
+ maxX: Number.NEGATIVE_INFINITY,
813
+ minZ: Number.POSITIVE_INFINITY,
814
+ maxZ: Number.NEGATIVE_INFINITY,
815
+ };
816
+ }
817
+
818
+ function includeBounds(bounds: ReturnType<typeof createEmptyBounds>, x: number, z: number) {
819
+ bounds.minX = Math.min(bounds.minX, x);
820
+ bounds.maxX = Math.max(bounds.maxX, x);
821
+ bounds.minZ = Math.min(bounds.minZ, z);
822
+ bounds.maxZ = Math.max(bounds.maxZ, z);
823
+ }
824
+
825
+ function getComponentFootprint(component: ResolvedOfficeComponentInstance): { width: number; depth: number } {
826
+ switch (component.kind) {
827
+ case "desk":
828
+ return { width: 3, depth: 1.8 };
829
+ case "officeChair":
830
+ return { width: 0.9, depth: 0.9 };
831
+ case "monitor":
832
+ return { width: 1.4, depth: 0.4 };
833
+ case "meetingTable":
834
+ return { width: 5, depth: component.props?.depth ?? 3 };
835
+ case "meetingGlassRoom":
836
+ return { width: 11.7 * OFFICE_LAYOUT_SCALE, depth: component.props?.depth ?? 8 * OFFICE_LAYOUT_SCALE };
837
+ case "largeMeetingTable":
838
+ return { width: 16.4, depth: 4.5 };
839
+ case "tableCup":
840
+ return { width: 0.2, depth: 0.2 };
841
+ case "dividerGlass":
842
+ return { width: 0.08, depth: 19 * OFFICE_LAYOUT_SCALE };
843
+ case "wallWhiteboard":
844
+ return { width: 3.2, depth: 0.2 };
845
+ case "wallWindow":
846
+ return { width: component.props?.width ?? 3.1, depth: 0.2 };
847
+ case "stickyNoteBoard":
848
+ return { width: 3, depth: 0.2 };
849
+ case "wallClock":
850
+ return { width: 0.9, depth: 0.2 };
851
+ case "sofa":
852
+ return { width: 4.6, depth: 1.3 };
853
+ case "loungeSideTable":
854
+ return { width: 1, depth: 0.8 };
855
+ case "bookcase":
856
+ return { width: 1.2, depth: 0.4 };
857
+ case "teaCounter":
858
+ return { width: 2.35, depth: 0.7 };
859
+ case "waterDispenser":
860
+ return { width: 0.75, depth: 0.45 };
861
+ case "presentationBoard":
862
+ return { width: 2.2, depth: 0.4 };
863
+ case "mobileDisplayStand":
864
+ return { width: 4.2, depth: 0.8 };
865
+ case "treadmill":
866
+ return { width: 2.4, depth: 4.2 };
867
+ case "stationaryBike":
868
+ return { width: 1.5, depth: 2.1 };
869
+ case "dumbbellRack":
870
+ return { width: 3.2, depth: 0.8 };
871
+ case "weightBench":
872
+ return { width: 2.2, depth: 1.1 };
873
+ case "yogaMat":
874
+ return { width: 1.4, depth: 3.2 };
875
+ case "gymMirror":
876
+ return { width: component.props?.width ?? 5.2, depth: 0.2 };
877
+ case "glassWall":
878
+ case "glassDoor":
879
+ return { width: component.props?.width ?? 1, depth: component.props?.depth ?? 1 };
880
+ }
881
+ }
882
+
883
+ function addComponent(
884
+ layout: MutableResolvedLayout,
885
+ roomId: string,
886
+ kind: OfficeComponentKind,
887
+ id: string,
888
+ x: number,
889
+ z: number,
890
+ options: {
891
+ y?: number;
892
+ rotation?: number;
893
+ scale?: number;
894
+ props?: ResolvedOfficeComponentInstance["props"];
895
+ } = {},
896
+ ) {
897
+ layout.components.push({
898
+ id: `${roomId}:${id}`,
899
+ kind,
900
+ roomId,
901
+ position: { x, y: options.y, z },
902
+ rotation: options.rotation,
903
+ scale: options.scale,
904
+ props: options.props,
905
+ });
906
+ }
907
+
908
+ function addWallDecoration(
909
+ layout: MutableResolvedLayout,
910
+ room: ResolvedOfficeRoom,
911
+ kind: Extract<OfficeComponentKind, "wallWhiteboard" | "stickyNoteBoard" | "wallClock">,
912
+ id: string,
913
+ x: number,
914
+ z: number,
915
+ rotation: number,
916
+ faceDirection: -1 | 1,
917
+ ) {
918
+ addComponent(layout, room.id, kind, id, x, z, { rotation, props: { faceDirection } });
919
+ layout.anchors.wallDecorations.push({
920
+ id: `${room.id}:${id}`,
921
+ roomId: room.id,
922
+ position: { x, z },
923
+ rotation,
924
+ faceDirection,
925
+ });
926
+ }
927
+
928
+ function addSeat(
929
+ layout: MutableResolvedLayout,
930
+ roomId: string,
931
+ id: string,
932
+ zoneId: AgentGameOfficeZoneId,
933
+ x: number,
934
+ z: number,
935
+ facing: { x: number; z: number },
936
+ ) {
937
+ const anchor = createAnchor(roomId, id, zoneId, roomId, x, z, facing);
938
+ layout.anchors.seats.push(anchor);
939
+ layout.anchors.zones.push(anchor);
940
+ }
941
+
942
+ function createAnchor(
943
+ roomId: string,
944
+ id: string,
945
+ zoneId: AgentGameOfficeZoneId,
946
+ anchorRoomId: string,
947
+ x: number,
948
+ z: number,
949
+ facing: { x: number; z: number },
950
+ ): ResolvedSeatAnchor {
951
+ return {
952
+ id: `${roomId}:${id}`,
953
+ roomId: anchorRoomId,
954
+ zoneId,
955
+ position: { x, z },
956
+ facing,
957
+ };
958
+ }
959
+
960
+ function compareAnchorId(left: ResolvedSeatAnchor, right: ResolvedSeatAnchor): number {
961
+ return left.id.localeCompare(right.id);
962
+ }
963
+
964
+ function stableHash(value: string): number {
965
+ let hash = 0;
966
+ for (let index = 0; index < value.length; index += 1) {
967
+ hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
968
+ }
969
+ return hash;
970
+ }
971
+
972
+ function scaleOfficeX(x: number): number {
973
+ return OFFICE_FLOOR_CENTER_X + (x - OFFICE_FLOOR_CENTER_X) * OFFICE_LAYOUT_SCALE;
974
+ }
975
+
976
+ function scaleOfficeZ(z: number): number {
977
+ return z * OFFICE_LAYOUT_SCALE;
978
+ }