pixi-reels 3.1.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.md +10 -0
- package/dist/PhaseCardSymbol-Ct9bCE4I.cjs +2 -0
- package/dist/PhaseCardSymbol-Ct9bCE4I.cjs.map +1 -0
- package/dist/PhaseCardSymbol-bVa7GpDn.js +213 -0
- package/dist/PhaseCardSymbol-bVa7GpDn.js.map +1 -0
- package/dist/ReelSymbol-B5QBhkmw.cjs.map +1 -1
- package/dist/ReelSymbol-C7Pd7BeO.js.map +1 -1
- package/dist/SpinMetrics-9nsu5jI6.cjs +2 -0
- package/dist/SpinMetrics-9nsu5jI6.cjs.map +1 -0
- package/dist/SpinMetrics-qPcdD4Hw.js +273 -0
- package/dist/SpinMetrics-qPcdD4Hw.js.map +1 -0
- package/dist/SpineSymbol-C6V1wpFV.cjs +2 -0
- package/dist/SpineSymbol-C6V1wpFV.cjs.map +1 -0
- package/dist/{SpineSymbol-CSTDCL4W.js → SpineSymbol-W5z5WZ2r.js} +29 -23
- package/dist/SpineSymbol-W5z5WZ2r.js.map +1 -0
- package/dist/board/HoldAndWinBoard.d.ts +2 -2
- package/dist/board/HoldAndWinBoard.d.ts.map +1 -1
- package/dist/board/HoldAndWinBuilder.d.ts +19 -9
- package/dist/board/HoldAndWinBuilder.d.ts.map +1 -1
- package/dist/board/HwTypes.d.ts +9 -1
- package/dist/board/HwTypes.d.ts.map +1 -1
- package/dist/config/types.d.ts +4 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/core/Reel.d.ts +23 -11
- package/dist/core/Reel.d.ts.map +1 -1
- package/dist/core/ReelSet.d.ts +238 -1
- package/dist/core/ReelSet.d.ts.map +1 -1
- package/dist/core/ReelSetBuilder.d.ts +3 -0
- package/dist/core/ReelSetBuilder.d.ts.map +1 -1
- package/dist/core/expand.d.ts +125 -0
- package/dist/core/expand.d.ts.map +1 -0
- package/dist/debug/DebugPlaque.d.ts +119 -0
- package/dist/debug/DebugPlaque.d.ts.map +1 -0
- package/dist/debug/SpinMetrics.d.ts +191 -0
- package/dist/debug/SpinMetrics.d.ts.map +1 -0
- package/dist/debug/WinLines.d.ts +78 -0
- package/dist/debug/WinLines.d.ts.map +1 -0
- package/dist/debug/debug.d.ts +4 -0
- package/dist/debug/debug.d.ts.map +1 -1
- package/dist/debug/debugOverlay.d.ts +45 -16
- package/dist/debug/debugOverlay.d.ts.map +1 -1
- package/dist/debug/index.d.ts +10 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/roundRectPath.d.ts +19 -0
- package/dist/debug/roundRectPath.d.ts.map +1 -0
- package/dist/{debug-CzvrK01X.js → debug-C_ynexnr.js} +893 -780
- package/dist/debug-C_ynexnr.js.map +1 -0
- package/dist/debug-hpj0Uzkm.cjs +4 -0
- package/dist/debug-hpj0Uzkm.cjs.map +1 -0
- package/dist/debug.cjs +4 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.js +797 -0
- package/dist/debug.js.map +1 -0
- package/dist/events/ReelEvents.d.ts +51 -0
- package/dist/events/ReelEvents.d.ts.map +1 -1
- package/dist/frame/ColumnTarget.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -396
- package/dist/index.js.map +1 -1
- package/dist/pool/ObjectPool.d.ts +8 -0
- package/dist/pool/ObjectPool.d.ts.map +1 -1
- package/dist/snapshot/SpinTextureCache.d.ts +34 -9
- package/dist/snapshot/SpinTextureCache.d.ts.map +1 -1
- package/dist/snapshot/StaticSpinSymbol.d.ts +1 -1
- package/dist/snapshot/StaticSpinSymbol.d.ts.map +1 -1
- package/dist/spin/SpinController.d.ts +105 -12
- package/dist/spin/SpinController.d.ts.map +1 -1
- package/dist/spin/bigSymbolBlocks.d.ts +17 -0
- package/dist/spin/bigSymbolBlocks.d.ts.map +1 -0
- package/dist/spine/SpineReelSymbol.d.ts +73 -17
- package/dist/spine/SpineReelSymbol.d.ts.map +1 -1
- package/dist/spine/index.d.ts +1 -1
- package/dist/spine/index.d.ts.map +1 -1
- package/dist/spine.cjs +1 -1
- package/dist/spine.cjs.map +1 -1
- package/dist/spine.js +57 -40
- package/dist/spine.js.map +1 -1
- package/dist/symbols/CardSymbol.d.ts +10 -0
- package/dist/symbols/CardSymbol.d.ts.map +1 -1
- package/dist/symbols/ReelSymbol.d.ts.map +1 -1
- package/dist/symbols/SpineSymbol.d.ts +9 -1
- package/dist/symbols/SpineSymbol.d.ts.map +1 -1
- package/dist/symbols/SpriteSymbol.d.ts +3 -0
- package/dist/symbols/SpriteSymbol.d.ts.map +1 -1
- package/dist/symbols/SymbolFactory.d.ts +5 -1
- package/dist/symbols/SymbolFactory.d.ts.map +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.js +12 -12
- package/package.json +15 -2
- package/dist/SpineSymbol-CSTDCL4W.js.map +0 -1
- package/dist/SpineSymbol-DorL_xII.cjs +0 -2
- package/dist/SpineSymbol-DorL_xII.cjs.map +0 -1
- package/dist/debug-CzvrK01X.js.map +0 -1
- package/dist/debug-mp1gR9Sd.cjs +0 -4
- package/dist/debug-mp1gR9Sd.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# pixi-reels
|
|
2
2
|
|
|
3
|
+
## 4.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#268](https://github.com/schmooky/pixi-reels/pull/268) [`bcd08f5`](https://github.com/schmooky/pixi-reels/commit/bcd08f566b5ff639a7963ae9c553d7c384d75450) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: big symbols stay whole wherever the engine places symbols outside a landing. A big symbol in the builder's `initialFrame()` or in `addReels({ initialFrame })` now gets its whole block, as in `setResult()` (it used to get no stubs, draw at one cell and leave its neighbours reported under it), and a block that does not fit throws before anything is built. `pin()` and `movePin()` at rest no longer turn the stubs of a block that shares the reel into extra anchors. A pin on a cell of a big symbol replaces the block, so the pin shows at once, and a big symbol pinned at rest has to fit, as in a landing.
|
|
8
|
+
|
|
9
|
+
- [#268](https://github.com/schmooky/pixi-reels/pull/268) [`bcd08f5`](https://github.com/schmooky/pixi-reels/commit/bcd08f566b5ff639a7963ae9c553d7c384d75450) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix: `SpineSymbol` no longer clears every listener on its skeleton's animation state. It listened for its win on the state and called `state.clearListeners()` when the win completed, on `stopAnimation()` and on every recycle, dropping any listener a game or subclass had added, and, run inside spine's dispatch, making the listeners after it miss the event. It now listens on the win's own track entry and leaves the state's listeners alone (it still clears them when the symbol is destroyed, with its skeleton).
|
|
10
|
+
|
|
11
|
+
Fix: a `SpineSymbol.playWin()` promise settles when something else takes track 0 before the win completes (a subclass setting another animation, a second `playWin()`), instead of waiting forever. A second `playWin()` settles the first.
|
|
12
|
+
|
|
13
|
+
## 4.0.0
|
|
14
|
+
|
|
15
|
+
### Major Changes
|
|
16
|
+
|
|
17
|
+
- [#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.
|
|
18
|
+
|
|
19
|
+
**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).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { debugOverlay } from 'pixi-reels/debug';
|
|
23
|
+
|
|
24
|
+
const overlay = debugOverlay(reelSet, { layers: 'all', live: true, ticker: app.ticker });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**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.
|
|
28
|
+
|
|
29
|
+
**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.
|
|
30
|
+
|
|
31
|
+
**`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.
|
|
32
|
+
|
|
33
|
+
**`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.
|
|
34
|
+
|
|
35
|
+
**`__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.
|
|
36
|
+
|
|
37
|
+
**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`.
|
|
38
|
+
|
|
39
|
+
- [#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.
|
|
40
|
+
|
|
41
|
+
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)`.
|
|
42
|
+
|
|
43
|
+
`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`.
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- [#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.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const spin = reelSet.spin();
|
|
51
|
+
reelSet.setResult(res.columns.slice(0, 5));
|
|
52
|
+
await spin;
|
|
53
|
+
await reelSet.expand({
|
|
54
|
+
columns: res.columns.slice(5), // the server decides how many
|
|
55
|
+
step: 1, // or 2, or (info) => ...
|
|
56
|
+
onStepAdded: () => camera.panTo(reelSet.viewport.maskWidth), // the board's new width
|
|
57
|
+
onStepLanded: (step) => showWays(step.result.symbols),
|
|
58
|
+
});
|
|
59
|
+
reelSet.removeReels(); // next round: back to the board the builder made
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
- [#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.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await coin.playOneShot('collect'); // track 0, then idle
|
|
74
|
+
await coin.playOneShot('react_u', { track: 1 }); // overlay, cleared after
|
|
75
|
+
await coin.playOneShot('explode', { then: 'hold' }); // stays on its last frame
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
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`.
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
- [#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.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { WinLines } from 'pixi-reels/debug';
|
|
88
|
+
|
|
89
|
+
const lines = new WinLines(reelSet);
|
|
90
|
+
lines.ways(win.perReel); // [[0, 2], [1], [0, 1, 2]]: winning cells per reel
|
|
91
|
+
reelSet.events.on('spin:start', () => lines.clear());
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Patch Changes
|
|
95
|
+
|
|
96
|
+
- [#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.
|
|
97
|
+
|
|
98
|
+
- [#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`).
|
|
99
|
+
|
|
100
|
+
- [#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.
|
|
101
|
+
|
|
102
|
+
`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.
|
|
103
|
+
|
|
104
|
+
- [#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.
|
|
105
|
+
|
|
3
106
|
## 3.1.0
|
|
4
107
|
|
|
5
108
|
### Minor 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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReelSymbol-B5QBhkmw.cjs","names":[],"sources":["../src/utils/gsap.ts","../src/symbols/ReelSymbol.ts"],"sourcesContent":["import { gsap as resolvedGsap } from 'gsap';\n\n/** The gsap namespace type, as the engine passes it around. */\nexport type Gsap = typeof resolvedGsap;\n\n/**\n * The gsap instance resolved at lib-load time. Used by any `ReelSet` whose\n * builder did not call `.gsap(...)`, and by any symbol not yet bound to a\n * set.\n *\n * **Why the engine passes gsap around at all:** under tools that resolve\n * modules through symlinked workspaces (vite + a locally-linked pixi-reels,\n * pnpm dev setups, esbuild plugin chains), the gsap import inside the lib's\n * compiled `dist/index.js` and the gsap import in the consumer's source can\n * resolve to *different module instances*. each with its own root timeline.\n * The consumer drives one, the lib's tweens live on the other, and reels\n * stall at progress 0. `ReelSetBuilder.gsap(myGsap)` hands the engine the\n * consumer's instance so both live on the same timeline.\n *\n * Held PER REEL SET, not process-wide: two sets on one stage can be driven\n * by different gsap instances, which is what a composed stage (a banner reel\n * above a main grid, a bonus board beside it) needs.\n */\nexport const DEFAULT_GSAP: Gsap = resolvedGsap;\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelCellInset, ReelCellQuad, ReelLandingContext } from '../config/types.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * One visible cell on a reel. the thing that actually draws.\n *\n * `ReelSymbol` is the abstract base class. Subclass it to pick a rendering\n * technology (`SpriteSymbol`, `AnimatedSpriteSymbol`, `SpineSymbol`, or a\n * custom class of your own). The reel set pools instances aggressively:\n * one instance is reused many times as it scrolls off one identity and on\n * to another, so implementations must never assume \"I was just created\".\n *\n * Required lifecycle hooks:\n *\n * - `onActivate(symbolId)`. the pool just handed me a new identity. Swap\n * texture, restart animations, bring myself out of any \"ended\" pose.\n * - `onDeactivate()`. I am about to be pooled. Pause animations, clear\n * listeners, leave myself in a clean state for the next activation.\n * - `playWin()`. the spotlight is celebrating me. Return a promise that\n * resolves when the one-shot animation is done.\n * - `stopAnimation()`. spotlight is over, return to idle.\n * - `resize(w, h)`. the reel's cell size changed (on every symbol swap).\n * Store the dimensions and reposition internal children. Forgetting\n * this is the single most common \"why do my symbols scatter\" bug.\n *\n * ```\n * create → activate(symbolId) → [playWin / stopAnimation]\n * → deactivate\n * → activate(newId) → ...\n * ```\n *\n * There's no hidden GC. Hold resources? Override `onDestroy()`.\n */\nexport abstract class ReelSymbol implements Disposable {\n /** The PixiJS container that holds this symbol's visual. */\n public readonly view: Container;\n\n private _symbolId: string = '';\n private _isDestroyed = false;\n\n private _gsap: Gsap = DEFAULT_GSAP;\n private _mainAxis: 'x' | 'y' = 'y';\n\n constructor() {\n this.view = new Container();\n }\n\n /**\n * The gsap instance this symbol should animate on. Use it instead of\n * importing `gsap` in a subclass: under a symlinked-workspace module\n * resolution your import and the engine's can be different instances, and\n * only this one is on the timeline the reel set actually drives.\n *\n * Bound to the owning set by `SymbolFactory`; falls back to the instance\n * resolved at lib-load time for a symbol built outside a set.\n */\n protected get gsap(): Gsap {\n return this._gsap;\n }\n\n /**\n * @internal. Called by `SymbolFactory` when the symbol is created, so a\n * pooled symbol animates on its own set's gsap rather than whichever set\n * happened to build last.\n */\n bindGsap(instance: Gsap): void {\n this._gsap = instance;\n }\n\n /**\n * The screen axis the owning set's strips travel along: `'y'` for a\n * vertical set, `'x'` for a horizontal one.\n *\n * Symbols are otherwise orientation-agnostic - `resize(width, height)` is\n * screen-space and always will be. This exists for the few effects that\n * genuinely follow travel, motion blur being the one in the box.\n */\n protected get mainAxis(): 'x' | 'y' {\n return this._mainAxis;\n }\n\n /** @internal. Bound by `SymbolFactory` from the set's orientation. */\n bindMainAxis(prop: 'x' | 'y'): void {\n this._mainAxis = prop;\n }\n\n get symbolId(): string {\n return this._symbolId;\n }\n\n private _landing: Promise<void> | null = null;\n\n /**\n * The landing beat this symbol started on its latest `onReelLanded()`, or\n * `null` if it started none. Stays set (resolved) until the reel moves\n * again or the symbol is pooled, so a presenter that lands cells one at a\n * time - `HoldAndWinBoard` on a lock, a win spotlight - can sequence after\n * the landing (`await symbol.landing`) instead of stomping it, and can\n * tell a symbol that already landed from one that still needs\n * `playLanding()`.\n *\n * Subclasses report a landing through {@link trackLanding}.\n */\n get landing(): Promise<void> | null {\n return this._landing;\n }\n\n /**\n * Subclass helper: record `run` as this symbol's landing beat (see\n * {@link landing}) and return it. `SpineReelSymbol` wraps its landing\n * one-shot in it; a sprite symbol wraps its settle tween.\n */\n protected trackLanding(run: Promise<void>): Promise<void> {\n this._landing = run;\n return run;\n }\n\n /** @internal Called by the reel when it leaves rest; the landing is over. */\n resetLanding(): void {\n this._landing = null;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Activate the symbol with a new identity. Called when the symbol enters\n * the visible reel or is recycled from the pool. Resets container\n * transform / filter state for parity with deactivate().\n */\n activate(symbolId: string): void {\n this._symbolId = symbolId;\n this.view.visible = true;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n // Tint is a mutable visual like the rest: a symbol arriving from the pool\n // must not wear a colour some previous cell put on it. Cleared on both\n // sides of the recycle so neither half has to trust the other.\n this.view.tint = 0xffffff;\n this.view.zIndex = 0;\n this.onActivate(symbolId);\n }\n\n /**\n * Deactivate the symbol before returning it to the pool. Stops\n * animations, hides the view, and resets container transform / filter\n * state so subclass decorations don't leak across recycles.\n */\n deactivate(): void {\n this.stopAnimation();\n this.onDeactivate();\n this._symbolId = '';\n this._landing = null;\n this.view.visible = false;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n // See activate(). A symbol dimmed by the board (or tinted by a game) is\n // released to the pool the moment its cell swaps, and the pool hands the\n // same instance to a different cell - which would inherit the colour.\n this.view.tint = 0xffffff;\n this.view.zIndex = 0;\n }\n\n /** Pool reset. aliases deactivate. */\n reset(): void {\n this.deactivate();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n // A pooled symbol's view stays a child of the reel container it was\n // released from, so `Reel.destroy()` has already destroyed it (children\n // included) by the time the pool disposes the symbol. There is nothing\n // left to stop or clean visually, and a `stopAnimation()` that reaches\n // into a destroyed child (`CardSymbol` resets its label's scale) throws.\n if (!this.view.destroyed) {\n this.stopAnimation();\n this.onDeactivate();\n }\n this.onDestroy();\n if (!this.view.destroyed) this.view.destroy({ children: true });\n this._isDestroyed = true;\n }\n\n /** Subclass hook: set up visuals for the given symbolId. */\n protected abstract onActivate(symbolId: string): void;\n\n /** Subclass hook: clean up visuals. */\n protected abstract onDeactivate(): void;\n\n /** Subclass hook: additional cleanup on destroy. */\n protected onDestroy(): void {}\n\n /** Play the win/highlight animation for this symbol. Resolves when complete. */\n abstract playWin(): Promise<void>;\n\n /**\n * Play the landing beat for this symbol - the settle a symbol does as its\n * reel stops, before (or instead of) any celebration. Resolves when done.\n *\n * The reel engine never calls this itself: `onReelLanded()` is the\n * engine-driven hook, fired on every landed symbol. This is the explicit\n * counterpart for a caller that lands cells one at a time - `HoldAndWinBoard`\n * calls it on each lock when built with `lockAnimation('landing')`.\n *\n * Default: resolves at once. `SpineReelSymbol` overrides it with the\n * skeleton's `landing` track; a sprite symbol can override it with a tween.\n */\n async playLanding(): Promise<void> {}\n\n /** Immediately stop any running animation and return to idle. */\n abstract stopAnimation(): void;\n\n /** Resize the symbol's visual to fit the given dimensions. */\n abstract resize(width: number, height: number): void;\n\n /**\n * Play the cascade-destruction animation for this symbol. Called by\n * consumers (typically via `reelSet.destroySymbols(...)`) to disintegrate\n * a winning cell before the next cascade refill drops fresh symbols in.\n *\n * Override in subclasses for art-appropriate destruction, e.g. a Spine\n * symbol can play its `disintegration` track here, or a sprite symbol can\n * swap to a shatter atlas. The promise must resolve when the symbol is no\n * longer visible.\n *\n * Default: a snappy \"poof\" centered on the symbol's bounds regardless of\n * the view's anchor. Tiny anticipation pop (~60 ms) then a fast implode to\n * `scale: 0` + `alpha: 0` (~140 ms), ~200 ms total, no rotation. Reads\n * cleanly under win-cluster pacing without competing with the win\n * presenter. The view is left at `alpha: 0` (destroyed); position / pivot\n * are restored so pool reuse via `_replaceSymbol`'s same-id fast path\n * doesn't inherit a stale pivot offset.\n *\n * `opts.delay`. seconds to wait before the animation starts. Use to\n * stagger a cluster of winners (e.g. `i * 0.015`).\n * `opts.signal`. abort signal. If aborted (now or mid-animation), the\n * tween is killed and the view is snapped to its destroyed pose\n * (`alpha: 0`, transform restored). The promise resolves normally. abort\n * means \"skip to the end,\" not \"fail\". Subclasses that override this\n * method MUST honor the signal or document why they can't (e.g. a Spine\n * `disintegration` track is uninterruptible).\n */\n async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n const view = this.view;\n // Capture original transform so pool reuse sees a clean state.\n const originalPivotX = view.pivot.x;\n const originalPivotY = view.pivot.y;\n const originalX = view.x;\n const originalY = view.y;\n\n // Pivot to bounds-center so the scale collapses around the visual\n // centre instead of the view's (0,0) corner. and compensate position\n // so the symbol doesn't visibly jump when the pivot moves.\n const bounds = view.getLocalBounds();\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n // Moving the pivot moves the view by `delta * scale`, not by `delta`: a\n // container renders a local point at `position + (point - pivot) * scale`.\n // Scale is 1 on a plain reel, which is why dropping it went unnoticed, but\n // a curved reel scales every cell and the symbol would jump on destroy.\n view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX) * view.scale.x;\n view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n const delay = opts?.delay ?? 0;\n const signal = opts?.signal;\n\n const snapDestroyed = (): void => {\n view.alpha = 0;\n view.scale.set(0, 0);\n };\n\n // Pre-abort: skip the tween entirely and snap to the destroyed pose.\n if (signal?.aborted) {\n snapDestroyed();\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n view.alpha = 0;\n return;\n }\n\n await new Promise<void>((resolve) => {\n const tl = this.gsap\n .timeline({ onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, delay })\n // Brief anticipation pop. small upscale, ~60 ms, with overshoot\n // so the implode reads as a release. No rotation.\n .to(view.scale, { x: 1.1, y: 1.1, duration: 0.06, ease: 'back.out(2.5)' })\n // Snap implode. scale -> 0 + alpha -> 0 together, snappy ease-in\n // so the symbol collapses into the cell centre and is gone.\n .to(view.scale, { x: 0, y: 0, duration: 0.14, ease: 'power3.in' }, '<+=0.04')\n .to(view, { alpha: 0, duration: 0.14, ease: 'power3.in' }, '<');\n\n const onAbort = (): void => {\n tl.kill();\n snapDestroyed();\n resolve();\n };\n if (signal) signal.addEventListener('abort', onAbort, { once: true });\n });\n\n // Restore transform. alpha stays 0 (the symbol IS destroyed). Scale\n // restored to 1 so pool reuse via `_replaceSymbol`'s same-id fast path\n // doesn't inherit a stale 0× scale; _replaceSymbol also resets scale\n // explicitly but a defensive restore here makes the destroyed cell\n // observably \"ready to be re-skinned\" between calls.\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n }\n\n /**\n * Animate this symbol AWAY, without destroying it.\n *\n * The counterpart to {@link playIn}, and the seam a mystery reveal needs:\n * the cells on screen dissolve, their identities are swapped underneath, and\n * the new ones arrive. `playDestroy` is the cascade's version of the same\n * beat and is deliberately separate - it is tuned as a \"this cell was a\n * winner and is being consumed\" poof, and a reveal is not that.\n *\n * Leaves the view hidden (`alpha: 0`) with its transform restored, so the\n * caller may swap the identity and call `playIn` next.\n *\n * Override for art-appropriate exits - a Spine symbol plays its own `out`\n * track here. Honour `opts.signal`: abort means \"snap to the end\", not\n * \"fail\", so the promise still resolves.\n *\n * Default: a ~180 ms shrink-and-fade with no overshoot, centred on the\n * symbol's bounds rather than the view origin.\n */\n async playOut(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n await this._tweenPresence('out', opts);\n }\n\n /**\n * Animate this symbol IN, from nothing to its resting pose.\n *\n * Owns its own start pose. A symbol that has just been re-activated is fully\n * visible (`activate()` resets alpha and scale), so an entrance that assumed\n * it started hidden would pop before it animated. This sets the hidden pose\n * first, then plays.\n *\n * Override for art-appropriate entrances - a Spine symbol plays its own `in`\n * track here. Honour `opts.signal`: abort snaps to the RESTING pose (alpha 1,\n * scale 1), because arriving is what the caller asked for.\n *\n * Default: a ~200 ms fade and scale-up with a small overshoot.\n */\n async playIn(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n await this._tweenPresence('in', opts);\n }\n\n /**\n * Shared body of {@link playIn} / {@link playOut}.\n *\n * One function because the two are the same tween read in opposite\n * directions, and because the fiddly part - pivoting to the visual centre so\n * the scale does not collapse toward the view's (0, 0) corner, and\n * compensating the position for the pivot move - is identical and easy to get\n * subtly wrong twice. Same correction as `playDestroy`: a container renders a\n * local point at `position + (point - pivot) * scale`, so a curved reel (which\n * scales every cell) would make the symbol jump without the `* scale` term.\n */\n private async _tweenPresence(\n direction: 'in' | 'out',\n opts?: { delay?: number; signal?: AbortSignal },\n ): Promise<void> {\n const view = this.view;\n const originalPivotX = view.pivot.x;\n const originalPivotY = view.pivot.y;\n const originalX = view.x;\n const originalY = view.y;\n\n const bounds = view.getLocalBounds();\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX) * view.scale.x;\n view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n const restorePivot = (): void => {\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n };\n const snapEnd = (): void => {\n if (direction === 'out') {\n view.alpha = 0;\n view.scale.set(1, 1);\n } else {\n view.visible = true;\n view.alpha = 1;\n view.scale.set(1, 1);\n }\n };\n\n const delay = opts?.delay ?? 0;\n const signal = opts?.signal;\n\n if (direction === 'in') {\n // Own the start pose: `activate()` leaves a freshly swapped symbol fully\n // visible, so without this the new art is on screen for a frame before\n // the entrance begins.\n view.visible = true;\n view.alpha = 0;\n view.scale.set(0.72, 0.72);\n }\n\n if (signal?.aborted) {\n snapEnd();\n restorePivot();\n return;\n }\n\n await new Promise<void>((resolve) => {\n const onAbort = (): void => {\n tl.kill();\n snapEnd();\n resolve();\n };\n const tl = this.gsap.timeline({\n delay,\n onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n },\n });\n if (direction === 'out') {\n tl.to(view.scale, { x: 0.72, y: 0.72, duration: 0.18, ease: 'power2.in' }).to(\n view,\n { alpha: 0, duration: 0.18, ease: 'power2.in' },\n '<',\n );\n } else {\n tl.to(view.scale, { x: 1, y: 1, duration: 0.2, ease: 'back.out(1.7)' }).to(\n view,\n { alpha: 1, duration: 0.16, ease: 'power2.out' },\n '<',\n );\n }\n if (signal) signal.addEventListener('abort', onAbort, { once: true });\n });\n\n // Scale back to 1 either way: a hidden symbol still has to be a legal\n // resting pose, or pool reuse inherits the shrunken scale.\n view.scale.set(1, 1);\n restorePivot();\n }\n\n /**\n * The owning reel is curved: render into this projected quad instead of the\n * flat cell rectangle. `null` means the reel is flat again.\n *\n * The quad's corners are SCREEN-space and local to this view's own origin,\n * clockwise from top-left, and `width` / `height` give the flat cell box the\n * quad replaces. Called on every frame of motion and on every placement, so\n * it must be cheap and idempotent.\n *\n * **The default is an approximation.** A `Container` transform is affine, so\n * it cannot express a trapezoid; the base class fits the quad as closely as\n * an affine transform can - a UNIFORM scale about the quad's centre. Uniform\n * on purpose: symbol art is usually smaller than its cell and has a shape a\n * player recognises, and squashing one axis turns a `7` into a squashed `7`\n * rather than a `7` seen at an angle.\n *\n * Override this to render the real perspective. {@link SpriteSymbol} and\n * {@link AnimatedSpriteSymbol} do, by drawing their texture through a\n * `PerspectiveMesh`, which costs no extra render pass because the content is\n * already a texture. A symbol whose content is an arbitrary subtree (Spine, a\n * composite of sprites and text) cannot do that without rendering itself to a\n * texture every frame, so it keeps the affine fit.\n *\n * Whatever you do here, do NOT move `view.position`: the reel reads that\n * coordinate back to work out which slot this symbol is in.\n */\n /**\n * The part of its cell this symbol's art actually covers, or `null` (the\n * default) for \"all of it\".\n *\n * Slot art is usually smaller than its cell - a trimmed atlas frame is a\n * shape floating in a much bigger transparent box - and the reel needs to\n * know that to project the rectangle the art is really in. Overriding this\n * is what stops a small symbol being inflated to the cell's edges and given\n * the cell's keystone instead of its own, milder one.\n *\n * Read once per projection, so it may change with the symbol's identity.\n */\n get cellInset(): ReelCellInset | null {\n return null;\n }\n\n applyCellQuad(quad: ReelCellQuad | null): void {\n const view = this.view;\n if (quad === null) {\n view.scale.set(1, 1);\n view.pivot.set(0, 0);\n return;\n }\n // Measure the trapezoid: the mean of its two parallel edges, and the\n // distance between their midpoints.\n const nearWidth = Math.hypot(quad.x1 - quad.x0, quad.y1 - quad.y0);\n const farWidth = Math.hypot(quad.x2 - quad.x3, quad.y2 - quad.y3);\n const across = (nearWidth + farWidth) / 2;\n const along = Math.hypot(\n (quad.x3 + quad.x2) / 2 - (quad.x0 + quad.x1) / 2,\n (quad.y3 + quad.y2) / 2 - (quad.y0 + quad.y1) / 2,\n );\n // CONTAIN, not cover. A quad in the middle of the window is TALLER than\n // the flat cell (that is the drum magnifying what faces you), so a scale\n // picked to fill it would also make the symbol wider than its column and\n // overlap its neighbours - very visible on art that fills its cell\n // edge-to-edge. Taking the smaller ratio keeps every symbol inside its own\n // projected footprint at the cost of a little slack on one axis.\n const scale =\n quad.width > 0 && quad.height > 0\n ? Math.min(across / quad.width, along / quad.height)\n : 1;\n\n const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n view.scale.set(scale, scale);\n // A container renders a local point at `position + (point - pivot) * scale`\n // and `position` must not move, so putting the flat box's centre on the\n // quad's centre has to be paid for entirely out of the pivot.\n view.pivot.set(\n quad.x + quad.width / 2 - cx / scale,\n quad.y + quad.height / 2 - cy / scale,\n );\n }\n\n /**\n * Lifecycle hook: the owning reel is spinning.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur,\n * StaticSpinSymbol) to swap to a spin presentation automatically.\n *\n * Fired on every strip symbol (visible AND buffer cells) when the reel\n * enters the spin phase, and again with `joinedMidSpin: true` on each\n * symbol freshly installed while the reel is already spinning (pool\n * recycling wipes symbol state, so a wrapped-in symbol can't know the\n * reel is moving without this). Implementations MUST be idempotent.\n * the same instance can be notified more than once per spin.\n *\n * @param joinedMidSpin true when this symbol was installed into a reel\n * already at speed (skip start-of-spin transitions like blur ramps).\n */\n onReelSpinStart(joinedMidSpin?: boolean): void {}\n\n /**\n * Lifecycle hook: the owning reel is about to stop (just before bounce).\n * Default: no-op.\n */\n onReelSpinEnd(): void {}\n\n /**\n * Lifecycle hook: the owning reel entered its anticipation (tease) phase.\n * it is still spinning, but slowed enough that the strip is readable.\n * Spin presentations that obscure symbols (blur textures, smear\n * animations) should relax so the player can follow the tease. Also\n * fired on symbols installed while the reel is anticipating.\n * Implementations MUST be idempotent. Default: no-op.\n */\n onReelAnticipationStart(): void {}\n\n /**\n * Lifecycle hook: the owning reel has landed on its final symbols.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayLanding) to fire\n * a landing animation concurrently with the bounce.\n *\n * `ctx` says which reel and cell this symbol landed in, so an override can\n * play a different beat - or none - on a particular reel. The engine always\n * supplies it; it is optional only so an override written as\n * `onReelLanded()` keeps compiling. Fired before the reel's `landing` event\n * and the set's `spin:reelLanding`, by contract.\n */\n onReelLanded(ctx?: ReelLandingContext): void {}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReelSymbol-B5QBhkmw.cjs","names":[],"sources":["../src/utils/gsap.ts","../src/symbols/ReelSymbol.ts"],"sourcesContent":["import { gsap as resolvedGsap } from 'gsap';\n\n/** The gsap namespace type, as the engine passes it around. */\nexport type Gsap = typeof resolvedGsap;\n\n/**\n * The gsap instance resolved at lib-load time. Used by any `ReelSet` whose\n * builder did not call `.gsap(...)`, and by any symbol not yet bound to a\n * set.\n *\n * **Why the engine passes gsap around at all:** under tools that resolve\n * modules through symlinked workspaces (vite + a locally-linked pixi-reels,\n * pnpm dev setups, esbuild plugin chains), the gsap import inside the lib's\n * compiled `dist/index.js` and the gsap import in the consumer's source can\n * resolve to *different module instances*. each with its own root timeline.\n * The consumer drives one, the lib's tweens live on the other, and reels\n * stall at progress 0. `ReelSetBuilder.gsap(myGsap)` hands the engine the\n * consumer's instance so both live on the same timeline.\n *\n * Held PER REEL SET, not process-wide: two sets on one stage can be driven\n * by different gsap instances, which is what a composed stage (a banner reel\n * above a main grid, a bonus board beside it) needs.\n */\nexport const DEFAULT_GSAP: Gsap = resolvedGsap;\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelCellInset, ReelCellQuad, ReelLandingContext } from '../config/types.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * One visible cell on a reel. the thing that actually draws.\n *\n * `ReelSymbol` is the abstract base class. Subclass it to pick a rendering\n * technology (`SpriteSymbol`, `AnimatedSpriteSymbol`, `SpineSymbol`, or a\n * custom class of your own). The reel set pools instances aggressively:\n * one instance is reused many times as it scrolls off one identity and on\n * to another, so implementations must never assume \"I was just created\".\n *\n * Required lifecycle hooks:\n *\n * - `onActivate(symbolId)`. the pool just handed me a new identity. Swap\n * texture, restart animations, bring myself out of any \"ended\" pose.\n * - `onDeactivate()`. I am about to be pooled. Pause animations, clear\n * listeners, leave myself in a clean state for the next activation.\n * - `playWin()`. the spotlight is celebrating me. Return a promise that\n * resolves when the one-shot animation is done.\n * - `stopAnimation()`. spotlight is over, return to idle.\n * - `resize(w, h)`. the reel's cell size changed (on every symbol swap).\n * Store the dimensions and reposition internal children. Forgetting\n * this is the single most common \"why do my symbols scatter\" bug.\n *\n * ```\n * create → activate(symbolId) → [playWin / stopAnimation]\n * → deactivate\n * → activate(newId) → ...\n * ```\n *\n * There's no hidden GC. Hold resources? Override `onDestroy()`.\n */\nexport abstract class ReelSymbol implements Disposable {\n /** The PixiJS container that holds this symbol's visual. */\n public readonly view: Container;\n\n private _symbolId: string = '';\n private _isDestroyed = false;\n\n private _gsap: Gsap = DEFAULT_GSAP;\n private _mainAxis: 'x' | 'y' = 'y';\n\n constructor() {\n this.view = new Container();\n }\n\n /**\n * The gsap instance this symbol should animate on. Use it instead of\n * importing `gsap` in a subclass: under a symlinked-workspace module\n * resolution your import and the engine's can be different instances, and\n * only this one is on the timeline the reel set actually drives.\n *\n * Bound to the owning set by `SymbolFactory`; falls back to the instance\n * resolved at lib-load time for a symbol built outside a set.\n */\n protected get gsap(): Gsap {\n return this._gsap;\n }\n\n /**\n * @internal. Called by `SymbolFactory` when the symbol is created, so a\n * pooled symbol animates on its own set's gsap rather than whichever set\n * happened to build last.\n */\n bindGsap(instance: Gsap): void {\n this._gsap = instance;\n }\n\n /**\n * The screen axis the owning set's strips travel along: `'y'` for a\n * vertical set, `'x'` for a horizontal one.\n *\n * Symbols are otherwise orientation-agnostic - `resize(width, height)` is\n * screen-space and always will be. This exists for the few effects that\n * genuinely follow travel, motion blur being the one in the box.\n */\n protected get mainAxis(): 'x' | 'y' {\n return this._mainAxis;\n }\n\n /** @internal. Bound by `SymbolFactory` from the set's orientation. */\n bindMainAxis(prop: 'x' | 'y'): void {\n this._mainAxis = prop;\n }\n\n get symbolId(): string {\n return this._symbolId;\n }\n\n private _landing: Promise<void> | null = null;\n\n /**\n * The landing beat this symbol started on its latest `onReelLanded()`, or\n * `null` if it started none. Stays set (resolved) until the reel moves\n * again or the symbol is pooled, so a presenter that lands cells one at a\n * time - `HoldAndWinBoard` on a lock, a win spotlight - can sequence after\n * the landing (`await symbol.landing`) instead of stomping it, and can\n * tell a symbol that already landed from one that still needs\n * `playLanding()`.\n *\n * Subclasses report a landing through {@link trackLanding}.\n */\n get landing(): Promise<void> | null {\n return this._landing;\n }\n\n /**\n * Subclass helper: record `run` as this symbol's landing beat (see\n * {@link landing}) and return it. `SpineReelSymbol` wraps its landing\n * one-shot in it; a sprite symbol wraps its settle tween.\n */\n protected trackLanding(run: Promise<void>): Promise<void> {\n this._landing = run;\n return run;\n }\n\n /** @internal Called by the reel when it leaves rest; the landing is over. */\n resetLanding(): void {\n this._landing = null;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Activate the symbol with a new identity. Called when the symbol enters\n * the visible reel or is recycled from the pool. Resets container\n * transform / filter state for parity with deactivate().\n */\n activate(symbolId: string): void {\n this._symbolId = symbolId;\n this.view.visible = true;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n // Tint is a mutable visual like the rest: a symbol arriving from the pool\n // must not wear a colour some previous cell put on it. Cleared on both\n // sides of the recycle so neither half has to trust the other.\n this.view.tint = 0xffffff;\n this.view.zIndex = 0;\n this.onActivate(symbolId);\n }\n\n /**\n * Deactivate the symbol before returning it to the pool. Stops\n * animations, hides the view, and resets container transform / filter\n * state so subclass decorations don't leak across recycles.\n */\n deactivate(): void {\n this.stopAnimation();\n this.onDeactivate();\n this._symbolId = '';\n this._landing = null;\n this.view.visible = false;\n this.view.alpha = 1;\n this.view.scale.set(1, 1);\n this.view.rotation = 0;\n this.view.filters = null;\n // See activate(). A symbol dimmed by the board (or tinted by a game) is\n // released to the pool the moment its cell swaps, and the pool hands the\n // same instance to a different cell - which would inherit the colour.\n this.view.tint = 0xffffff;\n this.view.zIndex = 0;\n }\n\n /** Pool reset. aliases deactivate. */\n reset(): void {\n this.deactivate();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n // The view may already be gone: a container the game destroyed with its\n // children while the view sat in it. There is nothing left to stop or\n // clean visually then, and a `stopAnimation()` that reaches into a\n // destroyed child (`CardSymbol` resets its label's scale) throws.\n if (!this.view.destroyed) {\n this.stopAnimation();\n this.onDeactivate();\n }\n this.onDestroy();\n if (!this.view.destroyed) this.view.destroy({ children: true });\n this._isDestroyed = true;\n }\n\n /** Subclass hook: set up visuals for the given symbolId. */\n protected abstract onActivate(symbolId: string): void;\n\n /** Subclass hook: clean up visuals. */\n protected abstract onDeactivate(): void;\n\n /** Subclass hook: additional cleanup on destroy. */\n protected onDestroy(): void {}\n\n /** Play the win/highlight animation for this symbol. Resolves when complete. */\n abstract playWin(): Promise<void>;\n\n /**\n * Play the landing beat for this symbol - the settle a symbol does as its\n * reel stops, before (or instead of) any celebration. Resolves when done.\n *\n * The reel engine never calls this itself: `onReelLanded()` is the\n * engine-driven hook, fired on every landed symbol. This is the explicit\n * counterpart for a caller that lands cells one at a time - `HoldAndWinBoard`\n * calls it on each lock when built with `lockAnimation('landing')`.\n *\n * Default: resolves at once. `SpineReelSymbol` overrides it with the\n * skeleton's `landing` track; a sprite symbol can override it with a tween.\n */\n async playLanding(): Promise<void> {}\n\n /** Immediately stop any running animation and return to idle. */\n abstract stopAnimation(): void;\n\n /** Resize the symbol's visual to fit the given dimensions. */\n abstract resize(width: number, height: number): void;\n\n /**\n * Play the cascade-destruction animation for this symbol. Called by\n * consumers (typically via `reelSet.destroySymbols(...)`) to disintegrate\n * a winning cell before the next cascade refill drops fresh symbols in.\n *\n * Override in subclasses for art-appropriate destruction, e.g. a Spine\n * symbol can play its `disintegration` track here, or a sprite symbol can\n * swap to a shatter atlas. The promise must resolve when the symbol is no\n * longer visible.\n *\n * Default: a snappy \"poof\" centered on the symbol's bounds regardless of\n * the view's anchor. Tiny anticipation pop (~60 ms) then a fast implode to\n * `scale: 0` + `alpha: 0` (~140 ms), ~200 ms total, no rotation. Reads\n * cleanly under win-cluster pacing without competing with the win\n * presenter. The view is left at `alpha: 0` (destroyed); position / pivot\n * are restored so pool reuse via `_replaceSymbol`'s same-id fast path\n * doesn't inherit a stale pivot offset.\n *\n * `opts.delay`. seconds to wait before the animation starts. Use to\n * stagger a cluster of winners (e.g. `i * 0.015`).\n * `opts.signal`. abort signal. If aborted (now or mid-animation), the\n * tween is killed and the view is snapped to its destroyed pose\n * (`alpha: 0`, transform restored). The promise resolves normally. abort\n * means \"skip to the end,\" not \"fail\". Subclasses that override this\n * method MUST honor the signal or document why they can't (e.g. a Spine\n * `disintegration` track is uninterruptible).\n */\n async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n const view = this.view;\n // Capture original transform so pool reuse sees a clean state.\n const originalPivotX = view.pivot.x;\n const originalPivotY = view.pivot.y;\n const originalX = view.x;\n const originalY = view.y;\n\n // Pivot to bounds-center so the scale collapses around the visual\n // centre instead of the view's (0,0) corner. and compensate position\n // so the symbol doesn't visibly jump when the pivot moves.\n const bounds = view.getLocalBounds();\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n // Moving the pivot moves the view by `delta * scale`, not by `delta`: a\n // container renders a local point at `position + (point - pivot) * scale`.\n // Scale is 1 on a plain reel, which is why dropping it went unnoticed, but\n // a curved reel scales every cell and the symbol would jump on destroy.\n view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX) * view.scale.x;\n view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n const delay = opts?.delay ?? 0;\n const signal = opts?.signal;\n\n const snapDestroyed = (): void => {\n view.alpha = 0;\n view.scale.set(0, 0);\n };\n\n // Pre-abort: skip the tween entirely and snap to the destroyed pose.\n if (signal?.aborted) {\n snapDestroyed();\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n view.alpha = 0;\n return;\n }\n\n await new Promise<void>((resolve) => {\n const tl = this.gsap\n .timeline({ onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, delay })\n // Brief anticipation pop. small upscale, ~60 ms, with overshoot\n // so the implode reads as a release. No rotation.\n .to(view.scale, { x: 1.1, y: 1.1, duration: 0.06, ease: 'back.out(2.5)' })\n // Snap implode. scale -> 0 + alpha -> 0 together, snappy ease-in\n // so the symbol collapses into the cell centre and is gone.\n .to(view.scale, { x: 0, y: 0, duration: 0.14, ease: 'power3.in' }, '<+=0.04')\n .to(view, { alpha: 0, duration: 0.14, ease: 'power3.in' }, '<');\n\n const onAbort = (): void => {\n tl.kill();\n snapDestroyed();\n resolve();\n };\n if (signal) signal.addEventListener('abort', onAbort, { once: true });\n });\n\n // Restore transform. alpha stays 0 (the symbol IS destroyed). Scale\n // restored to 1 so pool reuse via `_replaceSymbol`'s same-id fast path\n // doesn't inherit a stale 0× scale; _replaceSymbol also resets scale\n // explicitly but a defensive restore here makes the destroyed cell\n // observably \"ready to be re-skinned\" between calls.\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n view.scale.set(1, 1);\n }\n\n /**\n * Animate this symbol AWAY, without destroying it.\n *\n * The counterpart to {@link playIn}, and the seam a mystery reveal needs:\n * the cells on screen dissolve, their identities are swapped underneath, and\n * the new ones arrive. `playDestroy` is the cascade's version of the same\n * beat and is deliberately separate - it is tuned as a \"this cell was a\n * winner and is being consumed\" poof, and a reveal is not that.\n *\n * Leaves the view hidden (`alpha: 0`) with its transform restored, so the\n * caller may swap the identity and call `playIn` next.\n *\n * Override for art-appropriate exits - a Spine symbol plays its own `out`\n * track here. Honour `opts.signal`: abort means \"snap to the end\", not\n * \"fail\", so the promise still resolves.\n *\n * Default: a ~180 ms shrink-and-fade with no overshoot, centred on the\n * symbol's bounds rather than the view origin.\n */\n async playOut(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n await this._tweenPresence('out', opts);\n }\n\n /**\n * Animate this symbol IN, from nothing to its resting pose.\n *\n * Owns its own start pose. A symbol that has just been re-activated is fully\n * visible (`activate()` resets alpha and scale), so an entrance that assumed\n * it started hidden would pop before it animated. This sets the hidden pose\n * first, then plays.\n *\n * Override for art-appropriate entrances - a Spine symbol plays its own `in`\n * track here. Honour `opts.signal`: abort snaps to the RESTING pose (alpha 1,\n * scale 1), because arriving is what the caller asked for.\n *\n * Default: a ~200 ms fade and scale-up with a small overshoot.\n */\n async playIn(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n await this._tweenPresence('in', opts);\n }\n\n /**\n * Shared body of {@link playIn} / {@link playOut}.\n *\n * One function because the two are the same tween read in opposite\n * directions, and because the fiddly part - pivoting to the visual centre so\n * the scale does not collapse toward the view's (0, 0) corner, and\n * compensating the position for the pivot move - is identical and easy to get\n * subtly wrong twice. Same correction as `playDestroy`: a container renders a\n * local point at `position + (point - pivot) * scale`, so a curved reel (which\n * scales every cell) would make the symbol jump without the `* scale` term.\n */\n private async _tweenPresence(\n direction: 'in' | 'out',\n opts?: { delay?: number; signal?: AbortSignal },\n ): Promise<void> {\n const view = this.view;\n const originalPivotX = view.pivot.x;\n const originalPivotY = view.pivot.y;\n const originalX = view.x;\n const originalY = view.y;\n\n const bounds = view.getLocalBounds();\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX) * view.scale.x;\n view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n const restorePivot = (): void => {\n view.pivot.set(originalPivotX, originalPivotY);\n view.x = originalX;\n view.y = originalY;\n };\n const snapEnd = (): void => {\n if (direction === 'out') {\n view.alpha = 0;\n view.scale.set(1, 1);\n } else {\n view.visible = true;\n view.alpha = 1;\n view.scale.set(1, 1);\n }\n };\n\n const delay = opts?.delay ?? 0;\n const signal = opts?.signal;\n\n if (direction === 'in') {\n // Own the start pose: `activate()` leaves a freshly swapped symbol fully\n // visible, so without this the new art is on screen for a frame before\n // the entrance begins.\n view.visible = true;\n view.alpha = 0;\n view.scale.set(0.72, 0.72);\n }\n\n if (signal?.aborted) {\n snapEnd();\n restorePivot();\n return;\n }\n\n await new Promise<void>((resolve) => {\n const onAbort = (): void => {\n tl.kill();\n snapEnd();\n resolve();\n };\n const tl = this.gsap.timeline({\n delay,\n onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n },\n });\n if (direction === 'out') {\n tl.to(view.scale, { x: 0.72, y: 0.72, duration: 0.18, ease: 'power2.in' }).to(\n view,\n { alpha: 0, duration: 0.18, ease: 'power2.in' },\n '<',\n );\n } else {\n tl.to(view.scale, { x: 1, y: 1, duration: 0.2, ease: 'back.out(1.7)' }).to(\n view,\n { alpha: 1, duration: 0.16, ease: 'power2.out' },\n '<',\n );\n }\n if (signal) signal.addEventListener('abort', onAbort, { once: true });\n });\n\n // Scale back to 1 either way: a hidden symbol still has to be a legal\n // resting pose, or pool reuse inherits the shrunken scale.\n view.scale.set(1, 1);\n restorePivot();\n }\n\n /**\n * The owning reel is curved: render into this projected quad instead of the\n * flat cell rectangle. `null` means the reel is flat again.\n *\n * The quad's corners are SCREEN-space and local to this view's own origin,\n * clockwise from top-left, and `width` / `height` give the flat cell box the\n * quad replaces. Called on every frame of motion and on every placement, so\n * it must be cheap and idempotent.\n *\n * **The default is an approximation.** A `Container` transform is affine, so\n * it cannot express a trapezoid; the base class fits the quad as closely as\n * an affine transform can - a UNIFORM scale about the quad's centre. Uniform\n * on purpose: symbol art is usually smaller than its cell and has a shape a\n * player recognises, and squashing one axis turns a `7` into a squashed `7`\n * rather than a `7` seen at an angle.\n *\n * Override this to render the real perspective. {@link SpriteSymbol} and\n * {@link AnimatedSpriteSymbol} do, by drawing their texture through a\n * `PerspectiveMesh`, which costs no extra render pass because the content is\n * already a texture. A symbol whose content is an arbitrary subtree (Spine, a\n * composite of sprites and text) cannot do that without rendering itself to a\n * texture every frame, so it keeps the affine fit.\n *\n * Whatever you do here, do NOT move `view.position`: the reel reads that\n * coordinate back to work out which slot this symbol is in.\n */\n /**\n * The part of its cell this symbol's art actually covers, or `null` (the\n * default) for \"all of it\".\n *\n * Slot art is usually smaller than its cell - a trimmed atlas frame is a\n * shape floating in a much bigger transparent box - and the reel needs to\n * know that to project the rectangle the art is really in. Overriding this\n * is what stops a small symbol being inflated to the cell's edges and given\n * the cell's keystone instead of its own, milder one.\n *\n * Read once per projection, so it may change with the symbol's identity.\n */\n get cellInset(): ReelCellInset | null {\n return null;\n }\n\n applyCellQuad(quad: ReelCellQuad | null): void {\n const view = this.view;\n if (quad === null) {\n view.scale.set(1, 1);\n view.pivot.set(0, 0);\n return;\n }\n // Measure the trapezoid: the mean of its two parallel edges, and the\n // distance between their midpoints.\n const nearWidth = Math.hypot(quad.x1 - quad.x0, quad.y1 - quad.y0);\n const farWidth = Math.hypot(quad.x2 - quad.x3, quad.y2 - quad.y3);\n const across = (nearWidth + farWidth) / 2;\n const along = Math.hypot(\n (quad.x3 + quad.x2) / 2 - (quad.x0 + quad.x1) / 2,\n (quad.y3 + quad.y2) / 2 - (quad.y0 + quad.y1) / 2,\n );\n // CONTAIN, not cover. A quad in the middle of the window is TALLER than\n // the flat cell (that is the drum magnifying what faces you), so a scale\n // picked to fill it would also make the symbol wider than its column and\n // overlap its neighbours - very visible on art that fills its cell\n // edge-to-edge. Taking the smaller ratio keeps every symbol inside its own\n // projected footprint at the cost of a little slack on one axis.\n const scale =\n quad.width > 0 && quad.height > 0\n ? Math.min(across / quad.width, along / quad.height)\n : 1;\n\n const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n view.scale.set(scale, scale);\n // A container renders a local point at `position + (point - pivot) * scale`\n // and `position` must not move, so putting the flat box's centre on the\n // quad's centre has to be paid for entirely out of the pivot.\n view.pivot.set(\n quad.x + quad.width / 2 - cx / scale,\n quad.y + quad.height / 2 - cy / scale,\n );\n }\n\n /**\n * Lifecycle hook: the owning reel is spinning.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur,\n * StaticSpinSymbol) to swap to a spin presentation automatically.\n *\n * Fired on every strip symbol (visible AND buffer cells) when the reel\n * enters the spin phase, and again with `joinedMidSpin: true` on each\n * symbol freshly installed while the reel is already spinning (pool\n * recycling wipes symbol state, so a wrapped-in symbol can't know the\n * reel is moving without this). Implementations MUST be idempotent.\n * the same instance can be notified more than once per spin.\n *\n * @param joinedMidSpin true when this symbol was installed into a reel\n * already at speed (skip start-of-spin transitions like blur ramps).\n */\n onReelSpinStart(joinedMidSpin?: boolean): void {}\n\n /**\n * Lifecycle hook: the owning reel is about to stop (just before bounce).\n * Default: no-op.\n */\n onReelSpinEnd(): void {}\n\n /**\n * Lifecycle hook: the owning reel entered its anticipation (tease) phase.\n * it is still spinning, but slowed enough that the strip is readable.\n * Spin presentations that obscure symbols (blur textures, smear\n * animations) should relax so the player can follow the tease. Also\n * fired on symbols installed while the reel is anticipating.\n * Implementations MUST be idempotent. Default: no-op.\n */\n onReelAnticipationStart(): void {}\n\n /**\n * Lifecycle hook: the owning reel has landed on its final symbols.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayLanding) to fire\n * a landing animation concurrently with the bounce.\n *\n * `ctx` says which reel and cell this symbol landed in, so an override can\n * play a different beat - or none - on a particular reel. The engine always\n * supplies it; it is optional only so an override written as\n * `onReelLanded()` keeps compiling. Fired before the reel's `landing` event\n * and the set's `spin:reelLanding`, by contract.\n */\n onReelLanded(ctx?: ReelLandingContext): void {}\n}\n"],"mappings":"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"}
|