@energy8platform/platform-core 0.23.0 → 0.23.2

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
@@ -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);
@@ -1294,16 +1295,18 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1294
1295
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
1295
1296
  /* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
1296
1297
  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); }
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); }
1299
1300
  /* mobile: vertical stack at a fixed, readable size — scroll the list, don't shrink the cards */
1300
1301
  #${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; }
1302
+ #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid .ge-bonus-card { flex:0 0 auto; font-size:12px; }
1302
1303
  #${SHELL_ROOT_ID} .ge-bonus-card { display:flex; flex-direction:column; border-radius:1.4em; overflow:hidden;
1303
1304
  background:var(--shell-plaque-glass); border:1px solid var(--shell-plaque-line); color:#fff; text-align:center;
1304
1305
  pointer-events:auto; cursor:pointer; transition:box-shadow .12s ease, background .12s ease; }
1305
1306
  #${SHELL_ROOT_ID} .ge-bonus-card:hover:not(.ge-bonus-off) {
1306
1307
  box-shadow:0 0 0 1px var(--card-acc), 0 12px 34px -12px var(--card-acc); }
1308
+ /* custom card (BonusOption.custom): keep grid sizing + accent vars, drop the default chrome so the game owns the UI */
1309
+ #${SHELL_ROOT_ID} .ge-bonus-card--custom { background:none; border:none; cursor:default; }
1307
1310
  #${SHELL_ROOT_ID} .ge-bonus-body { display:flex; flex-direction:column; align-items:center; flex:1; padding:1.25em 1.1em .9em; }
1308
1311
  #${SHELL_ROOT_ID} .ge-bonus-title { font-size:1.3em; font-weight:800; letter-spacing:.04em; text-transform:uppercase;
1309
1312
  color:var(--card-acc); margin-bottom:.75em; }
@@ -1405,40 +1408,44 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1405
1408
  align-items:center; justify-content:center; padding:clamp(10px,4vh,24px); box-sizing:border-box;
1406
1409
  background:rgba(12,17,28,.5); backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%);
1407
1410
  -webkit-backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%); animation:ge-ov-in .16s ease-out; }
1408
- /* GameShell.fitModal() scales the whole card down (transform) so it fits short popouts uniformly */
1409
- #${SHELL_ROOT_ID} .ge-modal-card { width:100%; max-width:420px; box-sizing:border-box; overflow:hidden;
1410
- 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; }
1411
1418
  /* ✕ pinned to the overlay corner (the screen), not the card */
1412
1419
  #${SHELL_ROOT_ID} .ge-modal-close { position:absolute; top:12px; right:12px; z-index:2; width:36px; height:36px;
1413
1420
  border:none; border-radius:50%; cursor:pointer; pointer-events:auto; background:var(--shell-plaque-dark); color:#fff;
1414
1421
  display:flex; align-items:center; justify-content:center; font-size:20px; transition:background .12s ease, color .12s ease; }
1415
1422
  #${SHELL_ROOT_ID} .ge-modal-close:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
1416
- #${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; }
1417
1424
  #${SHELL_ROOT_ID} .ge-modal-title { margin:0; text-align:center; color:var(--card-acc, var(--shell-accent));
1418
- font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:18px; }
1419
- #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:14px; line-height:1.5; }
1420
- #${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; }
1421
1428
  #${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
1422
- border-radius:12px; background:rgba(255,255,255,.04); color:#fff; font-size:15px; font-weight:700;
1423
- 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; }
1424
1431
  #${SHELL_ROOT_ID} .ge-chip:hover { background:var(--shell-plaque-glass-hover); }
1425
1432
  #${SHELL_ROOT_ID} .ge-chip.ge-on { border-color:var(--shell-accent); background:var(--shell-accent); color:#fff; }
1426
1433
  /* full-bleed footer button(s), flush to the card's bottom edge (card clips the corners) */
1427
1434
  #${SHELL_ROOT_ID} .ge-modal-actions { display:flex; }
