@energy8platform/game-engine 0.34.0 → 0.34.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energy8platform/game-engine",
3
- "version": "0.34.0",
3
+ "version": "0.34.2",
4
4
  "description": "Universal casino game engine built on PixiJS v8 and @energy8platform/game-sdk",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs.js",
@@ -186,11 +186,23 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
186
186
  jurisdiction?: import('./shellConfig').JurisdictionRestrictions;
187
187
  betLevels?: number[];
188
188
  defaultBet?: number;
189
- stake?: { defaultBetLevel?: number };
189
+ stake?: { defaultBetLevel?: number; minBet?: number; maxBet?: number };
190
+ /** Set by the Stake bridge when `/wallet/authenticate` returned a still-open round. */
191
+ activeRound?: { bet?: number; roundId?: string; mode?: string };
190
192
  };
193
+ /** Present only on a resume — the bridge synthesises it from the open round. */
194
+ session?: { betAmount?: number };
191
195
  lang?: string;
192
196
  } | null;
193
197
  const config = initData?.config;
198
+ // A reload mid-round is just another entry: authenticate answers with BOTH the currency's
199
+ // default bet and the round still open from the previous page-load. That round was played at
200
+ // its own stake, so the default is the wrong bet to come back on — the bar would show it while
201
+ // the resume drain (and the ×bet win data the scene renders) ran against something else.
202
+ // `config.activeRound.bet` is the bridge stating it outright; `session.betAmount` is the older
203
+ // carrier for the same value (INIT only ever has a session on a resume). Both are ignored when
204
+ // absent or 0 so an ordinary launch still starts on the default.
205
+ const resumedBet = config?.activeRound?.bet || initData?.session?.betAmount || undefined;
194
206
  const { resolveCurrency } = await import('./shellConfig');
195
207
  // SINGLE source of truth for the symbol: the Stake bridge already puts a full CurrencyMetaData
196
208
  // (symbol + placement) on initData.config.currency. In the non-stake/devBridge path that meta
@@ -217,8 +229,21 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
217
229
  // Currency-specific ladder + per-currency default from /wallet/authenticate (Stake);
218
230
  // absent on dev/devBridge → buildShellConfig falls back to the spec.
219
231
  betLevels: config?.betLevels,
220
- defaultBet: config?.stake?.defaultBetLevel ?? config?.defaultBet,
232
+ defaultBet: resumedBet ?? config?.stake?.defaultBetLevel ?? config?.defaultBet,
233
+ // Hard stake window; the bridge rejects anything outside it before /bet/play.
234
+ minBet: config?.stake?.minBet,
235
+ maxBet: config?.stake?.maxBet,
221
236
  };
237
+ // On a real Stake launch the ladder is CURRENCY-SPECIFIC and mandatory. Falling back to the
238
+ // spec's (EUR-shaped) ladder here would put the game on bets the wallet can't honour — every
239
+ // spin rejected on a high-denomination currency (ARS minBet 50), or silently mispriced. Fail
240
+ // where the cause is visible instead of at the first spin.
241
+ if (isStakeNow && !runtime.betLevels?.length) {
242
+ fatal('Could not load the bet levels for your currency. Please relaunch the game.');
243
+ throw new Error(
244
+ 'createSlotGame: Stake launch returned no config.betLevels — refusing to fall back to the spec ladder',
245
+ );
246
+ }
222
247
  if (opts.dev) {
223
248
  // Dev-only diagnostic. Logged as PLAIN STRINGS (not collapsed objects) so the values are
224
249
  // readable in the console without expanding. If the shown symbol is a bare code ("EUR")
@@ -238,6 +263,12 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
238
263
  app: game.app,
239
264
  parent: game.uiLayer,
240
265
  };
266
+ // Adopt the bet the shell is about to display. `currentBet` was seeded from the SPEC above,
267
+ // which is only ever right by accident: the authoritative default is the per-currency one from
268
+ // /wallet/authenticate. The shell re-syncs us on `betChange`, but that fires ONLY when the
269
+ // player moves the bet — so without this the FIRST spin (and any bonus buy before it) plays at
270
+ // the spec's bet while the bar shows the RGS one.
271
+ currentBet = pixiShellCfg.currentBet ?? currentBet;
241
272
  // The game may swap in its own shell (a custom renderer over the same core) via shellFactory;
242
273
  // default is the built-in Pixi shell. The host drives whichever it gets through the Shell contract.
