@energy8platform/platform-core 0.21.0 → 0.23.0

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.
@@ -43,10 +43,13 @@ export function renderBottomBar(shell: GameShell): HTMLElement {
43
43
  // menu icon button (always)
44
44
  const menu = iconBtn('menu', 'menu', () => shell.openMenu());
45
45
 
46
- // All three modes share the base plaque layout. FS/replay hide the controls that
47
- // don't apply; FS puts the spins counter in the centre pill (where WIN normally is).
46
+ // All three modes share the base plaque layout. FS/replay hide the controls that don't apply
47
+ // and add Free Spins + Total Win blocks on the left; the per-spin WIN uses the base pill.
48
48
  const isBase = state.mode === 'base';
49
49
  const isFS = state.mode === 'freeSpins';
50
+ // FS always shows the spins counter + accumulated Total Win (even €0); a replay shows them
51
+ // only when it's a free-spins replay (freeSpins.total > 0).
52
+ const showFsBlocks = isFS || (state.mode === 'replay' && state.freeSpins.total > 0);
50
53
 
51
54
  const balance = readout('balance', shell.t('Balance'), fmt(state.balance));
52
55
  // With a feature active (e.g. Ante) the BET readout shows the effective stake, tinted with
@@ -72,22 +75,25 @@ export function renderBottomBar(shell: GameShell): HTMLElement {
72
75
  }
73
76
 
74
77
  const winEl = state.win > 0 ? readout('win', shell.t('Win'), fmt(state.win)) : null;
75
- // FS readouts the spins counter plus the accumulated/last win for the round.
76
- const fsCounter = isFS ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
77
- const fsTotalWin = isFS ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
78
- const fsLastWin = isFS ? readout('fs-lastwin', shell.t('Last win'), fmt(state.freeSpins.lastWin)) : null;
78
+ // FS/replay left blocks: spins counter + accumulated Total Win (shown even at €0).
79
+ const fsCounter = showFsBlocks ? readout('fs-counter', shell.t('Free spins'), `${state.freeSpins.current} / ${state.freeSpins.total}`) : null;
80
+ const fsTotalWin = showFsBlocks ? readout('fs-totalwin', shell.t('Total win'), fmt(state.freeSpins.totalWin)) : null;
79
81
 
80
82
  if (mobile) {
81
- // rows: [balance · win/(FS last+total)] · [menu · auto · (spin | FS counter) · turbo · buy] · [− bet +]
82
- bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl, fsLastWin, fsTotalWin])));
83
- const center = isBase ? spin : fsCounter;
84
- bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, turbo, buy])));
83
+ // rows: [balance · win] · [menu · auto · spin · FS counter · Total Win · turbo · buy] · [− bet +]
84
+ // FS counter + Total Win live in the controls row (alongside menu/turbo), not the top readouts.
85
+ bar.appendChild(plaque('ge-m-top ge-pl ge-pl-glass', compact([balance, winEl])));
86
+ const center = isBase ? spin : null;
87
+ bar.appendChild(plaque('ge-m-controls ge-pl-dark', compact([menu, auto, center, fsCounter, fsTotalWin, turbo, buy])));
85
88
  bar.appendChild(plaque('ge-m-bet ge-pl ge-pl-dark', compact([betDown, betValue, betUp])));
86
89
  } else {
87
- // LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance]
90
+ // LEFT: [menu] ⊐ BUY BONUS coin ⊏ [balance] · [Free Spins] · [Total Win]
91
+ // (the last two only render in FS / a free-spins replay)
88
92
  const menuPlaque = plaque('ge-pl ge-pl-dark ge-pl-menu', [menu]);
89
93
  const balPlaque = plaque('ge-pl ge-pl-glass ge-pl-bal', [balance]);
90
- const left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque]));
94
+ const fsPlaque = fsCounter ? plaque('ge-pl ge-pl-glass ge-pl-fs', [fsCounter]) : null;
95
+ const totalWinPlaque = fsTotalWin ? plaque('ge-pl ge-pl-glass ge-pl-totalwin', [fsTotalWin]) : null;
96
+ const left = zone('ge-zone-left ge-zone-plaques', ...compact([menuPlaque, buy, balPlaque, fsPlaque, totalWinPlaque]));
91
97
 
92
98
  // RIGHT: [bet (+ step)] · |divider| · [auto · SPIN · turbo]
93
99
  const betKids: HTMLElement[] = [betValue];
