@skewedaspect/sage 0.9.3 → 0.9.5

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Christopher Case
3
+ Copyright (c) 2026 Christopher Case
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -103,7 +103,6 @@ export declare class GameLevel extends Level {
103
103
  * and fix PBR material falloff for compatibility.
104
104
  */
105
105
  private _setupClustering;
106
- private _toColor3;
107
106
  /**
108
107
  * Configure and enable the Havok physics plugin with the level's gravity settings.
109
108
  */
@@ -112,21 +111,10 @@ export declare class GameLevel extends Level {
112
111
  * Walk all transform nodes and meshes, normalizing glTF metadata and dispatching to property handlers.
113
112
  */
114
113
  private _processNodeProperties;
115
- /**
116
- * Get normalized metadata from a node, handling glTF extras.
117
- *
118
- * Babylon.js stores glTF custom properties in node.metadata.gltf.extras,
119
- * so we need to extract them for easier access.
120
- */
121
- private _getNormalizedMetadata;
122
114
  /**
123
115
  * Check a node's metadata for spawn/entity markers and run any registered property handlers.
124
116
  */
125
117
  private _processNodeMetadata;
126
- /**
127
- * Run registered property handlers on a node.
128
- */
129
- private _runPropertyHandlers;
130
118
  /**
131
119
  * Match collected spawn points against the level config's spawn definitions and instantiate entities.
132
120
  */
@@ -144,11 +132,6 @@ export declare class GameLevel extends Level {
144
132
  * Create an entity from a spawn definition, inheriting position/rotation/scaling from the spawn point.
145
133
  */
146
134
  private _spawnEntity;
147
- /**
148
- * Build or import a mesh from the entity definition and attach it to the entity.
149
- * Supports primitive shapes (box/sphere/capsule/cylinder) and GLB/GLTF file paths.
150
- */
151
- private _createEntityMesh;
152
135
  /**
153
136
  * Create game entities for all nodes marked with the 'entity' metadata property.
154
137
  */
@@ -68,9 +68,18 @@ export declare class MouseInputPlugin {
68
68
  */
69
69
  private _handlePointerMove;
70
70
  /**
71
- * Handle mouse wheel events
71
+ * Handle mouse wheel events.
72
+ *
73
+ * The wheel is a pulse, not a level: the browser reports a delta per notch and never reports "stopped scrolling".
74
+ * So each event notifies twice — once carrying the deltas, once carrying zeros. Without that trailing zero,
75
+ * `emitOnChange` value bindings swallow every notch after the first (consecutive notches report identical deltas),
76
+ * trigger bindings never see a falling edge to re-arm on, and `getState()` latches the last scroll forever.
72
77
  */
73
78
  private _handleMouseWheel;
79
+ /**
80
+ * Notify subscribers of the current wheel state, attributed to the wheel event that produced it
81
+ */
82
+ private _notifyWheelState;
74
83
  /**
75
84
  * Notify subscribers of device connected event
76
85
  */
@@ -1,9 +1,23 @@
1
- import { Scene } from '@babylonjs/core';
1
+ import { Color3, Quaternion, Scene, TransformNode, Vector3 } from '@babylonjs/core';
2
2
  import { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer';
3
3
  import { LoggerInterface } from "../interfaces/logger.d.ts";
4
- import { LevelConfig, LevelContext, LevelInstance, PropertyHandler } from "../interfaces/level.d.ts";
4
+ import { ColorConfig, LevelConfig, LevelContext, LevelInstance, PropertyHandler } from "../interfaces/level.d.ts";
5
+ import { GameEntity } from "./entity.d.ts";
5
6
  import { GameEngine } from "./gameEngine.d.ts";
6
7
  import { OutlineManager } from "../managers/outline.d.ts";
8
+ /**
9
+ * Transform and visibility overrides applied to a mesh built by `Level.createEntityMesh()`.
10
+ */
11
+ export interface CreateEntityMeshOptions {
12
+ /** World position for the entity's root node. Left at the node default when omitted. */
13
+ position?: Vector3;
14
+ /** World rotation for the entity's root node. Left at the node default when omitted. */
15
+ rotation?: Quaternion;
16
+ /** Scaling for the entity's root node. Left at the node default when omitted. */
17
+ scaling?: Vector3;
18
+ /** When false, the node is disabled immediately on creation so nothing ever renders. Default true. */
19
+ enabled?: boolean;
20
+ }
7
21
  /**
8
22
  * Abstract base class for game levels.
9
23
  *
@@ -47,11 +61,34 @@ export declare abstract class Level implements LevelInstance {
47
61
  * @param message
48
62
  */
49
63
  protected $emitProgress(progress: number, message: string): void;
64
+ protected _toColor3(color: ColorConfig): Color3;
65
+ /**
66
+ * Get normalized metadata from a node, handling glTF extras.
67
+ *
68
+ * Babylon.js stores glTF custom properties in node.metadata.gltf.extras,
69
+ * so we need to extract them for easier access.
70
+ */
71
+ protected _getNormalizedMetadata(node: TransformNode): Record<string, unknown> | null;
72
+ /**
73
+ * Run registered property handlers on a node.
74
+ */
75
+ protected _runPropertyHandlers(node: TransformNode): Promise<void>;
50
76
  /**
51
77
  * Load and create the scene for this level.
52
78
  * Progress events will be emitted during loading.
53
79
  */
54
80
  load(): Promise<Scene>;
81
+ /**
82
+ * Build or import a mesh from the entity definition and attach it to the entity.
83
+ * Supports primitive shapes (box/sphere/capsule/cylinder) and GLB/GLTF file paths.
84
+ *
85
+ * @param entity - The entity whose definition supplies the mesh spec
86
+ * @param options - Transform overrides and initial enabled state
87
+ *
88
+ * @returns The root node the mesh was built under, or null if the level has no scene, the definition has no
89
+ * mesh spec, or the GLB import produced no meshes.
90
+ */
91
+ createEntityMesh(entity: GameEntity, options?: CreateEntityMeshOptions): Promise<TransformNode | null>;
55
92
  /**
56
93
  * Abstract method for building the scene content.
57
94
  * Concrete levels must implement this to create their specific content.
@@ -34,6 +34,8 @@ export interface GameEntityDefinition<State extends object = object> {
34
34
  y: number;
35
35
  z: number;
36
36
  };
37
+ /** If false, meshes spawn hidden (isVisible = false); they still collide and block raycasts. Default: true */
38
+ visible?: boolean;
37
39
  /** Material configuration */
38
40
  material?: {
39
41
  /** Material type: 'standard' (default) or 'pbr' */
@@ -18,6 +18,11 @@ export interface CreateEntityOptions<State extends object = object> {
18
18
  tags?: string[];
19
19
  /** Scene node to attach the entity to. */
20
20
  node?: TransformNode;
21
+ /**
22
+ * Build the definition's mesh via the current level ('disabled' loads it setEnabled(false)).
23
+ * Mutually exclusive with `node`.
24
+ */
25
+ mesh?: 'enabled' | 'disabled';
21
26
  }
22
27
  export declare class GameEntityManager implements Disposable {
23
28
  /** The event bus for the entity manager. */
@@ -85,6 +90,10 @@ export declare class GameEntityManager implements Disposable {
85
90
  */
86
91
  private _registerEntityActions;
87
92
  $frameUpdate(dt: number): void;
93
+ /**
94
+ * Honor the `mesh` create option by building the definition's mesh on the current level.
95
+ */
96
+ private _buildDefinitionMesh;
88
97
  /**
89
98
  * Registers a new entity definition.
90
99
  * Also registers any actions declared in the definition with the binding manager.
package/dist/sage.es.js CHANGED
@@ -59,7 +59,7 @@ var he = class {
59
59
  configurable: !0
60
60
  });
61
61
  }
62
- }, k = "0.9.3", A = class {
62
+ }, k = "0.9.5", A = class {
63
63
  canvas;
64
64
  renderEngine;
65
65
  physics;
@@ -1606,6 +1606,19 @@ var Ae = ke(16), G = class {
1606
1606
  this._log.trace(`Updating ${this.entities.size} entities with dt=${e}`);
1607
1607
  for (let t of this.entities.values()) t.parent || t.$update(e);
1608
1608
  }
1609
+ async _buildDefinitionMesh(e, t) {
1610
+ if (!t.mesh) return;
1611
+ if (t.node) {
1612
+ this._log.warn(`Ignoring 'mesh' option for entity ${e.id}: 'node' was supplied, and the two are exclusive.`);
1613
+ return;
1614
+ }
1615
+ let n = this._engine.managers.levelManager.currentLevel;
1616
+ if (!n?.isLoaded) {
1617
+ this._log.warn(`Cannot build a mesh for entity ${e.id}: no level is loaded.`);
1618
+ return;
1619
+ }
1620
+ await n.createEntityMesh(e, { enabled: t.mesh === "enabled" });
1621
+ }
1609
1622
  registerEntityDefinition(e) {
1610
1623
  this._log.debug(`Registering entity definition: ${e.type}`);
1611
1624
  let t = e.extends ? this._resolveInheritance(e) : e;
@@ -1630,7 +1643,7 @@ var Ae = ke(16), G = class {
1630
1643
  ...i.state,
1631
1644
  ...t.initialState
1632
1645
  }), t.tags) for (let e of t.tags) i.$addTag(e);
1633
- return t.node && i.$attachToNode(t.node, this._engine), this.entities.set(i.id, i), this._indexEntity(i), this._log.debug(`Recycled entity ${i.id} from pool`), i;
1646
+ return t.node && i.$attachToNode(t.node, this._engine), this.entities.set(i.id, i), this._indexEntity(i), await this._buildDefinitionMesh(i, t), this._log.debug(`Recycled entity ${i.id} from pool`), i;
1634
1647
  }
1635
1648
  }
1636
1649
  this._log.trace(`Using entity definition with ${n.behaviors?.length || 0} behaviors`);
@@ -1660,7 +1673,7 @@ var Ae = ke(16), G = class {
1660
1673
  });
1661
1674
  a.$addChild(n);
1662
1675
  }
1663
- return a;
1676
+ return await this._buildDefinitionMesh(a, t), a;
1664
1677
  }
1665
1678
  async destroyEntity(e) {
1666
1679
  this._log.debug(`Destroying entity: ${e}`);
@@ -1912,6 +1925,21 @@ var ze = class {
1912
1925
  }
1913
1926
  });
1914
1927
  }
1928
+ _toColor3(e) {
1929
+ return new r(e.r, e.g, e.b);
1930
+ }
1931
+ _getNormalizedMetadata(e) {
1932
+ if (!e.metadata) return null;
1933
+ let t = e.metadata?.gltf?.extras;
1934
+ return t ? { ...t } : e.metadata;
1935
+ }
1936
+ async _runPropertyHandlers(e) {
1937
+ for (let [t, n] of this.propertyHandlers) if (t in e.metadata) try {
1938
+ await n(e, e.metadata[t], this, this.gameEngine);
1939
+ } catch (n) {
1940
+ this._log.error(`Error in property handler '${t}' for node '${e.name}':`, n);
1941
+ }
1942
+ }
1915
1943
  async load() {
1916
1944
  if (this._scene) return this._log.warn(`Level ${this.name} is already loaded`), this._scene;
1917
1945
  this._log.info(`Loading level: ${this.name}`), this._log.time(`level-${this.name}-load`);
@@ -1935,6 +1963,74 @@ var ze = class {
1935
1963
  }), e;
1936
1964
  }
1937
1965
  }
1966
+ async createEntityMesh(e, t = {}) {
1967
+ if (!this._scene) return null;
1968
+ let n = this.gameEngine.managers.entityManager.getDefinition(e.type);
1969
+ if (!n?.mesh) return null;
1970
+ let r = n.mesh, i = this._scene, a = new D(`entity-${e.id}`, i);
1971
+ t.enabled === !1 && a.setEnabled(!1);
1972
+ let o;
1973
+ switch (r.source) {
1974
+ case "box":
1975
+ o = [b.CreateBox(`${e.type}-mesh`, {
1976
+ size: r.params?.size ?? 1,
1977
+ width: r.params?.width,
1978
+ height: r.params?.height,
1979
+ depth: r.params?.depth
1980
+ }, i)];
1981
+ break;
1982
+ case "sphere":
1983
+ o = [b.CreateSphere(`${e.type}-mesh`, {
1984
+ diameter: r.params?.diameter ?? 1,
1985
+ segments: r.params?.segments ?? 16
1986
+ }, i)];
1987
+ break;
1988
+ case "capsule":
1989
+ o = [b.CreateCapsule(`${e.type}-mesh`, {
1990
+ height: r.params?.height ?? 1.8,
1991
+ radius: r.params?.radius ?? .4
1992
+ }, i)];
1993
+ break;
1994
+ case "cylinder":
1995
+ o = [b.CreateCylinder(`${e.type}-mesh`, {
1996
+ height: r.params?.height ?? 1,
1997
+ diameter: r.params?.diameter ?? 1
1998
+ }, i)];
1999
+ break;
2000
+ default: {
2001
+ let e = await this.gameEngine.engines.sceneEngine.importMeshes([], r.source, i);
2002
+ if (e.meshes.length === 0) return this._log.warn(`No meshes loaded from: ${r.source}`), a.dispose(), null;
2003
+ o = e.meshes;
2004
+ for (let t of e.transformNodes) t.parent ||= a;
2005
+ break;
2006
+ }
2007
+ }
2008
+ for (let e of o) e.parent ||= a;
2009
+ t.position && a.position.copyFrom(t.position), t.rotation && (a.rotationQuaternion = t.rotation.clone()), t.scaling && a.scaling.copyFrom(t.scaling), r.scale && (typeof r.scale == "number" ? a.scaling.setAll(r.scale) : a.scaling.set(r.scale.x, r.scale.y, r.scale.z));
2010
+ let s = r.material;
2011
+ if (s?.color) {
2012
+ let t = s.color;
2013
+ if (s.type === "pbr") {
2014
+ let n = new S(`${e.type}-material`, i);
2015
+ n.albedoColor = this._toColor3(t), s.emissive && (n.emissiveColor = this._toColor3(s.emissive)), n.metallic = s.metallic ?? 0, n.roughness = s.roughness ?? 1;
2016
+ for (let e of o) e.material = n;
2017
+ } else {
2018
+ let n = new E(`${e.type}-material`, i);
2019
+ n.diffuseColor = this._toColor3(t), s.emissive && (n.emissiveColor = this._toColor3(s.emissive));
2020
+ for (let e of o) e.material = n;
2021
+ }
2022
+ }
2023
+ if (r.visible === !1) for (let e of o) e.isVisible = !1;
2024
+ let c = a.getDescendants(!1).filter((e) => e instanceof D);
2025
+ for (let e of c) {
2026
+ let t = this._getNormalizedMetadata(e);
2027
+ t && Object.keys(t).length > 0 && (e.metadata = {
2028
+ ...e.metadata,
2029
+ ...t
2030
+ }, await this._runPropertyHandlers(e));
2031
+ }
2032
+ return this.gameEngine.managers.entityManager.attachToNode(e, a), a;
2033
+ }
1938
2034
  async $dispose() {
1939
2035
  if (this._scene) {
1940
2036
  this._log.info(`Disposing level: ${this.name}`);
@@ -2163,9 +2259,6 @@ var ze = class {
2163
2259
  t.horizontalTiles !== void 0 && (r.horizontalTiles = t.horizontalTiles), t.verticalTiles !== void 0 && (r.verticalTiles = t.verticalTiles), t.depthSlices !== void 0 && (r.depthSlices = t.depthSlices), t.maxRange !== void 0 && (r.maxRange = t.maxRange), this.clusteredLights = r, this._log.debug(`Clustered lighting enabled: ${n.length} lights, ${r.horizontalTiles}x${r.verticalTiles} tiles, ${r.depthSlices} depth slices`);
2164
2260
  for (let t of e.materials) t instanceof S && (t.useGLTFLightFalloff = !0);
2165
2261
  }
2166
- _toColor3(e) {
2167
- return new r(e.r, e.g, e.b);
2168
- }
2169
2262
  async _enablePhysics(e) {
2170
2263
  let t = this._config.physics, n = new O(0, -9.81, 0);
2171
2264
  typeof t == "object" && t.gravity && (n = new O(t.gravity.x, t.gravity.y, t.gravity.z));
@@ -2183,11 +2276,6 @@ var ze = class {
2183
2276
  }
2184
2277
  this._log.debug(`Processed ${t.length} nodes, found ${this._spawnPoints.length} spawn points and ${this._entityNodes.length} entity nodes`);
2185
2278
  }
2186
- _getNormalizedMetadata(e) {
2187
- if (!e.metadata) return null;
2188
- let t = e.metadata?.gltf?.extras;
2189
- return t ? { ...t } : e.metadata;
2190
- }
2191
2279
  async _processNodeMetadata(e) {
2192
2280
  if ("spawn" in e.metadata) {
2193
2281
  let { position: t, rotation: n, scaling: r } = Re(e);
@@ -2204,13 +2292,6 @@ var ze = class {
2204
2292
  node: e
2205
2293
  }), await this._runPropertyHandlers(e);
2206
2294
  }
2207
- async _runPropertyHandlers(e) {
2208
- for (let [t, n] of this.propertyHandlers) if (t in e.metadata) try {
2209
- await n(e, e.metadata[t], this, this.gameEngine);
2210
- } catch (n) {
2211
- this._log.error(`Error in property handler '${t}' for node '${e.name}':`, n);
2212
- }
2213
- }
2214
2295
  async _processSpawnPoints() {
2215
2296
  for (let e of this._spawnPoints) {
2216
2297
  let t = this._config.spawns?.[e.name];
@@ -2240,75 +2321,11 @@ var ze = class {
2240
2321
  tags: e.tags,
2241
2322
  initialState: n
2242
2323
  });
2243
- return await this._createEntityMesh(r, t), r;
2244
- }
2245
- async _createEntityMesh(e, t) {
2246
- if (!this._scene) return;
2247
- let n = this.gameEngine.managers.entityManager.getDefinition(e.type);
2248
- if (!n?.mesh) return;
2249
- let r = n.mesh, i = this._scene, a = new D(`entity-${e.id}`, i), o;
2250
- switch (r.source) {
2251
- case "box":
2252
- o = [b.CreateBox(`${e.type}-mesh`, {
2253
- size: r.params?.size ?? 1,
2254
- width: r.params?.width,
2255
- height: r.params?.height,
2256
- depth: r.params?.depth
2257
- }, i)];
2258
- break;
2259
- case "sphere":
2260
- o = [b.CreateSphere(`${e.type}-mesh`, {
2261
- diameter: r.params?.diameter ?? 1,
2262
- segments: r.params?.segments ?? 16
2263
- }, i)];
2264
- break;
2265
- case "capsule":
2266
- o = [b.CreateCapsule(`${e.type}-mesh`, {
2267
- height: r.params?.height ?? 1.8,
2268
- radius: r.params?.radius ?? .4
2269
- }, i)];
2270
- break;
2271
- case "cylinder":
2272
- o = [b.CreateCylinder(`${e.type}-mesh`, {
2273
- height: r.params?.height ?? 1,
2274
- diameter: r.params?.diameter ?? 1
2275
- }, i)];
2276
- break;
2277
- default: {
2278
- let e = await this.gameEngine.engines.sceneEngine.importMeshes([], r.source, i);
2279
- if (e.meshes.length === 0) {
2280
- this._log.warn(`No meshes loaded from: ${r.source}`), a.dispose();
2281
- return;
2282
- }
2283
- o = e.meshes;
2284
- for (let t of e.transformNodes) t.parent ||= a;
2285
- break;
2286
- }
2287
- }
2288
- for (let e of o) e.parent ||= a;
2289
- a.position.copyFrom(t.position), a.rotationQuaternion = t.rotation.clone(), a.scaling.copyFrom(t.scaling), r.scale && (typeof r.scale == "number" ? a.scaling.setAll(r.scale) : a.scaling.set(r.scale.x, r.scale.y, r.scale.z));
2290
- let s = r.material;
2291
- if (s?.color) {
2292
- let t = s.color;
2293
- if (s.type === "pbr") {
2294
- let n = new S(`${e.type}-material`, i);
2295
- n.albedoColor = this._toColor3(t), s.emissive && (n.emissiveColor = this._toColor3(s.emissive)), n.metallic = s.metallic ?? 0, n.roughness = s.roughness ?? 1;
2296
- for (let e of o) e.material = n;
2297
- } else {
2298
- let n = new E(`${e.type}-material`, i);
2299
- n.diffuseColor = this._toColor3(t), s.emissive && (n.emissiveColor = this._toColor3(s.emissive));
2300
- for (let e of o) e.material = n;
2301
- }
2302
- }
2303
- let c = a.getDescendants(!1).filter((e) => e instanceof D);
2304
- for (let e of c) {
2305
- let t = this._getNormalizedMetadata(e);
2306
- t && Object.keys(t).length > 0 && (e.metadata = {
2307
- ...e.metadata,
2308
- ...t
2309
- }, await this._runPropertyHandlers(e));
2310
- }
2311
- this.gameEngine.managers.entityManager.attachToNode(e, a);
2324
+ return await this.createEntityMesh(r, {
2325
+ position: t.position,
2326
+ rotation: t.rotation,
2327
+ scaling: t.scaling
2328
+ }), r;
2312
2329
  }
2313
2330
  async _processEntityNodes() {
2314
2331
  for (let e of this._entityNodes) await this._processEntityNode(e);
@@ -2878,7 +2895,15 @@ var ze = class {
2878
2895
  deltaY: e.deltaY,
2879
2896
  deltaZ: e.deltaZ,
2880
2897
  deltaMode: e.deltaMode
2881
- }, this._notifyInputChanged({
2898
+ }, this._notifyWheelState(e), this._wheelState = {
2899
+ deltaX: 0,
2900
+ deltaY: 0,
2901
+ deltaZ: 0,
2902
+ deltaMode: 0
2903
+ }, this._notifyWheelState(e);
2904
+ }
2905
+ _notifyWheelState(e) {
2906
+ this._notifyInputChanged({
2882
2907
  type: "mouse",
2883
2908
  event: e,
2884
2909
  buttons: { ...this._buttonState },