@energy8platform/game-engine 0.32.2 → 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.d.ts CHANGED
@@ -96,6 +96,7 @@ declare class SymbolCell extends Container {
96
96
  private _style;
97
97
  private _frame;
98
98
  private _view;
99
+ private _data;
99
100
  private _badges;
100
101
  private _multBadge;
101
102
  private _bonusBadge;
@@ -103,6 +104,8 @@ declare class SymbolCell extends Container {
103
104
  frameStyleKey: 'idle' | 'winning' | 'removed' | 'fresh';
104
105
  constructor(config: SymbolCellConfig);
105
106
  get view(): SymbolView | null;
107
+ /** The last applied cell data (symbol + badges). Read by motion controllers that rebuild tapes. */
108
+ get data(): CellData;
106
109
  setData(data: CellData): void;
107
110
  setState(state: CellState): void;
108
111
  playWin(): Promise<void>;
@@ -816,6 +819,68 @@ declare class TumbleController {
816
819
  skip(): void;
817
820
  }
818
821
 
822
+ interface ReelStepData {
823
+ /** Cells that won on the current board — highlighted/paid before the shift. */
824
+ winningCells: {
825
+ col: number;
826
+ row: number;
827
+ }[];
828
+ /** How far to scroll each reel down (length = cols). 0 = the reel stays put. */
829
+ shifts: number[];
830
+ /** Board after every reel has scrolled down by shifts[col]. */
831
+ settledGrid: CellData[][];
832
+ }
833
+ /**
834
+ * PURE: lay out one reel's scroll tape (top→bottom). The `shift` fresh symbols (the top of the
835
+ * settled reel) stack above the reel's current symbols; the tape starts `shift` cells high so the
836
+ * current symbols fill the window, then slides down by `shift` to reveal the fresh ones. `shift` is
837
+ * clamped to the visible window height. Returns the stacked cells and the start offset (in cells,
838
+ * relative to row 0) the tape animates from.
839
+ */
840
+ declare function buildReelStepTape(before: CellData[], settledCol: CellData[], shift: number): {
841
+ stack: CellData[];
842
+ shift: number;
843
+ startOffsetCells: number;
844
+ };
845
+ declare class ReelStepController {
846
+ private _grid;
847
+ private _resolve;
848
+ private _cfg;
849
+ private _win;
850
+ private _killed;
851
+ private _mult;
852
+ private _temp;
853
+ /** Board the in-flight step settles to — used to snap on skip(). */
854
+ private _pending;
855
+ constructor(grid: ReelGrid, resolve: SymbolResolver, cfg: CascadeConfig, win?: WinConfig);
856
+ setConfig(cfg: CascadeConfig): void;
857
+ setWin(win: WinConfig): void;
858
+ /** Killed, or the grid was torn down underneath us (rebuild mid-chain). */
859
+ private get _dead();
860
+ get multiplier(): number;
861
+ resetMultiplier(): void;
862
+ private advanceMultiplier;
863
+ /** Run one ReelStep: pay the winning cells, then scroll each reel down by shifts[col]. */
864
+ step(step: ReelStepData, stepIndex?: number, opts?: {
865
+ turbo?: boolean;
866
+ }): Promise<void>;
867
+ /** Highlight + hold the winning cells, then release them back to rest before the shift. */
868
+ private _payWins;
869
+ /**
870
+ * Scroll one reel down by `n` positions. A tape carrying the reel's current symbols with `n`
871
+ * fresh symbols stacked on top slides down by `n` cells: the fresh symbols enter from the top,
872
+ * the existing ones ride down, and the bottom `n` ride off below the window. Ends on
873
+ * settledGrid[col]. The tape shares the cells' parent (so the reel mask, if any, clips it).
874
+ */
875
+ private _scrollReel;
876
+ private _dim;
877
+ private _undim;
878
+ private _resetPositions;
879
+ private _cleanupTemp;
880
+ /** Hard-cancel: kill tweens, drop tapes, snap to the in-flight step's settled board. */
881
+ skip(): void;
882
+ }
883
+
819
884
  interface FeatureContext {
820
885
  grid: ReelGrid;
821
886
  resolve: SymbolResolver;
@@ -875,7 +940,16 @@ interface ReelSystem {
875
940
  turbo?: boolean;
876
941
  freeSpins?: boolean;
877
942
  }): Promise<void>;
878
- /** Current running cascade multiplier. */
943
+ /**
944
+ * Run a ReelStep™ chain: each step pays its winning cells, then scrolls every reel down by
945
+ * `shifts[col]` positions (0 = reel stays put). Multiplier carries over with `freeSpins` +
946
+ * `cascade.multiplier.persistInFreeSpins`, same as `cascade`.
947
+ */
948
+ reelStep(steps: ReelStepData[], opts?: {
949
+ turbo?: boolean;
950
+ freeSpins?: boolean;
951
+ }): Promise<void>;
952
+ /** Current running cascade / reel-step multiplier. */
879
953
  readonly multiplier: number;
880
954
  /** Register a custom feature (or override a built-in by reusing its key). */
881
955
  registerFeature(feature: ReelFeature): void;
@@ -974,5 +1048,5 @@ declare class MultiplierAccumulator {
974
1048
  reset(boundary: CarryPolicy): void;
975
1049
  }
976
1050
 
977
- export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, SpinEngine, SymbolCell, TumbleController, cellPositionOf, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
978
- export type { AnimatedSymbolConfig, AnticipationConfig, AnticipationDecision, BigWinOverlayConfig, BlurConfig, CarryPolicy, CascadeAnim, CascadeConfig, CascadeStepData, CascadeTimings, CellData, CellFrameStyle, CellSizeSpec, CellState, CountUpConfig, CreateReelSystemOptions, DecorationConfig, DeepPartial, EasingName, EvaluationMode, ExpandingWildConfig, FeatureContext, FeatureKey, FeaturesConfig, GeometryInput, GiantConfig, GridConfig, HoldAndSpinConfig, Intensity, MotionConfig, MotionStyle, MultiplierConfig, MysteryConfig, NudgeConfig, PresetId, RandomWildConfig, ReelFeature, ReelGridConfig, ReelModifierConfig, ReelPreset, ReelSpinData, ReelSpinTimings, ReelStopPlan$1 as ReelStopPlan, ReelSystem, ReelSystemConfig, ResolvedGeometry, SettleConfig, SpinData, SpinRunOpts, ReelStopPlan as SpinStopPlan, SplitConfig, SquashConfig, StackedConfig, StickyConfig, StopMode, StopOrder, SymbolCellConfig, SymbolResolver, SymbolTextures, SymbolView, TransformConfig, TumbleStep, WalkingWildConfig, WinConfig, WinTier };
1051
+ export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, ReelStepController, SpinEngine, SymbolCell, TumbleController, buildReelStepTape, cellPositionOf, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
1052
+ export type { AnimatedSymbolConfig, AnticipationConfig, AnticipationDecision, BigWinOverlayConfig, BlurConfig, CarryPolicy, CascadeAnim, CascadeConfig, CascadeStepData, CascadeTimings, CellData, CellFrameStyle, CellSizeSpec, CellState, CountUpConfig, CreateReelSystemOptions, DecorationConfig, DeepPartial, EasingName, EvaluationMode, ExpandingWildConfig, FeatureContext, FeatureKey, FeaturesConfig, GeometryInput, GiantConfig, GridConfig, HoldAndSpinConfig, Intensity, MotionConfig, MotionStyle, MultiplierConfig, MysteryConfig, NudgeConfig, PresetId, RandomWildConfig, ReelFeature, ReelGridConfig, ReelModifierConfig, ReelPreset, ReelSpinData, ReelSpinTimings, ReelStepData, ReelStopPlan$1 as ReelStopPlan, ReelSystem, ReelSystemConfig, ResolvedGeometry, SettleConfig, SpinData, SpinRunOpts, ReelStopPlan as SpinStopPlan, SplitConfig, SquashConfig, StackedConfig, StickyConfig, StopMode, StopOrder, SymbolCellConfig, SymbolResolver, SymbolTextures, SymbolView, TransformConfig, TumbleStep, WalkingWildConfig, WinConfig, WinTier };
package/dist/slot.esm.js CHANGED
@@ -537,6 +537,7 @@ class SymbolCell extends Container {
537
537
  _style;
538
538
  _frame;
539
539
  _view = null;
540
+ _data = { symbol: null };
540
541
  _badges = new Container();
541
542
  _multBadge = null;
542
543
  _bonusBadge = null;
@@ -557,9 +558,14 @@ class SymbolCell extends Container {
557
558
  get view() {
558
559
  return this._view;
559
560
  }
561
+ /** The last applied cell data (symbol + badges). Read by motion controllers that rebuild tapes. */
562
+ get data() {
563
+ return this._data;
564
+ }
560
565
  setData(data) {
561
566
  if (this.destroyed)
562
567
  return; // a killed-tween chain may resume after the cell is gone
568
+ this._data = data;
563
569
  // symbol view
564
570
  if (data.symbol == null) {
565
571
  if (this._view) {
@@ -2023,6 +2029,227 @@ class TumbleController {
2023
2029
  }
2024
2030
  }
2025
2031
 
2032
+ // packages/game-engine/src/slot/cascade/ReelStepController.ts
2033
+ //
2034
+ // ReelStep™ mechanic. Flow: reels stop → winning lines are paid → each reel scrolls DOWN by N
2035
+ // positions (N = winning symbols that played on that reel) → the board is re-evaluated → repeat,
2036
+ // until no wins remain. Unlike a cascade/tumble, nothing is removed: the existing symbols ride
2037
+ // down and N fresh symbols enter from the top. Reels with N=0 stay put; each reel moves
2038
+ // independently by its own N.
2039
+ //
2040
+ // Presentation only — the caller supplies each step's per-reel shift vector and the post-shift
2041
+ // board. Fits classic fixed-line grids (5×3, 5×4, 5×5, …), not ways/cluster.
2042
+ /**
2043
+ * PURE: lay out one reel's scroll tape (top→bottom). The `shift` fresh symbols (the top of the
2044
+ * settled reel) stack above the reel's current symbols; the tape starts `shift` cells high so the
2045
+ * current symbols fill the window, then slides down by `shift` to reveal the fresh ones. `shift` is
2046
+ * clamped to the visible window height. Returns the stacked cells and the start offset (in cells,
2047
+ * relative to row 0) the tape animates from.
2048
+ */
2049
+ function buildReelStepTape(before, settledCol, shift) {
2050
+ const rows = before.length;
2051
+ const s = Math.max(0, Math.min(shift, rows));
2052
+ const incoming = Array.from({ length: s }, (_, i) => settledCol[i] ?? { symbol: null });
2053
+ return { stack: [...incoming, ...before], shift: s, startOffsetCells: 0 - s };
2054
+ }
2055
+ class ReelStepController {
2056
+ _grid;
2057
+ _resolve;
2058
+ _cfg;
2059
+ _win = DEFAULT_REEL_CONFIG.win;
2060
+ _killed = false;
2061
+ _mult;
2062
+ _temp = [];
2063
+ /** Board the in-flight step settles to — used to snap on skip(). */
2064
+ _pending = null;
2065
+ constructor(grid, resolve, cfg, win) {
2066
+ this._grid = grid;
2067
+ this._resolve = resolve;
2068
+ this._cfg = cfg;
2069
+ this._mult = cfg.multiplier.start;
2070
+ if (win)
2071
+ this._win = win;
2072
+ }
2073
+ setConfig(cfg) {
2074
+ this._cfg = cfg;
2075
+ }
2076
+ setWin(win) {
2077
+ this._win = win;
2078
+ }
2079
+ /** Killed, or the grid was torn down underneath us (rebuild mid-chain). */
2080
+ get _dead() {
2081
+ return this._killed || this._grid.destroyed;
2082
+ }
2083
+ get multiplier() {
2084
+ return this._mult;
2085
+ }
2086
+ resetMultiplier() {
2087
+ this._mult = this._cfg.multiplier.start;
2088
+ }
2089
+ advanceMultiplier() {
2090
+ const m = this._cfg.multiplier;
2091
+ if (!m.enabled)
2092
+ return;
2093
+ const next = m.mode === 'mul' ? this._mult * m.step : this._mult + m.step;
2094
+ this._mult = m.cap != null ? Math.min(next, m.cap) : next;
2095
+ }
2096
+ /** Run one ReelStep: pay the winning cells, then scroll each reel down by shifts[col]. */
2097
+ async step(step, stepIndex = 0, opts) {
2098
+ if (this._grid.destroyed)
2099
+ return;
2100
+ this._killed = false;
2101
+ this._pending = step.settledGrid;
2102
+ // 1. celebrate/pay the winning cells.
2103
+ await this._payWins(step, opts);
2104
+ if (this._dead)
2105
+ return;
2106
+ // No shift → just settle the board (defensive; a real ReelStep always shifts something).
2107
+ const hasShift = step.shifts.some((n) => n > 0);
2108
+ if (!this._cfg.enabled || !hasShift) {
2109
+ this._grid.setGrid(step.settledGrid);
2110
+ if (step.winningCells.length)
2111
+ this.advanceMultiplier();
2112
+ this._pending = null;
2113
+ return;
2114
+ }
2115
+ // 2. scroll every reel down by its own N (0 = untouched), all reels concurrently.
2116
+ const turbo = opts?.turbo ? 0.5 : 1;
2117
+ const decel = Math.min(this._cfg.perStepDecelCap, 1 + stepIndex * this._cfg.perStepDecel);
2118
+ const f = turbo * decel;
2119
+ await Promise.all(step.shifts.map((n, col) => n > 0 ? this._scrollReel(col, n, step.settledGrid, f) : Promise.resolve()));
2120
+ if (this._dead) {
2121
+ this._cleanupTemp();
2122
+ return;
2123
+ }
2124
+ // 3. normalise + advance multiplier.
2125
+ this._grid.setGrid(step.settledGrid);
2126
+ this._resetPositions();
2127
+ this._cleanupTemp();
2128
+ this._pending = null;
2129
+ if (step.winningCells.length)
2130
+ this.advanceMultiplier();
2131
+ }
2132
+ /** Highlight + hold the winning cells, then release them back to rest before the shift. */
2133
+ async _payWins(step, opts) {
2134
+ if (!step.winningCells.length)
2135
+ return;
2136
+ const turbo = opts?.turbo ? 0.5 : 1;
2137
+ const t = this._cfg.timings;
2138
+ const hs = this._win.highlightScale;
2139
+ const winSet = new Set(step.winningCells.map((w) => `${w.col}:${w.row}`));
2140
+ if (this._cfg.dimNonWinners)
2141
+ this._dim(winSet);
2142
+ await Promise.all(step.winningCells.map((w) => {
2143
+ const cell = this._grid.getCell(w.col, w.row);
2144
+ if (this._win.glow)
2145
+ cell.setState({ winning: true });
2146
+ return Tween.to(cell, { 'scale.x': hs, 'scale.y': hs }, t.highlight * turbo, easingByName(this._cfg.easings.highlight));
2147
+ }));
2148
+ if (this._dead) {
2149
+ this._undim();
2150
+ return;
2151
+ }
2152
+ await Tween.delay(t.wait * turbo);
2153
+ for (const w of step.winningCells) {
2154
+ const cell = this._grid.getCell(w.col, w.row);
2155
+ cell.setState({});
2156
+ cell.scale.set(1);
2157
+ }
2158
+ this._undim();
2159
+ }
2160
+ /**
2161
+ * Scroll one reel down by `n` positions. A tape carrying the reel's current symbols with `n`
2162
+ * fresh symbols stacked on top slides down by `n` cells: the fresh symbols enter from the top,
2163
+ * the existing ones ride down, and the bottom `n` ride off below the window. Ends on
2164
+ * settledGrid[col]. The tape shares the cells' parent (so the reel mask, if any, clips it).
2165
+ */
2166
+ async _scrollReel(col, n, settledGrid, f) {
2167
+ const rows = this._grid.rowsOf(col);
2168
+ if (rows === 0 || this._dead)
2169
+ return;
2170
+ const realCells = Array.from({ length: rows }, (_, r) => this._grid.getCell(col, r));
2171
+ const layer = realCells[0].parent ?? this._grid;
2172
+ const base = this._grid.cellPosition(col, 0);
2173
+ const step = rows > 1 ? this._grid.cellPosition(col, 1).y - base.y : this._grid.cellSize(col).height;
2174
+ // current visible symbols (top→bottom), captured before we hide them
2175
+ const before = realCells.map((c) => ({ ...c.data }));
2176
+ const { stack, startOffsetCells } = buildReelStepTape(before, settledGrid[col] ?? [], n);
2177
+ // Tape laid out top→bottom at local y = i*step: [incoming(shift)] above [before(rows)].
2178
+ const tape = new Container();
2179
+ tape.x = base.x;
2180
+ for (let i = 0; i < stack.length; i++) {
2181
+ const cell = new SymbolCell({ size: this._grid.cellSize(col), resolve: this._resolve });
2182
+ cell.setData(stack[i]);
2183
+ cell.position.set(0, i * step);
2184
+ tape.addChild(cell);
2185
+ }
2186
+ // Start: the `before` block fills the window; the incoming block sits above it (masked off).
2187
+ tape.y = base.y + startOffsetCells * step;
2188
+ realCells.forEach((c) => (c.visible = false));
2189
+ layer.addChild(tape);
2190
+ this._temp.push(tape);
2191
+ // Slide down by `shift` positions, with a small overshoot then settle-back.
2192
+ const overshoot = step * 0.12;
2193
+ await Tween.to(tape, { y: base.y + overshoot }, this._cfg.timings.drop * f, easingByName(this._cfg.easings.drop));
2194
+ if (this._dead)
2195
+ return;
2196
+ await Tween.to(tape, { y: base.y }, Math.max(90, this._cfg.timings.refill * f), easingByName('easeOutQuad'));
2197
+ if (this._dead)
2198
+ return;
2199
+ // Hand the settled symbols back to the real cells.
2200
+ for (let r = 0; r < rows; r++)
2201
+ realCells[r].setData(settledGrid[col]?.[r] ?? { symbol: null });
2202
+ realCells.forEach((c) => (c.visible = true));
2203
+ tape.destroy();
2204
+ this._temp = this._temp.filter((tp) => tp !== tape);
2205
+ }
2206
+ _dim(winSet) {
2207
+ for (let c = 0; c < this._grid.cols; c++)
2208
+ for (let r = 0; r < this._grid.rowsOf(c); r++)
2209
+ if (!winSet.has(`${c}:${r}`))
2210
+ this._grid.getCell(c, r).alpha = this._cfg.dimAlpha;
2211
+ }
2212
+ _undim() {
2213
+ for (let c = 0; c < this._grid.cols; c++)
2214
+ for (let r = 0; r < this._grid.rowsOf(c); r++) {
2215
+ const cell = this._grid.getCell(c, r);
2216
+ if (cell.alpha !== 0)
2217
+ cell.alpha = 1;
2218
+ }
2219
+ }
2220
+ _resetPositions() {
2221
+ if (this._grid.destroyed)
2222
+ return;
2223
+ for (let c = 0; c < this._grid.cols; c++)
2224
+ for (let r = 0; r < this._grid.rowsOf(c); r++) {
2225
+ const cell = this._grid.getCell(c, r);
2226
+ const { x, y } = this._grid.cellPosition(c, r);
2227
+ cell.position.set(x, y);
2228
+ cell.scale.set(1);
2229
+ cell.alpha = 1;
2230
+ cell.visible = true;
2231
+ }
2232
+ }
2233
+ _cleanupTemp() {
2234
+ for (const t of this._temp)
2235
+ if (!t.destroyed)
2236
+ t.destroy();
2237
+ this._temp = [];
2238
+ }
2239
+ /** Hard-cancel: kill tweens, drop tapes, snap to the in-flight step's settled board. */
2240
+ skip() {
2241
+ this._killed = true;
2242
+ for (let c = 0; c < this._grid.cols; c++)
2243
+ for (let r = 0; r < this._grid.rowsOf(c); r++)
2244
+ Tween.killTweensOf(this._grid.getCell(c, r));
2245
+ this._cleanupTemp();
2246
+ if (this._pending && !this._grid.destroyed)
2247
+ this._grid.setGrid(this._pending);
2248
+ this._pending = null;
2249
+ this._resetPositions();
2250
+ }
2251
+ }
2252
+
2026
2253
  // packages/game-engine/src/slot/features/types.ts
2027
2254
  //
2028
2255
  // Uniform interface for special reel feature mechanics. Each feature is a presentation module:
@@ -2571,6 +2798,7 @@ function createReelSystem(opts) {
2571
2798
  let spin;
2572
2799
  let anticipation;
2573
2800
  let tumble;
2801
+ let reelStepCtl;
2574
2802
  let board = opts.board ?? emptyBoard(config);
2575
2803
  // custom features keyed by id; built-ins live in FEATURES/FEATURE_LIST
2576
2804
  const custom = new Map();
@@ -2593,6 +2821,7 @@ function createReelSystem(opts) {
2593
2821
  if (grid) {
2594
2822
  spin?.skip();
2595
2823
  tumble?.skip();
2824
+ reelStepCtl?.skip();
2596
2825
  for (const child of fx?.children.slice() ?? [])
2597
2826
  Tween.killTweensOf(child);
2598
2827
  grid.destroy({ children: true });
@@ -2621,6 +2850,7 @@ function createReelSystem(opts) {
2621
2850
  spin = new SpinEngine(grid, resolve, config.motion, config.win);
2622
2851
  anticipation = new AnticipationController(config.anticipation);
2623
2852
  tumble = new TumbleController(grid, config.cascade, config.win);
2853
+ reelStepCtl = new ReelStepController(grid, resolve, config.cascade, config.win);
2624
2854
  grid.setGrid(board);
2625
2855
  }
2626
2856
  function geometryChanged(next) {
@@ -2676,6 +2906,8 @@ function createReelSystem(opts) {
2676
2906
  anticipation.setConfig(config.anticipation);
2677
2907
  tumble.setConfig(config.cascade);
2678
2908
  tumble.setWin(config.win);
2909
+ reelStepCtl.setConfig(config.cascade);
2910
+ reelStepCtl.setWin(config.win);
2679
2911
  }
2680
2912
  },
2681
2913
  update(partial) {
@@ -2700,7 +2932,8 @@ function createReelSystem(opts) {
2700
2932
  board = target;
2701
2933
  },
2702
2934
  get multiplier() {
2703
- return tumble.multiplier;
2935
+ // cascade and reelStep share the same config start; only the active mechanic climbs.
2936
+ return Math.max(tumble.multiplier, reelStepCtl.multiplier);
2704
2937
  },
2705
2938
  async cascade(steps, cOpts) {
2706
2939
  // keep the multiplier climbing across free-spins when configured; otherwise reset per spin
@@ -2714,6 +2947,17 @@ function createReelSystem(opts) {
2714
2947
  if (config.cascade.multiplier.enabled)
2715
2948
  log?.(`Cascade multiplier ×${tumble.multiplier}`);
2716
2949
  },
2950
+ async reelStep(steps, rOpts) {
2951
+ const persist = config.cascade.multiplier.persistInFreeSpins && !!rOpts?.freeSpins;
2952
+ if (!persist)
2953
+ reelStepCtl.resetMultiplier();
2954
+ for (let i = 0; i < steps.length; i++) {
2955
+ await reelStepCtl.step(steps[i], i, rOpts);
2956
+ board = steps[i].settledGrid;
2957
+ }
2958
+ if (config.cascade.multiplier.enabled)
2959
+ log?.(`ReelStep multiplier ×${reelStepCtl.multiplier}`);
2960
+ },
2717
2961
  registerFeature(feature) {
2718
2962
  custom.set(feature.key, feature);
2719
2963
  },
@@ -2745,6 +2989,7 @@ function createReelSystem(opts) {
2745
2989
  skip() {
2746
2990
  spin.skip();
2747
2991
  tumble.skip();
2992
+ reelStepCtl.skip();
2748
2993
  // kill in-flight overlay tweens (labels/rings) so a rebuild never animates destroyed nodes
2749
2994
  for (const child of fx.children.slice())
2750
2995
  Tween.killTweensOf(child);
@@ -2924,5 +3169,5 @@ class MultiplierAccumulator {
2924
3169
  }
2925
3170
  }
2926
3171
 
2927
- export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, SpinEngine, SymbolCell, TumbleController, cellPositionOf, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
3172
+ export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, ReelStepController, SpinEngine, SymbolCell, TumbleController, buildReelStepTape, cellPositionOf, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
2928
3173
  //# sourceMappingURL=slot.esm.js.map