@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.cjs.js CHANGED
@@ -65,6 +65,7 @@ function createInitialState(config) {
65
65
  turbo: 0,
66
66
  buyBonusEnabled: true,
67
67
  freeSpins: { current: 0, total: 0, totalWin: 0 },
68
+ bonus: null,
68
69
  activeFeature: null,
69
70
  };
70
71
  }
@@ -1438,7 +1439,7 @@ class KeyboardController {
1438
1439
 
1439
1440
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1440
1441
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
1441
- const PACKAGE_VERSION = '0.4.1';
1442
+ const PACKAGE_VERSION = '0.5.0';
1442
1443
 
1443
1444
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
1444
1445
  function resolveConfig(config) {
@@ -1499,9 +1500,15 @@ class ShellController extends EventEmitter {
1499
1500
  this.renderer.renderBar();
1500
1501
  }
1501
1502
  // ── ShellHost ──────────────────────────────────────────────────────────────
1502
- t(text) { return this.i18n.t(text); }
1503
- formatCurrency(n, win = false) { return formatCurrency(n, this.config.currency, win); }
1504
- formatWin(value) { return this.formatCurrency(value, true); }
1503
+ t(text) {
1504
+ return this.i18n.t(text);
1505
+ }
1506
+ formatCurrency(n, win = false) {
1507
+ return formatCurrency(n, this.config.currency, win);
1508
+ }
1509
+ formatWin(value) {
1510
+ return this.formatCurrency(value, true);
1511
+ }
1505
1512
  notifyResize(w, h) {
1506
1513
  const layout = w !== 0 && h > w ? 'mobile' : 'wide';
1507
1514
  if (layout !== this.layout) {
@@ -1568,12 +1575,24 @@ class ShellController extends EventEmitter {
1568
1575
  // eslint-disable-next-line @typescript-eslint/no-this-alias
1569
1576
  const self = this;
1570
1577
  const host = {
1571
- get state() { return self.state; },
1572
- get hotkeysEnabled() { return self.config.features.hotkeys !== false; },
1573
- get spacebarEnabled() { return self.config.features.spacebar !== false; },
1574
- get turboLevels() { return self.config.features.turbo; },
1575
- get autoplayEnabled() { return self.config.features.autoplay != null; },
1576
- get buyBonusEnabled() { return self.config.features.buyBonus !== false; },
1578
+ get state() {
1579
+ return self.state;
1580
+ },
1581
+ get hotkeysEnabled() {
1582
+ return self.config.features.hotkeys !== false;
1583
+ },
1584
+ get spacebarEnabled() {
1585
+ return self.config.features.spacebar !== false;
1586
+ },
1587
+ get turboLevels() {
1588
+ return self.config.features.turbo;
1589
+ },
1590
+ get autoplayEnabled() {
1591
+ return self.config.features.autoplay != null;
1592
+ },
1593
+ get buyBonusEnabled() {
1594
+ return self.config.features.buyBonus !== false;
1595
+ },
1577
1596
  hasOpenLayer: () => self.overlay !== null,
1578
1597
  routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
1579
1598
  spin: () => self.actions.spin(),
@@ -1589,27 +1608,52 @@ class ShellController extends EventEmitter {
1589
1608
  this.kbd = new KeyboardController(host);
1590
1609
  this.kbd.attach();
1591
1610
  }
1592
- pullFocus = () => { try {
1593
- globalThis.focus?.();
1594
- }
1595
- catch { /* cross-origin */ } };
1611
+ pullFocus = () => {
1612
+ try {
1613
+ globalThis.focus?.();
1614
+ }
1615
+ catch {
1616
+ /* cross-origin */
1617
+ }
1618
+ };
1596
1619
  // ── overlay flow ─────────────────────────────────────────────────────────────
1597
1620
  show(req) {
1598
1621
  this.closeModal();
1599
1622
  this.overlay = this.renderer.openOverlay(req) ?? null;
1600
1623
  }
1601
- openMenu() { this.emit('menuOpen'); this.openSettings(); }
1602
- openSettings() { this.emit('settingsOpen'); this.show({ kind: 'settings' }); }
1603
- openInfo() { this.emit('infoOpen'); this.show({ kind: 'gameInfo' }); }
1604
- openBuyBonus() { if (this.config.onBonusBuy) {
1605
- this.config.onBonusBuy();
1606
- return;
1607
- } this.show({ kind: 'buyBonus' }); }
1608
- openBetPicker() { this.show({ kind: 'betPicker' }); }
1609
- openAutoplayPicker() { this.show({ kind: 'autoplayPicker' }); }
1610
- openReplay(opts) { if (this.destroyed)
1611
- return; this.show({ kind: 'replay', opts }); }
1612
- openModal(opts) { this.show({ kind: 'modal', opts }); }
1624
+ openMenu() {
1625
+ this.emit('menuOpen');
1626
+ this.openSettings();
1627
+ }
1628
+ openSettings() {
1629
+ this.emit('settingsOpen');
1630
+ this.show({ kind: 'settings' });
1631
+ }
1632
+ openInfo() {
1633
+ this.emit('infoOpen');
1634
+ this.show({ kind: 'gameInfo' });
1635
+ }
1636
+ openBuyBonus() {
1637
+ if (this.config.onBonusBuy) {
1638
+ this.config.onBonusBuy();
1639
+ return;
1640
+ }
1641
+ this.show({ kind: 'buyBonus' });
1642
+ }
1643
+ openBetPicker() {
1644
+ this.show({ kind: 'betPicker' });
1645
+ }
1646
+ openAutoplayPicker() {
1647
+ this.show({ kind: 'autoplayPicker' });
1648
+ }
1649
+ openReplay(opts) {
1650
+ if (this.destroyed)
1651
+ return;
1652
+ this.show({ kind: 'replay', opts });
1653
+ }
1654
+ openModal(opts) {
1655
+ this.show({ kind: 'modal', opts });
1656
+ }
1613
1657
  /** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
1614
1658
  closeModal() {
1615
1659
  if (!this.overlay)
@@ -1625,7 +1669,9 @@ class ShellController extends EventEmitter {
1625
1669
  this.soundRefresh?.(on);
1626
1670
  this.renderer.refreshSoundIcon?.(on);
1627
1671
  }
1628
- setSoundRefresh(fn) { this.soundRefresh = fn; }
1672
+ setSoundRefresh(fn) {
1673
+ this.soundRefresh = fn;
1674
+ }
1629
1675
  // ── features ─────────────────────────────────────────────────────────────────
1630
1676
  activateFeature(bonus) {
1631
1677
  this.state.activeFeature = bonus;
@@ -1646,21 +1692,81 @@ class ShellController extends EventEmitter {
1646
1692
  if (to !== from)
1647
1693
  this.renderer.animateMoney(field, from, to);
1648
1694
  }
1649
- setBalance(n) { const from = this.prevBalance; this.state.balance = n; this.prevBalance = n; this.money('balance', from, n); }
1650
- setWin(n) { const from = this.prevWin; this.state.win = n; this.prevWin = n; this.money('win', from, n); }
1651
- setBet(n) { this.state.bet = n; this.renderer.renderBar(); }
1652
- setMode(mode) { if (mode === 'replay')
1653
- this.state.replay = true; this.state.mode = mode; this.renderer.renderBar(); }
1654
- setBusy(busy) { this.state.busy = busy; this.renderer.renderBar(); this.kbd?.notifyBusyChanged(busy); }
1655
- setAutoplay(a) { this.state.autoplay = a; this.renderer.renderBar(); }
1656
- setTurbo(level) { this.state.turbo = level; this.renderer.renderBar(); }
1657
- setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.renderer.renderBar(); }
1658
- setFreeSpins(fs) { this.state.freeSpins = fs; this.renderer.renderBar(); }
1659
- setTheme(theme) { this.config.theme = theme; this.tokens = resolveTheme(theme); this.renderer.applyTheme(this.tokens); this.renderer.renderBar(); }
1660
- setLanguage(lang) { this.config.language = lang; this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial }); this.renderer.renderBar(); }
1661
- setSocial(isSocial) { this.config.isSocial = isSocial; this.i18n = createI18n({ language: this.config.language, isSocial }); this.renderer.renderBar(); }
1662
- setLayout(layout) { if (layout === this.layout)
1663
- return; this.layout = layout; this.renderer.setLayout(layout); this.renderer.renderBar(); }
1695
+ setBalance(n) {
1696
+ const from = this.prevBalance;
1697
+ this.state.balance = n;
1698
+ this.prevBalance = n;
1699
+ this.money('balance', from, n);
1700
+ }
1701
+ setWin(n) {
1702
+ const from = this.prevWin;
1703
+ this.state.win = n;
1704
+ this.prevWin = n;
1705
+ this.money('win', from, n);
1706
+ }
1707
+ setBet(n) {
1708
+ this.state.bet = n;
1709
+ this.renderer.renderBar();
1710
+ }
1711
+ setMode(mode) {
1712
+ if (mode === 'replay')
1713
+ this.state.replay = true;
1714
+ this.state.mode = mode;
1715
+ this.renderer.renderBar();
1716
+ }
1717
+ setBusy(busy) {
1718
+ this.state.busy = busy;
1719
+ this.renderer.renderBar();
1720
+ this.kbd?.notifyBusyChanged(busy);
1721
+ }
1722
+ setAutoplay(a) {
1723
+ this.state.autoplay = a;
1724
+ this.renderer.renderBar();
1725
+ }
1726
+ setTurbo(level) {
1727
+ this.state.turbo = level;
1728
+ this.renderer.renderBar();
1729
+ }
1730
+ setBuyBonusEnabled(enabled) {
1731
+ this.state.buyBonusEnabled = enabled;
1732
+ this.renderer.renderBar();
1733
+ }
1734
+ setFreeSpins(fs) {
1735
+ this.state.freeSpins = fs;
1736
+ this.state.bonus = null;
1737
+ this.renderer.renderBar();
1738
+ }
1739
+ /** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
1740
+ * override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
1741
+ * `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
1742
+ setBonus(b) {
1743
+ this.state.bonus = { label: b.label, value: b.value };
1744
+ this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
1745
+ this.renderer.renderBar();
1746
+ }
1747
+ setTheme(theme) {
1748
+ this.config.theme = theme;
1749
+ this.tokens = resolveTheme(theme);
1750
+ this.renderer.applyTheme(this.tokens);
1751
+ this.renderer.renderBar();
1752
+ }
1753
+ setLanguage(lang) {
1754
+ this.config.language = lang;
1755
+ this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
1756
+ this.renderer.renderBar();
1757
+ }
1758
+ setSocial(isSocial) {
1759
+ this.config.isSocial = isSocial;
1760
+ this.i18n = createI18n({ language: this.config.language, isSocial });
1761
+ this.renderer.renderBar();
1762
+ }
1763
+ setLayout(layout) {
1764
+ if (layout === this.layout)
1765
+ return;
1766
+ this.layout = layout;
1767
+ this.renderer.setLayout(layout);
1768
+ this.renderer.renderBar();
1769
+ }
1664
1770
  destroy() {
1665
1771
  if (this.destroyed)
1666
1772
  return Promise.resolve();
@@ -2382,8 +2488,10 @@ function iconBtn(ge, name, onClick, active = false) {
2382
2488
  b.className = `ge-iconbtn${active ? ' ge-active' : ''}`;
2383
2489
  b.dataset.ge = ge;
2384
2490
  b.innerHTML = icon(name);
2385
- b.addEventListener('click', () => { if (!b.disabled)
2386
- onClick(); });
2491
+ b.addEventListener('click', () => {
2492
+ if (!b.disabled)
2493
+ onClick();
2494
+ });
2387
2495
  return b;
2388
2496
  }
2389
2497
  function renderBottomBar(host) {
@@ -2399,15 +2507,13 @@ function renderBottomBar(host) {
2399
2507
  // All three modes share the base plaque layout. FS/replay hide the controls that don't apply
2400
2508
  // and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
2401
2509
  const isBase = state.mode === 'base';
2402
- const isFS = state.mode === 'freeSpins';
2510
+ const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
2403
2511
  // FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
2404
2512
  // only when it's a free-spins replay (freeSpins.total > 0).
2405
2513
  const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
2406
2514
  // Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
2407
2515
  // the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
2408
- const balance = state.replay
2409
- ? null
2410
- : readout('balance', host.t('Balance'), fmt(state.balance));
2516
+ const balance = state.replay ? null : readout('balance', host.t('Balance'), fmt(state.balance));
2411
2517
  // With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
2412
2518
  // the feature accent; the base state.bet is unchanged and returns once the feature is off.
2413
2519
  const feature = state.activeFeature;
@@ -2430,21 +2536,25 @@ function renderBottomBar(host) {
2430
2536
  betDown = iconBtn('bet-down', 'minus', () => host.actions.stepBet(-1));
2431
2537
  betUp = iconBtn('bet-up', 'plus', () => host.actions.stepBet(1));
2432
2538
  betValue.classList.add('ge-betbtn'); // tap the stake → bet picker
2433
- betValue.addEventListener('click', () => { if (!betLocked(host))
2434
- host.actions.openBetPicker(); });
2539
+ betValue.addEventListener('click', () => {
2540
+ if (!betLocked(host))
2541
+ host.actions.openBetPicker();
2542
+ });
2435
2543
  spin = spinButton(host);
2436
2544
  auto = config.features.autoplay ? autoButton(host) : null;
2437
- buy = (config.features.buyBonus !== false || config.onBonusBuy) ? buyBtn(host) : null;
2545
+ buy = config.features.buyBonus !== false || config.onBonusBuy ? buyBtn(host) : null;
2438
2546
  }
2439
2547
  const winEl = state.win > 0 ? readout('win', host.t('Win'), fmtWin(state.win)) : null;
2440
2548
  // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
2441
2549
  // current = number → "current / total"; current = null/undefined → just the (game-driven) total.
2442
2550
  const fs = state.freeSpins;
2443
- const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
2551
+ const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
2444
2552
  // In FS the spins counter takes the SPIN slot as a rectangular hero plaque (same white/black-ring
2445
2553
  // style as the SPIN disc); Total Win stays an inline readout.
2446
- const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText) : null;
2447
- const fsTotalWin = showFsBlocks ? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin)) : null;
2554
+ const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText, state.bonus?.label) : null;
2555
+ const fsTotalWin = showFsBlocks
2556
+ ? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin))
2557
+ : null;
2448
2558
  // The hero in the centre/spin position: SPIN in base, the FS counter in free spins, nothing else.
2449
2559
  const hero = isBase ? spin : fsHero;
2450
2560
  if (mobile) {
@@ -2487,13 +2597,13 @@ function renderBottomBar(host) {
2487
2597
  }
2488
2598
  /** Free-spins hero plaque — takes the SPIN slot in FS: same white disc/black-ring language as SPIN,
2489
2599
  * but a rounded RECTANGLE showing the spins counter ("3 / 10"). */
2490
- function fsHeroPlaque(host, text) {
2600
+ function fsHeroPlaque(host, text, label) {
2491
2601
  const el = document.createElement('div');
2492
2602
  el.className = 'ge-fs-hero';
2493
2603
  el.dataset.ge = 'fs-counter';
2494
2604
  const lbl = document.createElement('span');
2495
2605
  lbl.className = 'ge-fs-lbl';
2496
- lbl.textContent = host.t('Free spins');
2606
+ lbl.textContent = host.t(label ?? 'Free spins');
2497
2607
  const num = document.createElement('span');
2498
2608
  num.className = 'ge-fs-num';
2499
2609
  num.textContent = text;
@@ -2513,7 +2623,9 @@ function plaque(cls, children) {
2513
2623
  d.append(...children);
2514
2624
  return d;
2515
2625
  }
2516
- function compact(items) { return items.filter((x) => x !== null); }
2626
+ function compact(items) {
2627
+ return items.filter((x) => x !== null);
2628
+ }
2517
2629
  function buyBtn(host) {
2518
2630
  const buy = document.createElement('button');
2519
2631
  buy.className = 'ge-shell-buybonus';
@@ -2526,15 +2638,19 @@ function buyBtn(host) {
2526
2638
  buy.innerHTML = `<span>${host.t('DISABLE')}</span>`;
2527
2639
  buy.style.background = accent;
2528
2640
  buy.style.color = contrastText(accent);
2529
- buy.addEventListener('click', () => { if (!buy.disabled)
2530
- host.actions.deactivateFeature(); });
2641
+ buy.addEventListener('click', () => {
2642
+ if (!buy.disabled)
2643
+ host.actions.deactivateFeature();
2644
+ });
2531
2645
  }
2532
2646
  else {
2533
2647
  // Ticket icon (no text); keep the label for screen readers.
2534
2648
  buy.setAttribute('aria-label', host.t('BUY BONUS'));
2535
2649
  buy.innerHTML = `<span class="ge-bb-tk">${icon('ticket')}</span>`;
2536
- buy.addEventListener('click', () => { if (!buy.disabled)
2537
- host.actions.openBuyBonus(); });
2650
+ buy.addEventListener('click', () => {
2651
+ if (!buy.disabled)
2652
+ host.actions.openBuyBonus();
2653
+ });
2538
2654
  }
2539
2655
  return buy;
2540
2656
  }
@@ -2552,15 +2668,19 @@ function spinButton(host) {
2552
2668
  const rem = state.autoplay.remaining;
2553
2669
  const label = Number.isFinite(rem) ? String(rem) : '∞';
2554
2670
  sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
2555
- sp.addEventListener('click', () => { if (!sp.disabled)
2556
- host.actions.stopAutoplay(); });
2671
+ sp.addEventListener('click', () => {
2672
+ if (!sp.disabled)
2673
+ host.actions.stopAutoplay();
2674
+ });
2557
2675
  }
2558
2676
  else {
2559
2677
  sp.innerHTML = icon('spin');
2560
2678
  if (state.busy)
2561
2679
  sp.classList.add('ge-spinning');
2562
- sp.addEventListener('click', () => { if (!sp.disabled)
2563
- host.actions.spin(); });
2680
+ sp.addEventListener('click', () => {
2681
+ if (!sp.disabled)
2682
+ host.actions.spin();
2683
+ });
2564
2684
  }
2565
2685
  return sp;
2566
2686
  }