PR pixi-reels
All demos
pyramid cascade ways gravity

Pyramid cascade (3-5-5-5-3)

Jagged pyramid - ways pay - gravity refill (survivors stay, only gaps drop)

The mechanic

new ReelSetBuilder()
  .reels(5)
  .visibleRowsPerReel([3, 5, 5, 5, 3])
  .reelAnchor('center')   // diamond shape
  .tumble({
    fall:   { duration: 280, ease: 'power3.in',  rowStagger: 60 },
    dropIn: { duration: 450, ease: 'power3.out', rowStagger: 60, distance: 'perHole' },
  })

A stiff power3.out drop-in (no overshoot) keeps the symbols landing hard. the bouncy default would fight the win animation that arrives a frame later.

Gravity refill (not “redrop the whole frame”)

A naive cascade phase would fall all old symbols out then drop all new ones in. That’s wrong for a tumble: survivors should stay put, only the gaps left by winners should refill from above.

The fix is the canonical cascade pair, reelSet.destroySymbols(winners) + reelSet.refill({ winners, grid }):

await reelSet.destroySymbols(winnerCells);                  // pop animation
const next = computeRefillGrid(grid, winnerCells);          // gravity-correct grid (string[][])
await reelSet.refill({                                       // survivors slide; new arrive
  winners: winnerCells,
  grid: next.map((visible) => ({ visible })),               // wrap as ColumnTarget[]
});

Per column the engine computes per-cell offsets:

  • New symbol filling cleared row R -> starts (R+1) slots above the viewport.
  • Survivor at final row R, originally at row N -> falls (R - N) slots.
  • A symbol whose R === N doesn’t move at all.

So a 5-row reel with a single winner at row 0 only animates one symbol. the new top. Everything below stays put. Jagged shapes work the same way; each reel’s visibleRows is read at refill time.

Ways evaluation on a jagged grid

Same algorithm as MultiWays: walk reels left-to-right counting reels that contain the kind (or wild). The pyramid’s 3-row outer reels just have fewer cells per reel. the chain logic doesn’t care:

for (let c = 0; c < REEL_COUNT; c++) {
  const matches = grid[c].flatMap((s, r) =>
    s === kind || s === 'wild' ? [{ reelIndex: c, rowIndex: r }] : []);
  if (matches.length === 0) break;
  cellsByReel.push(matches);
}

Round flow

  1. roundBus.emit('round:reset'). WinBox to 0.
  2. Initial reelSet.spin()setDropOrder('ltr')setResult(grid).
  3. reelSet.runCascade({ detectWinners, nextGrid }) loops the chain. the awaited promise resolves with RunCascadeResult when it ends.
  4. Inside onCascade: roundBus.emit('win:add', cascadeWin). WinBox additively tickups.
  5. Status shows N CASCADES. TOTAL.