@stakeplate/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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/game/config.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 delay multiplier for the current level (`ctx.turbo.delay` already applies it). */\n readonly speed: number;\n /** True once the player slam-stopped this round (delays resolve instantly). */\n readonly skipped: boolean;\n /** A turbo- + 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 private _level = 0;\n private _skipped = false;\n private readonly pending = new Set<() => void>();\n\n constructor(speeds?: number[]) {\n this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;\n }\n\n get level(): number {\n return this._level;\n }\n get speed(): number {\n return this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;\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 /** 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/** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */\nexport interface AudioPort {\n play(name: string, opts?: { bus?: string; volume?: number }): void;\n music(name: string, opts?: { fadeIn?: number }): void;\n stopMusic(opts?: { fade?: number }): 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 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 } 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 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 let settledApi = play.balance.amount;\n if (raw.active) settledApi = (await network.endRound()).balance.amount;\n const info = roundInfo(raw, bet, cost);\n const data = ctx.interpretBook(raw, info);\n ctx.round = {\n ...info,\n data,\n active: raw.active ?? false,\n balance: settledApi / API_AMOUNT_MULTIPLIER,\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 }\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","// 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 /** 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","// 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 { resolveCurrency, isSocialCurrency } from '@open-slot-ui/core';\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, type MixerLike } from '../audio/bind';\nimport type { GameAudioOptions, SoundEntry } from '../audio';\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}\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}\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\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/** 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\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): Record<string, unknown> => ({\n currency: resolveCurrency(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 controls: { fullscreen: { hidden: true }, ...(buyFeaturesOf(opts.config).length ? { bonus: { hidden: false } } : {}) },\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 const phases = [...defaultPhases<T, V, E>(), ...(opts.phases ?? [])];\n const machine = (fsm = new FSM<T, V, E>(phases)); // outer `fsm` powers inspect()/requestSpin()\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 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 }), (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 const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo });\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: resolveCurrency(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 } catch (err) {\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 auth = await network.authenticate();\n } catch (err) {\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 const betLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);\n const active = auth.round;\n const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;\n const defaultBet = 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 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(resolveCurrency(currency));\n hud.applyJurisdiction(juris);\n if (runtime.social || isSocialCurrency(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\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo });\n const ctx = makeCtx(machine, view);\n\n // reactions store→HUD + HUD events\n disposers.push(reaction(() => stores.balance.balance, (b) => hud!.setBalance(b), { 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 disposers.push(hud.on('autoplayStarted', beginSpin));\n disposers.push(hud.on('holdSpinStarted', () => { holdActive = true; beginSpin(); }));\n disposers.push(hud.on('holdSpinStopped', () => { holdActive = false; }));\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); }));\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 getBet: () => stores.balance.bet, // the base bet — the readout shows the boosted stake\n onBuy: (id) => {\n if (machine.current !== 'idle') return;\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) + unlock on the first\n // spin gesture — IF a mixer-like `audio` was provided. Structural check, no @schmooky/zvuk\n // import here, so audio-less games don't bundle it. (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 if (typeof mixer.unlock === 'function') {\n const off = hud.on('spinRequested', () => { void mixer.unlock!(); off(); });\n disposers.push(off);\n }\n }\n\n // ── ACTIVE-ROUND RESUME: settle it + play it back, else idle ────────────────\n if (active?.active) {\n const end = await network.endRound();\n const raw = active as Round<E>;\n const info = roundInfo(raw, defaultBet, activeCost);\n stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);\n ctx.round = { ...info, data: opts.interpretBook(raw, info), active: false, balance: end.balance.amount / API_AMOUNT_MULTIPLIER, raw };\n await machine.transition('present'); // view plays it back → settle → idle\n } else {\n stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);\n await machine.transition('idle');\n }\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 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/** 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 } 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 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 let settledApi = play.balance.amount;\n if (raw.active) settledApi = (await network.endRound()).balance.amount;\n const info = roundInfo(raw, bet, cost);\n const data = ctx.interpretBook(raw, info);\n ctx.round = {\n ...info,\n data,\n active: raw.active ?? false,\n balance: settledApi / API_AMOUNT_MULTIPLIER,\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 }\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 /** 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 — currency resolution with the 3-decimal fiat the\n// @open-slot-ui table doesn't (yet) carry patched in.\n//\n// The lib's `resolveCurrency` knows the common codes (USD 2dp, JPY 0dp, BTC 8dp, the\n// Kuwaiti/Bahraini/Jordanian dinars KWD/BHD/JOD 3dp, the Stake social coins, …). For a\n// code it DOESN'T know it falls back to 2 decimals — which silently truncates the sub-unit\n// of the other three-decimal currencies. That matters: e.g. the Omani Rial's minimal\n// stake 0.01 at the lowest ×0.2 coefficient is a 0.002 win, which renders as \"0.00\" at\n// two decimals. `currencyFor` passes the correct precision (+ a sensible symbol) through\n// `resolveCurrency`'s overrides so the full digits show end-to-end (bet, balance, net,\n// win plaque, buy modal, replay).\n\nimport { resolveCurrency, type CurrencySpec } from '@open-slot-ui/core';\n\n/** Three-decimal fiat missing from the @open-slot-ui currency table — the Gulf/Arab\n * dinars & rials (the lib already covers KWD/BHD/JOD). `symbol` follows the lib's short\n * latinised convention for the region (KWD→\"KD\", …). */\nexport const EXTRA_DECIMALS: Readonly<Record<string, { decimals: number; symbol: string }>> = {\n OMR: { decimals: 3, symbol: 'OMR' }, // Omani Rial\n TND: { decimals: 3, symbol: 'DT' }, // Tunisian Dinar\n LYD: { decimals: 3, symbol: 'LD' }, // Libyan Dinar\n IQD: { decimals: 3, symbol: 'IQD' }, // Iraqi Dinar\n};\n\n/**\n * `resolveCurrency`, but with the missing 3-decimal dinars/rials patched in. Use this\n * everywhere a currency code becomes a `CurrencySpec` so precision is correct for every\n * code — `createStakeGame` routes all of its resolution through here.\n */\nexport function currencyFor(code: string): CurrencySpec {\n const extra = code ? EXTRA_DECIMALS[code.toUpperCase()] : undefined;\n return extra ? resolveCurrency(code, extra) : resolveCurrency(code);\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 { isSocialCurrency } from '@open-slot-ui/core';\nimport { currencyFor } 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, 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}\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/** 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): 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 controls: { fullscreen: { hidden: true }, ...(buyFeaturesOf(opts.config).length ? { bonus: { hidden: false } } : {}) },\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 }), (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 const betLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);\n const active = auth.round;\n const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;\n const defaultBet = 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 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 || isSocialCurrency(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 // 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); }));\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 getBet: () => stores.balance.bet, // the base bet — the readout shows the boosted stake\n onBuy: (id) => {\n if (machine.current !== 'idle') return;\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) + unlock on the first\n // spin gesture — IF a mixer-like `audio` was provided. Structural check, no @schmooky/zvuk\n // import here, so audio-less games don't bundle it. (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 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 // ── ACTIVE-ROUND RESUME: settle it + play it back, else idle ────────────────\n if (active?.active) {\n const end = await network.endRound();\n const raw = active as Round<E>;\n const info = roundInfo(raw, defaultBet, activeCost);\n stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);\n ctx.round = { ...info, data: opts.interpretBook(raw, info), active: false, balance: end.balance.amount / API_AMOUNT_MULTIPLIER, raw };\n await machine.transition('present'); // view plays it back → settle → idle\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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"}
@@ -38,6 +38,30 @@ function bindMixerToHud(mixer, hud, opts = {}) {
38
38
  };
