@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,242 @@
1
+ import {
2
+ AgentGameError,
3
+ createAgentGameStore,
4
+ validateAgentGameAssetManifest,
5
+ validateAgentGameScene,
6
+ validateOfficeBuildingLayout,
7
+ validateOfficeLayoutDefinition,
8
+ } from "../core/index.js";
9
+ import { createAgentGamePhaserSceneConfig } from "./agent-game-scene.js";
10
+ import {
11
+ createSceneReconciler,
12
+ type AgentDomOverlayRoot,
13
+ type AgentGameSceneReconciler,
14
+ } from "./scene-reconciler.js";
15
+ import type { AgentGameCameraControlsController } from "./camera-controls.js";
16
+ import type { AgentGameRendererConfig } from "./scene-renderer.js";
17
+ import type { TownOfficeDomOverlayRoot } from "./town-office-renderer.js";
18
+ import type { AgentGameController, AgentGamePhaserOptions, PhaserGameLike } from "./types.js";
19
+ import { resolveAgentGameViewportSize } from "./viewport.js";
20
+
21
+ type RuntimeDomOverlayRoot = HTMLElement & AgentDomOverlayRoot & TownOfficeDomOverlayRoot;
22
+
23
+ export async function createAgentGame(
24
+ container: HTMLElement,
25
+ options: AgentGamePhaserOptions,
26
+ ): Promise<AgentGameController> {
27
+ const scene = validateAgentGameScene(options.scene);
28
+ const assets = validateAgentGameAssetManifest(options.assets);
29
+ if (options.officeLayout) {
30
+ validateOfficeLayoutDefinition(options.officeLayout.definition, options.officeLayout.atlas);
31
+ }
32
+ if (options.officeBuilding) {
33
+ validateOfficeBuildingLayout(options.officeBuilding.layout, options.officeBuilding.atlas);
34
+ }
35
+ const avatarIds = new Set(assets.avatars.map((avatar) => avatar.id));
36
+
37
+ const store = options.store ?? createAgentGameStore(scene);
38
+ let destroyed = false;
39
+ let reconciler: AgentGameSceneReconciler | null = null;
40
+ let cameraControls: AgentGameCameraControlsController | null = null;
41
+ let unsubscribeStore: (() => void) | null = null;
42
+ const domOverlayRoot = createDomOverlayRoot(container, options);
43
+ const phaserGame = await createPhaserGame(
44
+ container,
45
+ options,
46
+ domOverlayRoot,
47
+ (controls) => {
48
+ cameraControls = controls;
49
+ },
50
+ (createdReconciler) => {
51
+ reconciler = createdReconciler;
52
+ unsubscribeStore = store.subscribe((snapshot) => {
53
+ reconciler?.reconcile(snapshot);
54
+ });
55
+ reconciler.reconcile(store.getSnapshot());
56
+ },
57
+ );
58
+
59
+ return {
60
+ dispatch(command) {
61
+ if (destroyed) {
62
+ throw new AgentGameError("runtime_destroyed", "Agent game runtime has been destroyed");
63
+ }
64
+ if (command.type === "agent.spawn" && !avatarIds.has(command.avatarId)) {
65
+ throw new AgentGameError("missing_avatar", `Agent game command references missing avatar: ${command.avatarId}`);
66
+ }
67
+ return store.dispatch(command);
68
+ },
69
+ getSnapshot() {
70
+ return store.getSnapshot();
71
+ },
72
+ getCameraControls() {
73
+ return cameraControls;
74
+ },
75
+ destroy() {
76
+ if (!destroyed) {
77
+ destroyed = true;
78
+ cameraControls = null;
79
+ unsubscribeStore?.();
80
+ unsubscribeStore = null;
81
+ domOverlayRoot?.remove();
82
+ phaserGame.destroy(true);
83
+ }
84
+ },
85
+ };
86
+ }
87
+
88
+ async function createPhaserGame(
89
+ container: HTMLElement,
90
+ options: AgentGamePhaserOptions,
91
+ domOverlayRoot: RuntimeDomOverlayRoot | undefined,
92
+ onCameraControlsReady: (controls: AgentGameCameraControlsController | null) => void,
93
+ onReconcilerReady: (reconciler: AgentGameSceneReconciler) => void,
94
+ ): Promise<PhaserGameLike> {
95
+ if (options.createPhaserGame) {
96
+ return options.createPhaserGame(createPhaserConfig(
97
+ container,
98
+ options,
99
+ domOverlayRoot,
100
+ onCameraControlsReady,
101
+ onReconcilerReady,
102
+ ));
103
+ }
104
+
105
+ const Phaser = await import("phaser");
106
+ return new Phaser.Game({
107
+ ...createPhaserConfig(container, options, domOverlayRoot, onCameraControlsReady, onReconcilerReady),
108
+ type: Phaser.AUTO,
109
+ });
110
+ }
111
+
112
+ function createPhaserConfig(
113
+ container: HTMLElement,
114
+ options: AgentGamePhaserOptions,
115
+ domOverlayRoot: RuntimeDomOverlayRoot | undefined,
116
+ onCameraControlsReady: (controls: AgentGameCameraControlsController | null) => void,
117
+ onReconcilerReady: (reconciler: AgentGameSceneReconciler) => void,
118
+ ) {
119
+ const { width, height } = resolveAgentGameViewportSize({
120
+ width: options.width,
121
+ height: options.height,
122
+ viewport: options.viewport,
123
+ });
124
+ const resolution = options.resolution ?? getRuntimeResolution();
125
+
126
+ return {
127
+ parent: container,
128
+ width,
129
+ height,
130
+ resolution,
131
+ pixelArt: true,
132
+ roundPixels: true,
133
+ render: {
134
+ pixelArt: true,
135
+ antialias: false,
136
+ antialiasGL: false,
137
+ roundPixels: true,
138
+ },
139
+ backgroundColor: options.backgroundColor ?? "#111827",
140
+ scene: createAgentGamePhaserSceneConfig({
141
+ definition: options.scene,
142
+ avatars: options.assets.avatars,
143
+ width,
144
+ height,
145
+ backgroundImage: options.backgroundImage,
146
+ officeLayout: options.officeLayout,
147
+ officeBuilding: options.officeBuilding,
148
+ townOfficeBuilding: options.townOfficeBuilding,
149
+ renderer: options.renderer ?? createLegacyRendererConfig(validateAgentGameScene(options.scene), width, height, options),
150
+ rendererRegistry: options.rendererRegistry,
151
+ domOverlayRoot,
152
+ debugOverlays: options.debugOverlays,
153
+ showAnchorDebug: options.showAnchorDebug,
154
+ cameraControls: options.cameraControls,
155
+ movementDurationMs: options.movementDurationMs,
156
+ onCameraControlsReady,
157
+ onCreate(scene) {
158
+ const createdReconciler = createSceneReconciler(scene, {
159
+ avatars: options.assets.avatars,
160
+ domOverlay: domOverlayRoot ? { root: domOverlayRoot } : undefined,
161
+ movementDurationMs: options.movementDurationMs ?? 220,
162
+ movementSpeedPxPerSecond: options.movementSpeedPxPerSecond,
163
+ movementMinDurationMs: options.movementMinDurationMs,
164
+ movementMaxDurationMs: options.movementMaxDurationMs,
165
+ });
166
+ onReconcilerReady(createdReconciler);
167
+ return createdReconciler;
168
+ },
169
+ }),
170
+ };
171
+ }
172
+
173
+ function createDomOverlayRoot(container: HTMLElement, options: AgentGamePhaserOptions): RuntimeDomOverlayRoot | undefined {
174
+ if (options.domOverlay === false || typeof document === "undefined" || !container.appendChild) {
175
+ return undefined;
176
+ }
177
+
178
+ const root = document.createElement("div");
179
+ root.className = "agent-dom-overlay";
180
+ container.appendChild(root);
181
+ return root as unknown as RuntimeDomOverlayRoot;
182
+ }
183
+
184
+ function createLegacyRendererConfig(
185
+ scene: ReturnType<typeof validateAgentGameScene>,
186
+ width: number,
187
+ height: number,
188
+ options: AgentGamePhaserOptions,
189
+ ): AgentGameRendererConfig {
190
+ if (options.officeBuilding) {
191
+ return {
192
+ kind: "office-building-atlas",
193
+ options: {
194
+ atlasKey: options.officeBuilding.atlasKey,
195
+ imageUrl: options.officeBuilding.imageUrl,
196
+ atlasUrl: options.officeBuilding.atlasUrl,
197
+ layout: options.officeBuilding.layout,
198
+ },
199
+ };
200
+ }
201
+
202
+ if (options.townOfficeBuilding) {
203
+ return {
204
+ kind: "town-office-building",
205
+ options: {
206
+ layout: options.townOfficeBuilding.layout,
207
+ debugOverlays: options.debugOverlays,
208
+ },
209
+ };
210
+ }
211
+
212
+ if (options.officeLayout) {
213
+ return {
214
+ kind: "office-layout",
215
+ options: {
216
+ atlasKey: options.officeLayout.atlasKey,
217
+ imageUrl: options.officeLayout.imageUrl,
218
+ atlasUrl: options.officeLayout.atlasUrl,
219
+ definition: options.officeLayout.definition,
220
+ },
221
+ };
222
+ }
223
+
224
+ return {
225
+ kind: "background",
226
+ options: {
227
+ definition: scene,
228
+ width,
229
+ height,
230
+ backgroundImage: options.backgroundImage,
231
+ },
232
+ };
233
+ }
234
+
235
+ function getRuntimeResolution(): number {
236
+ const ratio = globalThis.devicePixelRatio;
237
+ if (!Number.isFinite(ratio) || ratio < 1) {
238
+ return 1;
239
+ }
240
+
241
+ return ratio;
242
+ }
@@ -0,0 +1,21 @@
1
+ import { parseHexColor } from "../core/index.js";
2
+ import type { TownOfficeSceneLike } from "./town-office-primitives.js";
3
+ import { debugDepth } from "./render-layers.js";
4
+
5
+ export type AgentGameDebugOverlays = {
6
+ worldBounds?: boolean;
7
+ anchors?: boolean;
8
+ camera?: boolean;
9
+ };
10
+
11
+ export function drawWorldBoundsDebugOverlay(
12
+ scene: TownOfficeSceneLike,
13
+ width: number,
14
+ height: number,
15
+ ): void {
16
+ scene.add
17
+ .rectangle(width / 2, height / 2, width, height, parseHexColor("#ff2d2d"))
18
+ .setDepth?.(debugDepth())
19
+ .setFillStyle?.(0xffffff, 0)
20
+ .setStrokeStyle?.(3, 0xff2d2d);
21
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./agent-game-scene.js";
2
+ export * from "./avatar-registry.js";
3
+ export * from "./camera-model.js";
4
+ export * from "./camera-controls.js";
5
+ export * from "./create-agent-game.js";
6
+ export * from "./debug-overlay.js";
7
+ export * from "./office-building-renderer.js";
8
+ export * from "./office-layout-renderer.js";
9
+ export * from "./scene-renderer.js";
10
+ export * from "./scene-reconciler.js";
11
+ export * from "./town-office-renderer.js";
12
+ export * from "./types.js";
13
+ export * from "./viewport.js";
@@ -0,0 +1,59 @@
1
+ export type MovementPoint = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+
6
+ export type MovementTweenConfig = {
7
+ x: number;
8
+ y: number;
9
+ duration: number;
10
+ ease: "Linear";
11
+ };
12
+
13
+ export type MovementTimingOptions = {
14
+ durationMs?: number;
15
+ speedPxPerSecond?: number;
16
+ minDurationMs?: number;
17
+ maxDurationMs?: number;
18
+ };
19
+
20
+ export function createMovementTweenConfig(
21
+ current: MovementPoint,
22
+ target: MovementPoint,
23
+ timing: number | MovementTimingOptions,
24
+ ): MovementTweenConfig | null {
25
+ if (current.x === target.x && current.y === target.y) {
26
+ return null;
27
+ }
28
+
29
+ const duration = resolveMovementDuration(current, target, timing);
30
+
31
+ return {
32
+ x: target.x,
33
+ y: target.y,
34
+ duration,
35
+ ease: "Linear",
36
+ };
37
+ }
38
+
39
+ export function resolveMovementDuration(
40
+ current: MovementPoint,
41
+ target: MovementPoint,
42
+ timing: number | MovementTimingOptions,
43
+ ): number {
44
+ if (typeof timing === "number") {
45
+ return timing;
46
+ }
47
+
48
+ if (!timing.speedPxPerSecond || timing.speedPxPerSecond <= 0) {
49
+ return timing.durationMs ?? 0;
50
+ }
51
+
52
+ const dx = target.x - current.x;
53
+ const dy = target.y - current.y;
54
+ const distance = Math.sqrt(dx * dx + dy * dy);
55
+ const rawDuration = Math.round((distance / timing.speedPxPerSecond) * 1000);
56
+ const minDuration = timing.minDurationMs ?? 0;
57
+ const maxDuration = timing.maxDurationMs ?? Number.POSITIVE_INFINITY;
58
+ return Math.min(maxDuration, Math.max(minDuration, rawDuration));
59
+ }
@@ -0,0 +1,48 @@
1
+ import type { AgentGameSceneDefinition } from "../core/index.js";
2
+ import { officeDepth } from "./render-layers.js";
3
+ import type { PhaserDisplayObjectLike } from "./scene-reconciler.js";
4
+
5
+ export type OfficeBackgroundSceneLike = {
6
+ add: {
7
+ rectangle: (x: number, y: number, width: number, height: number, fillColor: number) => PhaserDisplayObjectLike;
8
+ image?: (x: number, y: number, textureKey: string) => PhaserDisplayObjectLike;
9
+ };
10
+ };
11
+
12
+ export type OfficeBackgroundOptions = {
13
+ width: number;
14
+ height: number;
15
+ sceneImage?: {
16
+ key: string;
17
+ width: number;
18
+ height: number;
19
+ };
20
+ };
21
+
22
+ export function drawOfficeBackground(
23
+ scene: OfficeBackgroundSceneLike,
24
+ definition: AgentGameSceneDefinition,
25
+ options: OfficeBackgroundOptions,
26
+ ): void {
27
+ scene.add.rectangle(options.width / 2, options.height / 2, options.width, options.height, 0x111827).setDepth?.(officeDepth(0));
28
+
29
+ if (options.sceneImage && scene.add.image) {
30
+ const scale = Math.min(options.width / options.sceneImage.width, options.height / options.sceneImage.height);
31
+ scene.add.image(options.width / 2, options.height / 2, options.sceneImage.key).setScale?.(scale).setDepth?.(officeDepth(1));
32
+ return;
33
+ }
34
+
35
+ scene.add.rectangle(options.width / 2, 44, options.width - 64, 64, 0x1f2933).setDepth?.(officeDepth(1));
36
+
37
+ for (const anchor of definition.anchors) {
38
+ if (anchor.kind === "desk") {
39
+ scene.add.rectangle(anchor.x, anchor.y + 18, 72, 28, 0x26313a).setDepth?.(officeDepth(2));
40
+ }
41
+ if (anchor.kind === "meeting") {
42
+ scene.add.rectangle(anchor.x, anchor.y + 8, 104, 42, 0x3a4a52).setDepth?.(officeDepth(2));
43
+ }
44
+ if (anchor.kind === "break") {
45
+ scene.add.rectangle(anchor.x, anchor.y + 10, 86, 34, 0x40513f).setDepth?.(officeDepth(2));
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ getOfficeRoomBounds,
3
+ parseHexColor,
4
+ type OfficeBuildingLayout,
5
+ } from "../core/index.js";
6
+ import { officeDepth } from "./render-layers.js";
7
+ import type { PhaserDisplayObjectLike, PhaserTextLike } from "./scene-reconciler.js";
8
+ import { crispTextStyle } from "./text-style.js";
9
+
10
+ export type OfficeBuildingSceneLike = {
11
+ add: {
12
+ rectangle: (x: number, y: number, width: number, height: number, fillColor: number) => PhaserDisplayObjectLike;
13
+ text: (x: number, y: number, text: string, style?: Record<string, unknown>) => PhaserTextLike;
14
+ image?: (x: number, y: number, textureKey: string, frame?: string) => PhaserDisplayObjectLike;
15
+ };
16
+ };
17
+
18
+ export type OfficeBuildingRenderOptions = {
19
+ atlasKey: string;
20
+ };
21
+
22
+ const TITLE_BAR_HEIGHT = 34;
23
+
24
+ export function drawOfficeBuildingLayout(
25
+ scene: OfficeBuildingSceneLike,
26
+ layout: OfficeBuildingLayout,
27
+ options: OfficeBuildingRenderOptions,
28
+ ): void {
29
+ if (!scene.add.image) {
30
+ throw new Error("Office building rendering requires Phaser image support");
31
+ }
32
+
33
+ const size = getBuildingCanvasSize(layout);
34
+ scene.add
35
+ .rectangle(size.width / 2, size.height / 2, size.width, size.height, parseHexColor(layout.backgroundColor))
36
+ .setDepth?.(officeDepth(0));
37
+
38
+ for (const room of layout.rooms) {
39
+ const bounds = getOfficeRoomBounds(layout, room);
40
+ const floorColor = parseHexColor(room.theme.floorColor);
41
+ const titleColor = parseHexColor(room.theme.titleColor);
42
+ const borderColor = parseHexColor(room.theme.borderColor);
43
+ const titleTextColor = room.theme.titleTextColor ?? "#ffffff";
44
+
45
+ scene.add
46
+ .rectangle(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2, bounds.width, bounds.height, floorColor)
47
+ .setDepth?.(officeDepth(1))
48
+ .setStrokeStyle?.(2, borderColor);
49
+
50
+ scene.add
51
+ .rectangle(bounds.x + bounds.width / 2, bounds.y + TITLE_BAR_HEIGHT / 2, bounds.width, TITLE_BAR_HEIGHT, titleColor)
52
+ .setDepth?.(officeDepth(2));
53
+
54
+ scene.add
55
+ .text(bounds.x + bounds.width / 2, bounds.y + TITLE_BAR_HEIGHT / 2, room.name, crispTextStyle({
56
+ color: titleTextColor,
57
+ fontSize: "16px",
58
+ fontFamily: "sans-serif",
59
+ fontStyle: "700",
60
+ }))
61
+ .setOrigin?.(0.5, 0.5)
62
+ .setDepth?.(officeDepth(3));
63
+
64
+ for (const object of room.objects) {
65
+ const x = bounds.x + object.x;
66
+ const y = bounds.y + object.y;
67
+ const image = scene.add.image(x, y, options.atlasKey, object.frame);
68
+ image.setDepth?.(officeDepth(object.depth ?? y));
69
+ if (object.scale) {
70
+ image.setScale?.(object.scale);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ function getBuildingCanvasSize(layout: OfficeBuildingLayout): { width: number; height: number } {
77
+ let width = layout.padding * 2;
78
+ let height = layout.padding * 2;
79
+
80
+ for (const room of layout.rooms) {
81
+ const bounds = getOfficeRoomBounds(layout, room);
82
+ width = Math.max(width, bounds.x + bounds.width + layout.padding);
83
+ height = Math.max(height, bounds.y + bounds.height + layout.padding);
84
+ }
85
+
86
+ return { width, height };
87
+ }
@@ -0,0 +1,58 @@
1
+ import type { OfficeLayoutDefinition } from "../core/index.js";
2
+ import { officeDepth } from "./render-layers.js";
3
+ import type { PhaserDisplayObjectLike } from "./scene-reconciler.js";
4
+
5
+ export type OfficeLayoutSceneLike = {
6
+ add: {
7
+ rectangle?: (x: number, y: number, width: number, height: number, fillColor: number) => PhaserDisplayObjectLike;
8
+ image?: (x: number, y: number, textureKey: string, frame?: string) => PhaserDisplayObjectLike;
9
+ };
10
+ };
11
+
12
+ export type OfficeLayoutRenderOptions = {
13
+ atlasKey: string;
14
+ };
15
+
16
+ export function drawOfficeLayout(
17
+ scene: OfficeLayoutSceneLike,
18
+ layout: OfficeLayoutDefinition,
19
+ options: OfficeLayoutRenderOptions,
20
+ ): void {
21
+ if (!scene.add.image) {
22
+ throw new Error("Office layout rendering requires Phaser image support");
23
+ }
24
+
25
+ for (const wall of layout.walls ?? []) {
26
+ scene.add.rectangle?.(wall.x, wall.y, wall.width, wall.height, wall.fillColor).setDepth?.(officeDepth(wall.depth ?? 0));
27
+ }
28
+
29
+ const floorX = layout.floor.x ?? 0;
30
+ const floorY = layout.floor.y ?? 0;
31
+ const floorWidth = layout.floor.width ?? layout.width;
32
+ const floorHeight = layout.floor.height ?? layout.height;
33
+
34
+ if (layout.floorFill) {
35
+ scene.add.rectangle
36
+ ?.(floorX + floorWidth / 2, floorY + floorHeight / 2, floorWidth, floorHeight, layout.floorFill.fillColor)
37
+ .setDepth?.(officeDepth(layout.floorFill.depth ?? 1));
38
+ }
39
+
40
+ if (layout.floor.visible !== false) {
41
+ for (let y = floorY + layout.tileSize / 2; y < floorY + floorHeight; y += layout.tileSize) {
42
+ for (let x = floorX + layout.tileSize / 2; x < floorX + floorWidth; x += layout.tileSize) {
43
+ scene.add
44
+ .image(x, y, options.atlasKey, layout.floor.frame)
45
+ .setDisplaySize?.(layout.tileSize, layout.tileSize)
46
+ .setDepth?.(officeDepth(1));
47
+ }
48
+ }
49
+ }
50
+
51
+ for (const object of layout.objects) {
52
+ const image = scene.add.image(object.x, object.y, options.atlasKey, object.frame);
53
+ image.setDepth?.(officeDepth(object.depth ?? object.y));
54
+ if (object.scale) {
55
+ image.setScale?.(object.scale);
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,30 @@
1
+ export const RENDER_LAYERS = {
2
+ officeMin: 0,
3
+ officeMax: 9999,
4
+ agentBase: 20000,
5
+ agentUiBase: 40000,
6
+ debugBase: 90000,
7
+ } as const;
8
+
9
+ export function officeDepth(depth: number): number {
10
+ return clampDepth(depth, RENDER_LAYERS.officeMin, RENDER_LAYERS.officeMax);
11
+ }
12
+
13
+ export function agentDepth(worldY: number): number {
14
+ return RENDER_LAYERS.agentBase + Math.round(worldY);
15
+ }
16
+
17
+ export function agentUiDepth(offset = 0): number {
18
+ return RENDER_LAYERS.agentUiBase + offset;
19
+ }
20
+
21
+ export function debugDepth(offset = 0): number {
22
+ return RENDER_LAYERS.debugBase + offset;
23
+ }
24
+
25
+ function clampDepth(value: number, min: number, max: number): number {
26
+ if (!Number.isFinite(value)) {
27
+ return min;
28
+ }
29
+ return Math.min(max, Math.max(min, Math.round(value)));
30
+ }