@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
@@ -1,4 +1,92 @@
1
1
  const RESERVED = new Set(['children', 'key', 'ref']);
2
+ /** Props handled by the reconciler as flex item config, not forwarded to components */
3
+ const FLEX_ITEM_PROPS = new Set(['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude']);
4
+
5
+ // ─── UI Component helpers ────────────────────────────────
6
+
7
+ /**
8
+ * Extract a config object from React props.
9
+ * - Strips reserved keys (children, key, ref) and event props
10
+ * - Unfolds dash-notation into nested objects: `colors-default` → `{ colors: { default: ... } }`
11
+ */
12
+ export function extractConfig(props: Record<string, any>): Record<string, any> {
13
+ const config: Record<string, any> = {};
14
+
15
+ for (const key in props) {
16
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key) || isEventProp(key)) continue;
17
+
18
+ if (key.includes('-')) {
19
+ const parts = key.split('-');
20
+ const root = parts[0];
21
+ const nested = parts.slice(1).join('-');
22
+ if (!config[root] || typeof config[root] !== 'object') {
23
+ config[root] = {};
24
+ }
25
+ config[root][nested] = props[key];
26
+ } else {
27
+ config[key] = props[key];
28
+ }
29
+ }
30
+
31
+ return config;
32
+ }
33
+
34
+ /**
35
+ * Diff two prop sets and return a config object with only changed values.
36
+ * Uses extractConfig format (dash-notation unfolded).
37
+ */
38
+ export function diffConfig(
39
+ newProps: Record<string, any>,
40
+ oldProps: Record<string, any>,
41
+ ): Record<string, any> {
42
+ const changed: Record<string, any> = {};
43
+
44
+ // New or changed props
45
+ for (const key in newProps) {
46
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key) || isEventProp(key)) continue;
47
+ if (newProps[key] !== oldProps[key]) {
48
+ if (key.includes('-')) {
49
+ const parts = key.split('-');
50
+ const root = parts[0];
51
+ const nested = parts.slice(1).join('-');
52
+ if (!changed[root] || typeof changed[root] !== 'object') {
53
+ changed[root] = {};
54
+ }
55
+ changed[root][nested] = newProps[key];
56
+ } else {
57
+ changed[key] = newProps[key];
58
+ }
59
+ }
60
+ }
61
+
62
+ return changed;
63
+ }
64
+
65
+ /**
66
+ * Apply only event props from React props to a PixiJS instance.
67
+ */
68
+ export function applyEventProps(
69
+ instance: any,
70
+ newProps: Record<string, any>,
71
+ oldProps: Record<string, any> = {},
72
+ ): void {
73
+ // Remove old event handlers
74
+ for (const key in oldProps) {
75
+ if (!isEventProp(key) || key in newProps) continue;
76
+ instance[REACT_TO_PIXI_EVENTS[key]] = null;
77
+ }
78
+ // Apply new/changed event handlers + onPress (component-level callback)
79
+ for (const key in newProps) {
80
+ if (key === 'onPress') {
81
+ instance.onPress = newProps[key];
82
+ continue;
83
+ }
84
+ if (!isEventProp(key)) continue;
85
+ if (newProps[key] !== oldProps[key]) {
86
+ instance[REACT_TO_PIXI_EVENTS[key]] = newProps[key];
87
+ }
88
+ }
89
+ }
2
90
 
