@umicat/phaser-sdk 1.0.8 → 1.0.9

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 CHANGED
@@ -589,16 +589,48 @@ The `assets[]` table is the single source of truth for asset loading in scene-as
589
589
  }
590
590
  ```
591
591
 
592
- `kind` is the entity discriminator with seven variants: `sprite` / `rect` / `circle` / `code-rendered` / `group` / `tilemap` / `trigger`. Per-kind render fields live at the entity root (no nested `visual` sub-object):
592
+ `kind` is the entity discriminator: `sprite` / `rect` / `circle` / `code-rendered` / `group` / `tilemap-ref` / `trigger` / `prefab-ref` (plus the deprecated embedded `tilemap`). Per-kind render fields live at the entity root (no nested `visual` sub-object):
593
593
 
594
594
  - `sprite` → `assetId`, optional `frame` / `tint` / `alpha` / `flipX` / `flipY`
595
595
  - `rect` → `width`, `height`, optional `fillColor` / `strokeColor` / `strokeWidth` / `alpha`
596
596
  - `circle` → `radius`, optional `fillColor` / `strokeColor` / `strokeWidth` / `alpha`
597
597
  - `code-rendered` → `script` (path to render script), optional `params` / `width` / `height`
598
- - `tilemap` → `tileSize`, `size`, `layers[]`
598
+ - `tilemap-ref` → `tilemapId` — reference to a standalone tilemap file at `public/tilemaps/<tilemapId>.json` (see "Tilemaps as standalone resources" below). The scene carries only the reference + placement; the map data lives in the file.
599
+ - `tilemap` → `tileSize`, `size`, `layers[]` — **deprecated** embedded form (tilemap-as-resource, ADR-008). Still renders so existing scenes keep working; don't author new ones — write a tilemap file + `tilemap-ref` instead.
599
600
  - `trigger` → `shape`, optional `description` / `targets` / `behaviorScript`
600
601
  - `group` → `children[]`
601
602
 
603
+ ### Tilemaps as standalone resources (ADR-008)
604
+
605
+ A tilemap's cell data is a project resource, not scene content. Each map is one JSON file at `public/tilemaps/<id>.json`:
606
+
607
+ ```jsonc
608
+ {
609
+ "schemaVersion": 1,
610
+ "id": "forest-floor", // must match the filename stem
611
+ "name": "Forest Floor",
612
+ "tileSize": { "width": 32, "height": 32 }, // pixels per cell
613
+ "size": { "width": 40, "height": 30 }, // map size in CELLS
614
+ "humanCurated": true, // set by Tilemap Studio; agents must ask before overwriting cells
615
+ "metadata": { "createdAt": "...", "editedAt": "...", "author": "user" },
616
+ "layers": [ // same layer shape as the embedded form (minus editor-only `locked`)
617
+ { "id": "ground", "tilesetIds": ["forest-tileset"], "z": 0,
618
+ "data": [[0, 1, null, 2]] } // row-major, size.height rows × size.width cols, null = empty
619
+ ]
620
+ }
621
+ ```
622
+
623
+ The scene references it with a `tilemap-ref` entity — placement only:
624
+
625
+ ```json
626
+ { "id": "e-ground", "kind": "tilemap-ref", "tilemapId": "forest-floor",
627
+ "role": "terrain", "transform": { "x": 640, "y": 360, "depth": 1 } }
628
+ ```
629
+
630
+ `loadWorldScene` fetches every referenced tilemap file before spawning (clear error naming the expected `public/tilemaps/<id>.json` path when one is missing) and renders it through the SAME runtime path the embedded form uses — autotile, per-tile metadata/collision, and animated tiles all work identically, and `getTilemapAt` / `addTilemapCollider` are unchanged. To change a map, edit the FILE; to move it, edit the entity's transform. The in-Editor paint RPC (`setTilemapTool` / `editTilemap` / `tilemapEdited`) is deprecated — humans paint in Tilemap Studio, agents write the file.
631
+
632
+ For a standalone canvas (e.g. Tilemap Studio's preview), `renderTilemapPreview(scene, tilemapFile, resolveAsset, position?)` renders a `TilemapFile` in any Phaser scene without a scene file or entity registry — the caller loads the tileset textures first. Related exports: `TILEMAPS_BASE`, `tilemapFilePath(id)`, `tilemapFileCacheKey(id)`, `loadReferencedTilemapFiles(scene, sceneFile)`, types `TilemapRefEntity` / `TilemapFile` / `TilemapFileLayer`.
633
+
602
634
  **Optional physics body (since 0.2.54).** Any renderable entity (`sprite` / `rect` / `circle` / `code-rendered`) may carry a `physics` block — the same shape prefabs use (see the Prefabs section's `physics` field reference). When present, `loadWorldScene` gives the entity an Arcade body, sized and offset per the block, so behavior code doesn't call `physics.add.existing` / `body.setSize` for it. A code-rendered platform, for example, can declare `"physics": { "bodyW": 200, "bodyH": 32, "immovable": true }` and be collidable the moment the scene loads.
603
635
 
604
636
  **World gravity from scene data (since 0.2.55).** A world scene's `world.physics.gravity` (`{ x?, y? }`) — or, as a manifest-wide default, `manifest.globals.physics.gravity` — is applied to the scene's Arcade physics world by `loadWorldScene`. Scene-level wins over the manifest global. Example: a platformer's `world/main.json` carries `"world": { "width": 5120, "height": 720, "physics": { "gravity": { "x": 0, "y": 700 } } }` and the player just falls — no `this.physics.world.gravity` line in `GameScene.ts`. (Before 0.2.55 these two fields were typed but inert; only `game.json`'s `GameConfig.physics.gravity`, wired through `createUmicatGame` in `main.ts`, took effect.)
@@ -94,6 +94,11 @@ export declare function getEditorCamera(game: Phaser.Game): Phaser.Cameras.Scene
94
94
  * Without the await, addLayer's `textures.exists()` check fails on
95
95
  * the in-flight load → skips layer creation → painter can't find
96
96
  * layer → click falls through to drag-the-entity.
97
+ *
98
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — edit the standalone
99
+ * `public/tilemaps/{id}.json` file instead (Tilemap Studio / agent file
100
+ * write). Kept fully functional for legacy embedded tilemaps + undo replay
101
+ * on unmigrated hosts; host side removed in Phase 5.
97
102
  */
98
103
  export declare function handleEditTilemap(game: Phaser.Game, entityId: string, ops: TilemapEditOp[], manifestAsset?: AssetRecord): Promise<void>;
99
104
  /**
@@ -111,5 +116,9 @@ export declare function findTilemapLayerById(container: Phaser.GameObjects.Conta
111
116
  *
112
117
  * The optional `asset` arg is required for `autotilePaint` (the only op
113
118
  * that needs to resolve a terrain's ruleMap); other ops ignore it.
119
+ *
120
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — cell data now lives
121
+ * in `public/tilemaps/{id}.json`; edit the file. Behavior unchanged for
122
+ * legacy embedded tilemaps + the deprecated paint path.
114
123
  */
115
124
  export declare function applyTilemapOp(layer: Phaser.Tilemaps.TilemapLayer, op: TilemapEditOp, asset?: AssetRecord | null): void;
@@ -8,7 +8,8 @@ import { getEntityRegistry } from '../scene/EntityRegistry.js';
8
8
  import { applyAssetHitbox, applyTilesetAnimations, applyTilesetTileMetadata, applyTrackedHitArea, parseColor, resetTilesetAnimationsToRoot, spawnEntity } from '../scene/spawnEntity.js';
9
9
  import { applyAutotile, findTerrain, getAutotileKind, invalidateAutotileCells } from '../scene/autotile.js';
10
10
  import { resolveRenderScript } from '../scene/renderScripts.js';
11
- import { getManifest } from '../scene/SceneLoader.js';
11
+ import { getManifest, tilemapFilePath } from '../scene/SceneLoader.js';
12
+ import { tilemapFileCacheKey } from '../scene/types.js';
12
13
  import { getRules, patchRule } from '../scene/Rules.js';
13
14
  import { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './EditorOverlayScene.js';
14
15
  import { getEditorState, setEditorActive, setSelection, getSelection, getEditorMode, setEditorMode, getActiveSceneId, setActiveSceneId, setDebugOverlayState, setTilemapToolState, } from './EditorState.js';
@@ -1276,6 +1277,31 @@ async function createEntity(game, entity, manifestAsset) {
1276
1277
  }
1277
1278
  }
1278
1279
  }
1280
+ else if (entity.kind === 'tilemap-ref') {
1281
+ // Tilemap-as-resource (ADR-008): the ref's cell data lives in the
1282
+ // standalone file `public/tilemaps/{tilemapId}.json`. At scene LOAD,
1283
+ // SceneLoader's `loadReferencedTilemapFiles` fetches it into the JSON
1284
+ // cache before spawn; but editor-time drag-to-place bypasses that path,
1285
+ // so the file is absent and `spawnTilemapRef` soft-fails (placeholder /
1286
+ // nothing). Fetch the file here, THEN queue each tileset its layers
1287
+ // reference — mirrors the sprite/tilemap lazy-load above so a freshly
1288
+ // dropped tilemap renders LIVE without a save+reload round-trip.
1289
+ const tilemapId = entity.tilemapId;
1290
+ if (tilemapId) {
1291
+ try {
1292
+ await loadTilemapFileIntoScene(sceneRef, tilemapId);
1293
+ }
1294
+ catch (e) {
1295
+ console.warn('[umicat/editor] createEntity: tilemap-ref file load failed:', e);
1296
+ }
1297
+ const file = sceneRef.cache.json.get(tilemapFileCacheKey(tilemapId));
1298
+ for (const layer of file?.layers ?? []) {
1299
+ for (const tid of layer.tilesetIds ?? []) {
1300
+ queueIfMissing(resolveById(tid));
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1279
1305
  for (const a of assetsToLoad) {
1280
1306
  await loadAssetIntoScene(scene, a);
1281
1307
  }
@@ -1312,7 +1338,7 @@ async function createEntity(game, entity, manifestAsset) {
1312
1338
  // with hidden grid renders nothing, so the user sees nothing on the
1313
1339
  // canvas + thinks the drop failed. Re-run the visibility walk after
1314
1340
  // every editor-driven spawn so newly-dropped tilemaps show their bounds.
1315
- if (entity.kind === 'tilemap') {
1341
+ if (entity.kind === 'tilemap' || entity.kind === 'tilemap-ref') {
1316
1342
  setTilemapGridsVisible(game, true);
1317
1343
  }
1318
1344
  }
@@ -1449,6 +1475,57 @@ function loadAssetIntoScene(scene, asset) {
1449
1475
  scene.load.start();
1450
1476
  });
1451
1477
  }
1478
+ /**
1479
+ * Editor-time loader for a standalone tilemap file (`public/tilemaps/{id}.json`),
1480
+ * fetched into the Phaser JSON cache under `tilemapFileCacheKey(id)` — the same
1481
+ * key `spawnTilemapRef` reads from. Mirrors `loadAssetIntoScene`'s await-loader
1482
+ * pattern. Idempotent: resolves immediately if the file is already cached. Used
1483
+ * by `createEntity` when a `tilemap-ref` is dragged in, since the scene-load
1484
+ * path (`loadReferencedTilemapFiles`) that normally primes this cache never runs
1485
+ * for editor-time spawns.
1486
+ */
1487
+ function loadTilemapFileIntoScene(scene, tilemapId) {
1488
+ return new Promise((resolve, reject) => {
1489
+ const cacheKey = tilemapFileCacheKey(tilemapId);
1490
+ if (scene.cache.json.exists(cacheKey)) {
1491
+ resolve();
1492
+ return;
1493
+ }
1494
+ const cleanup = () => {
1495
+ scene.load.off(Phaser.Loader.Events.FILE_COMPLETE, perFileComplete);
1496
+ scene.load.off(Phaser.Loader.Events.FILE_LOAD_ERROR, onError);
1497
+ scene.load.off(Phaser.Loader.Events.COMPLETE, onComplete);
1498
+ };
1499
+ const perFileComplete = (key) => {
1500
+ if (key === cacheKey) {
1501
+ cleanup();
1502
+ resolve();
1503
+ }
1504
+ };
1505
+ const onComplete = () => {
1506
+ cleanup();
1507
+ // COMPLETE without the file landing means it 404'd / failed — surface
1508
+ // as a rejection so the caller logs, but spawnTilemapRef still soft-fails
1509
+ // gracefully (placeholder) if we proceed regardless.
1510
+ if (scene.cache.json.exists(cacheKey))
1511
+ resolve();
1512
+ else
1513
+ reject(new Error(`tilemap file not loaded: ${tilemapFilePath(tilemapId)}`));
1514
+ };
1515
+ const onError = (file) => {
1516
+ if (file.key !== cacheKey)
1517
+ return;
1518
+ cleanup();
1519
+ reject(new Error(`failed to load tilemap file ${tilemapId}: ${file.url}`));
1520
+ };
1521
+ scene.load.on(Phaser.Loader.Events.FILE_COMPLETE, perFileComplete);
1522
+ scene.load.on(Phaser.Loader.Events.FILE_LOAD_ERROR, onError);
1523
+ scene.load.on(Phaser.Loader.Events.COMPLETE, onComplete);
1524
+ scene.load.json(cacheKey, tilemapFilePath(tilemapId));
1525
+ if (!scene.load.isLoading())
1526
+ scene.load.start();
1527
+ });
1528
+ }
1452
1529
  function deleteEntity(game, entityId) {
1453
1530
  // HUD mode — dispatch to the HUD-scene helper, which destroys + unregisters
1454
1531
  // + drops the entity from the cached scene file.
@@ -2246,6 +2323,15 @@ function handlePatchRule(game, path, value) {
2246
2323
  patchRule(scene, path, value);
2247
2324
  }
2248
2325
  // --- Slice 6 Phase B — tilemap painter -----------------------------------
2326
+ //
2327
+ // @deprecated (whole section) — tilemap-as-resource (ADR-008, platform
2328
+ // restructure Phase 4). Tilemap editing moves to Tilemap Studio, which
2329
+ // writes the standalone `public/tilemaps/{id}.json` file; the agent edits
2330
+ // that file directly instead of pushing paint ops. Everything below KEEPS
2331
+ // WORKING unchanged for one minor version so legacy embedded
2332
+ // `kind:'tilemap'` entities + unmigrated hosts can still paint; the host
2333
+ // side is removed in Phase 5.
2334
+ /** @deprecated Phase 4 — in-Editor paint path retired (see section note). Behavior unchanged. */
2249
2335
  function handleSetTilemapTool(game, msg) {
2250
2336
  setTilemapToolState(game, {
2251
2337
  tilemapEditingId: msg.tilemapEditingId,
@@ -2286,6 +2372,11 @@ function handleSetTilemapTool(game, msg) {
2286
2372
  * Without the await, addLayer's `textures.exists()` check fails on
2287
2373
  * the in-flight load → skips layer creation → painter can't find
2288
2374
  * layer → click falls through to drag-the-entity.
2375
+ *
2376
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — edit the standalone
2377
+ * `public/tilemaps/{id}.json` file instead (Tilemap Studio / agent file
2378
+ * write). Kept fully functional for legacy embedded tilemaps + undo replay
2379
+ * on unmigrated hosts; host side removed in Phase 5.
2289
2380
  */
2290
2381
  export async function handleEditTilemap(game, entityId, ops, manifestAsset) {
2291
2382
  const scene = findWorldScene(game);
@@ -2359,6 +2450,10 @@ export async function handleEditTilemap(game, entityId, ops, manifestAsset) {
2359
2450
  * individual cells. Mirrors how `createTilemap` builds layers initially
2360
2451
  * (in spawnEntity.ts) — same Container parent, same per-layer tagging,
2361
2452
  * same centered (-w/2, -h/2) positioning.
2453
+ *
2454
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — layer structure now
2455
+ * lives in `public/tilemaps/{id}.json`; edit the file. Behavior unchanged
2456
+ * for legacy embedded tilemaps.
2362
2457
  */
2363
2458
  function applyTilemapStructureOp(scene, container, op) {
2364
2459
  switch (op.kind) {
@@ -2682,6 +2777,10 @@ export function findTilemapLayerById(container, layerId) {
2682
2777
  *
2683
2778
  * The optional `asset` arg is required for `autotilePaint` (the only op
2684
2779
  * that needs to resolve a terrain's ruleMap); other ops ignore it.
2780
+ *
2781
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — cell data now lives
2782
+ * in `public/tilemaps/{id}.json`; edit the file. Behavior unchanged for
2783
+ * legacy embedded tilemaps + the deprecated paint path.
2685
2784
  */
2686
2785
  export function applyTilemapOp(layer, op, asset) {
2687
2786
  switch (op.kind) {
@@ -465,6 +465,13 @@ export class EditorOverlayScene extends Phaser.Scene {
465
465
  this.postDragEnd(drag.entityId, before, after);
466
466
  }
467
467
  // --- Slice 6 Phase B — tilemap painter -----------------------------------
468
+ //
469
+ // @deprecated (whole section) — tilemap-as-resource (ADR-008, Phase 4).
470
+ // Human tilemap authoring moves to Tilemap Studio (writes the standalone
471
+ // `public/tilemaps/{id}.json` file); these in-canvas paint handlers KEEP
472
+ // WORKING unchanged for one minor version so legacy embedded
473
+ // `kind:'tilemap'` entities + unmigrated hosts can still paint. Host side
474
+ // removed in Phase 5.
468
475
  /**
469
476
  * Pointer-down inside paint mode. Returns true when the pointer hit the
470
477
  * active layer (stroke began); false when the pointer fell outside the
@@ -35,8 +35,16 @@ export interface EditorDebugOverlayState {
35
35
  *
36
36
  * `tilemapEditingId` doubles as the "are we in paint mode?" flag — when
37
37
  * non-null AND the selected entity is a tilemap, paint mode is active.
38
+ *
39
+ * @deprecated Tilemap-as-resource (ADR-008, platform restructure Phase 4).
40
+ * The in-Editor paint path (this state + the stroke/rect accumulators
41
+ * below) is retired in favor of Tilemap Studio writing
42
+ * `public/tilemaps/{id}.json` directly. Everything KEEPS WORKING unchanged
43
+ * for one minor version so legacy embedded `kind:'tilemap'` entities +
44
+ * unmigrated hosts can still paint; the host side is removed in Phase 5.
38
45
  */
39
46
  export type TilemapTool = 'brush' | 'eraser' | 'rect' | 'fill' | 'picker';
47
+ /** @deprecated Phase 4 — see {@link TilemapTool}'s deprecation note. Behavior unchanged. */
40
48
  export interface TilemapToolState {
41
49
  /** Tilemap entity currently being painted. Null = not in paint mode. */
42
50
  tilemapEditingId: string | null;
package/dist/index.d.ts CHANGED
@@ -16,9 +16,9 @@ export type { ChatMessage, ChatMessageKind } from './realtime/UmicatRoom.js';
16
16
  export { RpcError } from './core/Transport.js';
17
17
  export type { Transport, TransportKind } from './core/Transport.js';
18
18
  export type { UmicatUser } from './protocol.js';
19
- export { loadWorldScene, preloadManifest, preloadSceneAssets, applyPixelArtFilters, getManifest, SCENES_BASE, MANIFEST_PATH, suspendSceneUpdates, } from './scene/SceneLoader.js';
19
+ export { loadWorldScene, preloadManifest, preloadSceneAssets, applyPixelArtFilters, getManifest, SCENES_BASE, MANIFEST_PATH, TILEMAPS_BASE, tilemapFilePath, loadReferencedTilemapFiles, suspendSceneUpdates, } from './scene/SceneLoader.js';
20
20
  export type { LoadWorldSceneOptions } from './scene/SceneLoader.js';
21
- export { spawnEntity, parseColor, applyAssetHitbox, applyTilesetTileMetadata, getTilemapAt, addTilemapCollider, } from './scene/spawnEntity.js';
21
+ export { spawnEntity, parseColor, applyAssetHitbox, applyTilesetTileMetadata, getTilemapAt, addTilemapCollider, renderTilemapPreview, } from './scene/spawnEntity.js';
22
22
  export type { SpawnContext, AssetResolver, RenderScriptResolver, TilemapHit, } from './scene/spawnEntity.js';
23
23
  export { applyAutotile, findTerrain, getAutotileKind, invalidateAutotileCells, invalidateAutotileVertices, } from './scene/autotile.js';
24
24
  export type { AutotileAffectedCell } from './scene/autotile.js';
@@ -38,6 +38,6 @@ export type { RenderScriptModule } from './scene/renderScripts.js';
38
38
  export { setupEditorBridge } from './editor/EditorBridge.js';
39
39
  export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
40
40
  export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSetEditModeMessage, EditorSelectionRectMessage, EditorSdkToHostMessage, } from './protocol.js';
41
- export { SCHEMA_VERSION, isPerFrameNinePatch, isPerFrameHitbox, } from './scene/types.js';
42
- export type { Manifest, ManifestGroup, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, RenderableEntity, SpriteEntity, RectEntity, CircleEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TilemapLayer, TilesetMetadata, TileMetadata, TilesetAutotile, TilesetAnimation, WangTerrain, TriggerEntity, PrefabRefEntity, TriggerShape, HudEntity, HudEntityKind, HudEntityBase, HudTextEntity, HudImageEntity, HudIconButtonEntity, HudProgressBarEntity, HudPanelEntity, HudTextSource, HudNumberSource, HudLayer, Transform, Anchor, AnchorSide, NinePatchConfig, NinePatchPerFrame, HitboxRect, HitboxPerFrame, DepthAnchor, } from './scene/types.js';
41
+ export { SCHEMA_VERSION, isPerFrameNinePatch, isPerFrameHitbox, tilemapFileCacheKey, } from './scene/types.js';
42
+ export type { Manifest, ManifestGroup, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, RenderableEntity, SpriteEntity, RectEntity, CircleEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TilemapRefEntity, TilemapFile, TilemapFileLayer, TilemapLayer, TilesetMetadata, TileMetadata, TilesetAutotile, TilesetAnimation, WangTerrain, TriggerEntity, PrefabRefEntity, TriggerShape, HudEntity, HudEntityKind, HudEntityBase, HudTextEntity, HudImageEntity, HudIconButtonEntity, HudProgressBarEntity, HudPanelEntity, HudTextSource, HudNumberSource, HudLayer, Transform, Anchor, AnchorSide, NinePatchConfig, NinePatchPerFrame, HitboxRect, HitboxPerFrame, DepthAnchor, } from './scene/types.js';
43
43
  export { PROTOCOL_VERSION, type HelloMessage, type InitMessage, type RpcRequestMessage, type RpcResultOk, type RpcResultError, type HostToSdkMessage, type SdkToHostMessage, type RpcErrorPayload, type RpcMethod, type SavesGetParams, type SavesGetResult, type SavesSetParams, type SavesSetResult, type SavesDeleteParams, type SavesDeleteResult, type SavesListResult, type GameDataGetParams, type GameDataGetResult, type GameDataSetParams, type GameDataSetResult, type GameDataDeleteParams, type GameDataDeleteResult, type GameDataListResult, type RealtimeGetTokenParams, type RealtimeGetTokenResult, } from './protocol.js';
package/dist/index.js CHANGED
@@ -14,8 +14,14 @@ export { RealtimeModule } from './realtime/RealtimeModule.js';
14
14
  export { UmicatRoom, PlayerDataFacade, RoomDataFacade, ChatFacade, MAX_CHAT_TEXT_LEN, } from './realtime/UmicatRoom.js';
15
15
  export { RpcError } from './core/Transport.js';
16
16
  // Scene-as-data (visual editor foundation, slice 1)
17
- export { loadWorldScene, preloadManifest, preloadSceneAssets, applyPixelArtFilters, getManifest, SCENES_BASE, MANIFEST_PATH, suspendSceneUpdates, } from './scene/SceneLoader.js';
18
- export { spawnEntity, parseColor, applyAssetHitbox, applyTilesetTileMetadata, getTilemapAt, addTilemapCollider, } from './scene/spawnEntity.js';
17
+ export { loadWorldScene, preloadManifest, preloadSceneAssets, applyPixelArtFilters, getManifest, SCENES_BASE, MANIFEST_PATH, TILEMAPS_BASE, tilemapFilePath, loadReferencedTilemapFiles, suspendSceneUpdates, } from './scene/SceneLoader.js';
18
+ export { spawnEntity, parseColor, applyAssetHitbox, applyTilesetTileMetadata, getTilemapAt, addTilemapCollider,
19
+ // Tilemap-as-resource (ADR-008, Phase 4) — minimal rendering surface for
20
+ // Tilemap Studio's standalone canvas: renders a TilemapFile through the
21
+ // exact in-game path (layers, autotile-resolved cells, tile metadata,
22
+ // animations) without a scene file or entity registry. Pair with
23
+ // `tilemapFileCacheKey` / `TILEMAPS_BASE` to fetch the file.
24
+ renderTilemapPreview, } from './scene/spawnEntity.js';
19
25
  // Slice 6 Phase D — Wang autotile runtime.
20
26
  export { applyAutotile, findTerrain, getAutotileKind, invalidateAutotileCells, invalidateAutotileVertices, // deprecated alias kept for 0.2.122–0.2.124 callers
21
27
  } from './scene/autotile.js';
@@ -39,5 +45,5 @@ export { setupEditorModeListener, isEditMode } from './scene/EditorMode.js';
39
45
  export { setRenderScriptRegistry, getRenderScriptRegistry, resolveRenderScript, } from './scene/renderScripts.js';
40
46
  export { setupEditorBridge } from './editor/EditorBridge.js';
41
47
  export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
42
- export { SCHEMA_VERSION, isPerFrameNinePatch, isPerFrameHitbox, } from './scene/types.js';
48
+ export { SCHEMA_VERSION, isPerFrameNinePatch, isPerFrameHitbox, tilemapFileCacheKey, } from './scene/types.js';
43
49
  export { PROTOCOL_VERSION, } from './protocol.js';
@@ -366,6 +366,13 @@ export interface EditorSetDebugOverlayMessage {
366
366
  * many times per second; round-tripping each one through postMessage would
367
367
  * add visible latency. SDK paints locally + posts ONE `tilemapEdited` per
368
368
  * completed stroke for undo recording — same pattern as drag-to-move.
369
+ *
370
+ * @deprecated Tilemap-as-resource (ADR-008, platform restructure Phase 4).
371
+ * Tilemap editing moves to Tilemap Studio, which writes the standalone
372
+ * `public/tilemaps/{id}.json` file directly — the in-Editor paint path is
373
+ * deprecated in Phase 4 and the host side is removed in Phase 5. The SDK
374
+ * keeps this message working (behavior unchanged) for one minor version so
375
+ * unmigrated hosts / legacy embedded tilemaps keep painting.
369
376
  */
370
377
  export interface EditorSetTilemapToolMessage {
371
378
  type: 'umicat:editor:setTilemapTool';
@@ -405,6 +412,12 @@ export interface EditorSetTilemapToolMessage {
405
412
  * replay (host pushes the inverse op), and any future host-driven flow.
406
413
  * Live painting via pointer events does NOT use this message — that path
407
414
  * goes SDK→host (`tilemapEdited`) for undo recording.
415
+ *
416
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4). The agent now writes
417
+ * `public/tilemaps/{id}.json` directly instead of pushing paint ops through
418
+ * the editor; humans paint in Tilemap Studio. Kept working (behavior
419
+ * unchanged) for one minor version for legacy embedded `kind:'tilemap'`
420
+ * entities and undo replay on unmigrated hosts.
408
421
  */
409
422
  export interface EditorEditTilemapMessage {
410
423
  type: 'umicat:editor:editTilemap';
@@ -433,6 +446,10 @@ export interface EditorEditTilemapMessage {
433
446
  * paint/erase/fillRect/bucketFill mutate cell data within a layer (Phase B.3
434
447
  * + B.4); addLayer/removeLayer/setLayerVisibility mutate the layer list
435
448
  * itself (Phase B.5).
449
+ *
450
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — see
451
+ * {@link EditorEditTilemapMessage}. Edit the standalone tilemap file
452
+ * instead; ops remain functional for legacy embedded tilemaps.
436
453
  */
437
454
  export type TilemapEditOp = {
438
455
  kind: 'paint';
@@ -772,6 +789,11 @@ export interface EditorCameraStateMessage {
772
789
  * The SDK already applied the op locally before posting — so the iframe is
773
790
  * already showing the painted cells. This message exists for undo +
774
791
  * persistence, NOT live preview (mirrors how `dragEnd` works).
792
+ *
793
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4). The in-Editor paint
794
+ * path is retired in favor of Tilemap Studio writing
795
+ * `public/tilemaps/{id}.json`. Still emitted (behavior unchanged) while
796
+ * legacy embedded tilemaps + unmigrated hosts exist.
775
797
  */
776
798
  export interface EditorTilemapEditedMessage {
777
799
  type: 'umicat:editor:tilemapEdited';
@@ -799,6 +821,10 @@ export interface EditorTilemapEditedMessage {
799
821
  * Photoshop / Aseprite eyedropper convention — pick once, paint next).
800
822
  *
801
823
  * `tileIndex: null` means the picked cell is empty.
824
+ *
825
+ * @deprecated Tilemap-as-resource (ADR-008, Phase 4) — paint path retired,
826
+ * see {@link EditorTilemapEditedMessage}. Still emitted while legacy
827
+ * embedded tilemaps + unmigrated hosts exist.
802
828
  */
803
829
  export interface EditorTilemapTilePickedMessage {
804
830
  type: 'umicat:editor:tilemapTilePicked';
@@ -10,6 +10,16 @@ import { EntityRegistry } from './EntityRegistry.js';
10
10
  */
11
11
  export declare const SCENES_BASE = "scenes/";
12
12
  export declare const MANIFEST_PATH = "scenes/manifest.json";
13
+ /**
14
+ * Where standalone tilemap resource files live (tilemap-as-resource,
15
+ * ADR-008 / platform restructure Phase 4). A scene's `tilemap-ref` entity
16
+ * carries `tilemapId`; the file is `public/tilemaps/{tilemapId}.json`
17
+ * (served origin-relative as `tilemaps/{tilemapId}.json`, same convention
18
+ * as {@link SCENES_BASE}).
19
+ */
20
+ export declare const TILEMAPS_BASE = "tilemaps/";
21
+ /** Origin-relative URL of a standalone tilemap file. */
22
+ export declare function tilemapFilePath(tilemapId: string): string;
13
23
  /**
14
24
  * Fetches the manifest via Phaser's loader. Must be called from a Phaser
15
25
  * scene's `preload()` since it queues a load request. Subsequent calls
@@ -116,3 +126,18 @@ export declare function loadWorldScene(scene: Phaser.Scene, sceneId: string, opt
116
126
  sceneFile: WorldScene;
117
127
  registry: EntityRegistry;
118
128
  }>;
129
+ /**
130
+ * Fetch every standalone tilemap file referenced by the scene's
131
+ * `tilemap-ref` entities into the Phaser JSON cache (tilemap-as-resource,
132
+ * ADR-008 / Phase 4). Idempotent — files already in the cache aren't
133
+ * re-fetched, so scene transitions back to a tilemap-using scene are free.
134
+ *
135
+ * Fails LOUDLY with an actionable message when a referenced file is
136
+ * missing or malformed: a tilemap-ref without its file would render
137
+ * nothing, and the silent variant of that bug ("my ground disappeared")
138
+ * is much harder to diagnose than a thrown scene-load error naming the
139
+ * exact expected path. The spawn path additionally soft-fails (magenta
140
+ * placeholder) for refs spawned outside this loader, e.g. editor
141
+ * createEntity.
142
+ */
143
+ export declare function loadReferencedTilemapFiles(scene: Phaser.Scene, sceneFile: WorldScene): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import Phaser from 'phaser';
2
- import { SCHEMA_VERSION, } from './types.js';
2
+ import { SCHEMA_VERSION, tilemapFileCacheKey, } from './types.js';
3
3
  import { spawnEntity } from './spawnEntity.js';
4
4
  import { attachEntityRegistry } from './EntityRegistry.js';
5
5
  import { resolveRenderScript } from './renderScripts.js';
@@ -11,6 +11,18 @@ import { resolveRenderScript } from './renderScripts.js';
11
11
  */
12
12
  export const SCENES_BASE = 'scenes/';
13
13
  export const MANIFEST_PATH = `${SCENES_BASE}manifest.json`;
14
+ /**
15
+ * Where standalone tilemap resource files live (tilemap-as-resource,
16
+ * ADR-008 / platform restructure Phase 4). A scene's `tilemap-ref` entity
17
+ * carries `tilemapId`; the file is `public/tilemaps/{tilemapId}.json`
18
+ * (served origin-relative as `tilemaps/{tilemapId}.json`, same convention
19
+ * as {@link SCENES_BASE}).
20
+ */
21
+ export const TILEMAPS_BASE = 'tilemaps/';
22
+ /** Origin-relative URL of a standalone tilemap file. */
23
+ export function tilemapFilePath(tilemapId) {
24
+ return `${TILEMAPS_BASE}${tilemapId}.json`;
25
+ }
14
26
  const MANIFEST_CACHE_KEY = '__unboxyManifestState';
15
27
  /**
16
28
  * Fetches the manifest via Phaser's loader. Must be called from a Phaser
@@ -78,7 +90,7 @@ export function preloadSceneAssets(scene, sceneFile, manifest) {
78
90
  if (sceneFile.type !== 'world')
79
91
  return; // HUD slice will add its own walker
80
92
  const state = getOrInitState(scene, manifest);
81
- const ids = collectAssetIds(sceneFile.entities, manifest);
93
+ const ids = collectAssetIds(sceneFile.entities, manifest, (tilemapId) => scene.cache.json.get(tilemapFileCacheKey(tilemapId)));
82
94
  for (const id of ids) {
83
95
  if (state.requestedAssetIds.has(id))
84
96
  continue;
@@ -98,7 +110,7 @@ export function preloadSceneAssets(scene, sceneFile, manifest) {
98
110
  state.requestedAssetIds.add(id);
99
111
  }
100
112
  }
101
- function collectAssetIds(entities, manifest) {
113
+ function collectAssetIds(entities, manifest, resolveTilemapFile) {
102
114
  const ids = new Set();
103
115
  function walk(e) {
104
116
  if (e.kind === 'sprite') {
@@ -119,6 +131,18 @@ function collectAssetIds(entities, manifest) {
119
131
  ids.add(id);
120
132
  }
121
133
  }
134
+ else if (e.kind === 'tilemap-ref') {
135
+ // Tilemap-as-resource (Phase 4): the ref's layer data lives in
136
+ // `public/tilemaps/{tilemapId}.json` — loaded into the JSON cache by
137
+ // `loadReferencedTilemapFiles` BEFORE this walker runs, so the
138
+ // resolver can read the file's layers and queue their tilesets the
139
+ // same way the embedded-tilemap branch above does.
140
+ const file = resolveTilemapFile?.(e.tilemapId);
141
+ for (const layer of file?.layers ?? []) {
142
+ for (const id of layer.tilesetIds ?? [])
143
+ ids.add(id);
144
+ }
145
+ }
122
146
  else if (e.kind === 'prefab-ref') {
123
147
  // Authored prefab instance — preload whatever the prefab's visual
124
148
  // references (sprite prefabs carry an assetId at the record root).
@@ -386,6 +410,12 @@ async function loadWorldSceneImpl(scene, sceneId, options) {
386
410
  // entities spawn. Scene-level `world.physics.gravity` wins over the
387
411
  // manifest-wide `globals.physics.gravity`.
388
412
  applyWorldGravity(scene, sceneFile, manifest);
413
+ // Tilemap-as-resource (ADR-008, Phase 4): resolve `tilemap-ref` entities
414
+ // by fetching their standalone `public/tilemaps/{id}.json` files FIRST —
415
+ // the asset preload below reads each file's layers to know which tileset
416
+ // textures to queue. Files land in the Phaser JSON cache (keyed by
417
+ // `tilemapFileCacheKey`) so repeat loads of the same scene are free.
418
+ await loadReferencedTilemapFiles(scene, sceneFile);
389
419
  // Lazy preload of any new assets this scene needs, then await loader.
390
420
  preloadSceneAssets(scene, sceneFile, manifest);
391
421
  await runLoader(scene);
@@ -449,6 +479,72 @@ async function loadSceneJson(scene, ref) {
449
479
  await runLoader(scene);
450
480
  return scene.cache.json.get(cacheKey);
451
481
  }
482
+ /** Recursively collect every `tilemap-ref` entity (groups walked too). */
483
+ function collectTilemapRefs(entities) {
484
+ const refs = [];
485
+ function walk(e) {
486
+ if (e.kind === 'tilemap-ref') {
487
+ refs.push(e);
488
+ }
489
+ else if (e.kind === 'group') {
490
+ for (const child of e.children)
491
+ walk(child);
492
+ }
493
+ }
494
+ for (const e of entities)
495
+ walk(e);
496
+ return refs;
497
+ }
498
+ /**
499
+ * Fetch every standalone tilemap file referenced by the scene's
500
+ * `tilemap-ref` entities into the Phaser JSON cache (tilemap-as-resource,
501
+ * ADR-008 / Phase 4). Idempotent — files already in the cache aren't
502
+ * re-fetched, so scene transitions back to a tilemap-using scene are free.
503
+ *
504
+ * Fails LOUDLY with an actionable message when a referenced file is
505
+ * missing or malformed: a tilemap-ref without its file would render
506
+ * nothing, and the silent variant of that bug ("my ground disappeared")
507
+ * is much harder to diagnose than a thrown scene-load error naming the
508
+ * exact expected path. The spawn path additionally soft-fails (magenta
509
+ * placeholder) for refs spawned outside this loader, e.g. editor
510
+ * createEntity.
511
+ */
512
+ export async function loadReferencedTilemapFiles(scene, sceneFile) {
513
+ const refs = collectTilemapRefs(sceneFile.entities);
514
+ if (refs.length === 0)
515
+ return;
516
+ const queuedIds = new Set();
517
+ for (const ref of refs) {
518
+ if (queuedIds.has(ref.tilemapId))
519
+ continue;
520
+ queuedIds.add(ref.tilemapId);
521
+ const cacheKey = tilemapFileCacheKey(ref.tilemapId);
522
+ if (scene.cache.json.exists(cacheKey))
523
+ continue;
524
+ scene.load.json(cacheKey, tilemapFilePath(ref.tilemapId));
525
+ }
526
+ try {
527
+ await runLoader(scene);
528
+ }
529
+ catch (e) {
530
+ const detail = e instanceof Error ? e.message : String(e);
531
+ throw new Error(`[umicat/scene] scene '${sceneFile.id}' references standalone tilemap file(s) that failed to load (${detail}). ` +
532
+ `tilemap-ref entities expect a file at public/tilemaps/<tilemapId>.json — ` +
533
+ `referenced ids: ${Array.from(queuedIds).join(', ')}`);
534
+ }
535
+ // Validate every ref resolved to a structurally-plausible TilemapFile.
536
+ for (const ref of refs) {
537
+ const file = scene.cache.json.get(tilemapFileCacheKey(ref.tilemapId));
538
+ if (!file) {
539
+ throw new Error(`[umicat/scene] entity '${ref.id}' (kind=tilemap-ref) points at tilemap '${ref.tilemapId}' ` +
540
+ `but public/tilemaps/${ref.tilemapId}.json did not load — create the file or remove the ref`);
541
+ }
542
+ if (!file.tileSize || !file.size || !Array.isArray(file.layers)) {
543
+ throw new Error(`[umicat/scene] tilemap file public/tilemaps/${ref.tilemapId}.json is malformed — ` +
544
+ `expected { schemaVersion, id, tileSize: {width,height}, size: {width,height}, layers: [...] } (TilemapFile schema)`);
545
+ }
546
+ }
547
+ }
452
548
  /**
453
549
  * Phaser's `load` queue is fire-and-forget; this wraps it in a promise.
454
550
  * If the loader is already idle and has nothing queued, resolves
@@ -1,5 +1,5 @@
1
1
  import Phaser from 'phaser';
2
- import { AssetRecord, TileMetadata, WorldEntity } from './types.js';
2
+ import { AssetRecord, TileMetadata, TilemapFile, WorldEntity } from './types.js';
3
3
  import { EntityRegistry } from './EntityRegistry.js';
4
4
  /**
5
5
  * Resolves an asset id to its AssetRecord, throwing a clear error if the
@@ -23,6 +23,33 @@ export interface SpawnContext {
23
23
  * group's children) can attach it to a parent container.
24
24
  */
25
25
  export declare function spawnEntity(ctx: SpawnContext, entity: WorldEntity): Phaser.GameObjects.GameObject;
26
+ /**
27
+ * Minimal rendering surface for Tilemap Studio's standalone canvas
28
+ * (tilemap-as-resource, ADR-008 / Phase 4).
29
+ *
30
+ * Renders a `TilemapFile` into a bare Phaser scene through the SAME path
31
+ * scene-spawned tilemaps use (grid sketch + one TilemapLayer per layer,
32
+ * including autotile-resolved cells, NEAREST filtering, per-tile metadata
33
+ * and tile animations) — so the Studio preview is pixel-identical to the
34
+ * in-game render. Returns the container GameObject; destroy it (plus the
35
+ * layers stashed under its `tilemapLayers` data key — they are scene-root
36
+ * siblings, not children, due to the Phaser Container/TilemapLayer
37
+ * rendering quirk) before re-rendering after an edit.
38
+ *
39
+ * Prerequisites the caller owns:
40
+ * - every tileset texture referenced by `file.layers[].tilesetIds` is
41
+ * loaded into `scene.textures` and resolvable via `resolveAsset`
42
+ * - the preview is static: the per-frame layer-position sync hook that
43
+ * scene loads install is NOT active here, so move the container only by
44
+ * re-rendering (or position it once at `position` and leave it)
45
+ *
46
+ * Deliberately NOT registered in any EntityRegistry — this is a rendering
47
+ * helper, not an entity spawn.
48
+ */
49
+ export declare function renderTilemapPreview(scene: Phaser.Scene, file: TilemapFile, resolveAsset: AssetResolver, position?: {
50
+ x: number;
51
+ y: number;
52
+ }): Phaser.GameObjects.GameObject;
26
53
  /**
27
54
  * Read the auto-tracker's recorded draw extent and set editor hit-area
28
55
  * data on the GameObject. Falls back to declared `visual.width/height`
@@ -1,12 +1,26 @@
1
1
  import Phaser from 'phaser';
2
- import { isPerFrameHitbox, } from './types.js';
3
- import { getEntityRegistry } from './EntityRegistry.js';
2
+ import { isPerFrameHitbox, tilemapFileCacheKey, } from './types.js';
3
+ import { EntityRegistry, getEntityRegistry } from './EntityRegistry.js';
4
4
  /**
5
5
  * Spawn one entity into the scene and register it. Returns the created
6
6
  * GameObject so callers (notably `spawnEntity` itself, recursing on a
7
7
  * group's children) can attach it to a parent container.
8
8
  */
9
9
  export function spawnEntity(ctx, entity) {
10
+ if (entity.kind === 'tilemap-ref') {
11
+ // Tilemap-as-resource (ADR-008, Phase 4). The ref carries only
12
+ // reference + placement; cell data comes from the standalone file at
13
+ // `public/tilemaps/{tilemapId}.json` (fetched into the JSON cache by
14
+ // SceneLoader's `loadReferencedTilemapFiles` before spawn). We
15
+ // synthesize a legacy-shaped `kind:'tilemap'` entity in memory and
16
+ // recurse — the inner call runs the UNCHANGED createTilemap rendering
17
+ // path (grid sketch, layers, autotile, tile metadata, animations) and
18
+ // tags the GameObject `entityKind:'tilemap'`, so every existing
19
+ // runtime consumer (layer-position sync, Y-sort, getTilemapAt,
20
+ // addTilemapCollider, debug overlay) works on refs with zero changes.
21
+ // Early return: the recursion already applied transform/tag/register.
22
+ return spawnTilemapRef(ctx, entity);
23
+ }
10
24
  let go;
11
25
  switch (entity.kind) {
12
26
  case 'sprite':
@@ -111,6 +125,92 @@ function createMissingPlaceholder(scene, label) {
111
125
  void label;
112
126
  return g;
113
127
  }
128
+ // --- Tilemap as a project resource (ADR-008, Phase 4) ----------------------
129
+ /**
130
+ * Build the legacy-shaped embedded entity a `tilemap-ref` + its resolved
131
+ * `TilemapFile` are equivalent to. Placement (transform / role /
132
+ * properties) comes from the ref; tile/map dimensions + layer data come
133
+ * from the file. Shared by the scene spawn path and
134
+ * {@link renderTilemapPreview}.
135
+ */
136
+ function tilemapEntityFromFile(ref, file) {
137
+ return {
138
+ id: ref.id,
139
+ kind: 'tilemap',
140
+ ...(ref.role !== undefined ? { role: ref.role } : {}),
141
+ ...(ref.properties !== undefined ? { properties: ref.properties } : {}),
142
+ transform: ref.transform,
143
+ tileSize: file.tileSize,
144
+ size: file.size,
145
+ // TilemapFileLayer is TilemapLayer minus the editor-only `locked` flag,
146
+ // so the file's layers slot straight into the legacy shape.
147
+ layers: file.layers,
148
+ };
149
+ }
150
+ /**
151
+ * Spawn a `tilemap-ref` entity by resolving its standalone file from the
152
+ * Phaser JSON cache and recursing through `spawnEntity` with a synthesized
153
+ * legacy `kind:'tilemap'` entity — the same rendering path embedded
154
+ * tilemaps use; only the data source differs.
155
+ *
156
+ * Soft-fails to the magenta placeholder when the file isn't in the cache
157
+ * (SceneLoader's `loadReferencedTilemapFiles` throws a clearer error for
158
+ * the normal scene-load path; this branch covers direct spawns, e.g.
159
+ * editor createEntity, where killing the scene would be worse).
160
+ */
161
+ function spawnTilemapRef(ctx, entity) {
162
+ const file = ctx.scene.cache.json.get(tilemapFileCacheKey(entity.tilemapId));
163
+ if (!file || !file.tileSize || !file.size || !Array.isArray(file.layers)) {
164
+ console.warn(`[umicat/scene] tilemap-ref '${entity.id}' references tilemap '${entity.tilemapId}' but ` +
165
+ `public/tilemaps/${entity.tilemapId}.json is not loaded (or malformed); rendering placeholder. ` +
166
+ `Scene loads fetch it automatically — direct spawns must load it into the JSON cache first ` +
167
+ `(key: '${tilemapFileCacheKey(entity.tilemapId)}').`);
168
+ const go = createMissingPlaceholder(ctx.scene, `tilemap: ${entity.tilemapId}?`);
169
+ applyTransform(go, entity.transform);
170
+ tagGameObject(go, entity);
171
+ ctx.registry.register(entity.id, entity.role, go);
172
+ return go;
173
+ }
174
+ const go = spawnEntity(ctx, tilemapEntityFromFile(entity, file));
175
+ // Ref marker for the editor / Tilemap Studio: the GO renders + behaves as
176
+ // entityKind 'tilemap' (runtime consumers unchanged), but hosts can read
177
+ // this data key to know the cell data lives in a standalone file.
178
+ go.setData('tilemapId', entity.tilemapId);
179
+ return go;
180
+ }
181
+ /**
182
+ * Minimal rendering surface for Tilemap Studio's standalone canvas
183
+ * (tilemap-as-resource, ADR-008 / Phase 4).
184
+ *
185
+ * Renders a `TilemapFile` into a bare Phaser scene through the SAME path
186
+ * scene-spawned tilemaps use (grid sketch + one TilemapLayer per layer,
187
+ * including autotile-resolved cells, NEAREST filtering, per-tile metadata
188
+ * and tile animations) — so the Studio preview is pixel-identical to the
189
+ * in-game render. Returns the container GameObject; destroy it (plus the
190
+ * layers stashed under its `tilemapLayers` data key — they are scene-root
191
+ * siblings, not children, due to the Phaser Container/TilemapLayer
192
+ * rendering quirk) before re-rendering after an edit.
193
+ *
194
+ * Prerequisites the caller owns:
195
+ * - every tileset texture referenced by `file.layers[].tilesetIds` is
196
+ * loaded into `scene.textures` and resolvable via `resolveAsset`
197
+ * - the preview is static: the per-frame layer-position sync hook that
198
+ * scene loads install is NOT active here, so move the container only by
199
+ * re-rendering (or position it once at `position` and leave it)
200
+ *
201
+ * Deliberately NOT registered in any EntityRegistry — this is a rendering
202
+ * helper, not an entity spawn.
203
+ */
204
+ export function renderTilemapPreview(scene, file, resolveAsset, position = { x: 0, y: 0 }) {
205
+ const entity = tilemapEntityFromFile({ id: `__tilemap-preview:${file.id}`, transform: { x: position.x, y: position.y } }, file);
206
+ // createTilemap never touches ctx.registry (registration happens in
207
+ // spawnEntity, which we bypass) — a throwaway registry satisfies the
208
+ // SpawnContext shape without clobbering a live scene registry.
209
+ const ctx = { scene, registry: new EntityRegistry(), resolveAsset };
210
+ const go = createTilemap(ctx, entity);
211
+ applyTransform(go, entity.transform);
212
+ return go;
213
+ }
114
214
  /**
115
215
  * Apply a scene entity's optional `physics` block. Only renderable entity
116
216
  * kinds (sprite / rect / circle / code-rendered) carry a body — they mirror
@@ -753,7 +753,7 @@ export interface Anchor {
753
753
  offsetX?: number;
754
754
  offsetY?: number;
755
755
  }
756
- export type WorldEntityKind = 'sprite' | 'rect' | 'circle' | 'code-rendered' | 'group' | 'tilemap' | 'trigger' | 'prefab-ref';
756
+ export type WorldEntityKind = 'sprite' | 'rect' | 'circle' | 'code-rendered' | 'group' | 'tilemap' | 'tilemap-ref' | 'trigger' | 'prefab-ref';
757
757
  export interface WorldEntityBase {
758
758
  id: string;
759
759
  /** Optional semantic tag. Behavior code keys off this. */
@@ -836,6 +836,15 @@ export interface GroupEntity extends WorldEntityBase {
836
836
  *
837
837
  * Multi-tileset per layer (`tilesetIds: string[]`) is reserved in the
838
838
  * shape for later phases but Phase A only consumes the first id.
839
+ *
840
+ * @deprecated Tilemap-as-resource (ADR-008, platform restructure Phase 4).
841
+ * Cell data now lives in standalone files at `public/tilemaps/{id}.json`
842
+ * (see {@link TilemapFile}); scenes carry only a {@link TilemapRefEntity}
843
+ * (`kind: 'tilemap-ref'`) holding the reference + placement. Embedded
844
+ * tilemap entities KEEP RENDERING so unmigrated scenes still work — do not
845
+ * author new ones. The migration script in umicat-agent-session-service
846
+ * (`scripts/migrate-tilemaps.ts`) converts embedded → file+ref per
847
+ * workspace.
839
848
  */
840
849
  export interface TilemapEntity extends WorldEntityBase {
841
850
  kind: 'tilemap';
@@ -888,6 +897,72 @@ export interface TilemapLayer {
888
897
  };
889
898
  ySort?: boolean;
890
899
  }
900
+ /**
901
+ * Scene-side reference to a standalone tilemap file. Placement comes from
902
+ * the inherited `transform` (position / depth / scale); everything else —
903
+ * tile size, map size, layers, cell data — comes from the referenced
904
+ * `public/tilemaps/{tilemapId}.json` file at load time.
905
+ *
906
+ * ```json
907
+ * { "id": "e-ground", "kind": "tilemap-ref", "tilemapId": "forest-floor",
908
+ * "role": "terrain", "transform": { "x": 640, "y": 360, "depth": 1 } }
909
+ * ```
910
+ */
911
+ export interface TilemapRefEntity extends WorldEntityBase {
912
+ kind: 'tilemap-ref';
913
+ /** Id of the tilemap file — `public/tilemaps/{tilemapId}.json`. */
914
+ tilemapId: string;
915
+ }
916
+ /**
917
+ * One layer inside a `TilemapFile`. Identical to the scene-embedded
918
+ * {@link TilemapLayer} shape minus the editor-only `locked` flag (which
919
+ * never had a runtime effect and doesn't belong in a shared resource).
920
+ */
921
+ export type TilemapFileLayer = Omit<TilemapLayer, 'locked'>;
922
+ /**
923
+ * Standalone tilemap resource file — `public/tilemaps/{id}.json`.
924
+ *
925
+ * Written by Tilemap Studio (the single human-authoring surface) and by the
926
+ * agent directly (workspace file edit — the editor paint RPC is retired).
927
+ * Read by the SDK's SceneLoader when a scene's `tilemap-ref` entity points
928
+ * at it, and rendered through the same path as the legacy embedded entity.
929
+ */
930
+ export interface TilemapFile {
931
+ schemaVersion: number;
932
+ /** Stable id; the filename is `{id}.json` and refs carry it as `tilemapId`. */
933
+ id: string;
934
+ /** Display name for pickers / Studio. Falls back to `id`. */
935
+ name?: string;
936
+ /** Render-time cell size in pixels. Layer data indexes cells at this size. */
937
+ tileSize: {
938
+ width: number;
939
+ height: number;
940
+ };
941
+ /** Map size in cells (not pixels). */
942
+ size: {
943
+ width: number;
944
+ height: number;
945
+ };
946
+ /**
947
+ * Set when a human painted this map in Tilemap Studio. The agent must
948
+ * NOT overwrite cell data in a human-curated file without asking the
949
+ * user first.
950
+ */
951
+ humanCurated?: boolean;
952
+ metadata?: {
953
+ createdAt?: string;
954
+ editedAt?: string;
955
+ /** `'user'` | `'agent'` | `'migration-v1'` (free-form). */
956
+ author?: string;
957
+ };
958
+ layers: TilemapFileLayer[];
959
+ }
960
+ /**
961
+ * Phaser JSON-cache key under which a fetched tilemap file is stored.
962
+ * Shared by the SceneLoader preload pass (write) and the `tilemap-ref`
963
+ * spawn path + any external canvas harness (read).
964
+ */
965
+ export declare function tilemapFileCacheKey(tilemapId: string): string;
891
966
  export type TriggerShape = {
892
967
  kind: 'rect';
893
968
  width: number;
@@ -929,7 +1004,7 @@ export interface PrefabRefEntity extends WorldEntityBase {
929
1004
  kind: 'prefab-ref';
930
1005
  prefabId: string;
931
1006
  }
932
- export type NonGroupWorldEntity = SpriteEntity | RectEntity | CircleEntity | CodeRenderedEntity | TilemapEntity | TriggerEntity | PrefabRefEntity;
1007
+ export type NonGroupWorldEntity = SpriteEntity | RectEntity | CircleEntity | CodeRenderedEntity | TilemapEntity | TilemapRefEntity | TriggerEntity | PrefabRefEntity;
933
1008
  export type WorldEntity = NonGroupWorldEntity | GroupEntity;
934
1009
  /**
935
1010
  * Renderable entity kinds — those that produce a single GameObject with
@@ -32,3 +32,11 @@ export function isPerFrameNinePatch(np) {
32
32
  export function isPerFrameHitbox(h) {
33
33
  return !!h && h.default != null;
34
34
  }
35
+ /**
36
+ * Phaser JSON-cache key under which a fetched tilemap file is stored.
37
+ * Shared by the SceneLoader preload pass (write) and the `tilemap-ref`
38
+ * spawn path + any external canvas harness (read).
39
+ */
40
+ export function tilemapFileCacheKey(tilemapId) {
41
+ return `umicat:tilemap:${tilemapId}`;
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Umicat Phaser 3 SDK — game infrastructure for the Umicat platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",