@energy8platform/shell 0.4.0 → 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 +214 -69
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.d.ts +31 -4
- package/dist/html.esm.js +214 -69
- 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 +451 -107
- package/dist/pixi.cjs.js.map +1 -1
- package/dist/pixi.d.ts +31 -4
- package/dist/pixi.esm.js +451 -107
- 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/html/components/pickers.ts +13 -1
- package/src/ui/html/shell.css.ts +19 -3
- package/src/ui/pixi/components/BottomBar.ts +280 -64
- package/src/ui/pixi/components/GameInfo.ts +52 -13
- package/src/ui/pixi/components/pickers.ts +66 -15
package/dist/pixi.esm.js
CHANGED
|
@@ -64,6 +64,7 @@ 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,
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -1437,7 +1438,7 @@ class KeyboardController {
|
|
|
1437
1438
|
|
|
1438
1439
|
// AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
|
|
1439
1440
|
/** The @energy8platform/shell package version, stamped into the game-info footer. */
|
|
1440
|
-
const PACKAGE_VERSION = '0.
|
|
1441
|
+
const PACKAGE_VERSION = '0.5.0';
|
|
1441
1442
|
|
|
1442
1443
|
/** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
|
|
1443
1444
|
function resolveConfig(config) {
|
|
@@ -1498,9 +1499,15 @@ class ShellController extends EventEmitter {
|
|
|
1498
1499
|
this.renderer.renderBar();
|
|
1499
1500
|
}
|
|
1500
1501
|
// ── ShellHost ──────────────────────────────────────────────────────────────
|
|
1501
|
-
t(text) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1502
|
+
t(text) {
|
|
1503
|
+
return this.i18n.t(text);
|
|
1504
|
+
}
|
|
1505
|
+
formatCurrency(n, win = false) {
|
|
1506
|
+
return formatCurrency(n, this.config.currency, win);
|
|
1507
|
+
}
|
|
1508
|
+
formatWin(value) {
|
|
1509
|
+
return this.formatCurrency(value, true);
|
|
1510
|
+
}
|
|
1504
1511
|
notifyResize(w, h) {
|
|
1505
1512
|
const layout = w !== 0 && h > w ? 'mobile' : 'wide';
|
|
1506
1513
|
if (layout !== this.layout) {
|
|
@@ -1567,12 +1574,24 @@ class ShellController extends EventEmitter {
|
|
|
1567
1574
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
1568
1575
|
const self = this;
|
|
1569
1576
|
const host = {
|
|
1570
|
-
get state() {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
get
|
|
1574
|
-
|
|
1575
|
-
|
|
1577
|
+
get state() {
|
|
1578
|
+
return self.state;
|
|
1579
|
+
},
|
|
1580
|
+
get hotkeysEnabled() {
|
|
1581
|
+
return self.config.features.hotkeys !== false;
|
|
1582
|
+
},
|
|
1583
|
+
get spacebarEnabled() {
|
|
1584
|
+
return self.config.features.spacebar !== false;
|
|
1585
|
+
},
|
|
1586
|
+
get turboLevels() {
|
|
1587
|
+
return self.config.features.turbo;
|
|
1588
|
+
},
|
|
1589
|
+
get autoplayEnabled() {
|
|
1590
|
+
return self.config.features.autoplay != null;
|
|
1591
|
+
},
|
|
1592
|
+
get buyBonusEnabled() {
|
|
1593
|
+
return self.config.features.buyBonus !== false;
|
|
1594
|
+
},
|
|
1576
1595
|
hasOpenLayer: () => self.overlay !== null,
|
|
1577
1596
|
routeToLayer: (e) => self.overlay?.onKey?.(e) ?? false,
|
|
1578
1597
|
spin: () => self.actions.spin(),
|
|
@@ -1588,27 +1607,52 @@ class ShellController extends EventEmitter {
|
|
|
1588
1607
|
this.kbd = new KeyboardController(host);
|
|
1589
1608
|
this.kbd.attach();
|
|
1590
1609
|
}
|
|
1591
|
-
pullFocus = () => {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1610
|
+
pullFocus = () => {
|
|
1611
|
+
try {
|
|
1612
|
+
globalThis.focus?.();
|
|
1613
|
+
}
|
|
1614
|
+
catch {
|
|
1615
|
+
/* cross-origin */
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1595
1618
|
// ── overlay flow ─────────────────────────────────────────────────────────────
|
|
1596
1619
|
show(req) {
|
|
1597
1620
|
this.closeModal();
|
|
1598
1621
|
this.overlay = this.renderer.openOverlay(req) ?? null;
|
|
1599
1622
|
}
|
|
1600
|
-
openMenu() {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1623
|
+
openMenu() {
|
|
1624
|
+
this.emit('menuOpen');
|
|
1625
|
+
this.openSettings();
|
|
1626
|
+
}
|
|
1627
|
+
openSettings() {
|
|
1628
|
+
this.emit('settingsOpen');
|
|
1629
|
+
this.show({ kind: 'settings' });
|
|
1630
|
+
}
|
|
1631
|
+
openInfo() {
|
|
1632
|
+
this.emit('infoOpen');
|
|
1633
|
+
this.show({ kind: 'gameInfo' });
|
|
1634
|
+
}
|
|
1635
|
+
openBuyBonus() {
|
|
1636
|
+
if (this.config.onBonusBuy) {
|
|
1637
|
+
this.config.onBonusBuy();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
this.show({ kind: 'buyBonus' });
|
|
1641
|
+
}
|
|
1642
|
+
openBetPicker() {
|
|
1643
|
+
this.show({ kind: 'betPicker' });
|
|
1644
|
+
}
|
|
1645
|
+
openAutoplayPicker() {
|
|
1646
|
+
this.show({ kind: 'autoplayPicker' });
|
|
1647
|
+
}
|
|
1648
|
+
openReplay(opts) {
|
|
1649
|
+
if (this.destroyed)
|
|
1650
|
+
return;
|
|
1651
|
+
this.show({ kind: 'replay', opts });
|
|
1652
|
+
}
|
|
1653
|
+
openModal(opts) {
|
|
1654
|
+
this.show({ kind: 'modal', opts });
|
|
1655
|
+
}
|
|
1612
1656
|
/** Programmatically dismiss whatever overlay/modal is open. No-op when nothing is shown. */
|
|
1613
1657
|
closeModal() {
|
|
1614
1658
|
if (!this.overlay)
|
|
@@ -1624,7 +1668,9 @@ class ShellController extends EventEmitter {
|
|
|
1624
1668
|
this.soundRefresh?.(on);
|
|
1625
1669
|
this.renderer.refreshSoundIcon?.(on);
|
|
1626
1670
|
}
|
|
1627
|
-
setSoundRefresh(fn) {
|
|
1671
|
+
setSoundRefresh(fn) {
|
|
1672
|
+
this.soundRefresh = fn;
|
|
1673
|
+
}
|
|
1628
1674
|
// ── features ─────────────────────────────────────────────────────────────────
|
|
1629
1675
|
activateFeature(bonus) {
|
|
1630
1676
|
this.state.activeFeature = bonus;
|
|
@@ -1645,21 +1691,81 @@ class ShellController extends EventEmitter {
|
|
|
1645
1691
|
if (to !== from)
|
|
1646
1692
|
this.renderer.animateMoney(field, from, to);
|
|
1647
1693
|
}
|
|
1648
|
-
setBalance(n) {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
this.
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1694
|
+
setBalance(n) {
|
|
1695
|
+
const from = this.prevBalance;
|
|
1696
|
+
this.state.balance = n;
|
|
1697
|
+
this.prevBalance = n;
|
|
1698
|
+
this.money('balance', from, n);
|
|
1699
|
+
}
|
|
1700
|
+
setWin(n) {
|
|
1701
|
+
const from = this.prevWin;
|
|
1702
|
+
this.state.win = n;
|
|
1703
|
+
this.prevWin = n;
|
|
1704
|
+
this.money('win', from, n);
|
|
1705
|
+
}
|
|
1706
|
+
setBet(n) {
|
|
1707
|
+
this.state.bet = n;
|
|
1708
|
+
this.renderer.renderBar();
|
|
1709
|
+
}
|
|
1710
|
+
setMode(mode) {
|
|
1711
|
+
if (mode === 'replay')
|
|
1712
|
+
this.state.replay = true;
|
|
1713
|
+
this.state.mode = mode;
|
|
1714
|
+
this.renderer.renderBar();
|
|
1715
|
+
}
|
|
1716
|
+
setBusy(busy) {
|
|
1717
|
+
this.state.busy = busy;
|
|
1718
|
+
this.renderer.renderBar();
|
|
1719
|
+
this.kbd?.notifyBusyChanged(busy);
|
|
1720
|
+
}
|
|
1721
|
+
setAutoplay(a) {
|
|
1722
|
+
this.state.autoplay = a;
|
|
1723
|
+
this.renderer.renderBar();
|
|
1724
|
+
}
|
|
1725
|
+
setTurbo(level) {
|
|
1726
|
+
this.state.turbo = level;
|
|
1727
|
+
this.renderer.renderBar();
|
|
1728
|
+
}
|
|
1729
|
+
setBuyBonusEnabled(enabled) {
|
|
1730
|
+
this.state.buyBonusEnabled = enabled;
|
|
1731
|
+
this.renderer.renderBar();
|
|
1732
|
+
}
|
|
1733
|
+
setFreeSpins(fs) {
|
|
1734
|
+
this.state.freeSpins = fs;
|
|
1735
|
+
this.state.bonus = null;
|
|
1736
|
+
this.renderer.renderBar();
|
|
1737
|
+
}
|
|
1738
|
+
/** Generic bonus readout (adventure / hold-and-spin / respins). Sets a game-supplied label+value
|
|
1739
|
+
* override for the bar hero and folds `totalWin` into the shared accumulator. Pairs with
|
|
1740
|
+
* `setMode('bonus')`. `setFreeSpins()` clears the override back to the derived current/total. */
|
|
1741
|
+
setBonus(b) {
|
|
1742
|
+
this.state.bonus = { label: b.label, value: b.value };
|
|
1743
|
+
this.state.freeSpins = { ...this.state.freeSpins, totalWin: b.totalWin };
|
|
1744
|
+
this.renderer.renderBar();
|
|
1745
|
+
}
|
|
1746
|
+
setTheme(theme) {
|
|
1747
|
+
this.config.theme = theme;
|
|
1748
|
+
this.tokens = resolveTheme(theme);
|
|
1749
|
+
this.renderer.applyTheme(this.tokens);
|
|
1750
|
+
this.renderer.renderBar();
|
|
1751
|
+
}
|
|
1752
|
+
setLanguage(lang) {
|
|
1753
|
+
this.config.language = lang;
|
|
1754
|
+
this.i18n = createI18n({ language: lang, isSocial: this.config.isSocial });
|
|
1755
|
+
this.renderer.renderBar();
|
|
1756
|
+
}
|
|
1757
|
+
setSocial(isSocial) {
|
|
1758
|
+
this.config.isSocial = isSocial;
|
|
1759
|
+
this.i18n = createI18n({ language: this.config.language, isSocial });
|
|
1760
|
+
this.renderer.renderBar();
|
|
1761
|
+
}
|
|
1762
|
+
setLayout(layout) {
|
|
1763
|
+
if (layout === this.layout)
|
|
1764
|
+
return;
|
|
1765
|
+
this.layout = layout;
|
|
1766
|
+
this.renderer.setLayout(layout);
|
|
1767
|
+
this.renderer.renderBar();
|
|
1768
|
+
}
|
|
1663
1769
|
destroy() {
|
|
1664
1770
|
if (this.destroyed)
|
|
1665
1771
|
return Promise.resolve();
|
|
@@ -2922,8 +3028,12 @@ class TurboButton extends Container {
|
|
|
2922
3028
|
this.accent = accent;
|
|
2923
3029
|
this._paint(false);
|
|
2924
3030
|
}
|
|
2925
|
-
measureSize() {
|
|
2926
|
-
|
|
3031
|
+
measureSize() {
|
|
3032
|
+
return { w: this.box, h: this.box };
|
|
3033
|
+
}
|
|
3034
|
+
setLayoutSize() {
|
|
3035
|
+
/* fixed */
|
|
3036
|
+
}
|
|
2927
3037
|
}
|
|
2928
3038
|
/** A readout in the bar (white Oswald value, plaque-label caption, no shadow). */
|
|
2929
3039
|
function readout(host, label, value, opts = {}) {
|
|
@@ -2971,13 +3081,19 @@ class BottomBar extends Container {
|
|
|
2971
3081
|
buildWide() {
|
|
2972
3082
|
const { state, tokens } = this.host;
|
|
2973
3083
|
const isBase = state.mode === 'base';
|
|
2974
|
-
const isFS = state.mode === 'freeSpins';
|
|
3084
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
2975
3085
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
2976
3086
|
// BUY BONUS — floats outside-left of the panel (base only)
|
|
2977
|
-
this.buy = isBase ? this.buildBuy(BUY_W, 13, 3) ?? undefined : undefined;
|
|
3087
|
+
this.buy = isBase ? (this.buildBuy(BUY_W, 13, 3) ?? undefined) : undefined;
|
|
2978
3088
|
// LEFT info group: menu · balance · (Total win) · (Win)
|
|
2979
3089
|
const left = new FlexBox({ direction: 'row', align: 'center', gap: ZONE_GAP });
|
|
2980
|
-
left.add(new IconButton('menu', {
|
|
3090
|
+
left.add(new IconButton('menu', {
|
|
3091
|
+
size: 36,
|
|
3092
|
+
glyph: 30,
|
|
3093
|
+
color: '#ffffff',
|
|
3094
|
+
hover: tokens.accent,
|
|
3095
|
+
onTap: () => this.host.actions.openMenu(),
|
|
3096
|
+
}));
|
|
2981
3097
|
if (!state.replay) {
|
|
2982
3098
|
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance));
|
|
2983
3099
|
this.balanceValue = bal.valueText;
|
|
@@ -3016,16 +3132,30 @@ class BottomBar extends Container {
|
|
|
3016
3132
|
if (isBase) {
|
|
3017
3133
|
bet.eventMode = 'static';
|
|
3018
3134
|
bet.cursor = 'pointer';
|
|
3019
|
-
bet.on('pointertap', () => {
|
|
3020
|
-
this.
|
|
3135
|
+
bet.on('pointertap', () => {
|
|
3136
|
+
if (!this.betLocked())
|
|
3137
|
+
this.host.actions.openBetPicker();
|
|
3138
|
+
});
|
|
3021
3139
|
}
|
|
3022
3140
|
const betGroup = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
|
|
3023
3141
|
betGroup.add(bet);
|
|
3024
3142
|
if (isBase) {
|
|
3025
3143
|
// plain (borderless) +/- icons, bolder — stacked
|
|
3026
3144
|
const step = new FlexBox({ direction: 'column', align: 'center', gap: 2 });
|
|
3027
|
-
this.betUp = new IconButton('plus', {
|
|
3028
|
-
|
|
3145
|
+
this.betUp = new IconButton('plus', {
|
|
3146
|
+
size: 24,
|
|
3147
|
+
glyph: 22,
|
|
3148
|
+
color: '#ffffff',
|
|
3149
|
+
hover: tokens.accent,
|
|
3150
|
+
onTap: () => this.onBet(1),
|
|
3151
|
+
});
|
|
3152
|
+
this.betDown = new IconButton('minus', {
|
|
3153
|
+
size: 24,
|
|
3154
|
+
glyph: 22,
|
|
3155
|
+
color: '#ffffff',
|
|
3156
|
+
hover: tokens.accent,
|
|
3157
|
+
onTap: () => this.onBet(-1),
|
|
3158
|
+
});
|
|
3029
3159
|
step.add(this.betUp);
|
|
3030
3160
|
step.add(this.betDown);
|
|
3031
3161
|
betGroup.add(step);
|
|
@@ -3033,15 +3163,28 @@ class BottomBar extends Container {
|
|
|
3033
3163
|
const spinWrap = new FlexBox({ direction: 'row', align: 'center', gap: 8 });
|
|
3034
3164
|
if (isBase && config.features.autoplay) {
|
|
3035
3165
|
this.autoBtn = new IconButton('autoplay', {
|
|
3036
|
-
size: DISC,
|
|
3037
|
-
|
|
3166
|
+
size: DISC,
|
|
3167
|
+
glyph: 25,
|
|
3168
|
+
disc: tokens.btn,
|
|
3169
|
+
color: tokens.btnInk,
|
|
3170
|
+
hover: tokens.accent,
|
|
3171
|
+
activeColor: tokens.accent,
|
|
3172
|
+
active: state.autoplay.active,
|
|
3173
|
+
onTap: () => this.onAutoplay(),
|
|
3038
3174
|
});
|
|
3039
3175
|
if (state.autoplay.active)
|
|
3040
3176
|
this.autoBtn.setGlow(true);
|
|
3041
3177
|
spinWrap.add(this.autoBtn);
|
|
3042
3178
|
}
|
|
3043
3179
|
if (isBase) {
|
|
3044
|
-
this.spin = new SpinDisc({
|
|
3180
|
+
this.spin = new SpinDisc({
|
|
3181
|
+
size: SPIN,
|
|
3182
|
+
glyph: 65,
|
|
3183
|
+
tokens,
|
|
3184
|
+
ticker: this.host.ticker,
|
|
3185
|
+
onSpin: () => this.host.actions.spin(),
|
|
3186
|
+
onStop: () => this.stopAutoplay(),
|
|
3187
|
+
});
|
|
3045
3188
|
if (state.autoplay.active)
|
|
3046
3189
|
this.spin.setAutoplay(true, state.autoplay.remaining);
|
|
3047
3190
|
if (state.busy)
|
|
@@ -3050,13 +3193,23 @@ class BottomBar extends Container {
|
|
|
3050
3193
|
}
|
|
3051
3194
|
else if (showFsBlocks) {
|
|
3052
3195
|
const fs = state.freeSpins;
|
|
3053
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
3054
|
-
spinWrap.add(new FsHero({
|
|
3196
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
3197
|
+
spinWrap.add(new FsHero({
|
|
3198
|
+
label: this.host.t(state.bonus?.label ?? 'Free spins'),
|
|
3199
|
+
value: fsText,
|
|
3200
|
+
tokens,
|
|
3201
|
+
height: SPIN,
|
|
3202
|
+
}));
|
|
3055
3203
|
}
|
|
3056
3204
|
if (config.features.turbo > 0) {
|
|
3057
3205
|
this.turboBtn = new TurboButton({
|
|
3058
|
-
size: DISC,
|
|
3059
|
-
|
|
3206
|
+
size: DISC,
|
|
3207
|
+
glyph: 19,
|
|
3208
|
+
discFill: tokens.btn,
|
|
3209
|
+
discBorder: 2,
|
|
3210
|
+
accent: tokens.accent,
|
|
3211
|
+
level: state.turbo,
|
|
3212
|
+
onTap: () => this.onTurbo(),
|
|
3060
3213
|
});
|
|
3061
3214
|
spinWrap.add(this.turboBtn);
|
|
3062
3215
|
}
|
|
@@ -3074,21 +3227,49 @@ class BottomBar extends Container {
|
|
|
3074
3227
|
const feature = state.activeFeature;
|
|
3075
3228
|
if (feature) {
|
|
3076
3229
|
const accent = effectiveAccent(feature);
|
|
3077
|
-
return new BuyBonusBadge({
|
|
3230
|
+
return new BuyBonusBadge({
|
|
3231
|
+
size,
|
|
3232
|
+
fontSize,
|
|
3233
|
+
border,
|
|
3234
|
+
bg: accent,
|
|
3235
|
+
fg: contrastText(accent),
|
|
3236
|
+
label: this.host.t('DISABLE'),
|
|
3237
|
+
tokens,
|
|
3238
|
+
ticker: this.host.ticker,
|
|
3239
|
+
onTap: () => this.host.actions.deactivateFeature(),
|
|
3240
|
+
});
|
|
3078
3241
|
}
|
|
3079
|
-
return new BuyBonusBadge({
|
|
3242
|
+
return new BuyBonusBadge({
|
|
3243
|
+
size,
|
|
3244
|
+
border,
|
|
3245
|
+
bg: tokens.accent,
|
|
3246
|
+
icon: 'ticket',
|
|
3247
|
+
iconSize: size * 0.55,
|
|
3248
|
+
iconColor: tokens.btnInk,
|
|
3249
|
+
label: '',
|
|
3250
|
+
tokens,
|
|
3251
|
+
ticker: this.host.ticker,
|
|
3252
|
+
onTap: () => this.host.actions.openBuyBonus(),
|
|
3253
|
+
});
|
|
3080
3254
|
}
|
|
3081
3255
|
// ── mobile / portrait ─────────────────────────────────────────────────────
|
|
3082
3256
|
buildMobile() {
|
|
3083
3257
|
const { state, config, tokens } = this.host;
|
|
3084
3258
|
const isBase = state.mode === 'base';
|
|
3085
|
-
const isFS = state.mode === 'freeSpins';
|
|
3259
|
+
const isFS = state.mode === 'freeSpins' || state.mode === 'bonus';
|
|
3086
3260
|
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
3087
3261
|
const W = this.host.screenW - 2 * M_SIDE;
|
|
3088
3262
|
// hero (centre): SPIN in base, FS counter in free spins
|
|
3089
3263
|
let hero;
|
|
3090
3264
|
if (isBase) {
|
|
3091
|
-
this.spin = new SpinDisc({
|
|
3265
|
+
this.spin = new SpinDisc({
|
|
3266
|
+
size: SPIN,
|
|
3267
|
+
glyph: 65,
|
|
3268
|
+
tokens,
|
|
3269
|
+
ticker: this.host.ticker,
|
|
3270
|
+
onSpin: () => this.host.actions.spin(),
|
|
3271
|
+
onStop: () => this.stopAutoplay(),
|
|
3272
|
+
});
|
|
3092
3273
|
if (state.autoplay.active)
|
|
3093
3274
|
this.spin.setAutoplay(true, state.autoplay.remaining);
|
|
3094
3275
|
if (state.busy)
|
|
@@ -3097,25 +3278,58 @@ class BottomBar extends Container {
|
|
|
3097
3278
|
}
|
|
3098
3279
|
else if (showFsBlocks) {
|
|
3099
3280
|
const fs = state.freeSpins;
|
|
3100
|
-
const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}
|
|
3101
|
-
hero = new FsHero({
|
|
3281
|
+
const fsText = state.bonus?.value ?? (fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`);
|
|
3282
|
+
hero = new FsHero({
|
|
3283
|
+
label: this.host.t(state.bonus?.label ?? 'Free spins'),
|
|
3284
|
+
value: fsText,
|
|
3285
|
+
tokens,
|
|
3286
|
+
height: SPIN,
|
|
3287
|
+
});
|
|
3102
3288
|
}
|
|
3103
|
-
const menu = new IconButton('menu', {
|
|
3289
|
+
const menu = new IconButton('menu', {
|
|
3290
|
+
size: 40,
|
|
3291
|
+
glyph: 26,
|
|
3292
|
+
color: '#ffffff',
|
|
3293
|
+
hover: tokens.accent,
|
|
3294
|
+
onTap: () => this.host.actions.openMenu(),
|
|
3295
|
+
});
|
|
3104
3296
|
// autoplay is a base-mode control only — hidden in free spins / replay (matches the DOM bar)
|
|
3105
3297
|
if (isBase && config.features.autoplay) {
|
|
3106
|
-
this.autoBtn = new IconButton('autoplay', {
|
|
3298
|
+
this.autoBtn = new IconButton('autoplay', {
|
|
3299
|
+
size: 40,
|
|
3300
|
+
glyph: 26,
|
|
3301
|
+
color: '#ffffff',
|
|
3302
|
+
hover: tokens.accent,
|
|
3303
|
+
activeColor: tokens.accent,
|
|
3304
|
+
active: state.autoplay.active,
|
|
3305
|
+
onTap: () => this.onAutoplay(),
|
|
3306
|
+
});
|
|
3107
3307
|
if (state.autoplay.active)
|
|
3108
3308
|
this.autoBtn.setGlow(true);
|
|
3109
3309
|
}
|
|
3110
3310
|
if (config.features.turbo > 0) {
|
|
3111
3311
|
this.turboBtn = new TurboButton({
|
|
3112
|
-
size: 40,
|
|
3113
|
-
|
|
3312
|
+
size: 40,
|
|
3313
|
+
glyph: 22,
|
|
3314
|
+
discFill: tokens.btn,
|
|
3315
|
+
discBorder: 2,
|
|
3316
|
+
accent: tokens.accent,
|
|
3317
|
+
level: state.turbo,
|
|
3318
|
+
onTap: () => this.onTurbo(),
|
|
3114
3319
|
});
|
|
3115
3320
|
}
|
|
3116
|
-
const buy = isBase ? this.buildBuy(M_BUY, 9, 2) ?? undefined : undefined;
|
|
3321
|
+
const buy = isBase ? (this.buildBuy(M_BUY, 9, 2) ?? undefined) : undefined;
|
|
3117
3322
|
// level 1 — controls bar (dark)
|
|
3118
|
-
const controls = new FlexBox({
|
|
3323
|
+
const controls = new FlexBox({
|
|
3324
|
+
direction: 'row',
|
|
3325
|
+
align: 'center',
|
|
3326
|
+
justify: 'space-between',
|
|
3327
|
+
gap: 0,
|
|
3328
|
+
width: W,
|
|
3329
|
+
height: M_CTRL_H,
|
|
3330
|
+
padding: { left: 18, right: 18 },
|
|
3331
|
+
background: { fill: tokens.bar, radius: 16 },
|
|
3332
|
+
});
|
|
3119
3333
|
if (isBase && hero) {
|
|
3120
3334
|
const SIDE = 12;
|
|
3121
3335
|
const lz = new FlexBox({ direction: 'row', align: 'center', gap: SIDE, justify: 'start' });
|
|
@@ -3151,7 +3365,12 @@ class BottomBar extends Container {
|
|
|
3151
3365
|
const heroW = hero ? (heroSized.measureSize?.().w ?? hero.getLocalBounds().width) : 0;
|
|
3152
3366
|
const fixed = ICON /* menu */ + (this.autoBtn ? ICON : 0) + heroW + (this.turboBtn ? ICON : 0);
|
|
3153
3367
|
const twSlot = Math.max(60, W - 2 * PAD - fixed - GAPS);
|
|
3154
|
-
controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
|
|
3368
|
+
controls.add(readout(this.host, 'Total win', this.host.fmtWin(state.freeSpins.totalWin), {
|
|
3369
|
+
color: '#ffffff',
|
|
3370
|
+
muted: '#ffffff',
|
|
3371
|
+
align: 'center',
|
|
3372
|
+
maxWidth: twSlot,
|
|
3373
|
+
}));
|
|
3155
3374
|
}
|
|
3156
3375
|
if (this.turboBtn)
|
|
3157
3376
|
controls.add(this.turboBtn);
|
|
@@ -3162,36 +3381,76 @@ class BottomBar extends Container {
|
|
|
3162
3381
|
const feature = state.activeFeature;
|
|
3163
3382
|
const betShown = feature ? state.bet * feature.priceMultiplier : state.bet;
|
|
3164
3383
|
if (isBase) {
|
|
3165
|
-
this.betDown = new IconButton('minus', {
|
|
3384
|
+
this.betDown = new IconButton('minus', {
|
|
3385
|
+
size: 26,
|
|
3386
|
+
glyph: 18,
|
|
3387
|
+
color: '#ffffff',
|
|
3388
|
+
hover: tokens.accent,
|
|
3389
|
+
onTap: () => this.onBet(-1),
|
|
3390
|
+
});
|
|
3166
3391
|
betGroup.add(this.betDown);
|
|
3167
3392
|
}
|
|
3168
|
-
const bet = new Readout({
|
|
3393
|
+
const bet = new Readout({
|
|
3394
|
+
label: this.host.t('Bet'),
|
|
3395
|
+
value: this.host.fmt(betShown),
|
|
3396
|
+
muted: feature ? effectiveAccent(feature) : tokens.plaqueLabel,
|
|
3397
|
+
fg: feature ? effectiveAccent(feature) : '#ffffff',
|
|
3398
|
+
valueSize: 11,
|
|
3399
|
+
align: 'center',
|
|
3400
|
+
fixedWidth: 76,
|
|
3401
|
+
shadow: false,
|
|
3402
|
+
});
|
|
3169
3403
|
this.betReadout = bet;
|
|
3170
3404
|
if (isBase) {
|
|
3171
3405
|
bet.eventMode = 'static';
|
|
3172
3406
|
bet.cursor = 'pointer';
|
|
3173
|
-
bet.on('pointertap', () => {
|
|
3174
|
-
this.
|
|
3407
|
+
bet.on('pointertap', () => {
|
|
3408
|
+
if (!this.betLocked())
|
|
3409
|
+
this.host.actions.openBetPicker();
|
|
3410
|
+
});
|
|
3175
3411
|
}
|
|
3176
3412
|
betGroup.add(bet);
|
|
3177
3413
|
if (isBase) {
|
|
3178
|
-
this.betUp = new IconButton('plus', {
|
|
3414
|
+
this.betUp = new IconButton('plus', {
|
|
3415
|
+
size: 26,
|
|
3416
|
+
glyph: 18,
|
|
3417
|
+
color: '#ffffff',
|
|
3418
|
+
hover: tokens.accent,
|
|
3419
|
+
onTap: () => this.onBet(1),
|
|
3420
|
+
});
|
|
3179
3421
|
betGroup.add(this.betUp);
|
|
3180
3422
|
}
|
|
3181
3423
|
betGroup.layout();
|
|
3182
3424
|
const innerW = W - 28; // pad 14 each side
|
|
3183
3425
|
const slot = Math.max(40, (innerW - betGroup.outerWidth - 20) / 2);
|
|
3184
|
-
const info = new FlexBox({
|
|
3426
|
+
const info = new FlexBox({
|
|
3427
|
+
direction: 'row',
|
|
3428
|
+
align: 'center',
|
|
3429
|
+
justify: 'space-between',
|
|
3430
|
+
width: W,
|
|
3431
|
+
height: M_INFO_H,
|
|
3432
|
+
padding: { left: 14, right: 14 },
|
|
3433
|
+
gap: 10,
|
|
3434
|
+
background: { fill: tokens.plaqueGlass, radius: 12 },
|
|
3435
|
+
});
|
|
3185
3436
|
// replay is a read-only historical round — no real balance to show (sticky `replay`, so it stays
|
|
3186
3437
|
// hidden through a replay's free-spins phase too); matches the DOM bar.
|
|
3187
3438
|
if (!state.replay) {
|
|
3188
|
-
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
|
|
3439
|
+
const bal = readout(this.host, 'Balance', this.host.fmt(state.balance), {
|
|
3440
|
+
valueSize: 11,
|
|
3441
|
+
align: 'left',
|
|
3442
|
+
fixedWidth: slot,
|
|
3443
|
+
});
|
|
3189
3444
|
this.balanceValue = bal.valueText;
|
|
3190
3445
|
info.add(bal);
|
|
3191
3446
|
}
|
|
3192
3447
|
info.add(betGroup);
|
|
3193
3448
|
// WIN always present (no jiggle on win↔0)
|
|
3194
|
-
const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
|
|
3449
|
+
const win = readout(this.host, 'Win', this.host.fmtWin(state.win), {
|
|
3450
|
+
valueSize: 11,
|
|
3451
|
+
align: 'right',
|
|
3452
|
+
fixedWidth: slot,
|
|
3453
|
+
});
|
|
3195
3454
|
this.winValue = win.valueText;
|
|
3196
3455
|
info.add(win);
|
|
3197
3456
|
info.layout();
|
|
@@ -3282,7 +3541,7 @@ class BottomBar extends Container {
|
|
|
3282
3541
|
left.position.set(panelX + PANEL_PAD, panelCenterY - left.outerHeight / 2);
|
|
3283
3542
|
right.position.set(panelRight - PANEL_PAD - right.outerWidth, panelCenterY - right.outerHeight / 2);
|
|
3284
3543
|
this.inner.scale.set(s);
|
|
3285
|
-
this.inner.position.set((W - barW * s) / 2,
|
|
3544
|
+
this.inner.position.set((W - barW * s) / 2, H - WIDE_PAD_BOTTOM - SPIN * s);
|
|
3286
3545
|
this.measureFootprint();
|
|
3287
3546
|
}
|
|
3288
3547
|
applyFitMobile() {
|
|
@@ -4079,28 +4338,69 @@ function sectionHotkeys(host, title, width) {
|
|
|
4079
4338
|
visible.forEach((r, i) => {
|
|
4080
4339
|
if (i > 0)
|
|
4081
4340
|
sec.add(hairline$1(host, inner));
|
|
4082
|
-
sec.add(hkRow(host, r));
|
|
4341
|
+
sec.add(hkRow(host, r, inner));
|
|
4083
4342
|
});
|
|
4084
4343
|
return sec;
|
|
4085
4344
|
}
|
|
4086
|
-
/** Render a single hotkey row: keycap chip(s) on the left, action name on the right.
|
|
4345
|
+
/** Render a single hotkey row: keycap chip(s) on the left, action name on the right. The row is
|
|
4346
|
+
* constrained to `inner` (chips left, label right). On narrow widths the chip combos stack onto
|
|
4347
|
+
* their own lines and the label wraps within the leftover width, so nothing clips past the plaque
|
|
4348
|
+
* — the Pixi analogue of the HTML `.ge-gi-hk-chips { flex:0 1 auto; flex-wrap }` + label wrap. */
|
|
4087
4349
|
function hkRow(host, r, inner) {
|
|
4088
|
-
const
|
|
4089
|
-
//
|
|
4090
|
-
const chipsCol = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4350
|
+
const GAP = 14;
|
|
4351
|
+
const LABEL_MIN = 92; // reserve at least this much for the action label before wrapping the chips
|
|
4091
4352
|
const chipKeys = buildChipKeys(r);
|
|
4353
|
+
// Lay the chips on one line and measure. If that leaves too little room for the label, stack the
|
|
4354
|
+
// combos (split at the '/') onto their own lines so the chips give up horizontal width.
|
|
4355
|
+
let chips = chipLine(host, chipKeys);
|
|
4356
|
+
let chipsW = chips.measureSize().w;
|
|
4357
|
+
if (chipsW > inner - GAP - LABEL_MIN) {
|
|
4358
|
+
chips = chipCombos(host, chipKeys);
|
|
4359
|
+
chipsW = chips.measureSize().w;
|
|
4360
|
+
}
|
|
4361
|
+
// The label takes the leftover width and WRAPS within it (never clips); right-aligned to sit
|
|
4362
|
+
// against the plaque edge like the DOM `.ge-gi-hk-tx`.
|
|
4363
|
+
const labelW = Math.max(LABEL_MIN, inner - chipsW - GAP);
|
|
4364
|
+
const nameText = makeText(host.t(r.name), { size: 15, weight: '700', color: '#ffffff', wrapWidth: labelW, align: 'right' });
|
|
4365
|
+
const row = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', gap: GAP, padding: { top: 8, bottom: 8 }, width: inner });
|
|
4366
|
+
row.add(chips);
|
|
4367
|
+
row.add(nameText);
|
|
4368
|
+
return row;
|
|
4369
|
+
}
|
|
4370
|
+
/** Chips on a single row, with '/' separators between the combos (as flattened by buildChipKeys). */
|
|
4371
|
+
function chipLine(host, chipKeys) {
|
|
4372
|
+
const line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4373
|
+
for (const key of chipKeys) {
|
|
4374
|
+
if (key === '/')
|
|
4375
|
+
line.add(makeText('/', { size: 12, weight: '500', color: host.tokens.plaqueLabel }));
|
|
4376
|
+
else
|
|
4377
|
+
line.add(keycap(host, key));
|
|
4378
|
+
}
|
|
4379
|
+
return line;
|
|
4380
|
+
}
|
|
4381
|
+
/** Chips stacked as combos — split at the '/' separators, one combo per line (drops the inline
|
|
4382
|
+
* '/', which reads as a line break once the combos are stacked). */
|
|
4383
|
+
function chipCombos(host, chipKeys) {
|
|
4384
|
+
const col = new FlexBox({ direction: 'column', align: 'start', gap: 4 });
|
|
4385
|
+
let line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4386
|
+
let filled = false;
|
|
4387
|
+
const flush = () => {
|
|
4388
|
+
if (!filled)
|
|
4389
|
+
return;
|
|
4390
|
+
col.add(line);
|
|
4391
|
+
line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4392
|
+
filled = false;
|
|
4393
|
+
};
|
|
4092
4394
|
for (const key of chipKeys) {
|
|
4093
4395
|
if (key === '/') {
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
else {
|
|
4097
|
-
chipsCol.add(keycap(host, key));
|
|
4396
|
+
flush();
|
|
4397
|
+
continue;
|
|
4098
4398
|
}
|
|
4399
|
+
line.add(keycap(host, key));
|
|
4400
|
+
filled = true;
|
|
4099
4401
|
}
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
row.add(nameText);
|
|
4103
|
-
return row;
|
|
4402
|
+
flush();
|
|
4403
|
+
return col;
|
|
4104
4404
|
}
|
|
4105
4405
|
/** Flatten a row's chip list into display tokens — for bet rows, produce: Shift ↑ / Shift = style. */
|
|
4106
4406
|
function buildChipKeys(r) {
|
|
@@ -5266,8 +5566,27 @@ class PickerModal extends CardModal {
|
|
|
5266
5566
|
}
|
|
5267
5567
|
}
|
|
5268
5568
|
/** A centred picker (chips grid + accent Confirm) on the shared card modal. */
|
|
5569
|
+
/** A ScrollBox that reports a fixed box to the FlexBox layout — so a capped, scrolling grid slots
|
|
5570
|
+
* into the card body like any other sized child (measured/positioned by its viewport, not its
|
|
5571
|
+
* full content bounds). */
|
|
5572
|
+
class SizedScrollBox extends ScrollBox {
|
|
5573
|
+
boxW;
|
|
5574
|
+
boxH;
|
|
5575
|
+
constructor(boxW, boxH, canvas) {
|
|
5576
|
+
super(canvas);
|
|
5577
|
+
this.boxW = boxW;
|
|
5578
|
+
this.boxH = boxH;
|
|
5579
|
+
this.setViewport(boxW, boxH);
|
|
5580
|
+
}
|
|
5581
|
+
measureSize() {
|
|
5582
|
+
return { w: this.boxW, h: this.boxH };
|
|
5583
|
+
}
|
|
5584
|
+
setLayoutSize() {
|
|
5585
|
+
/* fixed viewport — ignore layout-imposed sizing */
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5269
5588
|
function buildSheet(host, opts) {
|
|
5270
|
-
const
|
|
5589
|
+
const maxColumns = typeof opts.columns === 'number'
|
|
5271
5590
|
? opts.columns
|
|
5272
5591
|
: host.layout === 'mobile' ? opts.columns.mobile : opts.columns.wide;
|
|
5273
5592
|
let selected = opts.selected;
|
|
@@ -5319,27 +5638,51 @@ function buildSheet(host, opts) {
|
|
|
5319
5638
|
const em = modal.emSize;
|
|
5320
5639
|
const gap = 0.65 * em;
|
|
5321
5640
|
const innerW = modal.cardWidth - 2.4 * em;
|
|
5641
|
+
// Build the chips first (at natural width) so autoFit can measure the widest label.
|
|
5642
|
+
for (let i = 0; i < opts.choices.length; i++) {
|
|
5643
|
+
const c = opts.choices[i];
|
|
5644
|
+
const chipIndex = i;
|
|
5645
|
+
chips.push(new Chip(host, c.id, c.label, chipIndex === focusIndex, em, (id) => {
|
|
5646
|
+
const idx = opts.choices.findIndex((ch) => ch.id === id);
|
|
5647
|
+
if (idx >= 0)
|
|
5648
|
+
setHighlight(idx);
|
|
5649
|
+
}));
|
|
5650
|
+
}
|
|
5651
|
+
// Column count: with autoFit, drop below the max until the widest label fits. Short labels
|
|
5652
|
+
// (a normal currency) exceed the max and clamp back to it — so the compact 6-wide layout is
|
|
5653
|
+
// preserved and only wide currencies reflow to fewer columns.
|
|
5654
|
+
let columns = maxColumns;
|
|
5655
|
+
if (opts.autoFit) {
|
|
5656
|
+
const widest = chips.reduce((m, ch) => Math.max(m, ch.measureSize().w), 0);
|
|
5657
|
+
const fitCols = Math.floor((innerW + gap) / (widest + gap));
|
|
5658
|
+
columns = Math.max(1, Math.min(maxColumns, fitCols));
|
|
5659
|
+
}
|
|
5322
5660
|
const colW = (innerW - gap * (columns - 1)) / columns;
|
|
5323
5661
|
const grid = new FlexBox({ direction: 'column', align: 'start', gap });
|
|
5324
|
-
for (let i = 0; i <
|
|
5325
|
-
const rowChoices = opts.choices.slice(i, i + columns);
|
|
5662
|
+
for (let i = 0; i < chips.length; i += columns) {
|
|
5326
5663
|
const row = new FlexBox({ direction: 'row', align: 'center', gap });
|
|
5327
|
-
for (let j = 0; j <
|
|
5328
|
-
const
|
|
5329
|
-
const chipIndex = i + j;
|
|
5330
|
-
const chip = new Chip(host, c.id, c.label, chipIndex === focusIndex, em, (id) => {
|
|
5331
|
-
const idx = opts.choices.findIndex((ch) => ch.id === id);
|
|
5332
|
-
if (idx >= 0)
|
|
5333
|
-
setHighlight(idx);
|
|
5334
|
-
});
|
|
5664
|
+
for (let j = 0; j < columns && i + j < chips.length; j++) {
|
|
5665
|
+
const chip = chips[i + j];
|
|
5335
5666
|
chip.setLayoutSize(colW, undefined);
|
|
5336
|
-
chips.push(chip);
|
|
5337
5667
|
row.add(chip);
|
|
5338
5668
|
}
|
|
5339
|
-
row.layout();
|
|
5340
5669
|
grid.add(row);
|
|
5341
5670
|
}
|
|
5342
|
-
|
|
5671
|
+
// Cap the grid height and scroll when the ladder overflows — the Pixi analogue of the HTML
|
|
5672
|
+
// shell's `max-height:min(50vh,28em); overflow-y:auto`. (Mask hit-testing clips to the viewport
|
|
5673
|
+
// but still lets clicks through to chips inside it — verified against pixi.js 8.16's
|
|
5674
|
+
// EventBoundary.) When it fits, drop the scroll box entirely so nothing is masked needlessly.
|
|
5675
|
+
const gridH = grid.measureSize().h;
|
|
5676
|
+
const maxGridH = Math.min(host.screenH * 0.5, 28 * em);
|
|
5677
|
+
if (opts.autoFit && gridH > maxGridH) {
|
|
5678
|
+
const scroll = new SizedScrollBox(innerW, maxGridH, host.canvas);
|
|
5679
|
+
scroll.content.addChild(grid);
|
|
5680
|
+
scroll.refresh();
|
|
5681
|
+
modal.body.add(scroll);
|
|
5682
|
+
}
|
|
5683
|
+
else {
|
|
5684
|
+
modal.body.add(grid);
|
|
5685
|
+
}
|
|
5343
5686
|
modal.setActions([
|
|
5344
5687
|
{
|
|
5345
5688
|
label: opts.confirmLabel,
|
|
@@ -5356,6 +5699,7 @@ function openBetPicker(host) {
|
|
|
5356
5699
|
tag: 'bet-modal',
|
|
5357
5700
|
title: host.t('Bet'),
|
|
5358
5701
|
columns: { wide: 6, mobile: 3 },
|
|
5702
|
+
autoFit: true, // reflow columns + scroll when a wide currency makes the labels grow
|
|
5359
5703
|
maxEm: 44, // wider card to fit 6 chips/row
|
|
5360
5704
|
confirmLabel: host.t('Confirm'),
|
|
5361
5705
|
choices: host.state.availableBets.map((b) => ({ id: String(b), label: host.fmt(b) })),
|