@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,321 @@
1
+ /**
2
+ * Wang autotile runtime (slice 6 Phase D — design doc 06 §7).
3
+ *
4
+ * The user paints with a TERRAIN (e.g. "grass"); the SDK picks the actual
5
+ * tile index for each affected cell from the tileset's per-terrain ruleMap
6
+ * based on the cell's bitmask. Editing one cell can cascade to up to 8
7
+ * neighbors because corner vertices (and edge neighbors, in 8-bit mode)
8
+ * are shared.
9
+ *
10
+ * Bit convention for 4-bit corner mode (matches design doc 06 §7.3):
11
+ *
12
+ * bit 0 = BR corner (1)
13
+ * bit 1 = BL corner (2)
14
+ * bit 2 = TR corner (4)
15
+ * bit 3 = TL corner (8)
16
+ *
17
+ * `0b1111 = 15` = fully interior. `0b0000 = 0` = cell is empty.
18
+ *
19
+ * D.2.5.1 storage model (replaces the prior vertex grid from 0.2.122–0.2.124):
20
+ * source of truth is a per-CELL boolean grid of size W×H, stashed on the
21
+ * layer's data manager. The corner state at a vertex is derived as "any
22
+ * of the 4 cells touching this vertex is painted with the terrain" — same
23
+ * semantics as the old vertex grid (each click set 4 vertices = the 4
24
+ * vertices of the clicked cell), expressed at cell granularity instead.
25
+ *
26
+ * Why cell-painted instead of vertex-painted:
27
+ * 1. **Truly authoritative**: vertex state was a derivation of cell-paint
28
+ * anyway. Storing the derivation invited drift (subtract paths on
29
+ * shared vertices got tricky).
30
+ * 2. **Sets up 8-bit Wang (D.2.5.2)**: edge bits = "is the neighbor cell
31
+ * on this side painted", which needs cell-level state. Vertex grid
32
+ * couldn't express this; cell grid does it for free.
33
+ *
34
+ * Reverse-derive on first paint of a fresh layer: walk `layer.data`, mark
35
+ * every cell whose tile index appears in the terrain's ruleMap as painted.
36
+ * Tiles outside the ruleMap leave cells unmarked (assumed not part of
37
+ * this terrain — stamped manually, or from a different terrain). One
38
+ * cell-painted grid per (layer, terrain) — v1 limits a layer to one
39
+ * terrain so we cache a single grid keyed by `terrainId`.
40
+ */
41
+ const CELL_GRID_KEY = 'unboxyAutotileCells';
42
+ // 4-bit Wang corner bits (INCLUSIVE convention).
43
+ // A corner bit is set iff ANY of the (up to 4) cells touching that
44
+ // corner-vertex is painted with the terrain.
45
+ const BIT_BR = 1;
46
+ const BIT_BL = 2;
47
+ const BIT_TR = 4;
48
+ const BIT_TL = 8;
49
+ // 8-bit Wang (STRICT / BLOB convention, design 06 §7.7 / Cr31 "2-edge +
50
+ // 2-corner"). Only PAINTED cells render; bitmask describes the 8 neighbors
51
+ // in terms that match how commercial blob tilesets (Sprout Lands' extended
52
+ // block, Godot 4 "Match Corners and Sides", RPG-Maker autotile) are drawn.
53
+ // Diagonal bits are "effective" — counted only when both adjacent
54
+ // cardinals are also painted (Cr31 geometric constraint).
55
+ //
56
+ // Bit positions (clockwise from N):
57
+ const BIT8_N = 1; // bit 0 — N cardinal neighbor painted
58
+ const BIT8_NE = 2; // bit 1 — NE diagonal effective (N+E+NE all painted)
59
+ const BIT8_E = 4; // bit 2 — E cardinal
60
+ const BIT8_SE = 8; // bit 3 — SE diagonal effective
61
+ const BIT8_S = 16; // bit 4 — S cardinal
62
+ const BIT8_SW = 32; // bit 5 — SW diagonal effective
63
+ const BIT8_W = 64; // bit 6 — W cardinal
64
+ const BIT8_NW = 128; // bit 7 — NW diagonal effective
65
+ function popCount(n) {
66
+ let c = 0;
67
+ while (n) {
68
+ c += n & 1;
69
+ n >>= 1;
70
+ }
71
+ return c;
72
+ }
73
+ function getCell(grid, cx, cy) {
74
+ if (cx < 0 || cy < 0 || cx >= grid.width || cy >= grid.height)
75
+ return 0;
76
+ return grid.data[cy * grid.width + cx];
77
+ }
78
+ function setCell(grid, cx, cy, v) {
79
+ if (cx < 0 || cy < 0 || cx >= grid.width || cy >= grid.height)
80
+ return;
81
+ grid.data[cy * grid.width + cx] = v;
82
+ }
83
+ /**
84
+ * Compute the 4-bit corner mask for cell (cx, cy). A corner bit is set
85
+ * iff ANY of the (up to 4) cells touching that corner is painted.
86
+ *
87
+ * TL vertex at (cx, cy) ← touches cells (cx-1,cy-1) (cx,cy-1) (cx-1,cy) (cx,cy)
88
+ * TR vertex at (cx+1, cy) ← touches cells (cx,cy-1) (cx+1,cy-1) (cx,cy) (cx+1,cy)
89
+ * BL vertex at (cx, cy+1) ← touches cells (cx-1,cy) (cx,cy) (cx-1,cy+1) (cx,cy+1)
90
+ * BR vertex at (cx+1, cy+1) ← touches cells (cx,cy) (cx+1,cy) (cx,cy+1) (cx+1,cy+1)
91
+ *
92
+ * Out-of-bounds cells count as "not painted" (boundary terrain is finite —
93
+ * the world edge is treated like a dirt neighbor).
94
+ */
95
+ function bitmaskAt4(grid, cx, cy) {
96
+ let mask = 0;
97
+ if (getCell(grid, cx - 1, cy - 1) | getCell(grid, cx, cy - 1) |
98
+ getCell(grid, cx - 1, cy) | getCell(grid, cx, cy))
99
+ mask |= BIT_TL;
100
+ if (getCell(grid, cx, cy - 1) | getCell(grid, cx + 1, cy - 1) |
101
+ getCell(grid, cx, cy) | getCell(grid, cx + 1, cy))
102
+ mask |= BIT_TR;
103
+ if (getCell(grid, cx - 1, cy) | getCell(grid, cx, cy) |
104
+ getCell(grid, cx - 1, cy + 1) | getCell(grid, cx, cy + 1))
105
+ mask |= BIT_BL;
106
+ if (getCell(grid, cx, cy) | getCell(grid, cx + 1, cy) |
107
+ getCell(grid, cx, cy + 1) | getCell(grid, cx + 1, cy + 1))
108
+ mask |= BIT_BR;
109
+ return mask;
110
+ }
111
+ /**
112
+ * Compute the 8-bit Wang bitmask for cell `(cx, cy)` under the
113
+ * **STRICT / BLOB** convention (design 06 §7.7 / Cr31 "2-edge + 2-corner").
114
+ *
115
+ * Semantics:
116
+ * - Only PAINTED cells render — unpainted cells return 0 (no tile).
117
+ * This matches commercial tileset conventions (Sprout Lands extended
118
+ * block, Godot 4 "Match Corners and Sides", RPG-Maker autotile).
119
+ * - The bitmask describes THIS cell's 8 NEIGHBORS, not its own corners.
120
+ * - Diagonal bits are "effective" — set only when the diagonal AND both
121
+ * adjacent cardinals are all painted (Cr31 geometric constraint).
122
+ * Reduces 256 raw combinations to 47 valid ones.
123
+ *
124
+ * Visual interpretation: a painted cell looks at its 8 neighbors. If all
125
+ * 8 are painted, it's a full-interior tile (bm 255). If isolated, it's a
126
+ * 1×1 island (bm 0). Boundary cells produce edge/corner bitmasks based
127
+ * on which neighbors are painted.
128
+ *
129
+ * Out-of-bounds neighbors count as "not painted" — the world edge is
130
+ * treated like a dirt neighbor.
131
+ */
132
+ function bitmaskAt8(grid, cx, cy) {
133
+ if (!getCell(grid, cx, cy))
134
+ return 0; // unpainted cell — no tile renders
135
+ let mask = 0;
136
+ const N = getCell(grid, cx, cy - 1);
137
+ const E = getCell(grid, cx + 1, cy);
138
+ const S = getCell(grid, cx, cy + 1);
139
+ const W = getCell(grid, cx - 1, cy);
140
+ if (N)
141
+ mask |= BIT8_N;
142
+ if (E)
143
+ mask |= BIT8_E;
144
+ if (S)
145
+ mask |= BIT8_S;
146
+ if (W)
147
+ mask |= BIT8_W;
148
+ // Effective diagonals: count only when both adjacent cardinals also
149
+ // painted. This is the Cr31 constraint that reduces 256 → 47 valid.
150
+ if (N && E && getCell(grid, cx + 1, cy - 1))
151
+ mask |= BIT8_NE;
152
+ if (S && E && getCell(grid, cx + 1, cy + 1))
153
+ mask |= BIT8_SE;
154
+ if (S && W && getCell(grid, cx - 1, cy + 1))
155
+ mask |= BIT8_SW;
156
+ if (N && W && getCell(grid, cx - 1, cy - 1))
157
+ mask |= BIT8_NW;
158
+ return mask;
159
+ }
160
+ /**
161
+ * Resolve `bitmask` → tile index via the terrain's ruleMap. JSON wire
162
+ * format uses string keys (Jackson `Map<Integer, V>` default); in-memory
163
+ * authoring may use number keys. Try both. `bitmask === 0` is always
164
+ * "empty cell" — we don't consult the rule map.
165
+ */
166
+ function tileForBitmask(terrain, bitmask) {
167
+ if (bitmask === 0)
168
+ return null;
169
+ const rm = terrain.ruleMap;
170
+ const sv = rm[String(bitmask)];
171
+ if (sv != null)
172
+ return sv;
173
+ const nv = rm[bitmask];
174
+ if (nv != null)
175
+ return nv;
176
+ return null;
177
+ }
178
+ /**
179
+ * Build the cell-painted grid by reverse-deriving from currently-painted
180
+ * tiles. A cell is "painted with this terrain" iff its current tile index
181
+ * appears anywhere in the terrain's ruleMap. Tiles outside the ruleMap
182
+ * (manually-stamped tiles, or tiles from a different terrain) leave the
183
+ * cell unmarked — that cell counts as "not in this terrain" for cascade
184
+ * purposes, which is correct: stamped tiles shouldn't influence wang
185
+ * boundary computation for the autotile terrain.
186
+ */
187
+ function buildCellPaintedGrid(layer, terrain) {
188
+ const layerW = layer.tilemap.width;
189
+ const layerH = layer.tilemap.height;
190
+ const grid = {
191
+ data: new Uint8Array(layerW * layerH),
192
+ width: layerW,
193
+ height: layerH,
194
+ terrainId: terrain.id,
195
+ };
196
+ const ruleTiles = new Set();
197
+ const rm = terrain.ruleMap;
198
+ for (const key of Object.keys(rm)) {
199
+ const tileIdx = rm[key];
200
+ if (typeof tileIdx === 'number' && tileIdx >= 0)
201
+ ruleTiles.add(tileIdx);
202
+ }
203
+ layer.forEachTile((tile) => {
204
+ if (tile.index < 0)
205
+ return;
206
+ if (!ruleTiles.has(tile.index))
207
+ return;
208
+ setCell(grid, tile.x, tile.y, 1);
209
+ });
210
+ return grid;
211
+ }
212
+ function getOrBuildCellGrid(layer, terrain) {
213
+ const cached = layer.getData(CELL_GRID_KEY);
214
+ if (cached && cached.terrainId === terrain.id)
215
+ return cached;
216
+ const fresh = buildCellPaintedGrid(layer, terrain);
217
+ layer.setData(CELL_GRID_KEY, fresh);
218
+ return fresh;
219
+ }
220
+ /**
221
+ * Resolve a terrain by id from a tileset asset's autotile config. Returns
222
+ * null when the asset lacks autotile metadata or no terrain matches —
223
+ * callers typically warn + skip in that case.
224
+ */
225
+ export function findTerrain(asset, terrainId) {
226
+ const terrains = asset?.tileset?.autotile?.terrains;
227
+ if (!terrains)
228
+ return null;
229
+ return terrains.find((t) => t.id === terrainId) ?? null;
230
+ }
231
+ /**
232
+ * Resolve the autotile kind (`'wang-4bit'` | `'wang-8bit'`) on a tileset
233
+ * asset. Defaults to `'wang-4bit'` for legacy assets where `autotile.kind`
234
+ * is missing (shouldn't happen with the validating backend parser, but
235
+ * defensive for hand-authored manifests).
236
+ */
237
+ export function getAutotileKind(asset) {
238
+ const k = asset?.tileset?.autotile?.kind;
239
+ return k === 'wang-8bit' ? 'wang-8bit' : 'wang-4bit';
240
+ }
241
+ /**
242
+ * Invalidate the cached autotile state on a layer. Next autotile op
243
+ * rebuilds from the layer's current `data`. Call after structural
244
+ * mutations the grid model can't track (resize, removeLayer, tileset
245
+ * swap, terrain rule edits — `assetUpdate` does this automatically).
246
+ *
247
+ * Kept the old `invalidateAutotileVertices` name as an alias for
248
+ * back-compat with the 0.2.122–0.2.124 export surface — internal SDK
249
+ * code uses the new name, external consumers (none today) keep working.
250
+ */
251
+ export function invalidateAutotileCells(layer) {
252
+ layer.setData(CELL_GRID_KEY, undefined);
253
+ }
254
+ /** @deprecated Renamed to `invalidateAutotileCells` (D.2.5.1). */
255
+ export const invalidateAutotileVertices = invalidateAutotileCells;
256
+ /**
257
+ * Apply a Wang autotile paint (or erase) at cell `(cellX, cellY)`.
258
+ *
259
+ * Paint mode (`mode = 'paint'`): the cell's painted-flag is set; the 3×3
260
+ * cell window around the click is recomputed. Each cell's new bitmask
261
+ * drives the tile index from `terrain.ruleMap`. Bitmask 0 (no corners) →
262
+ * cell becomes empty. Missing ruleMap entries are treated the same way
263
+ * (no tile defined → cell empty).
264
+ *
265
+ * Erase mode (`mode = 'erase'`): same flow with the cell's painted-flag
266
+ * cleared. Neighbors whose other corners are still terrain stay painted;
267
+ * cells whose bitmask drops to 0 become empty.
268
+ *
269
+ * Returns the affected cells with previous + new tile indices (one entry
270
+ * per cell in the 3×3 window that lies inside the layer's bounds, even
271
+ * if the index didn't change — the caller may dedup downstream). Cells
272
+ * outside the layer's bounds are silently skipped.
273
+ *
274
+ * Cheap: ~9 cell mutations + a Map lookup per cell. Cell-painted grid is
275
+ * stashed on the layer's data manager so subsequent calls reuse it
276
+ * without rebuilding from `layer.data`.
277
+ */
278
+ export function applyAutotile(layer, cellX, cellY, terrain, mode, kind = 'wang-4bit') {
279
+ const layerW = layer.tilemap.width;
280
+ const layerH = layer.tilemap.height;
281
+ if (cellX < 0 || cellY < 0 || cellX >= layerW || cellY >= layerH) {
282
+ return [];
283
+ }
284
+ const grid = getOrBuildCellGrid(layer, terrain);
285
+ setCell(grid, cellX, cellY, mode === 'paint' ? 1 : 0);
286
+ // D.2.5.2 — pick bitmask function by kind. 8-bit cascade range is still
287
+ // 3×3 (painting cell (x,y) only changes side-bits of immediate
288
+ // cardinal neighbors, all within the 3×3 window).
289
+ const computeBitmask = kind === 'wang-8bit' ? bitmaskAt8 : bitmaskAt4;
290
+ const affected = [];
291
+ for (let dy = -1; dy <= 1; dy++) {
292
+ for (let dx = -1; dx <= 1; dx++) {
293
+ const x = cellX + dx;
294
+ const y = cellY + dy;
295
+ if (x < 0 || y < 0 || x >= layerW || y >= layerH)
296
+ continue;
297
+ const prevTile = layer.getTileAt(x, y, true);
298
+ const prevIndex = prevTile && prevTile.index >= 0 ? prevTile.index : -1;
299
+ const bitmask = computeBitmask(grid, x, y);
300
+ const newIndex = tileForBitmask(terrain, bitmask);
301
+ if (newIndex == null) {
302
+ if (prevIndex >= 0)
303
+ layer.removeTileAt(x, y);
304
+ affected.push({ x, y, index: -1, prevIndex });
305
+ }
306
+ else if (newIndex !== prevIndex) {
307
+ layer.putTileAt(newIndex, x, y);
308
+ affected.push({ x, y, index: newIndex, prevIndex });
309
+ }
310
+ else {
311
+ affected.push({ x, y, index: newIndex, prevIndex });
312
+ }
313
+ }
314
+ }
315
+ // popCount is reserved for D.2.5.2 (8-bit Wang) where it'll be used to
316
+ // pick the "most-corners-filled" tile when multiple ruleMap entries
317
+ // resolve to the same valid bitmask. Keep the helper around so the
318
+ // diff to D.2.5.2 stays narrow.
319
+ void popCount;
320
+ return affected;
321
+ }
@@ -0,0 +1,53 @@
1
+ import Phaser from 'phaser';
2
+ /**
3
+ * Render-script module shape — slice 3.5.
4
+ *
5
+ * Spec: `umicat-design/features/visual-editor/02-render-scripts.md`.
6
+ *
7
+ * A `code-rendered` entity references a render script by path. The script's
8
+ * `render` is a **pure function** that draws into a Phaser Graphics object
9
+ * given a parameters object. The function:
10
+ *
11
+ * - Calls `g.clear()` first (idempotent re-renders).
12
+ * - Has no time/random dependency (motion is in tween system, not here).
13
+ * - Holds no module state.
14
+ *
15
+ * Optional exports the editor / SDK uses if present:
16
+ *
17
+ * - `defaultParams` — used as a starting point for new entities.
18
+ * - `paramSchema` — drives the Inspector's auto-generated UI (slice 4+).
19
+ *
20
+ * The script registry is built by the template (`import.meta.glob`), passed
21
+ * to `createUmicatGame({ renderScripts })`, and resolved by path string at
22
+ * spawn time. Path strings in scene files match the registry keys exactly:
23
+ * usually `src/visuals/<name>.ts`.
24
+ */
25
+ export interface RenderScriptModule<P extends Record<string, unknown> = Record<string, unknown>> {
26
+ render: (g: Phaser.GameObjects.Graphics, params: P) => void;
27
+ defaultParams?: P;
28
+ paramSchema?: unknown;
29
+ }
30
+ /**
31
+ * Stash a render-script registry on the Phaser game so `loadWorldScene` /
32
+ * `EditorBridge.applyEdit` can find it without explicit plumbing.
33
+ *
34
+ * The map's keys are absolute paths matching what scene files reference
35
+ * (e.g. `src/visuals/coin.ts`). Templates that build via `import.meta.glob`
36
+ * commonly produce relative keys like `./visuals/coin.ts`; the resolver
37
+ * normalises trailing slashes / leading `./` so both shapes work.
38
+ */
39
+ export declare function setRenderScriptRegistry(game: Phaser.Game, scripts: Record<string, RenderScriptModule>): void;
40
+ export declare function getRenderScriptRegistry(game: Phaser.Game): Record<string, RenderScriptModule> | undefined;
41
+ /**
42
+ * Returns the `render` function for a script path, or undefined if no
43
+ * matching module is registered. The matcher is forgiving:
44
+ *
45
+ * - exact match (`src/visuals/coin.ts`)
46
+ * - leading `./` stripped (`./visuals/coin.ts` matches `src/visuals/coin.ts`)
47
+ * - filename-only fallback so renames in the registry that drop the
48
+ * `src/` prefix still resolve.
49
+ *
50
+ * The forgiveness lets templates use whatever import.meta.glob shape they
51
+ * find natural without forcing a one-true-key convention.
52
+ */
53
+ export declare function resolveRenderScript(game: Phaser.Game, scriptPath: string): ((g: Phaser.GameObjects.Graphics, params: Record<string, unknown>) => void) | undefined;
@@ -0,0 +1,67 @@
1
+ const REGISTRY_KEY = '__unboxyRenderScriptRegistry';
2
+ /**
3
+ * Stash a render-script registry on the Phaser game so `loadWorldScene` /
4
+ * `EditorBridge.applyEdit` can find it without explicit plumbing.
5
+ *
6
+ * The map's keys are absolute paths matching what scene files reference
7
+ * (e.g. `src/visuals/coin.ts`). Templates that build via `import.meta.glob`
8
+ * commonly produce relative keys like `./visuals/coin.ts`; the resolver
9
+ * normalises trailing slashes / leading `./` so both shapes work.
10
+ */
11
+ export function setRenderScriptRegistry(game, scripts) {
12
+ const bag = game;
13
+ bag[REGISTRY_KEY] = scripts;
14
+ }
15
+ export function getRenderScriptRegistry(game) {
16
+ const bag = game;
17
+ return bag[REGISTRY_KEY];
18
+ }
19
+ /**
20
+ * Returns the `render` function for a script path, or undefined if no
21
+ * matching module is registered. The matcher is forgiving:
22
+ *
23
+ * - exact match (`src/visuals/coin.ts`)
24
+ * - leading `./` stripped (`./visuals/coin.ts` matches `src/visuals/coin.ts`)
25
+ * - filename-only fallback so renames in the registry that drop the
26
+ * `src/` prefix still resolve.
27
+ *
28
+ * The forgiveness lets templates use whatever import.meta.glob shape they
29
+ * find natural without forcing a one-true-key convention.
30
+ */
31
+ export function resolveRenderScript(game, scriptPath) {
32
+ const registry = getRenderScriptRegistry(game);
33
+ if (!registry)
34
+ return undefined;
35
+ // Direct hit.
36
+ if (registry[scriptPath])
37
+ return registry[scriptPath].render;
38
+ // Try common normalisations.
39
+ const candidates = candidateKeys(scriptPath);
40
+ for (const c of candidates) {
41
+ if (registry[c])
42
+ return registry[c].render;
43
+ }
44
+ // Filename-only fallback.
45
+ const filename = scriptPath.split('/').pop();
46
+ if (filename) {
47
+ for (const key of Object.keys(registry)) {
48
+ if (key.endsWith('/' + filename) || key === filename) {
49
+ return registry[key].render;
50
+ }
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+ function candidateKeys(scriptPath) {
56
+ const out = [];
57
+ const trimmed = scriptPath.replace(/^\.\//, '');
58
+ out.push(trimmed);
59
+ // src/visuals/coin.ts → ./visuals/coin.ts
60
+ if (trimmed.startsWith('src/'))
61
+ out.push('./' + trimmed.slice(4));
62
+ // visuals/coin.ts → src/visuals/coin.ts
63
+ if (!trimmed.startsWith('src/') && !trimmed.startsWith('./')) {
64
+ out.push('src/' + trimmed);
65
+ }
66
+ return out;
67
+ }
@@ -0,0 +1,201 @@
1
+ import Phaser from 'phaser';
2
+ import { AssetRecord, TileMetadata, WorldEntity } from './types.js';
3
+ import { EntityRegistry } from './EntityRegistry.js';
4
+ /**
5
+ * Resolves an asset id to its AssetRecord, throwing a clear error if the
6
+ * scene file references something the manifest doesn't know about. We
7
+ * surface this as an error (not a silent miss) because a missing asset
8
+ * means either the manifest is stale or the scene file was hand-edited
9
+ * — both worth catching at runtime, not skipping silently.
10
+ */
11
+ export type AssetResolver = (assetId: string) => AssetRecord;
12
+ /** Optional hook for `code-rendered` visuals. v1 throws if not provided. */
13
+ export type RenderScriptResolver = (scriptPath: string) => ((g: Phaser.GameObjects.Graphics, params: Record<string, unknown>) => void) | undefined;
14
+ export interface SpawnContext {
15
+ scene: Phaser.Scene;
16
+ registry: EntityRegistry;
17
+ resolveAsset: AssetResolver;
18
+ resolveRenderScript?: RenderScriptResolver;
19
+ }
20
+ /**
21
+ * Spawn one entity into the scene and register it. Returns the created
22
+ * GameObject so callers (notably `spawnEntity` itself, recursing on a
23
+ * group's children) can attach it to a parent container.
24
+ */
25
+ export declare function spawnEntity(ctx: SpawnContext, entity: WorldEntity): Phaser.GameObjects.GameObject;
26
+ /**
27
+ * Read the auto-tracker's recorded draw extent and set editor hit-area
28
+ * data on the GameObject. Falls back to declared `visual.width/height`
29
+ * (centered on transform) when nothing was tracked (script didn't call
30
+ * any tracked methods, or called them through unwrapped paths).
31
+ *
32
+ * Exported so EditorBridge can call it after a re-render too —
33
+ * applyCodeRenderedParamsPatch reads new params, calls render again,
34
+ * then this updates the hit area to the new draw extent.
35
+ */
36
+ export declare function applyTrackedHitArea(g: Phaser.GameObjects.Graphics, fallbackW: number, fallbackH: number): void;
37
+ /**
38
+ * Slice 6 Phase C — wire per-tile metadata onto a Phaser tileset + layer.
39
+ *
40
+ * Sources `asset.tileset.tiles` (sparse map keyed by tile index) and:
41
+ * 1. Replaces `tileset.tileProperties` so any FUTURE `putTileAt` call
42
+ * auto-stamps the new tile's `properties` from the lookup. Phaser
43
+ * reads `tileset.tileProperties[index]` at tile-creation time.
44
+ * 2. Walks existing tiles via `layer.forEachTile` and refreshes each
45
+ * tile's `properties` — needed for tiles that were already painted
46
+ * before this fn ran (initial scene load, or `assetUpdate` replay).
47
+ * 3. Calls `layer.setCollisionByProperty({ solid: true })` so any tile
48
+ * whose `properties.solid === true` participates in Arcade collision.
49
+ * Idempotent + safe to call when no tile is solid (no-op).
50
+ *
51
+ * Exported because `EditorBridge.applyTilemapStructureOp` (addLayer) and
52
+ * `EditorBridge.handleAssetUpdate` both need to re-arm collision wiring
53
+ * without going through the full scene load. SDK 0.2.115+.
54
+ */
55
+ export declare function applyTilesetTileMetadata(tileset: Phaser.Tilemaps.Tileset, layer: Phaser.Tilemaps.TilemapLayer, asset: AssetRecord): void;
56
+ /**
57
+ * Arm tile animations on this layer — Phase F (slice 6 — design doc 06 §8).
58
+ *
59
+ * Reads `asset.tileset.animations[]`, finds every painted cell whose source
60
+ * index matches an animation's `rootTileIndex`, installs a per-frame UPDATE
61
+ * listener that swaps each cell's `tile.index` to the current frame.
62
+ * All cells with the same root animate in lockstep (Sprout Lands water
63
+ * lake-cells flow together).
64
+ *
65
+ * Idempotent — re-calling tears down the prior listener and re-scans. Used
66
+ * by:
67
+ * - `createTilemap` at scene boot (initial arm)
68
+ * - `EditorBridge.handleAssetUpdate` (when Animation Editor saves)
69
+ * - `EditorBridge.applyTilemapStructureOp addLayer` (new layer arm)
70
+ * - any subsequent paint op (host's `handleEditTilemap` re-runs metadata)
71
+ *
72
+ * Edit-mode caveat: scenes are `setActive(false)` in edit mode → scene
73
+ * UPDATE doesn't fire → the swap handler stays dormant → animated cells
74
+ * stay on whatever frame they were on when edit was entered. To keep edit
75
+ * mode showing the static "data" view, `EditorBridge.enterEdit` calls
76
+ * `resetTilesetAnimationsToRoot` which walks all animated cells + sets
77
+ * them back to root. exitEdit → next UPDATE tick re-applies current frame.
78
+ *
79
+ * Save-path safety: the iframe's mutated `tile.index` per frame doesn't
80
+ * leak into the host's draft state (home-ui keeps its own copy in
81
+ * `useEditorDraft`'s baseline + commands). Mid-animation Cmd+S saves the
82
+ * root indices, not the displayed frame.
83
+ *
84
+ * Phaser API note: Phaser 3 parses Tiled's `tile.animation` field into
85
+ * `tileset.tileData[id].animation` but its TilemapLayer renderer does NOT
86
+ * consume it. The community `phaser-animated-tiles` plugin bridges that
87
+ * gap but has been bit-rotted since 3.80. This is a 50-line custom impl
88
+ * that does the swap directly — same algorithm, no external dep.
89
+ */
90
+ export declare function applyTilesetAnimations(layer: Phaser.Tilemaps.TilemapLayer, asset: AssetRecord): void;
91
+ /**
92
+ * Reset every animated cell on a tilemap container to its root tile index.
93
+ * Called by `EditorBridge.enterEdit` so animated cells show the static
94
+ * authored frame in the editor, not whatever was currently visible at the
95
+ * moment the user toggled into edit mode. exitEdit doesn't need a paired
96
+ * call — the next UPDATE tick after scenes resume swaps each cell back to
97
+ * its current animation frame.
98
+ */
99
+ export declare function resetTilesetAnimationsToRoot(container: Phaser.GameObjects.Container): void;
100
+ /**
101
+ * Wire collision between `target` (a sprite, group, or anything with a
102
+ * physics body) and all collision surfaces of a tilemap entity — both
103
+ * the layer's native cell-rect collision AND any sub-tile static bodies
104
+ * authored via the Tile Metadata Editor's collision-shape mode.
105
+ *
106
+ * The one-call alternative to:
107
+ * ```ts
108
+ * scene.physics.add.collider(player, layer);
109
+ * scene.physics.add.collider(player, layer.getData('unboxySubTileStaticGroup'));
110
+ * ```
111
+ *
112
+ * Behavior code in a typical game:
113
+ * ```ts
114
+ * import { addTilemapCollider } from '@umicat/phaser-sdk';
115
+ * create() {
116
+ * // ... spawn player ...
117
+ * addTilemapCollider(this, 'world', this.player);
118
+ * }
119
+ * ```
120
+ *
121
+ * Returns the created Collider instances so callers can `.destroy()` them
122
+ * if needed (e.g. on level transition).
123
+ */
124
+ export declare function addTilemapCollider(scene: Phaser.Scene, entityId: string, target: Phaser.Types.Physics.Arcade.ArcadeColliderType, callback?: Phaser.Types.Physics.Arcade.ArcadePhysicsCallback): Phaser.Physics.Arcade.Collider[];
125
+ /**
126
+ * Accepts `'#rrggbb'`, `'rrggbb'`, or `'0xrrggbb'` and returns a number
127
+ * suitable for Phaser's color APIs.
128
+ */
129
+ export declare function parseColor(input: string): number;
130
+ /**
131
+ * Apply the asset's `hitbox` metadata to a sprite's physics body.
132
+ *
133
+ * Called automatically at spawn from `createSprite`; ALSO callable by
134
+ * behavior code that attaches a body later (e.g. the agent's physics skill
135
+ * does `scene.physics.add.existing(sprite); applyAssetHitbox(sprite, asset);`).
136
+ *
137
+ * Semantics:
138
+ * - **No-op (silent)** when `asset.hitbox` is unset — that's the chat-only
139
+ * path (primitives, AI-gen images without vision metadata). Workflow A
140
+ * in design doc 09 §4.4 is unaffected.
141
+ * - **Dev-warn (NOT throw)** when called on a sprite without a physics body.
142
+ * The most likely agent mistake is calling this BEFORE
143
+ * `scene.physics.add.existing(sprite)`; the warning surfaces in the
144
+ * iframe console for the agent's post-turn diagnostic loop.
145
+ * - **Per-frame variant** installs an `ANIMATION_UPDATE` listener that
146
+ * swaps `body.setSize/setOffset` on every frame change during anim
147
+ * playback. Listener is idempotent — calling `applyAssetHitbox` twice
148
+ * on the same sprite tears down the prior listener before installing a
149
+ * new one (handles asset hot-reload + Inspector live-edits).
150
+ *
151
+ * Caveat: per-frame swap fires on `Phaser.Animations.Events.ANIMATION_UPDATE`
152
+ * only, which means manual `sprite.setFrame(idx)` calls outside of animation
153
+ * playback do NOT swap the body. v1 accepts this — combat sheets are
154
+ * animation-driven. A `setFrame` wrapper is a v2 candidate.
155
+ */
156
+ export declare function applyAssetHitbox(sprite: Phaser.GameObjects.Sprite, asset: AssetRecord): void;
157
+ /**
158
+ * Result of a `getTilemapAt` query. `metadata` holds the authored per-tile
159
+ * metadata for the matching tile (collision flags, damage, terrain tag,
160
+ * etc.); empty object when the tile is painted but has no metadata.
161
+ */
162
+ export interface TilemapHit {
163
+ /** Which layer of the tilemap entity holds the tile. */
164
+ layerId: string;
165
+ /** The tile's 0-based row-major index in the tileset image. */
166
+ tileIndex: number;
167
+ /** Per-tile metadata (collision + gameplay fields). Empty if unset. */
168
+ metadata: TileMetadata;
169
+ /** Tile column inside the layer (0-based). */
170
+ tileX: number;
171
+ /** Tile row inside the layer (0-based). */
172
+ tileY: number;
173
+ }
174
+ /**
175
+ * Look up the painted tile at a world coordinate inside a tilemap entity.
176
+ *
177
+ * Slice 6 Phase C (design doc 06 §5.2). Returns null when:
178
+ * - the entity doesn't exist / isn't a tilemap;
179
+ * - no layer in the tilemap has a painted (non-empty) tile at that
180
+ * world coord;
181
+ * - the world coord is outside every layer's bounds.
182
+ *
183
+ * Layer scan order (when `options.layerId` is omitted) is top-down — the
184
+ * layer with the highest `z` is queried first, matching the visual stack.
185
+ * Pass `options.layerId` to scope to one layer (e.g. for a "ground type"
186
+ * lookup on the floor layer specifically).
187
+ *
188
+ * Behavior-code use:
189
+ * ```ts
190
+ * const hit = getTilemapAt(this, 'world', player.x, player.y);
191
+ * if (hit?.metadata.damage) player.hp -= hit.metadata.damage * dt;
192
+ * if (hit?.metadata.movement === 'swim') player.speed *= 0.5;
193
+ * ```
194
+ *
195
+ * Per-frame perf: each layer query is a constant-time `worldToTileXY` +
196
+ * `getTileAt` Phaser call. Safe for `update()` loops at typical layer
197
+ * counts (1–4 layers per tilemap).
198
+ */
199
+ export declare function getTilemapAt(scene: Phaser.Scene, entityId: string, worldX: number, worldY: number, options?: {
200
+ layerId?: string;
201
+ }): TilemapHit | null;