@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.cjs.js
CHANGED
|
@@ -1075,6 +1075,14 @@ class ReelSpinController {
|
|
|
1075
1075
|
// override only what they need.
|
|
1076
1076
|
//
|
|
1077
1077
|
// Design notes are in docs/reels-analysis-and-design.md.
|
|
1078
|
+
/** Resolve a `PerReel<T>` for one reel. `undefined` (or a hole in the array) yields `fallback`. */
|
|
1079
|
+
function perReelValue(value, reel, fallback) {
|
|
1080
|
+
if (value === undefined)
|
|
1081
|
+
return fallback;
|
|
1082
|
+
if (Array.isArray(value))
|
|
1083
|
+
return value[reel] ?? fallback;
|
|
1084
|
+
return value;
|
|
1085
|
+
}
|
|
1078
1086
|
const FEATURE_KEYS = [
|
|
1079
1087
|
'reelModifier', // pre-spin
|
|
1080
1088
|
'giant',
|
|
@@ -1120,6 +1128,9 @@ const DEFAULT_REEL_CONFIG = {
|
|
|
1120
1128
|
intensity: 'full',
|
|
1121
1129
|
slamStop: true,
|
|
1122
1130
|
symbolsPerReel: 6,
|
|
1131
|
+
cellStagger: 24,
|
|
1132
|
+
reelStaggerFactor: 0.4,
|
|
1133
|
+
dropFallFactor: 0.6,
|
|
1123
1134
|
},
|
|
1124
1135
|
anticipation: {
|
|
1125
1136
|
enabled: false,
|
|
@@ -1128,6 +1139,9 @@ const DEFAULT_REEL_CONFIG = {
|
|
|
1128
1139
|
reels: 'trailing',
|
|
1129
1140
|
slowdownFactor: 0.3,
|
|
1130
1141
|
holdMs: 400,
|
|
1142
|
+
decide: null,
|
|
1143
|
+
progressiveSlowdown: 1,
|
|
1144
|
+
progressiveHoldMs: 0,
|
|
1131
1145
|
zoom: { enabled: false, scale: 1.15, ms: 600 },
|
|
1132
1146
|
},
|
|
1133
1147
|
cascade: {
|
|
@@ -1254,11 +1268,28 @@ function mergeReelConfig(base, partial) {
|
|
|
1254
1268
|
function resolveReelConfig(partial) {
|
|
1255
1269
|
return mergeReelConfig(DEFAULT_REEL_CONFIG, partial);
|
|
1256
1270
|
}
|
|
1271
|
+
/** True only for `{}`-shaped objects — a class instance or a Date is NOT one. */
|
|
1272
|
+
function isCloneableRecord(v) {
|
|
1273
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
1274
|
+
return false;
|
|
1275
|
+
const proto = Object.getPrototypeOf(v);
|
|
1276
|
+
return proto === Object.prototype || proto === null;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Deep-clone a config. Hand-rolled rather than `structuredClone` because a config may carry
|
|
1280
|
+
* functions (`anticipation.decide`), which `structuredClone` refuses to copy. Functions and
|
|
1281
|
+
* anything that is not a plain object/array pass through by reference.
|
|
1282
|
+
*/
|
|
1257
1283
|
function structuredCloneSafe(v) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1284
|
+
if (Array.isArray(v))
|
|
1285
|
+
return v.map((item) => structuredCloneSafe(item));
|
|
1286
|
+
if (isCloneableRecord(v)) {
|
|
1287
|
+
const out = {};
|
|
1288
|
+
for (const [k, val] of Object.entries(v))
|
|
1289
|
+
out[k] = structuredCloneSafe(val);
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
return v;
|
|
1262
1293
|
}
|
|
1263
1294
|
/** Effective per-reel row counts (resolves Megaways `rowsPerReel`, else uniform `rows`). */
|
|
1264
1295
|
function effectiveRowsPerReel(grid) {
|
|
@@ -1495,6 +1526,7 @@ class SpinEngine {
|
|
|
1495
1526
|
_killed = false;
|
|
1496
1527
|
_shaking = false;
|
|
1497
1528
|
_temp = [];
|
|
1529
|
+
_deferred = new Set();
|
|
1498
1530
|
constructor(grid, resolve, cfg, win) {
|
|
1499
1531
|
this._grid = grid;
|
|
1500
1532
|
this._resolve = resolve;
|
|
@@ -1538,6 +1570,7 @@ class SpinEngine {
|
|
|
1538
1570
|
const cols = this._grid.cols;
|
|
1539
1571
|
const order = (reel) => (this._cfg.stopOrder === 'rtl' ? cols - 1 - reel : reel);
|
|
1540
1572
|
const anticipate = new Set(opts?.anticipateReels ?? []);
|
|
1573
|
+
const defer = new Set(opts?.deferReveal ?? []);
|
|
1541
1574
|
const out = [];
|
|
1542
1575
|
for (let reel = 0; reel < cols; reel++) {
|
|
1543
1576
|
const idx = order(reel);
|
|
@@ -1554,13 +1587,16 @@ class SpinEngine {
|
|
|
1554
1587
|
stopTime = (this._cfg.spinUp + this._cfg.hold + idx * this._cfg.stopStagger) * f;
|
|
1555
1588
|
const isAnticipated = anticipate.has(reel);
|
|
1556
1589
|
if (isAnticipated)
|
|
1557
|
-
stopTime += (opts?.anticipateHoldMs
|
|
1590
|
+
stopTime += perReelValue(opts?.anticipateHoldMs, reel, 0) * f;
|
|
1591
|
+
const speed = isAnticipated ? perReelValue(opts?.anticipateSlowdown, reel, 1) : 1;
|
|
1558
1592
|
out.push({
|
|
1559
1593
|
reel,
|
|
1560
1594
|
stopTime,
|
|
1561
1595
|
landing: data.targetGrid[reel] ?? [],
|
|
1562
1596
|
settle: { amp: this._cfg.settle.amp, ms: this._cfg.settle.ms * f },
|
|
1563
1597
|
anticipated: isAnticipated,
|
|
1598
|
+
slowdown: Math.max(1, 1 / (speed || 1)),
|
|
1599
|
+
deferred: defer.has(reel),
|
|
1564
1600
|
});
|
|
1565
1601
|
}
|
|
1566
1602
|
return out;
|
|
@@ -1570,6 +1606,9 @@ class SpinEngine {
|
|
|
1570
1606
|
this._killed = false;
|
|
1571
1607
|
this._temp = [];
|
|
1572
1608
|
const plan = this.plan(data, opts);
|
|
1609
|
+
// remembered for skip(): a deferred reel's cells belong to the caller, not to us
|
|
1610
|
+
this._deferred = new Set(plan.filter((p) => p.deferred).map((p) => p.reel));
|
|
1611
|
+
opts?.onPlan?.(plan);
|
|
1573
1612
|
const f = this.scale(opts);
|
|
1574
1613
|
await Promise.all(plan.map((p) => this._runReel(p, data, opts, f)));
|
|
1575
1614
|
this._cleanupTemp();
|
|
@@ -1587,9 +1626,9 @@ class SpinEngine {
|
|
|
1587
1626
|
return this._runSwap(p, data, opts, f);
|
|
1588
1627
|
}
|
|
1589
1628
|
}
|
|
1590
|
-
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). */
|
|
1591
|
-
slowOf(p
|
|
1592
|
-
return p.
|
|
1629
|
+
/** Anticipation time-stretch factor for a reel (>=1, longer = slower). Resolved in `plan()`. */
|
|
1630
|
+
slowOf(p) {
|
|
1631
|
+
return p.slowdown;
|
|
1593
1632
|
}
|
|
1594
1633
|
// ── swap: cycle symbols quickly in the real cells, then land ──────────────
|
|
1595
1634
|
async _runSwap(p, data, opts, f) {
|
|
@@ -1600,7 +1639,7 @@ class SpinEngine {
|
|
|
1600
1639
|
const blur = this._applyBlur(cells, true);
|
|
1601
1640
|
const tickMs = 1000 / 30;
|
|
1602
1641
|
// anticipation makes the reel spin longer before it lands
|
|
1603
|
-
const ticks = Math.max(6, Math.floor((p.stopTime * this.slowOf(p
|
|
1642
|
+
const ticks = Math.max(6, Math.floor((p.stopTime * this.slowOf(p)) / tickMs));
|
|
1604
1643
|
for (let i = 0; i < ticks; i++) {
|
|
1605
1644
|
if (this._killed)
|
|
1606
1645
|
break;
|
|
@@ -1609,8 +1648,24 @@ class SpinEngine {
|
|
|
1609
1648
|
await Tween.delay(tickMs);
|
|
1610
1649
|
}
|
|
1611
1650
|
blur?.();
|
|
1612
|
-
|
|
1613
|
-
|
|
1651
|
+
if (p.deferred) {
|
|
1652
|
+
// the caller owns this reel's result — go dark and unseated instead of handing it back.
|
|
1653
|
+
// NB 'swap' cycles the tape THROUGH the real cells, so a deferred reel only truly withholds
|
|
1654
|
+
// its result when `SpinData.strip` supplies filler for it; otherwise the tape is built from
|
|
1655
|
+
// the landing symbols. 'strip' and 'cascade-drop' have no such caveat.
|
|
1656
|
+
cells.forEach((c) => {
|
|
1657
|
+
c.setData({ symbol: null });
|
|
1658
|
+
c.visible = false;
|
|
1659
|
+
});
|
|
1660
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
for (let r = 0; r < cells.length; r++) {
|
|
1664
|
+
const cell = p.landing[r] ?? { symbol: null };
|
|
1665
|
+
cells[r].setData(cell);
|
|
1666
|
+
opts?.onCellSeated?.(p.reel, r, cell);
|
|
1667
|
+
}
|
|
1668
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1614
1669
|
await this._settle(p.reel, p.settle, f);
|
|
1615
1670
|
await this._frameShake(p.landing);
|
|
1616
1671
|
}
|
|
@@ -1645,7 +1700,7 @@ class SpinEngine {
|
|
|
1645
1700
|
this._grid.addChild(tape);
|
|
1646
1701
|
this._temp.push(tape);
|
|
1647
1702
|
const clearBlur = this._applyBlur([tape], true);
|
|
1648
|
-
const slow = this.slowOf(p
|
|
1703
|
+
const slow = this.slowOf(p);
|
|
1649
1704
|
// overshoot/settle honour the configured settle (amp in px, easing)
|
|
1650
1705
|
const overshoot = p.settle.amp || step * 0.18;
|
|
1651
1706
|
await Tween.to(tape, { y: restY + overshoot }, p.stopTime * slow, easingByName('easeInOutQuad'));
|
|
@@ -1655,12 +1710,21 @@ class SpinEngine {
|
|
|
1655
1710
|
}
|
|
1656
1711
|
clearBlur?.();
|
|
1657
1712
|
await Tween.to(tape, { y: restY }, Math.max(120, p.settle.ms), easingByName(this._cfg.settle.easing));
|
|
1658
|
-
// hand the result back to the real cells
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1713
|
+
// hand the result back to the real cells — unless the caller deferred the reveal, in which
|
|
1714
|
+
// case the tape simply goes away and the reel is left dark, unseated and owned by the caller
|
|
1715
|
+
if (!p.deferred) {
|
|
1716
|
+
for (let r = 0; r < rows; r++) {
|
|
1717
|
+
const cell = p.landing[r] ?? { symbol: null };
|
|
1718
|
+
realCells[r].setData(cell);
|
|
1719
|
+
opts?.onCellSeated?.(p.reel, r, cell);
|
|
1720
|
+
}
|
|
1721
|
+
realCells.forEach((c) => (c.visible = true));
|
|
1722
|
+
}
|
|
1662
1723
|
tape.destroy();
|
|
1663
1724
|
this._temp = this._temp.filter((t) => t !== tape);
|
|
1725
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1726
|
+
if (p.deferred)
|
|
1727
|
+
return;
|
|
1664
1728
|
// squash the real cells on impact when enabled
|
|
1665
1729
|
if (this._cfg.squash.enabled)
|
|
1666
1730
|
await Promise.all(realCells.map((c) => this._squashCell(c, f)));
|
|
@@ -1669,22 +1733,36 @@ class SpinEngine {
|
|
|
1669
1733
|
// ── cascade-drop: symbols drop in from above with stagger + bounce + squash ─
|
|
1670
1734
|
async _runDrop(p, opts, f) {
|
|
1671
1735
|
const rows = this._grid.rowsOf(p.reel);
|
|
1736
|
+
if (p.deferred) {
|
|
1737
|
+
// nothing to drop — the caller brings this reel in itself
|
|
1738
|
+
for (let r = 0; r < rows; r++)
|
|
1739
|
+
this._grid.getCell(p.reel, r).visible = false;
|
|
1740
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1672
1743
|
const step = this._grid.cellPosition(p.reel, 1).y - this._grid.cellPosition(p.reel, 0).y;
|
|
1673
|
-
const slow = this.slowOf(p
|
|
1744
|
+
const slow = this.slowOf(p); // anticipation drops the reel in more slowly
|
|
1674
1745
|
await Promise.all(Array.from({ length: rows }, (_, r) => r).map(async (r) => {
|
|
1675
1746
|
if (this._killed)
|
|
1676
1747
|
return;
|
|
1677
1748
|
const cell = this._grid.getCell(p.reel, r);
|
|
1678
1749
|
const to = this._grid.cellPosition(p.reel, r);
|
|
1679
|
-
|
|
1750
|
+
const data = p.landing[r] ?? { symbol: null };
|
|
1751
|
+
cell.setData(data);
|
|
1680
1752
|
cell.position.set(to.x, to.y - step * (rows + 1));
|
|
1681
1753
|
cell.alpha = 1;
|
|
1682
|
-
const delay = (p.reel * this._cfg.stopStagger *
|
|
1754
|
+
const delay = (p.reel * this._cfg.stopStagger * this._cfg.reelStaggerFactor +
|
|
1755
|
+
r * this._cfg.cellStagger) *
|
|
1756
|
+
f *
|
|
1757
|
+
slow;
|
|
1683
1758
|
if (delay)
|
|
1684
1759
|
await Tween.delay(delay);
|
|
1685
|
-
await Tween.to(cell, { 'position.y': to.y }, this._cfg.spinUp *
|
|
1760
|
+
await Tween.to(cell, { 'position.y': to.y }, this._cfg.spinUp * this._cfg.dropFallFactor * f * slow, easingByName(this._cfg.settle.easing));
|
|
1761
|
+
// the impact frame — fired before the squash so a game can sync its own hit feedback
|
|
1762
|
+
opts?.onCellSeated?.(p.reel, r, data);
|
|
1686
1763
|
await this._squashCell(cell, f);
|
|
1687
1764
|
}));
|
|
1765
|
+
opts?.onReelStop?.(p.reel, p);
|
|
1688
1766
|
await this._frameShake(p.landing);
|
|
1689
1767
|
}
|
|
1690
1768
|
// ── shared helpers ────────────────────────────────────────────────────────
|
|
@@ -1748,13 +1826,17 @@ class SpinEngine {
|
|
|
1748
1826
|
Tween.killTweensOf(this._grid);
|
|
1749
1827
|
this._grid.x = 0; // undo any in-flight frame shake
|
|
1750
1828
|
for (let c = 0; c < this._grid.cols; c++) {
|
|
1829
|
+
// a deferred reel's visibility belongs to the caller — a slam stop must not reveal it
|
|
1830
|
+
const deferred = this._deferred.has(c);
|
|
1751
1831
|
for (let r = 0; r < this._grid.rowsOf(c); r++) {
|
|
1752
1832
|
const cell = this._grid.getCell(c, r);
|
|
1753
1833
|
if (cell.destroyed)
|
|
1754
1834
|
continue;
|
|
1755
1835
|
Tween.killTweensOf(cell);
|
|
1756
|
-
|
|
1757
|
-
|
|
1836
|
+
if (!deferred) {
|
|
1837
|
+
cell.visible = true;
|
|
1838
|
+
cell.alpha = 1;
|
|
1839
|
+
}
|
|
1758
1840
|
cell.filters = [];
|
|
1759
1841
|
cell.scale.set(1);
|
|
1760
1842
|
}
|
|
@@ -1767,6 +1849,8 @@ class SpinEngine {
|
|
|
1767
1849
|
//
|
|
1768
1850
|
// Decides which trailing reels get the "anticipation" slow-down treatment, based purely on
|
|
1769
1851
|
// the already-resolved landing grid (presentation only — never a secondary outcome decision).
|
|
1852
|
+
/** Fresh "nothing to anticipate" decision (fresh, so callers may mutate `reels` freely). */
|
|
1853
|
+
const none = () => ({ active: false, reels: [], slowdown: 1, holdMs: 0 });
|
|
1770
1854
|
class AnticipationController {
|
|
1771
1855
|
_cfg;
|
|
1772
1856
|
constructor(cfg) {
|
|
@@ -1790,19 +1874,23 @@ class AnticipationController {
|
|
|
1790
1874
|
*/
|
|
1791
1875
|
decide(targetGrid) {
|
|
1792
1876
|
if (!this._cfg.enabled)
|
|
1793
|
-
return
|
|
1877
|
+
return none();
|
|
1878
|
+
// A game-supplied predicate replaces the symbol counting entirely.
|
|
1879
|
+
if (this._cfg.decide) {
|
|
1880
|
+
const custom = this._cfg.decide(targetGrid);
|
|
1881
|
+
if (!custom)
|
|
1882
|
+
return none();
|
|
1883
|
+
const o = Array.isArray(custom) ? { reels: custom } : custom;
|
|
1884
|
+
if (!o.reels?.length)
|
|
1885
|
+
return none();
|
|
1886
|
+
return this.build(o.reels.slice(), o.slowdown, o.holdMs);
|
|
1887
|
+
}
|
|
1794
1888
|
if (Array.isArray(this._cfg.reels)) {
|
|
1795
1889
|
// explicit reel list — arm only if the threshold is met somewhere on the board
|
|
1796
1890
|
const total = targetGrid.reduce((sum, reel) => sum + this.countOnReel(reel), 0);
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
active,
|
|
1801
|
-
reels: this._cfg.reels.slice(),
|
|
1802
|
-
slowdown: this._cfg.slowdownFactor,
|
|
1803
|
-
holdMs: this._cfg.holdMs,
|
|
1804
|
-
}
|
|
1805
|
-
: { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
1891
|
+
if (total < this._cfg.threshold)
|
|
1892
|
+
return none();
|
|
1893
|
+
return this.build(this._cfg.reels.slice());
|
|
1806
1894
|
}
|
|
1807
1895
|
// 'trailing': find the reel where the cumulative count hits the threshold
|
|
1808
1896
|
let running = 0;
|
|
@@ -1815,13 +1903,37 @@ class AnticipationController {
|
|
|
1815
1903
|
}
|
|
1816
1904
|
}
|
|
1817
1905
|
if (armReel < 0)
|
|
1818
|
-
return
|
|
1906
|
+
return none();
|
|
1819
1907
|
const reels = [];
|
|
1820
1908
|
for (let c = armReel + 1; c < targetGrid.length; c++)
|
|
1821
1909
|
reels.push(c);
|
|
1822
1910
|
if (reels.length === 0)
|
|
1823
|
-
return
|
|
1824
|
-
return
|
|
1911
|
+
return none();
|
|
1912
|
+
return this.build(reels);
|
|
1913
|
+
}
|
|
1914
|
+
/** Assemble a decision, applying the configured progression unless the caller pinned values. */
|
|
1915
|
+
build(reels, slowdown, holdMs) {
|
|
1916
|
+
return {
|
|
1917
|
+
active: true,
|
|
1918
|
+
reels,
|
|
1919
|
+
slowdown: slowdown ??
|
|
1920
|
+
this.ramp(reels, this._cfg.slowdownFactor, (base, i) => i === 0 ? base : base * Math.pow(this._cfg.progressiveSlowdown, i)),
|
|
1921
|
+
holdMs: holdMs ??
|
|
1922
|
+
this.ramp(reels, this._cfg.holdMs, (base, i) => base + this._cfg.progressiveHoldMs * i),
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* A flat scalar when the progression is a no-op, else an array INDEXED BY REEL so the engine can
|
|
1927
|
+
* read a per-reel value straight out of `plan()`.
|
|
1928
|
+
*/
|
|
1929
|
+
ramp(reels, base, at) {
|
|
1930
|
+
if (at(base, 1) === base)
|
|
1931
|
+
return base;
|
|
1932
|
+
const out = [];
|
|
1933
|
+
reels.forEach((reel, i) => {
|
|
1934
|
+
out[reel] = at(base, i);
|
|
1935
|
+
});
|
|
1936
|
+
return out;
|
|
1825
1937
|
}
|
|
1826
1938
|
/** Optionally zoom the grid in while anticipating, then settle back. Returns a reset fn. */
|
|
1827
1939
|
async zoomIn(grid) {
|
|
@@ -2873,6 +2985,33 @@ function createReelSystem(opts) {
|
|
|
2873
2985
|
function ctx(freeSpins) {
|
|
2874
2986
|
return { grid, resolve, cfg: config, fx, board, freeSpins, log };
|
|
2875
2987
|
}
|
|
2988
|
+
/**
|
|
2989
|
+
* Which reels get the anticipation treatment for this spin. An explicit `anticipateReels` on the
|
|
2990
|
+
* run options WINS over the configured decision — passing it is how a game drives anticipation
|
|
2991
|
+
* from its own logic. Omit it and the configured `AnticipationController` decides.
|
|
2992
|
+
*/
|
|
2993
|
+
function resolveAnticipation(target, runOpts) {
|
|
2994
|
+
const explicit = runOpts?.anticipateReels;
|
|
2995
|
+
if (!explicit)
|
|
2996
|
+
return anticipation.decide(target);
|
|
2997
|
+
if (!explicit.length)
|
|
2998
|
+
return { active: false, reels: [], slowdown: 1, holdMs: 0 };
|
|
2999
|
+
return {
|
|
3000
|
+
active: true,
|
|
3001
|
+
reels: explicit.slice(),
|
|
3002
|
+
slowdown: runOpts?.anticipateSlowdown ?? config.anticipation.slowdownFactor,
|
|
3003
|
+
holdMs: runOpts?.anticipateHoldMs ?? config.anticipation.holdMs,
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
/** Fold a resolved decision back onto the caller's run options (everything else passes through). */
|
|
3007
|
+
function mergeAnticipation(runOpts, decision) {
|
|
3008
|
+
return {
|
|
3009
|
+
...runOpts,
|
|
3010
|
+
anticipateReels: decision.active ? decision.reels : undefined,
|
|
3011
|
+
anticipateSlowdown: decision.slowdown,
|
|
3012
|
+
anticipateHoldMs: decision.holdMs,
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
2876
3015
|
buildGrid();
|
|
2877
3016
|
const api = {
|
|
2878
3017
|
view,
|
|
@@ -2915,20 +3054,22 @@ function createReelSystem(opts) {
|
|
|
2915
3054
|
update(partial) {
|
|
2916
3055
|
api.setConfig(mergeReelConfig(config, partial));
|
|
2917
3056
|
},
|
|
3057
|
+
anticipationFor(target, runOpts) {
|
|
3058
|
+
return resolveAnticipation(target, runOpts);
|
|
3059
|
+
},
|
|
3060
|
+
planSpin(target, runOpts) {
|
|
3061
|
+
const decision = resolveAnticipation(target, runOpts);
|
|
3062
|
+
return spin.plan({ targetGrid: target }, mergeAnticipation(runOpts, decision));
|
|
3063
|
+
},
|
|
2918
3064
|
async spin(target, runOpts) {
|
|
2919
3065
|
const data = { targetGrid: target };
|
|
2920
|
-
const decision =
|
|
3066
|
+
const decision = resolveAnticipation(target, runOpts);
|
|
2921
3067
|
let resetZoom = null;
|
|
2922
3068
|
if (decision.active) {
|
|
2923
3069
|
log?.(`Anticipation on reels [${decision.reels.join(', ')}]`);
|
|
2924
3070
|
resetZoom = await anticipation.zoomIn(grid);
|
|
2925
3071
|
}
|
|
2926
|
-
await spin.run(data,
|
|
2927
|
-
...runOpts,
|
|
2928
|
-
anticipateReels: decision.active ? decision.reels : undefined,
|
|
2929
|
-
anticipateSlowdown: decision.slowdown,
|
|
2930
|
-
anticipateHoldMs: decision.holdMs,
|
|
2931
|
-
});
|
|
3072
|
+
await spin.run(data, mergeAnticipation(runOpts, decision));
|
|
2932
3073
|
if (resetZoom)
|
|
2933
3074
|
await resetZoom();
|
|
2934
3075
|
board = target;
|
|
@@ -3199,6 +3340,7 @@ exports.createReelSystem = createReelSystem;
|
|
|
3199
3340
|
exports.easingByName = easingByName;
|
|
3200
3341
|
exports.effectiveRowsPerReel = effectiveRowsPerReel;
|
|
3201
3342
|
exports.mergeReelConfig = mergeReelConfig;
|
|
3343
|
+
exports.perReelValue = perReelValue;
|
|
3202
3344
|
exports.pickTier = pickTier;
|
|
3203
3345
|
exports.resolveGeometry = resolveGeometry;
|
|
3204
3346
|
exports.resolveGridGeometry = resolveGridGeometry;
|