@energy8platform/shell 0.4.1 → 0.6.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 +223 -69
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +56 -5
- package/dist/html.esm.js +223 -69
- package/dist/html.esm.js.map +1 -1
- package/dist/index.cjs.js +176 -42
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +56 -5
- package/dist/index.esm.js +176 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/pixi.cjs.js +392 -88
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +56 -5
- package/dist/pixi.esm.js +392 -88
- package/dist/pixi.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ShellController.ts +204 -43
- package/src/core/renderer.ts +8 -1
- package/src/core/state.ts +11 -0
- package/src/core/types.ts +51 -4
- package/src/core/version.ts +1 -1
- package/src/ui/html/components/BottomBar.ts +71 -32
- package/src/ui/html/components/Settings.ts +14 -5
- package/src/ui/pixi/PixiRenderer.ts +3 -0
- package/src/ui/pixi/components/BottomBar.ts +280 -64
- package/src/ui/pixi/components/Settings.ts +23 -8
- package/src/ui/pixi/primitives/controls.ts +7 -0
package/dist/html.cjs.js
CHANGED
|
@@ -65,9 +65,19 @@ 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,
|
|
70
|
+
volumes: {
|
|
71
|
+
master: clampVolume(config.volumes?.master),
|
|
72
|
+
music: clampVolume(config.volumes?.music),
|
|
73
|
+
sfx: clampVolume(config.volumes?.sfx),
|
|
74
|
+
},
|
|
69
75
|
};
|
|
70
76
|
}
|
|
77
|
+
/** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
|
|
78
|
+
function clampVolume(v) {
|
|
79
|
+
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
|
80
|
+
}
|
|
71
81
|
/** Step bet up/down within availableBets, clamped at the ends. */
|
|
72
82
|
function stepBet(state, direction) {
|
|
73
83
|
const idx = state.availableBets.indexOf(state.bet);
|
|
@@ -1438,7 +1448,7 @@ class KeyboardController {
|
|
|
1438
1448
|
|
|
1439
1449
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1440
1450
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1441
|
-
const PACKAGE_VERSION = '0.
|
|
1451
|
+
const PACKAGE_VERSION = '0.6.0';
|
|
1442
1452
|
|
|
1443
1453
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1444
1454
|
function resolveConfig(config) {
|
|
@@ -1455,6 +1465,7 @@ function resolveConfig(config) {
|
|
|
1455
1465
|
features: config.features,
|
|
1456
1466
|
theme: config.theme,
|
|
1457
1467
|
onBonusBuy: config.onBonusBuy,
|
|
1468
|
+
volumes: config.volumes,
|
|
1458
1469
|
version: config.version ?? '1.0.0',
|
|
1459
1470
|
isSocial: config.isSocial ?? false,
|
|
1460
1471
|
replay: config.replay ?? config.mode === 'replay',
|
|
@@ -1476,6 +1487,7 @@ class ShellController extends EventEmitter {
|
|
|
1476
1487
|
kbd;
|
|
1477
1488
|
overlay = null;
|
|
1478
1489
|
soundRefresh = null;
|
|
1490
|
+
volumeRefresh = null;
|
|
1479
1491
|
prevBalance;
|
|
1480
1492
|
prevWin;
|
|
1481
1493
|
destroyed = false;
|
|
@@ -1499,9 +1511,15 @@ class ShellController extends EventEmitter {
|
|
|
1499
1511
|
this.renderer.renderBar();
|
|
1500
1512
|
}
|
|
1501
1513
|
// ── ShellHost ──────────────────────────────────────────────────────────────
|
|
1502
|
-
t(text) {
|
|
1503
|
-
|
|
1504
|
-
|
|
1514
|
+
t(text) {
|
|
1515
|
+
return this.i18n.t(text);
|
|
1516
|
+
}
|
|
1517
|
+
formatCurrency(n, win = false) {
|
|
1518
|
+
return formatCurrency(n, this.config.currency, win);
|
|
1519
|
+
}
|
|
1520
|
+
formatWin(value) {
|
|
1521
|
+
return this.formatCurrency(value, true);
|
|
1522
|
+
}
|
|
1505
1523
|
notifyResize(w, h) {
|
|
1506
1524
|
const layout = w !== 0 && h > w ? 'mobile' : 'wide';
|
|
1507
1525
|
if (layout !== this.layout) {
|
|
@@ -1568,12 +1586,24 @@ class ShellController extends EventEmitter {
|
|
|
1568
1586
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1569
1587
|
const self = this;
|
|
1570
1588
|
const host = {
|
|
1571
|
-
get state() {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
get
|
|
1575
|
-
|
|
1576
|
-
|
|
1589
|
+
get state() {
|
|
1590
|
+
return self.state;
|
|
1591
|
+
},
|
|
1592
|
+
get hotkeysEnabled() {
|
|
1593
|
+
return self.config.features.hotkeys !== false;
|
|
1594
|
+
},
|
|
1595
|
+
get spacebarEnabled() {
|
|
1596
|
+
return self.config.features.spacebar !== false;
|
|
1597
|
+
},
|
|
1598
|
+
get turboLevels() {
|
|
1599
|
+
return self.config.features.turbo;
|
|
1600
|
+
},
|
|
1601
|
+
get autoplayEnabled() {
|
|
1602
|
+
return self.config.features.autoplay != null;
|
|
1603
|
+
},
|
|
1604
|
+
get buyBonusEnabled() {
|
|
1605
|
+
return self.config.features.buyBonus !== false;
|
|
1606
|
+
},
|
|
1577
1607
|
hasOpenLayer: () => self.overlay !== null,
|
|
1578
1608
|
routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
|
|
1579
1609
|
spin: () => self.actions.spin(),
|
|
@@ -1589,33 +1619,59 @@ class ShellController extends EventEmitter {
|
|
|
1589
1619
|
this.kbd = new KeyboardController(host);
|
|
1590
1620
|
this.kbd.attach();
|
|
1591
1621
|
}
|
|
1592
|
-
pullFocus = () => {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1622
|
+
pullFocus = () => {
|
|
1623
|
+
try {
|
|
1624
|
+
globalThis.focus?.();
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
/* cross-origin */
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1596
1630
|
// ── overlay flow ─────────────────────────────────────────────────────────────
|
|
1597
1631
|
show(req) {
|
|
1598
1632
|
this.closeModal();
|
|
1599
1633
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1600
1634
|
}
|
|
1601
|
-
openMenu() {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1635
|
+
openMenu() {
|
|
1636
|
+
this.emit('menuOpen');
|
|
1637
|
+
this.openSettings();
|
|
1638
|
+
}
|
|
1639
|
+
openSettings() {
|
|
1640
|
+
this.emit('settingsOpen');
|
|
1641
|
+
this.show({ kind: 'settings' });
|
|
1642
|
+
}
|
|
1643
|
+
openInfo() {
|
|
1644
|
+
this.emit('infoOpen');
|
|
1645
|
+
this.show({ kind: 'gameInfo' });
|
|
1646
|
+
}
|
|
1647
|
+
openBuyBonus() {
|
|
1648
|
+
if (this.config.onBonusBuy) {
|
|
1649
|
+
this.config.onBonusBuy();
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
this.show({ kind: 'buyBonus' });
|
|
1653
|
+
}
|
|
1654
|
+
openBetPicker() {
|
|
1655
|
+
this.show({ kind: 'betPicker' });
|
|
1656
|
+
}
|
|
1657
|
+
openAutoplayPicker() {
|
|
1658
|
+
this.show({ kind: 'autoplayPicker' });
|
|
1659
|
+
}
|
|
1660
|
+
openReplay(opts) {
|
|
1661
|
+
if (this.destroyed)
|
|
1662
|
+
return;
|
|
1663
|
+
this.show({ kind: 'replay', opts });
|
|
1664
|
+
}
|
|
1665
|
+
openModal(opts) {
|
|
1666
|
+
this.show({ kind: 'modal', opts });
|
|
1667
|
+
}
|
|
1613
1668
|
/** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
|
|
1614
1669
|
closeModal() {
|
|
1615
1670
|
if (!this.overlay)
|
|
1616
1671
|
return;
|
|
1617
1672
|
this.overlay = null;
|
|
1618
1673
|
this.soundRefresh = null;
|
|
1674
|
+
this.volumeRefresh = null;
|
|
1619
1675
|
this.renderer.closeOverlay();
|
|
1620
1676
|
}
|
|
1621
1677
|
// ── sound ──────────────────────────────────────────────────────────────────
|
|
@@ -1625,7 +1681,25 @@ class ShellController extends EventEmitter {
|
|
|
1625
1681
|
this.soundRefresh?.(on);
|
|
1626
1682
|
this.renderer.refreshSoundIcon?.(on);
|
|
1627
1683
|
}
|
|
1628
|
-
setSoundRefresh(fn) {
|
|
1684
|
+
setSoundRefresh(fn) {
|
|
1685
|
+
this.soundRefresh = fn;
|
|
1686
|
+
}
|
|
1687
|
+
// ── volume ─────────────────────────────────────────────────────────────────
|
|
1688
|
+
getVolume(key) {
|
|
1689
|
+
return this.state.volumes[key];
|
|
1690
|
+
}
|
|
1691
|
+
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
1692
|
+
* clamps, stores so a reopened Settings overlay reflects it, emits `settingChange`, and
|
|
1693
|
+
* live-updates the slider if the overlay is currently open. */
|
|
1694
|
+
setVolume(key, value) {
|
|
1695
|
+
const v = Math.max(0, Math.min(1, value));
|
|
1696
|
+
this.state.volumes[key] = v;
|
|
1697
|
+
this.emit('settingChange', { key, value: v });
|
|
1698
|
+
this.volumeRefresh?.(key, v);
|
|
1699
|
+
}
|
|
1700
|
+
setVolumeRefresh(fn) {
|
|
1701
|
+
this.volumeRefresh = fn;
|
|
1702
|
+
}
|
|
1629
1703
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1630
1704
|
activateFeature(bonus) {
|
|
1631
1705
|
this.state.activeFeature = bonus;
|
|
@@ -1646,21 +1720,81 @@ class ShellController extends EventEmitter {
|
|
|
1646
1720
|
if (to !== from)
|
|
1647
1721
|
this.renderer.animateMoney(field, from, to);
|
|
1648
1722
|
}
|
|
1649
|
-
setBalance(n) {
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
this.
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1723
|
+
setBalance(n) {
|
|
1724
|
+
const from = this.prevBalance;
|
|
1725
|
+
this.state.balance = n;
|
|
1726
|
+
this.prevBalance = n;
|
|
1727
|
+
this.money('balance', from, n);
|
|
1728
|
+
}
|
|
1729
|
+
setWin(n) {
|
|
1730
|
+
const from = this.prevWin;
|
|
1731
|
+
this.state.win = n;
|
|
1732
|
+
this.prevWin = n;
|
|
1733
|
+
this.money('win', from, n);
|
|
1734
|
+
}
|
|
1735
|
+
setBet(n) {
|
|
1736
|
+
this.state.bet = n;
|
|
1737
|
+
this.renderer.renderBar();
|
|
1738
|
+
}
|
|
1739
|
+
setMode(mode) {
|
|
1740
|
+
if (mode === 'replay')
|
|
1741
|
+
this.state.replay = true;
|
|
1742
|
+
this.state.mode = mode;
|
|
1743
|
+
this.renderer.renderBar();
|
|
1744
|
+
}
|
|
1745
|
+
setBusy(busy) {
|
|
1746
|
+
this.state.busy = busy;
|
|
1747
|
+
this.renderer.renderBar();
|
|
1748
|
+
this.kbd?.notifyBusyChanged(busy);
|
|
1749
|
+
}
|
|
1750
|
+
setAutoplay(a) {
|
|
1751
|
+
this.state.autoplay = a;
|
|
1752
|
+
this.renderer.renderBar();
|
|
1753
|
+
}
|
|
1754
|
+
setTurbo(level) {
|
|
1755
|
+
this.state.turbo = level;
|
|
1756
|
+
this.renderer.renderBar();
|
|
1757
|
+
}
|
|
1758
|
+
setBuyBonusEnabled(enabled) {
|
|
1759
|
+
this.state.buyBonusEnabled = enabled;
|
|
1760
|
+
this.renderer.renderBar();
|
|
1761
|
+
}
|
|
1762
|
+
setFreeSpins(fs) {
|
|
1763
|
+
this.state.freeSpins = fs;
|
|
1764
|
+
this.state.bonus = null;
|
|
1765
|
+
this.renderer.renderBar();
|
|
1766
|
+
}
|
|
1767
|
+
/** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
|
|
1768
|
+
* override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
|
|
1769
|
+
* `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
|
|
1770
|
+
setBonus(b) {
|
|
1771
|
+
this.state.bonus = { label: b.label, value: b.value };
|
|
1772
|
+
this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
|
|
1773
|
+
this.renderer.renderBar();
|
|
1774
|
+
}
|
|
1775
|
+
setTheme(theme) {
|
|
1776
|
+
this.config.theme = theme;
|
|
1777
|
+
this.tokens = resolveTheme(theme);
|
|
1778
|
+
this.renderer.applyTheme(this.tokens);
|
|
1779
|
+
this.renderer.renderBar();
|
|
1780
|
+
}
|
|
1781
|
+
setLanguage(lang) {
|
|
1782
|
+
this.config.language = lang;
|
|
1783
|
+
this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
|
|
1784
|
+
this.renderer.renderBar();
|
|
1785
|
+
}
|
|
1786
|
+
setSocial(isSocial) {
|
|
1787
|
+
this.config.isSocial = isSocial;
|
|
1788
|
+
this.i18n = createI18n({ language: this.config.language, isSocial });
|
|
1789
|
+
this.renderer.renderBar();
|
|
1790
|
+
}
|
|
1791
|
+
setLayout(layout) {
|
|
1792
|
+
if (layout === this.layout)
|
|
1793
|
+
return;
|
|
1794
|
+
this.layout = layout;
|
|
1795
|
+
this.renderer.setLayout(layout);
|
|
1796
|
+
this.renderer.renderBar();
|
|
1797
|
+
}
|
|
1664
1798
|
destroy() {
|
|
1665
1799
|
if (this.destroyed)
|
|
1666
1800
|
return Promise.resolve();
|
|
@@ -2382,8 +2516,10 @@ function iconBtn(ge, name, onClick, active = false) {
|
|
|
2382
2516
|
b.className = `ge-iconbtn${active ? ' ge-active' : ''}`;
|
|
2383
2517
|
b.dataset.ge = ge;
|
|
2384
2518
|
b.innerHTML = icon(name);
|
|
2385
|
-
b.addEventListener('click', () => {
|
|
2386
|
-
|
|
2519
|
+
b.addEventListener('click', () => {
|
|
2520
|
+
if (!b.disabled)
|
|
2521
|
+
onClick();
|
|
2522
|
+
});
|
|
2387
2523
|
return b;
|
|
2388
2524
|
}
|
|
2389
2525
|
function renderBottomBar(host) {
|
|
@@ -2399,15 +2535,13 @@ function renderBottomBar(host) {
|
|
|
2399
2535
|
// All three modes share the base plaque layout. FS/replay hide the controls that don't apply
|
|
2400
2536
|
// and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
|
|
2401
2537
|
const isBase = state.mode === 'base';
|
|
2402
|
-
const isFS = state.mode === 'freeSpins';
|
|
2538
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
2403
2539
|
// FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
|
|
2404
2540
|
// only when it's a free-spins replay (freeSpins.total > 0).
|
|
2405
2541
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
2406
2542
|
// Replay is a read-only historical round — there's no real balance to show, so hide it. Keyed on
|
|
2407
2543
|
// 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));
|
|
2544
|
+
const balance = state.replay ? null : readout('balance', host.t('Balance'), fmt(state.balance));
|
|
2411
2545
|
// With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
|
|
2412
2546
|
// the feature accent; the base state.bet is unchanged and returns once the feature is off.
|
|
2413
2547
|
const feature = state.activeFeature;
|
|
@@ -2430,21 +2564,25 @@ function renderBottomBar(host) {
|
|
|
2430
2564
|
betDown = iconBtn('bet-down', 'minus', () => host.actions.stepBet(-1));
|
|
2431
2565
|
betUp = iconBtn('bet-up', 'plus', () => host.actions.stepBet(1));
|
|
2432
2566
|
betValue.classList.add('ge-betbtn'); // tap the stake → bet picker
|
|
2433
|
-
betValue.addEventListener('click', () => {
|
|
2434
|
-
host
|
|
2567
|
+
betValue.addEventListener('click', () => {
|
|
2568
|
+
if (!betLocked(host))
|
|
2569
|
+
host.actions.openBetPicker();
|
|
2570
|
+
});
|
|
2435
2571
|
spin = spinButton(host);
|
|
2436
2572
|
auto = config.features.autoplay ? autoButton(host) : null;
|
|
2437
|
-
buy =
|
|
2573
|
+
buy = config.features.buyBonus !== false || config.onBonusBuy ? buyBtn(host) : null;
|
|
2438
2574
|
}
|
|
2439
2575
|
const winEl = state.win > 0 ? readout('win', host.t('Win'), fmtWin(state.win)) : null;
|
|
2440
2576
|
// FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
|
|
2441
2577
|
// current = number → "current / total"; current = null/undefined → just the (game-driven) total.
|
|
2442
2578
|
const fs = state.freeSpins;
|
|
2443
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
2579
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
2444
2580
|
// In FS the spins counter takes the SPIN slot as a rectangular hero plaque (same white/black-ring
|
|
2445
2581
|
// style as the SPIN disc); Total Win stays an inline readout.
|
|
2446
|
-
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText) : null;
|
|
2447
|
-
const fsTotalWin = showFsBlocks
|
|
2582
|
+
const fsHero = showFsBlocks ? fsHeroPlaque(host, fsText, state.bonus?.label) : null;
|
|
2583
|
+
const fsTotalWin = showFsBlocks
|
|
2584
|
+
? readout('fs-totalwin', host.t('Total win'), fmtWin(fs.totalWin))
|
|
2585
|
+
: null;
|
|
2448
2586
|
// The hero in the centre/spin position: SPIN in base, the FS counter in free spins, nothing else.
|
|
2449
2587
|
const hero = isBase ? spin : fsHero;
|
|
2450
2588
|
if (mobile) {
|
|
@@ -2487,13 +2625,13 @@ function renderBottomBar(host) {
|
|
|
2487
2625
|
}
|
|
2488
2626
|
/** Free-spins hero plaque — takes the SPIN slot in FS: same white disc/black-ring language as SPIN,
|
|
2489
2627
|
* but a rounded RECTANGLE showing the spins counter ("3 / 10"). */
|
|
2490
|
-
function fsHeroPlaque(host, text) {
|
|
2628
|
+
function fsHeroPlaque(host, text, label) {
|
|
2491
2629
|
const el = document.createElement('div');
|
|
2492
2630
|
el.className = 'ge-fs-hero';
|
|
2493
2631
|
el.dataset.ge = 'fs-counter';
|
|
2494
2632
|
const lbl = document.createElement('span');
|
|
2495
2633
|
lbl.className = 'ge-fs-lbl';
|
|
2496
|
-
lbl.textContent = host.t('Free spins');
|
|
2634
|
+
lbl.textContent = host.t(label ?? 'Free spins');
|
|
2497
2635
|
const num = document.createElement('span');
|
|
2498
2636
|
num.className = 'ge-fs-num';
|
|
2499
2637
|
num.textContent = text;
|
|
@@ -2513,7 +2651,9 @@ function plaque(cls, children) {
|
|
|
2513
2651
|
d.append(...children);
|
|
2514
2652
|
return d;
|
|
2515
2653
|
}
|
|
2516
|
-
function compact(items) {
|
|
2654
|
+
function compact(items) {
|
|
2655
|
+
return items.filter((x) => x !== null);
|
|
2656
|
+
}
|
|
2517
2657
|
function buyBtn(host) {
|
|
2518
2658
|
const buy = document.createElement('button');
|
|
2519
2659
|
buy.className = 'ge-shell-buybonus';
|
|
@@ -2526,15 +2666,19 @@ function buyBtn(host) {
|
|
|
2526
2666
|
buy.innerHTML = `<span>${host.t('DISABLE')}</span>`;
|
|
2527
2667
|
buy.style.background = accent;
|
|
2528
2668
|
buy.style.color = contrastText(accent);
|
|
2529
|
-
buy.addEventListener('click', () => {
|
|
2530
|
-
|
|
2669
|
+
buy.addEventListener('click', () => {
|
|
2670
|
+
if (!buy.disabled)
|
|
2671
|
+
host.actions.deactivateFeature();
|
|
2672
|
+
});
|
|
2531
2673
|
}
|
|
2532
2674
|
else {
|
|
2533
2675
|
// Ticket icon (no text); keep the label for screen readers.
|
|
2534
2676
|
buy.setAttribute('aria-label', host.t('BUY BONUS'));
|
|
2535
2677
|
buy.innerHTML = `<span class="ge-bb-tk">${icon('ticket')}</span>`;
|
|
2536
|
-
buy.addEventListener('click', () => {
|
|
2537
|
-
|
|
2678
|
+
buy.addEventListener('click', () => {
|
|
2679
|
+
if (!buy.disabled)
|
|
2680
|
+
host.actions.openBuyBonus();
|
|
2681
|
+
});
|
|
2538
2682
|
}
|
|
2539
2683
|
return buy;
|
|
2540
2684
|
}
|
|
@@ -2552,15 +2696,19 @@ function spinButton(host) {
|
|
|
2552
2696
|
const rem = state.autoplay.remaining;
|
|
2553
2697
|
const label = Number.isFinite(rem) ? String(rem) : '∞';
|
|
2554
2698
|
sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
|
|
2555
|
-
sp.addEventListener('click', () => {
|
|
2556
|
-
|
|
2699
|
+
sp.addEventListener('click', () => {
|
|
2700
|
+
if (!sp.disabled)
|
|
2701
|
+
host.actions.stopAutoplay();
|
|
2702
|
+
});
|
|
2557
2703
|
}
|
|
2558
2704
|
else {
|
|
2559
2705
|
sp.innerHTML = icon('spin');
|
|
2560
2706
|
if (state.busy)
|
|
2561
2707
|
sp.classList.add('ge-spinning');
|
|
2562
|
-
sp.addEventListener('click', () => {
|
|
2563
|
-
|
|
2708
|
+
sp.addEventListener('click', () => {
|
|
2709
|
+
if (!sp.disabled)
|
|
2710
|
+
host.actions.spin();
|
|
2711
|
+
});
|
|
2564
2712
|
}
|
|
2565
2713
|
return sp;
|
|
2566
2714
|
}
|
|
@@ -2709,7 +2857,10 @@ function openSettingsModal(host) {
|
|
|
2709
2857
|
return row;
|
|
2710
2858
|
})();
|
|
2711
2859
|
body.appendChild(sound);
|
|
2712
|
-
// Volume sliders — full-width column rows with a live value readout
|
|
2860
|
+
// Volume sliders — full-width column rows with a live value readout. Positions are read from the
|
|
2861
|
+
// shell's stored volumes (not hardcoded to 100%), so reopening the overlay reflects the last set
|
|
2862
|
+
// value, and `host.setVolume()` from game code updates them live via the registered refreshers.
|
|
2863
|
+
const updaters = {};
|
|
2713
2864
|
const slider = (key, label) => {
|
|
2714
2865
|
const row = document.createElement('div');
|
|
2715
2866
|
row.className = 'ge-ov-row ge-col';
|
|
@@ -2717,7 +2868,6 @@ function openSettingsModal(host) {
|
|
|
2717
2868
|
head.className = 'ge-row-head';
|
|
2718
2869
|
const val = document.createElement('span');
|
|
2719
2870
|
val.className = 'ge-val';
|
|
2720
|
-
val.textContent = '100%';
|
|
2721
2871
|
head.innerHTML = `<span>${label}</span>`;
|
|
2722
2872
|
head.appendChild(val);
|
|
2723
2873
|
const input = document.createElement('input');
|
|
@@ -2725,19 +2875,23 @@ function openSettingsModal(host) {
|
|
|
2725
2875
|
input.min = '0';
|
|
2726
2876
|
input.max = '1';
|
|
2727
2877
|
input.step = '0.05';
|
|
2728
|
-
input.value = '1';
|
|
2729
2878
|
input.className = 'ge-slider';
|
|
2730
2879
|
input.dataset.ge = `setting-${key}`;
|
|
2880
|
+
const paint = (v) => { input.value = String(v); val.textContent = `${Math.round(v * 100)}%`; };
|
|
2881
|
+
paint(host.getVolume(key));
|
|
2731
2882
|
input.addEventListener('input', () => {
|
|
2732
2883
|
val.textContent = `${Math.round(Number(input.value) * 100)}%`;
|
|
2733
|
-
host.
|
|
2884
|
+
host.setVolume(key, Number(input.value));
|
|
2734
2885
|
});
|
|
2886
|
+
updaters[key] = paint;
|
|
2735
2887
|
row.append(head, input);
|
|
2736
2888
|
return row;
|
|
2737
2889
|
};
|
|
2738
2890
|
body.appendChild(slider('master', host.t('Master volume')));
|
|
2739
2891
|
body.appendChild(slider('music', host.t('Music')));
|
|
2740
2892
|
body.appendChild(slider('sfx', host.t('SFX')));
|
|
2893
|
+
// Live-update sliders when volume changes via host.setVolume (shell clears on close).
|
|
2894
|
+
host.setVolumeRefresh((key, v) => updaters[key]?.(v));
|
|
2741
2895
|
// Game info — full-width row button that opens its own overlay
|
|
2742
2896
|
const gameInfo = document.createElement('button');
|
|
2743
2897
|
gameInfo.className = 'ge-ov-row';
|