@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 +159 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +59 -2
- package/dist/host.esm.js +157 -4
- package/dist/host.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/host/createSlotGame.ts +13 -0
- package/src/host/index.ts +2 -0
- package/src/host/settingsStore.ts +148 -0
- package/src/host/shellConfig.ts +12 -4
- package/src/host/types.ts +7 -0
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());
|
|
@@ -3387,7 +3400,12 @@ function paytableSection(model, t = (s) => s) {
|
|
|
3387
3400
|
// localized (matching how the modes/wins sections rely on the shell's translated fallback).
|
|
3388
3401
|
return { type: 'paytable', rows };
|
|
3389
3402
|
}
|
|
3390
|
-
/** Build a "wins" illustration section sized to the grid; `kind` follows the spec mechanic hint.
|
|
3403
|
+
/** Build a "wins" illustration section sized to the grid; `kind` follows the spec mechanic hint.
|
|
3404
|
+
*
|
|
3405
|
+
* Every mechanic that has an illustration names itself. An unrecognised one renders NOTHING
|
|
3406
|
+
* rather than borrowing `anywhere`'s picture: the section exists to show the player how wins
|
|
3407
|
+
* form, and a drawing of the wrong mechanic teaches them something false. Silence is the honest
|
|
3408
|
+
* answer — the rest of the info screen (paytable, modes, controls) still renders. */
|
|
3391
3409
|
function winsSection(model) {
|
|
3392
3410
|
const { cols, rows } = model.spec.grid;
|
|
3393
3411
|
const grid = { cols, rows };
|
|
@@ -3396,8 +3414,10 @@ function winsSection(model) {
|
|
|
3396
3414
|
return { type: 'wins', kind: 'cluster', minCount: 5, grid };
|
|
3397
3415
|
case 'ways':
|
|
3398
3416
|
return { type: 'wins', kind: 'ways', grid };
|
|
3399
|
-
|
|
3417
|
+
case 'anywhere':
|
|
3400
3418
|
return { type: 'wins', kind: 'anywhere', minCount: 3, grid };
|
|
3419
|
+
default:
|
|
3420
|
+
return null;
|
|
3401
3421
|
}
|
|
3402
3422
|
}
|
|
3403
3423
|
/** Title of the legal disclaimer section — used to build it and to exempt it from socialization. */
|
|
@@ -3440,7 +3460,9 @@ function orderDisclaimerLast(sections) {
|
|
|
3440
3460
|
*/
|
|
3441
3461
|
function defaultGameInfo(model, runtime, t = (s) => s, tDisclaimer = t) {
|
|
3442
3462
|
const sections = [];
|
|
3443
|
-
|
|
3463
|
+
const wins = winsSection(model);
|
|
3464
|
+
if (wins)
|
|
3465
|
+
sections.push(wins);
|
|
3444
3466
|
const pay = paytableSection(model, t);
|
|
3445
3467
|
if (pay)
|
|
3446
3468
|
sections.push(pay);
|
|
@@ -3703,6 +3725,137 @@ var sceneStart = /*#__PURE__*/Object.freeze({
|
|
|
3703
3725
|
resolveStartScene: resolveStartScene
|
|
3704
3726
|
});
|
|
3705
3727
|
|
|
3728
|
+
function settingsKey(gameId) {
|
|
3729
|
+
return `e8:${gameId}:settings`;
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3732
|
+
* `localStorage` is not reliably there and not reliably usable: Safari's private mode, a browser
|
|
3733
|
+
* configured to block storage, and a full quota can all make even a READ throw. A saved turbo level
|
|
3734
|
+
* is never worth failing a game boot over, so every access degrades to "no persistence".
|
|
3735
|
+
*/
|
|
3736
|
+
function resolveStorage(explicit) {
|
|
3737
|
+
if (explicit !== undefined)
|
|
3738
|
+
return explicit;
|
|
3739
|
+
try {
|
|
3740
|
+
return typeof localStorage === 'undefined' ? null : localStorage;
|
|
3741
|
+
}
|
|
3742
|
+
catch {
|
|
3743
|
+
return null;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
/** A menu value is a boolean or a finite number — nothing else may reach the shell. */
|
|
3747
|
+
function isMenuValue(v) {
|
|
3748
|
+
return typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v));
|
|
3749
|
+
}
|
|
3750
|
+
/**
|
|
3751
|
+
* Read and VALIDATE. Whatever is in `localStorage` is player-writable — anyone can open devtools
|
|
3752
|
+
* and put a string, an object, or `turbo: 99` in there. It is parsed as untrusted input: a bad
|
|
3753
|
+
* field is dropped, not coerced, and a bad blob yields nothing at all.
|
|
3754
|
+
*/
|
|
3755
|
+
function readSettings(storage, key) {
|
|
3756
|
+
if (!storage)
|
|
3757
|
+
return {};
|
|
3758
|
+
let raw;
|
|
3759
|
+
try {
|
|
3760
|
+
raw = storage.getItem(key);
|
|
3761
|
+
}
|
|
3762
|
+
catch {
|
|
3763
|
+
return {};
|
|
3764
|
+
}
|
|
3765
|
+
if (!raw)
|
|
3766
|
+
return {};
|
|
3767
|
+
let parsed;
|
|
3768
|
+
try {
|
|
3769
|
+
parsed = JSON.parse(raw);
|
|
3770
|
+
}
|
|
3771
|
+
catch {
|
|
3772
|
+
return {};
|
|
3773
|
+
}
|
|
3774
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
3775
|
+
return {};
|
|
3776
|
+
const src = parsed;
|
|
3777
|
+
const out = {};
|
|
3778
|
+
if (typeof src.turbo === 'number' && Number.isInteger(src.turbo) && src.turbo >= 0) {
|
|
3779
|
+
out.turbo = src.turbo;
|
|
3780
|
+
}
|
|
3781
|
+
if (src.menu && typeof src.menu === 'object' && !Array.isArray(src.menu)) {
|
|
3782
|
+
const menu = {};
|
|
3783
|
+
for (const [id, value] of Object.entries(src.menu)) {
|
|
3784
|
+
if (isMenuValue(value))
|
|
3785
|
+
menu[id] = value;
|
|
3786
|
+
}
|
|
3787
|
+
if (Object.keys(menu).length)
|
|
3788
|
+
out.menu = menu;
|
|
3789
|
+
}
|
|
3790
|
+
return out;
|
|
3791
|
+
}
|
|
3792
|
+
function writeSettings(storage, key, value) {
|
|
3793
|
+
if (!storage)
|
|
3794
|
+
return;
|
|
3795
|
+
try {
|
|
3796
|
+
storage.setItem(key, JSON.stringify(value));
|
|
3797
|
+
}
|
|
3798
|
+
catch {
|
|
3799
|
+
// Quota, private mode, or storage disabled mid-session. The player keeps playing; only the
|
|
3800
|
+
// memory of their preference is lost, and that is not worth an error in their face.
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Restore the stored preferences onto a freshly created shell, then keep them in step.
|
|
3805
|
+
*
|
|
3806
|
+
* Returns an unsubscribe function.
|
|
3807
|
+
*
|
|
3808
|
+
* Two things are deliberate here:
|
|
3809
|
+
*
|
|
3810
|
+
* - **`maxTurbo` clamps the restored level.** `features.turbo` is the CEILING the shell offers
|
|
3811
|
+
* (`applyJurisdiction` lowers it where super-turbo or turbo is forbidden), while `state.turbo`
|
|
3812
|
+
* is what the player currently has. Restoring a saved 3 into a jurisdiction capped at 1 would
|
|
3813
|
+
* hand back exactly what the restriction took away, so the stored value is clamped, never
|
|
3814
|
+
* trusted.
|
|
3815
|
+
* - **Subscription happens AFTER restoring.** `setMenuValue` emits `settingChange`, so subscribing
|
|
3816
|
+
* first would have the restore write back what it just read. (`setTurbo` is quiet — only the
|
|
3817
|
+
* bar tap emits `turboChange` — which is why restoring the level can't echo. Do not "fix" that
|
|
3818
|
+
* asymmetry without moving this subscription.)
|
|
3819
|
+
*/
|
|
3820
|
+
function attachSettingsStore(shell, opts) {
|
|
3821
|
+
const storage = resolveStorage(opts.storage);
|
|
3822
|
+
const key = settingsKey(opts.key);
|
|
3823
|
+
const stored = readSettings(storage, key);
|
|
3824
|
+
if (stored.turbo !== undefined) {
|
|
3825
|
+
shell.setTurbo(Math.min(stored.turbo, opts.maxTurbo ?? stored.turbo));
|
|
3826
|
+
}
|
|
3827
|
+
for (const [id, value] of Object.entries(stored.menu ?? {})) {
|
|
3828
|
+
// `setMenuValue` routes presets to their own homes (sound → setSound, music/sfx → setVolume)
|
|
3829
|
+
// and clamps custom ranges, so one loop covers every row and the shell owns the bounds.
|
|
3830
|
+
shell.setMenuValue(id, value);
|
|
3831
|
+
}
|
|
3832
|
+
const live = { ...stored };
|
|
3833
|
+
const flush = () => writeSettings(storage, key, live);
|
|
3834
|
+
const onTurbo = (level) => {
|
|
3835
|
+
live.turbo = level;
|
|
3836
|
+
flush();
|
|
3837
|
+
};
|
|
3838
|
+
const onSetting = ({ key: id, value }) => {
|
|
3839
|
+
if (!isMenuValue(value))
|
|
3840
|
+
return;
|
|
3841
|
+
live.menu = { ...live.menu, [id]: value };
|
|
3842
|
+
flush();
|
|
3843
|
+
};
|
|
3844
|
+
shell.on('turboChange', onTurbo);
|
|
3845
|
+
shell.on('settingChange', onSetting);
|
|
3846
|
+
return () => {
|
|
3847
|
+
shell.off('turboChange', onTurbo);
|
|
3848
|
+
shell.off('settingChange', onSetting);
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
var settingsStore = /*#__PURE__*/Object.freeze({
|
|
3853
|
+
__proto__: null,
|
|
3854
|
+
attachSettingsStore: attachSettingsStore,
|
|
3855
|
+
readSettings: readSettings,
|
|
3856
|
+
settingsKey: settingsKey
|
|
3857
|
+
});
|
|
3858
|
+
|
|
3706
3859
|
function createWinReporter(paint) {
|
|
3707
3860
|
let accepting = false;
|
|
3708
3861
|
return {
|
|
@@ -4135,5 +4288,5 @@ var autoplay = /*#__PURE__*/Object.freeze({
|
|
|
4135
4288
|
createAutoplayLoop: createAutoplayLoop
|
|
4136
4289
|
});
|
|
4137
4290
|
|
|
4138
|
-
export { buildShellConfig, createSlotGame, createWinReporter, resolveReplayBonusId, resolveStartScene, stakeForAction };
|
|
4291
|
+
export { attachSettingsStore, buildShellConfig, createSlotGame, createWinReporter, readSettings, resolveReplayBonusId, resolveStartScene, settingsKey, stakeForAction };
|
|
4139
4292
|
//# sourceMappingURL=host.esm.js.map
|