@energy8platform/platform-core 0.22.0 → 0.23.1

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
@@ -714,7 +714,7 @@ the previous value.
714
714
  shell.setBalance(n); shell.setWin(n); shell.setBet(n);
715
715
  shell.setBusy(true); // disables controls mid-spin
716
716
  shell.setMode('freeSpins');
717
- 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
718
718
  shell.setAutoplay({ active: true, remaining: 25 });
719
719
  shell.setTurbo(2);
720
720
  shell.setBuyBonusEnabled(false); // grey out BUY BONUS (e.g. insufficient balance)
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
  }
@@ -1078,6 +1078,7 @@ const SHELL_ROOT_ID = '__ge-game-shell__';
1078
1078
  const SHELL_CSS = SHELL_FONT_CSS + `
1079
1079
  #${SHELL_ROOT_ID} {
1080
1080
  position: absolute; inset: 0;
1081
+ container-type: size; /* query container → centred modals size in cq units (responsive on every screen) */
1081
1082
  pointer-events: none; z-index: 9000;
1082
1083
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1083
1084
  color: var(--shell-fg);
@@ -1289,16 +1290,18 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1289
1290
  /* the buy-bonus scroll area is a SIZE CONTAINER, so the cards' cqh units measure the overlay
1290
1291
  (the popout frame) and not the browser window — cards fit without any vertical scroll. */
1291
1292
  #${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;
1293
+ /* buy-bonus uses the FULL overlay width (no 800px centre cap) so the card row isn't cropped at
1294
+ the sides; small horizontal padding keeps the cards off the screen edges. */
1295
+ #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { max-width:none; padding:clamp(8px,3cqh,16px) clamp(12px,3vw,28px); }
1296
+ #${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; justify-content:safe center; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
1294
1297
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
1295
1298
  /* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
1296
1299
  browser window, so cards shrink to fit the real container height. */
1297
- #${SHELL_ROOT_ID} .ge-bb-grid .ge-bonus-card { flex:0 0 18.5em; scroll-snap-align:start;
1298
- font-size:clamp(7px, 4cqh, 13px); }
1300
+ #${SHELL_ROOT_ID} .ge-bb-grid .ge-bonus-card { flex:0 0 18em; scroll-snap-align:start;
1301
+ font-size:clamp(7px, 3.6cqh, 12px); }
1299
1302
  /* mobile: vertical stack at a fixed, readable size — scroll the list, don't shrink the cards */
1300
1303
  #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid { display:flex; flex-direction:column; gap:14px; overflow:visible; }
1301
- #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid .ge-bonus-card { flex:0 0 auto; font-size:13px; }
1304
+ #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid .ge-bonus-card { flex:0 0 auto; font-size:12px; }
1302
1305
  #${SHELL_ROOT_ID} .ge-bonus-card { display:flex; flex-direction:column; border-radius:1.4em; overflow:hidden;
1303
1306
  background:var(--shell-plaque-glass); border:1px solid var(--shell-plaque-line); color:#fff; text-align:center;
1304
1307
  pointer-events:auto; cursor:pointer; transition:box-shadow .12s ease, background .12s ease; }
@@ -1353,8 +1356,10 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1353
1356
  border-radius:16px; padding:0 20px; gap:18px; }
1354
1357
  #${SHELL_ROOT_ID} .ge-pl-dark { background:var(--shell-plaque-dark); }
1355
1358
  #${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; }
1359
+ /* FS/replay left blocksFree Spins counter (compact) + Total Win, standalone glass plaques
1360
+ sitting just right of the balance pill */
1361
+ #${SHELL_ROOT_ID} .ge-pl-fs, #${SHELL_ROOT_ID} .ge-pl-totalwin { margin-left:8px; }
1362
+ #${SHELL_ROOT_ID} .ge-pl-fs { padding:0 16px; }
1358
1363
  #${SHELL_ROOT_ID} .ge-pl .ge-rd { color:#fff; text-shadow:none; }
1359
1364
  #${SHELL_ROOT_ID} .ge-pl .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
1360
1365
  #${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
@@ -1403,40 +1408,44 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1403
1408
  align-items:center; justify-content:center; padding:clamp(10px,4vh,24px); box-sizing:border-box;
1404
1409
  background:rgba(12,17,28,.5); backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%);
1405
1410
  -webkit-backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%); animation:ge-ov-in .16s ease-out; }
1406
- /* GameShell.fitModal() scales the whole card down (transform) so it fits short popouts uniformly */
1407
- #${SHELL_ROOT_ID} .ge-modal-card { width:100%; max-width:420px; box-sizing:border-box; overflow:hidden;
1408
- transform-origin:center center; background:var(--shell-plaque-solid); border-radius:20px; display:flex; flex-direction:column; }
1411
+ /* Card sizes in cq units of the shell root responsive on EVERY screen, not just popouts. The
1412
+ card's font-size is the one knob (clamped for readability); everything inside is em-relative so
1413
+ the whole card scales as a unit. GameShell.fitModal() still transform-scales it down as a
1414
+ backstop for very short popouts. */
1415
+ #${SHELL_ROOT_ID} .ge-modal-card { font-size:clamp(11px, 2cqmin, 15px); width:100%; max-width:28em; box-sizing:border-box;
1416
+ overflow:hidden; transform-origin:center center; background:var(--shell-plaque-solid); border-radius:1.3em;
1417
+ display:flex; flex-direction:column; }
1409
1418
  /* ✕ pinned to the overlay corner (the screen), not the card */
1410
1419
  #${SHELL_ROOT_ID} .ge-modal-close { position:absolute; top:12px; right:12px; z-index:2; width:36px; height:36px;
1411
1420
  border:none; border-radius:50%; cursor:pointer; pointer-events:auto; background:var(--shell-plaque-dark); color:#fff;
1412
1421
  display:flex; align-items:center; justify-content:center; font-size:20px; transition:background .12s ease, color .12s ease; }
1413
1422
  #${SHELL_ROOT_ID} .ge-modal-close:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
1414
- #${SHELL_ROOT_ID} .ge-modal-body { padding:18px; display:flex; flex-direction:column; gap:16px; }
1423
+ #${SHELL_ROOT_ID} .ge-modal-body { padding:1.2em; display:flex; flex-direction:column; gap:1.05em; }
1415
1424
  #${SHELL_ROOT_ID} .ge-modal-title { margin:0; text-align:center; color:var(--card-acc, var(--shell-accent));
1416
- font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:18px; }
1417
- #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:14px; line-height:1.5; }
1418
- #${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:10px; }
1425
+ font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:1.2em; }
1426
+ #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:.93em; line-height:1.5; }
1427
+ #${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:.65em; }
1419
1428
  #${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
1420
- border-radius:12px; background:rgba(255,255,255,.04); color:#fff; font-size:15px; font-weight:700;
1421
- font-variant-numeric:tabular-nums; padding:12px 8px; transition:background .12s ease, border-color .12s ease; }
1429
+ border-radius:.8em; background:rgba(255,255,255,.04); color:#fff; font-size:1em; font-weight:700;
1430
+ font-variant-numeric:tabular-nums; padding:.8em .55em; transition:background .12s ease, border-color .12s ease; }
1422
1431
  #${SHELL_ROOT_ID} .ge-chip:hover { background:var(--shell-plaque-glass-hover); }
1423
1432
  #${SHELL_ROOT_ID} .ge-chip.ge-on { border-color:var(--shell-accent); background:var(--shell-accent); color:#fff; }
1424
1433
  /* full-bleed footer button(s), flush to the card's bottom edge (card clips the corners) */
1425
1434
  #${SHELL_ROOT_ID} .ge-modal-actions { display:flex; }
1426
1435
  #${SHELL_ROOT_ID} .ge-modal-actions > * { flex:1; }
1427
- #${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:16px; font-size:15px; font-weight:800;
1436
+ #${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:1.05em; font-size:1em; font-weight:800;
1428
1437
  letter-spacing:.04em; text-transform:uppercase; cursor:pointer; pointer-events:auto; transition:filter .12s ease; }
1429
1438
  #${SHELL_ROOT_ID} .ge-modal-btn:hover:not([disabled]) { filter:brightness(1.08); }
1430
1439
  #${SHELL_ROOT_ID} .ge-modal-btn--accent { background:var(--card-acc, var(--shell-accent)); color:#fff; }
1431
1440
  #${SHELL_ROOT_ID} .ge-modal-btn--ghost { background:var(--shell-plaque-glass-hover); color:#fff; }
1432
1441
  /* replay summary — label/value rows, accented total-win row */
1433
1442
  #${SHELL_ROOT_ID} .ge-replay-rows { display:flex; flex-direction:column; }
1434
- #${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:16px; padding:11px 2px; }
1443
+ #${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:1.05em; padding:.73em .13em; }
1435
1444
  #${SHELL_ROOT_ID} .ge-replay-row + .ge-replay-row { border-top:1px solid var(--shell-plaque-line); }
1436
- #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:11px; font-weight:700; }
1437
- #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:15px; font-variant-numeric:tabular-nums; }
1438
- #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:12px; }
1439
- #${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:19px; }
1445
+ #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:.73em; font-weight:700; }
1446
+ #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:1em; font-variant-numeric:tabular-nums; }
1447
+ #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:.8em; }
1448
+ #${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:1.27em; }
1440
1449
 
1441
1450
  #${SHELL_ROOT_ID}.ge-shell-hidden { opacity:0; pointer-events:none; transition:opacity .25s ease; }
1442
1451
  `;
