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