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