39
39
  }
40
40
  //#endregion
41
- export { bindMixerToHud as t };
41
+ //#region src/audio/inputs.ts
42
+ /**
43
+ * Wire the HUD input events to sounds. Returns a disposer. Safe to call with a partial map —
44
+ * only the mapped inputs subscribe.
45
+ */
46
+ function bindInputSounds(audio, hud, map, opts = {}) {
47
+ const bus = opts.bus ?? "ui";
48
+ const play = (name) => {
49
+ if (name) audio.play(name, { bus });
50
+ };
51
+ const d = [];
52
+ if (map.spin) d.push(hud.on("spinRequested", () => play(map.spin)));
53
+ if (map.autoplay) d.push(hud.on("autoplayStarted", () => play(map.autoplay)));
54
+ if (map.skip) d.push(hud.on("skipRequested", () => play(map.skip)));
55
+ if (map.bet) d.push(hud.on("valueChanged", (p) => {
56
+ if (p?.id === "bet-stepper") play(map.bet);
57
+ }));
58
+ if (map.turbo) d.push(hud.on("turboChanged", () => play(map.turbo)));
59
+ if (map.toggle) d.push(hud.on("toggled", () => play(map.toggle)));
60
+ return () => {
61
+ for (const dispose of d.splice(0)) dispose();
62
+ };
63
+ }
64
+ //#endregion
65
+ export { bindMixerToHud as n, bindInputSounds as t };
42
66
 
