@waica/engine 0.11.0 → 0.12.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/dist/archetype.d.ts +7 -0
- package/dist/component.d.ts +5 -0
- package/dist/game.d.ts +46 -1
- package/dist/game.js +124 -16
- package/dist/index.d.ts +1 -1
- package/dist/runtime-bridge.d.ts +9 -2
- package/dist/runtime-bridge.js +11 -2
- package/dist/runtime-inspection.d.ts +2 -0
- package/dist/runtime-inspection.js +1 -0
- package/dist/scene.d.ts +5 -1
- package/dist/scene.js +9 -2
- package/dist/ui.d.ts +12 -1
- package/dist/ui.js +35 -3
- package/package.json +1 -1
package/dist/archetype.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface ArchetypeManifest {
|
|
|
22
22
|
id: string;
|
|
23
23
|
label: string;
|
|
24
24
|
scene: SceneJson;
|
|
25
|
+
/**
|
|
26
|
+
* Additional demo scenes beyond `scene` (its name is always "main"),
|
|
27
|
+
* keyed by name — the isometric archetype's second demo scene (CA-13).
|
|
28
|
+
* `start:blank` never emits these; `start:demo` emits one file per entry
|
|
29
|
+
* alongside `scene`.
|
|
30
|
+
*/
|
|
31
|
+
extraScenes?: Readonly<Record<string, SceneJson>>;
|
|
25
32
|
blankScene: SceneJson;
|
|
26
33
|
registry: SceneRegistry;
|
|
27
34
|
palette: EntityTemplate[];
|
package/dist/component.d.ts
CHANGED
|
@@ -69,6 +69,11 @@ export declare abstract class Component {
|
|
|
69
69
|
onCollide?(other: Entity): void;
|
|
70
70
|
/** Runs when this entity's DynamicBody physically contacts a Solid. */
|
|
71
71
|
onContact?(contact: SolidContact): void;
|
|
72
|
+
/**
|
|
73
|
+
* Runs when this entity wins the nearest-Interactable scan and the
|
|
74
|
+
* initiator interacts with it (a keypress or a click-to-move NPC order).
|
|
75
|
+
*/
|
|
76
|
+
onInteract?(initiator: Entity): void;
|
|
72
77
|
/** Runs when the entity is destroyed or the component removed. */
|
|
73
78
|
onDestroy?(): void;
|
|
74
79
|
}
|
package/dist/game.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Entity } from './entity.js';
|
|
|
5
5
|
import { Emitter } from './events.js';
|
|
6
6
|
import { Input, type InputBindings } from './input.js';
|
|
7
7
|
import { Pointer } from './pointer.js';
|
|
8
|
-
import { type SceneRegistry, type SceneRenderJson } from './scene.js';
|
|
8
|
+
import { type SceneJson, type SceneRegistry, type SceneRenderJson } from './scene.js';
|
|
9
9
|
import { Stats, type StatValue } from './stats.js';
|
|
10
10
|
import { GameUi } from './ui.js';
|
|
11
11
|
/** Fixed game resolution: the view keeps this aspect, letterboxed. */
|
|
@@ -32,6 +32,11 @@ export interface SpawnPrefabOptions {
|
|
|
32
32
|
name?: string;
|
|
33
33
|
position?: [number, number];
|
|
34
34
|
}
|
|
35
|
+
/** The Project's scenes by name (a file's stem), plus the registry shared by all of them. */
|
|
36
|
+
export interface SceneCatalog {
|
|
37
|
+
scenes: Record<string, SceneJson>;
|
|
38
|
+
registry: SceneRegistry;
|
|
39
|
+
}
|
|
35
40
|
/** Persisted overrides: entity → componentName → prop → value. */
|
|
36
41
|
export type ParamOverrides = Record<string, Record<string, Record<string, number | boolean | string>>>;
|
|
37
42
|
/**
|
|
@@ -61,12 +66,22 @@ export declare class Game {
|
|
|
61
66
|
private readonly updateFns;
|
|
62
67
|
private readonly invalidUpdateCompositions;
|
|
63
68
|
private readonly resolution;
|
|
69
|
+
/** The constructor's viewHeight — unloadScene() restores it. */
|
|
70
|
+
private readonly baseViewHeight;
|
|
64
71
|
private viewHeight;
|
|
65
72
|
private sceneCamera;
|
|
66
73
|
private renderSort;
|
|
67
74
|
private sceneProjection;
|
|
68
75
|
private lastTime;
|
|
69
76
|
private runtimeBridge;
|
|
77
|
+
/** Host-registered scenes by name, resolved by loadSceneByName. Session-scoped. */
|
|
78
|
+
private sceneCatalog;
|
|
79
|
+
/** The live scene's name (its catalog key), or null with no scene loaded. */
|
|
80
|
+
private liveSceneName;
|
|
81
|
+
/** True for the whole extent of a runFrame() call, incl. its tail. */
|
|
82
|
+
private insideFrame;
|
|
83
|
+
/** A loadSceneByName() enqueued while insideFrame; applied at the next runFrame's start. */
|
|
84
|
+
private pendingSceneLoad;
|
|
70
85
|
constructor(options: GameOptions);
|
|
71
86
|
/** Creates a live entity in the scene. */
|
|
72
87
|
spawn(name: string): Entity;
|
|
@@ -74,6 +89,35 @@ export declare class Game {
|
|
|
74
89
|
spawnPrefab(prefab: string, options?: SpawnPrefabOptions): Entity | null;
|
|
75
90
|
/** Finds an entity by name. */
|
|
76
91
|
find(name: string): Entity | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Destroys the live scene — every entity (Entity.destroy(), so onDestroy
|
|
94
|
+
* cascades and GPU resources release) and its scene-scoped UI — and
|
|
95
|
+
* leaves the Game as newly constructed: no registry, no scene camera, no
|
|
96
|
+
* render sort or projection, viewHeight back to the constructor's.
|
|
97
|
+
* Session-scoped state (stats, paramOverrides, subscriptions, the scene
|
|
98
|
+
* catalog) is untouched. See ADR 0011. Public: the seam `loadScene` calls
|
|
99
|
+
* to replace a scene, and how a host leaves the Game with none loaded.
|
|
100
|
+
*/
|
|
101
|
+
unloadScene(): void;
|
|
102
|
+
/** Registers the Project's scenes by name, resolved by loadSceneByName. */
|
|
103
|
+
registerSceneCatalog(catalog: SceneCatalog): void;
|
|
104
|
+
/** The live scene's name (its catalog key), or null with no scene loaded. */
|
|
105
|
+
get sceneName(): string | null;
|
|
106
|
+
/** Names registered via registerSceneCatalog, in registration order. */
|
|
107
|
+
get availableScenes(): string[];
|
|
108
|
+
/**
|
|
109
|
+
* Resolves `name` through the registered catalog and loads it, replacing
|
|
110
|
+
* the live scene. An unknown name warns and leaves the live scene
|
|
111
|
+
* untouched. Triggered mid-frame (e.g. from a SceneTransition's
|
|
112
|
+
* onCollide/onInteract) the swap is deferred to the very start of the
|
|
113
|
+
* next runFrame — dispatchCollisions finishes its double loop over the
|
|
114
|
+
* outgoing scene, and the incoming scene's entities are present only
|
|
115
|
+
* from the next frame. Called from outside a frame (boot, or the Runtime
|
|
116
|
+
* Bridge's `scene` control operation) it applies synchronously and wins
|
|
117
|
+
* over anything queued earlier this frame. A second mid-frame request
|
|
118
|
+
* loses to the first and says so. Returns whether the load took effect.
|
|
119
|
+
*/
|
|
120
|
+
loadSceneByName(name: string): boolean;
|
|
77
121
|
/** Loads persisted parameter overrides (waica.params.json). */
|
|
78
122
|
loadParams(url: string): Promise<void>;
|
|
79
123
|
/** Applies persisted overrides to a freshly added component. */
|
|
@@ -104,6 +148,7 @@ export declare class Game {
|
|
|
104
148
|
private resumeRuntime;
|
|
105
149
|
private tick;
|
|
106
150
|
private runFrame;
|
|
151
|
+
private flushPendingSceneLoad;
|
|
107
152
|
private unregisterRuntimeBridge;
|
|
108
153
|
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
109
154
|
private applyYSort;
|
package/dist/game.js
CHANGED
|
@@ -11,7 +11,7 @@ import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.
|
|
|
11
11
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
12
12
|
import { projectIsometric } from './projection.js';
|
|
13
13
|
import { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
14
|
-
import { registryEntry, spawnFromJson } from './scene.js';
|
|
14
|
+
import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
15
15
|
import { Stats } from './stats.js';
|
|
16
16
|
import { GameUi } from './ui.js';
|
|
17
17
|
/**
|
|
@@ -42,14 +42,25 @@ export class Game {
|
|
|
42
42
|
updateFns = new Set();
|
|
43
43
|
invalidUpdateCompositions = new WeakMap();
|
|
44
44
|
resolution;
|
|
45
|
+
/** The constructor's viewHeight — unloadScene() restores it. */
|
|
46
|
+
baseViewHeight;
|
|
45
47
|
viewHeight;
|
|
46
48
|
sceneCamera = null;
|
|
47
49
|
renderSort = null;
|
|
48
50
|
sceneProjection = null;
|
|
49
51
|
lastTime = 0;
|
|
50
52
|
runtimeBridge = null;
|
|
53
|
+
/** Host-registered scenes by name, resolved by loadSceneByName. Session-scoped. */
|
|
54
|
+
sceneCatalog = null;
|
|
55
|
+
/** The live scene's name (its catalog key), or null with no scene loaded. */
|
|
56
|
+
liveSceneName = null;
|
|
57
|
+
/** True for the whole extent of a runFrame() call, incl. its tail. */
|
|
58
|
+
insideFrame = false;
|
|
59
|
+
/** A loadSceneByName() enqueued while insideFrame; applied at the next runFrame's start. */
|
|
60
|
+
pendingSceneLoad = null;
|
|
51
61
|
constructor(options) {
|
|
52
62
|
const { canvas, background = 0x1a1a2e, viewHeight = 10 } = options;
|
|
63
|
+
this.baseViewHeight = viewHeight;
|
|
53
64
|
this.viewHeight = viewHeight;
|
|
54
65
|
this.resolution = options.resolution ?? null;
|
|
55
66
|
this.input = new Input(options.bindings);
|
|
@@ -97,6 +108,84 @@ export class Game {
|
|
|
97
108
|
find(name) {
|
|
98
109
|
return this.entities.find((e) => e.name === name);
|
|
99
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Destroys the live scene — every entity (Entity.destroy(), so onDestroy
|
|
113
|
+
* cascades and GPU resources release) and its scene-scoped UI — and
|
|
114
|
+
* leaves the Game as newly constructed: no registry, no scene camera, no
|
|
115
|
+
* render sort or projection, viewHeight back to the constructor's.
|
|
116
|
+
* Session-scoped state (stats, paramOverrides, subscriptions, the scene
|
|
117
|
+
* catalog) is untouched. See ADR 0011. Public: the seam `loadScene` calls
|
|
118
|
+
* to replace a scene, and how a host leaves the Game with none loaded.
|
|
119
|
+
*/
|
|
120
|
+
unloadScene() {
|
|
121
|
+
this.ui.unloadScene();
|
|
122
|
+
// An explicit unload means "no scene": a swap queued earlier this frame
|
|
123
|
+
// would otherwise flush next frame and resurrect one.
|
|
124
|
+
this.pendingSceneLoad = null;
|
|
125
|
+
// Entity.destroy() splices itself out of `this.entities` in place — the
|
|
126
|
+
// Pointer holds that array by reference, so it must never be reassigned.
|
|
127
|
+
for (const entity of [...this.entities])
|
|
128
|
+
entity.destroy();
|
|
129
|
+
this.registry = null;
|
|
130
|
+
this.renderSort = null;
|
|
131
|
+
this.sceneProjection = null;
|
|
132
|
+
this.sceneCamera = null;
|
|
133
|
+
this.liveSceneName = null;
|
|
134
|
+
this.setViewHeight(this.baseViewHeight);
|
|
135
|
+
}
|
|
136
|
+
/** Registers the Project's scenes by name, resolved by loadSceneByName. */
|
|
137
|
+
registerSceneCatalog(catalog) {
|
|
138
|
+
this.sceneCatalog = catalog;
|
|
139
|
+
}
|
|
140
|
+
/** The live scene's name (its catalog key), or null with no scene loaded. */
|
|
141
|
+
get sceneName() {
|
|
142
|
+
return this.liveSceneName;
|
|
143
|
+
}
|
|
144
|
+
/** Names registered via registerSceneCatalog, in registration order. */
|
|
145
|
+
get availableScenes() {
|
|
146
|
+
return this.sceneCatalog ? Object.keys(this.sceneCatalog.scenes) : [];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolves `name` through the registered catalog and loads it, replacing
|
|
150
|
+
* the live scene. An unknown name warns and leaves the live scene
|
|
151
|
+
* untouched. Triggered mid-frame (e.g. from a SceneTransition's
|
|
152
|
+
* onCollide/onInteract) the swap is deferred to the very start of the
|
|
153
|
+
* next runFrame — dispatchCollisions finishes its double loop over the
|
|
154
|
+
* outgoing scene, and the incoming scene's entities are present only
|
|
155
|
+
* from the next frame. Called from outside a frame (boot, or the Runtime
|
|
156
|
+
* Bridge's `scene` control operation) it applies synchronously and wins
|
|
157
|
+
* over anything queued earlier this frame. A second mid-frame request
|
|
158
|
+
* loses to the first and says so. Returns whether the load took effect.
|
|
159
|
+
*/
|
|
160
|
+
loadSceneByName(name) {
|
|
161
|
+
const catalog = this.sceneCatalog;
|
|
162
|
+
const json = catalog ? registryEntry(catalog.scenes, name) : undefined;
|
|
163
|
+
if (!catalog || !json) {
|
|
164
|
+
console.warn(`[waica] unknown scene: "${name}"`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const apply = () => {
|
|
168
|
+
loadScene(this, json, catalog.registry);
|
|
169
|
+
this.liveSceneName = name;
|
|
170
|
+
};
|
|
171
|
+
if (!this.insideFrame) {
|
|
172
|
+
// Authoritative: dropping the queue is the point. A swap a transition
|
|
173
|
+
// enqueued earlier would otherwise flush on the next frame and silently
|
|
174
|
+
// undo this load, reporting success for a scene the caller never got.
|
|
175
|
+
this.pendingSceneLoad = null;
|
|
176
|
+
apply();
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (this.pendingSceneLoad) {
|
|
180
|
+
// Two transitions resolving in the same collision dispatch: the one the
|
|
181
|
+
// simulation reached first wins, and the loser is told. Last-write-wins
|
|
182
|
+
// would drop the player in the other door's destination with no signal.
|
|
183
|
+
console.warn(`[waica] a scene swap is already queued this frame; ignoring "${name}"`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
this.pendingSceneLoad = apply;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
100
189
|
/** Loads persisted parameter overrides (waica.params.json). */
|
|
101
190
|
async loadParams(url) {
|
|
102
191
|
try {
|
|
@@ -173,6 +262,8 @@ export class Game {
|
|
|
173
262
|
click: (x, y) => {
|
|
174
263
|
this.pointer.injectClick(x, y);
|
|
175
264
|
},
|
|
265
|
+
loadScene: (name) => this.loadSceneByName(name),
|
|
266
|
+
availableScenes: () => this.availableScenes,
|
|
176
267
|
});
|
|
177
268
|
activation.register(this.runtimeBridge);
|
|
178
269
|
window.addEventListener('pagehide', this.unregisterRuntimeBridge);
|
|
@@ -230,23 +321,40 @@ export class Game {
|
|
|
230
321
|
this.runFrame(dt);
|
|
231
322
|
}
|
|
232
323
|
runFrame(dt) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
324
|
+
this.insideFrame = true;
|
|
325
|
+
try {
|
|
326
|
+
// Flushes a scene swap enqueued mid-frame last time (CA-7): applied
|
|
327
|
+
// before this frame's own simulation, so the incoming scene's
|
|
328
|
+
// entities are present only from this next frame onward.
|
|
329
|
+
this.flushPendingSceneLoad();
|
|
330
|
+
if (this.simulate) {
|
|
331
|
+
for (const entity of [...this.entities]) {
|
|
332
|
+
const schedule = this.componentUpdateSchedule(entity);
|
|
333
|
+
if (!schedule)
|
|
334
|
+
continue;
|
|
335
|
+
for (const component of schedule)
|
|
336
|
+
component.onUpdate?.(dt);
|
|
337
|
+
}
|
|
338
|
+
this.dispatchCollisions();
|
|
339
|
+
this.updateSceneCamera(dt);
|
|
240
340
|
}
|
|
241
|
-
|
|
242
|
-
this.
|
|
341
|
+
// The UI must react to the pause itself (hide until resumed).
|
|
342
|
+
this.ui.setActive(this.simulate);
|
|
343
|
+
for (const fn of this.updateFns)
|
|
344
|
+
fn(dt);
|
|
345
|
+
this.input.endFrame();
|
|
346
|
+
this.renderSurface();
|
|
243
347
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
348
|
+
finally {
|
|
349
|
+
this.insideFrame = false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
flushPendingSceneLoad() {
|
|
353
|
+
const pending = this.pendingSceneLoad;
|
|
354
|
+
if (!pending)
|
|
355
|
+
return;
|
|
356
|
+
this.pendingSceneLoad = null;
|
|
357
|
+
pending();
|
|
250
358
|
}
|
|
251
359
|
unregisterRuntimeBridge = () => {
|
|
252
360
|
window.removeEventListener('pagehide', this.unregisterRuntimeBridge);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
|
-
export type { GameOptions, GameResolution, SpawnPrefabOptions, UpdateFn, ParamOverrides, } from './game.js';
|
|
2
|
+
export type { GameOptions, GameResolution, SceneCatalog, SpawnPrefabOptions, UpdateFn, ParamOverrides, } from './game.js';
|
|
3
3
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
4
4
|
export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
|
|
5
5
|
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
11
11
|
* doesn't distinguish an engine that silently no-ops an unknown operation
|
|
12
12
|
* from one that runs it.
|
|
13
13
|
*/
|
|
14
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click'];
|
|
14
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene'];
|
|
15
15
|
export interface RuntimeMetadata {
|
|
16
16
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
17
17
|
engineVersion: string;
|
|
@@ -33,6 +33,9 @@ export type RuntimeControlRequest = {
|
|
|
33
33
|
operation: 'click';
|
|
34
34
|
x: number;
|
|
35
35
|
y: number;
|
|
36
|
+
} | {
|
|
37
|
+
operation: 'scene';
|
|
38
|
+
scene: string;
|
|
36
39
|
};
|
|
37
40
|
export interface RuntimeControlResult extends RuntimeMetadata {
|
|
38
41
|
heldActions: string[];
|
|
@@ -40,8 +43,9 @@ export interface RuntimeControlResult extends RuntimeMetadata {
|
|
|
40
43
|
export declare class RuntimeBridgeOperationError extends Error {
|
|
41
44
|
readonly code: 'runtime-invalid-state' | 'runtime-operation-failed';
|
|
42
45
|
readonly availableActions?: string[] | undefined;
|
|
46
|
+
readonly availableScenes?: string[] | undefined;
|
|
43
47
|
readonly stage: 'control';
|
|
44
|
-
constructor(code: 'runtime-invalid-state' | 'runtime-operation-failed', message: string, availableActions?: string[] | undefined);
|
|
48
|
+
constructor(code: 'runtime-invalid-state' | 'runtime-operation-failed', message: string, availableActions?: string[] | undefined, availableScenes?: string[] | undefined);
|
|
45
49
|
}
|
|
46
50
|
/** Engine-owned capability registered only in an MCP-activated page. */
|
|
47
51
|
export interface RuntimeBridge {
|
|
@@ -66,6 +70,9 @@ export interface RuntimeBridgeHost {
|
|
|
66
70
|
heldActions(): string[];
|
|
67
71
|
inspect(metadata: RuntimeMetadata, filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
68
72
|
click(x: number, y: number): void;
|
|
73
|
+
/** Resolves `name` through the registered catalog and loads it. */
|
|
74
|
+
loadScene(name: string): boolean;
|
|
75
|
+
availableScenes(): string[];
|
|
69
76
|
}
|
|
70
77
|
export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
71
78
|
readonly surface: HTMLCanvasElement;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -10,15 +10,17 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
10
10
|
* doesn't distinguish an engine that silently no-ops an unknown operation
|
|
11
11
|
* from one that runs it.
|
|
12
12
|
*/
|
|
13
|
-
export const RUNTIME_BRIDGE_CAPABILITIES = ['click'];
|
|
13
|
+
export const RUNTIME_BRIDGE_CAPABILITIES = ['click', 'scene'];
|
|
14
14
|
export class RuntimeBridgeOperationError extends Error {
|
|
15
15
|
code;
|
|
16
16
|
availableActions;
|
|
17
|
+
availableScenes;
|
|
17
18
|
stage = 'control';
|
|
18
|
-
constructor(code, message, availableActions) {
|
|
19
|
+
constructor(code, message, availableActions, availableScenes) {
|
|
19
20
|
super(message);
|
|
20
21
|
this.code = code;
|
|
21
22
|
this.availableActions = availableActions;
|
|
23
|
+
this.availableScenes = availableScenes;
|
|
22
24
|
this.name = 'RuntimeBridgeOperationError';
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -106,6 +108,13 @@ export class EngineRuntimeBridge {
|
|
|
106
108
|
this.host.click(request.x, request.y);
|
|
107
109
|
break;
|
|
108
110
|
}
|
|
111
|
+
case 'scene': {
|
|
112
|
+
if (!this.host.loadScene(request.scene)) {
|
|
113
|
+
const available = this.host.availableScenes();
|
|
114
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown scene "${request.scene}". Available scenes: ${available.join(', ') || '(none)'}.`, undefined, available);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
109
118
|
default: {
|
|
110
119
|
const unsupported = request;
|
|
111
120
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unsupported runtime control operation "${unsupported.operation}".`);
|
|
@@ -50,6 +50,8 @@ export interface RuntimeEntitySnapshot {
|
|
|
50
50
|
}
|
|
51
51
|
export interface RuntimeSnapshot extends RuntimeMetadata {
|
|
52
52
|
stats: Record<string, StatValue>;
|
|
53
|
+
/** The live scene's name (its catalog key), or null with no scene loaded. */
|
|
54
|
+
scene: string | null;
|
|
53
55
|
entities: RuntimeEntitySnapshot[];
|
|
54
56
|
projectionIssues: ProjectionIssue[];
|
|
55
57
|
}
|
|
@@ -234,6 +234,7 @@ export class RuntimeInspector {
|
|
|
234
234
|
return this.capSnapshot({
|
|
235
235
|
...metadata,
|
|
236
236
|
stats: Object.fromEntries([...this.game.stats.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
|
|
237
|
+
scene: this.game.sceneName,
|
|
237
238
|
entities,
|
|
238
239
|
projectionIssues,
|
|
239
240
|
});
|
package/dist/scene.d.ts
CHANGED
|
@@ -88,5 +88,9 @@ export declare function resolveProps(props: Record<string, unknown> | undefined,
|
|
|
88
88
|
export declare function resolveEntityComponents(entity: SceneEntityJson, prefabs?: Record<string, PrefabJson>): SceneComponentJson[];
|
|
89
89
|
/** Instantiates a scene entity into the game. */
|
|
90
90
|
export declare function spawnFromJson(game: Game, json: SceneEntityJson, registry: SceneRegistry): Entity;
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Loads a full scene into the game, replacing whatever scene is already
|
|
93
|
+
* live (Game.unloadScene() first) — see ADR 0011. A Game shows one scene
|
|
94
|
+
* at a time.
|
|
95
|
+
*/
|
|
92
96
|
export declare function loadScene(game: Game, scene: SceneJson, registry: SceneRegistry): void;
|
package/dist/scene.js
CHANGED
|
@@ -79,8 +79,14 @@ export function spawnFromJson(game, json, registry) {
|
|
|
79
79
|
}
|
|
80
80
|
return entity;
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Loads a full scene into the game, replacing whatever scene is already
|
|
84
|
+
* live (Game.unloadScene() first) — see ADR 0011. A Game shows one scene
|
|
85
|
+
* at a time.
|
|
86
|
+
*/
|
|
83
87
|
export function loadScene(game, scene, registry) {
|
|
88
|
+
if (game.registry)
|
|
89
|
+
game.unloadScene();
|
|
84
90
|
game.registry = registry;
|
|
85
91
|
game.setSceneRender(scene.render);
|
|
86
92
|
for (const entityJson of scene.entities)
|
|
@@ -89,6 +95,7 @@ export function loadScene(game, scene, registry) {
|
|
|
89
95
|
game.setSceneCamera(scene.camera);
|
|
90
96
|
if (registry.ui)
|
|
91
97
|
game.ui.defineAll(registry.ui);
|
|
98
|
+
// Scene-scoped: Game.unloadScene() unmounts these along with the entities.
|
|
92
99
|
for (const name of scene.ui ?? [])
|
|
93
|
-
game.ui.show(name);
|
|
100
|
+
game.ui.show(name, { scope: 'scene' });
|
|
94
101
|
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare class GameUi {
|
|
|
27
27
|
defineAll(pieces: Record<string, string>): void;
|
|
28
28
|
/** Piece names available to show (defined via the registry or define()). */
|
|
29
29
|
names(): string[];
|
|
30
|
-
show(name: string): void;
|
|
30
|
+
show(name: string, options?: ShowOptions): void;
|
|
31
31
|
hide(name: string): void;
|
|
32
32
|
toggle(name: string): void;
|
|
33
33
|
isVisible(name: string): boolean;
|
|
@@ -41,7 +41,18 @@ export declare class GameUi {
|
|
|
41
41
|
setActive(active: boolean): void;
|
|
42
42
|
/** Unmounts every piece and removes the overlay (Game.dispose). */
|
|
43
43
|
dispose(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Unmounts every scene-scoped piece: the ones `loadScene` showed from the
|
|
46
|
+
* outgoing scene's `ui` list, plus any shown with `{ scope: 'scene' }`.
|
|
47
|
+
* A piece the host showed with no scope is untouched. The definition
|
|
48
|
+
* catalog (sources) always survives — Game.unloadScene.
|
|
49
|
+
*/
|
|
50
|
+
unloadScene(): void;
|
|
44
51
|
private mount;
|
|
45
52
|
private mountOverlay;
|
|
46
53
|
private sync;
|
|
47
54
|
}
|
|
55
|
+
export interface ShowOptions {
|
|
56
|
+
/** 'scene': unmounted by Game.unloadScene() along with the rest of the scene. */
|
|
57
|
+
scope?: 'scene';
|
|
58
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -35,10 +35,19 @@ export class GameUi {
|
|
|
35
35
|
names() {
|
|
36
36
|
return [...this.sources.keys()];
|
|
37
37
|
}
|
|
38
|
-
show(name) {
|
|
38
|
+
show(name, options = {}) {
|
|
39
|
+
const mounted = this.pieces.has(name);
|
|
39
40
|
const piece = this.mount(name);
|
|
40
|
-
if (piece)
|
|
41
|
+
if (piece) {
|
|
41
42
|
piece.visible = true;
|
|
43
|
+
// Scope belongs to whoever mounts the piece, and a later show never
|
|
44
|
+
// changes it. Otherwise a scene whose `ui` list happens to name a piece
|
|
45
|
+
// the host already mounted would quietly take ownership of it and
|
|
46
|
+
// destroy it on the next unload — a session-scoped HUD dying with a
|
|
47
|
+
// map it merely shares a name with.
|
|
48
|
+
if (!mounted)
|
|
49
|
+
piece.scope = options.scope;
|
|
50
|
+
}
|
|
42
51
|
this.sync();
|
|
43
52
|
}
|
|
44
53
|
hide(name) {
|
|
@@ -81,6 +90,23 @@ export class GameUi {
|
|
|
81
90
|
this.overlay?.remove();
|
|
82
91
|
this.overlay = undefined;
|
|
83
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Unmounts every scene-scoped piece: the ones `loadScene` showed from the
|
|
95
|
+
* outgoing scene's `ui` list, plus any shown with `{ scope: 'scene' }`.
|
|
96
|
+
* A piece the host showed with no scope is untouched. The definition
|
|
97
|
+
* catalog (sources) always survives — Game.unloadScene.
|
|
98
|
+
*/
|
|
99
|
+
unloadScene() {
|
|
100
|
+
for (const [name, piece] of this.pieces) {
|
|
101
|
+
if (piece.scope !== 'scene')
|
|
102
|
+
continue;
|
|
103
|
+
for (const off of piece.unsubs)
|
|
104
|
+
off();
|
|
105
|
+
piece.shell.remove();
|
|
106
|
+
this.pieces.delete(name);
|
|
107
|
+
}
|
|
108
|
+
this.sync();
|
|
109
|
+
}
|
|
84
110
|
mount(name) {
|
|
85
111
|
const existing = this.pieces.get(name);
|
|
86
112
|
if (existing)
|
|
@@ -100,7 +126,13 @@ export class GameUi {
|
|
|
100
126
|
root.style.display = 'contents';
|
|
101
127
|
root.innerHTML = html;
|
|
102
128
|
shadow.append(root);
|
|
103
|
-
const piece = {
|
|
129
|
+
const piece = {
|
|
130
|
+
shell,
|
|
131
|
+
root,
|
|
132
|
+
visible: false,
|
|
133
|
+
scope: undefined,
|
|
134
|
+
unsubs: bindStats(root, this.stats),
|
|
135
|
+
};
|
|
104
136
|
this.mountOverlay().append(shell);
|
|
105
137
|
this.pieces.set(name, piece);
|
|
106
138
|
return piece;
|