@umicat/phaser-sdk 1.0.0

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.
Files changed (62) hide show
  1. package/SDK-GUIDE.md +1726 -0
  2. package/dist/core/Transport.d.ts +28 -0
  3. package/dist/core/Transport.js +7 -0
  4. package/dist/core/Umicat.d.ts +45 -0
  5. package/dist/core/Umicat.js +60 -0
  6. package/dist/core/UmicatGame.d.ts +43 -0
  7. package/dist/core/UmicatGame.js +64 -0
  8. package/dist/core/UmicatScene.d.ts +19 -0
  9. package/dist/core/UmicatScene.js +38 -0
  10. package/dist/core/transports/LocalStorageTransport.d.ts +22 -0
  11. package/dist/core/transports/LocalStorageTransport.js +78 -0
  12. package/dist/core/transports/PostMessageTransport.d.ts +28 -0
  13. package/dist/core/transports/PostMessageTransport.js +105 -0
  14. package/dist/editor/EditorBridge.d.ts +114 -0
  15. package/dist/editor/EditorBridge.js +2608 -0
  16. package/dist/editor/EditorOverlayScene.d.ts +333 -0
  17. package/dist/editor/EditorOverlayScene.js +1896 -0
  18. package/dist/editor/EditorState.d.ts +251 -0
  19. package/dist/editor/EditorState.js +197 -0
  20. package/dist/gamedata/GameDataModule.d.ts +45 -0
  21. package/dist/gamedata/GameDataModule.js +59 -0
  22. package/dist/index.d.ts +43 -0
  23. package/dist/index.js +43 -0
  24. package/dist/orientation.d.ts +5 -0
  25. package/dist/orientation.js +4 -0
  26. package/dist/protocol.d.ts +807 -0
  27. package/dist/protocol.js +3 -0
  28. package/dist/realtime/RealtimeModule.d.ts +93 -0
  29. package/dist/realtime/RealtimeModule.js +115 -0
  30. package/dist/realtime/UmicatRoom.d.ts +197 -0
  31. package/dist/realtime/UmicatRoom.js +353 -0
  32. package/dist/recording/RecordingManager.d.ts +11 -0
  33. package/dist/recording/RecordingManager.js +59 -0
  34. package/dist/saves/SavesModule.d.ts +23 -0
  35. package/dist/saves/SavesModule.js +37 -0
  36. package/dist/scene/EditorMode.d.ts +17 -0
  37. package/dist/scene/EditorMode.js +22 -0
  38. package/dist/scene/EntityRegistry.d.ts +39 -0
  39. package/dist/scene/EntityRegistry.js +103 -0
  40. package/dist/scene/GameConfig.d.ts +60 -0
  41. package/dist/scene/GameConfig.js +50 -0
  42. package/dist/scene/HudRuntime.d.ts +131 -0
  43. package/dist/scene/HudRuntime.js +1224 -0
  44. package/dist/scene/Prefabs.d.ts +92 -0
  45. package/dist/scene/Prefabs.js +175 -0
  46. package/dist/scene/Rules.d.ts +73 -0
  47. package/dist/scene/Rules.js +164 -0
  48. package/dist/scene/SceneLoader.d.ts +118 -0
  49. package/dist/scene/SceneLoader.js +615 -0
  50. package/dist/scene/Waves.d.ts +85 -0
  51. package/dist/scene/Waves.js +365 -0
  52. package/dist/scene/autotile.d.ts +103 -0
  53. package/dist/scene/autotile.js +321 -0
  54. package/dist/scene/renderScripts.d.ts +53 -0
  55. package/dist/scene/renderScripts.js +67 -0
  56. package/dist/scene/spawnEntity.d.ts +201 -0
  57. package/dist/scene/spawnEntity.js +1326 -0
  58. package/dist/scene/types.d.ts +1166 -0
  59. package/dist/scene/types.js +34 -0
  60. package/dist/screenshot/ScreenshotManager.d.ts +14 -0
  61. package/dist/screenshot/ScreenshotManager.js +33 -0
  62. package/package.json +35 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Prefabs — entity TYPE definitions for runtime spawning.
