@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.
@@ -28,7 +28,7 @@ export function openGameInfoModal(shell: GameShell): HTMLElement {
28
28
 
29
29
  function renderSection(shell: GameShell, s: GameInfoSection): HTMLElement {
30
30
  switch (s.type) {
31
- case 'modes': return sectionModes(s.modes, sec('info-modes', s.title, shell.t('Modes')));
31
+ case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
32
32
  case 'controls': return sectionControls(shell, sec('info-controls', s.title, shell.t('Controls')));
33
33
  case 'paytable': return sectionPaytable(s.rows, sec('info-paytable', s.title, shell.t('Paytable')));
34
34
  case 'wins': return sectionWins(s, sec('info-wins', s.title, shell.t(winFallbackTitle(s.kind))));
@@ -46,20 +46,20 @@ function sec(ge: string, title: string | undefined, fallback: string): HTMLEleme
46
46
  }
47
47
 
48
48
  // ── modes (rows — varying description lengths read better than fixed cards) ────
49
- function sectionModes(modes: GameMode[], el: HTMLElement): HTMLElement {
49
+ function sectionModes(shell: GameShell, modes: GameMode[], el: HTMLElement): HTMLElement {
50
50
  const list = document.createElement('div'); list.className = 'ge-gi-modes';
51
- for (const m of modes) list.appendChild(modeRow(m));
51
+ for (const m of modes) list.appendChild(modeRow(shell, m));
52
52
  el.appendChild(list);
53
53
  return el;
54
54
  }
55
- function modeRow(m: GameMode): HTMLElement {
55
+ function modeRow(shell: GameShell, m: GameMode): HTMLElement {
56
56
  const row = document.createElement('div'); row.className = 'ge-gi-mode';
57
57
  const stat = (label: string, val: string) =>
58
58
  `<span class="ge-gi-mode-st"><span>${label}</span><b>${val}</b></span>`;
59
59
  let stats = '';
60
- if (m.price != null) stats += stat('Price', m.price);
61
- if (typeof m.rtp === 'number') stats += stat('RTP', `${m.rtp}%`);
62
- if (m.maxWin != null) stats += stat('Max win', m.maxWin);
60
+ if (m.price != null) stats += stat(shell.t('Price'), m.price);
61
+ if (typeof m.rtp === 'number') stats += stat(shell.t('RTP'), `${m.rtp}%`);
62
+ if (m.maxWin != null) stats += stat(shell.t('Max win'), m.maxWin);
63
63
  row.innerHTML =
64
64
  `<div class="ge-gi-mode-top"><span class="ge-gi-mode-h">${m.title}</span>` +
65
65
  (stats ? `<span class="ge-gi-mode-stats">${stats}</span>` : '') + '</div>' +
package/src/shell/i18n.ts CHANGED
@@ -30,6 +30,7 @@ const RULES: ReadonlyArray<readonly [string, string]> = [
30
30
  ['paid', 'won'],
31
31
  ['bought', 'instantly triggered'],
32
32
  ['purchase', 'play'],
33
+ ['price', 'play'],
33
34
  ['deposit', 'get coins'],
34
35
  ['withdraw', 'redeem'],
35
36
  ['currency', 'token'],
@@ -7,6 +7,7 @@ export const SHELL_ROOT_ID = '__ge-game-shell__';
7
7
  export const SHELL_CSS = SHELL_FONT_CSS + `
8
8
  #${SHELL_ROOT_ID} {
9
9
  position: absolute; inset: 0;
10
+ container-type: size; /* query container → centred modals size in cq units (responsive on every screen) */
10
11
  pointer-events: none; z-index: 9000;
11
12
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12
13
  color: var(--shell-fg);
@@ -225,16 +226,18 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
225
226
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
226
227
  /* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
227
228
  browser window, so cards shrink to fit the real container height. */
228
- #${SHELL_ROOT_ID} .ge-bb-grid .ge-bonus-card { flex:0 0 18.5em; scroll-snap-align:start;
229
- font-size:clamp(7px, 4cqh, 13px); }
229
+ #${SHELL_ROOT_ID} .ge-bb-grid .ge-bonus-card { flex:0 0 18em; scroll-snap-align:start;
230
+ font-size:clamp(7px, 3.6cqh, 12px); }
230
231
  /* mobile: vertical stack at a fixed, readable size — scroll the list, don't shrink the cards */
231
232
  #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid { display:flex; flex-direction:column; gap:14px; overflow:visible; }
232
- #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid .ge-bonus-card { flex:0 0 auto; font-size:13px; }
233
+ #${SHELL_ROOT_ID}.ge-mobile .ge-bb-grid .ge-bonus-card { flex:0 0 auto; font-size:12px; }
233
234
  #${SHELL_ROOT_ID} .ge-bonus-card { display:flex; flex-direction:column; border-radius:1.4em; overflow:hidden;
234
235
  background:var(--shell-plaque-glass); border:1px solid var(--shell-plaque-line); color:#fff; text-align:center;
235
236
  pointer-events:auto; cursor:pointer; transition:box-shadow .12s ease, background .12s ease; }
236
237
  #${SHELL_ROOT_ID} .ge-bonus-card:hover:not(.ge-bonus-off) {
237
238
  box-shadow:0 0 0 1px var(--card-acc), 0 12px 34px -12px var(--card-acc); }
239
+ /* custom card (BonusOption.custom): keep grid sizing + accent vars, drop the default chrome so the game owns the UI */
240
+ #${SHELL_ROOT_ID} .ge-bonus-card--custom { background:none; border:none; cursor:default; }
238
241
  #${SHELL_ROOT_ID} .ge-bonus-body { display:flex; flex-direction:column; align-items:center; flex:1; padding:1.25em 1.1em .9em; }
239
242
  #${SHELL_ROOT_ID} .ge-bonus-title { font-size:1.3em; font-weight:800; letter-spacing:.04em; text-transform:uppercase;
240
243
  color:var(--card-acc); margin-bottom:.75em; }
@@ -336,40 +339,44 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
336
339
  align-items:center; justify-content:center; padding:clamp(10px,4vh,24px); box-sizing:border-box;
337
340
  background:rgba(12,17,28,.5); backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%);
338
341
  -webkit-backdrop-filter:blur(var(--ge-sheet-blur,20px)) saturate(120%); animation:ge-ov-in .16s ease-out; }
339
- /* GameShell.fitModal() scales the whole card down (transform) so it fits short popouts uniformly */
340
- #${SHELL_ROOT_ID} .ge-modal-card { width:100%; max-width:420px; box-sizing:border-box; overflow:hidden;
341
- transform-origin:center center; background:var(--shell-plaque-solid); border-radius:20px; display:flex; flex-direction:column; }
342
+ /* Card sizes in cq units of the shell root responsive on EVERY screen, not just popouts. The
343
+ card's font-size is the one knob (clamped for readability); everything inside is em-relative so
344
+ the whole card scales as a unit. GameShell.fitModal() still transform-scales it down as a
345
+ backstop for very short popouts. */
346
+ #${SHELL_ROOT_ID} .ge-modal-card { font-size:clamp(11px, 2cqmin, 15px); width:100%; max-width:28em; box-sizing:border-box;
347
+ overflow:hidden; transform-origin:center center; background:var(--shell-plaque-solid); border-radius:1.3em;
348
+ display:flex; flex-direction:column; }
342
349
  /* ✕ pinned to the overlay corner (the screen), not the card */
343
350
  #${SHELL_ROOT_ID} .ge-modal-close { position:absolute; top:12px; right:12px; z-index:2; width:36px; height:36px;
344
351
  border:none; border-radius:50%; cursor:pointer; pointer-events:auto; background:var(--shell-plaque-dark); color:#fff;
345
352
  display:flex; align-items:center; justify-content:center; font-size:20px; transition:background .12s ease, color .12s ease; }
346
353
  #${SHELL_ROOT_ID} .ge-modal-close:hover { background:var(--shell-plaque-glass); color:var(--shell-accent); }
347
- #${SHELL_ROOT_ID} .ge-modal-body { padding:18px; display:flex; flex-direction:column; gap:16px; }
354
+ #${SHELL_ROOT_ID} .ge-modal-body { padding:1.2em; display:flex; flex-direction:column; gap:1.05em; }
348
355
  #${SHELL_ROOT_ID} .ge-modal-title { margin:0; text-align:center; color:var(--card-acc, var(--shell-accent));
349
- font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:18px; }
350
- #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:14px; line-height:1.5; }
351
- #${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:10px; }
356
+ font-weight:800; letter-spacing:.04em; text-transform:uppercase; font-size:1.2em; }
357
+ #${SHELL_ROOT_ID} .ge-modal-text { margin:0; text-align:center; color:rgba(255,255,255,.85); font-size:.93em; line-height:1.5; }
358
+ #${SHELL_ROOT_ID} .ge-sheet-grid { display:grid; gap:.65em; }
352
359
  #${SHELL_ROOT_ID} .ge-chip { pointer-events:auto; cursor:pointer; border:1px solid var(--shell-plaque-line);
353
- border-radius:12px; background:rgba(255,255,255,.04); color:#fff; font-size:15px; font-weight:700;
354
- font-variant-numeric:tabular-nums; padding:12px 8px; transition:background .12s ease, border-color .12s ease; }
360
+ border-radius:.8em; background:rgba(255,255,255,.04); color:#fff; font-size:1em; font-weight:700;
361
+ font-variant-numeric:tabular-nums; padding:.8em .55em; transition:background .12s ease, border-color .12s ease; }
355
362
  #${SHELL_ROOT_ID} .ge-chip:hover { background:var(--shell-plaque-glass-hover); }
356
363
  #${SHELL_ROOT_ID} .ge-chip.ge-on { border-color:var(--shell-accent); background:var(--shell-accent); color:#fff; }
357
364
  /* full-bleed footer button(s), flush to the card's bottom edge (card clips the corners) */
358
365
  #${SHELL_ROOT_ID} .ge-modal-actions { display:flex; }
359
366
  #${SHELL_ROOT_ID} .ge-modal-actions > * { flex:1; }
360
- #${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:16px; font-size:15px; font-weight:800;
367
+ #${SHELL_ROOT_ID} .ge-modal-btn { width:100%; border:none; padding:1.05em; font-size:1em; font-weight:800;
361
368
  letter-spacing:.04em; text-transform:uppercase; cursor:pointer; pointer-events:auto; transition:filter .12s ease; }
362
369
  #${SHELL_ROOT_ID} .ge-modal-btn:hover:not([disabled]) { filter:brightness(1.08); }
363
370
  #${SHELL_ROOT_ID} .ge-modal-btn--accent { background:var(--card-acc, var(--shell-accent)); color:#fff; }
364
371
  #${SHELL_ROOT_ID} .ge-modal-btn--ghost { background:var(--shell-plaque-glass-hover); color:#fff; }
365
372
  /* replay summary — label/value rows, accented total-win row */
366
373
  #${SHELL_ROOT_ID} .ge-replay-rows { display:flex; flex-direction:column; }
367
- #${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:16px; padding:11px 2px; }
374
+ #${SHELL_ROOT_ID} .ge-replay-row { display:flex; justify-content:space-between; align-items:baseline; gap:1.05em; padding:.73em .13em; }
368
375
  #${SHELL_ROOT_ID} .ge-replay-row + .ge-replay-row { border-top:1px solid var(--shell-plaque-line); }
369
- #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:11px; font-weight:700; }
370
- #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:15px; font-variant-numeric:tabular-nums; }
371
- #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:12px; }
372
- #${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:19px; }
376
+ #${SHELL_ROOT_ID} .ge-replay-row span { color:var(--shell-plaque-label); text-transform:uppercase; letter-spacing:.07em; font-size:.73em; font-weight:700; }
377
+ #${SHELL_ROOT_ID} .ge-replay-row b { color:#fff; font-weight:800; font-size:1em; font-variant-numeric:tabular-nums; }
378
+ #${SHELL_ROOT_ID} .ge-replay-total span { color:#fff; font-size:.8em; }
379
+ #${SHELL_ROOT_ID} .ge-replay-total b { color:var(--shell-accent); font-size:1.27em; }
373
380
 
374
381
  #${SHELL_ROOT_ID}.ge-shell-hidden { opacity:0; pointer-events:none; transition:opacity .25s ease; }
375
382
  `;
@@ -26,6 +26,29 @@ export interface BonusOption {
26
26
  priceMultiplier: number;
27
27
  /** Per-option accent override. Falls back to the type default (bonus → purple, feature → gold). */
28
28
  accentColor?: string;
29
+ /** Override the card UI. Return the card's inner content; the shell keeps the grid wrapper,
30
+ * accent vars and live re-pricing, and runs the normal buy flow when you call `ctx.select()`. */
31
+ custom?: (ctx: BonusCardContext) => HTMLElement;
32
+ }
33
+
34
+ /** Context passed to a `BonusOption.custom` renderer. Render the card however you like and wire
35
+ * your own control to `select()` — the buy/confirm flow stays internal to the shell. */
36
+ export interface BonusCardContext {
37
+ bonus: BonusOption;
38
+ /** Current bet. */
39
+ bet: number;
40
+ /** Card price = `bonus.priceMultiplier × bet`. */
41
+ price: number;
42
+ /** `price` formatted in the shell currency. */
43
+ priceText: string;
44
+ /** True when the option can't be bought right now (unaffordable / busy / buy-bonus disabled);
45
+ * reflect it in your UI. `select()` is a no-op while disabled. */
46
+ disabled: boolean;
47
+ /** Card accent (per-option override or the type default); also set as the `--card-acc` CSS var. */
48
+ accent: string;
49
+ /** Proceed through the shell's normal flow: opens the confirm modal, then emits `buyBonusSelect`
50
+ * / activates the feature. No-op while `disabled`. */
51
+ select: () => void;
29
52
  }
30
53
 
31
54
  export interface ThemeConfig {
@@ -168,6 +191,10 @@ export interface ShellConfig {
168
191
  win: number;
169
192
  mode: ShellMode;
170
193
  features: ShellFeatures;
194
+ /** Override the BUY BONUS bar button's action: when set, tapping it calls this instead of
195
+ * opening the built-in buy-bonus overlay (e.g. the game shows its own bonus UI). The button
196
+ * is shown whenever this OR `features.buyBonus` is set. */
197
+ onBonusBuy?: () => void;
171
198
  }
172
199
 
173
200
  export interface ShellState {