@energy8platform/platform-core 0.23.3 → 0.24.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
@@ -684,7 +684,7 @@ await removeGameShell();
684
684
  | `theme` | `ThemeConfig?` | `{ scheme?: 'dark' \| 'light', accent? }`. Defaults to dark. `accent` also tints the BUY BONUS button; per-card accents are `BonusOption.accentColor`. |
685
685
  | `language` | `string` | Currently `'en'` is the source language. |
686
686
  | `isSocial` | `boolean?` | Swap built-in text to social-casino vocabulary (bet → play, win → …). Game-supplied strings are untouched. |
687
- | `currency` | `CurrencyConfig` | `{ symbol, position: 'left'\|'right', decimals?, minDecimals?, separator? }`. |
687
+ | `currency` | `CurrencyConfig` | `{ symbol, position: 'left'\|'right', maxDecimals?, minDecimals?, separator? }`. `maxDecimals` (default 2) / `minDecimals` (default `maxDecimals`): **win & total-win** show up to `maxDecimals`, trimming trailing zeros down to `minDecimals`; **balance / bet / prices** stay fixed at `minDecimals`. |
688
688
  | `availableBets` | `number[]` | Bet ladder shown in the bet picker. |
689
689
  | `defaultBet` / `currentBet` | `number` / `number \| null` | `currentBet` restores a saved bet; `null` falls back to `defaultBet`. |
690
690
  | `balance` / `win` | `number` | Initial readouts. |
@@ -715,7 +715,8 @@ the previous value.
715
715
  shell.setBalance(n); shell.setWin(n); shell.setBet(n);
716
716
  shell.setBusy(true); // disables controls mid-spin
717
717
  shell.setMode('freeSpins');
718
- shell.setFreeSpins({ current: 1, total: 10, totalWin: 0 }); // Free Spins + Total Win bar readout
718
+ shell.setFreeSpins({ current: 1, total: 10, totalWin: 0 }); // counter shows "1 / 10"
719
+ shell.setFreeSpins({ total: 9, totalWin: 0 }); // current omitted/null → single number "9" (decrement it for a countdown)
719
720
  shell.setAutoplay({ active: true, remaining: 25 });
720
721
  shell.setTurbo(2);
721
722
  shell.setBuyBonusEnabled(false); // grey out BUY BONUS (e.g. insufficient balance)
@@ -768,7 +769,8 @@ draws the rest:
768
769
  - `{ type: 'controls' }` — auto-generated control legend.
769
770
  - `{ type: 'paytable', rows: PaytableRow[] }` — symbol → win tiers (`"<count> x<multiplier>"`).
770
771
  - `{ type: 'wins', kind, grid, … }` — auto-drawn win illustration. `kind` is `'classic'` (paylines),
771
- `'cluster'`, `'anywhere'`, or `'ways'`.
772
+ `'cluster'`, `'anywhere'`, `'ways'`, or `'shapes'` — `{ kind: 'shapes', shapes: ShapeDef[] }` lists
773
+ named cell patterns (`{ cells: CellRef[], name, description? }`) as a grid-illustration row each.
772
774
  - `{ type: 'custom', title, html | node }` — your own rules markup.
773
775
 
