@energy8platform/platform-core 0.21.0 → 0.23.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/README.md +6 -4
- package/dist/index.cjs.js +58 -32
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +17 -6
- package/dist/index.esm.js +58 -32
- package/dist/index.esm.js.map +1 -1
- package/dist/shell.cjs.js +58 -32
- package/dist/shell.cjs.js.map +1 -1
- package/dist/shell.d.ts +17 -6
- package/dist/shell.esm.js +58 -32
- package/dist/shell.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/shell/GameShell.ts +5 -3
- package/src/shell/components/BottomBar.ts +20 -15
- package/src/shell/components/GameInfo.ts +1 -1
- package/src/shell/components/pickers.ts +17 -4
- package/src/shell/shell.css.ts +8 -4
- package/src/shell/state.ts +1 -1
- package/src/shell/theme.ts +3 -3
- package/src/shell/types.ts +13 -2
package/README.md
CHANGED
|
@@ -649,7 +649,8 @@ const shell = createGameShell({
|
|
|
649
649
|
gameInfo: { sections: [{ type: 'controls' }] }, // see "Game info" below
|
|
650
650
|
features: {
|
|
651
651
|
turbo: 3, // 0 = no turbo button, 1–3 = number of turbo levels
|
|
652
|
-
|
|
652
|
+
spacebar: true, // default true; set false to disable the Spacebar → spin shortcut
|
|
653
|
+
autoplay: {}, // null / omitted = off; {} = on; { maxCount: 100 } caps the picker
|
|
653
654
|
buyBonus: [
|
|
654
655
|
{ id: 'fs', type: 'bonus', title: 'Buy Free Spins', description: '10 free spins',
|
|
655
656
|
priceMultiplier: 100, volatility: 5 },
|
|
@@ -689,7 +690,7 @@ await removeGameShell();
|
|
|
689
690
|
| `balance` / `win` | `number` | Initial readouts. |
|
|
690
691
|
| `mode` | `'base' \| 'freeSpins' \| 'replay'` | Drives which bottom-bar variant renders. |
|
|
691
692
|
| `gameInfo` | `GameInfoContent` | Sections for the game-info overlay (see below). |
|
|
692
|
-
| `features` | `ShellFeatures` | `{ turbo: 0–3, autoplay, buyBonus: BonusOption[] \| false
|
|
693
|
+
| `features` | `ShellFeatures` | `{ turbo: 0–3, spacebar?, autoplay, buyBonus }`. `spacebar?: boolean` (default `true`) — `false` disables the Spacebar → spin shortcut. `autoplay: AutoplayConfig \| null` — `null`/omitted disables it; `{}` enables it; `{ maxCount }` caps the picker (drops ∞). `buyBonus: BonusOption[] \| false`. |
|
|
693
694
|
|
|
694
695
|
### Events (`shell.on(name, handler)`)
|
|
695
696
|
|
|
@@ -713,7 +714,7 @@ the previous value.
|
|
|
713
714
|
shell.setBalance(n); shell.setWin(n); shell.setBet(n);
|
|
714
715
|
shell.setBusy(true); // disables controls mid-spin
|
|
715
716
|
shell.setMode('freeSpins');
|
|
716
|
-
shell.setFreeSpins({ current: 1, total: 10, totalWin: 0
|
|
717
|
+
shell.setFreeSpins({ current: 1, total: 10, totalWin: 0 }); // Free Spins + Total Win bar readout
|
|
717
718
|
shell.setAutoplay({ active: true, remaining: 25 });
|
|
718
719
|
shell.setTurbo(2);
|
|
719
720
|
shell.setBuyBonusEnabled(false); // grey out BUY BONUS (e.g. insufficient balance)
|
|
@@ -790,7 +791,8 @@ BUY BONUS control and a duotone icon set. The bottom bar **adapts by viewport**
|
|
|
790
791
|
`ResizeObserver` on the mount): landscape → one row scaled to fit, portrait → stacked mobile
|
|
791
792
|
layout; Settings / Game info / Buy bonus open as full-screen overlays. Motion is minimal (press
|
|
792
793
|
feedback, money count-up, overlay fades) and respects `prefers-reduced-motion`. Spacebar triggers
|
|
793
|
-
a spin in base mode (ignored while busy, in autoplay,
|
|
794
|
+
a spin in base mode (ignored while busy, in autoplay, when a modal/input is focused, or when
|
|
795
|
+
`features.spacebar` is `false`).
|
|
794
796
|
|
|
795
797
|
### Live demo
|
|
796
798
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -963,7 +963,7 @@ function createInitialState(config) {
|
|
|
963
963
|
autoplay: { active: false, remaining: 0 },
|
|
964
964
|
turbo: 0,
|
|
965
965
|
buyBonusEnabled: true,
|
|
966
|
-
freeSpins: { current: 0, total: 0, totalWin: 0
|
|
966
|
+
freeSpins: { current: 0, total: 0, totalWin: 0 },
|
|
967
967
|
activeFeature: null,
|
|
968
968
|
};
|
|
969
969
|
}
|
|
@@ -1048,9 +1048,9 @@ function buildThemeVars(theme = {}) {
|
|
|
1048
1048
|
// Plaque tokens — the grouped dark/glass panel language shared by the control bar
|
|
1049
1049
|
// AND the overlays. Scheme-independent (always dark, white-on-dark) so bar + overlays
|
|
1050
1050
|
// stay visually identical regardless of the dark/light `scheme`.
|
|
1051
|
-
`--shell-plaque-dark: rgba(6,9,15,.
|
|
1052
|
-
`--shell-plaque-glass: rgba(30,36,48,.
|
|
1053
|
-
`--shell-plaque-glass-hover: rgba(40,48,64,.
|
|
1051
|
+
`--shell-plaque-dark: rgba(6,9,15,.86)`,
|
|
1052
|
+
`--shell-plaque-glass: rgba(30,36,48,.70)`,
|
|
1053
|
+
`--shell-plaque-glass-hover: rgba(40,48,64,.86)`,
|
|
1054
1054
|
// Opaque surface for centred modals (confirm, bet/autoplay pickers) so they read solid,
|
|
1055
1055
|
// not see-through, over the frosted backdrop.
|
|
1056
1056
|
`--shell-plaque-solid: #1a2030`,
|
|
@@ -1289,8 +1289,10 @@ const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
1289
1289
|
/* the buy-bonus scroll area is a SIZE CONTAINER, so the cards' cqh units measure the overlay
|
|
1290
1290
|
(the popout frame) and not the browser window — cards fit without any vertical scroll. */
|
|
1291
1291
|
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-scroll { container-type:size; }
|
|
1292
|
-
|
|
1293
|
-
|
|
1292
|
+
/* buy-bonus uses the FULL overlay width (no 800px centre cap) so the card row isn't cropped at
|
|
1293
|
+
the sides; small horizontal padding keeps the cards off the screen edges. */
|
|
1294
|
+
#${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { max-width:none; padding:clamp(8px,3cqh,16px) clamp(12px,3vw,28px); }
|
|
1295
|
+
#${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; justify-content:safe center; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
|
|
1294
1296
|
scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
|
|
1295
1297
|
/* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
|
|
1296
1298
|
browser window, so cards shrink to fit the real container height. */
|
|
@@ -1353,8 +1355,10 @@ const SHELL_CSS = SHELL_FONT_CSS + `
|
|
|
1353
1355
|
border-radius:16px; padding:0 20px; gap:18px; }
|
|
1354
1356
|
#${SHELL_ROOT_ID} .ge-pl-dark { background:var(--shell-plaque-dark); }
|
|
1355
1357
|
#${SHELL_ROOT_ID} .ge-pl-glass { background:var(--shell-plaque-glass); }
|
|
1356
|
-
/* FS
|
|
1357
|
-
|
|
1358
|
+
/* FS/replay left blocks — Free Spins counter (compact) + Total Win, standalone glass plaques
|
|
1359
|
+
sitting just right of the balance pill */
|
|
1360
|
+
#${SHELL_ROOT_ID} .ge-pl-fs, #${SHELL_ROOT_ID} .ge-pl-totalwin { margin-left:8px; }
|
|
1361
|
+
#${SHELL_ROOT_ID} .ge-pl-fs { padding:0 16px; }
|
|
1358
1362
|
#${SHELL_ROOT_ID} .ge-pl .ge-rd { color:#fff; text-shadow:none; }
|
|
1359
1363
|
#${SHELL_ROOT_ID} .ge-pl .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
|
|
1360
1364
|
#${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
|
|
@@ -1625,10 +1629,13 @@ function renderBottomBar(shell) {
|
|
|
1625
1629
|
bar.dataset.geMode = state.mode;
|
|
1626
1630
|
// menu icon button (always)
|
|
1627
1631
|
const menu = iconBtn('menu', 'menu', () => shell.openMenu());
|
|
1628
|
-
// All three modes share the base plaque layout. FS/replay hide the controls that
|
|
1629
|
-
//
|
|
1632
|
+
// All three modes share the base plaque layout. FS/replay hide the controls that don't apply
|
|
1633
|
+
// and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
|
|
1630
1634
|
const isBase = state.mode === 'base';
|
|
1631
1635
|
const isFS = state.mode === 'freeSpins';
|
|
1636
|
+
// FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
|
|
1637
|
+
// only when it's a free-spins replay (freeSpins.total > 0).
|
|
1638
|
+
const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
|
|
1632
1639
|
const balance = readout('balance', shell.t('Balance'), fmt(state.balance));
|
|
1633
1640
|
// With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
|
|
1634
1641
|
// the feature accent; the base state.bet is unchanged and returns once the feature is off.
|
|
@@ -1655,22 +1662,25 @@ function renderBottomBar(shell) {
|
|
|
1655
1662
|
buy = config.features.buyBonus !== false ? buyBtn(shell) : null;
|
|
1656
1663
|
}
|
|
1657
1664
|
const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmt(state.win)) : null;
|
|
1658
|
-
// FS
|
|
1659
|
-
const fsCounter =
|
|
1660
|
-
const fsTotalWin =
|
|
1661
|
-
const fsLastWin = isFS ? readout('fs-lastwin', shell.t('Last win'), fmt(state.freeSpins.lastWin)) : null;
|
|
1665
|
+
// FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
|
|
1666
|
+
const fsCounter = showFsBlocks ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
|
|
1667
|
+
const fsTotalWin = showFsBlocks ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
|
|
1662
1668
|
if (mobile) {
|
|
1663
|
-
// rows: [balance · win
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1669
|
+
// rows: [balance · win] · [menu · auto · spin · FS counter · Total Win · turbo · buy] · [− bet +]
|
|
1670
|
+
// FS counter + Total Win live in the controls row (alongside menu/turbo), not the top readouts.
|
|
1671
|
+
bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl])));
|
|
1672
|
+
const center = isBase ? spin : null;
|
|
1673
|
+
bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, fsCounter, fsTotalWin, turbo, buy])));
|
|
1667
1674
|
bar.appendChild(plaque('ge-m-bet ge-pl ge-pl-dark', compact([betDown, betValue, betUp])));
|
|
1668
1675
|
}
|
|
1669
1676
|
else {
|
|
1670
|
-
// LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance]
|
|
1677
|
+
// LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance] · [Free Spins] · [Total Win]
|
|
1678
|
+
// (the last two only render in FS / a free-spins replay)
|
|
1671
1679
|
const menuPlaque = plaque('ge-pl ge-pl-dark ge-pl-menu', [menu]);
|
|
1672
1680
|
const balPlaque = plaque('ge-pl ge-pl-glass ge-pl-bal', [balance]);
|
|
1673
|
-
const
|
|
1681
|
+
const fsPlaque = fsCounter ? plaque('ge-pl ge-pl-glass ge-pl-fs', [fsCounter]) : null;
|
|
1682
|
+
const totalWinPlaque = fsTotalWin ? plaque('ge-pl ge-pl-glass ge-pl-totalwin', [fsTotalWin]) : null;
|
|
1683
|
+
const left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque, fsPlaque, totalWinPlaque]));
|
|
1674
1684
|
// RIGHT: [bet (+ step)] · |divider| · [auto · SPIN · turbo]
|
|
1675
1685
|
const betKids = [betValue];
|
|
1676
1686
|
if (betUp && betDown) {
|
|
@@ -1686,11 +1696,9 @@ function renderBottomBar(shell) {
|
|
|
1686
1696
|
spinWrap.className = 'ge-spinwrap ge-pl-dark';
|
|
1687
1697
|
spinWrap.append(...compact([auto, spin, turbo]));
|
|
1688
1698
|
const right = zone('ge-zone-right ge-zone-plaques', betPlaque, divider, spinWrap);
|
|
1689
|
-
// MIDDLE:
|
|
1699
|
+
// MIDDLE: per-spin WIN pill in every mode — lifts above the bar on overflow.
|
|
1690
1700
|
let middle = null;
|
|
1691
|
-
if (
|
|
1692
|
-
middle = plaque('ge-pl ge-pl-glass ge-fscount', compact([fsLastWin, fsCounter, fsTotalWin]));
|
|
1693
|
-
else if (winEl) {
|
|
1701
|
+
if (winEl) {
|
|
1694
1702
|
winEl.classList.add('ge-winpill');
|
|
1695
1703
|
middle = winEl;
|
|
1696
1704
|
}
|
|
@@ -1964,7 +1972,7 @@ function sectionControls(shell, el) {
|
|
|
1964
1972
|
{ vis: slot(icon('spin')), name: 'Spin', desc: 'Start a spin at the current bet.', on: true },
|
|
1965
1973
|
{ vis: slot(icon('plus')), name: 'Raise bet', desc: 'Increase your stake.', on: true },
|
|
1966
1974
|
{ vis: slot(icon('minus')), name: 'Lower bet', desc: 'Decrease your stake.', on: true },
|
|
1967
|
-
{ vis: slot(icon('autoplay')), name: 'Autoplay', desc: 'Spin automatically a set number of times.', on: features.autoplay },
|
|
1975
|
+
{ vis: slot(icon('autoplay')), name: 'Autoplay', desc: 'Spin automatically a set number of times.', on: features.autoplay != null },
|
|
1968
1976
|
{ vis: slot(icon('turbo1')), name: 'Turbo', desc: 'Speed up spin animations.', on: features.turbo > 0 },
|
|
1969
1977
|
{ vis: buyBadge, name: 'Buy bonus', desc: 'Pay a fixed cost to enter a bonus feature.', on: features.buyBonus !== false },
|
|
1970
1978
|
];
|
|
@@ -2378,14 +2386,29 @@ function openBetModal(shell) {
|
|
|
2378
2386
|
});
|
|
2379
2387
|
}
|
|
2380
2388
|
const AUTOPLAY_COUNTS = [10, 25, 50, 100, 250, 500, 1000, 2000, Infinity];
|
|
2381
|
-
/**
|
|
2389
|
+
/** The selectable spin counts, honouring an optional jurisdiction max. With a `maxCount`:
|
|
2390
|
+
* drop ∞, keep presets ≤ max, and append the max itself when it isn't already a preset
|
|
2391
|
+
* (so the cap is always offered). Without one: the default presets including ∞. */
|
|
2392
|
+
function autoplayCounts(maxCount) {
|
|
2393
|
+
if (maxCount == null)
|
|
2394
|
+
return AUTOPLAY_COUNTS;
|
|
2395
|
+
const capped = AUTOPLAY_COUNTS.filter((n) => Number.isFinite(n) && n <= maxCount);
|
|
2396
|
+
if (!capped.includes(maxCount))
|
|
2397
|
+
capped.push(maxCount);
|
|
2398
|
+
return capped;
|
|
2399
|
+
}
|
|
2400
|
+
/** Autoplay picker — spin counts (incl. ∞ unless a maxCount caps them); Confirm starts autoplay. */
|
|
2382
2401
|
function openAutoplayModal(shell) {
|
|
2402
|
+
const maxCount = shell.config.features.autoplay?.maxCount;
|
|
2403
|
+
const counts = autoplayCounts(maxCount);
|
|
2383
2404
|
return buildSheet({
|
|
2384
2405
|
ge: 'autoplay-modal', title: shell.t('Autoplay'), columns: 3, confirmLabel: shell.t('Start'),
|
|
2385
|
-
choices:
|
|
2386
|
-
selected: String(shell.state.autoplay.remaining ||
|
|
2406
|
+
choices: counts.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
|
|
2407
|
+
selected: String(shell.state.autoplay.remaining || counts[0]),
|
|
2387
2408
|
onConfirm: (id) => {
|
|
2388
|
-
|
|
2409
|
+
let remaining = Number(id); // "Infinity" → Infinity
|
|
2410
|
+
if (maxCount != null)
|
|
2411
|
+
remaining = Math.min(remaining, maxCount); // defensive cap
|
|
2389
2412
|
shell.state.autoplay = { active: true, remaining };
|
|
2390
2413
|
shell.emit('autoplayStart', { active: true, remaining });
|
|
2391
2414
|
shell.render();
|
|
@@ -2703,12 +2726,15 @@ class GameShell extends EventEmitter {
|
|
|
2703
2726
|
const s = natural > 0 && avail > 0 ? Math.min(1, avail / natural) : 1;
|
|
2704
2727
|
host.style.transform = `translateX(-50%) scale(${s.toFixed(4)})`;
|
|
2705
2728
|
}
|
|
2706
|
-
/** Spacebar starts a spin — same path as the spin disc. Ignored
|
|
2707
|
-
* while autoplay is active, outside base mode, when an
|
|
2708
|
-
* editable element is focused. `repeat` (held key) is
|
|
2729
|
+
/** Spacebar starts a spin — same path as the spin disc. Ignored when `features.spacebar` is
|
|
2730
|
+
* false, while a spin is running, while autoplay is active, outside base mode, when an
|
|
2731
|
+
* overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
|
|
2732
|
+
* ignored so it can't spam. */
|
|
2709
2733
|
handleKeyDown = (e) => {
|
|
2710
2734
|
if (this.destroyed || e.code !== 'Space' || e.repeat)
|
|
2711
2735
|
return;
|
|
2736
|
+
if (this.config.features.spacebar === false)
|
|
2737
|
+
return; // shortcut disabled (e.g. jurisdiction)
|
|
2712
2738
|
const t = e.target;
|
|
2713
2739
|
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))
|
|
2714
2740
|
return;
|