43
- //# sourceMappingURL=bind-BBRxizWR.js.map
67
+ //# sourceMappingURL=inputs-X3Tc_HTV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inputs-X3Tc_HTV.js","names":["v"],"sources":["../src/audio/bind.ts","../src/audio/inputs.ts"],"sourcesContent":["// The mixer↔HUD binding, with NO @schmooky/zvuk dependency — so `createStakeGame` can\n// auto-wire audio without pulling zvuk into every game's bundle. It talks to the mixer\n// through a minimal structural port (`MixerLike`), which @stakeplate/core/audio's\n// `GameAudio` satisfies. `@open-slot-ui/pixi` is imported type-only (erased at build).\n\nimport type { BootedHud } from '@open-slot-ui/pixi';\n\n/** The two volume groups the HUD's two sliders drive. */\nexport type MixerGroup = 'music' | 'effects';\n\n/**\n * The minimal mixer surface the HUD binding needs. The HUD has exactly two sliders —\n * **Music** and **Effects** — so the port is group-level, not per-bus. GameAudio satisfies\n * it structurally.\n */\nexport interface MixerLike {\n /** Set a group's level (0..1) — applies to every bus in the group. */\n setGroupLevel(group: MixerGroup, level: number): void;\n /** Read a group's level (average across its buses). */\n getGroupLevel(group: MixerGroup): number;\n /** Mute/unmute everything (the Sound toggle). */\n setMuted(muted: boolean): void;\n /** Resume the AudioContext from a user gesture. Present on GameAudio. */\n unlock?(): Promise<void> | void;\n}\n\n/**\n * Bind the HUD sound controls to a mixer: the **Music slider → `music` group**, the\n * **Effects slider → `effects` group** (both persisted to localStorage + restored), and the\n * **Sound toggle → mute** everything. Returns a disposer.\n */\nexport function bindMixerToHud(mixer: MixerLike, hud: BootedHud, opts: { storageKey?: string } = {}): () => void {\n const key = opts.storageKey ?? 'stakeplate.mixer';\n const save = (): void => {\n try {\n localStorage.setItem(key, JSON.stringify({ music: mixer.getGroupLevel('music'), effects: mixer.getGroupLevel('effects') }));\n } catch {\n /* storage may be unavailable */\n }\n };\n try {\n const saved = JSON.parse(localStorage.getItem(key) || '{}') as { music?: number; effects?: number };\n if (typeof saved.music === 'number') mixer.setGroupLevel('music', saved.music);\n if (typeof saved.effects === 'number') mixer.setGroupLevel('effects', saved.effects);\n } catch {\n /* ignore */\n }\n\n const disposers: Array<() => void> = [];\n disposers.push(\n hud.on('valueChanged', (p) => {\n const v = p as { id?: string; value?: number };\n if (typeof v?.value !== 'number') return;\n // HUD control ids: 'music' → music group, 'sfx' (the Effects slider) → effects group.\n if (v.id === 'music') { mixer.setGroupLevel('music', v.value); save(); }\n if (v.id === 'sfx') { mixer.setGroupLevel('effects', v.value); save(); }\n }),\n );\n disposers.push(hud.ui.muted.subscribe((m: boolean) => mixer.setMuted(m)));\n return () => { for (const d of disposers.splice(0)) d(); };\n}\n","// Input sounds — bind the HUD's control events to one-shot cues so every button press,\n// bet step, autoplay start, turbo toggle and slam-stop makes a sound, with zero per-game\n// wiring. Purely the HUD event surface (no zvuk here); the sounds must already be loaded on\n// the mixer. `createStakeGame` calls this for you when an `AudioSpec.inputSounds` map is\n// given; call it directly to wire a custom set.\n\nimport type { BootedHud } from '@open-slot-ui/pixi';\nimport type { AudioPort } from '../engine/fsm';\n\n/** Cue name per HUD input event. Any omitted → that input stays silent. */\nexport interface InputSoundMap {\n /** Spin button press (and each autoplay/hold re-spin fires `spinRequested`). */\n spin?: string;\n /** Bet +/- (the bet stepper's `valueChanged`). */\n bet?: string;\n /** Autoplay engaged (count picked). */\n autoplay?: string;\n /** Turbo cycler toggled. */\n turbo?: string;\n /** Any switch toggled (turbo/sound/…) — a generic UI click. */\n toggle?: string;\n /** Slam-stop (tap-to-skip the spin). */\n skip?: string;\n}\n\nexport interface InputSoundOptions {\n /** Bus to route the cues to (default `'ui'`). */\n bus?: string;\n}\n\n/**\n * Wire the HUD input events to sounds. Returns a disposer. Safe to call with a partial map —\n * only the mapped inputs subscribe.\n */\nexport function bindInputSounds(\n audio: AudioPort,\n hud: BootedHud,\n map: InputSoundMap,\n opts: InputSoundOptions = {},\n): () => void {\n const bus = opts.bus ?? 'ui';\n const play = (name?: string): void => { if (name) audio.play(name, { bus }); };\n const d: Array<() => void> = [];\n\n if (map.spin) d.push(hud.on('spinRequested', () => play(map.spin)));\n if (map.autoplay) d.push(hud.on('autoplayStarted', () => play(map.autoplay)));\n if (map.skip) d.push(hud.on('skipRequested', () => play(map.skip)));\n if (map.bet) {\n d.push(hud.on('valueChanged', (p: unknown) => {\n const v = p as { id?: string };\n if (v?.id === 'bet-stepper') play(map.bet);\n }));\n }\n if (map.turbo) d.push(hud.on('turboChanged', () => play(map.turbo)));\n if (map.toggle) d.push(hud.on('toggled', () => play(map.toggle)));\n\n return () => { for (const dispose of d.splice(0)) dispose(); };\n}\n"],"mappings":";;;;;;AA+BA,SAAgB,eAAe,OAAkB,KAAgB,OAAgC,CAAC,GAAe;CAC/G,MAAM,MAAM,KAAK,cAAc;CAC/B,MAAM,aAAmB;EACvB,IAAI;GACF,aAAa,QAAQ,KAAK,KAAK,UAAU;IAAE,OAAO,MAAM,cAAc,OAAO;IAAG,SAAS,MAAM,cAAc,SAAS;GAAE,CAAC,CAAC;EAC5H,QAAQ,CAER;CACF;CACA,IAAI;EACF,MAAM,QAAQ,KAAK,MAAM,aAAa,QAAQ,GAAG,KAAK,IAAI;EAC1D,IAAI,OAAO,MAAM,UAAU,UAAU,MAAM,cAAc,SAAS,MAAM,KAAK;EAC7E,IAAI,OAAO,MAAM,YAAY,UAAU,MAAM,cAAc,WAAW,MAAM,OAAO;CACrF,QAAQ,CAER;CAEA,MAAM,YAA+B,CAAC;CACtC,UAAU,KACR,IAAI,GAAG,iBAAiB,MAAM;EAC5B,MAAM,IAAI;EACV,IAAI,OAAO,GAAG,UAAU,UAAU;EAElC,IAAI,EAAE,OAAO,SAAS;GAAE,MAAM,cAAc,SAAS,EAAE,KAAK;GAAG,KAAK;EAAG;EACvE,IAAI,EAAE,OAAO,OAAO;GAAE,MAAM,cAAc,WAAW,EAAE,KAAK;GAAG,KAAK;EAAG;CACzE,CAAC,CACH;CACA,UAAU,KAAK,IAAI,GAAG,MAAM,WAAW,MAAe,MAAM,SAAS,CAAC,CAAC,CAAC;CACxE,aAAa;EAAE,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC,GAAG,EAAE;CAAG;AAC3D;;;;;;;AC1BA,SAAgB,gBACd,OACA,KACA,KACA,OAA0B,CAAC,GACf;CACZ,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,QAAQ,SAAwB;EAAE,IAAI,MAAM,MAAM,KAAK,MAAM,EAAE,IAAI,CAAC;CAAG;CAC7E,MAAM,IAAuB,CAAC;CAE9B,IAAI,IAAI,MAAM,EAAE,KAAK,IAAI,GAAG,uBAAuB,KAAK,IAAI,IAAI,CAAC,CAAC;CAClE,IAAI,IAAI,UAAU,EAAE,KAAK,IAAI,GAAG,yBAAyB,KAAK,IAAI,QAAQ,CAAC,CAAC;CAC5E,IAAI,IAAI,MAAM,EAAE,KAAK,IAAI,GAAG,uBAAuB,KAAK,IAAI,IAAI,CAAC,CAAC;CAClE,IAAI,IAAI,KACN,EAAE,KAAK,IAAI,GAAG,iBAAiB,MAAe;EAE5C,IAAIA,GAAG,OAAO,eAAe,KAAK,IAAI,GAAG;CAC3C,CAAC,CAAC;CAEJ,IAAI,IAAI,OAAO,EAAE,KAAK,IAAI,GAAG,sBAAsB,KAAK,IAAI,KAAK,CAAC,CAAC;CACnE,IAAI,IAAI,QAAQ,EAAE,KAAK,IAAI,GAAG,iBAAiB,KAAK,IAAI,MAAM,CAAC,CAAC;CAEhE,aAAa;EAAE,KAAK,MAAM,WAAW,EAAE,OAAO,CAAC,GAAG,QAAQ;CAAG;AAC/D"}
package/dist/vite.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ //#region src/vite.d.ts
2
+ interface StakeplateBootOptions {
3
+ /** Path to the loader background image (the same one you pass to `loader.backgroundImage`),
4
+ * relative to the Vite project root (or absolute). PNG/JPEG/WebP — anything `sharp` reads. */
5
+ background: string;
6
+ /** Solid colour painted behind the placeholder (and the fallback when `sharp` is absent).
7
+ * Match `loader.background`. Default `#0b0d12`. */
8
+ backgroundColor?: string;
9
+ /** CSS blur radius (px) applied to the inlined placeholder. Default `28`. */
10
+ blur?: number;
11
+ /** Downscaled placeholder width (px) — smaller = tinier HTML. Default `64`. */
12
+ size?: number;
13
+ }
14
+ /** A structural Vite `Plugin` (typed here so the core needn't depend on `vite`). */
15
+ interface StakeplateBootPlugin {
16
+ name: string;
17
+ enforce: 'pre';
18
+ configResolved(config: {
19
+ root: string;
20
+ }): void;
21
+ transformIndexHtml(html: string): Promise<{
22
+ html: string;
23
+ tags: HtmlTag[];
24
+ }>;
25
+ }
26
+ interface HtmlTag {
27
+ tag: string;
28
+ attrs?: Record<string, string>;
29
+ children?: string;
30
+ injectTo: 'head' | 'head-prepend' | 'body' | 'body-prepend';
31
+ }
32
+ /**
33
+ * Inline a blurred placeholder of the loader background into index.html so the first paint
34
+ * shows the backdrop (no black flash), then the loader blurs its full image up over it.
35
+ */
36
+ declare function stakeplateBoot(options: StakeplateBootOptions): StakeplateBootPlugin;
37
+ //#endregion
38
+ export { StakeplateBootOptions, StakeplateBootPlugin, stakeplateBoot };
39
+ //# sourceMappingURL=vite.d.ts.map
package/dist/vite.js ADDED
@@ -0,0 +1,57 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ //#region src/vite.ts
4
+ /** Generate a tiny blurred base64 placeholder from the image, or `null` if `sharp` is absent. */
5
+ async function makeLqip(imgPath, size) {
6
+ try {
7
+ return `data:image/jpeg;base64,${(await (await import("sharp")).default(imgPath).resize(size, null, { fit: "inside" }).jpeg({ quality: 55 }).toBuffer()).toString("base64")}`;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ /**
13
+ * Inline a blurred placeholder of the loader background into index.html so the first paint
14
+ * shows the backdrop (no black flash), then the loader blurs its full image up over it.
15
+ */
16
+ function stakeplateBoot(options) {
17
+ const color = options.backgroundColor ?? "#0b0d12";
18
+ const blur = options.blur ?? 28;
19
+ const size = options.size ?? 64;
20
+ let root = process.cwd();
21
+ return {
22
+ name: "stakeplate-boot",
23
+ enforce: "pre",
24
+ configResolved(config) {
25
+ root = config.root;
26
+ },
27
+ async transformIndexHtml(html) {
28
+ const imgPath = isAbsolute(options.background) ? options.background : resolve(root, options.background);
29
+ let lqip = null;
30
+ try {
31
+ await readFile(imgPath);
32
+ lqip = await makeLqip(imgPath, size);
33
+ } catch {
34
+ lqip = null;
35
+ }
36
+ const tags = [{
37
+ tag: "style",
38
+ attrs: { id: "sp-boot-style" },
39
+ children: lqip ? `html{background:${color}}#sp-bootbg{position:fixed;inset:0;z-index:0;background:${color} url("${lqip}") center/cover no-repeat;filter:blur(${blur}px);transform:scale(1.08);pointer-events:none}` : `html{background:${color}}`,
40
+ injectTo: "head-prepend"
41
+ }];
42
+ if (lqip) tags.push({
43
+ tag: "div",
44
+ attrs: { id: "sp-bootbg" },
45
+ injectTo: "body-prepend"
46
+ });
47
+ return {
48
+ html,
49
+ tags
50
+ };
51
+ }
52
+ };
53
+ }
54
+ //#endregion
55
+ export { stakeplateBoot };
56
+
57
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","names":[],"sources":["../src/vite.ts"],"sourcesContent":["// @stakeplate/core/vite — a build helper so a game's index.html needs ZERO background setup\n// and there's no black flash on first paint.\n//\n// The problem: the loader (and its backdrop) are created by JS, which only runs after the\n// module bundle downloads + parses. Until then the page is blank/black. This plugin bakes a\n// tiny BLURRED placeholder of the loader background straight INTO index.html at build time,\n// so the very first paint — before any JS — already shows the (blurred) backdrop. The loader\n// then fades its full-resolution image up over the same spot: a seamless blur-up, no flash.\n//\n// import { stakeplateBoot } from '@stakeplate/core/vite';\n// export default defineConfig({ plugins: [stakeplateBoot({ background: 'src/art/bg.png' })] });\n//\n// The placeholder is a ~64px image (a few KB, base64-inlined — no extra request) under a CSS\n// blur, generated with `sharp` if it's installed (add it as a devDependency for the blur-up).\n// Without sharp it still injects the solid `backgroundColor`, so there is never a black flash —\n// you just don't get the blurred image until the loader paints it.\n\nimport { readFile } from 'node:fs/promises';\nimport { resolve, isAbsolute } from 'node:path';\n\nexport interface StakeplateBootOptions {\n /** Path to the loader background image (the same one you pass to `loader.backgroundImage`),\n * relative to the Vite project root (or absolute). PNG/JPEG/WebP — anything `sharp` reads. */\n background: string;\n /** Solid colour painted behind the placeholder (and the fallback when `sharp` is absent).\n * Match `loader.background`. Default `#0b0d12`. */\n backgroundColor?: string;\n /** CSS blur radius (px) applied to the inlined placeholder. Default `28`. */\n blur?: number;\n /** Downscaled placeholder width (px) — smaller = tinier HTML. Default `64`. */\n size?: number;\n}\n\n/** A structural Vite `Plugin` (typed here so the core needn't depend on `vite`). */\nexport interface StakeplateBootPlugin {\n name: string;\n enforce: 'pre';\n configResolved(config: { root: string }): void;\n transformIndexHtml(html: string): Promise<{ html: string; tags: HtmlTag[] }>;\n}\ninterface HtmlTag {\n tag: string;\n attrs?: Record<string, string>;\n children?: string;\n injectTo: 'head' | 'head-prepend' | 'body' | 'body-prepend';\n}\n\n/** Generate a tiny blurred base64 placeholder from the image, or `null` if `sharp` is absent. */\nasync function makeLqip(imgPath: string, size: number): Promise<string | null> {\n try {\n // Non-literal specifier so TS doesn't type-resolve the OPTIONAL `sharp` (games without it\n // still get the solid-colour fallback — never a black flash).\n const spec = 'sharp';\n const mod = (await import(spec)) as { default: (p: string) => SharpLike };\n const buf = await mod.default(imgPath).resize(size, null, { fit: 'inside' }).jpeg({ quality: 55 }).toBuffer();\n return `data:image/jpeg;base64,${buf.toString('base64')}`;\n } catch {\n return null; // sharp not installed / unreadable — caller falls back to the solid colour\n }\n}\ninterface SharpLike {\n resize(w: number, h: null, o: { fit: 'inside' }): SharpLike;\n jpeg(o: { quality: number }): SharpLike;\n toBuffer(): Promise<Buffer>;\n}\n\n/**\n * Inline a blurred placeholder of the loader background into index.html so the first paint\n * shows the backdrop (no black flash), then the loader blurs its full image up over it.\n */\nexport function stakeplateBoot(options: StakeplateBootOptions): StakeplateBootPlugin {\n const color = options.backgroundColor ?? '#0b0d12';\n const blur = options.blur ?? 28;\n const size = options.size ?? 64;\n let root = process.cwd();\n\n return {\n name: 'stakeplate-boot',\n enforce: 'pre',\n configResolved(config) {\n root = config.root;\n },\n async transformIndexHtml(html) {\n const imgPath = isAbsolute(options.background) ? options.background : resolve(root, options.background);\n let lqip: string | null = null;\n try {\n await readFile(imgPath); // fail fast with a clear reason if the path is wrong\n lqip = await makeLqip(imgPath, size);\n } catch {\n lqip = null;\n }\n // `html { background }` guarantees no black even before the placeholder decodes; the\n // #sp-bootbg layer carries the blurred image (removed by the loader once it's done).\n const css = lqip\n ? `html{background:${color}}` +\n `#sp-bootbg{position:fixed;inset:0;z-index:0;background:${color} url(\"${lqip}\") center/cover no-repeat;` +\n `filter:blur(${blur}px);transform:scale(1.08);pointer-events:none}`\n : `html{background:${color}}`;\n const tags: HtmlTag[] = [{ tag: 'style', attrs: { id: 'sp-boot-style' }, children: css, injectTo: 'head-prepend' }];\n if (lqip) tags.push({ tag: 'div', attrs: { id: 'sp-bootbg' }, injectTo: 'body-prepend' });\n return { html, tags };\n },\n };\n}\n"],"mappings":";;;;AAgDA,eAAe,SAAS,SAAiB,MAAsC;CAC7E,IAAI;EAMF,OAAO,2BAA0B,OADf,MADC,OAAO,SAAA,CACJ,QAAQ,OAAO,CAAC,CAAC,OAAO,MAAM,MAAM,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,SAAS,EAAA,CACvE,SAAS,QAAQ;CACxD,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAWA,SAAgB,eAAe,SAAsD;CACnF,MAAM,QAAQ,QAAQ,mBAAmB;CACzC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,IAAI,OAAO,QAAQ,IAAI;CAEvB,OAAO;EACL,MAAM;EACN,SAAS;EACT,eAAe,QAAQ;GACrB,OAAO,OAAO;EAChB;EACA,MAAM,mBAAmB,MAAM;GAC7B,MAAM,UAAU,WAAW,QAAQ,UAAU,IAAI,QAAQ,aAAa,QAAQ,MAAM,QAAQ,UAAU;GACtG,IAAI,OAAsB;GAC1B,IAAI;IACF,MAAM,SAAS,OAAO;IACtB,OAAO,MAAM,SAAS,SAAS,IAAI;GACrC,QAAQ;IACN,OAAO;GACT;GAQA,MAAM,OAAkB,CAAC;IAAE,KAAK;IAAS,OAAO,EAAE,IAAI,gBAAgB;IAAG,UAL7D,OACR,mBAAmB,MAAM,0DACiC,MAAM,QAAQ,KAAK,wCAC9D,KAAK,kDACpB,mBAAmB,MAAM;IAC2D,UAAU;GAAe,CAAC;GAClH,IAAI,MAAM,KAAK,KAAK;IAAE,KAAK;IAAO,OAAO,EAAE,IAAI,YAAY;IAAG,UAAU;GAAe,CAAC;GACxF,OAAO;IAAE;IAAM;GAAK;EACtB;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stakeplate/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",
@@ -64,6 +64,11 @@
64
64
  "types": "./dist/rules.d.ts",
65
65
  "import": "./dist/rules.js",
66
66
  "default": "./dist/rules.js"
67
+ },
68
+ "./vite": {
69
+ "types": "./dist/vite.d.ts",
70
+ "import": "./dist/vite.js",
71
+ "default": "./dist/vite.js"
67
72
  }
68
73
  },
