@energy8platform/game-engine 0.9.2 → 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 +293 -1414
- package/bin/simulate.ts +75 -0
- package/dist/debug.cjs.js +892 -18
- package/dist/debug.cjs.js.map +1 -1
- package/dist/debug.d.ts +64 -0
- package/dist/debug.esm.js +892 -18
- package/dist/debug.esm.js.map +1 -1
- package/dist/index.cjs.js +899 -18
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +239 -2
- package/dist/index.esm.js +893 -19
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +1000 -0
- package/dist/lua.cjs.js.map +1 -0
- package/dist/lua.d.ts +296 -0
- package/dist/lua.esm.js +990 -0
- package/dist/lua.esm.js.map +1 -0
- package/dist/vite.cjs.js +26 -0
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +26 -0
- package/dist/vite.esm.js.map +1 -1
- package/package.json +29 -13
- package/src/debug/DevBridge.ts +73 -22
- package/src/index.ts +17 -0
- package/src/lua/ActionRouter.ts +132 -0
- package/src/lua/LuaEngine.ts +322 -0
- package/src/lua/LuaEngineAPI.ts +305 -0
- package/src/lua/PersistentState.ts +80 -0
- package/src/lua/SessionManager.ts +178 -0
- package/src/lua/SimulationRunner.ts +195 -0
- package/src/lua/index.ts +22 -0
- package/src/lua/types.ts +132 -0
- package/src/vite/index.ts +30 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @energy8platform/game-engine
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
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
|
-
#
|
|
60
|
-
npm install @pixi/ui @pixi/layout yoga-layout
|
|
61
|
-
|
|
62
|
-
#
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 —
|
|
135
|
-
| `@pixi/layout` | `^3.2.0` | Optional —
|
|
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 —
|
|
138
|
-
| `@esotericsoftware/spine-pixi-v8` | `~4.2.0` | Optional —
|
|
139
|
-
| `react` | `>=18.0.0` | Optional —
|
|
140
|
-
| `react-
|
|
141
|
-
| `
|
|
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';
|
|
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** —
|
|
180
|
-
2. **PixiJS initialization** — creates `Application`, initializes `ResizeObserver
|
|
181
|
-
3. **SDK handshake** — connects to
|
|
182
|
-
4. **Canvas Loading Screen** —
|
|
183
|
-
5. **Asset loading** — remaining bundles
|
|
184
|
-
6. **Tap-to-start** — optional
|
|
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` |
|
|
199
|
-
| `orientation` | `Orientation` | `ANY` |
|
|
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` | — |
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
//
|
|
307
|
-
await scenes.
|
|
308
|
-
|
|
309
|
-
//
|
|
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,
|
|
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`
|
|
212
|
+
`GameApplication` events:
|
|
341
213
|
|
|
342
214
|
| Event | Payload | When |
|
|
343
215
|
| --- | --- | --- |
|
|
344
|
-
| `initialized` | `void` | Engine initialized, PixiJS and SDK
|
|
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 (
|
|
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
|
-
|
|
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
|
|
233
|
+
const manifest = {
|
|
379
234
|
bundles: [
|
|
380
|
-
{
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
460
|
-
audio.toggleMute(); // returns new state
|
|
268
|
+
audio.toggleMute();
|
|
461
269
|
|
|
462
|
-
//
|
|
463
|
-
audio.duckMusic(0.2);
|
|
464
|
-
audio.unduckMusic();
|
|
270
|
+
// Ducking during big win animations
|
|
271
|
+
audio.duckMusic(0.2);
|
|
272
|
+
audio.unduckMusic();
|
|
465
273
|
```
|
|
466
274
|
|
|
467
|
-
|
|
275
|
+
**Categories:** `music`, `sfx`, `ui`, `ambient` — each with independent volume and mute.
|
|
468
276
|
|
|
469
|
-
|
|
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` |
|
|
482
|
-
| `FILL` | Fills
|
|
483
|
-
| `STRETCH` | Stretches to fill
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
balance:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
666
|
-
const
|
|
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
|
|
679
|
-
const { sprite, finished } = SpriteAnimation.playOnce(
|
|
680
|
-
|
|
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
|
-
|
|
385
|
+
Requires `@esotericsoftware/spine-pixi-v8`:
|
|
699
386
|
|
|
700
387
|
```typescript
|
|
701
388
|
import { SpineHelper } from '@energy8platform/game-engine';
|
|
702
389
|
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
>
|
|
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
|
-
>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
894
|
-
spinBtn.
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
### Label
|
|
456
|
+
### Other UI Components
|
|
915
457
|
|
|
458
|
+
**Label** — styled text wrapper:
|
|
916
459
|
```typescript
|
|
917
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
Displays player balance with formatting:
|
|
928
|
-
|
|
463
|
+
**BalanceDisplay** — animated currency display:
|
|
929
464
|
```typescript
|
|
930
|
-
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
Animated win amount display with countup:
|
|
944
|
-
|
|
469
|
+
**WinDisplay** — countup win animation:
|
|
945
470
|
```typescript
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
Full-screen overlay dialog:
|
|
1010
|
-
|
|
487
|
+
**Modal** — full-screen overlay dialog:
|
|
1011
488
|
```typescript
|
|
1012
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1370
|
-
input.on('
|
|
1371
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
1418
|
-
devBridgeConfig: './dev.config', //
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
+
### DevBridge Integration (recommended)
|
|
1447
545
|
|
|
1448
546
|
```typescript
|
|
1449
547
|
// dev.config.ts
|
|
1450
|
-
import
|
|
548
|
+
import luaScript from './game.lua?raw';
|
|
1451
549
|
|
|
1452
550
|
export default {
|
|
1453
|
-
balance:
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
debug: true,
|
|
1483
|
-
gameConfig: {
|
|
553
|
+
luaScript,
|
|
554
|
+
gameDefinition: {
|
|
1484
555
|
id: 'my-slot',
|
|
1485
|
-
type: '
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
-
|
|
583
|
+
### Platform API (`engine.*` in Lua)
|
|
1534
584
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
598
|
+
Run the same Lua script from `dev.config.ts` through millions of iterations to verify math:
|
|
1545
599
|
|
|
1546
|
-
|
|
600
|
+
```bash
|
|
601
|
+
# Regular spins (1M iterations, default)
|
|
602
|
+
npx game-engine-simulate
|
|
1547
603
|
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
607
|
+
# Ante bet
|
|
608
|
+
npx game-engine-simulate --params '{"ante_bet":true}'
|
|
1576
609
|
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1664
|
-
|
|
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
|
-
|
|
654
|
+
---
|
|
1683
655
|
|
|
1684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
683
|
+
> With the Vite plugin (`devBridge: true`), DevBridge is injected automatically before your app entry point.
|
|
1748
684
|
|
|
1749
|
-
|
|
685
|
+
---
|
|
1750
686
|
|
|
1751
|
-
|
|
1752
|
-
class ScrollContainer extends ScrollBox {
|
|
1753
|
-
get scrollPosition(): { x: number; y: number };
|
|
687
|
+
## React Integration
|
|
1754
688
|
|
|
1755
|
-
|
|
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
|
-
|
|
691
|
+
```tsx
|
|
692
|
+
import { ReactScene, extendPixiElements, useSDK, useViewport, useBalance } from '@energy8platform/game-engine/react';
|
|
1763
693
|
|
|
1764
|
-
|
|
694
|
+
extendPixiElements(); // register PixiJS elements for JSX (call once)
|
|
1765
695
|
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
700
|
+
function GameRoot() {
|
|
701
|
+
const { width, height } = useViewport();
|
|
702
|
+
const balance = useBalance();
|
|
703
|
+
const sdk = useSDK();
|
|
1780
704
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
733
|
+
### Element Registration
|
|
1828
734
|
|
|
1829
735
|
```typescript
|
|
1830
|
-
|
|
1831
|
-
get content(): Container; // children added here participate in flexbox layout
|
|
736
|
+
import { extendPixiElements, extendLayoutElements, extend } from '@energy8platform/game-engine/react';
|
|
1832
737
|
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
-
|
|
743
|
+
After registration, use as lowercase JSX: `<container>`, `<sprite>`, `<text>`, `<graphics>`, `<button>`.
|
|
1839
744
|
|
|
1840
|
-
|
|
745
|
+
**Props:** Regular props set directly (`alpha`, `visible`, `scale`). Nested via dash: `position-x`, `scale-y`. Events: `onClick` → `onclick`, `onPointerDown` → `onpointerdown`.
|
|
1841
746
|
|
|
1842
|
-
|
|
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
|
-
|
|
1848
|
-
update(dt: number): void; // call each frame when animated: true
|
|
1849
|
-
}
|
|
1850
|
-
```
|
|
749
|
+
---
|
|
1851
750
|
|
|
1852
|
-
|
|
751
|
+
## Debug
|
|
1853
752
|
|
|
1854
|
-
|
|
753
|
+
When `debug: true` in config, an FPS overlay (avg FPS, min FPS, frame time) is shown automatically.
|
|
1855
754
|
|
|
1856
755
|
```typescript
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
765
|
+
## API Types
|
|
1878
766
|
|
|
1879
|
-
|
|
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
|
|