@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,455 @@
1
+ import type { OfficeBuildingObjectDefinition } from "./office-building-layout.js";
2
+ import type { TownOfficeObjectFrame, TownOfficeRoomType } from "./town-office-assets.js";
3
+
4
+ export type TownOfficeRoomObjectsOptions = {
5
+ roomId: string;
6
+ roomType: TownOfficeRoomType;
7
+ width: number;
8
+ height: number;
9
+ };
10
+
11
+ const HEADER_SAFE_Y = 48;
12
+ const ROOM_INSET = 22;
13
+
14
+ export function createTownOfficeRoomObjects(options: TownOfficeRoomObjectsOptions): OfficeBuildingObjectDefinition[] {
15
+ const objects: OfficeBuildingObjectDefinition[] = [];
16
+ const add = createObjectAdder(options, objects);
17
+ const room = createRoomMetrics(options.width, options.height);
18
+
19
+ switch (options.roomType) {
20
+ case "dispatch": {
21
+ add("ticker-board", room.x(0.38), room.y(0.08), room.scale(1.65));
22
+ add("monitor-wall", room.x(0.5), room.y(0.25), room.scale(1.35));
23
+ add("big-screen", room.x(0.78), room.y(0.32), room.scale(0.8));
24
+ add("schedule-board", room.x(0.11), room.y(0.28), room.scale(0.95));
25
+ add("server-rack", room.x(0.11), room.y(0.48), room.scale(0.95));
26
+ addDeskCluster(add, room, 0.36, 0.82);
27
+ addDeskCluster(add, room, 0.64, 0.82);
28
+ addBottomUtilities(add, room, ["phone", "rug-mat", "exit-sign"]);
29
+ addWallUtilities(add, room, ["window", "air-conditioner"]);
30
+ break;
31
+ }
32
+ case "trading": {
33
+ add("ticker-board", room.x(0.5), room.y(0.12), room.scale(1.55));
34
+ add("monitor-wall", room.x(0.5), room.y(0.34), room.scale(1.2));
35
+ add("chart-board", room.x(0.78), room.y(0.42), room.scale(0.85));
36
+ add("server-rack", room.x(0.12), room.y(0.48), room.scale(0.9));
37
+ addResponsiveDeskRow(add, room, 0.82, { chairs: true });
38
+ addBottomUtilities(add, room, ["printer", "trash-can", "plant-small"]);
39
+ addWallUtilities(add, room, ["window", "clock"]);
40
+ break;
41
+ }
42
+ case "testing":
43
+ case "testing_room": {
44
+ add("test-rack", room.x(0.22), room.y(0.38), room.scale(1.05));
45
+ add("cert-badge", room.x(0.78), room.y(0.2), room.scale(1.05));
46
+ add("chart-board", room.x(0.54), room.y(0.56), room.scale(1));
47
+ add("beaker", room.x(0.78), room.y(0.62), room.scale(1.05));
48
+ add("monitor", room.x(0.28), room.y(0.25), room.scale(0.55));
49
+ add("monitor", room.x(0.62), room.y(0.25), room.scale(0.55));
50
+ addDeskCluster(add, room, 0.5, 0.84, "desk-long");
51
+ addBottomUtilities(add, room, ["printer", "trash-can"]);
52
+ addWallUtilities(add, room, ["window", "air-conditioner"]);
53
+ break;
54
+ }
55
+ case "resource": {
56
+ add("battery", room.x(0.16), room.y(0.42), room.scale(1.2));
57
+ add("solar-panel", room.x(0.33), room.y(0.42), room.scale(1.1));
58
+ add("ev-charger", room.x(0.86), room.y(0.44), room.scale(1.1));
59
+ add("chart-board", room.x(0.58), room.y(0.24), room.scale(0.85));
60
+ add("kpi-card", room.x(0.72), room.y(0.22), room.scale(0.95));
61
+ add("bulletin-board", room.x(0.18), room.y(0.22), room.scale(0.9));
62
+ add("sofa-long", room.x(0.66), room.y(0.78), room.scale(0.68));
63
+ addBottomUtilities(add, room, ["plant-large", "flower-pot", "plant-large"]);
64
+ addWallUtilities(add, room, ["window", "picture"]);
65
+ break;
66
+ }
67
+ case "infrastructure": {
68
+ add("server-rack", room.x(0.22), room.y(0.36), room.scale(1.1));
69
+ add("server-rack", room.x(0.34), room.y(0.36), room.scale(1.1));
70
+ add("monitor-wall", room.x(0.58), room.y(0.22), room.scale(1.1));
71
+ add("battery", room.x(0.82), room.y(0.76), room.scale(1));
72
+ add("fire-extinguisher", room.x(0.12), room.y(0.58), room.scale(1));
73
+ addBottomUtilities(add, room, ["clock", "trash-can"]);
74
+ break;
75
+ }
76
+ case "business": {
77
+ add("megaphone", room.x(0.14), room.y(0.2), room.scale(0.9));
78
+ add("kpi-card", room.x(0.5), room.y(0.22), room.scale(1));
79
+ add("chart-board", room.x(0.8), room.y(0.32), room.scale(0.9));
80
+ add("bulletin-board", room.x(0.22), room.y(0.5), room.scale(0.85));
81
+ addResponsiveDeskRow(add, room, 0.82, { chairs: true });
82
+ addBottomUtilities(add, room, ["printer", "trash-can"]);
83
+ break;
84
+ }
85
+ case "innovation": {
86
+ add("big-screen", room.x(0.5), room.y(0.22), room.scale(1.05));
87
+ add("kpi-card", room.x(0.22), room.y(0.48), room.scale(0.95));
88
+ add("kpi-card", room.x(0.78), room.y(0.48), room.scale(0.95));
89
+ addResponsiveDeskRow(add, room, 0.82, { chairs: true });
90
+ addBottomUtilities(add, room, ["printer", "trash-can"]);
91
+ break;
92
+ }
93
+ case "admin": {
94
+ add("schedule-board", room.x(0.18), room.y(0.2), room.scale(0.9));
95
+ add("whiteboard", room.x(0.62), room.y(0.2), room.scale(0.9));
96
+ add("printer", room.x(0.84), room.y(0.72), room.scale(0.9));
97
+ add("filing-cabinet", room.x(0.18), room.y(0.66), room.scale(0.9));
98
+ addResponsiveDeskRow(add, room, 0.84, { chairs: true });
99
+ addWallUtilities(add, room, ["clock"]);
100
+ break;
101
+ }
102
+ case "meeting": {
103
+ if (room.usableWidth > 360) {
104
+ addLargeMeetingRoom(add, room);
105
+ } else {
106
+ addSmallMeetingRoom(add, room);
107
+ }
108
+ break;
109
+ }
110
+ case "meeting_small": {
111
+ addSmallMeetingRoom(add, room);
112
+ break;
113
+ }
114
+ case "lab":
115
+ case "research": {
116
+ add("whiteboard", room.x(0.5), room.y(0.18), room.scale(0.95));
117
+ add("beaker", room.x(0.18), room.y(0.52), room.scale(1));
118
+ add("chart-board", room.x(0.78), room.y(0.34), room.scale(0.9));
119
+ addResponsiveDeskRow(add, room, 0.82, { chairs: true });
120
+ addBottomUtilities(add, room, ["printer", "plant-small", "trash-can"]);
121
+ break;
122
+ }
123
+ case "legal": {
124
+ add("seal", room.x(0.18), room.y(0.28), room.scale(1));
125
+ add("schedule-board", room.x(0.46), room.y(0.22), room.scale(0.9));
126
+ add("filing-cabinet", room.x(0.84), room.y(0.42), room.scale(0.9));
127
+ add("bookshelf", room.x(0.18), room.y(0.7), room.scale(0.85));
128
+ addResponsiveDeskRow(add, room, 0.84, { chairs: true });
129
+ addBottomUtilities(add, room, ["printer", "trash-can"]);
130
+ break;
131
+ }
132
+ case "training": {
133
+ addTrainingClassroom(add, room);
134
+ break;
135
+ }
136
+ case "library": {
137
+ add("bookshelf", room.x(0.2), room.y(0.28), room.scale(1));
138
+ add("bookshelf", room.x(0.8), room.y(0.28), room.scale(1));
139
+ add("bench", room.x(0.5), room.y(0.64), room.scale(1.1));
140
+ add("picture", room.x(0.5), room.y(0.18), room.scale(1));
141
+ addBottomUtilities(add, room, ["plant-small", "trash-can"]);
142
+ break;
143
+ }
144
+ case "archive": {
145
+ add("henan-map", room.x(0.5), room.y(0.34), room.scale(0.9));
146
+ add("bookshelf", room.x(0.22), room.y(0.3), room.scale(0.8));
147
+ add("bookshelf", room.x(0.78), room.y(0.3), room.scale(0.8));
148
+ add("filing-cabinet", room.x(0.5), room.y(0.74), room.scale(0.8));
149
+ add("seal", room.x(0.86), room.y(0.68), room.scale(1));
150
+ addWallUtilities(add, room, ["picture", "clock"]);
151
+ break;
152
+ }
153
+ case "cafeteria": {
154
+ addTableWithChairs(add, room, 0.32, 0.48);
155
+ addTableWithChairs(add, room, 0.68, 0.48);
156
+ if (room.usableWidth > 360) {
157
+ addTableWithChairs(add, room, 0.5, 0.72);
158
+ }
159
+ add("coffee-machine", room.x(0.82), room.y(0.7), room.scale(0.9));
160
+ add("vending-machine", room.x(0.16), room.y(0.68), room.scale(1));
161
+ addBottomUtilities(add, room, ["water-dispenser", "trash-can", "exit-sign"]);
162
+ break;
163
+ }
164
+ case "lounge":
165
+ case "employee_home":
166
+ case "counseling":
167
+ case "mental_health": {
168
+ add("tv", room.x(0.5), room.y(0.18), room.scale(0.9));
169
+ add("sofa-long", room.x(0.5), room.y(0.6), room.scale(0.85));
170
+ add("bench", room.x(0.33), room.y(0.36), room.scale(0.9));
171
+ add("coffee-machine", room.x(0.82), room.y(0.62), room.scale(0.8));
172
+ add("rug-mat", room.x(0.5), room.y(0.8), room.scale(1.3));
173
+ addBottomUtilities(add, room, ["flower-pot", "plant-small"]);
174
+ addWallUtilities(add, room, ["clock", "picture"]);
175
+ break;
176
+ }
177
+ case "fitness": {
178
+ add("locker", room.x(0.22), room.y(0.36), room.scale(0.9));
179
+ add("bench", room.x(0.5), room.y(0.58), room.scale(1.1));
180
+ add("water-dispenser", room.x(0.78), room.y(0.68), room.scale(0.9));
181
+ add("plant-small", room.x(0.84), room.y(0.32), room.scale(0.8));
182
+ addWallUtilities(add, room, ["window", "exit-sign"]);
183
+ break;
184
+ }
185
+ case "interest_class": {
186
+ add("whiteboard", room.x(0.5), room.y(0.18), room.scale(0.9));
187
+ add("ping-pong-table", room.x(0.5), room.y(0.45), room.scale(0.95));
188
+ add("bookshelf", room.x(0.18), room.y(0.74), room.scale(0.8));
189
+ add("bench", room.x(0.78), room.y(0.72), room.scale(1));
190
+ add("flower-pot", room.x(0.5), room.y(0.76), room.scale(1));
191
+ addBottomUtilities(add, room, ["trash-can"]);
192
+ break;
193
+ }
194
+ case "club": {
195
+ add("schedule-board", room.x(0.18), room.y(0.2), room.scale(0.9));
196
+ add("ping-pong-table", room.x(0.52), room.y(0.42), room.scale(0.95));
197
+ add("bookshelf", room.x(0.22), room.y(0.72), room.scale(0.8));
198
+ add("bookshelf", room.x(0.78), room.y(0.72), room.scale(0.8));
199
+ addBottomUtilities(add, room, ["flower-pot", "trash-can"]);
200
+ break;
201
+ }
202
+ case "regional_office": {
203
+ add("henan-map", room.x(0.5), room.y(0.3), room.scale(0.95));
204
+ add("kpi-card", room.x(0.22), room.y(0.5), room.scale(0.9));
205
+ add("kpi-card", room.x(0.78), room.y(0.5), room.scale(0.9));
206
+ addResponsiveDeskRow(add, room, 0.84, { chairs: true });
207
+ addBottomUtilities(add, room, ["printer", "water-dispenser", "trash-can"]);
208
+ break;
209
+ }
210
+ case "party_building": {
211
+ add("red-banner", room.x(0.5), room.y(0.18), room.scale(1.25));
212
+ add("party-flag", room.x(0.2), room.y(0.34), room.scale(0.95));
213
+ add("podium", room.x(0.5), room.y(0.48), room.scale(0.95));
214
+ add("seal", room.x(0.82), room.y(0.34), room.scale(1));
215
+ addBottomUtilities(add, room, ["flower-pot", "trash-can"]);
216
+ break;
217
+ }
218
+ case "outdoor": {
219
+ add("tree", room.x(0.18), room.y(0.36), room.scale(1));
220
+ add("tree", room.x(0.82), room.y(0.36), room.scale(1));
221
+ add("gazebo", room.x(0.5), room.y(0.42), room.scale(0.95));
222
+ add("bicycle-rack", room.x(0.5), room.y(0.78), room.scale(1.1));
223
+ add("bench", room.x(0.28), room.y(0.68), room.scale(1));
224
+ add("bench", room.x(0.72), room.y(0.68), room.scale(1));
225
+ break;
226
+ }
227
+ case "executive": {
228
+ add("big-screen", room.x(0.5), room.y(0.18), room.scale(1));
229
+ add("kpi-card", room.x(0.18), room.y(0.44), room.scale(0.9));
230
+ add("kpi-card", room.x(0.82), room.y(0.44), room.scale(0.9));
231
+ add("meeting-table", room.x(0.5), room.y(0.7), room.scale(0.9));
232
+ addWallUtilities(add, room, ["clock"]);
233
+ break;
234
+ }
235
+ case "honor_hall": {
236
+ add("cert-badge", room.x(0.22), room.y(0.26), room.scale(1));
237
+ add("henan-map", room.x(0.5), room.y(0.42), room.scale(0.9));
238
+ add("seal", room.x(0.82), room.y(0.62), room.scale(1));
239
+ add("bookshelf", room.x(0.22), room.y(0.68), room.scale(0.8));
240
+ addWallUtilities(add, room, ["picture", "clock"]);
241
+ break;
242
+ }
243
+ default: {
244
+ addResponsiveDeskRow(add, room, 0.82, { chairs: true });
245
+ addBottomUtilities(add, room, ["plant-small", "trash-can"]);
246
+ break;
247
+ }
248
+ }
249
+
250
+ return objects;
251
+ }
252
+
253
+ type AddObject = (frame: TownOfficeObjectFrame, x: number, y: number, scale?: number) => void;
254
+
255
+ type RoomMetrics = {
256
+ width: number;
257
+ height: number;
258
+ usableWidth: number;
259
+ top: number;
260
+ bottom: number;
261
+ x: (ratio: number) => number;
262
+ y: (ratio: number) => number;
263
+ scale: (value: number) => number;
264
+ };
265
+
266
+ function createRoomMetrics(width: number, height: number): RoomMetrics {
267
+ const top = Math.min(height, HEADER_SAFE_Y);
268
+ const bottom = Math.max(top, height - 16);
269
+ const usableWidth = Math.max(1, width - ROOM_INSET * 2);
270
+ const usableHeight = Math.max(1, bottom - top);
271
+ const baseScale = clamp(Math.min(width / 240, height / 160), 0.72, 1.35);
272
+
273
+ return {
274
+ width,
275
+ height,
276
+ usableWidth,
277
+ top,
278
+ bottom,
279
+ x: (ratio) => ROOM_INSET + usableWidth * ratio,
280
+ y: (ratio) => top + usableHeight * ratio,
281
+ scale: (value) => Number((value * baseScale).toFixed(3)),
282
+ };
283
+ }
284
+
285
+ function createObjectAdder(
286
+ options: TownOfficeRoomObjectsOptions,
287
+ objects: OfficeBuildingObjectDefinition[],
288
+ ): AddObject {
289
+ const counts = new Map<TownOfficeObjectFrame, number>();
290
+ return (frame, x, y, scale = 1) => {
291
+ const index = counts.get(frame) ?? 0;
292
+ counts.set(frame, index + 1);
293
+ objects.push({
294
+ id: `${options.roomId}-${frame}-${index}`,
295
+ frame,
296
+ x: clamp(x, 0, options.width),
297
+ y: clamp(y, 0, options.height),
298
+ scale,
299
+ });
300
+ };
301
+ }
302
+
303
+ function addDeskCluster(
304
+ add: AddObject,
305
+ room: RoomMetrics,
306
+ xRatio: number,
307
+ yRatio: number,
308
+ deskFrame: Extract<TownOfficeObjectFrame, "desk-single" | "desk-long"> = "desk-single",
309
+ ): void {
310
+ const x = room.x(xRatio);
311
+ const y = room.y(yRatio);
312
+ add(deskFrame, x, y, room.scale(deskFrame === "desk-long" ? 0.8 : 0.74));
313
+ add("chair-office", x, Math.min(room.height, y + 24 * room.scale(1)), room.scale(0.62));
314
+ }
315
+
316
+ function addResponsiveDeskRow(
317
+ add: AddObject,
318
+ room: RoomMetrics,
319
+ yRatio: number,
320
+ options: { chairs?: boolean; maxCount?: number } = {},
321
+ ): void {
322
+ const maxCount = options.maxCount ?? 4;
323
+ const count = Math.max(1, Math.min(maxCount, Math.floor(room.usableWidth / 92)));
324
+ for (let index = 0; index < count; index++) {
325
+ const ratio = count === 1 ? 0.5 : (index + 0.5) / count;
326
+ const x = room.x(ratio);
327
+ const y = room.y(yRatio);
328
+ add("desk-single", x, y, room.scale(0.72));
329
+ if (options.chairs) {
330
+ add("chair-office", x, Math.min(room.height, y + 24 * room.scale(1)), room.scale(0.6));
331
+ }
332
+ }
333
+ }
334
+
335
+ function addTableWithChairs(add: AddObject, room: RoomMetrics, xRatio: number, yRatio: number): void {
336
+ const x = room.x(xRatio);
337
+ const y = room.y(yRatio);
338
+ add("meeting-table", x, y, room.scale(0.58));
339
+ add("chair-office", x - 22 * room.scale(1), y, room.scale(0.55));
340
+ add("chair-office", x + 22 * room.scale(1), y, room.scale(0.55));
341
+ }
342
+
343
+ function addSmallMeetingRoom(add: AddObject, room: RoomMetrics): void {
344
+ add("whiteboard", room.x(0.5), room.y(0.08), room.scale(0.9));
345
+ add("schedule-board", room.x(0.14), room.y(0.08), room.scale(0.82));
346
+ add("meeting-table", room.x(0.5), room.y(0.58), room.scale(0.9));
347
+ add("chair-office", room.x(0.36), room.y(0.42), room.scale(0.7));
348
+ add("chair-office", room.x(0.64), room.y(0.42), room.scale(0.7));
349
+ add("chair-office", room.x(0.36), room.y(0.74), room.scale(0.7));
350
+ add("chair-office", room.x(0.64), room.y(0.74), room.scale(0.7));
351
+ add("clock", room.x(0.9), room.y(0.08), room.scale(0.8));
352
+ add("trash-can", room.x(0.9), room.y(0.92), room.scale(0.78));
353
+ }
354
+
355
+ function addLargeMeetingRoom(add: AddObject, room: RoomMetrics): void {
356
+ const hasSideTables = room.width >= 560;
357
+ const mainTableWidth = hasSideTables ? room.usableWidth - 220 : room.usableWidth - 90;
358
+ const seatsPerSide = Math.max(5, Math.floor(mainTableWidth / 28));
359
+ const tableCenterX = room.x(0.5);
360
+ const mainTableY = clamp(room.y(0.38), room.top + 60, room.bottom - 130);
361
+ const seatStartX = tableCenterX - mainTableWidth / 2 + 8;
362
+ const seatGap = (mainTableWidth - 16) / seatsPerSide;
363
+
364
+ add("big-screen", room.x(0.5), room.top + 26, room.scale(1.85));
365
+ add("schedule-board", room.x(0.04), room.top + 12, room.scale(0.8));
366
+ add("clock", room.x(0.96), room.top + 10, room.scale(0.7));
367
+
368
+ if (hasSideTables) {
369
+ add("meeting-table", room.x(0.08), mainTableY + 14, room.scale(0.56));
370
+ for (const ratio of [0.035, 0.07, 0.105]) {
371
+ add("chair-office", room.x(ratio), mainTableY - 8, room.scale(0.5));
372
+ add("chair-office", room.x(ratio), mainTableY + 42, room.scale(0.5));
373
+ }
374
+
375
+ add("meeting-table", room.x(0.92), mainTableY + 14, room.scale(0.56));
376
+ for (const ratio of [0.895, 0.93, 0.965]) {
377
+ add("chair-office", room.x(ratio), mainTableY - 8, room.scale(0.5));
378
+ add("chair-office", room.x(ratio), mainTableY + 42, room.scale(0.5));
379
+ }
380
+ }
381
+
382
+ add("meeting-table", tableCenterX, mainTableY + 18, room.scale(hasSideTables ? 2.1 : 1.7));
383
+ for (let index = 0; index < seatsPerSide; index++) {
384
+ const x = seatStartX + index * seatGap;
385
+ add("chair-office", x, mainTableY - 8, room.scale(0.54));
386
+ add("chair-office", x, mainTableY + 46, room.scale(0.54));
387
+ }
388
+ add("phone", tableCenterX, mainTableY + 20, room.scale(0.72));
389
+
390
+ add("podium", tableCenterX, room.bottom - 30, room.scale(0.95));
391
+ add("rug-mat", tableCenterX, room.bottom - 8, room.scale(3.2));
392
+ for (let index = 0; index < 4; index++) {
393
+ add("chair-office", room.x(0.14) + index * 32, room.bottom - 24, room.scale(0.54));
394
+ add("chair-office", room.x(0.86) - index * 32, room.bottom - 24, room.scale(0.54));
395
+ }
396
+ add("flower-pot", room.x(0.02), room.bottom - 18, room.scale(0.84));
397
+ add("flower-pot", room.x(0.98), room.bottom - 18, room.scale(0.84));
398
+ }
399
+
400
+ function addTrainingClassroom(add: AddObject, room: RoomMetrics): void {
401
+ const deskGap = 6;
402
+ const deskWidth = 24;
403
+ const cols = Math.max(2, Math.floor((room.width - 30) / (deskWidth + deskGap)));
404
+ const totalWidth = cols * (deskWidth + deskGap) - deskGap;
405
+ const startX = room.width / 2 - totalWidth / 2 + deskWidth / 2;
406
+ const rows = getTrainingDeskRows(room.top, room.bottom);
407
+
408
+ add("whiteboard", room.x(0.5), room.top + 10, room.scale(1));
409
+ add("schedule-board", room.x(0.06), room.top + 10, room.scale(0.82));
410
+ add("clock", room.x(0.96), room.top + 10, room.scale(0.7));
411
+ for (const y of rows) {
412
+ for (let col = 0; col < cols; col++) {
413
+ const x = startX + col * (deskWidth + deskGap);
414
+ add("desk-single", x, y, room.scale(0.84));
415
+ add("chair-office", x, y + 24, room.scale(0.56));
416
+ }
417
+ }
418
+ add("water-dispenser", room.x(0.96), room.bottom - 24, room.scale(0.78));
419
+ add("trash-can", room.x(0.04), room.bottom - 16, room.scale(0.72));
420
+ }
421
+
422
+ function getTrainingDeskRows(top: number, bottom: number): number[] {
423
+ const minY = top + 32;
424
+ const maxY = bottom - 56;
425
+ if (maxY <= minY) {
426
+ return [minY];
427
+ }
428
+
429
+ const available = maxY - minY;
430
+ const rowCount = clamp(Math.floor(available / 56) + 2, 4, 7);
431
+ if (rowCount === 1) {
432
+ return [minY];
433
+ }
434
+
435
+ const step = available / (rowCount - 1);
436
+ return Array.from({ length: rowCount }, (_, index) => Number((minY + step * index).toFixed(3)));
437
+ }
438
+
439
+ function addBottomUtilities(add: AddObject, room: RoomMetrics, frames: TownOfficeObjectFrame[]): void {
440
+ for (let index = 0; index < frames.length; index++) {
441
+ const ratio = frames.length === 1 ? 0.5 : (index + 0.5) / frames.length;
442
+ add(frames[index]!, room.x(ratio), room.y(0.92), room.scale(0.85));
443
+ }
444
+ }
445
+
446
+ function addWallUtilities(add: AddObject, room: RoomMetrics, frames: TownOfficeObjectFrame[]): void {
447
+ for (let index = 0; index < frames.length; index++) {
448
+ const ratio = frames.length === 1 ? 0.5 : (index + 0.5) / frames.length;
449
+ add(frames[index]!, room.x(ratio), room.y(0.08), room.scale(0.85));
450
+ }
451
+ }
452
+
453
+ function clamp(value: number, min: number, max: number): number {
454
+ return Math.max(min, Math.min(max, value));
455
+ }
@@ -0,0 +1,238 @@
1
+ import type { OfficeBuildingAnchorDefinition } from "./office-building-layout.js";
2
+ import type { AgentGameAnchorKind } from "./scene.js";
3
+ import type { TownOfficeRoomType } from "./town-office-assets.js";
4
+
5
+ export type TownOfficeRoomAnchorsOptions = {
6
+ roomId: string;
7
+ roomType: TownOfficeRoomType;
8
+ width: number;
9
+ height: number;
10
+ };
11
+
12
+ const HEADER_SAFE_Y = 46;
13
+
14
+ export function createTownOfficeRoomAnchors(options: TownOfficeRoomAnchorsOptions): OfficeBuildingAnchorDefinition[] {
15
+ const anchors: OfficeBuildingAnchorDefinition[] = [];
16
+ const add = createAnchorAdder(options, anchors);
17
+ const workY = Math.max(HEADER_SAFE_Y, options.height - 16);
18
+
19
+ switch (options.roomType) {
20
+ case "trading":
21
+ case "dispatch":
22
+ case "testing":
23
+ case "testing_room":
24
+ case "lab":
25
+ case "business":
26
+ case "admin": {
27
+ addDeskAnchors(add, options.width, workY);
28
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
29
+ add("break", options.width - 20, workY);
30
+ break;
31
+ }
32
+ case "innovation": {
33
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
34
+ add("desk", 42, workY);
35
+ add("desk", options.width - 42, workY);
36
+ add("break", options.width - 24, Math.max(HEADER_SAFE_Y, options.height / 2));
37
+ break;
38
+ }
39
+ case "legal": {
40
+ add("desk", 40, workY);
41
+ add("desk", options.width - 40, workY);
42
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
43
+ break;
44
+ }
45
+ case "training": {
46
+ addTrainingClassroomAnchors(add, options.width, options.height);
47
+ break;
48
+ }
49
+ case "meeting": {
50
+ addMeetingRoomAnchors(add, options.width, options.height);
51
+ break;
52
+ }
53
+ case "meeting_small":
54
+ case "executive": {
55
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
56
+ add("meeting", options.width / 2 - 28, Math.max(HEADER_SAFE_Y, options.height / 2));
57
+ add("meeting", options.width / 2 + 28, Math.max(HEADER_SAFE_Y, options.height / 2));
58
+ add("meeting", options.width / 2, options.height - 24);
59
+ break;
60
+ }
61
+ case "cafeteria":
62
+ case "lounge":
63
+ case "employee_home": {
64
+ add("break", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
65
+ add("break", options.width / 2 - 28, Math.max(HEADER_SAFE_Y, options.height / 2));
66
+ add("break", options.width / 2 + 28, Math.max(HEADER_SAFE_Y, options.height / 2));
67
+ break;
68
+ }
69
+ case "outdoor": {
70
+ add("living", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
71
+ add("break", 28, options.height - 24);
72
+ add("break", options.width - 28, options.height - 24);
73
+ break;
74
+ }
75
+ case "party_building": {
76
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
77
+ add("meeting", options.width / 2 - 34, options.height - 24);
78
+ add("break", options.width - 26, options.height - 24);
79
+ break;
80
+ }
81
+ case "honor_hall": {
82
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
83
+ add("meeting", 34, Math.max(HEADER_SAFE_Y, options.height / 2));
84
+ add("living", options.width - 34, options.height - 24);
85
+ break;
86
+ }
87
+ case "library": {
88
+ add("desk", 36, options.height - 24);
89
+ add("desk", options.width - 36, options.height - 24);
90
+ add("break", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
91
+ break;
92
+ }
93
+ case "archive": {
94
+ add("desk", 36, options.height - 24);
95
+ add("desk", options.width - 36, options.height - 24);
96
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
97
+ break;
98
+ }
99
+ case "interest_class":
100
+ case "club": {
101
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
102
+ add("break", 34, options.height - 24);
103
+ add("break", options.width - 34, options.height - 24);
104
+ break;
105
+ }
106
+ case "regional_office": {
107
+ add("desk", options.width / 2, workY);
108
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
109
+ add("break", options.width - 26, options.height - 24);
110
+ break;
111
+ }
112
+ case "fitness": {
113
+ add("break", 42, options.height - 24);
114
+ add("break", options.width - 42, options.height - 24);
115
+ add("living", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
116
+ break;
117
+ }
118
+ default: {
119
+ addDeskAnchors(add, options.width, workY);
120
+ add("meeting", options.width / 2, Math.max(HEADER_SAFE_Y, options.height / 2));
121
+ break;
122
+ }
123
+ }
124
+
125
+ return anchors;
126
+ }
127
+
128
+ type AddAnchor = (kind: AgentGameAnchorKind, x: number, y: number) => void;
129
+
130
+ function createAnchorAdder(
131
+ options: TownOfficeRoomAnchorsOptions,
132
+ anchors: OfficeBuildingAnchorDefinition[],
133
+ ): AddAnchor {
134
+ const counts = new Map<AgentGameAnchorKind, number>();
135
+ return (kind, x, y) => {
136
+ const index = counts.get(kind) ?? 0;
137
+ counts.set(kind, index + 1);
138
+ anchors.push({
139
+ id: `${options.roomId}-${kind}-${index}`,
140
+ kind,
141
+ x: clamp(x, 0, options.width),
142
+ y: clamp(y, 0, options.height),
143
+ direction: "down",
144
+ });
145
+ };
146
+ }
147
+
148
+ function addDeskAnchors(add: AddAnchor, width: number, y: number): void {
149
+ const count = Math.max(1, Math.min(4, Math.floor((width - 24) / 48)));
150
+ const spacing = count === 1 ? 0 : Math.min(52, (width - 48) / (count - 1));
151
+ const startX = count === 1 ? width / 2 : 24;
152
+ for (let index = 0; index < count; index++) {
153
+ add("desk", startX + index * spacing, y);
154
+ }
155
+ }
156
+
157
+ function addMeetingRoomAnchors(add: AddAnchor, width: number, height: number): void {
158
+ const innerWidth = Math.max(1, width - 44);
159
+ const top = Math.min(height, HEADER_SAFE_Y);
160
+ const bottom = Math.max(top, height - 16);
161
+ if (innerWidth <= 360) {
162
+ add("meeting", width / 2, Math.max(HEADER_SAFE_Y, height / 2));
163
+ add("meeting", width / 2 - 28, Math.max(HEADER_SAFE_Y, height / 2));
164
+ add("meeting", width / 2 + 28, Math.max(HEADER_SAFE_Y, height / 2));
165
+ add("meeting", width / 2, height - 24);
166
+ return;
167
+ }
168
+
169
+ const hasSideTables = width >= 560;
170
+ const mainTableWidth = hasSideTables ? innerWidth - 220 : innerWidth - 90;
171
+ const seatsPerSide = Math.max(5, Math.floor(mainTableWidth / 28));
172
+ const tableCenterX = width / 2;
173
+ const mainTableY = clamp(top + (bottom - top) * 0.38, top + 60, bottom - 130);
174
+ const seatStartX = tableCenterX - mainTableWidth / 2 + 8;
175
+ const seatGap = (mainTableWidth - 16) / seatsPerSide;
176
+ for (let index = 0; index < seatsPerSide; index++) {
177
+ const x = seatStartX + index * seatGap;
178
+ add("meeting", x, mainTableY - 8);
179
+ add("meeting", x, mainTableY + 46);
180
+ }
181
+ if (hasSideTables) {
182
+ add("meeting", 54, mainTableY + 14);
183
+ add("meeting", 86, mainTableY + 14);
184
+ add("meeting", width - 86, mainTableY + 14);
185
+ add("meeting", width - 54, mainTableY + 14);
186
+ }
187
+ for (let index = 0; index < 4; index++) {
188
+ add("meeting", 80 + index * 32, bottom - 24);
189
+ add("meeting", width - 80 - index * 32, bottom - 24);
190
+ }
191
+ }
192
+
193
+ function addTrainingClassroomAnchors(add: AddAnchor, width: number, height: number): void {
194
+ const top = Math.min(height, HEADER_SAFE_Y);
195
+ const bottom = Math.max(top, height - 16);
196
+ const deskWidth = 24;
197
+ const deskGap = 6;
198
+ const cols = Math.max(2, Math.floor((width - 30) / (deskWidth + deskGap)));
199
+ const totalWidth = cols * (deskWidth + deskGap) - deskGap;
200
+ const startX = width / 2 - totalWidth / 2 + deskWidth / 2;
201
+ const rows = getTrainingDeskRows(top, bottom);
202
+ for (const y of rows) {
203
+ for (const col of pickTrainingAnchorColumns(cols)) {
204
+ add("desk", startX + col * (deskWidth + deskGap), y + 24);
205
+ }
206
+ }
207
+ }
208
+
209
+ function pickTrainingAnchorColumns(cols: number): number[] {
210
+ const targetCount = Math.min(4, cols);
211
+ if (targetCount <= 1) {
212
+ return [Math.floor(cols / 2)];
213
+ }
214
+
215
+ const maxIndex = cols - 1;
216
+ return Array.from({ length: targetCount }, (_, index) => Math.round((maxIndex * index) / (targetCount - 1)));
217
+ }
218
+
219
+ function getTrainingDeskRows(top: number, bottom: number): number[] {
220
+ const minY = top + 32;
221
+ const maxY = bottom - 56;
222
+ if (maxY <= minY) {
223
+ return [minY];
224
+ }
225
+
226
+ const available = maxY - minY;
227
+ const rowCount = clamp(Math.floor(available / 56) + 2, 4, 7);
228
+ if (rowCount === 1) {
229
+ return [minY];
230
+ }
231
+
232
+ const step = available / (rowCount - 1);
233
+ return Array.from({ length: rowCount }, (_, index) => Number((minY + step * index).toFixed(3)));
234
+ }
235
+
236
+ function clamp(value: number, min: number, max: number): number {
237
+ return Math.max(min, Math.min(max, value));
238
+ }