@umicat/phaser-sdk 1.0.0

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 (62) hide show
  1. package/SDK-GUIDE.md +1726 -0
  2. package/dist/core/Transport.d.ts +28 -0
  3. package/dist/core/Transport.js +7 -0
  4. package/dist/core/Umicat.d.ts +45 -0
  5. package/dist/core/Umicat.js +60 -0
  6. package/dist/core/UmicatGame.d.ts +43 -0
  7. package/dist/core/UmicatGame.js +64 -0
  8. package/dist/core/UmicatScene.d.ts +19 -0
  9. package/dist/core/UmicatScene.js +38 -0
  10. package/dist/core/transports/LocalStorageTransport.d.ts +22 -0
  11. package/dist/core/transports/LocalStorageTransport.js +78 -0
  12. package/dist/core/transports/PostMessageTransport.d.ts +28 -0
  13. package/dist/core/transports/PostMessageTransport.js +105 -0
  14. package/dist/editor/EditorBridge.d.ts +114 -0
  15. package/dist/editor/EditorBridge.js +2608 -0
  16. package/dist/editor/EditorOverlayScene.d.ts +333 -0
  17. package/dist/editor/EditorOverlayScene.js +1896 -0
  18. package/dist/editor/EditorState.d.ts +251 -0
  19. package/dist/editor/EditorState.js +197 -0
  20. package/dist/gamedata/GameDataModule.d.ts +45 -0
  21. package/dist/gamedata/GameDataModule.js +59 -0
  22. package/dist/index.d.ts +43 -0
  23. package/dist/index.js +43 -0
  24. package/dist/orientation.d.ts +5 -0
  25. package/dist/orientation.js +4 -0
  26. package/dist/protocol.d.ts +807 -0
  27. package/dist/protocol.js +3 -0
  28. package/dist/realtime/RealtimeModule.d.ts +93 -0
  29. package/dist/realtime/RealtimeModule.js +115 -0
  30. package/dist/realtime/UmicatRoom.d.ts +197 -0
  31. package/dist/realtime/UmicatRoom.js +353 -0
  32. package/dist/recording/RecordingManager.d.ts +11 -0
  33. package/dist/recording/RecordingManager.js +59 -0
  34. package/dist/saves/SavesModule.d.ts +23 -0
  35. package/dist/saves/SavesModule.js +37 -0
  36. package/dist/scene/EditorMode.d.ts +17 -0
  37. package/dist/scene/EditorMode.js +22 -0
  38. package/dist/scene/EntityRegistry.d.ts +39 -0
  39. package/dist/scene/EntityRegistry.js +103 -0
  40. package/dist/scene/GameConfig.d.ts +60 -0
  41. package/dist/scene/GameConfig.js +50 -0
  42. package/dist/scene/HudRuntime.d.ts +131 -0
  43. package/dist/scene/HudRuntime.js +1224 -0
  44. package/dist/scene/Prefabs.d.ts +92 -0
  45. package/dist/scene/Prefabs.js +175 -0
  46. package/dist/scene/Rules.d.ts +73 -0
  47. package/dist/scene/Rules.js +164 -0
  48. package/dist/scene/SceneLoader.d.ts +118 -0
  49. package/dist/scene/SceneLoader.js +615 -0
  50. package/dist/scene/Waves.d.ts +85 -0
  51. package/dist/scene/Waves.js +365 -0
  52. package/dist/scene/autotile.d.ts +103 -0
  53. package/dist/scene/autotile.js +321 -0
  54. package/dist/scene/renderScripts.d.ts +53 -0
  55. package/dist/scene/renderScripts.js +67 -0
  56. package/dist/scene/spawnEntity.d.ts +201 -0
  57. package/dist/scene/spawnEntity.js +1326 -0
  58. package/dist/scene/types.d.ts +1166 -0
  59. package/dist/scene/types.js +34 -0
  60. package/dist/screenshot/ScreenshotManager.d.ts +14 -0
  61. package/dist/screenshot/ScreenshotManager.js +33 -0
  62. package/package.json +35 -0