@@ -1625,10 +1634,13 @@ function renderBottomBar(shell) {
1625
1634
  bar.dataset.geMode = state.mode;
1626
1635
  // menu icon button (always)
1627
1636
  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).
1637
+ // All three modes share the base plaque layout. FS/replay hide the controls that don't apply
1638
+ // and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
1630
1639
  const isBase = state.mode === 'base';
1631
1640
  const isFS = state.mode === 'freeSpins';
1641
+ // FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
1642
+ // only when it's a free-spins replay (freeSpins.total > 0).
1643
+ const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
1632
1644
  const balance = readout('balance', shell.t('Balance'), fmt(state.balance));
1633
1645
  // With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
1634
1646
  // the feature accent; the base state.bet is unchanged and returns once the feature is off.
@@ -1655,22 +1667,25 @@ function renderBottomBar(shell) {
1655
1667
  buy = config.features.buyBonus !== false ? buyBtn(shell) : null;
1656
1668
  }
1657
1669
  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;
1670
+ // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
1671
+ const fsCounter = showFsBlocks ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
1672
+ const fsTotalWin = showFsBlocks ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
1662
1673
  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])));
1674
+ // rows: [balance · win] · [menu · auto · spin · FS counter · Total Win · turbo · buy] · [− bet +]
1675
+ // FS counter + Total Win live in the controls row (alongside menu/turbo), not the top readouts.
1676
+ bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl])));
1677
+ const center = isBase ? spin : null;
1678
+ bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, fsCounter, fsTotalWin, turbo, buy])));
1667
1679
  bar.appendChild(plaque('ge-m-bet ge-pl ge-pl-dark', compact([betDown, betValue, betUp])));
