@energy8platform/shell 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pixi.cjs.js CHANGED
@@ -66,6 +66,7 @@ function createInitialState(config) {
66
66
  turbo: 0,
67
67
  buyBonusEnabled: true,
68
68
  freeSpins: { current: 0, total: 0, totalWin: 0 },
69
+ bonus: null,
69
70
  activeFeature: null,
70
71
  };
71
72
  }
@@ -1439,7 +1440,7 @@ class KeyboardController {
1439
1440
 
1440
1441
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1441
1442
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
1442
- const PACKAGE_VERSION = '0.4.1';
1443
+ const PACKAGE_VERSION = '0.5.0';
1443
1444
 
1444
1445
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
1445
1446
  function resolveConfig(config) {
@@ -1500,9 +1501,15 @@ class ShellController extends EventEmitter {
1500
1501
  this.renderer.renderBar();
1501
1502
  }
1502
1503
  // ── ShellHost ──────────────────────────────────────────────────────────────
1503
- t(text) { return this.i18n.t(text); }
1504
- formatCurrency(n, win = false) { return formatCurrency(n, this.config.currency, win); }
1505
- formatWin(value) { return this.formatCurrency(value, true); }
1504
+ t(text) {
1505
+ return this.i18n.t(text);
1506
+ }
1507
+ formatCurrency(n, win = false) {
1508
+ return formatCurrency(n, this.config.currency, win);
1509
+ }
1510
+ formatWin(value) {
1511
+ return this.formatCurrency(value, true);
1512
+ }
1506
1513
  notifyResize(w, h) {
1507
1514
  const layout = w !== 0 && h > w ? 'mobile' : 'wide';
1508
1515
  if (layout !== this.layout) {
@@ -1569,12 +1576,24 @@ class ShellController extends EventEmitter {
1569
1576
  // eslint-disable-next-line @typescript-eslint/no-this-alias
1570
1577
  const self = this;
1571
1578
  const host = {
1572
- get state() { return self.state; },
1573
- get hotkeysEnabled() { return self.config.features.hotkeys !== false; },
1574
- get spacebarEnabled() { return self.config.features.spacebar !== false; },
1575
- get turboLevels() { return self.config.features.turbo; },
1576
- get autoplayEnabled() { return self.config.features.autoplay != null; },
1577
- get buyBonusEnabled() { return self.config.features.buyBonus !== false; },
1579
+ get state() {
1580
+ return self.state;
1581
+ },
1582
+ get hotkeysEnabled() {
1583
+ return self.config.features.hotkeys !== false;
1584
+ },
1585
+ get spacebarEnabled() {
1586
+ return self.config.features.spacebar !== false;
1587
+ },
1588
+ get turboLevels() {
1589
+ return self.config.features.turbo;
1590
+ },
1591
+ get autoplayEnabled() {
1592
+ return self.config.features.autoplay != null;
1593
+ },
1594
+ get buyBonusEnabled() {
1595
+ return self.config.features.buyBonus !== false;
1596
+ },
1578
1597
  hasOpenLayer: () => self.overlay !== null,
1579
1598
  routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
1580
1599
  spin: () => self.actions.spin(),
@@ -1590,27 +1609,52 @@ class ShellController extends EventEmitter {
1590
1609
  this.kbd = new KeyboardController(host);
1591
1610
  this.kbd.attach();
1592
1611
  }
1593
- pullFocus = () => { try {
1594
- globalThis.focus?.();
1595
- }
1596
- catch { /* cross-origin */ } };
1612
+ pullFocus = () => {
1613
+ try {
1614
+ globalThis.focus?.();
1615
+ }
1616
+ catch {
1617
+ /* cross-origin */
1618
+ }
1619
+ };
1597
1620
  // ── overlay flow ─────────────────────────────────────────────────────────────
1598
1621
  show(req) {
1599
1622
  this.closeModal();
1600
1623
  this.overlay = this.renderer.openOverlay(req) ?? null;
1601
1624
  }
1602
- openMenu() { this.emit('menuOpen'); this.openSettings(); }
1603
- openSettings() { this.emit('settingsOpen'); this.show({ kind: 'settings' }); }
1604
- openInfo() { this.emit('infoOpen'); this.show({ kind: 'gameInfo' }); }
1605
- openBuyBonus() { if (this.config.onBonusBuy) {
1606
- this.config.onBonusBuy();
1607
- return;
1608
- } this.show({ kind: 'buyBonus' }); }
1609
- openBetPicker() { this.show({ kind: 'betPicker' }); }
1610
- openAutoplayPicker() { this.show({ kind: 'autoplayPicker' }); }
1611
- openReplay(opts) { if (this.destroyed)
1612
- return; this.show({ kind: 'replay', opts }); }
1613
- openModal(opts) { this.show({ kind: 'modal', opts }); }
1625
+ openMenu() {
1626
+ this.emit('menuOpen');
1627
+ this.openSettings();
1628
+ }
1629
+ openSettings() {
1630
+ this.emit('settingsOpen');
1631
+ this.show({ kind: 'settings' });
1632
+ }
1633
+ openInfo() {
1634
+ this.emit('infoOpen');
1635
+ this.show({ kind: 'gameInfo' });
1636
+ }
1637
+ openBuyBonus() {
1638
+ if (this.config.onBonusBuy) {
1639
+ this.config.onBonusBuy();
1640
+ return;
1641
+ }
1642
+ this.show({ kind: 'buyBonus' });
1643
+ }
1644
+ openBetPicker() {
1645
+ this.show({ kind: 'betPicker' });
1646
+ }
1647
+ openAutoplayPicker() {
1648
+ this.show({ kind: 'autoplayPicker' });
1649
+ }
1650
+ openReplay(opts) {
1651
+ if (this.destroyed)
1652
+ return;
1653
+ this.show({ kind: 'replay', opts });
1654
+ }
1655
+ openModal(opts) {
1656
+ this.show({ kind: 'modal', opts });
1657
+ }
1614
1658
  /** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
1615
1659
  closeModal() {
1616
1660
  if (!this.overlay)
@@ -1626,7 +1670,9 @@ class ShellController extends EventEmitter {
1626
1670
  this.soundRefresh?.(on);
1627
1671
  this.renderer.refreshSoundIcon?.(on);
1628
1672
  }
1629
- setSoundRefresh(fn) { this.soundRefresh = fn; }
1673
+ setSoundRefresh(fn) {
1674
+ this.soundRefresh = fn;
1675
+ }
1630
1676
  // ── features ─────────────────────────────────────────────────────────────────
1631
1677
  activateFeature(bonus) {
1632
1678
  this.state.activeFeature = bonus;
@@ -1647,21 +1693,81 @@ class ShellController extends EventEmitter {
1647
1693
  if (to !== from)
1648
1694
  this.renderer.animateMoney(field, from, to);
1649
1695
  }
1650
- setBalance(n) { const from = this.prevBalance; this.state.balance = n; this.prevBalance = n; this.money('balance', from, n); }
1651
- setWin(n) { const from = this.prevWin; this.state.win = n; this.prevWin = n; this.money('win', from, n); }
1652
- setBet(n) { this.state.bet = n; this.renderer.renderBar(); }
1653
- setMode(mode) { if (mode === 'replay')
1654
- this.state.replay = true; this.state.mode = mode; this.renderer.renderBar(); }
1655
- setBusy(busy) { this.state.busy = busy; this.renderer.renderBar(); this.kbd?.notifyBusyChanged(busy); }
1656
- setAutoplay(a) { this.state.autoplay = a; this.renderer.renderBar(); }
1657
- setTurbo(level) { this.state.turbo = level; this.renderer.renderBar(); }
1658
- setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.renderer.renderBar(); }
1659
- setFreeSpins(fs) { this.state.freeSpins = fs; this.renderer.renderBar(); }
1660
- setTheme(theme) { this.config.theme = theme; this.tokens = resolveTheme(theme); this.renderer.applyTheme(this.tokens); this.renderer.renderBar(); }
1661
- setLanguage(lang) { this.config.language = lang; this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial }); this.renderer.renderBar(); }
1662
- setSocial(isSocial) { this.config.isSocial = isSocial; this.i18n = createI18n({ language: this.config.language, isSocial }); this.renderer.renderBar(); }
1663
- setLayout(layout) { if (layout === this.layout)
1664
- return; this.layout = layout; this.renderer.setLayout(layout); this.renderer.renderBar(); }
1696
+ setBalance(n) {
1697
+ const from = this.prevBalance;
1698
+ this.state.balance = n;
1699
+ this.prevBalance = n;
1700
+ this.money('balance', from, n);
1701
+ }
1702
+ setWin(n) {
1703
+ const from = this.prevWin;
1704
+ this.state.win = n;
1705
+ this.prevWin = n;
1706
+ this.money('win', from, n);
1707
+ }
1708
+ setBet(n) {
1709
+ this.state.bet = n;
1710
+ this.renderer.renderBar();
1711
+ }
1712
+ setMode(mode) {
1713
+ if (mode === 'replay')
1714
+ this.state.replay = true;
1715
+ this.state.mode = mode;
1716
+ this.renderer.renderBar();
1717
+ }
1718
+ setBusy(busy) {
1719
+ this.state.busy = busy;
1720
+ this.renderer.renderBar();
1721
+ this.kbd?.notifyBusyChanged(busy);
1722
+ }
1723
+ setAutoplay(a) {
1724
+ this.state.autoplay = a;
1725
+ this.renderer.renderBar();
1726
+ }
1727
+ setTurbo(level) {
1728
+ this.state.turbo = level;
1729
+ this.renderer.renderBar();
1730
+ }
1731
+ setBuyBonusEnabled(enabled) {
1732
+ this.state.buyBonusEnabled = enabled;
1733
+ this.renderer.renderBar();
1734
+ }
1735
+ setFreeSpins(fs) {
1736
+ this.state.freeSpins = fs;
1737
+ this.state.bonus = null;
1738
+ this.renderer.renderBar();
1739
+ }
1740
+ /** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
1741
+ * override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
1742
+ * `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
1743
+ setBonus(b) {
1744
+ this.state.bonus = { label: b.label, value: b.value };
1745
+ this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
1746
+ this.renderer.renderBar();
1747
+ }
1748
+ setTheme(theme) {
1749
+ this.config.theme = theme;
1750
+ this.tokens = resolveTheme(theme);
1751
+ this.renderer.applyTheme(this.tokens);
1752
+ this.renderer.renderBar();
1753
+ }
1754
+ setLanguage(lang) {
1755
+ this.config.language = lang;
1756
+ this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
1757
+ this.renderer.renderBar();
1758
+ }
1759
+ setSocial(isSocial) {
1760
+ this.config.isSocial = isSocial;
1761
+ this.i18n = createI18n({ language: this.config.language, isSocial });
1762
+ this.renderer.renderBar();
1763
+ }
1764
+ setLayout(layout) {
1765
+ if (layout === this.layout)
1766
+ return;
1767
+ this.layout = layout;
1768
+ this.renderer.setLayout(layout);
1769
+ this.renderer.renderBar();
1770
+ }
1665
1771
  destroy() {
1666
1772
  if (this.destroyed)
1667
1773
  return Promise.resolve();
@@ -2924,8 +3030,12 @@ class TurboButton extends pixi_js.Container {
2924
3030
  this.accent = accent;
2925
3031
  this._paint(false);
2926
3032
  }
2927
- measureSize() { return { w: this.box, h: this.box }; }
2928
- setLayoutSize() { }
3033
+ measureSize() {
3034
+ return { w: this.box, h: this.box };
3035
+ }
3036
+ setLayoutSize() {
3037
+ /* fixed */
3038
+ }
2929
3039
  }
2930
3040
  /** A readout in the bar (white Oswald value, plaque-label caption, no shadow). */
2931
3041
  function readout(host, label, value, opts = {}) {
@@ -2973,13 +3083,19 @@ class BottomBar extends pixi_js.Container {
2973
3083
  buildWide() {
2974
3084
  const { state, tokens } = this.host;
2975
3085
  const isBase = state.mode === 'base';
2976
- const isFS = state.mode === 'freeSpins';
3086
+ const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
2977
3087
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
2978
3088
  // BUY BONUS — floats outside-left of the panel (base only)
2979
- this.buy = isBase ? this.buildBuy(BUY_W, 13, 3) ?? undefined : undefined;
3089
+ this.buy = isBase ? (this.buildBuy(BUY_W, 13, 3) ?? undefined) : undefined;
2980
3090
  // LEFT info group: menu · balance · (Total win) · (Win)
2981
3091
  const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
2982
- left.add(new IconButton('menu', { size: 36, glyph: 30, color: '#ffffff', hover: tokens.accent, onTap: () => this.host.actions.openMenu() }));
3092
+ left.add(new IconButton('menu', {
3093
+ size: 36,
3094
+ glyph: 30,
3095
+ color: '#ffffff',
3096
+ hover: tokens.accent,
3097
+ onTap: () => this.host.actions.openMenu(),
3098
+ }));
2983
3099
  if (!state.replay) {
2984
3100
  const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
2985
3101
  this.balanceValue = bal.valueText;
@@ -3018,16 +3134,30 @@ class BottomBar extends pixi_js.Container {
3018
3134
  if (isBase) {
3019
3135
  bet.eventMode = 'static';
3020
3136
  bet.cursor = 'pointer';
3021
- bet.on('pointertap', () => { if (!this.betLocked())
3022
- this.host.actions.openBetPicker(); });
3137
+ bet.on('pointertap', () => {
3138
+ if (!this.betLocked())
3139
+ this.host.actions.openBetPicker();
3140
+ });
3023
3141
  }
3024
3142
  const betGroup = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
3025
3143
  betGroup.add(bet);
3026
3144
  if (isBase) {
3027
3145
  // plain (borderless) +/- icons, bolder — stacked
3028
3146
  const step = new FlexBox({ direction: 'column', align: 'center', gap: 2 });
3029
- this.betUp = new IconButton('plus', { size: 24, glyph: 22, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(1) });
3030
- this.betDown = new IconButton('minus', { size: 24, glyph: 22, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(-1) });
3147
+ this.betUp = new IconButton('plus', {
3148
+ size: 24,
3149
+ glyph: 22,
3150
+ color: '#ffffff',
3151
+ hover: tokens.accent,
3152
+ onTap: () => this.onBet(1),
3153
+ });
3154
+ this.betDown = new IconButton('minus', {
3155
+ size: 24,
3156
+ glyph: 22,
3157
+ color: '#ffffff',
3158
+ hover: tokens.accent,
3159
+ onTap: () => this.onBet(-1),
3160
+ });
3031
3161
  step.add(this.betUp);
3032
3162
  step.add(this.betDown);
3033
3163
  betGroup.add(step);
@@ -3035,15 +3165,28 @@ class BottomBar extends pixi_js.Container {
3035
3165
  const spinWrap = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
3036
3166
  if (isBase && config.features.autoplay) {
3037
3167
  this.autoBtn = new IconButton('autoplay', {
3038
- size: DISC, glyph: 25, disc: tokens.btn, color: tokens.btnInk, hover: tokens.accent, activeColor: tokens.accent,
3039
- active: state.autoplay.active, onTap: () => this.onAutoplay(),
3168
+ size: DISC,
3169
+ glyph: 25,
3170
+ disc: tokens.btn,
3171
+ color: tokens.btnInk,
3172
+ hover: tokens.accent,
3173
+ activeColor: tokens.accent,
3174
+ active: state.autoplay.active,
3175
+ onTap: () => this.onAutoplay(),
3040
3176
  });
3041
3177
  if (state.autoplay.active)
3042
3178
  this.autoBtn.setGlow(true);
3043
3179
  spinWrap.add(this.autoBtn);
3044
3180
  }
3045
3181
  if (isBase) {
3046
- this.spin = new SpinDisc({ size: SPIN, glyph: 65, tokens, ticker: this.host.ticker, onSpin: () => this.host.actions.spin(), onStop: () => this.stopAutoplay() });
3182
+ this.spin = new SpinDisc({
3183
+ size: SPIN,
3184
+ glyph: 65,
3185
+ tokens,
3186
+ ticker: this.host.ticker,
3187
+ onSpin: () => this.host.actions.spin(),
3188
+ onStop: () => this.stopAutoplay(),
3189
+ });
3047
3190
  if (state.autoplay.active)
3048
3191
  this.spin.setAutoplay(true, state.autoplay.remaining);
3049
3192
  if (state.busy)
@@ -3052,13 +3195,23 @@ class BottomBar extends pixi_js.Container {
3052
3195
  }
3053
3196
  else if (showFsBlocks) {
3054
3197
  const fs = state.freeSpins;
3055
- const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
3056
- spinWrap.add(new FsHero({ label: this.host.t('Free spins'), value: fsText, tokens, height: SPIN }));
3198
+ const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
3199
+ spinWrap.add(new FsHero({
3200
+ label: this.host.t(state.bonus?.label ?? 'Free spins'),
3201
+ value: fsText,
3202
+ tokens,
3203
+ height: SPIN,
3204
+ }));
3057
3205
  }
3058
3206
  if (config.features.turbo > 0) {
3059
3207
  this.turboBtn = new TurboButton({
3060
- size: DISC, glyph: 19, discFill: tokens.btn, discBorder: 2, accent: tokens.accent,
3061
- level: state.turbo, onTap: () => this.onTurbo(),
3208
+ size: DISC,
3209
+ glyph: 19,
3210
+ discFill: tokens.btn,
3211
+ discBorder: 2,
3212
+ accent: tokens.accent,
3213
+ level: state.turbo,
3214
+ onTap: () => this.onTurbo(),
3062
3215
  });
3063
3216
  spinWrap.add(this.turboBtn);
3064
3217
  }
@@ -3076,21 +3229,49 @@ class BottomBar extends pixi_js.Container {
3076
3229
  const feature = state.activeFeature;
3077
3230
  if (feature) {
3078
3231
  const accent = effectiveAccent(feature);
3079
- return new BuyBonusBadge({ size, fontSize, border, bg: accent, fg: contrastText(accent), label: this.host.t('DISABLE'), tokens, ticker: this.host.ticker, onTap: () => this.host.actions.deactivateFeature() });
3232
+ return new BuyBonusBadge({
3233
+ size,
3234
+ fontSize,
3235
+ border,
3236
+ bg: accent,
3237
+ fg: contrastText(accent),
3238
+ label: this.host.t('DISABLE'),
3239
+ tokens,
3240
+ ticker: this.host.ticker,
3241
+ onTap: () => this.host.actions.deactivateFeature(),
3242
+ });
3080
3243
  }
3081
- return new BuyBonusBadge({ size, border, bg: tokens.accent, icon: 'ticket', iconSize: size * 0.55, iconColor: tokens.btnInk, label: '', tokens, ticker: this.host.ticker, onTap: () => this.host.actions.openBuyBonus() });
3244
+ return new BuyBonusBadge({
3245
+ size,
3246
+ border,
3247
+ bg: tokens.accent,
3248
+ icon: 'ticket',
3249
+ iconSize: size * 0.55,
3250
+ iconColor: tokens.btnInk,
3251
+ label: '',
3252
+ tokens,
3253
+ ticker: this.host.ticker,
3254
+ onTap: () => this.host.actions.openBuyBonus(),
3255
+ });
3082
3256
  }
3083
3257
  // ── mobile / portrait ─────────────────────────────────────────────────────
3084
3258
  buildMobile() {
3085
3259
  const { state, config, tokens } = this.host;
3086
3260
  const isBase = state.mode === 'base';
3087
- const isFS = state.mode === 'freeSpins';
3261
+ const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
3088
3262
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
3089
3263
  const W = this.host.screenW - 2 * M_SIDE;
3090
3264
  // hero (centre): SPIN in base, FS counter in free spins
3091
3265
  let hero;
3092
3266
  if (isBase) {
3093
- this.spin = new SpinDisc({ size: SPIN, glyph: 65, tokens, ticker: this.host.ticker, onSpin: () => this.host.actions.spin(), onStop: () => this.stopAutoplay() });
3267
+ this.spin = new SpinDisc({
3268
+ size: SPIN,
3269
+ glyph: 65,
3270
+ tokens,
3271
+ ticker: this.host.ticker,
3272
+ onSpin: () => this.host.actions.spin(),
3273
+ onStop: () => this.stopAutoplay(),
3274
+ });
3094
3275
  if (state.autoplay.active)
3095
3276
  this.spin.setAutoplay(true, state.autoplay.remaining);
3096
3277
  if (state.busy)
@@ -3099,25 +3280,58 @@ class BottomBar extends pixi_js.Container {
3099
3280
  }
3100
3281
  else if (showFsBlocks) {
3101
3282
  const fs = state.freeSpins;
3102
- const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
3103
- hero = new FsHero({ label: this.host.t('Free spins'), value: fsText, tokens, height: SPIN });
3283
+ const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
3284
+ hero = new FsHero({
3285
+ label: this.host.t(state.bonus?.label ?? 'Free spins'),
3286
+ value: fsText,
3287
+ tokens,
3288
+ height: SPIN,
3289
+ });
3104
3290
  }
3105
- const menu = new IconButton('menu', { size: 40, glyph: 26, color: '#ffffff', hover: tokens.accent, onTap: () => this.host.actions.openMenu() });
3291
+ const menu = new IconButton('menu', {
3292
+ size: 40,
3293
+ glyph: 26,
3294
+ color: '#ffffff',
3295
+ hover: tokens.accent,
3296
+ onTap: () => this.host.actions.openMenu(),
3297
+ });
3106
3298
  // autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
3107
3299
  if (isBase && config.features.autoplay) {
3108
- this.autoBtn = new IconButton('autoplay', { size: 40, glyph: 26, color: '#ffffff', hover: tokens.accent, activeColor: tokens.accent, active: state.autoplay.active, onTap: () => this.onAutoplay() });
3300
+ this.autoBtn = new IconButton('autoplay', {
3301
+ size: 40,
3302
+ glyph: 26,
3303
+ color: '#ffffff',
3304
+ hover: tokens.accent,
3305
+ activeColor: tokens.accent,
3306
+ active: state.autoplay.active,
3307
+ onTap: () => this.onAutoplay(),
3308
+ });
3109
3309
  if (state.autoplay.active)
3110
3310
  this.autoBtn.setGlow(true);
3111
3311
  }
3112
3312
  if (config.features.turbo > 0) {
3113
3313
  this.turboBtn = new TurboButton({
3114
- size: 40, glyph: 22, discFill: tokens.btn, discBorder: 2, accent: tokens.accent,
3115
- level: state.turbo, onTap: () => this.onTurbo(),
3314
+ size: 40,
3315
+ glyph: 22,
3316
+ discFill: tokens.btn,
3317
+ discBorder: 2,
3318
+ accent: tokens.accent,
3319
+ level: state.turbo,
3320
+ onTap: () => this.onTurbo(),
3116
3321
  });
3117
3322
  }
3118
- const buy = isBase ? this.buildBuy(M_BUY, 9, 2) ?? undefined : undefined;
3323
+ const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
3119
3324
  // level 1 — controls bar (dark)
3120
- const controls = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', gap: 0, width: W, height: M_CTRL_H, padding: { left: 18, right: 18 }, background: { fill: tokens.bar, radius: 16 } });
3325
+ const controls = new FlexBox({
3326
+ direction: 'row',
3327
+ align: 'center',
3328
+ justify: 'space-between',
3329
+ gap: 0,
3330
+ width: W,
3331
+ height: M_CTRL_H,
3332
+ padding: { left: 18, right: 18 },
3333
+ background: { fill: tokens.bar, radius: 16 },
3334
+ });
3121
3335
  if (isBase && hero) {
3122
3336
  const SIDE = 12;
3123
3337
  const lz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'start' });
@@ -3153,7 +3367,12 @@ class BottomBar extends pixi_js.Container {
3153
3367
  const heroW = hero ? (heroSized.measureSize?.().w ?? hero.getLocalBounds().width) : 0;
3154
3368
  const fixed = ICON /* menu */ + (this.autoBtn ? ICON : 0) + heroW + (this.turboBtn ? ICON : 0);
3155
3369
  const twSlot = Math.max(60, W - 2 * PAD - fixed - GAPS);
3156
- controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), { color: '#ffffff', muted: '#ffffff', align: 'center', maxWidth: twSlot }));
3370
+ controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
3371
+ color: '#ffffff',
3372
+ muted: '#ffffff',
3373
+ align: 'center',
3374
+ maxWidth: twSlot,
3375
+ }));
3157
3376
  }
3158
3377
  if (this.turboBtn)
3159
3378
  controls.add(this.turboBtn);
@@ -3164,36 +3383,76 @@ class BottomBar extends pixi_js.Container {
3164
3383
  const feature = state.activeFeature;
3165
3384
  const betShown = feature ? state.bet * feature.priceMultiplier : state.bet;
3166
3385
  if (isBase) {
3167
- this.betDown = new IconButton('minus', { size: 26, glyph: 18, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(-1) });
3386
+ this.betDown = new IconButton('minus', {
3387
+ size: 26,
3388
+ glyph: 18,
3389
+ color: '#ffffff',
3390
+ hover: tokens.accent,
3391
+ onTap: () => this.onBet(-1),
3392
+ });
3168
3393
  betGroup.add(this.betDown);
3169
3394
  }
3170
- const bet = new Readout({ label: this.host.t('Bet'), value: this.host.fmt(betShown), muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel, fg: feature ? effectiveAccent(feature) : '#ffffff', valueSize: 11, align: 'center', fixedWidth: 76, shadow: false });
3395
+ const bet = new Readout({
3396
+ label: this.host.t('Bet'),
3397
+ value: this.host.fmt(betShown),
3398
+ muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel,
3399
+ fg: feature ? effectiveAccent(feature) : '#ffffff',
3400
+ valueSize: 11,
3401
+ align: 'center',
3402
+ fixedWidth: 76,
3403
+ shadow: false,
3404
+ });
3171
3405
  this.betReadout = bet;
3172
3406
  if (isBase) {
3173
3407
  bet.eventMode = 'static';
3174
3408
  bet.cursor = 'pointer';
3175
- bet.on('pointertap', () => { if (!this.betLocked())
3176
- this.host.actions.openBetPicker(); });
3409
+ bet.on('pointertap', () => {
3410
+ if (!this.betLocked())
3411
+ this.host.actions.openBetPicker();
3412
+ });
3177
3413
  }
3178
3414
  betGroup.add(bet);
3179
3415
  if (isBase) {
3180
- this.betUp = new IconButton('plus', { size: 26, glyph: 18, color: '#ffffff', hover: tokens.accent, onTap: () => this.onBet(1) });
3416
+ this.betUp = new IconButton('plus', {
3417
+ size: 26,
3418
+ glyph: 18,
3419
+ color: '#ffffff',
3420
+ hover: tokens.accent,
3421
+ onTap: () => this.onBet(1),
3422
+ });
3181
3423
  betGroup.add(this.betUp);
3182
3424
  }
3183
3425
  betGroup.layout();
3184
3426
  const innerW = W - 28; // pad 14 each side
3185
3427
  const slot = Math.max(40, (innerW - betGroup.outerWidth - 20) / 2);
3186
- const info = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', width: W, height: M_INFO_H, padding: { left: 14, right: 14 }, gap: 10, background: { fill: tokens.plaqueGlass, radius: 12 } });
3428
+ const info = new FlexBox({
3429
+ direction: 'row',
3430
+ align: 'center',
3431
+ justify: 'space-between',
3432
+ width: W,
3433
+ height: M_INFO_H,
3434
+ padding: { left: 14, right: 14 },
3435
+ gap: 10,
3436
+ background: { fill: tokens.plaqueGlass, radius: 12 },
3437
+ });
3187
3438
  // replay is a read-only historical round — no real balance to show (sticky `replay`, so it stays
3188
3439
  // hidden through a replay's free-spins phase too); matches the DOM bar.
3189
3440
  if (!state.replay) {
3190
- const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), { valueSize: 11, align: 'left', fixedWidth: slot });
3441
+ const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
3442
+ valueSize: 11,
3443
+ align: 'left',
3444
+ fixedWidth: slot,
3445
+ });
3191
3446
  this.balanceValue = bal.valueText;
3192
3447
  info.add(bal);
3193
3448
  }
3194
3449
  info.add(betGroup);
3195
3450
  // WIN always present (no jiggle on win↔0)
3196
- const win = readout(this.host, 'Win', this.host.fmtWin(state.win), { valueSize: 11, align: 'right', fixedWidth: slot });
3451
+ const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
3452
+ valueSize: 11,
3453
+ align: 'right',
3454
+ fixedWidth: slot,
3455
+ });
3197
3456
  this.winValue = win.valueText;
3198
3457
  info.add(win);
3199
3458
  info.layout();
@@ -3284,7 +3543,7 @@ class BottomBar extends pixi_js.Container {
3284
3543
  left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
3285
3544
  right.position.set(panelRight - PANEL_PAD - right.outerWidth, panelCenterY - right.outerHeight / 2);
3286
3545
  this.inner.scale.set(s);
3287
- this.inner.position.set((W - barW * s) / 2, (H - WIDE_PAD_BOTTOM) - SPIN * s);
3546
+ this.inner.position.set((W - barW * s) / 2, H - WIDE_PAD_BOTTOM - SPIN * s);
3288
3547
  this.measureFootprint();
3289
3548
  }
3290
3549
  applyFitMobile() {