@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/dist/host.cjs.js +41 -1
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +58 -2
- package/dist/host.esm.js +41 -2
- package/dist/host.esm.js.map +1 -1
- package/dist/slot.cjs.js +2 -0
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +18 -10
- package/dist/slot.esm.js +2 -0
- package/dist/slot.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/host/createSlotGame.ts +15 -1
- package/src/host/index.ts +3 -0
- package/src/host/sceneController.ts +20 -0
- package/src/host/winReporter.ts +57 -0
- package/src/slot/index.ts +1 -1
- package/src/slot/system/ReelSystem.ts +20 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@energy8platform/game-engine",
|
|
3
|
-
"version": "0.33.
|
|
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.
|
|
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
|
|
56
|
-
|
|
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:
|
|
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}`);
|