@energy8platform/game-engine 0.20.0 → 0.22.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 +18 -3
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +18 -3
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +18 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +3 -5
- package/dist/host.esm.js +18 -3
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +18 -2535
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -1020
- package/dist/index.esm.js +20 -2525
- package/dist/index.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -61
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -28
- package/dist/slot.esm.js +1857 -62
- package/dist/slot.esm.js.map +1 -1
- package/dist/vite.cjs.js +0 -5
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +0 -5
- package/dist/vite.esm.js.map +1 -1
- package/package.json +2 -35
- package/src/animation/Tween.ts +14 -3
- package/src/host/index.ts +0 -1
- package/src/host/types.ts +0 -3
- package/src/index.ts +0 -28
- 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 -2
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
- package/src/vite/index.ts +0 -5
- package/dist/react-jsx.cjs.js +0 -3
- package/dist/react-jsx.cjs.js.map +0 -1
- package/dist/react-jsx.d.ts +0 -602
- package/dist/react-jsx.esm.js +0 -2
- package/dist/react-jsx.esm.js.map +0 -1
- package/dist/react.cjs.js +0 -3787
- package/dist/react.cjs.js.map +0 -1
- package/dist/react.d.ts +0 -1467
- package/dist/react.esm.js +0 -3771
- package/dist/react.esm.js.map +0 -1
- package/dist/ui.cjs.js +0 -2999
- package/dist/ui.cjs.js.map +0 -1
- package/dist/ui.d.ts +0 -1116
- package/dist/ui.esm.js +0 -2983
- package/dist/ui.esm.js.map +0 -1
- package/src/react/EngineContext.ts +0 -26
- package/src/react/ReactScene.ts +0 -88
- package/src/react/applyProps.ts +0 -271
- package/src/react/catalogue.ts +0 -17
- package/src/react/createPixiRoot.ts +0 -31
- package/src/react/extendAll.ts +0 -74
- package/src/react/hooks.ts +0 -46
- package/src/react/index.ts +0 -29
- package/src/react/jsx-runtime.ts +0 -346
- package/src/react/reconciler.ts +0 -338
- package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
- package/src/state/StateMachine.ts +0 -231
- package/src/state/index.ts +0 -1
- package/src/ui/BalanceDisplay.ts +0 -159
- package/src/ui/Button.ts +0 -304
- package/src/ui/FlexContainer.ts +0 -775
- package/src/ui/Label.ts +0 -124
- package/src/ui/LabelValue.ts +0 -122
- package/src/ui/Layout.ts +0 -291
- package/src/ui/Modal.ts +0 -170
- package/src/ui/Panel.ts +0 -204
- package/src/ui/ProgressBar.ts +0 -170
- package/src/ui/ScrollContainer.ts +0 -478
- package/src/ui/Slider.ts +0 -241
- package/src/ui/Toast.ts +0 -150
- package/src/ui/Toggle.ts +0 -201
- package/src/ui/WinDisplay.ts +0 -145
- package/src/ui/index.ts +0 -33
- package/src/ui/view.ts +0 -28
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/motion/AnticipationController.ts
|
|
2
|
+
//
|
|
3
|
+
// Decides which trailing reels get the "anticipation" slow-down treatment, based purely on
|
|
4
|
+
// the already-resolved landing grid (presentation only — never a secondary outcome decision).
|
|
5
|
+
|
|
6
|
+
import { Tween } from '../../animation';
|
|
7
|
+
import { easingByName } from '../anim/easing-map';
|
|
8
|
+
import type { ReelGrid } from '../grid/ReelGrid';
|
|
9
|
+
import type { CellData } from '../grid/SymbolCell';
|
|
10
|
+
import type { AnticipationConfig } from '../config/ReelSystemConfig';
|
|
11
|
+
|
|
12
|
+
export interface AnticipationDecision {
|
|
13
|
+
active: boolean;
|
|
14
|
+
/** Reel indices that should spin slower / longer. */
|
|
15
|
+
reels: number[];
|
|
16
|
+
slowdown: number;
|
|
17
|
+
holdMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class AnticipationController {
|
|
21
|
+
private _cfg: AnticipationConfig;
|
|
22
|
+
constructor(cfg: AnticipationConfig) {
|
|
23
|
+
this._cfg = cfg;
|
|
24
|
+
}
|
|
25
|
+
setConfig(cfg: AnticipationConfig): void {
|
|
26
|
+
this._cfg = cfg;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Count how many trigger symbols land on a given reel. */
|
|
30
|
+
private countOnReel(reel: CellData[]): number {
|
|
31
|
+
let n = 0;
|
|
32
|
+
for (const c of reel) if (c?.symbol && this._cfg.triggerSymbols.includes(c.symbol)) n++;
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decide anticipation from the landing grid. Trigger symbols are counted left→right;
|
|
38
|
+
* once the running total reaches `threshold`, every still-spinning reel after that point
|
|
39
|
+
* is flagged for the slow treatment (this mirrors "searching for the last scatter").
|
|
40
|
+
*/
|
|
41
|
+
decide(targetGrid: CellData[][]): AnticipationDecision {
|
|
42
|
+
if (!this._cfg.enabled) return { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(this._cfg.reels)) {
|
|
45
|
+
// explicit reel list — arm only if the threshold is met somewhere on the board
|
|
46
|
+
const total = targetGrid.reduce((sum, reel) => sum + this.countOnReel(reel), 0);
|
|
47
|
+
const active = total >= this._cfg.threshold;
|
|
48
|
+
return active
|
|
49
|
+
? {
|
|
50
|
+
active,
|
|
51
|
+
reels: this._cfg.reels.slice(),
|
|
52
|
+
slowdown: this._cfg.slowdownFactor,
|
|
53
|
+
holdMs: this._cfg.holdMs,
|
|
54
|
+
}
|
|
55
|
+
: { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 'trailing': find the reel where the cumulative count hits the threshold
|
|
59
|
+
let running = 0;
|
|
60
|
+
let armReel = -1;
|
|
61
|
+
for (let c = 0; c < targetGrid.length; c++) {
|
|
62
|
+
running += this.countOnReel(targetGrid[c] ?? []);
|
|
63
|
+
if (running >= this._cfg.threshold) {
|
|
64
|
+
armReel = c;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (armReel < 0) return { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
69
|
+
|
|
70
|
+
const reels: number[] = [];
|
|
71
|
+
for (let c = armReel + 1; c < targetGrid.length; c++) reels.push(c);
|
|
72
|
+
if (reels.length === 0) return { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
73
|
+
return { active: true, reels, slowdown: this._cfg.slowdownFactor, holdMs: this._cfg.holdMs };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Optionally zoom the grid in while anticipating, then settle back. Returns a reset fn. */
|
|
77
|
+
async zoomIn(grid: ReelGrid): Promise<() => Promise<void>> {
|
|
78
|
+
if (!this._cfg.zoom.enabled) return async () => {};
|
|
79
|
+
const sx = grid.scale.x;
|
|
80
|
+
const sy = grid.scale.y;
|
|
81
|
+
await Tween.to(
|
|
82
|
+
grid,
|
|
83
|
+
{ 'scale.x': sx * this._cfg.zoom.scale, 'scale.y': sy * this._cfg.zoom.scale },
|
|
84
|
+
this._cfg.zoom.ms,
|
|
85
|
+
easingByName('easeOutCubic'),
|
|
86
|
+
);
|
|
87
|
+
return async () => {
|
|
88
|
+
await Tween.to(
|
|
89
|
+
grid,
|
|
90
|
+
{ 'scale.x': sx, 'scale.y': sy },
|
|
91
|
+
this._cfg.zoom.ms,
|
|
92
|
+
easingByName('easeOutCubic'),
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/motion/SpinEngine.ts
|
|
2
|
+
//
|
|
3
|
+
// Configurable spin motion. Supports three styles — 'swap' (texture-swap ring),
|
|
4
|
+
// 'strip' (a tape of symbols slides past a masked window) and 'cascade-drop'
|
|
5
|
+
// (symbols drop in from above) — plus stop modes, turbo/intensity scaling, motion
|
|
6
|
+
// blur, settle-bounce, squash-on-impact and slam (quick) stop.
|
|
7
|
+
//
|
|
8
|
+
// Presentation only: the landing grid is the already-resolved outcome. The engine
|
|
9
|
+
// never re-rolls a result.
|
|
10
|
+
|
|
11
|
+
import { BlurFilter, Container } from 'pixi.js';
|
|
12
|
+
import { Tween } from '../../animation';
|
|
13
|
+
import { EASING_BY_NAME, easingByName } from '../anim/easing-map';
|
|
14
|
+
import type { ReelGrid } from '../grid/ReelGrid';
|
|
15
|
+
import { SymbolCell, type CellData } from '../grid/SymbolCell';
|
|
16
|
+
import type { SymbolResolver } from '../grid/SymbolView';
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_REEL_CONFIG,
|
|
19
|
+
INTENSITY_SCALE,
|
|
20
|
+
type MotionConfig,
|
|
21
|
+
type WinConfig,
|
|
22
|
+
} from '../config/ReelSystemConfig';
|
|
23
|
+
|
|
24
|
+
export interface SpinData {
|
|
25
|
+
/** Target board: targetGrid[col][row]. */
|
|
26
|
+
targetGrid: CellData[][];
|
|
27
|
+
/** Optional per-reel symbol tape for the spinning illusion (defaults to random of the landing symbols). */
|
|
28
|
+
strip?: (reel: number) => string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SpinRunOpts {
|
|
32
|
+
turbo?: boolean;
|
|
33
|
+
/** Reels to slow for anticipation (computed by the ReelSystem from config + targetGrid). */
|
|
34
|
+
anticipateReels?: number[];
|
|
35
|
+
anticipateSlowdown?: number;
|
|
36
|
+
anticipateHoldMs?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ReelStopPlan {
|
|
40
|
+
reel: number;
|
|
41
|
+
/** Wall-clock time (ms) from spin start when this reel stops. */
|
|
42
|
+
stopTime: number;
|
|
43
|
+
landing: CellData[];
|
|
44
|
+
settle: { amp: number; ms: number };
|
|
45
|
+
anticipated: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class SpinEngine {
|
|
49
|
+
private _grid: ReelGrid;
|
|
50
|
+
private _resolve: SymbolResolver;
|
|
51
|
+
private _cfg: MotionConfig;
|
|
52
|
+
private _win: WinConfig = DEFAULT_REEL_CONFIG.win;
|
|
53
|
+
private _killed = false;
|
|
54
|
+
private _shaking = false;
|
|
55
|
+
private _temp: Container[] = [];
|
|
56
|
+
|
|
57
|
+
constructor(grid: ReelGrid, resolve: SymbolResolver, cfg: MotionConfig, win?: WinConfig) {
|
|
58
|
+
this._grid = grid;
|
|
59
|
+
this._resolve = resolve;
|
|
60
|
+
this._cfg = cfg;
|
|
61
|
+
if (win) this._win = win;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setConfig(cfg: MotionConfig): void {
|
|
65
|
+
this._cfg = cfg;
|
|
66
|
+
}
|
|
67
|
+
setWin(win: WinConfig): void {
|
|
68
|
+
this._win = win;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Quick decaying frame shake when a landed reel carries a configured trigger symbol. */
|
|
72
|
+
private async _frameShake(landing: CellData[]): Promise<void> {
|
|
73
|
+
const fs = this._win.frameShake;
|
|
74
|
+
if (!fs.enabled || this._shaking || this._killed) return;
|
|
75
|
+
const triggers = fs.onlyOnSymbols;
|
|
76
|
+
if (triggers && !landing.some((c) => c.symbol && triggers.includes(c.symbol))) return;
|
|
77
|
+
this._shaking = true;
|
|
78
|
+
const baseX = this._grid.x;
|
|
79
|
+
const amp = fs.amp * INTENSITY_SCALE[this._cfg.intensity];
|
|
80
|
+
const steps = 4;
|
|
81
|
+
for (let i = 0; i < steps && !this._killed; i++) {
|
|
82
|
+
const a = amp * (1 - i / steps) * (i % 2 ? -1 : 1);
|
|
83
|
+
await Tween.to(
|
|
84
|
+
this._grid,
|
|
85
|
+
{ x: baseX + a },
|
|
86
|
+
fs.ms / (steps + 1),
|
|
87
|
+
EASING_BY_NAME['easeOutQuad'],
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!this._killed && !this._grid.destroyed) this._grid.x = baseX;
|
|
91
|
+
this._shaking = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private scale(opts?: SpinRunOpts): number {
|
|
95
|
+
const turbo = opts?.turbo ? this._cfg.turboFactor : 1;
|
|
96
|
+
return turbo * INTENSITY_SCALE[this._cfg.intensity];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** PURE: per-reel stop schedule. No Pixi mutation. */
|
|
100
|
+
plan(data: SpinData, opts?: SpinRunOpts): ReelStopPlan[] {
|
|
101
|
+
const f = this.scale(opts);
|
|
102
|
+
const cols = this._grid.cols;
|
|
103
|
+
const order = (reel: number) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
|
|
104
|
+
const anticipate = new Set(opts?.anticipateReels ?? []);
|
|
105
|
+
const out: ReelStopPlan[] = [];
|
|
106
|
+
for (let reel = 0; reel < cols; reel++) {
|
|
107
|
+
const idx = order(reel);
|
|
108
|
+
let stopTime: number;
|
|
109
|
+
if (this._cfg.stopMode === 'sync') stopTime = (this._cfg.spinUp + this._cfg.hold) * f;
|
|
110
|
+
else if (this._cfg.stopMode === 'random')
|
|
111
|
+
stopTime =
|
|
112
|
+
(this._cfg.spinUp +
|
|
113
|
+
this._cfg.hold +
|
|
114
|
+
idx * this._cfg.stopStagger * (0.4 + (reel % 3) * 0.3)) *
|
|
115
|
+
f;
|
|
116
|
+
else stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
|
|
117
|
+
|
|
118
|
+
const isAnticipated = anticipate.has(reel);
|
|
119
|
+
if (isAnticipated) stopTime += (opts?.anticipateHoldMs ?? 0) * f;
|
|
120
|
+
|
|
121
|
+
out.push({
|
|
122
|
+
reel,
|
|
123
|
+
stopTime,
|
|
124
|
+
landing: data.targetGrid[reel] ?? [],
|
|
125
|
+
settle: { amp: this._cfg.settle.amp, ms: this._cfg.settle.ms * f },
|
|
126
|
+
anticipated: isAnticipated,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Execute the spin for every reel concurrently. */
|
|
133
|
+
async run(data: SpinData, opts?: SpinRunOpts): Promise<void> {
|
|
134
|
+
this._killed = false;
|
|
135
|
+
this._temp = [];
|
|
136
|
+
const plan = this.plan(data, opts);
|
|
137
|
+
const f = this.scale(opts);
|
|
138
|
+
await Promise.all(plan.map((p) => this._runReel(p, data, opts, f)));
|
|
139
|
+
this._cleanupTemp();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async _runReel(
|
|
143
|
+
p: ReelStopPlan,
|
|
144
|
+
data: SpinData,
|
|
145
|
+
opts: SpinRunOpts | undefined,
|
|
146
|
+
f: number,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
if (this._killed) return;
|
|
149
|
+
switch (this._cfg.style) {
|
|
150
|
+
case 'strip':
|
|
151
|
+
return this._runStrip(p, data, opts, f);
|
|
152
|
+
case 'cascade-drop':
|
|
153
|
+
return this._runDrop(p, opts, f);
|
|
154
|
+
case 'swap':
|
|
155
|
+
default:
|
|
156
|
+
return this._runSwap(p, data, opts, f);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). */
|
|
161
|
+
private slowOf(p: ReelStopPlan, opts?: SpinRunOpts): number {
|
|
162
|
+
return p.anticipated ? Math.max(1, 1 / (opts?.anticipateSlowdown ?? 1)) : 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── swap: cycle symbols quickly in the real cells, then land ──────────────
|
|
166
|
+
private async _runSwap(
|
|
167
|
+
p: ReelStopPlan,
|
|
168
|
+
data: SpinData,
|
|
169
|
+
opts: SpinRunOpts | undefined,
|
|
170
|
+
f: number,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
const rows = this._grid.rowsOf(p.reel);
|
|
173
|
+
const cells = Array.from({ length: rows }, (_, r) => this._grid.getCell(p.reel, r));
|
|
174
|
+
const strip = data.strip?.(p.reel) ?? p.landing.map((c) => c.symbol ?? '').filter(Boolean);
|
|
175
|
+
const tape = strip.length ? strip : ['?'];
|
|
176
|
+
const blur = this._applyBlur(cells, true);
|
|
177
|
+
const tickMs = 1000 / 30;
|
|
178
|
+
// anticipation makes the reel spin longer before it lands
|
|
179
|
+
const ticks = Math.max(6, Math.floor((p.stopTime * this.slowOf(p, opts)) / tickMs));
|
|
180
|
+
for (let i = 0; i < ticks; i++) {
|
|
181
|
+
if (this._killed) break;
|
|
182
|
+
for (let r = 0; r < cells.length; r++)
|
|
183
|
+
cells[r].setData({ symbol: tape[(i + r) % tape.length] || null });
|
|
184
|
+
await Tween.delay(tickMs);
|
|
185
|
+
}
|
|
186
|
+
blur?.();
|
|
187
|
+
for (let r = 0; r < cells.length; r++) cells[r].setData(p.landing[r] ?? { symbol: null });
|
|
188
|
+
await this._settle(p.reel, p.settle, f);
|
|
189
|
+
await this._frameShake(p.landing);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── strip: a tape Container slides down past the window, then lands ────────
|
|
193
|
+
private async _runStrip(
|
|
194
|
+
p: ReelStopPlan,
|
|
195
|
+
data: SpinData,
|
|
196
|
+
opts: SpinRunOpts | undefined,
|
|
197
|
+
f: number,
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
const rows = this._grid.rowsOf(p.reel);
|
|
200
|
+
const realCells = Array.from({ length: rows }, (_, r) => this._grid.getCell(p.reel, r));
|
|
201
|
+
const base = this._grid.cellPosition(p.reel, 0);
|
|
202
|
+
const step = this._grid.cellPosition(p.reel, 1).y - base.y;
|
|
203
|
+
const tapeLen = Math.max(this._cfg.symbolsPerReel, rows + 4);
|
|
204
|
+
const strip = data.strip?.(p.reel) ?? p.landing.map((c) => c.symbol ?? '').filter(Boolean);
|
|
205
|
+
const pool = strip.length ? strip : ['?'];
|
|
206
|
+
|
|
207
|
+
// Tape cells laid out top→bottom at local y = i*step. The bottom `rows` cells carry the
|
|
208
|
+
// landing symbols; everything above is filler.
|
|
209
|
+
const tape = new Container();
|
|
210
|
+
tape.x = base.x;
|
|
211
|
+
const landingStart = tapeLen - rows;
|
|
212
|
+
for (let i = 0; i < tapeLen; i++) {
|
|
213
|
+
const cell = new SymbolCell({ size: this._grid.cellSize, resolve: this._resolve });
|
|
214
|
+
const sym =
|
|
215
|
+
i >= landingStart
|
|
216
|
+
? (p.landing[i - landingStart]?.symbol ?? null)
|
|
217
|
+
: (pool[i % pool.length] ?? null);
|
|
218
|
+
cell.setData({ symbol: sym });
|
|
219
|
+
cell.position.set(0, i * step);
|
|
220
|
+
tape.addChild(cell);
|
|
221
|
+
}
|
|
222
|
+
// At rest the bottom block aligns with the window; start shifted up by the whole tape.
|
|
223
|
+
const restY = base.y - landingStart * step;
|
|
224
|
+
const startY = restY - tapeLen * step;
|
|
225
|
+
tape.y = startY;
|
|
226
|
+
realCells.forEach((c) => (c.visible = false));
|
|
227
|
+
this._grid.addChild(tape);
|
|
228
|
+
this._temp.push(tape);
|
|
229
|
+
const clearBlur = this._applyBlur([tape], true);
|
|
230
|
+
|
|
231
|
+
const slow = this.slowOf(p, opts);
|
|
232
|
+
// overshoot/settle honour the configured settle (amp in px, easing)
|
|
233
|
+
const overshoot = p.settle.amp || step * 0.18;
|
|
234
|
+
await Tween.to(
|
|
235
|
+
tape,
|
|
236
|
+
{ y: restY + overshoot },
|
|
237
|
+
p.stopTime * slow,
|
|
238
|
+
easingByName('easeInOutQuad'),
|
|
239
|
+
);
|
|
240
|
+
if (this._killed) {
|
|
241
|
+
clearBlur?.();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
clearBlur?.();
|
|
245
|
+
await Tween.to(
|
|
246
|
+
tape,
|
|
247
|
+
{ y: restY },
|
|
248
|
+
Math.max(120, p.settle.ms),
|
|
249
|
+
easingByName(this._cfg.settle.easing),
|
|
250
|
+
);
|
|
251
|
+
// hand the result back to the real cells
|
|
252
|
+
for (let r = 0; r < rows; r++) realCells[r].setData(p.landing[r] ?? { symbol: null });
|
|
253
|
+
realCells.forEach((c) => (c.visible = true));
|
|
254
|
+
tape.destroy();
|
|
255
|
+
this._temp = this._temp.filter((t) => t !== tape);
|
|
256
|
+
// squash the real cells on impact when enabled
|
|
257
|
+
if (this._cfg.squash.enabled) await Promise.all(realCells.map((c) => this._squashCell(c, f)));
|
|
258
|
+
await this._frameShake(p.landing);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── cascade-drop: symbols drop in from above with stagger + bounce + squash ─
|
|
262
|
+
private async _runDrop(p: ReelStopPlan, opts: SpinRunOpts | undefined, f: number): Promise<void> {
|
|
263
|
+
const rows = this._grid.rowsOf(p.reel);
|
|
264
|
+
const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
|
|
265
|
+
const slow = this.slowOf(p, opts); // anticipation drops the reel in more slowly
|
|
266
|
+
await Promise.all(
|
|
267
|
+
Array.from({ length: rows }, (_, r) => r).map(async (r) => {
|
|
268
|
+
if (this._killed) return;
|
|
269
|
+
const cell = this._grid.getCell(p.reel, r);
|
|
270
|
+
const to = this._grid.cellPosition(p.reel, r);
|
|
271
|
+
cell.setData(p.landing[r] ?? { symbol: null });
|
|
272
|
+
cell.position.set(to.x, to.y - step * (rows + 1));
|
|
273
|
+
cell.alpha = 1;
|
|
274
|
+
const delay = (p.reel * this._cfg.stopStagger * 0.4 + r * 24) * f * slow;
|
|
275
|
+
if (delay) await Tween.delay(delay);
|
|
276
|
+
await Tween.to(
|
|
277
|
+
cell,
|
|
278
|
+
{ 'position.y': to.y },
|
|
279
|
+
this._cfg.spinUp * 0.6 * f * slow,
|
|
280
|
+
easingByName(this._cfg.settle.easing),
|
|
281
|
+
);
|
|
282
|
+
await this._squashCell(cell, f);
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
await this._frameShake(p.landing);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── shared helpers ────────────────────────────────────────────────────────
|
|
289
|
+
private async _settle(
|
|
290
|
+
reel: number,
|
|
291
|
+
settle: { amp: number; ms: number },
|
|
292
|
+
f: number,
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
if (this._killed || settle.amp <= 0) return;
|
|
295
|
+
const cells = Array.from({ length: this._grid.rowsOf(reel) }, (_, r) =>
|
|
296
|
+
this._grid.getCell(reel, r),
|
|
297
|
+
);
|
|
298
|
+
const parent = cells[0]?.parent;
|
|
299
|
+
if (!parent) return;
|
|
300
|
+
// bounce the whole reel column by moving each cell, then squash the impact
|
|
301
|
+
await Promise.all(
|
|
302
|
+
cells.map((c) => {
|
|
303
|
+
const y = c.y;
|
|
304
|
+
return Tween.fromTo(
|
|
305
|
+
c,
|
|
306
|
+
{ y: y - settle.amp },
|
|
307
|
+
{ y },
|
|
308
|
+
settle.ms,
|
|
309
|
+
EASING_BY_NAME[this._cfg.settle.easing] ?? EASING_BY_NAME['easeOutBack'],
|
|
310
|
+
);
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
if (this._cfg.squash.enabled) await Promise.all(cells.map((c) => this._squashCell(c, f)));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async _squashCell(cell: SymbolCell, f: number): Promise<void> {
|
|
317
|
+
if (!this._cfg.squash.enabled || this._killed) return;
|
|
318
|
+
const { scaleX, scaleY, ms } = this._cfg.squash;
|
|
319
|
+
await Tween.to(
|
|
320
|
+
cell,
|
|
321
|
+
{ 'scale.x': scaleX, 'scale.y': scaleY },
|
|
322
|
+
ms * 0.5 * f,
|
|
323
|
+
EASING_BY_NAME['easeOutQuad'],
|
|
324
|
+
);
|
|
325
|
+
await Tween.to(
|
|
326
|
+
cell,
|
|
327
|
+
{ 'scale.x': 1, 'scale.y': 1 },
|
|
328
|
+
ms * 0.5 * f,
|
|
329
|
+
EASING_BY_NAME['easeOutBack'],
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Apply motion blur (alpha + optional BlurFilter) to targets; returns a disposer that clears it. */
|
|
334
|
+
private _applyBlur(targets: Container[], _motion: boolean): (() => void) | null {
|
|
335
|
+
if (!this._cfg.blur.enabled) return null;
|
|
336
|
+
const filter =
|
|
337
|
+
this._cfg.blur.strength > 0
|
|
338
|
+
? new BlurFilter({ strength: this._cfg.blur.strength, quality: 2 })
|
|
339
|
+
: null;
|
|
340
|
+
for (const t of targets) {
|
|
341
|
+
t.alpha = this._cfg.blur.alpha;
|
|
342
|
+
if (filter) t.filters = [filter];
|
|
343
|
+
}
|
|
344
|
+
return () => {
|
|
345
|
+
for (const t of targets) {
|
|
346
|
+
t.alpha = 1;
|
|
347
|
+
t.filters = [];
|
|
348
|
+
}
|
|
349
|
+
filter?.destroy();
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private _cleanupTemp(): void {
|
|
354
|
+
for (const t of this._temp) {
|
|
355
|
+
if (!t.destroyed) t.destroy();
|
|
356
|
+
}
|
|
357
|
+
this._temp = [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Slam / quick stop: snap everything to the target and stop animating. */
|
|
361
|
+
skip(): void {
|
|
362
|
+
if (!this._cfg.slamStop && !this._killed) {
|
|
363
|
+
// even without slam, skip() is the hard-cancel path
|
|
364
|
+
}
|
|
365
|
+
this._killed = true;
|
|
366
|
+
this._shaking = false;
|
|
367
|
+
this._cleanupTemp();
|
|
368
|
+
if (this._grid.destroyed) return;
|
|
369
|
+
Tween.killTweensOf(this._grid);
|
|
370
|
+
this._grid.x = 0; // undo any in-flight frame shake
|
|
371
|
+
for (let c = 0; c < this._grid.cols; c++) {
|
|
372
|
+
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
373
|
+
const cell = this._grid.getCell(c, r);
|
|
374
|
+
if (cell.destroyed) continue;
|
|
375
|
+
Tween.killTweensOf(cell);
|
|
376
|
+
cell.visible = true;
|
|
377
|
+
cell.alpha = 1;
|
|
378
|
+
cell.filters = [];
|
|
379
|
+
cell.scale.set(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this._cleanupTemp();
|
|
383
|
+
}
|
|
384
|
+
}
|