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