@yagejs/core 0.2.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,8 @@ __export(index_exports, {
99
99
  QueryCache: () => QueryCache,
100
100
  QueryCacheKey: () => QueryCacheKey,
101
101
  QueryResult: () => QueryResult,
102
+ RandomKey: () => RandomKey,
103
+ RendererAdapterKey: () => RendererAdapterKey,
102
104
  SERIALIZABLE_KEY: () => SERIALIZABLE_KEY,
103
105
  Scene: () => Scene,
104
106
  SceneHookRegistry: () => SceneHookRegistry,
@@ -119,9 +121,11 @@ __export(index_exports, {
119
121
  Vec2: () => Vec2,
120
122
  _resetEntityIdCounter: () => _resetEntityIdCounter,
121
123
  advanceFrames: () => advanceFrames,
124
+ createDefaultRandomSeed: () => createDefaultRandomSeed,
122
125
  createKeyframeTrack: () => createKeyframeTrack,
123
126
  createMockEntity: () => createMockEntity,
124
127
  createMockScene: () => createMockScene,
128
+ createRandomService: () => createRandomService,
125
129
  createTestEngine: () => createTestEngine,
126
130
  defineBlueprint: () => defineBlueprint,
127
131
  defineEvent: () => defineEvent,
@@ -133,8 +137,13 @@ __export(index_exports, {
133
137
  easeOutQuad: () => easeOutQuad,
134
138
  filterEntities: () => filterEntities,
135
139
  getSerializableType: () => getSerializableType,
140
+ globalRandom: () => globalRandom,
136
141
  interpolate: () => interpolate,
137
142
  isSerializable: () => isSerializable,
143
+ makeEntityScopedQueue: () => makeEntityScopedQueue,
144
+ makeGlobalScopedQueue: () => makeGlobalScopedQueue,
145
+ makeSceneScopedQueue: () => makeSceneScopedQueue,
146
+ normalizeSeed: () => normalizeSeed,
138
147
  resolveTransition: () => resolveTransition,
139
148
  serializable: () => serializable,
140
149
  trait: () => trait
@@ -265,14 +274,52 @@ var Vec2 = class _Vec2 {
265
274
  static lerp(a, b, t) {
266
275
  return new _Vec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
267
276
  }
277
+ /** Move current toward target by at most maxDelta without overshooting. */
278
+ static moveTowards(current, target, maxDelta) {
279
+ const dx = target.x - current.x;
280
+ const dy = target.y - current.y;
281
+ const distanceSq = dx * dx + dy * dy;
282
+ if (distanceSq < EPSILON * EPSILON) {
283
+ return new _Vec2(target.x, target.y);
284
+ }
285
+ if (maxDelta <= 0) {
286
+ return new _Vec2(current.x, current.y);
287
+ }
288
+ const distance = Math.sqrt(distanceSq);
289
+ if (distance <= maxDelta) {
290
+ return new _Vec2(target.x, target.y);
291
+ }
292
+ const scale = maxDelta / distance;
293
+ return new _Vec2(current.x + dx * scale, current.y + dy * scale);
294
+ }
268
295
  };
269
296
 
270
297
  // src/MathUtils.ts
298
+ var TAU = Math.PI * 2;
299
+ var MIN_SMOOTH_TIME = 1e-4;
300
+ function normalizeAngle(radians) {
301
+ const wrapped = ((radians + Math.PI) % TAU + TAU) % TAU - Math.PI;
302
+ return wrapped === -Math.PI && radians > 0 ? Math.PI : wrapped;
303
+ }
304
+ __name(normalizeAngle, "normalizeAngle");
271
305
  var MathUtils = {
272
306
  /** Linear interpolation between a and b. */
273
307
  lerp(a, b, t) {
274
308
  return a + (b - a) * t;
275
309
  },
310
+ /** Return the clamped interpolation factor that produces v between a and b. */
311
+ inverseLerp(a, b, v) {
312
+ if (a === b) return 0;
313
+ return MathUtils.clamp((v - a) / (b - a), 0, 1);
314
+ },
315
+ /** Interpolate between angles in radians along the shortest path. */
316
+ lerpAngle(a, b, t) {
317
+ return normalizeAngle(a + MathUtils.shortestAngleBetween(a, b) * t);
318
+ },
319
+ /** Signed shortest angular delta from a to b, in radians. */
320
+ shortestAngleBetween(a, b) {
321
+ return normalizeAngle(b - a);
322
+ },
276
323
  /** Clamp a value between min and max. */
277
324
  clamp(value, min, max) {
278
325
  return Math.max(min, Math.min(max, value));
@@ -282,13 +329,11 @@ var MathUtils = {
282
329
  const t = (value - inMin) / (inMax - inMin);
283
330
  return outMin + (outMax - outMin) * t;
284
331
  },
285
- /** Random float in [min, max). */
286
- randomRange(min, max) {
287
- return min + Math.random() * (max - min);
288
- },
289
- /** Random integer in [min, max] (inclusive). */
290
- randomInt(min, max) {
291
- return Math.floor(min + Math.random() * (max - min + 1));
332
+ /** Bounce t between 0 and length. */
333
+ pingPong(t, length) {
334
+ if (length <= 0) return 0;
335
+ const wrapped = MathUtils.wrap(t, 0, length * 2);
336
+ return length - Math.abs(wrapped - length);
292
337
  },
293
338
  /** Convert degrees to radians. */
294
339
  degToRad(degrees) {
@@ -305,6 +350,34 @@ var MathUtils = {
305
350
  }
306
351
  return Math.max(current - step, target);
307
352
  },
353
+ /**
354
+ * Smoothly damp current toward target without overshooting.
355
+ * Pass the returned velocity back into the next call.
356
+ */
357
+ smoothDamp(current, target, velocity, smoothTime, deltaTime, maxSpeed = Infinity) {
358
+ if (deltaTime <= 0) {
359
+ return { value: current, velocity };
360
+ }
361
+ const safeSmoothTime = Math.max(MIN_SMOOTH_TIME, smoothTime);
362
+ const omega = 2 / safeSmoothTime;
363
+ const x = omega * deltaTime;
364
+ const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
365
+ const originalTarget = target;
366
+ const maxChange = maxSpeed * safeSmoothTime;
367
+ const change = MathUtils.clamp(current - target, -maxChange, maxChange);
368
+ const adjustedTarget = current - change;
369
+ const temp = (velocity + omega * change) * deltaTime;
370
+ const nextVelocity = (velocity - omega * temp) * exp;
371
+ let value = adjustedTarget + (change + temp) * exp;
372
+ let resultVelocity = nextVelocity;
373
+ const targetIsAboveCurrent = originalTarget - current > 0;
374
+ const valuePassedTarget = targetIsAboveCurrent ? value > originalTarget : value < originalTarget;
375
+ if (valuePassedTarget) {
376
+ value = originalTarget;
377
+ resultVelocity = 0;
378
+ }
379
+ return { value, velocity: resultVelocity };
380
+ },
308
381
  /** Wrap value into the range [min, max). */
309
382
  wrap(value, min, max) {
310
383
  const range = max - min;
@@ -312,12 +385,138 @@ var MathUtils = {
312
385
  }
313
386
  };
314
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
+
315
513
  // src/EventBus.ts
316
514
  var EventBus = class {
317
515
  static {
318
516
  __name(this, "EventBus");
319
517
  }
320
518
  handlers = /* @__PURE__ */ new Map();
519
+ observers = /* @__PURE__ */ new Set();
321
520
  /** Subscribe to an event. Returns an unsubscribe function. */
322
521
  on(event, handler) {
323
522
  let list = this.handlers.get(event);
@@ -344,6 +543,12 @@ var EventBus = class {
344
543
  }
345
544
  /** Emit an event. Handlers are called synchronously in registration order. */
346
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
+ }
347
552
  const list = this.handlers.get(event);
348
553
  if (!list) return;
349
554
  const snapshot = [...list];
@@ -351,6 +556,16 @@ var EventBus = class {
351
556
  handler(data);
352
557
  }
353
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
+ }
354
569
  /** Remove all handlers for an event, or all handlers if no event specified. */
355
570
  clear(event) {
356
571
  if (event !== void 0) {
@@ -460,63 +675,6 @@ var Logger = class {
460
675
  }
461
676
  };
462
677
 
463
- // src/EngineContext.ts
464
- var ServiceKey = class {
465
- constructor(id, options) {
466
- this.id = id;
467
- this.scope = options?.scope ?? "engine";
468
- }
469
- id;
470
- static {
471
- __name(this, "ServiceKey");
472
- }
473
- /** Declared scope (engine or scene). Defaults to `"engine"`. */
474
- scope;
475
- };
476
- var EngineContext = class {
477
- static {
478
- __name(this, "EngineContext");
479
- }
480
- services = /* @__PURE__ */ new Map();
481
- /** Register a service. Throws if the key is already registered. */
482
- register(key, service) {
483
- if (this.services.has(key.id)) {
484
- throw new Error(`Service "${key.id}" is already registered.`);
485
- }
486
- this.services.set(key.id, service);
487
- }
488
- /** Resolve a service. Throws if not registered. */
489
- resolve(key) {
490
- if (!this.services.has(key.id)) {
491
- throw new Error(`Service "${key.id}" is not registered.`);
492
- }
493
- return this.services.get(key.id);
494
- }
495
- /** Resolve a service, returning undefined if not registered. */
496
- tryResolve(key) {
497
- return this.services.get(key.id);
498
- }
499
- /** Remove a registered service. No-op if not registered. */
500
- unregister(key) {
501
- this.services.delete(key.id);
502
- }
503
- /** Check if a service is registered. */
504
- has(key) {
505
- return this.services.has(key.id);
506
- }
507
- };
508
- var EngineKey = new ServiceKey("engine");
509
- var EventBusKey = new ServiceKey("eventBus");
510
- var SceneManagerKey = new ServiceKey("sceneManager");
511
- var LoggerKey = new ServiceKey("logger");
512
- var InspectorKey = new ServiceKey("inspector");
513
- var QueryCacheKey = new ServiceKey("queryCache");
514
- var ErrorBoundaryKey = new ServiceKey("errorBoundary");
515
- var GameLoopKey = new ServiceKey("gameLoop");
516
- var SystemSchedulerKey = new ServiceKey("systemScheduler");
517
- var ProcessSystemKey = new ServiceKey("processSystem");
518
- var AssetManagerKey = new ServiceKey("assetManager");
519
-
520
678
  // src/SceneHooks.ts
521
679
  var SceneHookRegistry = class {
522
680
  static {
@@ -1253,6 +1411,7 @@ var Entity = class {
1253
1411
  }
1254
1412
  }
1255
1413
  this._scene?._onEntityEvent(token.name, data, this);
1414
+ this._scene?._observeEntityEvent(token.name, data, this);
1256
1415
  }
1257
1416
  /** Get all components as an iterable. */
1258
1417
  getAll() {
@@ -1675,84 +1834,883 @@ var GameLoop = class {
1675
1834
  }
1676
1835
  };
1677
1836
 
1678
- // src/Scene.ts
1679
- 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 {
1680
1844
  static {
1681
- __name(this, "Scene");
1845
+ __name(this, "Inspector");
1682
1846
  }
1683
- /** Whether scenes below this one in the stack should be paused. Default: true. */
1684
- pauseBelow = true;
1685
- /** Whether scenes below this one should still render. Default: false. */
1686
- transparentBelow = false;
1687
- /** Asset handles to load before onEnter(). Override in subclasses. */
1688
- preload;
1689
- /** Default transition used when this scene is the destination of a push/pop/replace. */
1690
- defaultTransition;
1691
- /** Manual pause flag. Set by game code to pause this scene regardless of stack position. */
1692
- paused = false;
1693
- /** Time scale multiplier for this scene. 1.0 = normal, 0.5 = half speed. Default: 1. */
1694
- timeScale = 1;
1695
- entities = /* @__PURE__ */ new Set();
1696
- destroyQueue = [];
1697
- _context;
1698
- entityCallbacks;
1699
- queryCache;
1700
- bus;
1701
- _entityEventHandlers;
1702
- _scopedServices;
1703
- /** Access the EngineContext. */
1704
- get context() {
1705
- 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;
1706
2014
  }
1707
- /** Whether this scene is effectively paused (manual pause or paused by stack). */
1708
- get isPaused() {
1709
- if (this.paused) return true;
1710
- const sm = this._context?.tryResolve(SceneManagerKey);
1711
- if (!sm) return false;
1712
- const stack = sm.all;
1713
- const idx = stack.indexOf(this);
1714
- if (idx === -1) return false;
1715
- for (let i = idx + 1; i < stack.length; i++) {
1716
- 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.");
1717
2023
  }
1718
- 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;
2031
+ }
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);
1719
2039
  }
1720
- /** Whether a scene transition is currently running. */
1721
- get isTransitioning() {
1722
- const sm = this._context?.tryResolve(SceneManagerKey);
1723
- return sm?.isTransitioning ?? false;
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);
1724
2047
  }
1725
- /** Convenience accessor for the AssetManager. */
1726
- get assets() {
1727
- return this._context.resolve(AssetManagerKey);
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
+ };
1728
2064
  }
1729
- /**
1730
- * Lazy proxy-based service resolution. Can be used at field-declaration time:
1731
- * ```ts
1732
- * readonly layers = this.service(RenderLayerManagerKey);
1733
- * ```
1734
- * The actual resolution is deferred until first property access.
1735
- */
1736
- service(key) {
1737
- let resolved;
1738
- return new Proxy({}, {
1739
- get: /* @__PURE__ */ __name((_target, prop) => {
1740
- resolved ??= this._context.resolve(key);
1741
- const value = resolved[prop];
1742
- return typeof value === "function" ? value.bind(resolved) : value;
1743
- }, "get"),
1744
- set: /* @__PURE__ */ __name((_target, prop, value) => {
1745
- resolved ??= this._context.resolve(key);
1746
- resolved[prop] = value;
1747
- return true;
1748
- }, "set")
1749
- });
2065
+ /** Stable JSON form of {@link snapshot}. */
2066
+ snapshotJSON() {
2067
+ return stableStringify(this.snapshot());
1750
2068
  }
1751
- spawn(nameOrBlueprintOrClass, params) {
1752
- if (typeof nameOrBlueprintOrClass === "function") {
1753
- const entity2 = new nameOrBlueprintOrClass();
1754
- entity2._setScene(this, this.entityCallbacks);
1755
- this.entities.add(entity2);
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);
1756
2714
  this.bus?.emit("entity:created", { entity: entity2 });
1757
2715
  entity2.setup?.(params);
1758
2716
  return entity2;
@@ -1845,6 +2803,14 @@ var Scene = class {
1845
2803
  }
1846
2804
  }
1847
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
+ }
1848
2814
  // ---- Internal methods ----
1849
2815
  /**
1850
2816
  * Register a scene-scoped service. Called from a plugin's `beforeEnter`
@@ -1856,6 +2822,13 @@ var Scene = class {
1856
2822
  this._scopedServices ??= /* @__PURE__ */ new Map();
1857
2823
  this._scopedServices.set(key.id, value);
1858
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
+ }
1859
2832
  /**
1860
2833
  * Resolve a scene-scoped service, or `undefined` if none was registered.
1861
2834
  * @internal
@@ -1922,6 +2895,128 @@ var Scene = class {
1922
2895
  }
1923
2896
  };
1924
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
+
1925
3020
  // src/LoadingScene.ts
1926
3021
  var LoadingScene = class extends Scene {
1927
3022
  static {
@@ -1946,6 +3041,7 @@ var LoadingScene = class extends Scene {
1946
3041
  _active = true;
1947
3042
  _continueRequested = false;
1948
3043
  _continueGate;
3044
+ _pendingWaits = /* @__PURE__ */ new Set();
1949
3045
  // Bumped on every `_run` attempt. `AssetManager.loadAll` uses `Promise.all`
1950
3046
  // under the hood, so individual loaders from a failed attempt can still
1951
3047
  // resolve and fire `onProgress` after the attempt rejects. Without this
@@ -2000,28 +3096,32 @@ var LoadingScene = class extends Scene {
2000
3096
  onExit() {
2001
3097
  this._active = false;
2002
3098
  this._continueGate?.();
3099
+ for (const wait of this._pendingWaits) {
3100
+ wait.cancel();
3101
+ }
3102
+ this._pendingWaits.clear();
2003
3103
  }
2004
3104
  async _run() {
2005
- await new Promise((resolve) => setTimeout(resolve, 0));
3105
+ await Promise.resolve();
2006
3106
  if (!this._active) return;
2007
3107
  const attempt = ++this._attempt;
2008
3108
  const target = typeof this.target === "function" ? this.target() : this.target;
2009
- const startedAt = performance.now();
2010
3109
  const bus = this.context.resolve(EventBusKey);
3110
+ const minDuration = this._createEngineTimeDelay(this.minDuration);
2011
3111
  try {
2012
3112
  await this.assets.loadAll(target.preload ?? [], (ratio) => {
2013
3113
  if (!this._active || attempt !== this._attempt) return;
2014
3114
  this._progress = ratio;
2015
3115
  bus.emit("scene:loading:progress", { scene: this, ratio });
2016
3116
  });
2017
- if (!this._active || attempt !== this._attempt) return;
2018
- const elapsed = performance.now() - startedAt;
2019
- const remaining = this.minDuration - elapsed;
2020
- if (remaining > 0) {
2021
- await new Promise((resolve) => setTimeout(resolve, remaining));
2022
- if (!this._active || attempt !== this._attempt) return;
3117
+ if (!this._active || attempt !== this._attempt) {
3118
+ minDuration.cancel();
3119
+ return;
2023
3120
  }
3121
+ await minDuration.promise;
3122
+ if (!this._active || attempt !== this._attempt) return;
2024
3123
  } catch (err) {
3124
+ minDuration.cancel();
2025
3125
  if (!this._active || attempt !== this._attempt) return;
2026
3126
  const error = err instanceof Error ? err : new Error(String(err));
2027
3127
  this._started = false;
@@ -2045,6 +3145,26 @@ var LoadingScene = class extends Scene {
2045
3145
  this.transition ? { transition: this.transition } : void 0
2046
3146
  );
2047
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
+ }
2048
3168
  };
2049
3169
 
2050
3170
  // src/SceneTransition.ts
@@ -2069,6 +3189,29 @@ var SceneManager = class {
2069
3189
  _pendingChain = Promise.resolve();
2070
3190
  _mutationDepth = 0;
2071
3191
  _destroyed = false;
3192
+ _autoPauseOnBlur = false;
3193
+ _isBlurred = false;
3194
+ _visibilityPausedScenes = /* @__PURE__ */ new Set();
3195
+ _visibilityListenerCleanup;
3196
+ /**
3197
+ * Pause all non-paused scenes when `document.hidden` becomes `true`; restore
3198
+ * them on focus. Default: `false`. Only scenes paused by this mechanism are
3199
+ * restored — user-paused scenes (manual `scene.paused = true` or `pauseBelow`
3200
+ * cascade) are never touched.
3201
+ */
3202
+ get autoPauseOnBlur() {
3203
+ return this._autoPauseOnBlur;
3204
+ }
3205
+ set autoPauseOnBlur(value) {
3206
+ if (this._autoPauseOnBlur === value) return;
3207
+ this._autoPauseOnBlur = value;
3208
+ if (!this._isBlurred) return;
3209
+ if (value) {
3210
+ this._applyBlurPause();
3211
+ } else if (this._visibilityPausedScenes.size > 0) {
3212
+ this._restoreBlurPause();
3213
+ }
3214
+ }
2072
3215
  /**
2073
3216
  * Set the engine context.
2074
3217
  * @internal
@@ -2079,6 +3222,40 @@ var SceneManager = class {
2079
3222
  this.assetManager = context.tryResolve(AssetManagerKey);
2080
3223
  this.hookRegistry = context.tryResolve(SceneHookRegistryKey);
2081
3224
  this.logger = context.tryResolve(LoggerKey);
3225
+ if (this._visibilityListenerCleanup || typeof document === "undefined") {
3226
+ return;
3227
+ }
3228
+ const onVisibilityChange = /* @__PURE__ */ __name(() => {
3229
+ this._handleVisibilityChange(document.hidden);
3230
+ }, "onVisibilityChange");
3231
+ document.addEventListener("visibilitychange", onVisibilityChange);
3232
+ this._visibilityListenerCleanup = () => document.removeEventListener("visibilitychange", onVisibilityChange);
3233
+ }
3234
+ /**
3235
+ * React to a visibility change. Parameterised on `hidden` so unit tests can
3236
+ * drive it without a real `document`.
3237
+ * @internal
3238
+ */
3239
+ _handleVisibilityChange(hidden) {
3240
+ if (hidden && !this._isBlurred) {
3241
+ this._isBlurred = true;
3242
+ if (this._autoPauseOnBlur) this._applyBlurPause();
3243
+ } else if (!hidden && this._isBlurred) {
3244
+ this._isBlurred = false;
3245
+ if (this._visibilityPausedScenes.size > 0) this._restoreBlurPause();
3246
+ }
3247
+ }
3248
+ _applyBlurPause() {
3249
+ for (const scene of this.activeScenes) {
3250
+ scene.paused = true;
3251
+ this._visibilityPausedScenes.add(scene);
3252
+ }
3253
+ }
3254
+ _restoreBlurPause() {
3255
+ for (const scene of this._visibilityPausedScenes) {
3256
+ scene.paused = false;
3257
+ }
3258
+ this._visibilityPausedScenes.clear();
2082
3259
  }
2083
3260
  /** The topmost (active) scene. */
2084
3261
  get active() {
@@ -2205,6 +3382,9 @@ var SceneManager = class {
2205
3382
  this._cleanupRun(this._currentRun);
2206
3383
  }
2207
3384
  this._pendingChain = Promise.resolve();
3385
+ this._visibilityListenerCleanup?.();
3386
+ this._visibilityListenerCleanup = void 0;
3387
+ this._visibilityPausedScenes.clear();
2208
3388
  this._withMutationSync(() => {
2209
3389
  while (this.stack.length > 0) {
2210
3390
  const scene = this.stack.pop();
@@ -2324,6 +3504,7 @@ var SceneManager = class {
2324
3504
  scene._destroyAllEntities();
2325
3505
  this.hookRegistry?.runAfterExit(scene);
2326
3506
  scene._clearScopedServices();
3507
+ this._visibilityPausedScenes.delete(scene);
2327
3508
  }
2328
3509
  async _runTransition(kind, transition, fromScene, toScene) {
2329
3510
  if (this._destroyed) return;
@@ -2392,174 +3573,52 @@ var SceneManager = class {
2392
3573
  toScene: run.toScene
2393
3574
  };
2394
3575
  }
2395
- _snapshotPauseStates() {
2396
- return new Map(
2397
- this.stack.map((scene) => [scene, scene.isPaused])
2398
- );
2399
- }
2400
- _assertNotMutating(method) {
2401
- if (this._mutationDepth === 0) return;
2402
- throw new Error(
2403
- `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().`
2404
- );
2405
- }
2406
- async _withMutation(work) {
2407
- this._mutationDepth++;
2408
- try {
2409
- return await work();
2410
- } finally {
2411
- this._mutationDepth--;
2412
- }
2413
- }
2414
- _withMutationSync(work) {
2415
- this._mutationDepth++;
2416
- try {
2417
- return work();
2418
- } finally {
2419
- this._mutationDepth--;
2420
- }
2421
- }
2422
- /** Fire onPause() for scenes that transitioned from not-paused to paused. */
2423
- _firePauseTransitions(wasPaused) {
2424
- for (const scene of this.stack) {
2425
- const was = wasPaused.get(scene) ?? false;
2426
- if (scene.isPaused && !was) {
2427
- scene.onPause?.();
2428
- }
2429
- }
2430
- }
2431
- /** Fire onResume() for scenes that transitioned from paused to not-paused. */
2432
- _fireResumeTransitions(wasPaused) {
2433
- for (const scene of this.stack) {
2434
- const was = wasPaused.get(scene) ?? false;
2435
- if (!scene.isPaused && was) {
2436
- scene.onResume?.();
2437
- }
2438
- }
2439
- }
2440
- };
2441
-
2442
- // src/Process.ts
2443
- var Process = class _Process {
2444
- static {
2445
- __name(this, "Process");
2446
- }
2447
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
2448
- updateFn;
2449
- onCompleteFn;
2450
- duration;
2451
- loop;
2452
- /** Tags for filtering/grouping. */
2453
- tags;
2454
- elapsed = 0;
2455
- _completed = false;
2456
- _paused = false;
2457
- _cancelled = false;
2458
- resolvePromise;
2459
- /** Create a timer that fires `onComplete` after `duration` ms. */
2460
- static delay(duration, onComplete, tags) {
2461
- const opts = { duration };
2462
- if (onComplete !== void 0) opts.onComplete = onComplete;
2463
- if (tags !== void 0) opts.tags = tags;
2464
- return new _Process(opts);
2465
- }
2466
- constructor(options) {
2467
- this.updateFn = options.update ?? (() => {
2468
- });
2469
- this.onCompleteFn = options.onComplete;
2470
- this.duration = options.duration;
2471
- this.loop = options.loop ?? false;
2472
- this.tags = options.tags ?? [];
2473
- }
2474
- /** Whether the process has completed. */
2475
- get completed() {
2476
- return this._completed;
2477
- }
2478
- /** Whether the process is paused. */
2479
- get paused() {
2480
- return this._paused;
2481
- }
2482
- /** Pause the process. */
2483
- pause() {
2484
- this._paused = true;
3576
+ _snapshotPauseStates() {
3577
+ return new Map(
3578
+ this.stack.map((scene) => [scene, scene.isPaused])
3579
+ );
2485
3580
  }
2486
- /** Resume the process. */
2487
- resume() {
2488
- 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
+ );
2489
3586
  }
2490
- /** Cancel the process. */
2491
- cancel() {
2492
- this._cancelled = true;
2493
- this._completed = true;
2494
- this.resolvePromise?.();
3587
+ async _withMutation(work) {
3588
+ this._mutationDepth++;
3589
+ try {
3590
+ return await work();
3591
+ } finally {
3592
+ this._mutationDepth--;
3593
+ }
2495
3594
  }
2496
- /** Returns a promise that resolves when the process completes or is cancelled. */
2497
- toPromise() {
2498
- if (this._completed) return Promise.resolve();
2499
- return new Promise((resolve) => {
2500
- this.resolvePromise = resolve;
2501
- });
3595
+ _withMutationSync(work) {
3596
+ this._mutationDepth++;
3597
+ try {
3598
+ return work();
3599
+ } finally {
3600
+ this._mutationDepth--;
3601
+ }
2502
3602
  }
2503
- /**
2504
- * Advance the process by dt milliseconds.
2505
- * @internal
2506
- */
2507
- _update(dt) {
2508
- if (this._completed || this._paused || this._cancelled) return;
2509
- this.elapsed += dt;
2510
- if (this.duration !== void 0 && this.elapsed >= this.duration) {
2511
- const result2 = this.updateFn(dt, this.elapsed);
2512
- if (this.loop && result2 !== true) {
2513
- this.elapsed = this.elapsed % this.duration;
2514
- 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?.();
2515
3609
  }
2516
- this.complete();
2517
- return;
2518
3610
  }
2519
- const result = this.updateFn(dt, this.elapsed);
2520
- if (result === true) {
2521
- if (this.loop) {
2522
- this.elapsed = 0;
2523
- 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?.();
2524
3618
  }
2525
- this.complete();
2526
3619
  }
2527
3620
  }
2528
- /**
2529
- * Reset the process to its initial state so it can be re-run.
2530
- * @internal Used by Sequence for loop/repeat with direct instances.
2531
- */
2532
- _reset() {
2533
- this.elapsed = 0;
2534
- this._completed = false;
2535
- this._paused = false;
2536
- this._cancelled = false;
2537
- delete this.resolvePromise;
2538
- }
2539
- complete() {
2540
- this._completed = true;
2541
- this.onCompleteFn?.();
2542
- this.resolvePromise?.();
2543
- }
2544
3621
  };
2545
- var easeLinear = /* @__PURE__ */ __name((t) => t, "easeLinear");
2546
- var easeInQuad = /* @__PURE__ */ __name((t) => t * t, "easeInQuad");
2547
- var easeOutQuad = /* @__PURE__ */ __name((t) => t * (2 - t), "easeOutQuad");
2548
- var easeInOutQuad = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, "easeInOutQuad");
2549
- var easeOutBounce = /* @__PURE__ */ __name((t) => {
2550
- if (t < 1 / 2.75) {
2551
- return 7.5625 * t * t;
2552
- } else if (t < 2 / 2.75) {
2553
- const t2 = t - 1.5 / 2.75;
2554
- return 7.5625 * t2 * t2 + 0.75;
2555
- } else if (t < 2.5 / 2.75) {
2556
- const t2 = t - 2.25 / 2.75;
2557
- return 7.5625 * t2 * t2 + 0.9375;
2558
- } else {
2559
- const t2 = t - 2.625 / 2.75;
2560
- return 7.5625 * t2 * t2 + 0.984375;
2561
- }
2562
- }, "easeOutBounce");
2563
3622
 
2564
3623
  // src/Tween.ts
2565
3624
  var Tween = {
@@ -2786,7 +3845,9 @@ var ProcessSlot = class {
2786
3845
  };
2787
3846
 
2788
3847
  // src/ProcessComponent.ts
2789
- var ProcessComponent = class extends Component {
3848
+ var _ProcessComponent_decorators, _init2, _a2;
3849
+ _ProcessComponent_decorators = [serializable];
3850
+ var ProcessComponent = class extends (_a2 = Component) {
2790
3851
  static {
2791
3852
  __name(this, "ProcessComponent");
2792
3853
  }
@@ -2856,10 +3917,18 @@ var ProcessComponent = class extends Component {
2856
3917
  onDestroy() {
2857
3918
  this.cancel();
2858
3919
  }
3920
+ serialize() {
3921
+ return null;
3922
+ }
2859
3923
  };
3924
+ _init2 = __decoratorStart(_a2);
3925
+ ProcessComponent = __decorateElement(_init2, 0, "ProcessComponent", _ProcessComponent_decorators, ProcessComponent);
3926
+ __runInitializers(_init2, 1, ProcessComponent);
2860
3927
 
2861
3928
  // src/KeyframeAnimator.ts
2862
- var KeyframeAnimator = class extends Component {
3929
+ var _KeyframeAnimator_decorators, _init3, _a3;
3930
+ _KeyframeAnimator_decorators = [serializable];
3931
+ var KeyframeAnimator = class extends (_a3 = Component) {
2863
3932
  static {
2864
3933
  __name(this, "KeyframeAnimator");
2865
3934
  }
@@ -2911,6 +3980,9 @@ var KeyframeAnimator = class extends Component {
2911
3980
  onDestroy() {
2912
3981
  this.stopAll();
2913
3982
  }
3983
+ serialize() {
3984
+ return null;
3985
+ }
2914
3986
  stopInternal(name, complete) {
2915
3987
  const process = this.active.get(name);
2916
3988
  if (!process) return;
@@ -2919,6 +3991,9 @@ var KeyframeAnimator = class extends Component {
2919
3991
  this.defs[name]?.onExit?.(complete);
2920
3992
  }
2921
3993
  };
3994
+ _init3 = __decoratorStart(_a3);
3995
+ KeyframeAnimator = __decorateElement(_init3, 0, "KeyframeAnimator", _KeyframeAnimator_decorators, KeyframeAnimator);
3996
+ __runInitializers(_init3, 1, KeyframeAnimator);
2922
3997
 
2923
3998
  // src/Sequence.ts
2924
3999
  var Sequence = class {
@@ -3070,36 +4145,94 @@ var ProcessSystem = class extends System {
3070
4145
  /** Global time scale multiplier. Stacks multiplicatively with per-scene timeScale. */
3071
4146
  timeScale = 1;
3072
4147
  sceneManager;
3073
- sceneProcesses = /* @__PURE__ */ new Set();
4148
+ globalProcesses = /* @__PURE__ */ new Set();
4149
+ scenePools = /* @__PURE__ */ new Map();
4150
+ _unregisterSceneHook = null;
3074
4151
  onRegister(context) {
3075
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();
3076
4171
  }
3077
- /** 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
+ */
3078
4178
  add(process) {
3079
- 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);
3080
4195
  return process;
3081
4196
  }
3082
- /** Cancel scene-level processes, optionally by tag. */
4197
+ /** Cancel engine-global processes, optionally by tag. */
3083
4198
  cancel(tag) {
3084
- for (const p of this.sceneProcesses) {
4199
+ for (const p of this.globalProcesses) {
3085
4200
  if (tag === void 0 || p.tags.includes(tag)) {
3086
4201
  p.cancel();
4202
+ this.globalProcesses.delete(p);
3087
4203
  }
3088
4204
  }
3089
- if (tag === void 0) {
3090
- 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
+ }
3091
4215
  }
4216
+ if (pool.size === 0) this.scenePools.delete(scene);
3092
4217
  }
3093
4218
  update(dt) {
3094
4219
  const globalScaledDt = dt * this.timeScale;
3095
- for (const p of this.sceneProcesses) {
4220
+ for (const p of this.globalProcesses) {
3096
4221
  p._update(globalScaledDt);
3097
4222
  if (p.completed) {
3098
- this.sceneProcesses.delete(p);
4223
+ this.globalProcesses.delete(p);
3099
4224
  }
3100
4225
  }
3101
4226
  for (const scene of this.sceneManager.activeScenes) {
3102
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
+ }
3103
4236
  for (const entity of scene.getEntities()) {
3104
4237
  if (entity.isDestroyed) continue;
3105
4238
  const pc = entity.tryGet(ProcessComponent);
@@ -3110,145 +4243,45 @@ var ProcessSystem = class extends System {
3110
4243
  }
3111
4244
  };
3112
4245
 
3113
- // src/Inspector.ts
3114
- var Inspector = class {
3115
- static {
3116
- __name(this, "Inspector");
3117
- }
3118
- engine;
3119
- constructor(engine) {
3120
- this.engine = engine;
3121
- }
3122
- /** Full state snapshot (serializable). */
3123
- snapshot() {
3124
- return {
3125
- frameCount: this.engine.loop.frameCount,
3126
- sceneStack: this.getSceneStack(),
3127
- entityCount: this.countEntities(),
3128
- systemCount: this.getSystems().length,
3129
- errors: this.getErrors()
3130
- };
3131
- }
3132
- /** Find entity by name in the active scene. */
3133
- getEntityByName(name) {
3134
- const entity = this.findActiveEntity(name);
3135
- if (!entity) return void 0;
3136
- return this.entityToSnapshot(entity);
3137
- }
3138
- /** Get entity position (from Transform component). */
3139
- getEntityPosition(name) {
3140
- const entity = this.findActiveEntity(name);
3141
- if (!entity) return void 0;
3142
- const transform = this.getTransform(entity);
3143
- if (!transform) return void 0;
3144
- return { x: transform.position.x, y: transform.position.y };
3145
- }
3146
- /** Check if an entity has a component by class name string. */
3147
- hasComponent(entityName, componentClass) {
3148
- return this.findComponentByName(entityName, componentClass) !== void 0;
3149
- }
3150
- /** Get component data (serializable subset) by class name string. */
3151
- getComponentData(entityName, componentClass) {
3152
- const comp = this.findComponentByName(entityName, componentClass);
3153
- if (!comp) return void 0;
3154
- return this.serializeComponent(comp);
3155
- }
3156
- /** Get all entities in the active scene as snapshots. */
3157
- getEntities() {
3158
- const scene = this.engine.scenes.active;
3159
- if (!scene) return [];
3160
- const result = [];
3161
- for (const entity of scene.getEntities()) {
3162
- if (!entity.isDestroyed) {
3163
- 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);
3164
4253
  }
3165
- }
3166
- return result;
3167
- }
3168
- /** Get scene stack info. */
3169
- getSceneStack() {
3170
- return this.engine.scenes.all.map((scene) => ({
3171
- name: scene.name,
3172
- entityCount: scene.getEntities().size,
3173
- paused: scene.isPaused
3174
- }));
3175
- }
3176
- /** Get active system info. */
3177
- getSystems() {
3178
- const scheduler = this.engine.context.tryResolve(SystemSchedulerKey);
3179
- if (!scheduler) return [];
3180
- return scheduler.getAllSystems().map((sys) => ({
3181
- name: sys.constructor.name,
3182
- phase: sys.phase,
3183
- priority: sys.priority,
3184
- enabled: sys.enabled
3185
- }));
3186
- }
3187
- /** Get disabled components/systems from error boundary. */
3188
- getErrors() {
3189
- const boundary = this.engine.context.tryResolve(ErrorBoundaryKey);
3190
- if (!boundary) return { disabledSystems: [], disabledComponents: [] };
3191
- const disabled = boundary.getDisabled();
3192
- return {
3193
- disabledSystems: disabled.systems.map(
3194
- (s) => s.system.constructor.name
3195
- ),
3196
- disabledComponents: disabled.components.map((c) => ({
3197
- entity: c.component.entity?.name ?? "unknown",
3198
- component: c.component.constructor.name,
3199
- error: c.error
3200
- }))
3201
- };
3202
- }
3203
- findActiveEntity(name) {
3204
- return this.engine.scenes.active?.findEntity(name);
3205
- }
3206
- findComponentByName(entityName, componentClass) {
3207
- const entity = this.findActiveEntity(entityName);
3208
- if (!entity) return void 0;
3209
- for (const comp of entity.getAll()) {
3210
- if (comp.constructor.name === componentClass) return comp;
3211
- }
3212
- return void 0;
3213
- }
3214
- entityToSnapshot(entity) {
3215
- const transform = this.getTransform(entity);
3216
- const snapshot = {
3217
- id: entity.id,
3218
- name: entity.name,
3219
- tags: [...entity.tags],
3220
- components: [...entity.getAll()].map((c) => c.constructor.name)
3221
- };
3222
- if (transform) {
3223
- snapshot.position = {
3224
- x: transform.position.x,
3225
- y: transform.position.y
3226
- };
3227
- }
3228
- return snapshot;
3229
- }
3230
- getTransform(entity) {
3231
- return entity.has(Transform) ? entity.get(Transform) : void 0;
3232
- }
3233
- serializeComponent(comp) {
3234
- const result = {};
3235
- for (const key of Object.getOwnPropertyNames(comp)) {
3236
- if (key === "entity") continue;
3237
- const value = comp[key];
3238
- if (typeof value !== "function") {
3239
- 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();
3240
4261
  }
4262
+ ours.clear();
3241
4263
  }
3242
- return result;
3243
- }
3244
- countEntities() {
3245
- let count = 0;
3246
- for (const scene of this.engine.scenes.all) {
3247
- count += scene.getEntities().size;
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());
3248
4272
  }
3249
- return count;
3250
- }
3251
- };
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");
3252
4285
 
3253
4286
  // src/Engine.ts
3254
4287
  var Engine = class {
@@ -3302,6 +4335,15 @@ var Engine = class {
3302
4335
  this.context.register(SystemSchedulerKey, this.scheduler);
3303
4336
  this.context.register(AssetManagerKey, this.assets);
3304
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
+ });
3305
4347
  this.scenes._setContext(this.context);
3306
4348
  this.registerBuiltInSystems();
3307
4349
  this.loop.setCallbacks({
@@ -3383,6 +4425,7 @@ var Engine = class {
3383
4425
  if (this.debug && typeof globalThis !== "undefined" && "__yage__" in globalThis) {
3384
4426
  delete globalThis["__yage__"];
3385
4427
  }
4428
+ this.inspector.dispose();
3386
4429
  this.events.clear();
3387
4430
  this.started = false;
3388
4431
  }
@@ -3445,6 +4488,11 @@ var Engine = class {
3445
4488
  }
3446
4489
  };
3447
4490
 
4491
+ // src/RendererAdapter.ts
4492
+ var RendererAdapterKey = new ServiceKey(
4493
+ "rendererAdapter"
4494
+ );
4495
+
3448
4496
  // src/test-utils.ts
3449
4497
  var _TestScene = class extends Scene {
3450
4498
  static {
@@ -3475,6 +4523,7 @@ function createMockScene(name = "mock-scene") {
3475
4523
  ctx.register(ErrorBoundaryKey, boundary);
3476
4524
  const scene = new _TestScene(name);
3477
4525
  scene._setContext(ctx);
4526
+ scene._registerScoped(RandomKey, createRandomService(1234));
3478
4527
  return { scene, context: ctx };
3479
4528
  }
3480
4529
  __name(createMockScene, "createMockScene");
@@ -3529,6 +4578,8 @@ var VERSION = "0.0.0";
3529
4578
  QueryCache,
3530
4579
  QueryCacheKey,
3531
4580
  QueryResult,
4581
+ RandomKey,
4582
+ RendererAdapterKey,
3532
4583
  SERIALIZABLE_KEY,
3533
4584
  Scene,
3534
4585
  SceneHookRegistry,
@@ -3549,9 +4600,11 @@ var VERSION = "0.0.0";
3549
4600
  Vec2,
3550
4601
  _resetEntityIdCounter,
3551
4602
  advanceFrames,
4603
+ createDefaultRandomSeed,
3552
4604
  createKeyframeTrack,
3553
4605
  createMockEntity,
3554
4606
  createMockScene,
4607
+ createRandomService,
3555
4608
  createTestEngine,
3556
4609
  defineBlueprint,
3557
4610
  defineEvent,
@@ -3563,8 +4616,13 @@ var VERSION = "0.0.0";
3563
4616
  easeOutQuad,
3564
4617
  filterEntities,
3565
4618
  getSerializableType,
4619
+ globalRandom,
3566
4620
  interpolate,
3567
4621
  isSerializable,
4622
+ makeEntityScopedQueue,
4623
+ makeGlobalScopedQueue,
4624
+ makeSceneScopedQueue,
4625
+ normalizeSeed,
3568
4626
  resolveTransition,
3569
4627
  serializable,
3570
4628
  trait