@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/dist/index.esm.js CHANGED
@@ -1,8 +1,6 @@
1
- import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Graphics, NineSliceSprite } from 'pixi.js';
1
+ import { Ticker, Assets, Container, Text, Application, AnimatedSprite, Texture, Sprite, Graphics, NineSliceSprite } from 'pixi.js';
2
2
  import { CasinoGameSDK, Bridge } from '@energy8platform/game-sdk';
3
3
  export { BridgeDestroyedError, BridgeNotReadyError, SDKError, TimeoutError } from '@energy8platform/game-sdk';
4
- import { FancyButton, ProgressBar as ProgressBar$1, ScrollBox } from '@pixi/ui';
5
- import { LayoutContainer } from '@pixi/layout/components';
6
4
 
7
5
  // ─── Scale Modes ───────────────────────────────────────────
8
6
  var ScaleMode;
@@ -2909,6 +2907,449 @@ class SpriteAnimation {
2909
2907
  }
2910
2908
  }
2911
2909
 
2910
+ // ─── Helpers ─────────────────────────────────────────────
2911
+ function normalizePadding(p) {
2912
+ return typeof p === 'number' ? [p, p, p, p] : p;
2913
+ }
2914
+ /** Measure a child's size and bounds offset for layout purposes */
2915
+ function measureChild(child) {
2916
+ const cfg = child._flexConfig;
2917
+ if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
2918
+ return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
2919
+ }
2920
+ // For FlexContainers, use their explicit size if set
2921
+ if (child instanceof FlexContainer) {
2922
+ const fc = child;
2923
+ if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
2924
+ return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
2925
+ }
2926
+ }
2927
+ // Use localBounds to get the true visual extent and origin offset.
2928
+ // This handles children with non-zero anchors (e.g. Button, Label with centered text).
2929
+ const bounds = child.getLocalBounds();
2930
+ const w = cfg?.layoutWidth ?? bounds.width;
2931
+ const h = cfg?.layoutHeight ?? bounds.height;
2932
+ return { w, h, ox: bounds.x, oy: bounds.y };
2933
+ }
2934
+ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
2935
+ if (items.length === 0)
2936
+ return;
2937
+ // Compute total fixed main size and flex grow total
2938
+ let totalFixed = 0;
2939
+ let totalGrow = 0;
2940
+ for (const item of items) {
2941
+ const grow = item.child._flexConfig?.flexGrow ?? 0;
2942
+ if (grow > 0) {
2943
+ totalGrow += grow;
2944
+ }
2945
+ else {
2946
+ totalFixed += isRow ? item.w : item.h;
2947
+ }
2948
+ }
2949
+ const totalGap = gap * (items.length - 1);
2950
+ const availableForFlex = Math.max(0, mainSize - totalFixed - totalGap);
2951
+ // Resolve flex sizes
2952
+ if (totalGrow > 0) {
2953
+ for (const item of items) {
2954
+ const grow = item.child._flexConfig?.flexGrow ?? 0;
2955
+ if (grow > 0) {
2956
+ const flexSize = (grow / totalGrow) * availableForFlex;
2957
+ if (isRow) {
2958
+ item.w = flexSize;
2959
+ item.child.width = flexSize;
2960
+ }
2961
+ else {
2962
+ item.h = flexSize;
2963
+ item.child.height = flexSize;
2964
+ }
2965
+ }
2966
+ }
2967
+ }
2968
+ // Shrink: if content overflows and mainSize is finite, shrink eligible items
2969
+ if (totalGrow === 0 && mainSize > 0) {
2970
+ const overflow = totalFixed + totalGap - mainSize;
2971
+ if (overflow > 0) {
2972
+ let totalShrinkable = 0;
2973
+ for (const item of items) {
2974
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
2975
+ if (shrink > 0) {
2976
+ totalShrinkable += isRow ? item.w : item.h;
2977
+ }
2978
+ }
2979
+ if (totalShrinkable > 0) {
2980
+ for (const item of items) {
2981
+ const shrink = item.child._flexConfig?.flexShrink ?? 1;
2982
+ if (shrink > 0) {
2983
+ const itemMain = isRow ? item.w : item.h;
2984
+ const reduction = overflow * (itemMain / totalShrinkable);
2985
+ const newSize = Math.max(0, itemMain - reduction);
2986
+ if (isRow) {
2987
+ item.w = newSize;
2988
+ item.child.width = newSize;
2989
+ }
2990
+ else {
2991
+ item.h = newSize;
2992
+ item.child.height = newSize;
2993
+ }
2994
+ }
2995
+ }
2996
+ }
2997
+ }
2998
+ }
2999
+ // Calculate total main size after flex
3000
+ let totalMain = totalGap;
3001
+ for (const item of items) {
3002
+ totalMain += isRow ? item.w : item.h;
3003
+ }
3004
+ // Justify: compute starting offset and extra spacing
3005
+ let mainOffset = 0;
3006
+ let extraGap = 0;
3007
+ switch (justify) {
3008
+ case 'start':
3009
+ break;
3010
+ case 'center':
3011
+ mainOffset = Math.max(0, (mainSize - totalMain) / 2);
3012
+ break;
3013
+ case 'end':
3014
+ mainOffset = Math.max(0, mainSize - totalMain);
3015
+ break;
3016
+ case 'space-between':
3017
+ if (items.length > 1) {
3018
+ extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
3019
+ }
3020
+ break;
3021
+ case 'space-around':
3022
+ if (items.length > 0) {
3023
+ const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
3024
+ const segment = totalSpace / items.length;
3025
+ mainOffset = segment / 2;
3026
+ extraGap = segment - gap;
3027
+ }
3028
+ break;
3029
+ }
3030
+ // Position each item
3031
+ let pos = mainOffset;
3032
+ for (const item of items) {
3033
+ const mainDim = isRow ? item.w : item.h;
3034
+ const crossDim = isRow ? item.h : item.w;
3035
+ // Cross-axis alignment (alignSelf overrides align)
3036
+ const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
3037
+ ? item.child._flexConfig.alignSelf
3038
+ : align;
3039
+ let crossPos = crossOffset;
3040
+ switch (effectiveAlign) {
3041
+ case 'start':
3042
+ break;
3043
+ case 'center':
3044
+ crossPos += (crossSize - crossDim) / 2;
3045
+ break;
3046
+ case 'end':
3047
+ crossPos += crossSize - crossDim;
3048
+ break;
3049
+ case 'stretch':
3050
+ if (isRow) {
3051
+ item.child.height = crossSize;
3052
+ }
3053
+ else {
3054
+ item.child.width = crossSize;
3055
+ }
3056
+ break;
3057
+ }
3058
+ // Compensate for local bounds offset (e.g. centered anchors)
3059
+ if (isRow) {
3060
+ item.child.x = pos - item.ox;
3061
+ item.child.y = crossPos - item.oy;
3062
+ }
3063
+ else {
3064
+ item.child.x = crossPos - item.ox;
3065
+ item.child.y = pos - item.oy;
3066
+ }
3067
+ pos += mainDim + gap + extraGap;
3068
+ }
3069
+ }
3070
+ // ─── FlexContainer ───────────────────────────────────────
3071
+ /**
3072
+ * Lightweight flexbox-like layout container for PixiJS.
3073
+ *
3074
+ * Supports row/column direction, justify/align, gap, padding, wrapping,
3075
+ * and flex-grow distribution. Zero external dependencies.
3076
+ *
3077
+ * @example
3078
+ * ```ts
3079
+ * const toolbar = new FlexContainer({
3080
+ * direction: 'row',
3081
+ * justifyContent: 'space-between',
3082
+ * alignItems: 'center',
3083
+ * gap: 16,
3084
+ * padding: 12,
3085
+ * });
3086
+ *
3087
+ * toolbar.addFlexChild(button1);
3088
+ * toolbar.addFlexChild(button2);
3089
+ * toolbar.resize(800, 60);
3090
+ * ```
3091
+ */
3092
+ class FlexContainer extends Container {
3093
+ __uiComponent = true;
3094
+ _config;
3095
+ _padding;
3096
+ _maxWidth;
3097
+ _maxHeight;
3098
+ /** @internal */ _explicitWidth;
3099
+ /** @internal */ _explicitHeight;
3100
+ _layoutChildren = [];
3101
+ _layoutDirty = true;
3102
+ constructor(config = {}) {
3103
+ super();
3104
+ this._config = {
3105
+ direction: config.direction ?? 'row',
3106
+ justifyContent: config.justifyContent ?? 'start',
3107
+ alignItems: config.alignItems ?? 'start',
3108
+ gap: config.gap ?? 0,
3109
+ flexWrap: config.flexWrap ?? false,
3110
+ };
3111
+ this._padding = normalizePadding(config.padding ?? 0);
3112
+ this._maxWidth = config.maxWidth ?? Infinity;
3113
+ this._maxHeight = config.maxHeight ?? Infinity;
3114
+ this._explicitWidth = config.width ?? 0;
3115
+ this._explicitHeight = config.height ?? 0;
3116
+ }
3117
+ // ─── Public API ──────────────────────────────────────
3118
+ /** Add a child with optional flex config. Also registers in flex layout. */
3119
+ addFlexChild(child, flexConfig) {
3120
+ if (flexConfig)
3121
+ child._flexConfig = flexConfig;
3122
+ if (!this._layoutChildren.includes(child)) {
3123
+ this._layoutChildren.push(child);
3124
+ this._layoutDirty = true;
3125
+ }
3126
+ super.addChild(child);
3127
+ return this;
3128
+ }
3129
+ /** Remove a child from flex layout and display list */
3130
+ removeFlexChild(child) {
3131
+ const idx = this._layoutChildren.indexOf(child);
3132
+ if (idx !== -1) {
3133
+ this._layoutChildren.splice(idx, 1);
3134
+ this._layoutDirty = true;
3135
+ }
3136
+ super.removeChild(child);
3137
+ return this;
3138
+ }
3139
+ /** Remove all flex children */
3140
+ clearFlexChildren() {
3141
+ for (const child of this._layoutChildren) {
3142
+ super.removeChild(child);
3143
+ }
3144
+ this._layoutChildren.length = 0;
3145
+ this._layoutDirty = true;
3146
+ return this;
3147
+ }
3148
+ /**
3149
+ * Override addChild so children automatically participate in flex layout.
3150
+ * This enables declarative usage from React JSX.
3151
+ */
3152
+ addChild(...children) {
3153
+ for (const child of children) {
3154
+ if (!this._layoutChildren.includes(child)) {
3155
+ this._layoutChildren.push(child);
3156
+ this._layoutDirty = true;
3157
+ }
3158
+ }
3159
+ const result = super.addChild(...children);
3160
+ if (this._layoutDirty)
3161
+ this.updateLayout();
3162
+ return result;
3163
+ }
3164
+ removeChild(...children) {
3165
+ for (const child of children) {
3166
+ const idx = this._layoutChildren.indexOf(child);
3167
+ if (idx !== -1) {
3168
+ this._layoutChildren.splice(idx, 1);
3169
+ this._layoutDirty = true;
3170
+ }
3171
+ }
3172
+ return super.removeChild(...children);
3173
+ }
3174
+ /** Get all flex layout children (read-only) */
3175
+ get flexChildren() {
3176
+ return this._layoutChildren;
3177
+ }
3178
+ /** Update the container size and recalculate layout */
3179
+ resize(width, height) {
3180
+ this._explicitWidth = width;
3181
+ this._explicitHeight = height;
3182
+ this._layoutDirty = true;
3183
+ this.updateLayout();
3184
+ }
3185
+ /** Update layout direction */
3186
+ setDirection(direction) {
3187
+ this._config.direction = direction;
3188
+ this._layoutDirty = true;
3189
+ }
3190
+ /** Update justifyContent */
3191
+ setJustifyContent(justify) {
3192
+ this._config.justifyContent = justify;
3193
+ this._layoutDirty = true;
3194
+ }
3195
+ /** Update alignItems */
3196
+ setAlignItems(align) {
3197
+ this._config.alignItems = align;
3198
+ this._layoutDirty = true;
3199
+ }
3200
+ /** Update gap */
3201
+ setGap(gap) {
3202
+ this._config.gap = gap;
3203
+ this._layoutDirty = true;
3204
+ }
3205
+ /** Update padding */
3206
+ setPadding(padding) {
3207
+ this._padding = normalizePadding(padding);
3208
+ this._layoutDirty = true;
3209
+ }
3210
+ /**
3211
+ * Recalculate and apply layout positions for all children.
3212
+ * Called automatically by `resize()`. Call manually after
3213
+ * adding/removing children without resize.
3214
+ */
3215
+ updateLayout() {
3216
+ this._layoutDirty = false;
3217
+ const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
3218
+ const [pt, pr, pb, pl] = this._padding;
3219
+ const isRow = direction === 'row';
3220
+ const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
3221
+ const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
3222
+ const mainLimit = isRow ? contentW : contentH;
3223
+ const crossLimit = isRow ? contentH : contentW;
3224
+ // Measure children (skip flexExclude — they position themselves)
3225
+ const measured = [];
3226
+ for (const child of this._layoutChildren) {
3227
+ if (child._flexConfig?.flexExclude)
3228
+ continue;
3229
+ const { w, h, ox, oy } = measureChild(child);
3230
+ measured.push({ child, w, h, ox, oy });
3231
+ }
3232
+ // Split into lines (if wrapping)
3233
+ const lines = [];
3234
+ if (flexWrap && mainLimit < Infinity) {
3235
+ let currentLine = [];
3236
+ let lineMain = 0;
3237
+ for (const item of measured) {
3238
+ const itemMain = isRow ? item.w : item.h;
3239
+ const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
3240
+ if (currentLine.length > 0 && wouldBe > mainLimit) {
3241
+ lines.push(currentLine);
3242
+ currentLine = [item];
3243
+ lineMain = itemMain;
3244
+ }
3245
+ else {
3246
+ currentLine.push(item);
3247
+ lineMain = wouldBe;
3248
+ }
3249
+ }
3250
+ if (currentLine.length > 0)
3251
+ lines.push(currentLine);
3252
+ }
3253
+ else {
3254
+ lines.push(measured);
3255
+ }
3256
+ // Compute cross size per line
3257
+ const lineCrossSizes = lines.map((line) => {
3258
+ let maxCross = 0;
3259
+ for (const item of line) {
3260
+ const cross = isRow ? item.h : item.w;
3261
+ if (cross > maxCross)
3262
+ maxCross = cross;
3263
+ }
3264
+ return maxCross;
3265
+ });
3266
+ // Layout each line
3267
+ let crossOffset = isRow ? pt : pl;
3268
+ for (let i = 0; i < lines.length; i++) {
3269
+ const line = lines[i];
3270
+ const lineCross = lineCrossSizes[i];
3271
+ const mainStart = isRow ? pl : pt;
3272
+ // Offset items by padding
3273
+ const tempItems = line.map((item) => ({ ...item }));
3274
+ // For single-line layouts, use the full available cross space for alignment;
3275
+ // for multi-line (wrapping), each line gets its own measured cross size.
3276
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity
3277
+ ? crossLimit
3278
+ : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
3279
+ layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
3280
+ // Apply main-axis padding offset
3281
+ for (const item of tempItems) {
3282
+ const origChild = line.find((l) => l.child === item.child);
3283
+ origChild.child.x = item.child.x + (isRow ? mainStart : 0);
3284
+ origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
3285
+ }
3286
+ crossOffset += lineCross + gap;
3287
+ }
3288
+ }
3289
+ /** Computed content size (after layout) */
3290
+ getContentSize() {
3291
+ if (this._layoutDirty)
3292
+ this.updateLayout();
3293
+ let maxX = 0;
3294
+ let maxY = 0;
3295
+ for (const child of this._layoutChildren) {
3296
+ const { w, h } = measureChild(child);
3297
+ maxX = Math.max(maxX, child.x + w);
3298
+ maxY = Math.max(maxY, child.y + h);
3299
+ }
3300
+ const [, pr, pb] = this._padding;
3301
+ return { width: maxX + pr, height: maxY + pb };
3302
+ }
3303
+ /** React reconciler update hook — applies changed config props */
3304
+ updateConfig(changed) {
3305
+ if ('direction' in changed)
3306
+ this.setDirection(changed.direction);
3307
+ if ('justifyContent' in changed)
3308
+ this.setJustifyContent(changed.justifyContent);
3309
+ if ('alignItems' in changed)
3310
+ this.setAlignItems(changed.alignItems);
3311
+ if ('gap' in changed)
3312
+ this.setGap(changed.gap);
3313
+ if ('padding' in changed)
3314
+ this.setPadding(changed.padding);
3315
+ if ('flexWrap' in changed) {
3316
+ this._config.flexWrap = changed.flexWrap;
3317
+ this._layoutDirty = true;
3318
+ }
3319
+ if ('width' in changed || 'height' in changed) {
3320
+ this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
3321
+ return; // resize calls updateLayout
3322
+ }
3323
+ if (this._layoutDirty)
3324
+ this.updateLayout();
3325
+ }
3326
+ destroy(options) {
3327
+ this._layoutChildren.length = 0;
3328
+ super.destroy(options);
3329
+ }
3330
+ }
3331
+
3332
+ /**
3333
+ * Resolve a ViewInput to a Container instance.
3334
+ *
3335
+ * @example
3336
+ * ```ts
3337
+ * resolveView('btn-idle') // → Sprite.from('btn-idle')
3338
+ * resolveView(someTexture) // → new Sprite(someTexture)
3339
+ * resolveView(myCustomContainer) // → myCustomContainer (as-is)
3340
+ * resolveView(undefined) // → null
3341
+ * ```
3342
+ */
3343
+ function resolveView(input) {
3344
+ if (input == null)
3345
+ return null;
3346
+ if (typeof input === 'string')
3347
+ return Sprite.from(input);
3348
+ if (input instanceof Texture)
3349
+ return new Sprite(input);
3350
+ return input;
3351
+ }
3352
+
2912
3353
  const DEFAULT_COLORS = {
2913
3354
  default: 0xffd700,
2914
3355
  hover: 0xffe44d,
@@ -2917,33 +3358,55 @@ const DEFAULT_COLORS = {
2917
3358
  };
2918
3359
  function makeGraphicsView(w, h, radius, color) {
2919
3360
  const g = new Graphics();
2920
- g.roundRect(0, 0, w, h, radius).fill(color);
2921
- // Highlight overlay
2922
- g.roundRect(2, 2, w - 4, h * 0.45, radius).fill({ color: 0xffffff, alpha: 0.1 });
3361
+ g.roundRect(-w / 2, -h / 2, w, h, radius).fill(color);
2923
3362
  return g;
2924
3363
  }
2925
3364
  /**
2926
- * Interactive button component powered by `@pixi/ui` FancyButton.
3365
+ * Interactive button with per-state custom views and animations.
2927
3366
  *
2928
- * Supports both texture-based and Graphics-based rendering with
2929
- * per-state views, press animation, and text.
3367
+ * Each visual state accepts a `ViewInput`: texture name, Texture, or any Container
3368
+ * (Sprite, NineSliceSprite, AnimatedSprite, custom artwork, etc).
3369
+ * Falls back to colored Graphics when no custom view is provided.
2930
3370
  *
2931
3371
  * @example
2932
3372
  * ```ts
3373
+ * // Graphics-based (quick prototyping)
2933
3374
  * const btn = new Button({
2934
3375
  * width: 200, height: 60, borderRadius: 12,
2935
3376
  * colors: { default: 0x22aa22, hover: 0x33cc33 },
2936
3377
  * text: 'SPIN',
3378
+ * onPress: () => spin(),
3379
+ * });
3380
+ *
3381
+ * // Asset-based (production art)
3382
+ * const btn = new Button({
3383
+ * defaultView: 'btn-idle',
3384
+ * hoverView: 'btn-hover',
3385
+ * pressedView: 'btn-pressed',
3386
+ * disabledView: 'btn-disabled',
3387
+ * text: 'SPIN',
3388
+ * onPress: () => spin(),
2937
3389
  * });
2938
3390
  *
2939
- * btn.onPress.connect(() => console.log('Clicked!'));
2940
- * scene.container.addChild(btn);
3391
+ * // Custom Container view
3392
+ * const btn = new Button({
3393
+ * defaultView: myAnimatedSprite,
3394
+ * text: 'SPIN',
3395
+ * });
2941
3396
  * ```
2942
3397
  */
2943
- class Button extends FancyButton {
2944
- _buttonConfig;
3398
+ class Button extends Container {
3399
+ __uiComponent = true;
3400
+ _views = new Map();
3401
+ _state = 'default';
3402
+ _enabled = true;
3403
+ _config;
3404
+ _textObj = null;
3405
+ /** Press callback */
3406
+ onPress;
2945
3407
  constructor(config = {}) {
2946
- const resolvedConfig = {
3408
+ super();
3409
+ this._config = {
2947
3410
  width: config.width ?? 200,
2948
3411
  height: config.height ?? 60,
2949
3412
  borderRadius: config.borderRadius ?? 8,
@@ -2951,50 +3414,39 @@ class Button extends FancyButton {
2951
3414
  animationDuration: config.animationDuration ?? 100,
2952
3415
  ...config,
2953
3416
  };
2954
- const colorMap = { ...DEFAULT_COLORS, ...config.colors };
2955
- const { width, height, borderRadius } = resolvedConfig;
2956
- // Build FancyButton options
2957
- const options = {
2958
- anchor: 0.5,
2959
- animations: {
2960
- hover: {
2961
- props: { scale: { x: 1.03, y: 1.03 } },
2962
- duration: resolvedConfig.animationDuration,
2963
- },
2964
- pressed: {
2965
- props: { scale: { x: resolvedConfig.pressScale, y: resolvedConfig.pressScale } },
2966
- duration: resolvedConfig.animationDuration,
2967
- },
2968
- },
2969
- };
2970
- // Texture-based views
2971
- if (config.textures) {
2972
- if (config.textures.default)
2973
- options.defaultView = config.textures.default;
2974
- if (config.textures.hover)
2975
- options.hoverView = config.textures.hover;
2976
- if (config.textures.pressed)
2977
- options.pressedView = config.textures.pressed;
2978
- if (config.textures.disabled)
2979
- options.disabledView = config.textures.disabled;
2980
- }
2981
- else {
2982
- // Graphics-based views
2983
- options.defaultView = makeGraphicsView(width, height, borderRadius, colorMap.default);
2984
- options.hoverView = makeGraphicsView(width, height, borderRadius, colorMap.hover);
2985
- options.pressedView = makeGraphicsView(width, height, borderRadius, colorMap.pressed);
2986
- options.disabledView = makeGraphicsView(width, height, borderRadius, colorMap.disabled);
2987
- }
3417
+ this.onPress = config.onPress;
3418
+ this._buildViews(config);
2988
3419
  // Text
2989
3420
  if (config.text) {
2990
- options.text = config.text;
2991
- }
2992
- super(options);
2993
- this._buttonConfig = resolvedConfig;
3421
+ this._textObj = new Text({
3422
+ text: config.text,
3423
+ style: {
3424
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
3425
+ fontSize: 20,
3426
+ fill: 0xffffff,
3427
+ fontWeight: 'bold',
3428
+ ...config.textStyle,
3429
+ },
3430
+ });
3431
+ this._textObj.anchor.set(0.5);
3432
+ this.addChild(this._textObj);
3433
+ }
3434
+ // Interaction
3435
+ this.eventMode = 'static';
3436
+ this.cursor = 'pointer';
3437
+ this.on('pointerover', this._onPointerOver, this);
3438
+ this.on('pointerout', this._onPointerOut, this);
3439
+ this.on('pointerdown', this._onPointerDown, this);
3440
+ this.on('pointerup', this._onPointerUp, this);
3441
+ this.on('pointerupoutside', this._onPointerUpOutside, this);
2994
3442
  if (config.disabled) {
2995
3443
  this.enabled = false;
2996
3444
  }
2997
3445
  }
3446
+ /** Current button state */
3447
+ get state() {
3448
+ return this._state;
3449
+ }
2998
3450
  /** Enable the button */
2999
3451
  enable() {
3000
3452
  this.enabled = true;
@@ -3003,29 +3455,161 @@ class Button extends FancyButton {
3003
3455
  disable() {
3004
3456
  this.enabled = false;
3005
3457
  }
3458
+ /** Whether the button is enabled */
3459
+ get enabled() {
3460
+ return this._enabled;
3461
+ }
3462
+ set enabled(value) {
3463
+ this._enabled = value;
3464
+ this.cursor = value ? 'pointer' : 'default';
3465
+ this.eventMode = value ? 'static' : 'none';
3466
+ this._setState(value ? 'default' : 'disabled');
3467
+ }
3006
3468
  /** Whether the button is disabled */
3007
3469
  get disabled() {
3008
- return !this.enabled;
3470
+ return !this._enabled;
3471
+ }
3472
+ /** Update button text */
3473
+ set text(value) {
3474
+ if (this._textObj) {
3475
+ this._textObj.text = value;
3476
+ }
3477
+ }
3478
+ // ─── View building ──────────────────────────────────
3479
+ _buildViews(config) {
3480
+ const colorMap = { ...DEFAULT_COLORS, ...config.colors };
3481
+ const { width, height, borderRadius } = this._config;
3482
+ const stateViews = {
3483
+ default: config.defaultView,
3484
+ hover: config.hoverView,
3485
+ pressed: config.pressedView,
3486
+ disabled: config.disabledView,
3487
+ };
3488
+ const states = ['default', 'hover', 'pressed', 'disabled'];
3489
+ for (const state of states) {
3490
+ const customView = resolveView(stateViews[state]);
3491
+ const view = customView ?? makeGraphicsView(width, height, borderRadius, colorMap[state]);
3492
+ view.visible = state === 'default';
3493
+ this._views.set(state, view);
3494
+ this.addChild(view);
3495
+ }
3496
+ }
3497
+ _rebuildViews() {
3498
+ for (const [, view] of this._views) {
3499
+ this.removeChild(view);
3500
+ view.destroy();
3501
+ }
3502
+ this._views.clear();
3503
+ this._buildViews(this._config);
3504
+ // Re-insert views before text
3505
+ if (this._textObj && this._textObj.parent === this) {
3506
+ this.setChildIndex(this._textObj, this.children.length - 1);
3507
+ }
3508
+ }
3509
+ // ─── State management ───────────────────────────────
3510
+ _setState(state) {
3511
+ if (this._state === state)
3512
+ return;
3513
+ this._state = state;
3514
+ for (const [s, view] of this._views) {
3515
+ view.visible = s === state;
3516
+ }
3517
+ }
3518
+ _onPointerOver() {
3519
+ if (!this._enabled)
3520
+ return;
3521
+ this._setState('hover');
3522
+ Tween.killTweensOf(this);
3523
+ Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
3524
+ }
3525
+ _onPointerOut() {
3526
+ if (!this._enabled)
3527
+ return;
3528
+ this._setState('default');
3529
+ Tween.killTweensOf(this);
3530
+ Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
3531
+ }
3532
+ _onPointerDown() {
3533
+ if (!this._enabled)
3534
+ return;
3535
+ this._setState('pressed');
3536
+ Tween.killTweensOf(this);
3537
+ const s = this._config.pressScale;
3538
+ Tween.to(this, { 'scale.x': s, 'scale.y': s }, this._config.animationDuration, Easing.easeOutQuad);
3539
+ }
3540
+ _onPointerUp() {
3541
+ if (!this._enabled)
3542
+ return;
3543
+ this._setState('hover');
3544
+ Tween.killTweensOf(this);
3545
+ Tween.to(this, { 'scale.x': 1.03, 'scale.y': 1.03 }, this._config.animationDuration, Easing.easeOutQuad);
3546
+ this.onPress?.();
3547
+ }
3548
+ _onPointerUpOutside() {
3549
+ if (!this._enabled)
3550
+ return;
3551
+ this._setState('default');
3552
+ Tween.killTweensOf(this);
3553
+ Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, this._config.animationDuration, Easing.easeOutQuad);
3554
+ }
3555
+ /** React reconciler update hook */
3556
+ updateConfig(changed) {
3557
+ if ('text' in changed && this._textObj)
3558
+ this._textObj.text = changed.text;
3559
+ if ('disabled' in changed)
3560
+ this.enabled = !changed.disabled;
3561
+ if ('onPress' in changed)
3562
+ this.onPress = changed.onPress;
3563
+ const structural = [
3564
+ 'colors', 'width', 'height', 'borderRadius', 'textStyle',
3565
+ 'defaultView', 'hoverView', 'pressedView', 'disabledView',
3566
+ ];
3567
+ const needsRebuild = structural.some((k) => k in changed);
3568
+ if (needsRebuild) {
3569
+ Object.assign(this._config, changed);
3570
+ this._rebuildViews();
3571
+ }
3572
+ }
3573
+ destroy(options) {
3574
+ Tween.killTweensOf(this);
3575
+ this.off('pointerover', this._onPointerOver, this);
3576
+ this.off('pointerout', this._onPointerOut, this);
3577
+ this.off('pointerdown', this._onPointerDown, this);
3578
+ this.off('pointerup', this._onPointerUp, this);
3579
+ this.off('pointerupoutside', this._onPointerUpOutside, this);
3580
+ this._views.clear();
3581
+ this._textObj = null;
3582
+ super.destroy(options);
3009
3583
  }
3010
3584
  }
3011
3585
 
3012
- function makeBarGraphics(w, h, radius, color) {
3013
- return new Graphics().roundRect(0, 0, w, h, radius).fill(color);
3014
- }
3015
3586
  /**
3016
- * Horizontal progress bar powered by `@pixi/ui` ProgressBar.
3587
+ * Horizontal progress bar with optional custom track/fill views.
3017
3588
  *
3018
- * Provides optional smooth animated fill via per-frame `update()`.
3589
+ * Supports asset-based skinning: provide `trackView` and/or `fillView`
3590
+ * as texture names, Textures, or any Container (NineSliceSprite, custom artwork, etc).
3591
+ * Falls back to colored Graphics when no custom views are provided.
3019
3592
  *
3020
3593
  * @example
3021
3594
  * ```ts
3595
+ * // Graphics-based (quick prototyping)
3022
3596
  * const bar = new ProgressBar({ width: 300, height: 20, fillColor: 0x22cc22 });
3023
- * scene.container.addChild(bar);
3024
- * bar.progress = 0.5; // 50%
3597
+ * bar.progress = 0.5;
3598
+ *
3599
+ * // Asset-based (production art)
3600
+ * const bar = new ProgressBar({
3601
+ * width: 300, height: 20,
3602
+ * trackView: 'bar-track',
3603
+ * fillView: new NineSliceSprite({ texture: 'bar-fill', ... }),
3604
+ * });
3605
+ * bar.progress = 0.75;
3025
3606
  * ```
3026
3607
  */
3027
3608
  class ProgressBar extends Container {
3028
- _bar;
3609
+ __uiComponent = true;
3610
+ _track;
3611
+ _fill;
3612
+ _fillMask;
3029
3613
  _borderGfx;
3030
3614
  _config;
3031
3615
  _progress = 0;
@@ -3044,21 +3628,39 @@ class ProgressBar extends Container {
3044
3628
  animationSpeed: config.animationSpeed ?? 0.1,
3045
3629
  };
3046
3630
  const { width, height, borderRadius, fillColor, trackColor, borderColor, borderWidth } = this._config;
3047
- const bgGraphics = makeBarGraphics(width, height, borderRadius, trackColor);
3048
- const fillGraphics = makeBarGraphics(width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1), fillColor);
3049
- const options = {
3050
- bg: bgGraphics,
3051
- fill: fillGraphics,
3052
- fillPaddings: {
3053
- top: borderWidth,
3054
- right: borderWidth,
3055
- bottom: borderWidth,
3056
- left: borderWidth,
3057
- },
3058
- progress: 0,
3059
- };
3060
- this._bar = new ProgressBar$1(options);
3061
- this.addChild(this._bar);
3631
+ // Track background custom view or Graphics
3632
+ const customTrack = resolveView(config.trackView);
3633
+ if (customTrack) {
3634
+ customTrack.width = width;
3635
+ customTrack.height = height;
3636
+ this._track = customTrack;
3637
+ }
3638
+ else {
3639
+ const g = new Graphics();
3640
+ g.roundRect(0, 0, width, height, borderRadius).fill(trackColor);
3641
+ this._track = g;
3642
+ }
3643
+ this.addChild(this._track);
3644
+ // Fill bar — custom view or Graphics
3645
+ const customFill = resolveView(config.fillView);
3646
+ if (customFill) {
3647
+ customFill.x = borderWidth;
3648
+ customFill.y = borderWidth;
3649
+ customFill.width = width - borderWidth * 2;
3650
+ customFill.height = height - borderWidth * 2;
3651
+ this._fill = customFill;
3652
+ }
3653
+ else {
3654
+ const g = new Graphics();
3655
+ g.roundRect(borderWidth, borderWidth, width - borderWidth * 2, height - borderWidth * 2, Math.max(0, borderRadius - 1)).fill(fillColor);
3656
+ this._fill = g;
3657
+ }
3658
+ this.addChild(this._fill);
3659
+ // Mask for the fill (controls visible width)
3660
+ this._fillMask = new Graphics();
3661
+ this._fillMask.rect(0, 0, 0, height).fill(0xffffff);
3662
+ this.addChild(this._fillMask);
3663
+ this._fill.mask = this._fillMask;
3062
3664
  // Border overlay
3063
3665
  this._borderGfx = new Graphics();
3064
3666
  if (borderColor !== undefined && borderWidth > 0) {
@@ -3076,7 +3678,7 @@ class ProgressBar extends Container {
3076
3678
  this._progress = Math.max(0, Math.min(1, value));
3077
3679
  if (!this._config.animated) {
3078
3680
  this._displayedProgress = this._progress;
3079
- this._bar.progress = this._displayedProgress * 100;
3681
+ this.updateMask();
3080
3682
  }
3081
3683
  }
3082
3684
  /**
@@ -3087,11 +3689,26 @@ class ProgressBar extends Container {
3087
3689
  return;
3088
3690
  if (Math.abs(this._displayedProgress - this._progress) < 0.001) {
3089
3691
  this._displayedProgress = this._progress;
3692
+ this.updateMask();
3090
3693
  return;
3091
3694
  }
3092
3695
  this._displayedProgress +=
3093
3696
  (this._progress - this._displayedProgress) * this._config.animationSpeed;
3094
- this._bar.progress = this._displayedProgress * 100;
3697
+ this.updateMask();
3698
+ }
3699
+ /** React reconciler update hook */
3700
+ updateConfig(changed) {
3701
+ if ('progress' in changed)
3702
+ this.progress = changed.progress;
3703
+ if ('animated' in changed)
3704
+ this._config.animated = changed.animated;
3705
+ if ('animationSpeed' in changed)
3706
+ this._config.animationSpeed = changed.animationSpeed;
3707
+ }
3708
+ updateMask() {
3709
+ const w = this._config.width * this._displayedProgress;
3710
+ this._fillMask.clear();
3711
+ this._fillMask.rect(0, 0, w, this._config.height).fill(0xffffff);
3095
3712
  }
3096
3713
  }
3097
3714
 
@@ -3109,6 +3726,7 @@ class ProgressBar extends Container {
3109
3726
  * ```
3110
3727
  */
3111
3728
  class Label extends Container {
3729
+ __uiComponent = true;
3112
3730
  _text;
3113
3731
  _maxWidth;
3114
3732
  _autoFit;
@@ -3175,6 +3793,21 @@ class Label extends Container {
3175
3793
  maximumFractionDigits: decimals,
3176
3794
  }).format(value);
3177
3795
  }
3796
+ /** React reconciler update hook */
3797
+ updateConfig(changed) {
3798
+ if ('text' in changed)
3799
+ this.text = changed.text;
3800
+ if ('maxWidth' in changed)
3801
+ this.maxWidth = changed.maxWidth;
3802
+ if ('autoFit' in changed) {
3803
+ this._autoFit = changed.autoFit;
3804
+ this.fitText();
3805
+ }
3806
+ if ('style' in changed && typeof changed.style === 'object') {
3807
+ Object.assign(this._text.style, changed.style);
3808
+ this.fitText();
3809
+ }
3810
+ }
3178
3811
  fitText() {
3179
3812
  if (!this._autoFit || this._maxWidth === Infinity)
3180
3813
  return;
@@ -3187,10 +3820,10 @@ class Label extends Container {
3187
3820
  }
3188
3821
 
3189
3822
  /**
3190
- * Background panel powered by `@pixi/layout` LayoutContainer.
3823
+ * Background panel with optional flexbox content layout.
3191
3824
  *
3192
3825
  * Supports both Graphics-based (color + border) and 9-slice sprite backgrounds.
3193
- * Children added to `content` participate in flexbox layout automatically.
3826
+ * Children added via `addContent()` participate in flex layout automatically.
3194
3827
  *
3195
3828
  * @example
3196
3829
  * ```ts
@@ -3205,9 +3838,14 @@ class Label extends Container {
3205
3838
  * });
3206
3839
  * ```
3207
3840
  */
3208
- class Panel extends LayoutContainer {
3841
+ class Panel extends Container {
3842
+ __uiComponent = true;
3843
+ _bg;
3844
+ _content;
3845
+ _internalSetup = true;
3209
3846
  _panelConfig;
3210
3847
  constructor(config = {}) {
3848
+ super();
3211
3849
  const resolvedConfig = {
3212
3850
  width: config.width ?? 400,
3213
3851
  height: config.height ?? 300,
@@ -3215,8 +3853,8 @@ class Panel extends LayoutContainer {
3215
3853
  backgroundAlpha: config.backgroundAlpha ?? 1,
3216
3854
  ...config,
3217
3855
  };
3218
- // If using a 9-slice texture, pass it as a custom background
3219
- let customBackground;
3856
+ this._panelConfig = resolvedConfig;
3857
+ // Create background
3220
3858
  if (config.nineSliceTexture) {
3221
3859
  const texture = typeof config.nineSliceTexture === 'string'
3222
3860
  ? Texture.from(config.nineSliceTexture)
@@ -3232,40 +3870,102 @@ class Panel extends LayoutContainer {
3232
3870
  nineSlice.width = resolvedConfig.width;
3233
3871
  nineSlice.height = resolvedConfig.height;
3234
3872
  nineSlice.alpha = resolvedConfig.backgroundAlpha;
3235
- customBackground = nineSlice;
3873
+ this._bg = nineSlice;
3236
3874
  }
3237
- super(customBackground ? { background: customBackground } : undefined);
3238
- this._panelConfig = resolvedConfig;
3239
- // Apply layout styles
3240
- const layoutStyles = {
3241
- width: resolvedConfig.width,
3242
- height: resolvedConfig.height,
3243
- padding: resolvedConfig.padding,
3244
- flexDirection: 'column',
3245
- };
3246
- // Graphics-based background via layout styles
3247
- if (!config.nineSliceTexture) {
3248
- layoutStyles.backgroundColor = config.backgroundColor ?? 0x1a1a2e;
3249
- layoutStyles.borderRadius = config.borderRadius ?? 0;
3875
+ else {
3876
+ const g = new Graphics();
3877
+ const bgColor = config.backgroundColor ?? 0x1a1a2e;
3878
+ const radius = config.borderRadius ?? 0;
3879
+ g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius).fill(bgColor);
3250
3880
  if (config.borderColor !== undefined && config.borderWidth) {
3251
- layoutStyles.borderColor = config.borderColor;
3252
- layoutStyles.borderWidth = config.borderWidth;
3881
+ g.roundRect(0, 0, resolvedConfig.width, resolvedConfig.height, radius)
3882
+ .stroke({ color: config.borderColor, width: config.borderWidth });
3253
3883
  }
3884
+ g.alpha = resolvedConfig.backgroundAlpha;
3885
+ this._bg = g;
3254
3886
  }
3255
- this.layout = layoutStyles;
3256
- if (!config.nineSliceTexture) {
3257
- this.background.alpha = resolvedConfig.backgroundAlpha;
3258
- }
3887
+ this.addChild(this._bg);
3888
+ // Create content flex container
3889
+ this._content = new FlexContainer({
3890
+ ...config.layout,
3891
+ direction: config.layout?.direction ?? 'column',
3892
+ justifyContent: config.layout?.justifyContent ?? 'start',
3893
+ alignItems: config.layout?.alignItems ?? 'start',
3894
+ gap: config.layout?.gap ?? 0,
3895
+ padding: resolvedConfig.padding,
3896
+ width: resolvedConfig.width,
3897
+ height: resolvedConfig.height,
3898
+ });
3899
+ this.addChild(this._content);
3900
+ this._internalSetup = false;
3259
3901
  }
3260
- /** Access the content container (children added here participate in layout) */
3902
+ /** Access the content flex container — add children here for layout */
3261
3903
  get content() {
3262
- return this.overflowContainer;
3904
+ return this._content;
3905
+ }
3906
+ /** Convenience: add a child to the content layout */
3907
+ addContent(child) {
3908
+ this._content.addFlexChild(child);
3909
+ this._content.updateLayout();
3910
+ return this;
3263
3911
  }
3264
3912
  /** Resize the panel */
3265
3913
  setSize(width, height) {
3266
3914
  this._panelConfig.width = width;
3267
3915
  this._panelConfig.height = height;
3268
- this._layout?.setStyle({ width, height });
3916
+ // Resize background
3917
+ if (this._bg instanceof NineSliceSprite) {
3918
+ this._bg.width = width;
3919
+ this._bg.height = height;
3920
+ }
3921
+ else if (this._bg instanceof Graphics) {
3922
+ const radius = this._panelConfig.borderRadius ?? 0;
3923
+ const bgColor = this._panelConfig.backgroundColor ?? 0x1a1a2e;
3924
+ this._bg.clear();
3925
+ this._bg.roundRect(0, 0, width, height, radius).fill(bgColor);
3926
+ if (this._panelConfig.borderColor !== undefined && this._panelConfig.borderWidth) {
3927
+ this._bg.roundRect(0, 0, width, height, radius)
3928
+ .stroke({ color: this._panelConfig.borderColor, width: this._panelConfig.borderWidth });
3929
+ }
3930
+ this._bg.alpha = this._panelConfig.backgroundAlpha;
3931
+ }
3932
+ this._content.resize(width, height);
3933
+ }
3934
+ /**
3935
+ * Override addChild so external children are routed to content FlexContainer.
3936
+ * Enables `<panel><label /><button /></panel>` in React JSX.
3937
+ */
3938
+ addChild(...children) {
3939
+ if (this._internalSetup) {
3940
+ return super.addChild(...children);
3941
+ }
3942
+ for (const child of children) {
3943
+ this._content.addFlexChild(child);
3944
+ }
3945
+ this._content.updateLayout();
3946
+ return children[0];
3947
+ }
3948
+ removeChild(...children) {
3949
+ if (this._internalSetup) {
3950
+ return super.removeChild(...children);
3951
+ }
3952
+ for (const child of children) {
3953
+ this._content.removeFlexChild(child);
3954
+ }
3955
+ return children[0];
3956
+ }
3957
+ /** React reconciler update hook */
3958
+ updateConfig(changed) {
3959
+ if ('width' in changed || 'height' in changed) {
3960
+ this.setSize(changed.width ?? this._panelConfig.width, changed.height ?? this._panelConfig.height);
3961
+ }
3962
+ if ('backgroundAlpha' in changed) {
3963
+ this._panelConfig.backgroundAlpha = changed.backgroundAlpha;
3964
+ this._bg.alpha = changed.backgroundAlpha;
3965
+ }
3966
+ }
3967
+ destroy(options) {
3968
+ super.destroy(options);
3269
3969
  }
3270
3970
  }
3271
3971
 
@@ -3273,7 +3973,7 @@ class Panel extends LayoutContainer {
3273
3973
  * Reactive balance display component.
3274
3974
  *
3275
3975
  * Automatically formats currency and can animate value changes
3276
- * with a smooth countup/countdown effect.
3976
+ * with a smooth countup/countdown effect using engine Tween.
3277
3977
  *
3278
3978
  * @example
3279
3979
  * ```ts
@@ -3285,13 +3985,14 @@ class Panel extends LayoutContainer {
3285
3985
  * ```
3286
3986
  */
3287
3987
  class BalanceDisplay extends Container {
3988
+ __uiComponent = true;
3288
3989
  _prefixLabel = null;
3289
3990
  _valueLabel;
3290
3991
  _config;
3291
3992
  _currentValue = 0;
3292
3993
  _displayedValue = 0;
3293
- _animating = false;
3294
- _animationCancelled = false;
3994
+ /** Internal target for Tween animation */
3995
+ _tweenTarget = { value: 0 };
3295
3996
  constructor(config = {}) {
3296
3997
  super();
3297
3998
  this._config = {
@@ -3352,37 +4053,13 @@ class BalanceDisplay extends Container {
3352
4053
  this._config.currency = currency;
3353
4054
  this.updateDisplay();
3354
4055
  }
3355
- async animateValue(from, to) {
3356
- if (this._animating) {
3357
- this._animationCancelled = true;
3358
- }
3359
- this._animating = true;
3360
- this._animationCancelled = false;
3361
- const duration = this._config.animationDuration;
3362
- const startTime = Date.now();
3363
- return new Promise((resolve) => {
3364
- const tick = () => {
3365
- if (this._animationCancelled) {
3366
- this._animating = false;
3367
- resolve();
3368
- return;
3369
- }
3370
- const elapsed = Date.now() - startTime;
3371
- const t = Math.min(elapsed / duration, 1);
3372
- const eased = Easing.easeOutCubic(t);
3373
- this._displayedValue = from + (to - from) * eased;
3374
- this.updateDisplay();
3375
- if (t < 1) {
3376
- requestAnimationFrame(tick);
3377
- }
3378
- else {
3379
- this._displayedValue = to;
3380
- this.updateDisplay();
3381
- this._animating = false;
3382
- resolve();
3383
- }
3384
- };
3385
- requestAnimationFrame(tick);
4056
+ animateValue(from, to) {
4057
+ // Cancel any running animation
4058
+ Tween.killTweensOf(this._tweenTarget);
4059
+ this._tweenTarget.value = from;
4060
+ Tween.to(this._tweenTarget, { value: to }, this._config.animationDuration, Easing.easeOutCubic, () => {
4061
+ this._displayedValue = this._tweenTarget.value;
4062
+ this.updateDisplay();
3386
4063
  });
3387
4064
  }
3388
4065
  updateDisplay() {
@@ -3394,13 +4071,24 @@ class BalanceDisplay extends Container {
3394
4071
  this._valueLabel.y = 14;
3395
4072
  }
3396
4073
  }
4074
+ /** React reconciler update hook */
4075
+ updateConfig(changed) {
4076
+ if ('value' in changed)
4077
+ this.setValue(changed.value);
4078
+ if ('currency' in changed)
4079
+ this.setCurrency(changed.currency);
4080
+ }
4081
+ destroy(options) {
4082
+ Tween.killTweensOf(this._tweenTarget);
4083
+ super.destroy(options);
4084
+ }
3397
4085
  }
3398
4086
 
3399
4087
  /**
3400
4088
  * Win amount display with countup animation.
3401
4089
  *
3402
4090
  * Shows a dramatic countup from 0 to the win amount, with optional
3403
- * scale pop effect — typical of slot games.
4091
+ * scale pop effect — typical of slot games. Uses engine Tween system.
3404
4092
  *
3405
4093
  * @example
3406
4094
  * ```ts
@@ -3411,9 +4099,11 @@ class BalanceDisplay extends Container {
3411
4099
  * ```
3412
4100
  */
3413
4101
  class WinDisplay extends Container {
4102
+ __uiComponent = true;
3414
4103
  _label;
3415
4104
  _config;
3416
- _cancelCountup = false;
4105
+ /** Internal target for Tween countup */
4106
+ _tweenTarget = { value: 0 };
3417
4107
  constructor(config = {}) {
3418
4108
  super();
3419
4109
  this._config = {
@@ -3443,47 +4133,30 @@ class WinDisplay extends Container {
3443
4133
  */
3444
4134
  async showWin(amount) {
3445
4135
  this.visible = true;
3446
- this._cancelCountup = false;
3447
4136
  this.alpha = 1;
3448
- const duration = this._config.countupDuration;
3449
- const startTime = Date.now();
3450
- // Scale pop
4137
+ // Cancel any running animation
4138
+ Tween.killTweensOf(this._tweenTarget);
4139
+ Tween.killTweensOf(this);
4140
+ // Setup countup
4141
+ this._tweenTarget.value = 0;
3451
4142
  this.scale.set(0.5);
3452
- return new Promise((resolve) => {
3453
- const tick = () => {
3454
- if (this._cancelCountup) {
3455
- this.displayAmount(amount);
3456
- resolve();
3457
- return;
3458
- }
3459
- const elapsed = Date.now() - startTime;
3460
- const t = Math.min(elapsed / duration, 1);
3461
- const eased = Easing.easeOutCubic(t);
3462
- // Countup
3463
- const current = amount * eased;
3464
- this.displayAmount(current);
3465
- // Scale animation
3466
- const scaleT = Math.min(elapsed / 300, 1);
3467
- const scaleEased = Easing.easeOutBack(scaleT);
3468
- const targetScale = 1;
3469
- this.scale.set(0.5 + (targetScale - 0.5) * scaleEased);
3470
- if (t < 1) {
3471
- requestAnimationFrame(tick);
3472
- }
3473
- else {
3474
- this.displayAmount(amount);
3475
- this.scale.set(1);
3476
- resolve();
3477
- }
3478
- };
3479
- requestAnimationFrame(tick);
4143
+ // Scale pop animation
4144
+ const scalePromise = Tween.to(this, { 'scale.x': 1, 'scale.y': 1 }, 300, Easing.easeOutBack);
4145
+ // Countup animation
4146
+ const countupPromise = Tween.to(this._tweenTarget, { value: amount }, this._config.countupDuration, Easing.easeOutCubic, () => {
4147
+ this.displayAmount(this._tweenTarget.value);
3480
4148
  });
4149
+ await Promise.all([scalePromise, countupPromise]);
4150
+ // Ensure final value is exact
4151
+ this.displayAmount(amount);
4152
+ this.scale.set(1);
3481
4153
  }
3482
4154
  /**
3483
4155
  * Skip the countup animation and show the final amount immediately.
3484
4156
  */
3485
4157
  skipCountup(amount) {
3486
- this._cancelCountup = true;
4158
+ Tween.killTweensOf(this._tweenTarget);
4159
+ Tween.killTweensOf(this);
3487
4160
  this.displayAmount(amount);
3488
4161
  this.scale.set(1);
3489
4162
  }
@@ -3491,19 +4164,33 @@ class WinDisplay extends Container {
3491
4164
  * Hide the win display.
3492
4165
  */
3493
4166
  hide() {
4167
+ Tween.killTweensOf(this._tweenTarget);
4168
+ Tween.killTweensOf(this);
3494
4169
  this.visible = false;
3495
4170
  this._label.text = '';
3496
4171
  }
3497
4172
  displayAmount(amount) {
3498
4173
  this._label.setCurrency(amount, this._config.currency, this._config.locale);
3499
4174
  }
4175
+ /** React reconciler update hook */
4176
+ updateConfig(changed) {
4177
+ if ('currency' in changed)
4178
+ this._config.currency = changed.currency;
4179
+ if ('locale' in changed)
4180
+ this._config.locale = changed.locale;
4181
+ }
4182
+ destroy(options) {
4183
+ Tween.killTweensOf(this._tweenTarget);
4184
+ Tween.killTweensOf(this);
4185
+ super.destroy(options);
4186
+ }
3500
4187
  }
3501
4188
 
3502
4189
  /**
3503
4190
  * Modal overlay component.
3504
4191
  * Shows content on top of a dark overlay with enter/exit animations.
3505
4192
  *
3506
- * The content container uses `@pixi/layout` for automatic centering.
4193
+ * Content is automatically centered via position calculations.
3507
4194
  *
3508
4195
  * @example
3509
4196
  * ```ts
@@ -3514,6 +4201,7 @@ class WinDisplay extends Container {
3514
4201
  * ```
3515
4202
  */
3516
4203
  class Modal extends Container {
4204
+ __uiComponent = true;
3517
4205
  _overlay;
3518
4206
  _contentContainer;
3519
4207
  _config;
@@ -3583,6 +4271,17 @@ class Modal extends Container {
3583
4271
  this._showing = false;
3584
4272
  this.onClose?.();
3585
4273
  }
4274
+ /** React reconciler update hook */
4275
+ updateConfig(changed) {
4276
+ if ('overlayAlpha' in changed)
4277
+ this._config.overlayAlpha = changed.overlayAlpha;
4278
+ if ('closeOnOverlay' in changed)
4279
+ this._config.closeOnOverlay = changed.closeOnOverlay;
4280
+ if ('animationDuration' in changed)
4281
+ this._config.animationDuration = changed.animationDuration;
4282
+ if ('onClose' in changed)
4283
+ this.onClose = changed.onClose;
4284
+ }
3586
4285
  }
3587
4286
 
3588
4287
  const TOAST_COLORS = {
@@ -3602,17 +4301,21 @@ const TOAST_COLORS = {
3602
4301
  * ```
3603
4302
  */
3604
4303
  class Toast extends Container {
4304
+ __uiComponent = true;
3605
4305
  _bg;
4306
+ _customBg;
3606
4307
  _text;
3607
4308
  _config;
3608
- _dismissTimeout = null;
4309
+ _dismissPending = false;
3609
4310
  constructor(config = {}) {
3610
4311
  super();
3611
4312
  this._config = {
3612
4313
  duration: config.duration ?? 3000,
3613
4314
  bottomOffset: config.bottomOffset ?? 60,
3614
4315
  };
3615
- this._bg = new Graphics();
4316
+ const customBg = resolveView(config.backgroundView);
4317
+ this._customBg = !!customBg;
4318
+ this._bg = customBg ?? new Graphics();
3616
4319
  this.addChild(this._bg);
3617
4320
  this._text = new Text({
3618
4321
  text: '',
@@ -3630,18 +4333,27 @@ class Toast extends Container {
3630
4333
  * Show a toast message.
3631
4334
  */
3632
4335
  async show(message, type = 'info', viewWidth, viewHeight) {
3633
- if (this._dismissTimeout) {
3634
- clearTimeout(this._dismissTimeout);
3635
- }
4336
+ // Cancel any pending dismiss
4337
+ Tween.killTweensOf(this);
4338
+ this._dismissPending = false;
3636
4339
  this._text.text = message;
3637
4340
  const padding = 20;
3638
4341
  const width = Math.max(200, this._text.width + padding * 2);
3639
4342
  const height = 44;
3640
4343
  const radius = 8;
3641
4344
  // Draw the background
3642
- this._bg.clear();
3643
- this._bg.roundRect(-width / 2, -height / 2, width, height, radius);
3644
- this._bg.fill(TOAST_COLORS[type]);
4345
+ if (this._customBg) {
4346
+ this._bg.width = width;
4347
+ this._bg.height = height;
4348
+ this._bg.x = -width / 2;
4349
+ this._bg.y = -height / 2;
4350
+ }
4351
+ else {
4352
+ const g = this._bg;
4353
+ g.clear();
4354
+ g.roundRect(-width / 2, -height / 2, width, height, radius);
4355
+ g.fill(TOAST_COLORS[type]);
4356
+ }
3645
4357
  // Position
3646
4358
  if (viewWidth && viewHeight) {
3647
4359
  this.x = viewWidth / 2;
@@ -3652,9 +4364,12 @@ class Toast extends Container {
3652
4364
  this.y += 20;
3653
4365
  await Tween.to(this, { alpha: 1, y: this.y - 20 }, 300, Easing.easeOutCubic);
3654
4366
  if (this._config.duration > 0) {
3655
- this._dismissTimeout = setTimeout(() => {
3656
- this.dismiss();
3657
- }, this._config.duration);
4367
+ this._dismissPending = true;
4368
+ await Tween.delay(this._config.duration);
4369
+ if (this._dismissPending) {
4370
+ this._dismissPending = false;
4371
+ await this.dismiss();
4372
+ }
3658
4373
  }
3659
4374
  }
3660
4375
  /**
@@ -3663,57 +4378,36 @@ class Toast extends Container {
3663
4378
  async dismiss() {
3664
4379
  if (!this.visible)
3665
4380
  return;
3666
- if (this._dismissTimeout) {
3667
- clearTimeout(this._dismissTimeout);
3668
- this._dismissTimeout = null;
3669
- }
4381
+ this._dismissPending = false;
4382
+ Tween.killTweensOf(this);
3670
4383
  await Tween.to(this, { alpha: 0, y: this.y + 20 }, 200, Easing.easeInCubic);
3671
4384
  this.visible = false;
3672
4385
  }
4386
+ /** React reconciler update hook */
4387
+ updateConfig(changed) {
4388
+ if ('duration' in changed)
4389
+ this._config.duration = changed.duration;
4390
+ if ('bottomOffset' in changed)
4391
+ this._config.bottomOffset = changed.bottomOffset;
4392
+ }
4393
+ destroy(options) {
4394
+ this._dismissPending = false;
4395
+ Tween.killTweensOf(this);
4396
+ super.destroy(options);
4397
+ }
3673
4398
  }
3674
4399
 
3675
4400
  // ─── Helpers ─────────────────────────────────────────────
3676
- const ALIGNMENT_MAP = {
3677
- start: 'flex-start',
3678
- center: 'center',
3679
- end: 'flex-end',
3680
- stretch: 'stretch',
3681
- };
3682
- function normalizePadding(padding) {
3683
- if (typeof padding === 'number')
3684
- return [padding, padding, padding, padding];
3685
- return padding;
3686
- }
3687
- function directionToFlexStyles(direction, maxWidth) {
4401
+ function directionToFlex(direction) {
3688
4402
  switch (direction) {
3689
- case 'horizontal':
3690
- return { flexDirection: 'row', flexWrap: 'nowrap' };
3691
- case 'vertical':
3692
- return { flexDirection: 'column', flexWrap: 'nowrap' };
3693
- case 'grid':
3694
- return { flexDirection: 'row', flexWrap: 'wrap' };
3695
- case 'wrap':
3696
- return {
3697
- flexDirection: 'row',
3698
- flexWrap: 'wrap',
3699
- ...(maxWidth < Infinity ? { maxWidth } : {}),
3700
- };
4403
+ case 'horizontal': return { direction: 'row', wrap: false };
4404
+ case 'vertical': return { direction: 'column', wrap: false };
4405
+ case 'grid': return { direction: 'row', wrap: true };
4406
+ case 'wrap': return { direction: 'row', wrap: true };
3701
4407
  }
3702
4408
  }
3703
- function buildLayoutStyles(config) {
3704
- const [pt, pr, pb, pl] = config.padding;
3705
- return {
3706
- ...directionToFlexStyles(config.direction, config.maxWidth),
3707
- gap: config.gap,
3708
- alignItems: ALIGNMENT_MAP[config.alignment],
3709
- paddingTop: pt,
3710
- paddingRight: pr,
3711
- paddingBottom: pb,
3712
- paddingLeft: pl,
3713
- };
3714
- }
3715
4409
  /**
3716
- * Responsive layout container powered by `@pixi/layout` (Yoga flexbox engine).
4410
+ * Responsive layout container powered by a lightweight built-in flex layout solver.
3717
4411
  *
3718
4412
  * Supports horizontal, vertical, grid, and wrap layout modes with
3719
4413
  * alignment, padding, gap, and viewport-anchor positioning.
@@ -3740,6 +4434,7 @@ function buildLayoutStyles(config) {
3740
4434
  * ```
3741
4435
  */
3742
4436
  class Layout extends Container {
4437
+ __uiComponent = true;
3743
4438
  _layoutConfig;
3744
4439
  _padding;
3745
4440
  _anchor;
@@ -3748,6 +4443,7 @@ class Layout extends Container {
3748
4443
  _items = [];
3749
4444
  _viewportWidth = 0;
3750
4445
  _viewportHeight = 0;
4446
+ _flex;
3751
4447
  constructor(config = {}) {
3752
4448
  super();
3753
4449
  this._layoutConfig = {
@@ -3757,7 +4453,7 @@ class Layout extends Container {
3757
4453
  autoLayout: config.autoLayout ?? true,
3758
4454
  columns: config.columns ?? 2,
3759
4455
  };
3760
- this._padding = normalizePadding(config.padding ?? 0);
4456
+ this._padding = config.padding ?? 0;
3761
4457
  this._anchor = config.anchor ?? 'top-left';
3762
4458
  this._maxWidth = config.maxWidth ?? Infinity;
3763
4459
  this._breakpoints = config.breakpoints
@@ -3765,14 +4461,18 @@ class Layout extends Container {
3765
4461
  .map(([w, cfg]) => [Number(w), cfg])
3766
4462
  .sort((a, b) => a[0] - b[0])
3767
4463
  : [];
4464
+ // Create internal FlexContainer
4465
+ this._flex = new FlexContainer();
4466
+ super.addChild(this._flex);
3768
4467
  this.applyLayoutStyles();
3769
4468
  }
3770
4469
  /** Add an item to the layout */
3771
4470
  addItem(child) {
3772
4471
  this._items.push(child);
3773
- this.addChild(child);
3774
- if (this._layoutConfig.direction === 'grid') {
3775
- this.applyGridChildWidth(child);
4472
+ const flexConfig = this.buildFlexItemConfig(child);
4473
+ this._flex.addFlexChild(child, flexConfig);
4474
+ if (this._layoutConfig.autoLayout) {
4475
+ this.applyLayoutStyles();
3776
4476
  }
3777
4477
  return this;
3778
4478
  }
@@ -3781,15 +4481,13 @@ class Layout extends Container {
3781
4481
  const idx = this._items.indexOf(child);
3782
4482
  if (idx !== -1) {
3783
4483
  this._items.splice(idx, 1);
3784
- this.removeChild(child);
4484
+ this._flex.removeFlexChild(child);
3785
4485
  }
3786
4486
  return this;
3787
4487
  }
3788
4488
  /** Remove all items */
3789
4489
  clearItems() {
3790
- for (const item of this._items) {
3791
- this.removeChild(item);
3792
- }
4490
+ this._flex.clearFlexChildren();
3793
4491
  this._items.length = 0;
3794
4492
  return this;
3795
4493
  }
@@ -3812,43 +4510,58 @@ class Layout extends Container {
3812
4510
  const direction = effective.direction ?? this._layoutConfig.direction;
3813
4511
  const gap = effective.gap ?? this._layoutConfig.gap;
3814
4512
  const alignment = effective.alignment ?? this._layoutConfig.alignment;
3815
- effective.columns ?? this._layoutConfig.columns;
3816
- const padding = effective.padding !== undefined
3817
- ? normalizePadding(effective.padding)
3818
- : this._padding;
4513
+ const padding = effective.padding ?? this._padding;
3819
4514
  const maxWidth = effective.maxWidth ?? this._maxWidth;
3820
- const styles = buildLayoutStyles({ direction, gap, alignment, padding, maxWidth });
3821
- this.layout = styles;
4515
+ const { direction: flexDir, wrap } = directionToFlex(direction);
4516
+ this._flex.setDirection(flexDir);
4517
+ this._flex.setJustifyContent('start');
4518
+ this._flex.setAlignItems(alignment);
4519
+ this._flex.setGap(gap);
4520
+ this._flex.setPadding(padding);
4521
+ // Wrap and maxWidth
4522
+ if (wrap) {
4523
+ this._flex._config.flexWrap = true;
4524
+ if (direction === 'grid' && maxWidth < Infinity) {
4525
+ this._flex._maxWidth = maxWidth;
4526
+ }
4527
+ if (maxWidth < Infinity) {
4528
+ this._flex._maxWidth = maxWidth;
4529
+ }
4530
+ }
4531
+ else {
4532
+ this._flex._config.flexWrap = false;
4533
+ }
4534
+ // Update grid child widths
3822
4535
  if (direction === 'grid') {
3823
4536
  for (const item of this._items) {
3824
- this.applyGridChildWidth(item);
4537
+ const flexConfig = this.buildFlexItemConfig(item);
4538
+ item._flexConfig = flexConfig;
3825
4539
  }
3826
4540
  }
4541
+ // Set explicit size if we have viewport dimensions
4542
+ if (this._viewportWidth > 0 && this._viewportHeight > 0) {
4543
+ this._flex.resize(this._viewportWidth, this._viewportHeight);
4544
+ }
4545
+ else {
4546
+ this._flex.updateLayout();
4547
+ }
3827
4548
  }
3828
- applyGridChildWidth(child) {
4549
+ buildFlexItemConfig(_child) {
3829
4550
  const effective = this.resolveConfig();
4551
+ const direction = effective.direction ?? this._layoutConfig.direction;
3830
4552
  const columns = effective.columns ?? this._layoutConfig.columns;
3831
- const gap = effective.gap ?? this._layoutConfig.gap;
3832
- // Account for gaps between columns: total gap space = gap * (columns - 1)
3833
- // Each column gets: (100% - total_gap) / columns
3834
- // We use flexBasis + flexGrow to let Yoga handle the math when gap > 0
3835
- const styles = gap > 0
3836
- ? { flexBasis: 0, flexGrow: 1, flexShrink: 1, maxWidth: `${(100 / columns).toFixed(2)}%` }
3837
- : { width: `${(100 / columns).toFixed(2)}%` };
3838
- if (child._layout) {
3839
- child._layout.setStyle(styles);
3840
- }
3841
- else {
3842
- child.layout = styles;
4553
+ if (direction === 'grid' && columns > 0) {
4554
+ // For grid, give each item a proportional width
4555
+ // The actual pixel width will be computed during layout
4556
+ return { flexGrow: 1 };
3843
4557
  }
4558
+ return undefined;
3844
4559
  }
3845
4560
  applyAnchor() {
3846
4561
  const anchor = this.resolveConfig().anchor ?? this._anchor;
3847
4562
  if (this._viewportWidth === 0 || this._viewportHeight === 0)
3848
4563
  return;
3849
- const bounds = this.getLocalBounds();
3850
- const contentW = bounds.width * this.scale.x;
3851
- const contentH = bounds.height * this.scale.y;
4564
+ const { width: contentW, height: contentH } = this._flex.getContentSize();
3852
4565
  const vw = this._viewportWidth;
3853
4566
  const vh = this._viewportHeight;
3854
4567
  let anchorX = 0;
@@ -3871,8 +4584,8 @@ class Layout extends Container {
3871
4584
  else {
3872
4585
  anchorY = (vh - contentH) / 2;
3873
4586
  }
3874
- this.x = anchorX - bounds.x * this.scale.x;
3875
- this.y = anchorY - bounds.y * this.scale.y;
4587
+ this.x = anchorX;
4588
+ this.y = anchorY;
3876
4589
  }
3877
4590
  resolveConfig() {
3878
4591
  if (this._breakpoints.length === 0 || this._viewportWidth === 0) {
@@ -3885,18 +4598,34 @@ class Layout extends Container {
3885
4598
  }
3886
4599
  return {};
3887
4600
  }
4601
+ /** React reconciler update hook */
4602
+ updateConfig(changed) {
4603
+ if ('direction' in changed)
4604
+ this._layoutConfig.direction = changed.direction;
4605
+ if ('gap' in changed)
4606
+ this._layoutConfig.gap = changed.gap;
4607
+ if ('alignment' in changed)
4608
+ this._layoutConfig.alignment = changed.alignment;
4609
+ if ('anchor' in changed)
4610
+ this._anchor = changed.anchor;
4611
+ if ('padding' in changed)
4612
+ this._padding = changed.padding;
4613
+ if ('columns' in changed)
4614
+ this._layoutConfig.columns = changed.columns;
4615
+ this.applyLayoutStyles();
4616
+ if (this._viewportWidth > 0)
4617
+ this.applyAnchor();
4618
+ }
4619
+ destroy(options) {
4620
+ this._items.length = 0;
4621
+ super.destroy(options);
4622
+ }
3888
4623
  }
3889
4624
 
3890
- const DIRECTION_MAP = {
3891
- vertical: 'vertical',
3892
- horizontal: 'horizontal',
3893
- both: 'bidirectional',
3894
- };
4625
+ const DECELERATION = 0.95;
4626
+ const MIN_VELOCITY = 0.5;
3895
4627
  /**
3896
- * Scrollable container powered by `@pixi/ui` ScrollBox.
3897
- *
3898
- * Provides touch/drag scrolling, mouse wheel support, inertia, and
3899
- * dynamic rendering optimization for off-screen items.
4628
+ * Scrollable container with touch/drag, mouse wheel, and inertia.
3900
4629
  *
3901
4630
  * @example
3902
4631
  * ```ts
@@ -3914,53 +4643,349 @@ const DIRECTION_MAP = {
3914
4643
  * scene.container.addChild(scroll);
3915
4644
  * ```
3916
4645
  */
3917
- class ScrollContainer extends ScrollBox {
4646
+ class ScrollContainer extends Container {
4647
+ __uiComponent = true;
4648
+ _viewport;
4649
+ _internalSetup = true;
4650
+ _content;
4651
+ _maskGfx;
4652
+ _bg = null;
3918
4653
  _scrollConfig;
4654
+ _items = [];
4655
+ // Scrollbar
4656
+ _scrollbar = null;
4657
+ _scrollbarConfig;
4658
+ // Drag state
4659
+ _dragging = false;
4660
+ _dragStart = { x: 0, y: 0 };
4661
+ _contentStart = { x: 0, y: 0 };
4662
+ _velocity = { x: 0, y: 0 };
4663
+ _lastDragPos = { x: 0, y: 0 };
4664
+ _lastDragTime = 0;
4665
+ _inertiaActive = false;
4666
+ // Bound handlers for cleanup
4667
+ _onTickBound = null;
4668
+ _onWheelBound = null;
3919
4669
  constructor(config) {
3920
- const options = {
3921
- width: config.width,
3922
- height: config.height,
3923
- type: DIRECTION_MAP[config.direction ?? 'vertical'],
3924
- radius: config.borderRadius ?? 0,
4670
+ super();
4671
+ this._viewport = { width: config.width, height: config.height };
4672
+ this._scrollConfig = {
4673
+ direction: config.direction ?? 'vertical',
3925
4674
  elementsMargin: config.elementsMargin ?? 0,
3926
4675
  padding: config.padding ?? 0,
3927
- disableDynamicRendering: config.disableDynamicRendering ?? false,
4676
+ borderRadius: config.borderRadius ?? 0,
3928
4677
  disableEasing: config.disableEasing ?? false,
3929
- globalScroll: config.globalScroll ?? true,
3930
4678
  };
4679
+ // Background
3931
4680
  if (config.backgroundColor !== undefined) {
3932
- options.background = config.backgroundColor;
4681
+ this._bg = new Graphics();
4682
+ this._bg.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
4683
+ .fill(config.backgroundColor);
4684
+ this.addChild(this._bg);
4685
+ }
4686
+ // Mask
4687
+ this._maskGfx = new Graphics();
4688
+ this._maskGfx.roundRect(0, 0, config.width, config.height, this._scrollConfig.borderRadius)
4689
+ .fill(0xffffff);
4690
+ this.addChild(this._maskGfx);
4691
+ // Content container
4692
+ this._content = new Container();
4693
+ this._content.mask = this._maskGfx;
4694
+ this.addChild(this._content);
4695
+ // Interaction
4696
+ this.eventMode = 'static';
4697
+ this.hitArea = { contains: (x, y) => x >= 0 && x <= config.width && y >= 0 && y <= config.height };
4698
+ this.on('pointerdown', this._onPointerDown, this);
4699
+ this.on('pointermove', this._onPointerMove, this);
4700
+ this.on('pointerup', this._onPointerUp, this);
4701
+ this.on('pointerupoutside', this._onPointerUp, this);
4702
+ // Mouse wheel
4703
+ this._onWheelBound = this._onWheel.bind(this);
4704
+ // Scrollbar
4705
+ const sbWidth = config.scrollbarWidth ?? 6;
4706
+ const sbPadding = config.scrollbarPadding ?? 4;
4707
+ this._scrollbarConfig = { width: sbWidth, padding: sbPadding };
4708
+ if (config.scrollbar) {
4709
+ const customThumb = resolveView(config.thumbView);
4710
+ if (customThumb) {
4711
+ this._scrollbar = customThumb;
4712
+ }
4713
+ else {
4714
+ const g = new Graphics();
4715
+ g.roundRect(0, 0, sbWidth, 40, sbWidth / 2).fill(config.scrollbarColor ?? 0xaaaaaa);
4716
+ g.alpha = config.scrollbarAlpha ?? 0.5;
4717
+ this._scrollbar = g;
4718
+ }
4719
+ this._scrollbar.visible = false;
4720
+ super.addChild(this._scrollbar);
3933
4721
  }
3934
- super(options);
3935
- this._scrollConfig = config;
4722
+ this._internalSetup = false;
3936
4723
  }
3937
- /** Set scrollable content. Replaces any existing content. */
3938
- setContent(content) {
3939
- // Remove existing items
3940
- const existing = this.items;
3941
- if (existing.length > 0) {
3942
- for (let i = existing.length - 1; i >= 0; i--) {
3943
- this.removeItem(i);
4724
+ /**
4725
+ * Override addChild so external children are routed to scroll content.
4726
+ * Enables `<scrollContainer><label /><panel /></scrollContainer>` in React JSX.
4727
+ */
4728
+ addChild(...children) {
4729
+ if (this._internalSetup) {
4730
+ return super.addChild(...children);
4731
+ }
4732
+ for (const child of children) {
4733
+ this.addItem(child);
4734
+ }
4735
+ return children[0];
4736
+ }
4737
+ removeChild(...children) {
4738
+ if (this._internalSetup) {
4739
+ return super.removeChild(...children);
4740
+ }
4741
+ for (const child of children) {
4742
+ const idx = this._items.indexOf(child);
4743
+ if (idx !== -1) {
4744
+ this._items.splice(idx, 1);
4745
+ this._content.removeChild(child);
3944
4746
  }
3945
4747
  }
3946
- // Add all children from the content container
4748
+ this.layoutItems();
4749
+ return children[0];
4750
+ }
4751
+ /** React reconciler update hook */
4752
+ updateConfig(changed) {
4753
+ if ('width' in changed || 'height' in changed) {
4754
+ this.setViewportSize(changed.width ?? this._viewport.width, changed.height ?? this._viewport.height);
4755
+ }
4756
+ }
4757
+ /** Enable mouse wheel scrolling (call after adding to stage) */
4758
+ enableWheel(canvas) {
4759
+ if (this._onWheelBound) {
4760
+ canvas.addEventListener('wheel', this._onWheelBound, { passive: false });
4761
+ }
4762
+ }
4763
+ /** Set scrollable content. Replaces any existing items. */
4764
+ setContent(content) {
4765
+ this.clearItems();
3947
4766
  const children = [...content.children];
3948
- if (children.length > 0) {
3949
- this.addItems(children);
4767
+ for (const child of children) {
4768
+ this.addItem(child);
3950
4769
  }
3951
4770
  }
3952
4771
  /** Add a single item */
3953
- addItem(...items) {
3954
- this.addItems(items);
3955
- return items[0];
4772
+ addItem(child) {
4773
+ this._items.push(child);
4774
+ this._content.addChild(child);
4775
+ this.layoutItems();
4776
+ return this;
4777
+ }
4778
+ /** Remove all items */
4779
+ clearItems() {
4780
+ for (const item of this._items) {
4781
+ this._content.removeChild(item);
4782
+ }
4783
+ this._items.length = 0;
4784
+ }
4785
+ /** Get items */
4786
+ get items() {
4787
+ return this._items;
3956
4788
  }
3957
- /** Scroll to make a specific item/child visible */
4789
+ /** Scroll to make a specific item index visible */
3958
4790
  scrollToItem(index) {
3959
- this.scrollTo(index);
4791
+ if (index < 0 || index >= this._items.length)
4792
+ return;
4793
+ const item = this._items[index];
4794
+ const isVert = this._scrollConfig.direction !== 'horizontal';
4795
+ if (isVert) {
4796
+ this._content.y = -item.y + this._scrollConfig.padding;
4797
+ }
4798
+ else {
4799
+ this._content.x = -item.x + this._scrollConfig.padding;
4800
+ }
4801
+ this.clampScroll();
3960
4802
  }
3961
4803
  /** Current scroll position */
3962
4804
  get scrollPosition() {
3963
- return { x: this.scrollX, y: this.scrollY };
4805
+ return { x: this._content.x, y: this._content.y };
4806
+ }
4807
+ /** Resize the scroll viewport */
4808
+ setViewportSize(width, height) {
4809
+ this._viewport.width = width;
4810
+ this._viewport.height = height;
4811
+ this._maskGfx.clear();
4812
+ this._maskGfx.roundRect(0, 0, width, height, this._scrollConfig.borderRadius).fill(0xffffff);
4813
+ if (this._bg) {
4814
+ this._bg.clear();
4815
+ this._bg.roundRect(0, 0, width, height, this._scrollConfig.borderRadius)
4816
+ .fill(0xffffff); // color will be overridden if needed
4817
+ }
4818
+ this.clampScroll();
4819
+ }
4820
+ // ─── Layout ──────────────────────────────────────────
4821
+ layoutItems() {
4822
+ const { direction, elementsMargin, padding } = this._scrollConfig;
4823
+ const isVert = direction !== 'horizontal';
4824
+ let pos = padding;
4825
+ for (const item of this._items) {
4826
+ if (isVert) {
4827
+ item.x = padding;
4828
+ item.y = pos;
4829
+ pos += item.height + elementsMargin;
4830
+ }
4831
+ else {
4832
+ item.x = pos;
4833
+ item.y = padding;
4834
+ pos += item.width + elementsMargin;
4835
+ }
4836
+ }
4837
+ }
4838
+ // ─── Drag handling ───────────────────────────────────
4839
+ _onPointerDown(e) {
4840
+ this._dragging = true;
4841
+ this._inertiaActive = false;
4842
+ this._dragStart.x = e.globalX;
4843
+ this._dragStart.y = e.globalY;
4844
+ this._contentStart.x = this._content.x;
4845
+ this._contentStart.y = this._content.y;
4846
+ this._lastDragPos.x = e.globalX;
4847
+ this._lastDragPos.y = e.globalY;
4848
+ this._lastDragTime = Date.now();
4849
+ this._velocity.x = 0;
4850
+ this._velocity.y = 0;
4851
+ this.stopInertia();
4852
+ }
4853
+ _onPointerMove(e) {
4854
+ if (!this._dragging)
4855
+ return;
4856
+ const dx = e.globalX - this._dragStart.x;
4857
+ const dy = e.globalY - this._dragStart.y;
4858
+ const { direction } = this._scrollConfig;
4859
+ if (direction !== 'horizontal') {
4860
+ this._content.y = this._contentStart.y + dy;
4861
+ }
4862
+ if (direction !== 'vertical') {
4863
+ this._content.x = this._contentStart.x + dx;
4864
+ }
4865
+ // Track velocity
4866
+ const now = Date.now();
4867
+ const dt = now - this._lastDragTime;
4868
+ if (dt > 0) {
4869
+ this._velocity.x = (e.globalX - this._lastDragPos.x) / dt * 16;
4870
+ this._velocity.y = (e.globalY - this._lastDragPos.y) / dt * 16;
4871
+ }
4872
+ this._lastDragPos.x = e.globalX;
4873
+ this._lastDragPos.y = e.globalY;
4874
+ this._lastDragTime = now;
4875
+ this.clampScroll();
4876
+ }
4877
+ _onPointerUp() {
4878
+ if (!this._dragging)
4879
+ return;
4880
+ this._dragging = false;
4881
+ if (!this._scrollConfig.disableEasing &&
4882
+ (Math.abs(this._velocity.x) > MIN_VELOCITY || Math.abs(this._velocity.y) > MIN_VELOCITY)) {
4883
+ this.startInertia();
4884
+ }
4885
+ }
4886
+ // ─── Inertia ─────────────────────────────────────────
4887
+ startInertia() {
4888
+ this._inertiaActive = true;
4889
+ this._onTickBound = this._inertiaTick.bind(this);
4890
+ Ticker.shared.add(this._onTickBound);
4891
+ }
4892
+ stopInertia() {
4893
+ if (this._onTickBound && this._inertiaActive) {
4894
+ Ticker.shared.remove(this._onTickBound);
4895
+ this._inertiaActive = false;
4896
+ }
4897
+ }
4898
+ _inertiaTick() {
4899
+ const { direction } = this._scrollConfig;
4900
+ if (direction !== 'horizontal') {
4901
+ this._content.y += this._velocity.y;
4902
+ this._velocity.y *= DECELERATION;
4903
+ }
4904
+ if (direction !== 'vertical') {
4905
+ this._content.x += this._velocity.x;
4906
+ this._velocity.x *= DECELERATION;
4907
+ }
4908
+ this.clampScroll();
4909
+ if (Math.abs(this._velocity.x) < MIN_VELOCITY && Math.abs(this._velocity.y) < MIN_VELOCITY) {
4910
+ this.stopInertia();
4911
+ }
4912
+ }
4913
+ // ─── Mouse wheel ─────────────────────────────────────
4914
+ _onWheel(e) {
4915
+ const { direction } = this._scrollConfig;
4916
+ e.preventDefault();
4917
+ if (direction !== 'horizontal') {
4918
+ this._content.y -= e.deltaY;
4919
+ }
4920
+ if (direction !== 'vertical') {
4921
+ this._content.x -= e.deltaX;
4922
+ }
4923
+ this.clampScroll();
4924
+ }
4925
+ // ─── Scroll bounds ───────────────────────────────────
4926
+ clampScroll() {
4927
+ const { direction } = this._scrollConfig;
4928
+ const bounds = this._content.getLocalBounds();
4929
+ if (direction !== 'horizontal') {
4930
+ const contentHeight = bounds.height + bounds.y;
4931
+ const maxScroll = Math.min(0, this._viewport.height - contentHeight);
4932
+ this._content.y = Math.max(maxScroll, Math.min(0, this._content.y));
4933
+ }
4934
+ if (direction !== 'vertical') {
4935
+ const contentWidth = bounds.width + bounds.x;
4936
+ const maxScroll = Math.min(0, this._viewport.width - contentWidth);
4937
+ this._content.x = Math.max(maxScroll, Math.min(0, this._content.x));
4938
+ }
4939
+ this.updateScrollbar();
4940
+ }
4941
+ updateScrollbar() {
4942
+ if (!this._scrollbar)
4943
+ return;
4944
+ const { direction } = this._scrollConfig;
4945
+ const { width: sbW, padding: sbPad } = this._scrollbarConfig;
4946
+ const bounds = this._content.getLocalBounds();
4947
+ const isVert = direction !== 'horizontal';
4948
+ if (isVert) {
4949
+ const contentH = bounds.height + bounds.y;
4950
+ if (contentH <= this._viewport.height) {
4951
+ this._scrollbar.visible = false;
4952
+ return;
4953
+ }
4954
+ this._scrollbar.visible = true;
4955
+ const ratio = this._viewport.height / contentH;
4956
+ const thumbH = Math.max(20, this._viewport.height * ratio);
4957
+ const scrollRange = this._viewport.height - thumbH;
4958
+ const scrollProgress = -this._content.y / (contentH - this._viewport.height);
4959
+ this._scrollbar.x = this._viewport.width - sbW - sbPad;
4960
+ this._scrollbar.y = scrollProgress * scrollRange;
4961
+ this._scrollbar.height = thumbH;
4962
+ this._scrollbar.width = sbW;
4963
+ }
4964
+ else {
4965
+ const contentW = bounds.width + bounds.x;
4966
+ if (contentW <= this._viewport.width) {
4967
+ this._scrollbar.visible = false;
4968
+ return;
4969
+ }
4970
+ this._scrollbar.visible = true;
4971
+ const ratio = this._viewport.width / contentW;
4972
+ const thumbW = Math.max(20, this._viewport.width * ratio);
4973
+ const scrollRange = this._viewport.width - thumbW;
4974
+ const scrollProgress = -this._content.x / (contentW - this._viewport.width);
4975
+ this._scrollbar.y = this._viewport.height - sbW - sbPad;
4976
+ this._scrollbar.x = scrollProgress * scrollRange;
4977
+ this._scrollbar.width = thumbW;
4978
+ this._scrollbar.height = sbW;
4979
+ }
4980
+ }
4981
+ destroy(options) {
4982
+ this.stopInertia();
4983
+ this.off('pointerdown', this._onPointerDown, this);
4984
+ this.off('pointermove', this._onPointerMove, this);
4985
+ this.off('pointerup', this._onPointerUp, this);
4986
+ this.off('pointerupoutside', this._onPointerUp, this);
4987
+ this._items.length = 0;
4988
+ super.destroy(options);
3964
4989
  }
3965
4990
  }
3966
4991
 
@@ -4195,5 +5220,5 @@ class DevBridge {
4195
5220
  }
4196
5221
  }
4197
5222
 
4198
- export { AssetManager, AudioManager, BalanceDisplay, Button, DevBridge, Easing, EventEmitter, FPSOverlay, GameApplication, InputManager, Label, Layout, LoadingScene, Modal, Orientation, Panel, ProgressBar, ScaleMode, Scene, SceneManager, ScrollContainer, SpineHelper, SpriteAnimation, StateMachine, Timeline, Toast, TransitionType, Tween, ViewportManager, WinDisplay };
5223
+ export { AssetManager, AudioManager, BalanceDisplay, Button, DevBridge, Easing, EventEmitter, FPSOverlay, FlexContainer, GameApplication, InputManager, Label, Layout, LoadingScene, Modal, Orientation, Panel, ProgressBar, ScaleMode, Scene, SceneManager, ScrollContainer, SpineHelper, SpriteAnimation, StateMachine, Timeline, Toast, TransitionType, Tween, ViewportManager, WinDisplay };
4199
5224
  //# sourceMappingURL=index.esm.js.map