@open-slot-ui/core 0.5.0 → 0.6.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
@@ -1255,6 +1255,11 @@ declare class OpenUI {
1255
1255
  };
1256
1256
  /** Minimum round duration (ms) from jurisdiction — stored for the GAME to enforce. */
1257
1257
  minimumRoundDuration: number;
1258
+ /** Buy-feature confirm threshold, as a multiple of the base bet. A play (buy or
1259
+ * bet-mode activation) that costs STRICTLY MORE than this must show a confirm step
1260
+ * — Stake requires that bet modes above 2× never activate on a single click. `0`
1261
+ * (the default) = off. Set via `spec.buyFeature.confirmAboveCost` or a jurisdiction. */
1262
+ confirmBuyAboveCost: number;
1258
1263
  private sessionNet;
1259
1264
  private prevVolumes;
1260
1265
  /** The frozen UISpec `createUI` built this from, if any — authored = run = tested. */
@@ -1363,6 +1368,10 @@ declare class OpenUI {
1363
1368
  /** Read-back: did the applied jurisdiction disable this feature? (real guard, not
1364
1369
  * just a hide — `confirmBuy`/autoplay entry points consult this). */
1365
1370
  isDisabled(feature: 'autoplay' | 'buyFeature' | 'turbo' | 'fullscreen' | 'slamstop' | 'spacebar'): boolean;
1371
+ /** True when a buy / bet-mode activation of `cost`× the base bet must be confirmed
1372
+ * before it commits (Stake: no one-click activation above `confirmBuyAboveCost`×).
1373
+ * Always false when the threshold is 0/unset or `cost` isn't a finite number. */
1374
+ shouldConfirmBuy(cost: number): boolean;
1366
1375
  /**
1367
1376
  * Start a reality-check reminder (RTS 13). Every `everyMinutes` (WALL-CLOCK, so a
1368
1377
  * backgrounded tab can't cheat it) it emits a `realityCheck` event, stops autoplay,
@@ -1403,6 +1412,11 @@ interface JurisdictionConfig {
1403
1412
  displaySessionTimer?: boolean;
1404
1413
  /** Minimum ms a round must take — stored on the HUD for the GAME to enforce. */
1405
1414
  minimumRoundDuration?: number;
1415
+ /** Buy-feature confirm threshold (× base bet). A play costing more than this must
1416
+ * be confirmed before it commits — Stake requires that bet modes above 2× never
1417
+ * activate on a single click, so a Stake preset sets this to `2`. Not part of the
1418
+ * wallet schema; a convenience so a jurisdiction can require the confirm step. */
1419
+ confirmBuyAboveCost?: number;
1406
1420
  }
1407
1421
  /**
1408
1422
  * Apply a Stake Engine `jurisdiction` config to a live HUD — the whole switchboard
@@ -1679,6 +1693,15 @@ interface UISpec {
1679
1693
  name?: string;
1680
1694
  version?: string;
1681
1695
  };
1696
+ /** Buy-feature / bet-mode confirmation. `confirmAboveCost` (× base bet) makes any
1697
+ * play costing MORE than that require a confirm step before it commits — Stake
1698
+ * requires bet modes above 2× to never activate on a single click, so set `2`.
1699
+ * The game routes a purchase/activation through `hud.requestBuyFeature(...)`, which
1700
+ * shows the confirm popup itself when the cost exceeds the threshold. A jurisdiction
1701
+ * can set the same threshold (`JurisdictionConfig.confirmBuyAboveCost`). */
1702
+ buyFeature?: {
1703
+ confirmAboveCost?: number;
1704
+ };
1682
1705
  /** Reality-check reminder (RTS 13): open-ui runs a wall-clock timer, emits a
1683
1706
  * `realityCheck` event, stops autoplay, and (unless `showModal:false`) shows an
1684
1707
  * acknowledge modal. You supply only the cadence + text. `title`/`message` are
@@ -1791,14 +1814,26 @@ interface SocialPhraseIssue {
1791
1814
  }
1792
1815
  /** Every restricted phrase in `text` (empty when clean). Pure + total. */
1793
1816
  declare function findForbiddenPhrases(text: string): ForbiddenMatch[];
1817
+ /** A collected string. `text` is the base English (what renders with no social
1818
+ * override); `key` is the i18n key used to look up a social override — it defaults
1819
+ * to `text`, since a game's menu/rules copy doubles as its own key. */
1794
1820
  type Entry = {
1795
1821
  source: string;
1796
1822
  text: string;
1823
+ key?: string;
1797
1824
  };
1798
1825
  /** Collect every English string a spec ships (menu/rules blocks, titles, labels+hints,
1799
1826
  * game name, the `en` dictionary) with a source path — the input to {@link checkSocialPhrases}. */
