@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,87 @@
1
+ import type { AgentAvatarDefinition, AgentGameSceneDefinition, OfficeBuildingLayout, OfficeLayoutDefinition } from "../core/index.js";
2
+ import { drawAnchorDebug } from "./anchor-debug.js";
3
+ import { registerAgentAvatarAnimations } from "./avatar-registry.js";
4
+ import {
5
+ installCameraControls,
6
+ type AgentGameCameraControlsController,
7
+ type AgentGameCameraControlsOptions,
8
+ } from "./camera-controls.js";
9
+ import type { AgentGameDebugOverlays } from "./debug-overlay.js";
10
+ import {
11
+ createDefaultAgentGameSceneRendererRegistry,
12
+ type AgentGameRendererConfig,
13
+ type AgentGameSceneRendererRegistry,
14
+ } from "./scene-renderer.js";
15
+ import type { AgentGameSceneReconciler, PhaserReconcilerSceneLike } from "./scene-reconciler.js";
16
+ import type { TownOfficeDomOverlayRoot } from "./town-office-renderer.js";
17
+
18
+ export const AGENT_GAME_PHASER_SCENE_KEY = "agent-game-scene";
19
+
20
+ export type AgentGamePhaserSceneConfig = {
21
+ key?: string;
22
+ definition: AgentGameSceneDefinition;
23
+ avatars: AgentAvatarDefinition[];
24
+ width: number;
25
+ height: number;
26
+ backgroundImage?: {
27
+ key: string;
28
+ url: string;
29
+ width: number;
30
+ height: number;
31
+ };
32
+ officeLayout?: {
33
+ atlasKey: string;
34
+ imageUrl: string;
35
+ atlasUrl: string;
36
+ definition: OfficeLayoutDefinition;
37
+ };
38
+ officeBuilding?: {
39
+ atlasKey: string;
40
+ imageUrl: string;
41
+ atlasUrl: string;
42
+ layout: OfficeBuildingLayout;
43
+ };
44
+ townOfficeBuilding?: {
45
+ layout: OfficeBuildingLayout;
46
+ };
47
+ renderer: AgentGameRendererConfig;
48
+ rendererRegistry?: AgentGameSceneRendererRegistry;
49
+ domOverlayRoot?: TownOfficeDomOverlayRoot;
50
+ debugOverlays?: AgentGameDebugOverlays;
51
+ showAnchorDebug?: boolean;
52
+ cameraControls?: AgentGameCameraControlsOptions;
53
+ movementDurationMs?: number;
54
+ onCameraControlsReady?: (controls: AgentGameCameraControlsController | null) => void;
55
+ onCreate?: (scene: PhaserReconcilerSceneLike) => AgentGameSceneReconciler;
56
+ };
57
+
58
+ export function createAgentGamePhaserSceneConfig(config: AgentGamePhaserSceneConfig) {
59
+ const rendererRegistry = config.rendererRegistry ?? createDefaultAgentGameSceneRendererRegistry();
60
+ const renderer = rendererRegistry.create(config.renderer.kind, {
61
+ ...(typeof config.renderer.options === "object" && config.renderer.options ? config.renderer.options : {}),
62
+ domOverlayRoot: config.domOverlayRoot,
63
+ });
64
+
65
+ return {
66
+ key: config.key ?? AGENT_GAME_PHASER_SCENE_KEY,
67
+ preload(this: PhaserReconcilerSceneLike) {
68
+ renderer.preload?.(this);
69
+ for (const avatar of config.avatars) {
70
+ this.load?.atlas(avatar.id, avatar.imageUrl, avatar.atlasUrl);
71
+ }
72
+ },
73
+ create(this: PhaserReconcilerSceneLike) {
74
+ for (const avatar of config.avatars) {
75
+ registerAgentAvatarAnimations(this, avatar);
76
+ }
77
+ renderer.create(this);
78
+ if (config.debugOverlays?.anchors || config.showAnchorDebug) {
79
+ drawAnchorDebug(this, config.definition);
80
+ }
81
+ if (config.cameraControls) {
82
+ config.onCameraControlsReady?.(installCameraControls(this, config.cameraControls));
83
+ }
84
+ config.onCreate?.(this);
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,22 @@
1
+ import type { AgentGameSceneDefinition } from "../core/index.js";
2
+ import { debugDepth } from "./render-layers.js";
3
+ import type { PhaserDisplayObjectLike, PhaserTextLike } from "./scene-reconciler.js";
4
+ import { crispTextStyle } from "./text-style.js";
5
+
6
+ export type AnchorDebugSceneLike = {
7
+ add: {
8
+ rectangle: (x: number, y: number, width: number, height: number, fillColor: number) => PhaserDisplayObjectLike;
9
+ text: (x: number, y: number, text: string, style?: Record<string, unknown>) => PhaserTextLike;
10
+ };
11
+ };
12
+
13
+ export function drawAnchorDebug(scene: AnchorDebugSceneLike, definition: AgentGameSceneDefinition): void {
14
+ for (const anchor of definition.anchors) {
15
+ scene.add.rectangle(anchor.x, anchor.y, 8, 8, 0xf97316).setDepth?.(debugDepth(10));
16
+ scene.add.text(anchor.x + 8, anchor.y + 6, anchor.id, crispTextStyle({
17
+ color: "#f8fafc",
18
+ fontSize: "10px",
19
+ fontFamily: "monospace",
20
+ })).setDepth?.(debugDepth(11));
21
+ }
22
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ AgentGameError,
3
+ REQUIRED_AGENT_AVATAR_ANIMATIONS,
4
+ normalizeAgentAvatarAnimationFrames,
5
+ validateAgentAvatarDefinition,
6
+ type AgentAvatarDefinition,
7
+ type AgentAvatarAnimationName,
8
+ } from "../core/index.js";
9
+
10
+ export type PhaserAnimationSceneLike = {
11
+ textures?: {
12
+ getFrame?: (textureKey: string, frame: string) => unknown;
13
+ };
14
+ anims?: {
15
+ create: (config: {
16
+ key: string;
17
+ frames: Array<{ key: string; frame: string }>;
18
+ frameRate: number;
19
+ repeat: number;
20
+ }) => unknown;
21
+ };
22
+ };
23
+
24
+ export function createAgentAvatarAnimationKey(avatarId: string, animation: AgentAvatarAnimationName): string {
25
+ return `${avatarId}:${animation}`;
26
+ }
27
+
28
+ export function registerAgentAvatarAnimations(scene: PhaserAnimationSceneLike, avatar: AgentAvatarDefinition): void {
29
+ validateAgentAvatarDefinition(avatar);
30
+
31
+ for (const animation of REQUIRED_AGENT_AVATAR_ANIMATIONS) {
32
+ const frames = normalizeAgentAvatarAnimationFrames(avatar.animations[animation]);
33
+ for (const frame of frames) {
34
+ if (scene.textures?.getFrame && !scene.textures.getFrame(avatar.id, frame)) {
35
+ throw new AgentGameError("missing_animation", `Agent game avatar ${avatar.id} is missing frame: ${frame}`);
36
+ }
37
+ }
38
+
39
+ scene.anims?.create({
40
+ key: createAgentAvatarAnimationKey(avatar.id, animation),
41
+ frames: frames.map((frame) => ({ key: avatar.id, frame })),
42
+ frameRate: animation.startsWith("walk.") ? 8 : 4,
43
+ repeat: -1,
44
+ });
45
+ }
46
+ }
@@ -0,0 +1,419 @@
1
+ import {
2
+ clampCameraScrollValue,
3
+ dragCamera,
4
+ getCameraScrollBounds,
5
+ snapCameraScrollToPixel,
6
+ zoomAroundPoint,
7
+ } from "./camera-model.js";
8
+
9
+ export type AgentGameCameraControlsOptions = {
10
+ enabled?: boolean;
11
+ worldWidth: number;
12
+ worldHeight: number;
13
+ initialZoom?: number | "fit-width";
14
+ minZoom?: number;
15
+ maxZoom?: number;
16
+ zoomStep?: number;
17
+ zoomLevels?: number[];
18
+ wheelZoom?: boolean;
19
+ centerWhenViewLarger?: boolean;
20
+ onZoomChange?: (zoom: number) => void;
21
+ onViewChange?: (view: AgentGameCameraViewState) => void;
22
+ };
23
+
24
+ export type AgentGameCameraControlsController = {
25
+ getZoom(): number;
26
+ zoomIn(): void;
27
+ zoomOut(): void;
28
+ setZoom(zoom: number): void;
29
+ };
30
+
31
+ export type AgentGameCameraViewState = {
32
+ zoom: number;
33
+ scrollX: number;
34
+ scrollY: number;
35
+ visibleWidth: number;
36
+ visibleHeight: number;
37
+ bounds: {
38
+ minX: number;
39
+ maxX: number;
40
+ minY: number;
41
+ maxY: number;
42
+ };
43
+ };
44
+
45
+ export type PhaserCameraControlsSceneLike = {
46
+ cameras?: {
47
+ main?: PhaserCameraLike;
48
+ };
49
+ input?: {
50
+ on: (eventName: string, handler: (...args: any[]) => void) => unknown;
51
+ };
52
+ game?: {
53
+ canvas?: HTMLCanvasElement;
54
+ };
55
+ sys?: {
56
+ game?: {
57
+ canvas?: HTMLCanvasElement;
58
+ };
59
+ };
60
+ };
61
+
62
+ type PhaserCameraLike = {
63
+ zoom: number;
64
+ scrollX: number;
65
+ scrollY: number;
66
+ width?: number;
67
+ height?: number;
68
+ setBounds?: (x: number, y: number, width: number, height: number) => unknown;
69
+ setOrigin?: (x: number, y: number) => unknown;
70
+ setZoom?: (zoom: number) => unknown;
71
+ };
72
+
73
+ type PointerLike = {
74
+ x: number;
75
+ y: number;
76
+ isDown?: boolean;
77
+ };
78
+
79
+ export function installCameraControls(
80
+ scene: PhaserCameraControlsSceneLike,
81
+ options: AgentGameCameraControlsOptions,
82
+ ): AgentGameCameraControlsController | null {
83
+ if (options.enabled === false) {
84
+ return null;
85
+ }
86
+
87
+ const camera = scene.cameras?.main;
88
+ if (!camera || !scene.input) {
89
+ return null;
90
+ }
91
+
92
+ const minZoom = options.minZoom ?? 0.5;
93
+ const maxZoom = options.maxZoom ?? 2;
94
+ const zoomStep = options.zoomStep ?? 0.1;
95
+ camera.setOrigin?.(0, 0);
96
+ camera.setZoom?.(resolveInitialZoom(camera, options, minZoom, maxZoom));
97
+ clampCameraScroll(camera, options.worldWidth, options.worldHeight);
98
+ centerCameraIfViewLarger(camera, options);
99
+ snapCameraScroll(camera);
100
+ emitCameraView(camera, options);
101
+
102
+ const controls: AgentGameCameraControlsController = {
103
+ getZoom() {
104
+ return camera.zoom;
105
+ },
106
+ zoomIn() {
107
+ zoomCamera(camera, options, getNextZoom(camera.zoom, 1, {
108
+ minZoom,
109
+ maxZoom,
110
+ zoomStep,
111
+ zoomLevels: options.zoomLevels,
112
+ }));
113
+ },
114
+ zoomOut() {
115
+ zoomCamera(camera, options, getNextZoom(camera.zoom, -1, {
116
+ minZoom,
117
+ maxZoom,
118
+ zoomStep,
119
+ zoomLevels: options.zoomLevels,
120
+ }));
121
+ },
122
+ setZoom(zoom: number) {
123
+ zoomCamera(camera, options, clamp(zoom, minZoom, maxZoom));
124
+ },
125
+ };
126
+
127
+ let dragStart: { x: number; y: number; scrollX: number; scrollY: number } | null = null;
128
+ let windowMoveHandler: ((event: PointerEvent) => void) | null = null;
129
+ let windowUpHandler: (() => void) | null = null;
130
+
131
+ if (options.wheelZoom !== false) {
132
+ scene.input.on("wheel", (pointer: PointerLike, _gameObjects: unknown, _deltaX: number, deltaY: number) => {
133
+ const direction = deltaY < 0 ? 1 : -1;
134
+ const newZoom = getNextZoom(camera.zoom, direction, {
135
+ minZoom,
136
+ maxZoom,
137
+ zoomStep,
138
+ zoomLevels: options.zoomLevels,
139
+ });
140
+ zoomCamera(camera, options, newZoom, getZoomAnchor(pointer, camera));
141
+ });
142
+ }
143
+
144
+ scene.input.on("pointerdown", (pointer: PointerLike) => {
145
+ dragStart = {
146
+ x: pointer.x,
147
+ y: pointer.y,
148
+ scrollX: camera.scrollX,
149
+ scrollY: camera.scrollY,
150
+ };
151
+ installWindowDragHandlers(scene, camera, options, () => dragStart, (point) => {
152
+ updateDrag(camera, options, dragStart, point);
153
+ }, () => {
154
+ dragStart = null;
155
+ uninstallWindowDragHandlers(windowMoveHandler, windowUpHandler);
156
+ windowMoveHandler = null;
157
+ windowUpHandler = null;
158
+ }, (moveHandler, upHandler) => {
159
+ windowMoveHandler = moveHandler;
160
+ windowUpHandler = upHandler;
161
+ });
162
+ });
163
+
164
+ scene.input.on("pointermove", (pointer: PointerLike) => {
165
+ if (!dragStart || pointer.isDown === false) {
166
+ return;
167
+ }
168
+ updateDrag(camera, options, dragStart, pointer);
169
+ });
170
+
171
+ const stopDrag = () => {
172
+ dragStart = null;
173
+ uninstallWindowDragHandlers(windowMoveHandler, windowUpHandler);
174
+ windowMoveHandler = null;
175
+ windowUpHandler = null;
176
+ };
177
+ scene.input.on("pointerup", stopDrag);
178
+ scene.input.on("pointerupoutside", stopDrag);
179
+
180
+ return controls;
181
+ }
182
+
183
+ function zoomCamera(
184
+ camera: PhaserCameraLike,
185
+ options: AgentGameCameraControlsOptions,
186
+ targetZoom: number,
187
+ anchor = getZoomAnchor(undefined, camera),
188
+ ): void {
189
+ const zoomed = zoomAroundPoint({
190
+ scrollX: camera.scrollX,
191
+ scrollY: camera.scrollY,
192
+ zoom: camera.zoom,
193
+ targetZoom,
194
+ anchorX: anchor.x,
195
+ anchorY: anchor.y,
196
+ });
197
+
198
+ camera.setZoom?.(zoomed.zoom);
199
+ camera.scrollX = zoomed.scrollX;
200
+ camera.scrollY = zoomed.scrollY;
201
+ clampCameraScroll(camera, options.worldWidth, options.worldHeight);
202
+ centerCameraIfViewLarger(camera, options);
203
+ snapCameraScroll(camera);
204
+ emitCameraView(camera, options);
205
+ }
206
+
207
+ function updateDrag(
208
+ camera: PhaserCameraLike,
209
+ options: AgentGameCameraControlsOptions,
210
+ dragStart: { x: number; y: number; scrollX: number; scrollY: number } | null,
211
+ point: { x: number; y: number },
212
+ ): void {
213
+ if (!dragStart) {
214
+ return;
215
+ }
216
+
217
+ const next = dragCamera({
218
+ startX: dragStart.x,
219
+ startY: dragStart.y,
220
+ currentX: point.x,
221
+ currentY: point.y,
222
+ startScrollX: dragStart.scrollX,
223
+ startScrollY: dragStart.scrollY,
224
+ zoom: camera.zoom,
225
+ xBounds: getCameraScrollBounds(options.worldWidth, camera.width ?? 0, camera.zoom),
226
+ yBounds: getCameraScrollBounds(options.worldHeight, camera.height ?? 0, camera.zoom),
227
+ });
228
+ camera.scrollX = next.scrollX;
229
+ camera.scrollY = next.scrollY;
230
+ snapCameraScroll(camera);
231
+ emitCameraView(camera, options);
232
+ }
233
+
234
+ function installWindowDragHandlers(
235
+ scene: PhaserCameraControlsSceneLike,
236
+ camera: PhaserCameraLike,
237
+ options: AgentGameCameraControlsOptions,
238
+ getDragStart: () => { x: number; y: number; scrollX: number; scrollY: number } | null,
239
+ update: (point: { x: number; y: number }) => void,
240
+ stop: () => void,
241
+ setHandlers: (moveHandler: (event: PointerEvent) => void, upHandler: () => void) => void,
242
+ ): void {
243
+ if (!globalThis.window || !globalThis.window.addEventListener) {
244
+ return;
245
+ }
246
+
247
+ const canvas = getSceneCanvas(scene);
248
+ if (!canvas) {
249
+ return;
250
+ }
251
+
252
+ const moveHandler = (event: PointerEvent) => {
253
+ if (!getDragStart()) {
254
+ return;
255
+ }
256
+ update(clientPointToGamePoint(event.clientX, event.clientY, canvas, camera));
257
+ };
258
+ const upHandler = () => {
259
+ stop();
260
+ };
261
+ setHandlers(moveHandler, upHandler);
262
+ globalThis.window.addEventListener("pointermove", moveHandler);
263
+ globalThis.window.addEventListener("pointerup", upHandler);
264
+ }
265
+
266
+ function uninstallWindowDragHandlers(
267
+ moveHandler: ((event: PointerEvent) => void) | null,
268
+ upHandler: (() => void) | null,
269
+ ): void {
270
+ if (!globalThis.window || !globalThis.window.removeEventListener) {
271
+ return;
272
+ }
273
+ if (moveHandler) {
274
+ globalThis.window.removeEventListener("pointermove", moveHandler);
275
+ }
276
+ if (upHandler) {
277
+ globalThis.window.removeEventListener("pointerup", upHandler);
278
+ }
279
+ }
280
+
281
+ function getSceneCanvas(scene: PhaserCameraControlsSceneLike): HTMLCanvasElement | undefined {
282
+ return scene.game?.canvas ?? scene.sys?.game?.canvas;
283
+ }
284
+
285
+ function clientPointToGamePoint(
286
+ clientX: number,
287
+ clientY: number,
288
+ canvas: HTMLCanvasElement,
289
+ camera: PhaserCameraLike,
290
+ ): { x: number; y: number } {
291
+ const bounds = canvas.getBoundingClientRect();
292
+ const width = camera.width ?? bounds.width;
293
+ const height = camera.height ?? bounds.height;
294
+
295
+ return {
296
+ x: bounds.width > 0 ? (clientX - bounds.left) * width / bounds.width : clientX,
297
+ y: bounds.height > 0 ? (clientY - bounds.top) * height / bounds.height : clientY,
298
+ };
299
+ }
300
+
301
+ function clamp(value: number, min: number, max: number): number {
302
+ return Math.min(max, Math.max(min, value));
303
+ }
304
+
305
+ function resolveInitialZoom(
306
+ camera: PhaserCameraLike,
307
+ options: AgentGameCameraControlsOptions,
308
+ minZoom: number,
309
+ maxZoom: number,
310
+ ): number {
311
+ if (options.initialZoom === "fit-width") {
312
+ const width = camera.width ?? 0;
313
+ if (width > 0 && options.worldWidth > 0) {
314
+ return clamp(width / options.worldWidth, minZoom, maxZoom);
315
+ }
316
+ return clamp(camera.zoom ?? 1, minZoom, maxZoom);
317
+ }
318
+
319
+ return clamp(options.initialZoom ?? camera.zoom ?? 1, minZoom, maxZoom);
320
+ }
321
+
322
+ function getNextZoom(
323
+ currentZoom: number,
324
+ direction: number,
325
+ options: {
326
+ minZoom: number;
327
+ maxZoom: number;
328
+ zoomStep: number;
329
+ zoomLevels?: number[];
330
+ },
331
+ ): number {
332
+ const levels = normalizeZoomLevels(options.zoomLevels, options.minZoom, options.maxZoom);
333
+ if (levels.length > 0) {
334
+ if (direction > 0) {
335
+ return levels.find((level) => level > currentZoom + Number.EPSILON) ?? levels.at(-1)!;
336
+ }
337
+ return [...levels].reverse().find((level) => level < currentZoom - Number.EPSILON) ?? levels[0]!;
338
+ }
339
+
340
+ return clamp(currentZoom + direction * options.zoomStep, options.minZoom, options.maxZoom);
341
+ }
342
+
343
+ function normalizeZoomLevels(levels: number[] | undefined, minZoom: number, maxZoom: number): number[] {
344
+ return [...new Set((levels ?? [])
345
+ .filter((level) => Number.isFinite(level) && level >= minZoom && level <= maxZoom))]
346
+ .sort((a, b) => a - b);
347
+ }
348
+
349
+ function getZoomAnchor(pointer: PointerLike | undefined, camera: PhaserCameraLike): { x: number; y: number } {
350
+ return {
351
+ x: Number.isFinite(pointer?.x) ? pointer!.x : (camera.width ?? 0) / 2,
352
+ y: Number.isFinite(pointer?.y) ? pointer!.y : (camera.height ?? 0) / 2,
353
+ };
354
+ }
355
+
356
+ function clampScroll(value: number, worldSize: number, viewportSize: number | undefined, zoom: number): number {
357
+ if (!viewportSize || viewportSize <= 0) {
358
+ return value;
359
+ }
360
+
361
+ return clampCameraScrollValue(value, getCameraScrollBounds(worldSize, viewportSize, zoom));
362
+ }
363
+
364
+ function clampCameraScroll(camera: PhaserCameraLike, worldWidth: number, worldHeight: number): void {
365
+ camera.scrollX = clampScroll(camera.scrollX, worldWidth, camera.width, camera.zoom);
366
+ camera.scrollY = clampScroll(camera.scrollY, worldHeight, camera.height, camera.zoom);
367
+ }
368
+
369
+ function snapCameraScroll(camera: PhaserCameraLike): void {
370
+ camera.scrollX = snapCameraScrollToPixel(camera.scrollX, camera.zoom);
371
+ camera.scrollY = snapCameraScrollToPixel(camera.scrollY, camera.zoom);
372
+ }
373
+
374
+ function centerCameraIfViewLarger(camera: PhaserCameraLike, options: AgentGameCameraControlsOptions): void {
375
+ if (!options.centerWhenViewLarger) {
376
+ return;
377
+ }
378
+
379
+ const width = camera.width ?? 0;
380
+ const height = camera.height ?? 0;
381
+ if (width > 0 && width / camera.zoom > options.worldWidth) {
382
+ const bounds = getCameraScrollBounds(options.worldWidth, width, camera.zoom);
383
+ camera.scrollX = (bounds.min + bounds.max) / 2;
384
+ }
385
+ if (height > 0 && height / camera.zoom > options.worldHeight) {
386
+ const bounds = getCameraScrollBounds(options.worldHeight, height, camera.zoom);
387
+ camera.scrollY = (bounds.min + bounds.max) / 2;
388
+ }
389
+ }
390
+
391
+ function emitCameraView(camera: PhaserCameraLike, options: AgentGameCameraControlsOptions): void {
392
+ options.onZoomChange?.(camera.zoom);
393
+ options.onViewChange?.(createCameraViewState(camera, options.worldWidth, options.worldHeight));
394
+ }
395
+
396
+ function createCameraViewState(
397
+ camera: PhaserCameraLike,
398
+ worldWidth: number,
399
+ worldHeight: number,
400
+ ): AgentGameCameraViewState {
401
+ const visibleWidth = camera.width && camera.width > 0 ? camera.width / camera.zoom : 0;
402
+ const visibleHeight = camera.height && camera.height > 0 ? camera.height / camera.zoom : 0;
403
+ const xBounds = getCameraScrollBounds(worldWidth, camera.width ?? 0, camera.zoom);
404
+ const yBounds = getCameraScrollBounds(worldHeight, camera.height ?? 0, camera.zoom);
405
+
406
+ return {
407
+ zoom: camera.zoom,
408
+ scrollX: camera.scrollX,
409
+ scrollY: camera.scrollY,
410
+ visibleWidth,
411
+ visibleHeight,
412
+ bounds: {
413
+ minX: xBounds.min,
414
+ maxX: xBounds.max,
415
+ minY: yBounds.min,
416
+ maxY: yBounds.max,
417
+ },
418
+ };
419
+ }
@@ -0,0 +1,81 @@
1
+ export type CameraScrollBounds = {
2
+ min: number;
3
+ max: number;
4
+ };
5
+
6
+ export type ZoomAroundPointInput = {
7
+ scrollX: number;
8
+ scrollY: number;
9
+ zoom: number;
10
+ targetZoom: number;
11
+ anchorX: number;
12
+ anchorY: number;
13
+ };
14
+
15
+ export type DragCameraInput = {
16
+ startX: number;
17
+ startY: number;
18
+ currentX: number;
19
+ currentY: number;
20
+ startScrollX: number;
21
+ startScrollY: number;
22
+ zoom: number;
23
+ xBounds: CameraScrollBounds;
24
+ yBounds: CameraScrollBounds;
25
+ };
26
+
27
+ export function getCameraScrollBounds(worldSize: number, viewportSize: number, zoom: number): CameraScrollBounds {
28
+ if (viewportSize <= 0) {
29
+ return { min: 0, max: worldSize };
30
+ }
31
+
32
+ const visibleWorldSize = viewportSize / zoom;
33
+ const edgeAlignedScroll = worldSize - visibleWorldSize;
34
+ return {
35
+ min: Math.min(0, edgeAlignedScroll),
36
+ max: Math.max(0, edgeAlignedScroll),
37
+ };
38
+ }
39
+
40
+ export function clampCameraScrollValue(value: number, bounds: CameraScrollBounds): number {
41
+ return Math.min(bounds.max, Math.max(bounds.min, value));
42
+ }
43
+
44
+ export function getWorldOriginScreenPosition(scrollX: number, scrollY: number, zoom: number): { x: number; y: number } {
45
+ return {
46
+ x: -scrollX * zoom,
47
+ y: -scrollY * zoom,
48
+ };
49
+ }
50
+
51
+ export function snapCameraScrollToPixel(scroll: number, zoom: number): number {
52
+ if (!Number.isFinite(scroll) || !Number.isFinite(zoom) || zoom <= 0) {
53
+ return scroll;
54
+ }
55
+
56
+ return Math.round(scroll * zoom) / zoom;
57
+ }
58
+
59
+ export function zoomAroundPoint(input: ZoomAroundPointInput): { zoom: number; scrollX: number; scrollY: number } {
60
+ const anchorWorldX = input.scrollX + input.anchorX / input.zoom;
61
+ const anchorWorldY = input.scrollY + input.anchorY / input.zoom;
62
+
63
+ return {
64
+ zoom: input.targetZoom,
65
+ scrollX: anchorWorldX - input.anchorX / input.targetZoom,
66
+ scrollY: anchorWorldY - input.anchorY / input.targetZoom,
67
+ };
68
+ }
69
+
70
+ export function dragCamera(input: DragCameraInput): { scrollX: number; scrollY: number } {
71
+ return {
72
+ scrollX: clampCameraScrollValue(
73
+ input.startScrollX + (input.startX - input.currentX) / input.zoom,
74
+ input.xBounds,
75
+ ),
76
+ scrollY: clampCameraScrollValue(
77
+ input.startScrollY + (input.startY - input.currentY) / input.zoom,
78
+ input.yBounds,
79
+ ),
80
+ };
81
+ }