@unboxy/phaser-sdk 0.2.22 → 0.2.24

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.
@@ -2,6 +2,7 @@ import Phaser from 'phaser';
2
2
  import { getEntityRegistry } from '../scene/EntityRegistry.js';
3
3
  import { parseColor, spawnEntity } from '../scene/spawnEntity.js';
4
4
  import { resolveRenderScript } from '../scene/renderScripts.js';
5
+ import { getManifest } from '../scene/SceneLoader.js';
5
6
  import { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './EditorOverlayScene.js';
6
7
  import { getEditorState, setEditorActive, setSelection, } from './EditorState.js';
7
8
  /**
@@ -209,15 +210,15 @@ function hitTest(game, worldX, worldY) {
209
210
  let topmost = null;
210
211
  let topmostDepth = -Infinity;
211
212
  for (const go of registry.all()) {
212
- const withBounds = go;
213
- if (typeof withBounds.getBounds !== 'function')
213
+ const r = entityHitRect(go);
214
+ if (!r)
214
215
  continue;
215
- const r = withBounds.getBounds();
216
216
  if (worldX < r.x || worldX > r.x + r.width)
217
217
  continue;
218
218
  if (worldY < r.y || worldY > r.y + r.height)
219
219
  continue;
220
- const depth = typeof withBounds.depth === 'number' ? withBounds.depth : 0;
220
+ const withDepth = go;
221
+ const depth = typeof withDepth.depth === 'number' ? withDepth.depth : 0;
221
222
  if (depth >= topmostDepth) {
222
223
  topmostDepth = depth;
223
224
  topmost = go;
@@ -225,6 +226,37 @@ function hitTest(game, worldX, worldY) {
225
226
  }
226
227
  return topmost;
227
228
  }
229
+ /**
230
+ * Compute a hit-test rectangle for a spawned entity in world coords.
231
+ *
232
+ * 1. If the entity has `editorHitWidth` / `editorHitHeight` set in data
233
+ * (code-rendered entities — Graphics has no intrinsic bounds), use
234
+ * those centered on the entity's x/y.
235
+ * 2. Otherwise fall back to Phaser's `getBounds()` (works for Sprite,
236
+ * Rectangle, Arc, Container — anything with a Size component).
237
+ *
238
+ * Returns null if neither path yields a usable rect.
239
+ */
240
+ function entityHitRect(go) {
241
+ const hitW = go.getData('editorHitWidth');
242
+ const hitH = go.getData('editorHitHeight');
243
+ if (typeof hitW === 'number' && typeof hitH === 'number') {
244
+ const positioned = go;
245
+ return {
246
+ x: positioned.x - hitW / 2,
247
+ y: positioned.y - hitH / 2,
248
+ width: hitW,
249
+ height: hitH,
250
+ };
251
+ }
252
+ const withBounds = go;
253
+ if (typeof withBounds.getBounds !== 'function')
254
+ return null;
255
+ const r = withBounds.getBounds();
256
+ if (r.width === 0 && r.height === 0)
257
+ return null;
258
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
259
+ }
228
260
  function findRegistry(game) {
229
261
  for (const scene of game.scene.getScenes(false)) {
230
262
  const reg = getEntityRegistry(scene);
@@ -353,23 +385,49 @@ async function createEntity(game, entity, manifestAsset) {
353
385
  console.warn('[unboxy/editor] createEntity: world scene has no entity registry');
354
386
  return;
355
387
  }
388
+ // Resolve which AssetRecord to use for the lazy-load probe. Three paths:
389
+ // 1. Host passed `manifestAsset` (asset is brand-new) — use that.
390
+ // 2. Already in manifest but not provided (re-drag of existing asset)
391
+ // — look it up via the cached manifest.
392
+ // 3. Non-sprite entity (no asset needed) — leave undefined.
393
+ let assetForLoad = manifestAsset;
394
+ if (!assetForLoad && entity.kind === 'sprite') {
395
+ const visual = entity.visual;
396
+ const assetId = visual?.assetId;
397
+ if (assetId) {
398
+ try {
399
+ const manifest = getManifest(scene);
400
+ assetForLoad = manifest.assets.find((a) => a.id === assetId);
401
+ }
402
+ catch {
403
+ /* manifest not in cache — should not happen in edit mode */
404
+ }
405
+ }
406
+ }
356
407
  // Lazy-load texture if needed (sprite + asset not yet in cache).
357
- if (manifestAsset &&
408
+ if (assetForLoad &&
358
409
  entity.kind === 'sprite' &&
359
- !scene.textures.exists(manifestAsset.textureKey)) {
360
- await loadAssetIntoScene(scene, manifestAsset);
410
+ !scene.textures.exists(assetForLoad.textureKey)) {
411
+ await loadAssetIntoScene(scene, assetForLoad);
361
412
  }
362
413
  const ctx = {
363
414
  scene,
364
415
  registry,
365
416
  resolveAsset: (id) => {
366
- // For ad-hoc creation we may not have full manifest access. Rely on
367
- // the host to have stamped the right textureKey/path into the asset
368
- // we received via manifestAsset; fallback to a synthetic record so
369
- // spawnEntity can still find a textureKey.
370
417
  if (manifestAsset && manifestAsset.id === id)
371
418
  return manifestAsset;
372
- throw new Error(`[unboxy/editor] createEntity: asset '${id}' not in manifest payload host must include manifestAsset`);
419
+ // Fallback: look up in the manifest cache. Covers re-drag of an
420
+ // already-in-manifest asset where the host omitted manifestAsset.
421
+ try {
422
+ const manifest = getManifest(scene);
423
+ const found = manifest.assets.find((a) => a.id === id);
424
+ if (found)
425
+ return found;
426
+ }
427
+ catch {
428
+ /* fall through to throw */
429
+ }
430
+ throw new Error(`[unboxy/editor] createEntity: asset '${id}' not found in manifest`);
373
431
  },
374
432
  resolveRenderScript: undefined,
375
433
  };
@@ -190,10 +190,24 @@ export class EditorOverlayScene extends Phaser.Scene {
190
190
  }
191
191
  }
192
192
  function computeBounds(go) {
193
- // Most game objects implement getBounds; Container does too.
193
+ // Code-rendered entities stash hit dimensions on data because Phaser
194
+ // Graphics has no intrinsic bounds. Use those when present.
195
+ const hitW = go.getData('editorHitWidth');
196
+ const hitH = go.getData('editorHitHeight');
197
+ if (typeof hitW === 'number' && typeof hitH === 'number') {
198
+ const positioned = go;
199
+ return {
200
+ x: positioned.x - hitW / 2,
201
+ y: positioned.y - hitH / 2,
202
+ width: hitW,
203
+ height: hitH,
204
+ };
205
+ }
194
206
  const withBounds = go;
195
207
  if (typeof withBounds.getBounds !== 'function')
196
208
  return null;
197
209
  const r = withBounds.getBounds();
210
+ if (r.width === 0 && r.height === 0)
211
+ return null;
198
212
  return { x: r.x, y: r.y, width: r.width, height: r.height };
199
213
  }
@@ -94,6 +94,8 @@ function createGroup(ctx, entity) {
94
94
  return container;
95
95
  }
96
96
  function createCodeRendered(ctx, entity) {
97
+ const w = entity.visual.width ?? 64;
98
+ const h = entity.visual.height ?? 64;
97
99
  const render = ctx.resolveRenderScript?.(entity.visual.script);
98
100
  if (!render) {
99
101
  // Slice 3.5: render scripts have a registry now, but a missing entry
@@ -102,21 +104,35 @@ function createCodeRendered(ctx, entity) {
102
104
  // the user sees the entity exists but the visual is unresolved.
103
105
  const ph = ctx.scene.add.graphics();
104
106
  ph.fillStyle(0x666666, 0.4);
105
- ph.fillRect(-16, -16, 32, 32);
107
+ ph.fillRect(-w / 2, -h / 2, w, h);
106
108
  ph.lineStyle(2, 0xff8800, 0.9);
107
- ph.strokeRect(-16, -16, 32, 32);
108
- // Annotate so editor / debug overlays can identify.
109
+ ph.strokeRect(-w / 2, -h / 2, w, h);
110
+ sizeForHitTest(ph, w, h);
109
111
  ph.setData('renderScriptMissing', entity.visual.script);
110
112
  return ph;
111
113
  }
112
114
  const g = ctx.scene.add.graphics();
113
115
  const params = entity.visual.params ?? {};
114
116
  render(g, params);
117
+ sizeForHitTest(g, w, h);
115
118
  // Stash data needed for live re-render from EditorBridge.applyEdit.
116
119
  g.setData('renderScriptPath', entity.visual.script);
117
120
  g.setData('renderScriptParams', params);
118
121
  return g;
119
122
  }
123
+ /**
124
+ * Phaser Graphics doesn't track its drawn area — `getBounds()` returns
125
+ * a 0×0 rect because the Graphics class doesn't include the Size/Origin
126
+ * mixins. Rather than fight Phaser's class hierarchy with method casts,
127
+ * stash the editor hit area on the GameObject's data manager. The editor
128
+ * overlay reads these before falling back to `getBounds()`. The implicit
129
+ * convention: hit area is centered on the GameObject's x/y, matching how
130
+ * render scripts conventionally draw around (0, 0).
131
+ */
132
+ function sizeForHitTest(g, width, height) {
133
+ g.setData('editorHitWidth', width);
134
+ g.setData('editorHitHeight', height);
135
+ }
120
136
  function applyTransform(go, t) {
121
137
  // Most game objects implement Transform; Container does too. Cast through
122
138
  // a permissive shape so this works for sprite/rect/circle/container alike.
@@ -116,6 +116,14 @@ export interface CodeRenderedVisual {
116
116
  /** Path to render script, e.g. `"src/visuals/boss-renderer.ts"`. */
117
117
  script: string;
118
118
  params?: Record<string, unknown>;
119
+ /**
120
+ * Optional bounds used for editor hit-testing (click + drag) and the
121
+ * selection rectangle. Phaser Graphics has no intrinsic size, so we set
122
+ * it explicitly. Defaults to 64×64 centered on the entity. The agent can
123
+ * override per-entity when the script draws something larger or smaller.
124
+ */
125
+ width?: number;
126
+ height?: number;
119
127
  }
120
128
  export type WorldVisual = SpriteVisual | PrimitiveVisual | CodeRenderedVisual;
121
129
  export type WorldEntityKind = 'sprite' | 'primitive' | 'code-rendered' | 'group' | 'tilemap' | 'trigger';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
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",