pixi-reels 3.0.0 → 4.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 (112) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/README.md +10 -0
  3. package/dist/PhaseCardSymbol-Ct9bCE4I.cjs +2 -0
  4. package/dist/PhaseCardSymbol-Ct9bCE4I.cjs.map +1 -0
  5. package/dist/PhaseCardSymbol-bVa7GpDn.js +213 -0
  6. package/dist/PhaseCardSymbol-bVa7GpDn.js.map +1 -0
  7. package/dist/ReelSymbol-B5QBhkmw.cjs.map +1 -1
  8. package/dist/ReelSymbol-C7Pd7BeO.js.map +1 -1
  9. package/dist/SpinMetrics-9nsu5jI6.cjs +2 -0
  10. package/dist/SpinMetrics-9nsu5jI6.cjs.map +1 -0
  11. package/dist/SpinMetrics-qPcdD4Hw.js +273 -0
  12. package/dist/SpinMetrics-qPcdD4Hw.js.map +1 -0
  13. package/dist/board/HoldAndWinBoard.d.ts +2 -2
  14. package/dist/board/HoldAndWinBoard.d.ts.map +1 -1
  15. package/dist/board/HoldAndWinBuilder.d.ts +19 -9
  16. package/dist/board/HoldAndWinBuilder.d.ts.map +1 -1
  17. package/dist/board/HwTypes.d.ts +9 -1
  18. package/dist/board/HwTypes.d.ts.map +1 -1
  19. package/dist/config/phaseProfile.d.ts +43 -0
  20. package/dist/config/phaseProfile.d.ts.map +1 -0
  21. package/dist/config/types.d.ts +169 -4
  22. package/dist/config/types.d.ts.map +1 -1
  23. package/dist/core/Reel.d.ts +18 -11
  24. package/dist/core/Reel.d.ts.map +1 -1
  25. package/dist/core/ReelSet.d.ts +223 -1
  26. package/dist/core/ReelSet.d.ts.map +1 -1
  27. package/dist/core/ReelSetBuilder.d.ts.map +1 -1
  28. package/dist/core/expand.d.ts +124 -0
  29. package/dist/core/expand.d.ts.map +1 -0
  30. package/dist/debug/DebugPlaque.d.ts +119 -0
  31. package/dist/debug/DebugPlaque.d.ts.map +1 -0
  32. package/dist/debug/SpinMetrics.d.ts +191 -0
  33. package/dist/debug/SpinMetrics.d.ts.map +1 -0
  34. package/dist/debug/WinLines.d.ts +78 -0
  35. package/dist/debug/WinLines.d.ts.map +1 -0
  36. package/dist/debug/debug.d.ts +4 -0
  37. package/dist/debug/debug.d.ts.map +1 -1
  38. package/dist/debug/debugOverlay.d.ts +45 -16
  39. package/dist/debug/debugOverlay.d.ts.map +1 -1
  40. package/dist/debug/index.d.ts +10 -0
  41. package/dist/debug/index.d.ts.map +1 -0
  42. package/dist/debug/roundRectPath.d.ts +19 -0
  43. package/dist/debug/roundRectPath.d.ts.map +1 -0
  44. package/dist/debug-BjxZgftj.cjs +4 -0
  45. package/dist/debug-BjxZgftj.cjs.map +1 -0
  46. package/dist/{debug-DnrD1PHb.js → debug-CC62Ro-c.js} +1010 -813
  47. package/dist/debug-CC62Ro-c.js.map +1 -0
  48. package/dist/debug.cjs +4 -0
  49. package/dist/debug.cjs.map +1 -0
  50. package/dist/debug.js +797 -0
  51. package/dist/debug.js.map +1 -0
  52. package/dist/events/ReelEvents.d.ts +51 -0
  53. package/dist/events/ReelEvents.d.ts.map +1 -1
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.ts +7 -5
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +232 -398
  59. package/dist/index.js.map +1 -1
  60. package/dist/pool/ObjectPool.d.ts +8 -0
  61. package/dist/pool/ObjectPool.d.ts.map +1 -1
  62. package/dist/snapshot/SpinTextureCache.d.ts +34 -9
  63. package/dist/snapshot/SpinTextureCache.d.ts.map +1 -1
  64. package/dist/snapshot/StaticSpinSymbol.d.ts +1 -1
  65. package/dist/snapshot/StaticSpinSymbol.d.ts.map +1 -1
  66. package/dist/speed/SpeedManager.d.ts +25 -2
  67. package/dist/speed/SpeedManager.d.ts.map +1 -1
  68. package/dist/spin/SpinController.d.ts +119 -5
  69. package/dist/spin/SpinController.d.ts.map +1 -1
  70. package/dist/spin/phases/AdjustPhase.d.ts +2 -2
  71. package/dist/spin/phases/AdjustPhase.d.ts.map +1 -1
  72. package/dist/spin/phases/AnticipationPhase.d.ts +17 -10
  73. package/dist/spin/phases/AnticipationPhase.d.ts.map +1 -1
  74. package/dist/spin/phases/CascadeDropInPhase.d.ts +2 -2
  75. package/dist/spin/phases/CascadeDropInPhase.d.ts.map +1 -1
  76. package/dist/spin/phases/CascadeFallPhase.d.ts +2 -2
  77. package/dist/spin/phases/CascadeFallPhase.d.ts.map +1 -1
  78. package/dist/spin/phases/CascadePlacePhase.d.ts +2 -2
  79. package/dist/spin/phases/CascadePlacePhase.d.ts.map +1 -1
  80. package/dist/spin/phases/PhaseFactory.d.ts +31 -5
  81. package/dist/spin/phases/PhaseFactory.d.ts.map +1 -1
  82. package/dist/spin/phases/ReelPhase.d.ts +56 -2
  83. package/dist/spin/phases/ReelPhase.d.ts.map +1 -1
  84. package/dist/spin/phases/SpinPhase.d.ts +2 -2
  85. package/dist/spin/phases/SpinPhase.d.ts.map +1 -1
  86. package/dist/spin/phases/StartPhase.d.ts +9 -8
  87. package/dist/spin/phases/StartPhase.d.ts.map +1 -1
  88. package/dist/spin/phases/StopPhase.d.ts +9 -8
  89. package/dist/spin/phases/StopPhase.d.ts.map +1 -1
  90. package/dist/spin/phases/steps.d.ts +21 -4
  91. package/dist/spin/phases/steps.d.ts.map +1 -1
  92. package/dist/spine/SpineReelSymbol.d.ts +73 -17
  93. package/dist/spine/SpineReelSymbol.d.ts.map +1 -1
  94. package/dist/spine/index.d.ts +1 -1
  95. package/dist/spine/index.d.ts.map +1 -1
  96. package/dist/spine.cjs +1 -1
  97. package/dist/spine.cjs.map +1 -1
  98. package/dist/spine.js +56 -39
  99. package/dist/spine.js.map +1 -1
  100. package/dist/symbols/CardSymbol.d.ts +10 -0
  101. package/dist/symbols/CardSymbol.d.ts.map +1 -1
  102. package/dist/symbols/ReelSymbol.d.ts.map +1 -1
  103. package/dist/symbols/SpriteSymbol.d.ts +3 -0
  104. package/dist/symbols/SpriteSymbol.d.ts.map +1 -1
  105. package/dist/symbols/SymbolFactory.d.ts +5 -1
  106. package/dist/symbols/SymbolFactory.d.ts.map +1 -1
  107. package/dist/testing.cjs +1 -1
  108. package/dist/testing.js +12 -12
  109. package/package.json +17 -3
  110. package/dist/debug-Cou5DWoL.cjs +0 -4
  111. package/dist/debug-Cou5DWoL.cjs.map +0 -1
  112. package/dist/debug-DnrD1PHb.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,189 @@
