@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.cjs CHANGED
@@ -99,6 +99,7 @@ __export(index_exports, {
99
99
  QueryCache: () => QueryCache,
100
100
  QueryCacheKey: () => QueryCacheKey,
101
101
  QueryResult: () => QueryResult,
102
+ RandomKey: () => RandomKey,
102
103
  RendererAdapterKey: () => RendererAdapterKey,
103
104
  SERIALIZABLE_KEY: () => SERIALIZABLE_KEY,
104
105
  Scene: () => Scene,
@@ -120,9 +121,11 @@ __export(index_exports, {
120
121
  Vec2: () => Vec2,
121
122
  _resetEntityIdCounter: () => _resetEntityIdCounter,
122
123
  advanceFrames: () => advanceFrames,
124
+ createDefaultRandomSeed: () => createDefaultRandomSeed,
123
125
  createKeyframeTrack: () => createKeyframeTrack,
124
126
  createMockEntity: () => createMockEntity,
125
127
  createMockScene: () => createMockScene,
128
+ createRandomService: () => createRandomService,
126
129
  createTestEngine: () => createTestEngine,
127
130
  defineBlueprint: () => defineBlueprint,
128
131
  defineEvent: () => defineEvent,
@@ -134,8 +137,13 @@ __export(index_exports, {
134
137
  easeOutQuad: () => easeOutQuad,
135
138
  filterEntities: () => filterEntities,
136
139
  getSerializableType: () => getSerializableType,
140
+ globalRandom: () => globalRandom,
137
141
  interpolate: () => interpolate,
138
142
  isSerializable: () => isSerializable,
143
+ makeEntityScopedQueue: () => makeEntityScopedQueue,
144
+ makeGlobalScopedQueue: () => makeGlobalScopedQueue,
145
+ makeSceneScopedQueue: () => makeSceneScopedQueue,
146
+ normalizeSeed: () => normalizeSeed,
139
147
  resolveTransition: () => resolveTransition,
140
148
  serializable: () => serializable,
141
149
  trait: () => trait
@@ -327,14 +335,6 @@ var MathUtils = {
327
335
  const wrapped = MathUtils.wrap(t, 0, length * 2);
328
336
  return length - Math.abs(wrapped - length);
329
337
  },
330
- /** Random float in [min, max). */
331
- randomRange(min, max) {
332
- return min + Math.random() * (max - min);
333
- },
334
- /** Random integer in [min, max] (inclusive). */
335
- randomInt(min, max) {
336
- return Math.floor(min + Math.random() * (max - min + 1));
337
- },
338
338
  /** Convert degrees to radians. */
339
339
  degToRad(degrees) {
340
340
  return degrees * Math.PI / 180;
@@ -385,12 +385,138 @@ var MathUtils = {
385
385
  }
386
386
  };
387
387
 
388
+ // src/EngineContext.ts
389
+ var ServiceKey = class {
390
+ constructor(id, options) {
391
+ this.id = id;
392
+ this.scope = options?.scope ?? "engine";
393
+ }
394
+ id;
395
+ static {
396
+ __name(this, "ServiceKey");
397
+ }
398
+ /** Declared scope (engine or scene). Defaults to `"engine"`. */
399
+ scope;
400
+ };
401
+ var EngineContext = class {
402
+ static {
403
+ __name(this, "EngineContext");
404
+ }
405
+ services = /* @__PURE__ */ new Map();
406
+ /** Register a service. Throws if the key is already registered. */
407
+ register(key, service) {
408
+ if (this.services.has(key.id)) {
409
+ throw new Error(`Service "${key.id}" is already registered.`);
410
+ }
411
+ this.services.set(key.id, service);
412
+ }
413
+ /** Resolve a service. Throws if not registered. */
414
+ resolve(key) {
415
+ if (!this.services.has(key.id)) {
416
+ throw new Error(`Service "${key.id}" is not registered.`);
417
+ }
418
+ return this.services.get(key.id);
419
+ }
420
+ /** Resolve a service, returning undefined if not registered. */
421
+ tryResolve(key) {
422
+ return this.services.get(key.id);
423
+ }
424
+ /** Remove a registered service. No-op if not registered. */
425
+ unregister(key) {
426
+ this.services.delete(key.id);
427
+ }
428
+ /** Check if a service is registered. */
429
+ has(key) {
430
+ return this.services.has(key.id);
431
+ }
432
+ };
433
+ var EngineKey = new ServiceKey("engine");
434
+ var EventBusKey = new ServiceKey("eventBus");
435
+ var SceneManagerKey = new ServiceKey("sceneManager");
436
+ var LoggerKey = new ServiceKey("logger");
437
+ var InspectorKey = new ServiceKey("inspector");
438
+ var QueryCacheKey = new ServiceKey("queryCache");
439
+ var ErrorBoundaryKey = new ServiceKey("errorBoundary");
440
+ var GameLoopKey = new ServiceKey("gameLoop");
441
+ var SystemSchedulerKey = new ServiceKey(
442
+ "systemScheduler"
443
+ );
444
+ var ProcessSystemKey = new ServiceKey("processSystem");
445
+ var AssetManagerKey = new ServiceKey("assetManager");
446
+
447
+ // src/Random.ts
448
+ var RandomKey = new ServiceKey("random", {
449
+ scope: "scene"
450
+ });
451
+ var UINT32_MAX = 4294967296;
452
+ function normalizeSeed(seed) {
453
+ return seed >>> 0;
454
+ }
455
+ __name(normalizeSeed, "normalizeSeed");
456
+ function createDefaultRandomSeed() {
457
+ return normalizeSeed(Date.now() ^ Math.floor(Math.random() * 1e9));
458
+ }
459
+ __name(createDefaultRandomSeed, "createDefaultRandomSeed");
460
+ var Mulberry32Random = class {
461
+ static {
462
+ __name(this, "Mulberry32Random");
463
+ }
464
+ seed;
465
+ state;
466
+ constructor(seed) {
467
+ const normalized = normalizeSeed(seed);
468
+ this.seed = normalized;
469
+ this.state = normalized;
470
+ }
471
+ float() {
472
+ let t = this.state += 1831565813;
473
+ t = Math.imul(t ^ t >>> 15, t | 1);
474
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
475
+ return ((t ^ t >>> 14) >>> 0) / UINT32_MAX;
476
+ }
477
+ range(min, max) {
478
+ return min + this.float() * (max - min);
479
+ }
480
+ int(min, max) {
481
+ return Math.floor(this.range(min, max + 1));
482
+ }
483
+ pick(arr) {
484
+ if (arr.length === 0) {
485
+ throw new Error("RandomService.pick() requires a non-empty array.");
486
+ }
487
+ return arr[this.int(0, arr.length - 1)];
488
+ }
489
+ shuffle(arr) {
490
+ for (let i = arr.length - 1; i > 0; i--) {
491
+ const j = this.int(0, i);
492
+ const tmp = arr[i];
493
+ arr[i] = arr[j];
494
+ arr[j] = tmp;
495
+ }
496
+ return arr;
497
+ }
498
+ setSeed(seed) {
499
+ const normalized = normalizeSeed(seed);
500
+ this.seed = normalized;
501
+ this.state = normalized;
502
+ }
503
+ getSeed() {
504
+ return this.seed;
505
+ }
506
+ };
507
+ function createRandomService(seed = createDefaultRandomSeed()) {
508
+ return new Mulberry32Random(seed);
509
+ }
510
+ __name(createRandomService, "createRandomService");
511
+ var globalRandom = createRandomService();
512
+
388
513
  // src/EventBus.ts
389
514
  var EventBus = class {
390
515
  static {
391
516
  __name(this, "EventBus");
392
517
  }
393
518
  handlers = /* @__PURE__ */ new Map();
519
+ observers = /* @__PURE__ */ new Set();
394
520
  /** Subscribe to an event. Returns an unsubscribe function. */
395
521
  on(event, handler) {
396
522
  let list = this.handlers.get(event);
@@ -417,6 +543,12 @@ var EventBus = class {
417
543
  }
418
544
  /** Emit an event. Handlers are called synchronously in registration order. */
419
545
  emit(event, data) {
546
+ if (this.observers.size > 0) {
547
+ const observers = [...this.observers];
548
+ for (const observer of observers) {
549
+ observer(event, data);
550
+ }
551
+ }
420
552
  const list = this.handlers.get(event);
421
553
  if (!list) return;
422
554
  const snapshot = [...list];
@@ -424,6 +556,16 @@ var EventBus = class {
424
556
  handler(data);
425
557
  }
426
558
  }
559
+ /**
560
+ * Observe every emitted event without affecting handler order or control
561
+ * flow. Used by tooling such as the Inspector event log.
562
+ */
563
+ tap(observer) {
564
+ this.observers.add(observer);
565
+ return () => {
566
+ this.observers.delete(observer);
567
+ };
568
+ }
427
569
  /** Remove all handlers for an event, or all handlers if no event specified. */
428
570
  clear(event) {
429
571
  if (event !== void 0) {
@@ -533,63 +675,6 @@ var Logger = class {
533
675
  }
534
676
  };
535
677
 
536
- // src/EngineContext.ts
537
- var ServiceKey = class {
538
- constructor(id, options) {
539
- this.id = id;
540
- this.scope = options?.scope ?? "engine";
541
- }
542
- id;
543
- static {
544
- __name(this, "ServiceKey");
545
- }
546
- /** Declared scope (engine or scene). Defaults to `"engine"`. */
547
- scope;
548
- };
549
- var EngineContext = class {
550
- static {
551
- __name(this, "EngineContext");
552
- }
553
- services = /* @__PURE__ */ new Map();
554
- /** Register a service. Throws if the key is already registered. */
555
- register(key, service) {
556
- if (this.services.has(key.id)) {
557
- throw new Error(`Service "${key.id}" is already registered.`);
558
- }
559
- this.services.set(key.id, service);
560
- }
561
- /** Resolve a service. Throws if not registered. */
562
- resolve(key) {
563
- if (!this.services.has(key.id)) {
564
- throw new Error(`Service "${key.id}" is not registered.`);
565
- }
566
- return this.services.get(key.id);
567
- }
568
- /** Resolve a service, returning undefined if not registered. */
569
- tryResolve(key) {
570
- return this.services.get(key.id);
571
- }
572
- /** Remove a registered service. No-op if not registered. */
573
- unregister(key) {
574
- this.services.delete(key.id);
575
- }
576
- /** Check if a service is registered. */
577
- has(key) {
578
- return this.services.has(key.id);
579
- }
580
- };
581
- var EngineKey = new ServiceKey("engine");
582
- var EventBusKey = new ServiceKey("eventBus");
583
- var SceneManagerKey = new ServiceKey("sceneManager");
584
- var LoggerKey = new ServiceKey("logger");
585
- var InspectorKey = new ServiceKey("inspector");
586
- var QueryCacheKey = new ServiceKey("queryCache");
587
- var ErrorBoundaryKey = new ServiceKey("errorBoundary");
588
- var GameLoopKey = new ServiceKey("gameLoop");
589
- var SystemSchedulerKey = new ServiceKey("systemScheduler");
590
- var ProcessSystemKey = new ServiceKey("processSystem");
591
- var AssetManagerKey = new ServiceKey("assetManager");
592
-
593
678
  // src/SceneHooks.ts
594
679
  var SceneHookRegistry = class {
595
680
  static {
@@ -1326,6 +1411,7 @@ var Entity = class {
1326
1411
  }
1327
1412
  }
1328
1413
  this._scene?._onEntityEvent(token.name, data, this);
1414
+ this._scene?._observeEntityEvent(token.name, data, this);
1329
1415
  }
1330
1416
  /** Get all components as an iterable. */
1331
1417
  getAll() {
@@ -1748,87 +1834,886 @@ var GameLoop = class {
1748
1834
  }
1749
1835
  };
1750
1836
 
1751
- // src/Scene.ts
1752
- var Scene = class {
1837
+ // src/Inspector.ts
1838
+ var InputManagerRuntimeKey = new ServiceKey("inputManager");
1839
+ var PhysicsWorldManagerRuntimeKey = new ServiceKey(
1840
+ "physicsWorldManager"
1841
+ );
1842
+ var RendererRuntimeKey = new ServiceKey("renderer");
1843
+ var Inspector = class {
1753
1844
  static {
1754
- __name(this, "Scene");
1845
+ __name(this, "Inspector");
1755
1846
  }
1756
- /** Whether scenes below this one in the stack should be paused. Default: true. */
1757
- pauseBelow = true;
1758
- /** Whether scenes below this one should still render. Default: false. */
1759
- transparentBelow = false;
1760
- /** Asset handles to load before onEnter(). Override in subclasses. */
1761
- preload;
1762
- /** Default transition used when this scene is the destination of a push/pop/replace. */
1763
- defaultTransition;
1764
- /** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
1765
- paused = false;
1766
- /** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
1767
- timeScale = 1;
1768
- entities = /* @__PURE__ */ new Set();
1769
- destroyQueue = [];
1770
- _context;
1771
- entityCallbacks;
1772
- queryCache;
1773
- bus;
1774
- _entityEventHandlers;
1775
- _scopedServices;
1776
- /** Access the EngineContext. */
1777
- get context() {
1778
- return this._context;
1847
+ engine;
1848
+ extensions = /* @__PURE__ */ new Map();
1849
+ sceneIds = /* @__PURE__ */ new WeakMap();
1850
+ nextSceneId = 0;
1851
+ defaultSceneSeed;
1852
+ sceneSeedOverride;
1853
+ timeController = null;
1854
+ eventLogEnabled = false;
1855
+ eventCapacity = 500;
1856
+ /**
1857
+ * Ring buffer of recent events. `eventLogHead` points at the oldest slot;
1858
+ * a full ring contains exactly `eventCapacity` entries. We avoid `splice` to
1859
+ * keep `appendEvent` O(1) the previous shift-on-overflow approach was
1860
+ * O(n) per event once the buffer was full.
1861
+ */
1862
+ eventLog = [];
1863
+ eventLogHead = 0;
1864
+ eventWaiters = /* @__PURE__ */ new Set();
1865
+ detachBusTap = null;
1866
+ busEventObserver = /* @__PURE__ */ __name((event, data) => {
1867
+ this.recordBusEvent(String(event), data);
1868
+ }, "busEventObserver");
1869
+ sceneEventObserver = /* @__PURE__ */ __name((eventName, data, entity) => {
1870
+ this.recordEntityEvent(eventName, data, entity);
1871
+ }, "sceneEventObserver");
1872
+ time = {
1873
+ freeze: /* @__PURE__ */ __name(() => {
1874
+ this.requireTimeController().freeze();
1875
+ }, "freeze"),
1876
+ thaw: /* @__PURE__ */ __name(() => {
1877
+ this.requireTimeController().thaw();
1878
+ }, "thaw"),
1879
+ step: /* @__PURE__ */ __name((frames = 1) => {
1880
+ this.assertNonNegativeInteger(frames, "Inspector.time.step(frames)");
1881
+ if (frames === 0) return;
1882
+ this.requireTimeController().stepFrames(frames);
1883
+ this.expireDeadlineWaiters();
1884
+ }, "step"),
1885
+ setDelta: /* @__PURE__ */ __name((ms) => {
1886
+ if (!Number.isFinite(ms) || ms <= 0) {
1887
+ throw new Error("Inspector.time.setDelta(ms) requires a positive number.");
1888
+ }
1889
+ this.requireTimeController().setDelta(ms);
1890
+ }, "setDelta"),
1891
+ isFrozen: /* @__PURE__ */ __name(() => this.timeController?.isFrozen ?? false, "isFrozen"),
1892
+ getFrame: /* @__PURE__ */ __name(() => this.timeController?.getFrame() ?? this.engine.loop.frameCount, "getFrame")
1893
+ };
1894
+ input = {
1895
+ keyDown: /* @__PURE__ */ __name((code) => {
1896
+ this.requireInputManager().fireKeyDown(code);
1897
+ }, "keyDown"),
1898
+ keyUp: /* @__PURE__ */ __name((code) => {
1899
+ this.requireInputManager().fireKeyUp(code);
1900
+ }, "keyUp"),
1901
+ mouseMove: /* @__PURE__ */ __name((x, y) => {
1902
+ this.requireInputManager().firePointerMove(x, y);
1903
+ }, "mouseMove"),
1904
+ mouseDown: /* @__PURE__ */ __name((button = 0) => {
1905
+ this.requireInputManager().firePointerDown(button);
1906
+ }, "mouseDown"),
1907
+ mouseUp: /* @__PURE__ */ __name((button = 0) => {
1908
+ this.requireInputManager().firePointerUp(button);
1909
+ }, "mouseUp"),
1910
+ gamepadButton: /* @__PURE__ */ __name((idx, pressed) => {
1911
+ this.requireInputManager().fireGamepadButton(idx, pressed);
1912
+ }, "gamepadButton"),
1913
+ gamepadAxis: /* @__PURE__ */ __name((idx, value) => {
1914
+ this.requireInputManager().fireGamepadAxis(idx, value);
1915
+ }, "gamepadAxis"),
1916
+ tap: /* @__PURE__ */ __name((code, frames = 1) => {
1917
+ this.assertNonNegativeInteger(frames, "Inspector.input.tap(frames)");
1918
+ const input = this.requireInputManager();
1919
+ input.fireKeyDown(code);
1920
+ try {
1921
+ this.time.step(frames);
1922
+ } finally {
1923
+ input.fireKeyUp(code);
1924
+ }
1925
+ }, "tap"),
1926
+ hold: /* @__PURE__ */ __name((code, frames) => {
1927
+ this.assertNonNegativeInteger(frames, "Inspector.input.hold(frames)");
1928
+ const input = this.requireInputManager();
1929
+ input.fireKeyDown(code);
1930
+ try {
1931
+ this.time.step(frames);
1932
+ } finally {
1933
+ input.fireKeyUp(code);
1934
+ }
1935
+ }, "hold"),
1936
+ fireAction: /* @__PURE__ */ __name((name, frames = 1) => {
1937
+ this.assertNonNegativeInteger(
1938
+ frames,
1939
+ "Inspector.input.fireAction(frames)"
1940
+ );
1941
+ const input = this.requireInputManager();
1942
+ for (let i = 0; i < frames; i++) {
1943
+ input.fireAction(name);
1944
+ this.time.step(1);
1945
+ }
1946
+ }, "fireAction"),
1947
+ clearAll: /* @__PURE__ */ __name(() => {
1948
+ this.requireInputManager().clearAll();
1949
+ }, "clearAll")
1950
+ };
1951
+ events = {
1952
+ getLog: /* @__PURE__ */ __name(() => this.iterateLog().map(({ entry }) => ({ ...entry })), "getLog"),
1953
+ clearLog: /* @__PURE__ */ __name(() => {
1954
+ this.eventLog.length = 0;
1955
+ this.eventLogHead = 0;
1956
+ }, "clearLog"),
1957
+ setCapacity: /* @__PURE__ */ __name((n) => {
1958
+ this.assertNonNegativeInteger(
1959
+ n,
1960
+ "Inspector.events.setCapacity(capacity)"
1961
+ );
1962
+ const ordered = n === 0 ? [] : this.iterateLog().slice(-n);
1963
+ this.eventCapacity = n;
1964
+ this.eventLog = ordered;
1965
+ this.eventLogHead = 0;
1966
+ }, "setCapacity"),
1967
+ waitFor: /* @__PURE__ */ __name((pattern, options) => {
1968
+ const existing = this.findMatchingEvent(pattern, options?.source);
1969
+ if (existing) return Promise.resolve(existing);
1970
+ const withinFrames = options?.withinFrames;
1971
+ if (withinFrames !== void 0 && (!Number.isInteger(withinFrames) || withinFrames < 0)) {
1972
+ throw new Error(
1973
+ "Inspector.events.waitFor(withinFrames) requires a non-negative integer."
1974
+ );
1975
+ }
1976
+ return new Promise((resolve, reject) => {
1977
+ const waiter = {
1978
+ pattern,
1979
+ source: options?.source,
1980
+ withinFrames,
1981
+ deadlineFrame: withinFrames !== void 0 ? this.time.getFrame() + withinFrames : void 0,
1982
+ resolve,
1983
+ reject
1984
+ };
1985
+ this.eventWaiters.add(waiter);
1986
+ });
1987
+ }, "waitFor")
1988
+ };
1989
+ capture = {
1990
+ png: /* @__PURE__ */ __name(async () => {
1991
+ const base64 = await this.capture.pngBase64();
1992
+ return decodeBase64(base64);
1993
+ }, "png"),
1994
+ dataURL: /* @__PURE__ */ __name(async () => {
1995
+ const renderer = this.engine.context.tryResolve(RendererRuntimeKey);
1996
+ if (!renderer) {
1997
+ throw new Error(
1998
+ "Inspector.capture requires RendererPlugin to be active."
1999
+ );
2000
+ }
2001
+ const canvas = renderer.application.renderer.extract.canvas(
2002
+ renderer.application.stage
2003
+ );
2004
+ return canvas.toDataURL("image/png");
2005
+ }, "dataURL"),
2006
+ pngBase64: /* @__PURE__ */ __name(async () => {
2007
+ const dataUrl = await this.capture.dataURL();
2008
+ const comma = dataUrl.indexOf(",");
2009
+ return comma === -1 ? dataUrl : dataUrl.slice(comma + 1);
2010
+ }, "pngBase64")
2011
+ };
2012
+ constructor(engine) {
2013
+ this.engine = engine;
1779
2014
  }
1780
- /** Whether this scene is effectively paused (manual pause or paused by stack). */
1781
- get isPaused() {
1782
- if (this.paused) return true;
1783
- const sm = this._context?.tryResolve(SceneManagerKey);
1784
- if (!sm) return false;
1785
- const stack = sm.all;
1786
- const idx = stack.indexOf(this);
1787
- if (idx === -1) return false;
1788
- for (let i = idx + 1; i < stack.length; i++) {
1789
- if (stack[i].pauseBelow) return true;
2015
+ /** Register a namespaced extension API for plugin-specific debug helpers. */
2016
+ addExtension(namespace, api) {
2017
+ this.assertNonEmptyString(
2018
+ namespace,
2019
+ "Inspector.addExtension(namespace)"
2020
+ );
2021
+ if (!api || typeof api !== "object") {
2022
+ throw new Error("Inspector.addExtension(api) requires an object.");
1790
2023
  }
1791
- return false;
2024
+ if (this.extensions.has(namespace)) {
2025
+ throw new Error(
2026
+ `Inspector.addExtension(): namespace "${namespace}" is already registered.`
2027
+ );
2028
+ }
2029
+ this.extensions.set(namespace, api);
2030
+ return api;
1792
2031
  }
1793
- /** Whether a scene transition is currently running. */
1794
- get isTransitioning() {
1795
- const sm = this._context?.tryResolve(SceneManagerKey);
1796
- return sm?.isTransitioning ?? false;
2032
+ /** Look up a previously registered extension API by namespace. */
2033
+ getExtension(namespace) {
2034
+ this.assertNonEmptyString(
2035
+ namespace,
2036
+ "Inspector.getExtension(namespace)"
2037
+ );
2038
+ return this.extensions.get(namespace);
1797
2039
  }
1798
- /** Convenience accessor for the AssetManager. */
1799
- get assets() {
1800
- return this._context.resolve(AssetManagerKey);
2040
+ /** Remove a previously registered extension namespace. */
2041
+ removeExtension(namespace) {
2042
+ this.assertNonEmptyString(
2043
+ namespace,
2044
+ "Inspector.removeExtension(namespace)"
2045
+ );
2046
+ this.extensions.delete(namespace);
1801
2047
  }
1802
- /**
1803
- * Lazy proxy-based service resolution. Can be used at field-declaration time:
1804
- * ```ts
1805
- * readonly layers = this.service(RenderLayerManagerKey);
1806
- * ```
1807
- * The actual resolution is deferred until first property access.
1808
- */
1809
- service(key) {
1810
- let resolved;
1811
- return new Proxy({}, {
1812
- get: /* @__PURE__ */ __name((_target, prop) => {
1813
- resolved ??= this._context.resolve(key);
1814
- const value = resolved[prop];
1815
- return typeof value === "function" ? value.bind(resolved) : value;
1816
- }, "get"),
1817
- set: /* @__PURE__ */ __name((_target, prop, value) => {
1818
- resolved ??= this._context.resolve(key);
1819
- resolved[prop] = value;
1820
- return true;
1821
- }, "set")
1822
- });
2048
+ /** Full deterministic state snapshot (stable ordering, serializable). */
2049
+ snapshot() {
2050
+ const scenes = this.engine.scenes.all.map(
2051
+ (scene) => this.sceneToWorldSnapshot(scene)
2052
+ );
2053
+ return {
2054
+ version: 1,
2055
+ frame: this.time.getFrame(),
2056
+ sceneStack: this.getSceneStack(),
2057
+ entityCount: this.countEntities(),
2058
+ systemCount: this.getSystems().length,
2059
+ errors: this.getErrors(),
2060
+ scenes,
2061
+ camera: this.buildCameraSnapshot(),
2062
+ input: this.buildInputSnapshot()
2063
+ };
1823
2064
  }
1824
- spawn(nameOrBlueprintOrClass, params) {
1825
- if (typeof nameOrBlueprintOrClass === "function") {
1826
- const entity2 = new nameOrBlueprintOrClass();
1827
- entity2._setScene(this, this.entityCallbacks);
1828
- this.entities.add(entity2);
1829
- this.bus?.emit("entity:created", { entity: entity2 });
1830
- entity2.setup?.(params);
1831
- return entity2;
2065
+ /** Stable JSON form of {@link snapshot}. */
2066
+ snapshotJSON() {
2067
+ return stableStringify(this.snapshot());
2068
+ }
2069
+ /** Snapshot one scene by inspector scene id. */
2070
+ snapshotScene(id) {
2071
+ const scene = this.engine.scenes.all.find(
2072
+ (candidate) => this.getSceneId(candidate) === id
2073
+ );
2074
+ if (!scene) {
2075
+ throw new Error(`Inspector.snapshotScene(): unknown scene id "${id}".`);
2076
+ }
2077
+ return this.sceneToWorldSnapshot(scene);
2078
+ }
2079
+ /** Find entity by name in the active scene. */
2080
+ getEntityByName(name) {
2081
+ const entity = this.findActiveEntity(name);
2082
+ if (!entity) return void 0;
2083
+ return this.entityToQuerySnapshot(entity);
2084
+ }
2085
+ /** Get entity position (from Transform component). */
2086
+ getEntityPosition(name) {
2087
+ const entity = this.findActiveEntity(name);
2088
+ if (!entity) return void 0;
2089
+ const transform = this.getTransform(entity);
2090
+ if (!transform) return void 0;
2091
+ return { x: transform.position.x, y: transform.position.y };
2092
+ }
2093
+ /** Check if an entity has a component by class name string. */
2094
+ hasComponent(entityName, componentClass) {
2095
+ return this.findComponentByName(entityName, componentClass) !== void 0;
2096
+ }
2097
+ /** Get component data (serializable subset) by class name string. */
2098
+ getComponentData(entityName, componentClass) {
2099
+ const comp = this.findComponentByName(entityName, componentClass);
2100
+ if (!comp) return void 0;
2101
+ if (typeof comp.serialize === "function") {
2102
+ const data = trySerialize(comp);
2103
+ if (data !== void 0) return data;
2104
+ }
2105
+ return this.serializeComponentOwnProperties(comp);
2106
+ }
2107
+ /** Get all entities in the active scene as lightweight snapshots. */
2108
+ getEntities() {
2109
+ const scene = this.engine.scenes.active;
2110
+ if (!scene) return [];
2111
+ const result = [];
2112
+ for (const entity of scene.getEntities()) {
2113
+ if (!entity.isDestroyed) {
2114
+ result.push(this.entityToQuerySnapshot(entity));
2115
+ }
2116
+ }
2117
+ return result;
2118
+ }
2119
+ /** Get scene stack info. */
2120
+ getSceneStack() {
2121
+ return this.engine.scenes.all.map((scene) => ({
2122
+ name: scene.name,
2123
+ entityCount: scene.getEntities().size,
2124
+ paused: scene.isPaused
2125
+ }));
2126
+ }
2127
+ /** Get active system info. */
2128
+ getSystems() {
2129
+ const scheduler = this.engine.context.tryResolve(SystemSchedulerKey);
2130
+ if (!scheduler) return [];
2131
+ return scheduler.getAllSystems().map((sys) => ({
2132
+ name: sys.constructor.name,
2133
+ phase: sys.phase,
2134
+ priority: sys.priority,
2135
+ enabled: sys.enabled
2136
+ }));
2137
+ }
2138
+ /** Get disabled components/systems from error boundary. */
2139
+ getErrors() {
2140
+ const boundary = this.engine.context.tryResolve(ErrorBoundaryKey);
2141
+ if (!boundary) return { disabledSystems: [], disabledComponents: [] };
2142
+ const disabled = boundary.getDisabled();
2143
+ return {
2144
+ disabledSystems: disabled.systems.map(
2145
+ (s) => s.system.constructor.name
2146
+ ),
2147
+ disabledComponents: disabled.components.map((c) => ({
2148
+ entity: c.component.entity?.name ?? "unknown",
2149
+ component: c.component.constructor.name,
2150
+ error: c.error
2151
+ }))
2152
+ };
2153
+ }
2154
+ /** Create a new scene-scoped RNG instance using the current inspector seed policy. */
2155
+ createSceneRandom() {
2156
+ const seed = this.sceneSeedOverride ?? this.defaultSceneSeed ?? createDefaultRandomSeed();
2157
+ return createRandomService(seed);
2158
+ }
2159
+ /** Force every current and future scene RNG to the provided seed. */
2160
+ setSeed(seed) {
2161
+ const normalized = normalizeSeed(seed);
2162
+ this.sceneSeedOverride = normalized;
2163
+ for (const scene of this.engine.scenes.all) {
2164
+ this.resolveInternalRandom(scene)?.setSeed(normalized);
2165
+ }
2166
+ }
2167
+ /** @internal DebugPlugin installs a deterministic default seed through this hook. */
2168
+ setDefaultSceneSeed(seed) {
2169
+ this.defaultSceneSeed = seed === void 0 ? void 0 : normalizeSeed(seed);
2170
+ if (this.sceneSeedOverride !== void 0 || this.defaultSceneSeed === void 0) {
2171
+ return;
2172
+ }
2173
+ for (const scene of this.engine.scenes.all) {
2174
+ this.resolveInternalRandom(scene)?.setSeed(this.defaultSceneSeed);
2175
+ }
2176
+ }
2177
+ resolveInternalRandom(scene) {
2178
+ return scene._resolveScoped(RandomKey);
2179
+ }
2180
+ /** @internal DebugPlugin attaches the frozen-time controller through this hook. */
2181
+ attachTimeController(controller) {
2182
+ this.timeController = controller;
2183
+ }
2184
+ /** @internal Clear a previously attached time controller. */
2185
+ detachTimeController(controller) {
2186
+ if (!controller || this.timeController === controller) {
2187
+ this.timeController = null;
2188
+ }
2189
+ }
2190
+ /** @internal Enable or disable event log recording. */
2191
+ setEventLogEnabled(enabled) {
2192
+ if (this.eventLogEnabled === enabled) return;
2193
+ this.eventLogEnabled = enabled;
2194
+ if (enabled) {
2195
+ if (!this.detachBusTap && this.engine.events?.tap) {
2196
+ this.detachBusTap = this.engine.events.tap(this.busEventObserver);
2197
+ }
2198
+ } else {
2199
+ this.detachBusTap?.();
2200
+ this.detachBusTap = null;
2201
+ }
2202
+ for (const scene of this.engine.scenes.all) {
2203
+ if (enabled) {
2204
+ this.attachSceneEventObserver(scene);
2205
+ } else {
2206
+ this.detachSceneEventObserver(scene);
2207
+ }
2208
+ }
2209
+ }
2210
+ /** @internal Install entity-event observation for one scene. No-op if disabled. */
2211
+ attachSceneEventObserver(scene) {
2212
+ if (!this.eventLogEnabled) return;
2213
+ scene._setEntityEventObserver(this.sceneEventObserver);
2214
+ }
2215
+ /** @internal Clear entity-event observation for one scene. */
2216
+ detachSceneEventObserver(scene) {
2217
+ scene._setEntityEventObserver(void 0);
2218
+ }
2219
+ /** @internal Scene hooks forward entity events through this method. */
2220
+ recordEntityEvent(eventName, data, entity) {
2221
+ if (!this.eventLogEnabled) return;
2222
+ const scene = entity.tryScene;
2223
+ this.appendEvent(
2224
+ {
2225
+ frame: this.time.getFrame(),
2226
+ source: "entity",
2227
+ type: eventName,
2228
+ targetId: String(entity.id),
2229
+ payload: serializeEventPayload(data)
2230
+ },
2231
+ scene ? this.getSceneId(scene) : void 0
2232
+ );
2233
+ }
2234
+ /** @internal Engine teardown releases the event-bus tap through this hook. */
2235
+ dispose() {
2236
+ this.detachBusTap?.();
2237
+ this.detachBusTap = null;
2238
+ for (const scene of this.engine.scenes.all) {
2239
+ scene._setEntityEventObserver(void 0);
2240
+ }
2241
+ this.extensions.clear();
2242
+ }
2243
+ requireTimeController() {
2244
+ if (!this.timeController) {
2245
+ throw new Error(
2246
+ "Inspector.time requires DebugPlugin to be active."
2247
+ );
2248
+ }
2249
+ return this.timeController;
2250
+ }
2251
+ requireInputManager() {
2252
+ const input = this.engine.context.tryResolve(InputManagerRuntimeKey);
2253
+ if (!input) {
2254
+ throw new Error(
2255
+ "Inspector.input requires InputPlugin to be active."
2256
+ );
2257
+ }
2258
+ return input;
2259
+ }
2260
+ recordBusEvent(type, data) {
2261
+ if (!this.eventLogEnabled) return;
2262
+ this.appendEvent(
2263
+ {
2264
+ frame: this.time.getFrame(),
2265
+ source: "bus",
2266
+ type,
2267
+ payload: serializeEventPayload(data)
2268
+ },
2269
+ this.inferSceneIdFromPayload(data)
2270
+ );
2271
+ }
2272
+ appendEvent(entry, sceneId) {
2273
+ if (this.eventCapacity === 0) {
2274
+ this.flushMatchingWaiter(entry);
2275
+ return;
2276
+ }
2277
+ const logged = { entry, sceneId };
2278
+ if (this.eventLog.length < this.eventCapacity) {
2279
+ this.eventLog.push(logged);
2280
+ } else {
2281
+ this.eventLog[this.eventLogHead] = logged;
2282
+ this.eventLogHead = (this.eventLogHead + 1) % this.eventCapacity;
2283
+ }
2284
+ this.flushMatchingWaiter(entry);
2285
+ }
2286
+ /** Resolve waiters whose deadline has passed without a match. */
2287
+ expireDeadlineWaiters() {
2288
+ if (this.eventWaiters.size === 0) return;
2289
+ const frame = this.time.getFrame();
2290
+ for (const waiter of [...this.eventWaiters]) {
2291
+ if (waiter.deadlineFrame !== void 0 && frame > waiter.deadlineFrame) {
2292
+ this.eventWaiters.delete(waiter);
2293
+ waiter.reject(
2294
+ new Error(
2295
+ `Inspector.events.waitFor() timed out after ${waiter.withinFrames} frames.`
2296
+ )
2297
+ );
2298
+ }
2299
+ }
2300
+ }
2301
+ /** Resolve any waiter that matches the just-appended entry. */
2302
+ flushMatchingWaiter(entry) {
2303
+ if (this.eventWaiters.size === 0) return;
2304
+ for (const waiter of [...this.eventWaiters]) {
2305
+ if (this.eventMatches(entry, waiter.pattern, waiter.source)) {
2306
+ this.eventWaiters.delete(waiter);
2307
+ waiter.resolve(entry);
2308
+ }
2309
+ }
2310
+ }
2311
+ /**
2312
+ * Walk the ring buffer in chronological order. We avoid materializing the
2313
+ * ordered array on every event append; instead, every consumer that needs
2314
+ * order calls this helper.
2315
+ */
2316
+ iterateLog() {
2317
+ if (this.eventLog.length < this.eventCapacity || this.eventLogHead === 0) {
2318
+ return this.eventLog;
2319
+ }
2320
+ return [
2321
+ ...this.eventLog.slice(this.eventLogHead),
2322
+ ...this.eventLog.slice(0, this.eventLogHead)
2323
+ ];
2324
+ }
2325
+ findMatchingEvent(pattern, source) {
2326
+ for (const { entry } of this.iterateLog()) {
2327
+ if (this.eventMatches(entry, pattern, source)) {
2328
+ return { ...entry };
2329
+ }
2330
+ }
2331
+ return void 0;
2332
+ }
2333
+ eventMatches(entry, pattern, source) {
2334
+ if (source && entry.source !== source) return false;
2335
+ return typeof pattern === "string" ? entry.type === pattern : pattern.test(entry.type);
2336
+ }
2337
+ sceneToWorldSnapshot(scene) {
2338
+ const random = scene._resolveScoped(RandomKey);
2339
+ const physicsManager = this.engine.context.tryResolve(
2340
+ PhysicsWorldManagerRuntimeKey
2341
+ );
2342
+ return {
2343
+ id: this.getSceneId(scene),
2344
+ name: scene.name,
2345
+ paused: scene.isPaused,
2346
+ timeScale: scene.timeScale,
2347
+ seed: random?.getSeed() ?? 0,
2348
+ entities: this.getSceneEntities(scene),
2349
+ ui: this.buildUISnapshot(scene),
2350
+ physics: physicsManager?.getContext(scene)?.world.snapshot() ?? {
2351
+ bodies: [],
2352
+ contacts: []
2353
+ },
2354
+ events: this.getSceneEvents(scene)
2355
+ };
2356
+ }
2357
+ getSceneEntities(scene) {
2358
+ return [...scene.getEntities()].filter((entity) => !entity.isDestroyed).sort((a, b) => a.id - b.id).map((entity) => this.entityToWorldSnapshot(entity));
2359
+ }
2360
+ entityToWorldSnapshot(entity) {
2361
+ const transform = entity.has(Transform) ? entity.get(Transform) : void 0;
2362
+ const worldPosition = transform?.worldPosition;
2363
+ const worldScale = transform?.worldScale;
2364
+ const components = [...entity.getAll()].map((component) => this.componentToSnapshot(component)).sort((a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : 0);
2365
+ return {
2366
+ id: String(entity.id),
2367
+ type: entity.constructor.name,
2368
+ parent: entity.parent ? String(entity.parent.id) : null,
2369
+ transform: {
2370
+ x: worldPosition?.x ?? 0,
2371
+ y: worldPosition?.y ?? 0,
2372
+ rotation: transform?.worldRotation ?? 0,
2373
+ scaleX: worldScale?.x ?? 1,
2374
+ scaleY: worldScale?.y ?? 1
2375
+ },
2376
+ components
2377
+ };
2378
+ }
2379
+ componentToSnapshot(component) {
2380
+ return {
2381
+ type: component.constructor.name,
2382
+ state: typeof component.serialize === "function" ? trySerialize(component) ?? null : null
2383
+ };
2384
+ }
2385
+ buildUISnapshot(scene) {
2386
+ const roots = [...scene.getEntities()].filter((entity) => !entity.isDestroyed).flatMap(
2387
+ (entity) => [...entity.getAll()].filter(
2388
+ (component) => component.constructor.name === "UIPanel" && "_node" in component
2389
+ ).map(
2390
+ (component, index) => this.buildUINodeSnapshot(
2391
+ component._node,
2392
+ `entity-${entity.id}:UIPanel:${index}`
2393
+ )
2394
+ )
2395
+ );
2396
+ if (roots.length === 0) return null;
2397
+ if (roots.length === 1) {
2398
+ return { root: roots[0] };
2399
+ }
2400
+ return {
2401
+ root: {
2402
+ id: `scene-${this.getSceneId(scene)}:ui`,
2403
+ type: "UIRoot",
2404
+ layout: { x: 0, y: 0, width: 0, height: 0 },
2405
+ children: roots,
2406
+ state: null
2407
+ }
2408
+ };
2409
+ }
2410
+ buildUINodeSnapshot(node, id) {
2411
+ const layout = node.yogaNode?.getComputedLayout();
2412
+ const children = (node.children ?? []).map(
2413
+ (child, index) => this.buildUINodeSnapshot(child, `${id}/${index}`)
2414
+ );
2415
+ return {
2416
+ id,
2417
+ type: node.constructor.name,
2418
+ layout: {
2419
+ x: layout?.left ?? 0,
2420
+ y: layout?.top ?? 0,
2421
+ width: layout?.width ?? 0,
2422
+ height: layout?.height ?? 0
2423
+ },
2424
+ children,
2425
+ state: null
2426
+ };
2427
+ }
2428
+ buildCameraSnapshot() {
2429
+ const match = this.findTopmostCamera();
2430
+ if (!match) return null;
2431
+ const { scene, camera } = match;
2432
+ return {
2433
+ sceneId: this.getSceneId(scene),
2434
+ sceneName: scene.name,
2435
+ name: camera.cameraName ?? null,
2436
+ priority: camera.priority ?? 0,
2437
+ position: {
2438
+ x: camera.position.x,
2439
+ y: camera.position.y
2440
+ },
2441
+ zoom: camera.zoom,
2442
+ rotation: camera.rotation
2443
+ };
2444
+ }
2445
+ findTopmostCamera() {
2446
+ const stack = this.engine.scenes.all;
2447
+ for (let i = stack.length - 1; i >= 0; i--) {
2448
+ const scene = stack[i];
2449
+ if (!scene) continue;
2450
+ let highest;
2451
+ for (const entity of scene.getEntities()) {
2452
+ if (entity.isDestroyed) continue;
2453
+ for (const component of entity.getAll()) {
2454
+ if (component.constructor.name !== "CameraComponent") continue;
2455
+ const camera = component;
2456
+ if (camera.enabled && (!highest || (camera.priority ?? 0) > (highest.priority ?? 0))) {
2457
+ highest = camera;
2458
+ }
2459
+ }
2460
+ }
2461
+ if (highest) {
2462
+ return { scene, camera: highest };
2463
+ }
2464
+ }
2465
+ return void 0;
2466
+ }
2467
+ buildInputSnapshot() {
2468
+ const input = this.engine.context.tryResolve(InputManagerRuntimeKey);
2469
+ return input?.snapshotState() ?? {
2470
+ keys: [],
2471
+ actions: [],
2472
+ mouse: { x: 0, y: 0, buttons: [], down: false },
2473
+ gamepad: { buttons: [], axes: [] }
2474
+ };
2475
+ }
2476
+ getSceneEvents(scene) {
2477
+ const sceneId = this.getSceneId(scene);
2478
+ return this.iterateLog().filter((entry) => entry.sceneId === sceneId).map(({ entry }) => ({ ...entry }));
2479
+ }
2480
+ inferSceneIdFromPayload(data) {
2481
+ if (!data || typeof data !== "object") return void 0;
2482
+ const record = data;
2483
+ const scene = this.extractScene(record["scene"]) ?? this.extractSceneFromEntity(record["entity"]) ?? this.extractSceneFromEntity(record["oldScene"]) ?? this.extractSceneFromEntity(record["newScene"]);
2484
+ return scene ? this.getSceneId(scene) : void 0;
2485
+ }
2486
+ extractScene(value) {
2487
+ if (!value || typeof value !== "object") return void 0;
2488
+ return this.engine.scenes.all.find((scene) => scene === value);
2489
+ }
2490
+ extractSceneFromEntity(value) {
2491
+ if (!value || typeof value !== "object") return void 0;
2492
+ const maybeEntity = value;
2493
+ return maybeEntity.tryScene ?? this.extractScene(value);
2494
+ }
2495
+ findActiveEntity(name) {
2496
+ return this.engine.scenes.active?.findEntity(name);
2497
+ }
2498
+ findComponentByName(entityName, componentClass) {
2499
+ const entity = this.findActiveEntity(entityName);
2500
+ if (!entity) return void 0;
2501
+ for (const comp of entity.getAll()) {
2502
+ if (comp.constructor.name === componentClass) return comp;
2503
+ }
2504
+ return void 0;
2505
+ }
2506
+ entityToQuerySnapshot(entity) {
2507
+ const transform = this.getTransform(entity);
2508
+ const snapshot = {
2509
+ id: entity.id,
2510
+ name: entity.name,
2511
+ tags: [...entity.tags].sort((a, b) => a < b ? -1 : a > b ? 1 : 0),
2512
+ components: [...entity.getAll()].map((component) => component.constructor.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
2513
+ };
2514
+ if (transform) {
2515
+ snapshot.position = {
2516
+ x: transform.position.x,
2517
+ y: transform.position.y
2518
+ };
2519
+ }
2520
+ return snapshot;
2521
+ }
2522
+ getTransform(entity) {
2523
+ return entity.has(Transform) ? entity.get(Transform) : void 0;
2524
+ }
2525
+ serializeComponentOwnProperties(comp) {
2526
+ const result = {};
2527
+ for (const key of Object.getOwnPropertyNames(comp)) {
2528
+ if (key === "entity") continue;
2529
+ if (key.startsWith("_")) continue;
2530
+ const value = comp[key];
2531
+ if (!isSerializableValue(value)) continue;
2532
+ result[key] = value;
2533
+ }
2534
+ return result;
2535
+ }
2536
+ countEntities() {
2537
+ let count = 0;
2538
+ for (const scene of this.engine.scenes.all) {
2539
+ for (const entity of scene.getEntities()) {
2540
+ if (!entity.isDestroyed) count++;
2541
+ }
2542
+ }
2543
+ return count;
2544
+ }
2545
+ getSceneId(scene) {
2546
+ let id = this.sceneIds.get(scene);
2547
+ if (!id) {
2548
+ this.nextSceneId++;
2549
+ id = `scene-${this.nextSceneId}`;
2550
+ this.sceneIds.set(scene, id);
2551
+ }
2552
+ return id;
2553
+ }
2554
+ assertNonNegativeInteger(value, name) {
2555
+ if (!Number.isInteger(value) || value < 0) {
2556
+ throw new Error(`${name} requires a non-negative integer.`);
2557
+ }
2558
+ }
2559
+ assertNonEmptyString(value, name) {
2560
+ if (value.trim().length === 0) {
2561
+ throw new Error(`${name} requires a non-empty string.`);
2562
+ }
2563
+ }
2564
+ };
2565
+ function isSerializableValue(value) {
2566
+ if (value === null || value === void 0) return true;
2567
+ const t = typeof value;
2568
+ if (t === "function") return false;
2569
+ if (t !== "object") return true;
2570
+ if (Array.isArray(value)) return true;
2571
+ const proto = Object.getPrototypeOf(value);
2572
+ return proto === Object.prototype || proto === null;
2573
+ }
2574
+ __name(isSerializableValue, "isSerializableValue");
2575
+ function safeClone(value) {
2576
+ try {
2577
+ return JSON.parse(JSON.stringify(value));
2578
+ } catch {
2579
+ return void 0;
2580
+ }
2581
+ }
2582
+ __name(safeClone, "safeClone");
2583
+ function trySerialize(component) {
2584
+ try {
2585
+ return safeClone(component.serialize?.());
2586
+ } catch {
2587
+ return void 0;
2588
+ }
2589
+ }
2590
+ __name(trySerialize, "trySerialize");
2591
+ function serializeEventPayload(payload) {
2592
+ if (payload === void 0) return null;
2593
+ const cloned = safeClone(payload);
2594
+ return cloned === void 0 ? { _unserializable: true } : cloned;
2595
+ }
2596
+ __name(serializeEventPayload, "serializeEventPayload");
2597
+ function stableStringify(value) {
2598
+ return JSON.stringify(sortJsonValue(value));
2599
+ }
2600
+ __name(stableStringify, "stableStringify");
2601
+ function sortJsonValue(value) {
2602
+ if (Array.isArray(value)) {
2603
+ return value.map((item) => sortJsonValue(item));
2604
+ }
2605
+ if (value && typeof value === "object") {
2606
+ const entries = Object.entries(value).sort(
2607
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
2608
+ );
2609
+ const result = {};
2610
+ for (const [key, child] of entries) {
2611
+ result[key] = sortJsonValue(child);
2612
+ }
2613
+ return result;
2614
+ }
2615
+ return value;
2616
+ }
2617
+ __name(sortJsonValue, "sortJsonValue");
2618
+ function decodeBase64(base64) {
2619
+ if (typeof atob === "function") {
2620
+ const binary = atob(base64);
2621
+ const bytes = new Uint8Array(binary.length);
2622
+ for (let i = 0; i < binary.length; i++) {
2623
+ bytes[i] = binary.charCodeAt(i);
2624
+ }
2625
+ return bytes;
2626
+ }
2627
+ const bufferCtor = globalThis.Buffer;
2628
+ if (bufferCtor) {
2629
+ return bufferCtor.from(base64, "base64");
2630
+ }
2631
+ throw new Error("Inspector.capture.png() is not supported in this environment.");
2632
+ }
2633
+ __name(decodeBase64, "decodeBase64");
2634
+
2635
+ // src/Scene.ts
2636
+ var Scene = class {
2637
+ static {
2638
+ __name(this, "Scene");
2639
+ }
2640
+ /** Whether scenes below this one in the stack should be paused. Default: true. */
2641
+ pauseBelow = true;
2642
+ /** Whether scenes below this one should still render. Default: false. */
2643
+ transparentBelow = false;
2644
+ /** Asset handles to load before onEnter(). Override in subclasses. */
2645
+ preload;
2646
+ /** Default transition used when this scene is the destination of a push/pop/replace. */
2647
+ defaultTransition;
2648
+ /** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
2649
+ paused = false;
2650
+ /** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
2651
+ timeScale = 1;
2652
+ entities = /* @__PURE__ */ new Set();
2653
+ destroyQueue = [];
2654
+ _context;
2655
+ entityCallbacks;
2656
+ queryCache;
2657
+ bus;
2658
+ _entityEventHandlers;
2659
+ _entityEventObserver;
2660
+ _scopedServices;
2661
+ /** Access the EngineContext. */
2662
+ get context() {
2663
+ return this._context;
2664
+ }
2665
+ /** Whether this scene is effectively paused (manual pause or paused by stack). */
2666
+ get isPaused() {
2667
+ if (this.paused) return true;
2668
+ const sm = this._context?.tryResolve(SceneManagerKey);
2669
+ if (!sm) return false;
2670
+ const stack = sm.all;
2671
+ const idx = stack.indexOf(this);
2672
+ if (idx === -1) return false;
2673
+ for (let i = idx + 1; i < stack.length; i++) {
2674
+ if (stack[i].pauseBelow) return true;
2675
+ }
2676
+ return false;
2677
+ }
2678
+ /** Whether a scene transition is currently running. */
2679
+ get isTransitioning() {
2680
+ const sm = this._context?.tryResolve(SceneManagerKey);
2681
+ return sm?.isTransitioning ?? false;
2682
+ }
2683
+ /** Convenience accessor for the AssetManager. */
2684
+ get assets() {
2685
+ return this._context.resolve(AssetManagerKey);
2686
+ }
2687
+ /**
2688
+ * Lazy proxy-based service resolution. Can be used at field-declaration time:
2689
+ * ```ts
2690
+ * readonly layers = this.service(RenderLayerManagerKey);
2691
+ * ```
2692
+ * The actual resolution is deferred until first property access.
2693
+ */
2694
+ service(key) {
2695
+ let resolved;
2696
+ return new Proxy({}, {
2697
+ get: /* @__PURE__ */ __name((_target, prop) => {
2698
+ resolved ??= this._context.resolve(key);
2699
+ const value = resolved[prop];
2700
+ return typeof value === "function" ? value.bind(resolved) : value;
2701
+ }, "get"),
2702
+ set: /* @__PURE__ */ __name((_target, prop, value) => {
2703
+ resolved ??= this._context.resolve(key);
2704
+ resolved[prop] = value;
2705
+ return true;
2706
+ }, "set")
2707
+ });
2708
+ }
2709
+ spawn(nameOrBlueprintOrClass, params) {
2710
+ if (typeof nameOrBlueprintOrClass === "function") {
2711
+ const entity2 = new nameOrBlueprintOrClass();
2712
+ entity2._setScene(this, this.entityCallbacks);
2713
+ this.entities.add(entity2);
2714
+ this.bus?.emit("entity:created", { entity: entity2 });
2715
+ entity2.setup?.(params);
2716
+ return entity2;
1832
2717
  }
1833
2718
  const isBlueprint = typeof nameOrBlueprintOrClass === "object" && nameOrBlueprintOrClass !== null && "build" in nameOrBlueprintOrClass;
1834
2719
  const name = isBlueprint ? nameOrBlueprintOrClass.name : nameOrBlueprintOrClass;
@@ -1918,6 +2803,14 @@ var Scene = class {
1918
2803
  }
1919
2804
  }
1920
2805
  }
2806
+ /**
2807
+ * Observe entity-scoped event emissions after they dispatch locally and
2808
+ * bubble to the scene. Tooling only; game code should keep using `on()`.
2809
+ * @internal
2810
+ */
2811
+ _observeEntityEvent(eventName, data, entity) {
2812
+ this._entityEventObserver?.(eventName, data, entity);
2813
+ }
1921
2814
  // ---- Internal methods ----
1922
2815
  /**
1923
2816
  * Register a scene-scoped service. Called from a plugin's `beforeEnter`
@@ -1929,6 +2822,13 @@ var Scene = class {
1929
2822
  this._scopedServices ??= /* @__PURE__ */ new Map();
1930
2823
  this._scopedServices.set(key.id, value);
1931
2824
  }
2825
+ /**
2826
+ * Install or clear a tooling-only observer for bubbled entity events.
2827
+ * @internal
2828
+ */
2829
+ _setEntityEventObserver(observer) {
2830
+ this._entityEventObserver = observer;
2831
+ }
1932
2832
  /**
1933
2833
  * Resolve a scene-scoped service, or `undefined` if none was registered.
1934
2834
  * @internal
@@ -1995,6 +2895,128 @@ var Scene = class {
1995
2895
  }
1996
2896
  };
1997
2897
 
2898
+ // src/Process.ts
2899
+ var Process = class _Process {
2900
+ static {
2901
+ __name(this, "Process");
2902
+ }
2903
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
2904
+ updateFn;
2905
+ onCompleteFn;
2906
+ duration;
2907
+ loop;
2908
+ /** Tags for filtering/grouping. */
2909
+ tags;
2910
+ elapsed = 0;
2911
+ _completed = false;
2912
+ _paused = false;
2913
+ _cancelled = false;
2914
+ resolvePromise;
2915
+ /** Create a timer that fires `onComplete` after `duration` ms. */
2916
+ static delay(duration, onComplete, tags) {
2917
+ const opts = { duration };
2918
+ if (onComplete !== void 0) opts.onComplete = onComplete;
2919
+ if (tags !== void 0) opts.tags = tags;
2920
+ return new _Process(opts);
2921
+ }
2922
+ constructor(options) {
2923
+ this.updateFn = options.update ?? (() => {
2924
+ });
2925
+ this.onCompleteFn = options.onComplete;
2926
+ this.duration = options.duration;
2927
+ this.loop = options.loop ?? false;
2928
+ this.tags = options.tags ?? [];
2929
+ }
2930
+ /** Whether the process has completed. */
2931
+ get completed() {
2932
+ return this._completed;
2933
+ }
2934
+ /** Whether the process is paused. */
2935
+ get paused() {
2936
+ return this._paused;
2937
+ }
2938
+ /** Pause the process. */
2939
+ pause() {
2940
+ this._paused = true;
2941
+ }
2942
+ /** Resume the process. */
2943
+ resume() {
2944
+ this._paused = false;
2945
+ }
2946
+ /** Cancel the process. */
2947
+ cancel() {
2948
+ this._cancelled = true;
2949
+ this._completed = true;
2950
+ this.resolvePromise?.();
2951
+ }
2952
+ /** Returns a promise that resolves when the process completes or is cancelled. */
2953
+ toPromise() {
2954
+ if (this._completed) return Promise.resolve();
2955
+ return new Promise((resolve) => {
2956
+ this.resolvePromise = resolve;
2957
+ });
2958
+ }
2959
+ /**
2960
+ * Advance the process by dt milliseconds.
2961
+ * @internal
2962
+ */
2963
+ _update(dt) {
2964
+ if (this._completed || this._paused || this._cancelled) return;
2965
+ this.elapsed += dt;
2966
+ if (this.duration !== void 0 && this.elapsed >= this.duration) {
2967
+ const result2 = this.updateFn(dt, this.elapsed);
2968
+ if (this.loop && result2 !== true) {
2969
+ this.elapsed = this.elapsed % this.duration;
2970
+ return;
2971
+ }
2972
+ this.complete();
2973
+ return;
2974
+ }
2975
+ const result = this.updateFn(dt, this.elapsed);
2976
+ if (result === true) {
2977
+ if (this.loop) {
2978
+ this.elapsed = 0;
2979
+ return;
2980
+ }
2981
+ this.complete();
2982
+ }
2983
+ }
2984
+ /**
2985
+ * Reset the process to its initial state so it can be re-run.
2986
+ * @internal Used by Sequence for loop/repeat with direct instances.
2987
+ */
2988
+ _reset() {
2989
+ this.elapsed = 0;
2990
+ this._completed = false;
2991
+ this._paused = false;
2992
+ this._cancelled = false;
2993
+ delete this.resolvePromise;
2994
+ }
2995
+ complete() {
2996
+ this._completed = true;
2997
+ this.onCompleteFn?.();
2998
+ this.resolvePromise?.();
2999
+ }
3000
+ };
3001
+ var easeLinear = /* @__PURE__ */ __name((t) => t, "easeLinear");
3002
+ var easeInQuad = /* @__PURE__ */ __name((t) => t * t, "easeInQuad");
3003
+ var easeOutQuad = /* @__PURE__ */ __name((t) => t * (2 - t), "easeOutQuad");
3004
+ var easeInOutQuad = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, "easeInOutQuad");
3005
+ var easeOutBounce = /* @__PURE__ */ __name((t) => {
3006
+ if (t < 1 / 2.75) {
3007
+ return 7.5625 * t * t;
3008
+ } else if (t < 2 / 2.75) {
3009
+ const t2 = t - 1.5 / 2.75;
3010
+ return 7.5625 * t2 * t2 + 0.75;
3011
+ } else if (t < 2.5 / 2.75) {
3012
+ const t2 = t - 2.25 / 2.75;
3013
+ return 7.5625 * t2 * t2 + 0.9375;
3014
+ } else {
3015
+ const t2 = t - 2.625 / 2.75;
3016
+ return 7.5625 * t2 * t2 + 0.984375;
3017
+ }
3018
+ }, "easeOutBounce");
3019
+
1998
3020
  // src/LoadingScene.ts
1999
3021
  var LoadingScene = class extends Scene {
2000
3022
  static {
@@ -2019,6 +3041,7 @@ var LoadingScene = class extends Scene {
2019
3041
  _active = true;
2020
3042
  _continueRequested = false;
2021
3043
  _continueGate;
3044
+ _pendingWaits = /* @__PURE__ */ new Set();
2022
3045
  // Bumped on every `_run` attempt. `AssetManager.loadAll` uses `Promise.all`
2023
3046
  // under the hood, so individual loaders from a failed attempt can still
2024
3047
  // resolve and fire `onProgress` after the attempt rejects. Without this
@@ -2073,28 +3096,32 @@ var LoadingScene = class extends Scene {
2073
3096
  onExit() {
2074
3097
  this._active = false;
2075
3098
  this._continueGate?.();
3099
+ for (const wait of this._pendingWaits) {
3100
+ wait.cancel();
3101
+ }
3102
+ this._pendingWaits.clear();
2076
3103
  }
2077
3104
  async _run() {
2078
- await new Promise((resolve) => setTimeout(resolve, 0));
3105
+ await Promise.resolve();
2079
3106
  if (!this._active) return;
2080
3107
  const attempt = ++this._attempt;
2081
3108
  const target = typeof this.target === "function" ? this.target() : this.target;
2082
- const startedAt = performance.now();
2083
3109
  const bus = this.context.resolve(EventBusKey);
3110
+ const minDuration = this._createEngineTimeDelay(this.minDuration);
2084
3111
  try {
2085
3112
  await this.assets.loadAll(target.preload ?? [], (ratio) => {
2086
3113
  if (!this._active || attempt !== this._attempt) return;
2087
3114
  this._progress = ratio;
2088
3115
  bus.emit("scene:loading:progress", { scene: this, ratio });
2089
3116
  });
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;
3117
+ if (!this._active || attempt !== this._attempt) {
3118
+ minDuration.cancel();
3119
+ return;
2096
3120
  }
3121
+ await minDuration.promise;
3122
+ if (!this._active || attempt !== this._attempt) return;
2097
3123
  } catch (err) {
3124
+ minDuration.cancel();
2098
3125
  if (!this._active || attempt !== this._attempt) return;
2099
3126
  const error = err instanceof Error ? err : new Error(String(err));
2100
3127
  this._started = false;
@@ -2118,6 +3145,26 @@ var LoadingScene = class extends Scene {
2118
3145
  this.transition ? { transition: this.transition } : void 0
2119
3146
  );
2120
3147
  }
3148
+ _createEngineTimeDelay(ms) {
3149
+ if (ms <= 0) {
3150
+ return {
3151
+ promise: Promise.resolve(),
3152
+ cancel: /* @__PURE__ */ __name(() => {
3153
+ }, "cancel")
3154
+ };
3155
+ }
3156
+ const wait = Process.delay(ms);
3157
+ this._pendingWaits.add(wait);
3158
+ this.context.resolve(ProcessSystemKey).add(wait);
3159
+ return {
3160
+ promise: wait.toPromise().finally(() => {
3161
+ this._pendingWaits.delete(wait);
3162
+ }),
3163
+ cancel: /* @__PURE__ */ __name(() => {
3164
+ wait.cancel();
3165
+ }, "cancel")
3166
+ };
3167
+ }
2121
3168
  };
2122
3169
 
2123
3170
  // src/SceneTransition.ts
@@ -2523,177 +3570,55 @@ var SceneManager = class {
2523
3570
  kind: run.kind,
2524
3571
  engineContext: this._context,
2525
3572
  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
- }
2556
- /** Fire onPause() for scenes that transitioned from not-paused to paused. */
2557
- _firePauseTransitions(wasPaused) {
2558
- for (const scene of this.stack) {
2559
- const was = wasPaused.get(scene) ?? false;
2560
- if (scene.isPaused && !was) {
2561
- scene.onPause?.();
2562
- }
2563
- }
2564
- }
2565
- /** Fire onResume() for scenes that transitioned from paused to not-paused. */
2566
- _fireResumeTransitions(wasPaused) {
2567
- for (const scene of this.stack) {
2568
- const was = wasPaused.get(scene) ?? false;
2569
- if (!scene.isPaused && was) {
2570
- scene.onResume?.();
2571
- }
2572
- }
2573
- }
2574
- };
2575
-
2576
- // src/Process.ts
2577
- var Process = class _Process {
2578
- static {
2579
- __name(this, "Process");
2580
- }
2581
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
2582
- updateFn;
2583
- onCompleteFn;
2584
- duration;
2585
- loop;
2586
- /** Tags for filtering/grouping. */
2587
- tags;
2588
- elapsed = 0;
2589
- _completed = false;
2590
- _paused = false;
2591
- _cancelled = false;
2592
- resolvePromise;
2593
- /** Create a timer that fires `onComplete` after `duration` ms. */
2594
- static delay(duration, onComplete, tags) {
2595
- const opts = { duration };
2596
- if (onComplete !== void 0) opts.onComplete = onComplete;
2597
- if (tags !== void 0) opts.tags = tags;
2598
- return new _Process(opts);
2599
- }
2600
- constructor(options) {
2601
- this.updateFn = options.update ?? (() => {
2602
- });
2603
- this.onCompleteFn = options.onComplete;
2604
- this.duration = options.duration;
2605
- this.loop = options.loop ?? false;
2606
- this.tags = options.tags ?? [];
2607
- }
2608
- /** Whether the process has completed. */
2609
- get completed() {
2610
- return this._completed;
2611
- }
2612
- /** Whether the process is paused. */
2613
- get paused() {
2614
- return this._paused;
3573
+ toScene: run.toScene
3574
+ };
2615
3575
  }
2616
- /** Pause the process. */
2617
- pause() {
2618
- this._paused = true;
3576
+ _snapshotPauseStates() {
3577
+ return new Map(
3578
+ this.stack.map((scene) => [scene, scene.isPaused])
3579
+ );
2619
3580
  }
2620
- /** Resume the process. */
2621
- resume() {
2622
- this._paused = false;
3581
+ _assertNotMutating(method) {
3582
+ if (this._mutationDepth === 0) return;
3583
+ throw new Error(
3584
+ `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().`
3585
+ );
2623
3586
  }
2624
- /** Cancel the process. */
2625
- cancel() {
2626
- this._cancelled = true;
2627
- this._completed = true;
2628
- this.resolvePromise?.();
3587
+ async _withMutation(work) {
3588
+ this._mutationDepth++;
3589
+ try {
3590
+ return await work();
3591
+ } finally {
3592
+ this._mutationDepth--;
3593
+ }
2629
3594
  }
2630
- /** Returns a promise that resolves when the process completes or is cancelled. */
2631
- toPromise() {
2632
- if (this._completed) return Promise.resolve();
2633
- return new Promise((resolve) => {
2634
- this.resolvePromise = resolve;
2635
- });
3595
+ _withMutationSync(work) {
3596
+ this._mutationDepth++;
3597
+ try {
3598
+ return work();
3599
+ } finally {
3600
+ this._mutationDepth--;
3601
+ }
2636
3602
  }
2637
- /**
2638
- * Advance the process by dt milliseconds.
2639
- * @internal
2640
- */
2641
- _update(dt) {
2642
- if (this._completed || this._paused || this._cancelled) return;
2643
- this.elapsed += dt;
2644
- if (this.duration !== void 0 && this.elapsed >= this.duration) {
2645
- const result2 = this.updateFn(dt, this.elapsed);
2646
- if (this.loop && result2 !== true) {
2647
- this.elapsed = this.elapsed % this.duration;
2648
- return;
3603
+ /** Fire onPause() for scenes that transitioned from not-paused to paused. */
3604
+ _firePauseTransitions(wasPaused) {
3605
+ for (const scene of this.stack) {
3606
+ const was = wasPaused.get(scene) ?? false;
3607
+ if (scene.isPaused && !was) {
3608
+ scene.onPause?.();
2649
3609
  }
2650
- this.complete();
2651
- return;
2652
3610
  }
2653
- const result = this.updateFn(dt, this.elapsed);
2654
- if (result === true) {
2655
- if (this.loop) {
2656
- this.elapsed = 0;
2657
- return;
3611
+ }
3612
+ /** Fire onResume() for scenes that transitioned from paused to not-paused. */
3613
+ _fireResumeTransitions(wasPaused) {
3614
+ for (const scene of this.stack) {
3615
+ const was = wasPaused.get(scene) ?? false;
3616
+ if (!scene.isPaused && was) {
3617
+ scene.onResume?.();
2658
3618
  }
2659
- this.complete();
2660
3619
  }
2661
3620
  }
2662
- /**
2663
- * Reset the process to its initial state so it can be re-run.
2664
- * @internal Used by Sequence for loop/repeat with direct instances.
2665
- */
2666
- _reset() {
2667
- this.elapsed = 0;
2668
- this._completed = false;
2669
- this._paused = false;
2670
- this._cancelled = false;
2671
- delete this.resolvePromise;
2672
- }
2673
- complete() {
2674
- this._completed = true;
2675
- this.onCompleteFn?.();
2676
- this.resolvePromise?.();
2677
- }
2678
3621
  };
2679
- var easeLinear = /* @__PURE__ */ __name((t) => t, "easeLinear");
2680
- var easeInQuad = /* @__PURE__ */ __name((t) => t * t, "easeInQuad");
2681
- var easeOutQuad = /* @__PURE__ */ __name((t) => t * (2 - t), "easeOutQuad");
2682
- var easeInOutQuad = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, "easeInOutQuad");
2683
- var easeOutBounce = /* @__PURE__ */ __name((t) => {
2684
- if (t < 1 / 2.75) {
2685
- return 7.5625 * t * t;
2686
- } else if (t < 2 / 2.75) {
2687
- const t2 = t - 1.5 / 2.75;
2688
- return 7.5625 * t2 * t2 + 0.75;
2689
- } else if (t < 2.5 / 2.75) {
2690
- const t2 = t - 2.25 / 2.75;
2691
- return 7.5625 * t2 * t2 + 0.9375;
2692
- } else {
2693
- const t2 = t - 2.625 / 2.75;
2694
- return 7.5625 * t2 * t2 + 0.984375;
2695
- }
2696
- }, "easeOutBounce");
2697
3622
 
2698
3623
  // src/Tween.ts
2699
3624
  var Tween = {
@@ -2920,7 +3845,9 @@ var ProcessSlot = class {
2920
3845
  };
2921
3846
 
2922
3847
  // src/ProcessComponent.ts
2923
- var ProcessComponent = class extends Component {
3848
+ var _ProcessComponent_decorators, _init2, _a2;
3849
+ _ProcessComponent_decorators = [serializable];
3850
+ var ProcessComponent = class extends (_a2 = Component) {
2924
3851
  static {
2925
3852
  __name(this, "ProcessComponent");
2926
3853
  }
@@ -2990,10 +3917,18 @@ var ProcessComponent = class extends Component {
2990
3917
  onDestroy() {
2991
3918
  this.cancel();
2992
3919
  }
3920
+ serialize() {
3921
+ return null;
3922
+ }
2993
3923
  };
3924
+ _init2 = __decoratorStart(_a2);
3925
+ ProcessComponent = __decorateElement(_init2, 0, "ProcessComponent", _ProcessComponent_decorators, ProcessComponent);
3926
+ __runInitializers(_init2, 1, ProcessComponent);
2994
3927
 
2995
3928
  // src/KeyframeAnimator.ts
2996
- var KeyframeAnimator = class extends Component {
3929
+ var _KeyframeAnimator_decorators, _init3, _a3;
3930
+ _KeyframeAnimator_decorators = [serializable];
3931
+ var KeyframeAnimator = class extends (_a3 = Component) {
2997
3932
  static {
2998
3933
  __name(this, "KeyframeAnimator");
2999
3934
  }
@@ -3045,6 +3980,9 @@ var KeyframeAnimator = class extends Component {
3045
3980
  onDestroy() {
3046
3981
  this.stopAll();
3047
3982
  }
3983
+ serialize() {
3984
+ return null;
3985
+ }
3048
3986
  stopInternal(name, complete) {
3049
3987
  const process = this.active.get(name);
3050
3988
  if (!process) return;
@@ -3053,6 +3991,9 @@ var KeyframeAnimator = class extends Component {
3053
3991
  this.defs[name]?.onExit?.(complete);
3054
3992
  }
3055
3993
  };
3994
+ _init3 = __decoratorStart(_a3);
3995
+ KeyframeAnimator = __decorateElement(_init3, 0, "KeyframeAnimator", _KeyframeAnimator_decorators, KeyframeAnimator);
3996
+ __runInitializers(_init3, 1, KeyframeAnimator);
3056
3997
 
3057
3998
  // src/Sequence.ts
3058
3999
  var Sequence = class {
@@ -3204,36 +4145,94 @@ var ProcessSystem = class extends System {
3204
4145
  /** Global time scale multiplier. Stacks multiplicatively with per-scene timeScale. */
3205
4146
  timeScale = 1;
3206
4147
  sceneManager;
3207
- sceneProcesses = /* @__PURE__ */ new Set();
4148
+ globalProcesses = /* @__PURE__ */ new Set();
4149
+ scenePools = /* @__PURE__ */ new Map();
4150
+ _unregisterSceneHook = null;
3208
4151
  onRegister(context) {
3209
4152
  this.sceneManager = context.resolve(SceneManagerKey);
4153
+ const hooks = context.tryResolve(SceneHookRegistryKey);
4154
+ this._unregisterSceneHook = hooks?.register({
4155
+ afterExit: /* @__PURE__ */ __name((scene) => this.cancelForScene(scene), "afterExit")
4156
+ }) ?? null;
4157
+ }
4158
+ onUnregister() {
4159
+ this._unregisterSceneHook?.();
4160
+ this._unregisterSceneHook = null;
4161
+ for (const p of this.globalProcesses) {
4162
+ if (!p.completed) p.cancel();
4163
+ }
4164
+ this.globalProcesses.clear();
4165
+ for (const pool of this.scenePools.values()) {
4166
+ for (const p of pool) {
4167
+ if (!p.completed) p.cancel();
4168
+ }
4169
+ }
4170
+ this.scenePools.clear();
3210
4171
  }
3211
- /** Add a scene-level process (not tied to any entity). */
4172
+ /**
4173
+ * Add an engine-global process. Ticked under the global timeScale only;
4174
+ * NOT gated by per-scene pause or scaled by per-scene timeScale. Use this
4175
+ * for cross-scene effects (e.g. screen-scope filter fades on `app.stage`)
4176
+ * or processes that have no owning scene.
4177
+ */
3212
4178
  add(process) {
3213
- this.sceneProcesses.add(process);
4179
+ this.globalProcesses.add(process);
4180
+ return process;
4181
+ }
4182
+ /**
4183
+ * Add a process bound to a specific scene's lifecycle. Ticked only while
4184
+ * the scene is active (not paused) and scaled by the scene's `timeScale`,
4185
+ * exactly like an entity-owned `ProcessComponent`. Use this for layer or
4186
+ * scene-scope effect fades that should pause with the scene.
4187
+ */
4188
+ addForScene(scene, process) {
4189
+ let pool = this.scenePools.get(scene);
4190
+ if (!pool) {
4191
+ pool = /* @__PURE__ */ new Set();
4192
+ this.scenePools.set(scene, pool);
4193
+ }
4194
+ pool.add(process);
3214
4195
  return process;
3215
4196
  }
3216
- /** Cancel scene-level processes, optionally by tag. */
4197
+ /** Cancel engine-global processes, optionally by tag. */
3217
4198
  cancel(tag) {
3218
- for (const p of this.sceneProcesses) {
4199
+ for (const p of this.globalProcesses) {
3219
4200
  if (tag === void 0 || p.tags.includes(tag)) {
3220
4201
  p.cancel();
4202
+ this.globalProcesses.delete(p);
3221
4203
  }
3222
4204
  }
3223
- if (tag === void 0) {
3224
- this.sceneProcesses.clear();
4205
+ }
4206
+ /** Cancel every scene-bound process for `scene`, optionally by tag. */
4207
+ cancelForScene(scene, tag) {
4208
+ const pool = this.scenePools.get(scene);
4209
+ if (!pool) return;
4210
+ for (const p of pool) {
4211
+ if (tag === void 0 || p.tags.includes(tag)) {
4212
+ p.cancel();
4213
+ pool.delete(p);
4214
+ }
3225
4215
  }
4216
+ if (pool.size === 0) this.scenePools.delete(scene);
3226
4217
  }
3227
4218
  update(dt) {
3228
4219
  const globalScaledDt = dt * this.timeScale;
3229
- for (const p of this.sceneProcesses) {
4220
+ for (const p of this.globalProcesses) {
3230
4221
  p._update(globalScaledDt);
3231
4222
  if (p.completed) {
3232
- this.sceneProcesses.delete(p);
4223
+ this.globalProcesses.delete(p);
3233
4224
  }
3234
4225
  }
3235
4226
  for (const scene of this.sceneManager.activeScenes) {
3236
4227
  const effectiveDt = globalScaledDt * scene.timeScale;
4228
+ const pool = this.scenePools.get(scene);
4229
+ if (pool) {
4230
+ for (const p of pool) {
4231
+ p._update(effectiveDt);
4232
+ if (p.completed) pool.delete(p);
4233
+ }
4234
+ if (pool.size === 0) this.scenePools.delete(scene);
4235
+ }
3237
4236
  for (const entity of scene.getEntities()) {
3238
4237
  if (entity.isDestroyed) continue;
3239
4238
  const pc = entity.tryGet(ProcessComponent);
@@ -3244,145 +4243,45 @@ var ProcessSystem = class extends System {
3244
4243
  }
3245
4244
  };
3246
4245
 
3247
- // src/Inspector.ts
3248
- var Inspector = class {
3249
- static {
3250
- __name(this, "Inspector");
3251
- }
3252
- engine;
3253
- constructor(engine) {
3254
- this.engine = engine;
3255
- }
3256
- /** Full state snapshot (serializable). */
3257
- snapshot() {
3258
- return {
3259
- frameCount: this.engine.loop.frameCount,
3260
- sceneStack: this.getSceneStack(),
3261
- entityCount: this.countEntities(),
3262
- systemCount: this.getSystems().length,
3263
- errors: this.getErrors()
3264
- };
3265
- }
3266
- /** Find entity by name in the active scene. */
3267
- getEntityByName(name) {
3268
- const entity = this.findActiveEntity(name);
3269
- if (!entity) return void 0;
3270
- return this.entityToSnapshot(entity);
3271
- }
3272
- /** Get entity position (from Transform component). */
3273
- getEntityPosition(name) {
3274
- const entity = this.findActiveEntity(name);
3275
- if (!entity) return void 0;
3276
- const transform = this.getTransform(entity);
3277
- if (!transform) return void 0;
3278
- return { x: transform.position.x, y: transform.position.y };
3279
- }
3280
- /** Check if an entity has a component by class name string. */
3281
- hasComponent(entityName, componentClass) {
3282
- return this.findComponentByName(entityName, componentClass) !== void 0;
3283
- }
3284
- /** Get component data (serializable subset) by class name string. */
3285
- getComponentData(entityName, componentClass) {
3286
- const comp = this.findComponentByName(entityName, componentClass);
3287
- if (!comp) return void 0;
3288
- return this.serializeComponent(comp);
3289
- }
3290
- /** Get all entities in the active scene as snapshots. */
3291
- getEntities() {
3292
- const scene = this.engine.scenes.active;
3293
- if (!scene) return [];
3294
- const result = [];
3295
- for (const entity of scene.getEntities()) {
3296
- if (!entity.isDestroyed) {
3297
- result.push(this.entityToSnapshot(entity));
4246
+ // src/ProcessQueue.ts
4247
+ function makeQueue(route) {
4248
+ const ours = /* @__PURE__ */ new Set();
4249
+ return {
4250
+ run(p) {
4251
+ for (const old of ours) {
4252
+ if (old.completed) ours.delete(old);
3298
4253
  }
3299
- }
3300
- return result;
3301
- }
3302
- /** Get scene stack info. */
3303
- getSceneStack() {
3304
- return this.engine.scenes.all.map((scene) => ({
3305
- name: scene.name,
3306
- entityCount: scene.getEntities().size,
3307
- paused: scene.isPaused
3308
- }));
3309
- }
3310
- /** Get active system info. */
3311
- getSystems() {
3312
- const scheduler = this.engine.context.tryResolve(SystemSchedulerKey);
3313
- if (!scheduler) return [];
3314
- return scheduler.getAllSystems().map((sys) => ({
3315
- name: sys.constructor.name,
3316
- phase: sys.phase,
3317
- priority: sys.priority,
3318
- enabled: sys.enabled
3319
- }));
3320
- }
3321
- /** Get disabled components/systems from error boundary. */
3322
- getErrors() {
3323
- const boundary = this.engine.context.tryResolve(ErrorBoundaryKey);
3324
- if (!boundary) return { disabledSystems: [], disabledComponents: [] };
3325
- const disabled = boundary.getDisabled();
3326
- return {
3327
- disabledSystems: disabled.systems.map(
3328
- (s) => s.system.constructor.name
3329
- ),
3330
- disabledComponents: disabled.components.map((c) => ({
3331
- entity: c.component.entity?.name ?? "unknown",
3332
- component: c.component.constructor.name,
3333
- error: c.error
3334
- }))
3335
- };
3336
- }
3337
- findActiveEntity(name) {
3338
- return this.engine.scenes.active?.findEntity(name);
3339
- }
3340
- findComponentByName(entityName, componentClass) {
3341
- const entity = this.findActiveEntity(entityName);
3342
- if (!entity) return void 0;
3343
- for (const comp of entity.getAll()) {
3344
- if (comp.constructor.name === componentClass) return comp;
3345
- }
3346
- return void 0;
3347
- }
3348
- entityToSnapshot(entity) {
3349
- const transform = this.getTransform(entity);
3350
- const snapshot = {
3351
- id: entity.id,
3352
- name: entity.name,
3353
- tags: [...entity.tags],
3354
- components: [...entity.getAll()].map((c) => c.constructor.name)
3355
- };
3356
- if (transform) {
3357
- snapshot.position = {
3358
- x: transform.position.x,
3359
- y: transform.position.y
3360
- };
3361
- }
3362
- return snapshot;
3363
- }
3364
- getTransform(entity) {
3365
- return entity.has(Transform) ? entity.get(Transform) : void 0;
3366
- }
3367
- serializeComponent(comp) {
3368
- const result = {};
3369
- for (const key of Object.getOwnPropertyNames(comp)) {
3370
- if (key === "entity") continue;
3371
- const value = comp[key];
3372
- if (typeof value !== "function") {
3373
- result[key] = value;
4254
+ route(p);
4255
+ ours.add(p);
4256
+ return p;
4257
+ },
4258
+ cancelAll() {
4259
+ for (const p of ours) {
4260
+ if (!p.completed) p.cancel();
3374
4261
  }
4262
+ ours.clear();
3375
4263
  }
3376
- return result;
3377
- }
3378
- countEntities() {
3379
- let count = 0;
3380
- for (const scene of this.engine.scenes.all) {
3381
- count += scene.getEntities().size;
3382
- }
3383
- return count;
3384
- }
3385
- };
4264
+ };
4265
+ }
4266
+ __name(makeQueue, "makeQueue");
4267
+ function makeEntityScopedQueue(entity) {
4268
+ return makeQueue((p) => {
4269
+ let pc = entity.tryGet(ProcessComponent);
4270
+ if (!pc) {
4271
+ pc = entity.add(new ProcessComponent());
4272
+ }
4273
+ pc.run(p);
4274
+ });
4275
+ }
4276
+ __name(makeEntityScopedQueue, "makeEntityScopedQueue");
4277
+ function makeSceneScopedQueue(processSystem, scene) {
4278
+ return makeQueue((p) => processSystem.addForScene(scene, p));
4279
+ }
4280
+ __name(makeSceneScopedQueue, "makeSceneScopedQueue");
4281
+ function makeGlobalScopedQueue(processSystem) {
4282
+ return makeQueue((p) => processSystem.add(p));
4283
+ }
4284
+ __name(makeGlobalScopedQueue, "makeGlobalScopedQueue");
3386
4285
 
3387
4286
  // src/Engine.ts
3388
4287
  var Engine = class {
@@ -3436,6 +4335,15 @@ var Engine = class {
3436
4335
  this.context.register(SystemSchedulerKey, this.scheduler);
3437
4336
  this.context.register(AssetManagerKey, this.assets);
3438
4337
  this.context.register(SceneHookRegistryKey, this.sceneHooks);
4338
+ this.sceneHooks.register({
4339
+ beforeEnter: /* @__PURE__ */ __name((scene) => {
4340
+ scene._registerScoped(RandomKey, this.inspector.createSceneRandom());
4341
+ this.inspector.attachSceneEventObserver(scene);
4342
+ }, "beforeEnter"),
4343
+ afterExit: /* @__PURE__ */ __name((scene) => {
4344
+ this.inspector.detachSceneEventObserver(scene);
4345
+ }, "afterExit")
4346
+ });
3439
4347
  this.scenes._setContext(this.context);
3440
4348
  this.registerBuiltInSystems();
3441
4349
  this.loop.setCallbacks({
@@ -3517,6 +4425,7 @@ var Engine = class {
3517
4425
  if (this.debug && typeof globalThis !== "undefined" && "__yage__" in globalThis) {
3518
4426
  delete globalThis["__yage__"];
3519
4427
  }
4428
+ this.inspector.dispose();
3520
4429
  this.events.clear();
3521
4430
  this.started = false;
3522
4431
  }
@@ -3614,6 +4523,7 @@ function createMockScene(name = "mock-scene") {
3614
4523
  ctx.register(ErrorBoundaryKey, boundary);
3615
4524
  const scene = new _TestScene(name);
3616
4525
  scene._setContext(ctx);
4526
+ scene._registerScoped(RandomKey, createRandomService(1234));
3617
4527
  return { scene, context: ctx };
3618
4528
  }
3619
4529
  __name(createMockScene, "createMockScene");
@@ -3668,6 +4578,7 @@ var VERSION = "0.0.0";
3668
4578
  QueryCache,
3669
4579
  QueryCacheKey,
3670
4580
  QueryResult,
4581
+ RandomKey,
3671
4582
  RendererAdapterKey,
3672
4583
  SERIALIZABLE_KEY,
3673
4584
  Scene,
@@ -3689,9 +4600,11 @@ var VERSION = "0.0.0";
3689
4600
  Vec2,
3690
4601
  _resetEntityIdCounter,
3691
4602
  advanceFrames,
4603
+ createDefaultRandomSeed,
3692
4604
  createKeyframeTrack,
3693
4605
  createMockEntity,
3694
4606
  createMockScene,
4607
+ createRandomService,
3695
4608
  createTestEngine,
3696
4609
  defineBlueprint,
3697
4610
  defineEvent,
@@ -3703,8 +4616,13 @@ var VERSION = "0.0.0";
3703
4616
  easeOutQuad,
3704
4617
  filterEntities,
3705
4618
  getSerializableType,
4619
+ globalRandom,
3706
4620
  interpolate,
3707
4621
  isSerializable,
4622
+ makeEntityScopedQueue,
4623
+ makeGlobalScopedQueue,
4624
+ makeSceneScopedQueue,
4625
+ normalizeSeed,
3708
4626
  resolveTransition,
3709
4627
  serializable,
3710
4628
  trait