@energy8platform/game-engine 0.37.0 → 0.39.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 +46 -2
- package/dist/devtools.cjs.js +32 -4
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.d.ts +44 -0
- package/dist/devtools.esm.js +32 -4
- package/dist/devtools.esm.js.map +1 -1
- package/dist/host.cjs.js +279 -109
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +5 -0
- package/dist/host.esm.js +279 -109
- package/dist/host.esm.js.map +1 -1
- package/dist/reel-panel-client.cjs.js +32 -4
- package/dist/reel-panel-client.cjs.js.map +1 -1
- package/dist/reel-panel-client.esm.js +32 -4
- package/dist/reel-panel-client.esm.js.map +1 -1
- package/dist/slot.cjs.js +184 -42
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +86 -8
- package/dist/slot.esm.js +184 -43
- package/dist/slot.esm.js.map +1 -1
- package/package.json +3 -3
- package/src/host/autoplay.ts +25 -3
- package/src/host/connectionRecovery.ts +113 -0
- package/src/host/createSlotGame.ts +83 -103
- package/src/host/playError.ts +23 -0
- package/src/host/resumeDrain.ts +156 -0
- package/src/host/shellConfig.ts +12 -0
- package/src/slot/config/ReelSystemConfig.ts +71 -4
- package/src/slot/devtools/fieldSchema.ts +5 -0
- package/src/slot/index.ts +3 -0
- package/src/slot/motion/AnticipationController.ts +62 -17
- package/src/slot/motion/SpinEngine.ts +95 -18
- package/src/slot/system/ReelSystem.ts +59 -9
package/dist/slot.d.ts
CHANGED
|
@@ -366,6 +366,14 @@ declare function easingByName(name?: string): EasingFunction;
|
|
|
366
366
|
|
|
367
367
|
/** Names of easing functions available in the engine's `Easing` map (see anim/easing-map.ts). */
|
|
368
368
|
type EasingName = 'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' | 'easeInBack' | 'easeOutBack' | 'easeInOutBack' | 'easeOutBounce' | 'easeInBounce' | 'easeOutElastic' | 'easeInSine' | 'easeOutSine' | 'easeInOutSine';
|
|
369
|
+
/**
|
|
370
|
+
* A value that is either flat across the board, or per-reel: an array indexed by REEL INDEX
|
|
371
|
+
* (holes fall back to the scalar default). Used for anticipation timings so a game can make
|
|
372
|
+
* each successive reel slower than the last.
|
|
373
|
+
*/
|
|
374
|
+
type PerReel<T> = T | (T | undefined)[];
|
|
375
|
+
/** Resolve a `PerReel<T>` for one reel. `undefined` (or a hole in the array) yields `fallback`. */
|
|
376
|
+
declare function perReelValue<T>(value: PerReel<T> | undefined, reel: number, fallback: T): T;
|
|
369
377
|
/** Win-evaluation model. Purely presentational here (affects geometry + highlight), the math lives in Lua. */
|
|
370
378
|
type EvaluationMode = 'lines' | 'ways' | 'anywhere' | 'cluster' | 'megaways' | 'infinity';
|
|
371
379
|
interface GridConfig {
|
|
@@ -448,6 +456,21 @@ interface MotionConfig {
|
|
|
448
456
|
slamStop: boolean;
|
|
449
457
|
/** Symbols visible on a reel tape while spinning (swap/strip). */
|
|
450
458
|
symbolsPerReel: number;
|
|
459
|
+
/** `cascade-drop`: ms between consecutive cells of ONE reel (top→bottom). Default 24. */
|
|
460
|
+
cellStagger: number;
|
|
461
|
+
/** `cascade-drop`: multiplier on `stopStagger` for the per-reel offset. Default 0.4. */
|
|
462
|
+
reelStaggerFactor: number;
|
|
463
|
+
/** `cascade-drop`: fall duration as a fraction of `spinUp`. Default 0.6. */
|
|
464
|
+
dropFallFactor: number;
|
|
465
|
+
}
|
|
466
|
+
/** What a game-supplied `AnticipationConfig.decide` may return instead of a bare reel list. */
|
|
467
|
+
interface AnticipationOverride {
|
|
468
|
+
/** Reels to anticipate, in the order the progression should ramp. Empty = no anticipation. */
|
|
469
|
+
reels: number[];
|
|
470
|
+
/** Speed factor (lower = slower). Scalar, or per-reel indexed by reel index. */
|
|
471
|
+
slowdown?: PerReel<number>;
|
|
472
|
+
/** Extra hold before landing. Scalar, or per-reel indexed by reel index. */
|
|
473
|
+
holdMs?: PerReel<number>;
|
|
451
474
|
}
|
|
452
475
|
interface AnticipationConfig {
|
|
453
476
|
enabled: boolean;
|
|
@@ -461,6 +484,21 @@ interface AnticipationConfig {
|
|
|
461
484
|
slowdownFactor: number;
|
|
462
485
|
/** Extra hold (ms) before the final anticipation reel lands (300–500 typical). */
|
|
463
486
|
holdMs: number;
|
|
487
|
+
/**
|
|
488
|
+
* Game-supplied decision, REPLACING the built-in `triggerSymbols`/`threshold` counting.
|
|
489
|
+
* Return the reels to anticipate (or an `AnticipationOverride`); `null` / `[]` = no anticipation.
|
|
490
|
+
* Use this when the trigger is not expressible as "N of symbol X landed" — e.g. "the round is
|
|
491
|
+
* still alive on every reel so far", or "reel 3 missed its symbol, so let 4 and 5 stop normally".
|
|
492
|
+
*/
|
|
493
|
+
decide?: ((targetGrid: CellData[][]) => number[] | AnticipationOverride | null) | null;
|
|
494
|
+
/**
|
|
495
|
+
* Ramp the slowdown across successive anticipated reels: reel #i of the decision gets
|
|
496
|
+
* `slowdownFactor * progressiveSlowdown ** i`. 1 = flat (default); < 1 = each reel slower
|
|
497
|
+
* than the last.
|
|
498
|
+
*/
|
|
499
|
+
progressiveSlowdown: number;
|
|
500
|
+
/** Extra hold (ms) added per successive anticipated reel: reel #i gets `holdMs + i * this`. */
|
|
501
|
+
progressiveHoldMs: number;
|
|
464
502
|
/** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */
|
|
465
503
|
zoom: {
|
|
466
504
|
enabled: boolean;
|
|
@@ -701,8 +739,25 @@ interface SpinRunOpts {
|
|
|
701
739
|
turbo?: boolean;
|
|
702
740
|
/** Reels to slow for anticipation (computed by the ReelSystem from config + targetGrid). */
|
|
703
741
|
anticipateReels?: number[];
|
|
704
|
-
|
|
705
|
-
|
|
742
|
+
/** Speed factor for anticipated reels (lower = slower). Scalar, or per-reel indexed by reel. */
|
|
743
|
+
anticipateSlowdown?: PerReel<number>;
|
|
744
|
+
/** Extra hold (ms) for anticipated reels. Scalar, or per-reel indexed by reel. */
|
|
745
|
+
anticipateHoldMs?: PerReel<number>;
|
|
746
|
+
/**
|
|
747
|
+
* Reels whose tape runs normally but whose landing is NOT handed back. The engine stops and
|
|
748
|
+
* disposes of the tape as usual and leaves the real cells hidden and unseated; the caller owns
|
|
749
|
+
* their data and visibility from that point (and `skip()` will not reveal them either).
|
|
750
|
+
*/
|
|
751
|
+
deferReveal?: number[];
|
|
752
|
+
/** The resolved schedule, handed over before the first frame runs. Schedule against THESE
|
|
753
|
+
* numbers rather than re-deriving `plan()`'s formula. */
|
|
754
|
+
onPlan?: (plan: ReelStopPlan[]) => void;
|
|
755
|
+
/** Fires on the frame a reel lands — after its cells are seated, before settle/squash/shake.
|
|
756
|
+
* For a deferred reel it still fires (the reel DID stop); nothing was seated. */
|
|
757
|
+
onReelStop?: (reel: number, plan: ReelStopPlan) => void;
|
|
758
|
+
/** Fires as each cell takes its landing symbol. In `cascade-drop` this is the per-cell impact
|
|
759
|
+
* frame (after the fall, before the squash) — the hook for a per-cell sound or shake. */
|
|
760
|
+
onCellSeated?: (reel: number, row: number, data: CellData) => void;
|
|
706
761
|
}
|
|
707
762
|
interface ReelStopPlan {
|
|
708
763
|
reel: number;
|
|
@@ -714,6 +769,10 @@ interface ReelStopPlan {
|
|
|
714
769
|
ms: number;
|
|
715
770
|
};
|
|
716
771
|
anticipated: boolean;
|
|
772
|
+
/** Time-stretch applied to this reel's tape (>= 1, longer = slower). 1 when not anticipated. */
|
|
773
|
+
slowdown: number;
|
|
774
|
+
/** True when `deferReveal` withheld this reel's landing (see `SpinRunOpts.deferReveal`). */
|
|
775
|
+
deferred: boolean;
|
|
717
776
|
}
|
|
718
777
|
declare class SpinEngine {
|
|
719
778
|
private _grid;
|
|
@@ -723,6 +782,7 @@ declare class SpinEngine {
|
|
|
723
782
|
private _killed;
|
|
724
783
|
private _shaking;
|
|
725
784
|
private _temp;
|
|
785
|
+
private _deferred;
|
|
726
786
|
constructor(grid: ReelGrid, resolve: SymbolResolver, cfg: MotionConfig, win?: WinConfig);
|
|
727
787
|
setConfig(cfg: MotionConfig): void;
|
|
728
788
|
setWin(win: WinConfig): void;
|
|
@@ -734,7 +794,7 @@ declare class SpinEngine {
|
|
|
734
794
|
/** Execute the spin for every reel concurrently. */
|
|
735
795
|
run(data: SpinData, opts?: SpinRunOpts): Promise<void>;
|
|
736
796
|
private _runReel;
|
|
737
|
-
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). */
|
|
797
|
+
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). Resolved in `plan()`. */
|
|
738
798
|
private slowOf;
|
|
739
799
|
private _runSwap;
|
|
740
800
|
private _runStrip;
|
|
@@ -750,10 +810,13 @@ declare class SpinEngine {
|
|
|
750
810
|
|
|
751
811
|
interface AnticipationDecision {
|
|
752
812
|
active: boolean;
|
|
753
|
-
/** Reel indices that should spin slower / longer. */
|
|
813
|
+
/** Reel indices that should spin slower / longer, in the order the ramp applies. */
|
|
754
814
|
reels: number[];
|
|
755
|
-
|
|
756
|
-
|
|
815
|
+
/** Speed factor. A scalar when flat; a per-reel array when the decision ramps (see
|
|
816
|
+
* `progressiveSlowdown` / a game-supplied `decide`). Resolve with `perReelValue`. */
|
|
817
|
+
slowdown: PerReel<number>;
|
|
818
|
+
/** Extra hold before landing. Scalar or per-reel array, same as `slowdown`. */
|
|
819
|
+
holdMs: PerReel<number>;
|
|
757
820
|
}
|
|
758
821
|
declare class AnticipationController {
|
|
759
822
|
private _cfg;
|
|
@@ -767,6 +830,13 @@ declare class AnticipationController {
|
|
|
767
830
|
* is flagged for the slow treatment (this mirrors "searching for the last scatter").
|
|
768
831
|
*/
|
|
769
832
|
decide(targetGrid: CellData[][]): AnticipationDecision;
|
|
833
|
+
/** Assemble a decision, applying the configured progression unless the caller pinned values. */
|
|
834
|
+
private build;
|
|
835
|
+
/**
|
|
836
|
+
* A flat scalar when the progression is a no-op, else an array INDEXED BY REEL so the engine can
|
|
837
|
+
* read a per-reel value straight out of `plan()`.
|
|
838
|
+
*/
|
|
839
|
+
private ramp;
|
|
770
840
|
/** Optionally zoom the grid in while anticipating, then settle back. Returns a reset fn. */
|
|
771
841
|
zoomIn(grid: ReelGrid): Promise<() => Promise<void>>;
|
|
772
842
|
}
|
|
@@ -947,6 +1017,14 @@ interface ReelSystem {
|
|
|
947
1017
|
/** Replace the whole config. */
|
|
948
1018
|
setConfig(config: ReelSystemConfig): void;
|
|
949
1019
|
spin(target: CellData[][], opts?: SpinRunOpts): Promise<void>;
|
|
1020
|
+
/**
|
|
1021
|
+
* The schedule `spin(target, opts)` WOULD run, without running it — same anticipation decision,
|
|
1022
|
+
* same numbers. Schedule landing sounds / camera moves against this instead of re-deriving the
|
|
1023
|
+
* engine's formula. (`spin`'s `onPlan` hands you the same array once the spin is under way.)
|
|
1024
|
+
*/
|
|
1025
|
+
planSpin(target: CellData[][], opts?: SpinRunOpts): ReelStopPlan[];
|
|
1026
|
+
/** The anticipation decision `spin(target, opts)` would use (run options override the config). */
|
|
1027
|
+
anticipationFor(target: CellData[][], opts?: SpinRunOpts): AnticipationDecision;
|
|
950
1028
|
/** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier
|
|
951
1029
|
* carries over instead of resetting. Generic in the step type, so `onStep` hands back the game's
|
|
952
1030
|
* own step (with its per-step win) rather than the bare TumbleStep. */
|
|
@@ -1056,5 +1134,5 @@ declare class MultiplierAccumulator {
|
|
|
1056
1134
|
reset(boundary: CarryPolicy): void;
|
|
1057
1135
|
}
|
|
1058
1136
|
|
|
1059
|
-
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 };
|
|
1060
|
-
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, StepRunOpts, StickyConfig, StopMode, StopOrder, SymbolCellConfig, SymbolResolver, SymbolTextures, SymbolView, TransformConfig, TumbleStep, WalkingWildConfig, WinConfig, WinTier };
|
|
1137
|
+
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, perReelValue, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
|
|
1138
|
+
export type { AnimatedSymbolConfig, AnticipationConfig, AnticipationDecision, AnticipationOverride, 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, PerReel, 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, StepRunOpts, StickyConfig, StopMode, StopOrder, SymbolCellConfig, SymbolResolver, SymbolTextures, SymbolView, TransformConfig, TumbleStep, WalkingWildConfig, WinConfig, WinTier };
|
package/dist/slot.esm.js
CHANGED
|
@@ -1073,6 +1073,14 @@ class ReelSpinController {
|
|
|
1073
1073
|
// override only what they need.
|
|
1074
1074
|
//
|
|
1075
1075
|
// Design notes are in docs/reels-analysis-and-design.md.
|
|
1076
|
+
/** Resolve a `PerReel<T>` for one reel. `undefined` (or a hole in the array) yields `fallback`. */
|
|
1077
|
+
function perReelValue(value, reel, fallback) {
|
|
1078
|
+
if (value === undefined)
|
|
1079
|
+
return fallback;
|
|
1080
|
+
if (Array.isArray(value))
|
|
1081
|
+
return value[reel] ?? fallback;
|
|
1082
|
+
return value;
|
|
1083
|
+
}
|
|
1076
1084
|
const FEATURE_KEYS = [
|
|
1077
1085
|
'reelModifier', // pre-spin
|
|
1078
1086
|
'giant',
|
|
@@ -1118,6 +1126,9 @@ const DEFAULT_REEL_CONFIG = {
|
|
|
1118
1126
|
intensity: 'full',
|
|
1119
1127
|
slamStop: true,
|
|
1120
1128
|
symbolsPerReel: 6,
|
|
1129
|
+
cellStagger: 24,
|
|
1130
|
+
reelStaggerFactor: 0.4,
|
|
1131
|
+
dropFallFactor: 0.6,
|
|
1121
1132
|
},
|
|
1122
1133
|
anticipation: {
|
|
1123
1134
|
enabled: false,
|
|
@@ -1126,6 +1137,9 @@ const DEFAULT_REEL_CONFIG = {
|
|
|
1126
1137
|
reels: 'trailing',
|
|
1127
1138
|
slowdownFactor: 0.3,
|
|
1128
1139
|
holdMs: 400,
|
|
1140
|
+
decide: null,
|
|
1141
|
+
progressiveSlowdown: 1,
|
|
1142
|
+
progressiveHoldMs: 0,
|
|
1129
1143
|
zoom: { enabled: false, scale: 1.15, ms: 600 },
|
|
1130
1144
|
},
|
|
1131
1145
|
cascade: {
|
|
@@ -1252,11 +1266,28 @@ function mergeReelConfig(base, partial) {
|
|
|
1252
1266
|
function resolveReelConfig(partial) {
|
|
1253
1267
|
return mergeReelConfig(DEFAULT_REEL_CONFIG, partial);
|
|
1254
1268
|
}
|
|
1269
|
+
/** True only for `{}`-shaped objects — a class instance or a Date is NOT one. */
|
|
1270
|
+
function isCloneableRecord(v) {
|
|
1271
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
1272
|
+
return false;
|
|
1273
|
+
const proto = Object.getPrototypeOf(v);
|
|
1274
|
+
return proto === Object.prototype || proto === null;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Deep-clone a config. Hand-rolled rather than `structuredClone` because a config may carry
|
|
1278
|
+
* functions (`anticipation.decide`), which `structuredClone` refuses to copy. Functions and
|
|
1279
|
+
* anything that is not a plain object/array pass through by reference.
|
|
1280
|
+
*/
|
|
1255
1281
|
function structuredCloneSafe(v) {
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1282
|
+
if (Array.isArray(v))
|
|
1283
|
+
return v.map((item) => structuredCloneSafe(item));
|
|
1284
|
+
if (isCloneableRecord(v)) {
|
|
1285
|
+
const out = {};
|
|
1286
|
+
for (const [k, val] of Object.entries(v))
|
|
1287
|
+
out[k] = structuredCloneSafe(val);
|
|
1288
|
+
return out;
|
|
1289
|
+
}
|
|
1290
|
+
return v;
|
|
1260
1291
|
}
|
|
1261
1292
|
/** Effective per-reel row counts (resolves Megaways `rowsPerReel`, else uniform `rows`). */
|
|
1262
1293
|
function effectiveRowsPerReel(grid) {
|
|
@@ -1493,6 +1524,7 @@ class SpinEngine {
|
|
|
1493
1524
|
_killed = false;
|
|
1494
1525
|
_shaking = false;
|
|
1495
1526
|
_temp = [];
|
|
1527
|
+
_deferred = new Set();
|
|
1496
1528
|
constructor(grid, resolve, cfg, win) {
|
|
1497
1529
|
this._grid = grid;
|
|
1498
1530
|
this._resolve = resolve;
|
|
@@ -1536,6 +1568,7 @@ class SpinEngine {
|
|
|
1536
1568
|
const cols = this._grid.cols;
|
|
1537
1569
|
const order = (reel) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
|
|
1538
1570
|
const anticipate = new Set(opts?.anticipateReels ?? []);
|
|
1571
|
+
const defer = new Set(opts?.deferReveal ?? []);
|
|
1539
1572
|
const out = [];
|
|
1540
1573
|
for (let reel = 0; reel < cols; reel++) {
|
|
1541
1574
|
const idx = order(reel);
|
|
@@ -1552,13 +1585,16 @@ class SpinEngine {
|
|
|
1552
1585
|
stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
|
|
1553
1586
|
const isAnticipated = anticipate.has(reel);
|
|
1554
1587
|
if (isAnticipated)
|
|
1555
|
-
stopTime += (opts?.anticipateHoldMs
|
|
1588
|
+
stopTime += perReelValue(opts?.anticipateHoldMs, reel, 0) * f;
|
|
1589
|
+
const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
|
|
1556
1590
|
out.push({
|
|
1557
1591
|
reel,
|
|
1558
1592
|
stopTime,
|
|
1559
1593
|
landing: data.targetGrid[reel] ?? [],
|
|
1560
1594
|
settle: { amp: this._cfg.settle.amp, ms: this._cfg.settle.ms * f },
|
|
1561
1595
|
anticipated: isAnticipated,
|
|
1596
|
+
slowdown: Math.max(1, 1 / (speed || 1)),
|
|
1597
|
+
deferred: defer.has(reel),
|
|
1562
1598
|
});
|
|
1563
1599
|
}
|
|
1564
1600
|
return out;
|
|
@@ -1568,6 +1604,9 @@ class SpinEngine {
|
|
|
1568
1604
|
this._killed = false;
|
|
1569
1605
|
this._temp = [];
|
|
1570
1606
|
const plan = this.plan(data, opts);
|
|
1607
|
+
// remembered for skip(): a deferred reel's cells belong to the caller, not to us
|
|
1608
|
+
this._deferred = new Set(plan.filter((p) => p.deferred).map((p) => p.reel));
|
|
1609
|
+
opts?.onPlan?.(plan);
|
|
1571
1610
|
const f = this.scale(opts);
|
|
1572
1611
|
await Promise.all(plan.map((p) => this._runReel(p, data, opts, f)));
|
|
1573
1612
|
this._cleanupTemp();
|
|
@@ -1585,9 +1624,9 @@ class SpinEngine {
|
|
|
1585
1624
|
return this._runSwap(p, data, opts, f);
|
|
1586
1625
|
}
|
|
1587
1626
|
}
|
|
1588
|
-
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). */
|
|
1589
|
-
slowOf(p
|
|
1590
|
-
return p.
|
|
1627
|
+
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). Resolved in `plan()`. */
|
|
1628
|
+
slowOf(p) {
|
|
1629
|
+
return p.slowdown;
|
|
1591
1630
|
}
|
|
1592
1631
|
// ── swap: cycle symbols quickly in the real cells, then land ──────────────
|
|
1593
1632
|
async _runSwap(p, data, opts, f) {
|
|
@@ -1598,7 +1637,7 @@ class SpinEngine {
|
|
|
1598
1637
|
const blur = this._applyBlur(cells, true);
|
|
1599
1638
|
const tickMs = 1000 / 30;
|
|
1600
1639
|
// anticipation makes the reel spin longer before it lands
|
|
1601
|
-
const ticks = Math.max(6, Math.floor((p.stopTime * this.slowOf(p
|
|
1640
|
+
const ticks = Math.max(6, Math.floor((p.stopTime * this.slowOf(p)) / tickMs));
|
|
1602
1641
|
for (let i = 0; i < ticks; i++) {
|
|
1603
1642
|
if (this._killed)
|
|
1604
1643
|
break;
|
|
@@ -1607,8 +1646,24 @@ class SpinEngine {
|
|
|
1607
1646
|
await Tween.delay(tickMs);
|
|
1608
1647
|
}
|
|
1609
1648
|
blur?.();
|
|
1610
|
-
|
|
1611
|
-
|
|
1649
|
+
if (p.deferred) {
|
|
1650
|
+
// the caller owns this reel's result — go dark and unseated instead of handing it back.
|
|
1651
|
+
// NB 'swap' cycles the tape THROUGH the real cells, so a deferred reel only truly withholds
|
|
1652
|
+
// its result when `SpinData.strip` supplies filler for it; otherwise the tape is built from
|
|
1653
|
+
// the landing symbols. 'strip' and 'cascade-drop' have no such caveat.
|
|
1654
|
+
cells.forEach((c) => {
|
|
1655
|
+
c.setData({ symbol: null });
|
|
1656
|
+
c.visible = false;
|
|
1657
|
+
});
|
|
1658
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
for (let r = 0; r < cells.length; r++) {
|
|
1662
|
+
const cell = p.landing[r] ?? { symbol: null };
|
|
1663
|
+
cells[r].setData(cell);
|
|
1664
|
+
opts?.onCellSeated?.(p.reel, r, cell);
|
|
1665
|
+
}
|
|
1666
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1612
1667
|
await this._settle(p.reel, p.settle, f);
|
|
1613
1668
|
await this._frameShake(p.landing);
|
|
1614
1669
|
}
|
|
@@ -1643,7 +1698,7 @@ class SpinEngine {
|
|
|
1643
1698
|
this._grid.addChild(tape);
|
|
1644
1699
|
this._temp.push(tape);
|
|
1645
1700
|
const clearBlur = this._applyBlur([tape], true);
|
|
1646
|
-
const slow = this.slowOf(p
|
|
1701
|
+
const slow = this.slowOf(p);
|
|
1647
1702
|
// overshoot/settle honour the configured settle (amp in px, easing)
|
|
1648
1703
|
const overshoot = p.settle.amp || step * 0.18;
|
|
1649
1704
|
await Tween.to(tape, { y: restY + overshoot }, p.stopTime * slow, easingByName('easeInOutQuad'));
|
|
@@ -1653,12 +1708,21 @@ class SpinEngine {
|
|
|
1653
1708
|
}
|
|
1654
1709
|
clearBlur?.();
|
|
1655
1710
|
await Tween.to(tape, { y: restY }, Math.max(120, p.settle.ms), easingByName(this._cfg.settle.easing));
|
|
1656
|
-
// hand the result back to the real cells
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1711
|
+
// hand the result back to the real cells — unless the caller deferred the reveal, in which
|
|
1712
|
+
// case the tape simply goes away and the reel is left dark, unseated and owned by the caller
|
|
1713
|
+
if (!p.deferred) {
|
|
1714
|
+
for (let r = 0; r < rows; r++) {
|
|
1715
|
+
const cell = p.landing[r] ?? { symbol: null };
|
|
1716
|
+
realCells[r].setData(cell);
|
|
1717
|
+
opts?.onCellSeated?.(p.reel, r, cell);
|
|
1718
|
+
}
|
|
1719
|
+
realCells.forEach((c) => (c.visible = true));
|
|
1720
|
+
}
|
|
1660
1721
|
tape.destroy();
|
|
1661
1722
|
this._temp = this._temp.filter((t) => t !== tape);
|
|
1723
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1724
|
+
if (p.deferred)
|
|
1725
|
+
return;
|
|
1662
1726
|
// squash the real cells on impact when enabled
|
|
1663
1727
|
if (this._cfg.squash.enabled)
|
|
1664
1728
|
await Promise.all(realCells.map((c) => this._squashCell(c, f)));
|
|
@@ -1667,22 +1731,36 @@ class SpinEngine {
|
|
|
1667
1731
|
// ── cascade-drop: symbols drop in from above with stagger + bounce + squash ─
|
|
1668
1732
|
async _runDrop(p, opts, f) {
|
|
1669
1733
|
const rows = this._grid.rowsOf(p.reel);
|
|
1734
|
+
if (p.deferred) {
|
|
1735
|
+
// nothing to drop — the caller brings this reel in itself
|
|
1736
|
+
for (let r = 0; r < rows; r++)
|
|
1737
|
+
this._grid.getCell(p.reel, r).visible = false;
|
|
1738
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1670
1741
|
const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
|
|
1671
|
-
const slow = this.slowOf(p
|
|
1742
|
+
const slow = this.slowOf(p); // anticipation drops the reel in more slowly
|
|
1672
1743
|
await Promise.all(Array.from({ length: rows }, (_, r) => r).map(async (r) => {
|
|
1673
1744
|
if (this._killed)
|
|
1674
1745
|
return;
|
|
1675
1746
|
const cell = this._grid.getCell(p.reel, r);
|
|
1676
1747
|
const to = this._grid.cellPosition(p.reel, r);
|
|
1677
|
-
|
|
1748
|
+
const data = p.landing[r] ?? { symbol: null };
|
|
1749
|
+
cell.setData(data);
|
|
1678
1750
|
cell.position.set(to.x, to.y - step * (rows + 1));
|
|
1679
1751
|
cell.alpha = 1;
|
|
1680
|
-
const delay = (p.reel * this._cfg.stopStagger *
|
|
1752
|
+
const delay = (p.reel * this._cfg.stopStagger * this._cfg.reelStaggerFactor +
|
|
1753
|
+
r * this._cfg.cellStagger) *
|
|
1754
|
+
f *
|
|
1755
|
+
slow;
|
|
1681
1756
|
if (delay)
|
|
1682
1757
|
await Tween.delay(delay);
|
|
1683
|
-
await Tween.to(cell, { 'position.y': to.y }, this._cfg.spinUp *
|
|
1758
|
+
await Tween.to(cell, { 'position.y': to.y }, this._cfg.spinUp * this._cfg.dropFallFactor * f * slow, easingByName(this._cfg.settle.easing));
|
|
1759
|
+
// the impact frame — fired before the squash so a game can sync its own hit feedback
|
|
1760
|
+
opts?.onCellSeated?.(p.reel, r, data);
|
|
1684
1761
|
await this._squashCell(cell, f);
|
|
1685
1762
|
}));
|
|
1763
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1686
1764
|
await this._frameShake(p.landing);
|
|
1687
1765
|
}
|
|
1688
1766
|
// ── shared helpers ────────────────────────────────────────────────────────
|
|
@@ -1746,13 +1824,17 @@ class SpinEngine {
|
|
|
1746
1824
|
Tween.killTweensOf(this._grid);
|
|
1747
1825
|
this._grid.x = 0; // undo any in-flight frame shake
|
|
1748
1826
|
for (let c = 0; c < this._grid.cols; c++) {
|
|
1827
|
+
// a deferred reel's visibility belongs to the caller — a slam stop must not reveal it
|
|
1828
|
+
const deferred = this._deferred.has(c);
|
|
1749
1829
|
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
1750
1830
|
const cell = this._grid.getCell(c, r);
|
|
1751
1831
|
if (cell.destroyed)
|
|
1752
1832
|
continue;
|
|
1753
1833
|
Tween.killTweensOf(cell);
|
|
1754
|
-
|
|
1755
|
-
|
|
1834
|
+
if (!deferred) {
|
|
1835
|
+
cell.visible = true;
|
|
1836
|
+
cell.alpha = 1;
|
|
1837
|
+
}
|
|
1756
1838
|
cell.filters = [];
|
|
1757
1839
|
cell.scale.set(1);
|
|
1758
1840
|
}
|
|
@@ -1765,6 +1847,8 @@ class SpinEngine {
|
|
|
1765
1847
|
//
|
|
1766
1848
|
// Decides which trailing reels get the "anticipation" slow-down treatment, based purely on
|
|
1767
1849
|
// the already-resolved landing grid (presentation only — never a secondary outcome decision).
|
|
1850
|
+
/** Fresh "nothing to anticipate" decision (fresh, so callers may mutate `reels` freely). */
|
|
1851
|
+
const none = () => ({ active: false, reels: [], slowdown: 1, holdMs: 0 });
|
|
1768
1852
|
class AnticipationController {
|
|
1769
1853
|
_cfg;
|
|
1770
1854
|
constructor(cfg) {
|
|
@@ -1788,19 +1872,23 @@ class AnticipationController {
|
|
|
1788
1872
|
*/
|
|
1789
1873
|
decide(targetGrid) {
|
|
1790
1874
|
if (!this._cfg.enabled)
|
|
1791
|
-
return
|
|
1875
|
+
return none();
|
|
1876
|
+
// A game-supplied predicate replaces the symbol counting entirely.
|
|
1877
|
+
if (this._cfg.decide) {
|
|
1878
|
+
const custom = this._cfg.decide(targetGrid);
|
|
1879
|
+
if (!custom)
|
|
1880
|
+
return none();
|
|
1881
|
+
const o = Array.isArray(custom) ? { reels: custom } : custom;
|
|
1882
|
+
if (!o.reels?.length)
|
|
1883
|
+
return none();
|
|
1884
|
+
return this.build(o.reels.slice(), o.slowdown, o.holdMs);
|
|
1885
|
+
}
|
|
1792
1886
|
if (Array.isArray(this._cfg.reels)) {
|
|
1793
1887
|
// explicit reel list — arm only if the threshold is met somewhere on the board
|
|
1794
1888
|
const total = targetGrid.reduce((sum, reel) => sum + this.countOnReel(reel), 0);
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
active,
|
|
1799
|
-
reels: this._cfg.reels.slice(),
|
|
1800
|
-
slowdown: this._cfg.slowdownFactor,
|
|
1801
|
-
holdMs: this._cfg.holdMs,
|
|
1802
|
-
}
|
|
1803
|
-
: { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
1889
|
+
if (total < this._cfg.threshold)
|
|
1890
|
+
return none();
|
|
1891
|
+
return this.build(this._cfg.reels.slice());
|
|
1804
1892
|
}
|
|
1805
1893
|
// 'trailing': find the reel where the cumulative count hits the threshold
|
|
1806
1894
|
let running = 0;
|
|
@@ -1813,13 +1901,37 @@ class AnticipationController {
|
|
|
1813
1901
|
}
|
|
1814
1902
|
}
|
|
1815
1903
|
if (armReel < 0)
|
|
1816
|
-
return
|
|
1904
|
+
return none();
|
|
1817
1905
|
const reels = [];
|
|
1818
1906
|
for (let c = armReel + 1; c < targetGrid.length; c++)
|
|
1819
1907
|
reels.push(c);
|
|
1820
1908
|
if (reels.length === 0)
|
|
1821
|
-
return
|
|
1822
|
-
return
|
|
1909
|
+
return none();
|
|
1910
|
+
return this.build(reels);
|
|
1911
|
+
}
|
|
1912
|
+
/** Assemble a decision, applying the configured progression unless the caller pinned values. */
|
|
1913
|
+
build(reels, slowdown, holdMs) {
|
|
1914
|
+
return {
|
|
1915
|
+
active: true,
|
|
1916
|
+
reels,
|
|
1917
|
+
slowdown: slowdown ??
|
|
1918
|
+
this.ramp(reels, this._cfg.slowdownFactor, (base, i) => i === 0 ? base : base * Math.pow(this._cfg.progressiveSlowdown, i)),
|
|
1919
|
+
holdMs: holdMs ??
|
|
1920
|
+
this.ramp(reels, this._cfg.holdMs, (base, i) => base + this._cfg.progressiveHoldMs * i),
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* A flat scalar when the progression is a no-op, else an array INDEXED BY REEL so the engine can
|
|
1925
|
+
* read a per-reel value straight out of `plan()`.
|
|
1926
|
+
*/
|
|
1927
|
+
ramp(reels, base, at) {
|
|
1928
|
+
if (at(base, 1) === base)
|
|
1929
|
+
return base;
|
|
1930
|
+
const out = [];
|
|
1931
|
+
reels.forEach((reel, i) => {
|
|
1932
|
+
out[reel] = at(base, i);
|
|
1933
|
+
});
|
|
1934
|
+
return out;
|
|
1823
1935
|
}
|
|
1824
1936
|
/** Optionally zoom the grid in while anticipating, then settle back. Returns a reset fn. */
|
|
1825
1937
|
async zoomIn(grid) {
|
|
@@ -2871,6 +2983,33 @@ function createReelSystem(opts) {
|
|
|
2871
2983
|
function ctx(freeSpins) {
|
|
2872
2984
|
return { grid, resolve, cfg: config, fx, board, freeSpins, log };
|
|
2873
2985
|
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Which reels get the anticipation treatment for this spin. An explicit `anticipateReels` on the
|
|
2988
|
+
* run options WINS over the configured decision — passing it is how a game drives anticipation
|
|
2989
|
+
* from its own logic. Omit it and the configured `AnticipationController` decides.
|
|
2990
|
+
*/
|
|
2991
|
+
function resolveAnticipation(target, runOpts) {
|
|
2992
|
+
const explicit = runOpts?.anticipateReels;
|
|
2993
|
+
if (!explicit)
|
|
2994
|
+
return anticipation.decide(target);
|
|
2995
|
+
if (!explicit.length)
|
|
2996
|
+
return { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
2997
|
+
return {
|
|
2998
|
+
active: true,
|
|
2999
|
+
reels: explicit.slice(),
|
|
3000
|
+
slowdown: runOpts?.anticipateSlowdown ?? config.anticipation.slowdownFactor,
|
|
3001
|
+
holdMs: runOpts?.anticipateHoldMs ?? config.anticipation.holdMs,
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
/** Fold a resolved decision back onto the caller's run options (everything else passes through). */
|
|
3005
|
+
function mergeAnticipation(runOpts, decision) {
|
|
3006
|
+
return {
|
|
3007
|
+
...runOpts,
|
|
3008
|
+
anticipateReels: decision.active ? decision.reels : undefined,
|
|
3009
|
+
anticipateSlowdown: decision.slowdown,
|
|
3010
|
+
anticipateHoldMs: decision.holdMs,
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
2874
3013
|
buildGrid();
|
|
2875
3014
|
const api = {
|
|
2876
3015
|
view,
|
|
@@ -2913,20 +3052,22 @@ function createReelSystem(opts) {
|
|
|
2913
3052
|
update(partial) {
|
|
2914
3053
|
api.setConfig(mergeReelConfig(config, partial));
|
|
2915
3054
|
},
|
|
3055
|
+
anticipationFor(target, runOpts) {
|
|
3056
|
+
return resolveAnticipation(target, runOpts);
|
|
3057
|
+
},
|
|
3058
|
+
planSpin(target, runOpts) {
|
|
3059
|
+
const decision = resolveAnticipation(target, runOpts);
|
|
3060
|
+
return spin.plan({ targetGrid: target }, mergeAnticipation(runOpts, decision));
|
|
3061
|
+
},
|
|
2916
3062
|
async spin(target, runOpts) {
|
|
2917
3063
|
const data = { targetGrid: target };
|
|
2918
|
-
const decision =
|
|
3064
|
+
const decision = resolveAnticipation(target, runOpts);
|
|
2919
3065
|
let resetZoom = null;
|
|
2920
3066
|
if (decision.active) {
|
|
2921
3067
|
log?.(`Anticipation on reels [${decision.reels.join(', ')}]`);
|
|
2922
3068
|
resetZoom = await anticipation.zoomIn(grid);
|
|
2923
3069
|
}
|
|
2924
|
-
await spin.run(data,
|
|
2925
|
-
...runOpts,
|
|
2926
|
-
anticipateReels: decision.active ? decision.reels : undefined,
|
|
2927
|
-
anticipateSlowdown: decision.slowdown,
|
|
2928
|
-
anticipateHoldMs: decision.holdMs,
|
|
2929
|
-
});
|
|
3070
|
+
await spin.run(data, mergeAnticipation(runOpts, decision));
|
|
2930
3071
|
if (resetZoom)
|
|
2931
3072
|
await resetZoom();
|
|
2932
3073
|
board = target;
|
|
@@ -3171,5 +3312,5 @@ class MultiplierAccumulator {
|
|
|
3171
3312
|
}
|
|
3172
3313
|
}
|
|
3173
3314
|
|
|
3174
|
-
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 };
|
|
3315
|
+
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, perReelValue, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount };
|
|
3175
3316
|
//# sourceMappingURL=slot.esm.js.map
|