@stakeplate/core 0.7.0 → 0.7.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.
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -793,6 +793,7 @@ function createStakeGame(opts) {
|
|
|
793
793
|
...extra?.hideBalance ? { balance: { hidden: true } } : {}
|
|
794
794
|
},
|
|
795
795
|
buyFeature: { confirmAboveCost: s.confirmBuyAboveCost },
|
|
796
|
+
lockDuringSpin: false,
|
|
796
797
|
...opts.config.rules ? { menu: opts.config.rules } : {},
|
|
797
798
|
locale: {
|
|
798
799
|
locale: runtime.language,
|
|
@@ -948,8 +949,22 @@ function createStakeGame(opts) {
|
|
|
948
949
|
loader?.setProgress(.95);
|
|
949
950
|
disposers.push(reaction(() => stores.balance.balance, (b) => hud.setBalance(b), { fireImmediately: false }));
|
|
950
951
|
disposers.push(reaction(() => stores.ui.spinning, (sp) => {
|
|
951
|
-
|
|
952
|
-
|
|
952
|
+
const u = hud.ui;
|
|
953
|
+
if (sp) {
|
|
954
|
+
u.spin.busy();
|
|
955
|
+
if (!u.autoplay.isActive) u.autoplay.disable();
|
|
956
|
+
u.bonusButton.disable();
|
|
957
|
+
u.settingsButton.disable();
|
|
958
|
+
u.betPlus.disable();
|
|
959
|
+
u.betMinus.disable();
|
|
960
|
+
} else {
|
|
961
|
+
u.spin.idle();
|
|
962
|
+
u.autoplay.enable();
|
|
963
|
+
u.bonusButton.enable();
|
|
964
|
+
u.settingsButton.enable();
|
|
965
|
+
u.betStepper.canInc ? u.betPlus.enable() : u.betPlus.disable();
|
|
966
|
+
u.betStepper.canDec ? u.betMinus.enable() : u.betMinus.disable();
|
|
967
|
+
}
|
|
953
968
|
}, { fireImmediately: false }));
|
|
954
969
|
disposers.push(hud.on("valueChanged", (p) => {
|
|
955
970
|
const v = p;
|
|
@@ -1051,6 +1066,26 @@ function createStakeGame(opts) {
|
|
|
1051
1066
|
turbo.setLevel(prefs.turbo);
|
|
1052
1067
|
}
|
|
1053
1068
|
disposers.push(hud.ui.muted.subscribe((m) => writePrefs({ muted: m })));
|
|
1069
|
+
const speedSwitch = hud.ui.control("speed");
|
|
1070
|
+
const soundSwitch = hud.ui.control("sound");
|
|
1071
|
+
if (speedSwitch) {
|
|
1072
|
+
speedSwitch.set(hud.ui.turbo.isOn);
|
|
1073
|
+
disposers.push(hud.ui.turbo.index.subscribe(() => speedSwitch.set(hud.ui.turbo.isOn)));
|
|
1074
|
+
}
|
|
1075
|
+
if (soundSwitch) {
|
|
1076
|
+
soundSwitch.set(!hud.ui.muted.get());
|
|
1077
|
+
disposers.push(hud.ui.muted.subscribe((m) => soundSwitch.set(!m)));
|
|
1078
|
+
}
|
|
1079
|
+
disposers.push(hud.on("toggled", (p) => {
|
|
1080
|
+
const e = p;
|
|
1081
|
+
if (typeof e.on !== "boolean") return;
|
|
1082
|
+
if (e.id === "speed") {
|
|
1083
|
+
const idx = e.on ? 1 : 0;
|
|
1084
|
+
hud.ui.turbo.setIndex(idx);
|
|
1085
|
+
turbo.setLevel(idx);
|
|
1086
|
+
writePrefs({ turbo: idx });
|
|
1087
|
+
} else if (e.id === "sound") hud.setMuted(!e.on);
|
|
1088
|
+
}));
|
|
1054
1089
|
if (active?.active) {
|
|
1055
1090
|
const end = await network.endRound();
|
|
1056
1091
|
const raw = active;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/engine/turbo.ts","../src/engine/round.ts","../src/engine/fsm.ts","../src/engine/phases.ts","../src/engine/beat.ts","../src/game/config.ts","../src/currency/index.ts","../src/loader/index.ts","../src/game/createStakeGame.ts"],"sourcesContent":["// Turbo speed + slam-stop, owned by the core. The HUD's turbo control (a 2- or 3-mode cycler)\n// drives a speed multiplier; the game's Present phase awaits `ctx.turbo.delay(ms)` for its\n// spin/anim durations, so turbo shortens them and a slam-stop (`skipRequested`) resolves them\n// instantly — with ZERO game logic. Compliance waits (minimumRoundDuration) use `ctx.ticker`,\n// NOT this, so they're never sped up or skipped.\n\n/** Delay multipliers per turbo level: off (1×) / turbo (0.4×) / super (0.12×). */\nexport const DEFAULT_TURBO_SPEEDS = [1, 0.4, 0.12];\n\nexport interface TurboState {\n /** 0 = off, 1 = turbo, 2 = super — the HUD turbo cycler's index. */\n readonly level: number;\n /** The EFFECTIVE delay multiplier right now — the turbo level, floored by the autoplay\n * speed while autoplay is running (`ctx.turbo.delay` already applies it). */\n readonly speed: number;\n /** True while autoplay/hold is running — game animations are shortened even at turbo off. */\n readonly autoplay: boolean;\n /** True once the player slam-stopped this round (delays resolve instantly). */\n readonly skipped: boolean;\n /** A turbo- + autoplay- + slam-stop-aware delay for GAME animations. NOT for compliance waits. */\n delay(ms: number): Promise<void>;\n}\n\nexport class TurboClock implements TurboState {\n private readonly speeds: number[];\n /** Delay multiplier applied while autoplay/hold runs (the floor for `speed`). Defaults to\n * the turbo (level-1) speed, so autoplay plays at least at turbo pace even at turbo off. */\n private readonly autoplaySpeed: number;\n private _level = 0;\n private _autoplay = false;\n private _skipped = false;\n private readonly pending = new Set<() => void>();\n\n constructor(speeds?: number[], autoplaySpeed?: number) {\n this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;\n this.autoplaySpeed = autoplaySpeed ?? this.speeds[1] ?? 0.5;\n }\n\n get level(): number {\n return this._level;\n }\n get speed(): number {\n const levelSpeed = this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;\n // Autoplay shortens animations even at turbo off — but never SLOWS a faster turbo level.\n return this._autoplay ? Math.min(levelSpeed, this.autoplaySpeed) : levelSpeed;\n }\n get autoplay(): boolean {\n return this._autoplay;\n }\n get skipped(): boolean {\n return this._skipped;\n }\n\n /** Set the turbo level (clamped to the configured speeds). */\n setLevel(index: number): void {\n const i = Number.isFinite(index) ? Math.trunc(index) : 0;\n this._level = Math.max(0, Math.min(i, this.speeds.length - 1));\n }\n\n /** Mark autoplay/hold as running so `delay()` shortens game animations. The core sets\n * this on autoplayStarted/holdSpinStarted and clears it when they stop. */\n setAutoplay(active: boolean): void {\n this._autoplay = active;\n }\n\n /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */\n skip(): void {\n this._skipped = true;\n const cbs = [...this.pending];\n this.pending.clear();\n for (const cb of cbs) cb();\n }\n\n /** Clear the slam-stop flag — the core calls this at the start of each spin. */\n resetSkip(): void {\n this._skipped = false;\n }\n\n delay(ms: number): Promise<void> {\n if (this._skipped || ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n this.pending.delete(finish);\n resolve();\n };\n const timer = setTimeout(finish, ms * this.speed);\n this.pending.add(finish);\n });\n }\n}\n","// The round model the engine drives. The engine derives all MONEY (multiplier, win,\n// stake) from the raw wire round + the bet; the game's `interpretBook` only parses the\n// book EVENTS into its own model (`data`) — the one place a game touches a round.\n\nimport { API_AMOUNT_MULTIPLIER, BOOK_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\n\n/** Money facts of a settled round (all MAJOR units except `multiplier`, a ratio). */\nexport interface RoundInfo {\n mode: string;\n /** Base bet (before the mode's cost multiplier). */\n bet: number;\n /** Mode cost multiplier (× base bet) — the stake charged. */\n cost: number;\n /** The amount staked = bet × cost. */\n stake: number;\n /** Round multiplier relative to the BASE bet (totalWin / bet). */\n multiplier: number;\n /** Total win credited this round (derived: multiplier × bet). */\n totalWin: number;\n /** The server's AUTHORITATIVE win (`raw.payout`, major units); falls back to totalWin\n * when the round carries no explicit payout. Trust this over `totalWin` when they differ. */\n payout: number;\n}\n\n/**\n * A resolved round: money facts + the game's parsed model + the raw wire round. `E` is the\n * game's book-event type — declared once on `createStakeGame`, so `raw` (and `interpretBook`)\n * are TYPED rather than `unknown`.\n */\nexport interface GameRound<T = unknown, E = unknown> extends RoundInfo {\n /** The game's parsed model, from `interpretBook` — driven to the Present phase. */\n data: T;\n /** Whether the raw round still needs `/wallet/end-round` (the engine settles it). */\n active: boolean;\n /** Authoritative post-settlement balance (major units) — applied by the Settle phase. */\n balance: number;\n /** The raw, fully-typed wire round — the game's real server data. */\n raw: Round<E>;\n}\n\n/** The game's ONE money-logic seam: parse the raw round's (typed) events into your model. Pure. */\nexport type InterpretBook<T, E = unknown> = (raw: Round<E>, info: RoundInfo) => T;\n\n/** Build the money facts from a raw wire round + the base bet + the mode cost. */\nexport function roundInfo<E = unknown>(raw: Round<E>, bet: number, cost: number): RoundInfo {\n // Stake wire convention: `payoutMultiplier` is a plain Payout/Amount ratio (1× → 1)\n // in BOOK units (×100). totalWin is relative to the BASE bet; `payout` is the server's\n // own win amount (API units → major) when the round carries one.\n const multiplier = raw.payoutMultiplier / BOOK_AMOUNT_MULTIPLIER;\n const totalWin = multiplier * bet;\n const payout = raw.payout != null ? raw.payout / API_AMOUNT_MULTIPLIER : totalWin;\n return { mode: raw.mode, bet, cost, stake: bet * cost, multiplier, totalWin, payout };\n}\n","// The round state machine. A round is an explicit sequence of awaited phases\n// (Idle → Spin → Present → Settle); each `enter` does its work + transitions on. The\n// core ships Idle/Spin/Settle; the game writes Present (and may override any). The FSM\n// + context are pure — the HUD/view/audio arrive via ports, so a whole round runs\n// headless (see the InstantTicker + MockNetworkManager).\n\nimport type { NetworkManager } from '../rgs/network';\nimport type { RootStore } from '../stores/index';\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\nimport type { HudPort } from './hud-port';\nimport type { GameRound, InterpretBook } from './round';\nimport type { GameConfig } from '../game/config';\n\n/** A fixed value or a per-voice random spread (`{ base, jitter }`) — structurally matches\n * zvuk's `VoiceJitter` without importing it, so the engine stays zvuk-free. */\nexport type AudioValue = number | { base?: number; jitter?: number };\n\n/** The minimal boot-loader surface a game's phases touch (satisfied by the core's\n * `GameLoader`) — kept structural so the engine needn't import the loader module. */\nexport interface LoaderPort {\n setProgress(p: number): void;\n done(): Promise<void>;\n}\n\n/** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */\nexport interface AudioPort {\n /** Fire a one-shot. `volume`/`pitch` accept jitter so repeated cues vary per voice. */\n play(name: string, opts?: { bus?: string; volume?: AudioValue; pitch?: AudioValue }): void;\n music(name: string, opts?: { fadeIn?: number }): void;\n stopMusic(opts?: { fade?: number }): void;\n}\n\n/** A view MAY implement `onSpinStart` to begin its spin animation the MOMENT a round starts\n * — BEFORE the network request — so the reels move during latency and land on the result in\n * Present. Core calls it once per round inside `SpinPhase`, so a game gets eager-spin by\n * adding one method to its scene; it NEVER overrides the network-sending phase. Purely\n * opt-in: a view without `onSpinStart` just spins when Present lands the result. */\nexport interface SpinStartView {\n onSpinStart?(): void;\n}\n\n/** Handed to every phase. `round` is set by SpinPhase and read by Present/Settle. `E` is\n * the game's book-event type, so `interpretBook`/`round.raw` are typed. */\nexport interface PhaseContext<T = unknown, V = unknown, E = unknown> {\n readonly config: GameConfig;\n readonly stores: RootStore;\n readonly network: NetworkManager;\n readonly hud: HudPort;\n readonly ticker: Ticker;\n /** The game's mounted view (scene/presenter/stores) — whatever `mountView` returned. */\n readonly view: V;\n readonly audio: AudioPort | null;\n /** Turbo speed + slam-stop (core-owned). Use `ctx.turbo.delay(ms)` for spin/anim timing. */\n readonly turbo: TurboState;\n /** The boot loader, if one was configured (else `null`) — drive progress / `done()`. */\n readonly loader: LoaderPort | null;\n readonly interpretBook: InterpretBook<T, E>;\n readonly fsm: FSM<T, V, E>;\n round: GameRound<T, E> | null;\n /** Cost multiplier for a mode key (from `config.modes`, default 1). */\n modeCost(mode: string): number;\n}\n\nexport interface Phase<T = unknown, V = unknown, E = unknown> {\n readonly name: string;\n enter(ctx: PhaseContext<T, V, E>): Promise<void> | void;\n exit?(ctx: PhaseContext<T, V, E>): void;\n}\n\nexport class FSM<T = unknown, V = unknown, E = unknown> {\n private ctx: PhaseContext<T, V, E> | null = null;\n private readonly byName = new Map<string, Phase<T, V, E>>();\n private _current = '';\n\n /** Later phases with the same `name` win — so a game's Present overrides the default. */\n constructor(phases: Phase<T, V, E>[]) {\n for (const p of phases) this.byName.set(p.name, p);\n }\n\n bind(ctx: PhaseContext<T, V, E>): void {\n this.ctx = ctx;\n }\n\n get current(): string {\n return this._current;\n }\n\n has(name: string): boolean {\n return this.byName.has(name);\n }\n\n async transition(name: string): Promise<void> {\n if (!this.ctx) throw new Error('[FSM] not bound to a context');\n const next = this.byName.get(name);\n if (!next) throw new Error(`[FSM] unknown phase: ${name}`);\n this.byName.get(this._current)?.exit?.(this.ctx);\n this._current = name;\n await next.enter(this.ctx);\n }\n}\n","// The default round phases (Idle → Spin → Present → Settle). The game writes Present\n// (animate the round) and may override any of these. Balance/bet flow through the\n// stores (createStakeGame reacts store→HUD); the phases call the HUD directly only for\n// reportRound + errors.\n\nimport { API_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\nimport { roundInfo } from './round';\nimport type { Phase, PhaseContext, SpinStartView } from './fsm';\n\n/** Wait for the next spin (the HUD's `spinRequested` drives the transition to Spin). */\nexport class IdlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'idle';\n enter(ctx: PhaseContext<T, V, E>): void {\n ctx.stores.ui.setSpinning(false);\n }\n}\n\n/** Pick the mode, take the stake, ask the RGS, parse the round, hand off to Present. */\nexport class SpinPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'spin';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const { stores, network, hud } = ctx;\n const mode = stores.ui.nextMode();\n const cost = ctx.modeCost(mode);\n const bet = stores.balance.bet;\n const stake = bet * cost;\n\n stores.ui.setSpinning(true);\n stores.balance.debitStake(stake); // stake leaves now; the win lands at Settle\n\n // Round-start seam (opt-in): let the view begin its spin animation NOW — before the\n // network round-trip — so the reels move during latency and land on the result in\n // Present. The game just implements `onSpinStart()` on its scene; it never overrides\n // this network-sending phase. See `SpinStartView`.\n (ctx.view as SpinStartView | null | undefined)?.onSpinStart?.();\n\n try {\n const play = await network.play({ bet, mode });\n // The transport is game-agnostic (events: unknown); the game declared `E`, so we\n // narrow here — the game owns the shape it parses in `interpretBook`.\n const raw = play.round as Round<E>;\n const info = roundInfo(raw, bet, cost);\n const data = ctx.interpretBook(raw, info);\n // DO NOT /wallet/end-round here. An ACTIVE round is left OPEN so a refresh DURING\n // Present (the game's reel/scene animation) can recover it — SettlePhase settles it\n // once the round has finished playing out. For an active round `play.balance` is\n // post-DEBIT (stake gone, win not yet credited), so the post-win balance the player\n // sees at Settle is play.balance + the (authoritative-from-book) win; SettlePhase then\n // reconciles to the server's own settled figure. A round that is already settled (a\n // loss, or an immediate-settle mock) reports its FINAL balance in play.balance — use\n // it verbatim, or the win would be double-counted.\n const settledMoney = play.balance.amount / API_AMOUNT_MULTIPLIER;\n ctx.round = {\n ...info,\n data,\n active: raw.active ?? false,\n balance: raw.active ? settledMoney + info.totalWin : settledMoney,\n raw,\n };\n } catch (err) {\n stores.balance.refund(stake); // the round never happened\n stores.ui.setSpinning(false);\n const msg = err instanceof Error ? err.message : String(err);\n const code = msg.match(/ERR_[A-Z_]+/)?.[0];\n if (code) hud.showRgsError(code);\n else hud.showError(msg);\n await ctx.fsm.transition('idle');\n return;\n }\n\n await ctx.fsm.transition('present');\n }\n}\n\n/** DEFAULT Present — nothing to animate. The game overrides `present` to play its scene. */\nexport class PresentPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'present';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n await ctx.fsm.transition('settle');\n }\n}\n\n/** Apply the authoritative balance + win, settle the round for the HUD, then Idle. */\nexport class SettlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'settle';\n private roundStartedAt = 0;\n\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const r = ctx.round;\n if (r) {\n ctx.stores.balance.settle(r.balance, r.totalWin);\n ctx.hud.reportRound(r.totalWin, r.bet);\n // minimumRoundDuration: pad to the jurisdiction minimum (best-effort; the boot\n // records the round start, so this covers the whole spin→settle span).\n const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;\n const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1000;\n if (minMs > 0 && elapsed < minMs) await ctx.ticker.delay(minMs - elapsed);\n // The round has now finished playing out visually (Present + the min-duration pad).\n // ONLY NOW settle it on the server — SpinPhase deliberately left it OPEN so a refresh\n // mid-Present recovers the round instead of losing it. The win already shows (from the\n // book); this reconciles to the server's authoritative post-settlement balance.\n if (r.active) {\n try {\n const end = await ctx.network.endRound();\n ctx.stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);\n } catch (err) {\n // The win is already credited locally; a settle hiccup self-heals on the next\n // authenticate (the RGS reports the still-open round). Surface it, don't crash.\n console.warn('[settle] end-round failed; balance reconciles on next authenticate:', err);\n }\n }\n }\n ctx.stores.ui.setSpinning(false);\n await ctx.fsm.transition('idle');\n }\n\n /** Called by SpinPhase-adjacent boot wiring to timestamp the round start. */\n markStart(now: number): void {\n this.roundStartedAt = now;\n }\n}\n\n/** The default phase set (game phases are appended after → same-name overrides win). */\nexport function defaultPhases<T = unknown, V = unknown, E = unknown>(): Phase<T, V, E>[] {\n return [new IdlePhase<T, V, E>(), new SpinPhase<T, V, E>(), new PresentPhase<T, V, E>(), new SettlePhase<T, V, E>()];\n}\n","// A blocking \"announcement beat\" — the pattern behind moments like a bonus trigger or a\n// \"… ELIMINATED!\" banner: pause so the reveal settles, SHOW the announcement, HOLD it\n// alone on screen, HIDE it, then a trailing pause before play resumes. The point is that\n// the round never plays UNDER the announcement — everything waits for the beat.\n//\n// The game owns the visuals (`show`/`hide` — mount a banner, toggle a class, play an sfx);\n// the core owns the timing + sequencing so every game's beats feel consistent.\n\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\n\nexport interface BeatOptions {\n /** Pause before the announcement shows — lets the reveal settle. Default 0. */\n leadMs?: number;\n /** How long the announcement holds alone on screen. */\n holdMs: number;\n /** Pause after hiding, before play resumes. Default 0. */\n trailMs?: number;\n /** Reveal the announcement (banner/sfx). Called after the lead pause. */\n show: () => void;\n /** Hide it. Called after the hold, before the trailing pause. Optional (some banners\n * self-dismiss). */\n hide?: () => void;\n /**\n * Timing source. `'real'` (default) is a fixed, unskippable beat via the ticker — an\n * announcement is a deliberate moment that a slam-stop shouldn't blow past. `'turbo'`\n * makes the pauses turbo-/autoplay-/slam-stop-aware (shorter under turbo, skippable).\n */\n timing?: 'real' | 'turbo';\n}\n\n/** Timing surface the beat needs — a `PhaseContext` satisfies it (`ctx.ticker`/`ctx.turbo`). */\nexport interface BeatClock {\n ticker: Ticker;\n turbo: TurboState;\n}\n\n/**\n * Run a blocking announcement beat: lead → show → hold → hide → trail. Awaitable, so a\n * Present phase just `await blockingBeat(ctx, { … })` and the round pauses for the whole\n * thing. Returns when the beat (including the trailing pause) is over.\n */\nexport async function blockingBeat(ctx: BeatClock, opts: BeatOptions): Promise<void> {\n const wait = opts.timing === 'turbo' ? (ms: number) => ctx.turbo.delay(ms) : (ms: number) => ctx.ticker.delay(ms);\n if (opts.leadMs && opts.leadMs > 0) await wait(opts.leadMs);\n opts.show();\n await wait(opts.holdMs);\n opts.hide?.();\n if (opts.trailMs && opts.trailMs > 0) await wait(opts.trailMs);\n}\n","// The per-game configuration passed to `createStakeGame`. Everything the core needs to\n// stand up the HUD + drive the round; the game's mechanics live in its scene + phases.\n\nexport interface ModeConfig {\n /** Cost multiplier (× base bet). `base` is 1. */\n cost: number;\n /** A one-shot bought bonus — shown as a `Buy` card in the buy-feature modal (opened by the\n * bonus button). Buying it spins this mode once (through the jurisdiction confirm gate). */\n buy?: boolean;\n /** An activatable per-spin bet surcharge — shown as an `Activate` card (vs a one-tap `Buy`). */\n boost?: boolean;\n /** Display name for the feature card (defaults to the capitalized mode key). */\n name?: string;\n /** Card art (URL or data URI) for the feature card. A neutral gradient is used when absent. */\n image?: string;\n}\n\nexport interface GameConfig {\n title: string;\n version?: string;\n /** Fallback currency code; the session/`?currency=` wins. */\n currency?: string;\n /**\n * Theoretical RTP percentage — DISPLAY ONLY (the RTP readout + rules). NOT authoritative:\n * the server (`auth.config.rtp`) wins, and the certified math report is the source of\n * truth. This is only a last-resort fallback; prefer never hand-typing it (drift risk).\n */\n rtp?: number;\n /** Mode key → cost multiplier (a number) or a `ModeConfig`. `base` defaults to 1. */\n modes?: Record<string, ModeConfig | number>;\n /**\n * Minimum selectable bet, in MAJOR units of the account currency — an EXPLICIT client\n * floor. Any server bet-ladder level below this is dropped from the HUD ladder, so the\n * smallest possible win (bet × the game's minimum payout) can never round below one\n * minimal currency unit. Set it per game to your smallest legal stake — e.g. `0.05` when\n * the minimum payout is ×0.2 (0.05 × 0.2 = 0.01 = one cent). `undefined` → derive it from\n * the currency (5 minimal units — a safe default for a ×0.2 minimum payout). The server\n * ladder stays authoritative for the LEVELS offered; this only trims the low end + snaps\n * the default up to the first legal level.\n */\n minBet?: number;\n /** Rules/info menu for the HUD (`@open-slot-ui` `MenuSpec`) — build it with `@stakeplate/core/rules` `buildRules`. */\n rules?: unknown;\n /** i18n messages per locale (`{ en: { key: text }, es: {…} }`) for the HUD + rules text. */\n messages?: Record<string, Record<string, string>>;\n /** SOCIAL/sweepstakes wording per locale — swapped in when social mode is on. Merge in\n * `buildRules().socialEn` so the core's disclaimer/guide are social-safe. */\n socialMessages?: Record<string, Record<string, string>>;\n /** Delay multipliers per turbo level (off / turbo / super). Default `[1, 0.4, 0.12]`. */\n turboSpeeds?: number[];\n /** Pause (ms) between autoplay/hold spins. Default 250. Scales with turbo. */\n autoplayGapMs?: number;\n /** Extra `@open-slot-ui` `UISpec` fields merged into the built spec (escape hatch). */\n spec?: Record<string, unknown>;\n}\n\n// NOTE (server-authoritative): the bet ladder (`betLevels` + `defaultBetLevel`) and the\n// buy-feature confirm threshold are NOT game config — the RGS `authenticate` response owns\n// the ladder (per currency/jurisdiction) and the jurisdiction owns the confirm policy. The\n// core reads them from `auth.config`; a game only declares that a mode is a buy (`modes`).\n\n/** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */\nexport function modeCostOf(config: GameConfig, mode: string): number {\n const m = config.modes?.[mode];\n if (typeof m === 'number') return m;\n if (m && typeof m === 'object') return m.cost;\n return 1;\n}\n","// @stakeplate/core/currency — the Stake Engine currency table, the SINGLE source of\n// truth for how money renders (bet, balance, net, win plaque, buy modal, replay).\n//\n// The lib's `resolveCurrency` knows the common codes but falls back to a 2-dp \"$\"-ish\n// guess for many, and disagrees with the live Stake client on symbol/side for ~15 codes.\n// `currencyFor` layers the approval-doc table below on top of `resolveCurrency` (decimal\n// `.`, NO thousands separator) so every code renders byte-for-byte like the real client.\n//\n// Values come from Stake's \"Supported Currencies\" approval doc, VERIFIED against the live\n// client: fiats are PREFIX except the krone/dinar/rial group (DKK/TND/OMR/QAR/SAR/ILS…),\n// social coins are SUFFIX, and everything is 2-dp except JPY/IDR/KRW/VND/CLP (0-dp) — the\n// Gulf dinars included (KWD/JOD/BHD/TND/OMR render at 2-dp on Stake, NOT the ISO 3). An\n// earlier `CurrencyMeta` snippet disagreed on ~15 codes — it was WRONG; the live client wins.\n\nimport { resolveCurrency, isSocialCurrency, type CurrencySpec } from '@open-slot-ui/core';\n\n/** Per-code symbol / side / precision overrides, applied on top of `resolveCurrency`.\n * Unknown codes fall through to the lib's 2-dp default. */\nexport const CURRENCY_OVERRIDES: Readonly<Record<string, Partial<CurrencySpec>>> = {\n USD: { symbol: '$', position: 'prefix', decimals: 2 },\n CAD: { symbol: 'CA$', position: 'prefix', decimals: 2 },\n JPY: { symbol: '¥', position: 'prefix', decimals: 0 },\n EUR: { symbol: '€', position: 'prefix', decimals: 2 },\n RUB: { symbol: '₽', position: 'prefix', decimals: 2 },\n CNY: { symbol: 'CN¥', position: 'prefix', decimals: 2 },\n PHP: { symbol: '₱', position: 'prefix', decimals: 2 },\n INR: { symbol: '₹', position: 'prefix', decimals: 2 },\n IDR: { symbol: 'Rp', position: 'prefix', decimals: 0 },\n KRW: { symbol: '₩', position: 'prefix', decimals: 0 },\n BRL: { symbol: 'R$', position: 'prefix', decimals: 2 },\n MXN: { symbol: 'MX$', position: 'prefix', decimals: 2 },\n DKK: { symbol: 'KR', position: 'suffix', decimals: 2 }, // approval: uppercase \"KR\", not lib's \"kr\"\n PLN: { symbol: 'zł', position: 'suffix', decimals: 2 },\n VND: { symbol: '₫', position: 'suffix', decimals: 0 },\n TRY: { symbol: '₺', position: 'prefix', decimals: 2 },\n CLP: { symbol: 'CLP', position: 'suffix', decimals: 0 },\n ARS: { symbol: 'ARS', position: 'suffix', decimals: 2 },\n PEN: { symbol: 'S/', position: 'prefix', decimals: 2 },\n NGN: { symbol: '₦', position: 'prefix', decimals: 2 },\n SAR: { symbol: 'SAR', position: 'suffix', decimals: 2 },\n ILS: { symbol: 'ILS', position: 'suffix', decimals: 2 }, // letter code, amount before it (10.00 ILS)\n AED: { symbol: 'AED', position: 'suffix', decimals: 2 },\n TWD: { symbol: 'NT$', position: 'prefix', decimals: 2 },\n NOK: { symbol: 'kr', position: 'prefix', decimals: 2 },\n KWD: { symbol: 'KD', position: 'prefix', decimals: 2 }, // Stake renders dinars at 2-dp, not ISO 3\n JOD: { symbol: 'JD', position: 'prefix', decimals: 2 },\n CRC: { symbol: '₡', position: 'prefix', decimals: 2 },\n TND: { symbol: 'TND', position: 'suffix', decimals: 2 },\n SGD: { symbol: 'SG$', position: 'prefix', decimals: 2 },\n MYR: { symbol: 'RM', position: 'prefix', decimals: 2 },\n OMR: { symbol: 'OMR', position: 'suffix', decimals: 2 },\n QAR: { symbol: 'QAR', position: 'suffix', decimals: 2 },\n BHD: { symbol: 'BD', position: 'prefix', decimals: 2 },\n PKR: { symbol: '₨', position: 'prefix', decimals: 2 },\n EGP: { symbol: 'ج.م', position: 'suffix', decimals: 2 }, // amount before the symbol (10.00 ج.م)\n NZD: { symbol: 'NZ$', position: 'prefix', decimals: 2 },\n BOB: { symbol: 'Bs', position: 'prefix', decimals: 2 },\n GHS: { symbol: 'GH₵', position: 'prefix', decimals: 2 },\n KES: { symbol: 'KSh', position: 'prefix', decimals: 2 },\n MAD: { symbol: 'MAD', position: 'prefix', decimals: 2 },\n BAM: { symbol: 'KM', position: 'prefix', decimals: 2 },\n ISK: { symbol: 'kr', position: 'prefix', decimals: 2 },\n TZS: { symbol: 'TSh', position: 'prefix', decimals: 2 },\n UGX: { symbol: 'USh', position: 'prefix', decimals: 2 },\n XOF: { symbol: 'CFA', position: 'prefix', decimals: 2 },\n // Stake social/sweepstakes coins — coin abbreviation, SUFFIXED (per the doc).\n XGC: { symbol: 'GC', position: 'suffix', decimals: 2 }, // Stake Gold Coin → 10.00 GC\n XSC: { symbol: 'SC', position: 'suffix', decimals: 2 }, // Stake Cash → 10.00 SC\n XEC: { symbol: 'SC', position: 'suffix', decimals: 2 }, // Stake Euro Cash → 10.00 SC\n};\n\n/** Stake social/sweepstakes coins — they render their coin code AND flip the HUD to\n * social wording. The lib's `isSocialCurrency` knows XGC/XSC but not XEC, so recognise\n * the full set here. `createStakeGame` routes its social check through this. */\nconst SOCIAL_CURRENCIES = new Set(['XGC', 'XSC', 'XEC']);\nexport function isSocial(code: string): boolean {\n return SOCIAL_CURRENCIES.has(code ? code.toUpperCase() : '') || isSocialCurrency(code);\n}\n\n/**\n * Resolve a currency code to a `CurrencySpec` that renders exactly like the live Stake\n * client. Applies the approval-doc symbol/side/precision overrides and drops thousands\n * separators (Stake's reference `DisplayBalance` uses `toFixed()`, so `$12345.60`, not\n * `$12,345.60`). Use this EVERYWHERE a code becomes a spec — `createStakeGame` does.\n */\nexport function currencyFor(code: string): CurrencySpec {\n const extra = code ? CURRENCY_OVERRIDES[code.toUpperCase()] : undefined;\n return resolveCurrency(code, { ...extra, separator: '' });\n}\n","// The boot experience — a single overlay that carries the player from LOADING straight into\n// an optional FEATURES splash and then out of the way, all in one continuous element (no\n// flash between screens). `createStakeGame({ loader })` shows it the instant boot starts:\n//\n// LOADING — logo (pulsing) + a progress bar the core advances across the boot milestones\n// (init → auth → HUD → view), over an optional blurred cover image + vignette.\n// FEATURES — (if `features` are configured) once boot is ready the SAME logo glides up to\n// the top, the bar fades out, and 2–3 feature tiles (image + short caption, all\n// scaled to one size) fade + scale in with a \"press to continue\" prompt.\n// REVEAL — a tap / key press (or auto, with no features) fades the overlay away to the\n// game, which is already booted + idle behind it.\n//\n// So the game gets a `loader → features → idle` flow for free — it only writes its `present`\n// phase. Pure DOM + CSS: it paints before pixi/zvuk load and removes itself when done.\n\nlet seq = 0;\n\n/** One feature card on the intro splash — an image with a short (may be multi-line) caption. */\nexport interface FeatureItem {\n /** Image URL / data-URI. Rendered at a uniform height so mismatched art still lines up. */\n image: string;\n /** Short caption under the image. `\\n` for line breaks. */\n text: string;\n}\n\nexport interface LoaderConfig {\n /** Big title under the logo (LOADING phase only). Defaults to the game title. */\n title?: string;\n /** Small line under the title (LOADING phase). */\n subtitle?: string;\n /** URL/data-URI of the logo. Pulses while loading; glides to the top for the features\n * splash. Omit for a CSS ring spinner (and no move-up). */\n logo?: string;\n /** Solid colour behind everything. Default `#0b0d12`. */\n background?: string;\n /** Optional cover image (ideally the game's own backdrop, for a seamless reveal). */\n backgroundImage?: string;\n /** Blur the cover image. Default `true`. */\n blur?: boolean;\n /** Accent colour — progress fill + title glow + caption text. Default `#d99000`. */\n accent?: string;\n /** Minimum time the LOADING phase stays up, so the bar always fills. Default `1200`ms. */\n minDurationMs?: number;\n /** Stacking order. Default a very high value so it sits over the game. */\n zIndex?: number;\n /**\n * If `true`, the core shows the loader + advances progress but does NOT auto-advance past\n * LOADING — the game calls `ctx.loader.done()` itself (e.g. after its scene finishes loading\n * art). Default `false`. (The FEATURES splash always waits for the player regardless.)\n */\n manual?: boolean;\n /** 2–3 feature cards → show a FEATURES splash after loading. Omit for a plain loader. */\n features?: FeatureItem[];\n /** The dismiss prompt under the feature cards. Default `PRESS TO CONTINUE`. */\n continueText?: string;\n}\n\nexport interface GameLoader {\n /** The overlay element (already in the document). */\n readonly el: HTMLElement;\n /** Drive the progress bar. Clamped to the range it has already reached (never goes back). */\n setProgress(p: number): void;\n /** Advance out of LOADING: fill to 100%, honour `minDurationMs`, then either show the\n * FEATURES splash (waiting for the player to continue) or — with no features — pop away.\n * Resolves once the overlay is fully gone. Idempotent (first call wins). */\n done(): Promise<void>;\n /** Remove immediately without any outro (used on a boot error). */\n remove(): void;\n}\n\nconst isBrowser = (): boolean => typeof document !== 'undefined' && !!document.body;\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** Create + mount the boot overlay. Safe to call before anything else in boot. */\nexport function createLoader(config: LoaderConfig = {}): GameLoader {\n const startedAt = Date.now();\n const minMs = config.minDurationMs ?? 1200;\n const accent = config.accent ?? '#d99000';\n const bg = config.background ?? '#0b0d12';\n const z = config.zIndex ?? 2147483000;\n const blur = config.blur ?? true;\n const features = config.features ?? [];\n const hasLogo = !!config.logo;\n let progress = 0;\n let donePromise: Promise<void> | null = null;\n\n // Headless / SSR guard — a no-op handle so callers needn't branch.\n if (!isBrowser()) {\n return { el: null as unknown as HTMLElement, setProgress() {}, done: () => Promise.resolve(), remove() {} };\n }\n\n const id = `sp-loader-${++seq}`;\n const root = document.createElement('div');\n root.id = id;\n root.className = 'sp-loader phase-loading';\n\n const style = document.createElement('style');\n style.textContent = `\n #${id}{position:fixed;inset:0;z-index:${z};background:${config.backgroundImage ? 'transparent' : bg};overflow:hidden;\n font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n opacity:1;transition:opacity .5s ease}\n #${id}.sp-done{opacity:0}\n #${id} .sp-bgimg{position:absolute;inset:-8%;background-position:center;background-size:cover;\n ${blur ? 'filter:blur(6px);' : ''}transform:scale(1.06)}\n #${id} .sp-vign{position:absolute;inset:0;background:radial-gradient(120% 90% at 50% 42%,transparent 40%,rgba(0,0,0,.62) 100%)}\n /* Logo — same element in both phases; glides + shrinks from centre to the top. */\n #${id} .sp-logo{position:absolute;left:50%;top:36%;transform:translate(-50%,-50%);\n width:min(42vw,240px);height:auto;filter:drop-shadow(0 14px 24px rgba(0,0,0,.55));\n animation:sp-pulse 1.5s ease-in-out infinite;\n transition:top .6s cubic-bezier(.5,0,.2,1),width .6s cubic-bezier(.5,0,.2,1)}\n #${id}.phase-features .sp-logo{top:15%;width:min(30vw,180px);animation:none}\n /* LOADING group (title/subtitle + bar) — fades out on the way to features. */\n #${id} .sp-load{position:absolute;left:50%;top:58%;transform:translate(-50%,-50%);\n display:flex;flex-direction:column;align-items:center;gap:14px;\n transition:opacity .35s ease}\n #${id}.phase-features .sp-load{opacity:0;pointer-events:none}\n #${id} .sp-spin{width:60px;height:60px;border-radius:50%;border:5px solid rgba(255,255,255,.18);\n border-top-color:${accent};animation:sp-rot .8s linear infinite}\n #${id} .sp-title{margin:0;color:#fff;font-weight:800;letter-spacing:.02em;\n font-size:clamp(20px,3.6vw,36px);text-align:center;text-shadow:0 3px 10px rgba(0,0,0,.55),0 0 22px ${accent}55}\n #${id} .sp-sub{margin:0;color:#ffffffcc;font-size:clamp(12px,2vw,16px);text-align:center;text-shadow:0 2px 6px rgba(0,0,0,.5)}\n #${id} .sp-bar{position:relative;width:min(60vw,260px);height:8px;border-radius:99px;background:rgba(255,255,255,.16);overflow:hidden}\n #${id} .sp-fill{position:absolute;left:0;top:0;bottom:0;width:0%;border-radius:99px;background:${accent};transition:width .35s ease}\n /* FEATURES splash — tiles + prompt; scales/fades in once boot is ready. */\n #${id} .sp-feat{position:absolute;left:0;right:0;top:30%;bottom:0;display:flex;flex-direction:column;\n align-items:center;justify-content:flex-start;gap:5vh;padding:0 4vw;\n opacity:0;transform:scale(.94);pointer-events:none;transition:opacity .5s ease,transform .5s ease}\n #${id}.phase-features .sp-feat{opacity:1;transform:scale(1);pointer-events:auto}\n #${id} .sp-tiles{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;\n gap:clamp(18px,5vw,64px);max-width:94vw}\n #${id} .sp-tile{display:flex;flex-direction:column;align-items:center;gap:14px;\n width:clamp(110px,22vw,210px);opacity:0;transform:translateY(16px) scale(.9);\n transition:opacity .45s ease,transform .45s cubic-bezier(.34,1.56,.64,1)}\n #${id}.phase-features .sp-tile{opacity:1;transform:none}\n #${id} .sp-tile img{height:clamp(66px,13vh,150px);width:auto;max-width:100%;object-fit:contain;\n filter:drop-shadow(0 8px 14px rgba(0,0,0,.45))}\n #${id} .sp-tile span{color:${accent};font-weight:800;text-transform:uppercase;letter-spacing:.02em;\n text-align:center;white-space:pre-line;line-height:1.15;font-size:clamp(13px,2.1vw,21px);\n text-shadow:0 2px 6px rgba(0,0,0,.6)}\n #${id} .sp-cont{margin-top:auto;margin-bottom:5vh;color:#ffffffcc;text-transform:uppercase;\n letter-spacing:.16em;font-size:clamp(11px,1.6vw,15px);animation:sp-blink 1.7s ease-in-out infinite}\n @keyframes sp-pulse{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.06)}}\n @keyframes sp-rot{to{transform:rotate(360deg)}}\n @keyframes sp-blink{0%,100%{opacity:.4}50%{opacity:1}}\n @media (prefers-reduced-motion:reduce){#${id} .sp-logo,#${id} .sp-spin,#${id} .sp-cont{animation:none}}\n `;\n\n if (config.backgroundImage) {\n const img = document.createElement('div');\n img.className = 'sp-bgimg';\n img.style.backgroundImage = `url(${config.backgroundImage})`;\n root.appendChild(img);\n const vign = document.createElement('div');\n vign.className = 'sp-vign';\n root.appendChild(vign);\n }\n\n if (hasLogo) {\n const logo = document.createElement('img');\n logo.className = 'sp-logo';\n logo.src = config.logo!;\n logo.alt = '';\n root.appendChild(logo);\n }\n\n // LOADING group.\n const load = document.createElement('div');\n load.className = 'sp-load';\n if (!hasLogo) {\n const spin = document.createElement('div');\n spin.className = 'sp-spin';\n load.appendChild(spin);\n }\n if (config.title) {\n const h = document.createElement('h1');\n h.className = 'sp-title';\n h.textContent = config.title;\n load.appendChild(h);\n }\n if (config.subtitle) {\n const p = document.createElement('p');\n p.className = 'sp-sub';\n p.textContent = config.subtitle;\n load.appendChild(p);\n }\n const bar = document.createElement('div');\n bar.className = 'sp-bar';\n const fill = document.createElement('div');\n fill.className = 'sp-fill';\n bar.appendChild(fill);\n load.appendChild(bar);\n root.appendChild(load);\n\n // FEATURES group (built only when configured).\n if (features.length) {\n const feat = document.createElement('div');\n feat.className = 'sp-feat';\n const tiles = document.createElement('div');\n tiles.className = 'sp-tiles';\n features.forEach((f, i) => {\n const tile = document.createElement('div');\n tile.className = 'sp-tile';\n tile.style.transitionDelay = `${0.12 + i * 0.12}s`; // staggered entrance\n const img = document.createElement('img');\n img.src = f.image;\n img.alt = '';\n const cap = document.createElement('span');\n cap.textContent = f.text;\n tile.append(img, cap);\n tiles.appendChild(tile);\n });\n const cont = document.createElement('div');\n cont.className = 'sp-cont';\n cont.textContent = config.continueText ?? 'PRESS TO CONTINUE';\n feat.append(tiles, cont);\n root.appendChild(feat);\n }\n\n root.appendChild(style);\n document.body.appendChild(root);\n\n const setProgress = (p: number): void => {\n const next = Math.max(progress, Math.min(1, Number.isFinite(p) ? p : 0));\n progress = next;\n fill.style.width = `${Math.round(next * 100)}%`;\n };\n\n const remove = (): void => {\n root.remove();\n style.remove();\n // Clean up the build-time boot backdrop injected by the `stakeplateBoot` Vite plugin\n // (if present) — the game is now covering it.\n document.getElementById('sp-bootbg')?.remove();\n document.getElementById('sp-boot-style')?.remove();\n };\n\n // Transition LOADING → FEATURES, then resolve once the player dismisses it.\n const runFeatures = (): Promise<void> =>\n new Promise((resolve) => {\n root.classList.remove('phase-loading');\n root.classList.add('phase-features');\n const dismiss = (): void => {\n root.removeEventListener('pointerdown', dismiss);\n window.removeEventListener('keydown', onKey);\n resolve();\n };\n const onKey = (e: KeyboardEvent): void => { if (e.key === ' ' || e.key === 'Enter') dismiss(); };\n // Arm dismissal only after the splash has animated in (so a stray click can't skip it).\n setTimeout(() => {\n root.addEventListener('pointerdown', dismiss);\n window.addEventListener('keydown', onKey);\n }, 500);\n });\n\n const done = (): Promise<void> => {\n if (donePromise) return donePromise;\n donePromise = (async () => {\n setProgress(1);\n const wait = minMs - (Date.now() - startedAt);\n if (wait > 0) await sleep(wait);\n if (features.length) await runFeatures(); // hold on the splash until the player continues\n root.classList.add('sp-done'); // fade the whole overlay out\n await sleep(520);\n remove();\n })();\n return donePromise;\n };\n\n return { el: root, setProgress, done, remove };\n}\n","// createStakeGame — the one-call façade. `start()` runs the whole compliant boot with\n// zero game involvement: runtime → network → HUD → (replay | authenticate → configure\n// HUD → resume active round → wire events) → run the FSM. Connection/auth failure →\n// the themed, blocking showBootError. The game supplies only config + interpretBook +\n// mountView (+ optional Present phase, audio). This is the ONLY module that imports the\n// HUD/pixi peers; the engine stays decoupled behind ports.\n\nimport { Application } from 'pixi.js';\nimport { mountHud, showBootError, mountBuyFeatureModal, type BootedHud, type FeatureSpec } from '@open-slot-ui/pixi';\nimport { loadBuiltinArt } from '@open-slot-ui/pixi/art';\nimport { currencyFor, isSocial } from '../currency';\nimport { reaction } from 'mobx';\nimport { readRuntime, type RuntimeConfig } from '../rgs/runtime';\nimport { createNetwork, type NetworkManager } from '../rgs/network';\nimport { API_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\nimport { RootStore } from '../stores/index';\nimport { RealTicker, type Ticker } from '../engine/ticker';\nimport { TurboClock, type TurboState } from '../engine/turbo';\nimport { FSM, type AudioPort, type Phase, type PhaseContext } from '../engine/fsm';\nimport { defaultPhases } from '../engine/phases';\nimport { roundInfo, type InterpretBook } from '../engine/round';\nimport { bindMixerToHud, startMusicOnFirstGesture, type MixerLike } from '../audio/bind';\nimport { bindInputSounds, type InputSoundMap } from '../audio/inputs';\nimport type { GameAudioOptions, SoundEntry } from '../audio';\nimport { createLoader, type GameLoader, type LoaderConfig } from '../loader';\nimport { modeCostOf, type GameConfig } from './config';\n\n/**\n * Declarative audio: hand the core your sounds and it lazily creates the `@schmooky/zvuk`\n * mixer (master → music/sfx/ambience buses + ducking), preloads them, and auto-binds it to\n * the HUD (sliders/mute + unlock). zvuk loads in its OWN async chunk — a game without sound\n * pays nothing. (Advanced: pass a ready `GameAudio` instance instead, for custom buses/FX.)\n */\nexport interface AudioSpec extends GameAudioOptions {\n sounds: SoundEntry[];\n /** Optional: play a cue on HUD input events (spin/bet/autoplay/turbo/toggle/skip). The\n * core auto-binds these after the HUD mounts — the named sounds must be in `sounds`. */\n inputSounds?: InputSoundMap;\n /** Name of a `kind:'music'` entry in `sounds` — the core auto-starts it (unlocking the\n * AudioContext) on the FIRST user interaction. The game never starts music itself. */\n bgm?: string;\n /** Fade-in (seconds) for the auto-started `bgm`. Default 0.8. */\n bgmFadeIn?: number;\n}\n\n/** Passed to `mountView` — everything the game's scene needs (not the round/fsm yet). */\nexport interface ViewContext {\n config: GameConfig;\n stores: RootStore;\n hud: BootedHud;\n ticker: Ticker;\n audio: AudioPort | null;\n /** Turbo speed + slam-stop (core-owned) — the scene may branch on `turbo.level`/`speed`. */\n turbo: TurboState;\n /** The boot loader (if `loader` was configured) — drive `setProgress` while your scene\n * loads art, and (with `loader.manual`) call `done()` when the scene is ready. `null`\n * when no loader is used. */\n loader: GameLoader | null;\n}\n\nexport interface CreateStakeGameOptions<T = unknown, V = unknown, E = unknown> {\n config: GameConfig;\n /** The game's ONE money seam: typed RGS round (`Round<E>`) → your model. Pure. */\n interpretBook: InterpretBook<T, E>;\n /** Mount the game's pixi scene/presenter/stores; returns the view handed to phases. */\n mountView: (host: HTMLElement, ctx: ViewContext) => V;\n /** The game's Present phase (+ any overrides); Idle/Spin/Settle are provided. */\n phases?: Phase<T, V, E>[];\n /** Sounds (declarative — the core builds + wires the mixer) OR a ready `GameAudio`/AudioPort. */\n audio?: AudioPort | AudioSpec | null;\n /** Override the transport (tests / a supplied mock). */\n network?: NetworkManager;\n /** Override launch params (tests / embedding). */\n runtime?: Partial<RuntimeConfig>;\n /** Host for the pixi HUD canvas (default document.body). */\n hudHost?: HTMLElement;\n /** Host for the game scene (default = hudHost). */\n sceneHost?: HTMLElement;\n /** Passthrough to `mountHud` (spinSkin, icons, gsap, menu:false, hooks, …). */\n hudOptions?: Record<string, unknown>;\n /**\n * A configurable boot loader — shows the instant boot starts, advances across the boot\n * milestones, then fills + pops away when the game is ready. Pass a {@link LoaderConfig}\n * to enable it (title defaults to `config.title`); omit or `false` to render none (the\n * game keeps its own HTML loader). With `manual: true`, call `ctx.loader.done()` yourself\n * (e.g. after the scene's art finishes loading).\n */\n loader?: LoaderConfig | false;\n}\n\n/**\n * Stake policy: buys/activations costing more than this many × base bet require a confirm\n * (no one-click). A buy-feature always costs far more than 2×, so in practice this means\n * \"always confirm a buy\". The jurisdiction (`auth.config.jurisdiction`) may override it; a\n * game never sets it — it's compliance, not design.\n */\nconst STAKE_CONFIRM_ABOVE_COST = 2;\n\n/** Fallback min-payout coefficient used ONLY when `config.minBet` is unset: the\n * currency-derived floor is `minUnit / MIN_PAYOUT_COEF` (= 5 minimal units at 0.2). */\nconst MIN_PAYOUT_COEF = 0.2;\n\n/** Persisted player UI preferences (sound mute + turbo mode). Best-effort — every access\n * is guarded: Safari Private Browsing, a cross-origin-iframe storage block, or a hardened\n * session throws on access, so this degrades to \"no persistence\", never into the boot. */\nconst PREFS_KEY = 'stakeplate.prefs';\ninterface GamePrefs {\n muted?: boolean;\n turbo?: number;\n}\nfunction readPrefs(): GamePrefs {\n try {\n return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}') as GamePrefs;\n } catch {\n return {};\n }\n}\nfunction writePrefs(patch: GamePrefs): void {\n try {\n localStorage.setItem(PREFS_KEY, JSON.stringify({ ...readPrefs(), ...patch }));\n } catch {\n /* storage disabled / full — preferences just won't persist this session */\n }\n}\n\n/** Drop server bet-ladder levels below the minimum bet — the explicit `config.minBet`, else\n * the currency-derived floor (5 minimal units) — so the smallest win can't round below one\n * minimal unit. Never returns empty (keeps the largest level if every level is below). */\nfunction applyMinBet(levels: number[], minBet: number | undefined, decimals: number): number[] {\n const minUnit = 10 ** -decimals;\n const floor = minBet ?? minUnit / MIN_PAYOUT_COEF;\n const filtered = levels.filter((b) => b >= floor - minUnit / 1000);\n return filtered.length ? filtered : [Math.max(...levels)];\n}\n\n/** Snap a wanted bet up to the first ladder level that is >= it (or the closest). */\nfunction snapToLadder(levels: number[], want: number): number {\n if (levels.includes(want)) return want;\n return levels.find((b) => b >= want) ?? levels[levels.length - 1] ?? want;\n}\n\n/** Server/replay-sourced values that drive the HUD spec — never taken from the game config. */\ninterface HudSpecInput {\n currency: string;\n betLevels: number[]; // major units — the authoritative ladder\n defaultBet: number; // major units\n rtp: number; // display %\n confirmBuyAboveCost: number; // jurisdiction policy\n}\n\n/** A read-only snapshot of the running game — for the dev harness, tests and debugging. */\nexport interface GameSnapshot {\n phase: string;\n balance: number;\n bet: number;\n lastWin: number;\n currency: string;\n spinning: boolean;\n}\n\nexport interface StakeGame {\n start(): Promise<void>;\n dispose(): void;\n /** Current phase + store values. The declarative harness asserts on this (no screenshots). */\n inspect(): GameSnapshot;\n /** Trigger a spin the same way the HUD spin button does — only from Idle. For harness/tests. */\n requestSpin(): boolean;\n}\n\nexport function createStakeGame<T = unknown, V = unknown, E = unknown>(opts: CreateStakeGameOptions<T, V, E>): StakeGame {\n const runtime = readRuntime({ overrides: opts.runtime });\n const network = opts.network ?? createNetwork(runtime);\n const stores = new RootStore();\n const ticker = new RealTicker();\n const turbo = new TurboClock(opts.config.turboSpeeds); // core-owned turbo speed + slam-stop\n // A ready AudioPort instance is used as-is; an AudioSpec is resolved lazily in start()\n // (the core creates the zvuk mixer from its own async chunk) — see resolveAudio().\n let audio: AudioPort | null = opts.audio && !isAudioSpec(opts.audio) ? opts.audio : null;\n const app = new Application();\n const disposers: Array<() => void> = [];\n let hud: BootedHud | null = null;\n let fsm: FSM<T, V, E> | null = null;\n let loader: GameLoader | null = null;\n\n // The battery wires the library's DESIGNED default icon set (the white Figma coins +\n // rotating-arrows spin skin) so every game gets the intended HUD out of the box — not\n // the lib's no-art placeholder geometry. `loadBuiltinArt` pulls the whole /art bundle\n // once; a game overrides any icon or the spin skin via `hudOptions.icons`/`spinSkin`.\n let builtinArt: ReturnType<typeof loadBuiltinArt> | null = null;\n const hudOpts = async (): Promise<Record<string, unknown>> => {\n builtinArt ??= loadBuiltinArt();\n const art = await builtinArt;\n const user = (opts.hudOptions ?? {}) as { icons?: Record<string, unknown>; spinSkin?: unknown };\n return {\n spinSkin: art.spinSkin,\n ...user,\n icons: { ...(art.icons as Record<string, unknown>), ...(user.icons ?? {}) },\n };\n };\n\n // The HUD spec is driven by SERVER-authoritative values (the ladder + confirm policy come\n // from `authenticate`/jurisdiction, not the game). `buildSpec` just formats them.\n const buildSpec = (s: HudSpecInput, extra?: { hideBalance?: boolean }): Record<string, unknown> => ({\n currency: currencyFor(s.currency),\n betLadder: { levels: s.betLevels, index: Math.max(0, s.betLevels.indexOf(s.defaultBet)) },\n rtp: s.rtp,\n game: { name: opts.config.title, version: opts.config.version ?? '1.0.0' },\n // Stake owns fullscreen in its iframe — the game must not render its own. The bonus\n // (buy-feature) button shows only when a mode is a buy/boost card — it opens the feature list.\n // Replay mode has no wallet (it's a recorded round) — Stake approval: show NO balance\n // readout; the round's facts live in the replay panel instead.\n controls: {\n fullscreen: { hidden: true },\n ...(buyFeaturesOf(opts.config).length ? { bonus: { hidden: false } } : {}),\n ...(extra?.hideBalance ? { balance: { hidden: true } } : {}),\n },\n // Compliance: the buy-feature confirm threshold is jurisdiction policy, not a game knob.\n buyFeature: { confirmAboveCost: s.confirmBuyAboveCost },\n ...(opts.config.rules ? { menu: opts.config.rules } : {}),\n locale: {\n locale: runtime.language,\n messages: opts.config.messages ?? { en: {} },\n ...(opts.config.socialMessages ? { socialMessages: opts.config.socialMessages } : {}),\n },\n ...(opts.config.spec ?? {}),\n });\n\n const initApp = async (host: HTMLElement): Promise<void> => {\n await app.init({ resizeTo: window, backgroundAlpha: 0, antialias: true, resolution: Math.min(window.devicePixelRatio || 1, 2), autoDensity: true });\n host.appendChild(app.canvas);\n };\n\n // Build the mixer from a declarative AudioSpec. The `@stakeplate/core/audio` module (the\n // ONLY thing that pulls @schmooky/zvuk) is loaded via a DYNAMIC import, so it lands in its\n // own async chunk — an audio-less game never fetches zvuk. Sounds preload in the background.\n const resolveAudio = async (): Promise<void> => {\n if (!isAudioSpec(opts.audio)) return;\n const { createGameAudio } = await import('../audio');\n const mixer = createGameAudio(opts.audio);\n void mixer.load(opts.audio.sounds);\n audio = mixer;\n };\n\n async function start(): Promise<void> {\n const hudHost = opts.hudHost ?? document.body;\n const sceneHost = opts.sceneHost ?? hudHost;\n // Boot loader: paint it BEFORE anything heavy loads, then advance it across the milestones\n // below (init → auth → HUD → view) and fill + pop it away once the game is ready. `false`\n // (the default) renders none, so a game keeps whatever loader its HTML already has.\n if (opts.loader) loader = createLoader({ title: opts.config.title, ...opts.loader });\n const manualLoader = !!(opts.loader && opts.loader.manual);\n const phases = [...defaultPhases<T, V, E>(), ...(opts.phases ?? [])];\n const machine = (fsm = new FSM<T, V, E>(phases)); // outer `fsm` powers inspect()/requestSpin()\n loader?.setProgress(0.1);\n await resolveAudio(); // if `audio` is an AudioSpec, build the zvuk mixer (lazy chunk) + preload\n\n // ── REPLAY (Stake ?replay=true): fetch a round + play it back read-only ──────\n if (runtime.replay.active && network.replay) {\n try {\n await initApp(hudHost);\n loader?.setProgress(0.5);\n const cur = runtime.currency;\n const bet = runtime.replay.amount || 1; // replay carries its own bet; no ladder needed\n hud = mountHud(app, buildSpec({ currency: cur, betLevels: [bet], defaultBet: bet, rtp: opts.config.rtp ?? 96, confirmBuyAboveCost: STAKE_CONFIRM_ABOVE_COST }, { hideBalance: true }), (await hudOpts()) as never);\n stores.session.set({ currency: cur });\n stores.balance.setBalance(0);\n stores.balance.setBet(bet);\n if (runtime.social) hud.setSocial(true);\n loader?.setProgress(0.85);\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo, loader });\n hud.setReplay(true);\n hud.lockInput();\n const cost = modeCostOf(opts.config, runtime.replay.mode);\n const raw = (await network.replay({ ...runtime.replay })) as Round<E>;\n const info = roundInfo(raw, bet, cost);\n const ctx = makeCtx(machine, view);\n ctx.round = { ...info, data: opts.interpretBook(raw, info), active: false, balance: 0, raw };\n const replayInfo = { baseBet: bet, costMultiplier: cost, payoutMultiplier: info.multiplier, amount: info.totalWin, currency: currencyFor(cur) };\n const playRound = async (): Promise<void> => {\n stores.ui.setSpinning(true);\n await machine.transition('present'); // game's Present plays it back → settle → idle\n hud!.replayEnd(replayInfo, () => void playRound());\n };\n hud.replayStart(replayInfo, () => void playRound());\n if (!manualLoader) void loader?.done();\n } catch (err) {\n loader?.remove();\n showBootError({ title: 'Cannot load the replay', message: 'The recorded round could not be loaded. Please reload to try again.', detail: errMsg(err) });\n }\n return;\n }\n\n // ── NORMAL BOOT ─────────────────────────────────────────────────────────────\n let auth;\n try {\n await initApp(hudHost);\n loader?.setProgress(0.35);\n auth = await network.authenticate();\n loader?.setProgress(0.6);\n } catch (err) {\n loader?.remove();\n showBootError({\n title: 'Cannot reach the game server',\n message: 'The game could not connect to or authenticate with the game server. Please reload to try again.',\n detail: errMsg(err),\n });\n return;\n }\n\n // Everything the HUD needs is SERVER-authoritative (from `authenticate`): the bet\n // ladder + default bet (per currency/jurisdiction), RTP, and the buy-confirm policy.\n const currency = auth.balance.currency;\n const juris = auth.config.jurisdiction ?? {};\n const rtp = auth.config.rtp ?? opts.config.rtp ?? 96;\n // Server ladder, trimmed by the client min-bet floor (explicit `config.minBet`, else the\n // currency-derived 5-minimal-units) so the smallest possible win can't round to sub-unit.\n const rawLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);\n const betLevels = applyMinBet(rawLevels, opts.config.minBet, currencyFor(currency).decimals ?? 2);\n const active = auth.round;\n const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;\n // Wanted default (restored from the active bet on resume), snapped up to the first legal\n // (floored) level so the ladder + store agree.\n const wantedDefault = active?.active\n ? active.amount / API_AMOUNT_MULTIPLIER / activeCost // resume: restore bet from the active amount\n : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER;\n const defaultBet = snapToLadder(betLevels, wantedDefault);\n const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;\n\n hud = mountHud(app, buildSpec({ currency, betLevels, defaultBet, rtp, confirmBuyAboveCost }), (await hudOpts()) as never);\n hud.setCurrency(currencyFor(currency));\n hud.applyJurisdiction(juris);\n if (runtime.social || isSocial(currency)) hud.setSocial(true);\n\n stores.session.set({\n sessionId: runtime.sessionId,\n currency,\n rtp,\n availableBets: betLevels,\n jurisdiction: juris,\n });\n stores.balance.setBet(defaultBet);\n hud.setBet(defaultBet);\n loader?.setProgress(0.8);\n\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo, loader });\n const ctx = makeCtx(machine, view);\n loader?.setProgress(0.95);\n\n // reactions store→HUD + HUD events\n disposers.push(reaction(() => stores.balance.balance, (b) => hud!.setBalance(b), { fireImmediately: false }));\n // Bracket the spin control's `spinning` state around each active round. This drives the spin\n // button's spinning animation AND trips the lib's `lockDuringSpin`, so every other control\n // (autoplay · buy · menu · bet ±) dims + goes non-interactive while a round is in flight; spin\n // itself stays tappable to slam-stop. Cleared at settle when `spinning` returns to false.\n disposers.push(reaction(() => stores.ui.spinning, (sp) => { if (sp) hud!.ui.spin.busy(); else hud!.ui.spin.idle(); }, { fireImmediately: false }));\n // The bet stepper is the base-bet source of truth. It emits `valueChanged` with\n // `id: 'bet-stepper'` and the ladder value (never the boosted display), so this feeds the\n // BASE bet even while an ante is active. (The lib mirrors it into `ui.bet`; the boost\n // reaction then overwrites the readout with the effective stake.)\n disposers.push(hud.on('valueChanged', (p) => { const v = p as { id?: string; value?: number }; if (v?.id === 'bet-stepper' && typeof v.value === 'number') stores.balance.setBet(v.value); }));\n // ── Spin triggers + turbo speed + autoplay (ALL core-owned) ─────────────────\n // One entry point for every spin: clears the slam-stop flag, then spins from idle.\n const beginSpin = (): void => {\n if (machine.current !== 'idle') return;\n turbo.resetSkip();\n void machine.transition('spin');\n };\n let holdActive = false; // press-and-hold turbo spin\n disposers.push(hud.on('spinRequested', beginSpin));\n // Autoplay/hold: the HUD owns the count picker, remaining count + RG limits (via\n // reportRound); the CORE runs the loop. Start on the first tick, then re-spin below.\n // Autoplay/hold both shorten game animations (turbo.setAutoplay) so long auto sessions\n // don't crawl. It's a floor on turbo speed — a faster turbo level still wins.\n disposers.push(hud.on('autoplayStarted', () => { turbo.setAutoplay(true); beginSpin(); }));\n disposers.push(hud.on('autoplayStopped', () => { turbo.setAutoplay(holdActive); }));\n disposers.push(hud.on('holdSpinStarted', () => { holdActive = true; turbo.setAutoplay(true); beginSpin(); }));\n disposers.push(hud.on('holdSpinStopped', () => { holdActive = false; turbo.setAutoplay(hud!.ui.autoplay.isActive); }));\n // Turbo speed (2-/3-mode cycler) + slam-stop (tap-to-skip the reels).\n disposers.push(hud.on('turboChanged', (p) => { const e = p as { index?: number }; if (typeof e.index === 'number') { turbo.setLevel(e.index); writePrefs({ turbo: e.index }); } }));\n disposers.push(hud.on('skipRequested', () => turbo.skip()));\n // Buy-feature: the bonus button opens the lib's feature-LIST modal (a card per buyable\n // mode). Two kinds of card, both routed through the jurisdiction confirm gate (no one-click\n // above the threshold):\n // • `buy` → one-shot. On confirm, spin that mode ONCE (SpinPhase charges its full cost).\n // • `boost` → a persistent ante. Activating sets it as the ACTIVE mode, so every following\n // base spin plays it at its full `cost×` (via `nextMode()`); toggling off restores base.\n // The modal owns opening/closing/confirm + single-activation; the core supplies the cards +\n // the onBuy/onActivate hooks and reflects the boosted stake in the bet readout.\n const features = buyFeaturesOf(opts.config);\n if (features.length) {\n // The bet readout shows the EFFECTIVE stake (base × the active ante's cost, emphasised)\n // while a boost is on, and the plain base bet otherwise — re-applied on bet/mode change.\n const applyBetDisplay = (): void => {\n const boost = stores.ui.activeMode;\n const cost = boost ? modeCostOf(opts.config, boost) : 1;\n hud!.ui.bet.set(stores.balance.bet * cost);\n (hud!.ui.bet as unknown as { setEmphasis?: (on: boolean) => void }).setEmphasis?.(cost !== 1);\n };\n disposers.push(\n mountBuyFeatureModal(app, hud, features, {\n activation: 'single', // one ante at a time (Stake)\n // Read the BASE bet straight from the stepper CONTROL (updated synchronously by\n // inc()/dec()) — NOT `stores.balance.bet`, which the core updates a tick later off\n // `valueChanged`, so the modal would lag one step (first +/- press \"doesn't\n // register\", +then- re-fires +). The readout still shows the boosted stake.\n getBet: () => hud!.ui.betStepper.value,\n onBuy: (id) => {\n if (machine.current !== 'idle') return;\n // Buying plays at the BASE bet — clear any active ante so its multiplied\n // (emphasised, yellow) bet readout reverts to the plain base bet first.\n if (stores.ui.activeMode) {\n stores.ui.setActiveMode(null);\n applyBetDisplay();\n }\n stores.ui.setOneShotMode(id);\n beginSpin();\n },\n onActivate: (ids) => {\n stores.ui.setActiveMode(ids[0] ?? null); // persistent ante; nextMode() prefers it\n applyBetDisplay();\n },\n }),\n );\n disposers.push(reaction(() => [stores.balance.bet, stores.ui.activeMode] as const, applyBetDisplay));\n }\n // Keep spinning while autoplay/hold is active: after each settled round (spinning\n // true → false), once idle, pause the (turbo-scaled) gap and spin again — stopping on\n // a blocking notice/error or when the next base stake is unaffordable.\n const autoplayGapMs = opts.config.autoplayGapMs ?? 250;\n disposers.push(\n reaction(\n () => stores.ui.spinning,\n (spinning, prev) => {\n if (prev !== true || spinning !== false) return;\n void turbo.delay(autoplayGapMs).then(() => {\n if (machine.current !== 'idle') return;\n if (!hud!.ui.autoplay.isActive && !holdActive) return;\n if (hud!.ui.noticeBlocks.get().length > 0 || stores.balance.balance < stores.balance.bet) {\n hud!.ui.autoplay.stop();\n holdActive = false;\n return;\n }\n beginSpin();\n });\n },\n ),\n );\n\n // Auto-wire audio ↔ HUD (Music/Effects sliders + mute, persisted) — IF a mixer-like `audio`\n // was provided. Structural check, no @schmooky/zvuk import here, so audio-less games don't\n // bundle it. When the AudioSpec names a `bgm`, the CORE starts it (+ unlocks the context) on\n // the first user gesture — the game never touches music. Otherwise we just unlock on the first\n // spin. (A game may bind manually instead.)\n const mixer = audio as unknown as MixerLike | null;\n if (mixer && typeof mixer.setGroupLevel === 'function') {\n disposers.push(bindMixerToHud(mixer, hud));\n const bgm = isAudioSpec(opts.audio) ? opts.audio.bgm : undefined;\n if (bgm && audio) {\n disposers.push(startMusicOnFirstGesture(audio, mixer, hud, { bgm, fadeIn: isAudioSpec(opts.audio) ? opts.audio.bgmFadeIn : undefined }));\n } else if (typeof mixer.unlock === 'function') {\n const off = hud.on('spinRequested', () => { void mixer.unlock!(); off(); });\n disposers.push(off);\n }\n }\n // Input sounds: if the declarative AudioSpec named cues per HUD input, wire them now.\n if (audio && isAudioSpec(opts.audio) && opts.audio.inputSounds) {\n disposers.push(bindInputSounds(audio, hud, opts.audio.inputSounds));\n }\n\n // ── Persist player prefs (sound mute + turbo mode) across sessions ──────────\n // Restore BEFORE the game is interactive so the controls boot in the saved position\n // (setMuted also propagates to the audio mixer via bindMixerToHud's muted subscription\n // above). turboChanged below persists the turbo mode; anon/blocked-storage is safe.\n const prefs = readPrefs();\n if (typeof prefs.muted === 'boolean') hud.setMuted(prefs.muted);\n if (typeof prefs.turbo === 'number' && prefs.turbo > 0) {\n hud.ui.turbo.setIndex(prefs.turbo);\n turbo.setLevel(prefs.turbo);\n }\n disposers.push(hud.ui.muted.subscribe((m) => writePrefs({ muted: m })));\n\n // ── ACTIVE-ROUND RESUME: settle it, land idle, then a \"round in progress\" modal ──\n if (active?.active) {\n // The player refreshed while a round was still open. Settle it now (balance becomes\n // authoritative; the bet was restored into the ladder above), land on a clean idle\n // board, then show a NON-dismissible modal — on Continue we replay the recovered\n // round through the game's Present phase so the player watches how it resolved before\n // regaining control. (Not awaited — start() proceeds to fill/pop the loader; the modal\n // shows over the game once the loader fades.)\n const end = await network.endRound();\n const raw = active as Round<E>;\n const info = roundInfo(raw, defaultBet, activeCost);\n const settledBal = end.balance.amount / API_AMOUNT_MULTIPLIER;\n stores.balance.setBalance(settledBal);\n const resumeRound = { ...info, data: opts.interpretBook(raw, info), active: false, balance: settledBal, raw };\n await machine.transition('idle');\n hud.showFatal('You have an unfinished round. Continue to see how it ends.', {\n title: 'Round in progress',\n tone: 'info',\n actions: [\n {\n label: 'Continue',\n variant: 'primary',\n onSelect: () => {\n hud!.hideNotice();\n hud!.lockInput(); // no spins/buys while the recovered round replays\n ctx.round = resumeRound;\n void machine.transition('present').finally(() => hud!.unlockInput());\n },\n },\n ],\n });\n } else {\n stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);\n await machine.transition('idle');\n }\n // The game is booted + interactive → fill + pop the loader away (unless the game asked\n // to drive it manually, e.g. to wait for its scene art via `ctx.loader.done()`).\n if (!manualLoader) void loader?.done();\n }\n\n function makeCtx(fsm: FSM<T, V, E>, view: V): PhaseContext<T, V, E> {\n const ctx: PhaseContext<T, V, E> = {\n config: opts.config,\n stores,\n network,\n hud: hud as BootedHud,\n ticker,\n view,\n audio,\n turbo,\n loader,\n interpretBook: opts.interpretBook,\n fsm,\n round: null,\n modeCost: (mode) => modeCostOf(opts.config, mode),\n };\n fsm.bind(ctx);\n return ctx;\n }\n\n function dispose(): void {\n for (const d of disposers.splice(0)) d();\n hud?.dispose();\n app.destroy(true);\n }\n\n function inspect(): GameSnapshot {\n return {\n phase: fsm?.current ?? 'boot',\n balance: stores.balance.balance,\n bet: stores.balance.bet,\n lastWin: stores.balance.lastWin,\n currency: stores.session.currency,\n spinning: stores.ui.spinning,\n };\n }\n\n function requestSpin(): boolean {\n if (!fsm || fsm.current !== 'idle') return false;\n void fsm.transition('spin');\n return true;\n }\n\n return { start, dispose, inspect, requestSpin };\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** An AudioSpec (declarative sounds) vs a ready AudioPort/GameAudio instance. */\nfunction isAudioSpec(a: unknown): a is AudioSpec {\n return !!a && typeof a === 'object' && Array.isArray((a as AudioSpec).sounds);\n}\n\nconst capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);\n\n/** The buyable/activatable modes (`modes.<key>.buy` / `.boost`) as buy-feature cards — the\n * list the bonus button opens. A plain number mode or one without `buy`/`boost` is not a card.\n * `cost` in config is the FULL play multiplier (× bet); a boost card shows the SURCHARGE it\n * adds over a base spin (`cost − 1`, e.g. a 2× ante → `+1× bet`), so the card + confirm read\n * right while the mode is still charged its full `cost` when it spins. */\nfunction buyFeaturesOf(config: GameConfig): FeatureSpec[] {\n const out: FeatureSpec[] = [];\n for (const [key, m] of Object.entries(config.modes ?? {})) {\n if (!m || typeof m !== 'object' || !(m.buy || m.boost)) continue;\n const variant = m.boost ? 'boost' : 'buy';\n out.push({ id: key, name: m.name ?? capitalize(key), variant, cost: variant === 'boost' ? m.cost - 1 : m.cost, image: m.image });\n }\n return out;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/engine/turbo.ts","../src/engine/round.ts","../src/engine/fsm.ts","../src/engine/phases.ts","../src/engine/beat.ts","../src/game/config.ts","../src/currency/index.ts","../src/loader/index.ts","../src/game/createStakeGame.ts"],"sourcesContent":["// Turbo speed + slam-stop, owned by the core. The HUD's turbo control (a 2- or 3-mode cycler)\n// drives a speed multiplier; the game's Present phase awaits `ctx.turbo.delay(ms)` for its\n// spin/anim durations, so turbo shortens them and a slam-stop (`skipRequested`) resolves them\n// instantly — with ZERO game logic. Compliance waits (minimumRoundDuration) use `ctx.ticker`,\n// NOT this, so they're never sped up or skipped.\n\n/** Delay multipliers per turbo level: off (1×) / turbo (0.4×) / super (0.12×). */\nexport const DEFAULT_TURBO_SPEEDS = [1, 0.4, 0.12];\n\nexport interface TurboState {\n /** 0 = off, 1 = turbo, 2 = super — the HUD turbo cycler's index. */\n readonly level: number;\n /** The EFFECTIVE delay multiplier right now — the turbo level, floored by the autoplay\n * speed while autoplay is running (`ctx.turbo.delay` already applies it). */\n readonly speed: number;\n /** True while autoplay/hold is running — game animations are shortened even at turbo off. */\n readonly autoplay: boolean;\n /** True once the player slam-stopped this round (delays resolve instantly). */\n readonly skipped: boolean;\n /** A turbo- + autoplay- + slam-stop-aware delay for GAME animations. NOT for compliance waits. */\n delay(ms: number): Promise<void>;\n}\n\nexport class TurboClock implements TurboState {\n private readonly speeds: number[];\n /** Delay multiplier applied while autoplay/hold runs (the floor for `speed`). Defaults to\n * the turbo (level-1) speed, so autoplay plays at least at turbo pace even at turbo off. */\n private readonly autoplaySpeed: number;\n private _level = 0;\n private _autoplay = false;\n private _skipped = false;\n private readonly pending = new Set<() => void>();\n\n constructor(speeds?: number[], autoplaySpeed?: number) {\n this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;\n this.autoplaySpeed = autoplaySpeed ?? this.speeds[1] ?? 0.5;\n }\n\n get level(): number {\n return this._level;\n }\n get speed(): number {\n const levelSpeed = this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;\n // Autoplay shortens animations even at turbo off — but never SLOWS a faster turbo level.\n return this._autoplay ? Math.min(levelSpeed, this.autoplaySpeed) : levelSpeed;\n }\n get autoplay(): boolean {\n return this._autoplay;\n }\n get skipped(): boolean {\n return this._skipped;\n }\n\n /** Set the turbo level (clamped to the configured speeds). */\n setLevel(index: number): void {\n const i = Number.isFinite(index) ? Math.trunc(index) : 0;\n this._level = Math.max(0, Math.min(i, this.speeds.length - 1));\n }\n\n /** Mark autoplay/hold as running so `delay()` shortens game animations. The core sets\n * this on autoplayStarted/holdSpinStarted and clears it when they stop. */\n setAutoplay(active: boolean): void {\n this._autoplay = active;\n }\n\n /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */\n skip(): void {\n this._skipped = true;\n const cbs = [...this.pending];\n this.pending.clear();\n for (const cb of cbs) cb();\n }\n\n /** Clear the slam-stop flag — the core calls this at the start of each spin. */\n resetSkip(): void {\n this._skipped = false;\n }\n\n delay(ms: number): Promise<void> {\n if (this._skipped || ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n this.pending.delete(finish);\n resolve();\n };\n const timer = setTimeout(finish, ms * this.speed);\n this.pending.add(finish);\n });\n }\n}\n","// The round model the engine drives. The engine derives all MONEY (multiplier, win,\n// stake) from the raw wire round + the bet; the game's `interpretBook` only parses the\n// book EVENTS into its own model (`data`) — the one place a game touches a round.\n\nimport { API_AMOUNT_MULTIPLIER, BOOK_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\n\n/** Money facts of a settled round (all MAJOR units except `multiplier`, a ratio). */\nexport interface RoundInfo {\n mode: string;\n /** Base bet (before the mode's cost multiplier). */\n bet: number;\n /** Mode cost multiplier (× base bet) — the stake charged. */\n cost: number;\n /** The amount staked = bet × cost. */\n stake: number;\n /** Round multiplier relative to the BASE bet (totalWin / bet). */\n multiplier: number;\n /** Total win credited this round (derived: multiplier × bet). */\n totalWin: number;\n /** The server's AUTHORITATIVE win (`raw.payout`, major units); falls back to totalWin\n * when the round carries no explicit payout. Trust this over `totalWin` when they differ. */\n payout: number;\n}\n\n/**\n * A resolved round: money facts + the game's parsed model + the raw wire round. `E` is the\n * game's book-event type — declared once on `createStakeGame`, so `raw` (and `interpretBook`)\n * are TYPED rather than `unknown`.\n */\nexport interface GameRound<T = unknown, E = unknown> extends RoundInfo {\n /** The game's parsed model, from `interpretBook` — driven to the Present phase. */\n data: T;\n /** Whether the raw round still needs `/wallet/end-round` (the engine settles it). */\n active: boolean;\n /** Authoritative post-settlement balance (major units) — applied by the Settle phase. */\n balance: number;\n /** The raw, fully-typed wire round — the game's real server data. */\n raw: Round<E>;\n}\n\n/** The game's ONE money-logic seam: parse the raw round's (typed) events into your model. Pure. */\nexport type InterpretBook<T, E = unknown> = (raw: Round<E>, info: RoundInfo) => T;\n\n/** Build the money facts from a raw wire round + the base bet + the mode cost. */\nexport function roundInfo<E = unknown>(raw: Round<E>, bet: number, cost: number): RoundInfo {\n // Stake wire convention: `payoutMultiplier` is a plain Payout/Amount ratio (1× → 1)\n // in BOOK units (×100). totalWin is relative to the BASE bet; `payout` is the server's\n // own win amount (API units → major) when the round carries one.\n const multiplier = raw.payoutMultiplier / BOOK_AMOUNT_MULTIPLIER;\n const totalWin = multiplier * bet;\n const payout = raw.payout != null ? raw.payout / API_AMOUNT_MULTIPLIER : totalWin;\n return { mode: raw.mode, bet, cost, stake: bet * cost, multiplier, totalWin, payout };\n}\n","// The round state machine. A round is an explicit sequence of awaited phases\n// (Idle → Spin → Present → Settle); each `enter` does its work + transitions on. The\n// core ships Idle/Spin/Settle; the game writes Present (and may override any). The FSM\n// + context are pure — the HUD/view/audio arrive via ports, so a whole round runs\n// headless (see the InstantTicker + MockNetworkManager).\n\nimport type { NetworkManager } from '../rgs/network';\nimport type { RootStore } from '../stores/index';\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\nimport type { HudPort } from './hud-port';\nimport type { GameRound, InterpretBook } from './round';\nimport type { GameConfig } from '../game/config';\n\n/** A fixed value or a per-voice random spread (`{ base, jitter }`) — structurally matches\n * zvuk's `VoiceJitter` without importing it, so the engine stays zvuk-free. */\nexport type AudioValue = number | { base?: number; jitter?: number };\n\n/** The minimal boot-loader surface a game's phases touch (satisfied by the core's\n * `GameLoader`) — kept structural so the engine needn't import the loader module. */\nexport interface LoaderPort {\n setProgress(p: number): void;\n done(): Promise<void>;\n}\n\n/** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */\nexport interface AudioPort {\n /** Fire a one-shot. `volume`/`pitch` accept jitter so repeated cues vary per voice. */\n play(name: string, opts?: { bus?: string; volume?: AudioValue; pitch?: AudioValue }): void;\n music(name: string, opts?: { fadeIn?: number }): void;\n stopMusic(opts?: { fade?: number }): void;\n}\n\n/** A view MAY implement `onSpinStart` to begin its spin animation the MOMENT a round starts\n * — BEFORE the network request — so the reels move during latency and land on the result in\n * Present. Core calls it once per round inside `SpinPhase`, so a game gets eager-spin by\n * adding one method to its scene; it NEVER overrides the network-sending phase. Purely\n * opt-in: a view without `onSpinStart` just spins when Present lands the result. */\nexport interface SpinStartView {\n onSpinStart?(): void;\n}\n\n/** Handed to every phase. `round` is set by SpinPhase and read by Present/Settle. `E` is\n * the game's book-event type, so `interpretBook`/`round.raw` are typed. */\nexport interface PhaseContext<T = unknown, V = unknown, E = unknown> {\n readonly config: GameConfig;\n readonly stores: RootStore;\n readonly network: NetworkManager;\n readonly hud: HudPort;\n readonly ticker: Ticker;\n /** The game's mounted view (scene/presenter/stores) — whatever `mountView` returned. */\n readonly view: V;\n readonly audio: AudioPort | null;\n /** Turbo speed + slam-stop (core-owned). Use `ctx.turbo.delay(ms)` for spin/anim timing. */\n readonly turbo: TurboState;\n /** The boot loader, if one was configured (else `null`) — drive progress / `done()`. */\n readonly loader: LoaderPort | null;\n readonly interpretBook: InterpretBook<T, E>;\n readonly fsm: FSM<T, V, E>;\n round: GameRound<T, E> | null;\n /** Cost multiplier for a mode key (from `config.modes`, default 1). */\n modeCost(mode: string): number;\n}\n\nexport interface Phase<T = unknown, V = unknown, E = unknown> {\n readonly name: string;\n enter(ctx: PhaseContext<T, V, E>): Promise<void> | void;\n exit?(ctx: PhaseContext<T, V, E>): void;\n}\n\nexport class FSM<T = unknown, V = unknown, E = unknown> {\n private ctx: PhaseContext<T, V, E> | null = null;\n private readonly byName = new Map<string, Phase<T, V, E>>();\n private _current = '';\n\n /** Later phases with the same `name` win — so a game's Present overrides the default. */\n constructor(phases: Phase<T, V, E>[]) {\n for (const p of phases) this.byName.set(p.name, p);\n }\n\n bind(ctx: PhaseContext<T, V, E>): void {\n this.ctx = ctx;\n }\n\n get current(): string {\n return this._current;\n }\n\n has(name: string): boolean {\n return this.byName.has(name);\n }\n\n async transition(name: string): Promise<void> {\n if (!this.ctx) throw new Error('[FSM] not bound to a context');\n const next = this.byName.get(name);\n if (!next) throw new Error(`[FSM] unknown phase: ${name}`);\n this.byName.get(this._current)?.exit?.(this.ctx);\n this._current = name;\n await next.enter(this.ctx);\n }\n}\n","// The default round phases (Idle → Spin → Present → Settle). The game writes Present\n// (animate the round) and may override any of these. Balance/bet flow through the\n// stores (createStakeGame reacts store→HUD); the phases call the HUD directly only for\n// reportRound + errors.\n\nimport { API_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\nimport { roundInfo } from './round';\nimport type { Phase, PhaseContext, SpinStartView } from './fsm';\n\n/** Wait for the next spin (the HUD's `spinRequested` drives the transition to Spin). */\nexport class IdlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'idle';\n enter(ctx: PhaseContext<T, V, E>): void {\n ctx.stores.ui.setSpinning(false);\n }\n}\n\n/** Pick the mode, take the stake, ask the RGS, parse the round, hand off to Present. */\nexport class SpinPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'spin';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const { stores, network, hud } = ctx;\n const mode = stores.ui.nextMode();\n const cost = ctx.modeCost(mode);\n const bet = stores.balance.bet;\n const stake = bet * cost;\n\n stores.ui.setSpinning(true);\n stores.balance.debitStake(stake); // stake leaves now; the win lands at Settle\n\n // Round-start seam (opt-in): let the view begin its spin animation NOW — before the\n // network round-trip — so the reels move during latency and land on the result in\n // Present. The game just implements `onSpinStart()` on its scene; it never overrides\n // this network-sending phase. See `SpinStartView`.\n (ctx.view as SpinStartView | null | undefined)?.onSpinStart?.();\n\n try {\n const play = await network.play({ bet, mode });\n // The transport is game-agnostic (events: unknown); the game declared `E`, so we\n // narrow here — the game owns the shape it parses in `interpretBook`.\n const raw = play.round as Round<E>;\n const info = roundInfo(raw, bet, cost);\n const data = ctx.interpretBook(raw, info);\n // DO NOT /wallet/end-round here. An ACTIVE round is left OPEN so a refresh DURING\n // Present (the game's reel/scene animation) can recover it — SettlePhase settles it\n // once the round has finished playing out. For an active round `play.balance` is\n // post-DEBIT (stake gone, win not yet credited), so the post-win balance the player\n // sees at Settle is play.balance + the (authoritative-from-book) win; SettlePhase then\n // reconciles to the server's own settled figure. A round that is already settled (a\n // loss, or an immediate-settle mock) reports its FINAL balance in play.balance — use\n // it verbatim, or the win would be double-counted.\n const settledMoney = play.balance.amount / API_AMOUNT_MULTIPLIER;\n ctx.round = {\n ...info,\n data,\n active: raw.active ?? false,\n balance: raw.active ? settledMoney + info.totalWin : settledMoney,\n raw,\n };\n } catch (err) {\n stores.balance.refund(stake); // the round never happened\n stores.ui.setSpinning(false);\n const msg = err instanceof Error ? err.message : String(err);\n const code = msg.match(/ERR_[A-Z_]+/)?.[0];\n if (code) hud.showRgsError(code);\n else hud.showError(msg);\n await ctx.fsm.transition('idle');\n return;\n }\n\n await ctx.fsm.transition('present');\n }\n}\n\n/** DEFAULT Present — nothing to animate. The game overrides `present` to play its scene. */\nexport class PresentPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'present';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n await ctx.fsm.transition('settle');\n }\n}\n\n/** Apply the authoritative balance + win, settle the round for the HUD, then Idle. */\nexport class SettlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'settle';\n private roundStartedAt = 0;\n\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const r = ctx.round;\n if (r) {\n ctx.stores.balance.settle(r.balance, r.totalWin);\n ctx.hud.reportRound(r.totalWin, r.bet);\n // minimumRoundDuration: pad to the jurisdiction minimum (best-effort; the boot\n // records the round start, so this covers the whole spin→settle span).\n const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;\n const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1000;\n if (minMs > 0 && elapsed < minMs) await ctx.ticker.delay(minMs - elapsed);\n // The round has now finished playing out visually (Present + the min-duration pad).\n // ONLY NOW settle it on the server — SpinPhase deliberately left it OPEN so a refresh\n // mid-Present recovers the round instead of losing it. The win already shows (from the\n // book); this reconciles to the server's authoritative post-settlement balance.\n if (r.active) {\n try {\n const end = await ctx.network.endRound();\n ctx.stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);\n } catch (err) {\n // The win is already credited locally; a settle hiccup self-heals on the next\n // authenticate (the RGS reports the still-open round). Surface it, don't crash.\n console.warn('[settle] end-round failed; balance reconciles on next authenticate:', err);\n }\n }\n }\n ctx.stores.ui.setSpinning(false);\n await ctx.fsm.transition('idle');\n }\n\n /** Called by SpinPhase-adjacent boot wiring to timestamp the round start. */\n markStart(now: number): void {\n this.roundStartedAt = now;\n }\n}\n\n/** The default phase set (game phases are appended after → same-name overrides win). */\nexport function defaultPhases<T = unknown, V = unknown, E = unknown>(): Phase<T, V, E>[] {\n return [new IdlePhase<T, V, E>(), new SpinPhase<T, V, E>(), new PresentPhase<T, V, E>(), new SettlePhase<T, V, E>()];\n}\n","// A blocking \"announcement beat\" — the pattern behind moments like a bonus trigger or a\n// \"… ELIMINATED!\" banner: pause so the reveal settles, SHOW the announcement, HOLD it\n// alone on screen, HIDE it, then a trailing pause before play resumes. The point is that\n// the round never plays UNDER the announcement — everything waits for the beat.\n//\n// The game owns the visuals (`show`/`hide` — mount a banner, toggle a class, play an sfx);\n// the core owns the timing + sequencing so every game's beats feel consistent.\n\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\n\nexport interface BeatOptions {\n /** Pause before the announcement shows — lets the reveal settle. Default 0. */\n leadMs?: number;\n /** How long the announcement holds alone on screen. */\n holdMs: number;\n /** Pause after hiding, before play resumes. Default 0. */\n trailMs?: number;\n /** Reveal the announcement (banner/sfx). Called after the lead pause. */\n show: () => void;\n /** Hide it. Called after the hold, before the trailing pause. Optional (some banners\n * self-dismiss). */\n hide?: () => void;\n /**\n * Timing source. `'real'` (default) is a fixed, unskippable beat via the ticker — an\n * announcement is a deliberate moment that a slam-stop shouldn't blow past. `'turbo'`\n * makes the pauses turbo-/autoplay-/slam-stop-aware (shorter under turbo, skippable).\n */\n timing?: 'real' | 'turbo';\n}\n\n/** Timing surface the beat needs — a `PhaseContext` satisfies it (`ctx.ticker`/`ctx.turbo`). */\nexport interface BeatClock {\n ticker: Ticker;\n turbo: TurboState;\n}\n\n/**\n * Run a blocking announcement beat: lead → show → hold → hide → trail. Awaitable, so a\n * Present phase just `await blockingBeat(ctx, { … })` and the round pauses for the whole\n * thing. Returns when the beat (including the trailing pause) is over.\n */\nexport async function blockingBeat(ctx: BeatClock, opts: BeatOptions): Promise<void> {\n const wait = opts.timing === 'turbo' ? (ms: number) => ctx.turbo.delay(ms) : (ms: number) => ctx.ticker.delay(ms);\n if (opts.leadMs && opts.leadMs > 0) await wait(opts.leadMs);\n opts.show();\n await wait(opts.holdMs);\n opts.hide?.();\n if (opts.trailMs && opts.trailMs > 0) await wait(opts.trailMs);\n}\n","// The per-game configuration passed to `createStakeGame`. Everything the core needs to\n// stand up the HUD + drive the round; the game's mechanics live in its scene + phases.\n\nexport interface ModeConfig {\n /** Cost multiplier (× base bet). `base` is 1. */\n cost: number;\n /** A one-shot bought bonus — shown as a `Buy` card in the buy-feature modal (opened by the\n * bonus button). Buying it spins this mode once (through the jurisdiction confirm gate). */\n buy?: boolean;\n /** An activatable per-spin bet surcharge — shown as an `Activate` card (vs a one-tap `Buy`). */\n boost?: boolean;\n /** Display name for the feature card (defaults to the capitalized mode key). */\n name?: string;\n /** Card art (URL or data URI) for the feature card. A neutral gradient is used when absent. */\n image?: string;\n}\n\nexport interface GameConfig {\n title: string;\n version?: string;\n /** Fallback currency code; the session/`?currency=` wins. */\n currency?: string;\n /**\n * Theoretical RTP percentage — DISPLAY ONLY (the RTP readout + rules). NOT authoritative:\n * the server (`auth.config.rtp`) wins, and the certified math report is the source of\n * truth. This is only a last-resort fallback; prefer never hand-typing it (drift risk).\n */\n rtp?: number;\n /** Mode key → cost multiplier (a number) or a `ModeConfig`. `base` defaults to 1. */\n modes?: Record<string, ModeConfig | number>;\n /**\n * Minimum selectable bet, in MAJOR units of the account currency — an EXPLICIT client\n * floor. Any server bet-ladder level below this is dropped from the HUD ladder, so the\n * smallest possible win (bet × the game's minimum payout) can never round below one\n * minimal currency unit. Set it per game to your smallest legal stake — e.g. `0.05` when\n * the minimum payout is ×0.2 (0.05 × 0.2 = 0.01 = one cent). `undefined` → derive it from\n * the currency (5 minimal units — a safe default for a ×0.2 minimum payout). The server\n * ladder stays authoritative for the LEVELS offered; this only trims the low end + snaps\n * the default up to the first legal level.\n */\n minBet?: number;\n /** Rules/info menu for the HUD (`@open-slot-ui` `MenuSpec`) — build it with `@stakeplate/core/rules` `buildRules`. */\n rules?: unknown;\n /** i18n messages per locale (`{ en: { key: text }, es: {…} }`) for the HUD + rules text. */\n messages?: Record<string, Record<string, string>>;\n /** SOCIAL/sweepstakes wording per locale — swapped in when social mode is on. Merge in\n * `buildRules().socialEn` so the core's disclaimer/guide are social-safe. */\n socialMessages?: Record<string, Record<string, string>>;\n /** Delay multipliers per turbo level (off / turbo / super). Default `[1, 0.4, 0.12]`. */\n turboSpeeds?: number[];\n /** Pause (ms) between autoplay/hold spins. Default 250. Scales with turbo. */\n autoplayGapMs?: number;\n /** Extra `@open-slot-ui` `UISpec` fields merged into the built spec (escape hatch). */\n spec?: Record<string, unknown>;\n}\n\n// NOTE (server-authoritative): the bet ladder (`betLevels` + `defaultBetLevel`) and the\n// buy-feature confirm threshold are NOT game config — the RGS `authenticate` response owns\n// the ladder (per currency/jurisdiction) and the jurisdiction owns the confirm policy. The\n// core reads them from `auth.config`; a game only declares that a mode is a buy (`modes`).\n\n/** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */\nexport function modeCostOf(config: GameConfig, mode: string): number {\n const m = config.modes?.[mode];\n if (typeof m === 'number') return m;\n if (m && typeof m === 'object') return m.cost;\n return 1;\n}\n","// @stakeplate/core/currency — the Stake Engine currency table, the SINGLE source of\n// truth for how money renders (bet, balance, net, win plaque, buy modal, replay).\n//\n// The lib's `resolveCurrency` knows the common codes but falls back to a 2-dp \"$\"-ish\n// guess for many, and disagrees with the live Stake client on symbol/side for ~15 codes.\n// `currencyFor` layers the approval-doc table below on top of `resolveCurrency` (decimal\n// `.`, NO thousands separator) so every code renders byte-for-byte like the real client.\n//\n// Values come from Stake's \"Supported Currencies\" approval doc, VERIFIED against the live\n// client: fiats are PREFIX except the krone/dinar/rial group (DKK/TND/OMR/QAR/SAR/ILS…),\n// social coins are SUFFIX, and everything is 2-dp except JPY/IDR/KRW/VND/CLP (0-dp) — the\n// Gulf dinars included (KWD/JOD/BHD/TND/OMR render at 2-dp on Stake, NOT the ISO 3). An\n// earlier `CurrencyMeta` snippet disagreed on ~15 codes — it was WRONG; the live client wins.\n\nimport { resolveCurrency, isSocialCurrency, type CurrencySpec } from '@open-slot-ui/core';\n\n/** Per-code symbol / side / precision overrides, applied on top of `resolveCurrency`.\n * Unknown codes fall through to the lib's 2-dp default. */\nexport const CURRENCY_OVERRIDES: Readonly<Record<string, Partial<CurrencySpec>>> = {\n USD: { symbol: '$', position: 'prefix', decimals: 2 },\n CAD: { symbol: 'CA$', position: 'prefix', decimals: 2 },\n JPY: { symbol: '¥', position: 'prefix', decimals: 0 },\n EUR: { symbol: '€', position: 'prefix', decimals: 2 },\n RUB: { symbol: '₽', position: 'prefix', decimals: 2 },\n CNY: { symbol: 'CN¥', position: 'prefix', decimals: 2 },\n PHP: { symbol: '₱', position: 'prefix', decimals: 2 },\n INR: { symbol: '₹', position: 'prefix', decimals: 2 },\n IDR: { symbol: 'Rp', position: 'prefix', decimals: 0 },\n KRW: { symbol: '₩', position: 'prefix', decimals: 0 },\n BRL: { symbol: 'R$', position: 'prefix', decimals: 2 },\n MXN: { symbol: 'MX$', position: 'prefix', decimals: 2 },\n DKK: { symbol: 'KR', position: 'suffix', decimals: 2 }, // approval: uppercase \"KR\", not lib's \"kr\"\n PLN: { symbol: 'zł', position: 'suffix', decimals: 2 },\n VND: { symbol: '₫', position: 'suffix', decimals: 0 },\n TRY: { symbol: '₺', position: 'prefix', decimals: 2 },\n CLP: { symbol: 'CLP', position: 'suffix', decimals: 0 },\n ARS: { symbol: 'ARS', position: 'suffix', decimals: 2 },\n PEN: { symbol: 'S/', position: 'prefix', decimals: 2 },\n NGN: { symbol: '₦', position: 'prefix', decimals: 2 },\n SAR: { symbol: 'SAR', position: 'suffix', decimals: 2 },\n ILS: { symbol: 'ILS', position: 'suffix', decimals: 2 }, // letter code, amount before it (10.00 ILS)\n AED: { symbol: 'AED', position: 'suffix', decimals: 2 },\n TWD: { symbol: 'NT$', position: 'prefix', decimals: 2 },\n NOK: { symbol: 'kr', position: 'prefix', decimals: 2 },\n KWD: { symbol: 'KD', position: 'prefix', decimals: 2 }, // Stake renders dinars at 2-dp, not ISO 3\n JOD: { symbol: 'JD', position: 'prefix', decimals: 2 },\n CRC: { symbol: '₡', position: 'prefix', decimals: 2 },\n TND: { symbol: 'TND', position: 'suffix', decimals: 2 },\n SGD: { symbol: 'SG$', position: 'prefix', decimals: 2 },\n MYR: { symbol: 'RM', position: 'prefix', decimals: 2 },\n OMR: { symbol: 'OMR', position: 'suffix', decimals: 2 },\n QAR: { symbol: 'QAR', position: 'suffix', decimals: 2 },\n BHD: { symbol: 'BD', position: 'prefix', decimals: 2 },\n PKR: { symbol: '₨', position: 'prefix', decimals: 2 },\n EGP: { symbol: 'ج.م', position: 'suffix', decimals: 2 }, // amount before the symbol (10.00 ج.م)\n NZD: { symbol: 'NZ$', position: 'prefix', decimals: 2 },\n BOB: { symbol: 'Bs', position: 'prefix', decimals: 2 },\n GHS: { symbol: 'GH₵', position: 'prefix', decimals: 2 },\n KES: { symbol: 'KSh', position: 'prefix', decimals: 2 },\n MAD: { symbol: 'MAD', position: 'prefix', decimals: 2 },\n BAM: { symbol: 'KM', position: 'prefix', decimals: 2 },\n ISK: { symbol: 'kr', position: 'prefix', decimals: 2 },\n TZS: { symbol: 'TSh', position: 'prefix', decimals: 2 },\n UGX: { symbol: 'USh', position: 'prefix', decimals: 2 },\n XOF: { symbol: 'CFA', position: 'prefix', decimals: 2 },\n // Stake social/sweepstakes coins — coin abbreviation, SUFFIXED (per the doc).\n XGC: { symbol: 'GC', position: 'suffix', decimals: 2 }, // Stake Gold Coin → 10.00 GC\n XSC: { symbol: 'SC', position: 'suffix', decimals: 2 }, // Stake Cash → 10.00 SC\n XEC: { symbol: 'SC', position: 'suffix', decimals: 2 }, // Stake Euro Cash → 10.00 SC\n};\n\n/** Stake social/sweepstakes coins — they render their coin code AND flip the HUD to\n * social wording. The lib's `isSocialCurrency` knows XGC/XSC but not XEC, so recognise\n * the full set here. `createStakeGame` routes its social check through this. */\nconst SOCIAL_CURRENCIES = new Set(['XGC', 'XSC', 'XEC']);\nexport function isSocial(code: string): boolean {\n return SOCIAL_CURRENCIES.has(code ? code.toUpperCase() : '') || isSocialCurrency(code);\n}\n\n/**\n * Resolve a currency code to a `CurrencySpec` that renders exactly like the live Stake\n * client. Applies the approval-doc symbol/side/precision overrides and drops thousands\n * separators (Stake's reference `DisplayBalance` uses `toFixed()`, so `$12345.60`, not\n * `$12,345.60`). Use this EVERYWHERE a code becomes a spec — `createStakeGame` does.\n */\nexport function currencyFor(code: string): CurrencySpec {\n const extra = code ? CURRENCY_OVERRIDES[code.toUpperCase()] : undefined;\n return resolveCurrency(code, { ...extra, separator: '' });\n}\n","// The boot experience — a single overlay that carries the player from LOADING straight into\n// an optional FEATURES splash and then out of the way, all in one continuous element (no\n// flash between screens). `createStakeGame({ loader })` shows it the instant boot starts:\n//\n// LOADING — logo (pulsing) + a progress bar the core advances across the boot milestones\n// (init → auth → HUD → view), over an optional blurred cover image + vignette.\n// FEATURES — (if `features` are configured) once boot is ready the SAME logo glides up to\n// the top, the bar fades out, and 2–3 feature tiles (image + short caption, all\n// scaled to one size) fade + scale in with a \"press to continue\" prompt.\n// REVEAL — a tap / key press (or auto, with no features) fades the overlay away to the\n// game, which is already booted + idle behind it.\n//\n// So the game gets a `loader → features → idle` flow for free — it only writes its `present`\n// phase. Pure DOM + CSS: it paints before pixi/zvuk load and removes itself when done.\n\nlet seq = 0;\n\n/** One feature card on the intro splash — an image with a short (may be multi-line) caption. */\nexport interface FeatureItem {\n /** Image URL / data-URI. Rendered at a uniform height so mismatched art still lines up. */\n image: string;\n /** Short caption under the image. `\\n` for line breaks. */\n text: string;\n}\n\nexport interface LoaderConfig {\n /** Big title under the logo (LOADING phase only). Defaults to the game title. */\n title?: string;\n /** Small line under the title (LOADING phase). */\n subtitle?: string;\n /** URL/data-URI of the logo. Pulses while loading; glides to the top for the features\n * splash. Omit for a CSS ring spinner (and no move-up). */\n logo?: string;\n /** Solid colour behind everything. Default `#0b0d12`. */\n background?: string;\n /** Optional cover image (ideally the game's own backdrop, for a seamless reveal). */\n backgroundImage?: string;\n /** Blur the cover image. Default `true`. */\n blur?: boolean;\n /** Accent colour — progress fill + title glow + caption text. Default `#d99000`. */\n accent?: string;\n /** Minimum time the LOADING phase stays up, so the bar always fills. Default `1200`ms. */\n minDurationMs?: number;\n /** Stacking order. Default a very high value so it sits over the game. */\n zIndex?: number;\n /**\n * If `true`, the core shows the loader + advances progress but does NOT auto-advance past\n * LOADING — the game calls `ctx.loader.done()` itself (e.g. after its scene finishes loading\n * art). Default `false`. (The FEATURES splash always waits for the player regardless.)\n */\n manual?: boolean;\n /** 2–3 feature cards → show a FEATURES splash after loading. Omit for a plain loader. */\n features?: FeatureItem[];\n /** The dismiss prompt under the feature cards. Default `PRESS TO CONTINUE`. */\n continueText?: string;\n}\n\nexport interface GameLoader {\n /** The overlay element (already in the document). */\n readonly el: HTMLElement;\n /** Drive the progress bar. Clamped to the range it has already reached (never goes back). */\n setProgress(p: number): void;\n /** Advance out of LOADING: fill to 100%, honour `minDurationMs`, then either show the\n * FEATURES splash (waiting for the player to continue) or — with no features — pop away.\n * Resolves once the overlay is fully gone. Idempotent (first call wins). */\n done(): Promise<void>;\n /** Remove immediately without any outro (used on a boot error). */\n remove(): void;\n}\n\nconst isBrowser = (): boolean => typeof document !== 'undefined' && !!document.body;\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** Create + mount the boot overlay. Safe to call before anything else in boot. */\nexport function createLoader(config: LoaderConfig = {}): GameLoader {\n const startedAt = Date.now();\n const minMs = config.minDurationMs ?? 1200;\n const accent = config.accent ?? '#d99000';\n const bg = config.background ?? '#0b0d12';\n const z = config.zIndex ?? 2147483000;\n const blur = config.blur ?? true;\n const features = config.features ?? [];\n const hasLogo = !!config.logo;\n let progress = 0;\n let donePromise: Promise<void> | null = null;\n\n // Headless / SSR guard — a no-op handle so callers needn't branch.\n if (!isBrowser()) {\n return { el: null as unknown as HTMLElement, setProgress() {}, done: () => Promise.resolve(), remove() {} };\n }\n\n const id = `sp-loader-${++seq}`;\n const root = document.createElement('div');\n root.id = id;\n root.className = 'sp-loader phase-loading';\n\n const style = document.createElement('style');\n style.textContent = `\n #${id}{position:fixed;inset:0;z-index:${z};background:${config.backgroundImage ? 'transparent' : bg};overflow:hidden;\n font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n opacity:1;transition:opacity .5s ease}\n #${id}.sp-done{opacity:0}\n #${id} .sp-bgimg{position:absolute;inset:-8%;background-position:center;background-size:cover;\n ${blur ? 'filter:blur(6px);' : ''}transform:scale(1.06)}\n #${id} .sp-vign{position:absolute;inset:0;background:radial-gradient(120% 90% at 50% 42%,transparent 40%,rgba(0,0,0,.62) 100%)}\n /* Logo — same element in both phases; glides + shrinks from centre to the top. */\n #${id} .sp-logo{position:absolute;left:50%;top:36%;transform:translate(-50%,-50%);\n width:min(42vw,240px);height:auto;filter:drop-shadow(0 14px 24px rgba(0,0,0,.55));\n animation:sp-pulse 1.5s ease-in-out infinite;\n transition:top .6s cubic-bezier(.5,0,.2,1),width .6s cubic-bezier(.5,0,.2,1)}\n #${id}.phase-features .sp-logo{top:15%;width:min(30vw,180px);animation:none}\n /* LOADING group (title/subtitle + bar) — fades out on the way to features. */\n #${id} .sp-load{position:absolute;left:50%;top:58%;transform:translate(-50%,-50%);\n display:flex;flex-direction:column;align-items:center;gap:14px;\n transition:opacity .35s ease}\n #${id}.phase-features .sp-load{opacity:0;pointer-events:none}\n #${id} .sp-spin{width:60px;height:60px;border-radius:50%;border:5px solid rgba(255,255,255,.18);\n border-top-color:${accent};animation:sp-rot .8s linear infinite}\n #${id} .sp-title{margin:0;color:#fff;font-weight:800;letter-spacing:.02em;\n font-size:clamp(20px,3.6vw,36px);text-align:center;text-shadow:0 3px 10px rgba(0,0,0,.55),0 0 22px ${accent}55}\n #${id} .sp-sub{margin:0;color:#ffffffcc;font-size:clamp(12px,2vw,16px);text-align:center;text-shadow:0 2px 6px rgba(0,0,0,.5)}\n #${id} .sp-bar{position:relative;width:min(60vw,260px);height:8px;border-radius:99px;background:rgba(255,255,255,.16);overflow:hidden}\n #${id} .sp-fill{position:absolute;left:0;top:0;bottom:0;width:0%;border-radius:99px;background:${accent};transition:width .35s ease}\n /* FEATURES splash — tiles + prompt; scales/fades in once boot is ready. */\n #${id} .sp-feat{position:absolute;left:0;right:0;top:30%;bottom:0;display:flex;flex-direction:column;\n align-items:center;justify-content:flex-start;gap:5vh;padding:0 4vw;\n opacity:0;transform:scale(.94);pointer-events:none;transition:opacity .5s ease,transform .5s ease}\n #${id}.phase-features .sp-feat{opacity:1;transform:scale(1);pointer-events:auto}\n #${id} .sp-tiles{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;\n gap:clamp(18px,5vw,64px);max-width:94vw}\n #${id} .sp-tile{display:flex;flex-direction:column;align-items:center;gap:14px;\n width:clamp(110px,22vw,210px);opacity:0;transform:translateY(16px) scale(.9);\n transition:opacity .45s ease,transform .45s cubic-bezier(.34,1.56,.64,1)}\n #${id}.phase-features .sp-tile{opacity:1;transform:none}\n #${id} .sp-tile img{height:clamp(66px,13vh,150px);width:auto;max-width:100%;object-fit:contain;\n filter:drop-shadow(0 8px 14px rgba(0,0,0,.45))}\n #${id} .sp-tile span{color:${accent};font-weight:800;text-transform:uppercase;letter-spacing:.02em;\n text-align:center;white-space:pre-line;line-height:1.15;font-size:clamp(13px,2.1vw,21px);\n text-shadow:0 2px 6px rgba(0,0,0,.6)}\n #${id} .sp-cont{margin-top:auto;margin-bottom:5vh;color:#ffffffcc;text-transform:uppercase;\n letter-spacing:.16em;font-size:clamp(11px,1.6vw,15px);animation:sp-blink 1.7s ease-in-out infinite}\n @keyframes sp-pulse{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.06)}}\n @keyframes sp-rot{to{transform:rotate(360deg)}}\n @keyframes sp-blink{0%,100%{opacity:.4}50%{opacity:1}}\n @media (prefers-reduced-motion:reduce){#${id} .sp-logo,#${id} .sp-spin,#${id} .sp-cont{animation:none}}\n `;\n\n if (config.backgroundImage) {\n const img = document.createElement('div');\n img.className = 'sp-bgimg';\n img.style.backgroundImage = `url(${config.backgroundImage})`;\n root.appendChild(img);\n const vign = document.createElement('div');\n vign.className = 'sp-vign';\n root.appendChild(vign);\n }\n\n if (hasLogo) {\n const logo = document.createElement('img');\n logo.className = 'sp-logo';\n logo.src = config.logo!;\n logo.alt = '';\n root.appendChild(logo);\n }\n\n // LOADING group.\n const load = document.createElement('div');\n load.className = 'sp-load';\n if (!hasLogo) {\n const spin = document.createElement('div');\n spin.className = 'sp-spin';\n load.appendChild(spin);\n }\n if (config.title) {\n const h = document.createElement('h1');\n h.className = 'sp-title';\n h.textContent = config.title;\n load.appendChild(h);\n }\n if (config.subtitle) {\n const p = document.createElement('p');\n p.className = 'sp-sub';\n p.textContent = config.subtitle;\n load.appendChild(p);\n }\n const bar = document.createElement('div');\n bar.className = 'sp-bar';\n const fill = document.createElement('div');\n fill.className = 'sp-fill';\n bar.appendChild(fill);\n load.appendChild(bar);\n root.appendChild(load);\n\n // FEATURES group (built only when configured).\n if (features.length) {\n const feat = document.createElement('div');\n feat.className = 'sp-feat';\n const tiles = document.createElement('div');\n tiles.className = 'sp-tiles';\n features.forEach((f, i) => {\n const tile = document.createElement('div');\n tile.className = 'sp-tile';\n tile.style.transitionDelay = `${0.12 + i * 0.12}s`; // staggered entrance\n const img = document.createElement('img');\n img.src = f.image;\n img.alt = '';\n const cap = document.createElement('span');\n cap.textContent = f.text;\n tile.append(img, cap);\n tiles.appendChild(tile);\n });\n const cont = document.createElement('div');\n cont.className = 'sp-cont';\n cont.textContent = config.continueText ?? 'PRESS TO CONTINUE';\n feat.append(tiles, cont);\n root.appendChild(feat);\n }\n\n root.appendChild(style);\n document.body.appendChild(root);\n\n const setProgress = (p: number): void => {\n const next = Math.max(progress, Math.min(1, Number.isFinite(p) ? p : 0));\n progress = next;\n fill.style.width = `${Math.round(next * 100)}%`;\n };\n\n const remove = (): void => {\n root.remove();\n style.remove();\n // Clean up the build-time boot backdrop injected by the `stakeplateBoot` Vite plugin\n // (if present) — the game is now covering it.\n document.getElementById('sp-bootbg')?.remove();\n document.getElementById('sp-boot-style')?.remove();\n };\n\n // Transition LOADING → FEATURES, then resolve once the player dismisses it.\n const runFeatures = (): Promise<void> =>\n new Promise((resolve) => {\n root.classList.remove('phase-loading');\n root.classList.add('phase-features');\n const dismiss = (): void => {\n root.removeEventListener('pointerdown', dismiss);\n window.removeEventListener('keydown', onKey);\n resolve();\n };\n const onKey = (e: KeyboardEvent): void => { if (e.key === ' ' || e.key === 'Enter') dismiss(); };\n // Arm dismissal only after the splash has animated in (so a stray click can't skip it).\n setTimeout(() => {\n root.addEventListener('pointerdown', dismiss);\n window.addEventListener('keydown', onKey);\n }, 500);\n });\n\n const done = (): Promise<void> => {\n if (donePromise) return donePromise;\n donePromise = (async () => {\n setProgress(1);\n const wait = minMs - (Date.now() - startedAt);\n if (wait > 0) await sleep(wait);\n if (features.length) await runFeatures(); // hold on the splash until the player continues\n root.classList.add('sp-done'); // fade the whole overlay out\n await sleep(520);\n remove();\n })();\n return donePromise;\n };\n\n return { el: root, setProgress, done, remove };\n}\n","// createStakeGame — the one-call façade. `start()` runs the whole compliant boot with\n// zero game involvement: runtime → network → HUD → (replay | authenticate → configure\n// HUD → resume active round → wire events) → run the FSM. Connection/auth failure →\n// the themed, blocking showBootError. The game supplies only config + interpretBook +\n// mountView (+ optional Present phase, audio). This is the ONLY module that imports the\n// HUD/pixi peers; the engine stays decoupled behind ports.\n\nimport { Application } from 'pixi.js';\nimport { mountHud, showBootError, mountBuyFeatureModal, type BootedHud, type FeatureSpec } from '@open-slot-ui/pixi';\nimport { loadBuiltinArt } from '@open-slot-ui/pixi/art';\nimport { currencyFor, isSocial } from '../currency';\nimport { reaction } from 'mobx';\nimport { readRuntime, type RuntimeConfig } from '../rgs/runtime';\nimport { createNetwork, type NetworkManager } from '../rgs/network';\nimport { API_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\nimport { RootStore } from '../stores/index';\nimport { RealTicker, type Ticker } from '../engine/ticker';\nimport { TurboClock, type TurboState } from '../engine/turbo';\nimport { FSM, type AudioPort, type Phase, type PhaseContext } from '../engine/fsm';\nimport { defaultPhases } from '../engine/phases';\nimport { roundInfo, type InterpretBook } from '../engine/round';\nimport { bindMixerToHud, startMusicOnFirstGesture, type MixerLike } from '../audio/bind';\nimport { bindInputSounds, type InputSoundMap } from '../audio/inputs';\nimport type { GameAudioOptions, SoundEntry } from '../audio';\nimport { createLoader, type GameLoader, type LoaderConfig } from '../loader';\nimport { modeCostOf, type GameConfig } from './config';\n\n/**\n * Declarative audio: hand the core your sounds and it lazily creates the `@schmooky/zvuk`\n * mixer (master → music/sfx/ambience buses + ducking), preloads them, and auto-binds it to\n * the HUD (sliders/mute + unlock). zvuk loads in its OWN async chunk — a game without sound\n * pays nothing. (Advanced: pass a ready `GameAudio` instance instead, for custom buses/FX.)\n */\nexport interface AudioSpec extends GameAudioOptions {\n sounds: SoundEntry[];\n /** Optional: play a cue on HUD input events (spin/bet/autoplay/turbo/toggle/skip). The\n * core auto-binds these after the HUD mounts — the named sounds must be in `sounds`. */\n inputSounds?: InputSoundMap;\n /** Name of a `kind:'music'` entry in `sounds` — the core auto-starts it (unlocking the\n * AudioContext) on the FIRST user interaction. The game never starts music itself. */\n bgm?: string;\n /** Fade-in (seconds) for the auto-started `bgm`. Default 0.8. */\n bgmFadeIn?: number;\n}\n\n/** Passed to `mountView` — everything the game's scene needs (not the round/fsm yet). */\nexport interface ViewContext {\n config: GameConfig;\n stores: RootStore;\n hud: BootedHud;\n ticker: Ticker;\n audio: AudioPort | null;\n /** Turbo speed + slam-stop (core-owned) — the scene may branch on `turbo.level`/`speed`. */\n turbo: TurboState;\n /** The boot loader (if `loader` was configured) — drive `setProgress` while your scene\n * loads art, and (with `loader.manual`) call `done()` when the scene is ready. `null`\n * when no loader is used. */\n loader: GameLoader | null;\n}\n\nexport interface CreateStakeGameOptions<T = unknown, V = unknown, E = unknown> {\n config: GameConfig;\n /** The game's ONE money seam: typed RGS round (`Round<E>`) → your model. Pure. */\n interpretBook: InterpretBook<T, E>;\n /** Mount the game's pixi scene/presenter/stores; returns the view handed to phases. */\n mountView: (host: HTMLElement, ctx: ViewContext) => V;\n /** The game's Present phase (+ any overrides); Idle/Spin/Settle are provided. */\n phases?: Phase<T, V, E>[];\n /** Sounds (declarative — the core builds + wires the mixer) OR a ready `GameAudio`/AudioPort. */\n audio?: AudioPort | AudioSpec | null;\n /** Override the transport (tests / a supplied mock). */\n network?: NetworkManager;\n /** Override launch params (tests / embedding). */\n runtime?: Partial<RuntimeConfig>;\n /** Host for the pixi HUD canvas (default document.body). */\n hudHost?: HTMLElement;\n /** Host for the game scene (default = hudHost). */\n sceneHost?: HTMLElement;\n /** Passthrough to `mountHud` (spinSkin, icons, gsap, menu:false, hooks, …). */\n hudOptions?: Record<string, unknown>;\n /**\n * A configurable boot loader — shows the instant boot starts, advances across the boot\n * milestones, then fills + pops away when the game is ready. Pass a {@link LoaderConfig}\n * to enable it (title defaults to `config.title`); omit or `false` to render none (the\n * game keeps its own HTML loader). With `manual: true`, call `ctx.loader.done()` yourself\n * (e.g. after the scene's art finishes loading).\n */\n loader?: LoaderConfig | false;\n}\n\n/**\n * Stake policy: buys/activations costing more than this many × base bet require a confirm\n * (no one-click). A buy-feature always costs far more than 2×, so in practice this means\n * \"always confirm a buy\". The jurisdiction (`auth.config.jurisdiction`) may override it; a\n * game never sets it — it's compliance, not design.\n */\nconst STAKE_CONFIRM_ABOVE_COST = 2;\n\n/** Fallback min-payout coefficient used ONLY when `config.minBet` is unset: the\n * currency-derived floor is `minUnit / MIN_PAYOUT_COEF` (= 5 minimal units at 0.2). */\nconst MIN_PAYOUT_COEF = 0.2;\n\n/** Persisted player UI preferences (sound mute + turbo mode). Best-effort — every access\n * is guarded: Safari Private Browsing, a cross-origin-iframe storage block, or a hardened\n * session throws on access, so this degrades to \"no persistence\", never into the boot. */\nconst PREFS_KEY = 'stakeplate.prefs';\ninterface GamePrefs {\n muted?: boolean;\n turbo?: number;\n}\nfunction readPrefs(): GamePrefs {\n try {\n return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}') as GamePrefs;\n } catch {\n return {};\n }\n}\nfunction writePrefs(patch: GamePrefs): void {\n try {\n localStorage.setItem(PREFS_KEY, JSON.stringify({ ...readPrefs(), ...patch }));\n } catch {\n /* storage disabled / full — preferences just won't persist this session */\n }\n}\n\n/** Drop server bet-ladder levels below the minimum bet — the explicit `config.minBet`, else\n * the currency-derived floor (5 minimal units) — so the smallest win can't round below one\n * minimal unit. Never returns empty (keeps the largest level if every level is below). */\nfunction applyMinBet(levels: number[], minBet: number | undefined, decimals: number): number[] {\n const minUnit = 10 ** -decimals;\n const floor = minBet ?? minUnit / MIN_PAYOUT_COEF;\n const filtered = levels.filter((b) => b >= floor - minUnit / 1000);\n return filtered.length ? filtered : [Math.max(...levels)];\n}\n\n/** Snap a wanted bet up to the first ladder level that is >= it (or the closest). */\nfunction snapToLadder(levels: number[], want: number): number {\n if (levels.includes(want)) return want;\n return levels.find((b) => b >= want) ?? levels[levels.length - 1] ?? want;\n}\n\n/** Server/replay-sourced values that drive the HUD spec — never taken from the game config. */\ninterface HudSpecInput {\n currency: string;\n betLevels: number[]; // major units — the authoritative ladder\n defaultBet: number; // major units\n rtp: number; // display %\n confirmBuyAboveCost: number; // jurisdiction policy\n}\n\n/** A read-only snapshot of the running game — for the dev harness, tests and debugging. */\nexport interface GameSnapshot {\n phase: string;\n balance: number;\n bet: number;\n lastWin: number;\n currency: string;\n spinning: boolean;\n}\n\nexport interface StakeGame {\n start(): Promise<void>;\n dispose(): void;\n /** Current phase + store values. The declarative harness asserts on this (no screenshots). */\n inspect(): GameSnapshot;\n /** Trigger a spin the same way the HUD spin button does — only from Idle. For harness/tests. */\n requestSpin(): boolean;\n}\n\nexport function createStakeGame<T = unknown, V = unknown, E = unknown>(opts: CreateStakeGameOptions<T, V, E>): StakeGame {\n const runtime = readRuntime({ overrides: opts.runtime });\n const network = opts.network ?? createNetwork(runtime);\n const stores = new RootStore();\n const ticker = new RealTicker();\n const turbo = new TurboClock(opts.config.turboSpeeds); // core-owned turbo speed + slam-stop\n // A ready AudioPort instance is used as-is; an AudioSpec is resolved lazily in start()\n // (the core creates the zvuk mixer from its own async chunk) — see resolveAudio().\n let audio: AudioPort | null = opts.audio && !isAudioSpec(opts.audio) ? opts.audio : null;\n const app = new Application();\n const disposers: Array<() => void> = [];\n let hud: BootedHud | null = null;\n let fsm: FSM<T, V, E> | null = null;\n let loader: GameLoader | null = null;\n\n // The battery wires the library's DESIGNED default icon set (the white Figma coins +\n // rotating-arrows spin skin) so every game gets the intended HUD out of the box — not\n // the lib's no-art placeholder geometry. `loadBuiltinArt` pulls the whole /art bundle\n // once; a game overrides any icon or the spin skin via `hudOptions.icons`/`spinSkin`.\n let builtinArt: ReturnType<typeof loadBuiltinArt> | null = null;\n const hudOpts = async (): Promise<Record<string, unknown>> => {\n builtinArt ??= loadBuiltinArt();\n const art = await builtinArt;\n const user = (opts.hudOptions ?? {}) as { icons?: Record<string, unknown>; spinSkin?: unknown };\n return {\n spinSkin: art.spinSkin,\n ...user,\n icons: { ...(art.icons as Record<string, unknown>), ...(user.icons ?? {}) },\n };\n };\n\n // The HUD spec is driven by SERVER-authoritative values (the ladder + confirm policy come\n // from `authenticate`/jurisdiction, not the game). `buildSpec` just formats them.\n const buildSpec = (s: HudSpecInput, extra?: { hideBalance?: boolean }): Record<string, unknown> => ({\n currency: currencyFor(s.currency),\n betLadder: { levels: s.betLevels, index: Math.max(0, s.betLevels.indexOf(s.defaultBet)) },\n rtp: s.rtp,\n game: { name: opts.config.title, version: opts.config.version ?? '1.0.0' },\n // Stake owns fullscreen in its iframe — the game must not render its own. The bonus\n // (buy-feature) button shows only when a mode is a buy/boost card — it opens the feature list.\n // Replay mode has no wallet (it's a recorded round) — Stake approval: show NO balance\n // readout; the round's facts live in the replay panel instead.\n controls: {\n fullscreen: { hidden: true },\n ...(buyFeaturesOf(opts.config).length ? { bonus: { hidden: false } } : {}),\n ...(extra?.hideBalance ? { balance: { hidden: true } } : {}),\n },\n // Compliance: the buy-feature confirm threshold is jurisdiction policy, not a game knob.\n buyFeature: { confirmAboveCost: s.confirmBuyAboveCost },\n // We do the round lock OURSELVES (targeted: autoplay/buy/menu/bet dim, but SPIN and the mute\n // button stay live), so disable the library's blanket lock which would also dim mute.\n lockDuringSpin: false,\n ...(opts.config.rules ? { menu: opts.config.rules } : {}),\n locale: {\n locale: runtime.language,\n messages: opts.config.messages ?? { en: {} },\n ...(opts.config.socialMessages ? { socialMessages: opts.config.socialMessages } : {}),\n },\n ...(opts.config.spec ?? {}),\n });\n\n const initApp = async (host: HTMLElement): Promise<void> => {\n await app.init({ resizeTo: window, backgroundAlpha: 0, antialias: true, resolution: Math.min(window.devicePixelRatio || 1, 2), autoDensity: true });\n host.appendChild(app.canvas);\n };\n\n // Build the mixer from a declarative AudioSpec. The `@stakeplate/core/audio` module (the\n // ONLY thing that pulls @schmooky/zvuk) is loaded via a DYNAMIC import, so it lands in its\n // own async chunk — an audio-less game never fetches zvuk. Sounds preload in the background.\n const resolveAudio = async (): Promise<void> => {\n if (!isAudioSpec(opts.audio)) return;\n const { createGameAudio } = await import('../audio');\n const mixer = createGameAudio(opts.audio);\n void mixer.load(opts.audio.sounds);\n audio = mixer;\n };\n\n async function start(): Promise<void> {\n const hudHost = opts.hudHost ?? document.body;\n const sceneHost = opts.sceneHost ?? hudHost;\n // Boot loader: paint it BEFORE anything heavy loads, then advance it across the milestones\n // below (init → auth → HUD → view) and fill + pop it away once the game is ready. `false`\n // (the default) renders none, so a game keeps whatever loader its HTML already has.\n if (opts.loader) loader = createLoader({ title: opts.config.title, ...opts.loader });\n const manualLoader = !!(opts.loader && opts.loader.manual);\n const phases = [...defaultPhases<T, V, E>(), ...(opts.phases ?? [])];\n const machine = (fsm = new FSM<T, V, E>(phases)); // outer `fsm` powers inspect()/requestSpin()\n loader?.setProgress(0.1);\n await resolveAudio(); // if `audio` is an AudioSpec, build the zvuk mixer (lazy chunk) + preload\n\n // ── REPLAY (Stake ?replay=true): fetch a round + play it back read-only ──────\n if (runtime.replay.active && network.replay) {\n try {\n await initApp(hudHost);\n loader?.setProgress(0.5);\n const cur = runtime.currency;\n const bet = runtime.replay.amount || 1; // replay carries its own bet; no ladder needed\n hud = mountHud(app, buildSpec({ currency: cur, betLevels: [bet], defaultBet: bet, rtp: opts.config.rtp ?? 96, confirmBuyAboveCost: STAKE_CONFIRM_ABOVE_COST }, { hideBalance: true }), (await hudOpts()) as never);\n stores.session.set({ currency: cur });\n stores.balance.setBalance(0);\n stores.balance.setBet(bet);\n if (runtime.social) hud.setSocial(true);\n loader?.setProgress(0.85);\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo, loader });\n hud.setReplay(true);\n hud.lockInput();\n const cost = modeCostOf(opts.config, runtime.replay.mode);\n const raw = (await network.replay({ ...runtime.replay })) as Round<E>;\n const info = roundInfo(raw, bet, cost);\n const ctx = makeCtx(machine, view);\n ctx.round = { ...info, data: opts.interpretBook(raw, info), active: false, balance: 0, raw };\n const replayInfo = { baseBet: bet, costMultiplier: cost, payoutMultiplier: info.multiplier, amount: info.totalWin, currency: currencyFor(cur) };\n const playRound = async (): Promise<void> => {\n stores.ui.setSpinning(true);\n await machine.transition('present'); // game's Present plays it back → settle → idle\n hud!.replayEnd(replayInfo, () => void playRound());\n };\n hud.replayStart(replayInfo, () => void playRound());\n if (!manualLoader) void loader?.done();\n } catch (err) {\n loader?.remove();\n showBootError({ title: 'Cannot load the replay', message: 'The recorded round could not be loaded. Please reload to try again.', detail: errMsg(err) });\n }\n return;\n }\n\n // ── NORMAL BOOT ─────────────────────────────────────────────────────────────\n let auth;\n try {\n await initApp(hudHost);\n loader?.setProgress(0.35);\n auth = await network.authenticate();\n loader?.setProgress(0.6);\n } catch (err) {\n loader?.remove();\n showBootError({\n title: 'Cannot reach the game server',\n message: 'The game could not connect to or authenticate with the game server. Please reload to try again.',\n detail: errMsg(err),\n });\n return;\n }\n\n // Everything the HUD needs is SERVER-authoritative (from `authenticate`): the bet\n // ladder + default bet (per currency/jurisdiction), RTP, and the buy-confirm policy.\n const currency = auth.balance.currency;\n const juris = auth.config.jurisdiction ?? {};\n const rtp = auth.config.rtp ?? opts.config.rtp ?? 96;\n // Server ladder, trimmed by the client min-bet floor (explicit `config.minBet`, else the\n // currency-derived 5-minimal-units) so the smallest possible win can't round to sub-unit.\n const rawLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);\n const betLevels = applyMinBet(rawLevels, opts.config.minBet, currencyFor(currency).decimals ?? 2);\n const active = auth.round;\n const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;\n // Wanted default (restored from the active bet on resume), snapped up to the first legal\n // (floored) level so the ladder + store agree.\n const wantedDefault = active?.active\n ? active.amount / API_AMOUNT_MULTIPLIER / activeCost // resume: restore bet from the active amount\n : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER;\n const defaultBet = snapToLadder(betLevels, wantedDefault);\n const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;\n\n hud = mountHud(app, buildSpec({ currency, betLevels, defaultBet, rtp, confirmBuyAboveCost }), (await hudOpts()) as never);\n hud.setCurrency(currencyFor(currency));\n hud.applyJurisdiction(juris);\n if (runtime.social || isSocial(currency)) hud.setSocial(true);\n\n stores.session.set({\n sessionId: runtime.sessionId,\n currency,\n rtp,\n availableBets: betLevels,\n jurisdiction: juris,\n });\n stores.balance.setBet(defaultBet);\n hud.setBet(defaultBet);\n loader?.setProgress(0.8);\n\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo, loader });\n const ctx = makeCtx(machine, view);\n loader?.setProgress(0.95);\n\n // reactions store→HUD + HUD events\n disposers.push(reaction(() => stores.balance.balance, (b) => hud!.setBalance(b), { fireImmediately: false }));\n // Round lock (targeted): while a round is in flight, DIM + disable autoplay, buy, menu and the\n // bet steppers, and animate the spin button; SPIN stays tappable (slam-stop) and the mute\n // button stays live so the player can still cut the sound. At settle we re-enable them (bet ±\n // respect the ladder bounds; autoplay's own active state is left alone).\n disposers.push(reaction(() => stores.ui.spinning, (sp) => {\n const u = hud!.ui;\n if (sp) {\n u.spin.busy();\n if (!u.autoplay.isActive) u.autoplay.disable();\n u.bonusButton.disable();\n u.settingsButton.disable();\n u.betPlus.disable();\n u.betMinus.disable();\n } else {\n u.spin.idle();\n u.autoplay.enable();\n u.bonusButton.enable();\n u.settingsButton.enable();\n u.betStepper.canInc ? u.betPlus.enable() : u.betPlus.disable();\n u.betStepper.canDec ? u.betMinus.enable() : u.betMinus.disable();\n }\n }, { fireImmediately: false }));\n // The bet stepper is the base-bet source of truth. It emits `valueChanged` with\n // `id: 'bet-stepper'` and the ladder value (never the boosted display), so this feeds the\n // BASE bet even while an ante is active. (The lib mirrors it into `ui.bet`; the boost\n // reaction then overwrites the readout with the effective stake.)\n disposers.push(hud.on('valueChanged', (p) => { const v = p as { id?: string; value?: number }; if (v?.id === 'bet-stepper' && typeof v.value === 'number') stores.balance.setBet(v.value); }));\n // ── Spin triggers + turbo speed + autoplay (ALL core-owned) ─────────────────\n // One entry point for every spin: clears the slam-stop flag, then spins from idle.\n const beginSpin = (): void => {\n if (machine.current !== 'idle') return;\n turbo.resetSkip();\n void machine.transition('spin');\n };\n let holdActive = false; // press-and-hold turbo spin\n disposers.push(hud.on('spinRequested', beginSpin));\n // Autoplay/hold: the HUD owns the count picker, remaining count + RG limits (via\n // reportRound); the CORE runs the loop. Start on the first tick, then re-spin below.\n // Autoplay/hold both shorten game animations (turbo.setAutoplay) so long auto sessions\n // don't crawl. It's a floor on turbo speed — a faster turbo level still wins.\n disposers.push(hud.on('autoplayStarted', () => { turbo.setAutoplay(true); beginSpin(); }));\n disposers.push(hud.on('autoplayStopped', () => { turbo.setAutoplay(holdActive); }));\n disposers.push(hud.on('holdSpinStarted', () => { holdActive = true; turbo.setAutoplay(true); beginSpin(); }));\n disposers.push(hud.on('holdSpinStopped', () => { holdActive = false; turbo.setAutoplay(hud!.ui.autoplay.isActive); }));\n // Turbo speed (2-/3-mode cycler) + slam-stop (tap-to-skip the reels).\n disposers.push(hud.on('turboChanged', (p) => { const e = p as { index?: number }; if (typeof e.index === 'number') { turbo.setLevel(e.index); writePrefs({ turbo: e.index }); } }));\n disposers.push(hud.on('skipRequested', () => turbo.skip()));\n // Buy-feature: the bonus button opens the lib's feature-LIST modal (a card per buyable\n // mode). Two kinds of card, both routed through the jurisdiction confirm gate (no one-click\n // above the threshold):\n // • `buy` → one-shot. On confirm, spin that mode ONCE (SpinPhase charges its full cost).\n // • `boost` → a persistent ante. Activating sets it as the ACTIVE mode, so every following\n // base spin plays it at its full `cost×` (via `nextMode()`); toggling off restores base.\n // The modal owns opening/closing/confirm + single-activation; the core supplies the cards +\n // the onBuy/onActivate hooks and reflects the boosted stake in the bet readout.\n const features = buyFeaturesOf(opts.config);\n if (features.length) {\n // The bet readout shows the EFFECTIVE stake (base × the active ante's cost, emphasised)\n // while a boost is on, and the plain base bet otherwise — re-applied on bet/mode change.\n const applyBetDisplay = (): void => {\n const boost = stores.ui.activeMode;\n const cost = boost ? modeCostOf(opts.config, boost) : 1;\n hud!.ui.bet.set(stores.balance.bet * cost);\n (hud!.ui.bet as unknown as { setEmphasis?: (on: boolean) => void }).setEmphasis?.(cost !== 1);\n };\n disposers.push(\n mountBuyFeatureModal(app, hud, features, {\n activation: 'single', // one ante at a time (Stake)\n // Read the BASE bet straight from the stepper CONTROL (updated synchronously by\n // inc()/dec()) — NOT `stores.balance.bet`, which the core updates a tick later off\n // `valueChanged`, so the modal would lag one step (first +/- press \"doesn't\n // register\", +then- re-fires +). The readout still shows the boosted stake.\n getBet: () => hud!.ui.betStepper.value,\n onBuy: (id) => {\n if (machine.current !== 'idle') return;\n // Buying plays at the BASE bet — clear any active ante so its multiplied\n // (emphasised, yellow) bet readout reverts to the plain base bet first.\n if (stores.ui.activeMode) {\n stores.ui.setActiveMode(null);\n applyBetDisplay();\n }\n stores.ui.setOneShotMode(id);\n beginSpin();\n },\n onActivate: (ids) => {\n stores.ui.setActiveMode(ids[0] ?? null); // persistent ante; nextMode() prefers it\n applyBetDisplay();\n },\n }),\n );\n disposers.push(reaction(() => [stores.balance.bet, stores.ui.activeMode] as const, applyBetDisplay));\n }\n // Keep spinning while autoplay/hold is active: after each settled round (spinning\n // true → false), once idle, pause the (turbo-scaled) gap and spin again — stopping on\n // a blocking notice/error or when the next base stake is unaffordable.\n const autoplayGapMs = opts.config.autoplayGapMs ?? 250;\n disposers.push(\n reaction(\n () => stores.ui.spinning,\n (spinning, prev) => {\n if (prev !== true || spinning !== false) return;\n void turbo.delay(autoplayGapMs).then(() => {\n if (machine.current !== 'idle') return;\n if (!hud!.ui.autoplay.isActive && !holdActive) return;\n if (hud!.ui.noticeBlocks.get().length > 0 || stores.balance.balance < stores.balance.bet) {\n hud!.ui.autoplay.stop();\n holdActive = false;\n return;\n }\n beginSpin();\n });\n },\n ),\n );\n\n // Auto-wire audio ↔ HUD (Music/Effects sliders + mute, persisted) — IF a mixer-like `audio`\n // was provided. Structural check, no @schmooky/zvuk import here, so audio-less games don't\n // bundle it. When the AudioSpec names a `bgm`, the CORE starts it (+ unlocks the context) on\n // the first user gesture — the game never touches music. Otherwise we just unlock on the first\n // spin. (A game may bind manually instead.)\n const mixer = audio as unknown as MixerLike | null;\n if (mixer && typeof mixer.setGroupLevel === 'function') {\n disposers.push(bindMixerToHud(mixer, hud));\n const bgm = isAudioSpec(opts.audio) ? opts.audio.bgm : undefined;\n if (bgm && audio) {\n disposers.push(startMusicOnFirstGesture(audio, mixer, hud, { bgm, fadeIn: isAudioSpec(opts.audio) ? opts.audio.bgmFadeIn : undefined }));\n } else if (typeof mixer.unlock === 'function') {\n const off = hud.on('spinRequested', () => { void mixer.unlock!(); off(); });\n disposers.push(off);\n }\n }\n // Input sounds: if the declarative AudioSpec named cues per HUD input, wire them now.\n if (audio && isAudioSpec(opts.audio) && opts.audio.inputSounds) {\n disposers.push(bindInputSounds(audio, hud, opts.audio.inputSounds));\n }\n\n // ── Persist player prefs (sound mute + turbo mode) across sessions ──────────\n // Restore BEFORE the game is interactive so the controls boot in the saved position\n // (setMuted also propagates to the audio mixer via bindMixerToHud's muted subscription\n // above). turboChanged below persists the turbo mode; anon/blocked-storage is safe.\n const prefs = readPrefs();\n if (typeof prefs.muted === 'boolean') hud.setMuted(prefs.muted);\n if (typeof prefs.turbo === 'number' && prefs.turbo > 0) {\n hud.ui.turbo.setIndex(prefs.turbo);\n turbo.setLevel(prefs.turbo);\n }\n disposers.push(hud.ui.muted.subscribe((m) => writePrefs({ muted: m })));\n\n // ── Menu Settings switches (created by the pixi menu): Speed (turbo) + Sound (mute) ──\n // Wire both ways so the switches, the HUD turbo button and the mute state stay in sync.\n type SwitchCtl = { set(on: boolean): void };\n const speedSwitch = hud.ui.control('speed') as unknown as SwitchCtl | undefined;\n const soundSwitch = hud.ui.control('sound') as unknown as SwitchCtl | undefined;\n if (speedSwitch) {\n speedSwitch.set(hud.ui.turbo.isOn);\n disposers.push(hud.ui.turbo.index.subscribe(() => speedSwitch.set(hud!.ui.turbo.isOn)));\n }\n if (soundSwitch) {\n soundSwitch.set(!hud.ui.muted.get());\n disposers.push(hud.ui.muted.subscribe((m) => soundSwitch.set(!m)));\n }\n disposers.push(hud.on('toggled', (p) => {\n const e = p as { id?: string; on?: boolean };\n if (typeof e.on !== 'boolean') return;\n if (e.id === 'speed') {\n const idx = e.on ? 1 : 0;\n hud!.ui.turbo.setIndex(idx);\n turbo.setLevel(idx);\n writePrefs({ turbo: idx });\n } else if (e.id === 'sound') {\n hud!.setMuted(!e.on);\n }\n }));\n\n // ── ACTIVE-ROUND RESUME: settle it, land idle, then a \"round in progress\" modal ──\n if (active?.active) {\n // The player refreshed while a round was still open. Settle it now (balance becomes\n // authoritative; the bet was restored into the ladder above), land on a clean idle\n // board, then show a NON-dismissible modal — on Continue we replay the recovered\n // round through the game's Present phase so the player watches how it resolved before\n // regaining control. (Not awaited — start() proceeds to fill/pop the loader; the modal\n // shows over the game once the loader fades.)\n const end = await network.endRound();\n const raw = active as Round<E>;\n const info = roundInfo(raw, defaultBet, activeCost);\n const settledBal = end.balance.amount / API_AMOUNT_MULTIPLIER;\n stores.balance.setBalance(settledBal);\n const resumeRound = { ...info, data: opts.interpretBook(raw, info), active: false, balance: settledBal, raw };\n await machine.transition('idle');\n hud.showFatal('You have an unfinished round. Continue to see how it ends.', {\n title: 'Round in progress',\n tone: 'info',\n actions: [\n {\n label: 'Continue',\n variant: 'primary',\n onSelect: () => {\n hud!.hideNotice();\n hud!.lockInput(); // no spins/buys while the recovered round replays\n ctx.round = resumeRound;\n void machine.transition('present').finally(() => hud!.unlockInput());\n },\n },\n ],\n });\n } else {\n stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);\n await machine.transition('idle');\n }\n // The game is booted + interactive → fill + pop the loader away (unless the game asked\n // to drive it manually, e.g. to wait for its scene art via `ctx.loader.done()`).\n if (!manualLoader) void loader?.done();\n }\n\n function makeCtx(fsm: FSM<T, V, E>, view: V): PhaseContext<T, V, E> {\n const ctx: PhaseContext<T, V, E> = {\n config: opts.config,\n stores,\n network,\n hud: hud as BootedHud,\n ticker,\n view,\n audio,\n turbo,\n loader,\n interpretBook: opts.interpretBook,\n fsm,\n round: null,\n modeCost: (mode) => modeCostOf(opts.config, mode),\n };\n fsm.bind(ctx);\n return ctx;\n }\n\n function dispose(): void {\n for (const d of disposers.splice(0)) d();\n hud?.dispose();\n app.destroy(true);\n }\n\n function inspect(): GameSnapshot {\n return {\n phase: fsm?.current ?? 'boot',\n balance: stores.balance.balance,\n bet: stores.balance.bet,\n lastWin: stores.balance.lastWin,\n currency: stores.session.currency,\n spinning: stores.ui.spinning,\n };\n }\n\n function requestSpin(): boolean {\n if (!fsm || fsm.current !== 'idle') return false;\n void fsm.transition('spin');\n return true;\n }\n\n return { start, dispose, inspect, requestSpin };\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** An AudioSpec (declarative sounds) vs a ready AudioPort/GameAudio instance. */\nfunction isAudioSpec(a: unknown): a is AudioSpec {\n return !!a && typeof a === 'object' && Array.isArray((a as AudioSpec).sounds);\n}\n\nconst capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);\n\n/** The buyable/activatable modes (`modes.<key>.buy` / `.boost`) as buy-feature cards — the\n * list the bonus button opens. A plain number mode or one without `buy`/`boost` is not a card.\n * `cost` in config is the FULL play multiplier (× bet); a boost card shows the SURCHARGE it\n * adds over a base spin (`cost − 1`, e.g. a 2× ante → `+1× bet`), so the card + confirm read\n * right while the mode is still charged its full `cost` when it spins. */\nfunction buyFeaturesOf(config: GameConfig): FeatureSpec[] {\n const out: FeatureSpec[] = [];\n for (const [key, m] of Object.entries(config.modes ?? {})) {\n if (!m || typeof m !== 'object' || !(m.buy || m.boost)) continue;\n const variant = m.boost ? 'boost' : 'buy';\n out.push({ id: key, name: m.name ?? capitalize(key), variant, cost: variant === 'boost' ? m.cost - 1 : m.cost, image: m.image });\n }\n return out;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stakeplate/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Batteries-included Stake Engine game core — RGS transport, boot, round FSM, HUD binding, audio, i18n and a compliant rules builder. Bring a scene, a Present phase and a pure interpretBook; the core does the rest.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stake",
|