@skewedaspect/sage 0.10.0-beta.1 → 0.10.0-rc.1

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/behaviors/characterController.d.ts +58 -0
  4. package/dist/behaviors/physicsBody.d.ts +38 -0
  5. package/dist/behaviors/sound.d.ts +22 -0
  6. package/dist/classes/debugConsole.d.ts +8 -16
  7. package/dist/classes/gameEngine.d.ts +58 -89
  8. package/dist/classes/gameLevel.d.ts +27 -22
  9. package/dist/classes/level.d.ts +53 -43
  10. package/dist/classes/loggers/consoleBackend.d.ts +1 -1
  11. package/dist/classes/loggers/nullBackend.d.ts +1 -1
  12. package/dist/debug/builtins.d.ts +4 -10
  13. package/dist/engines/audio.d.ts +4 -7
  14. package/dist/engines/scene.d.ts +14 -73
  15. package/dist/entities/defineEntity.d.ts +7 -5
  16. package/dist/entities/entityPicking.d.ts +2 -1
  17. package/dist/entities/meshConfig.d.ts +11 -3
  18. package/dist/entities/nodeEntity.d.ts +23 -4
  19. package/dist/entities/nodePool.d.ts +1 -1
  20. package/dist/entities/nodeSimulation.d.ts +28 -9
  21. package/dist/entities/nodeSimulationContext.d.ts +6 -2
  22. package/dist/entities/standardMeshSource.d.ts +2 -2
  23. package/dist/events/payloads.d.ts +18 -35
  24. package/dist/handlers/collider.d.ts +7 -12
  25. package/dist/handlers/index.d.ts +13 -14
  26. package/dist/handlers/lod.d.ts +11 -9
  27. package/dist/handlers/occlusion.d.ts +8 -0
  28. package/dist/handlers/postProcessing.d.ts +7 -2
  29. package/dist/handlers/postProcessingFrameGraph.d.ts +7 -2
  30. package/dist/handlers/postProcessingPipeline.d.ts +4 -3
  31. package/dist/handlers/sound.d.ts +2 -4
  32. package/dist/handlers/trigger.d.ts +2 -4
  33. package/dist/handlers/visible.d.ts +4 -5
  34. package/dist/input/actionContext.d.ts +5 -1
  35. package/dist/input/binding.d.ts +3 -1
  36. package/dist/input/bindings/delta.d.ts +23 -0
  37. package/dist/input/bindings/toggle.d.ts +6 -2
  38. package/dist/input/bindings/trigger.d.ts +6 -2
  39. package/dist/input/bindings/value.d.ts +8 -5
  40. package/dist/input/configuration.d.ts +3 -3
  41. package/dist/input/deliverAction.d.ts +4 -3
  42. package/dist/input/deviceReaders/gamepad.d.ts +2 -2
  43. package/dist/input/deviceReaders/keyboard.d.ts +2 -2
  44. package/dist/input/deviceReaders/mouse.d.ts +3 -3
  45. package/dist/input/deviceState.d.ts +5 -0
  46. package/dist/input/inputCapture.d.ts +1 -1
  47. package/dist/input/readerFor.d.ts +3 -0
  48. package/dist/input/resolveBinding.d.ts +3 -3
  49. package/dist/interfaces/game.d.ts +20 -32
  50. package/dist/interfaces/level.d.ts +48 -51
  51. package/dist/interfaces/lifecycle.d.ts +3 -41
  52. package/dist/interfaces/logger.d.ts +0 -10
  53. package/dist/managers/asset.d.ts +27 -20
  54. package/dist/managers/audio.d.ts +10 -21
  55. package/dist/managers/colliderDebug.d.ts +7 -4
  56. package/dist/managers/game.d.ts +27 -81
  57. package/dist/managers/input.d.ts +35 -0
  58. package/dist/managers/level.d.ts +71 -120
  59. package/dist/managers/outline.d.ts +2 -2
  60. package/dist/managers/simulationSave.d.ts +20 -12
  61. package/dist/postProcessingFrameGraph-Dx3e6D33.js +93 -0
  62. package/dist/postProcessingFrameGraph-Dx3e6D33.js.map +1 -0
  63. package/dist/sage.d.ts +90 -78
  64. package/dist/sage.es.js +2356 -1628
  65. package/dist/sage.es.js.map +1 -1
  66. package/dist/utils/bounds.d.ts +11 -0
  67. package/dist/utils/graphics.d.ts +3 -3
  68. package/dist/utils/logger.d.ts +3 -7
  69. package/dist/utils/physics.d.ts +9 -1
  70. package/dist/utils/timer.d.ts +1 -1
  71. package/dist/utils/vectors.d.ts +13 -10
  72. package/package.json +7 -8
  73. package/dist/classes/eventBus.d.ts +0 -141
  74. package/dist/events/index.d.ts +0 -2
  75. package/dist/events/types.d.ts +0 -17
  76. package/dist/handlers/occluder.d.ts +0 -13
  77. package/dist/postProcessingFrameGraph-rr2DbgPi.js +0 -70
  78. package/dist/postProcessingFrameGraph-rr2DbgPi.js.map +0 -1
  79. package/dist/sage.umd.js +0 -2
  80. package/dist/sage.umd.js.map +0 -1
  81. package/dist/utils/id.d.ts +0 -11
  82. package/dist/utils/stateMachine.d.ts +0 -43
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christopher Case
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @skewedaspect/sage
2
+
3
+ The Babylon.js game engine: `NodeEntity` and `NodeSimulation` on top of `@skewedaspect/sage-core`'s entity
4
+ system, levels and their scenes, physics through Havok, the input system, audio, pooling, the parent-child
5
+ hierarchy, debug tooling, and the save manager.
6
+
7
+ ```
8
+ npm install @skewedaspect/sage @babylonjs/core @babylonjs/havok @babylonjs/loaders
9
+ ```
10
+
11
+ Documentation: the site built from `site/docs` in https://gitlab.com/skewed-aspect/sage/sage
@@ -0,0 +1,58 @@
1
+ import { Vector3, CharacterSurfaceInfo } from '@babylonjs/core';
2
+ import { Behavior } from '@skewedaspect/sage-core';
3
+ import { NodeBehaviorHooks, NodeEntity, Vector3State } from "../entities/nodeEntity.js";
4
+ /** A capsule height and a speed under a name (design/character.md#stances). */
5
+ export interface StanceConfig {
6
+ height: number;
7
+ radius: number;
8
+ /** Metres per second. */
9
+ speed: number;
10
+ }
11
+ export interface CharacterControllerState {
12
+ /** How hard a game is asking to move right, -1 to 1. */
13
+ moveX: number;
14
+ /** How hard a game is asking to move forward, -1 to 1. */
15
+ moveZ: number;
16
+ wantsJump: boolean;
17
+ wantsSprint: boolean;
18
+ /** The stance a game is asking for, by name. */
19
+ wantsStance: string;
20
+ stances: Record<string, StanceConfig>;
21
+ /** Take the tallest stance's capsule from the mesh instead. */
22
+ measureCapsule: boolean;
23
+ /** What sprinting multiplies the current stance's speed by. */
24
+ sprintScale: number;
25
+ /** Upward metres per second a jump starts with. */
26
+ jumpSpeed: number;
27
+ /** How long a jump request survives, in fixed steps. */
28
+ jumpBufferSteps: number;
29
+ /** How long after leaving the ground a jump is still allowed, in fixed steps. */
30
+ coyoteSteps: number;
31
+ /** Metres per second. */
32
+ velocity: Vector3State;
33
+ /** The character's foot, not the middle of its capsule (design/character.md#state). */
34
+ position: Vector3State;
35
+ /** True only when the last step's support query answered `SUPPORTED`. */
36
+ grounded: boolean;
37
+ /** The stance the character is actually in, which names an entry in `stances`. */
38
+ stance: string;
39
+ }
40
+ export declare class CharacterControllerBehavior extends Behavior<CharacterControllerState, object, NodeEntity> implements NodeBehaviorHooks {
41
+ #private;
42
+ readonly defaults: CharacterControllerState;
43
+ onCreate(): void;
44
+ onMeshLoaded(): void;
45
+ onUpdate(duration: number): void;
46
+ onDestroy(): void;
47
+ /**
48
+ * The velocity the character wants this step, in world space, before the surface it is standing on is added
49
+ * (design/character.md#what-a-game-overrides). The jump is not this method's: an override answers for
50
+ * intent, and the behavior takes the jump around it.
51
+ */
52
+ protected desiredVelocity(duration: number, surface: CharacterSurfaceInfo): Vector3;
53
+ /**
54
+ * Whether that stance's capsule fits where the character is standing
55
+ * (design/character.md#what-a-game-overrides).
56
+ */
57
+ protected stanceFits(stance: string): boolean;
58
+ }
@@ -0,0 +1,38 @@
1
+ import { Behavior } from '@skewedaspect/sage-core';
2
+ import { NodeBehaviorHooks, NodeEntity, QuaternionState, Vector3State } from "../entities/nodeEntity.js";
3
+ /** What a body's outline can be (design/physics.md#shapes). */
4
+ export type BodyShape = 'box' | 'sphere' | 'cylinder' | 'convex' | 'mesh' | 'compound';
5
+ /** Who decides where a body goes (design/physics.md#static-dynamic-and-kinematic). */
6
+ export type BodyMotion = 'static' | 'dynamic' | 'kinematic';
7
+ /** One child of a compound shape, placed relative to the entity's node. */
8
+ export interface CompoundPart {
9
+ shape: 'box' | 'sphere' | 'cylinder';
10
+ position: Vector3State;
11
+ rotation: QuaternionState;
12
+ /** Full size: a box's width, height, depth; a sphere's diameter in x; a cylinder's diameter in x, height in y. */
13
+ extents: Vector3State;
14
+ }
15
+ export interface PhysicsBodyState {
16
+ shape: BodyShape;
17
+ /** The children of a `compound` shape. Unused by every other shape. */
18
+ parts: CompoundPart[];
19
+ motion: BodyMotion;
20
+ /** In kilograms. */
21
+ mass: number;
22
+ friction: number;
23
+ restitution: number;
24
+ position: Vector3State;
25
+ rotation: QuaternionState;
26
+ /** Metres per second. */
27
+ velocity: Vector3State;
28
+ /** Radians per second. */
29
+ angularVelocity: Vector3State;
30
+ }
31
+ export declare class HavokPhysicsBodyBehavior extends Behavior<PhysicsBodyState, object, NodeEntity> implements NodeBehaviorHooks {
32
+ #private;
33
+ static readonly ops: readonly ["applyImpulse"];
34
+ readonly defaults: PhysicsBodyState;
35
+ onMeshLoaded(): void;
36
+ onDestroy(): void;
37
+ applyImpulse(impulse: Vector3State, location?: Vector3State): void;
38
+ }
@@ -0,0 +1,22 @@
1
+ import { Behavior } from '@skewedaspect/sage-core';
2
+ import { NodeBehaviorHooks, NodeEntity } from "../entities/nodeEntity.js";
3
+ export interface SoundConfig {
4
+ url: string;
5
+ volume?: number;
6
+ loop?: boolean;
7
+ spatial?: boolean;
8
+ maxDistance?: number;
9
+ channel?: string;
10
+ }
11
+ export interface SoundState {
12
+ sounds: Record<string, SoundConfig>;
13
+ }
14
+ export declare class SoundBehavior extends Behavior<SoundState, object, NodeEntity> implements NodeBehaviorHooks {
15
+ #private;
16
+ static readonly ops: readonly ["play", "stop"];
17
+ readonly defaults: SoundState;
18
+ onMeshLoaded(): void;
19
+ onDestroy(): void;
20
+ play(name: string): void;
21
+ stop(name: string): void;
22
+ }
@@ -1,23 +1,15 @@
1
- import { Disposable } from "../interfaces/lifecycle.d.ts";
2
- interface DynamicGetter {
1
+ /** What `expose` takes to define a property that resolves fresh on every read. */
2
+ export interface DynamicGetter {
3
3
  get: () => unknown;
4
4
  }
5
- export declare class DebugConsole implements Disposable {
6
- private _namespaceName;
7
- private _entries;
8
- private _disposed;
5
+ export declare class DebugConsole {
6
+ #private;
9
7
  constructor(namespaceName?: string);
10
- expose(name: string, value: unknown | DynamicGetter): void;
8
+ /** Defines a property that resolves fresh on every read (design/debugging.md#debug-tooling). */
9
+ expose(name: string, getter: DynamicGetter): void;
10
+ /** Puts a value, a callable command included, on the namespace (design/debugging.md#debug-tooling). */
11
+ expose(name: string, value: unknown): void;
11
12
  setNamespace(name: string): void;
12
13
  get namespaceName(): string;
13
14
  dispose(): void;
14
- $teardown(): Promise<void>;
15
- private _isDynamicGetter;
16
- private _getNamespace;
17
- private _assertNamespaceFree;
18
- private _installNamespace;
19
- private _removeNamespace;
20
- private _defineGetter;
21
- private _defineValue;
22
15
  }
23
- export {};
@@ -1,32 +1,32 @@
1
- import { AbstractEngine, HavokPlugin, Scene } from '@babylonjs/core';
2
- import { EntityMeshLoader, GameCanvas } from "../interfaces/game.d.ts";
3
- import { Disposable } from "../interfaces/lifecycle.d.ts";
4
- import { NodeSimulation } from "../entities/nodeSimulation.d.ts";
5
- import { SceneEngine } from "../engines/scene.d.ts";
6
- import { AudioEngine } from "../engines/audio.d.ts";
7
- import { AssetManager } from "../managers/asset.d.ts";
8
- import { ColliderDebugManager } from "../managers/colliderDebug.d.ts";
9
- import { DebugConsole } from "./debugConsole.d.ts";
10
- import { GameManager } from "../managers/game.d.ts";
11
- import { LevelManager } from "../managers/level.d.ts";
12
- import { SimulationSaveManager } from "../managers/simulationSave.d.ts";
13
- import { AudioManager } from "../managers/audio.d.ts";
14
- import { GameEventBus, GameEventCallback, Unsubscribe } from "./eventBus.d.ts";
15
- import { LibraryEventPayloadMap } from "../events/payloads.d.ts";
16
- import { WildcardPattern } from "../events/types.d.ts";
17
- import { LoggingUtility } from "../utils/logger.d.ts";
18
- import { GameTimer } from "../utils/timer.d.ts";
1
+ import { AbstractEngine, Scene } from '@babylonjs/core';
2
+ import { EventBus, Unsubscribe } from '@skewedaspect/sage-core';
3
+ import { GameCanvas } from "../interfaces/game.js";
4
+ import { Disposable } from "../interfaces/lifecycle.js";
5
+ import { NodeEntityRegistry, NodeSimulation } from "../entities/nodeSimulation.js";
6
+ import { MeshLoader } from "../entities/nodeSimulationContext.js";
7
+ import { SceneEngine } from "../engines/scene.js";
8
+ import { AudioEngine } from "../engines/audio.js";
9
+ import { AssetManager } from "../managers/asset.js";
10
+ import { ColliderDebugManager } from "../managers/colliderDebug.js";
11
+ import { DebugConsole } from "./debugConsole.js";
12
+ import { GameManager } from "../managers/game.js";
13
+ import { InputManager } from "../managers/input.js";
14
+ import { LevelManager } from "../managers/level.js";
15
+ import { SimulationSaveManager } from "../managers/simulationSave.js";
16
+ import { AudioManager } from "../managers/audio.js";
17
+ import { LoggingUtility } from "../utils/logger.js";
18
+ import { GameTimer } from "../utils/timer.js";
19
19
  /**
20
20
  * Type definition for game lifecycle hook functions
21
21
  */
22
- export type GameHook = (gameEngine: GameEngine) => Promise<void>;
23
- /** Builds a NodeSimulation bound to a scene and a mesh source -- a fresh one is built for every level that loads. */
24
- export type SimulationFactory = (scene: Scene, meshSource?: EntityMeshLoader) => NodeSimulation;
22
+ export type GameHook<Registry extends NodeEntityRegistry = NodeEntityRegistry> = (gameEngine: GameEngine<Registry>) => Promise<void>;
23
+ /** Builds a NodeSimulation named and bound to a scene and a mesh source -- a fresh one for every level that loads. */
24
+ export type SimulationFactory<Registry extends NodeEntityRegistry = NodeEntityRegistry> = (scene: Scene, name: string, meshSource?: MeshLoader) => NodeSimulation<Registry>;
25
25
  /**
26
26
  * Interface representing the engines used in the game.
27
27
  * All engines must implement Disposable for proper cleanup.
28
28
  */
29
- interface Engines extends Record<string, Disposable | undefined> {
29
+ export interface Engines extends Record<string, Disposable | undefined> {
30
30
  sceneEngine: SceneEngine;
31
31
  audioEngine?: AudioEngine;
32
32
  }
@@ -34,108 +34,77 @@ interface Engines extends Record<string, Disposable | undefined> {
34
34
  * Interface representing the managers used in the game.
35
35
  * All managers must implement Disposable for proper cleanup.
36
36
  */
37
- interface Managers extends Record<string, Disposable | undefined> {
37
+ export interface Managers<Registry extends NodeEntityRegistry = NodeEntityRegistry> extends Record<string, Disposable | undefined> {
38
38
  assetManager: AssetManager;
39
- gameManager: GameManager;
40
- levelManager: LevelManager;
41
- saveManager: SimulationSaveManager;
39
+ gameManager: GameManager<Registry>;
40
+ inputManager: InputManager;
41
+ levelManager: LevelManager<Registry>;
42
+ saveManager: SimulationSaveManager<Registry>;
42
43
  audioManager?: AudioManager;
43
44
  }
44
45
  /**
45
46
  * Interface representing the debug tools available on the engine.
46
47
  */
47
- interface DebugTools {
48
+ export interface DebugTools {
48
49
  colliders: ColliderDebugManager;
49
50
  console: DebugConsole | null;
50
51
  expose(name: string, value: unknown): void;
51
52
  }
52
53
  /**
53
- * Central hub that owns the render loop, physics, event bus, and all managers.
54
+ * Central hub that owns the render loop, the event bus, and all managers.
54
55
  * Created via `createGameEngine()` in sage.ts.
55
56
  */
56
- export declare class GameEngine {
57
+ export declare class GameEngine<Registry extends NodeEntityRegistry = NodeEntityRegistry> {
58
+ #private;
57
59
  canvas: GameCanvas;
58
60
  renderEngine: AbstractEngine;
59
- physics: HavokPlugin;
60
- managers: Managers;
61
+ managers: Managers<Registry>;
61
62
  engines: Engines;
62
- eventBus: GameEventBus;
63
+ bus: EventBus;
63
64
  logger: LoggingUtility;
64
- simulation: NodeSimulation | null;
65
65
  timer: GameTimer;
66
66
  debug: DebugTools;
67
67
  largeWorldRendering: boolean;
68
68
  started: boolean;
69
+ private _stopped;
69
70
  private _log;
70
71
  private _buildSimulation;
71
- private _beforeStartHook;
72
- private _onStartHook;
73
- private _onTeardownHook;
74
72
  /**
75
73
  * Creates an instance of GameEngine.
76
- * @param canvas
77
- * @param renderEngine
78
- * @param physics
79
- * @param eventBus
80
- * @param logger
81
- * @param timer
82
74
  * @param buildSimulation - Builds a fresh simulation for a scene, once rebuildSimulation is called with one.
83
- * @param engines
84
- * @param managers
85
75
  */
86
- constructor(canvas: GameCanvas, renderEngine: AbstractEngine, physics: HavokPlugin, eventBus: GameEventBus, logger: LoggingUtility, timer: GameTimer, buildSimulation: SimulationFactory, engines: Engines, managers: Managers, largeWorldRendering?: boolean, debug?: DebugTools);
76
+ constructor(canvas: GameCanvas, renderEngine: AbstractEngine, bus: EventBus, logger: LoggingUtility, timer: GameTimer, buildSimulation: SimulationFactory<Registry>, engines: Engines, managers: Managers<Registry>, largeWorldRendering: boolean | undefined, debug: DebugTools);
77
+ /** The simulation the engine holds: attached to the bus, stepped by the frame loop, and what a view reads. */
78
+ get simulation(): NodeSimulation<Registry> | null;
87
79
  /**
88
- * Builds a fresh NodeSimulation bound to the given scene and mesh source, and swaps it in. A level calls
89
- * this once its own scene is ready, so every NodeEntity it spawns gets a node in that scene and loads its
90
- * mesh through that source. Left unset, a spawned entity gets a node with no mesh.
80
+ * Builds a fresh NodeSimulation named `name` for the given scene and mesh source, handed this engine's bus and
81
+ * not yet bound. A level calls this once its own scene exists, naming the simulation after itself, so every
82
+ * NodeEntity it spawns gets a node in that scene and loads its mesh through that source; left unset, a spawned
83
+ * entity gets a node with no mesh (design/simulation.md#the-simulations-name).
91
84
  */
92
- rebuildSimulation(scene: Scene, meshSource?: EntityMeshLoader): NodeSimulation;
85
+ rebuildSimulation(scene: Scene, name: string, meshSource?: MeshLoader): NodeSimulation<Registry>;
93
86
  /**
94
- * Register a function to be called before the game engine starts
95
- * @param hook
96
- * @throws Error if a hook is already registered
87
+ * Binds the engine to a simulation, or to none. The one bound before detaches from the bus and the new one
88
+ * attaches, carrying its entities' subscriptions and held emissions across
89
+ * (design/events.md#one-bus-per-engine).
97
90
  */
98
- onBeforeStart(hook: GameHook): void;
91
+ bindSimulation(simulation: NodeSimulation<Registry> | null): void;
92
+ /** Runs before the frame loop starts, awaited, in registration order (design/engine.md#lifecycle-hooks). */
93
+ onBeforeStart(hook: GameHook<Registry>): Unsubscribe;
94
+ /** Runs after the frame loop has started, awaited, in registration order. */
95
+ onStart(hook: GameHook<Registry>): Unsubscribe;
96
+ /** Runs before anything is disposed, awaited; a hook that throws is logged and does not stop the teardown. */
97
+ onTeardown(hook: GameHook<Registry>): Unsubscribe;
99
98
  /**
100
- * Register a function to be called after the game engine starts
101
- * @param hook
102
- * @throws Error if a hook is already registered
103
- */
104
- onStart(hook: GameHook): void;
105
- /**
106
- * Register a function to be called when the game engine stops
107
- * @param hook
108
- * @throws Error if a hook is already registered
109
- */
110
- onTeardown(hook: GameHook): void;
111
- /**
112
- * Subscribe to an event on the event bus.
113
- *
114
- * This is a convenience method that forwards to `eventBus.subscribe()`.
115
- *
116
- * @param eventType - Exact event type string, wildcard pattern (e.g. 'level:*'), or RegExp
117
- * @param callback
118
- * @returns A function that removes this subscription when called
119
- *
120
- * @example
121
- * // Subscribe to a specific event
122
- * const unsub = engine.subscribe('level:complete', (event) => {
123
- * console.log('Level complete:', event.payload.levelName);
124
- * });
125
- *
126
- * // Subscribe to every level event
127
- * engine.subscribe('level:*', (event) => {
128
- * console.log('Level event:', event.type);
129
- * });
130
- */
131
- subscribe<T extends keyof LibraryEventPayloadMap & string>(eventType: T | WildcardPattern | RegExp, callback: GameEventCallback<T, LibraryEventPayloadMap>): Unsubscribe;
132
- /**
133
- * Start the engine: runs beforeStart hook, starts the game manager, then runs onStart hook.
99
+ * Starts the frame loop, with the before-start hooks ahead of it and the start hooks after. An engine that has
100
+ * stopped does not start again (design/architecture.md#teardown).
134
101
  */
135
102
  start(): Promise<void>;
136
103
  /**
137
- * Stop the engine: runs the teardown hook, then tears down all managers and engines in order.
104
+ * Tears the engine down in one order: the frame loop stops, the teardown hooks run, then the debug console, the
105
+ * managers, and the engines dispose. A failure in one disposal does not stop the rest, and the first one raised
106
+ * is raised again once everything else is disposed. Stopping twice changes nothing
107
+ * (design/architecture.md#teardown).
138
108
  */
139
109
  stop(): Promise<void>;
140
110
  }
141
- export {};
@@ -1,7 +1,8 @@
1
1
  import { Quaternion, Scene, StaticSound, TransformNode, Vector3 } from '@babylonjs/core';
2
- import { LevelConfig, LevelContext } from "../interfaces/level.d.ts";
3
- import { NodeEntity } from "../entities/nodeEntity.d.ts";
4
- import { Level } from "./level.d.ts";
2
+ import { LevelConfig, LevelContext } from "../interfaces/level.js";
3
+ import { NodeEntity } from "../entities/nodeEntity.js";
4
+ import { NodeEntityRegistry, NodeSimulation } from "../entities/nodeSimulation.js";
5
+ import { Level } from "./level.js";
5
6
  /**
6
7
  * Metadata collected from a spawn point node.
7
8
  * Transforms are normalized to canonical Babylon space — the source level's __root__
@@ -22,10 +23,12 @@ interface EntityNodeData {
22
23
  node: TransformNode;
23
24
  }
24
25
  /**
25
- * Default Level implementation that loads from YAML configuration.
26
- * Supports property handlers for processing scene node metadata.
26
+ * The level a game gets when it describes a scene in configuration instead of writing a `Level` subclass: it
27
+ * builds the scene its configuration names, in one fixed order, and spawns what the scene's own markers name
28
+ * (design/levels.md#gamelevel).
27
29
  */
28
- export declare class GameLevel extends Level {
30
+ export declare class GameLevel<Registry extends NodeEntityRegistry = NodeEntityRegistry> extends Level<Registry> {
31
+ #private;
29
32
  /** The level configuration */
30
33
  protected _config: LevelConfig;
31
34
  /** Collected spawn points from the scene */
@@ -38,15 +41,14 @@ export declare class GameLevel extends Level {
38
41
  protected _levelSounds: Map<string, StaticSound>;
39
42
  /** Sounds that were playing before deactivation (for resume on activate) */
40
43
  private _playingSoundsBeforeDeactivate;
41
- /**
42
- * Create a GameLevel from a configuration object
43
- *
44
- * @param config
45
- * @param context
46
- */
47
- constructor(config: LevelConfig, context: LevelContext);
44
+ constructor(config: LevelConfig, context: LevelContext<Registry>);
48
45
  get config(): LevelConfig;
49
46
  get spawnedEntities(): readonly NodeEntity[];
47
+ /**
48
+ * The simulation this level built its scene against: the one its own spawns and despawns go through,
49
+ * whichever simulation the engine holds at the time. Throws before `buildScene` has bound one.
50
+ */
51
+ protected get simulation(): NodeSimulation<Registry>;
50
52
  /**
51
53
  * Build the scene by loading assets, processing node metadata, and spawning entities.
52
54
  */
@@ -72,13 +74,15 @@ export declare class GameLevel extends Level {
72
74
  */
73
75
  private _createEnvironmentTexture;
74
76
  private _getFileExtension;
77
+ private _attachableCanvas;
75
78
  /**
76
- * Process cameras: apply YAML overrides to imported cameras, create new ones, and set the active camera.
77
- * If no YAML cameras config exists, the first imported camera (if any) is activated.
79
+ * An entry whose key names an imported camera overrides it, and one whose key names none creates a camera
80
+ * of the type it declares. With no camera configuration at all, the first imported camera activates.
78
81
  */
79
82
  private _processCameras;
80
83
  /**
81
- * Create a new camera from a YAML definition.
84
+ * Create a camera of the type an entry declares, or nothing for an entry that cannot be built, which is
85
+ * logged and skipped (design/levels.md#the-load-sequence).
82
86
  */
83
87
  private _createCamera;
84
88
  /**
@@ -86,12 +90,12 @@ export declare class GameLevel extends Level {
86
90
  */
87
91
  private _applyCameraConfig;
88
92
  /**
89
- * Process lights: apply YAML overrides to imported lights and create new ones.
90
- * If no YAML lights config exists, imported lights are left unchanged.
93
+ * Lights override and create by key the way cameras do. With no light configuration at all, imported
94
+ * lights are left as they are.
91
95
  */
92
96
  private _processLights;
93
97
  /**
94
- * Create a new light from a YAML definition.
98
+ * Create a light of the type an entry declares.
95
99
  */
96
100
  private _createLight;
97
101
  /**
@@ -111,6 +115,7 @@ export declare class GameLevel extends Level {
111
115
  * Walk all transform nodes and meshes, normalizing glTF metadata and dispatching to property handlers.
112
116
  */
113
117
  private _processNodeProperties;
118
+ private _warnUnhandledProperties;
114
119
  /**
115
120
  * Check a node's metadata for spawn/entity markers and run any registered property handlers.
116
121
  */
@@ -144,8 +149,8 @@ export declare class GameLevel extends Level {
144
149
  */
145
150
  private _spawnEntityNode;
146
151
  /**
147
- * Create level-scoped sounds from YAML config. Sounds are tracked for lifecycle management
148
- * (pause on deactivate, resume on activate, dispose on unload).
152
+ * Create the sounds the configuration names, keeping each by name so it follows the level: paused on
153
+ * deactivation, played again on activation, disposed with the level (design/levels.md#level-sounds).
149
154
  */
150
155
  private _processLevelSounds;
151
156
  /**
@@ -161,6 +166,6 @@ export declare class GameLevel extends Level {
161
166
  /**
162
167
  * Dispose of this level's resources
163
168
  */
164
- $dispose(): Promise<void>;
169
+ dispose(): Promise<void>;
165
170
  }
166
171
  export {};
@@ -1,59 +1,73 @@
1
1
  import { Color3, Scene, TransformNode } from '@babylonjs/core';
2
2
  import { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer';
3
- import { LoggerInterface } from "../interfaces/logger.d.ts";
4
- import { ColorConfig, LevelConfig, LevelContext, LevelInstance, LevelLoadOptions, PropertyHandler } from "../interfaces/level.d.ts";
5
- import { GameEngine } from "./gameEngine.d.ts";
6
- import { OutlineManager } from "../managers/outline.d.ts";
3
+ import { NodeEntityRegistry, NodeSimulation } from "../entities/nodeSimulation.js";
4
+ import { MeshLoader } from "../entities/nodeSimulationContext.js";
5
+ import { LoggerInterface } from "../interfaces/logger.js";
6
+ import { ColorConfig, LevelConfig, LevelContext, LevelInstance, LevelLoadOptions, PropertyHandler } from "../interfaces/level.js";
7
+ import { OutlineManager } from "../managers/outline.js";
7
8
  /**
8
- * Abstract base class for game levels.
9
+ * The base a game subclasses when it builds a level's scene in code. A subclass writes one method,
10
+ * `buildScene`, and reaches every dependency it has through `this.context`; the base drives the load,
11
+ * publishes the level's own events, and owns the scene, the simulation built against it, and the teardown
12
+ * (design/levels.md#the-base-class).
9
13
  *
10
- * Levels are responsible for creating and configuring their own scenes via the
11
- * `buildScene()` method. All runtime dependencies (eventBus, sceneEngine, simulation)
12
- * are injected via the constructor and are immediately available.
13
- *
14
- * Users should not instantiate Level subclasses directly. Instead, use LevelManager
15
- * which acts as a factory and injects the required dependencies:
14
+ * The level manager is the factory: it constructs a subclass with the configuration and the context, so a
15
+ * game registers a class and loads a level rather than constructing one itself.
16
16
  *
17
17
  * ```typescript
18
- * // Register a custom level class
19
18
  * levelManager.registerLevelClass('my-level', MyCustomLevel);
20
19
  *
21
- * // Load the level - manager creates instance with deps injected
22
20
  * const level = await levelManager.loadLevel({ name: 'Level1', class: 'my-level' });
23
21
  * ```
24
22
  */
25
- export declare abstract class Level implements LevelInstance {
23
+ export declare abstract class Level<Registry extends NodeEntityRegistry = NodeEntityRegistry> implements LevelInstance {
24
+ #private;
26
25
  readonly name: string;
27
26
  protected readonly _log: LoggerInterface;
28
- protected readonly _context: LevelContext;
27
+ protected readonly _context: LevelContext<Registry>;
29
28
  protected _scene: Scene | null;
30
29
  protected _forRestore: boolean;
31
- clusteredLights: ClusteredLightContainer | null;
32
- outlines: OutlineManager | null;
33
30
  /**
34
- * Creates a new Level instance.
35
- *
36
- * @param config
37
- * @param context
31
+ * The simulation this level's scene was built against. A transition loads the next level before
32
+ * disposing this one, so by the time this level despawns what it spawned, the engine already holds the
33
+ * next level's simulation, minting the same IDs.
38
34
  */
39
- constructor(config: LevelConfig, context: LevelContext);
35
+ protected _simulation: NodeSimulation<Registry> | null;
36
+ clusteredLights: ClusteredLightContainer | null;
37
+ outlines: OutlineManager | null;
38
+ constructor(config: LevelConfig, context: LevelContext<Registry>);
40
39
  get scene(): Scene | null;
41
40
  get isLoaded(): boolean;
42
- get gameEngine(): GameEngine;
41
+ protected get context(): LevelContext<Registry>;
43
42
  get propertyHandlers(): Map<string, PropertyHandler>;
44
43
  /**
45
- * Emit a progress event during loading
46
- *
47
- * @param progress - Percentage (0-100)
48
- * @param message
44
+ * The simulation this level owns, or nothing for a level that never built one. The level manager binds
45
+ * the engine to the current level's simulation (design/simulation.md#which-simulation-the-engine-holds).
46
+ */
47
+ get $simulation(): NodeSimulation<Registry> | null;
48
+ /**
49
+ * Takes over a simulation built to replace this level's own, as a save load does once it has restored
50
+ * into a fresh one (design/simulation.md#saved-games).
51
+ */
52
+ $adoptSimulation(simulation: NodeSimulation<Registry>): void;
53
+ /**
54
+ * Builds this level's simulation for the scene it just created, named after the level, and keeps it. A
55
+ * subclass calls this from `buildScene` before it spawns anything, since a NodeEntity constructs its node
56
+ * in its simulation's scene (design/levels.md#the-scene-and-its-simulation).
57
+ */
58
+ protected rebuildSimulation(scene: Scene, meshSource?: MeshLoader): NodeSimulation<Registry>;
59
+ /**
60
+ * Publishes one more `level:progress` at the number and the message a subclass names, which is how a
61
+ * level reports its own progress between the 0 and the 100 the base publishes
62
+ * (design/levels.md#the-base-class).
49
63
  */
50
64
  protected $emitProgress(progress: number, message: string): void;
51
65
  protected _toColor3(color: ColorConfig): Color3;
52
66
  /**
53
- * Get normalized metadata from a node, handling glTF extras.
54
- *
55
- * Babylon.js stores glTF custom properties in node.metadata.gltf.extras,
56
- * so we need to extract them for easier access.
67
+ * A node's metadata as one flat record: its own top-level keys with its glTF extras merged onto them, extras
68
+ * winning where both name one. The `gltf` container itself is left out, since it is the loader's record and
69
+ * never an authored property, so a node whose extras are empty and whose own keys are too normalizes to an
70
+ * empty record and the walk skips it (design/authoring.md#the-walk).
57
71
  */
58
72
  protected _getNormalizedMetadata(node: TransformNode): Record<string, unknown> | null;
59
73
  /**
@@ -61,16 +75,14 @@ export declare abstract class Level implements LevelInstance {
61
75
  */
62
76
  protected _runPropertyHandlers(node: TransformNode): Promise<void>;
63
77
  /**
64
- * Load and create the scene for this level.
65
- * Progress events will be emitted during loading.
78
+ * Builds the scene through `buildScene`, publishing `level:progress` at 0 and at 100 around it and
79
+ * `level:complete` after, or `level:error` before rethrowing when building fails. A level already holding a
80
+ * scene warns and answers with the one it has.
66
81
  */
67
82
  load(options?: LevelLoadOptions): Promise<Scene>;
68
83
  /**
69
- * Abstract method for building the scene content.
70
- * Concrete levels must implement this to create their specific content.
71
- *
72
- * All dependencies are available via `this.gameEngine`.
73
- *
84
+ * Builds the scene: a subclass answers with the scene it built, reaching what it needs through `this.context`
85
+ * (design/levels.md#the-base-class).
74
86
  */
75
87
  protected abstract buildScene(): Promise<Scene>;
76
88
  /**
@@ -86,10 +98,8 @@ export declare abstract class Level implements LevelInstance {
86
98
  */
87
99
  onDeactivate?(): Promise<void> | void;
88
100
  /**
89
- * Dispose of this level's resources and scene.
90
- *
91
- * The `$` prefix indicates this is an internal lifecycle method, consistent with
92
- * other engine lifecycle methods like `$teardown()`.
101
+ * Releases what the base owns: the outline manager, the clustered light container, and the scene. Nothing for
102
+ * a level that never loaded one, and nothing the second time (design/architecture.md#teardown).
93
103
  */
94
- $dispose(): Promise<void>;
104
+ dispose(): Promise<void>;
95
105
  }