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