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