@open-slot-ui/pixi 0.11.0 → 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.cjs CHANGED
@@ -1336,6 +1336,7 @@ var ValueDisplayView = class extends ControlView {
1336
1336
  super.dispose();
1337
1337
  }
1338
1338
  };
1339
+ var TAP_SLOP = 24;
1339
1340
  var GLYPH_SVG = {
1340
1341
  speaker: '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M13.6745 5.01252C13.9316 4.84399 14.2647 4.8292 14.5397 4.97456C14.8146 5.1199 14.9868 5.40142 14.9872 5.706L15.0007 16.8107C15.0011 17.1153 14.8296 17.3934 14.555 17.5333C14.2804 17.6731 13.9472 17.6517 13.6897 17.4781L10.4454 15.2905L8.61605 15.2722C6.76074 15.2537 5.25495 13.7655 5.25266 11.9484L5.25073 10.3739C5.2486 8.55679 6.75081 7.09863 8.6061 7.11714L10.4355 7.13539L13.6745 5.01252Z" fill="none" stroke="black" stroke-width="1.5" stroke-linejoin="round"/><path d="M16.1933 9.375C16.6252 10.4652 16.6015 11.654 16.1257 12.75" fill="none" stroke="black" stroke-width="3" stroke-linecap="round"/></svg>',
1341
1342
  "speaker-mute": '<svg viewBox="0 0 22.5 22.5" xmlns="http://www.w3.org/2000/svg"><path d="M13.6745 5.01252C13.9316 4.84399 14.2647 4.8292 14.5397 4.97456C14.8146 5.1199 14.9868 5.40142 14.9872 5.706L15.0007 16.8107C15.0011 17.1153 14.8296 17.3934 14.555 17.5333C14.2804 17.6731 13.9472 17.6517 13.6897 17.4781L10.4454 15.2905L8.61605 15.2722C6.76074 15.2537 5.25495 13.7655 5.25266 11.9484L5.25073 10.3739C5.2486 8.55679 6.75081 7.09863 8.6061 7.11714L10.4355 7.13539L13.6745 5.01252Z" fill="none" stroke="black" stroke-width="1.5" stroke-linejoin="round"/><path d="M15.5 8.5L19.5 13.5M19.5 8.5L15.5 13.5" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"/></svg>',
@@ -1433,17 +1434,26 @@ var ButtonView = class extends ControlView {
1433
1434
  this.sprite.scale.set(1);
1434
1435
  this.sprite.scale.set(this.iconTarget / this.sprite.height);
1435
1436
  }
1436
- onDown = () => {
1437
- if (this.btn.interactable) this.btn.setState("pressed");
1437
+ /** Screen position of the current press, for the tap-slop check on release. */
1438
+ downPt = null;
1439
+ onDown = (e) => {
1440
+ if (!this.btn.interactable) return;
1441
+ this.btn.setState("pressed");
1442
+ this.downPt = { x: e.global.x, y: e.global.y };
1438
1443
  };
1439
1444
  onUp = () => {
1440
1445
  if (this.btn.current === "pressed") {
1441
1446
  this.btn.setState("idle");
1442
1447
  this.btn.activate();
1443
1448
  }
1449
+ this.downPt = null;
1444
1450
  };
1445
- onUpOutside = () => {
1446
- if (this.btn.current === "pressed") this.btn.setState("idle");
1451
+ onUpOutside = (e) => {
1452
+ const p = this.downPt;
1453
+ this.downPt = null;
1454
+ if (this.btn.current !== "pressed") return;
1455
+ this.btn.setState("idle");
1456
+ if (p && Math.hypot(e.global.x - p.x, e.global.y - p.y) <= TAP_SLOP) this.btn.activate();
1447
1457
  };
1448
1458
  updateHit() {
1449
1459
  if (this.sprite) {
@@ -4145,67 +4155,10 @@ function mountInfoMenu(app, ui) {
4145
4155
  };
4146
4156
  }
4147
4157
 
4148
- // src/confirmModal.ts
4149
- var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4150
- function showConfirm(ui, opts) {
4151
- return new Promise((resolve) => {
4152
- const host2 = document.createElement("div");
4153
- host2.className = "osc-confirm";
4154
- host2.style.setProperty("--font", ui.theme.type.family);
4155
- const title = ui.t(opts.title ?? "Buy Feature");
4156
- const yes = ui.t(opts.confirmLabel ?? "openui.confirm");
4157
- const no = ui.t(opts.cancelLabel ?? "openui.cancel");
4158
- host2.innerHTML = `
4159
- <div class="osc-confirm-backdrop" data-no></div>
4160
- <div class="osc-confirm-card" role="dialog" aria-modal="true">
4161
- ${title ? `<h3 class="osc-confirm-title">${esc2(title)}</h3>` : ""}
4162
- <p class="osc-confirm-msg">${esc2(opts.message)}</p>
4163
- <div class="osc-confirm-row">
4164
- <button class="osc-confirm-btn osc-confirm-no" data-no>${esc2(no)}</button>
4165
- <button class="osc-confirm-btn osc-confirm-yes" data-yes>${esc2(yes)}</button>
4166
- </div>
4167
- </div>`;
4168
- const style = document.createElement("style");
4169
- style.textContent = CONFIRM_CSS;
4170
- host2.appendChild(style);
4171
- document.body.appendChild(host2);
4172
- let settled = false;
4173
- const close = (val) => {
4174
- if (settled) return;
4175
- settled = true;
4176
- window.removeEventListener("keydown", onKey);
4177
- host2.remove();
4178
- resolve(val);
4179
- };
4180
- const onKey = (e) => {
4181
- if (e.key === "Escape") close(false);
4182
- else if (e.key === "Enter") close(true);
4183
- };
4184
- host2.querySelectorAll("[data-no]").forEach((b) => b.addEventListener("click", () => close(false)));
4185
- host2.querySelector("[data-yes]")?.addEventListener("click", () => close(true));
4186
- window.addEventListener("keydown", onKey);
4187
- });
4188
- }
4189
- var CONFIRM_CSS = `
4190
- .osc-confirm { position: fixed; inset: 0; z-index: 12000; display: grid; place-items: center; font-family: var(--font); }
4191
- .osc-confirm *, .osc-confirm *::before, .osc-confirm *::after { box-sizing: border-box; }
4192
- .osc-confirm-backdrop { position: absolute; inset: 0; background: rgba(8,6,4,.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
4193
- .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; }
4194
- .osc-confirm-title { margin: 0 0 12px; font-size: 22px; font-weight: 800; letter-spacing: .3px; }
4195
- .osc-confirm-msg { margin: 0 0 22px; font-size: 19px; font-weight: 700; line-height: 1.4; }
4196
- .osc-confirm-row { display: flex; gap: 14px; }
4197
- .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; }
4198
- .osc-confirm-btn:active { transform: scale(.96); }
4199
- .osc-confirm-no { background: #ffffff; color: #181b20; }
4200
- .osc-confirm-no:hover { background: #eef1f6; }
4201
- .osc-confirm-yes { background: #d99000; color: #1a1200; }
4202
- .osc-confirm-yes:hover { filter: brightness(1.05); }
4203
- `;
4204
-
4205
4158
  // src/mountHud.ts
4206
4159
  function showReplayModal(ui, info, buttonKey, onSelect) {
4207
4160
  const cur = info.currency ?? ui.balance.currency.get();
4208
- const money2 = (n) => core.formatAmountPrecise(n, cur);
4161
+ const money = (n) => core.formatAmountPrecise(n, cur);
4209
4162
  ui.showNotice(
4210
4163
  [
4211
4164
  { kind: "heading", id: "openui-replay-h", text: "openui.replay.title" },
@@ -4213,10 +4166,10 @@ function showReplayModal(ui, info, buttonKey, onSelect) {
4213
4166
  kind: "stat-grid",
4214
4167
  id: "openui-replay-g",
4215
4168
  items: [
4216
- { label: "openui.replay.baseBet", value: money2(info.baseBet) },
4169
+ { label: "openui.replay.baseBet", value: money(info.baseBet) },
4217
4170
  { label: "openui.replay.costMultiplier", value: `${info.costMultiplier}\xD7` },
4218
4171
  { label: "openui.replay.payoutMultiplier", value: `${info.payoutMultiplier}\xD7` },
4219
- { label: "openui.replay.amount", value: money2(info.amount) }
4172
+ { label: "openui.replay.amount", value: money(info.amount) }
4220
4173
  ]
4221
4174
  }
4222
4175
  ],
@@ -4230,7 +4183,7 @@ function showReplayModal(ui, info, buttonKey, onSelect) {
4230
4183
  function mountHud(app, spec = {}, opts = {}) {
4231
4184
  const { hooks, infoMenu, ...pixiOpts } = opts;
4232
4185
  const ui = core.createUI(spec, hooks);
4233
- const useInfoMenu = infoMenu !== false && pixiOpts.menu !== false;
4186
+ const useInfoMenu = infoMenu === true && pixiOpts.menu !== false;
4234
4187
  const locales = spec.locale ? Array.from(/* @__PURE__ */ new Set([spec.locale.locale, ...Object.keys(spec.locale.messages)])) : [];
4235
4188
  const menu = pixiOpts.menu === false || useInfoMenu ? false : core.composeMenu(spec.menu, { locales, localeSelectId: spec.localeSelectId, rulesFallback: spec.rules });
4236
4189
  if (menu && spec.game && (spec.game.name || spec.game.version)) {
@@ -4239,6 +4192,21 @@ function mountHud(app, spec = {}, opts = {}) {
4239
4192
  const pixi = new OpenUIPixi(ui, { ...pixiOpts, menu });
4240
4193
  pixi.mount(app);
4241
4194
  const disposeInfoMenu = useInfoMenu ? mountInfoMenu(app, ui) : void 0;
4195
+ const canvas = app.canvas;
4196
+ const cancelToUp = (e) => {
4197
+ canvas?.dispatchEvent(new PointerEvent("pointerup", {
4198
+ pointerId: e.pointerId,
4199
+ pointerType: e.pointerType,
4200
+ isPrimary: e.isPrimary,
4201
+ clientX: e.clientX,
4202
+ clientY: e.clientY,
4203
+ pressure: 0,
4204
+ bubbles: true,
4205
+ cancelable: true,
4206
+ composed: true
4207
+ }));
4208
+ };
4209
+ canvas?.addEventListener("pointercancel", cancelToUp, true);
4242
4210
  const extra = [];
4243
4211
  if (spec.panels?.length) {
4244
4212
  for (const ps of spec.panels) {
@@ -4259,6 +4227,7 @@ function mountHud(app, spec = {}, opts = {}) {
4259
4227
  });
4260
4228
  const teardown = () => {
4261
4229
  offRelayout();
4230
+ canvas?.removeEventListener("pointercancel", cancelToUp, true);
4262
4231
  disposeInfoMenu?.();
4263
4232
  for (const v of extra) v.dispose();
4264
4233
  extra.length = 0;
@@ -4321,14 +4290,19 @@ function mountHud(app, spec = {}, opts = {}) {
4321
4290
  return;
4322
4291
  }
4323
4292
  const message = o.message ?? (o.name ? ui.t("openui.buyFeature.confirm", { name: ui.t(o.name), price: o.price ?? "" }) : ui.t("openui.buyFeature.message"));
4324
- void showConfirm(ui, {
4325
- title: o.title ?? "openui.buyFeature.title",
4326
- message,
4327
- confirmLabel: o.confirmLabel,
4328
- cancelLabel: o.cancelLabel
4329
- }).then((ok) => {
4330
- if (ok) o.onConfirm();
4331
- });
4293
+ ui.showNotice(
4294
+ [
4295
+ { kind: "heading", id: "buy-confirm-title", text: o.title ?? "openui.buyFeature.title" },
4296
+ { kind: "text", id: "buy-confirm-body", text: message }
4297
+ ],
4298
+ [
4299
+ { label: o.cancelLabel ?? "openui.cancel", variant: "secondary" },
4300
+ { label: o.confirmLabel ?? "openui.confirm", variant: "primary", onSelect: () => {
4301
+ ui.hideNotice();
4302
+ o.onConfirm();
4303
+ } }
4304
+ ]
4305
+ );
4332
4306
  },
4333
4307
  showControls: () => pixi.setControlsVisible(true),
4334
4308
  hideControls: () => pixi.setControlsVisible(false),
@@ -4342,207 +4316,332 @@ function mountHud(app, spec = {}, opts = {}) {
4342
4316
  dispose: teardown
4343
4317
  };
4344
4318
  }
4345
- function money(amount, cur) {
4346
- return core.formatAmountPrecise(amount, cur);
4319
+ var LIGHT3 = {
4320
+ surface: "#ffffff",
4321
+ surfaceAlt: "#eef1f6",
4322
+ text: "#181b20",
4323
+ textDim: "#5b6472",
4324
+ border: "#000000",
4325
+ accent: "#d99000",
4326
+ accentText: "#1a1200",
4327
+ scrim: "#080604"
4328
+ };
4329
+ var CARD_W = 200;
4330
+ var IMG_H = 120;
4331
+ var STRIP_H = 8;
4332
+ var CARD_H = 214;
4333
+ var ACT_GAP = 12;
4334
+ var ACT_H = 50;
4335
+ var CELL_H = CARD_H + ACT_GAP + ACT_H;
4336
+ var GAP3 = 18;
4337
+ var STEP_R = 27;
4338
+ var BET_W = 220;
4339
+ var BET_H = 60;
4340
+ var TITLE_H = 40;
4341
+ var ROW_GAP2 = 22;
4342
+ var TAP_SLOP2 = 24;
4343
+ var texCache = /* @__PURE__ */ new Map();
4344
+ function loadTex(url) {
4345
+ let p = texCache.get(url);
4346
+ if (!p) {
4347
+ p = pixi_js.Assets.load(url).catch(() => pixi_js.Texture.EMPTY);
4348
+ texCache.set(url, p);
4349
+ }
4350
+ return p;
4347
4351
  }
4348
- var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4349
- function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4350
- const ui = hud.ui;
4351
- const tr = (k) => ui.t(k);
4352
- const list = features.slice(0, 4);
4353
- ui.declareFacts({ modes: list.map((f) => ({ id: f.id, name: f.name, kind: f.variant, cost: f.cost })) });
4354
- const boosts = /* @__PURE__ */ new Set();
4355
- const activation = opts.activation ?? "multi";
4356
- const blocksBuy = opts.activationBlocksBuy ?? false;
4357
- const disposers = [];
4358
- const host2 = document.createElement("div");
4359
- host2.className = "bfm-root";
4360
- const vars = {
4361
- "--accent": "#d99000",
4362
- "--accent-text": "#1a1200",
4363
- "--surface": "#ffffff",
4364
- "--surface-alt": "#eef1f6",
4365
- "--text": "#181b20",
4366
- "--text-dim": "#5b6472",
4367
- "--font": ui.theme.type.family
4368
- };
4369
- for (const [k, v] of Object.entries(vars)) host2.style.setProperty(k, v);
4370
- host2.innerHTML = `
4371
- <div class="bfm-backdrop" data-close></div>
4372
- <button class="bfm-x" data-close aria-label="Close">\u2715</button>
4373
- <div class="bfm-panel" role="dialog" aria-modal="true">
4374
- <div class="bfm-fit" id="bfm-fit">
4375
- <h2 class="bfm-title" data-t="Buy Feature">${esc3(tr("Buy Feature"))}</h2>
4376
- <div class="bfm-bet">
4377
- <button class="bfm-step" id="bfm-minus" aria-label="Decrease">\u2212</button>
4378
- <div class="bfm-betbox"><span class="bfm-betlabel" data-t="Bet">${esc3(tr("Bet"))}</span><b id="bfm-betval">\u2014</b></div>
4379
- <button class="bfm-step" id="bfm-plus" aria-label="Increase">+</button>
4380
- </div>
4381
- <div class="bfm-cards" id="bfm-cards"></div>
4382
- </div>
4383
- </div>`;
4384
- const style = document.createElement("style");
4385
- style.textContent = BFM_CSS;
4386
- host2.appendChild(style);
4387
- document.body.appendChild(host2);
4388
- const $ = (sel) => host2.querySelector(sel);
4389
- const panel = $(".bfm-panel");
4390
- const fitEl = $("#bfm-fit");
4391
- const cardsEl = $("#bfm-cards");
4392
- const betValEl = $("#bfm-betval");
4393
- const askConfirm = (name, price, onYes) => {
4394
- const message = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
4395
- void showConfirm(ui, { title: "Buy Feature", message }).then((ok) => {
4396
- if (ok) onYes();
4397
- });
4398
- };
4399
- const layout = () => {
4400
- const vw = window.innerWidth;
4401
- const vh = window.innerHeight;
4402
- const cols = vh <= 540 || vw >= 900 ? Math.min(4, list.length) : Math.min(2, list.length);
4403
- cardsEl.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
4404
- const fitW = Math.min(vw * 0.96, cols >= 4 ? 1180 : cols === 1 ? 380 : 760);
4405
- fitEl.style.width = `${fitW}px`;
4406
- fitEl.style.transform = "none";
4407
- const natH = fitEl.offsetHeight;
4408
- const s = Math.min(1, vh * 0.95 / natH);
4409
- fitEl.style.transform = `translateX(-50%) scale(${s})`;
4410
- panel.style.width = `${Math.ceil(fitW * s)}px`;
4411
- panel.style.height = `${Math.ceil(natH * s)}px`;
4352
+ function wireTap(node, hit, onTap) {
4353
+ node.eventMode = "static";
4354
+ node.cursor = "pointer";
4355
+ node.hitArea = hit;
4356
+ let down = null;
4357
+ node.on("pointerdown", (e) => {
4358
+ down = { x: e.global.x, y: e.global.y };
4359
+ node.alpha = 0.82;
4360
+ });
4361
+ const end = (e, inside) => {
4362
+ node.alpha = 1;
4363
+ const p = down;
4364
+ down = null;
4365
+ if (!p) return;
4366
+ if (inside || Math.hypot(e.global.x - p.x, e.global.y - p.y) <= TAP_SLOP2) onTap();
4412
4367
  };
4413
- const renderCards = () => {
4414
- const bet = opts.getBet ? opts.getBet() : ui.bet.get();
4368
+ node.on("pointerup", (e) => end(e, true));
4369
+ node.on("pointerupoutside", (e) => end(e, false));
4370
+ }
4371
+ var FeatureCard = class extends pixi_js.Container {
4372
+ priceText;
4373
+ actionBg = new pixi_js.Graphics();
4374
+ actionLabel;
4375
+ action = new pixi_js.Container();
4376
+ constructor(spec, ui, onAction) {
4377
+ super();
4378
+ const fam = ui.theme.type.family;
4379
+ const fill = new pixi_js.Graphics().roundRect(0, 0, CARD_W, CARD_H, 14).fill({ color: LIGHT3.surface });
4380
+ const clip = new pixi_js.Container();
4381
+ const clipMask = new pixi_js.Graphics().roundRect(0, 0, CARD_W, CARD_H, 14).fill({ color: 16777215 });
4382
+ clip.mask = clipMask;
4383
+ const placeholder = new pixi_js.Graphics().rect(0, 0, CARD_W, IMG_H).fill({ color: LIGHT3.surfaceAlt });
4384
+ clip.addChild(placeholder);
4385
+ if (spec.image) {
4386
+ void loadTex(spec.image).then((tex) => {
4387
+ if (this.destroyed || tex === pixi_js.Texture.EMPTY) return;
4388
+ const sp = new pixi_js.Sprite(tex);
4389
+ const s = Math.max(CARD_W / tex.width, IMG_H / tex.height);
4390
+ sp.scale.set(s);
4391
+ sp.x = (CARD_W - tex.width * s) / 2;
4392
+ sp.y = (IMG_H - tex.height * s) / 2;
4393
+ clip.addChildAt(sp, 1);
4394
+ });
4395
+ }
4396
+ const strip = new pixi_js.Graphics().rect(0, IMG_H, CARD_W, STRIP_H).fill({ color: LIGHT3.accent });
4397
+ const name = new pixi_js.Text({ text: ui.t(spec.name), style: { fontFamily: fam, fontSize: 16, fontWeight: "600", fill: LIGHT3.text } });
4398
+ name.anchor.set(0.5, 0);
4399
+ name.position.set(CARD_W / 2, IMG_H + STRIP_H + 12);
4400
+ this.priceText = new pixi_js.Text({ text: "", style: { fontFamily: fam, fontSize: 22, fontWeight: "800", fill: LIGHT3.text } });
4401
+ this.priceText.anchor.set(0.5, 0);
4402
+ this.priceText.position.set(CARD_W / 2, IMG_H + STRIP_H + 36);
4403
+ clip.addChild(strip, name, this.priceText);
4404
+ const stroke = new pixi_js.Graphics().roundRect(0, 0, CARD_W, CARD_H, 14).stroke({ width: 4, color: LIGHT3.border });
4405
+ this.addChild(fill, clip, clipMask, stroke);
4406
+ this.actionLabel = new pixi_js.Text({ text: "", style: { fontFamily: fam, fontSize: 15, fontWeight: "800", fill: LIGHT3.text, letterSpacing: 0.5 } });
4407
+ this.actionLabel.anchor.set(0.5);
4408
+ this.actionLabel.position.set(CARD_W / 2, CARD_H + ACT_GAP + ACT_H / 2);
4409
+ this.action.addChild(this.actionBg, this.actionLabel);
4410
+ wireTap(this.action, new pixi_js.Rectangle(0, CARD_H + ACT_GAP, CARD_W, ACT_H), onAction);
4411
+ this.addChild(this.action);
4412
+ }
4413
+ /** Update price + action label/state for the current bet + boost state. */
4414
+ update(price, label, active, blocked) {
4415
+ this.priceText.text = price;
4416
+ this.actionLabel.text = label;
4417
+ this.actionLabel.style.fill = active ? LIGHT3.accentText : LIGHT3.text;
4418
+ this.actionBg.clear().roundRect(0, CARD_H + ACT_GAP, CARD_W, ACT_H, 12).fill({ color: active ? LIGHT3.accent : LIGHT3.surface }).stroke({ width: 4, color: LIGHT3.border });
4419
+ this.action.eventMode = blocked ? "none" : "static";
4420
+ this.action.alpha = blocked ? 0.38 : 1;
4421
+ }
4422
+ };
4423
+ var BuyFeatureModalView = class extends pixi_js.Container {
4424
+ constructor(hud, features, opts) {
4425
+ super();
4426
+ this.hud = hud;
4427
+ this.opts = opts;
4428
+ const ui = hud.ui;
4429
+ const fam = ui.theme.type.family;
4430
+ this.list = features.slice(0, 4);
4431
+ this.zIndex = 125;
4432
+ this.visible = false;
4433
+ this.eventMode = "none";
4434
+ ui.declareFacts({ modes: this.list.map((f) => ({ id: f.id, name: f.name, kind: f.variant, cost: f.cost })) });
4435
+ this.backdrop.eventMode = "static";
4436
+ this.backdrop.on("pointertap", () => this.close());
4437
+ this.title = new pixi_js.Text({ text: ui.t("Buy Feature"), style: { fontFamily: fam, fontSize: 30, fontWeight: "800", fill: "#ffffff", letterSpacing: 1 } });
4438
+ this.title.anchor.set(0.5, 0);
4439
+ const minus = this.stepButton("\u2212", () => ui.betStepper.dec());
4440
+ const plus = this.stepButton("+", () => ui.betStepper.inc());
4441
+ const betBox = new pixi_js.Container();
4442
+ const betBg = new pixi_js.Graphics().roundRect(-BET_W / 2, -BET_H / 2, BET_W, BET_H, 12).fill({ color: LIGHT3.surface }).stroke({ width: 3, color: LIGHT3.border });
4443
+ const betLabel = new pixi_js.Text({ text: ui.t("Bet"), style: { fontFamily: fam, fontSize: 12, fontWeight: "700", fill: LIGHT3.textDim, letterSpacing: 1 } });
4444
+ betLabel.anchor.set(0.5, 0);
4445
+ betLabel.position.set(0, -BET_H / 2 + 8);
4446
+ this.betValue = new pixi_js.Text({ text: "", style: { fontFamily: fam, fontSize: 24, fontWeight: "800", fill: LIGHT3.text } });
4447
+ this.betValue.anchor.set(0.5, 1);
4448
+ this.betValue.position.set(0, BET_H / 2 - 8);
4449
+ betBox.addChild(betBg, betLabel, this.betValue);
4450
+ const betRow = new pixi_js.Container();
4451
+ minus.position.set(STEP_R, 0);
4452
+ betBox.position.set(STEP_R * 2 + 16 + BET_W / 2, 0);
4453
+ plus.position.set(STEP_R * 2 + 16 + BET_W + 16 + STEP_R, 0);
4454
+ betRow.addChild(minus, betBox, plus);
4455
+ this.betRow = betRow;
4456
+ for (const spec of this.list) {
4457
+ const card = new FeatureCard(spec, ui, () => this.onAction(spec.id, spec.variant));
4458
+ this.cards.push(card);
4459
+ this.cardsRow.addChild(card);
4460
+ }
4461
+ this.content.addChild(this.title, betRow, this.cardsRow);
4462
+ const cbg = new pixi_js.Graphics().circle(0, 0, 23).fill({ color: LIGHT3.scrim, alpha: 0.85 });
4463
+ const cx = new pixi_js.Graphics().moveTo(-7, -7).lineTo(7, 7).moveTo(7, -7).lineTo(-7, 7).stroke({ width: 3, color: "#ffffff", cap: "round" });
4464
+ this.closeBtn.addChild(cbg, cx);
4465
+ wireTap(this.closeBtn, new pixi_js.Rectangle(-23, -23, 46, 46), () => this.close());
4466
+ this.addChild(this.backdrop, this.content, this.closeBtn);
4467
+ this.disposers.push(
4468
+ ui.bet.value.subscribe(() => this.refresh()),
4469
+ ui.locale.subscribe(() => {
4470
+ this.title.text = ui.t("Buy Feature");
4471
+ betLabel.text = ui.t("Bet");
4472
+ this.refresh();
4473
+ this.relayout();
4474
+ })
4475
+ );
4476
+ }
4477
+ hud;
4478
+ opts;
4479
+ backdrop = new pixi_js.Graphics();
4480
+ content = new pixi_js.Container();
4481
+ closeBtn = new pixi_js.Container();
4482
+ title;
4483
+ betValue;
4484
+ cardsRow = new pixi_js.Container();
4485
+ cards = [];
4486
+ list;
4487
+ boosts = /* @__PURE__ */ new Set();
4488
+ disposers = [];
4489
+ screen;
4490
+ betRow;
4491
+ stepButton(glyph, onTap) {
4492
+ const c = new pixi_js.Container();
4493
+ const fam = this.hud.ui.theme.type.family;
4494
+ const bg = new pixi_js.Graphics().circle(0, 0, STEP_R).fill({ color: LIGHT3.surface }).stroke({ width: 3, color: LIGHT3.border });
4495
+ const t = new pixi_js.Text({ text: glyph, style: { fontFamily: fam, fontSize: 28, fontWeight: "800", fill: LIGHT3.text } });
4496
+ t.anchor.set(0.5);
4497
+ t.position.set(0, -1);
4498
+ c.addChild(bg, t);
4499
+ wireTap(c, new pixi_js.Rectangle(-STEP_R, -STEP_R, STEP_R * 2, STEP_R * 2), onTap);
4500
+ return c;
4501
+ }
4502
+ /** Price + action state per current bet + boosts. */
4503
+ refresh() {
4504
+ const ui = this.hud.ui;
4505
+ const bet = this.opts.getBet ? this.opts.getBet() : ui.bet.get();
4415
4506
  const cur = ui.bet.currency.get();
4416
- cardsEl.innerHTML = list.map((f) => {
4417
- const active = boosts.has(f.id);
4418
- const buyBlocked = f.variant === "buy" && blocksBuy && boosts.size > 0;
4419
- const price = f.variant === "buy" ? money(f.cost * bet, cur) : `+${money(f.cost * bet, cur)}`;
4420
- const label = f.variant === "buy" ? tr("Buy") : active ? tr("Activated") : tr("Activate");
4421
- const cls = `bfm-action bfm-action--${f.variant}${active ? " is-active" : ""}${buyBlocked ? " is-blocked" : ""}`;
4422
- const img = f.image ? ` style="background-image:url('${f.image}')"` : "";
4423
- return `
4424
- <div class="bfm-cell">
4425
- <div class="bfm-card">
4426
- <div class="bfm-cardimg"${img}></div>
4427
- <div class="bfm-strip"></div>
4428
- <div class="bfm-cardbody">
4429
- <span class="bfm-name">${esc3(tr(f.name))}</span>
4430
- <b class="bfm-price">${esc3(price)}</b>
4431
- </div>
4432
- </div>
4433
- <button class="${cls}" data-id="${f.id}" data-variant="${f.variant}"${buyBlocked ? " disabled" : ""}>${esc3(label)}</button>
4434
- </div>`;
4435
- }).join("");
4436
- betValEl.textContent = money(bet, cur);
4437
- cardsEl.querySelectorAll(".bfm-action").forEach((b) => {
4438
- b.addEventListener("click", () => onAction(b.dataset.id, b.dataset.variant));
4507
+ const money = (n) => core.formatAmountPrecise(n, cur);
4508
+ this.betValue.text = money(bet);
4509
+ const blocksBuy = this.opts.activationBlocksBuy ?? false;
4510
+ this.list.forEach((f, i) => {
4511
+ const active = this.boosts.has(f.id);
4512
+ const perSpin = (f.variant === "buy" ? f.cost : 1 + f.cost) * bet;
4513
+ const label = f.variant === "buy" ? ui.t("Buy") : active ? ui.t("Activated") : ui.t("Activate");
4514
+ const blocked = f.variant === "buy" && blocksBuy && this.boosts.size > 0;
4515
+ this.cards[i].update(money(perSpin), label, active, blocked);
4439
4516
  });
4440
- layout();
4441
- };
4442
- const onAction = (id, variant) => {
4443
- const f = list.find((x) => x.id === id);
4444
- const bet = opts.getBet ? opts.getBet() : ui.bet.get();
4517
+ }
4518
+ onAction(id, variant) {
4519
+ const ui = this.hud.ui;
4520
+ const f = this.list.find((x) => x.id === id);
4521
+ const bet = this.opts.getBet ? this.opts.getBet() : ui.bet.get();
4445
4522
  const cur = ui.bet.currency.get();
4523
+ const money = (n) => core.formatAmountPrecise(n, cur);
4524
+ const activation = this.opts.activation ?? "multi";
4525
+ const blocksBuy = this.opts.activationBlocksBuy ?? false;
4446
4526
  if (variant === "boost") {
4447
- const wasActive = boosts.has(id);
4527
+ const wasActive = this.boosts.has(id);
4448
4528
  const commit = () => {
4449
- if (activation === "single") boosts.clear();
4450
- if (wasActive) boosts.delete(id);
4451
- else boosts.add(id);
4529
+ if (activation === "single") this.boosts.clear();
4530
+ if (wasActive) this.boosts.delete(id);
4531
+ else this.boosts.add(id);
4452
4532
  ui.bus.emit("cardActivated", { id });
4453
- renderCards();
4454
- opts.onActivate?.([...boosts], id, boosts.has(id));
4533
+ this.refresh();
4534
+ this.opts.onActivate?.([...this.boosts], id, this.boosts.has(id));
4455
4535
  };
4456
4536
  if (wasActive) {
4457
4537
  commit();
4458
4538
  return;
4459
4539
  }
4460
4540
  const total = 1 + (f?.cost ?? 0);
4461
- askConfirm(f?.name ?? id, money(total * bet, cur), commit);
4541
+ this.askConfirm(f?.name ?? id, money(total * bet), commit);
4462
4542
  } else {
4463
- if (blocksBuy && boosts.size > 0) return;
4543
+ if (blocksBuy && this.boosts.size > 0) return;
4464
4544
  const cost = (f?.cost ?? 0) * bet;
4465
- askConfirm(f?.name ?? id, money(cost, cur), () => {
4545
+ this.askConfirm(f?.name ?? id, money(cost), () => {
4466
4546
  ui.bus.emit("cardActivated", { id });
4467
- close();
4468
- opts.onBuy?.(id, cost);
4547
+ this.close();
4548
+ this.opts.onBuy?.(id, cost);
4469
4549
  });
4470
4550
  }
4471
- };
4472
- $("#bfm-minus").addEventListener("click", () => ui.betStepper.dec());
4473
- $("#bfm-plus").addEventListener("click", () => ui.betStepper.inc());
4474
- disposers.push(ui.bet.value.subscribe(() => renderCards()));
4475
- const open = () => {
4476
- if (host2.classList.contains("open")) return;
4477
- renderCards();
4478
- host2.classList.add("open");
4479
- ui.lock();
4480
- };
4481
- const close = () => {
4482
- if (!host2.classList.contains("open")) return;
4483
- host2.classList.remove("open");
4484
- ui.unlock();
4485
- };
4486
- host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", close));
4487
- disposers.push(ui.on("buttonActivated", ({ id }) => {
4488
- if (id === "bonus") open();
4489
- }));
4490
- const onResize = () => {
4491
- if (host2.classList.contains("open")) layout();
4492
- };
4493
- window.addEventListener("resize", onResize);
4494
- disposers.push(
4495
- ui.locale.subscribe(() => {
4496
- host2.querySelectorAll("[data-t]").forEach((n) => n.textContent = tr(n.dataset.t));
4497
- renderCards();
4498
- })
4499
- );
4500
- renderCards();
4551
+ }
4552
+ /** The ONE universal confirm — the in-canvas Pixi notice (DialogView, zIndex 130), so it
4553
+ * layers over this modal. Message + labels come from the library's i18n (social-aware). */
4554
+ askConfirm(name, price, onYes) {
4555
+ const ui = this.hud.ui;
4556
+ const message = ui.t("openui.buyFeature.confirm", { name: ui.t(name), price });
4557
+ ui.showNotice(
4558
+ [
4559
+ { kind: "heading", id: "bfm-confirm-h", text: "Buy Feature" },
4560
+ { kind: "text", id: "bfm-confirm-b", text: message }
4561
+ ],
4562
+ [
4563
+ { label: "openui.cancel", variant: "secondary" },
4564
+ { label: "openui.confirm", variant: "primary", onSelect: () => {
4565
+ ui.hideNotice();
4566
+ onYes();
4567
+ } }
4568
+ ]
4569
+ );
4570
+ }
4571
+ layout(screen) {
4572
+ this.screen = screen;
4573
+ if (this.visible) this.relayout();
4574
+ }
4575
+ relayout() {
4576
+ const s = this.screen;
4577
+ if (!s) return;
4578
+ const W = s.width;
4579
+ const H = s.height;
4580
+ this.backdrop.clear().rect(0, 0, W, H).fill({ color: LIGHT3.scrim, alpha: 0.55 });
4581
+ this.backdrop.hitArea = new pixi_js.Rectangle(0, 0, W, H);
4582
+ const n = this.list.length;
4583
+ const cols = W >= 720 && (W >= 900 || H <= 540) ? Math.min(4, n) : Math.min(2, n);
4584
+ const rows = Math.ceil(n / cols);
4585
+ this.cards.forEach((card, i) => {
4586
+ const r = Math.floor(i / cols);
4587
+ const c = i % cols;
4588
+ card.position.set(c * (CARD_W + GAP3), r * (CELL_H + GAP3));
4589
+ });
4590
+ const gridW = cols * CARD_W + (cols - 1) * GAP3;
4591
+ const gridH = rows * CELL_H + (rows - 1) * GAP3;
4592
+ const betW = 4 * STEP_R + BET_W + 32;
4593
+ const contentW = Math.max(gridW, betW, 340);
4594
+ this.title.position.set(contentW / 2, 0);
4595
+ this.betRow.position.set((contentW - betW) / 2, TITLE_H + ROW_GAP2 + STEP_R);
4596
+ this.cardsRow.position.set((contentW - gridW) / 2, TITLE_H + ROW_GAP2 + BET_H + ROW_GAP2);
4597
+ const contentH = TITLE_H + ROW_GAP2 + BET_H + ROW_GAP2 + gridH;
4598
+ const scale = Math.min(1, W * 0.94 / contentW, H * 0.92 / contentH);
4599
+ this.content.scale.set(scale);
4600
+ this.content.position.set((W - contentW * scale) / 2, (H - contentH * scale) / 2);
4601
+ this.closeBtn.position.set(W - 34, 34);
4602
+ }
4603
+ open() {
4604
+ if (this.visible) return;
4605
+ this.hud.ui.lock();
4606
+ this.visible = true;
4607
+ this.eventMode = "static";
4608
+ this.refresh();
4609
+ this.relayout();
4610
+ }
4611
+ close() {
4612
+ if (!this.visible) return;
4613
+ this.visible = false;
4614
+ this.eventMode = "none";
4615
+ this.hud.ui.unlock();
4616
+ }
4617
+ get isOpen() {
4618
+ return this.visible;
4619
+ }
4620
+ dispose() {
4621
+ if (this.visible) this.hud.ui.unlock();
4622
+ for (const d of this.disposers.splice(0)) d();
4623
+ if (!this.destroyed) this.destroy({ children: true });
4624
+ }
4625
+ };
4626
+ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4627
+ const ui = hud.ui;
4628
+ const view = new BuyFeatureModalView(hud, features, opts);
4629
+ hud.pixi.root.addChild(view);
4630
+ view.layout(ui.screen.get());
4631
+ const offScreen = ui.screen.subscribe(() => view.layout(ui.screen.get()));
4632
+ const offOpen = ui.on("buttonActivated", ({ id }) => {
4633
+ if (id === "bonus") view.open();
4634
+ });
4501
4635
  return () => {
4502
- window.removeEventListener("resize", onResize);
4503
- if (host2.classList.contains("open")) ui.unlock();
4504
- for (const d of disposers.splice(0)) d();
4505
- host2.remove();
4636
+ offScreen();
4637
+ offOpen();
4638
+ view.dispose();
4506
4639
  };
4507
4640
  }
4508
- var BFM_CSS = `
4509
- .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; }
4510
- .bfm-root.open { opacity: 1; pointer-events: auto; }
4511
- .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; }
4512
- .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); }
4513
- .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; }
4514
- .bfm-x:hover { transform: scale(1.08); background: rgba(18,14,10,.95); }
4515
- .bfm-root *, .bfm-root *::before, .bfm-root *::after { box-sizing: border-box; }
4516
- .bfm-panel { position: relative; transform: translateY(8px) scale(.985); transition: transform .18s ease; }
4517
- .bfm-root.open .bfm-panel { transform: none; }
4518
- .bfm-fit { position: absolute; top: 0; left: 50%; transform: translateX(-50%); transform-origin: top center; }
4519
- .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); }
4520
- .bfm-bet { display: flex; align-items: center; justify-content: center; gap: 16px; margin: 0 0 24px; }
4521
- .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; }
4522
- .bfm-betlabel { font-size: 12px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
4523
- .bfm-betbox b { font-size: 24px; color: var(--text); }
4524
- .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); }
4525
- .bfm-step:hover { background: var(--surface-alt); }
4526
- .bfm-step:active { transform: scale(.92); }
4527
- .bfm-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; align-items: start; }
4528
- .bfm-cell { display: flex; flex-direction: column; min-width: 0; }
4529
- .bfm-card { background: var(--surface); border: 4px solid #000; border-radius: 14px; overflow: hidden; box-shadow: 0 14px 34px rgba(0,0,0,.45); }
4530
- .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); }
4531
- .bfm-strip { height: 8px; background: linear-gradient(90deg, #f0a500, #ffd166, #f0a500); }
4532
- .bfm-cardbody { padding: 12px 14px 16px; text-align: center; }
4533
- .bfm-name { display: block; font-size: 15px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
4534
- .bfm-price { display: block; margin-top: 2px; font-size: 22px; font-weight: 800; color: var(--text); }
4535
- .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; }
4536
- .bfm-action:hover { background: var(--surface-alt); }
4537
- .bfm-action:active { transform: scale(.96); }
4538
- .bfm-action.is-active { background: var(--accent); color: var(--accent-text); border-color: #000; }
4539
- .bfm-action.is-blocked, .bfm-action:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
4540
- .bfm-action.is-blocked:hover, .bfm-action:disabled:hover { background: var(--surface); }
4541
- `;
4542
4641
 
4543
4642
  // src/bootError.ts
4544
4643
  var host = null;
4545
- var esc4 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4644
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4546
4645
  function showBootError(opts = {}) {
4547
4646
  const title = opts.title ?? "Connection lost";
4548
4647
  const message = opts.message ?? "The game could not reach the game server. Please reload to reconnect and continue.";
@@ -4556,10 +4655,10 @@ function showBootError(opts = {}) {
4556
4655
  <div class="openui-be-icon" aria-hidden="true">
4557
4656
  <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>
4558
4657
  </div>
4559
- <h1 class="openui-be-title">${esc4(title)}</h1>
4560
- <p class="openui-be-msg">${esc4(message)}</p>
4561
- ${opts.detail ? `<p class="openui-be-detail">${esc4(opts.detail)}</p>` : ""}
4562
- ${showReload ? `<button class="openui-be-reload" type="button">${esc4(opts.reloadLabel ?? "Reload")}</button>` : ""}
4658
+ <h1 class="openui-be-title">${esc2(title)}</h1>
4659
+ <p class="openui-be-msg">${esc2(message)}</p>
4660
+ ${opts.detail ? `<p class="openui-be-detail">${esc2(opts.detail)}</p>` : ""}
4661
+ ${showReload ? `<button class="openui-be-reload" type="button">${esc2(opts.reloadLabel ?? "Reload")}</button>` : ""}
4563
4662
  </div>
4564
4663
  <style>
4565
4664
  .openui-boot-error { position: fixed; inset: 0; z-index: 2147483000; display: grid; place-items: center;
@@ -4643,7 +4742,6 @@ exports.mountBuyFeatureModal = mountBuyFeatureModal;
4643
4742
  exports.mountHud = mountHud;
4644
4743
  exports.mountInfoMenu = mountInfoMenu;
4645
4744
  exports.showBootError = showBootError;
4646
- exports.showConfirm = showConfirm;
4647
4745
  exports.svgSpinSkin = svgSpinSkin;
4648
4746
  //# sourceMappingURL=index.cjs.map
4649
4747
  //# sourceMappingURL=index.cjs.map