@energy8platform/platform-core 0.24.6 → 0.25.1

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.
Files changed (57) hide show
  1. package/dist/game-spec.cjs.js +209 -0
  2. package/dist/game-spec.cjs.js.map +1 -0
  3. package/dist/game-spec.d.ts +164 -0
  4. package/dist/game-spec.esm.js +198 -0
  5. package/dist/game-spec.esm.js.map +1 -0
  6. package/dist/index.cjs.js +67 -1
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +44 -1
  9. package/dist/index.esm.js +67 -1
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/loading.cjs.js +10 -1
  12. package/dist/loading.cjs.js.map +1 -1
  13. package/dist/loading.esm.js +10 -1
  14. package/dist/loading.esm.js.map +1 -1
  15. package/dist/lua.cjs.js +5 -2
  16. package/dist/lua.cjs.js.map +1 -1
  17. package/dist/lua.d.ts +9 -0
  18. package/dist/lua.esm.js +5 -2
  19. package/dist/lua.esm.js.map +1 -1
  20. package/dist/shell.cjs.js +35 -0
  21. package/dist/shell.cjs.js.map +1 -1
  22. package/dist/shell.d.ts +16 -1
  23. package/dist/shell.esm.js +35 -1
  24. package/dist/shell.esm.js.map +1 -1
  25. package/dist/simulation.cjs.js +40 -22
  26. package/dist/simulation.cjs.js.map +1 -1
  27. package/dist/simulation.d.ts +35 -2
  28. package/dist/simulation.esm.js +39 -23
  29. package/dist/simulation.esm.js.map +1 -1
  30. package/dist/slot-result.cjs.js +17 -0
  31. package/dist/slot-result.cjs.js.map +1 -0
  32. package/dist/slot-result.d.ts +26 -0
  33. package/dist/slot-result.esm.js +14 -0
  34. package/dist/slot-result.esm.js.map +1 -0
  35. package/package.json +12 -1
  36. package/scripts/gen-version.mjs +21 -0
  37. package/src/PlatformSession.ts +28 -0
  38. package/src/game-spec/defineGame.ts +16 -0
  39. package/src/game-spec/derive.ts +135 -0
  40. package/src/game-spec/export.ts +17 -0
  41. package/src/game-spec/index.ts +6 -0
  42. package/src/game-spec/types.ts +81 -0
  43. package/src/game-spec/validate.ts +49 -0
  44. package/src/loading/CSSPreloader.ts +14 -1
  45. package/src/lua/LuaEngine.ts +5 -2
  46. package/src/lua/types.ts +8 -0
  47. package/src/shell/GameShell.ts +19 -1
  48. package/src/shell/components/GameInfo.ts +13 -0
  49. package/src/shell/index.ts +1 -0
  50. package/src/shell/shell.css.ts +2 -0
  51. package/src/shell/types.ts +3 -0
  52. package/src/shell/version.ts +3 -0
  53. package/src/simulation/NativeSimulationRunner.ts +62 -26
  54. package/src/simulation/index.ts +3 -0
  55. package/src/slot-result/coerce.ts +11 -0
  56. package/src/slot-result/index.ts +2 -0
  57. package/src/slot-result/types.ts +19 -0
package/dist/index.cjs.js CHANGED
@@ -610,6 +610,26 @@ class PlatformSession extends EventEmitter {
610
610
  }
611
611
  return this.sdk.play(params);
612
612
  }
613
+ /**
614
+ * Acknowledge a finished PLAY_RESULT (call AFTER the game has animated it).
615
+ *
616
+ * The host uses this to know the client is ready for the next action and, on
617
+ * Stake, to settle the round (`/wallet/end-round`) only once the win
618
+ * animation has played. No-op when constructed with `sdk: false`.
619
+ */
620
+ playAck(result) {
621
+ this.sdk?.playAck(result);
622
+ }
623
+ /**
624
+ * Query the host for an in-flight round (e.g. after a page reload). Resolves with the last
625
+ * result snapshot when a round is still open, or `null`. Used to offer a "resume / finish"
626
+ * choice on boot. Resolves `null` when constructed with `sdk: false`.
627
+ */
628
+ async getState() {
629
+ if (!this.sdk)
630
+ return null;
631
+ return this.sdk.getState();
632
+ }
613
633
  /** Tear down the SDK, DevBridge, and clear listeners. */
