@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,85 @@
1
+ /**
2
+ * Wave schedules — time-ordered spawn sequences as data.
3
+ *
4
+ * Slice 11 (Phase B.4, 2026-05-12). Each schedule lives in
5
+ * `public/waves/<id>.json` as a `WaveScheduleRecord`. Behavior code calls
6
+ * `runWaveSchedule(scene, id, callbacks)` to drive enemy waves / formations /
7
+ * staged spawns without hand-implementing wave loops.
8
+ *
9
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §7
10
+ *
11
+ * Public API:
12
+ * - `loadWaveSchedule(scene, id)` — async fetch via Phaser's loader. Cached.
13
+ * - `runWaveSchedule(scene, id, callbacks)` — start a schedule; returns a
14
+ * `WaveController` (pause / resume / skipTo / stop).
15
+ *
16
+ * Wave end conditions:
17
+ * - `'allDead'` (default) — waits until every spawned instance from this
18
+ * wave has been destroyed.
19
+ * - `{ type: 'time', ms }` — fixed duration from wave start.
20
+ * - `{ type: 'count', killed }` — wait until N spawned instances destroyed.
21
+ */
22
+ import Phaser from 'phaser';
23
+ import { WaveRecord, WaveScheduleRecord } from './types.js';
24
+ export interface SpawnInstance {
25
+ /** Prefab id requested by the schedule. */
26
+ prefabId: string;
27
+ /** World coordinate where the spawn should land. */
28
+ x: number;
29
+ y: number;
30
+ /** Wave id the spawn belongs to (for logging / per-wave logic). */
31
+ waveId: string;
32
+ /** Zero-based index of the wave within the schedule. */
33
+ waveIndex: number;
34
+ /** Per-spawn overrides from the schedule (deep-merged into the prefab). */
35
+ overrides?: Record<string, unknown>;
36
+ }
37
+ export interface WaveCallbacks {
38
+ /**
39
+ * Called for every spawn. Caller spawns the instance (typically via
40
+ * `spawnPrefab`) and returns the GameObject so the controller can
41
+ * track it for `endCondition: 'allDead'` / `{ type: 'count', killed }`.
42
+ * Return `void` if you don't want lifecycle tracking (rare).
43
+ */
44
+ onSpawn: (instance: SpawnInstance) => Phaser.GameObjects.GameObject | void;
45
+ onWaveStart?: (waveIndex: number, wave: WaveRecord) => void;
46
+ onWaveEnd?: (waveIndex: number, wave: WaveRecord) => void;
47
+ onScheduleEnd?: () => void;
48
+ }
49
+ export interface WaveController {
50
+ /** Pause all pending timers + advance checks. Idempotent. */
51
+ pause(): void;
52
+ /** Resume after pause. Idempotent if not paused. */
53
+ resume(): void;
54
+ /** Jump to the wave at `index` (0-based). Cancels in-flight spawns of current wave. */
55
+ skipTo(index: number): void;
56
+ /** Stop the schedule. Fires `onScheduleEnd`. Controller is dead after this. */
57
+ stop(): void;
58
+ /** Zero-based index of the wave currently spawning / waiting on. */
59
+ currentWaveIndex(): number;
60
+ /** `true` after the schedule fully completes (no more waves, no loop). */
61
+ isComplete(): boolean;
62
+ }
63
+ /**
64
+ * Fetch a wave schedule via Phaser's JSON loader. Cached per scene cache
65
+ * — re-calling with the same id returns the cached record immediately.
66
+ */
67
+ export declare function loadWaveSchedule(scene: Phaser.Scene, scheduleId: string): Promise<WaveScheduleRecord>;
68
+ /**
69
+ * Start a wave schedule. Returns a `WaveController` immediately; the
70
+ * schedule loads asynchronously and starts as soon as the file is in
71
+ * cache. Use this in `GameScene.create()`:
72
+ *
73
+ * ```ts
74
+ * const wc = runWaveSchedule(this, 'stage-1', {
75
+ * onSpawn: ({ prefabId, x, y, overrides }) => {
76
+ * const go = spawnPrefab(this, prefabId, x, y, overrides);
77
+ * this.enemies.add(go);
78
+ * return go;
79
+ * },
80
+ * onWaveStart: (i) => this.events.emit('hudWave', i + 1),
81
+ * onScheduleEnd: () => this.scene.start('VictoryScene'),
82
+ * });
83
+ * ```
84
+ */
85
+ export declare function runWaveSchedule(scene: Phaser.Scene, scheduleId: string, callbacks: WaveCallbacks): WaveController;
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Wave schedules — time-ordered spawn sequences as data.
3
+ *
4
+ * Slice 11 (Phase B.4, 2026-05-12). Each schedule lives in
5
+ * `public/waves/<id>.json` as a `WaveScheduleRecord`. Behavior code calls
6
+ * `runWaveSchedule(scene, id, callbacks)` to drive enemy waves / formations /
7
+ * staged spawns without hand-implementing wave loops.
8
+ *
9
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §7
10
+ *
11
+ * Public API:
12
+ * - `loadWaveSchedule(scene, id)` — async fetch via Phaser's loader. Cached.
13
+ * - `runWaveSchedule(scene, id, callbacks)` — start a schedule; returns a
14
+ * `WaveController` (pause / resume / skipTo / stop).
15
+ *
16
+ * Wave end conditions:
17
+ * - `'allDead'` (default) — waits until every spawned instance from this
18
+ * wave has been destroyed.
19
+ * - `{ type: 'time', ms }` — fixed duration from wave start.
20
+ * - `{ type: 'count', killed }` — wait until N spawned instances destroyed.
21
+ */
22
+ import Phaser from 'phaser';
23
+ const WAVE_CACHE_PREFIX = 'umicat:wave:';
24
+ /**
25
+ * Fetch a wave schedule via Phaser's JSON loader. Cached per scene cache
26
+ * — re-calling with the same id returns the cached record immediately.
27
+ */
28
+ export function loadWaveSchedule(scene, scheduleId) {
29
+ const cacheKey = `${WAVE_CACHE_PREFIX}${scheduleId}`;
30
+ if (scene.cache.json.exists(cacheKey)) {
31
+ return Promise.resolve(scene.cache.json.get(cacheKey));
32
+ }
33
+ return new Promise((resolve, reject) => {
34
+ scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
35
+ const record = scene.cache.json.get(cacheKey);
36
+ if (!record) {
37
+ reject(new Error(`[umicat/waves] schedule '${scheduleId}' loaded but cache is empty`));
38
+ return;
39
+ }
40
+ resolve(record);
41
+ });
42
+ scene.load.once(Phaser.Loader.Events.FILE_LOAD_ERROR, (file) => {
43
+ if (file.key === cacheKey) {
44
+ reject(new Error(`[umicat/waves] failed to load wave schedule '${scheduleId}' — check public/waves/${scheduleId}.json exists`));
45
+ }
46
+ });
47
+ scene.load.json(cacheKey, `waves/${scheduleId}.json`);
48
+ scene.load.start();
49
+ });
50
+ }
51
+ /**
52
+ * Start a wave schedule. Returns a `WaveController` immediately; the
53
+ * schedule loads asynchronously and starts as soon as the file is in
54
+ * cache. Use this in `GameScene.create()`:
55
+ *
56
+ * ```ts
57
+ * const wc = runWaveSchedule(this, 'stage-1', {
58
+ * onSpawn: ({ prefabId, x, y, overrides }) => {
59
+ * const go = spawnPrefab(this, prefabId, x, y, overrides);
60
+ * this.enemies.add(go);
61
+ * return go;
62
+ * },
63
+ * onWaveStart: (i) => this.events.emit('hudWave', i + 1),
64
+ * onScheduleEnd: () => this.scene.start('VictoryScene'),
65
+ * });
66
+ * ```
67
+ */
68
+ export function runWaveSchedule(scene, scheduleId, callbacks) {
69
+ const controller = new WaveControllerImpl(scene, callbacks);
70
+ loadWaveSchedule(scene, scheduleId)
71
+ .then((record) => controller.start(record))
72
+ .catch((err) => {
73
+ // eslint-disable-next-line no-console
74
+ console.warn('[umicat/waves]', err);
75
+ });
76
+ return controller;
77
+ }
78
+ // --- Internals ------------------------------------------------------------
79
+ class WaveControllerImpl {
80
+ constructor(scene, callbacks) {
81
+ this.scene = scene;
82
+ this.callbacks = callbacks;
83
+ this.record = null;
84
+ this.waveIndex = 0;
85
+ this.paused = false;
86
+ this.stopped = false;
87
+ this.complete = false;
88
+ /** Active TimerEvents we need to cancel on pause / skip / stop. */
89
+ this.timers = new Set();
90
+ /** GameObjects spawned in the current wave — tracked for endCondition. */
91
+ this.aliveInWave = new Set();
92
+ /** Count of GameObjects destroyed in the current wave (for `count` end condition). */
93
+ this.killedInWave = 0;
94
+ /** Wave-start timestamp (for `time` end condition). */
95
+ this.waveStartedAtMs = 0;
96
+ /** Polling timer for endCondition checks. */
97
+ this.endCheckTimer = null;
98
+ // Stop the schedule if the scene shuts down — frees all timers.
99
+ scene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => this.stop());
100
+ }
101
+ start(record) {
102
+ if (this.stopped)
103
+ return;
104
+ this.record = record;
105
+ this.runWave(0);
106
+ }
107
+ currentWaveIndex() {
108
+ return this.waveIndex;
109
+ }
110
+ isComplete() {
111
+ return this.complete;
112
+ }
113
+ pause() {
114
+ if (this.paused || this.stopped)
115
+ return;
116
+ this.paused = true;
117
+ this.scene.time.paused = false; // we manage our own timers; don't pause the whole scene clock
118
+ for (const t of this.timers)
119
+ t.paused = true;
120
+ if (this.endCheckTimer)
121
+ this.endCheckTimer.paused = true;
122
+ }
123
+ resume() {
124
+ if (!this.paused || this.stopped)
125
+ return;
126
+ this.paused = false;
127
+ for (const t of this.timers)
128
+ t.paused = false;
129
+ if (this.endCheckTimer)
130
+ this.endCheckTimer.paused = false;
131
+ }
132
+ skipTo(index) {
133
+ if (this.stopped || !this.record)
134
+ return;
135
+ this.cancelTimers();
136
+ this.aliveInWave.clear();
137
+ this.killedInWave = 0;
138
+ this.runWave(Math.max(0, Math.min(index, this.record.waves.length)));
139
+ }
140
+ stop() {
141
+ if (this.stopped)
142
+ return;
143
+ this.stopped = true;
144
+ this.cancelTimers();
145
+ this.aliveInWave.clear();
146
+ this.complete = true;
147
+ this.callbacks.onScheduleEnd?.();
148
+ }
149
+ // ---- Wave lifecycle ------------------------------------------------------
150
+ runWave(index) {
151
+ if (this.stopped || !this.record)
152
+ return;
153
+ // Beyond the last wave — loop or end.
154
+ if (index >= this.record.waves.length) {
155
+ if (this.record.loop) {
156
+ this.runWave(0);
157
+ }
158
+ else {
159
+ this.complete = true;
160
+ this.callbacks.onScheduleEnd?.();
161
+ }
162
+ return;
163
+ }
164
+ const wave = this.record.waves[index];
165
+ this.waveIndex = index;
166
+ const startWave = () => {
167
+ if (this.stopped)
168
+ return;
169
+ this.waveStartedAtMs = this.scene.time.now;
170
+ this.killedInWave = 0;
171
+ this.aliveInWave.clear();
172
+ this.callbacks.onWaveStart?.(index, wave);
173
+ this.scheduleSpawns(wave, index);
174
+ this.armEndConditionCheck(wave, index);
175
+ };
176
+ if (wave.delayMs && wave.delayMs > 0) {
177
+ this.addTimer(wave.delayMs, startWave);
178
+ }
179
+ else {
180
+ startWave();
181
+ }
182
+ }
183
+ scheduleSpawns(wave, waveIndex) {
184
+ for (const ins of wave.spawns) {
185
+ switch (ins.kind) {
186
+ case 'point':
187
+ this.schedulePoint(ins, wave, waveIndex);
188
+ break;
189
+ case 'formation':
190
+ this.scheduleFormation(ins, wave, waveIndex);
191
+ break;
192
+ default: {
193
+ // Unknown kind — log + skip rather than crash. v1 ships point + formation;
194
+ // future kinds (`random`, ...) wire here.
195
+ // eslint-disable-next-line no-console
196
+ console.warn('[umicat/waves] unknown spawn kind:', ins.kind);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ schedulePoint(ins, wave, waveIndex) {
202
+ this.addTimer(ins.atMs ?? 0, () => {
203
+ this.doSpawn({
204
+ prefabId: ins.prefabId,
205
+ x: ins.x,
206
+ y: ins.y,
207
+ waveId: wave.id,
208
+ waveIndex,
209
+ overrides: ins.overrides,
210
+ });
211
+ });
212
+ }
213
+ scheduleFormation(ins, wave, waveIndex) {
214
+ const positions = expandFormation(ins);
215
+ for (let i = 0; i < positions.length; i++) {
216
+ const [x, y] = positions[i];
217
+ const at = ins.startMs + i * (ins.intervalMs ?? 0);
218
+ this.addTimer(at, () => {
219
+ this.doSpawn({
220
+ prefabId: ins.prefabId,
221
+ x,
222
+ y,
223
+ waveId: wave.id,
224
+ waveIndex,
225
+ overrides: ins.overrides,
226
+ });
227
+ });
228
+ }
229
+ }
230
+ doSpawn(instance) {
231
+ if (this.stopped)
232
+ return;
233
+ const go = this.callbacks.onSpawn(instance);
234
+ if (!go)
235
+ return;
236
+ this.aliveInWave.add(go);
237
+ go.once(Phaser.GameObjects.Events.DESTROY, () => {
238
+ this.aliveInWave.delete(go);
239
+ this.killedInWave++;
240
+ });
241
+ }
242
+ armEndConditionCheck(wave, waveIndex) {
243
+ const end = wave.endCondition ?? 'allDead';
244
+ const tick = () => {
245
+ if (this.stopped || this.paused)
246
+ return;
247
+ if (this.shouldEndWave(end)) {
248
+ this.endWave(wave, waveIndex);
249
+ }
250
+ };
251
+ // Poll once per 100ms. Light enough at any reasonable wave count;
252
+ // simpler than per-destroy event aggregation across all end-conditions.
253
+ this.endCheckTimer = this.scene.time.addEvent({
254
+ delay: 100,
255
+ loop: true,
256
+ callback: tick,
257
+ });
258
+ }
259
+ shouldEndWave(end) {
260
+ if (end === 'allDead') {
261
+ // Wait until every scheduled spawn has fired AND every spawned instance
262
+ // is gone. While the schedule still has pending timer callbacks, alive
263
+ // can be 0 transiently — guard with the timer count.
264
+ const pendingTimers = pendingSpawnTimers(this.timers);
265
+ return pendingTimers === 0 && this.aliveInWave.size === 0;
266
+ }
267
+ if (end.type === 'time') {
268
+ return this.scene.time.now - this.waveStartedAtMs >= end.ms;
269
+ }
270
+ if (end.type === 'count') {
271
+ return this.killedInWave >= end.killed;
272
+ }
273
+ return false;
274
+ }
275
+ endWave(wave, waveIndex) {
276
+ if (this.endCheckTimer) {
277
+ this.endCheckTimer.remove(false);
278
+ this.endCheckTimer = null;
279
+ }
280
+ this.callbacks.onWaveEnd?.(waveIndex, wave);
281
+ this.cancelTimers();
282
+ this.runWave(waveIndex + 1);
283
+ }
284
+ // ---- Timer plumbing ------------------------------------------------------
285
+ addTimer(delay, callback) {
286
+ const t = this.scene.time.addEvent({
287
+ delay: Math.max(0, delay),
288
+ callback: () => {
289
+ this.timers.delete(t);
290
+ callback();
291
+ },
292
+ });
293
+ this.timers.add(t);
294
+ }
295
+ cancelTimers() {
296
+ for (const t of this.timers)
297
+ t.remove(false);
298
+ this.timers.clear();
299
+ if (this.endCheckTimer) {
300
+ this.endCheckTimer.remove(false);
301
+ this.endCheckTimer = null;
302
+ }
303
+ }
304
+ }
305
+ /** Count of `addTimer` events that haven't fired yet. */
306
+ function pendingSpawnTimers(timers) {
307
+ let n = 0;
308
+ for (const t of timers)
309
+ if (!t.hasDispatched)
310
+ n++;
311
+ return n;
312
+ }
313
+ /**
314
+ * Expand a formation record into a list of [x, y] positions. v1 ships
315
+ * `grid` and `line`; `arc` and `v-shape` are forward-compat'd but expand
316
+ * to a single point at the origin with a warn.
317
+ */
318
+ function expandFormation(ins) {
319
+ const f = ins.formation;
320
+ const rows = Math.max(1, f.rows ?? 1);
321
+ const cols = Math.max(1, f.cols ?? 1);
322
+ const sx = f.spacing?.x ?? 0;
323
+ const sy = f.spacing?.y ?? 0;
324
+ const ox = f.origin?.x ?? 0;
325
+ const oy = f.origin?.y ?? 0;
326
+ const positions = [];
327
+ if (f.shape === 'grid') {
328
+ for (let r = 0; r < rows; r++) {
329
+ for (let c = 0; c < cols; c++) {
330
+ positions.push([ox + c * sx, oy + r * sy]);
331
+ }
332
+ }
333
+ return positions;
334
+ }
335
+ if (f.shape === 'line') {
336
+ for (let c = 0; c < cols; c++) {
337
+ positions.push([ox + c * sx, oy]);
338
+ }
339
+ return positions;
340
+ }
341
+ if (f.shape === 'v-shape') {
342
+ // simple v: rows = depth, cols = members per side; falls back to grid
343
+ // when ambiguous. v1 minimum: one row, cols members in a V centered on origin.
344
+ const half = Math.floor(cols / 2);
345
+ for (let c = 0; c < cols; c++) {
346
+ const dx = (c - half) * sx;
347
+ const dy = Math.abs(c - half) * sy;
348
+ positions.push([ox + dx, oy + dy]);
349
+ }
350
+ return positions;
351
+ }
352
+ if (f.shape === 'arc') {
353
+ const r = f.radius ?? 0;
354
+ const span = f.arcRadians ?? Math.PI;
355
+ for (let c = 0; c < cols; c++) {
356
+ const t = cols === 1 ? 0.5 : c / (cols - 1);
357
+ const theta = -span / 2 + t * span;
358
+ positions.push([ox + r * Math.sin(theta), oy + r * (1 - Math.cos(theta))]);
359
+ }
360
+ return positions;
361
+ }
362
+ // eslint-disable-next-line no-console
363
+ console.warn(`[umicat/waves] unknown formation shape '${f.shape}'; spawning single point at origin`);
364
+ return [[ox, oy]];
365
+ }
@@ -0,0 +1,103 @@
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
+ import type Phaser from 'phaser';
42
+ import type { AssetRecord, WangTerrain } from './types.js';
43
+ /**
44
+ * One cell mutated by an autotile cascade. `prevIndex === -1` means the
45
+ * cell was empty before; `index === -1` means the cell is empty after.
46
+ * Callers emit these as the `previousCells` array on `tilemapEdited` so
47
+ * undo can restore the prior state in one shot.
48
+ */
49
+ export interface AutotileAffectedCell {
50
+ x: number;
51
+ y: number;
52
+ index: number;
53
+ prevIndex: number;
54
+ }
55
+ /**
56
+ * Resolve a terrain by id from a tileset asset's autotile config. Returns
57
+ * null when the asset lacks autotile metadata or no terrain matches —
58
+ * callers typically warn + skip in that case.
59
+ */
60
+ export declare function findTerrain(asset: AssetRecord | null | undefined, terrainId: string): WangTerrain | null;
61
+ /**
62
+ * Resolve the autotile kind (`'wang-4bit'` | `'wang-8bit'`) on a tileset
63
+ * asset. Defaults to `'wang-4bit'` for legacy assets where `autotile.kind`
64
+ * is missing (shouldn't happen with the validating backend parser, but
65
+ * defensive for hand-authored manifests).
66
+ */
67
+ export declare function getAutotileKind(asset: AssetRecord | null | undefined): 'wang-4bit' | 'wang-8bit';
68
+ /**
69
+ * Invalidate the cached autotile state on a layer. Next autotile op
70
+ * rebuilds from the layer's current `data`. Call after structural
71
+ * mutations the grid model can't track (resize, removeLayer, tileset
72
+ * swap, terrain rule edits — `assetUpdate` does this automatically).
73
+ *
74
+ * Kept the old `invalidateAutotileVertices` name as an alias for
75
+ * back-compat with the 0.2.122–0.2.124 export surface — internal SDK
76
+ * code uses the new name, external consumers (none today) keep working.
77
+ */
78
+ export declare function invalidateAutotileCells(layer: Phaser.Tilemaps.TilemapLayer): void;
79
+ /** @deprecated Renamed to `invalidateAutotileCells` (D.2.5.1). */
80
+ export declare const invalidateAutotileVertices: typeof invalidateAutotileCells;
81
+ /**
82
+ * Apply a Wang autotile paint (or erase) at cell `(cellX, cellY)`.
83
+ *
84
+ * Paint mode (`mode = 'paint'`): the cell's painted-flag is set; the 3×3
85
+ * cell window around the click is recomputed. Each cell's new bitmask
86
+ * drives the tile index from `terrain.ruleMap`. Bitmask 0 (no corners) →
87
+ * cell becomes empty. Missing ruleMap entries are treated the same way
88
+ * (no tile defined → cell empty).
89
+ *
90
+ * Erase mode (`mode = 'erase'`): same flow with the cell's painted-flag
91
+ * cleared. Neighbors whose other corners are still terrain stay painted;
92
+ * cells whose bitmask drops to 0 become empty.
93
+ *
94
+ * Returns the affected cells with previous + new tile indices (one entry
95
+ * per cell in the 3×3 window that lies inside the layer's bounds, even
96
+ * if the index didn't change — the caller may dedup downstream). Cells
97
+ * outside the layer's bounds are silently skipped.
98
+ *
99
+ * Cheap: ~9 cell mutations + a Map lookup per cell. Cell-painted grid is
100
+ * stashed on the layer's data manager so subsequent calls reuse it
101
+ * without rebuilding from `layer.data`.
102
+ */
103
+ export declare function applyAutotile(layer: Phaser.Tilemaps.TilemapLayer, cellX: number, cellY: number, terrain: WangTerrain, mode: 'paint' | 'erase', kind?: 'wang-4bit' | 'wang-8bit'): AutotileAffectedCell[];