@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 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
- - [Using with React (`@pixi/react`)](#using-with-react-pixireact)
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 @pixi/react react react-dom
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
- | `@pixi/react` | `^8.0.0` | Optional — for React integration (see [Using with React](#using-with-react-pixireact)) |
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
- ## Using with React (`@pixi/react`)
1205
+ ## React Integration
1200
1206
 
1201
- For React-based projects, [`@pixi/react`](https://github.com/pixijs/pixi-react) provides declarative JSX components for PixiJS. The engine is framework-agnostic — `@pixi/react` is **not** a dependency, but works seamlessly alongside it.
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 @pixi/react react react-dom
1212
+ npm install react react-dom react-reconciler
1207
1213
  ```
1208
1214
 
1209
- ### Wrapping the Engine in React
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
- import { Application, extend } from '@pixi/react';
1213
- import { Container, Graphics, Text } from 'pixi.js';
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
- extend({ Container, Graphics, Text });
1223
+ // Register PixiJS components for JSX (call once at startup)
1224
+ extendPixiElements();
1218
1225
 
1219
- function GameWrapper() {
1220
- return (
1221
- <Application
1222
- width={1920}
1223
- height={1080}
1224
- background={0x0f0f23}
1225
- >
1226
- <GameContent />
1227
- </Application>
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
- import { extend, useTick } from '@pixi/react';
1238
- import { Button, Label, Panel, ProgressBar } from '@energy8platform/game-engine/ui';
1241
+ // components/GameRoot.tsx
1242
+ import { useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
1239
1243
 
1240
- // Register engine components for JSX
1241
- extend({ Button, Label, Panel, ProgressBar });
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
- <panel
1247
- width={400}
1248
- height={80}
1249
- backgroundColor={0x1a1a2e}
1250
- borderRadius={12}
1251
- padding={16}
1252
- >
1253
- <label text={`Balance: $${balance.toFixed(2)}`} />
1254
- <label text={`Bet: $${bet.toFixed(2)}`} />
1255
- </panel>
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
- ### Combining Flexbox with React
1270
+ **Lifecycle:** `onEnter` mounts the React tree, `onResize` re-renders with updated viewport context, `onExit` and `onDestroy` unmount.
1262
1271
 
1263
- ```tsx
1264
- import { extend } from '@pixi/react';
1265
- import { LayoutContainer } from '@pixi/layout/components';
1272
+ ### Hooks
1266
1273
 
1267
- extend({ LayoutContainer });
1274
+ All hooks must be used inside a `ReactScene`:
1268
1275
 
1269
- function FlexRow({ children }: { children: React.ReactNode }) {
1270
- return (
1271
- <layoutContainer
1272
- layout={{
1273
- flexDirection: 'row',
1274
- gap: 16,
1275
- alignItems: 'center',
1276
- justifyContent: 'center',
1277
- }}
1278
- >
1279
- {children}
1280
- </layoutContainer>
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
- function Toolbar() {
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
- <FlexRow>
1287
- <button text="SPIN" width={180} height={60} />
1288
- <label text="BET: $1.00" />
1289
- </FlexRow>
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
- > **Note:** `@pixi/react` is entirely optional. The engine works without React. Choose whichever approach fits your team and project.
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`, and `yoga-layout` across all packages (prevents registration issues when used as a linked dependency)
1378
- - **Dependency optimization** — `pixi.js`, `@pixi/layout`, `@pixi/layout/components`, `@pixi/ui`, and `yoga-layout/load` 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
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
- _transitioning = false;
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._transitioning;
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
- return new Ctor();
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._transitioning = true;
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._transitioning = false;
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._transitioning = true;
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._transitioning = false;
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);