@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/README.md CHANGED
@@ -691,6 +691,7 @@ await removeGameShell();
691
691
  | `mode` | `'base' \| 'freeSpins' \| 'replay'` | Drives which bottom-bar variant renders. |
692
692
  | `gameInfo` | `GameInfoContent` | Sections for the game-info overlay (see below). |
693
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`. |
694
+ | `onBonusBuy` | `(() => void)?` | Override the BUY BONUS button action — opens your own UI instead of the built-in overlay (also shows the button without a `buyBonus` array). See [Buy bonus](#buy-bonus--features). |
694
695
 
695
696
  ### Events (`shell.on(name, handler)`)
696
697
 
@@ -738,6 +739,26 @@ shell.deactivateFeature(); // revert
738
739
 
739
740
  Each card price renders as `priceMultiplier × current bet` in the shell currency.
740
741
 
742
+ **Customisation.** Two override hooks let a game replace the built-in UI while keeping the
743
+ shell's buy flow:
744
+
745
+ ```typescript
746
+ // 1) Per-card UI — render your own card; the shell keeps the grid wrapper, accent vars and live
747
+ // re-pricing, and runs the normal confirm → buy flow when you call ctx.select().
748
+ { id: 'fs', title: 'Free Spins', description: '…', priceMultiplier: 100,
749
+ custom: ({ priceText, disabled, accent, select }) => {
750
+ const el = document.createElement('button');
751
+ el.textContent = priceText; el.disabled = disabled;
752
+ el.style.background = accent; el.addEventListener('click', select); // select() = internal flow
753
+ return el; // ctx also has { bonus, bet, price }
754
+ } }
755
+
756
+ // 2) Bar button action — open your OWN bonus UI instead of the built-in overlay.
757
+ createGameShell({ /* … */, onBonusBuy: () => myGame.openBonusScreen() });
758
+ ```
759
+
760
+ `onBonusBuy` also makes the BUY BONUS button appear without a `features.buyBonus` array.
761
+
741
762
  ### Game info (`gameInfo.sections`)
742
763
 
743
764
  The game-info overlay is composed from typed sections — declare what your game has and the shell
package/dist/index.cjs.js CHANGED
@@ -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);
@@ -1296,16 +1297,18 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1296
1297
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
1297
1298
  /* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
1298
1299
  browser window, so cards shrink to fit the real container height. */
1299
- #${SHELL_ROOT_ID} .ge-bb-grid .ge-bonus-card { flex:0 0 18.5em; scroll-snap-align:start;
1300
- 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); }
1301
1302
  /* mobile: vertical stack at a fixed, readable size — scroll the list, don't shrink the cards */
1302
1303
  #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid { display:flex; flex-direction:column; gap:14px; overflow:visible; }
1303
- #${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; }
1304
1305
  #${SHELL_ROOT_ID} .ge-bonus-card { display:flex; flex-direction:column; border-radius:1.4em; overflow:hidden;
1305
1306
  background:var(--shell-plaque-glass); border:1px solid var(--shell-plaque-line); color:#fff; text-align:center;
1306
1307
  pointer-events:auto; cursor:pointer; transition:box-shadow .12s ease, background .12s ease; }
1307
1308
  #${SHELL_ROOT_ID} .ge-bonus-card:hover:not(.ge-bonus-off) {
1308
1309
  box-shadow:0 0 0 1px var(--card-acc), 0 12px 34px -12px var(--card-acc); }
1310
+ /* custom card (BonusOption.custom): keep grid sizing + accent vars, drop the default chrome so the game owns the UI */
1311
+ #${SHELL_ROOT_ID} .ge-bonus-card--custom { background:none; border:none; cursor:default; }
1309
1312
  #${SHELL_ROOT_ID} .ge-bonus-body { display:flex; flex-direction:column; align-items:center; flex:1; padding:1.25em 1.1em .9em; }
1310
1313
  #${SHELL_ROOT_ID} .ge-bonus-title { font-size:1.3em; font-weight:800; letter-spacing:.04em; text-transform:uppercase;
1311
1314
  color:var(--card-acc); margin-bottom:.75em; }
@@ -1407,40 +1410,44 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1407
1410
  align-items:center; justify-content:center; padding:clamp(10px,4vh,24px); box-sizing:border-box;
1408
1411
  background:rgba(12,17,28,.5); backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%);
1409
1412
  -webkit-backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%); animation:ge-ov-in .16s ease-out; }
1410
- /* GameShell.fitModal() scales the whole card down (transform) so it fits short popouts uniformly */
1411
- #${SHELL_ROOT_ID} .ge-modal-card { width:100%; max-width:420px; box-sizing:border-box; overflow:hidden;
1412
- transform-origin:center center; background:var(--shell-plaque-solid); border-radius:20px; display:flex; flex-direction:column; }
1413
+ /* Card sizes in cq units of the shell root responsive on EVERY screen, not just popouts. The
1414
+ card's font-size is the one knob (clamped for readability); everything inside is em-relative so
1415
+ the whole card scales as a unit. GameShell.fitModal() still transform-scales it down as a
1416
+ backstop for very short popouts. */
1417
+ #${SHELL_ROOT_ID} .ge-modal-card { font-size:clamp(11px, 2cqmin, 15px); width:100%; max-width:28em; box-sizing:border-box;
1418
+ overflow:hidden; transform-origin:center center; background:var(--shell-plaque-solid); border-radius:1.3em;
1419
+ display:flex; flex-direction:column; }
1413
1420
  /* ✕ pinned to the overlay corner (the screen), not the card */
1414
1421
  #${SHELL_ROOT_ID} .ge-modal-close { position:absolute; top:12px; right:12px; z-index:2; width:36px; height:36px;
1415
1422
  border:none; border-radius:50%; cursor:pointer; pointer-events:auto; background:var(--shell-plaque-dark); color:#fff;
1416
1423
  display:flex; align-items:center; justify-content:center; font-size:20px; transition:background .12s ease, color .12s ease; }
1417
1424
  #${SHELL_ROOT_ID} .ge-modal-close:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
1418
- #${SHELL_ROOT_ID} .ge-modal-body { padding:18px; display:flex; flex-direction:column; gap:16px; }
1425
+ #${SHELL_ROOT_ID} .ge-modal-body { padding:1.2em; display:flex; flex-direction:column; gap:1.05em; }
1419
1426
  #${SHELL_ROOT_ID} .ge-modal-title { margin:0; text-align:center; color:var(--card-acc, var(--shell-accent));
1420
- font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:18px; }
1421
- #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:14px; line-height:1.5; }
1422
- #${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:10px; }
1427
+ font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:1.2em; }
1428
+ #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:.93em; line-height:1.5; }
1429
+ #${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:.65em; }
1423
1430
  #${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
1424
- border-radius:12px; background:rgba(255,255,255,.04); color:#fff; font-size:15px; font-weight:700;
1425
- font-variant-numeric:tabular-nums; padding:12px 8px; transition:background .12s ease, border-color .12s ease; }
1431
+ border-radius:.8em; background:rgba(255,255,255,.04); color:#fff; font-size:1em; font-weight:700;
1432
+ font-variant-numeric:tabular-nums; padding:.8em .55em; transition:background .12s ease, border-color .12s ease; }
1426
1433
  #${SHELL_ROOT_ID} .ge-chip:hover { background:var(--shell-plaque-glass-hover); }
1427
1434
  #${SHELL_ROOT_ID} .ge-chip.ge-on { border-color:var(--shell-accent); background:var(--shell-accent); color:#fff; }
1428
1435
  /* full-bleed footer button(s), flush to the card's bottom edge (card clips the corners) */
1429
1436
  #${SHELL_ROOT_ID} .ge-modal-actions { display:flex; }
1430
1437
  #${SHELL_ROOT_ID} .ge-modal-actions > * { flex:1; }
1431
- #${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:16px; font-size:15px; font-weight:800;
1438
+ #${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:1.05em; font-size:1em; font-weight:800;
1432
1439
  letter-spacing:.04em; text-transform:uppercase; cursor:pointer; pointer-events:auto; transition:filter .12s ease; }
1433
1440
  #${SHELL_ROOT_ID} .ge-modal-btn:hover:not([disabled]) { filter:brightness(1.08); }
1434
1441
  #${SHELL_ROOT_ID} .ge-modal-btn--accent { background:var(--card-acc, var(--shell-accent)); color:#fff; }
1435
1442
  #${SHELL_ROOT_ID} .ge-modal-btn--ghost { background:var(--shell-plaque-glass-hover); color:#fff; }
1436
1443
  /* replay summary — label/value rows, accented total-win row */
1437
1444
  #${SHELL_ROOT_ID} .ge-replay-rows { display:flex; flex-direction:column; }
1438
- #${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:16px; padding:11px 2px; }
1445
+ #${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:1.05em; padding:.73em .13em; }
1439
1446
  #${SHELL_ROOT_ID} .ge-replay-row + .ge-replay-row { border-top:1px solid var(--shell-plaque-line); }
1440
- #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:11px; font-weight:700; }
1441
- #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:15px; font-variant-numeric:tabular-nums; }
1442
- #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:12px; }
1443
- #${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:19px; }
1447
+ #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:.73em; font-weight:700; }
1448
+ #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:1em; font-variant-numeric:tabular-nums; }
1449
+ #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:.8em; }
1450
+ #${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:1.27em; }
1444
1451
 
1445
1452
  #${SHELL_ROOT_ID}.ge-shell-hidden { opacity:0; pointer-events:none; transition:opacity .25s ease; }
1446
1453
  `;