@@ -101,10 +107,9 @@ export function renderBottomBar(shell: GameShell): HTMLElement {
101
107
  spinWrap.append(...compact([auto, spin, turbo]));
102
108
  const right = zone('ge-zone-right ge-zone-plaques', betPlaque, divider, spinWrap);
103
109
 
104
- // MIDDLE: FS last win · counter · total win plaque; base/replay → WIN pill (lifts on overflow)
110
+ // MIDDLE: per-spin WIN pill in every mode lifts above the bar on overflow.
105
111
  let middle: HTMLElement | null = null;
106
- if (isFS) middle = plaque('ge-pl ge-pl-glass ge-fscount', compact([fsLastWin, fsCounter, fsTotalWin]));
107
- else if (winEl) { winEl.classList.add('ge-winpill'); middle = winEl; }
112
+ if (winEl) { winEl.classList.add('ge-winpill'); middle = winEl; }
108
113
  bar.append(...compact([left, middle, right]));
109
114
  }
110
115
 
@@ -81,7 +81,7 @@ function sectionControls(shell: GameShell, el: HTMLElement): HTMLElement {
81
81
  { vis: slot(icon('spin')), name: 'Spin', desc: 'Start a spin at the current bet.', on: true },
82
82
  { vis: slot(icon('plus')), name: 'Raise bet', desc: 'Increase your stake.', on: true },
83
83
  { vis: slot(icon('minus')), name: 'Lower bet', desc: 'Decrease your stake.', on: true },
84
- { vis: slot(icon('autoplay')), name: 'Autoplay', desc: 'Spin automatically a set number of times.', on: features.autoplay },
84
+ { vis: slot(icon('autoplay')), name: 'Autoplay', desc: 'Spin automatically a set number of times.', on: features.autoplay != null },
85
85
  { vis: slot(icon('turbo1')), name: 'Turbo', desc: 'Speed up spin animations.', on: features.turbo > 0 },
86
86
  { vis: buyBadge, name: 'Buy bonus', desc: 'Pay a fixed cost to enter a bonus feature.', on: features.buyBonus !== false },
87
87
  ];
