@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';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -141,7 +141,16 @@ export interface EditorDragEndMessage {
|
|
|
141
141
|
y: number;
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
-
|
|
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;
|