@energy8platform/game-engine 0.33.8 → 0.33.9

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.33.8",
3
+ "version": "0.33.9",
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",
@@ -94,7 +94,7 @@
94
94
  },
95
95
  "dependencies": {
96
96
  "@energy8platform/platform-core": ">=0.30.1",
97
- "@energy8platform/shell": ">=0.6.0"
97
+ "@energy8platform/shell": ">=0.6.6"
98
98
  },
99
99
  "peerDependencies": {
100
100
  "@energy8platform/game-sdk": "^2.9.0",
@@ -298,6 +298,12 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
298
298
  overlayCtl.resize(width, height),
299
299
  );
300
300
 
301
+ // Progressive WIN readout for cascade/tumble scenes. The window is open from the moment a
302
+ // segment starts (WIN cleared to 0) until the host has painted that segment's final win; outside
303
+ // it the host owns the readout alone. See winReporter.ts.
304
+ const { createWinReporter } = await import('./winReporter');
305
+ const winReporter = createWinReporter((amount, opts) => shell!.setWin(amount, opts));
306
+
301
307
  // Capabilities injected once per controller scene via onCreate (see `gameScene`/`ensureCreated`).
302
308
  sceneApi = {
303
309
  audio: createSceneAudio(game.audio),
@@ -306,6 +312,7 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
306
312
  get safeArea() {
307
313
  return shell!.safeArea;
308
314
  },
315
+ reportWin: winReporter.report,
309
316
  },
310
317
  formatAmount: (v) => shell!.formatWin(v),
311
318
  get bet() {
@@ -562,12 +569,15 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
562
569
  // then counts UP to this segment's delta. prevWin (the cumulative-delta tracker) is
563
570
  // untouched; this only resets the DISPLAY.
564
571
  shell!.setWin(0, { animate: false });
572
+ winReporter.open(); // the scene may now grow WIN per cascade step
565
573
  },
566
574
  onSpinStart: () => scene.onSpinStart?.(),
567
575
  onSpinEnd: (last, ctx) => scene.onSpinEnd?.(last, ctx),
568
576
  afterPresent: (r) => {
569
577
  // WIN readout = THIS spin's win (cumulative delta); the cumulative total goes to the
570
- // bonus counter (totalWin) via settle(), not the WIN readout.
578
+ // bonus counter (totalWin) via settle(), not the WIN readout. A scene that reported
579
+ // per-step wins already landed on this number, so the count-up is a no-op there.
580
+ winReporter.close();
571
581
  shell!.setWin(r.totalWin - prevWin);
572
582
  prevWin = r.totalWin;
573
583
  balanceGate.afterPresent();
@@ -581,6 +591,7 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
581
591
  .catch(showPlayError)
582
592
  .finally(() => {
583
593
  presenting = false;
594
+ winReporter.close(); // also closes the window when a segment threw mid-flight
584
595
  shell!.setBusy(false);
585
596
  });
586
597
  };
@@ -627,11 +638,13 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
627
638
  shell!.setMode(bonusShellMode);
628
639
  }
629
640
  shell!.setWin(0, { animate: false }); // clear WIN before this segment animates (see playRound)
641
+ if (animate) winReporter.open(); // a fast-forward drain doesn't present → no reports expected
630
642
  if (animate) await scene.onSpin(r, ctx);
631
643
  if (inBonus) {
632
644
  const v = fsView(raw, r.totalWin);
633
645
  if (v) applyBonusReadout(r, v, ctx.mode);
634
646
  }
647
+ winReporter.close();
635
648
  shell!.setWin(r.totalWin - prevWin); // THIS spin's win, not the cumulative bonus total
636
649
  prevWin = r.totalWin;
637
650
  ps!.playAck(raw); // settles via /wallet/end-round on the FINAL segment
@@ -655,6 +668,7 @@ export async function createSlotGame<T extends SlotSpinResultBase = SlotSpinResu
655
668
  shell!.setWin(r.totalWin);
656
669
  }
657
670
  } finally {
671
+ winReporter.close(); // also closes the window when a drained segment threw
658
672
  shell!.setBusy(false);
659
673
  }
660
674
  };
package/src/host/index.ts CHANGED
@@ -25,6 +25,9 @@ export type {
25
25
  SlotSceneController, RenderContext, SceneApi, SceneAudio, SceneOverlay, SceneShell,
26
26
  OverlayShowOptions, AutoplaySceneState,
27
27
  } from './sceneController';
