@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/dist/index.esm.js CHANGED
@@ -961,7 +961,7 @@ function createInitialState(config) {
961
961
  autoplay: { active: false, remaining: 0 },
962
962
  turbo: 0,
963
963
  buyBonusEnabled: true,
964
- freeSpins: { current: 0, total: 0, totalWin: 0, lastWin: 0 },
964
+ freeSpins: { current: 0, total: 0, totalWin: 0 },
965
965
  activeFeature: null,
966
966
  };
967
967
  }
@@ -1046,9 +1046,9 @@ function buildThemeVars(theme = {}) {
1046
1046
  // Plaque tokens — the grouped dark/glass panel language shared by the control bar
1047
1047
  // AND the overlays. Scheme-independent (always dark, white-on-dark) so bar + overlays
1048
1048
  // stay visually identical regardless of the dark/light `scheme`.
1049
- `--shell-plaque-dark: rgba(6,9,15,.76)`,
1050
- `--shell-plaque-glass: rgba(30,36,48,.5)`,
1051
- `--shell-plaque-glass-hover: rgba(40,48,64,.62)`,
1049
+ `--shell-plaque-dark: rgba(6,9,15,.86)`,
1050
+ `--shell-plaque-glass: rgba(30,36,48,.70)`,
1051
+ `--shell-plaque-glass-hover: rgba(40,48,64,.86)`,
1052
1052
  // Opaque surface for centred modals (confirm, bet/autoplay pickers) so they read solid,
1053
1053
  // not see-through, over the frosted backdrop.
1054
1054
  `--shell-plaque-solid: #1a2030`,
@@ -1287,8 +1287,10 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1287
1287
  /* the buy-bonus scroll area is a SIZE CONTAINER, so the cards' cqh units measure the overlay
1288
1288
  (the popout frame) and not the browser window — cards fit without any vertical scroll. */
1289
1289
  #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-scroll { container-type:size; }
1290
- #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { padding:clamp(8px,3cqh,16px); }
1291
- #${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
1290
+ /* buy-bonus uses the FULL overlay width (no 800px centre cap) so the card row isn't cropped at
1291
+ the sides; small horizontal padding keeps the cards off the screen edges. */
1292
+ #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { max-width:none; padding:clamp(8px,3cqh,16px) clamp(12px,3vw,28px); }
1293
+ #${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; justify-content:safe center; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
1292
1294
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
1293
1295
  /* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
1294
1296
  browser window, so cards shrink to fit the real container height. */
@@ -1351,8 +1353,10 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1351
1353
  border-radius:16px; padding:0 20px; gap:18px; }
1352
1354
  #${SHELL_ROOT_ID} .ge-pl-dark { background:var(--shell-plaque-dark); }
1353
1355
  #${SHELL_ROOT_ID} .ge-pl-glass { background:var(--shell-plaque-glass); }
1354
- /* FS spins-counter plaque (wide) sits between balance and bet, glass like balance */
1355
- #${SHELL_ROOT_ID} .ge-fscount { justify-content:center; min-width:150px; }
1356
+ /* FS/replay left blocksFree Spins counter (compact) + Total Win, standalone glass plaques
1357
+ sitting just right of the balance pill */
1358
+ #${SHELL_ROOT_ID} .ge-pl-fs, #${SHELL_ROOT_ID} .ge-pl-totalwin { margin-left:8px; }
1359
+ #${SHELL_ROOT_ID} .ge-pl-fs { padding:0 16px; }
1356
1360
  #${SHELL_ROOT_ID} .ge-pl .ge-rd { color:#fff; text-shadow:none; }
1357
1361
  #${SHELL_ROOT_ID} .ge-pl .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
1358
1362
  #${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
