@energy8platform/shell 0.8.0 → 0.10.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.
package/dist/html.d.ts CHANGED
@@ -303,8 +303,13 @@ interface AutoplayConfig {
303
303
  }
304
304
  interface ShellFeatures {
305
305
  turbo: 0 | 1 | 2 | 3;
306
- /** Master keyboard-shortcut switch. Defaults to `true`; set `false` to disable ALL hotkeys
307
- * (overrides `spacebar` and any future hotkey). */
306
+ /** Master keyboard-shortcut switch: `false` disables ALL hotkeys (overrides `spacebar` and any
307
+ * future hotkey) AND hides the Hotkeys section of Game Info, including one the game supplied
308
+ * itself — a keycap chart for keys that do nothing is worse than no chart.
309
+ *
310
+ * Left unset, the shell measures the client (`core/device.ts`): a touchscreen has no keys to
311
+ * press, so it gets neither the shortcuts nor the chart. Set it explicitly when you know better
312
+ * than the media query — which is what the host does with the platform's `device` field. */
308
313
  hotkeys?: boolean;
309
314
  /** Spacebar starts a spin in base mode. Defaults to `true`; set `false` to disable the
310
315
  * keyboard shortcut (e.g. jurisdictions that forbid quick-spin keys). */
@@ -636,7 +641,7 @@ declare class ShellController extends EventEmitter<ShellEvents> implements Shell
636
641
  tokens: ShellTokens;
637
642
  layout: ShellLayoutMode;
638
643
  soundOn: boolean;
639
- readonly engineVersion = "0.8.0";
644
+ readonly engineVersion = "0.10.0";
640
645
  readonly actions: ShellActions;
641
646
  private renderer;
642
647
  private i18n;
@@ -780,8 +785,22 @@ interface I18n {
780
785
  }
781
786
  declare function createI18n(opts: I18nOptions): I18n;
782
787
 
788
+ /**
789
+ * The disclaimer body every launch shows, in source English — and the SAME strings that key the
790
+ * translations below. One constant for both on purpose: a host that kept its own copy would drift
791
+ * from these keys by one character and the lookup would miss in silence, leaving a player on any
792
+ * language with English legal text. Certification asks for the opposite ("if multiple languages are
793
+ * supported, the disclaimer is translated and displayed in each language").
794
+ *
795
+ * Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
796
+ * Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
797
+ * `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
798
+ * `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
799
+ */
800
+ declare const DISCLAIMER_LINES: readonly string[];
801
+
783
802
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
784
- declare const PACKAGE_VERSION = "0.8.0";
803
+ declare const PACKAGE_VERSION = "0.10.0";
785
804
 
786
805
  /** A shell: the renderer-agnostic controller plus the surface facade (safeArea/barHeight/setVisible)
787
806
  * an embedding host reads. `createShell`, `createGameShell` and `createPixiShell` all return this. */
@@ -861,5 +880,5 @@ declare function createGameShell(config: HtmlShellConfig): ShellController;
861
880
  /** Tear down the active shell (no argument — singleton). Resolves immediately when nothing is active. */
862
881
  declare function removeGameShell(): Promise<void>;
863
882
 
864
- export { DEFAULT_ACCENT, DEFAULT_MENU, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
883
+ export { DEFAULT_ACCENT, DEFAULT_MENU, DISCLAIMER_LINES, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
865
884
  export type { AutoplayConfig, AutoplayOptions, BonusCardContext, BonusOption, BonusReadout, CellRef, CreateShellOptions, CurrencyConfig, FreeSpinsState, GameInfoContent, GameInfoSection, GameMode, HtmlShellConfig, I18n, I18nOptions, Lang, MenuHost, MenuItem, MenuPresetId, MenuRow, ModalAction, ModalOptions, OverlayHandle, OverlayRequest, PaylineDef, PaytableRow, PopoverPlacement, Rect as PopoverRect, ReplayModalOptions, ResolvedShellConfig, SafeArea, ShapeDef, Shell, ShellActions, HtmlShellConfig as ShellConfig, ShellEvents, ShellFeatures, ShellHost, ShellLayoutMode, ShellMode, ShellRenderer, ShellState, ShellSurface, ShellTokens, ThemeConfig, VolumeKey, VolumeLevels, WinSection };
package/dist/html.esm.js CHANGED
@@ -1306,6 +1306,19 @@ const D = {
1306
1306
  L4: 'The game display is not representative of any physical device and is for illustrative purposes only.',
1307
1307
  L5: 'Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser.',
1308
1308
  };
1309
+ /**
1310
+ * The disclaimer body every launch shows, in source English — and the SAME strings that key the
1311
+ * translations below. One constant for both on purpose: a host that kept its own copy would drift
1312
+ * from these keys by one character and the lookup would miss in silence, leaving a player on any
1313
+ * language with English legal text. Certification asks for the opposite ("if multiple languages are
1314
+ * supported, the disclaimer is translated and displayed in each language").
1315
+ *
1316
+ * Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
1317
+ * Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
1318
+ * `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
1319
+ * `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
1320
+ */
1321
+ const DISCLAIMER_LINES = [D.L1, D.L2, D.L3, D.L4, D.L5];
1309
1322
  const DISCLAIMER_LOCALES = {
1310
1323
  da: {
1311
1324
  [D.L1]: 'Fejlfunktion annullerer alle gevinster og spil.',
@@ -1689,9 +1702,41 @@ class KeyboardController {
1689
1702
  }
1690
1703
  }
1691
1704
 
1705
+ /**
1706
+ * Can the player in front of this client actually press a key?
1707
+ *
1708
+ * The shell documents its shortcuts in a Hotkeys section and binds Spacebar to spin. On a phone
1709
+ * neither is reachable, and showing a keycap chart to someone holding a touchscreen is a promise
1710
+ * the game can't keep — a certification lab reads it as a feature offered where it doesn't work.
1711
+ *
1712
+ * The question is deliberately NOT "is the layout narrow" (a portrait desktop window still has a
1713
+ * keyboard) and NOT "is there a touchscreen" (a touch laptop has both). It is: what does the
1714
+ * PRIMARY pointer look like, and can it hover? Coarse-and-hoverless is a touchscreen, and a
1715
+ * touchscreen is the one case where the keys genuinely aren't there.
1716
+ *
1717
+ * A tablet with a keyboard case answers "coarse, no hover" too and loses the chart. That is the
1718
+ * right side to be wrong on: the chart is a convenience, and the keys keep working for anyone who
1719
+ * has them — the media query only decides what the shell ADVERTISES (hosts can still say outright,
1720
+ * via `features.hotkeys`, and the platform's own `device` field does exactly that).
1721
+ */
1722
+ /** A touchscreen: the primary pointer is a finger, and nothing can hover. */
1723
+ const TOUCH_ONLY = '(pointer: coarse) and (hover: none)';
1724
+ function keyboardCapable(win = typeof window === 'undefined' ? undefined : window) {
1725
+ // No window (SSR, node tests) or a browser too old for matchMedia: assume a keyboard rather than
1726
+ // silently stripping shortcuts from a desktop we simply failed to measure.
1727
+ if (typeof win?.matchMedia !== 'function')
1728
+ return true;
1729
+ try {
1730
+ return !win.matchMedia(TOUCH_ONLY).matches;
1731
+ }
1732
+ catch {
1733
+ return true;
1734
+ }
1735
+ }
1736
+
1692
1737
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1693
1738
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
1694
- const PACKAGE_VERSION = '0.8.0';
1739
+ const PACKAGE_VERSION = '0.10.0';
1695
1740
 
1696
1741
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
1697
1742
  function resolveConfig(config) {
@@ -1705,7 +1750,10 @@ function resolveConfig(config) {
1705
1750
  win: config.win,
1706
1751
  mode: config.mode,
1707
1752
  gameInfo: config.gameInfo,
1708
- features: config.features,
1753
+ // `hotkeys` unset means "decide for me": a touchscreen has no keys to press, so the shell
1754
+ // neither binds them nor advertises them there. A host that knows better — the platform's own
1755
+ // `device` field, a jurisdiction rule — says so outright and that wins. See core/device.ts.
1756
+ features: { ...config.features, hotkeys: config.features.hotkeys ?? keyboardCapable() },
1709
1757
  theme: config.theme,
1710
1758
  onBonusBuy: config.onBonusBuy,
1711
1759
  volumes: config.volumes,
@@ -1799,7 +1847,11 @@ class ShellController extends EventEmitter {
1799
1847
  this.renderer.renderBar();
1800
1848
  },
1801
1849
  toggleAutoplay: () => {
1802
- if (this.state.autoplay.active)
1850
+ // A halted run (stopped, but with spins still owed after a lost connection) counts as
1851
+ // "autoplay is on screen": the toggle retires its leftover count, exactly as it stops a
1852
+ // running one. Resuming those spins is the disc's job, not this one's.
1853
+ const { active, remaining } = this.state.autoplay;
1854
+ if (active || remaining > 0)
1803
1855
  a.stopAutoplay();
1804
1856
  else
1805
1857
  this.openAutoplayPicker();
@@ -2262,6 +2314,11 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
2262
2314
  /* the STOP glyph is a solid dark square, so the autoplay count is always pure white to read on it. */
2263
2315
  #${SHELL_ROOT_ID} .ge-spin-count { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
2264
2316
  font-size:22px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; color:#fff; }
2317
+ /* autoplay halted with spins still owed (a lost connection): the run is stopped, so no STOP square —
2318
+ the auto glyph sits above the leftover count, both in the disc's own ink, and a tap resumes. */
2319
+ #${SHELL_ROOT_ID} .ge-shell-spin.ge-auto-paused { flex-direction:column; gap:1px; position:relative; }
2320
+ #${SHELL_ROOT_ID} .ge-auto-paused .ge-spin-auto { display:flex; font-size:.46em; line-height:0; }
2321
+ #${SHELL_ROOT_ID} .ge-auto-paused .ge-spin-count { position:static; inset:auto; color:inherit; font-size:20px; }
2265
2322
 
2266
2323
  /* BUY BONUS — round accent badge, 2-line label, text pulses + accent glow on hover */
2267
2324
  #${SHELL_ROOT_ID} .ge-shell-buybonus { pointer-events:auto; cursor:pointer; box-sizing:border-box;
@@ -3083,22 +3140,36 @@ function buyBtn(host) {
3083
3140
  function betLocked(host) {
3084
3141
  return host.state.busy || host.state.autoplay.active;
3085
3142
  }
3086
- /** SPIN disc — rotates while busy; becomes a STOP + countdown while autoplay runs. */
3143
+ /**
3144
+ * SPIN disc — rotates while busy; becomes a STOP + countdown while autoplay runs; becomes an
3145
+ * autoplay glyph + the SAME countdown when a run was halted with spins still owed (a lost
3146
+ * connection), where a tap resumes it. That third state is what a certification lab means by "after
3147
+ * reconnection the counter is displayed correctly": the run stopped, but the spins the player asked
3148
+ * for are still on screen and one tap away, instead of silently reset to zero.
3149
+ */
3087
3150
  function spinButton(host) {
3088
3151
  const { state } = host;
3089
3152
  const sp = document.createElement('button');
3090
3153
  sp.className = 'ge-shell-spin';
3091
3154
  sp.dataset.ge = 'spin';
3155
+ const rem = state.autoplay.remaining;
3156
+ const count = Number.isFinite(rem) ? String(rem) : '∞';
3092
3157
  if (state.autoplay.active) {
3093
3158
  sp.classList.add('ge-stop');
3094
- const rem = state.autoplay.remaining;
3095
- const label = Number.isFinite(rem) ? String(rem) : '∞';
3096
- sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
3159
+ sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${count}</span>`;
3097
3160
  sp.addEventListener('click', () => {
3098
3161
  if (!sp.disabled)
3099
3162
  host.actions.stopAutoplay();
3100
3163
  });
3101
3164
  }
3165
+ else if (rem > 0) {
3166
+ sp.classList.add('ge-auto-paused');
3167
+ sp.innerHTML = `<span class="ge-spin-auto">${icon('autoplay')}</span><span class="ge-spin-count">${count}</span>`;
3168
+ sp.addEventListener('click', () => {
3169
+ if (!sp.disabled)
3170
+ host.actions.startAutoplay(rem);
3171
+ });
3172
+ }
3102
3173
  else {
3103
3174
  sp.innerHTML = icon('spin');
3104
3175
  if (state.busy)
@@ -3118,8 +3189,11 @@ function autoButton(host) {
3118
3189
  b.classList.add('ge-glow');
3119
3190
  return b;
3120
3191
  }
3192
+ /** Same button, three jobs: stop a running run, retire a halted run's leftover count (which frees
3193
+ * the disc for a manual spin again), or open the picker. */
3121
3194
  function onAutoplay(host) {
3122
- if (host.state.autoplay.active)
3195
+ const { active, remaining } = host.state.autoplay;
3196
+ if (active || remaining > 0)
3123
3197
  host.actions.stopAutoplay();
3124
3198
  else
3125
3199
  host.actions.openAutoplayPicker();
@@ -3475,10 +3549,15 @@ function openGameInfoModal(host) {
3475
3549
  onBack: () => { root.remove(); host.actions.openMenu(); },
3476
3550
  });
3477
3551
  root.dataset.ge = 'info-modal';
3478
- const rawSections = host.config.gameInfo.sections ?? [];
3479
- // Auto-inject a hotkeys section unless the game already provides one or features.hotkeys === false.
3552
+ const allSections = host.config.gameInfo.sections ?? [];
3553
+ // Auto-inject a hotkeys section unless the game already provides one. With hotkeys off — a
3554
+ // jurisdiction that forbids them, or a touchscreen that has no keys at all (see core/device.ts) —
3555
+ // there is no keyboard surface to document, and a game-supplied section is dropped along with the
3556
+ // auto-injected one: a keycap chart for keys the player cannot press is a promise the game breaks.
3557
+ const keys = host.config.features.hotkeys !== false;
3558
+ const rawSections = keys ? allSections : allSections.filter((s) => s.type !== 'hotkeys');
3480
3559
  const sectionsWithHotkeys = [...rawSections];
3481
- if (host.config.features.hotkeys !== false && !rawSections.some((s) => s.type === 'hotkeys')) {
3560
+ if (keys && !rawSections.some((s) => s.type === 'hotkeys')) {
3482
3561
  sectionsWithHotkeys.push({ type: 'hotkeys', order: HOTKEYS_DEFAULT_ORDER });
3483
3562
  }
3484
3563
  const sections = sectionsWithHotkeys;
@@ -4736,5 +4815,5 @@ function removeGameShell() {
4736
4815
  return shell.destroy();
4737
4816
  }
4738
4817
 
4739
- export { DEFAULT_ACCENT, DEFAULT_MENU, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
4818
+ export { DEFAULT_ACCENT, DEFAULT_MENU, DISCLAIMER_LINES, ShellController as GameShell, HtmlRenderer, PACKAGE_VERSION, POPOVER, SCHEMES, ShellController, createGameShell, createI18n, createShell, isPresetId, normalizeLang, placePopover, popoverWidth, rangeBounds, removeGameShell, resolveConfig, resolveMenu, resolveTheme, seedMenuValues, socialize };
4740
4819
  //# sourceMappingURL=html.esm.js.map