@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/README.md CHANGED
@@ -61,7 +61,18 @@ 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, … }` to `createStakeGame`
73
+ for a configurable loading screen (pulsing logo/spinner + progress bar over your backdrop)
74
+ that shows before anything heavy loads and pops away when ready. `manual: true` +
75
+ `ctx.loader.done()` to hold it until your scene's art is in. `createLoader()` is also standalone.
65
76
  - `@stakeplate/core/stores` — the MobX stores (balance, ui) for composing game state.
66
77
  - `@stakeplate/core/testing` — the mock RGS, scriptable network, instant ticker.
67
78
  - (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-DsA8mDpk.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. */
@@ -118,11 +129,25 @@ interface GameConfig {
118
129
  declare function modeCostOf(config: GameConfig, mode: string): number;
119
130
  //#endregion
120
131
  //#region src/engine/fsm.d.ts
132
+ /** A fixed value or a per-voice random spread (`{ base, jitter }`) — structurally matches
133
+ * zvuk's `VoiceJitter` without importing it, so the engine stays zvuk-free. */
134
+ type AudioValue = number | {
135
+ base?: number;
136
+ jitter?: number;
137
+ };
138
+ /** The minimal boot-loader surface a game's phases touch (satisfied by the core's
139
+ * `GameLoader`) — kept structural so the engine needn't import the loader module. */
140
+ interface LoaderPort {
141
+ setProgress(p: number): void;
142
+ done(): Promise<void>;
143
+ }
121
144
  /** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */
122
145
  interface AudioPort {
146
+ /** Fire a one-shot. `volume`/`pitch` accept jitter so repeated cues vary per voice. */
123
147
  play(name: string, opts?: {
124
148
  bus?: string;
125
- volume?: number;
149
+ volume?: AudioValue;
150
+ pitch?: AudioValue;
126
151
  }): void;
127
152
  music(name: string, opts?: {
128
153
  fadeIn?: number;
@@ -144,6 +169,8 @@ interface PhaseContext<T = unknown, V = unknown, E = unknown> {
144
169
  readonly audio: AudioPort | null;
145
170
  /** Turbo speed + slam-stop (core-owned). Use `ctx.turbo.delay(ms)` for spin/anim timing. */
146
171
  readonly turbo: TurboState;
172
+ /** The boot loader, if one was configured (else `null`) — drive progress / `done()`. */
173
+ readonly loader: LoaderPort | null;
147
174
  readonly interpretBook: InterpretBook<T, E>;
148
175
  readonly fsm: FSM<T, V, E>;
149
176
  round: GameRound<T, E> | null;
@@ -194,6 +221,32 @@ declare function bindMixerToHud(mixer: MixerLike, hud: BootedHud, opts?: {
194
221
  storageKey?: string;
195
222
  }): () => void;
196
223
  //#endregion
224
+ //#region src/audio/inputs.d.ts
225
+ /** Cue name per HUD input event. Any omitted → that input stays silent. */
226
+ interface InputSoundMap {
227
+ /** Spin button press (and each autoplay/hold re-spin fires `spinRequested`). */
228
+ spin?: string;
229
+ /** Bet +/- (the bet stepper's `valueChanged`). */
230
+ bet?: string;
231
+ /** Autoplay engaged (count picked). */
232
+ autoplay?: string;
233
+ /** Turbo cycler toggled. */
234
+ turbo?: string;
235
+ /** Any switch toggled (turbo/sound/…) — a generic UI click. */
236
+ toggle?: string;
237
+ /** Slam-stop (tap-to-skip the spin). */
238
+ skip?: string;
239
+ }
240
+ interface InputSoundOptions {
241
+ /** Bus to route the cues to (default `'ui'`). */
242
+ bus?: string;
243
+ }
244
+ /**
245
+ * Wire the HUD input events to sounds. Returns a disposer. Safe to call with a partial map —
246
+ * only the mapped inputs subscribe.
247
+ */
248
+ declare function bindInputSounds(audio: AudioPort, hud: BootedHud, map: InputSoundMap, opts?: InputSoundOptions): () => void;
249
+ //#endregion
197
250
  //#region src/audio/index.d.ts
198
251
  /** Buses in the MUSIC group (driven by the Music slider). */
199
252
  type MusicBus = 'music' | 'ambience';
@@ -209,6 +262,18 @@ interface GameAudioOptions {
209
262
  /** Duck the MUSIC group while THESE sfx buses are active (default `['wins']`; `null` = off). */
210
263
  duckMusicFrom?: SfxBus | SfxBus[] | null;
211
264
  duckAmount?: number;
265
+ /**
266
+ * RMS loudness normalization applied to every loaded one-shot so the whole manifest sits
267
+ * at a consistent level (no hand-tuned per-file gains). A per-entry `normalize` overrides
268
+ * this. Default `true`. Pass a `LoudnessOptions` object to tune the target, or `false` off.
269
+ */
270
+ normalize?: boolean | LoudnessOptions;
271
+ /**
272
+ * Voice cap applied to EACH effects bus so stacked one-shots (a multi-pay cascade, rapid
273
+ * contacts) don't machine-gun — steals the oldest voice when the cap is hit. Default
274
+ * `{ max: 8, steal: 'oldest' }`. Pass `null` to leave the effects buses uncapped.
275
+ */
276
+ sfxConcurrency?: ConcurrencyConfig | null;
212
277
  }
213
278
  /**
214
279
  * A sound to load onto a bus, or a music track (intro/loop/outro → the `music` bus).
@@ -221,6 +286,7 @@ type SoundEntry = {
221
286
  kind?: 'sound';
222
287
  url: string | string[];
223
288
  bus?: BusName;
289
+ normalize?: boolean | LoudnessOptions;
224
290
  } | {
225
291
  name: string;
226
292
  kind: 'music';
@@ -236,6 +302,7 @@ declare class GameAudio implements AudioPort, MixerLike {
236
302
  private readonly effectsGroup;
237
303
  private readonly duckFrom;
238
304
  private readonly duckAmount;
305
+ private readonly normalizeDefault;
239
306
  private unlocked;
240
307
  private currentMusic;
241
308
  constructor(opts?: GameAudioOptions);
@@ -250,9 +317,12 @@ declare class GameAudio implements AudioPort, MixerLike {
250
317
  group(name: MixerGroup): BusGroup;
251
318
  /** Preload a manifest of sounds + music onto their buses. */
252
319
  load(entries: SoundEntry[]): Promise<void>;
320
+ /** Fire a one-shot. `volume`/`pitch` accept a `VoiceJitter` (`{ base, jitter }`) so
321
+ * repeated cues — contacts, ticks — vary per voice instead of sounding cloned. */
253
322
  play(name: string, opts?: {
254
323
  bus?: BusName;
255
- volume?: number;
324
+ volume?: number | VoiceJitter;
325
+ pitch?: number | VoiceJitter;
256
326
  }): void;
257
327
  music(name: string, opts?: {
258
328
  fadeIn?: number;
@@ -272,5 +342,5 @@ declare function bindAudioToHud(audio: GameAudio, hud: BootedHud, opts?: {
272
342
  storageKey?: string;
273
343
  }): () => void;
274
344
  //#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
345
+ 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 };
346
+ //# sourceMappingURL=index-DsA8mDpk.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-DsA8mDpk.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