1428
1435
  #${SHELL_ROOT_ID} .ge-modal-actions > * { flex:1; }
1429
- #${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;
1430
1437
  letter-spacing:.04em; text-transform:uppercase; cursor:pointer; pointer-events:auto; transition:filter .12s ease; }
1431
1438
  #${SHELL_ROOT_ID} .ge-modal-btn:hover:not([disabled]) { filter:brightness(1.08); }
1432
1439
  #${SHELL_ROOT_ID} .ge-modal-btn--accent { background:var(--card-acc, var(--shell-accent)); color:#fff; }
1433
1440
  #${SHELL_ROOT_ID} .ge-modal-btn--ghost { background:var(--shell-plaque-glass-hover); color:#fff; }
1434
1441
  /* replay summary — label/value rows, accented total-win row */
1435
1442
  #${SHELL_ROOT_ID} .ge-replay-rows { display:flex; flex-direction:column; }
1436
- #${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; }
1437
1444
  #${SHELL_ROOT_ID} .ge-replay-row + .ge-replay-row { border-top:1px solid var(--shell-plaque-line); }
1438
- #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:11px; font-weight:700; }
1439
- #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:15px; font-variant-numeric:tabular-nums; }
1440
- #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:12px; }
1441
- #${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; }
1442
1449
 
1443
1450
  #${SHELL_ROOT_ID}.ge-shell-hidden { opacity:0; pointer-events:none; transition:opacity .25s ease; }
