@agent-os-lab/agent-game-sdk 0.1.1

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.
Files changed (68) hide show
  1. package/README.md +99 -0
  2. package/package.json +38 -0
  3. package/src/core/agent-game-store.ts +110 -0
  4. package/src/core/agent-service-event-adapter.ts +20 -0
  5. package/src/core/assets.ts +119 -0
  6. package/src/core/commands.ts +42 -0
  7. package/src/core/errors.ts +19 -0
  8. package/src/core/event-adapter.ts +40 -0
  9. package/src/core/index.ts +23 -0
  10. package/src/core/life-presets.ts +54 -0
  11. package/src/core/movement.ts +50 -0
  12. package/src/core/office-building-layout.ts +376 -0
  13. package/src/core/office-layout.ts +152 -0
  14. package/src/core/pixel-character-avatar.ts +87 -0
  15. package/src/core/pixel-character.ts +684 -0
  16. package/src/core/realtime-events.ts +44 -0
  17. package/src/core/realtime-transport.ts +39 -0
  18. package/src/core/reducer.ts +105 -0
  19. package/src/core/scene.ts +144 -0
  20. package/src/core/schedule.ts +20 -0
  21. package/src/core/sequence.ts +48 -0
  22. package/src/core/state.ts +26 -0
  23. package/src/core/svg-pixel-avatar.ts +372 -0
  24. package/src/core/town-office-assets.ts +109 -0
  25. package/src/core/town-office-room-presets.ts +455 -0
  26. package/src/core/town-office-seat-layout.ts +238 -0
  27. package/src/graph.ts +112 -0
  28. package/src/index.ts +2 -0
  29. package/src/office/core/projection.ts +89 -0
  30. package/src/office/core/source.ts +46 -0
  31. package/src/office/core/types.ts +110 -0
  32. package/src/office/index.ts +4 -0
  33. package/src/office/mount.ts +104 -0
  34. package/src/office/react/AgentGameOfficeView.ts +58 -0
  35. package/src/office/react/index.ts +1 -0
  36. package/src/office/renderers/three/agent-activity-effects.ts +161 -0
  37. package/src/office/renderers/three/agent-animation.ts +205 -0
  38. package/src/office/renderers/three/agent-body-instancing.ts +119 -0
  39. package/src/office/renderers/three/agent-label.ts +82 -0
  40. package/src/office/renderers/three/agent-layout.ts +72 -0
  41. package/src/office/renderers/three/agent-mesh.ts +145 -0
  42. package/src/office/renderers/three/mount.ts +253 -0
  43. package/src/office/renderers/three/scene.ts +790 -0
  44. package/src/phaser/agent-game-scene.ts +87 -0
  45. package/src/phaser/anchor-debug.ts +22 -0
  46. package/src/phaser/avatar-registry.ts +46 -0
  47. package/src/phaser/camera-controls.ts +419 -0
  48. package/src/phaser/camera-model.ts +81 -0
  49. package/src/phaser/create-agent-game.ts +242 -0
  50. package/src/phaser/debug-overlay.ts +21 -0
  51. package/src/phaser/index.ts +13 -0
  52. package/src/phaser/movement-tween.ts +59 -0
  53. package/src/phaser/office-background.ts +48 -0
  54. package/src/phaser/office-building-renderer.ts +87 -0
  55. package/src/phaser/office-layout-renderer.ts +58 -0
  56. package/src/phaser/render-layers.ts +30 -0
  57. package/src/phaser/scene-reconciler.ts +614 -0
  58. package/src/phaser/scene-renderer.ts +138 -0
  59. package/src/phaser/text-style.ts +8 -0
  60. package/src/phaser/town-office-business-props.ts +256 -0
  61. package/src/phaser/town-office-environment.ts +89 -0
  62. package/src/phaser/town-office-furniture.ts +182 -0
  63. package/src/phaser/town-office-primitives.ts +53 -0
  64. package/src/phaser/town-office-renderer.ts +429 -0
  65. package/src/phaser/types.ts +67 -0
  66. package/src/phaser/viewport.ts +88 -0
  67. package/src/runtime-client.ts +435 -0
  68. package/src/types.ts +80 -0
