@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.
- package/README.md +272 -74
- package/dist/index.cjs.js +1322 -296
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +369 -46
- package/dist/index.esm.js +1323 -298
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +8 -18
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +0 -2
- package/dist/lua.esm.js +8 -18
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +2848 -35
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +17 -6
- package/dist/react.esm.js +2848 -36
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +1913 -592
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +528 -46
- package/dist/ui.esm.js +1911 -594
- package/dist/ui.esm.js.map +1 -1
- package/dist/vite.cjs.js +1 -11
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.esm.js +1 -11
- package/dist/vite.esm.js.map +1 -1
- package/package.json +3 -18
- package/src/index.ts +3 -3
- package/src/lua/LuaEngine.ts +8 -18
- package/src/react/applyProps.ts +90 -2
- package/src/react/extendAll.ts +29 -6
- package/src/react/index.ts +1 -1
- package/src/react/jsx.d.ts +249 -0
- package/src/react/reconciler.ts +80 -7
- package/src/ui/BalanceDisplay.ts +31 -38
- package/src/ui/Button.ts +217 -53
- package/src/ui/FlexContainer.ts +529 -0
- package/src/ui/Label.ts +13 -0
- package/src/ui/Layout.ts +86 -87
- package/src/ui/Modal.ts +11 -1
- package/src/ui/Panel.ts +108 -36
- package/src/ui/ProgressBar.ts +85 -31
- package/src/ui/ScrollContainer.ts +397 -45
- package/src/ui/Slider.ts +241 -0
- package/src/ui/Toast.ts +47 -17
- package/src/ui/Toggle.ts +201 -0
- package/src/ui/WinDisplay.ts +51 -39
- package/src/ui/index.ts +9 -11
- package/src/ui/view.ts +28 -0
- package/src/vite/index.ts +1 -11
package/src/ui/Slider.ts
ADDED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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.
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
}
|
package/src/ui/Toggle.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ui/WinDisplay.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
76
|
+
// Cancel any running animation
|
|
77
|
+
Tween.killTweensOf(this._tweenTarget);
|
|
78
|
+
Tween.killTweensOf(this);
|
|
75
79
|
|
|
76
|
-
//
|
|
80
|
+
// Setup countup
|
|
81
|
+
this._tweenTarget.value = 0;
|
|
77
82
|
this.scale.set(0.5);
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
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
|
}
|