28
+ // Progressive (cascade/tumble) WIN readout — the gate behind `api.shell.reportWin`.
29
+ export { createWinReporter } from './winReporter';
30
+ export type { WinReporter, WinReportOptions } from './winReporter';
28
31
  // Social-casino word-swap. The shell auto-socializes all gameInfo/buyBonus text in social mode;
29
32
  // authors only need this to socialize strings they render themselves (e.g. inside a custom DOM node).
30
33
  export { socialize } from '@energy8platform/shell';
@@ -1,5 +1,6 @@
1
1
  import type { Container } from 'pixi.js';
2
2
  import type { SlotSpinResultBase } from '@energy8platform/platform-core/slot-result';
3
+ import type { WinReportOptions } from './winReporter';
3
4
 
4
5
  /** Everything a scene needs to render one segment. The host builds it per segment. */
5
6
  export interface RenderContext {
@@ -57,6 +58,25 @@ export interface SceneOverlay {
57
58
  export interface SceneShell {
58
59
  /** Live insets (px). `bottom` = the shell bar height; read inside onResize. */
59
60
  readonly safeArea: { top: number; right: number; bottom: number; left: number };
61
+ /**
62
+ * Grow the shell's WIN readout WHILE a segment presents — for cascade/tumble games that pay in
63
+ * steps instead of one lump at the end.
64
+ *
65
+ * `amountSoFar` is ABSOLUTE: the win accumulated by this segment up to now (not the step's
66
+ * delta), so a re-report, a skip, or an aborted step can just restate the truth. The host owns
67
+ * the readout around it — it clears WIN to 0 when the segment starts and sets the segment's
68
+ * final value once `onSpin` resolves (counting up from your last report, so matching numbers
69
+ * produce no jump). In a bonus this moves WIN only; the Total Win accumulator still lands once
70
+ * per segment.
71
+ *
72
+ * Only honoured while a segment is presenting (inside `onSpin`); calls from anywhere else are
73
+ * ignored, so a scene still ticking after an abort can't overwrite the host's final number.
74
+ *
75
+ * `durationMs` sets this count-up's length (default 450ms) — pass your step length (or a shorter
76
+ * one under turbo) so each count-up finishes before the next step lands. `{ animate: false }`
77
+ * snaps, e.g. when collapsing to the final value on skip.
78
+ */
79
+ reportWin(amountSoFar: number, opts?: WinReportOptions): void;
60
80
  }
61
81
 
62
82
  export interface AutoplaySceneState {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Gate for the PROGRESSIVE (cascade / tumble) WIN readout.
3
+ *
4
+ * By default the WIN readout is entirely host-driven: cleared to 0 when a segment starts, set to
5
+ * that segment's win once `onSpin` resolves. A cascade game pays in steps, so its scene wants the
6
+ * number to climb WHILE the segment presents — `api.shell.reportWin(amountSoFar)`.
7
+ *
8
+ * The gate keeps the host in charge:
9
+ * - reports are honoured ONLY between `open()` (segment start, WIN already cleared) and `close()`
10
+ * (the host paints the segment's final value) — a scene still animating after an abort, or one
11
+ * reporting from a mode transition, can't overwrite the host's number,
12
+ * - `amountSoFar` is ABSOLUTE (the segment's win up to now, not the step delta), so a re-report or
13
+ * a collapse-to-final on skip is idempotent. Reports should be non-decreasing within a segment;
14
+ * a lower value counts the readout back DOWN,
15
+ * - garbage (NaN / Infinity) is dropped and negatives clamp to 0, so a math bug can't paint "NaN"
16
+ * into the bar.
17
+ *
18
+ * Pure + unit-testable: the shell paint is injected.
19
+ */
20
+ export interface WinReportOptions {
21
+ /** `false` snaps instead of counting up (e.g. collapsing to the final value on skip). */
22
+ animate?: boolean;
23
+ /** Count-up length in ms (shell default 450) — pass the cascade step's length. */
24
+ durationMs?: number;
25
+ }
26
+
27
+ export interface WinReporter {
28
+ /** Scene-facing (`api.shell.reportWin`): the win accumulated by the presenting segment so far. */
29
+ report(amountSoFar: number, opts?: WinReportOptions): void;
30
+ /** A segment starts presenting — the scene may now grow WIN. */
31
+ open(): void;
32
+ /** The host takes the readout back (final value, round end, or an error). */
33
+ close(): void;
34
+ /** True while reports are honoured. */
35
+ readonly accepting: boolean;
36
+ }
37
+
38
+ export function createWinReporter(
39
+ paint: (amount: number, opts?: WinReportOptions) => void,
40
+ ): WinReporter {
41
+ let accepting = false;
42
+ return {
43
+ report(amountSoFar: number, opts?: WinReportOptions): void {
44
+ if (!accepting || !Number.isFinite(amountSoFar)) return;
45
+ paint(Math.max(0, amountSoFar), opts);
46
+ },
47
+ open(): void {
48
+ accepting = true;
49
+ },
50
+ close(): void {
51
+ accepting = false;
52
+ },
53
+ get accepting(): boolean {
54
+ return accepting;
55
+ },
56
+ };
57
+ }
package/src/slot/index.ts CHANGED
@@ -73,7 +73,7 @@ export { FEATURES, FEATURE_LIST } from './features';
73
73
  export type { ReelFeature, FeatureContext } from './features';
74
74
 
75
75
  export { createReelSystem } from './system/ReelSystem';
76
- export type { ReelSystem, CreateReelSystemOptions } from './system/ReelSystem';
76
+ export type { ReelSystem, CreateReelSystemOptions, StepRunOpts } from './system/ReelSystem';
77
77
 
78
78
  export { pickTier, tierIndexAtValue } from './overlay/tiers';
79
79
  export type { WinTier } from './overlay/tiers';
@@ -35,6 +35,19 @@ export interface CreateReelSystemOptions {
35
35
  features?: ReelFeature[];
36
36
  }
37
37
 
38
+ /** Options for a stepped chain (`cascade` / `reelStep`). */
39
+ export interface StepRunOpts<TStep> {
40
+ turbo?: boolean;
41
+ /** Carry the running multiplier across free spins (with `cascade.multiplier.persistInFreeSpins`). */
42
+ freeSpins?: boolean;
43
+ /**
44
+ * Fires after each step has settled, with the step's index and the running multiplier AFTER it.
45
+ * The hook for a step-by-step payout readout: report the win accrued so far to the shell via
46
+ * `api.shell.reportWin(...)`. Awaited, so an async hook paces the chain.
47
+ */
48
+ onStep?: (index: number, step: TStep, multiplier: number) => void | Promise<void>;
49
+ }
50
+
38
51
  export interface ReelSystem {
39
52
  /** Root container — add this to your scene. */
40
53
  readonly view: Container;
@@ -52,14 +65,16 @@ export interface ReelSystem {
52
65
  /** Replace the whole config. */
53
66
  setConfig(config: ReelSystemConfig): void;
54
67
  spin(target: CellData[][], opts?: SpinRunOpts): Promise<void>;
55
- /** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier carries over instead of resetting. */
56
- cascade(steps: TumbleStep[], opts?: { turbo?: boolean; freeSpins?: boolean }): Promise<void>;
68
+ /** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier
69
+ * carries over instead of resetting. Generic in the step type, so `onStep` hands back the game's
70
+ * own step (with its per-step win) rather than the bare TumbleStep. */
71
+ cascade<TStep extends TumbleStep>(steps: TStep[], opts?: StepRunOpts<TStep>): Promise<void>;
57
72
  /**
58
73
  * Run a ReelStep™ chain: each step pays its winning cells, then scrolls every reel down by
59
74
  * `shifts[col]` positions (0 = reel stays put). Multiplier carries over with `freeSpins` +
60
75
  * `cascade.multiplier.persistInFreeSpins`, same as `cascade`.
61
76
  */
62
- reelStep(steps: ReelStepData[], opts?: { turbo?: boolean; freeSpins?: boolean }): Promise<void>;
77
+ reelStep<TStep extends ReelStepData>(steps: TStep[], opts?: StepRunOpts<TStep>): Promise<void>;
63
78
  /** Current running cascade / reel-step multiplier. */
64
79
  readonly multiplier: number;
65
80
  /** Register a custom feature (or override a built-in by reusing its key). */
@@ -252,6 +267,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
252
267
  for (let i = 0; i < steps.length; i++) {
253
268
  await tumble.step(steps[i], i, cOpts);
254
269
  board = steps[i].settledGrid;
270
+ await cOpts?.onStep?.(i, steps[i], tumble.multiplier);
255
271
  }
256
272
  if (config.cascade.multiplier.enabled) log?.(`Cascade multiplier ×${tumble.multiplier}`);
257
273
  },
@@ -262,6 +278,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
262
278
  for (let i = 0; i < steps.length; i++) {
263
279
  await reelStepCtl.step(steps[i], i, rOpts);
264
280
  board = steps[i].settledGrid;
281
+ await rOpts?.onStep?.(i, steps[i], reelStepCtl.multiplier);
265
282
  }
266
283
  if (config.cascade.multiplier.enabled)
267
284
  log?.(`ReelStep multiplier ×${reelStepCtl.multiplier}`);