@stakeplate/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -197,6 +211,21 @@ function defaultPhases() {
197
211
  ];
198
212
  }
199
213
  //#endregion
214
+ //#region src/engine/beat.ts
215
+ /**
216
+ * Run a blocking announcement beat: lead → show → hold → hide → trail. Awaitable, so a
217
+ * Present phase just `await blockingBeat(ctx, { … })` and the round pauses for the whole
218
+ * thing. Returns when the beat (including the trailing pause) is over.
219
+ */
220
+ async function blockingBeat(ctx, opts) {
221
+ const wait = opts.timing === "turbo" ? (ms) => ctx.turbo.delay(ms) : (ms) => ctx.ticker.delay(ms);
222
+ if (opts.leadMs && opts.leadMs > 0) await wait(opts.leadMs);
223
+ opts.show();
224
+ await wait(opts.holdMs);
225
+ opts.hide?.();
226
+ if (opts.trailMs && opts.trailMs > 0) await wait(opts.trailMs);
227
+ }
228
+ //#endregion
200
229
  //#region src/game/config.ts
201
230
  /** The cost multiplier for a mode key (default 1 for `base` / unknown modes). */
202
231
  function modeCostOf(config, mode) {
@@ -206,6 +235,229 @@ function modeCostOf(config, mode) {
206
235
  return 1;
207
236
  }
208
237
  //#endregion
238
+ //#region src/currency/index.ts
239
+ /** Three-decimal fiat missing from the @open-slot-ui currency table — the Gulf/Arab
240
+ * dinars & rials (the lib already covers KWD/BHD/JOD). `symbol` follows the lib's short
241
+ * latinised convention for the region (KWD→"KD", …). */
242
+ const EXTRA_DECIMALS = {
243
+ OMR: {
244
+ decimals: 3,
245
+ symbol: "OMR"
246
+ },
247
+ TND: {
248
+ decimals: 3,
249
+ symbol: "DT"
250
+ },
251
+ LYD: {
252
+ decimals: 3,
253
+ symbol: "LD"
254
+ },
255
+ IQD: {
256
+ decimals: 3,
257
+ symbol: "IQD"
258
+ }
259
+ };
260
+ /**
261
+ * `resolveCurrency`, but with the missing 3-decimal dinars/rials patched in. Use this
262
+ * everywhere a currency code becomes a `CurrencySpec` so precision is correct for every
263
+ * code — `createStakeGame` routes all of its resolution through here.
264
+ */
265
+ function currencyFor(code) {
266
+ const extra = code ? EXTRA_DECIMALS[code.toUpperCase()] : void 0;
267
+ return extra ? resolveCurrency(code, extra) : resolveCurrency(code);
268
+ }
269
+ //#endregion
270
+ //#region src/loader/index.ts
271
+ let seq = 0;
272
+ const isBrowser = () => typeof document !== "undefined" && !!document.body;
273
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
274
+ /** Create + mount the boot overlay. Safe to call before anything else in boot. */
275
+ function createLoader(config = {}) {
276
+ const startedAt = Date.now();
277
+ const minMs = config.minDurationMs ?? 1200;
278
+ const accent = config.accent ?? "#d99000";
279
+ const bg = config.background ?? "#0b0d12";
280
+ const z = config.zIndex ?? 2147483e3;
281
+ const blur = config.blur ?? true;
282
+ const features = config.features ?? [];
283
+ const hasLogo = !!config.logo;
284
+ let progress = 0;
285
+ let donePromise = null;
286
+ if (!isBrowser()) return {
287
+ el: null,
288
+ setProgress() {},
289
+ done: () => Promise.resolve(),
290
+ remove() {}
291
+ };
292
+ const id = `sp-loader-${++seq}`;
293
+ const root = document.createElement("div");
294
+ root.id = id;
295
+ root.className = "sp-loader phase-loading";
296
+ const style = document.createElement("style");
297
+ style.textContent = `
298
+ #${id}{position:fixed;inset:0;z-index:${z};background:${config.backgroundImage ? "transparent" : bg};overflow:hidden;
299
+ font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
300
+ opacity:1;transition:opacity .5s ease}
301
+ #${id}.sp-done{opacity:0}
302
+ #${id} .sp-bgimg{position:absolute;inset:-8%;background-position:center;background-size:cover;
303
+ ${blur ? "filter:blur(6px);" : ""}transform:scale(1.06)}
304
+ #${id} .sp-vign{position:absolute;inset:0;background:radial-gradient(120% 90% at 50% 42%,transparent 40%,rgba(0,0,0,.62) 100%)}
305
+ /* Logo — same element in both phases; glides + shrinks from centre to the top. */
306
+ #${id} .sp-logo{position:absolute;left:50%;top:36%;transform:translate(-50%,-50%);
307
+ width:min(42vw,240px);height:auto;filter:drop-shadow(0 14px 24px rgba(0,0,0,.55));
308
+ animation:sp-pulse 1.5s ease-in-out infinite;
309
+ transition:top .6s cubic-bezier(.5,0,.2,1),width .6s cubic-bezier(.5,0,.2,1)}
310
+ #${id}.phase-features .sp-logo{top:15%;width:min(30vw,180px);animation:none}
311
+ /* LOADING group (title/subtitle + bar) — fades out on the way to features. */
312
+ #${id} .sp-load{position:absolute;left:50%;top:58%;transform:translate(-50%,-50%);
313
+ display:flex;flex-direction:column;align-items:center;gap:14px;
314
+ transition:opacity .35s ease}
315
+ #${id}.phase-features .sp-load{opacity:0;pointer-events:none}
316
+ #${id} .sp-spin{width:60px;height:60px;border-radius:50%;border:5px solid rgba(255,255,255,.18);
317
+ border-top-color:${accent};animation:sp-rot .8s linear infinite}
318
+ #${id} .sp-title{margin:0;color:#fff;font-weight:800;letter-spacing:.02em;
319
+ 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}
320
+ #${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)}
321
+ #${id} .sp-bar{position:relative;width:min(60vw,260px);height:8px;border-radius:99px;background:rgba(255,255,255,.16);overflow:hidden}
322
+ #${id} .sp-fill{position:absolute;left:0;top:0;bottom:0;width:0%;border-radius:99px;background:${accent};transition:width .35s ease}
323
+ /* FEATURES splash — tiles + prompt; scales/fades in once boot is ready. */
324
+ #${id} .sp-feat{position:absolute;left:0;right:0;top:30%;bottom:0;display:flex;flex-direction:column;
325
+ align-items:center;justify-content:flex-start;gap:5vh;padding:0 4vw;
326
+ opacity:0;transform:scale(.94);pointer-events:none;transition:opacity .5s ease,transform .5s ease}
327
+ #${id}.phase-features .sp-feat{opacity:1;transform:scale(1);pointer-events:auto}
328
+ #${id} .sp-tiles{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;
329
+ gap:clamp(18px,5vw,64px);max-width:94vw}
330
+ #${id} .sp-tile{display:flex;flex-direction:column;align-items:center;gap:14px;
331
+ width:clamp(110px,22vw,210px);opacity:0;transform:translateY(16px) scale(.9);
332
+ transition:opacity .45s ease,transform .45s cubic-bezier(.34,1.56,.64,1)}
333
+ #${id}.phase-features .sp-tile{opacity:1;transform:none}
334
+ #${id} .sp-tile img{height:clamp(66px,13vh,150px);width:auto;max-width:100%;object-fit:contain;
335
+ filter:drop-shadow(0 8px 14px rgba(0,0,0,.45))}
336
+ #${id} .sp-tile span{color:${accent};font-weight:800;text-transform:uppercase;letter-spacing:.02em;
337
+ text-align:center;white-space:pre-line;line-height:1.15;font-size:clamp(13px,2.1vw,21px);
338
+ text-shadow:0 2px 6px rgba(0,0,0,.6)}
339
+ #${id} .sp-cont{margin-top:auto;margin-bottom:5vh;color:#ffffffcc;text-transform:uppercase;
340
+ letter-spacing:.16em;font-size:clamp(11px,1.6vw,15px);animation:sp-blink 1.7s ease-in-out infinite}
341
+ @keyframes sp-pulse{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.06)}}
342
+ @keyframes sp-rot{to{transform:rotate(360deg)}}
343
+ @keyframes sp-blink{0%,100%{opacity:.4}50%{opacity:1}}
344
+ @media (prefers-reduced-motion:reduce){#${id} .sp-logo,#${id} .sp-spin,#${id} .sp-cont{animation:none}}
345
+ `;
346
+ if (config.backgroundImage) {
347
+ const img = document.createElement("div");
348
+ img.className = "sp-bgimg";
349
+ img.style.backgroundImage = `url(${config.backgroundImage})`;
350
+ root.appendChild(img);
351
+ const vign = document.createElement("div");
352
+ vign.className = "sp-vign";
353
+ root.appendChild(vign);
354
+ }
355
+ if (hasLogo) {
356
+ const logo = document.createElement("img");
357
+ logo.className = "sp-logo";
358
+ logo.src = config.logo;
359
+ logo.alt = "";
360
+ root.appendChild(logo);
361
+ }
362
+ const load = document.createElement("div");
363
+ load.className = "sp-load";
364
+ if (!hasLogo) {
365
+ const spin = document.createElement("div");
366
+ spin.className = "sp-spin";
367
+ load.appendChild(spin);
368
+ }
369
+ if (config.title) {
370
+ const h = document.createElement("h1");
371
+ h.className = "sp-title";
372
+ h.textContent = config.title;
373
+ load.appendChild(h);
374
+ }
375
+ if (config.subtitle) {
376
+ const p = document.createElement("p");
377
+ p.className = "sp-sub";
378
+ p.textContent = config.subtitle;
379
+ load.appendChild(p);
380
+ }
381
+ const bar = document.createElement("div");
382
+ bar.className = "sp-bar";
383
+ const fill = document.createElement("div");
384
+ fill.className = "sp-fill";
385
+ bar.appendChild(fill);
386
+ load.appendChild(bar);
387
+ root.appendChild(load);
388
+ if (features.length) {
389
+ const feat = document.createElement("div");
390
+ feat.className = "sp-feat";
391
+ const tiles = document.createElement("div");
392
+ tiles.className = "sp-tiles";
393
+ features.forEach((f, i) => {
394
+ const tile = document.createElement("div");
395
+ tile.className = "sp-tile";
396
+ tile.style.transitionDelay = `${.12 + i * .12}s`;
397
+ const img = document.createElement("img");
398
+ img.src = f.image;
399
+ img.alt = "";
400
+ const cap = document.createElement("span");
401
+ cap.textContent = f.text;
402
+ tile.append(img, cap);
403
+ tiles.appendChild(tile);
404
+ });
405
+ const cont = document.createElement("div");
406
+ cont.className = "sp-cont";
407
+ cont.textContent = config.continueText ?? "PRESS TO CONTINUE";
408
+ feat.append(tiles, cont);
409
+ root.appendChild(feat);
410
+ }
411
+ root.appendChild(style);
412
+ document.body.appendChild(root);
413
+ const setProgress = (p) => {
414
+ const next = Math.max(progress, Math.min(1, Number.isFinite(p) ? p : 0));
415
+ progress = next;
416
+ fill.style.width = `${Math.round(next * 100)}%`;
417
+ };
418
+ const remove = () => {
419
+ root.remove();
420
+ style.remove();
421
+ document.getElementById("sp-bootbg")?.remove();
422
+ document.getElementById("sp-boot-style")?.remove();
423
+ };
424
+ const runFeatures = () => new Promise((resolve) => {
425
+ root.classList.remove("phase-loading");
426
+ root.classList.add("phase-features");
427
+ const dismiss = () => {
428
+ root.removeEventListener("pointerdown", dismiss);
429
+ window.removeEventListener("keydown", onKey);
430
+ resolve();
431
+ };
432
+ const onKey = (e) => {
433
+ if (e.key === " " || e.key === "Enter") dismiss();
434
+ };
435
+ setTimeout(() => {
436
+ root.addEventListener("pointerdown", dismiss);
437
+ window.addEventListener("keydown", onKey);
438
+ }, 500);
439
+ });
440
+ const done = () => {
441
+ if (donePromise) return donePromise;
442
+ donePromise = (async () => {
443
+ setProgress(1);
444
+ const wait = minMs - (Date.now() - startedAt);
445
+ if (wait > 0) await sleep(wait);
446
+ if (features.length) await runFeatures();
447
+ root.classList.add("sp-done");
448
+ await sleep(520);
449
+ remove();
450
+ })();
451
+ return donePromise;
452
+ };
453
+ return {
454
+ el: root,
455
+ setProgress,
456
+ done,
457
+ remove
458
+ };
459
+ }
460
+ //#endregion
209
461
  //#region src/game/createStakeGame.ts
210
462
  /**
211
463
  * Stake policy: buys/activations costing more than this many × base bet require a confirm
@@ -225,6 +477,7 @@ function createStakeGame(opts) {
225
477
  const disposers = [];
226
478
  let hud = null;
227
479
  let fsm = null;
480
+ let loader = null;
228
481
  let builtinArt = null;
229
482
  const hudOpts = async () => {
230
483
  builtinArt ??= loadBuiltinArt();
@@ -240,7 +493,7 @@ function createStakeGame(opts) {
240
493
  };
241
494
  };
242
495
  const buildSpec = (s) => ({
243
- currency: resolveCurrency(s.currency),
496
+ currency: currencyFor(s.currency),
244
497
  betLadder: {
245
498
  levels: s.betLevels,
246
499
  index: Math.max(0, s.betLevels.indexOf(s.defaultBet))
@@ -283,11 +536,18 @@ function createStakeGame(opts) {
283
536
  async function start() {
284
537
  const hudHost = opts.hudHost ?? document.body;
285
538
  const sceneHost = opts.sceneHost ?? hudHost;
539
+ if (opts.loader) loader = createLoader({
540
+ title: opts.config.title,
541
+ ...opts.loader
542
+ });
543
+ const manualLoader = !!(opts.loader && opts.loader.manual);
286
544
  const machine = fsm = new FSM([...defaultPhases(), ...opts.phases ?? []]);
545
+ loader?.setProgress(.1);
287
546
  await resolveAudio();
288
547
  if (runtime.replay.active && network.replay) {
289
548
  try {
290
549
  await initApp(hudHost);
550
+ loader?.setProgress(.5);
291
551
  const cur = runtime.currency;
292
552
  const bet = runtime.replay.amount || 1;
293
553
  hud = mountHud(app, buildSpec({
@@ -301,13 +561,15 @@ function createStakeGame(opts) {
301
561
  stores.balance.setBalance(0);
302
562
  stores.balance.setBet(bet);
303
563
  if (runtime.social) hud.setSocial(true);
564
+ loader?.setProgress(.85);
304
565
  const view = opts.mountView(sceneHost, {
305
566
  config: opts.config,
306
567
  stores,
307
568
  hud,
308
569
  ticker,
309
570
  audio,
310
- turbo
571
+ turbo,
572
+ loader
311
573
  });
312
574
  hud.setReplay(true);
313
575
  hud.lockInput();
@@ -327,7 +589,7 @@ function createStakeGame(opts) {
327
589
  costMultiplier: cost,
328
590
  payoutMultiplier: info.multiplier,
329
591
  amount: info.totalWin,
330
- currency: resolveCurrency(cur)
592
+ currency: currencyFor(cur)
331
593
  };
332
594
  const playRound = async () => {
333
595
  stores.ui.setSpinning(true);
@@ -335,7 +597,9 @@ function createStakeGame(opts) {
335
597
  hud.replayEnd(replayInfo, () => void playRound());
336
598
  };
337
599
  hud.replayStart(replayInfo, () => void playRound());
600
+ if (!manualLoader) loader?.done();
338
601
  } catch (err) {
602
+ loader?.remove();
339
603
  showBootError({
340
604
  title: "Cannot load the replay",
341
605
  message: "The recorded round could not be loaded. Please reload to try again.",
@@ -347,8 +611,11 @@ function createStakeGame(opts) {
347
611
  let auth;
348
612
  try {
349
613
  await initApp(hudHost);
614
+ loader?.setProgress(.35);
350
615
  auth = await network.authenticate();
616
+ loader?.setProgress(.6);
351
617
  } catch (err) {
618
+ loader?.remove();
352
619
  showBootError({
353
620
  title: "Cannot reach the game server",
354
621
  message: "The game could not connect to or authenticate with the game server. Please reload to try again.",
@@ -371,7 +638,7 @@ function createStakeGame(opts) {
371
638
  rtp,
372
639
  confirmBuyAboveCost
373
640
  }), await hudOpts());
374
- hud.setCurrency(resolveCurrency(currency));
641
+ hud.setCurrency(currencyFor(currency));
375
642
  hud.applyJurisdiction(juris);
376
643
  if (runtime.social || isSocialCurrency(currency)) hud.setSocial(true);
377
644
  stores.session.set({
@@ -383,14 +650,17 @@ function createStakeGame(opts) {
383
650
  });
384
651
  stores.balance.setBet(defaultBet);
385
652
  hud.setBet(defaultBet);
653
+ loader?.setProgress(.8);
386
654
  const ctx = makeCtx(machine, opts.mountView(sceneHost, {
387
655
  config: opts.config,
388
656
  stores,
389
657
  hud,
390
658
  ticker,
391
659
  audio,
392
- turbo
660
+ turbo,
661
+ loader
393
662
  }));
663
+ loader?.setProgress(.95);
394
664
  disposers.push(reaction(() => stores.balance.balance, (b) => hud.setBalance(b), { fireImmediately: false }));
395
665
  disposers.push(hud.on("valueChanged", (p) => {
396
666
  const v = p;
@@ -403,13 +673,21 @@ function createStakeGame(opts) {
403
673
  };
404
674
  let holdActive = false;
405
675
  disposers.push(hud.on("spinRequested", beginSpin));
406
- disposers.push(hud.on("autoplayStarted", beginSpin));
676
+ disposers.push(hud.on("autoplayStarted", () => {
677
+ turbo.setAutoplay(true);
678
+ beginSpin();
679
+ }));
680
+ disposers.push(hud.on("autoplayStopped", () => {
681
+ turbo.setAutoplay(holdActive);
682
+ }));
407
683
  disposers.push(hud.on("holdSpinStarted", () => {
408
684
  holdActive = true;
685
+ turbo.setAutoplay(true);
409
686
  beginSpin();
410
687
  }));
411
688
  disposers.push(hud.on("holdSpinStopped", () => {
412
689
  holdActive = false;
690
+ turbo.setAutoplay(hud.ui.autoplay.isActive);
413
691
  }));
414
692
  disposers.push(hud.on("turboChanged", (p) => {
415
693
  const e = p;
@@ -464,6 +742,7 @@ function createStakeGame(opts) {
464
742
  disposers.push(off);
465
743
  }
466
744
  }
745
+ if (audio && isAudioSpec(opts.audio) && opts.audio.inputSounds) disposers.push(bindInputSounds(audio, hud, opts.audio.inputSounds));
467
746
  if (active?.active) {
468
747
  const end = await network.endRound();
469
748
  const raw = active;
@@ -481,6 +760,7 @@ function createStakeGame(opts) {
481
760
  stores.balance.setBalance(auth.balance.amount / API_AMOUNT_MULTIPLIER);
482
761
  await machine.transition("idle");
483
762
  }
763
+ if (!manualLoader) loader?.done();
484
764
  }
485
765
  function makeCtx(fsm, view) {
486
766
  const ctx = {
@@ -492,6 +772,7 @@ function createStakeGame(opts) {
492
772
  view,
493
773
  audio,
494
774
  turbo,
775
+ loader,
495
776
  interpretBook: opts.interpretBook,
496
777
  fsm,
497
778
  round: null,
@@ -556,6 +837,6 @@ function buyFeaturesOf(config) {
556
837
  return out;
557
838
  }
558
839
  //#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 };
840
+ 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
841
 
561
842
  //# sourceMappingURL=index.js.map