@yagejs/core 0.3.0 → 0.4.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.js CHANGED
@@ -231,14 +231,6 @@ var MathUtils = {
231
231
  const wrapped = MathUtils.wrap(t, 0, length * 2);
232
232
  return length - Math.abs(wrapped - length);
233
233
  },
234
- /** Random float in [min, max). */
235
- randomRange(min, max) {
236
- return min + Math.random() * (max - min);
237
- },
238
- /** Random integer in [min, max] (inclusive). */
239
- randomInt(min, max) {
240
- return Math.floor(min + Math.random() * (max - min + 1));
241
- },
242
234
  /** Convert degrees to radians. */
243
235
  degToRad(degrees) {
244
236
  return degrees * Math.PI / 180;
@@ -289,12 +281,138 @@ var MathUtils = {
289
281
  }
290
282
  };
291
283
 
284
+ // src/EngineContext.ts
285
+ var ServiceKey = class {
286
+ constructor(id, options) {
287
+ this.id = id;
288
+ this.scope = options?.scope ?? "engine";
289
+ }
290
+ id;
291
+ static {
292
+ __name(this, "ServiceKey");
293
+ }
294
+ /** Declared scope (engine or scene). Defaults to `"engine"`. */
295
+ scope;
296
+ };
297
+ var EngineContext = class {
298
+ static {
299
+ __name(this, "EngineContext");
300
+ }
301
+ services = /* @__PURE__ */ new Map();
302
+ /** Register a service. Throws if the key is already registered. */
303
+ register(key, service) {
304
+ if (this.services.has(key.id)) {
305
+ throw new Error(`Service "${key.id}" is already registered.`);
306
+ }
307
+ this.services.set(key.id, service);
308
+ }
309
+ /** Resolve a service. Throws if not registered. */
310
+ resolve(key) {
311
+ if (!this.services.has(key.id)) {
312
+ throw new Error(`Service "${key.id}" is not registered.`);
313
+ }
314
+ return this.services.get(key.id);
315
+ }
316
+ /** Resolve a service, returning undefined if not registered. */
317
+ tryResolve(key) {
318
+ return this.services.get(key.id);
319
+ }
320
+ /** Remove a registered service. No-op if not registered. */
321
+ unregister(key) {
322
+ this.services.delete(key.id);
323
+ }
324
+ /** Check if a service is registered. */
325
+ has(key) {
326
+ return this.services.has(key.id);
327
+ }
328
+ };
329
+ var EngineKey = new ServiceKey("engine");
330
+ var EventBusKey = new ServiceKey("eventBus");
331
+ var SceneManagerKey = new ServiceKey("sceneManager");
332
+ var LoggerKey = new ServiceKey("logger");
333
+ var InspectorKey = new ServiceKey("inspector");
334
+ var QueryCacheKey = new ServiceKey("queryCache");
335
+ var ErrorBoundaryKey = new ServiceKey("errorBoundary");
336
+ var GameLoopKey = new ServiceKey("gameLoop");
337
+ var SystemSchedulerKey = new ServiceKey(
338
+ "systemScheduler"
339
+ );
340
+ var ProcessSystemKey = new ServiceKey("processSystem");
341
+ var AssetManagerKey = new ServiceKey("assetManager");
342
+
343
+ // src/Random.ts
344
+ var RandomKey = new ServiceKey("random", {
345
+ scope: "scene"
346
+ });
347
+ var UINT32_MAX = 4294967296;
348
+ function normalizeSeed(seed) {
349
+ return seed >>> 0;
350
+ }
351
+ __name(normalizeSeed, "normalizeSeed");
352
+ function createDefaultRandomSeed() {
353
+ return normalizeSeed(Date.now() ^ Math.floor(Math.random() * 1e9));
354
+ }
355
+ __name(createDefaultRandomSeed, "createDefaultRandomSeed");
356
+ var Mulberry32Random = class {
357
+ static {
358
+ __name(this, "Mulberry32Random");
359
+ }
360
+ seed;
361
+ state;
362
+ constructor(seed) {
363
+ const normalized = normalizeSeed(seed);
364
+ this.seed = normalized;
365
+ this.state = normalized;
366
+ }
367
+ float() {
368
+ let t = this.state += 1831565813;
369
+ t = Math.imul(t ^ t >>> 15, t | 1);
370
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
371
+ return ((t ^ t >>> 14) >>> 0) / UINT32_MAX;
372
+ }
373
+ range(min, max) {
374
+ return min + this.float() * (max - min);
375
+ }
376
+ int(min, max) {
377
+ return Math.floor(this.range(min, max + 1));
378
+ }
379
+ pick(arr) {
380
+ if (arr.length === 0) {
381
+ throw new Error("RandomService.pick() requires a non-empty array.");
382
+ }
383
+ return arr[this.int(0, arr.length - 1)];
384
+ }
385
+ shuffle(arr) {
386
+ for (let i = arr.length - 1; i > 0; i--) {
387
+ const j = this.int(0, i);
388
+ const tmp = arr[i];
389
+ arr[i] = arr[j];
390
+ arr[j] = tmp;
391
+ }
392
+ return arr;
393
+ }
394
+ setSeed(seed) {
395
+ const normalized = normalizeSeed(seed);
396
+ this.seed = normalized;
397
+ this.state = normalized;
398
+ }
399
+ getSeed() {
400
+ return this.seed;
401
+ }
402
+ };
403
+ function createRandomService(seed = createDefaultRandomSeed()) {
404
+ return new Mulberry32Random(seed);
405
+ }
406
+ __name(createRandomService, "createRandomService");
407
+ var globalRandom = createRandomService();
408
+
292
409
  // src/EventBus.ts
