@energy8platform/game-engine 0.7.1 → 0.9.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.esm.js CHANGED
@@ -363,14 +363,17 @@ class Tween {
363
363
  * ```
364
364
  */
365
365
  class SceneManager extends EventEmitter {
366
+ static MAX_TRANSITION_DEPTH = 10;
366
367
  /** Root container that scenes are added to */
367
368
  root;
368
369
  registry = new Map();
369
370
  stack = [];
370
- _transitioning = false;
371
+ _transitionDepth = 0;
371
372
  /** Current viewport dimensions — set by ViewportManager */
372
373
  _width = 0;
373
374
  _height = 0;
375
+ /** @internal GameApplication reference — passed to scenes */
376
+ _app;
374
377
  constructor(root) {
375
378
  super();
376
379
  if (root)
@@ -380,6 +383,10 @@ class SceneManager extends EventEmitter {
380
383
  setRoot(root) {
381
384
  this.root = root;
382
385
  }
386
+ /** @internal Set the app reference (called by GameApplication) */
387
+ setApp(app) {
388
+ this._app = app;
389
+ }
383
390
  /** Register a scene class by key */
384
391
  register(key, ctor) {
385
392
  this.registry.set(key, ctor);
@@ -395,12 +402,15 @@ class SceneManager extends EventEmitter {
395
402
  }
396
403
  /** Whether a scene transition is in progress */
397
404
  get isTransitioning() {
398
- return this._transitioning;
405
+ return this._transitionDepth > 0;
399
406
  }
400
407
  /**
401
408
  * Navigate to a scene, replacing the entire stack.
402
409
  */
403
410
  async goto(key, data, transition) {
411
+ if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
412
+ throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
413
+ }
404
414
  const prevKey = this.currentKey;
405
415
  // Exit all current scenes
406
416
  while (this.stack.length > 0) {
@@ -415,6 +425,9 @@ class SceneManager extends EventEmitter {
415
425
  * Useful for overlays, modals, pause screens.
416
426
  */
417
427
  async push(key, data, transition) {
428
+ if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
429
+ throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
430
+ }
418
431
  const prevKey = this.currentKey;
419
432
  await this.pushInternal(key, data, transition);
420
433
  this.emit('change', { from: prevKey, to: key });
@@ -427,6 +440,9 @@ class SceneManager extends EventEmitter {
427
440
  console.warn('[SceneManager] Cannot pop the last scene');
428
441
  return;
429
442
  }
443
+ if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
444
+ throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
445
+ }
430
446
  const prevKey = this.currentKey;
431
447
  await this.popInternal(true, transition);
432
448
  this.emit('change', { from: prevKey, to: this.currentKey });
@@ -435,6 +451,9 @@ class SceneManager extends EventEmitter {
435
451
  * Replace the top scene with a new one.
436
452
  */
437
453
  async replace(key, data, transition) {
454
+ if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
455
+ throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
456
+ }
438
457
  const prevKey = this.currentKey;
439
458
  await this.popInternal(false);
440
459
  await this.pushInternal(key, data, transition);
@@ -476,10 +495,14 @@ class SceneManager extends EventEmitter {
476
495
  if (!Ctor) {
477
496
  throw new Error(`[SceneManager] Scene "${key}" is not registered`);
478
497
  }
479
- return new Ctor();
498
+ const scene = new Ctor();
499
+ if (this._app) {
500
+ scene.__engineApp = this._app;
501
+ }
502
+ return scene;
480
503
  }
481
504
  async pushInternal(key, data, transition) {
482
- this._transitioning = true;
505
+ this._transitionDepth++;
483
506
  const scene = this.createScene(key);
484
507
  this.root.addChild(scene.container);
485
508
  // Set initial size
@@ -491,20 +514,20 @@ class SceneManager extends EventEmitter {
491
514
  // Push to stack BEFORE onEnter so currentKey is correct during initialization
492
515
  this.stack.push({ scene, key });
493
516
  await scene.onEnter?.(data);
494
- this._transitioning = false;
517
+ this._transitionDepth--;
495
518
  }
496
519
  async popInternal(showTransition, transition) {
497
520
  const entry = this.stack.pop();
498
521
  if (!entry)
499
522
  return;
500
- this._transitioning = true;
523
+ this._transitionDepth++;
501
524
  await entry.scene.onExit?.();
502
525
  if (showTransition) {
503
526
  await this.transitionOut(entry.scene.container, transition);
504
527
  }
505
528
  entry.scene.onDestroy?.();
506
529
  entry.scene.container.destroy({ children: true });
507
- this._transitioning = false;
530
+ this._transitionDepth--;
508
531
  }
509
532
  async transitionIn(container, config) {
510
533
  const type = config?.type ?? TransitionType.NONE;
@@ -2237,6 +2260,7 @@ class GameApplication extends EventEmitter {
2237
2260
  });
2238
2261
  // Wire SceneManager to the PixiJS stage
2239
2262
  this.scenes.setRoot(this.app.stage);
2263
+ this.scenes.setApp(this);
2240
2264
  // Wire viewport resize → scene manager + input manager
2241
2265
  this.viewport.on('resize', ({ width, height, scale }) => {
2242
2266
  this.scenes.resize(width, height);
@@ -2322,10 +2346,11 @@ class GameApplication extends EventEmitter {
2322
2346
  * ```
2323
2347
  */
2324
2348
  class StateMachine extends EventEmitter {
2349
+ static MAX_TRANSITION_DEPTH = 10;
2325
2350
  _states = new Map();
2326
2351
  _guards = new Map();
2327
2352
  _current = null;
2328
- _transitioning = false;
2353
+ _transitionDepth = 0;
2329
2354
  _context;
2330
2355
  constructor(context) {
2331
2356
  super();
@@ -2337,7 +2362,7 @@ class StateMachine extends EventEmitter {
2337
2362
  }
2338
2363
  /** Whether a transition is in progress */
2339
2364
  get isTransitioning() {
2340
- return this._transitioning;
2365
+ return this._transitionDepth > 0;
2341
2366
  }
2342
2367
  /** State machine context (shared data) */
2343
2368
  get context() {
@@ -2385,9 +2410,8 @@ class StateMachine extends EventEmitter {
2385
2410
  * @returns true if the transition succeeded, false if blocked by a guard
2386
2411
  */
2387
2412
  async transition(to, data) {
2388
- if (this._transitioning) {
2389
- console.warn('[StateMachine] Transition already in progress');
2390
- return false;
2413
+ if (this._transitionDepth >= StateMachine.MAX_TRANSITION_DEPTH) {
2414
+ throw new Error('[StateMachine] Max transition depth exceeded — possible infinite loop');
2391
2415
  }
2392
2416
  const from = this._current;
2393
2417
  // Check guard
@@ -2402,7 +2426,7 @@ class StateMachine extends EventEmitter {
2402
2426
  if (!toState) {
2403
2427
  throw new Error(`[StateMachine] State "${to}" not registered.`);
2404
2428
  }
2405
- this._transitioning = true;
2429
+ this._transitionDepth++;
2406
2430
  try {
2407
2431
  // Exit current state
2408
2432
  if (from !== null) {
@@ -2419,7 +2443,7 @@ class StateMachine extends EventEmitter {
2419
2443
  throw err;
2420
2444
  }
2421
2445
  finally {
2422
- this._transitioning = false;
2446
+ this._transitionDepth--;
2423
2447
  }
2424
2448
  return true;
2425
2449
  }
@@ -2460,7 +2484,7 @@ class StateMachine extends EventEmitter {
2460
2484
  await state?.exit?.(this._context);
2461
2485
  }
2462
2486
  this._current = null;
2463
- this._transitioning = false;
2487
+ this._transitionDepth = 0;
2464
2488
  }
2465
2489
  /**
2466
2490
  * Destroy the state machine.