@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,376 @@
1
+ import { AgentGameError } from "./errors.js";
2
+ import type { OfficeObjectAtlasDefinition } from "./office-layout.js";
3
+ import type {
4
+ AgentGameAnchor,
5
+ AgentGameAnchorKind,
6
+ AgentGameDirection,
7
+ AgentGameSceneDefinition,
8
+ } from "./scene.js";
9
+ import { validateAgentGameScene } from "./scene.js";
10
+ import { createTownOfficeRoomAnchors } from "./town-office-seat-layout.js";
11
+ import { isTownOfficeRoomType, type TownOfficeRoomType } from "./town-office-assets.js";
12
+
13
+ export type OfficeBuildingGridRect = {
14
+ x: number;
15
+ y: number;
16
+ w: number;
17
+ h: number;
18
+ };
19
+
20
+ export type OfficeBuildingRatio = {
21
+ width: number;
22
+ height: number;
23
+ };
24
+
25
+ export type OfficeBuildingRoomTheme = {
26
+ titleColor: string;
27
+ floorColor: string;
28
+ borderColor: string;
29
+ titleTextColor?: string;
30
+ gridColor?: string;
31
+ };
32
+
33
+ export type OfficeBuildingObjectDefinition = {
34
+ id: string;
35
+ frame: string;
36
+ x: number;
37
+ y: number;
38
+ scale?: number;
39
+ depth?: number;
40
+ };
41
+
42
+ export type OfficeBuildingAnchorDefinition = {
43
+ id: string;
44
+ kind: AgentGameAnchorKind;
45
+ x: number;
46
+ y: number;
47
+ direction?: AgentGameDirection;
48
+ };
49
+
50
+ export type OfficeBuildingRoomDefinition = {
51
+ id: string;
52
+ name: string;
53
+ type?: TownOfficeRoomType;
54
+ row?: number;
55
+ weight?: number;
56
+ rowHeight?: number;
57
+ grid: OfficeBuildingGridRect;
58
+ theme: OfficeBuildingRoomTheme;
59
+ objects: OfficeBuildingObjectDefinition[];
60
+ anchors: OfficeBuildingAnchorDefinition[];
61
+ };
62
+
63
+ export type OfficeBuildingLayout = {
64
+ id: string;
65
+ cellSize: number;
66
+ gap: number;
67
+ padding: number;
68
+ ratio?: OfficeBuildingRatio;
69
+ backgroundColor: string;
70
+ rooms: OfficeBuildingRoomDefinition[];
71
+ };
72
+
73
+ export type OfficeRoomBounds = {
74
+ x: number;
75
+ y: number;
76
+ width: number;
77
+ height: number;
78
+ };
79
+
80
+ export function validateOfficeBuildingLayout(
81
+ layout: OfficeBuildingLayout,
82
+ atlas: OfficeObjectAtlasDefinition,
83
+ ): OfficeBuildingLayout {
84
+ assertNonEmpty(layout.id, "office building layout id");
85
+ assertPositive(layout.cellSize, "office building cell size");
86
+ assertFiniteNumber(layout.gap, "office building gap");
87
+ assertFiniteNumber(layout.padding, "office building padding");
88
+ validateRatio(layout.ratio);
89
+ parseHexColor(layout.backgroundColor);
90
+
91
+ const roomIds = new Set<string>();
92
+ const anchorIds = new Set<string>();
93
+ for (const room of layout.rooms) {
94
+ assertNonEmpty(room.id, "office building room id");
95
+ assertNonEmpty(room.name, "office building room name");
96
+ if (roomIds.has(room.id)) {
97
+ throw new AgentGameError("invalid_asset_manifest", `Duplicate office building room id: ${room.id}`);
98
+ }
99
+ roomIds.add(room.id);
100
+ if (room.type && !isTownOfficeRoomType(room.type)) {
101
+ throw new AgentGameError("invalid_asset_manifest", `Unsupported office building room type: ${room.type}`);
102
+ }
103
+ validateRowLayoutHints(room);
104
+ assertGrid(room.grid, room.id);
105
+ validateTheme(room.theme);
106
+
107
+ for (const object of room.objects) {
108
+ assertNonEmpty(object.id, "office building object id");
109
+ assertNonEmpty(object.frame, "office building object frame");
110
+ if (!atlas.frames[object.frame]) {
111
+ throw new AgentGameError("invalid_asset_manifest", `Office building object references missing frame: ${object.frame}`);
112
+ }
113
+ assertFiniteNumber(object.x, `office building object ${object.id} x`);
114
+ assertFiniteNumber(object.y, `office building object ${object.id} y`);
115
+ }
116
+
117
+ for (const anchor of room.anchors) {
118
+ assertNonEmpty(anchor.id, "office building anchor id");
119
+ if (anchorIds.has(anchor.id)) {
120
+ throw new AgentGameError("invalid_asset_manifest", `Duplicate office building anchor id: ${anchor.id}`);
121
+ }
122
+ anchorIds.add(anchor.id);
123
+ assertFiniteNumber(anchor.x, `office building anchor ${anchor.id} x`);
124
+ assertFiniteNumber(anchor.y, `office building anchor ${anchor.id} y`);
125
+ }
126
+ }
127
+
128
+ return layout;
129
+ }
130
+
131
+ export function createOfficeBuildingSceneDefinition(layout: OfficeBuildingLayout): AgentGameSceneDefinition {
132
+ const normalizedLayout = normalizeOfficeBuildingLayoutForRatio(layout);
133
+ const anchors: AgentGameAnchor[] = [];
134
+ for (const room of normalizedLayout.rooms) {
135
+ const bounds = getOfficeRoomBounds(normalizedLayout, room);
136
+ const roomAnchors = room.anchors.length > 0 || !room.type
137
+ ? room.anchors
138
+ : createTownOfficeRoomAnchors({
139
+ roomId: room.id,
140
+ roomType: room.type,
141
+ width: bounds.width,
142
+ height: bounds.height,
143
+ });
144
+ for (const anchor of roomAnchors) {
145
+ anchors.push({
146
+ id: anchor.id,
147
+ kind: anchor.kind,
148
+ x: bounds.x + anchor.x,
149
+ y: bounds.y + anchor.y,
150
+ direction: anchor.direction,
151
+ });
152
+ }
153
+ }
154
+
155
+ return validateAgentGameScene({
156
+ id: layout.id,
157
+ map: {
158
+ key: layout.id,
159
+ url: `/assets/layouts/${layout.id}.json`,
160
+ tilesets: [],
161
+ },
162
+ anchors,
163
+ });
164
+ }
165
+
166
+ export function normalizeOfficeBuildingLayoutForRatio(layout: OfficeBuildingLayout): OfficeBuildingLayout {
167
+ if (!layout.ratio || layout.rooms.length === 0) {
168
+ return layout;
169
+ }
170
+
171
+ if (usesRowFillLayout(layout)) {
172
+ return normalizeRowFillOfficeBuildingLayout(layout);
173
+ }
174
+
175
+ const targetColumns = getTargetGridColumnCount(layout);
176
+ const rooms: OfficeBuildingRoomDefinition[] = [];
177
+ let cursorX = 0;
178
+ let cursorY = 0;
179
+ let rowHeight = 0;
180
+
181
+ for (const room of layout.rooms) {
182
+ if (cursorX > 0 && cursorX + room.grid.w > targetColumns) {
183
+ cursorX = 0;
184
+ cursorY += rowHeight;
185
+ rowHeight = 0;
186
+ }
187
+
188
+ rooms.push({
189
+ ...room,
190
+ grid: {
191
+ ...room.grid,
192
+ x: cursorX,
193
+ y: cursorY,
194
+ },
195
+ });
196
+ cursorX += room.grid.w;
197
+ rowHeight = Math.max(rowHeight, room.grid.h);
198
+ }
199
+
200
+ return {
201
+ ...layout,
202
+ rooms,
203
+ };
204
+ }
205
+
206
+ export function getOfficeRoomBounds(
207
+ layout: OfficeBuildingLayout,
208
+ room: OfficeBuildingRoomDefinition,
209
+ ): OfficeRoomBounds {
210
+ return {
211
+ x: layout.padding + room.grid.x * (layout.cellSize + layout.gap),
212
+ y: layout.padding + room.grid.y * (layout.cellSize + layout.gap),
213
+ width: room.grid.w * layout.cellSize + (room.grid.w - 1) * layout.gap,
214
+ height: room.grid.h * layout.cellSize + (room.grid.h - 1) * layout.gap,
215
+ };
216
+ }
217
+
218
+ export function measureOfficeBuildingLayoutSize(layout: OfficeBuildingLayout): { width: number; height: number } {
219
+ const measuredLayout = normalizeOfficeBuildingLayoutForRatio(layout);
220
+ let width = layout.padding * 2;
221
+ let height = layout.padding * 2;
222
+
223
+ for (const room of measuredLayout.rooms) {
224
+ const bounds = getOfficeRoomBounds(measuredLayout, room);
225
+ width = Math.max(width, bounds.x + bounds.width + measuredLayout.padding);
226
+ height = Math.max(height, bounds.y + bounds.height + measuredLayout.padding);
227
+ }
228
+
229
+ return { width, height };
230
+ }
231
+
232
+ export function parseHexColor(hex: string): number {
233
+ if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
234
+ throw new AgentGameError("invalid_asset_manifest", `Office building color must be a #rrggbb hex color: ${hex}`);
235
+ }
236
+ return Number.parseInt(hex.slice(1), 16);
237
+ }
238
+
239
+ function validateTheme(theme: OfficeBuildingRoomTheme): void {
240
+ parseHexColor(theme.titleColor);
241
+ parseHexColor(theme.floorColor);
242
+ parseHexColor(theme.borderColor);
243
+ if (theme.titleTextColor) {
244
+ parseHexColor(theme.titleTextColor);
245
+ }
246
+ if (theme.gridColor) {
247
+ parseHexColor(theme.gridColor);
248
+ }
249
+ }
250
+
251
+ function validateRatio(ratio: OfficeBuildingRatio | undefined): void {
252
+ if (!ratio) {
253
+ return;
254
+ }
255
+ assertPositive(ratio.width, "office building ratio width");
256
+ assertPositive(ratio.height, "office building ratio height");
257
+ }
258
+
259
+ function validateRowLayoutHints(room: OfficeBuildingRoomDefinition): void {
260
+ if (room.row !== undefined) {
261
+ assertFiniteNumber(room.row, `office building room ${room.id} row`);
262
+ if (!Number.isInteger(room.row) || room.row < 0) {
263
+ throw new AgentGameError("invalid_asset_manifest", `Office building room ${room.id} row must be a non-negative integer`);
264
+ }
265
+ }
266
+ if (room.weight !== undefined) {
267
+ assertPositive(room.weight, `office building room ${room.id} weight`);
268
+ }
269
+ if (room.rowHeight !== undefined) {
270
+ assertPositive(room.rowHeight, `office building room ${room.id} rowHeight`);
271
+ }
272
+ }
273
+
274
+ function usesRowFillLayout(layout: OfficeBuildingLayout): boolean {
275
+ return layout.rooms.some((room) => room.row !== undefined || room.weight !== undefined || room.rowHeight !== undefined);
276
+ }
277
+
278
+ function normalizeRowFillOfficeBuildingLayout(layout: OfficeBuildingLayout): OfficeBuildingLayout {
279
+ const rows = groupRoomsByRow(layout.rooms);
280
+ const totalGridHeight = rows.reduce((sum, row) => sum + row.height, 0);
281
+ const worldHeight = layout.padding * 2 + totalGridHeight * (layout.cellSize + layout.gap) - layout.gap;
282
+ const targetWorldWidth = worldHeight * (layout.ratio!.width / layout.ratio!.height);
283
+ const targetContentWidth = Math.max(layout.cellSize, targetWorldWidth - layout.padding * 2);
284
+ const targetGridWidth = (targetContentWidth + layout.gap) / (layout.cellSize + layout.gap);
285
+ const rooms: OfficeBuildingRoomDefinition[] = [];
286
+ let cursorY = 0;
287
+
288
+ for (const row of rows) {
289
+ const totalWeight = row.rooms.reduce((sum, room) => sum + getRoomLayoutWeight(room), 0);
290
+ let cursorX = 0;
291
+
292
+ row.rooms.forEach((room, index) => {
293
+ const isLast = index === row.rooms.length - 1;
294
+ const width = isLast
295
+ ? Math.max(0.01, targetGridWidth - cursorX)
296
+ : targetGridWidth * (getRoomLayoutWeight(room) / totalWeight);
297
+ rooms.push({
298
+ ...room,
299
+ grid: {
300
+ ...room.grid,
301
+ x: cursorX,
302
+ y: cursorY,
303
+ w: width,
304
+ h: row.height,
305
+ },
306
+ });
307
+ cursorX += width;
308
+ });
309
+
310
+ cursorY += row.height;
311
+ }
312
+
313
+ return {
314
+ ...layout,
315
+ rooms,
316
+ };
317
+ }
318
+
319
+ function groupRoomsByRow(rooms: OfficeBuildingRoomDefinition[]): Array<{ row: number; height: number; rooms: OfficeBuildingRoomDefinition[] }> {
320
+ const groups = new Map<number, OfficeBuildingRoomDefinition[]>();
321
+ rooms.forEach((room, index) => {
322
+ const row = room.row ?? room.grid.y ?? index;
323
+ const current = groups.get(row);
324
+ if (current) {
325
+ current.push(room);
326
+ } else {
327
+ groups.set(row, [room]);
328
+ }
329
+ });
330
+
331
+ return Array.from(groups.entries())
332
+ .sort(([a], [b]) => a - b)
333
+ .map(([row, rowRooms]) => ({
334
+ row,
335
+ rooms: rowRooms,
336
+ height: rowRooms.reduce((max, room) => Math.max(max, room.rowHeight ?? room.grid.h), 0),
337
+ }));
338
+ }
339
+
340
+ function getRoomLayoutWeight(room: OfficeBuildingRoomDefinition): number {
341
+ return room.weight ?? room.grid.w;
342
+ }
343
+
344
+ function getTargetGridColumnCount(layout: OfficeBuildingLayout): number {
345
+ const ratio = layout.ratio ? layout.ratio.width / layout.ratio.height : 1;
346
+ const area = layout.rooms.reduce((sum, room) => sum + room.grid.w * room.grid.h, 0);
347
+ const widestRoom = layout.rooms.reduce((max, room) => Math.max(max, room.grid.w), 1);
348
+
349
+ return Math.max(widestRoom, Math.ceil(Math.sqrt(area * ratio)));
350
+ }
351
+
352
+ function assertGrid(grid: OfficeBuildingGridRect, roomId: string): void {
353
+ assertFiniteNumber(grid.x, `office building room ${roomId} grid x`);
354
+ assertFiniteNumber(grid.y, `office building room ${roomId} grid y`);
355
+ assertPositive(grid.w, `office building room ${roomId} grid w`);
356
+ assertPositive(grid.h, `office building room ${roomId} grid h`);
357
+ }
358
+
359
+ function assertNonEmpty(value: string, field: string): void {
360
+ if (value.trim().length === 0) {
361
+ throw new AgentGameError("invalid_asset_manifest", `Agent game ${field} must not be empty`);
362
+ }
363
+ }
364
+
365
+ function assertPositive(value: number, field: string): void {
366
+ assertFiniteNumber(value, field);
367
+ if (value <= 0) {
368
+ throw new AgentGameError("invalid_asset_manifest", `Agent game ${field} must be positive`);
369
+ }
370
+ }
371
+
372
+ function assertFiniteNumber(value: number, field: string): void {
373
+ if (!Number.isFinite(value)) {
374
+ throw new AgentGameError("invalid_asset_manifest", `Agent game ${field} must be finite`);
375
+ }
376
+ }
@@ -0,0 +1,152 @@
1
+ import { AgentGameError } from "./errors.js";
2
+
3
+ export type OfficeObjectAtlasFrame = {
4
+ frame: {
5
+ x: number;
6
+ y: number;
7
+ w: number;
8
+ h: number;
9
+ };
10
+ };
11
+
12
+ export type OfficeObjectAtlasDefinition = {
13
+ frames: Record<string, OfficeObjectAtlasFrame>;
14
+ meta: {
15
+ image: string;
16
+ size: {
17
+ w: number;
18
+ h: number;
19
+ };
20
+ scale: string;
21
+ };
22
+ };
23
+
24
+ export type OfficeLayoutFloorDefinition = {
25
+ frame: string;
26
+ visible?: boolean;
27
+ x?: number;
28
+ y?: number;
29
+ width?: number;
30
+ height?: number;
31
+ };
32
+
33
+ export type OfficeLayoutFloorFillDefinition = {
34
+ fillColor: number;
35
+ depth?: number;
36
+ };
37
+
38
+ export type OfficeLayoutWallDefinition = {
39
+ id: string;
40
+ x: number;
41
+ y: number;
42
+ width: number;
43
+ height: number;
44
+ fillColor: number;
45
+ depth?: number;
46
+ };
47
+
48
+ export type OfficeLayoutObjectDefinition = {
49
+ id: string;
50
+ frame: string;
51
+ x: number;
52
+ y: number;
53
+ depth?: number;
54
+ scale?: number;
55
+ };
56
+
57
+ export type OfficeLayoutDefinition = {
58
+ width: number;
59
+ height: number;
60
+ tileSize: number;
61
+ floor: OfficeLayoutFloorDefinition;
62
+ floorFill?: OfficeLayoutFloorFillDefinition;
63
+ walls?: OfficeLayoutWallDefinition[];
64
+ objects: OfficeLayoutObjectDefinition[];
65
+ };
66
+
67
+ export function validateOfficeLayoutDefinition(
68
+ layout: OfficeLayoutDefinition,
69
+ atlas: OfficeObjectAtlasDefinition,
70
+ ): OfficeLayoutDefinition {
71
+ assertPositive(layout.width, "office layout width");
72
+ assertPositive(layout.height, "office layout height");
73
+ assertPositive(layout.tileSize, "office layout tile size");
74
+ assertNonEmpty(layout.floor.frame, "office layout floor frame");
75
+ assertOptionalFiniteNumber(layout.floor.x, "office layout floor x");
76
+ assertOptionalFiniteNumber(layout.floor.y, "office layout floor y");
77
+ assertOptionalPositive(layout.floor.width, "office layout floor width");
78
+ assertOptionalPositive(layout.floor.height, "office layout floor height");
79
+ if (layout.floorFill) {
80
+ assertFiniteNumber(layout.floorFill.fillColor, "office layout floor fill color");
81
+ }
82
+
83
+ const missingFrames = listMissingOfficeLayoutFrames(layout, atlas);
84
+ if (missingFrames.length > 0) {
85
+ throw new AgentGameError(
86
+ "invalid_asset_manifest",
87
+ `Office layout references missing object frame: ${missingFrames.join(", ")}`,
88
+ );
89
+ }
90
+
91
+ const objectIds = new Set<string>();
92
+ for (const wall of layout.walls ?? []) {
93
+ assertNonEmpty(wall.id, "office layout wall id");
94
+ assertFiniteNumber(wall.x, `office layout wall ${wall.id} x`);
95
+ assertFiniteNumber(wall.y, `office layout wall ${wall.id} y`);
96
+ assertPositive(wall.width, `office layout wall ${wall.id} width`);
97
+ assertPositive(wall.height, `office layout wall ${wall.id} height`);
98
+ assertFiniteNumber(wall.fillColor, `office layout wall ${wall.id} fill color`);
99
+ }
100
+
101
+ for (const object of layout.objects) {
102
+ assertNonEmpty(object.id, "office layout object id");
103
+ assertNonEmpty(object.frame, "office layout object frame");
104
+ assertFiniteNumber(object.x, `office layout object ${object.id} x`);
105
+ assertFiniteNumber(object.y, `office layout object ${object.id} y`);
106
+ if (objectIds.has(object.id)) {
107
+ throw new AgentGameError("invalid_asset_manifest", `Duplicate office layout object id: ${object.id}`);
108
+ }
109
+ objectIds.add(object.id);
110
+ }
111
+
112
+ return layout;
113
+ }
114
+
115
+ function assertOptionalPositive(value: number | undefined, field: string): void {
116
+ if (value !== undefined) {
117
+ assertPositive(value, field);
118
+ }
119
+ }
120
+
121
+ function assertOptionalFiniteNumber(value: number | undefined, field: string): void {
122
+ if (value !== undefined) {
123
+ assertFiniteNumber(value, field);
124
+ }
125
+ }
126
+
127
+ export function listMissingOfficeLayoutFrames(
128
+ layout: OfficeLayoutDefinition,
129
+ atlas: OfficeObjectAtlasDefinition,
130
+ ): string[] {
131
+ const frames = new Set<string>([layout.floor.frame, ...layout.objects.map((object) => object.frame)]);
132
+ return Array.from(frames).filter((frame) => !atlas.frames[frame]);
133
+ }
134
+
135
+ function assertNonEmpty(value: string, field: string): void {
136
+ if (value.trim().length === 0) {
137
+ throw new AgentGameError("invalid_asset_manifest", `Agent game ${field} must not be empty`);
138
+ }
139
+ }
140
+
141
+ function assertPositive(value: number, field: string): void {
142
+ assertFiniteNumber(value, field);
143
+ if (value <= 0) {
144
+ throw new AgentGameError("invalid_asset_manifest", `Agent game ${field} must be positive`);
145
+ }
146
+ }
147
+
148
+ function assertFiniteNumber(value: number, field: string): void {
149
+ if (!Number.isFinite(value)) {
150
+ throw new AgentGameError("invalid_asset_manifest", `Agent game ${field} must be finite`);
151
+ }
152
+ }
@@ -0,0 +1,87 @@
1
+ import type { AgentAvatarDefinition } from "./assets.js";
2
+ import type { PixelCharacterSprite } from "./pixel-character.js";
3
+
4
+ export type PixelCharacterAvatarAtlas = {
5
+ frames: Record<string, { frame: { x: number; y: number; w: number; h: number } }>;
6
+ meta: {
7
+ app: string;
8
+ image: string;
9
+ format: "RGBA8888";
10
+ size: { w: number; h: number };
11
+ scale: "1";
12
+ };
13
+ };
14
+
15
+ export function createAgentAvatarFromPixelCharacter(sprite: PixelCharacterSprite): AgentAvatarDefinition {
16
+ const frameNames = Object.keys(sprite.frames);
17
+ const sheetWidth = sprite.frame.width * frameNames.length;
18
+ const sheetHeight = sprite.frame.height;
19
+ const svg = createPixelCharacterSpriteSheetSvg(sprite, frameNames, sheetWidth, sheetHeight);
20
+ const atlas = createPixelCharacterAtlas(sprite, frameNames, sheetWidth, sheetHeight);
21
+
22
+ return {
23
+ id: sprite.id,
24
+ imageUrl: `data:image/svg+xml;base64,${encodeBase64(svg)}`,
25
+ atlasUrl: `data:application/json;base64,${encodeBase64(JSON.stringify(atlas))}`,
26
+ animations: sprite.animations,
27
+ };
28
+ }
29
+
30
+ function createPixelCharacterSpriteSheetSvg(
31
+ sprite: PixelCharacterSprite,
32
+ frameNames: string[],
33
+ sheetWidth: number,
34
+ sheetHeight: number,
35
+ ): string {
36
+ const frames = frameNames.map((frameName, index) => {
37
+ const frameX = index * sprite.frame.width;
38
+ const rects = sprite.frames[frameName].layers.map((layer) =>
39
+ `<rect x="${frameX + layer.x}" y="${layer.y}" width="${layer.width}" height="${layer.height}" fill="${layer.fill}"/>`
40
+ ).join("");
41
+ return `<g id="${frameName}">${rects}</g>`;
42
+ }).join("");
43
+
44
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${sheetWidth}" height="${sheetHeight}" viewBox="0 0 ${sheetWidth} ${sheetHeight}" shape-rendering="crispEdges">${frames}</svg>`;
45
+ }
46
+
47
+ function createPixelCharacterAtlas(
48
+ sprite: PixelCharacterSprite,
49
+ frameNames: string[],
50
+ sheetWidth: number,
51
+ sheetHeight: number,
52
+ ): PixelCharacterAvatarAtlas {
53
+ const frames: PixelCharacterAvatarAtlas["frames"] = {};
54
+ frameNames.forEach((frameName, index) => {
55
+ frames[frameName] = {
56
+ frame: {
57
+ x: index * sprite.frame.width,
58
+ y: 0,
59
+ w: sprite.frame.width,
60
+ h: sprite.frame.height,
61
+ },
62
+ };
63
+ });
64
+
65
+ return {
66
+ frames,
67
+ meta: {
68
+ app: "agent-game-sdk/pixel-character-avatar",
69
+ image: `${sprite.id}.svg`,
70
+ format: "RGBA8888",
71
+ size: {
72
+ w: sheetWidth,
73
+ h: sheetHeight,
74
+ },
75
+ scale: "1",
76
+ },
77
+ };
78
+ }
79
+
80
+ function encodeBase64(value: string): string {
81
+ const bytes = new TextEncoder().encode(value);
82
+ let binary = "";
83
+ for (let index = 0; index < bytes.length; index++) {
84
+ binary += String.fromCharCode(bytes[index]);
85
+ }
86
+ return btoa(binary);
87
+ }