@stakeplate/core 0.1.0 → 0.3.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/README.md CHANGED
@@ -61,7 +61,23 @@ config.socialMessages = { en: built.socialEn }; // auto-derived social wording
61
61
  - `@stakeplate/core/rgs` — wire protocol, launch-param runtime, `NetworkManager` +
62
62
  Stake/mock adapters, `createNetwork`.
63
63
  - `@stakeplate/core/audio` — zvuk bus graph (music/effects groups) + HUD slider/mute binding.
64
+ Loaded one-shots are RMS-normalized by default (consistent levels, no per-file gains) and
65
+ each effects bus is voice-capped so stacked cues don't machine-gun. `play(name, { volume,
66
+ pitch })` takes jitter (`{ base, jitter }`) so repeated cues vary. `bindInputSounds(audio,
67
+ hud, map)` (or an `AudioSpec.inputSounds` map) plays a cue on spin/bet/autoplay/turbo/skip.
68
+ Encode raw clips to shippable web assets (webm/opus + mp3 codec ladder, all metadata
69
+ stripped) with `scripts/encode-audio.sh OUTDIR raw/*.wav`, then load the pair with
70
+ `{ url: ['clip.webm', 'clip.mp3'] }`.
64
71
  - `@stakeplate/core/rules` — `buildRules` compliant menu + `toSocial`/`findRestricted` + dict.
72
+ - Boot loader — pass `loader: { title, backgroundImage, logo, accent, features, … }` to
73
+ `createStakeGame` for a `loader → features → idle` flow: a loading screen (pulsing
74
+ logo/spinner + progress bar) that, with `features: [{ image, text }]`, glides the logo up
75
+ and fades in a feature splash (tap to continue), then reveals the game. `manual: true` +
76
+ `ctx.loader.done()` holds it until your scene's art is in. `createLoader()` is standalone.
77
+ - `@stakeplate/core/vite` — `stakeplateBoot({ background })` Vite plugin that inlines a
78
+ blurred placeholder of the loader backdrop into index.html at build time, so the FIRST
79
+ paint shows the backdrop — no black flash before the JS loads. (Add `sharp` as a
80
+ devDependency for the blurred image; without it the solid colour is inlined — still never black.)
65
81
  - `@stakeplate/core/stores` — the MobX stores (balance, ui) for composing game state.
66
82
  - `@stakeplate/core/testing` — the mock RGS, scriptable network, instant ticker.
67
83
  - (soon) `/scene`, `/i18n`.
package/dist/audio.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SfxBus, c as createGameAudio, d as bindMixerToHud, i as MusicBus, l as MixerGroup, n as GameAudio, o as SoundEntry, r as GameAudioOptions, s as bindAudioToHud, t as BusName, u as MixerLike } from "./index-BcTRfis9.js";
2
- export { BusName, GameAudio, GameAudioOptions, type MixerGroup, type MixerLike, MusicBus, SfxBus, SoundEntry, bindAudioToHud, bindMixerToHud, createGameAudio };
1
+ import { a as SfxBus, c as createGameAudio, d as bindInputSounds, f as MixerGroup, i as MusicBus, l as InputSoundMap, m as bindMixerToHud, n as GameAudio, o as SoundEntry, p as MixerLike, r as GameAudioOptions, s as bindAudioToHud, t as BusName, u as InputSoundOptions } from "./index-BXD-eK1I.js";
2
+ export { BusName, GameAudio, GameAudioOptions, type InputSoundMap, type InputSoundOptions, type MixerGroup, type MixerLike, MusicBus, SfxBus, SoundEntry, bindAudioToHud, bindInputSounds, bindMixerToHud, createGameAudio };
package/dist/audio.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as bindMixerToHud } from "./bind-BBRxizWR.js";
1
+ import { n as bindMixerToHud, t as bindInputSounds } from "./inputs-X3Tc_HTV.js";
2
2
  import { Ducker, createEngine } from "@schmooky/zvuk";
3
3
  //#region src/audio/index.ts
4
4
  const MUSIC_BUSES = ["music", "ambience"];
@@ -18,14 +18,23 @@ var GameAudio = class {
18
18
  effectsGroup;
19
19
  duckFrom;
20
20
  duckAmount;
21
+ normalizeDefault;
21
22
  unlocked = false;
22
23
  currentMusic = null;
23
24
  constructor(opts = {}) {
24
25
  const musicLevel = opts.music ?? .8;
25
26
  const fxLevel = opts.effects ?? 1;
27
+ const conc = opts.sfxConcurrency === void 0 ? {
28
+ max: 8,
29
+ steal: "oldest"
30
+ } : opts.sfxConcurrency;
31
+ this.normalizeDefault = opts.normalize ?? true;
26
32
  const buses = {};
27
33
  for (const b of MUSIC_BUSES) buses[b] = { level: musicLevel };
28
- for (const b of SFX_BUSES) buses[b] = { level: fxLevel };
34
+ for (const b of SFX_BUSES) buses[b] = {
35
+ level: fxLevel,
36
+ ...conc ? { concurrency: conc } : {}
37
+ };
29
38
  this.engine = createEngine({
30
39
  buses,
31
40
  master: {
@@ -77,12 +86,18 @@ var GameAudio = class {
77
86
  loop: e.loop,
78
87
  ...e.intro ? { intro: e.intro } : {},
79
88
  ...e.outro ? { outro: e.outro } : {}
80
- }, e.loopCrossfadeMs != null ? { loopCrossfade: e.loopCrossfadeMs / 1e3 } : void 0) : this.engine.loadSound(e.name, e.url, { bus: e.bus ?? "ui" })));
89
+ }, e.loopCrossfadeMs != null ? { loopCrossfade: e.loopCrossfadeMs / 1e3 } : void 0) : this.engine.loadSound(e.name, e.url, {
90
+ bus: e.bus ?? "ui",
91
+ normalize: e.normalize ?? this.normalizeDefault
92
+ })));
81
93
  }
94
+ /** Fire a one-shot. `volume`/`pitch` accept a `VoiceJitter` (`{ base, jitter }`) so
95
+ * repeated cues — contacts, ticks — vary per voice instead of sounding cloned. */
82
96
  play(name, opts) {
83
97
  this.engine.sound(name).play({
84
98
  ...opts?.bus ? { bus: opts.bus } : {},
85
- ...opts?.volume != null ? { volume: opts.volume } : {}
99
+ ...opts?.volume != null ? { volume: opts.volume } : {},
100
+ ...opts?.pitch != null ? { pitch: opts.pitch } : {}
86
101
  });
87
102
  }
