@energy8platform/game-engine 0.1.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.
Files changed (80) hide show
  1. package/README.md +1134 -0
  2. package/dist/animation.cjs.js +505 -0
  3. package/dist/animation.cjs.js.map +1 -0
  4. package/dist/animation.d.ts +235 -0
  5. package/dist/animation.esm.js +500 -0
  6. package/dist/animation.esm.js.map +1 -0
  7. package/dist/assets.cjs.js +148 -0
  8. package/dist/assets.cjs.js.map +1 -0
  9. package/dist/assets.d.ts +97 -0
  10. package/dist/assets.esm.js +146 -0
  11. package/dist/assets.esm.js.map +1 -0
  12. package/dist/audio.cjs.js +345 -0
  13. package/dist/audio.cjs.js.map +1 -0
  14. package/dist/audio.d.ts +135 -0
  15. package/dist/audio.esm.js +343 -0
  16. package/dist/audio.esm.js.map +1 -0
  17. package/dist/core.cjs.js +1832 -0
  18. package/dist/core.cjs.js.map +1 -0
  19. package/dist/core.d.ts +633 -0
  20. package/dist/core.esm.js +1827 -0
  21. package/dist/core.esm.js.map +1 -0
  22. package/dist/debug.cjs.js +298 -0
  23. package/dist/debug.cjs.js.map +1 -0
  24. package/dist/debug.d.ts +114 -0
  25. package/dist/debug.esm.js +295 -0
  26. package/dist/debug.esm.js.map +1 -0
  27. package/dist/index.cjs.js +3623 -0
  28. package/dist/index.cjs.js.map +1 -0
  29. package/dist/index.d.ts +1607 -0
  30. package/dist/index.esm.js +3598 -0
  31. package/dist/index.esm.js.map +1 -0
  32. package/dist/ui.cjs.js +1081 -0
  33. package/dist/ui.cjs.js.map +1 -0
  34. package/dist/ui.d.ts +387 -0
  35. package/dist/ui.esm.js +1072 -0
  36. package/dist/ui.esm.js.map +1 -0
  37. package/dist/vite.cjs.js +125 -0
  38. package/dist/vite.cjs.js.map +1 -0
  39. package/dist/vite.d.ts +42 -0
  40. package/dist/vite.esm.js +122 -0
  41. package/dist/vite.esm.js.map +1 -0
  42. package/package.json +107 -0
  43. package/src/animation/Easing.ts +116 -0
  44. package/src/animation/SpineHelper.ts +162 -0
  45. package/src/animation/Timeline.ts +138 -0
  46. package/src/animation/Tween.ts +225 -0
  47. package/src/animation/index.ts +4 -0
  48. package/src/assets/AssetManager.ts +174 -0
  49. package/src/assets/index.ts +2 -0
  50. package/src/audio/AudioManager.ts +366 -0
  51. package/src/audio/index.ts +1 -0
  52. package/src/core/EventEmitter.ts +47 -0
  53. package/src/core/GameApplication.ts +306 -0
  54. package/src/core/Scene.ts +48 -0
  55. package/src/core/SceneManager.ts +258 -0
  56. package/src/core/index.ts +4 -0
  57. package/src/debug/DevBridge.ts +259 -0
  58. package/src/debug/FPSOverlay.ts +102 -0
  59. package/src/debug/index.ts +3 -0
  60. package/src/index.ts +71 -0
  61. package/src/input/InputManager.ts +171 -0
  62. package/src/input/index.ts +1 -0
  63. package/src/loading/CSSPreloader.ts +155 -0
  64. package/src/loading/LoadingScene.ts +356 -0
  65. package/src/loading/index.ts +2 -0
  66. package/src/state/StateMachine.ts +228 -0
  67. package/src/state/index.ts +1 -0
  68. package/src/types.ts +218 -0
  69. package/src/ui/BalanceDisplay.ts +155 -0
  70. package/src/ui/Button.ts +199 -0
  71. package/src/ui/Label.ts +111 -0
  72. package/src/ui/Modal.ts +134 -0
  73. package/src/ui/Panel.ts +125 -0
  74. package/src/ui/ProgressBar.ts +121 -0
  75. package/src/ui/Toast.ts +124 -0
  76. package/src/ui/WinDisplay.ts +133 -0
  77. package/src/ui/index.ts +16 -0
  78. package/src/viewport/ViewportManager.ts +241 -0
  79. package/src/viewport/index.ts +1 -0
  80. package/src/vite/index.ts +153 -0
