@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 +21 -0
- package/dist/index.cjs.js +60 -29
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.esm.js +60 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/shell.cjs.js +60 -29
- package/dist/shell.cjs.js.map +1 -1
- package/dist/shell.d.ts +27 -1
- package/dist/shell.esm.js +60 -29
- package/dist/shell.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/shell/GameShell.ts +1 -0
- package/src/shell/components/BottomBar.ts +1 -1
- package/src/shell/components/BuyBonus.ts +24 -4
- package/src/shell/components/GameInfo.ts +7 -7
- package/src/shell/i18n.ts +1 -0
- package/src/shell/shell.css.ts +25 -18
- package/src/shell/types.ts +27 -0
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
|
|
1300
|
-
font-size:clamp(7px,
|
|
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:
|
|
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
|
-
/*
|
|
1411
|
-
|
|
1412
|
-
|
|
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:
|
|
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:
|
|
1421
|
-
#${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size
|
|
1422
|
-
#${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap
|
|
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
|
|
1425
|
-
font-variant-numeric:tabular-nums; padding
|
|
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:
|
|
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:
|
|
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
|
|
1441
|
-
#${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:
|
|
1442
|
-
#${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size
|
|
1443
|
-
#${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:
|
|
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
|
-
|
|
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);
|