@umicat/phaser-sdk 1.0.6 → 1.0.8

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.
@@ -11,7 +11,7 @@ import { resolveRenderScript } from '../scene/renderScripts.js';
11
11
  import { getManifest } from '../scene/SceneLoader.js';
12
12
  import { getRules, patchRule } from '../scene/Rules.js';
13
13
  import { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './EditorOverlayScene.js';
14
- import { getEditorState, setEditorActive, setSelection, getSelection, getEditorMode, setEditorMode, setDebugOverlayState, setTilemapToolState, } from './EditorState.js';
14
+ import { getEditorState, setEditorActive, setSelection, getSelection, getEditorMode, setEditorMode, getActiveSceneId, setActiveSceneId, setDebugOverlayState, setTilemapToolState, } from './EditorState.js';
15
15
  import { applyHudPatch, createHudEntityInScene, deleteHudEntityFromScene, findHudRegistry, findHudSceneFile, UMICAT_HUD_SCENE_KEY, } from '../scene/HudRuntime.js';
16
16
  /**
17
17
  * EditorBridge wires the host (home-ui) to the iframe's Phaser game during
@@ -126,7 +126,67 @@ function handleMessage(game, msg) {
126
126
  // load when manifestAsset present); fire-and-forget the promise.
127
127
  void handleEditTilemap(game, msg.entityId, msg.ops, msg.manifestAsset);
128
128
  break;
129
+ case 'umicat:editor:loadScene':
130
+ // Scene browser (design 01 §6.1) — switch to a different world scene.
131
+ handleLoadScene(game, msg.sceneId, msg.sceneFile, msg.manifest);
132
+ break;
133
+ case 'umicat:editor:updateManifest':
134
+ // Scene browser — non-switch structural op (create / rename / reorder
135
+ // / folders / set-initial) refreshed the manifest on disk; mirror it
136
+ // into the iframe's cache so it never goes stale.
137
+ handleUpdateManifest(game, msg.manifest);
138
+ break;
139
+ }
140
+ }
141
+ // --- Scene browser (design 01 §6.1) ----------------------------------------
142
+ /**
143
+ * Replace the game-global JSON cache entry under `key` with `value`. The
144
+ * remove + add pair changes object identity, which is what makes
145
+ * SceneLoader's `getOrInitState` rebuild its assetsById index on the next
146
+ * `getManifest` call.
147
+ */
148
+ function replaceJsonCacheEntry(game, key, value) {
149
+ if (game.cache.json.exists(key))
150
+ game.cache.json.remove(key);
151
+ game.cache.json.add(key, value);
152
+ }
153
+ function handleLoadScene(game, sceneId, sceneFile, manifest) {
154
+ if (manifest)
155
+ replaceJsonCacheEntry(game, 'umicat:manifest', manifest);
156
+ if (sceneFile)
157
+ replaceJsonCacheEntry(game, `umicat:scene:${sceneId}`, sceneFile);
158
+ setActiveSceneId(game, sceneId);
159
+ setSelection(game, null);
160
+ // Allow the post-restart enterEdit to post a fresh snapshot — that
161
+ // snapshot (a new `sceneLoaded`) is the host's switch ACK.
162
+ delete game[SNAPSHOT_POSTED_FLAG];
163
+ const scene = findWorldScene(game);
164
+ if (!scene) {
165
+ // Boot race — no world scene yet. The activeSceneId is set; when the
166
+ // scene boots it loads whatever the template's init data says, and the
167
+ // PRE_STEP hook completes editor install. Rare enough to warn.
168
+ // eslint-disable-next-line no-console
169
+ console.warn('[umicat/editor] loadScene: no world scene to restart yet');
170
+ return;
129
171
  }
172
+ // Tear down editor chrome BEFORE the restart. The restart destroys the
173
+ // scene's cameras, but the game-bag EDITOR_CAM_KEY ref would go stale
174
+ // (still truthy) — the PRE_STEP late-boot hook checks `!getEditorCamera`
175
+ // and would never re-run enterEdit. Uninstalling clears the key so the
176
+ // hook fires once the scene rebuilds, exactly like the boot-flag path.
177
+ uninstallVoidFill(game);
178
+ uninstallEditorCameras(game);
179
+ // Restart with the new sceneId. The template's GameScene.init({ sceneId })
180
+ // accepts this; loadWorldScene then reads the scene JSON we just cached.
181
+ // The PRE_STEP late-boot hook (1.0.6) re-runs the idempotent enterEdit
182
+ // once the scene rebuilds, re-installing editor chrome and posting the
183
+ // fresh sceneLoaded.
184
+ scene.scene.restart({ sceneId });
185
+ }
186
+ function handleUpdateManifest(game, manifest) {
187
+ if (!manifest)
188
+ return;
189
+ replaceJsonCacheEntry(game, 'umicat:manifest', manifest);
130
190
  }
131
191
  // --- Enter / exit ---------------------------------------------------------
132
192
  function enterEdit(game) {
@@ -423,6 +483,10 @@ function postSceneSnapshot(game) {
423
483
  manifest,
424
484
  });
425
485
  game[SNAPSHOT_POSTED_FLAG] = sceneFile.id;
486
+ // Scene browser — pin the active-scene tracker on the boot snapshot so
487
+ // readActiveSceneFile stays deterministic after later scene switches.
488
+ if (!getActiveSceneId(game))
489
+ setActiveSceneId(game, sceneFile.id);
426
490
  }
