@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,684 @@
1
+ import {
2
+ REQUIRED_AGENT_AVATAR_ANIMATIONS,
3
+ type AgentAvatarAnimationName,
4
+ } from "./assets.js";
5
+
6
+ export type PixelCharacterRole = "engineer" | "manager" | "support" | string;
7
+ export type PixelCharacterStyle = "office" | "simple-chibi";
8
+ export type PixelCharacterComponentKind = "hair" | "eyes" | "mouth" | "top" | "bottom" | "accessory" | "prop";
9
+ export type PixelCharacterComponentRecipe = Record<PixelCharacterComponentKind, string>;
10
+
11
+ export type PixelCharacterAppearance = {
12
+ skin: string;
13
+ hair: string;
14
+ shirt: string;
15
+ pants: string;
16
+ accessory?: "glasses" | "none" | string;
17
+ };
18
+
19
+ export type PixelCharacterLayer = {
20
+ kind: "rect";
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ fill: string;
26
+ };
27
+
28
+ export type PixelCharacterContainer = {
29
+ id: string;
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ };
35
+
36
+ export type PixelCharacterContainerLayout = Record<string, PixelCharacterContainer>;
37
+
38
+ export type PixelCharacterContainerOverrides = Partial<Record<string, Partial<Omit<PixelCharacterContainer, "id">>>>;
39
+
40
+ export type PixelCharacterPartAlignment = {
41
+ horizontal: "left" | "center" | "right";
42
+ vertical: "top" | "center" | "bottom";
43
+ };
44
+
45
+ export type PixelCharacterPart = {
46
+ containerId: string;
47
+ align?: PixelCharacterPartAlignment;
48
+ layers: PixelCharacterLayer[];
49
+ };
50
+
51
+ export type PixelCharacterFrame = {
52
+ layers: PixelCharacterLayer[];
53
+ };
54
+
55
+ export type PixelCharacterSprite = {
56
+ schemaVersion: 1;
57
+ id: string;
58
+ role?: PixelCharacterRole;
59
+ style: PixelCharacterStyle;
60
+ components: PixelCharacterComponentRecipe;
61
+ frame: {
62
+ width: number;
63
+ height: number;
64
+ };
65
+ appearance: PixelCharacterAppearance;
66
+ containers: Record<string, PixelCharacterContainer>;
67
+ parts: Record<string, PixelCharacterPart>;
68
+ animations: Record<AgentAvatarAnimationName, string[]>;
69
+ frames: Record<string, PixelCharacterFrame>;
70
+ };
71
+
72
+ export type CreatePixelCharacterSpriteOptions = {
73
+ id: string;
74
+ seed: string;
75
+ role?: PixelCharacterRole;
76
+ style?: PixelCharacterStyle;
77
+ components?: Partial<PixelCharacterComponentRecipe>;
78
+ appearance?: PixelCharacterAppearance;
79
+ containers?: PixelCharacterContainerOverrides;
80
+ };
81
+
82
+ export type PixelCharacterComponentOption = {
83
+ kind: PixelCharacterComponentKind;
84
+ id: string;
85
+ label: string;
86
+ };
87
+
88
+ export type PixelCharacterPreset = {
89
+ id: string;
90
+ label: string;
91
+ components: PixelCharacterComponentRecipe;
92
+ appearance: PixelCharacterAppearance;
93
+ };
94
+
95
+ const DEFAULT_PALETTES: PixelCharacterAppearance[] = [
96
+ { skin: "#f3c7a1", hair: "#4b2e1f", shirt: "#2563eb", pants: "#1f2937", accessory: "none" },
97
+ { skin: "#d99b6c", hair: "#111827", shirt: "#16a34a", pants: "#334155", accessory: "glasses" },
98
+ { skin: "#f0c59a", hair: "#2f3440", shirt: "#38bdf8", pants: "#1d4ed8", accessory: "none" },
99
+ ];
100
+
101
+ const COMPONENT_OPTIONS: Record<PixelCharacterComponentKind, PixelCharacterComponentOption[]> = {
102
+ hair: [
103
+ { kind: "hair", id: "short", label: "Short" },
104
+ { kind: "hair", id: "bob", label: "Bob" },
105
+ { kind: "hair", id: "bun", label: "Bun" },
106
+ { kind: "hair", id: "flat-top", label: "Flat Top" },
107
+ { kind: "hair", id: "round", label: "Round" },
108
+ { kind: "hair", id: "side-sweep", label: "Side Sweep" },
109
+ { kind: "hair", id: "wide", label: "Wide" },
110
+ { kind: "hair", id: "afro", label: "Afro" },
111
+ ],
112
+ eyes: [
113
+ { kind: "eyes", id: "dot", label: "Dot" },
114
+ { kind: "eyes", id: "sleepy", label: "Sleepy" },
115
+ { kind: "eyes", id: "glasses", label: "Glasses" },
116
+ { kind: "eyes", id: "tiny-blue", label: "Tiny Blue" },
117
+ ],
118
+ mouth: [
119
+ { kind: "mouth", id: "small", label: "Small" },
120
+ { kind: "mouth", id: "open", label: "Open" },
121
+ { kind: "mouth", id: "wide", label: "Wide" },
122
+ ],
123
+ top: [
124
+ { kind: "top", id: "shirt", label: "Shirt" },
125
+ { kind: "top", id: "tshirt", label: "T-Shirt" },
126
+ { kind: "top", id: "jacket", label: "Jacket" },
127
+ { kind: "top", id: "hoodie", label: "Hoodie" },
128
+ { kind: "top", id: "dress", label: "Dress" },
129
+ ],
130
+ bottom: [
131
+ { kind: "bottom", id: "pants", label: "Pants" },
132
+ { kind: "bottom", id: "skirt", label: "Skirt" },
133
+ ],
134
+ accessory: [
135
+ { kind: "accessory", id: "none", label: "None" },
136
+ { kind: "accessory", id: "blush", label: "Blush" },
137
+ { kind: "accessory", id: "headband", label: "Headband" },
138
+ ],
139
+ prop: [
140
+ { kind: "prop", id: "none", label: "None" },
141
+ { kind: "prop", id: "guitar", label: "Guitar" },
142
+ ],
143
+ };
144
+
145
+ const DEFAULT_COMPONENTS: PixelCharacterComponentRecipe = {
146
+ hair: "bob",
147
+ eyes: "dot",
148
+ mouth: "open",
149
+ top: "shirt",
150
+ bottom: "pants",
151
+ accessory: "none",
152
+ prop: "none",
153
+ };
154
+
155
+ const SIMPLE_CHIBI_CONTAINERS: PixelCharacterContainerLayout = {
156
+ hair: { id: "hair", x: 6, y: 2, width: 53, height: 47 },
157
+ head: { id: "head", x: 14, y: 18, width: 36, height: 31 },
158
+ body: { id: "body", x: 15, y: 35, width: 36, height: 29 },
159
+ prop: { id: "prop", x: 0, y: 24, width: 64, height: 40 },
160
+ };
161
+
162
+ const PRESETS: PixelCharacterPreset[] = [
163
+ preset("reference-1", "Ref 1", { hair: "afro", eyes: "dot", mouth: "open", top: "shirt", bottom: "pants", accessory: "none", prop: "none" }, "#f6d96b", "#050505", "#d8d4d6", "#9a5a22"),
164
+ preset("reference-2", "Ref 2", { hair: "flat-top", eyes: "glasses", mouth: "small", top: "jacket", bottom: "pants", accessory: "none", prop: "none" }, "#f3d981", "#76500b", "#0f6b3a", "#8a5a11"),
165
+ preset("reference-3", "Ref 3", { hair: "round", eyes: "dot", mouth: "open", top: "tshirt", bottom: "pants", accessory: "none", prop: "none" }, "#f1d36f", "#7a6338", "#b858ff", "#4f3b25"),
166
+ preset("reference-4", "Ref 4", { hair: "flat-top", eyes: "dot", mouth: "open", top: "jacket", bottom: "pants", accessory: "none", prop: "none" }, "#f3dc86", "#8a5709", "#6d95db", "#4d78c8"),
167
+ preset("reference-5", "Ref 5", { hair: "wide", eyes: "dot", mouth: "wide", top: "tshirt", bottom: "pants", accessory: "none", prop: "none" }, "#efd775", "#2d2618", "#555555", "#8b5b17"),
168
+ preset("reference-6", "Ref 6", { hair: "round", eyes: "sleepy", mouth: "small", top: "hoodie", bottom: "pants", accessory: "headband", prop: "none" }, "#f2d56e", "#272111", "#ef4141", "#433217"),
169
+ preset("reference-7", "Ref 7", { hair: "side-sweep", eyes: "dot", mouth: "open", top: "jacket", bottom: "skirt", accessory: "blush", prop: "none" }, "#efe48f", "#b66e4a", "#87a9d8", "#cf1826"),
170
+ preset("reference-8", "Ref 8", { hair: "side-sweep", eyes: "dot", mouth: "open", top: "dress", bottom: "skirt", accessory: "blush", prop: "none" }, "#efe48f", "#b56f4a", "#ff7a13", "#22c65e"),
171
+ preset("reference-9", "Ref 9", { hair: "side-sweep", eyes: "tiny-blue", mouth: "small", top: "tshirt", bottom: "pants", accessory: "none", prop: "guitar" }, "#f0da72", "#7a4f05", "#4e73d6", "#b84655"),
172
+ preset("reference-10", "Ref 10", { hair: "wide", eyes: "dot", mouth: "open", top: "dress", bottom: "skirt", accessory: "blush", prop: "none" }, "#efe48f", "#b66e4a", "#ff5b9a", "#84a9d6"),
173
+ ];
174
+
175
+ export function getPixelCharacterComponentOptions(kind: PixelCharacterComponentKind): PixelCharacterComponentOption[] {
176
+ return COMPONENT_OPTIONS[kind];
177
+ }
178
+
179
+ export function getPixelCharacterPresets(): PixelCharacterPreset[] {
180
+ return PRESETS;
181
+ }
182
+
183
+ export function getSimpleChibiContainerLayout(): PixelCharacterContainerLayout {
184
+ return cloneContainers(SIMPLE_CHIBI_CONTAINERS);
185
+ }
186
+
187
+ export function createPixelCharacterSprite(options: CreatePixelCharacterSpriteOptions): PixelCharacterSprite {
188
+ const appearance = options.appearance ?? pickAppearance(options.seed);
189
+ const style = options.style ?? "office";
190
+ const components = { ...DEFAULT_COMPONENTS, ...options.components };
191
+ const animations = createAnimations();
192
+ const frameNames = new Set(Object.values(animations).flat());
193
+ const frames: Record<string, PixelCharacterFrame> = {};
194
+ const containers = style === "simple-chibi" ? createSimpleChibiContainers(options.containers) : {};
195
+ const parts = style === "simple-chibi" ? createSimpleChibiParts(appearance, components) : createOfficeParts(appearance);
196
+
197
+ for (const frameName of frameNames) {
198
+ frames[frameName] =
199
+ style === "simple-chibi"
200
+ ? createSimpleChibiFrame(frameName, appearance, components, containers, parts)
201
+ : createFrame(frameName, appearance);
202
+ }
203
+
204
+ return {
205
+ schemaVersion: 1,
206
+ id: options.id,
207
+ role: options.role,
208
+ style,
209
+ components,
210
+ frame: style === "simple-chibi" ? { width: 64, height: 64 } : { width: 48, height: 48 },
211
+ appearance,
212
+ containers,
213
+ parts,
214
+ animations,
215
+ frames,
216
+ };
217
+ }
218
+
219
+ function createSimpleChibiContainers(overrides: PixelCharacterContainerOverrides = {}): PixelCharacterContainerLayout {
220
+ const containers = cloneContainers(SIMPLE_CHIBI_CONTAINERS);
221
+ for (const [id, override] of Object.entries(overrides)) {
222
+ const base = containers[id];
223
+ if (!base || !override) {
224
+ continue;
225
+ }
226
+ containers[id] = {
227
+ id,
228
+ x: override.x ?? base.x,
229
+ y: override.y ?? base.y,
230
+ width: override.width ?? base.width,
231
+ height: override.height ?? base.height,
232
+ };
233
+ }
234
+ return containers;
235
+ }
236
+
237
+ function cloneContainers(containers: PixelCharacterContainerLayout): PixelCharacterContainerLayout {
238
+ return Object.fromEntries(Object.entries(containers).map(([id, container]) => [id, { ...container }]));
239
+ }
240
+
241
+ function createAnimations(): Record<AgentAvatarAnimationName, string[]> {
242
+ return {
243
+ "idle.down": ["idle-down-0"],
244
+ "idle.up": ["idle-up-0"],
245
+ "idle.left": ["idle-left-0"],
246
+ "idle.right": ["idle-right-0"],
247
+ "walk.down": ["walk-down-0", "walk-down-1", "walk-down-2", "walk-down-3"],
248
+ "walk.up": ["walk-up-0", "walk-up-1", "walk-up-2", "walk-up-3"],
249
+ "walk.left": ["walk-left-0", "walk-left-1", "walk-left-2", "walk-left-3"],
250
+ "walk.right": ["walk-right-0", "walk-right-1", "walk-right-2", "walk-right-3"],
251
+ "work.typing": ["work-typing-0", "work-typing-1"],
252
+ "emote.think": ["idle-down-0"],
253
+ "emote.talk": ["talk-down-0", "talk-down-1"],
254
+ };
255
+ }
256
+
257
+ function createFrame(frameName: string, appearance: PixelCharacterAppearance): PixelCharacterFrame {
258
+ const layers: PixelCharacterLayer[] = [
259
+ { kind: "rect", x: 15, y: 14, width: 18, height: 10, fill: appearance.skin },
260
+ { kind: "rect", x: 13, y: 10, width: 22, height: 8, fill: appearance.hair },
261
+ { kind: "rect", x: 14, y: 24, width: 20, height: 12, fill: appearance.shirt },
262
+ { kind: "rect", x: 9, y: 25, width: 5, height: 8, fill: appearance.shirt },
263
+ { kind: "rect", x: 34, y: 25, width: 5, height: 8, fill: appearance.shirt },
264
+ { kind: "rect", x: 9, y: 33, width: 5, height: 5, fill: appearance.skin },
265
+ { kind: "rect", x: 34, y: 33, width: 5, height: 5, fill: appearance.skin },
266
+ { kind: "rect", x: 14, y: 36, width: 8, height: 9, fill: appearance.pants },
267
+ { kind: "rect", x: 26, y: 36, width: 8, height: 9, fill: appearance.pants },
268
+ ];
269
+
270
+ if (frameName.startsWith("work-typing")) {
271
+ layers.push({ kind: "rect", x: 8, y: 32, width: 32, height: 9, fill: "#8b5e34" });
272
+ layers.push({ kind: "rect", x: 15, y: 29, width: 5, height: 4, fill: appearance.skin });
273
+ layers.push({ kind: "rect", x: 28, y: 29, width: 5, height: 4, fill: appearance.skin });
274
+ }
275
+
276
+ if (appearance.accessory === "glasses") {
277
+ layers.push({ kind: "rect", x: 17, y: 16, width: 6, height: 5, fill: "#111827" });
278
+ layers.push({ kind: "rect", x: 25, y: 16, width: 6, height: 5, fill: "#111827" });
279
+ }
280
+
281
+ return { layers };
282
+ }
283
+
284
+ function createOfficeParts(appearance: PixelCharacterAppearance): Record<string, PixelCharacterPart> {
285
+ return {
286
+ head: { containerId: "frame", layers: [{ kind: "rect", x: 15, y: 14, width: 18, height: 10, fill: appearance.skin }] },
287
+ torso: { containerId: "frame", layers: [{ kind: "rect", x: 14, y: 24, width: 20, height: 12, fill: appearance.shirt }] },
288
+ leftHand: { containerId: "frame", layers: [{ kind: "rect", x: 9, y: 33, width: 5, height: 5, fill: appearance.skin }] },
289
+ rightHand: { containerId: "frame", layers: [{ kind: "rect", x: 34, y: 33, width: 5, height: 5, fill: appearance.skin }] },
290
+ };
291
+ }
292
+
293
+ function createSimpleChibiFrame(
294
+ frameName: string,
295
+ appearance: PixelCharacterAppearance,
296
+ components: PixelCharacterComponentRecipe,
297
+ containers: Record<string, PixelCharacterContainer>,
298
+ parts: Record<string, PixelCharacterPart>,
299
+ ): PixelCharacterFrame {
300
+ const layers: PixelCharacterLayer[] = composeParts(containers, [
301
+ parts.hair,
302
+ parts.head,
303
+ parts.top,
304
+ parts.bottom,
305
+ parts.leftSleeve,
306
+ parts.rightSleeve,
307
+ parts.leftHand,
308
+ parts.rightHand,
309
+ parts.eyes,
310
+ parts.mouth,
311
+ parts.accessory,
312
+ parts.prop,
313
+ ]);
314
+
315
+ if (frameName.startsWith("walk-") && frameName.endsWith("-1")) {
316
+ replaceMatchingLayer(layers, { kind: "rect", x: 33, y: 63, width: 6, height: 1, fill: appearance.pants }, { kind: "rect", x: 32, y: 63, width: 6, height: 1, fill: appearance.pants });
317
+ }
318
+ if (frameName.startsWith("walk-") && frameName.endsWith("-3")) {
319
+ replaceMatchingLayer(layers, { kind: "rect", x: 33, y: 63, width: 6, height: 1, fill: appearance.pants }, { kind: "rect", x: 34, y: 63, width: 6, height: 1, fill: appearance.pants });
320
+ }
321
+ if (frameName.startsWith("work-typing")) {
322
+ layers.push({ kind: "rect", x: 15, y: 37, width: 18, height: 4, fill: "#8b5e34" });
323
+ }
324
+
325
+ return { layers };
326
+ }
327
+
328
+ function createSimpleChibiParts(
329
+ appearance: PixelCharacterAppearance,
330
+ components: PixelCharacterComponentRecipe,
331
+ ): Record<string, PixelCharacterPart> {
332
+ if (components.hair === "afro") {
333
+ return createAfroChibiParts(appearance, components);
334
+ }
335
+
336
+ return {
337
+ hair: part("hair", localizeLayers(SIMPLE_CHIBI_CONTAINERS.hair, createHairPart(components.hair, appearance))),
338
+ head: part("head", [
339
+ { kind: "rect", x: 0, y: 0, width: 24, height: 8, fill: appearance.skin },
340
+ { kind: "rect", x: 4, y: 6, width: 18, height: 4, fill: appearance.skin },
341
+ ]),
342
+ eyes: part("head", createEyesPart(components.eyes)),
343
+ mouth: part("head", createMouthPart(components.mouth)),
344
+ top: part("body", createTopPart(components.top, appearance), topCenterAlign()),
345
+ bottom: part("body", createBottomPart(components.bottom, appearance)),
346
+ leftSleeve: part("body", [{ kind: "rect", x: 1, y: 0, width: 3, height: 10, fill: "#7fb5d6" }]),
347
+ rightSleeve: part("body", [{ kind: "rect", x: 21, y: 0, width: 3, height: 10, fill: "#e7a2aa" }]),
348
+ leftHand: part("body", [{ kind: "rect", x: 0, y: 12, width: 5, height: 3, fill: appearance.skin }]),
349
+ rightHand: part("body", [{ kind: "rect", x: 21, y: 12, width: 5, height: 3, fill: appearance.skin }]),
350
+ accessory: part("head", createAccessoryPart(components.accessory)),
351
+ prop: part("prop", createPropPart(components.prop)),
352
+ };
353
+ }
354
+
355
+ function createAfroChibiParts(
356
+ appearance: PixelCharacterAppearance,
357
+ components: PixelCharacterComponentRecipe,
358
+ ): Record<string, PixelCharacterPart> {
359
+ return {
360
+ hair: part("hair", localizeLayers(SIMPLE_CHIBI_CONTAINERS.hair, createHairPart(components.hair, appearance))),
361
+ head: part("head", [
362
+ { kind: "rect", x: 13, y: 9, width: 14, height: 5, fill: appearance.skin },
363
+ { kind: "rect", x: 8, y: 14, width: 25, height: 5, fill: appearance.skin },
364
+ { kind: "rect", x: 5, y: 19, width: 30, height: 8, fill: appearance.skin },
365
+ { kind: "rect", x: 8, y: 27, width: 25, height: 4, fill: appearance.skin },
366
+ ]),
367
+ eyes: part("head", createAfroEyesPart(components.eyes)),
368
+ mouth: part("head", createAfroMouthPart(components.mouth)),
369
+ top: part("body", [
370
+ { kind: "rect", x: 9, y: 0, width: 22, height: 12, fill: appearance.shirt },
371
+ { kind: "rect", x: 9, y: 12, width: 22, height: 3, fill: "#df8f92" },
372
+ ], topCenterAlign()),
373
+ bottom: part("body", [
374
+ { kind: "rect", x: 9, y: 14, width: 6, height: 1, fill: appearance.pants },
375
+ { kind: "rect", x: 25, y: 14, width: 6, height: 1, fill: appearance.pants },
376
+ ]),
377
+ leftSleeve: part("body", [{ kind: "rect", x: 4, y: 0, width: 5, height: 12, fill: "#4f90b7" }]),
378
+ rightSleeve: part("body", [{ kind: "rect", x: 31, y: 0, width: 5, height: 12, fill: "#df8f92" }]),
379
+ leftHand: part("body", [{ kind: "rect", x: 4, y: 12, width: 5, height: 3, fill: appearance.skin }]),
380
+ rightHand: part("body", [{ kind: "rect", x: 31, y: 12, width: 5, height: 3, fill: appearance.skin }]),
381
+ accessory: part("head", createAccessoryPart(components.accessory)),
382
+ prop: part("prop", createPropPart(components.prop)),
383
+ };
384
+ }
385
+
386
+ function createAfroEyesPart(component: string): PixelCharacterLayer[] {
387
+ if (component === "sleepy") {
388
+ return [
389
+ { kind: "rect", x: 12, y: 19, width: 5, height: 1, fill: "#050505" },
390
+ { kind: "rect", x: 27, y: 19, width: 5, height: 1, fill: "#050505" },
391
+ ];
392
+ }
393
+ return [
394
+ { kind: "rect", x: 12, y: 18, width: 3, height: 4, fill: "#050505" },
395
+ { kind: "rect", x: 27, y: 18, width: 3, height: 4, fill: "#050505" },
396
+ ];
397
+ }
398
+
399
+ function createAfroMouthPart(component: string): PixelCharacterLayer[] {
400
+ return component === "small"
401
+ ? [{ kind: "rect", x: 17, y: 27, width: 7, height: 2, fill: "#050505" }]
402
+ : [{ kind: "rect", x: 16, y: 26, width: 10, height: 3, fill: "#050505" }];
403
+ }
404
+
405
+ function createHairPart(component: string, appearance: PixelCharacterAppearance): PixelCharacterLayer[] {
406
+ if (component === "flat-top") {
407
+ return [
408
+ { kind: "rect", x: 12, y: 9, width: 28, height: 9, fill: appearance.hair },
409
+ { kind: "rect", x: 12, y: 18, width: 9, height: 5, fill: appearance.hair },
410
+ { kind: "rect", x: 31, y: 18, width: 9, height: 5, fill: appearance.hair },
411
+ ];
412
+ }
413
+ if (component === "round") {
414
+ return [
415
+ { kind: "rect", x: 17, y: 9, width: 18, height: 5, fill: appearance.hair },
416
+ { kind: "rect", x: 14, y: 14, width: 24, height: 8, fill: appearance.hair },
417
+ { kind: "rect", x: 13, y: 22, width: 26, height: 5, fill: appearance.hair },
418
+ { kind: "rect", x: 14, y: 27, width: 5, height: 4, fill: appearance.hair },
419
+ { kind: "rect", x: 33, y: 27, width: 5, height: 4, fill: appearance.hair },
420
+ ];
421
+ }
422
+ if (component === "side-sweep") {
423
+ return [
424
+ { kind: "rect", x: 16, y: 8, width: 18, height: 5, fill: appearance.hair },
425
+ { kind: "rect", x: 13, y: 13, width: 25, height: 7, fill: appearance.hair },
426
+ { kind: "rect", x: 20, y: 20, width: 19, height: 5, fill: appearance.hair },
427
+ { kind: "rect", x: 10, y: 18, width: 7, height: 13, fill: appearance.hair },
428
+ { kind: "rect", x: 34, y: 17, width: 7, height: 11, fill: appearance.hair },
429
+ ];
430
+ }
431
+ if (component === "wide") {
432
+ return [
433
+ { kind: "rect", x: 9, y: 12, width: 34, height: 8, fill: appearance.hair },
434
+ { kind: "rect", x: 12, y: 20, width: 28, height: 8, fill: appearance.hair },
435
+ { kind: "rect", x: 13, y: 28, width: 7, height: 5, fill: appearance.hair },
436
+ { kind: "rect", x: 32, y: 28, width: 7, height: 5, fill: appearance.hair },
437
+ ];
438
+ }
439
+ if (component === "afro") {
440
+ return [
441
+ { kind: "rect", x: 25, y: 5, width: 10, height: 5, fill: appearance.hair },
442
+ { kind: "rect", x: 39, y: 5, width: 8, height: 5, fill: appearance.hair },
443
+ { kind: "rect", x: 21, y: 10, width: 30, height: 6, fill: appearance.hair },
444
+ { kind: "rect", x: 16, y: 16, width: 38, height: 7, fill: appearance.hair },
445
+ { kind: "rect", x: 12, y: 23, width: 44, height: 8, fill: appearance.hair },
446
+ { kind: "rect", x: 9, y: 31, width: 50, height: 6, fill: appearance.hair },
447
+ { kind: "rect", x: 12, y: 37, width: 9, height: 12, fill: appearance.hair },
448
+ { kind: "rect", x: 49, y: 37, width: 8, height: 12, fill: appearance.hair },
449
+ ];
450
+ }
451
+ if (component === "short") {
452
+ return [
453
+ { kind: "rect", x: 18, y: 13, width: 14, height: 3, fill: appearance.hair },
454
+ { kind: "rect", x: 15, y: 16, width: 22, height: 5, fill: appearance.hair },
455
+ { kind: "rect", x: 14, y: 19, width: 5, height: 4, fill: appearance.hair },
456
+ ];
457
+ }
458
+ if (component === "bun") {
459
+ return [
460
+ { kind: "rect", x: 20, y: 11, width: 9, height: 4, fill: appearance.hair },
461
+ { kind: "rect", x: 17, y: 15, width: 18, height: 7, fill: appearance.hair },
462
+ { kind: "rect", x: 33, y: 10, width: 6, height: 6, fill: appearance.hair },
463
+ { kind: "rect", x: 35, y: 8, width: 3, height: 3, fill: appearance.hair },
464
+ ];
465
+ }
466
+ return [
467
+ { kind: "rect", x: 20, y: 10, width: 8, height: 4, fill: appearance.hair },
468
+ { kind: "rect", x: 16, y: 14, width: 20, height: 8, fill: appearance.hair },
469
+ { kind: "rect", x: 15, y: 21, width: 5, height: 7, fill: appearance.hair },
470
+ { kind: "rect", x: 32, y: 21, width: 5, height: 7, fill: appearance.hair },
471
+ ];
472
+ }
473
+
474
+ function createEyesPart(component: string): PixelCharacterLayer[] {
475
+ if (component === "glasses") {
476
+ return [
477
+ { kind: "rect", x: 3, y: 1, width: 7, height: 5, fill: "#050505" },
478
+ { kind: "rect", x: 14, y: 1, width: 7, height: 5, fill: "#050505" },
479
+ { kind: "rect", x: 10, y: 3, width: 4, height: 1, fill: "#050505" },
480
+ { kind: "rect", x: 4, y: 2, width: 5, height: 3, fill: "#aeb4b8" },
481
+ { kind: "rect", x: 15, y: 2, width: 5, height: 3, fill: "#aeb4b8" },
482
+ ];
483
+ }
484
+ if (component === "tiny-blue") {
485
+ return [
486
+ { kind: "rect", x: 6, y: 3, width: 2, height: 2, fill: "#6377d8" },
487
+ { kind: "rect", x: 16, y: 3, width: 2, height: 2, fill: "#6377d8" },
488
+ ];
489
+ }
490
+ if (component === "sleepy") {
491
+ return [
492
+ { kind: "rect", x: 5, y: 2, width: 4, height: 1, fill: "#050505" },
493
+ { kind: "rect", x: 15, y: 2, width: 4, height: 1, fill: "#050505" },
494
+ ];
495
+ }
496
+ return [
497
+ { kind: "rect", x: 6, y: 2, width: 2, height: 3, fill: "#050505" },
498
+ { kind: "rect", x: 16, y: 2, width: 2, height: 3, fill: "#050505" },
499
+ ];
500
+ }
501
+
502
+ function createMouthPart(component: string): PixelCharacterLayer[] {
503
+ if (component === "wide") {
504
+ return [{ kind: "rect", x: 14, y: 12, width: 12, height: 3, fill: "#050505" }];
505
+ }
506
+ return component === "small"
507
+ ? [{ kind: "rect", x: 16, y: 12, width: 5, height: 2, fill: "#050505" }]
508
+ : [{ kind: "rect", x: 15, y: 12, width: 10, height: 3, fill: "#050505" }];
509
+ }
510
+
511
+ function createTopPart(component: string, appearance: PixelCharacterAppearance): PixelCharacterLayer[] {
512
+ if (component === "tshirt") {
513
+ return [{ kind: "rect", x: 2, y: 0, width: 18, height: 14, fill: appearance.shirt }];
514
+ }
515
+ if (component === "jacket") {
516
+ return [
517
+ { kind: "rect", x: 2, y: 0, width: 18, height: 14, fill: appearance.shirt },
518
+ { kind: "rect", x: 2, y: 0, width: 5, height: 14, fill: "#5d83c8" },
519
+ { kind: "rect", x: 15, y: 0, width: 5, height: 14, fill: "#5d83c8" },
520
+ { kind: "rect", x: 8, y: 1, width: 6, height: 8, fill: "#e8efe2" },
521
+ ];
522
+ }
523
+ if (component === "hoodie") {
524
+ return [
525
+ { kind: "rect", x: 2, y: 0, width: 18, height: 14, fill: appearance.shirt },
526
+ { kind: "rect", x: 8, y: 2, width: 6, height: 9, fill: "#138247" },
527
+ ];
528
+ }
529
+ if (component === "dress") {
530
+ return [
531
+ { kind: "rect", x: 1, y: 0, width: 20, height: 13, fill: appearance.shirt },
532
+ { kind: "rect", x: 0, y: 13, width: 22, height: 2, fill: appearance.pants },
533
+ ];
534
+ }
535
+ return [{ kind: "rect", x: 3, y: 0, width: 16, height: 14, fill: appearance.shirt }];
536
+ }
537
+
538
+ function createBottomPart(component: string, appearance: PixelCharacterAppearance): PixelCharacterLayer[] {
539
+ return component === "skirt"
540
+ ? [{ kind: "rect", x: 3, y: 12, width: 16, height: 3, fill: appearance.pants }]
541
+ : [
542
+ { kind: "rect", x: 3, y: 14, width: 6, height: 1, fill: appearance.pants },
543
+ { kind: "rect", x: 13, y: 14, width: 6, height: 1, fill: appearance.pants },
544
+ ];
545
+ }
546
+
547
+ function createAccessoryPart(component: string): PixelCharacterLayer[] {
548
+ if (component === "blush") {
549
+ return [
550
+ { kind: "rect", x: 2, y: 7, width: 4, height: 3, fill: "#ff9fc4" },
551
+ { kind: "rect", x: 18, y: 7, width: 4, height: 3, fill: "#ff9fc4" },
552
+ ];
553
+ }
554
+ if (component === "headband") {
555
+ return [{ kind: "rect", x: 3, y: 1, width: 18, height: 3, fill: "#7d50c8" }];
556
+ }
557
+ return [];
558
+ }
559
+
560
+ function createPropPart(component: string): PixelCharacterLayer[] {
561
+ if (component === "guitar") {
562
+ return [
563
+ { kind: "rect", x: 8, y: 5, width: 10, height: 13, fill: "#c8e86f" },
564
+ { kind: "rect", x: 15, y: 7, width: 20, height: 6, fill: "#c8e86f" },
565
+ { kind: "rect", x: 22, y: 10, width: 16, height: 7, fill: "#4e73d6" },
566
+ ];
567
+ }
568
+ return [];
569
+ }
570
+
571
+ function preset(
572
+ id: string,
573
+ label: string,
574
+ components: PixelCharacterComponentRecipe,
575
+ skin: string,
576
+ hair: string,
577
+ shirt: string,
578
+ pants: string,
579
+ ): PixelCharacterPreset {
580
+ return {
581
+ id,
582
+ label,
583
+ components,
584
+ appearance: { skin, hair, shirt, pants, accessory: "none" },
585
+ };
586
+ }
587
+
588
+ function part(containerId: string, layers: PixelCharacterLayer[], align?: PixelCharacterPartAlignment): PixelCharacterPart {
589
+ return { containerId, align, layers };
590
+ }
591
+
592
+ function composeParts(containers: Record<string, PixelCharacterContainer>, parts: Array<PixelCharacterPart | undefined>): PixelCharacterLayer[] {
593
+ const layers: PixelCharacterLayer[] = [];
594
+ for (const item of parts) {
595
+ if (!item) {
596
+ continue;
597
+ }
598
+ const container = containers[item.containerId] ?? { id: item.containerId, x: 0, y: 0, width: 0, height: 0 };
599
+ const alignmentOffset = getPartAlignmentOffset(container, item);
600
+ for (const layer of item.layers) {
601
+ layers.push({
602
+ ...layer,
603
+ x: container.x + alignmentOffset.x + layer.x,
604
+ y: container.y + alignmentOffset.y + layer.y,
605
+ });
606
+ }
607
+ }
608
+ return layers;
609
+ }
610
+
611
+ function getPartAlignmentOffset(container: PixelCharacterContainer, part: PixelCharacterPart): { x: number; y: number } {
612
+ if (!part.align || part.layers.length === 0) {
613
+ return { x: 0, y: 0 };
614
+ }
615
+
616
+ const bounds = getLayerBounds(part.layers);
617
+ const x =
618
+ part.align.horizontal === "center"
619
+ ? Math.round((container.width - bounds.width) / 2) - bounds.x
620
+ : part.align.horizontal === "right"
621
+ ? container.width - bounds.width - bounds.x
622
+ : -bounds.x;
623
+ const y =
624
+ part.align.vertical === "center"
625
+ ? Math.round((container.height - bounds.height) / 2) - bounds.y
626
+ : part.align.vertical === "bottom"
627
+ ? container.height - bounds.height - bounds.y
628
+ : -bounds.y;
629
+
630
+ return { x, y };
631
+ }
632
+
633
+ function getLayerBounds(layers: PixelCharacterLayer[]): { x: number; y: number; width: number; height: number } {
634
+ const minX = Math.min(...layers.map((layer) => layer.x));
635
+ const minY = Math.min(...layers.map((layer) => layer.y));
636
+ const maxX = Math.max(...layers.map((layer) => layer.x + layer.width));
637
+ const maxY = Math.max(...layers.map((layer) => layer.y + layer.height));
638
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
639
+ }
640
+
641
+ function bottomCenterAlign(): PixelCharacterPartAlignment {
642
+ return { horizontal: "center", vertical: "bottom" };
643
+ }
644
+
645
+ function topCenterAlign(): PixelCharacterPartAlignment {
646
+ return { horizontal: "center", vertical: "top" };
647
+ }
648
+
649
+ function localizeLayers(container: PixelCharacterContainer, layers: PixelCharacterLayer[]): PixelCharacterLayer[] {
650
+ return layers.map((layer) => ({
651
+ ...layer,
652
+ x: layer.x - container.x,
653
+ y: layer.y - container.y,
654
+ }));
655
+ }
656
+
657
+ function replaceMatchingLayer(layers: PixelCharacterLayer[], from: PixelCharacterLayer, to: PixelCharacterLayer): void {
658
+ const index = layers.findIndex(
659
+ (layer) =>
660
+ layer.kind === from.kind &&
661
+ layer.x === from.x &&
662
+ layer.y === from.y &&
663
+ layer.width === from.width &&
664
+ layer.height === from.height &&
665
+ layer.fill === from.fill,
666
+ );
667
+ if (index >= 0) {
668
+ layers[index] = to;
669
+ }
670
+ }
671
+
672
+ function pickAppearance(seed: string): PixelCharacterAppearance {
673
+ return DEFAULT_PALETTES[hashSeed(seed) % DEFAULT_PALETTES.length]!;
674
+ }
675
+
676
+ function hashSeed(seed: string): number {
677
+ let hash = 0;
678
+ for (const char of seed) {
679
+ hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
680
+ }
681
+ return hash;
682
+ }
683
+
684
+ void REQUIRED_AGENT_AVATAR_ANIMATIONS;