@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/pixi.cjs.js
CHANGED
|
@@ -66,9 +66,19 @@ function createInitialState(config) {
|
|
|
66
66
|
turbo: 0,
|
|
67
67
|
buyBonusEnabled: true,
|
|
68
68
|
freeSpins: { current: 0, total: 0, totalWin: 0 },
|
|
69
|
+
bonus: null,
|
|
69
70
|
activeFeature: null,
|
|
71
|
+
volumes: {
|
|
72
|
+
master: clampVolume(config.volumes?.master),
|
|
73
|
+
music: clampVolume(config.volumes?.music),
|
|
74
|
+
sfx: clampVolume(config.volumes?.sfx),
|
|
75
|
+
},
|
|
70
76
|
};
|
|
71
77
|
}
|
|
78
|
+
/** Clamp a configured volume to 0..1, defaulting to full (1) when unset/invalid. */
|
|
79
|
+
function clampVolume(v) {
|
|
80
|
+
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
|
81
|
+
}
|
|
72
82
|
/** Step bet up/down within availableBets, clamped at the ends. */
|
|
73
83
|
function stepBet(state, direction) {
|
|
74
84
|
const idx = state.availableBets.indexOf(state.bet);
|
|
@@ -1439,7 +1449,7 @@ class KeyboardController {
|
|
|
1439
1449
|
|
|
1440
1450
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1441
1451
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1442
|
-
const PACKAGE_VERSION = '0.
|
|
1452
|
+
const PACKAGE_VERSION = '0.6.0';
|
|
1443
1453
|
|
|
1444
1454
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1445
1455
|
function resolveConfig(config) {
|
|
@@ -1456,6 +1466,7 @@ function resolveConfig(config) {
|
|
|
1456
1466
|
features: config.features,
|
|
1457
1467
|
theme: config.theme,
|
|
1458
1468
|
onBonusBuy: config.onBonusBuy,
|
|
1469
|
+
volumes: config.volumes,
|
|
1459
1470
|
version: config.version ?? '1.0.0',
|
|
1460
1471
|
isSocial: config.isSocial ?? false,
|
|
1461
1472
|
replay: config.replay ?? config.mode === 'replay',
|
|
@@ -1477,6 +1488,7 @@ class ShellController extends EventEmitter {
|
|
|
1477
1488
|
kbd;
|
|
1478
1489
|
overlay = null;
|
|
1479
1490
|
soundRefresh = null;
|
|
1491
|
+
volumeRefresh = null;
|
|
1480
1492
|
prevBalance;
|
|
1481
1493
|
prevWin;
|
|
1482
1494
|
destroyed = false;
|
|
@@ -1500,9 +1512,15 @@ class ShellController extends EventEmitter {
|
|
|
1500
1512
|
this.renderer.renderBar();
|
|
1501
1513
|
}
|
|
1502
1514
|
// ── ShellHost ──────────────────────────────────────────────────────────────
|
|
1503
|
-
t(text) {
|
|
1504
|
-
|
|
1505
|
-
|
|
1515
|
+
t(text) {
|
|
1516
|
+
return this.i18n.t(text);
|
|
1517
|
+
}
|
|
1518
|
+
formatCurrency(n, win = false) {
|
|
1519
|
+
return formatCurrency(n, this.config.currency, win);
|
|
1520
|
+
}
|
|
1521
|
+
formatWin(value) {
|
|
1522
|
+
return this.formatCurrency(value, true);
|
|
1523
|
+
}
|
|
1506
1524
|
notifyResize(w, h) {
|
|
1507
1525
|
const layout = w !== 0 && h > w ? 'mobile' : 'wide';
|
|
1508
1526
|
if (layout !== this.layout) {
|
|
@@ -1569,12 +1587,24 @@ class ShellController extends EventEmitter {
|
|
|
1569
1587
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1570
1588
|
const self = this;
|
|
1571
1589
|
const host = {
|
|
1572
|
-
get state() {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
get
|
|
1576
|
-
|
|
1577
|
-
|
|
1590
|
+
get state() {
|
|
1591
|
+
return self.state;
|
|
1592
|
+
},
|
|
1593
|
+
get hotkeysEnabled() {
|
|
1594
|
+
return self.config.features.hotkeys !== false;
|
|
1595
|
+
},
|
|
1596
|
+
get spacebarEnabled() {
|
|
1597
|
+
return self.config.features.spacebar !== false;
|
|
1598
|
+
},
|
|
1599
|
+
get turboLevels() {
|
|
1600
|
+
return self.config.features.turbo;
|
|
1601
|
+
},
|
|
1602
|
+
get autoplayEnabled() {
|
|
1603
|
+
return self.config.features.autoplay != null;
|
|
1604
|
+
},
|
|
1605
|
+
get buyBonusEnabled() {
|
|
1606
|
+
return self.config.features.buyBonus !== false;
|
|
1607
|
+
},
|
|
1578
1608
|
hasOpenLayer: () => self.overlay !== null,
|
|
1579
1609
|
routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
|
|
1580
1610
|
spin: () => self.actions.spin(),
|
|
@@ -1590,33 +1620,59 @@ class ShellController extends EventEmitter {
|
|
|
1590
1620
|
this.kbd = new KeyboardController(host);
|
|
1591
1621
|
this.kbd.attach();
|
|
1592
1622
|
}
|
|
1593
|
-
pullFocus = () => {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1623
|
+
pullFocus = () => {
|
|
1624
|
+
try {
|
|
1625
|
+
globalThis.focus?.();
|
|
1626
|
+
}
|
|
1627
|
+
catch {
|
|
1628
|
+
/* cross-origin */
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1597
1631
|
// ── overlay flow ─────────────────────────────────────────────────────────────
|
|
1598
1632
|
show(req) {
|
|
1599
1633
|
this.closeModal();
|
|
1600
1634
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1601
1635
|
}
|
|
1602
|
-
openMenu() {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1636
|
+
openMenu() {
|
|
1637
|
+
this.emit('menuOpen');
|
|
1638
|
+
this.openSettings();
|
|
1639
|
+
}
|
|
1640
|
+
openSettings() {
|
|
1641
|
+
this.emit('settingsOpen');
|
|
1642
|
+
this.show({ kind: 'settings' });
|
|
1643
|
+
}
|
|
1644
|
+
openInfo() {
|
|
1645
|
+
this.emit('infoOpen');
|
|
1646
|
+
this.show({ kind: 'gameInfo' });
|
|
1647
|
+
}
|
|
1648
|
+
openBuyBonus() {
|
|
1649
|
+
if (this.config.onBonusBuy) {
|
|
1650
|
+
this.config.onBonusBuy();
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
this.show({ kind: 'buyBonus' });
|
|
1654
|
+
}
|
|
1655
|
+
openBetPicker() {
|
|
1656
|
+
this.show({ kind: 'betPicker' });
|
|
1657
|
+
}
|
|
1658
|
+
openAutoplayPicker() {
|
|
1659
|
+
this.show({ kind: 'autoplayPicker' });
|
|
1660
|
+
}
|
|
1661
|
+
openReplay(opts) {
|
|
1662
|
+
if (this.destroyed)
|
|
1663
|
+
return;
|
|
1664
|
+
this.show({ kind: 'replay', opts });
|
|
1665
|
+
}
|
|
1666
|
+
openModal(opts) {
|
|
1667
|
+
this.show({ kind: 'modal', opts });
|
|
1668
|
+
}
|
|
1614
1669
|
/** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
|
|
1615
1670
|
closeModal() {
|
|
1616
1671
|
if (!this.overlay)
|
|
1617
1672
|
return;
|
|
1618
1673
|
this.overlay = null;
|
|
1619
1674
|
this.soundRefresh = null;
|
|
1675
|
+
this.volumeRefresh = null;
|
|
1620
1676
|
this.renderer.closeOverlay();
|
|
1621
1677
|
}
|
|
1622
1678
|
// ── sound ──────────────────────────────────────────────────────────────────
|
|
@@ -1626,7 +1682,25 @@ class ShellController extends EventEmitter {
|
|
|
1626
1682
|
this.soundRefresh?.(on);
|
|
1627
1683
|
this.renderer.refreshSoundIcon?.(on);
|
|
1628
1684
|
}
|
|
1629
|
-
setSoundRefresh(fn) {
|
|
1685
|
+
setSoundRefresh(fn) {
|
|
1686
|
+
this.soundRefresh = fn;
|
|
1687
|
+
}
|
|
1688
|
+
// ── volume ─────────────────────────────────────────────────────────────────
|
|
1689
|
+
getVolume(key) {
|
|
1690
|
+
return this.state.volumes[key];
|
|
1691
|
+
}
|
|
1692
|
+
/** Set a volume slider (0..1). Shared by the slider control (drag) and game code (public API):
|
|
1693
|
+
* clamps, stores so a reopened Settings overlay reflects it, emits `settingChange`, and
|
|
1694
|
+
* live-updates the slider if the overlay is currently open. */
|
|
1695
|
+
setVolume(key, value) {
|
|
1696
|
+
const v = Math.max(0, Math.min(1, value));
|
|
1697
|
+
this.state.volumes[key] = v;
|
|
1698
|
+
this.emit('settingChange', { key, value: v });
|
|
1699
|
+
this.volumeRefresh?.(key, v);
|
|
1700
|
+
}
|
|
1701
|
+
setVolumeRefresh(fn) {
|
|
1702
|
+
this.volumeRefresh = fn;
|
|
1703
|
+
}
|
|
1630
1704
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1631
1705
|
activateFeature(bonus) {
|
|
1632
1706
|
this.state.activeFeature = bonus;
|
|
@@ -1647,21 +1721,81 @@ class ShellController extends EventEmitter {
|
|
|
1647
1721
|
if (to !== from)
|
|
1648
1722
|
this.renderer.animateMoney(field, from, to);
|
|
1649
1723
|
}
|
|
1650
|
-
setBalance(n) {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
this.
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1724
|
+
setBalance(n) {
|
|
1725
|
+
const from = this.prevBalance;
|
|
1726
|
+
this.state.balance = n;
|
|
1727
|
+
this.prevBalance = n;
|
|
1728
|
+
this.money('balance', from, n);
|
|
1729
|
+
}
|
|
1730
|
+
setWin(n) {
|
|
1731
|
+
const from = this.prevWin;
|
|
1732
|
+
this.state.win = n;
|
|
1733
|
+
this.prevWin = n;
|
|
1734
|
+
this.money('win', from, n);
|
|
1735
|
+
}
|
|
1736
|
+
setBet(n) {
|
|
1737
|
+
this.state.bet = n;
|
|
1738
|
+
this.renderer.renderBar();
|
|
1739
|
+
}
|
|
1740
|
+
setMode(mode) {
|
|
1741
|
+
if (mode === 'replay')
|
|
1742
|
+
this.state.replay = true;
|
|
1743
|
+
this.state.mode = mode;
|
|
1744
|
+
this.renderer.renderBar();
|
|
1745
|
+
}
|
|
1746
|
+
setBusy(busy) {
|
|
1747
|
+
this.state.busy = busy;
|
|
1748
|
+
this.renderer.renderBar();
|
|
1749
|
+
this.kbd?.notifyBusyChanged(busy);
|
|
1750
|
+
}
|
|
1751
|
+
setAutoplay(a) {
|
|
1752
|
+
this.state.autoplay = a;
|
|
1753
|
+
this.renderer.renderBar();
|
|
1754
|
+
}
|
|
1755
|
+
setTurbo(level) {
|
|
1756
|
+
this.state.turbo = level;
|
|
1757
|
+
this.renderer.renderBar();
|
|
1758
|
+
}
|
|
1759
|
+
setBuyBonusEnabled(enabled) {
|
|
1760
|
+
this.state.buyBonusEnabled = enabled;
|
|
1761
|
+
this.renderer.renderBar();
|
|
1762
|
+
}
|
|
1763
|
+
setFreeSpins(fs) {
|
|
1764
|
+
this.state.freeSpins = fs;
|
|
1765
|
+
this.state.bonus = null;
|
|
1766
|
+
this.renderer.renderBar();
|
|
1767
|
+
}
|
|
1768
|
+
/** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
|
|
1769
|
+
* override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
|
|
1770
|
+
* `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
|
|
1771
|
+
setBonus(b) {
|
|
1772
|
+
this.state.bonus = { label: b.label, value: b.value };
|
|
1773
|
+
this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
|
|
1774
|
+
this.renderer.renderBar();
|
|
1775
|
+
}
|
|
1776
|
+
setTheme(theme) {
|
|
1777
|
+
this.config.theme = theme;
|
|
1778
|
+
this.tokens = resolveTheme(theme);
|
|
1779
|
+
this.renderer.applyTheme(this.tokens);
|
|
1780
|
+
this.renderer.renderBar();
|
|
1781
|
+
}
|
|
1782
|
+
setLanguage(lang) {
|
|
1783
|
+
this.config.language = lang;
|
|
1784
|
+
this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
|
|
1785
|
+
this.renderer.renderBar();
|
|
1786
|
+
}
|
|
1787
|
+
setSocial(isSocial) {
|
|
1788
|
+
this.config.isSocial = isSocial;
|
|
1789
|
+
this.i18n = createI18n({ language: this.config.language, isSocial });
|
|
1790
|
+
this.renderer.renderBar();
|
|
1791
|
+
}
|
|
1792
|
+
setLayout(layout) {
|
|
1793
|
+
if (layout === this.layout)
|
|
1794
|
+
return;
|
|
1795
|
+
this.layout = layout;
|
|
1796
|
+
this.renderer.setLayout(layout);
|
|
1797
|
+
this.renderer.renderBar();
|
|
1798
|
+
}
|
|
1665
1799
|
destroy() {
|
|
1666
1800
|
if (this.destroyed)
|
|
1667
1801
|
return Promise.resolve();
|
|
@@ -2924,8 +3058,12 @@ class TurboButton extends pixi_js.Container {
|
|
|
2924
3058
|
this.accent = accent;
|
|
2925
3059
|
this._paint(false);
|
|
2926
3060
|
}
|
|
2927
|
-
measureSize() {
|
|
2928
|
-
|
|
3061
|
+
measureSize() {
|
|
3062
|
+
return { w: this.box, h: this.box };
|
|
3063
|
+
}
|
|
3064
|
+
setLayoutSize() {
|
|
3065
|
+
/* fixed */
|
|
3066
|
+
}
|
|
2929
3067
|
}
|
|
2930
3068
|
/** A readout in the bar (white Oswald value, plaque-label caption, no shadow). */
|
|
2931
3069
|
function readout(host, label, value, opts = {}) {
|
|
@@ -2973,13 +3111,19 @@ class BottomBar extends pixi_js.Container {
|
|
|
2973
3111
|
buildWide() {
|
|
2974
3112
|
const { state, tokens } = this.host;
|
|
2975
3113
|
const isBase = state.mode === 'base';
|
|
2976
|
-
const isFS = state.mode === 'freeSpins';
|
|
3114
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
2977
3115
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
2978
3116
|
// BUY BONUS — floats outside-left of the panel (base only)
|
|
2979
|
-
this.buy = isBase ? this.buildBuy(BUY_W, 13, 3) ?? undefined : undefined;
|
|
3117
|
+
this.buy = isBase ? (this.buildBuy(BUY_W, 13, 3) ?? undefined) : undefined;
|
|
2980
3118
|
// LEFT info group: menu · balance · (Total win) · (Win)
|
|
2981
3119
|
const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
|
|
2982
|
-
left.add(new IconButton('menu', {
|
|
3120
|
+
left.add(new IconButton('menu', {
|
|
3121
|
+
size: 36,
|
|
3122
|
+
glyph: 30,
|
|
3123
|
+
color: '#ffffff',
|
|
3124
|
+
hover: tokens.accent,
|
|
3125
|
+
onTap: () => this.host.actions.openMenu(),
|
|
3126
|
+
}));
|
|
2983
3127
|
if (!state.replay) {
|
|
2984
3128
|
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
|
|
2985
3129
|
this.balanceValue = bal.valueText;
|
|
@@ -3018,16 +3162,30 @@ class BottomBar extends pixi_js.Container {
|
|
|
3018
3162
|
if (isBase) {
|
|
3019
3163
|
bet.eventMode = 'static';
|
|
3020
3164
|
bet.cursor = 'pointer';
|
|
3021
|
-
bet.on('pointertap', () => {
|
|
3022
|
-
this.
|
|
3165
|
+
bet.on('pointertap', () => {
|
|
3166
|
+
if (!this.betLocked())
|
|
3167
|
+
this.host.actions.openBetPicker();
|
|
3168
|
+
});
|
|
3023
3169
|
}
|
|
3024
3170
|
const betGroup = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
|
|
3025
3171
|
betGroup.add(bet);
|
|
3026
3172
|
if (isBase) {
|
|
3027
3173
|
// plain (borderless) +/- icons, bolder — stacked
|
|
3028
3174
|
const step = new FlexBox({ direction: 'column', align: 'center', gap: 2 });
|
|
3029
|
-
this.betUp = new IconButton('plus', {
|
|
3030
|
-
|
|
3175
|
+
this.betUp = new IconButton('plus', {
|
|
3176
|
+
size: 24,
|
|
3177
|
+
glyph: 22,
|
|
3178
|
+
color: '#ffffff',
|
|
3179
|
+
hover: tokens.accent,
|
|
3180
|
+
onTap: () => this.onBet(1),
|
|
3181
|
+
});
|
|
3182
|
+
this.betDown = new IconButton('minus', {
|
|
3183
|
+
size: 24,
|
|
3184
|
+
glyph: 22,
|
|
3185
|
+
color: '#ffffff',
|
|
3186
|
+
hover: tokens.accent,
|
|
3187
|
+
onTap: () => this.onBet(-1),
|
|
3188
|
+
});
|
|
3031
3189
|
step.add(this.betUp);
|
|
3032
3190
|
step.add(this.betDown);
|
|
3033
3191
|
betGroup.add(step);
|
|
@@ -3035,15 +3193,28 @@ class BottomBar extends pixi_js.Container {
|
|
|
3035
3193
|
const spinWrap = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
|
|
3036
3194
|
if (isBase && config.features.autoplay) {
|
|
3037
3195
|
this.autoBtn = new IconButton('autoplay', {
|
|
3038
|
-
size: DISC,
|
|
3039
|
-
|
|
3196
|
+
size: DISC,
|
|
3197
|
+
glyph: 25,
|
|
3198
|
+
disc: tokens.btn,
|
|
3199
|
+
color: tokens.btnInk,
|
|
3200
|
+
hover: tokens.accent,
|
|
3201
|
+
activeColor: tokens.accent,
|
|
3202
|
+
active: state.autoplay.active,
|
|
3203
|
+
onTap: () => this.onAutoplay(),
|
|
3040
3204
|
});
|
|
3041
3205
|
if (state.autoplay.active)
|
|
3042
3206
|
this.autoBtn.setGlow(true);
|
|
3043
3207
|
spinWrap.add(this.autoBtn);
|
|
3044
3208
|
}
|
|
3045
3209
|
if (isBase) {
|
|
3046
|
-
this.spin = new SpinDisc({
|
|
3210
|
+
this.spin = new SpinDisc({
|
|
3211
|
+
size: SPIN,
|
|
3212
|
+
glyph: 65,
|
|
3213
|
+
tokens,
|
|
3214
|
+
ticker: this.host.ticker,
|
|
3215
|
+
onSpin: () => this.host.actions.spin(),
|
|
3216
|
+
onStop: () => this.stopAutoplay(),
|
|
3217
|
+
});
|
|
3047
3218
|
if (state.autoplay.active)
|
|
3048
3219
|
this.spin.setAutoplay(true, state.autoplay.remaining);
|
|
3049
3220
|
if (state.busy)
|
|
@@ -3052,13 +3223,23 @@ class BottomBar extends pixi_js.Container {
|
|
|
3052
3223
|
}
|
|
3053
3224
|
else if (showFsBlocks) {
|
|
3054
3225
|
const fs = state.freeSpins;
|
|
3055
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
3056
|
-
spinWrap.add(new FsHero({
|
|
3226
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
3227
|
+
spinWrap.add(new FsHero({
|
|
3228
|
+
label: this.host.t(state.bonus?.label ?? 'Free spins'),
|
|
3229
|
+
value: fsText,
|
|
3230
|
+
tokens,
|
|
3231
|
+
height: SPIN,
|
|
3232
|
+
}));
|
|
3057
3233
|
}
|
|
3058
3234
|
if (config.features.turbo > 0) {
|
|
3059
3235
|
this.turboBtn = new TurboButton({
|
|
3060
|
-
size: DISC,
|
|
3061
|
-
|
|
3236
|
+
size: DISC,
|
|
3237
|
+
glyph: 19,
|
|
3238
|
+
discFill: tokens.btn,
|
|
3239
|
+
discBorder: 2,
|
|
3240
|
+
accent: tokens.accent,
|
|
3241
|
+
level: state.turbo,
|
|
3242
|
+
onTap: () => this.onTurbo(),
|
|
3062
3243
|
});
|
|
3063
3244
|
spinWrap.add(this.turboBtn);
|
|
3064
3245
|
}
|
|
@@ -3076,21 +3257,49 @@ class BottomBar extends pixi_js.Container {
|
|
|
3076
3257
|
const feature = state.activeFeature;
|
|
3077
3258
|
if (feature) {
|
|
3078
3259
|
const accent = effectiveAccent(feature);
|
|
3079
|
-
return new BuyBonusBadge({
|
|
3260
|
+
return new BuyBonusBadge({
|
|
3261
|
+
size,
|
|
3262
|
+
fontSize,
|
|
3263
|
+
border,
|
|
3264
|
+
bg: accent,
|
|
3265
|
+
fg: contrastText(accent),
|
|
3266
|
+
label: this.host.t('DISABLE'),
|
|
3267
|
+
tokens,
|
|
3268
|
+
ticker: this.host.ticker,
|
|
3269
|
+
onTap: () => this.host.actions.deactivateFeature(),
|
|
3270
|
+
});
|
|
3080
3271
|
}
|
|
3081
|
-
return new BuyBonusBadge({
|
|
3272
|
+
return new BuyBonusBadge({
|
|
3273
|
+
size,
|
|
3274
|
+
border,
|
|
3275
|
+
bg: tokens.accent,
|
|
3276
|
+
icon: 'ticket',
|
|
3277
|
+
iconSize: size * 0.55,
|
|
3278
|
+
iconColor: tokens.btnInk,
|
|
3279
|
+
label: '',
|
|
3280
|
+
tokens,
|
|
3281
|
+
ticker: this.host.ticker,
|
|
3282
|
+
onTap: () => this.host.actions.openBuyBonus(),
|
|
3283
|
+
});
|
|
3082
3284
|
}
|
|
3083
3285
|
// ── mobile / portrait ─────────────────────────────────────────────────────
|
|
3084
3286
|
buildMobile() {
|
|
3085
3287
|
const { state, config, tokens } = this.host;
|
|
3086
3288
|
const isBase = state.mode === 'base';
|
|
3087
|
-
const isFS = state.mode === 'freeSpins';
|
|
3289
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
3088
3290
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
3089
3291
|
const W = this.host.screenW - 2 * M_SIDE;
|
|
3090
3292
|
// hero (centre): SPIN in base, FS counter in free spins
|
|
3091
3293
|
let hero;
|
|
3092
3294
|
if (isBase) {
|
|
3093
|
-
this.spin = new SpinDisc({
|
|
3295
|
+
this.spin = new SpinDisc({
|
|
3296
|
+
size: SPIN,
|
|
3297
|
+
glyph: 65,
|
|
3298
|
+
tokens,
|
|
3299
|
+
ticker: this.host.ticker,
|
|
3300
|
+
onSpin: () => this.host.actions.spin(),
|
|
3301
|
+
onStop: () => this.stopAutoplay(),
|
|
3302
|
+
});
|
|
3094
3303
|
if (state.autoplay.active)
|
|
3095
3304
|
this.spin.setAutoplay(true, state.autoplay.remaining);
|
|
3096
3305
|
if (state.busy)
|
|
@@ -3099,25 +3308,58 @@ class BottomBar extends pixi_js.Container {
|
|
|
3099
3308
|
}
|
|
3100
3309
|
else if (showFsBlocks) {
|
|
3101
3310
|
const fs = state.freeSpins;
|
|
3102
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
3103
|
-
hero = new FsHero({
|
|
3311
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
3312
|
+
hero = new FsHero({
|
|
3313
|
+
label: this.host.t(state.bonus?.label ?? 'Free spins'),
|
|
3314
|
+
value: fsText,
|
|
3315
|
+
tokens,
|
|
3316
|
+
height: SPIN,
|
|
3317
|
+
});
|
|
3104
3318
|
}
|
|
3105
|
-
const menu = new IconButton('menu', {
|
|
3319
|
+
const menu = new IconButton('menu', {
|
|
3320
|
+
size: 40,
|
|
3321
|
+
glyph: 26,
|
|
3322
|
+
color: '#ffffff',
|
|
3323
|
+
hover: tokens.accent,
|
|
3324
|
+
onTap: () => this.host.actions.openMenu(),
|
|
3325
|
+
});
|
|
3106
3326
|
// autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
|
|
3107
3327
|
if (isBase && config.features.autoplay) {
|
|
3108
|
-
this.autoBtn = new IconButton('autoplay', {
|
|
3328
|
+
this.autoBtn = new IconButton('autoplay', {
|
|
3329
|
+
size: 40,
|
|
3330
|
+
glyph: 26,
|
|
3331
|
+
color: '#ffffff',
|
|
3332
|
+
hover: tokens.accent,
|
|
3333
|
+
activeColor: tokens.accent,
|
|
3334
|
+
active: state.autoplay.active,
|
|
3335
|
+
onTap: () => this.onAutoplay(),
|
|
3336
|
+
});
|
|
3109
3337
|
if (state.autoplay.active)
|
|
3110
3338
|
this.autoBtn.setGlow(true);
|
|
3111
3339
|
}
|
|
3112
3340
|
if (config.features.turbo > 0) {
|
|
3113
3341
|
this.turboBtn = new TurboButton({
|
|
3114
|
-
size: 40,
|
|
3115
|
-
|
|
3342
|
+
size: 40,
|
|
3343
|
+
glyph: 22,
|
|
3344
|
+
discFill: tokens.btn,
|
|
3345
|
+
discBorder: 2,
|
|
3346
|
+
accent: tokens.accent,
|
|
3347
|
+
level: state.turbo,
|
|
3348
|
+
onTap: () => this.onTurbo(),
|
|
3116
3349
|
});
|
|
3117
3350
|
}
|
|
3118
|
-
const buy = isBase ? this.buildBuy(M_BUY, 9, 2) ?? undefined : undefined;
|
|
3351
|
+
const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
|
|
3119
3352
|
// level 1 — controls bar (dark)
|
|
3120
|
-
const controls = new FlexBox({
|
|
3353
|
+
const controls = new FlexBox({
|
|
3354
|
+
direction: 'row',
|
|
3355
|
+
align: 'center',
|
|
3356
|
+
justify: 'space-between',
|
|
3357
|
+
gap: 0,
|
|
3358
|
+
width: W,
|
|
3359
|
+
height: M_CTRL_H,
|
|
3360
|
+
padding: { left: 18, right: 18 },
|
|
3361
|
+
background: { fill: tokens.bar, radius: 16 },
|
|
3362
|
+
});
|
|
3121
3363
|
if (isBase && hero) {
|
|
3122
3364
|
const SIDE = 12;
|
|
3123
3365
|
const lz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'start' });
|
|
@@ -3153,7 +3395,12 @@ class BottomBar extends pixi_js.Container {
|
|
|
3153
3395
|
const heroW = hero ? (heroSized.measureSize?.().w ?? hero.getLocalBounds().width) : 0;
|
|
3154
3396
|
const fixed = ICON /* menu */ + (this.autoBtn ? ICON : 0) + heroW + (this.turboBtn ? ICON : 0);
|
|
3155
3397
|
const twSlot = Math.max(60, W - 2 * PAD - fixed - GAPS);
|
|
3156
|
-
controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
|
|
3398
|
+
controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
|
|
3399
|
+
color: '#ffffff',
|
|
3400
|
+
muted: '#ffffff',
|
|
3401
|
+
align: 'center',
|
|
3402
|
+
maxWidth: twSlot,
|
|
3403
|
+
}));
|
|
3157
3404
|
}
|
|
3158
3405
|
if (this.turboBtn)
|
|
3159
3406
|
controls.add(this.turboBtn);
|
|
@@ -3164,36 +3411,76 @@ class BottomBar extends pixi_js.Container {
|
|
|
3164
3411
|
const feature = state.activeFeature;
|
|
3165
3412
|
const betShown = feature ? state.bet * feature.priceMultiplier : state.bet;
|
|
3166
3413
|
if (isBase) {
|
|
3167
|
-
this.betDown = new IconButton('minus', {
|
|
3414
|
+
this.betDown = new IconButton('minus', {
|
|
3415
|
+
size: 26,
|
|
3416
|
+
glyph: 18,
|
|
3417
|
+
color: '#ffffff',
|
|
3418
|
+
hover: tokens.accent,
|
|
3419
|
+
onTap: () => this.onBet(-1),
|
|
3420
|
+
});
|
|
3168
3421
|
betGroup.add(this.betDown);
|
|
3169
3422
|
}
|
|
3170
|
-
const bet = new Readout({
|
|
3423
|
+
const bet = new Readout({
|
|
3424
|
+
label: this.host.t('Bet'),
|
|
3425
|
+
value: this.host.fmt(betShown),
|
|
3426
|
+
muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel,
|
|
3427
|
+
fg: feature ? effectiveAccent(feature) : '#ffffff',
|
|
3428
|
+
valueSize: 11,
|
|
3429
|
+
align: 'center',
|
|
3430
|
+
fixedWidth: 76,
|
|
3431
|
+
shadow: false,
|
|
3432
|
+
});
|
|
3171
3433
|
this.betReadout = bet;
|
|
3172
3434
|
if (isBase) {
|
|
3173
3435
|
bet.eventMode = 'static';
|
|
3174
3436
|
bet.cursor = 'pointer';
|
|
3175
|
-
bet.on('pointertap', () => {
|
|
3176
|
-
this.
|
|
3437
|
+
bet.on('pointertap', () => {
|
|
3438
|
+
if (!this.betLocked())
|
|
3439
|
+
this.host.actions.openBetPicker();
|
|
3440
|
+
});
|
|
3177
3441
|
}
|
|
3178
3442
|
betGroup.add(bet);
|
|
3179
3443
|
if (isBase) {
|
|
3180
|
-
this.betUp = new IconButton('plus', {
|
|
3444
|
+
this.betUp = new IconButton('plus', {
|
|
3445
|
+
size: 26,
|
|
3446
|
+
glyph: 18,
|
|
3447
|
+
color: '#ffffff',
|
|
3448
|
+
hover: tokens.accent,
|
|
3449
|
+
onTap: () => this.onBet(1),
|
|
3450
|
+
});
|
|
3181
3451
|
betGroup.add(this.betUp);
|
|
3182
3452
|
}
|
|
3183
3453
|
betGroup.layout();
|
|
3184
3454
|
const innerW = W - 28; // pad 14 each side
|
|
3185
3455
|
const slot = Math.max(40, (innerW - betGroup.outerWidth - 20) / 2);
|
|
3186
|
-
const info = new FlexBox({
|
|
3456
|
+
const info = new FlexBox({
|
|
3457
|
+
direction: 'row',
|
|
3458
|
+
align: 'center',
|
|
3459
|
+
justify: 'space-between',
|
|
3460
|
+
width: W,
|
|
3461
|
+
height: M_INFO_H,
|
|
3462
|
+
padding: { left: 14, right: 14 },
|
|
3463
|
+
gap: 10,
|
|
3464
|
+
background: { fill: tokens.plaqueGlass, radius: 12 },
|
|
3465
|
+
});
|
|
3187
3466
|
// replay is a read-only historical round — no real balance to show (sticky `replay`, so it stays
|
|
3188
3467
|
// hidden through a replay's free-spins phase too); matches the DOM bar.
|
|
3189
3468
|
if (!state.replay) {
|
|
3190
|
-
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
|
|
3469
|
+
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
|
|
3470
|
+
valueSize: 11,
|
|
3471
|
+
align: 'left',
|
|
3472
|
+
fixedWidth: slot,
|
|
3473
|
+
});
|
|
3191
3474
|
this.balanceValue = bal.valueText;
|
|
3192
3475
|
info.add(bal);
|
|
3193
3476
|
}
|
|
3194
3477
|
info.add(betGroup);
|
|
3195
3478
|
// WIN always present (no jiggle on win↔0)
|
|
3196
|
-
const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
|
|
3479
|
+
const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
|
|
3480
|
+
valueSize: 11,
|
|
3481
|
+
align: 'right',
|
|
3482
|
+
fixedWidth: slot,
|
|
3483
|
+
});
|
|
3197
3484
|
this.winValue = win.valueText;
|
|
3198
3485
|
info.add(win);
|
|
3199
3486
|
info.layout();
|
|
@@ -3284,7 +3571,7 @@ class BottomBar extends pixi_js.Container {
|
|
|
3284
3571
|
left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
|
|
3285
3572
|
right.position.set(panelRight - PANEL_PAD - right.outerWidth, panelCenterY - right.outerHeight / 2);
|
|
3286
3573
|
this.inner.scale.set(s);
|
|
3287
|
-
this.inner.position.set((W - barW * s) / 2,
|
|
3574
|
+
this.inner.position.set((W - barW * s) / 2, H - WIDE_PAD_BOTTOM - SPIN * s);
|
|
3288
3575
|
this.measureFootprint();
|
|
3289
3576
|
}
|
|
3290
3577
|
applyFitMobile() {
|
|
@@ -3690,6 +3977,12 @@ class Slider extends pixi_js.Container {
|
|
|
3690
3977
|
this.thumb.circle(cx, cy, r).fill('#ffffff');
|
|
3691
3978
|
this.hitArea = new pixi_js.Rectangle(0, 0, this.w, h);
|
|
3692
3979
|
}
|
|
3980
|
+
/** Programmatically move the thumb (0..1) without firing onInput — for live updates driven by
|
|
3981
|
+
* `host.setVolume()` while the overlay is open. */
|
|
3982
|
+
setValue(v) {
|
|
3983
|
+
this._value = Math.max(0, Math.min(1, v));
|
|
3984
|
+
this.draw();
|
|
3985
|
+
}
|
|
3693
3986
|
setLayoutSize(w) {
|
|
3694
3987
|
if (w != null)
|
|
3695
3988
|
this.w = w;
|
|
@@ -3778,10 +4071,13 @@ function buildBody$1(host, width) {
|
|
|
3778
4071
|
speaker.active = on;
|
|
3779
4072
|
});
|
|
3780
4073
|
col.add(glassRow(host, [textNode(host, host.t('Sound')), new Spacer(), speaker]));
|
|
3781
|
-
// Volume sliders
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
col.add(sliderRow(host, width, '
|
|
4074
|
+
// Volume sliders — positions read from the shell's stored volumes (stateful across opens); the
|
|
4075
|
+
// registered refreshers let `host.setVolume()` from game code move the thumbs live.
|
|
4076
|
+
const updaters = {};
|
|
4077
|
+
col.add(sliderRow(host, width, 'master', host.t('Master volume'), updaters));
|
|
4078
|
+
col.add(sliderRow(host, width, 'music', host.t('Music'), updaters));
|
|
4079
|
+
col.add(sliderRow(host, width, 'sfx', host.t('SFX'), updaters));
|
|
4080
|
+
host.setVolumeRefresh?.((key, v) => updaters[key]?.(v));
|
|
3785
4081
|
// Game info link
|
|
3786
4082
|
const infoIcon = makeIcon('info', 22, '#ffffff');
|
|
3787
4083
|
const infoLabel = textNode(host, host.t('Game info'));
|
|
@@ -3825,7 +4121,7 @@ function glassRow(host, children, opts = {}) {
|
|
|
3825
4121
|
return row;
|
|
3826
4122
|
}
|
|
3827
4123
|
/** A column row: head (label + live % value) over a draggable slider. */
|
|
3828
|
-
function sliderRow(host, bodyWidth, key, label) {
|
|
4124
|
+
function sliderRow(host, bodyWidth, key, label, updaters) {
|
|
3829
4125
|
const row = new FlexBox({
|
|
3830
4126
|
direction: 'column',
|
|
3831
4127
|
align: 'stretch',
|
|
@@ -3834,14 +4130,19 @@ function sliderRow(host, bodyWidth, key, label) {
|
|
|
3834
4130
|
background: { fill: host.tokens.plaqueGlass, radius: 16 },
|
|
3835
4131
|
});
|
|
3836
4132
|
const head = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between' });
|
|
3837
|
-
const
|
|
4133
|
+
const initial = host.getVolume(key);
|
|
4134
|
+
const valueText = makeText(`${Math.round(initial * 100)}%`, { size: 13, weight: '700', color: host.tokens.plaqueLabel });
|
|
3838
4135
|
head.add(makeText(label, { size: 14, weight: '600', color: '#ffffff' }));
|
|
3839
4136
|
head.add(new Spacer(), { grow: 1 });
|
|
3840
4137
|
head.add(valueText);
|
|
3841
|
-
const slider = new Slider(host,
|
|
4138
|
+
const slider = new Slider(host, initial, (v) => {
|
|
3842
4139
|
valueText.text = `${Math.round(v * 100)}%`;
|
|
3843
|
-
host.
|
|
4140
|
+
host.setVolume(key, v);
|
|
3844
4141
|
});
|
|
4142
|
+
updaters[key] = (v) => {
|
|
4143
|
+
valueText.text = `${Math.round(v * 100)}%`;
|
|
4144
|
+
slider.setValue(v);
|
|
4145
|
+
};
|
|
3845
4146
|
row.add(head);
|
|
3846
4147
|
row.add(slider);
|
|
3847
4148
|
return row;
|
|
@@ -5871,6 +6172,9 @@ class PixiRenderer {
|
|
|
5871
6172
|
notifyResize: (w, h) => host.notifyResize(w, h),
|
|
5872
6173
|
setSound: (on) => host.setSound(on),
|
|
5873
6174
|
setSoundRefresh: (fn) => host.setSoundRefresh(fn),
|
|
6175
|
+
getVolume: (key) => host.getVolume(key),
|
|
6176
|
+
setVolume: (key, v) => host.setVolume(key, v),
|
|
6177
|
+
setVolumeRefresh: (fn) => host.setVolumeRefresh(fn),
|
|
5874
6178
|
// — Pixi-specific surface —
|
|
5875
6179
|
get ticker() { return self.app.ticker; },
|
|
5876
6180
|
get canvas() { return self.app.canvas; },
|