427
491
  // Also post the HUD scene if attached + loaded. The host stashes both
428
492
  // snapshots so the World/HUD toggle is purely a UI switch — no fresh
@@ -631,9 +695,19 @@ function hasPostedSnapshot(game) {
631
695
  return !!game[SNAPSHOT_POSTED_FLAG];
632
696
  }
633
697
  function readActiveSceneFile(game) {
698
+ // Scene browser — once two scenes have been visited, the JSON cache holds
699
+ // multiple `umicat:scene:*` entries and the scan below becomes ambiguous.
700
+ // Prefer the exact entry for the tracked active scene when set.
701
+ const activeId = getActiveSceneId(game);
702
+ if (activeId) {
703
+ const exact = game.cache.json.get(`umicat:scene:${activeId}`);
704
+ if (isWorldScene(exact))
705
+ return exact;
706
+ }
634
707
  // The active world scene cached its scene file in Phaser's JSON cache via
635
708
  // SceneLoader. Pull it from there. We pick the first non-Boot scene we
636
- // find with a JSON entry matching `umicat:scene:<id>`.
709
+ // find with a JSON entry matching `umicat:scene:<id>`. (Boot path / old
710
+ // hosts — activeSceneId not yet set.)
637
711
  for (const scene of game.scene.getScenes(false)) {
638
712
  const key = scene.scene.key;
639
713
  if (BOOT_SCENE_KEYS.has(key))
@@ -1972,7 +2046,16 @@ function restoreHudCamera(game) {
1972
2046
  * `postCameraState`, and the overlay scene's mirror in `create`.
1973
2047
  */
1974
2048
  export function getEditorCamera(game) {
1975
- return (game[EDITOR_CAM_KEY] ?? null);
2049
+ const cam = game[EDITOR_CAM_KEY] ?? null;
2050
+ // Scene browser — a scene restart (loadScene switch) destroys the scene's
2051
+ // cameras but leaves this game-bag ref dangling. Phaser's BaseCamera
2052
+ // .destroy() nulls `cam.scene`; treat such a camera as absent and clear
2053
+ // the stale key so the PRE_STEP late-boot hook can re-run enterEdit.
2054
+ if (cam && !cam.scene) {
2055
+ delete game[EDITOR_CAM_KEY];
2056
+ return null;
2057
+ }
2058
+ return cam;
1976
2059
  }
1977
2060
  /**
1978
2061
  * Toggle HUD scene rendering. HUD widgets are canvas-relative (anchor-
@@ -62,6 +62,14 @@ interface EditorStateShape {
62
62
  active: boolean;
63
63
  selectedId: string | null;
64
64
  mode: EditorMode;
65
+ /**
66
+ * Scene browser (design 01 §6.1) — which world scene the editor is
67
+ * currently editing. Set by `umicat:editor:loadScene` and by the first
68
+ * scene snapshot. Once two scenes have been visited, the JSON cache holds
69
+ * multiple `umicat:scene:*` entries — this disambiguates which one
70
+ * `readActiveSceneFile` should return.
71
+ */
72
+ activeSceneId: string | null;
65
73
  /**
66
74
  * Drag-in-progress info. While set, the overlay scene mutates the entity's
67
75
  * x/y directly per pointermove without going through the host. On
@@ -219,6 +227,8 @@ export declare function getEditorState(game: AnyObject): EditorStateShape;
219
227
  export declare function setEditorActive(game: AnyObject, active: boolean): void;
220
228
  export declare function getEditorMode(game: AnyObject): EditorMode;
221
229
  export declare function setEditorMode(game: AnyObject, mode: EditorMode): void;
230
+ export declare function getActiveSceneId(game: AnyObject): string | null;
231
+ export declare function setActiveSceneId(game: AnyObject, id: string | null): void;
222
232
  export declare function setSelection(game: AnyObject, id: string | null): void;
223
233
  export declare function getSelection(game: AnyObject): string | null;
224
234
  export declare function startDrag(game: AnyObject, entityId: string, startWorld: {
@@ -19,6 +19,7 @@ export function getEditorState(game) {
19
19
  active: false,
20
20
  selectedId: null,
21
21
  mode: 'world',
22
+ activeSceneId: null,
22
23
  drag: null,
23
24
  debugOverlay: { showHitboxes: false },
24
25
  tilemap: {
@@ -45,6 +46,12 @@ export function getEditorMode(game) {
45
46
  export function setEditorMode(game, mode) {
46
47
  getEditorState(game).mode = mode;
47
48
  }
49
+ export function getActiveSceneId(game) {
50
+ return getEditorState(game).activeSceneId;
51
+ }
52
+ export function setActiveSceneId(game, id) {
53
+ getEditorState(game).activeSceneId = id;
54
+ }
48
55
  export function setSelection(game, id) {
49
56
  getEditorState(game).selectedId = id;
50
57
  }
package/dist/index.d.ts CHANGED
@@ -39,5 +39,5 @@ 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
41
  export { SCHEMA_VERSION, isPerFrameNinePatch, isPerFrameHitbox, } from './scene/types.js';
42
- export type { Manifest, 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, TriggerShape, HudEntity, HudEntityKind, HudEntityBase, HudTextEntity, HudImageEntity, HudIconButtonEntity, HudProgressBarEntity, HudPanelEntity, HudTextSource, HudNumberSource, HudLayer, Transform, Anchor, AnchorSide, NinePatchConfig, NinePatchPerFrame, HitboxRect, HitboxPerFrame, DepthAnchor, } 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';
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';
@@ -580,7 +580,37 @@ export type TilemapEditOp = {
580
580
  y: number;
581
581
  };
582
582
  };
583
- export type EditorHostToSdkMessage = EditorEnterMessage | EditorExitMessage | EditorGetSceneMessage | EditorApplyEditMessage | EditorSetSelectionMessage | EditorPanZoomMessage | EditorCreateEntityMessage | EditorDeleteEntityMessage | EditorSetEditModeMessage | EditorAssetUpdateMessage | EditorSetDebugOverlayMessage | EditorEditPrefabMessage | EditorPatchRuleMessage | EditorSetTilemapToolMessage | EditorEditTilemapMessage;
583
+ /**
584
+ * Switch the editor to a different world scene (scene browser — design 01
585
+ * §6.1). The iframe loads scene JSON from the deployed dist, NOT the
586
+ * workspace, so the host pushes the current on-disk truth here (fetched via
587
+ * the session-server's `request_workspace_file`) instead of letting the
588
+ * iframe fetch a stale copy.
589
+ *
590
+ * The bridge overwrites the Phaser JSON cache entries, then restarts the
591
+ * world scene with `{ sceneId }` init data. The restart destroys the editor
592
+ * camera; the PRE_STEP late-boot-completion hook (1.0.6) detects that and
593
+ * re-runs the idempotent `enterEdit`, which re-installs editor chrome and
594
+ * posts a fresh `sceneLoaded` — that snapshot is the host's switch ACK.
595
+ */
596
+ export interface EditorLoadSceneMessage {
597
+ type: 'umicat:editor:loadScene';
598
+ sceneId: string;
599
+ /** Scene file content (workspace truth). When present, replaces the cached copy. */
600
+ sceneFile?: unknown;
601
+ /** Updated manifest (new scenes / groups / initialScene). Replaces cache entry. */
602
+ manifest?: unknown;
603
+ }
604
+ /**
605
+ * Replace the iframe's cached manifest without a scene switch — used after
606
+ * structural scene ops (create / rename / reorder / folders / set-initial)
607
+ * so the cached copy never goes stale relative to the workspace.
608
+ */
609
+ export interface EditorUpdateManifestMessage {
610
+ type: 'umicat:editor:updateManifest';
611
+ manifest: unknown;
612
+ }
613
+ export type EditorHostToSdkMessage = EditorEnterMessage | EditorExitMessage | EditorGetSceneMessage | EditorApplyEditMessage | EditorSetSelectionMessage | EditorPanZoomMessage | EditorCreateEntityMessage | EditorDeleteEntityMessage | EditorSetEditModeMessage | EditorAssetUpdateMessage | EditorSetDebugOverlayMessage | EditorEditPrefabMessage | EditorPatchRuleMessage | EditorSetTilemapToolMessage | EditorEditTilemapMessage | EditorLoadSceneMessage | EditorUpdateManifestMessage;
584
614
  export interface EditorSceneLoadedMessage {
585
615
  type: 'umicat:editor:sceneLoaded';
586
616
  sceneId: string;
@@ -78,7 +78,7 @@ export function preloadSceneAssets(scene, sceneFile, manifest) {
78
78
  if (sceneFile.type !== 'world')
79
79
  return; // HUD slice will add its own walker
80
80
  const state = getOrInitState(scene, manifest);
81
- const ids = collectAssetIds(sceneFile.entities);
81
+ const ids = collectAssetIds(sceneFile.entities, manifest);
82
82
  for (const id of ids) {
83
83
  if (state.requestedAssetIds.has(id))
84
84
  continue;
@@ -98,7 +98,7 @@ export function preloadSceneAssets(scene, sceneFile, manifest) {
98
98
  state.requestedAssetIds.add(id);
99
99
  }
100
100
  }
101
- function collectAssetIds(entities) {
101
+ function collectAssetIds(entities, manifest) {
102
102
  const ids = new Set();
103
103
  function walk(e) {
104
104
  if (e.kind === 'sprite') {
@@ -119,6 +119,14 @@ function collectAssetIds(entities) {
119
119
  ids.add(id);
120
120
  }
121
121
  }
122
+ else if (e.kind === 'prefab-ref') {
123
+ // Authored prefab instance — preload whatever the prefab's visual
124
+ // references (sprite prefabs carry an assetId at the record root).
125
+ const prefab = manifest?.prefabs?.find((p) => p.id === e.prefabId);
126
+ const assetId = prefab?.assetId;
127
+ if (typeof assetId === 'string')
128
+ ids.add(assetId);
129
+ }
122
130
  // rect / circle / code-rendered / trigger don't reference assets.
123
131
  }
124
132
  for (const e of entities)
@@ -30,6 +30,14 @@ export function spawnEntity(ctx, entity) {
30
30
  case 'trigger':
31
31
  go = createTriggerStub(ctx, entity);
32
32
  break;
33
+ case 'prefab-ref':
34
+ // Authored prefab INSTANCE (editor "Convert to Type" / drag-from-Types,
35
+ // 2026-06-12). Resolve the prefab record, build a synthetic entity at
36
+ // this instance's transform, and recurse — the inner call creates the
37
+ // GameObject, applies the prefab's physics, tags, and registers under
38
+ // THIS entity's id. We then re-tag kind + prefabId below.
39
+ go = createPrefabRefInstance(ctx, entity);
40
+ break;
33
41
  default: {
34
42
  const exhaustive = entity;
35
43
  throw new Error(`[umicat/scene] unknown entity kind: ${JSON.stringify(exhaustive)}`);
@@ -38,9 +46,71 @@ export function spawnEntity(ctx, entity) {
38
46
  applyTransform(go, entity.transform);
39
47
  tagGameObject(go, entity);
40
48
  applyEntityPhysicsIfDeclared(ctx.scene, go, entity);
49
+ if (entity.kind === 'prefab-ref') {
50
+ // Tag for editor live-edit: `umicat:editor:editPrefab` walks instances
51
+ // via this data key, and canvas clicks route to the Prefab inspector
52
+ // (editing one instance = editing the type). Registry re-registers with
53
+ // the prefabId so byPrefabId() finds authored instances too.
54
+ go.setData('entityPrefabId', entity.prefabId);
55
+ ctx.registry.unregister(entity.id);
56
+ ctx.registry.register(entity.id, entity.role ?? prefabRoleOf(ctx.scene, entity.prefabId), go, entity.prefabId);
57
+ return go;
58
+ }
41
59
  ctx.registry.register(entity.id, entity.role, go);
42
60
  return go;
43
61
  }
62
+ /**
63
+ * Read a prefab record straight from the cached manifest. Deliberately NOT
64
+ * importing SceneLoader's getManifest — that module imports this one, and
65
+ * keeping the read inline avoids the import cycle.
66
+ */
67
+ function lookupPrefab(scene, prefabId) {
68
+ const manifest = scene.cache.json.get('umicat:manifest');
69
+ const prefab = manifest?.prefabs?.find((p) => p.id === prefabId);
70
+ return prefab ?? null;
71
+ }
72
+ function prefabRoleOf(scene, prefabId) {
73
+ const prefab = lookupPrefab(scene, prefabId);
74
+ return prefab?.role ?? undefined;
75
+ }
76
+ function createPrefabRefInstance(ctx, entity) {
77
+ const prefab = lookupPrefab(ctx.scene, entity.prefabId);
78
+ if (!prefab) {
79
+ console.warn(`[umicat/scene] prefab-ref '${entity.id}' references prefab '${entity.prefabId}' but the manifest has no such prefab; rendering placeholder`);
80
+ return createMissingPlaceholder(ctx.scene, `prefab: ${entity.prefabId}?`);
81
+ }
82
+ // Synthetic entity = prefab fields + this instance's id/transform. Instance
83
+ // role/properties override the prefab's when set (per-instance variance
84
+ // without breaking the type link).
85
+ const synthetic = {
86
+ ...prefab,
87
+ id: entity.id,
88
+ transform: entity.transform,
89
+ ...(entity.role !== undefined ? { role: entity.role } : {}),
90
+ ...(entity.properties !== undefined
91
+ ? { properties: { ...(prefab.properties ?? {}), ...entity.properties } }
92
+ : {}),
93
+ };
94
+ if (synthetic.kind === 'prefab-ref') {
95
+ // A prefab record can never be kind prefab-ref, but guard against a
96
+ // hand-edited manifest producing infinite recursion.
97
+ console.warn(`[umicat/scene] prefab '${entity.prefabId}' has invalid kind 'prefab-ref'`);
98
+ return createMissingPlaceholder(ctx.scene, `prefab: ${entity.prefabId}?`);
99
+ }
100
+ return spawnEntity(ctx, synthetic);
101
+ }
102
+ /** Magenta-bordered "?" placeholder — same soft-fail pattern as missing assets. */
103
+ function createMissingPlaceholder(scene, label) {
104
+ const g = scene.add.graphics();
105
+ g.lineStyle(2, 0xff00ff, 1);
106
+ g.strokeRect(-32, -32, 64, 64);
107
+ g.lineBetween(-32, -32, 32, 32);
108
+ g.lineBetween(-32, 32, 32, -32);
109
+ g.setData('editorHitWidth', 64);
110
+ g.setData('editorHitHeight', 64);
111
+ void label;
112
+ return g;
113
+ }
44
114
  /**
45
115
  * Apply a scene entity's optional `physics` block. Only renderable entity
46
116
  * kinds (sprite / rect / circle / code-rendered) carry a body — they mirror
@@ -11,12 +11,28 @@ export declare const SCHEMA_VERSION = 1;
11
11
  export type SceneType = 'world' | 'hud';
12
12
  export interface SceneRef {
13
13
  id: string;
14
+ /**
15
+ * Display name shown in the editor's scene browser (design 01 §6.1).
16
+ * Denormalized copy of the scene file's `name`; kept in sync by the
17
+ * editor's rename op. Optional — display falls back to `id`.
18
+ */
19
+ name?: string;
14
20
  type: SceneType;
15
21
  /** Path under `public/scenes/`, e.g. `"world/level-1-1.json"`. */
16
22
  file: string;
17
23
  /** Optional HUD scene id to overlay (world scenes only). Null = no HUD. */
18
24
  hud?: string | null;
19
25
  }
26
+ /**
27
+ * Editor-only scene folder (scene browser — design 03 §3.1's "loose
28
+ * enhancement"). Groups scenes for display; the runtime ignores it.
29
+ * Scenes not in any group render below a separator in `scenes[]` order.
30
+ */
31
+ export interface ManifestGroup {
32
+ id: string;
33
+ name: string;
34
+ sceneIds: string[];
35
+ }
20
36
  export interface HudRef {
21
37
  id: string;
22
38
  /** Path under `public/scenes/`, e.g. `"hud/game-hud.json"`. */
@@ -526,6 +542,11 @@ export interface Manifest {
526
542
  * spawning) can omit this field.
527
543
  */
528
544
  prefabs?: PrefabRecord[];
545
+ /**
546
+ * Editor-only scene folders (scene browser). Optional + purely additive —
547
+ * runtime never reads it. See {@link ManifestGroup}.
548
+ */
549
+ groups?: ManifestGroup[];
529
550
  globals?: {
530
551
  physics?: {
531
552
  gravity?: {
@@ -732,7 +753,7 @@ export interface Anchor {
732
753
  offsetX?: number;
733
754
  offsetY?: number;
734
755
  }
735
- export type WorldEntityKind = 'sprite' | 'rect' | 'circle' | 'code-rendered' | 'group' | 'tilemap' | 'trigger';
756
+ export type WorldEntityKind = 'sprite' | 'rect' | 'circle' | 'code-rendered' | 'group' | 'tilemap' | 'trigger' | 'prefab-ref';
736
757
  export interface WorldEntityBase {
737
758
  id: string;
738
759
  /** Optional semantic tag. Behavior code keys off this. */
@@ -892,7 +913,23 @@ export interface TriggerEntity extends WorldEntityBase {
892
913
  /** Optional preset name (open-door / damage-zone / scene-transition / spawner / heal). */
893
914
  preset?: string;
894
915
  }
895
- export type NonGroupWorldEntity = SpriteEntity | RectEntity | CircleEntity | CodeRenderedEntity | TilemapEntity | TriggerEntity;
916
+ /**
917
+ * Authored INSTANCE of a prefab — the Figma component/instance model
918
+ * brought to scene data (editor "Convert to Type" + drag-from-Types,
919
+ * 2026-06-12). The scene stores only "which type + where"; every render /
920
+ * physics / properties field comes from the prefab record at spawn time,
921
+ * so editing the prefab updates every placed instance across all scenes.
922
+ *
923
+ * Unlike `spawnPrefab` runtime instances (`<prefabId>#<n>` ids, created by
924
+ * behavior code), prefab-refs are scene-file entities: they appear in the
925
+ * editor Hierarchy, are draggable (transform lives here), and persist.
926
+ * `role` defaults to the prefab's role; an explicit role here overrides.
927
+ */
928
+ export interface PrefabRefEntity extends WorldEntityBase {
929
+ kind: 'prefab-ref';
930
+ prefabId: string;
931
+ }
932
+ export type NonGroupWorldEntity = SpriteEntity | RectEntity | CircleEntity | CodeRenderedEntity | TilemapEntity | TriggerEntity | PrefabRefEntity;
896
933
  export type WorldEntity = NonGroupWorldEntity | GroupEntity;
897
934
  /**
898
935
  * Renderable entity kinds — those that produce a single GameObject with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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",