import { Container, Texture, TextStyle } from 'pixi.js'; /** A symbol's visual. Any Container; optionally implements the lifecycle the cell drives. */ interface SymbolView extends Container { playIdle?(): void; playWin?(): Promise; showStatic?(): void; /** Resize the symbol to the given cell size in pixels (square scalar or rectangular). */ resize?(size: number | { width: number; height: number; }): void; } /** Game-supplied factory: build the view for a symbol id (sprite / layered sprites / Spine / composite). */ type SymbolResolver = (symbolId: string) => SymbolView | null; interface SymbolTextures { base: Texture; idle?: Texture[]; win?: Texture[]; } interface AnimatedSymbolConfig { textures: SymbolTextures; size: number | { width: number; height: number; }; fps?: number; } /** Built-in SymbolView: a static base sprite with optional idle/win spritesheet frames. */ declare class AnimatedSymbol extends Container implements SymbolView { private _base; private _anim; private _textures; private _w; private _h; private _fps; constructor(config: AnimatedSymbolConfig); setTextures(t: SymbolTextures): void; resize(size: number | { width: number; height: number; }): void; showStatic(): void; playIdle(): void; playWin(): Promise; private _swap; } interface CellFrameStyle { radius?: number; idle?: { color: number; alpha: number; }; winning?: { color: number; alpha: number; }; removed?: { color: number; alpha: number; }; fresh?: { color: number; alpha: number; }; } interface CellData { symbol: string | null; multiplier?: number; bonus?: number; sticky?: { remaining: number; }; } interface CellState { winning?: boolean; removed?: boolean; fresh?: boolean; } interface SymbolCellConfig { /** Square scalar or rectangular cell size in px. */ size: number | { width: number; height: number; }; resolve: SymbolResolver; frameStyle?: CellFrameStyle; } declare class SymbolCell extends Container { readonly __uiComponent: true; private _w; private _h; private _resolve; private _style; private _frame; private _view; private _data; private _badges; private _multBadge; private _bonusBadge; /** Last applied state key — exposed for tests/inspection. */ frameStyleKey: 'idle' | 'winning' | 'removed' | 'fresh'; constructor(config: SymbolCellConfig); get view(): SymbolView | null; /** The last applied cell data (symbol + badges). Read by motion controllers that rebuild tapes. */ get data(): CellData; setData(data: CellData): void; setState(state: CellState): void; playWin(): Promise; playIdle(): void; hasBadge(kind: 'multiplier' | 'bonus'): boolean; private _drawFrame; private _setMultiplier; private _setBonus; private _badge; } /** Per-strip cell size override: a square scalar, or an explicit width/height. */ type CellSizeSpec = number | { width: number; height: number; }; /** Structural input any grid config (GridConfig / ReelGridConfig) satisfies. */ interface GeometryInput { cols: number; /** Uniform row count. Ignored when `rowsPerReel` is set (and length matches `cols`). */ rows: number; rowsPerReel?: number[]; /** Square cell size (shorthand: same width & height, all reels). */ cellSize: number; /** Rectangular cells, uniform across reels. Override `cellSize`. */ cellWidth?: number; cellHeight?: number; /** Per-strip cell size (square scalar or {width,height}). Overrides the above for that reel. */ cellSizePerReel?: CellSizeSpec[]; /** Uniform gap (shorthand for both axes). */ gap?: number; /** Horizontal gap between adjacent reels. Scalar, or per-boundary (length cols-1). */ colGap?: number | number[]; /** Vertical gap between rows. Scalar, or per-reel (length cols). */ rowGap?: number | number[]; } interface ResolvedGeometry { cols: number; rowsPerReel: number[]; maxRows: number; /** Per-reel cell width / height (px). */ cellW: number[]; cellH: number[]; /** Per-reel vertical gap between rows (px). */ rowGap: number[]; /** Horizontal gap between reel i and i+1 (px), length cols-1. */ colGap: number[]; /** Cell-centre X of each reel (px). */ colX: number[]; /** Row-0 cell-centre Y of each reel (px) — centring offset baked in. */ yOff: number[]; /** Total grid bounding size (px). */ gridW: number; gridH: number; /** Grid bounding-box centre in local coords (px). */ centerX: number; centerY: number; /** Top-left corner of the grid bounding box in local coords (px). */ leftX: number; topY: number; } /** Resolve a grid config into a fully-populated per-reel geometry. */ declare function resolveGeometry(g: GeometryInput): ResolvedGeometry; /** Cell-centre position from a resolved geometry. */ declare function cellPositionOf(geom: ResolvedGeometry, col: number, row: number): { x: number; y: number; }; interface DecorationConfig { texture?: Texture; padding?: number; } interface ReelGridConfig { cols: number; /** Uniform row count. Ignored per-reel when `rowsPerReel` is provided. */ rows: number; /** Per-reel row counts for Megaways / variable-height reels. Length must equal `cols`. */ rowsPerReel?: number[]; /** Square cell size (shorthand: same width & height, all reels). */ cellSize: number; /** Rectangular cells, uniform across reels. Override `cellSize` when set. */ cellWidth?: number; cellHeight?: number; /** Per-strip cell size (square scalar or {width,height}). */ cellSizePerReel?: CellSizeSpec[]; /** Uniform gap (shorthand for both axes). */ gap?: number; /** Horizontal gap between adjacent reels. Scalar, or per-boundary (length cols-1). */ colGap?: number | number[]; /** Vertical gap between rows. Scalar, or per-reel (length cols). */ rowGap?: number | number[]; resolve: SymbolResolver; frameStyle?: CellFrameStyle; decoration?: DecorationConfig; mask?: boolean; } /** * A grid of `SymbolCell`s. Supports uniform grids, variable-height reels (Megaways), and * rectangular / per-strip cell sizes with per-strip gaps. All layout flows through a single * resolved geometry (see grid/geometry.ts); every consumer reads positions via `cellPosition` * and dimensions via `cellSize(col)` rather than assuming a square cell. */ declare class ReelGrid extends Container { readonly __uiComponent: true; private _cfg; private _geom; private _cells; private _cellLayer; private _resolve; private _frameStyle?; private _mask; constructor(config: ReelGridConfig); private get _cols(); private get _rowsPerReel(); private _buildCells; /** Per-strip mask: one window per reel sized to that reel's own cell width × column height. */ private _applyMask; get cols(): number; /** Tallest reel's row count (the grid's visual height in rows). */ get rows(): number; /** Cell dimensions (px) for a reel. Rectangular / per-strip aware. */ cellSize(col: number): { width: number; height: number; }; /** The fully-resolved grid geometry (read-only view). */ get geometry(): ResolvedGeometry; rowsOf(col: number): number; get rowsPerReel(): number[]; /** Cell centre position. Variable-height reels are centred about a shared centre line. */ cellPosition(col: number, row: number): { x: number; y: number; }; /** Grid bounding-box centre in local coords. */ center(): { x: number; y: number; }; getCell(col: number, row: number): SymbolCell; setGrid(cells: CellData[][]): void; /** Rebuild the grid with new per-reel row counts (Megaways re-roll / dynamic rows). */ reshape(rowsPerReel: number[]): void; /** * Re-resolve geometry after a base cell-size change and reposition cells. Accepts a square * scalar (updates `cellSize`) or explicit `{width,height}`. Per-strip overrides still apply. * (Cell frames are not re-drawn here — a geometry change routes through a full rebuild upstream.) */ resize(size: number | { width: number; height: number; }): void; } interface CascadeStepData { winningCells: { col: number; row: number; }[]; removedCells: { col: number; row: number; }[]; newCells: { col: number; row: number; symbol: string; }[]; settledGrid: CellData[][]; } interface CascadeTimings { reveal: number; highlight: number; remove: number; drop: number; refill: number; wait: number; } interface CascadeAnim { col: number; row: number; phase: 'reveal' | 'highlight' | 'remove' | 'drop' | 'refill'; from?: { x: number; y: number; }; to?: { x: number; y: number; }; scale?: number; alpha?: number; duration: number; easing?: string; delay?: number; } declare class CascadeController { private _grid; private _t; private _killed; constructor(grid: ReelGrid, timings?: Partial); /** PURE: ordered animation descriptors for a cascade step. */ plan(step: CascadeStepData, opts?: { turbo?: boolean; }): CascadeAnim[]; /** Execute the plan via Tween. Not unit-tested (Ticker doesn't tick in node). */ run(step: CascadeStepData, opts?: { turbo?: boolean; }): Promise; private _killOwnTweens; skip(): void; kill(): void; } interface ReelSpinData { targetGrid: CellData[][]; strip?: (reel: number) => string[]; } interface ReelSpinTimings { spinUp: number; hold: number; stopStagger: number; settle: number; } interface ReelStopPlan$1 { reel: number; stopTime: number; landing: CellData[]; settle: { amp: number; ms: number; }; } declare class ReelSpinController { private _grid; private _t; private _killed; constructor(grid: ReelGrid, timings?: Partial); /** PURE: per-reel stop timing + landing window. No Pixi mutation. */ plan(data: ReelSpinData, opts?: { turbo?: boolean; }): ReelStopPlan$1[]; /** Execute the spin: scroll each reel, decelerate, land on target, settle-bounce. Not unit-tested. */ run(data: ReelSpinData, opts?: { turbo?: boolean; }): Promise; private _killOwnTweens; skip(): void; } type EasingFunction = (t: number) => number; /** Resolve a descriptor's string easing name to the engine's easing function. */ declare const EASING_BY_NAME: Record; /** Resolve an easing name to a function, falling back to easeOutQuad. */ declare function easingByName(name?: string): EasingFunction; /** Names of easing functions available in the engine's `Easing` map (see anim/easing-map.ts). */ type EasingName = 'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic' | 'easeInBack' | 'easeOutBack' | 'easeInOutBack' | 'easeOutBounce' | 'easeInBounce' | 'easeOutElastic' | 'easeInSine' | 'easeOutSine' | 'easeInOutSine'; /** * A value that is either flat across the board, or per-reel: an array indexed by REEL INDEX * (holes fall back to the scalar default). Used for anticipation timings so a game can make * each successive reel slower than the last. */ type PerReel = T | (T | undefined)[]; /** Resolve a `PerReel` for one reel. `undefined` (or a hole in the array) yields `fallback`. */ declare function perReelValue(value: PerReel | undefined, reel: number, fallback: T): T; /** Win-evaluation model. Purely presentational here (affects geometry + highlight), the math lives in Lua. */ type EvaluationMode = 'lines' | 'ways' | 'anywhere' | 'cluster' | 'megaways' | 'infinity'; interface GridConfig { cols: number; /** Uniform row count. Ignored when `rowsPerReel` is set. */ rows: number; /** Per-reel row counts (Megaways / variable-height reels). Length should equal `cols`. */ rowsPerReel?: number[]; /** Square cell size (shorthand: same width & height, all reels). */ cellSize: number; /** Rectangular cells, uniform across reels. Override `cellSize` when set. */ cellWidth?: number; cellHeight?: number; /** Per-strip cell size (square scalar or {width,height}). Overrides the above for that reel. */ cellSizePerReel?: CellSizeSpec[]; /** Uniform gap (shorthand for both axes). */ gap: number; /** Horizontal gap between adjacent reels. Scalar, or per-boundary (length cols-1). Overrides `gap`. */ colGap?: number | number[]; /** Vertical gap between rows. Scalar, or per-reel (length cols). Overrides `gap`. */ rowGap?: number | number[]; evaluation: EvaluationMode; /** For Megaways: clamp per-reel rows to [minRows, maxRows]. */ minRows?: number; maxRows?: number; /** Megaways top horizontal reel (renders above the grid, adds to ways). */ topReel?: { size: number; } | null; mask: boolean; frameStyle?: CellFrameStyle; decoration?: { padding?: number; }; } type MotionStyle = 'swap' | 'strip' | 'cascade-drop'; type StopMode = 'sequential' | 'sync' | 'random'; /** `cascade-drop` fill direction within one reel. */ type DropOrder = 'top-down' | 'bottom-up'; /** How `cascade-drop` spaces reels: by formula, or one strictly after the other. */ type DropSequence = 'parallel' | 'chained' | 'chained-when-anticipated'; type StopOrder = 'ltr' | 'rtl'; type Intensity = 'full' | 'reduced' | 'minimal'; interface SettleConfig { /** Overshoot amplitude in px applied to the reel before it springs back. */ amp: number; ms: number; easing: EasingName; } /** Squash & stretch on landing impact (Stone-Rush / magnus style). */ interface SquashConfig { enabled: boolean; scaleX: number; scaleY: number; ms: number; } interface BlurConfig { enabled: boolean; /** Alpha applied to in-motion symbols (hot-ross uses 0.82). */ alpha: number; /** Runtime BlurFilter strength factor (PixiJS slots demo uses ~8), 0 disables the filter. */ strength: number; /** Draw vertical motion streaks behind moving symbols. */ streaks: boolean; } interface MotionConfig { style: MotionStyle; /** Base spin-up duration (ms) before the first reel can stop. */ spinUp: number; /** Extra hold (ms) at full speed before deceleration. */ hold: number; /** Delay (ms) between consecutive reel stops in sequential mode. */ stopStagger: number; stopMode: StopMode; stopOrder: StopOrder; settle: SettleConfig; squash: SquashConfig; blur: BlurConfig; /** Multiply all durations in turbo mode (0.5 = twice as fast). */ turboFactor: number; /** Global animation-intensity scale (accessibility): full=1, reduced=0.7, minimal=0.4. */ intensity: Intensity; /** Allow slam/quick-stop (snaps reels to target without changing outcome). */ slamStop: boolean; /** Symbols visible on a reel tape while spinning (swap/strip). */ symbolsPerReel: number; /** `cascade-drop`: ms between consecutive cells of ONE reel (top→bottom). Default 24. */ cellStagger: number; /** `cascade-drop`: multiplier on `stopStagger` for the per-reel offset. Default 0.4. */ reelStaggerFactor: number; /** `cascade-drop`: fall duration as a fraction of `spinUp`. Default 0.6. */ dropFallFactor: number; /** * `cascade-drop`: which cell of a reel lands first. `'top-down'` (default, the engine's original * behaviour) deals the reel like cards from the top; `'bottom-up'` fills it the way gravity * would — the lowest cell arrives first and the rest stack on top of it. */ dropOrder: DropOrder; /** * `cascade-drop`: how reels are spaced. * - `'parallel'` (default) starts every reel at its own formula offset — * `reel * stopStagger * reelStaggerFactor` — so a reel can open while the previous one is * still dropping. Fast, and what a normal spin wants. * - `'chained'` starts a reel only once the previous one has seated its LAST cell, plus that * same offset as the gap. One reel at a time, always — correct, but it makes every spin as * long as the sum of its reels. * - `'chained-when-anticipated'` runs the un-armed reels in parallel and switches to the chain * from the first ANTICIPATED reel onwards. The base spin keeps its stop window; the hunt for * the last scatter goes strictly reel by reel, each slower than the last. */ dropSequence: DropSequence; } /** What a game-supplied `AnticipationConfig.decide` may return instead of a bare reel list. */ interface AnticipationOverride { /** Reels to anticipate, in the order the progression should ramp. Empty = no anticipation. */ reels: number[]; /** Speed factor (lower = slower). Scalar, or per-reel indexed by reel index. */ slowdown?: PerReel; /** Extra hold before landing. Scalar, or per-reel indexed by reel index. */ holdMs?: PerReel; /** Growth of the gap between successive cells within a reel. Scalar, or per-reel. */ cellStaggerRamp?: PerReel; } interface AnticipationConfig { enabled: boolean; /** Symbols that count toward the anticipation threshold (scatter/bonus). */ triggerSymbols: string[]; /** Number of trigger symbols already landed that arms anticipation (N−1, usually 2). */ threshold: number; /** Which reels get the slow treatment: the still-spinning trailing reels, or explicit indices. */ reels: 'trailing' | number[]; /** Multiply the trailing reel's spin speed (lower = slower / longer). */ slowdownFactor: number; /** Extra hold (ms) before the final anticipation reel lands (300–500 typical). */ holdMs: number; /** * Game-supplied decision, REPLACING the built-in `triggerSymbols`/`threshold` counting. * Return the reels to anticipate (or an `AnticipationOverride`); `null` / `[]` = no anticipation. * Use this when the trigger is not expressible as "N of symbol X landed" — e.g. "the round is * still alive on every reel so far", or "reel 3 missed its symbol, so let 4 and 5 stop normally". */ decide?: ((targetGrid: CellData[][]) => number[] | AnticipationOverride | null) | null; /** * Ramp the slowdown across successive anticipated reels: reel #i of the decision gets * `slowdownFactor * progressiveSlowdown ** i`. 1 = flat (default); < 1 = each reel slower * than the last. */ progressiveSlowdown: number; /** Extra hold (ms) added per successive anticipated reel: reel #i gets `holdMs + i * this`. */ progressiveHoldMs: number; /** * `cascade-drop`: how the gap between SUCCESSIVE CELLS grows inside an anticipated reel. The * gap before cell #i is `cellStagger * * progressiveCellStagger ** i`, so 1 * (the default) keeps the reel's cells evenly spaced and > 1 makes each symbol land later than * the last — the drip that turns a reel into a countdown. * * It compounds with `progressiveSlowdown`: that widens the base gap reel by reel, this widens * it cell by cell, so a later reel drips both slower AND with a steeper ramp. */ progressiveCellStagger: number; /** Optional grid zoom while anticipating (magnum-opus uses 1.3×). */ zoom: { enabled: boolean; scale: number; ms: number; }; } interface CascadeConfig { enabled: boolean; /** Animate surviving symbols sliding down into the gaps (true tumble). */ gravity: boolean; timings: { reveal: number; highlight: number; remove: number; drop: number; refill: number; wait: number; }; easings: { highlight: EasingName; remove: EasingName; drop: EasingName; }; /** Slow each successive cascade step to build tension: factor = 1 + step*perStepDecel (capped). */ perStepDecel: number; perStepDecelCap: number; /** Dim non-winning symbols during the highlight phase. */ dimNonWinners: boolean; dimAlpha: number; /** Running win multiplier that climbs per cascade (Pragmatic Tumble / Avalanche). */ multiplier: { enabled: boolean; start: number; /** 'add' → +step each step; 'mul' → ×step each step. */ mode: 'add' | 'mul'; step: number; cap: number | null; /** Keep climbing across free-spins instead of resetting per spin. */ persistInFreeSpins: boolean; }; } interface WinConfig { highlightScale: number; glow: boolean; /** Frame shake on landing/win. */ frameShake: { enabled: boolean; amp: number; ms: number; onlyOnSymbols: string[] | null; }; } interface ExpandingWildConfig { enabled: boolean; symbol: string; /** Reels that may host an expanding wild (empty = all). */ reels: number[]; /** Expand to the full reel height. */ toFullReel: boolean; ms: number; easing: EasingName; onlyInFreeSpins: boolean; } interface StickyConfig { enabled: boolean; symbols: string[]; /** How many spins a sticky symbol persists (0 = whole feature/bonus). */ durationSpins: number; /** Ring colour drawn around sticky cells. */ ringColor: number; } interface WalkingWildConfig { enabled: boolean; symbol: string; direction: 'left' | 'right'; stepPerSpin: number; awardsRespin: boolean; asStacked: boolean; } interface MultiplierConfig { enabled: boolean; /** Multiplier symbol id (carries a value), or null when multipliers are per-cell data. */ symbol: string | null; scope: 'perSymbol' | 'perReel' | 'global'; combine: 'additive' | 'multiplicative'; attachedToWild: boolean; max: number; accumulateAcrossFreeSpins: boolean; } interface MysteryConfig { enabled: boolean; symbol: string; /** Eligible reveal targets; all mystery tiles reveal the SAME single draw per spin. */ revealPool: string[]; canRevealWild: boolean; ms: number; } interface TransformConfig { enabled: boolean; /** Trigger symbol whose landing fires the transform (null = data-driven). */ trigger: string | null; /** 'randomLow' picks one low symbol at random; or pin a specific source. */ source: 'randomLow' | string; target: string; /** Convert every instance of the source symbol. */ allInstances: boolean; /** Restrict to low→high (upgrade) direction. */ upgradeOnly: boolean; ms: number; } interface GiantConfig { enabled: boolean; /** Footprint in cells. */ width: number; height: number; /** Symbols that may appear giant (empty = any). */ symbols: string[]; /** Pick one giant symbol type per spin. */ chosenPerSpin: boolean; onlyInFreeSpins: boolean; } interface SplitConfig { enabled: boolean; symbol: string; factor: number; /** Reels that can host the split trigger (xSplit lands on the last reel). */ reels: number[]; /** Defer the split execution until all respins finish. */ deferUntilRespinsEnd: boolean; } interface StackedConfig { enabled: boolean; symbols: string[]; /** Max stack height (positions of the same symbol on one reel). */ height: number; } interface NudgeConfig { enabled: boolean; /** Reels eligible to nudge. */ reels: number[]; step: number; /** xNudge: nudge a stacked wild into full view, +1 multiplier per nudge. */ toFullReel: boolean; multiplierStart: number; multiplierPerNudge: number; } interface ReelModifierConfig { enabled: boolean; /** Random pre-spin modifiers, each with a trigger weight. */ pool: { effect: 'addRows' | 'addWilds' | 'setGiant' | 'guaranteedWilds'; magnitude: number; weight: number; }[]; appliesIn: 'base' | 'freeSpins' | 'both'; } interface HoldAndSpinConfig { enabled: boolean; lockSymbols: string[]; triggerThreshold: number; respinsAwarded: number; /** Reset the respin counter to full whenever a new symbol locks. */ resetOnNewSymbol: boolean; jackpotTiers: string[]; fullGridAwardsGrand: boolean; } interface RandomWildConfig { enabled: boolean; /** Fixed count, or [min,max] range. */ count: number | [number, number]; sticky: boolean; multiplier: number; trigger: 'randomBaseSpin' | 'onFreeSpins'; chance: number; } interface FeaturesConfig { expandingWild: ExpandingWildConfig; sticky: StickyConfig; walkingWild: WalkingWildConfig; multiplier: MultiplierConfig; mystery: MysteryConfig; transform: TransformConfig; giant: GiantConfig; split: SplitConfig; stacked: StackedConfig; nudge: NudgeConfig; reelModifier: ReelModifierConfig; holdAndSpin: HoldAndSpinConfig; randomWild: RandomWildConfig; } /** Ordered list of feature keys (resolve order matters; see docs §3.4). */ type FeatureKey = keyof FeaturesConfig; declare const FEATURE_KEYS: FeatureKey[]; interface ReelSystemConfig { grid: GridConfig; motion: MotionConfig; anticipation: AnticipationConfig; cascade: CascadeConfig; win: WinConfig; features: FeaturesConfig; } /** Deep-partial helper for overrides. */ type DeepPartial = { [K in keyof T]?: T[K] extends (infer U)[] ? T[K] : T[K] extends object ? DeepPartial : T[K]; }; declare const DEFAULT_REEL_CONFIG: ReelSystemConfig; /** Intensity → duration scale (accessibility). */ declare const INTENSITY_SCALE: Record; /** Deep-merge `partial` onto `base` (arrays replace, objects merge). Returns a new object. */ declare function mergeReelConfig(base: T, partial?: DeepPartial): T; /** Resolve a partial config to a fully-populated `ReelSystemConfig`. */ declare function resolveReelConfig(partial?: DeepPartial): ReelSystemConfig; /** Effective per-reel row counts (resolves Megaways `rowsPerReel`, else uniform `rows`). */ declare function effectiveRowsPerReel(grid: GridConfig): number[]; /** Resolve a `GridConfig` into a fully-populated per-reel geometry (rectangular / per-strip aware). */ declare function resolveGridGeometry(grid: GridConfig): ResolvedGeometry; /** Total ways-to-win for a ways/megaways grid (product of per-reel heights). */ declare function waysCount(grid: GridConfig): number; type PresetId = 'classic' | 'kitsune-wrath' | 'moon-spice' | 'stone-rush' | 'hot-ross' | 'magnus' | 'megaways'; interface ReelPreset { id: PresetId; name: string; note: string; config: DeepPartial; } declare const PRESETS: Record; declare const PRESET_LIST: ReelPreset[]; interface SpinData { /** Target board: targetGrid[col][row]. */ targetGrid: CellData[][]; /** Optional per-reel symbol tape for the spinning illusion (defaults to random of the landing symbols). */ strip?: (reel: number) => string[]; } interface SpinRunOpts { turbo?: boolean; /** Reels to slow for anticipation (computed by the ReelSystem from config + targetGrid). */ anticipateReels?: number[]; /** Speed factor for anticipated reels (lower = slower). Scalar, or per-reel indexed by reel. */ anticipateSlowdown?: PerReel; /** Extra hold (ms) for anticipated reels. Scalar, or per-reel indexed by reel. */ anticipateHoldMs?: PerReel; /** * `cascade-drop`: growth of the gap between successive cells inside an anticipated reel — the * gap before cell #i is `cellStagger * * ramp ** i`. 1 keeps the reel evenly * spaced. Scalar, or per-reel indexed by reel. */ anticipateCellStaggerRamp?: PerReel; /** * Reels whose tape runs normally but whose landing is NOT handed back. The engine stops and * disposes of the tape as usual and leaves the real cells hidden and unseated; the caller owns * their data and visibility from that point (and `skip()` will not reveal them either). */ deferReveal?: number[]; /** The resolved schedule, handed over before the first frame runs. Schedule against THESE * numbers rather than re-deriving `plan()`'s formula. */ onPlan?: (plan: ReelStopPlan[]) => void; /** Fires on the frame a reel lands — after its cells are seated, before settle/squash/shake. * For a deferred reel it still fires (the reel DID stop); nothing was seated. */ onReelStop?: (reel: number, plan: ReelStopPlan) => void; /** Fires as each cell takes its landing symbol. In `cascade-drop` this is the per-cell impact * frame (after the fall, before the squash) — the hook for a per-cell sound or shake. */ onCellSeated?: (reel: number, row: number, data: CellData) => void; } interface ReelStopPlan { reel: number; /** Wall-clock time (ms) from spin start when this reel stops. */ stopTime: number; landing: CellData[]; settle: { amp: number; ms: number; }; anticipated: boolean; /** Time-stretch applied to this reel's tape (>= 1, longer = slower). 1 when not anticipated. */ slowdown: number; /** True when `deferReveal` withheld this reel's landing (see `SpinRunOpts.deferReveal`). */ deferred: boolean; /** * `cascade-drop` only: ms from spin start at which each cell of this reel seats, INDEXED BY ROW. * The reel's fill direction lives in these numbers (`motion.dropOrder` decides which row is the * smallest), as does its place in the chain (`motion.dropSequence`). `stopTime` is the largest * of them — the frame the whole reel has landed. */ cellStopTimes?: number[]; } declare class SpinEngine { private _grid; private _resolve; private _cfg; private _win; private _killed; private _shaking; private _temp; private _deferred; constructor(grid: ReelGrid, resolve: SymbolResolver, cfg: MotionConfig, win?: WinConfig); setConfig(cfg: MotionConfig): void; setWin(win: WinConfig): void; /** Quick decaying frame shake when a landed reel carries a configured trigger symbol. */ private _frameShake; private scale; /** PURE: per-reel stop schedule. No Pixi mutation. */ plan(data: SpinData, opts?: SpinRunOpts): ReelStopPlan[]; /** * `cascade-drop` lays its reels out on a different clock from the tape styles: a reel is a * sequence of per-cell arrivals, not one deceleration. Overwrite `stopTime` with the moment the * reel has fully landed and fill in `cellStopTimes`, so `plan()` stays the single source of truth * for WHEN anything happens — `_runDrop` below only executes these numbers. */ private _planDrop; /** Execute the spin for every reel concurrently. */ run(data: SpinData, opts?: SpinRunOpts): Promise; private _runReel; /** Anticipation time-stretch factor for a reel (>=1, longer = slower). Resolved in `plan()`. */ private slowOf; private _runSwap; private _runStrip; private _runDrop; private _settle; private _squashCell; /** Apply motion blur (alpha + optional BlurFilter) to targets; returns a disposer that clears it. */ private _applyBlur; private _cleanupTemp; /** Slam / quick stop: snap everything to the target and stop animating. */ skip(): void; } interface AnticipationDecision { active: boolean; /** Reel indices that should spin slower / longer, in the order the ramp applies. */ reels: number[]; /** Speed factor. A scalar when flat; a per-reel array when the decision ramps (see * `progressiveSlowdown` / a game-supplied `decide`). Resolve with `perReelValue`. */ slowdown: PerReel; /** Extra hold before landing. Scalar or per-reel array, same as `slowdown`. */ holdMs: PerReel; /** `cascade-drop`: growth of the gap between successive cells inside the reel (1 = even). */ cellStaggerRamp: PerReel; } declare class AnticipationController { private _cfg; constructor(cfg: AnticipationConfig); setConfig(cfg: AnticipationConfig): void; /** Count how many trigger symbols land on a given reel. */ private countOnReel; /** * Decide anticipation from the landing grid. Trigger symbols are counted left→right; * once the running total reaches `threshold`, every still-spinning reel after that point * is flagged for the slow treatment (this mirrors "searching for the last scatter"). */ decide(targetGrid: CellData[][]): AnticipationDecision; /** Assemble a decision, applying the configured progression unless the caller pinned values. */ private build; /** * A flat scalar when the progression is a no-op, else an array INDEXED BY REEL so the engine can * read a per-reel value straight out of `plan()`. */ private ramp; /** Optionally zoom the grid in while anticipating, then settle back. Returns a reset fn. */ zoomIn(grid: ReelGrid): Promise<() => Promise>; } interface TumbleStep { winningCells: { col: number; row: number; }[]; removedCells: { col: number; row: number; }[]; /** Optional explicit survivor slides (col, fromRow→toRow). When omitted, gravity is derived. */ drops?: { col: number; fromRow: number; toRow: number; }[]; newCells: { col: number; row: number; symbol: string; }[]; settledGrid: CellData[][]; } declare class TumbleController { private _grid; private _cfg; private _win; private _killed; private _mult; constructor(grid: ReelGrid, cfg: CascadeConfig, win?: WinConfig); setConfig(cfg: CascadeConfig): void; setWin(win: WinConfig): void; /** Killed, or the grid was torn down underneath us (rebuild during a cascade). */ private get _dead(); get multiplier(): number; resetMultiplier(): void; private advanceMultiplier; /** Derive survivor slides for one column when explicit drops aren't supplied. */ private deriveDrops; /** Run a single cascade step. `stepIndex` drives per-step deceleration. */ step(step: TumbleStep, stepIndex?: number, opts?: { turbo?: boolean; }): Promise; private _dim; private _undim; private _resetPositions; skip(): void; } interface ReelStepData { /** Cells that won on the current board — highlighted/paid before the shift. */ winningCells: { col: number; row: number; }[]; /** How far to scroll each reel down (length = cols). 0 = the reel stays put. */ shifts: number[]; /** Board after every reel has scrolled down by shifts[col]. */ settledGrid: CellData[][]; } /** * PURE: lay out one reel's scroll tape (top→bottom). The `shift` fresh symbols (the top of the * settled reel) stack above the reel's current symbols; the tape starts `shift` cells high so the * current symbols fill the window, then slides down by `shift` to reveal the fresh ones. `shift` is * clamped to the visible window height. Returns the stacked cells and the start offset (in cells, * relative to row 0) the tape animates from. */ declare function buildReelStepTape(before: CellData[], settledCol: CellData[], shift: number): { stack: CellData[]; shift: number; startOffsetCells: number; }; declare class ReelStepController { private _grid; private _resolve; private _cfg; private _win; private _killed; private _mult; private _temp; /** Board the in-flight step settles to — used to snap on skip(). */ private _pending; constructor(grid: ReelGrid, resolve: SymbolResolver, cfg: CascadeConfig, win?: WinConfig); setConfig(cfg: CascadeConfig): void; setWin(win: WinConfig): void; /** Killed, or the grid was torn down underneath us (rebuild mid-chain). */ private get _dead(); get multiplier(): number; resetMultiplier(): void; private advanceMultiplier; /** Run one ReelStep: pay the winning cells, then scroll each reel down by shifts[col]. */ step(step: ReelStepData, stepIndex?: number, opts?: { turbo?: boolean; }): Promise; /** Highlight + hold the winning cells, then release them back to rest before the shift. */ private _payWins; /** * Scroll one reel down by `n` positions. A tape carrying the reel's current symbols with `n` * fresh symbols stacked on top slides down by `n` cells: the fresh symbols enter from the top, * the existing ones ride down, and the bottom `n` ride off below the window. Ends on * settledGrid[col]. The tape shares the cells' parent (so the reel mask, if any, clips it). */ private _scrollReel; private _dim; private _undim; private _resetPositions; private _cleanupTemp; /** Hard-cancel: kill tweens, drop tapes, snap to the in-flight step's settled board. */ skip(): void; } interface FeatureContext { grid: ReelGrid; resolve: SymbolResolver; cfg: ReelSystemConfig; /** Overlay layer above the grid for rings/labels/sprites. */ fx: Container; /** Mutable view of the current board (featureN may rewrite it). */ board: CellData[][]; /** Whether the current spin is a free-spin (gates onlyInFreeSpins features). */ freeSpins?: boolean; /** Emit a human-readable note for the playground log. */ log?: (msg: string) => void; } interface ReelFeature { /** Unique id. Built-ins use a `FeatureKey`; custom features may use any string. */ readonly key: FeatureKey | string; readonly label: string; /** Whether this feature is active for the given config. Custom features can just return `true`. */ enabled(cfg: ReelSystemConfig): boolean; /** Run the feature presentation on the current board. */ demo(ctx: FeatureContext): Promise; } /** All feature modules keyed by their FeatureKey. */ declare const FEATURES: Record; /** Features in canonical resolve order (see docs §3.4). */ declare const FEATURE_LIST: ReelFeature[]; interface CreateReelSystemOptions { resolve: SymbolResolver; config?: DeepPartial; /** Initial board (board[col][row]); defaults to empty cells. */ board?: CellData[][]; log?: (msg: string) => void; /** Custom feature modules to register alongside the 13 built-ins. */ features?: ReelFeature[]; } /** Options for a stepped chain (`cascade` / `reelStep`). */ interface StepRunOpts { turbo?: boolean; /** Carry the running multiplier across free spins (with `cascade.multiplier.persistInFreeSpins`). */ freeSpins?: boolean; /** * Fires after each step has settled, with the step's index and the running multiplier AFTER it. * The hook for a step-by-step payout readout: report the win accrued so far to the shell via * `api.shell.reportWin(...)`. Awaited, so an async hook paces the chain. */ onStep?: (index: number, step: TStep, multiplier: number) => void | Promise; } interface ReelSystem { /** Root container — add this to your scene. */ readonly view: Container; readonly grid: ReelGrid; /** Overlay layer above the grid (masked off) — draw bespoke rings/labels/sprites here. */ readonly fx: Container; readonly config: ReelSystemConfig; /** Current board (board[col][row]). */ readonly board: CellData[][]; /** Ways-to-win for the current grid (ways/megaways evaluation). */ readonly ways: number; setBoard(board: CellData[][]): void; /** Merge a partial config; rebuilds the grid when geometry changes. */ update(partial: DeepPartial): void; /** Replace the whole config. */ setConfig(config: ReelSystemConfig): void; spin(target: CellData[][], opts?: SpinRunOpts): Promise; /** * The schedule `spin(target, opts)` WOULD run, without running it — same anticipation decision, * same numbers. Schedule landing sounds / camera moves against this instead of re-deriving the * engine's formula. (`spin`'s `onPlan` hands you the same array once the spin is under way.) */ planSpin(target: CellData[][], opts?: SpinRunOpts): ReelStopPlan[]; /** The anticipation decision `spin(target, opts)` would use (run options override the config). */ anticipationFor(target: CellData[][], opts?: SpinRunOpts): AnticipationDecision; /** Run a cascade chain. With `freeSpins` + `cascade.multiplier.persistInFreeSpins`, the multiplier * carries over instead of resetting. Generic in the step type, so `onStep` hands back the game's * own step (with its per-step win) rather than the bare TumbleStep. */ cascade(steps: TStep[], opts?: StepRunOpts): Promise; /** * Run a ReelStep™ chain: each step pays its winning cells, then scrolls every reel down by * `shifts[col]` positions (0 = reel stays put). Multiplier carries over with `freeSpins` + * `cascade.multiplier.persistInFreeSpins`, same as `cascade`. */ reelStep(steps: TStep[], opts?: StepRunOpts): Promise; /** Current running cascade / reel-step multiplier. */ readonly multiplier: number; /** Register a custom feature (or override a built-in by reusing its key). */ registerFeature(feature: ReelFeature): void; /** All registered features (built-ins + custom) in canonical order. */ features(): ReelFeature[]; /** Enabled feature modules (built-ins + custom). */ enabledFeatures(): ReelFeature[]; /** Run a feature by key — built-in `FeatureKey` or a custom feature's key. */ runFeature(key: FeatureKey | string, opts?: { freeSpins?: boolean; }): Promise; /** Build a FeatureContext for driving a feature/bespoke animation yourself. */ featureContext(opts?: { freeSpins?: boolean; }): FeatureContext; resize(cellSize: number): void; skip(): void; destroy(): void; } declare function createReelSystem(opts: CreateReelSystemOptions): ReelSystem; interface WinTier { id: string; minMultiplier: number; title: string; accentColor: number; bannerTexture?: Texture; } /** Highest tier whose minMultiplier <= win/bet, or null if below the lowest. */ declare function pickTier(tiers: WinTier[], win: number, bet: number): WinTier | null; /** Index into `tiers` for the running value (or -1 below the lowest tier). */ declare function tierIndexAtValue(tiers: WinTier[], runningValue: number, bet: number): number; interface CountUpConfig { format: (v: number) => string; style?: Partial; } /** PURE eased interpolation 0→target over duration (ms). Clamped to [0, target]. */ declare function valueAt(elapsed: number, target: number, duration: number): number; declare class CountUpDisplay extends Container { readonly __uiComponent: true; private _text; private _format; private _value; constructor(config: CountUpConfig); get text(): string; setValue(v: number): void; /** Swap the value formatter and immediately re-render the current value. */ setFormat(format: (v: number) => string): void; /** Animate the value to target over duration; fires onTier on each tier-index increase. */ countTo(target: number, duration: number, onTier?: (idx: number) => void): Promise; skip(): void; } interface BigWinOverlayConfig { tiers: WinTier[]; formatMoney: (v: number) => string; countUpDuration?: (win: number) => number; particleCount?: number; width: number; height: number; } declare class BigWinOverlay extends Container { readonly __uiComponent: true; private _cfg; private _dim; private _banner; private _title; private _count; constructor(config: BigWinOverlayConfig); /** Pure helper (testable): the tier title for a given win/bet, or null below the lowest tier. */ tierTitleFor(win: number, bet: number): string | null; resize(width: number, height: number): void; show(win: number, bet: number, format?: (v: number) => string): Promise; skip(): void; hide(): void; } /** The boundary at which a collected multiplier resets. */ type CarryPolicy = 'spin' | 'cascade' | 'session'; /** * Headless sticky/collector multiplier — the unified abstraction behind * kitsunebi / recipe / orb / stage multipliers. reset(boundary) clears the * value only when the boundary is at or above the configured policy scope. */ declare class MultiplierAccumulator { value: number; private readonly base; private readonly policy; constructor(cfg: { policy: CarryPolicy; base?: number; }); add(delta: number): void; set(value: number): void; reset(boundary: CarryPolicy): void; } export { AnimatedSymbol, AnticipationController, BigWinOverlay, CascadeController, CountUpDisplay, DEFAULT_REEL_CONFIG, EASING_BY_NAME, FEATURES, FEATURE_KEYS, FEATURE_LIST, INTENSITY_SCALE, MultiplierAccumulator, PRESETS, PRESET_LIST, ReelGrid, ReelSpinController, ReelStepController, SpinEngine, SymbolCell, TumbleController, buildReelStepTape, cellPositionOf, createReelSystem, easingByName, effectiveRowsPerReel, mergeReelConfig, perReelValue, pickTier, resolveGeometry, resolveGridGeometry, resolveReelConfig, tierIndexAtValue, valueAt, waysCount }; export type { AnimatedSymbolConfig, AnticipationConfig, AnticipationDecision, AnticipationOverride, BigWinOverlayConfig, BlurConfig, CarryPolicy, CascadeAnim, CascadeConfig, CascadeStepData, CascadeTimings, CellData, CellFrameStyle, CellSizeSpec, CellState, CountUpConfig, CreateReelSystemOptions, DecorationConfig, DeepPartial, DropOrder, DropSequence, EasingName, EvaluationMode, ExpandingWildConfig, FeatureContext, FeatureKey, FeaturesConfig, GeometryInput, GiantConfig, GridConfig, HoldAndSpinConfig, Intensity, MotionConfig, MotionStyle, MultiplierConfig, MysteryConfig, NudgeConfig, PerReel, PresetId, RandomWildConfig, ReelFeature, ReelGridConfig, ReelModifierConfig, ReelPreset, ReelSpinData, ReelSpinTimings, ReelStepData, ReelStopPlan$1 as ReelStopPlan, ReelSystem, ReelSystemConfig, ResolvedGeometry, SettleConfig, SpinData, SpinRunOpts, ReelStopPlan as SpinStopPlan, SplitConfig, SquashConfig, StackedConfig, StepRunOpts, StickyConfig, StopMode, StopOrder, SymbolCellConfig, SymbolResolver, SymbolTextures, SymbolView, TransformConfig, TumbleStep, WalkingWildConfig, WinConfig, WinTier };