3
+ *
4
+ * Slice 11 (Phase B.1, 2026-05-12). Closes the editor-value gap for
5
+ * runtime-spawn-dominated games (shooters, runners, survivors). Today's
6
+ * scene-as-data only knows about *instances at positions*; prefabs add
7
+ * *types* — a Galaga enemy template the wave system spawns 18 instances
8
+ * of, all sharing one editable record in the manifest.
9
+ *
10
+ * SDK 0.3.0 flattened the schema: prefab fields (assetId / width / params /
11
+ * etc.) live at the top level — exactly like world entities. There is no
12
+ * nested `visual: { ... }` middle layer.
13
+ *
14
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §6
15
+ *
16
+ * Public API:
17
+ * - `spawnPrefab(scene, prefabId, x, y, overrides?)` — create and register
18
+ * a runtime instance. Replaces the inline
19
+ * `this.physics.add.sprite(x, y, 'tex')` pattern.
20
+ * - `getPrefab(scene, prefabId)` — read the prefab record from the cached
21
+ * manifest. Throws if the prefab is missing.
22
+ * - `listPrefabs(scene, role?)` — enumerate prefabs, optionally filtered
23
+ * by role (e.g. `listPrefabs(this, 'enemy')`).
24
+ */
25
+ import Phaser from 'phaser';
26
+ import { PrefabPhysics, PrefabRecord, Transform } from './types.js';
27
+ import { RenderScriptResolver, AssetResolver } from './spawnEntity.js';
28
+ /**
29
+ * Spawn-time overrides for a prefab. Each field is deep-merged on top of
30
+ * the prefab record before instantiation. Render-field overrides live at
31
+ * the root — same flat shape as the prefab record itself. The override
32
+ * type is intentionally loose (`Record<string, unknown>`) so per-kind
33
+ * fields (assetId on sprite, fillColor on rect/circle, params on
34
+ * code-rendered) can all be passed through the same channel.
35
+ *
36
+ * spawnPrefab(this, 'enemy_grunt', x, y, {
37
+ * fields: { params: { color: '#ff0066' } }, // boss recolor
38
+ * properties: { hp: 30 },
39
+ * });
40
+ */
41
+ export interface SpawnPrefabOverrides {
42
+ role?: string;
43
+ /**
44
+ * Render-field overrides — deep-merged onto the prefab's render fields
45
+ * (assetId / tint / width / fillColor / radius / params / etc., depending
46
+ * on prefab.kind). Pass the same field names that live at the root of the
47
+ * PrefabRecord.
48
+ */
49
+ fields?: Record<string, unknown>;
50
+ physics?: Partial<PrefabPhysics>;
51
+ properties?: Record<string, unknown>;
52
+ /** Optional transform overrides (rotation, scale, depth — x/y come from spawn args). */
53
+ transform?: Partial<Omit<Transform, 'x' | 'y'>>;
54
+ }
55
+ export interface SpawnPrefabOptions {
56
+ /** Custom asset resolver. Defaults to manifest.assets lookup. */
57
+ resolveAsset?: AssetResolver;
58
+ /** Custom render-script resolver. Defaults to the global registry. */
59
+ resolveRenderScript?: RenderScriptResolver;
60
+ }
61
+ /**
62
+ * Look up a prefab record by id. Reads from the cached manifest loaded
63
+ * by `loadWorldScene`. Throws if no manifest is cached or the id isn't
64
+ * declared. Use `listPrefabs(scene)` to enumerate first if the caller
65
+ * isn't sure what's available.
66
+ */
67
+ export declare function getPrefab(scene: Phaser.Scene, prefabId: string): PrefabRecord;
68
+ /**
69
+ * Enumerate prefabs in the manifest, optionally filtered by role. Returns
70
+ * an empty array when the manifest has no prefabs at all.
71
+ */
72
+ export declare function listPrefabs(scene: Phaser.Scene, role?: string): PrefabRecord[];
73
+ /**
74
+ * Spawn a runtime instance of a prefab at (x, y).
75
+ *
76
+ * Pipeline:
77
+ * 1. Look up the prefab in the manifest.
78
+ * 2. Merge `overrides` (deep) on top of the prefab record.
79
+ * 3. Build a synthetic `WorldEntity` carrying spawn coordinates and the
80
+ * merged render / physics / properties, then call `spawnEntity` (the
81
+ * same code path authored entities use) — which creates the GameObject
82
+ * AND applies the physics block (`physics.add.existing` + body size /
83
+ * offset / velocity / bounce / world-bounds).
84
+ * 4. Tag the GO with `entityPrefabId` for editor live-edit + record-keeping.
85
+ * 5. Register the instance with the EntityRegistry under a runtime-generated
86
+ * id (format `<prefabId>#<n>`) and the prefab's role.
87
+ *
88
+ * Returns the spawned GameObject. Caller is responsible for adding it to
89
+ * any Phaser groups/colliders/event listeners. On `destroy()`, the SDK
90
+ * auto-unregisters via a Phaser DESTROY event listener.
91
+ */
92
+ export declare function spawnPrefab(scene: Phaser.Scene, prefabId: string, x: number, y: number, overrides?: SpawnPrefabOverrides, options?: SpawnPrefabOptions): Phaser.GameObjects.GameObject;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Prefabs — entity TYPE definitions for runtime spawning.
3
+ *
4
+ * Slice 11 (Phase B.1, 2026-05-12). Closes the editor-value gap for
5
+ * runtime-spawn-dominated games (shooters, runners, survivors). Today's
6
+ * scene-as-data only knows about *instances at positions*; prefabs add
7
+ * *types* — a Galaga enemy template the wave system spawns 18 instances
8
+ * of, all sharing one editable record in the manifest.
9
+ *
10
+ * SDK 0.3.0 flattened the schema: prefab fields (assetId / width / params /
11
+ * etc.) live at the top level — exactly like world entities. There is no
12
+ * nested `visual: { ... }` middle layer.
13
+ *
14
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §6
15
+ *
16
+ * Public API:
17
+ * - `spawnPrefab(scene, prefabId, x, y, overrides?)` — create and register
18
+ * a runtime instance. Replaces the inline
19
+ * `this.physics.add.sprite(x, y, 'tex')` pattern.
20
+ * - `getPrefab(scene, prefabId)` — read the prefab record from the cached
21
+ * manifest. Throws if the prefab is missing.
22
+ * - `listPrefabs(scene, role?)` — enumerate prefabs, optionally filtered
23
+ * by role (e.g. `listPrefabs(this, 'enemy')`).
24
+ */
25
+ import Phaser from 'phaser';
26
+ import { spawnEntity } from './spawnEntity.js';
27
+ import { attachEntityRegistry, getEntityRegistry } from './EntityRegistry.js';
28
+ import { getManifest } from './SceneLoader.js';
29
+ import { resolveRenderScript } from './renderScripts.js';
30
+ /**
31
+ * Look up a prefab record by id. Reads from the cached manifest loaded
32
+ * by `loadWorldScene`. Throws if no manifest is cached or the id isn't
33
+ * declared. Use `listPrefabs(scene)` to enumerate first if the caller
34
+ * isn't sure what's available.
35
+ */
36
+ export function getPrefab(scene, prefabId) {
37
+ const manifest = getManifest(scene);
38
+ const prefab = manifest.prefabs?.find((p) => p.id === prefabId);
39
+ if (!prefab) {
40
+ throw new Error(`[umicat/prefab] no prefab with id '${prefabId}' in manifest.prefabs[] — declare it in scenes/manifest.json`);
41
+ }
42
+ return prefab;
43
+ }
44
+ /**
45
+ * Enumerate prefabs in the manifest, optionally filtered by role. Returns
46
+ * an empty array when the manifest has no prefabs at all.
47
+ */
48
+ export function listPrefabs(scene, role) {
49
+ const manifest = getManifest(scene);
50
+ const all = manifest.prefabs ?? [];
51
+ return role ? all.filter((p) => p.role === role) : all;
52
+ }
53
+ /**
54
+ * Spawn a runtime instance of a prefab at (x, y).
55
+ *
56
+ * Pipeline:
57
+ * 1. Look up the prefab in the manifest.
58
+ * 2. Merge `overrides` (deep) on top of the prefab record.
59
+ * 3. Build a synthetic `WorldEntity` carrying spawn coordinates and the
60
+ * merged render / physics / properties, then call `spawnEntity` (the
61
+ * same code path authored entities use) — which creates the GameObject
62
+ * AND applies the physics block (`physics.add.existing` + body size /
63
+ * offset / velocity / bounce / world-bounds).
64
+ * 4. Tag the GO with `entityPrefabId` for editor live-edit + record-keeping.
65
+ * 5. Register the instance with the EntityRegistry under a runtime-generated
66
+ * id (format `<prefabId>#<n>`) and the prefab's role.
67
+ *
68
+ * Returns the spawned GameObject. Caller is responsible for adding it to
69
+ * any Phaser groups/colliders/event listeners. On `destroy()`, the SDK
70
+ * auto-unregisters via a Phaser DESTROY event listener.
71
+ */
72
+ export function spawnPrefab(scene, prefabId, x, y, overrides = {}, options = {}) {
73
+ const prefab = getPrefab(scene, prefabId);
74
+ const merged = mergePrefab(prefab, overrides);
75
+ const registry = getOrAttachRegistry(scene);
76
+ const instanceId = registry.nextInstanceId(prefabId);
77
+ const entity = prefabToEntity(merged, instanceId, x, y, overrides.transform);
78
+ const ctx = {
79
+ scene,
80
+ registry,
81
+ resolveAsset: options.resolveAsset ?? defaultAssetResolver(scene),
82
+ resolveRenderScript: options.resolveRenderScript ?? ((path) => resolveRenderScript(scene.game, path)),
83
+ };
84
+ // spawnEntity creates the GameObject AND applies the merged physics block
85
+ // (carried on the synthetic entity by `prefabToEntity`) — same body-level
86
+ // path authored scene entities use.
87
+ const go = spawnEntity(ctx, entity);
88
+ // Tag the GO so editor live-edit (umicat:editor:editPrefab) can find all
89
+ // instances of this prefab to re-apply patches.
90
+ go.setData('entityPrefabId', prefabId);
91
+ // Re-register under the runtime id (spawnEntity already registered with
92
+ // the same id but without prefabId). Replace to add the prefab tag.
93
+ registry.unregister(instanceId);
94
+ registry.register(instanceId, merged.role, go, prefabId);
95
+ // Auto-unregister on destroy so dead instances don't bloat the registry.
96
+ go.once(Phaser.GameObjects.Events.DESTROY, () => {
97
+ registry.unregister(instanceId);
98
+ });
99
+ return go;
100
+ }
101
+ // --- Helpers --------------------------------------------------------------
102
+ function getOrAttachRegistry(scene) {
103
+ return getEntityRegistry(scene) ?? attachEntityRegistry(scene);
104
+ }
105
+ function defaultAssetResolver(scene) {
106
+ return (assetId) => {
107
+ const manifest = getManifest(scene);
108
+ const asset = manifest.assets?.find((a) => a.id === assetId);
109
+ if (!asset) {
110
+ throw new Error(`[umicat/prefab] manifest has no asset with id '${assetId}'`);
111
+ }
112
+ return asset;
113
+ };
114
+ }
115
+ /**
116
+ * Build a `WorldEntity` from a prefab + spawn coords. SDK 0.3.0: prefab
117
+ * and world-entity share the same flat shape per kind, so this is just a
118
+ * `{ ...prefab, id, transform }` spread — no shape transformation needed.
119
+ */
120
+ function prefabToEntity(prefab, instanceId, x, y, transformOverrides) {
121
+ const transform = { x, y, ...transformOverrides };
122
+ // PrefabBase fields (id, role, physics, properties) + per-kind render fields
123
+ // map 1:1 onto the matching WorldEntity variant. The discriminated `kind`
124
+ // makes the union-narrowing work without per-kind ceremony.
125
+ return {
126
+ ...prefab,
127
+ id: instanceId,
128
+ transform,
129
+ };
130
+ }
131
+ function mergePrefab(prefab, overrides) {
132
+ if (!hasAny(overrides))
133
+ return prefab;
134
+ // Render-field overrides merge into the prefab root (assetId / params / etc).
135
+ // PrefabBase fields (physics / properties / role) merge into their own slots.
136
+ const base = { ...prefab };
137
+ if (overrides.fields) {
138
+ for (const [k, v] of Object.entries(overrides.fields)) {
139
+ base[k] = deepMerge(base[k], v);
140
+ }
141
+ }
142
+ if (overrides.role !== undefined)
143
+ base.role = overrides.role;
144
+ if (overrides.physics !== undefined) {
145
+ base.physics = deepMerge(base.physics, overrides.physics);
146
+ }
147
+ if (overrides.properties !== undefined) {
148
+ base.properties = { ...(prefab.properties ?? {}), ...overrides.properties };
149
+ }
150
+ return base;
151
+ }
152
+ function hasAny(overrides) {
153
+ return !!(overrides.role ||
154
+ overrides.fields ||
155
+ overrides.physics ||
156
+ overrides.properties ||
157
+ overrides.transform);
158
+ }
159
+ function deepMerge(base, patch) {
160
+ if (patch === undefined)
161
+ return base;
162
+ if (base === undefined)
163
+ return patch;
164
+ if (isPlainObject(base) && isPlainObject(patch)) {
165
+ const out = { ...base };
166
+ for (const [k, v] of Object.entries(patch)) {
167
+ out[k] = deepMerge(base[k], v);
168
+ }
169
+ return out;
170
+ }
171
+ return patch;
172
+ }
173
+ function isPlainObject(v) {
174
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
175
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Rules — game parameters as data.
3
+ *
4
+ * Slice 11 (Phase B.2, 2026-05-12). Free-form key-value JSON tree at
5
+ * `public/rules.json`, accessed via dot-notation paths
6
+ * (`getRule(scene, 'balance.lives', 3)`). Replaces TS `const X = ...`
7
+ * declarations for any value a user might want to tune — lives, score
8
+ * thresholds, fire cooldowns, gravity, win conditions.
9
+ *
10
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §8
11
+ *
12
+ * Public API:
13
+ * - `preloadRules(scene)` — queue load of `public/rules.json` in BootScene's
14
+ * `preload`. Tolerant of missing file (treats as empty `{}`).
15
+ * - `getRule(scene, path, fallback)` — one-shot read of a value by dot path.
16
+ * - `getRules(scene)` — read the full rules tree.
17
+ * - `onRuleChange(scene, path, handler)` — subscribe to editor patches.
18
+ * - `patchRule(scene, path, value)` — apply an editor edit (internal use;
19
+ * the EditorBridge will call this when `umicat:editor:patchRule` arrives).
20
+ */
21
+ import Phaser from 'phaser';
22
+ type RulesTree = Record<string, unknown>;
23
+ /**
24
+ * Queue a load of `public/rules.json` in a Phaser scene's `preload()` —
25
+ * typically alongside `preloadManifest`. Missing file is tolerated:
26
+ * a 404 inserts an empty `{}` so subsequent `getRule` calls return
27
+ * fallback values cleanly.
28
+ */
29
+ export declare function preloadRules(scene: Phaser.Scene): void;
30
+ /**
31
+ * Read the full rules tree. Returns `{}` if rules were never loaded or
32
+ * the file was missing. Most callers should use `getRule(...)` for a
33
+ * specific path; this is for code that needs the whole tree (e.g.,
34
+ * displaying a debug HUD of all rules).
35
+ */
36
+ export declare function getRules(scene: Phaser.Scene): RulesTree;
37
+ /**
38
+ * Read a single rule by dot-notation path. Returns `fallback` if the path
39
+ * is unset or rules weren't loaded. The fallback's type is the return type
40
+ * — keep it explicit so calling code doesn't drift.
41
+ *
42
+ * Example:
43
+ * const lives = getRule(this, 'balance.lives', 3);
44
+ * const gravity = getRule(this, 'physics.gravity.y', 980);
45
+ */
46
+ export declare function getRule<T>(scene: Phaser.Scene, path: string, fallback: T): T;
47
+ /**
48
+ * Subscribe to live rule changes from the editor. Handler fires whenever:
49
+ * - The exact `path` is patched.
50
+ * - Any descendant of `path` is patched (subscribing to `'balance'`
51
+ * fires on edits to `'balance.lives'`, `'balance.shootCooldownMs'`, etc.).
52
+ *
53
+ * Returns an unsubscribe function. Always call it on scene shutdown to
54
+ * avoid leaks:
55
+ *
56
+ * const unsub = onRuleChange(this, 'balance.shootCooldownMs', (v) => {
57
+ * this.shootCooldown = v as number;
58
+ * });
59
+ * this.events.once(Phaser.Scenes.Events.SHUTDOWN, unsub);
60
+ */
61
+ export declare function onRuleChange<T>(scene: Phaser.Scene, path: string, handler: (value: T) => void): () => void;
62
+ /**
63
+ * Apply an editor patch to a rule path. Updates the cache + fires every
64
+ * subscriber whose path is the patched path OR an ancestor of it (so a
65
+ * subscriber to `'balance'` sees an edit to `'balance.lives'`).
66
+ *
67
+ * Called by `EditorBridge` when the host sends
68
+ * `{ type: 'umicat:editor:patchRule', path, value }`. Direct use from
69
+ * game code is allowed but unusual — rules are typically read-only at
70
+ * runtime; the source of truth is the JSON file + editor edits.
71
+ */
72
+ export declare function patchRule(scene: Phaser.Scene, path: string, value: unknown): void;
73
+ export {};
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Rules — game parameters as data.
3
+ *
4
+ * Slice 11 (Phase B.2, 2026-05-12). Free-form key-value JSON tree at
5
+ * `public/rules.json`, accessed via dot-notation paths
6
+ * (`getRule(scene, 'balance.lives', 3)`). Replaces TS `const X = ...`
7
+ * declarations for any value a user might want to tune — lives, score
8
+ * thresholds, fire cooldowns, gravity, win conditions.
9
+ *
10
+ * Design: umicat-design/features/visual-editor/11-game-data-foundation.md §8
11
+ *
12
+ * Public API:
13
+ * - `preloadRules(scene)` — queue load of `public/rules.json` in BootScene's
14
+ * `preload`. Tolerant of missing file (treats as empty `{}`).
15
+ * - `getRule(scene, path, fallback)` — one-shot read of a value by dot path.
16
+ * - `getRules(scene)` — read the full rules tree.
17
+ * - `onRuleChange(scene, path, handler)` — subscribe to editor patches.
18
+ * - `patchRule(scene, path, value)` — apply an editor edit (internal use;
19
+ * the EditorBridge will call this when `umicat:editor:patchRule` arrives).
20
+ */
21
+ import Phaser from 'phaser';
22
+ const RULES_PATH = 'rules.json';
23
+ const RULES_CACHE_KEY = 'umicat:rules';
24
+ const SUBSCRIBERS_KEY = '__unboxyRulesSubscribers';
25
+ /**
26
+ * Queue a load of `public/rules.json` in a Phaser scene's `preload()` —
27
+ * typically alongside `preloadManifest`. Missing file is tolerated:
28
+ * a 404 inserts an empty `{}` so subsequent `getRule` calls return
29
+ * fallback values cleanly.
30
+ */
31
+ export function preloadRules(scene) {
32
+ if (scene.cache.json.exists(RULES_CACHE_KEY))
33
+ return;
34
+ // Phaser fires FILE_LOAD_ERROR on 404; we install a single-shot listener
35
+ // that injects an empty cache entry so downstream `getRule` calls don't
36
+ // throw "rules not loaded". The listener removes itself after firing once.
37
+ const onError = (file) => {
38
+ if (file.key !== RULES_CACHE_KEY)
39
+ return;
40
+ if (!scene.cache.json.exists(RULES_CACHE_KEY)) {
41
+ scene.cache.json.add(RULES_CACHE_KEY, {});
42
+ }
43
+ scene.load.off(Phaser.Loader.Events.FILE_LOAD_ERROR, onError);
44
+ };
45
+ scene.load.on(Phaser.Loader.Events.FILE_LOAD_ERROR, onError);
46
+ scene.load.json(RULES_CACHE_KEY, RULES_PATH);
47
+ }
48
+ /**
49
+ * Read the full rules tree. Returns `{}` if rules were never loaded or
50
+ * the file was missing. Most callers should use `getRule(...)` for a
51
+ * specific path; this is for code that needs the whole tree (e.g.,
52
+ * displaying a debug HUD of all rules).
53
+ */
54
+ export function getRules(scene) {
55
+ const cached = scene.cache.json.get(RULES_CACHE_KEY);
56
+ return cached ?? {};
57
+ }
58
+ /**
59
+ * Read a single rule by dot-notation path. Returns `fallback` if the path
60
+ * is unset or rules weren't loaded. The fallback's type is the return type
61
+ * — keep it explicit so calling code doesn't drift.
62
+ *
63
+ * Example:
64
+ * const lives = getRule(this, 'balance.lives', 3);
65
+ * const gravity = getRule(this, 'physics.gravity.y', 980);
66
+ */
67
+ export function getRule(scene, path, fallback) {
68
+ const v = readPath(getRules(scene), path);
69
+ return v === undefined ? fallback : v;
70
+ }
71
+ /**
72
+ * Subscribe to live rule changes from the editor. Handler fires whenever:
73
+ * - The exact `path` is patched.
74
+ * - Any descendant of `path` is patched (subscribing to `'balance'`
75
+ * fires on edits to `'balance.lives'`, `'balance.shootCooldownMs'`, etc.).
76
+ *
77
+ * Returns an unsubscribe function. Always call it on scene shutdown to
78
+ * avoid leaks:
79
+ *
80
+ * const unsub = onRuleChange(this, 'balance.shootCooldownMs', (v) => {
81
+ * this.shootCooldown = v as number;
82
+ * });
83
+ * this.events.once(Phaser.Scenes.Events.SHUTDOWN, unsub);
84
+ */
85
+ export function onRuleChange(scene, path, handler) {
86
+ const subs = getOrCreateSubscribers(scene);
87
+ const set = subs.get(path) ?? new Set();
88
+ const wrapped = (v) => handler(v);
89
+ set.add(wrapped);
90
+ subs.set(path, set);
91
+ return () => {
92
+ set.delete(wrapped);
93
+ if (set.size === 0)
94
+ subs.delete(path);
95
+ };
96
+ }
97
+ /**
98
+ * Apply an editor patch to a rule path. Updates the cache + fires every
99
+ * subscriber whose path is the patched path OR an ancestor of it (so a
100
+ * subscriber to `'balance'` sees an edit to `'balance.lives'`).
101
+ *
102
+ * Called by `EditorBridge` when the host sends
103
+ * `{ type: 'umicat:editor:patchRule', path, value }`. Direct use from
104
+ * game code is allowed but unusual — rules are typically read-only at
105
+ * runtime; the source of truth is the JSON file + editor edits.
106
+ */
107
+ export function patchRule(scene, path, value) {
108
+ const rules = getRules(scene);
109
+ writePath(rules, path, value);
110
+ // Re-add to the cache so subsequent reads see the patched tree. Phaser's
111
+ // JSON cache stores by reference, so the in-place mutation already affects
112
+ // reads — but `add` calls invalidate the right internal flags too.
113
+ scene.cache.json.add(RULES_CACHE_KEY, rules);
114
+ const subs = getSubscribers(scene);
115
+ if (!subs)
116
+ return;
117
+ for (const [subPath, set] of subs) {
118
+ if (subPath === path || path === subPath || path.startsWith(subPath + '.') || subPath.startsWith(path + '.')) {
119
+ const v = readPath(rules, subPath);
120
+ set.forEach((h) => h(v));
121
+ }
122
+ }
123
+ }
124
+ // --- Internals ------------------------------------------------------------
125
+ function getSubscribers(scene) {
126
+ const game = scene.game;
127
+ return game[SUBSCRIBERS_KEY];
128
+ }
129
+ function getOrCreateSubscribers(scene) {
130
+ const game = scene.game;
131
+ let subs = game[SUBSCRIBERS_KEY];
132
+ if (!subs) {
133
+ subs = new Map();
134
+ game[SUBSCRIBERS_KEY] = subs;
135
+ }
136
+ return subs;
137
+ }
138
+ function readPath(obj, path) {
139
+ if (!path)
140
+ return obj;
141
+ const parts = path.split('.');
142
+ let cur = obj;
143
+ for (const p of parts) {
144
+ if (cur === null || typeof cur !== 'object')
145
+ return undefined;
146
+ cur = cur[p];
147
+ if (cur === undefined)
148
+ return undefined;
149
+ }
150
+ return cur;
151
+ }
152
+ function writePath(obj, path, value) {
153
+ const parts = path.split('.');
154
+ let cur = obj;
155
+ for (let i = 0; i < parts.length - 1; i++) {
156
+ const p = parts[i];
157
+ const existing = cur[p];
158
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
159
+ cur[p] = {};
160
+ }
161
+ cur = cur[p];
162
+ }
163
+ cur[parts[parts.length - 1]] = value;
164
+ }
@@ -0,0 +1,118 @@
1
+ import Phaser from 'phaser';
2
+ import { Manifest, SceneFile, WorldScene } from './types.js';
3
+ import { RenderScriptResolver } from './spawnEntity.js';
4
+ import { EntityRegistry } from './EntityRegistry.js';
5
+ /**
6
+ * Where scene files live in the workspace and at runtime. The Vite build
7
+ * copies `public/` to the served root, so paths are origin-relative
8
+ * (`scenes/manifest.json`). No leading slash — the iframe is sometimes
9
+ * served under a sub-path (`/preview/:gameId/`).
10
+ */
11
+ export declare const SCENES_BASE = "scenes/";
12
+ export declare const MANIFEST_PATH = "scenes/manifest.json";
13
+ /**
14
+ * Fetches the manifest via Phaser's loader. Must be called from a Phaser
15
+ * scene's `preload()` since it queues a load request. Subsequent calls
16
+ * (e.g. on scene transitions) hit the Phaser JSON cache.
17
+ *
18
+ * Resolves once Phaser fires `complete` for the load batch — callers
19
+ * typically await this in `create()` after `this.load.start()` is done.
20
+ */
21
+ export declare function preloadManifest(scene: Phaser.Scene): void;
22
+ /**
23
+ * Read the manifest from Phaser's JSON cache. Throws if `preloadManifest`
24
+ * hasn't completed first.
25
+ */
26
+ export declare function getManifest(scene: Phaser.Scene): Manifest;
27
+ /**
28
+ * Walk a scene file's entities and queue Phaser loads for any assets that
29
+ * aren't already in the texture cache. Caller is responsible for awaiting
30
+ * the loader (`scene.load.once('complete', ...)` or do it in `preload()`).
31
+ *
32
+ * Idempotent across scenes — once an asset is loaded, switching to another
33
+ * scene that uses it is free.
34
+ */
35
+ export declare function preloadSceneAssets(scene: Phaser.Scene, sceneFile: SceneFile, manifest: Manifest): void;
36
+ /**
37
+ * Walk every `kind=spritesheet` asset in the manifest and register each one's
38
+ * `animations[]` as a Phaser animation, so behavior code can call
39
+ * `sprite.play('walk-down')` directly. Idempotent — uses `scene.anims.exists()`
40
+ * to skip duplicates (Phaser's anims.create throws otherwise).
41
+ *
42
+ * <p>Animation keys are scene-global. Two sheets registering the same name
43
+ * (e.g. two characters both with `walk-down`) → first wins, second logs a
44
+ * warning. Sheets that need disambiguation should namespace their keys
45
+ * (e.g. `cow:walk-down`) at metadata time.
46
+ */
47
+ /**
48
+ * Walk every asset in the manifest and apply `Phaser.Textures.FilterMode.NEAREST`
49
+ * to each `pixelArt: true` entry's loaded texture. Without this, pixel-art
50
+ * sprites render bilinear-blurred — visible immediately on imported character
51
+ * sheets when the agent hasn't manually set Phaser game config's `pixelArt`.
52
+ *
53
+ * <p>Idempotent: setFilter is safe to call multiple times. Skip silently when
54
+ * a texture isn't loaded yet (next call after that asset is requested will
55
+ * pick it up). Wrap in try/catch so a single bad asset doesn't block the rest
56
+ * of scene init.
57
+ *
58
+ * <p>Per-asset NEAREST closes the visible blur gap; the game-wide flag
59
+ * (`Phaser.Game.config.pixelArt` + `camera.setRoundPixels`) is a separate
60
+ * concern (framebuffer-stretch crispness, perf) deferred to a follow-up.
61
+ */
62
+ export declare function applyPixelArtFilters(scene: Phaser.Scene, manifest: Manifest): void;
63
+ /**
64
+ * Look up per-frame 9-slice metadata for an atlas-format asset. Returns the
65
+ * `NinePatchConfig` declared inline on the named frame in the atlas JSON, or
66
+ * `undefined` if the asset isn't an atlas, the cache miss happened, or the
67
+ * frame has no `ninePatch` entry (plain stretched draw).
68
+ */
69
+ export declare function getAtlasFrameNinePatch(scene: Phaser.Scene, textureKey: string, frameName: string): {
70
+ leftWidth: number;
71
+ rightWidth: number;
72
+ topHeight: number;
73
+ bottomHeight: number;
74
+ } | undefined;
75
+ export interface LoadWorldSceneOptions {
76
+ /**
77
+ * Optional render-script resolver for `code-rendered` entities. When
78
+ * absent, the loader throws on code-rendered entities (slice 1).
79
+ */
80
+ resolveRenderScript?: RenderScriptResolver;
81
+ }
82
+ /**
83
+ * Suspend the scene's update/physics/tween processing while async work
84
+ * runs inside `create()`. Phaser does not await an async `create()` —
85
+ * the scene transitions to RUNNING as soon as the call returns its
86
+ * Promise, and `update()` starts ticking on the next frame regardless of
87
+ * whether the awaited load has settled. Without this guard, any
88
+ * `update()` code that reads class fields populated AFTER the await
89
+ * (e.g. `this.player.body`) crashes on frame 1 with
90
+ * `Cannot read properties of undefined (reading 'body')`.
91
+ *
92
+ * Idempotent: if the scene is already paused (e.g. the editor has
93
+ * paused everything via `setupEditorModeListener`), we leave it that
94
+ * way and the returned release is a no-op.
95
+ */
96
+ export declare function suspendSceneUpdates(scene: Phaser.Scene): () => void;
97
+ /**
98
+ * One-shot async loader: fetch scene JSON (if not already cached), lazy
99
+ * preload its assets, spawn entities, configure camera, and return the
100
+ * EntityRegistry. Idempotent re-loads of the same scene id reuse the
101
+ * Phaser JSON cache.
102
+ *
103
+ * Phaser's update loop is suspended for the duration of the await so
104
+ * `update()` can safely read fields the agent assigns AFTER `await
105
+ * loadWorldScene(...)` — no manual `if (!this.player) return;` guard
106
+ * required.
107
+ *
108
+ * Pattern (in `GameScene.create()`):
109
+ *
110
+ * ```ts
111
+ * const result = await loadWorldScene(this, sceneId);
112
+ * // entities live; behavior code can use result.registry.byRole('player')
113
+ * ```
114
+ */
115
+ export declare function loadWorldScene(scene: Phaser.Scene, sceneId: string, options?: LoadWorldSceneOptions): Promise<{
116
+ sceneFile: WorldScene;
117
+ registry: EntityRegistry;
118
+ }>;