A slam force-completes every phase, AnticipationPhase included - honest on an ordinary spin, and it hides the build-up on the spin that had one. These recipes are the levers that decide WHICH reels a press lands, and WHEN a reel may stop at all.
Land some reels, not all#
slamStop({ except: [3, 4] }) lands reels 0-2 now and leaves 3 and 4 to stop naturally.
Gotcha: a partial slam does not end the round - the button stays live, and only a full slam moves skipStage to 2 and applies the speed boost.
Loading recipe…
Name the reels that land#
The complement: name the reels to land instead of the ones to spare. Here a three-beat plan walks the board outside in.
Gotcha: neither form needs guarding - out-of-range indices are ignored, and held or landed reels are never slammed.
Loading recipe…
A tease a press cannot hide#
The same partial slam as a rule the engine applies for you. protect: 'once': press 1 lands every non-tease reel and leaves the tease running, so the trigger symbols are on screen; press 2 ends it.
Gotcha: call setAnticipation BEFORE setResult. A press in the pre-result window is queued by requestSkip() and fires the moment the result arrives, so the tease must already be registered.
Loading recipe…
Walk the tease forward, one reel per press#
The same rule per reel. Press 1 lands everything outside the tease; each press after releases the next tease reel. The player sets the pace.
Gotcha: release order is TEASE order, not reel index - setAnticipation([4, 2, 3], ...) releases 4 first.
Loading recipe…
Groups: reels that stop and skip together#
Reel index is the engine’s only ordering, and it cannot say “reels 1-2 land together, 3-4 tease one press each, reel 5 outlasts both”. Reel 5 is index 4, so its stop delay comes due mid-tease.
reelSet.setReelGroups([[0, 1], [2, 3], [4]]);
reelSet.setAnticipation([2, 3], { stagger: 'sequential', protect: 'stepwise' });
// Presses walk the board: [1,2] -> 3 -> 4 -> [5].
A group is a barrier both ways. Stopping: no reel starts its stop sequence, anticipation included, until every reel in the earlier groups has landed - and it keeps spinning at full speed while it waits. Skipping: a press releases the next un-landed group, protection still applying inside it. Stop delays become group-relative; an explicit setStopDelays() still wins.
Gotcha: set it any time up to setResult() - the barrier is read as each reel’s SpinPhase resolves - so a round can be grouped from its own server response. Every reel must appear exactly once, and changing the layout once reels have begun landing throws. null clears.
Loading recipe…
For any other release order, use protect: 'always' and drive it yourself with slamStop({ reels }) from a queue of your own.
Gotcha: two near-misses, both pinned by tests. Shrinking the set with setAnticipation() mid-spin does NOT release a teasing reel - the set is read once, as a reel leaves SPIN. And setSpeed() mid-spin does not re-time the round, which is why the skipSpin() boost lands on the NEXT spin.
A tease no press can end#
Press 1 lands everything outside the tease, like 'once'; every press after is a deliberate no-op. For when the tease IS the reward beat - a jackpot reveal, a final-reel bonus land.
Gotcha: a refused press emits no events rather than throwing, and slamStop() still lands everything. The engine refuses the player’s press, never your own code’s.
Loading recipe…
Your own release plan#
'always' plus your own queue is the general form: the engine guarantees no press ends a tease behind your back, your code decides the groups. Three teasing reels, two beats.
Loading recipe…
A press that lands, not cuts#
Every slam places the frame and lands in the same tick, so a press reads as a cut. requestSkip({ mode: 'quicken' }) frees the same reels a slam would - protection, 'stepwise' and groups all apply - and asks each for its landing instead: the tease ends, the stop delay is cut, the reel spins its frame in and bounces. speed: 'turbo' lands the quickened reels on the turbo spin-out and bounce; builder.skipMode('quicken') makes the mode the default.
Gotcha: a quicken is a skip. skipStage advances and wasSkipped is true, and SpinResult.skipMode says which mode ended the round. A mode: 'slam' press after it still cuts whatever is moving. Tap the readout to see the cut on the same spin.
Loading recipe…
Quicken with groups#
The same barrier, a different mode. A quicken walks exactly the groups a slam would and quickens each one instead of placing it: a freed group goes violet (the stop, spinning in), not straight to grey. The cards are PhaseCardSymbol, so the phases are the colours.
Gotcha: a quickened reel counts as released for the WALK, so a fast next press is taken early; the reel behind the barrier still waits for the group before it to land. Reel 5 stays blue until reel 4 is down, however fast you press.
Loading recipe…
Quicken first, slam second#
One button, two presses: requestSkip({ mode: 'quicken', speed: 'turbo' }) on the first, requestSkip({ mode: 'slam' }) on the second. A quicken press walks past the reels it already quickened; a slam press cuts them. Both are skips, and SpinResult.skipMode names the one that ended the round.
Each press carries a payload ({ press: 1 }, { press: 2 }) and the HUD reads it back off skip:requested, with the profile a quicken named as speed.name: the event carries the whole press, the same context every phase’s onSkip(ctx) gets, and SpinResult.skipContext keeps the last one.
Gotcha: read “has this round been quickened” off skip:requested and its mode, not off a local counter - a press that freed nothing must not arm the slam.
Loading recipe…
A phase that can be quickened#
onSkip(ctx) is where a custom phase hears a press, and ctx.mode is where it decides what to cut. This instant stop holds its frame a tenth of a cell short for 900 ms before sliding in; under 'quicken' the hook kills the hold and slides at once, under 'slam' it rests on the frame. The class declares quickenable = true to say its hook branches on the mode; without the flag a quicken leaves the phase alone and the reel still lands, the press just shortens nothing.
Gotcha: the quicken branch cuts a wait, never the landing itself - keep land() and bounce() exactly where the natural path has them, or a quickened reel lands differently from a patient one.
Loading recipe…
Pressing before the server answers#
skipSpin() throws before setResult() - there is nothing to land on. requestSkip() queues the intent and fires it when the result lands, protection intact. The demo’s result is deliberately slow.
Gotcha: this is why setAnticipation comes BEFORE setResult - a queued press can only protect a tease the engine already knows about.
Loading recipe…
Driving the button from skipStage#
0 no press yet, 1 a press landed around a protected tease and left it running, 2 the round-ending press. Stage 1 needs protection, so an unprotected game still sees a two-state button. Read off reelSet.skipStage every frame, not counted locally.
Loading recipe…
No hold, no tease, nothing to protect#
Turbo profiles set anticipationDelay: 0, and at a 0 ms hold protect does nothing - protecting a tease that never plays would stall the reels and put the response-time tell back. Spins alternate on one turbo profile; only duration differs.
Loading recipe…
Skip without a tell#
Why you protect the tease rather than slow the skip down. Spins alternate scatterless and teasing, both auto-press at the same moment; the panel times the last non-tease reel. The numbers stay equal - what differs happens after the press, on screen.
Raising minimumSpinTime instead gets the pacing right and leaks the outcome through response time: the player learns a feature is coming from how the button felt.
Loading recipe…
Per-reel spin floor#
No slam here. minimumSpinTime is one value on the speed profile, shared by every reel; setMinimumSpinTime([0, 0, 0, 1000, 1000]) overrides it per reel, so reels 0-2 take the result on arrival while 3 and 4 hold a full second.
Gotcha: sticky across spin() and refill() until cleared with null, like setStopDelays(). Set it at boot, not per spin.
Gotcha: a floor governs NATURAL landings only. A slam ignores it, so a mid-spin tap lands all five together and the labels read -> slam.
Loading recipe…
Uniform floor, and clearing it#
A single number raises the floor on every reel; null hands it back to the speed profile, which is not the same as a floor of 0. Spins alternate so the difference reads back to back.
Loading recipe…
Cascades#
Anticipation runs before the tumble/standard split, so a cascade spin teases and protects like a strip spin. Three things differ, all about what a press carries:
| Behaviour | |
|---|---|
| Initial spin | protect applies unchanged. Press 1 lands the reels outside the tease, press 2 ends it. |
| Round side effect | In cascade mode this is auto-slam-refills, not a speed boost. A protected press does NOT spend it; the press that ends the tease does, and every refill after that lands instantly. |
| Refills | Carry no tease. refill() clears the anticipation set on entry, so a press inside one is an ordinary full slam - though slamStop({ reels }) still works per reel there. |
Gotcha: a partial slam does not abort the chain, but it does mark the whole result wasSkipped: true.
Loading recipe…
Hold & Win#
Nothing to demo, and that is the point: a Hold & Win board is one single-reel ReelSet per cell, so slamStop({ reels }) can only ever name reel 0. The granularity you want is per cell - BoardGrid.skipSpinning() / HoldAndWinBoard.skip().
Protection on a cell’s set is consistent but rarely useful, since “every reel outside the tease” is empty:
'once'- press 1 spends the protection and lands nothing, press 2 lands the cell. Working as intended.'stepwise'- the release group IS the tease reel, so one press lands it.'always'- do not use it here. No press can ever land the cell, andBoardGrid.skipSpinning()’s recovery path needs it to land or the nextrespin()throws.
Subclass a built-in phase#
Every built-in phase is exported, so a small change is a subclass. StopPhase gains a landing backlight; sequencer, bounce and skip pose are inherited. Register under the same key to replace the built-in.
Gotcha: call super from every hook you override - a reel whose onEnter never reaches it spins forever. Override onSkip too if the beat should fire on a slammed reel.
Loading recipe…
A spin floor the phase decides for itself#
Subclass SpinPhase and a per-reel floor is one line. setMinimumSpinTime([...]) is the ready-made version; subclass when the floor is derived rather than configured.
Gotcha: merge the config, don’t replace it - super.onEnter({ ...config, minimumSpinTime }).
Gotcha: the staircase is natural landings only. A slam ignores the floor, so a mid-spin tap flattens it and the labels read -> slam.
Loading recipe…
Using update() from a subclass#
A per-reel countdown drawn from inside the tease: onEnter shows the label then calls super, update(deltaMs) runs every frame, onSkip clears it when a press force-completes the phase.
Gotcha: update accumulates ticker time, not wall clock, so it stays correct in a backgrounded tab. The active SpeedProfile is already on this._speed.
Loading recipe…
A phase written from scratch#
Extend ReelPhase for the whole behaviour rather than a tweak - here a flat linear ramp with no step-back pull, replacing StartPhase.
Gotcha: a custom name buys nothing. The controller only asks for lifecycle keys, so register('myPhase', ...) is orphaned - take the key instead. And call this._complete() exactly once, or the spin promise never settles.
Loading recipe…
Phases that take constructor arguments#
The cascade phases and AdjustPhase take build-time config as extra constructor arguments, so a subclass needs registerFactory and must forward them. resolveTumbleConfig(config) builds the shape they expect.
Gotcha: .phases(...) sits before .tumble(...) here and still wins - configurators are deferred to build() and applied after the built-in registrations.
Loading recipe…
See Phases for the full extension contract.