import { Renderer, Ticker } from 'pixi.js'; import { gsap } from 'gsap'; import { SpeedProfile, SymbolData, OffsetConfig, MultiWaysConfig, ReelAnchor, Stacking } from '../config/types.js'; import { MaskStrategy } from './ReelViewport.js'; import { ReelSet } from './ReelSet.js'; import { Orientation, Direction } from './ReelAxis.js'; import { ReelCurveInput, CurveFocus, CurveMode } from './ReelCurve.js'; import { SymbolRegistry } from '../symbols/SymbolRegistry.js'; import { SymbolPool, SymbolPoolScope } from '../frame/SymbolPool.js'; import { PhaseFactory } from '../spin/phases/PhaseFactory.js'; import { SpinningMode } from '../spin/modes/SpinningMode.js'; import { FrameMiddleware } from '../frame/FrameBuilder.js'; import { ColumnTarget } from '../frame/ColumnTarget.js'; import { TumbleConfig } from '../cascade/TumbleConfig.js'; /** * The configurator you call before every reel set. * * `ReelSetBuilder` is a fluent, chainable builder: every call returns the * builder so you can string setup onto one expression. It hides the * twenty-odd subsystems you would otherwise have to wire by hand, and its * `.build()` step validates that every required piece is present (throws * at construction, not at first spin). * * Required calls (in any order): `.reels(n)`, `.visibleCells(n)`, * `.symbolSize(w, h)`, `.symbols((registry) => ...)`, `.ticker(app.ticker)`. * Optional: `.symbolGap()`, `.weights()`, `.symbolData()`, `.speed()`, * `.bufferSymbols()`, `.offset()`, `.frameMiddleware()`, `.phases()`, * `.spinningMode()`. * * ```ts * const reelSet = new ReelSetBuilder() * .reels(5) * .visibleCells(3) * .symbolSize(200, 200) * .symbols((r) => { * r.register('cherry', SpriteSymbol, { textures: { cherry: tex } }); * }) * .weights({ cherry: 20 }) * .ticker(app.ticker) * .build(); * ``` */ export declare class ReelSetBuilder { private _reelCount?; private _visibleCells?; private _symbolWidth?; private _symbolHeight?; private _symbolGap; private _bufferStart; private _bufferEnd; private _symbolRegistry; private _weights; private _symbolPools; private _speeds; private _initialSpeed; private _offset; private _ticker?; private _spinningMode; private _phaseFactory; private _middlewares; private _initialFrame?; private _symbolDataOverrides; private _tumbleConfig?; private _defaultSpinMode; /** Per-reel static cell counts (jagged shapes like 3-5-5-5-3). */ private _visibleCellsPerReel?; /** Per-reel pixel-box heights. used for both pyramids and MultiWays. */ private _reelExtents?; /** Vertical alignment of short reels inside the tallest reel's box. */ private _reelAnchor; /** Render order of cells inside a reel, and of reels inside the set. */ private _cellStacking; private _reelStacking; private _orientation; private _direction; private _directionPerReel?; private _curve?; private _curvePerReel?; private _curveFocus; private _curveMode; private _curveBleed; private _renderer?; /** MultiWays configuration. Set by `.multiways(...)`. */ private _multiways?; /** Per-reel AdjustPhase tween duration in ms (MultiWays only). */ private _pinMigrationDuration; /** GSAP easing string used by AdjustPhase. Default: 'power2.out'. */ private _pinMigrationEase; /** Mask strategy. Default: per-reel `RectMaskStrategy`. */ private _maskStrategy; /** True if the user explicitly set a mask strategy (no auto-pick override). */ private _maskStrategyExplicit; private _gsap; private _rng; private _poolCapacity?; /** * @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.visibleCells}. * * TypeScript catches a v1 call at compile time, but an untyped consumer * would otherwise get "x.visibleRows is not a function", which names * neither the replacement nor the codemod. These stubs do. */ visibleRows(_count: number): never; /** @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.visibleCellsPerReel}. */ visibleRowsPerReel(_cells: number[]): never; /** @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.reelExtents}. */ reelPixelHeights(_heights: number[]): never; /** Set number of reel columns. */ reels(count: number): this; /** * Number of visible cells per reel (uniform across all reels). * Mutually exclusive with `visibleCellsPerReel()`. calling both throws * at `build()`. * * @example * builder.reels(5).visibleCells(3) // classic 5x3 */ visibleCells(count: number): this; /** * Per-reel static cell counts. Length MUST equal `reels()`. Mutually * exclusive with `visibleCells()`; calling both throws at `build()`. * * @example * builder.reels(5).visibleCellsPerReel([3, 5, 5, 5, 3]) // pyramid */ visibleCellsPerReel(cells: number[]): this; /** * Per-reel pixel-box heights. Length MUST equal `reels()`. * * - Pyramid: defaults to `visibleCellsPerReel[i] * symbolHeight`. Override * to make all reels the same height with different cell heights per * reel. * - MultiWays: every entry equals the same fixed reel height. Cell * height per reel is derived as `reelExtent / visibleCells[i]`. * * Precedence: when both `reelExtents` and `reelAnchor` are set, * `reelExtents` wins. anchor is derived from the explicit boxes. */ reelExtents(heights: number[]): this; /** Vertical alignment of short reels inside the tallest reel's box. Default 'center'. */ reelAnchor(anchor: ReelAnchor): this; /** * Render order of cells inside each reel. Default `'ascending'`. the cell * at the larger main coordinate (bottom for vertical, right for * horizontal) draws in front of its neighbour. * * Geometric on purpose: `direction('reverse')` and per-spin reversal do * NOT flip it, so symbol art lit from above keeps overlapping the way the * artist drew it. Set `'descending'` if your art wants the opposite. */ cellStacking(order: Stacking): this; /** * Render order of reels inside the set. Default `'ascending'`. the last * reel draws in front, which reads as "rightmost on top" for vertical and * "bottom-most on top" for horizontal. */ reelStacking(order: Stacking): this; /** * Strip travel axis for the whole set. `'vertical'` (default) runs strips on * Y with reels marched along X; `'horizontal'` runs them on X with reels * marched along Y. * * Everything else is orientation-neutral: uniform grids, pyramids * (`visibleCellsPerReel`), MultiWays, big symbols and cascades all work on * either axis from the same arithmetic. `symbolSize(width, height)` stays * SCREEN-space, so a horizontal set gives the cell its main extent through * `width` where a vertical one uses `height`. */ orientation(orientation: Orientation): this; /** * Default travel direction for every reel. `'forward'` (default) heads toward * the larger coordinate (down for vertical); `'reverse'` runs the other way * (roll-up on a vertical set). */ direction(direction: Direction): this; /** * Per-reel travel direction override (length must equal `reels()`), for * alternating-column effects. Reels omitted fall back to `direction()`. */ directionPerReel(directions: Direction[]): this; /** * Fake the curvature of the reel cylinder on every reel in the set. * * Cells bunch up and squash toward the window edges the way they would on a * real drum, while the middle of the window magnifies slightly because it is * the part facing you. It is a per-cell transform, so the art stays crisp, * there is no render texture or shader, and a flat set (the default) pays * nothing at all. * * @param curve `0` = flat, `1` = a hard barrel. Pass * {@link ReelCurveConfig} to also tune `depth`, the cross-axis narrowing * that keeps it reading as a drum rather than a squeezed flat strip. * * @example * builder.curve(0.35); * builder.curve({ amount: 0.5, depth: 0.3 }); */ curve(curve: ReelCurveInput): this; /** * Per-reel curvature override (length must equal `reels()`). Reels omitted * fall back to `curve()`. * * Use it when the reels are not all the same size, or for the common trick * of bending the middle reels harder than the outer ones so the board reads * as one wide drum rather than five identical ones. * * @example * builder.curvePerReel([0.2, 0.35, 0.5, 0.35, 0.2]); */ curvePerReel(curves: ReelCurveInput[]): this; /** * Where the camera looking at the drum sits, across the strip. * * `'reel'` (default) puts one dead ahead of every reel, so each is its own * little drum. `'set'` puts a single camera in front of the middle of the * board: cells that rotate away also lean IN toward the centre, and the grid * reads as one wide cylinder instead of five identical ones. `'set-lean'` is * halfway, which is usually the sweet spot on a 5-wide board. * * Only has an effect alongside `curve(...)` / `curvePerReel(...)`. * * **Mask-strategy auto-pick:** leaning cells cross their own column, and the * default per-reel {@link RectMaskStrategy} would clip them at the boundary. * Anything other than `'reel'` therefore switches the default to * {@link SharedRectMaskStrategy}. Passing `.maskStrategy(...)` explicitly * always wins. * * @example * builder.curve(0.4).curveFocus('set-lean'); */ /** * How the curve is drawn. * * `'symbol'` (default) projects each cell on its own: crisp, free, and a real * keystone - but only for symbols whose content IS a texture. A `Container` * transform is affine, so a Spine skeleton, a `Graphics`, or a composite * subtree can only be displaced and scaled by it, never bent. * * `'warp'` renders each reel to a texture and draws it through a mesh whose * VERTICES are displaced by the projection. Everything inside the reel bends * identically - skeletons, atlas sprites, text, effects - and no symbol has * to cooperate. It costs one extra render pass per reel per frame and * resamples the reel once, so hairline art is marginally softer. * * `'warp'` requires {@link ReelSetBuilder.renderer}. * * @example * builder.curve(0.5).curveMode('warp').renderer(app.renderer); */ curveMode(mode: CurveMode): this; /** * The renderer `curveMode('warp')` draws each reel's texture with. Required * for warp mode and unused otherwise. * * @example * builder.renderer(app.renderer) */ renderer(renderer: Renderer): this; /** * Cross-axis room, in pixels per side, for symbols whose art is WIDER than * their cell - an overflowing mystery plate, leaves spilling past the tile. * * `curveMode('warp')` renders each reel into a texture the size of the reel, * so anything hanging over the edge is sliced off at the texture boundary. * This gives the texture room, and the overflow is captured, warped with * everything else, and sticks out over its neighbours. * * Costs texture area, so keep it to what the art actually needs. Warp mode * only; ignored under `curveMode('symbol')`, where symbols are real display * objects and overflow already draws. * * Pair it with {@link SharedRectMaskStrategy} (or a `curveFocus` other than * `'reel'`, which selects it for you) or the per-reel mask clips the * overhang straight back off. * * @example * builder.curve(0.45).curveMode('warp').curveBleed(40).renderer(app.renderer); */ curveBleed(pixels: number): this; curveFocus(focus: CurveFocus): this; /** * Custom mask strategy for the viewport. Defaults to {@link RectMaskStrategy} * (one clip rect per reel. clean for pyramid + uniform layouts). * * Use {@link SharedRectMaskStrategy} when reels have horizontal gaps * AND symbols (typically big symbols) need to overlap across reel * boundaries. the per-reel default would clip them at the gaps. * * Or pass any custom `MaskStrategy` for non-rectangular masks (rounded * frames, hexagonal grids, etc.). * * @example * import { SharedRectMaskStrategy } from 'pixi-reels'; * builder.maskStrategy(new SharedRectMaskStrategy()) */ maskStrategy(strategy: MaskStrategy): this; /** * Configure this slot as MultiWays: per-spin cell variation. Pass minCells, * maxCells, and the fixed reel pixel height. After build, call * `reelSet.setShape(cellsPerReel)` mid-spin to set the next stop's shape. * * Mutually exclusive with big-symbol registration (`SymbolData.size`). * Mutually exclusive with cascade mode in v1. */ multiways(config: MultiWaysConfig): this; /** * AdjustPhase tween duration in ms (MultiWays only). Pass a number for a * uniform duration across reels, or a function `(reelIndex) => number` * for per-reel control. Default: 200. Pass `0` for an instant snap (no * tween). * * AdjustPhase plays on top of whatever stop staggering you've configured; * its duration is independent of `stopDelay`. */ pinMigrationDuration(value: number | ((reelIndex: number) => number)): this; /** * GSAP easing string used by AdjustPhase tweens (MultiWays only). * Applied to both the cell-resize tween and any pin-overlay migration * tween. Defaults to `'power2.out'`. See gsap.com/docs/v3/Eases for * the full vocabulary. * * @example * builder.pinMigrationEase('back.out(1.4)') // pop-in feel * builder.pinMigrationEase('expo.inOut') // slow start + slow end */ pinMigrationEase(ease: string): this; /** Set symbol dimensions in pixels. */ symbolSize(width: number, height: number): this; /** Set gap between symbols. Default: { x: 0, y: 0 }. */ symbolGap(x: number, y: number): this; /** * Set number of buffer symbols either side of the visible window. * Default: 1. * * `start` is the edge at the smaller main coordinate (above for * vertical, left for horizontal) and `end` the larger one. Both are * geometric, not travel-relative: flipping a reel's direction never * moves a buffer teaser to the opposite edge. * * Buffer cells are off-screen cells the reel keeps around the visible * window so symbols can fade/slide in cleanly. The motion layer's wrap * detection assumes at least one buffer cell each side. the minimum * supported count is **1**. Passing `0` (or a negative number) is * clamped to `1` and a single console warning is printed; the builder * does not throw, so existing user code keeps running. * * **Tumble-only reel sets** may drop the end-window buffer entirely * with the object form: `bufferSymbols({ start: 1, end: 0 })`. A pure * tumble never scrolls the strip, so nothing ever wraps through the * end-window cells. they exist only to be hidden by the mask. This * requires `.tumble(...)` on the builder (validated at `build()`), and * strip spins (`spin({ mode: 'standard' })`) and `nudge()` throw on * such a set. `start` keeps the minimum of 1 (drop-in movers are * pre-positioned outside the start edge). */ bufferSymbols(count: number | { start: number; end: number; }): this; private _clampBufferMin1; /** One-shot guard so we don't spam consoles when builders are constructed in a loop. */ private static _bufferWarnedThisProcess; /** Configure symbols via a registry callback. */ symbols(configurator: (registry: SymbolRegistry) => void): this; /** Set weights for random symbol generation. */ weights(weights: Record): this; /** * Narrow what the engine may draw when it fills a cell you did not name. * * `weights()` sets the base table for every reel; this layers pools on * top of it, so a symbol can be common on the strip and impossible in * the buffer cells, or heavy on one reel only. Call it once per scope. * * Buffer pools apply ON TOP of the spinning ones (see `SymbolPoolScope`), * and the same pools are reachable at run time as * `reelSet.randomSymbols`, which is where a game mode switch belongs. * * @example * .randomSymbols({ exclude: ['EMPTY'] }) // every reel * .randomSymbols({ exclude: ['COIN'] }, { slots: 'buffer' }) // buffers only * .randomSymbols({ weights: { WILD: 40 } }, { reel: 2 }) // reel 2 only */ randomSymbols(pool: SymbolPool, scope?: SymbolPoolScope): this; /** * Per-symbol metadata overrides (zIndex, unmask, or a custom weight that * replaces the one from `weights()`). Merged into the final symbolsData map; * any field you don't specify falls back to the default. * * `zIndex` sorts within ONE reel's container only. it can never lift a * symbol above the reel to its right (reels are separate containers). * Cross-reel and out-of-mask layering needs `unmask: true`, which is an * **at-rest** presentation: while the reel spins the symbol stays masked * like everything else; on land, visible-cell instances are lifted into * the viewport-wide `unmaskedContainer` (above every reel and the mask) * and pulled back down when the next spin starts. * * @example * .symbolData({ * wild: { zIndex: 5 }, // above reel-mates (same reel only) * bonus: { zIndex: 10, unmask: true }, // landed: above all reels + mask * }) */ symbolData(overrides: Record>): this; /** Add a named speed profile. */ speed(name: string, profile: SpeedProfile): this; /** Set which speed profile to use initially. Default: 'normal'. */ initialSpeed(name: string): this; /** Set X-axis offset config (e.g., trapezoid perspective). Default: 'none'. */ offsetConfig(config: OffsetConfig): this; /** Set the PixiJS ticker for frame updates. */ ticker(ticker: Ticker): this; /** * Inject the source of randomness used to fill the scrolling strip (buffer * fill, the symbols shown during SPIN before `setResult` lands, nudge * padding). Must return a value in [0, 1). Default: `Math.random`. * * **Why you'd set this:** server-authoritative *outcomes* do not make the * on-screen strip reproducible — the symbols a player sees scrolling are * drawn from this RNG. Injecting a seeded, audited PRNG lets you replay the * exact visual sequence from a seed, which provably-fair and regulated * real-money deployments are eventually required to produce. * * @example * import { ReelSetBuilder } from 'pixi-reels'; * const seeded = mulberry32(serverSeed); // your audited PRNG * const reelSet = new ReelSetBuilder().reels(5).visibleCells(3) * .symbols(...).ticker(app.ticker).rng(seeded).build(); */ rng(fn: () => number): this; /** * Override the per-symbol-id recycle-pool capacity. By default the engine * sizes the pool to the whole strip (every visible + buffer cell), so even a * grid that is briefly all one symbol recycles instead of churning through * `destroy()` + recreate. Set this only to cap memory on very large grids, or * to raise headroom for unusually heavy simultaneous symbol swaps. */ poolCapacity(maxPerSymbol: number): this; /** * Inject the GSAP instance the engine should use for tweens. * * **When you need this:** if your app already imports `gsap` and your * bundler resolves `gsap` to a different module instance than the one * `pixi-reels` resolved (common with symlinked workspaces, npm-link, or * misconfigured `dedupe`), every tween you start on a target the engine * also tweens will fight a separate timeline. Symptoms: spotlights that * render but never finish, animations that double-fire, tweens that * silently drop on hidden tabs in only one of the two instances. * * Calling `.gsap(myGsap)` binds every phase, motion tween, symbol * pin-flight tween, and SpriteSymbol win pulse to the GSAP you pass. * guaranteed to be the same instance that drives your own animations. * * Default: the `gsap` import resolved at the engine's own * `node_modules/gsap` path. If your app and the engine resolve to the * same instance (the common case in production bundles with proper * `dedupe`), you do NOT need to call this. * * **Per reel set, not process-wide.** v1 stored one instance in a module * global, so the last `.gsap()` call before any `build()` silently won for * every set. Each set now captures the instance at `build()` time, so a * composed stage can drive two sets from different instances. Pass the * same instance to `driveGsapWithTicker(ticker, instance)`. * * Read at `build()`. calling it afterwards does not move an existing set. * * @example * import { gsap } from 'gsap'; * const reelSet = new ReelSetBuilder() * .reels(5).visibleCells(3).symbolSize(200, 200) * .symbols(...) * .ticker(app.ticker) * .gsap(gsap) // ensure engine and app share one instance * .build(); */ gsap(instance: typeof gsap): this; /** Set the spinning mode. Default: StandardMode. */ spinningMode(mode: SpinningMode): this; /** Add custom frame middleware. */ frameMiddleware(middleware: FrameMiddleware): this; /** Override default phases. */ phases(configurator: (factory: PhaseFactory) => void): this; /** * Enable tumble cascade mechanics. Replaces strip-spin + bounce-stop with * a three-phase pipeline: * * 1. **`cascade:fall`**. on `spin()`, existing visible symbols fall * off the bottom of the viewport. * 2. **`cascade:place`**. when `setResult()` arrives, new symbol * identities swap into the buffer at their final grid positions. * 3. **`cascade:dropIn`**. new symbols animate from above (and * survivors slide down to fill holes) into the grid. * * For a Moment B refill after wins are cleared, call * `reelSet.refill({ winners, grid })`. that skips fall + wait and runs * `place` + `dropIn` only, with gravity-correct geometry driven by the * `winners` list (untouched symbols don't animate; survivors slide; * new symbols come from above). * * Every phase boundary fires a `cascade:*` event on * `reelSet.events`. per-symbol events (`cascade:fall:symbol` / * `cascade:dropIn:symbol`) carry the symbol, view, and the timing the * library is about to apply, so listeners can run parallel tweens on * any other property in sync with the library's `view.y` motion. * * Override any individual phase via `.phases(f => f.register('cascade:fall', MyPhase))`. * * @example * builder.tumble({ * fall: { duration: 300, ease: 'sine.in', cellStagger: 60 }, * dropIn: { duration: 600, ease: 'power2.out', cellStagger: 60, distance: 'perHole' }, * }); */ tumble(config?: TumbleConfig): this; /** * Set the initial symbol grid the reels show before the first spin. * * One `ColumnTarget` per reel. `visible` lists the symbols in the visible * window; optional `bufferStart` / `bufferEnd` prefill cells outside it * (`[0]` is the slot closest to the visible window, later indices go * further out). * * @example * builder.initialFrame([ * { visible: ['A','B','C'] }, * { visible: ['A','B','C'], bufferStart: ['COIN'] }, * { visible: ['A','B','C'], bufferEnd: ['SCATTER'] }, * ]); */ initialFrame(frame: ColumnTarget[]): this; /** Build the ReelSet. Validates configuration and assembles all internal objects. */ build(): ReelSet; private _validate; } //# sourceMappingURL=ReelSetBuilder.d.ts.map