1800
1827
  declare function collectSocialCopy(spec: UISpec): Entry[];
1801
- /** Scan a whole spec's English copy and return every restricted-phrase finding. */
1828
+ /**
1829
+ * Scan a spec's English copy AS IT RENDERS IN SOCIAL MODE and return every restricted
1830
+ * phrase. Each collected string is first resolved through the social dictionary
1831
+ * (`locale.socialMessages.en` → built-in `openuiSocialDefaults` → the base text) —
1832
+ * exactly like the translator does — so a game that already swaps "bet"/"pay(table)"
1833
+ * for compliant wording in social mode is NOT flagged, while a phrase with NO social
1834
+ * override (the classic slipped-through "paytable") IS. English by design: the check
1835
+ * targets the base `en` copy that every jurisdiction review starts from.
1836
+ */
1802
1837
  declare function checkSocialPhrases(spec: UISpec): SocialPhraseIssue[];
1803
1838
 
1804
1839
  /**
package/dist/index.d.ts CHANGED
@@ -1255,6 +1255,11 @@ declare class OpenUI {
1255
1255
  };
1256
1256
  /** Minimum round duration (ms) from jurisdiction — stored for the GAME to enforce. */
1257
1257
  minimumRoundDuration: number;
1258
+ /** Buy-feature confirm threshold, as a multiple of the base bet. A play (buy or
1259
+ * bet-mode activation) that costs STRICTLY MORE than this must show a confirm step
1260
+ * — Stake requires that bet modes above 2× never activate on a single click. `0`
1261
+ * (the default) = off. Set via `spec.buyFeature.confirmAboveCost` or a jurisdiction. */
1262
+ confirmBuyAboveCost: number;
1258
1263
  private sessionNet;
1259
1264
  private prevVolumes;
1260
1265
  /** The frozen UISpec `createUI` built this from, if any — authored = run = tested. */
@@ -1363,6 +1368,10 @@ declare class OpenUI {
1363
1368
  /** Read-back: did the applied jurisdiction disable this feature? (real guard, not
1364
1369
  * just a hide — `confirmBuy`/autoplay entry points consult this). */
1365
1370
  isDisabled(feature: 'autoplay' | 'buyFeature' | 'turbo' | 'fullscreen' | 'slamstop' | 'spacebar'): boolean;
1371
+ /** True when a buy / bet-mode activation of `cost`× the base bet must be confirmed
1372
+ * before it commits (Stake: no one-click activation above `confirmBuyAboveCost`×).
1373
+ * Always false when the threshold is 0/unset or `cost` isn't a finite number. */
1374
+ shouldConfirmBuy(cost: number): boolean;
1366
1375
  /**
1367
1376
  * Start a reality-check reminder (RTS 13). Every `everyMinutes` (WALL-CLOCK, so a
1368
1377
  * backgrounded tab can't cheat it) it emits a `realityCheck` event, stops autoplay,
@@ -1403,6 +1412,11 @@ interface JurisdictionConfig {
1403
1412
  displaySessionTimer?: boolean;
1404
1413
  /** Minimum ms a round must take — stored on the HUD for the GAME to enforce. */
1405
1414
  minimumRoundDuration?: number;
1415
+ /** Buy-feature confirm threshold (× base bet). A play costing more than this must
1416
+ * be confirmed before it commits — Stake requires that bet modes above 2× never
1417
+ * activate on a single click, so a Stake preset sets this to `2`. Not part of the
1418
+ * wallet schema; a convenience so a jurisdiction can require the confirm step. */
1419
+ confirmBuyAboveCost?: number;
1406
1420
  }
1407
1421
  /**
1408
1422
  * Apply a Stake Engine `jurisdiction` config to a live HUD — the whole switchboard
@@ -1679,6 +1693,15 @@ interface UISpec {
1679
1693
  name?: string;
1680
1694
  version?: string;
1681
1695
  };
1696
+ /** Buy-feature / bet-mode confirmation. `confirmAboveCost` (× base bet) makes any
1697
+ * play costing MORE than that require a confirm step before it commits — Stake
1698
+ * requires bet modes above 2× to never activate on a single click, so set `2`.
1699
+ * The game routes a purchase/activation through `hud.requestBuyFeature(...)`, which
1700
+ * shows the confirm popup itself when the cost exceeds the threshold. A jurisdiction
1701
+ * can set the same threshold (`JurisdictionConfig.confirmBuyAboveCost`). */
1702
+ buyFeature?: {
1703
+ confirmAboveCost?: number;
1704
+ };
1682
1705
  /** Reality-check reminder (RTS 13): open-ui runs a wall-clock timer, emits a
1683
1706
  * `realityCheck` event, stops autoplay, and (unless `showModal:false`) shows an
1684
1707
  * acknowledge modal. You supply only the cadence + text. `title`/`message` are
@@ -1791,14 +1814,26 @@ interface SocialPhraseIssue {
1791
1814
  }
1792
1815
  /** Every restricted phrase in `text` (empty when clean). Pure + total. */
