@yagejs/core 0.1.0 → 0.3.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.cjs +764 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +440 -23
- package/dist/index.d.ts +440 -23
- package/dist/index.js +759 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -170,14 +170,52 @@ var Vec2 = class _Vec2 {
|
|
|
170
170
|
static lerp(a, b, t) {
|
|
171
171
|
return new _Vec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
|
172
172
|
}
|
|
173
|
+
/** Move current toward target by at most maxDelta without overshooting. */
|
|
174
|
+
static moveTowards(current, target, maxDelta) {
|
|
175
|
+
const dx = target.x - current.x;
|
|
176
|
+
const dy = target.y - current.y;
|
|
177
|
+
const distanceSq = dx * dx + dy * dy;
|
|
178
|
+
if (distanceSq < EPSILON * EPSILON) {
|
|
179
|
+
return new _Vec2(target.x, target.y);
|
|
180
|
+
}
|
|
181
|
+
if (maxDelta <= 0) {
|
|
182
|
+
return new _Vec2(current.x, current.y);
|
|
183
|
+
}
|
|
184
|
+
const distance = Math.sqrt(distanceSq);
|
|
185
|
+
if (distance <= maxDelta) {
|
|
186
|
+
return new _Vec2(target.x, target.y);
|
|
187
|
+
}
|
|
188
|
+
const scale = maxDelta / distance;
|
|
189
|
+
return new _Vec2(current.x + dx * scale, current.y + dy * scale);
|
|
190
|
+
}
|
|
173
191
|
};
|
|
174
192
|
|
|
175
193
|
// src/MathUtils.ts
|
|
194
|
+
var TAU = Math.PI * 2;
|
|
195
|
+
var MIN_SMOOTH_TIME = 1e-4;
|
|
196
|
+
function normalizeAngle(radians) {
|
|
197
|
+
const wrapped = ((radians + Math.PI) % TAU + TAU) % TAU - Math.PI;
|
|
198
|
+
return wrapped === -Math.PI && radians > 0 ? Math.PI : wrapped;
|
|
199
|
+
}
|
|
200
|
+
__name(normalizeAngle, "normalizeAngle");
|
|
176
201
|
var MathUtils = {
|
|
177
202
|
/** Linear interpolation between a and b. */
|
|
178
203
|
lerp(a, b, t) {
|
|
179
204
|
return a + (b - a) * t;
|
|
180
205
|
},
|
|
206
|
+
/** Return the clamped interpolation factor that produces v between a and b. */
|
|
207
|
+
inverseLerp(a, b, v) {
|
|
208
|
+
if (a === b) return 0;
|
|
209
|
+
return MathUtils.clamp((v - a) / (b - a), 0, 1);
|
|
210
|
+
},
|
|
211
|
+
/** Interpolate between angles in radians along the shortest path. */
|
|
212
|
+
lerpAngle(a, b, t) {
|
|
213
|
+
return normalizeAngle(a + MathUtils.shortestAngleBetween(a, b) * t);
|
|
214
|
+
},
|
|
215
|
+
/** Signed shortest angular delta from a to b, in radians. */
|
|
216
|
+
shortestAngleBetween(a, b) {
|
|
217
|
+
return normalizeAngle(b - a);
|
|
218
|
+
},
|
|
181
219
|
/** Clamp a value between min and max. */
|
|
182
220
|
clamp(value, min, max) {
|
|
183
221
|
return Math.max(min, Math.min(max, value));
|
|
@@ -187,6 +225,12 @@ var MathUtils = {
|
|
|
187
225
|
const t = (value - inMin) / (inMax - inMin);
|
|
188
226
|
return outMin + (outMax - outMin) * t;
|
|
189
227
|
},
|
|
228
|
+
/** Bounce t between 0 and length. */
|
|
229
|
+
pingPong(t, length) {
|
|
230
|
+
if (length <= 0) return 0;
|
|
231
|
+
const wrapped = MathUtils.wrap(t, 0, length * 2);
|
|
232
|
+
return length - Math.abs(wrapped - length);
|
|
233
|
+
},
|
|
190
234
|
/** Random float in [min, max). */
|
|
191
235
|
randomRange(min, max) {
|
|
192
236
|
return min + Math.random() * (max - min);
|
|
@@ -210,6 +254,34 @@ var MathUtils = {
|
|
|
210
254
|
}
|
|
211
255
|
return Math.max(current - step, target);
|
|
212
256
|
},
|
|
257
|
+
/**
|
|
258
|
+
* Smoothly damp current toward target without overshooting.
|
|
259
|
+
* Pass the returned velocity back into the next call.
|
|
260
|
+
*/
|
|
261
|
+
smoothDamp(current, target, velocity, smoothTime, deltaTime, maxSpeed = Infinity) {
|
|
262
|
+
if (deltaTime <= 0) {
|
|
263
|
+
return { value: current, velocity };
|
|
264
|
+
}
|
|
265
|
+
const safeSmoothTime = Math.max(MIN_SMOOTH_TIME, smoothTime);
|
|
266
|
+
const omega = 2 / safeSmoothTime;
|
|
267
|
+
const x = omega * deltaTime;
|
|
268
|
+
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
|
|
269
|
+
const originalTarget = target;
|
|
270
|
+
const maxChange = maxSpeed * safeSmoothTime;
|
|
271
|
+
const change = MathUtils.clamp(current - target, -maxChange, maxChange);
|
|
272
|
+
const adjustedTarget = current - change;
|
|
273
|
+
const temp = (velocity + omega * change) * deltaTime;
|
|
274
|
+
const nextVelocity = (velocity - omega * temp) * exp;
|
|
275
|
+
let value = adjustedTarget + (change + temp) * exp;
|
|
276
|
+
let resultVelocity = nextVelocity;
|
|
277
|
+
const targetIsAboveCurrent = originalTarget - current > 0;
|
|
278
|
+
const valuePassedTarget = targetIsAboveCurrent ? value > originalTarget : value < originalTarget;
|
|
279
|
+
if (valuePassedTarget) {
|
|
280
|
+
value = originalTarget;
|
|
281
|
+
resultVelocity = 0;
|
|
282
|
+
}
|
|
283
|
+
return { value, velocity: resultVelocity };
|
|
284
|
+
},
|
|
213
285
|
/** Wrap value into the range [min, max). */
|
|
214
286
|
wrap(value, min, max) {
|
|
215
287
|
const range = max - min;
|
|
@@ -367,13 +439,16 @@ var Logger = class {
|
|
|
367
439
|
|
|
368
440
|
// src/EngineContext.ts
|
|
369
441
|
var ServiceKey = class {
|
|
370
|
-
constructor(id) {
|
|
442
|
+
constructor(id, options) {
|
|
371
443
|
this.id = id;
|
|
444
|
+
this.scope = options?.scope ?? "engine";
|
|
372
445
|
}
|
|
373
446
|
id;
|
|
374
447
|
static {
|
|
375
448
|
__name(this, "ServiceKey");
|
|
376
449
|
}
|
|
450
|
+
/** Declared scope (engine or scene). Defaults to `"engine"`. */
|
|
451
|
+
scope;
|
|
377
452
|
};
|
|
378
453
|
var EngineContext = class {
|
|
379
454
|
static {
|
|
@@ -419,6 +494,50 @@ var SystemSchedulerKey = new ServiceKey("systemScheduler");
|
|
|
419
494
|
var ProcessSystemKey = new ServiceKey("processSystem");
|
|
420
495
|
var AssetManagerKey = new ServiceKey("assetManager");
|
|
421
496
|
|
|
497
|
+
// src/SceneHooks.ts
|
|
498
|
+
var SceneHookRegistry = class {
|
|
499
|
+
static {
|
|
500
|
+
__name(this, "SceneHookRegistry");
|
|
501
|
+
}
|
|
502
|
+
hooks = [];
|
|
503
|
+
register(hooks) {
|
|
504
|
+
this.hooks.push(hooks);
|
|
505
|
+
return () => {
|
|
506
|
+
const idx = this.hooks.indexOf(hooks);
|
|
507
|
+
if (idx !== -1) this.hooks.splice(idx, 1);
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/** Run all `beforeEnter` hooks serially. */
|
|
511
|
+
async runBeforeEnter(scene) {
|
|
512
|
+
for (const h of this.hooks) {
|
|
513
|
+
await h.beforeEnter?.(scene);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
runAfterExit(scene) {
|
|
517
|
+
for (const h of this.hooks) {
|
|
518
|
+
try {
|
|
519
|
+
h.afterExit?.(scene);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
const logger = scene.context.tryResolve(LoggerKey);
|
|
522
|
+
if (logger) {
|
|
523
|
+
logger.error("core", "Scene afterExit hook threw", {
|
|
524
|
+
scene: scene.name,
|
|
525
|
+
error: err
|
|
526
|
+
});
|
|
527
|
+
} else {
|
|
528
|
+
console.error(
|
|
529
|
+
`[yage] Scene afterExit hook threw for scene "${scene.name}":`,
|
|
530
|
+
err
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
var SceneHookRegistryKey = new ServiceKey(
|
|
538
|
+
"sceneHookRegistry"
|
|
539
|
+
);
|
|
540
|
+
|
|
422
541
|
// src/EventToken.ts
|
|
423
542
|
var EventToken = class {
|
|
424
543
|
constructor(name) {
|
|
@@ -640,10 +759,11 @@ var Component = class {
|
|
|
640
759
|
_cleanups;
|
|
641
760
|
/**
|
|
642
761
|
* Access the entity's scene. Throws if the entity is not in a scene.
|
|
643
|
-
* Prefer this over `this.entity.scene
|
|
762
|
+
* Prefer this over threading through `this.entity.scene` in component
|
|
763
|
+
* code.
|
|
644
764
|
*/
|
|
645
765
|
get scene() {
|
|
646
|
-
const scene = this.entity.
|
|
766
|
+
const scene = this.entity.tryScene;
|
|
647
767
|
if (!scene) {
|
|
648
768
|
throw new Error(
|
|
649
769
|
"Cannot access scene: entity is not attached to a scene."
|
|
@@ -658,20 +778,43 @@ var Component = class {
|
|
|
658
778
|
get context() {
|
|
659
779
|
return this.scene.context;
|
|
660
780
|
}
|
|
661
|
-
/**
|
|
781
|
+
/**
|
|
782
|
+
* Resolve a service by key, cached after first lookup. Scene-scoped values
|
|
783
|
+
* (registered via `scene._registerScoped`) take precedence over engine
|
|
784
|
+
* scope. A key declared with `scope: "scene"` that falls back to engine
|
|
785
|
+
* scope emits a one-shot dev warning — almost always signals a missed
|
|
786
|
+
* `beforeEnter` hook.
|
|
787
|
+
*/
|
|
662
788
|
use(key) {
|
|
663
789
|
this._serviceCache ??= /* @__PURE__ */ new Map();
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
790
|
+
const cached = this._serviceCache.get(key.id);
|
|
791
|
+
if (cached !== void 0) return cached;
|
|
792
|
+
const scene = this.entity.tryScene;
|
|
793
|
+
const scoped = scene?._resolveScoped(key);
|
|
794
|
+
if (scoped !== void 0) {
|
|
795
|
+
this._serviceCache.set(key.id, scoped);
|
|
796
|
+
return scoped;
|
|
797
|
+
}
|
|
798
|
+
const value = this.context.resolve(key);
|
|
799
|
+
if (key.scope === "scene") {
|
|
800
|
+
this._warnScopedFallback(key);
|
|
801
|
+
return value;
|
|
802
|
+
}
|
|
803
|
+
this._serviceCache.set(key.id, value);
|
|
669
804
|
return value;
|
|
670
805
|
}
|
|
806
|
+
_warnScopedFallback(key) {
|
|
807
|
+
const logger = this.context.tryResolve(LoggerKey);
|
|
808
|
+
logger?.warn(
|
|
809
|
+
"core",
|
|
810
|
+
`Scoped key "${key.id}" fell back to engine scope \u2014 did a plugin forget to register a beforeEnter hook?`,
|
|
811
|
+
{ component: this.constructor.name }
|
|
812
|
+
);
|
|
813
|
+
}
|
|
671
814
|
/**
|
|
672
815
|
* Lazy proxy-based service resolution. Can be used at field-declaration time:
|
|
673
816
|
* ```ts
|
|
674
|
-
* readonly
|
|
817
|
+
* readonly input = this.service(InputManagerKey);
|
|
675
818
|
* ```
|
|
676
819
|
* The actual resolution is deferred until first property access.
|
|
677
820
|
*/
|
|
@@ -928,8 +1071,27 @@ var Entity = class {
|
|
|
928
1071
|
this.name = name ?? new.target.name ?? "Entity";
|
|
929
1072
|
this.tags = new Set(tags);
|
|
930
1073
|
}
|
|
931
|
-
/**
|
|
1074
|
+
/**
|
|
1075
|
+
* The scene this entity belongs to. Throws if the entity is not attached
|
|
1076
|
+
* to a scene — which in practice only happens before `scene.spawn` /
|
|
1077
|
+
* `addChild` wires it up, or after `destroy()` tears it down. Inside
|
|
1078
|
+
* lifecycle methods (`setup`, component `onAdd`, `update`, etc.) this is
|
|
1079
|
+
* always safe to access.
|
|
1080
|
+
*
|
|
1081
|
+
* For the rare case where you genuinely need to inspect whether an
|
|
1082
|
+
* entity has a scene (e.g. defensive code in systems iterating a query
|
|
1083
|
+
* result), use `tryScene` instead.
|
|
1084
|
+
*/
|
|
932
1085
|
get scene() {
|
|
1086
|
+
if (!this._scene) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`Entity "${this.name}" is not attached to a scene. Use \`tryScene\` if you need to check.`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
return this._scene;
|
|
1092
|
+
}
|
|
1093
|
+
/** The scene this entity belongs to, or `null` if detached. */
|
|
1094
|
+
get tryScene() {
|
|
933
1095
|
return this._scene;
|
|
934
1096
|
}
|
|
935
1097
|
/** True if destroy() has been called. */
|
|
@@ -967,6 +1129,20 @@ var Entity = class {
|
|
|
967
1129
|
this._scene._addExistingEntity(child);
|
|
968
1130
|
}
|
|
969
1131
|
}
|
|
1132
|
+
spawnChild(name, classOrBlueprint, params) {
|
|
1133
|
+
const scene = this.scene;
|
|
1134
|
+
if (this._children?.has(name)) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`Entity "${this.name}" already has a child named "${name}".`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const child = classOrBlueprint === void 0 ? scene.spawn(name) : scene.spawn(
|
|
1140
|
+
classOrBlueprint,
|
|
1141
|
+
params
|
|
1142
|
+
);
|
|
1143
|
+
this.addChild(name, child);
|
|
1144
|
+
return child;
|
|
1145
|
+
}
|
|
970
1146
|
/** Remove a named child. Returns the detached entity. */
|
|
971
1147
|
removeChild(name) {
|
|
972
1148
|
const child = this._children?.get(name);
|
|
@@ -1459,7 +1635,7 @@ var GameLoop = class {
|
|
|
1459
1635
|
}
|
|
1460
1636
|
/** Process one frame with the given dt in milliseconds. */
|
|
1461
1637
|
tick(dtMs) {
|
|
1462
|
-
if (!this.callbacks) return;
|
|
1638
|
+
if (!this.running || !this.callbacks) return;
|
|
1463
1639
|
this._frameCount++;
|
|
1464
1640
|
this.callbacks.earlyUpdate(dtMs);
|
|
1465
1641
|
this.accumulator += dtMs;
|
|
@@ -1487,6 +1663,8 @@ var Scene = class {
|
|
|
1487
1663
|
transparentBelow = false;
|
|
1488
1664
|
/** Asset handles to load before onEnter(). Override in subclasses. */
|
|
1489
1665
|
preload;
|
|
1666
|
+
/** Default transition used when this scene is the destination of a push/pop/replace. */
|
|
1667
|
+
defaultTransition;
|
|
1490
1668
|
/** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
|
|
1491
1669
|
paused = false;
|
|
1492
1670
|
/** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
|
|
@@ -1498,6 +1676,7 @@ var Scene = class {
|
|
|
1498
1676
|
queryCache;
|
|
1499
1677
|
bus;
|
|
1500
1678
|
_entityEventHandlers;
|
|
1679
|
+
_scopedServices;
|
|
1501
1680
|
/** Access the EngineContext. */
|
|
1502
1681
|
get context() {
|
|
1503
1682
|
return this._context;
|
|
@@ -1515,6 +1694,11 @@ var Scene = class {
|
|
|
1515
1694
|
}
|
|
1516
1695
|
return false;
|
|
1517
1696
|
}
|
|
1697
|
+
/** Whether a scene transition is currently running. */
|
|
1698
|
+
get isTransitioning() {
|
|
1699
|
+
const sm = this._context?.tryResolve(SceneManagerKey);
|
|
1700
|
+
return sm?.isTransitioning ?? false;
|
|
1701
|
+
}
|
|
1518
1702
|
/** Convenience accessor for the AssetManager. */
|
|
1519
1703
|
get assets() {
|
|
1520
1704
|
return this._context.resolve(AssetManagerKey);
|
|
@@ -1639,6 +1823,31 @@ var Scene = class {
|
|
|
1639
1823
|
}
|
|
1640
1824
|
}
|
|
1641
1825
|
// ---- Internal methods ----
|
|
1826
|
+
/**
|
|
1827
|
+
* Register a scene-scoped service. Called from a plugin's `beforeEnter`
|
|
1828
|
+
* hook to make per-scene state (render tree, physics world) resolvable via
|
|
1829
|
+
* `Component.use(key)`.
|
|
1830
|
+
* @internal
|
|
1831
|
+
*/
|
|
1832
|
+
_registerScoped(key, value) {
|
|
1833
|
+
this._scopedServices ??= /* @__PURE__ */ new Map();
|
|
1834
|
+
this._scopedServices.set(key.id, value);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Resolve a scene-scoped service, or `undefined` if none was registered.
|
|
1838
|
+
* @internal
|
|
1839
|
+
*/
|
|
1840
|
+
_resolveScoped(key) {
|
|
1841
|
+
return this._scopedServices?.get(key.id);
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Clear all scene-scoped services. Called by the SceneManager after
|
|
1845
|
+
* `afterExit` hooks run, so plugin cleanup code still sees scoped state.
|
|
1846
|
+
* @internal
|
|
1847
|
+
*/
|
|
1848
|
+
_clearScopedServices() {
|
|
1849
|
+
this._scopedServices?.clear();
|
|
1850
|
+
}
|
|
1642
1851
|
/**
|
|
1643
1852
|
* Set the engine context. Called by SceneManager when the scene is pushed.
|
|
1644
1853
|
* @internal
|
|
@@ -1690,6 +1899,138 @@ var Scene = class {
|
|
|
1690
1899
|
}
|
|
1691
1900
|
};
|
|
1692
1901
|
|
|
1902
|
+
// src/LoadingScene.ts
|
|
1903
|
+
var LoadingScene = class extends Scene {
|
|
1904
|
+
static {
|
|
1905
|
+
__name(this, "LoadingScene");
|
|
1906
|
+
}
|
|
1907
|
+
name = "loading";
|
|
1908
|
+
/**
|
|
1909
|
+
* Minimum wall-clock ms the scene stays visible before handing off.
|
|
1910
|
+
* Prevents flicker on cached loads. Default 0.
|
|
1911
|
+
*/
|
|
1912
|
+
minDuration = 0;
|
|
1913
|
+
/** Transition used for the loading → target handoff. */
|
|
1914
|
+
transition;
|
|
1915
|
+
/**
|
|
1916
|
+
* When true (default), the handoff fires automatically after loading and
|
|
1917
|
+
* `minDuration`. Set false to gate it behind `continue()` — useful when
|
|
1918
|
+
* the loading scene also asks the player to press a key or click.
|
|
1919
|
+
*/
|
|
1920
|
+
autoContinue = true;
|
|
1921
|
+
_progress = 0;
|
|
1922
|
+
_started = false;
|
|
1923
|
+
_active = true;
|
|
1924
|
+
_continueRequested = false;
|
|
1925
|
+
_continueGate;
|
|
1926
|
+
// Bumped on every `_run` attempt. `AssetManager.loadAll` uses `Promise.all`
|
|
1927
|
+
// under the hood, so individual loaders from a failed attempt can still
|
|
1928
|
+
// resolve and fire `onProgress` after the attempt rejects. Without this
|
|
1929
|
+
// guard, a retry kicked off from `onLoadError` would see stale progress
|
|
1930
|
+
// callbacks mutate `_progress` and emit `scene:loading:progress` events
|
|
1931
|
+
// attributed to the current attempt.
|
|
1932
|
+
_attempt = 0;
|
|
1933
|
+
/** Current load progress, 0 → 1. Updated as the AssetManager reports progress. */
|
|
1934
|
+
get progress() {
|
|
1935
|
+
return this._progress;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Kick off asset loading. While a load is in flight, subsequent calls
|
|
1939
|
+
* are no-ops. After a load failure the guard is released, so calling
|
|
1940
|
+
* `startLoading()` from `onLoadError` (or from a retry button) kicks off
|
|
1941
|
+
* a fresh load against the same target.
|
|
1942
|
+
*
|
|
1943
|
+
* Usually called once from `onEnter` after spawning the loading UI:
|
|
1944
|
+
* ```ts
|
|
1945
|
+
* override onEnter() {
|
|
1946
|
+
* this.spawn(LoadingSceneProgressBar);
|
|
1947
|
+
* this.startLoading();
|
|
1948
|
+
* }
|
|
1949
|
+
* ```
|
|
1950
|
+
*
|
|
1951
|
+
* Deferring the call lets you gate the start of the load behind a
|
|
1952
|
+
* title screen, "press any key" prompt, intro animation, etc.
|
|
1953
|
+
*/
|
|
1954
|
+
startLoading() {
|
|
1955
|
+
if (this._started) return;
|
|
1956
|
+
this._started = true;
|
|
1957
|
+
this._run().catch((err) => {
|
|
1958
|
+
if (!this._active) return;
|
|
1959
|
+
const logger = this.context.tryResolve(LoggerKey);
|
|
1960
|
+
if (logger) {
|
|
1961
|
+
logger.error("LoadingScene", "loading failed", { error: err });
|
|
1962
|
+
} else {
|
|
1963
|
+
console.error("[LoadingScene] loading failed:", err);
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Trigger the handoff to `target`. No-op if already called or if
|
|
1969
|
+
* `autoContinue` already fired it. If called before loading finishes,
|
|
1970
|
+
* the handoff runs as soon as loading + `minDuration` complete.
|
|
1971
|
+
*/
|
|
1972
|
+
continue() {
|
|
1973
|
+
if (this._continueRequested) return;
|
|
1974
|
+
this._continueRequested = true;
|
|
1975
|
+
this._continueGate?.();
|
|
1976
|
+
}
|
|
1977
|
+
onExit() {
|
|
1978
|
+
this._active = false;
|
|
1979
|
+
this._continueGate?.();
|
|
1980
|
+
}
|
|
1981
|
+
async _run() {
|
|
1982
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1983
|
+
if (!this._active) return;
|
|
1984
|
+
const attempt = ++this._attempt;
|
|
1985
|
+
const target = typeof this.target === "function" ? this.target() : this.target;
|
|
1986
|
+
const startedAt = performance.now();
|
|
1987
|
+
const bus = this.context.resolve(EventBusKey);
|
|
1988
|
+
try {
|
|
1989
|
+
await this.assets.loadAll(target.preload ?? [], (ratio) => {
|
|
1990
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1991
|
+
this._progress = ratio;
|
|
1992
|
+
bus.emit("scene:loading:progress", { scene: this, ratio });
|
|
1993
|
+
});
|
|
1994
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
1995
|
+
const elapsed = performance.now() - startedAt;
|
|
1996
|
+
const remaining = this.minDuration - elapsed;
|
|
1997
|
+
if (remaining > 0) {
|
|
1998
|
+
await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
1999
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2000
|
+
}
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2003
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2004
|
+
this._started = false;
|
|
2005
|
+
this._attempt++;
|
|
2006
|
+
if (this.onLoadError) {
|
|
2007
|
+
await this.onLoadError(error);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
throw error;
|
|
2011
|
+
}
|
|
2012
|
+
bus.emit("scene:loading:done", { scene: this });
|
|
2013
|
+
if (!this.autoContinue && !this._continueRequested) {
|
|
2014
|
+
await new Promise((resolve) => {
|
|
2015
|
+
this._continueGate = resolve;
|
|
2016
|
+
});
|
|
2017
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2018
|
+
}
|
|
2019
|
+
const scenes = this.context.resolve(SceneManagerKey);
|
|
2020
|
+
await scenes.replace(
|
|
2021
|
+
target,
|
|
2022
|
+
this.transition ? { transition: this.transition } : void 0
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
// src/SceneTransition.ts
|
|
2028
|
+
function resolveTransition(callSite, destination) {
|
|
2029
|
+
if (callSite) return callSite;
|
|
2030
|
+
return destination?.defaultTransition;
|
|
2031
|
+
}
|
|
2032
|
+
__name(resolveTransition, "resolveTransition");
|
|
2033
|
+
|
|
1693
2034
|
// src/SceneManager.ts
|
|
1694
2035
|
var SceneManager = class {
|
|
1695
2036
|
static {
|
|
@@ -1699,6 +2040,35 @@ var SceneManager = class {
|
|
|
1699
2040
|
_context;
|
|
1700
2041
|
bus;
|
|
1701
2042
|
assetManager;
|
|
2043
|
+
hookRegistry;
|
|
2044
|
+
logger;
|
|
2045
|
+
_currentRun;
|
|
2046
|
+
_pendingChain = Promise.resolve();
|
|
2047
|
+
_mutationDepth = 0;
|
|
2048
|
+
_destroyed = false;
|
|
2049
|
+
_autoPauseOnBlur = false;
|
|
2050
|
+
_isBlurred = false;
|
|
2051
|
+
_visibilityPausedScenes = /* @__PURE__ */ new Set();
|
|
2052
|
+
_visibilityListenerCleanup;
|
|
2053
|
+
/**
|
|
2054
|
+
* Pause all non-paused scenes when `document.hidden` becomes `true`; restore
|
|
2055
|
+
* them on focus. Default: `false`. Only scenes paused by this mechanism are
|
|
2056
|
+
* restored — user-paused scenes (manual `scene.paused = true` or `pauseBelow`
|
|
2057
|
+
* cascade) are never touched.
|
|
2058
|
+
*/
|
|
2059
|
+
get autoPauseOnBlur() {
|
|
2060
|
+
return this._autoPauseOnBlur;
|
|
2061
|
+
}
|
|
2062
|
+
set autoPauseOnBlur(value) {
|
|
2063
|
+
if (this._autoPauseOnBlur === value) return;
|
|
2064
|
+
this._autoPauseOnBlur = value;
|
|
2065
|
+
if (!this._isBlurred) return;
|
|
2066
|
+
if (value) {
|
|
2067
|
+
this._applyBlurPause();
|
|
2068
|
+
} else if (this._visibilityPausedScenes.size > 0) {
|
|
2069
|
+
this._restoreBlurPause();
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
1702
2072
|
/**
|
|
1703
2073
|
* Set the engine context.
|
|
1704
2074
|
* @internal
|
|
@@ -1707,6 +2077,42 @@ var SceneManager = class {
|
|
|
1707
2077
|
this._context = context;
|
|
1708
2078
|
this.bus = context.tryResolve(EventBusKey);
|
|
1709
2079
|
this.assetManager = context.tryResolve(AssetManagerKey);
|
|
2080
|
+
this.hookRegistry = context.tryResolve(SceneHookRegistryKey);
|
|
2081
|
+
this.logger = context.tryResolve(LoggerKey);
|
|
2082
|
+
if (this._visibilityListenerCleanup || typeof document === "undefined") {
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const onVisibilityChange = /* @__PURE__ */ __name(() => {
|
|
2086
|
+
this._handleVisibilityChange(document.hidden);
|
|
2087
|
+
}, "onVisibilityChange");
|
|
2088
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2089
|
+
this._visibilityListenerCleanup = () => document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* React to a visibility change. Parameterised on `hidden` so unit tests can
|
|
2093
|
+
* drive it without a real `document`.
|
|
2094
|
+
* @internal
|
|
2095
|
+
*/
|
|
2096
|
+
_handleVisibilityChange(hidden) {
|
|
2097
|
+
if (hidden && !this._isBlurred) {
|
|
2098
|
+
this._isBlurred = true;
|
|
2099
|
+
if (this._autoPauseOnBlur) this._applyBlurPause();
|
|
2100
|
+
} else if (!hidden && this._isBlurred) {
|
|
2101
|
+
this._isBlurred = false;
|
|
2102
|
+
if (this._visibilityPausedScenes.size > 0) this._restoreBlurPause();
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
_applyBlurPause() {
|
|
2106
|
+
for (const scene of this.activeScenes) {
|
|
2107
|
+
scene.paused = true;
|
|
2108
|
+
this._visibilityPausedScenes.add(scene);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
_restoreBlurPause() {
|
|
2112
|
+
for (const scene of this._visibilityPausedScenes) {
|
|
2113
|
+
scene.paused = false;
|
|
2114
|
+
}
|
|
2115
|
+
this._visibilityPausedScenes.clear();
|
|
1710
2116
|
}
|
|
1711
2117
|
/** The topmost (active) scene. */
|
|
1712
2118
|
get active() {
|
|
@@ -1718,84 +2124,132 @@ var SceneManager = class {
|
|
|
1718
2124
|
}
|
|
1719
2125
|
/** All non-paused scenes in the stack, bottom to top. */
|
|
1720
2126
|
get activeScenes() {
|
|
1721
|
-
return this.stack.filter((
|
|
2127
|
+
return this.stack.filter((scene) => !scene.isPaused);
|
|
2128
|
+
}
|
|
2129
|
+
/** Whether a scene transition is currently running. */
|
|
2130
|
+
get isTransitioning() {
|
|
2131
|
+
return this._currentRun !== void 0;
|
|
1722
2132
|
}
|
|
1723
2133
|
/**
|
|
1724
2134
|
* Push a scene onto the stack. Scenes below may receive onPause().
|
|
1725
2135
|
* If the scene declares a `preload` array, assets are loaded before onEnter().
|
|
1726
|
-
* Await the returned promise when using preloaded scenes.
|
|
1727
2136
|
*/
|
|
1728
|
-
push(scene) {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
scene.onEnter?.();
|
|
1740
|
-
this.bus?.emit("scene:pushed", { scene });
|
|
1741
|
-
return Promise.resolve();
|
|
2137
|
+
async push(scene, opts) {
|
|
2138
|
+
this._assertNotMutating("push");
|
|
2139
|
+
await this._enqueue(async () => {
|
|
2140
|
+
const fromScene = this.active;
|
|
2141
|
+
await this._pushScene(scene);
|
|
2142
|
+
const transition = resolveTransition(opts?.transition, scene);
|
|
2143
|
+
if (!transition) return;
|
|
2144
|
+
await this._runTransition("push", transition, fromScene, scene);
|
|
2145
|
+
});
|
|
1742
2146
|
}
|
|
1743
2147
|
/** Pop the top scene. Scenes below may receive onResume(). */
|
|
1744
|
-
pop() {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2148
|
+
async pop(opts) {
|
|
2149
|
+
this._assertNotMutating("pop");
|
|
2150
|
+
return this._enqueue(async () => {
|
|
2151
|
+
if (this.stack.length === 0) return void 0;
|
|
2152
|
+
const fromScene = this.active;
|
|
2153
|
+
const destination = this.stack.length > 1 ? this.stack[this.stack.length - 2] : void 0;
|
|
2154
|
+
const transition = resolveTransition(opts?.transition, destination);
|
|
2155
|
+
if (transition) {
|
|
2156
|
+
await this._runTransition("pop", transition, fromScene, destination);
|
|
2157
|
+
}
|
|
2158
|
+
return this._popScene();
|
|
2159
|
+
});
|
|
1753
2160
|
}
|
|
1754
2161
|
/**
|
|
1755
|
-
* Replace the top scene.
|
|
1756
|
-
*
|
|
2162
|
+
* Replace the top scene. Without a transition the old scene exits first,
|
|
2163
|
+
* then the new scene enters. With a transition the new scene is pushed
|
|
2164
|
+
* first, both scenes coexist for the transition duration, then the old
|
|
2165
|
+
* scene is removed at the end.
|
|
1757
2166
|
*/
|
|
1758
|
-
replace(scene) {
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2167
|
+
async replace(scene, opts) {
|
|
2168
|
+
this._assertNotMutating("replace");
|
|
2169
|
+
await this._enqueue(async () => {
|
|
2170
|
+
const transition = resolveTransition(opts?.transition, scene);
|
|
2171
|
+
if (!transition) {
|
|
2172
|
+
await this._replaceScene(scene);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const old = this.active;
|
|
2176
|
+
await this._pushScene(scene, true);
|
|
2177
|
+
await this._runTransition("replace", transition, old, scene);
|
|
2178
|
+
if (old) {
|
|
2179
|
+
this._removeScene(old, true);
|
|
2180
|
+
}
|
|
2181
|
+
this.bus?.emit("scene:replaced", {
|
|
2182
|
+
oldScene: old ?? scene,
|
|
2183
|
+
newScene: scene
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Pop every scene on the stack, top to bottom. Each receives onExit().
|
|
2189
|
+
* Queued like push/pop/replace — runs after any in-flight transition.
|
|
2190
|
+
* Use for "restart from menu"-style flows. Does not run transitions.
|
|
2191
|
+
*/
|
|
2192
|
+
async popAll() {
|
|
2193
|
+
this._assertNotMutating("popAll");
|
|
2194
|
+
await this._enqueue(async () => {
|
|
2195
|
+
this._withMutationSync(() => {
|
|
2196
|
+
while (this.stack.length > 0) {
|
|
2197
|
+
const scene = this.stack.pop();
|
|
2198
|
+
if (!scene) break;
|
|
2199
|
+
this._teardownScene(scene);
|
|
2200
|
+
this.bus?.emit("scene:popped", { scene });
|
|
1779
2201
|
}
|
|
1780
2202
|
});
|
|
1781
|
-
}
|
|
1782
|
-
scene.onEnter?.();
|
|
1783
|
-
if (old) {
|
|
1784
|
-
this.bus?.emit("scene:replaced", { oldScene: old, newScene: scene });
|
|
1785
|
-
} else {
|
|
1786
|
-
this.bus?.emit("scene:pushed", { scene });
|
|
1787
|
-
}
|
|
1788
|
-
return Promise.resolve();
|
|
2203
|
+
});
|
|
1789
2204
|
}
|
|
1790
|
-
/**
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2205
|
+
/**
|
|
2206
|
+
* Run the full scene-enter lifecycle (beforeEnter hooks, preload, onEnter)
|
|
2207
|
+
* for a scene that is NOT placed on the stack. Used by infrastructure
|
|
2208
|
+
* plugins like DebugPlugin that render a scene off-stack.
|
|
2209
|
+
* @internal
|
|
2210
|
+
*/
|
|
2211
|
+
async _mountDetached(scene) {
|
|
2212
|
+
await this._withMutation(async () => {
|
|
2213
|
+
scene._setContext(this._context);
|
|
2214
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2215
|
+
await this._preloadScene(scene);
|
|
2216
|
+
scene.onEnter?.();
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Run the scene-exit lifecycle (onExit, entity destruction, afterExit
|
|
2221
|
+
* hooks, scoped-service clear) for a detached scene.
|
|
2222
|
+
* @internal
|
|
2223
|
+
*/
|
|
2224
|
+
_unmountDetached(scene) {
|
|
2225
|
+
this._withMutationSync(() => {
|
|
2226
|
+
this._teardownScene(scene);
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Mark the manager destroyed and synchronously tear down every scene.
|
|
2231
|
+
* Called by Engine.destroy(). Any queued async work short-circuits on
|
|
2232
|
+
* resume; in-flight transitions' pending promises are resolved via
|
|
2233
|
+
* _cleanupRun so they don't leak.
|
|
2234
|
+
* @internal
|
|
2235
|
+
*/
|
|
2236
|
+
_destroy() {
|
|
2237
|
+
this._destroyed = true;
|
|
2238
|
+
if (this._currentRun) {
|
|
2239
|
+
this._cleanupRun(this._currentRun);
|
|
2240
|
+
}
|
|
2241
|
+
this._pendingChain = Promise.resolve();
|
|
2242
|
+
this._visibilityListenerCleanup?.();
|
|
2243
|
+
this._visibilityListenerCleanup = void 0;
|
|
2244
|
+
this._visibilityPausedScenes.clear();
|
|
2245
|
+
this._withMutationSync(() => {
|
|
2246
|
+
while (this.stack.length > 0) {
|
|
2247
|
+
const scene = this.stack.pop();
|
|
2248
|
+
if (!scene) break;
|
|
2249
|
+
this._teardownScene(scene);
|
|
2250
|
+
this.bus?.emit("scene:popped", { scene });
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
1799
2253
|
}
|
|
1800
2254
|
/**
|
|
1801
2255
|
* Flush destroy queues for all active scenes.
|
|
@@ -1807,21 +2261,217 @@ var SceneManager = class {
|
|
|
1807
2261
|
scene._flushDestroyQueue();
|
|
1808
2262
|
}
|
|
1809
2263
|
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Advance the active transition by `dt` ms. Called by Engine's earlyUpdate
|
|
2266
|
+
* callback with raw (unscaled) wall-clock dt.
|
|
2267
|
+
* @internal
|
|
2268
|
+
*/
|
|
2269
|
+
_tickTransition(dt) {
|
|
2270
|
+
const run = this._currentRun;
|
|
2271
|
+
if (!run) return;
|
|
2272
|
+
const remaining = run.transition.duration - run.elapsed;
|
|
2273
|
+
const consume = Math.min(dt, remaining);
|
|
2274
|
+
run.elapsed += consume;
|
|
2275
|
+
this._safeTick(run, consume);
|
|
2276
|
+
if (run.elapsed >= run.transition.duration) {
|
|
2277
|
+
this._cleanupRun(run);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
// ---- Private helpers ----
|
|
2281
|
+
_enqueue(work) {
|
|
2282
|
+
if (this._destroyed) return Promise.resolve(void 0);
|
|
2283
|
+
const next = this._pendingChain.then(async () => {
|
|
2284
|
+
if (this._destroyed) return void 0;
|
|
2285
|
+
return work();
|
|
2286
|
+
});
|
|
2287
|
+
this._pendingChain = next.then(
|
|
2288
|
+
() => void 0,
|
|
2289
|
+
() => void 0
|
|
2290
|
+
);
|
|
2291
|
+
return next;
|
|
2292
|
+
}
|
|
2293
|
+
async _pushScene(scene, suppressEvent = false) {
|
|
2294
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2295
|
+
await this._withMutation(async () => {
|
|
2296
|
+
scene._setContext(this._context);
|
|
2297
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2298
|
+
await this._preloadScene(scene);
|
|
2299
|
+
this.stack.push(scene);
|
|
2300
|
+
scene.onEnter?.();
|
|
2301
|
+
this._firePauseTransitions(wasPaused);
|
|
2302
|
+
if (!suppressEvent) {
|
|
2303
|
+
this.bus?.emit("scene:pushed", { scene });
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
_popScene(suppressEvent = false) {
|
|
2308
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2309
|
+
return this._withMutationSync(() => {
|
|
2310
|
+
const removed = this.stack.pop();
|
|
2311
|
+
if (!removed) return void 0;
|
|
2312
|
+
this._teardownScene(removed);
|
|
2313
|
+
this._fireResumeTransitions(wasPaused);
|
|
2314
|
+
if (!suppressEvent) {
|
|
2315
|
+
this.bus?.emit("scene:popped", { scene: removed });
|
|
2316
|
+
}
|
|
2317
|
+
return removed;
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
async _replaceScene(scene) {
|
|
2321
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2322
|
+
await this._withMutation(async () => {
|
|
2323
|
+
scene._setContext(this._context);
|
|
2324
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2325
|
+
await this._preloadScene(scene);
|
|
2326
|
+
const old = this.stack.pop();
|
|
2327
|
+
if (old) this._teardownScene(old);
|
|
2328
|
+
this.stack.push(scene);
|
|
2329
|
+
scene.onEnter?.();
|
|
2330
|
+
this._firePauseTransitions(wasPaused);
|
|
2331
|
+
this._fireResumeTransitions(wasPaused);
|
|
2332
|
+
this.bus?.emit("scene:replaced", {
|
|
2333
|
+
oldScene: old ?? scene,
|
|
2334
|
+
newScene: scene
|
|
2335
|
+
});
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
_removeScene(scene, suppressEvent = false) {
|
|
2339
|
+
this._withMutationSync(() => {
|
|
2340
|
+
const idx = this.stack.indexOf(scene);
|
|
2341
|
+
if (idx === -1) return;
|
|
2342
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2343
|
+
this.stack.splice(idx, 1);
|
|
2344
|
+
this._teardownScene(scene);
|
|
2345
|
+
this._firePauseTransitions(wasPaused);
|
|
2346
|
+
this._fireResumeTransitions(wasPaused);
|
|
2347
|
+
if (!suppressEvent) {
|
|
2348
|
+
this.bus?.emit("scene:popped", { scene });
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
async _preloadScene(scene) {
|
|
2353
|
+
if (!scene.preload?.length || !this.assetManager) return;
|
|
2354
|
+
await this.assetManager.loadAll(
|
|
2355
|
+
scene.preload,
|
|
2356
|
+
scene.onProgress?.bind(scene)
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
_teardownScene(scene) {
|
|
2360
|
+
scene.onExit?.();
|
|
2361
|
+
scene._destroyAllEntities();
|
|
2362
|
+
this.hookRegistry?.runAfterExit(scene);
|
|
2363
|
+
scene._clearScopedServices();
|
|
2364
|
+
this._visibilityPausedScenes.delete(scene);
|
|
2365
|
+
}
|
|
2366
|
+
async _runTransition(kind, transition, fromScene, toScene) {
|
|
2367
|
+
if (this._destroyed) return;
|
|
2368
|
+
let resolveRun;
|
|
2369
|
+
const promise = new Promise((resolve) => {
|
|
2370
|
+
resolveRun = resolve;
|
|
2371
|
+
});
|
|
2372
|
+
const run = {
|
|
2373
|
+
kind,
|
|
2374
|
+
transition,
|
|
2375
|
+
elapsed: 0,
|
|
2376
|
+
fromScene,
|
|
2377
|
+
toScene,
|
|
2378
|
+
resolve: resolveRun
|
|
2379
|
+
};
|
|
2380
|
+
this._currentRun = run;
|
|
2381
|
+
this.bus?.emit("scene:transition:started", {
|
|
2382
|
+
kind,
|
|
2383
|
+
fromScene,
|
|
2384
|
+
toScene
|
|
2385
|
+
});
|
|
2386
|
+
this._safeCall(run, "begin");
|
|
2387
|
+
if (!Number.isFinite(transition.duration) || transition.duration <= 0) {
|
|
2388
|
+
this._cleanupRun(run);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
await promise;
|
|
2392
|
+
}
|
|
2393
|
+
_cleanupRun(run) {
|
|
2394
|
+
if (this._currentRun !== run) return;
|
|
2395
|
+
this._safeCall(run, "end");
|
|
2396
|
+
this._currentRun = void 0;
|
|
2397
|
+
this.bus?.emit("scene:transition:ended", {
|
|
2398
|
+
kind: run.kind,
|
|
2399
|
+
fromScene: run.fromScene,
|
|
2400
|
+
toScene: run.toScene
|
|
2401
|
+
});
|
|
2402
|
+
run.resolve();
|
|
2403
|
+
}
|
|
2404
|
+
_safeTick(run, dt) {
|
|
2405
|
+
try {
|
|
2406
|
+
run.transition.tick(dt, this._makeContext(run));
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
this.logger?.warn(
|
|
2409
|
+
"SceneManager",
|
|
2410
|
+
`Transition tick error: ${err instanceof Error ? err.message : String(err)}`
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
_safeCall(run, method) {
|
|
2415
|
+
try {
|
|
2416
|
+
run.transition[method]?.(this._makeContext(run));
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
this.logger?.warn(
|
|
2419
|
+
"SceneManager",
|
|
2420
|
+
`Transition ${method} error: ${err instanceof Error ? err.message : String(err)}`
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
_makeContext(run) {
|
|
2425
|
+
return {
|
|
2426
|
+
elapsed: run.elapsed,
|
|
2427
|
+
kind: run.kind,
|
|
2428
|
+
engineContext: this._context,
|
|
2429
|
+
fromScene: run.fromScene,
|
|
2430
|
+
toScene: run.toScene
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
_snapshotPauseStates() {
|
|
2434
|
+
return new Map(
|
|
2435
|
+
this.stack.map((scene) => [scene, scene.isPaused])
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
_assertNotMutating(method) {
|
|
2439
|
+
if (this._mutationDepth === 0) return;
|
|
2440
|
+
throw new Error(
|
|
2441
|
+
`SceneManager.${method}() called reentrantly from a scene lifecycle hook (onEnter/onExit/onPause/onResume or a beforeEnter/afterExit hook). Defer the call outside the hook, e.g. via queueMicrotask() or from a component update().`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
async _withMutation(work) {
|
|
2445
|
+
this._mutationDepth++;
|
|
2446
|
+
try {
|
|
2447
|
+
return await work();
|
|
2448
|
+
} finally {
|
|
2449
|
+
this._mutationDepth--;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
_withMutationSync(work) {
|
|
2453
|
+
this._mutationDepth++;
|
|
2454
|
+
try {
|
|
2455
|
+
return work();
|
|
2456
|
+
} finally {
|
|
2457
|
+
this._mutationDepth--;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
1810
2460
|
/** Fire onPause() for scenes that transitioned from not-paused to paused. */
|
|
1811
2461
|
_firePauseTransitions(wasPaused) {
|
|
1812
|
-
for (const
|
|
1813
|
-
const was = wasPaused.get(
|
|
1814
|
-
if (
|
|
1815
|
-
|
|
2462
|
+
for (const scene of this.stack) {
|
|
2463
|
+
const was = wasPaused.get(scene) ?? false;
|
|
2464
|
+
if (scene.isPaused && !was) {
|
|
2465
|
+
scene.onPause?.();
|
|
1816
2466
|
}
|
|
1817
2467
|
}
|
|
1818
2468
|
}
|
|
1819
2469
|
/** Fire onResume() for scenes that transitioned from paused to not-paused. */
|
|
1820
2470
|
_fireResumeTransitions(wasPaused) {
|
|
1821
|
-
for (const
|
|
1822
|
-
const was = wasPaused.get(
|
|
1823
|
-
if (!
|
|
1824
|
-
|
|
2471
|
+
for (const scene of this.stack) {
|
|
2472
|
+
const was = wasPaused.get(scene) ?? false;
|
|
2473
|
+
if (!scene.isPaused && was) {
|
|
2474
|
+
scene.onResume?.();
|
|
1825
2475
|
}
|
|
1826
2476
|
}
|
|
1827
2477
|
}
|
|
@@ -2658,6 +3308,7 @@ var Engine = class {
|
|
|
2658
3308
|
scheduler;
|
|
2659
3309
|
errorBoundary;
|
|
2660
3310
|
queryCache;
|
|
3311
|
+
sceneHooks;
|
|
2661
3312
|
/** The asset manager. */
|
|
2662
3313
|
assets;
|
|
2663
3314
|
plugins = /* @__PURE__ */ new Map();
|
|
@@ -2676,6 +3327,7 @@ var Engine = class {
|
|
|
2676
3327
|
this.scheduler = new SystemScheduler();
|
|
2677
3328
|
this.inspector = new Inspector(this);
|
|
2678
3329
|
this.assets = new AssetManager();
|
|
3330
|
+
this.sceneHooks = new SceneHookRegistry();
|
|
2679
3331
|
this.scheduler.setErrorBoundary(this.errorBoundary);
|
|
2680
3332
|
this.context.register(EngineKey, this);
|
|
2681
3333
|
this.context.register(EventBusKey, this.events);
|
|
@@ -2687,11 +3339,13 @@ var Engine = class {
|
|
|
2687
3339
|
this.context.register(InspectorKey, this.inspector);
|
|
2688
3340
|
this.context.register(SystemSchedulerKey, this.scheduler);
|
|
2689
3341
|
this.context.register(AssetManagerKey, this.assets);
|
|
3342
|
+
this.context.register(SceneHookRegistryKey, this.sceneHooks);
|
|
2690
3343
|
this.scenes._setContext(this.context);
|
|
2691
3344
|
this.registerBuiltInSystems();
|
|
2692
3345
|
this.loop.setCallbacks({
|
|
2693
3346
|
earlyUpdate: /* @__PURE__ */ __name((dt) => {
|
|
2694
3347
|
this.logger.setFrame(this.loop.frameCount);
|
|
3348
|
+
this.scenes._tickTransition(dt);
|
|
2695
3349
|
this.scheduler.run("earlyUpdate" /* EarlyUpdate */, dt);
|
|
2696
3350
|
}, "earlyUpdate"),
|
|
2697
3351
|
fixedUpdate: /* @__PURE__ */ __name((dt) => this.scheduler.run("fixedUpdate" /* FixedUpdate */, dt), "fixedUpdate"),
|
|
@@ -2704,6 +3358,14 @@ var Engine = class {
|
|
|
2704
3358
|
}, "endOfFrame")
|
|
2705
3359
|
});
|
|
2706
3360
|
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Register scene lifecycle hooks. The returned function unregisters the
|
|
3363
|
+
* hooks. Infrastructure plugins (renderer, physics, debug) register hooks
|
|
3364
|
+
* in their `install` or `onStart` to set up and tear down per-scene state.
|
|
3365
|
+
*/
|
|
3366
|
+
registerSceneHooks(hooks) {
|
|
3367
|
+
return this.sceneHooks.register(hooks);
|
|
3368
|
+
}
|
|
2707
3369
|
/** Register a plugin. Must be called before start(). */
|
|
2708
3370
|
use(plugin) {
|
|
2709
3371
|
if (this.started) {
|
|
@@ -2739,7 +3401,7 @@ var Engine = class {
|
|
|
2739
3401
|
};
|
|
2740
3402
|
}
|
|
2741
3403
|
for (const plugin of sorted) {
|
|
2742
|
-
plugin.onStart?.();
|
|
3404
|
+
await plugin.onStart?.();
|
|
2743
3405
|
}
|
|
2744
3406
|
this.events.emit("engine:started", void 0);
|
|
2745
3407
|
}
|
|
@@ -2747,7 +3409,7 @@ var Engine = class {
|
|
|
2747
3409
|
destroy() {
|
|
2748
3410
|
this.events.emit("engine:stopped", void 0);
|
|
2749
3411
|
this.loop.stop();
|
|
2750
|
-
this.scenes.
|
|
3412
|
+
this.scenes._destroy();
|
|
2751
3413
|
const allSystems = this.scheduler.getAllSystems();
|
|
2752
3414
|
for (let i = allSystems.length - 1; i >= 0; i--) {
|
|
2753
3415
|
allSystems[i].onUnregister?.();
|
|
@@ -2821,6 +3483,11 @@ var Engine = class {
|
|
|
2821
3483
|
}
|
|
2822
3484
|
};
|
|
2823
3485
|
|
|
3486
|
+
// src/RendererAdapter.ts
|
|
3487
|
+
var RendererAdapterKey = new ServiceKey(
|
|
3488
|
+
"rendererAdapter"
|
|
3489
|
+
);
|
|
3490
|
+
|
|
2824
3491
|
// src/test-utils.ts
|
|
2825
3492
|
var _TestScene = class extends Scene {
|
|
2826
3493
|
static {
|
|
@@ -2890,6 +3557,7 @@ export {
|
|
|
2890
3557
|
Inspector,
|
|
2891
3558
|
InspectorKey,
|
|
2892
3559
|
KeyframeAnimator,
|
|
3560
|
+
LoadingScene,
|
|
2893
3561
|
LogLevel,
|
|
2894
3562
|
Logger,
|
|
2895
3563
|
LoggerKey,
|
|
@@ -2903,8 +3571,11 @@ export {
|
|
|
2903
3571
|
QueryCache,
|
|
2904
3572
|
QueryCacheKey,
|
|
2905
3573
|
QueryResult,
|
|
3574
|
+
RendererAdapterKey,
|
|
2906
3575
|
SERIALIZABLE_KEY,
|
|
2907
3576
|
Scene,
|
|
3577
|
+
SceneHookRegistry,
|
|
3578
|
+
SceneHookRegistryKey,
|
|
2908
3579
|
SceneManager,
|
|
2909
3580
|
SceneManagerKey,
|
|
2910
3581
|
Sequence,
|
|
@@ -2937,6 +3608,7 @@ export {
|
|
|
2937
3608
|
getSerializableType,
|
|
2938
3609
|
interpolate,
|
|
2939
3610
|
isSerializable,
|
|
3611
|
+
resolveTransition,
|
|
2940
3612
|
serializable,
|
|
2941
3613
|
trait
|
|
2942
3614
|
};
|