@stakeplate/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/audio.d.ts +2 -2
- package/dist/audio.js +20 -5
- package/dist/audio.js.map +1 -1
- package/dist/{index-BcTRfis9.d.ts → index-BXD-eK1I.d.ts} +89 -8
- package/dist/index.d.ts +119 -2
- package/dist/index.js +372 -21
- package/dist/index.js.map +1 -1
- package/dist/{bind-BBRxizWR.js → inputs-X3Tc_HTV.js} +26 -2
- package/dist/inputs-X3Tc_HTV.js.map +1 -0
- package/dist/rules.d.ts +5 -3
- package/dist/rules.js +17 -6
- package/dist/rules.js.map +1 -1
- package/dist/vite.d.ts +39 -0
- package/dist/vite.js +57 -0
- package/dist/vite.js.map +1 -0
- package/package.json +12 -6
- package/dist/bind-BBRxizWR.js.map +0 -1
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 {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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",
|
|
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")
|
|
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: () =>
|
|
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
|
-
|
|
472
|
-
|
|
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:
|
|
811
|
+
balance: settledBal,
|
|
477
812
|
raw
|
|
478
813
|
};
|
|
479
|
-
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
|
+
});
|
|
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
|