1793
1816
  declare function findForbiddenPhrases(text: string): ForbiddenMatch[];
1817
+ /** A collected string. `text` is the base English (what renders with no social
1818
+ * override); `key` is the i18n key used to look up a social override — it defaults
1819
+ * to `text`, since a game's menu/rules copy doubles as its own key. */
1794
1820
  type Entry = {
1795
1821
  source: string;
1796
1822
  text: string;
1823
+ key?: string;
1797
1824
  };
1798
1825
  /** Collect every English string a spec ships (menu/rules blocks, titles, labels+hints,
1799
1826
  * game name, the `en` dictionary) with a source path — the input to {@link checkSocialPhrases}. */
1800
1827
  declare function collectSocialCopy(spec: UISpec): Entry[];
1801
- /** Scan a whole spec's English copy and return every restricted-phrase finding. */
1828
+ /**
1829
+ * Scan a spec's English copy AS IT RENDERS IN SOCIAL MODE and return every restricted
1830
+ * phrase. Each collected string is first resolved through the social dictionary
1831
+ * (`locale.socialMessages.en` → built-in `openuiSocialDefaults` → the base text) —
1832
+ * exactly like the translator does — so a game that already swaps "bet"/"pay(table)"
1833
+ * for compliant wording in social mode is NOT flagged, while a phrase with NO social
1834
+ * override (the classic slipped-through "paytable") IS. English by design: the check
1835
+ * targets the base `en` copy that every jurisdiction review starts from.
1836
+ */
1802
1837
  declare function checkSocialPhrases(spec: UISpec): SocialPhraseIssue[];
1803
1838
 
1804
1839
  /**
package/dist/index.js CHANGED
@@ -1247,6 +1247,9 @@ function validateSpec(spec) {
1247
1247
  if (spec.realityCheck != null && !(typeof spec.realityCheck.everyMinutes === "number" && spec.realityCheck.everyMinutes > 0)) {
1248
1248
  add("error", "realityCheck.everyMinutes", "bad-interval", "realityCheck.everyMinutes must be a number > 0");
1249
1249
  }
1250
+ if (spec.buyFeature?.confirmAboveCost != null && !(typeof spec.buyFeature.confirmAboveCost === "number" && Number.isFinite(spec.buyFeature.confirmAboveCost) && spec.buyFeature.confirmAboveCost >= 0)) {
1251
+ add("error", "buyFeature.confirmAboveCost", "bad-threshold", "buyFeature.confirmAboveCost must be a finite number >= 0");
1252
+ }
1250
1253
  if (spec.controls) {
1251
1254
  for (const [id, ov] of Object.entries(spec.controls)) {
1252
1255
  checkLayout(ov.layout?.anchor, `controls.${id}.layout.anchor`);
@@ -1399,6 +1402,9 @@ function applyJurisdiction(ui, jur) {
1399
1402
  if (typeof jur.minimumRoundDuration === "number" && Number.isFinite(jur.minimumRoundDuration)) {
1400
1403
  ui.minimumRoundDuration = Math.max(0, jur.minimumRoundDuration);
1401
1404
  }
1405
+ if (typeof jur.confirmBuyAboveCost === "number" && Number.isFinite(jur.confirmBuyAboveCost)) {
1406
+ ui.confirmBuyAboveCost = Math.max(0, jur.confirmBuyAboveCost);
1407
+ }
1402
1408
  }
1403
1409
 
1404
1410
  // src/notice.ts
@@ -1456,6 +1462,8 @@ var openuiDefaults = {
1456
1462
  "openui.replay": "Replay",
1457
1463
  "openui.buyFeature.title": "Buy feature",
1458
1464
  "openui.buyFeature.message": "Buy this feature now?",
1465
+ // Confirm step for a higher-cost play — {{name}} / {{price}} are interpolated.
1466
+ "openui.buyFeature.confirm": "Buy {{name}} for {{price}}?",
1459
1467
  "openui.freeSpins": "FS",
1460
1468
  // reality check (RTS 13) — {{minutes}} is interpolated by open-ui's scheduler
1461
1469
  "openui.realityCheck.title": "Reality check",
@@ -1491,6 +1499,7 @@ var openuiSocialDefaults = {
1491
1499
  "openui.err.insufficient.title": "Insufficient balance",
1492
1500
  "openui.buyFeature.title": "Play bonus",
1493
1501
  "openui.buyFeature.message": "Play this bonus now?",
1502
+ "openui.buyFeature.confirm": "Play {{name}} for {{price}}?",
1494
1503
  "openui.err.insufficient.message": "You don't have enough balance for this play.",
1495
1504
  "openui.err.limit.message": "A play limit has been reached. Please try again later."
1496
1505
  };
@@ -1671,6 +1680,11 @@ var OpenUI = class {
1671
1680
  gameInfo = {};
1672
1681
  /** Minimum round duration (ms) from jurisdiction — stored for the GAME to enforce. */
