PR pixi-reels
All recipes

Feature mode swap

Toggle a bonus mode on and off at runtime by adding and removing a frame middleware. no ReelSet rebuild, no rebuild latency.

Loading recipe…

The base game and the bonus feature usually only differ in strip weighting. More wilds, fewer low-pays, maybe a symbol replacement. In the past, swapping between them meant destroying the ReelSet and rebuilding. a lot of work to change a few probabilities.

reelSet.frame.use() and .remove() are the surface for toggling middleware at runtime. The FrameBuilder underneath always supported runtime mutation; this recipe shows the API.

The mechanic

const featureMoreWildsMiddleware: FrameMiddleware = {
  name: 'feature-more-wilds',
  priority: 20, // after target-placement (10) so it sees the final grid
  process(ctx, next) {
    for (let i = ctx.bufferAbove; i < ctx.bufferAbove + ctx.visibleRows; i++) {
      if (ctx.symbols[i] === 'low-symbol' && Math.random() < 0.35) {
        ctx.symbols[i] = 'wild';
      }
    }
    next();
  },
};

function enterFeature() {
  reelSet.frame.use(featureMoreWildsMiddleware);
}

function exitFeature() {
  reelSet.frame.remove('feature-more-wilds');
}

Call enterFeature() when your bonus triggers; call exitFeature() when the respin counter runs out. The very next spin builds its frame with the new pipeline.

Why middleware over rebuild

  • Zero rebuild. no symbol pool recreation, no textures to reload, no state loss
  • Clean boundary. feature-mode logic lives in one named middleware; entry and exit are one line each
  • Stackable. add multiple feature-mode middleware together (more wilds + mystery injection + no-triples constraint)
  • Diagnosable. reelSet.frame.middleware lists the active pipeline at any time

Middleware priorities

Built-in middleware uses these priorities:

NamePriorityRole
random-fill0Fills empty slots with strip-random symbols
target-placement10Overlays the setResult() targets onto the visible area

Your custom middleware should generally run after target-placement (priority ≥ 10) so it can react to what the server chose.

When to reach for spin({ mode }) instead

Frame middleware is the right tool for same shape, different content. the reels still strip-spin, the cells still land on grid; only the per-cell symbol probabilities change.

If the FEATURE you’re toggling needs a different motion model (e.g. base game strip-spins, but the bonus drops symbols in from above like a cascade), the per-spin mode override is the cleaner path. Same ReelSet, no middleware needed:

// Builder: register the tumble phases up front so the override is legal.
const reelSet = new ReelSetBuilder()
  // ...
  .tumble({
    fall:   { duration: 280, ease: 'power3.in',  rowStagger: 60 },
    dropIn: { duration: 450, ease: 'power3.out', rowStagger: 60, distance: 'perHole' },
  })
  .ticker(app.ticker)
  .build();

// Base round: classic strip-spin (note: .tumble() flipped the default
// to 'cascade', so you have to explicitly request 'standard' here).
await reelSet.spin({ mode: 'standard' });

// Bonus round: drop-in motion. Same reels, same pool, same spotlight.
// only the phase chain swaps.
await reelSet.spin({ mode: 'cascade' });

The two tools compose: a feature can both swap its phase chain (mode) AND swap its strip weights (frame middleware) in the same round.