PR pixi-reels
All demos
cascade tumble multiplier

Cascade + multiplier

5×5 · one click plays the whole tumble · drop-in for streamed server responses

The mechanic

Each SPIN click plays the full cascade sequence from a single click:

  1. Land on stage 0 via reelSet.spin() + setResult(...).
  2. reelSet.runCascade({ detectWinners, nextGrid }) owns the chain.
  3. For every cascade level: detect winners → destroySymbols → pause → refill.
  4. When detectWinners returns [], the awaited runCascade promise resolves with RunCascadeResult and the round ends.

The library owns the orchestration; your two callbacks own the game rules (what’s a winner, what’s the next grid).

Setup

1. Ship a single-click cascade with a batch response

If your server returns all cascade stages in one response:

async function spin() {
  const response = await fetch('/api/spin').then((r) => r.json());
  // response.stages: string[][][]. stage 0 first, tumble stages next.

  // Land the first stage via the regular spin API.
  const p = reelSet.spin();
  reelSet.setResult(response.stages[0]);
  await p;

  // Cascade through the remaining stages.
  let level = 0;
  reelSet.setDropOrder('all');
  await reelSet.runCascade({
    detectWinners: (grid) =>
      level + 1 < response.stages.length
        ? diffWinners(grid, response.stages[level + 1])
        : [],
    nextGrid: () => {
      level += 1;
      return response.stages[level];
    },
    onCascade: ({ chain }) => hud.showMultiplier(chain + 1),
    pauseAfterDestroyMs: 140,
  });
}

diffWinners is whatever match-detector your game uses. it’s NOT a naive grid diff, because survivors that slide past cleared rows would look “changed” by id. Compute winners from your match rules.

2. Stream stages from a server that dribbles responses

If your backend computes cascades one at a time and streams them, do the same shape but await the next stage inside nextGrid:

const stream = streamFromServer();
const first = (await stream.next()).value!;

const p = reelSet.spin();
reelSet.setResult(first);
await p;

await reelSet.runCascade({
  detectWinners: (g) => yourMatchDetector(g),
  nextGrid:      async () => {
    const r = await stream.next();
    return r.done ? r.value /* shouldn't happen if detect returns [] first */ : r.value;
  },
  pauseAfterDestroyMs: 100,
});

nextGrid can be async, so awaiting a fresh server response between cascades is a one-liner.

3. Custom vanish animation

By default, destroySymbols defers to each symbol’s playDestroy() (sprite implode by default). Three ways to customize:

  • Per-symbol. your SpriteSymbol/SpineSymbol subclass overrides playDestroy(). Every cascade uses it.
  • Per-cascade options. pass destroyOptions: { direction, delay, dim } to runCascade.
  • Replace entirely. drive the visual yourself inside onCascade, then pass destroyOptions: { zIndex: null } so the engine’s implode is invisible. See cascade with WinPresenter.

4. Identifying winners

The canonical winner shape is Cell[]. { reel, row } for every cell your match rules consider a winner. Compute it from the grid your detectWinners callback receives; never from a naive diff of “the next grid differs at row X.” That bug only surfaces when survivors slide past cleared rows, and is the most common cascade implementation mistake.

Cheats on this page

  • One-click 4-stage cascade (default on). cascadingStages(STAGES) bundles the whole tumble in one response; runCascade consumes meta.stages via nextGrid.
  • One stage per click (legacy). cascadeSequence(STAGES); each click lands one stage. Useful for stepping through the sequence manually.

Test the chain

import { computeDropOffsets } from 'pixi-reels';

// Validate your server's gravity sim offline.
const offsets = computeDropOffsets(5 /* visible rows */, [1, 3] /* winnerRows */);
expect(offsets.find(o => o.row === 0)?.offsetRows).toBe(1); // new symbol, 1 slot
expect(offsets.find(o => o.row === 4)?.offsetRows).toBe(0); // survivor, no move

For end-to-end tests that drive a real ReelSet, use the testing utilities. createTestReelSet, FakeTicker, spinAndLand, captureEvents.