@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.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() {
|
|
@@ -4081,28 +4340,69 @@ function sectionHotkeys(host, title, width) {
|
|
|
4081
4340
|
visible.forEach((r, i) => {
|
|
4082
4341
|
if (i > 0)
|
|
4083
4342
|
sec.add(hairline$1(host, inner));
|
|
4084
|
-
sec.add(hkRow(host, r));
|
|
4343
|
+
sec.add(hkRow(host, r, inner));
|
|
4085
4344
|
});
|
|
4086
4345
|
return sec;
|
|
4087
4346
|
}
|
|
4088
|
-
/** Render a single hotkey row: keycap chip(s) on the left, action name on the right.
|
|
4347
|
+
/** Render a single hotkey row: keycap chip(s) on the left, action name on the right. The row is
|
|
4348
|
+
* constrained to `inner` (chips left, label right). On narrow widths the chip combos stack onto
|
|
4349
|
+
* their own lines and the label wraps within the leftover width, so nothing clips past the plaque
|
|
4350
|
+
* — the Pixi analogue of the HTML `.ge-gi-hk-chips { flex:0 1 auto; flex-wrap }` + label wrap. */
|
|
4089
4351
|
function hkRow(host, r, inner) {
|
|
4090
|
-
const
|
|
4091
|
-
//
|
|
4092
|
-
const chipsCol = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4352
|
+
const GAP = 14;
|
|
4353
|
+
const LABEL_MIN = 92; // reserve at least this much for the action label before wrapping the chips
|
|
4093
4354
|
const chipKeys = buildChipKeys(r);
|
|
4355
|
+
// Lay the chips on one line and measure. If that leaves too little room for the label, stack the
|
|
4356
|
+
// combos (split at the '/') onto their own lines so the chips give up horizontal width.
|
|
4357
|
+
let chips = chipLine(host, chipKeys);
|
|
4358
|
+
let chipsW = chips.measureSize().w;
|
|
4359
|
+
if (chipsW > inner - GAP - LABEL_MIN) {
|
|
4360
|
+
chips = chipCombos(host, chipKeys);
|
|
4361
|
+
chipsW = chips.measureSize().w;
|
|
4362
|
+
}
|
|
4363
|
+
// The label takes the leftover width and WRAPS within it (never clips); right-aligned to sit
|
|
4364
|
+
// against the plaque edge like the DOM `.ge-gi-hk-tx`.
|
|
4365
|
+
const labelW = Math.max(LABEL_MIN, inner - chipsW - GAP);
|
|
4366
|
+
const nameText = makeText(host.t(r.name), { size: 15, weight: '700', color: '#ffffff', wrapWidth: labelW, align: 'right' });
|
|
4367
|
+
const row = new FlexBox({ direction: 'row', align: 'center', justify: 'space-between', gap: GAP, padding: { top: 8, bottom: 8 }, width: inner });
|
|
4368
|
+
row.add(chips);
|
|
4369
|
+
row.add(nameText);
|
|
4370
|
+
return row;
|
|
4371
|
+
}
|
|
4372
|
+
/** Chips on a single row, with '/' separators between the combos (as flattened by buildChipKeys). */
|
|
4373
|
+
function chipLine(host, chipKeys) {
|
|
4374
|
+
const line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4375
|
+
for (const key of chipKeys) {
|
|
4376
|
+
if (key === '/')
|
|
4377
|
+
line.add(makeText('/', { size: 12, weight: '500', color: host.tokens.plaqueLabel }));
|
|
4378
|
+
else
|
|
4379
|
+
line.add(keycap(host, key));
|
|
4380
|
+
}
|
|
4381
|
+
return line;
|
|
4382
|
+
}
|
|
4383
|
+
/** Chips stacked as combos — split at the '/' separators, one combo per line (drops the inline
|
|
4384
|
+
* '/', which reads as a line break once the combos are stacked). */
|
|
4385
|
+
function chipCombos(host, chipKeys) {
|
|
4386
|
+
const col = new FlexBox({ direction: 'column', align: 'start', gap: 4 });
|
|
4387
|
+
let line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4388
|
+
let filled = false;
|
|
4389
|
+
const flush = () => {
|
|
4390
|
+
if (!filled)
|
|
4391
|
+
return;
|
|
4392
|
+
col.add(line);
|
|
4393
|
+
line = new FlexBox({ direction: 'row', align: 'center', gap: 4 });
|
|
4394
|
+
filled = false;
|
|
4395
|
+
};
|
|
4094
4396
|
for (const key of chipKeys) {
|
|
4095
4397
|
if (key === '/') {
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
else {
|
|
4099
|
-
chipsCol.add(keycap(host, key));
|
|
4398
|
+
flush();
|
|
4399
|
+
continue;
|
|
4100
4400
|
}
|
|
4401
|
+
line.add(keycap(host, key));
|
|
4402
|
+
filled = true;
|
|
4101
4403
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
row.add(nameText);
|
|
4105
|
-
return row;
|
|
4404
|
+
flush();
|
|
4405
|
+
return col;
|
|
4106
4406
|
}
|
|
4107
4407
|
/** Flatten a row's chip list into display tokens — for bet rows, produce: Shift ↑ / Shift = style. */
|
|
4108
4408
|
function buildChipKeys(r) {
|
|
@@ -5268,8 +5568,27 @@ class PickerModal extends CardModal {
|
|
|
5268
5568
|
}
|
|
5269
5569
|
}
|
|
5270
5570
|
/** A centred picker (chips grid + accent Confirm) on the shared card modal. */
|
|
5571
|
+
/** A ScrollBox that reports a fixed box to the FlexBox layout — so a capped, scrolling grid slots
|
|
5572
|
+
* into the card body like any other sized child (measured/positioned by its viewport, not its
|
|
5573
|
+
* full content bounds). */
|
|
5574
|
+
class SizedScrollBox extends ScrollBox {
|
|
5575
|
+
boxW;
|
|
5576
|
+
boxH;
|
|
5577
|
+
constructor(boxW, boxH, canvas) {
|
|
5578
|
+
super(canvas);
|
|
5579
|
+
this.boxW = boxW;
|
|
5580
|
+
this.boxH = boxH;
|
|
5581
|
+
this.setViewport(boxW, boxH);
|
|
5582
|
+
}
|
|
5583
|
+
measureSize() {
|
|
5584
|
+
return { w: this.boxW, h: this.boxH };
|
|
5585
|
+
}
|
|
5586
|
+
setLayoutSize() {
|
|
5587
|
+
/* fixed viewport — ignore layout-imposed sizing */
|
|
5588
|
+
}
|
|
5589
|
+
}
|
|
5271
5590
|
function buildSheet(host, opts) {
|
|
5272
|
-
const
|
|
5591
|
+
const maxColumns = typeof opts.columns === 'number'
|
|
5273
5592
|
? opts.columns
|
|
5274
5593
|
: host.layout === 'mobile' ? opts.columns.mobile : opts.columns.wide;
|
|
5275
5594
|
let selected = opts.selected;
|
|
@@ -5321,27 +5640,51 @@ function buildSheet(host, opts) {
|
|
|
5321
5640
|
const em = modal.emSize;
|
|
5322
5641
|
const gap = 0.65 * em;
|
|
5323
5642
|
const innerW = modal.cardWidth - 2.4 * em;
|
|
5643
|
+
// Build the chips first (at natural width) so autoFit can measure the widest label.
|
|
5644
|
+
for (let i = 0; i < opts.choices.length; i++) {
|
|
5645
|
+
const c = opts.choices[i];
|
|
5646
|
+
const chipIndex = i;
|
|
5647
|
+
chips.push(new Chip(host, c.id, c.label, chipIndex === focusIndex, em, (id) => {
|
|
5648
|
+
const idx = opts.choices.findIndex((ch) => ch.id === id);
|
|
5649
|
+
if (idx >= 0)
|
|
5650
|
+
setHighlight(idx);
|
|
5651
|
+
}));
|
|
5652
|
+
}
|
|
5653
|
+
// Column count: with autoFit, drop below the max until the widest label fits. Short labels
|
|
5654
|
+
// (a normal currency) exceed the max and clamp back to it — so the compact 6-wide layout is
|
|
5655
|
+
// preserved and only wide currencies reflow to fewer columns.
|
|
5656
|
+
let columns = maxColumns;
|
|
5657
|
+
if (opts.autoFit) {
|
|
5658
|
+
const widest = chips.reduce((m, ch) => Math.max(m, ch.measureSize().w), 0);
|
|
5659
|
+
const fitCols = Math.floor((innerW + gap) / (widest + gap));
|
|
5660
|
+
columns = Math.max(1, Math.min(maxColumns, fitCols));
|
|
5661
|
+
}
|
|
5324
5662
|
const colW = (innerW - gap * (columns - 1)) / columns;
|
|
5325
5663
|
const grid = new FlexBox({ direction: 'column', align: 'start', gap });
|
|
5326
|
-
for (let i = 0; i <
|
|
5327
|
-
const rowChoices = opts.choices.slice(i, i + columns);
|
|
5664
|
+
for (let i = 0; i < chips.length; i += columns) {
|
|
5328
5665
|
const row = new FlexBox({ direction: 'row', align: 'center', gap });
|
|
5329
|
-
for (let j = 0; j <
|
|
5330
|
-
const
|
|
5331
|
-
const chipIndex = i + j;
|
|
5332
|
-
const chip = new Chip(host, c.id, c.label, chipIndex === focusIndex, em, (id) => {
|
|
5333
|
-
const idx = opts.choices.findIndex((ch) => ch.id === id);
|
|
5334
|
-
if (idx >= 0)
|
|
5335
|
-
setHighlight(idx);
|
|
5336
|
-
});
|
|
5666
|
+
for (let j = 0; j < columns && i + j < chips.length; j++) {
|
|
5667
|
+
const chip = chips[i + j];
|
|
5337
5668
|
chip.setLayoutSize(colW, undefined);
|
|
5338
|
-
chips.push(chip);
|
|
5339
5669
|
row.add(chip);
|
|
5340
5670
|
}
|
|
5341
|
-
row.layout();
|
|
5342
5671
|
grid.add(row);
|
|
5343
5672
|
}
|
|
5344
|
-
|
|
5673
|
+
// Cap the grid height and scroll when the ladder overflows — the Pixi analogue of the HTML
|
|
5674
|
+
// shell's `max-height:min(50vh,28em); overflow-y:auto`. (Mask hit-testing clips to the viewport
|
|
5675
|
+
// but still lets clicks through to chips inside it — verified against pixi.js 8.16's
|
|
5676
|
+
// EventBoundary.) When it fits, drop the scroll box entirely so nothing is masked needlessly.
|
|
5677
|
+
const gridH = grid.measureSize().h;
|
|
5678
|
+
const maxGridH = Math.min(host.screenH * 0.5, 28 * em);
|
|
5679
|
+
if (opts.autoFit && gridH > maxGridH) {
|
|
5680
|
+
const scroll = new SizedScrollBox(innerW, maxGridH, host.canvas);
|
|
5681
|
+
scroll.content.addChild(grid);
|
|
5682
|
+
scroll.refresh();
|
|
5683
|
+
modal.body.add(scroll);
|
|
5684
|
+
}
|
|
5685
|
+
else {
|
|
5686
|
+
modal.body.add(grid);
|
|
5687
|
+
}
|
|
5345
5688
|
modal.setActions([
|
|
5346
5689
|
{
|
|
5347
5690
|
label: opts.confirmLabel,
|
|
@@ -5358,6 +5701,7 @@ function openBetPicker(host) {
|
|
|
5358
5701
|
tag: 'bet-modal',
|
|
5359
5702
|
title: host.t('Bet'),
|
|
5360
5703
|
columns: { wide: 6, mobile: 3 },
|
|
5704
|
+
autoFit: true, // reflow columns + scroll when a wide currency makes the labels grow
|
|
5361
5705
|
maxEm: 44, // wider card to fit 6 chips/row
|
|
5362
5706
|
confirmLabel: host.t('Confirm'),
|
|
5363
5707
|
choices: host.state.availableBets.map((b) => ({ id: String(b), label: host.fmt(b) })),
|