@open-slot-ui/pixi 0.8.2 → 0.9.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.d.cts CHANGED
@@ -762,6 +762,31 @@ interface BuyFeatureOptions {
762
762
  /** Mount the buy-feature modal. Returns a leak-free teardown. */
763
763
  declare function mountBuyFeatureModal(_app: Application, hud: BootedHud, features: FeatureSpec[], opts?: BuyFeatureOptions): () => void;
764
764
 
765
+ /** Minimal HUD surface the confirm needs — the translator + theme font. */
766
+ interface ConfirmUI {
767
+ t(key: string, params?: Record<string, string | number>): string;
768
+ theme: {
769
+ type: {
770
+ family: string;
771
+ };
772
+ };
773
+ }
774
+ interface ConfirmOptions {
775
+ /** Heading (already resolved text, or an i18n key). Default `Buy Feature`. */
776
+ title?: string;
777
+ /** Body message (already resolved). */
778
+ message: string;
779
+ /** Confirm button label (resolved text or i18n key). Default `openui.confirm`. */
780
+ confirmLabel?: string;
781
+ /** Cancel button label. Default `openui.cancel`. */
782
+ cancelLabel?: string;
783
+ }
784
+ /**
785
+ * Show the universal confirm dialog. Resolves `true` on Confirm, `false` on Cancel / backdrop
786
+ * / Escape. Self-mounting + self-cleaning; layers above everything (z-index above the buy modal).
787
+ */
788
+ declare function showConfirm(ui: ConfirmUI, opts: ConfirmOptions): Promise<boolean>;
789
+
765
790
  interface BootErrorOptions {
766
791
  title?: string;
767
792
  message?: string;
@@ -844,4 +869,4 @@ declare class Tweener {
844
869
  /** Treat touch devices as non-desktop so we don't fire phantom hover states. */
845
870
  declare function isDesktop(): boolean;
846
871
 
847
- export { AutoplayDrawerView, AutoplayView, type AutoplayViewOptions, type BootErrorOptions, type BootedHud, type ButtonGlyph, ButtonView, type ButtonViewOptions, type BuyFeatureOptions, ControlView, ControlViewFactory, DialogView, type DialogViewOptions, type FeatureSpec, type HudOptions, type InfoContentBuilder, MenuView, type MenuViewOptions, OpenUIPixi, OpenUIPixiOptions, type PanelBodyOptions, PanelBodyView, PanelView, type PanelViewOptions, type PopoverArt, type PopoverParts, PopoverView, ReadoutView, type ReadoutViewOptions, type ReplayInfo, SelectView, type SelectViewOptions, SliderView, SpinSkin, SpinSkinFactory, SpinView, type SpinViewOptions, StepperView, type SvgSpinTextures, TextCellRenderer, type TextCellRendererOptions, ToggleView, type ToggleViewOptions, TurboView, type TurboViewOptions, Tweener, ValueDisplayView, defaultSpinSkin, drawSpin, hideBootError, isDesktop, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError, svgSpinSkin };
872
+ export { AutoplayDrawerView, AutoplayView, type AutoplayViewOptions, type BootErrorOptions, type BootedHud, type ButtonGlyph, ButtonView, type ButtonViewOptions, type BuyFeatureOptions, type ConfirmOptions, ControlView, ControlViewFactory, DialogView, type DialogViewOptions, type FeatureSpec, type HudOptions, type InfoContentBuilder, MenuView, type MenuViewOptions, OpenUIPixi, OpenUIPixiOptions, type PanelBodyOptions, PanelBodyView, PanelView, type PanelViewOptions, type PopoverArt, type PopoverParts, PopoverView, ReadoutView, type ReadoutViewOptions, type ReplayInfo, SelectView, type SelectViewOptions, SliderView, SpinSkin, SpinSkinFactory, SpinView, type SpinViewOptions, StepperView, type SvgSpinTextures, TextCellRenderer, type TextCellRendererOptions, ToggleView, type ToggleViewOptions, TurboView, type TurboViewOptions, Tweener, ValueDisplayView, defaultSpinSkin, drawSpin, hideBootError, isDesktop, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError, showConfirm, svgSpinSkin };
package/dist/index.d.ts CHANGED
@@ -762,6 +762,31 @@ interface BuyFeatureOptions {
762
762
  /** Mount the buy-feature modal. Returns a leak-free teardown. */
763
763
  declare function mountBuyFeatureModal(_app: Application, hud: BootedHud, features: FeatureSpec[], opts?: BuyFeatureOptions): () => void;
764
764
 
765
+ /** Minimal HUD surface the confirm needs — the translator + theme font. */
766
+ interface ConfirmUI {
767
+ t(key: string, params?: Record<string, string | number>): string;
768
+ theme: {
769
+ type: {
770
+ family: string;
771
+ };
772
+ };
773
+ }
774
+ interface ConfirmOptions {
775
+ /** Heading (already resolved text, or an i18n key). Default `Buy Feature`. */
776
+ title?: string;
777
+ /** Body message (already resolved). */
778
+ message: string;
779
+ /** Confirm button label (resolved text or i18n key). Default `openui.confirm`. */
780
+ confirmLabel?: string;
781
+ /** Cancel button label. Default `openui.cancel`. */
782
+ cancelLabel?: string;
783
+ }
784
+ /**
785
+ * Show the universal confirm dialog. Resolves `true` on Confirm, `false` on Cancel / backdrop
786
+ * / Escape. Self-mounting + self-cleaning; layers above everything (z-index above the buy modal).
787
+ */
788
+ declare function showConfirm(ui: ConfirmUI, opts: ConfirmOptions): Promise<boolean>;
789
+
765
790
  interface BootErrorOptions {
766
791
  title?: string;
767
792
  message?: string;
@@ -844,4 +869,4 @@ declare class Tweener {
844
869
  /** Treat touch devices as non-desktop so we don't fire phantom hover states. */
845
870
  declare function isDesktop(): boolean;
846
871
 
847
- export { AutoplayDrawerView, AutoplayView, type AutoplayViewOptions, type BootErrorOptions, type BootedHud, type ButtonGlyph, ButtonView, type ButtonViewOptions, type BuyFeatureOptions, ControlView, ControlViewFactory, DialogView, type DialogViewOptions, type FeatureSpec, type HudOptions, type InfoContentBuilder, MenuView, type MenuViewOptions, OpenUIPixi, OpenUIPixiOptions, type PanelBodyOptions, PanelBodyView, PanelView, type PanelViewOptions, type PopoverArt, type PopoverParts, PopoverView, ReadoutView, type ReadoutViewOptions, type ReplayInfo, SelectView, type SelectViewOptions, SliderView, SpinSkin, SpinSkinFactory, SpinView, type SpinViewOptions, StepperView, type SvgSpinTextures, TextCellRenderer, type TextCellRendererOptions, ToggleView, type ToggleViewOptions, TurboView, type TurboViewOptions, Tweener, ValueDisplayView, defaultSpinSkin, drawSpin, hideBootError, isDesktop, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError, svgSpinSkin };
872
+ export { AutoplayDrawerView, AutoplayView, type AutoplayViewOptions, type BootErrorOptions, type BootedHud, type ButtonGlyph, ButtonView, type ButtonViewOptions, type BuyFeatureOptions, type ConfirmOptions, ControlView, ControlViewFactory, DialogView, type DialogViewOptions, type FeatureSpec, type HudOptions, type InfoContentBuilder, MenuView, type MenuViewOptions, OpenUIPixi, OpenUIPixiOptions, type PanelBodyOptions, PanelBodyView, PanelView, type PanelViewOptions, type PopoverArt, type PopoverParts, PopoverView, ReadoutView, type ReadoutViewOptions, type ReplayInfo, SelectView, type SelectViewOptions, SliderView, SpinSkin, SpinSkinFactory, SpinView, type SpinViewOptions, StepperView, type SvgSpinTextures, TextCellRenderer, type TextCellRendererOptions, ToggleView, type ToggleViewOptions, TurboView, type TurboViewOptions, Tweener, ValueDisplayView, defaultSpinSkin, drawSpin, hideBootError, isDesktop, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError, showConfirm, svgSpinSkin };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { ControlView, ButtonView, SliderView, buildBlockColumn, OpenUIPixi } from './chunk-PYXZIGXG.js';
2
- export { AutoplayDrawerView, AutoplayView, ButtonView, ControlView, DialogView, MenuView, OpenUIPixi, ReadoutView, SelectView, SliderView, SpinView, StepperView, TextCellRenderer, ToggleView, TurboView, Tweener, ValueDisplayView, buildBlockColumn, defaultSpinSkin, drawSpin, isDesktop, svgSpinSkin } from './chunk-PYXZIGXG.js';
1
+ import { ControlView, ButtonView, SliderView, buildBlockColumn, OpenUIPixi } from './chunk-Z7X5JH7H.js';
2
+ export { AutoplayDrawerView, AutoplayView, ButtonView, ControlView, DialogView, MenuView, OpenUIPixi, ReadoutView, SelectView, SliderView, SpinView, StepperView, TextCellRenderer, ToggleView, TurboView, Tweener, ValueDisplayView, buildBlockColumn, defaultSpinSkin, drawSpin, isDesktop, svgSpinSkin } from './chunk-Z7X5JH7H.js';
3
3
  import { Text, Graphics, Container, Rectangle } from 'pixi.js';
4
- import { LOCALE_LABELS, createUI, composeMenu, buildPanel, formatAmount } from '@open-slot-ui/core';
4
+ import { LOCALE_LABELS, createUI, composeMenu, buildPanel, modeStatsItems, formatAmountPrecise, factsVars, auditRules } from '@open-slot-ui/core';
5
5
 
6
6
  var RISE = 26;
7
7
  var STAGGER = 38;
@@ -280,7 +280,11 @@ var PanelBodyView = class extends ControlView {
280
280
  };
281
281
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
282
282
  var rich = (s) => esc(s).replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
283
- function renderBlocks(blocks, tr) {
283
+ function statGridHtml(items, tr) {
284
+ const rows = items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
285
+ return `<dl class="ohm-stats">${rows}</dl>`;
286
+ }
287
+ function renderBlocks(blocks, tr, facts) {
284
288
  const out = [];
285
289
  for (const b of blocks) {
286
290
  switch (b.kind) {
@@ -322,9 +326,16 @@ function renderBlocks(blocks, tr) {
322
326
  out.push(`<table class="ohm-table">${head}<tbody>${body}</tbody></table>`);
323
327
  break;
324
328
  }
325
- case "stat-grid": {
326
- const rows = b.items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
327
- out.push(`<dl class="ohm-stats">${rows}</dl>`);
329
+ case "stat-grid":
330
+ out.push(statGridHtml(b.items, tr));
331
+ break;
332
+ // The AUTO per-mode RTP / Max-win grid — rendered straight from the declared
333
+ // game facts (+ any host extras), so the table can never drift from the config.
334
+ // `modeStatsItems` localizes the label PARTS itself ("Max win" · the mode name),
335
+ // so the grid renderer gets identity-tr — no double translation.
336
+ case "mode-stats": {
337
+ const extras = (b.extras ?? []).map((e) => ({ label: tr(e.label), value: tr(e.value) }));
338
+ out.push(statGridHtml([...modeStatsItems(facts, tr), ...extras], (s) => s));
328
339
  break;
329
340
  }
330
341
  case "steps": {
@@ -394,8 +405,30 @@ function mountInfoMenu(app, ui) {
394
405
  const soundMode = menu.sound ?? "sliders";
395
406
  const volRow = (id, label, hint, val) => `<div class="ohm-setting"><label class="ohm-row"><span>${esc(tr(label))}</span><input class="ohm-slider" data-vol="${id}" type="range" min="0" max="1" step="0.01" value="${val}"></label><div class="ohm-hint">${esc(tr(hint))}</div></div>`;
396
407
  const soundSliders = soundMode === "master" ? volRow("master", "Volume", "Adjust the overall game volume.", ui.sfxSlider.value.get()) : soundMode === "sliders" ? volRow("music", "Music", "Adjust the background music volume.", ui.musicSlider.value.get()) + volRow("sfx", "Effects", "Adjust the sound-effects volume.", ui.sfxSlider.value.get()) : "";
397
- const paytableHtml = menu.paytable ? renderBlocks(menu.paytable, tr) : "";
398
- const rulesHtml = menu.rules ? renderBlocks(menu.rules, tr) : "";
408
+ const rulesTr = () => {
409
+ const vars2 = factsVars(ui.facts.get(), { "game.name": ui.gameInfo.name ?? "", "game.version": ui.gameInfo.version ?? "" });
410
+ return (s) => ui.t(s, vars2);
411
+ };
412
+ const auditCardHtml = (trr) => {
413
+ if (!menu.rules?.length) return "";
414
+ const issues = auditRules(ui.facts.get(), menu.rules, { resolve: trr });
415
+ if (!issues.length) return "";
416
+ const req = issues.filter((i) => i.level === "required");
417
+ const rec = issues.filter((i) => i.level === "recommended");
418
+ const list = (items) => `<ul>${items.map((i) => `<li>${esc(tr(i.message))}</li>`).join("")}</ul>`;
419
+ return `<div class="ohm-audit" role="alert">
420
+ <div class="ohm-audit-title">\u26A0 ${esc(tr("openui.rulesAudit.title"))}</div>
421
+ ${req.length ? `<div class="ohm-audit-sub">${esc(tr("openui.rulesAudit.required"))}</div>${list(req)}` : ""}
422
+ ${rec.length ? `<div class="ohm-audit-sub ohm-audit-sub--rec">${esc(tr("openui.rulesAudit.recommended"))}</div><div class="ohm-audit-rec">${list(rec)}</div>` : ""}
423
+ </div>`;
424
+ };
425
+ const rulesInnerHtml = () => {
426
+ if (!menu.rules) return "";
427
+ const trr = rulesTr();
428
+ return auditCardHtml(trr) + renderBlocks(menu.rules, trr, ui.facts.get());
429
+ };
430
+ const paytableHtml = menu.paytable ? renderBlocks(menu.paytable, tr, ui.facts.get()) : "";
431
+ const rulesHtml = rulesInnerHtml();
399
432
  const banner = menu.banner ? `<img class="ohm-logo" alt="" src="${menu.banner.src}"${menu.banner.width ? ` width="${menu.banner.width}"` : ""}${menu.banner.height ? ` height="${menu.banner.height}"` : ""}>` : ui.gameInfo.name ? `<h1 class="ohm-logo-text">${esc(ui.gameInfo.name)}</h1>` : "";
400
433
  host2.innerHTML = `
401
434
  <div class="ohm-backdrop" data-close></div>
@@ -467,19 +500,40 @@ function mountInfoMenu(app, ui) {
467
500
  else if (el instanceof HTMLSelectElement) el.addEventListener("change", () => ui.bus.emit("optionSelected", { id, value: el.value, index: el.selectedIndex }));
468
501
  });
469
502
  host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", () => ui.settingsPanel.closePanel()));
503
+ const refreshRules = () => {
504
+ const rulesEl = host2.querySelector("#ohm-rules");
505
+ if (rulesEl && menu.rules) rulesEl.innerHTML = rulesInnerHtml();
506
+ };
470
507
  disposers.push(
471
508
  ui.locale.subscribe(() => {
472
509
  const body = host2.querySelector(".ohm-body");
473
510
  if (body) {
474
- const rulesEl = host2.querySelector("#ohm-rules");
475
- if (rulesEl && menu.rules) rulesEl.innerHTML = renderBlocks(menu.rules, tr);
511
+ refreshRules();
476
512
  if (lang) lang.value = ui.locale.get();
477
513
  }
514
+ }),
515
+ ui.facts.subscribe(refreshRules)
516
+ );
517
+ let menuLocked = false;
518
+ disposers.push(
519
+ ui.settingsPanel.state.subscribe(() => {
520
+ const isOpen = ui.settingsPanel.isOpen;
521
+ host2.classList.toggle("open", isOpen);
522
+ if (isOpen && !menuLocked) {
523
+ ui.lock();
524
+ menuLocked = true;
525
+ } else if (!isOpen && menuLocked) {
526
+ ui.unlock();
527
+ menuLocked = false;
528
+ }
478
529
  })
479
530
  );
480
- disposers.push(ui.settingsPanel.state.subscribe(() => host2.classList.toggle("open", ui.settingsPanel.isOpen)));
481
531
  return () => {
482
532
  for (const d of disposers.splice(0)) d();
533
+ if (menuLocked) {
534
+ ui.unlock();
535
+ menuLocked = false;
536
+ }
483
537
  host2.remove();
484
538
  };
485
539
  }
@@ -533,6 +587,13 @@ var OHM_CSS = `
533
587
  .ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
534
588
  .ohm-stats > div { display: flex; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid color-mix(in srgb, var(--text-dim) 20%, transparent); }
535
589
  .ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }
590
+ .ohm-audit { margin: 12px 0 18px; padding: 14px 16px; border-radius: 12px; border: 2px solid #d03131; background: color-mix(in srgb, #d03131 8%, transparent); }
591
+ .ohm-audit-title { font-weight: 900; letter-spacing: .5px; color: #b31d1d; }
592
+ .ohm-audit-sub { margin-top: 8px; font-size: 13px; font-weight: 800; color: #b31d1d; }
593
+ .ohm-audit-sub--rec { color: #b07d09; }
594
+ .ohm-audit ul { margin: 6px 0 0; padding-left: 20px; color: var(--text); font-size: 13.5px; line-height: 1.55; }
595
+ .ohm-audit li { margin: 3px 0; }
596
+ .ohm-audit-rec ul { color: var(--text-dim); }
536
597
  .ohm-callout { margin: 16px 0 4px; padding: 14px 16px; border-radius: 12px; border-left: 4px solid var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); }
537
598
  .ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }
538
599
  .ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }
@@ -562,10 +623,67 @@ var OHM_CSS = `
562
623
  .ohm-group { margin: 8px 0; }
563
624
  `;
564
625
 
626
+ // src/confirmModal.ts
627
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
628
+ function showConfirm(ui, opts) {
629
+ return new Promise((resolve) => {
630
+ const host2 = document.createElement("div");
631
+ host2.className = "osc-confirm";
632
+ host2.style.setProperty("--font", ui.theme.type.family);
633
+ const title = ui.t(opts.title ?? "Buy Feature");
634
+ const yes = ui.t(opts.confirmLabel ?? "openui.confirm");
635
+ const no = ui.t(opts.cancelLabel ?? "openui.cancel");
636
+ host2.innerHTML = `
637
+ <div class="osc-confirm-backdrop" data-no></div>
638
+ <div class="osc-confirm-card" role="dialog" aria-modal="true">
639
+ ${title ? `<h3 class="osc-confirm-title">${esc2(title)}</h3>` : ""}
640
+ <p class="osc-confirm-msg">${esc2(opts.message)}</p>
641
+ <div class="osc-confirm-row">
642
+ <button class="osc-confirm-btn osc-confirm-no" data-no>${esc2(no)}</button>
643
+ <button class="osc-confirm-btn osc-confirm-yes" data-yes>${esc2(yes)}</button>
644
+ </div>
645
+ </div>`;
646
+ const style = document.createElement("style");
647
+ style.textContent = CONFIRM_CSS;
648
+ host2.appendChild(style);
649
+ document.body.appendChild(host2);
650
+ let settled = false;
651
+ const close = (val) => {
652
+ if (settled) return;
653
+ settled = true;
654
+ window.removeEventListener("keydown", onKey);
655
+ host2.remove();
656
+ resolve(val);
657
+ };
658
+ const onKey = (e) => {
659
+ if (e.key === "Escape") close(false);
660
+ else if (e.key === "Enter") close(true);
661
+ };
662
+ host2.querySelectorAll("[data-no]").forEach((b) => b.addEventListener("click", () => close(false)));
663
+ host2.querySelector("[data-yes]")?.addEventListener("click", () => close(true));
664
+ window.addEventListener("keydown", onKey);
665
+ });
666
+ }
667
+ var CONFIRM_CSS = `
668
+ .osc-confirm { position: fixed; inset: 0; z-index: 12000; display: grid; place-items: center; font-family: var(--font); }
669
+ .osc-confirm *, .osc-confirm *::before, .osc-confirm *::after { box-sizing: border-box; }
670
+ .osc-confirm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
671
+ .osc-confirm-card { position: relative; width: min(90vw, 420px); background: #ffffff; color: #181b20; border: 3px solid #000; border-radius: 14px; padding: 24px; box-shadow: 0 24px 60px rgba(0,0,0,.55); text-align: center; }
672
+ .osc-confirm-title { margin: 0 0 12px; font-size: 22px; font-weight: 800; letter-spacing: .3px; }
673
+ .osc-confirm-msg { margin: 0 0 22px; font-size: 19px; font-weight: 700; line-height: 1.4; }
674
+ .osc-confirm-row { display: flex; gap: 14px; }
675
+ .osc-confirm-btn { flex: 1; padding: 14px 10px; border-radius: 12px; border: 3px solid #000; font-size: 15px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase; cursor: pointer; transition: transform .1s, background .12s; }
676
+ .osc-confirm-btn:active { transform: scale(.96); }
677
+ .osc-confirm-no { background: #ffffff; color: #181b20; }
678
+ .osc-confirm-no:hover { background: #eef1f6; }
679
+ .osc-confirm-yes { background: #d99000; color: #1a1200; }
680
+ .osc-confirm-yes:hover { filter: brightness(1.05); }
681
+ `;
682
+
565
683
  // src/mountHud.ts
566
684
  function showReplayModal(ui, info, buttonKey, onSelect) {
567
685
  const cur = info.currency ?? ui.balance.currency.get();
568
- const money2 = (n) => formatAmount(n, cur);
686
+ const money2 = (n) => formatAmountPrecise(n, cur);
569
687
  ui.showNotice(
570
688
  [
571
689
  { kind: "heading", id: "openui-replay-h", text: "openui.replay.title" },
@@ -680,17 +798,15 @@ function mountHud(app, spec = {}, opts = {}) {
680
798
  o.onConfirm();
681
799
  return;
682
800
  }
683
- const message = o.message ?? (o.name ? ui.t("openui.buyFeature.confirm", { name: ui.t(o.name), price: o.price ?? "" }) : "openui.buyFeature.message");
684
- ui.showNotice(
685
- [
686
- { kind: "heading", id: "buy-title", text: o.title ?? "openui.buyFeature.title" },
687
- { kind: "text", id: "buy-body", text: message }
688
- ],
689
- [
690
- { label: o.cancelLabel ?? "openui.cancel", variant: "secondary" },
691
- { label: o.confirmLabel ?? "openui.confirm", variant: "primary", onSelect: o.onConfirm }
692
- ]
693
- );
801
+ const message = o.message ?? (o.name ? ui.t("openui.buyFeature.confirm", { name: ui.t(o.name), price: o.price ?? "" }) : ui.t("openui.buyFeature.message"));
802
+ void showConfirm(ui, {
803
+ title: o.title ?? "openui.buyFeature.title",
804
+ message,
805
+ confirmLabel: o.confirmLabel,
806
+ cancelLabel: o.cancelLabel
807
+ }).then((ok) => {
808
+ if (ok) o.onConfirm();
809
+ });
694
810
  },
695
811
  showControls: () => pixi.setControlsVisible(true),
696
812
  hideControls: () => pixi.setControlsVisible(false),
@@ -705,13 +821,14 @@ function mountHud(app, spec = {}, opts = {}) {
705
821
  };
706
822
  }
707
823
  function money(amount, cur) {
708
- return formatAmount(amount, cur);
824
+ return formatAmountPrecise(amount, cur);
709
825
  }
710
- var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
826
+ var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
711
827
  function mountBuyFeatureModal(_app, hud, features, opts = {}) {
712
828
  const ui = hud.ui;
713
829
  const tr = (k) => ui.t(k);
714
830
  const list = features.slice(0, 4);
831
+ ui.declareFacts({ modes: list.map((f) => ({ id: f.id, name: f.name, kind: f.variant, cost: f.cost })) });
715
832
  const boosts = /* @__PURE__ */ new Set();
716
833
  const activation = opts.activation ?? "multi";
717
834
  const blocksBuy = opts.activationBlocksBuy ?? false;
@@ -733,23 +850,14 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
733
850
  <button class="bfm-x" data-close aria-label="Close">\u2715</button>
734
851
  <div class="bfm-panel" role="dialog" aria-modal="true">
735
852
  <div class="bfm-fit" id="bfm-fit">
736
- <h2 class="bfm-title" data-t="Buy Feature">${esc2(tr("Buy Feature"))}</h2>
853
+ <h2 class="bfm-title" data-t="Buy Feature">${esc3(tr("Buy Feature"))}</h2>
737
854
  <div class="bfm-bet">
738
855
  <button class="bfm-step" id="bfm-minus" aria-label="Decrease">\u2212</button>
739
- <div class="bfm-betbox"><span class="bfm-betlabel" data-t="Bet">${esc2(tr("Bet"))}</span><b id="bfm-betval">\u2014</b></div>
856
+ <div class="bfm-betbox"><span class="bfm-betlabel" data-t="Bet">${esc3(tr("Bet"))}</span><b id="bfm-betval">\u2014</b></div>
740
857
  <button class="bfm-step" id="bfm-plus" aria-label="Increase">+</button>
741
858
  </div>
742
859
  <div class="bfm-cards" id="bfm-cards"></div>
743
860
  </div>
744
- </div>
745
- <div class="bfm-confirm" id="bfm-confirm">
746
- <div class="bfm-confirm-card">
747
- <p class="bfm-confirm-msg" id="bfm-confirm-msg"></p>
748
- <div class="bfm-confirm-row">
749
- <button class="bfm-confirm-btn bfm-confirm-no" id="bfm-confirm-no" data-t="openui.cancel">${esc2(tr("openui.cancel"))}</button>
750
- <button class="bfm-confirm-btn bfm-confirm-yes" id="bfm-confirm-yes" data-t="openui.confirm">${esc2(tr("openui.confirm"))}</button>
751
- </div>
752
- </div>
753
861
  </div>`;
754
862
  const style = document.createElement("style");
755
863
  style.textContent = BFM_CSS;
@@ -760,29 +868,11 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
760
868
  const fitEl = $("#bfm-fit");
761
869
  const cardsEl = $("#bfm-cards");
762
870
  const betValEl = $("#bfm-betval");
763
- const confirmEl = $("#bfm-confirm");
764
- const confirmMsg = $("#bfm-confirm-msg");
765
- const confirmYes = $("#bfm-confirm-yes");
766
- const confirmNo = $("#bfm-confirm-no");
767
- let pendingConfirm = null;
768
- const hideConfirm = () => {
769
- confirmEl.classList.remove("show");
770
- pendingConfirm = null;
771
- };
772
- confirmNo.addEventListener("click", hideConfirm);
773
- confirmYes.addEventListener("click", () => {
774
- const fn = pendingConfirm;
775
- hideConfirm();
776
- fn?.();
777
- });
778
- const askConfirm = (totalCost, name, price, onYes) => {
779
- if (!hud.shouldConfirmBuy(totalCost)) {
780
- onYes();
781
- return;
782
- }
783
- confirmMsg.textContent = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
784
- pendingConfirm = onYes;
785
- confirmEl.classList.add("show");
871
+ const askConfirm = (name, price, onYes) => {
872
+ const message = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
873
+ void showConfirm(ui, { title: "Buy Feature", message }).then((ok) => {
874
+ if (ok) onYes();
875
+ });
786
876
  };
787
877
  const layout = () => {
788
878
  const vw = window.innerWidth;
@@ -814,11 +904,11 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
814
904
  <div class="bfm-cardimg"${img}></div>
815
905
  <div class="bfm-strip"></div>
816
906
  <div class="bfm-cardbody">
817
- <span class="bfm-name">${esc2(tr(f.name))}</span>
818
- <b class="bfm-price">${esc2(price)}</b>
907
+ <span class="bfm-name">${esc3(tr(f.name))}</span>
908
+ <b class="bfm-price">${esc3(price)}</b>
819
909
  </div>
820
910
  </div>
821
- <button class="${cls}" data-id="${f.id}" data-variant="${f.variant}"${buyBlocked ? " disabled" : ""}>${esc2(label)}</button>
911
+ <button class="${cls}" data-id="${f.id}" data-variant="${f.variant}"${buyBlocked ? " disabled" : ""}>${esc3(label)}</button>
822
912
  </div>`;
823
913
  }).join("");
824
914
  betValEl.textContent = money(bet, cur);
@@ -846,11 +936,11 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
846
936
  return;
847
937
  }
848
938
  const total = 1 + (f?.cost ?? 0);
849
- askConfirm(total, f?.name ?? id, money(total * bet, cur), commit);
939
+ askConfirm(f?.name ?? id, money(total * bet, cur), commit);
850
940
  } else {
851
941
  if (blocksBuy && boosts.size > 0) return;
852
942
  const cost = (f?.cost ?? 0) * bet;
853
- askConfirm(f?.cost ?? 0, f?.name ?? id, money(cost, cur), () => {
943
+ askConfirm(f?.name ?? id, money(cost, cur), () => {
854
944
  ui.bus.emit("cardActivated", { id });
855
945
  close();
856
946
  opts.onBuy?.(id, cost);
@@ -861,10 +951,16 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
861
951
  $("#bfm-plus").addEventListener("click", () => ui.betStepper.inc());
862
952
  disposers.push(ui.bet.value.subscribe(() => renderCards()));
863
953
  const open = () => {
954
+ if (host2.classList.contains("open")) return;
864
955
  renderCards();
865
956
  host2.classList.add("open");
957
+ ui.lock();
958
+ };
959
+ const close = () => {
960
+ if (!host2.classList.contains("open")) return;
961
+ host2.classList.remove("open");
962
+ ui.unlock();
866
963
  };
867
- const close = () => host2.classList.remove("open");
868
964
  host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", close));
869
965
  disposers.push(ui.on("buttonActivated", ({ id }) => {
870
966
  if (id === "bonus") open();
@@ -882,6 +978,7 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
882
978
  renderCards();
883
979
  return () => {
884
980
  window.removeEventListener("resize", onResize);
981
+ if (host2.classList.contains("open")) ui.unlock();
885
982
  for (const d of disposers.splice(0)) d();
886
983
  host2.remove();
887
984
  };
@@ -919,22 +1016,11 @@ var BFM_CSS = `
919
1016
  .bfm-action.is-active { background: var(--accent); color: var(--accent-text); border-color: #000; }
920
1017
  .bfm-action.is-blocked, .bfm-action:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
921
1018
  .bfm-action.is-blocked:hover, .bfm-action:disabled:hover { background: var(--surface); }
922
- .bfm-confirm { position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; opacity: 0; pointer-events: none; transition: opacity .16s ease; }
923
- .bfm-confirm.show { opacity: 1; pointer-events: auto; }
924
- .bfm-confirm::before { content: ""; position: absolute; inset: 0; background: rgba(8,6,4,.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
925
- .bfm-confirm-card { position: relative; width: min(90%, 420px); background: var(--surface); color: var(--text); border: 3px solid #000; border-radius: 14px; padding: 26px 24px 22px; box-shadow: 0 24px 60px rgba(0,0,0,.55); text-align: center; }
926
- .bfm-confirm-msg { margin: 0 0 22px; font-size: 19px; font-weight: 700; line-height: 1.4; color: var(--text); }
927
- .bfm-confirm-row { display: flex; gap: 14px; }
928
- .bfm-confirm-btn { flex: 1; padding: 14px 10px; border-radius: 12px; border: 3px solid #000; font-size: 15px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase; cursor: pointer; transition: transform .1s, background .12s; }
929
- .bfm-confirm-btn:active { transform: scale(.96); }
930
- .bfm-confirm-no { background: var(--surface); color: var(--text); }
931
- .bfm-confirm-no:hover { background: var(--surface-alt); }
932
- .bfm-confirm-yes { background: var(--accent); color: var(--accent-text); }
933
1019
  `;
934
1020
 
935
1021
  // src/bootError.ts
936
1022
  var host = null;
937
- var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1023
+ var esc4 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
938
1024
  function showBootError(opts = {}) {
939
1025
  const title = opts.title ?? "Connection lost";
940
1026
  const message = opts.message ?? "The game could not reach the game server. Please reload to reconnect and continue.";
@@ -948,10 +1034,10 @@ function showBootError(opts = {}) {
948
1034
  <div class="openui-be-icon" aria-hidden="true">
949
1035
  <svg viewBox="0 0 48 48" width="48" height="48"><path d="M24 4 3 42h42L24 4Z" fill="none" stroke="#ffc935" stroke-width="3" stroke-linejoin="round"/><rect x="22" y="18" width="4" height="12" rx="2" fill="#ffc935"/><circle cx="24" cy="35" r="2.4" fill="#ffc935"/></svg>
950
1036
  </div>
951
- <h1 class="openui-be-title">${esc3(title)}</h1>
952
- <p class="openui-be-msg">${esc3(message)}</p>
953
- ${opts.detail ? `<p class="openui-be-detail">${esc3(opts.detail)}</p>` : ""}
954
- ${showReload ? `<button class="openui-be-reload" type="button">${esc3(opts.reloadLabel ?? "Reload")}</button>` : ""}
1037
+ <h1 class="openui-be-title">${esc4(title)}</h1>
1038
+ <p class="openui-be-msg">${esc4(message)}</p>
1039
+ ${opts.detail ? `<p class="openui-be-detail">${esc4(opts.detail)}</p>` : ""}
1040
+ ${showReload ? `<button class="openui-be-reload" type="button">${esc4(opts.reloadLabel ?? "Reload")}</button>` : ""}
955
1041
  </div>
956
1042
  <style>
957
1043
  .openui-boot-error { position: fixed; inset: 0; z-index: 2147483000; display: grid; place-items: center;
@@ -979,6 +1065,6 @@ function hideBootError() {
979
1065
  host = null;
980
1066
  }
981
1067
 
982
- export { PanelBodyView, PanelView, PopoverView, hideBootError, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError };
1068
+ export { PanelBodyView, PanelView, PopoverView, hideBootError, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError, showConfirm };
983
1069
  //# sourceMappingURL=index.js.map
984
1070
  //# sourceMappingURL=index.js.map