@@ -1623,10 +1627,13 @@ function renderBottomBar(shell) {
1623
1627
  bar.dataset.geMode = state.mode;
1624
1628
  // menu icon button (always)
1625
1629
  const menu = iconBtn('menu', 'menu', () => shell.openMenu());
1626
- // All three modes share the base plaque layout. FS/replay hide the controls that
1627
- // don't apply; FS puts the spins counter in the centre pill (where WIN normally is).
1630
+ // All three modes share the base plaque layout. FS/replay hide the controls that don't apply
1631
+ // and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
1628
1632
  const isBase = state.mode === 'base';
1629
1633
  const isFS = state.mode === 'freeSpins';
1634
+ // FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
1635
+ // only when it's a free-spins replay (freeSpins.total > 0).
1636
+ const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
1630
1637
  const balance = readout('balance', shell.t('Balance'), fmt(state.balance));
1631
1638
  // With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
1632
1639
  // the feature accent; the base state.bet is unchanged and returns once the feature is off.
@@ -1653,22 +1660,25 @@ function renderBottomBar(shell) {
1653
1660
  buy = config.features.buyBonus !== false ? buyBtn(shell) : null;
1654
1661
  }
1655
1662
  const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmt(state.win)) : null;
1656
- // FS readouts the spins counter plus the accumulated/last win for the round.
1657
- const fsCounter = isFS ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
1658
- const fsTotalWin = isFS ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
1659
- const fsLastWin = isFS ? readout('fs-lastwin', shell.t('Last win'), fmt(state.freeSpins.lastWin)) : null;
1663
+ // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
1664
+ const fsCounter = showFsBlocks ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
1665
+ const fsTotalWin = showFsBlocks ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
1660
1666
  if (mobile) {
1661
- // rows: [balance · win/(FS last+total)] · [menu · auto · (spin | FS counter) · turbo · buy] · [− bet +]
1662
- bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl, fsLastWin, fsTotalWin])));
1663
- const center = isBase ? spin : fsCounter;
1664
- bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, turbo, buy])));
1667
+ // rows: [balance · win] · [menu · auto · spin · FS counter · Total Win · turbo · buy] · [− bet +]
1668
+ // FS counter + Total Win live in the controls row (alongside menu/turbo), not the top readouts.
1669
+ bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl])));
1670
+ const center = isBase ? spin : null;
1671
+ bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, fsCounter, fsTotalWin, turbo, buy])));
1665
1672
  bar.appendChild(plaque('ge-m-bet ge-pl ge-pl-dark', compact([betDown, betValue, betUp])));
1666
1673
  }
1667
1674
  else {
1668
- // LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance]
1675
+ // LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance] · [Free Spins] · [Total Win]
1676
+ // (the last two only render in FS / a free-spins replay)
1669
1677
  const menuPlaque = plaque('ge-pl ge-pl-dark ge-pl-menu', [menu]);
1670
1678
  const balPlaque = plaque('ge-pl ge-pl-glass ge-pl-bal', [balance]);
1671
- const left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque]));
1679
+ const fsPlaque = fsCounter ? plaque('ge-pl ge-pl-glass ge-pl-fs', [fsCounter]) : null;
1680
+ const totalWinPlaque = fsTotalWin ? plaque('ge-pl ge-pl-glass ge-pl-totalwin', [fsTotalWin]) : null;
1681
+ const left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque, fsPlaque, totalWinPlaque]));
1672
1682
  // RIGHT: [bet (+ step)] · |divider| · [auto · SPIN · turbo]
1673
1683
  const betKids = [betValue];
