@energy8platform/game-engine 0.10.11 → 0.12.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 (50) hide show
  1. package/README.md +272 -74
  2. package/dist/index.cjs.js +1322 -296
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.d.ts +369 -46
  5. package/dist/index.esm.js +1323 -298
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/lua.cjs.js +8 -18
  8. package/dist/lua.cjs.js.map +1 -1
  9. package/dist/lua.d.ts +0 -2
  10. package/dist/lua.esm.js +8 -18
  11. package/dist/lua.esm.js.map +1 -1
  12. package/dist/react.cjs.js +2848 -35
  13. package/dist/react.cjs.js.map +1 -1
  14. package/dist/react.d.ts +17 -6
  15. package/dist/react.esm.js +2848 -36
  16. package/dist/react.esm.js.map +1 -1
  17. package/dist/ui.cjs.js +1913 -592
  18. package/dist/ui.cjs.js.map +1 -1
  19. package/dist/ui.d.ts +528 -46
  20. package/dist/ui.esm.js +1911 -594
  21. package/dist/ui.esm.js.map +1 -1
  22. package/dist/vite.cjs.js +1 -11
  23. package/dist/vite.cjs.js.map +1 -1
  24. package/dist/vite.d.ts +1 -1
  25. package/dist/vite.esm.js +1 -11
  26. package/dist/vite.esm.js.map +1 -1
  27. package/package.json +3 -18
  28. package/src/index.ts +3 -3
  29. package/src/lua/LuaEngine.ts +8 -18
  30. package/src/react/applyProps.ts +90 -2
  31. package/src/react/extendAll.ts +29 -6
  32. package/src/react/index.ts +1 -1
  33. package/src/react/jsx.d.ts +249 -0
  34. package/src/react/reconciler.ts +80 -7
  35. package/src/ui/BalanceDisplay.ts +31 -38
  36. package/src/ui/Button.ts +217 -53
  37. package/src/ui/FlexContainer.ts +529 -0
  38. package/src/ui/Label.ts +13 -0
  39. package/src/ui/Layout.ts +86 -87
  40. package/src/ui/Modal.ts +11 -1
  41. package/src/ui/Panel.ts +108 -36
  42. package/src/ui/ProgressBar.ts +85 -31
  43. package/src/ui/ScrollContainer.ts +397 -45
  44. package/src/ui/Slider.ts +241 -0
  45. package/src/ui/Toast.ts +47 -17
  46. package/src/ui/Toggle.ts +201 -0
  47. package/src/ui/WinDisplay.ts +51 -39
  48. package/src/ui/index.ts +9 -11
  49. package/src/ui/view.ts +28 -0
  50. package/src/vite/index.ts +1 -11
