@umicat/phaser-sdk 1.0.8 → 1.0.10
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 +34 -2
- package/dist/editor/EditorBridge.d.ts +9 -0
- package/dist/editor/EditorBridge.js +101 -2
- package/dist/editor/EditorOverlayScene.js +7 -0
- package/dist/editor/EditorState.d.ts +8 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +9 -3
- package/dist/protocol.d.ts +26 -0
- package/dist/scene/SceneLoader.d.ts +25 -0
- package/dist/scene/SceneLoader.js +114 -3
- package/dist/scene/spawnEntity.d.ts +28 -1
- package/dist/scene/spawnEntity.js +102 -2
- package/dist/scene/types.d.ts +77 -2
- package/dist/scene/types.js +8 -0
- package/package.json +1 -1
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
|
|
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` → `
|
|
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,
|
|
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';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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).
|
|
@@ -286,6 +310,21 @@ function queueAssetLoad(scene, asset) {
|
|
|
286
310
|
}
|
|
287
311
|
: null);
|
|
288
312
|
if (!cfg) {
|
|
313
|
+
// A TILESET asset can be modality 'spritesheet' yet carry no
|
|
314
|
+
// spriteSheetConfig — it's sliced by `tileset.cellSize` in
|
|
315
|
+
// `createTilemap`'s `addTilesetImage`, never consumed as Phaser
|
|
316
|
+
// animation frames. Load it as a plain image so tilemap layers
|
|
317
|
+
// referencing it actually render. Mirrors the editor's
|
|
318
|
+
// `loadAssetIntoScene` fallback (EditorBridge) — without this the
|
|
319
|
+
// two load paths diverge: a tilemap-ref renders live in the editor
|
|
320
|
+
// but its layer vanishes after save+reload because scene-load
|
|
321
|
+
// skipped the texture. (Host now writes tilesets as kind:'image',
|
|
322
|
+
// but this keeps agent-authored / legacy spritesheet-kind tilesets
|
|
323
|
+
// robust too.)
|
|
324
|
+
if (asset.tileset) {
|
|
325
|
+
scene.load.image(asset.textureKey, asset.path);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
289
328
|
// eslint-disable-next-line no-console
|
|
290
329
|
console.warn(`[umicat/scene] asset '${asset.id}' kind=spritesheet missing spriteSheetConfig (and no top-level frameWidth/frameHeight); skipping load`);
|
|
291
330
|
return;
|
|
@@ -386,6 +425,12 @@ async function loadWorldSceneImpl(scene, sceneId, options) {
|
|
|
386
425
|
// entities spawn. Scene-level `world.physics.gravity` wins over the
|
|
387
426
|
// manifest-wide `globals.physics.gravity`.
|
|
388
427
|
applyWorldGravity(scene, sceneFile, manifest);
|
|
428
|
+
// Tilemap-as-resource (ADR-008, Phase 4): resolve `tilemap-ref` entities
|
|
429
|
+
// by fetching their standalone `public/tilemaps/{id}.json` files FIRST —
|
|
430
|
+
// the asset preload below reads each file's layers to know which tileset
|
|
431
|
+
// textures to queue. Files land in the Phaser JSON cache (keyed by
|
|
432
|
+
// `tilemapFileCacheKey`) so repeat loads of the same scene are free.
|
|
433
|
+
await loadReferencedTilemapFiles(scene, sceneFile);
|
|
389
434
|
// Lazy preload of any new assets this scene needs, then await loader.
|
|
390
435
|
preloadSceneAssets(scene, sceneFile, manifest);
|
|
391
436
|
await runLoader(scene);
|
|
@@ -449,6 +494,72 @@ async function loadSceneJson(scene, ref) {
|
|
|
449
494
|
await runLoader(scene);
|
|
450
495
|
return scene.cache.json.get(cacheKey);
|
|
451
496
|
}
|
|
497
|
+
/** Recursively collect every `tilemap-ref` entity (groups walked too). */
|
|
498
|
+
function collectTilemapRefs(entities) {
|
|
499
|
+
const refs = [];
|
|
500
|
+
function walk(e) {
|
|
501
|
+
if (e.kind === 'tilemap-ref') {
|
|
502
|
+
refs.push(e);
|
|
503
|
+
}
|
|
504
|
+
else if (e.kind === 'group') {
|
|
505
|
+
for (const child of e.children)
|
|
506
|
+
walk(child);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const e of entities)
|
|
510
|
+
walk(e);
|
|
511
|
+
return refs;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Fetch every standalone tilemap file referenced by the scene's
|
|
515
|
+
* `tilemap-ref` entities into the Phaser JSON cache (tilemap-as-resource,
|
|
516
|
+
* ADR-008 / Phase 4). Idempotent — files already in the cache aren't
|
|
517
|
+
* re-fetched, so scene transitions back to a tilemap-using scene are free.
|
|
518
|
+
*
|
|
519
|
+
* Fails LOUDLY with an actionable message when a referenced file is
|
|
520
|
+
* missing or malformed: a tilemap-ref without its file would render
|
|
521
|
+
* nothing, and the silent variant of that bug ("my ground disappeared")
|
|
522
|
+
* is much harder to diagnose than a thrown scene-load error naming the
|
|
523
|
+
* exact expected path. The spawn path additionally soft-fails (magenta
|
|
524
|
+
* placeholder) for refs spawned outside this loader, e.g. editor
|
|
525
|
+
* createEntity.
|
|
526
|
+
*/
|
|
527
|
+
export async function loadReferencedTilemapFiles(scene, sceneFile) {
|
|
528
|
+
const refs = collectTilemapRefs(sceneFile.entities);
|
|
529
|
+
if (refs.length === 0)
|
|
530
|
+
return;
|
|
531
|
+
const queuedIds = new Set();
|
|
532
|
+
for (const ref of refs) {
|
|
533
|
+
if (queuedIds.has(ref.tilemapId))
|
|
534
|
+
continue;
|
|
535
|
+
queuedIds.add(ref.tilemapId);
|
|
536
|
+
const cacheKey = tilemapFileCacheKey(ref.tilemapId);
|
|
537
|
+
if (scene.cache.json.exists(cacheKey))
|
|
538
|
+
continue;
|
|
539
|
+
scene.load.json(cacheKey, tilemapFilePath(ref.tilemapId));
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
await runLoader(scene);
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
546
|
+
throw new Error(`[umicat/scene] scene '${sceneFile.id}' references standalone tilemap file(s) that failed to load (${detail}). ` +
|
|
547
|
+
`tilemap-ref entities expect a file at public/tilemaps/<tilemapId>.json — ` +
|
|
548
|
+
`referenced ids: ${Array.from(queuedIds).join(', ')}`);
|
|
549
|
+
}
|
|
550
|
+
// Validate every ref resolved to a structurally-plausible TilemapFile.
|
|
551
|
+
for (const ref of refs) {
|
|
552
|
+
const file = scene.cache.json.get(tilemapFileCacheKey(ref.tilemapId));
|
|
553
|
+
if (!file) {
|
|
554
|
+
throw new Error(`[umicat/scene] entity '${ref.id}' (kind=tilemap-ref) points at tilemap '${ref.tilemapId}' ` +
|
|
555
|
+
`but public/tilemaps/${ref.tilemapId}.json did not load — create the file or remove the ref`);
|
|
556
|
+
}
|
|
557
|
+
if (!file.tileSize || !file.size || !Array.isArray(file.layers)) {
|
|
558
|
+
throw new Error(`[umicat/scene] tilemap file public/tilemaps/${ref.tilemapId}.json is malformed — ` +
|
|
559
|
+
`expected { schemaVersion, id, tileSize: {width,height}, size: {width,height}, layers: [...] } (TilemapFile schema)`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
452
563
|
/**
|
|
453
564
|
* Phaser's `load` queue is fire-and-forget; this wraps it in a promise.
|
|
454
565
|
* 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
|
package/dist/scene/types.d.ts
CHANGED
|
@@ -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
|
package/dist/scene/types.js
CHANGED
|
@@ -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
|
+
}
|