@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/README.md +178 -68
- package/dist/core.cjs.js +31 -7
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +8 -1
- package/dist/core.esm.js +31 -7
- package/dist/core.esm.js.map +1 -1
- package/dist/index.cjs.js +39 -15
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.esm.js +39 -15
- package/dist/index.esm.js.map +1 -1
- package/dist/react.cjs.js +470 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.d.ts +871 -0
- package/dist/react.esm.js +455 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/vite.cjs.js +5 -0
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +5 -0
- package/dist/vite.esm.js.map +1 -1
- package/package.json +25 -3
- package/src/core/GameApplication.ts +1 -0
- package/src/core/SceneManager.ts +33 -7
- package/src/react/EngineContext.ts +26 -0
- package/src/react/ReactScene.ts +88 -0
- package/src/react/applyProps.ts +107 -0
- package/src/react/catalogue.ts +17 -0
- package/src/react/createPixiRoot.ts +31 -0
- package/src/react/extendAll.ts +51 -0
- package/src/react/hooks.ts +46 -0
- package/src/react/index.ts +23 -0
- package/src/react/reconciler.ts +169 -0
- package/src/state/StateMachine.ts +11 -8
- package/src/types.ts +3 -0
- package/src/vite/index.ts +5 -0
package/README.md
CHANGED
|
@@ -37,7 +37,10 @@ A universal casino game engine built on [PixiJS v8](https://pixijs.com/) and [@e
|
|
|
37
37
|
- [DevBridge](#devbridge)
|
|
38
38
|
- [Debug](#debug)
|
|
39
39
|
- [Flexbox-First Layout](#flexbox-first-layout)
|
|
40
|
-
- [
|
|
40
|
+
- [React Integration](#react-integration)
|
|
41
|
+
- [ReactScene](#reactscene)
|
|
42
|
+
- [Hooks](#hooks)
|
|
43
|
+
- [Standalone createPixiRoot](#standalone-createpixiroot)
|
|
41
44
|
- [API Reference](#api-reference)
|
|
42
45
|
- [License](#license)
|
|
43
46
|
|
|
@@ -57,7 +60,7 @@ npm install pixi.js @energy8platform/game-sdk @energy8platform/game-engine
|
|
|
57
60
|
npm install @pixi/ui @pixi/layout yoga-layout
|
|
58
61
|
|
|
59
62
|
# (Optional) React integration
|
|
60
|
-
npm install
|
|
63
|
+
npm install react react-dom react-reconciler
|
|
61
64
|
|
|
62
65
|
# (Optional) install spine and audio support
|
|
63
66
|
npm install @pixi/sound @esotericsoftware/spine-pixi-v8
|
|
@@ -133,7 +136,9 @@ bootstrap();
|
|
|
133
136
|
| `yoga-layout` | `^3.0.0` | Optional — peer dep of `@pixi/layout` |
|
|
134
137
|
| `@pixi/sound` | `^6.0.0` | Optional — for audio |
|
|
135
138
|
| `@esotericsoftware/spine-pixi-v8` | `~4.2.0` | Optional — for Spine animations |
|
|
136
|
-
|
|
|
139
|
+
| `react` | `>=18.0.0` | Optional — for ReactScene (see [React Integration](#react-integration)) |
|
|
140
|
+
| `react-dom` | `>=18.0.0` | Optional — peer of `react` |
|
|
141
|
+
| `react-reconciler` | `>=0.29.0` | Optional — for ReactScene (custom PixiJS reconciler) |
|
|
137
142
|
|
|
138
143
|
### Sub-path Exports
|
|
139
144
|
|
|
@@ -147,6 +152,7 @@ import { AudioManager } from '@energy8platform/game-engine/audio';
|
|
|
147
152
|
import { Button, Label, Modal, Layout, ScrollContainer, Panel, Toast } from '@energy8platform/game-engine/ui';
|
|
148
153
|
import { Tween, Timeline, Easing, SpriteAnimation } from '@energy8platform/game-engine/animation';
|
|
149
154
|
import { DevBridge, FPSOverlay } from '@energy8platform/game-engine/debug';
|
|
155
|
+
import { ReactScene, createPixiRoot, useSDK, useViewport } from '@energy8platform/game-engine/react';
|
|
150
156
|
import { defineGameConfig } from '@energy8platform/game-engine/vite';
|
|
151
157
|
```
|
|
152
158
|
|
|
@@ -1196,102 +1202,160 @@ panel.content.addChild(
|
|
|
1196
1202
|
|
|
1197
1203
|
---
|
|
1198
1204
|
|
|
1199
|
-
##
|
|
1205
|
+
## React Integration
|
|
1200
1206
|
|
|
1201
|
-
|
|
1207
|
+
The engine has a built-in React integration with its own PixiJS reconciler. No need for `@pixi/react` — the engine renders React component trees directly into PixiJS Containers while providing access to all engine sub-systems through hooks.
|
|
1202
1208
|
|
|
1203
1209
|
### Installation
|
|
1204
1210
|
|
|
1205
1211
|
```bash
|
|
1206
|
-
npm install
|
|
1212
|
+
npm install react react-dom react-reconciler
|
|
1207
1213
|
```
|
|
1208
1214
|
|
|
1209
|
-
###
|
|
1215
|
+
### ReactScene
|
|
1216
|
+
|
|
1217
|
+
`ReactScene` is an abstract `Scene` subclass that automatically mounts a React tree into the scene's PixiJS container. Implement the `render()` method to return your JSX:
|
|
1210
1218
|
|
|
1211
1219
|
```tsx
|
|
1212
|
-
|
|
1213
|
-
import {
|
|
1214
|
-
import { GameApplication, Scene } from '@energy8platform/game-engine';
|
|
1220
|
+
// scenes/GameScene.tsx
|
|
1221
|
+
import { ReactScene, extendPixiElements, useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
|
|
1215
1222
|
|
|
1216
|
-
// Register PixiJS components for JSX
|
|
1217
|
-
|
|
1223
|
+
// Register PixiJS components for JSX (call once at startup)
|
|
1224
|
+
extendPixiElements();
|
|
1218
1225
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
<
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1226
|
+
export class GameScene extends ReactScene {
|
|
1227
|
+
render() {
|
|
1228
|
+
return <GameRoot />;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// You can mix imperative code with React
|
|
1232
|
+
async onEnter(data?: unknown) {
|
|
1233
|
+
await super.onEnter(data);
|
|
1234
|
+
// Add imperative elements on top of the React tree
|
|
1235
|
+
// this.container.addChild(someParticleEmitter);
|
|
1236
|
+
}
|
|
1229
1237
|
}
|
|
1230
1238
|
```
|
|
1231
1239
|
|
|
1232
|
-
### Using Engine Components in JSX
|
|
1233
|
-
|
|
1234
|
-
Engine components (`Button`, `Panel`, `Layout`, etc.) are PixiJS `Container` subclasses — use them with `@pixi/react`'s `extend`:
|
|
1235
|
-
|
|
1236
1240
|
```tsx
|
|
1237
|
-
|
|
1238
|
-
import {
|
|
1241
|
+
// components/GameRoot.tsx
|
|
1242
|
+
import { useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
|
|
1239
1243
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1244
|
+
export function GameRoot() {
|
|
1245
|
+
const { width, height, isPortrait } = useViewport();
|
|
1246
|
+
const balance = useBalance();
|
|
1247
|
+
const sdk = useSDK();
|
|
1242
1248
|
|
|
1243
|
-
function GameHUD({ balance, bet }: { balance: number; bet: number }) {
|
|
1244
1249
|
return (
|
|
1245
1250
|
<container>
|
|
1246
|
-
<
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1251
|
+
<text text={`Balance: $${balance.toFixed(2)}`} style={{ fontSize: 32, fill: 0xffffff }} />
|
|
1252
|
+
<container position-y={height - 100}>
|
|
1253
|
+
<text
|
|
1254
|
+
text="SPIN"
|
|
1255
|
+
style={{ fontSize: 48, fill: 0xffd700 }}
|
|
1256
|
+
eventMode="static"
|
|
1257
|
+
cursor="pointer"
|
|
1258
|
+
onClick={async () => {
|
|
1259
|
+
const result = await sdk?.play({ action: 'spin', bet: 1 });
|
|
1260
|
+
// ... animate result
|
|
1261
|
+
sdk?.playAck(result!);
|
|
1262
|
+
}}
|
|
1263
|
+
/>
|
|
1264
|
+
</container>
|
|
1256
1265
|
</container>
|
|
1257
1266
|
);
|
|
1258
1267
|
}
|
|
1259
1268
|
```
|
|
1260
1269
|
|
|
1261
|
-
|
|
1270
|
+
**Lifecycle:** `onEnter` mounts the React tree, `onResize` re-renders with updated viewport context, `onExit` and `onDestroy` unmount.
|
|
1262
1271
|
|
|
1263
|
-
|
|
1264
|
-
import { extend } from '@pixi/react';
|
|
1265
|
-
import { LayoutContainer } from '@pixi/layout/components';
|
|
1272
|
+
### Hooks
|
|
1266
1273
|
|
|
1267
|
-
|
|
1274
|
+
All hooks must be used inside a `ReactScene`:
|
|
1268
1275
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1276
|
+
| Hook | Returns | Description |
|
|
1277
|
+
| --- | --- | --- |
|
|
1278
|
+
| `useEngine()` | `EngineContextValue` | Full engine context (app, sdk, audio, input, viewport, etc.) |
|
|
1279
|
+
| `useSDK()` | `CasinoGameSDK \| null` | SDK instance for `play()`, `playAck()`, etc. |
|
|
1280
|
+
| `useAudio()` | `AudioManager` | Audio manager for sound playback |
|
|
1281
|
+
| `useInput()` | `InputManager` | Input manager for pointer/keyboard |
|
|
1282
|
+
| `useViewport()` | `{ width, height, scale, isPortrait }` | Current viewport dimensions (updates on resize) |
|
|
1283
|
+
| `useBalance()` | `number` | Reactive balance (auto-updates on `balanceUpdate` events) |
|
|
1284
|
+
| `useSession()` | `SessionData \| null` | Current session data |
|
|
1285
|
+
| `useGameConfig<T>()` | `T \| null` | Game configuration from SDK |
|
|
1286
|
+
|
|
1287
|
+
### Element Registration
|
|
1288
|
+
|
|
1289
|
+
Before rendering JSX, register PixiJS classes so the reconciler can create instances:
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
import { extendPixiElements, extendLayoutElements, extend } from '@energy8platform/game-engine/react';
|
|
1293
|
+
|
|
1294
|
+
// Register standard PixiJS elements (Container, Sprite, Text, Graphics, etc.)
|
|
1295
|
+
extendPixiElements();
|
|
1296
|
+
|
|
1297
|
+
// Register @pixi/layout components (if installed)
|
|
1298
|
+
const layout = await import('@pixi/layout/components');
|
|
1299
|
+
extendLayoutElements(layout);
|
|
1283
1300
|
|
|
1284
|
-
|
|
1301
|
+
// Register custom classes
|
|
1302
|
+
import { Button, Panel } from '@energy8platform/game-engine/ui';
|
|
1303
|
+
extend({ Button, Panel });
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
After registration, use elements as lowercase JSX tags: `<container>`, `<sprite>`, `<text>`, `<graphics>`, `<button>`, etc.
|
|
1307
|
+
|
|
1308
|
+
### Using `@pixi/layout` with React
|
|
1309
|
+
|
|
1310
|
+
```tsx
|
|
1311
|
+
import '@pixi/layout'; // side-effect: activates layout mixin
|
|
1312
|
+
|
|
1313
|
+
function FlexColumn() {
|
|
1285
1314
|
return (
|
|
1286
|
-
<
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1315
|
+
<container layout={{
|
|
1316
|
+
width: '100%',
|
|
1317
|
+
height: '100%',
|
|
1318
|
+
flexDirection: 'column',
|
|
1319
|
+
justifyContent: 'space-between',
|
|
1320
|
+
alignItems: 'center',
|
|
1321
|
+
padding: 40,
|
|
1322
|
+
}}>
|
|
1323
|
+
<text text="HEADER" style={{ fontSize: 36, fill: 0xffffff }} />
|
|
1324
|
+
<text text="CENTER" style={{ fontSize: 48, fill: 0xffd700 }} />
|
|
1325
|
+
<text text="FOOTER" style={{ fontSize: 36, fill: 0xffffff }} />
|
|
1326
|
+
</container>
|
|
1290
1327
|
);
|
|
1291
1328
|
}
|
|
1292
1329
|
```
|
|
1293
1330
|
|
|
1294
|
-
|
|
1331
|
+
### Props
|
|
1332
|
+
|
|
1333
|
+
- **Regular props** are set directly on the PixiJS instance: `alpha`, `visible`, `position`, `scale`, `rotation`, etc.
|
|
1334
|
+
- **Nested props** use dash notation: `position-x`, `scale-y`, `anchor-x`
|
|
1335
|
+
- **Event props** map React-style names to PixiJS: `onClick` → `onclick`, `onPointerDown` → `onpointerdown`, etc.
|
|
1336
|
+
- **`draw` prop** (Graphics): pass a function that receives the Graphics instance for imperative drawing.
|
|
1337
|
+
|
|
1338
|
+
### Standalone `createPixiRoot`
|
|
1339
|
+
|
|
1340
|
+
For advanced use cases where you don't need `ReactScene`, render a React tree into any PixiJS Container:
|
|
1341
|
+
|
|
1342
|
+
```typescript
|
|
1343
|
+
import { createPixiRoot, extend } from '@energy8platform/game-engine/react';
|
|
1344
|
+
import { Container, Text } from 'pixi.js';
|
|
1345
|
+
import { createElement } from 'react';
|
|
1346
|
+
|
|
1347
|
+
extend({ Container, Text });
|
|
1348
|
+
|
|
1349
|
+
const container = new Container();
|
|
1350
|
+
const root = createPixiRoot(container);
|
|
1351
|
+
|
|
1352
|
+
root.render(createElement('text', { text: 'Hello', style: { fill: 0xffffff } }));
|
|
1353
|
+
|
|
1354
|
+
// Later:
|
|
1355
|
+
root.unmount();
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
> **Note:** React is entirely optional. The engine works without it. Imperative scenes (`Scene`) and React scenes (`ReactScene`) can coexist in the same game.
|
|
1295
1359
|
|
|
1296
1360
|
---
|
|
1297
1361
|
|
|
@@ -1374,8 +1438,8 @@ export default defineGameConfig({
|
|
|
1374
1438
|
- **PixiJS chunk splitting** — `pixi.js` is extracted into a separate chunk for caching
|
|
1375
1439
|
- **DevBridge injection** — automatically available in dev mode via virtual module
|
|
1376
1440
|
- **Dev server** — port 3000, auto-open browser
|
|
1377
|
-
- **Dependency deduplication** — `resolve.dedupe` ensures a single copy of `pixi.js`, `@pixi/layout`, `@pixi/ui`,
|
|
1378
|
-
- **Dependency optimization** — `pixi.js`, `@pixi/layout`, `@pixi/layout/components`, `@pixi/ui`,
|
|
1441
|
+
- **Dependency deduplication** — `resolve.dedupe` ensures a single copy of `pixi.js`, `@pixi/layout`, `@pixi/ui`, `yoga-layout`, `react`, `react-dom`, and `react-reconciler` across all packages (prevents registration issues when used as a linked dependency)
|
|
1442
|
+
- **Dependency optimization** — `pixi.js`, `@pixi/layout`, `@pixi/layout/components`, `@pixi/ui`, `yoga-layout/load`, `react`, and `react-dom` are pre-bundled for faster dev starts; `yoga-layout` main entry is excluded from pre-bundling because it contains a top-level `await` incompatible with esbuild's default target
|
|
1379
1443
|
|
|
1380
1444
|
### Custom DevBridge Configuration
|
|
1381
1445
|
|
|
@@ -1517,6 +1581,7 @@ class SceneManager extends EventEmitter<{ change: { from: string | null; to: str
|
|
|
1517
1581
|
get isTransitioning(): boolean;
|
|
1518
1582
|
|
|
1519
1583
|
setRoot(root: Container): void;
|
|
1584
|
+
setApp(app: any): void; // @internal — called by GameApplication
|
|
1520
1585
|
register(key: string, ctor: SceneConstructor): this;
|
|
1521
1586
|
async goto(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
|
|
1522
1587
|
async push(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
|
|
@@ -1694,6 +1759,51 @@ class ScrollContainer extends ScrollBox {
|
|
|
1694
1759
|
}
|
|
1695
1760
|
```
|
|
1696
1761
|
|
|
1762
|
+
### ReactScene
|
|
1763
|
+
|
|
1764
|
+
> Requires `react`, `react-dom`, `react-reconciler` as peer dependencies.
|
|
1765
|
+
|
|
1766
|
+
```typescript
|
|
1767
|
+
abstract class ReactScene extends Scene {
|
|
1768
|
+
abstract render(): ReactElement;
|
|
1769
|
+
protected getApp(): GameApplication;
|
|
1770
|
+
|
|
1771
|
+
// Lifecycle (auto-managed):
|
|
1772
|
+
override async onEnter(data?: unknown): Promise<void>; // mounts React tree
|
|
1773
|
+
override async onExit(): Promise<void>; // unmounts React tree
|
|
1774
|
+
override onResize(width: number, height: number): void; // re-renders with updated context
|
|
1775
|
+
override onDestroy(): void; // cleanup
|
|
1776
|
+
}
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
### createPixiRoot
|
|
1780
|
+
|
|
1781
|
+
```typescript
|
|
1782
|
+
interface PixiRoot {
|
|
1783
|
+
render(element: ReactElement): void;
|
|
1784
|
+
unmount(): void;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function createPixiRoot(container: Container): PixiRoot;
|
|
1788
|
+
```
|
|
1789
|
+
|
|
1790
|
+
### React Hooks
|
|
1791
|
+
|
|
1792
|
+
```typescript
|
|
1793
|
+
function useEngine(): EngineContextValue;
|
|
1794
|
+
function useSDK(): CasinoGameSDK | null;
|
|
1795
|
+
function useAudio(): AudioManager;
|
|
1796
|
+
function useInput(): InputManager;
|
|
1797
|
+
function useViewport(): { width: number; height: number; scale: number; isPortrait: boolean };
|
|
1798
|
+
function useBalance(): number;
|
|
1799
|
+
function useSession(): SessionData | null;
|
|
1800
|
+
function useGameConfig<T = GameConfigData>(): T | null;
|
|
1801
|
+
|
|
1802
|
+
function extend(components: Record<string, any>): void;
|
|
1803
|
+
function extendPixiElements(): void;
|
|
1804
|
+
function extendLayoutElements(layoutModule: Record<string, any>): void;
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1697
1807
|
### Button
|
|
1698
1808
|
|
|
1699
1809
|
> Extends `@pixi/ui` FancyButton — per-state views, press animation, and text.
|
package/dist/core.cjs.js
CHANGED
|
@@ -292,14 +292,17 @@ class Tween {
|
|
|
292
292
|
* ```
|
|
293
293
|
*/
|
|
294
294
|
class SceneManager extends EventEmitter {
|
|
295
|
+
static MAX_TRANSITION_DEPTH = 10;
|
|
295
296
|
/** Root container that scenes are added to */
|
|
296
297
|
root;
|
|
297
298
|
registry = new Map();
|
|
298
299
|
stack = [];
|
|
299
|
-
|
|
300
|
+
_transitionDepth = 0;
|
|
300
301
|
/** Current viewport dimensions — set by ViewportManager */
|
|
301
302
|
_width = 0;
|
|
302
303
|
_height = 0;
|
|
304
|
+
/** @internal GameApplication reference — passed to scenes */
|
|
305
|
+
_app;
|
|
303
306
|
constructor(root) {
|
|
304
307
|
super();
|
|
305
308
|
if (root)
|
|
@@ -309,6 +312,10 @@ class SceneManager extends EventEmitter {
|
|
|
309
312
|
setRoot(root) {
|
|
310
313
|
this.root = root;
|
|
311
314
|
}
|
|
315
|
+
/** @internal Set the app reference (called by GameApplication) */
|
|
316
|
+
setApp(app) {
|
|
317
|
+
this._app = app;
|
|
318
|
+
}
|
|
312
319
|
/** Register a scene class by key */
|
|
313
320
|
register(key, ctor) {
|
|
314
321
|
this.registry.set(key, ctor);
|
|
@@ -324,12 +331,15 @@ class SceneManager extends EventEmitter {
|
|
|
324
331
|
}
|
|
325
332
|
/** Whether a scene transition is in progress */
|
|
326
333
|
get isTransitioning() {
|
|
327
|
-
return this.
|
|
334
|
+
return this._transitionDepth > 0;
|
|
328
335
|
}
|
|
329
336
|
/**
|
|
330
337
|
* Navigate to a scene, replacing the entire stack.
|
|
331
338
|
*/
|
|
332
339
|
async goto(key, data, transition) {
|
|
340
|
+
if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
|
|
341
|
+
throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
|
|
342
|
+
}
|
|
333
343
|
const prevKey = this.currentKey;
|
|
334
344
|
// Exit all current scenes
|
|
335
345
|
while (this.stack.length > 0) {
|
|
@@ -344,6 +354,9 @@ class SceneManager extends EventEmitter {
|
|
|
344
354
|
* Useful for overlays, modals, pause screens.
|
|
345
355
|
*/
|
|
346
356
|
async push(key, data, transition) {
|
|
357
|
+
if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
|
|
358
|
+
throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
|
|
359
|
+
}
|
|
347
360
|
const prevKey = this.currentKey;
|
|
348
361
|
await this.pushInternal(key, data, transition);
|
|
349
362
|
this.emit('change', { from: prevKey, to: key });
|
|
@@ -356,6 +369,9 @@ class SceneManager extends EventEmitter {
|
|
|
356
369
|
console.warn('[SceneManager] Cannot pop the last scene');
|
|
357
370
|
return;
|
|
358
371
|
}
|
|
372
|
+
if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
|
|
373
|
+
throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
|
|
374
|
+
}
|
|
359
375
|
const prevKey = this.currentKey;
|
|
360
376
|
await this.popInternal(true, transition);
|
|
361
377
|
this.emit('change', { from: prevKey, to: this.currentKey });
|
|
@@ -364,6 +380,9 @@ class SceneManager extends EventEmitter {
|
|
|
364
380
|
* Replace the top scene with a new one.
|
|
365
381
|
*/
|
|
366
382
|
async replace(key, data, transition) {
|
|
383
|
+
if (this._transitionDepth >= SceneManager.MAX_TRANSITION_DEPTH) {
|
|
384
|
+
throw new Error('[SceneManager] Max transition depth exceeded — possible infinite loop');
|
|
385
|
+
}
|
|
367
386
|
const prevKey = this.currentKey;
|
|
368
387
|
await this.popInternal(false);
|
|
369
388
|
await this.pushInternal(key, data, transition);
|
|
@@ -405,10 +424,14 @@ class SceneManager extends EventEmitter {
|
|
|
405
424
|
if (!Ctor) {
|
|
406
425
|
throw new Error(`[SceneManager] Scene "${key}" is not registered`);
|
|
407
426
|
}
|
|
408
|
-
|
|
427
|
+
const scene = new Ctor();
|
|
428
|
+
if (this._app) {
|
|
429
|
+
scene.__engineApp = this._app;
|
|
430
|
+
}
|
|
431
|
+
return scene;
|
|
409
432
|
}
|
|
410
433
|
async pushInternal(key, data, transition) {
|
|
411
|
-
this.
|
|
434
|
+
this._transitionDepth++;
|
|
412
435
|
const scene = this.createScene(key);
|
|
413
436
|
this.root.addChild(scene.container);
|
|
414
437
|
// Set initial size
|
|
@@ -420,20 +443,20 @@ class SceneManager extends EventEmitter {
|
|
|
420
443
|
// Push to stack BEFORE onEnter so currentKey is correct during initialization
|
|
421
444
|
this.stack.push({ scene, key });
|
|
422
445
|
await scene.onEnter?.(data);
|
|
423
|
-
this.
|
|
446
|
+
this._transitionDepth--;
|
|
424
447
|
}
|
|
425
448
|
async popInternal(showTransition, transition) {
|
|
426
449
|
const entry = this.stack.pop();
|
|
427
450
|
if (!entry)
|
|
428
451
|
return;
|
|
429
|
-
this.
|
|
452
|
+
this._transitionDepth++;
|
|
430
453
|
await entry.scene.onExit?.();
|
|
431
454
|
if (showTransition) {
|
|
432
455
|
await this.transitionOut(entry.scene.container, transition);
|
|
433
456
|
}
|
|
434
457
|
entry.scene.onDestroy?.();
|
|
435
458
|
entry.scene.container.destroy({ children: true });
|
|
436
|
-
this.
|
|
459
|
+
this._transitionDepth--;
|
|
437
460
|
}
|
|
438
461
|
async transitionIn(container, config) {
|
|
439
462
|
const type = config?.type ?? TransitionType.NONE;
|
|
@@ -2166,6 +2189,7 @@ class GameApplication extends EventEmitter {
|
|
|
2166
2189
|
});
|
|
2167
2190
|
// Wire SceneManager to the PixiJS stage
|
|
2168
2191
|
this.scenes.setRoot(this.app.stage);
|
|
2192
|
+
this.scenes.setApp(this);
|
|
2169
2193
|
// Wire viewport resize → scene manager + input manager
|
|
2170
2194
|
this.viewport.on('resize', ({ width, height, scale }) => {
|
|
2171
2195
|
this.scenes.resize(width, height);
|