88
103
  music(name, opts) {
@@ -107,6 +122,6 @@ function bindAudioToHud(audio, hud, opts = {}) {
107
122
  return bindMixerToHud(audio, hud, opts);
108
123
  }
109
124
  //#endregion
110
- export { GameAudio, bindAudioToHud, bindMixerToHud, createGameAudio };
125
+ export { GameAudio, bindAudioToHud, bindInputSounds, bindMixerToHud, createGameAudio };
111
126
 
112
127
  //# sourceMappingURL=audio.js.map
package/dist/audio.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"audio.js","names":[],"sources":["../src/audio/index.ts"],"sourcesContent":["// @stakeplate/core/audio — a thin PRE-WIRING over @schmooky/zvuk. The core stands up the\n// standard slot bus graph (nine buses in TWO groups) + master, binds the two groups to the\n// HUD's Music/Effects sliders (persisted) and mutes on the Sound toggle, and ducks music\n// while chosen sfx play. The game just declares sounds and calls `audio.play('win','wins')`.\n//\n// master\n// ├── MUSIC group (Music slider) → music · ambience\n// └── EFFECTS group (Effects slider) → reels · symbols · anticipation · wins ·\n// voiceover · ui · reverb\n//\n// Groups are zvuk BusGroups (a logical handle: a slider sets every member's level). Keep the\n// per-sound mix in the sound volumes; the sliders scale the whole group.\n\nimport { createEngine, Ducker, type Engine, type Bus, type BusConfig, type BusGroup } from '@schmooky/zvuk';\nimport type { BootedHud } from '@open-slot-ui/pixi';\nimport type { AudioPort } from '../engine/fsm';\nimport { bindMixerToHud, type MixerLike, type MixerGroup } from './bind';\n\nexport { bindMixerToHud, type MixerLike, type MixerGroup } from './bind';\n\n/** Buses in the MUSIC group (driven by the Music slider). */\nexport type MusicBus = 'music' | 'ambience';\n/** Buses in the EFFECTS group (driven by the Effects slider). */\nexport type SfxBus = 'reels' | 'symbols' | 'anticipation' | 'wins' | 'voiceover' | 'ui' | 'reverb';\nexport type BusName = MusicBus | SfxBus;\n\nconst MUSIC_BUSES: MusicBus[] = ['music', 'ambience'];\nconst SFX_BUSES: SfxBus[] = ['reels', 'symbols', 'anticipation', 'wins', 'voiceover', 'ui', 'reverb'];\n\nexport interface GameAudioOptions {\n /** Initial MUSIC-group level (music + ambience), 0..1. Default 0.8. */\n music?: number;\n /** Initial EFFECTS-group level (reels…reverb), 0..1. Default 1. */\n effects?: number;\n masterHeadroom?: number;\n /** Duck the MUSIC group while THESE sfx buses are active (default `['wins']`; `null` = off). */\n duckMusicFrom?: SfxBus | SfxBus[] | null;\n duckAmount?: number;\n}\n\n/**\n * A sound to load onto a bus, or a music track (intro/loop/outro → the `music` bus).\n * For a PLAIN loop with no authored intro/outro, set `loopCrossfadeMs`: zvuk equal-power\n * crossfades the loop boundary so a single file loops seamlessly (no click, no stinger/tail\n * needed) — just the crossfade window in ms.\n */\nexport type SoundEntry =\n | { name: string; kind?: 'sound'; url: string | string[]; bus?: BusName }\n | { name: string; kind: 'music'; loop: string | string[]; intro?: string | string[]; outro?: string | string[]; loopCrossfadeMs?: number };\n\n/** The pre-wired game mixer. Satisfies {@link AudioPort} + {@link MixerLike}. */\nexport class GameAudio implements AudioPort, MixerLike {\n readonly engine: Engine<BusName>;\n private readonly musicGroup: BusGroup;\n private readonly effectsGroup: BusGroup;\n private readonly duckFrom: SfxBus[];\n private readonly duckAmount: number;\n private unlocked = false;\n private currentMusic: { stop(opts?: { fade?: number }): void } | null = null;\n\n constructor(opts: GameAudioOptions = {}) {\n const musicLevel = opts.music ?? 0.8;\n const fxLevel = opts.effects ?? 1;\n const buses = {} as Record<BusName, BusConfig>;\n for (const b of MUSIC_BUSES) buses[b] = { level: musicLevel };\n for (const b of SFX_BUSES) buses[b] = { level: fxLevel };\n this.engine = createEngine<BusName>({\n buses,\n master: { headroom: opts.masterHeadroom ?? -3, limiter: { threshold: -1 } },\n });\n this.musicGroup = this.engine.busGroup('music', MUSIC_BUSES.map((b) => this.engine.bus(b)));\n this.effectsGroup = this.engine.busGroup('effects', SFX_BUSES.map((b) => this.engine.bus(b)));\n const df = opts.duckMusicFrom === undefined ? (['wins'] as SfxBus[]) : opts.duckMusicFrom;\n this.duckFrom = df == null ? [] : Array.isArray(df) ? df : [df];\n this.duckAmount = opts.duckAmount ?? 0.5;\n }\n\n /** Resume the AudioContext from a user gesture + arm ducking. Idempotent. */\n async unlock(): Promise<void> {\n if (this.unlocked) return;\n await this.engine.unlock();\n this.unlocked = true;\n // Duck music + ambience while each chosen sfx bus is active.\n for (const src of this.duckFrom) {\n for (const target of MUSIC_BUSES) {\n const ducker = new Ducker(this.engine.context, this.engine.bus(src), { amount: this.duckAmount, attack: 0.05, release: 0.3 });\n this.engine.bus(target).addFx(ducker);\n }\n }\n }\n\n // ── MixerLike (the HUD sliders + mute drive these) ────────────────────────────\n setGroupLevel(group: MixerGroup, level: number): void {\n (group === 'music' ? this.musicGroup : this.effectsGroup).level = level;\n }\n getGroupLevel(group: MixerGroup): number {\n return (group === 'music' ? this.musicGroup : this.effectsGroup).level;\n }\n setMuted(muted: boolean): void {\n this.musicGroup.muted = muted;\n this.effectsGroup.muted = muted;\n }\n\n /** A single bus (for per-sound routing, sends, FX inserts). */\n bus(name: BusName): Bus {\n return this.engine.bus(name);\n }\n /** A volume group handle (`'music'` or `'effects'`). */\n group(name: MixerGroup): BusGroup {\n return name === 'music' ? this.musicGroup : this.effectsGroup;\n }\n\n /** Preload a manifest of sounds + music onto their buses. */\n async load(entries: SoundEntry[]): Promise<void> {\n await Promise.all(\n entries.map((e) =>\n 'kind' in e && e.kind === 'music'\n ? this.engine.loadMusic(\n e.name,\n { loop: e.loop, ...(e.intro ? { intro: e.intro } : {}), ...(e.outro ? { outro: e.outro } : {}) },\n e.loopCrossfadeMs != null ? { loopCrossfade: e.loopCrossfadeMs / 1000 } : undefined,\n )\n : this.engine.loadSound(e.name, e.url, { bus: e.bus ?? 'ui' }),\n ),\n );\n }\n\n play(name: string, opts?: { bus?: BusName; volume?: number }): void {\n this.engine.sound(name).play({ ...(opts?.bus ? { bus: opts.bus } : {}), ...(opts?.volume != null ? { volume: opts.volume } : {}) });\n }\n\n music(name: string, opts?: { fadeIn?: number }): void {\n this.currentMusic?.stop({ fade: 0.3 });\n this.currentMusic = this.engine.music(name).play({ fadeIn: opts?.fadeIn ?? 0.4 });\n }\n\n stopMusic(opts?: { fade?: number }): void {\n this.currentMusic?.stop({ fade: opts?.fade ?? 0.3 });\n this.currentMusic = null;\n }\n}\n\nexport function createGameAudio(opts?: GameAudioOptions): GameAudio {\n return new GameAudio(opts);\n}\n\n/**\n * Bind the HUD's sound controls to the audio groups (Music slider → music group, Effects →\n * effects group, persisted; Sound toggle → mute). Returns a disposer. `createStakeGame` calls\n * this for you (via {@link bindMixerToHud}) when you pass `audio`; use it directly only to opt\n * out or wire a mixer the core didn't get.\n */\nexport function bindAudioToHud(audio: GameAudio, hud: BootedHud, opts: { storageKey?: string } = {}): () => void {\n return bindMixerToHud(audio, hud, opts);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"audio.js","names":[],"sources":["../src/audio/index.ts"],"sourcesContent":["// @stakeplate/core/audio — a thin PRE-WIRING over @schmooky/zvuk. The core stands up the\n// standard slot bus graph (nine buses in TWO groups) + master, binds the two groups to the\n// HUD's Music/Effects sliders (persisted) and mutes on the Sound toggle, and ducks music\n// while chosen sfx play. The game just declares sounds and calls `audio.play('win','wins')`.\n//\n// master\n// ├── MUSIC group (Music slider) → music · ambience\n// └── EFFECTS group (Effects slider) → reels · symbols · anticipation · wins ·\n// voiceover · ui · reverb\n//\n// Groups are zvuk BusGroups (a logical handle: a slider sets every member's level). Keep the\n// per-sound mix in the sound volumes; the sliders scale the whole group.\n\nimport { createEngine, Ducker, type Engine, type Bus, type BusConfig, type BusGroup, type VoiceJitter, type ConcurrencyConfig, type LoudnessOptions } from '@schmooky/zvuk';\nimport type { BootedHud } from '@open-slot-ui/pixi';\nimport type { AudioPort } from '../engine/fsm';\nimport { bindMixerToHud, type MixerLike, type MixerGroup } from './bind';\n\nexport { bindMixerToHud, type MixerLike, type MixerGroup } from './bind';\nexport { bindInputSounds, type InputSoundMap, type InputSoundOptions } from './inputs';\n\n/** Buses in the MUSIC group (driven by the Music slider). */\nexport type MusicBus = 'music' | 'ambience';\n/** Buses in the EFFECTS group (driven by the Effects slider). */\nexport type SfxBus = 'reels' | 'symbols' | 'anticipation' | 'wins' | 'voiceover' | 'ui' | 'reverb';\nexport type BusName = MusicBus | SfxBus;\n\nconst MUSIC_BUSES: MusicBus[] = ['music', 'ambience'];\nconst SFX_BUSES: SfxBus[] = ['reels', 'symbols', 'anticipation', 'wins', 'voiceover', 'ui', 'reverb'];\n\nexport interface GameAudioOptions {\n /** Initial MUSIC-group level (music + ambience), 0..1. Default 0.8. */\n music?: number;\n /** Initial EFFECTS-group level (reels…reverb), 0..1. Default 1. */\n effects?: number;\n masterHeadroom?: number;\n /** Duck the MUSIC group while THESE sfx buses are active (default `['wins']`; `null` = off). */\n duckMusicFrom?: SfxBus | SfxBus[] | null;\n duckAmount?: number;\n /**\n * RMS loudness normalization applied to every loaded one-shot so the whole manifest sits\n * at a consistent level (no hand-tuned per-file gains). A per-entry `normalize` overrides\n * this. Default `true`. Pass a `LoudnessOptions` object to tune the target, or `false` off.\n */\n normalize?: boolean | LoudnessOptions;\n /**\n * Voice cap applied to EACH effects bus so stacked one-shots (a multi-pay cascade, rapid\n * contacts) don't machine-gun — steals the oldest voice when the cap is hit. Default\n * `{ max: 8, steal: 'oldest' }`. Pass `null` to leave the effects buses uncapped.\n */\n sfxConcurrency?: ConcurrencyConfig | null;\n}\n\n/**\n * A sound to load onto a bus, or a music track (intro/loop/outro → the `music` bus).\n * For a PLAIN loop with no authored intro/outro, set `loopCrossfadeMs`: zvuk equal-power\n * crossfades the loop boundary so a single file loops seamlessly (no click, no stinger/tail\n * needed) — just the crossfade window in ms.\n */\nexport type SoundEntry =\n | { name: string; kind?: 'sound'; url: string | string[]; bus?: BusName; normalize?: boolean | LoudnessOptions }\n | { name: string; kind: 'music'; loop: string | string[]; intro?: string | string[]; outro?: string | string[]; loopCrossfadeMs?: number };\n\n/** The pre-wired game mixer. Satisfies {@link AudioPort} + {@link MixerLike}. */\nexport class GameAudio implements AudioPort, MixerLike {\n readonly engine: Engine<BusName>;\n private readonly musicGroup: BusGroup;\n private readonly effectsGroup: BusGroup;\n private readonly duckFrom: SfxBus[];\n private readonly duckAmount: number;\n private readonly normalizeDefault: boolean | LoudnessOptions;\n private unlocked = false;\n private currentMusic: { stop(opts?: { fade?: number }): void } | null = null;\n\n constructor(opts: GameAudioOptions = {}) {\n const musicLevel = opts.music ?? 0.8;\n const fxLevel = opts.effects ?? 1;\n // Cap each effects bus so stacked one-shots don't machine-gun (opt out with `null`).\n const conc = opts.sfxConcurrency === undefined ? ({ max: 8, steal: 'oldest' } as ConcurrencyConfig) : opts.sfxConcurrency;\n this.normalizeDefault = opts.normalize ?? true;\n const buses = {} as Record<BusName, BusConfig>;\n for (const b of MUSIC_BUSES) buses[b] = { level: musicLevel };\n for (const b of SFX_BUSES) buses[b] = { level: fxLevel, ...(conc ? { concurrency: conc } : {}) };\n this.engine = createEngine<BusName>({\n buses,\n master: { headroom: opts.masterHeadroom ?? -3, limiter: { threshold: -1 } },\n });\n this.musicGroup = this.engine.busGroup('music', MUSIC_BUSES.map((b) => this.engine.bus(b)));\n this.effectsGroup = this.engine.busGroup('effects', SFX_BUSES.map((b) => this.engine.bus(b)));\n const df = opts.duckMusicFrom === undefined ? (['wins'] as SfxBus[]) : opts.duckMusicFrom;\n this.duckFrom = df == null ? [] : Array.isArray(df) ? df : [df];\n this.duckAmount = opts.duckAmount ?? 0.5;\n }\n\n /** Resume the AudioContext from a user gesture + arm ducking. Idempotent. */\n async unlock(): Promise<void> {\n if (this.unlocked) return;\n await this.engine.unlock();\n this.unlocked = true;\n // Duck music + ambience while each chosen sfx bus is active.\n for (const src of this.duckFrom) {\n for (const target of MUSIC_BUSES) {\n const ducker = new Ducker(this.engine.context, this.engine.bus(src), { amount: this.duckAmount, attack: 0.05, release: 0.3 });\n this.engine.bus(target).addFx(ducker);\n }\n }\n }\n\n // ── MixerLike (the HUD sliders + mute drive these) ────────────────────────────\n setGroupLevel(group: MixerGroup, level: number): void {\n (group === 'music' ? this.musicGroup : this.effectsGroup).level = level;\n }\n getGroupLevel(group: MixerGroup): number {\n return (group === 'music' ? this.musicGroup : this.effectsGroup).level;\n }\n setMuted(muted: boolean): void {\n this.musicGroup.muted = muted;\n this.effectsGroup.muted = muted;\n }\n\n /** A single bus (for per-sound routing, sends, FX inserts). */\n bus(name: BusName): Bus {\n return this.engine.bus(name);\n }\n /** A volume group handle (`'music'` or `'effects'`). */\n group(name: MixerGroup): BusGroup {\n return name === 'music' ? this.musicGroup : this.effectsGroup;\n }\n\n /** Preload a manifest of sounds + music onto their buses. */\n async load(entries: SoundEntry[]): Promise<void> {\n await Promise.all(\n entries.map((e) =>\n 'kind' in e && e.kind === 'music'\n ? this.engine.loadMusic(\n e.name,\n { loop: e.loop, ...(e.intro ? { intro: e.intro } : {}), ...(e.outro ? { outro: e.outro } : {}) },\n e.loopCrossfadeMs != null ? { loopCrossfade: e.loopCrossfadeMs / 1000 } : undefined,\n )\n : this.engine.loadSound(e.name, e.url, { bus: e.bus ?? 'ui', normalize: e.normalize ?? this.normalizeDefault }),\n ),\n );\n }\n\n /** Fire a one-shot. `volume`/`pitch` accept a `VoiceJitter` (`{ base, jitter }`) so\n * repeated cues — contacts, ticks — vary per voice instead of sounding cloned. */\n play(name: string, opts?: { bus?: BusName; volume?: number | VoiceJitter; pitch?: number | VoiceJitter }): void {\n this.engine.sound(name).play({\n ...(opts?.bus ? { bus: opts.bus } : {}),\n ...(opts?.volume != null ? { volume: opts.volume } : {}),\n ...(opts?.pitch != null ? { pitch: opts.pitch } : {}),\n });\n }\n\n music(name: string, opts?: { fadeIn?: number }): void {\n this.currentMusic?.stop({ fade: 0.3 });\n this.currentMusic = this.engine.music(name).play({ fadeIn: opts?.fadeIn ?? 0.4 });\n }\n\n stopMusic(opts?: { fade?: number }): void {\n this.currentMusic?.stop({ fade: opts?.fade ?? 0.3 });\n this.currentMusic = null;\n }\n}\n\nexport function createGameAudio(opts?: GameAudioOptions): GameAudio {\n return new GameAudio(opts);\n}\n\n/**\n * Bind the HUD's sound controls to the audio groups (Music slider → music group, Effects →\n * effects group, persisted; Sound toggle → mute). Returns a disposer. `createStakeGame` calls\n * this for you (via {@link bindMixerToHud}) when you pass `audio`; use it directly only to opt\n * out or wire a mixer the core didn't get.\n */\nexport function bindAudioToHud(audio: GameAudio, hud: BootedHud, opts: { storageKey?: string } = {}): () => void {\n return bindMixerToHud(audio, hud, opts);\n}\n"],"mappings":"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"}
@@ -3,31 +3,42 @@ import { t as NetworkManager } from "./network-pBW8CX1o.js";
3
3
  import { n as RootStore } from "./index-BBlN97RY.js";
4
4
  import { a as Ticker, t as HudPort } from "./hud-port-DNfotn4U.js";
5
5
  import { BootedHud } from "@open-slot-ui/pixi";
6
- import { Bus, BusGroup, Engine } from "@schmooky/zvuk";
6
+ import { Bus, BusGroup, ConcurrencyConfig, Engine, LoudnessOptions, VoiceJitter } from "@schmooky/zvuk";
7
7
  //#region src/engine/turbo.d.ts
8
8
  /** Delay multipliers per turbo level: off (1×) / turbo (0.4×) / super (0.12×). */
9
9
  declare const DEFAULT_TURBO_SPEEDS: number[];
10
10
  interface TurboState {
11
11
  /** 0 = off, 1 = turbo, 2 = super — the HUD turbo cycler's index. */
12
12
  readonly level: number;
13
- /** The delay multiplier for the current level (`ctx.turbo.delay` already applies it). */
13
+ /** The EFFECTIVE delay multiplier right now — the turbo level, floored by the autoplay
14
+ * speed while autoplay is running (`ctx.turbo.delay` already applies it). */
14
15
  readonly speed: number;
16
+ /** True while autoplay/hold is running — game animations are shortened even at turbo off. */
17
+ readonly autoplay: boolean;
15
18
  /** True once the player slam-stopped this round (delays resolve instantly). */
16
19
  readonly skipped: boolean;
17
- /** A turbo- + slam-stop-aware delay for GAME animations. NOT for compliance waits. */
20
+ /** A turbo- + autoplay- + slam-stop-aware delay for GAME animations. NOT for compliance waits. */
18
21
  delay(ms: number): Promise<void>;
19
22
  }
20
23
  declare class TurboClock implements TurboState {
21
24
  private readonly speeds;
25
+ /** Delay multiplier applied while autoplay/hold runs (the floor for `speed`). Defaults to
26
+ * the turbo (level-1) speed, so autoplay plays at least at turbo pace even at turbo off. */
27
+ private readonly autoplaySpeed;
22
28
  private _level;
29
+ private _autoplay;
23
30
  private _skipped;
24
31
  private readonly pending;
25
- constructor(speeds?: number[]);
32
+ constructor(speeds?: number[], autoplaySpeed?: number);
26
33
  get level(): number;
27
34
  get speed(): number;
35
+ get autoplay(): boolean;
28
36
  get skipped(): boolean;
29
37
  /** Set the turbo level (clamped to the configured speeds). */
30
38
  setLevel(index: number): void;
39
+ /** Mark autoplay/hold as running so `delay()` shortens game animations. The core sets
40
+ * this on autoplayStarted/holdSpinStarted and clears it when they stop. */
41
+ setAutoplay(active: boolean): void;
31
42
  /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */
32
43
  skip(): void;
33
44
  /** Clear the slam-stop flag — the core calls this at the start of each spin. */
@@ -100,6 +111,17 @@ interface GameConfig {
100
111
  rtp?: number;
101
112
  /** Mode key → cost multiplier (a number) or a `ModeConfig`. `base` defaults to 1. */
102
113
  modes?: Record<string, ModeConfig | number>;
114
+ /**
115
+ * Minimum selectable bet, in MAJOR units of the account currency — an EXPLICIT client
116
+ * floor. Any server bet-ladder level below this is dropped from the HUD ladder, so the
117
+ * smallest possible win (bet × the game's minimum payout) can never round below one
118
+ * minimal currency unit. Set it per game to your smallest legal stake — e.g. `0.05` when
119
+ * the minimum payout is ×0.2 (0.05 × 0.2 = 0.01 = one cent). `undefined` → derive it from
120
+ * the currency (5 minimal units — a safe default for a ×0.2 minimum payout). The server
121
+ * ladder stays authoritative for the LEVELS offered; this only trims the low end + snaps
122
+ * the default up to the first legal level.
123
+ */
124
+ minBet?: number;
103
125
  /** Rules/info menu for the HUD (`@open-slot-ui` `MenuSpec`) — build it with `@stakeplate/core/rules` `buildRules`. */
104
126
  rules?: unknown;
105
127
  /** i18n messages per locale (`{ en: { key: text }, es: {…} }`) for the HUD + rules text. */
@@ -118,11 +140,25 @@ interface GameConfig {
118
140
  declare function modeCostOf(config: GameConfig, mode: string): number;
119
141
  //#endregion
120
142
  //#region src/engine/fsm.d.ts
143
+ /** A fixed value or a per-voice random spread (`{ base, jitter }`) — structurally matches
144
+ * zvuk's `VoiceJitter` without importing it, so the engine stays zvuk-free. */
145
+ type AudioValue = number | {
146
+ base?: number;
147
+ jitter?: number;
148
+ };
149
+ /** The minimal boot-loader surface a game's phases touch (satisfied by the core's
150
+ * `GameLoader`) — kept structural so the engine needn't import the loader module. */
151
+ interface LoaderPort {
152
+ setProgress(p: number): void;
153
+ done(): Promise<void>;
154
+ }
121
155
  /** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */
122
156
  interface AudioPort {
157
+ /** Fire a one-shot. `volume`/`pitch` accept jitter so repeated cues vary per voice. */
123
158
  play(name: string, opts?: {
124
159
  bus?: string;
125
- volume?: number;
160
+ volume?: AudioValue;
161
+ pitch?: AudioValue;
126
162
  }): void;
127
163
  music(name: string, opts?: {
128
164
  fadeIn?: number;
@@ -144,6 +180,8 @@ interface PhaseContext<T = unknown, V = unknown, E = unknown> {
144
180
  readonly audio: AudioPort | null;
145
181
  /** Turbo speed + slam-stop (core-owned). Use `ctx.turbo.delay(ms)` for spin/anim timing. */
146
182
  readonly turbo: TurboState;
183
+ /** The boot loader, if one was configured (else `null`) — drive progress / `done()`. */
184
+ readonly loader: LoaderPort | null;
147
185
  readonly interpretBook: InterpretBook<T, E>;
148
186
  readonly fsm: FSM<T, V, E>;
149
187
  round: GameRound<T, E> | null;
@@ -194,6 +232,32 @@ declare function bindMixerToHud(mixer: MixerLike, hud: BootedHud, opts?: {
194
232
  storageKey?: string;
195
233
  }): () => void;
196
234
  //#endregion
235
+ //#region src/audio/inputs.d.ts
236
+ /** Cue name per HUD input event. Any omitted → that input stays silent. */
237
+ interface InputSoundMap {
238
+ /** Spin button press (and each autoplay/hold re-spin fires `spinRequested`). */
239
+ spin?: string;
240
+ /** Bet +/- (the bet stepper's `valueChanged`). */
241
+ bet?: string;
242
+ /** Autoplay engaged (count picked). */
243
+ autoplay?: string;
244
+ /** Turbo cycler toggled. */
245
+ turbo?: string;
246
+ /** Any switch toggled (turbo/sound/…) — a generic UI click. */
247
+ toggle?: string;
248
+ /** Slam-stop (tap-to-skip the spin). */
249
+ skip?: string;
250
+ }
251
+ interface InputSoundOptions {
252
+ /** Bus to route the cues to (default `'ui'`). */
253
+ bus?: string;
254
+ }
255
+ /**
256
+ * Wire the HUD input events to sounds. Returns a disposer. Safe to call with a partial map —
257
+ * only the mapped inputs subscribe.
258
+ */
259
+ declare function bindInputSounds(audio: AudioPort, hud: BootedHud, map: InputSoundMap, opts?: InputSoundOptions): () => void;
260
+ //#endregion
197
261
  //#region src/audio/index.d.ts
198
262
  /** Buses in the MUSIC group (driven by the Music slider). */
199
263
  type MusicBus = 'music' | 'ambience';
@@ -209,6 +273,18 @@ interface GameAudioOptions {
209
273
  /** Duck the MUSIC group while THESE sfx buses are active (default `['wins']`; `null` = off). */
210
274
  duckMusicFrom?: SfxBus | SfxBus[] | null;
211
275
  duckAmount?: number;
276
+ /**
277
+ * RMS loudness normalization applied to every loaded one-shot so the whole manifest sits
278
+ * at a consistent level (no hand-tuned per-file gains). A per-entry `normalize` overrides
279
+ * this. Default `true`. Pass a `LoudnessOptions` object to tune the target, or `false` off.
280
+ */
281
+ normalize?: boolean | LoudnessOptions;
282
+ /**
283
+ * Voice cap applied to EACH effects bus so stacked one-shots (a multi-pay cascade, rapid
284
+ * contacts) don't machine-gun — steals the oldest voice when the cap is hit. Default
285
+ * `{ max: 8, steal: 'oldest' }`. Pass `null` to leave the effects buses uncapped.
286
+ */
287
+ sfxConcurrency?: ConcurrencyConfig | null;
212
288
  }
213
289
  /**
214
290
  * A sound to load onto a bus, or a music track (intro/loop/outro → the `music` bus).
@@ -221,6 +297,7 @@ type SoundEntry = {
221
297
  kind?: 'sound';
222
298
  url: string | string[];
223
299
  bus?: BusName;
300
+ normalize?: boolean | LoudnessOptions;
224
301
  } | {
225
302
  name: string;
226
303
  kind: 'music';
@@ -236,6 +313,7 @@ declare class GameAudio implements AudioPort, MixerLike {
236
313
  private readonly effectsGroup;
237
314
  private readonly duckFrom;
238
315
  private readonly duckAmount;
316
+ private readonly normalizeDefault;
239
317
  private unlocked;
240
318
  private currentMusic;
241
319
  constructor(opts?: GameAudioOptions);
@@ -250,9 +328,12 @@ declare class GameAudio implements AudioPort, MixerLike {
250
328
  group(name: MixerGroup): BusGroup;
251
329
  /** Preload a manifest of sounds + music onto their buses. */
252
330
  load(entries: SoundEntry[]): Promise<void>;
331
+ /** Fire a one-shot. `volume`/`pitch` accept a `VoiceJitter` (`{ base, jitter }`) so
332
+ * repeated cues — contacts, ticks — vary per voice instead of sounding cloned. */
253
333
  play(name: string, opts?: {
254
334
  bus?: BusName;
255
- volume?: number;
335
+ volume?: number | VoiceJitter;
336
+ pitch?: number | VoiceJitter;
256
337
  }): void;
257
338
  music(name: string, opts?: {
258
339
  fadeIn?: number;
@@ -272,5 +353,5 @@ declare function bindAudioToHud(audio: GameAudio, hud: BootedHud, opts?: {
272
353
  storageKey?: string;
273
354
  }): () => void;
274
355
  //#endregion
275
- export { DEFAULT_TURBO_SPEEDS as C, roundInfo as S, TurboState as T, ModeConfig as _, SfxBus as a, InterpretBook as b, createGameAudio as c, bindMixerToHud as d, AudioPort as f, GameConfig as g, PhaseContext as h, MusicBus as i, MixerGroup as l, Phase as m, GameAudio as n, SoundEntry as o, FSM as p, GameAudioOptions as r, bindAudioToHud as s, BusName as t, MixerLike as u, modeCostOf as v, TurboClock as w, RoundInfo as x, GameRound as y };
276
- //# sourceMappingURL=index-BcTRfis9.d.ts.map
356
+ export { TurboState as A, modeCostOf as C, roundInfo as D, RoundInfo as E, DEFAULT_TURBO_SPEEDS as O, ModeConfig as S, InterpretBook as T, FSM as _, SfxBus as a, PhaseContext as b, createGameAudio as c, bindInputSounds as d, MixerGroup as f, AudioValue as g, AudioPort as h, MusicBus as i, TurboClock as k, InputSoundMap as l, bindMixerToHud as m, GameAudio as n, SoundEntry as o, MixerLike as p, GameAudioOptions as r, bindAudioToHud as s, BusName as t, InputSoundOptions as u, LoaderPort as v, GameRound as w, GameConfig as x, Phase as y };
357
+ //# sourceMappingURL=index-BXD-eK1I.d.ts.map
package/dist/index.d.ts CHANGED
@@ -2,9 +2,10 @@ import { a as Balance, c as JurisdictionConfig, d as ReplayParams, f as RgsConfi
2
2
  import { a as MockOptions, c as RuntimeConfig, i as MockNetworkManager, l as readRuntime, n as PlayArgs, o as ScriptedRound, r as createNetwork, s as ReplayLaunch, t as NetworkManager, u as urlParam } from "./network-pBW8CX1o.js";
3
3
  import { i as UiStore, n as RootStore, r as SessionStore, t as BalanceStore } from "./index-BBlN97RY.js";
4
4
  import { a as Ticker, i as RealTicker, n as ReplayInfo, r as InstantTicker, t as HudPort } from "./hud-port-DNfotn4U.js";
5
- import { C as DEFAULT_TURBO_SPEEDS, S as roundInfo, T as TurboState, _ as ModeConfig, b as InterpretBook, f as AudioPort, g as GameConfig, h as PhaseContext, m as Phase, o as SoundEntry, p as FSM, r as GameAudioOptions, v as modeCostOf, w as TurboClock, x as RoundInfo, y as GameRound } from "./index-BcTRfis9.js";
5
+ import { A as TurboState, C as modeCostOf, D as roundInfo, E as RoundInfo, O as DEFAULT_TURBO_SPEEDS, S as ModeConfig, T as InterpretBook, _ as FSM, b as PhaseContext, g as AudioValue, h as AudioPort, k as TurboClock, l as InputSoundMap, o as SoundEntry, r as GameAudioOptions, v as LoaderPort, w as GameRound, x as GameConfig, y as Phase } from "./index-BXD-eK1I.js";
6
6
  import { StakeNetworkManager, StakeNetworkOptions } from "./rgs.js";
7
7
  import { BootedHud } from "@open-slot-ui/pixi";
8
+ import { CurrencySpec } from "@open-slot-ui/core";
8
9
  //#region src/engine/phases.d.ts
9
10
  /** Wait for the next spin (the HUD's `spinRequested` drives the transition to Spin). */
10
11
  declare class IdlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {
@@ -32,6 +33,92 @@ declare class SettlePhase<T = unknown, V = unknown, E = unknown> implements Phas
32
33
  /** The default phase set (game phases are appended after → same-name overrides win). */
33
34
  declare function defaultPhases<T = unknown, V = unknown, E = unknown>(): Phase<T, V, E>[];
34
35
  //#endregion
36
+ //#region src/engine/beat.d.ts
37
+ interface BeatOptions {
38
+ /** Pause before the announcement shows — lets the reveal settle. Default 0. */
39
+ leadMs?: number;
40
+ /** How long the announcement holds alone on screen. */
41
+ holdMs: number;
42
+ /** Pause after hiding, before play resumes. Default 0. */
43
+ trailMs?: number;
44
+ /** Reveal the announcement (banner/sfx). Called after the lead pause. */
45
+ show: () => void;
46
+ /** Hide it. Called after the hold, before the trailing pause. Optional (some banners
47
+ * self-dismiss). */
48
+ hide?: () => void;
49
+ /**
50
+ * Timing source. `'real'` (default) is a fixed, unskippable beat via the ticker — an
51
+ * announcement is a deliberate moment that a slam-stop shouldn't blow past. `'turbo'`
52
+ * makes the pauses turbo-/autoplay-/slam-stop-aware (shorter under turbo, skippable).
53
+ */
54
+ timing?: 'real' | 'turbo';
55
+ }
56
+ /** Timing surface the beat needs — a `PhaseContext` satisfies it (`ctx.ticker`/`ctx.turbo`). */
57
+ interface BeatClock {
58
+ ticker: Ticker;
59
+ turbo: TurboState;
60
+ }
61
+ /**
62
+ * Run a blocking announcement beat: lead → show → hold → hide → trail. Awaitable, so a
63
+ * Present phase just `await blockingBeat(ctx, { … })` and the round pauses for the whole
64
+ * thing. Returns when the beat (including the trailing pause) is over.
65
+ */
66
+ declare function blockingBeat(ctx: BeatClock, opts: BeatOptions): Promise<void>;
67
+ //#endregion
68
+ //#region src/loader/index.d.ts
69
+ /** One feature card on the intro splash — an image with a short (may be multi-line) caption. */
70
+ interface FeatureItem {
71
+ /** Image URL / data-URI. Rendered at a uniform height so mismatched art still lines up. */
72
+ image: string;
73
+ /** Short caption under the image. `\n` for line breaks. */
74
+ text: string;
75
+ }
76
+ interface LoaderConfig {
77
+ /** Big title under the logo (LOADING phase only). Defaults to the game title. */
78
+ title?: string;
79
+ /** Small line under the title (LOADING phase). */
80
+ subtitle?: string;
81
+ /** URL/data-URI of the logo. Pulses while loading; glides to the top for the features
82
+ * splash. Omit for a CSS ring spinner (and no move-up). */
83
+ logo?: string;
84
+ /** Solid colour behind everything. Default `#0b0d12`. */
85
+ background?: string;
86
+ /** Optional cover image (ideally the game's own backdrop, for a seamless reveal). */
87
+ backgroundImage?: string;
88
+ /** Blur the cover image. Default `true`. */
89
+ blur?: boolean;
90
+ /** Accent colour — progress fill + title glow + caption text. Default `#d99000`. */
91
+ accent?: string;
92
+ /** Minimum time the LOADING phase stays up, so the bar always fills. Default `1200`ms. */
93
+ minDurationMs?: number;
94
+ /** Stacking order. Default a very high value so it sits over the game. */
95
+ zIndex?: number;
96
+ /**
97
+ * If `true`, the core shows the loader + advances progress but does NOT auto-advance past
98
+ * LOADING — the game calls `ctx.loader.done()` itself (e.g. after its scene finishes loading
99
+ * art). Default `false`. (The FEATURES splash always waits for the player regardless.)
100
+ */
101
+ manual?: boolean;
102
+ /** 2–3 feature cards → show a FEATURES splash after loading. Omit for a plain loader. */
103
+ features?: FeatureItem[];
104
+ /** The dismiss prompt under the feature cards. Default `PRESS TO CONTINUE`. */
105
+ continueText?: string;
106
+ }
107
+ interface GameLoader {
108
+ /** The overlay element (already in the document). */
109
+ readonly el: HTMLElement;
110
+ /** Drive the progress bar. Clamped to the range it has already reached (never goes back). */
111
+ setProgress(p: number): void;
112
+ /** Advance out of LOADING: fill to 100%, honour `minDurationMs`, then either show the
113
+ * FEATURES splash (waiting for the player to continue) or — with no features — pop away.
114
+ * Resolves once the overlay is fully gone. Idempotent (first call wins). */
115
+ done(): Promise<void>;
116
+ /** Remove immediately without any outro (used on a boot error). */
117
+ remove(): void;
118
+ }
119
+ /** Create + mount the boot overlay. Safe to call before anything else in boot. */
120
+ declare function createLoader(config?: LoaderConfig): GameLoader;
121
+ //#endregion
35
122
  //#region src/game/createStakeGame.d.ts
36
123
  /**
37
124
  * Declarative audio: hand the core your sounds and it lazily creates the `@schmooky/zvuk`
@@ -41,6 +128,9 @@ declare function defaultPhases<T = unknown, V = unknown, E = unknown>(): Phase<T
41
128
  */
42
129
  interface AudioSpec extends GameAudioOptions {
43
130
  sounds: SoundEntry[];
131
+ /** Optional: play a cue on HUD input events (spin/bet/autoplay/turbo/toggle/skip). The
132
+ * core auto-binds these after the HUD mounts — the named sounds must be in `sounds`. */
133
+ inputSounds?: InputSoundMap;
44
134
  }
45
135
  /** Passed to `mountView` — everything the game's scene needs (not the round/fsm yet). */
46
136
  interface ViewContext {
@@ -51,6 +141,10 @@ interface ViewContext {
51
141
  audio: AudioPort | null;
52
142
  /** Turbo speed + slam-stop (core-owned) — the scene may branch on `turbo.level`/`speed`. */
53
143
  turbo: TurboState;
144
+ /** The boot loader (if `loader` was configured) — drive `setProgress` while your scene
145
+ * loads art, and (with `loader.manual`) call `done()` when the scene is ready. `null`
146
+ * when no loader is used. */
147
+ loader: GameLoader | null;
54
148
  }
55
149
  interface CreateStakeGameOptions<T = unknown, V = unknown, E = unknown> {
56
150
  config: GameConfig;
@@ -72,6 +166,14 @@ interface CreateStakeGameOptions<T = unknown, V = unknown, E = unknown> {
72
166
  sceneHost?: HTMLElement;
73
167
  /** Passthrough to `mountHud` (spinSkin, icons, gsap, menu:false, hooks, …). */
74
168
  hudOptions?: Record<string, unknown>;
169
+ /**
170
+ * A configurable boot loader — shows the instant boot starts, advances across the boot
171
+ * milestones, then fills + pops away when the game is ready. Pass a {@link LoaderConfig}
172
+ * to enable it (title defaults to `config.title`); omit or `false` to render none (the
173
+ * game keeps its own HTML loader). With `manual: true`, call `ctx.loader.done()` yourself
174
+ * (e.g. after the scene's art finishes loading).
175
+ */
176
+ loader?: LoaderConfig | false;
75
177
  }
76
178
  /** A read-only snapshot of the running game — for the dev harness, tests and debugging. */
77
179
  interface GameSnapshot {
@@ -92,5 +194,20 @@ interface StakeGame {
92
194
  }
93
195
  declare function createStakeGame<T = unknown, V = unknown, E = unknown>(opts: CreateStakeGameOptions<T, V, E>): StakeGame;
94
196
  //#endregion
95
- export { API_AMOUNT_MULTIPLIER, AudioPort, AudioSpec, AuthenticateRequest, AuthenticateResponse, BOOK_AMOUNT_MULTIPLIER, Balance, BalanceStore, CreateStakeGameOptions, DEFAULT_TURBO_SPEEDS, EndRoundRequest, EndRoundResponse, FSM, GameConfig, GameRound, GameSnapshot, HudPort, IdlePhase, InstantTicker, InterpretBook, JurisdictionConfig, MockNetworkManager, MockOptions, ModeConfig, NetworkManager, Phase, PhaseContext, PlayArgs, PlayRequest, PlayResponse, PresentPhase, RealTicker, ReplayInfo, ReplayLaunch, ReplayParams, RgsConfig, RgsError, RootStore, Round, RoundInfo, RuntimeConfig, ScriptedRound, SessionStore, SettlePhase, SpinPhase, StakeGame, StakeNetworkManager, StakeNetworkOptions, Ticker, TurboClock, TurboState, UiStore, ViewContext, createNetwork, createStakeGame, defaultPhases, modeCostOf, readRuntime, roundEvents, roundInfo, urlParam };
197
+ //#region src/currency/index.d.ts
198
+ /** Three-decimal fiat missing from the @open-slot-ui currency table — the Gulf/Arab
199
+ * dinars & rials (the lib already covers KWD/BHD/JOD). `symbol` follows the lib's short
200
+ * latinised convention for the region (KWD→"KD", …). */
201
+ declare const EXTRA_DECIMALS: Readonly<Record<string, {
202
+ decimals: number;
203
+ symbol: string;
204
+ }>>;
205
+ /**
206
+ * `resolveCurrency`, but with the missing 3-decimal dinars/rials patched in. Use this
207
+ * everywhere a currency code becomes a `CurrencySpec` so precision is correct for every
208
+ * code — `createStakeGame` routes all of its resolution through here.
209
+ */
210
+ declare function currencyFor(code: string): CurrencySpec;
211
+ //#endregion
212
+ export { API_AMOUNT_MULTIPLIER, AudioPort, AudioSpec, AudioValue, AuthenticateRequest, AuthenticateResponse, BOOK_AMOUNT_MULTIPLIER, Balance, BalanceStore, BeatClock, BeatOptions, CreateStakeGameOptions, DEFAULT_TURBO_SPEEDS, EXTRA_DECIMALS, EndRoundRequest, EndRoundResponse, FSM, FeatureItem, GameConfig, GameLoader, GameRound, GameSnapshot, HudPort, IdlePhase, InstantTicker, InterpretBook, JurisdictionConfig, LoaderConfig, LoaderPort, MockNetworkManager, MockOptions, ModeConfig, NetworkManager, Phase, PhaseContext, PlayArgs, PlayRequest, PlayResponse, PresentPhase, RealTicker, ReplayInfo, ReplayLaunch, ReplayParams, RgsConfig, RgsError, RootStore, Round, RoundInfo, RuntimeConfig, ScriptedRound, SessionStore, SettlePhase, SpinPhase, StakeGame, StakeNetworkManager, StakeNetworkOptions, Ticker, TurboClock, TurboState, UiStore, ViewContext, blockingBeat, createLoader, createNetwork, createStakeGame, currencyFor, defaultPhases, modeCostOf, readRuntime, roundEvents, roundInfo, urlParam };
96
213
  //# sourceMappingURL=index.d.ts.map