3
91
  const REACT_TO_PIXI_EVENTS: Record<string, string> = {
4
92
  onClick: 'onclick',
@@ -63,7 +151,7 @@ export function applyProps(
63
151
  ): void {
64
152
  // Remove old props not in newProps
65
153
  for (const key in oldProps) {
66
- if (RESERVED.has(key) || key in newProps) continue;
154
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key) || key in newProps) continue;
67
155
 
68
156
  const pixiEvent = REACT_TO_PIXI_EVENTS[key];
69
157
  if (pixiEvent) {
@@ -83,7 +171,7 @@ export function applyProps(
83
171
 
84
172
  // Apply new props
85
173
  for (const key in newProps) {
86
- if (RESERVED.has(key)) continue;
174
+ if (RESERVED.has(key) || FLEX_ITEM_PROPS.has(key)) continue;
87
175
 
88
176
  const value = newProps[key];
89
177
  const pixiEvent = REACT_TO_PIXI_EVENTS[key];
@@ -14,6 +14,11 @@ import {
14
14
  HTMLText,
15
15
  } from 'pixi.js';
16
16
  import { extend } from './catalogue';
17
+ import {
18
+ Button, Label, Panel, FlexContainer, ProgressBar,
19
+ ScrollContainer, Modal, Toast, BalanceDisplay, WinDisplay, Layout,
20
+ Slider, Toggle,
21
+ } from '../ui';
17
22
 
18
23
  /**
19
24
  * Register all standard PixiJS display objects for JSX use.
@@ -38,14 +43,32 @@ export function extendPixiElements(): void {
38
43
  }
39
44
 
40
45
  /**
41
- * Register @pixi/layout components for JSX use.
42
- * Pass the dynamically imported module:
46
+ * Register all engine UI components for JSX use.
47
+ * Call once at app startup before rendering React scenes that use UI components.
43
48
  *
49
+ * @example
44
50
  * ```ts
45
- * const layout = await import('@pixi/layout/components');
46
- * extendLayoutElements(layout);
51
+ * extendPixiElements();
52
+ * extendUIElements();
53
+ *
54
+ * // Now you can use:
55
+ * // <button text="SPIN" onPress={handler} />
56
+ * // <flexContainer direction="row" gap={16}>...</flexContainer>
57
+ * // <label text="Hello" style-fontSize={24} />
47
58
  * ```
48
59
  */
49
- export function extendLayoutElements(layoutModule: Record<string, any>): void {
50
- extend(layoutModule);
60
+ export function extendUIElements(): void {
61
+ extend({
62
+ Button, Label, Panel, FlexContainer, ProgressBar,
63
+ ScrollContainer, Modal, Toast, BalanceDisplay, WinDisplay, Layout,
64
+ Slider, Toggle,
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Register additional custom components for JSX use.
70
+ * Pass an object mapping component names to their constructors.
71
+ */
72
+ export function extendCustomElements(components: Record<string, any>): void {
73
+ extend(components);
51
74
  }
@@ -4,7 +4,7 @@ export type { PixiRoot } from './createPixiRoot';
4
4
 
5
5
  // Catalogue
6
6
  export { extend } from './catalogue';
7
- export { extendPixiElements, extendLayoutElements } from './extendAll';
7
+ export { extendPixiElements, extendUIElements, extendCustomElements } from './extendAll';
8
8
 
9
9
  // Scene
10
10
  export { ReactScene } from './ReactScene';
@@ -0,0 +1,249 @@
1
+ /**
2
+ * JSX IntrinsicElements type declarations for the PixiJS React reconciler.
3
+ *
4
+ * Provides TypeScript autocompletion and type safety for:
5
+ * - Standard PixiJS elements: <container>, <sprite>, <text>, <graphics>
6
+ * - Engine UI components: <button>, <label>, <panel>, <flexContainer>, etc.
7
+ *
8
+ * Dash-notation is supported for nested config objects:
9
+ * <button colors-default={0xff0000} colors-hover={0x00ff00} />
10
+ */
11
+
12
+ import type { Container, Texture, TextStyle } from 'pixi.js';
13
+ import type { ViewInput } from '../ui/view';
14
+ import type { ButtonConfig, ButtonState } from '../ui/Button';
15
+ import type { LabelConfig } from '../ui/Label';
16
+ import type { PanelConfig } from '../ui/Panel';
17
+ import type { FlexContainerConfig, FlexItemConfig, AlignSelf } from '../ui/FlexContainer';
18
+ import type { ProgressBarConfig } from '../ui/ProgressBar';
19
+ import type { ScrollContainerConfig } from '../ui/ScrollContainer';
20
+ import type { ModalConfig } from '../ui/Modal';
21
+ import type { ToastConfig } from '../ui/Toast';
22
+ import type { BalanceDisplayConfig } from '../ui/BalanceDisplay';
23
+ import type { WinDisplayConfig } from '../ui/WinDisplay';
24
+ import type { LayoutConfig } from '../ui/Layout';
25
+ import type { SliderConfig } from '../ui/Slider';
26
+ import type { ToggleConfig } from '../ui/Toggle';
27
+
28
+ // ─── Event props ─────────────────────────────────────────
29
+
30
+ interface PixiEventProps {
31
+ onClick?: (e: any) => void;
32
+ onPointerDown?: (e: any) => void;
33
+ onPointerUp?: (e: any) => void;
34
+ onPointerMove?: (e: any) => void;
35
+ onPointerOver?: (e: any) => void;
36
+ onPointerOut?: (e: any) => void;
37
+ onPointerEnter?: (e: any) => void;
38
+ onPointerLeave?: (e: any) => void;
39
+ onPointerCancel?: (e: any) => void;
40
+ onPointerTap?: (e: any) => void;
41
+ onPointerUpOutside?: (e: any) => void;
42
+ onMouseDown?: (e: any) => void;
43
+ onMouseUp?: (e: any) => void;
44
+ onMouseMove?: (e: any) => void;
45
+ onMouseOver?: (e: any) => void;
46
+ onMouseOut?: (e: any) => void;
47
+ onWheel?: (e: any) => void;
48
+ onTap?: (e: any) => void;
49
+ onRightClick?: (e: any) => void;
50
+ }
51
+
52
+ // ─── Base container props (shared by all elements) ───────
53
+
54
+ interface BaseProps extends PixiEventProps {
55
+ key?: React.Key;
56
+ ref?: React.Ref<any>;
57
+ children?: React.ReactNode;
58
+
59
+ // Container props
60
+ x?: number;
61
+ y?: number;
62
+ width?: number;
63
+ height?: number;
64
+ alpha?: number;
65
+ visible?: boolean;
66
+ rotation?: number;
67
+ angle?: number;
68
+ zIndex?: number;
69
+ label?: string;
70
+ cursor?: string;
71
+ eventMode?: 'auto' | 'none' | 'passive' | 'static' | 'dynamic';
72
+
73
+ // Nested via dash-notation
74
+ 'scale-x'?: number;
75
+ 'scale-y'?: number;
76
+ 'pivot-x'?: number;
77
+ 'pivot-y'?: number;
78
+ 'position-x'?: number;
79
+ 'position-y'?: number;
80
+ 'anchor-x'?: number;
81
+ 'anchor-y'?: number;
82
+
83
+ // Allow scale as number (uniform)
84
+ scale?: number | { x: number; y: number };
85
+
86
+ // Flex item props (used when child of <flexContainer>)
87
+ flexGrow?: number;
88
+ flexShrink?: number;
89
+ layoutWidth?: number;
90
+ layoutHeight?: number;
91
+ alignSelf?: AlignSelf;
92
+ flexExclude?: boolean;
93
+ }
94
+
95
+ // ─── PixiJS primitive elements ───────────────────────────
96
+
97
+ interface SpriteProps extends BaseProps {
98
+ texture?: string | Texture;
99
+ anchor?: number | { x: number; y: number };
100
+ tint?: number;
101
+ }
102
+
103
+ interface TextProps extends BaseProps {
104
+ text?: string;
105
+ style?: Partial<TextStyle>;
106
+ anchor?: number | { x: number; y: number };
107
+ // Dash-notation for style
108
+ 'style-fontSize'?: number;
109
+ 'style-fill'?: number | string;
110
+ 'style-fontFamily'?: string;
111
+ 'style-fontWeight'?: string;
112
+ 'style-letterSpacing'?: number;
113
+ }
114
+
115
+ interface GraphicsProps extends BaseProps {
116
+ /** Draw function: receives the Graphics instance, called on mount and updates */
117
+ draw?: (g: any) => void;
118
+ }
119
+
120
+ // ─── Engine UI component props ───────────────────────────
121
+
122
+ interface ButtonComponentProps extends BaseProps, Omit<ButtonConfig, 'colors' | 'textStyle'> {
123
+ // Full object form
124
+ colors?: Partial<Record<ButtonState, number>>;
125
+ textStyle?: Record<string, unknown>;
126
+ // Custom views (ViewInput: string texture name, Texture, or Container)
127
+ defaultView?: ViewInput;
128
+ hoverView?: ViewInput;
129
+ pressedView?: ViewInput;
130
+ disabledView?: ViewInput;
131
+ // Dash-notation for nested objects
132
+ 'colors-default'?: number;
133
+ 'colors-hover'?: number;
134
+ 'colors-pressed'?: number;
135
+ 'colors-disabled'?: number;
136
+ 'textStyle-fontSize'?: number;
137
+ 'textStyle-fill'?: number | string;
138
+ 'textStyle-fontFamily'?: string;
139
+ 'textStyle-fontWeight'?: string;
140
+ 'textStyle-fontStyle'?: string;
141
+ 'textStyle-letterSpacing'?: number;
142
+ // Component callbacks
143
+ onPress?: () => void;
144
+ }
145
+
146
+ interface LabelComponentProps extends BaseProps, Omit<LabelConfig, 'style'> {
147
+ style?: Partial<TextStyle>;
148
+ 'style-fontSize'?: number;
149
+ 'style-fill'?: number | string;
150
+ 'style-fontFamily'?: string;
151
+ 'style-fontWeight'?: string;
152
+ 'style-letterSpacing'?: number;
153
+ }
154
+
155
+ interface PanelComponentProps extends BaseProps, Omit<PanelConfig, 'layout'> {
156
+ layout?: Partial<FlexContainerConfig>;
157
+ }
158
+
159
+ interface FlexContainerComponentProps extends BaseProps, FlexContainerConfig {
160
+ padding?: number | [number, number, number, number];
161
+ }
162
+
163
+ interface ProgressBarComponentProps extends BaseProps, ProgressBarConfig {
164
+ progress?: number;
165
+ /** Custom track background (string texture name, Texture, or Container) */
166
+ trackView?: ViewInput;
167
+ /** Custom fill bar */
168
+ fillView?: ViewInput;
169
+ }
170
+
171
+ interface ScrollContainerComponentProps extends BaseProps, Omit<ScrollContainerConfig, 'width' | 'height'> {
172
+ width: number;
173
+ height: number;
174
+ /** Show scrollbar indicator */
175
+ scrollbar?: boolean;
176
+ /** Custom scrollbar thumb view */
177
+ thumbView?: ViewInput;
178
+ scrollbarWidth?: number;
179
+ scrollbarPadding?: number;
180
+ scrollbarColor?: number;
181
+ scrollbarAlpha?: number;
182
+ }
183
+
184
+ interface ModalComponentProps extends BaseProps, ModalConfig {
185
+ onClose?: () => void;
186
+ }
187
+
188
+ interface ToastComponentProps extends BaseProps, ToastConfig {
189
+ /** Custom background view */
190
+ backgroundView?: ViewInput;
191
+ }
192
+
193
+ interface BalanceDisplayComponentProps extends BaseProps, BalanceDisplayConfig {
194
+ /** Current balance value */
195
+ value?: number;
196
+ }
197
+
198
+ interface WinDisplayComponentProps extends BaseProps, WinDisplayConfig {}
199
+
200
+ interface LayoutComponentProps extends BaseProps, LayoutConfig {}
201
+
202
+ interface SliderComponentProps extends BaseProps, Omit<SliderConfig, 'width' | 'height'> {
203
+ width?: number;
204
+ height?: number;
205
+ onUpdate?: (value: number) => void;
206
+ onChange?: (value: number) => void;
207
+ }
208
+
209
+ interface ToggleComponentProps extends BaseProps, Omit<ToggleConfig, 'width' | 'height'> {
210
+ width?: number;
211
+ height?: number;
212
+ onView?: ViewInput;
213
+ offView?: ViewInput;
214
+ onChange?: (value: boolean) => void;
215
+ }
216
+
217
+ // ─── JSX IntrinsicElements ───────────────────────────────
218
+
219
+ declare global {
220
+ namespace JSX {
221
+ interface IntrinsicElements {
222
+ // PixiJS primitives
223
+ container: BaseProps;
224
+ sprite: SpriteProps;
225
+ text: TextProps;
226
+ graphics: GraphicsProps;
227
+ animatedSprite: BaseProps & { textures?: Texture[]; animationSpeed?: number; loop?: boolean; playing?: boolean };
228
+ nineSliceSprite: BaseProps & { texture?: string | Texture; leftWidth?: number; topHeight?: number; rightWidth?: number; bottomHeight?: number };
229
+ tilingSprite: BaseProps & { texture?: string | Texture; tilePosition?: { x: number; y: number } };
230
+
231
+ // Engine UI components
232
+ button: ButtonComponentProps;
233
+ label: LabelComponentProps;
234
+ panel: PanelComponentProps;
235
+ flexContainer: FlexContainerComponentProps;
236
+ progressBar: ProgressBarComponentProps;
237
+ scrollContainer: ScrollContainerComponentProps;
238
+ modal: ModalComponentProps;
239
+ toast: ToastComponentProps;
240
+ balanceDisplay: BalanceDisplayComponentProps;
241
+ winDisplay: WinDisplayComponentProps;
242
+ layout: LayoutComponentProps;
243
+ slider: SliderComponentProps;
244
+ toggle: ToggleComponentProps;
245
+ }
246
+ }
247
+ }
248
+
249
+ export {};
@@ -2,7 +2,34 @@ import Reconciler from 'react-reconciler';
2
2
  import { DefaultEventPriority } from 'react-reconciler/constants';
3
3
  import { Container } from 'pixi.js';
4
4
  import { catalogue } from './catalogue';
5
- import { applyProps, hasEventProps } from './applyProps';
5
+ import { applyProps, hasEventProps, extractConfig, diffConfig, applyEventProps } from './applyProps';
6
+ import { FlexContainer } from '../ui/FlexContainer';
7
+ import type { FlexItemConfig } from '../ui/FlexContainer';
8
+
9
+ /** Flex item prop names that should be forwarded to _flexConfig on the child */
10
+ const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude'] as const;
11
+
12
+ /** Extract FlexItemConfig from props if any flex item props are present */
13
+ function extractFlexItemConfig(props: Record<string, any>): FlexItemConfig | undefined {
14
+ let config: FlexItemConfig | undefined;
15
+ for (const key of FLEX_ITEM_PROPS) {
16
+ if (key in props) {
17
+ if (!config) config = {};
18
+ (config as any)[key] = props[key];
19
+ }
20
+ }
21
+ return config;
22
+ }
23
+
24
+ /** Apply flex item config to a child being added to a FlexContainer */
25
+ function addChildToFlex(parent: FlexContainer, child: Container & { _flexConfig?: FlexItemConfig }): void {
26
+ const flexConfig = child._flexConfig;
27
+ if (flexConfig && Object.keys(flexConfig).length > 0) {
28
+ parent.addFlexChild(child, flexConfig);
29
+ } else {
30
+ parent.addChild(child);
31
+ }
32
+ }
6
33
 
7
34
  function toPascalCase(str: string): string {
8
35
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -37,14 +64,29 @@ const hostConfig: Reconciler.HostConfig<
37
64
  `Call extend({ ${name} }) before rendering.`,
38
65
  );
39
66
  }
40
- const instance = new Ctor();
41
- applyProps(instance, props);
42
67
 
43
- // Enable interactivity if any event prop is present
68
+ let instance;
69
+ if (Ctor.prototype.__uiComponent) {
70
+ // Config-based UI component: pass props as constructor config
71
+ const config = extractConfig(props);
72
+ instance = new Ctor(config);
73
+ applyEventProps(instance, props);
74
+ } else {
75
+ // Standard PixiJS element
76
+ instance = new Ctor();
77
+ applyProps(instance, props);
78
+ }
79
+
44
80
  if (hasEventProps(props) && instance.eventMode === 'auto') {
45
81
  instance.eventMode = 'static';
46
82
  }
47
83
 
84
+ // Store flex item config for when this child is added to a FlexContainer parent
85
+ const flexItemConfig = extractFlexItemConfig(props);
86
+ if (flexItemConfig) {
87
+ instance._flexConfig = { ...instance._flexConfig, ...flexItemConfig };
88
+ }
89
+
48
90
  return instance;
49
91
  },
50
92
 
@@ -55,11 +97,23 @@ const hostConfig: Reconciler.HostConfig<
55
97
  },
56
98
 
57
99
  appendInitialChild(parent, child) {
58
- if (child instanceof Container) parent.addChild(child);
100
+ if (child instanceof Container) {
101
+ if (parent instanceof FlexContainer) {
102
+ addChildToFlex(parent, child);
103
+ } else {
104
+ parent.addChild(child);
105
+ }
106
+ }
59
107
  },
60
108
 
61
109
  appendChild(parent, child) {
62
- if (child instanceof Container) parent.addChild(child);
110
+ if (child instanceof Container) {
111
+ if (parent instanceof FlexContainer) {
112
+ addChildToFlex(parent, child);
113
+ } else {
114
+ parent.addChild(child);
115
+ }
116
+ }
63
117
  },
64
118
 
65
119
  appendChildToContainer(container, child) {
@@ -97,7 +151,26 @@ const hostConfig: Reconciler.HostConfig<
97
151
  },
98
152
 
99
153
  commitUpdate(instance, _updatePayload, _type, oldProps, newProps) {
100
- applyProps(instance, newProps, oldProps);
154
+ if (instance.__uiComponent && typeof instance.updateConfig === 'function') {
155
+ const changed = diffConfig(newProps, oldProps);
156
+ if (Object.keys(changed).length > 0) {
157
+ instance.updateConfig(changed);
158
+ }
159
+ applyEventProps(instance, newProps, oldProps);
160
+ } else {
161
+ applyProps(instance, newProps, oldProps);
162
+ }
163
+
164
+ // Update flex item config if parent is FlexContainer
165
+ const newFlexConfig = extractFlexItemConfig(newProps);
166
+ const oldFlexConfig = extractFlexItemConfig(oldProps);
167
+ if (newFlexConfig || oldFlexConfig) {
168
+ instance._flexConfig = { ...instance._flexConfig, ...newFlexConfig };
169
+ // Trigger parent relayout
170
+ if (instance.parent instanceof FlexContainer) {
171
+ instance.parent.updateLayout();
172
+ }
173
+ }
101
174
 
102
175
  if (hasEventProps(newProps) && instance.eventMode === 'auto') {
103
176
  instance.eventMode = 'static';
@@ -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 BalanceDisplayConfig {
@@ -23,7 +24,7 @@ export interface BalanceDisplayConfig {
23
24
  * Reactive balance display component.
24
25
  *
25
26
  * Automatically formats currency and can animate value changes
26
- * with a smooth countup/countdown effect.
27
+ * with a smooth countup/countdown effect using engine Tween.
27
28
  *
28
29
  * @example
29
30
  * ```ts
@@ -35,13 +36,15 @@ export interface BalanceDisplayConfig {
35
36
  * ```
36
37
  */
37
38
  export class BalanceDisplay extends Container {
39
+ readonly __uiComponent = true as const;
40
+
38
41
  private _prefixLabel: Label | null = null;
39
42
  private _valueLabel: Label;
40
43
  private _config: Required<Pick<BalanceDisplayConfig, 'currency' | 'locale' | 'animated' | 'animationDuration'>>;
41
44
  private _currentValue = 0;
42
45
  private _displayedValue = 0;
43
- private _animating = false;
44
- private _animationCancelled = false;
46
+ /** Internal target for Tween animation */
47
+ private _tweenTarget = { value: 0 };
45
48
 
46
49
  constructor(config: BalanceDisplayConfig = {}) {
47
50
  super();
@@ -111,42 +114,21 @@ export class BalanceDisplay extends Container {
111
114
  this.updateDisplay();
112
115
  }
113
116
 
114
- private async animateValue(from: number, to: number): Promise<void> {
115
- if (this._animating) {
116
- this._animationCancelled = true;
117
- }
118
-
119
- this._animating = true;
120
- this._animationCancelled = false;
121
- const duration = this._config.animationDuration;
122
- const startTime = Date.now();
123
-
124
- return new Promise<void>((resolve) => {
125
- const tick = () => {
126
- if (this._animationCancelled) {
127
- this._animating = false;
128
- resolve();
129
- return;
130
- }
131
-
132
- const elapsed = Date.now() - startTime;
133
- const t = Math.min(elapsed / duration, 1);
134
- const eased = Easing.easeOutCubic(t);
135
-
136
- this._displayedValue = from + (to - from) * eased;
117
+ private animateValue(from: number, to: number): void {
118
+ // Cancel any running animation
119
+ Tween.killTweensOf(this._tweenTarget);
120
+
121
+ this._tweenTarget.value = from;
122
+ Tween.to(
123
+ this._tweenTarget,
124
+ { value: to },
125
+ this._config.animationDuration,
126
+ Easing.easeOutCubic,
127
+ () => {
128
+ this._displayedValue = this._tweenTarget.value;
137
129
  this.updateDisplay();
138
-
139
- if (t < 1) {
140
- requestAnimationFrame(tick);
141
- } else {
142
- this._displayedValue = to;
143
- this.updateDisplay();
144
- this._animating = false;
145
- resolve();
146
- }
147
- };
148
- requestAnimationFrame(tick);
149
- });
130
+ },
131
+ );
150
132
  }
151
133
 
152
134
  private updateDisplay(): void {
@@ -163,4 +145,15 @@ export class BalanceDisplay extends Container {
163
145
  this._valueLabel.y = 14;
164
146
  }
165
147
  }
148
+
149
+ /** React reconciler update hook */
150
+ updateConfig(changed: Record<string, any>): void {
151
+ if ('value' in changed) this.setValue(changed.value);
152
+ if ('currency' in changed) this.setCurrency(changed.currency);
153
+ }
154
+
155
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
156
+ Tween.killTweensOf(this._tweenTarget);
157
+ super.destroy(options);
158
+ }
166
159
  }