pixi-reels

ReelSet

pixi-reels


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 symbols
  • setAnticipation(reelIndices). slow the given reels before they stop, for “will the third scatter land?” tension
  • skipSpin() 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#

ParameterType
paramsReelSetParams

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

EventEmitter<ReelSetEvents>


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

FrameAPI


isDestroyed#

Get Signature#

get isDestroyed(): boolean;

Defined in: core/ReelSet.ts:2649

Returns

boolean

Implementation of#

Disposable.isDestroyed


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

RandomSymbolControl


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

SpeedManager


spotlight#

Get Signature#

get spotlight(): SymbolSpotlight;

Defined in: core/ReelSet.ts:2081

Returns

SymbolSpotlight


viewport#

Get Signature#

get viewport(): ReelViewport;

Defined in: core/ReelSet.ts:2341

Get the viewport.

Returns

ReelViewport

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#

Disposable.destroy

Overrides#

Container.destroy

destroySymbols()#

destroySymbols(cells: readonly Cell[], opts?: DestroySymbolsOptions): Promise<void>;

Defined in: core/ReelSet.ts:968

Parameters#

ParameterType
cellsreadonly 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#

ParameterType
reelnumber
cellnumber

Returns#

CellBounds


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#

ParameterType
reelnumber
cellnumber

Returns#

CellBounds


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#

ParameterType
reelnumber
cellnumber

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#

ParameterType
reelnumber
cellnumber

Returns#

CellPin | undefined


getReel()#

getReel(index: number): Reel;

Defined in: core/ReelSet.ts:2227

Get a reel by index.

Parameters#

ParameterType
indexnumber

Returns#

Reel


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#

ParameterType
reelnumber
cellnumber

Returns#

{
  anchor: {
     cell: number;
     reel: number;
  };
  size: {
     cells: number;
     reels: number;
  };
}
NameTypeDefined in
anchor{ cell: number; reel: number; }core/ReelSet.ts:1953
anchor.cellnumbercore/ReelSet.ts:1953
anchor.reelnumbercore/ReelSet.ts:1953
size{ cells: number; reels: number; }core/ReelSet.ts:1953
size.cellsnumbercore/ReelSet.ts:1953
size.reelsnumbercore/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#

ColumnTarget[]


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).
  • to must be within the grid; no pin may already exist there.
  • Calling with from === to is a no-op that still fires pin:moved.

Parameters#

ParameterType
fromCellCoord
toCellCoord
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),
  • reel is out of range,
  • any visible cell on the target reel has an active pin,
  • Reel.nudge itself 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#

ParameterType
reelnumber
optionsNudgeOptions

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#

ParameterType
reelnumber
cellnumber
symbolIdstring
options?CellPinOptions

Returns#

CellPin

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#

ParameterType
positionsSymbolPosition[]

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.length cells 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#

ParameterType
optsRefillOptions

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#

ParameterType
reelIndexnumber

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#

ParameterType
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 before runCascade and the same order applies to every drop.
  • cascade:fall:symbol, cascade:place:end, cascade:dropIn:symbol fire on each refill.
  • reelSet.skipSpin() ends the chain immediately; the returned summary reports wasSkipped: true.

Event order per stage with winners: cascade:chain:startcascade:destroy:start → (destroy tweens) → cascade:destroy:endonCascade → 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#

ParameterType
optsRunCascadeOptions

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-order k starts its slow-down k * stagger ms 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:

  • slowdown interpolates across the tease sequence so each successive reel slows to a lower speed (fromto) and/or holds longer (holdFromholdTo) — the escalating “each reel crawls slower than the last” build-up. See AnticipationSlowdown.
  • duration (ms) overrides the profile’s anticipationDelay, so the tease plays even in Turbo / SuperTurbo (whose profiles use anticipationDelay: 0 and would otherwise skip anticipation).
  • protect keeps 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.
  • curve replaces 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 with slowdown, which is sugar for a two-leg curve.
  • cells ends 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#

ParameterTypeDefault value
reelIndicesnumber[]undefined
options| AnticipationStagger | AnticipationOptions0

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#

ParameterTypeDescription
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') before spin(). left-to-right reveal on the initial drop.
  • setDropOrder('all') before runCascade(). 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#

ParameterType
ordernumber[] | "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#

ParameterType
msnumber | 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#

ParameterType
groupsnumber[][] | 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#

ParameterType
symbolsColumnTarget[]

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#

ParameterType
cellsPerReelnumber[]

Returns#

void


setSpeed()#

setSpeed(name: string): void;

Defined in: core/ReelSet.ts:2069

Change speed and emit event.

Parameters#

ParameterType
namestring

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#

ParameterType
delaysnumber[] | 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#

ParameterType
reelnumber
cellnumber
symbolIdstring

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#

ParameterTypeDescription
reel?numberReel 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 next spin() (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-flight nudge() to its landed position. Spin state is unrelated.
  • slamStop() lands every un-landed reel unconditionally. No boost.

Parameters#

ParameterType
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#

ParameterType
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#

ParameterType
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#

ParameterType
swapsreadonly 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#

ParameterType
reelnumber
cellnumber

Returns#

void