@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.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 +3 -3
- 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
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
|
-
|
|
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;
|