1674
1684
  if (betUp && betDown) {
@@ -1684,11 +1694,9 @@ function renderBottomBar(shell) {
1684
1694
  spinWrap.className = 'ge-spinwrap ge-pl-dark';
1685
1695
  spinWrap.append(...compact([auto, spin, turbo]));
1686
1696
  const right = zone('ge-zone-right ge-zone-plaques', betPlaque, divider, spinWrap);
1687
- // MIDDLE: FS last win · counter · total win plaque; base/replay → WIN pill (lifts on overflow)
1697
+ // MIDDLE: per-spin WIN pill in every mode lifts above the bar on overflow.
1688
1698
  let middle = null;
1689
- if (isFS)
1690
- middle = plaque('ge-pl ge-pl-glass ge-fscount', compact([fsLastWin, fsCounter, fsTotalWin]));
1691
- else if (winEl) {
1699
+ if (winEl) {
1692
1700
  winEl.classList.add('ge-winpill');
1693
1701
  middle = winEl;
1694
1702
  }
@@ -1962,7 +1970,7 @@ function sectionControls(shell, el) {
1962
1970
  { vis: slot(icon('spin')), name: 'Spin', desc: 'Start a spin at the current bet.', on: true },
1963
1971
  { vis: slot(icon('plus')), name: 'Raise bet', desc: 'Increase your stake.', on: true },
1964
1972
  { vis: slot(icon('minus')), name: 'Lower bet', desc: 'Decrease your stake.', on: true },
1965
- { vis: slot(icon('autoplay')), name: 'Autoplay', desc: 'Spin automatically a set number of times.', on: features.autoplay },
1973
+ { vis: slot(icon('autoplay')), name: 'Autoplay', desc: 'Spin automatically a set number of times.', on: features.autoplay != null },
1966
1974
  { vis: slot(icon('turbo1')), name: 'Turbo', desc: 'Speed up spin animations.', on: features.turbo > 0 },
1967
1975
  { vis: buyBadge, name: 'Buy bonus', desc: 'Pay a fixed cost to enter a bonus feature.', on: features.buyBonus !== false },
1968
1976
  ];
@@ -2376,14 +2384,29 @@ function openBetModal(shell) {
2376
2384
  });
2377
2385
  }
2378
2386
  const AUTOPLAY_COUNTS = [10, 25, 50, 100, 250, 500, 1000, 2000, Infinity];
2379
- /** Autoplay picker spin counts incl. ∞; Confirm starts autoplay. */
2387
+ /** The selectable spin counts, honouring an optional jurisdiction max. With a `maxCount`:
2388
+ * drop ∞, keep presets ≤ max, and append the max itself when it isn't already a preset
2389
+ * (so the cap is always offered). Without one: the default presets including ∞. */
2390
+ function autoplayCounts(maxCount) {
2391
+ if (maxCount == null)
2392
+ return AUTOPLAY_COUNTS;
2393
+ const capped = AUTOPLAY_COUNTS.filter((n) => Number.isFinite(n) && n <= maxCount);
2394
+ if (!capped.includes(maxCount))
2395
+ capped.push(maxCount);
2396
+ return capped;
2397
+ }
2398
+ /** Autoplay picker — spin counts (incl. ∞ unless a maxCount caps them); Confirm starts autoplay. */
2380
2399
  function openAutoplayModal(shell) {
2400
+ const maxCount = shell.config.features.autoplay?.maxCount;
2401
+ const counts = autoplayCounts(maxCount);
2381
2402
  return buildSheet({
2382
2403
  ge: 'autoplay-modal', title: shell.t('Autoplay'), columns: 3, confirmLabel: shell.t('Start'),
2383
- choices: AUTOPLAY_COUNTS.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
2384
- selected: String(shell.state.autoplay.remaining || 10),
2404
+ choices: counts.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
2405
+ selected: String(shell.state.autoplay.remaining || counts[0]),
2385
2406
  onConfirm: (id) => {
2386
- const remaining = Number(id); // "Infinity" → Infinity
2407
+ let remaining = Number(id); // "Infinity" → Infinity
2408
+ if (maxCount != null)
2409
+ remaining = Math.min(remaining, maxCount); // defensive cap
2387
2410
  shell.state.autoplay = { active: true, remaining };
2388
2411
  shell.emit('autoplayStart', { active: true, remaining });
2389
2412
  shell.render();
@@ -2701,12 +2724,15 @@ class GameShell extends EventEmitter {
2701
2724
  const s = natural > 0 && avail > 0 ? Math.min(1, avail / natural) : 1;
2702
2725
  host.style.transform = `translateX(-50%) scale(${s.toFixed(4)})`;
2703
2726
  }
2704
- /** Spacebar starts a spin — same path as the spin disc. Ignored while a spin is running,
2705
- * while autoplay is active, outside base mode, when an overlay/modal is open, or when an
2706
- * editable element is focused. `repeat` (held key) is ignored so it can't spam. */
2727
+ /** Spacebar starts a spin — same path as the spin disc. Ignored when `features.spacebar` is
2728
+ * false, while a spin is running, while autoplay is active, outside base mode, when an
2729
+ * overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
2730
+ * ignored so it can't spam. */
2707
2731
  handleKeyDown = (e) => {
2708
2732
  if (this.destroyed || e.code !== 'Space' || e.repeat)
2709
2733
  return;
2734
+ if (this.config.features.spacebar === false)
2735
+ return; // shortcut disabled (e.g. jurisdiction)
2710
2736
  const t = e.target;
2711
2737
  if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))
2712
2738
  return;