293
410
  var EventBus = class {
294
411
  static {
295
412
  __name(this, "EventBus");
296
413
  }
297
414
  handlers = /* @__PURE__ */ new Map();
415
+ observers = /* @__PURE__ */ new Set();
298
416
  /** Subscribe to an event. Returns an unsubscribe function. */
299
417
  on(event, handler) {
300
418
  let list = this.handlers.get(event);
@@ -321,6 +439,12 @@ var EventBus = class {
321
439
  }
322
440
  /** Emit an event. Handlers are called synchronously in registration order. */
323
441
  emit(event, data) {
442
+ if (this.observers.size > 0) {
443
+ const observers = [...this.observers];
444
+ for (const observer of observers) {
445
+ observer(event, data);
446
+ }
447
+ }
324
448
  const list = this.handlers.get(event);
325
449
  if (!list) return;
326
450
  const snapshot = [...list];
@@ -328,6 +452,16 @@ var EventBus = class {
328
452
  handler(data);
329
453
  }
330
454
  }
455
+ /**
456
+ * Observe every emitted event without affecting handler order or control
457
+ * flow. Used by tooling such as the Inspector event log.
458
+ */
459
+ tap(observer) {
460
+ this.observers.add(observer);
461
+ return () => {
462
+ this.observers.delete(observer);
463
+ };
464
+ }
331
465
  /** Remove all handlers for an event, or all handlers if no event specified. */
332
466
  clear(event) {
333
467
  if (event !== void 0) {
@@ -437,63 +571,6 @@ var Logger = class {
437
571
  }
438
572
  };
439
573
 
440
- // src/EngineContext.ts
441
- var ServiceKey = class {
442
- constructor(id, options) {
443
- this.id = id;
444
- this.scope = options?.scope ?? "engine";
445
- }
446
- id;
447
- static {
448
- __name(this, "ServiceKey");
449
- }
450
- /** Declared scope (engine or scene). Defaults to `"engine"`. */
451
- scope;
452
- };
453
- var EngineContext = class {
454
- static {
455
- __name(this, "EngineContext");
456
- }
457
- services = /* @__PURE__ */ new Map();
458
- /** Register a service. Throws if the key is already registered. */
459
- register(key, service) {
460
- if (this.services.has(key.id)) {
461
- throw new Error(`Service "${key.id}" is already registered.`);
462
- }
463
- this.services.set(key.id, service);
464
- }
465
- /** Resolve a service. Throws if not registered. */
466
- resolve(key) {
467
- if (!this.services.has(key.id)) {
468
- throw new Error(`Service "${key.id}" is not registered.`);
469
- }
470
- return this.services.get(key.id);
471
- }
472
- /** Resolve a service, returning undefined if not registered. */
473
- tryResolve(key) {
474
- return this.services.get(key.id);
475
- }
476
- /** Remove a registered service. No-op if not registered. */
477
- unregister(key) {
478
- this.services.delete(key.id);
479
- }
480
- /** Check if a service is registered. */
481
- has(key) {
482
- return this.services.has(key.id);
483
- }
484
- };
485
- var EngineKey = new ServiceKey("engine");
486
- var EventBusKey = new ServiceKey("eventBus");
487
- var SceneManagerKey = new ServiceKey("sceneManager");
488
- var LoggerKey = new ServiceKey("logger");
489
- var InspectorKey = new ServiceKey("inspector");
490
- var QueryCacheKey = new ServiceKey("queryCache");
491
- var ErrorBoundaryKey = new ServiceKey("errorBoundary");
492
- var GameLoopKey = new ServiceKey("gameLoop");
493
- var SystemSchedulerKey = new ServiceKey("systemScheduler");
494
- var ProcessSystemKey = new ServiceKey("processSystem");
495
- var AssetManagerKey = new ServiceKey("assetManager");
496
-
497
574
  // src/SceneHooks.ts
498
575
  var SceneHookRegistry = class {
499
576
  static {
@@ -1230,6 +1307,7 @@ var Entity = class {
1230
1307
  }
1231
1308
  }
1232
1309
  this._scene?._onEntityEvent(token.name, data, this);
1310
+ this._scene?._observeEntityEvent(token.name, data, this);
1233
1311
  }
1234
1312
  /** Get all components as an iterable. */
1235
1313
  getAll() {
@@ -1652,89 +1730,888 @@ var GameLoop = class {
1652
1730
  }
1653
1731
  };
1654
1732
 
1655
- // src/Scene.ts
1656
- var Scene = class {
1733
+ // src/Inspector.ts
1734
+ var InputManagerRuntimeKey = new ServiceKey("inputManager");
1735
+ var PhysicsWorldManagerRuntimeKey = new ServiceKey(
1736
+ "physicsWorldManager"
1737
+ );
1738
+ var RendererRuntimeKey = new ServiceKey("renderer");
1739
+ var Inspector = class {
1657
1740
  static {
1658
- __name(this, "Scene");
1741
+ __name(this, "Inspector");
1659
1742
  }
1660
- /** Whether scenes below this one in the stack should be paused. Default: true. */
1661
- pauseBelow = true;
1662
- /** Whether scenes below this one should still render. Default: false. */
1663
- transparentBelow = false;
1664
- /** Asset handles to load before onEnter(). Override in subclasses. */
1665
- preload;
1666
- /** Default transition used when this scene is the destination of a push/pop/replace. */
1667
- defaultTransition;
1668
- /** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
1669
- paused = false;
1670
- /** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
1671
- timeScale = 1;
1672
- entities = /* @__PURE__ */ new Set();
1673
- destroyQueue = [];
1674
- _context;
1675
- entityCallbacks;
1676
- queryCache;
1677
- bus;
1678
- _entityEventHandlers;
1679
- _scopedServices;
1680
- /** Access the EngineContext. */
1681
- get context() {
1682
- return this._context;
1743
+ engine;
1744
+ extensions = /* @__PURE__ */ new Map();
1745
+ sceneIds = /* @__PURE__ */ new WeakMap();
1746
+ nextSceneId = 0;
1747
+ defaultSceneSeed;
1748
+ sceneSeedOverride;
1749
+ timeController = null;
1750
+ eventLogEnabled = false;
1751
+ eventCapacity = 500;
1752
+ /**
1753
+ * Ring buffer of recent events. `eventLogHead` points at the oldest slot;
1754
+ * a full ring contains exactly `eventCapacity` entries. We avoid `splice` to
1755
+ * keep `appendEvent` O(1) the previous shift-on-overflow approach was
1756
+ * O(n) per event once the buffer was full.
1757
+ */
1758
+ eventLog = [];
1759
+ eventLogHead = 0;
1760
+ eventWaiters = /* @__PURE__ */ new Set();
1761
+ detachBusTap = null;
1762
+ busEventObserver = /* @__PURE__ */ __name((event, data) => {
1763
+ this.recordBusEvent(String(event), data);
1764
+ }, "busEventObserver");
1765
+ sceneEventObserver = /* @__PURE__ */ __name((eventName, data, entity) => {
1766
+ this.recordEntityEvent(eventName, data, entity);
1767
+ }, "sceneEventObserver");
1768
+ time = {
1769
+ freeze: /* @__PURE__ */ __name(() => {
1770
+ this.requireTimeController().freeze();
1771
+ }, "freeze"),
1772
+ thaw: /* @__PURE__ */ __name(() => {
1773
+ this.requireTimeController().thaw();
1774
+ }, "thaw"),
1775
+ step: /* @__PURE__ */ __name((frames = 1) => {
1776
+ this.assertNonNegativeInteger(frames, "Inspector.time.step(frames)");
1777
+ if (frames === 0) return;
1778
+ this.requireTimeController().stepFrames(frames);
1779
+ this.expireDeadlineWaiters();
1780
+ }, "step"),
1781
+ setDelta: /* @__PURE__ */ __name((ms) => {
1782
+ if (!Number.isFinite(ms) || ms <= 0) {
1783
+ throw new Error("Inspector.time.setDelta(ms) requires a positive number.");
1784
+ }
1785
+ this.requireTimeController().setDelta(ms);
1786
+ }, "setDelta"),
1787
+ isFrozen: /* @__PURE__ */ __name(() => this.timeController?.isFrozen ?? false, "isFrozen"),
1788
+ getFrame: /* @__PURE__ */ __name(() => this.timeController?.getFrame() ?? this.engine.loop.frameCount, "getFrame")
1789
+ };
1790
+ input = {
1791
+ keyDown: /* @__PURE__ */ __name((code) => {
1792
+ this.requireInputManager().fireKeyDown(code);
1793
+ }, "keyDown"),
1794
+ keyUp: /* @__PURE__ */ __name((code) => {
1795
+ this.requireInputManager().fireKeyUp(code);
1796
+ }, "keyUp"),
1797
+ mouseMove: /* @__PURE__ */ __name((x, y) => {
1798
+ this.requireInputManager().firePointerMove(x, y);
1799
+ }, "mouseMove"),
1800
+ mouseDown: /* @__PURE__ */ __name((button = 0) => {
1801
+ this.requireInputManager().firePointerDown(button);
1802
+ }, "mouseDown"),
1803
+ mouseUp: /* @__PURE__ */ __name((button = 0) => {
1804
+ this.requireInputManager().firePointerUp(button);
1805
+ }, "mouseUp"),
1806
+ gamepadButton: /* @__PURE__ */ __name((idx, pressed) => {
1807
+ this.requireInputManager().fireGamepadButton(idx, pressed);
1808
+ }, "gamepadButton"),
1809
+ gamepadAxis: /* @__PURE__ */ __name((idx, value) => {
1810
+ this.requireInputManager().fireGamepadAxis(idx, value);
1811
+ }, "gamepadAxis"),
1812
+ tap: /* @__PURE__ */ __name((code, frames = 1) => {
1813
+ this.assertNonNegativeInteger(frames, "Inspector.input.tap(frames)");
1814
+ const input = this.requireInputManager();
1815
+ input.fireKeyDown(code);
1816
+ try {
1817
+ this.time.step(frames);
1818
+ } finally {
1819
+ input.fireKeyUp(code);
1820
+ }
1821
+ }, "tap"),
1822
+ hold: /* @__PURE__ */ __name((code, frames) => {
1823
+ this.assertNonNegativeInteger(frames, "Inspector.input.hold(frames)");
1824
+ const input = this.requireInputManager();
1825
+ input.fireKeyDown(code);
1826
+ try {
1827
+ this.time.step(frames);
1828
+ } finally {
1829
+ input.fireKeyUp(code);
1830
+ }
1831
+ }, "hold"),
1832
+ fireAction: /* @__PURE__ */ __name((name, frames = 1) => {
1833
+ this.assertNonNegativeInteger(
1834
+ frames,
1835
+ "Inspector.input.fireAction(frames)"
1836
+ );
1837
+ const input = this.requireInputManager();
1838
+ for (let i = 0; i < frames; i++) {
1839
+ input.fireAction(name);
1840
+ this.time.step(1);
1841
+ }
1842
+ }, "fireAction"),
1843
+ clearAll: /* @__PURE__ */ __name(() => {
1844
+ this.requireInputManager().clearAll();
1845
+ }, "clearAll")
1846
+ };
1847
+ events = {
1848
+ getLog: /* @__PURE__ */ __name(() => this.iterateLog().map(({ entry }) => ({ ...entry })), "getLog"),
1849
+ clearLog: /* @__PURE__ */ __name(() => {
1850
+ this.eventLog.length = 0;
1851
+ this.eventLogHead = 0;
1852
+ }, "clearLog"),
1853
+ setCapacity: /* @__PURE__ */ __name((n) => {
1854
+ this.assertNonNegativeInteger(
1855
+ n,
1856
+ "Inspector.events.setCapacity(capacity)"
1857
+ );
1858
+ const ordered = n === 0 ? [] : this.iterateLog().slice(-n);
1859
+ this.eventCapacity = n;
1860
+ this.eventLog = ordered;
1861
+ this.eventLogHead = 0;
1862
+ }, "setCapacity"),
1863
+ waitFor: /* @__PURE__ */ __name((pattern, options) => {
1864
+ const existing = this.findMatchingEvent(pattern, options?.source);
1865
+ if (existing) return Promise.resolve(existing);
1866
+ const withinFrames = options?.withinFrames;
1867
+ if (withinFrames !== void 0 && (!Number.isInteger(withinFrames) || withinFrames < 0)) {
1868
+ throw new Error(
1869
+ "Inspector.events.waitFor(withinFrames) requires a non-negative integer."
1870
+ );
1871
+ }
1872
+ return new Promise((resolve, reject) => {
1873
+ const waiter = {
1874
+ pattern,
1875
+ source: options?.source,
1876
+ withinFrames,
1877
+ deadlineFrame: withinFrames !== void 0 ? this.time.getFrame() + withinFrames : void 0,
1878
+ resolve,
1879
+ reject
1880
+ };
1881
+ this.eventWaiters.add(waiter);
1882
+ });
1883
+ }, "waitFor")
1884
+ };
1885
+ capture = {
1886
+ png: /* @__PURE__ */ __name(async () => {
1887
+ const base64 = await this.capture.pngBase64();
1888
+ return decodeBase64(base64);
1889
+ }, "png"),
1890
+ dataURL: /* @__PURE__ */ __name(async () => {
1891
+ const renderer = this.engine.context.tryResolve(RendererRuntimeKey);
1892
+ if (!renderer) {
1893
+ throw new Error(
1894
+ "Inspector.capture requires RendererPlugin to be active."
1895
+ );
1896
+ }
1897
+ const canvas = renderer.application.renderer.extract.canvas(
1898
+ renderer.application.stage
1899
+ );
1900
+ return canvas.toDataURL("image/png");
1901
+ }, "dataURL"),
1902
+ pngBase64: /* @__PURE__ */ __name(async () => {
1903
+ const dataUrl = await this.capture.dataURL();
1904
+ const comma = dataUrl.indexOf(",");
1905
+ return comma === -1 ? dataUrl : dataUrl.slice(comma + 1);
1906
+ }, "pngBase64")
1907
+ };
1908
+ constructor(engine) {
1909
+ this.engine = engine;
1683
1910
  }
1684
- /** Whether this scene is effectively paused (manual pause or paused by stack). */
1685
- get isPaused() {
1686
- if (this.paused) return true;
1687
- const sm = this._context?.tryResolve(SceneManagerKey);
1688
- if (!sm) return false;
1689
- const stack = sm.all;
1690
- const idx = stack.indexOf(this);
1691
- if (idx === -1) return false;
1692
- for (let i = idx + 1; i < stack.length; i++) {
1693
- if (stack[i].pauseBelow) return true;
1911
+ /** Register a namespaced extension API for plugin-specific debug helpers. */
1912
+ addExtension(namespace, api) {
1913
+ this.assertNonEmptyString(
1914
+ namespace,
1915
+ "Inspector.addExtension(namespace)"
1916
+ );
1917
+ if (!api || typeof api !== "object") {
1918
+ throw new Error("Inspector.addExtension(api) requires an object.");
1694
1919
  }
1695
- return false;
1920
+ if (this.extensions.has(namespace)) {
1921
+ throw new Error(
1922
+ `Inspector.addExtension(): namespace "${namespace}" is already registered.`
1923
+ );
1924
+ }
1925
+ this.extensions.set(namespace, api);
1926
+ return api;
1696
1927
  }
1697
- /** Whether a scene transition is currently running. */
1698
- get isTransitioning() {
1699
- const sm = this._context?.tryResolve(SceneManagerKey);
1700
- return sm?.isTransitioning ?? false;
1928
+ /** Look up a previously registered extension API by namespace. */
1929
+ getExtension(namespace) {
1930
+ this.assertNonEmptyString(
1931
+ namespace,
1932
+ "Inspector.getExtension(namespace)"
1933
+ );
1934
+ return this.extensions.get(namespace);
1701
1935
  }
1702
- /** Convenience accessor for the AssetManager. */
1703
- get assets() {
1704
- return this._context.resolve(AssetManagerKey);
1936
+ /** Remove a previously registered extension namespace. */
1937
+ removeExtension(namespace) {
1938
+ this.assertNonEmptyString(
1939
+ namespace,
1940
+ "Inspector.removeExtension(namespace)"
1941
+ );
1942
+ this.extensions.delete(namespace);
1705
1943
  }
1706
- /**
1707
- * Lazy proxy-based service resolution. Can be used at field-declaration time:
1708
- * ```ts
1709
- * readonly layers = this.service(RenderLayerManagerKey);
1710
- * ```
1711
- * The actual resolution is deferred until first property access.
1712
- */
1713
- service(key) {
1714
- let resolved;
1715
- return new Proxy({}, {
1716
- get: /* @__PURE__ */ __name((_target, prop) => {
1717
- resolved ??= this._context.resolve(key);
1718
- const value = resolved[prop];
1719
- return typeof value === "function" ? value.bind(resolved) : value;
1720
- }, "get"),
1721
- set: /* @__PURE__ */ __name((_target, prop, value) => {
1722
- resolved ??= this._context.resolve(key);
1723
- resolved[prop] = value;
1724
- return true;
1725
- }, "set")
1726
- });
1944
+ /** Full deterministic state snapshot (stable ordering, serializable). */
1945
+ snapshot() {
1946
+ const scenes = this.engine.scenes.all.map(
1947
+ (scene) => this.sceneToWorldSnapshot(scene)
1948
+ );
1949
+ return {
1950
+ version: 1,
1951
+ frame: this.time.getFrame(),
1952
+ sceneStack: this.getSceneStack(),
1953
+ entityCount: this.countEntities(),
1954
+ systemCount: this.getSystems().length,
1955
+ errors: this.getErrors(),
1956
+ scenes,
1957
+ camera: this.buildCameraSnapshot(),
1958
+ input: this.buildInputSnapshot()
1959
+ };
1727
1960
  }
1728
- spawn(nameOrBlueprintOrClass, params) {
1729
- if (typeof nameOrBlueprintOrClass === "function") {
1730
- const entity2 = new nameOrBlueprintOrClass();
1731
- entity2._setScene(this, this.entityCallbacks);
1732
- this.entities.add(entity2);
1733
- this.bus?.emit("entity:created", { entity: entity2 });
1734
- entity2.setup?.(params);
1735
- return entity2;
1961
+ /** Stable JSON form of {@link snapshot}. */
1962
+ snapshotJSON() {
1963
+ return stableStringify(this.snapshot());
1964
+ }
1965
+ /** Snapshot one scene by inspector scene id. */
1966
+ snapshotScene(id) {
1967
+ const scene = this.engine.scenes.all.find(
1968
+ (candidate) => this.getSceneId(candidate) === id
1969
+ );
1970
+ if (!scene) {
1971
+ throw new Error(`Inspector.snapshotScene(): unknown scene id "${id}".`);
1736
1972
  }
1737
- const isBlueprint = typeof nameOrBlueprintOrClass === "object" && nameOrBlueprintOrClass !== null && "build" in nameOrBlueprintOrClass;
1973
+ return this.sceneToWorldSnapshot(scene);
1974
+ }
1975
+ /** Find entity by name in the active scene. */
1976
+ getEntityByName(name) {
1977
+ const entity = this.findActiveEntity(name);
1978
+ if (!entity) return void 0;
1979
+ return this.entityToQuerySnapshot(entity);
1980
+ }
1981
+ /** Get entity position (from Transform component). */
1982
+ getEntityPosition(name) {
1983
+ const entity = this.findActiveEntity(name);
1984
+ if (!entity) return void 0;
1985
+ const transform = this.getTransform(entity);
1986
+ if (!transform) return void 0;
1987
+ return { x: transform.position.x, y: transform.position.y };
1988
+ }
1989
+ /** Check if an entity has a component by class name string. */
1990
+ hasComponent(entityName, componentClass) {
1991
+ return this.findComponentByName(entityName, componentClass) !== void 0;
1992
+ }
1993
+ /** Get component data (serializable subset) by class name string. */
1994
+ getComponentData(entityName, componentClass) {
1995
+ const comp = this.findComponentByName(entityName, componentClass);
1996
+ if (!comp) return void 0;
1997
+ if (typeof comp.serialize === "function") {
1998
+ const data = trySerialize(comp);
1999
+ if (data !== void 0) return data;
2000
+ }
2001
+ return this.serializeComponentOwnProperties(comp);
2002
+ }
2003
+ /** Get all entities in the active scene as lightweight snapshots. */
2004
+ getEntities() {
2005
+ const scene = this.engine.scenes.active;
2006
+ if (!scene) return [];
2007
+ const result = [];
2008
+ for (const entity of scene.getEntities()) {
2009
+ if (!entity.isDestroyed) {
2010
+ result.push(this.entityToQuerySnapshot(entity));
2011
+ }
2012
+ }
2013
+ return result;
2014
+ }
2015
+ /** Get scene stack info. */
2016
+ getSceneStack() {
2017
+ return this.engine.scenes.all.map((scene) => ({
2018
+ name: scene.name,
2019
+ entityCount: scene.getEntities().size,
2020
+ paused: scene.isPaused
2021
+ }));
2022
+ }
2023
+ /** Get active system info. */
2024
+ getSystems() {
2025
+ const scheduler = this.engine.context.tryResolve(SystemSchedulerKey);
2026
+ if (!scheduler) return [];
2027
+ return scheduler.getAllSystems().map((sys) => ({
2028
+ name: sys.constructor.name,
2029
+ phase: sys.phase,
2030
+ priority: sys.priority,
2031
+ enabled: sys.enabled
2032
+ }));
2033
+ }
2034
+ /** Get disabled components/systems from error boundary. */
2035
+ getErrors() {
2036
+ const boundary = this.engine.context.tryResolve(ErrorBoundaryKey);
2037
+ if (!boundary) return { disabledSystems: [], disabledComponents: [] };
2038
+ const disabled = boundary.getDisabled();
2039
+ return {
2040
+ disabledSystems: disabled.systems.map(
2041
+ (s) => s.system.constructor.name
2042
+ ),
2043
+ disabledComponents: disabled.components.map((c) => ({
2044
+ entity: c.component.entity?.name ?? "unknown",
2045
+ component: c.component.constructor.name,
2046
+ error: c.error
2047
+ }))
2048
+ };
2049
+ }
2050
+ /** Create a new scene-scoped RNG instance using the current inspector seed policy. */
2051
+ createSceneRandom() {
2052
+ const seed = this.sceneSeedOverride ?? this.defaultSceneSeed ?? createDefaultRandomSeed();
2053
+ return createRandomService(seed);
2054
+ }
2055
+ /** Force every current and future scene RNG to the provided seed. */
2056
+ setSeed(seed) {
2057
+ const normalized = normalizeSeed(seed);
2058
+ this.sceneSeedOverride = normalized;
2059
+ for (const scene of this.engine.scenes.all) {
2060
+ this.resolveInternalRandom(scene)?.setSeed(normalized);
2061
+ }
2062
+ }
2063
+ /** @internal DebugPlugin installs a deterministic default seed through this hook. */
2064
+ setDefaultSceneSeed(seed) {
2065
+ this.defaultSceneSeed = seed === void 0 ? void 0 : normalizeSeed(seed);
2066
+ if (this.sceneSeedOverride !== void 0 || this.defaultSceneSeed === void 0) {
2067
+ return;
2068
+ }
2069
+ for (const scene of this.engine.scenes.all) {
2070
+ this.resolveInternalRandom(scene)?.setSeed(this.defaultSceneSeed);
2071
+ }
2072
+ }
2073
+ resolveInternalRandom(scene) {
2074
+ return scene._resolveScoped(RandomKey);
2075
+ }
2076
+ /** @internal DebugPlugin attaches the frozen-time controller through this hook. */
2077
+ attachTimeController(controller) {
2078
+ this.timeController = controller;
2079
+ }
2080
+ /** @internal Clear a previously attached time controller. */
2081
+ detachTimeController(controller) {
2082
+ if (!controller || this.timeController === controller) {
2083
+ this.timeController = null;
2084
+ }
2085
+ }
2086
+ /** @internal Enable or disable event log recording. */
2087
+ setEventLogEnabled(enabled) {
2088
+ if (this.eventLogEnabled === enabled) return;
2089
+ this.eventLogEnabled = enabled;
2090
+ if (enabled) {
2091
+ if (!this.detachBusTap && this.engine.events?.tap) {
2092
+ this.detachBusTap = this.engine.events.tap(this.busEventObserver);
2093
+ }
2094
+ } else {
2095
+ this.detachBusTap?.();
2096
+ this.detachBusTap = null;
2097
+ }
2098
+ for (const scene of this.engine.scenes.all) {
2099
+ if (enabled) {
2100
+ this.attachSceneEventObserver(scene);
2101
+ } else {
2102
+ this.detachSceneEventObserver(scene);
2103
+ }
2104
+ }
2105
+ }
2106
+ /** @internal Install entity-event observation for one scene. No-op if disabled. */
2107
+ attachSceneEventObserver(scene) {
2108
+ if (!this.eventLogEnabled) return;
2109
+ scene._setEntityEventObserver(this.sceneEventObserver);
2110
+ }
2111
+ /** @internal Clear entity-event observation for one scene. */
2112
+ detachSceneEventObserver(scene) {
2113
+ scene._setEntityEventObserver(void 0);
2114
+ }
2115
+ /** @internal Scene hooks forward entity events through this method. */
2116
+ recordEntityEvent(eventName, data, entity) {
2117
+ if (!this.eventLogEnabled) return;
2118
+ const scene = entity.tryScene;
2119
+ this.appendEvent(
2120
+ {
2121
+ frame: this.time.getFrame(),
2122
+ source: "entity",
2123
+ type: eventName,
2124
+ targetId: String(entity.id),
2125
+ payload: serializeEventPayload(data)
2126
+ },
2127
+ scene ? this.getSceneId(scene) : void 0
2128
+ );
2129
+ }
2130
+ /** @internal Engine teardown releases the event-bus tap through this hook. */
2131
+ dispose() {
2132
+ this.detachBusTap?.();
2133
+ this.detachBusTap = null;
2134
+ for (const scene of this.engine.scenes.all) {
2135
+ scene._setEntityEventObserver(void 0);
2136
+ }
2137
+ this.extensions.clear();
2138
+ }
2139
+ requireTimeController() {
2140
+ if (!this.timeController) {
2141
+ throw new Error(
2142
+ "Inspector.time requires DebugPlugin to be active."
2143
+ );
2144
+ }
2145
+ return this.timeController;
2146
+ }
2147
+ requireInputManager() {
2148
+ const input = this.engine.context.tryResolve(InputManagerRuntimeKey);
2149
+ if (!input) {
2150
+ throw new Error(
2151
+ "Inspector.input requires InputPlugin to be active."
2152
+ );
2153
+ }
2154
+ return input;
2155
+ }
2156
+ recordBusEvent(type, data) {
2157
+ if (!this.eventLogEnabled) return;
2158
+ this.appendEvent(
2159
+ {
2160
+ frame: this.time.getFrame(),
2161
+ source: "bus",
2162
+ type,
2163
+ payload: serializeEventPayload(data)
2164
+ },
2165
+ this.inferSceneIdFromPayload(data)
2166
+ );
2167
+ }
2168
+ appendEvent(entry, sceneId) {
2169
+ if (this.eventCapacity === 0) {
2170
+ this.flushMatchingWaiter(entry);
2171
+ return;
2172
+ }
2173
+ const logged = { entry, sceneId };
2174
+ if (this.eventLog.length < this.eventCapacity) {
2175
+ this.eventLog.push(logged);
2176
+ } else {
2177
+ this.eventLog[this.eventLogHead] = logged;
2178
+ this.eventLogHead = (this.eventLogHead + 1) % this.eventCapacity;
2179
+ }
2180
+ this.flushMatchingWaiter(entry);
2181
+ }
2182
+ /** Resolve waiters whose deadline has passed without a match. */
2183
+ expireDeadlineWaiters() {
2184
+ if (this.eventWaiters.size === 0) return;
2185
+ const frame = this.time.getFrame();
2186
+ for (const waiter of [...this.eventWaiters]) {
2187
+ if (waiter.deadlineFrame !== void 0 && frame > waiter.deadlineFrame) {
2188
+ this.eventWaiters.delete(waiter);
2189
+ waiter.reject(
2190
+ new Error(
2191
+ `Inspector.events.waitFor() timed out after ${waiter.withinFrames} frames.`
2192
+ )
2193
+ );
2194
+ }
2195
+ }
2196
+ }
2197
+ /** Resolve any waiter that matches the just-appended entry. */
2198
+ flushMatchingWaiter(entry) {
2199
+ if (this.eventWaiters.size === 0) return;
2200
+ for (const waiter of [...this.eventWaiters]) {
2201
+ if (this.eventMatches(entry, waiter.pattern, waiter.source)) {
2202
+ this.eventWaiters.delete(waiter);
2203
+ waiter.resolve(entry);
2204
+ }
2205
+ }
2206
+ }
2207
+ /**
2208
+ * Walk the ring buffer in chronological order. We avoid materializing the
2209
+ * ordered array on every event append; instead, every consumer that needs
2210
+ * order calls this helper.
2211
+ */
2212
+ iterateLog() {
2213
+ if (this.eventLog.length < this.eventCapacity || this.eventLogHead === 0) {
2214
+ return this.eventLog;
2215
+ }
2216
+ return [
2217
+ ...this.eventLog.slice(this.eventLogHead),
2218
+ ...this.eventLog.slice(0, this.eventLogHead)
2219
+ ];
2220
+ }
2221
+ findMatchingEvent(pattern, source) {
2222
+ for (const { entry } of this.iterateLog()) {
2223
+ if (this.eventMatches(entry, pattern, source)) {
2224
+ return { ...entry };
2225
+ }
2226
+ }
2227
+ return void 0;
2228
+ }
2229
+ eventMatches(entry, pattern, source) {
2230
+ if (source && entry.source !== source) return false;
2231
+ return typeof pattern === "string" ? entry.type === pattern : pattern.test(entry.type);
2232
+ }
2233
+ sceneToWorldSnapshot(scene) {
2234
+ const random = scene._resolveScoped(RandomKey);
2235
+ const physicsManager = this.engine.context.tryResolve(
2236
+ PhysicsWorldManagerRuntimeKey
2237
+ );
2238
+ return {
2239
+ id: this.getSceneId(scene),
2240
+ name: scene.name,
2241
+ paused: scene.isPaused,
2242
+ timeScale: scene.timeScale,
2243
+ seed: random?.getSeed() ?? 0,
2244
+ entities: this.getSceneEntities(scene),
2245
+ ui: this.buildUISnapshot(scene),
2246
+ physics: physicsManager?.getContext(scene)?.world.snapshot() ?? {
2247
+ bodies: [],
2248
+ contacts: []
2249
+ },
2250
+ events: this.getSceneEvents(scene)
2251
+ };
2252
+ }
2253
+ getSceneEntities(scene) {
2254
+ return [...scene.getEntities()].filter((entity) => !entity.isDestroyed).sort((a, b) => a.id - b.id).map((entity) => this.entityToWorldSnapshot(entity));
2255
+ }
2256
+ entityToWorldSnapshot(entity) {
2257
+ const transform = entity.has(Transform) ? entity.get(Transform) : void 0;
2258
+ const worldPosition = transform?.worldPosition;
2259
+ const worldScale = transform?.worldScale;
2260
+ const components = [...entity.getAll()].map((component) => this.componentToSnapshot(component)).sort((a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : 0);
2261
+ return {
2262
+ id: String(entity.id),
2263
+ type: entity.constructor.name,
2264
+ parent: entity.parent ? String(entity.parent.id) : null,
2265
+ transform: {
2266
+ x: worldPosition?.x ?? 0,
2267
+ y: worldPosition?.y ?? 0,
2268
+ rotation: transform?.worldRotation ?? 0,
2269
+ scaleX: worldScale?.x ?? 1,
2270
+ scaleY: worldScale?.y ?? 1
2271
+ },
2272
+ components
2273
+ };
2274
+ }
2275
+ componentToSnapshot(component) {
2276
+ return {
2277
+ type: component.constructor.name,
2278
+ state: typeof component.serialize === "function" ? trySerialize(component) ?? null : null
2279
+ };
2280
+ }
2281
+ buildUISnapshot(scene) {
2282
+ const roots = [...scene.getEntities()].filter((entity) => !entity.isDestroyed).flatMap(
2283
+ (entity) => [...entity.getAll()].filter(
2284
+ (component) => component.constructor.name === "UIPanel" && "_node" in component
2285
+ ).map(
2286
+ (component, index) => this.buildUINodeSnapshot(
2287
+ component._node,
2288
+ `entity-${entity.id}:UIPanel:${index}`
2289
+ )
2290
+ )
2291
+ );
2292
+ if (roots.length === 0) return null;
2293
+ if (roots.length === 1) {
2294
+ return { root: roots[0] };
2295
+ }
2296
+ return {
2297
+ root: {
2298
+ id: `scene-${this.getSceneId(scene)}:ui`,
2299
+ type: "UIRoot",
2300
+ layout: { x: 0, y: 0, width: 0, height: 0 },
2301
+ children: roots,
2302
+ state: null
2303
+ }
2304
+ };
2305
+ }
2306
+ buildUINodeSnapshot(node, id) {
2307
+ const layout = node.yogaNode?.getComputedLayout();
2308
+ const children = (node.children ?? []).map(
2309
+ (child, index) => this.buildUINodeSnapshot(child, `${id}/${index}`)
2310
+ );
2311
+ return {
2312
+ id,
2313
+ type: node.constructor.name,
2314
+ layout: {
2315
+ x: layout?.left ?? 0,
2316
+ y: layout?.top ?? 0,
2317
+ width: layout?.width ?? 0,
2318
+ height: layout?.height ?? 0
2319
+ },
2320
+ children,
2321
+ state: null
2322
+ };
2323
+ }
2324
+ buildCameraSnapshot() {
2325
+ const match = this.findTopmostCamera();
2326
+ if (!match) return null;
2327
+ const { scene, camera } = match;
2328
+ return {
2329
+ sceneId: this.getSceneId(scene),
2330
+ sceneName: scene.name,
2331
+ name: camera.cameraName ?? null,
2332
+ priority: camera.priority ?? 0,
2333
+ position: {
2334
+ x: camera.position.x,
2335
+ y: camera.position.y
2336
+ },
2337
+ zoom: camera.zoom,
2338
+ rotation: camera.rotation
2339
+ };
2340
+ }
2341
+ findTopmostCamera() {
2342
+ const stack = this.engine.scenes.all;
2343
+ for (let i = stack.length - 1; i >= 0; i--) {
2344
+ const scene = stack[i];
2345
+ if (!scene) continue;
2346
+ let highest;
2347
+ for (const entity of scene.getEntities()) {
2348
+ if (entity.isDestroyed) continue;
2349
+ for (const component of entity.getAll()) {
2350
+ if (component.constructor.name !== "CameraComponent") continue;
2351
+ const camera = component;
2352
+ if (camera.enabled && (!highest || (camera.priority ?? 0) > (highest.priority ?? 0))) {
2353
+ highest = camera;
2354
+ }
2355
+ }
2356
+ }
2357
+ if (highest) {
2358
+ return { scene, camera: highest };
2359
+ }
2360
+ }
2361
+ return void 0;
2362
+ }
2363
+ buildInputSnapshot() {
2364
+ const input = this.engine.context.tryResolve(InputManagerRuntimeKey);
2365
+ return input?.snapshotState() ?? {
2366
+ keys: [],
2367
+ actions: [],
2368
+ mouse: { x: 0, y: 0, buttons: [], down: false },
2369
+ gamepad: { buttons: [], axes: [] }
2370
+ };
2371
+ }
2372
+ getSceneEvents(scene) {
2373
+ const sceneId = this.getSceneId(scene);
2374
+ return this.iterateLog().filter((entry) => entry.sceneId === sceneId).map(({ entry }) => ({ ...entry }));
2375
+ }
2376
+ inferSceneIdFromPayload(data) {
2377
+ if (!data || typeof data !== "object") return void 0;
2378
+ const record = data;
2379
+ const scene = this.extractScene(record["scene"]) ?? this.extractSceneFromEntity(record["entity"]) ?? this.extractSceneFromEntity(record["oldScene"]) ?? this.extractSceneFromEntity(record["newScene"]);
2380
+ return scene ? this.getSceneId(scene) : void 0;
2381
+ }
2382
+ extractScene(value) {
2383
+ if (!value || typeof value !== "object") return void 0;
2384
+ return this.engine.scenes.all.find((scene) => scene === value);
2385
+ }
2386
+ extractSceneFromEntity(value) {
2387
+ if (!value || typeof value !== "object") return void 0;
2388
+ const maybeEntity = value;
2389
+ return maybeEntity.tryScene ?? this.extractScene(value);
2390
+ }
2391
+ findActiveEntity(name) {
2392
+ return this.engine.scenes.active?.findEntity(name);
2393
+ }
2394
+ findComponentByName(entityName, componentClass) {
2395
+ const entity = this.findActiveEntity(entityName);
2396
+ if (!entity) return void 0;
2397
+ for (const comp of entity.getAll()) {
2398
+ if (comp.constructor.name === componentClass) return comp;
2399
+ }
2400
+ return void 0;
2401
+ }
2402
+ entityToQuerySnapshot(entity) {
2403
+ const transform = this.getTransform(entity);
2404
+ const snapshot = {
2405
+ id: entity.id,
2406
+ name: entity.name,
2407
+ tags: [...entity.tags].sort((a, b) => a < b ? -1 : a > b ? 1 : 0),
2408
+ components: [...entity.getAll()].map((component) => component.constructor.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
2409
+ };
2410
+ if (transform) {
2411
+ snapshot.position = {
2412
+ x: transform.position.x,
2413
+ y: transform.position.y
2414
+ };
2415
+ }
2416
+ return snapshot;
2417
+ }
2418
+ getTransform(entity) {
2419
+ return entity.has(Transform) ? entity.get(Transform) : void 0;
2420
+ }
2421
+ serializeComponentOwnProperties(comp) {
2422
+ const result = {};
2423
+ for (const key of Object.getOwnPropertyNames(comp)) {
2424
+ if (key === "entity") continue;
2425
+ if (key.startsWith("_")) continue;
2426
+ const value = comp[key];
2427
+ if (!isSerializableValue(value)) continue;
2428
+ result[key] = value;
2429
+ }
2430
+ return result;
2431
+ }
2432
+ countEntities() {
2433
+ let count = 0;
2434
+ for (const scene of this.engine.scenes.all) {
2435
+ for (const entity of scene.getEntities()) {
2436
+ if (!entity.isDestroyed) count++;
2437
+ }
2438
+ }
2439
+ return count;
2440
+ }
2441
+ getSceneId(scene) {
2442
+ let id = this.sceneIds.get(scene);
2443
+ if (!id) {
2444
+ this.nextSceneId++;
2445
+ id = `scene-${this.nextSceneId}`;
2446
+ this.sceneIds.set(scene, id);
2447
+ }
2448
+ return id;
2449
+ }
2450
+ assertNonNegativeInteger(value, name) {
2451
+ if (!Number.isInteger(value) || value < 0) {
2452
+ throw new Error(`${name} requires a non-negative integer.`);
2453
+ }
2454
+ }
2455
+ assertNonEmptyString(value, name) {
2456
+ if (value.trim().length === 0) {
2457
+ throw new Error(`${name} requires a non-empty string.`);
2458
+ }
2459
+ }
2460
+ };
2461
+ function isSerializableValue(value) {
2462
+ if (value === null || value === void 0) return true;
2463
+ const t = typeof value;
2464
+ if (t === "function") return false;
2465
+ if (t !== "object") return true;
2466
+ if (Array.isArray(value)) return true;
2467
+ const proto = Object.getPrototypeOf(value);
2468
+ return proto === Object.prototype || proto === null;
2469
+ }
2470
+ __name(isSerializableValue, "isSerializableValue");
2471
+ function safeClone(value) {
2472
+ try {
2473
+ return JSON.parse(JSON.stringify(value));
2474
+ } catch {
2475
+ return void 0;
2476
+ }
2477
+ }
2478
+ __name(safeClone, "safeClone");
2479
+ function trySerialize(component) {
2480
+ try {
2481
+ return safeClone(component.serialize?.());
2482
+ } catch {
2483
+ return void 0;
2484
+ }
2485
+ }
2486
+ __name(trySerialize, "trySerialize");
2487
+ function serializeEventPayload(payload) {
2488
+ if (payload === void 0) return null;
2489
+ const cloned = safeClone(payload);
2490
+ return cloned === void 0 ? { _unserializable: true } : cloned;
2491
+ }
2492
+ __name(serializeEventPayload, "serializeEventPayload");
2493
+ function stableStringify(value) {
2494
+ return JSON.stringify(sortJsonValue(value));
2495
+ }
2496
+ __name(stableStringify, "stableStringify");
2497
+ function sortJsonValue(value) {
2498
+ if (Array.isArray(value)) {
2499
+ return value.map((item) => sortJsonValue(item));
2500
+ }
2501
+ if (value && typeof value === "object") {
2502
+ const entries = Object.entries(value).sort(
2503
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
2504
+ );
2505
+ const result = {};
2506
+ for (const [key, child] of entries) {
2507
+ result[key] = sortJsonValue(child);
2508
+ }
2509
+ return result;
2510
+ }
2511
+ return value;
2512
+ }
2513
+ __name(sortJsonValue, "sortJsonValue");
2514
+ function decodeBase64(base64) {
2515
+ if (typeof atob === "function") {
2516
+ const binary = atob(base64);
2517
+ const bytes = new Uint8Array(binary.length);
2518
+ for (let i = 0; i < binary.length; i++) {
2519
+ bytes[i] = binary.charCodeAt(i);
2520
+ }
2521
+ return bytes;
2522
+ }
2523
+ const bufferCtor = globalThis.Buffer;
2524
+ if (bufferCtor) {
2525
+ return bufferCtor.from(base64, "base64");
2526
+ }
2527
+ throw new Error("Inspector.capture.png() is not supported in this environment.");
2528
+ }
2529
+ __name(decodeBase64, "decodeBase64");
2530
+
2531
+ // src/Scene.ts
2532
+ var Scene = class {
2533
+ static {
2534
+ __name(this, "Scene");
2535
+ }
2536
+ /** Whether scenes below this one in the stack should be paused. Default: true. */
2537
+ pauseBelow = true;
2538
+ /** Whether scenes below this one should still render. Default: false. */
2539
+ transparentBelow = false;
2540
+ /** Asset handles to load before onEnter(). Override in subclasses. */
2541
+ preload;
2542
+ /** Default transition used when this scene is the destination of a push/pop/replace. */
2543
+ defaultTransition;
2544
+ /** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
2545
+ paused = false;
2546
+ /** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
2547
+ timeScale = 1;
2548
+ entities = /* @__PURE__ */ new Set();
2549
+ destroyQueue = [];
2550
+ _context;
2551
+ entityCallbacks;
2552
+ queryCache;
2553
+ bus;
2554
+ _entityEventHandlers;
2555
+ _entityEventObserver;
2556
+ _scopedServices;
2557
+ /** Access the EngineContext. */
2558
+ get context() {
2559
+ return this._context;
2560
+ }
2561
+ /** Whether this scene is effectively paused (manual pause or paused by stack). */
2562
+ get isPaused() {
2563
+ if (this.paused) return true;
2564
+ const sm = this._context?.tryResolve(SceneManagerKey);
2565
+ if (!sm) return false;
2566
+ const stack = sm.all;
2567
+ const idx = stack.indexOf(this);
2568
+ if (idx === -1) return false;
2569
+ for (let i = idx + 1; i < stack.length; i++) {
2570
+ if (stack[i].pauseBelow) return true;
2571
+ }
2572
+ return false;
2573
+ }
2574
+ /** Whether a scene transition is currently running. */
2575
+ get isTransitioning() {
2576
+ const sm = this._context?.tryResolve(SceneManagerKey);
2577
+ return sm?.isTransitioning ?? false;
2578
+ }
2579
+ /** Convenience accessor for the AssetManager. */
2580
+ get assets() {
2581
+ return this._context.resolve(AssetManagerKey);
2582
+ }
2583
+ /**
2584
+ * Lazy proxy-based service resolution. Can be used at field-declaration time:
2585
+ * ```ts
2586
+ * readonly layers = this.service(RenderLayerManagerKey);
2587
+ * ```
2588
+ * The actual resolution is deferred until first property access.
2589
+ */
2590
+ service(key) {
2591
+ let resolved;
2592
+ return new Proxy({}, {
2593
+ get: /* @__PURE__ */ __name((_target, prop) => {
2594
+ resolved ??= this._context.resolve(key);
2595
+ const value = resolved[prop];
2596
+ return typeof value === "function" ? value.bind(resolved) : value;
2597
+ }, "get"),
2598
+ set: /* @__PURE__ */ __name((_target, prop, value) => {
2599
+ resolved ??= this._context.resolve(key);
2600
+ resolved[prop] = value;
2601
+ return true;
2602
+ }, "set")
2603
+ });
2604
+ }
2605
+ spawn(nameOrBlueprintOrClass, params) {
2606
+ if (typeof nameOrBlueprintOrClass === "function") {
2607
+ const entity2 = new nameOrBlueprintOrClass();
2608
+ entity2._setScene(this, this.entityCallbacks);
2609
+ this.entities.add(entity2);
2610
+ this.bus?.emit("entity:created", { entity: entity2 });
2611
+ entity2.setup?.(params);
2612
+ return entity2;
2613
+ }
2614
+ const isBlueprint = typeof nameOrBlueprintOrClass === "object" && nameOrBlueprintOrClass !== null && "build" in nameOrBlueprintOrClass;
1738
2615
  const name = isBlueprint ? nameOrBlueprintOrClass.name : nameOrBlueprintOrClass;
1739
2616
  const entity = new Entity(name);
1740
2617
  entity._setScene(this, this.entityCallbacks);
@@ -1822,6 +2699,14 @@ var Scene = class {
1822
2699
  }
1823
2700
  }
1824
2701
  }
2702
+ /**
2703
+ * Observe entity-scoped event emissions after they dispatch locally and
2704
+ * bubble to the scene. Tooling only; game code should keep using `on()`.
2705
+ * @internal
2706
+ */
2707
+ _observeEntityEvent(eventName, data, entity) {
2708
+ this._entityEventObserver?.(eventName, data, entity);
2709
+ }
1825
2710
  // ---- Internal methods ----
1826
2711
  /**
1827
2712
  * Register a scene-scoped service. Called from a plugin's `beforeEnter`
@@ -1833,6 +2718,13 @@ var Scene = class {
1833
2718
  this._scopedServices ??= /* @__PURE__ */ new Map();
1834
2719
  this._scopedServices.set(key.id, value);
1835
2720
  }
2721
+ /**
2722
+ * Install or clear a tooling-only observer for bubbled entity events.
2723
+ * @internal
2724
+ */
2725
+ _setEntityEventObserver(observer) {
2726
+ this._entityEventObserver = observer;
2727
+ }
1836
2728
  /**
1837
2729
  * Resolve a scene-scoped service, or `undefined` if none was registered.
1838
2730
  * @internal
@@ -1899,6 +2791,128 @@ var Scene = class {
1899
2791
  }
1900
2792
  };
1901
2793
 
2794
+ // src/Process.ts
2795
+ var Process = class _Process {
2796
+ static {
2797
+ __name(this, "Process");
2798
+ }
2799
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
2800
+ updateFn;
2801
+ onCompleteFn;
2802
+ duration;
2803
+ loop;
2804
+ /** Tags for filtering/grouping. */
2805
+ tags;
2806
+ elapsed = 0;
2807
+ _completed = false;
2808
+ _paused = false;
2809
+ _cancelled = false;
2810
+ resolvePromise;
2811
+ /** Create a timer that fires `onComplete` after `duration` ms. */
2812
+ static delay(duration, onComplete, tags) {
2813
+ const opts = { duration };
2814
+ if (onComplete !== void 0) opts.onComplete = onComplete;
2815
+ if (tags !== void 0) opts.tags = tags;
2816
+ return new _Process(opts);
2817
+ }
2818
+ constructor(options) {
2819
+ this.updateFn = options.update ?? (() => {
2820
+ });
2821
+ this.onCompleteFn = options.onComplete;
2822
+ this.duration = options.duration;
2823
+ this.loop = options.loop ?? false;
2824
+ this.tags = options.tags ?? [];
2825
+ }
2826
+ /** Whether the process has completed. */
2827
+ get completed() {
2828
+ return this._completed;
2829
+ }
2830
+ /** Whether the process is paused. */
2831
+ get paused() {
2832
+ return this._paused;
2833
+ }
2834
+ /** Pause the process. */
2835
+ pause() {
2836
+ this._paused = true;
2837
+ }
2838
+ /** Resume the process. */
2839
+ resume() {
2840
+ this._paused = false;
2841
+ }
2842
+ /** Cancel the process. */
2843
+ cancel() {
2844
+ this._cancelled = true;
2845
+ this._completed = true;
2846
+ this.resolvePromise?.();
2847
+ }
2848
+ /** Returns a promise that resolves when the process completes or is cancelled. */
2849
+ toPromise() {
2850
+ if (this._completed) return Promise.resolve();
2851
+ return new Promise((resolve) => {
2852
+ this.resolvePromise = resolve;
2853
+ });
2854
+ }
2855
+ /**
2856
+ * Advance the process by dt milliseconds.
2857
+ * @internal
2858
+ */
2859
+ _update(dt) {
2860
+ if (this._completed || this._paused || this._cancelled) return;
2861
+ this.elapsed += dt;
2862
+ if (this.duration !== void 0 && this.elapsed >= this.duration) {
2863
+ const result2 = this.updateFn(dt, this.elapsed);
2864
+ if (this.loop && result2 !== true) {
2865
+ this.elapsed = this.elapsed % this.duration;
2866
+ return;
2867
+ }
2868
+ this.complete();
2869
+ return;
2870
+ }
2871
+ const result = this.updateFn(dt, this.elapsed);
2872
+ if (result === true) {
2873
+ if (this.loop) {
2874
+ this.elapsed = 0;
2875
+ return;
2876
+ }
2877
+ this.complete();
2878
+ }
2879
+ }
2880
+ /**
2881
+ * Reset the process to its initial state so it can be re-run.
2882
+ * @internal Used by Sequence for loop/repeat with direct instances.
2883
+ */
2884
+ _reset() {
2885
+ this.elapsed = 0;
2886
+ this._completed = false;
2887
+ this._paused = false;
2888
+ this._cancelled = false;
2889
+ delete this.resolvePromise;
2890
+ }
2891
+ complete() {
2892
+ this._completed = true;
2893
+ this.onCompleteFn?.();
2894
+ this.resolvePromise?.();
2895
+ }
2896
+ };
2897
+ var easeLinear = /* @__PURE__ */ __name((t) => t, "easeLinear");
2898
+ var easeInQuad = /* @__PURE__ */ __name((t) => t * t, "easeInQuad");
2899
+ var easeOutQuad = /* @__PURE__ */ __name((t) => t * (2 - t), "easeOutQuad");
2900
+ var easeInOutQuad = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, "easeInOutQuad");
2901
+ var easeOutBounce = /* @__PURE__ */ __name((t) => {
2902
+ if (t < 1 / 2.75) {
2903
+ return 7.5625 * t * t;
2904
+ } else if (t < 2 / 2.75) {
2905
+ const t2 = t - 1.5 / 2.75;
2906
+ return 7.5625 * t2 * t2 + 0.75;
2907
+ } else if (t < 2.5 / 2.75) {
2908
+ const t2 = t - 2.25 / 2.75;
2909
+ return 7.5625 * t2 * t2 + 0.9375;
2910
+ } else {
2911
+ const t2 = t - 2.625 / 2.75;
2912
+ return 7.5625 * t2 * t2 + 0.984375;
2913
+ }
2914
+ }, "easeOutBounce");
2915
+
1902
2916
  // src/LoadingScene.ts
1903
2917
  var LoadingScene = class extends Scene {
1904
2918
  static {
@@ -1923,6 +2937,7 @@ var LoadingScene = class extends Scene {
1923
2937
  _active = true;
1924
2938
  _continueRequested = false;
1925
2939
  _continueGate;
2940
+ _pendingWaits = /* @__PURE__ */ new Set();
1926
2941
  // Bumped on every `_run` attempt. `AssetManager.loadAll` uses `Promise.all`
1927
2942
  // under the hood, so individual loaders from a failed attempt can still
1928
2943
  // resolve and fire `onProgress` after the attempt rejects. Without this
@@ -1977,28 +2992,32 @@ var LoadingScene = class extends Scene {
1977
2992
  onExit() {
1978
2993
  this._active = false;
1979
2994
  this._continueGate?.();
2995
+ for (const wait of this._pendingWaits) {
2996
+ wait.cancel();
2997
+ }
2998
+ this._pendingWaits.clear();
1980
2999
  }
1981
3000
  async _run() {
1982
- await new Promise((resolve) => setTimeout(resolve, 0));
3001
+ await Promise.resolve();
1983
3002
  if (!this._active) return;
1984
3003
  const attempt = ++this._attempt;
1985
3004
  const target = typeof this.target === "function" ? this.target() : this.target;
1986
- const startedAt = performance.now();
1987
3005
  const bus = this.context.resolve(EventBusKey);
3006
+ const minDuration = this._createEngineTimeDelay(this.minDuration);
1988
3007
  try {
1989
3008
  await this.assets.loadAll(target.preload ?? [], (ratio) => {
1990
3009
  if (!this._active || attempt !== this._attempt) return;
1991
3010
  this._progress = ratio;
1992
3011
  bus.emit("scene:loading:progress", { scene: this, ratio });
1993
3012
  });
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;
3013
+ if (!this._active || attempt !== this._attempt) {
3014
+ minDuration.cancel();
3015
+ return;
2000
3016
  }
3017
+ await minDuration.promise;
3018
+ if (!this._active || attempt !== this._attempt) return;
2001
3019
  } catch (err) {
3020
+ minDuration.cancel();
2002
3021
  if (!this._active || attempt !== this._attempt) return;
2003
3022
  const error = err instanceof Error ? err : new Error(String(err));
2004
3023
  this._started = false;
@@ -2022,6 +3041,26 @@ var LoadingScene = class extends Scene {
2022
3041
  this.transition ? { transition: this.transition } : void 0
2023
3042
  );
2024
3043
  }
3044
+ _createEngineTimeDelay(ms) {
3045
+ if (ms <= 0) {
3046
+ return {
3047
+ promise: Promise.resolve(),
3048
+ cancel: /* @__PURE__ */ __name(() => {
3049
+ }, "cancel")
3050
+ };
3051
+ }
3052
+ const wait = Process.delay(ms);
3053
+ this._pendingWaits.add(wait);
3054
+ this.context.resolve(ProcessSystemKey).add(wait);
3055
+ return {
3056
+ promise: wait.toPromise().finally(() => {
3057
+ this._pendingWaits.delete(wait);
3058
+ }),
3059
+ cancel: /* @__PURE__ */ __name(() => {
3060
+ wait.cancel();
3061
+ }, "cancel")
3062
+ };
3063
+ }
2025
3064
  };
2026
3065
 
2027
3066
  // src/SceneTransition.ts
@@ -2427,177 +3466,55 @@ var SceneManager = class {
2427
3466
  kind: run.kind,
2428
3467
  engineContext: this._context,
2429
3468
  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
- }
2460
- /** Fire onPause() for scenes that transitioned from not-paused to paused. */
2461
- _firePauseTransitions(wasPaused) {
2462
- for (const scene of this.stack) {
2463
- const was = wasPaused.get(scene) ?? false;
2464
- if (scene.isPaused && !was) {
2465
- scene.onPause?.();
2466
- }
2467
- }
2468
- }
2469
- /** Fire onResume() for scenes that transitioned from paused to not-paused. */
2470
- _fireResumeTransitions(wasPaused) {
2471
- for (const scene of this.stack) {
2472
- const was = wasPaused.get(scene) ?? false;
2473
- if (!scene.isPaused && was) {
2474
- scene.onResume?.();
2475
- }
2476
- }
2477
- }
2478
- };
2479
-
2480
- // src/Process.ts
2481
- var Process = class _Process {
2482
- static {
2483
- __name(this, "Process");
2484
- }
2485
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
2486
- updateFn;
2487
- onCompleteFn;
2488
- duration;
2489
- loop;
2490
- /** Tags for filtering/grouping. */
2491
- tags;
2492
- elapsed = 0;
2493
- _completed = false;
2494
- _paused = false;
2495
- _cancelled = false;
2496
- resolvePromise;
2497
- /** Create a timer that fires `onComplete` after `duration` ms. */
2498
- static delay(duration, onComplete, tags) {
2499
- const opts = { duration };
2500
- if (onComplete !== void 0) opts.onComplete = onComplete;
2501
- if (tags !== void 0) opts.tags = tags;
2502
- return new _Process(opts);
2503
- }
2504
- constructor(options) {
2505
- this.updateFn = options.update ?? (() => {
2506
- });
2507
- this.onCompleteFn = options.onComplete;
2508
- this.duration = options.duration;
2509
- this.loop = options.loop ?? false;
2510
- this.tags = options.tags ?? [];
2511
- }
2512
- /** Whether the process has completed. */
2513
- get completed() {
2514
- return this._completed;
2515
- }
2516
- /** Whether the process is paused. */
2517
- get paused() {
2518
- return this._paused;
3469
+ toScene: run.toScene
3470
+ };
2519
3471
  }
2520
- /** Pause the process. */
2521
- pause() {
2522
- this._paused = true;
3472
+ _snapshotPauseStates() {
3473
+ return new Map(
3474
+ this.stack.map((scene) => [scene, scene.isPaused])
3475
+ );
2523
3476
  }
2524
- /** Resume the process. */
2525
- resume() {
2526
- this._paused = false;
3477
+ _assertNotMutating(method) {
3478
+ if (this._mutationDepth === 0) return;
3479
+ throw new Error(
3480
+ `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().`
3481
+ );
2527
3482
  }
2528
- /** Cancel the process. */
2529
- cancel() {
2530
- this._cancelled = true;
2531
- this._completed = true;
2532
- this.resolvePromise?.();
3483
+ async _withMutation(work) {
3484
+ this._mutationDepth++;
3485
+ try {
3486
+ return await work();
3487
+ } finally {
3488
+ this._mutationDepth--;
3489
+ }
2533
3490
  }
2534
- /** Returns a promise that resolves when the process completes or is cancelled. */
2535
- toPromise() {
2536
- if (this._completed) return Promise.resolve();
2537
- return new Promise((resolve) => {
2538
- this.resolvePromise = resolve;
2539
- });
3491
+ _withMutationSync(work) {
3492
+ this._mutationDepth++;
3493
+ try {
3494
+ return work();
3495
+ } finally {
3496
+ this._mutationDepth--;
3497
+ }
2540
3498
  }
2541
- /**
2542
- * Advance the process by dt milliseconds.
2543
- * @internal
2544
- */
2545
- _update(dt) {
2546
- if (this._completed || this._paused || this._cancelled) return;
2547
- this.elapsed += dt;
2548
- if (this.duration !== void 0 && this.elapsed >= this.duration) {
2549
- const result2 = this.updateFn(dt, this.elapsed);
2550
- if (this.loop && result2 !== true) {
2551
- this.elapsed = this.elapsed % this.duration;
2552
- return;
3499
+ /** Fire onPause() for scenes that transitioned from not-paused to paused. */
3500
+ _firePauseTransitions(wasPaused) {
3501
+ for (const scene of this.stack) {
3502
+ const was = wasPaused.get(scene) ?? false;
3503
+ if (scene.isPaused && !was) {
3504
+ scene.onPause?.();
2553
3505
  }
2554
- this.complete();
2555
- return;
2556
3506
  }
2557
- const result = this.updateFn(dt, this.elapsed);
2558
- if (result === true) {
2559
- if (this.loop) {
2560
- this.elapsed = 0;
2561
- return;
3507
+ }
3508
+ /** Fire onResume() for scenes that transitioned from paused to not-paused. */
3509
+ _fireResumeTransitions(wasPaused) {
3510
+ for (const scene of this.stack) {
3511
+ const was = wasPaused.get(scene) ?? false;
3512
+ if (!scene.isPaused && was) {
3513
+ scene.onResume?.();
2562
3514
  }
2563
- this.complete();
2564
3515
  }
2565
3516
  }
2566
- /**
2567
- * Reset the process to its initial state so it can be re-run.
2568
- * @internal Used by Sequence for loop/repeat with direct instances.
2569
- */
2570
- _reset() {
2571
- this.elapsed = 0;
2572
- this._completed = false;
2573
- this._paused = false;
2574
- this._cancelled = false;
2575
- delete this.resolvePromise;
2576
- }
2577
- complete() {
2578
- this._completed = true;
2579
- this.onCompleteFn?.();
2580
- this.resolvePromise?.();
2581
- }
2582
3517
  };
2583
- var easeLinear = /* @__PURE__ */ __name((t) => t, "easeLinear");
2584
- var easeInQuad = /* @__PURE__ */ __name((t) => t * t, "easeInQuad");
2585
- var easeOutQuad = /* @__PURE__ */ __name((t) => t * (2 - t), "easeOutQuad");
2586
- var easeInOutQuad = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, "easeInOutQuad");
2587
- var easeOutBounce = /* @__PURE__ */ __name((t) => {
2588
- if (t < 1 / 2.75) {
2589
- return 7.5625 * t * t;
2590
- } else if (t < 2 / 2.75) {
2591
- const t2 = t - 1.5 / 2.75;
2592
- return 7.5625 * t2 * t2 + 0.75;
2593
- } else if (t < 2.5 / 2.75) {
2594
- const t2 = t - 2.25 / 2.75;
2595
- return 7.5625 * t2 * t2 + 0.9375;
2596
- } else {
2597
- const t2 = t - 2.625 / 2.75;
2598
- return 7.5625 * t2 * t2 + 0.984375;
2599
- }
2600
- }, "easeOutBounce");
2601
3518
 
2602
3519
  // src/Tween.ts
2603
3520
  var Tween = {
@@ -2824,7 +3741,9 @@ var ProcessSlot = class {
2824
3741
  };
2825
3742
 
2826
3743
  // src/ProcessComponent.ts
2827
- var ProcessComponent = class extends Component {
3744
+ var _ProcessComponent_decorators, _init2, _a2;
3745
+ _ProcessComponent_decorators = [serializable];
3746
+ var ProcessComponent = class extends (_a2 = Component) {
2828
3747
  static {
2829
3748
  __name(this, "ProcessComponent");
2830
3749
  }
@@ -2894,10 +3813,18 @@ var ProcessComponent = class extends Component {
2894
3813
  onDestroy() {
2895
3814
  this.cancel();
2896
3815
  }
3816
+ serialize() {
3817
+ return null;
3818
+ }
2897
3819
  };
3820
+ _init2 = __decoratorStart(_a2);
3821
+ ProcessComponent = __decorateElement(_init2, 0, "ProcessComponent", _ProcessComponent_decorators, ProcessComponent);
3822
+ __runInitializers(_init2, 1, ProcessComponent);
2898
3823
 
2899
3824
  // src/KeyframeAnimator.ts
2900
- var KeyframeAnimator = class extends Component {
3825
+ var _KeyframeAnimator_decorators, _init3, _a3;
3826
+ _KeyframeAnimator_decorators = [serializable];
3827
+ var KeyframeAnimator = class extends (_a3 = Component) {
2901
3828
  static {
2902
3829
  __name(this, "KeyframeAnimator");
2903
3830
  }
@@ -2949,6 +3876,9 @@ var KeyframeAnimator = class extends Component {
2949
3876
  onDestroy() {
2950
3877
  this.stopAll();
2951
3878
  }
3879
+ serialize() {
3880
+ return null;
3881
+ }
2952
3882
  stopInternal(name, complete) {
2953
3883
  const process = this.active.get(name);
2954
3884
  if (!process) return;
@@ -2957,6 +3887,9 @@ var KeyframeAnimator = class extends Component {
2957
3887
  this.defs[name]?.onExit?.(complete);
2958
3888
  }
2959
3889
  };
3890
+ _init3 = __decoratorStart(_a3);
3891
+ KeyframeAnimator = __decorateElement(_init3, 0, "KeyframeAnimator", _KeyframeAnimator_decorators, KeyframeAnimator);
3892
+ __runInitializers(_init3, 1, KeyframeAnimator);
2960
3893
 
2961
3894
  // src/Sequence.ts
2962
3895
  var Sequence = class {
@@ -3108,36 +4041,94 @@ var ProcessSystem = class extends System {
3108
4041
  /** Global time scale multiplier. Stacks multiplicatively with per-scene timeScale. */
3109
4042
  timeScale = 1;
3110
4043
  sceneManager;
3111
- sceneProcesses = /* @__PURE__ */ new Set();
4044
+ globalProcesses = /* @__PURE__ */ new Set();
4045
+ scenePools = /* @__PURE__ */ new Map();
4046
+ _unregisterSceneHook = null;
3112
4047
  onRegister(context) {
3113
4048
  this.sceneManager = context.resolve(SceneManagerKey);
4049
+ const hooks = context.tryResolve(SceneHookRegistryKey);
4050
+ this._unregisterSceneHook = hooks?.register({
4051
+ afterExit: /* @__PURE__ */ __name((scene) => this.cancelForScene(scene), "afterExit")
4052
+ }) ?? null;
4053
+ }
4054
+ onUnregister() {
4055
+ this._unregisterSceneHook?.();
4056
+ this._unregisterSceneHook = null;
4057
+ for (const p of this.globalProcesses) {
4058
+ if (!p.completed) p.cancel();
4059
+ }
4060
+ this.globalProcesses.clear();
4061
+ for (const pool of this.scenePools.values()) {
4062
+ for (const p of pool) {
4063
+ if (!p.completed) p.cancel();
4064
+ }
4065
+ }
4066
+ this.scenePools.clear();
3114
4067
  }
3115
- /** Add a scene-level process (not tied to any entity). */
4068
+ /**
4069
+ * Add an engine-global process. Ticked under the global timeScale only;
4070
+ * NOT gated by per-scene pause or scaled by per-scene timeScale. Use this
4071
+ * for cross-scene effects (e.g. screen-scope filter fades on `app.stage`)
4072
+ * or processes that have no owning scene.
4073
+ */
3116
4074
  add(process) {
3117
- this.sceneProcesses.add(process);
4075
+ this.globalProcesses.add(process);
4076
+ return process;
4077
+ }
4078
+ /**
4079
+ * Add a process bound to a specific scene's lifecycle. Ticked only while
4080
+ * the scene is active (not paused) and scaled by the scene's `timeScale`,
4081
+ * exactly like an entity-owned `ProcessComponent`. Use this for layer or
4082
+ * scene-scope effect fades that should pause with the scene.
4083
+ */
4084
+ addForScene(scene, process) {
4085
+ let pool = this.scenePools.get(scene);
4086
+ if (!pool) {
4087
+ pool = /* @__PURE__ */ new Set();
4088
+ this.scenePools.set(scene, pool);
4089
+ }
4090
+ pool.add(process);
3118
4091
  return process;
3119
4092
  }
3120
- /** Cancel scene-level processes, optionally by tag. */
4093
+ /** Cancel engine-global processes, optionally by tag. */
3121
4094
  cancel(tag) {
3122
- for (const p of this.sceneProcesses) {
4095
+ for (const p of this.globalProcesses) {
3123
4096
  if (tag === void 0 || p.tags.includes(tag)) {
3124
4097
  p.cancel();
4098
+ this.globalProcesses.delete(p);
3125
4099
  }
3126
4100
  }
3127
- if (tag === void 0) {
3128
- this.sceneProcesses.clear();
4101
+ }
4102
+ /** Cancel every scene-bound process for `scene`, optionally by tag. */
4103
+ cancelForScene(scene, tag) {
4104
+ const pool = this.scenePools.get(scene);
4105
+ if (!pool) return;
4106
+ for (const p of pool) {
4107
+ if (tag === void 0 || p.tags.includes(tag)) {
4108
+ p.cancel();
4109
+ pool.delete(p);
4110
+ }
3129
4111
  }
4112
+ if (pool.size === 0) this.scenePools.delete(scene);
3130
4113
  }
3131
4114
  update(dt) {
3132
4115
  const globalScaledDt = dt * this.timeScale;
3133
- for (const p of this.sceneProcesses) {
4116
+ for (const p of this.globalProcesses) {
3134
4117
  p._update(globalScaledDt);
3135
4118
  if (p.completed) {
3136
- this.sceneProcesses.delete(p);
4119
+ this.globalProcesses.delete(p);
3137
4120
  }
3138
4121
  }
3139
4122
  for (const scene of this.sceneManager.activeScenes) {
3140
4123
  const effectiveDt = globalScaledDt * scene.timeScale;
4124
+ const pool = this.scenePools.get(scene);
4125
+ if (pool) {
4126
+ for (const p of pool) {
4127
+ p._update(effectiveDt);
4128
+ if (p.completed) pool.delete(p);
4129
+ }
4130
+ if (pool.size === 0) this.scenePools.delete(scene);
4131
+ }
3141
4132
  for (const entity of scene.getEntities()) {
3142
4133
  if (entity.isDestroyed) continue;
3143
4134
  const pc = entity.tryGet(ProcessComponent);
@@ -3148,145 +4139,45 @@ var ProcessSystem = class extends System {
3148
4139
  }
3149
4140
  };
3150
4141
 
3151
- // src/Inspector.ts
3152
- var Inspector = class {
3153
- static {
3154
- __name(this, "Inspector");
3155
- }
3156
- engine;
3157
- constructor(engine) {
3158
- this.engine = engine;
3159
- }
3160
- /** Full state snapshot (serializable). */
3161
- snapshot() {
3162
- return {
3163
- frameCount: this.engine.loop.frameCount,
3164
- sceneStack: this.getSceneStack(),
3165
- entityCount: this.countEntities(),
3166
- systemCount: this.getSystems().length,
3167
- errors: this.getErrors()
3168
- };
3169
- }
3170
- /** Find entity by name in the active scene. */
3171
- getEntityByName(name) {
3172
- const entity = this.findActiveEntity(name);
3173
- if (!entity) return void 0;
3174
- return this.entityToSnapshot(entity);
3175
- }
3176
- /** Get entity position (from Transform component). */
3177
- getEntityPosition(name) {
3178
- const entity = this.findActiveEntity(name);
3179
- if (!entity) return void 0;
3180
- const transform = this.getTransform(entity);
3181
- if (!transform) return void 0;
3182
- return { x: transform.position.x, y: transform.position.y };
3183
- }
3184
- /** Check if an entity has a component by class name string. */
3185
- hasComponent(entityName, componentClass) {
3186
- return this.findComponentByName(entityName, componentClass) !== void 0;
3187
- }
3188
- /** Get component data (serializable subset) by class name string. */
3189
- getComponentData(entityName, componentClass) {
3190
- const comp = this.findComponentByName(entityName, componentClass);
3191
- if (!comp) return void 0;
3192
- return this.serializeComponent(comp);
3193
- }
3194
- /** Get all entities in the active scene as snapshots. */
3195
- getEntities() {
3196
- const scene = this.engine.scenes.active;
3197
- if (!scene) return [];
3198
- const result = [];
3199
- for (const entity of scene.getEntities()) {
3200
- if (!entity.isDestroyed) {
3201
- result.push(this.entityToSnapshot(entity));
4142
+ // src/ProcessQueue.ts
4143
+ function makeQueue(route) {
4144
+ const ours = /* @__PURE__ */ new Set();
4145
+ return {
4146
+ run(p) {
4147
+ for (const old of ours) {
4148
+ if (old.completed) ours.delete(old);
3202
4149
  }
3203
- }
3204
- return result;
3205
- }
3206
- /** Get scene stack info. */
3207
- getSceneStack() {
3208
- return this.engine.scenes.all.map((scene) => ({
3209
- name: scene.name,
3210
- entityCount: scene.getEntities().size,
3211
- paused: scene.isPaused
3212
- }));
3213
- }
3214
- /** Get active system info. */
3215
- getSystems() {
3216
- const scheduler = this.engine.context.tryResolve(SystemSchedulerKey);
3217
- if (!scheduler) return [];
3218
- return scheduler.getAllSystems().map((sys) => ({
3219
- name: sys.constructor.name,
3220
- phase: sys.phase,
3221
- priority: sys.priority,
3222
- enabled: sys.enabled
3223
- }));
3224
- }
3225
- /** Get disabled components/systems from error boundary. */
3226
- getErrors() {
3227
- const boundary = this.engine.context.tryResolve(ErrorBoundaryKey);
3228
- if (!boundary) return { disabledSystems: [], disabledComponents: [] };
3229
- const disabled = boundary.getDisabled();
3230
- return {
3231
- disabledSystems: disabled.systems.map(
3232
- (s) => s.system.constructor.name
3233
- ),
3234
- disabledComponents: disabled.components.map((c) => ({
3235
- entity: c.component.entity?.name ?? "unknown",
3236
- component: c.component.constructor.name,
3237
- error: c.error
3238
- }))
3239
- };
3240
- }
3241
- findActiveEntity(name) {
3242
- return this.engine.scenes.active?.findEntity(name);
3243
- }
3244
- findComponentByName(entityName, componentClass) {
3245
- const entity = this.findActiveEntity(entityName);
3246
- if (!entity) return void 0;
3247
- for (const comp of entity.getAll()) {
3248
- if (comp.constructor.name === componentClass) return comp;
3249
- }
3250
- return void 0;
3251
- }
3252
- entityToSnapshot(entity) {
3253
- const transform = this.getTransform(entity);
3254
- const snapshot = {
3255
- id: entity.id,
3256
- name: entity.name,
3257
- tags: [...entity.tags],
3258
- components: [...entity.getAll()].map((c) => c.constructor.name)
3259
- };
3260
- if (transform) {
3261
- snapshot.position = {
3262
- x: transform.position.x,
3263
- y: transform.position.y
3264
- };
3265
- }
3266
- return snapshot;
3267
- }
3268
- getTransform(entity) {
3269
- return entity.has(Transform) ? entity.get(Transform) : void 0;
3270
- }
3271
- serializeComponent(comp) {
3272
- const result = {};
3273
- for (const key of Object.getOwnPropertyNames(comp)) {
3274
- if (key === "entity") continue;
3275
- const value = comp[key];
3276
- if (typeof value !== "function") {
3277
- result[key] = value;
4150
+ route(p);
4151
+ ours.add(p);
4152
+ return p;
4153
+ },
4154
+ cancelAll() {
4155
+ for (const p of ours) {
4156
+ if (!p.completed) p.cancel();
3278
4157
  }
4158
+ ours.clear();
3279
4159
  }
3280
- return result;
3281
- }
3282
- countEntities() {
3283
- let count = 0;
3284
- for (const scene of this.engine.scenes.all) {
3285
- count += scene.getEntities().size;
3286
- }
3287
- return count;
3288
- }
3289
- };
4160
+ };
4161
+ }
4162
+ __name(makeQueue, "makeQueue");
4163
+ function makeEntityScopedQueue(entity) {
4164
+ return makeQueue((p) => {
4165
+ let pc = entity.tryGet(ProcessComponent);
4166
+ if (!pc) {
4167
+ pc = entity.add(new ProcessComponent());
4168
+ }
4169
+ pc.run(p);
4170
+ });
4171
+ }
4172
+ __name(makeEntityScopedQueue, "makeEntityScopedQueue");
4173
+ function makeSceneScopedQueue(processSystem, scene) {
4174
+ return makeQueue((p) => processSystem.addForScene(scene, p));
4175
+ }
4176
+ __name(makeSceneScopedQueue, "makeSceneScopedQueue");
4177
+ function makeGlobalScopedQueue(processSystem) {
4178
+ return makeQueue((p) => processSystem.add(p));
4179
+ }
4180
+ __name(makeGlobalScopedQueue, "makeGlobalScopedQueue");
3290
4181
 
3291
4182
  // src/Engine.ts
3292
4183
  var Engine = class {
@@ -3340,6 +4231,15 @@ var Engine = class {
3340
4231
  this.context.register(SystemSchedulerKey, this.scheduler);
3341
4232
  this.context.register(AssetManagerKey, this.assets);
3342
4233
  this.context.register(SceneHookRegistryKey, this.sceneHooks);
4234
+ this.sceneHooks.register({
4235
+ beforeEnter: /* @__PURE__ */ __name((scene) => {
4236
+ scene._registerScoped(RandomKey, this.inspector.createSceneRandom());
4237
+ this.inspector.attachSceneEventObserver(scene);
4238
+ }, "beforeEnter"),
4239
+ afterExit: /* @__PURE__ */ __name((scene) => {
4240
+ this.inspector.detachSceneEventObserver(scene);
4241
+ }, "afterExit")
4242
+ });
3343
4243
  this.scenes._setContext(this.context);
3344
4244
  this.registerBuiltInSystems();
3345
4245
  this.loop.setCallbacks({
@@ -3421,6 +4321,7 @@ var Engine = class {
3421
4321
  if (this.debug && typeof globalThis !== "undefined" && "__yage__" in globalThis) {
3422
4322
  delete globalThis["__yage__"];
3423
4323
  }
4324
+ this.inspector.dispose();
3424
4325
  this.events.clear();
3425
4326
  this.started = false;
3426
4327
  }
@@ -3518,6 +4419,7 @@ function createMockScene(name = "mock-scene") {
3518
4419
  ctx.register(ErrorBoundaryKey, boundary);
3519
4420
  const scene = new _TestScene(name);
3520
4421
  scene._setContext(ctx);
4422
+ scene._registerScoped(RandomKey, createRandomService(1234));
3521
4423
  return { scene, context: ctx };
3522
4424
  }
3523
4425
  __name(createMockScene, "createMockScene");
@@ -3571,6 +4473,7 @@ export {
3571
4473
  QueryCache,
3572
4474
  QueryCacheKey,
3573
4475
  QueryResult,
4476
+ RandomKey,
3574
4477
  RendererAdapterKey,
3575
4478
  SERIALIZABLE_KEY,
3576
4479
  Scene,
@@ -3592,9 +4495,11 @@ export {
3592
4495
  Vec2,
3593
4496
  _resetEntityIdCounter,
3594
4497
  advanceFrames,
4498
+ createDefaultRandomSeed,
3595
4499
  createKeyframeTrack,
3596
4500
  createMockEntity,
3597
4501
  createMockScene,
4502
+ createRandomService,
3598
4503
  createTestEngine,
3599
4504
  defineBlueprint,
3600
4505
  defineEvent,
@@ -3606,8 +4511,13 @@ export {
3606
4511
  easeOutQuad,
3607
4512
  filterEntities,
3608
4513
  getSerializableType,
4514
+ globalRandom,
3609
4515
  interpolate,
3610
4516
  isSerializable,
4517
+ makeEntityScopedQueue,
4518
+ makeGlobalScopedQueue,
4519
+ makeSceneScopedQueue,
4520
+ normalizeSeed,
3611
4521
  resolveTransition,
3612
4522
  serializable,
3613
4523
  trait