@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,559 @@
|
|
|
1
|
+
// packages/game-engine/src/slot/config/ReelSystemConfig.ts
|
|
2
|
+
//
|
|
3
|
+
// The single, fully-typed configuration object for the configurable reel system.
|
|
4
|
+
// Everything is optional with sensible defaults — `resolveReelConfig(partial)` deep-merges
|
|
5
|
+
// a partial config onto DEFAULT_REEL_CONFIG so games (and the reel-lab playground) can
|
|
6
|
+
// override only what they need.
|
|
7
|
+
//
|
|
8
|
+
// Design notes are in docs/reels-analysis-and-design.md.
|
|
9
|
+
|
|
10
|
+
import type { CellFrameStyle } from '../grid/SymbolCell';
|
|
11
|
+
|
|
12
|
+
/** Names of easing functions available in the engine's `Easing` map (see anim/easing-map.ts). */
|
|
13
|
+
export type EasingName =
|
|
14
|
+
| 'linear'
|
|
15
|
+
| 'easeInQuad'
|
|
16
|
+
| 'easeOutQuad'
|
|
17
|
+
| 'easeInOutQuad'
|
|
18
|
+
| 'easeInCubic'
|
|
19
|
+
| 'easeOutCubic'
|
|
20
|
+
| 'easeInOutCubic'
|
|
21
|
+
| 'easeInBack'
|
|
22
|
+
| 'easeOutBack'
|
|
23
|
+
| 'easeInOutBack'
|
|
24
|
+
| 'easeOutBounce'
|
|
25
|
+
| 'easeInBounce'
|
|
26
|
+
| 'easeOutElastic'
|
|
27
|
+
| 'easeInSine'
|
|
28
|
+
| 'easeOutSine'
|
|
29
|
+
| 'easeInOutSine';
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Grid
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/** Win-evaluation model. Purely presentational here (affects geometry + highlight), the math lives in Lua. */
|
|
36
|
+
export type EvaluationMode = 'lines' | 'ways' | 'anywhere' | 'cluster' | 'megaways' | 'infinity';
|
|
37
|
+
|
|
38
|
+
export interface GridConfig {
|
|
39
|
+
cols: number;
|
|
40
|
+
/** Uniform row count. Ignored when `rowsPerReel` is set. */
|
|
41
|
+
rows: number;
|
|
42
|
+
/** Per-reel row counts (Megaways / variable-height reels). Length should equal `cols`. */
|
|
43
|
+
rowsPerReel?: number[];
|
|
44
|
+
cellSize: number;
|
|
45
|
+
gap: number;
|
|
46
|
+
evaluation: EvaluationMode;
|
|
47
|
+
/** For Megaways: clamp per-reel rows to [minRows, maxRows]. */
|
|
48
|
+
minRows?: number;
|
|
49
|
+
maxRows?: number;
|
|
50
|
+
/** Megaways top horizontal reel (renders above the grid, adds to ways). */
|
|
51
|
+
topReel?: { size: number } | null;
|
|
52
|
+
mask: boolean;
|
|
53
|
+
frameStyle?: CellFrameStyle;
|
|
54
|
+
decoration?: { padding?: number };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
// Motion
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export type MotionStyle =
|
|
62
|
+
| 'swap' // texture-swap ring (cheap, the engine's original behaviour)
|
|
63
|
+
| 'strip' // strip-scroll: a tape of symbols slides past a masked window
|
|
64
|
+
| 'cascade-drop'; // symbols drop in from above (tumble-style boards)
|
|
65
|
+
|
|
66
|
+
export type StopMode = 'sequential' | 'sync' | 'random';
|
|
67
|
+
export type StopOrder = 'ltr' | 'rtl';
|
|
68
|
+
export type Intensity = 'full' | 'reduced' | 'minimal';
|
|
69
|
+
|
|
70
|
+
export interface SettleConfig {
|
|
71
|
+
/** Overshoot amplitude in px applied to the reel before it springs back. */
|
|
72
|
+
amp: number;
|
|
73
|
+
ms: number;
|
|
74
|
+
easing: EasingName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Squash & stretch on landing impact (Stone-Rush / magnus style). */
|
|
78
|
+
export interface SquashConfig {
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
scaleX: number; // e.g. 1.3 (wider)
|
|
81
|
+
scaleY: number; // e.g. 0.7 (shorter)
|
|
82
|
+
ms: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface BlurConfig {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
/** Alpha applied to in-motion symbols (hot-ross uses 0.82). */
|
|
88
|
+
alpha: number;
|
|
89
|
+
/** Runtime BlurFilter strength factor (PixiJS slots demo uses ~8), 0 disables the filter. */
|
|
90
|
+
strength: number;
|
|
91
|
+
/** Draw vertical motion streaks behind moving symbols. */
|
|
92
|
+
streaks: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface MotionConfig {
|
|
96
|
+
style: MotionStyle;
|
|
97
|
+
/** Base spin-up duration (ms) before the first reel can stop. */
|
|
98
|
+
spinUp: number;
|
|
99
|
+
/** Extra hold (ms) at full speed before deceleration. */
|
|
100
|
+
hold: number;
|
|
101
|
+
/** Delay (ms) between consecutive reel stops in sequential mode. */
|
|
102
|
+
stopStagger: number;
|
|
103
|
+
stopMode: StopMode;
|
|
104
|
+
stopOrder: StopOrder;
|
|
105
|
+
settle: SettleConfig;
|
|
106
|
+
squash: SquashConfig;
|
|
107
|
+
blur: BlurConfig;
|
|
108
|
+
/** Multiply all durations in turbo mode (0.5 = twice as fast). */
|
|
109
|
+
turboFactor: number;
|
|
110
|
+
/** Global animation-intensity scale (accessibility): full=1, reduced=0.7, minimal=0.4. */
|
|
111
|
+
intensity: Intensity;
|
|
112
|
+
/** Allow slam/quick-stop (snaps reels to target without changing outcome). */
|
|
113
|
+
slamStop: boolean;
|
|
114
|
+
/** Symbols visible on a reel tape while spinning (swap/strip). */
|
|
115
|
+
symbolsPerReel: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// Anticipation
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export interface AnticipationConfig {
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
/** Symbols that count toward the anticipation threshold (scatter/bonus). */
|
|
125
|
+
triggerSymbols: string[];
|
|
126
|
+
/** Number of trigger symbols already landed that arms anticipation (N−1, usually 2). */
|
|
127
|
+
threshold: number;
|
|
128
|
+
/** Which reels get the slow treatment: the still-spinning trailing reels, or explicit indices. */
|
|
129
|
+
reels: 'trailing' | number[];
|
|
130
|
+
/** Multiply the trailing reel's spin speed (lower = slower / longer). */
|
|
131
|
+
slowdownFactor: number;
|
|
132
|
+
/** Extra hold (ms) before the final anticipation reel lands (300–500 typical). */
|
|
133
|
+
holdMs: number;
|
|
134
|
+
/** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */
|
|
135
|
+
zoom: { enabled: boolean; scale: number; ms: number };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
// Cascade
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export interface CascadeConfig {
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
/** Animate surviving symbols sliding down into the gaps (true tumble). */
|
|
145
|
+
gravity: boolean;
|
|
146
|
+
timings: {
|
|
147
|
+
reveal: number;
|
|
148
|
+
highlight: number;
|
|
149
|
+
remove: number;
|
|
150
|
+
drop: number;
|
|
151
|
+
refill: number;
|
|
152
|
+
wait: number;
|
|
153
|
+
};
|
|
154
|
+
easings: { highlight: EasingName; remove: EasingName; drop: EasingName };
|
|
155
|
+
/** Slow each successive cascade step to build tension: factor = 1 + step*perStepDecel (capped). */
|
|
156
|
+
perStepDecel: number;
|
|
157
|
+
perStepDecelCap: number;
|
|
158
|
+
/** Dim non-winning symbols during the highlight phase. */
|
|
159
|
+
dimNonWinners: boolean;
|
|
160
|
+
dimAlpha: number;
|
|
161
|
+
/** Running win multiplier that climbs per cascade (Pragmatic Tumble / Avalanche). */
|
|
162
|
+
multiplier: {
|
|
163
|
+
enabled: boolean;
|
|
164
|
+
start: number;
|
|
165
|
+
/** 'add' → +step each step; 'mul' → ×step each step. */
|
|
166
|
+
mode: 'add' | 'mul';
|
|
167
|
+
step: number;
|
|
168
|
+
cap: number | null;
|
|
169
|
+
/** Keep climbing across free-spins instead of resetting per spin. */
|
|
170
|
+
persistInFreeSpins: boolean;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
175
|
+
// Win presentation
|
|
176
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export interface WinConfig {
|
|
179
|
+
highlightScale: number;
|
|
180
|
+
glow: boolean;
|
|
181
|
+
/** Frame shake on landing/win. */
|
|
182
|
+
frameShake: { enabled: boolean; amp: number; ms: number; onlyOnSymbols: string[] | null };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
// Special feature mechanics
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export interface ExpandingWildConfig {
|
|
190
|
+
enabled: boolean;
|
|
191
|
+
symbol: string;
|
|
192
|
+
/** Reels that may host an expanding wild (empty = all). */
|
|
193
|
+
reels: number[];
|
|
194
|
+
/** Expand to the full reel height. */
|
|
195
|
+
toFullReel: boolean;
|
|
196
|
+
ms: number;
|
|
197
|
+
easing: EasingName;
|
|
198
|
+
onlyInFreeSpins: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface StickyConfig {
|
|
202
|
+
enabled: boolean;
|
|
203
|
+
symbols: string[];
|
|
204
|
+
/** How many spins a sticky symbol persists (0 = whole feature/bonus). */
|
|
205
|
+
durationSpins: number;
|
|
206
|
+
/** Ring colour drawn around sticky cells. */
|
|
207
|
+
ringColor: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface WalkingWildConfig {
|
|
211
|
+
enabled: boolean;
|
|
212
|
+
symbol: string;
|
|
213
|
+
direction: 'left' | 'right';
|
|
214
|
+
stepPerSpin: number;
|
|
215
|
+
awardsRespin: boolean;
|
|
216
|
+
asStacked: boolean;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface MultiplierConfig {
|
|
220
|
+
enabled: boolean;
|
|
221
|
+
/** Multiplier symbol id (carries a value), or null when multipliers are per-cell data. */
|
|
222
|
+
symbol: string | null;
|
|
223
|
+
scope: 'perSymbol' | 'perReel' | 'global';
|
|
224
|
+
combine: 'additive' | 'multiplicative';
|
|
225
|
+
attachedToWild: boolean;
|
|
226
|
+
max: number;
|
|
227
|
+
accumulateAcrossFreeSpins: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface MysteryConfig {
|
|
231
|
+
enabled: boolean;
|
|
232
|
+
symbol: string;
|
|
233
|
+
/** Eligible reveal targets; all mystery tiles reveal the SAME single draw per spin. */
|
|
234
|
+
revealPool: string[];
|
|
235
|
+
canRevealWild: boolean;
|
|
236
|
+
ms: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface TransformConfig {
|
|
240
|
+
enabled: boolean;
|
|
241
|
+
/** Trigger symbol whose landing fires the transform (null = data-driven). */
|
|
242
|
+
trigger: string | null;
|
|
243
|
+
/** 'randomLow' picks one low symbol at random; or pin a specific source. */
|
|
244
|
+
source: 'randomLow' | string;
|
|
245
|
+
target: string;
|
|
246
|
+
/** Convert every instance of the source symbol. */
|
|
247
|
+
allInstances: boolean;
|
|
248
|
+
/** Restrict to low→high (upgrade) direction. */
|
|
249
|
+
upgradeOnly: boolean;
|
|
250
|
+
ms: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface GiantConfig {
|
|
254
|
+
enabled: boolean;
|
|
255
|
+
/** Footprint in cells. */
|
|
256
|
+
width: number;
|
|
257
|
+
height: number;
|
|
258
|
+
/** Symbols that may appear giant (empty = any). */
|
|
259
|
+
symbols: string[];
|
|
260
|
+
/** Pick one giant symbol type per spin. */
|
|
261
|
+
chosenPerSpin: boolean;
|
|
262
|
+
onlyInFreeSpins: boolean;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface SplitConfig {
|
|
266
|
+
enabled: boolean;
|
|
267
|
+
symbol: string;
|
|
268
|
+
factor: number; // ×2 typical
|
|
269
|
+
/** Reels that can host the split trigger (xSplit lands on the last reel). */
|
|
270
|
+
reels: number[];
|
|
271
|
+
/** Defer the split execution until all respins finish. */
|
|
272
|
+
deferUntilRespinsEnd: boolean;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface StackedConfig {
|
|
276
|
+
enabled: boolean;
|
|
277
|
+
symbols: string[];
|
|
278
|
+
/** Max stack height (positions of the same symbol on one reel). */
|
|
279
|
+
height: number;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface NudgeConfig {
|
|
283
|
+
enabled: boolean;
|
|
284
|
+
/** Reels eligible to nudge. */
|
|
285
|
+
reels: number[];
|
|
286
|
+
step: number; // ±1 position
|
|
287
|
+
/** xNudge: nudge a stacked wild into full view, +1 multiplier per nudge. */
|
|
288
|
+
toFullReel: boolean;
|
|
289
|
+
multiplierStart: number;
|
|
290
|
+
multiplierPerNudge: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface ReelModifierConfig {
|
|
294
|
+
enabled: boolean;
|
|
295
|
+
/** Random pre-spin modifiers, each with a trigger weight. */
|
|
296
|
+
pool: {
|
|
297
|
+
effect: 'addRows' | 'addWilds' | 'setGiant' | 'guaranteedWilds';
|
|
298
|
+
magnitude: number;
|
|
299
|
+
weight: number;
|
|
300
|
+
}[];
|
|
301
|
+
appliesIn: 'base' | 'freeSpins' | 'both';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface HoldAndSpinConfig {
|
|
305
|
+
enabled: boolean;
|
|
306
|
+
lockSymbols: string[];
|
|
307
|
+
triggerThreshold: number; // e.g. 6
|
|
308
|
+
respinsAwarded: number; // e.g. 3
|
|
309
|
+
/** Reset the respin counter to full whenever a new symbol locks. */
|
|
310
|
+
resetOnNewSymbol: boolean;
|
|
311
|
+
jackpotTiers: string[]; // ['Mini','Minor','Major','Grand']
|
|
312
|
+
fullGridAwardsGrand: boolean;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface RandomWildConfig {
|
|
316
|
+
enabled: boolean;
|
|
317
|
+
/** Fixed count, or [min,max] range. */
|
|
318
|
+
count: number | [number, number];
|
|
319
|
+
sticky: boolean;
|
|
320
|
+
multiplier: number;
|
|
321
|
+
trigger: 'randomBaseSpin' | 'onFreeSpins';
|
|
322
|
+
chance: number; // 0..1 for randomBaseSpin
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface FeaturesConfig {
|
|
326
|
+
expandingWild: ExpandingWildConfig;
|
|
327
|
+
sticky: StickyConfig;
|
|
328
|
+
walkingWild: WalkingWildConfig;
|
|
329
|
+
multiplier: MultiplierConfig;
|
|
330
|
+
mystery: MysteryConfig;
|
|
331
|
+
transform: TransformConfig;
|
|
332
|
+
giant: GiantConfig;
|
|
333
|
+
split: SplitConfig;
|
|
334
|
+
stacked: StackedConfig;
|
|
335
|
+
nudge: NudgeConfig;
|
|
336
|
+
reelModifier: ReelModifierConfig;
|
|
337
|
+
holdAndSpin: HoldAndSpinConfig;
|
|
338
|
+
randomWild: RandomWildConfig;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Ordered list of feature keys (resolve order matters; see docs §3.4). */
|
|
342
|
+
export type FeatureKey = keyof FeaturesConfig;
|
|
343
|
+
export const FEATURE_KEYS: FeatureKey[] = [
|
|
344
|
+
'reelModifier', // pre-spin
|
|
345
|
+
'giant',
|
|
346
|
+
'stacked',
|
|
347
|
+
'mystery', // post-spin reveal
|
|
348
|
+
'expandingWild',
|
|
349
|
+
'walkingWild',
|
|
350
|
+
'randomWild',
|
|
351
|
+
'transform',
|
|
352
|
+
'split',
|
|
353
|
+
'nudge',
|
|
354
|
+
'multiplier',
|
|
355
|
+
'sticky',
|
|
356
|
+
'holdAndSpin',
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
360
|
+
// Root config
|
|
361
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
export interface ReelSystemConfig {
|
|
364
|
+
grid: GridConfig;
|
|
365
|
+
motion: MotionConfig;
|
|
366
|
+
anticipation: AnticipationConfig;
|
|
367
|
+
cascade: CascadeConfig;
|
|
368
|
+
win: WinConfig;
|
|
369
|
+
features: FeaturesConfig;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Deep-partial helper for overrides. */
|
|
373
|
+
export type DeepPartial<T> = {
|
|
374
|
+
[K in keyof T]?: T[K] extends (infer U)[] ? T[K] : T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
378
|
+
// Defaults
|
|
379
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
export const DEFAULT_REEL_CONFIG: ReelSystemConfig = {
|
|
382
|
+
grid: {
|
|
383
|
+
cols: 5,
|
|
384
|
+
rows: 3,
|
|
385
|
+
cellSize: 96,
|
|
386
|
+
gap: 6,
|
|
387
|
+
evaluation: 'lines',
|
|
388
|
+
minRows: 2,
|
|
389
|
+
maxRows: 7,
|
|
390
|
+
topReel: null,
|
|
391
|
+
mask: true,
|
|
392
|
+
decoration: { padding: 0 },
|
|
393
|
+
},
|
|
394
|
+
motion: {
|
|
395
|
+
style: 'swap',
|
|
396
|
+
spinUp: 500,
|
|
397
|
+
hold: 200,
|
|
398
|
+
stopStagger: 120,
|
|
399
|
+
stopMode: 'sequential',
|
|
400
|
+
stopOrder: 'ltr',
|
|
401
|
+
settle: { amp: 7, ms: 240, easing: 'easeOutBack' },
|
|
402
|
+
squash: { enabled: false, scaleX: 1.18, scaleY: 0.82, ms: 90 },
|
|
403
|
+
blur: { enabled: false, alpha: 0.85, strength: 8, streaks: false },
|
|
404
|
+
turboFactor: 0.5,
|
|
405
|
+
intensity: 'full',
|
|
406
|
+
slamStop: true,
|
|
407
|
+
symbolsPerReel: 6,
|
|
408
|
+
},
|
|
409
|
+
anticipation: {
|
|
410
|
+
enabled: false,
|
|
411
|
+
triggerSymbols: ['scatter'],
|
|
412
|
+
threshold: 2,
|
|
413
|
+
reels: 'trailing',
|
|
414
|
+
slowdownFactor: 0.3,
|
|
415
|
+
holdMs: 400,
|
|
416
|
+
zoom: { enabled: false, scale: 1.15, ms: 600 },
|
|
417
|
+
},
|
|
418
|
+
cascade: {
|
|
419
|
+
enabled: false,
|
|
420
|
+
gravity: true,
|
|
421
|
+
timings: { reveal: 300, highlight: 400, remove: 250, drop: 220, refill: 220, wait: 150 },
|
|
422
|
+
easings: { highlight: 'easeOutQuad', remove: 'easeInBack', drop: 'easeOutBounce' },
|
|
423
|
+
perStepDecel: 0.08,
|
|
424
|
+
perStepDecelCap: 1.5,
|
|
425
|
+
dimNonWinners: false,
|
|
426
|
+
dimAlpha: 0.35,
|
|
427
|
+
multiplier: {
|
|
428
|
+
enabled: false,
|
|
429
|
+
start: 1,
|
|
430
|
+
mode: 'add',
|
|
431
|
+
step: 1,
|
|
432
|
+
cap: null,
|
|
433
|
+
persistInFreeSpins: false,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
win: {
|
|
437
|
+
highlightScale: 1.12,
|
|
438
|
+
glow: true,
|
|
439
|
+
frameShake: { enabled: false, amp: 3, ms: 180, onlyOnSymbols: null },
|
|
440
|
+
},
|
|
441
|
+
features: {
|
|
442
|
+
expandingWild: {
|
|
443
|
+
enabled: false,
|
|
444
|
+
symbol: 'wild',
|
|
445
|
+
reels: [],
|
|
446
|
+
toFullReel: true,
|
|
447
|
+
ms: 420,
|
|
448
|
+
easing: 'easeOutBack',
|
|
449
|
+
onlyInFreeSpins: false,
|
|
450
|
+
},
|
|
451
|
+
sticky: { enabled: false, symbols: ['wild'], durationSpins: 3, ringColor: 0xec4899 },
|
|
452
|
+
walkingWild: {
|
|
453
|
+
enabled: false,
|
|
454
|
+
symbol: 'wild',
|
|
455
|
+
direction: 'left',
|
|
456
|
+
stepPerSpin: 1,
|
|
457
|
+
awardsRespin: true,
|
|
458
|
+
asStacked: false,
|
|
459
|
+
},
|
|
460
|
+
multiplier: {
|
|
461
|
+
enabled: false,
|
|
462
|
+
symbol: null,
|
|
463
|
+
scope: 'perSymbol',
|
|
464
|
+
combine: 'additive',
|
|
465
|
+
attachedToWild: false,
|
|
466
|
+
max: 128,
|
|
467
|
+
accumulateAcrossFreeSpins: false,
|
|
468
|
+
},
|
|
469
|
+
mystery: { enabled: false, symbol: 'mystery', revealPool: [], canRevealWild: false, ms: 300 },
|
|
470
|
+
transform: {
|
|
471
|
+
enabled: false,
|
|
472
|
+
trigger: null,
|
|
473
|
+
source: 'randomLow',
|
|
474
|
+
target: 'wild',
|
|
475
|
+
allInstances: true,
|
|
476
|
+
upgradeOnly: false,
|
|
477
|
+
ms: 320,
|
|
478
|
+
},
|
|
479
|
+
giant: {
|
|
480
|
+
enabled: false,
|
|
481
|
+
width: 2,
|
|
482
|
+
height: 2,
|
|
483
|
+
symbols: [],
|
|
484
|
+
chosenPerSpin: false,
|
|
485
|
+
onlyInFreeSpins: false,
|
|
486
|
+
},
|
|
487
|
+
split: { enabled: false, symbol: 'split', factor: 2, reels: [], deferUntilRespinsEnd: true },
|
|
488
|
+
stacked: { enabled: false, symbols: [], height: 3 },
|
|
489
|
+
nudge: {
|
|
490
|
+
enabled: false,
|
|
491
|
+
reels: [],
|
|
492
|
+
step: 1,
|
|
493
|
+
toFullReel: false,
|
|
494
|
+
multiplierStart: 1,
|
|
495
|
+
multiplierPerNudge: 1,
|
|
496
|
+
},
|
|
497
|
+
reelModifier: { enabled: false, pool: [], appliesIn: 'both' },
|
|
498
|
+
holdAndSpin: {
|
|
499
|
+
enabled: false,
|
|
500
|
+
lockSymbols: ['coin'],
|
|
501
|
+
triggerThreshold: 6,
|
|
502
|
+
respinsAwarded: 3,
|
|
503
|
+
resetOnNewSymbol: true,
|
|
504
|
+
jackpotTiers: ['Mini', 'Minor', 'Major', 'Grand'],
|
|
505
|
+
fullGridAwardsGrand: true,
|
|
506
|
+
},
|
|
507
|
+
randomWild: {
|
|
508
|
+
enabled: false,
|
|
509
|
+
count: [1, 3],
|
|
510
|
+
sticky: false,
|
|
511
|
+
multiplier: 1,
|
|
512
|
+
trigger: 'onFreeSpins',
|
|
513
|
+
chance: 0.2,
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/** Intensity → duration scale (accessibility). */
|
|
519
|
+
export const INTENSITY_SCALE: Record<Intensity, number> = { full: 1, reduced: 0.7, minimal: 0.4 };
|
|
520
|
+
|
|
521
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
522
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Deep-merge `partial` onto `base` (arrays replace, objects merge). Returns a new object. */
|
|
526
|
+
export function mergeReelConfig<T>(base: T, partial?: DeepPartial<T>): T {
|
|
527
|
+
if (partial == null) return structuredCloneSafe(base);
|
|
528
|
+
const out = structuredCloneSafe(base) as Record<string, unknown>;
|
|
529
|
+
for (const [k, v] of Object.entries(partial as Record<string, unknown>)) {
|
|
530
|
+
if (v === undefined) continue;
|
|
531
|
+
const cur = out[k];
|
|
532
|
+
if (isPlainObject(v) && isPlainObject(cur))
|
|
533
|
+
out[k] = mergeReelConfig(cur, v as DeepPartial<unknown>);
|
|
534
|
+
else out[k] = v;
|
|
535
|
+
}
|
|
536
|
+
return out as T;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Resolve a partial config to a fully-populated `ReelSystemConfig`. */
|
|
540
|
+
export function resolveReelConfig(partial?: DeepPartial<ReelSystemConfig>): ReelSystemConfig {
|
|
541
|
+
return mergeReelConfig(DEFAULT_REEL_CONFIG, partial);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function structuredCloneSafe<T>(v: T): T {
|
|
545
|
+
// structuredClone is available in modern browsers + Node 17+; fall back to JSON for safety.
|
|
546
|
+
if (typeof structuredClone === 'function') return structuredClone(v);
|
|
547
|
+
return JSON.parse(JSON.stringify(v)) as T;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Effective per-reel row counts (resolves Megaways `rowsPerReel`, else uniform `rows`). */
|
|
551
|
+
export function effectiveRowsPerReel(grid: GridConfig): number[] {
|
|
552
|
+
if (grid.rowsPerReel && grid.rowsPerReel.length === grid.cols) return grid.rowsPerReel.slice();
|
|
553
|
+
return Array.from({ length: grid.cols }, () => grid.rows);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Total ways-to-win for a ways/megaways grid (product of per-reel heights). */
|
|
557
|
+
export function waysCount(grid: GridConfig): number {
|
|
558
|
+
return effectiveRowsPerReel(grid).reduce((a, b) => a * b, 1);
|
|
559
|
+
}
|