pixi-reels 0.0.0-dependabot-github-actions-github-codeql-action-4-35-4-20260514194131

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 (125) hide show
  1. package/CHANGELOG.md +260 -0
  2. package/CONTRIBUTING.md +85 -0
  3. package/LICENSE +28 -0
  4. package/README.md +168 -0
  5. package/dist/SpineSymbol-CfVrc5Pk.js +88 -0
  6. package/dist/SpineSymbol-CfVrc5Pk.js.map +1 -0
  7. package/dist/SpineSymbol-JA5PdEbT.cjs +2 -0
  8. package/dist/SpineSymbol-JA5PdEbT.cjs.map +1 -0
  9. package/dist/cascade/CascadeAnticipationPhase.d.ts +23 -0
  10. package/dist/cascade/CascadeAnticipationPhase.d.ts.map +1 -0
  11. package/dist/cascade/DropRecipes.d.ts +40 -0
  12. package/dist/cascade/DropRecipes.d.ts.map +1 -0
  13. package/dist/config/SpeedPresets.d.ts +49 -0
  14. package/dist/config/SpeedPresets.d.ts.map +1 -0
  15. package/dist/config/defaults.d.ts +12 -0
  16. package/dist/config/defaults.d.ts.map +1 -0
  17. package/dist/config/types.d.ts +296 -0
  18. package/dist/config/types.d.ts.map +1 -0
  19. package/dist/core/Reel.d.ts +339 -0
  20. package/dist/core/Reel.d.ts.map +1 -0
  21. package/dist/core/ReelMotion.d.ts +51 -0
  22. package/dist/core/ReelMotion.d.ts.map +1 -0
  23. package/dist/core/ReelSet.d.ts +509 -0
  24. package/dist/core/ReelSet.d.ts.map +1 -0
  25. package/dist/core/ReelSetBuilder.d.ts +282 -0
  26. package/dist/core/ReelSetBuilder.d.ts.map +1 -0
  27. package/dist/core/ReelViewport.d.ts +129 -0
  28. package/dist/core/ReelViewport.d.ts.map +1 -0
  29. package/dist/core/StopSequencer.d.ts +26 -0
  30. package/dist/core/StopSequencer.d.ts.map +1 -0
  31. package/dist/debug/debug.d.ts +129 -0
  32. package/dist/debug/debug.d.ts.map +1 -0
  33. package/dist/events/EventEmitter.d.ts +25 -0
  34. package/dist/events/EventEmitter.d.ts.map +1 -0
  35. package/dist/events/ReelEvents.d.ts +125 -0
  36. package/dist/events/ReelEvents.d.ts.map +1 -0
  37. package/dist/frame/ColumnTarget.d.ts +77 -0
  38. package/dist/frame/ColumnTarget.d.ts.map +1 -0
  39. package/dist/frame/FrameBuilder.d.ts +51 -0
  40. package/dist/frame/FrameBuilder.d.ts.map +1 -0
  41. package/dist/frame/OffsetCalculator.d.ts +19 -0
  42. package/dist/frame/OffsetCalculator.d.ts.map +1 -0
  43. package/dist/frame/RandomSymbolProvider.d.ts +27 -0
  44. package/dist/frame/RandomSymbolProvider.d.ts.map +1 -0
  45. package/dist/index.cjs +5 -0
  46. package/dist/index.cjs.map +1 -0
  47. package/dist/index.d.ts +70 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +2571 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/pins/CellPin.d.ts +140 -0
  52. package/dist/pins/CellPin.d.ts.map +1 -0
  53. package/dist/pool/ObjectPool.d.ts +36 -0
  54. package/dist/pool/ObjectPool.d.ts.map +1 -0
  55. package/dist/speed/SpeedManager.d.ts +38 -0
  56. package/dist/speed/SpeedManager.d.ts.map +1 -0
  57. package/dist/spin/SpinController.d.ts +183 -0
  58. package/dist/spin/SpinController.d.ts.map +1 -0
  59. package/dist/spin/modes/CascadeMode.d.ts +16 -0
  60. package/dist/spin/modes/CascadeMode.d.ts.map +1 -0
  61. package/dist/spin/modes/ImmediateMode.d.ts +10 -0
  62. package/dist/spin/modes/ImmediateMode.d.ts.map +1 -0
  63. package/dist/spin/modes/SpinningMode.d.ts +18 -0
  64. package/dist/spin/modes/SpinningMode.d.ts.map +1 -0
  65. package/dist/spin/modes/StandardMode.d.ts +10 -0
  66. package/dist/spin/modes/StandardMode.d.ts.map +1 -0
  67. package/dist/spin/phases/AdjustPhase.d.ts +73 -0
  68. package/dist/spin/phases/AdjustPhase.d.ts.map +1 -0
  69. package/dist/spin/phases/AnticipationPhase.d.ts +24 -0
  70. package/dist/spin/phases/AnticipationPhase.d.ts.map +1 -0
  71. package/dist/spin/phases/DropStartPhase.d.ts +21 -0
  72. package/dist/spin/phases/DropStartPhase.d.ts.map +1 -0
  73. package/dist/spin/phases/DropStopPhase.d.ts +44 -0
  74. package/dist/spin/phases/DropStopPhase.d.ts.map +1 -0
  75. package/dist/spin/phases/PhaseFactory.d.ts +32 -0
  76. package/dist/spin/phases/PhaseFactory.d.ts.map +1 -0
  77. package/dist/spin/phases/ReelPhase.d.ts +39 -0
  78. package/dist/spin/phases/ReelPhase.d.ts.map +1 -0
  79. package/dist/spin/phases/SpinPhase.d.ts +25 -0
  80. package/dist/spin/phases/SpinPhase.d.ts.map +1 -0
  81. package/dist/spin/phases/StartPhase.d.ts +26 -0
  82. package/dist/spin/phases/StartPhase.d.ts.map +1 -0
  83. package/dist/spin/phases/StopPhase.d.ts +37 -0
  84. package/dist/spin/phases/StopPhase.d.ts.map +1 -0
  85. package/dist/spine/SpineReelSymbol.d.ts +130 -0
  86. package/dist/spine/SpineReelSymbol.d.ts.map +1 -0
  87. package/dist/spine/index.d.ts +5 -0
  88. package/dist/spine/index.d.ts.map +1 -0
  89. package/dist/spine.cjs +2 -0
  90. package/dist/spine.cjs.map +1 -0
  91. package/dist/spine.js +123 -0
  92. package/dist/spine.js.map +1 -0
  93. package/dist/spotlight/SymbolSpotlight.d.ts +70 -0
  94. package/dist/spotlight/SymbolSpotlight.d.ts.map +1 -0
  95. package/dist/symbols/AnimatedSpriteSymbol.d.ts +30 -0
  96. package/dist/symbols/AnimatedSpriteSymbol.d.ts.map +1 -0
  97. package/dist/symbols/ReelSymbol.d.ts +86 -0
  98. package/dist/symbols/ReelSymbol.d.ts.map +1 -0
  99. package/dist/symbols/SpineSymbol.d.ts +34 -0
  100. package/dist/symbols/SpineSymbol.d.ts.map +1 -0
  101. package/dist/symbols/SpriteSymbol.d.ts +29 -0
  102. package/dist/symbols/SpriteSymbol.d.ts.map +1 -0
  103. package/dist/symbols/SymbolFactory.d.ts +20 -0
  104. package/dist/symbols/SymbolFactory.d.ts.map +1 -0
  105. package/dist/symbols/SymbolRegistry.d.ts +25 -0
  106. package/dist/symbols/SymbolRegistry.d.ts.map +1 -0
  107. package/dist/testing/FakeTicker.d.ts +45 -0
  108. package/dist/testing/FakeTicker.d.ts.map +1 -0
  109. package/dist/testing/HeadlessSymbol.d.ts +28 -0
  110. package/dist/testing/HeadlessSymbol.d.ts.map +1 -0
  111. package/dist/testing/index.d.ts +5 -0
  112. package/dist/testing/index.d.ts.map +1 -0
  113. package/dist/testing/testHarness.d.ts +97 -0
  114. package/dist/testing/testHarness.d.ts.map +1 -0
  115. package/dist/utils/Disposable.d.ts +10 -0
  116. package/dist/utils/Disposable.d.ts.map +1 -0
  117. package/dist/utils/TickerRef.d.ts +30 -0
  118. package/dist/utils/TickerRef.d.ts.map +1 -0
  119. package/dist/utils/gsapRef.d.ts +14 -0
  120. package/dist/utils/gsapRef.d.ts.map +1 -0
  121. package/dist/wins/Win.d.ts +7 -0
  122. package/dist/wins/Win.d.ts.map +1 -0
  123. package/dist/wins/WinPresenter.d.ts +100 -0
  124. package/dist/wins/WinPresenter.d.ts.map +1 -0
  125. package/package.json +87 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,260 @@
