pixi-reels 0.6.0 → 1.0.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +239 -71
  2. package/README.md +29 -75
  3. package/dist/ReelSymbol-BxoiwQ-r.cjs +2 -0
  4. package/dist/ReelSymbol-BxoiwQ-r.cjs.map +1 -0
  5. package/dist/ReelSymbol-C9QQ30ER.js +82 -0
  6. package/dist/ReelSymbol-C9QQ30ER.js.map +1 -0
  7. package/dist/SpineSymbol-C_pacfc4.js +70 -0
  8. package/dist/SpineSymbol-C_pacfc4.js.map +1 -0
  9. package/dist/SpineSymbol-DAqBuFX2.cjs +2 -0
  10. package/dist/SpineSymbol-DAqBuFX2.cjs.map +1 -0
  11. package/dist/cascade/TumbleConfig.d.ts +25 -11
  12. package/dist/cascade/TumbleConfig.d.ts.map +1 -1
  13. package/dist/cascade/tumbleAlgorithm.d.ts +6 -6
  14. package/dist/cascade/tumbleAlgorithm.d.ts.map +1 -1
  15. package/dist/config/types.d.ts +59 -25
  16. package/dist/config/types.d.ts.map +1 -1
  17. package/dist/core/Reel.d.ts +205 -74
  18. package/dist/core/Reel.d.ts.map +1 -1
  19. package/dist/core/ReelMotion.d.ts +4 -4
  20. package/dist/core/ReelMotion.d.ts.map +1 -1
  21. package/dist/core/ReelSet.d.ts +322 -168
  22. package/dist/core/ReelSet.d.ts.map +1 -1
  23. package/dist/core/ReelSetBuilder.d.ts +61 -44
  24. package/dist/core/ReelSetBuilder.d.ts.map +1 -1
  25. package/dist/core/ReelViewport.d.ts +21 -14
  26. package/dist/core/ReelViewport.d.ts.map +1 -1
  27. package/dist/core/StopSequencer.d.ts +1 -1
  28. package/dist/debug/debug.d.ts +14 -5
  29. package/dist/debug/debug.d.ts.map +1 -1
  30. package/dist/debug-BeY8_ibp.js +2962 -0
  31. package/dist/debug-BeY8_ibp.js.map +1 -0
  32. package/dist/debug-DRm4gd_O.cjs +4 -0
  33. package/dist/debug-DRm4gd_O.cjs.map +1 -0
  34. package/dist/events/EventEmitter.d.ts +1 -0
  35. package/dist/events/EventEmitter.d.ts.map +1 -1
  36. package/dist/events/ReelEvents.d.ts +106 -37
  37. package/dist/events/ReelEvents.d.ts.map +1 -1
  38. package/dist/frame/ColumnTarget.d.ts +41 -56
  39. package/dist/frame/ColumnTarget.d.ts.map +1 -1
  40. package/dist/frame/RandomSymbolProvider.d.ts +9 -1
  41. package/dist/frame/RandomSymbolProvider.d.ts.map +1 -1
  42. package/dist/index.cjs +1 -4
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.ts +6 -22
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +26 -2745
  47. package/dist/index.js.map +1 -1
  48. package/dist/pins/CellPin.d.ts +18 -11
  49. package/dist/pins/CellPin.d.ts.map +1 -1
  50. package/dist/pool/ObjectPool.d.ts +2 -0
  51. package/dist/pool/ObjectPool.d.ts.map +1 -1
  52. package/dist/speed/SpeedManager.d.ts +2 -2
  53. package/dist/spin/SpinController.d.ts +50 -29
  54. package/dist/spin/SpinController.d.ts.map +1 -1
  55. package/dist/spin/modes/ImmediateMode.d.ts +1 -1
  56. package/dist/spin/modes/StandardMode.d.ts.map +1 -1
  57. package/dist/spin/phases/AdjustPhase.d.ts +3 -26
  58. package/dist/spin/phases/AdjustPhase.d.ts.map +1 -1
  59. package/dist/spin/phases/CascadeDropInPhase.d.ts +17 -6
  60. package/dist/spin/phases/CascadeDropInPhase.d.ts.map +1 -1
  61. package/dist/spin/phases/CascadeFallPhase.d.ts +14 -3
  62. package/dist/spin/phases/CascadeFallPhase.d.ts.map +1 -1
  63. package/dist/spin/phases/CascadePlacePhase.d.ts +1 -1
  64. package/dist/spin/phases/StopPhase.d.ts +3 -3
  65. package/dist/spin/phases/StopPhase.d.ts.map +1 -1
  66. package/dist/spine/SpineReelSymbol.d.ts +8 -10
  67. package/dist/spine/SpineReelSymbol.d.ts.map +1 -1
  68. package/dist/spine.cjs +1 -1
  69. package/dist/spine.cjs.map +1 -1
  70. package/dist/spine.js +2 -1
  71. package/dist/spine.js.map +1 -1
  72. package/dist/spotlight/SymbolSpotlight.d.ts +14 -3
  73. package/dist/spotlight/SymbolSpotlight.d.ts.map +1 -1
  74. package/dist/symbols/ReelSymbol.d.ts +17 -15
  75. package/dist/symbols/ReelSymbol.d.ts.map +1 -1
  76. package/dist/symbols/SpineSymbol.d.ts +15 -0
  77. package/dist/symbols/SpineSymbol.d.ts.map +1 -1
  78. package/dist/symbols/SymbolFactory.d.ts +4 -1
  79. package/dist/symbols/SymbolFactory.d.ts.map +1 -1
  80. package/dist/testing/testHarness.d.ts +26 -9
  81. package/dist/testing/testHarness.d.ts.map +1 -1
  82. package/dist/testing.cjs +3 -0
  83. package/dist/testing.cjs.map +1 -0
  84. package/dist/testing.js +132 -0
  85. package/dist/testing.js.map +1 -0
  86. package/dist/utils/gsapTicker.d.ts +38 -0
  87. package/dist/utils/gsapTicker.d.ts.map +1 -0
  88. package/dist/wins/WinPresenter.d.ts +6 -6
  89. package/package.json +10 -5
  90. package/dist/SpineSymbol-9NlrGsFv.js +0 -135
  91. package/dist/SpineSymbol-9NlrGsFv.js.map +0 -1
  92. package/dist/SpineSymbol-ojWlEPwt.cjs +0 -2
  93. package/dist/SpineSymbol-ojWlEPwt.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,12 +1,180 @@