1668
1680
  }
1669
1681
  else {
1670
- // LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance]
1682
+ // LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance] · [Free Spins] · [Total Win]
1683
+ // (the last two only render in FS / a free-spins replay)
1671
1684
  const menuPlaque = plaque('ge-pl ge-pl-dark ge-pl-menu', [menu]);
1672
1685
  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]));
1686
+ const fsPlaque = fsCounter ? plaque('ge-pl ge-pl-glass ge-pl-fs', [fsCounter]) : null;
1687
+ const totalWinPlaque = fsTotalWin ? plaque('ge-pl ge-pl-glass ge-pl-totalwin', [fsTotalWin]) : null;
1688
+ const left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque, fsPlaque, totalWinPlaque]));
1674
1689
  // RIGHT: [bet (+ step)] · |divider| · [auto · SPIN · turbo]
1675
1690
  const betKids = [betValue];
1676
1691
  if (betUp && betDown) {
@@ -1686,11 +1701,9 @@ function renderBottomBar(shell) {
1686
1701
  spinWrap.className = 'ge-spinwrap ge-pl-dark';
1687
1702
  spinWrap.append(...compact([auto, spin, turbo]));
1688
1703
  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)
1704
+ // MIDDLE: per-spin WIN pill in every mode lifts above the bar on overflow.
1690
1705
  let middle = null;
1691
- if (isFS)
1692
- middle = plaque('ge-pl ge-pl-glass ge-fscount', compact([fsLastWin, fsCounter, fsTotalWin]));
1693
- else if (winEl) {
1706
+ if (winEl) {
1694
1707
  winEl.classList.add('ge-winpill');
1695
1708
  middle = winEl;
1696
1709
  }
@@ -1908,7 +1921,7 @@ function openGameInfoModal(shell) {
1908
1921
  }
1909
1922
  function renderSection(shell, s) {
1910
1923
  switch (s.type) {
1911
- case 'modes': return sectionModes(s.modes, sec('info-modes', s.title, shell.t('Modes')));
1924
+ case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
1912
1925
  case 'controls': return sectionControls(shell, sec('info-controls', s.title, shell.t('Controls')));
1913
1926
  case 'paytable': return sectionPaytable(s.rows, sec('info-paytable', s.title, shell.t('Paytable')));
1914
1927
  case 'wins': return sectionWins(s, sec('info-wins', s.title, shell.t(winFallbackTitle(s.kind))));
@@ -1929,25 +1942,25 @@ function sec(ge, title, fallback) {
1929
1942
  return el;
1930
1943
  }
1931
1944
  // ── modes (rows — varying description lengths read better than fixed cards) ────
1932
- function sectionModes(modes, el) {
1945
+ function sectionModes(shell, modes, el) {
1933
1946
  const list = document.createElement('div');
1934
1947
  list.className = 'ge-gi-modes';
1935
1948
  for (const m of modes)
1936
- list.appendChild(modeRow(m));
1949
+ list.appendChild(modeRow(shell, m));
1937
1950
  el.appendChild(list);
1938
1951
  return el;
1939
1952
  }
1940
- function modeRow(m) {
1953
+ function modeRow(shell, m) {
1941
1954
  const row = document.createElement('div');
1942
1955
  row.className = 'ge-gi-mode';
1943
1956
  const stat = (label, val) => `<span class="ge-gi-mode-st"><span>${label}</span><b>${val}</b></span>`;
1944
1957
  let stats = '';
1945
1958
  if (m.price != null)
1946
- stats += stat('Price', m.price);
1959
+ stats += stat(shell.t('Price'), m.price);
1947
1960
  if (typeof m.rtp === 'number')
1948
- stats += stat('RTP', `${m.rtp}%`);
1961
+ stats += stat(shell.t('RTP'), `${m.rtp}%`);
1949
1962
  if (m.maxWin != null)
1950
- stats += stat('Max win', m.maxWin);
1963
+ stats += stat(shell.t('Max win'), m.maxWin);
1951
1964
  row.innerHTML =
1952
1965
  `<div class="ge-gi-mode-top"><span class="ge-gi-mode-h">${m.title}</span>` +
1953
1966
  (stats ? `<span class="ge-gi-mode-stats">${stats}</span>` : '') + '</div>' +
@@ -2575,6 +2588,7 @@ const RULES = [
2575
2588
  ['paid', 'won'],
2576
2589
  ['bought', 'instantly triggered'],
2577
2590
  ['purchase', 'play'],
2591
+ ['price', 'play'],
2578
2592
  ['deposit', 'get coins'],
2579
2593
  ['withdraw', 'redeem'],
2580
2594
  ['currency', 'token'],