@stakeplate/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { i as roundEvents, n as API_AMOUNT_MULTIPLIER, r as BOOK_AMOUNT_MULTIPLI
2
2
  import { StakeNetworkManager, createNetwork, readRuntime, urlParam } from "./rgs.js";
3
3
  import { n as RealTicker, t as InstantTicker } from "./ticker-A-XgayZv.js";
4
4
  import { BalanceStore, RootStore, SessionStore, UiStore } from "./stores.js";
5
- import { t as bindMixerToHud } from "./bind-BBRxizWR.js";
5
+ import { n as bindMixerToHud, t as bindInputSounds } from "./inputs-X3Tc_HTV.js";
6
6
  import { reaction } from "mobx";
7
7
  import { Application } from "pixi.js";
8
8
  import { mountBuyFeatureModal, mountHud, showBootError } from "@open-slot-ui/pixi";
@@ -17,17 +17,26 @@ const DEFAULT_TURBO_SPEEDS = [
17
17
  ];
18
18
  var TurboClock = class {
19
19
  speeds;
20
+ /** Delay multiplier applied while autoplay/hold runs (the floor for `speed`). Defaults to
21
+ * the turbo (level-1) speed, so autoplay plays at least at turbo pace even at turbo off. */
22
+ autoplaySpeed;
20
23
  _level = 0;
24
+ _autoplay = false;
21
25
  _skipped = false;
22
26
  pending = /* @__PURE__ */ new Set();
23
- constructor(speeds) {
27
+ constructor(speeds, autoplaySpeed) {
24
28
  this.speeds = speeds && speeds.length ? speeds : DEFAULT_TURBO_SPEEDS;
29
+ this.autoplaySpeed = autoplaySpeed ?? this.speeds[1] ?? .5;
25
30
  }
26
31
  get level() {
27
32
  return this._level;
28
33
  }
29
34
  get speed() {
30
- return this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;
35
+ const levelSpeed = this.speeds[this._level] ?? this.speeds[this.speeds.length - 1] ?? 1;
36
+ return this._autoplay ? Math.min(levelSpeed, this.autoplaySpeed) : levelSpeed;
37
+ }
38
+ get autoplay() {
39
+ return this._autoplay;
31
40
  }
32
41
  get skipped() {
33
42
  return this._skipped;
@@ -37,6 +46,11 @@ var TurboClock = class {
37
46
  const i = Number.isFinite(index) ? Math.trunc(index) : 0;
38
47
  this._level = Math.max(0, Math.min(i, this.speeds.length - 1));
39
48
  }
49
+ /** Mark autoplay/hold as running so `delay()` shortens game animations. The core sets
50
+ * this on autoplayStarted/holdSpinStarted and clears it when they stop. */
51
+ setAutoplay(active) {
52
+ this._autoplay = active;
53
+ }
40
54
  /** Slam-stop: resolve every in-flight delay + make the rest of the round instant. */
41
55
  skip() {
42
56
  this._skipped = true;
@@ -135,15 +149,14 @@ var SpinPhase = class {
135
149
  mode
136
150
  });
137
151
  const raw = play.round;
138
- let settledApi = play.balance.amount;
139
- if (raw.active) settledApi = (await network.endRound()).balance.amount;
140
152
  const info = roundInfo(raw, bet, cost);
141
153
  const data = ctx.interpretBook(raw, info);
154
+ const settledMoney = play.balance.amount / API_AMOUNT_MULTIPLIER;
142
155
  ctx.round = {
143
156
  ...info,
144
157
  data,
145
158
  active: raw.active ?? false,
146
- balance: settledApi / API_AMOUNT_MULTIPLIER,
159
+ balance: raw.active ? settledMoney + info.totalWin : settledMoney,
147
160
  raw
148
161
  };
149
162
  } catch (err) {
@@ -178,6 +191,12 @@ var SettlePhase = class {
178
191
  const minMs = ctx.stores.session.jurisdiction.minimumRoundDuration ?? 0;
179
192
  const elapsed = (ctx.ticker.now() - this.roundStartedAt) * 1e3;
180
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
+ }
181
200
  }
182
201
  ctx.stores.ui.setSpinning(false);
183
202
  await ctx.fsm.transition("idle");
@@ -197,6 +216,21 @@ function defaultPhases() {
197
216
  ];
198
217
  }
199
218
  //#endregion
219
+ //#region src/engine/beat.ts
220
+ /**
221
+ * Run a blocking announcement beat: lead → show → hold → hide → trail. Awaitable, so a
222
+ * Present phase just `await blockingBeat(ctx, { … })` and the round pauses for the whole
223
+ * thing. Returns when the beat (including the trailing pause) is over.
224
+ */
225
+ async function blockingBeat(ctx, opts) {
226
+ const wait = opts.timing === "turbo" ? (ms) => ctx.turbo.delay(ms) : (ms) => ctx.ticker.delay(ms);
227
+ if (opts.leadMs && opts.leadMs > 0) await wait(opts.leadMs);
228
+ opts.show();
229
+ await wait(opts.holdMs);
230
+ opts.hide?.();
231
+ if (opts.trailMs && opts.trailMs > 0) await wait(opts.trailMs);
232
+ }
233
+ //#endregion
200
234
  //#region src/game/config.ts
201
235
  /** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */
202
236
  function modeCostOf(config, mode) {
@@ -206,6 +240,229 @@ function modeCostOf(config, mode) {
206
240
  return 1;
207
241
  }
208
242
  //#endregion
243
+ //#region src/currency/index.ts
244
+ /** Three-decimal fiat missing from the @open-slot-ui currency table — the Gulf/Arab
245
+ * dinars & rials (the lib already covers KWD/BHD/JOD). `symbol` follows the lib's short
246
+ * latinised convention for the region (KWD→"KD", …). */
247
+ const EXTRA_DECIMALS = {
248
+ OMR: {
249
+ decimals: 3,
250
+ symbol: "OMR"
251
+ },
252
+ TND: {
253
+ decimals: 3,
254
+ symbol: "DT"
255
+ },
256
+ LYD: {
257
+ decimals: 3,
258
+ symbol: "LD"
259
+ },
260
+ IQD: {
261
+ decimals: 3,
262
+ symbol: "IQD"
263
+ }
264
+ };
265
+ /**
266
+ * `resolveCurrency`, but with the missing 3-decimal dinars/rials patched in. Use this
267
+ * everywhere a currency code becomes a `CurrencySpec` so precision is correct for every
268
+ * code — `createStakeGame` routes all of its resolution through here.
269
+ */
270
+ function currencyFor(code) {
271
+ const extra = code ? EXTRA_DECIMALS[code.toUpperCase()] : void 0;
272
+ return extra ? resolveCurrency(code, extra) : resolveCurrency(code);
273
+ }
274
+ //#endregion
275
+ //#region src/loader/index.ts
276
+ let seq = 0;
277
+ const isBrowser = () => typeof document !== "undefined" && !!document.body;
278
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
279
+ /** Create + mount the boot overlay. Safe to call before anything else in boot. */
280
+ function createLoader(config = {}) {
281
+ const startedAt = Date.now();
282
+ const minMs = config.minDurationMs ?? 1200;
283
+ const accent = config.accent ?? "#d99000";
284
+ const bg = config.background ?? "#0b0d12";
285
+ const z = config.zIndex ?? 2147483e3;
286
+ const blur = config.blur ?? true;
287
+ const features = config.features ?? [];
288
+ const hasLogo = !!config.logo;
289
+ let progress = 0;
290
+ let donePromise = null;
291
+ if (!isBrowser()) return {
292
+ el: null,
293
+ setProgress() {},
294
+ done: () => Promise.resolve(),
295
+ remove() {}
296
+ };
297
+ const id = `sp-loader-${++seq}`;
298
+ const root = document.createElement("div");
299
+ root.id = id;
300
+ root.className = "sp-loader phase-loading";
301
+ const style = document.createElement("style");
302
+ style.textContent = `
303
+ #${id}{position:fixed;inset:0;z-index:${z};background:${config.backgroundImage ? "transparent" : bg};overflow:hidden;
304
+ font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
305
+ opacity:1;transition:opacity .5s ease}
306
+ #${id}.sp-done{opacity:0}
307
+ #${id} .sp-bgimg{position:absolute;inset:-8%;background-position:center;background-size:cover;
308
+ ${blur ? "filter:blur(6px);" : ""}transform:scale(1.06)}
309
+ #${id} .sp-vign{position:absolute;inset:0;background:radial-gradient(120% 90% at 50% 42%,transparent 40%,rgba(0,0,0,.62) 100%)}
310
+ /* Logo — same element in both phases; glides + shrinks from centre to the top. */
311
+ #${id} .sp-logo{position:absolute;left:50%;top:36%;transform:translate(-50%,-50%);
312
+ width:min(42vw,240px);height:auto;filter:drop-shadow(0 14px 24px rgba(0,0,0,.55));
313
+ animation:sp-pulse 1.5s ease-in-out infinite;
314
+ transition:top .6s cubic-bezier(.5,0,.2,1),width .6s cubic-bezier(.5,0,.2,1)}
315
+ #${id}.phase-features .sp-logo{top:15%;width:min(30vw,180px);animation:none}
316
+ /* LOADING group (title/subtitle + bar) — fades out on the way to features. */
317
+ #${id} .sp-load{position:absolute;left:50%;top:58%;transform:translate(-50%,-50%);
318
+ display:flex;flex-direction:column;align-items:center;gap:14px;
319
+ transition:opacity .35s ease}
320
+ #${id}.phase-features .sp-load{opacity:0;pointer-events:none}
321
+ #${id} .sp-spin{width:60px;height:60px;border-radius:50%;border:5px solid rgba(255,255,255,.18);
322
+ border-top-color:${accent};animation:sp-rot .8s linear infinite}
323
+ #${id} .sp-title{margin:0;color:#fff;font-weight:800;letter-spacing:.02em;
324
+ 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}
325
+ #${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)}
326
+ #${id} .sp-bar{position:relative;width:min(60vw,260px);height:8px;border-radius:99px;background:rgba(255,255,255,.16);overflow:hidden}
327
+ #${id} .sp-fill{position:absolute;left:0;top:0;bottom:0;width:0%;border-radius:99px;background:${accent};transition:width .35s ease}
328
+ /* FEATURES splash — tiles + prompt; scales/fades in once boot is ready. */
329
+ #${id} .sp-feat{position:absolute;left:0;right:0;top:30%;bottom:0;display:flex;flex-direction:column;
330
+ align-items:center;justify-content:flex-start;gap:5vh;padding:0 4vw;
331
+ opacity:0;transform:scale(.94);pointer-events:none;transition:opacity .5s ease,transform .5s ease}
332
+ #${id}.phase-features .sp-feat{opacity:1;transform:scale(1);pointer-events:auto}
333
+ #${id} .sp-tiles{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;
334
+ gap:clamp(18px,5vw,64px);max-width:94vw}
335
+ #${id} .sp-tile{display:flex;flex-direction:column;align-items:center;gap:14px;
336
+ width:clamp(110px,22vw,210px);opacity:0;transform:translateY(16px) scale(.9);
337
+ transition:opacity .45s ease,transform .45s cubic-bezier(.34,1.56,.64,1)}
338
+ #${id}.phase-features .sp-tile{opacity:1;transform:none}
339
+ #${id} .sp-tile img{height:clamp(66px,13vh,150px);width:auto;max-width:100%;object-fit:contain;
340
+ filter:drop-shadow(0 8px 14px rgba(0,0,0,.45))}
341
+ #${id} .sp-tile span{color:${accent};font-weight:800;text-transform:uppercase;letter-spacing:.02em;
342
+ text-align:center;white-space:pre-line;line-height:1.15;font-size:clamp(13px,2.1vw,21px);
343
+ text-shadow:0 2px 6px rgba(0,0,0,.6)}
344
+ #${id} .sp-cont{margin-top:auto;margin-bottom:5vh;color:#ffffffcc;text-transform:uppercase;
345
+ letter-spacing:.16em;font-size:clamp(11px,1.6vw,15px);animation:sp-blink 1.7s ease-in-out infinite}
346
+ @keyframes sp-pulse{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.06)}}
347
+ @keyframes sp-rot{to{transform:rotate(360deg)}}
348
+ @keyframes sp-blink{0%,100%{opacity:.4}50%{opacity:1}}
349
+ @media (prefers-reduced-motion:reduce){#${id} .sp-logo,#${id} .sp-spin,#${id} .sp-cont{animation:none}}
350
+ `;
351
+ if (config.backgroundImage) {
352
+ const img = document.createElement("div");
353
+ img.className = "sp-bgimg";
354
+ img.style.backgroundImage = `url(${config.backgroundImage})`;
355
+ root.appendChild(img);
356
+ const vign = document.createElement("div");
357
+ vign.className = "sp-vign";
358
+ root.appendChild(vign);
359
+ }
360
+ if (hasLogo) {
361
+ const logo = document.createElement("img");
362
+ logo.className = "sp-logo";
363
+ logo.src = config.logo;
364
+ logo.alt = "";
365
+ root.appendChild(logo);
366
+ }
367
+ const load = document.createElement("div");
368
+ load.className = "sp-load";
369
+ if (!hasLogo) {
370
+ const spin = document.createElement("div");
371
+ spin.className = "sp-spin";
372
+ load.appendChild(spin);
373
+ }
374
+ if (config.title) {
375
+ const h = document.createElement("h1");
376
+ h.className = "sp-title";
377
+ h.textContent = config.title;
378
+ load.appendChild(h);
379
+ }
380
+ if (config.subtitle) {
381
+ const p = document.createElement("p");
382
+ p.className = "sp-sub";
383
+ p.textContent = config.subtitle;
384
+ load.appendChild(p);
385
+ }
386
+ const bar = document.createElement("div");
387
+ bar.className = "sp-bar";
388
+ const fill = document.createElement("div");
389
+ fill.className = "sp-fill";
390
+ bar.appendChild(fill);
391
+ load.appendChild(bar);
392
+ root.appendChild(load);
393
+ if (features.length) {
394
+ const feat = document.createElement("div");
395
+ feat.className = "sp-feat";
396
+ const tiles = document.createElement("div");
397
+ tiles.className = "sp-tiles";
398
+ features.forEach((f, i) => {
399
+ const tile = document.createElement("div");
400
+ tile.className = "sp-tile";
401
+ tile.style.transitionDelay = `${.12 + i * .12}s`;
402
+ const img = document.createElement("img");
403
+ img.src = f.image;
404
+ img.alt = "";
405
+ const cap = document.createElement("span");
406
+ cap.textContent = f.text;
407
+ tile.append(img, cap);
408
+ tiles.appendChild(tile);
409
+ });
410
+ const cont = document.createElement("div");
411
+ cont.className = "sp-cont";
412
+ cont.textContent = config.continueText ?? "PRESS TO CONTINUE";
413
+ feat.append(tiles, cont);
414
+ root.appendChild(feat);
415
+ }
416
+ root.appendChild(style);
417
+ document.body.appendChild(root);
418
+ const setProgress = (p) => {
419
+ const next = Math.max(progress, Math.min(1, Number.isFinite(p) ? p : 0));
420
+ progress = next;
421
+ fill.style.width = `${Math.round(next * 100)}%`;
422
+ };
423
+ const remove = () => {
424
+ root.remove();
425
+ style.remove();
426
+ document.getElementById("sp-bootbg")?.remove();
427
+ document.getElementById("sp-boot-style")?.remove();
428
+ };
429
+ const runFeatures = () => new Promise((resolve) => {
430
+ root.classList.remove("phase-loading");
431
+ root.classList.add("phase-features");
432
+ const dismiss = () => {
433
+ root.removeEventListener("pointerdown", dismiss);
434
+ window.removeEventListener("keydown", onKey);
435
+ resolve();
436
+ };
437
+ const onKey = (e) => {
438
+ if (e.key === " " || e.key === "Enter") dismiss();
439
+ };
440
+ setTimeout(() => {
441
+ root.addEventListener("pointerdown", dismiss);
442
+ window.addEventListener("keydown", onKey);
443
+ }, 500);
444
+ });
445
+ const done = () => {
446
+ if (donePromise) return donePromise;
447
+ donePromise = (async () => {
448
+ setProgress(1);
449
+ const wait = minMs - (Date.now() - startedAt);
450
+ if (wait > 0) await sleep(wait);
451
+ if (features.length) await runFeatures();
452
+ root.classList.add("sp-done");
453
+ await sleep(520);
454
+ remove();
455
+ })();
456
+ return donePromise;
457
+ };
458
+ return {
459
+ el: root,
460
+ setProgress,
461
+ done,
462
+ remove
463
+ };
464
+ }
465
+ //#endregion
209
466
  //#region src/game/createStakeGame.ts
210
467
  /**
211
468
  * Stake policy: buys/activations costing more than this many × base bet require a confirm
@@ -214,6 +471,42 @@ function modeCostOf(config, mode) {
214
471
  * game never sets it — it's compliance, not design.
215
472
  */
216
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
+ }
217
510
  function createStakeGame(opts) {
218
511
  const runtime = readRuntime({ overrides: opts.runtime });
219
512
  const network = opts.network ?? createNetwork(runtime);
@@ -225,6 +518,7 @@ function createStakeGame(opts) {
225
518
  const disposers = [];
226
519
  let hud = null;
227
520
  let fsm = null;
521
+ let loader = null;
228
522
  let builtinArt = null;
229
523
  const hudOpts = async () => {
230
524
  builtinArt ??= loadBuiltinArt();
@@ -240,7 +534,7 @@ function createStakeGame(opts) {
240
534
  };
241
535
  };
242
536
  const buildSpec = (s) => ({
243
- currency: resolveCurrency(s.currency),
537
+ currency: currencyFor(s.currency),
244
538
  betLadder: {
245
539
  levels: s.betLevels,
246
540
  index: Math.max(0, s.betLevels.indexOf(s.defaultBet))
@@ -283,11 +577,18 @@ function createStakeGame(opts) {
283
577
  async function start() {
284
578
  const hudHost = opts.hudHost ?? document.body;
285
579
  const sceneHost = opts.sceneHost ?? hudHost;
580
+ if (opts.loader) loader = createLoader({
581
+ title: opts.config.title,
582
+ ...opts.loader
583
+ });
584
+ const manualLoader = !!(opts.loader && opts.loader.manual);
286
585
  const machine = fsm = new FSM([...defaultPhases(), ...opts.phases ?? []]);
586
+ loader?.setProgress(.1);
287
587
  await resolveAudio();
288
588
  if (runtime.replay.active && network.replay) {
289
589
  try {
290
590
  await initApp(hudHost);
591
+ loader?.setProgress(.5);
291
592
  const cur = runtime.currency;
292
593
  const bet = runtime.replay.amount || 1;
293
594
  hud = mountHud(app, buildSpec({
@@ -301,13 +602,15 @@ function createStakeGame(opts) {
301
602
  stores.balance.setBalance(0);
302
603
  stores.balance.setBet(bet);
303
604
  if (runtime.social) hud.setSocial(true);
605
+ loader?.setProgress(.85);
304
606
  const view = opts.mountView(sceneHost, {
305
607
  config: opts.config,
306
608
  stores,
307
609
  hud,
308
610
  ticker,
309
611
  audio,
310
- turbo
612
+ turbo,
613
+ loader
311
614
  });
312
615
  hud.setReplay(true);
313
616
  hud.lockInput();
@@ -327,7 +630,7 @@ function createStakeGame(opts) {
327
630
  costMultiplier: cost,
328
631
  payoutMultiplier: info.multiplier,
329
632
  amount: info.totalWin,
330
- currency: resolveCurrency(cur)
633
+ currency: currencyFor(cur)
331
634
  };
332
635
  const playRound = async () => {
333
636
  stores.ui.setSpinning(true);
@@ -335,7 +638,9 @@ function createStakeGame(opts) {
335
638
  hud.replayEnd(replayInfo, () => void playRound());
336
639
  };
337
640
  hud.replayStart(replayInfo, () => void playRound());
641
+ if (!manualLoader) loader?.done();
338
642
  } catch (err) {
643
+ loader?.remove();
339
644
  showBootError({
340
645
  title: "Cannot load the replay",
341
646
  message: "The recorded round could not be loaded. Please reload to try again.",
@@ -347,8 +652,11 @@ function createStakeGame(opts) {
347
652
  let auth;
348
653
  try {
349
654
  await initApp(hudHost);
655
+ loader?.setProgress(.35);
350
656
  auth = await network.authenticate();
657
+ loader?.setProgress(.6);
351
658
  } catch (err) {
659
+ loader?.remove();
352
660
  showBootError({
353
661
  title: "Cannot reach the game server",
354
662
  message: "The game could not connect to or authenticate with the game server. Please reload to try again.",
@@ -359,10 +667,10 @@ function createStakeGame(opts) {
359
667
  const currency = auth.balance.currency;
360
668
  const juris = auth.config.jurisdiction ?? {};
361
669
  const rtp = auth.config.rtp ?? opts.config.rtp ?? 96;
362
- 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);
363
671
  const active = auth.round;
364
672
  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;
673
+ const defaultBet = snapToLadder(betLevels, active?.active ? active.amount / API_AMOUNT_MULTIPLIER / activeCost : auth.config.defaultBetLevel / API_AMOUNT_MULTIPLIER);
366
674
  const confirmBuyAboveCost = juris.confirmBuyAboveCost ?? STAKE_CONFIRM_ABOVE_COST;
367
675
  hud = mountHud(app, buildSpec({
368
676
  currency,
@@ -371,7 +679,7 @@ function createStakeGame(opts) {
371
679
  rtp,
372
680
  confirmBuyAboveCost
373
681
  }), await hudOpts());
374
- hud.setCurrency(resolveCurrency(currency));
682
+ hud.setCurrency(currencyFor(currency));
375
683
  hud.applyJurisdiction(juris);
376
684
  if (runtime.social || isSocialCurrency(currency)) hud.setSocial(true);
377
685
  stores.session.set({
@@ -383,14 +691,17 @@ function createStakeGame(opts) {
383
691
  });
384
692
  stores.balance.setBet(defaultBet);
385
693
  hud.setBet(defaultBet);
694
+ loader?.setProgress(.8);
386
695
  const ctx = makeCtx(machine, opts.mountView(sceneHost, {
387
696
  config: opts.config,
388
697
  stores,
389
698
  hud,
390
699
  ticker,
391
700
  audio,
392
- turbo
701
+ turbo,
702
+ loader
393
703
  }));
704
+ loader?.setProgress(.95);
394
705
  disposers.push(reaction(() => stores.balance.balance, (b) => hud.setBalance(b), { fireImmediately: false }));
395
706
  disposers.push(hud.on("valueChanged", (p) => {
396
707
  const v = p;
@@ -403,17 +714,28 @@ function createStakeGame(opts) {
403
714
  };
404
715
  let holdActive = false;
405
716
  disposers.push(hud.on("spinRequested", beginSpin));
406
- disposers.push(hud.on("autoplayStarted", beginSpin));
717
+ disposers.push(hud.on("autoplayStarted", () => {
718
+ turbo.setAutoplay(true);
719
+ beginSpin();
720
+ }));
721
+ disposers.push(hud.on("autoplayStopped", () => {
722
+ turbo.setAutoplay(holdActive);
723
+ }));
407
724
  disposers.push(hud.on("holdSpinStarted", () => {
408
725
  holdActive = true;
726
+ turbo.setAutoplay(true);
409
727
  beginSpin();
410
728
  }));
411
729
  disposers.push(hud.on("holdSpinStopped", () => {
412
730
  holdActive = false;
731
+ turbo.setAutoplay(hud.ui.autoplay.isActive);
413
732
  }));
414
733
  disposers.push(hud.on("turboChanged", (p) => {
415
734
  const e = p;
416
- if (typeof e.index === "number") turbo.setLevel(e.index);
735
+ if (typeof e.index === "number") {
736
+ turbo.setLevel(e.index);
737
+ writePrefs({ turbo: e.index });
738
+ }
417
739
  }));
418
740
  disposers.push(hud.on("skipRequested", () => turbo.skip()));
419
741
  const features = buyFeaturesOf(opts.config);
@@ -426,9 +748,13 @@ function createStakeGame(opts) {
426
748
  };
427
749
  disposers.push(mountBuyFeatureModal(app, hud, features, {
428
750
  activation: "single",
429
- getBet: () => stores.balance.bet,
751
+ getBet: () => hud.ui.betStepper.value,
430
752
  onBuy: (id) => {
431
753
  if (machine.current !== "idle") return;
754
+ if (stores.ui.activeMode) {
755
+ stores.ui.setActiveMode(null);
756
+ applyBetDisplay();
757
+ }
432
758
  stores.ui.setOneShotMode(id);
433
759
  beginSpin();
434
760
  },
@@ -464,23 +790,47 @@ function createStakeGame(opts) {
464
790
  disposers.push(off);
465
791
  }
466
792
  }
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 })));
467
801
  if (active?.active) {
468
802
  const end = await network.endRound();
469
803
  const raw = active;
470
804
  const info = roundInfo(raw, defaultBet, activeCost);
471
- stores.balance.setBalance(end.balance.amount / API_AMOUNT_MULTIPLIER);
472
- ctx.round = {
805
+ const settledBal = end.balance.amount / API_AMOUNT_MULTIPLIER;
806
+ stores.balance.setBalance(settledBal);
807
+ const resumeRound = {
473
808
  ...info,
474
809
  data: opts.interpretBook(raw, info),
475
810
  active: false,
476
- balance: end.balance.amount / API_AMOUNT_MULTIPLIER,
811
+ balance: settledBal,
477
812
  raw
478
813
  };
479
- await machine.transition("present");
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
+ });
480
829
  } else {
481
830
  stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);
482
831
  await machine.transition("idle");
483
832
  }
833
+ if (!manualLoader) loader?.done();
484
834
  }
485
835
  function makeCtx(fsm, view) {
486
836
  const ctx = {
@@ -492,6 +842,7 @@ function createStakeGame(opts) {
492
842
  view,
493
843
  audio,
494
844
  turbo,
845
+ loader,
495
846
  interpretBook: opts.interpretBook,
496
847
  fsm,
497
848
  round: null,
@@ -556,6 +907,6 @@ function buyFeaturesOf(config) {
556
907
  return out;
557
908
  }
558
909
  //#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 };
910
+ export { API_AMOUNT_MULTIPLIER, BOOK_AMOUNT_MULTIPLIER, BalanceStore, DEFAULT_TURBO_SPEEDS, EXTRA_DECIMALS, FSM, IdlePhase, InstantTicker, MockNetworkManager, PresentPhase, RealTicker, RootStore, SessionStore, SettlePhase, SpinPhase, StakeNetworkManager, TurboClock, UiStore, blockingBeat, createLoader, createNetwork, createStakeGame, currencyFor, defaultPhases, modeCostOf, readRuntime, roundEvents, roundInfo, urlParam };
560
911
 
561
912
  //# sourceMappingURL=index.js.map