@stakeplate/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 schmooky and the stakeplate contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @stakeplate/core
2
+
3
+ The batteries-included **Stake Engine game core**. Import it, and the RGS handshake,
4
+ boot flow, round lifecycle, HUD wiring, audio, i18n and compliant rules all *just work*
5
+ — "plug a battery into a slot." A game supplies only its **scene(s)**, its **Present
6
+ phase**, its **sounds**, and a pure **`interpretBook`**.
7
+
8
+ Built on [`@open-slot-ui`](https://github.com/schmooky/open-slot-ui) (HUD + compliance),
9
+ [`@schmooky/zvuk`](https://github.com/schmooky/zvuk) (audio), pixi-reels (boards) and
10
+ Pixi v8.
11
+
12
+ > Status: **usable, pre-1.0.** In: the `@stakeplate/core/rgs` transport + runtime (real
13
+ > Stake wire — `authenticate {sessionID, language}`, `play {mode, currency, amount}`,
14
+ > end-round, replay — plus an authoritative, scriptable mock RGS), the `createStakeGame(...)`
15
+ > façade, the engine/phases + stores, `/audio` (zvuk buses + HUD binding), `/rules`
16
+ > (`buildRules` + social dict), turbo/autoplay, buy-features and `/testing`. Still landing:
17
+ > `/scene` helpers and `/i18n`.
18
+
19
+ ## The idea
20
+
21
+ ```ts
22
+ import { createStakeGame } from '@stakeplate/core';
23
+
24
+ const game = createStakeGame({
25
+ config, // title, currency, modes (+buy/boost), rules
26
+ interpretBook: raw => parseRound(raw), // pure: RGS book → your model
27
+ mountView: (host, ctx) => new MyScene(host, ctx), // your pixi + presenters + stores
28
+ audio: manifest,
29
+ phases: [new PresentPhase()], // Idle/Spin/Settle are provided
30
+ });
31
+ await game.start(); // auth+language, resume, replay, errors, jurisdiction, HUD, audio…
32
+ ```
33
+
34
+ ## Buy-features & rules
35
+
36
+ A mode flagged `buy` or `boost` makes the **bonus button open the feature-list modal**
37
+ (shipped by `@open-slot-ui/pixi`) — one card per feature, with the jurisdiction confirm gate:
38
+
39
+ ```ts
40
+ config.modes = {
41
+ base: 1,
42
+ bonus: { cost: 100, buy: true, name: 'Free Spins', image: art }, // Buy card — spins once
43
+ lucky: { cost: 2, boost: true, name: 'Lucky Bet', image: art }, // Activate card — 2× ante
44
+ };
45
+ ```
46
+
47
+ `buy` cards spend `cost × bet` and spin that mode once; `boost` cards toggle a persistent ante
48
+ (every spin plays at `cost×`, the readout shows the boosted stake). Build the **compliant info
49
+ menu** — how-to-play, per-button guide, stats, and the exact Stake disclaimer — with `buildRules`:
50
+
51
+ ```ts
52
+ import { buildRules } from '@stakeplate/core/rules';
53
+ const built = buildRules({ about, howToPlay, features, paytable, stats });
54
+ config.rules = built.menu; // → the white HTML menu
55
+ config.socialMessages = { en: built.socialEn }; // auto-derived social wording
56
+ ```
57
+
58
+ ## Subpaths
59
+
60
+ - `@stakeplate/core` — the one-call API + engine (turbo, autoplay, buy-features).
61
+ - `@stakeplate/core/rgs` — wire protocol, launch-param runtime, `NetworkManager` +
62
+ Stake/mock adapters, `createNetwork`.
63
+ - `@stakeplate/core/audio` — zvuk bus graph (music/effects groups) + HUD slider/mute binding.
64
+ - `@stakeplate/core/rules` — `buildRules` compliant menu + `toSocial`/`findRestricted` + dict.
65
+ - `@stakeplate/core/stores` — the MobX stores (balance, ui) for composing game state.
66
+ - `@stakeplate/core/testing` — the mock RGS, scriptable network, instant ticker.
67
+ - (soon) `/scene`, `/i18n`.
68
+
69
+ ## License
70
+
71
+ MIT © schmooky and the stakeplate contributors.
@@ -0,0 +1,126 @@
1
+ //#region src/rgs/protocol.ts
2
+ /**
3
+ * Stake Engine wire protocol — the GAME-AGNOSTIC shapes every Stake RGS speaks.
4
+ * (A game's own book/event types live in the game and are parsed by its
5
+ * `interpretBook`; here a round's events are opaque `unknown[]`.)
6
+ *
7
+ * Two money scales (Stake convention):
8
+ * - API units : value × 1_000_000 (six implied decimals). Every RGS endpoint.
9
+ * - BOOK units: value × 100. A book's `payoutMultiplier` + its event amounts.
10
+ * So: win_api = round(betAmount_api × payoutMultiplier_book / 100).
11
+ *
12
+ * @see https://github.com/StakeEngine/web-sdk — packages/rgs-requests, rgs-fetcher/schema
13
+ */
14
+ const API_AMOUNT_MULTIPLIER = 1e6;
15
+ const BOOK_AMOUNT_MULTIPLIER = 100;
16
+ /** The book events for a round, whichever field the RGS used (`state` or `events`). */
17
+ function roundEvents(round) {
18
+ if (Array.isArray(round.state)) return round.state;
19
+ if (Array.isArray(round.events)) return round.events;
20
+ return [];
21
+ }
22
+ //#endregion
23
+ //#region src/rgs/MockNetworkManager.ts
24
+ const toApi = (major) => Math.round(major * API_AMOUNT_MULTIPLIER);
25
+ var MockNetworkManager = class {
26
+ balanceApi;
27
+ currency;
28
+ config;
29
+ active;
30
+ modes;
31
+ queue = [];
32
+ /** The last round returned but not yet end-round'd (its win is applied on settle). */
33
+ pendingWinApi = 0;
34
+ constructor(opts = {}) {
35
+ this.balanceApi = toApi(opts.balance ?? 1e3);
36
+ this.currency = opts.currency ?? "USD";
37
+ const levels = (opts.betLevels ?? [
38
+ .2,
39
+ .5,
40
+ 1,
41
+ 2,
42
+ 5,
43
+ 10
44
+ ]).map(toApi);
45
+ this.config = {
46
+ minBet: levels[0],
47
+ maxBet: levels[levels.length - 1],
48
+ stepBet: levels[0],
49
+ betLevels: levels,
50
+ defaultBetLevel: toApi(opts.defaultBet ?? 1),
51
+ ...opts.rtp != null ? { rtp: opts.rtp } : {},
52
+ ...opts.jurisdiction ? { jurisdiction: opts.jurisdiction } : {}
53
+ };
54
+ this.active = opts.activeRound ?? null;
55
+ this.modes = opts.modes ?? {};
56
+ }
57
+ /** Queue the next round `play()` will return (call repeatedly to queue several). */
58
+ forceRound(round) {
59
+ this.queue.push(round);
60
+ return this;
61
+ }
62
+ /** Set the current balance (MAJOR units). */
63
+ setBalance(major) {
64
+ this.balanceApi = toApi(major);
65
+ return this;
66
+ }
67
+ balance() {
68
+ return {
69
+ amount: this.balanceApi,
70
+ currency: this.currency
71
+ };
72
+ }
73
+ async authenticate() {
74
+ return {
75
+ balance: this.balance(),
76
+ config: this.config,
77
+ round: this.active
78
+ };
79
+ }
80
+ async play(args) {
81
+ const s = this.queue.shift() ?? {};
82
+ const cost = this.modes[args.mode] ?? 1;
83
+ const stakeApi = s.stake != null ? toApi(s.stake) : toApi(args.bet * cost);
84
+ const betApi = toApi(args.bet);
85
+ const pm = s.payoutMultiplier ?? (s.win != null && args.bet > 0 ? Math.round(s.win / args.bet * 100) : 0);
86
+ const winApi = Math.round(betApi * pm / 100);
87
+ this.balanceApi -= stakeApi;
88
+ const round = {
89
+ betID: `mock-${Date.now()}`,
90
+ mode: s.mode ?? args.mode,
91
+ amount: stakeApi,
92
+ payout: winApi,
93
+ payoutMultiplier: pm,
94
+ state: s.events ?? [],
95
+ active: s.active ?? false
96
+ };
97
+ if (round.active) this.pendingWinApi = winApi;
98
+ else this.balanceApi += winApi;
99
+ return {
100
+ round,
101
+ balance: this.balance()
102
+ };
103
+ }
104
+ async endRound() {
105
+ this.balanceApi += this.pendingWinApi;
106
+ this.pendingWinApi = 0;
107
+ this.active = null;
108
+ return { balance: this.balance() };
109
+ }
110
+ async replay(_p) {
111
+ const s = this.queue.shift() ?? { payoutMultiplier: 0 };
112
+ return {
113
+ betID: "replay",
114
+ mode: s.mode ?? "base",
115
+ amount: toApi(s.stake ?? 1),
116
+ payout: 0,
117
+ payoutMultiplier: s.payoutMultiplier ?? 0,
118
+ state: s.events ?? [],
119
+ active: false
120
+ };
121
+ }
122
+ };
123
+ //#endregion
124
+ export { roundEvents as i, API_AMOUNT_MULTIPLIER as n, BOOK_AMOUNT_MULTIPLIER as r, MockNetworkManager as t };
125
+
126
+ //# sourceMappingURL=MockNetworkManager-cj8WT5v0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockNetworkManager-cj8WT5v0.js","names":[],"sources":["../src/rgs/protocol.ts","../src/rgs/MockNetworkManager.ts"],"sourcesContent":["/**\n * Stake Engine wire protocol — the GAME-AGNOSTIC shapes every Stake RGS speaks.\n * (A game's own book/event types live in the game and are parsed by its\n * `interpretBook`; here a round's events are opaque `unknown[]`.)\n *\n * Two money scales (Stake convention):\n * - API units : value × 1_000_000 (six implied decimals). Every RGS endpoint.\n * - BOOK units: value × 100. A book's `payoutMultiplier` + its event amounts.\n * So: win_api = round(betAmount_api × payoutMultiplier_book / 100).\n *\n * @see https://github.com/StakeEngine/web-sdk — packages/rgs-requests, rgs-fetcher/schema\n */\n\nexport const API_AMOUNT_MULTIPLIER = 1_000_000;\nexport const BOOK_AMOUNT_MULTIPLIER = 100;\n\nexport interface Balance {\n amount: number; // API units\n currency: string;\n}\n\n/**\n * Stake Engine's per-player jurisdiction config, delivered at `/wallet/authenticate`\n * as `config.jurisdiction`. Structurally compatible with `@open-slot-ui`'s\n * `JurisdictionConfig`, so it feeds straight into `hud.applyJurisdiction`.\n */\nexport interface JurisdictionConfig {\n socialCasino?: boolean;\n disabledFullscreen?: boolean;\n disabledTurbo?: boolean;\n disabledSuperTurbo?: boolean;\n disabledAutoplay?: boolean;\n disabledSlamstop?: boolean;\n disabledSpacebar?: boolean;\n disabledBuyFeature?: boolean;\n displayNetPosition?: boolean;\n displayRTP?: boolean;\n displaySessionTimer?: boolean;\n /** Minimum ms a round must take — enforced by the core, not the platform. */\n minimumRoundDuration?: number;\n /** Buy-feature confirm threshold (× base bet) — fed to the HUD's confirm gate. */\n confirmBuyAboveCost?: number;\n}\n\nexport interface RgsConfig {\n minBet: number; // API units\n maxBet: number; // API units\n stepBet: number; // API units\n betLevels: number[]; // API units\n defaultBetLevel: number; // API units\n /** Theoretical RTP percentage (e.g. 96.0) for the RTP readout. */\n rtp?: number;\n jurisdiction?: JurisdictionConfig;\n}\n\n/**\n * One RGS round. `E` is the game's book-event type (opaque to the core — the game's\n * `interpretBook` narrows it). The real Stake RGS returns the event list under\n * `state`; some emulators use `events`; read via {@link roundEvents}.\n */\nexport interface Round<E = unknown> {\n betID: string | number;\n mode: string;\n amount: number; // API units (stake)\n payout?: number; // API units (win)\n payoutMultiplier: number; // BOOK units\n state?: E[];\n events?: E[];\n /** True while a (winning) round still awaits `/wallet/end-round`. */\n active?: boolean;\n}\n\n/** The book events for a round, whichever field the RGS used (`state` or `events`). */\nexport function roundEvents<E>(round: Round<E>): E[] {\n if (Array.isArray(round.state)) return round.state;\n if (Array.isArray(round.events)) return round.events;\n return [];\n}\n\nexport interface AuthenticateRequest {\n sessionID: string;\n /** REQUIRED by the real RGS — omitting it 400s `ERR_VAL \"could not parse request json\"`. */\n language: string;\n}\nexport interface AuthenticateResponse {\n balance: Balance;\n config: RgsConfig;\n round: Round | null;\n}\n\nexport interface PlayRequest {\n sessionID: string;\n amount: number; // API units (base bet)\n mode: string;\n currency: string;\n}\nexport interface PlayResponse {\n round: Round;\n balance: Balance;\n}\n\nexport interface EndRoundRequest {\n sessionID: string;\n}\nexport interface EndRoundResponse {\n balance: Balance;\n}\n\n/** `GET /bet/replay/{game}/{version}/{mode}/{event}` params (from `?replay=…` launch). */\nexport interface ReplayParams {\n game: string;\n version: string;\n mode: string;\n event: string;\n /** The bet amount for the replayed round (API units are NOT used here; major units). */\n amount: number;\n}\n\n/** RGS error body (`{ error: 'ERR_VAL', message }`) — codes map to `hud.showRgsError`. */\nexport interface RgsError {\n error: string;\n message?: string;\n}\n","// MockNetworkManager — an in-process, AUTHORITATIVE fake RGS for local dev + tests.\n// It owns the balance, serves a config/jurisdiction, and is SCRIPTABLE: queue exact\n// rounds with `forceRound(...)` (a 5000×, a bonus, a no-win) for deterministic play.\n// Money is API units on the wire; `forceRound` amounts are convenient MAJOR units.\n\nimport {\n API_AMOUNT_MULTIPLIER,\n type AuthenticateResponse,\n type Balance,\n type EndRoundResponse,\n type JurisdictionConfig,\n type PlayResponse,\n type ReplayParams,\n type RgsConfig,\n type Round,\n} from './protocol';\nimport type { NetworkManager, PlayArgs } from './network';\n\nexport interface MockOptions {\n /** Starting balance in MAJOR units (default 1000). */\n balance?: number;\n currency?: string;\n betLevels?: number[]; // major units\n defaultBet?: number; // major units\n rtp?: number;\n jurisdiction?: JurisdictionConfig;\n /** Mode key → cost multiplier, so the mock charges bet × cost like the platform. */\n modes?: Record<string, number>;\n /** An incomplete round present at authenticate — for testing mid-spin resume. */\n activeRound?: Round | null;\n}\n\n/** A scripted round. `payoutMultiplier` is BOOK units (×100); `win` (major) is a shortcut. */\nexport interface ScriptedRound {\n mode?: string;\n /** BOOK units (100 = 1×). Provide this OR `win`. */\n payoutMultiplier?: number;\n /** Convenience: the win in MAJOR units for the given bet (sets payoutMultiplier). */\n win?: number;\n /** The stake charged, MAJOR units (default = the bet passed to play). */\n stake?: number;\n /** The book events the game's `interpretBook` will parse. */\n events?: unknown[];\n /** Leave the round open (needs `end-round`) — for exercising the settle path. */\n active?: boolean;\n}\n\nconst toApi = (major: number): number => Math.round(major * API_AMOUNT_MULTIPLIER);\n\nexport class MockNetworkManager implements NetworkManager {\n private balanceApi: number;\n private currency: string;\n private readonly config: RgsConfig;\n private active: Round | null;\n private readonly modes: Record<string, number>;\n private readonly queue: ScriptedRound[] = [];\n /** The last round returned but not yet end-round'd (its win is applied on settle). */\n private pendingWinApi = 0;\n\n constructor(opts: MockOptions = {}) {\n this.balanceApi = toApi(opts.balance ?? 1000);\n this.currency = opts.currency ?? 'USD';\n const levels = (opts.betLevels ?? [0.2, 0.5, 1, 2, 5, 10]).map(toApi);\n this.config = {\n minBet: levels[0]!,\n maxBet: levels[levels.length - 1]!,\n stepBet: levels[0]!,\n betLevels: levels,\n defaultBetLevel: toApi(opts.defaultBet ?? 1),\n ...(opts.rtp != null ? { rtp: opts.rtp } : {}),\n ...(opts.jurisdiction ? { jurisdiction: opts.jurisdiction } : {}),\n };\n this.active = opts.activeRound ?? null;\n this.modes = opts.modes ?? {};\n }\n\n /** Queue the next round `play()` will return (call repeatedly to queue several). */\n forceRound(round: ScriptedRound): this {\n this.queue.push(round);\n return this;\n }\n\n /** Set the current balance (MAJOR units). */\n setBalance(major: number): this {\n this.balanceApi = toApi(major);\n return this;\n }\n\n private balance(): Balance {\n return { amount: this.balanceApi, currency: this.currency };\n }\n\n async authenticate(): Promise<AuthenticateResponse> {\n return { balance: this.balance(), config: this.config, round: this.active };\n }\n\n async play(args: PlayArgs): Promise<PlayResponse> {\n const s = this.queue.shift() ?? {};\n const cost = this.modes[args.mode] ?? 1;\n const stakeApi = s.stake != null ? toApi(s.stake) : toApi(args.bet * cost);\n const betApi = toApi(args.bet); // win is relative to the BASE bet\n const pm = s.payoutMultiplier ?? (s.win != null && args.bet > 0 ? Math.round((s.win / args.bet) * 100) : 0);\n const winApi = Math.round((betApi * pm) / 100);\n this.balanceApi -= stakeApi;\n const round: Round = {\n betID: `mock-${Date.now()}`,\n mode: s.mode ?? args.mode,\n amount: stakeApi,\n payout: winApi,\n payoutMultiplier: pm,\n state: (s.events ?? []) as Round['state'],\n // Default: settle the win inline (balance returned by play is final). Script\n // `active: true` to leave it open + exercise the engine's end-round settle path.\n active: s.active ?? false,\n };\n if (round.active) this.pendingWinApi = winApi; // credited on end-round\n else this.balanceApi += winApi; // settled inline\n return { round, balance: this.balance() };\n }\n\n async endRound(): Promise<EndRoundResponse> {\n this.balanceApi += this.pendingWinApi;\n this.pendingWinApi = 0;\n this.active = null;\n return { balance: this.balance() };\n }\n\n async replay(_p: ReplayParams): Promise<Round> {\n const s = this.queue.shift() ?? { payoutMultiplier: 0 };\n return {\n betID: 'replay',\n mode: s.mode ?? 'base',\n amount: toApi(s.stake ?? 1),\n payout: 0,\n payoutMultiplier: s.payoutMultiplier ?? 0,\n state: (s.events ?? []) as Round['state'],\n active: false,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,wBAAwB;AACrC,MAAa,yBAAyB;;AA2DtC,SAAgB,YAAe,OAAsB;CACnD,IAAI,MAAM,QAAQ,MAAM,KAAK,GAAG,OAAO,MAAM;CAC7C,IAAI,MAAM,QAAQ,MAAM,MAAM,GAAG,OAAO,MAAM;CAC9C,OAAO,CAAC;AACV;;;AC9BA,MAAM,SAAS,UAA0B,KAAK,MAAM,QAAQ,qBAAqB;AAEjF,IAAa,qBAAb,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA,QAA0C,CAAC;;CAE3C,gBAAwB;CAExB,YAAY,OAAoB,CAAC,GAAG;EAClC,KAAK,aAAa,MAAM,KAAK,WAAW,GAAI;EAC5C,KAAK,WAAW,KAAK,YAAY;EACjC,MAAM,UAAU,KAAK,aAAa;GAAC;GAAK;GAAK;GAAG;GAAG;GAAG;EAAE,EAAA,CAAG,IAAI,KAAK;EACpE,KAAK,SAAS;GACZ,QAAQ,OAAO;GACf,QAAQ,OAAO,OAAO,SAAS;GAC/B,SAAS,OAAO;GAChB,WAAW;GACX,iBAAiB,MAAM,KAAK,cAAc,CAAC;GAC3C,GAAI,KAAK,OAAO,OAAO,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GAC5C,GAAI,KAAK,eAAe,EAAE,cAAc,KAAK,aAAa,IAAI,CAAC;EACjE;EACA,KAAK,SAAS,KAAK,eAAe;EAClC,KAAK,QAAQ,KAAK,SAAS,CAAC;CAC9B;;CAGA,WAAW,OAA4B;EACrC,KAAK,MAAM,KAAK,KAAK;EACrB,OAAO;CACT;;CAGA,WAAW,OAAqB;EAC9B,KAAK,aAAa,MAAM,KAAK;EAC7B,OAAO;CACT;CAEA,UAA2B;EACzB,OAAO;GAAE,QAAQ,KAAK;GAAY,UAAU,KAAK;EAAS;CAC5D;CAEA,MAAM,eAA8C;EAClD,OAAO;GAAE,SAAS,KAAK,QAAQ;GAAG,QAAQ,KAAK;GAAQ,OAAO,KAAK;EAAO;CAC5E;CAEA,MAAM,KAAK,MAAuC;EAChD,MAAM,IAAI,KAAK,MAAM,MAAM,KAAK,CAAC;EACjC,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS;EACtC,MAAM,WAAW,EAAE,SAAS,OAAO,MAAM,EAAE,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI;EACzE,MAAM,SAAS,MAAM,KAAK,GAAG;EAC7B,MAAM,KAAK,EAAE,qBAAqB,EAAE,OAAO,QAAQ,KAAK,MAAM,IAAI,KAAK,MAAO,EAAE,MAAM,KAAK,MAAO,GAAG,IAAI;EACzG,MAAM,SAAS,KAAK,MAAO,SAAS,KAAM,GAAG;EAC7C,KAAK,cAAc;EACnB,MAAM,QAAe;GACnB,OAAO,QAAQ,KAAK,IAAI;GACxB,MAAM,EAAE,QAAQ,KAAK;GACrB,QAAQ;GACR,QAAQ;GACR,kBAAkB;GAClB,OAAQ,EAAE,UAAU,CAAC;GAGrB,QAAQ,EAAE,UAAU;EACtB;EACA,IAAI,MAAM,QAAQ,KAAK,gBAAgB;OAClC,KAAK,cAAc;EACxB,OAAO;GAAE;GAAO,SAAS,KAAK,QAAQ;EAAE;CAC1C;CAEA,MAAM,WAAsC;EAC1C,KAAK,cAAc,KAAK;EACxB,KAAK,gBAAgB;EACrB,KAAK,SAAS;EACd,OAAO,EAAE,SAAS,KAAK,QAAQ,EAAE;CACnC;CAEA,MAAM,OAAO,IAAkC;EAC7C,MAAM,IAAI,KAAK,MAAM,MAAM,KAAK,EAAE,kBAAkB,EAAE;EACtD,OAAO;GACL,OAAO;GACP,MAAM,EAAE,QAAQ;GAChB,QAAQ,MAAM,EAAE,SAAS,CAAC;GAC1B,QAAQ;GACR,kBAAkB,EAAE,oBAAoB;GACxC,OAAQ,EAAE,UAAU,CAAC;GACrB,QAAQ;EACV;CACF;AACF"}
@@ -0,0 +1,2 @@
1
+ import { a as SfxBus, c as createGameAudio, d as bindMixerToHud, i as MusicBus, l as MixerGroup, n as GameAudio, o as SoundEntry, r as GameAudioOptions, s as bindAudioToHud, t as BusName, u as MixerLike } from "./index-BcTRfis9.js";
2
+ export { BusName, GameAudio, GameAudioOptions, type MixerGroup, type MixerLike, MusicBus, SfxBus, SoundEntry, bindAudioToHud, bindMixerToHud, createGameAudio };
package/dist/audio.js ADDED
@@ -0,0 +1,112 @@
1
+ import { t as bindMixerToHud } from "./bind-BBRxizWR.js";
2
+ import { Ducker, createEngine } from "@schmooky/zvuk";
3
+ //#region src/audio/index.ts
4
+ const MUSIC_BUSES = ["music", "ambience"];
5
+ const SFX_BUSES = [
6
+ "reels",
7
+ "symbols",
8
+ "anticipation",
9
+ "wins",
10
+ "voiceover",
11
+ "ui",
12
+ "reverb"
13
+ ];
14
+ /** The pre-wired game mixer. Satisfies {@link AudioPort} + {@link MixerLike}. */
15
+ var GameAudio = class {
16
+ engine;
17
+ musicGroup;
18
+ effectsGroup;
19
+ duckFrom;
20
+ duckAmount;
21
+ unlocked = false;
22
+ currentMusic = null;
23
+ constructor(opts = {}) {
24
+ const musicLevel = opts.music ?? .8;
25
+ const fxLevel = opts.effects ?? 1;
26
+ const buses = {};
27
+ for (const b of MUSIC_BUSES) buses[b] = { level: musicLevel };
28
+ for (const b of SFX_BUSES) buses[b] = { level: fxLevel };
29
+ this.engine = createEngine({
30
+ buses,
31
+ master: {
32
+ headroom: opts.masterHeadroom ?? -3,
33
+ limiter: { threshold: -1 }
34
+ }
35
+ });
36
+ this.musicGroup = this.engine.busGroup("music", MUSIC_BUSES.map((b) => this.engine.bus(b)));
37
+ this.effectsGroup = this.engine.busGroup("effects", SFX_BUSES.map((b) => this.engine.bus(b)));
38
+ const df = opts.duckMusicFrom === void 0 ? ["wins"] : opts.duckMusicFrom;
39
+ this.duckFrom = df == null ? [] : Array.isArray(df) ? df : [df];
40
+ this.duckAmount = opts.duckAmount ?? .5;
41
+ }
42
+ /** Resume the AudioContext from a user gesture + arm ducking. Idempotent. */
43
+ async unlock() {
44
+ if (this.unlocked) return;
45
+ await this.engine.unlock();
46
+ this.unlocked = true;
47
+ for (const src of this.duckFrom) for (const target of MUSIC_BUSES) {
48
+ const ducker = new Ducker(this.engine.context, this.engine.bus(src), {
49
+ amount: this.duckAmount,
50
+ attack: .05,
51
+ release: .3
52
+ });
53
+ this.engine.bus(target).addFx(ducker);
54
+ }
55
+ }
56
+ setGroupLevel(group, level) {
57
+ (group === "music" ? this.musicGroup : this.effectsGroup).level = level;
58
+ }
59
+ getGroupLevel(group) {
60
+ return (group === "music" ? this.musicGroup : this.effectsGroup).level;
61
+ }
62
+ setMuted(muted) {
63
+ this.musicGroup.muted = muted;
64
+ this.effectsGroup.muted = muted;
65
+ }
66
+ /** A single bus (for per-sound routing, sends, FX inserts). */
67
+ bus(name) {
68
+ return this.engine.bus(name);
69
+ }
70
+ /** A volume group handle (`'music'` or `'effects'`). */
71
+ group(name) {
72
+ return name === "music" ? this.musicGroup : this.effectsGroup;
73
+ }
74
+ /** Preload a manifest of sounds + music onto their buses. */
75
+ async load(entries) {
76
+ await Promise.all(entries.map((e) => "kind" in e && e.kind === "music" ? this.engine.loadMusic(e.name, {
77
+ loop: e.loop,
78
+ ...e.intro ? { intro: e.intro } : {},
79
+ ...e.outro ? { outro: e.outro } : {}
80
+ }, e.loopCrossfadeMs != null ? { loopCrossfade: e.loopCrossfadeMs / 1e3 } : void 0) : this.engine.loadSound(e.name, e.url, { bus: e.bus ?? "ui" })));
81
+ }
82
+ play(name, opts) {
83
+ this.engine.sound(name).play({
84
+ ...opts?.bus ? { bus: opts.bus } : {},
85
+ ...opts?.volume != null ? { volume: opts.volume } : {}
86
+ });
87
+ }
88
+ music(name, opts) {
89
+ this.currentMusic?.stop({ fade: .3 });
90
+ this.currentMusic = this.engine.music(name).play({ fadeIn: opts?.fadeIn ?? .4 });
91
+ }
92
+ stopMusic(opts) {
93
+ this.currentMusic?.stop({ fade: opts?.fade ?? .3 });
94
+ this.currentMusic = null;
95
+ }
96
+ };
97
+ function createGameAudio(opts) {
98
+ return new GameAudio(opts);
99
+ }
100
+ /**
101
+ * Bind the HUD's sound controls to the audio groups (Music slider → music group, Effects →
102
+ * effects group, persisted; Sound toggle → mute). Returns a disposer. `createStakeGame` calls
103
+ * this for you (via {@link bindMixerToHud}) when you pass `audio`; use it directly only to opt
104
+ * out or wire a mixer the core didn't get.
105
+ */
106
+ function bindAudioToHud(audio, hud, opts = {}) {
107
+ return bindMixerToHud(audio, hud, opts);
108
+ }
109
+ //#endregion
110
+ export { GameAudio, bindAudioToHud, bindMixerToHud, createGameAudio };
111
+
112
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","names":[],"sources":["../src/audio/index.ts"],"sourcesContent":["// @stakeplate/core/audio — a thin PRE-WIRING over @schmooky/zvuk. The core stands up the\n// standard slot bus graph (nine buses in TWO groups) + master, binds the two groups to the\n// HUD's Music/Effects sliders (persisted) and mutes on the Sound toggle, and ducks music\n// while chosen sfx play. The game just declares sounds and calls `audio.play('win','wins')`.\n//\n// master\n// ├── MUSIC group (Music slider) → music · ambience\n// └── EFFECTS group (Effects slider) → reels · symbols · anticipation · wins ·\n// voiceover · ui · reverb\n//\n// Groups are zvuk BusGroups (a logical handle: a slider sets every member's level). Keep the\n// per-sound mix in the sound volumes; the sliders scale the whole group.\n\nimport { createEngine, Ducker, type Engine, type Bus, type BusConfig, type BusGroup } from '@schmooky/zvuk';\nimport type { BootedHud } from '@open-slot-ui/pixi';\nimport type { AudioPort } from '../engine/fsm';\nimport { bindMixerToHud, type MixerLike, type MixerGroup } from './bind';\n\nexport { bindMixerToHud, type MixerLike, type MixerGroup } from './bind';\n\n/** Buses in the MUSIC group (driven by the Music slider). */\nexport type MusicBus = 'music' | 'ambience';\n/** Buses in the EFFECTS group (driven by the Effects slider). */\nexport type SfxBus = 'reels' | 'symbols' | 'anticipation' | 'wins' | 'voiceover' | 'ui' | 'reverb';\nexport type BusName = MusicBus | SfxBus;\n\nconst MUSIC_BUSES: MusicBus[] = ['music', 'ambience'];\nconst SFX_BUSES: SfxBus[] = ['reels', 'symbols', 'anticipation', 'wins', 'voiceover', 'ui', 'reverb'];\n\nexport interface GameAudioOptions {\n /** Initial MUSIC-group level (music + ambience), 0..1. Default 0.8. */\n music?: number;\n /** Initial EFFECTS-group level (reels…reverb), 0..1. Default 1. */\n effects?: number;\n masterHeadroom?: number;\n /** Duck the MUSIC group while THESE sfx buses are active (default `['wins']`; `null` = off). */\n duckMusicFrom?: SfxBus | SfxBus[] | null;\n duckAmount?: number;\n}\n\n/**\n * A sound to load onto a bus, or a music track (intro/loop/outro → the `music` bus).\n * For a PLAIN loop with no authored intro/outro, set `loopCrossfadeMs`: zvuk equal-power\n * crossfades the loop boundary so a single file loops seamlessly (no click, no stinger/tail\n * needed) — just the crossfade window in ms.\n */\nexport type SoundEntry =\n | { name: string; kind?: 'sound'; url: string | string[]; bus?: BusName }\n | { name: string; kind: 'music'; loop: string | string[]; intro?: string | string[]; outro?: string | string[]; loopCrossfadeMs?: number };\n\n/** The pre-wired game mixer. Satisfies {@link AudioPort} + {@link MixerLike}. */\nexport class GameAudio implements AudioPort, MixerLike {\n readonly engine: Engine<BusName>;\n private readonly musicGroup: BusGroup;\n private readonly effectsGroup: BusGroup;\n private readonly duckFrom: SfxBus[];\n private readonly duckAmount: number;\n private unlocked = false;\n private currentMusic: { stop(opts?: { fade?: number }): void } | null = null;\n\n constructor(opts: GameAudioOptions = {}) {\n const musicLevel = opts.music ?? 0.8;\n const fxLevel = opts.effects ?? 1;\n const buses = {} as Record<BusName, BusConfig>;\n for (const b of MUSIC_BUSES) buses[b] = { level: musicLevel };\n for (const b of SFX_BUSES) buses[b] = { level: fxLevel };\n this.engine = createEngine<BusName>({\n buses,\n master: { headroom: opts.masterHeadroom ?? -3, limiter: { threshold: -1 } },\n });\n this.musicGroup = this.engine.busGroup('music', MUSIC_BUSES.map((b) => this.engine.bus(b)));\n this.effectsGroup = this.engine.busGroup('effects', SFX_BUSES.map((b) => this.engine.bus(b)));\n const df = opts.duckMusicFrom === undefined ? (['wins'] as SfxBus[]) : opts.duckMusicFrom;\n this.duckFrom = df == null ? [] : Array.isArray(df) ? df : [df];\n this.duckAmount = opts.duckAmount ?? 0.5;\n }\n\n /** Resume the AudioContext from a user gesture + arm ducking. Idempotent. */\n async unlock(): Promise<void> {\n if (this.unlocked) return;\n await this.engine.unlock();\n this.unlocked = true;\n // Duck music + ambience while each chosen sfx bus is active.\n for (const src of this.duckFrom) {\n for (const target of MUSIC_BUSES) {\n const ducker = new Ducker(this.engine.context, this.engine.bus(src), { amount: this.duckAmount, attack: 0.05, release: 0.3 });\n this.engine.bus(target).addFx(ducker);\n }\n }\n }\n\n // ── MixerLike (the HUD sliders + mute drive these) ────────────────────────────\n setGroupLevel(group: MixerGroup, level: number): void {\n (group === 'music' ? this.musicGroup : this.effectsGroup).level = level;\n }\n getGroupLevel(group: MixerGroup): number {\n return (group === 'music' ? this.musicGroup : this.effectsGroup).level;\n }\n setMuted(muted: boolean): void {\n this.musicGroup.muted = muted;\n this.effectsGroup.muted = muted;\n }\n\n /** A single bus (for per-sound routing, sends, FX inserts). */\n bus(name: BusName): Bus {\n return this.engine.bus(name);\n }\n /** A volume group handle (`'music'` or `'effects'`). */\n group(name: MixerGroup): BusGroup {\n return name === 'music' ? this.musicGroup : this.effectsGroup;\n }\n\n /** Preload a manifest of sounds + music onto their buses. */\n async load(entries: SoundEntry[]): Promise<void> {\n await Promise.all(\n entries.map((e) =>\n 'kind' in e && e.kind === 'music'\n ? this.engine.loadMusic(\n e.name,\n { loop: e.loop, ...(e.intro ? { intro: e.intro } : {}), ...(e.outro ? { outro: e.outro } : {}) },\n e.loopCrossfadeMs != null ? { loopCrossfade: e.loopCrossfadeMs / 1000 } : undefined,\n )\n : this.engine.loadSound(e.name, e.url, { bus: e.bus ?? 'ui' }),\n ),\n );\n }\n\n play(name: string, opts?: { bus?: BusName; volume?: number }): void {\n this.engine.sound(name).play({ ...(opts?.bus ? { bus: opts.bus } : {}), ...(opts?.volume != null ? { volume: opts.volume } : {}) });\n }\n\n music(name: string, opts?: { fadeIn?: number }): void {\n this.currentMusic?.stop({ fade: 0.3 });\n this.currentMusic = this.engine.music(name).play({ fadeIn: opts?.fadeIn ?? 0.4 });\n }\n\n stopMusic(opts?: { fade?: number }): void {\n this.currentMusic?.stop({ fade: opts?.fade ?? 0.3 });\n this.currentMusic = null;\n }\n}\n\nexport function createGameAudio(opts?: GameAudioOptions): GameAudio {\n return new GameAudio(opts);\n}\n\n/**\n * Bind the HUD's sound controls to the audio groups (Music slider → music group, Effects →\n * effects group, persisted; Sound toggle → mute). Returns a disposer. `createStakeGame` calls\n * this for you (via {@link bindMixerToHud}) when you pass `audio`; use it directly only to opt\n * out or wire a mixer the core didn't get.\n */\nexport function bindAudioToHud(audio: GameAudio, hud: BootedHud, opts: { storageKey?: string } = {}): () => void {\n return bindMixerToHud(audio, hud, opts);\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ //#region src/audio/bind.ts
2
+ /**
3
+ * Bind the HUD sound controls to a mixer: the **Music slider → `music` group**, the
4
+ * **Effects slider → `effects` group** (both persisted to localStorage + restored), and the
5
+ * **Sound toggle → mute** everything. Returns a disposer.
6
+ */
7
+ function bindMixerToHud(mixer, hud, opts = {}) {
8
+ const key = opts.storageKey ?? "stakeplate.mixer";
9
+ const save = () => {
10
+ try {
11
+ localStorage.setItem(key, JSON.stringify({
12
+ music: mixer.getGroupLevel("music"),
13
+ effects: mixer.getGroupLevel("effects")
14
+ }));
15
+ } catch {}
16
+ };
17
+ try {
18
+ const saved = JSON.parse(localStorage.getItem(key) || "{}");
19
+ if (typeof saved.music === "number") mixer.setGroupLevel("music", saved.music);
20
+ if (typeof saved.effects === "number") mixer.setGroupLevel("effects", saved.effects);
21
+ } catch {}
22
+ const disposers = [];
23
+ disposers.push(hud.on("valueChanged", (p) => {
24
+ const v = p;
25
+ if (typeof v?.value !== "number") return;
26
+ if (v.id === "music") {
27
+ mixer.setGroupLevel("music", v.value);
28
+ save();
29
+ }
30
+ if (v.id === "sfx") {
31
+ mixer.setGroupLevel("effects", v.value);
32
+ save();
33
+ }
34
+ }));
35
+ disposers.push(hud.ui.muted.subscribe((m) => mixer.setMuted(m)));
36
+ return () => {
37
+ for (const d of disposers.splice(0)) d();
38
+ };
39
+ }
40
+ //#endregion
41
+ export { bindMixerToHud as t };
42
+
43
+ //# sourceMappingURL=bind-BBRxizWR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-BBRxizWR.js","names":[],"sources":["../src/audio/bind.ts"],"sourcesContent":["// The mixer↔HUD binding, with NO @schmooky/zvuk dependency — so `createStakeGame` can\n// auto-wire audio without pulling zvuk into every game's bundle. It talks to the mixer\n// through a minimal structural port (`MixerLike`), which @stakeplate/core/audio's\n// `GameAudio` satisfies. `@open-slot-ui/pixi` is imported type-only (erased at build).\n\nimport type { BootedHud } from '@open-slot-ui/pixi';\n\n/** The two volume groups the HUD's two sliders drive. */\nexport type MixerGroup = 'music' | 'effects';\n\n/**\n * The minimal mixer surface the HUD binding needs. The HUD has exactly two sliders —\n * **Music** and **Effects** — so the port is group-level, not per-bus. GameAudio satisfies\n * it structurally.\n */\nexport interface MixerLike {\n /** Set a group's level (0..1) — applies to every bus in the group. */\n setGroupLevel(group: MixerGroup, level: number): void;\n /** Read a group's level (average across its buses). */\n getGroupLevel(group: MixerGroup): number;\n /** Mute/unmute everything (the Sound toggle). */\n setMuted(muted: boolean): void;\n /** Resume the AudioContext from a user gesture. Present on GameAudio. */\n unlock?(): Promise<void> | void;\n}\n\n/**\n * Bind the HUD sound controls to a mixer: the **Music slider → `music` group**, the\n * **Effects slider → `effects` group** (both persisted to localStorage + restored), and the\n * **Sound toggle → mute** everything. Returns a disposer.\n */\nexport function bindMixerToHud(mixer: MixerLike, hud: BootedHud, opts: { storageKey?: string } = {}): () => void {\n const key = opts.storageKey ?? 'stakeplate.mixer';\n const save = (): void => {\n try {\n localStorage.setItem(key, JSON.stringify({ music: mixer.getGroupLevel('music'), effects: mixer.getGroupLevel('effects') }));\n } catch {\n /* storage may be unavailable */\n }\n };\n try {\n const saved = JSON.parse(localStorage.getItem(key) || '{}') as { music?: number; effects?: number };\n if (typeof saved.music === 'number') mixer.setGroupLevel('music', saved.music);\n if (typeof saved.effects === 'number') mixer.setGroupLevel('effects', saved.effects);\n } catch {\n /* ignore */\n }\n\n const disposers: Array<() => void> = [];\n disposers.push(\n hud.on('valueChanged', (p) => {\n const v = p as { id?: string; value?: number };\n if (typeof v?.value !== 'number') return;\n // HUD control ids: 'music' → music group, 'sfx' (the Effects slider) → effects group.\n if (v.id === 'music') { mixer.setGroupLevel('music', v.value); save(); }\n if (v.id === 'sfx') { mixer.setGroupLevel('effects', v.value); save(); }\n }),\n );\n disposers.push(hud.ui.muted.subscribe((m: boolean) => mixer.setMuted(m)));\n return () => { for (const d of disposers.splice(0)) d(); };\n}\n"],"mappings":";;;;;;AA+BA,SAAgB,eAAe,OAAkB,KAAgB,OAAgC,CAAC,GAAe;CAC/G,MAAM,MAAM,KAAK,cAAc;CAC/B,MAAM,aAAmB;EACvB,IAAI;GACF,aAAa,QAAQ,KAAK,KAAK,UAAU;IAAE,OAAO,MAAM,cAAc,OAAO;IAAG,SAAS,MAAM,cAAc,SAAS;GAAE,CAAC,CAAC;EAC5H,QAAQ,CAER;CACF;CACA,IAAI;EACF,MAAM,QAAQ,KAAK,MAAM,aAAa,QAAQ,GAAG,KAAK,IAAI;EAC1D,IAAI,OAAO,MAAM,UAAU,UAAU,MAAM,cAAc,SAAS,MAAM,KAAK;EAC7E,IAAI,OAAO,MAAM,YAAY,UAAU,MAAM,cAAc,WAAW,MAAM,OAAO;CACrF,QAAQ,CAER;CAEA,MAAM,YAA+B,CAAC;CACtC,UAAU,KACR,IAAI,GAAG,iBAAiB,MAAM;EAC5B,MAAM,IAAI;EACV,IAAI,OAAO,GAAG,UAAU,UAAU;EAElC,IAAI,EAAE,OAAO,SAAS;GAAE,MAAM,cAAc,SAAS,EAAE,KAAK;GAAG,KAAK;EAAG;EACvE,IAAI,EAAE,OAAO,OAAO;GAAE,MAAM,cAAc,WAAW,EAAE,KAAK;GAAG,KAAK;EAAG;CACzE,CAAC,CACH;CACA,UAAU,KAAK,IAAI,GAAG,MAAM,WAAW,MAAe,MAAM,SAAS,CAAC,CAAC,CAAC;CACxE,aAAa;EAAE,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC,GAAG,EAAE;CAAG;AAC3D"}
@@ -0,0 +1,49 @@
1
+ //#region src/engine/ticker.d.ts
2
+ interface Ticker {
3
+ /** Seconds since some epoch (monotonic-ish). */
4
+ now(): number;
5
+ /** Resolve after `ms` milliseconds. */
6
+ delay(ms: number): Promise<void>;
7
+ }
8
+ /** Real timer — `setTimeout`-backed, `performance.now()` clock. */
9
+ declare class RealTicker implements Ticker {
10
+ now(): number;
11
+ delay(ms: number): Promise<void>;
12
+ }
13
+ /** Instant ticker for tests — every delay resolves immediately; the clock advances by
14
+ * the requested amount so `now()` deltas + `minimumRoundDuration` maths stay sane. */
15
+ declare class InstantTicker implements Ticker {
16
+ private t;
17
+ now(): number;
18
+ delay(ms: number): Promise<void>;
19
+ }
20
+ //#endregion
21
+ //#region src/engine/hud-port.d.ts
22
+ /** Facts shown at the start/end of a replay. Mirrors `@open-slot-ui`'s `ReplayInfo`
23
+ * (currency kept opaque so the engine needn't import the HUD's `CurrencySpec`). */
24
+ interface ReplayInfo {
25
+ baseBet: number;
26
+ costMultiplier: number;
27
+ payoutMultiplier: number;
28
+ amount: number;
29
+ currency?: unknown;
30
+ }
31
+ interface HudPort {
32
+ setBalance(major: number): void;
33
+ setBet(major: number): void;
34
+ setTotalWin(major: number): void;
35
+ setFreeSpins(n: number): void;
36
+ /** Settle one round — updates net position + autoplay limits. */
37
+ reportRound(win: number, bet: number): void;
38
+ showRgsError(code: string): void;
39
+ showError(message: string): void;
40
+ setReplay(on: boolean): void;
41
+ replayStart(info: ReplayInfo, onPlay?: () => void): void;
42
+ replayEnd(info: ReplayInfo, onReplay: () => void): void;
43
+ lockInput(): void;
44
+ unlockInput(): void;
45
+ on(type: string, fn: (payload: unknown) => void): () => void;
46
+ }
47
+ //#endregion
48
+ export { Ticker as a, RealTicker as i, ReplayInfo as n, InstantTicker as r, HudPort as t };
49
+ //# sourceMappingURL=hud-port-DNfotn4U.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { c as JurisdictionConfig } from "./protocol-C1KamF3t.js";
2
+ //#region src/stores/index.d.ts
3
+ /** Player funds + the current bet. Mutated only through actions. */
4
+ declare class BalanceStore {
5
+ balance: number;
6
+ /** Base bet (per line/spin), before any mode cost multiplier. */
7
+ bet: number;
8
+ lastWin: number;
9
+ constructor();
10
+ setBalance(v: number): void;
11
+ setBet(v: number): void;
12
+ /** Take the stake at spin press (win is credited on settle). */
13
+ debitStake(stake: number): void;
14
+ /** Undo a debit (the round never happened). */
15
+ refund(stake: number): void;
16
+ /** Record + credit a settled win. */
17
+ credit(win: number): void;
18
+ /** Apply the AUTHORITATIVE post-round balance + record the win (server-settled). */
19
+ settle(balance: number, win: number): void;
20
+ }
21
+ /** Session facts from `/wallet/authenticate` (read-mostly). */
22
+ declare class SessionStore {
23
+ sessionId: string;
24
+ currency: string;
25
+ rtp: number;
26
+ availableBets: number[];
27
+ jurisdiction: JurisdictionConfig;
28
+ constructor();
29
+ set(patch: Partial<SessionStore>): void;
30
+ }
31
+ /** Player-facing UI/session state. Games extend with their own store for feature flags. */
32
+ declare class UiStore {
33
+ spinning: boolean;
34
+ /** Free spins remaining (0 = normal play). */
35
+ freeSpins: number;
36
+ /** A one-shot mode for the next spin (e.g. a bought bonus) — wins over `activeMode`. */
37
+ oneShotMode: string | null;
38
+ /** The persistent active mode (e.g. a toggled boost). */
39
+ activeMode: string | null;
40
+ /** Replay mode (Stake `replay=true`). */
41
+ replay: boolean;
42
+ constructor();
43
+ setSpinning(v: boolean): void;
44
+ setFreeSpins(n: number): void;
45
+ setOneShotMode(m: string | null): void;
46
+ setActiveMode(m: string | null): void;
47
+ /** The mode for the next spin: one-shot wins, then the active mode, then 'base'. */
48
+ nextMode(): string;
49
+ }
50
+ /** Aggregates the base stores. A game may compose this with its own stores. */
51
+ declare class RootStore {
52
+ readonly balance: BalanceStore;
53
+ readonly session: SessionStore;
54
+ readonly ui: UiStore;
55
+ }
56
+ //#endregion
57
+ export { UiStore as i, RootStore as n, SessionStore as r, BalanceStore as t };
58
+ //# sourceMappingURL=index-BBlN97RY.d.ts.map