@energy8platform/game-engine 0.39.0 → 0.40.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 {
@@ -773,6 +796,13 @@ interface ReelStopPlan {
773
796
  slowdown: number;
774
797
  /** True when `deferReveal` withheld this reel's landing (see `SpinRunOpts.deferReveal`). */
775
798
  deferred: boolean;
799
+ /**
800
+ * `cascade-drop` only: ms from spin start at which each cell of this reel seats, INDEXED BY ROW.
801
+ * The reel's fill direction lives in these numbers (`motion.dropOrder` decides which row is the
802
+ * smallest), as does its place in the chain (`motion.dropSequence`). `stopTime` is the largest
803
+ * of them — the frame the whole reel has landed.
804
+ */
805
+ cellStopTimes?: number[];
776
806
  }
777
807
  declare class SpinEngine {
778
808
  private _grid;
@@ -791,6 +821,13 @@ declare class SpinEngine {
791
821
  private scale;
792
822
  /** PURE: per-reel stop schedule. No Pixi mutation. */
793
823
  plan(data: SpinData, opts?: SpinRunOpts): ReelStopPlan[];
824
+ /**
825
+ * `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a
826
+ * sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the
827
+ * reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth
828
+ * for WHEN anything happens — `_runDrop` below only executes these numbers.
829
+ */
830
+ private _planDrop;
794
831
  /** Execute the spin for every reel concurrently. */
795
832
  run(data: SpinData, opts?: SpinRunOpts): Promise<void>;
796
833
  private _runReel;
@@ -1135,4 +1172,4 @@ declare class MultiplierAccumulator {
1135
1172
  }
1136
1173
 
1137
1174
  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 };
1175
+ 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,
@@ -1569,6 +1571,7 @@ class SpinEngine {
1569
1571
  const order = (reel) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
1570
1572
  const anticipate = new Set(opts?.anticipateReels ?? []);
1571
1573
  const defer = new Set(opts?.deferReveal ?? []);
1574
+ const holds = [];
1572
1575
  const out = [];
1573
1576
  for (let reel = 0; reel < cols; reel++) {
1574
1577
  const idx = order(reel);
@@ -1584,8 +1587,9 @@ class SpinEngine {
1584
1587
  else
1585
1588
  stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
1586
1589
  const isAnticipated = anticipate.has(reel);
1587
- if (isAnticipated)
1588
- stopTime += perReelValue(opts?.anticipateHoldMs, reel, 0) * f;
1590
+ const hold = isAnticipated ? perReelValue(opts?.anticipateHoldMs, reel, 0) * f : 0;
1591
+ stopTime += hold;
1592
+ holds[reel] = hold;
1589
1593
  const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
1590
1594
  out.push({
1591
1595
  reel,
@@ -1597,8 +1601,48 @@ class SpinEngine {
1597
1601
  deferred: defer.has(reel),
1598
1602
  });
1599
1603
  }
1604
+ if (this._cfg.style === 'cascade-drop')
1605
+ this._planDrop(out, holds, f, order);
1600
1606
  return out;
1601
1607
  }
1608
+ /**
1609
+ * `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a
1610
+ * sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the
1611
+ * reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth
1612
+ * for WHEN anything happens — `_runDrop` below only executes these numbers.
1613
+ */
1614
+ _planDrop(out, holds, f, order) {
1615
+ const bottomUp = this._cfg.dropOrder === 'bottom-up';
1616
+ // walk the reels in STOP order, so `stopOrder: 'rtl'` reverses the drop as well
1617
+ const byPosition = [];
1618
+ for (let reel = 0; reel < out.length; reel++)
1619
+ byPosition[order(reel)] = reel;
1620
+ // the position from which reels stop overlapping and start queueing behind each other
1621
+ const chainFrom = this._cfg.dropSequence === 'chained'
1622
+ ? 0
1623
+ : this._cfg.dropSequence === 'chained-when-anticipated'
1624
+ ? byPosition.findIndex((reel) => out[reel].anticipated)
1625
+ : -1;
1626
+ let previousEnd = 0;
1627
+ for (let position = 0; position < byPosition.length; position++) {
1628
+ const p = out[byPosition[position]];
1629
+ const rows = this._grid.rowsOf(p.reel);
1630
+ const scale = f * p.slowdown;
1631
+ const gap = this._cfg.stopStagger * this._cfg.reelStaggerFactor * scale;
1632
+ const fall = this._cfg.spinUp * this._cfg.dropFallFactor * scale;
1633
+ // by formula while reels may overlap; queued behind the previous reel once the chain starts
1634
+ const chained = chainFrom >= 0 && position > 0 && position >= chainFrom;
1635
+ const start = (chained ? previousEnd + gap : position * gap) + (holds[p.reel] ?? 0);
1636
+ const cells = [];
1637
+ for (let i = 0; i < rows; i++) {
1638
+ const row = bottomUp ? rows - 1 - i : i;
1639
+ cells[row] = start + i * this._cfg.cellStagger * scale + fall;
1640
+ }
1641
+ p.cellStopTimes = cells;
1642
+ p.stopTime = cells.length ? Math.max(...cells) : start;
1643
+ previousEnd = p.stopTime;
1644
+ }
1645
+ }
1602
1646
  /** Execute the spin for every reel concurrently. */
1603
1647
  async run(data, opts) {
1604
1648
  this._killed = false;
@@ -1740,6 +1784,8 @@ class SpinEngine {
1740
1784
  }
1741
1785
  const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
1742
1786
  const slow = this.slowOf(p); // anticipation drops the reel in more slowly
1787
+ const fall = this._cfg.spinUp * this._cfg.dropFallFactor * f * slow;
1788
+ const schedule = p.cellStopTimes ?? [];
1743
1789
  await Promise.all(Array.from({ length: rows }, (_, r) => r).map(async (r) => {
1744
1790
  if (this._killed)
1745
1791
  return;
@@ -1749,13 +1795,11 @@ class SpinEngine {
1749
1795
  cell.setData(data);
1750
1796
  cell.position.set(to.x, to.y - step * (rows + 1));
1751
1797
  cell.alpha = 1;
1752
- const delay = (p.reel * this._cfg.stopStagger * this._cfg.reelStaggerFactor +
1753
- r * this._cfg.cellStagger) *
1754
- f *
1755
- slow;
1798
+ // the plan says WHEN this cell seats; back off the fall to get when it must let go
1799
+ const delay = Math.max(0, (schedule[r] ?? fall) - fall);
1756
1800
  if (delay)
1757
1801
  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));
1802
+ await Tween.to(cell, { 'position.y': to.y }, fall, easingByName(this._cfg.settle.easing));
1759
1803
  // the impact frame — fired before the squash so a game can sync its own hit feedback
1760
1804
  opts?.onCellSeated?.(p.reel, r, data);
1761
1805
  await this._squashCell(cell, f);