@@ -1659,7 +1666,7 @@ function renderBottomBar(shell) {
1659
1666
  shell.openBetPicker(); });
1660
1667
  spin = spinButton(shell);
1661
1668
  auto = config.features.autoplay ? autoButton(shell) : null;
1662
- buy = config.features.buyBonus !== false ? buyBtn(shell) : null;
1669
+ buy = (config.features.buyBonus !== false || config.onBonusBuy) ? buyBtn(shell) : null;
1663
1670
  }
1664
1671
  const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmt(state.win)) : null;
1665
1672
  // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
@@ -1916,7 +1923,7 @@ function openGameInfoModal(shell) {
1916
1923
  }
1917
1924
  function renderSection(shell, s) {
1918
1925
  switch (s.type) {
1919
- case 'modes': return sectionModes(s.modes, sec('info-modes', s.title, shell.t('Modes')));
1926
+ case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
1920
1927
  case 'controls': return sectionControls(shell, sec('info-controls', s.title, shell.t('Controls')));
1921
1928
  case 'paytable': return sectionPaytable(s.rows, sec('info-paytable', s.title, shell.t('Paytable')));
1922
1929
  case 'wins': return sectionWins(s, sec('info-wins', s.title, shell.t(winFallbackTitle(s.kind))));
@@ -1937,25 +1944,25 @@ function sec(ge, title, fallback) {
1937
1944
  return el;
1938
1945
  }
1939
1946
  // ── modes (rows — varying description lengths read better than fixed cards) ────
1940
- function sectionModes(modes, el) {
1947
+ function sectionModes(shell, modes, el) {
1941
1948
  const list = document.createElement('div');
1942
1949
  list.className = 'ge-gi-modes';
1943
1950
  for (const m of modes)
1944
- list.appendChild(modeRow(m));
1951
+ list.appendChild(modeRow(shell, m));
1945
1952
  el.appendChild(list);
1946
1953
  return el;
1947
1954
  }
1948
- function modeRow(m) {
1955
+ function modeRow(shell, m) {
1949
1956
  const row = document.createElement('div');
1950
1957
  row.className = 'ge-gi-mode';
1951
1958
  const stat = (label, val) => `<span class="ge-gi-mode-st"><span>${label}</span><b>${val}</b></span>`;
1952
1959
  let stats = '';
1953
1960
  if (m.price != null)
1954
- stats += stat('Price', m.price);
1961
+ stats += stat(shell.t('Price'), m.price);
1955
1962
  if (typeof m.rtp === 'number')
1956
- stats += stat('RTP', `${m.rtp}%`);
1963
+ stats += stat(shell.t('RTP'), `${m.rtp}%`);
1957
1964
  if (m.maxWin != null)
1958
- stats += stat('Max win', m.maxWin);
1965
+ stats += stat(shell.t('Max win'), m.maxWin);
1959
1966
  row.innerHTML =
1960
1967
  `<div class="ge-gi-mode-top"><span class="ge-gi-mode-h">${m.title}</span>` +
1961
1968
  (stats ? `<span class="ge-gi-mode-stats">${stats}</span>` : '') + '</div>' +
@@ -2244,20 +2251,39 @@ function buildCard(shell, bonus, overlay) {
2244
2251
  card.dataset.ge = `bonus-card-${bonus.id}`;
2245
2252
  card.style.setProperty('--card-acc', accent);
2246
2253
  card.style.setProperty('--card-ink', contrastText(accent));
2254
+ const enabled = isAffordable(shell, bonus);
2255
+ // Stack the confirm on top of the overlay grid (cancel returns to the grid). Re-checks
2256
+ // affordability at click time, so it's a safe no-op when the option can't be bought.
2257
+ const select = () => {
2258
+ if (!isAffordable(shell, bonus))
2259
+ return;
2260
+ overlay.appendChild(buildConfirm(shell, bonus, overlay));
2261
+ shell.fitModals();
2262
+ };
2263
+ // Game-supplied card UI: the shell keeps the wrapper (grid sizing + accent vars) and runs the
2264
+ // buy flow when the game calls ctx.select(); the game owns everything inside.
2265
+ if (bonus.custom) {
2266
+ card.classList.add('ge-bonus-card--custom');
2267
+ const price = bonus.priceMultiplier * shell.state.bet;
2268
+ card.appendChild(bonus.custom({
2269
+ bonus, bet: shell.state.bet, price,
2270
+ priceText: formatCurrency(price, shell.config.currency),
2271
+ disabled: !enabled, accent, select,
2272
+ }));
2273
+ return card;
2274
+ }
2247
2275
  card.appendChild(cardBody(shell, bonus));
2248
2276
  const cta = document.createElement('button');
2249
2277
  cta.className = 'ge-bonus-cta';
2250
2278
  cta.dataset.ge = `bonus-cta-${bonus.id}`;
2251
2279
  cta.textContent = shell.t(actionLabel(bonus));
2252
2280
  card.appendChild(cta);
2253
- const enabled = isAffordable(shell, bonus);
2254
2281
  if (!enabled) {
2255
2282
  card.classList.add('ge-bonus-off');
2256
2283
  cta.disabled = true;
2257
2284
  }
2258
2285
  else {
2259
- // Stack the confirm on top of the overlay grid (cancel returns to the grid).
2260
- card.addEventListener('click', () => { overlay.appendChild(buildConfirm(shell, bonus, overlay)); shell.fitModals(); });
2286
+ card.addEventListener('click', select);
2261
2287
  }
2262
2288
  return card;
2263
2289
  }
@@ -2583,6 +2609,7 @@ const RULES = [
2583
2609
  ['paid', 'won'],
2584
2610
  ['bought', 'instantly triggered'],
2585
2611
  ['purchase', 'play'],
2612
+ ['price', 'play'],
2586
2613
  ['deposit', 'get coins'],
2587
2614
  ['withdraw', 'redeem'],
2588
2615
  ['currency', 'token'],
@@ -2846,6 +2873,10 @@ class GameShell extends EventEmitter {
2846
2873
  openSettings() { this.emit('settingsOpen'); this.showModal(openSettingsModal(this)); }
2847
2874
  openInfo() { this.emit('infoOpen'); this.showModal(openGameInfoModal(this)); }
2848
2875
  openBuyBonus() {
2876
+ if (this.config.onBonusBuy) {
2877
+ this.config.onBonusBuy();
2878
+ return;
2879
+ } // game handles it (own UI)
2849
2880
  const overlay = openBuyBonusOverlay(this);
2850
2881
  if (overlay)
2851
2882
  this.showModal(overlay);