pixi-reels 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/README.md +2 -1
- package/dist/{SpineSymbol-CfVrc5Pk.js → SpineSymbol-9NlrGsFv.js} +57 -10
- package/dist/SpineSymbol-9NlrGsFv.js.map +1 -0
- package/dist/SpineSymbol-ojWlEPwt.cjs +2 -0
- package/dist/SpineSymbol-ojWlEPwt.cjs.map +1 -0
- package/dist/cascade/TumbleConfig.d.ts +94 -0
- package/dist/cascade/TumbleConfig.d.ts.map +1 -0
- package/dist/cascade/tumbleAlgorithm.d.ts +61 -0
- package/dist/cascade/tumbleAlgorithm.d.ts.map +1 -0
- package/dist/config/types.d.ts +1 -1
- package/dist/core/Reel.d.ts +10 -1
- package/dist/core/Reel.d.ts.map +1 -1
- package/dist/core/ReelSet.d.ts +433 -12
- package/dist/core/ReelSet.d.ts.map +1 -1
- package/dist/core/ReelSetBuilder.d.ts +44 -12
- package/dist/core/ReelSetBuilder.d.ts.map +1 -1
- package/dist/events/ReelEvents.d.ts +258 -0
- package/dist/events/ReelEvents.d.ts.map +1 -1
- package/dist/frame/ColumnTarget.d.ts +77 -0
- package/dist/frame/ColumnTarget.d.ts.map +1 -0
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +799 -392
- package/dist/index.js.map +1 -1
- package/dist/spin/SpinController.d.ts +203 -1
- package/dist/spin/SpinController.d.ts.map +1 -1
- package/dist/spin/phases/CascadeDropInPhase.d.ts +61 -0
- package/dist/spin/phases/CascadeDropInPhase.d.ts.map +1 -0
- package/dist/spin/phases/CascadeFallPhase.d.ts +53 -0
- package/dist/spin/phases/CascadeFallPhase.d.ts.map +1 -0
- package/dist/spin/phases/CascadePlacePhase.d.ts +37 -0
- package/dist/spin/phases/CascadePlacePhase.d.ts.map +1 -0
- package/dist/spin/phases/PhaseFactory.d.ts +1 -1
- package/dist/spine/SpineReelSymbol.d.ts +17 -0
- package/dist/spine/SpineReelSymbol.d.ts.map +1 -1
- package/dist/spine.cjs +1 -1
- package/dist/spine.cjs.map +1 -1
- package/dist/spine.js +39 -1
- package/dist/spine.js.map +1 -1
- package/dist/symbols/ReelSymbol.d.ts +34 -0
- package/dist/symbols/ReelSymbol.d.ts.map +1 -1
- package/dist/testing/testHarness.d.ts +10 -4
- package/dist/testing/testHarness.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/SpineSymbol-CfVrc5Pk.js.map +0 -1
- package/dist/SpineSymbol-JA5PdEbT.cjs +0 -2
- package/dist/SpineSymbol-JA5PdEbT.cjs.map +0 -1
- package/dist/cascade/CascadeAnticipationPhase.d.ts +0 -23
- package/dist/cascade/CascadeAnticipationPhase.d.ts.map +0 -1
- package/dist/cascade/DropRecipes.d.ts +0 -40
- package/dist/cascade/DropRecipes.d.ts.map +0 -1
- package/dist/spin/phases/DropStartPhase.d.ts +0 -21
- package/dist/spin/phases/DropStartPhase.d.ts.map +0 -1
- package/dist/spin/phases/DropStopPhase.d.ts +0 -44
- package/dist/spin/phases/DropStopPhase.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,168 @@
|
|
|
1
1
|
# pixi-reels
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: two-stage cascade refill (gravity → hold → drop-in) for tumble slots that want an anticipation beat between survivors landing and new symbols entering.
|
|
8
|
+
|
|
9
|
+
The default refill animates survivors and new symbols together in one beat (the Sweet Bonanza / Sugar Rush feel). A handful of slots split it in two: survivors slide first, a global beat for anticipation visuals (multiplier roll, mascot react, SFX peak), then new symbols enter — often staggered per column. That flavor is now first-class.
|
|
10
|
+
|
|
11
|
+
Opt in via `mode: 'gravity-then-drop'` on `refill()` (or `refillMode: 'gravity-then-drop'` on `runCascade()`):
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
await reelSet.destroySymbols(winners);
|
|
15
|
+
reelSet.setDropOrder("ltr", 110); // per-column wave for stage B
|
|
16
|
+
|
|
17
|
+
await reelSet.refill({
|
|
18
|
+
winners,
|
|
19
|
+
grid: nextGrid,
|
|
20
|
+
mode: "gravity-then-drop",
|
|
21
|
+
gravityHoldMs: 350, // anticipation window
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
New options:
|
|
26
|
+
|
|
27
|
+
- `refill({ mode })` — `'combined'` (default, unchanged) or `'gravity-then-drop'`.
|
|
28
|
+
- `refill({ gravityHoldMs })` — global pause between gravity end and drop-in start. Default `250`.
|
|
29
|
+
- `refill({ onGravityComplete })` — awaitable hook between stages; extends the hold for async work (multiplier count-ups, etc.).
|
|
30
|
+
- `runCascade({ refillMode, gravityHoldMs, onGravityComplete })` — same options forwarded into every refill in the chain. The hook receives `{ chain, winners }`.
|
|
31
|
+
|
|
32
|
+
New events:
|
|
33
|
+
|
|
34
|
+
- `cascade:gravity:start` — `{ reelIndex }`. A reel's gravity stage begins.
|
|
35
|
+
- `cascade:gravity:symbol` — same shape as `cascade:dropIn:symbol`, scoped to survivors.
|
|
36
|
+
- `cascade:gravity:end` — `{ reelIndex }`. A reel's gravity stage settled.
|
|
37
|
+
|
|
38
|
+
These fire only in two-stage mode; combined mode is unchanged. Per-column stagger inside the drop-in stage uses the existing `setDropOrder('ltr', stepMs)` — `step < dropIn.duration` gives an overlapping wave, `step >= dropIn.duration` gives strictly sequential columns. The gravity stage always runs all reels in parallel.
|
|
39
|
+
|
|
40
|
+
See the [Cascade anticipation refill recipe](https://pixi-reels.com/recipes/tumble-anticipation/) for a live example.
|
|
41
|
+
|
|
42
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Cascade DX pass: collapse ~30 lines of slot orchestration to ~3 with a canonical detect → destroy → refill chain, retire the legacy `examples/shared/cascadeLoop.ts` helper, and align every recipe / example / doc onto the new API.
|
|
43
|
+
|
|
44
|
+
**`reelSet.destroySymbols(cells, opts?)`** — the canonical "fade out winners" step. Defers to each symbol's `playDestroy()` so subclasses (Spine, particles) get art-appropriate disintegration without the spin handler caring. Bumps each view's zIndex so destroys aren't clipped, alternates rotation by column for cohesive cluster pops, optional viewport dim. Replaces ~10 lines of duplicated `destroyWinners` helpers in every cascade recipe.
|
|
45
|
+
|
|
46
|
+
**`reelSet.runCascade({ detectWinners, nextGrid, onCascade?, pauseAfterDestroyMs?, maxChain?, destroyOptions?, signal? })`** — the canonical cascade chain orchestration. Loops detect → destroy → pause → refill until `detectWinners` returns `[]`. Caller supplies the game-rules callbacks; the library owns the timing. Both callbacks may be `async`. Pass `signal: AbortSignal` for caller-driven cancellation (the right shape for "player tapped slam between refills," where `reelSet.skip()` is a no-op because the engine is idle). The awaited `RunCascadeResult` (`{ chainLength, totalWinners, finalGrid, wasSkipped }`) is the canonical "the chain is over" signal — no separate event for that, since "round" is a slot-UX term (bet→payout) rather than a reel-engine one and the engine-level "press-spin → all-stopped" is already covered by `spin:start` / `spin:allLanded`.
|
|
47
|
+
|
|
48
|
+
**`cascade:place:end`** payload now includes `isInitial: boolean` and `winnerRows: readonly number[]` so decoration listeners can tell new arrivals from survivors sliding into a hole.
|
|
49
|
+
|
|
50
|
+
Also exports the named option / result types — `DestroySymbolsOptions`, `RunCascadeOptions`, `RunCascadeResult` — so apps can pass typed config objects around or extend them in adapter layers.
|
|
51
|
+
|
|
52
|
+
Non-breaking for the library API. Removed the legacy `examples/shared/cascadeLoop.ts` helper (`runCascade(reelSet, stages, opts)`, `tumbleToGrid`, `diffCells`) since every recipe + example + integration test has been migrated to the new `reelSet.runCascade` / `reelSet.destroySymbols` / `reelSet.refill` surface. Site recipes (`cascade-6x5`, `spin-then-cascade`, `multiways-cascade`, `cascade-winpresenter`, `remove-symbol`) and React recipe components (`RemoveSymbolRecipe`, `CascadeStarterRecipe`) all use the new API; the `cascade-tumble` and `pyramid-cascade` examples were rewritten the same way.
|
|
53
|
+
|
|
54
|
+
New guide `your-first-cascade.mdx` walks a tutorial through the canonical API end-to-end. `cascades.mdx` documents the two-moments mental model, the `pauseAfterDestroyMs` / `destroyOptions` / `signal` knobs on `runCascade`, and the choice between `refill()` and `runCascade()`.
|
|
55
|
+
|
|
56
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: chain- and destroy-scoped cascade lifecycle events so HUDs and audio buses can hook a cascade chain without polling `isSpinning` (which oscillates between refills).
|
|
57
|
+
|
|
58
|
+
New events on `reelSet.events`:
|
|
59
|
+
|
|
60
|
+
- `cascade:chain:start` — `{ chain, winners, currentGrid }`. Fired inside `runCascade(...)` after `detectWinners` returns winners, before `destroySymbols` runs. `chain` is 1-indexed.
|
|
61
|
+
- `cascade:chain:end` — `{ chain, winners, nextGrid }`. Mirror of `chain:start` — fired after the refill drop-in settles, before the loop iterates to the next `detectWinners`.
|
|
62
|
+
- `cascade:destroy:start` / `cascade:destroy:end` — `{ cells }`. Fired around every `destroySymbols(...)` call (both direct and inside `runCascade`). Empty-batch calls do not emit. Use these to cue a shatter SFX, dim a HUD, or capture pre-destroy grids for replay logging — without overriding the cascade loop.
|
|
63
|
+
|
|
64
|
+
Event ordering per `runCascade()` call (per stage with winners):
|
|
65
|
+
|
|
66
|
+
`cascade:chain:start` → `cascade:destroy:start` → (destroy tweens) → `cascade:destroy:end` → `onCascade` callback → pause → refill (`cascade:place:end` + `cascade:dropIn:*` per reel) → `cascade:chain:end`
|
|
67
|
+
|
|
68
|
+
The runCascade chain itself is delimited by the returned `Promise` — `await` the call to know when it's done and read the `RunCascadeResult` summary. There is intentionally no `cascade:round:*` event pair: "round" in slot UX is a bet→payout transaction (your concern, not the engine's), and the engine-level "press-spin → all-stopped" is already covered by `spin:start` / `spin:allLanded`.
|
|
69
|
+
|
|
70
|
+
Every cascade event uses a consistent three-part `cascade:<scope>:<step>` taxonomy.
|
|
71
|
+
|
|
72
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add `gravityHold: Promise<void>` to `refill()` and `runCascade()` so callers can gate the drop-in stage on an already-in-flight animation / SFX / network call without wrapping it in a callback.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// Single refill — pass the promise directly.
|
|
76
|
+
await reelSet.refill({
|
|
77
|
+
winners,
|
|
78
|
+
grid: next,
|
|
79
|
+
mode: "gravity-then-drop",
|
|
80
|
+
gravityHoldMs: 150, // minimum wall-clock floor
|
|
81
|
+
gravityHold: multiplierRoll.done, // wait for the in-flight roll
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`gravityHoldMs` and `gravityHold` race in **parallel** via `Promise.all` — whichever finishes LAST gates the drop-in. Pass both when you want a wall-clock floor under an animation that might finish quickly. `onGravityComplete` (the existing callback hook) still runs AFTER both resolve, so it can read post-hold state.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// Per-cascade — runCascade calls the builder once per stage.
|
|
89
|
+
await reelSet.runCascade({
|
|
90
|
+
detectWinners,
|
|
91
|
+
nextGrid,
|
|
92
|
+
refillMode: "gravity-then-drop",
|
|
93
|
+
gravityHoldMs: 150,
|
|
94
|
+
gravityHold: ({ chain, winners }) => {
|
|
95
|
+
multiplier.bumpTo(chain + 1);
|
|
96
|
+
return multiplier.done; // each cascade waits for its own roll
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Site recipes: SPIN/SKIP button is now bigger (56x56 vs 40x40), vertically centered on the right edge of the canvas, and uses the `SkipForward` icon (lucide-react) instead of `Square` when active. Larger touch target, more obvious as the primary action.
|
|
102
|
+
|
|
103
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Round-aware slam-stop: single-press `skip()` with side effects, new `slamStop()`, new `skipStage`.
|
|
104
|
+
|
|
105
|
+
`ReelSet.skip()` is now round-aware. A "round" is one `spin()` plus all its `refill()`s, until the next `spin()`. The first press of `skip()` in a round slams the current drop AND applies a round-scoped side effect:
|
|
106
|
+
|
|
107
|
+
- **Standard mode**: boosts the active speed profile to the fastest registered one (emits `skip:boosted`). The speed takes effect on the NEXT spin (mid-spin speed switching is not supported by phases). Boost persists across `refill()` calls and is restored on the next `spin()` — unless the app changed speed manually between rounds, in which case the manual choice is preserved.
|
|
108
|
+
- **Cascade/tumble mode**: flags the round so every subsequent `refill()` auto-slams with no animation. One press ends a multi-drop cascade.
|
|
109
|
+
|
|
110
|
+
Subsequent `skip()` presses in the same round each slam the current drop. The universal `if (isSpinning) reelSet.skip()` button pattern across recipes now always lands the spin on a single press, while still benefiting from the boost / auto-slam side effect.
|
|
111
|
+
|
|
112
|
+
Breaking:
|
|
113
|
+
|
|
114
|
+
- `skip()` no longer needs two presses to slam — single press lands the drop. Callers that already relied on `skip()` slamming work as before. Callers expecting a _non-slamming_ "boost only" press should use `reelSet.setSpeed('superTurbo')` directly.
|
|
115
|
+
- `skip()` THROWS if called before `setResult()` arrives (no result to land on — pre-result slam would land on random spin-buffer state). Use `requestSkip()` for the deferred-slam pattern, or wrap `skip()` in `try { ... } catch {}` and route to `requestSkip()` in the catch. Refill paths take a result at entry, so this guard only fires in the initial-spin pre-`setResult` window.
|
|
116
|
+
- `requestSkip()` bypasses staging entirely and slams when `setResult()` arrives.
|
|
117
|
+
- The test harness `spinAndLand()` was migrated to `slamStop()` to keep its semantics explicit.
|
|
118
|
+
|
|
119
|
+
Added:
|
|
120
|
+
|
|
121
|
+
- `ReelSet.slamStop()` — always slams, no side effects.
|
|
122
|
+
- `ReelSet.skipStage` — `0 | 1 | 2` getter; `0` until the first press, `2` after. (`1` is reserved for forward compat.)
|
|
123
|
+
- `skip:boosted` event — `{ previous, current }: SpeedProfile`. Fires only on standard-mode boost; cascade auto-slam doesn't emit it.
|
|
124
|
+
- `ReelSymbol.playDestroy(opts?)` — `opts.direction: 1 | -1` for coherent rotation (e.g. `w.reel % 2 === 0 ? 1 : -1`), `opts.delay: number` (seconds) for per-winner stagger, and `opts.signal: AbortSignal` so a mid-destroy abort can snap to the destroyed pose without waiting for the full ~300 ms tween. Default direction stays random for back-compat.
|
|
125
|
+
|
|
126
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Replace `.cascade()` with `.tumble()` and split cascade-drop into three independently overridable phases.
|
|
127
|
+
|
|
128
|
+
Breaking changes: `.cascade(DropRecipes...)` is removed. `DropRecipes`, `DropStartPhase`, `DropStopPhase`, `CascadeAnticipationPhase`, and their `*Config` types no longer export from `pixi-reels`. Use `.tumble({ fall, dropIn })` on the builder and override individual phases via `.phases(f => f.register('cascade:fall'|'cascade:place'|'cascade:dropIn', MyPhase))`.
|
|
129
|
+
|
|
130
|
+
New: `reelSet.refill({ winners, grid })` for Moment B cascade refills. Gravity-correct geometry — untouched survivors stay, survivors above a hole slide down, new symbols enter from above into the top `winners.length` rows. Per-symbol `cascade:fall:symbol` / `cascade:dropIn:symbol` events fire right before each tween so listeners can run parallel tweens on any view property in sync with the library's motion. Per-reel boundary events: `cascade:fall:start` / `cascade:fall:end` / `cascade:place:end` / `cascade:dropIn:start` / `cascade:dropIn:end`.
|
|
131
|
+
|
|
132
|
+
See `docs/recipes/tumble-cascade.md` for the full recipe (drop-on-click, server wait with spinner, cascading multiplier).
|
|
133
|
+
|
|
134
|
+
### Patch Changes
|
|
135
|
+
|
|
136
|
+
- [#120](https://github.com/schmooky/pixi-reels/pull/120) [`579ed0c`](https://github.com/schmooky/pixi-reels/commit/579ed0c2d16ba36b2672a55c251b9e029db4f088) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Fix five audit-discovered defects in the tumble-cascade pipeline:
|
|
137
|
+
|
|
138
|
+
- `CascadeFallPhase` / `CascadeDropInPhase` now emit their `:end` events on skip. Previously a slam mid-fall (or mid-drop, mid-gravity) killed the timeline without firing the paired `cascade:fall:end` / `cascade:dropIn:end` / `cascade:gravity:end`, so any HUD or audio bus pairing `:start` / `:end` to track in-flight cascade work drifted out of balance on every slam. The pre-fall delay window (where `:start` has not yet fired) still skips silently, so no unpaired `:end` is emitted.
|
|
139
|
+
|
|
140
|
+
- `runCascade({ gravityHold })` now invokes the per-cascade builder at the **gravity-end boundary** as documented, not at refill-start. Side effects in the builder (e.g. `multiplier.bumpTo(chain + 1); return multiplier.done`) now line up with the gravity-end beat the player sees. To support this, `refill({ gravityHold })` accepts a factory `() => Promise<void>` in addition to a bare `Promise<void>` — pass a factory when the side effect of starting the promise should fire at gravity-end; pass a bare promise when you already hold an in-flight handle.
|
|
141
|
+
|
|
142
|
+
- `runCascade({ pauseAfterDestroyMs })` wait is now cancellable via `signal`. Previously an abort during the pause ran the setTimeout to completion before the loop exited — up to `pauseAfterDestroyMs` of dead air between slam intent and exit. Now the wait races against `signal.aborted` and unblocks within a microtask.
|
|
143
|
+
|
|
144
|
+
- A new `cascade:gravity:error` event surfaces user-supplied `gravityHold` / `onGravityComplete` rejections (or throws). The engine still slams to recover so the refill promise settles, but the original rejection reason is no longer silently swallowed — listen on the event to forward the error to your own logger / alarm. The console.error log was also tightened to identify the likely culprit.
|
|
145
|
+
|
|
146
|
+
- `movePin` `onFlightCreated` / `onFlightCompleted` hook throws now log via `console.error` instead of being silently swallowed. The animation still continues (a throwing hook MUST NOT leak a flight symbol or leave the pin map out of sync) but the bug is no longer invisible.
|
|
147
|
+
|
|
148
|
+
Also clarifies the `skip()` documentation: `skip()` THROWS before `setResult()` arrives. The docstring on `requestSkip()` and `skipStage` now notes that queued-pre-`setResult` requests do not advance `skipStage` until the slam fires.
|
|
149
|
+
|
|
150
|
+
## 0.5.0
|
|
151
|
+
|
|
152
|
+
### Minor Changes
|
|
153
|
+
|
|
154
|
+
- [#111](https://github.com/schmooky/pixi-reels/pull/111) [`dc2a526`](https://github.com/schmooky/pixi-reels/commit/dc2a526cf13c8670d10680f9104b93675332468f) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: cascade + multiways combination. `ReelSetBuilder.multiways(...)` can now be paired with `.cascade(...)` or `spinningMode(new CascadeMode())` — the build-time throw added in ADR 012 is lifted. `AdjustPhase` runs between `SpinPhase` and `DropStopPhase` so the new shape commits before the drop-in fills it. Shape changes apply per-spin only; mid-cascade-chain reshape is unsupported (see ADR 015). Closes [#74](https://github.com/schmooky/pixi-reels/issues/74).
|
|
155
|
+
|
|
156
|
+
- [#116](https://github.com/schmooky/pixi-reels/pull/116) [`7afe3a9`](https://github.com/schmooky/pixi-reels/commit/7afe3a9a6edd70aaab4c985fb0167050e93fbd49) Thanks [@igaming-bulochka](https://github.com/igaming-bulochka)! - Add: `ColumnTarget` — explicit `{ visible, bufferAbove?, bufferBelow? }` input shape. Accepted by both `ReelSet.setResult` and `ReelSetBuilder.initialFrame` alongside the legacy `string[][]` form. Survives `structuredClone`, JSON, and `postMessage` (the legacy negative-index form does not).
|
|
157
|
+
|
|
158
|
+
Fix: `setResult` (legacy `string[][]` form) now honours `frame[col][-1]…[-bufferAbove]` end-to-end. Previously the negative-index slots were dropped inside `_applyPinsToGrid` (when pins were active) and `_coordinateBigSymbols` (always) by plain spread clones, so the convention only worked through `initialFrame`. The clones now use a property-preserving helper.
|
|
159
|
+
|
|
160
|
+
Fix: `Reel.placeSymbols` (skip / turbo land path) now reads the negative-index slot for the buffer-above cell instead of always random-filling it. Buffer-below targeting via `symbolIds[visibleRows]` is unchanged.
|
|
161
|
+
|
|
162
|
+
### Patch Changes
|
|
163
|
+
|
|
164
|
+
- [#115](https://github.com/schmooky/pixi-reels/pull/115) [`1f30d8e`](https://github.com/schmooky/pixi-reels/commit/1f30d8e1b5d997872c85400122ee2613d35e0933) Thanks [@MaksimKiselev](https://github.com/MaksimKiselev)! - Fix: negative indices in `initialFrame` now correctly populate buffer-above slots. Setting `frame[col][-1]` (or `[-2]` for deeper buffers) places the symbol in the corresponding buffer-above cell instead of being silently ignored.
|
|
165
|
+
|
|
3
166
|
## 0.4.0
|
|
4
167
|
|
|
5
168
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ You'll find:
|
|
|
73
73
|
- **Recipes** — small how-tos for common mechanics (walking wilds, sticky wilds, cascade, mystery reveal, hold & win, ...). Each ships with a live mini-demo.
|
|
74
74
|
- **Demos** — full mechanic sandboxes with cheat panels. One click forces a scatter, a near-miss, a guaranteed jackpot.
|
|
75
75
|
- **Sandbox** — in-browser TypeScript playground; edit the file, hit Run, reels rebuild.
|
|
76
|
+
- **Studio** — bring-your-own-assets workbench. Drop in sprites or Spine bundles, wire them to symbol ids, edit builder code, share the result via a password-protected link (see [`apps/share-api`](apps/share-api/) for the relay).
|
|
76
77
|
- **Wiki** — API reference.
|
|
77
78
|
|
|
78
79
|
## Examples
|
|
@@ -90,7 +91,7 @@ Runnable apps in [`examples/`](examples/):
|
|
|
90
91
|
|
|
91
92
|
```ts
|
|
92
93
|
reelSet.spin(): Promise<SpinResult> // Start spinning
|
|
93
|
-
reelSet.setResult(symbols: string[][])
|
|
94
|
+
reelSet.setResult(symbols: string[][] | ColumnTarget[]) // Pass the target grid (triggers the stop). Use frame[col][-1] (or { bufferAbove: [...] }) to prefill cells above the visible window — see /recipes/buffer-indexing-cheatsheet/.
|
|
94
95
|
reelSet.setAnticipation([3, 4]) // Slow reels 3+4 before their landing
|
|
95
96
|
reelSet.setStopDelays([0, 140, 280, 600, 1100]) // Override per-reel stop stagger
|
|
96
97
|
reelSet.skip() // Slam-stop
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Container as e } from "pixi.js";
|
|
2
|
+
import { gsap as t } from "gsap";
|
|
3
|
+
//#region src/utils/gsapRef.ts
|
|
4
|
+
var n = t;
|
|
5
|
+
function r(e) {
|
|
6
|
+
n = e;
|
|
7
|
+
}
|
|
8
|
+
function i() {
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
2
12
|
//#region src/symbols/ReelSymbol.ts
|
|
3
|
-
var
|
|
13
|
+
var a = class {
|
|
4
14
|
view;
|
|
5
15
|
_symbolId = "";
|
|
6
16
|
_isDestroyed = !1;
|
|
@@ -26,17 +36,54 @@ var t = class {
|
|
|
26
36
|
this._isDestroyed ||= (this.stopAnimation(), this.onDeactivate(), this.onDestroy(), this.view.destroyed || this.view.destroy({ children: !0 }), !0);
|
|
27
37
|
}
|
|
28
38
|
onDestroy() {}
|
|
39
|
+
async playDestroy(e) {
|
|
40
|
+
let t = this.view, n = t.pivot.x, r = t.pivot.y, a = t.x, o = t.y, s = t.getLocalBounds(), c = s.x + s.width / 2, l = s.y + s.height / 2;
|
|
41
|
+
t.pivot.set(c, l), t.x = a + (c - n), t.y = o + (l - r);
|
|
42
|
+
let u = e?.direction ?? (Math.random() < .5 ? 1 : -1), d = e?.delay ?? 0, f = e?.signal, p = () => {
|
|
43
|
+
t.alpha = 0, t.scale.set(0, 0);
|
|
44
|
+
};
|
|
45
|
+
if (f?.aborted) {
|
|
46
|
+
p(), t.pivot.set(n, r), t.x = a, t.y = o, t.rotation = 0, t.scale.set(1, 1), t.alpha = 0;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await new Promise((e) => {
|
|
50
|
+
let n = i().timeline({
|
|
51
|
+
onComplete: () => {
|
|
52
|
+
f && f.removeEventListener("abort", r), e();
|
|
53
|
+
},
|
|
54
|
+
delay: d
|
|
55
|
+
}).to(t.scale, {
|
|
56
|
+
x: 1.25,
|
|
57
|
+
y: 1.25,
|
|
58
|
+
duration: .08,
|
|
59
|
+
ease: "back.out(2.5)"
|
|
60
|
+
}).to(t, {
|
|
61
|
+
rotation: u * .8,
|
|
62
|
+
alpha: 0,
|
|
63
|
+
duration: .24,
|
|
64
|
+
ease: "power2.in"
|
|
65
|
+
}, "<+=0.05").to(t.scale, {
|
|
66
|
+
x: 0,
|
|
67
|
+
y: 0,
|
|
68
|
+
duration: .24,
|
|
69
|
+
ease: "power2.in"
|
|
70
|
+
}, "<"), r = () => {
|
|
71
|
+
n.kill(), p(), e();
|
|
72
|
+
};
|
|
73
|
+
f && f.addEventListener("abort", r, { once: !0 });
|
|
74
|
+
}), t.pivot.set(n, r), t.x = a, t.y = o, t.rotation = 0, t.scale.set(1, 1);
|
|
75
|
+
}
|
|
29
76
|
onReelSpinStart() {}
|
|
30
77
|
onReelSpinEnd() {}
|
|
31
78
|
onReelLanded() {}
|
|
32
|
-
},
|
|
33
|
-
async function
|
|
79
|
+
}, o = null;
|
|
80
|
+
async function s() {
|
|
34
81
|
try {
|
|
35
|
-
|
|
82
|
+
o = (await import("@esotericsoftware/spine-pixi-v8")).Spine;
|
|
36
83
|
} catch {}
|
|
37
84
|
}
|
|
38
|
-
|
|
39
|
-
var
|
|
85
|
+
s();
|
|
86
|
+
var c = class extends a {
|
|
40
87
|
_spine = null;
|
|
41
88
|
_skeletonDataMap;
|
|
42
89
|
_idleAnimation;
|
|
@@ -45,12 +92,12 @@ var i = class extends t {
|
|
|
45
92
|
_winResolve = null;
|
|
46
93
|
_currentSkeletonKey = "";
|
|
47
94
|
constructor(e) {
|
|
48
|
-
if (super(), !
|
|
95
|
+
if (super(), !o) throw Error("SpineSymbol requires @esotericsoftware/spine-pixi-v8 to be installed. Install it with: npm install @esotericsoftware/spine-pixi-v8");
|
|
49
96
|
this._skeletonDataMap = e.skeletonDataMap, this._idleAnimation = e.idleAnimation ?? "idle", this._winAnimation = e.winAnimation ?? "win", this._defaultSkin = e.defaultSkin ?? "default";
|
|
50
97
|
}
|
|
51
98
|
onActivate(e) {
|
|
52
99
|
let t = this._skeletonDataMap[e];
|
|
53
|
-
t && (this._currentSkeletonKey !== e && (this._spine && (this.view.removeChild(this._spine), this._spine.destroy()), this._spine = new
|
|
100
|
+
t && (this._currentSkeletonKey !== e && (this._spine && (this.view.removeChild(this._spine), this._spine.destroy()), this._spine = new o({ skeletonData: t }), this.view.addChild(this._spine), this._currentSkeletonKey = e), this._spine.skeleton.data.findSkin(this._defaultSkin) && (this._spine.skeleton.setSkinByName(this._defaultSkin), this._spine.skeleton.setSlotsToSetupPose()), this._spine.skeleton.data.findAnimation(this._idleAnimation) && this._spine.state.setAnimation(0, this._idleAnimation, !0));
|
|
54
101
|
}
|
|
55
102
|
onDeactivate() {
|
|
56
103
|
if (this._spine && (this._spine.state.clearListeners(), this._spine.state.clearTracks()), this._winResolve) {
|
|
@@ -83,6 +130,6 @@ var i = class extends t {
|
|
|
83
130
|
}
|
|
84
131
|
};
|
|
85
132
|
//#endregion
|
|
86
|
-
export {
|
|
133
|
+
export { r as i, a as n, i as r, c as t };
|
|
87
134
|
|
|
88
|
-
//# sourceMappingURL=SpineSymbol-
|
|
135
|
+
//# sourceMappingURL=SpineSymbol-9NlrGsFv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpineSymbol-9NlrGsFv.js","names":[],"sources":["../src/utils/gsapRef.ts","../src/symbols/ReelSymbol.ts","../src/symbols/SpineSymbol.ts"],"sourcesContent":["import { gsap as defaultGsap } from 'gsap';\n\n/**\n * The gsap instance every phase, motion tween, and cascade animation in\n * the engine reads from.\n *\n * **Why this indirection exists:** under tools that resolve modules through\n * symlinked workspaces (vite + a locally-linked pixi-reels, pnpm dev\n * 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\n * can resolve to *different module instances* — each with its own root\n * timeline. The consumer drives one, the lib's tweens live on the other,\n * and reels stall at progress 0.\n *\n * The fix: every internal animation site reads gsap through `getGsap()`\n * instead of importing the `'gsap'` module directly. The builder method\n * `ReelSetBuilder.gsap(myGsap)` rebinds the singleton so the consumer's\n * gsap is the one the engine uses.\n *\n * Defaults to the gsap import resolved at lib-load time, so consumers\n * who don't hit the dual-instance trap don't have to do anything.\n */\nlet currentGsap: typeof defaultGsap = defaultGsap;\n\n/**\n * Replace the gsap instance the engine drives. Call this BEFORE\n * `ReelSetBuilder.build()` (the builder does this for you when you call\n * `.gsap(instance)`).\n *\n * Process-global: there's only one bound instance at a time. If you\n * build several ReelSets with different gsap instances, the last\n * `setGsap` call wins.\n */\nexport function setGsap(g: typeof defaultGsap): void {\n currentGsap = g;\n}\n\n/** The currently-bound gsap instance. */\nexport function getGsap(): typeof defaultGsap {\n return currentGsap;\n}\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { getGsap } from '../utils/gsapRef.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 constructor() {\n this.view = new Container();\n }\n\n get symbolId(): string {\n return this._symbolId;\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 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.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 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 this.stopAnimation();\n this.onDeactivate();\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 // Override if needed\n }\n\n /** Play the win/highlight animation for this symbol. Resolves when complete. */\n abstract playWin(): 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 * Default implementation: brief scale-up \"charge\" then implode (scale 0\n * + spin + fade), squishing around the symbol's bounding-box CENTER\n * regardless of the view's anchor. Total ~320 ms. The view is left at\n * `alpha: 0` (destroyed); position / pivot are restored so pool reuse\n * via `_replaceSymbol`'s same-id fast path doesn't inherit a stale\n * pivot offset.\n *\n * Override in subclasses for art-appropriate destruction — e.g. a\n * Spine symbol can play its `disintegration` track here, or a sprite\n * symbol can swap to a shatter atlas. The promise must resolve when\n * the symbol is no longer visible.\n *\n * `opts.direction` — rotation direction (`1` or `-1`). Default: random.\n * For coherent clusters, callers should pass `w.reel % 2 === 0 ? 1 : -1`\n * (alternate by column) instead of relying on random.\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?: { direction?: 1 | -1; 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 scale + rotation squish 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 view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX);\n view.y = originalY + (cy - originalPivotY);\n\n const dir = opts?.direction ?? (Math.random() < 0.5 ? 1 : -1);\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.rotation = 0;\n view.scale.set(1, 1);\n view.alpha = 0;\n return;\n }\n\n await new Promise<void>((resolve) => {\n const tl = getGsap()\n .timeline({ onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, delay })\n // Brief scale-up \"charge\" so the impending destruction has a beat\n // of anticipation before the implode.\n .to(view.scale, { x: 1.25, y: 1.25, duration: 0.08, ease: 'back.out(2.5)' })\n // Then implode: scale → 0, fade, slight spin.\n .to(view, { rotation: dir * 0.8, alpha: 0, duration: 0.24, ease: 'power2.in' }, '<+=0.05')\n .to(view.scale, { x: 0, y: 0, duration: 0.24, ease: 'power2.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.rotation = 0;\n view.scale.set(1, 1);\n }\n\n /**\n * Lifecycle hook: the owning reel has started spinning.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur) to swap to\n * a blur animation automatically.\n */\n onReelSpinStart(): 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 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 onReelLanded(): void {}\n}\n","import { ReelSymbol } from './ReelSymbol.js';\n\n// Spine types imported dynamically — this is an optional peer dependency\nlet SpineClass: any = null;\n\nasync function loadSpine(): Promise<void> {\n try {\n const spineModule = await import('@esotericsoftware/spine-pixi-v8');\n SpineClass = spineModule.Spine;\n } catch {\n // Spine not available — SpineSymbol will throw on construction\n }\n}\n\n// Attempt to load Spine on module init\nloadSpine();\n\nexport interface SpineSymbolOptions {\n /** Map of symbolId → SkeletonData. */\n skeletonDataMap: Record<string, any>;\n /** Default animation name to play in idle. Default: 'idle'. */\n idleAnimation?: string;\n /** Animation name to play on win. Default: 'win'. */\n winAnimation?: string;\n /** Default skin name. Default: 'default'. */\n defaultSkin?: string;\n}\n\n/**\n * Symbol implementation using Spine 2D skeletal animation.\n *\n * Requires `@esotericsoftware/spine-pixi-v8` as an optional peer dependency.\n * If Spine is not installed, constructing a SpineSymbol will throw.\n */\nexport class SpineSymbol extends ReelSymbol {\n private _spine: any = null;\n private _skeletonDataMap: Record<string, any>;\n private _idleAnimation: string;\n private _winAnimation: string;\n private _defaultSkin: string;\n private _winResolve: (() => void) | null = null;\n private _currentSkeletonKey: string = '';\n\n constructor(options: SpineSymbolOptions) {\n super();\n if (!SpineClass) {\n throw new Error(\n 'SpineSymbol requires @esotericsoftware/spine-pixi-v8 to be installed. ' +\n 'Install it with: npm install @esotericsoftware/spine-pixi-v8',\n );\n }\n this._skeletonDataMap = options.skeletonDataMap;\n this._idleAnimation = options.idleAnimation ?? 'idle';\n this._winAnimation = options.winAnimation ?? 'win';\n this._defaultSkin = options.defaultSkin ?? 'default';\n }\n\n protected onActivate(symbolId: string): void {\n const skeletonData = this._skeletonDataMap[symbolId];\n if (!skeletonData) return;\n\n // Reuse existing spine if same skeleton data\n if (this._currentSkeletonKey !== symbolId) {\n if (this._spine) {\n this.view.removeChild(this._spine);\n this._spine.destroy();\n }\n this._spine = new SpineClass({ skeletonData });\n this.view.addChild(this._spine);\n this._currentSkeletonKey = symbolId;\n }\n\n // Set to idle\n if (this._spine.skeleton.data.findSkin(this._defaultSkin)) {\n this._spine.skeleton.setSkinByName(this._defaultSkin);\n this._spine.skeleton.setSlotsToSetupPose();\n }\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n }\n\n protected onDeactivate(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.state.clearTracks();\n }\n // Settle any pending playWin so awaiters don't hang when the symbol is\n // recycled mid-animation. Mirrors the same fix in SpineReelSymbol.\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n\n async playWin(): Promise<void> {\n if (!this._spine) return;\n if (!this._spine.skeleton.data.findAnimation(this._winAnimation)) return;\n\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n const entry = this._spine.state.setAnimation(0, this._winAnimation, false);\n this._spine.state.addListener({\n complete: (trackEntry: any) => {\n if (trackEntry === entry) {\n this._spine.state.clearListeners();\n // Return to idle\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n this._winResolve = null;\n resolve();\n }\n },\n });\n });\n }\n\n stopAnimation(): void {\n if (!this._spine) return;\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n if (!this._spine) return;\n const bounds = this._spine.getBounds();\n if (bounds.width > 0 && bounds.height > 0) {\n this._spine.scale.set(\n width / bounds.width,\n height / bounds.height,\n );\n }\n }\n\n protected override onDestroy(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.destroy();\n this._spine = null;\n }\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require(`pixi.js`);var t=require(`gsap`).gsap;function n(e){t=e}function r(){return t}var i=class{view;_symbolId=``;_isDestroyed=!1;constructor(){this.view=new e.Container}get symbolId(){return this._symbolId}get isDestroyed(){return this._isDestroyed}activate(e){this._symbolId=e,this.view.visible=!0,this.view.alpha=1,this.view.scale.set(1,1),this.view.rotation=0,this.view.filters=null,this.view.zIndex=0,this.onActivate(e)}deactivate(){this.stopAnimation(),this.onDeactivate(),this._symbolId=``,this.view.visible=!1,this.view.alpha=1,this.view.scale.set(1,1),this.view.rotation=0,this.view.filters=null,this.view.zIndex=0}reset(){this.deactivate()}destroy(){this._isDestroyed||=(this.stopAnimation(),this.onDeactivate(),this.onDestroy(),this.view.destroyed||this.view.destroy({children:!0}),!0)}onDestroy(){}async playDestroy(e){let t=this.view,n=t.pivot.x,i=t.pivot.y,a=t.x,o=t.y,s=t.getLocalBounds(),c=s.x+s.width/2,l=s.y+s.height/2;t.pivot.set(c,l),t.x=a+(c-n),t.y=o+(l-i);let u=e?.direction??(Math.random()<.5?1:-1),d=e?.delay??0,f=e?.signal,p=()=>{t.alpha=0,t.scale.set(0,0)};if(f?.aborted){p(),t.pivot.set(n,i),t.x=a,t.y=o,t.rotation=0,t.scale.set(1,1),t.alpha=0;return}await new Promise(e=>{let n=r().timeline({onComplete:()=>{f&&f.removeEventListener(`abort`,i),e()},delay:d}).to(t.scale,{x:1.25,y:1.25,duration:.08,ease:`back.out(2.5)`}).to(t,{rotation:u*.8,alpha:0,duration:.24,ease:`power2.in`},`<+=0.05`).to(t.scale,{x:0,y:0,duration:.24,ease:`power2.in`},`<`),i=()=>{n.kill(),p(),e()};f&&f.addEventListener(`abort`,i,{once:!0})}),t.pivot.set(n,i),t.x=a,t.y=o,t.rotation=0,t.scale.set(1,1)}onReelSpinStart(){}onReelSpinEnd(){}onReelLanded(){}},a=null;async function o(){try{a=(await import(`@esotericsoftware/spine-pixi-v8`)).Spine}catch{}}o();var s=class extends i{_spine=null;_skeletonDataMap;_idleAnimation;_winAnimation;_defaultSkin;_winResolve=null;_currentSkeletonKey=``;constructor(e){if(super(),!a)throw Error(`SpineSymbol requires @esotericsoftware/spine-pixi-v8 to be installed. Install it with: npm install @esotericsoftware/spine-pixi-v8`);this._skeletonDataMap=e.skeletonDataMap,this._idleAnimation=e.idleAnimation??`idle`,this._winAnimation=e.winAnimation??`win`,this._defaultSkin=e.defaultSkin??`default`}onActivate(e){let t=this._skeletonDataMap[e];t&&(this._currentSkeletonKey!==e&&(this._spine&&(this.view.removeChild(this._spine),this._spine.destroy()),this._spine=new a({skeletonData:t}),this.view.addChild(this._spine),this._currentSkeletonKey=e),this._spine.skeleton.data.findSkin(this._defaultSkin)&&(this._spine.skeleton.setSkinByName(this._defaultSkin),this._spine.skeleton.setSlotsToSetupPose()),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0))}onDeactivate(){if(this._spine&&(this._spine.state.clearListeners(),this._spine.state.clearTracks()),this._winResolve){let e=this._winResolve;this._winResolve=null,e()}}async playWin(){if(this._spine&&this._spine.skeleton.data.findAnimation(this._winAnimation))return new Promise(e=>{this._winResolve=e;let t=this._spine.state.setAnimation(0,this._winAnimation,!1);this._spine.state.addListener({complete:n=>{n===t&&(this._spine.state.clearListeners(),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0),this._winResolve=null,e())}})})}stopAnimation(){this._spine&&(this._spine.state.clearListeners(),this._spine.skeleton.data.findAnimation(this._idleAnimation)&&this._spine.state.setAnimation(0,this._idleAnimation,!0),this._winResolve&&=(this._winResolve(),null))}resize(e,t){if(!this._spine)return;let n=this._spine.getBounds();n.width>0&&n.height>0&&this._spine.scale.set(e/n.width,t/n.height)}onDestroy(){if(this._spine&&=(this._spine.state.clearListeners(),this._spine.destroy(),null),this._winResolve){let e=this._winResolve;this._winResolve=null,e()}}};Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return s}});
|
|
2
|
+
//# sourceMappingURL=SpineSymbol-ojWlEPwt.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpineSymbol-ojWlEPwt.cjs","names":[],"sources":["../src/utils/gsapRef.ts","../src/symbols/ReelSymbol.ts","../src/symbols/SpineSymbol.ts"],"sourcesContent":["import { gsap as defaultGsap } from 'gsap';\n\n/**\n * The gsap instance every phase, motion tween, and cascade animation in\n * the engine reads from.\n *\n * **Why this indirection exists:** under tools that resolve modules through\n * symlinked workspaces (vite + a locally-linked pixi-reels, pnpm dev\n * 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\n * can resolve to *different module instances* — each with its own root\n * timeline. The consumer drives one, the lib's tweens live on the other,\n * and reels stall at progress 0.\n *\n * The fix: every internal animation site reads gsap through `getGsap()`\n * instead of importing the `'gsap'` module directly. The builder method\n * `ReelSetBuilder.gsap(myGsap)` rebinds the singleton so the consumer's\n * gsap is the one the engine uses.\n *\n * Defaults to the gsap import resolved at lib-load time, so consumers\n * who don't hit the dual-instance trap don't have to do anything.\n */\nlet currentGsap: typeof defaultGsap = defaultGsap;\n\n/**\n * Replace the gsap instance the engine drives. Call this BEFORE\n * `ReelSetBuilder.build()` (the builder does this for you when you call\n * `.gsap(instance)`).\n *\n * Process-global: there's only one bound instance at a time. If you\n * build several ReelSets with different gsap instances, the last\n * `setGsap` call wins.\n */\nexport function setGsap(g: typeof defaultGsap): void {\n currentGsap = g;\n}\n\n/** The currently-bound gsap instance. */\nexport function getGsap(): typeof defaultGsap {\n return currentGsap;\n}\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { getGsap } from '../utils/gsapRef.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 constructor() {\n this.view = new Container();\n }\n\n get symbolId(): string {\n return this._symbolId;\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 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.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 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 this.stopAnimation();\n this.onDeactivate();\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 // Override if needed\n }\n\n /** Play the win/highlight animation for this symbol. Resolves when complete. */\n abstract playWin(): 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 * Default implementation: brief scale-up \"charge\" then implode (scale 0\n * + spin + fade), squishing around the symbol's bounding-box CENTER\n * regardless of the view's anchor. Total ~320 ms. The view is left at\n * `alpha: 0` (destroyed); position / pivot are restored so pool reuse\n * via `_replaceSymbol`'s same-id fast path doesn't inherit a stale\n * pivot offset.\n *\n * Override in subclasses for art-appropriate destruction — e.g. a\n * Spine symbol can play its `disintegration` track here, or a sprite\n * symbol can swap to a shatter atlas. The promise must resolve when\n * the symbol is no longer visible.\n *\n * `opts.direction` — rotation direction (`1` or `-1`). Default: random.\n * For coherent clusters, callers should pass `w.reel % 2 === 0 ? 1 : -1`\n * (alternate by column) instead of relying on random.\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?: { direction?: 1 | -1; 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 scale + rotation squish 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 view.pivot.set(cx, cy);\n view.x = originalX + (cx - originalPivotX);\n view.y = originalY + (cy - originalPivotY);\n\n const dir = opts?.direction ?? (Math.random() < 0.5 ? 1 : -1);\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.rotation = 0;\n view.scale.set(1, 1);\n view.alpha = 0;\n return;\n }\n\n await new Promise<void>((resolve) => {\n const tl = getGsap()\n .timeline({ onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, delay })\n // Brief scale-up \"charge\" so the impending destruction has a beat\n // of anticipation before the implode.\n .to(view.scale, { x: 1.25, y: 1.25, duration: 0.08, ease: 'back.out(2.5)' })\n // Then implode: scale → 0, fade, slight spin.\n .to(view, { rotation: dir * 0.8, alpha: 0, duration: 0.24, ease: 'power2.in' }, '<+=0.05')\n .to(view.scale, { x: 0, y: 0, duration: 0.24, ease: 'power2.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.rotation = 0;\n view.scale.set(1, 1);\n }\n\n /**\n * Lifecycle hook: the owning reel has started spinning.\n * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur) to swap to\n * a blur animation automatically.\n */\n onReelSpinStart(): 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 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 onReelLanded(): void {}\n}\n","import { ReelSymbol } from './ReelSymbol.js';\n\n// Spine types imported dynamically — this is an optional peer dependency\nlet SpineClass: any = null;\n\nasync function loadSpine(): Promise<void> {\n try {\n const spineModule = await import('@esotericsoftware/spine-pixi-v8');\n SpineClass = spineModule.Spine;\n } catch {\n // Spine not available — SpineSymbol will throw on construction\n }\n}\n\n// Attempt to load Spine on module init\nloadSpine();\n\nexport interface SpineSymbolOptions {\n /** Map of symbolId → SkeletonData. */\n skeletonDataMap: Record<string, any>;\n /** Default animation name to play in idle. Default: 'idle'. */\n idleAnimation?: string;\n /** Animation name to play on win. Default: 'win'. */\n winAnimation?: string;\n /** Default skin name. Default: 'default'. */\n defaultSkin?: string;\n}\n\n/**\n * Symbol implementation using Spine 2D skeletal animation.\n *\n * Requires `@esotericsoftware/spine-pixi-v8` as an optional peer dependency.\n * If Spine is not installed, constructing a SpineSymbol will throw.\n */\nexport class SpineSymbol extends ReelSymbol {\n private _spine: any = null;\n private _skeletonDataMap: Record<string, any>;\n private _idleAnimation: string;\n private _winAnimation: string;\n private _defaultSkin: string;\n private _winResolve: (() => void) | null = null;\n private _currentSkeletonKey: string = '';\n\n constructor(options: SpineSymbolOptions) {\n super();\n if (!SpineClass) {\n throw new Error(\n 'SpineSymbol requires @esotericsoftware/spine-pixi-v8 to be installed. ' +\n 'Install it with: npm install @esotericsoftware/spine-pixi-v8',\n );\n }\n this._skeletonDataMap = options.skeletonDataMap;\n this._idleAnimation = options.idleAnimation ?? 'idle';\n this._winAnimation = options.winAnimation ?? 'win';\n this._defaultSkin = options.defaultSkin ?? 'default';\n }\n\n protected onActivate(symbolId: string): void {\n const skeletonData = this._skeletonDataMap[symbolId];\n if (!skeletonData) return;\n\n // Reuse existing spine if same skeleton data\n if (this._currentSkeletonKey !== symbolId) {\n if (this._spine) {\n this.view.removeChild(this._spine);\n this._spine.destroy();\n }\n this._spine = new SpineClass({ skeletonData });\n this.view.addChild(this._spine);\n this._currentSkeletonKey = symbolId;\n }\n\n // Set to idle\n if (this._spine.skeleton.data.findSkin(this._defaultSkin)) {\n this._spine.skeleton.setSkinByName(this._defaultSkin);\n this._spine.skeleton.setSlotsToSetupPose();\n }\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n }\n\n protected onDeactivate(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.state.clearTracks();\n }\n // Settle any pending playWin so awaiters don't hang when the symbol is\n // recycled mid-animation. Mirrors the same fix in SpineReelSymbol.\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n\n async playWin(): Promise<void> {\n if (!this._spine) return;\n if (!this._spine.skeleton.data.findAnimation(this._winAnimation)) return;\n\n return new Promise<void>((resolve) => {\n this._winResolve = resolve;\n const entry = this._spine.state.setAnimation(0, this._winAnimation, false);\n this._spine.state.addListener({\n complete: (trackEntry: any) => {\n if (trackEntry === entry) {\n this._spine.state.clearListeners();\n // Return to idle\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n this._winResolve = null;\n resolve();\n }\n },\n });\n });\n }\n\n stopAnimation(): void {\n if (!this._spine) return;\n this._spine.state.clearListeners();\n if (this._spine.skeleton.data.findAnimation(this._idleAnimation)) {\n this._spine.state.setAnimation(0, this._idleAnimation, true);\n }\n if (this._winResolve) {\n this._winResolve();\n this._winResolve = null;\n }\n }\n\n resize(width: number, height: number): void {\n if (!this._spine) return;\n const bounds = this._spine.getBounds();\n if (bounds.width > 0 && bounds.height > 0) {\n this._spine.scale.set(\n width / bounds.width,\n height / bounds.height,\n );\n }\n }\n\n protected override onDestroy(): void {\n if (this._spine) {\n this._spine.state.clearListeners();\n this._spine.destroy();\n this._spine = null;\n }\n if (this._winResolve) {\n const r = this._winResolve;\n this._winResolve = null;\n r();\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for tumble cascade phases. Passed to
|
|
3
|
+
* `ReelSetBuilder.tumble(config)` and baked into the three phase classes at
|
|
4
|
+
* build time. Pure animation values — every callback you want is an event
|
|
5
|
+
* (`reelSet.events.on('cascade:...', ...)`), never a config field.
|
|
6
|
+
*/
|
|
7
|
+
export interface TumbleFallConfig {
|
|
8
|
+
/**
|
|
9
|
+
* How long each symbol's fall-out tween runs, in ms. Default 300.
|
|
10
|
+
*/
|
|
11
|
+
duration?: number;
|
|
12
|
+
/**
|
|
13
|
+
* GSAP easing string for the fall trajectory. Default `'sine.in'`
|
|
14
|
+
* (gravity feel). Anything from gsap.com/docs/v3/Eases works.
|
|
15
|
+
*/
|
|
16
|
+
ease?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Delay between successive rows starting their fall, in ms. `0` makes
|
|
19
|
+
* every row fall together. Default 0.
|
|
20
|
+
*/
|
|
21
|
+
rowStagger?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Which row of each reel begins its fall first.
|
|
24
|
+
*
|
|
25
|
+
* - `'bottomToTop'` (default) — bottom row falls first, top row last.
|
|
26
|
+
* Pairs with the per-reel left-to-right stagger from `speed.spinDelay`
|
|
27
|
+
* to give the canonical "bottom-left falls first, top-right last"
|
|
28
|
+
* feel of commercial tumble slots.
|
|
29
|
+
* - `'topToBottom'` — top row falls first. Reads as the column
|
|
30
|
+
* "peeling" downward; useful for theme-specific effects.
|
|
31
|
+
*/
|
|
32
|
+
rowOrder?: 'bottomToTop' | 'topToBottom';
|
|
33
|
+
}
|
|
34
|
+
export interface TumbleDropInConfig {
|
|
35
|
+
/**
|
|
36
|
+
* How long each symbol's drop-in tween runs, in ms. Default 600.
|
|
37
|
+
*/
|
|
38
|
+
duration?: number;
|
|
39
|
+
/**
|
|
40
|
+
* GSAP easing string for the drop-in trajectory. Default `'power2.out'`
|
|
41
|
+
* — symbols decelerate cleanly into their slot with NO overshoot, which
|
|
42
|
+
* matches the canonical commercial-slot pattern: fall straight in, then
|
|
43
|
+
* play a per-symbol landing spine animation. Use `'back.out(1.5)'` for a
|
|
44
|
+
* soft overshoot, `'bounce.out'` for cartoon bounce, `'sine.in'` for
|
|
45
|
+
* gravity, `'expo.in'` for slam.
|
|
46
|
+
*/
|
|
47
|
+
ease?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Delay between successive rows starting their drop, in ms. Default 60.
|
|
50
|
+
* `0` makes every animated row drop in simultaneously — the most common
|
|
51
|
+
* choice for cascade refills.
|
|
52
|
+
*/
|
|
53
|
+
rowStagger?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Which row lands first when `rowStagger > 0`.
|
|
56
|
+
*
|
|
57
|
+
* - `'bottomToTop'` (default) — bottom row arrives first, top row last.
|
|
58
|
+
* Paired with `setDropOrder('ltr')` per-reel stagger this gives the
|
|
59
|
+
* canonical "bottom-left first, top-right last" reveal that every
|
|
60
|
+
* commercial tumble slot ships with.
|
|
61
|
+
* - `'topToBottom'` — top row arrives first. Reads as "new symbols
|
|
62
|
+
* pour from above"; fits gravity-themed or rain-style slots.
|
|
63
|
+
*/
|
|
64
|
+
rowOrder?: 'bottomToTop' | 'topToBottom';
|
|
65
|
+
/**
|
|
66
|
+
* How far symbols fall, in cells.
|
|
67
|
+
*
|
|
68
|
+
* - `'perHole'` (default) — gravity-correct. Each symbol falls exactly
|
|
69
|
+
* as far as its hole demands: new symbols from above, survivors slide
|
|
70
|
+
* down the count of holes below them, untouched symbols don't move.
|
|
71
|
+
* - `'auto'` — every symbol falls the full visible-rows distance. Best
|
|
72
|
+
* for Moment A (initial drop, "the entire column drops in unison")
|
|
73
|
+
* and for refills made up entirely of new symbols. For refills with
|
|
74
|
+
* SURVIVORS the engine silently falls back to per-hole geometry for
|
|
75
|
+
* those movers — `'auto'` would teleport a sliding survivor above
|
|
76
|
+
* the viewport before dropping it back down, which reads as a flash.
|
|
77
|
+
* - `number` — explicit pixel distance applied uniformly to every
|
|
78
|
+
* animated symbol.
|
|
79
|
+
*/
|
|
80
|
+
distance?: 'perHole' | 'auto' | number;
|
|
81
|
+
}
|
|
82
|
+
export interface TumbleConfig {
|
|
83
|
+
/** Fall-out animation (existing symbols leaving on `spin()` click). */
|
|
84
|
+
fall?: TumbleFallConfig;
|
|
85
|
+
/** Drop-in animation (new symbols arriving after `setResult` or in `refill`). */
|
|
86
|
+
dropIn?: TumbleDropInConfig;
|
|
87
|
+
}
|
|
88
|
+
/** Resolved config with defaults applied. Internal type. */
|
|
89
|
+
export interface ResolvedTumbleConfig {
|
|
90
|
+
fall: Required<TumbleFallConfig>;
|
|
91
|
+
dropIn: Required<TumbleDropInConfig>;
|
|
92
|
+
}
|
|
93
|
+
export declare function resolveTumbleConfig(config: TumbleConfig | undefined): ResolvedTumbleConfig;
|
|
94
|
+
//# sourceMappingURL=TumbleConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TumbleConfig.d.ts","sourceRoot":"","sources":["../../src/cascade/TumbleConfig.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC;CAC1C;AAED,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC;IAEzC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,iFAAiF;IACjF,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED,4DAA4D;AAC5D,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;CACtC;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,oBAAoB,CAqB1F"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gravity-correct refill geometry for tumble cascades.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct moments use the same algorithm with different inputs:
|
|
5
|
+
*
|
|
6
|
+
* - **Moment A (initial drop):** `winnerRows = []`. The entire visible
|
|
7
|
+
* column is treated as "new" — every row falls in from above the
|
|
8
|
+
* viewport. The vertical distance per row is `visibleRows` cells, so
|
|
9
|
+
* all rows arrive at their grid positions in the same beat.
|
|
10
|
+
*
|
|
11
|
+
* - **Moment B (cascade refill):** `winnerRows` lists the rows whose
|
|
12
|
+
* symbols were removed by the most recent win. Survivors slide DOWN
|
|
13
|
+
* to fill the gaps below them; new symbols enter from above into the
|
|
14
|
+
* top holes. The new grid follows the server convention that survivors
|
|
15
|
+
* keep their relative order and pack to the bottom, with `winnerRows.length`
|
|
16
|
+
* new symbols stacked above them.
|
|
17
|
+
*/
|
|
18
|
+
/** A cell coordinate on the reel set — `reel` is column, `row` is visible row. */
|
|
19
|
+
export interface Cell {
|
|
20
|
+
reel: number;
|
|
21
|
+
row: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DropOffset {
|
|
24
|
+
/** Visible row in the new grid (top-to-bottom, 0-indexed). */
|
|
25
|
+
row: number;
|
|
26
|
+
/**
|
|
27
|
+
* Where this symbol "came from" expressed as a virtual row index. Negative
|
|
28
|
+
* values indicate "above the viewport" (e.g. -1 is one cell above row 0).
|
|
29
|
+
* Non-negative values indicate "this row in the OLD grid" — a survivor.
|
|
30
|
+
*/
|
|
31
|
+
originalRow: number;
|
|
32
|
+
/**
|
|
33
|
+
* Number of cells this symbol must traverse downward. Equals
|
|
34
|
+
* `row - originalRow`. Zero means the symbol stays put (no animation).
|
|
35
|
+
*/
|
|
36
|
+
offsetRows: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compute per-row drop offsets for one reel given its winner set.
|
|
40
|
+
*
|
|
41
|
+
* Returns one entry per visible row, top-to-bottom. Rows with
|
|
42
|
+
* `offsetRows === 0` should NOT be animated — they're survivors that
|
|
43
|
+
* didn't move.
|
|
44
|
+
*
|
|
45
|
+
* **Convention** (Moment B): the new grid must place new symbols at the
|
|
46
|
+
* top `winnerRows.length` rows and survivors at the bottom rows in their
|
|
47
|
+
* original top-to-bottom order. This matches how server-side gravity
|
|
48
|
+
* simulations emit cascade results.
|
|
49
|
+
*
|
|
50
|
+
* @param options.initial - When `true` (Moment A — the player's first
|
|
51
|
+
* spin click), every row is treated as new regardless of `winnerRows`
|
|
52
|
+
* (which is normally empty for initial spins). When `false` (Moment B
|
|
53
|
+
* — cascade refill), an empty `winnerRows` means *no movement on this
|
|
54
|
+
* reel*; survivor reels in a refill correctly return all-zero offsets.
|
|
55
|
+
* Default `false` so callers can't accidentally trigger a full re-drop
|
|
56
|
+
* on a reel that had no winners.
|
|
57
|
+
*/
|
|
58
|
+
export declare function computeDropOffsets(visibleRows: number, winnerRows: readonly number[], options?: {
|
|
59
|
+
initial?: boolean;
|
|
60
|
+
}): DropOffset[];
|
|
61
|
+
//# sourceMappingURL=tumbleAlgorithm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tumbleAlgorithm.d.ts","sourceRoot":"","sources":["../../src/cascade/tumbleAlgorithm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,kFAAkF;AAClF,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,UAAU,EAAE,CA8Bd"}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Container, Ticker } from 'pixi.js';
|
|
|
6
6
|
export interface SpinOptions {
|
|
7
7
|
/**
|
|
8
8
|
* Phase chain selector for this spin.
|
|
9
|
-
* `'cascade'` requires `.
|
|
9
|
+
* `'cascade'` requires `.tumble(...)` on the builder.
|
|
10
10
|
*/
|
|
11
11
|
mode?: 'standard' | 'cascade';
|
|
12
12
|
/**
|
package/dist/core/Reel.d.ts
CHANGED
|
@@ -232,7 +232,16 @@ export declare class Reel implements Disposable {
|
|
|
232
232
|
* caller-facing surface that also throws on pinned cells.
|
|
233
233
|
*/
|
|
234
234
|
setSymbolAt(visibleRow: number, symbolId: string): void;
|
|
235
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Place symbols immediately at target positions (for skip/turbo).
|
|
237
|
+
*
|
|
238
|
+
* `symbolIds[0..n-1]` is the visible area. `symbolIds[n..]` (if present)
|
|
239
|
+
* targets buffer-below slots. Buffer-above slots are addressed via
|
|
240
|
+
* negative-index string properties: `symbolIds[-1]` is the slot closest to
|
|
241
|
+
* the visible top row, `symbolIds[-bufferAbove]` the furthest above.
|
|
242
|
+
* Unset slots are filled with random symbols, matching the previous
|
|
243
|
+
* behaviour when only visible-area entries were provided.
|
|
244
|
+
*/
|
|
236
245
|
placeSymbols(symbolIds: string[]): void;
|
|
237
246
|
/**
|
|
238
247
|
* @internal — MultiWays orchestration only.
|
package/dist/core/Reel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Reel.d.ts","sourceRoot":"","sources":["../../src/core/Reel.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"Reel.d.ts","sourceRoot":"","sources":["../../src/core/Reel.ts"],"names":[],"mappings":"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"}
|