@@ -0,0 +1,241 @@
1
+ import { Container, Graphics, FederatedPointerEvent } from 'pixi.js';
2
+ import { resolveView } from './view';
3
+ import type { ViewInput } from './view';
4
+
5
+ export interface SliderConfig {
6
+ /** Minimum value (default: 0) */
7
+ min?: number;
8
+ /** Maximum value (default: 1) */
9
+ max?: number;
10
+ /** Step increment (0 = continuous, default: 0) */
11
+ step?: number;
12
+ /** Initial value (default: min) */
13
+ value?: number;
14
+ /** Track width in pixels (default: 200) */
15
+ width?: number;
16
+ /** Track height in pixels (default: 8) */
17
+ height?: number;
18
+ /** Corner radius for Graphics-based track/fill (default: 4) */
19
+ borderRadius?: number;
20
+ /** Track background color (ignored when trackView provided) */
21
+ trackColor?: number;
22
+ /** Fill bar color (ignored when fillView provided) */
23
+ fillColor?: number;
24
+ /** Handle radius (for Graphics-based handle, default: 12) */
25
+ handleRadius?: number;
26
+ /** Handle color (for Graphics-based handle, ignored when handleView provided) */
27
+ handleColor?: number;
28
+
29
+ /** Custom track background view */
30
+ trackView?: ViewInput;
31
+ /** Custom fill bar view */
32
+ fillView?: ViewInput;
33
+ /** Custom handle view */
34
+ handleView?: ViewInput;
35
+
36
+ /** Called when value changes during drag */
37
+ onUpdate?: (value: number) => void;
38
+ /** Called when drag ends */
39
+ onChange?: (value: number) => void;
40
+ }
41
+
42
+ /**
43
+ * Draggable slider with customizable track, fill, and handle views.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const volume = new Slider({
48
+ * min: 0, max: 1, value: 0.5,
49
+ * width: 200, height: 8,
50
+ * fillColor: 0xffd700,
51
+ * onUpdate: (v) => console.log('Volume:', v),
52
+ * });
53
+ * ```
54
+ */
55
+ export class Slider extends Container {
56
+ readonly __uiComponent = true as const;
57
+
58
+ private _track: Container;
59
+ private _fill: Container;
60
+ private _fillMask: Graphics;
61
+ private _handle: Container;
62
+ private _config: Required<Pick<SliderConfig, 'min' | 'max' | 'step' | 'width' | 'height' | 'borderRadius' | 'trackColor' | 'fillColor' | 'handleRadius' | 'handleColor'>>;
63
+ private _value: number;
64
+ private _dragging = false;
65
+
66
+ onUpdate: ((value: number) => void) | null = null;
67
+ onChange: ((value: number) => void) | null = null;
68
+
69
+ constructor(config: SliderConfig = {}) {
70
+ super();
71
+
72
+ this._config = {
73
+ min: config.min ?? 0,
74
+ max: config.max ?? 1,
75
+ step: config.step ?? 0,
76
+ width: config.width ?? 200,
77
+ height: config.height ?? 8,
78
+ borderRadius: config.borderRadius ?? 4,
79
+ trackColor: config.trackColor ?? 0x333333,
80
+ fillColor: config.fillColor ?? 0xffd700,
81
+ handleRadius: config.handleRadius ?? 12,
82
+ handleColor: config.handleColor ?? 0xffffff,
83
+ };
84
+
85
+ this._value = config.value ?? this._config.min;
86
+ this.onUpdate = config.onUpdate ?? null;
87
+ this.onChange = config.onChange ?? null;
88
+
89
+ const { width, height, borderRadius, trackColor, fillColor, handleRadius, handleColor } = this._config;
90
+
91
+ // Track
92
+ const customTrack = resolveView(config.trackView);
93
+ if (customTrack) {
94
+ customTrack.width = width;
95
+ customTrack.height = height;
96
+ this._track = customTrack;
97
+ } else {
98
+ const g = new Graphics();
99
+ g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
100
+ this._track = g;
101
+ }
102
+ this.addChild(this._track);
103
+
104
+ // Fill
105
+ const customFill = resolveView(config.fillView);
106
+ if (customFill) {
107
+ customFill.width = width;
108
+ customFill.height = height;
109
+ this._fill = customFill;
110
+ } else {
111
+ const g = new Graphics();
112
+ g.roundRect(0, 0, width, height, borderRadius).fill(fillColor);
113
+ this._fill = g;
114
+ }
115
+ this.addChild(this._fill);
116
+
117
+ // Fill mask
118
+ this._fillMask = new Graphics();
119
+ this.addChild(this._fillMask);
120
+ this._fill.mask = this._fillMask;
121
+
122
+ // Handle
123
+ const customHandle = resolveView(config.handleView);
124
+ if (customHandle) {
125
+ this._handle = customHandle;
126
+ } else {
127
+ const g = new Graphics();
128
+ g.circle(0, 0, handleRadius).fill(handleColor);
129
+ this._handle = g;
130
+ }
131
+ this._handle.y = height / 2;
132
+ this.addChild(this._handle);
133
+
134
+ // Interaction
135
+ this.eventMode = 'static';
136
+ this.cursor = 'pointer';
137
+
138
+ // Hit area covers track + handle overflow
139
+ const hitPad = Math.max(handleRadius - height / 2, 0);
140
+ this.hitArea = { contains: (x: number, y: number) => x >= -hitPad && x <= width + hitPad && y >= -hitPad && y <= height + hitPad };
141
+
142
+ this.on('pointerdown', this._onPointerDown, this);
143
+ this.on('globalpointermove', this._onPointerMove, this);
144
+ this.on('pointerup', this._onPointerUp, this);
145
+ this.on('pointerupoutside', this._onPointerUp, this);
146
+
147
+ this._updateVisuals();
148
+ }
149
+
150
+ /** Current value */
151
+ get value(): number {
152
+ return this._value;
153
+ }
154
+
155
+ set value(v: number) {
156
+ const clamped = this._applyStep(Math.max(this._config.min, Math.min(this._config.max, v)));
157
+ if (clamped === this._value) return;
158
+ this._value = clamped;
159
+ this._updateVisuals();
160
+ }
161
+
162
+ get min(): number { return this._config.min; }
163
+ get max(): number { return this._config.max; }
164
+
165
+ /** React reconciler update hook */
166
+ updateConfig(changed: Record<string, any>): void {
167
+ if ('value' in changed) this.value = changed.value;
168
+ if ('min' in changed) { this._config.min = changed.min; this._updateVisuals(); }
169
+ if ('max' in changed) { this._config.max = changed.max; this._updateVisuals(); }
170
+ if ('step' in changed) this._config.step = changed.step;
171
+ if ('onUpdate' in changed) this.onUpdate = changed.onUpdate;
172
+ if ('onChange' in changed) this.onChange = changed.onChange;
173
+ }
174
+
175
+ private _fraction(): number {
176
+ const { min, max } = this._config;
177
+ return max === min ? 0 : (this._value - min) / (max - min);
178
+ }
179
+
180
+ private _applyStep(v: number): number {
181
+ const { step, min } = this._config;
182
+ if (step <= 0) return v;
183
+ return min + Math.round((v - min) / step) * step;
184
+ }
185
+
186
+ private _updateVisuals(): void {
187
+ const frac = this._fraction();
188
+ const w = this._config.width;
189
+ const h = this._config.height;
190
+
191
+ // Update fill mask
192
+ this._fillMask.clear();
193
+ this._fillMask.rect(0, 0, w * frac, h).fill(0xffffff);
194
+
195
+ // Update handle position
196
+ this._handle.x = w * frac;
197
+ }
198
+
199
+ private _valueFromPointer(e: FederatedPointerEvent): number {
200
+ const local = this.toLocal(e.global);
201
+ const frac = Math.max(0, Math.min(1, local.x / this._config.width));
202
+ const { min, max } = this._config;
203
+ return this._applyStep(min + frac * (max - min));
204
+ }
205
+
206
+ private _onPointerDown(e: FederatedPointerEvent): void {
207
+ this._dragging = true;
208
+ const newValue = this._valueFromPointer(e);
209
+ if (newValue !== this._value) {
210
+ this._value = newValue;
211
+ this._updateVisuals();
212
+ this.onUpdate?.(this._value);
213
+ }
214
+ }
215
+
216
+ private _onPointerMove(e: FederatedPointerEvent): void {
217
+ if (!this._dragging) return;
218
+ const newValue = this._valueFromPointer(e);
219
+ if (newValue !== this._value) {
220
+ this._value = newValue;
221
+ this._updateVisuals();
222
+ this.onUpdate?.(this._value);
223
+ }
224
+ }
225
+
226
+ private _onPointerUp(_e: FederatedPointerEvent): void {
227
+ if (!this._dragging) return;
228
+ this._dragging = false;
229
+ this.onChange?.(this._value);
230
+ }
231
+
232
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
233
+ this.off('pointerdown', this._onPointerDown, this);
234
+ this.off('globalpointermove', this._onPointerMove, this);
235
+ this.off('pointerup', this._onPointerUp, this);
236
+ this.off('pointerupoutside', this._onPointerUp, this);
237
+ this.onUpdate = null;
238
+ this.onChange = null;
239
+ super.destroy(options);
240
+ }
241
+ }
package/src/ui/Toast.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Container, Graphics, Text } from 'pixi.js';
2
2
  import { Tween } from '../animation/Tween';