69
74
  "files": [
@@ -84,8 +89,8 @@
84
89
  "typecheck": "tsc --noEmit"
85
90
  },
86
91
  "peerDependencies": {
87
- "@open-slot-ui/core": ">=0.8.2",
88
- "@open-slot-ui/pixi": ">=0.8.2",
92
+ "@open-slot-ui/core": ">=0.8.3",
93
+ "@open-slot-ui/pixi": ">=0.8.3",
89
94
  "@schmooky/zvuk": ">=1.13",
90
95
  "pixi.js": "^8"
91
96
  },
@@ -107,12 +112,13 @@
107
112
  "mobx": "^6.13.0"
108
113
  },
109
114
  "devDependencies": {
110
- "@open-slot-ui/core": "^0.8.2",
111
- "@open-slot-ui/pixi": "^0.8.2",
115
+ "@open-slot-ui/core": "^0.8.3",
116
+ "@open-slot-ui/pixi": "^0.8.3",
112
117
  "@schmooky/zvuk": "^1.13.0",
113
118
  "pixi.js": "^8",
114
119
  "tsdown": "^0.22.4",
115
120
  "typescript": "^5.7.2",
116
- "vitest": "^3.0.5"
121
+ "vitest": "^3.0.5",
122
+ "@types/node": "^22.20.0"
117
123
  }