1
1
  # pixi-reels
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: a `pixi-reels/debug` subpath drawn with `pixi-silk` (smooth, exact vector graphics), with a spin timeline, a metrics plaque, a phase-aware hud, `SpinMetrics`, `DebugPlaque` and `roundRectPath`. Move: `debugOverlay` and `OVERLAY_LABEL` from `pixi-reels` to `pixi-reels/debug`. Remove: `showMask()` and `overlay()` from the `enableDebug()` window hook. Breaking only for code that imports the overlay or calls those two; `debugSnapshot`, `debugGrid`, the frame recorder and `enableDebug` stay where they were.
8
+
9
+ **Opt in with one import.** `pixi-silk` is an optional peer dependency, like the Spine runtime: install it next to `pixi-reels` (`pnpm add -D pixi-silk`) and import from `pixi-reels/debug`. A build that never imports the subpath never needs it, and the main entry no longer carries any drawing code for debugging. pixi-silk 0.1 draws through WebGL: on a WebGPU renderer its shapes draw nothing (debug on `preference: 'webgl'`), and building one needs `DOMAdapter.createCanvas()`, so a headless test installs a canvas stand-in (the migration guide shows it).
10
+
11
+ ```ts
12
+ import { debugOverlay } from 'pixi-reels/debug';
13
+
14
+ const overlay = debugOverlay(reelSet, { layers: 'all', live: true, ticker: app.ticker });
15
+ ```
16
+
17
+ **The overlay is drawn with pixi-silk.** Every layer (`mask`, `cells`, `buffers`, `thresholds`, `axis`, `feed`, `bounds`, `blocks`, `pins`) is a `SilkGraphics`, so outlines, dashes and arrows stay exact at the scale the reel set is fitted to instead of faceting or crawling. Layer objects keep their `pixi-reels:debugOverlay:<layer>` labels.
18
+
19
+ **New layers.** `metrics` is a plaque under the mask with the current round: its length, time to the first stop, the landing order and the largest gap between landings, every tease window, the last skip press (mode, reels, time), how many symbols were swapped into cells, event count, fps, pins, spotlight, wins and cascade chains. `timeline` is a panel beside it: every reel's phases for the round on one time axis, colored like `PhaseCardSymbol`, with the reel's speed traced on top, a tick where it was asked to stop, a dot where it landed, and a dashed line at every skip press. `hud` is now one plaque with a phase swatch and a speed meter per reel row. `describe()` also returns the `round` the panels show.
20
+
21
+ **`SpinMetrics`.** The recorder behind those panels, usable on its own: `new SpinMetrics(reelSet, { ticker })` keeps the last rounds (`current`, `rounds`, `totals`, `snapshot()`), each with per-reel phase spans, stop, landing and settle times, tease windows, skip presses and, given a ticker, a speed trace. Pass one to `debugOverlay(reelSet, { metrics })` and the panels show the rounds from before the overlay existed.
22
+
23
+ **`DebugPlaque`.** Rows of monospace text on a smooth rounded plate, with optional title, swatches (legends) and meters, sized from the character count so it is exact and cheap enough to update every frame. `plaque.text = '...'` works like a `Text`, and a plain `destroy()` frees its plate and text. Every readout on the docs site's recipes now uses it.
24
+
25
+ **`__PIXI_REELS_DEBUG.metrics()`.** `enableDebug()` records rounds from the moment it is called (one recorder per reel set, however often it is called), and `metrics()` hands them back as plain JSON, so an agent can ask how long a tease lasted or which reel landed last without a canvas.
26
+
27
+ **Removed from the window hook.** `showMask(enabled)` drew with plain `Graphics` into the masked container, under the spotlight; `debugOverlay(reelSet, { layers: ['mask'] })` draws the same mask box and per-reel rects above everything. `overlay(options)` is `debugOverlay(reelSet, options)` from `pixi-reels/debug`.
28
+
29
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Breaking: a Hold & Win board's landing wave steps by the active speed profile's `stopDelay`, the way reels stop `stopDelay` apart on a reel set. Each cell spins `(reel + cell) * stopDelay` ms on top of the profile's `minimumSpinTime`, so the wave follows `board.setSpeed()`: a turbo profile with `stopDelay: 0` lands every cell together, and no `stagger` function has to branch on profile names to do it.
30
+
31
+ The builder's default `'normal'` profile sets `stopDelay: 70`, so a board on defaults keeps its 70 ms wave. A board that registers its own profiles and no `.stagger()` now waves by their `stopDelay`: 140 ms a step on the NORMAL preset, none on TURBO and SUPER_TURBO. To keep the old fixed wave, `.stagger((reel, cell) => (reel + cell) * 70)`.
32
+
33
+ `HoldAndWinBuilder.stagger(fn)` still replaces the wave, and `fn` now also gets the speed's profile: `.stagger((reel, cell, speed, profile) => reel * profile.stopDelay)` is a column-by-column wave that scales with the speed. Its type is exported as `HwStagger`.
34
+
35
+ ### Minor Changes
36
+
37
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: reel sets that grow sideways at runtime, for "infinity reels" style mechanics. `reelSet.addReels(count, { visibleCells, initialFrame })` appends reels after the last one (laid out, masked and pooled like the rest), `reelSet.removeReels(count?)` destroys the last ones (every added one without a count, back to the board the builder made) and gives the memory back, and `reelSet.expand({ columns, step, onStepAdded, onStepLanded, anticipation, signal })` runs the whole chain from one server result: add a step's reels, spin only them, land them on their columns, repeat.
38
+
39
+ ```ts
40
+ const spin = reelSet.spin();
41
+ reelSet.setResult(res.columns.slice(0, 5));
42
+ await spin;
43
+ await reelSet.expand({
44
+ columns: res.columns.slice(5), // the server decides how many
45
+ step: 1, // or 2, or (info) => ...
46
+ onStepAdded: () => camera.panTo(reelSet.viewport.maskWidth), // the board's new width
47
+ onStepLanded: (step) => showWays(step.result.symbols),
48
+ });
49
+ reelSet.removeReels(); // next round: back to the board the builder made
50
+ ```
51
+
52
+ The steps are one round: pin turns and `'eval'` pins are not spent per step, and start and stop delays stagger by a reel's place in its step rather than its index, so reel 40 does not wait forty delays. Skip works a step at a time: `skipSpin()` frees the step in flight (slam or quicken, per `skipMode()`) and speeds up the rest of the round once (the fastest profile; a cascade-mode expansion lands its later steps at once, the way one press ends a cascade); a press between steps, while a hook runs and nothing spins, carries to the next step instead of being dropped (both `skipSpin()` and `requestSkip()`); a step's reels join a `setReelGroups()` layout as one trailing group; `anticipation` takes `AnticipationOptions`, so a step's tease can be protected from the first press (`{ protect: 'once' }`). Aborting `signal` fast-forwards rather than cancels: everything still to come lands in one step (`AbortSignal.abort()` lands a whole expansion at once, for quick spin, autoplay and tests). On a MultiWays set each column's length is that reel's shape, so the ways multiply as the board grows (eight reels of seven cells is 5,764,801 ways). A step that a big symbol would straddle is widened to take the block whole, so a 2-wide block arriving in a one-reel step adds two. Columns, symbol ids, shapes, blocks and an `anticipation` object are validated before the first reel is added, so a bad result throws on an untouched board; an `anticipation` function's answer is checked before its step's reels exist. An abort listener never outlives the expansion on the caller's signal, even when the set is destroyed mid-step.
53
+
54
+ Added reels are built the way the builder built the others: a reel without a count copies the last reel as the board is now, `reelExtents()` boxes and a runtime `setCurve()` carry over, and a `setShape()` still pending lands them at their own shape. A named `setDropOrder('ltr' | 'rtl' | 'all')` now counts over the reels that spin rather than the reel count when it was set, so it covers added reels and orders an `expand()` step by place. `removeReels()` hands the removed reels' symbols back to the pool, which stops any animation still playing on them, and destroys anything a game added to those reels' containers.
55
+
56
+ Rows can grow too: `reelSet.addRows(count, { cells })` makes every reel `count` rows taller at its own cell size (the viewport and mask grow with it, `cells` is what the new rows show), and `removeRows(count?)` goes back. Inside an expansion they run between steps, in `onStepLanded`, and the reels after it may arrive at the new height. Big symbols stay whole; one that no longer fits a shorter strip is random-filled. Not on MultiWays, where a reel's rows are its shape.
57
+
58
+ New events: `reels:added`, `reels:removed`, `rows:added`, `rows:removed`, `expand:start`, `expand:stepAdded`, `expand:stepLanded`, `expand:complete`, and `expand:end` (`'complete'` or `'failed'`, always last, the place to put a UI back). `reelSet.isExpanding` is `true` from `expand:start` on, so a `spin:*` listener can tell an expansion step from a new round, and `false` again by `expand:complete` and `expand:end`, so a listener there can start the next round. `Reel.reelCount` follows the board. `SpinMetrics` and `debugOverlay` follow reels added and removed after they were created, and the overlay redraws when the rows change.
59
+
60
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `SpineReelSymbol.playOneShot(animation, { track, then })`, the awaitable one-shot that `playWin()`, `playLanding()` and `playOut()` are built on. It resolves when the animation completes and leaves the track on the idle loop (`then: 'idle'`, the default on track 0), on its last frame (`'hold'`), or empty (`'clear'`, the default on any other track, so an overlay mixes out and the tracks under it show through). Use it where `playOnTrack(track, name, false)` gave no way to know the one-shot had ended and never went back to idle; `playOnTrack` stays for loops and raw track entries.
61
+
62
+ ```ts
63
+ await coin.playOneShot('collect'); // track 0, then idle
64
+ await coin.playOneShot('react_u', { track: 1 }); // overlay, cleared after
65
+ await coin.playOneShot('explode', { then: 'hold' }); // stays on its last frame
66
+ ```
67
+
68
+ One-shots on different tracks run side by side: each track settles only its own promise, and `stopAnimation()` settles all of them and clears the overlays, including one that ended holding its last frame (`then: 'hold'`). `OneShotOptions` and `OneShotEnd` are exported from `pixi-reels/spine`.
69
+
70
+ Fix: a `playWin()` / `playLanding()` / `playOut()` promise no longer waits forever when something else takes its track (`playOnTrack()`, a raw `setAnimation()`): it settles the moment its animation is replaced.
71
+
72
+ Fix: a game's own `spine.state` listener no longer misses the `complete`, `interrupt` or `end` of a one-shot. The symbol listened on the state and removed itself during spine's dispatch, which walks the listener list live, so the listener after it was skipped; it now listens on the one-shot's own track entry.
73
+
74
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `WinLines` in `pixi-reels/debug`, paylines and payways drawn over a reel set with pixi-silk. `lines.line(cells)` draws one polyline through the cell centres; `lines.ways(perReel)` joins every winning cell to every winning cell on the next reel, a lattice whose paths are the ways, so a win of millions of ways is still a few dozen segments. Each call returns its own `SilkGraphics` for fading or pulsing; `clear()` removes them. It is `Disposable`: `destroy()` frees every line, and drawing after it throws. Cells are read at draw time, so a board that grew with `expand()` is drawn where it is now. The library still computes no wins: it draws the cells it is handed.
75
+
76
+ ```ts
77
+ import { WinLines } from 'pixi-reels/debug';
78
+
79
+ const lines = new WinLines(reelSet);
80
+ lines.ways(win.perReel); // [[0, 2], [1], [0, 1, 2]]: winning cells per reel
81
+ reelSet.events.on('spin:start', () => lines.clear());
82
+ ```
83
+
84
+ ### Patch Changes
85
+
86
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: the symbols a MultiWays reel adds when it grows mid-spin join the spin (`onReelSpinStart(true)`, and the tease hook when the reel is teasing), the way a symbol swapped in mid-spin does. A `StaticSpinSymbol` among them showed its live art in a strip of blurred snapshots until it wrapped.
87
+
88
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: a reel on a set that is not MultiWays lays its cells out at its share of a `reelExtents()` box. The builder sized each cell to fit the box but gave every reel the symbol size as the pitch its strip moves on, and such a reel never reshapes when it lands: a 4-cell reel in a 300px box drew 75px cells 100px apart, the last past the bottom of its mask, and `getCellBounds()` reported them there. The spin pitch is now the reel's own cell, spinning and landed. A set without `reelExtents()` keeps the symbol size exactly, instead of re-deriving it from the reel's height, which could drift by a bit (`111.65` came back as `111.65000000000002`).
89
+
90
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `SpinTextureCache` no longer destroys a texture another reel is still drawing. It kept one capture per symbol id, so on a MultiWays set, where every reel sizes the same id differently, each reel's capture at its own cell size destroyed the one the previous reel was spinning on and the next render crashed on a destroyed texture. Captures are now keyed by id and cell size (and smear axis for blurred ones), sizes are kept side by side, and `clear()` / `invalidate()` / `destroy()` remain the only calls that destroy them. `getStatic()` / `getBlurred()` take an optional size to read one capture; without it they return the most recent, as before. Sizes are compared to a thousandth of a pixel, so one cell size reached along two float paths (`111.65` and `111.65000000000002`) is one capture and a prewarmed texture is found. `captureBlurred()` blurs only the provided static or the capture at its own size, and throws rather than stretch a capture of another size into a cached blur.
91
+
92
+ `StaticSpinSymbol` now spins every reel on the snapshot of its own cell size, and a MultiWays reshape mid-spin re-points the snapshot at the new size instead of stretching the old one. To prewarm a MultiWays set, call `prewarmSpinTextures` once per cell size the reels can land on.
93
+
94
+ - [#265](https://github.com/schmooky/pixi-reels/pull/265) [`05db169`](https://github.com/schmooky/pixi-reels/commit/05db169caaa4c4e8fe557848c03d21f7a8935a7e) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `CardSymbol` and `SpriteSymbol` settle a pending `playWin()` when `stopAnimation()` cuts the win short (a recycle, a spotlight hide, `removeReels()`), as `AnimatedSpriteSymbol` and the Spine symbols already did, so an `await` on it, or a `WinPresenter.show()`, no longer hangs. `CardSymbol` also kills the rest of its win timeline and its two fill calls, which used to fire into a symbol that had moved on or been destroyed. A second `playWin()` settles the first.
95
+
96
+ ## 3.1.0
97
+
98
+ ### Minor Changes
99
+
100
+ - [#259](https://github.com/schmooky/pixi-reels/pull/259) [`cb4d013`](https://github.com/schmooky/pixi-reels/commit/cb4d013b1ff4b73666fb408247e19e17f12ae31b) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: per-phase sections on a speed profile, so a phase's timing, its steps, and its anticipated variant are configured where they belong instead of in one flat bag.
101
+
102
+ A profile's flat fields are unchanged and still the contract: a phase that read `profile.spinSpeed` reads exactly what it always did, and every profile written before this release behaves identically. On top of them a profile may now carry one optional section per phase:
103
+
104
+ ```ts
105
+ const normal = {
106
+ name: 'normal',
107
+ spinDelay: 0,
108
+ spinSpeed: 200,
109
+ stopDelay: 140,
110
+ anticipationDelay: 450,
111
+ bounceDistance: 80,
112
+ bounceDuration: 600,
113
+ start: { steps: { accelerate: { ease: 'sine.out', duration: 300 } } },
114
+ stop: {
115
+ bounceDistance: 40,
116
+ whenAnticipated: { steps: { bounce: { ease: 'sine.out' } } },
117
+ },
118
+ } satisfies SpeedProfile;
119
+ ```
120
+
121
+ A section restates any flat field for that phase alone, configures the phase's named steps by name, and carries a `whenAnticipated` half that is merged on top for a reel that teased earlier in the spin. Nesting rather than a `'anticipation:stop'` key keeps the variant distinct from a phase NAME like `'cascade:fall'`.
122
+
123
+ **Custom phases are typed, not `object`.** `PhaseProfiles` is the registry the built-in sections live in, and a game merges its own phases into it:
124
+
125
+ ```ts
126
+ declare module 'pixi-reels' {
127
+ interface PhaseProfiles {
128
+ 'bigwin:flash': PhaseSection<{ pulse: StepTiming }, { flashes: number }>;
129
+ }
130
+ }
131
+ ```
132
+
133
+ That section is then checked and autocompleted everywhere a profile is written, and a typo in a step name is a compile error.
134
+
135
+ **A phase can tell whether its reel teased.** `PhaseFactory.create` now passes a `PhaseCreateContext` (`phase`, `reelIndex`, `anticipated`, `quickened`) as a third argument to a registered factory, so a registration can build a different phase for a reel that teased:
136
+
137
+ ```ts
138
+ f.registerFactory('anticipation', (reel, speed, ctx) =>
139
+ ctx.reelIndex === 4 ? new CustomAnticipationPhase(reel, speed) : new AnticipationPhase(reel, speed));
140
+ ```
141
+
142
+ The same context reaches the phase: `phase.anticipated`, and `ctx.anticipated` in every step.
143
+
144
+ Also new: `ReelPhase.timing` (the profile with this phase's section folded in — what the built-in phases now read), `ctx.step` in a step (that step's config from the profile, in segment order), `resolvePhaseProfile` / `resolveStepTiming` for a phase that does not extend `ReelPhase`, and `speed.tune(phase, section)` / `speed.clearTune()` to override part of a section at run time. Like a speed change, a tune takes effect on the next spin.
145
+
146
+ **The built-in phases are generic over their profile.** `StopPhase<TProfile>`, and the same for the other seven, so subclassing one keeps the profile type instead of pinning it back to `SpeedProfile`. A subclass reads its own fields through `this._speed` and `ctx.profile` with no cast:
147
+
148
+ ```ts
149
+ class TeasedStop extends StopPhase<MyProfile> {
150
+ override defaultSteps(): PhaseStep<StopStepContext<MyProfile>>[] { /* ctx.profile.myField */ }
151
+ }
152
+ ```
153
+
154
+ `StopPhase` with no argument is `StopPhase<SpeedProfile>`, exactly as before, and `f.register('stop', StopPhase, { steps })` infers as before — `register` now reads its options type off the class (`PhaseOptionsOf`) so it works for a generic one.
155
+
156
+ **`bounceEase` is a profile field.** The third of the bounce's three numbers, beside `bounceDistance` and `bounceDuration`, read by `phase.bounce()` when the call names no ease. `bounce({ ease })` still wins; with neither it is `'power1.out'` as today. Being a flat field it sections like the rest, so a teased landing that differs only in its ease is `stop: { whenAnticipated: { bounceEase: 'sine.out' } }` and no phase code.
157
+
158
+ `anticipated` means a tease that RAN TO ITS END. A `'quicken'` press that cuts a tease short leaves the reel on the section's own half rather than `whenAnticipated`: the press asked for the landing, and what it cut is the tease the variant exists to pay off. A press before the tease skips it outright, as it already did. `quickened` is in the same context, so a phase that wants the other reading can take it.
159
+
160
+ **Deprecate: `decelerationEase`.** It is declared, set in all three built-in presets, and read nowhere in the engine — it names a deceleration phase that does not exist (a stop is a spin-out plus a bounce) while being the first place a consumer looks for the landing ease. It still compiles and still does nothing; the typings now say so and point at `bounceEase`. Removed in the next major.
161
+
162
+ **One thing that can break a consumer:** `SpeedProfile` is now `SpeedProfileBase & PhaseSections` rather than a lone interface, so it can no longer be extended by declaration merging. `interface MyProfile extends SpeedProfile { ... }` is unaffected and stays the documented way to carry extra timing. If you were merging into the library's own interface, merge into `SpeedProfileBase` instead:
163
+
164
+ ```ts
165
+ // before
166
+ declare module 'pixi-reels' { interface SpeedProfile { houseEase: string } }
167
+ // after
168
+ declare module 'pixi-reels' { interface SpeedProfileBase { houseEase: string } }
169
+ ```
170
+
171
+ A profile carrying a top-level OBJECT field named after a phase (`start`, `spin`, `anticipation`, `stop`, `adjust`, `cascade:*`) now collides with that phase's section and fails to compile, loudly, at the profile's declaration. Rename the field, or make it the phase's section.
172
+
173
+ Two things the probes for those breaks turned up, fixed here: a profile field named after a phase whose value is an ARRAY is no longer read as that phase's section (spreading one landed its indices on the resolved profile as `'0'`, `'1'`), and a `steps` key naming no step of the phase now warns once with code `profile-step-<phase>-<name>` instead of configuring nothing in silence — `insertAfter` and friends throw on the same mistake, but a profile is data, so this warns rather than failing the spin.
174
+
175
+ **A step you INSERT into a built-in phase is typed too.** Each built-in's step map is a named interface — `StartSteps`, `AnticipationSteps`, `StopSteps` — so a game merges its own step name into the one it inserted into, and a step's config can carry more than a `StepTiming`:
176
+
177
+ ```ts
178
+ declare module 'pixi-reels' {
179
+ interface StopSteps { kick: { lift: number; ease?: string } }
180
+ }
181
+
182
+ const turbo = { ...SpeedPresets.TURBO, stop: { steps: { kick: { lift: 6 } } } };
183
+ ```
184
+
185
+ An unknown step name is still rejected, and the step's own shape is still checked.
186
+
3
187
  ## 3.0.0
4
188
 
5
189
  ### Major Changes
package/README.md CHANGED
@@ -133,6 +133,15 @@ In the browser console (or via Playwright / agent eval):
133
133
  __PIXI_REELS_DEBUG.log() // ASCII grid + state snapshot
134
134
  __PIXI_REELS_DEBUG.snapshot() // Full JSON state
135
135
  __PIXI_REELS_DEBUG.trace() // Log spin, skip, speed, spotlight, shape, pin events
136
+ __PIXI_REELS_DEBUG.metrics() // Per-spin timings: phases, stops, landings, teases, skips
137
+ ```
138
+
139
+ To see it on the canvas, draw the overlay from the `pixi-reels/debug` subpath. It needs [pixi-silk](https://pixi-silk.schmooky.dev) (`pnpm add -D pixi-silk`), and only where you import it:
140
+
141
+ ```ts
142
+ import { debugOverlay } from 'pixi-reels/debug';
143
+ debugOverlay(reelSet, { layers: 'all', live: true, ticker: app.ticker });
144
+ // cell geometry, travel axis, a per-reel hud, a metrics plaque and a phase timeline per spin
136
145
  ```
137
146
 
138
147
  ## Examples
@@ -150,6 +159,7 @@ The standalone `examples/` apps moved to their own repo in 2.0. Keeping two para
150
159
  - `pixi.js` ^8.18.1
151
160
  - `gsap` ^3.15.0
152
161
  - `@esotericsoftware/spine-pixi-v8` ~4.2.110 (optional, only if you use `SpineReelSymbol`)
162
+ - `pixi-silk` ^0.1.0 (optional, only where you import `pixi-reels/debug`)
153
163
 
154
164
  ## Contributing
155
165
 
@@ -0,0 +1,2 @@
1
+ const e=require("./ReelSymbol-B5QBhkmw.cjs");let t=require("pixi.js");var n=class extends e.t{_color;_label;_textColor;_gfx;_text;_win=null;constructor(e){super(),this._color=e.color,this._label=e.label,this._textColor=e.textColor??16777215,this._gfx=new t.Graphics,this._text=new t.Text({text:this._label,style:{fontFamily:`"Roboto Condensed", "Arial Narrow", "Helvetica Neue Condensed", "Liberation Sans Narrow", system-ui, sans-serif`,fontSize:32,fontWeight:`700`,fill:this._textColor,align:`center`}}),this._text.anchor.set(.5),this.view.addChild(this._gfx),this.view.addChild(this._text)}onActivate(e){}onDeactivate(){}async playWin(){return this._endWin(),new Promise(e=>{this.gsap.killTweensOf(this._text),this.gsap.killTweensOf(this._text.scale);let t=this._textColor,n=this.gsap.timeline({onComplete:()=>{this._win=null,this._text.scale.set(1,1),this._text.rotation=0,this._text.style.fill=t,e()}});n.to(this._text.scale,{x:1.4,y:1.4,duration:.18,ease:`back.out(2)`},0).to(this._text,{rotation:-.12,duration:.09,ease:`sine.inOut`},0).to(this._text,{rotation:.12,duration:.18,ease:`sine.inOut`},.09).to(this._text,{rotation:0,duration:.09,ease:`sine.inOut`},.27).to(this._text.scale,{x:1,y:1,duration:.18,ease:`power2.out`},.32);let r=[this.gsap.delayedCall(.06,()=>this._text.style.fill=16769384),this.gsap.delayedCall(.42,()=>this._text.style.fill=t)];this._win={tl:n,calls:r,resolve:e}})}stopAnimation(){this._endWin(),this.gsap.killTweensOf(this._text),this.gsap.killTweensOf(this._text.scale),this._text.scale.set(1,1),this._text.rotation=0,this._text.style.fill=this._textColor,this._gfx.alpha=1}onDestroy(){this._endWin()}_endWin(){let e=this._win;if(e){this._win=null,e.tl.kill();for(let t of e.calls)t.kill();e.resolve()}}resize(e,t){this._gfx.clear(),this._gfx.rect(0,0,e,t).fill({color:this._color}),this._gfx.rect(1,1,e-2,t-2).stroke({color:0,width:2,alpha:.25}),this._text.x=e/2,this._text.y=t/2;let n=Math.max(1,this._label.length),r=t*.38,i=e*.7/(n*.45);this._text.style.fontSize=Math.max(7,Math.floor(Math.min(r,i)))}},r=[{id:`7`,color:12597547,label:`7`},{id:`8`,color:15105570,label:`8`},{id:`9`,color:15844367,label:`9`},{id:`10`,color:2600544,label:`10`},{id:`J`,color:1482885,label:`J`},{id:`Q`,color:2719929,label:`Q`},{id:`K`,color:9323693,label:`K`},{id:`A`,color:2899536,label:`A`}],i={id:`wild`,color:16774048,label:`WILD`,textColor:7033856},a={rest:4937059,start:3718648,spin:2450411,anticipation:16096779,stop:9133302,landed:2278750,adjust:15485081,"cascade:fall":959977,"cascade:place":1357990,"cascade:dropIn":440020,other:10265519},o=350,s=class e extends n{_colors;_landedMs;_phase=`rest`;_width=0;_height=0;_restCall=null;constructor(e){let t={...a};for(let[n,r]of Object.entries(e.colors??{}))r!==void 0&&(t[n]=r);super({color:t.rest,label:e.label,textColor:e.textColor}),this._colors=t,this._landedMs=e.landedMs??o}get phase(){return this._phase}setPhase(e){if(this._cancelRest(),this._phase=e,this._color=this._colors[e]??this._colors.other,this._width>0&&this.resize(this._width,this._height),e===`landed`){if(this._landedMs<=0){this.setPhase(`rest`);return}this._restCall=this.gsap.delayedCall(this._landedMs/1e3,()=>{this._restCall=null,this.setPhase(`rest`)})}}resize(e,t){this._width=e,this._height=t,super.resize(e,t)}static watch(t){let n=[];for(let r of t){let t=`rest`,i=(t,n)=>{t instanceof e&&t.setPhase(n)},a=e=>{for(let t of r.symbols)i(t,e)},o=e=>{t=e,a(e)},s=()=>{t=`rest`,a(`landed`)},c=(e,n)=>{let a=r.symbols[n];a&&i(a,t)};r.events.on(`phase:enter`,o),r.events.on(`landed`,s),r.events.on(`symbol:created`,c),n.push(()=>{r.events.off(`phase:enter`,o),r.events.off(`landed`,s),r.events.off(`symbol:created`,c)})}return()=>{for(let e of n)e();n.length=0}}onDeactivate(){this._cancelRest(),this._phase=`rest`,this._color=this._colors.rest,super.onDeactivate()}onDestroy(){this._cancelRest(),super.onDestroy()}_cancelRest(){this._restCall&&=(this._restCall.kill(),null)}};Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return a}});
2
+ //# sourceMappingURL=PhaseCardSymbol-Ct9bCE4I.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhaseCardSymbol-Ct9bCE4I.cjs","names":[],"sources":["../src/symbols/CardSymbol.ts","../src/symbols/PhaseCardSymbol.ts"],"sourcesContent":["import { Graphics, Text } from 'pixi.js';\nimport { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * **Debug / prototyping symbol - NOT for production.**\n *\n * `CardSymbol` is a flat-rectangle-plus-centered-text `ReelSymbol` drawn\n * entirely with `PIXI.Graphics`. It exists to give recipes, demos, and\n * mechanic tests a no-asset, no-loader, infinitely-resizable visual that\n * always renders crisply at any cell size, showing how the engine treats\n * cell space across MultiWays reshapes, big-symbol blocks, and pyramid\n * layouts.\n *\n * In a real game, ship one of:\n *\n * - `SpriteSymbol` - pre-rendered art at one resolution. Cheapest.\n * - `AnimatedSpriteSymbol` - frame-by-frame win/idle animation.\n * - `SpineSymbol` (via `pixi-reels/spine`) - vector skeletal animation\n * that scales without quality loss. Best for MultiWays where cells\n * resize across spins.\n * - Or a custom `ReelSymbol` subclass for game-specific visuals.\n *\n * Ships from the package so a prototype runs with no art at all. It is\n * deliberately plain `Graphics`: swap it for real art before you ship.\n *\n * ```ts\n * import { CardSymbol, CARD_DECK } from 'pixi-reels';\n *\n * builder.symbols((registry) => {\n * for (const card of CARD_DECK) {\n * registry.register(card.id, CardSymbol, { color: card.color, label: card.label });\n * }\n * });\n * ```\n */\nexport interface CardSymbolOptions {\n /** Hex fill color (e.g. `0xc0392b`). */\n color: number;\n /** Centered label text (e.g. `'A'`, `'10'`, `'WILD'`). */\n label: string;\n /** Text fill color. Defaults to white. */\n textColor?: number;\n}\n\nexport class CardSymbol extends ReelSymbol {\n protected _color: number;\n private _label: string;\n private _textColor: number;\n private _gfx: Graphics;\n private _text: Text;\n /** The win in flight: its timeline, its two fill calls and the promise to settle. */\n private _win: { tl: gsap.core.Timeline; calls: gsap.core.Tween[]; resolve: () => void } | null = null;\n\n constructor(opts: CardSymbolOptions) {\n super();\n this._color = opts.color;\n this._label = opts.label;\n this._textColor = opts.textColor ?? 0xffffff;\n this._gfx = new Graphics();\n this._text = new Text({\n text: this._label,\n style: {\n // Roboto Condensed if installed (the site loads it via @fontsource);\n // falls through to a system condensed sans-serif on plain pages.\n fontFamily:\n '\"Roboto Condensed\", \"Arial Narrow\", \"Helvetica Neue Condensed\", \"Liberation Sans Narrow\", system-ui, sans-serif',\n fontSize: 32,\n fontWeight: '700',\n fill: this._textColor,\n align: 'center',\n },\n });\n this._text.anchor.set(0.5);\n this.view.addChild(this._gfx);\n this.view.addChild(this._text);\n }\n\n protected onActivate(_symbolId: string): void {\n // No-op - this symbol's visual identity is set at construction.\n }\n\n protected onDeactivate(): void {\n // No-op - leave _gfx/_text in their last state until next activate.\n }\n\n async playWin(): Promise<void> {\n // A win already playing ends here, its promise settled, before this one starts.\n this._endWin();\n return new Promise((resolve) => {\n // Animate only the glyph - the card body stays put. Useful for big\n // symbols (the rectangle is the cell footprint) and any setting\n // where movement of the cell border is distracting (cascade refills,\n // MultiWays adjust). The text is anchored at (0.5, 0.5) so its\n // scale tween pivots about the centre.\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n const originalFill = this._textColor;\n const tl = this.gsap.timeline({\n onComplete: () => {\n this._win = null;\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = originalFill;\n resolve();\n },\n });\n tl.to(this._text.scale, { x: 1.4, y: 1.4, duration: 0.18, ease: 'back.out(2)' }, 0)\n .to(this._text, { rotation: -0.12, duration: 0.09, ease: 'sine.inOut' }, 0)\n .to(this._text, { rotation: 0.12, duration: 0.18, ease: 'sine.inOut' }, 0.09)\n .to(this._text, { rotation: 0, duration: 0.09, ease: 'sine.inOut' }, 0.27)\n .to(this._text.scale, { x: 1, y: 1, duration: 0.18, ease: 'power2.out' }, 0.32);\n // Glyph fill flashes warm gold mid-pulse - `Text.style.fill` isn't\n // a tweenable target on every Pixi version, so we set it discretely.\n const calls = [\n this.gsap.delayedCall(0.06, () => (this._text.style.fill = 0xffe168)),\n this.gsap.delayedCall(0.42, () => (this._text.style.fill = originalFill)),\n ];\n this._win = { tl, calls, resolve };\n });\n }\n\n stopAnimation(): void {\n this._endWin();\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = this._textColor;\n this._gfx.alpha = 1;\n }\n\n protected override onDestroy(): void {\n this._endWin();\n }\n\n /**\n * Kill the win in flight and settle its promise. Killing the glyph's tweens\n * alone would leave the timeline's `onComplete` and both fill calls armed,\n * and they would fire into a symbol that has since been recycled or\n * destroyed, with the `await playWin()` never settling.\n */\n private _endWin(): void {\n const win = this._win;\n if (!win) return;\n this._win = null;\n win.tl.kill();\n for (const call of win.calls) call.kill();\n win.resolve();\n }\n\n resize(width: number, height: number): void {\n this._gfx.clear();\n this._gfx.rect(0, 0, width, height).fill({ color: this._color });\n // Inner darker stroke so adjacent cells are visually separated even\n // when they share the same color.\n this._gfx.rect(1, 1, width - 2, height - 2).stroke({ color: 0x000000, width: 2, alpha: 0.25 });\n this._text.x = width / 2;\n this._text.y = height / 2;\n // Font size constrained by both height (~38%) and label width (so\n // multi-char labels like '10' or 'WILD' don't overflow narrow cells).\n // Smaller than half-cell to leave breathing room - looks less crowded.\n const labelLen = Math.max(1, this._label.length);\n const fitH = height * 0.38;\n // Roboto Condensed glyphs are narrower (~0.45 avg ratio) than serif fonts.\n const fitW = (width * 0.7) / (labelLen * 0.45);\n this._text.style.fontSize = Math.max(7, Math.floor(Math.min(fitH, fitW)));\n }\n}\n\n/**\n * Standard high-card deck for prototyping. Each card has its own color\n * so a full grid is visually unambiguous at a glance.\n */\nexport const CARD_DECK: ReadonlyArray<{ id: string; color: number; label: string }> = [\n { id: '7', color: 0xc0392b, label: '7' }, // crimson\n { id: '8', color: 0xe67e22, label: '8' }, // orange\n { id: '9', color: 0xf1c40f, label: '9' }, // amber\n { id: '10', color: 0x27ae60, label: '10' }, // green\n { id: 'J', color: 0x16a085, label: 'J' }, // teal\n { id: 'Q', color: 0x2980b9, label: 'Q' }, // blue\n { id: 'K', color: 0x8e44ad, label: 'K' }, // purple\n { id: 'A', color: 0x2c3e50, label: 'A' }, // navy\n];\n\n/** A pale-yellow `WILD` card for sticky/expanding-wild prototypes. */\nexport const WILD_CARD = {\n id: 'wild',\n color: 0xfff3a0,\n label: 'WILD',\n textColor: 0x6b5400,\n} as const;\n","import type { gsap } from 'gsap';\nimport type { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelEvents } from '../events/ReelEvents.js';\nimport { CardSymbol } from './CardSymbol.js';\nimport type { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * Fill per phase for {@link PhaseCardSymbol}. Keys are the names phases\n * register under (`'start'`, `'spin'`, `'anticipation'`, `'stop'`, the\n * cascade and MultiWays keys, and any custom key), plus `rest` (at rest,\n * the default) and `landed` (the beat between landing and rest). A phase\n * with no entry draws `other`.\n */\nexport const PHASE_CARD_COLORS: Readonly<Record<string, number>> = {\n rest: 0x4b5563,\n start: 0x38bdf8,\n spin: 0x2563eb,\n anticipation: 0xf59e0b,\n stop: 0x8b5cf6,\n landed: 0x22c55e,\n adjust: 0xec4899,\n 'cascade:fall': 0x0ea5e9,\n 'cascade:place': 0x14b8a6,\n 'cascade:dropIn': 0x06b6d4,\n other: 0x9ca3af,\n};\n\nexport interface PhaseCardSymbolOptions {\n /** Centered label text (e.g. `'A'`, `'10'`). */\n label: string;\n /** Text fill color. Defaults to white. */\n textColor?: number;\n /** Fills to override or add; unlisted phases keep {@link PHASE_CARD_COLORS}. */\n colors?: Partial<Record<string, number>>;\n /** How long the `landed` fill shows before the card rests, in ms. `0` rests at once. Default 350. */\n landedMs?: number;\n}\n\n/**\n * The slice of a `Reel` that {@link PhaseCardSymbol.watch} reads. Every\n * entry of `ReelSet.reels` satisfies it.\n */\nexport interface PhaseWatchedReel {\n readonly symbols: readonly ReelSymbol[];\n readonly events: EventEmitter<ReelEvents>;\n}\n\n/** The `landed` beat, in ms, when the options do not say. */\nconst DEFAULT_LANDED_MS = 350;\n\n/**\n * **Debug / prototyping symbol - NOT for production.**\n *\n * A {@link CardSymbol} that is grey at rest and takes the colour of whatever\n * phase its reel is running, so a spin can be read off the board: sky while\n * the reel accelerates, blue at full speed, amber through a tease, violet\n * while the stop spins the frame in, a green beat on landing, then grey\n * again. A slam shows as amber or blue going straight to green; a quicken\n * shows the violet in between.\n *\n * The card does not know its reel. Point a watcher at the reels once and it\n * paints every card on them from the reel's own `phase:enter`, `landed` and\n * `symbol:created` events, custom phases included:\n *\n * ```ts\n * import { PhaseCardSymbol } from 'pixi-reels';\n *\n * builder.symbols((r) => r.register('A', PhaseCardSymbol, { label: 'A' }));\n * const stop = PhaseCardSymbol.watch(reelSet.reels);\n * // ...later, before reelSet.destroy():\n * stop();\n * ```\n *\n * `setPhase()` is public, so anything else that knows the phase can drive a\n * card directly.\n */\nexport class PhaseCardSymbol extends CardSymbol {\n private readonly _colors: Record<string, number>;\n private readonly _landedMs: number;\n private _phase = 'rest';\n private _width = 0;\n private _height = 0;\n private _restCall: gsap.core.Tween | null = null;\n\n constructor(opts: PhaseCardSymbolOptions) {\n const colors: Record<string, number> = { ...PHASE_CARD_COLORS };\n for (const [phase, color] of Object.entries(opts.colors ?? {})) {\n if (color !== undefined) colors[phase] = color;\n }\n super({ color: colors.rest, label: opts.label, textColor: opts.textColor });\n this._colors = colors;\n this._landedMs = opts.landedMs ?? DEFAULT_LANDED_MS;\n }\n\n /** The phase the card currently shows: a phase name, `'landed'` or `'rest'`. */\n get phase(): string {\n return this._phase;\n }\n\n /**\n * Show `phase`. A phase with no colour of its own draws `other`. `'landed'`\n * shows its beat and then rests on its own; anything set in the meantime\n * cancels that.\n */\n setPhase(phase: string): void {\n this._cancelRest();\n this._phase = phase;\n this._color = this._colors[phase] ?? this._colors.other;\n if (this._width > 0) this.resize(this._width, this._height);\n if (phase === 'landed') {\n if (this._landedMs <= 0) {\n this.setPhase('rest');\n return;\n }\n this._restCall = this.gsap.delayedCall(this._landedMs / 1000, () => {\n this._restCall = null;\n this.setPhase('rest');\n });\n }\n }\n\n override resize(width: number, height: number): void {\n this._width = width;\n this._height = height;\n super.resize(width, height);\n }\n\n /**\n * Paint every {@link PhaseCardSymbol} on `reels` from the reels' own\n * events, for as long as the returned function has not been called. Other\n * symbol classes on the same reels are left alone. A card that arrives\n * mid-phase (a pool swap as the strip wraps) is painted on arrival.\n */\n static watch(reels: Iterable<PhaseWatchedReel>): () => void {\n const stops: Array<() => void> = [];\n for (const reel of reels) {\n // What a card created from now on should show. `rest` after a landing:\n // the landed beat is for the cards that landed, not for a swap-in.\n let current = 'rest';\n const paint = (symbol: ReelSymbol, phase: string): void => {\n if (symbol instanceof PhaseCardSymbol) symbol.setPhase(phase);\n };\n const paintAll = (phase: string): void => {\n for (const symbol of reel.symbols) paint(symbol, phase);\n };\n const onEnter = (name: string): void => {\n current = name;\n paintAll(name);\n };\n const onLanded = (): void => {\n current = 'rest';\n paintAll('landed');\n };\n const onCreated = (_id: string, stripIndex: number): void => {\n const symbol = reel.symbols[stripIndex];\n if (symbol) paint(symbol, current);\n };\n reel.events.on('phase:enter', onEnter);\n reel.events.on('landed', onLanded);\n reel.events.on('symbol:created', onCreated);\n stops.push(() => {\n reel.events.off('phase:enter', onEnter);\n reel.events.off('landed', onLanded);\n reel.events.off('symbol:created', onCreated);\n });\n }\n return () => {\n for (const stop of stops) stop();\n stops.length = 0;\n };\n }\n\n protected override onDeactivate(): void {\n // A pooled card must not carry a phase into whichever cell takes it next.\n this._cancelRest();\n this._phase = 'rest';\n this._color = this._colors.rest;\n super.onDeactivate();\n }\n\n protected override onDestroy(): void {\n this._cancelRest();\n super.onDestroy();\n }\n\n private _cancelRest(): void {\n if (this._restCall) {\n this._restCall.kill();\n this._restCall = null;\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,213 @@
1
+ import { t as e } from "./ReelSymbol-C7Pd7BeO.js";
2
+ import { Graphics as t, Text as n } from "pixi.js";
3
+ //#region src/symbols/CardSymbol.ts
4
+ var r = class extends e {
5
+ _color;
6
+ _label;
7
+ _textColor;
8
+ _gfx;
9
+ _text;
10
+ _win = null;
11
+ constructor(e) {
12
+ super(), this._color = e.color, this._label = e.label, this._textColor = e.textColor ?? 16777215, this._gfx = new t(), this._text = new n({
13
+ text: this._label,
14
+ style: {
15
+ fontFamily: "\"Roboto Condensed\", \"Arial Narrow\", \"Helvetica Neue Condensed\", \"Liberation Sans Narrow\", system-ui, sans-serif",
16
+ fontSize: 32,
17
+ fontWeight: "700",
18
+ fill: this._textColor,
19
+ align: "center"
20
+ }
21
+ }), this._text.anchor.set(.5), this.view.addChild(this._gfx), this.view.addChild(this._text);
22
+ }
23
+ onActivate(e) {}
24
+ onDeactivate() {}
25
+ async playWin() {
26
+ return this._endWin(), new Promise((e) => {
27
+ this.gsap.killTweensOf(this._text), this.gsap.killTweensOf(this._text.scale);
28
+ let t = this._textColor, n = this.gsap.timeline({ onComplete: () => {
29
+ this._win = null, this._text.scale.set(1, 1), this._text.rotation = 0, this._text.style.fill = t, e();
30
+ } });
31
+ n.to(this._text.scale, {
32
+ x: 1.4,
33
+ y: 1.4,
34
+ duration: .18,
35
+ ease: "back.out(2)"
36
+ }, 0).to(this._text, {
37
+ rotation: -.12,
38
+ duration: .09,
39
+ ease: "sine.inOut"
40
+ }, 0).to(this._text, {
41
+ rotation: .12,
42
+ duration: .18,
43
+ ease: "sine.inOut"
44
+ }, .09).to(this._text, {
45
+ rotation: 0,
46
+ duration: .09,
47
+ ease: "sine.inOut"
48
+ }, .27).to(this._text.scale, {
49
+ x: 1,
50
+ y: 1,
51
+ duration: .18,
52
+ ease: "power2.out"
53
+ }, .32);
54
+ let r = [this.gsap.delayedCall(.06, () => this._text.style.fill = 16769384), this.gsap.delayedCall(.42, () => this._text.style.fill = t)];
55
+ this._win = {
56
+ tl: n,
57
+ calls: r,
58
+ resolve: e
59
+ };
60
+ });
61
+ }
62
+ stopAnimation() {
63
+ this._endWin(), this.gsap.killTweensOf(this._text), this.gsap.killTweensOf(this._text.scale), this._text.scale.set(1, 1), this._text.rotation = 0, this._text.style.fill = this._textColor, this._gfx.alpha = 1;
64
+ }
65
+ onDestroy() {
66
+ this._endWin();
67
+ }
68
+ _endWin() {
69
+ let e = this._win;
70
+ if (e) {
71
+ this._win = null, e.tl.kill();
72
+ for (let t of e.calls) t.kill();
73
+ e.resolve();
74
+ }
75
+ }
76
+ resize(e, t) {
77
+ this._gfx.clear(), this._gfx.rect(0, 0, e, t).fill({ color: this._color }), this._gfx.rect(1, 1, e - 2, t - 2).stroke({
78
+ color: 0,
79
+ width: 2,
80
+ alpha: .25
81
+ }), this._text.x = e / 2, this._text.y = t / 2;
82
+ let n = Math.max(1, this._label.length), r = t * .38, i = e * .7 / (n * .45);
83
+ this._text.style.fontSize = Math.max(7, Math.floor(Math.min(r, i)));
84
+ }
85
+ }, i = [
86
+ {
87
+ id: "7",
88
+ color: 12597547,
89
+ label: "7"
90
+ },
91
+ {
92
+ id: "8",
93
+ color: 15105570,
94
+ label: "8"
95
+ },
96
+ {
97
+ id: "9",
98
+ color: 15844367,
99
+ label: "9"
100
+ },
101
+ {
102
+ id: "10",
103
+ color: 2600544,
104
+ label: "10"
105
+ },
106
+ {
107
+ id: "J",
108
+ color: 1482885,
109
+ label: "J"
110
+ },
111
+ {
112
+ id: "Q",
113
+ color: 2719929,
114
+ label: "Q"
115
+ },
116
+ {
117
+ id: "K",
118
+ color: 9323693,
119
+ label: "K"
120
+ },
121
+ {
122
+ id: "A",
123
+ color: 2899536,
124
+ label: "A"
125
+ }
126
+ ], a = {
127
+ id: "wild",
128
+ color: 16774048,
129
+ label: "WILD",
130
+ textColor: 7033856
131
+ }, o = {
132
+ rest: 4937059,
133
+ start: 3718648,
134
+ spin: 2450411,
135
+ anticipation: 16096779,
136
+ stop: 9133302,
137
+ landed: 2278750,
138
+ adjust: 15485081,
139
+ "cascade:fall": 959977,
140
+ "cascade:place": 1357990,
141
+ "cascade:dropIn": 440020,
142
+ other: 10265519
143
+ }, s = 350, c = class e extends r {
144
+ _colors;
145
+ _landedMs;
146
+ _phase = "rest";
147
+ _width = 0;
148
+ _height = 0;
149
+ _restCall = null;
150
+ constructor(e) {
151
+ let t = { ...o };
152
+ for (let [n, r] of Object.entries(e.colors ?? {})) r !== void 0 && (t[n] = r);
153
+ super({
154
+ color: t.rest,
155
+ label: e.label,
156
+ textColor: e.textColor
157
+ }), this._colors = t, this._landedMs = e.landedMs ?? s;
158
+ }
159
+ get phase() {
160
+ return this._phase;
161
+ }
162
+ setPhase(e) {
163
+ if (this._cancelRest(), this._phase = e, this._color = this._colors[e] ?? this._colors.other, this._width > 0 && this.resize(this._width, this._height), e === "landed") {
164
+ if (this._landedMs <= 0) {
165
+ this.setPhase("rest");
166
+ return;
167
+ }
168
+ this._restCall = this.gsap.delayedCall(this._landedMs / 1e3, () => {
169
+ this._restCall = null, this.setPhase("rest");
170
+ });
171
+ }
172
+ }
173
+ resize(e, t) {
174
+ this._width = e, this._height = t, super.resize(e, t);
175
+ }
176
+ static watch(t) {
177
+ let n = [];
178
+ for (let r of t) {
179
+ let t = "rest", i = (t, n) => {
180
+ t instanceof e && t.setPhase(n);
181
+ }, a = (e) => {
182
+ for (let t of r.symbols) i(t, e);
183
+ }, o = (e) => {
184
+ t = e, a(e);
185
+ }, s = () => {
186
+ t = "rest", a("landed");
187
+ }, c = (e, n) => {
188
+ let a = r.symbols[n];
189
+ a && i(a, t);
190
+ };
191
+ r.events.on("phase:enter", o), r.events.on("landed", s), r.events.on("symbol:created", c), n.push(() => {
192
+ r.events.off("phase:enter", o), r.events.off("landed", s), r.events.off("symbol:created", c);
193
+ });
194
+ }
195
+ return () => {
196
+ for (let e of n) e();
197
+ n.length = 0;
198
+ };
199
+ }
200
+ onDeactivate() {
201
+ this._cancelRest(), this._phase = "rest", this._color = this._colors.rest, super.onDeactivate();
202
+ }
203
+ onDestroy() {
204
+ this._cancelRest(), super.onDestroy();
205
+ }
206
+ _cancelRest() {
207
+ this._restCall &&= (this._restCall.kill(), null);
208
+ }
209
+ };
210
+ //#endregion
211
+ export { a, r as i, c as n, i as r, o as t };
212
+
213
+ //# sourceMappingURL=PhaseCardSymbol-bVa7GpDn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhaseCardSymbol-bVa7GpDn.js","names":[],"sources":["../src/symbols/CardSymbol.ts","../src/symbols/PhaseCardSymbol.ts"],"sourcesContent":["import { Graphics, Text } from 'pixi.js';\nimport { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * **Debug / prototyping symbol - NOT for production.**\n *\n * `CardSymbol` is a flat-rectangle-plus-centered-text `ReelSymbol` drawn\n * entirely with `PIXI.Graphics`. It exists to give recipes, demos, and\n * mechanic tests a no-asset, no-loader, infinitely-resizable visual that\n * always renders crisply at any cell size, showing how the engine treats\n * cell space across MultiWays reshapes, big-symbol blocks, and pyramid\n * layouts.\n *\n * In a real game, ship one of:\n *\n * - `SpriteSymbol` - pre-rendered art at one resolution. Cheapest.\n * - `AnimatedSpriteSymbol` - frame-by-frame win/idle animation.\n * - `SpineSymbol` (via `pixi-reels/spine`) - vector skeletal animation\n * that scales without quality loss. Best for MultiWays where cells\n * resize across spins.\n * - Or a custom `ReelSymbol` subclass for game-specific visuals.\n *\n * Ships from the package so a prototype runs with no art at all. It is\n * deliberately plain `Graphics`: swap it for real art before you ship.\n *\n * ```ts\n * import { CardSymbol, CARD_DECK } from 'pixi-reels';\n *\n * builder.symbols((registry) => {\n * for (const card of CARD_DECK) {\n * registry.register(card.id, CardSymbol, { color: card.color, label: card.label });\n * }\n * });\n * ```\n */\nexport interface CardSymbolOptions {\n /** Hex fill color (e.g. `0xc0392b`). */\n color: number;\n /** Centered label text (e.g. `'A'`, `'10'`, `'WILD'`). */\n label: string;\n /** Text fill color. Defaults to white. */\n textColor?: number;\n}\n\nexport class CardSymbol extends ReelSymbol {\n protected _color: number;\n private _label: string;\n private _textColor: number;\n private _gfx: Graphics;\n private _text: Text;\n /** The win in flight: its timeline, its two fill calls and the promise to settle. */\n private _win: { tl: gsap.core.Timeline; calls: gsap.core.Tween[]; resolve: () => void } | null = null;\n\n constructor(opts: CardSymbolOptions) {\n super();\n this._color = opts.color;\n this._label = opts.label;\n this._textColor = opts.textColor ?? 0xffffff;\n this._gfx = new Graphics();\n this._text = new Text({\n text: this._label,\n style: {\n // Roboto Condensed if installed (the site loads it via @fontsource);\n // falls through to a system condensed sans-serif on plain pages.\n fontFamily:\n '\"Roboto Condensed\", \"Arial Narrow\", \"Helvetica Neue Condensed\", \"Liberation Sans Narrow\", system-ui, sans-serif',\n fontSize: 32,\n fontWeight: '700',\n fill: this._textColor,\n align: 'center',\n },\n });\n this._text.anchor.set(0.5);\n this.view.addChild(this._gfx);\n this.view.addChild(this._text);\n }\n\n protected onActivate(_symbolId: string): void {\n // No-op - this symbol's visual identity is set at construction.\n }\n\n protected onDeactivate(): void {\n // No-op - leave _gfx/_text in their last state until next activate.\n }\n\n async playWin(): Promise<void> {\n // A win already playing ends here, its promise settled, before this one starts.\n this._endWin();\n return new Promise((resolve) => {\n // Animate only the glyph - the card body stays put. Useful for big\n // symbols (the rectangle is the cell footprint) and any setting\n // where movement of the cell border is distracting (cascade refills,\n // MultiWays adjust). The text is anchored at (0.5, 0.5) so its\n // scale tween pivots about the centre.\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n const originalFill = this._textColor;\n const tl = this.gsap.timeline({\n onComplete: () => {\n this._win = null;\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = originalFill;\n resolve();\n },\n });\n tl.to(this._text.scale, { x: 1.4, y: 1.4, duration: 0.18, ease: 'back.out(2)' }, 0)\n .to(this._text, { rotation: -0.12, duration: 0.09, ease: 'sine.inOut' }, 0)\n .to(this._text, { rotation: 0.12, duration: 0.18, ease: 'sine.inOut' }, 0.09)\n .to(this._text, { rotation: 0, duration: 0.09, ease: 'sine.inOut' }, 0.27)\n .to(this._text.scale, { x: 1, y: 1, duration: 0.18, ease: 'power2.out' }, 0.32);\n // Glyph fill flashes warm gold mid-pulse - `Text.style.fill` isn't\n // a tweenable target on every Pixi version, so we set it discretely.\n const calls = [\n this.gsap.delayedCall(0.06, () => (this._text.style.fill = 0xffe168)),\n this.gsap.delayedCall(0.42, () => (this._text.style.fill = originalFill)),\n ];\n this._win = { tl, calls, resolve };\n });\n }\n\n stopAnimation(): void {\n this._endWin();\n this.gsap.killTweensOf(this._text);\n this.gsap.killTweensOf(this._text.scale);\n this._text.scale.set(1, 1);\n this._text.rotation = 0;\n this._text.style.fill = this._textColor;\n this._gfx.alpha = 1;\n }\n\n protected override onDestroy(): void {\n this._endWin();\n }\n\n /**\n * Kill the win in flight and settle its promise. Killing the glyph's tweens\n * alone would leave the timeline's `onComplete` and both fill calls armed,\n * and they would fire into a symbol that has since been recycled or\n * destroyed, with the `await playWin()` never settling.\n */\n private _endWin(): void {\n const win = this._win;\n if (!win) return;\n this._win = null;\n win.tl.kill();\n for (const call of win.calls) call.kill();\n win.resolve();\n }\n\n resize(width: number, height: number): void {\n this._gfx.clear();\n this._gfx.rect(0, 0, width, height).fill({ color: this._color });\n // Inner darker stroke so adjacent cells are visually separated even\n // when they share the same color.\n this._gfx.rect(1, 1, width - 2, height - 2).stroke({ color: 0x000000, width: 2, alpha: 0.25 });\n this._text.x = width / 2;\n this._text.y = height / 2;\n // Font size constrained by both height (~38%) and label width (so\n // multi-char labels like '10' or 'WILD' don't overflow narrow cells).\n // Smaller than half-cell to leave breathing room - looks less crowded.\n const labelLen = Math.max(1, this._label.length);\n const fitH = height * 0.38;\n // Roboto Condensed glyphs are narrower (~0.45 avg ratio) than serif fonts.\n const fitW = (width * 0.7) / (labelLen * 0.45);\n this._text.style.fontSize = Math.max(7, Math.floor(Math.min(fitH, fitW)));\n }\n}\n\n/**\n * Standard high-card deck for prototyping. Each card has its own color\n * so a full grid is visually unambiguous at a glance.\n */\nexport const CARD_DECK: ReadonlyArray<{ id: string; color: number; label: string }> = [\n { id: '7', color: 0xc0392b, label: '7' }, // crimson\n { id: '8', color: 0xe67e22, label: '8' }, // orange\n { id: '9', color: 0xf1c40f, label: '9' }, // amber\n { id: '10', color: 0x27ae60, label: '10' }, // green\n { id: 'J', color: 0x16a085, label: 'J' }, // teal\n { id: 'Q', color: 0x2980b9, label: 'Q' }, // blue\n { id: 'K', color: 0x8e44ad, label: 'K' }, // purple\n { id: 'A', color: 0x2c3e50, label: 'A' }, // navy\n];\n\n/** A pale-yellow `WILD` card for sticky/expanding-wild prototypes. */\nexport const WILD_CARD = {\n id: 'wild',\n color: 0xfff3a0,\n label: 'WILD',\n textColor: 0x6b5400,\n} as const;\n","import type { gsap } from 'gsap';\nimport type { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelEvents } from '../events/ReelEvents.js';\nimport { CardSymbol } from './CardSymbol.js';\nimport type { ReelSymbol } from './ReelSymbol.js';\n\n/**\n * Fill per phase for {@link PhaseCardSymbol}. Keys are the names phases\n * register under (`'start'`, `'spin'`, `'anticipation'`, `'stop'`, the\n * cascade and MultiWays keys, and any custom key), plus `rest` (at rest,\n * the default) and `landed` (the beat between landing and rest). A phase\n * with no entry draws `other`.\n */\nexport const PHASE_CARD_COLORS: Readonly<Record<string, number>> = {\n rest: 0x4b5563,\n start: 0x38bdf8,\n spin: 0x2563eb,\n anticipation: 0xf59e0b,\n stop: 0x8b5cf6,\n landed: 0x22c55e,\n adjust: 0xec4899,\n 'cascade:fall': 0x0ea5e9,\n 'cascade:place': 0x14b8a6,\n 'cascade:dropIn': 0x06b6d4,\n other: 0x9ca3af,\n};\n\nexport interface PhaseCardSymbolOptions {\n /** Centered label text (e.g. `'A'`, `'10'`). */\n label: string;\n /** Text fill color. Defaults to white. */\n textColor?: number;\n /** Fills to override or add; unlisted phases keep {@link PHASE_CARD_COLORS}. */\n colors?: Partial<Record<string, number>>;\n /** How long the `landed` fill shows before the card rests, in ms. `0` rests at once. Default 350. */\n landedMs?: number;\n}\n\n/**\n * The slice of a `Reel` that {@link PhaseCardSymbol.watch} reads. Every\n * entry of `ReelSet.reels` satisfies it.\n */\nexport interface PhaseWatchedReel {\n readonly symbols: readonly ReelSymbol[];\n readonly events: EventEmitter<ReelEvents>;\n}\n\n/** The `landed` beat, in ms, when the options do not say. */\nconst DEFAULT_LANDED_MS = 350;\n\n/**\n * **Debug / prototyping symbol - NOT for production.**\n *\n * A {@link CardSymbol} that is grey at rest and takes the colour of whatever\n * phase its reel is running, so a spin can be read off the board: sky while\n * the reel accelerates, blue at full speed, amber through a tease, violet\n * while the stop spins the frame in, a green beat on landing, then grey\n * again. A slam shows as amber or blue going straight to green; a quicken\n * shows the violet in between.\n *\n * The card does not know its reel. Point a watcher at the reels once and it\n * paints every card on them from the reel's own `phase:enter`, `landed` and\n * `symbol:created` events, custom phases included:\n *\n * ```ts\n * import { PhaseCardSymbol } from 'pixi-reels';\n *\n * builder.symbols((r) => r.register('A', PhaseCardSymbol, { label: 'A' }));\n * const stop = PhaseCardSymbol.watch(reelSet.reels);\n * // ...later, before reelSet.destroy():\n * stop();\n * ```\n *\n * `setPhase()` is public, so anything else that knows the phase can drive a\n * card directly.\n */\nexport class PhaseCardSymbol extends CardSymbol {\n private readonly _colors: Record<string, number>;\n private readonly _landedMs: number;\n private _phase = 'rest';\n private _width = 0;\n private _height = 0;\n private _restCall: gsap.core.Tween | null = null;\n\n constructor(opts: PhaseCardSymbolOptions) {\n const colors: Record<string, number> = { ...PHASE_CARD_COLORS };\n for (const [phase, color] of Object.entries(opts.colors ?? {})) {\n if (color !== undefined) colors[phase] = color;\n }\n super({ color: colors.rest, label: opts.label, textColor: opts.textColor });\n this._colors = colors;\n this._landedMs = opts.landedMs ?? DEFAULT_LANDED_MS;\n }\n\n /** The phase the card currently shows: a phase name, `'landed'` or `'rest'`. */\n get phase(): string {\n return this._phase;\n }\n\n /**\n * Show `phase`. A phase with no colour of its own draws `other`. `'landed'`\n * shows its beat and then rests on its own; anything set in the meantime\n * cancels that.\n */\n setPhase(phase: string): void {\n this._cancelRest();\n this._phase = phase;\n this._color = this._colors[phase] ?? this._colors.other;\n if (this._width > 0) this.resize(this._width, this._height);\n if (phase === 'landed') {\n if (this._landedMs <= 0) {\n this.setPhase('rest');\n return;\n }\n this._restCall = this.gsap.delayedCall(this._landedMs / 1000, () => {\n this._restCall = null;\n this.setPhase('rest');\n });\n }\n }\n\n override resize(width: number, height: number): void {\n this._width = width;\n this._height = height;\n super.resize(width, height);\n }\n\n /**\n * Paint every {@link PhaseCardSymbol} on `reels` from the reels' own\n * events, for as long as the returned function has not been called. Other\n * symbol classes on the same reels are left alone. A card that arrives\n * mid-phase (a pool swap as the strip wraps) is painted on arrival.\n */\n static watch(reels: Iterable<PhaseWatchedReel>): () => void {\n const stops: Array<() => void> = [];\n for (const reel of reels) {\n // What a card created from now on should show. `rest` after a landing:\n // the landed beat is for the cards that landed, not for a swap-in.\n let current = 'rest';\n const paint = (symbol: ReelSymbol, phase: string): void => {\n if (symbol instanceof PhaseCardSymbol) symbol.setPhase(phase);\n };\n const paintAll = (phase: string): void => {\n for (const symbol of reel.symbols) paint(symbol, phase);\n };\n const onEnter = (name: string): void => {\n current = name;\n paintAll(name);\n };\n const onLanded = (): void => {\n current = 'rest';\n paintAll('landed');\n };\n const onCreated = (_id: string, stripIndex: number): void => {\n const symbol = reel.symbols[stripIndex];\n if (symbol) paint(symbol, current);\n };\n reel.events.on('phase:enter', onEnter);\n reel.events.on('landed', onLanded);\n reel.events.on('symbol:created', onCreated);\n stops.push(() => {\n reel.events.off('phase:enter', onEnter);\n reel.events.off('landed', onLanded);\n reel.events.off('symbol:created', onCreated);\n });\n }\n return () => {\n for (const stop of stops) stop();\n stops.length = 0;\n };\n }\n\n protected override onDeactivate(): void {\n // A pooled card must not carry a phase into whichever cell takes it next.\n this._cancelRest();\n this._phase = 'rest';\n this._color = this._colors.rest;\n super.onDeactivate();\n }\n\n protected override onDestroy(): void {\n this._cancelRest();\n super.onDestroy();\n }\n\n private _cancelRest(): void {\n if (this._restCall) {\n this._restCall.kill();\n this._restCall = null;\n }\n }\n}\n"],"mappings":";;;AA4CA,IAAa,IAAb,cAAgC,EAAW;CACzC;CACA;CACA;CACA;CACA;CAEA,OAAiG;CAEjG,YAAY,GAAyB;EAqBnC,AApBA,MAAM,GACN,KAAK,SAAS,EAAK,OACnB,KAAK,SAAS,EAAK,OACnB,KAAK,aAAa,EAAK,aAAa,UACpC,KAAK,OAAO,IAAI,EAAS,GACzB,KAAK,QAAQ,IAAI,EAAK;GACpB,MAAM,KAAK;GACX,OAAO;IAGL,YACE;IACF,UAAU;IACV,YAAY;IACZ,MAAM,KAAK;IACX,OAAO;GACT;EACF,CAAC,GACD,KAAK,MAAM,OAAO,IAAI,EAAG,GACzB,KAAK,KAAK,SAAS,KAAK,IAAI,GAC5B,KAAK,KAAK,SAAS,KAAK,KAAK;CAC/B;CAEA,WAAqB,GAAyB,CAE9C;CAEA,eAA+B,CAE/B;CAEA,MAAM,UAAyB;EAG7B,OADA,KAAK,QAAQ,GACN,IAAI,SAAS,MAAY;GAO9B,AADA,KAAK,KAAK,aAAa,KAAK,KAAK,GACjC,KAAK,KAAK,aAAa,KAAK,MAAM,KAAK;GACvC,IAAM,IAAe,KAAK,YACpB,IAAK,KAAK,KAAK,SAAS,EAC5B,kBAAkB;IAKhB,AAJA,KAAK,OAAO,MACZ,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,GACzB,KAAK,MAAM,WAAW,GACtB,KAAK,MAAM,MAAM,OAAO,GACxB,EAAQ;GACV,EACF,CAAC;GACD,EAAG,GAAG,KAAK,MAAM,OAAO;IAAE,GAAG;IAAK,GAAG;IAAK,UAAU;IAAM,MAAM;GAAc,GAAG,CAAC,CAAC,CAChF,GAAG,KAAK,OAAO;IAAE,UAAU;IAAO,UAAU;IAAM,MAAM;GAAa,GAAG,CAAC,CAAC,CAC1E,GAAG,KAAK,OAAO;IAAE,UAAU;IAAM,UAAU;IAAM,MAAM;GAAa,GAAG,GAAI,CAAC,CAC5E,GAAG,KAAK,OAAO;IAAE,UAAU;IAAG,UAAU;IAAM,MAAM;GAAa,GAAG,GAAI,CAAC,CACzE,GAAG,KAAK,MAAM,OAAO;IAAE,GAAG;IAAG,GAAG;IAAG,UAAU;IAAM,MAAM;GAAa,GAAG,GAAI;GAGhF,IAAM,IAAQ,CACZ,KAAK,KAAK,YAAY,WAAa,KAAK,MAAM,MAAM,OAAO,QAAS,GACpE,KAAK,KAAK,YAAY,WAAa,KAAK,MAAM,MAAM,OAAO,CAAa,CAC1E;GACA,KAAK,OAAO;IAAE;IAAI;IAAO;GAAQ;EACnC,CAAC;CACH;CAEA,gBAAsB;EAOpB,AANA,KAAK,QAAQ,GACb,KAAK,KAAK,aAAa,KAAK,KAAK,GACjC,KAAK,KAAK,aAAa,KAAK,MAAM,KAAK,GACvC,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,GACzB,KAAK,MAAM,WAAW,GACtB,KAAK,MAAM,MAAM,OAAO,KAAK,YAC7B,KAAK,KAAK,QAAQ;CACpB;CAEA,YAAqC;EACnC,KAAK,QAAQ;CACf;CAQA,UAAwB;EACtB,IAAM,IAAM,KAAK;EACZ,OAEL;GADA,KAAK,OAAO,MACZ,EAAI,GAAG,KAAK;GACZ,KAAK,IAAM,KAAQ,EAAI,OAAO,EAAK,KAAK;GACxC,EAAI,QAAQ;EAFA;CAGd;CAEA,OAAO,GAAe,GAAsB;EAO1C,AANA,KAAK,KAAK,MAAM,GAChB,KAAK,KAAK,KAAK,GAAG,GAAG,GAAO,CAAM,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,GAG/D,KAAK,KAAK,KAAK,GAAG,GAAG,IAAQ,GAAG,IAAS,CAAC,CAAC,CAAC,OAAO;GAAE,OAAO;GAAU,OAAO;GAAG,OAAO;EAAK,CAAC,GAC7F,KAAK,MAAM,IAAI,IAAQ,GACvB,KAAK,MAAM,IAAI,IAAS;EAIxB,IAAM,IAAW,KAAK,IAAI,GAAG,KAAK,OAAO,MAAM,GACzC,IAAO,IAAS,KAEhB,IAAQ,IAAQ,MAAQ,IAAW;EACzC,KAAK,MAAM,MAAM,WAAW,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,IAAI,GAAM,CAAI,CAAC,CAAC;CAC1E;AACF,GAMa,IAAyE;CACpF;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;CACxC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;CACxC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;CACxC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAK;CACzC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;CACxC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;CACxC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;CACxC;EAAE,IAAI;EAAM,OAAO;EAAU,OAAO;CAAI;AAC1C,GAGa,IAAY;CACvB,IAAI;CACJ,OAAO;CACP,OAAO;CACP,WAAW;AACb,GCjLa,IAAsD;CACjE,MAAM;CACN,OAAO;CACP,MAAM;CACN,cAAc;CACd,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,gBAAgB;CAChB,iBAAiB;CACjB,kBAAkB;CAClB,OAAO;AACT,GAuBM,IAAoB,KA4Bb,IAAb,MAAa,UAAwB,EAAW;CAC9C;CACA;CACA,SAAiB;CACjB,SAAiB;CACjB,UAAkB;CAClB,YAA4C;CAE5C,YAAY,GAA8B;EACxC,IAAM,IAAiC,EAAE,GAAG,EAAkB;EAC9D,KAAK,IAAM,CAAC,GAAO,MAAU,OAAO,QAAQ,EAAK,UAAU,CAAC,CAAC,GAC3D,AAAI,MAAU,KAAA,MAAW,EAAO,KAAS;EAI3C,AAFA,MAAM;GAAE,OAAO,EAAO;GAAM,OAAO,EAAK;GAAO,WAAW,EAAK;EAAU,CAAC,GAC1E,KAAK,UAAU,GACf,KAAK,YAAY,EAAK,YAAY;CACpC;CAGA,IAAI,QAAgB;EAClB,OAAO,KAAK;CACd;CAOA,SAAS,GAAqB;EAK5B,IAJA,KAAK,YAAY,GACjB,KAAK,SAAS,GACd,KAAK,SAAS,KAAK,QAAQ,MAAU,KAAK,QAAQ,OAC9C,KAAK,SAAS,KAAG,KAAK,OAAO,KAAK,QAAQ,KAAK,OAAO,GACtD,MAAU,UAAU;GACtB,IAAI,KAAK,aAAa,GAAG;IACvB,KAAK,SAAS,MAAM;IACpB;GACF;GACA,KAAK,YAAY,KAAK,KAAK,YAAY,KAAK,YAAY,WAAY;IAElE,AADA,KAAK,YAAY,MACjB,KAAK,SAAS,MAAM;GACtB,CAAC;EACH;CACF;CAEA,OAAgB,GAAe,GAAsB;EAGnD,AAFA,KAAK,SAAS,GACd,KAAK,UAAU,GACf,MAAM,OAAO,GAAO,CAAM;CAC5B;CAQA,OAAO,MAAM,GAA+C;EAC1D,IAAM,IAA2B,CAAC;EAClC,KAAK,IAAM,KAAQ,GAAO;GAGxB,IAAI,IAAU,QACR,KAAS,GAAoB,MAAwB;IACzD,AAAI,aAAkB,KAAiB,EAAO,SAAS,CAAK;GAC9D,GACM,KAAY,MAAwB;IACxC,KAAK,IAAM,KAAU,EAAK,SAAS,EAAM,GAAQ,CAAK;GACxD,GACM,KAAW,MAAuB;IAEtC,AADA,IAAU,GACV,EAAS,CAAI;GACf,GACM,UAAuB;IAE3B,AADA,IAAU,QACV,EAAS,QAAQ;GACnB,GACM,KAAa,GAAa,MAA6B;IAC3D,IAAM,IAAS,EAAK,QAAQ;IAC5B,AAAI,KAAQ,EAAM,GAAQ,CAAO;GACnC;GAIA,AAHA,EAAK,OAAO,GAAG,eAAe,CAAO,GACrC,EAAK,OAAO,GAAG,UAAU,CAAQ,GACjC,EAAK,OAAO,GAAG,kBAAkB,CAAS,GAC1C,EAAM,WAAW;IAGf,AAFA,EAAK,OAAO,IAAI,eAAe,CAAO,GACtC,EAAK,OAAO,IAAI,UAAU,CAAQ,GAClC,EAAK,OAAO,IAAI,kBAAkB,CAAS;GAC7C,CAAC;EACH;EACA,aAAa;GACX,KAAK,IAAM,KAAQ,GAAO,EAAK;GAC/B,EAAM,SAAS;EACjB;CACF;CAEA,eAAwC;EAKtC,AAHA,KAAK,YAAY,GACjB,KAAK,SAAS,QACd,KAAK,SAAS,KAAK,QAAQ,MAC3B,MAAM,aAAa;CACrB;CAEA,YAAqC;EAEnC,AADA,KAAK,YAAY,GACjB,MAAM,UAAU;CAClB;CAEA,cAA4B;EAC1B,AAEE,KAAK,eADL,KAAK,UAAU,KAAK,GACH;CAErB;AACF"}