1
+ # pixi-reels
2
+
3
+ ## 0.0.0-dependabot-github-actions-github-codeql-action-4-35-4-20260514194131
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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).
8
+
9
+ - [#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).
10
+
11
+ 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.
12
+
13
+ Fix: `Reel.placeSymbols` (skip / turbo land path) now reads the negative-index slot for the buffer-above cell instead of always random-filling it. Buffer-below targeting via `symbolIds[visibleRows]` is unchanged.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#115](https://github.com/schmooky/pixi-reels/pull/115) [`1f30d8e`](https://github.com/schmooky/pixi-reels/commit/1f30d8e1b5d997872c85400122ee2613d35e0933) Thanks [@MaksimKiselev](https://github.com/MaksimKiselev)! - Fix: negative indices in `initialFrame` now correctly populate buffer-above slots. Setting `frame[col][-1]` (or `[-2]` for deeper buffers) places the symbol in the corresponding buffer-above cell instead of being silently ignored.
18
+
19
+ ## 0.4.0
20
+
21
+ ### Minor Changes
22
+
23
+ - [#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`.
24
+
25
+ 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.
26
+
27
+ The auto-pick now triggers in either case:
28
+
29
+ - **big symbols** registered (`SymbolData.size` with `w > 1` or `h > 1`), or
30
+ - **unmasked symbols** registered (`SymbolData.unmask: true`),
31
+
32
+ provided the layout has a horizontal gap (`symbolGap.x > 0`). Explicit `.maskStrategy(...)` calls always win.
33
+
34
+ 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.
35
+
36
+ - [#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.
37
+
38
+ 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.
39
+
40
+ 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:
41
+
42
+ ```ts
43
+ import { gsap } from 'gsap';
44
+
45
+ const reelSet = new ReelSetBuilder()
46
+ .reels(5).visibleRows(3).symbolSize(200, 200)
47
+ .symbols(...)
48
+ .ticker(app.ticker)
49
+ .gsap(gsap) // ensure engine and app share one instance
50
+ .build();
51
+ ```
52
+
53
+ Internally this is implemented via a tiny `getGsap()`/`setGsap()` shim in `utils/gsapRef.ts`. Every internal animation site now reads through `getGsap()` instead of importing `'gsap'` directly. A regression-guard test asserts no runtime `gsap.timeline(`/`gsap.to(`/`gsap.delayedCall(` calls outside the shim itself.
54
+
55
+ No behavioural change for consumers who don't call `.gsap()`.
56
+
57
+ - [#99](https://github.com/schmooky/pixi-reels/pull/99) [`544607d`](https://github.com/schmooky/pixi-reels/commit/544607d8f413d9fa7dfcba65f3219819096a65f6) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add a frame-state recorder to the debug module: `startRecording(reelSet, tag)`, `stopRecording(reelSet)`, `getFrames(tag?)`, `clearFrames()`.
58
+
59
+ 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'`.
60
+
61
+ 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`.
62
+
63
+ Also exposed on `__PIXI_REELS_DEBUG` after `enableDebug(reelSet)`:
64
+
65
+ ```js
66
+ __PIXI_REELS_DEBUG.startRecording("my-tag");
67
+ await reelSet.spin();
68
+ __PIXI_REELS_DEBUG.stopRecording();
69
+ __PIXI_REELS_DEBUG.getFrames("my-tag");
70
+ ```
71
+
72
+ `startRecording` is idempotent per reel set — calling it twice on the same set replaces the prior session.
73
+
74
+ - [#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.
75
+
76
+ Useful for live presentation effects that don't fit the `setResult` / `placeSymbols` flow:
77
+
78
+ - converting a symbol to a wild after a cascade pop,
79
+ - swapping to a sticky variant after a win is paid out.
80
+
81
+ 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.
82
+
83
+ Validation (all guards fail loud):
84
+
85
+ - 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.
86
+ - throws if `visibleRow` is not an integer in `[0, visibleRows)`.
87
+ - throws if `symbolId` is not registered.
88
+ - throws if the target row is a non-anchor cell of a big-symbol block.
89
+ - 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.
90
+ - throws if `symbolId` itself is a big symbol — same reason.
91
+ - `ReelSet.setSymbolAt` additionally throws if the cell currently has an active pin; call `unpin(col, row)` first to overwrite.
92
+
93
+ Emits `symbol:created` on the per-reel event bus, matching motion-driven swaps.
94
+
95
+ - [#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.
96
+
97
+ 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.
98
+
99
+ ```ts
100
+ // Hold reels 0 and 4; only reels 1, 2, 3 reroll.
101
+ const spin = reelSet.spin({ holdReels: [0, 4] });
102
+ reelSet.setResult(serverGrid); // entries at 0/4 are ignored
103
+ await spin;
104
+ ```
105
+
106
+ Behaviour:
107
+
108
+ - `setResult(grid)` still expects a full `reelCount`-length grid; held entries are ignored.
109
+ - `setAnticipation([...])` silently filters held indices.
110
+ - `setStopDelays([...])` entries at held indices are ignored.
111
+ - No `spin:reelLanded` / `spin:stopping` event fires for held reels; `spin:allLanded` fires once every non-held reel lands.
112
+ - Out-of-range / duplicate / non-integer entries in `holdReels` are silently filtered.
113
+ - 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.
114
+
115
+ Exports `SpinOptions` from the package root.
116
+
117
+ - [#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`.
118
+
119
+ 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.
120
+
121
+ 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).
122
+
123
+ 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.
124
+
125
+ **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.
126
+
127
+ - [#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.
128
+
129
+ `.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.
130
+
131
+ 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.
132
+
133
+ - [#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.
134
+
135
+ 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.
136
+
137
+ ### Patch Changes
138
+
139
+ - [#93](https://github.com/schmooky/pixi-reels/pull/93) [`f111da8`](https://github.com/schmooky/pixi-reels/commit/f111da858ec0ca11a72ac389538b29f43f8c4262) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `Reel._replaceSymbol` now sets the canonical zIndex inline on every symbol activation.
140
+
141
+ Previously the activate path set `view.zIndex = 0` and relied on a follow-up `refreshZIndex()` call to apply the real formula `(symbolData.zIndex ?? 0) * 100 + arrayIndex`. All current callers happen to call `refreshZIndex` after, but the contract was fragile: any future caller that swapped a single symbol via the activate path would see the wrong layering until the next motion-wrap.
142
+
143
+ A new private helper `_computeSymbolZIndex(symbolId, index)` centralizes the formula and is used by both `refreshZIndex` (full rescan) and `_replaceSymbol` (single-symbol activate). OCCUPIED stubs receive `arrayIndex` directly, matching what `refreshZIndex` would assign.
144
+
145
+ No public API change. The fix unblocks future single-symbol swap APIs (e.g. a public `setSymbolAt`) without forcing every caller to remember to `refreshZIndex` afterwards.
146
+
147
+ - [#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.
148
+
149
+ 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.
150
+
151
+ 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.
152
+
153
+ - [#94](https://github.com/schmooky/pixi-reels/pull/94) [`6a5c8d1`](https://github.com/schmooky/pixi-reels/commit/6a5c8d192025c0746cab311491b2984173c15d30) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `SpineReelSymbol` one-shot animation promises (`playWin` / `playLanding` / `playOut`) no longer dangle when the track is hijacked.
154
+
155
+ Three previously-leaking scenarios now settle the returned promise instead of hanging forever:
156
+
157
+ - **Concurrent one-shots** — calling `playOut()` while `playWin()` is in flight resolves the prior `playWin` promise (its track was overwritten) before starting the new one.
158
+ - **`playBlur` mid-animation** — entering a SPIN that triggers blur while a win is still animating settles the win promise.
159
+ - **Listener leak** — back-to-back one-shots no longer accumulate stale listeners on the Spine state. Each new one-shot detaches the prior listener.
160
+
161
+ 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.
162
+
163
+ This unblocks reliable `await symbol.playWin()` patterns in win presenters and cascade orchestration.
164
+
165
+ - [#77](https://github.com/schmooky/pixi-reels/pull/77) [`265136a`](https://github.com/schmooky/pixi-reels/commit/265136a58cbcc4b289b6a070928345ca656c2cc1) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: stop reparenting recycled symbols on spotlight hide and always anchor `Reel._replaceSymbol` to its own container.
166
+
167
+ 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:
168
+
169
+ - `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.
170
+ - `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.
171
+
172
+ 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).
173
+
174
+ - [#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.
175
+
176
+ `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 })`.
177
+
178
+ The same-id early-return path inside `Reel._setSymbolAt` bypasses the deactivate/activate cycle, so the matching reset has been added there too.
179
+
180
+ No public API change. Subclasses that already cleared their own filter / transform state continue to work and just do a few redundant assignments.
181
+
182
+ - [#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.
183
+
184
+ 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.
185
+
186
+ ## 0.3.2
187
+
188
+ ### Patch Changes
189
+
190
+ - [`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.
191
+
192
+ ## 0.3.1
193
+
194
+ ### Patch Changes
195
+
196
+ - [`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.
197
+
198
+ ## 0.3.0
199
+
200
+ ### Minor Changes
201
+
202
+ - [#61](https://github.com/schmooky/pixi-reels/pull/61) [`28551ca`](https://github.com/schmooky/pixi-reels/commit/28551ca72e6cbc1e95984cf1b35e71bdb5f18d22) Thanks [@schmooky](https://github.com/schmooky)! - Add: per-reel geometry, MultiWays, big symbols, and expanding wilds.
203
+
204
+ - **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.
205
+ - **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.
206
+ - **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.
207
+ - **Expanding wilds:** unchanged from the existing pin API; reaffirmed via tests as a degenerate big-symbol case.
208
+
209
+ New events: `shape:changed`, `adjust:start`, `adjust:complete`, `pin:migrated`. They only fire on MultiWays slots — non-MultiWays event surfaces are unchanged.
210
+
211
+ New runtime: `reelSet.setShape()`, `reelSet.getSymbolFootprint()`, `reelSet.getVisibleGrid()`, `reelSet.isMultiWaysSlot`. New builder fluents: `.visibleRowsPerReel()`, `.reelPixelHeights()`, `.reelAnchor()`, `.multiways()`, `.pinMigrationDuration()`, `.pinMigrationEase()`. Pin gains optional `originRow`.
212
+
213
+ AdjustPhase animates the reshape: every visible symbol tweens its height + Y from the old shape to the new one over `pinMigrationDuration` ms with the configurable `pinMigrationEase`. Pin overlays tween in lock-step so a sticky wild visibly slides to its migrated row. Set `pinMigrationDuration(0)` for an instant snap.
214
+
215
+ Constraints: big symbols and MultiWays are mutually exclusive per slot in v1. Cascade mode + MultiWays throws at build.
216
+
217
+ **Breaking** (debug-only, not protected by semver but called out): `DebugSnapshot.visibleRows` widens from `number` to `number[]` so jagged shapes are representable. Adapt downstream code that deep-reads the snapshot.
218
+
219
+ ### Patch Changes
220
+
221
+ - [#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:
222
+
223
+ - `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.
224
+ - `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`.
225
+ - 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).
226
+
227
+ No new public API; behaviour for existing well-formed callers is unchanged.
228
+
229
+ ## 0.2.0
230
+
231
+ ### Minor Changes
232
+
233
+ - [`3fd806a`](https://github.com/schmooky/pixi-reels/commit/3fd806a31d76be5fc6ac7ff8e23852814c542e1a) - Backfill for three engine PRs merged without changesets after `0.1.0`:
234
+
235
+ - Cascade drop-in mechanic and anticipation recipe ([#51](https://github.com/schmooky/pixi-reels/issues/51)).
236
+ - Engine primitives: `CellPin`, `movePin`, and `reelSet.frame` exposure ([#52](https://github.com/schmooky/pixi-reels/issues/52)).
237
+ - `ReelSet.getCellBounds` for overlays, paylines, and hit areas ([#53](https://github.com/schmooky/pixi-reels/issues/53)).
238
+
239
+ All three are additive, so this bundles them into a single minor bump.
240
+
241
+ - [`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.
242
+
243
+ - `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.
244
+ - `Win` — one shape: `{ cells: SymbolPosition[]; value?: number; kind?: string; id?: number }`. Covers paylines, clusters, cascade pops, scatters.
245
+ - `dimLosers` (default 0.35 alpha) fades non-winning cells during each win; restored on `win:end`.
246
+ - `symbolAnim`: `'win'` (default, calls `playWin()`), a named spine animation, or `(symbol, cell, win) => Promise<void>` for a custom callback.
247
+ - 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.
248
+ - Cascades: call `presenter.show([{ cells: winners }])` from `runCascade`'s `onWinnersVanish` hook — same API.
249
+ - Helper: `sortByValueDesc` exported for convenience.
250
+ - Types: `Win`, `SymbolPosition` (canonicalised to `config/types`, re-exported from events).
251
+ - 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.
252
+
253
+ No existing API is changed or removed.
254
+
255
+ ### Patch Changes
256
+
257
+ - [`7792142`](https://github.com/schmooky/pixi-reels/commit/779214217bb341cfb66f2db74616b2e8608893b9) - Fix: Two `AnimatedSpriteSymbol` bugs that only manifest on symbols with non-trivial win animations:
258
+
259
+ - `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.
260
+ - `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).
@@ -0,0 +1,85 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in pixi-reels. This file covers the mechanics of contributing. For the house style and load-bearing design constraints, read [`AGENTS.md`](./AGENTS.md) first — several of those rules are enforced by lint guards and pre-commit hooks, so they'll block a merge if you break them.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ git clone https://github.com/schmooky/pixi-reels.git
9
+ cd pixi-reels
10
+ pnpm install
11
+ pnpm --filter pixi-reels test # vitest + typecheck
12
+ pnpm site:dev # docs site at http://localhost:4321
13
+ pnpm --filter classic-spin dev # classic 5×3 example
14
+ ```
15
+
16
+ Node 20+ is required. The repo uses pnpm workspaces.
17
+
18
+ ## Workflow
19
+
20
+ 1. **Branch from `main`.** Name it something human like `fix/stop-phase-slicing` or `feat/expanding-wilds`. Long-lived preview branches use `v*` (e.g. `v0.2`) and publish [snapshot releases](./README.md#snapshot-releases) automatically.
21
+
22
+ 2. **Make focused changes.** One logical change per PR. If you notice a second bug while fixing the first, open a second PR.
23
+
24
+ 3. **Run the test suite:** `pnpm test`. This runs the lint guards and all vitest suites. They must pass before review.
25
+
26
+ 4. **If your change ships user-visible behavior in a publishable package, add a changeset:**
27
+
28
+ ```bash
29
+ pnpm changeset
30
+ ```
31
+
32
+ Pick the affected packages and the bump kind (`patch` / `minor` / `major`) and commit the resulting `.changeset/*.md` file. Changes to private apps (`@pixi-reels/site`, any `examples/*`) don't need a changeset — those are deployed, not published.
33
+
34
+ 5. **Open a PR.** The template asks for a summary, a test plan, and confirmation that a changeset was added.
35
+
36
+ ## What "good" looks like in this repo
37
+
38
+ - **Small, readable diffs.** Don't sneak in refactors that weren't asked for. If a refactor is needed for a fix, do it in a separate commit in the same PR with a clear message.
39
+ - **Comments explain "why", not "what".** The code already says what it does; comments should capture the non-obvious reason a line exists.
40
+ - **No emoji in source, commit messages, changelog entries, or UI strings.** The fancy-Unicode lint guard enforces this. Use ASCII punctuation.
41
+ - **No default exports.** Always named. Tree-shaking and auto-imports both depend on this.
42
+ - **`.js` extensions in imports.** Even from `.ts` sources — this is required by Node ESM resolution of the published build.
43
+
44
+ ## GitHub Actions are pinned to SHAs
45
+
46
+ Every third-party action in `.github/workflows/` is pinned to a full commit SHA with the human version as a trailing comment, e.g.
47
+
48
+ ```yaml
49
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
50
+ ```
51
+
52
+ This is a supply-chain hardening step: a moving tag like `@v4` could be repointed at a malicious commit by a compromised maintainer account, but a SHA cannot. Dependabot keeps the SHAs fresh. Approve those `chore(ci)` PRs like any other dependency bump.
53
+
54
+ If you are adding a new action, resolve its major-version tag to a SHA with `git ls-remote` and commit both the SHA and the version comment in the same `uses:` line.
55
+
56
+ ## Releases
57
+
58
+ Every published package is versioned and shipped by [changesets](https://github.com/changesets/changesets) on merge to `main`. The full flow (and the snapshot release workflow for `v*` branches) is documented in the [Releases section of the README](./README.md#releases).
59
+
60
+ TL;DR:
61
+
62
+ - Your PR should include a `.changeset/*.md` file if it ships user-visible changes in a publishable package.
63
+ - After merge, a `chore: version packages` PR opens automatically. Merging that PR publishes the affected packages to npm.
64
+ - Branch previews publish to npm under a per-branch dist-tag (e.g. `pixi-reels@v0-2`) so reviewers can install work-in-progress versions without waiting for a merge.
65
+
66
+ ## Reporting bugs and proposing features
67
+
68
+ Use the issue forms:
69
+
70
+ - [Bug report](./.github/ISSUE_TEMPLATE/bug_report.yml)
71
+ - [Feature request](./.github/ISSUE_TEMPLATE/feature_request.yml)
72
+
73
+ For security issues, follow [`SECURITY.md`](./SECURITY.md) and do not open a public issue.
74
+
75
+ ## Crediting contributors
76
+
77
+ This project follows the [all-contributors](https://allcontributors.org) spec. Anyone who helps — code, docs, design, reviews, bug reports, ideas — gets credited in the README.
78
+
79
+ To add someone (or yourself), comment on any issue or PR:
80
+
81
+ ```
82
+ @all-contributors please add @their-github-handle for code, doc
83
+ ```
84
+
85
+ The bot opens a PR updating `.all-contributorsrc` and the contributors block in the README.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 schmooky and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ This repository bundles third-party runtimes and libraries (PixiJS, GSAP, the
24
+ Spine Runtime, and others) governed by their own license terms. See each
25
+ package's node_modules or upstream project for those terms. In particular, the
26
+ Spine Runtime (@esotericsoftware/spine-*) is licensed under the Spine Runtimes
27
+ License Agreement and requires users to hold a valid Spine Editor license when
28
+ integrating it into their own products.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # pixi-reels
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pixi-reels?color=cb3837&logo=npm)](https://www.npmjs.com/package/pixi-reels)
4
+ [![npm downloads](https://img.shields.io/npm/dm/pixi-reels?color=cb3837&logo=npm)](https://www.npmjs.com/package/pixi-reels)
5
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/pixi-reels?label=gzip)](https://bundlephobia.com/package/pixi-reels)
6
+ [![CI](https://github.com/schmooky/pixi-reels/actions/workflows/ci.yml/badge.svg)](https://github.com/schmooky/pixi-reels/actions/workflows/ci.yml)
7
+ [![Release](https://github.com/schmooky/pixi-reels/actions/workflows/release.yml/badge.svg)](https://github.com/schmooky/pixi-reels/actions/workflows/release.yml)
8
+ [![CodeQL](https://github.com/schmooky/pixi-reels/actions/workflows/codeql.yml/badge.svg)](https://github.com/schmooky/pixi-reels/actions/workflows/codeql.yml)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
10
+ [![PixiJS v8](https://img.shields.io/badge/PixiJS-v8-e91e63)](https://pixijs.com/)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
12
+
13
+ A slot machine reel engine for [PixiJS v8](https://pixijs.com/). Fluent builder, typed events, and the weighty spin+stop feel modeled on real-money games — in about 35 kB gzipped.
14
+
15
+ ```bash
16
+ pnpm add pixi-reels pixi.js gsap
17
+ ```
18
+
19
+ ```ts
20
+ import { Application } from 'pixi.js';
21
+ import { ReelSetBuilder, SpriteSymbol, SpeedPresets } from 'pixi-reels';
22
+
23
+ const app = new Application();
24
+ await app.init({ width: 900, height: 540, background: '#0a0d14' });
25
+ document.body.appendChild(app.canvas);
26
+
27
+ const reelSet = new ReelSetBuilder()
28
+ .reels(5).visibleSymbols(3).symbolSize(140, 140)
29
+ .symbols((r) => {
30
+ r.register('cherry', SpriteSymbol, { textures: { cherry: cherryTex } });
31
+ r.register('seven', SpriteSymbol, { textures: { seven: sevenTex } });
32
+ r.register('bar', SpriteSymbol, { textures: { bar: barTex } });
33
+ })
34
+ .weights({ cherry: 40, seven: 10, bar: 20 })
35
+ .speed('normal', SpeedPresets.NORMAL)
36
+ .speed('turbo', SpeedPresets.TURBO)
37
+ .ticker(app.ticker)
38
+ .build();
39
+
40
+ app.stage.addChild(reelSet);
41
+
42
+ // Kick off the spin, tell it where to land, await the bounce.
43
+ const spin = reelSet.spin();
44
+ reelSet.setResult(await fetchSpinFromServer());
45
+ const { symbols } = await spin;
46
+ ```
47
+
48
+ ## What it does
49
+
50
+ - **Spin lifecycle** — `START -> SPIN -> ANTICIPATION -> STOP` phases, each pluggable.
51
+ - **Weighty stops** — the reel carries momentum through the target frame, snaps, then bounces. No floaty ease-in deceleration.
52
+ - **Speed modes** — Normal / Turbo / SuperTurbo built in, or register your own profile.
53
+ - **Skip / slam-stop** — second tap of the spin button immediately lands the reels on target.
54
+ - **Win spotlight** — dim non-winners, promote winning symbols above the mask, cycle lines.
55
+ - **Symbol plugins** — SpriteSymbol, AnimatedSpriteSymbol, SpineSymbol, or implement `ReelSymbol`.
56
+ - **Frame middleware** — intercept the symbol generator (e.g. "no triples", multiplier injection).
57
+ - **Object pooling** — zero-allocation spinning via `ObjectPool<T>`.
58
+ - **Typed events** — `spin:start`, `spin:reelLanded`, `speed:changed`, `spotlight:end`, ...
59
+ - **Headless testing** — `createTestReelSet` + `FakeTicker` run the full lifecycle in Node.
60
+ - **Debug mode** — `enableDebug(reelSet)` exposes JSON and ASCII snapshots on `window`.
61
+
62
+ ## Docs
63
+
64
+ The docs site lives in [`apps/site`](apps/site/). Run it locally:
65
+
66
+ ```bash
67
+ pnpm site:dev # http://localhost:4321
68
+ ```
69
+
70
+ You'll find:
71
+
72
+ - **Guides** — getting started, spin lifecycle, symbols, speed modes, win animations, debugging.
73
+ - **Recipes** — small how-tos for common mechanics (walking wilds, sticky wilds, cascade, mystery reveal, hold & win, ...). Each ships with a live mini-demo.
74
+ - **Demos** — full mechanic sandboxes with cheat panels. One click forces a scatter, a near-miss, a guaranteed jackpot.
75
+ - **Sandbox** — in-browser TypeScript playground; edit the file, hit Run, reels rebuild.
76
+ - **Studio** — bring-your-own-assets workbench. Drop in sprites or Spine bundles, wire them to symbol ids, edit builder code, share the result via a password-protected link (see [`apps/share-api`](apps/share-api/) for the relay).
77
+ - **Wiki** — API reference.
78
+
79
+ ## Examples
80
+
81
+ Runnable apps in [`examples/`](examples/):
82
+
83
+ | Example | What it shows | Run |
84
+ |------------------|------------------------------------------------------------|----------------------------------------|
85
+ | `classic-spin` | 5x3 line-pay slot with Spine symbols and speed toggle | `pnpm --filter classic-spin dev` |
86
+ | `cascade-tumble` | 6x5 tumble mechanic with win spotlight between stages | `pnpm --filter cascade-tumble dev` |
87
+ | `hold-and-win` | 5x3 base game + respin bonus with locking coins | `pnpm --filter hold-and-win dev` |
88
+ | `sandbox` | Single editable TS file, HMR rebuild | `pnpm --filter sandbox dev` |
89
+
90
+ ## Core API at a glance
91
+
92
+ ```ts
93
+ reelSet.spin(): Promise<SpinResult> // Start spinning
94
+ reelSet.setResult(symbols: string[][] | ColumnTarget[]) // Pass the target grid (triggers the stop). Use frame[col][-1] (or { bufferAbove: [...] }) to prefill cells above the visible window — see /recipes/buffer-indexing-cheatsheet/.
95
+ reelSet.setAnticipation([3, 4]) // Slow reels 3+4 before their landing
96
+ reelSet.setStopDelays([0, 140, 280, 600, 1100]) // Override per-reel stop stagger
97
+ reelSet.skip() // Slam-stop
98
+ reelSet.setSpeed('turbo') // Switch speed profile
99
+ reelSet.spotlight.show(positions, opts) // One-shot win highlight
100
+ reelSet.spotlight.cycle(lines, opts) // Cycle through win lines
101
+ reelSet.events.on('spin:reelLanded', (i, s) => {/* ... */})
102
+ reelSet.destroy() // Full teardown
103
+ ```
104
+
105
+ Full reference: `/wiki/` on the docs site.
106
+
107
+ ## Spine symbols (optional)
108
+
109
+ pixi-reels ships a Spine adapter on a separate subpath so the runtime tree-shakes out when you don't need it:
110
+
111
+ ```ts
112
+ import { SpineReelSymbol } from 'pixi-reels/spine';
113
+
114
+ r.register('wild', SpineReelSymbol, {
115
+ spineMap: { wild: { skeleton: 'wildData', atlas: 'myAtlas' } },
116
+ autoPlayBlur: true, // plays `blur` during spin
117
+ autoPlayLanding: true, // plays `landing` on reel stop
118
+ });
119
+ ```
120
+
121
+ Install the peer: `pnpm add @esotericsoftware/spine-pixi-v8`.
122
+
123
+ ## Debug mode
124
+
125
+ Handy for development and essential when an AI agent needs to inspect reel state without parsing a canvas:
126
+
127
+ ```ts
128
+ import { enableDebug } from 'pixi-reels';
129
+ enableDebug(reelSet);
130
+ ```
131
+
132
+ In the browser console (or via Playwright / an agent's `eval`):
133
+
134
+ ```
135
+ __PIXI_REELS_DEBUG.log() // ASCII grid + state snapshot
136
+ __PIXI_REELS_DEBUG.snapshot() // Full JSON state
137
+ __PIXI_REELS_DEBUG.trace() // Log every domain event as it fires
138
+ ```
139
+
140
+ ## Architecture
141
+
142
+ ```
143
+ ReelSetBuilder --builds--> ReelSet
144
+ |- SpinController ..... orchestrates the phases per reel
145
+ |- SpeedManager ....... named profiles + live switching
146
+ |- SymbolSpotlight .... win animations
147
+ |- ReelViewport ....... masked + unmasked containers
148
+ '- Reel[] ............. one per column
149
+ |- ReelSymbol[] .. SpriteSymbol / AnimatedSpriteSymbol / SpineSymbol
150
+ |- ReelMotion .... y displacement + wrap
151
+ '- StopSequencer . target-frame consumption
152
+ ```
153
+
154
+ Single ticker, no circular deps, no default exports, tree-shakes cleanly.
155
+
156
+ ## Peer dependencies
157
+
158
+ - `pixi.js` ^8.17.0
159
+ - `gsap` ^3.14.0
160
+ - `@esotericsoftware/spine-pixi-v8` ^4.2.108 _(optional — only if you use `SpineReelSymbol`)_
161
+
162
+ ## Contributing
163
+
164
+ PRs welcome. [CONTRIBUTING.md](./CONTRIBUTING.md) covers the workflow, changesets, and the handful of style rules the lint guards enforce.
165
+
166
+ ## License
167
+
168
+ MIT.
@@ -0,0 +1,88 @@
1
+ import { Container as e } from "pixi.js";
2
+ //#region src/symbols/ReelSymbol.ts
3
+ var t = class {
4
+ view;
5
+ _symbolId = "";
6
+ _isDestroyed = !1;
7
+ constructor() {
8
+ this.view = new e();
9
+ }
10
+ get symbolId() {
11
+ return this._symbolId;
12
+ }
13
+ get isDestroyed() {
14
+ return this._isDestroyed;
15
+ }
16
+ activate(e) {
17
+ this._symbolId = e, this.view.visible = !0, this.view.alpha = 1, this.view.scale.set(1, 1), this.view.rotation = 0, this.view.filters = null, this.view.zIndex = 0, this.onActivate(e);
18
+ }
19
+ deactivate() {
20
+ this.stopAnimation(), this.onDeactivate(), this._symbolId = "", this.view.visible = !1, this.view.alpha = 1, this.view.scale.set(1, 1), this.view.rotation = 0, this.view.filters = null, this.view.zIndex = 0;
21
+ }
22
+ reset() {
23
+ this.deactivate();
24
+ }
25
+ destroy() {
26
+ this._isDestroyed ||= (this.stopAnimation(), this.onDeactivate(), this.onDestroy(), this.view.destroyed || this.view.destroy({ children: !0 }), !0);
27
+ }
28
+ onDestroy() {}
29
+ onReelSpinStart() {}
30
+ onReelSpinEnd() {}
31
+ onReelLanded() {}
32
+ }, n = null;
33
+ async function r() {
34
+ try {
35
+ n = (await import("@esotericsoftware/spine-pixi-v8")).Spine;
36
+ } catch {}
37
+ }
38
+ r();
39
+ var i = class extends t {
40
+ _spine = null;
41
+ _skeletonDataMap;
42
+ _idleAnimation;
43
+ _winAnimation;
44
+ _defaultSkin;
45
+ _winResolve = null;
46
+ _currentSkeletonKey = "";
47
+ constructor(e) {
48
+ if (super(), !n) throw Error("SpineSymbol requires @esotericsoftware/spine-pixi-v8 to be installed. Install it with: npm install @esotericsoftware/spine-pixi-v8");
49
+ this._skeletonDataMap = e.skeletonDataMap, this._idleAnimation = e.idleAnimation ?? "idle", this._winAnimation = e.winAnimation ?? "win", this._defaultSkin = e.defaultSkin ?? "default";
50
+ }
51
+ onActivate(e) {
52
+ let t = this._skeletonDataMap[e];
53
+ t && (this._currentSkeletonKey !== e && (this._spine && (this.view.removeChild(this._spine), this._spine.destroy()), this._spine = new n({ skeletonData: t }), this.view.addChild(this._spine), this._currentSkeletonKey = e), this._spine.skeleton.data.findSkin(this._defaultSkin) && (this._spine.skeleton.setSkinByName(this._defaultSkin), this._spine.skeleton.setSlotsToSetupPose()), this._spine.skeleton.data.findAnimation(this._idleAnimation) && this._spine.state.setAnimation(0, this._idleAnimation, !0));
54
+ }
55
+ onDeactivate() {
56
+ if (this._spine && (this._spine.state.clearListeners(), this._spine.state.clearTracks()), this._winResolve) {
57
+ let e = this._winResolve;
58
+ this._winResolve = null, e();
59
+ }
60
+ }
61
+ async playWin() {
62
+ if (this._spine && this._spine.skeleton.data.findAnimation(this._winAnimation)) return new Promise((e) => {
63
+ this._winResolve = e;
64
+ let t = this._spine.state.setAnimation(0, this._winAnimation, !1);
65
+ this._spine.state.addListener({ complete: (n) => {
66
+ n === t && (this._spine.state.clearListeners(), this._spine.skeleton.data.findAnimation(this._idleAnimation) && this._spine.state.setAnimation(0, this._idleAnimation, !0), this._winResolve = null, e());
67
+ } });
68
+ });
69
+ }
70
+ stopAnimation() {
71
+ this._spine && (this._spine.state.clearListeners(), this._spine.skeleton.data.findAnimation(this._idleAnimation) && this._spine.state.setAnimation(0, this._idleAnimation, !0), this._winResolve &&= (this._winResolve(), null));
72
+ }
73
+ resize(e, t) {
74
+ if (!this._spine) return;
75
+ let n = this._spine.getBounds();
76
+ n.width > 0 && n.height > 0 && this._spine.scale.set(e / n.width, t / n.height);
77
+ }
78
+ onDestroy() {
79
+ if (this._spine &&= (this._spine.state.clearListeners(), this._spine.destroy(), null), this._winResolve) {
80
+ let e = this._winResolve;
81
+ this._winResolve = null, e();
82
+ }
83
+ }
84
+ };
85
+ //#endregion
86
+ export { t as n, i as t };
87
+
88
+ //# sourceMappingURL=SpineSymbol-CfVrc5Pk.js.map