@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.
- package/SDK-GUIDE.md +1726 -0
- package/dist/core/Transport.d.ts +28 -0
- package/dist/core/Transport.js +7 -0
- package/dist/core/Umicat.d.ts +45 -0
- package/dist/core/Umicat.js +60 -0
- package/dist/core/UmicatGame.d.ts +43 -0
- package/dist/core/UmicatGame.js +64 -0
- package/dist/core/UmicatScene.d.ts +19 -0
- package/dist/core/UmicatScene.js +38 -0
- package/dist/core/transports/LocalStorageTransport.d.ts +22 -0
- package/dist/core/transports/LocalStorageTransport.js +78 -0
- package/dist/core/transports/PostMessageTransport.d.ts +28 -0
- package/dist/core/transports/PostMessageTransport.js +105 -0
- package/dist/editor/EditorBridge.d.ts +114 -0
- package/dist/editor/EditorBridge.js +2608 -0
- package/dist/editor/EditorOverlayScene.d.ts +333 -0
- package/dist/editor/EditorOverlayScene.js +1896 -0
- package/dist/editor/EditorState.d.ts +251 -0
- package/dist/editor/EditorState.js +197 -0
- package/dist/gamedata/GameDataModule.d.ts +45 -0
- package/dist/gamedata/GameDataModule.js +59 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/orientation.d.ts +5 -0
- package/dist/orientation.js +4 -0
- package/dist/protocol.d.ts +807 -0
- package/dist/protocol.js +3 -0
- package/dist/realtime/RealtimeModule.d.ts +93 -0
- package/dist/realtime/RealtimeModule.js +115 -0
- package/dist/realtime/UmicatRoom.d.ts +197 -0
- package/dist/realtime/UmicatRoom.js +353 -0
- package/dist/recording/RecordingManager.d.ts +11 -0
- package/dist/recording/RecordingManager.js +59 -0
- package/dist/saves/SavesModule.d.ts +23 -0
- package/dist/saves/SavesModule.js +37 -0
- package/dist/scene/EditorMode.d.ts +17 -0
- package/dist/scene/EditorMode.js +22 -0
- package/dist/scene/EntityRegistry.d.ts +39 -0
- package/dist/scene/EntityRegistry.js +103 -0
- package/dist/scene/GameConfig.d.ts +60 -0
- package/dist/scene/GameConfig.js +50 -0
- package/dist/scene/HudRuntime.d.ts +131 -0
- package/dist/scene/HudRuntime.js +1224 -0
- package/dist/scene/Prefabs.d.ts +92 -0
- package/dist/scene/Prefabs.js +175 -0
- package/dist/scene/Rules.d.ts +73 -0
- package/dist/scene/Rules.js +164 -0
- package/dist/scene/SceneLoader.d.ts +118 -0
- package/dist/scene/SceneLoader.js +615 -0
- package/dist/scene/Waves.d.ts +85 -0
- package/dist/scene/Waves.js +365 -0
- package/dist/scene/autotile.d.ts +103 -0
- package/dist/scene/autotile.js +321 -0
- package/dist/scene/renderScripts.d.ts +53 -0
- package/dist/scene/renderScripts.js +67 -0
- package/dist/scene/spawnEntity.d.ts +201 -0
- package/dist/scene/spawnEntity.js +1326 -0
- package/dist/scene/types.d.ts +1166 -0
- package/dist/scene/types.js +34 -0
- package/dist/screenshot/ScreenshotManager.d.ts +14 -0
- package/dist/screenshot/ScreenshotManager.js +33 -0
- 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
|
+
}>;
|