@stakeplate/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,561 @@
1
+ import { i as roundEvents, n as API_AMOUNT_MULTIPLIER, r as BOOK_AMOUNT_MULTIPLIER, t as MockNetworkManager } from "./MockNetworkManager-cj8WT5v0.js";
2
+ import { StakeNetworkManager, createNetwork, readRuntime, urlParam } from "./rgs.js";
3
+ import { n as RealTicker, t as InstantTicker } from "./ticker-A-XgayZv.js";
4
+ import { BalanceStore, RootStore, SessionStore, UiStore } from "./stores.js";
5
+ import { t as bindMixerToHud } from "./bind-BBRxizWR.js";
6
+ import { reaction } from "mobx";
7
+ import { Application } from "pixi.js";
8
+ import { mountBuyFeatureModal, mountHud, showBootError } from "@open-slot-ui/pixi";
9
+ import { loadBuiltinArt } from "@open-slot-ui/pixi/art";
10
+ import { isSocialCurrency, resolveCurrency } from "@open-slot-ui/core";
11
+ //#region src/engine/turbo.ts
12
+ /** Delay multipliers per turbo level: off (1×) / turbo (0.4×) / super (0.12×). */
13
+ const DEFAULT_TURBO_SPEEDS = [
14
+ 1,
15
+ .4,
16
+ .12
17
+ ];
18
+ var TurboClock = class {
19
+ speeds;
20
+ _level = 0;
21
+ _skipped = false;
22
+ pending = /* @__PURE__ */ new Set();
23
+ constructor(speeds) {
24
+ this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;
25
+ }
26
+ get level() {
27
+ return this._level;
28
+ }
29
+ get speed() {
30
+ return this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;
31
+ }
32
+ get skipped() {
33
+ return this._skipped;
34
+ }
35
+ /** Set the turbo level (clamped to the configured speeds). */
36
+ setLevel(index) {
37
+ const i = Number.isFinite(index) ? Math.trunc(index) : 0;
38
+ this._level = Math.max(0, Math.min(i, this.speeds.length - 1));
39
+ }
40
+ /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */
41
+ skip() {
42
+ this._skipped = true;
43
+ const cbs = [...this.pending];
44
+ this.pending.clear();
45
+ for (const cb of cbs) cb();
46
+ }
47
+ /** Clear the slam-stop flag — the core calls this at the start of each spin. */
48
+ resetSkip() {
49
+ this._skipped = false;
50
+ }
51
+ delay(ms) {
52
+ if (this._skipped || ms <= 0) return Promise.resolve();
53
+ return new Promise((resolve) => {
54
+ let done = false;
55
+ const finish = () => {
56
+ if (done) return;
57
+ done = true;
58
+ clearTimeout(timer);
59
+ this.pending.delete(finish);
60
+ resolve();
61
+ };
62
+ const timer = setTimeout(finish, ms * this.speed);
63
+ this.pending.add(finish);
64
+ });
65
+ }
66
+ };
67
+ //#endregion
68
+ //#region src/engine/round.ts
69
+ /** Build the money facts from a raw wire round + the base bet + the mode cost. */
70
+ function roundInfo(raw, bet, cost) {
71
+ const multiplier = raw.payoutMultiplier / 100;
72
+ const totalWin = multiplier * bet;
73
+ const payout = raw.payout != null ? raw.payout / API_AMOUNT_MULTIPLIER : totalWin;
74
+ return {
75
+ mode: raw.mode,
76
+ bet,
77
+ cost,
78
+ stake: bet * cost,
79
+ multiplier,
80
+ totalWin,
81
+ payout
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/engine/fsm.ts
86
+ var FSM = class {
87
+ ctx = null;
88
+ byName = /* @__PURE__ */ new Map();
89
+ _current = "";
90
+ /** Later phases with the same `name` win — so a game's Present overrides the default. */
91
+ constructor(phases) {
92
+ for (const p of phases) this.byName.set(p.name, p);
93
+ }
94
+ bind(ctx) {
95
+ this.ctx = ctx;
96
+ }
97
+ get current() {
98
+ return this._current;
99
+ }
100
+ has(name) {
101
+ return this.byName.has(name);
102
+ }
103
+ async transition(name) {
104
+ if (!this.ctx) throw new Error("[FSM] not bound to a context");
105
+ const next = this.byName.get(name);
106
+ if (!next) throw new Error(`[FSM] unknown phase: ${name}`);
107
+ this.byName.get(this._current)?.exit?.(this.ctx);
108
+ this._current = name;
109
+ await next.enter(this.ctx);
110
+ }
111
+ };
112
+ //#endregion
113
+ //#region src/engine/phases.ts
114
+ /** Wait for the next spin (the HUD's `spinRequested` drives the transition to Spin). */
115
+ var IdlePhase = class {
116
+ name = "idle";
117
+ enter(ctx) {
118
+ ctx.stores.ui.setSpinning(false);
119
+ }
120
+ };
121
+ /** Pick the mode, take the stake, ask the RGS, parse the round, hand off to Present. */
122
+ var SpinPhase = class {
123
+ name = "spin";
124
+ async enter(ctx) {
125
+ const { stores, network, hud } = ctx;
126
+ const mode = stores.ui.nextMode();
127
+ const cost = ctx.modeCost(mode);
128
+ const bet = stores.balance.bet;
129
+ const stake = bet * cost;
130
+ stores.ui.setSpinning(true);
131
+ stores.balance.debitStake(stake);
132
+ try {
133
+ const play = await network.play({
134
+ bet,
135
+ mode
136
+ });
137
+ const raw = play.round;
138
+ let settledApi = play.balance.amount;
139
+ if (raw.active) settledApi = (await network.endRound()).balance.amount;
140
+ const info = roundInfo(raw, bet, cost);
141
+ const data = ctx.interpretBook(raw, info);
142
+ ctx.round = {
143
+ ...info,
144
+ data,
145
+ active: raw.active ?? false,
146
+ balance: settledApi / API_AMOUNT_MULTIPLIER,
147
+ raw
148
+ };
149
+ } catch (err) {
150
+ stores.balance.refund(stake);
151
+ stores.ui.setSpinning(false);
152
+ const msg = err instanceof Error ? err.message : String(err);
153
+ const code = msg.match(/ERR_[A-Z_]+/)?.[0];
154
+ if (code) hud.showRgsError(code);
155
+ else hud.showError(msg);
156
+ await ctx.fsm.transition("idle");
157
+ return;
158
+ }
159
+ await ctx.fsm.transition("present");
160
+ }
161
+ };
162
+ /** DEFAULT Present — nothing to animate. The game overrides `present` to play its scene. */
163
+ var PresentPhase = class {
164
+ name = "present";
165
+ async enter(ctx) {
166
+ await ctx.fsm.transition("settle");
167
+ }
168
+ };
169
+ /** Apply the authoritative balance + win, settle the round for the HUD, then Idle. */
170
+ var SettlePhase = class {
171
+ name = "settle";
172
+ roundStartedAt = 0;
173
+ async enter(ctx) {
174
+ const r = ctx.round;
175
+ if (r) {
176
+ ctx.stores.balance.settle(r.balance, r.totalWin);
177
+ ctx.hud.reportRound(r.totalWin, r.bet);
178
+ const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;
179
+ const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1e3;
180
+ if (minMs > 0 && elapsed < minMs) await ctx.ticker.delay(minMs - elapsed);
181
+ }
182
+ ctx.stores.ui.setSpinning(false);
183
+ await ctx.fsm.transition("idle");
184
+ }
185
+ /** Called by SpinPhase-adjacent boot wiring to timestamp the round start. */
186
+ markStart(now) {
187
+ this.roundStartedAt = now;
188
+ }
189
+ };
190
+ /** The default phase set (game phases are appended after → same-name overrides win). */
191
+ function defaultPhases() {
192
+ return [
193
+ new IdlePhase(),
194
+ new SpinPhase(),
195
+ new PresentPhase(),
196
+ new SettlePhase()
197
+ ];
198
+ }
199
+ //#endregion
200
+ //#region src/game/config.ts
201
+ /** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */
202
+ function modeCostOf(config, mode) {
203
+ const m = config.modes?.[mode];
204
+ if (typeof m === "number") return m;
205
+ if (m && typeof m === "object") return m.cost;
206
+ return 1;
207
+ }
208
+ //#endregion
209
+ //#region src/game/createStakeGame.ts
210
+ /**
211
+ * Stake policy: buys/activations costing more than this many × base bet require a confirm
212
+ * (no one-click). A buy-feature always costs far more than 2×, so in practice this means
213
+ * "always confirm a buy". The jurisdiction (`auth.config.jurisdiction`) may override it; a
214
+ * game never sets it — it's compliance, not design.
215
+ */
216
+ const STAKE_CONFIRM_ABOVE_COST = 2;
217
+ function createStakeGame(opts) {
218
+ const runtime = readRuntime({ overrides: opts.runtime });
219
+ const network = opts.network ?? createNetwork(runtime);
220
+ const stores = new RootStore();
221
+ const ticker = new RealTicker();
222
+ const turbo = new TurboClock(opts.config.turboSpeeds);
223
+ let audio = opts.audio && !isAudioSpec(opts.audio) ? opts.audio : null;
224
+ const app = new Application();
225
+ const disposers = [];
226
+ let hud = null;
227
+ let fsm = null;
228
+ let builtinArt = null;
229
+ const hudOpts = async () => {
230
+ builtinArt ??= loadBuiltinArt();
231
+ const art = await builtinArt;
232
+ const user = opts.hudOptions ?? {};
233
+ return {
234
+ spinSkin: art.spinSkin,
235
+ ...user,
236
+ icons: {
237
+ ...art.icons,
238
+ ...user.icons ?? {}
239
+ }
240
+ };
241
+ };
242
+ const buildSpec = (s) => ({
243
+ currency: resolveCurrency(s.currency),
244
+ betLadder: {
245
+ levels: s.betLevels,
246
+ index: Math.max(0, s.betLevels.indexOf(s.defaultBet))
247
+ },
248
+ rtp: s.rtp,
249
+ game: {
250
+ name: opts.config.title,
251
+ version: opts.config.version ?? "1.0.0"
252
+ },
253
+ controls: {
254
+ fullscreen: { hidden: true },
255
+ ...buyFeaturesOf(opts.config).length ? { bonus: { hidden: false } } : {}
256
+ },
257
+ buyFeature: { confirmAboveCost: s.confirmBuyAboveCost },
258
+ ...opts.config.rules ? { menu: opts.config.rules } : {},
259
+ locale: {
260
+ locale: runtime.language,
261
+ messages: opts.config.messages ?? { en: {} },
262
+ ...opts.config.socialMessages ? { socialMessages: opts.config.socialMessages } : {}
263
+ },
264
+ ...opts.config.spec ?? {}
265
+ });
266
+ const initApp = async (host) => {
267
+ await app.init({
268
+ resizeTo: window,
269
+ backgroundAlpha: 0,
270
+ antialias: true,
271
+ resolution: Math.min(window.devicePixelRatio || 1, 2),
272
+ autoDensity: true
273
+ });
274
+ host.appendChild(app.canvas);
275
+ };
276
+ const resolveAudio = async () => {
277
+ if (!isAudioSpec(opts.audio)) return;
278
+ const { createGameAudio } = await import("./audio.js");
279
+ const mixer = createGameAudio(opts.audio);
280
+ mixer.load(opts.audio.sounds);
281
+ audio = mixer;
282
+ };
283
+ async function start() {
284
+ const hudHost = opts.hudHost ?? document.body;
285
+ const sceneHost = opts.sceneHost ?? hudHost;
286
+ const machine = fsm = new FSM([...defaultPhases(), ...opts.phases ?? []]);
287
+ await resolveAudio();
288
+ if (runtime.replay.active && network.replay) {
289
+ try {
290
+ await initApp(hudHost);
291
+ const cur = runtime.currency;
292
+ const bet = runtime.replay.amount || 1;
293
+ hud = mountHud(app, buildSpec({
294
+ currency: cur,
295
+ betLevels: [bet],
296
+ defaultBet: bet,
297
+ rtp: opts.config.rtp ?? 96,
298
+ confirmBuyAboveCost: STAKE_CONFIRM_ABOVE_COST
299
+ }), await hudOpts());
300
+ stores.session.set({ currency: cur });
301
+ stores.balance.setBalance(0);
302
+ stores.balance.setBet(bet);
303
+ if (runtime.social) hud.setSocial(true);
304
+ const view = opts.mountView(sceneHost, {
305
+ config: opts.config,
306
+ stores,
307
+ hud,
308
+ ticker,
309
+ audio,
310
+ turbo
311
+ });
312
+ hud.setReplay(true);
313
+ hud.lockInput();
314
+ const cost = modeCostOf(opts.config, runtime.replay.mode);
315
+ const raw = await network.replay({ ...runtime.replay });
316
+ const info = roundInfo(raw, bet, cost);
317
+ const ctx = makeCtx(machine, view);
318
+ ctx.round = {
319
+ ...info,
320
+ data: opts.interpretBook(raw, info),
321
+ active: false,
322
+ balance: 0,
323
+ raw
324
+ };
325
+ const replayInfo = {
326
+ baseBet: bet,
327
+ costMultiplier: cost,
328
+ payoutMultiplier: info.multiplier,
329
+ amount: info.totalWin,
330
+ currency: resolveCurrency(cur)
331
+ };
332
+ const playRound = async () => {
333
+ stores.ui.setSpinning(true);
334
+ await machine.transition("present");
335
+ hud.replayEnd(replayInfo, () => void playRound());
336
+ };
337
+ hud.replayStart(replayInfo, () => void playRound());
338
+ } catch (err) {
339
+ showBootError({
340
+ title: "Cannot load the replay",
341
+ message: "The recorded round could not be loaded. Please reload to try again.",
342
+ detail: errMsg(err)
343
+ });
344
+ }
345
+ return;
346
+ }
347
+ let auth;
348
+ try {
349
+ await initApp(hudHost);
350
+ auth = await network.authenticate();
351
+ } catch (err) {
352
+ showBootError({
353
+ title: "Cannot reach the game server",
354
+ message: "The game could not connect to or authenticate with the game server. Please reload to try again.",
355
+ detail: errMsg(err)
356
+ });
357
+ return;
358
+ }
359
+ const currency = auth.balance.currency;
360
+ const juris = auth.config.jurisdiction ?? {};
361
+ const rtp = auth.config.rtp ?? opts.config.rtp ?? 96;
362
+ const betLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);
363
+ const active = auth.round;
364
+ const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;
365
+ const defaultBet = active?.active ? active.amount / API_AMOUNT_MULTIPLIER / activeCost : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER;
366
+ const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;
367
+ hud = mountHud(app, buildSpec({
368
+ currency,
369
+ betLevels,
370
+ defaultBet,
371
+ rtp,
372
+ confirmBuyAboveCost
373
+ }), await hudOpts());
374
+ hud.setCurrency(resolveCurrency(currency));
375
+ hud.applyJurisdiction(juris);
376
+ if (runtime.social || isSocialCurrency(currency)) hud.setSocial(true);
377
+ stores.session.set({
378
+ sessionId: runtime.sessionId,
379
+ currency,
380
+ rtp,
381
+ availableBets: betLevels,
382
+ jurisdiction: juris
383
+ });
384
+ stores.balance.setBet(defaultBet);
385
+ hud.setBet(defaultBet);
386
+ const ctx = makeCtx(machine, opts.mountView(sceneHost, {
387
+ config: opts.config,
388
+ stores,
389
+ hud,
390
+ ticker,
391
+ audio,
392
+ turbo
393
+ }));
394
+ disposers.push(reaction(() => stores.balance.balance, (b) => hud.setBalance(b), { fireImmediately: false }));
395
+ disposers.push(hud.on("valueChanged", (p) => {
396
+ const v = p;
397
+ if (v?.id === "bet-stepper" && typeof v.value === "number") stores.balance.setBet(v.value);
398
+ }));
399
+ const beginSpin = () => {
400
+ if (machine.current !== "idle") return;
401
+ turbo.resetSkip();
402
+ machine.transition("spin");
403
+ };
404
+ let holdActive = false;
405
+ disposers.push(hud.on("spinRequested", beginSpin));
406
+ disposers.push(hud.on("autoplayStarted", beginSpin));
407
+ disposers.push(hud.on("holdSpinStarted", () => {
408
+ holdActive = true;
409
+ beginSpin();
410
+ }));
411
+ disposers.push(hud.on("holdSpinStopped", () => {
412
+ holdActive = false;
413
+ }));
414
+ disposers.push(hud.on("turboChanged", (p) => {
415
+ const e = p;
416
+ if (typeof e.index === "number") turbo.setLevel(e.index);
417
+ }));
418
+ disposers.push(hud.on("skipRequested", () => turbo.skip()));
419
+ const features = buyFeaturesOf(opts.config);
420
+ if (features.length) {
421
+ const applyBetDisplay = () => {
422
+ const boost = stores.ui.activeMode;
423
+ const cost = boost ? modeCostOf(opts.config, boost) : 1;
424
+ hud.ui.bet.set(stores.balance.bet * cost);
425
+ hud.ui.bet.setEmphasis?.(cost !== 1);
426
+ };
427
+ disposers.push(mountBuyFeatureModal(app, hud, features, {
428
+ activation: "single",
429
+ getBet: () => stores.balance.bet,
430
+ onBuy: (id) => {
431
+ if (machine.current !== "idle") return;
432
+ stores.ui.setOneShotMode(id);
433
+ beginSpin();
434
+ },
435
+ onActivate: (ids) => {
436
+ stores.ui.setActiveMode(ids[0] ?? null);
437
+ applyBetDisplay();
438
+ }
439
+ }));
440
+ disposers.push(reaction(() => [stores.balance.bet, stores.ui.activeMode], applyBetDisplay));
441
+ }
442
+ const autoplayGapMs = opts.config.autoplayGapMs ?? 250;
443
+ disposers.push(reaction(() => stores.ui.spinning, (spinning, prev) => {
444
+ if (prev !== true || spinning !== false) return;
445
+ turbo.delay(autoplayGapMs).then(() => {
446
+ if (machine.current !== "idle") return;
447
+ if (!hud.ui.autoplay.isActive && !holdActive) return;
448
+ if (hud.ui.noticeBlocks.get().length > 0 || stores.balance.balance < stores.balance.bet) {
449
+ hud.ui.autoplay.stop();
450
+ holdActive = false;
451
+ return;
452
+ }
453
+ beginSpin();
454
+ });
455
+ }));
456
+ const mixer = audio;
457
+ if (mixer && typeof mixer.setGroupLevel === "function") {
458
+ disposers.push(bindMixerToHud(mixer, hud));
459
+ if (typeof mixer.unlock === "function") {
460
+ const off = hud.on("spinRequested", () => {
461
+ mixer.unlock();
462
+ off();
463
+ });
464
+ disposers.push(off);
465
+ }
466
+ }
467
+ if (active?.active) {
468
+ const end = await network.endRound();
469
+ const raw = active;
470
+ const info = roundInfo(raw, defaultBet, activeCost);
471
+ stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);
472
+ ctx.round = {
473
+ ...info,
474
+ data: opts.interpretBook(raw, info),
475
+ active: false,
476
+ balance: end.balance.amount / API_AMOUNT_MULTIPLIER,
477
+ raw
478
+ };
479
+ await machine.transition("present");
480
+ } else {
481
+ stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);
482
+ await machine.transition("idle");
483
+ }
484
+ }
485
+ function makeCtx(fsm, view) {
486
+ const ctx = {
487
+ config: opts.config,
488
+ stores,
489
+ network,
490
+ hud,
491
+ ticker,
492
+ view,
493
+ audio,
494
+ turbo,
495
+ interpretBook: opts.interpretBook,
496
+ fsm,
497
+ round: null,
498
+ modeCost: (mode) => modeCostOf(opts.config, mode)
499
+ };
500
+ fsm.bind(ctx);
501
+ return ctx;
502
+ }
503
+ function dispose() {
504
+ for (const d of disposers.splice(0)) d();
505
+ hud?.dispose();
506
+ app.destroy(true);
507
+ }
508
+ function inspect() {
509
+ return {
510
+ phase: fsm?.current ?? "boot",
511
+ balance: stores.balance.balance,
512
+ bet: stores.balance.bet,
513
+ lastWin: stores.balance.lastWin,
514
+ currency: stores.session.currency,
515
+ spinning: stores.ui.spinning
516
+ };
517
+ }
518
+ function requestSpin() {
519
+ if (!fsm || fsm.current !== "idle") return false;
520
+ fsm.transition("spin");
521
+ return true;
522
+ }
523
+ return {
524
+ start,
525
+ dispose,
526
+ inspect,
527
+ requestSpin
528
+ };
529
+ }
530
+ function errMsg(err) {
531
+ return err instanceof Error ? err.message : String(err);
532
+ }
533
+ /** An AudioSpec (declarative sounds) vs a ready AudioPort/GameAudio instance. */
534
+ function isAudioSpec(a) {
535
+ return !!a && typeof a === "object" && Array.isArray(a.sounds);
536
+ }
537
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
538
+ /** The buyable/activatable modes (`modes.<key>.buy` / `.boost`) as buy-feature cards — the
539
+ * list the bonus button opens. A plain number mode or one without `buy`/`boost` is not a card.
540
+ * `cost` in config is the FULL play multiplier (× bet); a boost card shows the SURCHARGE it
541
+ * adds over a base spin (`cost − 1`, e.g. a 2× ante → `+1× bet`), so the card + confirm read
542
+ * right while the mode is still charged its full `cost` when it spins. */
543
+ function buyFeaturesOf(config) {
544
+ const out = [];
545
+ for (const [key, m] of Object.entries(config.modes ?? {})) {
546
+ if (!m || typeof m !== "object" || !(m.buy || m.boost)) continue;
547
+ const variant = m.boost ? "boost" : "buy";
548
+ out.push({
549
+ id: key,
550
+ name: m.name ?? capitalize(key),
551
+ variant,
552
+ cost: variant === "boost" ? m.cost - 1 : m.cost,
553
+ image: m.image
554
+ });
555
+ }
556
+ return out;
557
+ }
558
+ //#endregion
559
+ export { API_AMOUNT_MULTIPLIER, BOOK_AMOUNT_MULTIPLIER, BalanceStore, DEFAULT_TURBO_SPEEDS, FSM, IdlePhase, InstantTicker, MockNetworkManager, PresentPhase, RealTicker, RootStore, SessionStore, SettlePhase, SpinPhase, StakeNetworkManager, TurboClock, UiStore, createNetwork, createStakeGame, defaultPhases, modeCostOf, readRuntime, roundEvents, roundInfo, urlParam };
560
+
561
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/engine/turbo.ts","../src/engine/round.ts","../src/engine/fsm.ts","../src/engine/phases.ts","../src/game/config.ts","../src/game/createStakeGame.ts"],"sourcesContent":["// Turbo speed + slam-stop, owned by the core. The HUD's turbo control (a 2- or 3-mode cycler)\n// drives a speed multiplier; the game's Present phase awaits `ctx.turbo.delay(ms)` for its\n// spin/anim durations, so turbo shortens them and a slam-stop (`skipRequested`) resolves them\n// instantly — with ZERO game logic. Compliance waits (minimumRoundDuration) use `ctx.ticker`,\n// NOT this, so they're never sped up or skipped.\n\n/** Delay multipliers per turbo level: off (1×) / turbo (0.4×) / super (0.12×). */\nexport const DEFAULT_TURBO_SPEEDS = [1, 0.4, 0.12];\n\nexport interface TurboState {\n /** 0 = off, 1 = turbo, 2 = super — the HUD turbo cycler's index. */\n readonly level: number;\n /** The delay multiplier for the current level (`ctx.turbo.delay` already applies it). */\n readonly speed: number;\n /** True once the player slam-stopped this round (delays resolve instantly). */\n readonly skipped: boolean;\n /** A turbo- + slam-stop-aware delay for GAME animations. NOT for compliance waits. */\n delay(ms: number): Promise<void>;\n}\n\nexport class TurboClock implements TurboState {\n private readonly speeds: number[];\n private _level = 0;\n private _skipped = false;\n private readonly pending = new Set<() => void>();\n\n constructor(speeds?: number[]) {\n this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;\n }\n\n get level(): number {\n return this._level;\n }\n get speed(): number {\n return this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;\n }\n get skipped(): boolean {\n return this._skipped;\n }\n\n /** Set the turbo level (clamped to the configured speeds). */\n setLevel(index: number): void {\n const i = Number.isFinite(index) ? Math.trunc(index) : 0;\n this._level = Math.max(0, Math.min(i, this.speeds.length - 1));\n }\n\n /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */\n skip(): void {\n this._skipped = true;\n const cbs = [...this.pending];\n this.pending.clear();\n for (const cb of cbs) cb();\n }\n\n /** Clear the slam-stop flag — the core calls this at the start of each spin. */\n resetSkip(): void {\n this._skipped = false;\n }\n\n delay(ms: number): Promise<void> {\n if (this._skipped || ms <= 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n let done = false;\n const finish = (): void => {\n if (done) return;\n done = true;\n clearTimeout(timer);\n this.pending.delete(finish);\n resolve();\n };\n const timer = setTimeout(finish, ms * this.speed);\n this.pending.add(finish);\n });\n }\n}\n","// The round model the engine drives. The engine derives all MONEY (multiplier, win,\n// stake) from the raw wire round + the bet; the game's `interpretBook` only parses the\n// book EVENTS into its own model (`data`) — the one place a game touches a round.\n\nimport { API_AMOUNT_MULTIPLIER, BOOK_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\n\n/** Money facts of a settled round (all MAJOR units except `multiplier`, a ratio). */\nexport interface RoundInfo {\n mode: string;\n /** Base bet (before the mode's cost multiplier). */\n bet: number;\n /** Mode cost multiplier (× base bet) — the stake charged. */\n cost: number;\n /** The amount staked = bet × cost. */\n stake: number;\n /** Round multiplier relative to the BASE bet (totalWin / bet). */\n multiplier: number;\n /** Total win credited this round (derived: multiplier × bet). */\n totalWin: number;\n /** The server's AUTHORITATIVE win (`raw.payout`, major units); falls back to totalWin\n * when the round carries no explicit payout. Trust this over `totalWin` when they differ. */\n payout: number;\n}\n\n/**\n * A resolved round: money facts + the game's parsed model + the raw wire round. `E` is the\n * game's book-event type — declared once on `createStakeGame`, so `raw` (and `interpretBook`)\n * are TYPED rather than `unknown`.\n */\nexport interface GameRound<T = unknown, E = unknown> extends RoundInfo {\n /** The game's parsed model, from `interpretBook` — driven to the Present phase. */\n data: T;\n /** Whether the raw round still needs `/wallet/end-round` (the engine settles it). */\n active: boolean;\n /** Authoritative post-settlement balance (major units) — applied by the Settle phase. */\n balance: number;\n /** The raw, fully-typed wire round — the game's real server data. */\n raw: Round<E>;\n}\n\n/** The game's ONE money-logic seam: parse the raw round's (typed) events into your model. Pure. */\nexport type InterpretBook<T, E = unknown> = (raw: Round<E>, info: RoundInfo) => T;\n\n/** Build the money facts from a raw wire round + the base bet + the mode cost. */\nexport function roundInfo<E = unknown>(raw: Round<E>, bet: number, cost: number): RoundInfo {\n // Stake wire convention: `payoutMultiplier` is a plain Payout/Amount ratio (1× → 1)\n // in BOOK units (×100). totalWin is relative to the BASE bet; `payout` is the server's\n // own win amount (API units → major) when the round carries one.\n const multiplier = raw.payoutMultiplier / BOOK_AMOUNT_MULTIPLIER;\n const totalWin = multiplier * bet;\n const payout = raw.payout != null ? raw.payout / API_AMOUNT_MULTIPLIER : totalWin;\n return { mode: raw.mode, bet, cost, stake: bet * cost, multiplier, totalWin, payout };\n}\n","// The round state machine. A round is an explicit sequence of awaited phases\n// (Idle → Spin → Present → Settle); each `enter` does its work + transitions on. The\n// core ships Idle/Spin/Settle; the game writes Present (and may override any). The FSM\n// + context are pure — the HUD/view/audio arrive via ports, so a whole round runs\n// headless (see the InstantTicker + MockNetworkManager).\n\nimport type { NetworkManager } from '../rgs/network';\nimport type { RootStore } from '../stores/index';\nimport type { Ticker } from './ticker';\nimport type { TurboState } from './turbo';\nimport type { HudPort } from './hud-port';\nimport type { GameRound, InterpretBook } from './round';\nimport type { GameConfig } from '../game/config';\n\n/** The minimal audio surface a game's phases call (satisfied by `@stakeplate/core/audio`). */\nexport interface AudioPort {\n play(name: string, opts?: { bus?: string; volume?: number }): void;\n music(name: string, opts?: { fadeIn?: number }): void;\n stopMusic(opts?: { fade?: number }): void;\n}\n\n/** Handed to every phase. `round` is set by SpinPhase and read by Present/Settle. `E` is\n * the game's book-event type, so `interpretBook`/`round.raw` are typed. */\nexport interface PhaseContext<T = unknown, V = unknown, E = unknown> {\n readonly config: GameConfig;\n readonly stores: RootStore;\n readonly network: NetworkManager;\n readonly hud: HudPort;\n readonly ticker: Ticker;\n /** The game's mounted view (scene/presenter/stores) — whatever `mountView` returned. */\n readonly view: V;\n readonly audio: AudioPort | null;\n /** Turbo speed + slam-stop (core-owned). Use `ctx.turbo.delay(ms)` for spin/anim timing. */\n readonly turbo: TurboState;\n readonly interpretBook: InterpretBook<T, E>;\n readonly fsm: FSM<T, V, E>;\n round: GameRound<T, E> | null;\n /** Cost multiplier for a mode key (from `config.modes`, default 1). */\n modeCost(mode: string): number;\n}\n\nexport interface Phase<T = unknown, V = unknown, E = unknown> {\n readonly name: string;\n enter(ctx: PhaseContext<T, V, E>): Promise<void> | void;\n exit?(ctx: PhaseContext<T, V, E>): void;\n}\n\nexport class FSM<T = unknown, V = unknown, E = unknown> {\n private ctx: PhaseContext<T, V, E> | null = null;\n private readonly byName = new Map<string, Phase<T, V, E>>();\n private _current = '';\n\n /** Later phases with the same `name` win — so a game's Present overrides the default. */\n constructor(phases: Phase<T, V, E>[]) {\n for (const p of phases) this.byName.set(p.name, p);\n }\n\n bind(ctx: PhaseContext<T, V, E>): void {\n this.ctx = ctx;\n }\n\n get current(): string {\n return this._current;\n }\n\n has(name: string): boolean {\n return this.byName.has(name);\n }\n\n async transition(name: string): Promise<void> {\n if (!this.ctx) throw new Error('[FSM] not bound to a context');\n const next = this.byName.get(name);\n if (!next) throw new Error(`[FSM] unknown phase: ${name}`);\n this.byName.get(this._current)?.exit?.(this.ctx);\n this._current = name;\n await next.enter(this.ctx);\n }\n}\n","// The default round phases (Idle → Spin → Present → Settle). The game writes Present\n// (animate the round) and may override any of these. Balance/bet flow through the\n// stores (createStakeGame reacts store→HUD); the phases call the HUD directly only for\n// reportRound + errors.\n\nimport { API_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\nimport { roundInfo } from './round';\nimport type { Phase, PhaseContext } from './fsm';\n\n/** Wait for the next spin (the HUD's `spinRequested` drives the transition to Spin). */\nexport class IdlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'idle';\n enter(ctx: PhaseContext<T, V, E>): void {\n ctx.stores.ui.setSpinning(false);\n }\n}\n\n/** Pick the mode, take the stake, ask the RGS, parse the round, hand off to Present. */\nexport class SpinPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'spin';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const { stores, network, hud } = ctx;\n const mode = stores.ui.nextMode();\n const cost = ctx.modeCost(mode);\n const bet = stores.balance.bet;\n const stake = bet * cost;\n\n stores.ui.setSpinning(true);\n stores.balance.debitStake(stake); // stake leaves now; the win lands at Settle\n\n try {\n const play = await network.play({ bet, mode });\n // The transport is game-agnostic (events: unknown); the game declared `E`, so we\n // narrow here — the game owns the shape it parses in `interpretBook`.\n const raw = play.round as Round<E>;\n let settledApi = play.balance.amount;\n if (raw.active) settledApi = (await network.endRound()).balance.amount;\n const info = roundInfo(raw, bet, cost);\n const data = ctx.interpretBook(raw, info);\n ctx.round = {\n ...info,\n data,\n active: raw.active ?? false,\n balance: settledApi / API_AMOUNT_MULTIPLIER,\n raw,\n };\n } catch (err) {\n stores.balance.refund(stake); // the round never happened\n stores.ui.setSpinning(false);\n const msg = err instanceof Error ? err.message : String(err);\n const code = msg.match(/ERR_[A-Z_]+/)?.[0];\n if (code) hud.showRgsError(code);\n else hud.showError(msg);\n await ctx.fsm.transition('idle');\n return;\n }\n\n await ctx.fsm.transition('present');\n }\n}\n\n/** DEFAULT Present — nothing to animate. The game overrides `present` to play its scene. */\nexport class PresentPhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'present';\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n await ctx.fsm.transition('settle');\n }\n}\n\n/** Apply the authoritative balance + win, settle the round for the HUD, then Idle. */\nexport class SettlePhase<T = unknown, V = unknown, E = unknown> implements Phase<T, V, E> {\n readonly name = 'settle';\n private roundStartedAt = 0;\n\n async enter(ctx: PhaseContext<T, V, E>): Promise<void> {\n const r = ctx.round;\n if (r) {\n ctx.stores.balance.settle(r.balance, r.totalWin);\n ctx.hud.reportRound(r.totalWin, r.bet);\n // minimumRoundDuration: pad to the jurisdiction minimum (best-effort; the boot\n // records the round start, so this covers the whole spin→settle span).\n const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;\n const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1000;\n if (minMs > 0 && elapsed < minMs) await ctx.ticker.delay(minMs - elapsed);\n }\n ctx.stores.ui.setSpinning(false);\n await ctx.fsm.transition('idle');\n }\n\n /** Called by SpinPhase-adjacent boot wiring to timestamp the round start. */\n markStart(now: number): void {\n this.roundStartedAt = now;\n }\n}\n\n/** The default phase set (game phases are appended after → same-name overrides win). */\nexport function defaultPhases<T = unknown, V = unknown, E = unknown>(): Phase<T, V, E>[] {\n return [new IdlePhase<T, V, E>(), new SpinPhase<T, V, E>(), new PresentPhase<T, V, E>(), new SettlePhase<T, V, E>()];\n}\n","// The per-game configuration passed to `createStakeGame`. Everything the core needs to\n// stand up the HUD + drive the round; the game's mechanics live in its scene + phases.\n\nexport interface ModeConfig {\n /** Cost multiplier (× base bet). `base` is 1. */\n cost: number;\n /** A one-shot bought bonus — shown as a `Buy` card in the buy-feature modal (opened by the\n * bonus button). Buying it spins this mode once (through the jurisdiction confirm gate). */\n buy?: boolean;\n /** An activatable per-spin bet surcharge — shown as an `Activate` card (vs a one-tap `Buy`). */\n boost?: boolean;\n /** Display name for the feature card (defaults to the capitalized mode key). */\n name?: string;\n /** Card art (URL or data URI) for the feature card. A neutral gradient is used when absent. */\n image?: string;\n}\n\nexport interface GameConfig {\n title: string;\n version?: string;\n /** Fallback currency code; the session/`?currency=` wins. */\n currency?: string;\n /**\n * Theoretical RTP percentage — DISPLAY ONLY (the RTP readout + rules). NOT authoritative:\n * the server (`auth.config.rtp`) wins, and the certified math report is the source of\n * truth. This is only a last-resort fallback; prefer never hand-typing it (drift risk).\n */\n rtp?: number;\n /** Mode key → cost multiplier (a number) or a `ModeConfig`. `base` defaults to 1. */\n modes?: Record<string, ModeConfig | number>;\n /** Rules/info menu for the HUD (`@open-slot-ui` `MenuSpec`) — build it with `@stakeplate/core/rules` `buildRules`. */\n rules?: unknown;\n /** i18n messages per locale (`{ en: { key: text }, es: {…} }`) for the HUD + rules text. */\n messages?: Record<string, Record<string, string>>;\n /** SOCIAL/sweepstakes wording per locale — swapped in when social mode is on. Merge in\n * `buildRules().socialEn` so the core's disclaimer/guide are social-safe. */\n socialMessages?: Record<string, Record<string, string>>;\n /** Delay multipliers per turbo level (off / turbo / super). Default `[1, 0.4, 0.12]`. */\n turboSpeeds?: number[];\n /** Pause (ms) between autoplay/hold spins. Default 250. Scales with turbo. */\n autoplayGapMs?: number;\n /** Extra `@open-slot-ui` `UISpec` fields merged into the built spec (escape hatch). */\n spec?: Record<string, unknown>;\n}\n\n// NOTE (server-authoritative): the bet ladder (`betLevels` + `defaultBetLevel`) and the\n// buy-feature confirm threshold are NOT game config — the RGS `authenticate` response owns\n// the ladder (per currency/jurisdiction) and the jurisdiction owns the confirm policy. The\n// core reads them from `auth.config`; a game only declares that a mode is a buy (`modes`).\n\n/** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */\nexport function modeCostOf(config: GameConfig, mode: string): number {\n const m = config.modes?.[mode];\n if (typeof m === 'number') return m;\n if (m && typeof m === 'object') return m.cost;\n return 1;\n}\n","// createStakeGame — the one-call façade. `start()` runs the whole compliant boot with\n// zero game involvement: runtime → network → HUD → (replay | authenticate → configure\n// HUD → resume active round → wire events) → run the FSM. Connection/auth failure →\n// the themed, blocking showBootError. The game supplies only config + interpretBook +\n// mountView (+ optional Present phase, audio). This is the ONLY module that imports the\n// HUD/pixi peers; the engine stays decoupled behind ports.\n\nimport { Application } from 'pixi.js';\nimport { mountHud, showBootError, mountBuyFeatureModal, type BootedHud, type FeatureSpec } from '@open-slot-ui/pixi';\nimport { loadBuiltinArt } from '@open-slot-ui/pixi/art';\nimport { resolveCurrency, isSocialCurrency } from '@open-slot-ui/core';\nimport { reaction } from 'mobx';\nimport { readRuntime, type RuntimeConfig } from '../rgs/runtime';\nimport { createNetwork, type NetworkManager } from '../rgs/network';\nimport { API_AMOUNT_MULTIPLIER, type Round } from '../rgs/protocol';\nimport { RootStore } from '../stores/index';\nimport { RealTicker, type Ticker } from '../engine/ticker';\nimport { TurboClock, type TurboState } from '../engine/turbo';\nimport { FSM, type AudioPort, type Phase, type PhaseContext } from '../engine/fsm';\nimport { defaultPhases } from '../engine/phases';\nimport { roundInfo, type InterpretBook } from '../engine/round';\nimport { bindMixerToHud, type MixerLike } from '../audio/bind';\nimport type { GameAudioOptions, SoundEntry } from '../audio';\nimport { modeCostOf, type GameConfig } from './config';\n\n/**\n * Declarative audio: hand the core your sounds and it lazily creates the `@schmooky/zvuk`\n * mixer (master → music/sfx/ambience buses + ducking), preloads them, and auto-binds it to\n * the HUD (sliders/mute + unlock). zvuk loads in its OWN async chunk — a game without sound\n * pays nothing. (Advanced: pass a ready `GameAudio` instance instead, for custom buses/FX.)\n */\nexport interface AudioSpec extends GameAudioOptions {\n sounds: SoundEntry[];\n}\n\n/** Passed to `mountView` — everything the game's scene needs (not the round/fsm yet). */\nexport interface ViewContext {\n config: GameConfig;\n stores: RootStore;\n hud: BootedHud;\n ticker: Ticker;\n audio: AudioPort | null;\n /** Turbo speed + slam-stop (core-owned) — the scene may branch on `turbo.level`/`speed`. */\n turbo: TurboState;\n}\n\nexport interface CreateStakeGameOptions<T = unknown, V = unknown, E = unknown> {\n config: GameConfig;\n /** The game's ONE money seam: typed RGS round (`Round<E>`) → your model. Pure. */\n interpretBook: InterpretBook<T, E>;\n /** Mount the game's pixi scene/presenter/stores; returns the view handed to phases. */\n mountView: (host: HTMLElement, ctx: ViewContext) => V;\n /** The game's Present phase (+ any overrides); Idle/Spin/Settle are provided. */\n phases?: Phase<T, V, E>[];\n /** Sounds (declarative — the core builds + wires the mixer) OR a ready `GameAudio`/AudioPort. */\n audio?: AudioPort | AudioSpec | null;\n /** Override the transport (tests / a supplied mock). */\n network?: NetworkManager;\n /** Override launch params (tests / embedding). */\n runtime?: Partial<RuntimeConfig>;\n /** Host for the pixi HUD canvas (default document.body). */\n hudHost?: HTMLElement;\n /** Host for the game scene (default = hudHost). */\n sceneHost?: HTMLElement;\n /** Passthrough to `mountHud` (spinSkin, icons, gsap, menu:false, hooks, …). */\n hudOptions?: Record<string, unknown>;\n}\n\n/**\n * Stake policy: buys/activations costing more than this many × base bet require a confirm\n * (no one-click). A buy-feature always costs far more than 2×, so in practice this means\n * \"always confirm a buy\". The jurisdiction (`auth.config.jurisdiction`) may override it; a\n * game never sets it — it's compliance, not design.\n */\nconst STAKE_CONFIRM_ABOVE_COST = 2;\n\n/** Server/replay-sourced values that drive the HUD spec — never taken from the game config. */\ninterface HudSpecInput {\n currency: string;\n betLevels: number[]; // major units — the authoritative ladder\n defaultBet: number; // major units\n rtp: number; // display %\n confirmBuyAboveCost: number; // jurisdiction policy\n}\n\n/** A read-only snapshot of the running game — for the dev harness, tests and debugging. */\nexport interface GameSnapshot {\n phase: string;\n balance: number;\n bet: number;\n lastWin: number;\n currency: string;\n spinning: boolean;\n}\n\nexport interface StakeGame {\n start(): Promise<void>;\n dispose(): void;\n /** Current phase + store values. The declarative harness asserts on this (no screenshots). */\n inspect(): GameSnapshot;\n /** Trigger a spin the same way the HUD spin button does — only from Idle. For harness/tests. */\n requestSpin(): boolean;\n}\n\nexport function createStakeGame<T = unknown, V = unknown, E = unknown>(opts: CreateStakeGameOptions<T, V, E>): StakeGame {\n const runtime = readRuntime({ overrides: opts.runtime });\n const network = opts.network ?? createNetwork(runtime);\n const stores = new RootStore();\n const ticker = new RealTicker();\n const turbo = new TurboClock(opts.config.turboSpeeds); // core-owned turbo speed + slam-stop\n // A ready AudioPort instance is used as-is; an AudioSpec is resolved lazily in start()\n // (the core creates the zvuk mixer from its own async chunk) — see resolveAudio().\n let audio: AudioPort | null = opts.audio && !isAudioSpec(opts.audio) ? opts.audio : null;\n const app = new Application();\n const disposers: Array<() => void> = [];\n let hud: BootedHud | null = null;\n let fsm: FSM<T, V, E> | null = null;\n\n // The battery wires the library's DESIGNED default icon set (the white Figma coins +\n // rotating-arrows spin skin) so every game gets the intended HUD out of the box — not\n // the lib's no-art placeholder geometry. `loadBuiltinArt` pulls the whole /art bundle\n // once; a game overrides any icon or the spin skin via `hudOptions.icons`/`spinSkin`.\n let builtinArt: ReturnType<typeof loadBuiltinArt> | null = null;\n const hudOpts = async (): Promise<Record<string, unknown>> => {\n builtinArt ??= loadBuiltinArt();\n const art = await builtinArt;\n const user = (opts.hudOptions ?? {}) as { icons?: Record<string, unknown>; spinSkin?: unknown };\n return {\n spinSkin: art.spinSkin,\n ...user,\n icons: { ...(art.icons as Record<string, unknown>), ...(user.icons ?? {}) },\n };\n };\n\n // The HUD spec is driven by SERVER-authoritative values (the ladder + confirm policy come\n // from `authenticate`/jurisdiction, not the game). `buildSpec` just formats them.\n const buildSpec = (s: HudSpecInput): Record<string, unknown> => ({\n currency: resolveCurrency(s.currency),\n betLadder: { levels: s.betLevels, index: Math.max(0, s.betLevels.indexOf(s.defaultBet)) },\n rtp: s.rtp,\n game: { name: opts.config.title, version: opts.config.version ?? '1.0.0' },\n // Stake owns fullscreen in its iframe — the game must not render its own. The bonus\n // (buy-feature) button shows only when a mode is a buy/boost card — it opens the feature list.\n controls: { fullscreen: { hidden: true }, ...(buyFeaturesOf(opts.config).length ? { bonus: { hidden: false } } : {}) },\n // Compliance: the buy-feature confirm threshold is jurisdiction policy, not a game knob.\n buyFeature: { confirmAboveCost: s.confirmBuyAboveCost },\n ...(opts.config.rules ? { menu: opts.config.rules } : {}),\n locale: {\n locale: runtime.language,\n messages: opts.config.messages ?? { en: {} },\n ...(opts.config.socialMessages ? { socialMessages: opts.config.socialMessages } : {}),\n },\n ...(opts.config.spec ?? {}),\n });\n\n const initApp = async (host: HTMLElement): Promise<void> => {\n await app.init({ resizeTo: window, backgroundAlpha: 0, antialias: true, resolution: Math.min(window.devicePixelRatio || 1, 2), autoDensity: true });\n host.appendChild(app.canvas);\n };\n\n // Build the mixer from a declarative AudioSpec. The `@stakeplate/core/audio` module (the\n // ONLY thing that pulls @schmooky/zvuk) is loaded via a DYNAMIC import, so it lands in its\n // own async chunk — an audio-less game never fetches zvuk. Sounds preload in the background.\n const resolveAudio = async (): Promise<void> => {\n if (!isAudioSpec(opts.audio)) return;\n const { createGameAudio } = await import('../audio');\n const mixer = createGameAudio(opts.audio);\n void mixer.load(opts.audio.sounds);\n audio = mixer;\n };\n\n async function start(): Promise<void> {\n const hudHost = opts.hudHost ?? document.body;\n const sceneHost = opts.sceneHost ?? hudHost;\n const phases = [...defaultPhases<T, V, E>(), ...(opts.phases ?? [])];\n const machine = (fsm = new FSM<T, V, E>(phases)); // outer `fsm` powers inspect()/requestSpin()\n await resolveAudio(); // if `audio` is an AudioSpec, build the zvuk mixer (lazy chunk) + preload\n\n // ── REPLAY (Stake ?replay=true): fetch a round + play it back read-only ──────\n if (runtime.replay.active && network.replay) {\n try {\n await initApp(hudHost);\n const cur = runtime.currency;\n const bet = runtime.replay.amount || 1; // replay carries its own bet; no ladder needed\n hud = mountHud(app, buildSpec({ currency: cur, betLevels: [bet], defaultBet: bet, rtp: opts.config.rtp ?? 96, confirmBuyAboveCost: STAKE_CONFIRM_ABOVE_COST }), (await hudOpts()) as never);\n stores.session.set({ currency: cur });\n stores.balance.setBalance(0);\n stores.balance.setBet(bet);\n if (runtime.social) hud.setSocial(true);\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo });\n hud.setReplay(true);\n hud.lockInput();\n const cost = modeCostOf(opts.config, runtime.replay.mode);\n const raw = (await network.replay({ ...runtime.replay })) as Round<E>;\n const info = roundInfo(raw, bet, cost);\n const ctx = makeCtx(machine, view);\n ctx.round = { ...info, data: opts.interpretBook(raw, info), active: false, balance: 0, raw };\n const replayInfo = { baseBet: bet, costMultiplier: cost, payoutMultiplier: info.multiplier, amount: info.totalWin, currency: resolveCurrency(cur) };\n const playRound = async (): Promise<void> => {\n stores.ui.setSpinning(true);\n await machine.transition('present'); // game's Present plays it back → settle → idle\n hud!.replayEnd(replayInfo, () => void playRound());\n };\n hud.replayStart(replayInfo, () => void playRound());\n } catch (err) {\n showBootError({ title: 'Cannot load the replay', message: 'The recorded round could not be loaded. Please reload to try again.', detail: errMsg(err) });\n }\n return;\n }\n\n // ── NORMAL BOOT ─────────────────────────────────────────────────────────────\n let auth;\n try {\n await initApp(hudHost);\n auth = await network.authenticate();\n } catch (err) {\n showBootError({\n title: 'Cannot reach the game server',\n message: 'The game could not connect to or authenticate with the game server. Please reload to try again.',\n detail: errMsg(err),\n });\n return;\n }\n\n // Everything the HUD needs is SERVER-authoritative (from `authenticate`): the bet\n // ladder + default bet (per currency/jurisdiction), RTP, and the buy-confirm policy.\n const currency = auth.balance.currency;\n const juris = auth.config.jurisdiction ?? {};\n const rtp = auth.config.rtp ?? opts.config.rtp ?? 96;\n const betLevels = auth.config.betLevels.map((b) => b / API_AMOUNT_MULTIPLIER);\n const active = auth.round;\n const activeCost = active?.active ? modeCostOf(opts.config, active.mode) : 1;\n const defaultBet = active?.active\n ? active.amount / API_AMOUNT_MULTIPLIER / activeCost // resume: restore bet from the active amount\n : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER;\n const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;\n\n hud = mountHud(app, buildSpec({ currency, betLevels, defaultBet, rtp, confirmBuyAboveCost }), (await hudOpts()) as never);\n hud.setCurrency(resolveCurrency(currency));\n hud.applyJurisdiction(juris);\n if (runtime.social || isSocialCurrency(currency)) hud.setSocial(true);\n\n stores.session.set({\n sessionId: runtime.sessionId,\n currency,\n rtp,\n availableBets: betLevels,\n jurisdiction: juris,\n });\n stores.balance.setBet(defaultBet);\n hud.setBet(defaultBet);\n\n const view = opts.mountView(sceneHost, { config: opts.config, stores, hud, ticker, audio, turbo });\n const ctx = makeCtx(machine, view);\n\n // reactions store→HUD + HUD events\n disposers.push(reaction(() => stores.balance.balance, (b) => hud!.setBalance(b), { fireImmediately: false }));\n // The bet stepper is the base-bet source of truth. It emits `valueChanged` with\n // `id: 'bet-stepper'` and the ladder value (never the boosted display), so this feeds the\n // BASE bet even while an ante is active. (The lib mirrors it into `ui.bet`; the boost\n // reaction then overwrites the readout with the effective stake.)\n disposers.push(hud.on('valueChanged', (p) => { const v = p as { id?: string; value?: number }; if (v?.id === 'bet-stepper' && typeof v.value === 'number') stores.balance.setBet(v.value); }));\n // ── Spin triggers + turbo speed + autoplay (ALL core-owned) ─────────────────\n // One entry point for every spin: clears the slam-stop flag, then spins from idle.\n const beginSpin = (): void => {\n if (machine.current !== 'idle') return;\n turbo.resetSkip();\n void machine.transition('spin');\n };\n let holdActive = false; // press-and-hold turbo spin\n disposers.push(hud.on('spinRequested', beginSpin));\n // Autoplay/hold: the HUD owns the count picker, remaining count + RG limits (via\n // reportRound); the CORE runs the loop. Start on the first tick, then re-spin below.\n disposers.push(hud.on('autoplayStarted', beginSpin));\n disposers.push(hud.on('holdSpinStarted', () => { holdActive = true; beginSpin(); }));\n disposers.push(hud.on('holdSpinStopped', () => { holdActive = false; }));\n // Turbo speed (2-/3-mode cycler) + slam-stop (tap-to-skip the reels).\n disposers.push(hud.on('turboChanged', (p) => { const e = p as { index?: number }; if (typeof e.index === 'number') turbo.setLevel(e.index); }));\n disposers.push(hud.on('skipRequested', () => turbo.skip()));\n // Buy-feature: the bonus button opens the lib's feature-LIST modal (a card per buyable\n // mode). Two kinds of card, both routed through the jurisdiction confirm gate (no one-click\n // above the threshold):\n // • `buy` → one-shot. On confirm, spin that mode ONCE (SpinPhase charges its full cost).\n // • `boost` → a persistent ante. Activating sets it as the ACTIVE mode, so every following\n // base spin plays it at its full `cost×` (via `nextMode()`); toggling off restores base.\n // The modal owns opening/closing/confirm + single-activation; the core supplies the cards +\n // the onBuy/onActivate hooks and reflects the boosted stake in the bet readout.\n const features = buyFeaturesOf(opts.config);\n if (features.length) {\n // The bet readout shows the EFFECTIVE stake (base × the active ante's cost, emphasised)\n // while a boost is on, and the plain base bet otherwise — re-applied on bet/mode change.\n const applyBetDisplay = (): void => {\n const boost = stores.ui.activeMode;\n const cost = boost ? modeCostOf(opts.config, boost) : 1;\n hud!.ui.bet.set(stores.balance.bet * cost);\n (hud!.ui.bet as unknown as { setEmphasis?: (on: boolean) => void }).setEmphasis?.(cost !== 1);\n };\n disposers.push(\n mountBuyFeatureModal(app, hud, features, {\n activation: 'single', // one ante at a time (Stake)\n getBet: () => stores.balance.bet, // the base bet — the readout shows the boosted stake\n onBuy: (id) => {\n if (machine.current !== 'idle') return;\n stores.ui.setOneShotMode(id);\n beginSpin();\n },\n onActivate: (ids) => {\n stores.ui.setActiveMode(ids[0] ?? null); // persistent ante; nextMode() prefers it\n applyBetDisplay();\n },\n }),\n );\n disposers.push(reaction(() => [stores.balance.bet, stores.ui.activeMode] as const, applyBetDisplay));\n }\n // Keep spinning while autoplay/hold is active: after each settled round (spinning\n // true → false), once idle, pause the (turbo-scaled) gap and spin again — stopping on\n // a blocking notice/error or when the next base stake is unaffordable.\n const autoplayGapMs = opts.config.autoplayGapMs ?? 250;\n disposers.push(\n reaction(\n () => stores.ui.spinning,\n (spinning, prev) => {\n if (prev !== true || spinning !== false) return;\n void turbo.delay(autoplayGapMs).then(() => {\n if (machine.current !== 'idle') return;\n if (!hud!.ui.autoplay.isActive && !holdActive) return;\n if (hud!.ui.noticeBlocks.get().length > 0 || stores.balance.balance < stores.balance.bet) {\n hud!.ui.autoplay.stop();\n holdActive = false;\n return;\n }\n beginSpin();\n });\n },\n ),\n );\n\n // Auto-wire audio ↔ HUD (Music/Effects sliders + mute, persisted) + unlock on the first\n // spin gesture — IF a mixer-like `audio` was provided. Structural check, no @schmooky/zvuk\n // import here, so audio-less games don't bundle it. (A game may bind manually instead.)\n const mixer = audio as unknown as MixerLike | null;\n if (mixer && typeof mixer.setGroupLevel === 'function') {\n disposers.push(bindMixerToHud(mixer, hud));\n if (typeof mixer.unlock === 'function') {\n const off = hud.on('spinRequested', () => { void mixer.unlock!(); off(); });\n disposers.push(off);\n }\n }\n\n // ── ACTIVE-ROUND RESUME: settle it + play it back, else idle ────────────────\n if (active?.active) {\n const end = await network.endRound();\n const raw = active as Round<E>;\n const info = roundInfo(raw, defaultBet, activeCost);\n stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);\n ctx.round = { ...info, data: opts.interpretBook(raw, info), active: false, balance: end.balance.amount / API_AMOUNT_MULTIPLIER, raw };\n await machine.transition('present'); // view plays it back → settle → idle\n } else {\n stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);\n await machine.transition('idle');\n }\n }\n\n function makeCtx(fsm: FSM<T, V, E>, view: V): PhaseContext<T, V, E> {\n const ctx: PhaseContext<T, V, E> = {\n config: opts.config,\n stores,\n network,\n hud: hud as BootedHud,\n ticker,\n view,\n audio,\n turbo,\n interpretBook: opts.interpretBook,\n fsm,\n round: null,\n modeCost: (mode) => modeCostOf(opts.config, mode),\n };\n fsm.bind(ctx);\n return ctx;\n }\n\n function dispose(): void {\n for (const d of disposers.splice(0)) d();\n hud?.dispose();\n app.destroy(true);\n }\n\n function inspect(): GameSnapshot {\n return {\n phase: fsm?.current ?? 'boot',\n balance: stores.balance.balance,\n bet: stores.balance.bet,\n lastWin: stores.balance.lastWin,\n currency: stores.session.currency,\n spinning: stores.ui.spinning,\n };\n }\n\n function requestSpin(): boolean {\n if (!fsm || fsm.current !== 'idle') return false;\n void fsm.transition('spin');\n return true;\n }\n\n return { start, dispose, inspect, requestSpin };\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** An AudioSpec (declarative sounds) vs a ready AudioPort/GameAudio instance. */\nfunction isAudioSpec(a: unknown): a is AudioSpec {\n return !!a && typeof a === 'object' && Array.isArray((a as AudioSpec).sounds);\n}\n\nconst capitalize = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);\n\n/** The buyable/activatable modes (`modes.<key>.buy` / `.boost`) as buy-feature cards — the\n * list the bonus button opens. A plain number mode or one without `buy`/`boost` is not a card.\n * `cost` in config is the FULL play multiplier (× bet); a boost card shows the SURCHARGE it\n * adds over a base spin (`cost − 1`, e.g. a 2× ante → `+1× bet`), so the card + confirm read\n * right while the mode is still charged its full `cost` when it spins. */\nfunction buyFeaturesOf(config: GameConfig): FeatureSpec[] {\n const out: FeatureSpec[] = [];\n for (const [key, m] of Object.entries(config.modes ?? {})) {\n if (!m || typeof m !== 'object' || !(m.buy || m.boost)) continue;\n const variant = m.boost ? 'boost' : 'buy';\n out.push({ id: key, name: m.name ?? capitalize(key), variant, cost: variant === 'boost' ? m.cost - 1 : m.cost, image: m.image });\n }\n return out;\n}\n"],"mappings":"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"}