@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 CHANGED
@@ -539,6 +539,7 @@ class SymbolCell extends pixi_js.Container {
539
539
  _style;
540
540
  _frame;
541
541
  _view = null;
542
+ _data = { symbol: null };
542
543
  _badges = new pixi_js.Container();
543
544
  _multBadge = null;
544
545
  _bonusBadge = null;
@@ -559,9 +560,14 @@ class SymbolCell extends pixi_js.Container {
559
560
  get view() {
560
561
  return this._view;
561
562
  }
563
+ /** The last applied cell data (symbol + badges). Read by motion controllers that rebuild tapes. */
564
+ get data() {
565
+ return this._data;
566
+ }
562
567
  setData(data) {
563
568
  if (this.destroyed)
564
569
  return; // a killed-tween chain may resume after the cell is gone
570
+ this._data = data;
565
571
  // symbol view
566
572
  if (data.symbol == null) {
567
573
  if (this._view) {
@@ -2025,6 +2031,227 @@ class TumbleController {
2025
2031
  }
2026
2032
  }
2027
2033
 
2034
+ // packages/game-engine/src/slot/cascade/ReelStepController.ts
2035
+ //
2036
+ // ReelStep™ mechanic. Flow: reels stop → winning lines are paid → each reel scrolls DOWN by N
2037
+ // positions (N = winning symbols that played on that reel) → the board is re-evaluated → repeat,
2038
+ // until no wins remain. Unlike a cascade/tumble, nothing is removed: the existing symbols ride
2039
+ // down and N fresh symbols enter from the top. Reels with N=0 stay put; each reel moves
2040
+ // independently by its own N.
2041
+ //
2042
+ // Presentation only — the caller supplies each step's per-reel shift vector and the post-shift
2043
+ // board. Fits classic fixed-line grids (5×3, 5×4, 5×5, …), not ways/cluster.
2044
+ /**
2045
+ * PURE: lay out one reel's scroll tape (top→bottom). The `shift` fresh symbols (the top of the
2046
+ * settled reel) stack above the reel's current symbols; the tape starts `shift` cells high so the
2047
+ * current symbols fill the window, then slides down by `shift` to reveal the fresh ones. `shift` is
2048
+ * clamped to the visible window height. Returns the stacked cells and the start offset (in cells,
2049
+ * relative to row 0) the tape animates from.
2050
+ */
2051
+ function buildReelStepTape(before, settledCol, shift) {
2052
+ const rows = before.length;
2053
+ const s = Math.max(0, Math.min(shift, rows));
2054
+ const incoming = Array.from({ length: s }, (_, i) => settledCol[i] ?? { symbol: null });
2055
+ return { stack: [...incoming, ...before], shift: s, startOffsetCells: 0 - s };
2056
+ }
2057
+ class ReelStepController {
2058
+ _grid;
2059
+ _resolve;
2060
+ _cfg;
2061
+ _win = DEFAULT_REEL_CONFIG.win;
2062
+ _killed = false;
2063
+ _mult;
2064
+ _temp = [];
2065
+ /** Board the in-flight step settles to — used to snap on skip(). */
2066
+ _pending = null;
2067
+ constructor(grid, resolve, cfg, win) {
2068
+ this._grid = grid;
2069
+ this._resolve = resolve;
2070
+ this._cfg = cfg;
2071
+ this._mult = cfg.multiplier.start;
2072
+ if (win)
2073
+ this._win = win;
2074
+ }
2075
+ setConfig(cfg) {
2076
+ this._cfg = cfg;
2077
+ }
2078
+ setWin(win) {
2079
+ this._win = win;
2080
+ }
2081
+ /** Killed, or the grid was torn down underneath us (rebuild mid-chain). */
2082
+ get _dead() {
2083
+ return this._killed || this._grid.destroyed;
2084
+ }
2085
+ get multiplier() {
2086
+ return this._mult;
2087
+ }
2088
+ resetMultiplier() {
2089
+ this._mult = this._cfg.multiplier.start;
2090
+ }
2091
+ advanceMultiplier() {
2092
+ const m = this._cfg.multiplier;
2093
+ if (!m.enabled)
2094
+ return;
2095
+ const next = m.mode === 'mul' ? this._mult * m.step : this._mult + m.step;
2096
+ this._mult = m.cap != null ? Math.min(next, m.cap) : next;
2097
+ }
2098
+ /** Run one ReelStep: pay the winning cells, then scroll each reel down by shifts[col]. */
2099
+ async step(step, stepIndex = 0, opts) {
2100
+ if (this._grid.destroyed)
2101
+ return;
2102
+ this._killed = false;
2103
+ this._pending = step.settledGrid;
2104
+ // 1. celebrate/pay the winning cells.
2105
+ await this._payWins(step, opts);
2106
+ if (this._dead)
2107
+ return;
2108
+ // No shift → just settle the board (defensive; a real ReelStep always shifts something).
2109
+ const hasShift = step.shifts.some((n) => n > 0);
2110
+ if (!this._cfg.enabled || !hasShift) {
2111
+ this._grid.setGrid(step.settledGrid);
2112
+ if (step.winningCells.length)
2113
+ this.advanceMultiplier();
2114
+ this._pending = null;
2115
+ return;
2116
+ }
2117
+ // 2. scroll every reel down by its own N (0 = untouched), all reels concurrently.
2118
+ const turbo = opts?.turbo ? 0.5 : 1;
2119
+ const decel = Math.min(this._cfg.perStepDecelCap, 1 + stepIndex * this._cfg.perStepDecel);
2120
+ const f = turbo * decel;
2121
+ await Promise.all(step.shifts.map((n, col) => n > 0 ? this._scrollReel(col, n, step.settledGrid, f) : Promise.resolve()));
2122
+ if (this._dead) {
2123
+ this._cleanupTemp();
2124
+ return;
2125
+ }
2126
+ // 3. normalise + advance multiplier.
2127
+ this._grid.setGrid(step.settledGrid);
2128
+ this._resetPositions();
2129
+ this._cleanupTemp();
2130
+ this._pending = null;
2131
+ if (step.winningCells.length)
2132
+ this.advanceMultiplier();
2133
+ }
2134
+ /** Highlight + hold the winning cells, then release them back to rest before the shift. */
2135
+ async _payWins(step, opts) {
2136
+ if (!step.winningCells.length)
2137
+ return;
2138
+ const turbo = opts?.turbo ? 0.5 : 1;
2139
+ const t = this._cfg.timings;
2140
+ const hs = this._win.highlightScale;
2141
+ const winSet = new Set(step.winningCells.map((w) => `${w.col}:${w.row}`));
2142
+ if (this._cfg.dimNonWinners)
2143
+ this._dim(winSet);
2144
+ await Promise.all(step.winningCells.map((w) => {
2145
+ const cell = this._grid.getCell(w.col, w.row);
2146
+ if (this._win.glow)
2147
+ cell.setState({ winning: true });
2148
+ return Tween.to(cell, { 'scale.x': hs, 'scale.y': hs }, t.highlight * turbo, easingByName(this._cfg.easings.highlight));
2149
+ }));
2150
+ if (this._dead) {
2151
+ this._undim();
2152
+ return;
2153
+ }
2154
+ await Tween.delay(t.wait * turbo);
2155
+ for (const w of step.winningCells) {
2156
+ const cell = this._grid.getCell(w.col, w.row);
2157
+ cell.setState({});
2158
+ cell.scale.set(1);
2159
+ }
2160
+ this._undim();
2161
+ }
2162
+ /**
2163
+ * Scroll one reel down by `n` positions. A tape carrying the reel's current symbols with `n`
2164
+ * fresh symbols stacked on top slides down by `n` cells: the fresh symbols enter from the top,
2165
+ * the existing ones ride down, and the bottom `n` ride off below the window. Ends on
2166
+ * settledGrid[col]. The tape shares the cells' parent (so the reel mask, if any, clips it).
2167
+ */
2168
+ async _scrollReel(col, n, settledGrid, f) {
2169
+ const rows = this._grid.rowsOf(col);
2170
+ if (rows === 0 || this._dead)
2171
+ return;
2172
+ const realCells = Array.from({ length: rows }, (_, r) => this._grid.getCell(col, r));
2173
+ const layer = realCells[0].parent ?? this._grid;
2174
+ const base = this._grid.cellPosition(col, 0);
2175
+ const step = rows > 1 ? this._grid.cellPosition(col, 1).y - base.y : this._grid.cellSize(col).height;
2176
+ // current visible symbols (top→bottom), captured before we hide them
2177
+ const before = realCells.map((c) => ({ ...c.data }));
2178
+ const { stack, startOffsetCells } = buildReelStepTape(before, settledGrid[col] ?? [], n);
2179
+ // Tape laid out top→bottom at local y = i*step: [incoming(shift)] above [before(rows)].
2180
+ const tape = new pixi_js.Container();
2181
+ tape.x = base.x;
2182
+ for (let i = 0; i < stack.length; i++) {
2183
+ const cell = new SymbolCell({ size: this._grid.cellSize(col), resolve: this._resolve });
2184
+ cell.setData(stack[i]);
2185
+ cell.position.set(0, i * step);
2186
+ tape.addChild(cell);
2187
+ }
2188
+ // Start: the `before` block fills the window; the incoming block sits above it (masked off).
2189
+ tape.y = base.y + startOffsetCells * step;
2190
+ realCells.forEach((c) => (c.visible = false));
2191
+ layer.addChild(tape);
2192
+ this._temp.push(tape);
2193
+ // Slide down by `shift` positions, with a small overshoot then settle-back.
2194
+ const overshoot = step * 0.12;
2195
+ await Tween.to(tape, { y: base.y + overshoot }, this._cfg.timings.drop * f, easingByName(this._cfg.easings.drop));
2196
+ if (this._dead)
2197
+ return;
2198
+ await Tween.to(tape, { y: base.y }, Math.max(90, this._cfg.timings.refill * f), easingByName('easeOutQuad'));
2199
+ if (this._dead)
2200
+ return;
2201
+ // Hand the settled symbols back to the real cells.
2202
+ for (let r = 0; r < rows; r++)
2203
+ realCells[r].setData(settledGrid[col]?.[r] ?? { symbol: null });
2204
+ realCells.forEach((c) => (c.visible = true));
2205
+ tape.destroy();
2206
+ this._temp = this._temp.filter((tp) => tp !== tape);
2207
+ }
2208
+ _dim(winSet) {
2209
+ for (let c = 0; c < this._grid.cols; c++)
2210
+ for (let r = 0; r < this._grid.rowsOf(c); r++)
2211
+ if (!winSet.has(`${c}:${r}`))
2212
+ this._grid.getCell(c, r).alpha = this._cfg.dimAlpha;
2213
+ }
2214
+ _undim() {
2215
+ for (let c = 0; c < this._grid.cols; c++)
2216
+ for (let r = 0; r < this._grid.rowsOf(c); r++) {
2217
+ const cell = this._grid.getCell(c, r);
2218
+ if (cell.alpha !== 0)
2219
+ cell.alpha = 1;
2220
+ }
2221
+ }
2222
+ _resetPositions() {
2223
+ if (this._grid.destroyed)
2224
+ return;
2225
+ for (let c = 0; c < this._grid.cols; c++)
2226
+ for (let r = 0; r < this._grid.rowsOf(c); r++) {
2227
+ const cell = this._grid.getCell(c, r);
2228
+ const { x, y } = this._grid.cellPosition(c, r);
2229
+ cell.position.set(x, y);
2230
+ cell.scale.set(1);
2231
+ cell.alpha = 1;
2232
+ cell.visible = true;
2233
+ }
2234
+ }
2235
+ _cleanupTemp() {
2236
+ for (const t of this._temp)
2237
+ if (!t.destroyed)
2238
+ t.destroy();
2239
+ this._temp = [];
2240
+ }
2241
+ /** Hard-cancel: kill tweens, drop tapes, snap to the in-flight step's settled board. */
2242
+ skip() {
2243
+ this._killed = true;
2244
+ for (let c = 0; c < this._grid.cols; c++)
2245
+ for (let r = 0; r < this._grid.rowsOf(c); r++)
2246
+ Tween.killTweensOf(this._grid.getCell(c, r));
2247
+ this._cleanupTemp();
2248
+ if (this._pending && !this._grid.destroyed)
2249
+ this._grid.setGrid(this._pending);
2250
+ this._pending = null;
2251
+ this._resetPositions();
2252
+ }
2253
+ }
2254
+
2028
2255
  // packages/game-engine/src/slot/features/types.ts
2029
2256
  //
2030
2257
  // Uniform interface for special reel feature mechanics. Each feature is a presentation module:
@@ -2573,6 +2800,7 @@ function createReelSystem(opts) {
2573
2800
  let spin;
2574
2801
  let anticipation;
2575
2802
  let tumble;
2803
+ let reelStepCtl;
2576
2804
  let board = opts.board ?? emptyBoard(config);
2577
2805
  // custom features keyed by id; built-ins live in FEATURES/FEATURE_LIST
2578
2806
  const custom = new Map();
@@ -2595,6 +2823,7 @@ function createReelSystem(opts) {
2595
2823
  if (grid) {
2596
2824
  spin?.skip();
2597
2825
  tumble?.skip();
2826
+ reelStepCtl?.skip();
2598
2827
  for (const child of fx?.children.slice() ?? [])
2599
2828
  Tween.killTweensOf(child);
2600
2829
  grid.destroy({ children: true });
@@ -2623,6 +2852,7 @@ function createReelSystem(opts) {
2623
2852
  spin = new SpinEngine(grid, resolve, config.motion, config.win);
2624
2853
  anticipation = new AnticipationController(config.anticipation);
2625
2854
  tumble = new TumbleController(grid, config.cascade, config.win);
2855
+ reelStepCtl = new ReelStepController(grid, resolve, config.cascade, config.win);
2626
2856
  grid.setGrid(board);
2627
2857
  }
2628
2858
  function geometryChanged(next) {
@@ -2678,6 +2908,8 @@ function createReelSystem(opts) {
2678
2908
  anticipation.setConfig(config.anticipation);
2679
2909
  tumble.setConfig(config.cascade);
2680
2910
  tumble.setWin(config.win);
2911
+ reelStepCtl.setConfig(config.cascade);
2912
+ reelStepCtl.setWin(config.win);
2681
2913
  }
2682
2914
  },
2683
2915
  update(partial) {
@@ -2702,7 +2934,8 @@ function createReelSystem(opts) {
2702
2934
  board = target;
2703
2935
  },
2704
2936
  get multiplier() {
2705
- return tumble.multiplier;
2937
+ // cascade and reelStep share the same config start; only the active mechanic climbs.
2938
+ return Math.max(tumble.multiplier, reelStepCtl.multiplier);
2706
2939
  },
2707
2940
  async cascade(steps, cOpts) {
2708
2941
  // keep the multiplier climbing across free-spins when configured; otherwise reset per spin
@@ -2716,6 +2949,17 @@ function createReelSystem(opts) {
2716
2949
  if (config.cascade.multiplier.enabled)
2717
2950
  log?.(`Cascade multiplier ×${tumble.multiplier}`);
2718
2951
  },
2952
+ async reelStep(steps, rOpts) {
2953
+ const persist = config.cascade.multiplier.persistInFreeSpins && !!rOpts?.freeSpins;
2954
+ if (!persist)
2955
+ reelStepCtl.resetMultiplier();
2956
+ for (let i = 0; i < steps.length; i++) {
2957
+ await reelStepCtl.step(steps[i], i, rOpts);
2958
+ board = steps[i].settledGrid;
2959
+ }
2960
+ if (config.cascade.multiplier.enabled)
2961
+ log?.(`ReelStep multiplier ×${reelStepCtl.multiplier}`);
2962
+ },
2719
2963
  registerFeature(feature) {
2720
2964
  custom.set(feature.key, feature);
2721
2965
  },
@@ -2747,6 +2991,7 @@ function createReelSystem(opts) {
2747
2991
  skip() {
2748
2992
  spin.skip();
2749
2993
  tumble.skip();
2994
+ reelStepCtl.skip();
2750
2995
  // kill in-flight overlay tweens (labels/rings) so a rebuild never animates destroyed nodes
2751
2996
  for (const child of fx.children.slice())
2752
2997
  Tween.killTweensOf(child);
@@ -2942,9 +3187,11 @@ exports.PRESETS = PRESETS;
2942
3187
  exports.PRESET_LIST = PRESET_LIST;
2943
3188
  exports.ReelGrid = ReelGrid;
2944
3189
  exports.ReelSpinController = ReelSpinController;
3190
+ exports.ReelStepController = ReelStepController;
2945
3191
  exports.SpinEngine = SpinEngine;
2946
3192
  exports.SymbolCell = SymbolCell;
2947
3193
  exports.TumbleController = TumbleController;
3194
+ exports.buildReelStepTape = buildReelStepTape;
2948
3195
  exports.cellPositionOf = cellPositionOf;
2949
3196
  exports.createReelSystem = createReelSystem;
2950
3197
  exports.easingByName = easingByName;