774
776
  ```typescript
package/dist/index.cjs.js CHANGED
@@ -1278,6 +1278,12 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1278
1278
  #${SHELL_ROOT_ID} .ge-gi-mode-st span { color:var(--shell-plaque-label); font-size:10px; letter-spacing:.1em; text-transform:uppercase; }
1279
1279
  #${SHELL_ROOT_ID} .ge-gi-mode-st b { color:#fff; font-size:14px; font-weight:800; font-variant-numeric:tabular-nums; }
1280
1280
  #${SHELL_ROOT_ID} .ge-gi-mode-desc { color:rgba(255,255,255,.78); font-size:14px; line-height:1.5; margin:0; }
1281
+ /* shapes — row list (grid illustration left, name + description right), modes-style text */
1282
+ #${SHELL_ROOT_ID} .ge-gi-shapes { display:flex; flex-direction:column; }
1283
+ #${SHELL_ROOT_ID} .ge-gi-shape { display:flex; align-items:center; gap:16px; padding:12px 0; }
1284
+ #${SHELL_ROOT_ID} .ge-gi-shape + .ge-gi-shape { border-top:1px solid var(--shell-plaque-line); }
1285
+ #${SHELL_ROOT_ID} .ge-gi-shape .ge-gi-pl-svg { flex:0 0 auto; width:96px; }
1286
+ #${SHELL_ROOT_ID} .ge-gi-shape-tx { flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; }
1281
1287
  #${SHELL_ROOT_ID} .ge-gi-custom { color:rgba(255,255,255,.88); font-size:15px; line-height:1.6; }
1282
1288
 
1283
1289
  /* buy bonus cards — art-forward, centred, flat (no gradients); --card-acc/--card-ink per card.
@@ -1452,22 +1458,25 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1452
1458
 
1453
1459
  /** The shared money formatter for every shell readout (balance, win, total win, bet, prices).
1454
1460
  *
1455
- * `decimals` is the MAXIMUM number of fraction digits; `minDecimals` (defaults to `decimals`)
1456
- * is the MINIMUM. The value is rounded to `decimals`, then trailing zeros are trimmed down to
1457
- * — but never past `minDecimals`. With `minDecimals` unset both bounds are equal, so the
1458
- * output is always exactly `decimals` places (the classic behaviour).
1461
+ * `maxDecimals` is the MAXIMUM fraction digits (default 2); `minDecimals` (defaults to
1462
+ * `maxDecimals`) is the MINIMUM. By default the value is shown at exactly `minDecimals` places.
1463
+ * With `variableDecimals` used only for win / total-win it is rounded to `maxDecimals`, then
1464
+ * trailing zeros are trimmed down to (but never past) `minDecimals`, so small wins keep their
1465
+ * significant digits. Balance / bet / prices stay fixed at `minDecimals`.
1459
1466
  *
1460
- * Example with `decimals: 4, minDecimals: 2`:
1461
- * 0.0673 → 0,0673 0.0670 → 0,067 0.0600 → 0,06
1462
- * 0.0004 → 0,0004 0.0040 → 0,004 0.3000 → 0,30 0.0000 → 0,00
1467
+ * Example with `maxDecimals: 4, minDecimals: 2`:
1468
+ * fixed → 0.0673 → 0,07 0.3 → 0,30
1469
+ * variable → 0.0673 → 0,0673 0.067 → 0,067 0.3 → 0,30 0 → 0,00
1463
1470
  */
1464
- function formatCurrency(value, currency) {
1465
- const decimals = currency.decimals ?? 2;
1466
- const minDecimals = Math.max(0, Math.min(decimals, currency.minDecimals ?? decimals));
1471
+ function formatCurrency(value, currency, variableDecimals = false) {
1472
+ const maxDecimals = currency.maxDecimals ?? 2;
1473
+ const minDecimals = Math.max(0, Math.min(maxDecimals, currency.minDecimals ?? maxDecimals));
1467
1474
  const thousands = currency.separator?.thousands ?? '.';
1468
1475
  const decimal = currency.separator?.decimal ?? ',';
1469
1476
  const safe = Number.isFinite(value) ? value : 0;
1470
- const fixed = safe.toFixed(decimals); // round at the max precision, e.g. "0.0670"
1477
+ // fixed callers round at minDecimals; variable callers round at maxDecimals then trim back down.
1478
+ const places = variableDecimals ? maxDecimals : minDecimals;
1479
+ const fixed = safe.toFixed(places);
1471
1480
  const [intPart, rawFrac = ''] = fixed.split('.');
1472
1481
  // trim trailing zeros, but keep at least `minDecimals` fraction digits
1473
1482
  let fracPart = rawFrac;
@@ -1628,6 +1637,7 @@ function iconBtn(ge, name, onClick, active = false) {
1628
1637
  function renderBottomBar(shell) {
1629
1638
  const { state, config } = shell;
1630
1639
  const fmt = (n) => formatCurrency(n, config.currency);
1640
+ const fmtWin = (n) => formatCurrency(n, config.currency, true); // win / total-win: variable decimals
1631
1641
  const mobile = shell.layout === 'mobile';
1632
1642
  const bar = document.createElement('div');
1633
1643
  bar.className = 'ge-shell-bottom';
@@ -1671,10 +1681,13 @@ function renderBottomBar(shell) {
1671
1681
  auto = config.features.autoplay ? autoButton(shell) : null;
1672
1682
  buy = (config.features.buyBonus !== false || config.onBonusBuy) ? buyBtn(shell) : null;
1673
1683
  }
1674
- const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmt(state.win)) : null;
1684
+ const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmtWin(state.win)) : null;
1675
1685
  // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
1676
- const fsCounter = showFsBlocks ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
1677
- const fsTotalWin = showFsBlocks ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
1686
+ // current = number "current / total"; current = null/undefined → just the (game-driven) total.
1687
+ const fs = state.freeSpins;
1688
+ const fsText = fs.current == null ? `${fs.total}` : `${fs.current} / ${fs.total}`;
1689
+ const fsCounter = showFsBlocks ? readout('fs-counter', shell.t('Free spins'), fsText) : null;
1690
+ const fsTotalWin = showFsBlocks ? readout('fs-totalwin', shell.t('Total win'), fmtWin(fs.totalWin)) : null;
1678
1691
  if (mobile) {
1679
1692
  // rows: [balance · win] · [menu · auto · spin · FS counter · Total Win · turbo · buy] · [− bet +]
1680
1693
  // FS counter + Total Win live in the controls row (alongside menu/turbo), not the top readouts.
@@ -2050,7 +2063,7 @@ function paytableCard(r) {
2050
2063
  }
2051
2064
  // ── wins (one section = one pay type; cells filled in the accent colour, no line) ──
2052
2065
  function winFallbackTitle(kind) {
2053
- return { classic: 'Paylines', cluster: 'Cluster pays', anywhere: 'Pays anywhere', ways: 'Ways to win' }[kind];
2066
+ return { classic: 'Paylines', cluster: 'Cluster pays', anywhere: 'Pays anywhere', ways: 'Ways to win', shapes: 'Winning shapes' }[kind];
2054
2067
  }
2055
2068
  function sectionWins(s, el) {
2056
2069
  if (s.kind === 'classic') {
@@ -2074,6 +2087,15 @@ function sectionWins(s, el) {
2074
2087
  row.appendChild(winDesc(s.description));
2075
2088
  el.appendChild(row);
2076
2089
  }
2090
+ else if (s.kind === 'shapes') {
2091
+ if (s.description)
2092
+ el.appendChild(winDesc(s.description));
2093
+ const list = document.createElement('div');
2094
+ list.className = 'ge-gi-shapes';
2095
+ for (const sh of s.shapes)
2096
+ list.appendChild(shapeRow(s.grid, sh));
2097
+ el.appendChild(list);
2098
+ }
2077
2099
  else {
2078
2100
  if (s.description)
2079
2101
  el.appendChild(winDesc(s.description));
@@ -2084,6 +2106,25 @@ function sectionWins(s, el) {
2084
2106
  }
2085
2107
  return el;
2086
2108
  }
2109
+ /** One named shape: grid illustration (left) + name and optional description (right) — modes-style. */
2110
+ function shapeRow(grid, sh) {
2111
+ const row = document.createElement('div');
2112
+ row.className = 'ge-gi-shape';
2113
+ const tx = document.createElement('div');
2114
+ tx.className = 'ge-gi-shape-tx';
2115
+ const h = document.createElement('b');
2116
+ h.className = 'ge-gi-mode-h';
2117
+ h.textContent = sh.name;
2118
+ tx.appendChild(h);
2119
+ if (sh.description) {
2120
+ const p = document.createElement('p');
2121
+ p.className = 'ge-gi-mode-desc';
2122
+ p.textContent = sh.description;
2123
+ tx.appendChild(p);
2124
+ }
2125
+ row.append(gridSvg(grid, sh.cells), tx);
2126
+ return row;
2127
+ }
2087
2128
  function winDesc(text) {
2088
2129
  const p = document.createElement('p');
2089
2130
  p.className = 'ge-gi-win-desc';
@@ -2490,6 +2531,7 @@ function buildModal(opts) {
2490
2531
  function buildReplayModal(shell, opts) {
2491
2532
  const { bonusId, bet, payoutMultiplier } = opts;
2492
2533
  const fmt = (n) => formatCurrency(n, shell.config.currency);
2534
+ const fmtWin = (n) => formatCurrency(n, shell.config.currency, true); // total win: variable decimals
2493
2535
  const bonus = Array.isArray(shell.config.features.buyBonus)
2494
2536
  ? shell.config.features.buyBonus.find((b) => b.id === bonusId)
2495
2537
  : undefined;
@@ -2518,7 +2560,7 @@ function buildReplayModal(shell, opts) {
2518
2560
  row('Cost multiplier', `${costMultiplier}×`);
2519
2561
  row('Total cost bet', fmt(bet * costMultiplier));
2520
2562
  row('Payout multiplier', `${payoutMultiplier}×`);
2521
- row('Total win', fmt(payoutMultiplier * bet), true);
2563
+ row('Total win', fmtWin(payoutMultiplier * bet), true);
2522
2564
  ui.body.appendChild(rows);
2523
2565
  const actions = document.createElement('div');
2524
2566
  actions.className = 'ge-modal-actions';
@@ -2807,12 +2849,13 @@ class GameShell extends EventEmitter {
2807
2849
  }
2808
2850
  animateMoney() {
2809
2851
  const fmt = (n) => formatCurrency(n, this.config.currency);
2852
+ const fmtWin = (n) => formatCurrency(n, this.config.currency, true); // win: variable decimals
2810
2853
  const bal = this.barHost.querySelector('[data-ge="balance"]');
2811
2854
  const win = this.barHost.querySelector('[data-ge="win"]');
2812
2855
  if (bal && this.state.balance !== this.prevBalance)
2813
2856
  this.moneyAnims.push(animateReadout(bal, this.prevBalance, this.state.balance, fmt));
2814
2857
  if (win && this.state.win !== this.prevWin)
2815
- this.moneyAnims.push(animateReadout(win, this.prevWin, this.state.win, fmt));
2858
+ this.moneyAnims.push(animateReadout(win, this.prevWin, this.state.win, fmtWin));
2816
2859
  this.prevBalance = this.state.balance;
2817
2860
  this.prevWin = this.state.win;
2818
2861
  }