@unboxy/phaser-sdk 0.2.18 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SDK-GUIDE.md CHANGED
@@ -638,6 +638,7 @@ The `scene-data-architecture` agent skill has the full guidance. The `scene-data
638
638
 
639
639
  ## Changelog
640
640
 
641
+ - **0.2.19** — fix: editor `enter` arriving while BootScene is still preloading (the post-flush iframe-rebuild path) had nothing to pause, so the world scene resumed running freely once it started up. Now the bridge re-attempts the pause + scene snapshot for ~3 seconds after enter, catching the scene as it transitions from BootScene → GameScene. Snapshot is also re-posted once a scene file lands in cache. Surfaced when slice-2's auto-flush rebuilt the iframe and the user found the sprite started moving again post-save (2026-05-07).
641
642
  - **0.2.18** — added editor bridge (visual editor slice 2). Replaces the slice-1 `unboxy:setEditMode` message with a full editor protocol: `unboxy:editor:enter`/`:exit` (toggles edit mode + launches the overlay scene), `:applyEdit` (mutates a live entity from the host), `:setSelection` (host pushes selection), `:panZoom` (editor camera control), plus SDK→host `:sceneLoaded` (initial snapshot), `:pickEntity` (pointer-down selection), `:dragEnd` (entity dragged to new position). New module `src/editor/` with `EditorOverlayScene` (high-depth Phaser.Scene drawing selection rect + world bounds, capturing pointer events) and `EditorBridge` (postMessage handler + applyEdit logic). New exports: `setupEditorBridge`, `EditorOverlayScene`, `EDITOR_OVERLAY_KEY`, plus all editor protocol types. `setupEditorModeListener` is preserved as a thin wrapper that delegates to the bridge so existing `createUnboxyGame` wiring continues to work. Pairs with home-ui's new Hierarchy + Inspector panels and `useEditorDraft` hook.
642
643
  - **0.2.17** — added scene-as-data foundation (visual editor slice 1). New exports: `loadWorldScene`, `preloadManifest`, `getManifest`, `preloadSceneAssets`, `spawnEntity`, `EntityRegistry`, `attachEntityRegistry`, `getEntityRegistry`, `setupEditorModeListener`, `isEditMode`, `parseColor`, `SCHEMA_VERSION`, plus all schema types (`Manifest`, `WorldScene`, `WorldEntity`, `Transform`, `AssetRecord`, etc.). New games that ship with `public/scenes/manifest.json` load entities + camera config from JSON; `GameScene.ts` becomes a generic loader that calls `await loadWorldScene(this, sceneId)`. `createUnboxyGame` now also wires `setupEditorModeListener` so the host (home-ui) can pause active scenes via `postMessage({ type: 'unboxy:setEditMode', enabled: boolean })`. Purely additive — existing scene-as-code games are unaffected. Migration to scene-as-data is opt-in via the `scene-data-migration` agent skill.
643
644
  - **0.2.16** — added orientation presets. `createUnboxyGame` now accepts an `orientation: 'portrait' | 'landscape'` option as an alternative to explicit `width`/`height` (TS union — pass one or the other). New exports: `Orientation` type and `ORIENTATION_DIMENSIONS` map (`landscape: 1280×720`, `portrait: 720×1280`). Lets games declare orientation once and have a single source of truth for canvas dimensions.
@@ -60,6 +60,36 @@ function enterEdit(game) {
60
60
  if (getEditorState(game).active)
61
61
  return;
62
62
  setEditorActive(game, true);
63
+ pauseActiveNonEditor(game);
64
+ // Launch the overlay AFTER pausing world scenes so it sits on top in render
65
+ // order (Phaser renders scenes in the order they were started).
66
+ const overlayInit = buildOverlayInit(game);
67
+ if (!game.scene.isActive(EDITOR_OVERLAY_KEY)) {
68
+ game.scene.run(EDITOR_OVERLAY_KEY, overlayInit);
69
+ }
70
+ // Send the initial scene snapshot so home-ui can populate Hierarchy /
71
+ // Inspector without a separate getScene round-trip.
72
+ postSceneSnapshot(game);
73
+ // Race-window catch: when home-ui sends `enter` right after iframe load
74
+ // (which is exactly the auto-flush rebuild path), BootScene may still be
75
+ // in preload / GameScene may not have started, so the synchronous pause
76
+ // above had nothing to pause and the snapshot found no scene file in
77
+ // cache yet. Re-attempt for ~3 seconds. Each attempt is cheap and stops
78
+ // automatically once the user exits edit mode.
79
+ let attempts = 0;
80
+ const reattempt = () => {
81
+ if (!getEditorState(game).active)
82
+ return;
83
+ pauseActiveNonEditor(game);
84
+ if (!hasPostedSnapshot(game))
85
+ postSceneSnapshot(game);
86
+ attempts += 1;
87
+ if (attempts < 30)
88
+ setTimeout(reattempt, 100);
89
+ };
90
+ setTimeout(reattempt, 100);
91
+ }
92
+ function pauseActiveNonEditor(game) {
63
93
  for (const scene of game.scene.getScenes(true)) {
64
94
  const key = scene.scene.key;
65
95
  if (BOOT_SCENE_KEYS.has(key))
@@ -69,19 +99,14 @@ function enterEdit(game) {
69
99
  if (scene.scene.isActive())
70
100
  scene.scene.pause();
71
101
  }
72
- // Launch the overlay AFTER pausing world scenes so it sits on top in render
73
- // order (Phaser renders scenes in the order they were started).
74
- const overlayInit = buildOverlayInit(game);
75
- game.scene.run(EDITOR_OVERLAY_KEY, overlayInit);
76
- // Send the initial scene snapshot so home-ui can populate Hierarchy /
77
- // Inspector without a separate getScene round-trip.
78
- postSceneSnapshot(game);
79
102
  }
80
103
  function exitEdit(game) {
81
104
  if (!getEditorState(game).active)
82
105
  return;
83
106
  setEditorActive(game, false);
84
107
  setSelection(game, null);
108
+ // Allow next enter to re-post the snapshot (scene file may have changed).
109
+ delete game[SNAPSHOT_POSTED_FLAG];
85
110
  if (game.scene.isActive(EDITOR_OVERLAY_KEY)) {
86
111
  game.scene.stop(EDITOR_OVERLAY_KEY);
87
112
  }
@@ -113,9 +138,11 @@ function buildOverlayInit(game) {
113
138
  before,
114
139
  after,
115
140
  }),
141
+ postShortcut: (action) => postToHost({ type: 'unboxy:editor:shortcut', action }),
116
142
  };
117
143
  }
118
144
  // --- Snapshot -------------------------------------------------------------
145
+ const SNAPSHOT_POSTED_FLAG = '__unboxyEditorSnapshotPostedFor';
119
146
  function postSceneSnapshot(game) {
120
147
  const sceneFile = readActiveSceneFile(game);
121
148
  if (!sceneFile)
@@ -125,6 +152,10 @@ function postSceneSnapshot(game) {
125
152
  sceneId: sceneFile.id,
126
153
  sceneFile,
127
154
  });
155
+ game[SNAPSHOT_POSTED_FLAG] = sceneFile.id;
156
+ }
157
+ function hasPostedSnapshot(game) {
158
+ return !!game[SNAPSHOT_POSTED_FLAG];
128
159
  }
129
160
  function readActiveSceneFile(game) {
130
161
  // The active world scene cached its scene file in Phaser's JSON cache via
@@ -43,6 +43,12 @@ interface EditorOverlayInitData {
43
43
  x: number;
44
44
  y: number;
45
45
  }) => void;
46
+ /**
47
+ * Forward an editor keyboard shortcut to the host. Set when the iframe has
48
+ * focus (which is always after the user clicks the canvas to select);
49
+ * native Cmd+Z otherwise can't reach the host.
50
+ */
51
+ postShortcut: (action: 'undo' | 'redo' | 'save') => void;
46
52
  }
47
53
  export declare class EditorOverlayScene extends Phaser.Scene {
48
54
  private graphics;
@@ -50,9 +56,11 @@ export declare class EditorOverlayScene extends Phaser.Scene {
50
56
  private hitTest;
51
57
  private postPick;
52
58
  private postDragEnd;
59
+ private postShortcut;
53
60
  constructor();
54
61
  init(data: EditorOverlayInitData): void;
55
62
  create(): void;
63
+ private handleShortcut;
56
64
  private handlePointerDown;
57
65
  private handlePointerMove;
58
66
  private handlePointerUp;
@@ -26,12 +26,40 @@ const WORLD_BOUNDS_ALPHA = 0.5;
26
26
  export class EditorOverlayScene extends Phaser.Scene {
27
27
  constructor() {
28
28
  super({ key: EDITOR_OVERLAY_KEY });
29
+ this.handleShortcut = (e) => {
30
+ if (!(e.metaKey || e.ctrlKey))
31
+ return;
32
+ // Don't swallow keys when the user is typing in a real form control —
33
+ // shouldn't happen inside the iframe, but be defensive.
34
+ const target = e.target;
35
+ if (target) {
36
+ const tag = target.tagName;
37
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
38
+ return;
39
+ if (target.isContentEditable)
40
+ return;
41
+ }
42
+ const k = e.key.toLowerCase();
43
+ if (k === 'z') {
44
+ e.preventDefault();
45
+ this.postShortcut(e.shiftKey ? 'redo' : 'undo');
46
+ }
47
+ else if (k === 'y') {
48
+ e.preventDefault();
49
+ this.postShortcut('redo');
50
+ }
51
+ else if (k === 's') {
52
+ e.preventDefault();
53
+ this.postShortcut('save');
54
+ }
55
+ };
29
56
  }
30
57
  init(data) {
31
58
  this.worldBounds = data.worldBounds;
32
59
  this.hitTest = data.hitTest;
33
60
  this.postPick = data.postPick;
34
61
  this.postDragEnd = data.postDragEnd;
62
+ this.postShortcut = data.postShortcut;
35
63
  }
36
64
  create() {
37
65
  this.graphics = this.add.graphics();
@@ -45,6 +73,19 @@ export class EditorOverlayScene extends Phaser.Scene {
45
73
  this.input.on('pointerdown', this.handlePointerDown, this);
46
74
  this.input.on('pointermove', this.handlePointerMove, this);
47
75
  this.input.on('pointerup', this.handlePointerUp, this);
76
+ // Forward editor shortcuts back to the host. The user clicks the canvas
77
+ // to select an entity, which moves focus into the iframe — after that,
78
+ // native Cmd+Z lands here, not on the host's window. We catch it before
79
+ // Phaser's keyboard plugin gets it (DOM phase is fine because Phaser
80
+ // listens via its own KeyboardManager), preventDefault'ing browser
81
+ // back-nav-on-Cmd+Left etc. is unnecessary; we only intercept the few
82
+ // editor keys we care about.
83
+ if (typeof document !== 'undefined') {
84
+ document.addEventListener('keydown', this.handleShortcut, true);
85
+ this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
86
+ document.removeEventListener('keydown', this.handleShortcut, true);
87
+ });
88
+ }
48
89
  }
49
90
  handlePointerDown(pointer) {
50
91
  const state = getEditorState(this.game);
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export { EntityRegistry, attachEntityRegistry, getEntityRegistry, } from './scen
24
24
  export { setupEditorModeListener, isEditMode } from './scene/EditorMode.js';
25
25
  export { setupEditorBridge } from './editor/EditorBridge.js';
26
26
  export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
27
- export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorSdkToHostMessage, } from './protocol.js';
27
+ export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorSdkToHostMessage, } from './protocol.js';
28
28
  export { SCHEMA_VERSION, } from './scene/types.js';
29
29
  export type { Manifest, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, SpriteEntity, PrimitiveEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TriggerEntity, WorldVisual, SpriteVisual, PrimitiveVisual, PrimitiveRectVisual, PrimitiveCircleVisual, CodeRenderedVisual, Transform, Anchor, AnchorSide, } from './scene/types.js';
30
30
  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';
@@ -141,7 +141,16 @@ export interface EditorDragEndMessage {
141
141
  y: number;
142
142
  };
143
143
  }
144
- export type EditorSdkToHostMessage = EditorSceneLoadedMessage | EditorSelectionPickedMessage | EditorDragEndMessage;
144
+ /**
145
+ * Editor keyboard shortcut intercepted inside the iframe (user clicked the
146
+ * canvas, so focus is on the iframe and the host window doesn't see the
147
+ * native keydown). The SDK forwards a small allowlist back to the host.
148
+ */
149
+ export interface EditorShortcutMessage {
150
+ type: 'unboxy:editor:shortcut';
151
+ action: 'undo' | 'redo' | 'save';
152
+ }
153
+ export type EditorSdkToHostMessage = EditorSceneLoadedMessage | EditorSelectionPickedMessage | EditorDragEndMessage | EditorShortcutMessage;
145
154
  export type RpcMethod = 'saves.get' | 'saves.set' | 'saves.delete' | 'saves.list' | 'gameData.get' | 'gameData.set' | 'gameData.delete' | 'gameData.list' | 'realtime.getToken';
146
155
  export interface SavesGetParams {
147
156
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Unboxy Phaser 3 SDK — game infrastructure for the Unboxy platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",