614
634
  destroy() {
615
635
  this.sdk?.destroy();
@@ -649,6 +669,9 @@ async function createPlatformSession(config = {}) {
649
669
  sdk.on('balanceUpdate', (data) => {
650
670
  session.emit('balanceUpdate', data);
651
671
  });
672
+ sdk.on('connectionStateChanged', (state) => {
673
+ session.emit('connectionStateChanged', state);
674
+ });
652
675
  }
653
676
  return session;
654
677
  }
@@ -828,6 +851,10 @@ function createCSSPreloader(container, config) {
828
851
  50% { opacity: 1; }
829
852
  }
830
853
  `;
854
+ // The absolute overlay needs a positioned ancestor. Only override a STATIC container, and
855
+ // remember the prior inline value so removeCSSPreloader can restore it (an inline `relative`
856
+ // left behind would beat the game's `#game { position: fixed; inset: 0 }` and collapse it).
857
+ const prevPosition = container.style.position;
831
858
  container.style.position = container.style.position || 'relative';
832
859
  container.appendChild(styleEl);
833
860
  container.appendChild(overlay);
@@ -838,6 +865,7 @@ function createCSSPreloader(container, config) {
838
865
  // We still record state so removeCSSPreloader works.
839
866
  state = {
840
867
  container,
868
+ prevPosition,
841
869
  overlay,
842
870
  styleEl,
843
871
  rectEl: null,
@@ -856,6 +884,7 @@ function createCSSPreloader(container, config) {
856
884
  }
857
885
  state = {
858
886
  container,
887
+ prevPosition,
859
888
  overlay,
860
889
  styleEl,
861
890
  rectEl,
@@ -934,7 +963,7 @@ function removeCSSPreloader(_container) {
934
963
  state.tapResolve = null;
935
964
  }
936
965
  state.removed = true;
937
- const { overlay, styleEl } = state;
966
+ const { overlay, styleEl, container, prevPosition } = state;
938
967
  overlay.classList.add('ge-preloader-hidden');
939
968
  return new Promise((resolve) => {
940
969
  let settled = false;
@@ -944,6 +973,9 @@ function removeCSSPreloader(_container) {
944
973
  settled = true;
945
974
  overlay.remove();
946
975
  styleEl.remove();
976
+ // Restore the container's original inline position so the game's own layout
977
+ // (e.g. `#game { position: fixed; inset: 0 }`) is no longer defeated by our inline override.
978
+ container.style.position = prevPosition;
947
979
  state = null;
948
980
  resolve();
949
981
  };
@@ -1218,6 +1250,8 @@ const SHELL_CSS = SHELL_FONT_CSS + `
1218
1250
  #${SHELL_ROOT_ID} .ge-gi-sec h3 { color:var(--shell-plaque-label); font-size:11px; letter-spacing:.14em;
1219
1251
  text-transform:uppercase; margin:0 0 12px; }
1220
1252
  #${SHELL_ROOT_ID} .ge-gi-sec p { color:rgba(255,255,255,.88); font-size:15px; line-height:1.6; margin:0; }
1253
+ #${SHELL_ROOT_ID} .ge-gi-version { text-align:center; color:var(--shell-muted); font-size:11px;
1254
+ letter-spacing:.08em; opacity:.7; margin:4px 0 2px; }
1221
1255
 
1222
1256
  /* controls — two blocks (gameplay / menu & info), icon/name/description per control */
1223
1257
  #${SHELL_ROOT_ID} .ge-gi-ctl-block + .ge-gi-ctl-block { margin-top:16px; padding-top:4px; border-top:1px solid var(--shell-plaque-line); }
@@ -1927,6 +1961,10 @@ function openSettingsModal(shell) {
1927
1961
  return root;
1928
1962
  }
1929
1963
 
1964
+ // AUTO-GENERATED by scripts/gen-version.mjs — do not edit. Mirrors package.json "version".
1965
+ /** The @energy8platform/platform-core package version, stamped at build time. */
1966
+ const PACKAGE_VERSION = '0.25.1';
1967
+
1930
1968
  const SVG_NS = 'http://www.w3.org/2000/svg';
1931
1969
  function openGameInfoModal(shell) {
1932
1970
  const { root, body } = createOverlay({
@@ -1943,8 +1981,19 @@ function openGameInfoModal(shell) {
1943
1981
  .map((s, i) => ({ s, i, k: base(s, i) }))
1944
1982
  .sort((a, b) => a.k - b.k || a.i - b.i)
1945
1983
  .forEach(({ s }) => body.appendChild(renderSection(shell, s)));
1984
+ body.appendChild(versionFooter(shell));
1946
1985
  return root;
1947
1986
  }
1987
+ /** A muted version stamp pinned to the bottom of the game-info modal:
1988
+ * `${config.version ?? '1.0.0'}.${engine version without dots}` (e.g. '1.0.0.0246'). */
1989
+ function versionFooter(shell) {
1990
+ const gameVersion = shell.config.version ?? '1.0.0';
1991
+ const el = document.createElement('div');
1992
+ el.dataset.ge = 'info-version';
1993
+ el.className = 'ge-gi-version';
1994
+ el.textContent = `${gameVersion}.${PACKAGE_VERSION.replaceAll('.', '')}`;
1995
+ return el;
1996
+ }
1948
1997
  function renderSection(shell, s) {
1949
1998
  switch (s.type) {
1950
1999
  case 'modes': return sectionModes(shell, s.modes, sec('info-modes', s.title, shell.t('Modes')));
@@ -2738,6 +2787,10 @@ class GameShell extends EventEmitter {
2738
2787
  this.observeLayout();
2739
2788
  if (typeof document !== 'undefined') {
2740
2789
  document.addEventListener('keydown', this.handleKeyDown);
2790
+ // Stake serves the game in an iframe; on first paint focus is on the HOST page, so a `document`
2791
+ // keydown never fires and Space scrolls the parent. Pull window focus into the iframe on the
2792
+ // first pointer interaction so the spacebar shortcut works. Harmless on full-page Energy8.
2793
+ document.addEventListener('pointerdown', this.pullFocus, true);
2741
2794
  this.keysBound = true;
2742
2795
  }
2743
2796
  this.render();
@@ -2823,6 +2876,12 @@ class GameShell extends EventEmitter {
2823
2876
  * false, while a spin is running, while autoplay is active, outside base mode, when an
2824
2877
  * overlay/modal is open, or when an editable element is focused. `repeat` (held key) is
2825
2878
  * ignored so it can't spam. */
2879
+ /** Pull window focus into the iframe on first pointer interaction so `document` keydown (the
2880
+ * spacebar shortcut) fires. No-op / harmless when already focused or full-page. */
2881
+ pullFocus = () => { try {
2882
+ window.focus();
2883
+ }
2884
+ catch { /* cross-origin / non-browser */ } };
2826
2885
  handleKeyDown = (e) => {
2827
2886
  if (this.destroyed || e.code !== 'Space' || e.repeat)
2828
2887
  return;
@@ -2896,6 +2955,9 @@ class GameShell extends EventEmitter {
2896
2955
  setBusy(busy) { this.state.busy = busy; this.render(); }
2897
2956
  setAutoplay(a) { this.state.autoplay = a; this.render(); }
2898
2957
  setTurbo(level) { this.state.turbo = level; this.render(); }
2958
+ /** Currency-aware money formatter for WIN amounts (variable decimals: 0.0041 stays 0.0041, not
2959
+ * 0.00). The host hands this to a scene so games format money without knowing the currency. */
2960
+ formatWin(value) { return formatCurrency(value, this.config.currency, true); }
2899
2961
  setBuyBonusEnabled(enabled) { this.state.buyBonusEnabled = enabled; this.render(); }
2900
2962
  setFreeSpins(fs) { this.state.freeSpins = fs; this.render(); }
2901
2963
  showModal(el) {
@@ -2966,6 +3028,9 @@ class GameShell extends EventEmitter {
2966
3028
  /** Open a generic, externally-driven modal (title + body + optional action buttons).
2967
3029
  * Each action runs its `on` then closes; the ✕ shows when `availableClose` is true. */
2968
3030
  openModal(opts) { this.showModal(buildModal(opts)); }
3031
+ /** Programmatically dismiss whatever modal/overlay is currently shown (e.g. auto-close the
3032
+ * reconnect overlay once the link is restored). No-op when nothing is open. */
3033
+ closeModal() { this.modalHost.innerHTML = ''; }
2969
3034
  /** Open the non-dismissable replay summary modal (START REPLAY → onReplay → reopen). */
2970
3035
  openReplay(opts) {
2971
3036
  if (this.destroyed)
@@ -2984,6 +3049,7 @@ class GameShell extends EventEmitter {
2984
3049
  this.ro = null;
2985
3050
  if (this.keysBound) {
2986
3051
  document.removeEventListener('keydown', this.handleKeyDown);
3052
+ document.removeEventListener('pointerdown', this.pullFocus, true);
2987
3053
  this.keysBound = false;
2988
3054
  }
2989
3055
  this.cancelMoneyAnims();