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.middlewarelists the active pipeline at any time
Middleware priorities
Built-in middleware uses these priorities:
| Name | Priority | Role |
|---|---|---|
random-fill | 0 | Fills empty slots with strip-random symbols |
target-placement | 10 | Overlays 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.
Related
- Spin first, cascade after. full demo of the per-spin
modeoverride - Mystery reveal (CellPin). similar idea using pins instead of middleware
- Walking wild (movePin). pair with feature mode swap for a “walking wild only during the bonus” pattern