@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.cjs
CHANGED
|
@@ -85,6 +85,7 @@ __export(index_exports, {
|
|
|
85
85
|
Inspector: () => Inspector,
|
|
86
86
|
InspectorKey: () => InspectorKey,
|
|
87
87
|
KeyframeAnimator: () => KeyframeAnimator,
|
|
88
|
+
LoadingScene: () => LoadingScene,
|
|
88
89
|
LogLevel: () => LogLevel,
|
|
89
90
|
Logger: () => Logger,
|
|
90
91
|
LoggerKey: () => LoggerKey,
|
|
@@ -98,8 +99,11 @@ __export(index_exports, {
|
|
|
98
99
|
QueryCache: () => QueryCache,
|
|
99
100
|
QueryCacheKey: () => QueryCacheKey,
|
|
100
101
|
QueryResult: () => QueryResult,
|
|
102
|
+
RendererAdapterKey: () => RendererAdapterKey,
|
|
101
103
|
SERIALIZABLE_KEY: () => SERIALIZABLE_KEY,
|
|
102
104
|
Scene: () => Scene,
|
|
105
|
+
SceneHookRegistry: () => SceneHookRegistry,
|
|
106
|
+
SceneHookRegistryKey: () => SceneHookRegistryKey,
|
|
103
107
|
SceneManager: () => SceneManager,
|
|
104
108
|
SceneManagerKey: () => SceneManagerKey,
|
|
105
109
|
Sequence: () => Sequence,
|
|
@@ -132,6 +136,7 @@ __export(index_exports, {
|
|
|
132
136
|
getSerializableType: () => getSerializableType,
|
|
133
137
|
interpolate: () => interpolate,
|
|
134
138
|
isSerializable: () => isSerializable,
|
|
139
|
+
resolveTransition: () => resolveTransition,
|
|
135
140
|
serializable: () => serializable,
|
|
136
141
|
trait: () => trait
|
|
137
142
|
});
|
|
@@ -261,14 +266,52 @@ var Vec2 = class _Vec2 {
|
|
|
261
266
|
static lerp(a, b, t) {
|
|
262
267
|
return new _Vec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
|
263
268
|
}
|
|
269
|
+
/** Move current toward target by at most maxDelta without overshooting. */
|
|
270
|
+
static moveTowards(current, target, maxDelta) {
|
|
271
|
+
const dx = target.x - current.x;
|
|
272
|
+
const dy = target.y - current.y;
|
|
273
|
+
const distanceSq = dx * dx + dy * dy;
|
|
274
|
+
if (distanceSq < EPSILON * EPSILON) {
|
|
275
|
+
return new _Vec2(target.x, target.y);
|
|
276
|
+
}
|
|
277
|
+
if (maxDelta <= 0) {
|
|
278
|
+
return new _Vec2(current.x, current.y);
|
|
279
|
+
}
|
|
280
|
+
const distance = Math.sqrt(distanceSq);
|
|
281
|
+
if (distance <= maxDelta) {
|
|
282
|
+
return new _Vec2(target.x, target.y);
|
|
283
|
+
}
|
|
284
|
+
const scale = maxDelta / distance;
|
|
285
|
+
return new _Vec2(current.x + dx * scale, current.y + dy * scale);
|
|
286
|
+
}
|
|
264
287
|
};
|
|
265
288
|
|
|
266
289
|
// src/MathUtils.ts
|
|
290
|
+
var TAU = Math.PI * 2;
|
|
291
|
+
var MIN_SMOOTH_TIME = 1e-4;
|
|
292
|
+
function normalizeAngle(radians) {
|
|
293
|
+
const wrapped = ((radians + Math.PI) % TAU + TAU) % TAU - Math.PI;
|
|
294
|
+
return wrapped === -Math.PI && radians > 0 ? Math.PI : wrapped;
|
|
295
|
+
}
|
|
296
|
+
__name(normalizeAngle, "normalizeAngle");
|
|
267
297
|
var MathUtils = {
|
|
268
298
|
/** Linear interpolation between a and b. */
|
|
269
299
|
lerp(a, b, t) {
|
|
270
300
|
return a + (b - a) * t;
|
|
271
301
|
},
|
|
302
|
+
/** Return the clamped interpolation factor that produces v between a and b. */
|
|
303
|
+
inverseLerp(a, b, v) {
|
|
304
|
+
if (a === b) return 0;
|
|
305
|
+
return MathUtils.clamp((v - a) / (b - a), 0, 1);
|
|
306
|
+
},
|
|
307
|
+
/** Interpolate between angles in radians along the shortest path. */
|
|
308
|
+
lerpAngle(a, b, t) {
|
|
309
|
+
return normalizeAngle(a + MathUtils.shortestAngleBetween(a, b) * t);
|
|
310
|
+
},
|
|
311
|
+
/** Signed shortest angular delta from a to b, in radians. */
|
|
312
|
+
shortestAngleBetween(a, b) {
|
|
313
|
+
return normalizeAngle(b - a);
|
|
314
|
+
},
|
|
272
315
|
/** Clamp a value between min and max. */
|
|
273
316
|
clamp(value, min, max) {
|
|
274
317
|
return Math.max(min, Math.min(max, value));
|
|
@@ -278,6 +321,12 @@ var MathUtils = {
|
|
|
278
321
|
const t = (value - inMin) / (inMax - inMin);
|
|
279
322
|
return outMin + (outMax - outMin) * t;
|
|
280
323
|
},
|
|
324
|
+
/** Bounce t between 0 and length. */
|
|
325
|
+
pingPong(t, length) {
|
|
326
|
+
if (length <= 0) return 0;
|
|
327
|
+
const wrapped = MathUtils.wrap(t, 0, length * 2);
|
|
328
|
+
return length - Math.abs(wrapped - length);
|
|
329
|
+
},
|
|
281
330
|
/** Random float in [min, max). */
|
|
282
331
|
randomRange(min, max) {
|
|
283
332
|
return min + Math.random() * (max - min);
|
|
@@ -301,6 +350,34 @@ var MathUtils = {
|
|
|
301
350
|
}
|
|
302
351
|
return Math.max(current - step, target);
|
|
303
352
|
},
|
|
353
|
+
/**
|
|
354
|
+
* Smoothly damp current toward target without overshooting.
|
|
355
|
+
* Pass the returned velocity back into the next call.
|
|
356
|
+
*/
|
|
357
|
+
smoothDamp(current, target, velocity, smoothTime, deltaTime, maxSpeed = Infinity) {
|
|
358
|
+
if (deltaTime <= 0) {
|
|
359
|
+
return { value: current, velocity };
|
|
360
|
+
}
|
|
361
|
+
const safeSmoothTime = Math.max(MIN_SMOOTH_TIME, smoothTime);
|
|
362
|
+
const omega = 2 / safeSmoothTime;
|
|
363
|
+
const x = omega * deltaTime;
|
|
364
|
+
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
|
|
365
|
+
const originalTarget = target;
|
|
366
|
+
const maxChange = maxSpeed * safeSmoothTime;
|
|
367
|
+
const change = MathUtils.clamp(current - target, -maxChange, maxChange);
|
|
368
|
+
const adjustedTarget = current - change;
|
|
369
|
+
const temp = (velocity + omega * change) * deltaTime;
|
|
370
|
+
const nextVelocity = (velocity - omega * temp) * exp;
|
|
371
|
+
let value = adjustedTarget + (change + temp) * exp;
|
|
372
|
+
let resultVelocity = nextVelocity;
|
|
373
|
+
const targetIsAboveCurrent = originalTarget - current > 0;
|
|
374
|
+
const valuePassedTarget = targetIsAboveCurrent ? value > originalTarget : value < originalTarget;
|
|
375
|
+
if (valuePassedTarget) {
|
|
376
|
+
value = originalTarget;
|
|
377
|
+
resultVelocity = 0;
|
|
378
|
+
}
|
|
379
|
+
return { value, velocity: resultVelocity };
|
|
380
|
+
},
|
|
304
381
|
/** Wrap value into the range [min, max). */
|
|
305
382
|
wrap(value, min, max) {
|
|
306
383
|
const range = max - min;
|
|
@@ -458,13 +535,16 @@ var Logger = class {
|
|
|
458
535
|
|
|
459
536
|
// src/EngineContext.ts
|
|
460
537
|
var ServiceKey = class {
|
|
461
|
-
constructor(id) {
|
|
538
|
+
constructor(id, options) {
|
|
462
539
|
this.id = id;
|
|
540
|
+
this.scope = options?.scope ?? "engine";
|
|
463
541
|
}
|
|
464
542
|
id;
|
|
465
543
|
static {
|
|
466
544
|
__name(this, "ServiceKey");
|
|
467
545
|
}
|
|
546
|
+
/** Declared scope (engine or scene). Defaults to `"engine"`. */
|
|
547
|
+
scope;
|
|
468
548
|
};
|
|
469
549
|
var EngineContext = class {
|
|
470
550
|
static {
|
|
@@ -510,6 +590,50 @@ var SystemSchedulerKey = new ServiceKey("systemScheduler");
|
|
|
510
590
|
var ProcessSystemKey = new ServiceKey("processSystem");
|
|
511
591
|
var AssetManagerKey = new ServiceKey("assetManager");
|
|
512
592
|
|
|
593
|
+
// src/SceneHooks.ts
|
|
594
|
+
var SceneHookRegistry = class {
|
|
595
|
+
static {
|
|
596
|
+
__name(this, "SceneHookRegistry");
|
|
597
|
+
}
|
|
598
|
+
hooks = [];
|
|
599
|
+
register(hooks) {
|
|
600
|
+
this.hooks.push(hooks);
|
|
601
|
+
return () => {
|
|
602
|
+
const idx = this.hooks.indexOf(hooks);
|
|
603
|
+
if (idx !== -1) this.hooks.splice(idx, 1);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** Run all `beforeEnter` hooks serially. */
|
|
607
|
+
async runBeforeEnter(scene) {
|
|
608
|
+
for (const h of this.hooks) {
|
|
609
|
+
await h.beforeEnter?.(scene);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
runAfterExit(scene) {
|
|
613
|
+
for (const h of this.hooks) {
|
|
614
|
+
try {
|
|
615
|
+
h.afterExit?.(scene);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
const logger = scene.context.tryResolve(LoggerKey);
|
|
618
|
+
if (logger) {
|
|
619
|
+
logger.error("core", "Scene afterExit hook threw", {
|
|
620
|
+
scene: scene.name,
|
|
621
|
+
error: err
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
console.error(
|
|
625
|
+
`[yage] Scene afterExit hook threw for scene "${scene.name}":`,
|
|
626
|
+
err
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
var SceneHookRegistryKey = new ServiceKey(
|
|
634
|
+
"sceneHookRegistry"
|
|
635
|
+
);
|
|
636
|
+
|
|
513
637
|
// src/EventToken.ts
|
|
514
638
|
var EventToken = class {
|
|
515
639
|
constructor(name) {
|
|
@@ -731,10 +855,11 @@ var Component = class {
|
|
|
731
855
|
_cleanups;
|
|
732
856
|
/**
|
|
733
857
|
* Access the entity's scene. Throws if the entity is not in a scene.
|
|
734
|
-
* Prefer this over `this.entity.scene
|
|
858
|
+
* Prefer this over threading through `this.entity.scene` in component
|
|
859
|
+
* code.
|
|
735
860
|
*/
|
|
736
861
|
get scene() {
|
|
737
|
-
const scene = this.entity.
|
|
862
|
+
const scene = this.entity.tryScene;
|
|
738
863
|
if (!scene) {
|
|
739
864
|
throw new Error(
|
|
740
865
|
"Cannot access scene: entity is not attached to a scene."
|
|
@@ -749,20 +874,43 @@ var Component = class {
|
|
|
749
874
|
get context() {
|
|
750
875
|
return this.scene.context;
|
|
751
876
|
}
|
|
752
|
-
/**
|
|
877
|
+
/**
|
|
878
|
+
* Resolve a service by key, cached after first lookup. Scene-scoped values
|
|
879
|
+
* (registered via `scene._registerScoped`) take precedence over engine
|
|
880
|
+
* scope. A key declared with `scope: "scene"` that falls back to engine
|
|
881
|
+
* scope emits a one-shot dev warning — almost always signals a missed
|
|
882
|
+
* `beforeEnter` hook.
|
|
883
|
+
*/
|
|
753
884
|
use(key) {
|
|
754
885
|
this._serviceCache ??= /* @__PURE__ */ new Map();
|
|
755
|
-
|
|
756
|
-
if (
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
886
|
+
const cached = this._serviceCache.get(key.id);
|
|
887
|
+
if (cached !== void 0) return cached;
|
|
888
|
+
const scene = this.entity.tryScene;
|
|
889
|
+
const scoped = scene?._resolveScoped(key);
|
|
890
|
+
if (scoped !== void 0) {
|
|
891
|
+
this._serviceCache.set(key.id, scoped);
|
|
892
|
+
return scoped;
|
|
893
|
+
}
|
|
894
|
+
const value = this.context.resolve(key);
|
|
895
|
+
if (key.scope === "scene") {
|
|
896
|
+
this._warnScopedFallback(key);
|
|
897
|
+
return value;
|
|
898
|
+
}
|
|
899
|
+
this._serviceCache.set(key.id, value);
|
|
760
900
|
return value;
|
|
761
901
|
}
|
|
902
|
+
_warnScopedFallback(key) {
|
|
903
|
+
const logger = this.context.tryResolve(LoggerKey);
|
|
904
|
+
logger?.warn(
|
|
905
|
+
"core",
|
|
906
|
+
`Scoped key "${key.id}" fell back to engine scope \u2014 did a plugin forget to register a beforeEnter hook?`,
|
|
907
|
+
{ component: this.constructor.name }
|
|
908
|
+
);
|
|
909
|
+
}
|
|
762
910
|
/**
|
|
763
911
|
* Lazy proxy-based service resolution. Can be used at field-declaration time:
|
|
764
912
|
* ```ts
|
|
765
|
-
* readonly
|
|
913
|
+
* readonly input = this.service(InputManagerKey);
|
|
766
914
|
* ```
|
|
767
915
|
* The actual resolution is deferred until first property access.
|
|
768
916
|
*/
|
|
@@ -1019,8 +1167,27 @@ var Entity = class {
|
|
|
1019
1167
|
this.name = name ?? new.target.name ?? "Entity";
|
|
1020
1168
|
this.tags = new Set(tags);
|
|
1021
1169
|
}
|
|
1022
|
-
/**
|
|
1170
|
+
/**
|
|
1171
|
+
* The scene this entity belongs to. Throws if the entity is not attached
|
|
1172
|
+
* to a scene — which in practice only happens before `scene.spawn` /
|
|
1173
|
+
* `addChild` wires it up, or after `destroy()` tears it down. Inside
|
|
1174
|
+
* lifecycle methods (`setup`, component `onAdd`, `update`, etc.) this is
|
|
1175
|
+
* always safe to access.
|
|
1176
|
+
*
|
|
1177
|
+
* For the rare case where you genuinely need to inspect whether an
|
|
1178
|
+
* entity has a scene (e.g. defensive code in systems iterating a query
|
|
1179
|
+
* result), use `tryScene` instead.
|
|
1180
|
+
*/
|
|
1023
1181
|
get scene() {
|
|
1182
|
+
if (!this._scene) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Entity "${this.name}" is not attached to a scene. Use \`tryScene\` if you need to check.`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return this._scene;
|
|
1188
|
+
}
|
|
1189
|
+
/** The scene this entity belongs to, or `null` if detached. */
|
|
1190
|
+
get tryScene() {
|
|
1024
1191
|
return this._scene;
|
|
1025
1192
|
}
|
|
1026
1193
|
/** True if destroy() has been called. */
|
|
@@ -1058,6 +1225,20 @@ var Entity = class {
|
|
|
1058
1225
|
this._scene._addExistingEntity(child);
|
|
1059
1226
|
}
|
|
1060
1227
|
}
|
|
1228
|
+
spawnChild(name, classOrBlueprint, params) {
|
|
1229
|
+
const scene = this.scene;
|
|
1230
|
+
if (this._children?.has(name)) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`Entity "${this.name}" already has a child named "${name}".`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
const child = classOrBlueprint === void 0 ? scene.spawn(name) : scene.spawn(
|
|
1236
|
+
classOrBlueprint,
|
|
1237
|
+
params
|
|
1238
|
+
);
|
|
1239
|
+
this.addChild(name, child);
|
|
1240
|
+
return child;
|
|
1241
|
+
}
|
|
1061
1242
|
/** Remove a named child. Returns the detached entity. */
|
|
1062
1243
|
removeChild(name) {
|
|
1063
1244
|
const child = this._children?.get(name);
|
|
@@ -1550,7 +1731,7 @@ var GameLoop = class {
|
|
|
1550
1731
|
}
|
|
1551
1732
|
/** Process one frame with the given dt in milliseconds. */
|
|
1552
1733
|
tick(dtMs) {
|
|
1553
|
-
if (!this.callbacks) return;
|
|
1734
|
+
if (!this.running || !this.callbacks) return;
|
|
1554
1735
|
this._frameCount++;
|
|
1555
1736
|
this.callbacks.earlyUpdate(dtMs);
|
|
1556
1737
|
this.accumulator += dtMs;
|
|
@@ -1578,6 +1759,8 @@ var Scene = class {
|
|
|
1578
1759
|
transparentBelow = false;
|
|
1579
1760
|
/** Asset handles to load before onEnter(). Override in subclasses. */
|
|
1580
1761
|
preload;
|
|
1762
|
+
/** Default transition used when this scene is the destination of a push/pop/replace. */
|
|
1763
|
+
defaultTransition;
|
|
1581
1764
|
/** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
|
|
1582
1765
|
paused = false;
|
|
1583
1766
|
/** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
|
|
@@ -1589,6 +1772,7 @@ var Scene = class {
|
|
|
1589
1772
|
queryCache;
|
|
1590
1773
|
bus;
|
|
1591
1774
|
_entityEventHandlers;
|
|
1775
|
+
_scopedServices;
|
|
1592
1776
|
/** Access the EngineContext. */
|
|
1593
1777
|
get context() {
|
|
1594
1778
|
return this._context;
|
|
@@ -1606,6 +1790,11 @@ var Scene = class {
|
|
|
1606
1790
|
}
|
|
1607
1791
|
return false;
|
|
1608
1792
|
}
|
|
1793
|
+
/** Whether a scene transition is currently running. */
|
|
1794
|
+
get isTransitioning() {
|
|
1795
|
+
const sm = this._context?.tryResolve(SceneManagerKey);
|
|
1796
|
+
return sm?.isTransitioning ?? false;
|
|
1797
|
+
}
|
|
1609
1798
|
/** Convenience accessor for the AssetManager. */
|
|
1610
1799
|
get assets() {
|
|
1611
1800
|
return this._context.resolve(AssetManagerKey);
|
|
@@ -1730,6 +1919,31 @@ var Scene = class {
|
|
|
1730
1919
|
}
|
|
1731
1920
|
}
|
|
1732
1921
|
// ---- Internal methods ----
|
|
1922
|
+
/**
|
|
1923
|
+
* Register a scene-scoped service. Called from a plugin's `beforeEnter`
|
|
1924
|
+
* hook to make per-scene state (render tree, physics world) resolvable via
|
|
1925
|
+
* `Component.use(key)`.
|
|
1926
|
+
* @internal
|
|
1927
|
+
*/
|
|
1928
|
+
_registerScoped(key, value) {
|
|
1929
|
+
this._scopedServices ??= /* @__PURE__ */ new Map();
|
|
1930
|
+
this._scopedServices.set(key.id, value);
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Resolve a scene-scoped service, or `undefined` if none was registered.
|
|
1934
|
+
* @internal
|
|
1935
|
+
*/
|
|
1936
|
+
_resolveScoped(key) {
|
|
1937
|
+
return this._scopedServices?.get(key.id);
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Clear all scene-scoped services. Called by the SceneManager after
|
|
1941
|
+
* `afterExit` hooks run, so plugin cleanup code still sees scoped state.
|
|
1942
|
+
* @internal
|
|
1943
|
+
*/
|
|
1944
|
+
_clearScopedServices() {
|
|
1945
|
+
this._scopedServices?.clear();
|
|
1946
|
+
}
|
|
1733
1947
|
/**
|
|
1734
1948
|
* Set the engine context. Called by SceneManager when the scene is pushed.
|
|
1735
1949
|
* @internal
|
|
@@ -1781,6 +1995,138 @@ var Scene = class {
|
|
|
1781
1995
|
}
|
|
1782
1996
|
};
|
|
1783
1997
|
|
|
1998
|
+
// src/LoadingScene.ts
|
|
1999
|
+
var LoadingScene = class extends Scene {
|
|
2000
|
+
static {
|
|
2001
|
+
__name(this, "LoadingScene");
|
|
2002
|
+
}
|
|
2003
|
+
name = "loading";
|
|
2004
|
+
/**
|
|
2005
|
+
* Minimum wall-clock ms the scene stays visible before handing off.
|
|
2006
|
+
* Prevents flicker on cached loads. Default 0.
|
|
2007
|
+
*/
|
|
2008
|
+
minDuration = 0;
|
|
2009
|
+
/** Transition used for the loading → target handoff. */
|
|
2010
|
+
transition;
|
|
2011
|
+
/**
|
|
2012
|
+
* When true (default), the handoff fires automatically after loading and
|
|
2013
|
+
* `minDuration`. Set false to gate it behind `continue()` — useful when
|
|
2014
|
+
* the loading scene also asks the player to press a key or click.
|
|
2015
|
+
*/
|
|
2016
|
+
autoContinue = true;
|
|
2017
|
+
_progress = 0;
|
|
2018
|
+
_started = false;
|
|
2019
|
+
_active = true;
|
|
2020
|
+
_continueRequested = false;
|
|
2021
|
+
_continueGate;
|
|
2022
|
+
// Bumped on every `_run` attempt. `AssetManager.loadAll` uses `Promise.all`
|
|
2023
|
+
// under the hood, so individual loaders from a failed attempt can still
|
|
2024
|
+
// resolve and fire `onProgress` after the attempt rejects. Without this
|
|
2025
|
+
// guard, a retry kicked off from `onLoadError` would see stale progress
|
|
2026
|
+
// callbacks mutate `_progress` and emit `scene:loading:progress` events
|
|
2027
|
+
// attributed to the current attempt.
|
|
2028
|
+
_attempt = 0;
|
|
2029
|
+
/** Current load progress, 0 → 1. Updated as the AssetManager reports progress. */
|
|
2030
|
+
get progress() {
|
|
2031
|
+
return this._progress;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Kick off asset loading. While a load is in flight, subsequent calls
|
|
2035
|
+
* are no-ops. After a load failure the guard is released, so calling
|
|
2036
|
+
* `startLoading()` from `onLoadError` (or from a retry button) kicks off
|
|
2037
|
+
* a fresh load against the same target.
|
|
2038
|
+
*
|
|
2039
|
+
* Usually called once from `onEnter` after spawning the loading UI:
|
|
2040
|
+
* ```ts
|
|
2041
|
+
* override onEnter() {
|
|
2042
|
+
* this.spawn(LoadingSceneProgressBar);
|
|
2043
|
+
* this.startLoading();
|
|
2044
|
+
* }
|
|
2045
|
+
* ```
|
|
2046
|
+
*
|
|
2047
|
+
* Deferring the call lets you gate the start of the load behind a
|
|
2048
|
+
* title screen, "press any key" prompt, intro animation, etc.
|
|
2049
|
+
*/
|
|
2050
|
+
startLoading() {
|
|
2051
|
+
if (this._started) return;
|
|
2052
|
+
this._started = true;
|
|
2053
|
+
this._run().catch((err) => {
|
|
2054
|
+
if (!this._active) return;
|
|
2055
|
+
const logger = this.context.tryResolve(LoggerKey);
|
|
2056
|
+
if (logger) {
|
|
2057
|
+
logger.error("LoadingScene", "loading failed", { error: err });
|
|
2058
|
+
} else {
|
|
2059
|
+
console.error("[LoadingScene] loading failed:", err);
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Trigger the handoff to `target`. No-op if already called or if
|
|
2065
|
+
* `autoContinue` already fired it. If called before loading finishes,
|
|
2066
|
+
* the handoff runs as soon as loading + `minDuration` complete.
|
|
2067
|
+
*/
|
|
2068
|
+
continue() {
|
|
2069
|
+
if (this._continueRequested) return;
|
|
2070
|
+
this._continueRequested = true;
|
|
2071
|
+
this._continueGate?.();
|
|
2072
|
+
}
|
|
2073
|
+
onExit() {
|
|
2074
|
+
this._active = false;
|
|
2075
|
+
this._continueGate?.();
|
|
2076
|
+
}
|
|
2077
|
+
async _run() {
|
|
2078
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2079
|
+
if (!this._active) return;
|
|
2080
|
+
const attempt = ++this._attempt;
|
|
2081
|
+
const target = typeof this.target === "function" ? this.target() : this.target;
|
|
2082
|
+
const startedAt = performance.now();
|
|
2083
|
+
const bus = this.context.resolve(EventBusKey);
|
|
2084
|
+
try {
|
|
2085
|
+
await this.assets.loadAll(target.preload ?? [], (ratio) => {
|
|
2086
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2087
|
+
this._progress = ratio;
|
|
2088
|
+
bus.emit("scene:loading:progress", { scene: this, ratio });
|
|
2089
|
+
});
|
|
2090
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2091
|
+
const elapsed = performance.now() - startedAt;
|
|
2092
|
+
const remaining = this.minDuration - elapsed;
|
|
2093
|
+
if (remaining > 0) {
|
|
2094
|
+
await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
2095
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2096
|
+
}
|
|
2097
|
+
} catch (err) {
|
|
2098
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2099
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2100
|
+
this._started = false;
|
|
2101
|
+
this._attempt++;
|
|
2102
|
+
if (this.onLoadError) {
|
|
2103
|
+
await this.onLoadError(error);
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
throw error;
|
|
2107
|
+
}
|
|
2108
|
+
bus.emit("scene:loading:done", { scene: this });
|
|
2109
|
+
if (!this.autoContinue && !this._continueRequested) {
|
|
2110
|
+
await new Promise((resolve) => {
|
|
2111
|
+
this._continueGate = resolve;
|
|
2112
|
+
});
|
|
2113
|
+
if (!this._active || attempt !== this._attempt) return;
|
|
2114
|
+
}
|
|
2115
|
+
const scenes = this.context.resolve(SceneManagerKey);
|
|
2116
|
+
await scenes.replace(
|
|
2117
|
+
target,
|
|
2118
|
+
this.transition ? { transition: this.transition } : void 0
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
// src/SceneTransition.ts
|
|
2124
|
+
function resolveTransition(callSite, destination) {
|
|
2125
|
+
if (callSite) return callSite;
|
|
2126
|
+
return destination?.defaultTransition;
|
|
2127
|
+
}
|
|
2128
|
+
__name(resolveTransition, "resolveTransition");
|
|
2129
|
+
|
|
1784
2130
|
// src/SceneManager.ts
|
|
1785
2131
|
var SceneManager = class {
|
|
1786
2132
|
static {
|
|
@@ -1790,6 +2136,35 @@ var SceneManager = class {
|
|
|
1790
2136
|
_context;
|
|
1791
2137
|
bus;
|
|
1792
2138
|
assetManager;
|
|
2139
|
+
hookRegistry;
|
|
2140
|
+
logger;
|
|
2141
|
+
_currentRun;
|
|
2142
|
+
_pendingChain = Promise.resolve();
|
|
2143
|
+
_mutationDepth = 0;
|
|
2144
|
+
_destroyed = false;
|
|
2145
|
+
_autoPauseOnBlur = false;
|
|
2146
|
+
_isBlurred = false;
|
|
2147
|
+
_visibilityPausedScenes = /* @__PURE__ */ new Set();
|
|
2148
|
+
_visibilityListenerCleanup;
|
|
2149
|
+
/**
|
|
2150
|
+
* Pause all non-paused scenes when `document.hidden` becomes `true`; restore
|
|
2151
|
+
* them on focus. Default: `false`. Only scenes paused by this mechanism are
|
|
2152
|
+
* restored — user-paused scenes (manual `scene.paused = true` or `pauseBelow`
|
|
2153
|
+
* cascade) are never touched.
|
|
2154
|
+
*/
|
|
2155
|
+
get autoPauseOnBlur() {
|
|
2156
|
+
return this._autoPauseOnBlur;
|
|
2157
|
+
}
|
|
2158
|
+
set autoPauseOnBlur(value) {
|
|
2159
|
+
if (this._autoPauseOnBlur === value) return;
|
|
2160
|
+
this._autoPauseOnBlur = value;
|
|
2161
|
+
if (!this._isBlurred) return;
|
|
2162
|
+
if (value) {
|
|
2163
|
+
this._applyBlurPause();
|
|
2164
|
+
} else if (this._visibilityPausedScenes.size > 0) {
|
|
2165
|
+
this._restoreBlurPause();
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1793
2168
|
/**
|
|
1794
2169
|
* Set the engine context.
|
|
1795
2170
|
* @internal
|
|
@@ -1798,6 +2173,42 @@ var SceneManager = class {
|
|
|
1798
2173
|
this._context = context;
|
|
1799
2174
|
this.bus = context.tryResolve(EventBusKey);
|
|
1800
2175
|
this.assetManager = context.tryResolve(AssetManagerKey);
|
|
2176
|
+
this.hookRegistry = context.tryResolve(SceneHookRegistryKey);
|
|
2177
|
+
this.logger = context.tryResolve(LoggerKey);
|
|
2178
|
+
if (this._visibilityListenerCleanup || typeof document === "undefined") {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
const onVisibilityChange = /* @__PURE__ */ __name(() => {
|
|
2182
|
+
this._handleVisibilityChange(document.hidden);
|
|
2183
|
+
}, "onVisibilityChange");
|
|
2184
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2185
|
+
this._visibilityListenerCleanup = () => document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* React to a visibility change. Parameterised on `hidden` so unit tests can
|
|
2189
|
+
* drive it without a real `document`.
|
|
2190
|
+
* @internal
|
|
2191
|
+
*/
|
|
2192
|
+
_handleVisibilityChange(hidden) {
|
|
2193
|
+
if (hidden && !this._isBlurred) {
|
|
2194
|
+
this._isBlurred = true;
|
|
2195
|
+
if (this._autoPauseOnBlur) this._applyBlurPause();
|
|
2196
|
+
} else if (!hidden && this._isBlurred) {
|
|
2197
|
+
this._isBlurred = false;
|
|
2198
|
+
if (this._visibilityPausedScenes.size > 0) this._restoreBlurPause();
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
_applyBlurPause() {
|
|
2202
|
+
for (const scene of this.activeScenes) {
|
|
2203
|
+
scene.paused = true;
|
|
2204
|
+
this._visibilityPausedScenes.add(scene);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
_restoreBlurPause() {
|
|
2208
|
+
for (const scene of this._visibilityPausedScenes) {
|
|
2209
|
+
scene.paused = false;
|
|
2210
|
+
}
|
|
2211
|
+
this._visibilityPausedScenes.clear();
|
|
1801
2212
|
}
|
|
1802
2213
|
/** The topmost (active) scene. */
|
|
1803
2214
|
get active() {
|
|
@@ -1809,84 +2220,132 @@ var SceneManager = class {
|
|
|
1809
2220
|
}
|
|
1810
2221
|
/** All non-paused scenes in the stack, bottom to top. */
|
|
1811
2222
|
get activeScenes() {
|
|
1812
|
-
return this.stack.filter((
|
|
2223
|
+
return this.stack.filter((scene) => !scene.isPaused);
|
|
2224
|
+
}
|
|
2225
|
+
/** Whether a scene transition is currently running. */
|
|
2226
|
+
get isTransitioning() {
|
|
2227
|
+
return this._currentRun !== void 0;
|
|
1813
2228
|
}
|
|
1814
2229
|
/**
|
|
1815
2230
|
* Push a scene onto the stack. Scenes below may receive onPause().
|
|
1816
2231
|
* If the scene declares a `preload` array, assets are loaded before onEnter().
|
|
1817
|
-
* Await the returned promise when using preloaded scenes.
|
|
1818
2232
|
*/
|
|
1819
|
-
push(scene) {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
});
|
|
1829
|
-
}
|
|
1830
|
-
scene.onEnter?.();
|
|
1831
|
-
this.bus?.emit("scene:pushed", { scene });
|
|
1832
|
-
return Promise.resolve();
|
|
2233
|
+
async push(scene, opts) {
|
|
2234
|
+
this._assertNotMutating("push");
|
|
2235
|
+
await this._enqueue(async () => {
|
|
2236
|
+
const fromScene = this.active;
|
|
2237
|
+
await this._pushScene(scene);
|
|
2238
|
+
const transition = resolveTransition(opts?.transition, scene);
|
|
2239
|
+
if (!transition) return;
|
|
2240
|
+
await this._runTransition("push", transition, fromScene, scene);
|
|
2241
|
+
});
|
|
1833
2242
|
}
|
|
1834
2243
|
/** Pop the top scene. Scenes below may receive onResume(). */
|
|
1835
|
-
pop() {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2244
|
+
async pop(opts) {
|
|
2245
|
+
this._assertNotMutating("pop");
|
|
2246
|
+
return this._enqueue(async () => {
|
|
2247
|
+
if (this.stack.length === 0) return void 0;
|
|
2248
|
+
const fromScene = this.active;
|
|
2249
|
+
const destination = this.stack.length > 1 ? this.stack[this.stack.length - 2] : void 0;
|
|
2250
|
+
const transition = resolveTransition(opts?.transition, destination);
|
|
2251
|
+
if (transition) {
|
|
2252
|
+
await this._runTransition("pop", transition, fromScene, destination);
|
|
2253
|
+
}
|
|
2254
|
+
return this._popScene();
|
|
2255
|
+
});
|
|
1844
2256
|
}
|
|
1845
2257
|
/**
|
|
1846
|
-
* Replace the top scene.
|
|
1847
|
-
*
|
|
2258
|
+
* Replace the top scene. Without a transition the old scene exits first,
|
|
2259
|
+
* then the new scene enters. With a transition the new scene is pushed
|
|
2260
|
+
* first, both scenes coexist for the transition duration, then the old
|
|
2261
|
+
* scene is removed at the end.
|
|
1848
2262
|
*/
|
|
1849
|
-
replace(scene) {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2263
|
+
async replace(scene, opts) {
|
|
2264
|
+
this._assertNotMutating("replace");
|
|
2265
|
+
await this._enqueue(async () => {
|
|
2266
|
+
const transition = resolveTransition(opts?.transition, scene);
|
|
2267
|
+
if (!transition) {
|
|
2268
|
+
await this._replaceScene(scene);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
const old = this.active;
|
|
2272
|
+
await this._pushScene(scene, true);
|
|
2273
|
+
await this._runTransition("replace", transition, old, scene);
|
|
2274
|
+
if (old) {
|
|
2275
|
+
this._removeScene(old, true);
|
|
2276
|
+
}
|
|
2277
|
+
this.bus?.emit("scene:replaced", {
|
|
2278
|
+
oldScene: old ?? scene,
|
|
2279
|
+
newScene: scene
|
|
2280
|
+
});
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Pop every scene on the stack, top to bottom. Each receives onExit().
|
|
2285
|
+
* Queued like push/pop/replace — runs after any in-flight transition.
|
|
2286
|
+
* Use for "restart from menu"-style flows. Does not run transitions.
|
|
2287
|
+
*/
|
|
2288
|
+
async popAll() {
|
|
2289
|
+
this._assertNotMutating("popAll");
|
|
2290
|
+
await this._enqueue(async () => {
|
|
2291
|
+
this._withMutationSync(() => {
|
|
2292
|
+
while (this.stack.length > 0) {
|
|
2293
|
+
const scene = this.stack.pop();
|
|
2294
|
+
if (!scene) break;
|
|
2295
|
+
this._teardownScene(scene);
|
|
2296
|
+
this.bus?.emit("scene:popped", { scene });
|
|
1870
2297
|
}
|
|
1871
2298
|
});
|
|
1872
|
-
}
|
|
1873
|
-
scene.onEnter?.();
|
|
1874
|
-
if (old) {
|
|
1875
|
-
this.bus?.emit("scene:replaced", { oldScene: old, newScene: scene });
|
|
1876
|
-
} else {
|
|
1877
|
-
this.bus?.emit("scene:pushed", { scene });
|
|
1878
|
-
}
|
|
1879
|
-
return Promise.resolve();
|
|
2299
|
+
});
|
|
1880
2300
|
}
|
|
1881
|
-
/**
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2301
|
+
/**
|
|
2302
|
+
* Run the full scene-enter lifecycle (beforeEnter hooks, preload, onEnter)
|
|
2303
|
+
* for a scene that is NOT placed on the stack. Used by infrastructure
|
|
2304
|
+
* plugins like DebugPlugin that render a scene off-stack.
|
|
2305
|
+
* @internal
|
|
2306
|
+
*/
|
|
2307
|
+
async _mountDetached(scene) {
|
|
2308
|
+
await this._withMutation(async () => {
|
|
2309
|
+
scene._setContext(this._context);
|
|
2310
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2311
|
+
await this._preloadScene(scene);
|
|
2312
|
+
scene.onEnter?.();
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Run the scene-exit lifecycle (onExit, entity destruction, afterExit
|
|
2317
|
+
* hooks, scoped-service clear) for a detached scene.
|
|
2318
|
+
* @internal
|
|
2319
|
+
*/
|
|
2320
|
+
_unmountDetached(scene) {
|
|
2321
|
+
this._withMutationSync(() => {
|
|
2322
|
+
this._teardownScene(scene);
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Mark the manager destroyed and synchronously tear down every scene.
|
|
2327
|
+
* Called by Engine.destroy(). Any queued async work short-circuits on
|
|
2328
|
+
* resume; in-flight transitions' pending promises are resolved via
|
|
2329
|
+
* _cleanupRun so they don't leak.
|
|
2330
|
+
* @internal
|
|
2331
|
+
*/
|
|
2332
|
+
_destroy() {
|
|
2333
|
+
this._destroyed = true;
|
|
2334
|
+
if (this._currentRun) {
|
|
2335
|
+
this._cleanupRun(this._currentRun);
|
|
2336
|
+
}
|
|
2337
|
+
this._pendingChain = Promise.resolve();
|
|
2338
|
+
this._visibilityListenerCleanup?.();
|
|
2339
|
+
this._visibilityListenerCleanup = void 0;
|
|
2340
|
+
this._visibilityPausedScenes.clear();
|
|
2341
|
+
this._withMutationSync(() => {
|
|
2342
|
+
while (this.stack.length > 0) {
|
|
2343
|
+
const scene = this.stack.pop();
|
|
2344
|
+
if (!scene) break;
|
|
2345
|
+
this._teardownScene(scene);
|
|
2346
|
+
this.bus?.emit("scene:popped", { scene });
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
1890
2349
|
}
|
|
1891
2350
|
/**
|
|
1892
2351
|
* Flush destroy queues for all active scenes.
|
|
@@ -1898,21 +2357,217 @@ var SceneManager = class {
|
|
|
1898
2357
|
scene._flushDestroyQueue();
|
|
1899
2358
|
}
|
|
1900
2359
|
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Advance the active transition by `dt` ms. Called by Engine's earlyUpdate
|
|
2362
|
+
* callback with raw (unscaled) wall-clock dt.
|
|
2363
|
+
* @internal
|
|
2364
|
+
*/
|
|
2365
|
+
_tickTransition(dt) {
|
|
2366
|
+
const run = this._currentRun;
|
|
2367
|
+
if (!run) return;
|
|
2368
|
+
const remaining = run.transition.duration - run.elapsed;
|
|
2369
|
+
const consume = Math.min(dt, remaining);
|
|
2370
|
+
run.elapsed += consume;
|
|
2371
|
+
this._safeTick(run, consume);
|
|
2372
|
+
if (run.elapsed >= run.transition.duration) {
|
|
2373
|
+
this._cleanupRun(run);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
// ---- Private helpers ----
|
|
2377
|
+
_enqueue(work) {
|
|
2378
|
+
if (this._destroyed) return Promise.resolve(void 0);
|
|
2379
|
+
const next = this._pendingChain.then(async () => {
|
|
2380
|
+
if (this._destroyed) return void 0;
|
|
2381
|
+
return work();
|
|
2382
|
+
});
|
|
2383
|
+
this._pendingChain = next.then(
|
|
2384
|
+
() => void 0,
|
|
2385
|
+
() => void 0
|
|
2386
|
+
);
|
|
2387
|
+
return next;
|
|
2388
|
+
}
|
|
2389
|
+
async _pushScene(scene, suppressEvent = false) {
|
|
2390
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2391
|
+
await this._withMutation(async () => {
|
|
2392
|
+
scene._setContext(this._context);
|
|
2393
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2394
|
+
await this._preloadScene(scene);
|
|
2395
|
+
this.stack.push(scene);
|
|
2396
|
+
scene.onEnter?.();
|
|
2397
|
+
this._firePauseTransitions(wasPaused);
|
|
2398
|
+
if (!suppressEvent) {
|
|
2399
|
+
this.bus?.emit("scene:pushed", { scene });
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
_popScene(suppressEvent = false) {
|
|
2404
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2405
|
+
return this._withMutationSync(() => {
|
|
2406
|
+
const removed = this.stack.pop();
|
|
2407
|
+
if (!removed) return void 0;
|
|
2408
|
+
this._teardownScene(removed);
|
|
2409
|
+
this._fireResumeTransitions(wasPaused);
|
|
2410
|
+
if (!suppressEvent) {
|
|
2411
|
+
this.bus?.emit("scene:popped", { scene: removed });
|
|
2412
|
+
}
|
|
2413
|
+
return removed;
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
async _replaceScene(scene) {
|
|
2417
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2418
|
+
await this._withMutation(async () => {
|
|
2419
|
+
scene._setContext(this._context);
|
|
2420
|
+
await this.hookRegistry?.runBeforeEnter(scene);
|
|
2421
|
+
await this._preloadScene(scene);
|
|
2422
|
+
const old = this.stack.pop();
|
|
2423
|
+
if (old) this._teardownScene(old);
|
|
2424
|
+
this.stack.push(scene);
|
|
2425
|
+
scene.onEnter?.();
|
|
2426
|
+
this._firePauseTransitions(wasPaused);
|
|
2427
|
+
this._fireResumeTransitions(wasPaused);
|
|
2428
|
+
this.bus?.emit("scene:replaced", {
|
|
2429
|
+
oldScene: old ?? scene,
|
|
2430
|
+
newScene: scene
|
|
2431
|
+
});
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
_removeScene(scene, suppressEvent = false) {
|
|
2435
|
+
this._withMutationSync(() => {
|
|
2436
|
+
const idx = this.stack.indexOf(scene);
|
|
2437
|
+
if (idx === -1) return;
|
|
2438
|
+
const wasPaused = this._snapshotPauseStates();
|
|
2439
|
+
this.stack.splice(idx, 1);
|
|
2440
|
+
this._teardownScene(scene);
|
|
2441
|
+
this._firePauseTransitions(wasPaused);
|
|
2442
|
+
this._fireResumeTransitions(wasPaused);
|
|
2443
|
+
if (!suppressEvent) {
|
|
2444
|
+
this.bus?.emit("scene:popped", { scene });
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
async _preloadScene(scene) {
|
|
2449
|
+
if (!scene.preload?.length || !this.assetManager) return;
|
|
2450
|
+
await this.assetManager.loadAll(
|
|
2451
|
+
scene.preload,
|
|
2452
|
+
scene.onProgress?.bind(scene)
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
_teardownScene(scene) {
|
|
2456
|
+
scene.onExit?.();
|
|
2457
|
+
scene._destroyAllEntities();
|
|
2458
|
+
this.hookRegistry?.runAfterExit(scene);
|
|
2459
|
+
scene._clearScopedServices();
|
|
2460
|
+
this._visibilityPausedScenes.delete(scene);
|
|
2461
|
+
}
|
|
2462
|
+
async _runTransition(kind, transition, fromScene, toScene) {
|
|
2463
|
+
if (this._destroyed) return;
|
|
2464
|
+
let resolveRun;
|
|
2465
|
+
const promise = new Promise((resolve) => {
|
|
2466
|
+
resolveRun = resolve;
|
|
2467
|
+
});
|
|
2468
|
+
const run = {
|
|
2469
|
+
kind,
|
|
2470
|
+
transition,
|
|
2471
|
+
elapsed: 0,
|
|
2472
|
+
fromScene,
|
|
2473
|
+
toScene,
|
|
2474
|
+
resolve: resolveRun
|
|
2475
|
+
};
|
|
2476
|
+
this._currentRun = run;
|
|
2477
|
+
this.bus?.emit("scene:transition:started", {
|
|
2478
|
+
kind,
|
|
2479
|
+
fromScene,
|
|
2480
|
+
toScene
|
|
2481
|
+
});
|
|
2482
|
+
this._safeCall(run, "begin");
|
|
2483
|
+
if (!Number.isFinite(transition.duration) || transition.duration <= 0) {
|
|
2484
|
+
this._cleanupRun(run);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
await promise;
|
|
2488
|
+
}
|
|
2489
|
+
_cleanupRun(run) {
|
|
2490
|
+
if (this._currentRun !== run) return;
|
|
2491
|
+
this._safeCall(run, "end");
|
|
2492
|
+
this._currentRun = void 0;
|
|
2493
|
+
this.bus?.emit("scene:transition:ended", {
|
|
2494
|
+
kind: run.kind,
|
|
2495
|
+
fromScene: run.fromScene,
|
|
2496
|
+
toScene: run.toScene
|
|
2497
|
+
});
|
|
2498
|
+
run.resolve();
|
|
2499
|
+
}
|
|
2500
|
+
_safeTick(run, dt) {
|
|
2501
|
+
try {
|
|
2502
|
+
run.transition.tick(dt, this._makeContext(run));
|
|
2503
|
+
} catch (err) {
|
|
2504
|
+
this.logger?.warn(
|
|
2505
|
+
"SceneManager",
|
|
2506
|
+
`Transition tick error: ${err instanceof Error ? err.message : String(err)}`
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
_safeCall(run, method) {
|
|
2511
|
+
try {
|
|
2512
|
+
run.transition[method]?.(this._makeContext(run));
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
this.logger?.warn(
|
|
2515
|
+
"SceneManager",
|
|
2516
|
+
`Transition ${method} error: ${err instanceof Error ? err.message : String(err)}`
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
_makeContext(run) {
|
|
2521
|
+
return {
|
|
2522
|
+
elapsed: run.elapsed,
|
|
2523
|
+
kind: run.kind,
|
|
2524
|
+
engineContext: this._context,
|
|
2525
|
+
fromScene: run.fromScene,
|
|
2526
|
+
toScene: run.toScene
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
_snapshotPauseStates() {
|
|
2530
|
+
return new Map(
|
|
2531
|
+
this.stack.map((scene) => [scene, scene.isPaused])
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
_assertNotMutating(method) {
|
|
2535
|
+
if (this._mutationDepth === 0) return;
|
|
2536
|
+
throw new Error(
|
|
2537
|
+
`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().`
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
async _withMutation(work) {
|
|
2541
|
+
this._mutationDepth++;
|
|
2542
|
+
try {
|
|
2543
|
+
return await work();
|
|
2544
|
+
} finally {
|
|
2545
|
+
this._mutationDepth--;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
_withMutationSync(work) {
|
|
2549
|
+
this._mutationDepth++;
|
|
2550
|
+
try {
|
|
2551
|
+
return work();
|
|
2552
|
+
} finally {
|
|
2553
|
+
this._mutationDepth--;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
1901
2556
|
/** Fire onPause() for scenes that transitioned from not-paused to paused. */
|
|
1902
2557
|
_firePauseTransitions(wasPaused) {
|
|
1903
|
-
for (const
|
|
1904
|
-
const was = wasPaused.get(
|
|
1905
|
-
if (
|
|
1906
|
-
|
|
2558
|
+
for (const scene of this.stack) {
|
|
2559
|
+
const was = wasPaused.get(scene) ?? false;
|
|
2560
|
+
if (scene.isPaused && !was) {
|
|
2561
|
+
scene.onPause?.();
|
|
1907
2562
|
}
|
|
1908
2563
|
}
|
|
1909
2564
|
}
|
|
1910
2565
|
/** Fire onResume() for scenes that transitioned from paused to not-paused. */
|
|
1911
2566
|
_fireResumeTransitions(wasPaused) {
|
|
1912
|
-
for (const
|
|
1913
|
-
const was = wasPaused.get(
|
|
1914
|
-
if (!
|
|
1915
|
-
|
|
2567
|
+
for (const scene of this.stack) {
|
|
2568
|
+
const was = wasPaused.get(scene) ?? false;
|
|
2569
|
+
if (!scene.isPaused && was) {
|
|
2570
|
+
scene.onResume?.();
|
|
1916
2571
|
}
|
|
1917
2572
|
}
|
|
1918
2573
|
}
|
|
@@ -2749,6 +3404,7 @@ var Engine = class {
|
|
|
2749
3404
|
scheduler;
|
|
2750
3405
|
errorBoundary;
|
|
2751
3406
|
queryCache;
|
|
3407
|
+
sceneHooks;
|
|
2752
3408
|
/** The asset manager. */
|
|
2753
3409
|
assets;
|
|
2754
3410
|
plugins = /* @__PURE__ */ new Map();
|
|
@@ -2767,6 +3423,7 @@ var Engine = class {
|
|
|
2767
3423
|
this.scheduler = new SystemScheduler();
|
|
2768
3424
|
this.inspector = new Inspector(this);
|
|
2769
3425
|
this.assets = new AssetManager();
|
|
3426
|
+
this.sceneHooks = new SceneHookRegistry();
|
|
2770
3427
|
this.scheduler.setErrorBoundary(this.errorBoundary);
|
|
2771
3428
|
this.context.register(EngineKey, this);
|
|
2772
3429
|
this.context.register(EventBusKey, this.events);
|
|
@@ -2778,11 +3435,13 @@ var Engine = class {
|
|
|
2778
3435
|
this.context.register(InspectorKey, this.inspector);
|
|
2779
3436
|
this.context.register(SystemSchedulerKey, this.scheduler);
|
|
2780
3437
|
this.context.register(AssetManagerKey, this.assets);
|
|
3438
|
+
this.context.register(SceneHookRegistryKey, this.sceneHooks);
|
|
2781
3439
|
this.scenes._setContext(this.context);
|
|
2782
3440
|
this.registerBuiltInSystems();
|
|
2783
3441
|
this.loop.setCallbacks({
|
|
2784
3442
|
earlyUpdate: /* @__PURE__ */ __name((dt) => {
|
|
2785
3443
|
this.logger.setFrame(this.loop.frameCount);
|
|
3444
|
+
this.scenes._tickTransition(dt);
|
|
2786
3445
|
this.scheduler.run("earlyUpdate" /* EarlyUpdate */, dt);
|
|
2787
3446
|
}, "earlyUpdate"),
|
|
2788
3447
|
fixedUpdate: /* @__PURE__ */ __name((dt) => this.scheduler.run("fixedUpdate" /* FixedUpdate */, dt), "fixedUpdate"),
|
|
@@ -2795,6 +3454,14 @@ var Engine = class {
|
|
|
2795
3454
|
}, "endOfFrame")
|
|
2796
3455
|
});
|
|
2797
3456
|
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Register scene lifecycle hooks. The returned function unregisters the
|
|
3459
|
+
* hooks. Infrastructure plugins (renderer, physics, debug) register hooks
|
|
3460
|
+
* in their `install` or `onStart` to set up and tear down per-scene state.
|
|
3461
|
+
*/
|
|
3462
|
+
registerSceneHooks(hooks) {
|
|
3463
|
+
return this.sceneHooks.register(hooks);
|
|
3464
|
+
}
|
|
2798
3465
|
/** Register a plugin. Must be called before start(). */
|
|
2799
3466
|
use(plugin) {
|
|
2800
3467
|
if (this.started) {
|
|
@@ -2830,7 +3497,7 @@ var Engine = class {
|
|
|
2830
3497
|
};
|
|
2831
3498
|
}
|
|
2832
3499
|
for (const plugin of sorted) {
|
|
2833
|
-
plugin.onStart?.();
|
|
3500
|
+
await plugin.onStart?.();
|
|
2834
3501
|
}
|
|
2835
3502
|
this.events.emit("engine:started", void 0);
|
|
2836
3503
|
}
|
|
@@ -2838,7 +3505,7 @@ var Engine = class {
|
|
|
2838
3505
|
destroy() {
|
|
2839
3506
|
this.events.emit("engine:stopped", void 0);
|
|
2840
3507
|
this.loop.stop();
|
|
2841
|
-
this.scenes.
|
|
3508
|
+
this.scenes._destroy();
|
|
2842
3509
|
const allSystems = this.scheduler.getAllSystems();
|
|
2843
3510
|
for (let i = allSystems.length - 1; i >= 0; i--) {
|
|
2844
3511
|
allSystems[i].onUnregister?.();
|
|
@@ -2912,6 +3579,11 @@ var Engine = class {
|
|
|
2912
3579
|
}
|
|
2913
3580
|
};
|
|
2914
3581
|
|
|
3582
|
+
// src/RendererAdapter.ts
|
|
3583
|
+
var RendererAdapterKey = new ServiceKey(
|
|
3584
|
+
"rendererAdapter"
|
|
3585
|
+
);
|
|
3586
|
+
|
|
2915
3587
|
// src/test-utils.ts
|
|
2916
3588
|
var _TestScene = class extends Scene {
|
|
2917
3589
|
static {
|
|
@@ -2982,6 +3654,7 @@ var VERSION = "0.0.0";
|
|
|
2982
3654
|
Inspector,
|
|
2983
3655
|
InspectorKey,
|
|
2984
3656
|
KeyframeAnimator,
|
|
3657
|
+
LoadingScene,
|
|
2985
3658
|
LogLevel,
|
|
2986
3659
|
Logger,
|
|
2987
3660
|
LoggerKey,
|
|
@@ -2995,8 +3668,11 @@ var VERSION = "0.0.0";
|
|
|
2995
3668
|
QueryCache,
|
|
2996
3669
|
QueryCacheKey,
|
|
2997
3670
|
QueryResult,
|
|
3671
|
+
RendererAdapterKey,
|
|
2998
3672
|
SERIALIZABLE_KEY,
|
|
2999
3673
|
Scene,
|
|
3674
|
+
SceneHookRegistry,
|
|
3675
|
+
SceneHookRegistryKey,
|
|
3000
3676
|
SceneManager,
|
|
3001
3677
|
SceneManagerKey,
|
|
3002
3678
|
Sequence,
|
|
@@ -3029,6 +3705,7 @@ var VERSION = "0.0.0";
|
|
|
3029
3705
|
getSerializableType,
|
|
3030
3706
|
interpolate,
|
|
3031
3707
|
isSerializable,
|
|
3708
|
+
resolveTransition,
|
|
3032
3709
|
serializable,
|
|
3033
3710
|
trait
|
|
3034
3711
|
});
|