3
3
  import { Easing } from '../animation/Easing';
4
+ import { resolveView } from './view';
5
+ import type { ViewInput } from './view';
4
6
 
5
7
  export type ToastType = 'info' | 'success' | 'warning' | 'error';
6
8
 
@@ -9,6 +11,8 @@ export interface ToastConfig {
9
11
  duration?: number;
10
12
  /** Toast position from bottom */
11
13
  bottomOffset?: number;
14
+ /** Custom background view (string texture name, Texture, or Container). Sized to fit text. */
15
+ backgroundView?: ViewInput;
12
16
  }
13
17
 
14
18
  const TOAST_COLORS: Record<ToastType, number> = {
@@ -29,10 +33,13 @@ const TOAST_COLORS: Record<ToastType, number> = {
29
33
  * ```
30
34
  */
31
35
  export class Toast extends Container {
32
- private _bg: Graphics;
36
+ readonly __uiComponent = true as const;
37
+
38
+ private _bg: Container;
39
+ private _customBg: boolean;
33
40
  private _text: Text;
34
- private _config: Required<ToastConfig>;
35
- private _dismissTimeout: ReturnType<typeof setTimeout> | null = null;
41
+ private _config: Required<Pick<ToastConfig, 'duration' | 'bottomOffset'>>;
42
+ private _dismissPending = false;
36
43
 
37
44
  constructor(config: ToastConfig = {}) {
38
45
  super();
@@ -42,7 +49,9 @@ export class Toast extends Container {
42
49
  bottomOffset: config.bottomOffset ?? 60,
43
50
  };
44
51
 
45
- this._bg = new Graphics();
52
+ const customBg = resolveView(config.backgroundView);
53
+ this._customBg = !!customBg;
54
+ this._bg = customBg ?? new Graphics();
46
55
  this.addChild(this._bg);
47
56
 
48
57
  this._text = new Text({
@@ -68,9 +77,9 @@ export class Toast extends Container {
68
77
  viewWidth?: number,
69
78
  viewHeight?: number,
70
79
  ): Promise<void> {
71
- if (this._dismissTimeout) {
72
- clearTimeout(this._dismissTimeout);
73
- }
80
+ // Cancel any pending dismiss
81
+ Tween.killTweensOf(this);
82
+ this._dismissPending = false;
74
83
 
75
84
  this._text.text = message;
76
85
 
@@ -80,9 +89,17 @@ export class Toast extends Container {
80
89
  const radius = 8;
81
90
 
82
91
  // Draw the background
83
- this._bg.clear();
84
- this._bg.roundRect(-width / 2, -height / 2, width, height, radius);
85
- this._bg.fill(TOAST_COLORS[type]);
92
+ if (this._customBg) {
93
+ this._bg.width = width;
94
+ this._bg.height = height;
95
+ this._bg.x = -width / 2;
96
+ this._bg.y = -height / 2;
97
+ } else {
98
+ const g = this._bg as Graphics;
99
+ g.clear();
100
+ g.roundRect(-width / 2, -height / 2, width, height, radius);
101
+ g.fill(TOAST_COLORS[type]);
102
+ }
86
103
 
87
104
  // Position
88
105
  if (viewWidth && viewHeight) {
@@ -97,9 +114,12 @@ export class Toast extends Container {
97
114
  await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
98
115
 
99
116
  if (this._config.duration > 0) {
100
- this._dismissTimeout = setTimeout(() => {
101
- this.dismiss();
102
- }, this._config.duration);
117
+ this._dismissPending = true;
118
+ await Tween.delay(this._config.duration);
119
+ if (this._dismissPending) {
120
+ this._dismissPending = false;
121
+ await this.dismiss();
122
+ }
103
123
  }
104
124
  }
105
125
 
@@ -109,12 +129,22 @@ export class Toast extends Container {
109
129
  async dismiss(): Promise<void> {
110
130
  if (!this.visible) return;
111
131
 
112
- if (this._dismissTimeout) {
113
- clearTimeout(this._dismissTimeout);
114
- this._dismissTimeout = null;
115
- }
132
+ this._dismissPending = false;
133
+ Tween.killTweensOf(this);
116
134
 
117
135
  await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
118
136
  this.visible = false;
119
137
  }
138
+
139
+ /** React reconciler update hook */
140
+ updateConfig(changed: Record<string, any>): void {
141
+ if ('duration' in changed) this._config.duration = changed.duration;
142
+ if ('bottomOffset' in changed) this._config.bottomOffset = changed.bottomOffset;
143
+ }
144
+
145
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
146
+ this._dismissPending = false;
147
+ Tween.killTweensOf(this);
148
+ super.destroy(options);
149
+ }
120
150
  }
@@ -0,0 +1,201 @@
1
+ import { Container, Graphics } from 'pixi.js';
2
+ import { Tween } from '../animation/Tween';
3
+ import { resolveView } from './view';
4
+ import type { ViewInput } from './view';
5
+
6
+ export interface ToggleConfig {
7
+ /** Initial state (default: false) */
8
+ value?: boolean;
9
+ /** Custom view for the ON state */
10
+ onView?: ViewInput;
11
+ /** Custom view for the OFF state */
12
+ offView?: ViewInput;
13
+ /** Width (for Graphics-based toggle, default: 52) */
14
+ width?: number;
15
+ /** Height (for Graphics-based toggle, default: 28) */
16
+ height?: number;
17
+ /** Track color when ON (ignored when custom views provided) */
18
+ onColor?: number;
19
+ /** Track color when OFF (ignored when custom views provided) */
20
+ offColor?: number;
21
+ /** Handle color (for Graphics-based toggle) */
22
+ handleColor?: number;
23
+ /** Handle radius (for Graphics-based toggle, default: auto) */
24
+ handleRadius?: number;
25
+ /** Animation duration in ms (default: 200) */
26
+ animationDuration?: number;
27
+
28
+ /** Called when value changes */
29
+ onChange?: (value: boolean) => void;
30
+ }
31
+
32
+ /**
33
+ * Toggle switch with two states.
34
+ *
35
+ * Supports custom ON/OFF views or auto-generated Graphics-based toggle.
36
+ * Click to toggle, or use `forceSwitch(value)` programmatically.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const mute = new Toggle({
41
+ * value: false,
42
+ * onColor: 0x22cc22,
43
+ * onChange: (on) => audioManager.mute(!on),
44
+ * });
45
+ * ```
46
+ */
47
+ export class Toggle extends Container {
48
+ readonly __uiComponent = true as const;
49
+
50
+ private _value: boolean;
51
+ private _onView: Container | null = null;
52
+ private _offView: Container | null = null;
53
+ private _handle: Container | null = null;
54
+ private _trackGfx: Graphics | null = null;
55
+ private _config: Required<Pick<ToggleConfig, 'width' | 'height' | 'onColor' | 'offColor' | 'handleColor' | 'handleRadius' | 'animationDuration'>>;
56
+ private _useCustomViews: boolean;
57
+
58
+ onChange: ((value: boolean) => void) | null = null;
59
+
60
+ constructor(config: ToggleConfig = {}) {
61
+ super();
62
+
63
+ this._config = {
64
+ width: config.width ?? 52,
65
+ height: config.height ?? 28,
66
+ onColor: config.onColor ?? 0x22cc22,
67
+ offColor: config.offColor ?? 0x666666,
68
+ handleColor: config.handleColor ?? 0xffffff,
69
+ handleRadius: config.handleRadius ?? 0, // 0 = auto
70
+ animationDuration: config.animationDuration ?? 200,
71
+ };
72
+
73
+ this._value = config.value ?? false;
74
+ this.onChange = config.onChange ?? null;
75
+
76
+ const customOn = resolveView(config.onView);
77
+ const customOff = resolveView(config.offView);
78
+ this._useCustomViews = !!(customOn || customOff);
79
+
80
+ if (this._useCustomViews) {
81
+ // Custom view mode: show/hide ON and OFF views
82
+ if (customOn) {
83
+ this._onView = customOn;
84
+ this._onView.visible = this._value;
85
+ this.addChild(this._onView);
86
+ }
87
+ if (customOff) {
88
+ this._offView = customOff;
89
+ this._offView.visible = !this._value;
90
+ this.addChild(this._offView);
91
+ }
92
+ } else {
93
+ // Graphics mode: track + sliding handle
94
+ const { width, height, handleColor } = this._config;
95
+ const handleRadius = this._config.handleRadius || (height / 2 - 3);
96
+ this._config.handleRadius = handleRadius;
97
+
98
+ this._trackGfx = new Graphics();
99
+ this.addChild(this._trackGfx);
100
+ this._drawTrack();
101
+
102
+ const handle = new Graphics();
103
+ handle.circle(0, 0, handleRadius).fill(handleColor);
104
+ handle.y = height / 2;
105
+ handle.x = this._value ? width - handleRadius - 3 : handleRadius + 3;
106
+ this._handle = handle;
107
+ this.addChild(handle);
108
+ }
109
+
110
+ // Interaction
111
+ this.eventMode = 'static';
112
+ this.cursor = 'pointer';
113
+ this.on('pointertap', this._onTap, this);
114
+ }
115
+
116
+ /** Current toggle state */
117
+ get value(): boolean {
118
+ return this._value;
119
+ }
120
+
121
+ set value(v: boolean) {
122
+ if (v === this._value) return;
123
+ this.forceSwitch(v);
124
+ }
125
+
126
+ /** Programmatically switch to a specific state with animation */
127
+ forceSwitch(value: boolean): void {
128
+ this._value = value;
129
+ this._animateToState();
130
+ }
131
+
132
+ /** React reconciler update hook */
133
+ updateConfig(changed: Record<string, any>): void {
134
+ if ('value' in changed) this.value = changed.value;
135
+ if ('onChange' in changed) this.onChange = changed.onChange;
136
+ if ('animationDuration' in changed) this._config.animationDuration = changed.animationDuration;
137
+ }
138
+
139
+ private _onTap(): void {
140
+ this._value = !this._value;
141
+ this._animateToState();
142
+ this.onChange?.(this._value);
143
+ }
144
+
145
+ private _animateToState(): void {
146
+ const duration = this._config.animationDuration;
147
+
148
+ if (this._useCustomViews) {
149
+ // Custom views: crossfade
150
+ if (this._onView) {
151
+ Tween.killTweensOf(this._onView);
152
+ if (this._value) {
153
+ this._onView.visible = true;
154
+ Tween.to(this._onView, { alpha: 1 }, duration);
155
+ } else {
156
+ Tween.to(this._onView, { alpha: 0 }, duration).then(() => {
157
+ if (this._onView) this._onView.visible = false;
158
+ });
159
+ }
160
+ }
161
+ if (this._offView) {
162
+ Tween.killTweensOf(this._offView);
163
+ if (!this._value) {
164
+ this._offView.visible = true;
165
+ Tween.to(this._offView, { alpha: 1 }, duration);
166
+ } else {
167
+ Tween.to(this._offView, { alpha: 0 }, duration).then(() => {
168
+ if (this._offView) this._offView.visible = false;
169
+ });
170
+ }
171
+ }
172
+ } else {
173
+ // Graphics mode: slide handle + recolor track
174
+ this._drawTrack();
175
+ if (this._handle) {
176
+ const { width } = this._config;
177
+ const handleRadius = this._config.handleRadius;
178
+ const targetX = this._value ? width - handleRadius - 3 : handleRadius + 3;
179
+ Tween.killTweensOf(this._handle);
180
+ Tween.to(this._handle, { x: targetX }, duration);
181
+ }
182
+ }
183
+ }
184
+
185
+ private _drawTrack(): void {
186
+ if (!this._trackGfx) return;
187
+ const { width, height, onColor, offColor } = this._config;
188
+ const radius = height / 2;
189
+ this._trackGfx.clear();
190
+ this._trackGfx.roundRect(0, 0, width, height, radius).fill(this._value ? onColor : offColor);
191
+ }
192
+
193
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
194
+ this.off('pointertap', this._onTap, this);
195
+ if (this._handle) Tween.killTweensOf(this._handle);
196
+ if (this._onView) Tween.killTweensOf(this._onView);
197
+ if (this._offView) Tween.killTweensOf(this._offView);
198
+ this.onChange = null;
199
+ super.destroy(options);
200
+ }
201
+ }
@@ -1,5 +1,6 @@
1
1
  import { Container } from 'pixi.js';
2
2
  import { Label } from './Label';
3
+ import { Tween } from '../animation/Tween';
3
4
  import { Easing } from '../animation/Easing';
4
5
 
5
6
  export interface WinDisplayConfig {
@@ -19,7 +20,7 @@ export interface WinDisplayConfig {
19
20
  * Win amount display with countup animation.
20
21
  *
21
22
  * Shows a dramatic countup from 0 to the win amount, with optional
22
- * scale pop effect — typical of slot games.
23
+ * scale pop effect — typical of slot games. Uses engine Tween system.
23
24
  *
24
25
  * @example
25
26
  * ```ts
@@ -30,9 +31,12 @@ export interface WinDisplayConfig {
30
31
  * ```
31
32
  */
32
33
  export class WinDisplay extends Container {
34
+ readonly __uiComponent = true as const;
35
+
33
36
  private _label: Label;
34
37
  private _config: Required<Pick<WinDisplayConfig, 'currency' | 'locale' | 'countupDuration' | 'popScale'>>;
35
- private _cancelCountup = false;
38
+ /** Internal target for Tween countup */
39
+ private _tweenTarget = { value: 0 };
36
40
 
37
41
  constructor(config: WinDisplayConfig = {}) {
38
42
  super();
@@ -67,54 +71,48 @@ export class WinDisplay extends Container {
67
71
  */
68
72
  async showWin(amount: number): Promise<void> {
69
73
  this.visible = true;
70
- this._cancelCountup = false;
71
74
  this.alpha = 1;
72
75
 
73
- const duration = this._config.countupDuration;
74
- const startTime = Date.now();
76
+ // Cancel any running animation
77
+ Tween.killTweensOf(this._tweenTarget);
78
+ Tween.killTweensOf(this);
75
79
 
76
- // Scale pop
80
+ // Setup countup
81
+ this._tweenTarget.value = 0;
77
82
  this.scale.set(0.5);
78
83
 
79
- return new Promise<void>((resolve) => {
80
- const tick = () => {
81
- if (this._cancelCountup) {
82
- this.displayAmount(amount);
83
- resolve();
84
- return;
85
- }
86
-
87
- const elapsed = Date.now() - startTime;
88
- const t = Math.min(elapsed / duration, 1);
89
- const eased = Easing.easeOutCubic(t);
90
-
91
- // Countup
92
- const current = amount * eased;
93
- this.displayAmount(current);
94
-
95
- // Scale animation
96
- const scaleT = Math.min(elapsed / 300, 1);
97
- const scaleEased = Easing.easeOutBack(scaleT);
98
- const targetScale = 1;
99
- this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
100
-
101
- if (t < 1) {
102
- requestAnimationFrame(tick);
103
- } else {
104
- this.displayAmount(amount);
105
- this.scale.set(1);
106
- resolve();
107
- }
108
- };
109
- requestAnimationFrame(tick);
110
- });
84
+ // Scale pop animation
85
+ const scalePromise = Tween.to(
86
+ this,
87
+ { 'scale.x': 1, 'scale.y': 1 },
88
+ 300,
89
+ Easing.easeOutBack,
90
+ );
91
+
92
+ // Countup animation
93
+ const countupPromise = Tween.to(
94
+ this._tweenTarget,
95
+ { value: amount },
96
+ this._config.countupDuration,
97
+ Easing.easeOutCubic,
98
+ () => {
99
+ this.displayAmount(this._tweenTarget.value);
100
+ },
101
+ );
102
+
103
+ await Promise.all([scalePromise, countupPromise]);
104
+
105
+ // Ensure final value is exact
106
+ this.displayAmount(amount);
107
+ this.scale.set(1);
111
108
  }
112
109
 
113
110
  /**
114
111
  * Skip the countup animation and show the final amount immediately.
115
112
  */
116
113
  skipCountup(amount: number): void {
117
- this._cancelCountup = true;
114
+ Tween.killTweensOf(this._tweenTarget);
115
+ Tween.killTweensOf(this);
118
116
  this.displayAmount(amount);
119
117
  this.scale.set(1);
120
118
  }
@@ -123,6 +121,8 @@ export class WinDisplay extends Container {
123
121
  * Hide the win display.
124
122
  */
125
123
  hide(): void {
124
+ Tween.killTweensOf(this._tweenTarget);
125
+ Tween.killTweensOf(this);
126
126
  this.visible = false;
127
127
  this._label.text = '';
128
128
  }
@@ -130,4 +130,16 @@ export class WinDisplay extends Container {
130
130
  private displayAmount(amount: number): void {
131
131
  this._label.setCurrency(amount, this._config.currency, this._config.locale);
132
132
  }
133
+
134
+ /** React reconciler update hook */
135
+ updateConfig(changed: Record<string, any>): void {
136
+ if ('currency' in changed) this._config.currency = changed.currency;
137
+ if ('locale' in changed) this._config.locale = changed.locale;
138
+ }
139
+
140
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
141
+ Tween.killTweensOf(this._tweenTarget);
142
+ Tween.killTweensOf(this);
143
+ super.destroy(options);
144
+ }
133
145
  }