@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/html.d.ts CHANGED
@@ -16,7 +16,11 @@ declare class EventEmitter<TEvents extends {}> {
16
16
  removeAllListeners(event?: keyof TEvents): this;
17
17
  }
18
18
 
19
- type ShellMode = 'base' | 'freeSpins' | 'replay';
19
+ /** `freeSpins` and `bonus` are the SAME bar layout (host-driven hero + Total Win); `freeSpins` is
20
+ * kept as a back-compat alias for the common case (its readout is derived current/total), while
21
+ * `bonus` pairs with `setBonus()` to show a game-supplied label + value (adventure, hold-and-spin,
22
+ * respins — anything that isn't a plain free-spins counter). */
23
+ type ShellMode = 'base' | 'bonus' | 'freeSpins' | 'replay';
20
24
  interface CurrencyConfig {
21
25
  symbol: string;
22
26
  position: 'left' | 'right';
@@ -205,6 +209,19 @@ interface FreeSpinsState {
205
209
  total: number;
206
210
  totalWin: number;
207
211
  }
212
+ /** A game-supplied bonus readout for the bar hero, set via `setBonus()`. Generalizes the
213
+ * free-spins counter: the shell renders `label` (localized via the shell translator, so a game
214
+ * i18n entry for it is honoured) + `value` (a pre-formatted string, shown verbatim — "2 / 10",
215
+ * "3", "×5", "7 coins") + the Total Win accumulator. The shell stays free of any per-game bonus
216
+ * concept — the label/value semantics live in the game/host. */
217
+ interface BonusReadout {
218
+ /** Bar label, e.g. 'Free spins' | 'Adventure' | 'Hold & Spin'. Run through the shell translator. */
219
+ label: string;
220
+ /** Pre-formatted counter value shown verbatim (NOT translated). */
221
+ value: string;
222
+ /** Cumulative bonus win for the Total Win readout. */
223
+ totalWin: number;
224
+ }
208
225
  /** One footer button of a generic modal. Clicking it runs `on` (if any), then closes the modal. */
209
226
  interface ModalAction {
210
227
  title: string;
@@ -283,6 +300,12 @@ interface ShellState {
283
300
  turbo: number;
284
301
  buyBonusEnabled: boolean;
285
302
  freeSpins: FreeSpinsState;
303
+ /** Game-supplied bonus label + value override (from `setBonus()`). When null the bar derives the
304
+ * readout from `freeSpins` (label 'Free spins', value current/total) — the back-compat default. */
305
+ bonus: {
306
+ label: string;
307
+ value: string;
308
+ } | null;
286
309
  /** The currently activated `feature` option (e.g. Ante), or null. Drives the
287
310
  * effective-bet readout tint and the BUY BONUS → DISABLE toggle on the bar. */
288
311
  activeFeature: BonusOption | null;
@@ -471,7 +494,7 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
471
494
  tokens: ShellTokens;
472
495
  layout: ShellLayoutMode;
473
496
  soundOn: boolean;
474
- readonly engineVersion = "0.4.1";
497
+ readonly engineVersion = "0.5.0";
475
498
  readonly actions: ShellActions;
476
499
  private renderer;
477
500
  private i18n;
@@ -514,6 +537,10 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
514
537
  setTurbo(level: number): void;
515
538
  setBuyBonusEnabled(enabled: boolean): void;
516
539
  setFreeSpins(fs: FreeSpinsState): void;
540
+ /** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
541
+ * override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
542
+ * `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
543
+ setBonus(b: BonusReadout): void;
517
544
  setTheme(theme: ThemeConfig): void;
518
545
  setLanguage(lang: string): void;
519
546
  setSocial(isSocial: boolean): void;
@@ -537,7 +564,7 @@ interface I18n {
537
564
  declare function createI18n(opts: I18nOptions): I18n;
538
565
 
539
566
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
540
- declare const PACKAGE_VERSION = "0.4.1";
567
+ declare const PACKAGE_VERSION = "0.5.0";
541
568
 
542
569
  /** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
543
570
  * an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
@@ -613,4 +640,4 @@ declare function createGameShell(config: HtmlShellConfig): ShellController;
613
640
  declare function removeGameShell(): Promise<void>;
614
641
 
615
642
  export { DEFAULT_ACCENT, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, SCHEMES, ShellController, createGameShell, createI18n, createShell, normalizeLang, removeGameShell, resolveConfig, resolveTheme, socialize };
616
- export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, WinSection };
643
+ export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, WinSection };
package/dist/html.esm.js CHANGED
@@ -63,6 +63,7 @@ function createInitialState(config) {
63
63
  turbo: 0,
64
64
  buyBonusEnabled: true,
65
65
  freeSpins: { current: 0, total: 0, totalWin: 0 },
66
+ bonus: null,
66
67
  activeFeature: null,
67
68
  };
68
69
  }
@@ -1436,7 +1437,7 @@ class KeyboardController {
1436
1437
 
1437
1438
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1438
1439
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
1439
- const PACKAGE_VERSION = '0.4.1';
1440
+ const PACKAGE_VERSION = '0.5.0';
1440
1441
 
1441
1442
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
1442
1443
  function resolveConfig(config) {
@@ -1497,9 +1498,15 @@ class ShellController extends EventEmitter {
1497
1498
  this.renderer.renderBar();
1498
1499
  }
1499
1500
  // ── ShellHost ──────────────────────────────────────────────────────────────
1500
- t(text) { return this.i18n.t(text); }
1501
- formatCurrency(n, win = false) { return formatCurrency(n, this.config.currency, win); }
1502
- formatWin(value) { return this.formatCurrency(value, true); }
1501
+ t(text) {
1502
+ return this.i18n.t(text);
1503
+ }
1504
+ formatCurrency(n, win = false) {
1505
+ return formatCurrency(n, this.config.currency, win);
1506
+ }
1507
+ formatWin(value) {
1508
+ return this.formatCurrency(value, true);
1509
+ }
1503
1510
  notifyResize(w, h) {
1504
1511
  const layout = w !== 0 && h > w ? 'mobile' : 'wide';
1505
1512
  if (layout !== this.layout) {
@@ -1566,12 +1573,24 @@ class ShellController extends EventEmitter {
1566
1573
  // eslint-disable-next-line @typescript-eslint/no-this-alias
1567
1574
  const self = this;
1568
1575
  const host = {
1569
- get state() { return self.state; },
1570
- get hotkeysEnabled() { return self.config.features.hotkeys !== false; },
1571
- get spacebarEnabled() { return self.config.features.spacebar !== false; },
1572
- get turboLevels() { return self.config.features.turbo; },
1573
- get autoplayEnabled() { return self.config.features.autoplay != null; },
1574
- get buyBonusEnabled() { return self.config.features.buyBonus !== false; },
1576
+ get state() {
1577
+ return self.state;
1578
+ },
1579
+ get hotkeysEnabled() {
1580
+ return self.config.features.hotkeys !== false;
1581
+ },
1582
+ get spacebarEnabled() {
1583
+ return self.config.features.spacebar !== false;
1584
+ },
1585
+ get turboLevels() {
1586
+ return self.config.features.turbo;
1587
+ },
1588
+ get autoplayEnabled() {
1589
+ return self.config.features.autoplay != null;
1590
+ },
1591
+ get buyBonusEnabled() {
1592
+ return self.config.features.buyBonus !== false;
1593
+ },
1575
1594
  hasOpenLayer: () => self.overlay !== null,
1576
1595
  routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
1577
1596
  spin: () => self.actions.spin(),
@@ -1587,27 +1606,52 @@ class ShellController extends EventEmitter {
1587
1606
  this.kbd = new KeyboardController(host);
1588
1607
  this.kbd.attach();
1589
1608
  }
1590
- pullFocus = () => { try {
1591
- globalThis.focus?.();
1592
- }
1593
- catch { /* cross-origin */ } };
1609
+ pullFocus = () => {
1610
+ try {
1611
+ globalThis.focus?.();
1612
+ }
1613
+ catch {
1614
+ /* cross-origin */
1615
+ }
1616
+ };
1594
1617
  // ── overlay flow ─────────────────────────────────────────────────────────────
1595
1618
  show(req) {
1596
1619
  this.closeModal();
1597
1620
  this.overlay = this.renderer.openOverlay(req) ?? null;
1598
1621
  }
1599
- openMenu() { this.emit('menuOpen'); this.openSettings(); }
1600
- openSettings() { this.emit('settingsOpen'); this.show({ kind: 'settings' }); }
1601
- openInfo() { this.emit('infoOpen'); this.show({ kind: 'gameInfo' }); }
1602
- openBuyBonus() { if (this.config.onBonusBuy) {
1603
- this.config.onBonusBuy();
1604
- return;
1605
- } this.show({ kind: 'buyBonus' }); }
1606
- openBetPicker() { this.show({ kind: 'betPicker' }); }
1607
- openAutoplayPicker() { this.show({ kind: 'autoplayPicker' }); }
1608
- openReplay(opts) { if (this.destroyed)
1609
- return; this.show({ kind: 'replay', opts }); }
1610
- openModal(opts) { this.show({ kind: 'modal', opts }); }
1622
+ openMenu() {
1623
+ this.emit('menuOpen');
1624
+ this.openSettings();
1625
+ }
1626
+ openSettings() {
1627
+ this.emit('settingsOpen');
1628
+ this.show({ kind: 'settings' });
1629
+ }
1630
+ openInfo() {
1631
+ this.emit('infoOpen');
1632
+ this.show({ kind: 'gameInfo' });
1633
+ }
1634
+ openBuyBonus() {
1635
+ if (this.config.onBonusBuy) {
1636
+ this.config.onBonusBuy();
1637
+ return;
1638
+ }
1639
+ this.show({ kind: 'buyBonus' });
1640
+ }
1641
+ openBetPicker() {
1642
+ this.show({ kind: 'betPicker' });
1643
+ }
1644
+ openAutoplayPicker() {
1645
+ this.show({ kind: 'autoplayPicker' });
1646
+ }
1647
+ openReplay(opts) {
1648
+ if (this.destroyed)
1649
+ return;
1650
+ this.show({ kind: 'replay', opts });
1651
+ }
1652
+ openModal(opts) {
1653
+ this.show({ kind: 'modal', opts });
1654
+ }
1611
1655
  /** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
1612
1656
  closeModal() {
1613
1657
  if (!this.overlay)
@@ -1623,7 +1667,9 @@ class ShellController extends EventEmitter {
1623
1667
  this.soundRefresh?.(on);
1624
1668
  this.renderer.refreshSoundIcon?.(on);
1625
1669
  }
1626
- setSoundRefresh(fn) { this.soundRefresh = fn; }
1670
+ setSoundRefresh(fn) {
1671
+ this.soundRefresh = fn;
1672
+ }
1627
1673
  // ── features ─────────────────────────────────────────────────────────────────
1628
1674
  activateFeature(bonus) {
1629
1675
  this.state.activeFeature = bonus;
@@ -1644,21 +1690,81 @@ class ShellController extends EventEmitter {
1644
1690
  if (to !== from)
1645
1691
  this.renderer.animateMoney(field, from, to);
1646
1692
  }
1647
- setBalance(n) { const from = this.prevBalance; this.state.balance = n; this.prevBalance = n; this.money('balance', from, n); }
1648
- setWin(n) { const from = this.prevWin; this.state.win = n; this.prevWin = n; this.money('win', from, n); }
1649
- setBet(n) { this.state.bet = n; this.renderer.renderBar(); }
1650
- setMode(mode) { if (mode === 'replay')
1651
- this.state.replay = true; this.state.mode = mode; this.renderer.renderBar(); }
1652
- setBusy(busy) { this.state.busy = busy; this.renderer.renderBar(); this.kbd?.notifyBusyChanged(busy); }
1653
- setAutoplay(a) { this.state.autoplay = a; this.renderer.renderBar(); }
1654
- setTurbo(level) { this.state.turbo = level; this.renderer.renderBar(); }
1655
- setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.renderer.renderBar(); }
1656
- setFreeSpins(fs) { this.state.freeSpins = fs; this.renderer.renderBar(); }
1657
- setTheme(theme) { this.config.theme = theme; this.tokens = resolveTheme(theme); this.renderer.applyTheme(this.tokens); this.renderer.renderBar(); }
1658
- setLanguage(lang) { this.config.language = lang; this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial }); this.renderer.renderBar(); }
1659
- setSocial(isSocial) { this.config.isSocial = isSocial; this.i18n = createI18n({ language: this.config.language, isSocial }); this.renderer.renderBar(); }
1660
- setLayout(layout) { if (layout === this.layout)
1661
- return; this.layout = layout; this.renderer.setLayout(layout); this.renderer.renderBar(); }
1693
+ setBalance(n) {
1694
+ const from = this.prevBalance;
1695
+ this.state.balance = n;
1696
+ this.prevBalance = n;
1697
+ this.money('balance', from, n);
1698
+ }
1699
+ setWin(n) {
1700
+ const from = this.prevWin;
1701
+ this.state.win = n;
1702
+ this.prevWin = n;
1703
+ this.money('win', from, n);
1704
+ }
1705
+ setBet(n) {
1706
+ this.state.bet = n;
1707
+ this.renderer.renderBar();
1708
+ }
1709
+ setMode(mode) {
1710
+ if (mode === 'replay')
1711
+ this.state.replay = true;
1712
+ this.state.mode = mode;
1713
+ this.renderer.renderBar();
1714
+ }
1715
+ setBusy(busy) {
1716
+ this.state.busy = busy;
1717
+ this.renderer.renderBar();
1718
+ this.kbd?.notifyBusyChanged(busy);
1719
+ }
1720
+ setAutoplay(a) {
1721
+ this.state.autoplay = a;
1722
+ this.renderer.renderBar();
1723
+ }
1724
+ setTurbo(level) {
1725
+ this.state.turbo = level;
1726
+ this.renderer.renderBar();
1727
+ }
1728
+ setBuyBonusEnabled(enabled) {
1729
+ this.state.buyBonusEnabled = enabled;
1730
+ this.renderer.renderBar();
1731
+ }
1732
+ setFreeSpins(fs) {
1733
+ this.state.freeSpins = fs;
1734
+ this.state.bonus = null;
1735
+ this.renderer.renderBar();
1736
+ }
1737
+ /** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
1738
+ * override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
1739
+ * `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
1740
+ setBonus(b) {
1741
+ this.state.bonus = { label: b.label, value: b.value };
1742
+ this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
1743
+ this.renderer.renderBar();
1744
+ }
1745
+ setTheme(theme) {
1746
+ this.config.theme = theme;
1747
+ this.tokens = resolveTheme(theme);
1748
+ this.renderer.applyTheme(this.tokens);
1749
+ this.renderer.renderBar();
1750
+ }
1751
+ setLanguage(lang) {
1752
+ this.config.language = lang;
1753
+ this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
1754
+ this.renderer.renderBar();
1755
+ }
1756
+ setSocial(isSocial) {
1757
+ this.config.isSocial = isSocial;
1758
+ this.i18n = createI18n({ language: this.config.language, isSocial });
1759
+ this.renderer.renderBar();
1760
+ }
1761
+ setLayout(layout) {
1762
+ if (layout === this.layout)
1763
+ return;
1764
+ this.layout = layout;
1765
+ this.renderer.setLayout(layout);
1766
+ this.renderer.renderBar();
1767
+ }
1662
1768
  destroy() {
1663
1769
  if (this.destroyed)
1664
1770
  return Promise.resolve();
@@ -2380,8 +2486,10 @@ function iconBtn(ge, name, onClick, active = false) {
2380
2486
  b.className = `ge-iconbtn${active ? ' ge-active' : ''}`;
2381
2487
  b.dataset.ge = ge;
2382
2488
  b.innerHTML = icon(name);
2383
- b.addEventListener('click', () => { if (!b.disabled)
2384
- onClick(); });
2489
+ b.addEventListener('click', () => {
2490
+ if (!b.disabled)
2491
+ onClick();
2492
+ });
2385
2493
  return b;
2386
2494
  }
2387
2495
  function renderBottomBar(host) {
@@ -2397,15 +2505,13 @@ function renderBottomBar(host) {
2397
2505
  // All three modes share the base plaque layout. FS/replay hide the controls that don't apply
2398
2506
  // and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
2399
2507
  const isBase = state.mode === 'base';
2400
- const isFS = state.mode === 'freeSpins';
2508
+ const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
2401
2509
  // FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
2402
2510
  // only when it's a free-spins replay (freeSpins.total > 0).
2403
2511
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
2404
2512
  // Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
2405
2513
  // the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
2406
- const balance = state.replay
2407
- ? null
2408
- : readout('balance', host.t('Balance'), fmt(state.balance));
2514
+ const balance = state.replay ? null : readout('balance', host.t('Balance'), fmt(state.balance));
2409
2515
  // With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
2410
2516
  // the feature accent; the base state.bet is unchanged and returns once the feature is off.
2411
2517
  const feature = state.activeFeature;
@@ -2428,21 +2534,25 @@ function renderBottomBar(host) {
2428
2534
  betDown = iconBtn('bet-down', 'minus', () => host.actions.stepBet(-1));
2429
2535
  betUp = iconBtn('bet-up', 'plus', () => host.actions.stepBet(1));
2430
2536
  betValue.classList.add('ge-betbtn'); // tap the stake → bet picker
2431
- betValue.addEventListener('click', () => { if (!betLocked(host))
2432
- host.actions.openBetPicker(); });
2537
+ betValue.addEventListener('click', () => {
2538
+ if (!betLocked(host))
2539
+ host.actions.openBetPicker();
2540
+ });
2433
2541
  spin = spinButton(host);
2434
2542
  auto = config.features.autoplay ? autoButton(host) : null;
2435
- buy = (config.features.buyBonus !== false || config.onBonusBuy) ? buyBtn(host) : null;
2543
+ buy = config.features.buyBonus !== false || config.onBonusBuy ? buyBtn(host) : null;
2436
2544
  }
2437
2545
  const winEl = state.win > 0 ? readout('win', host.t('Win'), fmtWin(state.win)) : null;
2438
2546
  // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
2439
2547
  // current = number → "current / total"; current = null/undefined → just the (game-driven) total.
2440
2548
  const fs = state.freeSpins;
2441
- const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
2549
+ const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
2442
2550
  // In FS the spins counter takes the SPIN slot as a rectangular hero plaque (same white/black-ring
2443
2551
  // style as the SPIN disc); Total Win stays an inline readout.
2444
- const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText) : null;
2445
- const fsTotalWin = showFsBlocks ? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin)) : null;
2552
+ const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText, state.bonus?.label) : null;
2553
+ const fsTotalWin = showFsBlocks
2554
+ ? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin))
2555
+ : null;
2446
2556
  // The hero in the centre/spin position: SPIN in base, the FS counter in free spins, nothing else.
2447
2557
  const hero = isBase ? spin : fsHero;
2448
2558
  if (mobile) {
@@ -2485,13 +2595,13 @@ function renderBottomBar(host) {
2485
2595
  }
2486
2596
  /** Free-spins hero plaque — takes the SPIN slot in FS: same white disc/black-ring language as SPIN,
2487
2597
  * but a rounded RECTANGLE showing the spins counter ("3 / 10"). */
2488
- function fsHeroPlaque(host, text) {
2598
+ function fsHeroPlaque(host, text, label) {
2489
2599
  const el = document.createElement('div');
2490
2600
  el.className = 'ge-fs-hero';
2491
2601
  el.dataset.ge = 'fs-counter';
2492
2602
  const lbl = document.createElement('span');
2493
2603
  lbl.className = 'ge-fs-lbl';
2494
- lbl.textContent = host.t('Free spins');
2604
+ lbl.textContent = host.t(label ?? 'Free spins');
2495
2605
  const num = document.createElement('span');
2496
2606
  num.className = 'ge-fs-num';
2497
2607
  num.textContent = text;
@@ -2511,7 +2621,9 @@ function plaque(cls, children) {
2511
2621
  d.append(...children);
2512
2622
  return d;
2513
2623
  }
2514
- function compact(items) { return items.filter((x) => x !== null); }
2624
+ function compact(items) {
2625
+ return items.filter((x) => x !== null);
2626
+ }
2515
2627
  function buyBtn(host) {
2516
2628
  const buy = document.createElement('button');
2517
2629
  buy.className = 'ge-shell-buybonus';
@@ -2524,15 +2636,19 @@ function buyBtn(host) {
2524
2636
  buy.innerHTML = `<span>${host.t('DISABLE')}</span>`;
2525
2637
  buy.style.background = accent;
2526
2638
  buy.style.color = contrastText(accent);
2527
- buy.addEventListener('click', () => { if (!buy.disabled)
2528
- host.actions.deactivateFeature(); });
2639
+ buy.addEventListener('click', () => {
2640
+ if (!buy.disabled)
2641
+ host.actions.deactivateFeature();
2642
+ });
2529
2643
  }
2530
2644
  else {
2531
2645
  // Ticket icon (no text); keep the label for screen readers.
2532
2646
  buy.setAttribute('aria-label', host.t('BUY BONUS'));
2533
2647
  buy.innerHTML = `<span class="ge-bb-tk">${icon('ticket')}</span>`;
2534
- buy.addEventListener('click', () => { if (!buy.disabled)
2535
- host.actions.openBuyBonus(); });
2648
+ buy.addEventListener('click', () => {
2649
+ if (!buy.disabled)
2650
+ host.actions.openBuyBonus();
2651
+ });
2536
2652
  }
2537
2653
  return buy;
2538
2654
  }
@@ -2550,15 +2666,19 @@ function spinButton(host) {
2550
2666
  const rem = state.autoplay.remaining;
2551
2667
  const label = Number.isFinite(rem) ? String(rem) : '∞';
2552
2668
  sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
2553
- sp.addEventListener('click', () => { if (!sp.disabled)
2554
- host.actions.stopAutoplay(); });
2669
+ sp.addEventListener('click', () => {
2670
+ if (!sp.disabled)
2671
+ host.actions.stopAutoplay();
2672
+ });
2555
2673
  }
2556
2674
  else {
2557
2675
  sp.innerHTML = icon('spin');
2558
2676
  if (state.busy)
2559
2677
  sp.classList.add('ge-spinning');
2560
- sp.addEventListener('click', () => { if (!sp.disabled)
2561
- host.actions.spin(); });
2678
+ sp.addEventListener('click', () => {
2679
+ if (!sp.disabled)
2680
+ host.actions.spin();
2681
+ });
2562
2682
  }
2563
2683
  return sp;
2564
2684
  }