118
124
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"bind-BBRxizWR.js","names":[],"sources":["../src/audio/bind.ts"],"sourcesContent":["// The mixer↔HUD binding, with NO @schmooky/zvuk dependency — so `createStakeGame` can\n// auto-wire audio without pulling zvuk into every game's bundle. It talks to the mixer\n// through a minimal structural port (`MixerLike`), which @stakeplate/core/audio's\n// `GameAudio` satisfies. `@open-slot-ui/pixi` is imported type-only (erased at build).\n\nimport type { BootedHud } from '@open-slot-ui/pixi';\n\n/** The two volume groups the HUD's two sliders drive. */\nexport type MixerGroup = 'music' | 'effects';\n\n/**\n * The minimal mixer surface the HUD binding needs. The HUD has exactly two sliders —\n * **Music** and **Effects** — so the port is group-level, not per-bus. GameAudio satisfies\n * it structurally.\n */\nexport interface MixerLike {\n /** Set a group's level (0..1) — applies to every bus in the group. */\n setGroupLevel(group: MixerGroup, level: number): void;\n /** Read a group's level (average across its buses). */\n getGroupLevel(group: MixerGroup): number;\n /** Mute/unmute everything (the Sound toggle). */\n setMuted(muted: boolean): void;\n /** Resume the AudioContext from a user gesture. Present on GameAudio. */\n unlock?(): Promise<void> | void;\n}\n\n/**\n * Bind the HUD sound controls to a mixer: the **Music slider → `music` group**, the\n * **Effects slider → `effects` group** (both persisted to localStorage + restored), and the\n * **Sound toggle → mute** everything. Returns a disposer.\n */\nexport function bindMixerToHud(mixer: MixerLike, hud: BootedHud, opts: { storageKey?: string } = {}): () => void {\n const key = opts.storageKey ?? 'stakeplate.mixer';\n const save = (): void => {\n try {\n localStorage.setItem(key, JSON.stringify({ music: mixer.getGroupLevel('music'), effects: mixer.getGroupLevel('effects') }));\n } catch {\n /* storage may be unavailable */\n }\n };\n try {\n const saved = JSON.parse(localStorage.getItem(key) || '{}') as { music?: number; effects?: number };\n if (typeof saved.music === 'number') mixer.setGroupLevel('music', saved.music);\n if (typeof saved.effects === 'number') mixer.setGroupLevel('effects', saved.effects);\n } catch {\n /* ignore */\n }\n\n const disposers: Array<() => void> = [];\n disposers.push(\n hud.on('valueChanged', (p) => {\n const v = p as { id?: string; value?: number };\n if (typeof v?.value !== 'number') return;\n // HUD control ids: 'music' → music group, 'sfx' (the Effects slider) → effects group.\n if (v.id === 'music') { mixer.setGroupLevel('music', v.value); save(); }\n if (v.id === 'sfx') { mixer.setGroupLevel('effects', v.value); save(); }\n }),\n );\n disposers.push(hud.ui.muted.subscribe((m: boolean) => mixer.setMuted(m)));\n return () => { for (const d of disposers.splice(0)) d(); };\n}\n"],"mappings":";;;;;;AA+BA,SAAgB,eAAe,OAAkB,KAAgB,OAAgC,CAAC,GAAe;CAC/G,MAAM,MAAM,KAAK,cAAc;CAC/B,MAAM,aAAmB;EACvB,IAAI;GACF,aAAa,QAAQ,KAAK,KAAK,UAAU;IAAE,OAAO,MAAM,cAAc,OAAO;IAAG,SAAS,MAAM,cAAc,SAAS;GAAE,CAAC,CAAC;EAC5H,QAAQ,CAER;CACF;CACA,IAAI;EACF,MAAM,QAAQ,KAAK,MAAM,aAAa,QAAQ,GAAG,KAAK,IAAI;EAC1D,IAAI,OAAO,MAAM,UAAU,UAAU,MAAM,cAAc,SAAS,MAAM,KAAK;EAC7E,IAAI,OAAO,MAAM,YAAY,UAAU,MAAM,cAAc,WAAW,MAAM,OAAO;CACrF,QAAQ,CAER;CAEA,MAAM,YAA+B,CAAC;CACtC,UAAU,KACR,IAAI,GAAG,iBAAiB,MAAM;EAC5B,MAAM,IAAI;EACV,IAAI,OAAO,GAAG,UAAU,UAAU;EAElC,IAAI,EAAE,OAAO,SAAS;GAAE,MAAM,cAAc,SAAS,EAAE,KAAK;GAAG,KAAK;EAAG;EACvE,IAAI,EAAE,OAAO,OAAO;GAAE,MAAM,cAAc,WAAW,EAAE,KAAK;GAAG,KAAK;EAAG;CACzE,CAAC,CACH;CACA,UAAU,KAAK,IAAI,GAAG,MAAM,WAAW,MAAe,MAAM,SAAS,CAAC,CAAC,CAAC;CACxE,aAAa;EAAE,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC,GAAG,EAAE;CAAG;AAC3D"}