@open-slot-ui/pixi 0.11.1 → 0.12.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
@@ -1,7 +1,7 @@
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
- import { Text, Graphics, Container, Rectangle } from 'pixi.js';
4
- import { INFO_MENU_VARS, escapeHtml, LOCALE_LABELS, renderBlocksHtml, INFO_MENU_CSS, createUI, composeMenu, buildPanel, formatAmountPrecise, factsVars, renderRulesAuditHtml, auditRules } from '@open-slot-ui/core';
1
+ import { ControlView, ButtonView, SliderView, buildBlockColumn, OpenUIPixi } from './chunk-FN6UJ3WO.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-FN6UJ3WO.js';
3
+ import { Text, Graphics, Container, Rectangle, Texture, Sprite, Assets } from 'pixi.js';
4
+ import { INFO_MENU_VARS, escapeHtml, LOCALE_LABELS, renderBlocksHtml, INFO_MENU_CSS, createUI, composeMenu, buildPanel, factsVars, renderRulesAuditHtml, auditRules, formatAmountPrecise } from '@open-slot-ui/core';
5
5
 
6
6
  var RISE = 26;
7
7
  var STAGGER = 38;
@@ -435,67 +435,10 @@ function mountInfoMenu(app, ui) {
435
435
  };
436
436
  }
437
437
 
438
- // src/confirmModal.ts
439
- var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
440
- function showConfirm(ui, opts) {
441
- return new Promise((resolve) => {
442
- const host2 = document.createElement("div");
443
- host2.className = "osc-confirm";
444
- host2.style.setProperty("--font", ui.theme.type.family);
445
- const title = ui.t(opts.title ?? "Buy Feature");
446
- const yes = ui.t(opts.confirmLabel ?? "openui.confirm");
447
- const no = ui.t(opts.cancelLabel ?? "openui.cancel");
448
- host2.innerHTML = `
449
- <div class="osc-confirm-backdrop" data-no></div>
450
- <div class="osc-confirm-card" role="dialog" aria-modal="true">
451
- ${title ? `<h3 class="osc-confirm-title">${esc2(title)}</h3>` : ""}
452
- <p class="osc-confirm-msg">${esc2(opts.message)}</p>
453
- <div class="osc-confirm-row">
454
- <button class="osc-confirm-btn osc-confirm-no" data-no>${esc2(no)}</button>
455
- <button class="osc-confirm-btn osc-confirm-yes" data-yes>${esc2(yes)}</button>
456
- </div>
457
- </div>`;
458
- const style = document.createElement("style");
459
- style.textContent = CONFIRM_CSS;
460
- host2.appendChild(style);
461
- document.body.appendChild(host2);
462
- let settled = false;
463
- const close = (val) => {
464
- if (settled) return;
465
- settled = true;
466
- window.removeEventListener("keydown", onKey);
467
- host2.remove();
468
- resolve(val);
469
- };
470
- const onKey = (e) => {
471
- if (e.key === "Escape") close(false);
472
- else if (e.key === "Enter") close(true);
473
- };
474
- host2.querySelectorAll("[data-no]").forEach((b) => b.addEventListener("click", () => close(false)));
475
- host2.querySelector("[data-yes]")?.addEventListener("click", () => close(true));
476
- window.addEventListener("keydown", onKey);
477
- });
478
- }
479
- var CONFIRM_CSS = `
480
- .osc-confirm { position: fixed; inset: 0; z-index: 12000; display: grid; place-items: center; font-family: var(--font); }
481
- .osc-confirm *, .osc-confirm *::before, .osc-confirm *::after { box-sizing: border-box; }
482
- .osc-confirm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
483
- .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; }
484
- .osc-confirm-title { margin: 0 0 12px; font-size: 22px; font-weight: 800; letter-spacing: .3px; }
485
- .osc-confirm-msg { margin: 0 0 22px; font-size: 19px; font-weight: 700; line-height: 1.4; }
486
- .osc-confirm-row { display: flex; gap: 14px; }
487
- .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; }
488
- .osc-confirm-btn:active { transform: scale(.96); }
489
- .osc-confirm-no { background: #ffffff; color: #181b20; }
490
- .osc-confirm-no:hover { background: #eef1f6; }
491
- .osc-confirm-yes { background: #d99000; color: #1a1200; }
492
- .osc-confirm-yes:hover { filter: brightness(1.05); }
493
- `;
494
-
495
438
  // src/mountHud.ts