1673
1682
  minimumRoundDuration = 0;
1683
+ /** Buy-feature confirm threshold, as a multiple of the base bet. A play (buy or
1684
+ * bet-mode activation) that costs STRICTLY MORE than this must show a confirm step
1685
+ * — Stake requires that bet modes above 2× never activate on a single click. `0`
1686
+ * (the default) = off. Set via `spec.buyFeature.confirmAboveCost` or a jurisdiction. */
1687
+ confirmBuyAboveCost = 0;
1674
1688
  sessionNet = 0;
1675
1689
  prevVolumes = null;
1676
1690
  /** The frozen UISpec `createUI` built this from, if any — authored = run = tested. */
@@ -2020,6 +2034,13 @@ var OpenUI = class {
2020
2034
  const j = this.jurisdiction;
2021
2035
  return feature === "autoplay" && !!j.disabledAutoplay || feature === "buyFeature" && !!j.disabledBuyFeature || feature === "turbo" && !!j.disabledTurbo || feature === "fullscreen" && !!j.disabledFullscreen || feature === "slamstop" && !!j.disabledSlamstop || feature === "spacebar" && !!j.disabledSpacebar;
2022
2036
  }
2037
+ /** True when a buy / bet-mode activation of `cost`× the base bet must be confirmed
2038
+ * before it commits (Stake: no one-click activation above `confirmBuyAboveCost`×).
2039
+ * Always false when the threshold is 0/unset or `cost` isn't a finite number. */
2040
+ shouldConfirmBuy(cost) {
2041
+ const t = this.confirmBuyAboveCost;
2042
+ return t > 0 && Number.isFinite(cost) && cost > t;
2043
+ }
2023
2044
  /**
2024
2045
  * Start a reality-check reminder (RTS 13). Every `everyMinutes` (WALL-CLOCK, so a
2025
2046
  * backgrounded tab can't cheat it) it emits a `realityCheck` event, stops autoplay,
@@ -2251,15 +2272,18 @@ function collectSocialCopy(spec) {
2251
2272
  if (spec.game?.name) out.push({ source: "game.name", text: spec.game.name });
2252
2273
  const en = spec.locale?.messages?.en;
2253
2274
  if (en) {
2254
- for (const [k, v] of Object.entries(en)) if (typeof v === "string") out.push({ source: `locale.en[${JSON.stringify(k)}]`, text: v });
2275
+ for (const [k, v] of Object.entries(en)) if (typeof v === "string") out.push({ source: `locale.en[${JSON.stringify(k)}]`, text: v, key: k });
2255
2276
  }
2256
2277
  return out;
2257
2278
  }
2258
2279
  function checkSocialPhrases(spec) {
2280
+ const socialEn = spec.locale?.socialMessages?.en ?? {};
2281
+ const render = (e) => socialEn[e.key ?? e.text] ?? openuiSocialDefaults[e.key ?? e.text] ?? e.text;
2259
2282
  const issues = [];
2260
- for (const { source, text } of collectSocialCopy(spec)) {
2283
+ for (const e of collectSocialCopy(spec)) {
2284
+ const text = render(e);
2261
2285
  const matches = findForbiddenPhrases(text);
2262
- if (matches.length) issues.push({ source, text, matches });
2286
+ if (matches.length) issues.push({ source: e.source, text, matches });
2263
2287
  }
2264
2288
  return issues;
2265
2289
  }
@@ -2350,6 +2374,9 @@ function createUI(spec = {}, hooks = {}) {
2350
2374
  }
2351
2375
  }
2352
2376
  }
2377
+ if (spec.buyFeature?.confirmAboveCost != null && Number.isFinite(spec.buyFeature.confirmAboveCost)) {
2378
+ ui.confirmBuyAboveCost = Math.max(0, spec.buyFeature.confirmAboveCost);
2379
+ }
2353
2380
  if (spec.jurisdiction) ui.applyJurisdiction(spec.jurisdiction);
2354
2381
  const portraitControls = {};
2355
2382
  for (const [id, layout] of Object.entries(portraitDefaultLayouts)) {