@@ -0,0 +1,1166 @@
1
+ /**
2
+ * Scene-as-data types — visual editor foundation (slice 1).
3
+ *
4
+ * Spec: umicat-design/features/visual-editor/01-scene-data-foundation.md
5
+ *
6
+ * v1 schema (`schemaVersion: 1`) supports the entity kinds needed for the
7
+ * read-only scene viewer. HUD entity kinds, tilemap, trigger, and
8
+ * code-rendered visuals will land in later slices.
9
+ */
10
+ export declare const SCHEMA_VERSION = 1;
11
+ export type SceneType = 'world' | 'hud';
12
+ export interface SceneRef {
13
+ id: string;
14
+ type: SceneType;
15
+ /** Path under `public/scenes/`, e.g. `"world/level-1-1.json"`. */
16
+ file: string;
17
+ /** Optional HUD scene id to overlay (world scenes only). Null = no HUD. */
18
+ hud?: string | null;
19
+ }
20
+ export interface HudRef {
21
+ id: string;
22
+ /** Path under `public/scenes/`, e.g. `"hud/game-hud.json"`. */
23
+ file: string;
24
+ }
25
+ /**
26
+ * Per-game asset table. Source of truth for asset id → Phaser textureKey
27
+ * resolution. The editor and runtime both read from this.
28
+ */
29
+ export type AssetKind = 'image' | 'spritesheet' | 'atlas' | 'audio' | 'json';
30
+ export interface AssetRecord {
31
+ /** Stable asset id used in scene files (`sprite.assetId` / `image.assetId`). */
32
+ id: string;
33
+ /** Phaser texture / cache key. May equal `id` for simple cases. */
34
+ textureKey: string;
35
+ /** Path relative to `public/`. */
36
+ path: string;
37
+ kind: AssetKind;
38
+ /** For `spritesheet`: Phaser SpriteSheetConfig. */
39
+ spriteSheetConfig?: {
40
+ frameWidth: number;
41
+ frameHeight: number;
42
+ margin?: number;
43
+ spacing?: number;
44
+ };
45
+ /**
46
+ * Top-level convenience fields. When the agent writes a spritesheet asset
47
+ * entry, it commonly puts these at the top level instead of nested in
48
+ * `spriteSheetConfig` — both shapes are now accepted by the loader.
49
+ */
50
+ frameWidth?: number;
51
+ frameHeight?: number;
52
+ margin?: number;
53
+ spacing?: number;
54
+ /** For `atlas`: companion atlas file path (xml or json). */
55
+ atlasPath?: string;
56
+ /** `'xml'` (Starling) or `'json'` (TexturePacker). */
57
+ atlasFormat?: 'xml' | 'json';
58
+ /**
59
+ * For `spritesheet`: named animation ranges. SDK auto-registers these as
60
+ * Phaser anims at preload, so behavior code can call
61
+ * `sprite.play('walk-down')` without manual `anims.create()` setup.
62
+ *
63
+ * <p>Pack import populates this from vision detection (4-direction RPG /
64
+ * sidescroller / multi-action layouts) or from Aseprite `frameTags`. The
65
+ * agent can also add/edit entries by hand when wiring a sheet asset.
66
+ *
67
+ * <p>Animation keys are scene-global in Phaser; if two sheets register
68
+ * the same name, the second registration is skipped with a warning.
69
+ * Disambiguate by namespacing the name (e.g. `cow-walk-down`) when
70
+ * collisions matter.
71
+ */
72
+ animations?: SheetAnimation[];
73
+ /** Default playback rate for `animations[]`. Defaults to 8. */
74
+ fps?: number;
75
+ /**
76
+ * 9-slice cut-line metadata. Two shapes accepted:
77
+ *
78
+ * - **Single image** (slice 7.5) — `NinePatchConfig` directly. Used on
79
+ * `kind: 'image'` assets like a single button/panel background. All four
80
+ * corner widths apply to the image as a whole.
81
+ * - **Per-frame on a uniform grid** (slice 7.6) — `{ perFrame: NinePatchConfig }`.
82
+ * Used on `kind: 'spritesheet'` assets where every cell is its own
83
+ * stretchable button/panel sharing the same corner radius (typical
84
+ * itch.io UI button packs, e.g. `Square Buttons 26x26.png`). The HUD
85
+ * widget references which cell to render via `backgroundFrame`.
86
+ *
87
+ * Per-cell keyed config (`{ perFrame: Record<number, NinePatchConfig> }`)
88
+ * is deferred to v3 per design 07 §7.9.5 — promote only when a real pack
89
+ * needs different corner radii on different cells.
90
+ */
91
+ ninePatch?: NinePatchConfig | NinePatchPerFrame;
92
+ /**
93
+ * Vision-detected pixel-art flag. When true, SDK applies
94
+ * `Phaser.Textures.FilterMode.NEAREST` to the texture after load so it
95
+ * renders crisp instead of bilinear-blurred. Population is per-source:
96
+ * pack-import sets it via vision; AI generation / manual upload / library
97
+ * backfill close their own gaps separately (see design doc 07 §8.4).
98
+ */
99
+ pixelArt?: boolean;
100
+ /**
101
+ * Per-asset collision body metadata (slice 8 — see design doc 09).
102
+ *
103
+ * Two shapes:
104
+ * - `HitboxRect` — single rect shared across every frame (top-down
105
+ * characters with constant foot footprint, trees, buildings, walls).
106
+ * - `HitboxPerFrame` — `default` rect plus per-frame overrides indexed by
107
+ * frame number. Used for melee combat where an attack-swing frame's
108
+ * hitbox extends to where the tool reaches (Sprout Lands chop-the-tree
109
+ * case). The SDK installs an `ANIMATION_UPDATE` listener that swaps
110
+ * `body.setSize/setOffset` per frame during animation playback.
111
+ *
112
+ * v1 ships `kind: 'rect'` only. v2 adds `'circle'` as a pure forward-compat
113
+ * extension — see design doc 09 §9.2.1.
114
+ *
115
+ * Consumed by `applyAssetHitbox(sprite, asset)` at spawn time AND callable
116
+ * by behavior code after attaching a body. No-op if the sprite has no body.
117
+ */
118
+ hitbox?: HitboxRect | HitboxPerFrame;
119
+ /**
120
+ * Tileset metadata (slice 6 — see design doc 06).
121
+ *
122
+ * Present on assets used as tilemap tilesets. When set, the SDK knows
123
+ * the source-pixel cell size so `Phaser.Tilemap.addTilesetImage` can
124
+ * slice the image correctly. Phase A ships `cellSize` only; Phase C adds
125
+ * per-tile metadata (collision / damage / terrainTag); Phase D adds
126
+ * `autotile`; Phase F adds `animations`. All forward-compat — new
127
+ * optional sub-fields, never breaking the existing shape.
128
+ *
129
+ * Population paths:
130
+ * - Pack import wizard: vision-detected from uniform-grid sheets +
131
+ * "looks like a tileset" heuristic. Cell size auto-filled.
132
+ * - Configure Tileset modal: user picks a single-image asset as a
133
+ * tileset; modal prompts for cell size + persists onto this field.
134
+ *
135
+ * Note: a tileset asset typically has `kind: 'image'` (single PNG) — the
136
+ * `tileset` field is what marks it as "available as a tileset" in the
137
+ * editor's tilemap picker. Sprite-sheet assets (`kind: 'spritesheet'`)
138
+ * already carry frame dims via `spriteSheetConfig`; the SDK falls back
139
+ * to those when a layer's tileset is a spritesheet without a separate
140
+ * `tileset.cellSize`. See `resolveTilesetCellSize` in spawnEntity.ts.
141
+ */
142
+ tileset?: TilesetMetadata;
143
+ /**
144
+ * Asset-wide footprint anchor (slice 8 — see design doc 09 §2).
145
+ *
146
+ * Pixel within the asset's frame (top-left = 0,0) that defines where the
147
+ * sprite "touches the ground." SDK applies as
148
+ * `sprite.setOrigin(x / frameW, y / frameH)` at spawn, so `sprite.y` in
149
+ * world space equals the world y of the anchor pixel — perfect for ySort
150
+ * (`sprite.depth = sprite.y`).
151
+ *
152
+ * Typical values: character feet (e.g. `(16, 28)` on a 32×32 cell), tree
153
+ * trunk base, building foundation midpoint. Decorations / centered objects
154
+ * leave unset → Phaser default origin (0.5, 0.5) → ySort by geometric
155
+ * center (acceptable fallback; the scene panel surfaces a warning when
156
+ * ySort is on and some sprite assets lack anchor).
157
+ */
158
+ depthAnchor?: DepthAnchor;
159
+ }
160
+ /** Single-image 9-slice config. Pixels from each edge to the slice line. */
161
+ export interface NinePatchConfig {
162
+ leftWidth: number;
163
+ rightWidth: number;
164
+ topHeight: number;
165
+ bottomHeight: number;
166
+ }
167
+ /**
168
+ * Per-frame 9-slice config (slice 7.6). One shared `NinePatchConfig` applied
169
+ * to every frame of a uniform-grid spritesheet. Covers ~95% of itch.io UI
170
+ * button packs, where each cell is a different button color/state but all
171
+ * share the same corner radius.
172
+ */
173
+ export interface NinePatchPerFrame {
174
+ perFrame: NinePatchConfig;
175
+ }
176
+ /**
177
+ * Type guard — true when `ninePatch` is the per-frame variant. Lets call
178
+ * sites that only need to handle one shape stay narrow.
179
+ */
180
+ export declare function isPerFrameNinePatch(np: NinePatchConfig | NinePatchPerFrame | undefined): np is NinePatchPerFrame;
181
+ /**
182
+ * Rectangular hitbox in source-pixel coordinates (top-left = 0,0 within the
183
+ * asset's frame). Consumed by the SDK at sprite spawn time as
184
+ * `body.setSize(w, h)` + `body.setOffset(x, y)` when a physics body exists.
185
+ *
186
+ * v1 is the only supported `kind`. v2 adds `'circle'` (see design doc 09
187
+ * §9.2.1 for the full forward-compat migration story); polygon is v3+.
188
+ */
189
+ export interface HitboxRect {
190
+ kind: 'rect';
191
+ /** Top-left x of the hitbox in source pixels. */
192
+ x: number;
193
+ /** Top-left y of the hitbox in source pixels. */
194
+ y: number;
195
+ w: number;
196
+ h: number;
197
+ }
198
+ /**
199
+ * v2 — placeholder doc only. NOT in v1 schema. When v2 adds circle, this
200
+ * interface lands and the `AssetRecord.hitbox` + `HitboxPerFrame.default` /
201
+ * `.frames` unions widen to include it. v1 backend validation whitelists
202
+ * `kind in ['rect']`; v2 widens the whitelist. All v1 data stays valid.
203
+ *
204
+ * interface HitboxCircle {
205
+ * kind: 'circle';
206
+ * x: number; // center x in source-pixel coords of frame 0
207
+ * y: number; // center y in source-pixel coords of frame 0
208
+ * radius: number;
209
+ * }
210
+ */
211
+ /**
212
+ * Per-frame hitbox configuration. The `default` shape applies to every frame
213
+ * not present in `frames`; entries in `frames` override per frame index.
214
+ *
215
+ * This default+overrides model matches how combat sheets decompose — most
216
+ * frames share one hitbox (idle / walk / hurt at the feet), only attack-swing
217
+ * frames need a wider shape that reaches to where the tool / weapon visually
218
+ * extends. The motivating Sprout Lands case: 16-frame character sheet where
219
+ * frames 8–11 are an axe swing reaching ~16 pixels sideways past the foot.
220
+ *
221
+ * Discriminator: `'default' in hitbox` (HitboxRect has `kind` instead).
222
+ */
223
+ export interface HitboxPerFrame {
224
+ default: HitboxRect;
225
+ /** Frame index → override shape. Frames absent fall back to `default`. */
226
+ frames: Record<number, HitboxRect>;
227
+ }
228
+ /**
229
+ * Type guard — true when `hitbox` is the per-frame variant. Used by SDK
230
+ * runtime to decide whether to install an `ANIMATION_UPDATE` listener that
231
+ * swaps body shape per frame.
232
+ *
233
+ * <p>Checks `default != null` (handles both `null` and `undefined`) rather
234
+ * than `'default' in h`. The backend stores hitboxes after a mode switch
235
+ * with EXPLICIT NULLS on the alternate shape's fields — `{ kind: 'rect',
236
+ * x, y, w, h, default: null, frames: null }` for Single mode — because
237
+ * OpenSearch's _update API deep-merges nested objects and would otherwise
238
+ * leave stale fields. `'default' in h` would return true on that shape and
239
+ * trip the per-frame path with a null default. The null check is the
240
+ * correct discriminator.
241
+ */
242
+ export declare function isPerFrameHitbox(h: HitboxRect | HitboxPerFrame | undefined): h is HitboxPerFrame;
243
+ /**
244
+ * Depth anchor — pixel within the asset's frame (top-left = 0,0) that defines
245
+ * the sprite's footprint position. The SDK applies it at spawn as
246
+ * `sprite.setOrigin(x / frameW, y / frameH)` so the sprite's world `y` aligns
247
+ * with the world position of this pixel. ySort then compares foot positions
248
+ * directly via `sprite.depth = sprite.y` without per-asset offset math.
249
+ *
250
+ * Asset-wide — shared across all frames of a spritesheet. Per-frame variation
251
+ * is not in scope (footprint position doesn't change across animations even
252
+ * when hitbox shape does).
253
+ */
254
+ export interface DepthAnchor {
255
+ x: number;
256
+ y: number;
257
+ }
258
+ /**
259
+ * Tileset metadata for tilemap rendering (slice 6 — design doc 06).
260
+ *
261
+ * Carried on `AssetRecord.tileset`. Phase A ships the cell-size + layout
262
+ * fields needed to render a Phaser tilemap; later phases extend this
263
+ * shape additively (per-tile metadata, Wang autotile rules, animations).
264
+ */
265
+ export interface TilesetMetadata {
266
+ /**
267
+ * Source-image cell size in pixels. Each cell becomes one tile in the
268
+ * Phaser tileset (`map.addTilesetImage(id, key, cellSize.w, cellSize.h)`).
269
+ * Required — without this the SDK can't slice the source image.
270
+ */
271
+ cellSize: {
272
+ width: number;
273
+ height: number;
274
+ };
275
+ /**
276
+ * Optional column count of the cell grid. Used by the editor's tile
277
+ * palette and as a sanity check during slice; derivable from
278
+ * `texture.width / cellSize.width` when omitted.
279
+ */
280
+ cols?: number;
281
+ /** Optional row count of the cell grid. See `cols`. */
282
+ rows?: number;
283
+ /**
284
+ * Optional margin (pixels) around the entire tileset image — pixels
285
+ * from the image edge before the first cell starts. Defaults to 0.
286
+ */
287
+ margin?: number;
288
+ /**
289
+ * Optional spacing (pixels) between adjacent cells. Defaults to 0.
290
+ */
291
+ spacing?: number;
292
+ /**
293
+ * Per-tile metadata keyed by tile index (0-based, row-major in the tileset
294
+ * image). Sparse — only tiles with non-default metadata are stored.
295
+ * Authored via the Tile Metadata Editor modal (right-click a cell in the
296
+ * tile palette). Slice 6 Phase C — design doc 06 §5.
297
+ *
298
+ * Runtime wiring:
299
+ * - `solid: true` → SDK auto-flags via
300
+ * `layer.setCollisionByProperty({ solid: true })` at scene load.
301
+ * - other fields readable-but-not-auto-applied — game code reads via
302
+ * the SDK's `getTilemapAt(scene, entityId, x, y)` helper and decides.
303
+ */
304
+ tiles?: {
305
+ [tileIndex: number]: TileMetadata;
306
+ };
307
+ /**
308
+ * Wang 4-bit autotile configuration (slice 6 Phase D — design doc 06 §7).
309
+ *
310
+ * Present when this tileset supports terrain-mode painting. Multi-terrain
311
+ * baseline: one tileset can carry N terrains (grass + dirt-cliff + stone),
312
+ * each with its own rule map (§7.5). Authored via the manual rule-map
313
+ * editor (Phase D.3) or auto-populated by vision on pack import (Phase
314
+ * D.4); SDK runtime (Phase D.2) reads `terrains[i].ruleMap` at paint time
315
+ * to pick the right tile index from the 4-corner bitmask.
316
+ */
317
+ autotile?: TilesetAutotile;
318
+ /**
319
+ * Animated tiles (slice 6 Phase F — design doc 06 §8). Each entry
320
+ * defines an animation cycle keyed on a "root" tile index. At runtime
321
+ * the SDK installs a per-frame update hook that swaps every painted
322
+ * cell whose source index === `rootTileIndex` through the animation's
323
+ * frame sequence, all cells synchronously (water-flow / lava-bubble /
324
+ * torch-flicker pattern).
325
+ *
326
+ * Painted data stores the root tile index; the SDK overlays the
327
+ * current-frame index at render time. On enterEdit, animated cells are
328
+ * reset back to root so the editor always shows the static "data"
329
+ * frame, not whatever was currently visible at the moment of toggle.
330
+ * Save paths read from the host-side draft (not the iframe's mutated
331
+ * tile state), so saving mid-animation is safe.
332
+ *
333
+ * Phaser's TilemapLayer does NOT auto-render Tiled `tile.animation`
334
+ * data — it only parses it. The SDK drives frame swaps via a scene
335
+ * UPDATE listener; design 06 §8.8's claim of "Phaser handles timing
336
+ * and rendering" was wrong (the parser stores the data on
337
+ * `tileset.tileData[id].animation` but no rendering code consumes it).
338
+ * 50-line custom impl avoids depending on the bit-rotted
339
+ * `phaser-animated-tiles` community plugin.
340
+ */
341
+ animations?: TilesetAnimation[];
342
+ }
343
+ /**
344
+ * One animation cycle on a tileset (slice 6 Phase F).
345
+ *
346
+ * Painted tiles whose source index equals `rootTileIndex` cycle through
347
+ * `frames[*].tileIndex` per their `duration` (ms). All cells animate
348
+ * synchronously — Sprout Lands water lake-cells all flow together.
349
+ * Per-cell phase offset (organic-water look) is v2.
350
+ *
351
+ * Example — Sprout Lands 4-frame water at 4fps:
352
+ * ```json
353
+ * {
354
+ * "id": "water-flow",
355
+ * "rootTileIndex": 12,
356
+ * "frames": [
357
+ * { "tileIndex": 12, "duration": 250 },
358
+ * { "tileIndex": 13, "duration": 250 },
359
+ * { "tileIndex": 14, "duration": 250 },
360
+ * { "tileIndex": 15, "duration": 250 }
361
+ * ]
362
+ * }
363
+ * ```
364
+ *
365
+ * The first frame conventionally references `rootTileIndex` so the
366
+ * static editor view (where animations don't run) matches frame 0 of
367
+ * the animation. Not enforced — the cycle starts on whichever frame the
368
+ * elapsed-time math lands on; users who set `frames[0].tileIndex` !==
369
+ * `rootTileIndex` get a visible jump when the animation starts.
370
+ */
371
+ export interface TilesetAnimation {
372
+ /** Stable id within the tileset's `animations[]`. Used by editor UI for selection. */
373
+ id: string;
374
+ /**
375
+ * Tile index (0-based, row-major in the tileset image) that the user
376
+ * paints with. All cells painted with this index get animated.
377
+ */
378
+ rootTileIndex: number;
379
+ /**
380
+ * Frames cycled through in order, then looping. Empty frames array →
381
+ * SDK skips this animation (no error, just no-op).
382
+ */
383
+ frames: Array<{
384
+ /** Tile index to display during this frame. */
385
+ tileIndex: number;
386
+ /** Milliseconds to hold this frame before advancing. Minimum 16ms (~60fps). */
387
+ duration: number;
388
+ }>;
389
+ }
390
+ /**
391
+ * Wang autotile configuration on a tileset (slice 6 Phase D — design
392
+ * doc 06 §7). Multi-terrain: one tileset can drive multiple independent
393
+ * autotile palettes.
394
+ *
395
+ * Two modes:
396
+ *
397
+ * - **`'wang-4bit'`** (Phase D.1–D.2 baseline, design §7.1–§7.4) — 4 corner
398
+ * bits per cell, max 16 ruleMap entries. Covers 9 unique edge/corner
399
+ * patterns + interior; missing 6 concave/diagonal cases per §7.7. Each
400
+ * terrain typically needs 9-15 tiles.
401
+ *
402
+ * - **`'wang-8bit'`** (Phase D.2.5.2, design §7.7 "blob" / Cr31
403
+ * "2-edge + 2-corner") — 4 corner bits + 4 side bits per cell, max 256
404
+ * ruleMap entries (~47 geometrically valid). Distinguishes patterns
405
+ * the 4-bit model collapses to bm 15 (isolated 1×1, 1-wide strips,
406
+ * T-intersections). Each terrain typically needs 47 tiles for full
407
+ * coverage; commercial tilesets like Sprout Lands ship these.
408
+ */
409
+ export interface TilesetAutotile {
410
+ kind: 'wang-4bit' | 'wang-8bit';
411
+ /**
412
+ * One entry per terrain this tileset supports. Order is editor-display
413
+ * order (also serves as the default selection in the palette).
414
+ */
415
+ terrains: WangTerrain[];
416
+ }
417
+ /**
418
+ * One Wang terrain palette inside a tileset (slice 6 Phase D).
419
+ *
420
+ * Each terrain owns a `ruleMap` from a bitmask to a tile index. The bit
421
+ * encoding depends on the parent `TilesetAutotile.kind`:
422
+ *
423
+ * - **`'wang-4bit'`**: bitmask is 4 bits (0..15). Bit 0 = BR corner, 1 = BL,
424
+ * 2 = TR, 3 = TL. So `0b1111 = 15` = interior, `0b0000 = 0` = empty.
425
+ *
426
+ * - **`'wang-8bit'`**: bitmask is 8 bits (0..255). Low 4 bits = corners
427
+ * (same as 4-bit). High 4 bits = sides: bit 4 = E neighbor painted,
428
+ * 5 = W, 6 = S, 7 = N. So `0b00001111 = 15` = isolated 1×1 island
429
+ * (all corners but no painted neighbors), `0b11111111 = 255` = full
430
+ * interior. Geometrically: if a side bit is set, the 2 corners adjacent
431
+ * to that side must also be set (the painted neighbor touches them).
432
+ *
433
+ * Sparse — keys without an entry mean "no tile for this configuration",
434
+ * and the cell stays empty when the painter resolves that bitmask. Bitmask
435
+ * 0 is typically absent (it's the "empty / unpainted" cell).
436
+ */
437
+ export interface WangTerrain {
438
+ /**
439
+ * Stable id unique within this tileset's `autotile.terrains[]`. Layers
440
+ * reference this via `TilemapLayer.autotile.terrainId`. Typically the
441
+ * same string as `terrainTag` (e.g. `"grass"`) but kept separate so
442
+ * users can rename the display tag without rewriting every layer.
443
+ */
444
+ id: string;
445
+ /**
446
+ * Free-form terrain label mirroring `TileMetadata.terrainTag`. Allows
447
+ * behavior code to cross-reference "what terrain did the user paint
448
+ * here?" with per-tile gameplay metadata (movement, damage, etc.).
449
+ */
450
+ terrainTag: string;
451
+ /**
452
+ * Map from bitmask to tile index in the tileset image (0-based,
453
+ * row-major). Key range depends on parent kind: 0..15 for `wang-4bit`,
454
+ * 0..255 for `wang-8bit`. JSON wire format uses string keys (Jackson
455
+ * default for `Map<Integer, V>`); the SDK accepts both string and
456
+ * numeric keys at runtime.
457
+ */
458
+ ruleMap: {
459
+ [bitmask: number]: number;
460
+ };
461
+ }
462
+ /**
463
+ * Per-tile metadata for tilemap collision / gameplay (slice 6 Phase C —
464
+ * design doc 06 §5.1). All fields optional and default at runtime when
465
+ * undefined. Mirrors backend `TilesetMetadata.TileMetadata`.
466
+ */
467
+ export interface TileMetadata {
468
+ /** Blocks player via Phaser's `setCollisionByProperty({ solid: true })`. */
469
+ solid?: boolean;
470
+ /** Only collides from above (jump-through platforms). v1 stores flag only. */
471
+ oneWay?: boolean;
472
+ /** `"up-left"` / `"up-right"` / undefined — slope direction. */
473
+ slope?: string;
474
+ /** HP/sec dealt when player stands on this tile. Game code reads + applies. */
475
+ damage?: number;
476
+ /** Free-form terrain label (`"grass"` / `"water"` / …) for Phase D + behavior. */
477
+ terrainTag?: string;
478
+ /** `"walk"` / `"swim"` / `"fly"` / `"block"` — movement modifier. */
479
+ movement?: string;
480
+ /** Free-form ground-type label for footstep-sound selection. */
481
+ groundType?: string;
482
+ /** Free-form user tag for game-specific logic (e.g. `"checkpoint"`). */
483
+ customTag?: string;
484
+ /**
485
+ * Sub-tile collision rectangles (slice 6 Phase C follow-up). N axis-
486
+ * aligned rects in source-pixel coords, relative to tile top-left.
487
+ * Multi-rect supports L / U / frame-shaped collision (wall corners,
488
+ * borders). When non-empty, the SDK creates one invisible Arcade static
489
+ * body per rect at each painted instance and disables Phaser's native
490
+ * cell-rect collision for those cells. When empty but `solid: true`,
491
+ * default Phaser cell-rect collision applies (full tile collides).
492
+ */
493
+ collisionRects?: Array<{
494
+ x: number;
495
+ y: number;
496
+ w: number;
497
+ h: number;
498
+ }>;
499
+ }
500
+ export interface SheetAnimation {
501
+ /** Phaser animation key — used as `sprite.play('<name>')`. */
502
+ name: string;
503
+ /** First frame index, inclusive. */
504
+ from: number;
505
+ /** Last frame index, inclusive. */
506
+ to: number;
507
+ /** When true, Phaser registers with `repeat: -1`. Defaults to true. */
508
+ loop?: boolean;
509
+ /** Override the asset-level fps for this specific animation. */
510
+ fps?: number;
511
+ }
512
+ export interface Manifest {
513
+ schemaVersion: number;
514
+ id: string;
515
+ title: string;
516
+ version: string;
517
+ initialScene: string;
518
+ scenes: SceneRef[];
519
+ huds: HudRef[];
520
+ assets: AssetRecord[];
521
+ /**
522
+ * Entity TYPE definitions for runtime spawning — slice 11, Phase B.1.
523
+ * Each prefab is a self-contained template: visual + physics + properties.
524
+ * Code spawns instances via the SDK's `spawnPrefab(scene, prefabId, x, y)`.
525
+ * Optional — games that have only authored static entities (no runtime
526
+ * spawning) can omit this field.
527
+ */
528
+ prefabs?: PrefabRecord[];
529
+ globals?: {
530
+ physics?: {
531
+ gravity?: {
532
+ x?: number;
533
+ y?: number;
534
+ };
535
+ };
536
+ world?: {
537
+ pixelArt?: boolean;
538
+ };
539
+ };
540
+ }
541
+ /**
542
+ * Prefab kinds — same renderable kinds as world entities. SDK 0.3.0 flattened
543
+ * `primitive` → `rect | circle`. (Prefabs never targeted `tilemap` / `trigger`
544
+ * / `group` — those are scene-authored, not runtime-spawned.)
545
+ */
546
+ export type PrefabKind = 'sprite' | 'rect' | 'circle' | 'code-rendered';
547
+ /**
548
+ * Arcade physics body configuration. Used by prefabs (`PrefabRecord.physics`,
549
+ * applied inside `spawnPrefab`) AND by authored scene entities
550
+ * (`WorldEntityBase.physics`, applied inside `spawnEntity`). In both cases the
551
+ * SDK calls `physics.add.existing` and applies these knobs to the body, so
552
+ * behavior code doesn't have to call `body.setSize` / `setOffset` manually.
553
+ *
554
+ * Conventions:
555
+ * - `bodyW` / `bodyH` default to the visual's drawn extent (sprite
556
+ * texture size, primitive width/height, or code-rendered visual.width
557
+ * / visual.height — 64 if unset).
558
+ * - `offsetX` / `offsetY` default to centering the body inside the
559
+ * GameObject's local bounds (i.e. `(visualW - bodyW) / 2`).
560
+ * - All fields are optional; pass an empty `physics: {}` to get the
561
+ * default body sized to the visual.
562
+ */
563
+ export interface PrefabPhysics {
564
+ bodyW?: number;
565
+ bodyH?: number;
566
+ offsetX?: number;
567
+ offsetY?: number;
568
+ immovable?: boolean;
569
+ velocityX?: number;
570
+ velocityY?: number;
571
+ collideWorldBounds?: boolean;
572
+ bounceX?: number;
573
+ bounceY?: number;
574
+ }
575
+ /**
576
+ * Entity TYPE — used as a template by `spawnPrefab` to create runtime
577
+ * instances. Spawn-time coordinates and per-instance overrides come from
578
+ * the caller; everything else is shared across all instances.
579
+ *
580
+ * Prefabs do NOT have a `transform` field — that's set per spawn.
581
+ * Editor live-edits to a prefab record propagate to existing instances
582
+ * via `umicat:editor:editPrefab` (see slice 11 §6.4).
583
+ *
584
+ * **Flat schema** (SDK 0.3.0). Prefab fields live at the top level —
585
+ * exactly like world entities. The shape depends on `kind`:
586
+ *
587
+ * - `kind: 'sprite'` + `{ assetId, frame?, tint?, alpha?, flipX?, flipY? }`
588
+ * - `kind: 'rect'` + `{ width, height, fillColor?, strokeColor?, strokeWidth?, alpha? }`
589
+ * - `kind: 'circle'` + `{ radius, fillColor?, strokeColor?, strokeWidth?, alpha? }`
590
+ * - `kind: 'code-rendered'` + `{ script, params?, width?, height? }`
591
+ */
592
+ export type PrefabRecord = SpritePrefab | RectPrefab | CirclePrefab | CodeRenderedPrefab;
593
+ export interface PrefabBase {
594
+ /** Stable id used by `spawnPrefab(scene, '<id>', ...)`. */
595
+ id: string;
596
+ /** Semantic tag — `registry.byRole('enemy')` lists every spawned instance. */
597
+ role?: string;
598
+ /** Physics body config applied at spawn. Omit when the prefab has no body. */
599
+ physics?: PrefabPhysics;
600
+ /** Free-form per-type data behavior code reads via `go.getData('entityProperties').<key>`. */
601
+ properties?: Record<string, unknown>;
602
+ }
603
+ export interface SpritePrefab extends PrefabBase {
604
+ kind: 'sprite';
605
+ assetId: string;
606
+ frame?: string | number;
607
+ tint?: string;
608
+ alpha?: number;
609
+ flipX?: boolean;
610
+ flipY?: boolean;
611
+ }
612
+ export interface RectPrefab extends PrefabBase {
613
+ kind: 'rect';
614
+ width: number;
615
+ height: number;
616
+ fillColor?: string;
617
+ strokeColor?: string | null;
618
+ strokeWidth?: number;
619
+ alpha?: number;
620
+ }
621
+ export interface CirclePrefab extends PrefabBase {
622
+ kind: 'circle';
623
+ radius: number;
624
+ fillColor?: string;
625
+ strokeColor?: string | null;
626
+ strokeWidth?: number;
627
+ alpha?: number;
628
+ }
629
+ export interface CodeRenderedPrefab extends PrefabBase {
630
+ kind: 'code-rendered';
631
+ script: string;
632
+ params?: Record<string, unknown>;
633
+ width?: number;
634
+ height?: number;
635
+ }
636
+ /**
637
+ * Wave schedule — a time-ordered sequence of spawn instructions that
638
+ * reference prefab ids. Models everything from a Galaga formation to a
639
+ * survivor game's difficulty curve.
640
+ *
641
+ * Each schedule lives in its own file under `public/waves/<id>.json`.
642
+ * Behavior code consumes them via `runWaveSchedule(scene, id, callbacks)`
643
+ * which returns a `WaveController` for game-flow operations
644
+ * (pause / stop / skip).
645
+ *
646
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §7
647
+ */
648
+ export interface WaveScheduleRecord {
649
+ schemaVersion: number;
650
+ id: string;
651
+ name?: string;
652
+ waves: WaveRecord[];
653
+ /** Restart from wave 0 after the last wave's endCondition fires. */
654
+ loop?: boolean;
655
+ }
656
+ export interface WaveRecord {
657
+ id: string;
658
+ /** Wait this long after the previous wave's endCondition fires before starting. */
659
+ delayMs?: number;
660
+ spawns: SpawnInstruction[];
661
+ /** When this wave is considered "complete" — default `'allDead'`. */
662
+ endCondition?: WaveEndCondition;
663
+ }
664
+ /**
665
+ * One spawn instruction within a wave. v1 ships `point` and `formation`
666
+ * (with `shape: 'grid' | 'line'`) — `arc`, `v-shape`, and `random` are
667
+ * forward-compat'd in the wire shape but the runtime treats unknown
668
+ * shapes as a single `point` at the formation origin and warns.
669
+ */
670
+ export type SpawnInstruction = SpawnPointInstruction | SpawnFormationInstruction;
671
+ export interface SpawnPointInstruction {
672
+ kind: 'point';
673
+ prefabId: string;
674
+ x: number;
675
+ y: number;
676
+ /** Time within the wave (relative to wave start) at which to spawn. */
677
+ atMs: number;
678
+ /** Optional per-spawn overrides — same shape as `SpawnPrefabOverrides`. */
679
+ overrides?: Record<string, unknown>;
680
+ }
681
+ export interface SpawnFormationInstruction {
682
+ kind: 'formation';
683
+ prefabId: string;
684
+ formation: FormationRecord;
685
+ /** Time within the wave at which the formation starts spawning. */
686
+ startMs: number;
687
+ /** Delay between successive entity spawns inside the formation. */
688
+ intervalMs: number;
689
+ /** Optional per-spawn overrides shared by every formation member. */
690
+ overrides?: Record<string, unknown>;
691
+ }
692
+ export type FormationShape = 'grid' | 'line' | 'arc' | 'v-shape';
693
+ export interface FormationRecord {
694
+ shape: FormationShape;
695
+ /** Grid + v-shape: number of rows. Line + arc: usually 1. */
696
+ rows?: number;
697
+ /** Grid + line + arc + v-shape: number of columns / members per row. */
698
+ cols?: number;
699
+ spacing?: {
700
+ x?: number;
701
+ y?: number;
702
+ };
703
+ /** World-coordinate top-left (grid) or center (arc) of the formation. */
704
+ origin?: {
705
+ x: number;
706
+ y: number;
707
+ };
708
+ /** Arc-only: radius from `origin`. */
709
+ radius?: number;
710
+ /** Arc-only: span in radians (default π). */
711
+ arcRadians?: number;
712
+ }
713
+ export type WaveEndCondition = 'allDead' | {
714
+ type: 'time';
715
+ ms: number;
716
+ } | {
717
+ type: 'count';
718
+ killed: number;
719
+ };
720
+ export interface Transform {
721
+ x: number;
722
+ y: number;
723
+ /** Radians. */
724
+ rotation?: number;
725
+ scaleX?: number;
726
+ scaleY?: number;
727
+ depth?: number;
728
+ }
729
+ export type AnchorSide = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
730
+ export interface Anchor {
731
+ side: AnchorSide;
732
+ offsetX?: number;
733
+ offsetY?: number;
734
+ }
735
+ export type WorldEntityKind = 'sprite' | 'rect' | 'circle' | 'code-rendered' | 'group' | 'tilemap' | 'trigger';
736
+ export interface WorldEntityBase {
737
+ id: string;
738
+ /** Optional semantic tag. Behavior code keys off this. */
739
+ role?: string;
740
+ /** Free-form per-entity data the agent's behavior code reads. */
741
+ properties?: Record<string, unknown>;
742
+ transform: Transform;
743
+ /**
744
+ * Optional Arcade physics body. When set, `spawnEntity` calls
745
+ * `physics.add.existing` and applies the body's size / offset / immovable /
746
+ * velocity / bounce — the same body-level path and `PrefabPhysics` shape
747
+ * `spawnPrefab` uses, so authored entities and runtime instances get
748
+ * identical bodies. Applied for renderable entities (`sprite` / `rect` /
749
+ * `circle` / `code-rendered`); ignored on `group` / `tilemap` / `trigger`.
750
+ * Omit for entities with no body, or that wire physics in behavior code.
751
+ */
752
+ physics?: PrefabPhysics;
753
+ }
754
+ export interface SpriteEntity extends WorldEntityBase {
755
+ kind: 'sprite';
756
+ assetId: string;
757
+ /** Atlas frame name OR sprite-sheet frame index. */
758
+ frame?: string | number;
759
+ tint?: string;
760
+ alpha?: number;
761
+ flipX?: boolean;
762
+ flipY?: boolean;
763
+ }
764
+ export interface RectEntity extends WorldEntityBase {
765
+ kind: 'rect';
766
+ width: number;
767
+ height: number;
768
+ fillColor?: string;
769
+ strokeColor?: string | null;
770
+ strokeWidth?: number;
771
+ alpha?: number;
772
+ }
773
+ export interface CircleEntity extends WorldEntityBase {
774
+ kind: 'circle';
775
+ radius: number;
776
+ fillColor?: string;
777
+ strokeColor?: string | null;
778
+ strokeWidth?: number;
779
+ alpha?: number;
780
+ }
781
+ export interface CodeRenderedEntity extends WorldEntityBase {
782
+ kind: 'code-rendered';
783
+ /** Path to render script, e.g. `"src/visuals/boss-renderer.ts"`. */
784
+ script: string;
785
+ params?: Record<string, unknown>;
786
+ /**
787
+ * Optional bounds used for editor hit-testing (click + drag) and the
788
+ * selection rectangle. Phaser Graphics has no intrinsic size, so we set
789
+ * it explicitly. Defaults to 64×64 centered on the entity. The agent can
790
+ * override per-entity when the script draws something larger or smaller.
791
+ */
792
+ width?: number;
793
+ height?: number;
794
+ }
795
+ export interface GroupEntity extends WorldEntityBase {
796
+ kind: 'group';
797
+ /**
798
+ * One level of nesting in v1: a child cannot itself be a `group`.
799
+ * If you need deeper hierarchy, the agent should flatten via the
800
+ * existing transform and add a role tag for grouping queries.
801
+ */
802
+ children: NonGroupWorldEntity[];
803
+ }
804
+ /**
805
+ * Tilemap entity — slice 6.
806
+ *
807
+ * Phase A (render-only foundation, 2026-05-18): SDK reads tilemap entities
808
+ * and renders them as Phaser Tilemaps. One tileset per layer; per-cell
809
+ * `data` is a 2D array of tile indices (numeric, matching Phaser's native
810
+ * tile-id model). Empty layers (no `data`) fall back to the slice-3 grid
811
+ * sketch so unpainted tilemaps still show their bounds in the editor.
812
+ *
813
+ * Phase B adds the painter UI; Phase C adds per-tile metadata + Y-sort;
814
+ * Phase D adds Wang autotile resolution; Phase F adds animated tiles.
815
+ *
816
+ * Multi-tileset per layer (`tilesetIds: string[]`) is reserved in the
817
+ * shape for later phases but Phase A only consumes the first id.
818
+ */
819
+ export interface TilemapEntity extends WorldEntityBase {
820
+ kind: 'tilemap';
821
+ /** Render-time cell size in pixels. Layer data indexes cells at this size. */
822
+ tileSize: {
823
+ width: number;
824
+ height: number;
825
+ };
826
+ /** Map size in cells (not pixels). */
827
+ size: {
828
+ width: number;
829
+ height: number;
830
+ };
831
+ layers: TilemapLayer[];
832
+ }
833
+ export interface TilemapLayer {
834
+ id: string;
835
+ name?: string;
836
+ /**
837
+ * Tilesets this layer can paint with. Phase A consumes the FIRST id only
838
+ * (single-tileset rendering); the array shape is kept so later phases
839
+ * can paint blended layers (grass + flowers) without a schema break.
840
+ */
841
+ tilesetIds?: string[];
842
+ z?: number;
843
+ /**
844
+ * 2D array of tile indices (row-major: `data[y][x]`). `null` cells are
845
+ * empty (not painted). Integer indices match Phaser's native tile-id
846
+ * scheme — the index within the tileset image, 0-based, row-major. Phase
847
+ * D will introduce a parallel autotile bitmask channel for Wang layers.
848
+ */
849
+ data?: Array<Array<number | null>>;
850
+ visible?: boolean;
851
+ /** Editor-only: prevents accidental painting. No runtime effect. */
852
+ locked?: boolean;
853
+ /**
854
+ * Wang 4-bit autotile binding (slice 6 Phase D — design doc 06 §7).
855
+ *
856
+ * When set, the painter operates in "terrain" mode on this layer: each
857
+ * brush stroke marks corner-vertices for `terrainId` and the SDK
858
+ * resolves the tile index from the tileset's `autotile.terrains[]`
859
+ * ruleMap on every affected cell. `terrainId` references
860
+ * `TilesetAutotile.terrains[].id` on the layer's first tileset.
861
+ *
862
+ * Absent (the common case) → layer is a plain stamp painter; cells
863
+ * carry author-chosen tile indices verbatim.
864
+ */
865
+ autotile?: {
866
+ terrainId: string;
867
+ };
868
+ ySort?: boolean;
869
+ }
870
+ export type TriggerShape = {
871
+ kind: 'rect';
872
+ width: number;
873
+ height: number;
874
+ } | {
875
+ kind: 'circle';
876
+ radius: number;
877
+ };
878
+ /**
879
+ * Trigger entity — slice 3 ships a stub renderer (semi-transparent rect/circle
880
+ * with the design's cyan tint). Full behavior wiring (description / targets /
881
+ * behavior script) lands in slice 5 (03-world-editor.md §8).
882
+ */
883
+ export interface TriggerEntity extends WorldEntityBase {
884
+ kind: 'trigger';
885
+ shape: TriggerShape;
886
+ /** User-facing label shown in Hierarchy + on canvas. */
887
+ description?: string;
888
+ /** Entity ids this trigger affects. Slice 5 wires the relationship UI. */
889
+ targets?: string[];
890
+ /** Optional path to behavior script. Slice 5 fills this in. */
891
+ behaviorScript?: string;
892
+ /** Optional preset name (open-door / damage-zone / scene-transition / spawner / heal). */
893
+ preset?: string;
894
+ }
895
+ export type NonGroupWorldEntity = SpriteEntity | RectEntity | CircleEntity | CodeRenderedEntity | TilemapEntity | TriggerEntity;
896
+ export type WorldEntity = NonGroupWorldEntity | GroupEntity;
897
+ /**
898
+ * Renderable entity kinds — those that produce a single GameObject with
899
+ * physics + transform. Excludes `group` (container), `tilemap` (multi-layer
900
+ * data), `trigger` (invisible volume).
901
+ */
902
+ export type RenderableEntity = SpriteEntity | RectEntity | CircleEntity | CodeRenderedEntity;
903
+ export interface CameraConfig {
904
+ /** Entity id to follow. Null = static camera. */
905
+ follow?: string | null;
906
+ smoothing?: number;
907
+ deadzone?: {
908
+ x: number;
909
+ y: number;
910
+ };
911
+ lookahead?: number;
912
+ bounds?: {
913
+ x: number;
914
+ y: number;
915
+ width: number;
916
+ height: number;
917
+ };
918
+ zoom?: number;
919
+ pixelPerfect?: boolean;
920
+ shakeDecay?: number;
921
+ }
922
+ export interface WorldSceneConfig {
923
+ width: number;
924
+ height: number;
925
+ background?: {
926
+ color?: string;
927
+ };
928
+ physics?: {
929
+ gravity?: {
930
+ x?: number;
931
+ y?: number;
932
+ };
933
+ };
934
+ }
935
+ export interface WorldScene {
936
+ schemaVersion: number;
937
+ id: string;
938
+ name: string;
939
+ type: 'world';
940
+ world: WorldSceneConfig;
941
+ camera?: CameraConfig;
942
+ entities: WorldEntity[];
943
+ /**
944
+ * Scene-level Y-sort opt-in (slice 8 — see design doc 09 §2.2).
945
+ *
946
+ * When `true`, the SDK installs a per-frame update hook in `loadWorldScene`
947
+ * that walks the entity registry and sets `sprite.depth = sprite.y` so
948
+ * closer-to-camera sprites overlap farther ones. Default `false` —
949
+ * side-scrollers, single-screen arcade, and chat-only Workflow A games
950
+ * don't pay the per-frame cost.
951
+ *
952
+ * Two opt-out mechanisms (orthogonal, cover different intents):
953
+ * - `entity.transform.depth` set to a number → that constant wins, ySort
954
+ * skips the entity. For "cloud always on top," "tilemap at -1000," etc.
955
+ * - `entity.properties.skipYSort: true` → ySort doesn't touch the entity's
956
+ * depth at all. For behavior code that manages depth dynamically.
957
+ *
958
+ * Top-down RPG / farming / iso / city-builder / MOBA / 2.5D platformer
959
+ * are the target genres.
960
+ */
961
+ ySort?: boolean;
962
+ metadata?: {
963
+ tags?: string[];
964
+ author?: string;
965
+ createdAt?: string;
966
+ updatedAt?: string;
967
+ };
968
+ }
969
+ export type HudLayer = 'base' | 'overlay' | 'modal';
970
+ /**
971
+ * Text content source — `static` is a literal string the user typed;
972
+ * `dynamic` reads a value from `umicat.gameData` at runtime, with optional
973
+ * prefix/suffix formatting. The Inspector only edits the static path; the
974
+ * agent flips a widget to `dynamic` via the inline AI popover when the user
975
+ * asks for live values ("show current score"). The variable registry +
976
+ * picker UX from design 04 §4.7 is intentionally NOT implemented — Umicat's
977
+ * thesis is users don't see "variables", agent handles bindings.
978
+ */
979
+ export type HudTextSource = {
980
+ mode: 'static';
981
+ text: string;
982
+ } | {
983
+ mode: 'dynamic';
984
+ /** Key in `umicat.gameData` whose value drives the text. */
985
+ binding: string;
986
+ prefix?: string;
987
+ suffix?: string;
988
+ /** Fallback value rendered when the binding is unset. */
989
+ fallback?: string;
990
+ };
991
+ /**
992
+ * A progress bar's numeric input. Static = literal number; dynamic = read
993
+ * from `umicat.gameData` / Phaser's game registry by key. Mirrors
994
+ * `HudTextSource` but for numbers (no prefix/suffix — bar is visual).
995
+ */
996
+ export type HudNumberSource = {
997
+ mode: 'static';
998
+ value: number;
999
+ } | {
1000
+ mode: 'dynamic';
1001
+ binding: string;
1002
+ /** Used when the binding is unset at runtime. Defaults to 0. */
1003
+ fallback?: number;
1004
+ };
1005
+ export type HudEntityKind = 'text' | 'image' | 'icon-button' | 'progress-bar' | 'panel';
1006
+ export interface HudEntityBase {
1007
+ id: string;
1008
+ /** Optional semantic tag. Behavior code keys off this. */
1009
+ role?: string;
1010
+ /** Free-form per-entity data the agent's behavior code reads. */
1011
+ properties?: Record<string, unknown>;
1012
+ anchor: Anchor;
1013
+ /** Render layer (base < overlay < modal). Defaults to `base`. */
1014
+ layer?: HudLayer;
1015
+ /**
1016
+ * Z-order within the same layer. Higher = on top. Optional; defaults to
1017
+ * insertion order if absent.
1018
+ */
1019
+ z?: number;
1020
+ /** When false, the widget is hidden at runtime. Useful for modal panels
1021
+ * that the agent toggles via gameData / scene events. */
1022
+ visible?: boolean;
1023
+ }
1024
+ /**
1025
+ * HUD text widget — renders a string at an anchor position. Source is either
1026
+ * static (a literal) or dynamic (a key into `umicat.gameData` / Phaser's
1027
+ * game registry). All fields flat per SDK 0.3.0.
1028
+ */
1029
+ export interface HudTextEntity extends HudEntityBase {
1030
+ kind: 'text';
1031
+ source: HudTextSource;
1032
+ fontFamily?: string;
1033
+ fontSize?: number;
1034
+ color?: string;
1035
+ align?: 'left' | 'center' | 'right';
1036
+ }
1037
+ /**
1038
+ * HUD image widget — renders an asset by id at an anchor position. Supports
1039
+ * single images, atlas frames, and sprite-sheet frame indexes. Optional 9-slice
1040
+ * stretching when the referenced asset has `ninePatch` metadata.
1041
+ */
1042
+ export interface HudImageEntity extends HudEntityBase {
1043
+ kind: 'image';
1044
+ assetId: string;
1045
+ frame?: string | number;
1046
+ /** CSS pixel size on the HUD scene's logical canvas. */
1047
+ width?: number;
1048
+ height?: number;
1049
+ tint?: string;
1050
+ alpha?: number;
1051
+ }
1052
+ /**
1053
+ * Icon button. v1 has no built-in onPress wiring — clicks emit a Phaser
1054
+ * scene event `hud:press` with the entity id, and the agent's behavior code
1055
+ * subscribes (`scene.events.on('hud:press', id => ...)`). The slice-4
1056
+ * popover is the user's path to wire one up: select the button, ✨, "pause
1057
+ * the game when this is pressed". design 04 §6 / §7's bind-to-action +
1058
+ * built-in-presets system is reserved for slice 5.5.
1059
+ */
1060
+ export interface HudIconButtonEntity extends HudEntityBase {
1061
+ kind: 'icon-button';
1062
+ /** Optional displayed label. */
1063
+ label?: string;
1064
+ /** Optional icon asset shown inside the button. */
1065
+ iconAssetId?: string;
1066
+ /**
1067
+ * Optional image asset rendered as the button background. When the
1068
+ * referenced asset has `ninePatch` metadata, the SDK renders it via
1069
+ * `phaser3-rex-plugins` NinePatch so corners stay fixed at any width/height;
1070
+ * otherwise the image is stretched to the widget's size. When set, the
1071
+ * colored-rect bg below (fillColor / strokeColor / shape) is ignored, and
1072
+ * `pressedFillColor` no-ops (no fill swap on a textured bg in v1).
1073
+ *
1074
+ * For uniform-grid spritesheet assets with `ninePatch.perFrame` set
1075
+ * (slice 7.6), pair this with `backgroundFrame` to pick which cell to
1076
+ * render — common case is a button-pack sheet where every cell is a
1077
+ * different button color/state.
1078
+ */
1079
+ backgroundAssetId?: string;
1080
+ /**
1081
+ * For uniform-grid spritesheet backgrounds (slice 7.6): index of the cell
1082
+ * to render. Defaults to 0 when omitted. Ignored when the background asset
1083
+ * is a single image. Mutually exclusive with `backgroundRegion` — when both
1084
+ * are set, the SDK picks `backgroundRegion` (atlas-name lookup wins).
1085
+ */
1086
+ backgroundFrame?: number;
1087
+ /**
1088
+ * For region-atlas backgrounds (slice 10): name of the atlas frame to
1089
+ * render. Used when the referenced asset is `kind: 'atlas'` + `atlasFormat:
1090
+ * 'json'` — the region atlas can carry per-frame `ninePatch` metadata, in
1091
+ * which case the SDK 9-slices each region independently. Mutually exclusive
1092
+ * with `backgroundFrame`.
1093
+ */
1094
+ backgroundRegion?: string;
1095
+ /** Button shape. Default `rounded-rect`. Ignored when backgroundAssetId is set. */
1096
+ shape?: 'rounded-rect' | 'circle';
1097
+ width?: number;
1098
+ height?: number;
1099
+ fillColor?: string;
1100
+ strokeColor?: string | null;
1101
+ strokeWidth?: number;
1102
+ pressedFillColor?: string;
1103
+ textColor?: string;
1104
+ fontSize?: number;
1105
+ }
1106
+ export interface HudProgressBarEntity extends HudEntityBase {
1107
+ kind: 'progress-bar';
1108
+ /** Current value source (e.g. player's HP). */
1109
+ value: HudNumberSource;
1110
+ /** Max value source (e.g. player's max HP). */
1111
+ max: HudNumberSource;
1112
+ width?: number;
1113
+ height?: number;
1114
+ fillColor?: string;
1115
+ backgroundColor?: string;
1116
+ borderColor?: string | null;
1117
+ borderWidth?: number;
1118
+ /** Border radius for rounded rectangles. Defaults to 0 (sharp corners). */
1119
+ borderRadius?: number;
1120
+ }
1121
+ export interface HudPanelEntity extends HudEntityBase {
1122
+ kind: 'panel';
1123
+ width?: number;
1124
+ height?: number;
1125
+ /**
1126
+ * Optional image asset rendered as the panel background. When the
1127
+ * referenced asset has `ninePatch` metadata, the SDK renders it via
1128
+ * `phaser3-rex-plugins` NinePatch so corners stay fixed at any width/height;
1129
+ * otherwise the image is stretched to the widget's size. When set,
1130
+ * `backgroundColor` / `backgroundAlpha` / `borderColor` / `borderWidth` are
1131
+ * ignored — the asset image carries its own pixels.
1132
+ */
1133
+ backgroundAssetId?: string;
1134
+ backgroundFrame?: number;
1135
+ backgroundRegion?: string;
1136
+ /** Solid fill colour. Falls back to a transparent panel if omitted. */
1137
+ backgroundColor?: string;
1138
+ /** Background alpha (independent of widget-level alpha). */
1139
+ backgroundAlpha?: number;
1140
+ borderColor?: string | null;
1141
+ borderWidth?: number;
1142
+ /** Border radius for the rect. Defaults to 0. */
1143
+ borderRadius?: number;
1144
+ }
1145
+ export type HudEntity = HudTextEntity | HudImageEntity | HudIconButtonEntity | HudProgressBarEntity | HudPanelEntity;
1146
+ export interface HudScene {
1147
+ schemaVersion: number;
1148
+ id: string;
1149
+ name: string;
1150
+ type: 'hud';
1151
+ design?: {
1152
+ /** e.g. `"16:9"`, `"9:16"`. Free-form for now; the editor uses it for the
1153
+ * preview-frame visualization (design 04 §2.1). v1 doesn't enforce it
1154
+ * at runtime — anchors resolve against the actual canvas size. */
1155
+ designAspectRatio?: string;
1156
+ safeArea?: {
1157
+ top: number;
1158
+ right: number;
1159
+ bottom: number;
1160
+ left: number;
1161
+ };
1162
+ };
1163
+ entities: HudEntity[];
1164
+ metadata?: WorldScene['metadata'];
1165
+ }
1166
+ export type SceneFile = WorldScene | HudScene;