@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.
- package/SDK-GUIDE.md +1726 -0
- package/dist/core/Transport.d.ts +28 -0
- package/dist/core/Transport.js +7 -0
- package/dist/core/Umicat.d.ts +45 -0
- package/dist/core/Umicat.js +60 -0
- package/dist/core/UmicatGame.d.ts +43 -0
- package/dist/core/UmicatGame.js +64 -0
- package/dist/core/UmicatScene.d.ts +19 -0
- package/dist/core/UmicatScene.js +38 -0
- package/dist/core/transports/LocalStorageTransport.d.ts +22 -0
- package/dist/core/transports/LocalStorageTransport.js +78 -0
- package/dist/core/transports/PostMessageTransport.d.ts +28 -0
- package/dist/core/transports/PostMessageTransport.js +105 -0
- package/dist/editor/EditorBridge.d.ts +114 -0
- package/dist/editor/EditorBridge.js +2608 -0
- package/dist/editor/EditorOverlayScene.d.ts +333 -0
- package/dist/editor/EditorOverlayScene.js +1896 -0
- package/dist/editor/EditorState.d.ts +251 -0
- package/dist/editor/EditorState.js +197 -0
- package/dist/gamedata/GameDataModule.d.ts +45 -0
- package/dist/gamedata/GameDataModule.js +59 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/orientation.d.ts +5 -0
- package/dist/orientation.js +4 -0
- package/dist/protocol.d.ts +807 -0
- package/dist/protocol.js +3 -0
- package/dist/realtime/RealtimeModule.d.ts +93 -0
- package/dist/realtime/RealtimeModule.js +115 -0
- package/dist/realtime/UmicatRoom.d.ts +197 -0
- package/dist/realtime/UmicatRoom.js +353 -0
- package/dist/recording/RecordingManager.d.ts +11 -0
- package/dist/recording/RecordingManager.js +59 -0
- package/dist/saves/SavesModule.d.ts +23 -0
- package/dist/saves/SavesModule.js +37 -0
- package/dist/scene/EditorMode.d.ts +17 -0
- package/dist/scene/EditorMode.js +22 -0
- package/dist/scene/EntityRegistry.d.ts +39 -0
- package/dist/scene/EntityRegistry.js +103 -0
- package/dist/scene/GameConfig.d.ts +60 -0
- package/dist/scene/GameConfig.js +50 -0
- package/dist/scene/HudRuntime.d.ts +131 -0
- package/dist/scene/HudRuntime.js +1224 -0
- package/dist/scene/Prefabs.d.ts +92 -0
- package/dist/scene/Prefabs.js +175 -0
- package/dist/scene/Rules.d.ts +73 -0
- package/dist/scene/Rules.js +164 -0
- package/dist/scene/SceneLoader.d.ts +118 -0
- package/dist/scene/SceneLoader.js +615 -0
- package/dist/scene/Waves.d.ts +85 -0
- package/dist/scene/Waves.js +365 -0
- package/dist/scene/autotile.d.ts +103 -0
- package/dist/scene/autotile.js +321 -0
- package/dist/scene/renderScripts.d.ts +53 -0
- package/dist/scene/renderScripts.js +67 -0
- package/dist/scene/spawnEntity.d.ts +201 -0
- package/dist/scene/spawnEntity.js +1326 -0
- package/dist/scene/types.d.ts +1166 -0
- package/dist/scene/types.js +34 -0
- package/dist/screenshot/ScreenshotManager.d.ts +14 -0
- package/dist/screenshot/ScreenshotManager.js +33 -0
- 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;
|