@unboxy/phaser-sdk 0.2.28 → 0.2.30

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.
@@ -4,6 +4,7 @@ import { setupRecordingListener } from '../recording/RecordingManager.js';
4
4
  import { setupEditorModeListener } from '../scene/EditorMode.js';
5
5
  import { ORIENTATION_DIMENSIONS } from '../orientation.js';
6
6
  import { setRenderScriptRegistry, } from '../scene/renderScripts.js';
7
+ import { UnboxyHudScene } from '../scene/HudRuntime.js';
7
8
  /**
8
9
  * Create an Unboxy-enhanced Phaser game instance.
9
10
  * Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
@@ -30,7 +31,11 @@ export function createUnboxyGame(options) {
30
31
  arcade: { gravity: { x: 0, y: 0 }, debug: false },
31
32
  },
32
33
  ...(options.plugins ? { plugins: options.plugins } : {}),
33
- scene: options.scenes,
34
+ // UnboxyHudScene is auto-registered alongside the game's scenes so
35
+ // `loadWorldScene` can launch it when the active world scene's manifest
36
+ // entry sets `hud: '<id>'`. Registered at the end so the user's scenes
37
+ // own the boot order.
38
+ scene: [...options.scenes, UnboxyHudScene],
34
39
  };
35
40
  const game = new Phaser.Game(config);
36
41
  // Built-in integrations
@@ -4,7 +4,8 @@ import { parseColor, spawnEntity } from '../scene/spawnEntity.js';
4
4
  import { resolveRenderScript } from '../scene/renderScripts.js';
5
5
  import { getManifest } from '../scene/SceneLoader.js';
6
6
  import { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './EditorOverlayScene.js';
7
- import { getEditorState, setEditorActive, setSelection, getSelection, } from './EditorState.js';
7
+ import { getEditorState, setEditorActive, setSelection, getSelection, getEditorMode, setEditorMode, } from './EditorState.js';
8
+ import { applyHudPatch, createHudEntityInScene, deleteHudEntityFromScene, findHudRegistry, findHudSceneFile, UNBOXY_HUD_SCENE_KEY, } from '../scene/HudRuntime.js';
8
9
  /**
9
10
  * EditorBridge wires the host (home-ui) to the iframe's Phaser game during
10
11
  * Edit mode. Manages:
@@ -65,6 +66,16 @@ function handleMessage(game, msg) {
65
66
  deleteEntity(game, msg.entityId);
66
67
  postSelectionRect(game);
67
68
  break;
69
+ case 'unboxy:editor:setEditMode':
70
+ // Slice 5 — World/HUD toggle. Mode change is purely state; the host
71
+ // already has both scene snapshots from enterEdit and just switches
72
+ // which one its UI renders. Selection may carry over between modes,
73
+ // but selectionRect re-posts so the ✨ button moves to the active
74
+ // mode's selected entity (or hides if none).
75
+ setEditorMode(game, msg.mode);
76
+ setSelection(game, null);
77
+ postSelectionRect(game);
78
+ break;
68
79
  }
69
80
  }
70
81
  // --- Enter / exit ---------------------------------------------------------
@@ -156,16 +167,31 @@ function buildOverlayInit(game) {
156
167
  // --- Snapshot -------------------------------------------------------------
157
168
  const SNAPSHOT_POSTED_FLAG = '__unboxyEditorSnapshotPostedFor';
158
169
  function postSceneSnapshot(game) {
170
+ const manifest = readActiveManifest(game);
159
171
  const sceneFile = readActiveSceneFile(game);
160
- if (!sceneFile)
161
- return;
162
- postToHost({
163
- type: 'unboxy:editor:sceneLoaded',
164
- sceneId: sceneFile.id,
165
- sceneFile,
166
- manifest: readActiveManifest(game),
167
- });
168
- game[SNAPSHOT_POSTED_FLAG] = sceneFile.id;
172
+ if (sceneFile) {
173
+ postToHost({
174
+ type: 'unboxy:editor:sceneLoaded',
175
+ sceneId: sceneFile.id,
176
+ mode: 'world',
177
+ sceneFile,
178
+ manifest,
179
+ });
180
+ game[SNAPSHOT_POSTED_FLAG] = sceneFile.id;
181
+ }
182
+ // Also post the HUD scene if attached + loaded. The host stashes both
183
+ // snapshots so the World/HUD toggle is purely a UI switch — no fresh
184
+ // SDK round-trip needed when the user flips it.
185
+ const hudFile = findHudSceneFile(game);
186
+ if (hudFile) {
187
+ postToHost({
188
+ type: 'unboxy:editor:sceneLoaded',
189
+ sceneId: hudFile.id,
190
+ mode: 'hud',
191
+ sceneFile: hudFile,
192
+ manifest,
193
+ });
194
+ }
169
195
  }
170
196
  // --- Selection rect (slice 4) ---------------------------------------------
171
197
  //
@@ -217,13 +243,13 @@ function doPostSelectionRect(game) {
217
243
  * so we factor in the canvas's parent offset too.
218
244
  */
