@stakeplate/core 0.2.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 +9 -4
- package/dist/audio.d.ts +1 -1
- package/dist/{index-DsA8mDpk.d.ts → index-BXD-eK1I.d.ts} +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +81 -11
- package/dist/index.js.map +1 -1
- package/dist/rules.d.ts +5 -3
- package/dist/rules.js +17 -6
- package/dist/rules.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,10 +69,15 @@ config.socialMessages = { en: built.socialEn }; // auto-derived social wording
|
|
|
69
69
|
stripped) with `scripts/encode-audio.sh OUTDIR raw/*.wav`, then load the pair with
|
|
70
70
|
`{ url: ['clip.webm', 'clip.mp3'] }`.
|
|
71
71
|
- `@stakeplate/core/rules` — `buildRules` compliant menu + `toSocial`/`findRestricted` + dict.
|
|
72
|
-
- Boot loader — pass `loader: { title, backgroundImage, logo, accent, … }` to
|
|
73
|
-
for a
|
|
74
|
-
|
|
75
|
-
|
|
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.)
|
|
76
81
|
- `@stakeplate/core/stores` — the MobX stores (balance, ui) for composing game state.
|
|
77
82
|
- `@stakeplate/core/testing` — the mock RGS, scriptable network, instant ticker.
|
|
78
83
|
- (soon) `/scene`, `/i18n`.
|
package/dist/audio.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
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-
|
|
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
2
|
export { BusName, GameAudio, GameAudioOptions, type InputSoundMap, type InputSoundOptions, type MixerGroup, type MixerLike, MusicBus, SfxBus, SoundEntry, bindAudioToHud, bindInputSounds, bindMixerToHud, createGameAudio };
|
|
@@ -111,6 +111,17 @@ interface GameConfig {
|
|
|
111
111
|
rtp?: number;
|
|
112
112
|
/** Mode key → cost multiplier (a number) or a `ModeConfig`. `base` defaults to 1. */
|
|
113
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;
|
|
114
125
|
/** Rules/info menu for the HUD (`@open-slot-ui` `MenuSpec`) — build it with `@stakeplate/core/rules` `buildRules`. */
|
|
115
126
|
rules?: unknown;
|
|
116
127
|
/** i18n messages per locale (`{ en: { key: text }, es: {…} }`) for the HUD + rules text. */
|
|
@@ -343,4 +354,4 @@ declare function bindAudioToHud(audio: GameAudio, hud: BootedHud, opts?: {
|
|
|
343
354
|
}): () => void;
|
|
344
355
|
//#endregion
|
|
345
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 };
|
|
346
|
-
//# sourceMappingURL=index-
|
|
357
|
+
//# sourceMappingURL=index-BXD-eK1I.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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 { 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-
|
|
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
8
|
import { CurrencySpec } from "@open-slot-ui/core";
|
package/dist/index.js
CHANGED
|
@@ -149,15 +149,14 @@ var SpinPhase = class {
|
|
|
149
149
|
mode
|
|
150
150
|
});
|
|
151
151
|
const raw = play.round;
|
|
152
|
-
let settledApi = play.balance.amount;
|
|
153
|
-
if (raw.active) settledApi = (await network.endRound()).balance.amount;
|
|
154
152
|
const info = roundInfo(raw, bet, cost);
|
|
155
153
|
const data = ctx.interpretBook(raw, info);
|
|
154
|
+
const settledMoney = play.balance.amount / API_AMOUNT_MULTIPLIER;
|
|
156
155
|
ctx.round = {
|
|
157
156
|
...info,
|
|
158
157
|
data,
|
|
159
158
|
active: raw.active ?? false,
|
|
160
|
-
balance:
|
|
159
|
+
balance: raw.active ? settledMoney + info.totalWin : settledMoney,
|
|
161
160
|
raw
|
|
162
161
|
};
|
|
163
162
|
} catch (err) {
|
|
@@ -192,6 +191,12 @@ var SettlePhase = class {
|
|
|
192
191
|
const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;
|
|
193
192
|
const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1e3;
|
|
194
193
|
if (minMs > 0 && elapsed < minMs) await ctx.ticker.delay(minMs - elapsed);
|
|
194
|
+
if (r.active) try {
|
|
195
|
+
const end = await ctx.network.endRound();
|
|
196
|
+
ctx.stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.warn("[settle] end-round failed; balance reconciles on next authenticate:", err);
|
|
199
|
+
}
|
|
195
200
|
}
|
|
196
201
|
ctx.stores.ui.setSpinning(false);
|
|
197
202
|
await ctx.fsm.transition("idle");
|
|
@@ -466,6 +471,42 @@ function createLoader(config = {}) {
|
|
|
466
471
|
* game never sets it — it's compliance, not design.
|
|
467
472
|
*/
|
|
468
473
|
const STAKE_CONFIRM_ABOVE_COST = 2;
|
|
474
|
+
/** Fallback min-payout coefficient used ONLY when `config.minBet` is unset: the
|
|
475
|
+
* currency-derived floor is `minUnit / MIN_PAYOUT_COEF` (= 5 minimal units at 0.2). */
|
|
476
|
+
const MIN_PAYOUT_COEF = .2;
|
|
477
|
+
/** Persisted player UI preferences (sound mute + turbo mode). Best-effort — every access
|
|
478
|
+
* is guarded: Safari Private Browsing, a cross-origin-iframe storage block, or a hardened
|
|
479
|
+
* session throws on access, so this degrades to "no persistence", never into the boot. */
|
|
480
|
+
const PREFS_KEY = "stakeplate.prefs";
|
|
481
|
+
function readPrefs() {
|
|
482
|
+
try {
|
|
483
|
+
return JSON.parse(localStorage.getItem(PREFS_KEY) || "{}");
|
|
484
|
+
} catch {
|
|
485
|
+
return {};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function writePrefs(patch) {
|
|
489
|
+
try {
|
|
490
|
+
localStorage.setItem(PREFS_KEY, JSON.stringify({
|
|
491
|
+
...readPrefs(),
|
|
492
|
+
...patch
|
|
493
|
+
}));
|
|
494
|
+
} catch {}
|
|
495
|
+
}
|
|
496
|
+
/** Drop server bet-ladder levels below the minimum bet — the explicit `config.minBet`, else
|
|
497
|
+
* the currency-derived floor (5 minimal units) — so the smallest win can't round below one
|
|
498
|
+
* minimal unit. Never returns empty (keeps the largest level if every level is below). */
|
|
499
|
+
function applyMinBet(levels, minBet, decimals) {
|
|
500
|
+
const minUnit = 10 ** -decimals;
|
|
501
|
+
const floor = minBet ?? minUnit / MIN_PAYOUT_COEF;
|
|
502
|
+
const filtered = levels.filter((b) => b >= floor - minUnit / 1e3);
|
|
503
|
+
return filtered.length ? filtered : [Math.max(...levels)];
|
|
504
|
+
}
|
|
505
|
+
/** Snap a wanted bet up to the first ladder level that is >= it (or the closest). */
|
|
506
|
+
function snapToLadder(levels, want) {
|
|
507
|
+
if (levels.includes(want)) return want;
|
|
508
|
+
return levels.find((b) => b >= want) ?? levels[levels.length - 1] ?? want;
|
|
509
|
+
}
|
|
469
510
|
function createStakeGame(opts) {
|
|
470
511
|
const runtime = readRuntime({ overrides: opts.runtime });
|
|
471
512
|
const network = opts.network ?? createNetwork(runtime);
|
|
@@ -626,10 +667,10 @@ function createStakeGame(opts) {
|
|
|
626
667
|
const currency = auth.balance.currency;
|
|
627
668
|
const juris = auth.config.jurisdiction ?? {};
|
|
628
669
|
const rtp = auth.config.rtp ?? opts.config.rtp ?? 96;
|
|
629
|
-
const betLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);
|
|
670
|
+
const betLevels = applyMinBet(auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER), opts.config.minBet, currencyFor(currency).decimals ?? 2);
|
|
630
671
|
const active = auth.round;
|
|
631
672
|
const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;
|
|
632
|
-
const defaultBet = active?.active ? active.amount / API_AMOUNT_MULTIPLIER / activeCost : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER;
|
|
673
|
+
const defaultBet = snapToLadder(betLevels, active?.active ? active.amount / API_AMOUNT_MULTIPLIER / activeCost : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER);
|
|
633
674
|
const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;
|
|
634
675
|
hud = mountHud(app, buildSpec({
|
|
635
676
|
currency,
|
|
@@ -691,7 +732,10 @@ function createStakeGame(opts) {
|
|
|
691
732
|
}));
|
|
692
733
|
disposers.push(hud.on("turboChanged", (p) => {
|
|
693
734
|
const e = p;
|
|
694
|
-
if (typeof e.index === "number")
|
|
735
|
+
if (typeof e.index === "number") {
|
|
736
|
+
turbo.setLevel(e.index);
|
|
737
|
+
writePrefs({ turbo: e.index });
|
|
738
|
+
}
|
|
695
739
|
}));
|
|
696
740
|
disposers.push(hud.on("skipRequested", () => turbo.skip()));
|
|
697
741
|
const features = buyFeaturesOf(opts.config);
|
|
@@ -704,9 +748,13 @@ function createStakeGame(opts) {
|
|
|
704
748
|
};
|
|
705
749
|
disposers.push(mountBuyFeatureModal(app, hud, features, {
|
|
706
750
|
activation: "single",
|
|
707
|
-
getBet: () =>
|
|
751
|
+
getBet: () => hud.ui.betStepper.value,
|
|
708
752
|
onBuy: (id) => {
|
|
709
753
|
if (machine.current !== "idle") return;
|
|
754
|
+
if (stores.ui.activeMode) {
|
|
755
|
+
stores.ui.setActiveMode(null);
|
|
756
|
+
applyBetDisplay();
|
|
757
|
+
}
|
|
710
758
|
stores.ui.setOneShotMode(id);
|
|
711
759
|
beginSpin();
|
|
712
760
|
},
|
|
@@ -743,19 +791,41 @@ function createStakeGame(opts) {
|
|
|
743
791
|
}
|
|
744
792
|
}
|
|
745
793
|
if (audio && isAudioSpec(opts.audio) && opts.audio.inputSounds) disposers.push(bindInputSounds(audio, hud, opts.audio.inputSounds));
|
|
794
|
+
const prefs = readPrefs();
|
|
795
|
+
if (typeof prefs.muted === "boolean") hud.setMuted(prefs.muted);
|
|
796
|
+
if (typeof prefs.turbo === "number" && prefs.turbo > 0) {
|
|
797
|
+
hud.ui.turbo.setIndex(prefs.turbo);
|
|
798
|
+
turbo.setLevel(prefs.turbo);
|
|
799
|
+
}
|
|
800
|
+
disposers.push(hud.ui.muted.subscribe((m) => writePrefs({ muted: m })));
|
|
746
801
|
if (active?.active) {
|
|
747
802
|
const end = await network.endRound();
|
|
748
803
|
const raw = active;
|
|
749
804
|
const info = roundInfo(raw, defaultBet, activeCost);
|
|
750
|
-
|
|
751
|
-
|
|
805
|
+
const settledBal = end.balance.amount / API_AMOUNT_MULTIPLIER;
|
|
806
|
+
stores.balance.setBalance(settledBal);
|
|
807
|
+
const resumeRound = {
|
|
752
808
|
...info,
|
|
753
809
|
data: opts.interpretBook(raw, info),
|
|
754
810
|
active: false,
|
|
755
|
-
balance:
|
|
811
|
+
balance: settledBal,
|
|
756
812
|
raw
|
|
757
813
|
};
|
|
758
|
-
await machine.transition("
|
|
814
|
+
await machine.transition("idle");
|
|
815
|
+
hud.showFatal("You have an unfinished round. Continue to see how it ends.", {
|
|
816
|
+
title: "Round in progress",
|
|
817
|
+
tone: "info",
|
|
818
|
+
actions: [{
|
|
819
|
+
label: "Continue",
|
|
820
|
+
variant: "primary",
|
|
821
|
+
onSelect: () => {
|
|
822
|
+
hud.hideNotice();
|
|
823
|
+
hud.lockInput();
|
|
824
|
+
ctx.round = resumeRound;
|
|
825
|
+
machine.transition("present").finally(() => hud.unlockInput());
|
|
826
|
+
}
|
|
827
|
+
}]
|
|
828
|
+
});
|
|
759
829
|
} else {
|
|
760
830
|
stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);
|
|
761
831
|
await machine.transition("idle");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/engine/turbo.ts","../src/engine/round.ts","../src/engine/fsm.ts","../src/engine/phases.ts","../src/engine/beat.ts","../src/game/config.ts","../src/currency/index.ts","../src/loader/index.ts","../src/game/createStakeGame.ts"],"sourcesContent":["// Turbo speed + slam-stop, owned by the core. The HUD's turbo control (a 2- or 3-mode cycler)\n// drives a speed multiplier; the game's Present phase awaits `ctx.turbo.delay(ms)` for its\n// spin/anim durations, so turbo shortens them and a slam-stop (`skipRequested`) resolves them\n// instantly — with ZERO game logic. Compliance waits (minimumRoundDuration) use `ctx.ticker`,\n// NOT this, so they're never sped up or skipped.\n\n/** Delay multipliers per turbo level: off (1×) / turbo (0.4×) / super (0.12×). */\nexport const DEFAULT_TURBO_SPEEDS = [1, 0.4, 0.12];\n\nexport interface TurboState {\n /** 0 = off, 1 = turbo, 2 = super — the HUD turbo cycler's index. */\n readonly level: number;\n /** The EFFECTIVE delay multiplier right now — the turbo level, floored by the autoplay\n * speed while autoplay is running (`ctx.turbo.delay` already applies it). */\n readonly speed: number;\n /** True while autoplay/hold is running — game animations are shortened even at turbo off. */\n readonly autoplay: boolean;\n /** True once the player slam-stopped this round (delays resolve instantly). */\n readonly skipped: boolean;\n /** A turbo- + autoplay- + slam-stop-aware delay for GAME animations. NOT for compliance waits. */\n delay(ms: number): Promise<void>;\n}\n\nexport class TurboClock implements TurboState {\n private readonly speeds: number[];\n /** Delay multiplier applied while autoplay/hold runs (the floor for `speed`). Defaults to\n * the turbo (level-1) speed, so autoplay plays at least at turbo pace even at turbo off. */\n private readonly autoplaySpeed: number;\n private _level = 0;\n private _autoplay = false;\n private _skipped = false;\n private readonly pending = new Set<() => void>();\n\n constructor(speeds?: number[], autoplaySpeed?: number) {\n this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;\n this.autoplaySpeed = autoplaySpeed ?? this.speeds[1] ?? 0.5;\n }\n\n get level(): number {\n return this._level;\n }\n get speed(): number {\n const levelSpeed = this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;\n // Autoplay shortens animations even at turbo off — but never SLOWS a faster turbo level.\n return this._autoplay ? Math.min(levelSpeed, this.autoplaySpeed) : levelSpeed;\n }\n get autoplay(): boolean {\n return this._autoplay;\n }\n get skipped(): boolean {\n return this._skipped;\n }\n\n /** Set the turbo level (clamped to the configured speeds). */\n setLevel(index: number): void {\n const i = Number.isFinite(index) ? Math.trunc(index) : 0;\n this._level = Math.max(0, Math.min(i, this.speeds.length - 1));\n }\n\n /** Mark autoplay/hold as running so `delay()` shortens game animations. The core sets\n * this on autoplayStarted/holdSpinStarted and clears it when they stop. */\n setAutoplay(active: boolean): void {\n this._autoplay = active;\n }\n\n /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */\n skip(): void {\n this._skipped = true;\n const cbs = [...this.pending];\n this.pending.clear();\n for (const cb of cbs) cb();\n }\n\n /** Clear the slam-stop flag — the core calls this at the start of each spin. */\n resetSkip(): void {\n this._skipped = false;\n }\n\n delay(ms: number): Promise<void> {\n if (this._skipped || ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n this.pending.delete(finish);\n resolve();\n };\n const timer = setTimeout(finish, ms * this.speed);\n this.pending.add(finish);\n });\n }\n}\n","// The round model the engine drives. The engine derives all MONEY (multiplier, win,\n// stake) from the raw wire round + the bet; the game's `interpretBook` only parses the\n// book EVENTS into its own model (`data`) — the one place a game touches a round.\n\nimport { API_AMOUNT_MULTIPLIER, BOOK_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\n\n/** Money facts of a settled round (all MAJOR units except `multiplier`, a ratio). */\nexport interface RoundInfo {\n mode: string;\n /** Base bet (before the mode's cost multiplier). */\n bet: number;\n /** Mode cost multiplier (× base bet) — the stake charged. */\n cost: number;\n /** The amount staked = bet × cost. */\n stake: number;\n /** Round multiplier relative to the BASE bet (totalWin / bet). */\n multiplier: number;\n /** Total win credited this round (derived: multiplier × bet). */\n totalWin: number;\n /** The server's AUTHORITATIVE win (`raw.payout`, major units); falls back to totalWin\n * when the round carries no explicit payout. Trust this over `totalWin` when they differ. */\n payout: number;\n}\n\n/**\n * A resolved round: money facts + the game's parsed model + the raw wire round. `E` is the\n * game's book-event type — declared once on `createStakeGame`, so `raw` (and `interpretBook`)\n * are TYPED rather than `unknown`.\n */\nexport interface GameRound<T = unknown, E = unknown> extends RoundInfo {\n /** The game's parsed model, from `interpretBook` — driven to the Present phase. */\n data: T;\n /** Whether the raw round still needs `/wallet/end-round` (the engine settles it). */\n active: boolean;\n /** Authoritative post-settlement balance (major units) — applied by the Settle phase. */\n balance: number;\n /** The raw, fully-typed wire round — the game's real server data. */\n raw: Round<E>;\n}\n\n/** The game's ONE money-logic seam: parse the raw round's (typed) events into your model. Pure. */\nexport type InterpretBook<T, E = unknown> = (raw: Round<E>, info: RoundInfo) => T;\n\n/** Build the money facts from a raw wire round + the base bet + the mode cost. */\nexport function roundInfo<E = unknown>(raw: Round<E>, bet: number, cost: number): RoundInfo {\n // Stake wire convention: `payoutMultiplier` is a plain Payout/Amount ratio (1× → 1)\n // in BOOK units (×100). totalWin is relative to the BASE bet; `payout` is the server's\n // own win amount (API units → major) when the round carries one.\n const multiplier = raw.payoutMultiplier / BOOK_AMOUNT_MULTIPLIER;\n const totalWin = multiplier * bet;\n const payout = raw.payout != null ? raw.payout / API_AMOUNT_MULTIPLIER : totalWin;\n return { mode: raw.mode, bet, cost, stake: bet * cost, multiplier, totalWin, payout };\n}\n","// The round state machine. A round is an explicit sequence of awaited phases\n// (Idle → Spin → Present → Settle); each `enter` does its work + transitions on. The\n// core ships Idle/Spin/Settle; the game writes Present (and may override any). The FSM\n// + context are pure — the HUD/view/audio arrive via ports, so a whole round runs\n// headless (see the InstantTicker + MockNetworkManager).\n\nimport type { NetworkManager } from '../rgs/network';\nimport type { RootStore } from '../stores/index';\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\nimport type { HudPort } from './hud-port';\nimport type { GameRound, InterpretBook } from './round';\nimport type { GameConfig } from '../game/config';\n\n/** A fixed value or a per-voice random spread (`{ base, jitter }`) — structurally matches\n * zvuk's `VoiceJitter` without importing it, so the engine stays zvuk-free. */\nexport type AudioValue = number | { base?: number; jitter?: number };\n\n/** The minimal boot-loader surface a game's phases touch (satisfied by the core's\n * `GameLoader`) — kept structural so the engine needn't import the loader module. */\nexport interface LoaderPort {\n setProgress(p: number): void;\n done(): Promise<void>;\n}\n\n/** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */\nexport interface AudioPort {\n /** Fire a one-shot. `volume`/`pitch` accept jitter so repeated cues vary per voice. */\n play(name: string, opts?: { bus?: string; volume?: AudioValue; pitch?: AudioValue }): void;\n music(name: string, opts?: { fadeIn?: number }): void;\n stopMusic(opts?: { fade?: number }): void;\n}\n\n/** 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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"}
|
|
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 const info = roundInfo(raw, bet, cost);\n const data = ctx.interpretBook(raw, info);\n // DO NOT /wallet/end-round here. An ACTIVE round is left OPEN so a refresh DURING\n // Present (the game's reel/scene animation) can recover it — SettlePhase settles it\n // once the round has finished playing out. For an active round `play.balance` is\n // post-DEBIT (stake gone, win not yet credited), so the post-win balance the player\n // sees at Settle is play.balance + the (authoritative-from-book) win; SettlePhase then\n // reconciles to the server's own settled figure. A round that is already settled (a\n // loss, or an immediate-settle mock) reports its FINAL balance in play.balance — use\n // it verbatim, or the win would be double-counted.\n const settledMoney = play.balance.amount / API_AMOUNT_MULTIPLIER;\n ctx.round = {\n ...info,\n data,\n active: raw.active ?? false,\n balance: raw.active ? settledMoney + info.totalWin : settledMoney,\n raw,\n };\n } catch (err) {\n stores.balance.refund(stake); // the round never happened\n stores.ui.setSpinning(false);\n const msg = err instanceof Error ? err.message : String(err);\n const code = msg.match(/ERR_[A-Z_]+/)?.[0];\n if (code) hud.showRgsError(code);\n else hud.showError(msg);\n await ctx.fsm.transition('idle');\n return;\n }\n\n await ctx.fsm.transition('present');\n }\n}\n\n/** DEFAULT Present — nothing to animate. The game overrides `present` to play its scene. */\nexport class PresentPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'present';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n await ctx.fsm.transition('settle');\n }\n}\n\n/** Apply the authoritative balance + win, settle the round for the HUD, then Idle. */\nexport class SettlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'settle';\n private roundStartedAt = 0;\n\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const r = ctx.round;\n if (r) {\n ctx.stores.balance.settle(r.balance, r.totalWin);\n ctx.hud.reportRound(r.totalWin, r.bet);\n // minimumRoundDuration: pad to the jurisdiction minimum (best-effort; the boot\n // records the round start, so this covers the whole spin→settle span).\n const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;\n const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1000;\n if (minMs > 0 && elapsed < minMs) await ctx.ticker.delay(minMs - elapsed);\n // The round has now finished playing out visually (Present + the min-duration pad).\n // ONLY NOW settle it on the server — SpinPhase deliberately left it OPEN so a refresh\n // mid-Present recovers the round instead of losing it. The win already shows (from the\n // book); this reconciles to the server's authoritative post-settlement balance.\n if (r.active) {\n try {\n const end = await ctx.network.endRound();\n ctx.stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);\n } catch (err) {\n // The win is already credited locally; a settle hiccup self-heals on the next\n // authenticate (the RGS reports the still-open round). Surface it, don't crash.\n console.warn('[settle] end-round failed; balance reconciles on next authenticate:', err);\n }\n }\n }\n ctx.stores.ui.setSpinning(false);\n await ctx.fsm.transition('idle');\n }\n\n /** Called by SpinPhase-adjacent boot wiring to timestamp the round start. */\n markStart(now: number): void {\n this.roundStartedAt = now;\n }\n}\n\n/** The default phase set (game phases are appended after → same-name overrides win). */\nexport function defaultPhases<T = unknown, V = unknown, E = unknown>(): Phase<T, V, E>[] {\n return [new IdlePhase<T, V, E>(), new SpinPhase<T, V, E>(), new PresentPhase<T, V, E>(), new SettlePhase<T, V, E>()];\n}\n","// A blocking \"announcement beat\" — the pattern behind moments like a bonus trigger or a\n// \"… ELIMINATED!\" banner: pause so the reveal settles, SHOW the announcement, HOLD it\n// alone on screen, HIDE it, then a trailing pause before play resumes. The point is that\n// the round never plays UNDER the announcement — everything waits for the beat.\n//\n// The game owns the visuals (`show`/`hide` — mount a banner, toggle a class, play an sfx);\n// the core owns the timing + sequencing so every game's beats feel consistent.\n\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\n\nexport interface BeatOptions {\n /** Pause before the announcement shows — lets the reveal settle. Default 0. */\n leadMs?: number;\n /** How long the announcement holds alone on screen. */\n holdMs: number;\n /** Pause after hiding, before play resumes. Default 0. */\n trailMs?: number;\n /** Reveal the announcement (banner/sfx). Called after the lead pause. */\n show: () => void;\n /** Hide it. Called after the hold, before the trailing pause. Optional (some banners\n * self-dismiss). */\n hide?: () => void;\n /**\n * Timing source. `'real'` (default) is a fixed, unskippable beat via the ticker — an\n * announcement is a deliberate moment that a slam-stop shouldn't blow past. `'turbo'`\n * makes the pauses turbo-/autoplay-/slam-stop-aware (shorter under turbo, skippable).\n */\n timing?: 'real' | 'turbo';\n}\n\n/** Timing surface the beat needs — a `PhaseContext` satisfies it (`ctx.ticker`/`ctx.turbo`). */\nexport interface BeatClock {\n ticker: Ticker;\n turbo: TurboState;\n}\n\n/**\n * Run a blocking announcement beat: lead → show → hold → hide → trail. Awaitable, so a\n * Present phase just `await blockingBeat(ctx, { … })` and the round pauses for the whole\n * thing. Returns when the beat (including the trailing pause) is over.\n */\nexport async function blockingBeat(ctx: BeatClock, opts: BeatOptions): Promise<void> {\n const wait = opts.timing === 'turbo' ? (ms: number) => ctx.turbo.delay(ms) : (ms: number) => ctx.ticker.delay(ms);\n if (opts.leadMs && opts.leadMs > 0) await wait(opts.leadMs);\n opts.show();\n await wait(opts.holdMs);\n opts.hide?.();\n if (opts.trailMs && opts.trailMs > 0) await wait(opts.trailMs);\n}\n","// The per-game configuration passed to `createStakeGame`. Everything the core needs to\n// stand up the HUD + drive the round; the game's mechanics live in its scene + phases.\n\nexport interface ModeConfig {\n /** Cost multiplier (× base bet). `base` is 1. */\n cost: number;\n /** A one-shot bought bonus — shown as a `Buy` card in the buy-feature modal (opened by the\n * bonus button). Buying it spins this mode once (through the jurisdiction confirm gate). */\n buy?: boolean;\n /** An activatable per-spin bet surcharge — shown as an `Activate` card (vs a one-tap `Buy`). */\n boost?: boolean;\n /** Display name for the feature card (defaults to the capitalized mode key). */\n name?: string;\n /** Card art (URL or data URI) for the feature card. A neutral gradient is used when absent. */\n image?: string;\n}\n\nexport interface GameConfig {\n title: string;\n version?: string;\n /** Fallback currency code; the session/`?currency=` wins. */\n currency?: string;\n /**\n * Theoretical RTP percentage — DISPLAY ONLY (the RTP readout + rules). NOT authoritative:\n * the server (`auth.config.rtp`) wins, and the certified math report is the source of\n * truth. This is only a last-resort fallback; prefer never hand-typing it (drift risk).\n */\n rtp?: number;\n /** Mode key → cost multiplier (a number) or a `ModeConfig`. `base` defaults to 1. */\n modes?: Record<string, ModeConfig | number>;\n /**\n * Minimum selectable bet, in MAJOR units of the account currency — an EXPLICIT client\n * floor. Any server bet-ladder level below this is dropped from the HUD ladder, so the\n * smallest possible win (bet × the game's minimum payout) can never round below one\n * minimal currency unit. Set it per game to your smallest legal stake — e.g. `0.05` when\n * the minimum payout is ×0.2 (0.05 × 0.2 = 0.01 = one cent). `undefined` → derive it from\n * the currency (5 minimal units — a safe default for a ×0.2 minimum payout). The server\n * ladder stays authoritative for the LEVELS offered; this only trims the low end + snaps\n * the default up to the first legal level.\n */\n minBet?: number;\n /** Rules/info menu for the HUD (`@open-slot-ui` `MenuSpec`) — build it with `@stakeplate/core/rules` `buildRules`. */\n rules?: unknown;\n /** i18n messages per locale (`{ en: { key: text }, es: {…} }`) for the HUD + rules text. */\n messages?: Record<string, Record<string, string>>;\n /** SOCIAL/sweepstakes wording per locale — swapped in when social mode is on. Merge in\n * `buildRules().socialEn` so the core's disclaimer/guide are social-safe. */\n socialMessages?: Record<string, Record<string, string>>;\n /** Delay multipliers per turbo level (off / turbo / super). Default `[1, 0.4, 0.12]`. */\n turboSpeeds?: number[];\n /** Pause (ms) between autoplay/hold spins. Default 250. Scales with turbo. */\n autoplayGapMs?: number;\n /** Extra `@open-slot-ui` `UISpec` fields merged into the built spec (escape hatch). */\n spec?: Record<string, unknown>;\n}\n\n// NOTE (server-authoritative): the bet ladder (`betLevels` + `defaultBetLevel`) and the\n// buy-feature confirm threshold are NOT game config — the RGS `authenticate` response owns\n// the ladder (per currency/jurisdiction) and the jurisdiction owns the confirm policy. The\n// core reads them from `auth.config`; a game only declares that a mode is a buy (`modes`).\n\n/** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */\nexport function modeCostOf(config: GameConfig, mode: string): number {\n const m = config.modes?.[mode];\n if (typeof m === 'number') return m;\n if (m && typeof m === 'object') return m.cost;\n return 1;\n}\n","// @stakeplate/core/currency — 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/** Fallback min-payout coefficient used ONLY when `config.minBet` is unset: the\n * currency-derived floor is `minUnit / MIN_PAYOUT_COEF` (= 5 minimal units at 0.2). */\nconst MIN_PAYOUT_COEF = 0.2;\n\n/** Persisted player UI preferences (sound mute + turbo mode). Best-effort — every access\n * is guarded: Safari Private Browsing, a cross-origin-iframe storage block, or a hardened\n * session throws on access, so this degrades to \"no persistence\", never into the boot. */\nconst PREFS_KEY = 'stakeplate.prefs';\ninterface GamePrefs {\n muted?: boolean;\n turbo?: number;\n}\nfunction readPrefs(): GamePrefs {\n try {\n return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}') as GamePrefs;\n } catch {\n return {};\n }\n}\nfunction writePrefs(patch: GamePrefs): void {\n try {\n localStorage.setItem(PREFS_KEY, JSON.stringify({ ...readPrefs(), ...patch }));\n } catch {\n /* storage disabled / full — preferences just won't persist this session */\n }\n}\n\n/** Drop server bet-ladder levels below the minimum bet — the explicit `config.minBet`, else\n * the currency-derived floor (5 minimal units) — so the smallest win can't round below one\n * minimal unit. Never returns empty (keeps the largest level if every level is below). */\nfunction applyMinBet(levels: number[], minBet: number | undefined, decimals: number): number[] {\n const minUnit = 10 ** -decimals;\n const floor = minBet ?? minUnit / MIN_PAYOUT_COEF;\n const filtered = levels.filter((b) => b >= floor - minUnit / 1000);\n return filtered.length ? filtered : [Math.max(...levels)];\n}\n\n/** Snap a wanted bet up to the first ladder level that is >= it (or the closest). */\nfunction snapToLadder(levels: number[], want: number): number {\n if (levels.includes(want)) return want;\n return levels.find((b) => b >= want) ?? levels[levels.length - 1] ?? want;\n}\n\n/** Server/replay-sourced values that drive the HUD spec — never taken from the game config. */\ninterface HudSpecInput {\n currency: string;\n betLevels: number[]; // major units — the authoritative ladder\n defaultBet: number; // major units\n rtp: number; // display %\n confirmBuyAboveCost: number; // jurisdiction policy\n}\n\n/** A read-only snapshot of the running game — for the dev harness, tests and debugging. */\nexport interface GameSnapshot {\n phase: string;\n balance: number;\n bet: number;\n lastWin: number;\n currency: string;\n spinning: boolean;\n}\n\nexport interface StakeGame {\n start(): Promise<void>;\n dispose(): void;\n /** Current phase + store values. The declarative harness asserts on this (no screenshots). */\n inspect(): GameSnapshot;\n /** Trigger a spin the same way the HUD spin button does — only from Idle. For harness/tests. */\n requestSpin(): boolean;\n}\n\nexport function createStakeGame<T = unknown, V = unknown, E = unknown>(opts: CreateStakeGameOptions<T, V, E>): StakeGame {\n const runtime = readRuntime({ overrides: opts.runtime });\n const network = opts.network ?? createNetwork(runtime);\n const stores = new RootStore();\n const ticker = new RealTicker();\n const turbo = new TurboClock(opts.config.turboSpeeds); // core-owned turbo speed + slam-stop\n // A ready AudioPort instance is used as-is; an AudioSpec is resolved lazily in start()\n // (the core creates the zvuk mixer from its own async chunk) — see resolveAudio().\n let audio: AudioPort | null = opts.audio && !isAudioSpec(opts.audio) ? opts.audio : null;\n const app = new Application();\n const disposers: Array<() => void> = [];\n let hud: BootedHud | null = null;\n let fsm: FSM<T, V, E> | null = null;\n let loader: GameLoader | null = null;\n\n // The battery wires the library's DESIGNED default icon set (the white Figma coins +\n // rotating-arrows spin skin) so every game gets the intended HUD out of the box — not\n // the lib's no-art placeholder geometry. `loadBuiltinArt` pulls the whole /art bundle\n // once; a game overrides any icon or the spin skin via `hudOptions.icons`/`spinSkin`.\n let builtinArt: ReturnType<typeof loadBuiltinArt> | null = null;\n const hudOpts = async (): Promise<Record<string, unknown>> => {\n builtinArt ??= loadBuiltinArt();\n const art = await builtinArt;\n const user = (opts.hudOptions ?? {}) as { icons?: Record<string, unknown>; spinSkin?: unknown };\n return {\n spinSkin: art.spinSkin,\n ...user,\n icons: { ...(art.icons as Record<string, unknown>), ...(user.icons ?? {}) },\n };\n };\n\n // The HUD spec is driven by SERVER-authoritative values (the ladder + confirm policy come\n // from `authenticate`/jurisdiction, not the game). `buildSpec` just formats them.\n const buildSpec = (s: HudSpecInput): 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 // Server ladder, trimmed by the client min-bet floor (explicit `config.minBet`, else the\n // currency-derived 5-minimal-units) so the smallest possible win can't round to sub-unit.\n const rawLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);\n const betLevels = applyMinBet(rawLevels, opts.config.minBet, currencyFor(currency).decimals ?? 2);\n const active = auth.round;\n const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;\n // Wanted default (restored from the active bet on resume), snapped up to the first legal\n // (floored) level so the ladder + store agree.\n const wantedDefault = active?.active\n ? active.amount / API_AMOUNT_MULTIPLIER / activeCost // resume: restore bet from the active amount\n : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER;\n const defaultBet = snapToLadder(betLevels, wantedDefault);\n const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;\n\n hud = mountHud(app, buildSpec({ currency, betLevels, defaultBet, rtp, confirmBuyAboveCost }), (await hudOpts()) as never);\n hud.setCurrency(currencyFor(currency));\n hud.applyJurisdiction(juris);\n if (runtime.social || 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); writePrefs({ turbo: e.index }); } }));\n disposers.push(hud.on('skipRequested', () => turbo.skip()));\n // Buy-feature: the bonus button opens the lib's feature-LIST modal (a card per buyable\n // mode). Two kinds of card, both routed through the jurisdiction confirm gate (no one-click\n // above the threshold):\n // • `buy` → one-shot. On confirm, spin that mode ONCE (SpinPhase charges its full cost).\n // • `boost` → a persistent ante. Activating sets it as the ACTIVE mode, so every following\n // base spin plays it at its full `cost×` (via `nextMode()`); toggling off restores base.\n // The modal owns opening/closing/confirm + single-activation; the core supplies the cards +\n // the onBuy/onActivate hooks and reflects the boosted stake in the bet readout.\n const features = buyFeaturesOf(opts.config);\n if (features.length) {\n // The bet readout shows the EFFECTIVE stake (base × the active ante's cost, emphasised)\n // while a boost is on, and the plain base bet otherwise — re-applied on bet/mode change.\n const applyBetDisplay = (): void => {\n const boost = stores.ui.activeMode;\n const cost = boost ? modeCostOf(opts.config, boost) : 1;\n hud!.ui.bet.set(stores.balance.bet * cost);\n (hud!.ui.bet as unknown as { setEmphasis?: (on: boolean) => void }).setEmphasis?.(cost !== 1);\n };\n disposers.push(\n mountBuyFeatureModal(app, hud, features, {\n activation: 'single', // one ante at a time (Stake)\n // Read the BASE bet straight from the stepper CONTROL (updated synchronously by\n // inc()/dec()) — NOT `stores.balance.bet`, which the core updates a tick later off\n // `valueChanged`, so the modal would lag one step (first +/- press \"doesn't\n // register\", +then- re-fires +). The readout still shows the boosted stake.\n getBet: () => hud!.ui.betStepper.value,\n onBuy: (id) => {\n if (machine.current !== 'idle') return;\n // Buying plays at the BASE bet — clear any active ante so its multiplied\n // (emphasised, yellow) bet readout reverts to the plain base bet first.\n if (stores.ui.activeMode) {\n stores.ui.setActiveMode(null);\n applyBetDisplay();\n }\n stores.ui.setOneShotMode(id);\n beginSpin();\n },\n onActivate: (ids) => {\n stores.ui.setActiveMode(ids[0] ?? null); // persistent ante; nextMode() prefers it\n applyBetDisplay();\n },\n }),\n );\n disposers.push(reaction(() => [stores.balance.bet, stores.ui.activeMode] as const, applyBetDisplay));\n }\n // Keep spinning while autoplay/hold is active: after each settled round (spinning\n // true → false), once idle, pause the (turbo-scaled) gap and spin again — stopping on\n // a blocking notice/error or when the next base stake is unaffordable.\n const autoplayGapMs = opts.config.autoplayGapMs ?? 250;\n disposers.push(\n reaction(\n () => stores.ui.spinning,\n (spinning, prev) => {\n if (prev !== true || spinning !== false) return;\n void turbo.delay(autoplayGapMs).then(() => {\n if (machine.current !== 'idle') return;\n if (!hud!.ui.autoplay.isActive && !holdActive) return;\n if (hud!.ui.noticeBlocks.get().length > 0 || stores.balance.balance < stores.balance.bet) {\n hud!.ui.autoplay.stop();\n holdActive = false;\n return;\n }\n beginSpin();\n });\n },\n ),\n );\n\n // Auto-wire audio ↔ HUD (Music/Effects sliders + mute, persisted) + 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 // ── Persist player prefs (sound mute + turbo mode) across sessions ──────────\n // Restore BEFORE the game is interactive so the controls boot in the saved position\n // (setMuted also propagates to the audio mixer via bindMixerToHud's muted subscription\n // above). turboChanged below persists the turbo mode; anon/blocked-storage is safe.\n const prefs = readPrefs();\n if (typeof prefs.muted === 'boolean') hud.setMuted(prefs.muted);\n if (typeof prefs.turbo === 'number' && prefs.turbo > 0) {\n hud.ui.turbo.setIndex(prefs.turbo);\n turbo.setLevel(prefs.turbo);\n }\n disposers.push(hud.ui.muted.subscribe((m) => writePrefs({ muted: m })));\n\n // ── ACTIVE-ROUND RESUME: settle it, land idle, then a \"round in progress\" modal ──\n if (active?.active) {\n // The player refreshed while a round was still open. Settle it now (balance becomes\n // authoritative; the bet was restored into the ladder above), land on a clean idle\n // board, then show a NON-dismissible modal — on Continue we replay the recovered\n // round through the game's Present phase so the player watches how it resolved before\n // regaining control. (Not awaited — start() proceeds to fill/pop the loader; the modal\n // shows over the game once the loader fades.)\n const end = await network.endRound();\n const raw = active as Round<E>;\n const info = roundInfo(raw, defaultBet, activeCost);\n const settledBal = end.balance.amount / API_AMOUNT_MULTIPLIER;\n stores.balance.setBalance(settledBal);\n const resumeRound = { ...info, data: opts.interpretBook(raw, info), active: false, balance: settledBal, raw };\n await machine.transition('idle');\n hud.showFatal('You have an unfinished round. Continue to see how it ends.', {\n title: 'Round in progress',\n tone: 'info',\n actions: [\n {\n label: 'Continue',\n variant: 'primary',\n onSelect: () => {\n hud!.hideNotice();\n hud!.lockInput(); // no spins/buys while the recovered round replays\n ctx.round = resumeRound;\n void machine.transition('present').finally(() => hud!.unlockInput());\n },\n },\n ],\n });\n } else {\n stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);\n await machine.transition('idle');\n }\n // The game is booted + interactive → fill + pop the loader away (unless the game asked\n // to drive it manually, e.g. to wait for its scene art via `ctx.loader.done()`).\n if (!manualLoader) void loader?.done();\n }\n\n function makeCtx(fsm: FSM<T, V, E>, view: V): PhaseContext<T, V, E> {\n const ctx: PhaseContext<T, V, E> = {\n config: opts.config,\n stores,\n network,\n hud: hud as BootedHud,\n ticker,\n view,\n audio,\n turbo,\n loader,\n interpretBook: opts.interpretBook,\n fsm,\n round: null,\n modeCost: (mode) => modeCostOf(opts.config, mode),\n };\n fsm.bind(ctx);\n return ctx;\n }\n\n function dispose(): void {\n for (const d of disposers.splice(0)) d();\n hud?.dispose();\n app.destroy(true);\n }\n\n function inspect(): GameSnapshot {\n return {\n phase: fsm?.current ?? 'boot',\n balance: stores.balance.balance,\n bet: stores.balance.bet,\n lastWin: stores.balance.lastWin,\n currency: stores.session.currency,\n spinning: stores.ui.spinning,\n };\n }\n\n function requestSpin(): boolean {\n if (!fsm || fsm.current !== 'idle') return false;\n void fsm.transition('spin');\n return true;\n }\n\n return { start, dispose, inspect, requestSpin };\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** An AudioSpec (declarative sounds) vs a ready AudioPort/GameAudio instance. */\nfunction isAudioSpec(a: unknown): a is AudioSpec {\n return !!a && typeof a === 'object' && Array.isArray((a as AudioSpec).sounds);\n}\n\nconst capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);\n\n/** The buyable/activatable modes (`modes.<key>.buy` / `.boost`) as buy-feature cards — the\n * list the bonus button opens. A plain number mode or one without `buy`/`boost` is not a card.\n * `cost` in config is the FULL play multiplier (× bet); a boost card shows the SURCHARGE it\n * adds over a base spin (`cost − 1`, e.g. a 2× ante → `+1× bet`), so the card + confirm read\n * right while the mode is still charged its full `cost` when it spins. */\nfunction buyFeaturesOf(config: GameConfig): FeatureSpec[] {\n const out: FeatureSpec[] = [];\n for (const [key, m] of Object.entries(config.modes ?? {})) {\n if (!m || typeof m !== 'object' || !(m.buy || m.boost)) continue;\n const variant = m.boost ? 'boost' : 'buy';\n out.push({ id: key, name: m.name ?? capitalize(key), variant, cost: variant === 'boost' ? m.cost - 1 : m.cost, image: m.image });\n }\n return out;\n}\n"],"mappings":"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"}
|
package/dist/rules.d.ts
CHANGED
|
@@ -9,9 +9,11 @@ declare const SOCIAL_REPLACEMENTS: ReadonlyArray<readonly [string, string]>;
|
|
|
9
9
|
*/
|
|
10
10
|
declare function toSocial(text: string): string;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Find restricted gambling wording in an English string — including DERIVED forms (`credited`,
|
|
13
|
+
* `payouts`, `paying`, `betting`) via word-boundary-anchored stems, but WITHOUT false-flagging
|
|
14
|
+
* innocent words that merely contain a stem (`between`, `better`, `cashew`, `display`). Returns
|
|
15
|
+
* the offending matches (as they appear). Use it at launch/build to fail on any social copy that
|
|
16
|
+
* would still surface gambling wording — including a suffixed form the auto-rewrite missed.
|
|
15
17
|
*/
|
|
16
18
|
declare function findRestricted(text: string): string[];
|
|
17
19
|
//#endregion
|
package/dist/rules.js
CHANGED
|
@@ -47,7 +47,14 @@ const SOCIAL_REPLACEMENTS = [
|
|
|
47
47
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
48
48
|
const matchCase = (matched, repl) => matched[0] && matched[0] === matched[0].toUpperCase() && matched[0] !== matched[0].toLowerCase() ? repl.charAt(0).toUpperCase() + repl.slice(1) : repl;
|
|
49
49
|
const REPLACERS = SOCIAL_REPLACEMENTS.map(([term, repl]) => [new RegExp(`\\b${escapeRe(term)}\\b`, "gi"), repl]);
|
|
50
|
-
const
|
|
50
|
+
const CHECK_MATCHERS = SOCIAL_REPLACEMENTS.map(([term]) => term).map((term) => {
|
|
51
|
+
if (/\s/.test(term)) return new RegExp(`\\b${escapeRe(term)}\\b`, "i");
|
|
52
|
+
if (term.endsWith("e")) {
|
|
53
|
+
const root = escapeRe(term.slice(0, -1));
|
|
54
|
+
return new RegExp(`\\b${root}(?:e|es|ed|ing|er|ers|ement|ements)?\\b`, "i");
|
|
55
|
+
}
|
|
56
|
+
return new RegExp(`\\b${escapeRe(term)}(?:s|es|ed|d|ing|er|ers|or|ors|able|ment|ments|al|als|n)?\\b`, "i");
|
|
57
|
+
});
|
|
51
58
|
/**
|
|
52
59
|
* Rewrite an English string into its social/sweepstakes wording — restricted phrases swapped
|
|
53
60
|
* for their alternatives (word-boundary safe, longest-phrase-first, case-preserving). Idempotent
|
|
@@ -59,14 +66,18 @@ function toSocial(text) {
|
|
|
59
66
|
return out;
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
69
|
+
* Find restricted gambling wording in an English string — including DERIVED forms (`credited`,
|
|
70
|
+
* `payouts`, `paying`, `betting`) via word-boundary-anchored stems, but WITHOUT false-flagging
|
|
71
|
+
* innocent words that merely contain a stem (`between`, `better`, `cashew`, `display`). Returns
|
|
72
|
+
* the offending matches (as they appear). Use it at launch/build to fail on any social copy that
|
|
73
|
+
* would still surface gambling wording — including a suffixed form the auto-rewrite missed.
|
|
65
74
|
*/
|
|
66
75
|
function findRestricted(text) {
|
|
67
|
-
const lower = text.toLowerCase();
|
|
68
76
|
const hits = [];
|
|
69
|
-
for (const
|
|
77
|
+
for (const re of CHECK_MATCHERS) {
|
|
78
|
+
const m = text.match(re);
|
|
79
|
+
if (m) hits.push(m[0]);
|
|
80
|
+
}
|
|
70
81
|
return hits;
|
|
71
82
|
}
|
|
72
83
|
//#endregion
|
package/dist/rules.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","names":[],"sources":["../src/rules/social.ts","../src/rules/index.ts"],"sourcesContent":["// The canonical Stake SOCIAL/sweepstakes wording dictionary. In social mode, restricted\n// gambling phrases must be swapped for the alternatives below. `toSocial` auto-rewrites an\n// English string (word-safe, longest-phrase-first, case-preserving) so a game rarely\n// hand-authors social copy; `findRestricted` AGGRESSIVELY scans for restricted terms — even\n// as PART of a word (paytable, betting…) — for the launch/build compliance gate.\n\n/** Restricted → replacement, ORDERED longest-phrase-first (so multi-word phrases win). */\nexport const SOCIAL_REPLACEMENTS: ReadonlyArray<readonly [string, string]> = [\n [\"be awarded to player's accounts\", \"appear in player's accounts\"],\n ['place your bets', 'come and play / join in the game'],\n ['at the cost of', 'for'],\n ['bonus buy', 'bonus / feature'],\n ['buy bonus', 'get bonus'],\n ['win feature', 'play feature'],\n ['total bet', 'total play'],\n ['pays out', 'won'],\n ['paid out', 'win'],\n ['pay out', 'win'],\n ['cost of', 'can be played for'],\n ['paytable', 'prize table'],\n ['payouts', 'wins'],\n ['payout', 'win'],\n ['betting', 'playing'],\n ['gambling', 'playing'],\n ['wagering', 'playing'],\n ['paying', 'awarding'],\n ['buying', 'playing'],\n ['buys', 'plays'],\n ['deposit', 'get coins'],\n ['withdraw', 'redeem'],\n ['purchase', 'play'],\n ['currency', 'token'],\n ['credits', 'coins'],\n ['credit', 'coins'],\n ['winnings', 'prizes'],\n ['payer', 'winner'],\n ['bought', 'instantly triggered'],\n ['gamble', 'play'],\n ['wager', 'play'],\n ['rebet', 'respin'],\n ['stakes', 'play amounts'],\n ['stake', 'play amount'],\n ['money', 'coins'],\n ['cash', 'coins'],\n ['bets', 'plays'],\n ['bet', 'play'],\n ['pays', 'wins'],\n ['paid', 'won'],\n ['pay', 'win'],\n ['buy', 'play'],\n];\n\nconst escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst matchCase = (matched: string, repl: string): string =>\n matched[0] && matched[0] === matched[0].toUpperCase() && matched[0] !== matched[0].toLowerCase()\n ? repl.charAt(0).toUpperCase() + repl.slice(1)\n : repl;\n\n// Word-boundary regexes (so \"bet\"→\"play\" never breaks \"better\"/\"internet\"). Built once.\nconst REPLACERS = SOCIAL_REPLACEMENTS.map(([term, repl]) => [new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'gi'), repl] as const);\n// Aggressive substring matchers for the CHECK (catch \"pay\" inside \"paytable\", etc.).\nconst RESTRICTED = SOCIAL_REPLACEMENTS.map(([term]) => term);\n\n/**\n * Rewrite an English string into its social/sweepstakes wording — restricted phrases swapped\n * for their alternatives (word-boundary safe, longest-phrase-first, case-preserving). Idempotent\n * enough for authoring: run it over your menu/feature copy to auto-generate social variants.\n */\nexport function toSocial(text: string): string {\n let out = text;\n for (const [re, repl] of REPLACERS) out = out.replace(re, (m) => matchCase(m, repl));\n return out;\n}\n\n/**\n * AGGRESSIVELY find restricted phrases in an English string — matches even as part of a word\n * (so `paytable`, `betting`, `payout` are all flagged). Returns the offending terms. Use it at\n * launch/build to fail on any social-copy that would still surface gambling wording.\n */\nexport function findRestricted(text: string): string[] {\n const lower = text.toLowerCase();\n const hits: string[] = [];\n for (const term of RESTRICTED) if (lower.includes(term)) hits.push(term);\n return hits;\n}\n","// @stakeplate/core/rules — `buildRules` assembles a Stake-COMPLIANT rules section (an\n// @open-slot-ui `MenuSpec`) so every game ships the mandatory copy without re-authoring it:\n// a **User Interaction Guide** (a description of every standard control), the **Game info**\n// grid (RTP / volatility / max win) and the **EXACT Stake Engine disclaimer** — authored ONCE\n// here. The game supplies only its own about/paytable/features. The library's white HTML menu\n// (open-slot-ui) renders it; the built-in Settings (Sound · Language · Quick spin) are the\n// menu's own — a game NEVER invents settings.\n//\n// Social wording: `buildRules` returns `socialEn` — { normal → social } for the strings it\n// authored that carry restricted terms (bet/pay…). Merge into `config.socialMessages.en`. The\n// disclaimer is legal-exact — mandated verbatim on every platform, so it is NEVER swapped.\n\nimport type { BlockSpec, MenuSpec } from '@open-slot-ui/core';\nimport { toSocial } from './social';\n\nexport * from './social';\n\n/** RTP / volatility / max-win etc. — shown as a stat grid. Values are display strings. */\nexport interface RulesStats {\n rtp?: string; // \"96.00%\"\n volatility?: string; // \"High\"\n maxWin?: string; // \"5,000×\"\n lines?: string; // \"5\"\n /** Any extra label → value rows (e.g. per-mode RTP/Max win). */\n extra?: Array<{ label: string; value: string }>;\n}\n\nexport interface BuildRulesOptions {\n /** How Settings exposes sound: a `'toggle'` only · `'master'` slider · `'sliders'`\n * (Music + Effects). Default `'sliders'`. */\n sound?: 'toggle' | 'master' | 'sliders';\n /** An \"About the game\" intro paragraph. */\n about?: string;\n /** \"How to play\" steps. */\n howToPlay?: string[];\n /** Feature cards (Wild / Scatter / Bonus …). */\n features?: Array<{ icon?: string; title: string; text?: string }>;\n /** The game's paytable blocks (usually one `{ kind: 'paytable' }`). */\n paytable?: BlockSpec[];\n /** RTP / volatility / max-win — the Game info grid. Must match the certified report. */\n stats?: RulesStats;\n /** Extra rules blocks appended before the disclaimer. */\n extra?: BlockSpec[];\n /** Override the built-in per-control guide lines (each: `**Name** — what it does.`). */\n controlGuide?: string[];\n /** Include the Stake disclaimer (default `true`). */\n disclaimer?: boolean;\n}\n\nexport interface BuiltRules {\n menu: MenuSpec;\n /** { normal → social } for the strings buildRules authored — merge into `config.socialMessages.en`. */\n socialEn: Record<string, string>;\n}\n\n// ── Canonical, core-authored copy ───────────────────────────────────────────────────\n\n/** Every interactive control described (Stake: the Info/Help must explain each button). */\nexport const DEFAULT_CONTROL_GUIDE: string[] = [\n '**Spin** — plays one round at your current bet; press and hold to spin in turbo.',\n '**+ / −** — raise or lower your bet before spinning.',\n '**Autoplay** — plays a set number of rounds automatically; tap again to stop any time.',\n '**Turbo / Quick spin** — speeds up spins by shortening the animation; the result is identical.',\n '**Buy Feature** — instantly buys entry to the bonus for the shown price (needs confirmation).',\n '**Menu** — opens settings, the paytable and these rules.',\n '**Sound** — mutes or unmutes all game audio.',\n];\n\n/** The EXACT Stake Engine disclaimer — never reworded, never given a social override. */\nexport const STAKE_DISCLAIMER =\n 'Malfunction voids all wins and plays. A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds. The expected return is calculated over many plays. The game display is not representative of any physical device and is for illustrative purposes only. Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser. TM and © 2026 Stake Engine.';\n\n/** Collect every translatable English string in a block tree (for social auto-derivation). */\nfunction collectStrings(blocks: BlockSpec[], out: string[] = []): string[] {\n for (const b of blocks) {\n switch (b.kind) {\n case 'text':\n case 'heading':\n case 'subheading':\n case 'legal':\n out.push(b.text);\n break;\n case 'callout':\n if (b.title) out.push(b.title);\n out.push(b.text);\n break;\n case 'steps':\n out.push(...b.items);\n break;\n case 'stat-grid':\n for (const it of b.items) out.push(it.label, it.value);\n break;\n case 'cards':\n for (const it of b.items) { out.push(it.title); if (it.text) out.push(it.text); }\n break;\n case 'table':\n if (b.columns) out.push(...b.columns);\n for (const r of b.rows) out.push(...r);\n break;\n case 'media':\n if (b.title) out.push(b.title);\n out.push(b.text);\n break;\n case 'paytable':\n for (const r of b.rows) if (r.symbol) out.push(r.symbol);\n break;\n case 'group':\n if (b.title) out.push(b.title);\n collectStrings(b.children, out);\n break;\n default:\n break;\n }\n }\n return out;\n}\n\n// ── The builder ────────────────────────────────────────────────────────────────────\n\nexport function buildRules(opts: BuildRulesOptions = {}): BuiltRules {\n const rules: BlockSpec[] = [];\n\n if (opts.about) {\n rules.push({ kind: 'heading', id: 'r-about-h', text: 'About the game' });\n rules.push({ kind: 'text', id: 'r-about', text: opts.about });\n }\n\n if (opts.howToPlay?.length) {\n rules.push({ kind: 'heading', id: 'r-play-h', text: 'How to play' });\n rules.push({ kind: 'steps', id: 'r-play', ordered: true, items: opts.howToPlay });\n }\n\n if (opts.features?.length) {\n rules.push({ kind: 'heading', id: 'r-feat-h', text: 'Features' });\n rules.push({ kind: 'cards', id: 'r-feat', items: opts.features });\n }\n\n // ── User Interaction Guide (Stake compliance: a description for EVERY control) ──\n rules.push({ kind: 'heading', id: 'r-ctrl-h', text: 'Controls' });\n rules.push({ kind: 'text', id: 'r-ctrl-0', text: 'Every interactive button and what it does:' });\n rules.push({ kind: 'steps', id: 'r-ctrl', ordered: false, items: opts.controlGuide ?? DEFAULT_CONTROL_GUIDE });\n\n // ── Game info (RTP / volatility / max win — must match the certified report) ──\n const stats = opts.stats;\n if (stats) {\n const items: Array<{ label: string; value: string }> = [];\n if (stats.rtp) items.push({ label: 'RTP', value: stats.rtp });\n if (stats.volatility) items.push({ label: 'Volatility', value: stats.volatility });\n if (stats.maxWin) items.push({ label: 'Max win', value: stats.maxWin });\n if (stats.lines) items.push({ label: 'Lines', value: stats.lines });\n if (stats.extra) items.push(...stats.extra);\n if (items.length) {\n rules.push({ kind: 'heading', id: 'r-info-h', text: 'Game info' });\n rules.push({ kind: 'stat-grid', id: 'r-info', items });\n }\n }\n\n if (opts.extra?.length) rules.push(...opts.extra);\n\n // ── The exact Stake disclaimer ──\n if (opts.disclaimer !== false) {\n rules.push({ kind: 'heading', id: 'r-general-h', text: 'General' });\n rules.push({ kind: 'text', id: 'r-general', text: STAKE_DISCLAIMER });\n }\n\n const menu: MenuSpec = {\n ...(opts.sound ? { sound: opts.sound } : {}),\n ...(opts.paytable ? { paytable: opts.paytable } : {}),\n rules,\n };\n\n // Auto-derive the social copy: scan EVERY English string in the menu and swap restricted\n // wording (`toSocial`). The disclaimer is legal-exact (mandated verbatim) → never rewritten.\n const socialEn: Record<string, string> = {};\n for (const s of collectStrings([...rules, ...(opts.paytable ?? [])])) {\n if (s === STAKE_DISCLAIMER) continue;\n const social = toSocial(s);\n if (social !== s) socialEn[s] = social;\n }\n return { menu, socialEn };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rules.js","names":[],"sources":["../src/rules/social.ts","../src/rules/index.ts"],"sourcesContent":["// The canonical Stake SOCIAL/sweepstakes wording dictionary. In social mode, restricted\n// gambling phrases must be swapped for the alternatives below. `toSocial` auto-rewrites an\n// English string (word-safe, longest-phrase-first, case-preserving) so a game rarely\n// hand-authors social copy; `findRestricted` AGGRESSIVELY scans for restricted terms — even\n// as PART of a word (paytable, betting…) — for the launch/build compliance gate.\n\n/** Restricted → replacement, ORDERED longest-phrase-first (so multi-word phrases win). */\nexport const SOCIAL_REPLACEMENTS: ReadonlyArray<readonly [string, string]> = [\n [\"be awarded to player's accounts\", \"appear in player's accounts\"],\n ['place your bets', 'come and play / join in the game'],\n ['at the cost of', 'for'],\n ['bonus buy', 'bonus / feature'],\n ['buy bonus', 'get bonus'],\n ['win feature', 'play feature'],\n ['total bet', 'total play'],\n ['pays out', 'won'],\n ['paid out', 'win'],\n ['pay out', 'win'],\n ['cost of', 'can be played for'],\n ['paytable', 'prize table'],\n ['payouts', 'wins'],\n ['payout', 'win'],\n ['betting', 'playing'],\n ['gambling', 'playing'],\n ['wagering', 'playing'],\n ['paying', 'awarding'],\n ['buying', 'playing'],\n ['buys', 'plays'],\n ['deposit', 'get coins'],\n ['withdraw', 'redeem'],\n ['purchase', 'play'],\n ['currency', 'token'],\n ['credits', 'coins'],\n ['credit', 'coins'],\n ['winnings', 'prizes'],\n ['payer', 'winner'],\n ['bought', 'instantly triggered'],\n ['gamble', 'play'],\n ['wager', 'play'],\n ['rebet', 'respin'],\n ['stakes', 'play amounts'],\n ['stake', 'play amount'],\n ['money', 'coins'],\n ['cash', 'coins'],\n ['bets', 'plays'],\n ['bet', 'play'],\n ['pays', 'wins'],\n ['paid', 'won'],\n ['pay', 'win'],\n ['buy', 'play'],\n];\n\nconst escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst matchCase = (matched: string, repl: string): string =>\n matched[0] && matched[0] === matched[0].toUpperCase() && matched[0] !== matched[0].toLowerCase()\n ? repl.charAt(0).toUpperCase() + repl.slice(1)\n : repl;\n\n// Word-boundary regexes (so \"bet\"→\"play\" never breaks \"better\"/\"internet\"). Built once.\nconst REPLACERS = SOCIAL_REPLACEMENTS.map(([term, repl]) => [new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'gi'), repl] as const);\nconst RESTRICTED = SOCIAL_REPLACEMENTS.map(([term]) => term);\n\n// CHECK matchers: word-boundary-anchored stems that ALSO catch common inflections — so a\n// DERIVED form leaks through nothing (credit → credit/credits/credited/crediting; pay →\n// pay/pays/paying/payer/payment). The trailing `\\b` after the optional suffix keeps innocent\n// words safe: \"between\"/\"better\" (contain \"bet\"), \"cashew\" (\"cash\"), \"stakeholder\" (\"stake\")\n// and \"display\" (\"play\") never false-match — where a raw substring scan would flag them all.\n// Multi-word phrases match verbatim (no suffix). A stem's own inflected entries (e.g. the\n// explicit \"paying\"/\"bets\") still match too; this just widens coverage to unlisted forms.\nconst CHECK_MATCHERS = RESTRICTED.map((term) => {\n // Multi-word phrases match verbatim (word-boundary anchored).\n if (/\\s/.test(term)) return new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'i');\n // A stem ending in `e` (purchase, gamble, stake) drops it before -ed/-ing: match the\n // e-less root plus the e-inflections (purchase/purchased/purchasing/gambler).\n if (term.endsWith('e')) {\n const root = escapeRe(term.slice(0, -1));\n return new RegExp(`\\\\b${root}(?:e|es|ed|ing|er|ers|ement|ements)?\\\\b`, 'i');\n }\n // Otherwise the stem plus common inflections (credit→credited, withdraw→withdrawn).\n return new RegExp(`\\\\b${escapeRe(term)}(?:s|es|ed|d|ing|er|ers|or|ors|able|ment|ments|al|als|n)?\\\\b`, 'i');\n});\n\n/**\n * Rewrite an English string into its social/sweepstakes wording — restricted phrases swapped\n * for their alternatives (word-boundary safe, longest-phrase-first, case-preserving). Idempotent\n * enough for authoring: run it over your menu/feature copy to auto-generate social variants.\n */\nexport function toSocial(text: string): string {\n let out = text;\n for (const [re, repl] of REPLACERS) out = out.replace(re, (m) => matchCase(m, repl));\n return out;\n}\n\n/**\n * Find restricted gambling wording in an English string — including DERIVED forms (`credited`,\n * `payouts`, `paying`, `betting`) via word-boundary-anchored stems, but WITHOUT false-flagging\n * innocent words that merely contain a stem (`between`, `better`, `cashew`, `display`). Returns\n * the offending matches (as they appear). Use it at launch/build to fail on any social copy that\n * would still surface gambling wording — including a suffixed form the auto-rewrite missed.\n */\nexport function findRestricted(text: string): string[] {\n const hits: string[] = [];\n for (const re of CHECK_MATCHERS) {\n const m = text.match(re);\n if (m) hits.push(m[0]);\n }\n return hits;\n}\n","// @stakeplate/core/rules — `buildRules` assembles a Stake-COMPLIANT rules section (an\n// @open-slot-ui `MenuSpec`) so every game ships the mandatory copy without re-authoring it:\n// a **User Interaction Guide** (a description of every standard control), the **Game info**\n// grid (RTP / volatility / max win) and the **EXACT Stake Engine disclaimer** — authored ONCE\n// here. The game supplies only its own about/paytable/features. The library's white HTML menu\n// (open-slot-ui) renders it; the built-in Settings (Sound · Language · Quick spin) are the\n// menu's own — a game NEVER invents settings.\n//\n// Social wording: `buildRules` returns `socialEn` — { normal → social } for the strings it\n// authored that carry restricted terms (bet/pay…). Merge into `config.socialMessages.en`. The\n// disclaimer is legal-exact — mandated verbatim on every platform, so it is NEVER swapped.\n\nimport type { BlockSpec, MenuSpec } from '@open-slot-ui/core';\nimport { toSocial } from './social';\n\nexport * from './social';\n\n/** RTP / volatility / max-win etc. — shown as a stat grid. Values are display strings. */\nexport interface RulesStats {\n rtp?: string; // \"96.00%\"\n volatility?: string; // \"High\"\n maxWin?: string; // \"5,000×\"\n lines?: string; // \"5\"\n /** Any extra label → value rows (e.g. per-mode RTP/Max win). */\n extra?: Array<{ label: string; value: string }>;\n}\n\nexport interface BuildRulesOptions {\n /** How Settings exposes sound: a `'toggle'` only · `'master'` slider · `'sliders'`\n * (Music + Effects). Default `'sliders'`. */\n sound?: 'toggle' | 'master' | 'sliders';\n /** An \"About the game\" intro paragraph. */\n about?: string;\n /** \"How to play\" steps. */\n howToPlay?: string[];\n /** Feature cards (Wild / Scatter / Bonus …). */\n features?: Array<{ icon?: string; title: string; text?: string }>;\n /** The game's paytable blocks (usually one `{ kind: 'paytable' }`). */\n paytable?: BlockSpec[];\n /** RTP / volatility / max-win — the Game info grid. Must match the certified report. */\n stats?: RulesStats;\n /** Extra rules blocks appended before the disclaimer. */\n extra?: BlockSpec[];\n /** Override the built-in per-control guide lines (each: `**Name** — what it does.`). */\n controlGuide?: string[];\n /** Include the Stake disclaimer (default `true`). */\n disclaimer?: boolean;\n}\n\nexport interface BuiltRules {\n menu: MenuSpec;\n /** { normal → social } for the strings buildRules authored — merge into `config.socialMessages.en`. */\n socialEn: Record<string, string>;\n}\n\n// ── Canonical, core-authored copy ───────────────────────────────────────────────────\n\n/** Every interactive control described (Stake: the Info/Help must explain each button). */\nexport const DEFAULT_CONTROL_GUIDE: string[] = [\n '**Spin** — plays one round at your current bet; press and hold to spin in turbo.',\n '**+ / −** — raise or lower your bet before spinning.',\n '**Autoplay** — plays a set number of rounds automatically; tap again to stop any time.',\n '**Turbo / Quick spin** — speeds up spins by shortening the animation; the result is identical.',\n '**Buy Feature** — instantly buys entry to the bonus for the shown price (needs confirmation).',\n '**Menu** — opens settings, the paytable and these rules.',\n '**Sound** — mutes or unmutes all game audio.',\n];\n\n/** The EXACT Stake Engine disclaimer — never reworded, never given a social override. */\nexport const STAKE_DISCLAIMER =\n 'Malfunction voids all wins and plays. A consistent internet connection is required. In the event of a disconnection, reload the game to finish any uncompleted rounds. The expected return is calculated over many plays. The game display is not representative of any physical device and is for illustrative purposes only. Winnings are settled according to the amount received from the Remote Game Server and not from events within the web browser. TM and © 2026 Stake Engine.';\n\n/** Collect every translatable English string in a block tree (for social auto-derivation). */\nfunction collectStrings(blocks: BlockSpec[], out: string[] = []): string[] {\n for (const b of blocks) {\n switch (b.kind) {\n case 'text':\n case 'heading':\n case 'subheading':\n case 'legal':\n out.push(b.text);\n break;\n case 'callout':\n if (b.title) out.push(b.title);\n out.push(b.text);\n break;\n case 'steps':\n out.push(...b.items);\n break;\n case 'stat-grid':\n for (const it of b.items) out.push(it.label, it.value);\n break;\n case 'cards':\n for (const it of b.items) { out.push(it.title); if (it.text) out.push(it.text); }\n break;\n case 'table':\n if (b.columns) out.push(...b.columns);\n for (const r of b.rows) out.push(...r);\n break;\n case 'media':\n if (b.title) out.push(b.title);\n out.push(b.text);\n break;\n case 'paytable':\n for (const r of b.rows) if (r.symbol) out.push(r.symbol);\n break;\n case 'group':\n if (b.title) out.push(b.title);\n collectStrings(b.children, out);\n break;\n default:\n break;\n }\n }\n return out;\n}\n\n// ── The builder ────────────────────────────────────────────────────────────────────\n\nexport function buildRules(opts: BuildRulesOptions = {}): BuiltRules {\n const rules: BlockSpec[] = [];\n\n if (opts.about) {\n rules.push({ kind: 'heading', id: 'r-about-h', text: 'About the game' });\n rules.push({ kind: 'text', id: 'r-about', text: opts.about });\n }\n\n if (opts.howToPlay?.length) {\n rules.push({ kind: 'heading', id: 'r-play-h', text: 'How to play' });\n rules.push({ kind: 'steps', id: 'r-play', ordered: true, items: opts.howToPlay });\n }\n\n if (opts.features?.length) {\n rules.push({ kind: 'heading', id: 'r-feat-h', text: 'Features' });\n rules.push({ kind: 'cards', id: 'r-feat', items: opts.features });\n }\n\n // ── User Interaction Guide (Stake compliance: a description for EVERY control) ──\n rules.push({ kind: 'heading', id: 'r-ctrl-h', text: 'Controls' });\n rules.push({ kind: 'text', id: 'r-ctrl-0', text: 'Every interactive button and what it does:' });\n rules.push({ kind: 'steps', id: 'r-ctrl', ordered: false, items: opts.controlGuide ?? DEFAULT_CONTROL_GUIDE });\n\n // ── Game info (RTP / volatility / max win — must match the certified report) ──\n const stats = opts.stats;\n if (stats) {\n const items: Array<{ label: string; value: string }> = [];\n if (stats.rtp) items.push({ label: 'RTP', value: stats.rtp });\n if (stats.volatility) items.push({ label: 'Volatility', value: stats.volatility });\n if (stats.maxWin) items.push({ label: 'Max win', value: stats.maxWin });\n if (stats.lines) items.push({ label: 'Lines', value: stats.lines });\n if (stats.extra) items.push(...stats.extra);\n if (items.length) {\n rules.push({ kind: 'heading', id: 'r-info-h', text: 'Game info' });\n rules.push({ kind: 'stat-grid', id: 'r-info', items });\n }\n }\n\n if (opts.extra?.length) rules.push(...opts.extra);\n\n // ── The exact Stake disclaimer ──\n if (opts.disclaimer !== false) {\n rules.push({ kind: 'heading', id: 'r-general-h', text: 'General' });\n rules.push({ kind: 'text', id: 'r-general', text: STAKE_DISCLAIMER });\n }\n\n const menu: MenuSpec = {\n ...(opts.sound ? { sound: opts.sound } : {}),\n ...(opts.paytable ? { paytable: opts.paytable } : {}),\n rules,\n };\n\n // Auto-derive the social copy: scan EVERY English string in the menu and swap restricted\n // wording (`toSocial`). The disclaimer is legal-exact (mandated verbatim) → never rewritten.\n const socialEn: Record<string, string> = {};\n for (const s of collectStrings([...rules, ...(opts.paytable ?? [])])) {\n if (s === STAKE_DISCLAIMER) continue;\n const social = toSocial(s);\n if (social !== s) socialEn[s] = social;\n }\n return { menu, socialEn };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stakeplate/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|