@energy8platform/game-engine 0.32.3 → 0.32.4
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/slot.cjs.js +248 -1
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +77 -3
- package/dist/slot.esm.js +247 -2
- package/dist/slot.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/slot/cascade/ReelStepController.ts +270 -0
- package/src/slot/grid/SymbolCell.ts +7 -0
- package/src/slot/index.ts +2 -0
- package/src/slot/system/ReelSystem.ts +27 -2
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/cascade/ReelStepController.ts
|
|
2
|
+
//
|
|
3
|
+
// ReelStep™ mechanic. Flow: reels stop → winning lines are paid → each reel scrolls DOWN by N
|
|
4
|
+
// positions (N = winning symbols that played on that reel) → the board is re-evaluated → repeat,
|
|
5
|
+
// until no wins remain. Unlike a cascade/tumble, nothing is removed: the existing symbols ride
|
|
6
|
+
// down and N fresh symbols enter from the top. Reels with N=0 stay put; each reel moves
|
|
7
|
+
// independently by its own N.
|
|
8
|
+
//
|
|
9
|
+
// Presentation only — the caller supplies each step's per-reel shift vector and the post-shift
|
|
10
|
+
// board. Fits classic fixed-line grids (5×3, 5×4, 5×5, …), not ways/cluster.
|
|
11
|
+
|
|
12
|
+
import { Container } from 'pixi.js';
|
|
13
|
+
import { Tween } from '../../animation';
|
|
14
|
+
import { easingByName } from '../anim/easing-map';
|
|
15
|
+
import type { ReelGrid } from '../grid/ReelGrid';
|
|
16
|
+
import { SymbolCell, type CellData } from '../grid/SymbolCell';
|
|
17
|
+
import type { SymbolResolver } from '../grid/SymbolView';
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_REEL_CONFIG,
|
|
20
|
+
type CascadeConfig,
|
|
21
|
+
type WinConfig,
|
|
22
|
+
} from '../config/ReelSystemConfig';
|
|
23
|
+
|
|
24
|
+
export interface ReelStepData {
|
|
25
|
+
/** Cells that won on the current board — highlighted/paid before the shift. */
|
|
26
|
+
winningCells: { col: number; row: number }[];
|
|
27
|
+
/** How far to scroll each reel down (length = cols). 0 = the reel stays put. */
|
|
28
|
+
shifts: number[];
|
|
29
|
+
/** Board after every reel has scrolled down by shifts[col]. */
|
|
30
|
+
settledGrid: CellData[][];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PURE: lay out one reel's scroll tape (top→bottom). The `shift` fresh symbols (the top of the
|
|
35
|
+
* settled reel) stack above the reel's current symbols; the tape starts `shift` cells high so the
|
|
36
|
+
* current symbols fill the window, then slides down by `shift` to reveal the fresh ones. `shift` is
|
|
37
|
+
* clamped to the visible window height. Returns the stacked cells and the start offset (in cells,
|
|
38
|
+
* relative to row 0) the tape animates from.
|
|
39
|
+
*/
|
|
40
|
+
export function buildReelStepTape(
|
|
41
|
+
before: CellData[],
|
|
42
|
+
settledCol: CellData[],
|
|
43
|
+
shift: number,
|
|
44
|
+
): { stack: CellData[]; shift: number; startOffsetCells: number } {
|
|
45
|
+
const rows = before.length;
|
|
46
|
+
const s = Math.max(0, Math.min(shift, rows));
|
|
47
|
+
const incoming = Array.from({ length: s }, (_, i) => settledCol[i] ?? { symbol: null });
|
|
48
|
+
return { stack: [...incoming, ...before], shift: s, startOffsetCells: 0 - s };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class ReelStepController {
|
|
52
|
+
private _grid: ReelGrid;
|
|
53
|
+
private _resolve: SymbolResolver;
|
|
54
|
+
private _cfg: CascadeConfig;
|
|
55
|
+
private _win: WinConfig = DEFAULT_REEL_CONFIG.win;
|
|
56
|
+
private _killed = false;
|
|
57
|
+
private _mult: number;
|
|
58
|
+
private _temp: Container[] = [];
|
|
59
|
+
/** Board the in-flight step settles to — used to snap on skip(). */
|
|
60
|
+
private _pending: CellData[][] | null = null;
|
|
61
|
+
|
|
62
|
+
constructor(grid: ReelGrid, resolve: SymbolResolver, cfg: CascadeConfig, win?: WinConfig) {
|
|
63
|
+
this._grid = grid;
|
|
64
|
+
this._resolve = resolve;
|
|
65
|
+
this._cfg = cfg;
|
|
66
|
+
this._mult = cfg.multiplier.start;
|
|
67
|
+
if (win) this._win = win;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setConfig(cfg: CascadeConfig): void {
|
|
71
|
+
this._cfg = cfg;
|
|
72
|
+
}
|
|
73
|
+
setWin(win: WinConfig): void {
|
|
74
|
+
this._win = win;
|
|
75
|
+
}
|
|
76
|
+
/** Killed, or the grid was torn down underneath us (rebuild mid-chain). */
|
|
77
|
+
private get _dead(): boolean {
|
|
78
|
+
return this._killed || this._grid.destroyed;
|
|
79
|
+
}
|
|
80
|
+
get multiplier(): number {
|
|
81
|
+
return this._mult;
|
|
82
|
+
}
|
|
83
|
+
resetMultiplier(): void {
|
|
84
|
+
this._mult = this._cfg.multiplier.start;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private advanceMultiplier(): void {
|
|
88
|
+
const m = this._cfg.multiplier;
|
|
89
|
+
if (!m.enabled) return;
|
|
90
|
+
const next = m.mode === 'mul' ? this._mult * m.step : this._mult + m.step;
|
|
91
|
+
this._mult = m.cap != null ? Math.min(next, m.cap) : next;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Run one ReelStep: pay the winning cells, then scroll each reel down by shifts[col]. */
|
|
95
|
+
async step(step: ReelStepData, stepIndex = 0, opts?: { turbo?: boolean }): Promise<void> {
|
|
96
|
+
if (this._grid.destroyed) return;
|
|
97
|
+
this._killed = false;
|
|
98
|
+
this._pending = step.settledGrid;
|
|
99
|
+
|
|
100
|
+
// 1. celebrate/pay the winning cells.
|
|
101
|
+
await this._payWins(step, opts);
|
|
102
|
+
if (this._dead) return;
|
|
103
|
+
|
|
104
|
+
// No shift → just settle the board (defensive; a real ReelStep always shifts something).
|
|
105
|
+
const hasShift = step.shifts.some((n) => n > 0);
|
|
106
|
+
if (!this._cfg.enabled || !hasShift) {
|
|
107
|
+
this._grid.setGrid(step.settledGrid);
|
|
108
|
+
if (step.winningCells.length) this.advanceMultiplier();
|
|
109
|
+
this._pending = null;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. scroll every reel down by its own N (0 = untouched), all reels concurrently.
|
|
114
|
+
const turbo = opts?.turbo ? 0.5 : 1;
|
|
115
|
+
const decel = Math.min(this._cfg.perStepDecelCap, 1 + stepIndex * this._cfg.perStepDecel);
|
|
116
|
+
const f = turbo * decel;
|
|
117
|
+
await Promise.all(
|
|
118
|
+
step.shifts.map((n, col) =>
|
|
119
|
+
n > 0 ? this._scrollReel(col, n, step.settledGrid, f) : Promise.resolve(),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
if (this._dead) {
|
|
123
|
+
this._cleanupTemp();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. normalise + advance multiplier.
|
|
128
|
+
this._grid.setGrid(step.settledGrid);
|
|
129
|
+
this._resetPositions();
|
|
130
|
+
this._cleanupTemp();
|
|
131
|
+
this._pending = null;
|
|
132
|
+
if (step.winningCells.length) this.advanceMultiplier();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Highlight + hold the winning cells, then release them back to rest before the shift. */
|
|
136
|
+
private async _payWins(step: ReelStepData, opts?: { turbo?: boolean }): Promise<void> {
|
|
137
|
+
if (!step.winningCells.length) return;
|
|
138
|
+
const turbo = opts?.turbo ? 0.5 : 1;
|
|
139
|
+
const t = this._cfg.timings;
|
|
140
|
+
const hs = this._win.highlightScale;
|
|
141
|
+
const winSet = new Set(step.winningCells.map((w) => `${w.col}:${w.row}`));
|
|
142
|
+
if (this._cfg.dimNonWinners) this._dim(winSet);
|
|
143
|
+
await Promise.all(
|
|
144
|
+
step.winningCells.map((w) => {
|
|
145
|
+
const cell = this._grid.getCell(w.col, w.row);
|
|
146
|
+
if (this._win.glow) cell.setState({ winning: true });
|
|
147
|
+
return Tween.to(
|
|
148
|
+
cell,
|
|
149
|
+
{ 'scale.x': hs, 'scale.y': hs },
|
|
150
|
+
t.highlight * turbo,
|
|
151
|
+
easingByName(this._cfg.easings.highlight),
|
|
152
|
+
);
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
if (this._dead) {
|
|
156
|
+
this._undim();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await Tween.delay(t.wait * turbo);
|
|
160
|
+
for (const w of step.winningCells) {
|
|
161
|
+
const cell = this._grid.getCell(w.col, w.row);
|
|
162
|
+
cell.setState({});
|
|
163
|
+
cell.scale.set(1);
|
|
164
|
+
}
|
|
165
|
+
this._undim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scroll one reel down by `n` positions. A tape carrying the reel's current symbols with `n`
|
|
170
|
+
* fresh symbols stacked on top slides down by `n` cells: the fresh symbols enter from the top,
|
|
171
|
+
* the existing ones ride down, and the bottom `n` ride off below the window. Ends on
|
|
172
|
+
* settledGrid[col]. The tape shares the cells' parent (so the reel mask, if any, clips it).
|
|
173
|
+
*/
|
|
174
|
+
private async _scrollReel(
|
|
175
|
+
col: number,
|
|
176
|
+
n: number,
|
|
177
|
+
settledGrid: CellData[][],
|
|
178
|
+
f: number,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const rows = this._grid.rowsOf(col);
|
|
181
|
+
if (rows === 0 || this._dead) return;
|
|
182
|
+
const realCells = Array.from({ length: rows }, (_, r) => this._grid.getCell(col, r));
|
|
183
|
+
const layer = realCells[0].parent ?? this._grid;
|
|
184
|
+
const base = this._grid.cellPosition(col, 0);
|
|
185
|
+
const step =
|
|
186
|
+
rows > 1 ? this._grid.cellPosition(col, 1).y - base.y : this._grid.cellSize(col).height;
|
|
187
|
+
|
|
188
|
+
// current visible symbols (top→bottom), captured before we hide them
|
|
189
|
+
const before = realCells.map((c) => ({ ...c.data }));
|
|
190
|
+
const { stack, startOffsetCells } = buildReelStepTape(before, settledGrid[col] ?? [], n);
|
|
191
|
+
|
|
192
|
+
// Tape laid out top→bottom at local y = i*step: [incoming(shift)] above [before(rows)].
|
|
193
|
+
const tape = new Container();
|
|
194
|
+
tape.x = base.x;
|
|
195
|
+
for (let i = 0; i < stack.length; i++) {
|
|
196
|
+
const cell = new SymbolCell({ size: this._grid.cellSize(col), resolve: this._resolve });
|
|
197
|
+
cell.setData(stack[i]);
|
|
198
|
+
cell.position.set(0, i * step);
|
|
199
|
+
tape.addChild(cell);
|
|
200
|
+
}
|
|
201
|
+
// Start: the `before` block fills the window; the incoming block sits above it (masked off).
|
|
202
|
+
tape.y = base.y + startOffsetCells * step;
|
|
203
|
+
realCells.forEach((c) => (c.visible = false));
|
|
204
|
+
layer.addChild(tape);
|
|
205
|
+
this._temp.push(tape);
|
|
206
|
+
|
|
207
|
+
// Slide down by `shift` positions, with a small overshoot then settle-back.
|
|
208
|
+
const overshoot = step * 0.12;
|
|
209
|
+
await Tween.to(
|
|
210
|
+
tape,
|
|
211
|
+
{ y: base.y + overshoot },
|
|
212
|
+
this._cfg.timings.drop * f,
|
|
213
|
+
easingByName(this._cfg.easings.drop),
|
|
214
|
+
);
|
|
215
|
+
if (this._dead) return;
|
|
216
|
+
await Tween.to(
|
|
217
|
+
tape,
|
|
218
|
+
{ y: base.y },
|
|
219
|
+
Math.max(90, this._cfg.timings.refill * f),
|
|
220
|
+
easingByName('easeOutQuad'),
|
|
221
|
+
);
|
|
222
|
+
if (this._dead) return;
|
|
223
|
+
|
|
224
|
+
// Hand the settled symbols back to the real cells.
|
|
225
|
+
for (let r = 0; r < rows; r++) realCells[r].setData(settledGrid[col]?.[r] ?? { symbol: null });
|
|
226
|
+
realCells.forEach((c) => (c.visible = true));
|
|
227
|
+
tape.destroy();
|
|
228
|
+
this._temp = this._temp.filter((tp) => tp !== tape);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private _dim(winSet: Set<string>): void {
|
|
232
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
233
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++)
|
|
234
|
+
if (!winSet.has(`${c}:${r}`)) this._grid.getCell(c, r).alpha = this._cfg.dimAlpha;
|
|
235
|
+
}
|
|
236
|
+
private _undim(): void {
|
|
237
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
238
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
239
|
+
const cell = this._grid.getCell(c, r);
|
|
240
|
+
if (cell.alpha !== 0) cell.alpha = 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
private _resetPositions(): void {
|
|
244
|
+
if (this._grid.destroyed) return;
|
|
245
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
246
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
247
|
+
const cell = this._grid.getCell(c, r);
|
|
248
|
+
const { x, y } = this._grid.cellPosition(c, r);
|
|
249
|
+
cell.position.set(x, y);
|
|
250
|
+
cell.scale.set(1);
|
|
251
|
+
cell.alpha = 1;
|
|
252
|
+
cell.visible = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
private _cleanupTemp(): void {
|
|
256
|
+
for (const t of this._temp) if (!t.destroyed) t.destroy();
|
|
257
|
+
this._temp = [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Hard-cancel: kill tweens, drop tapes, snap to the in-flight step's settled board. */
|
|
261
|
+
skip(): void {
|
|
262
|
+
this._killed = true;
|
|
263
|
+
for (let c = 0; c < this._grid.cols; c++)
|
|
264
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) Tween.killTweensOf(this._grid.getCell(c, r));
|
|
265
|
+
this._cleanupTemp();
|
|
266
|
+
if (this._pending && !this._grid.destroyed) this._grid.setGrid(this._pending);
|
|
267
|
+
this._pending = null;
|
|
268
|
+
this._resetPositions();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -47,6 +47,7 @@ export class SymbolCell extends Container {
|
|
|
47
47
|
private _style: Required<CellFrameStyle>;
|
|
48
48
|
private _frame: Graphics;
|
|
49
49
|
private _view: SymbolView | null = null;
|
|
50
|
+
private _data: CellData = { symbol: null };
|
|
50
51
|
private _badges = new Container();
|
|
51
52
|
private _multBadge: Container | null = null;
|
|
52
53
|
private _bonusBadge: Container | null = null;
|
|
@@ -70,8 +71,14 @@ export class SymbolCell extends Container {
|
|
|
70
71
|
return this._view;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/** The last applied cell data (symbol + badges). Read by motion controllers that rebuild tapes. */
|
|
75
|
+
get data(): CellData {
|
|
76
|
+
return this._data;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
setData(data: CellData): void {
|
|
74
80
|
if (this.destroyed) return; // a killed-tween chain may resume after the cell is gone
|
|
81
|
+
this._data = data;
|
|
75
82
|
// symbol view
|
|
76
83
|
if (data.symbol == null) {
|
|
77
84
|
if (this._view) {
|
package/src/slot/index.ts
CHANGED
|
@@ -66,6 +66,8 @@ export { AnticipationController } from './motion/AnticipationController';
|
|
|
66
66
|
export type { AnticipationDecision } from './motion/AnticipationController';
|
|
67
67
|
export { TumbleController } from './cascade/TumbleController';
|
|
68
68
|
export type { TumbleStep } from './cascade/TumbleController';
|
|
69
|
+
export { ReelStepController, buildReelStepTape } from './cascade/ReelStepController';
|
|
70
|
+
export type { ReelStepData } from './cascade/ReelStepController';
|
|
69
71
|
|
|
70
72
|
export { FEATURES, FEATURE_LIST } from './features';
|
|
71
73
|
export type { ReelFeature, FeatureContext } from './features';
|
|
@@ -12,6 +12,7 @@ import type { SymbolResolver } from '../grid/SymbolView';
|
|
|
12
12
|
import { SpinEngine, type SpinData, type SpinRunOpts } from '../motion/SpinEngine';
|
|
13
13
|
import { AnticipationController } from '../motion/AnticipationController';
|
|
14
14
|
import { TumbleController, type TumbleStep } from '../cascade/TumbleController';
|
|
15
|
+
import { ReelStepController, type ReelStepData } from '../cascade/ReelStepController';
|
|
15
16
|
import { FEATURES, FEATURE_LIST, type FeatureContext, type ReelFeature } from '../features';
|
|
16
17
|
import {
|
|
17
18
|
DEFAULT_REEL_CONFIG,
|
|
@@ -53,7 +54,13 @@ export interface ReelSystem {
|
|
|
53
54
|
spin(target: CellData[][], opts?: SpinRunOpts): Promise<void>;
|
|
54
55
|
/** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier carries over instead of resetting. */
|
|
55
56
|
cascade(steps: TumbleStep[], opts?: { turbo?: boolean; freeSpins?: boolean }): Promise<void>;
|
|
56
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Run a ReelStep™ chain: each step pays its winning cells, then scrolls every reel down by
|
|
59
|
+
* `shifts[col]` positions (0 = reel stays put). Multiplier carries over with `freeSpins` +
|
|
60
|
+
* `cascade.multiplier.persistInFreeSpins`, same as `cascade`.
|
|
61
|
+
*/
|
|
62
|
+
reelStep(steps: ReelStepData[], opts?: { turbo?: boolean; freeSpins?: boolean }): Promise<void>;
|
|
63
|
+
/** Current running cascade / reel-step multiplier. */
|
|
57
64
|
readonly multiplier: number;
|
|
58
65
|
/** Register a custom feature (or override a built-in by reusing its key). */
|
|
59
66
|
registerFeature(feature: ReelFeature): void;
|
|
@@ -91,6 +98,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
91
98
|
let spin!: SpinEngine;
|
|
92
99
|
let anticipation!: AnticipationController;
|
|
93
100
|
let tumble!: TumbleController;
|
|
101
|
+
let reelStepCtl!: ReelStepController;
|
|
94
102
|
let board: CellData[][] = opts.board ?? emptyBoard(config);
|
|
95
103
|
// custom features keyed by id; built-ins live in FEATURES/FEATURE_LIST
|
|
96
104
|
const custom = new Map<string, ReelFeature>();
|
|
@@ -113,6 +121,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
113
121
|
if (grid) {
|
|
114
122
|
spin?.skip();
|
|
115
123
|
tumble?.skip();
|
|
124
|
+
reelStepCtl?.skip();
|
|
116
125
|
for (const child of fx?.children.slice() ?? []) Tween.killTweensOf(child);
|
|
117
126
|
grid.destroy({ children: true });
|
|
118
127
|
}
|
|
@@ -140,6 +149,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
140
149
|
spin = new SpinEngine(grid, resolve, config.motion, config.win);
|
|
141
150
|
anticipation = new AnticipationController(config.anticipation);
|
|
142
151
|
tumble = new TumbleController(grid, config.cascade, config.win);
|
|
152
|
+
reelStepCtl = new ReelStepController(grid, resolve, config.cascade, config.win);
|
|
143
153
|
grid.setGrid(board);
|
|
144
154
|
}
|
|
145
155
|
|
|
@@ -203,6 +213,8 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
203
213
|
anticipation.setConfig(config.anticipation);
|
|
204
214
|
tumble.setConfig(config.cascade);
|
|
205
215
|
tumble.setWin(config.win);
|
|
216
|
+
reelStepCtl.setConfig(config.cascade);
|
|
217
|
+
reelStepCtl.setWin(config.win);
|
|
206
218
|
}
|
|
207
219
|
},
|
|
208
220
|
|
|
@@ -229,7 +241,8 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
229
241
|
},
|
|
230
242
|
|
|
231
243
|
get multiplier() {
|
|
232
|
-
|
|
244
|
+
// cascade and reelStep share the same config start; only the active mechanic climbs.
|
|
245
|
+
return Math.max(tumble.multiplier, reelStepCtl.multiplier);
|
|
233
246
|
},
|
|
234
247
|
|
|
235
248
|
async cascade(steps, cOpts) {
|
|
@@ -243,6 +256,17 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
243
256
|
if (config.cascade.multiplier.enabled) log?.(`Cascade multiplier ×${tumble.multiplier}`);
|
|
244
257
|
},
|
|
245
258
|
|
|
259
|
+
async reelStep(steps, rOpts) {
|
|
260
|
+
const persist = config.cascade.multiplier.persistInFreeSpins && !!rOpts?.freeSpins;
|
|
261
|
+
if (!persist) reelStepCtl.resetMultiplier();
|
|
262
|
+
for (let i = 0; i < steps.length; i++) {
|
|
263
|
+
await reelStepCtl.step(steps[i], i, rOpts);
|
|
264
|
+
board = steps[i].settledGrid;
|
|
265
|
+
}
|
|
266
|
+
if (config.cascade.multiplier.enabled)
|
|
267
|
+
log?.(`ReelStep multiplier ×${reelStepCtl.multiplier}`);
|
|
268
|
+
},
|
|
269
|
+
|
|
246
270
|
registerFeature(feature) {
|
|
247
271
|
custom.set(feature.key, feature);
|
|
248
272
|
},
|
|
@@ -280,6 +304,7 @@ export function createReelSystem(opts: CreateReelSystemOptions): ReelSystem {
|
|
|
280
304
|
skip() {
|
|
281
305
|
spin.skip();
|
|
282
306
|
tumble.skip();
|
|
307
|
+
reelStepCtl.skip();
|
|
283
308
|
// kill in-flight overlay tweens (labels/rings) so a rebuild never animates destroyed nodes
|
|
284
309
|
for (const child of fx.children.slice()) Tween.killTweensOf(child);
|
|
285
310
|
fx.removeChildren().forEach((c) => c.destroy());
|