@@ -0,0 +1,790 @@
1
+ import * as THREE from "three";
2
+ import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
3
+
4
+ export const OFFICE_LAYOUT_SCALE = 1.2;
5
+ const OFFICE_FLOOR_CENTER_X = 16;
6
+ const OFFICE_FLOOR_WIDTH = 68 * OFFICE_LAYOUT_SCALE;
7
+ const OFFICE_FLOOR_DEPTH = 24 * OFFICE_LAYOUT_SCALE;
8
+ const OFFICE_FLOOR_TILE_SIZE = 3;
9
+
10
+ export function scaleOfficeX(x: number): number {
11
+ return OFFICE_FLOOR_CENTER_X + (x - OFFICE_FLOOR_CENTER_X) * OFFICE_LAYOUT_SCALE;
12
+ }
13
+
14
+ export function scaleOfficeZ(z: number): number {
15
+ return z * OFFICE_LAYOUT_SCALE;
16
+ }
17
+
18
+ const DESK_TEMPLATE = [
19
+ { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(-8) } },
20
+ { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(-8) } },
21
+ { seat: { x: scaleOfficeX(-2), z: scaleOfficeZ(-6.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-2), z: scaleOfficeZ(-8) } },
22
+ { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(-3) } },
23
+ { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(-3) } },
24
+ { seat: { x: scaleOfficeX(-2), z: scaleOfficeZ(-1.2) }, rotation: Math.PI, desk: { x: scaleOfficeX(-2), z: scaleOfficeZ(-3) } },
25
+ { seat: { x: scaleOfficeX(-12), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(-12), z: scaleOfficeZ(2) } },
26
+ { seat: { x: scaleOfficeX(-7), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(-7), z: scaleOfficeZ(2) } },
27
+ { seat: { x: scaleOfficeX(-2), z: scaleOfficeZ(3.8) }, rotation: Math.PI, desk: { x: scaleOfficeX(-2), z: scaleOfficeZ(2) } },
28
+ ] as const;
29
+ export const DESK_SEATS = DESK_TEMPLATE.map((slot) => slot.seat) as ReadonlyArray<{ x: number; z: number }>;
30
+ export const MEETING_ROOMS = [
31
+ {
32
+ center: { x: scaleOfficeX(16.5), z: scaleOfficeZ(3.6) },
33
+ seats: [
34
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(1.4), rotation: Math.PI / 2 },
35
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(2.8), rotation: Math.PI / 2 },
36
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(4.4), rotation: Math.PI / 2 },
37
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(5.8), rotation: Math.PI / 2 },
38
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(1.4), rotation: -Math.PI / 2 },
39
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(2.8), rotation: -Math.PI / 2 },
40
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(4.4), rotation: -Math.PI / 2 },
41
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(5.8), rotation: -Math.PI / 2 },
42
+ ],
43
+ signColor: 0x3498db,
44
+ topColor: 0xb8860b,
45
+ legColor: 0x8b6914,
46
+ tableDepth: 6.4,
47
+ },
48
+ {
49
+ center: { x: scaleOfficeX(16.5), z: scaleOfficeZ(-7.25) },
50
+ seats: [
51
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(-8.25), rotation: Math.PI / 2 },
52
+ { x: scaleOfficeX(13.7), z: scaleOfficeZ(-6.25), rotation: Math.PI / 2 },
53
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(-8.25), rotation: -Math.PI / 2 },
54
+ { x: scaleOfficeX(19.3), z: scaleOfficeZ(-6.25), rotation: -Math.PI / 2 },
55
+ ],
56
+ signColor: 0xe67e22,
57
+ topColor: 0xa0522d,
58
+ legColor: 0x8b4513,
59
+ tableDepth: 3,
60
+ },
61
+ ] as const;
62
+ export const MEETING_SEATS = MEETING_ROOMS.flatMap((room) => [...room.seats]) as ReadonlyArray<{
63
+ x: number;
64
+ z: number;
65
+ rotation: number;
66
+ }>;
67
+ export const LARGE_MEETING_X = (scaleOfficeX(24) + (OFFICE_FLOOR_CENTER_X + OFFICE_FLOOR_WIDTH / 2)) / 2;
68
+ export const LARGE_MEETING_Z = 0;
69
+ const LARGE_MEETING_CHAIRS_PER_SIDE = 8;
70
+ const LARGE_MEETING_CHAIR_SPACING_X = 1.55 * OFFICE_LAYOUT_SCALE;
71
+ const LARGE_MEETING_CHAIR_DISTANCE_Z = 2.55 * OFFICE_LAYOUT_SCALE;
72
+ export const LOUNGE_CENTER = { x: scaleOfficeX(4.8), z: scaleOfficeZ(8.2) } as const;
73
+ const matrixHelper = new THREE.Object3D();
74
+
75
+ export function resolveLargeMeetingChairRotation(side: -1 | 1): number {
76
+ return side === -1 ? 0 : Math.PI;
77
+ }
78
+
79
+ export function resolveLargeMeetingChairZ(side: -1 | 1): number {
80
+ return LARGE_MEETING_Z + side * LARGE_MEETING_CHAIR_DISTANCE_Z;
81
+ }
82
+
83
+ export type OfficeFurniturePlan = {
84
+ desks: number;
85
+ deskChairs: number;
86
+ deskChairBases: number;
87
+ deskChairCasters: number;
88
+ monitors: number;
89
+ meetingTables: number;
90
+ meetingTableLegs: number;
91
+ meetingChairs: number;
92
+ meetingChairBases: number;
93
+ meetingChairCasters: number;
94
+ presentationBoards: number;
95
+ whiteboards: number;
96
+ noteBoards: number;
97
+ stickyNotes: number;
98
+ wallClocks: number;
99
+ mobileDisplays: number;
100
+ tableCups: number;
101
+ waterDispensers: number;
102
+ teaBars: number;
103
+ sofas: number;
104
+ coffeeTables: number;
105
+ reviewStations: number;
106
+ zonePads: number;
107
+ };
108
+
109
+ export function createOfficeFurniturePlan(): OfficeFurniturePlan {
110
+ return {
111
+ desks: 9,
112
+ deskChairs: 9,
113
+ deskChairBases: 9,
114
+ deskChairCasters: 45,
115
+ monitors: 9,
116
+ meetingTables: 3,
117
+ meetingTableLegs: 18,
118
+ meetingChairs: 29,
119
+ meetingChairBases: 29,
120
+ meetingChairCasters: 145,
121
+ presentationBoards: 0,
122
+ whiteboards: 3,
123
+ noteBoards: 1,
124
+ stickyNotes: 6,
125
+ wallClocks: 1,
126
+ mobileDisplays: 1,
127
+ tableCups: 17,
128
+ waterDispensers: 1,
129
+ teaBars: 1,
130
+ sofas: 1,
131
+ coffeeTables: 0,
132
+ reviewStations: 0,
133
+ zonePads: 2,
134
+ };
135
+ }
136
+
137
+ export function addLights(scene: THREE.Scene): void {
138
+ scene.add(new THREE.AmbientLight(0xffffff, 0.7));
139
+ const directional = new THREE.DirectionalLight(0xffffff, 1.2);
140
+ directional.position.set(10, 18, 8);
141
+ directional.castShadow = false;
142
+ scene.add(directional);
143
+ }
144
+
145
+ export function buildOfficeScene(scene: THREE.Scene): void {
146
+ buildOfficeBaseFloors(scene);
147
+ buildOfficeWalls(scene);
148
+
149
+ const batcher = new BoxInstanceBatcher(scene);
150
+ const transparentCache = new TransparentBoxCache();
151
+ activeBoxBatcher = batcher;
152
+ activeTransparentBoxCache = transparentCache;
153
+ try {
154
+ buildWallDecorations(scene);
155
+ buildDeskCluster(scene);
156
+ buildMeetingRoom(scene);
157
+ buildLargeMeetingRoom(scene);
158
+ buildLounge(scene);
159
+ buildTeaBar(scene);
160
+ } finally {
161
+ activeBoxBatcher = undefined;
162
+ activeTransparentBoxCache = undefined;
163
+ }
164
+ batcher.flush();
165
+ }
166
+
167
+ export function disposeObject(object: THREE.Object3D): void {
168
+ object.traverse((child) => {
169
+ const mesh = child as THREE.Mesh;
170
+ if (mesh.geometry) {
171
+ mesh.geometry.dispose();
172
+ }
173
+ const material = mesh.material;
174
+ if (Array.isArray(material)) {
175
+ material.forEach((item) => item.dispose());
176
+ } else if (material) {
177
+ material.dispose();
178
+ }
179
+ });
180
+ }
181
+
182
+ class BoxInstanceBatcher {
183
+ private readonly scene: THREE.Scene;
184
+ private readonly geometryCache = new Map<string, THREE.BoxGeometry>();
185
+ private readonly materialCache = new Map<number, THREE.MeshLambertMaterial>();
186
+ private readonly batches = new Map<number, THREE.BufferGeometry[]>();
187
+
188
+ constructor(scene: THREE.Scene) {
189
+ this.scene = scene;
190
+ }
191
+
192
+ addBox(
193
+ color: number,
194
+ width: number,
195
+ height: number,
196
+ depth: number,
197
+ x: number,
198
+ y: number,
199
+ z: number,
200
+ rotationY: number,
201
+ ) {
202
+ const geometryKey = `${width}:${height}:${depth}`;
203
+ const geometry = this.geometryCache.get(geometryKey) ?? (() => {
204
+ const created = new THREE.BoxGeometry(width, height, depth);
205
+ this.geometryCache.set(geometryKey, created);
206
+ return created;
207
+ })();
208
+ matrixHelper.position.set(x, y, z);
209
+ matrixHelper.rotation.set(0, rotationY, 0);
210
+ matrixHelper.scale.set(1, 1, 1);
211
+ matrixHelper.updateMatrix();
212
+ const transformed = geometry.clone();
213
+ transformed.applyMatrix4(matrixHelper.matrix);
214
+
215
+ const batch = this.batches.get(color);
216
+ if (batch) {
217
+ batch.push(transformed);
218
+ return;
219
+ }
220
+ this.batches.set(color, [transformed]);
221
+ }
222
+
223
+ flush() {
224
+ this.batches.forEach((geometries, color) => {
225
+ const mergedGeometry = mergeGeometries(geometries, false);
226
+ geometries.forEach((geometry) => geometry.dispose());
227
+ if (!mergedGeometry) {
228
+ return;
229
+ }
230
+ const material = this.materialCache.get(color) ?? (() => {
231
+ const created = new THREE.MeshLambertMaterial({ color });
232
+ this.materialCache.set(color, created);
233
+ return created;
234
+ })();
235
+ const mesh = new THREE.Mesh(mergedGeometry, material);
236
+ mesh.castShadow = false;
237
+ mesh.receiveShadow = false;
238
+ mesh.name = `staticOfficeBoxes:${color}`;
239
+ this.scene.add(mesh);
240
+ });
241
+ this.geometryCache.forEach((geometry) => geometry.dispose());
242
+ this.geometryCache.clear();
243
+ }
244
+ }
245
+
246
+ let activeBoxBatcher: BoxInstanceBatcher | undefined;
247
+
248
+ class TransparentBoxCache {
249
+ private readonly geometryCache = new Map<string, THREE.BoxGeometry>();
250
+ private readonly materialCache = new Map<string, THREE.MeshLambertMaterial>();
251
+
252
+ getGeometry(width: number, height: number, depth: number): THREE.BoxGeometry {
253
+ const key = `${width}:${height}:${depth}`;
254
+ const cached = this.geometryCache.get(key);
255
+ if (cached) {
256
+ return cached;
257
+ }
258
+
259
+ const geometry = new THREE.BoxGeometry(width, height, depth);
260
+ this.geometryCache.set(key, geometry);
261
+ return geometry;
262
+ }
263
+
264
+ getMaterial(color: number, opacity: number): THREE.MeshLambertMaterial {
265
+ const key = `${color}:${opacity}`;
266
+ const cached = this.materialCache.get(key);
267
+ if (cached) {
268
+ return cached;
269
+ }
270
+
271
+ const material = new THREE.MeshLambertMaterial({
272
+ color,
273
+ transparent: true,
274
+ opacity,
275
+ });
276
+ this.materialCache.set(key, material);
277
+ return material;
278
+ }
279
+ }
280
+
281
+ let activeTransparentBoxCache: TransparentBoxCache | undefined;
282
+
283
+ function buildOfficeBaseFloors(scene: THREE.Scene) {
284
+ const floorMinX = OFFICE_FLOOR_CENTER_X - OFFICE_FLOOR_WIDTH / 2;
285
+ const floorMaxX = OFFICE_FLOOR_CENTER_X + OFFICE_FLOOR_WIDTH / 2;
286
+ const officeSplitX = scaleOfficeX(24);
287
+ addBaseFloor(scene, 0xe5e7eb, (floorMinX + officeSplitX) / 2, officeSplitX - floorMinX, OFFICE_FLOOR_DEPTH);
288
+ addBaseFloor(scene, 0xdbe4ec, (officeSplitX + floorMaxX) / 2, floorMaxX - officeSplitX, OFFICE_FLOOR_DEPTH);
289
+ }
290
+
291
+ function addBaseFloor(scene: THREE.Scene, color: number, x: number, width: number, depth: number) {
292
+ const floor = new THREE.Mesh(
293
+ new THREE.BoxGeometry(width, 0.2, depth),
294
+ new THREE.MeshLambertMaterial({ color }),
295
+ );
296
+ floor.receiveShadow = true;
297
+ floor.position.set(x, -0.1, 0);
298
+ scene.add(floor);
299
+ addGridFloorLines(scene, x, width, depth);
300
+ }
301
+
302
+ function addGridFloorLines(scene: THREE.Scene, x: number, width: number, depth: number) {
303
+ const geometry = new THREE.BufferGeometry();
304
+ const positions: number[] = [];
305
+ const minX = x - width / 2;
306
+ const maxX = x + width / 2;
307
+ const minZ = -depth / 2;
308
+ const maxZ = depth / 2;
309
+
310
+ for (let lineX = minX; lineX <= maxX + 0.001; lineX += OFFICE_FLOOR_TILE_SIZE) {
311
+ positions.push(lineX, 0.012, minZ, lineX, 0.012, maxZ);
312
+ }
313
+ for (let lineZ = minZ; lineZ <= maxZ + 0.001; lineZ += OFFICE_FLOOR_TILE_SIZE) {
314
+ positions.push(minX, 0.012, lineZ, maxX, 0.012, lineZ);
315
+ }
316
+
317
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
318
+ const lines = new THREE.LineSegments(
319
+ geometry,
320
+ new THREE.LineBasicMaterial({ color: 0x738084, transparent: true, opacity: 0.9 }),
321
+ );
322
+ lines.name = "officeGridFloorLines";
323
+ scene.add(lines);
324
+ }
325
+
326
+ function buildOfficeWalls(scene: THREE.Scene) {
327
+ const wallHeight = 2.5;
328
+ const wallThickness = 0.5;
329
+ const wallMaterial = new THREE.MeshLambertMaterial({ color: 0xe8e0d0 });
330
+ const floorMinX = OFFICE_FLOOR_CENTER_X - OFFICE_FLOOR_WIDTH / 2;
331
+ const floorMaxX = OFFICE_FLOOR_CENTER_X + OFFICE_FLOOR_WIDTH / 2;
332
+ const floorMinZ = -OFFICE_FLOOR_DEPTH / 2;
333
+
334
+ addOfficeWallBox(scene, OFFICE_FLOOR_WIDTH, wallHeight, wallThickness, OFFICE_FLOOR_CENTER_X, wallHeight / 2, floorMinZ, wallMaterial);
335
+ addOfficeWallBox(scene, wallThickness, wallHeight, OFFICE_FLOOR_DEPTH, floorMinX, wallHeight / 2, 0, wallMaterial);
336
+ addOfficeWallBox(scene, wallThickness, wallHeight, OFFICE_FLOOR_DEPTH, floorMaxX, wallHeight / 2, 0, wallMaterial);
337
+ }
338
+
339
+ function addOfficeWallBox(
340
+ scene: THREE.Scene,
341
+ width: number,
342
+ height: number,
343
+ depth: number,
344
+ x: number,
345
+ y: number,
346
+ z: number,
347
+ material: THREE.Material,
348
+ ) {
349
+ const mesh = new THREE.Mesh(
350
+ new THREE.BoxGeometry(width, height, depth),
351
+ material,
352
+ );
353
+ mesh.castShadow = false;
354
+ mesh.receiveShadow = true;
355
+ mesh.position.set(x, y, z);
356
+ scene.add(mesh);
357
+ }
358
+
359
+ function buildWallDecorations(scene: THREE.Scene) {
360
+ const northWallFaceZ = -OFFICE_FLOOR_DEPTH / 2 + 0.28;
361
+ addWallWhiteboard(scene, scaleOfficeX(3.8), northWallFaceZ, 0, 1);
362
+ addStickyNoteBoard(scene, scaleOfficeX(-11.5), northWallFaceZ);
363
+ addWallClock(scene, scaleOfficeX(-2.2), northWallFaceZ);
364
+ }
365
+
366
+ export function resolveWallWhiteboardOffsets(faceDirection: -1 | 1) {
367
+ return {
368
+ frameZ: 0.09 * faceDirection,
369
+ panelZ: 0.14 * faceDirection,
370
+ trayZ: 0.2 * faceDirection,
371
+ };
372
+ }
373
+
374
+ function addWallWhiteboard(scene: THREE.Scene, x: number, wallZ: number, rotation: number, faceDirection: -1 | 1) {
375
+ const offsets = resolveWallWhiteboardOffsets(faceDirection);
376
+ addLocalBox(scene, 0x888888, 3.2, 1.7, 0.05, x, wallZ, 0, 1.5, offsets.frameZ, rotation);
377
+ addLocalBox(scene, 0xffffff, 3, 1.5, 0.08, x, wallZ, 0, 1.5, offsets.panelZ, rotation);
378
+ addLocalBox(scene, 0x64748b, 1.2, 0.06, 0.05, x, wallZ, -0.5, 1.02, offsets.trayZ, rotation);
379
+ }
380
+
381
+ function addStickyNoteBoard(scene: THREE.Scene, x: number, z: number) {
382
+ addBox(scene, 0xb87333, 3, 1.5, 0.08, x, 1.5, z - 0.04);
383
+ addBox(scene, 0xd4a76a, 2.8, 1.3, 0.05, x, 1.5, z + 0.02);
384
+
385
+ const noteColors = [0xffff88, 0xff88aa, 0x88ff88, 0x88bbff, 0xffbb88, 0xf8fafc] as const;
386
+ for (let noteIndex = 0; noteIndex < noteColors.length; noteIndex += 1) {
387
+ addBox(
388
+ scene,
389
+ noteColors[noteIndex]!,
390
+ 0.5,
391
+ 0.5,
392
+ 0.03,
393
+ x - 0.8 + (noteIndex % 3) * 0.8,
394
+ 1.8 - Math.floor(noteIndex / 3) * 0.6,
395
+ z + 0.06,
396
+ );
397
+ }
398
+ }
399
+
400
+ function addWallClock(scene: THREE.Scene, x: number, z: number) {
401
+ addBox(scene, 0x333333, 0.9, 0.9, 0.05, x, 2, z - 0.04);
402
+ addBox(scene, 0xffffff, 0.8, 0.8, 0.08, x, 2, z + 0.02);
403
+ addBox(scene, 0x333333, 0.08, 0.32, 0.04, x, 2.1, z + 0.08);
404
+ addBox(scene, 0x333333, 0.28, 0.06, 0.04, x + 0.1, 1.98, z + 0.08);
405
+ }
406
+
407
+ function buildDeskCluster(scene: THREE.Scene) {
408
+ DESK_TEMPLATE.forEach((slot) => {
409
+ addDesk(scene, slot.desk.x, slot.desk.z);
410
+ addOfficeChair(scene, slot.seat.x, slot.seat.z, slot.rotation);
411
+ addMonitor(scene, slot.desk.x, slot.desk.z - 0.4);
412
+ });
413
+ }
414
+
415
+ function buildMeetingRoom(scene: THREE.Scene) {
416
+ MEETING_ROOMS.forEach((room, roomIndex) => {
417
+ addMeetingGlassRoom(scene, roomIndex);
418
+ addMeetingTable(scene, room.center.x, room.center.z, room.topColor, room.legColor, room.tableDepth);
419
+ room.seats.forEach((seat) => addOfficeChair(scene, seat.x, seat.z, seat.rotation));
420
+ });
421
+ addOfficeDividerGlass(scene);
422
+ }
423
+
424
+ function buildLargeMeetingRoom(scene: THREE.Scene) {
425
+ addLargeMeetingTable(scene, LARGE_MEETING_X, LARGE_MEETING_Z);
426
+ for (let index = 0; index < LARGE_MEETING_CHAIRS_PER_SIDE * 2; index += 1) {
427
+ const side = index < LARGE_MEETING_CHAIRS_PER_SIDE ? -1 : 1;
428
+ const offset = (index % LARGE_MEETING_CHAIRS_PER_SIDE) - (LARGE_MEETING_CHAIRS_PER_SIDE - 1) / 2;
429
+ addOfficeChair(
430
+ scene,
431
+ LARGE_MEETING_X + offset * LARGE_MEETING_CHAIR_SPACING_X,
432
+ resolveLargeMeetingChairZ(side),
433
+ resolveLargeMeetingChairRotation(side),
434
+ );
435
+ addTableCup(scene, LARGE_MEETING_X + offset * LARGE_MEETING_CHAIR_SPACING_X, LARGE_MEETING_Z + side * 1.75);
436
+ }
437
+ addOfficeChair(scene, LARGE_MEETING_X - 9.1, LARGE_MEETING_Z, Math.PI / 2);
438
+ addTableCup(scene, LARGE_MEETING_X - 7.6, LARGE_MEETING_Z);
439
+ addMobileDisplayStand(scene, LARGE_MEETING_X + 9.3, LARGE_MEETING_Z, -Math.PI / 2);
440
+ }
441
+
442
+ function buildLounge(scene: THREE.Scene) {
443
+ addSofa(scene, LOUNGE_CENTER.x, LOUNGE_CENTER.z);
444
+ addLoungeSideTable(scene, LOUNGE_CENTER.x - 3.05, LOUNGE_CENTER.z - 0.05);
445
+ addBookcase(scene, LOUNGE_CENTER.x + 3.25, LOUNGE_CENTER.z + 0.15);
446
+ }
447
+
448
+ function buildTeaBar(scene: THREE.Scene) {
449
+ addTeaCounter(scene, scaleOfficeX(-14.8), scaleOfficeZ(10.4));
450
+ addWaterDispenser(scene, scaleOfficeX(-16.9), scaleOfficeZ(10.4));
451
+ }
452
+
453
+ function addDesk(scene: THREE.Scene, x: number, z: number) {
454
+ addBox(scene, 0xd2b48c, 3, 0.15, 1.8, x, 1.5, z);
455
+ addBox(scene, 0x8b7355, 0.15, 1.5, 0.15, x - 1.3, 0.75, z - 0.7);
456
+ addBox(scene, 0x8b7355, 0.15, 1.5, 0.15, x + 1.3, 0.75, z - 0.7);
457
+ addBox(scene, 0x8b7355, 0.15, 1.5, 0.15, x - 1.3, 0.75, z + 0.7);
458
+ addBox(scene, 0x8b7355, 0.15, 1.5, 0.15, x + 1.3, 0.75, z + 0.7);
459
+ addBox(scene, 0x444444, 0.8, 0.05, 0.3, x, 1.6, z + 0.3);
460
+ addBox(scene, 0x555555, 0.15, 0.05, 0.2, x + 0.6, 1.6, z + 0.3);
461
+ }
462
+
463
+ function addOfficeChair(scene: THREE.Scene, x: number, z: number, rotation = 0) {
464
+ addLocalBox(scene, 0x2c3e50, 0.8, 0.1, 0.8, x, z, 0, 1, 0, rotation);
465
+ addLocalBox(scene, 0x2c3e50, 0.8, 0.9, 0.1, x, z, 0, 1.5, -0.35, rotation);
466
+ addLocalBox(scene, 0x888888, 0.1, 0.6, 0.1, x, z, 0, 0.6, 0, rotation);
467
+ for (let armIndex = 0; armIndex < 5; armIndex += 1) {
468
+ const angle = (armIndex / 5) * Math.PI * 2;
469
+ addLocalBox(scene, 0x888888, 0.6, 0.06, 0.08, x, z, 0, 0.25, 0, rotation + angle);
470
+ addLocalBox(
471
+ scene,
472
+ 0x333333,
473
+ 0.12,
474
+ 0.12,
475
+ 0.12,
476
+ x,
477
+ z,
478
+ Math.sin(angle) * 0.3,
479
+ 0.18,
480
+ Math.cos(angle) * 0.3,
481
+ rotation,
482
+ );
483
+ }
484
+ }
485
+
486
+ function addMonitor(scene: THREE.Scene, x: number, z: number) {
487
+ addBox(scene, 0x333333, 0.1, 0.4, 0.1, x, 1.8, z + 0.1);
488
+ addBox(scene, 0x333333, 0.6, 0.08, 0.4, x, 1.62, z + 0.1);
489
+ addBox(scene, 0x222222, 1.4, 1, 0.08, x, 2.5, z);
490
+ addBox(scene, 0x3498db, 1.2, 0.8, 0.02, x, 2.5, z + 0.05);
491
+ }
492
+
493
+ function addMeetingTable(
494
+ scene: THREE.Scene,
495
+ x: number,
496
+ z: number,
497
+ topColor = 0xb8860b,
498
+ legColor = 0x8b6914,
499
+ depth = 3,
500
+ ) {
501
+ const legZ = depth / 2 - 0.3;
502
+ addBox(scene, topColor, 5, 0.15, depth, x, 1.5, z);
503
+ addBox(scene, legColor, 0.2, 1.5, 0.2, x - 2.2, 0.75, z - legZ);
504
+ addBox(scene, legColor, 0.2, 1.5, 0.2, x + 2.2, 0.75, z - legZ);
505
+ addBox(scene, legColor, 0.2, 1.5, 0.2, x - 2.2, 0.75, z + legZ);
506
+ addBox(scene, legColor, 0.2, 1.5, 0.2, x + 2.2, 0.75, z + legZ);
507
+ }
508
+
509
+ function addMeetingGlassRoom(scene: THREE.Scene, roomIndex: number) {
510
+ const sharedWallZ = -2.75;
511
+ const northWallFaceZ = -OFFICE_FLOOR_DEPTH / 2 + 0.28;
512
+ if (roomIndex === 0) {
513
+ addGlassWallWithDoor(
514
+ scene,
515
+ roomIndex,
516
+ scaleOfficeX(10.5),
517
+ scaleOfficeZ((sharedWallZ + 9.6) / 2),
518
+ (9.6 - sharedWallZ) * OFFICE_LAYOUT_SCALE,
519
+ 1.9,
520
+ 0.25,
521
+ );
522
+ addTransparentBox(scene, 0x88ccee, 11.7 * OFFICE_LAYOUT_SCALE, 2.5, 0.08, scaleOfficeX(16.35), 1.25, scaleOfficeZ(sharedWallZ), 0.2);
523
+ addTransparentBox(scene, 0x88ccee, 11.7 * OFFICE_LAYOUT_SCALE, 2.5, 0.08, scaleOfficeX(16.35), 1.25, scaleOfficeZ(9.6), 0.2);
524
+ addMeetingWhiteboard(scene, scaleOfficeX(16.35), scaleOfficeZ(sharedWallZ), Math.PI);
525
+ addBox(scene, 0xcccccc, 0.15, 2.5, 0.15, scaleOfficeX(10.5), 1.25, scaleOfficeZ(sharedWallZ));
526
+ addBox(scene, 0xcccccc, 0.15, 2.5, 0.15, scaleOfficeX(10.5), 1.25, scaleOfficeZ(9.6));
527
+ return;
528
+ }
529
+
530
+ addGlassWallWithDoor(
531
+ scene,
532
+ roomIndex,
533
+ scaleOfficeX(10.5),
534
+ (northWallFaceZ + scaleOfficeZ(sharedWallZ)) / 2,
535
+ scaleOfficeZ(sharedWallZ) - northWallFaceZ,
536
+ 1.6,
537
+ 0.25,
538
+ );
539
+ addTransparentBox(scene, 0x88ccee, 11.7 * OFFICE_LAYOUT_SCALE, 2.5, 0.08, scaleOfficeX(16.35), 1.25, northWallFaceZ, 0.2);
540
+ addTransparentBox(
541
+ scene,
542
+ 0x88ccee,
543
+ 0.08,
544
+ 2.5,
545
+ scaleOfficeZ(-9.3) - northWallFaceZ,
546
+ scaleOfficeX(22.2),
547
+ 1.25,
548
+ (northWallFaceZ + scaleOfficeZ(-9.3)) / 2,
549
+ 0.15,
550
+ );
551
+ addMeetingWhiteboard(scene, scaleOfficeX(16.35), northWallFaceZ, 0);
552
+ [
553
+ [scaleOfficeX(10.5), northWallFaceZ],
554
+ [scaleOfficeX(10.5), scaleOfficeZ(sharedWallZ)],
555
+ [scaleOfficeX(22.2), northWallFaceZ],
556
+ [scaleOfficeX(22.2), scaleOfficeZ(sharedWallZ)],
557
+ ].forEach(([x, z]) => addBox(scene, 0xcccccc, 0.15, 2.5, 0.15, x, 1.25, z));
558
+ }
559
+
560
+ function addOfficeDividerGlass(scene: THREE.Scene) {
561
+ const minZ = -9.3;
562
+ const maxZ = 9.6;
563
+ addTransparentBox(
564
+ scene,
565
+ 0x88ccee,
566
+ 0.08,
567
+ 2.5,
568
+ (maxZ - minZ) * OFFICE_LAYOUT_SCALE,
569
+ scaleOfficeX(22.2),
570
+ 1.25,
571
+ scaleOfficeZ((minZ + maxZ) / 2),
572
+ 0.15,
573
+ );
574
+ }
575
+
576
+ function addMeetingWhiteboard(scene: THREE.Scene, x: number, wallZ: number, rotation: number) {
577
+ addWallWhiteboard(scene, x, wallZ, rotation, rotation === 0 ? 1 : -1);
578
+ }
579
+
580
+ function addGlassWallWithDoor(
581
+ scene: THREE.Scene,
582
+ roomIndex: number,
583
+ x: number,
584
+ z: number,
585
+ depth: number,
586
+ doorDepth: number,
587
+ opacity: number,
588
+ ) {
589
+ const sideDepth = (depth - doorDepth) / 2;
590
+ const sideOffset = (doorDepth + sideDepth) / 2;
591
+ addTransparentBox(
592
+ scene,
593
+ 0x88ccee,
594
+ 0.08,
595
+ 2.5,
596
+ sideDepth,
597
+ x,
598
+ 1.25,
599
+ z - sideOffset,
600
+ opacity,
601
+ `meetingGlassWall:${roomIndex}:beforeDoor`,
602
+ );
603
+ addTransparentBox(
604
+ scene,
605
+ 0x88ccee,
606
+ 0.08,
607
+ 2.5,
608
+ sideDepth,
609
+ x,
610
+ 1.25,
611
+ z + sideOffset,
612
+ opacity,
613
+ `meetingGlassWall:${roomIndex}:afterDoor`,
614
+ );
615
+ addGlassDoor(scene, roomIndex, x, z, doorDepth);
616
+ }
617
+
618
+ function addGlassDoor(scene: THREE.Scene, roomIndex: number, x: number, z: number, depth: number) {
619
+ const doorWidth = depth - 0.25;
620
+ const hingeZ = z - depth / 2 + 0.15;
621
+ addTransparentBox(scene, 0xa5f3fc, doorWidth, 2.05, 0.06, x - doorWidth / 2, 1.03, hingeZ, 0.2, `meetingGlassDoor:${roomIndex}`);
622
+ addBox(scene, 0xcccccc, 0.15, 2.5, 0.15, x, 1.25, z - depth / 2);
623
+ addBox(scene, 0xcccccc, 0.15, 2.5, 0.15, x, 1.25, z + depth / 2);
624
+ addBox(scene, 0xcccccc, 0.15, 0.15, depth, x, 2.42, z);
625
+ addBox(scene, 0x334155, 0.2, 0.05, 0.12, x - doorWidth + 0.18, 1.1, hingeZ - 0.08);
626
+ }
627
+
628
+ function addLargeMeetingTable(scene: THREE.Scene, x: number, z: number) {
629
+ addBox(scene, 0xb8860b, 16.4, 0.15, 4.5, x, 1.5, z);
630
+ for (const legX of [-7, -3.5, 0, 3.5, 7]) {
631
+ addBox(scene, 0x8b6914, 0.2, 1.5, 0.2, x + legX, 0.75, z - 1.95);
632
+ addBox(scene, 0x8b6914, 0.2, 1.5, 0.2, x + legX, 0.75, z + 1.95);
633
+ }
634
+ }
635
+
636
+ function addTableCup(scene: THREE.Scene, x: number, z: number) {
637
+ addBox(scene, 0xf8fafc, 0.2, 0.22, 0.2, x, 1.69, z);
638
+ addBox(scene, 0x92400e, 0.16, 0.03, 0.16, x, 1.815, z);
639
+ }
640
+
641
+ function addPresentationBoard(scene: THREE.Scene, x: number, z: number, screenColor = 0x7dd3fc, rotation = 0) {
642
+ addLocalBox(scene, 0x2c3e50, 2, 0.4, 0.08, x, z, 0, 2.1, 0, rotation);
643
+ addLocalBox(scene, screenColor, 1.8, 0.3, 0.02, x, z, 0, 2.1, 0.05, rotation);
644
+ addLocalBox(scene, 0x64748b, 0.12, 1.55, 0.12, x, z, -1.08, 0.78, -0.02, rotation);
645
+ addLocalBox(scene, 0x64748b, 0.12, 1.55, 0.12, x, z, 1.08, 0.78, -0.02, rotation);
646
+ }
647
+
648
+ function addMobileDisplayStand(scene: THREE.Scene, x: number, z: number, rotation = 0) {
649
+ addLocalBox(scene, 0x111827, 4, 2.3, 0.12, x, z, 0, 3, 0, rotation);
650
+ addLocalBox(scene, 0x38bdf8, 3.52, 1.76, 0.03, x, z, 0, 3, 0.07, rotation);
651
+ addLocalBox(scene, 0x64748b, 0.16, 2.4, 0.16, x, z, 0, 1.45, -0.02, rotation);
652
+ addLocalBox(scene, 0x475569, 1.15, 0.08, 0.5, x, z, 0, 0.48, -0.02, rotation);
653
+ for (const wheelX of [-0.48, 0.48]) {
654
+ for (const wheelZ of [-0.18, 0.18]) {
655
+ addLocalBox(scene, 0x111827, 0.14, 0.14, 0.14, x, z, wheelX, 0.32, wheelZ, rotation);
656
+ }
657
+ }
658
+ }
659
+
660
+ function addSofa(scene: THREE.Scene, x: number, z: number) {
661
+ addBox(scene, 0x0f766e, 4.45, 0.52, 1.22, x, 0.42, z);
662
+ addBox(scene, 0x115e59, 4.52, 0.88, 0.26, x, 0.86, z + 0.59);
663
+ addBox(scene, 0x0f766e, 0.3, 0.78, 1.25, x - 2.28, 0.7, z);
664
+ addBox(scene, 0x0f766e, 0.3, 0.78, 1.25, x + 2.28, 0.7, z);
665
+ addBox(scene, 0x14b8a6, 1.28, 0.1, 1.05, x - 1.38, 0.68, z - 0.04);
666
+ addBox(scene, 0x14b8a6, 1.28, 0.1, 1.05, x, 0.68, z - 0.04);
667
+ addBox(scene, 0x14b8a6, 1.28, 0.1, 1.05, x + 1.38, 0.68, z - 0.04);
668
+ }
669
+
670
+ function addLoungeSideTable(scene: THREE.Scene, x: number, z: number) {
671
+ addBox(scene, 0xd97706, 1, 0.14, 0.8, x, 0.38, z);
672
+ addBox(scene, 0x92400e, 0.09, 0.4, 0.09, x - 0.38, 0.16, z - 0.3);
673
+ addBox(scene, 0x92400e, 0.09, 0.4, 0.09, x + 0.38, 0.16, z - 0.3);
674
+ addBox(scene, 0x92400e, 0.09, 0.4, 0.09, x - 0.38, 0.16, z + 0.3);
675
+ addBox(scene, 0x92400e, 0.09, 0.4, 0.09, x + 0.38, 0.16, z + 0.3);
676
+ addBox(scene, 0xfef08a, 0.28, 0.28, 0.28, x, 0.62, z);
677
+ }
678
+
679
+ function addBookcase(scene: THREE.Scene, x: number, z: number) {
680
+ addBox(scene, 0x475569, 1.2, 1.45, 0.28, x, 0.78, z);
681
+ addBox(scene, 0x334155, 1.08, 0.08, 0.32, x, 1.18, z + 0.01);
682
+ addBox(scene, 0x334155, 1.08, 0.08, 0.32, x, 0.78, z + 0.01);
683
+ addBox(scene, 0x334155, 1.08, 0.08, 0.32, x, 0.38, z + 0.01);
684
+ addBox(scene, 0x38bdf8, 0.16, 0.28, 0.08, x - 0.34, 1, z + 0.18);
685
+ addBox(scene, 0xf97316, 0.16, 0.3, 0.08, x - 0.12, 1, z + 0.18);
686
+ addBox(scene, 0x22c55e, 0.16, 0.26, 0.08, x + 0.1, 1, z + 0.18);
687
+ addBox(scene, 0xeab308, 0.46, 0.22, 0.08, x + 0.2, 0.58, z + 0.18);
688
+ }
689
+
690
+ function addTeaCounter(scene: THREE.Scene, x: number, z: number) {
691
+ addBox(scene, 0xd97706, 2.2, 0.75, 0.55, x, 0.38, z);
692
+ addBox(scene, 0x92400e, 2.35, 0.12, 0.7, x, 0.82, z);
693
+ addBox(scene, 0x334155, 1.08, 0.08, 0.32, x, 1.15, z - 0.2);
694
+ addBox(scene, 0xfef08a, 0.28, 0.28, 0.28, x - 0.65, 1.02, z);
695
+ addBox(scene, 0xf8fafc, 0.2, 0.22, 0.2, x + 0.05, 1.02, z - 0.12);
696
+ addBox(scene, 0xf8fafc, 0.2, 0.22, 0.2, x + 0.38, 1.02, z - 0.12);
697
+ addBox(scene, 0x22c55e, 0.16, 0.26, 0.08, x + 0.78, 1.03, z + 0.18);
698
+ }
699
+
700
+ function addWaterDispenser(scene: THREE.Scene, x: number, z: number) {
701
+ addBox(scene, 0x475569, 0.75, 1.35, 0.45, x, 0.72, z);
702
+ addBox(scene, 0x38bdf8, 0.46, 0.45, 0.28, x, 1.62, z);
703
+ addBox(scene, 0x334155, 0.46, 0.08, 0.32, x, 1.18, z + 0.01);
704
+ addBox(scene, 0xf8fafc, 0.2, 0.22, 0.2, x + 0.25, 0.42, z - 0.16);
705
+ }
706
+
707
+ function addBox(
708
+ scene: THREE.Scene,
709
+ color: number,
710
+ width: number,
711
+ height: number,
712
+ depth: number,
713
+ x: number,
714
+ y: number,
715
+ z: number,
716
+ rotationY = 0,
717
+ ) {
718
+ if (activeBoxBatcher) {
719
+ activeBoxBatcher.addBox(color, width, height, depth, x, y, z, rotationY);
720
+ return;
721
+ }
722
+
723
+ const mesh = new THREE.Mesh(
724
+ new THREE.BoxGeometry(width, height, depth),
725
+ new THREE.MeshLambertMaterial({ color }),
726
+ );
727
+ mesh.castShadow = true;
728
+ mesh.receiveShadow = true;
729
+ mesh.position.set(x, y, z);
730
+ mesh.rotation.y = rotationY;
731
+ scene.add(mesh);
732
+ }
733
+
734
+ function addTransparentBox(
735
+ scene: THREE.Scene,
736
+ color: number,
737
+ width: number,
738
+ height: number,
739
+ depth: number,
740
+ x: number,
741
+ y: number,
742
+ z: number,
743
+ opacity: number,
744
+ name?: string,
745
+ ) {
746
+ const mesh = new THREE.Mesh(
747
+ activeTransparentBoxCache?.getGeometry(width, height, depth) ?? new THREE.BoxGeometry(width, height, depth),
748
+ activeTransparentBoxCache?.getMaterial(color, opacity) ??
749
+ new THREE.MeshLambertMaterial({
750
+ color,
751
+ transparent: true,
752
+ opacity,
753
+ }),
754
+ );
755
+ mesh.castShadow = false;
756
+ mesh.receiveShadow = false;
757
+ if (name) {
758
+ mesh.name = name;
759
+ }
760
+ mesh.position.set(x, y, z);
761
+ scene.add(mesh);
762
+ }
763
+
764
+ function addLocalBox(
765
+ scene: THREE.Scene,
766
+ color: number,
767
+ width: number,
768
+ height: number,
769
+ depth: number,
770
+ originX: number,
771
+ originZ: number,
772
+ localX: number,
773
+ y: number,
774
+ localZ: number,
775
+ rotationY: number,
776
+ ) {
777
+ const cos = Math.cos(rotationY);
778
+ const sin = Math.sin(rotationY);
779
+ addBox(
780
+ scene,
781
+ color,
782
+ width,
783
+ height,
784
+ depth,
785
+ originX + localX * cos + localZ * sin,
786
+ y,
787
+ originZ - localX * sin + localZ * cos,
788
+ rotationY,
789
+ );
790
+ }