@unboxy/phaser-sdk 0.2.21 → 0.2.23
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 +1 -0
- package/dist/core/UnboxyGame.d.ts +8 -0
- package/dist/core/UnboxyGame.js +4 -0
- package/dist/editor/EditorBridge.js +58 -4
- package/dist/editor/EditorOverlayScene.js +15 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/protocol.d.ts +6 -0
- package/dist/scene/SceneLoader.js +3 -1
- package/dist/scene/renderScripts.d.ts +53 -0
- package/dist/scene/renderScripts.js +67 -0
- package/dist/scene/spawnEntity.js +33 -2
- package/dist/scene/types.d.ts +8 -0
- package/package.json +1 -1
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.22** — render-script registry wired (visual editor slice 3.5). `createUnboxyGame` accepts a new `renderScripts: Record<path, RenderScriptModule>` option; templates build it via `import.meta.glob('./visuals/*.ts', { eager: true })`. `loadWorldScene` falls back to that registry when no explicit `resolveRenderScript` is passed, so games don't have to plumb it through every scene call. Missing scripts no longer crash the scene boot — `spawnEntity` renders a clear orange-bordered "?" placeholder instead and tags the GameObject with `renderScriptMissing` for debug. `applyEdit` now handles `visual.params` patches on `code-rendered` entities by re-calling render with merged params (live editor preview as you tune in the Inspector). New exports: `RenderScriptModule`, `setRenderScriptRegistry`, `getRenderScriptRegistry`, `resolveRenderScript`. Pairs with the `phaser-render-script` agent skill which teaches the pure-function contract.
|
|
641
642
|
- **0.2.21** — visual editor slice 3 (drag-to-place). Formalized trigger + tilemap entity data shapes (TriggerEntity now has `shape: rect|circle`, `description`, `targets[]`; TilemapEntity has `tileSize`, `size`, `layers[]`). spawnEntity renders both as placeholders (trigger = semi-transparent cyan rect/circle, tilemap = translucent grid sketch — full features ship in slices 5+6). New protocol: `unboxy:editor:createEntity { entity, manifestAsset? }` (host-side drag-to-place spawns entity at world coord; lazy-loads asset texture if needed) + `unboxy:editor:deleteEntity { entityId }` (Backspace + undo of create). `sceneLoaded` now also carries the manifest snapshot so home-ui can mutate the asset table when a new asset gets dropped on the canvas.
|
|
642
643
|
- **0.2.20** — added editor shortcut forwarding (Cmd+Z/Y/S land in iframe when canvas focused; SDK posts back to host via `unboxy:editor:shortcut`). Without this, native shortcuts didn't reach the host's keydown listener after the user clicked the canvas.
|
|
643
644
|
- **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).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Phaser from 'phaser';
|
|
2
2
|
import { type Orientation } from '../orientation.js';
|
|
3
|
+
import { RenderScriptModule } from '../scene/renderScripts.js';
|
|
3
4
|
interface UnboxyGameBaseOptions {
|
|
4
5
|
/** Phaser scene classes to register */
|
|
5
6
|
scenes: (new (...args: any[]) => Phaser.Scene)[];
|
|
@@ -11,6 +12,13 @@ interface UnboxyGameBaseOptions {
|
|
|
11
12
|
physics?: Phaser.Types.Core.PhysicsConfig;
|
|
12
13
|
/** Phaser plugin registrations (e.g. `phaser3-rex-plugins` virtual joystick) */
|
|
13
14
|
plugins?: Phaser.Types.Core.PluginObject;
|
|
15
|
+
/**
|
|
16
|
+
* Map of render-script paths → modules that export `render(g, params)`.
|
|
17
|
+
* Templates build this via `import.meta.glob('./visuals/*.ts', { eager: true })`.
|
|
18
|
+
* Used by `code-rendered` entities and consumed inside `loadWorldScene`.
|
|
19
|
+
* Optional — games without code-rendered visuals can omit it.
|
|
20
|
+
*/
|
|
21
|
+
renderScripts?: Record<string, RenderScriptModule>;
|
|
14
22
|
}
|
|
15
23
|
/**
|
|
16
24
|
* Either pass `orientation` (preset dims from ORIENTATION_DIMENSIONS) OR
|
package/dist/core/UnboxyGame.js
CHANGED
|
@@ -3,6 +3,7 @@ import { setupScreenshotListener } from '../screenshot/ScreenshotManager.js';
|
|
|
3
3
|
import { setupRecordingListener } from '../recording/RecordingManager.js';
|
|
4
4
|
import { setupEditorModeListener } from '../scene/EditorMode.js';
|
|
5
5
|
import { ORIENTATION_DIMENSIONS } from '../orientation.js';
|
|
6
|
+
import { setRenderScriptRegistry, } from '../scene/renderScripts.js';
|
|
6
7
|
/**
|
|
7
8
|
* Create an Unboxy-enhanced Phaser game instance.
|
|
8
9
|
* Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
|
|
@@ -36,5 +37,8 @@ export function createUnboxyGame(options) {
|
|
|
36
37
|
setupScreenshotListener(game);
|
|
37
38
|
setupRecordingListener(game);
|
|
38
39
|
setupEditorModeListener(game);
|
|
40
|
+
if (options.renderScripts) {
|
|
41
|
+
setRenderScriptRegistry(game, options.renderScripts);
|
|
42
|
+
}
|
|
39
43
|
return game;
|
|
40
44
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Phaser from 'phaser';
|
|
2
2
|
import { getEntityRegistry } from '../scene/EntityRegistry.js';
|
|
3
3
|
import { parseColor, spawnEntity } from '../scene/spawnEntity.js';
|
|
4
|
+
import { resolveRenderScript } from '../scene/renderScripts.js';
|
|
4
5
|
import { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './EditorOverlayScene.js';
|
|
5
6
|
import { getEditorState, setEditorActive, setSelection, } from './EditorState.js';
|
|
6
7
|
/**
|
|
@@ -208,15 +209,15 @@ function hitTest(game, worldX, worldY) {
|
|
|
208
209
|
let topmost = null;
|
|
209
210
|
let topmostDepth = -Infinity;
|
|
210
211
|
for (const go of registry.all()) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
212
|
+
const r = entityHitRect(go);
|
|
213
|
+
if (!r)
|
|
213
214
|
continue;
|
|
214
|
-
const r = withBounds.getBounds();
|
|
215
215
|
if (worldX < r.x || worldX > r.x + r.width)
|
|
216
216
|
continue;
|
|
217
217
|
if (worldY < r.y || worldY > r.y + r.height)
|
|
218
218
|
continue;
|
|
219
|
-
const
|
|
219
|
+
const withDepth = go;
|
|
220
|
+
const depth = typeof withDepth.depth === 'number' ? withDepth.depth : 0;
|
|
220
221
|
if (depth >= topmostDepth) {
|
|
221
222
|
topmostDepth = depth;
|
|
222
223
|
topmost = go;
|
|
@@ -224,6 +225,37 @@ function hitTest(game, worldX, worldY) {
|
|
|
224
225
|
}
|
|
225
226
|
return topmost;
|
|
226
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Compute a hit-test rectangle for a spawned entity in world coords.
|
|
230
|
+
*
|
|
231
|
+
* 1. If the entity has `editorHitWidth` / `editorHitHeight` set in data
|
|
232
|
+
* (code-rendered entities — Graphics has no intrinsic bounds), use
|
|
233
|
+
* those centered on the entity's x/y.
|
|
234
|
+
* 2. Otherwise fall back to Phaser's `getBounds()` (works for Sprite,
|
|
235
|
+
* Rectangle, Arc, Container — anything with a Size component).
|
|
236
|
+
*
|
|
237
|
+
* Returns null if neither path yields a usable rect.
|
|
238
|
+
*/
|
|
239
|
+
function entityHitRect(go) {
|
|
240
|
+
const hitW = go.getData('editorHitWidth');
|
|
241
|
+
const hitH = go.getData('editorHitHeight');
|
|
242
|
+
if (typeof hitW === 'number' && typeof hitH === 'number') {
|
|
243
|
+
const positioned = go;
|
|
244
|
+
return {
|
|
245
|
+
x: positioned.x - hitW / 2,
|
|
246
|
+
y: positioned.y - hitH / 2,
|
|
247
|
+
width: hitW,
|
|
248
|
+
height: hitH,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const withBounds = go;
|
|
252
|
+
if (typeof withBounds.getBounds !== 'function')
|
|
253
|
+
return null;
|
|
254
|
+
const r = withBounds.getBounds();
|
|
255
|
+
if (r.width === 0 && r.height === 0)
|
|
256
|
+
return null;
|
|
257
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
258
|
+
}
|
|
227
259
|
function findRegistry(game) {
|
|
228
260
|
for (const scene of game.scene.getScenes(false)) {
|
|
229
261
|
const reg = getEntityRegistry(scene);
|
|
@@ -242,6 +274,7 @@ function applyEdit(game, entityId, patch) {
|
|
|
242
274
|
return;
|
|
243
275
|
applyTransformPatch(go, patch.transform);
|
|
244
276
|
applyVisualPatch(go, patch.visual);
|
|
277
|
+
applyCodeRenderedParamsPatch(game, go, patch.visual?.params);
|
|
245
278
|
if (patch.role !== undefined) {
|
|
246
279
|
if (patch.role === null)
|
|
247
280
|
go.setData('entityRole', undefined);
|
|
@@ -252,6 +285,27 @@ function applyEdit(game, entityId, patch) {
|
|
|
252
285
|
go.setData('entityProperties', patch.properties);
|
|
253
286
|
}
|
|
254
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Re-render a code-rendered entity's visual when its params change.
|
|
290
|
+
* Slice 3.5 — Inspector params editor produces these patches.
|
|
291
|
+
*
|
|
292
|
+
* No-op for non-code-rendered entities (no renderScriptPath in data) so
|
|
293
|
+
* sprite/primitive patches that incidentally include `visual.params` (the
|
|
294
|
+
* type allows it) flow through harmlessly.
|
|
295
|
+
*/
|
|
296
|
+
function applyCodeRenderedParamsPatch(game, go, newParams) {
|
|
297
|
+
if (newParams === undefined)
|
|
298
|
+
return;
|
|
299
|
+
const scriptPath = go.getData('renderScriptPath');
|
|
300
|
+
if (!scriptPath)
|
|
301
|
+
return; // not a code-rendered entity
|
|
302
|
+
const render = resolveRenderScript(game, scriptPath);
|
|
303
|
+
if (!render)
|
|
304
|
+
return;
|
|
305
|
+
const g = go;
|
|
306
|
+
render(g, newParams);
|
|
307
|
+
go.setData('renderScriptParams', newParams);
|
|
308
|
+
}
|
|
255
309
|
function applyTransformPatch(go, t) {
|
|
256
310
|
if (!t)
|
|
257
311
|
return;
|
|
@@ -190,10 +190,24 @@ export class EditorOverlayScene extends Phaser.Scene {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
function computeBounds(go) {
|
|
193
|
-
//
|
|
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
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export { spawnEntity, parseColor } from './scene/spawnEntity.js';
|
|
|
22
22
|
export type { SpawnContext, AssetResolver, RenderScriptResolver, } from './scene/spawnEntity.js';
|
|
23
23
|
export { EntityRegistry, attachEntityRegistry, getEntityRegistry, } from './scene/EntityRegistry.js';
|
|
24
24
|
export { setupEditorModeListener, isEditMode } from './scene/EditorMode.js';
|
|
25
|
+
export { setRenderScriptRegistry, getRenderScriptRegistry, resolveRenderScript, } from './scene/renderScripts.js';
|
|
26
|
+
export type { RenderScriptModule } from './scene/renderScripts.js';
|
|
25
27
|
export { setupEditorBridge } from './editor/EditorBridge.js';
|
|
26
28
|
export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
|
|
27
29
|
export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSdkToHostMessage, } from './protocol.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { loadWorldScene, preloadManifest, preloadSceneAssets, getManifest, SCENE
|
|
|
18
18
|
export { spawnEntity, parseColor } from './scene/spawnEntity.js';
|
|
19
19
|
export { EntityRegistry, attachEntityRegistry, getEntityRegistry, } from './scene/EntityRegistry.js';
|
|
20
20
|
export { setupEditorModeListener, isEditMode } from './scene/EditorMode.js';
|
|
21
|
+
export { setRenderScriptRegistry, getRenderScriptRegistry, resolveRenderScript, } from './scene/renderScripts.js';
|
|
21
22
|
export { setupEditorBridge } from './editor/EditorBridge.js';
|
|
22
23
|
export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
|
|
23
24
|
export { SCHEMA_VERSION, } from './scene/types.js';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ export interface EditorEntityPatch {
|
|
|
74
74
|
fillColor?: string;
|
|
75
75
|
strokeColor?: string | null;
|
|
76
76
|
strokeWidth?: number;
|
|
77
|
+
/**
|
|
78
|
+
* For `code-rendered` entities — the entire params object replaces
|
|
79
|
+
* `visual.params`. SDK re-calls `render(g, params)` on receipt.
|
|
80
|
+
* Slice 3.5.
|
|
81
|
+
*/
|
|
82
|
+
params?: Record<string, unknown>;
|
|
77
83
|
};
|
|
78
84
|
role?: string | null;
|
|
79
85
|
properties?: Record<string, unknown>;
|
|
@@ -2,6 +2,7 @@ import Phaser from 'phaser';
|
|
|
2
2
|
import { SCHEMA_VERSION, } from './types.js';
|
|
3
3
|
import { spawnEntity } from './spawnEntity.js';
|
|
4
4
|
import { attachEntityRegistry } from './EntityRegistry.js';
|
|
5
|
+
import { resolveRenderScript } from './renderScripts.js';
|
|
5
6
|
/**
|
|
6
7
|
* Where scene files live in the workspace and at runtime. The Vite build
|
|
7
8
|
* copies `public/` to the served root, so paths are origin-relative
|
|
@@ -179,7 +180,8 @@ export async function loadWorldScene(scene, sceneId, options = {}) {
|
|
|
179
180
|
scene,
|
|
180
181
|
registry,
|
|
181
182
|
resolveAsset: (id) => resolveAsset(manifest, id),
|
|
182
|
-
resolveRenderScript: options.resolveRenderScript
|
|
183
|
+
resolveRenderScript: options.resolveRenderScript ??
|
|
184
|
+
((path) => resolveRenderScript(scene.game, path)),
|
|
183
185
|
};
|
|
184
186
|
for (const entity of sceneFile.entities)
|
|
185
187
|
spawnEntity(ctx, entity);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
/**
|
|
3
|
+
* Render-script module shape — slice 3.5.
|
|
4
|
+
*
|
|
5
|
+
* Spec: `unboxy-design/features/visual-editor/02-render-scripts.md`.
|
|
6
|
+
*
|
|
7
|
+
* A `code-rendered` entity references a render script by path. The script's
|
|
8
|
+
* `render` is a **pure function** that draws into a Phaser Graphics object
|
|
9
|
+
* given a parameters object. The function:
|
|
10
|
+
*
|
|
11
|
+
* - Calls `g.clear()` first (idempotent re-renders).
|
|
12
|
+
* - Has no time/random dependency (motion is in tween system, not here).
|
|
13
|
+
* - Holds no module state.
|
|
14
|
+
*
|
|
15
|
+
* Optional exports the editor / SDK uses if present:
|
|
16
|
+
*
|
|
17
|
+
* - `defaultParams` — used as a starting point for new entities.
|
|
18
|
+
* - `paramSchema` — drives the Inspector's auto-generated UI (slice 4+).
|
|
19
|
+
*
|
|
20
|
+
* The script registry is built by the template (`import.meta.glob`), passed
|
|
21
|
+
* to `createUnboxyGame({ renderScripts })`, and resolved by path string at
|
|
22
|
+
* spawn time. Path strings in scene files match the registry keys exactly:
|
|
23
|
+
* usually `src/visuals/<name>.ts`.
|
|
24
|
+
*/
|
|
25
|
+
export interface RenderScriptModule<P extends Record<string, unknown> = Record<string, unknown>> {
|
|
26
|
+
render: (g: Phaser.GameObjects.Graphics, params: P) => void;
|
|
27
|
+
defaultParams?: P;
|
|
28
|
+
paramSchema?: unknown;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Stash a render-script registry on the Phaser game so `loadWorldScene` /
|
|
32
|
+
* `EditorBridge.applyEdit` can find it without explicit plumbing.
|
|
33
|
+
*
|
|
34
|
+
* The map's keys are absolute paths matching what scene files reference
|
|
35
|
+
* (e.g. `src/visuals/coin.ts`). Templates that build via `import.meta.glob`
|
|
36
|
+
* commonly produce relative keys like `./visuals/coin.ts`; the resolver
|
|
37
|
+
* normalises trailing slashes / leading `./` so both shapes work.
|
|
38
|
+
*/
|
|
39
|
+
export declare function setRenderScriptRegistry(game: Phaser.Game, scripts: Record<string, RenderScriptModule>): void;
|
|
40
|
+
export declare function getRenderScriptRegistry(game: Phaser.Game): Record<string, RenderScriptModule> | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the `render` function for a script path, or undefined if no
|
|
43
|
+
* matching module is registered. The matcher is forgiving:
|
|
44
|
+
*
|
|
45
|
+
* - exact match (`src/visuals/coin.ts`)
|
|
46
|
+
* - leading `./` stripped (`./visuals/coin.ts` matches `src/visuals/coin.ts`)
|
|
47
|
+
* - filename-only fallback so renames in the registry that drop the
|
|
48
|
+
* `src/` prefix still resolve.
|
|
49
|
+
*
|
|
50
|
+
* The forgiveness lets templates use whatever import.meta.glob shape they
|
|
51
|
+
* find natural without forcing a one-true-key convention.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveRenderScript(game: Phaser.Game, scriptPath: string): ((g: Phaser.GameObjects.Graphics, params: Record<string, unknown>) => void) | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const REGISTRY_KEY = '__unboxyRenderScriptRegistry';
|
|
2
|
+
/**
|
|
3
|
+
* Stash a render-script registry on the Phaser game so `loadWorldScene` /
|
|
4
|
+
* `EditorBridge.applyEdit` can find it without explicit plumbing.
|
|
5
|
+
*
|
|
6
|
+
* The map's keys are absolute paths matching what scene files reference
|
|
7
|
+
* (e.g. `src/visuals/coin.ts`). Templates that build via `import.meta.glob`
|
|
8
|
+
* commonly produce relative keys like `./visuals/coin.ts`; the resolver
|
|
9
|
+
* normalises trailing slashes / leading `./` so both shapes work.
|
|
10
|
+
*/
|
|
11
|
+
export function setRenderScriptRegistry(game, scripts) {
|
|
12
|
+
const bag = game;
|
|
13
|
+
bag[REGISTRY_KEY] = scripts;
|
|
14
|
+
}
|
|
15
|
+
export function getRenderScriptRegistry(game) {
|
|
16
|
+
const bag = game;
|
|
17
|
+
return bag[REGISTRY_KEY];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns the `render` function for a script path, or undefined if no
|
|
21
|
+
* matching module is registered. The matcher is forgiving:
|
|
22
|
+
*
|
|
23
|
+
* - exact match (`src/visuals/coin.ts`)
|
|
24
|
+
* - leading `./` stripped (`./visuals/coin.ts` matches `src/visuals/coin.ts`)
|
|
25
|
+
* - filename-only fallback so renames in the registry that drop the
|
|
26
|
+
* `src/` prefix still resolve.
|
|
27
|
+
*
|
|
28
|
+
* The forgiveness lets templates use whatever import.meta.glob shape they
|
|
29
|
+
* find natural without forcing a one-true-key convention.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveRenderScript(game, scriptPath) {
|
|
32
|
+
const registry = getRenderScriptRegistry(game);
|
|
33
|
+
if (!registry)
|
|
34
|
+
return undefined;
|
|
35
|
+
// Direct hit.
|
|
36
|
+
if (registry[scriptPath])
|
|
37
|
+
return registry[scriptPath].render;
|
|
38
|
+
// Try common normalisations.
|
|
39
|
+
const candidates = candidateKeys(scriptPath);
|
|
40
|
+
for (const c of candidates) {
|
|
41
|
+
if (registry[c])
|
|
42
|
+
return registry[c].render;
|
|
43
|
+
}
|
|
44
|
+
// Filename-only fallback.
|
|
45
|
+
const filename = scriptPath.split('/').pop();
|
|
46
|
+
if (filename) {
|
|
47
|
+
for (const key of Object.keys(registry)) {
|
|
48
|
+
if (key.endsWith('/' + filename) || key === filename) {
|
|
49
|
+
return registry[key].render;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function candidateKeys(scriptPath) {
|
|
56
|
+
const out = [];
|
|
57
|
+
const trimmed = scriptPath.replace(/^\.\//, '');
|
|
58
|
+
out.push(trimmed);
|
|
59
|
+
// src/visuals/coin.ts → ./visuals/coin.ts
|
|
60
|
+
if (trimmed.startsWith('src/'))
|
|
61
|
+
out.push('./' + trimmed.slice(4));
|
|
62
|
+
// visuals/coin.ts → src/visuals/coin.ts
|
|
63
|
+
if (!trimmed.startsWith('src/') && !trimmed.startsWith('./')) {
|
|
64
|
+
out.push('src/' + trimmed);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -94,14 +94,45 @@ 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
|
+
// shouldn't crash the whole scene boot — the agent might be writing
|
|
103
|
+
// the script in a follow-up turn. Render a clear "?" placeholder so
|
|
104
|
+
// the user sees the entity exists but the visual is unresolved.
|
|
105
|
+
const ph = ctx.scene.add.graphics();
|
|
106
|
+
ph.fillStyle(0x666666, 0.4);
|
|
107
|
+
ph.fillRect(-w / 2, -h / 2, w, h);
|
|
108
|
+
ph.lineStyle(2, 0xff8800, 0.9);
|
|
109
|
+
ph.strokeRect(-w / 2, -h / 2, w, h);
|
|
110
|
+
sizeForHitTest(ph, w, h);
|
|
111
|
+
ph.setData('renderScriptMissing', entity.visual.script);
|
|
112
|
+
return ph;
|
|
100
113
|
}
|
|
101
114
|
const g = ctx.scene.add.graphics();
|
|
102
|
-
|
|
115
|
+
const params = entity.visual.params ?? {};
|
|
116
|
+
render(g, params);
|
|
117
|
+
sizeForHitTest(g, w, h);
|
|
118
|
+
// Stash data needed for live re-render from EditorBridge.applyEdit.
|
|
119
|
+
g.setData('renderScriptPath', entity.visual.script);
|
|
120
|
+
g.setData('renderScriptParams', params);
|
|
103
121
|
return g;
|
|
104
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
|
+
}
|
|
105
136
|
function applyTransform(go, t) {
|
|
106
137
|
// Most game objects implement Transform; Container does too. Cast through
|
|
107
138
|
// a permissive shape so this works for sprite/rect/circle/container alike.
|
package/dist/scene/types.d.ts
CHANGED
|
@@ -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';
|