219
245
  function computeScreenRect(game, go) {
220
- // Pull world-coord bounds — same logic the editor uses for hit-test.
246
+ // Pull entity-local bounds — same logic the editor uses for hit-test.
221
247
  const hitW = go.getData('editorHitWidth');
222
248
  const hitH = go.getData('editorHitHeight');
223
249
  const positioned = go;
224
- let worldRect;
250
+ let entRect;
225
251
  if (typeof hitW === 'number' && typeof hitH === 'number') {
226
- worldRect = {
252
+ entRect = {
227
253
  x: positioned.x - hitW / 2,
228
254
  y: positioned.y - hitH / 2,
229
255
  width: hitW,
@@ -235,30 +261,40 @@ function computeScreenRect(game, go) {
235
261
  if (typeof withBounds.getBounds !== 'function')
236
262
  return null;
237
263
  const r = withBounds.getBounds();
238
- worldRect = { x: r.x, y: r.y, width: r.width, height: r.height };
264
+ entRect = { x: r.x, y: r.y, width: r.width, height: r.height };
239
265
  }
240
- // Find the world scene's camera it carries scrollX/Y + zoom.
241
- let cam = null;
242
- for (const scene of game.scene.getScenes(false)) {
243
- const key = scene.scene.key;
244
- if (BOOT_SCENE_KEYS.has(key))
245
- continue;
246
- if (key === EDITOR_OVERLAY_KEY)
247
- continue;
248
- if (getEntityRegistry(scene)) {
249
- cam = scene.cameras.main;
250
- break;
266
+ // HUD mode entity coords are already in canvas-pixel space (the HUD
267
+ // scene has identity camera). Skip the world→canvas camera transform.
268
+ let canvasX, canvasY, canvasW, canvasH;
269
+ if (getEditorMode(game) === 'hud') {
270
+ canvasX = entRect.x;
271
+ canvasY = entRect.y;
272
+ canvasW = entRect.width;
273
+ canvasH = entRect.height;
274
+ }
275
+ else {
276
+ // World mode — convert via the world scene's camera (scrollX/Y + zoom).
277
+ let cam = null;
278
+ for (const scene of game.scene.getScenes(false)) {
279
+ const key = scene.scene.key;
280
+ if (BOOT_SCENE_KEYS.has(key))
281
+ continue;
282
+ if (key === EDITOR_OVERLAY_KEY)
283
+ continue;
284
+ if (key === UNBOXY_HUD_SCENE_KEY)
285
+ continue;
286
+ if (getEntityRegistry(scene)) {
287
+ cam = scene.cameras.main;
288
+ break;
289
+ }
251
290
  }
291
+ if (!cam)
292
+ return null;
293
+ canvasX = (entRect.x - cam.scrollX) * cam.zoom;
294
+ canvasY = (entRect.y - cam.scrollY) * cam.zoom;
295
+ canvasW = entRect.width * cam.zoom;
296
+ canvasH = entRect.height * cam.zoom;
252
297
  }
253
- if (!cam)
254
- return null;
255
- // World → canvas pixels (relative to canvas top-left, inside Phaser's
256
- // logical canvas space — i.e. the game.config width/height, NOT the
257
- // scaled visible pixels).
258
- const canvasX = (worldRect.x - cam.scrollX) * cam.zoom;
259
- const canvasY = (worldRect.y - cam.scrollY) * cam.zoom;
260
- const canvasW = worldRect.width * cam.zoom;
261
- const canvasH = worldRect.height * cam.zoom;
262
298
  // Convert logical canvas pixels → CSS pixels using the canvas's actual
263
299
  // displayed size (avoids depending on which direction Phaser's displayScale
264
300
  // goes — we just measure it). Canvas may be letterboxed/pillarboxed inside
@@ -374,8 +410,18 @@ function entityHitRect(go) {
374
410
  return null;
375
411
  return { x: r.x, y: r.y, width: r.width, height: r.height };
376
412
  }
413
+ /**
414
+ * Find the entity registry for the current editor mode. World mode picks the
415
+ * first non-Boot non-Editor scene with a registry; HUD mode picks the
416
+ * HUD scene's registry. Slice 5+.
417
+ */
377
418
  function findRegistry(game) {
419
+ if (getEditorMode(game) === 'hud')
420
+ return findHudRegistry(game);
378
421
  for (const scene of game.scene.getScenes(false)) {
422
+ const key = scene.scene.key;
423
+ if (key === UNBOXY_HUD_SCENE_KEY)
424
+ continue;
379
425
  const reg = getEntityRegistry(scene);
380
426
  if (reg)
381
427
  return reg;
@@ -384,6 +430,15 @@ function findRegistry(game) {
384
430
  }
385
431
  // --- applyEdit ------------------------------------------------------------
386
432
  function applyEdit(game, entityId, patch) {
433
+ // HUD mode — patches modify anchor / HUD-visual fields. World-style
434
+ // transform / sprite-tint patches don't apply to HUD widgets, so we
435
+ // dispatch through a HUD-specific helper that knows the widget kinds.
436
+ if (getEditorMode(game) === 'hud') {
437
+ applyHudPatch(game, entityId, patch);
438
+ if (getSelection(game) === entityId)
439
+ postSelectionRect(game);
440
+ return;
441
+ }
387
442
  const registry = findRegistry(game);
388
443
  if (!registry)
389
444
  return;
@@ -496,6 +551,17 @@ function applyVisualPatch(go, v) {
496
551
  * sprite, which defeats the point of drag-to-place.
497
552
  */
498
553
  async function createEntity(game, entity, manifestAsset) {
554
+ // HUD mode dispatches to the HUD-runtime spawner (slice 5). The host
555
+ // sends HudEntity records (not WorldEntity), so we type-erase.
556
+ if (getEditorMode(game) === 'hud') {
557
+ // Lazy-load asset if the HUD widget needs one and it's not in cache yet.
558
+ const hud = game.scene.getScene(UNBOXY_HUD_SCENE_KEY);
559
+ if (manifestAsset && hud && !hud.textures.exists(manifestAsset.textureKey)) {
560
+ await loadAssetIntoScene(hud, manifestAsset);
561
+ }
562
+ createHudEntityInScene(game, entity);
563
+ return;
564
+ }
499
565
  const scene = findWorldScene(game);
500
566
  if (!scene) {
501
567
  console.warn('[unboxy/editor] createEntity: no world scene to spawn into');
@@ -622,6 +688,14 @@ function loadAssetIntoScene(scene, asset) {
622
688
  });
623
689
  }
624
690
  function deleteEntity(game, entityId) {
691
+ // HUD mode — dispatch to the HUD-scene helper, which destroys + unregisters
692
+ // + drops the entity from the cached scene file.
693
+ if (getEditorMode(game) === 'hud') {
694
+ deleteHudEntityFromScene(game, entityId);
695
+ if (getSelection(game) === entityId)
696
+ setSelection(game, null);
697
+ return;
698
+ }
625
699
  const scene = findWorldScene(game);
626
700
  if (!scene)
627
701
  return;
@@ -631,23 +705,18 @@ function deleteEntity(game, entityId) {
631
705
  const go = registry.byId(entityId);
632
706
  if (!go)
633
707
  return;
634
- // Remove from registry by clearing + rebuilding the entries we care about.
635
- // The slice-1 EntityRegistry doesn't expose a direct removeById; the
636
- // simplest thing is to destroy the GameObject and let stale registry
637
- // entries dangle until the next scene reload (post-flush). Slice 3.5
638
- // can add an explicit `registry.unregister` if it becomes a problem.
639
708
  go.destroy();
640
- // Drop selection if it pointed at this entity.
641
- const sel = game;
642
- const state = sel['__unboxyEditorState'];
643
- if (state && state.selectedId === entityId)
644
- state.selectedId = null;
709
+ registry.unregister(entityId);
710
+ if (getSelection(game) === entityId)
711
+ setSelection(game, null);
645
712
  }
646
713
  function findWorldScene(game) {
647
714
  for (const scene of game.scene.getScenes(false)) {
648
715
  const key = scene.scene.key;
649
716
  if (BOOT_SCENE_KEYS.has(key))
650
717
  continue;
718
+ if (key === UNBOXY_HUD_SCENE_KEY)
719
+ continue;
651
720
  if (key === EDITOR_OVERLAY_KEY)
652
721
  continue;
653
722
  if (getEntityRegistry(scene))
@@ -61,6 +61,11 @@ export declare class EditorOverlayScene extends Phaser.Scene {
61
61
  init(data: EditorOverlayInitData): void;
62
62
  create(): void;
63
63
  private handleShortcut;
64
+ /**
65
+ * Pointer coords mode-aware. World mode uses world coords (camera-relative);
66
+ * HUD mode uses canvas-pixel coords (HUD scene has identity camera).
67
+ */
68
+ private pointerCoords;
64
69
  private handlePointerDown;
65
70
  private handlePointerMove;
66
71
  private handlePointerUp;
@@ -72,8 +77,8 @@ export declare class EditorOverlayScene extends Phaser.Scene {
72
77
  */
73
78
  private findWorldSceneCamera;
74
79
  /**
75
- * The EntityRegistry lives on the world scene. Pull it from there so we
76
- * don't have to plumb registry references through the bridge.
80
+ * Mode-aware registry lookup. World mode skips the HUD scene; HUD mode
81
+ * picks the HUD scene specifically.
77
82
  */
78
83
  private findEntityRegistry;
79
84
  }
@@ -1,6 +1,7 @@
1
1
  import Phaser from 'phaser';
2
2
  import { getEntityRegistry } from '../scene/EntityRegistry.js';
3
- import { getEditorState, startDrag, clearDrag, getDrag, getSelection, } from './EditorState.js';
3
+ import { findHudEntity, findHudRegistry, UNBOXY_HUD_SCENE_KEY, } from '../scene/HudRuntime.js';
4
+ import { getEditorState, startDrag, clearDrag, getDrag, getSelection, getEditorMode, } from './EditorState.js';
4
5
  /**
5
6
  * Editor overlay — slice 2.
6
7
  *
@@ -94,13 +95,21 @@ export class EditorOverlayScene extends Phaser.Scene {
94
95
  });
95
96
  }
96
97
  }
98
+ /**
99
+ * Pointer coords mode-aware. World mode uses world coords (camera-relative);
100
+ * HUD mode uses canvas-pixel coords (HUD scene has identity camera).
101
+ */
102
+ pointerCoords(pointer) {
103
+ if (getEditorMode(this.game) === 'hud')
104
+ return { x: pointer.x, y: pointer.y };
105
+ return { x: pointer.worldX, y: pointer.worldY };
106
+ }
97
107
  handlePointerDown(pointer) {
98
108
  const state = getEditorState(this.game);
99
109
  if (!state.active)
100
110
  return;
101
- const wx = pointer.worldX;
102
- const wy = pointer.worldY;
103
- const hit = this.hitTest(wx, wy);
111
+ const { x: px, y: py } = this.pointerCoords(pointer);
112
+ const hit = this.hitTest(px, py);
104
113
  const event = pointer.event;
105
114
  const modifiers = {
106
115
  shift: !!event?.shiftKey,
@@ -118,8 +127,21 @@ export class EditorOverlayScene extends Phaser.Scene {
118
127
  }
119
128
  this.postPick(entityId, modifiers);
120
129
  // Begin drag immediately on pointerdown (drag threshold can be added later).
121
- const target = hit;
122
- startDrag(this.game, entityId, { x: wx, y: wy }, { x: target.x, y: target.y });
130
+ // In HUD mode `startEntity` carries the entity's anchor offset (not the
131
+ // GameObject's canvas-pixel position) so dragEnd can report the new
132
+ // offset values directly.
133
+ if (getEditorMode(this.game) === 'hud') {
134
+ const ent = findHudEntity(this.game, entityId);
135
+ const startOffset = {
136
+ x: ent?.anchor.offsetX ?? 0,
137
+ y: ent?.anchor.offsetY ?? 0,
138
+ };
139
+ startDrag(this.game, entityId, { x: px, y: py }, startOffset);
140
+ }
141
+ else {
142
+ const target = hit;
143
+ startDrag(this.game, entityId, { x: px, y: py }, { x: target.x, y: target.y });
144
+ }
123
145
  }
124
146
  handlePointerMove(pointer) {
125
147
  const drag = getDrag(this.game);
@@ -131,22 +153,46 @@ export class EditorOverlayScene extends Phaser.Scene {
131
153
  const go = registry.byId(drag.entityId);
132
154
  if (!go)
133
155
  return;
134
- const dx = pointer.worldX - drag.startWorld.x;
135
- const dy = pointer.worldY - drag.startWorld.y;
136
- go.x = drag.startEntity.x + dx;
137
- go.y = drag.startEntity.y + dy;
156
+ const { x: px, y: py } = this.pointerCoords(pointer);
157
+ const dx = px - drag.startWorld.x;
158
+ const dy = py - drag.startWorld.y;
159
+ if (getEditorMode(this.game) === 'hud') {
160
+ // HUD `startEntity` carries the offset, not the canvas-pixel start
161
+ // pos. We track per-frame delta on the GO so each pointermove just
162
+ // applies the incremental nudge — `go.x = startGoPos.x + dx` doesn't
163
+ // work because startGoPos isn't stored. The visual position the user
164
+ // sees is the canvas pos derived from the live anchor base + delta.
165
+ const lastDx = go.__lastDragDx ?? 0;
166
+ const lastDy = go.__lastDragDy ?? 0;
167
+ go.x += dx - lastDx;
168
+ go.y += dy - lastDy;
169
+ go.__lastDragDx = dx;
170
+ go.__lastDragDy = dy;
171
+ }
172
+ else {
173
+ go.x = drag.startEntity.x + dx;
174
+ go.y = drag.startEntity.y + dy;
175
+ }
138
176
  }
139
177
  handlePointerUp(pointer) {
140
178
  const drag = getDrag(this.game);
141
179
  if (!drag)
142
180
  return;
143
- const dx = pointer.worldX - drag.startWorld.x;
144
- const dy = pointer.worldY - drag.startWorld.y;
181
+ const { x: px, y: py } = this.pointerCoords(pointer);
182
+ const dx = px - drag.startWorld.x;
183
+ const dy = py - drag.startWorld.y;
145
184
  const before = drag.startEntity;
146
185
  const after = { x: drag.startEntity.x + dx, y: drag.startEntity.y + dy };
186
+ // Reset the per-drag delta accumulator on the GO if HUD mode left one.
187
+ if (getEditorMode(this.game) === 'hud') {
188
+ const reg = this.findEntityRegistry();
189
+ const go = reg?.byId(drag.entityId);
190
+ if (go) {
191
+ go.__lastDragDx = 0;
192
+ go.__lastDragDy = 0;
193
+ }
194
+ }
147
195
  clearDrag(this.game);
148
- // Suppress dragEnd for trivial "click without movement" — the host already
149
- // got pickEntity for those.
150
196
  if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5)
151
197
  return;
152
198
  this.postDragEnd(drag.entityId, before, after);
@@ -184,11 +230,16 @@ export class EditorOverlayScene extends Phaser.Scene {
184
230
  return null;
185
231
  }
186
232
  /**
187
- * The EntityRegistry lives on the world scene. Pull it from there so we
188
- * don't have to plumb registry references through the bridge.
233
+ * Mode-aware registry lookup. World mode skips the HUD scene; HUD mode
234
+ * picks the HUD scene specifically.
189
235
  */
190
236
  findEntityRegistry() {
237
+ if (getEditorMode(this.game) === 'hud')
238
+ return findHudRegistry(this.game);
191
239
  for (const scene of this.game.scene.getScenes(false)) {
240
+ const key = scene.scene.key;
241
+ if (key === UNBOXY_HUD_SCENE_KEY)
242
+ continue;
192
243
  const reg = getEntityRegistry(scene);
193
244
  if (reg)
194
245
  return reg;
@@ -6,9 +6,16 @@
6
6
  * they reach game systems; this state carries the selection + drag-in-progress
7
7
  * info both sides read.
8
8
  */
9
+ /**
10
+ * Editor mode — slice 5. World mode edits the active world scene; HUD mode
11
+ * edits the HUD scene attached to that world (manifest's `scenes[].hud`).
12
+ * Toggled by host via `unboxy:editor:setEditMode`.
13
+ */
14
+ export type EditorMode = 'world' | 'hud';
9
15
  interface EditorStateShape {
10
16
  active: boolean;
11
17
  selectedId: string | null;
18
+ mode: EditorMode;
12
19
  /**
13
20
  * Drag-in-progress info. While set, the overlay scene mutates the entity's
14
21
  * x/y directly per pointermove without going through the host. On
@@ -35,6 +42,8 @@ interface EditorStateShape {
35
42
  type AnyObject = object;
36
43
  export declare function getEditorState(game: AnyObject): EditorStateShape;
37
44
  export declare function setEditorActive(game: AnyObject, active: boolean): void;
45
+ export declare function getEditorMode(game: AnyObject): EditorMode;
46
+ export declare function setEditorMode(game: AnyObject, mode: EditorMode): void;
38
47
  export declare function setSelection(game: AnyObject, id: string | null): void;
39
48
  export declare function getSelection(game: AnyObject): string | null;
40
49
  export declare function startDrag(game: AnyObject, entityId: string, startWorld: {
@@ -15,13 +15,19 @@ export function getEditorState(game) {
15
15
  const existing = b[KEY];
16
16
  if (existing)
17
17
  return existing;
18
- const fresh = { active: false, selectedId: null, drag: null };
18
+ const fresh = { active: false, selectedId: null, mode: 'world', drag: null };
19
19
  b[KEY] = fresh;
20
20
  return fresh;
21
21
  }
22
22
  export function setEditorActive(game, active) {
23
23
  getEditorState(game).active = active;
24
24
  }
25
+ export function getEditorMode(game) {
26
+ return getEditorState(game).mode;
27
+ }
28
+ export function setEditorMode(game, mode) {
29
+ getEditorState(game).mode = mode;
30
+ }
25
31
  export function setSelection(game, id) {
26
32
  getEditorState(game).selectedId = id;
27
33
  }
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export { setRenderScriptRegistry, getRenderScriptRegistry, resolveRenderScript,
26
26
  export type { RenderScriptModule } from './scene/renderScripts.js';
27
27
  export { setupEditorBridge } from './editor/EditorBridge.js';
28
28
  export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
29
- export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSelectionRectMessage, EditorSdkToHostMessage, } from './protocol.js';
29
+ export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSetEditModeMessage, EditorSelectionRectMessage, EditorSdkToHostMessage, } from './protocol.js';
30
30
  export { SCHEMA_VERSION, } from './scene/types.js';
31
- export type { Manifest, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, SpriteEntity, PrimitiveEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TriggerEntity, TriggerShape, WorldVisual, SpriteVisual, PrimitiveVisual, PrimitiveRectVisual, PrimitiveCircleVisual, CodeRenderedVisual, Transform, Anchor, AnchorSide, } from './scene/types.js';
31
+ export type { Manifest, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, SpriteEntity, PrimitiveEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TriggerEntity, TriggerShape, WorldVisual, SpriteVisual, PrimitiveVisual, PrimitiveRectVisual, PrimitiveCircleVisual, CodeRenderedVisual, HudEntity, HudEntityKind, HudEntityBase, HudTextEntity, HudImageEntity, HudIconButtonEntity, HudProgressBarEntity, HudPanelEntity, HudVisual, HudTextVisual, HudImageVisual, HudIconButtonVisual, HudProgressBarVisual, HudPanelVisual, HudTextSource, HudNumberSource, HudLayer, Transform, Anchor, AnchorSide, } from './scene/types.js';
32
32
  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';
@@ -61,13 +61,26 @@ export interface EditorEntityPatch {
61
61
  scaleY: number;
62
62
  depth: number;
63
63
  }>;
64
+ /**
65
+ * HUD-only (slice 5). Updates an entity's anchor side / offset. Only one
66
+ * of `transform` (world entities) or `anchor` (HUD entities) is set per
67
+ * patch; the SDK picks the right path based on the active mode.
68
+ */
69
+ anchor?: Partial<{
70
+ side: string;
71
+ offsetX: number;
72
+ offsetY: number;
73
+ }>;
74
+ /** HUD-only (slice 5). Render layer / z-order overrides. */
75
+ layer?: string;
76
+ z?: number;
64
77
  visual?: {
65
78
  tint?: string | null;
66
79
  alpha?: number;
67
80
  flipX?: boolean;
68
81
  flipY?: boolean;
69
82
  frame?: string | number;
70
- /** For primitives. */
83
+ /** For primitives + HUD widgets. */
71
84
  width?: number;
72
85
  height?: number;
73
86
  radius?: number;
@@ -80,6 +93,25 @@ export interface EditorEntityPatch {
80
93
  * Slice 3.5.
81
94
  */
82
95
  params?: Record<string, unknown>;
96
+ /**
97
+ * HUD text widgets (slice 5). Replaces `visual.source` wholesale —
98
+ * static→dynamic switches and prefix/suffix edits land here. Static
99
+ * mode also accepts `text`; dynamic accepts `binding`/`prefix`/`suffix`/`fallback`.
100
+ */
101
+ source?: Record<string, unknown>;
102
+ /** HUD text font / colour. */
103
+ fontFamily?: string;
104
+ fontSize?: number;
105
+ color?: string;
106
+ align?: 'left' | 'center' | 'right';
107
+ /** HUD image / icon-button asset. */
108
+ assetId?: string;
109
+ iconAssetId?: string;
110
+ /** HUD icon-button. */
111
+ label?: string;
112
+ shape?: 'rounded-rect' | 'circle';
113
+ pressedFillColor?: string;
114
+ textColor?: string;
83
115
  };
84
116
  role?: string | null;
85
117
  properties?: Record<string, unknown>;
@@ -132,11 +164,31 @@ export interface EditorDeleteEntityMessage {
132
164
  type: 'unboxy:editor:deleteEntity';
133
165
  entityId: string;
134
166
  }
135
- export type EditorHostToSdkMessage = EditorEnterMessage | EditorExitMessage | EditorGetSceneMessage | EditorApplyEditMessage | EditorSetSelectionMessage | EditorPanZoomMessage | EditorCreateEntityMessage | EditorDeleteEntityMessage;
167
+ /**
168
+ * Editor mode — slice 5. World mode edits the world scene; HUD mode edits
169
+ * the HUD scene attached to the active world. The host sends this when the
170
+ * user toggles the World/HUD switch in the editor canvas.
171
+ *
172
+ * The mode change does NOT clear selection at the SDK level; the host owns
173
+ * selection state per mode (separate selectedId for world / HUD drafts).
174
+ */
175
+ export interface EditorSetEditModeMessage {
176
+ type: 'unboxy:editor:setEditMode';
177
+ mode: 'world' | 'hud';
178
+ }
179
+ export type EditorHostToSdkMessage = EditorEnterMessage | EditorExitMessage | EditorGetSceneMessage | EditorApplyEditMessage | EditorSetSelectionMessage | EditorPanZoomMessage | EditorCreateEntityMessage | EditorDeleteEntityMessage | EditorSetEditModeMessage;
136
180
  export interface EditorSceneLoadedMessage {
137
181
  type: 'unboxy:editor:sceneLoaded';
138
182
  sceneId: string;
139
- /** Snapshot of the world scene file's current entities + camera. */
183
+ /**
184
+ * Discriminator (slice 5+). 'world' (default for back-compat) is the
185
+ * world scene; 'hud' is the HUD overlay scene. SDK posts one message per
186
+ * scene type when entering edit mode, so the host can populate both
187
+ * Hierarchy / Inspector drafts and switch between them via the
188
+ * World/HUD toggle without a fresh SDK round-trip.
189
+ */
190
+ mode?: 'world' | 'hud';
191
+ /** Snapshot of the scene file's current entities. */
140
192
  sceneFile: unknown;
141
193
  /**
142
194
  * Snapshot of the manifest (asset table + scene list). Slice 3+ — home-ui
@@ -14,6 +14,9 @@ export declare class EntityRegistry {
14
14
  byId(id: string): Phaser.GameObjects.GameObject | undefined;
15
15
  byRole(role: string): Phaser.GameObjects.GameObject[];
16
16
  all(): Phaser.GameObjects.GameObject[];
17
+ /** Remove an entity from the registry. Does NOT destroy the GameObject —
18
+ * callers (editor delete path) destroy first, then unregister. */
19
+ unregister(id: string): void;
17
20
  clear(): void;
18
21
  }
19
22
  export declare function attachEntityRegistry(scene: Phaser.Scene): EntityRegistry;
@@ -28,6 +28,22 @@ export class EntityRegistry {
28
28
  all() {
29
29
  return Array.from(this.byIdMap.values());
30
30
  }
31
+ /** Remove an entity from the registry. Does NOT destroy the GameObject —
32
+ * callers (editor delete path) destroy first, then unregister. */
33
+ unregister(id) {
34
+ const go = this.byIdMap.get(id);
35
+ if (!go)
36
+ return;
37
+ this.byIdMap.delete(id);
38
+ for (const [role, list] of this.byRoleMap) {
39
+ const idx = list.indexOf(go);
40
+ if (idx >= 0) {
41
+ list.splice(idx, 1);
42
+ if (list.length === 0)
43
+ this.byRoleMap.delete(role);
44
+ }
45
+ }
46
+ }
31
47
  clear() {
32
48
  this.byIdMap.clear();
33
49
  this.byRoleMap.clear();