@@ -60,14 +60,27 @@ export function openBetModal(shell: GameShell): HTMLElement {
60
60
 
61
61
  const AUTOPLAY_COUNTS = [10, 25, 50, 100, 250, 500, 1000, 2000, Infinity];
62
62
 
63
- /** Autoplay picker spin counts incl. ∞; Confirm starts autoplay. */
63
+ /** The selectable spin counts, honouring an optional jurisdiction max. With a `maxCount`:
64
+ * drop ∞, keep presets ≤ max, and append the max itself when it isn't already a preset
65
+ * (so the cap is always offered). Without one: the default presets including ∞. */
66
+ function autoplayCounts(maxCount?: number): number[] {
67
+ if (maxCount == null) return AUTOPLAY_COUNTS;
68
+ const capped = AUTOPLAY_COUNTS.filter((n) => Number.isFinite(n) && n <= maxCount);
69
+ if (!capped.includes(maxCount)) capped.push(maxCount);
70
+ return capped;
71
+ }
72
+
73
+ /** Autoplay picker — spin counts (incl. ∞ unless a maxCount caps them); Confirm starts autoplay. */
64
74
  export function openAutoplayModal(shell: GameShell): HTMLElement {
75
+ const maxCount = shell.config.features.autoplay?.maxCount;
76
+ const counts = autoplayCounts(maxCount);
65
77
  return buildSheet({
66
78
  ge: 'autoplay-modal', title: shell.t('Autoplay'), columns: 3, confirmLabel: shell.t('Start'),
67
- choices: AUTOPLAY_COUNTS.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
68
- selected: String(shell.state.autoplay.remaining || 10),
79
+ choices: counts.map((n) => ({ id: String(n), label: Number.isFinite(n) ? String(n) : '∞' })),
80
+ selected: String(shell.state.autoplay.remaining || counts[0]),
69
81
  onConfirm: (id) => {
70
- const remaining = Number(id); // "Infinity" → Infinity
82
+ let remaining = Number(id); // "Infinity" → Infinity
83
+ if (maxCount != null) remaining = Math.min(remaining, maxCount); // defensive cap
71
84
  shell.state.autoplay = { active: true, remaining };
72
85
  shell.emit('autoplayStart', { active: true, remaining });
73
86
  shell.render();
@@ -218,8 +218,10 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
218
218
  /* the buy-bonus scroll area is a SIZE CONTAINER, so the cards' cqh units measure the overlay
219
219
  (the popout frame) and not the browser window — cards fit without any vertical scroll. */
220
220
  #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-scroll { container-type:size; }
221
- #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { padding:clamp(8px,3cqh,16px); }
222
- #${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
221
+ /* buy-bonus uses the FULL overlay width (no 800px centre cap) so the card row isn't cropped at
222
+ the sides; small horizontal padding keeps the cards off the screen edges. */
223
+ #${SHELL_ROOT_ID} [data-ge="buybonus-overlay"] .ge-ov-body { max-width:none; padding:clamp(8px,3cqh,16px) clamp(12px,3vw,28px); }
224
+ #${SHELL_ROOT_ID} .ge-bb-grid { display:flex; gap:14px; justify-content:safe center; overflow-x:auto; overflow-y:hidden; padding-bottom:6px;
223
225
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; }
224
226
  /* the one knob that scales the whole card — cqh measures the overlay (popout frame), not the
225
227
  browser window, so cards shrink to fit the real container height. */
@@ -282,8 +284,10 @@ export const SHELL_CSS = SHELL_FONT_CSS + `
282
284
  border-radius:16px; padding:0 20px; gap:18px; }
283
285
  #${SHELL_ROOT_ID} .ge-pl-dark { background:var(--shell-plaque-dark); }
284
286
  #${SHELL_ROOT_ID} .ge-pl-glass { background:var(--shell-plaque-glass); }
285
- /* FS spins-counter plaque (wide) sits between balance and bet, glass like balance */
286
- #${SHELL_ROOT_ID} .ge-fscount { justify-content:center; min-width:150px; }
287
+ /* FS/replay left blocksFree Spins counter (compact) + Total Win, standalone glass plaques
288
+ sitting just right of the balance pill */
289
+ #${SHELL_ROOT_ID} .ge-pl-fs, #${SHELL_ROOT_ID} .ge-pl-totalwin { margin-left:8px; }
290
+ #${SHELL_ROOT_ID} .ge-pl-fs { padding:0 16px; }
287
291
  #${SHELL_ROOT_ID} .ge-pl .ge-rd { color:#fff; text-shadow:none; }
288
292
  #${SHELL_ROOT_ID} .ge-pl .ge-rd .ge-lbl { color:var(--shell-plaque-label); }
289
293
  #${SHELL_ROOT_ID} .ge-pl .ge-iconbtn { color:#fff; }
@@ -11,7 +11,7 @@ export function createInitialState(config: ShellConfig): ShellState {
11
11
  autoplay: { active: false, remaining: 0 },
12
12
  turbo: 0,
13
13
  buyBonusEnabled: true,
14
- freeSpins: { current: 0, total: 0, totalWin: 0, lastWin: 0 },
14
+ freeSpins: { current: 0, total: 0, totalWin: 0 },
15
15
  activeFeature: null,
16
16
  };
17
17
  }
@@ -41,9 +41,9 @@ export function buildThemeVars(theme: ThemeConfig = {}): string {
41
41
  // Plaque tokens — the grouped dark/glass panel language shared by the control bar
42
42
  // AND the overlays. Scheme-independent (always dark, white-on-dark) so bar + overlays
43
43
  // stay visually identical regardless of the dark/light `scheme`.
44
- `--shell-plaque-dark: rgba(6,9,15,.76)`,
45
- `--shell-plaque-glass: rgba(30,36,48,.5)`,
46
- `--shell-plaque-glass-hover: rgba(40,48,64,.62)`,
44
+ `--shell-plaque-dark: rgba(6,9,15,.86)`,
45
+ `--shell-plaque-glass: rgba(30,36,48,.70)`,
46
+ `--shell-plaque-glass-hover: rgba(40,48,64,.86)`,
47
47
  // Opaque surface for centred modals (confirm, bet/autoplay pickers) so they read solid,
48
48
  // not see-through, over the frosted backdrop.
49
49
  `--shell-plaque-solid: #1a2030`,
@@ -90,9 +90,21 @@ export interface GameInfoContent {
90
90
  sections?: GameInfoSection[];
91
91
  }
92
92
 
93
+ /** Autoplay limits. Presence of this object (vs `null`) is what enables autoplay. */
94
+ export interface AutoplayConfig {
95
+ /** Maximum selectable spin count in the autoplay picker. Caps the built-in presets and
96
+ * drops the unlimited (∞) choice; if it isn't already a preset it becomes the top choice.
97
+ * Omit for the default presets (including ∞). */
98
+ maxCount?: number;
99
+ }
100
+
93
101
  export interface ShellFeatures {
94
102
  turbo: 0 | 1 | 2 | 3;
95
- autoplay: boolean;
103
+ /** Spacebar starts a spin in base mode. Defaults to `true`; set `false` to disable the
104
+ * keyboard shortcut (e.g. jurisdictions that forbid quick-spin keys). */
105
+ spacebar?: boolean;
106
+ /** Autoplay: `null` (or omitted) disables it; an object enables it (optionally with limits). */
107
+ autoplay?: AutoplayConfig | null;
96
108
  buyBonus: BonusOption[] | false;
97
109
  }
98
110
 
@@ -105,7 +117,6 @@ export interface FreeSpinsState {
105
117
  current: number;
106
118
  total: number;
107
119
  totalWin: number;
108
- lastWin: number;
109
120
  }
110
121
 
111
122
  /** One footer button of a generic modal. Clicking it runs `on` (if any), then closes the modal. */