@open-slot-ui/pixi 0.8.3 → 0.10.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
@@ -208,6 +208,9 @@ var SpinView = class extends ControlView {
208
208
  }),
209
209
  // re-dim if slam-stop is toggled at runtime (e.g. applyJurisdiction)
210
210
  this.spin.allowSlamStop.subscribe(() => this.updateInteractive()),
211
+ // the ref-counted HUD lock (menu / buy modal / blocking notice) gates
212
+ // `spin.interactable` — refresh eventMode/cursor when it flips
213
+ this.ui.locked.subscribe(() => this.updateInteractive()),
211
214
  this.spin.onTransition((t) => this.play(t)),
212
215
  // free-spins face: switch between the normal skin and the "N FS" counter
213
216
  this.spin.freeSpins.subscribe(() => this.updateFaces()),
@@ -3104,7 +3107,7 @@ var ReadoutView = class extends ControlView {
3104
3107
  let text;
3105
3108
  switch (this.ro.kind) {
3106
3109
  case "currency":
3107
- text = this.ro.currency ? core.formatAmount(v, this.ro.currency.get(), { signed: this.ro.signed }) : String(v);
3110
+ text = this.ro.currency ? core.formatAmountPrecise(v, this.ro.currency.get(), { signed: this.ro.signed }) : String(v);
3108
3111
  break;
3109
3112
  case "percent":
3110
3113
  text = `${v.toFixed(this.ro.decimals)}%`;
@@ -3987,7 +3990,11 @@ var PanelBodyView = class extends ControlView {
3987
3990
  };
3988
3991
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3989
3992
  var rich = (s) => esc(s).replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
3990
- function renderBlocks(blocks, tr) {
3993
+ function statGridHtml(items, tr) {
3994
+ const rows = items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
3995
+ return `<dl class="ohm-stats">${rows}</dl>`;
3996
+ }
3997
+ function renderBlocks(blocks, tr, facts) {
3991
3998
  const out = [];
3992
3999
  for (const b of blocks) {
3993
4000
  switch (b.kind) {
@@ -4029,9 +4036,16 @@ function renderBlocks(blocks, tr) {
4029
4036
  out.push(`<table class="ohm-table">${head}<tbody>${body}</tbody></table>`);
4030
4037
  break;
4031
4038
  }
4032
- case "stat-grid": {
4033
- const rows = b.items.map((it) => `<div><dt>${esc(tr(it.label))}</dt><dd>${esc(tr(it.value))}</dd></div>`).join("");
4034
- out.push(`<dl class="ohm-stats">${rows}</dl>`);
4039
+ case "stat-grid":
4040
+ out.push(statGridHtml(b.items, tr));
4041
+ break;
4042
+ // The AUTO per-mode RTP / Max-win grid — rendered straight from the declared
4043
+ // game facts (+ any host extras), so the table can never drift from the config.
4044
+ // `modeStatsItems` localizes the label PARTS itself ("Max win" · the mode name),
4045
+ // so the grid renderer gets identity-tr — no double translation.
4046
+ case "mode-stats": {
4047
+ const extras = (b.extras ?? []).map((e) => ({ label: tr(e.label), value: tr(e.value) }));
4048
+ out.push(statGridHtml([...core.modeStatsItems(facts, tr), ...extras], (s) => s));
4035
4049
  break;
4036
4050
  }
4037
4051
  case "steps": {
@@ -4101,8 +4115,30 @@ function mountInfoMenu(app, ui) {
4101
4115
  const soundMode = menu.sound ?? "sliders";
4102
4116
  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>`;
4103
4117
  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()) : "";
4104
- const paytableHtml = menu.paytable ? renderBlocks(menu.paytable, tr) : "";
4105
- const rulesHtml = menu.rules ? renderBlocks(menu.rules, tr) : "";
4118
+ const rulesTr = () => {
4119
+ const vars2 = core.factsVars(ui.facts.get(), { "game.name": ui.gameInfo.name ?? "", "game.version": ui.gameInfo.version ?? "" });
4120
+ return (s) => ui.t(s, vars2);
4121
+ };
4122
+ const auditCardHtml = (trr) => {
4123
+ if (!menu.rules?.length) return "";
4124
+ const issues = core.auditRules(ui.facts.get(), menu.rules, { resolve: trr });
4125
+ if (!issues.length) return "";
4126
+ const req = issues.filter((i) => i.level === "required");
4127
+ const rec = issues.filter((i) => i.level === "recommended");
4128
+ const list = (items) => `<ul>${items.map((i) => `<li>${esc(tr(i.message))}</li>`).join("")}</ul>`;
4129
+ return `<div class="ohm-audit" role="alert">
4130
+ <div class="ohm-audit-title">\u26A0 ${esc(tr("openui.rulesAudit.title"))}</div>
4131
+ ${req.length ? `<div class="ohm-audit-sub">${esc(tr("openui.rulesAudit.required"))}</div>${list(req)}` : ""}
4132
+ ${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>` : ""}
4133
+ </div>`;
4134
+ };
4135
+ const rulesInnerHtml = () => {
4136
+ if (!menu.rules) return "";
4137
+ const trr = rulesTr();
4138
+ return auditCardHtml(trr) + renderBlocks(menu.rules, trr, ui.facts.get());
4139
+ };
4140
+ const paytableHtml = menu.paytable ? renderBlocks(menu.paytable, tr, ui.facts.get()) : "";
4141
+ const rulesHtml = rulesInnerHtml();
4106
4142
  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>` : "";
4107
4143
  host2.innerHTML = `
4108
4144
  <div class="ohm-backdrop" data-close></div>
@@ -4174,19 +4210,40 @@ function mountInfoMenu(app, ui) {
4174
4210
  else if (el instanceof HTMLSelectElement) el.addEventListener("change", () => ui.bus.emit("optionSelected", { id, value: el.value, index: el.selectedIndex }));
4175
4211
  });
4176
4212
  host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", () => ui.settingsPanel.closePanel()));
4213
+ const refreshRules = () => {
4214
+ const rulesEl = host2.querySelector("#ohm-rules");
4215
+ if (rulesEl && menu.rules) rulesEl.innerHTML = rulesInnerHtml();
4216
+ };
4177
4217
  disposers.push(
4178
4218
  ui.locale.subscribe(() => {
4179
4219
  const body = host2.querySelector(".ohm-body");
4180
4220
  if (body) {
4181
- const rulesEl = host2.querySelector("#ohm-rules");
4182
- if (rulesEl && menu.rules) rulesEl.innerHTML = renderBlocks(menu.rules, tr);
4221
+ refreshRules();
4183
4222
  if (lang) lang.value = ui.locale.get();
4184
4223
  }
4224
+ }),
4225
+ ui.facts.subscribe(refreshRules)
4226
+ );
4227
+ let menuLocked = false;
4228
+ disposers.push(
4229
+ ui.settingsPanel.state.subscribe(() => {
4230
+ const isOpen = ui.settingsPanel.isOpen;
4231
+ host2.classList.toggle("open", isOpen);
4232
+ if (isOpen && !menuLocked) {
4233
+ ui.lock();
4234
+ menuLocked = true;
4235
+ } else if (!isOpen && menuLocked) {
4236
+ ui.unlock();
4237
+ menuLocked = false;
4238
+ }
4185
4239
  })
4186
4240
  );
4187
- disposers.push(ui.settingsPanel.state.subscribe(() => host2.classList.toggle("open", ui.settingsPanel.isOpen)));
4188
4241
  return () => {
4189
4242
  for (const d of disposers.splice(0)) d();
4243
+ if (menuLocked) {
4244
+ ui.unlock();
4245
+ menuLocked = false;
4246
+ }
4190
4247
  host2.remove();
4191
4248
  };
4192
4249
  }
@@ -4240,6 +4297,13 @@ var OHM_CSS = `
4240
4297
  .ohm-stats { margin: 12px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
4241
4298
  .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); }
4242
4299
  .ohm-stats dt { color: var(--text-dim); margin: 0; } .ohm-stats dd { margin: 0; font-weight: 700; }
4300
+ .ohm-audit { margin: 12px 0 18px; padding: 14px 16px; border-radius: 12px; border: 2px solid #d03131; background: color-mix(in srgb, #d03131 8%, transparent); }
4301
+ .ohm-audit-title { font-weight: 900; letter-spacing: .5px; color: #b31d1d; }
4302
+ .ohm-audit-sub { margin-top: 8px; font-size: 13px; font-weight: 800; color: #b31d1d; }
4303
+ .ohm-audit-sub--rec { color: #b07d09; }
4304
+ .ohm-audit ul { margin: 6px 0 0; padding-left: 20px; color: var(--text); font-size: 13.5px; line-height: 1.55; }
4305
+ .ohm-audit li { margin: 3px 0; }
4306
+ .ohm-audit-rec ul { color: var(--text-dim); }
4243
4307
  .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); }
4244
4308
  .ohm-callout b { color: var(--accent); } .ohm-callout p { margin: 4px 0 0; color: var(--text); }
4245
4309
  .ohm-callout--warning { border-left-color: #e0a106; background: color-mix(in srgb, #e0a106 10%, transparent); }
@@ -4329,7 +4393,7 @@ var CONFIRM_CSS = `
4329
4393
  // src/mountHud.ts
4330
4394
  function showReplayModal(ui, info, buttonKey, onSelect) {
4331
4395
  const cur = info.currency ?? ui.balance.currency.get();
4332
- const money2 = (n) => core.formatAmount(n, cur);
4396
+ const money2 = (n) => core.formatAmountPrecise(n, cur);
4333
4397
  ui.showNotice(
4334
4398
  [
4335
4399
  { kind: "heading", id: "openui-replay-h", text: "openui.replay.title" },
@@ -4467,13 +4531,14 @@ function mountHud(app, spec = {}, opts = {}) {
4467
4531
  };
4468
4532
  }
4469
4533
  function money(amount, cur) {
4470
- return core.formatAmount(amount, cur);
4534
+ return core.formatAmountPrecise(amount, cur);
4471
4535
  }
4472
4536
  var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4473
4537
  function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4474
4538
  const ui = hud.ui;
4475
4539
  const tr = (k) => ui.t(k);
4476
4540
  const list = features.slice(0, 4);
4541
+ ui.declareFacts({ modes: list.map((f) => ({ id: f.id, name: f.name, kind: f.variant, cost: f.cost })) });
4477
4542
  const boosts = /* @__PURE__ */ new Set();
4478
4543
  const activation = opts.activation ?? "multi";
4479
4544
  const blocksBuy = opts.activationBlocksBuy ?? false;
@@ -4596,10 +4661,16 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4596
4661
  $("#bfm-plus").addEventListener("click", () => ui.betStepper.inc());
4597
4662
  disposers.push(ui.bet.value.subscribe(() => renderCards()));
4598
4663
  const open = () => {
4664
+ if (host2.classList.contains("open")) return;
4599
4665
  renderCards();
4600
4666
  host2.classList.add("open");
4667
+ ui.lock();
4668
+ };
4669
+ const close = () => {
4670
+ if (!host2.classList.contains("open")) return;
4671
+ host2.classList.remove("open");
4672
+ ui.unlock();
4601
4673
  };
4602
- const close = () => host2.classList.remove("open");
4603
4674
  host2.querySelectorAll("[data-close]").forEach((b) => b.addEventListener("click", close));
4604
4675
  disposers.push(ui.on("buttonActivated", ({ id }) => {
4605
4676
  if (id === "bonus") open();
@@ -4617,6 +4688,7 @@ function mountBuyFeatureModal(_app, hud, features, opts = {}) {
4617
4688
  renderCards();
4618
4689
  return () => {
4619
4690
  window.removeEventListener("resize", onResize);
4691
+ if (host2.classList.contains("open")) ui.unlock();
4620
4692
  for (const d of disposers.splice(0)) d();
4621
4693
  host2.remove();
4622
4694
  };