@waica/engine 0.11.0 → 0.13.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/index.d.ts CHANGED
@@ -1,5 +1,9 @@
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
+ export { AudioSubsystem } from './audio/audio-subsystem.js';
4
+ export type { AudioSubsystemOptions } from './audio/audio-subsystem.js';
5
+ export type { AudioBackend, AudioResource, BackendPlayHandle, BackendPlayOptions } from './audio/backend.js';
6
+ export type { AudioChannelState, AudioPlayOptions, LiveSoundInfo, SoundHandle } from './audio/types.js';
3
7
  export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
4
8
  export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
5
9
  export { isYSortParticipant, ySortZ } from './render-sort.js';
@@ -25,7 +29,7 @@ export type { PointerCamera, PointerDeps, PointerPick, PointerResolution } from
25
29
  export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
26
30
  export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
27
31
  export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
28
- export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotFilters, RuntimeTransformSnapshot, } from './runtime-inspection.js';
32
+ export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotFilters, RuntimeTransformSnapshot, } from './runtime-inspection.js';
29
33
  export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
30
34
  export { Stats } from './stats.js';
31
35
  export type { StatValue } from './stats.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Game } from './game.js';
2
+ export { AudioSubsystem } from './audio/audio-subsystem.js';
2
3
  export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
3
4
  export { isYSortParticipant, ySortZ } from './render-sort.js';
4
5
  export { projectIsometric, screenInputToLogical, unprojectIsometric } from './projection.js';
@@ -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;
@@ -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}".`);
@@ -1,3 +1,4 @@
1
+ import type { AudioChannelState, LiveSoundInfo } from './audio/types.js';
1
2
  import type { Game } from './game.js';
2
3
  import type { RuntimeMetadata } from './runtime-bridge.js';
3
4
  import type { StatValue } from './stats.js';
@@ -48,10 +49,29 @@ export interface RuntimeEntitySnapshot {
48
49
  transform: RuntimeTransformSnapshot;
49
50
  components: RuntimeComponentSnapshot[];
50
51
  }
52
+ /**
53
+ * The mixer's state (CA-15): `master` and every channel's volume/mute,
54
+ * sorted by name, plus `playing` — `game.audio.liveSounds()` verbatim,
55
+ * already sorted (by uri then channel). Despite the name, `playing` is not
56
+ * "every currently-audible sound": per `liveSounds()`'s own docstring it
57
+ * also carries a loop retained before the autoplay unlock, a sound still
58
+ * loading, and even one about to fail to load (gone a tick later). Emitted
59
+ * unconditionally, like every other snapshot section —
60
+ * `[DEVIATION 2026-09-08]` in the spec: no section of RuntimeSnapshot is
61
+ * filterable today, so audio does not invent the first one.
62
+ */
63
+ export interface RuntimeSnapshotAudio {
64
+ master: number;
65
+ channels: Record<string, AudioChannelState>;
66
+ playing: LiveSoundInfo[];
67
+ }
51
68
  export interface RuntimeSnapshot extends RuntimeMetadata {
52
69
  stats: Record<string, StatValue>;
70
+ /** The live scene's name (its catalog key), or null with no scene loaded. */
71
+ scene: string | null;
53
72
  entities: RuntimeEntitySnapshot[];
54
73
  projectionIssues: ProjectionIssue[];
74
+ audio: RuntimeSnapshotAudio;
55
75
  }
56
76
  export declare const RUNTIME_PROJECTION_LIMITS: {
57
77
  readonly depth: 5;
@@ -66,6 +86,7 @@ export declare class RuntimeInspector {
66
86
  private nextId;
67
87
  constructor(game: Game);
68
88
  snapshot(metadata: RuntimeMetadata, filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
89
+ private audioSnapshot;
69
90
  private capSnapshot;
70
91
  private idFor;
71
92
  }
@@ -234,10 +234,23 @@ 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,
240
+ audio: this.audioSnapshot(),
239
241
  });
240
242
  }
243
+ audioSnapshot() {
244
+ const channels = {};
245
+ for (const name of this.game.audio.channels().sort()) {
246
+ channels[name] = this.game.audio.channelState(name);
247
+ }
248
+ return {
249
+ master: this.game.audio.master,
250
+ channels,
251
+ playing: this.game.audio.liveSounds(),
252
+ };
253
+ }
241
254
  capSnapshot(snapshot) {
242
255
  if (utf8Bytes(JSON.stringify(snapshot)) <= RUNTIME_PROJECTION_LIMITS.snapshotBytes) {
243
256
  return snapshot;
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
- /** Loads a full scene into the game. */
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
- /** Loads a full scene into the game. */
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 = { shell, root, visible: false, unsubs: bindStats(root, this.stats) };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/engine",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Waica game engine core — archetype-driven, web-first, 2D & 3D",
5
5
  "license": "MIT",
6
6
  "type": "module",