@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/dist/ui.esm.js ADDED
@@ -0,0 +1,1072 @@
1
+ import { Ticker, Container, Graphics, Texture, Sprite, Text, NineSliceSprite } from 'pixi.js';
2
+
3
+ /**
4
+ * Collection of easing functions for use with Tween and Timeline.
5
+ *
6
+ * All functions take a progress value t (0..1) and return the eased value.
7
+ */
8
+ const Easing = {
9
+ linear: (t) => t,
10
+ easeInQuad: (t) => t * t,
11
+ easeOutQuad: (t) => t * (2 - t),
12
+ easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
13
+ easeInCubic: (t) => t * t * t,
14
+ easeOutCubic: (t) => --t * t * t + 1,
15
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
16
+ easeInQuart: (t) => t * t * t * t,
17
+ easeOutQuart: (t) => 1 - --t * t * t * t,
18
+ easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
19
+ easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
20
+ easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
21
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
22
+ easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
23
+ easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
24
+ easeInOutExpo: (t) => t === 0
25
+ ? 0
26
+ : t === 1
27
+ ? 1
28
+ : t < 0.5
29
+ ? Math.pow(2, 20 * t - 10) / 2
30
+ : (2 - Math.pow(2, -20 * t + 10)) / 2,
31
+ easeInBack: (t) => {
32
+ const c1 = 1.70158;
33
+ const c3 = c1 + 1;
34
+ return c3 * t * t * t - c1 * t * t;
35
+ },
36
+ easeOutBack: (t) => {
37
+ const c1 = 1.70158;
38
+ const c3 = c1 + 1;
39
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
40
+ },
41
+ easeInOutBack: (t) => {
42
+ const c1 = 1.70158;
43
+ const c2 = c1 * 1.525;
44
+ return t < 0.5
45
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
46
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
47
+ },
48
+ easeOutBounce: (t) => {
49
+ const n1 = 7.5625;
50
+ const d1 = 2.75;
51
+ if (t < 1 / d1)
52
+ return n1 * t * t;
53
+ if (t < 2 / d1)
54
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
55
+ if (t < 2.5 / d1)
56
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
57
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
58
+ },
59
+ easeInBounce: (t) => 1 - Easing.easeOutBounce(1 - t),
60
+ easeInOutBounce: (t) => t < 0.5
61
+ ? (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
62
+ : (1 + Easing.easeOutBounce(2 * t - 1)) / 2,
63
+ easeOutElastic: (t) => {
64
+ const c4 = (2 * Math.PI) / 3;
65
+ return t === 0
66
+ ? 0
67
+ : t === 1
68
+ ? 1
69
+ : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
70
+ },
71
+ easeInElastic: (t) => {
72
+ const c4 = (2 * Math.PI) / 3;
73
+ return t === 0
74
+ ? 0
75
+ : t === 1
76
+ ? 1
77
+ : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
78
+ },
79
+ };
80
+
81
+ /**
82
+ * Lightweight tween system integrated with PixiJS Ticker.
83
+ * Zero external dependencies — no GSAP required.
84
+ *
85
+ * All tweens return a Promise that resolves on completion.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * // Fade in a sprite
90
+ * await Tween.to(sprite, { alpha: 1, y: 100 }, 500, Easing.easeOutBack);
91
+ *
92
+ * // Move and wait
93
+ * await Tween.to(sprite, { x: 500 }, 300);
94
+ *
95
+ * // From a starting value
96
+ * await Tween.from(sprite, { scale: 0, alpha: 0 }, 400);
97
+ * ```
98
+ */
99
+ class Tween {
100
+ static _tweens = [];
101
+ static _tickerAdded = false;
102
+ /**
103
+ * Animate properties from current values to target values.
104
+ *
105
+ * @param target - Object to animate (Sprite, Container, etc.)
106
+ * @param props - Target property values
107
+ * @param duration - Duration in milliseconds
108
+ * @param easing - Easing function (default: easeOutQuad)
109
+ * @param onUpdate - Progress callback (0..1)
110
+ */
111
+ static to(target, props, duration, easing, onUpdate) {
112
+ return new Promise((resolve) => {
113
+ // Capture starting values
114
+ const from = {};
115
+ for (const key of Object.keys(props)) {
116
+ from[key] = Tween.getProperty(target, key);
117
+ }
118
+ const tween = {
119
+ target,
120
+ from,
121
+ to: { ...props },
122
+ duration: Math.max(1, duration),
123
+ easing: easing ?? Easing.easeOutQuad,
124
+ elapsed: 0,
125
+ delay: 0,
126
+ resolve,
127
+ onUpdate,
128
+ };
129
+ Tween._tweens.push(tween);
130
+ Tween.ensureTicker();
131
+ });
132
+ }
133
+ /**
134
+ * Animate properties from given values to current values.
135
+ */
136
+ static from(target, props, duration, easing, onUpdate) {
137
+ // Capture current values as "to"
138
+ const to = {};
139
+ for (const key of Object.keys(props)) {
140
+ to[key] = Tween.getProperty(target, key);
141
+ Tween.setProperty(target, key, props[key]);
142
+ }
143
+ return Tween.to(target, to, duration, easing, onUpdate);
144
+ }
145
+ /**
146
+ * Animate from one set of values to another.
147
+ */
148
+ static fromTo(target, fromProps, toProps, duration, easing, onUpdate) {
149
+ // Set starting values
150
+ for (const key of Object.keys(fromProps)) {
151
+ Tween.setProperty(target, key, fromProps[key]);
152
+ }
153
+ return Tween.to(target, toProps, duration, easing, onUpdate);
154
+ }
155
+ /**
156
+ * Wait for a given duration (useful in timelines).
157
+ */
158
+ static delay(ms) {
159
+ return new Promise((resolve) => setTimeout(resolve, ms));
160
+ }
161
+ /**
162
+ * Kill all tweens on a target.
163
+ */
164
+ static killTweensOf(target) {
165
+ Tween._tweens = Tween._tweens.filter((tw) => {
166
+ if (tw.target === target) {
167
+ tw.resolve();
168
+ return false;
169
+ }
170
+ return true;
171
+ });
172
+ }
173
+ /**
174
+ * Kill all active tweens.
175
+ */
176
+ static killAll() {
177
+ for (const tw of Tween._tweens) {
178
+ tw.resolve();
179
+ }
180
+ Tween._tweens.length = 0;
181
+ }
182
+ /** Number of active tweens */
183
+ static get activeTweens() {
184
+ return Tween._tweens.length;
185
+ }
186
+ // ─── Internal ──────────────────────────────────────────
187
+ static ensureTicker() {
188
+ if (Tween._tickerAdded)
189
+ return;
190
+ Tween._tickerAdded = true;
191
+ Ticker.shared.add(Tween.tick);
192
+ }
193
+ static tick = (ticker) => {
194
+ const dt = ticker.deltaMS;
195
+ const completed = [];
196
+ for (const tw of Tween._tweens) {
197
+ tw.elapsed += dt;
198
+ if (tw.elapsed < tw.delay)
199
+ continue;
200
+ const raw = Math.min((tw.elapsed - tw.delay) / tw.duration, 1);
201
+ const t = tw.easing(raw);
202
+ // Interpolate each property
203
+ for (const key of Object.keys(tw.to)) {
204
+ const start = tw.from[key];
205
+ const end = tw.to[key];
206
+ const value = start + (end - start) * t;
207
+ Tween.setProperty(tw.target, key, value);
208
+ }
209
+ tw.onUpdate?.(raw);
210
+ if (raw >= 1) {
211
+ completed.push(tw);
212
+ }
213
+ }
214
+ // Remove completed tweens
215
+ for (const tw of completed) {
216
+ const idx = Tween._tweens.indexOf(tw);
217
+ if (idx !== -1)
218
+ Tween._tweens.splice(idx, 1);
219
+ tw.resolve();
220
+ }
221
+ // Remove ticker when no active tweens
222
+ if (Tween._tweens.length === 0 && Tween._tickerAdded) {
223
+ Ticker.shared.remove(Tween.tick);
224
+ Tween._tickerAdded = false;
225
+ }
226
+ };
227
+ /**
228
+ * Get a potentially nested property (supports 'scale.x', 'position.y', etc.)
229
+ */
230
+ static getProperty(target, key) {
231
+ const parts = key.split('.');
232
+ let obj = target;
233
+ for (let i = 0; i < parts.length - 1; i++) {
234
+ obj = obj[parts[i]];
235
+ }
236
+ return obj[parts[parts.length - 1]] ?? 0;
237
+ }
238
+ /**
239
+ * Set a potentially nested property.
240
+ */
241
+ static setProperty(target, key, value) {
242
+ const parts = key.split('.');
243
+ let obj = target;
244
+ for (let i = 0; i < parts.length - 1; i++) {
245
+ obj = obj[parts[i]];
246
+ }
247
+ obj[parts[parts.length - 1]] = value;
248
+ }
249
+ }
250
+
251
+ const DEFAULT_COLORS = {
252
+ normal: 0xffd700,
253
+ hover: 0xffe44d,
254
+ pressed: 0xccac00,
255
+ disabled: 0x666666,
256
+ };
257
+ /**
258
+ * Interactive button component with state management and animation.
259
+ *
260
+ * Supports both texture-based and Graphics-based rendering.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * const btn = new Button({
265
+ * width: 200, height: 60, borderRadius: 12,
266
+ * colors: { normal: 0x22aa22, hover: 0x33cc33 },
267
+ * });
268
+ *
269
+ * btn.onTap = () => console.log('Clicked!');
270
+ * scene.container.addChild(btn);
271
+ * ```
272
+ */
273
+ class Button extends Container {
274
+ _state = 'normal';
275
+ _bg;
276
+ _sprites = {};
277
+ _config;
278
+ /** Called when the button is tapped/clicked */
279
+ onTap;
280
+ /** Called when the button state changes */
281
+ onStateChange;
282
+ constructor(config = {}) {
283
+ super();
284
+ this._config = {
285
+ width: 200,
286
+ height: 60,
287
+ borderRadius: 8,
288
+ pressScale: 0.95,
289
+ animationDuration: 100,
290
+ ...config,
291
+ };
292
+ // Create Graphics background
293
+ this._bg = new Graphics();
294
+ this.addChild(this._bg);
295
+ // Create texture sprites if provided
296
+ if (config.textures) {
297
+ for (const [state, tex] of Object.entries(config.textures)) {
298
+ const texture = typeof tex === 'string' ? Texture.from(tex) : tex;
299
+ const sprite = new Sprite(texture);
300
+ sprite.anchor.set(0.5);
301
+ sprite.visible = state === 'normal';
302
+ this._sprites[state] = sprite;
303
+ this.addChild(sprite);
304
+ }
305
+ }
306
+ // Make interactive
307
+ this.eventMode = 'static';
308
+ this.cursor = 'pointer';
309
+ // Set up hit area for Graphics-based
310
+ this.pivot.set(this._config.width / 2, this._config.height / 2);
311
+ // Bind events
312
+ this.on('pointerover', this.onPointerOver);
313
+ this.on('pointerout', this.onPointerOut);
314
+ this.on('pointerdown', this.onPointerDown);
315
+ this.on('pointerup', this.onPointerUp);
316
+ this.on('pointertap', this.onPointerTap);
317
+ // Initial render
318
+ this.setState('normal');
319
+ if (config.disabled) {
320
+ this.disable();
321
+ }
322
+ }
323
+ /** Current button state */
324
+ get state() {
325
+ return this._state;
326
+ }
327
+ /** Enable the button */
328
+ enable() {
329
+ if (this._state === 'disabled') {
330
+ this.setState('normal');
331
+ this.eventMode = 'static';
332
+ this.cursor = 'pointer';
333
+ }
334
+ }
335
+ /** Disable the button */
336
+ disable() {
337
+ this.setState('disabled');
338
+ this.eventMode = 'none';
339
+ this.cursor = 'default';
340
+ }
341
+ /** Whether the button is disabled */
342
+ get disabled() {
343
+ return this._state === 'disabled';
344
+ }
345
+ setState(state) {
346
+ if (this._state === state)
347
+ return;
348
+ this._state = state;
349
+ this.render();
350
+ this.onStateChange?.(state);
351
+ }
352
+ render() {
353
+ const { width, height, borderRadius, colors } = this._config;
354
+ const colorMap = { ...DEFAULT_COLORS, ...colors };
355
+ // Update Graphics
356
+ this._bg.clear();
357
+ this._bg.roundRect(0, 0, width, height, borderRadius).fill(colorMap[this._state]);
358
+ // Add highlight for normal/hover
359
+ if (this._state === 'normal' || this._state === 'hover') {
360
+ this._bg
361
+ .roundRect(2, 2, width - 4, height * 0.45, borderRadius)
362
+ .fill({ color: 0xffffff, alpha: 0.1 });
363
+ }
364
+ // Update sprite visibility
365
+ for (const [state, sprite] of Object.entries(this._sprites)) {
366
+ if (sprite)
367
+ sprite.visible = state === this._state;
368
+ }
369
+ // Fall back to normal sprite if state sprite doesn't exist
370
+ if (!this._sprites[this._state] && this._sprites.normal) {
371
+ this._sprites.normal.visible = true;
372
+ }
373
+ }
374
+ onPointerOver = () => {
375
+ if (this._state === 'disabled')
376
+ return;
377
+ this.setState('hover');
378
+ };
379
+ onPointerOut = () => {
380
+ if (this._state === 'disabled')
381
+ return;
382
+ this.setState('normal');
383
+ Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration);
384
+ };
385
+ onPointerDown = () => {
386
+ if (this._state === 'disabled')
387
+ return;
388
+ this.setState('pressed');
389
+ const s = this._config.pressScale;
390
+ Tween.to(this.scale, { x: s, y: s }, this._config.animationDuration, Easing.easeOutQuad);
391
+ };
392
+ onPointerUp = () => {
393
+ if (this._state === 'disabled')
394
+ return;
395
+ this.setState('hover');
396
+ Tween.to(this.scale, { x: 1, y: 1 }, this._config.animationDuration, Easing.easeOutBack);
397
+ };
398
+ onPointerTap = () => {
399
+ if (this._state === 'disabled')
400
+ return;
401
+ this.onTap?.();
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Horizontal progress bar with optional smooth fill animation.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
411
+ * scene.container.addChild(bar);
412
+ * bar.progress = 0.5; // 50%
413
+ * ```
414
+ */
415
+ class ProgressBar extends Container {
416
+ _track;
417
+ _fill;
418
+ _border;
419
+ _config;
420
+ _progress = 0;
421
+ _displayedProgress = 0;
422
+ constructor(config = {}) {
423
+ super();
424
+ this._config = {
425
+ width: 300,
426
+ height: 16,
427
+ borderRadius: 8,
428
+ fillColor: 0xffd700,
429
+ trackColor: 0x333333,
430
+ borderColor: 0x555555,
431
+ borderWidth: 1,
432
+ animated: true,
433
+ animationSpeed: 0.1,
434
+ ...config,
435
+ };
436
+ this._track = new Graphics();
437
+ this._fill = new Graphics();
438
+ this._border = new Graphics();
439
+ this.addChild(this._track, this._fill, this._border);
440
+ this.drawTrack();
441
+ this.drawBorder();
442
+ this.drawFill(0);
443
+ }
444
+ /** Get/set progress (0..1) */
445
+ get progress() {
446
+ return this._progress;
447
+ }
448
+ set progress(value) {
449
+ this._progress = Math.max(0, Math.min(1, value));
450
+ if (!this._config.animated) {
451
+ this._displayedProgress = this._progress;
452
+ this.drawFill(this._displayedProgress);
453
+ }
454
+ }
455
+ /**
456
+ * Call each frame if animated is true.
457
+ */
458
+ update(dt) {
459
+ if (!this._config.animated)
460
+ return;
461
+ if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
462
+ this._displayedProgress = this._progress;
463
+ return;
464
+ }
465
+ this._displayedProgress +=
466
+ (this._progress - this._displayedProgress) * this._config.animationSpeed;
467
+ this.drawFill(this._displayedProgress);
468
+ }
469
+ drawTrack() {
470
+ const { width, height, borderRadius, trackColor } = this._config;
471
+ this._track.clear();
472
+ this._track.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
473
+ }
474
+ drawBorder() {
475
+ const { width, height, borderRadius, borderColor, borderWidth } = this._config;
476
+ this._border.clear();
477
+ this._border
478
+ .roundRect(0, 0, width, height, borderRadius)
479
+ .stroke({ color: borderColor, width: borderWidth });
480
+ }
481
+ drawFill(progress) {
482
+ const { width, height, borderRadius, fillColor, borderWidth } = this._config;
483
+ const innerWidth = width - borderWidth * 2;
484
+ const innerHeight = height - borderWidth * 2;
485
+ const fillWidth = Math.max(0, innerWidth * progress);
486
+ this._fill.clear();
487
+ if (fillWidth > 0) {
488
+ this._fill.x = borderWidth;
489
+ this._fill.y = borderWidth;
490
+ this._fill.roundRect(0, 0, fillWidth, innerHeight, borderRadius - 1).fill(fillColor);
491
+ // Highlight
492
+ this._fill
493
+ .roundRect(0, 0, fillWidth, innerHeight * 0.4, borderRadius - 1)
494
+ .fill({ color: 0xffffff, alpha: 0.15 });
495
+ }
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Enhanced text label with auto-fit scaling and currency formatting.
501
+ *
502
+ * @example
503
+ * ```ts
504
+ * const label = new Label({
505
+ * text: 'BALANCE',
506
+ * style: { fontSize: 24, fill: 0xffd700 },
507
+ * maxWidth: 200,
508
+ * autoFit: true,
509
+ * });
510
+ * ```
511
+ */
512
+ class Label extends Container {
513
+ _text;
514
+ _maxWidth;
515
+ _autoFit;
516
+ constructor(config = {}) {
517
+ super();
518
+ this._maxWidth = config.maxWidth ?? Infinity;
519
+ this._autoFit = config.autoFit ?? false;
520
+ this._text = new Text({
521
+ text: config.text ?? '',
522
+ style: {
523
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
524
+ fontSize: 24,
525
+ fill: 0xffffff,
526
+ ...config.style,
527
+ },
528
+ });
529
+ this._text.anchor.set(0.5);
530
+ this.addChild(this._text);
531
+ this.fitText();
532
+ }
533
+ /** Get/set the displayed text */
534
+ get text() {
535
+ return this._text.text;
536
+ }
537
+ set text(value) {
538
+ this._text.text = value;
539
+ this.fitText();
540
+ }
541
+ /** Get/set the text style */
542
+ get style() {
543
+ return this._text.style;
544
+ }
545
+ /** Set max width constraint */
546
+ set maxWidth(value) {
547
+ this._maxWidth = value;
548
+ this.fitText();
549
+ }
550
+ /**
551
+ * Format and display a number as currency.
552
+ *
553
+ * @param amount - The numeric amount
554
+ * @param currency - Currency code (e.g., 'USD', 'EUR')
555
+ * @param locale - Locale string (default: 'en-US')
556
+ */
557
+ setCurrency(amount, currency, locale = 'en-US') {
558
+ try {
559
+ this.text = new Intl.NumberFormat(locale, {
560
+ style: 'currency',
561
+ currency,
562
+ minimumFractionDigits: 2,
563
+ maximumFractionDigits: 2,
564
+ }).format(amount);
565
+ }
566
+ catch {
567
+ this.text = `${amount.toFixed(2)} ${currency}`;
568
+ }
569
+ }
570
+ /**
571
+ * Format a number with thousands separators.
572
+ */
573
+ setNumber(value, decimals = 0, locale = 'en-US') {
574
+ this.text = new Intl.NumberFormat(locale, {
575
+ minimumFractionDigits: decimals,
576
+ maximumFractionDigits: decimals,
577
+ }).format(value);
578
+ }
579
+ fitText() {
580
+ if (!this._autoFit || this._maxWidth === Infinity)
581
+ return;
582
+ this._text.scale.set(1);
583
+ if (this._text.width > this._maxWidth) {
584
+ const scale = this._maxWidth / this._text.width;
585
+ this._text.scale.set(scale);
586
+ }
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Background panel that can use either Graphics or 9-slice sprite.
592
+ *
593
+ * @example
594
+ * ```ts
595
+ * // Simple colored panel
596
+ * const panel = new Panel({ width: 400, height: 300, backgroundColor: 0x222222, borderRadius: 12 });
597
+ *
598
+ * // 9-slice panel (texture-based)
599
+ * const panel = new Panel({
600
+ * nineSliceTexture: 'panel-bg',
601
+ * nineSliceBorders: [20, 20, 20, 20],
602
+ * width: 400, height: 300,
603
+ * });
604
+ * ```
605
+ */
606
+ class Panel extends Container {
607
+ _bg;
608
+ _content;
609
+ _config;
610
+ constructor(config = {}) {
611
+ super();
612
+ this._config = {
613
+ width: 400,
614
+ height: 300,
615
+ padding: 16,
616
+ backgroundAlpha: 1,
617
+ ...config,
618
+ };
619
+ // Create background
620
+ if (config.nineSliceTexture) {
621
+ const texture = typeof config.nineSliceTexture === 'string'
622
+ ? Texture.from(config.nineSliceTexture)
623
+ : config.nineSliceTexture;
624
+ const [left, top, right, bottom] = config.nineSliceBorders ?? [10, 10, 10, 10];
625
+ this._bg = new NineSliceSprite({
626
+ texture,
627
+ leftWidth: left,
628
+ topHeight: top,
629
+ rightWidth: right,
630
+ bottomHeight: bottom,
631
+ });
632
+ this._bg.width = this._config.width;
633
+ this._bg.height = this._config.height;
634
+ }
635
+ else {
636
+ this._bg = new Graphics();
637
+ this.drawGraphicsBg();
638
+ }
639
+ this._bg.alpha = this._config.backgroundAlpha;
640
+ this.addChild(this._bg);
641
+ // Content container with padding
642
+ this._content = new Container();
643
+ this._content.x = this._config.padding;
644
+ this._content.y = this._config.padding;
645
+ this.addChild(this._content);
646
+ }
647
+ /** Content container — add children here */
648
+ get content() {
649
+ return this._content;
650
+ }
651
+ /** Resize the panel */
652
+ setSize(width, height) {
653
+ this._config.width = width;
654
+ this._config.height = height;
655
+ if (this._bg instanceof Graphics) {
656
+ this.drawGraphicsBg();
657
+ }
658
+ else {
659
+ this._bg.width = width;
660
+ this._bg.height = height;
661
+ }
662
+ }
663
+ drawGraphicsBg() {
664
+ const bg = this._bg;
665
+ const { width, height, backgroundColor, borderRadius, borderColor, borderWidth, } = this._config;
666
+ bg.clear();
667
+ bg.roundRect(0, 0, width, height, borderRadius ?? 0).fill(backgroundColor ?? 0x1a1a2e);
668
+ if (borderColor !== undefined && borderWidth) {
669
+ bg.roundRect(0, 0, width, height, borderRadius ?? 0)
670
+ .stroke({ color: borderColor, width: borderWidth });
671
+ }
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Reactive balance display component.
677
+ *
678
+ * Automatically formats currency and can animate value changes
679
+ * with a smooth countup/countdown effect.
680
+ *
681
+ * @example
682
+ * ```ts
683
+ * const balance = new BalanceDisplay({ currency: 'USD', animated: true });
684
+ * balance.setValue(1000);
685
+ *
686
+ * // Wire to SDK
687
+ * sdk.on('balanceUpdate', ({ balance: val }) => balance.setValue(val));
688
+ * ```
689
+ */
690
+ class BalanceDisplay extends Container {
691
+ _prefixLabel = null;
692
+ _valueLabel;
693
+ _config;
694
+ _currentValue = 0;
695
+ _displayedValue = 0;
696
+ _animating = false;
697
+ constructor(config = {}) {
698
+ super();
699
+ this._config = {
700
+ currency: config.currency ?? 'USD',
701
+ locale: config.locale ?? 'en-US',
702
+ animated: config.animated ?? true,
703
+ animationDuration: config.animationDuration ?? 500,
704
+ };
705
+ // Prefix label
706
+ if (config.prefix) {
707
+ this._prefixLabel = new Label({
708
+ text: config.prefix,
709
+ style: {
710
+ fontSize: 16,
711
+ fill: 0xaaaaaa,
712
+ ...config.style,
713
+ },
714
+ });
715
+ this.addChild(this._prefixLabel);
716
+ }
717
+ // Value label
718
+ this._valueLabel = new Label({
719
+ text: '0.00',
720
+ style: {
721
+ fontSize: 28,
722
+ fontWeight: 'bold',
723
+ fill: 0xffffff,
724
+ ...config.style,
725
+ },
726
+ maxWidth: config.maxWidth,
727
+ autoFit: !!config.maxWidth,
728
+ });
729
+ this.addChild(this._valueLabel);
730
+ this.layoutLabels();
731
+ }
732
+ /** Current displayed value */
733
+ get value() {
734
+ return this._currentValue;
735
+ }
736
+ /**
737
+ * Set the balance value. If animated, smoothly counts to the new value.
738
+ */
739
+ setValue(value) {
740
+ const oldValue = this._currentValue;
741
+ this._currentValue = value;
742
+ if (this._config.animated && oldValue !== value) {
743
+ this.animateValue(oldValue, value);
744
+ }
745
+ else {
746
+ this._displayedValue = value;
747
+ this.updateDisplay();
748
+ }
749
+ }
750
+ /**
751
+ * Set the currency code.
752
+ */
753
+ setCurrency(currency) {
754
+ this._config.currency = currency;
755
+ this.updateDisplay();
756
+ }
757
+ async animateValue(from, to) {
758
+ this._animating = true;
759
+ const duration = this._config.animationDuration;
760
+ const startTime = Date.now();
761
+ return new Promise((resolve) => {
762
+ const tick = () => {
763
+ const elapsed = Date.now() - startTime;
764
+ const t = Math.min(elapsed / duration, 1);
765
+ const eased = Easing.easeOutCubic(t);
766
+ this._displayedValue = from + (to - from) * eased;
767
+ this.updateDisplay();
768
+ if (t < 1) {
769
+ requestAnimationFrame(tick);
770
+ }
771
+ else {
772
+ this._displayedValue = to;
773
+ this.updateDisplay();
774
+ this._animating = false;
775
+ resolve();
776
+ }
777
+ };
778
+ requestAnimationFrame(tick);
779
+ });
780
+ }
781
+ updateDisplay() {
782
+ this._valueLabel.setCurrency(this._displayedValue, this._config.currency, this._config.locale);
783
+ }
784
+ layoutLabels() {
785
+ if (this._prefixLabel) {
786
+ this._prefixLabel.y = -14;
787
+ this._valueLabel.y = 14;
788
+ }
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Win amount display with countup animation.
794
+ *
795
+ * Shows a dramatic countup from 0 to the win amount, with optional
796
+ * scale pop effect — typical of slot games.
797
+ *
798
+ * @example
799
+ * ```ts
800
+ * const winDisplay = new WinDisplay({ currency: 'USD' });
801
+ * scene.container.addChild(winDisplay);
802
+ * await winDisplay.showWin(150.50); // countup animation
803
+ * winDisplay.hide();
804
+ * ```
805
+ */
806
+ class WinDisplay extends Container {
807
+ _label;
808
+ _config;
809
+ _cancelCountup = false;
810
+ constructor(config = {}) {
811
+ super();
812
+ this._config = {
813
+ currency: config.currency ?? 'USD',
814
+ locale: config.locale ?? 'en-US',
815
+ countupDuration: config.countupDuration ?? 1500,
816
+ popScale: config.popScale ?? 1.2,
817
+ };
818
+ this._label = new Label({
819
+ text: '',
820
+ style: {
821
+ fontSize: 48,
822
+ fontWeight: 'bold',
823
+ fill: 0xffd700,
824
+ stroke: { color: 0x000000, width: 3 },
825
+ ...config.style,
826
+ },
827
+ });
828
+ this.addChild(this._label);
829
+ this.visible = false;
830
+ }
831
+ /**
832
+ * Show a win with countup animation.
833
+ *
834
+ * @param amount - Win amount
835
+ * @returns Promise that resolves when the animation completes
836
+ */
837
+ async showWin(amount) {
838
+ this.visible = true;
839
+ this._cancelCountup = false;
840
+ this.alpha = 1;
841
+ const duration = this._config.countupDuration;
842
+ const startTime = Date.now();
843
+ // Scale pop
844
+ this.scale.set(0.5);
845
+ return new Promise((resolve) => {
846
+ const tick = () => {
847
+ if (this._cancelCountup) {
848
+ this.displayAmount(amount);
849
+ resolve();
850
+ return;
851
+ }
852
+ const elapsed = Date.now() - startTime;
853
+ const t = Math.min(elapsed / duration, 1);
854
+ const eased = Easing.easeOutCubic(t);
855
+ // Countup
856
+ const current = amount * eased;
857
+ this.displayAmount(current);
858
+ // Scale animation
859
+ const scaleT = Math.min(elapsed / 300, 1);
860
+ const scaleEased = Easing.easeOutBack(scaleT);
861
+ const targetScale = 1;
862
+ this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
863
+ if (t < 1) {
864
+ requestAnimationFrame(tick);
865
+ }
866
+ else {
867
+ this.displayAmount(amount);
868
+ this.scale.set(1);
869
+ resolve();
870
+ }
871
+ };
872
+ requestAnimationFrame(tick);
873
+ });
874
+ }
875
+ /**
876
+ * Skip the countup animation and show the final amount immediately.
877
+ */
878
+ skipCountup(amount) {
879
+ this._cancelCountup = true;
880
+ this.displayAmount(amount);
881
+ this.scale.set(1);
882
+ }
883
+ /**
884
+ * Hide the win display.
885
+ */
886
+ hide() {
887
+ this.visible = false;
888
+ this._label.text = '';
889
+ }
890
+ displayAmount(amount) {
891
+ this._label.setCurrency(amount, this._config.currency, this._config.locale);
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Modal overlay component.
897
+ * Shows content on top of a dark overlay with enter/exit animations.
898
+ *
899
+ * @example
900
+ * ```ts
901
+ * const modal = new Modal({ closeOnOverlay: true });
902
+ * modal.content.addChild(settingsPanel);
903
+ * modal.onClose = () => console.log('Closed');
904
+ * await modal.show(1920, 1080);
905
+ * ```
906
+ */
907
+ class Modal extends Container {
908
+ _overlay;
909
+ _contentContainer;
910
+ _config;
911
+ _showing = false;
912
+ /** Called when the modal is closed */
913
+ onClose;
914
+ constructor(config = {}) {
915
+ super();
916
+ this._config = {
917
+ overlayColor: 0x000000,
918
+ overlayAlpha: 0.7,
919
+ closeOnOverlay: true,
920
+ animationDuration: 300,
921
+ ...config,
922
+ };
923
+ // Overlay
924
+ this._overlay = new Graphics();
925
+ this._overlay.eventMode = 'static';
926
+ this.addChild(this._overlay);
927
+ if (this._config.closeOnOverlay) {
928
+ this._overlay.on('pointertap', () => this.hide());
929
+ }
930
+ // Content container
931
+ this._contentContainer = new Container();
932
+ this.addChild(this._contentContainer);
933
+ this.visible = false;
934
+ }
935
+ /** Content container — add your UI here */
936
+ get content() {
937
+ return this._contentContainer;
938
+ }
939
+ /** Whether the modal is currently showing */
940
+ get isShowing() {
941
+ return this._showing;
942
+ }
943
+ /**
944
+ * Show the modal with animation.
945
+ */
946
+ async show(viewWidth, viewHeight) {
947
+ this._showing = true;
948
+ this.visible = true;
949
+ // Draw overlay to cover full screen
950
+ this._overlay.clear();
951
+ this._overlay.rect(0, 0, viewWidth, viewHeight).fill(this._config.overlayColor);
952
+ this._overlay.alpha = 0;
953
+ // Center content
954
+ this._contentContainer.x = viewWidth / 2;
955
+ this._contentContainer.y = viewHeight / 2;
956
+ this._contentContainer.alpha = 0;
957
+ this._contentContainer.scale.set(0.8);
958
+ // Animate in
959
+ await Promise.all([
960
+ Tween.to(this._overlay, { alpha: this._config.overlayAlpha }, this._config.animationDuration, Easing.easeOutCubic),
961
+ Tween.to(this._contentContainer, { alpha: 1, 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutBack),
962
+ ]);
963
+ }
964
+ /**
965
+ * Hide the modal with animation.
966
+ */
967
+ async hide() {
968
+ if (!this._showing)
969
+ return;
970
+ await Promise.all([
971
+ Tween.to(this._overlay, { alpha: 0 }, this._config.animationDuration * 0.7, Easing.easeInCubic),
972
+ Tween.to(this._contentContainer, { alpha: 0, 'scale.x': 0.8, 'scale.y': 0.8 }, this._config.animationDuration * 0.7, Easing.easeInCubic),
973
+ ]);
974
+ this.visible = false;
975
+ this._showing = false;
976
+ this.onClose?.();
977
+ }
978
+ }
979
+
980
+ const TOAST_COLORS = {
981
+ info: 0x3498db,
982
+ success: 0x27ae60,
983
+ warning: 0xf39c12,
984
+ error: 0xe74c3c,
985
+ };
986
+ /**
987
+ * Toast notification component for displaying transient messages.
988
+ *
989
+ * @example
990
+ * ```ts
991
+ * const toast = new Toast();
992
+ * scene.container.addChild(toast);
993
+ * await toast.show('Connection lost', 'error', 1920, 1080);
994
+ * ```
995
+ */
996
+ class Toast extends Container {
997
+ _bg;
998
+ _text;
999
+ _config;
1000
+ _dismissTimeout = null;
1001
+ constructor(config = {}) {
1002
+ super();
1003
+ this._config = {
1004
+ duration: 3000,
1005
+ bottomOffset: 60,
1006
+ ...config,
1007
+ };
1008
+ this._bg = new Graphics();
1009
+ this.addChild(this._bg);
1010
+ this._text = new Text({
1011
+ text: '',
1012
+ style: {
1013
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
1014
+ fontSize: 16,
1015
+ fill: 0xffffff,
1016
+ },
1017
+ });
1018
+ this._text.anchor.set(0.5);
1019
+ this.addChild(this._text);
1020
+ this.visible = false;
1021
+ }
1022
+ /**
1023
+ * Show a toast message.
1024
+ */
1025
+ async show(message, type = 'info', viewWidth, viewHeight) {
1026
+ // Clear previous dismiss
1027
+ if (this._dismissTimeout) {
1028
+ clearTimeout(this._dismissTimeout);
1029
+ }
1030
+ this._text.text = message;
1031
+ const padding = 20;
1032
+ const width = Math.max(200, this._text.width + padding * 2);
1033
+ const height = 44;
1034
+ const radius = 8;
1035
+ this._bg.clear();
1036
+ this._bg.roundRect(-width / 2, -height / 2, width, height, radius).fill(TOAST_COLORS[type]);
1037
+ this._bg.roundRect(-width / 2, -height / 2, width, height, radius)
1038
+ .fill({ color: 0x000000, alpha: 0.2 });
1039
+ // Position
1040
+ if (viewWidth && viewHeight) {
1041
+ this.x = viewWidth / 2;
1042
+ this.y = viewHeight - this._config.bottomOffset;
1043
+ }
1044
+ this.visible = true;
1045
+ this.alpha = 0;
1046
+ this.y += 20;
1047
+ // Animate in
1048
+ await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
1049
+ // Auto-dismiss
1050
+ if (this._config.duration > 0) {
1051
+ this._dismissTimeout = setTimeout(() => {
1052
+ this.dismiss();
1053
+ }, this._config.duration);
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Dismiss the toast.
1058
+ */
1059
+ async dismiss() {
1060
+ if (!this.visible)
1061
+ return;
1062
+ if (this._dismissTimeout) {
1063
+ clearTimeout(this._dismissTimeout);
1064
+ this._dismissTimeout = null;
1065
+ }
1066
+ await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
1067
+ this.visible = false;
1068
+ }
1069
+ }
1070
+
1071
+ export { BalanceDisplay, Button, Label, Modal, Panel, ProgressBar, Toast, WinDisplay };
1072
+ //# sourceMappingURL=ui.esm.js.map