@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 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
- autoplay: true,
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, lastWin: 0 }); // freeSpins bar readout
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, or when a modal/input is focused).
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, lastWin: 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,.76)`,
1052
- `--shell-plaque-glass: rgba(30,36,48,.5)`,
1053
- `--shell-plaque-glass-hover: rgba(40,48,64,.62)`,
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
- #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { padding:clamp(8px,3cqh,16px); }
1293
- #${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
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 spins-counter plaque (wide) sits between balance and bet, glass like balance */
1357
- #${SHELL_ROOT_ID} .ge-fscount { justify-content:center; min-width:150px; }
1358
+ /* FS/replay left blocksFree 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
- // don't apply; FS puts the spins counter in the centre pill (where WIN normally is).
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 readouts the spins counter plus the accumulated/last win for the round.
1659
- const fsCounter = isFS ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
1660
- const fsTotalWin = isFS ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
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/(FS last+total)] · [menu · auto · (spin | FS counter) · turbo · buy] · [− bet +]
1664
- bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl, fsLastWin, fsTotalWin])));
1665
- const center = isBase ? spin : fsCounter;
1666
- bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, turbo, buy])));
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 left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque]));
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: FS last win · counter · total win plaque; base/replay → WIN pill (lifts on overflow)
1699
+ // MIDDLE: per-spin WIN pill in every mode lifts above the bar on overflow.
1690
1700
  let middle = null;
1691
- if (isFS)
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
- /** Autoplay picker spin counts incl. ∞; Confirm starts autoplay. */
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: AUTOPLAY_COUNTS.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
2386
- selected: String(shell.state.autoplay.remaining || 10),
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
- const remaining = Number(id); // "Infinity" → Infinity
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 while a spin is running,
2707
- * while autoplay is active, outside base mode, when an overlay/modal is open, or when an
2708
- * editable element is focused. `repeat` (held key) is ignored so it can't spam. */
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;