@energy8platform/shell 0.4.0 → 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 +214 -69
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +31 -4
- package/dist/html.esm.js +214 -69
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +148 -42
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.esm.js +148 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +451 -107
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +31 -4
- package/dist/pixi.esm.js +451 -107
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +183 -43
- package/src/core/state.ts +1 -0
- package/src/core/types.ts +40 -4
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BottomBar.ts +71 -32
- package/src/ui/html/components/pickers.ts +13 -1
- package/src/ui/html/shell.css.ts +19 -3
- package/src/ui/pixi/components/BottomBar.ts +280 -64
- package/src/ui/pixi/components/GameInfo.ts +52 -13
- package/src/ui/pixi/components/pickers.ts +66 -15
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.
|
|
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) {
|
|
1503
|
-
|
|
1504
|
-
|
|
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() {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
get
|
|
1575
|
-
|
|
1576
|
-
|
|
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 = () => {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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() {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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) {
|
|
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) {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
this.
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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();
|
|
@@ -1912,7 +2018,12 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
1912
2018
|
#${SHELL_ROOT_ID} .ge-gi-hk-block { display:flex; flex-direction:column; }
|
|
1913
2019
|
#${SHELL_ROOT_ID} .ge-gi-hk { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 0; }
|
|
1914
2020
|
#${SHELL_ROOT_ID} .ge-gi-hk + .ge-gi-hk { border-top:1px solid var(--shell-plaque-line); }
|
|
1915
|
-
|
|
2021
|
+
/* Let the keycaps SHRINK instead of hogging the row: flex:0 1 auto lets the chips block give up
|
|
2022
|
+
width down to its widest single combo, and flex-wrap reflows the extra combos onto another line.
|
|
2023
|
+
On narrow/mobile widths the many-chip rows (Raise/Lower bet) otherwise squeeze the label off the
|
|
2024
|
+
right edge and clip it. (No min-width:0 here — the min-content floor is what keeps a combo intact
|
|
2025
|
+
and wrapping rather than overflowing.) */
|
|
2026
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-chips { display:flex; align-items:center; flex-wrap:wrap; gap:4px; flex:0 1 auto; }
|
|
1916
2027
|
#${SHELL_ROOT_ID} .ge-gi-hk-combo { display:inline-flex; align-items:center; gap:4px; }
|
|
1917
2028
|
#${SHELL_ROOT_ID} .ge-gi-hk-chip { display:inline-flex; align-items:center; justify-content:center;
|
|
1918
2029
|
padding:2px 7px; border-radius:6px; border:1px solid var(--shell-plaque-line);
|
|
@@ -1921,7 +2032,10 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
1921
2032
|
font-weight:600; line-height:1.5; white-space:nowrap; min-width:1.6em; text-align:center; }
|
|
1922
2033
|
#${SHELL_ROOT_ID} .ge-gi-hk-sep { color:var(--shell-plaque-label); font-size:11px; padding:0 1px; }
|
|
1923
2034
|
#${SHELL_ROOT_ID} .ge-gi-hk-sep2 { color:var(--shell-plaque-label); font-size:11px; padding:0 4px; }
|
|
1924
|
-
|
|
2035
|
+
/* flex:1 1 auto (content-based basis, NOT flex:1's 0 basis) so the label keeps demanding its word
|
|
2036
|
+
width and shrinks in PROPORTION to the chips — both give ground and wrap, instead of the label
|
|
2037
|
+
collapsing to nothing. No min-width:0: the min-content floor stops a word being clipped. */
|
|
2038
|
+
#${SHELL_ROOT_ID} .ge-gi-hk-tx { color:rgba(255,255,255,.88); font-size:14px; font-weight:600; text-align:right; flex:1 1 auto; }
|
|
1925
2039
|
|
|
1926
2040
|
/* controls — two blocks (gameplay / menu & info), icon/name/description per control */
|
|
1927
2041
|
#${SHELL_ROOT_ID} .ge-gi-ctl-block + .ge-gi-ctl-block { margin-top:16px; padding-top:4px; border-top:1px solid var(--shell-plaque-line); }
|
|
@@ -1935,7 +2049,7 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
1935
2049
|
#${SHELL_ROOT_ID} .ge-gi-ctl-ic--bet { flex-direction:column; font-size:18px; gap:0; }
|
|
1936
2050
|
/* buy bonus draws the real control-bar badge, scaled down & non-interactive */
|
|
1937
2051
|
#${SHELL_ROOT_ID} .ge-gi-ctl-ic .ge-shell-buybonus { width:46px; height:46px; font-size:8px; border-width:2px; pointer-events:none; }
|
|
1938
|
-
#${SHELL_ROOT_ID} .ge-gi-ctl-tx { display:flex; flex-direction:column; gap:2px; }
|
|
2052
|
+
#${SHELL_ROOT_ID} .ge-gi-ctl-tx { display:flex; flex-direction:column; gap:2px; min-width:0; }
|
|
1939
2053
|
#${SHELL_ROOT_ID} .ge-gi-ctl-tx b { color:#fff; font-size:15px; font-weight:700; }
|
|
1940
2054
|
#${SHELL_ROOT_ID} .ge-gi-ctl-tx span { color:rgba(255,255,255,.7); font-size:13px; line-height:1.4; }
|
|
1941
2055
|
|
|
@@ -2222,6 +2336,14 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
|
|
|
2222
2336
|
#${SHELL_ROOT_ID}.ge-mobile .ge-sheet-grid { grid-template-columns:repeat(var(--cols-m,var(--cols,3)),1fr); }
|
|
2223
2337
|
/* the bet picker packs 6 chips/row → give its card room (autoplay & co. stay at the 28em default) */
|
|
2224
2338
|
#${SHELL_ROOT_ID} .ge-sheet[data-ge="bet-modal"] .ge-modal-card { max-width:44em; }
|
|
2339
|
+
/* Bet chips carry variable-width currency (a wide currency makes labels grow). Reflow the columns
|
|
2340
|
+
to the widest label (--chip-min, computed in JS) so no chip is ever clipped, and cap the grid
|
|
2341
|
+
height so a long bet ladder scrolls instead of overflowing the card's clipped edge. */
|
|
2342
|
+
#${SHELL_ROOT_ID} .ge-sheet[data-ge="bet-modal"] .ge-sheet-grid,
|
|
2343
|
+
#${SHELL_ROOT_ID}.ge-mobile .ge-sheet[data-ge="bet-modal"] .ge-sheet-grid {
|
|
2344
|
+
grid-template-columns:repeat(auto-fill, minmax(min(100%, var(--chip-min, 6em)), 1fr));
|
|
2345
|
+
max-height:min(50vh, 28em); overflow-y:auto; overflow-x:hidden; }
|
|
2346
|
+
#${SHELL_ROOT_ID} .ge-sheet[data-ge="bet-modal"] .ge-chip { min-width:0; }
|
|
2225
2347
|
#${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
|
|
2226
2348
|
border-radius:.8em; background:rgba(255,255,255,.04); color:#fff; font-size:1em; font-weight:700;
|
|
2227
2349
|
font-variant-numeric:tabular-nums; padding:.8em .55em; transition:background .12s ease, border-color .12s ease; }
|
|
@@ -2366,8 +2488,10 @@ function iconBtn(ge, name, onClick, active = false) {
|
|
|
2366
2488
|
b.className = `ge-iconbtn${active ? ' ge-active' : ''}`;
|
|
2367
2489
|
b.dataset.ge = ge;
|
|
2368
2490
|
b.innerHTML = icon(name);
|
|
2369
|
-
b.addEventListener('click', () => {
|
|
2370
|
-
|
|
2491
|
+
b.addEventListener('click', () => {
|
|
2492
|
+
if (!b.disabled)
|
|
2493
|
+
onClick();
|
|
2494
|
+
});
|
|
2371
2495
|
return b;
|
|
2372
2496
|
}
|
|
2373
2497
|
function renderBottomBar(host) {
|
|
@@ -2383,15 +2507,13 @@ function renderBottomBar(host) {
|
|
|
2383
2507
|
// All three modes share the base plaque layout. FS/replay hide the controls that don't apply
|
|
2384
2508
|
// and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
|
|
2385
2509
|
const isBase = state.mode === 'base';
|
|
2386
|
-
const isFS = state.mode === 'freeSpins';
|
|
2510
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
2387
2511
|
// FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
|
|
2388
2512
|
// only when it's a free-spins replay (freeSpins.total > 0).
|
|
2389
2513
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
2390
2514
|
// Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
|
|
2391
2515
|
// the sticky `replay` flag (not `mode`) so it stays hidden through a replay's free-spins phase.
|
|
2392
|
-
const balance = state.replay
|
|
2393
|
-
? null
|
|
2394
|
-
: readout('balance', host.t('Balance'), fmt(state.balance));
|
|
2516
|
+
const balance = state.replay ? null : readout('balance', host.t('Balance'), fmt(state.balance));
|
|
2395
2517
|
// With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
|
|
2396
2518
|
// the feature accent; the base state.bet is unchanged and returns once the feature is off.
|
|
2397
2519
|
const feature = state.activeFeature;
|
|
@@ -2414,21 +2536,25 @@ function renderBottomBar(host) {
|
|
|
2414
2536
|
betDown = iconBtn('bet-down', 'minus', () => host.actions.stepBet(-1));
|
|
2415
2537
|
betUp = iconBtn('bet-up', 'plus', () => host.actions.stepBet(1));
|
|
2416
2538
|
betValue.classList.add('ge-betbtn'); // tap the stake → bet picker
|
|
2417
|
-
betValue.addEventListener('click', () => {
|
|
2418
|
-
host
|
|
2539
|
+
betValue.addEventListener('click', () => {
|
|
2540
|
+
if (!betLocked(host))
|
|
2541
|
+
host.actions.openBetPicker();
|
|
2542
|
+
});
|
|
2419
2543
|
spin = spinButton(host);
|
|
2420
2544
|
auto = config.features.autoplay ? autoButton(host) : null;
|
|
2421
|
-
buy =
|
|
2545
|
+
buy = config.features.buyBonus !== false || config.onBonusBuy ? buyBtn(host) : null;
|
|
2422
2546
|
}
|
|
2423
2547
|
const winEl = state.win > 0 ? readout('win', host.t('Win'), fmtWin(state.win)) : null;
|
|
2424
2548
|
// FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
|
|
2425
2549
|
// current = number → "current / total"; current = null/undefined → just the (game-driven) total.
|
|
2426
2550
|
const fs = state.freeSpins;
|
|
2427
|
-
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}`);
|
|
2428
2552
|
// In FS the spins counter takes the SPIN slot as a rectangular hero plaque (same white/black-ring
|
|
2429
2553
|
// style as the SPIN disc); Total Win stays an inline readout.
|
|
2430
|
-
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText) : null;
|
|
2431
|
-
const fsTotalWin = showFsBlocks
|
|
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;
|
|
2432
2558
|
// The hero in the centre/spin position: SPIN in base, the FS counter in free spins, nothing else.
|
|
2433
2559
|
const hero = isBase ? spin : fsHero;
|
|
2434
2560
|
if (mobile) {
|
|
@@ -2471,13 +2597,13 @@ function renderBottomBar(host) {
|
|
|
2471
2597
|
}
|
|
2472
2598
|
/** Free-spins hero plaque — takes the SPIN slot in FS: same white disc/black-ring language as SPIN,
|
|
2473
2599
|
* but a rounded RECTANGLE showing the spins counter ("3 / 10"). */
|
|
2474
|
-
function fsHeroPlaque(host, text) {
|
|
2600
|
+
function fsHeroPlaque(host, text, label) {
|
|
2475
2601
|
const el = document.createElement('div');
|
|
2476
2602
|
el.className = 'ge-fs-hero';
|
|
2477
2603
|
el.dataset.ge = 'fs-counter';
|
|
2478
2604
|
const lbl = document.createElement('span');
|
|
2479
2605
|
lbl.className = 'ge-fs-lbl';
|
|
2480
|
-
lbl.textContent = host.t('Free spins');
|
|
2606
|
+
lbl.textContent = host.t(label ?? 'Free spins');
|
|
2481
2607
|
const num = document.createElement('span');
|
|
2482
2608
|
num.className = 'ge-fs-num';
|
|
2483
2609
|
num.textContent = text;
|
|
@@ -2497,7 +2623,9 @@ function plaque(cls, children) {
|
|
|
2497
2623
|
d.append(...children);
|
|
2498
2624
|
return d;
|
|
2499
2625
|
}
|
|
2500
|
-
function compact(items) {
|
|
2626
|
+
function compact(items) {
|
|
2627
|
+
return items.filter((x) => x !== null);
|
|
2628
|
+
}
|
|
2501
2629
|
function buyBtn(host) {
|
|
2502
2630
|
const buy = document.createElement('button');
|
|
2503
2631
|
buy.className = 'ge-shell-buybonus';
|
|
@@ -2510,15 +2638,19 @@ function buyBtn(host) {
|
|
|
2510
2638
|
buy.innerHTML = `<span>${host.t('DISABLE')}</span>`;
|
|
2511
2639
|
buy.style.background = accent;
|
|
2512
2640
|
buy.style.color = contrastText(accent);
|
|
2513
|
-
buy.addEventListener('click', () => {
|
|
2514
|
-
|
|
2641
|
+
buy.addEventListener('click', () => {
|
|
2642
|
+
if (!buy.disabled)
|
|
2643
|
+
host.actions.deactivateFeature();
|
|
2644
|
+
});
|
|
2515
2645
|
}
|
|
2516
2646
|
else {
|
|
2517
2647
|
// Ticket icon (no text); keep the label for screen readers.
|
|
2518
2648
|
buy.setAttribute('aria-label', host.t('BUY BONUS'));
|
|
2519
2649
|
buy.innerHTML = `<span class="ge-bb-tk">${icon('ticket')}</span>`;
|
|
2520
|
-
buy.addEventListener('click', () => {
|
|
2521
|
-
|
|
2650
|
+
buy.addEventListener('click', () => {
|
|
2651
|
+
if (!buy.disabled)
|
|
2652
|
+
host.actions.openBuyBonus();
|
|
2653
|
+
});
|
|
2522
2654
|
}
|
|
2523
2655
|
return buy;
|
|
2524
2656
|
}
|
|
@@ -2536,15 +2668,19 @@ function spinButton(host) {
|
|
|
2536
2668
|
const rem = state.autoplay.remaining;
|
|
2537
2669
|
const label = Number.isFinite(rem) ? String(rem) : '∞';
|
|
2538
2670
|
sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
|
|
2539
|
-
sp.addEventListener('click', () => {
|
|
2540
|
-
|
|
2671
|
+
sp.addEventListener('click', () => {
|
|
2672
|
+
if (!sp.disabled)
|
|
2673
|
+
host.actions.stopAutoplay();
|
|
2674
|
+
});
|
|
2541
2675
|
}
|
|
2542
2676
|
else {
|
|
2543
2677
|
sp.innerHTML = icon('spin');
|
|
2544
2678
|
if (state.busy)
|
|
2545
2679
|
sp.classList.add('ge-spinning');
|
|
2546
|
-
sp.addEventListener('click', () => {
|
|
2547
|
-
|
|
2680
|
+
sp.addEventListener('click', () => {
|
|
2681
|
+
if (!sp.disabled)
|
|
2682
|
+
host.actions.spin();
|
|
2683
|
+
});
|
|
2548
2684
|
}
|
|
2549
2685
|
return sp;
|
|
2550
2686
|
}
|
|
@@ -3472,6 +3608,15 @@ function buildSheet(opts) {
|
|
|
3472
3608
|
const cols = typeof opts.columns === 'number' ? { wide: opts.columns, mobile: opts.columns } : opts.columns;
|
|
3473
3609
|
grid.style.setProperty('--cols', String(cols.wide));
|
|
3474
3610
|
grid.style.setProperty('--cols-m', String(cols.mobile));
|
|
3611
|
+
if (opts.autoFit) {
|
|
3612
|
+
// Widest label in ch (tabular digits ≈ 1ch each; symbols/separators are narrower, so this
|
|
3613
|
+
// slightly over-estimates → safe) + chip padding/border. Floored so normal-width currencies
|
|
3614
|
+
// keep the compact ~6-per-row look and only wide currencies drop to fewer columns.
|
|
3615
|
+
const maxLen = opts.choices.reduce((m, c) => Math.max(m, c.label.length), 0);
|
|
3616
|
+
// Floor at 6em ≈ the natural width of a chip in the 6-wide layout (44em card): short labels
|
|
3617
|
+
// clamp here so a normal currency keeps 6 columns; wider labels raise it and drop to fewer.
|
|
3618
|
+
grid.style.setProperty('--chip-min', `max(6em, calc(${maxLen}ch + 1.4em))`);
|
|
3619
|
+
}
|
|
3475
3620
|
let selected = opts.selected;
|
|
3476
3621
|
let focusIndex = opts.choices.findIndex((c) => c.id === selected);
|
|
3477
3622
|
if (focusIndex < 0)
|
|
@@ -3543,7 +3688,7 @@ function buildSheet(opts) {
|
|
|
3543
3688
|
/** Bet picker — all available bets as chips (6 per row, 3 on mobile), accent Confirm applies it. */
|
|
3544
3689
|
function openBetModal(host) {
|
|
3545
3690
|
return buildSheet({
|
|
3546
|
-
ge: 'bet-modal', title: host.t('Bet'), columns: { wide: 6, mobile: 3 }, confirmLabel: host.t('Confirm'),
|
|
3691
|
+
ge: 'bet-modal', title: host.t('Bet'), columns: { wide: 6, mobile: 3 }, autoFit: true, confirmLabel: host.t('Confirm'),
|
|
3547
3692
|
choices: host.state.availableBets.map((b) => ({ id: String(b), label: host.formatCurrency(b) })),
|
|
3548
3693
|
selected: String(host.state.bet),
|
|
3549
3694
|
onClose: () => host.actions.closeOverlay(),
|