@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 +185 -65
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +31 -4
- package/dist/html.esm.js +185 -65
- 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 +339 -80
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +31 -4
- package/dist/pixi.esm.js +339 -80
- 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/pixi/components/BottomBar.ts +280 -64
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();
|
|
@@ -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', () => {
|
|
2386
|
-
|
|
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', () => {
|
|
2434
|
-
host
|
|
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 =
|
|
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
|
|
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) {
|
|
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', () => {
|
|
2530
|
-
|
|
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', () => {
|
|
2537
|
-
|
|
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', () => {
|
|
2556
|
-
|
|
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', () => {
|
|
2563
|
-
|
|
2680
|
+
sp.addEventListener('click', () => {
|
|
2681
|
+
if (!sp.disabled)
|
|
2682
|
+
host.actions.spin();
|
|
2683
|
+
});
|
|
2564
2684
|
}
|
|
2565
2685
|
return sp;
|
|
2566
2686
|
}
|