ReelSet
pixi-reels / index / ReelSet
Class: ReelSet
Defined in: core/ReelSet.ts:486
The whole slot board as one object.
A ReelSet is a PixiJS Container that owns every reel, the spin
controller, the speed manager, and the win spotlight. You addChild it
to your stage and then drive it from the four public verbs below:
spin(). start the reels moving, returns a promise that resolves when every reel has landed (or been slam-stopped)setResult(grid). tell the reels what to land on; the spin controller consumes this and each reel queues its target symbolssetAnticipation(reelIndices). slow the given reels before they stop, for “will the third scatter land?” tensionskipSpin()lands the in-flight spin immediately. The slam-stop button calls this.
Everything else is subsystems: speed, spotlight, events, viewport.
Construction goes through ReelSetBuilder, never new ReelSet()
directly. the builder enforces that every required piece is wired.
const reelSet = new ReelSetBuilder()
.reels(5).visibleCells(3).symbolSize(140, 140)
.symbols((r) => r.register('cherry', SpriteSymbol, { textures }))
.ticker(app.ticker)
.build();
app.stage.addChild(reelSet);
const spin = reelSet.spin();
reelSet.setResult(await server.spin());
await spin;
Teardown cascades: one reelSet.destroy() disposes every child.
Extends#
Container
Implements#
Constructors#
Constructor#
new ReelSet(params: ReelSetParams): ReelSet;
Defined in: core/ReelSet.ts:568
Horizontal symbol gap (px). Used by getBlockBounds for big symbols.
Parameters#
| Parameter | Type |
|---|---|
params | ReelSetParams |
Returns#
ReelSet
Overrides#
Container.constructor
Accessors#
events#
Get Signature#
get events(): EventEmitter<ReelSetEvents>;
Defined in: core/ReelSet.ts:648
The event emitter for reel-specific events.
Returns
frame#
Get Signature#
get frame(): FrameAPI;
Defined in: core/ReelSet.ts:2643
Runtime-mutable middleware pipeline for symbol-frame generation.
Example
// Feature entry. swap to a middleware that injects more wilds
reelSet.frame.use(moreWildsMiddleware);
// Feature exit
reelSet.frame.remove('more-wilds');
Returns
isDestroyed#
Get Signature#
get isDestroyed(): boolean;
Defined in: core/ReelSet.ts:2649
Returns
boolean
Implementation of#
isMultiWaysSlot#
Get Signature#
get isMultiWaysSlot(): boolean;
Defined in: core/ReelSet.ts:1827
Whether this slot was built with .multiways(...).
Returns
boolean
isSpinning#
Get Signature#
get isSpinning(): boolean;
Defined in: core/ReelSet.ts:1822
Returns
boolean
pins#
Get Signature#
get pins(): ReadonlyMap<string, CellPin>;
Defined in: core/ReelSet.ts:2433
All active pins, keyed by "reel:cell".
Reads are safe at any time. during a spin the map reflects pins that
will apply to the NEXT setResult(), not the one already in flight.
Returns
ReadonlyMap<string, CellPin>
promotedViews#
Get Signature#
get promotedViews(): readonly Container<ContainerChild>[];
Defined in: core/ReelSet.ts:2168
Symbol views currently raised by promote. Debug / assertion surface.
Returns
readonly Container<ContainerChild>[]
randomSymbols#
Get Signature#
get randomSymbols(): RandomSymbolControl;
Defined in: core/ReelSet.ts:2215
Control over what the engine may draw when it fills a cell the game did not name: the strip streaming past during a spin, and the buffer cells parked either side of the visible window.
reelSet.randomSymbols.set({ exclude: ['EMPTY'] });
reelSet.randomSymbols.set({ weights: { WILD: 40 } }, { reel: 2 });
reelSet.randomSymbols.set({ exclude: ['COIN'] }, { slots: 'buffer' });
Build-time equivalent: builder.randomSymbols(pool, scope).
Returns
reelGroups#
Get Signature#
get reelGroups(): number[][] | null;
Defined in: core/ReelSet.ts:1403
The reel groups in force, or null. A copy; mutating it changes nothing.
Returns
number[][] | null
reels#
Get Signature#
get reels(): readonly Reel[];
Defined in: core/ReelSet.ts:2222
Get all reels.
Returns
readonly Reel[]
skipStage#
Get Signature#
get skipStage(): 0 | 1 | 2;
Defined in: core/ReelSet.ts:1545
Current skipSpin() position within the active round. 0 until the
player presses the slam button, 2 after. Read this to drive button
labels (e.g. “Skip” to “Skipped”). 1 means a press landed the reels
around a protected tease and left the tease running — the button should
stay live, because the next press is the one that ends it.
requestSkip() that gets queued pre-setResult() does NOT advance
the stage until the queued slam actually fires (i.e. once
setResult() arrives). If you need a “queued” UI state, track that
yourself alongside skipStage.
The stage is ROUND-scoped and only resets on the next spin(), which is
what lets it survive the refill() calls of a cascade round. So gate a
button on isSpinning before reading it: a protect: 'always' round
never reaches 2 (no press can end the tease), and therefore ends parked
at 1 for the whole idle window.
Returns
0 | 1 | 2
speed#
Get Signature#
get speed(): SpeedManager;
Defined in: core/ReelSet.ts:2064
Speed profile manager.
Returns
spotlight#
Get Signature#
get spotlight(): SymbolSpotlight;
Defined in: core/ReelSet.ts:2081
Returns
viewport#
Get Signature#
get viewport(): ReelViewport;
Defined in: core/ReelSet.ts:2341
Get the viewport.
Returns
Methods#
destroy()#
destroy(): void;
Defined in: core/ReelSet.ts:2653
Removes all internal references and listeners as well as removes children from the display list.
Do not use a Container after calling destroy.
Returns#
void
Example#
container.destroy();
container.destroy(true);
container.destroy({ children: true });
container.destroy({ children: true, texture: true, textureSource: true });
Implementation of#
Overrides#
Container.destroy
destroySymbols()#
destroySymbols(cells: readonly Cell[], opts?: DestroySymbolsOptions): Promise<void>;
Defined in: core/ReelSet.ts:968
Parameters#
| Parameter | Type |
|---|---|
cells | readonly Cell[] |
opts? | DestroySymbolsOptions |
Returns#
Promise<void>
getBlockBounds()#
getBlockBounds(reel: number, cell: number): CellBounds;
Defined in: core/ReelSet.ts:2015
Pixel rectangle covering a big symbol’s whole N×M block, in
ReelSet-local coordinates. Returns the anchor cell’s bounds for 1×1
symbols. Pass any cell of a block. anchor or non-anchor. and you
get the same rect.
Useful for win presenters drawing an outline around a whole bonus, or any overlay aligned to the visible footprint of a big symbol:
const rect = reelSet.getBlockBounds(2, 1);
gfx.rect(rect.x, rect.y, rect.width, rect.height)
.stroke({ color: 0xff6b35, width: 4 });
reelSet.addChild(gfx);
For 1×1 cells this is equivalent to getCellBounds(reel, cell). For
big-symbol cells it multiplies width/height by the block size and
starts from the anchor cell’s bounds.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
Returns#
getCellBounds()#
getCellBounds(reel: number, cell: number): CellBounds;
Defined in: core/ReelSet.ts:2250
Returns the bounding box of a visible grid cell in ReelSet-local coordinates (i.e. relative to this Container, before any parent transforms). Row 0 is the top visible cell.
Use this to place payline graphics, hit areas, or debug overlays that must align with a specific symbol cell:
const b = reelSet.getCellBounds(2, 1);
gfx.rect(b.x, b.y, b.width, b.height).stroke({ color: 0xff6b35 });
reelSet.addChild(gfx);
To convert to stage / global coordinates use PixiJS:
const global = reelSet.toGlobal({ x: b.x, y: b.y });
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
Returns#
getCellQuad()#
getCellQuad(reel: number, cell: number):
| {
x: number;
y: number;
}[]
| null;
Defined in: core/ReelSet.ts:2313
The four corners of a visible cell as the drum actually draws it, in
ReelSet-local pixels, clockwise from top-left. null when the reel is
flat, in which case ReelSet.getCellBounds already describes it
exactly.
getCellBounds has to return a rectangle, so on a curved reel it widens
to the trapezoid’s bounding box. Use this instead to draw anything that
should sit ON the curve rather than around it - a cell outline, a payline
that follows the bend, a debug overlay.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
Returns#
| {
x: number;
y: number;
}[]
| null
Example#
const q = reelSet.getCellQuad(2, 0);
if (q) gfx.poly(q).stroke({ color: 0xff6b35 });
getPin()#
getPin(reel: number, cell: number): CellPin | undefined;
Defined in: core/ReelSet.ts:2438
Convenience: get the pin at (reel, cell) or undefined.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
Returns#
CellPin | undefined
getReel()#
getReel(index: number): Reel;
Defined in: core/ReelSet.ts:2227
Get a reel by index.
Parameters#
| Parameter | Type |
|---|---|
index | number |
Returns#
getSymbolFootprint()#
getSymbolFootprint(reel: number, cell: number): {
anchor: {
cell: number;
reel: number;
};
size: {
cells: number;
reels: number;
};
};
Defined in: core/ReelSet.ts:1950
Footprint of the symbol at (reel, cell).
- 1×1 symbols:
{ anchor: { reel, cell }, size: { reels: 1, cells: 1 } }. - Big symbols: returns the anchor cell and block size.
- OCCUPIED cells: resolves transparently to the anchor.
Useful for win presenters that need to highlight a whole NxM block.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
Returns#
{
anchor: {
cell: number;
reel: number;
};
size: {
cells: number;
reels: number;
};
}
| Name | Type | Defined in |
|---|---|---|
anchor | { cell: number; reel: number; } | core/ReelSet.ts:1953 |
anchor.cell | number | core/ReelSet.ts:1953 |
anchor.reel | number | core/ReelSet.ts:1953 |
size | { cells: number; reels: number; } | core/ReelSet.ts:1953 |
size.cells | number | core/ReelSet.ts:1953 |
size.reels | number | core/ReelSet.ts:1953 |
getTargets()#
getTargets(): ColumnTarget[];
Defined in: core/ReelSet.ts:1937
The whole board as ColumnTarget[] — buffers included, big-symbol
anchors at their true positions, so it can be handed straight back:
reelSet.setResult(reelSet.getTargets()) reproduces what is on screen.
getVisibleGrid() cannot do that, and its string[][] type says so. It
reports the visible window only, so a block anchored in bufferStart
with just its tail showing reads as that id at visible cell 0; replaying
that re-anchors the block there and it expands over the cells below.
Use getVisibleGrid() to read the board for win logic, and this to
capture and replay one.
Returns#
getVisibleGrid()#
getVisibleGrid(): string[][];
Defined in: core/ReelSet.ts:1921
Resolved grid, with all OCCUPIED cells (same-reel and cross-reel)
replaced by their anchor’s symbol id. A 2×2 bonus reads as four
'bonus' cells.
Equivalent to reelSet.reels.map(r => r.getVisibleSymbols()) because
each reel has a cross-reel resolver wired in by ReelSet’s constructor.
the per-reel surface and the grid surface are the same.
Returns#
string[][]
movePin()#
movePin(
from: CellCoord,
to: CellCoord,
opts?: MovePinOptions
): Promise<void>;
Defined in: core/ReelSet.ts:2472
Move an existing pin from one cell to another. Animates a flight symbol between the two cells, updates pin state atomically, and resolves when the animation completes.
This is the engine-native replacement for ghost sprites in walking-wild
recipes. The flight symbol is a pooled ReelSymbol acquired from the
factory, parented briefly to the viewport’s unmaskedContainer so it
can travel across reel boundaries without being clipped.
Constraints:
- Only callable at rest (throws if
isSpinning === true). tomust be within the grid; no pin may already exist there.- Calling with
from === tois a no-op that still firespin:moved.
Parameters#
| Parameter | Type |
|---|---|
from | CellCoord |
to | CellCoord |
opts? | MovePinOptions |
Returns#
Promise<void>
Example#
// Walking wild. move the pinned wild one column left each spin
reelSet.events.on('spin:complete', async () => {
for (const pin of [...reelSet.pins.values()]) {
if (pin.reel > 0) {
await reelSet.movePin(
{ reel: pin.reel, cell: pin.cell },
{ reel: pin.reel - 1, cell: pin.cell },
);
} else {
reelSet.unpin(pin.reel, pin.cell);
}
}
});
nudge()#
nudge(reel: number, options: NudgeOptions): Promise<{
symbols: string[];
}>;
Defined in: core/ReelSet.ts:1633
Shift a single reel by distance positions after it has landed, revealing
caller-supplied symbols.
Per-reel by design. multi-reel sync is via Promise.all([...]) of
independent calls. Each call emits its own nudge:start / nudge:complete
pair on the ReelSet bus and phase:enter('nudge') / phase:exit('nudge')
on the per-reel bus.
Big-symbol blocks on the target reel are nudged through as a unit as long as they fit on the strip post-rotation. Use case: a 1xH block lands with stubs in bufferEnd; nudge up to reveal it fully.
nudge:start fires AFTER pre-placement so listeners observe the
about-to-tween state, mirroring nudge:complete which fires after
the strip has snapped. To capture the pre-nudge state, snapshot the
grid in your call site before awaiting.
Throws (synchronously) if:
- the reel set is currently spinning (avoid races with the spin pipeline),
reelis out of range,- any visible cell on the target reel has an active pin,
Reel.nudgeitself rejects (bad distance / direction / incoming / incompatible big-symbol layout).
While nudge() is in flight, calling spin(), setResult(), pin(),
or setShape() throws. Await the returned promise before calling any
of those methods.
Rejects with an AbortError if options.signal aborts or the reel
is destroyed mid-tween. nudge:cancelled fires on the bus in that case.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
options | NudgeOptions |
Returns#
Promise<{
symbols: string[];
}>
Examples#
await reelSet.spin(); // landed
await reelSet.nudge(2, { distance: 1, direction: 'forward', incoming: ['wild'] });
Parallel nudges across two reels:
await Promise.all([
reelSet.nudge(2, { distance: 1, direction: 'forward', incoming: ['wild'] }),
reelSet.nudge(3, { distance: 1, direction: 'forward', incoming: ['wild'] }),
]);
Staggered parallel via `startDelay`:
await Promise.all(
[1, 2, 3].map((reel, i) =>
reelSet.nudge(reel, { ...opts, startDelay: i * 80 }),
),
);
Abortable nudge:
const controller = new AbortController();
skipButton.onclick = () => controller.abort();
await reelSet.nudge(2, { ...opts, signal: controller.signal })
.catch((e) => { if (e.name !== 'AbortError') throw e; });
pin()#
pin(
reel: number,
cell: number,
symbolId: string,
options?: CellPinOptions
): CellPin;
Defined in: core/ReelSet.ts:2372
Pin a symbol to a grid cell. Applied immediately if the reel is idle;
applied at the next setResult() otherwise. Fires pin:placed.
Passing the same (reel, cell) replaces the previous pin. The old one
is replaced silently (no pin:expired fires for replacement).
Negative cells are rejected. Place buffer-cell anchors via setResult()
with bufferStart / bufferEnd on the column’s ColumnTarget.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
symbolId | string |
options? | CellPinOptions |
Returns#
Example#
// Sticky wild for 3 spins
reelSet.pin(2, 1, 'wild', { turns: 3 })
// Hold & Win coin with a payout value
reelSet.pin(reel, cell, 'coin', { turns: 'permanent', payload: { value: 50 } })
// Expanding wild: fill column for the current spin's evaluation only
for (let r = 0; r < 3; r++) reelSet.pin(2, r, 'wild', { turns: 'eval' })
promote()#
promote(positions: SymbolPosition[]): () => void;
Defined in: core/ReelSet.ts:2114
Draw the symbols at these positions above every other symbol in the set,
and above the mask, until the returned function is called. The bare
promotion the spotlight does as one step of a bigger presentation: no
dim, no playWin(), nothing to await.
Unlike spotlight.show() this does not reparent. the views are attached
to viewport.promotedLayer, which changes render order only, so their
transforms are untouched and there is no stale-parent hazard when the
symbol pool recycles an instance.
The promotion covers the symbol INSTANCES currently at those positions.
A swap under a promoted symbol - a spin, a cascade, a setResult - hands
the cell a different instance, and that symbol’s promotion ends with it:
the raised view goes back to the pool, so leaving it attached would raise
whatever cell the pool hands it to next. Promote at rest, or re-promote
after the swap. The release stays safe to call either way, and calling it
twice is a no-op.
For the board-level equivalent - promote a whole cell, surviving swaps -
see BoardGrid.lift.
Parameters#
| Parameter | Type |
|---|---|
positions | SymbolPosition[] |
Returns#
() => void
Example#
const drop = reelSet.promote([{ reelIndex: 2, cellIndex: 1 }]);
await reelSet.getReel(2).getSymbolAt(1).playWin();
drop();
refill()#
refill(opts: RefillOptions): Promise<RefillResult>;
Defined in: core/ReelSet.ts:760
Tumble cascade: cascade refill (Moment B). Call this AFTER you’ve faded out the winning symbols in your own code, with the list of winner cells and the next grid the server returned.
- Untouched survivors don’t animate.
- Survivors behind a hole slide toward the gravity-exit edge to fill it.
- New symbols enter from the gravity-entry edge into the
winners.lengthcells left at that end.
The new grid must follow the gravity convention: per reel, the
winnerCells.length cells nearest the gravity-ENTRY edge are the new
symbols and the rest are survivors in their original order. On the
default vertical/forward reel that is the familiar “top N are new”;
on a reverse reel it is the last N. This matches what server-side
gravity simulations emit.
Resolves with a RefillResult (mirror of RunCascadeResult.
one stage’s worth). Requires the builder to have been configured with
.tumble(...).
For the common destroy → refill → check → repeat loop, prefer ReelSet.runCascade. it composes refill, destroySymbols, and win-detection with the same cancellation semantics.
Parameters#
| Parameter | Type |
|---|---|
opts | RefillOptions |
Returns#
Promise<RefillResult>
Examples#
const winners = detectWins(currentGrid);
await reelSet.destroySymbols(winners);
const next = await server.cascade(winners);
const result = await reelSet.refill({ winners, grid: next });
console.log(result.finalGrid, result.wasSkipped);
// Abort mid-refill: slams the in-flight animation, resolves with wasSkipped.
const ac = new AbortController();
skipButton.onclick = () => ac.abort();
const result = await reelSet.refill({
winners, grid: next, signal: ac.signal,
});
refreshPinOverlaysForReel()#
refreshPinOverlaysForReel(reelIndex: number): void;
Defined in: core/ReelSet.ts:2939
Reposition + resize every pin overlay on the given reel.
The engine calls this automatically after every MultiWays AdjustPhase
reshape (and from the skip path), so applications that just use
setShape() / setResult() never need to invoke it. Call it
yourself only if you mutate Reel.symbolWidth, Reel.symbolHeight,
or a pin’s cell outside the normal MultiWays flow. e.g. a custom
mid-spin layout swap that bypasses AdjustPhase.
No-op for reels with no active pin overlays.
Parameters#
| Parameter | Type |
|---|---|
reelIndex | number |
Returns#
void
requestSkip()#
requestSkip(options?: SkipOptions): void;
Defined in: core/ReelSet.ts:1476
A skip press safe before setResult() arrives: queues until then, and
fires the moment the result lands. Bypasses the two-stage skipSpin()
machine: no speed boost, no cascade auto-slam.
options.mode is what the press does to the reels it frees. 'slam'
places them on the result now; 'quicken' asks each for its landing
sooner without changing what the landing looks like: a tease ends, a stop
delay is cut, the spin-out and bounce still play, and speed names a
registered profile to finish on (the turbo bounce for a pressed reel, say).
Which reels a press frees (tease protection, 'stepwise', reel groups) is
the same either way; a quicken press treats the reels it already
quickened as down and walks on to the next group, a slam press cuts them.
Omit mode for the builder’s skipMode(), 'slam' unless set. In
cascade mode a quicken slams and warns once with code quicken-cascade:
a tumble reel lands by placing, there is nothing to spin out.
Either mode is a skip: skipStage advances, wasSkipped is true,
SpinResult.skipMode says which. payload reaches every phase the press
touches as ctx.payload.
Note on skipStage: when this call queues a press (pre-setResult)
rather than firing one, skipStage stays at 0 until setResult()
arrives and the queued press actually runs. If your UI labels the
button off skipStage, expect a beat of “Skip” still shown while
the queued intent is in flight; the queued state is not exposed as
its own stage on purpose (kept the 0 | 1 | 2 shape stable).
Parameters#
| Parameter | Type |
|---|---|
options? | SkipOptions |
Returns#
void
Example#
reelSet.requestSkip({ mode: 'quicken', speed: 'turbo' });
runCascade()#
runCascade(opts: RunCascadeOptions): Promise<RunCascadeResult>;
Defined in: core/ReelSet.ts:1096
Run the canonical cascade chain on top of refill(). Loops:
detect winners → destroy → pause → refill → emit. until
detectWinners returns an empty list (or maxChain is hit, or the
player slammed via skipSpin() / abort). Resolves with the final grid
and a summary.
The orchestration is library-owned; the game rules (what counts
as a winner, how the next grid is computed) stay in your callbacks.
This is the cascade equivalent of spin() + setResult(). three
lines instead of fifteen, and the slam path is handled for you.
Typical usage:
await reelSet.spin();
reelSet.setResult(await server.spin());
const summary = await reelSet.runCascade({
detectWinners: (grid) => detectClusters(grid),
nextGrid: async (grid, winners) => server.cascade(winners),
onCascade: ({ chain, winners }) => bumpMultiplier(chain),
});
console.log(summary.chainLength, summary.totalWinners);
Composes with everything else in the library:
setDropOrder(...)is honoured on every refill in the chain. set it beforerunCascadeand the same order applies to every drop.cascade:fall:symbol,cascade:place:end,cascade:dropIn:symbolfire on each refill.reelSet.skipSpin()ends the chain immediately; the returned summary reportswasSkipped: true.
Event order per stage with winners: cascade:chain:start →
cascade:destroy:start → (destroy tweens) → cascade:destroy:end →
onCascade → pause → refill (cascade:place:end +
cascade:dropIn:* per reel) → cascade:chain:end. The chain itself
is delimited by the returned Promise. await the call to know
when it’s done.
Requires .tumble(...) on the builder (same as refill()).
Parameters#
| Parameter | Type |
|---|---|
opts | RunCascadeOptions |
Returns#
Promise<RunCascadeResult>
setAnticipation()#
setAnticipation(reelIndices: number[], options?:
| AnticipationStagger
| AnticipationOptions): void;
Defined in: core/ReelSet.ts:1311
Set which reels should show anticipation before stopping, and how their
slow-downs are spaced via stagger:
0(default): every anticipation reel begins slowing at once (the historical parallel behaviour).number: reel at tease-orderkstarts its slow-downk * staggerms after the first, so the tease sweeps across the reels.number[]: explicit per-tease-order offset in ms.'sequential': each reel waits until the previous anticipation reel has fully landed before it starts. maximal one-at-a-time tension.
Offsets are by tease-order (position in reelIndices), not raw reel
index. Reset at the start of every spin().
Pass a { stagger, slowdown, duration } object to shape the tease more:
slowdowninterpolates across the tease sequence so each successive reel slows to a lower speed (from→to) and/or holds longer (holdFrom→holdTo) — the escalating “each reel crawls slower than the last” build-up. See AnticipationSlowdown.duration(ms) overrides the profile’santicipationDelay, so the tease plays even in Turbo / SuperTurbo (whose profiles useanticipationDelay: 0and would otherwise skip anticipation).protectkeeps a skip press from ending the tease before the player has seen it.'once'lands every non-tease reel on the first press and leaves the tease running (a second press ends it);'always'never lets a press end a tease. See AnticipationProtect.curvereplaces the built-in decelerate-then-hold with an explicit list of speed legs, so a tease can surge before it crawls and its transitions ramp instead of stepping. See AnticipationCurve. Mutually exclusive withslowdown, which is sugar for a two-leg curve.cellsends the tease after that many symbol pitches of travel rather than after a fixed time — cut the tease to symbols going past the window instead of to a clock.
Listen to anticipation:reel ({ reelIndex, order, total }) to drive
per-step tension SFX / a pitch ramp, and anticipation:reelEnd to stop it.
For a pitch ramp that tracks the actual slow-down rather than just its
start and end, sample reelSet.reels[i].speedNormalized from your ticker.
Parameters#
| Parameter | Type | Default value |
|---|---|---|
reelIndices | number[] | undefined |
options | | AnticipationStagger | AnticipationOptions | 0 |
Returns#
void
Example#
// Classic "2 scatters showing" sweep across the last three reels:
reelSet.setResult(grid);
reelSet.setAnticipation([2, 3, 4], 450); // 450ms apart
reelSet.setAnticipation([2, 3, 4], 'sequential'); // strict one-by-one
// Keep the tease alive in turbo (profile anticipationDelay is 0):
reelSet.setAnticipation([2, 3, 4], { duration: 350, stagger: 200 });
// Escalating slow-down: later reels crawl slower and hold longer.
reelSet.setAnticipation([2, 3, 4], {
stagger: 400,
slowdown: { from: 0.45, to: 0.12, holdTo: 2 },
});
// Drive which reels tease straight from the result grid:
const reels = anticipationForScatters(grid, { symbol: 'SCAT', trigger: 2 });
reelSet.setAnticipation(reels, { stagger: 'sequential', slowdown: { from: 0.4, to: 0.1 } });
// Skip must not hide the tease: the first press lands reels 0-1 instantly
// so the two scatters are on screen, and reels 2-4 keep teasing. The next
// press ends the tease.
reelSet.setAnticipation([2, 3, 4], { stagger: 400, protect: 'once' });
// Surge, then crawl. The reel speeds UP before it slows, and both
// transitions ramp rather than stepping.
reelSet.setAnticipation([2, 3, 4], {
stagger: 'sequential',
curve: [
{ speed: 1.8, duration: 220, ease: 'power2.in' },
{ speed: 0.12, duration: 700, ease: 'power3.inOut', hold: 400 },
],
});
// Tease for exactly four symbols of travel, however long that takes.
reelSet.setAnticipation([4], { curve: [{ speed: 0.25, duration: 300 }], cells: 4 });
setCurve()#
setCurve(curve:
| ReelCurveInput
| ReelCurveInput[]): void;
Defined in: core/ReelSet.ts:2189
Re-curve the whole set at runtime, the same way builder.curve(...) does
at build time. Takes effect immediately on reels at rest and on the next
frame for reels in motion.
Mostly a tuning affordance: dial the curvature live against the real art
instead of rebuilding the set on every guess. Pass 0 to flatten.
Parameters#
| Parameter | Type | Description |
|---|---|---|
curve | | ReelCurveInput | ReelCurveInput[] | one value for every reel, or one entry per reel (length must equal the reel count). |
Returns#
void
Example#
reelSet.setCurve(0.4);
reelSet.setCurve([0.2, 0.35, 0.5, 0.35, 0.2]);
setDropOrder()#
setDropOrder(order: number[] | "all" | "ltr" | "rtl" | null, stepMs?: number): void;
Defined in: core/ReelSet.ts:1797
Set the per-reel drop order for the next stop / refill sequence.
Convenience wrapper over setStopDelays() for common patterns. The
stagger step defaults to the active speed profile’s stopDelay (or
150 ms if stopDelay is 0).
Sticky. The override persists indefinitely. until another
setDropOrder() / setStopDelays() call overwrites it (a null /
absent override falls back to the default i * speed.stopDelay
stagger). It survives across spin() AND refill() boundaries by
design, because runCascade(...) calls refill() in a loop and the
order set once before the chain must apply to every iteration.
The canonical cascade pattern resets it per phase:
setDropOrder('ltr')beforespin(). left-to-right reveal on the initial drop.setDropOrder('all')beforerunCascade(). every refill in the chain drops all columns simultaneously (the commercial-cascade pattern).
If you leave the order set between rounds and don’t re-set before the
next spin(), the previous value carries over. Re-set explicitly per
round if your rounds use different patterns.
Call again with a different value to change it; the previous value is replaced, not stacked.
Parameters#
| Parameter | Type |
|---|---|
order | number[] | "all" | "ltr" | "rtl" | null |
stepMs? | number |
Returns#
void
Example#
reelSet.setDropOrder('ltr'); // left-to-right
reelSet.setDropOrder('rtl'); // right-to-left
reelSet.setDropOrder('all'); // all columns simultaneously
reelSet.setDropOrder([0, 0, 200, 200, 400]); // custom per-reel delays
reelSet.setDropOrder(null); // clear the override, restore the default
setMinimumSpinTime()#
setMinimumSpinTime(ms: number | number[] | null): void;
Defined in: core/ReelSet.ts:1523
Override the minimum spin time (ms) every reel must accumulate before it
may start stopping. This replaces the active speed profile’s
minimumSpinTime, which is one value shared by every reel and therefore
a floor no single reel can land below — the reason setStopDelays()
alone can’t make one reel land instantly while another keeps spinning.
Pass a number for a uniform floor, one value per reel for a per-reel
floor (entries past the end fall back to the profile), or null to
clear. Like setStopDelays(), the override persists across spin() and
refill() until it is cleared.
Parameters#
| Parameter | Type |
|---|---|
ms | number | number[] | null |
Returns#
void
Example#
reelSet.setMinimumSpinTime([0, 0, 0, 900, 900]); // last two hold longer
reelSet.setMinimumSpinTime(null); // back to the profile
setReelGroups()#
setReelGroups(groups: number[][] | null): void;
Defined in: core/ReelSet.ts:1398
Group the reels, so they stop and skip as blocks instead of individually.
Without groups the engine’s only ordering is reel index: stop delays are
one flat reelIndex * stopDelay stagger across the whole board, and a skip
press lands “everything outside the tease” at once. That breaks down the
moment a reel’s job is not tied to its neighbours - a filler reel meant to
outlast a tease on the reels before it will still land in the middle of it,
because index 4 comes after index 3 and nothing else is being said.
A group is a barrier in both directions:
- Stopping. No reel in a group starts its stop sequence (anticipation included) until every reel in the earlier groups has LANDED. A reel waiting its turn keeps spinning at full speed, so the wait reads as “still going”, not as a pause.
- Skipping. A press releases the next un-landed group, not the whole
board. Tease protection still applies inside a group: with
protect: 'stepwise'a group of teasing reels comes down one press at a time, in tease order.
Stop delays become group-relative, so the profile’s stopDelay staggers
reels WITHIN a group instead of re-adding a whole-board offset on top of
the barrier. An explicit setStopDelays is still taken as given.
Every reel must appear exactly once - listing some and leaving the rest to
an implicit trailing group would make the barrier depend on something the
caller never wrote down. Pass null to clear.
Sticky, like setStopDelays: a layout survives spin() and
refill() until changed, so a fixed board is configured once.
Per round, from the server response. The barrier is read as each reel’s
SpinPhase resolves - which is exactly when setResult() lands - so a layout
set any time up to that point is honoured in full, including between
spin() and setResult(). Group each round however that round’s response
says to. Changing the layout after reels have begun landing throws: a reel
that already passed the barrier cannot un-pass it, so the new layout would
apply to some reels and not others, silently.
Parameters#
| Parameter | Type |
|---|---|
groups | number[][] | null |
Returns#
void
Examples#
// Reels 1-2 land together; 3-4 tease, one press each; 5 outlasts them all.
reelSet.setReelGroups([[0, 1], [2, 3], [4]]);
reelSet.setAnticipation([2, 3], { stagger: 400, protect: 'stepwise' });
// Presses walk the board: [0,1] -> 2 -> 3 -> [4].
// A different shape every round, decided by the server response.
const res = await api.spin();
const p = reelSet.spin();
reelSet.setReelGroups(res.teasing.length ? [[0, 1], res.teasing, [4]] : [[0, 1, 2, 3, 4]]);
reelSet.setResult(res.grid);
await p;
setResult()#
setResult(symbols: ColumnTarget[]): void;
Defined in: core/ReelSet.ts:710
Set the target result symbols. Triggers the stop sequence.
One ColumnTarget per reel. visible is the visible-window target;
optional bufferStart / bufferEnd target cells outside it.
If any pins are active (reelSet.pin(...)), their symbols are overlaid
onto the result before it reaches the stop sequencer, so pinned cells
always land on the pin’s symbolId regardless of what the server sent.
Parameters#
| Parameter | Type |
|---|---|
symbols | ColumnTarget[] |
Returns#
void
Example#
reelSet.setResult([
{ visible: ['A','B','C'] },
{ visible: ['A','B','C'] },
{ visible: ['A','B','C'], bufferStart: ['COIN'] },
{ visible: ['A','B','C'] },
{ visible: ['A','B','C'] },
]);
setShape()#
setShape(cellsPerReel: number[]): void;
Defined in: core/ReelSet.ts:1846
MultiWays: record the cell count each reel should land on this spin. The AdjustPhase between SPIN and STOP will reshape reels (resize symbols, reshape motion) before the stop sequence runs.
Must be called between spin() and setResult(). The shape stays in
effect for the current spin only. call again on every spin.
Throws if:
- this slot was not built with
.multiways(...) cellsPerReel.length !== reelCount- any entry falls outside
[multiways.minCells, multiways.maxCells]
Parameters#
| Parameter | Type |
|---|---|
cellsPerReel | number[] |
Returns#
void
setSpeed()#
setSpeed(name: string): void;
Defined in: core/ReelSet.ts:2069
Change speed and emit event.
Parameters#
| Parameter | Type |
|---|---|
name | string |
Returns#
void
setStopDelays()#
setStopDelays(delays: number[] | null): void;
Defined in: core/ReelSet.ts:1339
Override the per-reel stop delay (in ms). Pass one value per reel.
Sticky. The override persists indefinitely. it survives across
spin() AND refill() boundaries until you call setStopDelays()
(or setDropOrder()) again. The persistence is deliberate: cascade
recipes that set setDropOrder('all') once before runCascade(...)
want every internal refill() to honor it. If your rounds use
different patterns, re-set explicitly per round.
Pass null to CLEAR the override and restore the default
i * speed.stopDelay stagger. this is distinct from [] / all-zeros
(which lands every reel simultaneously). Use it to undo a one-off
per-round pattern without hard-coding the default back in.
Parameters#
| Parameter | Type |
|---|---|
delays | number[] | null |
Returns#
void
Example#
// Stagger the last two reels more than the default for dramatic effect:
reelSet.setStopDelays([0, 140, 280, 600, 1100]);
// ...later, go back to the profile default:
reelSet.setStopDelays(null);
setSymbolAt()#
setSymbolAt(
reel: number,
cell: number,
symbolId: string
): void;
Defined in: core/ReelSet.ts:1565
Swap the symbol at a single grid cell in-place, at rest.
Caller-facing wrapper over Reel.setSymbolAt that ALSO refuses
pinned cells (since Reel itself can’t see the pin map). Use this
for live presentation effects. sticky-after-win, mid-feature
rewrites. without going through setResult().
Throws (in addition to the per-reel guards documented on
Reel.setSymbolAt) if (reel, cell) currently has an active pin.
Use unpin(reel, cell) first if you intentionally want to overwrite it.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
symbolId | string |
Returns#
void
Example#
await reelSet.spin(); // landed
reelSet.setSymbolAt(2, 1, 'wild'); // swap centre cell to wild
skipNudge()#
skipNudge(reel?: number): void;
Defined in: core/ReelSet.ts:1748
Fast-forward an in-flight nudge() to its landed state. No-op if the
given reel is not currently nudging.
The tween’s onComplete fires synchronously, the strip snaps to the
final position, and the original nudge() promise resolves on the
next microtask. nudge:complete fires normally. From a listener’s
POV the nudge just landed fast.
Pairs with skipSpin() (round-aware spin land + boost) and
slamStop() (unconditional spin land-now). These three are distinct:
spin actions do not affect a nudge in flight, and skipNudge does
not touch spin state.
Parameters#
| Parameter | Type | Description |
|---|---|---|
reel? | number | Reel index, or undefined to skip all in-flight nudges. |
Returns#
void
skipSpin()#
skipSpin(options?: SkipOptions): void;
Defined in: core/ReelSet.ts:1441
Round-aware spin skip. The button-press entry point. The first press in a round slams the current drop AND applies a round-scoped side effect:
- Standard mode: boost the active speed profile to the fastest
registered one (emits
skip:boosted). Restored on the nextspin()(unless the app manually changed speed in between). - Cascade/tumble mode: flag every subsequent
refill()to auto-slam with no animation. One press ends a multi-drop cascade.
Subsequent presses also slam each current drop.
When the spin set a protected tease (setAnticipation(reels, { protect })),
a press that would otherwise end the tease instead lands only the reels
around it, holds the round side effect back, and parks skipStage at 1.
The next press does the full slam.
Throws if called before setResult() arrives (nothing to land on:
slamming now would land on random spin-buffer content). The universal
“spin/skip” button pattern should call requestSkip() in that window
(or wrap skipSpin() in a try/catch that routes to requestSkip()
in the catch). Callers that want a slam without the round-scoped side
effects (tests, anti-cheat) should use slamStop().
Pairs with skipNudge() (skip an in-flight nudge()) and slamStop()
(unconditional land-now, no boost). Three distinct actions:
skipSpin()lands the in-flight spin and applies the round-scoped boost / auto-slam-refills side effect.skipNudge()fast-forwards an in-flightnudge()to its landed position. Spin state is unrelated.slamStop()lands every un-landed reel unconditionally. No boost.
Parameters#
| Parameter | Type |
|---|---|
options? | SkipOptions |
Returns#
void
slamStop()#
slamStop(options?: SlamOptions): void;
Defined in: core/ReelSet.ts:1503
Hard slam-stop. Lands un-landed reels immediately, bypassing the
two-stage skipSpin() machine, any speed boost, and tease protection.
For tests, anti-cheat flows, or any caller with unambiguous
“end now” intent.
With no argument it lands EVERY un-landed reel and ends the round.
Pass { reels } or { except } (not both) for a PARTIAL slam: those
reels land now, every other reel keeps running its phase chain to a
natural landing, and skipStage is left alone. This is the raw lever
under setAnticipation(..., { protect }) — reach for it directly when
you want your own skip granularity rather than the tease rule.
Throws before setResult(), like skipSpin(): there is nothing to land
on yet. Use requestSkip() in that window.
Pairs with skipSpin() (round-aware land + boost) and skipNudge()
(fast-forward an in-flight nudge()).
Parameters#
| Parameter | Type |
|---|---|
options? | SlamOptions |
Returns#
void
Example#
// Land everything except the two reels still teasing.
reelSet.slamStop({ except: [3, 4] });
spin()#
spin(options?: SpinOptions): Promise<SpinResult>;
Defined in: core/ReelSet.ts:686
Start spinning. Returns a promise that resolves when all (non-held) reels land.
Pass { holdReels: [i, ...] } to keep specific columns frozen for
this spin. they skip START / SPIN / STOP entirely and stay on
whatever symbols they’re currently showing. The use cases are
Hold & Win respins, sticky / expanding wilds, and “the trigger
column stays in place” bonus rounds.
Pass { mode: 'standard' | 'cascade' } to override the builder-time
default for a single spin (e.g. classic strip-spin on the first round,
drop-in on the cascade waves). 'cascade' requires .tumble(...)
on the builder.
Parameters#
| Parameter | Type |
|---|---|
options? | SpinOptions |
Returns#
Promise<SpinResult>
Examples#
// Plain spin. every reel animates.
await reelSet.spin();
// Hold reels 0 and 4; only the middle three reroll.
const spin = reelSet.spin({ holdReels: [0, 4] });
reelSet.setResult(serverGrid); // entries at 0/4 are ignored
await spin;
// Per-spin cascade override.
await reelSet.spin({ mode: 'cascade' });
See {@link SpinOptions} for the full contract (event behaviour,
setResult interaction, setAnticipation filtering).
swapSymbols()#
swapSymbols(swaps: readonly SymbolSwap[], opts?: SwapSymbolsOptions): Promise<void>;
Defined in: core/ReelSet.ts:862
Re-skin cells in place: animate the current symbols out, swap their identities, animate the new ones in.
The mystery-reveal beat, and the upgrade beat, as one call. setSymbolAt
already swaps an identity, but it swaps it INSTANTLY - so a game that wants
“the reel dissolves, the symbol underneath changes, the reveal arrives” has
to hand-roll the ordering, the stagger, the zIndex bump so the entrance is
not clipped, and the abort handling, every time.
The three beats are separately skippable, because the middle one is the
only one the engine has to own. A game whose art plays its own Spine out
track passes skipOut: true and keeps the rest.
Cells are validated up front, so a bad coordinate fails before anything has
animated rather than half way through. Single-cell symbols only: a big
symbol spans cells the frame layer has to reserve, so revealing one is a
setResult / setShape job and setSymbolAt says so.
Only valid at rest - setSymbolAt throws mid-motion.
Parameters#
| Parameter | Type |
|---|---|
swaps | readonly SymbolSwap[] |
opts? | SwapSymbolsOptions |
Returns#
Promise<void>
Examples#
// Mystery reveal: the reel peels away, then one symbol arrives late.
await reelSet.swapSymbols(
[0, 1, 2].map((cell) => ({ reel: 2, cell, id: 'WILD' })),
{ outDelay: (_, i) => i * 0.05, holdMs: 220, inDelay: (_, i) => i * 0.08 },
);
// Upgrade in place, art driving its own exit.
await reelSet.swapSymbols([{ reel: 1, cell: 1, id: 'GOLD_K' }], { skipOut: true });
unpin()#
unpin(reel: number, cell: number): void;
Defined in: core/ReelSet.ts:2418
Remove a pin at (reel, cell). If no pin exists at that cell, this is a
no-op. Fires pin:expired with reason 'explicit'.
Parameters#
| Parameter | Type |
|---|---|
reel | number |
cell | number |
Returns#
void