pixi-reels
All recipes

Cascade 6×5 tumble

Modern tumble/cascade mechanic on the runCascade orchestrator. the MultiWays-adjacent style every slot studio is shipping.

Loading recipe…

The same starter, with real art

Everything below applies to both canvases. This one swaps the card-symbol registry for real Thunderkick spine symbols and changes nothing else: same spin, same runCascade, same callbacks, line for line. The whole diff is the registration block (spine map + outAnimation: 'explode' so the destroy plays the authored explosion) and the id constants. Clone the card one to start building today; diff it against this one when your art lands:

Loading recipe…

A cascade/tumble slot on the modern API. .tumble({ fall, dropIn }) replaces the strip-spin mechanic with a drop-in: existing symbols fall off on spin() click, new symbols arrive when setResult returns. The cascade loop. destroy winners, pause, refill. is owned by reelSet.runCascade(...); you supply two callbacks (detectWinners, nextGrid) that encode the game rules.

import { ReelSetBuilder, SpriteSymbol, type Cell } from 'pixi-reels';

const reelSet = new ReelSetBuilder()
  .reels(6)
  .visibleRows(5)
  .symbolSize(110, 110)
  .symbols((r) => {
    for (const id of SYMBOLS) r.register(id, SpriteSymbol, { textures });
  })
  .tumble({
    fall:   { duration: 283, ease: 'power3.in', rowStagger: 67 },
    dropIn: { duration: 450, ease: 'power2.in', rowStagger: 67, distance: 'perHole' },
  })
  .ticker(app.ticker)
  .build();

app.stage.addChild(reelSet);

let multiplier = 1;
document.getElementById('spin')!.addEventListener('click', async () => {
  multiplier = 1;

  // Moment A. initial drop, left-to-right reveal.
  reelSet.setDropOrder('ltr');
  const spinDone = reelSet.spin();
  reelSet.setResult(await server.spin());
  await spinDone;

  // Moment B. runCascade owns the loop. setDropOrder('all') is the
  // canonical refill pattern (every column drops together).
  reelSet.setDropOrder('all');
  await reelSet.runCascade({
    detectWinners: (grid) => detectClusterWins(grid),
    nextGrid:      (_, winners) => server.cascade(winners),
    onCascade:     ({ chain }) => {
      multiplier += 1;
      showMultiplier(multiplier);
    },
  });
});

runCascade(...) returns Promise<RunCascadeResult>. await it for the single “the cascade chain is over” hook (big-win UI, autoplay continuation, analytics). The summary carries chainLength, totalWinners, finalGrid, and wasSkipped.

Drop order controls the reveal feel

setDropOrder is a per-spin call. mix orders freely:

reelSet.setDropOrder('ltr');   // left → right (initial spin reveal)
reelSet.setDropOrder('rtl');   // right → left
reelSet.setDropOrder('all');   // all columns simultaneously
reelSet.setDropOrder([0, 0, 150, 150, 300, 300]); // custom ms per reel

Tumble config. pick a feel

.tumble({ fall, dropIn }) is pure animation values; swap them to change the entire feel without touching code.

These symbols have full-size rectangular plates, so every ease is a plain acceleration into a dead stop. no overshoot (back/bounce gaps the frames; frameless-art only) and no decelerating settle (a framed tile easing into place reads mushy):

Feelfall.easedropIn.easedropIn.rowStaggernotes
Classicpower2.inpower2.in50 msgravity both ways, all-rounder
Slow droppower2.inpower2.in (700 ms)70 mslong hang, each row its own beat
Slam stoppower3.inpower3.in25 mssteepest hit
Rain columnpower2.inpower2.in0 ms (+ distance: 'auto')whole column drops as a slab
Wavepower2.inpower2.in110 msrolling top-to-bottom arrival

See the full tumble recipe doc for the recipe vocabulary and the per-symbol event hooks (cascade:fall:symbol / cascade:dropIn:symbol) you’d use for squish, spine, badge mutations, etc.

What the library owns vs. what you own

pixi-reels deliberately doesn’t own the win-detection rules. every game has quirks (cluster size, adjacency, minimum matches, sticky survivors). What you get from the library:

  • .tumble({ fall, dropIn }). three named phases (cascade:fall / cascade:place / cascade:dropIn), each independently overridable
  • reelSet.destroySymbols(cells). defers to each symbol’s playDestroy(); sprite symbols get an implode, Spine subclasses route to out
  • reelSet.refill({ winners, grid }). gravity-correct Moment B: untouched survivors stay, survivors above a hole slide, new symbols enter from above
  • reelSet.runCascade({ detectWinners, nextGrid }). the canonical detect → destroy → pause → refill orchestration; await resolves with RunCascadeResult when the chain ends
  • reelSet.setDropOrder(). per-call column reveal order (set once, persists across the chain)

Your responsibilities:

  • detectWinners(grid). cluster / adjacency / line-pay rules
  • nextGrid(prev, winners). server-side gravity sim (or the client fallback)
  • showMultiplier(n), SFX, big-win UI. anything game-specific

If you outgrow runCascade (per-cascade asymmetric pauses, conditional bonus triggers, custom slam handling), reach for refill() directly. The wrapper is opt-in.

Assets provided by Thunderkick