@energy8platform/game-engine 0.35.1 → 0.37.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.cjs.js CHANGED
@@ -2624,6 +2624,7 @@ async function createSlotGame(opts) {
2624
2624
  };
2625
2625
  const { runRound } = await Promise.resolve().then(function () { return runRound$1; });
2626
2626
  const { createBalanceGate } = await Promise.resolve().then(function () { return balanceGate; });
2627
+ const { attachSettingsStore } = await Promise.resolve().then(function () { return settingsStore; });
2627
2628
  const { createFreeSpinsCounter } = await Promise.resolve().then(function () { return freeSpinsCounter; });
2628
2629
  const { resolvePlayError } = await Promise.resolve().then(function () { return playError; });
2629
2630
  // slotPlay references shell via closure — define it after shell is assigned below.
@@ -2738,6 +2739,18 @@ async function createSlotGame(opts) {
2738
2739
  // The game may swap in its own shell (a custom renderer over the same core) via shellFactory;
2739
2740
  // default is the built-in Pixi shell. The host drives whichever it gets through the Shell contract.
2740
2741
  shell = (opts.shellFactory ?? createPixiShell)(pixiShellCfg);
2742
+ // Opt-in, per game, and BEFORE `currentTurbo` is seeded from `shell.state.turbo` below — the
2743
+ // restored level has to be in place by the time the host reads it, or the first spin would run
2744
+ // at the default speed while the bar shows the remembered one. `features.turbo` is passed as the
2745
+ // ceiling so a jurisdiction that capped turbo still wins over whatever is in storage.
2746
+ if (opts.persistSettings) {
2747
+ const p = opts.persistSettings === true ? {} : opts.persistSettings;
2748
+ attachSettingsStore(shell, {
2749
+ key: p.key ?? opts.model.spec.id,
2750
+ storage: p.storage,
2751
+ maxTurbo: pixiShellCfg.features.turbo,
2752
+ });
2753
+ }
2741
2754
  // Scope the bar to the slot scene: show only when a SlotSceneController scene is current
2742
2755
  // (hidden over the intro / non-slot scenes). Applies in BOTH base and replay modes.
2743
2756
  shell.setVisible(!!gameScene());
@@ -3388,7 +3401,12 @@ function paytableSection(model, t = (s) => s) {
3388
3401
  // localized (matching how the modes/wins sections rely on the shell's translated fallback).
3389
3402
  return { type: 'paytable', rows };
3390
3403
  }
3391
- /** Build a "wins" illustration section sized to the grid; `kind` follows the spec mechanic hint. */
3404
+ /** Build a "wins" illustration section sized to the grid; `kind` follows the spec mechanic hint.
3405
+ *
3406
+ * Every mechanic that has an illustration names itself. An unrecognised one renders NOTHING
3407
+ * rather than borrowing `anywhere`'s picture: the section exists to show the player how wins
3408
+ * form, and a drawing of the wrong mechanic teaches them something false. Silence is the honest
3409
+ * answer — the rest of the info screen (paytable, modes, controls) still renders. */
3392
3410
  function winsSection(model) {
3393
3411
  const { cols, rows } = model.spec.grid;
3394
3412
  const grid = { cols, rows };
@@ -3397,8 +3415,10 @@ function winsSection(model) {
3397
3415
  return { type: 'wins', kind: 'cluster', minCount: 5, grid };
3398
3416
  case 'ways':
3399
3417
  return { type: 'wins', kind: 'ways', grid };
3400
- default:
3418
+ case 'anywhere':
3401
3419
  return { type: 'wins', kind: 'anywhere', minCount: 3, grid };
3420
+ default:
3421
+ return null;
3402
3422
  }
3403
3423
  }
3404
3424
  /** Title of the legal disclaimer section — used to build it and to exempt it from socialization. */
@@ -3441,7 +3461,9 @@ function orderDisclaimerLast(sections) {
3441
3461
  */
3442
3462
  function defaultGameInfo(model, runtime, t = (s) => s, tDisclaimer = t) {
3443
3463
  const sections = [];
3444
- sections.push(winsSection(model));
3464
+ const wins = winsSection(model);
3465
+ if (wins)
3466
+ sections.push(wins);
3445
3467
  const pay = paytableSection(model, t);
3446
3468
  if (pay)
3447
3469
  sections.push(pay);
@@ -3704,6 +3726,137 @@ var sceneStart = /*#__PURE__*/Object.freeze({
3704
3726
  resolveStartScene: resolveStartScene
3705
3727
  });
3706
3728
 
3729
+ function settingsKey(gameId) {
3730
+ return `e8:${gameId}:settings`;
3731
+ }
3732
+ /**
3733
+ * `localStorage` is not reliably there and not reliably usable: Safari's private mode, a browser
3734
+ * configured to block storage, and a full quota can all make even a READ throw. A saved turbo level
3735
+ * is never worth failing a game boot over, so every access degrades to "no persistence".
3736
+ */
3737
+ function resolveStorage(explicit) {
3738
+ if (explicit !== undefined)
3739
+ return explicit;
3740
+ try {
3741
+ return typeof localStorage === 'undefined' ? null : localStorage;
3742
+ }
3743
+ catch {
3744
+ return null;
3745
+ }
3746
+ }
3747
+ /** A menu value is a boolean or a finite number — nothing else may reach the shell. */
3748
+ function isMenuValue(v) {
3749
+ return typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v));
3750
+ }
3751
+ /**
3752
+ * Read and VALIDATE. Whatever is in `localStorage` is player-writable — anyone can open devtools
3753
+ * and put a string, an object, or `turbo: 99` in there. It is parsed as untrusted input: a bad
3754
+ * field is dropped, not coerced, and a bad blob yields nothing at all.
3755
+ */
3756
+ function readSettings(storage, key) {
3757
+ if (!storage)
3758
+ return {};
3759
+ let raw;
3760
+ try {
3761
+ raw = storage.getItem(key);
3762
+ }
3763
+ catch {
3764
+ return {};
3765
+ }
3766
+ if (!raw)
3767
+ return {};
3768
+ let parsed;
3769
+ try {
3770
+ parsed = JSON.parse(raw);
3771
+ }
3772
+ catch {
3773
+ return {};
3774
+ }
3775
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
3776
+ return {};
3777
+ const src = parsed;
3778
+ const out = {};
3779
+ if (typeof src.turbo === 'number' && Number.isInteger(src.turbo) && src.turbo >= 0) {
3780
+ out.turbo = src.turbo;
3781
+ }
3782
+ if (src.menu && typeof src.menu === 'object' && !Array.isArray(src.menu)) {
3783
+ const menu = {};
3784
+ for (const [id, value] of Object.entries(src.menu)) {
3785
+ if (isMenuValue(value))
3786
+ menu[id] = value;
3787
+ }
3788
+ if (Object.keys(menu).length)
3789
+ out.menu = menu;
3790
+ }
3791
+ return out;
3792
+ }
3793
+ function writeSettings(storage, key, value) {
3794
+ if (!storage)
3795
+ return;
3796
+ try {
3797
+ storage.setItem(key, JSON.stringify(value));
3798
+ }
3799
+ catch {
3800
+ // Quota, private mode, or storage disabled mid-session. The player keeps playing; only the
3801
+ // memory of their preference is lost, and that is not worth an error in their face.
3802
+ }
3803
+ }
3804
+ /**
3805
+ * Restore the stored preferences onto a freshly created shell, then keep them in step.
3806
+ *
3807
+ * Returns an unsubscribe function.
3808
+ *
3809
+ * Two things are deliberate here:
3810
+ *
3811
+ * - **`maxTurbo` clamps the restored level.** `features.turbo` is the CEILING the shell offers
3812
+ * (`applyJurisdiction` lowers it where super-turbo or turbo is forbidden), while `state.turbo`
3813
+ * is what the player currently has. Restoring a saved 3 into a jurisdiction capped at 1 would
3814
+ * hand back exactly what the restriction took away, so the stored value is clamped, never
3815
+ * trusted.
3816
+ * - **Subscription happens AFTER restoring.** `setMenuValue` emits `settingChange`, so subscribing
3817
+ * first would have the restore write back what it just read. (`setTurbo` is quiet — only the
3818
+ * bar tap emits `turboChange` — which is why restoring the level can't echo. Do not "fix" that
3819
+ * asymmetry without moving this subscription.)
3820
+ */
3821
+ function attachSettingsStore(shell, opts) {
3822
+ const storage = resolveStorage(opts.storage);
3823
+ const key = settingsKey(opts.key);
3824
+ const stored = readSettings(storage, key);
3825
+ if (stored.turbo !== undefined) {
3826
+ shell.setTurbo(Math.min(stored.turbo, opts.maxTurbo ?? stored.turbo));
3827
+ }
3828
+ for (const [id, value] of Object.entries(stored.menu ?? {})) {
3829
+ // `setMenuValue` routes presets to their own homes (sound → setSound, music/sfx → setVolume)
3830
+ // and clamps custom ranges, so one loop covers every row and the shell owns the bounds.
3831
+ shell.setMenuValue(id, value);
3832
+ }
3833
+ const live = { ...stored };
3834
+ const flush = () => writeSettings(storage, key, live);
3835
+ const onTurbo = (level) => {
3836
+ live.turbo = level;
3837
+ flush();
3838
+ };
3839
+ const onSetting = ({ key: id, value }) => {
3840
+ if (!isMenuValue(value))
3841
+ return;
3842
+ live.menu = { ...live.menu, [id]: value };
3843
+ flush();
3844
+ };
3845
+ shell.on('turboChange', onTurbo);
3846
+ shell.on('settingChange', onSetting);
3847
+ return () => {
3848
+ shell.off('turboChange', onTurbo);
3849
+ shell.off('settingChange', onSetting);
3850
+ };
3851
+ }
3852
+
3853
+ var settingsStore = /*#__PURE__*/Object.freeze({
3854
+ __proto__: null,
3855
+ attachSettingsStore: attachSettingsStore,
3856
+ readSettings: readSettings,
3857
+ settingsKey: settingsKey
3858
+ });
3859
+
3707
3860
  function createWinReporter(paint) {
3708
3861
  let accepting = false;
3709
3862
  return {
@@ -4152,10 +4305,13 @@ Object.defineProperty(exports, "socialize", {
4152
4305
  enumerable: true,
4153
4306
  get: function () { return shell.socialize; }
4154
4307
  });
4308
+ exports.attachSettingsStore = attachSettingsStore;
4155
4309
  exports.buildShellConfig = buildShellConfig;
4156
4310
  exports.createSlotGame = createSlotGame;
4157
4311
  exports.createWinReporter = createWinReporter;
4312
+ exports.readSettings = readSettings;
4158
4313
  exports.resolveReplayBonusId = resolveReplayBonusId;
4159
4314
  exports.resolveStartScene = resolveStartScene;
4315
+ exports.settingsKey = settingsKey;
4160
4316
  exports.stakeForAction = stakeForAction;
4161
4317
  //# sourceMappingURL=host.cjs.js.map