@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
@@ -0,0 +1,228 @@
1
+ import { EventEmitter } from '../core/EventEmitter';
2
+
3
+ interface StateConfig<TContext> {
4
+ enter?: (ctx: TContext, data?: unknown) => void | Promise<void>;
5
+ exit?: (ctx: TContext) => void | Promise<void>;
6
+ update?: (ctx: TContext, dt: number) => void;
7
+ }
8
+
9
+ interface StateMachineEvents {
10
+ transition: { from: string | null; to: string };
11
+ error: Error;
12
+ }
13
+
14
+ /**
15
+ * Generic finite state machine for game flow management.
16
+ *
17
+ * Supports:
18
+ * - Typed context object shared across all states
19
+ * - Async enter/exit hooks
20
+ * - Per-frame update per state
21
+ * - Transition guards
22
+ * - Event emission on state change
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * interface GameContext {
27
+ * balance: number;
28
+ * bet: number;
29
+ * lastWin: number;
30
+ * }
31
+ *
32
+ * const fsm = new StateMachine<GameContext>({ balance: 1000, bet: 10, lastWin: 0 });
33
+ *
34
+ * fsm.addState('idle', {
35
+ * enter: (ctx) => console.log('Waiting for spin...'),
36
+ * update: (ctx, dt) => { // optional per-frame },
37
+ * });
38
+ *
39
+ * fsm.addState('spinning', {
40
+ * enter: async (ctx) => {
41
+ * const result = await sdk.play({ action: 'spin', bet: ctx.bet });
42
+ * ctx.lastWin = result.totalWin;
43
+ * await fsm.transition('presenting');
44
+ * },
45
+ * });
46
+ *
47
+ * fsm.addState('presenting', {
48
+ * enter: async (ctx) => {
49
+ * await showWinPresentation(ctx.lastWin);
50
+ * await fsm.transition('idle');
51
+ * },
52
+ * });
53
+ *
54
+ * // Optional guard
55
+ * fsm.addGuard('idle', 'spinning', (ctx) => ctx.balance >= ctx.bet);
56
+ *
57
+ * await fsm.start('idle');
58
+ * ```
59
+ */
60
+ export class StateMachine<TContext = Record<string, unknown>> extends EventEmitter<StateMachineEvents> {
61
+ private _states = new Map<string, StateConfig<TContext>>();
62
+ private _guards = new Map<string, (ctx: TContext) => boolean>();
63
+ private _current: string | null = null;
64
+ private _transitioning = false;
65
+ private _context: TContext;
66
+
67
+ constructor(context: TContext) {
68
+ super();
69
+ this._context = context;
70
+ }
71
+
72
+ /** Current state name */
73
+ get current(): string | null {
74
+ return this._current;
75
+ }
76
+
77
+ /** Whether a transition is in progress */
78
+ get isTransitioning(): boolean {
79
+ return this._transitioning;
80
+ }
81
+
82
+ /** State machine context (shared data) */
83
+ get context(): TContext {
84
+ return this._context;
85
+ }
86
+
87
+ /**
88
+ * Register a state with optional enter/exit/update hooks.
89
+ */
90
+ addState(name: string, config: StateConfig<TContext>): this {
91
+ this._states.set(name, config);
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Add a transition guard.
97
+ * The guard function must return true to allow the transition.
98
+ *
99
+ * @param from - Source state
100
+ * @param to - Target state
101
+ * @param guard - Guard function
102
+ */
103
+ addGuard(from: string, to: string, guard: (ctx: TContext) => boolean): this {
104
+ this._guards.set(`${from}->${to}`, guard);
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * Start the state machine in the given initial state.
110
+ */
111
+ async start(initialState: string, data?: unknown): Promise<void> {
112
+ if (this._current !== null) {
113
+ throw new Error('[StateMachine] Already started. Use transition() to change states.');
114
+ }
115
+
116
+ const state = this._states.get(initialState);
117
+ if (!state) {
118
+ throw new Error(`[StateMachine] State "${initialState}" not registered.`);
119
+ }
120
+
121
+ this._current = initialState;
122
+ await state.enter?.(this._context, data);
123
+ this.emit('transition', { from: null, to: initialState });
124
+ }
125
+
126
+ /**
127
+ * Transition to a new state.
128
+ *
129
+ * @param to - Target state name
130
+ * @param data - Optional data passed to the new state's enter hook
131
+ * @returns true if the transition succeeded, false if blocked by a guard
132
+ */
133
+ async transition(to: string, data?: unknown): Promise<boolean> {
134
+ if (this._transitioning) {
135
+ console.warn('[StateMachine] Transition already in progress');
136
+ return false;
137
+ }
138
+
139
+ const from = this._current;
140
+
141
+ // Check guard
142
+ if (from !== null) {
143
+ const guardKey = `${from}->${to}`;
144
+ const guard = this._guards.get(guardKey);
145
+ if (guard && !guard(this._context)) {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ const toState = this._states.get(to);
151
+ if (!toState) {
152
+ throw new Error(`[StateMachine] State "${to}" not registered.`);
153
+ }
154
+
155
+ this._transitioning = true;
156
+
157
+ try {
158
+ // Exit current state
159
+ if (from !== null) {
160
+ const fromState = this._states.get(from);
161
+ await fromState?.exit?.(this._context);
162
+ }
163
+
164
+ // Enter new state
165
+ this._current = to;
166
+ await toState.enter?.(this._context, data);
167
+
168
+ this.emit('transition', { from, to });
169
+ } catch (err) {
170
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
171
+ throw err;
172
+ } finally {
173
+ this._transitioning = false;
174
+ }
175
+
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * Call the current state's update function.
181
+ * Should be called from the game loop.
182
+ */
183
+ update(dt: number): void {
184
+ if (this._current === null) return;
185
+ const state = this._states.get(this._current);
186
+ state?.update?.(this._context, dt);
187
+ }
188
+
189
+ /**
190
+ * Check if a state is registered.
191
+ */
192
+ hasState(name: string): boolean {
193
+ return this._states.has(name);
194
+ }
195
+
196
+ /**
197
+ * Check if a transition is allowed (guard passes).
198
+ */
199
+ canTransition(to: string): boolean {
200
+ if (this._current === null) return false;
201
+ const guardKey = `${this._current}->${to}`;
202
+ const guard = this._guards.get(guardKey);
203
+ if (!guard) return true;
204
+ return guard(this._context);
205
+ }
206
+
207
+ /**
208
+ * Reset the state machine (exit current state, clear current).
209
+ */
210
+ async reset(): Promise<void> {
211
+ if (this._current !== null) {
212
+ const state = this._states.get(this._current);
213
+ await state?.exit?.(this._context);
214
+ }
215
+ this._current = null;
216
+ this._transitioning = false;
217
+ }
218
+
219
+ /**
220
+ * Destroy the state machine.
221
+ */
222
+ async destroy(): Promise<void> {
223
+ await this.reset();
224
+ this._states.clear();
225
+ this._guards.clear();
226
+ this.removeAllListeners();
227
+ }
228
+ }
@@ -0,0 +1 @@
1
+ export { StateMachine } from './StateMachine';
package/src/types.ts ADDED
@@ -0,0 +1,218 @@
1
+ import type { ApplicationOptions, Container } from 'pixi.js';
2
+ import type {
3
+ InitData,
4
+ GameConfigData,
5
+ SessionData,
6
+ PlayParams,
7
+ PlayResultData,
8
+ } from '@energy8platform/game-sdk';
9
+
10
+ // ─── Scale Modes ───────────────────────────────────────────
11
+
12
+ export enum ScaleMode {
13
+ /** Fit inside container, maintain aspect ratio (letterbox/pillarbox) */
14
+ FIT = 'FIT',
15
+ /** Fill container, maintain aspect ratio (crop edges) */
16
+ FILL = 'FILL',
17
+ /** Stretch to fill (distorts) */
18
+ STRETCH = 'STRETCH',
19
+ }
20
+
21
+ // ─── Orientation ───────────────────────────────────────────
22
+
23
+ export enum Orientation {
24
+ LANDSCAPE = 'landscape',
25
+ PORTRAIT = 'portrait',
26
+ ANY = 'any',
27
+ }
28
+
29
+ // ─── Loading Screen Config ─────────────────────────────────
30
+
31
+ export interface LoadingScreenConfig {
32
+ /** Background color (hex number or CSS string) */
33
+ backgroundColor?: number | string;
34
+ /** Background gradient (CSS string applied to the CSS preloader) */
35
+ backgroundGradient?: string;
36
+ /** Logo texture alias (must be in 'preload' bundle) */
37
+ logoAsset?: string;
38
+ /** Logo scale (default: 1) */
39
+ logoScale?: number;
40
+ /** Show percentage text below the loader bar */
41
+ showPercentage?: boolean;
42
+ /** Custom progress text formatter */
43
+ progressTextFormatter?: (progress: number) => string;
44
+ /** Show "Tap to start" after loading (needed for mobile audio unlock) */
45
+ tapToStart?: boolean;
46
+ /** "Tap to start" label text */
47
+ tapToStartText?: string;
48
+ /** Minimum display time in ms (so the user sees the brand, even if loading is fast) */
49
+ minDisplayTime?: number;
50
+ /** CSS preloader custom HTML (shown before PixiJS is ready) */
51
+ cssPreloaderHTML?: string;
52
+ }
53
+
54
+ // ─── Asset Manifest ────────────────────────────────────────
55
+
56
+ export interface AssetEntry {
57
+ alias: string;
58
+ src: string | string[];
59
+ /** Optional data to pass to the loader */
60
+ data?: Record<string, unknown>;
61
+ }
62
+
63
+ export interface AssetBundle {
64
+ name: string;
65
+ assets: AssetEntry[];
66
+ }
67
+
68
+ export interface AssetManifest {
69
+ bundles: AssetBundle[];
70
+ }
71
+
72
+ // ─── Audio Config ──────────────────────────────────────────
73
+
74
+ export interface AudioCategory {
75
+ volume: number;
76
+ muted: boolean;
77
+ }
78
+
79
+ export interface AudioConfig {
80
+ /** Default volumes per category (0..1) */
81
+ music?: number;
82
+ sfx?: number;
83
+ ui?: number;
84
+ ambient?: number;
85
+ /** Persist mute state in localStorage */
86
+ persist?: boolean;
87
+ /** LocalStorage key prefix */
88
+ storageKey?: string;
89
+ }
90
+
91
+ // ─── Game Application Config ───────────────────────────────
92
+
93
+ export interface GameApplicationConfig {
94
+ /** Container element or CSS selector to mount canvas into */
95
+ container?: HTMLElement | string;
96
+
97
+ /** Reference design width (fallback: GameConfigData.viewport.width or 1920) */
98
+ designWidth?: number;
99
+
100
+ /** Reference design height (fallback: GameConfigData.viewport.height or 1080) */
101
+ designHeight?: number;
102
+
103
+ /** How to scale the game to fit the container */
104
+ scaleMode?: ScaleMode;
105
+
106
+ /** Preferred orientation */
107
+ orientation?: Orientation;
108
+
109
+ /** Loading screen configuration */
110
+ loading?: LoadingScreenConfig;
111
+
112
+ /** Asset manifest — what to load */
113
+ manifest?: AssetManifest;
114
+
115
+ /** Audio configuration */
116
+ audio?: AudioConfig;
117
+
118
+ /** SDK options. Set to false to disable SDK (offline/development mode) */
119
+ sdk?:
120
+ | {
121
+ parentOrigin?: string;
122
+ timeout?: number;
123
+ debug?: boolean;
124
+ }
125
+ | false;
126
+
127
+ /** PixiJS Application options (pass-through) */
128
+ pixi?: Partial<ApplicationOptions>;
129
+
130
+ /** Enable debug overlay (FPS, draw calls) */
131
+ debug?: boolean;
132
+ }
133
+
134
+ // ─── Scene Types ───────────────────────────────────────────
135
+
136
+ export interface SceneConstructor {
137
+ new (): IScene;
138
+ }
139
+
140
+ export interface IScene {
141
+ /** Root display container for this scene */
142
+ readonly container: Container;
143
+
144
+ /** Called when the scene is entered */
145
+ onEnter?(data?: unknown): Promise<void> | void;
146
+
147
+ /** Called when the scene is exited */
148
+ onExit?(): Promise<void> | void;
149
+
150
+ /** Called every frame */
151
+ onUpdate?(dt: number): void;
152
+
153
+ /** Called when viewport resizes */
154
+ onResize?(width: number, height: number): void;
155
+
156
+ /** Called when the scene is destroyed */
157
+ onDestroy?(): void;
158
+ }
159
+
160
+ // ─── Transition Types ──────────────────────────────────────
161
+
162
+ export enum TransitionType {
163
+ NONE = 'none',
164
+ FADE = 'fade',
165
+ SLIDE_LEFT = 'slide-left',
166
+ SLIDE_RIGHT = 'slide-right',
167
+ }
168
+
169
+ export interface TransitionConfig {
170
+ type: TransitionType;
171
+ duration?: number;
172
+ easing?: (t: number) => number;
173
+ }
174
+
175
+ // ─── Event Types ───────────────────────────────────────────
176
+
177
+ export interface GameEngineEvents {
178
+ /** Fired when engine initialization is complete */
179
+ initialized: void;
180
+ /** Fired when all assets are loaded */
181
+ loaded: void;
182
+ /** Fired when the engine starts running */
183
+ started: void;
184
+ /** Fired on viewport resize */
185
+ resize: { width: number; height: number };
186
+ /** Fired on orientation change */
187
+ orientationChange: Orientation;
188
+ /** Fired on scene change */
189
+ sceneChange: { from: string | null; to: string };
190
+ /** Fired on error */
191
+ error: Error;
192
+ /** Fired when engine is destroyed */
193
+ destroyed: void;
194
+ }
195
+
196
+ // ─── Tween Types ───────────────────────────────────────────
197
+
198
+ export type EasingFunction = (t: number) => number;
199
+
200
+ export interface TweenOptions {
201
+ duration: number;
202
+ easing?: EasingFunction;
203
+ delay?: number;
204
+ repeat?: number;
205
+ yoyo?: boolean;
206
+ onUpdate?: (progress: number) => void;
207
+ onComplete?: () => void;
208
+ }
209
+
210
+ // ─── Re-exports from SDK for convenience ───────────────────
211
+
212
+ export type {
213
+ InitData,
214
+ GameConfigData,
215
+ SessionData,
216
+ PlayParams,
217
+ PlayResultData,
218
+ };
@@ -0,0 +1,155 @@
1
+ import { Container } from 'pixi.js';
2
+ import { Label } from './Label';
3
+ import { Tween } from '../animation/Tween';
4
+ import { Easing } from '../animation/Easing';
5
+
6
+ export interface BalanceDisplayConfig {
7
+ /** Label prefix (e.g., "BALANCE") */
8
+ prefix?: string;
9
+ /** Text style overrides */
10
+ style?: Record<string, unknown>;
11
+ /** Currency code */
12
+ currency?: string;
13
+ /** Locale for number formatting */
14
+ locale?: string;
15
+ /** Max width */
16
+ maxWidth?: number;
17
+ /** Animate value changes */
18
+ animated?: boolean;
19
+ /** Animation duration in ms */
20
+ animationDuration?: number;
21
+ }
22
+
23
+ /**
24
+ * Reactive balance display component.
25
+ *
26
+ * Automatically formats currency and can animate value changes
27
+ * with a smooth countup/countdown effect.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const balance = new BalanceDisplay({ currency: 'USD', animated: true });
32
+ * balance.setValue(1000);
33
+ *
34
+ * // Wire to SDK
35
+ * sdk.on('balanceUpdate', ({ balance: val }) => balance.setValue(val));
36
+ * ```
37
+ */
38
+ export class BalanceDisplay extends Container {
39
+ private _prefixLabel: Label | null = null;
40
+ private _valueLabel: Label;
41
+ private _config: Required<Pick<BalanceDisplayConfig, 'currency' | 'locale' | 'animated' | 'animationDuration'>>;
42
+ private _currentValue = 0;
43
+ private _displayedValue = 0;
44
+ private _animating = false;
45
+
46
+ constructor(config: BalanceDisplayConfig = {}) {
47
+ super();
48
+
49
+ this._config = {
50
+ currency: config.currency ?? 'USD',
51
+ locale: config.locale ?? 'en-US',
52
+ animated: config.animated ?? true,
53
+ animationDuration: config.animationDuration ?? 500,
54
+ };
55
+
56
+ // Prefix label
57
+ if (config.prefix) {
58
+ this._prefixLabel = new Label({
59
+ text: config.prefix,
60
+ style: {
61
+ fontSize: 16,
62
+ fill: 0xaaaaaa,
63
+ ...(config.style as any),
64
+ },
65
+ });
66
+ this.addChild(this._prefixLabel);
67
+ }
68
+
69
+ // Value label
70
+ this._valueLabel = new Label({
71
+ text: '0.00',
72
+ style: {
73
+ fontSize: 28,
74
+ fontWeight: 'bold',
75
+ fill: 0xffffff,
76
+ ...(config.style as any),
77
+ },
78
+ maxWidth: config.maxWidth,
79
+ autoFit: !!config.maxWidth,
80
+ });
81
+ this.addChild(this._valueLabel);
82
+
83
+ this.layoutLabels();
84
+ }
85
+
86
+ /** Current displayed value */
87
+ get value(): number {
88
+ return this._currentValue;
89
+ }
90
+
91
+ /**
92
+ * Set the balance value. If animated, smoothly counts to the new value.
93
+ */
94
+ setValue(value: number): void {
95
+ const oldValue = this._currentValue;
96
+ this._currentValue = value;
97
+
98
+ if (this._config.animated && oldValue !== value) {
99
+ this.animateValue(oldValue, value);
100
+ } else {
101
+ this._displayedValue = value;
102
+ this.updateDisplay();
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Set the currency code.
108
+ */
109
+ setCurrency(currency: string): void {
110
+ this._config.currency = currency;
111
+ this.updateDisplay();
112
+ }
113
+
114
+ private async animateValue(from: number, to: number): Promise<void> {
115
+ this._animating = true;
116
+ const duration = this._config.animationDuration;
117
+ const startTime = Date.now();
118
+
119
+ return new Promise<void>((resolve) => {
120
+ const tick = () => {
121
+ const elapsed = Date.now() - startTime;
122
+ const t = Math.min(elapsed / duration, 1);
123
+ const eased = Easing.easeOutCubic(t);
124
+
125
+ this._displayedValue = from + (to - from) * eased;
126
+ this.updateDisplay();
127
+
128
+ if (t < 1) {
129
+ requestAnimationFrame(tick);
130
+ } else {
131
+ this._displayedValue = to;
132
+ this.updateDisplay();
133
+ this._animating = false;
134
+ resolve();
135
+ }
136
+ };
137
+ requestAnimationFrame(tick);
138
+ });
139
+ }
140
+
141
+ private updateDisplay(): void {
142
+ this._valueLabel.setCurrency(
143
+ this._displayedValue,
144
+ this._config.currency,
145
+ this._config.locale,
146
+ );
147
+ }
148
+
149
+ private layoutLabels(): void {
150
+ if (this._prefixLabel) {
151
+ this._prefixLabel.y = -14;
152
+ this._valueLabel.y = 14;
153
+ }
154
+ }
155
+ }