1444
1451
  `;
@@ -1657,7 +1664,7 @@ function renderBottomBar(shell) {
1657
1664
  shell.openBetPicker(); });
1658
1665
  spin = spinButton(shell);
1659
1666
  auto = config.features.autoplay ? autoButton(shell) : null;
1660
- buy = config.features.buyBonus !== false ? buyBtn(shell) : null;
1667
+ buy = (config.features.buyBonus !== false || config.onBonusBuy) ? buyBtn(shell) : null;
1661
1668
  }
1662
1669
  const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmt(state.win)) : null;
1663
1670
  // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
@@ -1914,7 +1921,7 @@ function openGameInfoModal(shell) {
1914
1921
  }
1915
1922
  function renderSection(shell, s) {
1916
1923
  switch (s.type) {
1917
- 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')));
1918
1925
  case 'controls': return sectionControls(shell, sec('info-controls', s.title, shell.t('Controls')));
1919
1926
  case 'paytable': return sectionPaytable(s.rows, sec('info-paytable', s.title, shell.t('Paytable')));
1920
1927
  case 'wins': return sectionWins(s, sec('info-wins', s.title, shell.t(winFallbackTitle(s.kind))));
@@ -1935,25 +1942,25 @@ function sec(ge, title, fallback) {
1935
1942
  return el;
1936
1943
  }
1937
1944
  // ── modes (rows — varying description lengths read better than fixed cards) ────
1938
- function sectionModes(modes, el) {
1945
+ function sectionModes(shell, modes, el) {
1939
1946
  const list = document.createElement('div');
1940
1947
  list.className = 'ge-gi-modes';
1941
1948
  for (const m of modes)
1942
- list.appendChild(modeRow(m));
1949
+ list.appendChild(modeRow(shell, m));
1943
1950
  el.appendChild(list);
1944
1951
  return el;
1945
1952
  }
1946
- function modeRow(m) {
1953
+ function modeRow(shell, m) {
1947
1954
  const row = document.createElement('div');
1948
1955
  row.className = 'ge-gi-mode';
1949
1956
  const stat = (label, val) => `<span class="ge-gi-mode-st"><span>${label}</span><b>${val}</b></span>`;
1950
1957
  let stats = '';
1951
1958
  if (m.price != null)
1952
- stats += stat('Price', m.price);
1959
+ stats += stat(shell.t('Price'), m.price);
1953
1960
  if (typeof m.rtp === 'number')
1954
- stats += stat('RTP', `${m.rtp}%`);
1961
+ stats += stat(shell.t('RTP'), `${m.rtp}%`);
1955
1962
  if (m.maxWin != null)
1956
- stats += stat('Max win', m.maxWin);
1963
+ stats += stat(shell.t('Max win'), m.maxWin);
1957
1964
  row.innerHTML =
1958
1965
  `<div class="ge-gi-mode-top"><span class="ge-gi-mode-h">${m.title}</span>` +
1959
1966
  (stats ? `<span class="ge-gi-mode-stats">${stats}</span>` : '') + '</div>' +
@@ -2242,20 +2249,39 @@ function buildCard(shell, bonus, overlay) {
2242
2249
  card.dataset.ge = `bonus-card-${bonus.id}`;
2243
2250
  card.style.setProperty('--card-acc', accent);
2244
2251
  card.style.setProperty('--card-ink', contrastText(accent));
2252
+ const enabled = isAffordable(shell, bonus);
2253
+ // Stack the confirm on top of the overlay grid (cancel returns to the grid). Re-checks
2254
+ // affordability at click time, so it's a safe no-op when the option can't be bought.
2255
+ const select = () => {
2256
+ if (!isAffordable(shell, bonus))
2257
+ return;
2258
+ overlay.appendChild(buildConfirm(shell, bonus, overlay));
2259
+ shell.fitModals();
2260
+ };
2261
+ // Game-supplied card UI: the shell keeps the wrapper (grid sizing + accent vars) and runs the
2262
+ // buy flow when the game calls ctx.select(); the game owns everything inside.
2263
+ if (bonus.custom) {
2264
+ card.classList.add('ge-bonus-card--custom');
2265
+ const price = bonus.priceMultiplier * shell.state.bet;
2266
+ card.appendChild(bonus.custom({
2267
+ bonus, bet: shell.state.bet, price,
2268
+ priceText: formatCurrency(price, shell.config.currency),
2269
+ disabled: !enabled, accent, select,
2270
+ }));
2271
+ return card;
2272
+ }
2245
2273
  card.appendChild(cardBody(shell, bonus));
2246
2274
  const cta = document.createElement('button');
2247
2275
  cta.className = 'ge-bonus-cta';
2248
2276
  cta.dataset.ge = `bonus-cta-${bonus.id}`;
2249
2277
  cta.textContent = shell.t(actionLabel(bonus));
2250
2278
  card.appendChild(cta);
2251
- const enabled = isAffordable(shell, bonus);
2252
2279
  if (!enabled) {
2253
2280
  card.classList.add('ge-bonus-off');
2254
2281
  cta.disabled = true;
2255
2282
  }
2256
2283
  else {
2257
- // Stack the confirm on top of the overlay grid (cancel returns to the grid).
2258
- card.addEventListener('click', () => { overlay.appendChild(buildConfirm(shell, bonus, overlay)); shell.fitModals(); });
2284
+ card.addEventListener('click', select);
2259
2285
  }
2260
2286
  return card;
2261
2287
  }
@@ -2581,6 +2607,7 @@ const RULES = [
2581
2607
  ['paid', 'won'],
2582
2608
  ['bought', 'instantly triggered'],
2583
2609
  ['purchase', 'play'],
2610
+ ['price', 'play'],
2584
2611
  ['deposit', 'get coins'],
2585
2612
  ['withdraw', 'redeem'],
2586
2613
  ['currency', 'token'],
@@ -2844,6 +2871,10 @@ class GameShell extends EventEmitter {
2844
2871
  openSettings() { this.emit('settingsOpen'); this.showModal(openSettingsModal(this)); }
2845
2872
  openInfo() { this.emit('infoOpen'); this.showModal(openGameInfoModal(this)); }
2846
2873
  openBuyBonus() {
2874
+ if (this.config.onBonusBuy) {
2875
+ this.config.onBonusBuy();
2876
+ return;
2877
+ } // game handles it (own UI)
2847
2878
  const overlay = openBuyBonusOverlay(this);
2848
2879
  if (overlay)
2849
2880
  this.showModal(overlay);