@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.cjs.js CHANGED
@@ -1308,6 +1308,19 @@ const D = {
1308
1308
  L4: 'The game display is not representative of any physical device and is for illustrative purposes only.',
1309
1309
  L5: 'Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser.',
1310
1310
  };
1311
+ /**
1312
+ * The disclaimer body every launch shows, in source English — and the SAME strings that key the
1313
+ * translations below. One constant for both on purpose: a host that kept its own copy would drift
1314
+ * from these keys by one character and the lookup would miss in silence, leaving a player on any
1315
+ * language with English legal text. Certification asks for the opposite ("if multiple languages are
1316
+ * supported, the disclaimer is translated and displayed in each language").
1317
+ *
1318
+ * Brand-free by construction. Stake's own template ends with a seventh line — "TM and © {year}
1319
+ * Stake Engine." — and that one belongs to a Stake launch alone: it arrives with the Stake bridge's
1320
+ * `INIT.config.disclaimerLines`, which outrank this default, and renders verbatim (see the host's
1321
+ * `isBrandLine`). Every other platform gets the same mandated wording without someone else's mark.
1322
+ */
1323
+ const DISCLAIMER_LINES = [D.L1, D.L2, D.L3, D.L4, D.L5];
1311
1324
  const DISCLAIMER_LOCALES = {
1312
1325
  da: {
1313
1326
  [D.L1]: 'Fejlfunktion annullerer alle gevinster og spil.',
@@ -1691,9 +1704,41 @@ class KeyboardController {
1691
1704
  }
1692
1705
  }
1693
1706
 
1707
+ /**
1708
+ * Can the player in front of this client actually press a key?
1709
+ *
1710
+ * The shell documents its shortcuts in a Hotkeys section and binds Spacebar to spin. On a phone
1711
+ * neither is reachable, and showing a keycap chart to someone holding a touchscreen is a promise
1712
+ * the game can't keep — a certification lab reads it as a feature offered where it doesn't work.
1713
+ *
1714
+ * The question is deliberately NOT "is the layout narrow" (a portrait desktop window still has a
1715
+ * keyboard) and NOT "is there a touchscreen" (a touch laptop has both). It is: what does the
1716
+ * PRIMARY pointer look like, and can it hover? Coarse-and-hoverless is a touchscreen, and a
1717
+ * touchscreen is the one case where the keys genuinely aren't there.
1718
+ *
1719
+ * A tablet with a keyboard case answers "coarse, no hover" too and loses the chart. That is the
1720
+ * right side to be wrong on: the chart is a convenience, and the keys keep working for anyone who
1721
+ * has them — the media query only decides what the shell ADVERTISES (hosts can still say outright,
1722
+ * via `features.hotkeys`, and the platform's own `device` field does exactly that).
1723
+ */
1724
+ /** A touchscreen: the primary pointer is a finger, and nothing can hover. */
1725
+ const TOUCH_ONLY = '(pointer: coarse) and (hover: none)';
1726
+ function keyboardCapable(win = typeof window === 'undefined' ? undefined : window) {
1727
+ // No window (SSR, node tests) or a browser too old for matchMedia: assume a keyboard rather than
1728
+ // silently stripping shortcuts from a desktop we simply failed to measure.
1729
+ if (typeof win?.matchMedia !== 'function')
1730
+ return true;
1731
+ try {
1732
+ return !win.matchMedia(TOUCH_ONLY).matches;
1733
+ }
1734
+ catch {
1735
+ return true;
1736
+ }
1737
+ }
1738
+
1694
1739
  // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1695
1740
  /** The @energy8platform/shell package version, stamped into the game-info footer. */
1696
- const PACKAGE_VERSION = '0.8.0';
1741
+ const PACKAGE_VERSION = '0.10.0';
1697
1742
 
1698
1743
  /** Apply defaults to the raw config (the mount target lives on the renderer, not here). */
1699
1744
  function resolveConfig(config) {
@@ -1707,7 +1752,10 @@ function resolveConfig(config) {
1707
1752
  win: config.win,
1708
1753
  mode: config.mode,
1709
1754
  gameInfo: config.gameInfo,
1710
- features: config.features,
1755
+ // `hotkeys` unset means "decide for me": a touchscreen has no keys to press, so the shell
1756
+ // neither binds them nor advertises them there. A host that knows better — the platform's own
1757
+ // `device` field, a jurisdiction rule — says so outright and that wins. See core/device.ts.
1758
+ features: { ...config.features, hotkeys: config.features.hotkeys ?? keyboardCapable() },
1711
1759
  theme: config.theme,
1712
1760
  onBonusBuy: config.onBonusBuy,
1713
1761
  volumes: config.volumes,
@@ -1801,7 +1849,11 @@ class ShellController extends EventEmitter {
1801
1849
  this.renderer.renderBar();
1802
1850
  },
1803
1851
  toggleAutoplay: () => {
1804
- if (this.state.autoplay.active)
1852
+ // A halted run (stopped, but with spins still owed after a lost connection) counts as
1853
+ // "autoplay is on screen": the toggle retires its leftover count, exactly as it stops a
1854
+ // running one. Resuming those spins is the disc's job, not this one's.
1855
+ const { active, remaining } = this.state.autoplay;
1856
+ if (active || remaining > 0)
1805
1857
  a.stopAutoplay();
1806
1858
  else
1807
1859
  this.openAutoplayPicker();
@@ -2264,6 +2316,11 @@ const SHELL_CSS = SHELL_FONT_CSS + SHELL_DIGIT_FONT_CSS + `
2264
2316
  /* the STOP glyph is a solid dark square, so the autoplay count is always pure white to read on it. */
2265
2317
  #${SHELL_ROOT_ID} .ge-spin-count { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
2266
2318
  font-size:22px; font-weight:800; line-height:1; font-variant-numeric:tabular-nums; color:#fff; }
2319
+ /* autoplay halted with spins still owed (a lost connection): the run is stopped, so no STOP square —
2320
+ the auto glyph sits above the leftover count, both in the disc's own ink, and a tap resumes. */
2321
+ #${SHELL_ROOT_ID} .ge-shell-spin.ge-auto-paused { flex-direction:column; gap:1px; position:relative; }
2322
+ #${SHELL_ROOT_ID} .ge-auto-paused .ge-spin-auto { display:flex; font-size:.46em; line-height:0; }
2323
+ #${SHELL_ROOT_ID} .ge-auto-paused .ge-spin-count { position:static; inset:auto; color:inherit; font-size:20px; }
2267
2324
 
2268
2325
  /* BUY BONUS — round accent badge, 2-line label, text pulses + accent glow on hover */
2269
2326
  #${SHELL_ROOT_ID} .ge-shell-buybonus { pointer-events:auto; cursor:pointer; box-sizing:border-box;
@@ -3085,22 +3142,36 @@ function buyBtn(host) {
3085
3142
  function betLocked(host) {
3086
3143
  return host.state.busy || host.state.autoplay.active;
3087
3144
  }
3088
- /** SPIN disc — rotates while busy; becomes a STOP + countdown while autoplay runs. */
3145
+ /**
3146
+ * SPIN disc — rotates while busy; becomes a STOP + countdown while autoplay runs; becomes an
3147
+ * autoplay glyph + the SAME countdown when a run was halted with spins still owed (a lost
3148
+ * connection), where a tap resumes it. That third state is what a certification lab means by "after
3149
+ * reconnection the counter is displayed correctly": the run stopped, but the spins the player asked
3150
+ * for are still on screen and one tap away, instead of silently reset to zero.
3151
+ */
3089
3152
  function spinButton(host) {
3090
3153
  const { state } = host;
3091
3154
  const sp = document.createElement('button');
3092
3155
  sp.className = 'ge-shell-spin';
3093
3156
  sp.dataset.ge = 'spin';
3157
+ const rem = state.autoplay.remaining;
3158
+ const count = Number.isFinite(rem) ? String(rem) : '∞';
3094
3159
  if (state.autoplay.active) {
3095
3160
  sp.classList.add('ge-stop');
3096
- const rem = state.autoplay.remaining;
3097
- const label = Number.isFinite(rem) ? String(rem) : '∞';
3098
- sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${label}</span>`;
3161
+ sp.innerHTML = `<span class="ge-spin-stop">${icon('stop')}</span><span class="ge-spin-count">${count}</span>`;
3099
3162
  sp.addEventListener('click', () => {
3100
3163
  if (!sp.disabled)
3101
3164
  host.actions.stopAutoplay();
3102
3165
  });
3103
3166
  }
3167
+ else if (rem > 0) {
3168
+ sp.classList.add('ge-auto-paused');
3169
+ sp.innerHTML = `<span class="ge-spin-auto">${icon('autoplay')}</span><span class="ge-spin-count">${count}</span>`;
3170
+ sp.addEventListener('click', () => {
3171
+ if (!sp.disabled)
3172
+ host.actions.startAutoplay(rem);
3173
+ });
3174
+ }
3104
3175
  else {
3105
3176
  sp.innerHTML = icon('spin');
3106
3177
  if (state.busy)
@@ -3120,8 +3191,11 @@ function autoButton(host) {
3120
3191
  b.classList.add('ge-glow');
3121
3192
  return b;
3122
3193
  }
3194
+ /** Same button, three jobs: stop a running run, retire a halted run's leftover count (which frees
3195
+ * the disc for a manual spin again), or open the picker. */
3123
3196
  function onAutoplay(host) {
3124
- if (host.state.autoplay.active)
3197
+ const { active, remaining } = host.state.autoplay;
3198
+ if (active || remaining > 0)
3125
3199
  host.actions.stopAutoplay();
3126
3200
  else
3127
3201
  host.actions.openAutoplayPicker();
@@ -3477,10 +3551,15 @@ function openGameInfoModal(host) {
3477
3551
  onBack: () => { root.remove(); host.actions.openMenu(); },
3478
3552
  });
3479
3553
  root.dataset.ge = 'info-modal';
3480
- const rawSections = host.config.gameInfo.sections ?? [];
3481
- // Auto-inject a hotkeys section unless the game already provides one or features.hotkeys === false.
3554
+ const allSections = host.config.gameInfo.sections ?? [];
3555
+ // Auto-inject a hotkeys section unless the game already provides one. With hotkeys off — a
3556
+ // jurisdiction that forbids them, or a touchscreen that has no keys at all (see core/device.ts) —
3557
+ // there is no keyboard surface to document, and a game-supplied section is dropped along with the
3558
+ // auto-injected one: a keycap chart for keys the player cannot press is a promise the game breaks.
3559
+ const keys = host.config.features.hotkeys !== false;
3560
+ const rawSections = keys ? allSections : allSections.filter((s) => s.type !== 'hotkeys');
3482
3561
  const sectionsWithHotkeys = [...rawSections];
3483
- if (host.config.features.hotkeys !== false && !rawSections.some((s) => s.type === 'hotkeys')) {
3562
+ if (keys && !rawSections.some((s) => s.type === 'hotkeys')) {
3484
3563
  sectionsWithHotkeys.push({ type: 'hotkeys', order: HOTKEYS_DEFAULT_ORDER });
3485
3564
  }
3486
3565
  const sections = sectionsWithHotkeys;
@@ -4740,6 +4819,7 @@ function removeGameShell() {
4740
4819
 
4741
4820
  exports.DEFAULT_ACCENT = DEFAULT_ACCENT;
4742
4821
  exports.DEFAULT_MENU = DEFAULT_MENU;
4822
+ exports.DISCLAIMER_LINES = DISCLAIMER_LINES;
4743
4823
  exports.GameShell = ShellController;
4744
4824
  exports.HtmlRenderer = HtmlRenderer;
4745
4825
  exports.PACKAGE_VERSION = PACKAGE_VERSION;