Each reel moves through four phases, independently. The diagram below
shows the transitions and what triggers each one. Anticipation is
optional — a reel skips it unless setAnticipation([i]) put
its index on the list. skip() can short-circuit any phase
into an immediate landing.
What each phase actually does
A brief downward "step-back" (pulling the handle), then GSAP tweens reel.speed from 0 to spinSpeed with accelerationEase. Each reel's start is delayed by reelIndex × spinDelay, producing the classic staggered kickoff.
Reel holds at full speed indefinitely. Symbols wrap vertically via ReelMotion; as each symbol wraps, its identity is swapped for a new one from the RandomSymbolProvider. Waits for setResult() and spin:allStarted before any stop can begin.
Reel holds at a reduced speed for anticipationDelay ms. Entered only if setAnticipation() included this reel. Fires spin:stopping on entry, not phase:enter "stop" yet.
GSAP timeline decelerates to 0, calls reel.placeSymbols(visible) to lock in the target, then overshoots by bounceDistance and snaps back. Fires landed when complete.
The generation counter
Internally SpinController increments _spinGeneration
on every spin() and skip(). Every async chain
checks the generation before continuing — an in-flight phase from a
previous spin quietly no-ops if the player spun again or skipped. That's
how you can hammer the spin button without state corruption.
What skip() does exactly
- Emits
skip:requested. - Force-completes every active phase's GSAP timeline.
- Bumps
_spinGenerationso pending async chains exit. - If
setResult()was already called: places the target symbols directly. Otherwise snaps current symbols to grid. - Emits
spin:reelLanded+spin:allLanded+spin:completewithwasSkipped: true. - Emits
skip:completed.
The spinAndLand test helper uses this exact path. That's why
it's synchronous — no ticker, no tweens, just direct symbol placement
and event emission.