@energy8platform/game-engine 0.19.0 → 0.21.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/README.md +121 -0
- package/dist/animation.cjs.js +18 -3
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.esm.js +18 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/core.cjs.js +51 -159
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +52 -160
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +88 -189
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.esm.js +90 -191
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +51 -159
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/index.esm.js +52 -160
- package/dist/index.esm.js.map +1 -1
- package/dist/react.cjs.js +18 -3
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.esm.js +18 -3
- package/dist/react.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -31
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -5
- package/dist/slot.esm.js +1857 -33
- package/dist/slot.esm.js.map +1 -1
- package/dist/ui.cjs.js +18 -3
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +18 -3
- package/dist/ui.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/animation/Tween.ts +14 -3
- package/src/core/GameApplication.ts +6 -5
- package/src/host/shellConfig.ts +44 -31
- package/src/loading/LoadingScene.ts +31 -174
- package/src/slot/anim/easing-map.ts +16 -2
- package/src/slot/cascade/TumbleController.ts +230 -0
- package/src/slot/config/ReelSystemConfig.ts +559 -0
- package/src/slot/config/presets.ts +222 -0
- package/src/slot/features/extra.ts +189 -0
- package/src/slot/features/index.ts +44 -0
- package/src/slot/features/symbols.ts +183 -0
- package/src/slot/features/types.ts +136 -0
- package/src/slot/features/wilds.ts +151 -0
- package/src/slot/grid/ReelGrid.ts +93 -24
- package/src/slot/grid/SymbolCell.ts +45 -11
- package/src/slot/index.ts +61 -0
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/cascade/TumbleController.ts
|
|
2
|
+
//
|
|
3
|
+
// Richer cascade/tumble than the back-compat CascadeController: animates surviving symbols
|
|
4
|
+
// sliding into the gaps (gravity), supports per-step deceleration (tension build), dimming of
|
|
5
|
+
// non-winning symbols, and a running win multiplier (Pragmatic Tumble / NetEnt Avalanche style).
|
|
6
|
+
//
|
|
7
|
+
// Presentation only — the settled board is provided by the caller.
|
|
8
|
+
|
|
9
|
+
import { Tween } from '../../animation';
|
|
10
|
+
import { easingByName } from '../anim/easing-map';
|
|
11
|
+
import type { ReelGrid } from '../grid/ReelGrid';
|
|
12
|
+
import type { CellData } from '../grid/SymbolCell';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_REEL_CONFIG,
|
|
15
|
+
type CascadeConfig,
|
|
16
|
+
type WinConfig,
|
|
17
|
+
} from '../config/ReelSystemConfig';
|
|
18
|
+
|
|
19
|
+
export interface TumbleStep {
|
|
20
|
+
winningCells: { col: number; row: number }[];
|
|
21
|
+
removedCells: { col: number; row: number }[];
|
|
22
|
+
/** Optional explicit survivor slides (col, fromRow→toRow). When omitted, gravity is derived. */
|
|
23
|
+
drops?: { col: number; fromRow: number; toRow: number }[];
|
|
24
|
+
newCells: { col: number; row: number; symbol: string }[];
|
|
25
|
+
settledGrid: CellData[][];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class TumbleController {
|
|
29
|
+
private _grid: ReelGrid;
|
|
30
|
+
private _cfg: CascadeConfig;
|
|
31
|
+
private _win: WinConfig = DEFAULT_REEL_CONFIG.win;
|
|
32
|
+
private _killed = false;
|
|
33
|
+
private _mult: number;
|
|
34
|
+
|
|
35
|
+
constructor(grid: ReelGrid, cfg: CascadeConfig, win?: WinConfig) {
|
|
36
|
+
this._grid = grid;
|
|
37
|
+
this._cfg = cfg;
|
|
38
|
+
this._mult = cfg.multiplier.start;
|
|
39
|
+
if (win) this._win = win;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setConfig(cfg: CascadeConfig): void {
|
|
43
|
+
this._cfg = cfg;
|
|
44
|
+
}
|
|
45
|
+
setWin(win: WinConfig): void {
|
|
46
|
+
this._win = win;
|
|
47
|
+
}
|
|
48
|
+
/** Killed, or the grid was torn down underneath us (rebuild during a cascade). */
|
|
49
|
+
private get _dead(): boolean {
|
|
50
|
+
return this._killed || this._grid.destroyed;
|
|
51
|
+
}
|
|
52
|
+
get multiplier(): number {
|
|
53
|
+
return this._mult;
|
|
54
|
+
}
|
|
55
|
+
resetMultiplier(): void {
|
|
56
|
+
this._mult = this._cfg.multiplier.start;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private advanceMultiplier(): void {
|
|
60
|
+
const m = this._cfg.multiplier;
|
|
61
|
+
if (!m.enabled) return;
|
|
62
|
+
const next = m.mode === 'mul' ? this._mult * m.step : this._mult + m.step;
|
|
63
|
+
this._mult = m.cap != null ? Math.min(next, m.cap) : next;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Derive survivor slides for one column when explicit drops aren't supplied. */
|
|
67
|
+
private deriveDrops(step: TumbleStep): { col: number; fromRow: number; toRow: number }[] {
|
|
68
|
+
const removedByCol = new Map<number, Set<number>>();
|
|
69
|
+
for (const r of step.removedCells) {
|
|
70
|
+
if (!removedByCol.has(r.col)) removedByCol.set(r.col, new Set());
|
|
71
|
+
removedByCol.get(r.col)!.add(r.row);
|
|
72
|
+
}
|
|
73
|
+
const out: { col: number; fromRow: number; toRow: number }[] = [];
|
|
74
|
+
for (const [col, removed] of removedByCol) {
|
|
75
|
+
const rows = this._grid.rowsOf(col);
|
|
76
|
+
// survivors keep order and fall to the bottom; count holes below each survivor
|
|
77
|
+
const survivors: number[] = [];
|
|
78
|
+
for (let r = 0; r < rows; r++) if (!removed.has(r)) survivors.push(r);
|
|
79
|
+
const newCount = rows - survivors.length;
|
|
80
|
+
survivors.forEach((fromRow, i) => {
|
|
81
|
+
const toRow = newCount + i;
|
|
82
|
+
if (toRow !== fromRow) out.push({ col, fromRow, toRow });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Run a single cascade step. `stepIndex` drives per-step deceleration. */
|
|
89
|
+
async step(step: TumbleStep, stepIndex = 0, opts?: { turbo?: boolean }): Promise<void> {
|
|
90
|
+
if (this._grid.destroyed) return;
|
|
91
|
+
if (!this._cfg.enabled) {
|
|
92
|
+
this._grid.setGrid(step.settledGrid);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this._killed = false;
|
|
96
|
+
const turbo = opts?.turbo ? 0.5 : 1;
|
|
97
|
+
const decel = Math.min(this._cfg.perStepDecelCap, 1 + stepIndex * this._cfg.perStepDecel);
|
|
98
|
+
const f = turbo * decel;
|
|
99
|
+
const t = this._cfg.timings;
|
|
100
|
+
|
|
101
|
+
// 1. highlight winners (+ dim others). win.highlightScale / win.glow drive the pop.
|
|
102
|
+
const winSet = new Set(step.winningCells.map((w) => `${w.col}:${w.row}`));
|
|
103
|
+
const dimmed = this._cfg.dimNonWinners && step.winningCells.length > 0;
|
|
104
|
+
if (dimmed) this._dim(winSet);
|
|
105
|
+
const hs = this._win.highlightScale;
|
|
106
|
+
await Promise.all(
|
|
107
|
+
step.winningCells.map((w) => {
|
|
108
|
+
const cell = this._grid.getCell(w.col, w.row);
|
|
109
|
+
if (this._win.glow) cell.setState({ winning: true });
|
|
110
|
+
return Tween.to(
|
|
111
|
+
cell,
|
|
112
|
+
{ 'scale.x': hs, 'scale.y': hs },
|
|
113
|
+
t.highlight * f,
|
|
114
|
+
easingByName(this._cfg.easings.highlight),
|
|
115
|
+
);
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
if (this._dead) {
|
|
119
|
+
if (dimmed) this._undim();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await Tween.delay(t.wait * f);
|
|
123
|
+
|
|
124
|
+
// 2. remove the cleared cells (removedCells, falling back to winningCells)
|
|
125
|
+
const cleared = step.removedCells.length ? step.removedCells : step.winningCells;
|
|
126
|
+
await Promise.all(
|
|
127
|
+
cleared.map((w) => {
|
|
128
|
+
const cell = this._grid.getCell(w.col, w.row);
|
|
129
|
+
return Tween.to(
|
|
130
|
+
cell,
|
|
131
|
+
{ 'scale.x': 0, 'scale.y': 0, alpha: 0 },
|
|
132
|
+
t.remove * f,
|
|
133
|
+
easingByName(this._cfg.easings.remove),
|
|
134
|
+
);
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
if (this._dead) {
|
|
138
|
+
if (dimmed) this._undim();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const w of cleared) {
|
|
142
|
+
const cell = this._grid.getCell(w.col, w.row);
|
|
143
|
+
cell.setData({ symbol: null });
|
|
144
|
+
cell.setState({});
|
|
145
|
+
cell.scale.set(1);
|
|
146
|
+
cell.alpha = 1;
|
|
147
|
+
}
|
|
148
|
+
this._undim();
|
|
149
|
+
|
|
150
|
+
// 3. gravity: survivors slide down, new cells drop from above
|
|
151
|
+
const rowStep = this._grid.cellPosition(0, 1).y - this._grid.cellPosition(0, 0).y;
|
|
152
|
+
const slides = this._cfg.gravity ? (step.drops ?? this.deriveDrops(step)) : [];
|
|
153
|
+
const anims: Promise<void>[] = [];
|
|
154
|
+
|
|
155
|
+
for (const d of slides) {
|
|
156
|
+
const cell = this._grid.getCell(d.col, d.toRow);
|
|
157
|
+
const home = this._grid.cellPosition(d.col, d.toRow);
|
|
158
|
+
cell.setData(step.settledGrid[d.col]?.[d.toRow] ?? { symbol: null });
|
|
159
|
+
cell.alpha = 1;
|
|
160
|
+
cell.scale.set(1);
|
|
161
|
+
cell.position.set(home.x, this._grid.cellPosition(d.col, d.fromRow).y);
|
|
162
|
+
anims.push(
|
|
163
|
+
Tween.to(cell, { 'position.y': home.y }, t.drop * f, easingByName(this._cfg.easings.drop)),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const perCol: Record<number, number> = {};
|
|
168
|
+
for (const n of step.newCells) {
|
|
169
|
+
const cell = this._grid.getCell(n.col, n.row);
|
|
170
|
+
const home = this._grid.cellPosition(n.col, n.row);
|
|
171
|
+
cell.setData({ symbol: n.symbol });
|
|
172
|
+
cell.setState({ fresh: true });
|
|
173
|
+
cell.alpha = 1;
|
|
174
|
+
cell.scale.set(1);
|
|
175
|
+
cell.position.set(home.x, home.y - rowStep * (this._grid.rowsOf(n.col) + 1));
|
|
176
|
+
const idx = (perCol[n.col] = (perCol[n.col] ?? 0) + 1);
|
|
177
|
+
anims.push(
|
|
178
|
+
(async () => {
|
|
179
|
+
await Tween.delay(idx * 28 * f);
|
|
180
|
+
if (this._dead) return;
|
|
181
|
+
await Tween.to(
|
|
182
|
+
cell,
|
|
183
|
+
{ 'position.y': home.y },
|
|
184
|
+
t.refill * f,
|
|
185
|
+
easingByName(this._cfg.easings.drop),
|
|
186
|
+
);
|
|
187
|
+
cell.setState({});
|
|
188
|
+
})(),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
await Promise.all(anims);
|
|
192
|
+
if (this._dead) return;
|
|
193
|
+
|
|
194
|
+
// 4. normalise + advance multiplier
|
|
195
|
+
this._grid.setGrid(step.settledGrid);
|
|
196
|
+
this._resetPositions();
|
|
197
|
+
if (step.winningCells.length) this.advanceMultiplier();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _dim(winSet: Set<string>): void {
|
|
201
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
202
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++)
|
|
203
|
+
if (!winSet.has(`${c}:${r}`)) this._grid.getCell(c, r).alpha = this._cfg.dimAlpha;
|
|
204
|
+
}
|
|
205
|
+
private _undim(): void {
|
|
206
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
207
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
208
|
+
const cell = this._grid.getCell(c, r);
|
|
209
|
+
if (cell.alpha !== 0) cell.alpha = 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
private _resetPositions(): void {
|
|
213
|
+
if (this._grid.destroyed) return;
|
|
214
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
215
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
216
|
+
const cell = this._grid.getCell(c, r);
|
|
217
|
+
const { x, y } = this._grid.cellPosition(c, r);
|
|
218
|
+
cell.position.set(x, y);
|
|
219
|
+
cell.scale.set(1);
|
|
220
|
+
cell.alpha = 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
skip(): void {
|
|
225
|
+
this._killed = true;
|
|
226
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
227
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) Tween.killTweensOf(this._grid.getCell(c, r));
|
|
228
|
+
this._resetPositions();
|
|
229
|
+
}
|
|
230
|
+
}
|