@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/pixi.cjs.js
CHANGED
|
@@ -66,6 +66,7 @@ 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,
|
|
70
71
|
};
|
|
71
72
|
}
|
|
@@ -1439,7 +1440,7 @@ class KeyboardController {
|
|
|
1439
1440
|
|
|
1440
1441
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1441
1442
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1442
|
-
const PACKAGE_VERSION = '0.
|
|
1443
|
+
const PACKAGE_VERSION = '0.5.0';
|
|
1443
1444
|
|
|
1444
1445
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1445
1446
|
function resolveConfig(config) {
|
|
@@ -1500,9 +1501,15 @@ class ShellController extends EventEmitter {
|
|
|
1500
1501
|
this.renderer.renderBar();
|
|
1501
1502
|
}
|
|
1502
1503
|
// ── ShellHost ──────────────────────────────────────────────────────────────
|
|
1503
|
-
t(text) {
|
|
1504
|
-
|
|
1505
|
-
|
|
1504
|
+
t(text) {
|
|
1505
|
+
return this.i18n.t(text);
|
|
1506
|
+
}
|
|
1507
|
+
formatCurrency(n, win = false) {
|
|
1508
|
+
return formatCurrency(n, this.config.currency, win);
|
|
1509
|
+
}
|
|
1510
|
+
formatWin(value) {
|
|
1511
|
+
return this.formatCurrency(value, true);
|
|
1512
|
+
}
|
|
1506
1513
|
notifyResize(w, h) {
|
|
1507
1514
|
const layout = w !== 0 && h > w ? 'mobile' : 'wide';
|
|
1508
1515
|
if (layout !== this.layout) {
|
|
@@ -1569,12 +1576,24 @@ class ShellController extends EventEmitter {
|
|
|
1569
1576
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1570
1577
|
const self = this;
|
|
1571
1578
|
const host = {
|
|
1572
|
-
get state() {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
get
|
|
1576
|
-
|
|
1577
|
-
|
|
1579
|
+
get state() {
|
|
1580
|
+
return self.state;
|
|
1581
|
+
},
|
|
1582
|
+
get hotkeysEnabled() {
|
|
1583
|
+
return self.config.features.hotkeys !== false;
|
|
1584
|
+
},
|
|
1585
|
+
get spacebarEnabled() {
|
|
1586
|
+
return self.config.features.spacebar !== false;
|
|
1587
|
+
},
|
|
1588
|
+
get turboLevels() {
|
|
1589
|
+
return self.config.features.turbo;
|
|
1590
|
+
},
|
|
1591
|
+
get autoplayEnabled() {
|
|
1592
|
+
return self.config.features.autoplay != null;
|
|
1593
|
+
},
|
|
1594
|
+
get buyBonusEnabled() {
|
|
1595
|
+
return self.config.features.buyBonus !== false;
|
|
1596
|
+
},
|
|
1578
1597
|
hasOpenLayer: () => self.overlay !== null,
|
|
1579
1598
|
routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
|
|
1580
1599
|
spin: () => self.actions.spin(),
|
|
@@ -1590,27 +1609,52 @@ class ShellController extends EventEmitter {
|
|
|
1590
1609
|
this.kbd = new KeyboardController(host);
|
|
1591
1610
|
this.kbd.attach();
|
|
1592
1611
|
}
|
|
1593
|
-
pullFocus = () => {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1612
|
+
pullFocus = () => {
|
|
1613
|
+
try {
|
|
1614
|
+
globalThis.focus?.();
|
|
1615
|
+
}
|
|
1616
|
+
catch {
|
|
1617
|
+
/* cross-origin */
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1597
1620
|
// ── overlay flow ─────────────────────────────────────────────────────────────
|
|
1598
1621
|
show(req) {
|
|
1599
1622
|
this.closeModal();
|
|
1600
1623
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1601
1624
|
}
|
|
1602
|
-
openMenu() {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1625
|
+
openMenu() {
|
|
1626
|
+
this.emit('menuOpen');
|
|
1627
|
+
this.openSettings();
|
|
1628
|
+
}
|
|
1629
|
+
openSettings() {
|
|
1630
|
+
this.emit('settingsOpen');
|
|
1631
|
+
this.show({ kind: 'settings' });
|
|
1632
|
+
}
|
|
1633
|
+
openInfo() {
|
|
1634
|
+
this.emit('infoOpen');
|
|
1635
|
+
this.show({ kind: 'gameInfo' });
|
|
1636
|
+
}
|
|
1637
|
+
openBuyBonus() {
|
|
1638
|
+
if (this.config.onBonusBuy) {
|
|
1639
|
+
this.config.onBonusBuy();
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
this.show({ kind: 'buyBonus' });
|
|
1643
|
+
}
|
|
1644
|
+
openBetPicker() {
|
|
1645
|
+
this.show({ kind: 'betPicker' });
|
|
1646
|
+
}
|
|
1647
|
+
openAutoplayPicker() {
|
|
1648
|
+
this.show({ kind: 'autoplayPicker' });
|
|
1649
|
+
}
|
|
1650
|
+
openReplay(opts) {
|
|
1651
|
+
if (this.destroyed)
|
|
1652
|
+
return;
|
|
1653
|
+
this.show({ kind: 'replay', opts });
|
|
1654
|
+
}
|
|
1655
|
+
openModal(opts) {
|
|
1656
|
+
this.show({ kind: 'modal', opts });
|
|
1657
|
+
}
|
|
1614
1658
|
/** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
|
|
1615
1659
|
closeModal() {
|
|
1616
1660
|
if (!this.overlay)
|
|
@@ -1626,7 +1670,9 @@ class ShellController extends EventEmitter {
|
|
|
1626
1670
|
this.soundRefresh?.(on);
|
|
1627
1671
|
this.renderer.refreshSoundIcon?.(on);
|
|
1628
1672
|
}
|
|
1629
|
-
setSoundRefresh(fn) {
|
|
1673
|
+
setSoundRefresh(fn) {
|
|
1674
|
+
this.soundRefresh = fn;
|
|
1675
|
+
}
|
|
1630
1676
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1631
1677
|
activateFeature(bonus) {
|
|
1632
1678
|
this.state.activeFeature = bonus;
|
|
@@ -1647,21 +1693,81 @@ class ShellController extends EventEmitter {
|
|
|
1647
1693
|
if (to !== from)
|
|
1648
1694
|
this.renderer.animateMoney(field, from, to);
|
|
1649
1695
|
}
|
|
1650
|
-
setBalance(n) {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
this.
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1696
|
+
setBalance(n) {
|
|
1697
|
+
const from = this.prevBalance;
|
|
1698
|
+
this.state.balance = n;
|
|
1699
|
+
this.prevBalance = n;
|
|
1700
|
+
this.money('balance', from, n);
|
|
1701
|
+
}
|
|
1702
|
+
setWin(n) {
|
|
1703
|
+
const from = this.prevWin;
|
|
1704
|
+
this.state.win = n;
|
|
1705
|
+
this.prevWin = n;
|
|
1706
|
+
this.money('win', from, n);
|
|
1707
|
+
}
|
|
1708
|
+
setBet(n) {
|
|
1709
|
+
this.state.bet = n;
|
|
1710
|
+
this.renderer.renderBar();
|
|
1711
|
+
}
|
|
1712
|
+
setMode(mode) {
|
|
1713
|
+
if (mode === 'replay')
|
|
1714
|
+
this.state.replay = true;
|
|
1715
|
+
this.state.mode = mode;
|
|
1716
|
+
this.renderer.renderBar();
|
|
1717
|
+
}
|
|
1718
|
+
setBusy(busy) {
|
|
1719
|
+
this.state.busy = busy;
|
|
1720
|
+
this.renderer.renderBar();
|
|
1721
|
+
this.kbd?.notifyBusyChanged(busy);
|
|
1722
|
+
}
|
|
1723
|
+
setAutoplay(a) {
|
|
1724
|
+
this.state.autoplay = a;
|
|
1725
|
+
this.renderer.renderBar();
|
|
1726
|
+
}
|
|
1727
|
+
setTurbo(level) {
|
|
1728
|
+
this.state.turbo = level;
|
|
1729
|
+
this.renderer.renderBar();
|
|
1730
|
+
}
|
|
1731
|
+
setBuyBonusEnabled(enabled) {
|
|
1732
|
+
this.state.buyBonusEnabled = enabled;
|
|
1733
|
+
this.renderer.renderBar();
|
|
1734
|
+
}
|
|
1735
|
+
setFreeSpins(fs) {
|
|
1736
|
+
this.state.freeSpins = fs;
|
|
1737
|
+
this.state.bonus = null;
|
|
1738
|
+
this.renderer.renderBar();
|
|
1739
|
+
}
|
|
1740
|
+
/** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
|
|
1741
|
+
* override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
|
|
1742
|
+
* `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
|
|
1743
|
+
setBonus(b) {
|
|
1744
|
+
this.state.bonus = { label: b.label, value: b.value };
|
|
1745
|
+
this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
|
|
1746
|
+
this.renderer.renderBar();
|
|
1747
|
+
}
|
|
1748
|
+
setTheme(theme) {
|
|
1749
|
+
this.config.theme = theme;
|
|
1750
|
+
this.tokens = resolveTheme(theme);
|
|
1751
|
+
this.renderer.applyTheme(this.tokens);
|
|
1752
|
+
this.renderer.renderBar();
|
|
1753
|
+
}
|
|
1754
|
+
setLanguage(lang) {
|
|
1755
|
+
this.config.language = lang;
|
|
1756
|
+
this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
|
|
1757
|
+
this.renderer.renderBar();
|
|
1758
|
+
}
|
|
1759
|
+
setSocial(isSocial) {
|
|
1760
|
+
this.config.isSocial = isSocial;
|
|
1761
|
+
this.i18n = createI18n({ language: this.config.language, isSocial });
|
|
1762
|
+
this.renderer.renderBar();
|
|
1763
|
+
}
|
|
1764
|
+
setLayout(layout) {
|
|
1765
|
+
if (layout === this.layout)
|
|
1766
|
+
return;
|
|
1767
|
+
this.layout = layout;
|
|
1768
|
+
this.renderer.setLayout(layout);
|
|
1769
|
+
this.renderer.renderBar();
|
|
1770
|
+
}
|
|
1665
1771
|
destroy() {
|
|
1666
1772
|
if (this.destroyed)
|
|
1667
1773
|
return Promise.resolve();
|
|
@@ -2924,8 +3030,12 @@ class TurboButton extends pixi_js.Container {
|
|
|
2924
3030
|
this.accent = accent;
|
|
2925
3031
|
this._paint(false);
|
|
2926
3032
|
}
|
|
2927
|
-
measureSize() {
|
|
2928
|
-
|
|
3033
|
+
measureSize() {
|
|
3034
|
+
return { w: this.box, h: this.box };
|
|
3035
|
+
}
|
|
3036
|
+
setLayoutSize() {
|
|
3037
|
+
/* fixed */
|
|
3038
|
+
}
|
|
2929
3039
|
}
|
|
2930
3040
|
/** A readout in the bar (white Oswald value, plaque-label caption, no shadow). */
|
|
2931
3041
|
function readout(host, label, value, opts = {}) {
|
|
@@ -2973,13 +3083,19 @@ class BottomBar extends pixi_js.Container {
|
|
|
2973
3083
|
buildWide() {
|
|
2974
3084
|
const { state, tokens } = this.host;
|
|
2975
3085
|
const isBase = state.mode === 'base';
|
|
2976
|
-
const isFS = state.mode === 'freeSpins';
|
|
3086
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
2977
3087
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
2978
3088
|
// BUY BONUS — floats outside-left of the panel (base only)
|
|
2979
|
-
this.buy = isBase ? this.buildBuy(BUY_W, 13, 3) ?? undefined : undefined;
|
|
3089
|
+
this.buy = isBase ? (this.buildBuy(BUY_W, 13, 3) ?? undefined) : undefined;
|
|
2980
3090
|
// LEFT info group: menu · balance · (Total win) · (Win)
|
|
2981
3091
|
const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
|
|
2982
|
-
left.add(new IconButton('menu', {
|
|
3092
|
+
left.add(new IconButton('menu', {
|
|
3093
|
+
size: 36,
|
|
3094
|
+
glyph: 30,
|
|
3095
|
+
color: '#ffffff',
|
|
3096
|
+
hover: tokens.accent,
|
|
3097
|
+
onTap: () => this.host.actions.openMenu(),
|
|
3098
|
+
}));
|
|
2983
3099
|
if (!state.replay) {
|
|
2984
3100
|
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
|
|
2985
3101
|
this.balanceValue = bal.valueText;
|
|
@@ -3018,16 +3134,30 @@ class BottomBar extends pixi_js.Container {
|
|
|
3018
3134
|
if (isBase) {
|
|
3019
3135
|
bet.eventMode = 'static';
|
|
3020
3136
|
bet.cursor = 'pointer';
|
|
3021
|
-
bet.on('pointertap', () => {
|
|
3022
|
-
this.
|
|
3137
|
+
bet.on('pointertap', () => {
|
|
3138
|
+
if (!this.betLocked())
|
|
3139
|
+
this.host.actions.openBetPicker();
|
|
3140
|
+
});
|
|
3023
3141
|
}
|
|
3024
3142
|
const betGroup = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
|
|
3025
3143
|
betGroup.add(bet);
|
|
3026
3144
|
if (isBase) {
|
|
3027
3145
|
// plain (borderless) +/- icons, bolder — stacked
|
|
3028
3146
|
const step = new FlexBox({ direction: 'column', align: 'center', gap: 2 });
|
|
3029
|
-
this.betUp = new IconButton('plus', {
|
|
3030
|
-
|
|
3147
|
+
this.betUp = new IconButton('plus', {
|
|
3148
|
+
size: 24,
|
|
3149
|
+
glyph: 22,
|
|
3150
|
+
color: '#ffffff',
|
|
3151
|
+
hover: tokens.accent,
|
|
3152
|
+
onTap: () => this.onBet(1),
|
|
3153
|
+
});
|
|
3154
|
+
this.betDown = new IconButton('minus', {
|
|
3155
|
+
size: 24,
|
|
3156
|
+
glyph: 22,
|
|
3157
|
+
color: '#ffffff',
|
|
3158
|
+
hover: tokens.accent,
|
|
3159
|
+
onTap: () => this.onBet(-1),
|
|
3160
|
+
});
|
|
3031
3161
|
step.add(this.betUp);
|
|
3032
3162
|
step.add(this.betDown);
|
|
3033
3163
|
betGroup.add(step);
|
|
@@ -3035,15 +3165,28 @@ class BottomBar extends pixi_js.Container {
|
|
|
3035
3165
|
const spinWrap = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
|
|
3036
3166
|
if (isBase && config.features.autoplay) {
|
|
3037
3167
|
this.autoBtn = new IconButton('autoplay', {
|
|
3038
|
-
size: DISC,
|
|
3039
|
-
|
|
3168
|
+
size: DISC,
|
|
3169
|
+
glyph: 25,
|
|
3170
|
+
disc: tokens.btn,
|
|
3171
|
+
color: tokens.btnInk,
|
|
3172
|
+
hover: tokens.accent,
|
|
3173
|
+
activeColor: tokens.accent,
|
|
3174
|
+
active: state.autoplay.active,
|
|
3175
|
+
onTap: () => this.onAutoplay(),
|
|
3040
3176
|
});
|
|
3041
3177
|
if (state.autoplay.active)
|
|
3042
3178
|
this.autoBtn.setGlow(true);
|
|
3043
3179
|
spinWrap.add(this.autoBtn);
|
|
3044
3180
|
}
|
|
3045
3181
|
if (isBase) {
|
|
3046
|
-
this.spin = new SpinDisc({
|
|
3182
|
+
this.spin = new SpinDisc({
|
|
3183
|
+
size: SPIN,
|
|
3184
|
+
glyph: 65,
|
|
3185
|
+
tokens,
|
|
3186
|
+
ticker: this.host.ticker,
|
|
3187
|
+
onSpin: () => this.host.actions.spin(),
|
|
3188
|
+
onStop: () => this.stopAutoplay(),
|
|
3189
|
+
});
|
|
3047
3190
|
if (state.autoplay.active)
|
|
3048
3191
|
this.spin.setAutoplay(true, state.autoplay.remaining);
|
|
3049
3192
|
if (state.busy)
|
|
@@ -3052,13 +3195,23 @@ class BottomBar extends pixi_js.Container {
|
|
|
3052
3195
|
}
|
|
3053
3196
|
else if (showFsBlocks) {
|
|
3054
3197
|
const fs = state.freeSpins;
|
|
3055
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
3056
|
-
spinWrap.add(new FsHero({
|
|
3198
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
3199
|
+
spinWrap.add(new FsHero({
|
|
3200
|
+
label: this.host.t(state.bonus?.label ?? 'Free spins'),
|
|
3201
|
+
value: fsText,
|
|
3202
|
+
tokens,
|
|
3203
|
+
height: SPIN,
|
|
3204
|
+
}));
|
|
3057
3205
|
}
|
|
3058
3206
|
if (config.features.turbo > 0) {
|
|
3059
3207
|
this.turboBtn = new TurboButton({
|
|
3060
|
-
size: DISC,
|
|
3061
|
-
|
|
3208
|
+
size: DISC,
|
|
3209
|
+
glyph: 19,
|
|
3210
|
+
discFill: tokens.btn,
|
|
3211
|
+
discBorder: 2,
|
|
3212
|
+
accent: tokens.accent,
|
|
3213
|
+
level: state.turbo,
|
|
3214
|
+
onTap: () => this.onTurbo(),
|
|
3062
3215
|
});
|
|
3063
3216
|
spinWrap.add(this.turboBtn);
|
|
3064
3217
|
}
|
|
@@ -3076,21 +3229,49 @@ class BottomBar extends pixi_js.Container {
|
|
|
3076
3229
|
const feature = state.activeFeature;
|
|
3077
3230
|
if (feature) {
|
|
3078
3231
|
const accent = effectiveAccent(feature);
|
|
3079
|
-
return new BuyBonusBadge({
|
|
3232
|
+
return new BuyBonusBadge({
|
|
3233
|
+
size,
|
|
3234
|
+
fontSize,
|
|
3235
|
+
border,
|
|
3236
|
+
bg: accent,
|
|
3237
|
+
fg: contrastText(accent),
|
|
3238
|
+
label: this.host.t('DISABLE'),
|
|
3239
|
+
tokens,
|
|
3240
|
+
ticker: this.host.ticker,
|
|
3241
|
+
onTap: () => this.host.actions.deactivateFeature(),
|
|
3242
|
+
});
|
|
3080
3243
|
}
|
|
3081
|
-
return new BuyBonusBadge({
|
|
3244
|
+
return new BuyBonusBadge({
|
|
3245
|
+
size,
|
|
3246
|
+
border,
|
|
3247
|
+
bg: tokens.accent,
|
|
3248
|
+
icon: 'ticket',
|
|
3249
|
+
iconSize: size * 0.55,
|
|
3250
|
+
iconColor: tokens.btnInk,
|
|
3251
|
+
label: '',
|
|
3252
|
+
tokens,
|
|
3253
|
+
ticker: this.host.ticker,
|
|
3254
|
+
onTap: () => this.host.actions.openBuyBonus(),
|
|
3255
|
+
});
|
|
3082
3256
|
}
|
|
3083
3257
|
// ── mobile / portrait ─────────────────────────────────────────────────────
|
|
3084
3258
|
buildMobile() {
|
|
3085
3259
|
const { state, config, tokens } = this.host;
|
|
3086
3260
|
const isBase = state.mode === 'base';
|
|
3087
|
-
const isFS = state.mode === 'freeSpins';
|
|
3261
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
3088
3262
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
3089
3263
|
const W = this.host.screenW - 2 * M_SIDE;
|
|
3090
3264
|
// hero (centre): SPIN in base, FS counter in free spins
|
|
3091
3265
|
let hero;
|
|
3092
3266
|
if (isBase) {
|
|
3093
|
-
this.spin = new SpinDisc({
|
|
3267
|
+
this.spin = new SpinDisc({
|
|
3268
|
+
size: SPIN,
|
|
3269
|
+
glyph: 65,
|
|
3270
|
+
tokens,
|
|
3271
|
+
ticker: this.host.ticker,
|
|
3272
|
+
onSpin: () => this.host.actions.spin(),
|
|
3273
|
+
onStop: () => this.stopAutoplay(),
|
|
3274
|
+
});
|
|
3094
3275
|
if (state.autoplay.active)
|
|
3095
3276
|
this.spin.setAutoplay(true, state.autoplay.remaining);
|
|
3096
3277
|
if (state.busy)
|
|
@@ -3099,25 +3280,58 @@ class BottomBar extends pixi_js.Container {
|
|
|
3099
3280
|
}
|
|
3100
3281
|
else if (showFsBlocks) {
|
|
3101
3282
|
const fs = state.freeSpins;
|
|
3102
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
3103
|
-
hero = new FsHero({
|
|
3283
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
3284
|
+
hero = new FsHero({
|
|
3285
|
+
label: this.host.t(state.bonus?.label ?? 'Free spins'),
|
|
3286
|
+
value: fsText,
|
|
3287
|
+
tokens,
|
|
3288
|
+
height: SPIN,
|
|
3289
|
+
});
|
|
3104
3290
|
}
|
|
3105
|
-
const menu = new IconButton('menu', {
|
|
3291
|
+
const menu = new IconButton('menu', {
|
|
3292
|
+
size: 40,
|
|
3293
|
+
glyph: 26,
|
|
3294
|
+
color: '#ffffff',
|
|
3295
|
+
hover: tokens.accent,
|
|
3296
|
+
onTap: () => this.host.actions.openMenu(),
|
|
3297
|
+
});
|
|
3106
3298
|
// autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
|
|
3107
3299
|
if (isBase && config.features.autoplay) {
|
|
3108
|
-
this.autoBtn = new IconButton('autoplay', {
|
|
3300
|
+
this.autoBtn = new IconButton('autoplay', {
|
|
3301
|
+
size: 40,
|
|
3302
|
+
glyph: 26,
|
|
3303
|
+
color: '#ffffff',
|
|
3304
|
+
hover: tokens.accent,
|
|
3305
|
+
activeColor: tokens.accent,
|
|
3306
|
+
active: state.autoplay.active,
|
|
3307
|
+
onTap: () => this.onAutoplay(),
|
|
3308
|
+
});
|
|
3109
3309
|
if (state.autoplay.active)
|
|
3110
3310
|
this.autoBtn.setGlow(true);
|
|
3111
3311
|
}
|
|
3112
3312
|
if (config.features.turbo > 0) {
|
|
3113
3313
|
this.turboBtn = new TurboButton({
|
|
3114
|
-
size: 40,
|
|
3115
|
-
|
|
3314
|
+
size: 40,
|
|
3315
|
+
glyph: 22,
|
|
3316
|
+
discFill: tokens.btn,
|
|
3317
|
+
discBorder: 2,
|
|
3318
|
+
accent: tokens.accent,
|
|
3319
|
+
level: state.turbo,
|
|
3320
|
+
onTap: () => this.onTurbo(),
|
|
3116
3321
|
});
|
|
3117
3322
|
}
|
|
3118
|
-
const buy = isBase ? this.buildBuy(M_BUY, 9, 2) ?? undefined : undefined;
|
|
3323
|
+
const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
|
|
3119
3324
|
// level 1 — controls bar (dark)
|
|
3120
|
-
const controls = new FlexBox({
|
|
3325
|
+
const controls = new FlexBox({
|
|
3326
|
+
direction: 'row',
|
|
3327
|
+
align: 'center',
|
|
3328
|
+
justify: 'space-between',
|
|
3329
|
+
gap: 0,
|
|
3330
|
+
width: W,
|
|
3331
|
+
height: M_CTRL_H,
|
|
3332
|
+
padding: { left: 18, right: 18 },
|
|
3333
|
+
background: { fill: tokens.bar, radius: 16 },
|
|
3334
|
+
});
|
|
3121
3335
|
if (isBase && hero) {
|
|
3122
3336
|
const SIDE = 12;
|
|
3123
3337
|
const lz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'start' });
|
|
@@ -3153,7 +3367,12 @@ class BottomBar extends pixi_js.Container {
|
|
|
3153
3367
|
const heroW = hero ? (heroSized.measureSize?.().w ?? hero.getLocalBounds().width) : 0;
|
|
3154
3368
|
const fixed = ICON /* menu */ + (this.autoBtn ? ICON : 0) + heroW + (this.turboBtn ? ICON : 0);
|
|
3155
3369
|
const twSlot = Math.max(60, W - 2 * PAD - fixed - GAPS);
|
|
3156
|
-
controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
|
|
3370
|
+
controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
|
|
3371
|
+
color: '#ffffff',
|
|
3372
|
+
muted: '#ffffff',
|
|
3373
|
+
align: 'center',
|
|
3374
|
+
maxWidth: twSlot,
|
|
3375
|
+
}));
|
|
3157
3376
|
}
|
|
3158
3377
|
if (this.turboBtn)
|
|
3159
3378
|
controls.add(this.turboBtn);
|
|
@@ -3164,36 +3383,76 @@ class BottomBar extends pixi_js.Container {
|
|
|
3164
3383
|
const feature = state.activeFeature;
|
|
3165
3384
|
const betShown = feature ? state.bet * feature.priceMultiplier : state.bet;
|
|
3166
3385
|
if (isBase) {
|
|
3167
|
-
this.betDown = new IconButton('minus', {
|
|
3386
|
+
this.betDown = new IconButton('minus', {
|
|
3387
|
+
size: 26,
|
|
3388
|
+
glyph: 18,
|
|
3389
|
+
color: '#ffffff',
|
|
3390
|
+
hover: tokens.accent,
|
|
3391
|
+
onTap: () => this.onBet(-1),
|
|
3392
|
+
});
|
|
3168
3393
|
betGroup.add(this.betDown);
|
|
3169
3394
|
}
|
|
3170
|
-
const bet = new Readout({
|
|
3395
|
+
const bet = new Readout({
|
|
3396
|
+
label: this.host.t('Bet'),
|
|
3397
|
+
value: this.host.fmt(betShown),
|
|
3398
|
+
muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel,
|
|
3399
|
+
fg: feature ? effectiveAccent(feature) : '#ffffff',
|
|
3400
|
+
valueSize: 11,
|
|
3401
|
+
align: 'center',
|
|
3402
|
+
fixedWidth: 76,
|
|
3403
|
+
shadow: false,
|
|
3404
|
+
});
|
|
3171
3405
|
this.betReadout = bet;
|
|
3172
3406
|
if (isBase) {
|
|
3173
3407
|
bet.eventMode = 'static';
|
|
3174
3408
|
bet.cursor = 'pointer';
|
|
3175
|
-
bet.on('pointertap', () => {
|
|
3176
|
-
this.
|
|
3409
|
+
bet.on('pointertap', () => {
|
|
3410
|
+
if (!this.betLocked())
|
|
3411
|
+
this.host.actions.openBetPicker();
|
|
3412
|
+
});
|
|
3177
3413
|
}
|
|
3178
3414
|
betGroup.add(bet);
|
|
3179
3415
|
if (isBase) {
|
|
3180
|
-
this.betUp = new IconButton('plus', {
|
|
3416
|
+
this.betUp = new IconButton('plus', {
|
|
3417
|
+
size: 26,
|
|
3418
|
+
glyph: 18,
|
|
3419
|
+
color: '#ffffff',
|
|
3420
|
+
hover: tokens.accent,
|
|
3421
|
+
onTap: () => this.onBet(1),
|
|
3422
|
+
});
|
|
3181
3423
|
betGroup.add(this.betUp);
|
|
3182
3424
|
}
|
|
3183
3425
|
betGroup.layout();
|
|
3184
3426
|
const innerW = W - 28; // pad 14 each side
|
|
3185
3427
|
const slot = Math.max(40, (innerW - betGroup.outerWidth - 20) / 2);
|
|
3186
|
-
const info = new FlexBox({
|
|
3428
|
+
const info = new FlexBox({
|
|
3429
|
+
direction: 'row',
|
|
3430
|
+
align: 'center',
|
|
3431
|
+
justify: 'space-between',
|
|
3432
|
+
width: W,
|
|
3433
|
+
height: M_INFO_H,
|
|
3434
|
+
padding: { left: 14, right: 14 },
|
|
3435
|
+
gap: 10,
|
|
3436
|
+
background: { fill: tokens.plaqueGlass, radius: 12 },
|
|
3437
|
+
});
|
|
3187
3438
|
// replay is a read-only historical round — no real balance to show (sticky `replay`, so it stays
|
|
3188
3439
|
// hidden through a replay's free-spins phase too); matches the DOM bar.
|
|
3189
3440
|
if (!state.replay) {
|
|
3190
|
-
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
|
|
3441
|
+
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
|
|
3442
|
+
valueSize: 11,
|
|
3443
|
+
align: 'left',
|
|
3444
|
+
fixedWidth: slot,
|
|
3445
|
+
});
|
|
3191
3446
|
this.balanceValue = bal.valueText;
|
|
3192
3447
|
info.add(bal);
|
|
3193
3448
|
}
|
|
3194
3449
|
info.add(betGroup);
|
|
3195
3450
|
// WIN always present (no jiggle on win↔0)
|
|
3196
|
-
const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
|
|
3451
|
+
const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
|
|
3452
|
+
valueSize: 11,
|
|
3453
|
+
align: 'right',
|
|
3454
|
+
fixedWidth: slot,
|
|
3455
|
+
});
|
|
3197
3456
|
this.winValue = win.valueText;
|
|
3198
3457
|
info.add(win);
|
|
3199
3458
|
info.layout();
|
|
@@ -3284,7 +3543,7 @@ class BottomBar extends pixi_js.Container {
|
|
|
3284
3543
|
left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
|
|
3285
3544
|
right.position.set(panelRight - PANEL_PAD - right.outerWidth, panelCenterY - right.outerHeight / 2);
|
|
3286
3545
|
this.inner.scale.set(s);
|
|
3287
|
-
this.inner.position.set((W - barW * s) / 2,
|
|
3546
|
+
this.inner.position.set((W - barW * s) / 2, H - WIDE_PAD_BOTTOM - SPIN * s);
|
|
3288
3547
|
this.measureFootprint();
|
|
3289
3548
|
}
|
|
3290
3549
|
applyFitMobile() {
|