@energy8platform/game-engine 0.39.0 → 0.41.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/dist/slot.d.ts CHANGED
@@ -411,6 +411,10 @@ interface GridConfig {
411
411
  }
412
412
  type MotionStyle = 'swap' | 'strip' | 'cascade-drop';
413
413
  type StopMode = 'sequential' | 'sync' | 'random';
414
+ /** `cascade-drop` fill direction within one reel. */
415
+ type DropOrder = 'top-down' | 'bottom-up';
416
+ /** How `cascade-drop` spaces reels: by formula, or one strictly after the other. */
417
+ type DropSequence = 'parallel' | 'chained' | 'chained-when-anticipated';
414
418
  type StopOrder = 'ltr' | 'rtl';
415
419
  type Intensity = 'full' | 'reduced' | 'minimal';
416
420
  interface SettleConfig {
@@ -462,6 +466,25 @@ interface MotionConfig {
462
466
  reelStaggerFactor: number;
463
467
  /** `cascade-drop`: fall duration as a fraction of `spinUp`. Default 0.6. */
464
468
  dropFallFactor: number;
469
+ /**
470
+ * `cascade-drop`: which cell of a reel lands first. `'top-down'` (default, the engine's original
471
+ * behaviour) deals the reel like cards from the top; `'bottom-up'` fills it the way gravity
472
+ * would — the lowest cell arrives first and the rest stack on top of it.
473
+ */
474
+ dropOrder: DropOrder;
475
+ /**
476
+ * `cascade-drop`: how reels are spaced.
477
+ * - `'parallel'` (default) starts every reel at its own formula offset —
478
+ * `reel * stopStagger * reelStaggerFactor` — so a reel can open while the previous one is
479
+ * still dropping. Fast, and what a normal spin wants.
480
+ * - `'chained'` starts a reel only once the previous one has seated its LAST cell, plus that
481
+ * same offset as the gap. One reel at a time, always — correct, but it makes every spin as
482
+ * long as the sum of its reels.
483
+ * - `'chained-when-anticipated'` runs the un-armed reels in parallel and switches to the chain
484
+ * from the first ANTICIPATED reel onwards. The base spin keeps its stop window; the hunt for
485
+ * the last scatter goes strictly reel by reel, each slower than the last.
486
+ */
487
+ dropSequence: DropSequence;
465
488
  }
466
489
  /** What a game-supplied `AnticipationConfig.decide` may return instead of a bare reel list. */
467
490
  interface AnticipationOverride {
@@ -471,6 +494,8 @@ interface AnticipationOverride {
471
494
  slowdown?: PerReel<number>;
472
495
  /** Extra hold before landing. Scalar, or per-reel indexed by reel index. */
473
496
  holdMs?: PerReel<number>;
497
+ /** Growth of the gap between successive cells within a reel. Scalar, or per-reel. */
498
+ cellStaggerRamp?: PerReel<number>;
474
499
  }
475
500
  interface AnticipationConfig {
476
501
  enabled: boolean;
@@ -499,6 +524,16 @@ interface AnticipationConfig {
499
524
  progressiveSlowdown: number;
500
525
  /** Extra hold (ms) added per successive anticipated reel: reel #i gets `holdMs + i * this`. */
501
526
  progressiveHoldMs: number;
527
+ /**
528
+ * `cascade-drop`: how the gap between SUCCESSIVE CELLS grows inside an anticipated reel. The
529
+ * gap before cell #i is `cellStagger * <reel slowdown> * progressiveCellStagger ** i`, so 1
530
+ * (the default) keeps the reel's cells evenly spaced and > 1 makes each symbol land later than
531
+ * the last — the drip that turns a reel into a countdown.
532
+ *
533
+ * It compounds with `progressiveSlowdown`: that widens the base gap reel by reel, this widens
534
+ * it cell by cell, so a later reel drips both slower AND with a steeper ramp.
535
+ */
536
+ progressiveCellStagger: number;
502
537
  /** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */
503
538
  zoom: {
504
539
  enabled: boolean;
@@ -743,6 +778,12 @@ interface SpinRunOpts {
743
778
  anticipateSlowdown?: PerReel<number>;
744
779
  /** Extra hold (ms) for anticipated reels. Scalar, or per-reel indexed by reel. */
745
780
  anticipateHoldMs?: PerReel<number>;
781
+ /**
782
+ * `cascade-drop`: growth of the gap between successive cells inside an anticipated reel — the
783
+ * gap before cell #i is `cellStagger * <reel slowdown> * ramp ** i`. 1 keeps the reel evenly
784
+ * spaced. Scalar, or per-reel indexed by reel.
785
+ */
786
+ anticipateCellStaggerRamp?: PerReel<number>;
746
787
  /**
747
788
  * Reels whose tape runs normally but whose landing is NOT handed back. The engine stops and
748
789
  * disposes of the tape as usual and leaves the real cells hidden and unseated; the caller owns
@@ -773,6 +814,13 @@ interface ReelStopPlan {
773
814
  slowdown: number;
774
815
  /** True when `deferReveal` withheld this reel's landing (see `SpinRunOpts.deferReveal`). */
775
816
  deferred: boolean;
817
+ /**
818
+ * `cascade-drop` only: ms from spin start at which each cell of this reel seats, INDEXED BY ROW.
819
+ * The reel's fill direction lives in these numbers (`motion.dropOrder` decides which row is the
820
+ * smallest), as does its place in the chain (`motion.dropSequence`). `stopTime` is the largest
821
+ * of them — the frame the whole reel has landed.
822
+ */
823
+ cellStopTimes?: number[];
776
824
  }
777
825
  declare class SpinEngine {
778
826
  private _grid;
@@ -791,6 +839,13 @@ declare class SpinEngine {
791
839
  private scale;
792
840
  /** PURE: per-reel stop schedule. No Pixi mutation. */
793
841
  plan(data: SpinData, opts?: SpinRunOpts): ReelStopPlan[];
842
+ /**
843
+ * `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a
844
+ * sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the
845
+ * reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth
846
+ * for WHEN anything happens — `_runDrop` below only executes these numbers.
847
+ */
848
+ private _planDrop;
794
849
  /** Execute the spin for every reel concurrently. */
795
850
  run(data: SpinData, opts?: SpinRunOpts): Promise<void>;
796
851
  private _runReel;
@@ -817,6 +872,8 @@ interface AnticipationDecision {
817
872
  slowdown: PerReel<number>;
818
873
  /** Extra hold before landing. Scalar or per-reel array, same as `slowdown`. */
819
874
  holdMs: PerReel<number>;
875
+ /** `cascade-drop`: growth of the gap between successive cells inside the reel (1 = even). */
876
+ cellStaggerRamp: PerReel<number>;
820
877
  }
821
878
  declare class AnticipationController {
822
879
  private _cfg;
@@ -1135,4 +1192,4 @@ declare class MultiplierAccumulator {
1135
1192
  }
1136
1193
 
1137
1194
  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 };
1195
+ export type { AnimatedSymbolConfig, AnticipationConfig, AnticipationDecision, AnticipationOverride, BigWinOverlayConfig, BlurConfig, CarryPolicy, CascadeAnim, CascadeConfig, CascadeStepData, CascadeTimings, CellData, CellFrameStyle, CellSizeSpec, CellState, CountUpConfig, CreateReelSystemOptions, DecorationConfig, DeepPartial, DropOrder, DropSequence, 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
@@ -1129,6 +1129,8 @@ const DEFAULT_REEL_CONFIG = {
1129
1129
  cellStagger: 24,
1130
1130
  reelStaggerFactor: 0.4,
1131
1131
  dropFallFactor: 0.6,
1132
+ dropOrder: 'top-down',
1133
+ dropSequence: 'parallel',
1132
1134
  },
1133
1135
  anticipation: {
1134
1136
  enabled: false,
@@ -1140,6 +1142,7 @@ const DEFAULT_REEL_CONFIG = {
1140
1142
  decide: null,
1141
1143
  progressiveSlowdown: 1,
1142
1144
  progressiveHoldMs: 0,
1145
+ progressiveCellStagger: 1,
1143
1146
  zoom: { enabled: false, scale: 1.15, ms: 600 },
1144
1147
  },
1145
1148
  cascade: {
@@ -1569,6 +1572,8 @@ class SpinEngine {
1569
1572
  const order = (reel) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
1570
1573
  const anticipate = new Set(opts?.anticipateReels ?? []);
1571
1574
  const defer = new Set(opts?.deferReveal ?? []);
1575
+ const holds = [];
1576
+ const ramps = [];
1572
1577
  const out = [];
1573
1578
  for (let reel = 0; reel < cols; reel++) {
1574
1579
  const idx = order(reel);
@@ -1584,8 +1589,10 @@ class SpinEngine {
1584
1589
  else
1585
1590
  stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
1586
1591
  const isAnticipated = anticipate.has(reel);
1587
- if (isAnticipated)
1588
- stopTime += perReelValue(opts?.anticipateHoldMs, reel, 0) * f;
1592
+ const hold = isAnticipated ? perReelValue(opts?.anticipateHoldMs, reel, 0) * f : 0;
1593
+ stopTime += hold;
1594
+ holds[reel] = hold;
1595
+ ramps[reel] = isAnticipated ? perReelValue(opts?.anticipateCellStaggerRamp, reel, 1) : 1;
1589
1596
  const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
1590
1597
  out.push({
1591
1598
  reel,
@@ -1597,8 +1604,53 @@ class SpinEngine {
1597
1604
  deferred: defer.has(reel),
1598
1605
  });
1599
1606
  }
1607
+ if (this._cfg.style === 'cascade-drop')
1608
+ this._planDrop(out, holds, ramps, f, order);
1600
1609
  return out;
1601
1610
  }
1611
+ /**
1612
+ * `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a
1613
+ * sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the
1614
+ * reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth
1615
+ * for WHEN anything happens — `_runDrop` below only executes these numbers.
1616
+ */
1617
+ _planDrop(out, holds, ramps, f, order) {
1618
+ const bottomUp = this._cfg.dropOrder === 'bottom-up';
1619
+ // walk the reels in STOP order, so `stopOrder: 'rtl'` reverses the drop as well
1620
+ const byPosition = [];
1621
+ for (let reel = 0; reel < out.length; reel++)
1622
+ byPosition[order(reel)] = reel;
1623
+ // the position from which reels stop overlapping and start queueing behind each other
1624
+ const chainFrom = this._cfg.dropSequence === 'chained'
1625
+ ? 0
1626
+ : this._cfg.dropSequence === 'chained-when-anticipated'
1627
+ ? byPosition.findIndex((reel) => out[reel].anticipated)
1628
+ : -1;
1629
+ let previousEnd = 0;
1630
+ for (let position = 0; position < byPosition.length; position++) {
1631
+ const p = out[byPosition[position]];
1632
+ const rows = this._grid.rowsOf(p.reel);
1633
+ const scale = f * p.slowdown;
1634
+ const gap = this._cfg.stopStagger * this._cfg.reelStaggerFactor * scale;
1635
+ const fall = this._cfg.spinUp * this._cfg.dropFallFactor * scale;
1636
+ // by formula while reels may overlap; queued behind the previous reel once the chain starts
1637
+ const chained = chainFrom >= 0 && position > 0 && position >= chainFrom;
1638
+ const start = (chained ? previousEnd + gap : position * gap) + (holds[p.reel] ?? 0);
1639
+ // The gap before cell #i is `cellStagger * scale * ramp**i` — accumulate rather than
1640
+ // multiply, so a ramp of 1 stays exactly the old evenly-spaced schedule.
1641
+ const ramp = ramps[p.reel] ?? 1;
1642
+ const cells = [];
1643
+ let offset = 0;
1644
+ for (let i = 0; i < rows; i++) {
1645
+ const row = bottomUp ? rows - 1 - i : i;
1646
+ cells[row] = start + offset + fall;
1647
+ offset += this._cfg.cellStagger * scale * Math.pow(ramp, i);
1648
+ }
1649
+ p.cellStopTimes = cells;
1650
+ p.stopTime = cells.length ? Math.max(...cells) : start;
1651
+ previousEnd = p.stopTime;
1652
+ }
1653
+ }
1602
1654
  /** Execute the spin for every reel concurrently. */
1603
1655
  async run(data, opts) {
1604
1656
  this._killed = false;
@@ -1740,6 +1792,8 @@ class SpinEngine {
1740
1792
  }
1741
1793
  const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
1742
1794
  const slow = this.slowOf(p); // anticipation drops the reel in more slowly
1795
+ const fall = this._cfg.spinUp * this._cfg.dropFallFactor * f * slow;
1796
+ const schedule = p.cellStopTimes ?? [];
1743
1797
  await Promise.all(Array.from({ length: rows }, (_, r) => r).map(async (r) => {
1744
1798
  if (this._killed)
1745
1799
  return;
@@ -1749,13 +1803,11 @@ class SpinEngine {
1749
1803
  cell.setData(data);
1750
1804
  cell.position.set(to.x, to.y - step * (rows + 1));
1751
1805
  cell.alpha = 1;
1752
- const delay = (p.reel * this._cfg.stopStagger * this._cfg.reelStaggerFactor +
1753
- r * this._cfg.cellStagger) *
1754
- f *
1755
- slow;
1806
+ // the plan says WHEN this cell seats; back off the fall to get when it must let go
1807
+ const delay = Math.max(0, (schedule[r] ?? fall) - fall);
1756
1808
  if (delay)
1757
1809
  await Tween.delay(delay);
1758
- await Tween.to(cell, { 'position.y': to.y }, this._cfg.spinUp * this._cfg.dropFallFactor * f * slow, easingByName(this._cfg.settle.easing));
1810
+ await Tween.to(cell, { 'position.y': to.y }, fall, easingByName(this._cfg.settle.easing));
1759
1811
  // the impact frame — fired before the squash so a game can sync its own hit feedback
1760
1812
  opts?.onCellSeated?.(p.reel, r, data);
1761
1813
  await this._squashCell(cell, f);
@@ -1848,7 +1900,13 @@ class SpinEngine {
1848
1900
  // Decides which trailing reels get the "anticipation" slow-down treatment, based purely on
1849
1901
  // the already-resolved landing grid (presentation only — never a secondary outcome decision).
1850
1902
  /** Fresh "nothing to anticipate" decision (fresh, so callers may mutate `reels` freely). */
1851
- const none = () => ({ active: false, reels: [], slowdown: 1, holdMs: 0 });
1903
+ const none = () => ({
1904
+ active: false,
1905
+ reels: [],
1906
+ slowdown: 1,
1907
+ holdMs: 0,
1908
+ cellStaggerRamp: 1,
1909
+ });
1852
1910
  class AnticipationController {
1853
1911
  _cfg;
1854
1912
  constructor(cfg) {
@@ -1881,7 +1939,7 @@ class AnticipationController {
1881
1939
  const o = Array.isArray(custom) ? { reels: custom } : custom;
1882
1940
  if (!o.reels?.length)
1883
1941
  return none();
1884
- return this.build(o.reels.slice(), o.slowdown, o.holdMs);
1942
+ return this.build(o.reels.slice(), o.slowdown, o.holdMs, o.cellStaggerRamp);
1885
1943
  }
1886
1944
  if (Array.isArray(this._cfg.reels)) {
1887
1945
  // explicit reel list — arm only if the threshold is met somewhere on the board
@@ -1910,10 +1968,11 @@ class AnticipationController {
1910
1968
  return this.build(reels);
1911
1969
  }
1912
1970
  /** Assemble a decision, applying the configured progression unless the caller pinned values. */
1913
- build(reels, slowdown, holdMs) {
1971
+ build(reels, slowdown, holdMs, cellStaggerRamp) {
1914
1972
  return {
1915
1973
  active: true,
1916
1974
  reels,
1975
+ cellStaggerRamp: cellStaggerRamp ?? this._cfg.progressiveCellStagger,
1917
1976
  slowdown: slowdown ??
1918
1977
  this.ramp(reels, this._cfg.slowdownFactor, (base, i) => i === 0 ? base : base * Math.pow(this._cfg.progressiveSlowdown, i)),
1919
1978
  holdMs: holdMs ??
@@ -2993,12 +3052,13 @@ function createReelSystem(opts) {
2993
3052
  if (!explicit)
2994
3053
  return anticipation.decide(target);
2995
3054
  if (!explicit.length)
2996
- return { active: false, reels: [], slowdown: 1, holdMs: 0 };
3055
+ return { active: false, reels: [], slowdown: 1, holdMs: 0, cellStaggerRamp: 1 };
2997
3056
  return {
2998
3057
  active: true,
2999
3058
  reels: explicit.slice(),
3000
3059
  slowdown: runOpts?.anticipateSlowdown ?? config.anticipation.slowdownFactor,
3001
3060
  holdMs: runOpts?.anticipateHoldMs ?? config.anticipation.holdMs,
3061
+ cellStaggerRamp: runOpts?.anticipateCellStaggerRamp ?? config.anticipation.progressiveCellStagger,
3002
3062
  };
3003
3063
  }
3004
3064
  /** Fold a resolved decision back onto the caller's run options (everything else passes through). */
@@ -3008,6 +3068,7 @@ function createReelSystem(opts) {
3008
3068
  anticipateReels: decision.active ? decision.reels : undefined,
3009
3069
  anticipateSlowdown: decision.slowdown,
3010
3070
  anticipateHoldMs: decision.holdMs,
3071
+ anticipateCellStaggerRamp: decision.cellStaggerRamp,
3011
3072
  };
3012
3073
  }
3013
3074
  buildGrid();