import { Container } from 'pixi.js'; import { Disposable } from '../utils/Disposable.js'; import { ReelSymbol } from '../symbols/ReelSymbol.js'; import { SymbolFactory } from '../symbols/SymbolFactory.js'; import { SymbolData } from '../config/types.js'; import { ReelMotion } from './ReelMotion.js'; import { StopSequencer } from './StopSequencer.js'; import { EventEmitter } from '../events/EventEmitter.js'; import { ReelEvents } from '../events/ReelEvents.js'; import { RandomSymbolProvider } from '../frame/RandomSymbolProvider.js'; import { ReelViewport } from './ReelViewport.js'; import { SpinningMode } from '../spin/modes/SpinningMode.js'; /** * Options for `Reel.nudge()` / `ReelSet.nudge()`. a post-stop reposition * that shifts the reel by `distance` symbol positions and reveals new * caller-supplied symbols. * * Nudges run only while the reel is at rest (post-stop). Calling on a * moving reel throws. */ export interface NudgeOptions { /** * Number of full symbol positions to shift. Must be a positive integer * strictly less than the reel's total strip capacity * (`bufferAbove + visibleRows + bufferBelow`). `incoming.length` must * equal this exactly. */ distance: number; /** * Travel direction. * * - `'down'`. symbols visually move down the screen; new symbols * enter from the top. * - `'up'`. symbols visually move up; new symbols enter from the * bottom. */ direction: 'up' | 'down'; /** * Symbol ids in **top-down order of their final on-strip position**. * including any overflow into the off-screen buffer. Length must equal * `distance` exactly. * * - `incoming[0]` ends up at the topmost new position. For `'down'` * this is the new top visible row (or, if `distance > bufferAbove + visibleRows`, * spills into bufferBelow tail-first via the trailing entries). * For `'up'` with `distance > visibleRows`, `incoming[0]` lands in * bufferAbove (still topmost). * - `incoming[distance-1]` ends up at the bottommost new position. * Mirror of the above. * * For the common case of `distance <= visibleRows`, every entry is a * visible row top-to-bottom and you can ignore the overflow rules. */ incoming: string[]; /** Total animation duration in ms. Defaults to `200 * distance`. */ duration?: number; /** * GSAP easing function name. Defaults to `'power2.out'`. a smooth * deceleration with NO overshoot. If you pass an overshooting ease * (`back.out(N)`, `elastic.out(...)`), the engine clamps the displacement * so wraps never fire past the landing position; the eased value is * computed but the strip's travel is bounded. */ ease?: string; /** * Optional delay (ms) before the tween begins. Validation throws fire * immediately on the call, but the actual reel mutation + tween are * deferred by this much. Useful with `Promise.all([...])` to stagger * parallel nudges: * * ```ts * await Promise.all( * cols.map((col, i) => * reelSet.nudge(col, { ..., startDelay: i * 80 }), * ), * ); * ``` * * `ReelSet.nudge(col, options, { stagger })` is sugar for the common * uniform-stagger case. */ startDelay?: number; /** * Abort the nudge mid-flight. If signalled before the tween starts, the * call rejects with an `AbortError` and no strip mutation happens. If * signalled during the tween, the tween is killed, the strip is snapped * to its post-nudge position (deterministic landing. the contract is * "incoming lands at these positions"), and the promise rejects with an * `AbortError`. `nudge:cancelled` fires on the reel-set bus. */ signal?: AbortSignal; } export interface ReelConfig { reelIndex: number; visibleRows: number; bufferAbove: number; bufferBelow: number; symbolWidth: number; symbolHeight: number; symbolGapX: number; symbolGapY: number; symbolsData: Record; initialSymbols: string[]; /** * Y offset of this reel relative to the viewport's top edge. Set by the * builder so jagged shapes (pyramids) align according to `reelAnchor`. * Default 0. */ offsetY?: number; /** * Pixel height of this reel's box. Used for MultiWays cell-height * derivation (`reelHeight / visibleRows`). Defaults to * `visibleRows * symbolHeight`. */ reelHeight?: number; /** * SPIN-time uniform cell height. During SPIN every reel uses this same * height. AdjustPhase later swaps to per-reel `reelHeight / visibleRows`. * Defaults to `symbolHeight`. */ spinSymbolHeight?: number; } /** * Internal sentinel marking non-anchor cells of a big symbol's block. * Never crosses the public API. `getVisibleSymbols()` resolves it to the * anchor's id. */ export declare const OCCUPIED_SENTINEL = "__pixi_reels_occupied__"; /** * One vertical column of a slot board. * * A `Reel` owns: * - the `ReelSymbol[]` currently on screen (a small buffer above the * visible rows + the visible rows + a small buffer below. so symbols * can fade in from off-screen cleanly) * - the `ReelMotion` that adds a Y delta each tick and wraps symbols * that scroll off the ends * - a `StopSequencer`. the queue of target symbols the reel still has * to land on before it can stop * * You generally do not touch a `Reel` directly. Drive the `ReelSet` and * let it fan out. Reels are exposed on `reelSet.reels` so you can read * the current grid (`reel.getSymbolAt(row)`) or listen to per-reel * events (`phase:enter`, `landed`, `symbol:created`, ...). */ export declare class Reel implements Disposable { readonly container: Container; readonly events: EventEmitter; readonly reelIndex: number; /** Current symbols in order (top buffer → visible → bottom buffer). */ symbols: ReelSymbol[]; /** Current spin speed (pixels per frame). Set by phases. */ speed: number; /** Current spinning mode. */ spinningMode: SpinningMode; readonly motion: ReelMotion; readonly stopSequencer: StopSequencer; private _symbolFactory; private _randomProvider; private _viewport; private _symbolsData; private _visibleRows; private _bufferAbove; private _symbolWidth; private _symbolHeight; private _offsetY; private _reelHeight; private _spinSymbolHeight; private _symbolGapY; private _symbolGapX; private _isDestroyed; private _isStopping; /** * True between `notifySpinStart()` and `notifySpinEnd()`. While set, * `_replaceSymbol` fires `onReelSpinStart(true)` on each freshly * installed symbol so it can join the spin presentation (pool recycling * wipes per-instance state, so the symbol can't know on its own). */ private _spinPresentationActive; private _anticipationActive; /** * True only while the reel is fully at rest (build time, and from * `notifyLanded()` until the next `notifySpinStart()`). NOT the inverse * of `_spinPresentationActive`: that flag drops at `notifySpinEnd()`, * just before the bounce, while the strip is still visibly moving and * the stop sequencer is still installing the result symbols. */ private _atRest; private _isNudging; /** * Symbol-id queue consulted by `_onSymbolWrapped` during a nudge. Each * wrap pulls one id from the front; when empty (or `null`), the wrap * falls back to `stopSequencer` (if `_isStopping`) or `_randomProvider`. * * Populated by `nudge()` and cleared once the tween completes. */ private _nudgeQueue; /** * GSAP tween handle for the active nudge animation. Stored so `destroy()` * and `skipNudge()` can `kill()` it cleanly; cleared in `onComplete` and * on cancellation. `null` between nudges. */ private _nudgeTween; /** * Rejection function for the in-flight nudge's promise. Called by * `destroy()` and `signal.abort()` so consumers `await`-ing the nudge * see a deterministic error instead of a hung promise. Cleared on * `onComplete`. `null` between nudges. */ private _nudgeReject; /** * Internal stub instances reused for OCCUPIED cells inside a big-symbol * block. Allocated on demand (one per concurrent OCCUPIED cell on this * reel), never pooled through `SymbolFactory`. The views are invisible. * the anchor symbol is sized up to cover the whole block. */ private _occupiedStubs; /** * Per-row marker recording which rows are non-anchor cells of a big * symbol. Populated when frames are placed; consulted by `getVisibleSymbols` * and `getSymbolAt` so anchor identity propagates through the block. * * Indexed by visible-row 0..visibleRows-1. Each entry is `null` for a * normal cell, or `{ anchorRow }` for a cell occupied by another row's * anchor. */ private _occupancy; /** * Optional resolver for cross-reel OCCUPIED cells. Set by `ReelSet` so * `getVisibleSymbols()` returns the anchor's id even when the anchor * lives on a different reel (a 2×2 bonus straddles cols c, c+1). * Without it, cross-reel OCCUPIED cells return the OCCUPIED sentinel. */ private _crossReelResolver; constructor(config: ReelConfig, symbolFactory: SymbolFactory, randomProvider: RandomSymbolProvider, viewport: ReelViewport); get isDestroyed(): boolean; get isStopping(): boolean; set isStopping(value: boolean); /** True while a `nudge()` tween is in flight on this reel. */ get isNudging(): boolean; get bufferAbove(): number; get bufferBelow(): number; get visibleRows(): number; /** The symbol cell width (in pixels). Constant for the reel's lifetime. */ get symbolWidth(): number; /** * The symbol cell height (in pixels). Mutates on MultiWays reshape via * `reshape()`. During SPIN this still equals `spinSymbolHeight`; the * per-reel target value comes into effect when AdjustPhase commits the * reshape. For non-MultiWays slots this is constant for the reel's lifetime. */ get symbolHeight(): number; /** Pixel height of this reel's box. Set by builder, immutable. */ get reelHeight(): number; /** Y offset of this reel relative to the viewport top. Set by builder, immutable. */ get offsetY(): number; /** * SPIN-time uniform cell height. All reels in a slot use this value during * the SPIN phase regardless of their per-reel `symbolHeight`. Frozen at * construction. */ get spinSymbolHeight(): number; /** Update reel for one frame. Called by SpinController via ticker. */ update(deltaMs: number): void; /** * Get visible symbol IDs (top to bottom, excluding buffers). * * Big-symbol cells resolve to the anchor's id. both **same-reel** * (the anchor lives on this reel) and **cross-reel** (the anchor is on * a leftward reel of a wider block). The cross-reel resolver is * injected by `ReelSet`; without it, cross-reel OCCUPIED cells would * return the OCCUPIED sentinel, which is the only difference vs. * `ReelSet.getVisibleGrid()`. With the resolver wired, the two are * equivalent for any reel. `reels.map(r => r.getVisibleSymbols())` * matches `reelSet.getVisibleGrid()`. */ getVisibleSymbols(): string[]; /** * Get symbol at a visible row (0-indexed from top visible). * For non-anchor cells of a big symbol, walks up to the anchor row and * returns the anchor symbol so animations target the actual visual. */ getSymbolAt(visibleRow: number): ReelSymbol; /** * Swap the symbol at a single visible row in-place, without restarting * the spin or rebuilding the rest of the strip. * * Useful for live presentation effects at rest. converting a wild * after a cascade pop, swapping to a sticky variant after a win. * without going through the full `placeSymbols` / `setResult` paths. * * The symbol's `zIndex`, parent (masked vs unmasked), and visual state * are reset by `_replaceSymbol` so callers don't need to follow up * with `refreshZIndex`. The motion layer is **not** snapped. call * `snapToGrid()` separately if you need to re-grid. * * Throws if: * - the reel is currently moving (`speed !== 0` or `isStopping`). * A mid-spin swap would be overwritten by the next wrap/stop frame * anyway; the fail-loud throw spares the caller the silent loss. * - `visibleRow` is out of `[0, visibleRows)`. * - `symbolId` is not registered. * - the row is a non-anchor cell of an existing big-symbol block. * - the row currently holds the anchor of a big-symbol block. big * blocks span multiple cells (and possibly reels) and require * `placeSymbols` + the cross-reel OCCUPIED coordinator. * - `symbolId` itself is a big symbol. same reason. * * Pin overlap is **not** detected at this layer (Reel doesn't see the * pin map). Use `ReelSet.setSymbolAt(col, row, id)` for the safe * caller-facing surface that also throws on pinned cells. */ setSymbolAt(visibleRow: number, symbolId: string): void; /** * Shift the reel by `distance` symbol positions, animating the strip with * a GSAP tween and revealing caller-supplied `incoming` symbols. The reel * must be at rest (post-stop). throws otherwise. * * The wrap pipeline drives identity changes during the tween: any incoming * symbol whose final destination is reachable via pre-placement (within * the leading buffer) is set up front; the rest stream through the wrap * callback as the strip moves. `incoming` is always top-down by final * on-strip position. see `NudgeOptions.incoming` for the overflow rules. * * **Big symbols are supported** as long as every block on the strip * (anchor + stubs) survives the rotation without crossing the wrap * boundary: * - down: anchorRow + h - 1 + distance < total * - up: anchorRow ≥ distance * * Blocks that wouldn't survive throw, as do cross-reel blocks (w > 1). * Use case: a 1xH block lands with stubs in bufferBelow. nudge up to * bring the whole block into view. * * Throws if: * - the reel is spinning, stopping, already nudging, or destroyed, * - `distance < 1`, `>= total strip capacity`, `direction` invalid, or * `incoming.length !== distance`, * - any `incoming` id is unregistered or is a big symbol, * - any block on the reel wouldn't survive the rotation, * - any cell on this reel is part of a cross-reel block (w > 1), * - the abort signal is already aborted on entry. * * Resolves with `{ symbols }`. the new visible column top-to-bottom. * Rejects with an `AbortError` if `options.signal` aborts mid-tween or * if the reel is destroyed before the tween completes. * * @param onPrepared Internal hook fired once pre-placement + grid snap * are done but before the tween starts. `ReelSet.nudge` uses this to * emit `nudge:start` after the strip has been mutated, so listeners * observe the about-to-animate state, not the pre-mutation state. */ nudge(options: NudgeOptions, onPrepared?: () => void): Promise<{ symbols: string[]; }>; /** * Fast-forward the active nudge tween to its landed state and resolve. * No-op if no nudge is in flight. The tween's `onComplete` fires * synchronously, the strip snaps to the final position, `_nudgeQueue` * drains, and the original `nudge()` promise resolves on the next * microtask. * * Useful for player-driven "skip" buttons or accessibility paths that * want to land immediately without waiting for the full animation. */ skipNudge(): void; /** * Place symbols immediately at target positions (for skip/turbo). * * `symbolIds[0..n-1]` is the visible area. `symbolIds[n..]` (if present) * targets buffer-below slots. Buffer-above slots are addressed via * negative-index string properties: `symbolIds[-1]` is the slot closest to * the visible top row, `symbolIds[-bufferAbove]` the furthest above. * Unset slots are filled with random symbols, matching the previous * behaviour when only visible-area entries were provided. */ placeSymbols(symbolIds: string[]): void; /** * Compute the canonical zIndex for a single symbol view at a given * array index. Centralizes the formula used by both `refreshZIndex` * (full rescan) and the per-swap activate path (so newly placed * symbols land with their correct zIndex without the caller needing * to remember to call `refreshZIndex` afterwards). */ private _computeSymbolZIndex; /** * Recompute `zIndex` for every symbol in the reel. * * Formula: `symbolData.zIndex ?? 0` (scaled by 100 to leave room for row * ordering), plus the symbol's current array index. so bottom-row symbols * render in front of top-row symbols and any symbol with a higher * configured base zIndex (e.g. wild, bonus) renders above its neighbors. * * Called automatically after wraps, snaps, and direct placement. Also * called inline by `_replaceSymbol` for the single newly-placed symbol. * so consumers who swap one symbol at a time (via the public APIs that * funnel into `_replaceSymbol`) get correct layering for free, no * manual `refreshZIndex` required. Call it manually after mutating * `symbolsData.zIndex` at runtime. */ refreshZIndex(): void; destroy(): void; /** * Whether the symbol with this id has `unmask: true` in its data. i.e. * its view should be parented to `viewport.unmaskedContainer` to render * above the reel mask. */ private _isUnmasked; /** * Whether the symbol should render above the mask RIGHT NOW. Unmask is * an at-rest presentation: while the reel is in motion (including the * stop approach and bounce, when the result symbols are installed), * every view (unmask ids included) stays in the masked reel container so * nothing scrolls visibly outside the grid or sits parked in a buffer * row. Landed visible-row symbols are lifted by `notifyLanded()`; * `notifySpinStart()` pulls them back down before the strip moves. */ private _effectiveUnmask; /** * Pick the right parent container for a symbol view based on its * `unmask` flag and the reel's spin state. At-rest unmasked symbols sit * in `viewport.unmaskedContainer` (above the reel mask); everything * else lives in this reel's own container (which is itself inside * `viewport.maskedContainer`). */ private _parentForSymbolId; /** * Position a symbol view at a given reel-local Y, choosing X and any * parent-translation offset based on whether the symbol is unmasked. * * Unmasked views live in `viewport.unmaskedContainer` (at viewport * (0,0)), so we add `reel.container.x` and `reel.container.y` to keep * the at-rest cell position aligned with the reel column. Masked views * live in `this.container`, so reel-local coords map directly. */ private _placeSymbolView; /** * Convert a view's current y back to reel-local coords. The view may * be parented to either `this.container` (already reel-local) or * `viewport.unmaskedContainer` (viewport-local. needs the reel offset * subtracted). */ private _toReelLocalY; /** * Re-bake the reel's `container.x/y` offset into any currently-lifted * (unmasked) view. * * `ReelMotion.snapToGrid()` writes bare reel-local Y to every symbol * view — it has no notion that some views were re-parented into * `viewport.unmaskedContainer` and need the reel offset added to stay * aligned. Masked reels have `container.y === 0`, so the two spaces * coincide and this is a no-op; on a jagged/pyramid layout (non-zero * `offsetY`) the snap would drop the offset and jump the lifted view. * Call this right after any ABSOLUTE motion snap. `displace()` is * incremental (`+=`) and preserves the offset, so it needs no fixup. * * Lifted views only exist while the reel is at rest, so during a spin * (when the frequent snaps happen) this loop finds nothing. */ private _syncUnmaskedViewOffsets; private _setupSymbolPositions; private _onSymbolWrapped; private _replaceSymbol; /** * Acquire an OCCUPIED stub. Reuses any free stub stored locally; allocates * a new one if none are available. Stubs are never returned to * `SymbolFactory`. */ private _acquireOccupiedStub; private _releaseOccupiedStub; /** * After the visible target frame has been placed, scan the strip to * size big-symbol anchors and populate the OCCUPIED occupancy map. * * Called from `snapToGrid` and `placeSymbols` so it runs both for normal * stop landing AND for skip/turbo. For non-anchor rows of a block, the * anchor symbol is sized to span the block; the OCCUPIED stub at that * row stays invisible underneath. * * **Two scans:** * * 1. Visible anchors. sizes blocks whose anchor is in `[0, visibleRows)`. * This is the common case (most blocks land fully visible). Blocks * whose stubs spill into bufferBelow are handled here: the anchor is * in visible, the sprite is sized to span `h * cellH`, and the mask * clips the off-screen tail. No occupancy entry is written for the * bufferBelow stubs because `_occupancy` is keyed by visible rows * only. consumers can't query a non-visible cell anyway. * 2. BufferAbove anchors. sizes blocks whose anchor sits above visible * but whose body extends into the visible window. This is the "tail * visible" partial-visibility case: a 1xH block whose top is clipped * by the reel mask, with only its bottom cells showing in the visible * window. Without this scan, the anchor sprite would stay at the * default 1x1 size and the block wouldn't render its visible portion * correctly. * * **No Scan 3 for bufferBelow-only anchors.** A block whose anchor is at * `row >= visibleRows` would lie entirely off-screen (the strip ends at * `visibleRows + bufferBelow - 1` and `h >= 1`, so no visible cell is * covered). The cross-reel coordinator already accepts such anchors as a * legal-but-invisible placement; there's nothing to size and nothing for * the consumer-facing query API to return. If you ever add a scenario * where bufferBelow-only anchors need rendering, add Scan 3 here. * * For bufferAbove anchors, `_occupancy[visibleRow].anchorRow` is set to * a NEGATIVE value. the offset from `bufferAbove`. So * `this.symbols[this._bufferAbove + anchorRow]` walks back to the anchor * regardless of which side it lives on. Consumers (`getSymbolFootprint`, * `getBlockBounds`) handle negative anchor rows by clipping bounds to * the visible portion of the block. */ private _finalizeFrame; } //# sourceMappingURL=Reel.d.ts.map