1
1
  # pixi-reels
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Hide internal exports from the package entry: `OCCUPIED_SENTINEL`, `ReelSetInternalConfig`, `ResolvedReelGridConfig`, `OffsetCalculator`, `RandomSymbolProvider`, `SymbolFactory`, `StopSequencer`, and `ReelMotion`.
8
+
9
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Hide `SpinController`, `SpinControllerHooks`, and the built-in phase classes (`StartPhase`, `SpinPhase`, `StopPhase`, `AnticipationPhase`, `AdjustPhase`, `CascadeFallPhase`, `CascadePlacePhase`, `CascadeDropInPhase`) from the package entry — they are internal wiring. Register custom phases by extending `ReelPhase` and calling `builder.phases(f => f.register(...))`. Phase config TYPES (`StartPhaseConfig`, etc.) remain exported.
10
+
11
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Remove the `direction` option from `DestroySymbolsOptions` and `ReelSymbol.playDestroy()`. The default destroy is now a pure "poof" — a brief anticipation pop then a fast scale-to-0 + alpha-to-0 implode (~200 ms total, no rotation). Subclasses overriding `playDestroy` should drop the `direction` parameter from their signature.
12
+
13
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Remove the legacy `string[][]` form from `setResult` and `initialFrame`. Use the `ColumnTarget[]` shape, which survives `structuredClone` / JSON / `postMessage`.
14
+
15
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Remove negative-index slot mutation on result grids. Use `ColumnTarget.bufferAbove` and `ColumnTarget.bufferBelow` to target buffer cells.
16
+
17
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Remove the unused `symbol:recycled` event from `ReelEvents`.
18
+
19
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Remove `ReelSetBuilder.visibleSymbols()`. Use `.visibleRows()` instead.
20
+
21
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Rename internal-leaking methods on `Reel` / `ReelSet` to drop their leading underscore: `getAnchorRow`, `peekTargetShape`, `clearTargetShape`.
22
+
23
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Rename `ReelSet.skip()` to `ReelSet.skipSpin()` for symmetry with `skipNudge()`.
24
+
25
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Enable `stripInternal` in tsconfig: methods marked `@internal` are removed from the published `.d.ts` (`Reel.reshape`, `Reel.setStopFrame`, `Reel.setCrossReelResolver`, `Reel.getAnchorRow`, `Reel.notifySpinStart`, `Reel.notifySpinEnd`, `Reel.notifyLanded`, `Reel.snapToGrid`). The runtime methods still exist; only the type declarations are removed.
26
+
27
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Move the headless testing harness to a dedicated subpath: `import { createTestReelSet, FakeTicker, HeadlessSymbol, spinAndLand, captureEvents, expectGrid, countSymbol } from 'pixi-reels/testing'`. It is no longer re-exported from `pixi-reels`, so production bundles never pull it in.
28
+
29
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Replace the inline-options-object signature of `ReelSet.refill()` with a typed `RefillOptions` interface and a `RefillResult` return type that mirrors `RunCascadeResult`. Adds `signal: AbortSignal` for mid-refill cancellation. The result now exposes `winnersRefilled`, `finalGrid`, `wasSkipped`, and `duration` (previously the misnamed `SpinResult` shape).
30
+
31
+ ### Minor Changes
32
+
33
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `driveGsapWithTicker(ticker)` helper that pins GSAP to the PixiJS ticker (and returns a disposer that restores GSAP's own ticker). Encapsulates the one-line incantation every integration had to remember, so engine animations don't freeze in hidden tabs / iframes.
34
+
35
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: injectable `rng` on `ReelSetBuilder` (and `RandomSymbolProvider`), defaulting to `Math.random`. Regulated / provably-fair deployments can now inject a seeded, audited PRNG so the on-screen scrolling strip is reproducible from a seed for dispute resolution and frame-level regression.
36
+
37
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: the symbol recycle pool now auto-sizes its per-id capacity to the whole strip (every visible + buffer cell, floored at 20), eliminating destroy/recreate churn on large and MultiWays grids. A new `ReelSetBuilder.poolCapacity(n)` override is available for memory-constrained or unusually swap-heavy deployments.
38
+
39
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `SpinOptions.signal` (AbortSignal) and `SpinOptions.timeoutMs` (watchdog). A spin whose result never arrives can no longer hang forever — aborting the signal or exceeding the timeout rejects the `spin()` promise and force-stops the reels to a clean grid. `signal` rejects with `signal.reason` when it is an `Error`, so a failed/cancelled fetch propagates directly.
40
+
41
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `whenSpineReady()` resolves once the optional Spine import settles, so constructing `SpineSymbol`s on a cold start no longer throws a misleading "not installed" error before the dynamic import resolves (the constructor message now names that cause too). Adds an opt-in `SpineSymbolOptions.strict` that throws on an unmapped idle/win animation instead of silently showing nothing.
42
+
43
+ ### Patch Changes
44
+
45
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `enableDebug(reelSet, key?)` now registers each reel set under a per-instance key on `window.__PIXI_REELS_DEBUG_INSTANCES` instead of letting multiple reel sets clobber the single `window.__PIXI_REELS_DEBUG` global (which still points at the most recently enabled instance for convenience).
46
+
47
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `EventEmitter` no longer drops a persistent `on()` listener when the same handler reference is also registered via `once()`. `emit` now removes the fired `once` entry by identity instead of by `(fn, context)`, which previously deleted every listener sharing that function reference.
48
+
49
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `StandardMode.computeDeltaY` now clamps displacement symmetrically (±half a symbol). The upward step-back in `StartPhase` (and large frame deltas) previously moved more than one slot per tick, skipping `ReelMotion`'s single-wrap-per-call invariant and desyncing the symbol array from the view. `Reel.update` also clamps pathological `deltaMs` spikes (backgrounded-tab refocus, non-Pixi tickers).
50
+
51
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: the "nudge in flight" guard that blocks `spin()` / `setResult()` / `pin()` is now reference-counted. With parallel nudges across reels, the first to settle no longer clears the guard early and lets a later call race a still-live nudge (which could tear a frame or desync a pin).
52
+
53
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `ObjectPool` now guards against double-release (the same instance was pooled twice and then handed to two cells, silently aliasing one symbol) and against use after `destroy()` (`acquire` throws, `release` no-ops) so a late ticker/promise callback can't resurrect or leak the pool.
54
+
55
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: pin migration on a MultiWays reshape now resolves cell collisions deterministically. When two pins clamp onto the same row, the topmost keeps the cell and the other is expired (with `pin:expired` reason `'collision'`) and its overlay released — previously the second silently overwrote the first in the pin map and orphaned an overlay. Pin-overlay Y is also computed through a single helper so placement agrees across reshape.
56
+
57
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `Reel.destroy()` now emits `'destroyed'` before `removeAllListeners()` (so listeners actually receive it) and destroys each symbol's view instead of releasing live symbols back into the shared pool and then destroying their views out from under it (which handed a destroyed view to the next `acquire()`).
58
+
59
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `setResult` / `initialFrame` buffer-count validation now measures the highest defined index, not raw array length. A sparse `bufferAbove: ['X', undefined, undefined]` (common from serializers that pre-size arrays) no longer throws a spurious `RangeError`, while a defined entry beyond the consumable range still throws.
60
+
61
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `SymbolSpotlight.cycle()` now actually cycles. It previously aborted its own signal on the first line (because `show()` called `hide()`), flashing only the first win line for zero time and ignoring `displayDuration` / `gapDuration` / `cycles`. Teardown between lines is now separated from the cycle-abort, and `hide()` still interrupts a running cycle promptly.
62
+
63
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `StopPhase.onSkip()` now places the full target frame (buffers included) instead of slicing to the visible window. A direct `skip()` previously dropped `bufferAbove` / `bufferBelow` targets — e.g. a big symbol's tail parked above the visible area — and landed the wrong frame.
64
+
65
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `ReelViewport` dim overlay is now reference-counted. The spotlight and cascade `destroySymbols({ dim })` share one overlay; an overlapping pair no longer hides the dim out from under the other (flicker / lost dim in cascade+win sequences). The overlay hides only when the last consumer releases it.
66
+
67
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `RandomSymbolProvider` now fails loud instead of degrading silently — it throws on an empty symbol set or an all-zero total weight (which previously returned `undefined` or ignored weights), and `updateWeights()` drops exclusions referencing symbols no longer present so stale game-mode exclusions don't linger.
68
+
69
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: throw on a concurrent `spin()`, `setResult()`, `pin()`, or `setShape()` call while `nudge()` is in flight, instead of leaving the behavior undefined.
70
+
71
+ - [#140](https://github.com/schmooky/pixi-reels/pull/140) [`d7dfc9d`](https://github.com/schmooky/pixi-reels/commit/d7dfc9d76d3d6d9df1a0e0a93d1c966ecbd29d93) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Perf: the main entry is now under 5 KB gzipped (down from ~20.8 KB) after hiding `SpinController` + the built-in phase classes and moving the testing harness to the `pixi-reels/testing` subpath.
72
+
73
+ ## 0.9.0
74
+
75
+ ### Minor Changes
76
+
77
+ - [#138](https://github.com/schmooky/pixi-reels/pull/138) [`2728db7`](https://github.com/schmooky/pixi-reels/commit/2728db7db37e231649fc91711511da788cc0d073) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: big-symbol anchors can now sit in bufferAbove or bufferBelow. The classic UK fruit-machine landing. a 1xH wild lands with most of it hidden above the visible window, only the bottom cell ("the tail") shows at row 0. works end-to-end through `setResult`, `refill`, and `nudge`.
78
+
79
+ `_coordinateBigSymbols` now iterates the full strip range (`-bufferAbove` to `visibleRows + bufferBelow`) and validates against strip capacity instead of just visible. Anchors at any strip slot are accepted as long as the block fits end-to-end. Pass an anchor at `bufferAbove[i]` via the explicit `ColumnTarget` form (`{ visible: [...], bufferAbove: [...] }`) or via the legacy `frame[col][-1]` negative-index form; the coordinator paints OCCUPIED stubs at the rest of the block's cells (in buffer, visible, or buffer-below as needed).
80
+
81
+ The validation error message changed: `exceeds reel height` was visible-only; now reads `extends past the bottom of the strip` with the exact computed values. The new check is more permissive. a 1x4 block on a 3-visible-row reel with 1 bufferBelow is now LEGAL where it previously threw.
82
+
83
+ `getSymbolFootprint` may return a negative `anchor.row` for blocks anchored in bufferAbove. `getBlockBounds` handles this by computing pixel coordinates from the row offset directly rather than delegating to `getCellBounds` (which still rejects negative rows). Consumers reading `anchor.row` should accept negative values.
84
+
85
+ Fix: `ReelMotion._maxY` was hard-coded to `(visibleRows + 1) * slotH`, which collapsed to `strip[last].y` exactly when `bufferBelow >= 2` and fired a phantom wrap on the first nudge displacement. the anchor landed one strip slot too far. The threshold now scales with `bufferBelow` (`maxY = (visibleRows + bufferBelow) * slotH`), symmetric with the existing `minY = -(bufferAbove + 1) * slotH`. Nudges with `bufferBelow >= 2` now match the documented survival math.
86
+
87
+ Live recipes: `/recipes/big-symbol-partial-land/`, `/recipes/big-symbol-held-respin/`.
88
+
89
+ ### Patch Changes
90
+
91
+ - [#138](https://github.com/schmooky/pixi-reels/pull/138) [`2728db7`](https://github.com/schmooky/pixi-reels/commit/2728db7db37e231649fc91711511da788cc0d073) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Internal: sharpen comments around the big-symbol coordinator's
92
+ uniform-buffer assumption and `_finalizeFrame`'s scan asymmetry. both
93
+ were silently load-bearing on contracts that weren't spelled out.
94
+ Also extends `ColumnTarget.bufferAbove` / `bufferBelow` JSDoc to
95
+ explicitly document the big-symbol anchor capability. discoverable
96
+ in IDE tooltips. No runtime change.
97
+
98
+ - [#138](https://github.com/schmooky/pixi-reels/pull/138) [`2728db7`](https://github.com/schmooky/pixi-reels/commit/2728db7db37e231649fc91711511da788cc0d073) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `ReelSet.setResult` and `ReelSetBuilder.initialFrame` now throw a `RangeError` when a `ColumnTarget.bufferAbove` / `bufferBelow` carries more entries than the engine's configured `bufferSymbols(...)`, instead of silently dropping the extras.
99
+
100
+ Previously, calling `.bufferSymbols(1)` and passing `bufferAbove: ['X', 'Y']` would materialize both `arr[at -1] set to 'X'` and `arr[at -2] set to 'Y'`, but the next clone (`cloneColumn`) only iterates `-1..-bufferAbove`. `Y` was written to the array, dropped on the next pass, and never reached the reel. No error, no warning; the only symptom was "my targeted symbol never lands." Same problem on the `bufferBelow` side via indices past `visible + bufferBelow`.
101
+
102
+ The check now fails fast at the API entry point with a column-pointing message: `setResult column 2: bufferAbove has 2 entries but engine bufferSymbols=1. extra entries would be silently dropped. Increase bufferSymbols(...) on the builder or remove the extra entries.` The legacy `frame[col][-k]` form is also validated for negative-index keys beyond `-bufferAbove`. The legacy form's array `length` is intentionally not checked. in MultiWays the per-reel `visibleRows` changes between `setShape()` and `setResult()`, and any length-based check would false-positive on legitimate post-reshape calls.
103
+
104
+ This is user-visible error behavior: input that previously silently failed now throws. Callers passing more entries than the configured buffer size should either increase `bufferSymbols(...)` or trim the extra entries.
105
+
106
+ ## 0.8.0
107
+
108
+ ### Minor Changes
109
+
110
+ - [#136](https://github.com/schmooky/pixi-reels/pull/136) [`743e73d`](https://github.com/schmooky/pixi-reels/commit/743e73de64bb7e02e6142ed284ccd569e03bc555) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `ReelSet.nudge(col, options)`. shift a single reel by N positions after it has landed, revealing caller-supplied `incoming` symbols. The classic UK fruit-machine nudge.
111
+
112
+ API surface includes:
113
+
114
+ - `NudgeOptions.distance` / `.direction` / `.incoming`. required; `incoming` is top-down by FINAL on-strip position (overflow lands in the matching off-screen buffer).
115
+ - `NudgeOptions.duration` / `.ease`. default `'power2.out'`; overshooting eases are clamped so wraps never fire past the landing position.
116
+ - `NudgeOptions.startDelay`. defer the tween for staggered `Promise.all` waves.
117
+ - `NudgeOptions.signal: AbortSignal`. cancel mid-tween; strip still snaps to landed; promise rejects with `AbortError` and `nudge:cancelled` fires.
118
+ - `ReelSet.skipNudge(col?)` / `Reel.skipNudge()`. fast-forward an in-flight tween; `nudge()` resolves normally.
119
+ - Events: `nudge:start` (after pre-placement), `nudge:complete`, `nudge:cancelled` on the reel-set bus; `phase:enter('nudge')` / `phase:exit('nudge')` per-reel.
120
+
121
+ Big-symbol blocks on the target reel are nudged through as a unit when the rotation preserves the block:
122
+
123
+ - down: `anchor + h - 1 + distance < total` (block may extend into bufferBelow)
124
+ - up: `anchor - distance >= bufferAbove` (anchor must land in visible. engine doesn't render bufferAbove anchors today)
125
+
126
+ Cross-reel blocks (`w > 1`) throw. splitting an anchor from its other-reel cells isn't safe under a single-reel nudge.
127
+
128
+ Also fixes `ReelMotion._wrapTopToBottom` to use a symmetric `<= minY` boundary check (previously strict `< minY`, so an upward shift that landed exactly on the threshold no-op'd silently. exposed by `nudge` since standard spinning only moves downward).
129
+
130
+ ## 0.7.0
131
+
132
+ ### Minor Changes
133
+
134
+ - [#133](https://github.com/schmooky/pixi-reels/pull/133) [`fbe6ac0`](https://github.com/schmooky/pixi-reels/commit/fbe6ac0ed24abdc3d5193dfef455833b7ecb75f3) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: speed-scoped tumble overrides + AbortSignal on cascade symbol events.
135
+
136
+ `SpeedProfile` now accepts an optional `tumble?: TumbleConfig` field. When the active speed profile defines one, the cascade fall + drop-in phases merge its fields over the base config registered via `.tumble(...)`. so `setSpeed('turbo')` can shorten `fall.duration`, `dropIn.duration`, and per-row staggers, not just the per-reel `stopDelay`. Profiles without a `tumble` field behave identically to before.
137
+
138
+ ```ts
139
+ .tumble({ fall: { duration: 300 }, dropIn: { duration: 600, rowStagger: 60 } })
140
+ .speed('default', SPEED_DEFAULT)
141
+ .speed('turbo', {
142
+ ...SPEED_TURBO,
143
+ tumble: {
144
+ fall: { duration: 120 },
145
+ dropIn: { duration: 220, rowStagger: 20 },
146
+ },
147
+ })
148
+ .speed('snap', { ...SPEED_TURBO, tumble: { fall: { duration: 0 }, dropIn: { duration: 0 } } })
149
+ ```
150
+
151
+ `cascade:fall:symbol`, `cascade:dropIn:symbol`, and `cascade:gravity:symbol` now carry a `signal: AbortSignal` field. The signal aborts when the phase is skipped / slammed; listeners that schedule parallel tweens (squish, bounce, badge animations) can register a one-shot cleanup so a slam-stop kills their work alongside the library's own timeline. The signal stays un-aborted on natural completion. only explicit skips trigger it.
152
+
153
+ ```ts
154
+ events.on("cascade:dropIn:symbol", ({ view, duration, signal }) => {
155
+ const t = gsap.to(view.scale, {
156
+ x: 1.15,
157
+ y: 0.78,
158
+ duration: duration / 1000,
159
+ });
160
+ signal.addEventListener(
161
+ "abort",
162
+ () => {
163
+ t.kill();
164
+ view.scale.set(1, 1);
165
+ },
166
+ { once: true }
167
+ );
168
+ });
169
+ ```
170
+
3
171
  ## 0.6.0
4
172
 
5
173
  ### Minor Changes
6
174
 
7
175
  - [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: two-stage cascade refill (gravity → hold → drop-in) for tumble slots that want an anticipation beat between survivors landing and new symbols entering.
8
176
 
9
- The default refill animates survivors and new symbols together in one beat (the Sweet Bonanza / Sugar Rush feel). A handful of slots split it in two: survivors slide first, a global beat for anticipation visuals (multiplier roll, mascot react, SFX peak), then new symbols enter often staggered per column. That flavor is now first-class.
177
+ The default refill animates survivors and new symbols together in one beat (the Sweet Bonanza / Sugar Rush feel). A handful of slots split it in two: survivors slide first, a global beat for anticipation visuals (multiplier roll, mascot react, SFX peak), then new symbols enter. often staggered per column. That flavor is now first-class.
10
178
 
11
179
  Opt in via `mode: 'gravity-then-drop'` on `refill()` (or `refillMode: 'gravity-then-drop'` on `runCascade()`):
12
180
 
@@ -24,30 +192,30 @@
24
192
 
25
193
  New options:
26
194
 
27
- - `refill({ mode })` `'combined'` (default, unchanged) or `'gravity-then-drop'`.
28
- - `refill({ gravityHoldMs })` global pause between gravity end and drop-in start. Default `250`.
29
- - `refill({ onGravityComplete })` awaitable hook between stages; extends the hold for async work (multiplier count-ups, etc.).
30
- - `runCascade({ refillMode, gravityHoldMs, onGravityComplete })` same options forwarded into every refill in the chain. The hook receives `{ chain, winners }`.
195
+ - `refill({ mode })`. `'combined'` (default, unchanged) or `'gravity-then-drop'`.
196
+ - `refill({ gravityHoldMs })`. global pause between gravity end and drop-in start. Default `250`.
197
+ - `refill({ onGravityComplete })`. awaitable hook between stages; extends the hold for async work (multiplier count-ups, etc.).
198
+ - `runCascade({ refillMode, gravityHoldMs, onGravityComplete })`. same options forwarded into every refill in the chain. The hook receives `{ chain, winners }`.
31
199
 
32
200
  New events:
33
201
 
34
- - `cascade:gravity:start` `{ reelIndex }`. A reel's gravity stage begins.
35
- - `cascade:gravity:symbol` same shape as `cascade:dropIn:symbol`, scoped to survivors.
36
- - `cascade:gravity:end` `{ reelIndex }`. A reel's gravity stage settled.
202
+ - `cascade:gravity:start`. `{ reelIndex }`. A reel's gravity stage begins.
203
+ - `cascade:gravity:symbol`. same shape as `cascade:dropIn:symbol`, scoped to survivors.
204
+ - `cascade:gravity:end`. `{ reelIndex }`. A reel's gravity stage settled.
37
205
 
38
- These fire only in two-stage mode; combined mode is unchanged. Per-column stagger inside the drop-in stage uses the existing `setDropOrder('ltr', stepMs)` `step < dropIn.duration` gives an overlapping wave, `step >= dropIn.duration` gives strictly sequential columns. The gravity stage always runs all reels in parallel.
206
+ These fire only in two-stage mode; combined mode is unchanged. Per-column stagger inside the drop-in stage uses the existing `setDropOrder('ltr', stepMs)`. `step < dropIn.duration` gives an overlapping wave, `step >= dropIn.duration` gives strictly sequential columns. The gravity stage always runs all reels in parallel.
39
207
 
40
208
  See the [Cascade anticipation refill recipe](https://pixi-reels.com/recipes/tumble-anticipation/) for a live example.
41
209
 
42
210
  - [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Cascade DX pass: collapse ~30 lines of slot orchestration to ~3 with a canonical detect → destroy → refill chain, retire the legacy `examples/shared/cascadeLoop.ts` helper, and align every recipe / example / doc onto the new API.
43
211
 
44
- **`reelSet.destroySymbols(cells, opts?)`** the canonical "fade out winners" step. Defers to each symbol's `playDestroy()` so subclasses (Spine, particles) get art-appropriate disintegration without the spin handler caring. Bumps each view's zIndex so destroys aren't clipped, alternates rotation by column for cohesive cluster pops, optional viewport dim. Replaces ~10 lines of duplicated `destroyWinners` helpers in every cascade recipe.
212
+ **`reelSet.destroySymbols(cells, opts?)`**. the canonical "fade out winners" step. Defers to each symbol's `playDestroy()` so subclasses (Spine, particles) get art-appropriate disintegration without the spin handler caring. Bumps each view's zIndex so destroys aren't clipped, alternates rotation by column for cohesive cluster pops, optional viewport dim. Replaces ~10 lines of duplicated `destroyWinners` helpers in every cascade recipe.
45
213
 
46
- **`reelSet.runCascade({ detectWinners, nextGrid, onCascade?, pauseAfterDestroyMs?, maxChain?, destroyOptions?, signal? })`** the canonical cascade chain orchestration. Loops detect → destroy → pause → refill until `detectWinners` returns `[]`. Caller supplies the game-rules callbacks; the library owns the timing. Both callbacks may be `async`. Pass `signal: AbortSignal` for caller-driven cancellation (the right shape for "player tapped slam between refills," where `reelSet.skip()` is a no-op because the engine is idle). The awaited `RunCascadeResult` (`{ chainLength, totalWinners, finalGrid, wasSkipped }`) is the canonical "the chain is over" signal no separate event for that, since "round" is a slot-UX term (bet→payout) rather than a reel-engine one and the engine-level "press-spin → all-stopped" is already covered by `spin:start` / `spin:allLanded`.
214
+ **`reelSet.runCascade({ detectWinners, nextGrid, onCascade?, pauseAfterDestroyMs?, maxChain?, destroyOptions?, signal? })`**. the canonical cascade chain orchestration. Loops detect → destroy → pause → refill until `detectWinners` returns `[]`. Caller supplies the game-rules callbacks; the library owns the timing. Both callbacks may be `async`. Pass `signal: AbortSignal` for caller-driven cancellation (the right shape for "player tapped slam between refills," where `reelSet.skip()` is a no-op because the engine is idle). The awaited `RunCascadeResult` (`{ chainLength, totalWinners, finalGrid, wasSkipped }`) is the canonical "the chain is over" signal. no separate event for that, since "round" is a slot-UX term (bet→payout) rather than a reel-engine one and the engine-level "press-spin → all-stopped" is already covered by `spin:start` / `spin:allLanded`.
47
215
 
48
216
  **`cascade:place:end`** payload now includes `isInitial: boolean` and `winnerRows: readonly number[]` so decoration listeners can tell new arrivals from survivors sliding into a hole.
49
217
 
50
- Also exports the named option / result types `DestroySymbolsOptions`, `RunCascadeOptions`, `RunCascadeResult` so apps can pass typed config objects around or extend them in adapter layers.
218
+ Also exports the named option / result types. `DestroySymbolsOptions`, `RunCascadeOptions`, `RunCascadeResult`. so apps can pass typed config objects around or extend them in adapter layers.
51
219
 
52
220
  Non-breaking for the library API. Removed the legacy `examples/shared/cascadeLoop.ts` helper (`runCascade(reelSet, stages, opts)`, `tumbleToGrid`, `diffCells`) since every recipe + example + integration test has been migrated to the new `reelSet.runCascade` / `reelSet.destroySymbols` / `reelSet.refill` surface. Site recipes (`cascade-6x5`, `spin-then-cascade`, `multiways-cascade`, `cascade-winpresenter`, `remove-symbol`) and React recipe components (`RemoveSymbolRecipe`, `CascadeStarterRecipe`) all use the new API; the `cascade-tumble` and `pyramid-cascade` examples were rewritten the same way.
53
221
 
@@ -57,22 +225,22 @@
57
225
 
58
226
  New events on `reelSet.events`:
59
227
 
60
- - `cascade:chain:start` `{ chain, winners, currentGrid }`. Fired inside `runCascade(...)` after `detectWinners` returns winners, before `destroySymbols` runs. `chain` is 1-indexed.
61
- - `cascade:chain:end` `{ chain, winners, nextGrid }`. Mirror of `chain:start` fired after the refill drop-in settles, before the loop iterates to the next `detectWinners`.
62
- - `cascade:destroy:start` / `cascade:destroy:end` `{ cells }`. Fired around every `destroySymbols(...)` call (both direct and inside `runCascade`). Empty-batch calls do not emit. Use these to cue a shatter SFX, dim a HUD, or capture pre-destroy grids for replay logging without overriding the cascade loop.
228
+ - `cascade:chain:start`. `{ chain, winners, currentGrid }`. Fired inside `runCascade(...)` after `detectWinners` returns winners, before `destroySymbols` runs. `chain` is 1-indexed.
229
+ - `cascade:chain:end`. `{ chain, winners, nextGrid }`. Mirror of `chain:start`. fired after the refill drop-in settles, before the loop iterates to the next `detectWinners`.
230
+ - `cascade:destroy:start` / `cascade:destroy:end`. `{ cells }`. Fired around every `destroySymbols(...)` call (both direct and inside `runCascade`). Empty-batch calls do not emit. Use these to cue a shatter SFX, dim a HUD, or capture pre-destroy grids for replay logging. without overriding the cascade loop.
63
231
 
64
232
  Event ordering per `runCascade()` call (per stage with winners):
65
233
 
66
234
  `cascade:chain:start` → `cascade:destroy:start` → (destroy tweens) → `cascade:destroy:end` → `onCascade` callback → pause → refill (`cascade:place:end` + `cascade:dropIn:*` per reel) → `cascade:chain:end`
67
235
 
68
- The runCascade chain itself is delimited by the returned `Promise` `await` the call to know when it's done and read the `RunCascadeResult` summary. There is intentionally no `cascade:round:*` event pair: "round" in slot UX is a bet→payout transaction (your concern, not the engine's), and the engine-level "press-spin → all-stopped" is already covered by `spin:start` / `spin:allLanded`.
236
+ The runCascade chain itself is delimited by the returned `Promise`. `await` the call to know when it's done and read the `RunCascadeResult` summary. There is intentionally no `cascade:round:*` event pair: "round" in slot UX is a bet→payout transaction (your concern, not the engine's), and the engine-level "press-spin → all-stopped" is already covered by `spin:start` / `spin:allLanded`.
69
237
 
70
238
  Every cascade event uses a consistent three-part `cascade:<scope>:<step>` taxonomy.
71
239
 
72
240
  - [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add `gravityHold: Promise<void>` to `refill()` and `runCascade()` so callers can gate the drop-in stage on an already-in-flight animation / SFX / network call without wrapping it in a callback.
73
241
 
74
242
  ```ts
75
- // Single refill pass the promise directly.
243
+ // Single refill. pass the promise directly.
76
244
  await reelSet.refill({
77
245
  winners,
78
246
  grid: next,
@@ -82,10 +250,10 @@
82
250
  });
83
251
  ```
84
252
 
85
- `gravityHoldMs` and `gravityHold` race in **parallel** via `Promise.all` whichever finishes LAST gates the drop-in. Pass both when you want a wall-clock floor under an animation that might finish quickly. `onGravityComplete` (the existing callback hook) still runs AFTER both resolve, so it can read post-hold state.
253
+ `gravityHoldMs` and `gravityHold` race in **parallel** via `Promise.all`. whichever finishes LAST gates the drop-in. Pass both when you want a wall-clock floor under an animation that might finish quickly. `onGravityComplete` (the existing callback hook) still runs AFTER both resolve, so it can read post-hold state.
86
254
 
87
255
  ```ts
88
- // Per-cascade runCascade calls the builder once per stage.
256
+ // Per-cascade. runCascade calls the builder once per stage.
89
257
  await reelSet.runCascade({
90
258
  detectWinners,
91
259
  nextGrid,
@@ -104,30 +272,30 @@
104
272
 
105
273
  `ReelSet.skip()` is now round-aware. A "round" is one `spin()` plus all its `refill()`s, until the next `spin()`. The first press of `skip()` in a round slams the current drop AND applies a round-scoped side effect:
106
274
 
107
- - **Standard mode**: boosts the active speed profile to the fastest registered one (emits `skip:boosted`). The speed takes effect on the NEXT spin (mid-spin speed switching is not supported by phases). Boost persists across `refill()` calls and is restored on the next `spin()` unless the app changed speed manually between rounds, in which case the manual choice is preserved.
275
+ - **Standard mode**: boosts the active speed profile to the fastest registered one (emits `skip:boosted`). The speed takes effect on the NEXT spin (mid-spin speed switching is not supported by phases). Boost persists across `refill()` calls and is restored on the next `spin()`. unless the app changed speed manually between rounds, in which case the manual choice is preserved.
108
276
  - **Cascade/tumble mode**: flags the round so every subsequent `refill()` auto-slams with no animation. One press ends a multi-drop cascade.
109
277
 
110
278
  Subsequent `skip()` presses in the same round each slam the current drop. The universal `if (isSpinning) reelSet.skip()` button pattern across recipes now always lands the spin on a single press, while still benefiting from the boost / auto-slam side effect.
111
279
 
112
280
  Breaking:
113
281
 
114
- - `skip()` no longer needs two presses to slam single press lands the drop. Callers that already relied on `skip()` slamming work as before. Callers expecting a _non-slamming_ "boost only" press should use `reelSet.setSpeed('superTurbo')` directly.
115
- - `skip()` THROWS if called before `setResult()` arrives (no result to land on pre-result slam would land on random spin-buffer state). Use `requestSkip()` for the deferred-slam pattern, or wrap `skip()` in `try { ... } catch {}` and route to `requestSkip()` in the catch. Refill paths take a result at entry, so this guard only fires in the initial-spin pre-`setResult` window.
282
+ - `skip()` no longer needs two presses to slam. single press lands the drop. Callers that already relied on `skip()` slamming work as before. Callers expecting a _non-slamming_ "boost only" press should use `reelSet.setSpeed('superTurbo')` directly.
283
+ - `skip()` THROWS if called before `setResult()` arrives (no result to land on. pre-result slam would land on random spin-buffer state). Use `requestSkip()` for the deferred-slam pattern, or wrap `skip()` in `try { ... } catch {}` and route to `requestSkip()` in the catch. Refill paths take a result at entry, so this guard only fires in the initial-spin pre-`setResult` window.
116
284
  - `requestSkip()` bypasses staging entirely and slams when `setResult()` arrives.
117
285
  - The test harness `spinAndLand()` was migrated to `slamStop()` to keep its semantics explicit.
118
286
 
119
287
  Added:
120
288
 
121
- - `ReelSet.slamStop()` always slams, no side effects.
122
- - `ReelSet.skipStage` `0 | 1 | 2` getter; `0` until the first press, `2` after. (`1` is reserved for forward compat.)
123
- - `skip:boosted` event `{ previous, current }: SpeedProfile`. Fires only on standard-mode boost; cascade auto-slam doesn't emit it.
124
- - `ReelSymbol.playDestroy(opts?)` `opts.direction: 1 | -1` for coherent rotation (e.g. `w.reel % 2 === 0 ? 1 : -1`), `opts.delay: number` (seconds) for per-winner stagger, and `opts.signal: AbortSignal` so a mid-destroy abort can snap to the destroyed pose without waiting for the full ~300 ms tween. Default direction stays random for back-compat.
289
+ - `ReelSet.slamStop()`. always slams, no side effects.
290
+ - `ReelSet.skipStage`. `0 | 1 | 2` getter; `0` until the first press, `2` after. (`1` is reserved for forward compat.)
291
+ - `skip:boosted` event. `{ previous, current }: SpeedProfile`. Fires only on standard-mode boost; cascade auto-slam doesn't emit it.
292
+ - `ReelSymbol.playDestroy(opts?)`. `opts.direction: 1 | -1` for coherent rotation (e.g. `w.reel % 2 === 0 ? 1 : -1`), `opts.delay: number` (seconds) for per-winner stagger, and `opts.signal: AbortSignal` so a mid-destroy abort can snap to the destroyed pose without waiting for the full ~300 ms tween. Default direction stays random for back-compat.
125
293
 
126
294
  - [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Replace `.cascade()` with `.tumble()` and split cascade-drop into three independently overridable phases.
127
295
 
128
296
  Breaking changes: `.cascade(DropRecipes...)` is removed. `DropRecipes`, `DropStartPhase`, `DropStopPhase`, `CascadeAnticipationPhase`, and their `*Config` types no longer export from `pixi-reels`. Use `.tumble({ fall, dropIn })` on the builder and override individual phases via `.phases(f => f.register('cascade:fall'|'cascade:place'|'cascade:dropIn', MyPhase))`.
129
297
 
130
- New: `reelSet.refill({ winners, grid })` for Moment B cascade refills. Gravity-correct geometry untouched survivors stay, survivors above a hole slide down, new symbols enter from above into the top `winners.length` rows. Per-symbol `cascade:fall:symbol` / `cascade:dropIn:symbol` events fire right before each tween so listeners can run parallel tweens on any view property in sync with the library's motion. Per-reel boundary events: `cascade:fall:start` / `cascade:fall:end` / `cascade:place:end` / `cascade:dropIn:start` / `cascade:dropIn:end`.
298
+ New: `reelSet.refill({ winners, grid })` for Moment B cascade refills. Gravity-correct geometry. untouched survivors stay, survivors above a hole slide down, new symbols enter from above into the top `winners.length` rows. Per-symbol `cascade:fall:symbol` / `cascade:dropIn:symbol` events fire right before each tween so listeners can run parallel tweens on any view property in sync with the library's motion. Per-reel boundary events: `cascade:fall:start` / `cascade:fall:end` / `cascade:place:end` / `cascade:dropIn:start` / `cascade:dropIn:end`.
131
299
 
132
300
  See `docs/recipes/tumble-cascade.md` for the full recipe (drop-on-click, server wait with spinner, cascading multiplier).
133
301
 
@@ -137,11 +305,11 @@
137
305
 
138
306
  - `CascadeFallPhase` / `CascadeDropInPhase` now emit their `:end` events on skip. Previously a slam mid-fall (or mid-drop, mid-gravity) killed the timeline without firing the paired `cascade:fall:end` / `cascade:dropIn:end` / `cascade:gravity:end`, so any HUD or audio bus pairing `:start` / `:end` to track in-flight cascade work drifted out of balance on every slam. The pre-fall delay window (where `:start` has not yet fired) still skips silently, so no unpaired `:end` is emitted.
139
307
 
140
- - `runCascade({ gravityHold })` now invokes the per-cascade builder at the **gravity-end boundary** as documented, not at refill-start. Side effects in the builder (e.g. `multiplier.bumpTo(chain + 1); return multiplier.done`) now line up with the gravity-end beat the player sees. To support this, `refill({ gravityHold })` accepts a factory `() => Promise<void>` in addition to a bare `Promise<void>` pass a factory when the side effect of starting the promise should fire at gravity-end; pass a bare promise when you already hold an in-flight handle.
308
+ - `runCascade({ gravityHold })` now invokes the per-cascade builder at the **gravity-end boundary** as documented, not at refill-start. Side effects in the builder (e.g. `multiplier.bumpTo(chain + 1); return multiplier.done`) now line up with the gravity-end beat the player sees. To support this, `refill({ gravityHold })` accepts a factory `() => Promise<void>` in addition to a bare `Promise<void>`. pass a factory when the side effect of starting the promise should fire at gravity-end; pass a bare promise when you already hold an in-flight handle.
141
309
 
142
- - `runCascade({ pauseAfterDestroyMs })` wait is now cancellable via `signal`. Previously an abort during the pause ran the setTimeout to completion before the loop exited up to `pauseAfterDestroyMs` of dead air between slam intent and exit. Now the wait races against `signal.aborted` and unblocks within a microtask.
310
+ - `runCascade({ pauseAfterDestroyMs })` wait is now cancellable via `signal`. Previously an abort during the pause ran the setTimeout to completion before the loop exited. up to `pauseAfterDestroyMs` of dead air between slam intent and exit. Now the wait races against `signal.aborted` and unblocks within a microtask.
143
311
 
144
- - A new `cascade:gravity:error` event surfaces user-supplied `gravityHold` / `onGravityComplete` rejections (or throws). The engine still slams to recover so the refill promise settles, but the original rejection reason is no longer silently swallowed listen on the event to forward the error to your own logger / alarm. The console.error log was also tightened to identify the likely culprit.
312
+ - A new `cascade:gravity:error` event surfaces user-supplied `gravityHold` / `onGravityComplete` rejections (or throws). The engine still slams to recover so the refill promise settles, but the original rejection reason is no longer silently swallowed. listen on the event to forward the error to your own logger / alarm. The console.error log was also tightened to identify the likely culprit.
145
313
 
146
314
  - `movePin` `onFlightCreated` / `onFlightCompleted` hook throws now log via `console.error` instead of being silently swallowed. The animation still continues (a throwing hook MUST NOT leak a flight symbol or leave the pin map out of sync) but the bug is no longer invisible.
147
315
 
@@ -151,9 +319,9 @@
151
319
 
152
320
  ### Minor Changes
153
321
 
154
- - [#111](https://github.com/schmooky/pixi-reels/pull/111) [`dc2a526`](https://github.com/schmooky/pixi-reels/commit/dc2a526cf13c8670d10680f9104b93675332468f) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: cascade + multiways combination. `ReelSetBuilder.multiways(...)` can now be paired with `.cascade(...)` or `spinningMode(new CascadeMode())` the build-time throw added in ADR 012 is lifted. `AdjustPhase` runs between `SpinPhase` and `DropStopPhase` so the new shape commits before the drop-in fills it. Shape changes apply per-spin only; mid-cascade-chain reshape is unsupported (see ADR 015). Closes [#74](https://github.com/schmooky/pixi-reels/issues/74).
322
+ - [#111](https://github.com/schmooky/pixi-reels/pull/111) [`dc2a526`](https://github.com/schmooky/pixi-reels/commit/dc2a526cf13c8670d10680f9104b93675332468f) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: cascade + multiways combination. `ReelSetBuilder.multiways(...)` can now be paired with `.cascade(...)` or `spinningMode(new CascadeMode())`. the build-time throw added in ADR 012 is lifted. `AdjustPhase` runs between `SpinPhase` and `DropStopPhase` so the new shape commits before the drop-in fills it. Shape changes apply per-spin only; mid-cascade-chain reshape is unsupported (see ADR 015). Closes [#74](https://github.com/schmooky/pixi-reels/issues/74).
155
323
 
156
- - [#116](https://github.com/schmooky/pixi-reels/pull/116) [`7afe3a9`](https://github.com/schmooky/pixi-reels/commit/7afe3a9a6edd70aaab4c985fb0167050e93fbd49) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `ColumnTarget` explicit `{ visible, bufferAbove?, bufferBelow? }` input shape. Accepted by both `ReelSet.setResult` and `ReelSetBuilder.initialFrame` alongside the legacy `string[][]` form. Survives `structuredClone`, JSON, and `postMessage` (the legacy negative-index form does not).
324
+ - [#116](https://github.com/schmooky/pixi-reels/pull/116) [`7afe3a9`](https://github.com/schmooky/pixi-reels/commit/7afe3a9a6edd70aaab4c985fb0167050e93fbd49) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `ColumnTarget`. explicit `{ visible, bufferAbove?, bufferBelow? }` input shape. Accepted by both `ReelSet.setResult` and `ReelSetBuilder.initialFrame` alongside the legacy `string[][]` form. Survives `structuredClone`, JSON, and `postMessage` (the legacy negative-index form does not).
157
325
 
158
326
  Fix: `setResult` (legacy `string[][]` form) now honours `frame[col][-1]…[-bufferAbove]` end-to-end. Previously the negative-index slots were dropped inside `_applyPinsToGrid` (when pins were active) and `_coordinateBigSymbols` (always) by plain spread clones, so the convention only worked through `initialFrame`. The clones now use a property-preserving helper.
159
327
 
@@ -169,7 +337,7 @@
169
337
 
170
338
  - [#98](https://github.com/schmooky/pixi-reels/pull/98) [`b4bacca`](https://github.com/schmooky/pixi-reels/commit/b4bacca9bac5aa6048ca9d5062de8ef1e04aeeea) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Auto-pick `SharedRectMaskStrategy` when any registered symbol has `unmask: true` and `symbolGap.x > 0`.
171
339
 
172
- The default `RectMaskStrategy` draws one mask rect per reel, with the gaps between reels NOT clipped fine in the common case. But when an `unmask: true` symbol renders above the reel mask, neighboring (still-masked) symbols on adjacent reels visibly clip at the column gap, and players see a half-cropped neighbor next to the unmasked overlay.
340
+ The default `RectMaskStrategy` draws one mask rect per reel, with the gaps between reels NOT clipped. fine in the common case. But when an `unmask: true` symbol renders above the reel mask, neighboring (still-masked) symbols on adjacent reels visibly clip at the column gap, and players see a half-cropped neighbor next to the unmasked overlay.
173
341
 
174
342
  The auto-pick now triggers in either case:
175
343
 
@@ -178,13 +346,13 @@
178
346
 
179
347
  provided the layout has a horizontal gap (`symbolGap.x > 0`). Explicit `.maskStrategy(...)` calls always win.
180
348
 
181
- Console emits a one-line `console.info` hint identifying which condition triggered the auto-pick. Pairs with the existing big-symbol auto-pick the same mechanism, broader trigger set.
349
+ Console emits a one-line `console.info` hint identifying which condition triggered the auto-pick. Pairs with the existing big-symbol auto-pick. the same mechanism, broader trigger set.
182
350
 
183
351
  - [#91](https://github.com/schmooky/pixi-reels/pull/91) [`d211ca4`](https://github.com/schmooky/pixi-reels/commit/d211ca495e626c18b92187902a527aa182d0bbbb) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add `ReelSetBuilder.gsap(instance)` for explicit GSAP dependency injection.
184
352
 
185
- The engine internally drives every tween, timeline, and `delayedCall` through a single bound `gsap` instance. By default that is the `gsap` resolved at the engine's own module path fine for the common case where bundler `dedupe` collapses both the engine's and the consumer's `'gsap'` to one module instance.
353
+ The engine internally drives every tween, timeline, and `delayedCall` through a single bound `gsap` instance. By default that is the `gsap` resolved at the engine's own module path. fine for the common case where bundler `dedupe` collapses both the engine's and the consumer's `'gsap'` to one module instance.
186
354
 
187
- In setups where two `gsap` instances exist at runtime (symlinked workspaces, npm-link, misconfigured `dedupe`), tweens started by the engine live on a different root timeline than the one the consumer drives animations stall, double-fire, or freeze on hidden tabs. Calling `.gsap(myGsap)` in the builder rebinds the engine to the consumer's instance:
355
+ In setups where two `gsap` instances exist at runtime (symlinked workspaces, npm-link, misconfigured `dedupe`), tweens started by the engine live on a different root timeline than the one the consumer drives. animations stall, double-fire, or freeze on hidden tabs. Calling `.gsap(myGsap)` in the builder rebinds the engine to the consumer's instance:
188
356
 
189
357
  ```ts
190
358
  import { gsap } from 'gsap';
@@ -205,7 +373,7 @@
205
373
 
206
374
  Each lifecycle event (`spin:start`, `spin:reelLanded`, `spin:allLanded`, `spin:complete`) captures one `DebugSnapshot` while a recording session is active. Frames are tagged with the string passed to `startRecording`, so multiple sessions can share one global log and be filtered out via `getFrames(tag)`. Per-process buffer is capped at 1000 frames by default (rolling window); override via `startRecording(reelSet, tag, { maxFrames })`. Recording auto-detaches when the reel set emits `'destroyed'`.
207
375
 
208
- Designed for AI agents and debug harnesses that need a frame-by-frame trace of a spin sequence particularly useful for diagnosing flicker, double-fires, or off-by-one frame issues that aren't visible from a single point-in-time `debugSnapshot`.
376
+ Designed for AI agents and debug harnesses that need a frame-by-frame trace of a spin sequence. particularly useful for diagnosing flicker, double-fires, or off-by-one frame issues that aren't visible from a single point-in-time `debugSnapshot`.
209
377
 
210
378
  Also exposed on `__PIXI_REELS_DEBUG` after `enableDebug(reelSet)`:
211
379
 
@@ -216,32 +384,32 @@
216
384
  __PIXI_REELS_DEBUG.getFrames("my-tag");
217
385
  ```
218
386
 
219
- `startRecording` is idempotent per reel set calling it twice on the same set replaces the prior session.
387
+ `startRecording` is idempotent per reel set. calling it twice on the same set replaces the prior session.
220
388
 
221
- - [#95](https://github.com/schmooky/pixi-reels/pull/95) [`1abfc45`](https://github.com/schmooky/pixi-reels/commit/1abfc45a445ec9491ddee69367f827333735acdf) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add `Reel.setSymbolAt(visibleRow, symbolId)` and `ReelSet.setSymbolAt(col, row, symbolId)` public API for swapping a single visible cell's symbol identity in place at rest.
389
+ - [#95](https://github.com/schmooky/pixi-reels/pull/95) [`1abfc45`](https://github.com/schmooky/pixi-reels/commit/1abfc45a445ec9491ddee69367f827333735acdf) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add `Reel.setSymbolAt(visibleRow, symbolId)` and `ReelSet.setSymbolAt(col, row, symbolId)`. public API for swapping a single visible cell's symbol identity in place at rest.
222
390
 
223
391
  Useful for live presentation effects that don't fit the `setResult` / `placeSymbols` flow:
224
392
 
225
393
  - converting a symbol to a wild after a cascade pop,
226
394
  - swapping to a sticky variant after a win is paid out.
227
395
 
228
- The method funnels into the same internal activate path as the rest of the engine, so the swapped-in symbol gets its proper parent (masked vs unmasked container), `zIndex`, and visual reset for free no follow-up `refreshZIndex` required.
396
+ The method funnels into the same internal activate path as the rest of the engine, so the swapped-in symbol gets its proper parent (masked vs unmasked container), `zIndex`, and visual reset for free. no follow-up `refreshZIndex` required.
229
397
 
230
398
  Validation (all guards fail loud):
231
399
 
232
- - throws if the reel is in motion (`speed !== 0` or `isStopping`) a mid-spin swap would be overwritten by the next wrap/stop frame anyway.
400
+ - throws if the reel is in motion (`speed !== 0` or `isStopping`). a mid-spin swap would be overwritten by the next wrap/stop frame anyway.
233
401
  - throws if `visibleRow` is not an integer in `[0, visibleRows)`.
234
402
  - throws if `symbolId` is not registered.
235
403
  - throws if the target row is a non-anchor cell of a big-symbol block.
236
- - throws if the target row currently holds the anchor of a big-symbol block big blocks span multiple cells (and possibly reels) and require `placeSymbols` plus the cross-reel OCCUPIED coordinator.
237
- - throws if `symbolId` itself is a big symbol same reason.
404
+ - throws if the target row currently holds the anchor of a big-symbol block. big blocks span multiple cells (and possibly reels) and require `placeSymbols` plus the cross-reel OCCUPIED coordinator.
405
+ - throws if `symbolId` itself is a big symbol. same reason.
238
406
  - `ReelSet.setSymbolAt` additionally throws if the cell currently has an active pin; call `unpin(col, row)` first to overwrite.
239
407
 
240
408
  Emits `symbol:created` on the per-reel event bus, matching motion-driven swaps.
241
409
 
242
410
  - [#78](https://github.com/schmooky/pixi-reels/pull/78) [`9f6f0da`](https://github.com/schmooky/pixi-reels/commit/9f6f0dac52bcb01936422e719db020c2e6b76280) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `reelSet.spin({ holdReels: [...] })` for subset spinning.
243
411
 
244
- Held reels skip START / SPIN / STOP entirely and stay on whatever symbols they're currently showing no more "fragment the board into one ReelSet per column" workaround for Hold & Win, sticky / expanding wilds, or trigger-column bonus respins. Held reels count as already-landed for the `spin:allLanded` resolver, so only the non-held reels actually animate.
412
+ Held reels skip START / SPIN / STOP entirely and stay on whatever symbols they're currently showing. no more "fragment the board into one ReelSet per column" workaround for Hold & Win, sticky / expanding wilds, or trigger-column bonus respins. Held reels count as already-landed for the `spin:allLanded` resolver, so only the non-held reels actually animate.
245
413
 
246
414
  ```ts
247
415
  // Hold reels 0 and 4; only reels 1, 2, 3 reroll.
@@ -257,27 +425,27 @@
257
425
  - `setStopDelays([...])` entries at held indices are ignored.
258
426
  - No `spin:reelLanded` / `spin:stopping` event fires for held reels; `spin:allLanded` fires once every non-held reel lands.
259
427
  - Out-of-range / duplicate / non-integer entries in `holdReels` are silently filtered.
260
- - Big-symbol blocks crossing the held / non-held boundary are not supported author results so big symbols stay inside a contiguous run of non-held reels.
428
+ - Big-symbol blocks crossing the held / non-held boundary are not supported. author results so big symbols stay inside a contiguous run of non-held reels.
261
429
 
262
430
  Exports `SpinOptions` from the package root.
263
431
 
264
432
  - [#92](https://github.com/schmooky/pixi-reels/pull/92) [`aa8be14`](https://github.com/schmooky/pixi-reels/commit/aa8be149aa7c9f8ff4195b6850b767b8bf402bcc) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Make `SymbolData.unmask: true` actually re-parent the symbol view to `viewport.unmaskedContainer`.
265
433
 
266
- Until now the `unmask` flag on `SymbolData` was accepted by the builder but never read by the engine symbols always landed inside the reel's masked container regardless of the flag. With this change, every code path that places a symbol into the reel `_setupSymbolPositions`, `_replaceSymbol` (both stub-install and stub-replace branches and the regular swap), and `reshape` consults `_symbolsData[id].unmask` and parents the view to `viewport.unmaskedContainer` when set.
434
+ Until now the `unmask` flag on `SymbolData` was accepted by the builder but never read by the engine. symbols always landed inside the reel's masked container regardless of the flag. With this change, every code path that places a symbol into the reel. `_setupSymbolPositions`, `_replaceSymbol` (both stub-install and stub-replace branches and the regular swap), and `reshape`. consults `_symbolsData[id].unmask` and parents the view to `viewport.unmaskedContainer` when set.
267
435
 
268
436
  When unmasked, the engine sets the view's X to `reel.container.x` and adds `reel.container.y` to the view's Y so the at-rest cell position aligns with the reel column (since `unmaskedContainer` sits at viewport-local 0,0).
269
437
 
270
- Documented limitation in `SymbolData.unmask` JSDoc: `ReelMotion` writes `view.y` in reel-local coords every frame, so an unmasked symbol on the strip will appear shifted vertically by `reel.container.y` while the reel is spinning. Treat `unmask: true` as a _landed-state_ flag it is correct at rest and during static frames, but not designed to stay visually accurate while the reel is spinning. For mid-spin "stays visible above mask" overlays, use a cell pin instead.
438
+ Documented limitation in `SymbolData.unmask` JSDoc: `ReelMotion` writes `view.y` in reel-local coords every frame, so an unmasked symbol on the strip will appear shifted vertically by `reel.container.y` while the reel is spinning. Treat `unmask: true` as a _landed-state_ flag. it is correct at rest and during static frames, but not designed to stay visually accurate while the reel is spinning. For mid-spin "stays visible above mask" overlays, use a cell pin instead.
271
439
 
272
- **Pyramid layouts:** registering any unmasked symbol on a slot where any reel has a non-zero `offsetY` (pyramid / trapezoid) now throws at `build()`. Reason: the same motion-layer issue persists at landing `snapToGrid` writes reel-local Y, mispositioning the unmasked view by `reel.container.y` even at rest. Use cell pins for above-mask overlays on pyramid slots, or remove the per-reel offset.
440
+ **Pyramid layouts:** registering any unmasked symbol on a slot where any reel has a non-zero `offsetY` (pyramid / trapezoid) now throws at `build()`. Reason: the same motion-layer issue persists at landing. `snapToGrid` writes reel-local Y, mispositioning the unmasked view by `reel.container.y` even at rest. Use cell pins for above-mask overlays on pyramid slots, or remove the per-reel offset.
273
441
 
274
442
  - [#104](https://github.com/schmooky/pixi-reels/pull/104) [`1dc8d08`](https://github.com/schmooky/pixi-reels/commit/1dc8d084ad171b8347312991c98cfbfc07bed451) Thanks [@feddorovich](https://github.com/feddorovich)! - `reelSet.spin()` accepts an optional `{ mode: 'standard' | 'cascade' }` argument that picks the phase chain for a single spin. Tumble-cascade slots can now do classic strip-spin + bounce on the first round and drop-in tumble on subsequent waves.
275
443
 
276
- `.cascade(...)` on the builder still wires the drop-in phases but they are now registered under `dropStart` / `dropStop` keys instead of overwriting `start` / `stop`. The default mode flips to `'cascade'` when `.cascade(...)` was called, so existing callers that just call `spin()` without args see no change.
444
+ `.cascade(...)` on the builder still wires the drop-in phases. but they are now registered under `dropStart` / `dropStop` keys instead of overwriting `start` / `stop`. The default mode flips to `'cascade'` when `.cascade(...)` was called, so existing callers that just call `spin()` without args see no change.
277
445
 
278
446
  Calling `spin({ mode: 'cascade' })` on a builder that didn't configure `.cascade(...)` throws a clear error. The new `SpinOptions` type is exported from the package barrel.
279
447
 
280
- - [#103](https://github.com/schmooky/pixi-reels/pull/103) [`18474ee`](https://github.com/schmooky/pixi-reels/commit/18474eebbc0ed16b63f2e6b9f8af1acb9c5ea2d2) Thanks [@feddorovich](https://github.com/feddorovich)! - Added `ReelSet.requestSkip()` (and `SpinController.requestSkip()`) a slam-stop entry point that's safe to call before `setResult()` arrives. If the result is already pending, it behaves exactly like `skip()`. Otherwise the skip is queued and fires automatically as soon as `setResult()` lands.
448
+ - [#103](https://github.com/schmooky/pixi-reels/pull/103) [`18474ee`](https://github.com/schmooky/pixi-reels/commit/18474eebbc0ed16b63f2e6b9f8af1acb9c5ea2d2) Thanks [@feddorovich](https://github.com/feddorovich)! - Added `ReelSet.requestSkip()` (and `SpinController.requestSkip()`). a slam-stop entry point that's safe to call before `setResult()` arrives. If the result is already pending, it behaves exactly like `skip()`. Otherwise the skip is queued and fires automatically as soon as `setResult()` lands.
281
449
 
282
450
  Use this from UI handlers in server-driven slots: a player tapping the spin button to slam-stop before the WebSocket response reaches the client no longer snaps every reel onto whatever buffer state happened to be mid-scroll. Existing `skip()` is unchanged.
283
451
 
@@ -293,7 +461,7 @@
293
461
 
294
462
  - [#97](https://github.com/schmooky/pixi-reels/pull/97) [`db32899`](https://github.com/schmooky/pixi-reels/commit/db32899c832ce68e7ba1aaf797bedaf3a85d6fa3) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `ReelSetBuilder.bufferSymbols(count)` now clamps `0`, negative numbers, `NaN`, and non-finite values to the minimum of 1, with a single console warning per process.
295
463
 
296
- Buffer rows are off-screen cells the reel keeps around the visible window so symbols can fade/slide in cleanly. The motion layer's wrap detection assumes at least one buffer row above and one below passing `0` would produce an inconsistent state that surfaced later as visible flicker on motion-wrap, not as a clear configuration error at build time.
464
+ Buffer rows are off-screen cells the reel keeps around the visible window so symbols can fade/slide in cleanly. The motion layer's wrap detection assumes at least one buffer row above and one below. passing `0` would produce an inconsistent state that surfaced later as visible flicker on motion-wrap, not as a clear configuration error at build time.
297
465
 
298
466
  The clamp is preferred over a thrown error so existing user code that accidentally passed `0` keeps running. The warning fires once per process (regardless of how many builders hit the bad value) so logs stay readable when a faulty default is wired into a loop.
299
467
 
@@ -301,9 +469,9 @@
301
469
 
302
470
  Three previously-leaking scenarios now settle the returned promise instead of hanging forever:
303
471
 
304
- - **Concurrent one-shots** calling `playOut()` while `playWin()` is in flight resolves the prior `playWin` promise (its track was overwritten) before starting the new one.
305
- - **`playBlur` mid-animation** entering a SPIN that triggers blur while a win is still animating settles the win promise.
306
- - **Listener leak** back-to-back one-shots no longer accumulate stale listeners on the Spine state. Each new one-shot detaches the prior listener.
472
+ - **Concurrent one-shots**. calling `playOut()` while `playWin()` is in flight resolves the prior `playWin` promise (its track was overwritten) before starting the new one.
473
+ - **`playBlur` mid-animation**. entering a SPIN that triggers blur while a win is still animating settles the win promise.
474
+ - **Listener leak**. back-to-back one-shots no longer accumulate stale listeners on the Spine state. Each new one-shot detaches the prior listener.
307
475
 
308
476
  Refactored to a single internal `_resolveOneShot()` helper called from `onActivate`, `onDeactivate`, `stopAnimation`, `playBlur`, and the start of every new `_playOneShot`. The track-entry guard (`done !== entry`) is preserved so unrelated entries firing complete on the same track are correctly ignored.
309
477
 
@@ -314,11 +482,11 @@
314
482
  Two related bugs caused symbols to render in the wrong reel after rapid spin/skip cycles, particularly when the win spotlight runs alongside an expanding-wild mechanic that triggers many `placeSymbols` calls in quick succession:
315
483
 
316
484
  - `SymbolSpotlight.hide()` reparented every symbol it had ever tracked back to its `originalParent`, even when `promoteAboveMask: false` (no reparenting on `show()`) or after the shared symbol pool had recycled the instance into a different reel. The recycled symbol got yanked from its new owner, leaving a hole there and a stranger in the original reel.
317
- - `Reel._replaceSymbol` used the captured `oldSymbol.view.parent` as the destination for the replacement view. If the old symbol had been moved (by the spotlight or by pool recycling), the new symbol landed in a foreign container symbols accumulated in the wrong reel across spins.
485
+ - `Reel._replaceSymbol` used the captured `oldSymbol.view.parent` as the destination for the replacement view. If the old symbol had been moved (by the spotlight or by pool recycling), the new symbol landed in a foreign container. symbols accumulated in the wrong reel across spins.
318
486
 
319
487
  Both paths now anchor to the reel's own container; the spotlight only reparents symbols whose view is still in `spotlightContainer` (i.e., never recycled away).
320
488
 
321
- - [#101](https://github.com/schmooky/pixi-reels/pull/101) [`7a7670c`](https://github.com/schmooky/pixi-reels/commit/7a7670cf1a98e2b2778069a728147452ece2dc66) Thanks [@feddorovich](https://github.com/feddorovich)! - `ReelSymbol.activate()` and `ReelSymbol.deactivate()` now both reset the container's `alpha`, `scale`, `rotation`, `filters`, and `zIndex`. Previously a subclass that decorated `view` from a spin-lifecycle hook (e.g. attaching a `BlurFilter` in `onReelSpinStart`) had to remember to undo every property on its own and any path that skipped a hook (a buffer cell that exited spin without `onReelSpinEnd`, a slam-stop that bypassed the lifecycle) left a recycled symbol carrying stale state into its next life. The most visible symptom was a "blurred" cell appearing after a cascade refill once a symbol had been pooled mid-spin.
489
+ - [#101](https://github.com/schmooky/pixi-reels/pull/101) [`7a7670c`](https://github.com/schmooky/pixi-reels/commit/7a7670cf1a98e2b2778069a728147452ece2dc66) Thanks [@feddorovich](https://github.com/feddorovich)! - `ReelSymbol.activate()` and `ReelSymbol.deactivate()` now both reset the container's `alpha`, `scale`, `rotation`, `filters`, and `zIndex`. Previously a subclass that decorated `view` from a spin-lifecycle hook (e.g. attaching a `BlurFilter` in `onReelSpinStart`) had to remember to undo every property on its own. and any path that skipped a hook (a buffer cell that exited spin without `onReelSpinEnd`, a slam-stop that bypassed the lifecycle) left a recycled symbol carrying stale state into its next life. The most visible symptom was a "blurred" cell appearing after a cascade refill once a symbol had been pooled mid-spin.
322
490
 
323
491
  `ReelSymbol.destroy()` now inlines the lifecycle hooks (`stopAnimation`, `onDeactivate`) instead of going through `deactivate()`, so it doesn't try to reset transform / filter state on a view that was already torn down by a parent `container.destroy({ children: true })`.
324
492
 
@@ -326,21 +494,21 @@
326
494
 
327
495
  No public API change. Subclasses that already cleared their own filter / transform state continue to work and just do a few redundant assignments.
328
496
 
329
- - [#102](https://github.com/schmooky/pixi-reels/pull/102) [`a2be4b8`](https://github.com/schmooky/pixi-reels/commit/a2be4b83544b66bd3650f14de251dcf51424b552) Thanks [@feddorovich](https://github.com/feddorovich)! - `SpinController.skip()` now fires `onReelSpinEnd` and `onReelLanded` on every reel that hadn't already landed, regardless of which phase was active when the slam-stop arrived. Previously these symbol-level hooks fired only when the active phase happened to be `StopPhase` or `DropStopPhase` (their `onSkip()` called the notifications); a skip during `StartPhase` / `SpinPhase` / `AnticipationPhase` / `AdjustPhase` left visible symbols without an end-of-spin signal most visibly, motion blur (or any other decoration attached in `onReelSpinStart`) stayed on the cell after the slam.
497
+ - [#102](https://github.com/schmooky/pixi-reels/pull/102) [`a2be4b8`](https://github.com/schmooky/pixi-reels/commit/a2be4b83544b66bd3650f14de251dcf51424b552) Thanks [@feddorovich](https://github.com/feddorovich)! - `SpinController.skip()` now fires `onReelSpinEnd` and `onReelLanded` on every reel that hadn't already landed, regardless of which phase was active when the slam-stop arrived. Previously these symbol-level hooks fired only when the active phase happened to be `StopPhase` or `DropStopPhase` (their `onSkip()` called the notifications); a skip during `StartPhase` / `SpinPhase` / `AnticipationPhase` / `AdjustPhase` left visible symbols without an end-of-spin signal. most visibly, motion blur (or any other decoration attached in `onReelSpinStart`) stayed on the cell after the slam.
330
498
 
331
- The notifications moved out of `StopPhase.onSkip` / `DropStopPhase.onSkip` into the controller so there's a single source of truth and no double-fire. Natural-stop flow is unchanged those phases still fire the hooks themselves before the bounce.
499
+ The notifications moved out of `StopPhase.onSkip` / `DropStopPhase.onSkip` into the controller so there's a single source of truth and no double-fire. Natural-stop flow is unchanged. those phases still fire the hooks themselves before the bounce.
332
500
 
333
501
  ## 0.3.2
334
502
 
335
503
  ### Patch Changes
336
504
 
337
- - [`b86dad7`](https://github.com/schmooky/pixi-reels/commit/b86dad75fcdd4936170bb96a6084904bad419dd3) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: ship `CONTRIBUTING.md` in the npm tarball so the npmjs.com "Contributing" sidebar link resolves. npmjs builds that link from `repository.directory` (`packages/pixi-reels`) and a standard filename, but the file previously only existed at the monorepo root the link 404'd. The build script now syncs `CONTRIBUTING.md` into the package alongside `README.md` and `LICENSE`, and the package's `files` array includes it.
505
+ - [`b86dad7`](https://github.com/schmooky/pixi-reels/commit/b86dad75fcdd4936170bb96a6084904bad419dd3) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: ship `CONTRIBUTING.md` in the npm tarball so the npmjs.com "Contributing" sidebar link resolves. npmjs builds that link from `repository.directory` (`packages/pixi-reels`) and a standard filename, but the file previously only existed at the monorepo root. the link 404'd. The build script now syncs `CONTRIBUTING.md` into the package alongside `README.md` and `LICENSE`, and the package's `files` array includes it.
338
506
 
339
507
  ## 0.3.1
340
508
 
341
509
  ### Patch Changes
342
510
 
343
- - [`93aa66c`](https://github.com/schmooky/pixi-reels/commit/93aa66c103ef0f624345c76a92a22621fc3c676a) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Update: package `homepage` now points at the canonical docs site, `https://pixi-reels.schmooky.dev`. No code or runtime change npm metadata and the docs site URL only.
511
+ - [`93aa66c`](https://github.com/schmooky/pixi-reels/commit/93aa66c103ef0f624345c76a92a22621fc3c676a) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Update: package `homepage` now points at the canonical docs site, `https://pixi-reels.schmooky.dev`. No code or runtime change. npm metadata and the docs site URL only.
344
512
 
345
513
  ## 0.3.0
346
514
 
@@ -350,10 +518,10 @@
350
518
 
351
519
  - **Per-reel static shape (pyramids):** `builder.visibleRowsPerReel([3, 5, 5, 5, 3])`, optional `reelPixelHeights`, `reelAnchor: 'top' | 'center' | 'bottom'`. Reels can now have non-uniform row counts at build time.
352
520
  - **MultiWays (per-spin row variation):** `builder.multiways({ minRows, maxRows, reelPixelHeight })` plus `reelSet.setShape(rowsPerReel)` mid-spin. A new `AdjustPhase` (inserted only when `.multiways(...)` is called) reshapes reels between SPIN and STOP. Pin migration follows: pins gain a frozen `originRow` and migrate back toward it on each reshape.
353
- - **Big symbols (`N×M` blocks):** `register('bonus', SymbolClass, { size: { w: 2, h: 2 } })`. The result grid stays `string[][]` the engine paints OCCUPIED across the block. `getSymbolFootprint(col, row)` resolves any cell to the anchor.
521
+ - **Big symbols (`N×M` blocks):** `register('bonus', SymbolClass, { size: { w: 2, h: 2 } })`. The result grid stays `string[][]`. the engine paints OCCUPIED across the block. `getSymbolFootprint(col, row)` resolves any cell to the anchor.
354
522
  - **Expanding wilds:** unchanged from the existing pin API; reaffirmed via tests as a degenerate big-symbol case.
355
523
 
356
- New events: `shape:changed`, `adjust:start`, `adjust:complete`, `pin:migrated`. They only fire on MultiWays slots non-MultiWays event surfaces are unchanged.
524
+ New events: `shape:changed`, `adjust:start`, `adjust:complete`, `pin:migrated`. They only fire on MultiWays slots. non-MultiWays event surfaces are unchanged.
357
525
 
358
526
  New runtime: `reelSet.setShape()`, `reelSet.getSymbolFootprint()`, `reelSet.getVisibleGrid()`, `reelSet.isMultiWaysSlot`. New builder fluents: `.visibleRowsPerReel()`, `.reelPixelHeights()`, `.reelAnchor()`, `.multiways()`, `.pinMigrationDuration()`, `.pinMigrationEase()`. Pin gains optional `originRow`.
359
527
 
@@ -367,9 +535,9 @@
367
535
 
368
536
  - [#61](https://github.com/schmooky/pixi-reels/pull/61) [`4b22c00`](https://github.com/schmooky/pixi-reels/commit/4b22c00b0f5733d141de1fee4ed8bf515cc2a513) Thanks [@schmooky](https://github.com/schmooky)! - Fix and harden a handful of follow-ups from the per-reel-geometry / MultiWays / big-symbols PR:
369
537
 
370
- - `Reel.reshape()` now keeps `_reelHeight` in sync with the new geometry so the field doesn't go stale after a reshape. Previously a direct external call left `reelHeight` reporting the construction-time value. The method is also marked `@internal` in JSDoc `ReelSet.setShape()` is the supported entry point.
538
+ - `Reel.reshape()` now keeps `_reelHeight` in sync with the new geometry so the field doesn't go stale after a reshape. Previously a direct external call left `reelHeight` reporting the construction-time value. The method is also marked `@internal` in JSDoc. `ReelSet.setShape()` is the supported entry point.
371
539
  - `ReelSetBuilder.maskStrategy()` now validates its argument synchronously: passing `null`, `undefined`, or an object missing `build()` / `update()` methods throws with a grep-able error instead of crashing later inside `ReelViewport`.
372
- - Added a comment in `SpinController.skip()` documenting the reshape-on-skip contract pin overlays migrate instantly on slam-stop regardless of `pinMigrationDuration`, and the rationale (overlays are destroyed at land anyway).
540
+ - Added a comment in `SpinController.skip()` documenting the reshape-on-skip contract. pin overlays migrate instantly on slam-stop regardless of `pinMigrationDuration`, and the rationale (overlays are destroyed at land anyway).
373
541
 
374
542
  No new public API; behaviour for existing well-formed callers is unchanged.
375
543
 
@@ -385,17 +553,17 @@
385
553
 
386
554
  All three are additive, so this bundles them into a single minor bump.
387
555
 
388
- - [`555c9f0`](https://github.com/schmooky/pixi-reels/commit/555c9f007d749a8e2329a53dc17208fc94d7b5f3) - Add: `WinPresenter` a minimal win-presentation layer that animates winning cells and fires events. Paylines, cluster pops, scatter splashes all use the same shape. The library never draws lines or overlays; user code does that by reacting to events.
556
+ - [`555c9f0`](https://github.com/schmooky/pixi-reels/commit/555c9f007d749a8e2329a53dc17208fc94d7b5f3) - Add: `WinPresenter`. a minimal win-presentation layer that animates winning cells and fires events. Paylines, cluster pops, scatter splashes all use the same shape. The library never draws lines or overlays; user code does that by reacting to events.
389
557
 
390
- - `WinPresenter.show(wins: Win[])` animates each win's cells, one by one. `stagger: 0` flashes simultaneously, `stagger > 0` sweeps left-to-right in cell order.
391
- - `Win` one shape: `{ cells: SymbolPosition[]; value?: number; kind?: string; id?: number }`. Covers paylines, clusters, cascade pops, scatters.
558
+ - `WinPresenter.show(wins: Win[])`. animates each win's cells, one by one. `stagger: 0` flashes simultaneously, `stagger > 0` sweeps left-to-right in cell order.
559
+ - `Win`. one shape: `{ cells: SymbolPosition[]; value?: number; kind?: string; id?: number }`. Covers paylines, clusters, cascade pops, scatters.
392
560
  - `dimLosers` (default 0.35 alpha) fades non-winning cells during each win; restored on `win:end`.
393
561
  - `symbolAnim`: `'win'` (default, calls `playWin()`), a named spine animation, or `(symbol, cell, win) => Promise<void>` for a custom callback.
394
562
  - Events fire on `ReelSet.events`: `win:start` (full list), `win:group` (per-win), `win:symbol` (per-cell), `win:end` (`complete` / `aborted`). Subscribe with `reelSet.getCellBounds` to draw any overlay you want.
395
- - Cascades: call `presenter.show([{ cells: winners }])` from `runCascade`'s `onWinnersVanish` hook same API.
563
+ - Cascades: call `presenter.show([{ cells: winners }])` from `runCascade`'s `onWinnersVanish` hook. same API.
396
564
  - Helper: `sortByValueDesc` exported for convenience.
397
565
  - Types: `Win`, `SymbolPosition` (canonicalised to `config/types`, re-exported from events).
398
- - Reels now have an explicit `container.zIndex = reelIndex` so the viewport's sorted `maskedContainer` draws reels deterministically same order as before, but callers can flip it for bottom-left diagonal overflow.
566
+ - Reels now have an explicit `container.zIndex = reelIndex` so the viewport's sorted `maskedContainer` draws reels deterministically. same order as before, but callers can flip it for bottom-left diagonal overflow.
399
567
 
400
568
  No existing API is changed or removed.
401
569
 
@@ -403,5 +571,5 @@
403
571
 
404
572
  - [`7792142`](https://github.com/schmooky/pixi-reels/commit/779214217bb341cfb66f2db74616b2e8608893b9) - Fix: Two `AnimatedSpriteSymbol` bugs that only manifest on symbols with non-trivial win animations:
405
573
 
406
- - `resize()` now positions the sprite according to its configured anchor, so `anchor: { x: 0.5, y: 0.5 }` renders the symbol centred in its cell instead of with its centre pinned to the cell's top-left corner (which clipped three quarters of the symbol under the reel mask). `anchor: (0, 0)` the prior default and only combination that worked is unchanged.
407
- - `playWin()` now returns the animation to frame 0 (`gotoAndStop(0)`) when the sequence completes, so the idle visible state settles on the neutral base frame. Previously the sprite held its last animation frame indefinitely fine for symmetric pulses that happen to end where they started, a visible glitch for anything else (AI-generated or keyframe sequences that end mid-action).
574
+ - `resize()` now positions the sprite according to its configured anchor, so `anchor: { x: 0.5, y: 0.5 }` renders the symbol centred in its cell instead of with its centre pinned to the cell's top-left corner (which clipped three quarters of the symbol under the reel mask). `anchor: (0, 0)`. the prior default and only combination that worked. is unchanged.
575
+ - `playWin()` now returns the animation to frame 0 (`gotoAndStop(0)`) when the sequence completes, so the idle visible state settles on the neutral base frame. Previously the sprite held its last animation frame indefinitely. fine for symmetric pulses that happen to end where they started, a visible glitch for anything else (AI-generated or keyframe sequences that end mid-action).