496
439
  function showReplayModal(ui, info, buttonKey, onSelect) {
497
440
  const cur = info.currency ?? ui.balance.currency.get();
498
- const money2 = (n) => formatAmountPrecise(n, cur);
441
+ const money = (n) => formatAmountPrecise(n, cur);
499
442
  ui.showNotice(
500
443
  [
501
444
  { kind: "heading", id: "openui-replay-h", text: "openui.replay.title" },
@@ -503,10 +446,10 @@ function showReplayModal(ui, info, buttonKey, onSelect) {
503
446
  kind: "stat-grid",
504
447
  id: "openui-replay-g",
505
448
  items: [
506
- { label: "openui.replay.baseBet", value: money2(info.baseBet) },
449
+ { label: "openui.replay.baseBet", value: money(info.baseBet) },
507
450
  { label: "openui.replay.costMultiplier", value: `${info.costMultiplier}\xD7` },
508
451
  { label: "openui.replay.payoutMultiplier", value: `${info.payoutMultiplier}\xD7` },
509
- { label: "openui.replay.amount", value: money2(info.amount) }
452
+ { label: "openui.replay.amount", value: money(info.amount) }
510
453
  ]
511
454
  }
512
455
  ],
@@ -520,7 +463,7 @@ function showReplayModal(ui, info, buttonKey, onSelect) {
520
463
  function mountHud(app, spec = {}, opts = {}) {
521
464
  const { hooks, infoMenu, ...pixiOpts } = opts;
522
465
  const ui = createUI(spec, hooks);
523
- const useInfoMenu = infoMenu !== false && pixiOpts.menu !== false;
466
+ const useInfoMenu = infoMenu === true && pixiOpts.menu !== false;
524
467
  const locales = spec.locale ? Array.from(/* @__PURE__ */ new Set([spec.locale.locale, ...Object.keys(spec.locale.messages)])) : [];
525
468
  const menu = pixiOpts.menu === false || useInfoMenu ? false : composeMenu(spec.menu, { locales, localeSelectId: spec.localeSelectId, rulesFallback: spec.rules });
526
469
  if (menu && spec.game && (spec.game.name || spec.game.version)) {
@@ -529,6 +472,21 @@ function mountHud(app, spec = {}, opts = {}) {
529
472
  const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu });
530
473
  pixi.mount(app);
531
474
  const disposeInfoMenu = useInfoMenu ? mountInfoMenu(app, ui) : void 0;
475
+ const canvas = app.canvas;
476
+ const cancelToUp = (e) => {
477
+ canvas?.dispatchEvent(new PointerEvent("pointerup", {
478
+ pointerId: e.pointerId,
479
+ pointerType: e.pointerType,
480
+ isPrimary: e.isPrimary,
481
+ clientX: e.clientX,
482
+ clientY: e.clientY,
483
+ pressure: 0,
484
+ bubbles: true,
485
+ cancelable: true,
486
+ composed: true
487
+ }));
488
+ };
489
+ canvas?.addEventListener("pointercancel", cancelToUp, true);
532
490
  const extra = [];
533
491
  if (spec.panels?.length) {
534
492
  for (const ps of spec.panels) {
@@ -549,6 +507,7 @@ function mountHud(app, spec = {}, opts = {}) {
549
507
  });
550
508
  const teardown = () => {
551
509
  offRelayout();
510
+ canvas?.removeEventListener("pointercancel", cancelToUp, true);
552
511
  disposeInfoMenu?.();
553
512
  for (const v of extra) v.dispose();
554
513
  extra.length = 0;
@@ -611,14 +570,19 @@ function mountHud(app, spec = {}, opts = {}) {
611
570
  return;
612
571
  }
613
572
  const message = o.message ?? (o.name ? ui.t("openui.buyFeature.confirm", { name: ui.t(o.name), price: o.price ?? "" }) : ui.t("openui.buyFeature.message"));
614
- void showConfirm(ui, {
615
- title: o.title ?? "openui.buyFeature.title",
616
- message,
617
- confirmLabel: o.confirmLabel,
618
- cancelLabel: o.cancelLabel
619
- }).then((ok) => {
620
- if (ok) o.onConfirm();
621
- });
573
+ ui.showNotice(
574
+ [
575
+ { kind: "heading", id: "buy-confirm-title", text: o.title ?? "openui.buyFeature.title" },
576
+ { kind: "text", id: "buy-confirm-body", text: message }
577
+ ],
578
+ [
579
+ { label: o.cancelLabel ?? "openui.cancel", variant: "secondary" },
580
+ { label: o.confirmLabel ?? "openui.confirm", variant: "primary", onSelect: () => {
581
+ ui.hideNotice();
582
+ o.onConfirm();
583
+ } }
584
+ ]
585
+ );
622
586
  },
623
587
  showControls: () => pixi.setControlsVisible(true),
624
588
  hideControls: () => pixi.setControlsVisible(false),
@@ -632,208 +596,332 @@ function mountHud(app, spec = {}, opts = {}) {
632
596
  dispose: teardown
633
597
  };
634
598
  }
635
- function money(amount, cur) {
636
- return formatAmountPrecise(amount, cur);
599
+ var LIGHT = {
600
+ surface: "#ffffff",
601
+ surfaceAlt: "#eef1f6",
602
+ text: "#181b20",
603
+ textDim: "#5b6472",
604
+ border: "#000000",
605
+ accent: "#d99000",
606
+ accentText: "#1a1200",
607
+ scrim: "#080604"
608
+ };
609
+ var CARD_W = 200;
610
+ var IMG_H = 120;
611
+ var STRIP_H = 8;
612
+ var CARD_H = 214;
613
+ var ACT_GAP = 12;
614
+ var ACT_H = 50;
615
+ var CELL_H = CARD_H + ACT_GAP + ACT_H;
616
+ var GAP2 = 18;
617
+ var STEP_R = 27;
618
+ var BET_W = 220;
619
+ var BET_H = 60;
620
+ var TITLE_H = 40;
621
+ var ROW_GAP = 22;
622
+ var TAP_SLOP = 24;
623
+ var texCache = /* @__PURE__ */ new Map();
624
+ function loadTex(url) {
625
+ let p = texCache.get(url);
626
+ if (!p) {
627
+ p = Assets.load(url).catch(() => Texture.EMPTY);
628
+ texCache.set(url, p);
629
+ }
630
+ return p;
637
631
  }
638
- var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
639
- function mountBuyFeatureModal(_app, hud, features, opts = {}) {
640
- const ui = hud.ui;
641
- const tr = (k) => ui.t(k);
642
- const list = features.slice(0, 4);
643
- ui.declareFacts({ modes: list.map((f) => ({ id: f.id, name: f.name, kind: f.variant, cost: f.cost })) });
644
- const boosts = /* @__PURE__ */ new Set();
645
- const activation = opts.activation ?? "multi";
646
- const blocksBuy = opts.activationBlocksBuy ?? false;
647
- const disposers = [];
648
- const host2 = document.createElement("div");
649
- host2.className = "bfm-root";
650
- const vars = {
651
- "--accent": "#d99000",
652
- "--accent-text": "#1a1200",
653
- "--surface": "#ffffff",
654
- "--surface-alt": "#eef1f6",
655
- "--text": "#181b20",
656
- "--text-dim": "#5b6472",
657
- "--font": ui.theme.type.family
658
- };
659
- for (const [k, v] of Object.entries(vars)) host2.style.setProperty(k, v);
660
- host2.innerHTML = `
661
- <div class="bfm-backdrop" data-close></div>
662
- <button class="bfm-x" data-close aria-label="Close">\u2715</button>
663
- <div class="bfm-panel" role="dialog" aria-modal="true">
664
- <div class="bfm-fit" id="bfm-fit">
665
- <h2 class="bfm-title" data-t="Buy Feature">${esc3(tr("Buy Feature"))}</h2>
666
- <div class="bfm-bet">
667
- <button class="bfm-step" id="bfm-minus" aria-label="Decrease">\u2212</button>
668
- <div class="bfm-betbox"><span class="bfm-betlabel" data-t="Bet">${esc3(tr("Bet"))}</span><b id="bfm-betval">\u2014</b></div>
669
- <button class="bfm-step" id="bfm-plus" aria-label="Increase">+</button>
670
- </div>
671
- <div class="bfm-cards" id="bfm-cards"></div>
672
- </div>
673
- </div>`;
674
- const style = document.createElement("style");
675
- style.textContent = BFM_CSS;
676
- host2.appendChild(style);
677
- document.body.appendChild(host2);
678
- const $ = (sel) => host2.querySelector(sel);
679
- const panel = $(".bfm-panel");
680
- const fitEl = $("#bfm-fit");
681
- const cardsEl = $("#bfm-cards");
682
- const betValEl = $("#bfm-betval");
683
- const askConfirm = (name, price, onYes) => {
684
- const message = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
685
- void showConfirm(ui, { title: "Buy Feature", message }).then((ok) => {
686
- if (ok) onYes();
687
- });
688
- };
689
- const layout = () => {
690
- const vw = window.innerWidth;
691
- const vh = window.innerHeight;
692
- const cols = vw >= 720 && (vw >= 900 || vh <= 540) ? Math.min(4, list.length) : Math.min(2, list.length);
693
- cardsEl.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
694
- const fitW = Math.min(vw * 0.96, cols >= 4 ? 1180 : cols === 1 ? 380 : 760);
695
- fitEl.style.width = `${fitW}px`;
696
- fitEl.style.transform = "none";
697
- const natH = fitEl.offsetHeight;
698
- const s = Math.min(1, vh * 0.95 / natH);
699
- fitEl.style.transform = `translateX(-50%) scale(${s})`;
700
- panel.style.width = `${Math.ceil(fitW * s)}px`;
701
- panel.style.height = `${Math.ceil(natH * s)}px`;
632
+ function wireTap(node, hit, onTap) {
633
+ node.eventMode = "static";
634
+ node.cursor = "pointer";
635
+ node.hitArea = hit;
636
+ let down = null;
637
+ node.on("pointerdown", (e) => {
638
+ down = { x: e.global.x, y: e.global.y };
639
+ node.alpha = 0.82;
640
+ });
641
+ const end = (e, inside) => {
642
+ node.alpha = 1;
643
+ const p = down;
644
+ down = null;
645
+ if (!p) return;
646
+ if (inside || Math.hypot(e.global.x - p.x, e.global.y - p.y) <= TAP_SLOP) onTap();
702
647
  };
703
- const renderCards = () => {
704
- const bet = opts.getBet ? opts.getBet() : ui.bet.get();
648
+ node.on("pointerup", (e) => end(e, true));
649
+ node.on("pointerupoutside", (e) => end(e, false));
650
+ }
651
+ var FeatureCard = class extends Container {
652
+ priceText;
653
+ actionBg = new Graphics();
654
+ actionLabel;
655
+ action = new Container();
656
+ constructor(spec, ui, onAction) {
657
+ super();
658
+ const fam = ui.theme.type.family;
659
+ const fill = new Graphics().roundRect(0, 0, CARD_W, CARD_H, 14).fill({ color: LIGHT.surface });
660
+ const clip = new Container();
661
+ const clipMask = new Graphics().roundRect(0, 0, CARD_W, CARD_H, 14).fill({ color: 16777215 });
662
+ clip.mask = clipMask;
663
+ const placeholder = new Graphics().rect(0, 0, CARD_W, IMG_H).fill({ color: LIGHT.surfaceAlt });
664
+ clip.addChild(placeholder);
665
+ if (spec.image) {
666
+ void loadTex(spec.image).then((tex) => {
667
+ if (this.destroyed || tex === Texture.EMPTY) return;
668
+ const sp = new Sprite(tex);
669
+ const s = Math.max(CARD_W / tex.width, IMG_H / tex.height);
670
+ sp.scale.set(s);
671
+ sp.x = (CARD_W - tex.width * s) / 2;
672
+ sp.y = (IMG_H - tex.height * s) / 2;
673
+ clip.addChildAt(sp, 1);
674
+ });
675
+ }
676
+ const strip = new Graphics().rect(0, IMG_H, CARD_W, STRIP_H).fill({ color: LIGHT.accent });
677
+ const name = new Text({ text: ui.t(spec.name), style: { fontFamily: fam, fontSize: 16, fontWeight: "600", fill: LIGHT.text } });
678
+ name.anchor.set(0.5, 0);
679
+ name.position.set(CARD_W / 2, IMG_H + STRIP_H + 12);
680
+ this.priceText = new Text({ text: "", style: { fontFamily: fam, fontSize: 22, fontWeight: "800", fill: LIGHT.text } });
681
+ this.priceText.anchor.set(0.5, 0);
682
+ this.priceText.position.set(CARD_W / 2, IMG_H + STRIP_H + 36);
683
+ clip.addChild(strip, name, this.priceText);
684
+ const stroke = new Graphics().roundRect(0, 0, CARD_W, CARD_H, 14).stroke({ width: 4, color: LIGHT.border });
685
+ this.addChild(fill, clip, clipMask, stroke);
686
+ this.actionLabel = new Text({ text: "", style: { fontFamily: fam, fontSize: 15, fontWeight: "800", fill: LIGHT.text, letterSpacing: 0.5 } });
687
+ this.actionLabel.anchor.set(0.5);
688
+ this.actionLabel.position.set(CARD_W / 2, CARD_H + ACT_GAP + ACT_H / 2);
689
+ this.action.addChild(this.actionBg, this.actionLabel);
690
+ wireTap(this.action, new Rectangle(0, CARD_H + ACT_GAP, CARD_W, ACT_H), onAction);
691
+ this.addChild(this.action);
692
+ }
693
+ /** Update price + action label/state for the current bet + boost state. */
694
+ update(price, label, active, blocked) {
695
+ this.priceText.text = price;
696
+ this.actionLabel.text = label;
697
+ this.actionLabel.style.fill = active ? LIGHT.accentText : LIGHT.text;
698
+ this.actionBg.clear().roundRect(0, CARD_H + ACT_GAP, CARD_W, ACT_H, 12).fill({ color: active ? LIGHT.accent : LIGHT.surface }).stroke({ width: 4, color: LIGHT.border });
699
+ this.action.eventMode = blocked ? "none" : "static";
700
+ this.action.alpha = blocked ? 0.38 : 1;
701
+ }
702
+ };
703
+ var BuyFeatureModalView = class extends Container {
704
+ constructor(hud, features, opts) {
705
+ super();
706
+ this.hud = hud;
707
+ this.opts = opts;
708
+ const ui = hud.ui;
709
+ const fam = ui.theme.type.family;
710
+ this.list = features.slice(0, 4);
711
+ this.zIndex = 125;
712
+ this.visible = false;
713
+ this.eventMode = "none";
714
+ ui.declareFacts({ modes: this.list.map((f) => ({ id: f.id, name: f.name, kind: f.variant, cost: f.cost })) });
715
+ this.backdrop.eventMode = "static";
716
+ this.backdrop.on("pointertap", () => this.close());
717
+ this.title = new Text({ text: ui.t("Buy Feature"), style: { fontFamily: fam, fontSize: 30, fontWeight: "800", fill: "#ffffff", letterSpacing: 1 } });
718
+ this.title.anchor.set(0.5, 0);
719
+ const minus = this.stepButton("\u2212", () => ui.betStepper.dec());
720
+ const plus = this.stepButton("+", () => ui.betStepper.inc());
721
+ const betBox = new Container();
722
+ const betBg = new Graphics().roundRect(-BET_W / 2, -BET_H / 2, BET_W, BET_H, 12).fill({ color: LIGHT.surface }).stroke({ width: 3, color: LIGHT.border });
723
+ const betLabel = new Text({ text: ui.t("Bet"), style: { fontFamily: fam, fontSize: 12, fontWeight: "700", fill: LIGHT.textDim, letterSpacing: 1 } });
724
+ betLabel.anchor.set(0.5, 0);
725
+ betLabel.position.set(0, -BET_H / 2 + 8);
726
+ this.betValue = new Text({ text: "", style: { fontFamily: fam, fontSize: 24, fontWeight: "800", fill: LIGHT.text } });
727
+ this.betValue.anchor.set(0.5, 1);
728
+ this.betValue.position.set(0, BET_H / 2 - 8);
729
+ betBox.addChild(betBg, betLabel, this.betValue);
730
+ const betRow = new Container();
731
+ minus.position.set(STEP_R, 0);
732
+ betBox.position.set(STEP_R * 2 + 16 + BET_W / 2, 0);
733
+ plus.position.set(STEP_R * 2 + 16 + BET_W + 16 + STEP_R, 0);
734
+ betRow.addChild(minus, betBox, plus);
735
+ this.betRow = betRow;
736
+ for (const spec of this.list) {
737
+ const card = new FeatureCard(spec, ui, () => this.onAction(spec.id, spec.variant));
738
+ this.cards.push(card);
739
+ this.cardsRow.addChild(card);
740
+ }
741
+ this.content.addChild(this.title, betRow, this.cardsRow);
742
+ const cbg = new Graphics().circle(0, 0, 23).fill({ color: LIGHT.scrim, alpha: 0.85 });
743
+ const cx = new Graphics().moveTo(-7, -7).lineTo(7, 7).moveTo(7, -7).lineTo(-7, 7).stroke({ width: 3, color: "#ffffff", cap: "round" });
744
+ this.closeBtn.addChild(cbg, cx);
745
+ wireTap(this.closeBtn, new Rectangle(-23, -23, 46, 46), () => this.close());
746
+ this.addChild(this.backdrop, this.content, this.closeBtn);
747
+ this.disposers.push(
748
+ ui.bet.value.subscribe(() => this.refresh()),
749
+ ui.locale.subscribe(() => {
750
+ this.title.text = ui.t("Buy Feature");
751
+ betLabel.text = ui.t("Bet");
752
+ this.refresh();
753
+ this.relayout();
754
+ })
755
+ );
756
+ }
757
+ hud;
758
+ opts;
759
+ backdrop = new Graphics();
760
+ content = new Container();
761
+ closeBtn = new Container();
762
+ title;
763
+ betValue;
764
+ cardsRow = new Container();
765
+ cards = [];
766
+ list;
767
+ boosts = /* @__PURE__ */ new Set();
768
+ disposers = [];
769
+ screen;
770
+ betRow;
771
+ stepButton(glyph, onTap) {
772
+ const c = new Container();
773
+ const fam = this.hud.ui.theme.type.family;
774
+ const bg = new Graphics().circle(0, 0, STEP_R).fill({ color: LIGHT.surface }).stroke({ width: 3, color: LIGHT.border });
775
+ const t = new Text({ text: glyph, style: { fontFamily: fam, fontSize: 28, fontWeight: "800", fill: LIGHT.text } });
776
+ t.anchor.set(0.5);
777
+ t.position.set(0, -1);
778
+ c.addChild(bg, t);
779
+ wireTap(c, new Rectangle(-STEP_R, -STEP_R, STEP_R * 2, STEP_R * 2), onTap);
780
+ return c;
781
+ }
782
+ /** Price + action state per current bet + boosts. */
783
+ refresh() {
784
+ const ui = this.hud.ui;
785
+ const bet = this.opts.getBet ? this.opts.getBet() : ui.bet.get();
705
786
  const cur = ui.bet.currency.get();
706
- cardsEl.innerHTML = list.map((f) => {
707
- const active = boosts.has(f.id);
708
- const buyBlocked = f.variant === "buy" && blocksBuy && boosts.size > 0;
787
+ const money = (n) => formatAmountPrecise(n, cur);
788
+ this.betValue.text = money(bet);
789
+ const blocksBuy = this.opts.activationBlocksBuy ?? false;
790
+ this.list.forEach((f, i) => {
791
+ const active = this.boosts.has(f.id);
709
792
  const perSpin = (f.variant === "buy" ? f.cost : 1 + f.cost) * bet;
710
- const price = money(perSpin, cur);
711
- const label = f.variant === "buy" ? tr("Buy") : active ? tr("Activated") : tr("Activate");
712
- const cls = `bfm-action bfm-action--${f.variant}${active ? " is-active" : ""}${buyBlocked ? " is-blocked" : ""}`;
713
- const img = f.image ? ` style="background-image:url('${f.image}')"` : "";
714
- return `
715
- <div class="bfm-cell">
716
- <div class="bfm-card">
717
- <div class="bfm-cardimg"${img}></div>
718
- <div class="bfm-strip"></div>
719
- <div class="bfm-cardbody">
720
- <span class="bfm-name">${esc3(tr(f.name))}</span>
721
- <b class="bfm-price">${esc3(price)}</b>
722
- </div>
723
- </div>
724
- <button class="${cls}" data-id="${f.id}" data-variant="${f.variant}"${buyBlocked ? " disabled" : ""}>${esc3(label)}</button>
725
- </div>`;
726
- }).join("");
727
- betValEl.textContent = money(bet, cur);
728
- cardsEl.querySelectorAll(".bfm-action").forEach((b) => {
729
- b.addEventListener("click", () => onAction(b.dataset.id, b.dataset.variant));
793
+ const label = f.variant === "buy" ? ui.t("Buy") : active ? ui.t("Activated") : ui.t("Activate");
794
+ const blocked = f.variant === "buy" && blocksBuy && this.boosts.size > 0;
795
+ this.cards[i].update(money(perSpin), label, active, blocked);
730
796
  });
731
- layout();
732
- };
733
- const onAction = (id, variant) => {
734
- const f = list.find((x) => x.id === id);
735
- const bet = opts.getBet ? opts.getBet() : ui.bet.get();
797
+ }
798
+ onAction(id, variant) {
799
+ const ui = this.hud.ui;
800
+ const f = this.list.find((x) => x.id === id);
801
+ const bet = this.opts.getBet ? this.opts.getBet() : ui.bet.get();
736
802
  const cur = ui.bet.currency.get();
803
+ const money = (n) => formatAmountPrecise(n, cur);
804
+ const activation = this.opts.activation ?? "multi";
805
+ const blocksBuy = this.opts.activationBlocksBuy ?? false;
737
806
  if (variant === "boost") {
738
- const wasActive = boosts.has(id);
807
+ const wasActive = this.boosts.has(id);
739
808
  const commit = () => {
740
- if (activation === "single") boosts.clear();
741
- if (wasActive) boosts.delete(id);
742
- else boosts.add(id);
809
+ if (activation === "single") this.boosts.clear();
810
+ if (wasActive) this.boosts.delete(id);
811
+ else this.boosts.add(id);
743
812
  ui.bus.emit("cardActivated", { id });
744
- renderCards();
745
- opts.onActivate?.([...boosts], id, boosts.has(id));
813
+ this.refresh();
814
+ this.opts.onActivate?.([...this.boosts], id, this.boosts.has(id));
746
815
  };
747
816
  if (wasActive) {
748
817
  commit();
749
818
  return;
750
819
  }
751
820
  const total = 1 + (f?.cost ?? 0);
752
- askConfirm(f?.name ?? id, money(total * bet, cur), commit);
821
+ this.askConfirm(f?.name ?? id, money(total * bet), commit);
753
822
  } else {
754
- if (blocksBuy && boosts.size > 0) return;
823
+ if (blocksBuy && this.boosts.size > 0) return;
755
824
  const cost = (f?.cost ?? 0) * bet;
756
- askConfirm(f?.name ?? id, money(cost, cur), () => {
825
+ this.askConfirm(f?.name ?? id, money(cost), () => {
757
826
  ui.bus.emit("cardActivated", { id });
758
- close();
759
- opts.onBuy?.(id, cost);
827
+ this.close();
828
+ this.opts.onBuy?.(id, cost);
760
829
  });
761
830
  }
762
- };
763
- $("#bfm-minus").addEventListener("click", () => ui.betStepper.dec());
764
- $("#bfm-plus").addEventListener("click", () => ui.betStepper.inc());
765
- disposers.push(ui.bet.value.subscribe(() => renderCards()));
766
- const open = () => {
767
- if (host2.classList.contains("open")) return;
768
- renderCards();
769
- host2.classList.add("open");
770
- ui.lock();
771
- };
772
- const close = () => {
773
- if (!host2.classList.contains("open")) return;
774
- host2.classList.remove("open");
775
- ui.unlock();
776
- };
777
- host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", close));
778
- disposers.push(ui.on("buttonActivated", ({ id }) => {
779
- if (id === "bonus") open();
780
- }));
781
- const onResize = () => {
782
- if (host2.classList.contains("open")) layout();
783
- };
784
- window.addEventListener("resize", onResize);
785
- disposers.push(
786
- ui.locale.subscribe(() => {
787
- host2.querySelectorAll("[data-t]").forEach((n) => n.textContent = tr(n.dataset.t));
788
- renderCards();
789
- })
790
- );
791
- renderCards();
831
+ }
832
+ /** The ONE universal confirm — the in-canvas Pixi notice (DialogView, zIndex 130), so it
833
+ * layers over this modal. Message + labels come from the library's i18n (social-aware). */
834
+ askConfirm(name, price, onYes) {
835
+ const ui = this.hud.ui;
836
+ const message = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
837
+ ui.showNotice(
838
+ [
839
+ { kind: "heading", id: "bfm-confirm-h", text: "Buy Feature" },
840
+ { kind: "text", id: "bfm-confirm-b", text: message }
841
+ ],
842
+ [
843
+ { label: "openui.cancel", variant: "secondary" },
844
+ { label: "openui.confirm", variant: "primary", onSelect: () => {
845
+ ui.hideNotice();
846
+ onYes();
847
+ } }
848
+ ]
849
+ );
850
+ }
851
+ layout(screen) {
852
+ this.screen = screen;
853
+ if (this.visible) this.relayout();
854
+ }
855
+ relayout() {
856
+ const s = this.screen;
857
+ if (!s) return;
858
+ const W = s.width;
859
+ const H = s.height;
860
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: LIGHT.scrim, alpha: 0.55 });
861
+ this.backdrop.hitArea = new Rectangle(0, 0, W, H);
862
+ const n = this.list.length;
863
+ const cols = W >= 720 && (W >= 900 || H <= 540) ? Math.min(4, n) : Math.min(2, n);
864
+ const rows = Math.ceil(n / cols);
865
+ this.cards.forEach((card, i) => {
866
+ const r = Math.floor(i / cols);
867
+ const c = i % cols;
868
+ card.position.set(c * (CARD_W + GAP2), r * (CELL_H + GAP2));
869
+ });
870
+ const gridW = cols * CARD_W + (cols - 1) * GAP2;
871
+ const gridH = rows * CELL_H + (rows - 1) * GAP2;
872
+ const betW = 4 * STEP_R + BET_W + 32;
873
+ const contentW = Math.max(gridW, betW, 340);
874
+ this.title.position.set(contentW / 2, 0);
875
+ this.betRow.position.set((contentW - betW) / 2, TITLE_H + ROW_GAP + STEP_R);
876
+ this.cardsRow.position.set((contentW - gridW) / 2, TITLE_H + ROW_GAP + BET_H + ROW_GAP);
877
+ const contentH = TITLE_H + ROW_GAP + BET_H + ROW_GAP + gridH;
878
+ const scale = Math.min(1, W * 0.94 / contentW, H * 0.92 / contentH);
879
+ this.content.scale.set(scale);
880
+ this.content.position.set((W - contentW * scale) / 2, (H - contentH * scale) / 2);
881
+ this.closeBtn.position.set(W - 34, 34);
882
+ }
883
+ open() {
884
+ if (this.visible) return;
885
+ this.hud.ui.lock();
886
+ this.visible = true;
887
+ this.eventMode = "static";
888
+ this.refresh();
889
+ this.relayout();
890
+ }
891
+ close() {
892
+ if (!this.visible) return;
893
+ this.visible = false;
894
+ this.eventMode = "none";
895
+ this.hud.ui.unlock();
896
+ }
897
+ get isOpen() {
898
+ return this.visible;
899
+ }
900
+ dispose() {
901
+ if (this.visible) this.hud.ui.unlock();
902
+ for (const d of this.disposers.splice(0)) d();
903
+ if (!this.destroyed) this.destroy({ children: true });
904
+ }
905
+ };
906
+ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
907
+ const ui = hud.ui;
908
+ const view = new BuyFeatureModalView(hud, features, opts);
909
+ hud.pixi.root.addChild(view);
910
+ view.layout(ui.screen.get());
911
+ const offScreen = ui.screen.subscribe(() => view.layout(ui.screen.get()));
912
+ const offOpen = ui.on("buttonActivated", ({ id }) => {
913
+ if (id === "bonus") view.open();
914
+ });
792
915
  return () => {
793
- window.removeEventListener("resize", onResize);
794
- if (host2.classList.contains("open")) ui.unlock();
795
- for (const d of disposers.splice(0)) d();
796
- host2.remove();
916
+ offScreen();
917
+ offOpen();
918
+ view.dispose();
797
919
  };
798
920
  }
799
- var BFM_CSS = `
800
- .bfm-root { position: fixed; inset: 0; z-index: 11000; display: grid; place-items: center; font-family: var(--font); opacity: 0; pointer-events: none; transition: opacity .18s ease; }
801
- .bfm-root.open { opacity: 1; pointer-events: auto; }
802
- .bfm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,0); backdrop-filter: blur(0px) saturate(1); -webkit-backdrop-filter: blur(0px) saturate(1); transition: background .4s ease, backdrop-filter .4s ease, -webkit-backdrop-filter .4s ease; }
803
- .bfm-root.open .bfm-backdrop { background: rgba(8,6,4,.5); backdrop-filter: blur(10px) saturate(1.1); -webkit-backdrop-filter: blur(10px) saturate(1.1); }
804
- .bfm-x { position: absolute; top: 18px; right: 22px; width: 46px; height: 46px; border-radius: 999px; border: 0; background: rgba(18,14,10,.82); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0,0,0,.45); z-index: 2; transition: transform .12s, background .12s; }
805
- .bfm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }
806
- .bfm-root *, .bfm-root *::before, .bfm-root *::after { box-sizing: border-box; }
807
- .bfm-panel { position: relative; transform: translateY(8px) scale(.985); transition: transform .18s ease; }
808
- .bfm-root.open .bfm-panel { transform: none; }
809
- .bfm-fit { position: absolute; top: 0; left: 50%; transform: translateX(-50%); transform-origin: top center; }
810
- .bfm-title { margin: 0 0 14px; text-align: center; color: #fff; font-size: 30px; font-weight: 800; letter-spacing: 1px; text-shadow: 0 2px 12px rgba(0,0,0,.6); }
811
- .bfm-bet { display: flex; align-items: center; justify-content: center; gap: 16px; margin: 0 0 24px; }
812
- .bfm-betbox { min-width: 200px; padding: 10px 22px; border-radius: 12px; background: var(--surface); border: 3px solid #000; display: flex; flex-direction: column; align-items: center; line-height: 1.1; }
813
- .bfm-betlabel { font-size: 12px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
814
- .bfm-betbox b { font-size: 24px; color: var(--text); }
815
- .bfm-step { flex: none; width: 54px; height: 54px; border-radius: 999px; border: 3px solid #000; background: var(--surface); color: var(--text); font-size: 28px; font-weight: 800; cursor: pointer; display: grid; place-items: center; line-height: 1; transition: transform .1s, background .12s; box-shadow: 0 4px 12px rgba(0,0,0,.3); }
816
- .bfm-step:hover { background: var(--surface-alt); }
817
- .bfm-step:active { transform: scale(.92); }
818
- .bfm-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; align-items: start; }
819
- .bfm-cell { display: flex; flex-direction: column; min-width: 0; }
820
- .bfm-card { background: var(--surface); border: 4px solid #000; border-radius: 14px; overflow: hidden; box-shadow: 0 14px 34px rgba(0,0,0,.45); }
821
- .bfm-cardimg { width: 100%; aspect-ratio: 16 / 10; background-size: cover; background-position: center; background-color: var(--surface-alt); background-image: linear-gradient(135deg, #e7ebf2, #cfd6e2); }
822
- .bfm-strip { height: 8px; background: linear-gradient(90deg, #f0a500, #ffd166, #f0a500); }
823
- .bfm-cardbody { padding: 12px 14px 16px; text-align: center; }
824
- .bfm-name { display: block; font-size: 15px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
825
- .bfm-price { display: block; margin-top: 2px; font-size: 22px; font-weight: 800; color: var(--text); }
826
- .bfm-action { display: block; width: 100%; margin-top: 12px; padding: 14px 10px; border-radius: 12px; border: 4px solid #000; background: var(--surface); color: var(--text); font-size: 15px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase; cursor: pointer; transition: transform .1s, background .12s, color .12s; box-shadow: 0 5px 14px rgba(0,0,0,.35); white-space: nowrap; }
827
- .bfm-action:hover { background: var(--surface-alt); }
828
- .bfm-action:active { transform: scale(.96); }
829
- .bfm-action.is-active { background: var(--accent); color: var(--accent-text); border-color: #000; }
830
- .bfm-action.is-blocked, .bfm-action:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
831
- .bfm-action.is-blocked:hover, .bfm-action:disabled:hover { background: var(--surface); }
832
- `;
833
921
 
834
922
  // src/bootError.ts
835
923
  var host = null;
836
- var esc4 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
924
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
837
925
  function showBootError(opts = {}) {
838
926
  const title = opts.title ?? "Connection lost";
839
927
  const message = opts.message ?? "The game could not reach the game server. Please reload to reconnect and continue.";
@@ -847,10 +935,10 @@ function showBootError(opts = {}) {
847
935
  <div class="openui-be-icon" aria-hidden="true">
848
936
  <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>
849
937
  </div>
850
- <h1 class="openui-be-title">${esc4(title)}</h1>
851
- <p class="openui-be-msg">${esc4(message)}</p>
852
- ${opts.detail ? `<p class="openui-be-detail">${esc4(opts.detail)}</p>` : ""}
853
- ${showReload ? `<button class="openui-be-reload" type="button">${esc4(opts.reloadLabel ?? "Reload")}</button>` : ""}
938
+ <h1 class="openui-be-title">${esc2(title)}</h1>
939
+ <p class="openui-be-msg">${esc2(message)}</p>
940
+ ${opts.detail ? `<p class="openui-be-detail">${esc2(opts.detail)}</p>` : ""}
941
+ ${showReload ? `<button class="openui-be-reload" type="button">${esc2(opts.reloadLabel ?? "Reload")}</button>` : ""}
854
942
  </div>
855
943
  <style>
856
944
  .openui-boot-error { position: fixed; inset: 0; z-index: 2147483000; display: grid; place-items: center;
@@ -878,6 +966,6 @@ function hideBootError() {
878
966
  host = null;
879
967
  }
880
968
 
881
- export { PanelBodyView, PanelView, PopoverView, hideBootError, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError, showConfirm };
969
+ export { PanelBodyView, PanelView, PopoverView, hideBootError, mountBuyFeatureModal, mountHud, mountInfoMenu, showBootError };
882
970
  //# sourceMappingURL=index.js.map
883
971
  //# sourceMappingURL=index.js.map