243
274
  shell = (opts.shellFactory ?? createPixiShell)(pixiShellCfg);
@@ -73,6 +73,40 @@ export interface ShellRuntime {
73
73
  /** Per-currency default bet from `/wallet/authenticate` (the bridge surfaces it as
74
74
  * `config.stake.defaultBetLevel`). Stake requires the selector to start here on every entry. */
75
75
  defaultBet?: number;
76
+ /** Hard per-currency stake bounds from `/wallet/authenticate` (`config.stake.minBet/maxBet`,
77
+ * major units). The bridge REJECTS any bet outside them before `/bet/play`, so the resolved
78
+ * default is clamped into this window — see `resolveDefaultBet`. */
79
+ minBet?: number;
80
+ maxBet?: number;
81
+ }
82
+
83
+ /**
84
+ * Resolve the bet the game must START on — the single source of truth for BOTH the shell's
85
+ * selector and the host's `currentBet` (they must never disagree; if they do, the player is
86
+ * charged a bet they never chose).
87
+ *
88
+ * The `preferred` bet is the RGS default when there is one, else the spec's. Neither is trusted
89
+ * blindly: the result is always an actual rung of `betLevels`, inside `[minBet, maxBet]`.
90
+ *
91
+ * Why snapping matters beyond the bounds check: the shell's `stepBet()` locates the current bet
92
+ * with `availableBets.indexOf(state.bet)`. An off-ladder default makes that return -1, so the very
93
+ * first +/- press jumps the bet to `availableBets[0]` instead of stepping.
94
+ */
95
+ export function resolveDefaultBet(
96
+ betLevels: number[],
97
+ preferred: number | undefined,
98
+ bounds?: { minBet?: number; maxBet?: number },
99
+ ): number {
100
+ if (!betLevels.length) return preferred ?? 0; // no ladder to snap to — nothing better to offer
101
+ // Drop rungs the RGS would reject outright. If the bounds exclude the WHOLE ladder the data is
102
+ // self-contradictory; keep the ladder rather than hand back a bet that isn't even selectable.
103
+ const inBounds = betLevels.filter(
104
+ (l) => (bounds?.minBet == null || l >= bounds.minBet) && (bounds?.maxBet == null || l <= bounds.maxBet),
105
+ );
106
+ const pool = inBounds.length ? inBounds : betLevels;
107
+ if (preferred == null) return pool[0];
108
+ // Nearest rung; ties go to the cheaper one (`<` keeps the earlier, ascending-ladder entry).
109
+ return pool.reduce((best, l) => (Math.abs(l - preferred) < Math.abs(best - preferred) ? l : best), pool[0]);
76
110
  }
77
111
 
78
112
  /** The subset of Stake's jurisdiction flags the shell can enforce via `ShellFeatures`. */
@@ -389,8 +423,19 @@ export function buildShellConfig(
389
423
  ): Omit<PixiShellConfig, 'app' | 'parent' | 'gameInfo'> & { gameInfo: GameInfoContent } {
390
424
  // Prefer the currency-specific ladder from /wallet/authenticate; fall back to the spec (dev/devBridge).
391
425
  const betLevels = runtime.betLevels?.length ? runtime.betLevels : model.spec.betLevels;
392
- // Stake requires the default to come from authenticate on every entry; spec default is the dev fallback.
393
- const defaultBet = runtime.defaultBet ?? model.spec.defaultBet ?? betLevels[0];
426
+ // Stake requires the default to come from authenticate on every entry; spec default is the dev
427
+ // fallback. Snapped onto the ladder + clamped to the RGS bounds so the selector and the host's
428
+ // `currentBet` both start on a bet the server will actually accept.
429
+ // EXCEPT in replay: there the bridge reports the replayed round's OWN stake, a historical amount
430
+ // that need not be a rung of today's ladder. Snapping it would misreport what was actually bet.
431
+ const preferredBet = runtime.defaultBet ?? model.spec.defaultBet;
432
+ const defaultBet =
433
+ runtime.mode === 'replay'
434
+ ? (preferredBet ?? betLevels[0])
435
+ : resolveDefaultBet(betLevels, preferredBet, {
436
+ minBet: runtime.minBet,
437
+ maxBet: runtime.maxBet,
438
+ });
394
439
  // runtime.currency is the resolved CurrencyConfig (derived from initData.config.currency by the
395
440
  // host); opts.currency still wins. Fall back to the spec code, then a neutral euro.
396
441
  const currency =