package/README.md ADDED
@@ -0,0 +1,1134 @@
1
+ # @energy8platform/game-engine
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.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Quick Start](#quick-start)
10
+ - [Installation](#installation)
11
+ - [Architecture](#architecture)
12
+ - [Configuration](#configuration)
13
+ - [Scenes](#scenes)
14
+ - [Lifecycle](#lifecycle)
15
+ - [Assets](#assets)
16
+ - [Audio](#audio)
17
+ - [Viewport & Scaling](#viewport--scaling)
18
+ - [State Machine](#state-machine)
19
+ - [Animation](#animation)
20
+ - [UI Components](#ui-components)
21
+ - [Input](#input)
22
+ - [Vite Configuration](#vite-configuration)
23
+ - [DevBridge](#devbridge)
24
+ - [Debug](#debug)
25
+ - [API Reference](#api-reference)
26
+ - [License](#license)
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # Create a new project
34
+ mkdir my-game && cd my-game
35
+ npm init -y
36
+
37
+ # Install dependencies
38
+ npm install pixi.js @energy8platform/game-sdk @energy8platform/game-engine
39
+
40
+ # (Optional) install spine and audio support
41
+ npm install @pixi/sound @esotericsoftware/spine-pixi-v8
42
+ ```
43
+
44
+ Create the entry point:
45
+
46
+ ```typescript
47
+ // src/main.ts
48
+ import { GameApplication, ScaleMode } from '@energy8platform/game-engine';
49
+ import { GameScene } from './scenes/GameScene';
50
+
51
+ async function bootstrap() {
52
+ const game = new GameApplication({
53
+ container: '#game',
54
+ designWidth: 1920,
55
+ designHeight: 1080,
56
+ scaleMode: ScaleMode.FIT,
57
+ loading: {
58
+ backgroundColor: 0x0a0a1a,
59
+ backgroundGradient:
60
+ 'linear-gradient(135deg, #0a0a1a 0%, #1a1a3e 50%, #0a0a1a 100%)',
61
+ showPercentage: true,
62
+ tapToStart: true,
63
+ tapToStartText: 'TAP TO PLAY',
64
+ minDisplayTime: 2000,
65
+ },
66
+ manifest: {
67
+ bundles: [
68
+ { name: 'preload', assets: [] },
69
+ {
70
+ name: 'game',
71
+ assets: [
72
+ { alias: 'background', src: 'background.png' },
73
+ { alias: 'symbols', src: 'symbols.json' },
74
+ ],
75
+ },
76
+ ],
77
+ },
78
+ audio: {
79
+ music: 0.5,
80
+ sfx: 1.0,
81
+ persist: true,
82
+ },
83
+ debug: true,
84
+ });
85
+
86
+ game.scenes.register('game', GameScene as any);
87
+
88
+ game.on('initialized', () => console.log('Engine initialized'));
89
+ game.on('loaded', () => console.log('Assets loaded'));
90
+ game.on('started', () => console.log('Game started'));
91
+ game.on('error', (err) => console.error('Error:', err));
92
+
93
+ await game.start('game');
94
+ }
95
+
96
+ bootstrap();
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Installation
102
+
103
+ ### Peer Dependencies
104
+
105
+ | Package | Version | Required |
106
+ | --- | --- | --- |
107
+ | `pixi.js` | `^8.16.0` | Yes |
108
+ | `@energy8platform/game-sdk` | `^2.5.0` | Yes |
109
+ | `@pixi/sound` | `^6.0.0` | Optional — for audio |
110
+ | `@esotericsoftware/spine-pixi-v8` | `~4.2.0` | Optional — for Spine animations |
111
+
112
+ ### Sub-path Exports
113
+
114
+ The package exposes granular entry points for tree-shaking:
115
+
116
+ ```typescript
117
+ import { GameApplication } from '@energy8platform/game-engine'; // full bundle
118
+ import { Scene, SceneManager } from '@energy8platform/game-engine/core';
119
+ import { AssetManager } from '@energy8platform/game-engine/assets';
120
+ import { AudioManager } from '@energy8platform/game-engine/audio';
121
+ import { Button, Label, Modal } from '@energy8platform/game-engine/ui';
122
+ import { Tween, Timeline, Easing } from '@energy8platform/game-engine/animation';
123
+ import { DevBridge, FPSOverlay } from '@energy8platform/game-engine/debug';
124
+ import { defineGameConfig } from '@energy8platform/game-engine/vite';
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Architecture
130
+
131
+ ```
132
+ ┌──────────────────────────────────────────────────────┐
133
+ │ GameApplication │
134
+ │ (orchestrates lifecycle, holds all sub-systems) │
135
+ ├──────────┬───────────┬───────────┬───────────────────┤
136
+ │ Viewport │ Scenes │ Assets │ Audio │ Input │
137
+ │ Manager │ Manager │ Manager │ Manager │ Manager │
138
+ ├──────────┴───────────┴───────────┴─────────┴─────────┤
139
+ │ PixiJS v8 Application │
140
+ ├──────────────────────────────────────────────────────┤
141
+ │ @energy8platform/game-sdk │
142
+ └──────────────────────────────────────────────────────┘
143
+ ```
144
+
145
+ ### Boot Sequence
146
+
147
+ 1. **CSS Preloader** — an instant HTML/CSS overlay shown while PixiJS initializes (inline SVG logo with a shimmer animation and "Loading..." text).
148
+ 2. **PixiJS initialization** — creates `Application`, initializes `ResizeObserver`.
149
+ 3. **SDK handshake** — connects to the casino host (or DevBridge in dev mode).
150
+ 4. **Canvas Loading Screen** — `LoadingScene` displays the SVG logo with an animated progress bar, `preload` bundle is loaded first.
151
+ 5. **Asset loading** — remaining bundles are loaded; the progress bar fills in real time.
152
+ 6. **Tap-to-start** — optional screen shown after loading (required on mobile for audio unlock).
153
+ 7. **First scene** — transitions to the registered first scene.
154
+
155
+ ---
156
+
157
+ ## Configuration
158
+
159
+ ### `GameApplicationConfig`
160
+
161
+ | Property | Type | Default | Description |
162
+ | --- | --- | --- | --- |
163
+ | `container` | `HTMLElement \| string` | `document.body` | Container element or CSS selector |
164
+ | `designWidth` | `number` | `1920` | Reference design width |
165
+ | `designHeight` | `number` | `1080` | Reference design height |
166
+ | `scaleMode` | `ScaleMode` | `FIT` | Scaling strategy |
167
+ | `orientation` | `Orientation` | `ANY` | Preferred orientation |
168
+ | `loading` | `LoadingScreenConfig` | — | Loading screen options |
169
+ | `manifest` | `AssetManifest` | — | Asset manifest |
170
+ | `audio` | `AudioConfig` | — | Audio configuration |
171
+ | `sdk` | `object \| false` | — | SDK options; `false` to disable |
172
+ | `pixi` | `Partial<ApplicationOptions>` | — | PixiJS pass-through options |
173
+ | `debug` | `boolean` | `false` | Enable FPS overlay |
174
+
175
+ ### `LoadingScreenConfig`
176
+
177
+ | Property | Type | Default | Description |
178
+ | --- | --- | --- | --- |
179
+ | `backgroundColor` | `number \| string` | `0x0a0a1a` | Background color |
180
+ | `backgroundGradient` | `string` | — | CSS gradient for the preloader background |
181
+ | `logoAsset` | `string` | — | Logo texture alias from `preload` bundle |
182
+ | `logoScale` | `number` | `1` | Logo scale factor |
183
+ | `showPercentage` | `boolean` | `true` | Show loading percentage text |
184
+ | `progressTextFormatter` | `(progress: number) => string` | — | Custom progress text formatter |
185
+ | `tapToStart` | `boolean` | `false` | Show "Tap to start" overlay |
186
+ | `tapToStartText` | `string` | `'TAP TO START'` | Tap-to-start label |
187
+ | `minDisplayTime` | `number` | `0` | Minimum display time (ms) |
188
+ | `cssPreloaderHTML` | `string` | — | Custom HTML for the CSS preloader |
189
+
190
+ ### `AudioConfig`
191
+
192
+ | Property | Type | Default | Description |
193
+ | --- | --- | --- | --- |
194
+ | `music` | `number` | `1` | Default music volume (0–1) |
195
+ | `sfx` | `number` | `1` | Default SFX volume |
196
+ | `ui` | `number` | `1` | Default UI sounds volume |
197
+ | `ambient` | `number` | `1` | Default ambient volume |
198
+ | `persist` | `boolean` | `false` | Save mute state to localStorage |
199
+ | `storageKey` | `string` | `'ge-audio'` | localStorage key prefix |
200
+
201
+ ### Enums
202
+
203
+ ```typescript
204
+ enum ScaleMode {
205
+ FIT = 'FIT', // Letterbox/pillarbox — preserves aspect ratio
206
+ FILL = 'FILL', // Fill container, crop edges
207
+ STRETCH = 'STRETCH' // Stretch to fill (distorts)
208
+ }
209
+
210
+ enum Orientation {
211
+ LANDSCAPE = 'landscape',
212
+ PORTRAIT = 'portrait',
213
+ ANY = 'any'
214
+ }
215
+
216
+ enum TransitionType {
217
+ NONE = 'none',
218
+ FADE = 'fade',
219
+ SLIDE_LEFT = 'slide-left',
220
+ SLIDE_RIGHT = 'slide-right'
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Scenes
227
+
228
+ All game screens are scenes. Extend the base `Scene` class and override lifecycle hooks:
229
+
230
+ ```typescript
231
+ import { Scene } from '@energy8platform/game-engine';
232
+ import { Sprite, Assets } from 'pixi.js';
233
+
234
+ export class GameScene extends Scene {
235
+ async onEnter(data?: unknown) {
236
+ const bg = new Sprite(Assets.get('background'));
237
+ this.container.addChild(bg);
238
+ }
239
+
240
+ onUpdate(dt: number) {
241
+ // Called every frame (dt = delta time from PixiJS ticker)
242
+ }
243
+
244
+ onResize(width: number, height: number) {
245
+ // Called on viewport resize — use for responsive layout
246
+ }
247
+
248
+ async onExit() {
249
+ // Cleanup before leaving the scene
250
+ }
251
+
252
+ onDestroy() {
253
+ // Final cleanup when the scene is removed from the stack
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### Scene Navigation
259
+
260
+ ```typescript
261
+ const scenes = game.scenes;
262
+
263
+ // Register scenes
264
+ scenes.register('menu', MenuScene as any);
265
+ scenes.register('game', GameScene as any);
266
+ scenes.register('bonus', BonusScene as any);
267
+
268
+ // Navigate (replaces entire stack)
269
+ await scenes.goto('game');
270
+
271
+ // Push overlay/modal (previous scene stays rendered)
272
+ await scenes.push('bonus', { multiplier: 3 });
273
+
274
+ // Pop back
275
+ await scenes.pop();
276
+
277
+ // Replace top scene
278
+ await scenes.replace('game');
279
+ ```
280
+
281
+ ### Transitions
282
+
283
+ ```typescript
284
+ import { TransitionType } from '@energy8platform/game-engine';
285
+
286
+ await scenes.goto('game', undefined, {
287
+ type: TransitionType.FADE,
288
+ duration: 500, // ms
289
+ });
290
+
291
+ await scenes.push('bonus', { data: 42 }, {
292
+ type: TransitionType.SLIDE_LEFT,
293
+ duration: 300,
294
+ easing: Easing.easeOutCubic,
295
+ });
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Lifecycle
301
+
302
+ `GameApplication` emits the following events:
303
+
304
+ | Event | Payload | When |
305
+ | --- | --- | --- |
306
+ | `initialized` | `void` | Engine initialized, PixiJS and SDK are ready |
307
+ | `loaded` | `void` | All asset bundles loaded |
308
+ | `started` | `void` | First scene entered, game loop running |
309
+ | `resize` | `{ width, height }` | Viewport resized |
310
+ | `orientationChange` | `Orientation` | Device orientation changed |
311
+ | `sceneChange` | `{ from, to }` | Scene transition completed |
312
+ | `error` | `Error` | An error occurred |
313
+ | `destroyed` | `void` | Engine destroyed |
314
+
315
+ ```typescript
316
+ game.on('resize', ({ width, height }) => {
317
+ console.log(`New size: ${width}x${height}`);
318
+ });
319
+
320
+ game.once('started', () => {
321
+ // Runs once after the first scene starts
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Assets
328
+
329
+ ### Asset Manifest
330
+
331
+ Assets are organized in named **bundles**:
332
+
333
+ ```typescript
334
+ const manifest: AssetManifest = {
335
+ bundles: [
336
+ {
337
+ name: 'preload',
338
+ assets: [
339
+ { alias: 'logo', src: 'logo.png' },
340
+ ],
341
+ },
342
+ {
343
+ name: 'game',
344
+ assets: [
345
+ { alias: 'background', src: 'bg.webp' },
346
+ { alias: 'symbols', src: 'symbols.json' },
347
+ { alias: 'win-sound', src: 'sounds/win.mp3' },
348
+ ],
349
+ },
350
+ {
351
+ name: 'bonus',
352
+ assets: [
353
+ { alias: 'bonus-bg', src: 'bonus/bg.webp' },
354
+ ],
355
+ },
356
+ ],
357
+ };
358
+ ```
359
+
360
+ The `preload` bundle is loaded first (before the progress bar fills). All other bundles load together with a combined progress indicator.
361
+
362
+ ### AssetManager API
363
+
364
+ ```typescript
365
+ const assets = game.assets;
366
+
367
+ // Load on demand
368
+ await assets.loadBundle('bonus', (progress) => {
369
+ console.log(`${Math.round(progress * 100)}%`);
370
+ });
371
+
372
+ // Synchronous cache access
373
+ const texture = assets.get<Texture>('background');
374
+
375
+ // Background preloading (low priority)
376
+ await assets.backgroundLoad('bonus');
377
+
378
+ // Unload to free memory
379
+ await assets.unloadBundle('bonus');
380
+
381
+ // Check state
382
+ assets.isBundleLoaded('game'); // true
383
+ assets.getBundleNames(); // ['preload', 'game', 'bonus']
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Audio
389
+
390
+ `AudioManager` wraps `@pixi/sound` with category-based volume management. If `@pixi/sound` is not installed, all methods work silently as no-ops.
391
+
392
+ ### Audio Categories
393
+
394
+ Four categories with independent volume and mute controls: `music`, `sfx`, `ui`, `ambient`.
395
+
396
+ ```typescript
397
+ const audio = game.audio;
398
+
399
+ // Play a sound effect
400
+ audio.play('click', 'ui');
401
+ audio.play('coin-drop', 'sfx', { volume: 0.8 });
402
+
403
+ // Music with crossfade
404
+ audio.playMusic('main-theme', 1000); // 1s crossfade
405
+ audio.stopMusic();
406
+
407
+ // Volume control
408
+ audio.setVolume('music', 0.3);
409
+ audio.muteCategory('sfx');
410
+ audio.unmuteCategory('sfx');
411
+ audio.toggleCategory('sfx'); // returns new state
412
+
413
+ // Global mute
414
+ audio.muteAll();
415
+ audio.unmuteAll();
416
+ audio.toggleMute(); // returns new state
417
+
418
+ // Music ducking (e.g., during a big win animation)
419
+ audio.duckMusic(0.2); // reduce to 20%
420
+ audio.unduckMusic(); // restore
421
+ ```
422
+
423
+ ### Mobile Audio Unlock
424
+
425
+ 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.
426
+
427
+ ---
428
+
429
+ ## Viewport & Scaling
430
+
431
+ `ViewportManager` handles responsive scaling using `ResizeObserver` with debouncing.
432
+
433
+ ### Scale Modes
434
+
435
+ | Mode | Behavior |
436
+ | --- | --- |
437
+ | `FIT` | Fits the entire design area inside the container. Adds letterbox (horizontal bars) or pillarbox (vertical bars) as needed. **Industry standard for iGaming.** |
438
+ | `FILL` | Fills the entire container, cropping edges. No bars, but some content may be hidden. |
439
+ | `STRETCH` | Stretches to fill the container. Distorts aspect ratio. Not recommended. |
440
+
441
+ ```typescript
442
+ const vp = game.viewport;
443
+
444
+ // Current dimensions
445
+ console.log(vp.width, vp.height, vp.scale);
446
+ console.log(vp.orientation); // 'landscape' | 'portrait'
447
+
448
+ // Reference design size
449
+ console.log(vp.designWidth, vp.designHeight);
450
+
451
+ // Force re-calculation
452
+ vp.refresh();
453
+
454
+ // Listen for changes
455
+ game.on('resize', ({ width, height }) => {
456
+ // Respond to viewport changes
457
+ });
458
+ ```
459
+
460
+ ---
461
+
462
+ ## State Machine
463
+
464
+ `StateMachine` is a generic finite state machine with typed context, async hooks, guards, and per-frame updates.
465
+
466
+ ```typescript
467
+ import { StateMachine } from '@energy8platform/game-engine';
468
+
469
+ interface GameContext {
470
+ balance: number;
471
+ bet: number;
472
+ lastWin: number;
473
+ }
474
+
475
+ const fsm = new StateMachine<GameContext>({
476
+ balance: 10000,
477
+ bet: 100,
478
+ lastWin: 0,
479
+ });
480
+
481
+ fsm.addState('idle', {
482
+ enter: (ctx) => console.log('Waiting for player...'),
483
+ update: (ctx, dt) => { /* per-frame logic */ },
484
+ });
485
+
486
+ fsm.addState('spinning', {
487
+ enter: async (ctx) => {
488
+ // Play spin animation
489
+ await spinReels();
490
+ // Auto-transition to result
491
+ await fsm.transition('result');
492
+ },
493
+ });
494
+
495
+ fsm.addState('result', {
496
+ enter: async (ctx) => {
497
+ if (ctx.lastWin > 0) {
498
+ await showWinAnimation(ctx.lastWin);
499
+ }
500
+ await fsm.transition('idle');
501
+ },
502
+ });
503
+
504
+ // Guards
505
+ fsm.addGuard('idle', 'spinning', (ctx) => ctx.balance >= ctx.bet);
506
+
507
+ // Events
508
+ fsm.on('transition', ({ from, to }) => {
509
+ console.log(`${from} → ${to}`);
510
+ });
511
+
512
+ // Start
513
+ await fsm.start('idle');
514
+
515
+ // Trigger transitions
516
+ const success = await fsm.transition('spinning');
517
+ if (!success) {
518
+ console.log('Transition blocked by guard');
519
+ }
520
+
521
+ // Per-frame update (usually called from Scene.onUpdate)
522
+ fsm.update(dt);
523
+ ```
524
+
525
+ ---
526
+
527
+ ## Animation
528
+
529
+ ### Tween
530
+
531
+ `Tween` provides a Promise-based animation system integrated with the PixiJS Ticker:
532
+
533
+ ```typescript
534
+ import { Tween, Easing } from '@energy8platform/game-engine';
535
+
536
+ // Animate to target values
537
+ await Tween.to(sprite, { alpha: 0, y: 100 }, 500, Easing.easeOutCubic);
538
+
539
+ // Animate from starting values to current
540
+ await Tween.from(sprite, { scale: 0 }, 300, Easing.easeOutBack);
541
+
542
+ // Animate between two sets of values
543
+ await Tween.fromTo(sprite, { x: -100 }, { x: 500 }, 1000, Easing.easeInOutQuad);
544
+
545
+ // Wait (useful in timelines or sequences)
546
+ await Tween.delay(1000);
547
+
548
+ // Cancel tweens
549
+ Tween.killTweensOf(sprite);
550
+ Tween.killAll();
551
+
552
+ // Supports nested properties
553
+ await Tween.to(sprite, { 'scale.x': 2, 'position.y': 300 }, 500);
554
+ ```
555
+
556
+ ### Timeline
557
+
558
+ `Timeline` chains sequential and parallel animation steps:
559
+
560
+ ```typescript
561
+ import { Timeline, Tween, Easing } from '@energy8platform/game-engine';
562
+
563
+ const tl = new Timeline();
564
+
565
+ tl.to(title, { alpha: 1, y: 0 }, 500, Easing.easeOutCubic)
566
+ .delay(200)
567
+ .parallel(
568
+ () => Tween.to(btn1, { alpha: 1 }, 300),
569
+ () => Tween.to(btn2, { alpha: 1 }, 300),
570
+ () => Tween.to(btn3, { alpha: 1 }, 300),
571
+ )
572
+ .call(() => console.log('Intro complete!'));
573
+
574
+ await tl.play();
575
+ ```
576
+
577
+ ### Available Easings
578
+
579
+ All 24 easing functions:
580
+
581
+ | Linear | Quad | Cubic | Quart |
582
+ | --- | --- | --- | --- |
583
+ | `linear` | `easeInQuad` | `easeInCubic` | `easeInQuart` |
584
+ | | `easeOutQuad` | `easeOutCubic` | `easeOutQuart` |
585
+ | | `easeInOutQuad` | `easeInOutCubic` | `easeInOutQuart` |
586
+
587
+ | Sine | Expo | Back | Bounce / Elastic |
588
+ | --- | --- | --- | --- |
589
+ | `easeInSine` | `easeInExpo` | `easeInBack` | `easeOutBounce` |
590
+ | `easeOutSine` | `easeOutExpo` | `easeOutBack` | `easeInBounce` |
591
+ | `easeInOutSine` | `easeInOutExpo` | `easeInOutBack` | `easeInOutBounce` |
592
+ | | | | `easeOutElastic` |
593
+ | | | | `easeInElastic` |
594
+
595
+ ### Spine Animations
596
+
597
+ If `@esotericsoftware/spine-pixi-v8` is installed:
598
+
599
+ ```typescript
600
+ import { SpineHelper } from '@energy8platform/game-engine';
601
+
602
+ // Create a Spine instance
603
+ const spine = await SpineHelper.create('character-skel', 'character-atlas', {
604
+ scale: 0.5,
605
+ });
606
+ container.addChild(spine);
607
+
608
+ // Play animation (returns Promise that resolves on completion)
609
+ await SpineHelper.playAnimation(spine, 'idle', true); // loop
610
+
611
+ // Queue animation
612
+ SpineHelper.addAnimation(spine, 'walk', 0.2, true);
613
+
614
+ // Skins
615
+ SpineHelper.setSkin(spine, 'warrior');
616
+ console.log(SpineHelper.getSkinNames(spine));
617
+ console.log(SpineHelper.getAnimationNames(spine));
618
+ ```
619
+
620
+ ---
621
+
622
+ ## UI Components
623
+
624
+ ### Button
625
+
626
+ ```typescript
627
+ import { Button } from '@energy8platform/game-engine';
628
+
629
+ const spinBtn = new Button({
630
+ width: 200,
631
+ height: 60,
632
+ borderRadius: 12,
633
+ colors: {
634
+ normal: 0xffd700,
635
+ hover: 0xffe44d,
636
+ pressed: 0xccac00,
637
+ disabled: 0x666666,
638
+ },
639
+ pressScale: 0.95,
640
+ animationDuration: 100,
641
+ });
642
+
643
+ spinBtn.onTap = () => {
644
+ console.log('Spin!');
645
+ };
646
+
647
+ // Or use texture-based states
648
+ const btn = new Button({
649
+ textures: {
650
+ normal: 'btn-normal',
651
+ hover: 'btn-hover',
652
+ pressed: 'btn-pressed',
653
+ disabled: 'btn-disabled',
654
+ },
655
+ });
656
+
657
+ btn.enable();
658
+ btn.disable();
659
+ ```
660
+
661
+ ### Label
662
+
663
+ ```typescript
664
+ import { Label } from '@energy8platform/game-engine';
665
+
666
+ const label = new Label({
667
+ text: 'TOTAL WIN',
668
+ style: { fontSize: 36, fill: 0xffffff },
669
+ });
670
+ ```
671
+
672
+ ### BalanceDisplay
673
+
674
+ Displays player balance with formatting:
675
+
676
+ ```typescript
677
+ import { BalanceDisplay } from '@energy8platform/game-engine';
678
+
679
+ const balance = new BalanceDisplay({
680
+ initialBalance: 10000,
681
+ currency: 'USD',
682
+ // ... text style options
683
+ });
684
+
685
+ balance.updateBalance(9500); // Animates the balance change
686
+ ```
687
+
688
+ ### WinDisplay
689
+
690
+ Animated win amount display with countup:
691
+
692
+ ```typescript
693
+ import { WinDisplay } from '@energy8platform/game-engine';
694
+
695
+ const winDisplay = new WinDisplay({
696
+ // ... text style options
697
+ });
698
+
699
+ await winDisplay.show(5000, 2000); // Show $50.00, countup over 2 seconds
700
+ winDisplay.hide();
701
+ ```
702
+
703
+ ### ProgressBar
704
+
705
+ ```typescript
706
+ import { ProgressBar } from '@energy8platform/game-engine';
707
+
708
+ const bar = new ProgressBar({
709
+ width: 400,
710
+ height: 20,
711
+ fillColor: 0x00ff00,
712
+ backgroundColor: 0x333333,
713
+ borderRadius: 10,
714
+ });
715
+
716
+ bar.setProgress(0.75); // 75%
717
+ ```
718
+
719
+ ### Panel
720
+
721
+ Container with a background:
722
+
723
+ ```typescript
724
+ import { Panel } from '@energy8platform/game-engine';
725
+
726
+ const panel = new Panel({
727
+ width: 600,
728
+ height: 400,
729
+ backgroundColor: 0x1a1a2e,
730
+ borderRadius: 16,
731
+ alpha: 0.9,
732
+ });
733
+ ```
734
+
735
+ ### Modal
736
+
737
+ Full-screen overlay dialog:
738
+
739
+ ```typescript
740
+ import { Modal } from '@energy8platform/game-engine';
741
+
742
+ const modal = new Modal({
743
+ width: 500,
744
+ height: 350,
745
+ overlayAlpha: 0.7,
746
+ backgroundColor: 0x222244,
747
+ borderRadius: 16,
748
+ });
749
+
750
+ await modal.show();
751
+ await modal.hide();
752
+ ```
753
+
754
+ ### Toast
755
+
756
+ Brief notification messages:
757
+
758
+ ```typescript
759
+ import { Toast } from '@energy8platform/game-engine';
760
+
761
+ const toast = new Toast({
762
+ duration: 2000,
763
+ backgroundColor: 0x333333,
764
+ textStyle: { fill: 0xffffff, fontSize: 20 },
765
+ });
766
+
767
+ await toast.show('Free spins activated!');
768
+ ```
769
+
770
+ ---
771
+
772
+ ## Input
773
+
774
+ `InputManager` provides unified touch/mouse/keyboard handling with gesture detection:
775
+
776
+ ```typescript
777
+ const input = game.input;
778
+
779
+ // Tap/click
780
+ input.on('tap', ({ x, y }) => {
781
+ console.log(`Tap at ${x}, ${y}`);
782
+ });
783
+
784
+ // Swipe gesture
785
+ input.on('swipe', ({ direction, velocity }) => {
786
+ console.log(`Swipe ${direction} at ${velocity}px/s`);
787
+ // direction: 'up' | 'down' | 'left' | 'right'
788
+ });
789
+
790
+ // Keyboard
791
+ input.on('keydown', ({ key, code }) => {
792
+ if (code === 'Space') startSpin();
793
+ });
794
+
795
+ // Check current key state
796
+ if (input.isKeyDown('ArrowLeft')) {
797
+ // Move left
798
+ }
799
+
800
+ // Lock input during animations
801
+ input.lock();
802
+ // ... animation plays ...
803
+ input.unlock();
804
+ ```
805
+
806
+ **Events:** `tap`, `press`, `release`, `move`, `swipe`, `keydown`, `keyup`
807
+
808
+ ---
809
+
810
+ ## Vite Configuration
811
+
812
+ The engine provides a pre-configured Vite setup via `defineGameConfig`:
813
+
814
+ ```typescript
815
+ // vite.config.ts
816
+ import { defineGameConfig } from '@energy8platform/game-engine/vite';
817
+
818
+ export default defineGameConfig({
819
+ base: '/games/my-slot/',
820
+ outDir: 'dist',
821
+ devBridge: true, // Auto-inject DevBridge in dev mode
822
+ assetExtensions: [
823
+ 'png', 'jpg', 'webp', 'avif',
824
+ 'mp3', 'ogg', 'wav',
825
+ 'json', 'atlas', 'skel',
826
+ 'fnt', 'ttf', 'woff2',
827
+ ],
828
+ vite: {
829
+ // Additional Vite config overrides
830
+ },
831
+ });
832
+ ```
833
+
834
+ ### What `defineGameConfig` Provides
835
+
836
+ - **PixiJS optimizations** — tree-shaking, proper externals handling
837
+ - **DevBridge injection** — automatically available in dev mode via virtual module
838
+ - **Asset handling** — preconfigured loader patterns for game assets
839
+ - **HTML minification** — optimized production builds
840
+ - **Gzip-friendly output** — chunking strategy optimized for compression
841
+ - **Base path** — configurable for deployment subpaths
842
+
843
+ ### Custom DevBridge Configuration
844
+
845
+ Create `dev.config.ts` at the project root:
846
+
847
+ ```typescript
848
+ // dev.config.ts
849
+ import type { DevBridgeConfig } from '@energy8platform/game-engine/debug';
850
+
851
+ export default {
852
+ balance: 50000,
853
+ currency: 'EUR',
854
+ networkDelay: 100,
855
+ onPlay: ({ action, bet }) => {
856
+ // Custom play result logic
857
+ const win = Math.random() < 0.3 ? bet * 10 : 0;
858
+ return { win, balance: 50000 - bet + win };
859
+ },
860
+ } satisfies DevBridgeConfig;
861
+ ```
862
+
863
+ This file is auto-imported by the Vite plugin when `devBridge: true`.
864
+
865
+ ---
866
+
867
+ ## DevBridge
868
+
869
+ `DevBridge` simulates a casino host for local development. It intercepts `postMessage` calls from the SDK and responds with mock data.
870
+
871
+ ```typescript
872
+ import { DevBridge } from '@energy8platform/game-engine/debug';
873
+
874
+ const bridge = new DevBridge({
875
+ balance: 10000,
876
+ currency: 'USD',
877
+ assetsUrl: '/assets/',
878
+ networkDelay: 200,
879
+ debug: true,
880
+ gameConfig: {
881
+ viewport: { width: 1920, height: 1080 },
882
+ },
883
+ onPlay: ({ action, bet, roundId }) => {
884
+ // Return custom play result
885
+ const win = Math.random() < 0.4 ? bet * 5 : 0;
886
+ return { win };
887
+ },
888
+ });
889
+
890
+ bridge.start();
891
+
892
+ // Update balance programmatically
893
+ bridge.setBalance(5000);
894
+
895
+ // Cleanup
896
+ bridge.destroy();
897
+ ```
898
+
899
+ ### Handled Messages
900
+
901
+ | Message | Description |
902
+ | --- | --- |
903
+ | `GAME_READY` | SDK initialization handshake |
904
+ | `PLAY_REQUEST` | Player action (spin, deal, etc.) |
905
+ | `PLAY_RESULT_ACK` | Acknowledge play result |
906
+ | `GET_BALANCE` | Balance query |
907
+ | `GET_STATE` | Game state query |
908
+ | `OPEN_DEPOSIT` | Deposit dialog request |
909
+
910
+ ---
911
+
912
+ ## Debug
913
+
914
+ ### FPS Overlay
915
+
916
+ ```typescript
917
+ import { FPSOverlay } from '@energy8platform/game-engine/debug';
918
+
919
+ const fps = new FPSOverlay(game.app);
920
+ fps.show();
921
+ fps.toggle();
922
+ fps.hide();
923
+ ```
924
+
925
+ When `debug: true` is set in `GameApplicationConfig`, the FPS overlay is enabled automatically.
926
+
927
+ The overlay displays:
928
+ - Average FPS
929
+ - Minimum FPS
930
+ - Frame time (ms)
931
+
932
+ Updated every ~500ms, sampled over 60 frames.
933
+
934
+ ---
935
+
936
+ ## API Reference
937
+
938
+ ### GameApplication
939
+
940
+ ```typescript
941
+ class GameApplication extends EventEmitter<GameEngineEvents> {
942
+ // Fields
943
+ app: Application;
944
+ scenes: SceneManager;
945
+ assets: AssetManager;
946
+ audio: AudioManager;
947
+ viewport: ViewportManager;
948
+ sdk: CasinoGameSDK | null;
949
+ initData: InitData | null;
950
+ readonly config: GameApplicationConfig;
951
+
952
+ // Getters
953
+ get gameConfig(): GameConfigData | null;
954
+ get session(): SessionData | null;
955
+ get balance(): number;
956
+ get currency(): string;
957
+ get isRunning(): boolean;
958
+
959
+ // Methods
960
+ constructor(config?: GameApplicationConfig);
961
+ async start(firstScene: string, sceneData?: unknown): Promise<void>;
962
+ destroy(): void;
963
+ }
964
+ ```
965
+
966
+ ### SceneManager
967
+
968
+ ```typescript
969
+ class SceneManager extends EventEmitter<{ change: { from: string | null; to: string } }> {
970
+ get current(): SceneEntry | null;
971
+ get currentKey(): string | null;
972
+ get isTransitioning(): boolean;
973
+
974
+ setRoot(root: Container): void;
975
+ register(key: string, ctor: SceneConstructor): this;
976
+ async goto(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
977
+ async push(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
978
+ async pop(transition?: TransitionConfig): Promise<void>;
979
+ async replace(key: string, data?: unknown, transition?: TransitionConfig): Promise<void>;
980
+ update(dt: number): void;
981
+ resize(width: number, height: number): void;
982
+ destroy(): void;
983
+ }
984
+ ```
985
+
986
+ ### AssetManager
987
+
988
+ ```typescript
989
+ class AssetManager {
990
+ get initialized(): boolean;
991
+ get basePath(): string;
992
+ get loadedBundles(): ReadonlySet<string>;
993
+
994
+ constructor(basePath?: string, manifest?: AssetManifest);
995
+ async init(): Promise<void>;
996
+ async loadBundle(name: string, onProgress?: (p: number) => void): Promise<Record<string, unknown>>;
997
+ async loadBundles(names: string[], onProgress?: (p: number) => void): Promise<Record<string, unknown>>;
998
+ async load<T>(urls: string | string[], onProgress?: (p: number) => void): Promise<T>;
999
+ get<T>(alias: string): T;
1000
+ async unloadBundle(name: string): Promise<void>;
1001
+ async backgroundLoad(name: string): Promise<void>;
1002
+ getBundleNames(): string[];
1003
+ isBundleLoaded(name: string): boolean;
1004
+ }
1005
+ ```
1006
+
1007
+ ### AudioManager
1008
+
1009
+ ```typescript
1010
+ class AudioManager {
1011
+ get initialized(): boolean;
1012
+ get muted(): boolean;
1013
+
1014
+ constructor(config?: AudioConfig);
1015
+ async init(): Promise<void>;
1016
+ play(alias: string, category?: AudioCategoryName, options?: { volume?: number; loop?: boolean; speed?: number }): void;
1017
+ playMusic(alias: string, fadeDuration?: number): void;
1018
+ stopMusic(): void;
1019
+ stopAll(): void;
1020
+ setVolume(category: AudioCategoryName, volume: number): void;
1021
+ getVolume(category: AudioCategoryName): number;
1022
+ muteCategory(category: AudioCategoryName): void;
1023
+ unmuteCategory(category: AudioCategoryName): void;
1024
+ toggleCategory(category: AudioCategoryName): boolean;
1025
+ muteAll(): void;
1026
+ unmuteAll(): void;
1027
+ toggleMute(): boolean;
1028
+ duckMusic(factor: number): void;
1029
+ unduckMusic(): void;
1030
+ destroy(): void;
1031
+ }
1032
+ ```
1033
+
1034
+ ### ViewportManager
1035
+
1036
+ ```typescript
1037
+ class ViewportManager extends EventEmitter<ViewportEvents> {
1038
+ get width(): number;
1039
+ get height(): number;
1040
+ get scale(): number;
1041
+ get orientation(): Orientation;
1042
+ get designWidth(): number;
1043
+ get designHeight(): number;
1044
+
1045
+ constructor(app: Application, container: HTMLElement, config: ViewportConfig);
1046
+ refresh(): void;
1047
+ destroy(): void;
1048
+ }
1049
+ ```
1050
+
1051
+ ### StateMachine
1052
+
1053
+ ```typescript
1054
+ class StateMachine<TContext> extends EventEmitter<StateMachineEvents> {
1055
+ get current(): string | null;
1056
+ get isTransitioning(): boolean;
1057
+ get context(): TContext;
1058
+
1059
+ constructor(context: TContext);
1060
+ addState(name: string, config: { enter?, exit?, update? }): this;
1061
+ addGuard(from: string, to: string, guard: (ctx: TContext) => boolean): this;
1062
+ async start(initialState: string, data?: unknown): Promise<void>;
1063
+ async transition(to: string, data?: unknown): Promise<boolean>;
1064
+ update(dt: number): void;
1065
+ hasState(name: string): boolean;
1066
+ canTransition(to: string): boolean;
1067
+ async reset(): Promise<void>;
1068
+ async destroy(): Promise<void>;
1069
+ }
1070
+ ```
1071
+
1072
+ ### Tween
1073
+
1074
+ ```typescript
1075
+ class Tween {
1076
+ static get activeTweens(): number;
1077
+
1078
+ static to(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction, onUpdate?: (p: number) => void): Promise<void>;
1079
+ static from(target: any, props: Record<string, number>, duration: number, easing?, onUpdate?): Promise<void>;
1080
+ static fromTo(target: any, fromProps: Record<string, number>, toProps: Record<string, number>, duration: number, easing?, onUpdate?): Promise<void>;
1081
+ static delay(ms: number): Promise<void>;
1082
+ static killTweensOf(target: any): void;
1083
+ static killAll(): void;
1084
+ }
1085
+ ```
1086
+
1087
+ ### Timeline
1088
+
1089
+ ```typescript
1090
+ class Timeline {
1091
+ get isPlaying(): boolean;
1092
+
1093
+ to(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction): this;
1094
+ from(target: any, props: Record<string, number>, duration: number, easing?: EasingFunction): this;
1095
+ delay(ms: number): this;
1096
+ call(fn: () => void | Promise<void>): this;
1097
+ parallel(...fns: Array<() => Promise<void>>): this;
1098
+ async play(): Promise<void>;
1099
+ cancel(): void;
1100
+ clear(): this;
1101
+ }
1102
+ ```
1103
+
1104
+ ### InputManager
1105
+
1106
+ ```typescript
1107
+ class InputManager extends EventEmitter<InputEvents> {
1108
+ get locked(): boolean;
1109
+
1110
+ constructor(canvas: HTMLCanvasElement);
1111
+ lock(): void;
1112
+ unlock(): void;
1113
+ isKeyDown(key: string): boolean;
1114
+ destroy(): void;
1115
+ }
1116
+ ```
1117
+
1118
+ ### EventEmitter
1119
+
1120
+ ```typescript
1121
+ class EventEmitter<TEvents extends {}> {
1122
+ on<K>(event: K, handler: (data: TEvents[K]) => void): this;
1123
+ once<K>(event: K, handler: (data: TEvents[K]) => void): this;
1124
+ off<K>(event: K, handler: (data: TEvents[K]) => void): this;
1125
+ emit<K>(event: K, data: TEvents[K]): void;
1126
+ removeAllListeners(event?: keyof TEvents): this;
1127
+ }
1128
+ ```
1129
+
1130
+ ---
1131
+
1132
+ ## License
1133
+
1134
+ MIT