@energy8platform/game-engine 0.36.0 → 0.38.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/host.esm.js CHANGED
@@ -2623,6 +2623,7 @@ async function createSlotGame(opts) {
2623
2623
  };
2624
2624
  const { runRound } = await Promise.resolve().then(function () { return runRound$1; });
2625
2625
  const { createBalanceGate } = await Promise.resolve().then(function () { return balanceGate; });
2626
+ const { attachSettingsStore } = await Promise.resolve().then(function () { return settingsStore; });
2626
2627
  const { createFreeSpinsCounter } = await Promise.resolve().then(function () { return freeSpinsCounter; });
2627
2628
  const { resolvePlayError } = await Promise.resolve().then(function () { return playError; });
2628
2629
  // slotPlay references shell via closure — define it after shell is assigned below.
@@ -2737,6 +2738,18 @@ async function createSlotGame(opts) {
2737
2738
  // The game may swap in its own shell (a custom renderer over the same core) via shellFactory;
2738
2739
  // default is the built-in Pixi shell. The host drives whichever it gets through the Shell contract.
2739
2740
  shell = (opts.shellFactory ?? createPixiShell)(pixiShellCfg);
2741
+ // Opt-in, per game, and BEFORE `currentTurbo` is seeded from `shell.state.turbo` below — the
2742
+ // restored level has to be in place by the time the host reads it, or the first spin would run
2743
+ // at the default speed while the bar shows the remembered one. `features.turbo` is passed as the
2744
+ // ceiling so a jurisdiction that capped turbo still wins over whatever is in storage.
2745
+ if (opts.persistSettings) {
2746
+ const p = opts.persistSettings === true ? {} : opts.persistSettings;
2747
+ attachSettingsStore(shell, {
2748
+ key: p.key ?? opts.model.spec.id,
2749
+ storage: p.storage,
2750
+ maxTurbo: pixiShellCfg.features.turbo,
2751
+ });
2752
+ }
2740
2753
  // Scope the bar to the slot scene: show only when a SlotSceneController scene is current
2741
2754
  // (hidden over the intro / non-slot scenes). Applies in BOTH base and replay modes.
2742
2755
  shell.setVisible(!!gameScene());
@@ -3363,6 +3376,9 @@ function toBonusOptions(model, t = (s) => s) {
3363
3376
  // Hero art (SSOT) → card thumbnail. Passed verbatim: the shell loads it as-is, so no URL
3364
3377
  // resolver is needed (matches a static buyBonus `thumbnail`). Keeps i18n/price/accent/social.
3365
3378
  ...(action.art ? { thumbnail: action.art } : {}),
3379
+ // Variant grouping (SSOT): actions sharing the key share one card in the pixi shell, flipped
3380
+ // through with arrows. Each stays its own action, so the id on activate/buy is unambiguous.
3381
+ ...(action.groupedBy ? { groupedBy: action.groupedBy } : {}),
3366
3382
  });
3367
3383
  }
3368
3384
  return out;
@@ -3712,6 +3728,137 @@ var sceneStart = /*#__PURE__*/Object.freeze({
3712
3728
  resolveStartScene: resolveStartScene
3713
3729
  });
3714
3730
 
