@energy8platform/game-engine 0.9.1 → 0.10.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
@@ -1,6 +1,6 @@
1
1
  # @energy8platform/game-engine
2
2
 
3
- A universal casino game engine built on [PixiJS v8](https://pixijs.com/) and [@energy8platform/game-sdk](https://github.com/energy8platform/game-sdk). Provides a batteries-included framework for developing slot machines, card games, and other iGaming titles with responsive scaling, animated loading screens, scene management, audio, state machines, tweens, and a rich UI component library.
3
+ A casino game engine built on [PixiJS v8](https://pixijs.com/) and [@energy8platform/game-sdk](https://github.com/energy8platform/game-sdk). Provides scene management, responsive scaling, audio, state machines, tweens, UI components, Lua scripting, and React integration for developing slot machines, card games, and other iGaming titles.
4
4
 
5
5
  ---
6
6
 
@@ -17,31 +17,13 @@ A universal casino game engine built on [PixiJS v8](https://pixijs.com/) and [@e
17
17
  - [Viewport & Scaling](#viewport--scaling)
18
18
  - [State Machine](#state-machine)
19
19
  - [Animation](#animation)
20
- - [Tween](#tween)
21
- - [Timeline](#timeline)
22
- - [SpriteAnimation](#spriteanimation)
23
- - [Spine Animations](#spine-animations)
24
20
  - [UI Components](#ui-components)
25
- - [Layout](#layout)
26
- - [ScrollContainer](#scrollcontainer)
27
- - [Button](#button)
28
- - [Label](#label)
29
- - [BalanceDisplay](#balancedisplay)
30
- - [WinDisplay](#windisplay)
31
- - [ProgressBar](#progressbar)
32
- - [Panel](#panel)
33
- - [Modal](#modal)
34
- - [Toast](#toast)
35
21
  - [Input](#input)
36
22
  - [Vite Configuration](#vite-configuration)
23
+ - [Lua Engine](#lua-engine)
37
24
  - [DevBridge](#devbridge)
38
- - [Debug](#debug)
39
- - [Flexbox-First Layout](#flexbox-first-layout)
40
25
  - [React Integration](#react-integration)
41
- - [ReactScene](#reactscene)
42
- - [Hooks](#hooks)
43
- - [Standalone createPixiRoot](#standalone-createpixiroot)
44
- - [API Reference](#api-reference)
26
+ - [Debug](#debug)
45
27
  - [License](#license)
46
28
 
47
29
  ---
@@ -49,25 +31,17 @@ A universal casino game engine built on [PixiJS v8](https://pixijs.com/) and [@e
49
31
  ## Quick Start
50
32
 
51
33
  ```bash
52
- # Create a new project
53
- mkdir my-game && cd my-game
54
- npm init -y
55
-
56
34
  # Install dependencies
57
35
  npm install pixi.js @energy8platform/game-sdk @energy8platform/game-engine
58
36
 
59
- # Install UI layout dependencies (optional — needed for Layout, Panel, ScrollContainer)
60
- npm install @pixi/ui @pixi/layout yoga-layout
61
-
62
- # (Optional) React integration
63
- npm install react react-dom react-reconciler
64
-
65
- # (Optional) install spine and audio support
66
- npm install @pixi/sound @esotericsoftware/spine-pixi-v8
37
+ # Optional peer dependencies
38
+ npm install @pixi/ui @pixi/layout yoga-layout # UI components (Layout, Panel, ScrollContainer)
39
+ npm install @pixi/sound # Audio
40
+ npm install @esotericsoftware/spine-pixi-v8 # Spine animations
41
+ npm install react react-dom react-reconciler # React integration
42
+ npm install fengari # Lua engine
67
43
  ```
68
44
 
69
- Create the entry point:
70
-
71
45
  ```typescript
72
46
  // src/main.ts
73
47
  import { GameApplication, ScaleMode } from '@energy8platform/game-engine';
@@ -81,40 +55,23 @@ async function bootstrap() {
81
55
  scaleMode: ScaleMode.FIT,
82
56
  loading: {
83
57
  backgroundColor: 0x0a0a1a,
84
- backgroundGradient:
85
- 'linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 50%, #0a0a1a 100%)',
86
- showPercentage: true,
87
58
  tapToStart: true,
88
- tapToStartText: 'TAP TO PLAY',
89
59
  minDisplayTime: 2000,
90
60
  },
91
61
  manifest: {
92
62
  bundles: [
93
63
  { name: 'preload', assets: [] },
94
- {
95
- name: 'game',
96
- assets: [
97
- { alias: 'background', src: 'background.png' },
98
- { alias: 'symbols', src: 'symbols.json' },
99
- ],
100
- },
64
+ { name: 'game', assets: [
65
+ { alias: 'background', src: 'background.png' },
66
+ { alias: 'symbols', src: 'symbols.json' },
67
+ ]},
101
68
  ],
102
69
  },
103
- audio: {
104
- music: 0.5,
105
- sfx: 1.0,
106
- persist: true,
107
- },
70
+ audio: { music: 0.5, sfx: 1.0, persist: true },
108
71
  debug: true,
109
72
  });
110
73
 
111
74
  game.scenes.register('game', GameScene);
112
-
113
- game.on('initialized', () => console.log('Engine initialized'));
114
- game.on('loaded', () => console.log('Assets loaded'));
115
- game.on('started', () => console.log('Game started'));
116
- game.on('error', (err) => console.error('Error:', err));
117
-
118
75
  await game.start('game');
119
76
  }
120
77
 
@@ -131,21 +88,19 @@ bootstrap();
131
88
  | --- | --- | --- |
132
89
  | `pixi.js` | `^8.16.0` | Yes |
133
90
  | `@energy8platform/game-sdk` | `^2.7.0` | Yes |
134
- | `@pixi/ui` | `^2.3.0` | Optional — for Button, ScrollContainer, ProgressBar |
135
- | `@pixi/layout` | `^3.2.0` | Optional — for Layout, Panel (Yoga flexbox) |
91
+ | `@pixi/ui` | `^2.3.0` | Optional — Button, ScrollContainer, ProgressBar |
92
+ | `@pixi/layout` | `^3.2.0` | Optional — Layout, Panel (Yoga flexbox) |
136
93
  | `yoga-layout` | `^3.0.0` | Optional — peer dep of `@pixi/layout` |
137
- | `@pixi/sound` | `^6.0.0` | Optional — for audio |
138
- | `@esotericsoftware/spine-pixi-v8` | `~4.2.0` | Optional — for Spine animations |
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) |
94
+ | `@pixi/sound` | `^6.0.0` | Optional — audio |
95
+ | `@esotericsoftware/spine-pixi-v8` | `~4.2.0` | Optional — Spine animations |
96
+ | `react`, `react-dom` | `>=18.0.0` | Optional — ReactScene |
97
+ | `react-reconciler` | `>=0.29.0` | Optional — ReactScene (custom PixiJS reconciler) |
98
+ | `fengari` | `^0.1.4` | Optional — Lua engine |
142
99
 
143
100
  ### Sub-path Exports
144
101
 
145
- The package exposes granular entry points for tree-shaking:
146
-
147
102
  ```typescript
148
- import { GameApplication } from '@energy8platform/game-engine'; // full bundle
103
+ import { GameApplication } from '@energy8platform/game-engine'; // full bundle
149
104
  import { Scene, SceneManager } from '@energy8platform/game-engine/core';
150
105
  import { AssetManager } from '@energy8platform/game-engine/assets';
151
106
  import { AudioManager } from '@energy8platform/game-engine/audio';
@@ -154,6 +109,7 @@ import { Tween, Timeline, Easing, SpriteAnimation } from '@energy8platform/game-
154
109
  import { DevBridge, FPSOverlay } from '@energy8platform/game-engine/debug';
155
110
  import { ReactScene, createPixiRoot, useSDK, useViewport } from '@energy8platform/game-engine/react';
156
111
  import { defineGameConfig } from '@energy8platform/game-engine/vite';
112
+ import { LuaEngine, ActionRouter } from '@energy8platform/game-engine/lua';
157
113
  ```
158
114
 
159
115
  ---
@@ -176,13 +132,13 @@ import { defineGameConfig } from '@energy8platform/game-engine/vite';
176
132
 
177
133
  ### Boot Sequence
178
134
 
179
- 1. **CSS Preloader** — an instant HTML/CSS overlay shown while PixiJS initializes (inline SVG logo with a shimmer animation and "Loading..." text).
180
- 2. **PixiJS initialization** — creates `Application`, initializes `ResizeObserver`.
181
- 3. **SDK handshake** — connects to the casino host (or DevBridge in dev mode via shared `MemoryChannel`).
182
- 4. **Canvas Loading Screen** — `LoadingScene` displays the SVG logo with an animated progress bar, `preload` bundle is loaded first.
183
- 5. **Asset loading** — remaining bundles are loaded; the progress bar fills in real time.
184
- 6. **Tap-to-start** — optional screen shown after loading (required on mobile for audio unlock).
185
- 7. **First scene** — transitions to the registered first scene.
135
+ 1. **CSS Preloader** — HTML/CSS overlay while PixiJS initializes
136
+ 2. **PixiJS initialization** — creates `Application`, initializes `ResizeObserver`
137
+ 3. **SDK handshake** — connects to casino host (or DevBridge in dev mode)
138
+ 4. **Canvas Loading Screen** — progress bar, `preload` bundle loaded first
139
+ 5. **Asset loading** — remaining bundles loaded with combined progress
140
+ 6. **Tap-to-start** — optional (required on mobile for audio unlock)
141
+ 7. **First scene** — transitions to the registered first scene
186
142
 
187
143
  ---
188
144
 
@@ -195,73 +151,22 @@ import { defineGameConfig } from '@energy8platform/game-engine/vite';
195
151
  | `container` | `HTMLElement \| string` | `document.body` | Container element or CSS selector |
196
152
  | `designWidth` | `number` | `1920` | Reference design width |
197
153
  | `designHeight` | `number` | `1080` | Reference design height |
198
- | `scaleMode` | `ScaleMode` | `FIT` | Scaling strategy |
199
- | `orientation` | `Orientation` | `ANY` | Preferred orientation |
200
- | `loading` | `LoadingScreenConfig` | — | Loading screen options |
154
+ | `scaleMode` | `ScaleMode` | `FIT` | `FIT` (letterbox), `FILL` (crop), `STRETCH` |
155
+ | `orientation` | `Orientation` | `ANY` | `LANDSCAPE`, `PORTRAIT`, `ANY` |
156
+ | `loading` | `LoadingScreenConfig` | — | Loading screen options (see types) |
201
157
  | `manifest` | `AssetManifest` | — | Asset manifest |
202
- | `audio` | `AudioConfig` | — | Audio configuration |
158
+ | `audio` | `AudioConfig` | — | `{ music, sfx, ui, ambient }` volumes (0-1), `persist` flag |
203
159
  | `sdk` | `object \| false` | — | SDK options; `false` to disable |
204
- | `sdk.devMode` | `boolean` | `false` | Use in-memory channel instead of `postMessage` (no iframe needed) |
205
- | `sdk.parentOrigin` | `string` | — | Expected parent origin for `postMessage` validation |
206
- | `sdk.timeout` | `number` | — | SDK handshake timeout in ms |
207
- | `sdk.debug` | `boolean` | — | Enable SDK debug logging |
208
160
  | `pixi` | `Partial<ApplicationOptions>` | — | PixiJS pass-through options |
209
161
  | `debug` | `boolean` | `false` | Enable FPS overlay |
210
162
 
211
- ### `LoadingScreenConfig`
212
-
213
- | Property | Type | Default | Description |
214
- | --- | --- | --- | --- |
215
- | `backgroundColor` | `number \| string` | `0x0a0a1a` | Background color |
216
- | `backgroundGradient` | `string` | — | CSS gradient for the preloader background |
217
- | `logoAsset` | `string` | — | Logo texture alias from `preload` bundle |
218
- | `logoScale` | `number` | `1` | Logo scale factor |
219
- | `showPercentage` | `boolean` | `true` | Show loading percentage text |
220
- | `progressTextFormatter` | `(progress: number) => string` | — | Custom progress text formatter |
221
- | `tapToStart` | `boolean` | `true` | Show "Tap to start" overlay |
222
- | `tapToStartText` | `string` | `'TAP TO START'` | Tap-to-start label |
223
- | `minDisplayTime` | `number` | `1500` | Minimum display time (ms) |
224
- | `cssPreloaderHTML` | `string` | — | Custom HTML for the CSS preloader |
225
-
226
- ### `AudioConfig`
227
-
228
- | Property | Type | Default | Description |
229
- | --- | --- | --- | --- |
230
- | `music` | `number` | `0.7` | Default music volume (0–1) |
231
- | `sfx` | `number` | `1` | Default SFX volume |
232
- | `ui` | `number` | `0.8` | Default UI sounds volume |
233
- | `ambient` | `number` | `0.5` | Default ambient volume |
234
- | `persist` | `boolean` | `true` | Save mute state to localStorage |
235
- | `storageKey` | `string` | `'ge_audio'` | localStorage key prefix |
236
-
237
- ### Enums
238
-
239
- ```typescript
240
- enum ScaleMode {
241
- FIT = 'FIT', // Letterbox/pillarbox — preserves aspect ratio
242
- FILL = 'FILL', // Fill container, crop edges
243
- STRETCH = 'STRETCH' // Stretch to fill (distorts)
244
- }
245
-
246
- enum Orientation {
247
- LANDSCAPE = 'landscape',
248
- PORTRAIT = 'portrait',
249
- ANY = 'any'
250
- }
251
-
252
- enum TransitionType {
253
- NONE = 'none',
254
- FADE = 'fade',
255
- SLIDE_LEFT = 'slide-left',
256
- SLIDE_RIGHT = 'slide-right'
257
- }
258
- ```
163
+ > Full config types including `LoadingScreenConfig`, `AudioConfig`, `TransitionType` are documented in `src/types.ts`.
259
164
 
260
165
  ---
261
166
 
262
167
  ## Scenes
263
168
 
264
- All game screens are scenes. Extend the base `Scene` class and override lifecycle hooks:
169
+ All game screens are scenes. Extend the base `Scene` class:
265
170
 
266
171
  ```typescript
267
172
  import { Scene } from '@energy8platform/game-engine';
@@ -269,67 +174,34 @@ import { Sprite, Assets } from 'pixi.js';
269
174
 
270
175
  export class GameScene extends Scene {
271
176
  async onEnter(data?: unknown) {
272
- const bg = new Sprite(Assets.get('background'));
273
- this.container.addChild(bg);
274
- }
275
-
276
- onUpdate(dt: number) {
277
- // Called every frame (dt = delta time from PixiJS ticker)
278
- }
279
-
280
- onResize(width: number, height: number) {
281
- // Called on viewport resize — use for responsive layout
282
- }
283
-
284
- async onExit() {
285
- // Cleanup before leaving the scene
177
+ this.container.addChild(new Sprite(Assets.get('background')));
286
178
  }
287
179
 
288
- onDestroy() {
289
- // Final cleanup when the scene is removed from the stack
290
- }
180
+ onUpdate(dt: number) { /* called every frame */ }
181
+ onResize(width: number, height: number) { /* responsive layout */ }
182
+ async onExit() { /* cleanup before leaving */ }
183
+ onDestroy() { /* final cleanup */ }
291
184
  }
292
185
  ```
293
186
 
294
- > **Tip:** After processing a play result, call `sdk.playAck(result)` to signal the host that animations are finished and the player can interact again.
295
-
296
187
  ### Scene Navigation
297
188
 
298
189
  ```typescript
299
190
  const scenes = game.scenes;
300
191
 
301
- // Register scenes
302
192
  scenes.register('menu', MenuScene);
303
193
  scenes.register('game', GameScene);
304
194
  scenes.register('bonus', BonusScene);
305
195
 
306
- // Navigate (replaces entire stack)
307
- await scenes.goto('game');
308
-
309
- // Push overlay/modal (previous scene stays rendered)
310
- await scenes.push('bonus', { multiplier: 3 });
311
-
312
- // Pop back
313
- await scenes.pop();
314
-
315
- // Replace top scene
316
- await scenes.replace('game');
317
- ```
318
-
319
- ### Transitions
320
-
321
- ```typescript
322
- import { TransitionType } from '@energy8platform/game-engine';
196
+ await scenes.goto('game'); // replaces entire stack
197
+ await scenes.push('bonus', { multiplier: 3 }); // overlay (previous stays)
198
+ await scenes.pop(); // pop back
199
+ await scenes.replace('game'); // replace top scene
323
200
 
201
+ // With transitions
324
202
  await scenes.goto('game', undefined, {
325
203
  type: TransitionType.FADE,
326
- duration: 500, // ms
327
- });
328
-
329
- await scenes.push('bonus', { data: 42 }, {
330
- type: TransitionType.SLIDE_LEFT,
331
- duration: 300,
332
- easing: Easing.easeOutCubic,
204
+ duration: 500,
333
205
  });
334
206
  ```
335
207
 
@@ -337,136 +209,72 @@ await scenes.push('bonus', { data: 42 }, {
337
209
 
338
210
  ## Lifecycle
339
211
 
340
- `GameApplication` emits the following events:
212
+ `GameApplication` events:
341
213
 
342
214
  | Event | Payload | When |
343
215
  | --- | --- | --- |
344
- | `initialized` | `void` | Engine initialized, PixiJS and SDK are ready |
216
+ | `initialized` | `void` | Engine initialized, PixiJS and SDK ready |
345
217
  | `loaded` | `void` | All asset bundles loaded |
346
218
  | `started` | `void` | First scene entered, game loop running |
347
219
  | `resize` | `{ width, height }` | Viewport resized |
348
220
  | `orientationChange` | `Orientation` | Device orientation changed |
349
221
  | `sceneChange` | `{ from, to }` | Scene transition completed |
350
- | `balanceUpdate` | `{ balance }` | Player balance changed (forwarded from SDK) |
222
+ | `balanceUpdate` | `{ balance }` | Player balance changed (from SDK) |
351
223
  | `error` | `Error` | An error occurred |
352
224
  | `destroyed` | `void` | Engine destroyed |
353
225
 
354
- ```typescript
355
- game.on('resize', ({ width, height }) => {
356
- console.log(`New size: ${width}x${height}`);
357
- });
358
-
359
- game.once('started', () => {
360
- // Runs once after the first scene starts
361
- });
362
-
363
- game.on('balanceUpdate', ({ balance }) => {
364
- // SDK balance changed (after play action or host-pushed update)
365
- console.log(`New balance: ${balance}`);
366
- });
367
- ```
368
-
369
226
  ---
370
227
 
371
228
  ## Assets
372
229
 
373
- ### Asset Manifest
374
-
375
- Assets are organized in named **bundles**:
230
+ Assets are organized in named bundles. The `preload` bundle loads first, the rest load together with a combined progress bar.
376
231
 
377
232
  ```typescript
378
- const manifest: AssetManifest = {
233
+ const manifest = {
379
234
  bundles: [
380
- {
381
- name: 'preload',
382
- assets: [
383
- { alias: 'logo', src: 'logo.png' },
384
- ],
385
- },
386
- {
387
- name: 'game',
388
- assets: [
389
- { alias: 'background', src: 'bg.webp' },
390
- { alias: 'symbols', src: 'symbols.json' },
391
- { alias: 'win-sound', src: 'sounds/win.mp3' },
392
- ],
393
- },
394
- {
395
- name: 'bonus',
396
- assets: [
397
- { alias: 'bonus-bg', src: 'bonus/bg.webp' },
398
- ],
399
- },
235
+ { name: 'preload', assets: [{ alias: 'logo', src: 'logo.png' }] },
236
+ { name: 'game', assets: [
237
+ { alias: 'background', src: 'bg.webp' },
238
+ { alias: 'symbols', src: 'symbols.json' },
239
+ ]},
400
240
  ],
401
241
  };
402
- ```
403
-
404
- The `preload` bundle is loaded first (before the progress bar fills). All other bundles load together with a combined progress indicator.
405
242
 
406
- ### AssetManager API
407
-
408
- ```typescript
243
+ // Runtime API
409
244
  const assets = game.assets;
410
-
411
- // Load on demand
412
- await assets.loadBundle('bonus', (progress) => {
413
- console.log(`${Math.round(progress * 100)}%`);
414
- });
415
-
416
- // Synchronous cache access
245
+ await assets.loadBundle('bonus', (progress) => console.log(`${Math.round(progress * 100)}%`));
417
246
  const texture = assets.get<Texture>('background');
418
-
419
- // Background preloading (low priority)
420
- await assets.backgroundLoad('bonus');
421
-
422
- // Unload to free memory
423
- await assets.unloadBundle('bonus');
424
-
425
- // Check state
426
- assets.isBundleLoaded('game'); // true
427
- assets.getBundleNames(); // ['preload', 'game', 'bonus']
247
+ await assets.backgroundLoad('bonus'); // low-priority preload
248
+ await assets.unloadBundle('bonus'); // free memory
428
249
  ```
429
250
 
430
251
  ---
431
252
 
432
253
  ## Audio
433
254
 
434
- `AudioManager` wraps `@pixi/sound` with category-based volume management. If `@pixi/sound` is not installed, all methods work silently as no-ops.
435
-
436
- ### Audio Categories
437
-
438
- Four categories with independent volume and mute controls: `music`, `sfx`, `ui`, `ambient`.
255
+ `AudioManager` wraps `@pixi/sound` with category-based volume. All methods are no-ops if `@pixi/sound` is not installed.
439
256
 
440
257
  ```typescript
441
258
  const audio = game.audio;
442
259
 
443
- // Play a sound effect
444
260
  audio.play('click', 'ui');
445
261
  audio.play('coin-drop', 'sfx', { volume: 0.8 });
446
-
447
- // Music with crossfade (smooth volume transition between tracks)
448
- audio.playMusic('main-theme', 1000); // 1s crossfade
262
+ audio.playMusic('main-theme', 1000); // 1s crossfade
449
263
  audio.stopMusic();
450
264
 
451
- // Volume control
452
265
  audio.setVolume('music', 0.3);
453
266
  audio.muteCategory('sfx');
454
- audio.unmuteCategory('sfx');
455
- audio.toggleCategory('sfx'); // returns new state
456
-
457
- // Global mute
458
267
  audio.muteAll();
459
- audio.unmuteAll();
460
- audio.toggleMute(); // returns new state
268
+ audio.toggleMute();
461
269
 
462
- // Music ducking (e.g., during a big win animation)
463
- audio.duckMusic(0.2); // reduce to 20%
464
- audio.unduckMusic(); // restore
270
+ // Ducking during big win animations
271
+ audio.duckMusic(0.2);
272
+ audio.unduckMusic();
465
273
  ```
466
274
 
467
- ### Mobile Audio Unlock
275
+ **Categories:** `music`, `sfx`, `ui`, `ambient` — each with independent volume and mute.
468
276
 
469
- On iOS and many mobile browsers, audio cannot play until the first user interaction. The engine handles this automatically when `tapToStart: true` is set — the tap event serves as the audio context unlock.
277
+ > Mobile audio unlock is handled automatically when `tapToStart: true`.
470
278
 
471
279
  ---
472
280
 
@@ -474,96 +282,47 @@ On iOS and many mobile browsers, audio cannot play until the first user interact
474
282
 
475
283
  `ViewportManager` handles responsive scaling using `ResizeObserver` with debouncing.
476
284
 
477
- ### Scale Modes
478
-
479
285
  | Mode | Behavior |
480
286
  | --- | --- |
481
- | `FIT` | Fits the entire design area inside the container. Adds letterbox (horizontal bars) or pillarbox (vertical bars) as needed. **Industry standard for iGaming.** |
482
- | `FILL` | Fills the entire container, cropping edges. No bars, but some content may be hidden. |
483
- | `STRETCH` | Stretches to fill the container. Distorts aspect ratio. Not recommended. |
287
+ | `FIT` | Letterbox/pillarbox preserves aspect ratio. **Industry standard for iGaming.** |
288
+ | `FILL` | Fills container, crops edges |
289
+ | `STRETCH` | Stretches to fill (distorts). Not recommended. |
484
290
 
485
291
  ```typescript
486
292
  const vp = game.viewport;
487
-
488
- // Current dimensions
489
- console.log(vp.width, vp.height, vp.scale);
490
- console.log(vp.orientation); // 'landscape' | 'portrait'
491
-
492
- // Reference design size
493
- console.log(vp.designWidth, vp.designHeight);
494
-
495
- // Force re-calculation
496
- vp.refresh();
497
-
498
- // Listen for changes
499
- game.on('resize', ({ width, height }) => {
500
- // Respond to viewport changes
501
- });
293
+ console.log(vp.width, vp.height, vp.scale, vp.orientation);
294
+ vp.refresh(); // force re-calculation
502
295
  ```
503
296
 
504
297
  ---
505
298
 
506
299
  ## State Machine
507
300
 
508
- `StateMachine` is a generic finite state machine with typed context, async hooks, guards, and per-frame updates.
301
+ Generic typed FSM with transition guards, async hooks, and per-frame updates.
509
302
 
510
303
  ```typescript
511
304
  import { StateMachine } from '@energy8platform/game-engine';
512
305
 
513
- interface GameContext {
514
- balance: number;
515
- bet: number;
516
- lastWin: number;
517
- }
518
-
519
- const fsm = new StateMachine<GameContext>({
520
- balance: 10000,
521
- bet: 100,
522
- lastWin: 0,
306
+ const fsm = new StateMachine<{ balance: number; bet: number }>({
307
+ balance: 10000, bet: 100,
523
308
  });
524
309
 
525
310
  fsm.addState('idle', {
526
- enter: (ctx) => console.log('Waiting for player...'),
527
- update: (ctx, dt) => { /* per-frame logic */ },
311
+ enter: (ctx) => console.log('Waiting...'),
528
312
  });
529
313
 
530
314
  fsm.addState('spinning', {
531
315
  enter: async (ctx) => {
532
- // Play spin animation
533
316
  await spinReels();
534
- // Auto-transition to result
535
- await fsm.transition('result');
536
- },
537
- });
538
-
539
- fsm.addState('result', {
540
- enter: async (ctx) => {
541
- if (ctx.lastWin > 0) {
542
- await showWinAnimation(ctx.lastWin);
543
- }
544
317
  await fsm.transition('idle');
545
318
  },
546
319
  });
547
320
 
548
- // Guards
549
321
  fsm.addGuard('idle', 'spinning', (ctx) => ctx.balance >= ctx.bet);
550
322
 
551
- // Events
552
- fsm.on('transition', ({ from, to }) => {
553
- console.log(`${from} → ${to}`);
554
- });
555
-
556
- // Start
557
323
  await fsm.start('idle');
558
-
559
- // Trigger transitions
560
- const success = await fsm.transition('spinning');
561
- if (!success) {
562
- console.log('Transition blocked by guard');
563
- }
564
-
565
- // Per-frame update (usually called from Scene.onUpdate)
566
- fsm.update(dt);
324
+ const success = await fsm.transition('spinning'); // false if guard blocks
325
+ fsm.update(dt); // call from Scene.onUpdate
567
326
  ```
568
327
 
569
328
  ---
@@ -572,1320 +331,440 @@ fsm.update(dt);
572
331
 
573
332
  ### Tween
574
333
 
575
- `Tween` provides a Promise-based animation system integrated with the PixiJS Ticker:
334
+ Promise-based animation system on the PixiJS Ticker. No external libraries.
576
335
 
577
336
  ```typescript
578
337
  import { Tween, Easing } from '@energy8platform/game-engine';
579
338
 
580
- // Animate to target values
581
339
  await Tween.to(sprite, { alpha: 0, y: 100 }, 500, Easing.easeOutCubic);
582
-
583
- // Animate from starting values to current
584
340
  await Tween.from(sprite, { scale: 0 }, 300, Easing.easeOutBack);
585
-
586
- // Animate between two sets of values
587
341
  await Tween.fromTo(sprite, { x: -100 }, { x: 500 }, 1000, Easing.easeInOutQuad);
588
-
589
- // Wait (uses PixiJS Ticker for consistent timing)
590
342
  await Tween.delay(1000);
591
343
 
592
- // Cancel tweens
593
344
  Tween.killTweensOf(sprite);
594
345
  Tween.killAll();
595
-
596
- // Full reset — kill all tweens and remove ticker listener
597
- // Useful for cleanup between game instances, tests, or hot-reload
598
- Tween.reset();
599
-
600
- // Supports nested properties
601
- await Tween.to(sprite, { 'scale.x': 2, 'position.y': 300 }, 500);
346
+ Tween.reset(); // kill all + remove ticker listener
602
347
  ```
603
348
 
349
+ All standard easings available: `linear`, `easeIn/Out/InOut` for `Quad`, `Cubic`, `Quart`, `Sine`, `Expo`, `Back`, `Bounce`, `Elastic`.
350
+
604
351
  ### Timeline
605
352
 
606
- `Timeline` chains sequential and parallel animation steps:
353
+ Chains sequential and parallel animation steps:
607
354
 
608
355
  ```typescript
609
- import { Timeline, Tween, Easing } from '@energy8platform/game-engine';
610
-
611
356
  const tl = new Timeline();
612
-
613
357
  tl.to(title, { alpha: 1, y: 0 }, 500, Easing.easeOutCubic)
614
358
  .delay(200)
615
359
  .parallel(
616
360
  () => Tween.to(btn1, { alpha: 1 }, 300),
617
361
  () => Tween.to(btn2, { alpha: 1 }, 300),
618
- () => Tween.to(btn3, { alpha: 1 }, 300),
619
362
  )
620
- .call(() => console.log('Intro complete!'));
621
-
363
+ .call(() => console.log('Done!'));
622
364
  await tl.play();
623
365
  ```
624
366
 
625
- ### Available Easings
626
-
627
- All 24 easing functions:
628
-
629
- | Linear | Quad | Cubic | Quart |
630
- | --- | --- | --- | --- |
631
- | `linear` | `easeInQuad` | `easeInCubic` | `easeInQuart` |
632
- | | `easeOutQuad` | `easeOutCubic` | `easeOutQuart` |
633
- | | `easeInOutQuad` | `easeInOutCubic` | `easeInOutQuart` |
634
-
635
- | Sine | Expo | Back | Bounce / Elastic |
636
- | --- | --- | --- | --- |
637
- | `easeInSine` | `easeInExpo` | `easeInBack` | `easeOutBounce` |
638
- | `easeOutSine` | `easeOutExpo` | `easeOutBack` | `easeInBounce` |
639
- | `easeInOutSine` | `easeInOutExpo` | `easeInOutBack` | `easeInOutBounce` |
640
- | | | | `easeOutElastic` |
641
- | | | | `easeInElastic` |
642
-
643
367
  ### SpriteAnimation
644
368
 
645
- Frame-based animation helper wrapping PixiJS `AnimatedSprite`. Cheaper than Spine for simple frame sequences — perfect for coin showers, symbol animations, sparkle trails, and win celebrations.
369
+ Frame-based animation wrapping PixiJS `AnimatedSprite`. Config: `{ fps, loop, autoPlay, onComplete, anchor }`.
646
370
 
647
371
  ```typescript
648
372
  import { SpriteAnimation } from '@energy8platform/game-engine';
649
- import { Assets } from 'pixi.js';
650
-
651
- // From an array of textures
652
- const coinAnim = SpriteAnimation.create(coinTextures, {
653
- fps: 30,
654
- loop: true,
655
- });
656
- scene.container.addChild(coinAnim);
657
-
658
- // From a spritesheet using a name prefix
659
- const sheet = Assets.get('effects');
660
- const sparkle = SpriteAnimation.fromSpritesheet(sheet, 'sparkle_', {
661
- fps: 24,
662
- loop: true,
663
- });
664
373
 
665
- // From a numbered range (e.g., 'explosion_00' to 'explosion_24')
666
- const explosion = SpriteAnimation.fromRange(sheet, 'explosion_{i}', 0, 24, {
667
- fps: 60,
668
- loop: false,
669
- onComplete: () => explosion.destroy(),
670
- });
671
-
672
- // From pre-loaded texture aliases
673
- const anim = SpriteAnimation.fromAliases(
674
- ['frame_0', 'frame_1', 'frame_2', 'frame_3'],
675
- { fps: 12 },
676
- );
374
+ const coin = SpriteAnimation.create(coinTextures, { fps: 30, loop: true });
375
+ const sparkle = SpriteAnimation.fromSpritesheet(sheet, 'sparkle_', { fps: 24 });
677
376
 
678
- // Fire-and-forget: play once and auto-destroy
679
- const { sprite, finished } = SpriteAnimation.playOnce(coinTextures, {
680
- fps: 30,
681
- });
682
- scene.container.addChild(sprite);
683
- await finished; // resolves when animation completes
377
+ // Fire-and-forget
378
+ const { sprite, finished } = SpriteAnimation.playOnce(textures, { fps: 30 });
379
+ container.addChild(sprite);
380
+ await finished;
684
381
  ```
685
382
 
686
- #### SpriteAnimationConfig
687
-
688
- | Property | Type | Default | Description |
689
- | --- | --- | --- | --- |
690
- | `fps` | `number` | `24` | Frames per second |
691
- | `loop` | `boolean` | `true` | Whether to loop |
692
- | `autoPlay` | `boolean` | `true` | Start playing immediately |
693
- | `onComplete` | `() => void` | — | Callback when animation completes (non-looping) |
694
- | `anchor` | `number \| { x, y }` | `0.5` | Anchor point |
695
-
696
383
  ### Spine Animations
697
384
 
698
- If `@esotericsoftware/spine-pixi-v8` is installed:
385
+ Requires `@esotericsoftware/spine-pixi-v8`:
699
386
 
700
387
  ```typescript
701
388
  import { SpineHelper } from '@energy8platform/game-engine';
702
389
 
703
- // Create a Spine instance
704
- const spine = await SpineHelper.create('character-skel', 'character-atlas', {
705
- scale: 0.5,
706
- });
707
- container.addChild(spine);
708
-
709
- // Play animation (returns Promise that resolves on completion)
710
- await SpineHelper.playAnimation(spine, 'idle', true); // loop
711
-
712
- // Queue animation
713
- SpineHelper.addAnimation(spine, 'walk', 0.2, true);
714
-
715
- // Skins
390
+ const spine = await SpineHelper.create('character-skel', 'character-atlas', { scale: 0.5 });
391
+ await SpineHelper.playAnimation(spine, 'idle', true);
716
392
  SpineHelper.setSkin(spine, 'warrior');
717
- console.log(SpineHelper.getSkinNames(spine));
718
- console.log(SpineHelper.getAnimationNames(spine));
719
393
  ```
720
394
 
721
395
  ---
722
396
 
723
397
  ## UI Components
724
398
 
725
- > UI components are built on top of [`@pixi/ui`](https://github.com/pixijs/ui) and [`@pixi/layout`](https://github.com/pixijs/layout) (Yoga flexbox). Install them as peer dependencies:
726
- >
727
- > ```bash
728
- > npm install @pixi/ui @pixi/layout yoga-layout
729
- > ```
730
- >
731
- > **Important:** These packages are optional peer dependencies. Import UI components from the `@energy8platform/game-engine/ui` sub-path to ensure tree-shaking works correctly. The root `@energy8platform/game-engine` barrel re-exports UI components but does **not** activate the `@pixi/layout` mixin — that only happens when importing from `/ui`.
732
- >
733
- > **Direct access:** The engine wraps only the most common UI components. For anything else from `@pixi/ui` (e.g. `Slider`, `CheckBox`, `Input`, `Select`, `RadioGroup`, `List`, `DoubleSlider`, `Switcher`) or `@pixi/layout` (e.g. `LayoutContainer`, `LayoutView`, `Trackpad`, layout-aware sprites), import directly from the source package:
734
- >
735
- > ```typescript
736
- > // Engine-wrapped components
737
- > import { Button, Panel, Layout, ScrollContainer } from '@energy8platform/game-engine/ui';
399
+ > Requires `@pixi/ui` and `@pixi/layout` + `yoga-layout` as peer dependencies. Import from `@energy8platform/game-engine/ui`.
738
400
  >
739
- > // Raw @pixi/ui components (not wrapped by the engine)
740
- > import { Slider, CheckBox, Input, Select, RadioGroup } from '@pixi/ui';
741
- >
742
- > // Raw @pixi/layout components
743
- > import { LayoutContainer, LayoutView } from '@pixi/layout/components';
744
- > import type { LayoutStyles } from '@pixi/layout';
745
- > ```
401
+ > For components not wrapped by the engine (`Slider`, `CheckBox`, `Input`, `Select`, etc.), import directly from `@pixi/ui`.
746
402
 
747
403
  ### Layout
748
404
 
749
- Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine). Supports horizontal, vertical, grid, and wrap modes with alignment, padding, gap, anchor positioning, and viewport breakpoints.
405
+ Responsive flexbox container powered by `@pixi/layout`. Supports `horizontal`, `vertical`, `grid`, `wrap` modes with alignment, padding, gap, anchoring, and viewport breakpoints.
750
406
 
751
407
  ```typescript
752
- import { Layout } from '@energy8platform/game-engine';
408
+ import { Layout } from '@energy8platform/game-engine/ui';
753
409
 
754
- // Horizontal toolbar anchored to bottom-center
755
410
  const toolbar = new Layout({
756
411
  direction: 'horizontal',
757
412
  gap: 20,
758
413
  alignment: 'center',
759
414
  anchor: 'bottom-center',
760
415
  padding: 16,
761
- breakpoints: {
762
- 768: { direction: 'vertical', gap: 10 },
763
- },
416
+ breakpoints: { 768: { direction: 'vertical', gap: 10 } },
764
417
  });
765
-
766
418
  toolbar.addItem(spinButton);
767
419
  toolbar.addItem(betLabel);
768
- toolbar.addItem(balanceDisplay);
769
- scene.container.addChild(toolbar);
770
-
771
- // Update position on resize
772
420
  toolbar.updateViewport(width, height);
773
421
  ```
774
422
 
775
- ```typescript
776
- // Grid layout for a symbol paytable
777
- const grid = new Layout({
778
- direction: 'grid',
779
- columns: 3,
780
- gap: 16,
781
- alignment: 'center',
782
- anchor: 'center',
783
- padding: [20, 40, 20, 40],
784
- });
785
-
786
- symbols.forEach((sym) => grid.addItem(sym));
787
- grid.updateViewport(viewWidth, viewHeight);
788
- ```
789
-
790
- ```typescript
791
- // Wrap layout — items flow and wrap to next line
792
- const tags = new Layout({
793
- direction: 'wrap',
794
- gap: 8,
795
- maxWidth: 600,
796
- });
797
- ```
798
-
799
- #### LayoutConfig
800
-
801
- | Property | Type | Default | Description |
802
- | --- | --- | --- | --- |
803
- | `direction` | `'horizontal' \| 'vertical' \| 'grid' \| 'wrap'` | `'vertical'` | Layout direction |
804
- | `gap` | `number` | `0` | Gap between children (px) |
805
- | `padding` | `number \| [top, right, bottom, left]` | `0` | Inner padding |
806
- | `alignment` | `'start' \| 'center' \| 'end' \| 'stretch'` | `'start'` | Cross-axis alignment |
807
- | `anchor` | `LayoutAnchor` | `'top-left'` | Position relative to viewport |
808
- | `columns` | `number` | `2` | Column count (grid mode only) |
809
- | `maxWidth` | `number` | `Infinity` | Max width before wrapping (wrap mode) |
810
- | `autoLayout` | `boolean` | `true` | Auto-recalculate on add/remove |
811
- | `breakpoints` | `Record<number, Partial<LayoutConfig>>` | — | Override config per viewport width |
812
-
813
- **Anchor values:** `top-left`, `top-center`, `top-right`, `center-left`, `center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right`
814
-
815
- > **Under the hood**: Layout maps `direction` → Yoga `flexDirection`/`flexWrap`, `alignment` → `alignItems`, and uses `@pixi/layout`'s `container.layout = { ... }` mixin. Grid mode uses `flexGrow`/`flexBasis` when `gap > 0` to correctly distribute space, and percentage widths when `gap === 0`.
423
+ **Anchors:** `top-left`, `top-center`, `top-right`, `center-left`, `center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right`
816
424
 
817
425
  ### ScrollContainer
818
426
 
819
- Scrollable container powered by `@pixi/ui` ScrollBox. Provides touch/drag scrolling, mouse wheel support, inertia, and dynamic rendering optimization for off-screen items.
427
+ Touch/drag scrollable container with mouse wheel, inertia, and dynamic rendering. Extends `@pixi/ui` ScrollBox.
820
428
 
821
429
  ```typescript
822
- import { ScrollContainer } from '@energy8platform/game-engine';
823
-
824
430
  const scroll = new ScrollContainer({
825
- width: 600,
826
- height: 400,
431
+ width: 600, height: 400,
827
432
  direction: 'vertical',
828
433
  elementsMargin: 8,
829
- borderRadius: 12,
830
434
  backgroundColor: 0x1a1a2e,
831
435
  });
832
-
833
- // Add items directly
834
- for (let i = 0; i < 50; i++) {
835
- scroll.addItem(createRow(i));
836
- }
837
-
838
- scene.container.addChild(scroll);
436
+ for (let i = 0; i < 50; i++) scroll.addItem(createRow(i));
839
437
  ```
840
438
 
841
- ```typescript
842
- // Or set content from a Container
843
- const list = new Container();
844
- items.forEach((item) => list.addChild(item));
845
- scroll.setContent(list);
846
-
847
- // Scroll to a specific item index
848
- scroll.scrollToItem(5);
849
-
850
- // Current position
851
- const { x, y } = scroll.scrollPosition;
852
- ```
853
-
854
- #### ScrollContainerConfig
855
-
856
- | Property | Type | Default | Description |
857
- | --- | --- | --- | --- |
858
- | `width` | `number` | — | Visible viewport width |
859
- | `height` | `number` | — | Visible viewport height |
860
- | `direction` | `'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | Scroll direction |
861
- | `backgroundColor` | `ColorSource` | — | Background color (transparent if omitted) |
862
- | `borderRadius` | `number` | `0` | Mask border radius |
863
- | `elementsMargin` | `number` | `0` | Gap between items |
864
- | `padding` | `number` | `0` | Content padding |
865
- | `disableDynamicRendering` | `boolean` | `false` | Render all items even when off-screen |
866
- | `disableEasing` | `boolean` | `false` | Disable scroll inertia/easing |
867
- | `globalScroll` | `boolean` | `true` | Scroll even when mouse is not over the component |
868
-
869
- > **Note:** ScrollContainer extends `@pixi/ui` `ScrollBox`. All `ScrollBox` methods and options are available.
870
-
871
439
  ### Button
872
440
 
873
- Powered by `@pixi/ui` `FancyButton`. Supports both texture-based and Graphics-based rendering with per-state views, press animation, and built-in text.
441
+ Extends `@pixi/ui` FancyButton. Supports Graphics-based and texture-based states, press animation, and text.
874
442
 
875
443
  ```typescript
876
- import { Button } from '@energy8platform/game-engine';
877
-
878
444
  const spinBtn = new Button({
879
- width: 200,
880
- height: 60,
445
+ width: 200, height: 60,
881
446
  borderRadius: 12,
882
- colors: {
883
- default: 0xffd700,
884
- hover: 0xffe44d,
885
- pressed: 0xccac00,
886
- disabled: 0x666666,
887
- },
447
+ colors: { default: 0xffd700, hover: 0xffe44d, pressed: 0xccac00, disabled: 0x666666 },
888
448
  pressScale: 0.95,
889
- animationDuration: 100,
890
449
  text: 'SPIN',
891
450
  });
892
-
893
- // Connect press event (Signal-based)
894
- spinBtn.onPress.connect(() => {
895
- console.log('Spin!');
896
- });
897
-
898
- // Or use texture-based states
899
- const btn = new Button({
900
- textures: {
901
- default: 'btn-default',
902
- hover: 'btn-hover',
903
- pressed: 'btn-pressed',
904
- disabled: 'btn-disabled',
905
- },
906
- });
907
-
908
- btn.enable();
909
- btn.disable();
451
+ spinBtn.onPress.connect(() => console.log('Spin!'));
452
+ spinBtn.disable();
453
+ spinBtn.enable();
910
454
  ```
911
455
 
912
- > **Breaking change:** Button states renamed from `normal` to `default`. The `onTap` callback is replaced by `onPress.connect()` (typed-signals Signal).
913
-
914
- ### Label
456
+ ### Other UI Components
915
457
 
458
+ **Label** — styled text wrapper:
916
459
  ```typescript
917
- import { Label } from '@energy8platform/game-engine';
918
-
919
- const label = new Label({
920
- text: 'TOTAL WIN',
921
- style: { fontSize: 36, fill: 0xffffff },
922
- });
460
+ new Label({ text: 'TOTAL WIN', style: { fontSize: 36, fill: 0xffffff } });
923
461
  ```
924
462
 
925
- ### BalanceDisplay
926
-
927
- Displays player balance with formatting:
928
-
463
+ **BalanceDisplay** — animated currency display:
929
464
  ```typescript
930
- import { BalanceDisplay } from '@energy8platform/game-engine';
931
-
932
- const balance = new BalanceDisplay({
933
- currency: 'USD',
934
- animated: true,
935
- // ... text style options
936
- });
937
-
938
- balance.setValue(9500); // Animates the balance change
465
+ const balance = new BalanceDisplay({ currency: 'USD', animated: true });
466
+ balance.setValue(9500);
939
467
  ```
940
468
 
941
- ### WinDisplay
942
-
943
- Animated win amount display with countup:
944
-
469
+ **WinDisplay** — countup win animation:
945
470
  ```typescript
946
- import { WinDisplay } from '@energy8platform/game-engine';
947
-
948
- const winDisplay = new WinDisplay({
949
- countupDuration: 2000,
950
- // ... text style options
951
- });
952
-
953
- await winDisplay.showWin(5000); // Show $50.00, countup over 2 seconds
954
- winDisplay.hide();
471
+ await winDisplay.showWin(5000); // countup over 2 seconds
955
472
  ```
956
473
 
957
- ### ProgressBar
958
-
959
- Horizontal progress bar powered by `@pixi/ui` ProgressBar. Supports animated smooth fill.
960
-
474
+ **ProgressBar** — animated fill bar (wraps `@pixi/ui`):
961
475
  ```typescript
962
- import { ProgressBar } from '@energy8platform/game-engine';
963
-
964
- const bar = new ProgressBar({
965
- width: 400,
966
- height: 20,
967
- fillColor: 0x00ff00,
968
- trackColor: 0x333333,
969
- borderRadius: 10,
970
- animated: true,
971
- });
972
-
973
- bar.progress = 0.75; // 75%
974
-
975
- // Call each frame for smooth animation
976
- bar.update(dt);
476
+ const bar = new ProgressBar({ width: 400, height: 20, fillColor: 0x00ff00, animated: true });
477
+ bar.progress = 0.75;
478
+ bar.update(dt); // call each frame for animation
977
479
  ```
978
480
 
979
- ### Panel
980
-
981
- Background panel powered by `@pixi/layout` `LayoutContainer`. Supports both Graphics-based (color + border) and 9-slice sprite backgrounds. Children added to `content` participate in flexbox layout.
982
-
481
+ **Panel** — background panel with flex layout (Graphics or 9-slice):
983
482
  ```typescript
984
- import { Panel } from '@energy8platform/game-engine';
985
-
986
- // Simple colored panel
987
- const panel = new Panel({
988
- width: 600,
989
- height: 400,
990
- backgroundColor: 0x1a1a2e,
991
- borderRadius: 16,
992
- backgroundAlpha: 0.9,
993
- padding: 16,
994
- });
995
-
996
- panel.content.addChild(myText);
997
-
998
- // 9-slice panel (texture-based)
999
- const nineSlicePanel = new Panel({
1000
- nineSliceTexture: 'panel-bg',
1001
- nineSliceBorders: [20, 20, 20, 20],
1002
- width: 400,
1003
- height: 300,
1004
- });
483
+ const panel = new Panel({ width: 600, height: 400, backgroundColor: 0x1a1a2e, borderRadius: 16, padding: 16 });
484
+ panel.content.addChild(myText); // children participate in flexbox
1005
485
  ```
1006
486
 
1007
- ### Modal
1008
-
1009
- Full-screen overlay dialog:
1010
-
487
+ **Modal** — full-screen overlay dialog:
1011
488
  ```typescript
1012
- import { Modal } from '@energy8platform/game-engine';
1013
-
1014
- const modal = new Modal({
1015
- overlayAlpha: 0.7,
1016
- overlayColor: 0x000000,
1017
- closeOnOverlay: true,
1018
- animationDuration: 300,
1019
- });
1020
-
489
+ const modal = new Modal({ overlayAlpha: 0.7, closeOnOverlay: true, animationDuration: 300 });
1021
490
  await modal.show(viewWidth, viewHeight);
1022
- await modal.hide();
1023
491
  ```
1024
492
 
1025
- ### Toast
1026
-
1027
- Brief notification messages with animated appearance/dismissal.
1028
-
493
+ **Toast** — notification messages (`info`, `success`, `warning`, `error`):
1029
494
  ```typescript
1030
- import { Toast } from '@energy8platform/game-engine';
1031
-
1032
- const toast = new Toast({
1033
- duration: 2000,
1034
- bottomOffset: 60,
1035
- });
1036
-
1037
- // Show with type and viewport size
1038
495
  await toast.show('Free spins activated!', 'success', viewWidth, viewHeight);
1039
-
1040
- // Dismiss manually
1041
- await toast.dismiss();
1042
- ```
1043
-
1044
- **Toast types:** `info`, `success`, `warning`, `error` — each with a distinct color.
1045
-
1046
- ---
1047
-
1048
- ### Using Raw `@pixi/ui` and `@pixi/layout` Components
1049
-
1050
- The engine wraps the most common components, but both libraries offer much more. Here are examples of using raw components alongside the engine:
1051
-
1052
- ```typescript
1053
- import { Slider, CheckBox, Input, Select } from '@pixi/ui';
1054
- import { LayoutContainer } from '@pixi/layout/components';
1055
- import type { LayoutStyles } from '@pixi/layout';
1056
-
1057
- // Slider (not wrapped by the engine)
1058
- const volumeSlider = new Slider({
1059
- bg: bgSprite,
1060
- fill: fillSprite,
1061
- slider: handleSprite,
1062
- min: 0,
1063
- max: 100,
1064
- value: 50,
1065
- });
1066
-
1067
- // CheckBox
1068
- const muteCheck = new CheckBox({
1069
- style: {
1070
- unchecked: uncheckedView,
1071
- checked: checkedView,
1072
- },
1073
- });
1074
-
1075
- // LayoutContainer with flexbox styles
1076
- const row = new LayoutContainer();
1077
- row.layout = {
1078
- flexDirection: 'row',
1079
- gap: 12,
1080
- alignItems: 'center',
1081
- padding: 16,
1082
- } satisfies LayoutStyles;
1083
- row.addChild(volumeSlider, muteCheck);
1084
- ```
1085
-
1086
- > **Tip:** Any container created after importing `@energy8platform/game-engine/ui` can use the `container.layout = { ... }` mixin — the `@pixi/layout` side-effect is automatically activated.
1087
-
1088
- ---
1089
-
1090
- ## Flexbox-First Layout
1091
-
1092
- > **Best practice:** Use `@pixi/layout` flexbox instead of manual pixel positioning. Flexbox adapts to screen sizes, handles RTL, and eliminates fragile `x = width / 2 - 100` math.
1093
-
1094
- ### ❌ Avoid: Manual Pixel Positioning
1095
-
1096
- ```typescript
1097
- // Fragile — breaks when screen size, text length, or element sizes change
1098
- onResize(width: number, height: number) {
1099
- this.title.x = width / 2;
1100
- this.title.y = 80;
1101
- this.balance.x = width / 2;
1102
- this.balance.y = 220;
1103
- this.spinButton.x = width / 2;
1104
- this.spinButton.y = height - 120;
1105
- }
1106
- ```
1107
-
1108
- ### ✅ Prefer: Flexbox Layout
1109
-
1110
- ```typescript
1111
- import { Layout } from '@energy8platform/game-engine/ui';
1112
- import { LayoutContainer } from '@pixi/layout/components';
1113
- import type { LayoutStyles } from '@pixi/layout';
1114
-
1115
- // Root layout — fills the screen, stacks children vertically
1116
- const root = new LayoutContainer();
1117
- root.layout = {
1118
- width: '100%',
1119
- height: '100%',
1120
- flexDirection: 'column',
1121
- alignItems: 'center',
1122
- justifyContent: 'space-between',
1123
- padding: 40,
1124
- } satisfies LayoutStyles;
1125
-
1126
- // Header area
1127
- const header = new LayoutContainer();
1128
- header.layout = {
1129
- flexDirection: 'column',
1130
- alignItems: 'center',
1131
- gap: 12,
1132
- };
1133
- header.addChild(title, subtitle, balance);
1134
-
1135
- // Center area — expands to fill available space
1136
- const center = new LayoutContainer();
1137
- center.layout = {
1138
- flexGrow: 1,
1139
- alignItems: 'center',
1140
- justifyContent: 'center',
1141
- };
1142
- center.addChild(winDisplay);
1143
-
1144
- // Footer toolbar
1145
- const footer = new Layout({
1146
- direction: 'horizontal',
1147
- gap: 20,
1148
- alignment: 'center',
1149
- });
1150
- footer.addItem(betLabel);
1151
- footer.addItem(spinButton);
1152
-
1153
- root.addChild(header, center, footer);
1154
- scene.container.addChild(root);
1155
- ```
1156
-
1157
- ### When to Use Each Approach
1158
-
1159
- | Approach | Use When |
1160
- |---|---|
1161
- | Engine `Layout` | Toolbar-style rows/columns with anchor positioning and breakpoints |
1162
- | `LayoutContainer` (raw) | Full flexbox control — `flexGrow`, `justifyContent`, percentage sizes |
1163
- | `container.layout = { ... }` (mixin) | Adding flex styles to any existing PixiJS container |
1164
- | Manual pixel positioning | Exact artistic placement (e.g. particle emitters, spine anchors) |
1165
-
1166
- ### Nested Flexbox Example
1167
-
1168
- ```typescript
1169
- // settings-panel.ts — responsive settings UI
1170
- import { LayoutContainer } from '@pixi/layout/components';
1171
- import { Slider, CheckBox } from '@pixi/ui';
1172
- import { Panel, Label } from '@energy8platform/game-engine/ui';
1173
-
1174
- const panel = new Panel({
1175
- width: 500,
1176
- height: 400,
1177
- backgroundColor: 0x1a1a2e,
1178
- borderRadius: 16,
1179
- padding: 24,
1180
- });
1181
-
1182
- // Each row: label on left, control on right
1183
- function settingsRow(labelText: string, control: Container): LayoutContainer {
1184
- const row = new LayoutContainer();
1185
- row.layout = {
1186
- flexDirection: 'row',
1187
- justifyContent: 'space-between',
1188
- alignItems: 'center',
1189
- width: '100%',
1190
- height: 48,
1191
- };
1192
- row.addChild(new Label({ text: labelText }), control);
1193
- return row;
1194
- }
1195
-
1196
- panel.content.addChild(
1197
- settingsRow('Music Volume', musicSlider),
1198
- settingsRow('SFX Volume', sfxSlider),
1199
- settingsRow('Mute', muteCheckbox),
1200
- );
1201
496
  ```
1202
497
 
1203
498
  ---
1204
499
 
1205
- ## React Integration
1206
-
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.
1208
-
1209
- ### Installation
1210
-
1211
- ```bash
1212
- npm install react react-dom react-reconciler
1213
- ```
1214
-
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:
1218
-
1219
- ```tsx
1220
- // scenes/GameScene.tsx
1221
- import { ReactScene, extendPixiElements, useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
1222
-
1223
- // Register PixiJS components for JSX (call once at startup)
1224
- extendPixiElements();
1225
-
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
- }
1237
- }
1238
- ```
1239
-
1240
- ```tsx
1241
- // components/GameRoot.tsx
1242
- import { useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
1243
-
1244
- export function GameRoot() {
1245
- const { width, height, isPortrait } = useViewport();
1246
- const balance = useBalance();
1247
- const sdk = useSDK();
1248
-
1249
- return (
1250
- <container>
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>
1265
- </container>
1266
- );
1267
- }
1268
- ```
1269
-
1270
- **Lifecycle:** `onEnter` mounts the React tree, `onResize` re-renders with updated viewport context, `onExit` and `onDestroy` unmount.
1271
-
1272
- ### Hooks
1273
-
1274
- All hooks must be used inside a `ReactScene`:
1275
-
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);
1300
-
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() {
1314
- return (
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>
1327
- );
1328
- }
1329
- ```
1330
-
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.
1359
-
1360
- ---
1361
-
1362
500
  ## Input
1363
501
 
1364
- `InputManager` provides unified touch/mouse/keyboard handling with gesture detection:
502
+ `InputManager` provides unified touch/mouse/keyboard handling with gesture detection.
1365
503
 
1366
504
  ```typescript
1367
505
  const input = game.input;
1368
506
 
1369
- // Tap/click
1370
- input.on('tap', ({ x, y }) => {
1371
- console.log(`Tap at ${x}, ${y}`);
1372
- });
1373
-
1374
- // Swipe gesture
1375
- input.on('swipe', ({ direction, velocity }) => {
1376
- console.log(`Swipe ${direction} at ${velocity}px/s`);
1377
- // direction: 'up' | 'down' | 'left' | 'right'
1378
- });
507
+ input.on('tap', ({ x, y }) => console.log(`Tap at ${x}, ${y}`));
508
+ input.on('swipe', ({ direction, velocity }) => console.log(`Swipe ${direction}`));
509
+ input.on('keydown', ({ key, code }) => { if (code === 'Space') startSpin(); });
510
+ if (input.isKeyDown('ArrowLeft')) { /* move left */ }
1379
511
 
1380
- // Keyboard
1381
- input.on('keydown', ({ key, code }) => {
1382
- if (code === 'Space') startSpin();
1383
- });
1384
-
1385
- // Check current key state
1386
- if (input.isKeyDown('ArrowLeft')) {
1387
- // Move left
1388
- }
1389
-
1390
- // Lock input during animations
1391
- input.lock();
1392
- // ... animation plays ...
512
+ input.lock(); // lock during animations
1393
513
  input.unlock();
1394
514
 
1395
- // Convert DOM canvas position to game-world coordinates
1396
- // (accounts for viewport scaling and offset)
1397
- const worldPos = input.getWorldPosition(canvasX, canvasY);
1398
- console.log(worldPos.x, worldPos.y);
515
+ const worldPos = input.getWorldPosition(canvasX, canvasY); // DOM → game-world coords
1399
516
  ```
1400
517
 
1401
518
  **Events:** `tap`, `press`, `release`, `move`, `swipe`, `keydown`, `keyup`
1402
519
 
1403
- > **Note:** The viewport transform for coordinate mapping is wired up automatically by `GameApplication`. Call `getWorldPosition()` when you need to convert raw DOM coordinates to game-world space.
1404
-
1405
520
  ---
1406
521
 
1407
522
  ## Vite Configuration
1408
523
 
1409
- The engine provides a pre-configured Vite setup via `defineGameConfig`:
1410
-
1411
524
  ```typescript
1412
525
  // vite.config.ts
1413
526
  import { defineGameConfig } from '@energy8platform/game-engine/vite';
1414
527
 
1415
528
  export default defineGameConfig({
1416
529
  base: '/games/my-slot/',
1417
- devBridge: true, // Auto-inject DevBridge in dev mode
1418
- devBridgeConfig: './dev.config', // Custom config path (optional)
1419
- vite: {
1420
- // Additional Vite config overrides
1421
- },
530
+ devBridge: true,
531
+ devBridgeConfig: './dev.config', // optional custom config path
532
+ vite: { /* additional Vite config */ },
1422
533
  });
1423
534
  ```
1424
535
 
1425
- ### `GameConfig`
536
+ **What `defineGameConfig` provides:** ESNext build target (for yoga-layout WASM), asset inlining (<8KB), PixiJS chunk splitting, DevBridge auto-injection in dev mode, dependency deduplication (`pixi.js`, `@pixi/layout`, `react`, etc.), and pre-bundling optimization.
1426
537
 
1427
- | Property | Type | Default | Description |
1428
- | --- | --- | --- | --- |
1429
- | `base` | `string` | `'/'` | Vite `base` path for deployment |
1430
- | `devBridge` | `boolean` | `false` | Auto-inject DevBridge in dev mode |
1431
- | `devBridgeConfig` | `string` | `'./dev.config'` | Path to DevBridge config file |
1432
- | `vite` | `UserConfig` | — | Additional Vite config to merge |
1433
-
1434
- ### What `defineGameConfig` Provides
538
+ ---
1435
539
 
1436
- - **Build target: ESNext** — required for `yoga-layout` WASM (uses top-level `await`)
1437
- - **Asset inlining** — files under 8KB are auto-inlined
1438
- - **PixiJS chunk splitting** — `pixi.js` is extracted into a separate chunk for caching
1439
- - **DevBridge injection** — automatically available in dev mode via virtual module
1440
- - **Dev server** — port 3000, auto-open browser
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
540
+ ## Lua Engine
1443
541
 
1444
- ### Custom DevBridge Configuration
542
+ Runs platform Lua game scripts in the browser via `fengari` (Lua 5.3, pure JS). Replicates server-side execution for development — no backend required.
1445
543
 
1446
- Create `dev.config.ts` at the project root:
544
+ ### DevBridge Integration (recommended)
1447
545
 
1448
546
  ```typescript
1449
547
  // dev.config.ts
1450
- import type { DevBridgeConfig } from '@energy8platform/game-engine/debug';
548
+ import luaScript from './game.lua?raw';
1451
549
 
1452
550
  export default {
1453
- balance: 50000,
1454
- currency: 'EUR',
1455
- networkDelay: 100,
1456
- onPlay: ({ action, bet }) => {
1457
- // Custom play result logic
1458
- const win = Math.random() < 0.3 ? bet * 10 : 0;
1459
- return { win, balance: 50000 - bet + win };
1460
- },
1461
- } satisfies DevBridgeConfig;
1462
- ```
1463
-
1464
- This file is auto-imported by the Vite plugin when `devBridge: true`. The plugin injects a virtual module (`/@dev-bridge-entry.js`) that starts DevBridge **before** importing your app entry point, ensuring the `MemoryChannel` is ready when the SDK calls `ready()`.
1465
-
1466
- ---
1467
-
1468
- ## DevBridge
1469
-
1470
- `DevBridge` simulates a casino host for local development. It uses the SDK's `Bridge` class in `devMode`, communicating with `CasinoGameSDK` through a shared in-memory `MemoryChannel` — no `postMessage` or iframe required.
1471
-
1472
- > **Requires `@energy8platform/game-sdk` >= 2.7.0**
1473
-
1474
- ```typescript
1475
- import { DevBridge } from '@energy8platform/game-engine/debug';
1476
-
1477
- const bridge = new DevBridge({
1478
- balance: 10000,
551
+ balance: 5000,
1479
552
  currency: 'USD',
1480
- assetsUrl: '/assets/',
1481
- networkDelay: 200,
1482
- debug: true,
1483
- gameConfig: {
553
+ luaScript,
554
+ gameDefinition: {
1484
555
  id: 'my-slot',
1485
- type: 'slot',
1486
- viewport: { width: 1920, height: 1080 },
1487
- betLevels: [0.1, 0.2, 0.5, 1, 2, 5, 10],
1488
- },
1489
- onPlay: ({ action, bet, roundId }) => {
1490
- // Return custom play result
1491
- const win = Math.random() < 0.4 ? bet * 5 : 0;
1492
- return { win };
556
+ type: 'SLOT',
557
+ actions: {
558
+ spin: {
559
+ stage: 'base_game', debit: 'bet', credit: 'win',
560
+ transitions: [
561
+ { condition: 'free_spins_awarded > 0', creates_session: true, next_actions: ['free_spin'] },
562
+ { condition: 'always', next_actions: ['spin'] },
563
+ ],
564
+ },
565
+ free_spin: { stage: 'free_spins', debit: 'none', requires_session: true,
566
+ transitions: [{ condition: 'always', next_actions: ['free_spin'] }],
567
+ },
568
+ },
569
+ bet_levels: [0.2, 0.5, 1, 2, 5],
1493
570
  },
1494
- });
1495
-
1496
- bridge.start(); // Creates SDK Bridge({ devMode: true }) + registers handlers
1497
-
1498
- // Update balance programmatically
1499
- bridge.setBalance(5000);
1500
-
1501
- // Cleanup
1502
- bridge.destroy();
571
+ };
1503
572
  ```
1504
573
 
1505
- When using the Vite plugin with `devBridge: true`, the SDK is automatically configured with `devMode: true` so both sides use the same `MemoryChannel`.
1506
-
1507
- ### Handled Messages
1508
-
1509
- | Message | Description |
1510
- | --- | --- |
1511
- | `GAME_READY` | SDK initialization handshake |
1512
- | `PLAY_REQUEST` | Player action (spin, deal, etc.) |
1513
- | `PLAY_RESULT_ACK` | Acknowledge play result |
1514
- | `GET_BALANCE` | Balance query |
1515
- | `GET_STATE` | Game state query |
1516
- | `OPEN_DEPOSIT` | Deposit dialog request |
1517
-
1518
- ---
1519
-
1520
- ## Debug
1521
-
1522
- ### FPS Overlay
574
+ ### Standalone Usage
1523
575
 
1524
576
  ```typescript
1525
- import { FPSOverlay } from '@energy8platform/game-engine/debug';
1526
-
1527
- const fps = new FPSOverlay(game.app);
1528
- fps.show();
1529
- fps.toggle();
1530
- fps.hide();
577
+ const engine = new LuaEngine({ script: luaSource, gameDefinition, seed: 42 });
578
+ const result = engine.execute({ action: 'spin', bet: 1.0 });
579
+ // result: { totalWin, data, nextActions, session }
580
+ engine.destroy();
1531
581
  ```
1532
582
 
1533
- When `debug: true` is set in `GameApplicationConfig`, the FPS overlay is created and shown automatically — no manual setup needed.
583
+ ### Platform API (`engine.*` in Lua)
1534
584
 
1535
- The overlay displays:
1536
- - Average FPS
1537
- - Minimum FPS
1538
- - Frame time (ms)
585
+ | Function | Description |
586
+ | --- | --- |
587
+ | `engine.random(min, max)` | Random integer `[min, max]` |
588
+ | `engine.random_float()` | Random float `[0.0, 1.0)` |
589
+ | `engine.random_weighted(weights)` | 1-based index from weight table |
590
+ | `engine.shuffle(arr)` | Fisher-Yates shuffle, returns copy |
591
+ | `engine.log(level, msg)` | Log (`"debug"`, `"info"`, `"warn"`, `"error"`) |
592
+ | `engine.get_config()` | Returns `{id, type, bet_levels}` |
1539
593
 
1540
- Updated every ~500ms, sampled over 60 frames.
594
+ **Features:** Action routing, transition evaluation (`>`, `>=`, `==`, `!=`, `&&`, `||`, `"always"`), session management (free spins, retriggers), cross-spin persistent state, max win cap, buy bonus, deterministic seeded PRNG (xoshiro128**).
1541
595
 
1542
- ---
596
+ ### RTP Simulation (CLI)
1543
597
 
1544
- ## API Reference
598
+ Run the same Lua script from `dev.config.ts` through millions of iterations to verify math:
1545
599
 
1546
- ### GameApplication
600
+ ```bash
601
+ # Regular spins (1M iterations, default)
602
+ npx game-engine-simulate
1547
603
 
1548
- ```typescript
1549
- class GameApplication extends EventEmitter<GameEngineEvents> {
1550
- // Fields
1551
- app: Application;
1552
- scenes: SceneManager;
1553
- assets: AssetManager;
1554
- audio: AudioManager;
1555
- input: InputManager;
1556
- viewport: ViewportManager;
1557
- sdk: CasinoGameSDK | null;
1558
- initData: InitData | null;
1559
- readonly config: GameApplicationConfig;
1560
-
1561
- // Getters
1562
- get gameConfig(): GameConfigData | null;
1563
- get session(): SessionData | null;
1564
- get balance(): number;
1565
- get currency(): string;
1566
- get isRunning(): boolean;
1567
-
1568
- // Methods
1569
- constructor(config?: GameApplicationConfig);
1570
- async start(firstScene: string, sceneData?: unknown): Promise<void>;
1571
- destroy(): void;
1572
- }
1573
- ```
604
+ # Buy bonus simulation
605
+ npx game-engine-simulate --action buy_bonus
1574
606
 
1575
- ### SceneManager
607
+ # Ante bet
608
+ npx game-engine-simulate --params '{"ante_bet":true}'
1576
609
 
1577
- ```typescript
1578
- class SceneManager extends EventEmitter<{ change: { from: string | null; to: string } }> {
1579
- get current(): SceneEntry | null;
1580
- get currentKey(): string | null;
1581
- get isTransitioning(): boolean;
1582
-
1583
- setRoot(root: Container): void;
1584
- setApp(app: any): void; // @internal — called by GameApplication
1585
- register(key: string, ctor: SceneConstructor): this;
1586
- async goto(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
1587
- async push(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
1588
- async pop(transition?: TransitionConfig): Promise<void>;
1589
- async replace(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
1590
- update(dt: number): void;
1591
- resize(width: number, height: number): void;
1592
- destroy(): void;
1593
- }
610
+ # Custom parameters
611
+ npx game-engine-simulate --iterations 5000000 --bet 1 --seed 42 --config ./dev.config.ts
1594
612
  ```
1595
613
 
1596
- ### AssetManager
614
+ Output matches the platform's server-side simulation format:
1597
615
 
1598
- ```typescript
1599
- class AssetManager {
1600
- get initialized(): boolean;
1601
- get basePath(): string;
1602
- get loadedBundles(): ReadonlySet<string>;
1603
-
1604
- constructor(basePath?: string, manifest?: AssetManifest);
1605
- async init(): Promise<void>;
1606
- async loadBundle(name: string, onProgress?: (p: number) => void): Promise<Record<string, unknown>>;
1607
- async loadBundles(names: string[], onProgress?: (p: number) => void): Promise<Record<string, unknown>>;
1608
- async load<T>(urls: string | string[], onProgress?: (p: number) => void): Promise<T>;
1609
- get<T>(alias: string): T;
1610
- async unloadBundle(name: string): Promise<void>;
1611
- async backgroundLoad(name: string): Promise<void>;
1612
- getBundleNames(): string[];
1613
- isBundleLoaded(name: string): boolean;
1614
- }
1615
616
  ```
617
+ Starting simulation for my-slot (1000000 iterations, action: spin)...
618
+ Progress: 100000/1000000 (10%)
619
+ ...
1616
620
 
1617
- ### AudioManager
1618
-
1619
- ```typescript
1620
- class AudioManager {
1621
- get initialized(): boolean;
1622
- get muted(): boolean;
1623
-
1624
- constructor(config?: AudioConfig);
1625
- async init(): Promise<void>;
1626
- play(alias: string, category?: AudioCategoryName, options?: { volume?: number; loop?: boolean; speed?: number }): void;
1627
- playMusic(alias: string, fadeDuration?: number): void;
1628
- stopMusic(): void;
1629
- stopAll(): void;
1630
- setVolume(category: AudioCategoryName, volume: number): void;
1631
- getVolume(category: AudioCategoryName): number;
1632
- muteCategory(category: AudioCategoryName): void;
1633
- unmuteCategory(category: AudioCategoryName): void;
1634
- toggleCategory(category: AudioCategoryName): boolean;
1635
- muteAll(): void;
1636
- unmuteAll(): void;
1637
- toggleMute(): boolean;
1638
- duckMusic(factor: number): void;
1639
- unduckMusic(): void;
1640
- destroy(): void;
1641
- }
621
+ --- Simulation Results ---
622
+ Game: my-slot
623
+ Action: spin
624
+ Iterations: 1,000,000
625
+ Duration: 45.2s
626
+ Total RTP: 96.48%
627
+ Base Game RTP: 72.31%
628
+ Bonus RTP: 24.17%
629
+ Hit Frequency: 28.45%
630
+ Max Win: 5234.50x
631
+ Max Win Hits: 3 (rounds capped by max_win)
632
+ Bonus Triggered: 4,521 (1 in 221 spins)
633
+ Bonus Spins Played: 52,847
1642
634
  ```
1643
635
 
1644
- ### ViewportManager
636
+ The CLI reads `luaScript` and `gameDefinition` from your `dev.config.ts` — the same config used for DevBridge. Programmatic usage:
1645
637
 
1646
638
  ```typescript
1647
- class ViewportManager extends EventEmitter<ViewportEvents> {
1648
- get width(): number;
1649
- get height(): number;
1650
- get scale(): number;
1651
- get orientation(): Orientation;
1652
- get designWidth(): number;
1653
- get designHeight(): number;
1654
-
1655
- constructor(app: Application, container: HTMLElement, config: ViewportConfig);
1656
- refresh(): void;
1657
- destroy(): void;
1658
- }
1659
- ```
639
+ import { SimulationRunner, formatSimulationResult } from '@energy8platform/game-engine/lua';
1660
640
 
1661
- ### StateMachine
641
+ const runner = new SimulationRunner({
642
+ script: luaSource,
643
+ gameDefinition,
644
+ iterations: 1_000_000,
645
+ bet: 1.0,
646
+ seed: 42,
647
+ onProgress: (done, total) => console.log(`${done}/${total}`),
648
+ });
1662
649
 
1663
- ```typescript
1664
- class StateMachine<TContext> extends EventEmitter<StateMachineEvents> {
1665
- get current(): string | null;
1666
- get isTransitioning(): boolean;
1667
- get context(): TContext;
1668
-
1669
- constructor(context: TContext);
1670
- addState(name: string, config: { enter?, exit?, update? }): this;
1671
- addGuard(from: string, to: string, guard: (ctx: TContext) => boolean): this;
1672
- async start(initialState: string, data?: unknown): Promise<void>;
1673
- async transition(to: string, data?: unknown): Promise<boolean>;
1674
- update(dt: number): void;
1675
- hasState(name: string): boolean;
1676
- canTransition(to: string): boolean;
1677
- async reset(): Promise<void>;
1678
- async destroy(): Promise<void>;
1679
- }
650
+ const result = runner.run();
651
+ console.log(formatSimulationResult(result));
1680
652
  ```
1681
653
 
1682
- ### Tween
654
+ ---
1683
655
 
1684
- ```typescript
1685
- class Tween {
1686
- static get activeTweens(): number;
1687
-
1688
- static to(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction, onUpdate?: (p: number) => void): Promise<void>;
1689
- static from(target: any, props: Record<string, number>, duration: number, easing?, onUpdate?): Promise<void>;
1690
- static fromTo(target: any, fromProps: Record<string, number>, toProps: Record<string, number>, duration: number, easing?, onUpdate?): Promise<void>;
1691
- static delay(ms: number): Promise<void>; // Uses PixiJS Ticker
1692
- static killTweensOf(target: any): void;
1693
- static killAll(): void;
1694
- static reset(): void; // Kill all + remove ticker listener
1695
- }
1696
- ```
656
+ ## DevBridge
1697
657
 
1698
- ### Timeline
658
+ Simulates a casino host for local development using SDK's `Bridge` in `devMode` (shared `MemoryChannel`, no iframe needed).
1699
659
 
1700
660
  ```typescript
1701
- class Timeline {
1702
- get isPlaying(): boolean;
1703
-
1704
- to(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction): this;
1705
- from(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction): this;
1706
- delay(ms: number): this;
1707
- call(fn: () => void | Promise<void>): this;
1708
- parallel(...fns: Array<() => Promise<void>>): this;
1709
- async play(): Promise<void>;
1710
- cancel(): void;
1711
- clear(): this;
1712
- }
1713
- ```
661
+ import { DevBridge } from '@energy8platform/game-engine/debug';
1714
662
 
1715
- ### InputManager
663
+ const bridge = new DevBridge({
664
+ balance: 10000,
665
+ currency: 'USD',
666
+ networkDelay: 200,
667
+ debug: true,
668
+ gameConfig: { id: 'my-slot', type: 'slot', betLevels: [0.1, 0.5, 1, 5, 10] },
669
+ onPlay: ({ action, bet }) => {
670
+ const win = Math.random() < 0.4 ? bet * 5 : 0;
671
+ return { win };
672
+ },
673
+ // OR use Lua: luaScript, gameDefinition, luaSeed
674
+ });
1716
675
 
1717
- ```typescript
1718
- class InputManager extends EventEmitter<InputEvents> {
1719
- get locked(): boolean;
1720
-
1721
- constructor(canvas: HTMLCanvasElement);
1722
- lock(): void;
1723
- unlock(): void;
1724
- isKeyDown(key: string): boolean;
1725
- setViewportTransform(scale: number, offsetX: number, offsetY: number): void;
1726
- getWorldPosition(canvasX: number, canvasY: number): { x: number; y: number };
1727
- destroy(): void;
1728
- }
676
+ bridge.start();
677
+ bridge.setBalance(5000);
678
+ bridge.destroy();
1729
679
  ```
1730
680
 
1731
- ### Layout
1732
-
1733
- > Powered by `@pixi/layout` (Yoga flexbox). Uses the `@pixi/layout` mixin on a standard `Container`.
1734
-
1735
- ```typescript
1736
- class Layout extends Container {
1737
- get items(): readonly Container[];
1738
-
1739
- constructor(config?: LayoutConfig);
1740
- addItem(child: Container): this;
1741
- removeItem(child: Container): this;
1742
- clearItems(): this;
1743
- updateViewport(width: number, height: number): void;
1744
- }
1745
- ```
681
+ **Handled messages:** `GAME_READY`, `PLAY_REQUEST`, `PLAY_RESULT_ACK`, `GET_BALANCE`, `GET_STATE`, `OPEN_DEPOSIT`.
1746
682
 
1747
- ### ScrollContainer
683
+ > With the Vite plugin (`devBridge: true`), DevBridge is injected automatically before your app entry point.
1748
684
 
1749
- > Extends `@pixi/ui` ScrollBox — inherits touch/drag scrolling, mouse wheel, inertia, and dynamic rendering.
685
+ ---
1750
686
 
1751
- ```typescript
1752
- class ScrollContainer extends ScrollBox {
1753
- get scrollPosition(): { x: number; y: number };
687
+ ## React Integration
1754
688
 
1755
- constructor(config: ScrollContainerConfig);
1756
- setContent(content: Container): void;
1757
- addItem(...items: Container[]): Container;
1758
- scrollToItem(index: number): void;
1759
- }
1760
- ```
689
+ Built-in React reconciler for PixiJS. No `@pixi/react` needed — renders React trees directly into PixiJS Containers.
1761
690
 
1762
- ### ReactScene
691
+ ```tsx
692
+ import { ReactScene, extendPixiElements, useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
1763
693
 
1764
- > Requires `react`, `react-dom`, `react-reconciler` as peer dependencies.
694
+ extendPixiElements(); // register PixiJS elements for JSX (call once)
1765
695
 
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
696
+ export class GameScene extends ReactScene {
697
+ render() { return <GameRoot />; }
1776
698
  }
1777
- ```
1778
699
 
1779
- ### createPixiRoot
700
+ function GameRoot() {
701
+ const { width, height } = useViewport();
702
+ const balance = useBalance();
703
+ const sdk = useSDK();
1780
704
 
1781
- ```typescript
1782
- interface PixiRoot {
1783
- render(element: ReactElement): void;
1784
- unmount(): void;
705
+ return (
706
+ <container>
707
+ <text text={`Balance: $${balance.toFixed(2)}`} style={{ fontSize: 32, fill: 0xffffff }} />
708
+ <text text="SPIN" style={{ fontSize: 48, fill: 0xffd700 }}
709
+ eventMode="static" cursor="pointer"
710
+ onClick={async () => {
711
+ const result = await sdk?.play({ action: 'spin', bet: 1 });
712
+ sdk?.playAck(result!);
713
+ }}
714
+ />
715
+ </container>
716
+ );
1785
717
  }
1786
-
1787
- function createPixiRoot(container: Container): PixiRoot;
1788
718
  ```
1789
719
 
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
-
1807
- ### Button
1808
-
1809
- > Extends `@pixi/ui` FancyButton — per-state views, press animation, and text.
1810
-
1811
- ```typescript
1812
- class Button extends FancyButton {
1813
- get disabled(): boolean;
1814
-
1815
- constructor(config?: ButtonConfig);
1816
- enable(): void;
1817
- disable(): void;
1818
-
1819
- // Inherited from FancyButton
1820
- onPress: Signal; // btn.onPress.connect(() => { … })
1821
- enabled: boolean;
1822
- }
1823
- ```
720
+ ### Hooks
1824
721
 
1825
- ### Panel
722
+ | Hook | Returns | Description |
723
+ | --- | --- | --- |
724
+ | `useEngine()` | `EngineContextValue` | Full engine context |
725
+ | `useSDK()` | `CasinoGameSDK \| null` | SDK instance |
726
+ | `useAudio()` | `AudioManager` | Audio manager |
727
+ | `useInput()` | `InputManager` | Input manager |
728
+ | `useViewport()` | `{ width, height, scale, isPortrait }` | Reactive viewport |
729
+ | `useBalance()` | `number` | Reactive balance |
730
+ | `useSession()` | `SessionData \| null` | Current session |
731
+ | `useGameConfig<T>()` | `T \| null` | Game config from SDK |
1826
732
 
1827
- > Extends `@pixi/layout/components` LayoutContainer — flex-layout background panel with optional 9-slice texture.
733
+ ### Element Registration
1828
734
 
1829
735
  ```typescript
1830
- class Panel extends LayoutContainer {
1831
- get content(): Container; // children added here participate in flexbox layout
736
+ import { extendPixiElements, extendLayoutElements, extend } from '@energy8platform/game-engine/react';
1832
737
 
1833
- constructor(config?: PanelConfig);
1834
- setSize(width: number, height: number): void;
1835
- }
738
+ extendPixiElements(); // Container, Sprite, Text, Graphics, etc.
739
+ extendLayoutElements(await import('@pixi/layout/components')); // @pixi/layout (if installed)
740
+ extend({ Button, Panel }); // custom classes
1836
741
  ```
1837
742
 
1838
- ### ProgressBar
743
+ After registration, use as lowercase JSX: `<container>`, `<sprite>`, `<text>`, `<graphics>`, `<button>`.
1839
744
 
1840
- > Wraps `@pixi/ui` ProgressBar optional smooth animated fill via per-frame `update()`.
745
+ **Props:** Regular props set directly (`alpha`, `visible`, `scale`). Nested via dash: `position-x`, `scale-y`. Events: `onClick` → `onclick`, `onPointerDown` → `onpointerdown`.
1841
746
 
1842
- ```typescript
1843
- class ProgressBar extends Container {
1844
- get progress(): number;
1845
- set progress(value: number); // 0..1
747
+ > React is entirely optional. Imperative (`Scene`) and React (`ReactScene`) scenes can coexist.
1846
748
 
1847
- constructor(config?: ProgressBarConfig);
1848
- update(dt: number): void; // call each frame when animated: true
1849
- }
1850
- ```
749
+ ---
1851
750
 
1852
- ### Toast
751
+ ## Debug
1853
752
 
1854
- > Lightweight toast using `Graphics` for the background. Supports info / success / warning / error types.
753
+ When `debug: true` in config, an FPS overlay (avg FPS, min FPS, frame time) is shown automatically.
1855
754
 
1856
755
  ```typescript
1857
- class Toast extends Container {
1858
- constructor(config?: ToastConfig);
1859
- async show(message: string, type?: ToastType, viewWidth?: number, viewHeight?: number): Promise<void>;
1860
- async dismiss(): Promise<void>;
1861
- }
756
+ import { FPSOverlay } from '@energy8platform/game-engine/debug';
757
+ const fps = new FPSOverlay(game.app);
758
+ fps.show();
759
+ fps.toggle();
760
+ fps.hide();
1862
761
  ```
1863
762
 
1864
- ### SpriteAnimation
1865
-
1866
- ```typescript
1867
- class SpriteAnimation {
1868
- static create(textures: Texture[], config?: SpriteAnimationConfig): AnimatedSprite;
1869
- static fromSpritesheet(sheet: Spritesheet, prefix: string, config?: SpriteAnimationConfig): AnimatedSprite;
1870
- static fromRange(sheet: Spritesheet, pattern: string, start: number, end: number, config?: SpriteAnimationConfig): AnimatedSprite;
1871
- static fromAliases(aliases: string[], config?: SpriteAnimationConfig): AnimatedSprite;
1872
- static playOnce(textures: Texture[], config?: SpriteAnimationConfig): { sprite: AnimatedSprite; finished: Promise<void> };
1873
- static getTexturesByPrefix(sheet: Spritesheet, prefix: string): Texture[];
1874
- }
1875
- ```
763
+ ---
1876
764
 
1877
- ### EventEmitter
765
+ ## API Types
1878
766
 
1879
- ```typescript
1880
- class EventEmitter<TEvents extends {}> {
1881
- on<K>(event: K, handler: (data: TEvents[K]) => void): this;
1882
- once<K>(event: K, handler: (data: TEvents[K]) => void): this;
1883
- off<K>(event: K, handler: (data: TEvents[K]) => void): this;
1884
- // Void events can be emitted without a data argument
1885
- emit<K>(event: K, ...args): void;
1886
- removeAllListeners(event?: keyof TEvents): this;
1887
- }
1888
- ```
767
+ All API types are fully documented in TypeScript. Explore `src/types.ts` for config interfaces, enums, and re-exported SDK types. Individual class APIs are visible via IDE autocompletion or by reading the source modules directly.
1889
768
 
1890
769
  ---
1891
770