@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
package/src/ui/Layout.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Container } from 'pixi.js';
2
- import type { LayoutStyles } from '@pixi/layout';
2
+ import { FlexContainer } from './FlexContainer';
3
+ import type { FlexDirection, AlignItems, FlexItemConfig } from './FlexContainer';
3
4
 
4
5
  // ─── Types ───────────────────────────────────────────────
5
6
 
@@ -41,63 +42,17 @@ export interface LayoutConfig {
41
42
 
42
43
  // ─── Helpers ─────────────────────────────────────────────
43
44
 
44
- const ALIGNMENT_MAP: Record<LayoutAlignment, LayoutStyles['alignItems']> = {
45
- start: 'flex-start',
46
- center: 'center',
47
- end: 'flex-end',
48
- stretch: 'stretch',
49
- };
50
-
51
- function normalizePadding(
52
- padding: number | [number, number, number, number],
53
- ): [number, number, number, number] {
54
- if (typeof padding === 'number') return [padding, padding, padding, padding];
55
- return padding;
56
- }
57
-
58
- function directionToFlexStyles(
59
- direction: LayoutDirection,
60
- maxWidth: number,
61
- ): Partial<LayoutStyles> {
45
+ function directionToFlex(direction: LayoutDirection): { direction: FlexDirection; wrap: boolean } {
62
46
  switch (direction) {
63
- case 'horizontal':
64
- return { flexDirection: 'row', flexWrap: 'nowrap' };
65
- case 'vertical':
66
- return { flexDirection: 'column', flexWrap: 'nowrap' };
67
- case 'grid':
68
- return { flexDirection: 'row', flexWrap: 'wrap' };
69
- case 'wrap':
70
- return {
71
- flexDirection: 'row',
72
- flexWrap: 'wrap',
73
- ...(maxWidth < Infinity ? { maxWidth } : {}),
74
- };
47
+ case 'horizontal': return { direction: 'row', wrap: false };
48
+ case 'vertical': return { direction: 'column', wrap: false };
49
+ case 'grid': return { direction: 'row', wrap: true };
50
+ case 'wrap': return { direction: 'row', wrap: true };
75
51
  }
76
52
  }
77
53
 
78
- function buildLayoutStyles(config: {
79
- direction: LayoutDirection;
80
- gap: number;
81
- alignment: LayoutAlignment;
82
- columns: number;
83
- padding: [number, number, number, number];
84
- maxWidth: number;
85
- }): LayoutStyles {
86
- const [pt, pr, pb, pl] = config.padding;
87
-
88
- return {
89
- ...directionToFlexStyles(config.direction, config.maxWidth),
90
- gap: config.gap,
91
- alignItems: ALIGNMENT_MAP[config.alignment],
92
- paddingTop: pt,
93
- paddingRight: pr,
94
- paddingBottom: pb,
95
- paddingLeft: pl,
96
- };
97
- }
98
-
99
54
  /**
100
- * Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
55
+ * Responsive layout container powered by a lightweight built-in flex layout solver.
101
56
  *
102
57
  * Supports horizontal, vertical, grid, and wrap layout modes with
103
58
  * alignment, padding, gap, and viewport-anchor positioning.
@@ -124,14 +79,17 @@ function buildLayoutStyles(config: {
124
79
  * ```
125
80
  */
126
81
  export class Layout extends Container {
82
+ readonly __uiComponent = true as const;
83
+
127
84
  private _layoutConfig: Required<Pick<LayoutConfig, 'direction' | 'gap' | 'alignment' | 'autoLayout' | 'columns'>>;
128
- private _padding: [number, number, number, number];
85
+ private _padding: number | [number, number, number, number];
129
86
  private _anchor: LayoutAnchor;
130
87
  private _maxWidth: number;
131
88
  private _breakpoints: [number, Partial<LayoutConfig>][];
132
89
  private _items: Container[] = [];
133
90
  private _viewportWidth = 0;
134
91
  private _viewportHeight = 0;
92
+ private _flex: FlexContainer;
135
93
 
136
94
  constructor(config: LayoutConfig = {}) {
137
95
  super();
@@ -144,7 +102,7 @@ export class Layout extends Container {
144
102
  columns: config.columns ?? 2,
145
103
  };
146
104
 
147
- this._padding = normalizePadding(config.padding ?? 0);
105
+ this._padding = config.padding ?? 0;
148
106
  this._anchor = config.anchor ?? 'top-left';
149
107
  this._maxWidth = config.maxWidth ?? Infinity;
150
108
 
@@ -154,16 +112,22 @@ export class Layout extends Container {
154
112
  .sort((a, b) => a[0] - b[0])
155
113
  : [];
156
114
 
115
+ // Create internal FlexContainer
116
+ this._flex = new FlexContainer();
117
+ super.addChild(this._flex);
118
+
157
119
  this.applyLayoutStyles();
158
120
  }
159
121
 
160
122
  /** Add an item to the layout */
161
123
  addItem(child: Container): this {
162
124
  this._items.push(child);
163
- this.addChild(child);
164
125
 
165
- if (this._layoutConfig.direction === 'grid') {
166
- this.applyGridChildWidth(child);
126
+ const flexConfig = this.buildFlexItemConfig(child);
127
+ this._flex.addFlexChild(child, flexConfig);
128
+
129
+ if (this._layoutConfig.autoLayout) {
130
+ this.applyLayoutStyles();
167
131
  }
168
132
 
169
133
  return this;
@@ -174,16 +138,14 @@ export class Layout extends Container {
174
138
  const idx = this._items.indexOf(child);
175
139
  if (idx !== -1) {
176
140
  this._items.splice(idx, 1);
177
- this.removeChild(child);
141
+ this._flex.removeFlexChild(child);
178
142
  }
179
143
  return this;
180
144
  }
181
145
 
182
146
  /** Remove all items */
183
147
  clearItems(): this {
184
- for (const item of this._items) {
185
- this.removeChild(item);
186
- }
148
+ this._flex.clearFlexChildren();
187
149
  this._items.length = 0;
188
150
  return this;
189
151
  }
@@ -209,48 +171,68 @@ export class Layout extends Container {
209
171
  const direction = effective.direction ?? this._layoutConfig.direction;
210
172
  const gap = effective.gap ?? this._layoutConfig.gap;
211
173
  const alignment = effective.alignment ?? this._layoutConfig.alignment;
212
- const columns = effective.columns ?? this._layoutConfig.columns;
213
- const padding = effective.padding !== undefined
214
- ? normalizePadding(effective.padding)
215
- : this._padding;
174
+ const padding = effective.padding ?? this._padding;
216
175
  const maxWidth = effective.maxWidth ?? this._maxWidth;
217
176
 
218
- const styles = buildLayoutStyles({ direction, gap, alignment, columns, padding, maxWidth });
219
- this.layout = styles;
177
+ const { direction: flexDir, wrap } = directionToFlex(direction);
178
+
179
+ this._flex.setDirection(flexDir);
180
+ this._flex.setJustifyContent('start');
181
+ this._flex.setAlignItems(alignment as AlignItems);
182
+ this._flex.setGap(gap);
183
+ this._flex.setPadding(padding);
184
+
185
+ // Wrap and maxWidth
186
+ if (wrap) {
187
+ (this._flex as any)._config.flexWrap = true;
188
+ if (direction === 'grid' && maxWidth < Infinity) {
189
+ (this._flex as any)._maxWidth = maxWidth;
190
+ } else if (direction === 'grid') {
191
+ // For grid without explicit maxWidth, we don't constrain wrapping —
192
+ // each child gets a proportional width via flexConfig
193
+ }
194
+ if (maxWidth < Infinity) {
195
+ (this._flex as any)._maxWidth = maxWidth;
196
+ }
197
+ } else {
198
+ (this._flex as any)._config.flexWrap = false;
199
+ }
220
200
 
201
+ // Update grid child widths
221
202
  if (direction === 'grid') {
222
203
  for (const item of this._items) {
223
- this.applyGridChildWidth(item);
204
+ const flexConfig = this.buildFlexItemConfig(item);
205
+ (item as any)._flexConfig = flexConfig;
224
206
  }
225
207
  }
208
+
209
+ // Set explicit size if we have viewport dimensions
210
+ if (this._viewportWidth > 0 && this._viewportHeight > 0) {
211
+ this._flex.resize(this._viewportWidth, this._viewportHeight);
212
+ } else {
213
+ this._flex.updateLayout();
214
+ }
226
215
  }
227
216
 
228
- private applyGridChildWidth(child: Container): void {
217
+ private buildFlexItemConfig(_child: Container): FlexItemConfig | undefined {
229
218
  const effective = this.resolveConfig();
219
+ const direction = effective.direction ?? this._layoutConfig.direction;
230
220
  const columns = effective.columns ?? this._layoutConfig.columns;
231
- const gap = effective.gap ?? this._layoutConfig.gap;
232
221
 
233
- // Account for gaps between columns: total gap space = gap * (columns - 1)
234
- // Each column gets: (100% - total_gap) / columns
235
- // We use flexBasis + flexGrow to let Yoga handle the math when gap > 0
236
- const styles: Record<string, unknown> = gap > 0
237
- ? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
238
- : { width: `${(100 / columns).toFixed(2)}%` };
239
-
240
- if (child._layout) {
241
- child._layout.setStyle(styles);
242
- } else {
243
- child.layout = styles;
222
+ if (direction === 'grid' && columns > 0) {
223
+ // For grid, give each item a proportional width
224
+ // The actual pixel width will be computed during layout
225
+ return { flexGrow: 1 };
244
226
  }
227
+
228
+ return undefined;
245
229
  }
246
230
 
247
231
  private applyAnchor(): void {
248
232
  const anchor = this.resolveConfig().anchor ?? this._anchor;
249
233
  if (this._viewportWidth === 0 || this._viewportHeight === 0) return;
250
234
 
251
- const bounds = this.getLocalBounds();
252
- const contentW = bounds.width * this.scale.x;
253
- const contentH = bounds.height * this.scale.y;
235
+ const { width: contentW, height: contentH } = this._flex.getContentSize();
254
236
  const vw = this._viewportWidth;
255
237
  const vh = this._viewportHeight;
256
238
 
@@ -273,8 +255,8 @@ export class Layout extends Container {
273
255
  anchorY = (vh - contentH) / 2;
274
256
  }
275
257
 
276
- this.x = anchorX - bounds.x * this.scale.x;
277
- this.y = anchorY - bounds.y * this.scale.y;
258
+ this.x = anchorX;
259
+ this.y = anchorY;
278
260
  }
279
261
 
280
262
  private resolveConfig(): Partial<LayoutConfig> {
@@ -289,4 +271,21 @@ export class Layout extends Container {
289
271
  }
290
272
  return {};
291
273
  }
274
+
275
+ /** React reconciler update hook */
276
+ updateConfig(changed: Record<string, any>): void {
277
+ if ('direction' in changed) this._layoutConfig.direction = changed.direction;
278
+ if ('gap' in changed) this._layoutConfig.gap = changed.gap;
279
+ if ('alignment' in changed) this._layoutConfig.alignment = changed.alignment;
280
+ if ('anchor' in changed) this._anchor = changed.anchor;
281
+ if ('padding' in changed) this._padding = changed.padding;
282
+ if ('columns' in changed) this._layoutConfig.columns = changed.columns;
283
+ this.applyLayoutStyles();
284
+ if (this._viewportWidth > 0) this.applyAnchor();
285
+ }
286
+
287
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
288
+ this._items.length = 0;
289
+ super.destroy(options);
290
+ }
292
291
  }
package/src/ui/Modal.ts CHANGED
@@ -17,7 +17,7 @@ export interface ModalConfig {
17
17
  * Modal overlay component.
18
18
  * Shows content on top of a dark overlay with enter/exit animations.
19
19
  *
20
- * The content container uses `@pixi/layout` for automatic centering.
20
+ * Content is automatically centered via position calculations.
21
21
  *
22
22
  * @example
23
23
  * ```ts
@@ -28,6 +28,8 @@ export interface ModalConfig {
28
28
  * ```
29
29
  */
30
30
  export class Modal extends Container {
31
+ readonly __uiComponent = true as const;
32
+
31
33
  private _overlay: Graphics;
32
34
  private _contentContainer: Container;
33
35
  private _config: Required<ModalConfig>;
@@ -132,4 +134,12 @@ export class Modal extends Container {
132
134
  this._showing = false;
133
135
  this.onClose?.();
134
136
  }
137
+
138
+ /** React reconciler update hook */
139
+ updateConfig(changed: Record<string, any>): void {
140
+ if ('overlayAlpha' in changed) this._config.overlayAlpha = changed.overlayAlpha;
141
+ if ('closeOnOverlay' in changed) this._config.closeOnOverlay = changed.closeOnOverlay;
142
+ if ('animationDuration' in changed) this._config.animationDuration = changed.animationDuration;
143
+ if ('onClose' in changed) this.onClose = changed.onClose;
144
+ }
135
145
  }
package/src/ui/Panel.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Container, NineSliceSprite, Texture } from 'pixi.js';
2
- import { LayoutContainer } from '@pixi/layout/components';
3
- import type { LayoutStyles } from '@pixi/layout';
1
+ import { Container, Graphics, NineSliceSprite, Texture } from 'pixi.js';
2
+ import { FlexContainer } from './FlexContainer';
3
+ import type { FlexContainerConfig } from './FlexContainer';
4
4
 
5
5
  export interface PanelConfig {
6
6
  /** Width */
@@ -23,13 +23,15 @@ export interface PanelConfig {
23
23
  nineSliceBorders?: [number, number, number, number];
24
24
  /** Padding inside the panel */
25
25
  padding?: number;
26
+ /** Flex layout config for content */
27
+ layout?: Partial<FlexContainerConfig>;
26
28
  }
27
29
 
28
30
  /**
29
- * Background panel powered by `@pixi/layout` LayoutContainer.
31
+ * Background panel with optional flexbox content layout.
30
32
  *
31
33
  * Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
32
- * Children added to `content` participate in flexbox layout automatically.
34
+ * Children added via `addContent()` participate in flex layout automatically.
33
35
  *
34
36
  * @example
35
37
  * ```ts
@@ -44,12 +46,19 @@ export interface PanelConfig {
44
46
  * });
45
47
  * ```
46
48
  */
47
- export class Panel extends LayoutContainer {
49
+ export class Panel extends Container {
50
+ readonly __uiComponent = true as const;
51
+
52
+ private _bg: Container;
53
+ private _content: FlexContainer;
54
+ private _internalSetup = true;
48
55
  private _panelConfig: Required<
49
56
  Pick<PanelConfig, 'width' | 'height' | 'padding' | 'backgroundAlpha'>
50
57
  > & PanelConfig;
51
58
 
52
59
  constructor(config: PanelConfig = {}) {
60
+ super();
61
+
53
62
  const resolvedConfig = {
54
63
  width: config.width ?? 400,
55
64
  height: config.height ?? 300,
@@ -58,8 +67,9 @@ export class Panel extends LayoutContainer {
58
67
  ...config,
59
68
  };
60
69
 
61
- // If using a 9-slice texture, pass it as a custom background
62
- let customBackground: Container | undefined;
70
+ this._panelConfig = resolvedConfig;
71
+
72
+ // Create background
63
73
  if (config.nineSliceTexture) {
64
74
  const texture =
65
75
  typeof config.nineSliceTexture === 'string'
@@ -76,47 +86,109 @@ export class Panel extends LayoutContainer {
76
86
  nineSlice.width = resolvedConfig.width;
77
87
  nineSlice.height = resolvedConfig.height;
78
88
  nineSlice.alpha = resolvedConfig.backgroundAlpha;
79
- customBackground = nineSlice;
89
+ this._bg = nineSlice;
90
+ } else {
91
+ const g = new Graphics();
92
+ const bgColor = config.backgroundColor ?? 0x1a1a2e;
93
+ const radius = config.borderRadius ?? 0;
94
+ g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
95
+ if (config.borderColor !== undefined && config.borderWidth) {
96
+ g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
97
+ .stroke({ color: config.borderColor, width: config.borderWidth });
98
+ }
99
+ g.alpha = resolvedConfig.backgroundAlpha;
100
+ this._bg = g;
80
101
  }
102
+ this.addChild(this._bg);
81
103
 
82
- super(customBackground ? { background: customBackground } : undefined);
83
-
84
- this._panelConfig = resolvedConfig;
85
-
86
- // Apply layout styles
87
- const layoutStyles: LayoutStyles = {
104
+ // Create content flex container
105
+ this._content = new FlexContainer({
106
+ ...config.layout,
107
+ direction: config.layout?.direction ?? 'column',
108
+ justifyContent: config.layout?.justifyContent ?? 'start',
109
+ alignItems: config.layout?.alignItems ?? 'start',
110
+ gap: config.layout?.gap ?? 0,
111
+ padding: resolvedConfig.padding,
88
112
  width: resolvedConfig.width,
89
113
  height: resolvedConfig.height,
90
- padding: resolvedConfig.padding,
91
- flexDirection: 'column',
92
- };
114
+ });
115
+ this.addChild(this._content);
116
+ this._internalSetup = false;
117
+ }
93
118
 
94
- // Graphics-based background via layout styles
95
- if (!config.nineSliceTexture) {
96
- layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
97
- layoutStyles.borderRadius = config.borderRadius ?? 0;
98
- if (config.borderColor !== undefined && config.borderWidth) {
99
- layoutStyles.borderColor = config.borderColor;
100
- layoutStyles.borderWidth = config.borderWidth;
119
+ /** Access the content flex container — add children here for layout */
120
+ get content(): FlexContainer {
121
+ return this._content;
122
+ }
123
+
124
+ /** Convenience: add a child to the content layout */
125
+ addContent(child: Container): this {
126
+ this._content.addFlexChild(child);
127
+ this._content.updateLayout();
128
+ return this;
129
+ }
130
+
131
+ /** Resize the panel */
132
+ setSize(width: number, height: number): void {
133
+ this._panelConfig.width = width;
134
+ this._panelConfig.height = height;
135
+
136
+ // Resize background
137
+ if (this._bg instanceof NineSliceSprite) {
138
+ this._bg.width = width;
139
+ this._bg.height = height;
140
+ } else if (this._bg instanceof Graphics) {
141
+ const radius = this._panelConfig.borderRadius ?? 0;
142
+ const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
143
+ this._bg.clear();
144
+ (this._bg as Graphics).roundRect(0, 0, width, height, radius).fill(bgColor);
145
+ if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
146
+ (this._bg as Graphics).roundRect(0, 0, width, height, radius)
147
+ .stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
101
148
  }
149
+ this._bg.alpha = this._panelConfig.backgroundAlpha;
102
150
  }
103
151
 
104
- this.layout = layoutStyles;
152
+ this._content.resize(width, height);
153
+ }
105
154
 
106
- if (!config.nineSliceTexture) {
107
- this.background.alpha = resolvedConfig.backgroundAlpha;
155
+ /**
156
+ * Override addChild so external children are routed to content FlexContainer.
157
+ * Enables `<panel><label /><button /></panel>` in React JSX.
158
+ */
159
+ override addChild<T extends Container>(...children: T[]): T {
160
+ if (this._internalSetup) {
161
+ return super.addChild(...children);
162
+ }
163
+ for (const child of children) {
164
+ this._content.addFlexChild(child as any);
108
165
  }
166
+ this._content.updateLayout();
167
+ return children[0];
109
168
  }
110
169
 
111
- /** Access the content container (children added here participate in layout) */
112
- get content(): Container {
113
- return this.overflowContainer;
170
+ override removeChild<T extends Container>(...children: T[]): T {
171
+ if (this._internalSetup) {
172
+ return super.removeChild(...children);
173
+ }
174
+ for (const child of children) {
175
+ this._content.removeFlexChild(child as any);
176
+ }
177
+ return children[0];
114
178
  }
115
179
 
116
- /** Resize the panel */
117
- setSize(width: number, height: number): void {
118
- this._panelConfig.width = width;
119
- this._panelConfig.height = height;
120
- this._layout?.setStyle({ width, height });
180
+ /** React reconciler update hook */
181
+ updateConfig(changed: Record<string, any>): void {
182
+ if ('width' in changed || 'height' in changed) {
183
+ this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
184
+ }
185
+ if ('backgroundAlpha' in changed) {
186
+ this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
187
+ this._bg.alpha = changed.backgroundAlpha;
188
+ }
189
+ }
190
+
191
+ override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
192
+ super.destroy(options);
121
193
  }
122
194
  }
@@ -1,43 +1,63 @@
1
1
  import { Container, Graphics } from 'pixi.js';
2
- import { ProgressBar as PixiProgressBar } from '@pixi/ui';
3
- import type { ProgressBarOptions } from '@pixi/ui';
2
+ import { resolveView } from './view';
3
+ import type { ViewInput } from './view';
4
4
 
5
5
  export interface ProgressBarConfig {
6
+ /** Width of the bar */
6
7
  width?: number;
8
+ /** Height of the bar */
7
9
  height?: number;
10
+ /** Corner radius (for Graphics-based bar) */
8
11
  borderRadius?: number;
12
+ /** Fill color (for Graphics-based bar, ignored when fillView provided) */
9
13
  fillColor?: number;
14
+ /** Track background color (for Graphics-based bar, ignored when trackView provided) */
10
15
  trackColor?: number;
16
+ /** Border color */
11
17
  borderColor?: number;
18
+ /** Border width */
12
19
  borderWidth?: number;
13
20
  /** Animated fill (smoothly interpolate) */
14
21
  animated?: boolean;
15
22
  /** Animation speed (0..1 per frame, default: 0.1) */
16
23
  animationSpeed?: number;
17
- }
18
24
 
19
- function makeBarGraphics(
20
- w: number, h: number, radius: number, color: number,
21
- ): Graphics {
22
- return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
25
+ /** Custom track background (string texture name, Texture, or Container) */
26
+ trackView?: ViewInput;
27
+ /** Custom fill bar (string texture name, Texture, or Container) */
28
+ fillView?: ViewInput;
23
29
  }
24
30
 
25
31
  /**
26
- * Horizontal progress bar powered by `@pixi/ui` ProgressBar.
32
+ * Horizontal progress bar with optional custom track/fill views.
27
33
  *
28
- * Provides optional smooth animated fill via per-frame `update()`.
34
+ * Supports asset-based skinning: provide `trackView` and/or `fillView`
35
+ * as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
36
+ * Falls back to colored Graphics when no custom views are provided.
29
37
  *
30
38
  * @example
31
39
  * ```ts
40
+ * // Graphics-based (quick prototyping)
32
41
  * const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
33
- * scene.container.addChild(bar);
34
- * bar.progress = 0.5; // 50%
42
+ * bar.progress = 0.5;
43
+ *
44
+ * // Asset-based (production art)
45
+ * const bar = new ProgressBar({
46
+ * width: 300, height: 20,
47
+ * trackView: 'bar-track',
48
+ * fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
49
+ * });
50
+ * bar.progress = 0.75;
35
51
  * ```
36
52
  */
37
53
  export class ProgressBar extends Container {
38
- private _bar: PixiProgressBar;
54
+ readonly __uiComponent = true as const;
55
+
56
+ private _track: Container;
57
+ private _fill: Container;
58
+ private _fillMask: Graphics;
39
59
  private _borderGfx: Graphics;
40
- private _config: Required<ProgressBarConfig>;
60
+ private _config: Required<Pick<ProgressBarConfig, 'width' | 'height' | 'borderRadius' | 'fillColor' | 'trackColor' | 'borderColor' | 'borderWidth' | 'animated' | 'animationSpeed'>>;
41
61
  private _progress = 0;
42
62
  private _displayedProgress = 0;
43
63
 
@@ -58,23 +78,43 @@ export class ProgressBar extends Container {
58
78
 
59
79
  const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
60
80
 
61
- const bgGraphics = makeBarGraphics(width, height, borderRadius, trackColor);
62
- const fillGraphics = makeBarGraphics(width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1), fillColor);
63
-
64
- const options: ProgressBarOptions = {
65
- bg: bgGraphics,
66
- fill: fillGraphics,
67
- fillPaddings: {
68
- top: borderWidth,
69
- right: borderWidth,
70
- bottom: borderWidth,
71
- left: borderWidth,
72
- },
73
- progress: 0,
74
- };
81
+ // Track background custom view or Graphics
82
+ const customTrack = resolveView(config.trackView);
83
+ if (customTrack) {
84
+ customTrack.width = width;
85
+ customTrack.height = height;
86
+ this._track = customTrack;
87
+ } else {
88
+ const g = new Graphics();
89
+ g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
90
+ this._track = g;
91
+ }
92
+ this.addChild(this._track);
75
93
 
76
- this._bar = new PixiProgressBar(options);
77
- this.addChild(this._bar);
94
+ // Fill bar — custom view or Graphics
95
+ const customFill = resolveView(config.fillView);
96
+ if (customFill) {
97
+ customFill.x = borderWidth;
98
+ customFill.y = borderWidth;
99
+ customFill.width = width - borderWidth * 2;
100
+ customFill.height = height - borderWidth * 2;
101
+ this._fill = customFill;
102
+ } else {
103
+ const g = new Graphics();
104
+ g.roundRect(
105
+ borderWidth, borderWidth,
106
+ width - borderWidth * 2, height - borderWidth * 2,
107
+ Math.max(0, borderRadius - 1),
108
+ ).fill(fillColor);
109
+ this._fill = g;
110
+ }
111
+ this.addChild(this._fill);
112
+
113
+ // Mask for the fill (controls visible width)
114
+ this._fillMask = new Graphics();
115
+ this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
116
+ this.addChild(this._fillMask);
117
+ this._fill.mask = this._fillMask;
78
118
 
79
119
  // Border overlay
80
120
  this._borderGfx = new Graphics();
@@ -95,7 +135,7 @@ export class ProgressBar extends Container {
95
135
  this._progress = Math.max(0, Math.min(1, value));
96
136
  if (!this._config.animated) {
97
137
  this._displayedProgress = this._progress;
98
- this._bar.progress = this._displayedProgress * 100;
138
+ this.updateMask();
99
139
  }
100
140
  }
101
141
 
@@ -106,11 +146,25 @@ export class ProgressBar extends Container {
106
146
  if (!this._config.animated) return;
107
147
  if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
108
148
  this._displayedProgress = this._progress;
149
+ this.updateMask();
109
150
  return;
110
151
  }
111
152
 
112
153
  this._displayedProgress +=
113
154
  (this._progress - this._displayedProgress) * this._config.animationSpeed;
114
- this._bar.progress = this._displayedProgress * 100;
155
+ this.updateMask();
156
+ }
157
+
158
+ /** React reconciler update hook */
159
+ updateConfig(changed: Record<string, any>): void {
160
+ if ('progress' in changed) this.progress = changed.progress;
161
+ if ('animated' in changed) this._config.animated = changed.animated;
162
+ if ('animationSpeed' in changed) this._config.animationSpeed = changed.animationSpeed;
163
+ }
164
+
165
+ private updateMask(): void {
166
+ const w = this._config.width * this._displayedProgress;
167
+ this._fillMask.clear();
168
+ this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
115
169
  }
116
170
  }