3731
+ function settingsKey(gameId) {
3732
+ return `e8:${gameId}:settings`;
3733
+ }
3734
+ /**
3735
+ * `localStorage` is not reliably there and not reliably usable: Safari's private mode, a browser
3736
+ * configured to block storage, and a full quota can all make even a READ throw. A saved turbo level
3737
+ * is never worth failing a game boot over, so every access degrades to "no persistence".
3738
+ */
3739
+ function resolveStorage(explicit) {
3740
+ if (explicit !== undefined)
3741
+ return explicit;
3742
+ try {
3743
+ return typeof localStorage === 'undefined' ? null : localStorage;
3744
+ }
3745
+ catch {
3746
+ return null;
3747
+ }
3748
+ }
3749
+ /** A menu value is a boolean or a finite number — nothing else may reach the shell. */
3750
+ function isMenuValue(v) {
3751
+ return typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v));
3752
+ }
3753
+ /**
3754
+ * Read and VALIDATE. Whatever is in `localStorage` is player-writable — anyone can open devtools
3755
+ * and put a string, an object, or `turbo: 99` in there. It is parsed as untrusted input: a bad
3756
+ * field is dropped, not coerced, and a bad blob yields nothing at all.
3757
+ */
3758
+ function readSettings(storage, key) {
3759
+ if (!storage)
3760
+ return {};
3761
+ let raw;
3762
+ try {
3763
+ raw = storage.getItem(key);
3764
+ }
3765
+ catch {
3766
+ return {};
3767
+ }
3768
+ if (!raw)
3769
+ return {};
3770
+ let parsed;
3771
+ try {
3772
+ parsed = JSON.parse(raw);
3773
+ }
3774
+ catch {
3775
+ return {};
3776
+ }
3777
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
3778
+ return {};
3779
+ const src = parsed;
3780
+ const out = {};
3781
+ if (typeof src.turbo === 'number' && Number.isInteger(src.turbo) && src.turbo >= 0) {
3782
+ out.turbo = src.turbo;
3783
+ }
3784
+ if (src.menu && typeof src.menu === 'object' && !Array.isArray(src.menu)) {
3785
+ const menu = {};
3786
+ for (const [id, value] of Object.entries(src.menu)) {
3787
+ if (isMenuValue(value))
3788
+ menu[id] = value;
3789
+ }
3790
+ if (Object.keys(menu).length)
3791
+ out.menu = menu;
3792
+ }
3793
+ return out;
3794
+ }
3795
+ function writeSettings(storage, key, value) {
3796
+ if (!storage)
3797
+ return;
3798
+ try {
3799
+ storage.setItem(key, JSON.stringify(value));
3800
+ }
3801
+ catch {
3802
+ // Quota, private mode, or storage disabled mid-session. The player keeps playing; only the
3803
+ // memory of their preference is lost, and that is not worth an error in their face.
3804
+ }
3805
+ }
3806
+ /**
3807
+ * Restore the stored preferences onto a freshly created shell, then keep them in step.
3808
+ *
3809
+ * Returns an unsubscribe function.
3810
+ *
3811
+ * Two things are deliberate here:
3812
+ *
3813
+ * - **`maxTurbo` clamps the restored level.** `features.turbo` is the CEILING the shell offers
3814
+ * (`applyJurisdiction` lowers it where super-turbo or turbo is forbidden), while `state.turbo`
3815
+ * is what the player currently has. Restoring a saved 3 into a jurisdiction capped at 1 would
3816
+ * hand back exactly what the restriction took away, so the stored value is clamped, never
3817
+ * trusted.
3818
+ * - **Subscription happens AFTER restoring.** `setMenuValue` emits `settingChange`, so subscribing
3819
+ * first would have the restore write back what it just read. (`setTurbo` is quiet — only the
3820
+ * bar tap emits `turboChange` — which is why restoring the level can't echo. Do not "fix" that
3821
+ * asymmetry without moving this subscription.)
3822
+ */
3823
+ function attachSettingsStore(shell, opts) {
3824
+ const storage = resolveStorage(opts.storage);
3825
+ const key = settingsKey(opts.key);
3826
+ const stored = readSettings(storage, key);
3827
+ if (stored.turbo !== undefined) {
3828
+ shell.setTurbo(Math.min(stored.turbo, opts.maxTurbo ?? stored.turbo));
3829
+ }
3830
+ for (const [id, value] of Object.entries(stored.menu ?? {})) {
3831
+ // `setMenuValue` routes presets to their own homes (sound → setSound, music/sfx → setVolume)
3832
+ // and clamps custom ranges, so one loop covers every row and the shell owns the bounds.
3833
+ shell.setMenuValue(id, value);
3834
+ }
3835
+ const live = { ...stored };
3836
+ const flush = () => writeSettings(storage, key, live);
3837
+ const onTurbo = (level) => {
3838
+ live.turbo = level;
3839
+ flush();
3840
+ };
3841
+ const onSetting = ({ key: id, value }) => {
3842
+ if (!isMenuValue(value))
3843
+ return;
3844
+ live.menu = { ...live.menu, [id]: value };
3845
+ flush();
3846
+ };
3847
+ shell.on('turboChange', onTurbo);
3848
+ shell.on('settingChange', onSetting);
3849
+ return () => {
3850
+ shell.off('turboChange', onTurbo);
3851
+ shell.off('settingChange', onSetting);
3852
+ };
3853
+ }
3854
+
3855
+ var settingsStore = /*#__PURE__*/Object.freeze({
3856
+ __proto__: null,
3857
+ attachSettingsStore: attachSettingsStore,
3858
+ readSettings: readSettings,
3859
+ settingsKey: settingsKey
3860
+ });
3861
+
3715
3862
  function createWinReporter(paint) {
3716
3863
  let accepting = false;
3717
3864
  return {
@@ -4144,5 +4291,5 @@ var autoplay = /*#__PURE__*/Object.freeze({
4144
4291
  createAutoplayLoop: createAutoplayLoop
4145
4292
  });
4146
4293
 
4147
- export { buildShellConfig, createSlotGame, createWinReporter, resolveReplayBonusId, resolveStartScene, stakeForAction };
4294
+ export { attachSettingsStore, buildShellConfig, createSlotGame, createWinReporter, readSettings, resolveReplayBonusId, resolveStartScene, settingsKey, stakeForAction };
4148
4295
  //# sourceMappingURL=host.esm.js.map