@umicat/phaser-sdk 1.0.5 → 1.0.7

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) {
@@ -233,6 +293,17 @@ function installPauseEnforcement(game) {
233
293
  if (!getEditorState(game).active)
234
294
  return;
235
295
  pauseActiveNonEditor(game);
296
+ // Late-boot completion (1.0.6). The boot-flag path (`umicatEdit=1`)
297
+ // runs enterEdit BEFORE any scene exists — the scene-dependent
298
+ // installs (editor cameras, canvas expand, void fill, overlay launch)
299
+ // all no-op'd, leaving an editor that is paused and snapshotted but
300
+ // can't pan/zoom or pick. Once the world scene appears, re-run the
301
+ // full idempotent enterEdit to install them. The editor-camera check
302
+ // makes this fire at most once per enter: after a successful install
303
+ // it's permanently truthy (cheap per-frame no-op otherwise).
304
+ if (!getEditorCamera(game) && findWorldScene(game)) {
305
+ enterEdit(game);
306
+ }
236
307
  };
237
308
  game.events.on(Phaser.Core.Events.PRE_STEP, handler);
238
309
  bag[PAUSE_ENFORCER_FLAG] = handler;
@@ -412,6 +483,10 @@ function postSceneSnapshot(game) {
412
483
  manifest,
413
484
  });
414
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);
415
490
  }
416
491
  // Also post the HUD scene if attached + loaded. The host stashes both
417
492
  // snapshots so the World/HUD toggle is purely a UI switch — no fresh
@@ -620,9 +695,19 @@ function hasPostedSnapshot(game) {
620
695
  return !!game[SNAPSHOT_POSTED_FLAG];
621
696
  }
622
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
+ }
623
707
  // The active world scene cached its scene file in Phaser's JSON cache via
624
708
  // SceneLoader. Pull it from there. We pick the first non-Boot scene we
625
- // 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.)
626
711
  for (const scene of game.scene.getScenes(false)) {
627
712
  const key = scene.scene.key;
628
713
  if (BOOT_SCENE_KEYS.has(key))
@@ -1961,7 +2046,16 @@ function restoreHudCamera(game) {
1961
2046
  * `postCameraState`, and the overlay scene's mirror in `create`.
1962
2047
  */
1963
2048
  export function getEditorCamera(game) {
1964
- 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;
1965
2059
  }
1966
2060
  /**
1967
2061
  * Toggle HUD scene rendering. HUD widgets are canvas-relative (anchor-
@@ -304,20 +304,6 @@ export declare class EditorOverlayScene extends Phaser.Scene {
304
304
  * replays the `before` patch through the normal applyEdit path.
305
305
  */
306
306
  private commitEntityResize;
307
- /**
308
- * Render the CORNER resize handles around the selected rect entity —
309
- * Figma convention (2026-06-11): no edge-midpoint squares. On a small
310
- * entity 8 handles visually swallowed the rect (a 3×3 grid of squares
311
- * with the entity peeking through the middle). Edges stay fully
312
- * resizable — the entire selection edge is a hit target and the cursor
313
- * flips to ns/ew-resize on hover, which is affordance enough. No bounds
314
- * outline (the blue selection rect already draws it) and no ghost rect
315
- * (the live GO resizes in place during the drag).
316
- *
317
- * Visual is 10px; the hit target stays 14px (see hitTestResizeHandlesAt)
318
- * — bigger-than-visible click zones are standard.
319
- */
320
- private drawEntityResizeOverlay;
321
307
  update(): void;
322
308
  /**
323
309
  * Render the 8 resize handles (when paint mode is active) and the
@@ -1691,36 +1691,12 @@ export class EditorOverlayScene extends Phaser.Scene {
1691
1691
  // The selection rect changed size — refresh the host's anchor rect.
1692
1692
  postSelectionRect(this.game);
1693
1693
  }
1694
- /**
1695
- * Render the CORNER resize handles around the selected rect entity
1696
- * Figma convention (2026-06-11): no edge-midpoint squares. On a small
1697
- * entity 8 handles visually swallowed the rect (a 3×3 grid of squares
1698
- * with the entity peeking through the middle). Edges stay fully
1699
- * resizable the entire selection edge is a hit target and the cursor
1700
- * flips to ns/ew-resize on hover, which is affordance enough. No bounds
1701
- * outline (the blue selection rect already draws it) and no ghost rect
1702
- * (the live GO resizes in place during the drag).
1703
- *
1704
- * Visual is 10px; the hit target stays 14px (see hitTestResizeHandlesAt)
1705
- * — bigger-than-visible click zones are standard.
1706
- */
1707
- drawEntityResizeOverlay() {
1708
- const info = this.entityResizeHandlePositions();
1709
- if (!info)
1710
- return;
1711
- const cam = this.findActiveEditorCamera();
1712
- const zoom = cam?.zoom ?? 1;
1713
- const handleSize = 10 / zoom; // 10px on screen regardless of zoom
1714
- const half = handleSize / 2;
1715
- for (const h of info.handles) {
1716
- if (h.id !== 'nw' && h.id !== 'ne' && h.id !== 'sw' && h.id !== 'se')
1717
- continue;
1718
- this.graphics.fillStyle(0xffffff, 1);
1719
- this.graphics.fillRect(h.x - half, h.y - half, handleSize, handleSize);
1720
- this.graphics.fillStyle(SELECTION_COLOR, 1);
1721
- this.graphics.fillRect(h.x - half + 1.5 / zoom, h.y - half + 1.5 / zoom, handleSize - 3 / zoom, handleSize - 3 / zoom);
1722
- }
1723
- }
1694
+ // Rect entities draw NO handle squares at all (2026-06-11, user decision
1695
+ // after comparing with Figma) the blue selection outline is the whole
1696
+ // visual. Resizability is communicated by the hover cursor: ns/ew-resize
1697
+ // along the edges, nwse/nesw-resize in the corner zones. Hit targets are
1698
+ // unchanged (entityResizeHandlePositions + hitTestResizeHandlesAt still
1699
+ // serve all 8 zones for the pointer logic).
1724
1700
  update() {
1725
1701
  this.graphics.clear();
1726
1702
  // Slice 6 Phase B (fix): sync tilemap layer world positions to their
@@ -1844,9 +1820,9 @@ export class EditorOverlayScene extends Phaser.Scene {
1844
1820
  // host Inspector). Ghost rect preview shows the new bounds during
1845
1821
  // an active resize drag.
1846
1822
  this.drawTilemapResizeOverlay();
1847
- // Rect-entity resize handles render whenever a rect entity is
1848
- // selected (2026-06-10).
1849
- this.drawEntityResizeOverlay();
1823
+ // (Rect-entity resize handles draw nothing resize affordance is the
1824
+ // hover cursor on the selection edges/corners. See the note above
1825
+ // entityResizeHandlePositions' draw section, 2026-06-11.)
1850
1826
  // Snap-to-align guide lines — render while a drag is snapped (2026-06-11).
1851
1827
  this.drawSnapGuides();
1852
1828
  }
@@ -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, 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;
@@ -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?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",