pixi-reels 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"file":"debug-BVIT3Sjs.js","names":[],"sources":["../src/events/EventEmitter.ts","../src/core/ReelAxis.ts","../src/core/ReelMotion.ts","../src/core/ReelCurve.ts","../src/utils/TickerRef.ts","../src/core/ReelWarp.ts","../src/core/StopSequencer.ts","../src/frame/ColumnTarget.ts","../src/spin/modes/StandardMode.ts","../src/core/Reel.ts","../src/core/ReelViewport.ts","../src/spin/phases/ReelPhase.ts","../src/spin/phases/StartPhase.ts","../src/spin/phases/SpinPhase.ts","../src/spin/phases/StopPhase.ts","../src/spin/phases/AnticipationPhase.ts","../src/spin/phases/PhaseFactory.ts","../src/spin/SpinController.ts","../src/speed/SpeedManager.ts","../src/spotlight/SymbolSpotlight.ts","../src/pins/CellPin.ts","../src/config/v1Renames.ts","../src/core/ReelSet.ts","../src/config/defaults.ts","../src/config/SpeedPresets.ts","../src/symbols/SymbolRegistry.ts","../src/pool/ObjectPool.ts","../src/symbols/SymbolFactory.ts","../src/frame/RandomSymbolProvider.ts","../src/frame/FrameBuilder.ts","../src/cascade/TumbleConfig.ts","../src/spin/phases/CascadeFallPhase.ts","../src/cascade/tumbleAlgorithm.ts","../src/spin/phases/CascadePlacePhase.ts","../src/spin/phases/CascadeDropInPhase.ts","../src/spin/phases/AdjustPhase.ts","../src/core/ReelSetBuilder.ts","../src/debug/debugOverlay.ts","../src/debug/debug.ts"],"sourcesContent":["type Listener = (...args: any[]) => void;\n\ninterface ListenerEntry {\n fn: Listener;\n context: unknown;\n once: boolean;\n}\n\n/**\n * Typed event emitter with zero dependencies.\n *\n * Usage:\n * ```ts\n * interface MyEvents {\n * 'foo': [x: number, y: string];\n * 'bar': [];\n * }\n * const emitter = new EventEmitter<MyEvents>();\n * emitter.on('foo', (x, y) => console.log(x, y));\n * emitter.emit('foo', 42, 'hello');\n * ```\n */\nexport class EventEmitter<TEvents extends Record<string, unknown[]>> {\n private _listeners = new Map<keyof TEvents, ListenerEntry[]>();\n\n on<K extends keyof TEvents>(\n event: K,\n fn: (...args: TEvents[K]) => void,\n context?: unknown,\n ): this {\n return this._add(event, fn as Listener, context, false);\n }\n\n once<K extends keyof TEvents>(\n event: K,\n fn: (...args: TEvents[K]) => void,\n context?: unknown,\n ): this {\n return this._add(event, fn as Listener, context, true);\n }\n\n off<K extends keyof TEvents>(\n event: K,\n fn?: (...args: TEvents[K]) => void,\n context?: unknown,\n ): this {\n const entries = this._listeners.get(event);\n if (!entries) return this;\n\n if (!fn) {\n this._listeners.delete(event);\n return this;\n }\n\n const filtered = entries.filter(\n (e) => e.fn !== fn || (context !== undefined && e.context !== context),\n );\n if (filtered.length === 0) {\n this._listeners.delete(event);\n } else {\n this._listeners.set(event, filtered);\n }\n return this;\n }\n\n emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): boolean {\n const entries = this._listeners.get(event);\n if (!entries || entries.length === 0) return false;\n\n // Snapshot to allow mutations during iteration\n const snapshot = entries.slice();\n for (const entry of snapshot) {\n if (entry.once) {\n // Remove this specific entry by identity. Calling off(fn, context)\n // would drop *every* listener with the same fn reference — including a\n // separate persistent on() registration of the same handler.\n this._removeEntry(event, entry);\n }\n entry.fn.apply(entry.context, args);\n }\n return true;\n }\n\n private _removeEntry(event: keyof TEvents, entry: ListenerEntry): void {\n const entries = this._listeners.get(event);\n if (!entries) return;\n const idx = entries.indexOf(entry);\n if (idx === -1) return;\n entries.splice(idx, 1);\n if (entries.length === 0) this._listeners.delete(event);\n }\n\n removeAllListeners(event?: keyof TEvents): this {\n if (event !== undefined) {\n this._listeners.delete(event);\n } else {\n this._listeners.clear();\n }\n return this;\n }\n\n listenerCount(event: keyof TEvents): number {\n return this._listeners.get(event)?.length ?? 0;\n }\n\n private _add(\n event: keyof TEvents,\n fn: Listener,\n context: unknown,\n once: boolean,\n ): this {\n let entries = this._listeners.get(event);\n if (!entries) {\n entries = [];\n this._listeners.set(event, entries);\n }\n entries.push({ fn, context, once });\n return this;\n }\n}\n","import type { Container } from 'pixi.js';\n\n/**\n * Which screen axis a reel's strip travels along. `'vertical'` runs the strip\n * on Y with reels marched along X; `'horizontal'` runs the strip on X with\n * reels marched along Y. ReelSet-level: one shared orientation per set.\n */\nexport type Orientation = 'vertical' | 'horizontal';\n\n/**\n * Which way along the travel axis symbols move. `'forward'` heads toward the\n * larger coordinate (down / right), `'reverse'` toward the smaller (up / left).\n * Per-reel, overridable per spin.\n */\nexport type Direction = 'forward' | 'reverse';\n\n/**\n * Immutable projection between screen space and a reel's travel/cross axes.\n *\n * One per reel, built by the builder and injected into `ReelMotion`, `Reel`,\n * and every phase. It carries no per-frame allocation on the hot path -\n * `addMain` is one property lookup and one `+=`. See ADR 016 sections 3.1-3.2.\n *\n * The point is to keep direction (\"which way is backwards for this reel\") out\n * of the sign of a raw delta, and orientation (\"which screen axis\") out of a\n * hardcoded `.y`. Public geometry stays screen-space (see `toLocal`/`toScreen`),\n * so third-party `ReelSymbol` subclasses are unaffected.\n */\nexport interface ReelAxis {\n readonly orientation: Orientation;\n readonly direction: Direction;\n /** +1 for 'forward', -1 for 'reverse'. */\n readonly polarity: 1 | -1;\n /** Pixi / GSAP property key for the travel axis. */\n readonly mainProp: 'x' | 'y';\n /** Pixi / GSAP property key for the reel-marching axis. */\n readonly crossProp: 'x' | 'y';\n /** Which strip edge new symbols enter from: 'start' when polarity > 0. */\n readonly feedEdge: 'start' | 'end';\n\n getMain(view: Container): number;\n setMain(view: Container, v: number): void;\n addMain(view: Container, d: number): void;\n getCross(view: Container): number;\n setCross(view: Container, v: number): void;\n /** Screen-space (width, height) to (cross size, main size). */\n toLocal(width: number, height: number): { cross: number; main: number };\n /** (cross, main) to screen-space (x, y). */\n toScreen(cross: number, main: number): { x: number; y: number };\n}\n\nclass Axis implements ReelAxis {\n readonly polarity: 1 | -1;\n readonly mainProp: 'x' | 'y';\n readonly crossProp: 'x' | 'y';\n readonly feedEdge: 'start' | 'end';\n private readonly _vertical: boolean;\n\n constructor(\n readonly orientation: Orientation,\n readonly direction: Direction,\n ) {\n this._vertical = orientation === 'vertical';\n this.mainProp = this._vertical ? 'y' : 'x';\n this.crossProp = this._vertical ? 'x' : 'y';\n this.polarity = direction === 'forward' ? 1 : -1;\n this.feedEdge = this.polarity > 0 ? 'start' : 'end';\n }\n\n getMain(view: Container): number {\n return view[this.mainProp];\n }\n setMain(view: Container, v: number): void {\n view[this.mainProp] = v;\n }\n addMain(view: Container, d: number): void {\n view[this.mainProp] += d;\n }\n getCross(view: Container): number {\n return view[this.crossProp];\n }\n setCross(view: Container, v: number): void {\n view[this.crossProp] = v;\n }\n\n toLocal(width: number, height: number): { cross: number; main: number } {\n return this._vertical ? { cross: width, main: height } : { cross: height, main: width };\n }\n toScreen(cross: number, main: number): { x: number; y: number } {\n return this._vertical ? { x: cross, y: main } : { x: main, y: cross };\n }\n}\n\n/** Build a `ReelAxis` for the given orientation and travel direction. */\nexport function reelAxis(orientation: Orientation, direction: Direction): ReelAxis {\n return new Axis(orientation, direction);\n}\n\n/** The v1 default: a vertical strip travelling downward. */\nexport const VERTICAL_FORWARD: ReelAxis = reelAxis('vertical', 'forward');\n","import type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport { VERTICAL_FORWARD } from './ReelAxis.js';\nimport type { ReelCurve } from './ReelCurve.js';\n\nconst EPS = 1e-9;\n\n/**\n * The physics of one reel: march symbols along the travel axis and wrap them\n * around. Projected through a {@link ReelAxis} so the same code runs vertical\n * or horizontal, forward or reverse; the default axis is vertical/forward,\n * matching every v1 layout.\n *\n * Positions are DERIVED from array index every frame, never accumulated, and\n * the rotation count is DERIVED from total travel. Rigidity, ordering and\n * boundedness are therefore true by construction, and no \"at most one wrap per\n * call\" precondition exists - any step size is legal (ADR 016 section 3.2,\n * ADR 018). `_symbols[0]` is always the symbol nearest the strip's start edge\n * (top for vertical, left for horizontal); each wrap moves the wrapping symbol\n * to the array end that matches its travel so the ordering stays consistent\n * with the grid.\n */\nexport class ReelMotion {\n private _pitch: number;\n private _bufferStart: number;\n private _axis: ReelAxis;\n private _travel = 0; // total signed travel since the last snap\n private _rot = 0; // whole-slot rotations already applied\n private _off = 0; // sub-slot remainder\n\n constructor(\n private _symbols: ReelSymbol[],\n symbolHeight: number,\n symbolGapY: number,\n bufferStart: number,\n _visibleCells: number,\n _bufferEnd: number,\n private _onSymbolWrapped: (symbol: ReelSymbol) => void,\n axis: ReelAxis = VERTICAL_FORWARD,\n private _curve?: ReelCurve,\n ) {\n this._pitch = symbolHeight + symbolGapY;\n this._bufferStart = bufferStart;\n this._axis = axis;\n this._render();\n }\n\n /**\n * Move the strip by `delta` screen pixels along the travel axis (positive =\n * toward the larger coordinate: down for vertical, right for horizontal).\n * `delta` is relative to the reel's direction via the axis polarity, so\n * StartPhase's step-back pull (a negative delta) reads as \"backwards for this\n * reel\" in either direction. Any magnitude is legal.\n */\n advance(delta: number): void {\n if (delta === 0) return;\n this._travel += this._axis.polarity * delta;\n\n // Derive the rotation count from total travel rather than mutating it.\n // Snapping q to a whole number inside EPS lands an exact N-slot travel on\n // rotation N instead of N-1 when float residue leaves it 1e-14 short.\n let q = this._travel / this._pitch;\n const r = Math.round(q);\n if (Math.abs(q - r) < EPS) q = r;\n const targetRot = Math.floor(q);\n\n while (this._rot < targetRot) {\n this._rot++;\n this._rotateToStart();\n }\n while (this._rot > targetRot) {\n this._rot--;\n this._rotateToEnd();\n }\n\n this._off = this._travel - targetRot * this._pitch;\n if (Math.abs(this._off) < EPS) this._off = 0;\n this._render();\n }\n\n /**\n * Swap the curvature in or out at runtime. Symbols projected by the outgoing\n * curve are flattened first, otherwise dropping to `amount: 0` would leave\n * the last quad it handed out still on screen.\n */\n setCurve(curve: ReelCurve | undefined): void {\n if (this._curve && !curve) {\n // Tell every symbol the reel is flat again, otherwise dropping to\n // `amount: 0` leaves the last projection it was given on screen.\n for (let i = 0; i < this._symbols.length; i++) this._symbols[i].applyCellQuad(null);\n }\n this._curve = curve;\n this._render();\n }\n\n /** Snap all symbols to their grid positions (array index = visual cell). */\n snapToGrid(): void {\n this._travel = 0;\n this._rot = 0;\n this._off = 0;\n this._render();\n }\n\n /** The correct main-axis coordinate for a symbol at visual cell `cell`. */\n getCellMain(cell: number): number {\n return (cell - this._bufferStart) * this._pitch;\n }\n\n get slotPitch(): number {\n return this._pitch;\n }\n\n /**\n * Reshape the motion layer for a new visible-cell count and cell pitch.\n * Called by `Reel.reshape()` during AdjustPhase on MultiWays slots; the\n * symbol array is re-bound via the same reference, and `Reel` snaps right\n * after, so this only refreshes the geometry.\n */\n reshape(\n symbolHeight: number,\n symbolGapY: number,\n bufferStart: number,\n _visibleCells: number,\n _bufferEnd: number,\n ): void {\n this._pitch = symbolHeight + symbolGapY;\n this._bufferStart = bufferStart;\n }\n\n private _rotateToStart(): void {\n const s = this._symbols.pop() as ReelSymbol;\n this._symbols.unshift(s);\n this._onSymbolWrapped(s);\n }\n\n private _rotateToEnd(): void {\n const s = this._symbols.shift() as ReelSymbol;\n this._symbols.push(s);\n this._onSymbolWrapped(s);\n }\n\n private _render(): void {\n const off = this._off;\n const curve = this._curve;\n for (let i = 0; i < this._symbols.length; i++) {\n const symbol = this._symbols[i];\n const main = (i - this._bufferStart) * this._pitch + off;\n this._axis.setMain(symbol.view, main);\n // The projection is derived from this same flat coordinate and handed to\n // the symbol as a view-LOCAL quad, so the value we just wrote survives\n // untouched for the reads in `Reel` that recover a slot from a view.\n if (curve) symbol.applyCellQuad(curve.quadFor(main, symbol.cellInset));\n }\n }\n}\n","import type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport type { ReelAxis } from './ReelAxis.js';\n\n/**\n * Fake the curvature of a spinning reel cylinder.\n *\n * The middle cell faces you; the outer ones have rotated away, so their far\n * edge sits further from your eye. A camera turns those into TRAPEZOIDS, not\n * smaller rectangles.\n *\n * ```\n * amount: 0 amount: 0.5\n * +-----------+ +-----------+\n * | A A A | | /-\\ /-\\ | <- far edge narrower: keystone\n * | B B B | | | B | B | | <- faces you, full size\n * | C C C | | \\-/ \\-/ |\n * +-----------+ +-----------+\n * ```\n *\n * @example\n * ```ts\n * builder.curve(0.35); // whole set\n * builder.curvePerReel([0.2, 0.35, 0.5, 0.35, 0.2]); // deeper in the middle\n * ```\n */\nexport interface ReelCurveConfig {\n /**\n * How far round the drum the window sees. `0` flat (default), `1` hard\n * barrel. Drives both the bunching toward the edges and the keystone.\n */\n amount: number;\n /**\n * Perspective strength: how much smaller an edge cell is than the middle one.\n * `0.25` renders the window edge a fifth smaller. `0` is orthographic - cells\n * bunch, nothing recedes, nothing keystones. Defaults to `amount * 0.5`.\n *\n * Clamped below `cos(arc)`, past which the projection folds cells back over\n * each other, so it saturates as `amount` approaches `1`.\n */\n depth?: number;\n}\n\n/** `curve(0.35)` and `curve({ amount: 0.35 })` mean the same thing. */\nexport type ReelCurveInput = number | ReelCurveConfig;\n\n/**\n * Where the camera sits across the strip.\n *\n * - `'reel'` (default). One per reel, dead ahead. Every reel its own drum.\n * Right when the reels read as separate - framed columns, wide gaps.\n * - `'set'`. One in front of the middle of the board. Receding cells also\n * lean IN, so the grid reads as one wide cylinder. Outer reels do the\n * leaning; the middle one barely moves.\n * - `'set-lean'`. Halfway. Usually the sweet spot on a 5-wide board.\n */\nexport type CurveFocus = 'reel' | 'set-lean' | 'set';\n\n/**\n * How the curve is drawn.\n *\n * - `'symbol'` (default). Project each cell alone. Crisp, free, a real\n * keystone - but only for content that IS a texture, because a `Container`\n * transform is affine and can displace a Spine skeleton without bending it.\n * - `'warp'`. Render each reel to a texture, draw it through a mesh whose\n * VERTICES are displaced. Everything inside bends, no symbol cooperates.\n * Costs one render pass per reel per frame and one resample.\n */\nexport type CurveMode = 'symbol' | 'warp';\n\n/** How far each focus mode leans from the reel's centreline toward the set's. */\nexport const CURVE_FOCUS_WEIGHT: Record<CurveFocus, number> = {\n reel: 0,\n 'set-lean': 0.5,\n set: 1,\n};\n\n/**\n * Widest arc `amount: 1` maps to, in radians (~57 degrees). Well under `PI / 2`\n * so `sin` stays monotonic AND `cos(arc)` leaves a usable `depth` range - the\n * fold-over limit is `depth < cos(arc)`, zero at 90 degrees.\n */\nconst MAX_ARC = 1.0;\n\n/** Below this the curve is indistinguishable from flat and the math degenerates. */\nconst MIN_ARC = 1e-4;\n\n/** Fraction of the fold-over limit `depth` is allowed to reach. */\nconst DEPTH_SAFETY = 0.9;\n\n/** Normalize the shorthand and fill in the derived, fold-safe default. */\nexport function resolveCurveConfig(input: ReelCurveInput): Required<ReelCurveConfig> {\n const cfg = typeof input === 'number' ? { amount: input } : input;\n const amount = clamp01(cfg.amount);\n const requested = clamp01(cfg.depth ?? amount * 0.5);\n // `cos(arc)` is where `d/dphi (sin phi / (1 + k(1 - cos phi)))` hits zero and\n // the projection stops being monotonic - past it, cells at the window edge\n // reverse and the strip visibly turns inside out.\n const limit = DEPTH_SAFETY * Math.cos(amount * MAX_ARC);\n return { amount, depth: Math.min(requested, limit) };\n}\n\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return v < 0 ? 0 : v > 1 ? 1 : v;\n}\n\n/**\n * The curvature of one reel: a camera looking at a drum.\n *\n * The strip wraps a cylinder whose radius makes the window cover `2 * arc`\n * radians, with the camera far enough in front that the window edge renders\n * `depth` smaller than the middle. Every cell edge goes through that one\n * model, so the result is a real perspective quad, not a scaled rectangle.\n *\n * It never writes a symbol's `position`. That coordinate is load-bearing -\n * `Reel` reads it back in `beginMotion`, `notifyLanded` and `_replaceSymbol`\n * to recover which slot a symbol is in, and a bent value taken for a flat one\n * compounds on every round trip. The projection is handed over as a view-LOCAL\n * quad instead.\n */\nexport class ReelCurve {\n private readonly _arc: number;\n /** Cylinder radius over camera distance. Drives the perspective divide. */\n private readonly _k: number;\n /** Perspective factor at the window edge. */\n private readonly _edgeScale: number;\n /** Normalization divisor. See the note in the constructor. */\n private readonly _norm: number;\n /** Where the window edge lands, as a fraction of the half-extent. */\n private readonly _edgeMapped: number;\n /** Slope of the projection past the window edge, in flat-coordinate units. */\n private readonly _edgeSlope: number;\n\n private _cellMain = 0;\n private _cellCross = 0;\n private _halfExtent = 0;\n private _radius = 0;\n /**\n * Reel-local cross coordinate the perspective converges on. Defaults to the\n * reel's own centreline; `ReelSetBuilder.curveFocus()` can move it toward\n * the middle of the whole board.\n */\n private _focusCross: number | null = null;\n\n constructor(\n private readonly _config: Required<ReelCurveConfig>,\n private readonly _axis: ReelAxis,\n ) {\n this._arc = _config.amount * MAX_ARC;\n const sinArc = Math.sin(this._arc);\n const cosArc = Math.cos(this._arc);\n // `depth` is defined as the shrink at the window edge, so\n // `1 / (1 + k * (1 - cos arc)) = 1 / (1 + depth)` fixes k directly.\n const versine = 1 - cosArc;\n this._k = versine > 0 ? _config.depth / versine : 0;\n this._edgeScale = this._perspectiveAt(this._arc);\n // Normalize so the MIDDLE of the window is drawn at 1:1: `d/dm` at the\n // centre is `arc / norm`, so `norm = arc` leaves the cell facing the\n // camera at authored size, both axes, no keystone.\n //\n // Normalizing to `sin(arc) * s(arc)` would pin the ends to the window\n // instead, but magnifies the main axis at the centre and not the cross\n // axis - a visibly STRETCHED middle row. So the ends fall short, and the\n // buffer cells fill that band. Positive constant either way, so\n // monotonicity is unaffected.\n this._norm = this._arc > 0 ? this._arc : 1;\n // Where the drum's edge reaches, as a fraction of the half-extent. `1`\n // would touch the window edge; short of that is the band the buffer cells\n // (and your frame art) live in.\n this._edgeMapped = (sinArc * this._edgeScale) / this._norm;\n // d/dphi of `sin(phi) * s(phi)` reduces to `(cos phi (1 + k) - k) * s^2`;\n // normalized, then converted from radians to flat-coordinate units.\n this._edgeSlope =\n this._arc > 0\n ? (this._arc *\n (cosArc * (1 + this._k) - this._k) *\n this._edgeScale *\n this._edgeScale) /\n this._norm\n : 1;\n }\n\n /** The resolved config this curve was built from. */\n get config(): Required<ReelCurveConfig> {\n return this._config;\n }\n\n /** True when the curve is flat enough that projecting anything is a waste. */\n get isFlat(): boolean {\n return this._arc < MIN_ARC;\n }\n\n /**\n * (Re)bind the geometry the projection is defined against. Called on build\n * and from `Reel.reshape()`, which changes both cell size and cell count.\n *\n * @param cellMain main-axis extent of one cell's art\n * @param cellCross cross-axis extent of one cell's art\n * @param pitch main-axis distance between two cell origins (cell + gap)\n * @param visibleCells how many cells the window shows\n */\n setGeometry(cellMain: number, cellCross: number, pitch: number, visibleCells: number): void {\n this._cellMain = cellMain;\n this._cellCross = cellCross;\n // The window spans the first cell's leading edge to the last cell's\n // trailing edge. the trailing gap is not part of it.\n this._halfExtent = (visibleCells * pitch - (pitch - cellMain)) / 2;\n // Wrap the window onto the drum: half the window is `arc` radians of arc\n // length, so `halfExtent = R * arc`.\n this._radius = this._arc > 0 ? this._halfExtent / this._arc : 0;\n }\n\n /**\n * Point the camera somewhere other than this reel's own centreline. Cells\n * converge on THAT point as they recede - what turns five drums into one.\n *\n * @param cross reel-local cross coordinate, or `null` for the reel's centre\n */\n setFocus(cross: number | null): void {\n this._focusCross = cross;\n }\n\n /** The cross coordinate the perspective converges on, reel-local. */\n get focusCross(): number {\n return this._focusCross ?? this._cellCross / 2;\n }\n\n /**\n * Project the cell whose flat leading edge sits at reel-local `mainStart`.\n *\n * Returns `null` when there is nothing to project, so callers can hand the\n * flat case straight through without allocating.\n */\n quadFor(mainStart: number, inset?: ReelCellInset | null): ReelCellQuad | null {\n if (this.isFlat || this._halfExtent <= 0) return null;\n\n // Project the rectangle the ART is really in - for a trimmed atlas frame,\n // much smaller than the cell. Using the whole cell would inflate a small\n // symbol and give it the cell's keystone instead of its own, milder one.\n let mainFrom = mainStart;\n let mainTo = mainStart + this._cellMain;\n let crossFrom = 0;\n let crossTo = this._cellCross;\n if (inset) {\n // The inset is screen-space; project it onto the reel's own axes so\n // \"left/top\" means the right thing on a sideways set too.\n const from = this._axis.toLocal(inset.left, inset.top);\n const to = this._axis.toLocal(inset.right, inset.bottom);\n mainFrom = mainStart + from.main * this._cellMain;\n mainTo = mainStart + to.main * this._cellMain;\n crossFrom = from.cross * this._cellCross;\n crossTo = to.cross * this._cellCross;\n }\n\n const near = this._project(mainFrom);\n const far = this._project(mainTo);\n // Converge on the camera's optical axis. Default is the reel's own\n // centreline, so a cell narrows in place; aimed at the board's middle, a\n // receding cell also LEANS toward it and the reels read as one drum.\n const centre = this.focusCross;\n const nearFrom = centre + (crossFrom - centre) * near.scale;\n const nearTo = centre + (crossTo - centre) * near.scale;\n const farFrom = centre + (crossFrom - centre) * far.scale;\n const farTo = centre + (crossTo - centre) * far.scale;\n // View-local: the view's origin is the flat cell's leading corner.\n const nearMain = near.main - mainStart;\n const farMain = far.main - mainStart;\n\n const a0 = this._axis.toScreen(nearFrom, nearMain);\n const a1 = this._axis.toScreen(nearTo, nearMain);\n const b0 = this._axis.toScreen(farFrom, farMain);\n const b1 = this._axis.toScreen(farTo, farMain);\n\n const origin = this._axis.toScreen(crossFrom, mainFrom - mainStart);\n const size = this._axis.toScreen(crossTo - crossFrom, mainTo - mainFrom);\n const box = { x: origin.x, y: origin.y, width: size.x, height: size.y };\n const vertical = this._axis.orientation === 'vertical';\n\n // Clockwise from screen top-left. Vertical: smaller main is the TOP edge,\n // so the near pair is (TL, TR). Horizontal: it is the LEFT edge, so the\n // near pair is (TL, BL). Art stays upright either way, so the texture's\n // top-left must keep landing on the screen's top-left.\n return vertical\n ? {\n ...box,\n x0: a0.x, y0: a0.y,\n x1: a1.x, y1: a1.y,\n x2: b1.x, y2: b1.y,\n x3: b0.x, y3: b0.y,\n }\n : {\n ...box,\n x0: a0.x, y0: a0.y,\n x1: b0.x, y1: b0.y,\n x2: b1.x, y2: b1.y,\n x3: a1.x, y3: a1.y,\n };\n }\n\n /**\n * Where a flat reel-local main coordinate lands on the drum. Public so\n * `getCellBounds()` and game-drawn overlays follow the curve, not the flat\n * grid behind it.\n */\n mapMain(main: number): number {\n return this._project(main).main;\n }\n\n /**\n * How much smaller the drum renders whatever sits at `main`. `1` at the\n * window's middle, `1 / (1 + depth)` at its edges. Public for the same\n * reason as {@link ReelCurve.mapMain}.\n */\n scaleAt(main: number): number {\n return this._project(main).scale;\n }\n\n /**\n * Project one flat reel-local main coordinate: where it lands, and how much\n * the perspective divide shrinks whatever is there.\n *\n * Inside the window: a point wrapped on the cylinder, pushed through the\n * perspective divide.\n *\n * POSITION continues as a straight line past the window - carrying `sin`\n * beyond its peak folds the buffer back on itself. SCALE does not need that:\n * `1 - cos(phi)` is still climbing out there and nothing folds. Pinning it\n * to the edge value gave every buffer cell two equal edges, i.e. a flat\n * rectangle beside a hard-curved neighbour.\n */\n private _project(main: number): { main: number; scale: number } {\n if (this.isFlat || this._halfExtent <= 0) return { main, scale: 1 };\n const h = this._halfExtent;\n const phi = (main - h) / this._radius;\n // Clamp only the ANGLE, and only at half a turn, where `cos` would turn\n // back on itself. Buffers never reach it at any sane arc; this is just so\n // a pathological strip length cannot un-shrink a cell.\n const scale = this._perspectiveAt(Math.min(Math.abs(phi), Math.PI));\n // Continue from where the window edge ACTUALLY lands - not the window\n // edge itself, now the centre is normalized to 1:1. Anchoring on `2h`/`0`\n // left a jump there and detached the buffer cells from the strip.\n const edge = h * this._edgeMapped;\n if (phi > this._arc) {\n return { main: h + edge + (main - 2 * h) * this._edgeSlope, scale };\n }\n if (phi < -this._arc) {\n return { main: h - edge + main * this._edgeSlope, scale };\n }\n return { main: h + (h * Math.sin(phi) * scale) / this._norm, scale };\n }\n\n /**\n * Perspective divide at arc angle `phi`. A point rotated `phi` round the\n * drum has receded `R * (1 - cos phi)`; the camera shrinks it by that.\n */\n private _perspectiveAt(phi: number): number {\n return 1 / (1 + this._k * (1 - Math.cos(phi)));\n }\n}\n","import type { Ticker } from 'pixi.js';\nimport type { Disposable } from './Disposable.js';\n\nexport type TickerCallback = (ticker: Ticker) => void;\n\n/**\n * Safe wrapper around PixiJS Ticker subscriptions.\n *\n * Solves the #1 memory leak in the original library: dangling ticker callbacks.\n * When `destroy()` is called, ALL registered callbacks are automatically\n * removed from the ticker.\n *\n * Usage:\n * ```ts\n * const ref = new TickerRef(app.ticker);\n * ref.add((ticker) => reel.update(ticker));\n * // Later:\n * ref.destroy(); // all callbacks removed\n * ```\n */\nexport class TickerRef implements Disposable {\n private _callbacks: TickerCallback[] = [];\n private _isDestroyed = false;\n\n constructor(private _ticker: Ticker) {}\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n add(fn: TickerCallback): void {\n if (this._isDestroyed) return;\n this._callbacks.push(fn);\n this._ticker.add(fn);\n }\n\n remove(fn: TickerCallback): void {\n const idx = this._callbacks.indexOf(fn);\n if (idx !== -1) {\n this._callbacks.splice(idx, 1);\n this._ticker.remove(fn);\n }\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n for (const fn of this._callbacks) {\n this._ticker.remove(fn);\n }\n this._callbacks.length = 0;\n this._isDestroyed = true;\n }\n}\n","import { Container, MeshPlane, RenderTexture, type Renderer, type Ticker } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { TickerRef } from '../utils/TickerRef.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport type { ReelCurve } from './ReelCurve.js';\n\n/**\n * Vertices per axis. The quality knob: too few and the drum reads as flat\n * facets, too many and every reel costs geometry for a smooth curve. 16 is well\n * past visible faceting at slot-cell sizes.\n */\nconst GRID = 16;\n\n/**\n * Bend a whole reel, whatever is inside it.\n *\n * {@link ReelCurve}'s per-symbol projection only bends content that IS a\n * texture: a `Container` transform is affine, so Spine skeletons, `Graphics`,\n * text and composites come out moved-but-flat.\n *\n * This renders the reel to a texture and draws it through a mesh whose\n * vertices carry the same projection, so everything inside bends and no symbol\n * cooperates. It also sidesteps the per-symbol path's atlas-frame problem,\n * since a render texture owns its whole source.\n *\n * Costs one render pass per reel per frame and one resample, so hairline art is\n * marginally softer. Symbols are no longer real display objects to the\n * renderer, so use `ReelSet.getCellQuad()` rather than screen-space hit tests.\n */\nexport class ReelWarp extends Container implements Disposable {\n private readonly _mesh: MeshPlane;\n private _texture: RenderTexture;\n private _isDestroyed = false;\n private _width: number;\n private _height: number;\n private readonly _tickerRef: TickerRef;\n\n /**\n * @param _source the reel's own container. It is rendered to a texture rather\n * than drawn, so the caller must keep it OUT of the scene graph\n * @param _renderer the renderer to draw the reel with\n * @param _curve the projection to displace vertices by\n * @param _axis the reel's travel projection\n * @param width screen width of the reel's box\n * @param height screen height of the reel's box\n */\n constructor(\n private readonly _source: Container,\n private readonly _renderer: Renderer,\n private readonly _curve: ReelCurve,\n private readonly _axis: ReelAxis,\n width: number,\n height: number,\n ticker: Ticker,\n private readonly _margin = 0,\n private readonly _bleed = 0,\n ) {\n super();\n // Texture covers the window plus a slot of buffer at each end of the\n // strip: the drum's ends curve away from the window edges and the buffer\n // cells fill that band.\n //\n // `_bleed` does the same ACROSS the strip, for art wider than its cell -\n // an overflowing mystery plate, leaves past the tile. Without it the\n // overhang is sliced at the texture edge.\n const grown = this._axis.toScreen(_bleed * 2, _margin * 2);\n this._width = Math.max(1, Math.ceil(width + Math.abs(grown.x)));\n this._height = Math.max(1, Math.ceil(height + Math.abs(grown.y)));\n this._texture = RenderTexture.create({\n width: this._width,\n height: this._height,\n resolution: _renderer.resolution,\n });\n this._mesh = new MeshPlane({\n texture: this._texture,\n verticesX: GRID,\n verticesY: GRID,\n });\n // `MeshPlane` sizes its grid from the texture and would rebuild it (and\n // undo our displacement) whenever that texture reports a resize.\n this._mesh.autoResize = false;\n this.addChild(this._mesh);\n this._displace();\n // Redraw every tick, not only while the strip moves: win pulses, cascade\n // destroys and the spotlight animate symbols on a reel at rest, and the\n // texture is the only thing the player sees.\n this._tickerRef = new TickerRef(ticker);\n this._tickerRef.add(() => this.update());\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Redraw the reel into its texture. Called once per frame, after the motion\n * layer has moved the symbols and before the stage is drawn.\n */\n update(): void {\n if (this._isDestroyed) return;\n // The container carries the reel's own offset - reel 3 sits 300px along -\n // because everything from `getCellBounds` to the unmasked lift reads it.\n // The off-screen draw needs the strip at the texture's origin instead, or\n // every reel but the first renders outside its texture and comes out blank.\n //\n // Moving the container, not `render`'s `transform` option: that transform\n // does not replace the container's own, so the offset survived it.\n // Restored in the same synchronous block.\n const { x, y } = this._source.position;\n // Park it AT the shift, not offset by it. Buffer cells sit at negative\n // main, so the margin brings them inside the texture.\n const shift = this._axis.toScreen(this._bleed, this._margin);\n this._source.position.set(shift.x, shift.y);\n this._renderer.render({\n container: this._source,\n target: this._texture,\n clear: true,\n });\n this._source.position.set(x, y);\n }\n\n /** Re-measure after a reshape, and re-displace the grid. */\n resize(width: number, height: number): void {\n // Same margin the constructor added, or a reshape drops the buffer slack\n // and clips the drum's ends.\n const grown = this._axis.toScreen(this._bleed * 2, this._margin * 2);\n const w = Math.max(1, Math.ceil(width + Math.abs(grown.x)));\n const h = Math.max(1, Math.ceil(height + Math.abs(grown.y)));\n if (w === this._width && h === this._height) return;\n this._width = w;\n this._height = h;\n this._texture.resize(w, h);\n this._displace();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._isDestroyed = true;\n this._tickerRef.destroy();\n this._mesh.destroy();\n this._texture.destroy(true);\n super.destroy({ children: true });\n }\n\n /**\n * Push every grid vertex through the cylinder projection.\n *\n * Grid is screen-space over the reel's box: map each vertex's MAIN\n * coordinate along the drum, pull its CROSS coordinate toward the camera's\n * axis by the same perspective factor. Once per geometry change - the\n * projection does not move while the strip does.\n */\n private _displace(): void {\n const positions = this._mesh.geometry.positions;\n const focus = this._curve.focusCross;\n const span = GRID - 1;\n for (let i = 0; i < positions.length / 2; i++) {\n const gx = (i % GRID) / span;\n const gy = Math.floor(i / GRID) / span;\n // Screen grid -> axis-relative, so one loop serves both orientations.\n const texel = this._axis.toLocal(gx * this._width, gy * this._height);\n // Texture space starts a margin before the window and a bleed across it;\n // undo both for the reel-local coordinate the projection uses.\n const main = texel.main - this._margin;\n const cross = texel.cross - this._bleed;\n const scale = this._curve.scaleAt(main);\n const screen = this._axis.toScreen(\n focus + (cross - focus) * scale,\n this._curve.mapMain(main),\n );\n positions[i * 2] = screen.x;\n positions[i * 2 + 1] = screen.y;\n }\n this._mesh.geometry.positions = positions;\n }\n}\n","/**\n * The \"what do I land on\" queue for one reel.\n *\n * When a reel enters its stop phase, the `SpinController` loads the\n * target frame (the exact list of symbol ids that should appear on\n * screen, top-to-bottom, including the off-screen buffers). As the reel\n * keeps scrolling downward during deceleration, every `ReelMotion` wrap\n * event asks this sequencer for the next symbol. and it hands them back\n * from the END of the frame first, because new symbols arrive at the\n * top of a reel scrolling downward.\n *\n * After the last symbol is consumed the reel lands, and what you see on\n * screen matches the loaded frame exactly.\n */\nexport class StopSequencer {\n private _frame: string[] = [];\n private _remaining: number = 0;\n private _cursor: number = 0;\n private _step: 1 | -1 = -1;\n\n /**\n * Load a target frame in start-to-end order (top-to-bottom for a vertical\n * reel). `feedEdge` is the strip edge new symbols enter from during the stop:\n * `'start'` for a forward reel (symbols arrive at the start edge and are\n * pushed toward the end, so the frame is consumed end-first) and `'end'` for\n * a reverse reel (symbols arrive at the end edge, so the frame is consumed\n * head-first). Getting this backwards lands a correct-looking frame reversed.\n */\n setFrame(frame: string[], feedEdge: 'start' | 'end' = 'start'): void {\n this._frame = [...frame];\n this._remaining = this._frame.length;\n if (feedEdge === 'end') {\n this._cursor = 0;\n this._step = 1;\n } else {\n this._cursor = this._frame.length - 1;\n this._step = -1;\n }\n }\n\n /**\n * Deliver the next symbol, consumed from the feed-appropriate end.\n *\n * Throws when the frame is exhausted. Every caller is expected to gate on\n * {@link hasRemaining} first (the library's only one, `Reel._replaceSymbol`,\n * does). The old behaviour returned `_frame[0]`, or `''` after a `reset()` —\n * a symbol id that resolves to nothing, so an over-consuming caller landed a\n * silently wrong frame instead of failing where the bug was.\n */\n next(): string {\n if (this._remaining === 0) {\n throw new Error(\n 'StopSequencer.next(): frame exhausted. Gate on `hasRemaining` before ' +\n 'calling, or reload a frame with `setFrame()`.',\n );\n }\n this._remaining--;\n const value = this._frame[this._cursor];\n this._cursor += this._step;\n return value;\n }\n\n get hasRemaining(): boolean {\n return this._remaining > 0;\n }\n\n get remaining(): number {\n return this._remaining;\n }\n\n /** Drop the loaded frame and return to the just-constructed state. */\n reset(): void {\n this._frame = [];\n this._remaining = 0;\n this._cursor = 0;\n this._step = -1;\n }\n}\n","/**\n * Per-reel target shape for `ReelSet.setResult` and\n * `ReelSetBuilder.initialFrame`. One object per reel.\n *\n * Use this for every result grid that crosses a worker, network, or\n * serializer boundary. The shape survives `structuredClone`, JSON, and\n * `postMessage` round-trips.\n */\nexport interface ColumnTarget {\n /** Visible-area target symbols, indexed `0 ... visibleCells-1`. */\n visible: string[];\n /**\n * Buffer-above target symbols. `bufferStart[0]` is the slot closest to the\n * visible top cell; later indices go further above. Up to `bufferSymbols`\n * entries are honored.\n *\n * Big-symbol anchors may sit here. Place a multi-cell symbol id (one whose\n * `SymbolData.size.cells > 1`) at any `bufferStart[i]` and the coordinator\n * paints OCCUPIED stubs across the rest of the block, including any cells\n * that fall in visible. The block must fit on the strip end-to-end\n * (`anchor.cell + h <= visibleCells + bufferEnd`); the portion above\n * visible is clipped by the reel mask. This is the \"tail-visible\"\n * partial-landing pattern.\n */\n bufferStart?: (string | undefined)[];\n /**\n * Buffer-below target symbols. `bufferEnd[0]` is the slot closest to the\n * visible bottom cell; later indices go further below. Up to `bufferSymbols`\n * entries are honored.\n *\n * Big-symbol stubs may sit here. A block anchored at the last visible cell\n * with `h > 1` will have its non-anchor cells spill into `bufferEnd`\n * automatically. You can also place an anchor here, but the block then\n * lies entirely off-screen (legal but invisible).\n */\n bufferEnd?: (string | undefined)[];\n}\n\n/**\n * Read one slot of a `ColumnTarget` by **cell**, the engine's\n * visible-relative coordinate: `0` is the first visible cell, negative cells\n * address `bufferStart` (`-1` is the slot closest to the visible top cell),\n * and cells `>= visible.length` address `bufferEnd`.\n *\n * Returns `undefined` for any cell the target does not specify.\n */\nexport function getTargetSlot(target: ColumnTarget, cell: number): string | undefined {\n if (cell < 0) return target.bufferStart?.[-1 - cell];\n if (cell < target.visible.length) return target.visible[cell];\n return target.bufferEnd?.[cell - target.visible.length];\n}\n\n/**\n * Write one slot of a `ColumnTarget` by cell, using the same coordinate as\n * {@link getTargetSlot}. Creates and extends `bufferStart` / `bufferEnd`\n * as needed, so a caller can address any cell on the strip without knowing\n * whether the target declared a buffer.\n *\n * Mutates `target`. Clone first if the caller must not touch the original.\n */\nexport function setTargetSlot(target: ColumnTarget, cell: number, id: string): void {\n if (cell < 0) {\n (target.bufferStart ??= [])[-1 - cell] = id;\n } else if (cell < target.visible.length) {\n target.visible[cell] = id;\n } else {\n (target.bufferEnd ??= [])[cell - target.visible.length] = id;\n }\n}\n\n/**\n * Materialize a `ColumnTarget` into **strip form**: one entry per strip\n * slot, top to bottom. Index `0` is the furthest buffer-above cell, index\n * `bufferStart` is the first visible cell, and the tail holds buffer-below\n * cells. This is the same indexing `FrameBuilder.build` returns and\n * `Reel.placeStrip` consumes.\n *\n * Entries the target does not specify come back `undefined`; the caller\n * decides what to do with them (the engine random-fills).\n *\n * `bufferStart` is the reel's buffer-above *capacity*. Target entries past\n * it cannot reach the strip and are dropped here. `assertBufferCountsInRange`\n * rejects them at the public entry points so that drop is never silent.\n */\nexport function columnTargetToStrip(\n target: ColumnTarget,\n bufferStart: number,\n): (string | undefined)[] {\n const belowLength = target.bufferEnd?.length ?? 0;\n const strip = new Array<string | undefined>(\n bufferStart + target.visible.length + belowLength,\n );\n for (let i = 0; i < strip.length; i++) {\n strip[i] = getTargetSlot(target, i - bufferStart);\n }\n return strip;\n}\n\n/** Deep-clone a `ColumnTarget` one level down, so slots can be rewritten safely. */\nexport function cloneColumnTarget(target: ColumnTarget): ColumnTarget {\n return {\n visible: [...target.visible],\n bufferStart: target.bufferStart ? [...target.bufferStart] : undefined,\n bufferEnd: target.bufferEnd ? [...target.bufferEnd] : undefined,\n };\n}\n\n/**\n * Validate that a target grid does not carry more `bufferStart` / `bufferEnd`\n * entries than the engine can consume. Throws a `RangeError` with a\n * column-pointing message if it does; otherwise a no-op.\n *\n * Background: without this check the failure is silent. `columnTargetToStrip`\n * only lays down as many buffer slots as the reel actually has, so an entry\n * past that capacity never reaches the strip. Failing here at the entry point\n * is cheaper than a \"why did not my target land\" debugging session.\n *\n * `callerLabel` shows up in the thrown message so the caller knows which\n * public API surfaced the error.\n */\n/**\n * Throw a readable error when a caller hands `setResult` / `initialFrame`\n * the pre-v2 `string[][]` instead of `ColumnTarget[]`.\n *\n * Without this the first thing to touch the value is a spread of\n * `target.visible`, so the caller gets `TypeError: target.visible is not\n * iterable` from deep inside the frame pipeline -- and, worse, the spin\n * promise never settles, because the throw happens after the reels are\n * already moving. The symptom is a reel that spins forever with no clue\n * why. A recipe on the docs site shipped exactly that.\n */\nexport function assertColumnTargets(grid: unknown, callerLabel: string): asserts grid is ColumnTarget[] {\n if (!Array.isArray(grid)) {\n throw new TypeError(`${callerLabel}: expected ColumnTarget[], got ${typeof grid}.`);\n }\n for (let c = 0; c < grid.length; c++) {\n const item = grid[c];\n if (Array.isArray(item)) {\n throw new TypeError(\n `${callerLabel}: column ${c} is a plain string[]. ${callerLabel} takes ColumnTarget[] ` +\n `- wrap each column: grid.map((visible) => ({ visible })).`,\n );\n }\n if (item === null || typeof item !== 'object' || !Array.isArray((item as ColumnTarget).visible)) {\n throw new TypeError(\n `${callerLabel}: column ${c} has no 'visible' array. Each column is ` +\n `{ visible: string[], bufferStart?: string[], bufferEnd?: string[] }.`,\n );\n }\n }\n}\n\nexport function assertBufferCountsInRange(\n grid: ColumnTarget[],\n bufferStartPerReel: ReadonlyArray<number>,\n bufferEndPerReel: ReadonlyArray<number>,\n callerLabel: string,\n): void {\n for (let c = 0; c < grid.length; c++) {\n const maxAbove = bufferStartPerReel[c] ?? 0;\n const maxBelow = bufferEndPerReel[c] ?? 0;\n const item = grid[c];\n // Validate by the highest DEFINED index, not raw `.length`. A sparse array\n // (e.g. ['X', undefined, undefined], as serializers that pre-size arrays\n // produce) materializes only its defined entries, so its length must not\n // trip the guard. A defined entry at index >= max IS dropped downstream\n // (only slots 0..max-1 are consumed), so that index is the real ceiling.\n const aboveMax = highestDefinedIndex(item.bufferStart);\n const belowMax = highestDefinedIndex(item.bufferEnd);\n // `highestDefinedIndex` returns -1 for \"no entries at all\", so a bare\n // `max >= capacity` test fires on an EMPTY buffer whenever capacity is\n // negative -- which a reel reports transiently mid-cascade, when its\n // strip is shorter than bufferStart + visibleCells. A column that\n // specifies nothing can never have an entry dropped, so it is always\n // in range.\n if (aboveMax >= 0 && aboveMax >= maxAbove) {\n throw new RangeError(\n `${callerLabel} column ${c}: bufferStart has a symbol at index ${aboveMax}, ` +\n `beyond engine bufferSymbols=${maxAbove}; it would be silently dropped. ` +\n `Increase bufferSymbols(...) on the builder or remove the extra entry.`,\n );\n }\n if (belowMax >= 0 && belowMax >= maxBelow) {\n throw new RangeError(\n `${callerLabel} column ${c}: bufferEnd has a symbol at index ${belowMax}, ` +\n `beyond engine bufferSymbols=${maxBelow}; it would be silently dropped. ` +\n `Increase bufferSymbols(...) on the builder or remove the extra entry.`,\n );\n }\n }\n}\n\n/** Highest index holding a defined value, or -1 if the array is empty/undefined. */\nfunction highestDefinedIndex(arr: (string | undefined)[] | undefined): number {\n if (!arr) return -1;\n for (let i = arr.length - 1; i >= 0; i--) {\n if (arr[i] !== undefined) return i;\n }\n return -1;\n}\n","import type { SpinningMode } from './SpinningMode.js';\n\n/**\n * Standard top-to-bottom reel spinning.\n * Symbols scroll downward at constant speed, wrapping around.\n */\nexport class StandardMode implements SpinningMode {\n readonly name = 'standard';\n\n computeDelta(slotPitch: number, speed: number, deltaMs: number): number {\n const raw = (slotPitch * speed * deltaMs) / 1000;\n // Cap displacement to half a slot in either direction. ReelMotion no longer\n // requires this for correctness (it derives rotation from total travel), but\n // a half-slot cap keeps per-frame motion smooth and bounds pathological\n // deltaMs spikes; the sign carries StartPhase's step-back pull unchanged.\n const cap = slotPitch / 2;\n return Math.max(Math.min(raw, cap), -cap);\n }\n}\n","import { Container, type Renderer, type Ticker } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { SymbolFactory } from '../symbols/SymbolFactory.js';\nimport type { SymbolData, Stacking } from '../config/types.js';\nimport { ReelMotion } from './ReelMotion.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport { VERTICAL_FORWARD } from './ReelAxis.js';\nimport { ReelCurve, resolveCurveConfig, type ReelCurveInput } from './ReelCurve.js';\nimport { ReelWarp } from './ReelWarp.js';\nimport { StopSequencer } from './StopSequencer.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelEvents } from '../events/ReelEvents.js';\nimport type { DrawSlot, RandomSymbolProvider } from '../frame/RandomSymbolProvider.js';\nimport { columnTargetToStrip, type ColumnTarget } from '../frame/ColumnTarget.js';\nimport type { ReelViewport } from './ReelViewport.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport { StandardMode } from '../spin/modes/StandardMode.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * Upper bound (ms) on a single `update()` delta. Matches Pixi's default\n * minFPS-derived `maxElapsedMS`; bounds spin displacement when a backgrounded\n * tab refocuses or a non-Pixi ticker reports a huge delta.\n */\nconst MAX_TICK_MS = 100;\n\n/**\n * Options for `Reel.nudge()` / `ReelSet.nudge()`. a post-stop reposition\n * that shifts the reel by `distance` symbol positions and reveals new\n * caller-supplied symbols.\n *\n * Nudges run only while the reel is at rest (post-stop). Calling on a\n * moving reel throws.\n */\nexport interface NudgeOptions {\n /**\n * Number of full symbol positions to shift. Must be a positive integer\n * strictly less than the reel's total strip capacity\n * (`bufferStart + visibleCells + bufferEnd`). `incoming.length` must\n * equal this exactly.\n */\n distance: number;\n /**\n * Travel direction, **relative to the reel's own axis**.\n *\n * - `'forward'`. the strip travels the way this reel normally spins.\n * On a vertical/forward reel that is downward, with new symbols\n * entering from the top.\n * - `'reverse'`. the strip travels the other way, with new symbols\n * entering from the opposite edge.\n *\n * Which screen edge feeds the reel is derived from the axis polarity,\n * so a reel built with `direction('reverse')` nudges upward on\n * `'forward'` without the caller re-deriving anything.\n */\n direction: 'forward' | 'reverse';\n /**\n * Symbol ids in **start-to-end order of their final on-strip position**\n * (top-down for vertical, left-to-right for horizontal), including any\n * overflow into the off-screen buffer. Length must equal `distance`\n * exactly.\n *\n * - `incoming[0]` ends up at the start-most new position. When the\n * reel feeds from its start edge this is the new first visible cell\n * (or, if `distance > bufferStart + visibleCells`, spills into\n * bufferEnd tail-first via the trailing entries). When it feeds from\n * the end edge and `distance > visibleCells`, `incoming[0]` lands in\n * bufferStart (still start-most).\n * - `incoming[distance-1]` ends up at the end-most new position.\n * Mirror of the above.\n *\n * For the common case of `distance <= visibleCells`, every entry is a\n * visible cell in strip order and you can ignore the overflow rules.\n */\n incoming: string[];\n /** Total animation duration in ms. Defaults to `200 * distance`. */\n duration?: number;\n /**\n * GSAP easing function name. Defaults to `'power2.out'`. a smooth\n * deceleration with NO overshoot. If you pass an overshooting ease\n * (`back.out(N)`, `elastic.out(...)`), the engine clamps the displacement\n * so wraps never fire past the landing position; the eased value is\n * computed but the strip's travel is bounded.\n */\n ease?: string;\n /**\n * Optional delay (ms) before the tween begins. Validation throws fire\n * immediately on the call, but the actual reel mutation + tween are\n * deferred by this much. Useful with `Promise.all([...])` to stagger\n * parallel nudges:\n *\n * ```ts\n * await Promise.all(\n * reels.map((reel, i) =>\n * reelSet.nudge(reel, { ..., startDelay: i * 80 }),\n * ),\n * );\n * ```\n *\n * `ReelSet.nudge(reel, options, { stagger })` is sugar for the common\n * uniform-stagger case.\n */\n startDelay?: number;\n /**\n * Abort the nudge mid-flight. If signalled before the tween starts, the\n * call rejects with an `AbortError` and no strip mutation happens. If\n * signalled during the tween, the tween is killed, the strip is snapped\n * to its post-nudge position (deterministic landing. the contract is\n * \"incoming lands at these positions\"), and the promise rejects with an\n * `AbortError`. `nudge:cancelled` fires on the reel-set bus.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Internal placeholder for OCCUPIED cells inside a big-symbol block. Has\n * no animation, no rendering. its view is invisible. Not registered in\n * `SymbolFactory`; allocated directly by `Reel` and disposed with it.\n */\nclass OccupiedStub extends ReelSymbol {\n protected onActivate(): void { this.view.alpha = 0; this.view.visible = false; }\n protected onDeactivate(): void {}\n async playWin(): Promise<void> {}\n stopAnimation(): void {}\n resize(): void {}\n}\n\nexport interface ReelConfig {\n reelIndex: number;\n visibleCells: number;\n bufferStart: number;\n bufferEnd: number;\n symbolWidth: number;\n symbolHeight: number;\n symbolGapX: number;\n symbolGapY: number;\n symbolsData: Record<string, SymbolData>;\n initialSymbols: string[];\n /**\n * Y offset of this reel relative to the viewport's top edge. Set by the\n * builder so jagged shapes (pyramids) align according to `reelAnchor`.\n * Default 0.\n */\n mainOffset?: number;\n /** Travel projection for this reel. Defaults to vertical/forward. */\n axis?: ReelAxis;\n /**\n * Pixel height of this reel's box. Used for MultiWays cell-height\n * derivation (`extent / visibleCells`). Defaults to\n * `visibleCells * symbolHeight`.\n */\n extent?: number;\n /**\n * SPIN-time uniform cell height. During SPIN every reel uses this same\n * height. AdjustPhase later swaps to per-reel `extent / visibleCells`.\n * Defaults to `symbolHeight`.\n */\n spinCellSize?: number;\n /**\n * Render order of cells within the reel. Default `'ascending'`. the cell\n * at the larger main coordinate draws in front.\n */\n /**\n * The gsap instance this reel's tweens live on. Defaults to the one\n * resolved at lib-load time; `ReelSetBuilder.gsap(...)` overrides it PER\n * SET, so two sets on one stage can use different instances.\n */\n gsap?: Gsap;\n cellStacking?: Stacking;\n /**\n * Render order of reels within the set. Default `'ascending'`. the last\n * reel draws in front.\n */\n reelStacking?: Stacking;\n /**\n * Cylinder curvature for this reel. Omitted or `0` leaves it dead flat and\n * costs nothing. See {@link ReelCurveConfig}.\n */\n curve?: ReelCurveInput;\n /**\n * Reel-local cross coordinate the curve's perspective converges on. Set by\n * the builder from `curveFocus(...)`; omitted means the reel's own centre.\n */\n curveFocus?: number;\n /**\n * Renderer to draw the reel's warp texture with. Present only when the set\n * was built with `curveMode('warp')` and a `renderer(...)`; its presence is\n * what switches this reel from the per-symbol projection to the whole-reel\n * vertex warp.\n */\n curveRenderer?: Renderer;\n /** Ticker driving the warp's per-frame texture refresh. Warp mode only. */\n curveTicker?: Ticker;\n /**\n * Cross-axis room, in pixels per side, for art that is wider than its cell.\n * Warp mode only; set by `ReelSetBuilder.curveBleed()`.\n */\n curveBleed?: number;\n}\n\n/**\n * Internal sentinel marking non-anchor cells of a big symbol's block.\n * Never crosses the public API. `getVisibleSymbols()` resolves it to the\n * anchor's id.\n */\nexport const OCCUPIED_SENTINEL = '__pixi_reels_occupied__';\n\n/**\n * One vertical column of a slot board.\n *\n * A `Reel` owns:\n * - the `ReelSymbol[]` currently on screen (a small buffer above the\n * visible cells + the visible cells + a small buffer below. so symbols\n * can fade in from off-screen cleanly)\n * - the `ReelMotion` that adds a Y delta each tick and wraps symbols\n * that scroll off the ends\n * - a `StopSequencer`. the queue of target symbols the reel still has\n * to land on before it can stop\n *\n * You generally do not touch a `Reel` directly. Drive the `ReelSet` and\n * let it fan out. Reels are exposed on `reelSet.reels` so you can read\n * the current grid (`reel.getSymbolAt(cell)`) or listen to per-reel\n * events (`phase:enter`, `landed`, `symbol:created`, ...).\n */\nexport class Reel implements Disposable {\n public readonly container: Container;\n public readonly events: EventEmitter<ReelEvents>;\n public readonly reelIndex: number;\n\n /** Current symbols in order (top buffer → visible → bottom buffer). */\n public symbols: ReelSymbol[];\n\n /** Current spin speed (pixels per frame). Set by phases. */\n public speed: number = 0;\n\n /** Current spinning mode. */\n public spinningMode: SpinningMode = new StandardMode();\n\n /**\n * The reel's motion layer.\n *\n * @internal `ReelMotion` was hidden from the package entry in 1.0.0 (PR #140)\n * along with `StopSequencer` and `RandomSymbolProvider`. This field being\n * public re-exposed the type through `dist/core/Reel.d.ts` and semver-locked\n * it into 2.x anyway. Public geometry lives on `ReelSet.getCellBounds()` /\n * `getBlockBounds()` and `Reel.cellMain` / `.extent` / `.mainOffset`.\n */\n public readonly motion: ReelMotion;\n /**\n * The reel's target-frame queue for the current stop.\n *\n * @internal Same as `motion`: hidden as a type in 1.0.0, re-exposed by this\n * field. Consumers drive landing through `setResult()` / `slamStop()`.\n */\n public readonly stopSequencer: StopSequencer;\n private readonly _axis: ReelAxis;\n /** Cylinder curvature, or `undefined` when this reel is flat. */\n private _curve?: ReelCurve;\n /** Reel-local cross coordinate the curve converges on. `null` = own centre. */\n private readonly _curveFocus: number | null;\n /**\n * Whole-reel vertex warp, when the set is in `curveMode('warp')`. Present\n * means the reel container is rendered to a texture and drawn through a\n * displaced mesh instead of being drawn directly, and symbols are left\n * completely alone - the warp bends all of them at once.\n */\n private _warp?: ReelWarp;\n /** True when this reel is drawn through a whole-reel warp. */\n private readonly _warping: boolean;\n private readonly _mainCell: number;\n private readonly _mainGap: number;\n private readonly _crossGap: number;\n /** This reel's cell extent along the strip. Varies per reel; reshape mutates it. */\n private _cellMain: number;\n /** This reel's cell extent across the strip. Uniform across the set. */\n private readonly _cellCross: number;\n\n private _symbolFactory: SymbolFactory;\n private _randomProvider: RandomSymbolProvider;\n private _viewport: ReelViewport;\n private _symbolsData: Record<string, SymbolData>;\n private _visibleCells: number;\n private _bufferStart: number;\n private _mainOffset: number;\n private readonly _gsap: Gsap;\n private _cellStacking: Stacking;\n private _reelStacking: Stacking;\n private _extent: number;\n private _spinCellSize: number;\n private _symbolGapY: number;\n private _symbolGapX: number;\n private _isDestroyed = false;\n private _isStopping = false;\n\n /**\n * True between `notifySpinStart()` and `notifySpinEnd()`. While set,\n * `_replaceSymbol` fires `onReelSpinStart(true)` on each freshly\n * installed symbol so it can join the spin presentation (pool recycling\n * wipes per-instance state, so the symbol can't know on its own).\n */\n private _spinPresentationActive = false;\n private _anticipationActive = false;\n /**\n * True only while the reel is fully at rest (build time, and from\n * `notifyLanded()` until the next `notifySpinStart()`). NOT the inverse\n * of `_spinPresentationActive`: that flag drops at `notifySpinEnd()`,\n * just before the bounce, while the strip is still visibly moving and\n * the stop sequencer is still installing the result symbols.\n */\n private _atRest = true;\n private _isNudging = false;\n /**\n * Symbol-id queue consulted by `_onSymbolWrapped` during a nudge. Each\n * wrap pulls one id from the front; when empty (or `null`), the wrap\n * falls back to `stopSequencer` (if `_isStopping`) or `_randomProvider`.\n *\n * Populated by `nudge()` and cleared once the tween completes.\n */\n private _nudgeQueue: string[] | null = null;\n /**\n * GSAP tween handle for the active nudge animation. Stored so `destroy()`\n * and `skipNudge()` can `kill()` it cleanly; cleared in `onComplete` and\n * on cancellation. `null` between nudges.\n */\n private _nudgeTween: ReturnType<Gsap['to']> | null = null;\n /**\n * Rejection function for the in-flight nudge's promise. Called by\n * `destroy()` and `signal.abort()` so consumers `await`-ing the nudge\n * see a deterministic error instead of a hung promise. Cleared on\n * `onComplete`. `null` between nudges.\n */\n private _nudgeReject: ((err: Error) => void) | null = null;\n /**\n * Internal stub instances reused for OCCUPIED cells inside a big-symbol\n * block. Allocated on demand (one per concurrent OCCUPIED cell on this\n * reel), never pooled through `SymbolFactory`. The views are invisible.\n * the anchor symbol is sized up to cover the whole block.\n */\n private _occupiedStubs: OccupiedStub[] = [];\n /**\n * Per-cell marker recording which cells are non-anchor cells of a big\n * symbol. Populated when frames are placed; consulted by `getVisibleSymbols`\n * and `getSymbolAt` so anchor identity propagates through the block.\n *\n * Indexed by visible-cell 0..visibleCells-1. Each entry is `null` for a\n * normal cell, or `{ anchorCell }` for a cell occupied by another cell's\n * anchor.\n */\n private _occupancy: Array<{ anchorCell: number } | null> = [];\n /**\n * Optional resolver for cross-reel OCCUPIED cells. Set by `ReelSet` so\n * `getVisibleSymbols()` returns the anchor's id even when the anchor\n * lives on a different reel (a 2x2 bonus straddles reels c, c+1).\n * Without it, cross-reel OCCUPIED cells return the OCCUPIED sentinel.\n */\n private _crossReelResolver: ((reel: number, cell: number) => string) | null = null;\n\n constructor(\n config: ReelConfig,\n symbolFactory: SymbolFactory,\n randomProvider: RandomSymbolProvider,\n viewport: ReelViewport,\n ) {\n this.reelIndex = config.reelIndex;\n this._symbolFactory = symbolFactory;\n this._randomProvider = randomProvider;\n this._viewport = viewport;\n this._symbolsData = config.symbolsData;\n this._visibleCells = config.visibleCells;\n this._bufferStart = config.bufferStart;\n this._mainOffset = config.mainOffset ?? 0;\n this._gsap = config.gsap ?? DEFAULT_GSAP;\n this._cellStacking = config.cellStacking ?? 'ascending';\n this._reelStacking = config.reelStacking ?? 'ascending';\n this._symbolGapY = config.symbolGapY;\n this._symbolGapX = config.symbolGapX;\n this._occupancy = new Array(config.visibleCells).fill(null);\n this.events = new EventEmitter<ReelEvents>();\n this.stopSequencer = new StopSequencer();\n\n // Create container positioned at the reel's X column. Sortable so that\n // per-symbol zIndex (set from symbolData.zIndex + visual cell) controls\n // render order. bottom-cell symbols render in front, and flagged \"big\"\n // symbols like wild/bonus can override to render above neighbors.\n this._axis = config.axis ?? VERTICAL_FORWARD;\n // The reel stores its cell size AXIS-RELATIVE, not as screen width and\n // height. `cellMain` is the extent along the strip and is the value a\n // pyramid or MultiWays reshape varies per reel; `cellCross` is the\n // reel-marching extent and is uniform across the set. Screen dimensions\n // are projected back out of the pair whenever art has to be resized, so\n // a jagged horizontal set varies WIDTH where a vertical one varies\n // height, from the same arithmetic.\n const cell = this._axis.toLocal(config.symbolWidth, config.symbolHeight);\n const gap = this._axis.toLocal(config.symbolGapX, config.symbolGapY);\n this._cellMain = cell.main;\n this._cellCross = cell.cross;\n this._mainGap = gap.main;\n this._crossGap = gap.cross;\n this._extent = config.extent ?? config.visibleCells * cell.main;\n this._spinCellSize = config.spinCellSize ?? cell.main;\n this._mainCell = this._spinCellSize;\n const crossPitch = cell.cross + gap.cross;\n this.container = new Container();\n this.container.sortableChildren = true;\n // Cross axis marches the reels; the main axis carries the reel's own\n // offset. For vertical this is (x = column, y = mainOffset), unchanged.\n this._axis.setCross(this.container, config.reelIndex * crossPitch);\n this._axis.setMain(this.container, this._mainOffset);\n // Explicit zIndex so the reel's layer in `ReelViewport.maskedContainer`\n // (sortableChildren = true) is deterministic. Rightmost reel draws on\n // top by default. same visual order as insertion, but now set via\n // zIndex so callers can flip it for bottom-left diagonal overflow.\n this.container.zIndex =\n this._reelStacking === 'ascending'\n ? config.reelIndex\n : -config.reelIndex;\n\n // Create initial symbols. Use spinCellSize so during SPIN every reel\n // uses the same uniform cell height regardless of post-AdjustPhase shape.\n this.symbols = config.initialSymbols.map((symbolId, cell) => {\n const symbol = symbolFactory.acquire(symbolId);\n const spinSize = this._screenSize(this._spinCellSize, this._cellCross);\n symbol.resize(spinSize.width, spinSize.height);\n return symbol;\n });\n\n // Curvature is bound to the same geometry the motion layer marches on, so\n // the bend follows a MultiWays reshape without a second source of truth.\n this._curveFocus = config.curveFocus ?? null;\n // In warp mode the whole reel is bent at once, so the motion layer must NOT\n // also hand each symbol a quad - that would curve the strip twice - and the\n // projection is left un-normalized so it magnifies both axes equally.\n // Recorded BEFORE the initial placement. `_warp` itself cannot be built\n // until the symbols exist, and keying off it meant `_setupSymbolPositions`\n // still handed every symbol a per-symbol quad, which the warp then bent a\n // second time - the strip came out shrunk into the middle of its texture.\n this._warping = config.curveRenderer !== undefined && config.curve !== undefined;\n this._curve = this._buildCurve(config.curve, this._mainCell, config.visibleCells);\n const warping = this._warping;\n\n // Create motion handler. SPIN-time slot height is `spinCellSize`;\n // AdjustPhase reshapes motion to the per-reel cell height.\n this.motion = new ReelMotion(\n this.symbols,\n this._mainCell,\n this._mainGap,\n config.bufferStart,\n config.visibleCells,\n config.bufferEnd,\n (symbol) => this._onSymbolWrapped(symbol),\n this._axis,\n warping ? undefined : this._curve,\n );\n\n this._setupSymbolPositions(config);\n\n if (warping && config.curveRenderer && config.curveTicker && this._curve) {\n // Swap the reel out of the scene for its warp. The container keeps its\n // position and offsets - everything from `getCellBounds` to the unmasked\n // lift still reads them - it is simply drawn off-screen from now on.\n const box = this._screenSize(this._extent, this._cellCross);\n this._warp = new ReelWarp(\n this.container,\n config.curveRenderer,\n this._curve,\n this._axis,\n box.width,\n box.height,\n config.curveTicker,\n this.motion.slotPitch,\n config.curveBleed ?? 0,\n );\n this._warp.zIndex = this.container.zIndex;\n this._axis.setCross(this._warp, this._axis.getCross(this.container));\n this._axis.setMain(this._warp, this._axis.getMain(this.container));\n this._viewport.maskedContainer.removeChild(this.container);\n this._viewport.maskedContainer.addChild(this._warp);\n }\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n get isStopping(): boolean {\n return this._isStopping;\n }\n\n set isStopping(value: boolean) {\n this._isStopping = value;\n }\n\n /** True while a `nudge()` tween is in flight on this reel. */\n get isNudging(): boolean {\n return this._isNudging;\n }\n\n get bufferStart(): number {\n return this._bufferStart;\n }\n\n get bufferEnd(): number {\n return this.symbols.length - this._bufferStart - this._visibleCells;\n }\n\n get visibleCells(): number {\n return this._visibleCells;\n }\n\n /**\n * This reel's cell width in SCREEN pixels - the first argument to\n * `ReelSymbol.resize`. On a vertical set this is the cross extent and is\n * constant; on a horizontal set it is the MAIN extent, so a pyramid or\n * MultiWays reshape moves it.\n */\n get symbolWidth(): number {\n return this._axis.toScreen(this._cellCross, this._cellMain).x;\n }\n\n /**\n * This reel's cell height in SCREEN pixels - the second argument to\n * `ReelSymbol.resize`. Mirror of {@link Reel.symbolWidth}: on a vertical\n * set this is the main extent and a MultiWays reshape moves it; on a\n * horizontal set it is the constant cross extent.\n *\n * During SPIN the main extent is still `spinCellSize`; the per-reel target\n * comes into effect when AdjustPhase commits the reshape.\n */\n get symbolHeight(): number {\n return this._axis.toScreen(this._cellCross, this._cellMain).y;\n }\n\n /**\n * Project an axis-relative (main, cross) pair back to the screen\n * `(width, height)` that `ReelSymbol.resize` takes. The single place the\n * engine converts back, so a jagged horizontal set varies width from\n * exactly the arithmetic a vertical one uses to vary height.\n */\n private _screenSize(main: number, cross: number): { width: number; height: number } {\n const s = this._axis.toScreen(cross, main);\n return { width: s.x, height: s.y };\n }\n\n /**\n * Screen size of an `reels x cells` block, gaps included. `reels` always\n * spans the cross axis and `cells` the main axis, so the screen width and\n * height this maps to swap between orientations.\n */\n private _blockSize(reels: number, cells: number): { width: number; height: number } {\n return this._screenSize(\n cells * this._cellMain + (cells - 1) * this._mainGap,\n reels * this._cellCross + (reels - 1) * this._crossGap,\n );\n }\n\n /** This reel's cell extent ALONG the strip. A reshape moves it. */\n get cellMain(): number {\n return this._cellMain;\n }\n\n /** This reel's cell extent ACROSS the strip. Uniform across the set. */\n get cellCross(): number {\n return this._cellCross;\n }\n\n /** The inter-cell gap along the strip (symbolGap.y vertical, .x horizontal). */\n get mainGap(): number {\n return this._mainGap;\n }\n\n /** The inter-reel gap across the strip (symbolGap.x vertical, .y horizontal). */\n get crossGap(): number {\n return this._crossGap;\n }\n\n /** Pixel extent of this reel's box along the strip. Set by builder. */\n get extent(): number {\n return this._extent;\n }\n\n /** Y offset of this reel relative to the viewport top. Set by builder, immutable. */\n get mainOffset(): number {\n return this._mainOffset;\n }\n\n /**\n * SPIN-time uniform cell height. All reels in a slot use this value during\n * the SPIN phase regardless of their per-reel `symbolHeight`. Frozen at\n * construction.\n */\n get spinCellSize(): number {\n return this._spinCellSize;\n }\n\n /** The gsap instance this reel's tweens live on. Read by every phase. */\n get gsap(): Gsap {\n return this._gsap;\n }\n\n /** This reel's travel projection (orientation + direction). */\n get axis(): ReelAxis {\n return this._axis;\n }\n\n /**\n * This reel's cylinder curvature, or `undefined` when it renders flat.\n * Read it to map your own overlays onto the bent grid.\n */\n get curve(): ReelCurve | undefined {\n return this._curve;\n }\n\n /**\n * Re-curve this reel. Takes effect on the next frame of motion, and\n * immediately for a reel already at rest. Pass `0` (or `undefined`) to go\n * back to flat.\n *\n * @internal Drive this through `ReelSet.setCurve()` so a whole set stays\n * consistent; it is public so a game tuning one reel does not have to\n * rebuild the set.\n */\n setCurve(input: ReelCurveInput | undefined): void {\n // Derive the cell extent from the motion layer rather than `_cellMain`:\n // during SPIN a MultiWays reel marches on `spinCellSize`, and the curve\n // has to measure the window the strip is actually laid out on.\n const cellMain = this.motion.slotPitch - this._mainGap;\n this._curve = this._buildCurve(input, cellMain, this._visibleCells);\n this.motion.setCurve(this._curve);\n // `setCurve` -> `motion.setCurve` re-renders from the motion layer's own\n // positions, which is right for masked views but writes bare reel-local\n // main over any view the at-rest unmask lift moved into viewport space.\n this._syncUnmaskedViewOffsets();\n }\n\n /** Update reel for one frame. Called by SpinController via ticker. */\n update(deltaMs: number): void {\n if (this.speed === 0) return;\n\n // Clamp pathological frame spikes (a backgrounded tab refocusing, a custom\n // or fake ticker without Pixi's minFPS floor) to a sane per-tick budget.\n // Defence in depth on top of each mode's own displacement cap — the tumble\n // mode caps at a full slot, so an unbounded deltaMs there could still skip.\n const dt = Math.min(deltaMs, MAX_TICK_MS);\n\n const deltaY = this.spinningMode.computeDelta(\n this.motion.slotPitch,\n this.speed,\n dt,\n );\n\n if (deltaY !== 0) {\n this.motion.advance(deltaY);\n }\n }\n\n /**\n * Set the target frame for stopping.\n *\n * @internal Called by SpinController during the stop sequence.\n */\n setStopFrame(frame: string[]): void {\n // Feed the frame from the edge new symbols enter during the stop: forward\n // reels fill from the start edge (consume end-first), reverse reels fill\n // from the end edge (consume head-first). See StopSequencer.setFrame.\n this.stopSequencer.setFrame(frame, this._axis.feedEdge);\n }\n\n /**\n * Get visible symbol IDs (top to bottom, excluding buffers).\n *\n * Big-symbol cells resolve to the anchor's id. both **same-reel**\n * (the anchor lives on this reel) and **cross-reel** (the anchor is on\n * a leftward reel of a wider block). The cross-reel resolver is\n * injected by `ReelSet`; without it, cross-reel OCCUPIED cells would\n * return the OCCUPIED sentinel, which is the only difference vs.\n * `ReelSet.getVisibleGrid()`. With the resolver wired, the two are\n * equivalent for any reel. `reels.map(r => r.getVisibleSymbols())`\n * matches `reelSet.getVisibleGrid()`.\n */\n getVisibleSymbols(): string[] {\n const result: string[] = [];\n for (let cell = 0; cell < this._visibleCells; cell++) {\n const occ = this._occupancy[cell];\n if (occ) {\n const anchor = this.symbols[this._bufferStart + occ.anchorCell];\n result.push(anchor.symbolId);\n } else {\n const id = this.symbols[this._bufferStart + cell].symbolId;\n if (id === OCCUPIED_SENTINEL && this._crossReelResolver) {\n result.push(this._crossReelResolver(this.reelIndex, cell));\n } else {\n result.push(id);\n }\n }\n }\n return result;\n }\n\n /**\n * This reel's full strip as a `ColumnTarget` -- buffers included, anchors\n * at their true positions.\n *\n * `getVisibleSymbols()` reports the visible window only, so it cannot be\n * handed back: a block anchored in `bufferStart` with just its tail\n * showing reads as that id at visible cell 0, and feeding it to\n * `setResult` re-anchors the block there. This keeps the anchor where it\n * is, so `setResult(reelSet.getTargets())` reproduces the board.\n */\n getTarget(): ColumnTarget {\n const idAt = (stripIndex: number): string => {\n const id = this.symbols[stripIndex]?.symbolId ?? '';\n if (id !== OCCUPIED_SENTINEL) return id;\n // Never leak the sentinel. Walk back along the strip to the anchor that\n // owns this slot. Deliberately NOT the occupancy map or the cross-reel\n // resolver: both are keyed on VISIBLE cells, and a block anchored in\n // bufferStart has occupied slots at negative cells, where the resolver\n // throws \"cell -1 out of range\".\n for (let i = stripIndex - 1; i >= 0; i--) {\n const prev = this.symbols[i]?.symbolId;\n if (prev && prev !== OCCUPIED_SENTINEL) return prev;\n }\n // Cross-reel block whose anchor lives on another reel: any id is safe\n // here, since that anchor repaints this slot on replay.\n const cell = stripIndex - this._bufferStart;\n if (cell >= 0 && this._crossReelResolver) return this._crossReelResolver(this.reelIndex, cell);\n return this.symbols[this._bufferStart]?.symbolId ?? '';\n };\n\n const visible: string[] = [];\n for (let cell = 0; cell < this._visibleCells; cell++) visible.push(idAt(this._bufferStart + cell));\n\n const target: ColumnTarget = { visible };\n // `bufferStart[0]` is the slot nearest the window; later indices go further out.\n if (this._bufferStart > 0) {\n const start: string[] = [];\n for (let k = 0; k < this._bufferStart; k++) start.push(idAt(this._bufferStart - 1 - k));\n target.bufferStart = start;\n }\n if (this.bufferEnd > 0) {\n const end: string[] = [];\n for (let k = 0; k < this.bufferEnd; k++) end.push(idAt(this._bufferStart + this._visibleCells + k));\n target.bufferEnd = end;\n }\n return target;\n }\n\n /**\n * Internal: register a callback used to resolve cross-reel OCCUPIED\n * cells to the originating big-symbol's id. Wired by `ReelSet` so this\n * reel can answer \"what id is at (myReel, cell)?\" even when the anchor is\n * on a different reel.\n *\n * @internal\n */\n setCrossReelResolver(resolver: ((reel: number, cell: number) => string) | null): void {\n this._crossReelResolver = resolver;\n }\n\n /**\n * Get symbol at a visible cell (0-indexed from top visible).\n * For non-anchor cells of a big symbol, walks up to the anchor cell and\n * returns the anchor symbol so animations target the actual visual.\n */\n getSymbolAt(visibleCell: number): ReelSymbol {\n const occ = this._occupancy[visibleCell];\n const anchorCell = occ ? occ.anchorCell : visibleCell;\n return this.symbols[this._bufferStart + anchorCell];\n }\n\n /**\n * Resolve a visible cell to its anchor cell when a big symbol occupies it.\n *\n * @internal Wired by ReelSet and SymbolSpotlight. Consumers should call\n * `ReelSet.getSymbolFootprint()` or `ReelSet.getBlockBounds()` instead.\n */\n getAnchorCell(visibleCell: number): number {\n const occ = this._occupancy[visibleCell];\n return occ ? occ.anchorCell : visibleCell;\n }\n\n /**\n * Record that the given visible cell is the non-anchor cell of a big\n * symbol whose anchor lives at `anchorCell`. Pass `null` to clear the\n * occupancy mark.\n *\n * @internal. called by `_finalizeFrame` and the big-symbol coordinator.\n */\n _setOccupancy(visibleCell: number, anchorCell: number | null): void {\n if (anchorCell === null) {\n this._occupancy[visibleCell] = null;\n } else {\n this._occupancy[visibleCell] = { anchorCell };\n }\n }\n\n /**\n * Notify all strip symbols (visible and buffer cells. buffers scroll into\n * view within a frame of spin start) that the reel has started spinning,\n * and arm mid-spin notification: every symbol installed by\n * `_replaceSymbol` until `notifySpinEnd()` receives\n * `onReelSpinStart(true)` so pool-recycled symbols joining a moving reel\n * can apply their spin presentation (blur, static snapshot).\n *\n * @internal Called by SpinController on phase transition.\n */\n notifySpinStart(): void {\n this._spinPresentationActive = true;\n this._anticipationActive = false;\n // Safety net for callers that don't run `beginMotion()` first (skip\n // path, cascade phases). No-op once already re-masked. idempotent.\n this.beginMotion();\n for (let i = 0; i < this.symbols.length; i++) {\n this.symbols[i].onReelSpinStart();\n }\n }\n\n /**\n * Mark the reel as leaving rest and re-mask any lifted unmask symbols.\n *\n * Called the INSTANT this reel begins to move (start of the accel ramp),\n * not at `notifySpinStart` which fires only once the reel reaches full\n * speed. Unmask is an at-rest presentation: an unmasked symbol left in\n * `viewport.unmaskedContainer` would float above the mask while the\n * strip scrolls underneath it for the whole acceleration. Pull every\n * lifted view back into the masked reel container up front, and clear\n * `_atRest` so `_replaceSymbol` doesn't re-lift a result symbol mid-spin.\n *\n * @internal Called by StartPhase on launch. Idempotent.\n */\n beginMotion(): void {\n if (!this._atRest) return;\n this._atRest = false;\n for (let i = 0; i < this.symbols.length; i++) {\n const symbol = this.symbols[i];\n const view = symbol.view;\n if (view.parent === this._viewport.unmaskedContainer) {\n const reelLocalY = this._axis.getMain(view) - this._axis.getMain(this.container);\n this.container.addChild(view);\n this._placeSymbolView(symbol, reelLocalY, false);\n }\n }\n }\n\n /**\n * Notify all strip symbols that this reel entered its anticipation\n * (tease) phase, and arm mid-anticipation notification: every symbol\n * installed by `_replaceSymbol` until `notifySpinEnd()` also receives\n * `onReelAnticipationStart()` so cells wrapping in during the tease\n * apply the readable (un-blurred) presentation.\n *\n * @internal Called by SpinController when the anticipation phase starts.\n */\n notifyAnticipationStart(): void {\n this._anticipationActive = true;\n for (let i = 0; i < this.symbols.length; i++) {\n this.symbols[i].onReelAnticipationStart();\n }\n }\n\n /**\n * Notify all strip symbols that the reel is about to stop (just before\n * bounce) and disarm mid-spin notification.\n *\n * @internal Called by SpinController on phase transition.\n */\n notifySpinEnd(): void {\n this._spinPresentationActive = false;\n this._anticipationActive = false;\n for (let i = 0; i < this.symbols.length; i++) {\n this.symbols[i].onReelSpinEnd();\n }\n }\n\n /**\n * Notify visible symbols that the reel has landed on its target.\n *\n * @param landedCells - Optional filter of visible cells (0-indexed) whose\n * symbols receive `onReelLanded()`. Omit for a strip-spin landing\n * (every visible symbol landed). Cascade refills pass only the cells\n * that MOVED: an untouched survivor (offsetCells 0) replaying its\n * landing animation on every cascade stage reads as the whole board\n * twitching after each pop. The at-rest unmask lift always applies\n * to every visible cell. it's presentation state, not a landing.\n *\n * @internal Called by SpinController / CascadeDropInPhase on phase transition.\n */\n notifyLanded(landedCells?: readonly number[]): void {\n this._atRest = true;\n const only = landedCells ? new Set(landedCells) : null;\n for (let i = this._bufferStart; i < this._bufferStart + this._visibleCells; i++) {\n const symbol = this.symbols[i];\n // Lift landed unmask symbols above the mask. visible cells only, so\n // a buffer-cell scatter never sits parked outside the grid.\n if (this._isUnmasked(symbol.symbolId) && symbol.view.parent === this.container) {\n const reelLocalY = this._axis.getMain(symbol.view);\n this._viewport.unmaskedContainer.addChild(symbol.view);\n this._placeSymbolView(symbol, reelLocalY, true);\n }\n if (only === null || only.has(i - this._bufferStart)) {\n symbol.onReelLanded();\n }\n }\n }\n\n /**\n * Snap all symbols to grid and finalize big-symbol layout. Called at the\n * end of every stop sequence.\n *\n * @internal SpinController and AdjustPhase finalization only.\n */\n snapToGrid(): void {\n this._reMaskLiftedBufferSlots();\n this.motion.snapToGrid();\n this._syncUnmaskedViewOffsets();\n this._finalizeFrame();\n this.refreshZIndex();\n }\n\n /**\n * Pull any lifted (unmasked) view that has ended up in a buffer slot back\n * under the mask.\n *\n * `_replaceSymbol` never lifts a buffer slot, but a symbol lifted while it\n * was VISIBLE can still travel into a buffer slot without being replaced:\n * a nudge rotates the array and only the wrapped symbol goes through\n * `_replaceSymbol`, so an unmask symbol nudged out of the window kept its\n * seat above the mask and hung there outside the grid. Runs on every\n * settle, where the strip's final slots are known.\n */\n private _reMaskLiftedBufferSlots(): void {\n for (let i = 0; i < this.symbols.length; i++) {\n const symbol = this.symbols[i];\n const view = symbol.view;\n if (view.parent !== this._viewport.unmaskedContainer) continue;\n if (!this._isBufferSlot(i)) continue;\n const reelLocalMain = this._toReelLocalY(view);\n this.container.addChild(view);\n // Placing by SYMBOL, not view, so dropping back under the mask also\n // re-applies the curve - a re-masked buffer cell that kept the pose it\n // had while lifted would sit flat next to its bent neighbours.\n this._placeSymbolView(symbol, reelLocalMain, false);\n }\n }\n\n /**\n * Swap the symbol at a single visible cell in-place, without restarting\n * the spin or rebuilding the rest of the strip.\n *\n * Useful for live presentation effects at rest. converting a wild\n * after a cascade pop, swapping to a sticky variant after a win.\n * without going through the full `placeSymbols` / `setResult` paths.\n *\n * The symbol's `zIndex`, parent (masked vs unmasked), and visual state\n * are reset by `_replaceSymbol` so callers don't need to follow up\n * with `refreshZIndex`. The motion layer is **not** snapped. call\n * `snapToGrid()` separately if you need to re-grid.\n *\n * Throws if:\n * - the reel is currently moving (`speed !== 0` or `isStopping`).\n * A mid-spin swap would be overwritten by the next wrap/stop frame\n * anyway; the fail-loud throw spares the caller the silent loss.\n * - `visibleCell` is out of `[0, visibleCells)`.\n * - `symbolId` is not registered.\n * - the cell is a non-anchor cell of an existing big-symbol block.\n * - the cell currently holds the anchor of a big-symbol block. big\n * blocks span multiple cells (and possibly reels) and require\n * `placeSymbols` + the cross-reel OCCUPIED coordinator.\n * - `symbolId` itself is a big symbol. same reason.\n *\n * Pin overlap is **not** detected at this layer (Reel doesn't see the\n * pin map). Use `ReelSet.setSymbolAt(reel, cell, id)` for the safe\n * caller-facing surface that also throws on pinned cells.\n */\n setSymbolAt(visibleCell: number, symbolId: string): void {\n if (this.speed !== 0 || this._isStopping || this._isNudging) {\n throw new Error(\n `setSymbolAt: cannot swap mid-motion (speed=${this.speed}, isStopping=${this._isStopping}, isNudging=${this._isNudging}). ` +\n `Wait for the spin or nudge to land before calling, or use the result grid via setResult().`,\n );\n }\n if (!Number.isInteger(visibleCell) || visibleCell < 0 || visibleCell >= this._visibleCells) {\n throw new Error(\n `setSymbolAt: visibleCell ${visibleCell} is out of range [0, ${this._visibleCells}).`,\n );\n }\n if (!Object.prototype.hasOwnProperty.call(this._symbolsData, symbolId)) {\n throw new Error(\n `setSymbolAt: symbolId '${symbolId}' is not registered. Register it via builder.symbols(...).`,\n );\n }\n const occ = this._occupancy[visibleCell];\n if (occ) {\n throw new Error(\n `setSymbolAt: visible cell ${visibleCell} is a non-anchor cell of a big symbol (anchor at cell ${occ.anchorCell}). ` +\n `Use placeSymbols to rebuild the frame.`,\n );\n }\n const arrayIndex = this._bufferStart + visibleCell;\n const oldSym = this.symbols[arrayIndex];\n const oldMeta = this._symbolsData[oldSym.symbolId];\n if (oldMeta?.size && (oldMeta.size.reels > 1 || oldMeta.size.cells > 1)) {\n throw new Error(\n `setSymbolAt: cell ${visibleCell} currently holds the anchor of big symbol ` +\n `'${oldSym.symbolId}' (${oldMeta.size.reels}x${oldMeta.size.cells}). Big blocks span multiple ` +\n `cells (and possibly reels); use placeSymbols + the OCCUPIED coordinator instead.`,\n );\n }\n const newMeta = this._symbolsData[symbolId];\n if (newMeta?.size && (newMeta.size.reels > 1 || newMeta.size.cells > 1)) {\n throw new Error(\n `setSymbolAt: '${symbolId}' is a big symbol (${newMeta.size.reels}x${newMeta.size.cells}). ` +\n `Use placeSymbols + the OCCUPIED coordinator instead.`,\n );\n }\n this._replaceSymbol(arrayIndex, symbolId);\n }\n\n /**\n * Shift the reel by `distance` symbol positions, animating the strip with\n * a GSAP tween and revealing caller-supplied `incoming` symbols. The reel\n * must be at rest (post-stop). throws otherwise.\n *\n * The wrap pipeline drives identity changes during the tween: any incoming\n * symbol whose final destination is reachable via pre-placement (within\n * the leading buffer) is set up front; the rest stream through the wrap\n * callback as the strip moves. `incoming` is always top-down by final\n * on-strip position. see `NudgeOptions.incoming` for the overflow rules.\n *\n * **Big symbols are supported** as long as every block on the strip\n * (anchor + stubs) survives the rotation without crossing the wrap\n * boundary:\n * - down: anchorCell + h - 1 + distance < total\n * - up: anchorCell ≥ distance\n *\n * Blocks that wouldn't survive throw, as do cross-reel blocks (w > 1).\n * Use case: a 1xH block lands with stubs in bufferEnd. nudge up to\n * bring the whole block into view.\n *\n * Throws if:\n * - the reel is spinning, stopping, already nudging, or destroyed,\n * - `distance < 1`, `>= total strip capacity`, `direction` invalid, or\n * `incoming.length !== distance`,\n * - any `incoming` id is unregistered or is a big symbol,\n * - any block on the reel wouldn't survive the rotation,\n * - any cell on this reel is part of a cross-reel block (w > 1),\n * - the abort signal is already aborted on entry.\n *\n * Resolves with `{ symbols }`. the new visible column top-to-bottom.\n * Rejects with an `AbortError` if `options.signal` aborts mid-tween or\n * if the reel is destroyed before the tween completes.\n *\n * @param onPrepared Internal hook fired once pre-placement + grid snap\n * are done but before the tween starts. `ReelSet.nudge` uses this to\n * emit `nudge:start` after the strip has been mutated, so listeners\n * observe the about-to-animate state, not the pre-mutation state.\n */\n async nudge(\n options: NudgeOptions,\n onPrepared?: () => void,\n ): Promise<{ symbols: string[] }> {\n if (this._isDestroyed) {\n throw new Error('nudge: reel has been destroyed.');\n }\n if (this.speed !== 0 || this._isStopping || this._isNudging) {\n throw new Error(\n `nudge: cannot nudge a reel in motion (speed=${this.speed}, isStopping=${this._isStopping}, isNudging=${this._isNudging}). ` +\n `Wait for the spin or previous nudge to land first.`,\n );\n }\n const { distance, direction, incoming, signal } = options;\n if (!Number.isInteger(distance) || distance < 1) {\n throw new Error(`nudge: distance must be a positive integer, got ${distance}.`);\n }\n const total = this.symbols.length;\n if (distance >= total) {\n throw new Error(\n `nudge: distance ${distance} must be strictly less than total strip capacity ` +\n `(bufferStart + visibleCells + bufferEnd = ${total}). At distance = total the strip ` +\n `rotates fully and pre-placed buffer entries would be silently dropped.`,\n );\n }\n if (direction !== 'forward' && direction !== 'reverse') {\n throw new Error(\n `nudge: direction must be 'forward' or 'reverse', got ${String(direction)}.`,\n );\n }\n if (!Array.isArray(incoming) || incoming.length !== distance) {\n throw new Error(\n `nudge: incoming must be an array of exactly ${distance} symbol id(s), got length ${incoming?.length}.`,\n );\n }\n // `direction` is relative to the reel's own travel, so a signed travel\n // request is all `motion.advance` needs. it applies the polarity itself.\n const travelSign = direction === 'forward' ? 1 : -1;\n // Which array end new symbols arrive at. `advance` rotates toward the\n // array start when `polarity * delta > 0`, so a reverse-polarity reel\n // nudging 'forward' feeds from the opposite edge to a forward one.\n const wrapsIntoStart = travelSign * this._axis.polarity > 0;\n for (const id of incoming) {\n if (!Object.prototype.hasOwnProperty.call(this._symbolsData, id)) {\n throw new Error(`nudge: incoming symbol '${id}' is not registered. Register it via builder.symbols(...).`);\n }\n const meta = this._symbolsData[id];\n if (meta?.size && (meta.size.reels > 1 || meta.size.cells > 1)) {\n throw new Error(\n `nudge: incoming symbol '${id}' is a big symbol (${meta.size.reels}x${meta.size.cells}). ` +\n `Big symbols are not supported as incoming items (they need an anchor + OCCUPIED ` +\n `coordinator). Pre-existing big symbols on the strip CAN be nudged through.`,\n );\n }\n }\n\n // Scan the ENTIRE strip (not just visible) for big-symbol anchors.\n // A block survives the rotation iff none of its cells crosses the wrap\n // boundary during the `distance` advance ticks:\n // - down: anchor + h - 1 + distance < total\n // (the block's bottommost cell stays on the strip; it may land\n // in bufferEnd. rendered half-clipped by the mask, which is\n // fine: `_finalizeFrame` sizes anchors that extend past visible\n // in either direction.)\n // - up: anchor - distance >= 0\n // (the anchor stays on the strip; it may land in bufferStart.\n // rendered correctly because `_finalizeFrame` scans bufferStart\n // anchors too and sizes them to the full block.)\n //\n // Cross-reel blocks (w > 1) can never be nudged on a single reel.\n // the other-reel cells stay put and the block splits visually + logically.\n for (let i = 0; i < total; i++) {\n const sym = this.symbols[i];\n if (sym instanceof OccupiedStub) continue;\n const meta = this._symbolsData[sym.symbolId];\n if (!meta?.size) continue;\n const { reels: w, cells: h } = meta.size;\n if (w === 1 && h === 1) continue;\n if (w > 1) {\n throw new Error(\n `nudge: reel ${this.reelIndex} carries cross-reel big symbol '${sym.symbolId}' ` +\n `(${w}x${h}) at strip[${i}]. Cross-reel blocks can't be nudged from a single ` +\n `reel. the other-reel cells would stay put and split the block.`,\n );\n }\n if (h > 1) {\n const survives = wrapsIntoStart\n ? i + h - 1 + distance < total\n : i - distance >= 0;\n if (!survives) {\n const failureDetail = wrapsIntoStart\n ? `anchor + h - 1 + distance < total (${i} + ${h} - 1 + ${distance} = ${i + h - 1 + distance} vs ${total})`\n : `anchor - distance >= 0 (${i} - ${distance} = ${i - distance})`;\n throw new Error(\n `nudge: block '${sym.symbolId}' (${w}x${h}) at strip[${i}] wouldn't survive a ` +\n `distance=${distance} ${direction} nudge. the wrap boundary would split the ` +\n `anchor from its stubs. Block survival: ${failureDetail}.`,\n );\n }\n }\n }\n // Cross-reel stubs (cells with OCCUPIED sentinel whose anchor lives on\n // another reel) appear with `symbolId === OCCUPIED_SENTINEL` and no\n // entry in our local `_occupancy` map.\n for (let cell = 0; cell < this._visibleCells; cell++) {\n const sym = this.symbols[this._bufferStart + cell];\n if (sym.symbolId === OCCUPIED_SENTINEL && !this._occupancy[cell]) {\n throw new Error(\n `nudge: visible cell ${cell} is a non-anchor cell of a cross-reel big symbol. ` +\n `Cross-reel blocks can't be nudged from a single reel.`,\n );\n }\n }\n\n // Abort signal check. bail before any mutation if already aborted.\n if (signal?.aborted) {\n const err = new Error('nudge: aborted before start.');\n err.name = 'AbortError';\n throw err;\n }\n\n // Optional pre-tween delay. useful for staggered Promise.all calls.\n // Validation already passed, so consumers can rely on synchronous\n // error throws for invalid input.\n const startDelay = options.startDelay ?? 0;\n if (startDelay > 0) {\n // The abort listener must come back off on the NORMAL path too. `{ once:\n // true }` only self-removes when the event actually fires, so a signal\n // reused across the documented staggered-`Promise.all` pattern accrued\n // one dead listener per delayed nudge for the life of the controller.\n let onAbort: (() => void) | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n const tId = setTimeout(resolve, startDelay);\n if (signal) {\n onAbort = () => {\n clearTimeout(tId);\n const err = new Error('nudge: aborted during startDelay.');\n err.name = 'AbortError';\n reject(err);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n } finally {\n if (onAbort) signal?.removeEventListener('abort', onAbort);\n }\n // Re-check destroy / motion after the async gap.\n if (this._isDestroyed) {\n const err = new Error('nudge: reel destroyed during startDelay.');\n err.name = 'AbortError';\n throw err;\n }\n }\n\n const duration = options.duration ?? 200 * distance;\n const ease = options.ease ?? 'power2.out';\n const slotH = this.motion.slotPitch;\n const bufferStart = this._bufferStart;\n const bufferEnd = this.bufferEnd;\n\n // Pre-place incoming into the appropriate buffer; build the wrap queue\n // for the rest. Random fillers use `next(true)` so buffer-excluded\n // symbols don't leak into off-screen slots (matching placeSymbols).\n //\n // **Big-symbol awareness**: if a buffer slot we're about to write to\n // currently holds an `OccupiedStub` (a non-anchor cell of a surviving\n // block), we MUST NOT overwrite it. that would split the block from\n // its anchor. The corresponding `incoming` slot is silently dropped\n // for that position; the block \"wins\" the visible cell it survives\n // into. Same for slots that hold a same-reel big-symbol anchor.\n const isProtectedSlot = (stripIdx: number): boolean => {\n const sym = this.symbols[stripIdx];\n if (sym instanceof OccupiedStub) return true;\n const meta = this._symbolsData[sym.symbolId];\n return !!(meta?.size && (meta.size.reels > 1 || meta.size.cells > 1));\n };\n if (wrapsIntoStart) {\n const bufferSet = Math.min(distance, bufferStart);\n for (let i = 0; i < bufferSet; i++) {\n const stripIdx = bufferStart - bufferSet + i;\n const incIdx = distance - bufferSet + i;\n if (isProtectedSlot(stripIdx)) continue;\n this._replaceSymbol(stripIdx, incoming[incIdx]);\n }\n const queue: string[] = [];\n const wrapsToVisible = distance - bufferStart;\n for (let k = 1; k <= distance; k++) {\n if (k <= wrapsToVisible) {\n queue.push(incoming[wrapsToVisible - k]);\n } else {\n // These wraps enter at the buffer-start end, so that side's pools apply.\n queue.push(this._randomProvider.next('bufferStart', this.reelIndex));\n }\n }\n this._nudgeQueue = queue;\n } else {\n const bufferSet = Math.min(distance, bufferEnd);\n for (let i = 0; i < bufferSet; i++) {\n const stripIdx = bufferStart + this._visibleCells + i;\n if (isProtectedSlot(stripIdx)) continue;\n this._replaceSymbol(stripIdx, incoming[i]);\n }\n const queue: string[] = [];\n const wrapsToVisible = distance - bufferEnd;\n for (let k = 1; k <= distance; k++) {\n if (k <= wrapsToVisible) {\n queue.push(incoming[bufferEnd + k - 1]);\n } else {\n // Mirror of the branch above: these enter at the buffer-end side.\n queue.push(this._randomProvider.next('bufferEnd', this.reelIndex));\n }\n }\n this._nudgeQueue = queue;\n }\n\n // Re-snap so pre-set symbols sit on the grid before the tween begins.\n this.motion.snapToGrid();\n this._syncUnmaskedViewOffsets();\n this.refreshZIndex();\n\n this._isNudging = true;\n this.events.emit('phase:enter', 'nudge');\n // Hook fires AFTER pre-placement so listeners see the about-to-tween\n // state (ReelSet uses this to emit `nudge:start` at the right time).\n onPrepared?.();\n\n const totalDelta = travelSign * distance * slotH;\n // Cap per-tick displacement at < half a slot so ReelMotion fires exactly\n // one wrap per `advance` call (mirrors SpinningMode.computeDelta).\n const stepLimit = slotH * 0.45;\n\n // Finalize closure. runs at natural completion AND on skip / abort.\n // Captured here so `skipNudge()` can jump straight to the landed state\n // without re-deriving anything from the half-tweened strip.\n const finalize = () => {\n // Drain any remaining queue entries by completing the remaining\n // displacement in one shot. Each pending wrap fires its callback\n // and pulls from `_nudgeQueue` exactly as if the tween had run.\n const remainingQueue = this._nudgeQueue?.length ?? 0;\n if (remainingQueue > 0) {\n // Complete the remaining wraps. The strip's cumulative position\n // after k of D wraps is k * slotH worth of displacement (in the\n // tween's direction). We're at some intermediate position; just\n // drive to the final position one step at a time so each wrap fires.\n const stepsLeft = remainingQueue;\n const stepDir = travelSign * stepLimit;\n // ceil(slotH / stepLimit) substeps per wrap = ceil(1/0.45) = 3\n // per remaining wrap. Conservative. actual wraps fire when the\n // tail symbol crosses the boundary.\n for (let i = 0; i < stepsLeft * 3 && (this._nudgeQueue?.length ?? 0) > 0; i++) {\n this.motion.advance(stepDir);\n }\n }\n this.snapToGrid();\n this._isNudging = false;\n this._nudgeQueue = null;\n this._nudgeTween = null;\n this._nudgeReject = null;\n this.events.emit('phase:exit', 'nudge');\n };\n\n try {\n await new Promise<void>((resolve, reject) => {\n this._nudgeReject = reject;\n\n const onAbort = () => {\n if (this._nudgeTween) {\n this._nudgeTween.kill();\n this._nudgeTween = null;\n }\n finalize();\n const err = new Error('nudge: aborted.');\n err.name = 'AbortError';\n reject(err);\n };\n\n if (signal) {\n signal.addEventListener('abort', onAbort, { once: true });\n }\n\n const state = { p: 0 };\n let lastDisplaced = 0;\n this._nudgeTween = this._gsap.to(state, {\n p: 1,\n duration: duration / 1000,\n ease,\n onUpdate: () => {\n // Clamp `state.p * totalDelta` to the intended trajectory so\n // overshooting eases (back.out(N), elastic.out, ...) can't fire\n // a spurious wrap past the landing position. The eased curve\n // is still computed by GSAP; we just don't ride the overshoot\n // into the wrap mechanism.\n const eased = state.p * totalDelta;\n const target = totalDelta > 0\n ? Math.min(eased, totalDelta)\n : Math.max(eased, totalDelta);\n let remaining = target - lastDisplaced;\n while (Math.abs(remaining) > stepLimit) {\n const step = remaining > 0 ? stepLimit : -stepLimit;\n this.motion.advance(step);\n remaining -= step;\n }\n if (remaining !== 0) {\n this.motion.advance(remaining);\n }\n // `advance()` re-derives every main coordinate from the array\n // index, so it OVERWRITES the reel offset baked into any lifted\n // view. A nudge runs while the reel is at rest, which is exactly\n // when lifted views exist, so the fixup belongs on every tick\n // here - not just after an absolute snap.\n this._syncUnmaskedViewOffsets();\n lastDisplaced = target;\n },\n onComplete: () => {\n if (signal) signal.removeEventListener('abort', onAbort);\n finalize();\n resolve();\n },\n });\n });\n } catch (err) {\n // Re-throw so the caller's await sees it; finalize already ran in\n // the abort path. Don't re-finalize on caught errors.\n throw err;\n }\n\n const symbols = this.getVisibleSymbols();\n return { symbols };\n }\n\n /**\n * Fast-forward the active nudge tween to its landed state and resolve.\n * No-op if no nudge is in flight. The tween's `onComplete` fires\n * synchronously, the strip snaps to the final position, `_nudgeQueue`\n * drains, and the original `nudge()` promise resolves on the next\n * microtask.\n *\n * Useful for player-driven \"skip\" buttons or accessibility paths that\n * want to land immediately without waiting for the full animation.\n */\n skipNudge(): void {\n if (!this._isNudging || !this._nudgeTween) return;\n // GSAP's progress(1) fires onComplete which invokes our finalize +\n // resolves the awaiting promise. Drop the tween reference first so\n // `destroy()` doesn't try to kill an already-completed tween.\n const tween = this._nudgeTween;\n this._nudgeTween = null;\n tween.progress(1);\n }\n\n /**\n * Place a target column immediately (for skip/turbo/cascade landing).\n *\n * `target.visible[0..n-1]` fills the visible window; `bufferStart` and\n * `bufferEnd` fill the off-window slots either side, closest cell first.\n * Slots the target does not specify are filled with random symbols.\n */\n placeSymbols(target: ColumnTarget): void {\n this.placeStrip(columnTargetToStrip(target, this._bufferStart));\n }\n\n /**\n * @internal. Engine and custom phases only.\n *\n * Place a full strip frame: one entry per strip slot, top to bottom,\n * index `0` being the furthest buffer-above cell. This is exactly what\n * `FrameBuilder.build` returns, so a phase holding a built frame can\n * land it without re-deriving buffer offsets. Missing or `undefined`\n * entries are filled with random symbols.\n */\n placeStrip(frame: ReadonlyArray<string | undefined>): void {\n const totalSlots = this.symbols.length;\n for (let i = 0; i < totalSlots; i++) {\n // Buffer pools apply to the buffer slots only, and each side has its\n // own: a visible cell the frame left blank is a spinning-pool draw,\n // same rule FrameBuilder uses when it random-fills a frame.\n const targetId = frame[i] ?? this._randomProvider.next(this._slotKind(i), this.reelIndex);\n this._replaceSymbol(i, targetId);\n }\n this.motion.snapToGrid();\n this._syncUnmaskedViewOffsets();\n this._finalizeFrame();\n this.refreshZIndex();\n }\n\n /**\n * @internal. MultiWays orchestration only.\n *\n * Commit a new visible-cell count and per-reel cell height. Resizes every\n * existing symbol on the strip to the new cell height, rebuilds the\n * symbol array (extending or truncating buffers as needed), reshapes the\n * motion layer, and recomputes `_extent` from the new geometry so\n * `extent` stays consistent. Idempotent if the shape doesn't change.\n *\n * Only the engine should call this. `SpinController._applyReshape` is\n * the single source of truth for reshape orchestration. Direct external\n * calls are unsupported and may leave pin overlays, the cross-reel\n * resolver, and the parent `ReelSet`'s shape state out of sync. Use\n * `ReelSet.setShape()` instead, which gates this method on a MultiWays\n * slot and migrates pins atomically.\n */\n reshape(\n newVisibleCells: number,\n newCellSize: number,\n bufferStart: number,\n bufferEnd: number,\n ): void {\n const newTotal = bufferStart + newVisibleCells + bufferEnd;\n const newSize = this._screenSize(newCellSize, this._cellCross);\n\n // Grow: append additional symbols at the bottom buffer. New symbols are\n // parented based on `unmask` flag. same rule as `_replaceSymbol`.\n while (this.symbols.length < newTotal) {\n // The appended slot is always the new tail, i.e. the buffer-end side.\n const id = this._randomProvider.next('bufferEnd', this.reelIndex);\n const sym = this._symbolFactory.acquire(id);\n const slot = this.symbols.length;\n sym.resize(newSize.width, newSize.height);\n this._placeSymbolView(sym, this._axis.getMain(sym.view), this._effectiveUnmask(id, slot));\n this._parentForSymbolId(id, slot).addChild(sym.view);\n this.symbols.push(sym);\n }\n\n // Shrink: release tail symbols.\n while (this.symbols.length > newTotal) {\n const sym = this.symbols.pop()!;\n if (sym instanceof OccupiedStub) {\n sym.view.parent?.removeChild(sym.view);\n } else {\n this._symbolFactory.release(sym);\n }\n }\n\n this._visibleCells = newVisibleCells;\n this._cellMain = newCellSize;\n this._bufferStart = bufferStart;\n this._occupancy = new Array(newVisibleCells).fill(null);\n // Recompute the reel's main-axis extent from the new geometry, using the\n // MAIN gap rather than symbolGapY (ADR 016 section 6.6 - under horizontal\n // the strip is spaced by the X gap). For MultiWays this equals the fixed\n // `multiways.reelExtent` by construction; for any non-MultiWays caller it\n // matches what the builder set at construction. Keeps `extent` from going\n // stale across reshape.\n this._extent =\n newVisibleCells * newCellSize + (newVisibleCells - 1) * this._mainGap;\n\n // Resize every kept symbol to the new cell extent.\n for (const sym of this.symbols) {\n if (sym instanceof OccupiedStub) continue;\n sym.resize(newSize.width, newSize.height);\n }\n\n // Re-bind curvature to the reshaped window before the snap re-renders:\n // both the cell extent and the number of cells the window holds changed,\n // and a curve still measuring the old window would bend the new cells\n // against the wrong centre.\n this._curve?.setGeometry(\n newCellSize,\n this._cellCross,\n newCellSize + this._mainGap,\n newVisibleCells,\n );\n\n // ...and re-measure the warp against it. Its texture and displaced mesh\n // are both sized from the reel box, so a MultiWays reshape that grew or\n // shrank the window left the drum drawn at the old size, with the strip\n // sliding inside a texture that no longer matches it.\n if (this._warp) {\n const box = this._screenSize(this._extent, this._cellCross);\n this._warp.resize(box.width, box.height);\n }\n\n // Update motion: new slot pitch + bounds, on the main axis.\n this.motion.reshape(newCellSize, this._mainGap, bufferStart, newVisibleCells, bufferEnd);\n this.motion.snapToGrid();\n this._syncUnmaskedViewOffsets();\n this.refreshZIndex();\n }\n\n /**\n * Compute the canonical zIndex for a single symbol view at a given\n * array index. Centralizes the formula used by both `refreshZIndex`\n * (full rescan) and the per-swap activate path (so newly placed\n * symbols land with their correct zIndex without the caller needing\n * to remember to call `refreshZIndex` afterwards).\n */\n private _computeSymbolZIndex(symbolId: string, index: number): number {\n const base = this._symbolsData[symbolId]?.zIndex ?? 0;\n const within =\n this._cellStacking === 'ascending' ? index : this.symbols.length - 1 - index;\n return base * 100 + within;\n }\n\n /**\n * Recompute `zIndex` for every symbol in the reel.\n *\n * Formula: `symbolData.zIndex ?? 0` (scaled by 100 to leave room for cell\n * ordering), plus the symbol's current array index. so bottom-cell symbols\n * render in front of top-cell symbols and any symbol with a higher\n * configured base zIndex (e.g. wild, bonus) renders above its neighbors.\n *\n * Called automatically after wraps, snaps, and direct placement. Also\n * called inline by `_replaceSymbol` for the single newly-placed symbol.\n * so consumers who swap one symbol at a time (via the public APIs that\n * funnel into `_replaceSymbol`) get correct layering for free, no\n * manual `refreshZIndex` required. Call it manually after mutating\n * `symbolsData.zIndex` at runtime.\n */\n refreshZIndex(): void {\n for (let i = 0; i < this.symbols.length; i++) {\n const symbol = this.symbols[i];\n if (symbol instanceof OccupiedStub) {\n symbol.view.zIndex = i;\n continue;\n }\n symbol.view.zIndex = this._computeSymbolZIndex(symbol.symbolId, i);\n }\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n // Kill any in-flight nudge tween BEFORE we tear down views. otherwise\n // its next onUpdate writes to destroyed PixiJS containers and crashes.\n // Reject the outstanding promise so awaiters see a deterministic error.\n if (this._nudgeTween) {\n this._nudgeTween.kill();\n this._nudgeTween = null;\n }\n if (this._nudgeReject) {\n const err = new Error('nudge: reel was destroyed.');\n err.name = 'AbortError';\n this._nudgeReject(err);\n this._nudgeReject = null;\n }\n this._nudgeQueue = null;\n this._isNudging = false;\n // Destroy every symbol's view. We must NOT release live symbols back into\n // the shared pool here: the container.destroy({ children: true }) below\n // would then destroy the views of symbols now sitting in the pool, so the\n // next acquire() would hand out a destroyed view. (Full and partial reel\n // teardown both run through here.)\n for (const symbol of this.symbols) {\n symbol.destroy();\n }\n for (const stub of this._occupiedStubs) {\n if (!stub.isDestroyed) stub.destroy();\n }\n this._occupiedStubs = [];\n this.symbols = [];\n this._warp?.destroy();\n this.container.destroy({ children: true });\n this._isDestroyed = true;\n // Emit 'destroyed' while listeners are still attached, THEN remove them —\n // emitting after removeAllListeners() would reach nobody.\n this.events.emit('destroyed');\n this.events.removeAllListeners();\n }\n\n /**\n * Whether the symbol with this id has `unmask: true` in its data. i.e.\n * its view should be parented to `viewport.unmaskedContainer` to render\n * above the reel mask.\n */\n private _isUnmasked(symbolId: string): boolean {\n return !!this._symbolsData[symbolId]?.unmask;\n }\n\n /** Whether strip slot `index` sits outside the visible window. */\n private _isBufferSlot(index: number): boolean {\n return index < this._bufferStart || index >= this._bufferStart + this._visibleCells;\n }\n\n /**\n * Which pool slot `index` draws from: the visible window is `'spinning'`,\n * and a buffer cell names its side so that side's pools apply.\n */\n private _slotKind(index: number): DrawSlot {\n if (index < this._bufferStart) return 'bufferStart';\n if (index >= this._bufferStart + this._visibleCells) return 'bufferEnd';\n return 'spinning';\n }\n\n /**\n * Whether the symbol at strip slot `index` should render above the mask\n * RIGHT NOW. Unmask is an at-rest presentation of a VISIBLE cell:\n *\n * - while the reel is in motion (including the stop approach and\n * bounce, when the result symbols are installed), every view\n * (unmask ids included) stays in the masked reel container so\n * nothing scrolls visibly outside the grid, and\n * - a buffer slot never lifts at all. it is parked outside the window\n * precisely so the mask hides it, and a lifted one hangs above or\n * below the grid in plain sight until the next spin re-masks it.\n * `placeStrip` writes buffer slots at rest on every skip, which is\n * exactly when that used to happen.\n *\n * Landed visible-cell symbols are lifted by `notifyLanded()`;\n * `notifySpinStart()` pulls them back down before the strip moves.\n */\n private _effectiveUnmask(symbolId: string, index: number): boolean {\n return this._atRest && !this._isBufferSlot(index) && this._isUnmasked(symbolId);\n }\n\n /**\n * Pick the right parent container for a symbol view based on its\n * `unmask` flag, its slot, and the reel's spin state. At-rest unmasked\n * symbols in a visible cell sit in `viewport.unmaskedContainer` (above\n * the reel mask); everything else lives in this reel's own container\n * (which is itself inside `viewport.maskedContainer`).\n */\n private _parentForSymbolId(symbolId: string, index: number): Container {\n return this._effectiveUnmask(symbolId, index)\n ? this._viewport.unmaskedContainer\n : this.container;\n }\n\n /**\n * Position a symbol view at a given reel-local Y, choosing X and any\n * parent-translation offset based on whether the symbol is unmasked.\n *\n * Unmasked views live in `viewport.unmaskedContainer` (at viewport\n * (0,0)), so we add `reel.container.x` and `reel.container.y` to keep\n * the at-rest cell position aligned with the reel column. Masked views\n * live in `this.container`, so reel-local coords map directly.\n */\n private _placeSymbolView(\n symbol: ReelSymbol,\n reelLocalMain: number,\n isUnmasked: boolean,\n ): void {\n const view = symbol.view;\n if (isUnmasked) {\n // Viewport space: bake in the reel container's own offset on both axes.\n this._axis.setCross(view, this._axis.getCross(this.container));\n this._axis.setMain(view, this._axis.getMain(this.container) + reelLocalMain);\n } else {\n this._axis.setCross(view, 0);\n this._axis.setMain(view, reelLocalMain);\n }\n // The projection is defined in reel-local main and handed over as a\n // VIEW-LOCAL quad, so the same call is correct whichever of the two spaces\n // above the view was just placed in. In warp mode the whole reel is bent\n // downstream instead, and symbols are left completely alone.\n if (this._curve && !this._warping) {\n symbol.applyCellQuad(this._curve.quadFor(reelLocalMain, symbol.cellInset));\n }\n }\n\n /**\n * Build the curvature for this reel, or `undefined` when it would be flat.\n * A flat set must not carry a curve object at all: that keeps the render\n * loop and every placement byte-identical to an uncurved build.\n */\n private _buildCurve(\n input: ReelCurveInput | undefined,\n cellMain: number,\n visibleCells: number,\n ): ReelCurve | undefined {\n if (input === undefined) return undefined;\n const curve = new ReelCurve(resolveCurveConfig(input), this._axis);\n if (curve.isFlat) return undefined;\n curve.setGeometry(cellMain, this._cellCross, cellMain + this._mainGap, visibleCells);\n curve.setFocus(this._curveFocus);\n return curve;\n }\n\n /**\n * Convert a view's current y back to reel-local coords. The view may\n * be parented to either `this.container` (already reel-local) or\n * `viewport.unmaskedContainer` (viewport-local. needs the reel offset\n * subtracted).\n */\n private _toReelLocalY(view: Container): number {\n return view.parent === this._viewport.unmaskedContainer\n ? this._axis.getMain(view) - this._axis.getMain(this.container)\n : this._axis.getMain(view);\n }\n\n /**\n * Re-bake the reel's `container.x/y` offset into any currently-lifted\n * (unmasked) view.\n *\n * `ReelMotion.snapToGrid()` writes bare reel-local Y to every symbol\n * view — it has no notion that some views were re-parented into\n * `viewport.unmaskedContainer` and need the reel offset added to stay\n * aligned. Masked reels have `container.y === 0`, so the two spaces\n * coincide and this is a no-op; on a jagged/pyramid layout (non-zero\n * `mainOffset`) the snap would drop the offset and jump the lifted view.\n *\n * Call this after ANY motion write. `advance()` derives positions from the\n * array index and writes them absolutely (it was `+=` in v1, which is why\n * this used to be snap-only), so it drops the offset just as `snapToGrid`\n * does.\n *\n * During a spin the loop finds nothing: `beginMotion()` pulls every lifted\n * view back down before the strip moves. A nudge is the case that matters,\n * because it runs at rest with views still lifted.\n */\n private _syncUnmaskedViewOffsets(): void {\n const mainOff = this._axis.getMain(this.container);\n const crossOff = this._axis.getCross(this.container);\n if (mainOff === 0 && crossOff === 0) return;\n for (let i = 0; i < this.symbols.length; i++) {\n const view = this.symbols[i].view;\n if (view.parent === this._viewport.unmaskedContainer) {\n // Cross offset is absolute (set once); main offset is incremental\n // (the view already holds its reel-local main coordinate).\n this._axis.setCross(view, crossOff);\n this._axis.addMain(view, mainOff);\n }\n }\n }\n\n /**\n * Shift every currently-lifted (unmask) view by `delta` on the main axis.\n *\n * Lifted views sit in `viewport.unmaskedContainer`, so the reel container's\n * offset is *baked into their own coordinate* rather than inherited from a\n * parent. Anything that moves `this.container` while views are lifted leaves\n * them behind — today that is the stop bounce, which lifts in `notifyLanded()`\n * and then tweens the container for the whole ~600 ms overshoot, so an\n * unmasked scatter hung motionless while the reel bounced underneath it.\n *\n * A delta rather than an absolute re-anchor: the caller owns the tween and\n * already knows where the container was, and `_syncUnmaskedViewOffsets` is\n * incremental for the same reason (the view holds reel-local main plus the\n * baked offset, and the two are not separable after the fact).\n *\n * @internal `StopPhase`'s bounce only.\n */\n offsetLiftedViews(delta: number): void {\n if (delta === 0) return;\n for (let i = 0; i < this.symbols.length; i++) {\n const view = this.symbols[i].view;\n if (view.parent === this._viewport.unmaskedContainer) {\n this._axis.addMain(view, delta);\n }\n }\n }\n\n private _setupSymbolPositions(config: ReelConfig): void {\n // MAIN-axis pitch, not `symbolGapY`. The two coincide on a vertical set,\n // which is why this survived the axis refactor: a horizontal set laid its\n // initial strip out at `cellMain` with no gap at all, and only looked\n // right after the first spin, when ReelMotion (which does use `_mainGap`)\n // took over the positions.\n const slotH = this._spinCellSize + this._mainGap;\n // Add the reel container to the viewport's masked area first so\n // `this.container.x/y` are in viewport coords if any initial symbol\n // has `unmask: true` and needs parent-translation.\n this._viewport.maskedContainer.addChild(this.container);\n\n for (let i = 0; i < this.symbols.length; i++) {\n const symbol = this.symbols[i];\n const main = (i - config.bufferStart) * slotH;\n // Unmask applies to visible cells only. a buffer-cell symbol lifted\n // above the mask would sit visibly parked outside the grid.\n const unmasked = this._effectiveUnmask(symbol.symbolId, i);\n this._placeSymbolView(symbol, main, unmasked);\n (unmasked ? this._viewport.unmaskedContainer : this.container).addChild(symbol.view);\n }\n }\n\n private _onSymbolWrapped(symbol: ReelSymbol): void {\n let newSymbolId: string;\n if (this._nudgeQueue && this._nudgeQueue.length > 0) {\n // Nudge queue is exhaustively pre-built by `nudge()` to cover every\n // wrap fired during the tween (caller-supplied incoming first, then\n // random padding for wraps that target the off-screen buffer). Always\n // wins over the stop sequencer so a queued slam-stop on a stale spin\n // can't bleed symbols into a fresh nudge.\n newSymbolId = this._nudgeQueue.shift()!;\n } else if (this._isStopping && this.stopSequencer.hasRemaining) {\n newSymbolId = this.stopSequencer.next();\n } else {\n newSymbolId = this._randomProvider.next('spinning', this.reelIndex);\n }\n\n this._replaceSymbol(this.symbols.indexOf(symbol), newSymbolId);\n // During a nudge tween, defer the O(N) zIndex rescan to `snapToGrid()`\n // in the tween's finalize step. `distance` wraps fire back-to-back\n // and a single refresh at the end produces the same final state.\n // Spin / cascade refill paths keep the per-wrap refresh so live\n // bottom-to-top stacking stays correct mid-spin.\n if (!this._isNudging) {\n // Array was rearranged by ReelMotion (pop+unshift or shift+push), so the\n // array index of every remaining symbol changed. refresh all zIndexes.\n this.refreshZIndex();\n }\n }\n\n private _replaceSymbol(index: number, newSymbolId: string): void {\n const oldSymbol = this.symbols[index];\n const isOldStub = oldSymbol instanceof OccupiedStub;\n // The old symbol's `view.parent` is unsafe as a destination because\n // the shared symbol pool can recycle a view across reels (or the\n // spotlight may have promoted it above the mask). Always re-pick\n // the destination from `_parentForSymbolId(newSymbolId)` (or\n // `this.container` for OCCUPIED stubs, which never carry `unmask`).\n\n // Capture old Y in reel-local coords before releasing. old view may\n // have been parented to viewport.unmaskedContainer and need an offset\n // subtraction to be reused as the new symbol's reel-local Y.\n const reelLocalY = isOldStub\n ? this._axis.getMain(oldSymbol.view)\n : this._toReelLocalY(oldSymbol.view);\n\n // OCCUPIED: install a stub. Stubs are not pooled through SymbolFactory\n // and never carry an `unmask` flag. they always live in `this.container`.\n if (newSymbolId === OCCUPIED_SENTINEL) {\n if (isOldStub) {\n oldSymbol.view.alpha = 0;\n return;\n }\n this._symbolFactory.release(oldSymbol);\n const stub = this._acquireOccupiedStub();\n this._axis.setMain(stub.view, reelLocalY);\n this._axis.setCross(stub.view, 0);\n stub.view.alpha = 0;\n stub.view.visible = true;\n stub.view.scale.set(1, 1);\n stub.view.zIndex = index;\n // Stubs are never unmasked. always live in this reel's container.\n if (stub.view.parent !== this.container) this.container.addChild(stub.view);\n this.symbols[index] = stub;\n return;\n }\n\n // Replacing a stub with a real symbol: release stub back to internal\n // cache. The new symbol may be unmasked → choose parent + offset by id.\n if (isOldStub) {\n this._releaseOccupiedStub(oldSymbol);\n const newSymbol = this._symbolFactory.acquire(newSymbolId);\n const newIsUnmasked = this._effectiveUnmask(newSymbolId, index);\n newSymbol.resize(this.symbolWidth, this.symbolHeight);\n newSymbol.view.alpha = 1;\n // Reset BEFORE placing: on a curved reel the placement is what writes\n // the cell's scale, and a reset after it would flatten the symbol back\n // out until the next frame of motion re-bent it.\n newSymbol.view.scale.set(1, 1);\n this._placeSymbolView(newSymbol, reelLocalY, newIsUnmasked);\n newSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n this._parentForSymbolId(newSymbolId, index).addChild(newSymbol.view);\n this.symbols[index] = newSymbol;\n if (this._spinPresentationActive) newSymbol.onReelSpinStart(true);\n if (this._anticipationActive) newSymbol.onReelAnticipationStart();\n this.events.emit('symbol:created', newSymbolId, index);\n return;\n }\n\n // Same id fast-path. Reset every mutable visual property (alpha, scale,\n // rotation, filters, zIndex) AND re-anchor the view to this reel's\n // container in case the pool moved it elsewhere since the last\n // activation (e.g. spotlight promotion above the mask).\n if (oldSymbol.symbolId === newSymbolId) {\n oldSymbol.view.alpha = 1;\n oldSymbol.view.scale.set(1, 1);\n oldSymbol.view.rotation = 0;\n oldSymbol.view.filters = null;\n oldSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n // Same id → same unmask status; pick the right destination by id\n // so an unmasked symbol stays in `unmaskedContainer` post-spotlight.\n const target = this._parentForSymbolId(newSymbolId, index);\n if (oldSymbol.view.parent !== target) target.addChild(oldSymbol.view);\n // Reset Y in case spotlight or another mutator displaced it.\n this._placeSymbolView(oldSymbol, reelLocalY, this._effectiveUnmask(newSymbolId, index));\n // The instance was never deactivated, so it usually still carries its\n // spin state. re-notify anyway for uniformity (hooks are idempotent).\n if (this._spinPresentationActive) oldSymbol.onReelSpinStart(true);\n if (this._anticipationActive) oldSymbol.onReelAnticipationStart();\n return;\n }\n\n this._symbolFactory.release(oldSymbol);\n const newSymbol = this._symbolFactory.acquire(newSymbolId);\n const newIsUnmasked = this._effectiveUnmask(newSymbolId, index);\n newSymbol.resize(this.symbolWidth, this.symbolHeight);\n newSymbol.view.alpha = 1;\n // Reset before placing. see the stub-replacement path above.\n newSymbol.view.scale.set(1, 1);\n this._placeSymbolView(newSymbol, reelLocalY, newIsUnmasked);\n newSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n\n this._parentForSymbolId(newSymbolId, index).addChild(newSymbol.view);\n\n this.symbols[index] = newSymbol;\n if (this._spinPresentationActive) newSymbol.onReelSpinStart(true);\n if (this._anticipationActive) newSymbol.onReelAnticipationStart();\n this.events.emit('symbol:created', newSymbolId, index);\n }\n\n /**\n * Acquire an OCCUPIED stub. Reuses any free stub stored locally; allocates\n * a new one if none are available. Stubs are never returned to\n * `SymbolFactory`.\n */\n private _acquireOccupiedStub(): OccupiedStub {\n for (const stub of this._occupiedStubs) {\n if (!stub.view.parent) return stub;\n }\n const stub = new OccupiedStub();\n stub.activate(OCCUPIED_SENTINEL);\n this._occupiedStubs.push(stub);\n return stub;\n }\n\n private _releaseOccupiedStub(stub: ReelSymbol): void {\n stub.view.parent?.removeChild(stub.view);\n }\n\n /**\n * After the visible target frame has been placed, scan the strip to\n * size big-symbol anchors and populate the OCCUPIED occupancy map.\n *\n * Called from `snapToGrid` and `placeSymbols` so it runs both for normal\n * stop landing AND for skip/turbo. For non-anchor cells of a block, the\n * anchor symbol is sized to span the block; the OCCUPIED stub at that\n * cell stays invisible underneath.\n *\n * **Two scans:**\n *\n * 1. Visible anchors. sizes blocks whose anchor is in `[0, visibleCells)`.\n * This is the common case (most blocks land fully visible). Blocks\n * whose stubs spill into bufferEnd are handled here: the anchor is\n * in visible, the sprite is sized to span `h * cellH`, and the mask\n * clips the off-screen tail. No occupancy entry is written for the\n * bufferEnd stubs because `_occupancy` is keyed by visible cells\n * only. consumers can't query a non-visible cell anyway.\n * 2. BufferAbove anchors. sizes blocks whose anchor sits above visible\n * but whose body extends into the visible window. This is the \"tail\n * visible\" partial-visibility case: a 1xH block whose top is clipped\n * by the reel mask, with only its bottom cells showing in the visible\n * window. Without this scan, the anchor sprite would stay at the\n * default 1x1 size and the block wouldn't render its visible portion\n * correctly.\n *\n * **No Scan 3 for bufferEnd-only anchors.** A block whose anchor is at\n * `cell >= visibleCells` would lie entirely off-screen (the strip ends at\n * `visibleCells + bufferEnd - 1` and `h >= 1`, so no visible cell is\n * covered). The cross-reel coordinator already accepts such anchors as a\n * legal-but-invisible placement; there's nothing to size and nothing for\n * the consumer-facing query API to return. If you ever add a scenario\n * where bufferEnd-only anchors need rendering, add Scan 3 here.\n *\n * For bufferStart anchors, `_occupancy[visibleCell].anchorCell` is set to\n * a NEGATIVE value. the offset from `bufferStart`. So\n * `this.symbols[this._bufferStart + anchorCell]` walks back to the anchor\n * regardless of which side it lives on. Consumers (`getSymbolFootprint`,\n * `getBlockBounds`) handle negative anchor cells by clipping bounds to\n * the visible portion of the block.\n */\n private _finalizeFrame(): void {\n this._occupancy = new Array(this._visibleCells).fill(null);\n\n // Scan 1: visible-cell anchors.\n for (let cell = 0; cell < this._visibleCells; cell++) {\n const sym = this.symbols[this._bufferStart + cell];\n if (sym instanceof OccupiedStub) continue;\n const meta = this._symbolsData[sym.symbolId];\n if (!meta?.size) continue;\n const w = meta.size.reels;\n const h = meta.size.cells;\n if (w === 1 && h === 1) continue;\n\n // Size the anchor to span the block PLUS inter-cell gaps. A 2x2\n // block on a (cell=80, gap=4) layout covers 2*80 + 1*4 = 164px, not\n // 160px. Without the gap, the anchor leaves a thin uncovered strip.\n //\n // `size.reels` spans the CROSS axis and `size.cells` the MAIN axis in\n // every orientation (ADR 016 section 6.7), so a 2x2 is 2 reels by 2\n // cells whichever way the strip runs - which means the screen width\n // and height it maps to invert under horizontal.\n const block = this._blockSize(w, h);\n sym.resize(block.width, block.height);\n for (let dy = 1; dy < h; dy++) {\n const occCell = cell + dy;\n if (occCell < this._visibleCells) {\n this._occupancy[occCell] = { anchorCell: cell };\n }\n }\n }\n\n // Scan 2: bufferStart anchors whose block extends into visible.\n // Iterating strip cells [0, bufferStart); the anchor's visible-cell\n // equivalent is `stripIdx - bufferStart` (negative).\n for (let stripIdx = 0; stripIdx < this._bufferStart; stripIdx++) {\n const sym = this.symbols[stripIdx];\n if (sym instanceof OccupiedStub) continue;\n const meta = this._symbolsData[sym.symbolId];\n if (!meta?.size) continue;\n const w = meta.size.reels;\n const h = meta.size.cells;\n if (w === 1 && h === 1) continue;\n\n // Does the block extend into visible? The block spans strip indices\n // [stripIdx, stripIdx + h). Visible starts at `bufferStart`.\n const blockBottomStrip = stripIdx + h - 1;\n if (blockBottomStrip < this._bufferStart) continue;\n\n const block = this._blockSize(w, h);\n sym.resize(block.width, block.height);\n\n const anchorCell = stripIdx - this._bufferStart; // negative\n for (let dy = 1; dy < h; dy++) {\n const occCell = anchorCell + dy;\n if (occCell >= 0 && occCell < this._visibleCells) {\n this._occupancy[occCell] = { anchorCell };\n }\n }\n }\n }\n}\n","import { Container, Graphics } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport { VERTICAL_FORWARD } from './ReelAxis.js';\n\n/**\n * Bounding rectangle for one reel. what `MaskStrategy` builds the clip\n * geometry from. SCREEN-space and viewport-local (origin = viewport\n * top-left), in reel order.\n *\n * The rect is screen-space in every orientation, so which of its four\n * numbers means \"along the strip\" depends on the axis: on a vertical set\n * `y`/`height` run along the strip and `x`/`width` march the reels; on a\n * horizontal set that is exactly reversed. Read {@link MaskContext.axis}\n * rather than assuming - `axis.toLocal(width, height)` gives you the pair\n * as `{ cross, main }`.\n */\nexport interface ReelMaskRect {\n /** Left edge, viewport-local. */\n x: number;\n /** Top edge, viewport-local. */\n y: number;\n /** Screen width of the reel box. */\n width: number;\n /** Screen height of the reel box. */\n height: number;\n}\n\n/**\n * Everything a mask strategy is given. Passed as one object so the axis\n * cannot be forgotten, and so later additions do not break the signature.\n */\nexport interface MaskContext {\n /** One rect per reel, in reel order. Empty before the builder supplies them. */\n readonly rects: readonly ReelMaskRect[];\n /** Viewport bounding-box width. */\n readonly width: number;\n /** Viewport bounding-box height. */\n readonly height: number;\n /**\n * The set's travel axis. `axis.mainProp` is the screen axis strips run\n * along, so a strategy that rounds \"the ends of each reel\" can round the\n * right two corners instead of guessing.\n */\n readonly axis: ReelAxis;\n /**\n * Cross-axis room, per side, that `curveBleed` asked for. Art wider than its\n * cell hangs over by this much, so a strategy that clips to the board would\n * cut it straight back off - the outermost reels worst of all, where the\n * overhang leaves the board entirely. Inflate by it on the CROSS axis only:\n * the main axis is where the buffer cells live, and they are meant to stay\n * hidden. `0` unless `ReelSetBuilder.curveBleed()` was called.\n */\n readonly bleed: number;\n}\n\n/**\n * Version marker every strategy must carry. A v1 strategy has positional\n * `(rects, totalWidth, totalHeight)` parameters and no axis; handed a\n * {@link MaskContext} it would read `rects` as an object, find no `.length`,\n * and quietly fall through to a full-bleed rect - clipping nothing, with no\n * error. The marker turns that into a named throw at `maskStrategy()`.\n */\nexport const MASK_STRATEGY_VERSION = 2;\n\n/**\n * Strategy for building the viewport's clip mask. Public. pass a custom\n * implementation to `ReelSetBuilder.maskStrategy(...)` to clip the reels\n * with any shape PixiJS Graphics can express (rounded frames, hex grids,\n * etc.). Two ship with the engine:\n *\n * - {@link RectMaskStrategy}. one rect per reel (default). Good for\n * pyramid layouts; symbols never leak buffer cells past a short reel's\n * ends.\n * - {@link SharedRectMaskStrategy}. single bounding-box rect spanning\n * every reel. Big symbols spanning multiple reels render correctly even\n * when reels have a cross-axis gap; cross-reel overlap (e.g. a 2x2 bonus\n * straddling reel 2 and 3 with a non-zero cross gap) needs this strategy.\n *\n * **v2:** both methods take a single {@link MaskContext} instead of\n * positional arguments, and the context carries the axis. Implementations\n * must set `version = MASK_STRATEGY_VERSION`.\n */\nexport interface MaskStrategy {\n /**\n * Must equal {@link MASK_STRATEGY_VERSION}. Validated by\n * `ReelSetBuilder.maskStrategy()`.\n */\n readonly version: typeof MASK_STRATEGY_VERSION;\n /** Build (or rebuild) the mask graphic. Returns the Graphics to use as the mask. */\n build(ctx: MaskContext): Graphics;\n /** Update the mask when reel boxes resize (e.g. MultiWays reshape). */\n update(graphics: Graphics, ctx: MaskContext): void;\n}\n\n/**\n * v1 default: a per-reel rectangular mask. Each reel is clipped to its own\n * `(mainOffset, extent)` box so pyramid shapes clip cleanly without\n * buffer-cell peek above or below short reels.\n *\n * PixiJS masks support multiple shapes inside a single Graphics. the union\n * of every filled shape is the visible region. So drawing one rect per reel\n * gives the engine a jagged-but-rectangular mask without a custom shader.\n *\n * **Caveat:** if reels have a non-zero CROSS-axis gap (`symbolGap.x` on a\n * vertical set, `symbolGap.y` on a horizontal one), a symbol that extends\n * across the gap (e.g. a 2x2 bonus) will be clipped between the reels. Use\n * {@link SharedRectMaskStrategy} in that case, or drop the cross gap to 0.\n *\n * If `rects` is empty (the builder hasn't supplied per-reel rects yet),\n * this falls back to a single bounding-box rect.\n */\nexport class RectMaskStrategy implements MaskStrategy {\n readonly version = MASK_STRATEGY_VERSION;\n\n build(ctx: MaskContext): Graphics {\n const g = new Graphics();\n this._draw(g, ctx);\n return g;\n }\n\n update(g: Graphics, ctx: MaskContext): void {\n g.clear();\n this._draw(g, ctx);\n }\n\n private _draw(g: Graphics, ctx: MaskContext): void {\n if (ctx.rects.length === 0) {\n g.rect(0, 0, ctx.width, ctx.height).fill({ color: 0xffffff });\n return;\n }\n for (const r of ctx.rects) {\n g.rect(r.x, r.y, r.width, r.height).fill({ color: 0xffffff });\n }\n }\n}\n\n/**\n * Single bounding-box mask covering every reel. Use this\n * when symbols need to overlap across reel boundaries. typical for slots\n * with big symbols that span multiple reels (a 2x2 bonus, a 3x3 giant)\n * AND a non-zero cross-axis gap. Per-reel rects would clip those symbols at\n * the gaps; a single shared rect keeps them visible.\n *\n * Pyramid layouts using this strategy will show buffer cells past the ends\n * of short reels (the \"pyramid peek\". covered by frame art in production).\n *\n * @example\n * builder.maskStrategy(new SharedRectMaskStrategy())\n */\nexport class SharedRectMaskStrategy implements MaskStrategy {\n readonly version = MASK_STRATEGY_VERSION;\n\n build(ctx: MaskContext): Graphics {\n const g = new Graphics();\n this._draw(g, ctx);\n return g;\n }\n\n update(g: Graphics, ctx: MaskContext): void {\n g.clear();\n this._draw(g, ctx);\n }\n\n private _draw(g: Graphics, ctx: MaskContext): void {\n // Cross axis only - `toScreen(bleed, 0)` puts it on x for a vertical set\n // and on y for a horizontal one, so this needs no orientation branch.\n // `?? 0` because `MaskContext` is public and `bleed` was added after v2\n // shipped: a context built by third-party code predates the field, and\n // `toScreen(undefined, 0)` would quietly produce a NaN rect - a mask that\n // clips everything, with no error anywhere.\n const out = ctx.axis.toScreen(ctx.bleed ?? 0, 0);\n g.rect(-out.x, -out.y, ctx.width + out.x * 2, ctx.height + out.y * 2).fill({\n color: 0xffffff,\n });\n }\n}\n\n/**\n * The clipping window + layering tricks for a reel set.\n *\n * The viewport is the \"looking-glass\" of the slot: a rectangle the size\n * of the visible grid with a PixiJS mask so symbols scrolling above or\n * below the visible cells are hidden. It also provides three stacking\n * layers so win animations can break out of the mask:\n *\n * - `maskedContainer`. the normal place for reels. Clipped to the\n * visible area so buffer cells never leak.\n * - `unmaskedContainer`. rendered on top of the mask. Use for a symbol\n * whose celebration animation expands beyond its cell (a big expanding\n * wild, a splash frame).\n * - `spotlightContainer`. above everything else. Win spotlight lifts\n * winning symbols here temporarily so dim overlay + bounce don't clip.\n *\n * `dimOverlay` is a semi-transparent rectangle the spotlight fades in\n * behind the promoted winners to visually push the losers into the\n * background.\n */\nexport class ReelViewport extends Container implements Disposable {\n public readonly maskedContainer: Container;\n public readonly unmaskedContainer: Container;\n public readonly spotlightContainer: Container;\n public readonly dimOverlay: Graphics;\n\n private _mask: Graphics;\n private _maskStrategy: MaskStrategy;\n private _maskWidth: number;\n private _maskHeight: number;\n private _maskRects: ReelMaskRect[] = [];\n private readonly _axis: ReelAxis;\n private readonly _bleed: number;\n private _isDestroyed = false;\n /**\n * Number of active dim requests. The single overlay is shared by the\n * spotlight and cascade `destroySymbols({ dim })`; reference-counting it\n * keeps the dim up until the LAST consumer releases it, so an overlapping\n * pair can't hide it out from under the other.\n */\n private _dimCount = 0;\n\n constructor(\n width: number,\n height: number,\n position: { x: number; y: number } = { x: 0, y: 0 },\n maskStrategy: MaskStrategy = new RectMaskStrategy(),\n axis: ReelAxis = VERTICAL_FORWARD,\n bleed = 0,\n ) {\n super();\n this.x = position.x;\n this.y = position.y;\n this._maskStrategy = maskStrategy;\n this._maskWidth = width;\n this._maskHeight = height;\n this._axis = axis;\n this._bleed = bleed;\n\n this._mask = this._maskStrategy.build(this._maskContext());\n\n this.maskedContainer = new Container();\n this.maskedContainer.sortableChildren = true;\n this.maskedContainer.addChild(this._mask);\n this.maskedContainer.mask = this._mask;\n this.addChild(this.maskedContainer);\n\n this.unmaskedContainer = new Container();\n this.unmaskedContainer.sortableChildren = true;\n this.addChild(this.unmaskedContainer);\n\n this.dimOverlay = new Graphics();\n this.dimOverlay.rect(0, 0, width, height).fill({ color: 0x000000, alpha: 0.5 });\n this.dimOverlay.visible = false;\n this.addChild(this.dimOverlay);\n\n this.spotlightContainer = new Container();\n this.spotlightContainer.sortableChildren = true;\n this.addChild(this.spotlightContainer);\n }\n\n /** The viewport mask bounding box width (independent of children bounds). */\n get maskWidth(): number { return this._maskWidth; }\n /** The viewport mask bounding box height. */\n get maskHeight(): number { return this._maskHeight; }\n /** Per-reel mask rects last passed to the strategy. Used by debug overlays. */\n get maskRects(): readonly ReelMaskRect[] { return this._maskRects; }\n /** Internal mask Graphics. Exposed so debug helpers can recolor it. */\n get maskGraphics(): Graphics { return this._mask; }\n /** The set's travel axis. Read by debug overlays and mask strategies. */\n get axis(): ReelAxis { return this._axis; }\n\n private _maskContext(): MaskContext {\n return {\n rects: this._maskRects,\n width: this._maskWidth,\n height: this._maskHeight,\n axis: this._axis,\n bleed: this._bleed,\n };\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /** Show the dim overlay with given opacity. Reference-counted with hideDim. */\n showDim(alpha: number = 0.5): void {\n this._dimCount++;\n this.dimOverlay.alpha = alpha;\n this.dimOverlay.visible = true;\n }\n\n /** Release one dim request; hides the overlay only when the last one clears. */\n hideDim(): void {\n if (this._dimCount > 0) this._dimCount--;\n if (this._dimCount === 0) this.dimOverlay.visible = false;\n }\n\n /** Update mask size and per-reel rects. Used after pyramid/MultiWays shape changes. */\n updateMaskSize(width: number, height: number, rects: ReelMaskRect[] = []): void {\n this._maskWidth = width;\n this._maskHeight = height;\n this._maskRects = rects;\n this._maskStrategy.update(this._mask, this._maskContext());\n // The dim overlay is sized once at construction; a viewport resize (e.g.\n // a MultiWays reshape growing the tallest reel) must resize it too, or the\n // spotlight dims a stale rectangle. Its runtime alpha is set by showDim.\n this.dimOverlay.clear();\n this.dimOverlay.rect(0, 0, width, height).fill({ color: 0x000000, alpha: 0.5 });\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._isDestroyed = true;\n super.destroy({ children: true });\n }\n}\n","import type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\n\n/**\n * Abstract base for reel spin phases.\n *\n * Each phase represents one stage of the spin lifecycle:\n * START → SPIN → ANTICIPATION → STOP.\n *\n * Phases are entered and exited by SpinController, and can be skipped\n * if marked as skippable and the user triggers skip/slam-stop.\n *\n * @typeParam TConfig - Phase-specific configuration type.\n */\nexport abstract class ReelPhase<TConfig = void> {\n abstract readonly name: string;\n abstract readonly skippable: boolean;\n\n protected _reel: Reel;\n protected _speed: SpeedProfile;\n protected _resolve: (() => void) | null = null;\n protected _isActive = false;\n\n constructor(reel: Reel, speed: SpeedProfile) {\n this._reel = reel;\n this._speed = speed;\n }\n\n get reel(): Reel {\n return this._reel;\n }\n\n get isActive(): boolean {\n return this._isActive;\n }\n\n /** Enter the phase. Returns a promise that resolves when the phase is complete. */\n async run(config: TConfig): Promise<void> {\n this._isActive = true;\n this._reel.events.emit('phase:enter', this.name);\n\n return new Promise<void>((resolve) => {\n this._resolve = () => {\n this._isActive = false;\n this._reel.events.emit('phase:exit', this.name);\n resolve();\n };\n this.onEnter(config);\n });\n }\n\n /** Skip the phase immediately (if skippable). */\n skip(): void {\n if (!this.skippable || !this._isActive) return;\n this.onSkip();\n this._complete();\n }\n\n /** Force-complete the phase regardless of skippable flag. */\n forceComplete(): void {\n if (!this._isActive) return;\n this.onSkip();\n this._complete();\n }\n\n /** Called each frame while the phase is active. */\n abstract update(deltaMs: number): void;\n\n /** Subclass: set up the phase (start tweens, set speed, etc). */\n protected abstract onEnter(config: TConfig): void;\n\n /** Subclass: clean up when skipped or force-completed. */\n protected abstract onSkip(): void;\n\n /** Call when the phase naturally completes. */\n protected _complete(): void {\n if (this._resolve) {\n const resolve = this._resolve;\n this._resolve = null;\n resolve();\n }\n }\n}\n","import type { gsap } from 'gsap';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { SpinningMode } from '../modes/SpinningMode.js';\n\nexport interface StartPhaseConfig {\n /** Spinning mode to set on enter. */\n spinningMode: SpinningMode;\n /** Delay before this reel starts (for staggered start). */\n delay?: number;\n}\n\n/**\n * Accelerates the reel from rest to full spin speed.\n *\n * Optionally performs a brief step-back (reel reverses a tiny amount) before\n * accelerating upward, giving the classic slot machine \"pull\" feel.\n */\nexport class StartPhase extends ReelPhase<StartPhaseConfig> {\n readonly name = 'start';\n readonly skippable = true;\n\n private _tween: gsap.core.Timeline | null = null;\n private _delayedCall: gsap.core.Tween | null = null;\n\n protected onEnter(config: StartPhaseConfig): void {\n const reel = this._reel;\n const delay = config.delay ?? 0;\n\n reel.spinningMode = config.spinningMode;\n reel.speed = 0;\n\n if (delay > 0) {\n this._delayedCall = this._reel.gsap.delayedCall(delay / 1000, () => this._launch());\n } else {\n this._launch();\n }\n }\n\n private _launch(): void {\n this._delayedCall = null;\n const reel = this._reel;\n const speed = this._speed;\n // Re-mask any lifted unmask symbols the instant this reel starts to\n // move. notifySpinStart only fires at accel-end, which would leave an\n // unmasked symbol floating above the mask for the whole ramp.\n reel.beginMotion();\n const accelDuration = (speed.accelerationDuration ?? 300) / 1000;\n const accelEase = speed.accelerationEase ?? 'power2.in';\n\n this._tween = this._reel.gsap.timeline();\n\n // Step-back: brief reverse to give a \"pull\" before launch. This tweens\n // reel.speed, not a position, so it needs no axis routing: the negative\n // speed is direction-relative and ReelMotion.advance multiplies travel by\n // axis.polarity, making it read as \"backwards for this reel\" in any\n // orientation/direction. Multiplying speed by polarity here would instead\n // invert the pull on reverse reels.\n if (speed.bounceDistance > 0) {\n this._tween.to(reel, {\n speed: -2,\n duration: 0.05,\n ease: 'power1.out',\n });\n }\n\n this._tween.to(reel, {\n speed: speed.spinSpeed,\n duration: accelDuration,\n ease: accelEase,\n onComplete: () => {\n reel.notifySpinStart();\n this._complete();\n },\n });\n }\n\n update(_deltaMs: number): void {\n // Motion is driven by reel.speed, updated by Reel.update()\n }\n\n protected onSkip(): void {\n this._kill();\n this._reel.speed = this._speed.spinSpeed;\n // The accel tween died with _kill() before its onComplete could fire\n // notifySpinStart, but the reel keeps spinning through StopPhase.\n // symbols must still learn they're in a spin (blur / static-spin\n // presentations). Safe if it already fired: the hook is idempotent.\n this._reel.notifySpinStart();\n }\n\n private _kill(): void {\n if (this._delayedCall) {\n this._delayedCall.kill();\n this._delayedCall = null;\n }\n if (this._tween) {\n this._tween.kill();\n this._tween = null;\n }\n }\n}\n","import { ReelPhase } from './ReelPhase.js';\n\nexport interface SpinPhaseConfig {\n /** Minimum time to spin before allowing stop. Overrides speed profile if set. */\n minimumSpinTime?: number;\n}\n\n/**\n * Continuous spinning at constant speed.\n *\n * Runs until externally resolved (when setResult arrives). Tracks minimum\n * spin time via ticker accumulation so it behaves consistently when the tab\n * is hidden (no reliance on wall-clock performance.now()).\n */\nexport class SpinPhase extends ReelPhase<SpinPhaseConfig> {\n readonly name = 'spin';\n readonly skippable = false;\n\n private _elapsed = 0;\n private _minTime = 0;\n private _readyToStop = false;\n\n protected onEnter(config: SpinPhaseConfig): void {\n this._elapsed = 0;\n this._minTime = config.minimumSpinTime ?? this._speed.minimumSpinTime ?? 500;\n this._readyToStop = false;\n }\n\n update(deltaMs: number): void {\n this._elapsed += deltaMs;\n if (this._readyToStop && this._elapsed >= this._minTime) {\n this._complete();\n }\n }\n\n /** Signal that this phase should end (called by SpinController when result arrives). */\n resolve(): void {\n this._readyToStop = true;\n if (this._elapsed >= this._minTime) {\n this._complete();\n }\n }\n\n protected onSkip(): void {\n // SpinPhase is not skippable.\n }\n}\n","import type { gsap } from 'gsap';\nimport { ReelPhase } from './ReelPhase.js';\n\nexport interface StopPhaseConfig {\n /** Target symbols for this reel (full frame including buffers, top-to-bottom). */\n targetFrame: string[];\n /** Delay before this reel starts stopping (for staggered stop). */\n delay?: number;\n /**\n * Keep the reel's CURRENT speed into the spin-out instead of restoring full\n * spin speed. Set by the controller when this stop follows an anticipation\n * tease, so the reel crawls its target into place at the slow anticipation\n * speed and stops exactly there. rather than snapping back to full speed and\n * doing a fast spin-out. A small floor is applied so a `slowdown.to: 0`\n * curve can't stall the reel.\n */\n preserveSpeed?: boolean;\n}\n\n/**\n * Stops the reel on the target frame.\n *\n * Sequence:\n * 1. Wait for the staggered delay.\n * 2. Keep spinning at full speed with `isStopping` flagged. The target frame\n * is loaded into the StopSequencer; each wrap event at the top of the\n * reel pulls the next frame symbol. so targets arrive in the visible\n * area naturally, carrying the full momentum of the spin.\n * 3. When the sequencer is exhausted, snap to grid and bounce:\n * - overshoot downward by `bounceDistance` with `power1.out`\n * - settle back upward with `power1.out`\n * Both legs share a duration so the down + up motion is symmetric.\n */\nexport class StopPhase extends ReelPhase<StopPhaseConfig> {\n readonly name = 'stop';\n readonly skippable = true;\n\n private _config: StopPhaseConfig | null = null;\n private _delayTween: gsap.core.Tween | null = null;\n private _bounceTween: gsap.core.Timeline | null = null;\n private _stage: 'delay' | 'spinning' | 'bouncing' | 'done' = 'delay';\n private _baseY = 0;\n\n protected onEnter(config: StopPhaseConfig): void {\n this._config = config;\n this._stage = 'delay';\n this._baseY = this._reel.axis.getMain(this._reel.container);\n\n const delay = (config.delay ?? 0) / 1000;\n if (delay > 0) {\n this._delayTween = this._reel.gsap.delayedCall(delay, () => this._beginSpinOut());\n } else {\n this._beginSpinOut();\n }\n }\n\n private _beginSpinOut(): void {\n if (!this._config) return;\n const reel = this._reel;\n const speed = this._speed;\n\n reel.setStopFrame(this._config.targetFrame);\n reel.isStopping = true;\n if (this._config.preserveSpeed) {\n // Following an anticipation tease: keep the current (slow) speed so the\n // reel crawls its target frame into place and stops exactly there,\n // rather than re-accelerating to full speed. Floor it so a near-zero\n // anticipation speed can't stall the spin-out forever.\n reel.speed = Math.max(reel.speed, speed.spinSpeed * 0.08);\n } else {\n // Restore full spin speed. anticipation or other phases may have lowered\n // it. The full momentum carries through the final frame placement.\n reel.speed = speed.spinSpeed;\n }\n\n this._stage = 'spinning';\n }\n\n update(_deltaMs: number): void {\n if (this._stage !== 'spinning') return;\n // Sequencer consumes one symbol per wrap via Reel._onSymbolWrapped.\n // When it's empty, the target frame is fully placed. time to land.\n if (!this._reel.stopSequencer.hasRemaining) {\n this._landAndBounce();\n }\n }\n\n private _landAndBounce(): void {\n const reel = this._reel;\n const speed = this._speed;\n\n reel.speed = 0;\n reel.isStopping = false;\n reel.snapToGrid();\n reel.notifySpinEnd();\n reel.notifyLanded();\n\n const bounceDistance = speed.bounceDistance;\n if (bounceDistance <= 0) {\n this._stage = 'done';\n this._complete();\n return;\n }\n\n const legDuration = (speed.bounceDuration ?? 600) / 2000; // half of total, in seconds\n // Overshoot in the direction of travel: forward reels overshoot toward the\n // larger main coordinate, reverse reels toward the smaller. axis.polarity\n // makes this automatic and keeps vertical/forward at `base + bounceDistance`.\n const axis = reel.axis;\n this._stage = 'bouncing';\n\n // `notifyLanded()` just lifted every at-rest unmask symbol into\n // `viewport.unmaskedContainer`, where the reel offset is baked into the\n // view's own coordinate instead of inherited from `reel.container`. The\n // bounce moves the container, so without this the lifted views hang\n // motionless for the whole overshoot while the reel travels under them.\n let followedMain = this._baseY;\n const followLifted = (): void => {\n const main = axis.getMain(reel.container);\n reel.offsetLiftedViews(main - followedMain);\n followedMain = main;\n };\n\n this._bounceTween = this._reel.gsap.timeline();\n this._bounceTween.to(reel.container, {\n [axis.mainProp]: this._baseY + axis.polarity * bounceDistance,\n duration: legDuration,\n ease: 'power1.out',\n onUpdate: followLifted,\n });\n this._bounceTween.to(reel.container, {\n [axis.mainProp]: this._baseY,\n duration: legDuration,\n ease: 'power1.out',\n onUpdate: followLifted,\n onComplete: () => {\n // The last onUpdate can land a hair short of the end value; settle the\n // lifted views on the exact resting position rather than that epsilon.\n followLifted();\n this._stage = 'done';\n this._complete();\n },\n });\n }\n\n protected onSkip(): void {\n this._killTweens();\n const reel = this._reel;\n reel.speed = 0;\n reel.isStopping = false;\n\n if (this._stage !== 'done' && this._config) {\n // Place the FULL target frame, not just the visible window — slicing to\n // [bufferStart, bufferStart+visible] dropped buffer-above/below targets\n // (e.g. a big symbol's tail parked in bufferStart), so a direct skip()\n // landed the wrong frame. targetFrame is already a flat top-to-bottom\n // strip, which is exactly what placeStrip consumes.\n reel.placeStrip(this._config.targetFrame);\n }\n // Rest the container BEFORE snapping. `snapToGrid` re-bakes the container's\n // current main coordinate into any lifted unmask view, so skipping mid-bounce\n // used to bake the overshoot position and then move the container out from\n // under it, leaving the view off by however far the bounce had travelled.\n reel.axis.setMain(reel.container, this._baseY);\n reel.snapToGrid();\n this._stage = 'done';\n }\n\n private _killTweens(): void {\n if (this._delayTween) {\n this._delayTween.kill();\n this._delayTween = null;\n }\n if (this._bounceTween) {\n this._bounceTween.kill();\n this._bounceTween = null;\n }\n }\n}\n","import type { gsap } from 'gsap';\nimport { ReelPhase } from './ReelPhase.js';\n\nexport interface AnticipationPhaseConfig {\n /** Duration override in ms. Uses speed profile anticipationDelay if not set. */\n duration?: number;\n /** Speed multiplier during anticipation. Default: 0.3 (30% of spin speed). */\n speedMultiplier?: number;\n}\n\n/**\n * Anticipation phase: slow-down tease before a reel stops.\n *\n * Decelerates to a fraction of spin speed, holds for a duration, then hands\n * off to StopPhase. The controller runs StopPhase with `preserveSpeed: true`\n * after a tease, so this low speed carries into the spin-out and the reel\n * crawls onto its landing frame instead of re-accelerating.\n */\nexport class AnticipationPhase extends ReelPhase<AnticipationPhaseConfig> {\n readonly name = 'anticipation';\n readonly skippable = true;\n\n private _tween: gsap.core.Timeline | null = null;\n\n protected onEnter(config: AnticipationPhaseConfig): void {\n const reel = this._reel;\n const speed = this._speed;\n const duration = (config.duration ?? speed.anticipationDelay) / 1000;\n const targetSpeed = speed.spinSpeed * (config.speedMultiplier ?? 0.3);\n\n if (duration <= 0) {\n this._complete();\n return;\n }\n\n this._tween = this._reel.gsap.timeline();\n\n this._tween.to(reel, {\n speed: targetSpeed,\n duration: duration * 0.35,\n ease: 'power2.out',\n });\n this._tween.to({}, { duration: duration * 0.65, onComplete: () => this._complete() });\n }\n\n update(_deltaMs: number): void {\n // Driven by GSAP tweens\n }\n\n protected onSkip(): void {\n this._kill();\n this._reel.speed = this._speed.spinSpeed;\n }\n\n private _kill(): void {\n if (this._tween) {\n this._tween.kill();\n this._tween = null;\n }\n }\n}\n","import type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport { StartPhase } from './StartPhase.js';\nimport { SpinPhase } from './SpinPhase.js';\nimport { StopPhase } from './StopPhase.js';\nimport { AnticipationPhase } from './AnticipationPhase.js';\n\nexport type PhaseConstructor<T extends ReelPhase<any> = ReelPhase<any>> =\n new (reel: Reel, speed: SpeedProfile) => T;\n\nexport type PhaseCreatorFn<T extends ReelPhase<any> = ReelPhase<any>> =\n (reel: Reel, speed: SpeedProfile) => T;\n\n/**\n * Factory for creating reel phase instances.\n *\n * Ships with all four default phases pre-registered.\n * Users can override any phase by registering a custom constructor or factory function.\n * Use registerFactory() when the phase needs extra construction-time config\n * (e.g. cascade drop settings baked in via closure).\n */\nexport class PhaseFactory {\n private _registry = new Map<string, PhaseCreatorFn>();\n\n constructor() {\n this._registry.set('start', (r, s) => new StartPhase(r, s));\n this._registry.set('spin', (r, s) => new SpinPhase(r, s));\n this._registry.set('stop', (r, s) => new StopPhase(r, s));\n this._registry.set('anticipation', (r, s) => new AnticipationPhase(r, s));\n }\n\n /** Register or override a phase type by constructor. */\n register<T extends ReelPhase<any>>(name: string, PhaseClass: PhaseConstructor<T>): void {\n this._registry.set(name, (r, s) => new PhaseClass(r, s));\n }\n\n /**\n * Register or override a phase type by factory function.\n * Use this when the phase needs extra args at construction time.\n *\n * @example\n * factory.registerFactory('cascade:dropIn', (reel, speed) => new CascadeDropInPhase(reel, speed, dropConfig));\n */\n registerFactory<T extends ReelPhase<any>>(\n name: string,\n factory: PhaseCreatorFn<T>,\n ): void {\n this._registry.set(name, factory);\n }\n\n /** Create a phase instance for a reel. */\n create<T extends ReelPhase<any> = ReelPhase<any>>(\n name: string,\n reel: Reel,\n speed: SpeedProfile,\n ): T {\n const creator = this._registry.get(name);\n if (!creator) {\n throw new Error(\n `Phase '${name}' not registered. Available: ${[...this._registry.keys()].join(', ')}`,\n );\n }\n return creator(reel, speed) as T;\n }\n\n has(name: string): boolean {\n return this._registry.has(name);\n }\n}\n","import type { Ticker } from 'pixi.js';\nimport type { Reel } from '../core/Reel.js';\nimport type {\n AnticipationOptions,\n AnticipationSlowdown,\n AnticipationStagger,\n SpeedProfile,\n SpinOptions,\n SymbolData,\n} from '../config/types.js';\nimport type { SpeedManager } from '../speed/SpeedManager.js';\nimport type { FrameBuilder } from '../frame/FrameBuilder.js';\nimport type { SpinResult } from '../events/ReelEvents.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../events/ReelEvents.js';\nimport { PhaseFactory } from './phases/PhaseFactory.js';\nimport type { SpinPhase } from './phases/SpinPhase.js';\nimport type { ReelPhase } from './phases/ReelPhase.js';\nimport type { StartPhaseConfig } from './phases/StartPhase.js';\nimport type { StopPhaseConfig } from './phases/StopPhase.js';\nimport type { AnticipationPhaseConfig } from './phases/AnticipationPhase.js';\nimport type { AdjustPhaseConfig } from './phases/AdjustPhase.js';\nimport type { CascadeFallPhaseConfig } from './phases/CascadeFallPhase.js';\nimport type { CascadePlacePhaseConfig } from './phases/CascadePlacePhase.js';\nimport type { CascadeDropInPhaseConfig } from './phases/CascadeDropInPhase.js';\nimport type { SpinningMode } from './modes/SpinningMode.js';\nimport { StandardMode } from './modes/StandardMode.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { TickerRef } from '../utils/TickerRef.js';\nimport { OCCUPIED_SENTINEL } from '../core/Reel.js';\nimport type { CellPin } from '../pins/CellPin.js';\nimport {\n cloneColumnTarget,\n getTargetSlot,\n setTargetSlot,\n type ColumnTarget,\n} from '../frame/ColumnTarget.js';\nimport type { Cell } from '../cascade/tumbleAlgorithm.js';\n\n/**\n * MultiWays/big-symbol coordination hook injected by `ReelSet` into\n * `SpinController`. All callbacks are no-ops (and `isMultiWaysSlot=false`)\n * for non-MultiWays slots, so the standard chain is unchanged.\n */\nexport interface SpinControllerHooks {\n isMultiWaysSlot: boolean;\n symbolsData: Record<string, SymbolData>;\n /** Read pending MultiWays shape. Returns null when no shape is pending. */\n peekTargetShape(): number[] | null;\n /** Clear pending shape after AdjustPhase runs. */\n clearTargetShape(): void;\n /** Reel pixel-box height for MultiWays cell-height derivation. */\n multiwaysReelExtent: number;\n /** Reel-scoped pin lookup. Used to build AdjustPhase tween descriptors. */\n getPinsOnReel(reelIndex: number): CellPin[];\n /**\n * Migrate pins on a reel to a new visible-cell count, returning the\n * resulting moves. Mutates the pin map directly inside ReelSet.\n */\n migratePinsForReel(reelIndex: number, newCells: number): {\n pin: CellPin;\n fromCell: number;\n toCell: number;\n clamped: boolean;\n }[];\n /**\n * Reposition + resize every pin overlay on the given reel. Called after\n * AdjustPhase commits a MultiWays reshape so overlays move to their new\n * (post-migration) cell at the new cell size.\n */\n refreshPinOverlaysForReel(reelIndex: number): void;\n /**\n * Build AdjustPhase pin-overlay tween descriptors for a reel. one per\n * active pin overlay. Captures pre-reshape (current) Y/size from the\n * overlay and computes post-reshape target. Called BEFORE the reshape\n * commits so the \"from\" state reflects what's actually on screen.\n */\n buildPinOverlayTweens(\n reelIndex: number,\n targetCellMain: number,\n ): import('./phases/AdjustPhase.js').PinOverlayTween[];\n}\n\n/**\n * The conductor of a spin.\n *\n * A reel set has many moving parts; the `SpinController` is the single\n * brain that drives them in time. On `spin()` it walks every reel through\n * its phase state machine (`StartPhase` → `SpinPhase` → optional\n * `AnticipationPhase` → `StopPhase`), applies the per-reel staggered\n * delays from the `SpeedProfile`, and resolves a promise when the last\n * reel lands (or the spin is skipped).\n *\n * It does not draw anything. drawing lives on `Reel` and `ReelSymbol`.\n * It does not decide outcomes. that's `setResult(grid)` coming in from\n * your game code. Its one job is timing.\n *\n * Every interesting moment fires on the event bus:\n * `spin:start`, `spin:allStarted`, `spin:stopping`, `spin:reelLanded`,\n * `spin:allLanded`, `spin:complete`, `skip:requested`, `skip:completed`.\n */\nexport class SpinController implements Disposable {\n private _reels: Reel[];\n private _speedManager: SpeedManager;\n private _frameBuilder: FrameBuilder;\n private _phaseFactory: PhaseFactory;\n private _events: EventEmitter<ReelSetEvents>;\n private _tickerRef: TickerRef;\n private _spinningMode: SpinningMode;\n private _defaultSpinMode: 'standard' | 'cascade';\n private _currentSpinMode: 'standard' | 'cascade' = 'standard';\n private _hooks: SpinControllerHooks;\n\n private _isSpinning = false;\n private _spinStartTime = 0;\n private _resultSymbols: ColumnTarget[] | null = null;\n private _anticipationReels: number[] = [];\n /**\n * How the START of each anticipation reel's slow-down is spaced. See\n * {@link setAnticipation}. `0` (or a single tease reel) reproduces the\n * legacy behaviour where every anticipation reel begins slowing at once.\n */\n private _anticipationStagger: AnticipationStagger = 0;\n /**\n * Progressive slow-down curve applied across the tease sequence, or `null`\n * for the flat default (every anticipation reel drops to the phase default\n * of 30% spin speed). See {@link setAnticipation}. Cleared per spin.\n */\n private _anticipationSlowdown: AnticipationSlowdown | null = null;\n /**\n * Explicit anticipation hold (ms) that OVERRIDES the active speed profile's\n * `anticipationDelay`. Set via `setAnticipation(reels, { duration })`. `null`\n * means \"use the profile\". A positive value also lets the tease play when the\n * profile's `anticipationDelay` is `0` (Turbo / SuperTurbo). Cleared per spin.\n */\n private _anticipationDuration: number | null = null;\n /**\n * Reels that actually entered a tease this spin. populated when\n * `anticipation:reel` fires, drained in `_markLanded` to fire\n * `anticipation:reelEnd` only for reels that teased. Cleared per spin.\n */\n private _teasingReels = new Set<number>();\n /**\n * `'sequential'` anticipation chaining state: one deferred per anticipation\n * reel, resolved when that reel lands (in `_markLanded`). Reel at tease-order\n * `k` awaits the deferred of the reel at order `k-1` before starting its\n * tease. Rebuilt each `setAnticipation('sequential')`; cleared per spin.\n */\n private _reelLandedResolvers: Map<number, () => void> = new Map();\n private _reelLandedPromises: Map<number, Promise<void>> = new Map();\n private _stopDelayOverride: number[] | null = null;\n private _activePhases: Map<number, ReelPhase<any>> = new Map();\n private _landedReels = new Set<number>();\n /**\n * Reels held for the current spin (per `SpinOptions.holdReels`). Held\n * reels skip START / SPIN / STOP and stay on their current symbols.\n * Cleared at the start of every spin.\n */\n private _heldReels = new Set<number>();\n private _wasSkipped = false;\n private _skipPending = false;\n private _isDestroyed = false;\n private _currentSpinResolve: ((result: SpinResult) => void) | null = null;\n private _currentSpinReject: ((error: Error) => void) | null = null;\n /**\n * Set by `_abortSpin()` so the shared settle point `_finishSpin()` rejects\n * the spin promise (and skips the success events) instead of resolving.\n */\n private _pendingAbortError: Error | null = null;\n /** Removes the active spin's abort listener and clears its watchdog timer. */\n private _spinWatchdogCleanup: (() => void) | null = null;\n /** Incremented on each new spin. If a callback sees a stale generation, it no-ops. */\n private _spinGeneration = 0;\n /**\n * Round-aware `skip()` state. Lives across `refill()` calls within a\n * round (one `spin()` + its cascade refills) and resets on the next\n * `spin()`.\n *\n * `0`. no press yet this round.\n * `2`. a press has slammed (and applied the round's side effect: a\n * speed boost in standard mode or auto-slam-refills in cascade).\n * Subsequent presses also slam.\n *\n * `1` is reserved (kept for forward compat in the type) but currently\n * unreachable. every press slams now, side effects are applied on the\n * first press together with the slam.\n */\n private _skipStage: 0 | 1 | 2 = 0;\n /**\n * Speed profile name that was active when the round-start boost fired,\n * captured so the next `spin()` can restore it. `null` between rounds and\n * during rounds where the player never pressed skip.\n */\n private _skipPreviousSpeedName: string | null = null;\n /**\n * Speed profile name we boosted INTO. Kept for telemetry / debugging;\n * the restore decision uses `_manualSpeedSinceBoost` instead, which\n * correctly distinguishes \"user didn't touch speed\" from \"user happened\n * to manually re-set to the boosted value\" (the activeName check alone\n * can't tell those apart).\n */\n private _skipBoostedToName: string | null = null;\n /**\n * `true` when the app called `setSpeed()` between the round-start boost\n * and the next `spin()`. i.e. the user made an explicit speed choice\n * after the boost. The next `spin()` restore path checks this flag and\n * SKIPS the restore so the manual choice survives, even if the manual\n * choice happens to be the same name we boosted into.\n *\n * Set by `notifyManualSpeedChange()` (called from `ReelSet.setSpeed`).\n * Cleared at the start of every `spin()` together with the boost\n * bookkeeping.\n */\n private _manualSpeedSinceBoost = false;\n /**\n * Cascade-mode round flag. When true, the next `refill()` skips its\n * phase chain and slams instantly. Set when the player presses `skip()`\n * during a cascade round (one press = \"fast-forward to end of round\").\n * Cleared on the next `spin()` alongside the rest of the stage state.\n */\n private _autoSlamRefills = false;\n\n constructor(\n reels: Reel[],\n speedManager: SpeedManager,\n frameBuilder: FrameBuilder,\n phaseFactory: PhaseFactory,\n events: EventEmitter<ReelSetEvents>,\n ticker: Ticker,\n spinningMode?: SpinningMode,\n defaultSpinMode: 'standard' | 'cascade' = 'standard',\n hooks?: SpinControllerHooks,\n ) {\n this._reels = reels;\n this._speedManager = speedManager;\n this._frameBuilder = frameBuilder;\n this._phaseFactory = phaseFactory;\n this._events = events;\n this._tickerRef = new TickerRef(ticker);\n this._spinningMode = spinningMode ?? new StandardMode();\n this._defaultSpinMode = defaultSpinMode;\n this._hooks = hooks ?? {\n isMultiWaysSlot: false,\n symbolsData: {},\n peekTargetShape: () => null,\n clearTargetShape: () => {},\n multiwaysReelExtent: 0,\n getPinsOnReel: () => [],\n migratePinsForReel: () => [],\n refreshPinOverlaysForReel: () => {},\n buildPinOverlayTweens: () => [],\n };\n\n this._tickerRef.add((ticker) => this._onTick(ticker));\n }\n\n get isSpinning(): boolean {\n return this._isSpinning;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Current `skip()` position within the active round. `0` until the\n * player presses the slam button, `2` after. Use to drive UI button\n * labels (e.g. \"Skip\" → \"Skipped\"). `1` is reserved for forward compat\n * and is not currently reachable.\n */\n get skipStage(): 0 | 1 | 2 {\n return this._skipStage;\n }\n\n async spin(options?: SpinOptions): Promise<SpinResult> {\n if (this._isSpinning) {\n throw new Error('Cannot start a new spin while one is in progress.');\n }\n\n // Already-aborted signal: never even start the reels.\n if (options?.signal?.aborted) {\n return Promise.reject(this._abortError(options.signal));\n }\n\n const mode = options?.mode ?? this._defaultSpinMode;\n if (mode === 'cascade' && !this._phaseFactory.has('cascade:fall')) {\n throw new Error(\n \"spin({ mode: 'cascade' }) requires .tumble(...) on the builder.\",\n );\n }\n if (mode === 'standard' && this._reels.some((r) => r.bufferEnd === 0)) {\n throw new Error(\n \"spin({ mode: 'standard' }) requires bufferEnd >= 1: strip scrolling \" +\n 'wraps symbols through the below-window buffer. This reel set was ' +\n 'built with bufferSymbols({ end: 0 }) for tumble-only use.',\n );\n }\n this._currentSpinMode = mode;\n\n // Round boundary: a new `spin()` ends the previous round. If the\n // player boosted via `skip()` last round AND did NOT manually call\n // `setSpeed()` between rounds, restore the pre-boost speed. The\n // manual-flag check is what distinguishes \"user untouched, restore\"\n // from \"user explicitly chose the boosted name, leave alone\". the\n // activeName comparison alone can't tell those apart.\n if (this._skipPreviousSpeedName !== null) {\n const prev = this._skipPreviousSpeedName;\n this._skipPreviousSpeedName = null;\n this._skipBoostedToName = null;\n if (!this._manualSpeedSinceBoost && this._speedManager.activeName !== prev) {\n this._speedManager.set(prev);\n }\n }\n this._manualSpeedSinceBoost = false;\n this._skipStage = 0;\n this._autoSlamRefills = false;\n\n this._isSpinning = true;\n this._wasSkipped = false;\n this._skipPending = false;\n this._pendingAbortError = null;\n this._spinStartTime = performance.now();\n this._resultSymbols = null;\n this._anticipationReels = [];\n this._anticipationStagger = 0;\n this._anticipationSlowdown = null;\n this._anticipationDuration = null;\n this._teasingReels.clear();\n this._reelLandedResolvers.clear();\n this._reelLandedPromises.clear();\n // NOTE: _stopDelayOverride is NOT cleared here. The contract is that\n // `setDropOrder()` (or `setStopDelays()`) is called right before\n // `spin()` / `refill()` and represents user intent for the upcoming\n // sequence. Clearing it on entry would silently drop the value the\n // user just set. The override persists until the next setDropOrder()\n // call overwrites it.\n this._landedReels.clear();\n this._activePhases.clear();\n this._heldReels = this._normalizeHoldReels(options?.holdReels);\n this._spinGeneration++;\n\n const generation = this._spinGeneration;\n const speed = this._speedManager.active;\n\n this._events.emit('spin:start');\n\n const resultPromise = new Promise<SpinResult>((resolve, reject) => {\n this._currentSpinResolve = resolve;\n this._currentSpinReject = reject;\n });\n this._armSpinWatchdog(options, generation);\n\n // Degenerate case: every reel held → resolve next microtask with the\n // current visible grid. Spin emitted, but no animation runs.\n if (this._heldReels.size === this._reels.length) {\n Promise.resolve().then(() => {\n if (generation !== this._spinGeneration) return;\n this._finishSpin();\n });\n return resultPromise;\n }\n\n for (let i = 0; i < this._reels.length; i++) {\n if (this._heldReels.has(i)) continue;\n this._runReelTask(this._startReel(i, speed, generation), 'spin', i, generation);\n }\n\n return resultPromise;\n }\n\n /**\n * Wrap a per-reel async phase chain with an error guard. If the chain\n * rejects we log the error and force a slam so:\n * 1. the spin promise resolves with `wasSkipped: true` instead of\n * hanging forever waiting for the failed reel to land,\n * 2. every other reel is brought to a clean landed state,\n * 3. the next `spin()` / `refill()` starts from a coherent snapshot.\n *\n * Generation-guarded so a late rejection from a stale spin (one that\n * was already replaced by a fresh `spin()` call) is dropped silently.\n */\n private _runReelTask(\n p: Promise<void>,\n kind: 'spin' | 'refill',\n reelIndex: number,\n generation: number,\n ): void {\n p.catch((err: unknown) => {\n if (generation !== this._spinGeneration) return;\n // eslint-disable-next-line no-console\n console.error(\n `[pixi-reels] reel ${reelIndex} (${kind}) phase chain threw. slamming to recover:`,\n err,\n );\n this._slam();\n });\n }\n\n /**\n * Filter `holdReels` down to a clean Set: drop out-of-range, drop\n * duplicates, drop non-integer entries. Returning a normalized set\n * makes every internal call site safe to read without re-validating.\n */\n private _normalizeHoldReels(input: number[] | undefined): Set<number> {\n const out = new Set<number>();\n if (!input) return out;\n for (const i of input) {\n if (Number.isInteger(i) && i >= 0 && i < this._reels.length) {\n out.add(i);\n }\n }\n return out;\n }\n\n setResult(symbols: ColumnTarget[]): void {\n if (!this._isSpinning) return;\n // Fail-fast: validate big-symbol block fit so setResult throws at the\n // call site rather than later inside skip()/_tryBeginStopSequence().\n const visibleCellsForReel = (i: number): number => {\n const pendingShape = this._hooks.peekTargetShape();\n return pendingShape ? pendingShape[i] : this._reels[i].visibleCells;\n };\n this._coordinateBigSymbols(symbols, visibleCellsForReel);\n this._resultSymbols = symbols;\n this._tryBeginStopSequence();\n if (this._skipPending) {\n // Deferred `requestSkip()` is an explicit slam intent. bypass the\n // two-stage `skip()` machine and slam directly.\n this._skipPending = false;\n this._slam();\n this._skipStage = 2;\n }\n }\n\n /**\n * Tumble cascade: place + drop-in for a refill (Moment B). Skips the\n * fall and the wait-for-result. the caller already cleared the winning\n * cells in user code and is now handing us the next grid directly.\n *\n * Two refill modes:\n *\n * - `'combined'` (default). survivors and new symbols animate together\n * in one drop-in phase. The classic Sweet Bonanza / Sugar Rush feel.\n * - `'gravity-then-drop'`. survivors slide down to fill holes FIRST\n * (gravity stage), then a global hold, then new symbols drop in from\n * above (drop-in stage). The Mummyland Treasures / Reactoonz feel.\n * gives space for anticipation visuals between the two beats. Per-reel\n * stop delays (`setDropOrder`) apply to the drop-in stage only; the\n * gravity stage runs simultaneously across all reels.\n *\n * The hold between gravity and drop-in is the **max** of three sources\n * (Promise.all semantics. whichever finishes LAST gates the drop-in):\n *\n * - `gravityHoldMs` (default `250`). fixed wall-clock pause via setTimeout.\n * - `gravityHold: Promise<void>`. caller-supplied promise. Use when you\n * already have an in-flight animation/SFX/etc. and want to wait for it\n * by handle rather than wrapping in a callback.\n * - `onGravityComplete: () => Promise<void> | void`. callback invoked\n * at the gravity-end boundary; its returned promise is awaited.\n *\n * `gravityHoldMs` and `gravityHold` race in parallel (Promise.all of the\n * two. both must finish before drop-in starts). `onGravityComplete` runs\n * AFTER both complete, so it can read final state of whatever they were\n * waiting on.\n *\n * Throws if a spin or refill is already in flight, if `.tumble(...)` was\n * not configured on the builder, if the grid shape doesn't match the\n * reel set, or if any winner cell is out of range. All validation runs\n * BEFORE the spinning state is taken so a thrown error leaves the engine\n * idle (callers can retry without re-entry errors).\n */\n async refill(opts: {\n winners: ReadonlyArray<Cell>;\n grid: ColumnTarget[];\n mode?: 'combined' | 'gravity-then-drop';\n gravityHoldMs?: number;\n /**\n * Promise (or zero-arg factory) gating the drop-in stage. Pass a\n * factory function. `() => Promise<void>`. to defer creation until\n * the engine actually reaches the gravity-end boundary; the side\n * effect of building the promise (e.g. starting a multiplier\n * animation) then lines up with the gravity-end beat the player sees.\n * Pass a bare `Promise<void>` if you already have an in-flight\n * animation handle you just want the engine to wait on.\n */\n gravityHold?: Promise<void> | (() => Promise<void>);\n onGravityComplete?: () => Promise<void> | void;\n }): Promise<SpinResult> {\n if (this._isSpinning) {\n throw new Error('Cannot refill while a spin or refill is in progress.');\n }\n if (!this._phaseFactory.has('cascade:place')) {\n throw new Error('refill() requires .tumble(...) on the builder.');\n }\n\n // The cascade grid describes the visible window; buffer entries, if any,\n // ride along untouched. Check the visible run per column.\n const normalizedGrid = opts.grid;\n if (normalizedGrid.length !== this._reels.length) {\n throw new RangeError(\n `refill: grid has ${normalizedGrid.length} column(s) but the reel set has ` +\n `${this._reels.length}.`,\n );\n }\n for (let i = 0; i < normalizedGrid.length; i++) {\n const expected = this._reels[i].visibleCells;\n if (normalizedGrid[i].visible.length !== expected) {\n throw new RangeError(\n `refill: grid column ${i} has ${normalizedGrid[i].visible.length} cell(s) but ` +\n `reel ${i} has ${expected} visible cell(s).`,\n );\n }\n }\n for (const w of opts.winners) {\n if (!Number.isInteger(w.reel) || w.reel < 0 || w.reel >= this._reels.length) {\n throw new RangeError(\n `refill: winner.reel ${w.reel} out of range [0, ${this._reels.length}).`,\n );\n }\n const cells = this._reels[w.reel].visibleCells;\n if (!Number.isInteger(w.cell) || w.cell < 0 || w.cell >= cells) {\n throw new RangeError(\n `refill: winner.cell ${w.cell} out of range [0, ${cells}) for reel ${w.reel}.`,\n );\n }\n }\n\n this._isSpinning = true;\n this._wasSkipped = false;\n this._skipPending = false;\n this._pendingAbortError = null;\n this._spinStartTime = performance.now();\n this._resultSymbols = null;\n this._anticipationReels = [];\n this._anticipationStagger = 0;\n this._anticipationSlowdown = null;\n this._anticipationDuration = null;\n this._teasingReels.clear();\n this._reelLandedResolvers.clear();\n this._reelLandedPromises.clear();\n // _stopDelayOverride preserved across entry. see spin() for rationale.\n // Cascade recipes set `setDropOrder('all')` right before refill() and\n // would otherwise see their setting clobbered, falling back to the\n // default `i * speed.stopDelay` left-to-right stagger.\n this._landedReels.clear();\n this._activePhases.clear();\n this._heldReels = new Set();\n this._spinGeneration++;\n this._currentSpinMode = 'cascade';\n\n const generation = this._spinGeneration;\n const speed = this._speedManager.active;\n\n // Normalize grid + build per-reel frames upfront. No waiting on\n // `setResult` here. the caller provided everything. Reuses the\n // already-validated `normalizedGrid` from the entry guards.\n this._resultSymbols = normalizedGrid;\n const decorated = this._coordinateBigSymbols(normalizedGrid, (i) => this._reels[i].visibleCells);\n const frames: string[][] = [];\n for (let i = 0; i < this._reels.length; i++) {\n const reel = this._reels[i];\n frames.push(\n this._frameBuilder.build(i, reel.visibleCells, reel.bufferStart, reel.bufferEnd, decorated[i]),\n );\n }\n this._cachedFrames = frames;\n\n // Group winners per reel and sort ascending. the gravity algorithm\n // expects ascending winner cells when it builds nonWinnerCells.\n const winnersByReel = new Map<number, number[]>();\n for (const w of opts.winners) {\n let arr = winnersByReel.get(w.reel);\n if (!arr) {\n arr = [];\n winnersByReel.set(w.reel, arr);\n }\n arr.push(w.cell);\n }\n for (const arr of winnersByReel.values()) arr.sort((a, b) => a - b);\n\n this._events.emit('spin:start');\n\n const resultPromise = new Promise<SpinResult>((resolve) => {\n this._currentSpinResolve = resolve;\n // Refills are driven from an already-known grid, so they carry no\n // external watchdog. Drop any stale reject handle from the spin() that\n // opened this round.\n this._currentSpinReject = null;\n });\n\n // Auto-slam: skip() set this earlier in the round to mean \"fast-forward\n // the rest of this cascade.\" Bypass the place + dropIn phase chain and\n // land instantly. `_slam()` sees no active phases, `_resultSymbols` is\n // set, and per-reel placement happens synchronously.\n if (this._autoSlamRefills) {\n this._slam();\n this._skipStage = 2;\n return resultPromise;\n }\n\n const mode = opts.mode ?? 'combined';\n\n if (mode === 'gravity-then-drop') {\n // Two-stage orchestration. All reels do place + gravity in parallel\n // (no per-reel stop delay. gravity is a global \"settling\" beat,\n // not a reveal). Once every reel's gravity is done, wait for the\n // combined hold (Promise.all of `gravityHoldMs` setTimeout +\n // optional `gravityHold` promise + optional `onGravityComplete`\n // callback's returned promise), then start the drop-in stage with\n // the user's per-reel stop delays applied.\n const gravityHoldMs = opts.gravityHoldMs ?? 250;\n this._refillTwoStage(\n speed,\n generation,\n winnersByReel,\n gravityHoldMs,\n opts.gravityHold,\n opts.onGravityComplete,\n ).catch((err: unknown) => {\n if (generation !== this._spinGeneration) return;\n // The likely culprits at this layer are a `gravityHold` promise\n // (or factory) rejection and an `onGravityComplete` callback\n // throw. Surface BOTH a structured event (so a HUD / error\n // reporter can react) AND a console.error (so an unhandled\n // user-code rejection still leaves an obvious diagnostic).\n // We still slam so the engine returns to a coherent idle state\n //. without this the refill promise would hang forever.\n this._events.emit('cascade:gravity:error', { error: err });\n // eslint-disable-next-line no-console\n console.error(\n '[pixi-reels] two-stage refill threw (likely from a user-supplied ' +\n 'gravityHold/onGravityComplete). slamming to recover:',\n err,\n );\n this._slam();\n });\n } else {\n for (let i = 0; i < this._reels.length; i++) {\n const winnerCells = winnersByReel.get(i) ?? [];\n this._runReelTask(this._refillReel(i, speed, generation, winnerCells), 'refill', i, generation);\n }\n }\n\n return resultPromise;\n }\n\n private async _refillReel(\n reelIndex: number,\n speed: SpeedProfile,\n generation: number,\n winnerCells: number[],\n ): Promise<void> {\n if (generation !== this._spinGeneration) return;\n\n const reel = this._reels[reelIndex];\n const targetFrame = this._frameFor(reelIndex);\n const stopDelay = this._stopDelayFor(reelIndex, speed);\n\n const placePhase = this._phaseFactory.create<any>('cascade:place', reel, speed);\n this._activePhases.set(reelIndex, placePhase);\n await placePhase.run({\n targetFrame,\n winnerCells,\n initial: false,\n delay: stopDelay,\n events: this._events,\n } satisfies CascadePlacePhaseConfig);\n if (generation !== this._spinGeneration) return;\n\n const dropInPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n this._activePhases.set(reelIndex, dropInPhase);\n await dropInPhase.run({\n winnerCells,\n initial: false,\n events: this._events,\n } satisfies CascadeDropInPhaseConfig);\n if (generation !== this._spinGeneration) return;\n\n this._markLanded(reelIndex);\n }\n\n /**\n * Two-stage refill: place + gravity (all reels parallel, no stop delay),\n * global hold, then drop-in (all reels parallel, with stop delays).\n * Survivors slide first; new symbols enter after the hold. See `refill`\n * for the player-facing description.\n */\n private async _refillTwoStage(\n speed: SpeedProfile,\n generation: number,\n winnersByReel: Map<number, number[]>,\n gravityHoldMs: number,\n gravityHold?: Promise<void> | (() => Promise<void>),\n onGravityComplete?: () => Promise<void> | void,\n ): Promise<void> {\n // Stage 1. place + gravity. Place phase runs with delay = 0 so all\n // reels swap identities in lockstep; the staggered \"reveal\" lives in\n // stage 2.\n const stage1 = this._reels.map(async (_, i) => {\n if (generation !== this._spinGeneration) return;\n const reel = this._reels[i];\n const targetFrame = this._frameFor(i);\n const winnerCells = winnersByReel.get(i) ?? [];\n\n const placePhase = this._phaseFactory.create<any>('cascade:place', reel, speed);\n this._activePhases.set(i, placePhase);\n await placePhase.run({\n targetFrame,\n winnerCells,\n initial: false,\n delay: 0,\n events: this._events,\n } satisfies CascadePlacePhaseConfig);\n if (generation !== this._spinGeneration) return;\n\n const gravityPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n this._activePhases.set(i, gravityPhase);\n await gravityPhase.run({\n winnerCells,\n initial: false,\n role: 'gravity',\n events: this._events,\n } satisfies CascadeDropInPhaseConfig);\n });\n await Promise.all(stage1);\n if (generation !== this._spinGeneration) return;\n\n // Global hold. the beat where the player reads \"the wins are gone, the\n // surviving symbols have settled\" and any user-code anticipation\n // visuals (multiplier bump, mascot react) play. Two sources race in\n // PARALLEL via Promise.all: a fixed `gravityHoldMs` setTimeout and a\n // caller-supplied `gravityHold` promise. Whichever finishes last gates\n // the drop-in. pass both when you want a min-wall-clock floor under\n // an animation that might be fast. Skip during this window bumps the\n // generation; the post-await guard bails before the drop-in stage.\n //\n // `gravityHold` accepts a factory (`() => Promise<void>`) so that its\n // side effects (e.g. starting a multiplier-roll animation) fire HERE,\n // at gravity-end. not back when the refill args were assembled. A\n // bare Promise is also accepted for callers that already hold an\n // in-flight handle.\n const holdPromises: Promise<void>[] = [];\n if (gravityHoldMs > 0) {\n holdPromises.push(new Promise<void>((r) => setTimeout(r, gravityHoldMs)));\n }\n if (gravityHold) {\n holdPromises.push(typeof gravityHold === 'function' ? gravityHold() : gravityHold);\n }\n if (holdPromises.length > 0) {\n await Promise.all(holdPromises);\n if (generation !== this._spinGeneration) return;\n }\n\n // Awaitable callback. runs AFTER the parallel hold sources resolve,\n // so it can read final state of whatever they were waiting on\n // (e.g. a multiplier display that just finished counting up). Errors\n // are surfaced so the caller's bug doesn't silently hang the drop-in\n // stage forever; the catch bumps the generation, which causes the\n // post-await guard to bail and `_finishSpin` will be triggered by the\n // slam path if user code calls skip() in response.\n if (onGravityComplete) {\n await onGravityComplete();\n if (generation !== this._spinGeneration) return;\n }\n\n // Stage 2. drop-in (new symbols only). Per-reel stop delays apply\n // here so `setDropOrder('ltr', step)` produces the column-by-column\n // refill wave. The drop-in phase calls `notifyLanded` when its tween\n // completes, which marks the reel landed and resolves `refill()`.\n for (let i = 0; i < this._reels.length; i++) {\n this._runReelTask(\n this._refillReelDropInOnly(i, speed, generation, winnersByReel.get(i) ?? []),\n 'refill',\n i,\n generation,\n );\n }\n }\n\n private async _refillReelDropInOnly(\n reelIndex: number,\n speed: SpeedProfile,\n generation: number,\n winnerCells: number[],\n ): Promise<void> {\n if (generation !== this._spinGeneration) return;\n\n const reel = this._reels[reelIndex];\n const stopDelay = this._stopDelayFor(reelIndex, speed);\n\n // setDropOrder produces per-reel start delays; honour them here as a\n // sleep before kicking off the drop-in phase. Sleeping outside the\n // phase keeps the phase API simple. it doesn't need its own delay\n // parameter (Phase delay is a CascadePlacePhase concern).\n if (stopDelay > 0) {\n await new Promise<void>((r) => setTimeout(r, stopDelay));\n if (generation !== this._spinGeneration) return;\n }\n\n const dropInPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n this._activePhases.set(reelIndex, dropInPhase);\n await dropInPhase.run({\n winnerCells,\n initial: false,\n role: 'new',\n events: this._events,\n } satisfies CascadeDropInPhaseConfig);\n if (generation !== this._spinGeneration) return;\n\n this._markLanded(reelIndex);\n }\n\n /**\n * Mark reels to tease, and shape how they slow down.\n *\n * The second argument is either a bare `stagger` value or a full\n * `{ stagger, slowdown }` options object.\n *\n * `stagger` controls when each anticipation reel BEGINS slowing (offsets\n * are by tease-order. position within `reelIndices`. not raw reel index):\n * - `0` (default): all teases start together (legacy parallel behaviour).\n * - `number`: reel at tease-order `k` starts after `k * stagger` ms.\n * - `number[]`: explicit per-tease-order offset in ms (`stagger[k]`).\n * - `'sequential'`: each reel waits until the previous anticipation reel\n * has fully landed before it starts. maximal one-at-a-time tension.\n *\n * `slowdown` makes the deceleration progressive across the sequence: each\n * successive reel slows to a lower speed (`from` → `to`) and/or holds longer\n * (`holdFrom` → `holdTo`). Omit it for the flat 30%-and-hold default.\n *\n * `duration` overrides the active speed profile's `anticipationDelay` (ms).\n * Pass a positive value to make the tease play even in Turbo / SuperTurbo,\n * whose profiles have `anticipationDelay: 0` and would otherwise skip it.\n */\n setAnticipation(\n reelIndices: number[],\n options: AnticipationStagger | AnticipationOptions = 0,\n ): void {\n const opts: AnticipationOptions =\n typeof options === 'object' && !Array.isArray(options)\n ? options\n : { stagger: options };\n const stagger = opts.stagger ?? 0;\n\n // Held reels never reach AnticipationPhase, but filter here too so the\n // public API is forgiving. callers can pass a flat list without\n // tracking which indices are held this spin.\n this._anticipationReels = reelIndices.filter((i) => !this._heldReels.has(i));\n this._anticipationStagger = stagger;\n this._anticipationSlowdown = opts.slowdown ?? null;\n this._anticipationDuration = opts.duration ?? null;\n this._teasingReels.clear();\n\n // Sequential chaining needs a landed-deferred per anticipation reel so a\n // reel can await the previous one's landing. Build them here (setResult\n // resolves the spin phases synchronously, so the deferreds must exist\n // before the anticipation branch runs on the next microtask).\n this._reelLandedResolvers.clear();\n this._reelLandedPromises.clear();\n if (stagger === 'sequential') {\n for (const i of this._anticipationReels) {\n this._reelLandedPromises.set(\n i,\n new Promise<void>((resolve) => this._reelLandedResolvers.set(i, resolve)),\n );\n }\n }\n }\n\n /**\n * Override the per-reel stop delay (in ms). Pass one value per reel.\n * When set, these replace the staggered `reelIndex * speed.stopDelay`\n * pattern. Pass `null` to CLEAR the override and restore that default\n * (distinct from passing all-zeros, which lands every reel at once).\n */\n setStopDelays(delays: number[] | null): void {\n this._stopDelayOverride = delays ? [...delays] : null;\n }\n\n /**\n * Slam-stop safe before `setResult()` arrives. Queues until a result is\n * set, then slams. Bypasses the two-stage `skip()` machine. this API is\n * for callers with explicit slam intent (e.g. UIs that wire the queued\n * slam separately from a stage-aware button).\n */\n requestSkip(): void {\n if (!this._isSpinning) return;\n if (this._resultSymbols) {\n this._slam();\n this._skipStage = 2;\n return;\n }\n this._skipPending = true;\n }\n\n /**\n * Round-aware skip. the button-press entry point used by the universal\n * \"spin/skip\" button pattern across recipes. First press in a round\n * slams the current drop AND applies the round's speed effect as a\n * side-effect:\n *\n * - Standard mode: boost the active speed profile to the fastest\n * registered one and emit `skip:boosted`. The speed change takes\n * effect on subsequent spins (mid-spin speed switching is not\n * supported by phases). Restored to the player's original profile\n * on the next `spin()`.\n * - Cascade/tumble mode: flag the round so every subsequent\n * `refill()` auto-slams instantly (no animation). One press ends\n * a multi-drop cascade round.\n *\n * Subsequent presses in the same round slam each current drop.\n *\n * Throws if called before `setResult()` arrives (no result to slam onto\n *. slamming now would land the reels on the random spin-buffer state).\n * Use {@link requestSkip} for the deferred slam pattern: it queues the\n * slam and fires it the moment `setResult()` arrives, so the reels land\n * on the intended grid. (Refill paths set the result at entry, so this\n * guard fires only during the pre-`setResult` window of `spin()`.)\n *\n * Callers who want only the slam without the boost or auto-slam side\n * effects (tests, anti-cheat, programmatic automation) should use\n * `slamStop()` instead.\n */\n skip(): void {\n if (!this._isSpinning) return;\n\n // Pre-result guard. Slamming before setResult() lands on the random\n // spin-buffer state (standard mode = random visible grid; cascade\n // mode = alpha-0 fall-out residue, i.e. invisible). Both are wrong;\n // fail loud and steer the caller to `requestSkip()` which queues the\n // intent until setResult arrives.\n //\n // Held-only spins (every reel held) resolve on a microtask without\n // ever taking a result and never reach this branch.\n if (!this._resultSymbols) {\n throw new Error(\n 'skip() called before setResult(). there is nothing to land on yet ' +\n '(standard mode would land on random buffer fill; cascade mode would land ' +\n \"invisible). Use reelSet.requestSkip() to queue the slam until setResult() \" +\n 'arrives, or wait for setResult() before calling skip().',\n );\n }\n\n if (this._skipStage === 0) {\n if (this._currentSpinMode === 'cascade') {\n // Cascade: phase durations are static (don't read `speed.spinSpeed`),\n // so a boost would be invisible. Auto-slam future refills instead.\n this._autoSlamRefills = true;\n } else {\n // Standard: try to boost speed for the rest of the round. If the\n // active profile is already the fastest (or only one is registered),\n // we just slam. no boost is observable.\n const fastest = this._findFastestSpeedName();\n if (fastest !== null && fastest !== this._speedManager.activeName) {\n const { previous, current } = this._speedManager.set(fastest);\n this._skipPreviousSpeedName = previous.name;\n this._skipBoostedToName = current.name;\n this._events.emit('skip:boosted', { previous, current });\n }\n }\n }\n\n this._slam();\n this._skipStage = 2;\n }\n\n /**\n * Hard slam-stop. Always lands every un-landed reel immediately, regardless\n * of stage. Sets `skipStage` to 2 so future `skip()` presses in this round\n * also slam (the boost ship has sailed).\n */\n slamStop(): void {\n if (!this._isSpinning) return;\n this._slam();\n this._skipStage = 2;\n }\n\n /**\n * The slam path itself: force-complete active phases, place results (or\n * snap to current symbols when no result is set), mark every un-landed\n * reel as landed. Shared by `skip()` (stage 1+), `requestSkip()`'s\n * deferred path, `slamStop()`, and the per-reel error-recovery path\n * inside `_runReelTask`.\n *\n * Idempotent: a second call once the spin has finished is a no-op. Lets\n * cascading rejection handlers each safely invoke `_slam` without\n * triple-emitting `skip:requested`.\n */\n private _slam(): void {\n if (!this._isSpinning) return;\n this._wasSkipped = true;\n this._events.emit('skip:requested');\n\n for (const [, phase] of this._activePhases) {\n phase.forceComplete();\n }\n this._activePhases.clear();\n\n this._spinGeneration++;\n\n if (this._resultSymbols) {\n // MultiWays skip: apply pending shape and big-symbol coordinator before\n // placement so reels land at the new shape with OCCUPIED sentinels.\n const pendingShape = this._hooks.peekTargetShape();\n const visibleCellsForReel = (i: number): number =>\n pendingShape ? pendingShape[i] : this._reels[i].visibleCells;\n const decorated = this._coordinateBigSymbols(this._resultSymbols, visibleCellsForReel);\n\n for (let i = 0; i < this._reels.length; i++) {\n if (this._landedReels.has(i)) continue;\n if (this._heldReels.has(i)) continue;\n const reel = this._reels[i];\n reel.speed = 0;\n reel.isStopping = false;\n\n if (this._hooks.isMultiWaysSlot && pendingShape) {\n // Pin migration already ran at setShape() time; reshape via the\n // shared helper that both paths use. No tween. skip is instant.\n //\n // Edge case: pins exist but the shape didn't change (`pendingShape`\n // is null). We don't refresh overlays here because they're about\n // to be destroyed in `_onSpinLanded` anyway. the cell symbols at\n // the pinned coords land via `placeSymbols(decorated[i])` below\n // and overlay the same id, so the player sees the right thing.\n // `pinMigrationDuration` doesn't apply on skip by design (slam\n // stop is meant to land *now*, not run a tween on the way there).\n this._applyReshape(i, pendingShape[i]);\n }\n\n reel.placeSymbols(decorated[i]);\n reel.notifySpinEnd();\n reel.notifyLanded();\n this._markLanded(i);\n }\n } else {\n for (let i = 0; i < this._reels.length; i++) {\n if (this._landedReels.has(i)) continue;\n if (this._heldReels.has(i)) continue;\n const reel = this._reels[i];\n reel.speed = 0;\n reel.isStopping = false;\n reel.snapToGrid();\n reel.notifySpinEnd();\n reel.notifyLanded();\n this._markLanded(i);\n }\n }\n\n this._events.emit('skip:completed');\n }\n\n /**\n * Called by `ReelSet.setSpeed()` after the speed manager applies a\n * user-driven profile change. Sets the flag the next `spin()` checks\n * to decide whether to undo a prior `skip()` boost. Internal-only.\n * not part of the SpinController public API.\n *\n * Idempotent if no boost is pending (the flag is consulted only when\n * `_skipPreviousSpeedName !== null`).\n */\n notifyManualSpeedChange(): void {\n this._manualSpeedSinceBoost = true;\n }\n\n /**\n * Pick the registered speed profile with the highest `spinSpeed` (pixels\n * per frame at full motion). Returns `null` if only one profile exists,\n * since a \"boost to yourself\" is meaningless.\n */\n private _findFastestSpeedName(): string | null {\n const names = this._speedManager.profileNames;\n if (names.length < 2) return null;\n let bestName: string | null = null;\n let bestSpeed = -Infinity;\n for (const name of names) {\n const p = this._speedManager.getProfile(name);\n if (!p) continue;\n if (p.spinSpeed > bestSpeed) {\n bestSpeed = p.spinSpeed;\n bestName = name;\n }\n }\n return bestName;\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._clearSpinWatchdog();\n\n // Invalidate the in-flight phase chains BEFORE force-completing them, so\n // the `generation !== this._spinGeneration` guard that follows every\n // `await phase.run(...)` bails instead of starting the next phase -- and\n // its tweens -- on a set that is being torn down.\n this._spinGeneration++;\n\n // Every phase owns a gsap timeline writing reel speed and symbol view\n // positions, and `onSkip()` (reached via `forceComplete`) is the only\n // thing that kills them. Dropping the map without this left those\n // timelines on the gsap root timeline, still writing to display objects\n // that `ReelSet.destroy()` frees moments later. Consumers who drive gsap\n // from a PixiJS ticker feel it worst: the tweens do not stop when the\n // set's own app goes away, because any other live ticker keeps advancing\n // the shared root timeline.\n //\n // Safe to run the skip poses here: ReelSet.destroy() calls us before\n // reel.destroy(), so the views these tweens touch are still alive.\n for (const phase of this._activePhases.values()) {\n phase.forceComplete();\n }\n\n this._tickerRef.destroy();\n this._activePhases.clear();\n this._isDestroyed = true;\n }\n\n /**\n * Compute the target MAIN-axis cell extent for a reel given a target cell\n * count. MultiWays slots divide the fixed `multiwaysReelExtent` by the new\n * count, minus the inter-cell gaps; non-MultiWays slots return the reel's\n * current cell extent unchanged.\n *\n * The gap comes from the reel's own axis, not `symbolGap.y`. under\n * horizontal the strip is spaced by the X gap (ADR 016 section 6.6).\n */\n private _targetCellSizeFor(reel: Reel, targetCells: number): number {\n if (this._hooks.multiwaysReelExtent <= 0) return reel.cellMain;\n return (this._hooks.multiwaysReelExtent - (targetCells - 1) * reel.mainGap) / targetCells;\n }\n\n /**\n * Commit a reshape on one reel: emit `adjust:start`, call `reel.reshape()`,\n * refresh pin overlays, emit `adjust:complete`. Returns whether work was\n * actually done.\n *\n * **The single source of truth** for reshape orchestration. both the\n * normal AdjustPhase path AND the skip path call this. Avoids the\n * \"two parallel implementations\" bug magnet that previously had each\n * path duplicating the same compute-target-height + reshape + refresh +\n * emit-events logic.\n *\n * Pin migration already happened at `setShape()` time, so this method\n * only handles geometry + overlays.\n */\n private _applyReshape(reelIndex: number, targetCells: number): boolean {\n const reel = this._reels[reelIndex];\n const targetCellMain = this._targetCellSizeFor(reel, targetCells);\n const fromCells = reel.visibleCells;\n\n if (targetCells === fromCells && targetCellMain === reel.cellMain) {\n return false;\n }\n\n this._events.emit('adjust:start', { reelIndex, fromCells, toCells: targetCells });\n reel.reshape(targetCells, targetCellMain, reel.bufferStart, reel.bufferEnd);\n this._hooks.refreshPinOverlaysForReel(reelIndex);\n this._events.emit('adjust:complete', { reelIndex });\n return true;\n }\n\n // ── Internal ──────────────────────────────────────────\n\n private async _startReel(reelIndex: number, speed: SpeedProfile, generation: number): Promise<void> {\n if (generation !== this._spinGeneration) return;\n\n const reel = this._reels[reelIndex];\n const isTumble = this._currentSpinMode === 'cascade';\n const canAdjust = this._hooks.isMultiWaysSlot && this._phaseFactory.has('adjust');\n\n // Cascade (classic-tumble) reshape ordering. In standard mode the reshape\n // runs between SPIN and STOP (below), where the spin blur hides a reel\n // changing height. Cascade mode has no such cover: `CascadeFallPhase` drops\n // the reel's CURRENT visible cells, so if the reshape ran after the fall the\n // reel would drop its OLD, differently-sized board and then snap to the new\n // shape. a reel visibly changing height mid-tumble. When the target shape is\n // already known at spin time (the game called `setShape()` BEFORE\n // `spin({ mode: 'cascade' })`), commit the reshape HERE, before the fall, so\n // the fall drops the reel at its target height. If the shape arrives later\n // (legacy `spin()` then `setShape()`), `peekTargetShape()` is still null and\n // this is skipped; the reshape falls back to the post-SPIN slot unchanged.\n const reshapeBeforeFall = isTumble && canAdjust && this._hooks.peekTargetShape() !== null;\n if (reshapeBeforeFall) {\n await this._runAdjustForReel(reel, reelIndex, speed, generation);\n if (generation !== this._spinGeneration) return;\n }\n\n // START or FALL: chain via phase.run() promises (no busy-polling).\n if (isTumble) {\n const fallPhase = this._phaseFactory.create<any>('cascade:fall', reel, speed);\n this._activePhases.set(reelIndex, fallPhase);\n await fallPhase.run({\n spinningMode: this._spinningMode,\n delay: reelIndex * speed.spinDelay,\n events: this._events,\n } satisfies CascadeFallPhaseConfig);\n } else {\n const startPhase = this._phaseFactory.create<any>('start', reel, speed);\n this._activePhases.set(reelIndex, startPhase);\n await startPhase.run({\n spinningMode: this._spinningMode,\n delay: reelIndex * speed.spinDelay,\n } satisfies StartPhaseConfig);\n }\n\n if (generation !== this._spinGeneration) return;\n\n const spinPhase = this._phaseFactory.create<SpinPhase>('spin', reel, speed);\n this._activePhases.set(reelIndex, spinPhase);\n const spinDone = spinPhase.run({});\n\n let allSpinning = true;\n for (let i = 0; i < this._reels.length; i++) {\n // Held reels never enter the phase chain; they don't gate\n // `spin:allStarted` or the stop-sequence start.\n if (this._heldReels.has(i)) continue;\n const phase = this._activePhases.get(i);\n if (!phase || phase.name !== 'spin') { allSpinning = false; break; }\n }\n if (allSpinning) {\n this._events.emit('spin:allStarted');\n this._tryBeginStopSequence();\n }\n\n await spinDone;\n if (generation !== this._spinGeneration) return;\n\n // MultiWays: AdjustPhase commits the new shape and migrates pins between\n // SpinPhase and StopPhase. Inserted only when builder.multiways() was\n // called. non-MultiWays slots skip this entirely. Skipped when a cascade\n // spin already committed the reshape before the fall (see above).\n if (canAdjust && !reshapeBeforeFall) {\n await this._runAdjustForReel(reel, reelIndex, speed, generation);\n if (generation !== this._spinGeneration) return;\n }\n\n // SpinPhase resolved (result arrived). Run ANTICIPATION (if requested) then STOP.\n const stopDelay = this._stopDelayFor(reelIndex, speed);\n const targetFrame = this._frameFor(reelIndex);\n\n // Effective tease hold: the per-call `duration` override wins over the\n // profile's `anticipationDelay`, so a positive override plays the tease\n // even in Turbo / SuperTurbo (whose profiles set anticipationDelay: 0).\n const antBaseDuration = this._anticipationDuration ?? speed.anticipationDelay;\n\n let didAnticipate = false;\n if (this._anticipationReels.includes(reelIndex) && antBaseDuration > 0) {\n // Stagger the START of the slow-down so anticipation reels tease one\n // after another instead of all at once. The reel keeps spinning at full\n // speed during this wait (its SpinPhase resolved but `reel.speed` is\n // still spinSpeed and the ticker keeps advancing it), so earlier reels\n // visibly hold while later ones stay at full blur.\n const proceed = await this._awaitAnticipationOffset(reelIndex, generation);\n if (!proceed) return; // slam / new spin superseded us during the wait\n\n // A dedicated tease-start signal carrying the reel's place in the\n // sequence, so games can layer per-step SFX / pitch ramps without\n // re-deriving which reels are teasing from `spin:stopping`.\n const order = this._anticipationReels.indexOf(reelIndex);\n this._teasingReels.add(reelIndex);\n // Symbols relax their spin presentation (e.g. StaticSpinSymbol fades\n // the blur out) so the slowed strip is readable during the tease.\n reel.notifyAnticipationStart();\n this._events.emit('anticipation:reel', {\n reelIndex,\n order,\n total: this._anticipationReels.length,\n });\n this._events.emit('spin:stopping', reelIndex);\n const anticipationPhase = this._phaseFactory.create<any>('anticipation', reel, speed);\n this._activePhases.set(reelIndex, anticipationPhase);\n await anticipationPhase.run(this._anticipationConfigFor(reelIndex, speed));\n if (generation !== this._spinGeneration) return;\n didAnticipate = true;\n } else {\n this._events.emit('spin:stopping', reelIndex);\n }\n\n if (isTumble) {\n // Tumble stop = place + dropIn. Both phases are user-overridable via\n // the factory; the orchestration here is internal.\n const placePhase = this._phaseFactory.create<any>('cascade:place', reel, speed);\n this._activePhases.set(reelIndex, placePhase);\n await placePhase.run({\n targetFrame,\n winnerCells: [],\n initial: true,\n delay: stopDelay,\n events: this._events,\n } satisfies CascadePlacePhaseConfig);\n if (generation !== this._spinGeneration) return;\n\n const dropInPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n this._activePhases.set(reelIndex, dropInPhase);\n await dropInPhase.run({\n winnerCells: [],\n initial: true,\n events: this._events,\n } satisfies CascadeDropInPhaseConfig);\n if (generation !== this._spinGeneration) return;\n } else {\n const stopPhase = this._phaseFactory.create<any>('stop', reel, speed);\n this._activePhases.set(reelIndex, stopPhase);\n // After a tease, carry the slow anticipation speed into the stop so the\n // reel crawls to its landing position instead of re-accelerating.\n await stopPhase.run({\n targetFrame,\n delay: stopDelay,\n preserveSpeed: didAnticipate,\n } satisfies StopPhaseConfig);\n if (generation !== this._spinGeneration) return;\n }\n\n this._markLanded(reelIndex);\n }\n\n /**\n * MultiWays AdjustPhase orchestration: pull the pending shape, migrate\n * pins to their new cells, build pin-overlay tween descriptors, run the\n * phase. Emits `adjust:start` on entry and `adjust:complete` on exit.\n *\n * **Skips entirely** when there's no shape change AND no pin overlay on\n * this reel. no phase instance is constructed and no `adjust:*` events\n * fire. A spin where most reels have no work shouldn't pay for a phase\n * boundary or spam the event bus.\n */\n private async _runAdjustForReel(\n reel: Reel,\n reelIndex: number,\n speed: SpeedProfile,\n generation: number,\n ): Promise<void> {\n const targetShape = this._hooks.peekTargetShape();\n const targetCells = targetShape ? targetShape[reelIndex] : reel.visibleCells;\n const targetCellMain = this._targetCellSizeFor(reel, targetCells);\n\n // Build tween descriptors BEFORE the reshape commits. they capture\n // each overlay's current on-screen pose as the tween's `from` state.\n const pinOverlays = this._hooks.buildPinOverlayTweens(reelIndex, targetCellMain);\n\n // Commit the reshape via the shared helper (events + reel.reshape +\n // overlay refresh). Skip if no work and no overlays to tween.\n const reshapeHappened = this._applyReshape(reelIndex, targetCells);\n if (!reshapeHappened && pinOverlays.length === 0) {\n return;\n }\n\n // Run AdjustPhase purely as a tween phase. the geometry is already\n // committed. Phase only animates the pin overlays from their captured\n // pre-reshape pose to the new cell positions.\n if (pinOverlays.length === 0) {\n return;\n }\n const adjust = this._phaseFactory.create<any>('adjust', reel, speed);\n this._activePhases.set(reelIndex, adjust);\n await adjust.run({ pinOverlays } satisfies AdjustPhaseConfig);\n }\n\n /**\n * Wait for a reel's anticipation-start offset before it begins slowing.\n * Returns `false` if the spin generation changed during the wait (a slam or\n * a fresh spin superseded this task) so the caller bails cleanly.\n *\n * Offsets are keyed by tease-order (position within `_anticipationReels`),\n * not raw reel index, so teasing `[2,3,4]` spaces them `[0, S, 2S]`\n * regardless of which physical reels they are.\n */\n private async _awaitAnticipationOffset(\n reelIndex: number,\n generation: number,\n ): Promise<boolean> {\n const order = this._anticipationReels.indexOf(reelIndex);\n if (order <= 0) return true; // first tease reel starts immediately\n\n const stagger = this._anticipationStagger;\n if (stagger === 'sequential') {\n const prevLanded = this._reelLandedPromises.get(this._anticipationReels[order - 1]);\n if (prevLanded) {\n await prevLanded;\n if (generation !== this._spinGeneration) return false;\n }\n return true;\n }\n\n const offsetMs = Array.isArray(stagger) ? (stagger[order] ?? 0) : order * stagger;\n if (offsetMs > 0) {\n await new Promise<void>((r) => setTimeout(r, offsetMs));\n if (generation !== this._spinGeneration) return false;\n }\n return true;\n }\n\n /**\n * Build the per-reel AnticipationPhase config from the active `slowdown`\n * curve and `duration` override. Interpolates `from`→`to` (speed) and\n * `holdFrom`→`holdTo` (duration) across tease-order so each successive reel\n * decelerates deeper / holds longer. Base hold is the `duration` override\n * when set, else the profile's `anticipationDelay`. Returns an empty config\n * (phase defaults: 30% speed, `anticipationDelay` hold) when neither a\n * slowdown nor a duration override is configured.\n */\n private _anticipationConfigFor(\n reelIndex: number,\n speed: SpeedProfile,\n ): AnticipationPhaseConfig {\n const slowdown = this._anticipationSlowdown;\n const baseDuration = this._anticipationDuration;\n\n // No slowdown curve: only the (optional) duration override matters. Passing\n // it explicitly is what lets the tease run when the profile's\n // anticipationDelay is 0 (Turbo / SuperTurbo).\n if (!slowdown) {\n return baseDuration != null ? { duration: baseDuration } : {};\n }\n\n const count = this._anticipationReels.length;\n const order = this._anticipationReels.indexOf(reelIndex);\n // Fraction along the tease sequence: 0 for the first reel, 1 for the last.\n const f = count > 1 ? order / (count - 1) : 0;\n\n const from = slowdown.from ?? 0.3;\n const to = slowdown.to ?? from;\n const holdFrom = slowdown.holdFrom ?? 1;\n const holdTo = slowdown.holdTo ?? holdFrom;\n const base = baseDuration ?? speed.anticipationDelay;\n\n const config: AnticipationPhaseConfig = {\n speedMultiplier: from + (to - from) * f,\n };\n const holdMult = holdFrom + (holdTo - holdFrom) * f;\n // Set duration whenever an override is active OR the hold is scaled; leave\n // it off only when the plain profile hold (holdMult 1, no override) applies.\n if (baseDuration != null || holdMult !== 1) config.duration = base * holdMult;\n return config;\n }\n\n private _stopDelayFor(reelIndex: number, speed: SpeedProfile): number {\n if (this._stopDelayOverride) {\n return this._stopDelayOverride[reelIndex] ?? 0;\n }\n return reelIndex * speed.stopDelay;\n }\n\n private _cachedFrames: string[][] | null = null;\n\n private _frameFor(reelIndex: number): string[] {\n if (!this._cachedFrames) return [];\n return this._cachedFrames[reelIndex];\n }\n\n private _tryBeginStopSequence(): void {\n if (!this._resultSymbols) return;\n\n for (let i = 0; i < this._reels.length; i++) {\n // Held reels never enter a phase chain. don't gate the stop\n // sequence on them.\n if (this._heldReels.has(i)) continue;\n const phase = this._activePhases.get(i);\n if (!phase || phase.name !== 'spin') return;\n }\n\n // For MultiWays, the per-reel target cell count is whatever AdjustPhase\n // will reshape to. For frame-building purposes we need to send the\n // correct number of visible cells per reel. Pull the pending shape; if\n // unset, fall back to current reel.visibleCells.\n const pendingShape = this._hooks.peekTargetShape();\n const visibleCellsForReel = (i: number): number =>\n pendingShape ? pendingShape[i] : this._reels[i].visibleCells;\n\n // Big symbols: paint cross-reel OCCUPIED sentinels into the result grid\n // BEFORE per-reel frame building. The coordinator validates block fit\n // and rewrites cells; per-reel FrameBuilder then sees the sentinels and\n // RandomFillMiddleware skips them. Non-big-symbol slots are zero-cost.\n const decorated = this._coordinateBigSymbols(this._resultSymbols, visibleCellsForReel);\n\n // Build and cache frames using each reel's actual buffer/visible config.\n // Reels may differ in buffer size; build each independently. Held reels\n // get an empty placeholder. their entry is never read because no\n // StopPhase ever fires for them.\n const frames: string[][] = [];\n for (let i = 0; i < this._reels.length; i++) {\n if (this._heldReels.has(i)) {\n frames.push([]);\n continue;\n }\n const reel = this._reels[i];\n const cells = visibleCellsForReel(i);\n frames.push(\n this._frameBuilder.build(\n i,\n cells,\n reel.bufferStart,\n reel.bufferEnd,\n decorated[i],\n ),\n );\n }\n this._cachedFrames = frames;\n\n // Resolve all non-held SpinPhases; each reel's _startReel awaits its own\n // spinDone, then independently runs ANTICIPATION/STOP. Held reels have\n // no SpinPhase to resolve.\n for (let i = 0; i < this._reels.length; i++) {\n if (this._heldReels.has(i)) continue;\n const spinPhase = this._activePhases.get(i) as SpinPhase;\n if (spinPhase?.resolve) spinPhase.resolve();\n }\n }\n\n /**\n * Big symbols cross-reel coordinator. Walks the result grid, locates big\n * symbols (those with `SymbolData.size.reels * size.cells > 1`), validates that\n * the block fits within reel bounds, and paints OCCUPIED sentinels into\n * the non-anchor cells so per-reel FrameBuilder leaves them alone.\n *\n * Pure: returns a new grid; does not mutate the input. Zero-overhead for\n * slots with no big symbols (the loop runs but never matches metadata).\n */\n private _coordinateBigSymbols(\n grid: ColumnTarget[],\n visibleCellsForReel: (i: number) => number,\n ): ColumnTarget[] {\n const bufferStart = this._reels[0]?.bufferStart ?? 0;\n const bufferEnd = this._reels[0]?.bufferEnd ?? 0;\n const out = grid.map(cloneColumnTarget);\n const symData = this._hooks.symbolsData;\n\n // Buffer geometry is read from reel[0] and treated as uniform across\n // all reels. This holds today because `ReelSetBuilder.bufferSymbols(n)`\n // is the only buffer-setting API and applies a single global value;\n // there is no per-reel buffer API. If you ever add one (e.g. a\n // `bufferSymbolsPerReel([...])` builder method), propagate per-reel\n // values into the validator loop below: the `targetCells` lookup\n // already supports per-reel geometry; only the buffers are still\n // global here.\n\n // Read/write a per-reel target slot for any cell in\n // `[-bufferStart, cells + bufferEnd)`. Row is visible-relative: negative\n // cells address `bufferStart`, cells past `visible.length` address\n // `bufferEnd`. See `getTargetSlot` / `setTargetSlot`.\n const readSlot = (reel: number, cell: number): string | undefined =>\n getTargetSlot(out[reel], cell);\n const writeSlot = (reel: number, cell: number, value: string): void => {\n setTargetSlot(out[reel], cell, value);\n };\n\n for (let reel = 0; reel < out.length; reel++) {\n const cells = visibleCellsForReel(reel);\n // Iterate the FULL strip range, not just visible. A big-symbol anchor\n // may sit in bufferStart (partial-visibility from the top. only the\n // block's tail shows in cell 0) or in bufferEnd (the head shows at\n // the last visible cell, the rest is clipped below the mask).\n // `_finalizeFrame` sizes anchors anywhere on the strip, so the engine\n // renders both cases correctly.\n for (let cell = -bufferStart; cell < cells + bufferEnd; cell++) {\n const id = readSlot(reel, cell);\n if (id === undefined) continue;\n const meta = symData[id];\n if (!meta?.size) continue;\n const w = meta.size.reels;\n const h = meta.size.cells;\n if (w === 1 && h === 1) continue;\n\n // Validate block fit on this reel: anchor + h must stay on the\n // strip. The strip ends at `cells + bufferEnd - 1` (last bufferEnd\n // slot) and starts at `-bufferStart` (first bufferStart slot).\n if (cell + h > cells + bufferEnd) {\n throw new Error(\n `big symbol '${id}' (${w}x${h}) at (reel=${reel}, cell=${cell}) ` +\n `extends past the bottom of the strip on reel ${reel} ` +\n `(anchor cell + h = ${cell + h} > visibleCells + bufferEnd = ${cells + bufferEnd}).`,\n );\n }\n if (reel + w > out.length) {\n throw new Error(\n `big symbol '${id}' (${w}x${h}) at (reel=${reel}, cell=${cell}) ` +\n `exceeds reel count ${out.length}.`,\n );\n }\n for (let dx = 0; dx < w; dx++) {\n const targetReel = reel + dx;\n const targetCells = visibleCellsForReel(targetReel);\n if (cell + h > targetCells + bufferEnd) {\n throw new Error(\n `big symbol '${id}' (${w}x${h}) at (reel=${reel}, cell=${cell}) ` +\n `extends past the bottom of the strip on reel ${targetReel} ` +\n `(anchor cell + h = ${cell + h} > visibleCells + bufferEnd = ${targetCells + bufferEnd}).`,\n );\n }\n }\n\n // Paint OCCUPIED across the block (skip the anchor itself at dx=0,dy=0).\n // Stub cells may land in bufferStart (negative cell), visible, or\n // bufferEnd (cell >= visibleCells). `writeSlot` handles all three.\n for (let dy = 0; dy < h; dy++) {\n for (let dx = 0; dx < w; dx++) {\n if (dx === 0 && dy === 0) continue;\n writeSlot(reel + dx, cell + dy, OCCUPIED_SENTINEL);\n }\n }\n }\n }\n return out;\n }\n\n private _markLanded(reelIndex: number): void {\n if (this._landedReels.has(reelIndex)) return;\n this._landedReels.add(reelIndex);\n\n // Unblock the next reel in a 'sequential' anticipation chain (no-op for\n // other stagger modes, which register no resolvers).\n const landedResolve = this._reelLandedResolvers.get(reelIndex);\n if (landedResolve) {\n this._reelLandedResolvers.delete(reelIndex);\n landedResolve();\n }\n\n // Tease-end signal, fired only for reels that actually teased this spin, so\n // a listener can stop that reel's tension SFX / glow without tracking the\n // anticipation set itself. Fired before `spin:reelLanded` so consumers see\n // \"tease over\" then \"reel landed\" in a natural order.\n if (this._teasingReels.delete(reelIndex)) {\n this._events.emit('anticipation:reelEnd', { reelIndex });\n }\n\n const reel = this._reels[reelIndex];\n const symbols = reel.getVisibleSymbols();\n reel.events.emit('landed', symbols);\n this._events.emit('spin:reelLanded', reelIndex, symbols);\n\n // All NON-HELD reels accounted for → finish. Held reels never\n // _markLanded, but their slots count toward `reels.length`, so we\n // compare against the count that was supposed to actually animate.\n if (this._landedReels.size === this._reels.length - this._heldReels.size) {\n this._finishSpin();\n }\n }\n\n /**\n * Wire up the optional abort signal and timeout watchdog for this spin.\n * Both routes call `_abortSpin`, which force-stops the reels and rejects the\n * spin promise. Cleared by `_finishSpin` / `_abortSpin` when the spin settles.\n */\n private _armSpinWatchdog(options: SpinOptions | undefined, generation: number): void {\n this._clearSpinWatchdog();\n\n const signal = options?.signal;\n const timeoutMs = options?.timeoutMs;\n if (!signal && (timeoutMs === undefined || timeoutMs <= 0)) return;\n\n const cleanups: Array<() => void> = [];\n\n if (signal) {\n const onAbort = (): void => {\n if (generation !== this._spinGeneration) return;\n this._abortSpin(this._abortError(signal));\n };\n signal.addEventListener('abort', onAbort);\n cleanups.push(() => signal.removeEventListener('abort', onAbort));\n }\n\n if (timeoutMs !== undefined && timeoutMs > 0) {\n const timer = setTimeout(() => {\n if (generation !== this._spinGeneration) return;\n this._abortSpin(\n new Error(\n `spin() exceeded its ${timeoutMs}ms watchdog without landing. ` +\n 'setResult() / requestSkip() / slamStop() was never called (most often a ' +\n 'failed or timed-out server request). The reels have been force-stopped.',\n ),\n );\n }, timeoutMs);\n cleanups.push(() => clearTimeout(timer));\n }\n\n this._spinWatchdogCleanup = () => {\n for (const c of cleanups) c();\n };\n }\n\n private _clearSpinWatchdog(): void {\n if (this._spinWatchdogCleanup) {\n this._spinWatchdogCleanup();\n this._spinWatchdogCleanup = null;\n }\n }\n\n private _abortError(signal: AbortSignal): Error {\n const reason = (signal as AbortSignal & { reason?: unknown }).reason;\n if (reason instanceof Error) return reason;\n if (typeof reason === 'string' && reason.length > 0) return new Error(reason);\n return new Error('spin() was aborted via SpinOptions.signal before the reels landed.');\n }\n\n /**\n * Force-stop an in-flight spin and reject its promise. Reuses the proven\n * `_slam()` recovery (kills phase tweens, snaps reels to a clean grid when no\n * result is set), then `_finishSpin()` rejects instead of resolving because\n * `_pendingAbortError` is set.\n */\n private _abortSpin(error: Error): void {\n if (!this._isSpinning) return;\n this._clearSpinWatchdog();\n this._pendingAbortError = error;\n this._slam();\n }\n\n private _finishSpin(): void {\n this._clearSpinWatchdog();\n\n // Abort/timeout path: reject and skip the success events. _slam() already\n // force-stopped the reels on the way here.\n const abortError = this._pendingAbortError;\n if (abortError) {\n this._pendingAbortError = null;\n this._isSpinning = false;\n this._activePhases.clear();\n this._cachedFrames = null;\n this._hooks.clearTargetShape();\n const reject = this._currentSpinReject;\n this._currentSpinResolve = null;\n this._currentSpinReject = null;\n if (reject) reject(abortError);\n return;\n }\n\n const result: SpinResult = {\n symbols: this._reels.map((r) => r.getVisibleSymbols()),\n wasSkipped: this._wasSkipped,\n duration: performance.now() - this._spinStartTime,\n };\n\n this._isSpinning = false;\n this._activePhases.clear();\n this._cachedFrames = null;\n // MultiWays: the target shape was applied this spin; clear it so the next\n // spin starts fresh. Non-MultiWays: this is a no-op.\n this._hooks.clearTargetShape();\n\n this._events.emit('spin:allLanded', result);\n this._events.emit('spin:complete', result);\n\n if (this._currentSpinResolve) {\n this._currentSpinResolve(result);\n this._currentSpinResolve = null;\n }\n this._currentSpinReject = null;\n }\n\n private _onTick(ticker: Ticker): void {\n if (!this._isSpinning) return;\n\n const deltaMs = ticker.deltaMS;\n for (const reel of this._reels) {\n reel.update(deltaMs);\n }\n for (const phase of this._activePhases.values()) {\n if (phase.isActive) {\n phase.update(deltaMs);\n }\n }\n }\n}\n","import type { SpeedProfile } from '../config/types.js';\n\n/**\n * The tempo of your reels, as named presets.\n *\n * A `SpeedProfile` is a bundle of timings. how long the wind-up takes,\n * how fast the reel scrolls at full speed, how deep the landing bounce\n * is, which GSAP easing drives each transition. `SpeedManager` holds\n * those profiles by name and tracks which one is active.\n *\n * Built-in profiles: `normal` (default), `turbo`, `superTurbo`. Add your\n * own via `reelSet.speed.addProfile('cinematic', {...})`. Switch at\n * runtime with `reelSet.setSpeed('turbo')`.\n *\n * Speed changes take effect on the next spin. mid-spin switching\n * is deliberately not supported to keep animation state simple.\n */\nexport class SpeedManager {\n private _profiles = new Map<string, SpeedProfile>();\n private _activeName: string;\n private _active: SpeedProfile;\n\n constructor(profiles: Map<string, SpeedProfile>, initialSpeed: string) {\n for (const [name, profile] of profiles) {\n this._profiles.set(name, profile);\n }\n const initial = this._profiles.get(initialSpeed);\n if (!initial) {\n throw new Error(\n `Speed profile '${initialSpeed}' not found. Available: ${[...this._profiles.keys()].join(', ')}`,\n );\n }\n this._activeName = initialSpeed;\n this._active = initial;\n }\n\n /** The currently active speed profile. */\n get active(): Readonly<SpeedProfile> {\n return this._active;\n }\n\n /** Name of the currently active speed profile. */\n get activeName(): string {\n return this._activeName;\n }\n\n /** Switch to a different named speed profile. */\n set(name: string): { previous: SpeedProfile; current: SpeedProfile } {\n const profile = this._profiles.get(name);\n if (!profile) {\n throw new Error(\n `Speed profile '${name}' not found. Available: ${[...this._profiles.keys()].join(', ')}`,\n );\n }\n const previous = this._active;\n this._activeName = name;\n this._active = profile;\n return { previous, current: profile };\n }\n\n /** Add or replace a speed profile. */\n addProfile(name: string, profile: SpeedProfile): void {\n this._profiles.set(name, profile);\n }\n\n /** Get a profile by name, or undefined if not found. */\n getProfile(name: string): SpeedProfile | undefined {\n return this._profiles.get(name);\n }\n\n /** All registered profile names. */\n get profileNames(): string[] {\n return [...this._profiles.keys()];\n }\n}\n","import type { Reel } from '../core/Reel.js';\nimport type { ReelViewport } from '../core/ReelViewport.js';\nimport type { SymbolPosition, ReelSetEvents } from '../events/ReelEvents.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { Disposable } from '../utils/Disposable.js';\n\nexport interface SpotlightOptions {\n /** Opacity of the dim overlay (0-1). Default: 0.5. */\n dimAmount?: number;\n /** Whether to play win animation on spotlighted symbols. Default: true. */\n playWinAnimation?: boolean;\n /** Whether to re-parent symbols above the mask. Default: true. */\n promoteAboveMask?: boolean;\n}\n\nexport interface WinLine {\n positions: SymbolPosition[];\n}\n\nexport interface CycleOptions extends SpotlightOptions {\n /** Milliseconds to display each win line. Default: 2000. */\n displayDuration?: number;\n /** Milliseconds between lines. Default: 300. */\n gapDuration?: number;\n /** Number of cycles (-1 for infinite). Default: 1. */\n cycles?: number;\n}\n\ninterface PromotedSymbol {\n symbol: ReelSymbol;\n originalParent: any;\n position: SymbolPosition;\n}\n\n/**\n * The \"we just won\" visual primitive.\n *\n * The spotlight is what turns a landed grid into a celebration. Given a\n * list of winning cell positions, it:\n *\n * 1. Fades in the dim overlay behind everything (everything that is\n * not winning visually sinks into the background).\n * 2. Re-parents each winning `ReelSymbol` into the viewport's\n * spotlight layer so its animation isn't clipped by the reel mask.\n * 3. Calls `playWin()` on each winner (your symbol class's one-shot).\n * 4. When you call `hide()` or the cycle ends, it puts every symbol\n * back where it came from and removes the dim overlay.\n *\n * Two modes:\n * - `show(positions, options)`. one-shot. Cell highlight + promote +\n * play win. Returns when the animation fully ends.\n * - `cycle(lines, options)`. iterate multiple win lines with a\n * configurable per-line duration and gap, optionally repeating.\n *\n * Win detection is NOT part of this. pixi-reels never computes wins.\n * your server / game code decides which cells are winners and passes\n * them here. See [ADR 007](https://github.com/schmooky/pixi-reels/blob/main/docs/adr/007-scope.md).\n */\nexport class SymbolSpotlight implements Disposable {\n private _reels: Reel[];\n private _viewport: ReelViewport;\n private _promoted: PromotedSymbol[] = [];\n private _isActive = false;\n private _isDestroyed = false;\n private _cycleAbort: AbortController | null = null;\n private _events: EventEmitter<ReelSetEvents>;\n\n constructor(reels: Reel[], viewport: ReelViewport, events: EventEmitter<ReelSetEvents>) {\n this._reels = reels;\n this._viewport = viewport;\n this._events = events;\n }\n\n get isActive(): boolean {\n return this._isActive;\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /** Show spotlight on specific positions. */\n async show(positions: SymbolPosition[], options: SpotlightOptions = {}): Promise<void> {\n this.hide(); // Cancel any running cycle and clear the previous spotlight\n await this._showInternal(positions, options);\n }\n\n /**\n * Promote + play win for one set of positions. Unlike the public `show()`,\n * this does NOT call `hide()` first, so it never aborts a running cycle.\n */\n private async _showInternal(\n positions: SymbolPosition[],\n options: SpotlightOptions = {},\n ): Promise<void> {\n const {\n dimAmount = 0.5,\n playWinAnimation = true,\n promoteAboveMask = true,\n } = options;\n\n this._isActive = true;\n this._events.emit('spotlight:start', positions);\n\n this._viewport.showDim(dimAmount);\n\n const winPromises: Promise<void>[] = [];\n\n const seen = new Set<string>();\n for (const pos of positions) {\n const reel = this._reels[pos.reelIndex];\n if (!reel) continue;\n\n const symbol = reel.getSymbolAt(pos.cellIndex);\n if (!symbol) continue;\n\n // Avoid promoting the same physical symbol twice (e.g. a 2×2 big\n // symbol's anchor cell + its OCCUPIED cells all resolve to one symbol).\n const key = `${pos.reelIndex}:${reel.getAnchorCell(pos.cellIndex)}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n // Track for hide() only when we're actually moving the view.\n // otherwise the entry's `originalParent` would become stale if the\n // shared symbol pool recycles this instance into a different reel\n // before the next hide(), and `hide()` would reparent it back to a\n // reel that no longer owns it (leaving a hole on the new owner).\n if (promoteAboveMask) {\n const originalParent = symbol.view.parent;\n this._promoted.push({ symbol, originalParent, position: pos });\n const globalPos = symbol.view.getGlobalPosition();\n this._viewport.spotlightContainer.addChild(symbol.view);\n const localPos = this._viewport.spotlightContainer.toLocal(globalPos);\n symbol.view.x = localPos.x;\n symbol.view.y = localPos.y;\n }\n\n if (playWinAnimation) {\n winPromises.push(symbol.playWin());\n }\n }\n\n if (winPromises.length > 0) {\n await Promise.all(winPromises);\n }\n }\n\n /** Hide the spotlight and return symbols to their original positions. */\n hide(): void {\n // Cancel any running cycle\n if (this._cycleAbort) {\n this._cycleAbort.abort();\n this._cycleAbort = null;\n }\n this._teardownVisual();\n }\n\n /**\n * Return promoted symbols and remove the dim overlay, WITHOUT aborting a\n * running cycle. The cycle loop calls this between lines; `hide()` adds the\n * abort on top for the public stop-everything behaviour.\n */\n private _teardownVisual(): void {\n // Return promoted symbols. Skip any whose view has been moved out of\n // the spotlight container. that means the shared symbol pool has\n // recycled them into another reel since show(), and reparenting back\n // to `originalParent` would steal them from their new owner.\n for (const { symbol, originalParent } of this._promoted) {\n if (symbol.view.parent !== this._viewport.spotlightContainer) continue;\n if (originalParent) {\n const globalPos = symbol.view.getGlobalPosition();\n originalParent.addChild(symbol.view);\n const localPos = originalParent.toLocal(globalPos);\n symbol.view.x = localPos.x;\n symbol.view.y = localPos.y;\n }\n symbol.stopAnimation();\n }\n this._promoted = [];\n\n this._viewport.hideDim();\n // Only fire spotlight:end for a teardown that actually ends an active\n // presentation. hide() runs teardown eagerly (e.g. show() clears a prior\n // spotlight first), and those must not emit a spurious end.\n const wasActive = this._isActive;\n this._isActive = false;\n if (wasActive) this._events.emit('spotlight:end');\n }\n\n /**\n * Cycle through win lines, showing each for a duration.\n * Returns when all cycles complete or when hide() is called.\n */\n async cycle(winLines: WinLine[], options: CycleOptions = {}): Promise<void> {\n const {\n displayDuration = 2000,\n gapDuration = 300,\n cycles = 1,\n } = options;\n\n if (winLines.length === 0) return;\n\n // Stop anything already showing/cycling, then start a fresh controller.\n this.hide();\n const abort = new AbortController();\n this._cycleAbort = abort;\n const signal = abort.signal;\n\n let cycleCount = 0;\n while (cycles === -1 || cycleCount < cycles) {\n for (const line of winLines) {\n if (signal.aborted) return;\n // Use the internal show/teardown so the cycle does not abort itself.\n await this._showInternal(line.positions, options);\n await this._wait(displayDuration, signal);\n if (signal.aborted) return;\n this._teardownVisual();\n await this._wait(gapDuration, signal);\n }\n cycleCount++;\n }\n\n // Normal completion: clear only our own controller (a newer cycle/show\n // that pre-empted us would have aborted this signal and returned above).\n if (this._cycleAbort === abort) this._cycleAbort = null;\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this.hide();\n this._isDestroyed = true;\n }\n\n private _wait(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal.addEventListener('abort', () => {\n clearTimeout(timer);\n resolve();\n }, { once: true });\n });\n }\n}\n","/**\n * A claim on a grid cell.\n *\n * A `CellPin` occupies a `{reel, cell}` position on the reel grid. It is\n * applied as a forced stop target when `setResult()` is called, so the\n * reel lands on the pinned symbol regardless of what the server sent for\n * that cell. It persists across spins according to its `turns` field.\n *\n * Pins unify every \"stays put\" mechanic in the library:\n *\n * - Sticky wild → `pin(reel, cell, 'wild', { turns: 3 })`\n * - Expanding wild → `pin(reel, cell, 'wild', { turns: 'eval' })`\n * - Hold & Win coin → `pin(reel, cell, 'coin', { turns: 'permanent', payload: { value: 50 } })`\n * - Multiplier wild → `pin(reel, cell, 'wild', { turns: 'permanent', payload: { multiplier: 3 } })`\n * - Sticky-win respin → `pin(reel, cell, symbolId, { turns: respinsLeft })`\n *\n * Movement (walking wild, trailing wild) is done via `reelSet.movePin()`\n * in a separate slice; state-only pins ship first.\n */\n/**\n * How a pin behaves when a MultiWays reshape changes the cell count of its\n * reel. Non-MultiWays slots never reshape, so this value is irrelevant\n * there.\n *\n * - **`'origin'`** (default). the pin migrates to `min(originCell, newCells - 1)`\n * on every reshape. Clamps when the shape is too small; **restores to\n * the origin** when the shape grows back. Prevents wander. a pin at\n * `originCell=3` clamped to cell 2 on a 3-cell shape returns to cell 3 on\n * a later 5-cell shape. The right default for sticky wilds, trailing\n * wilds, and any \"this position has meaning\" mechanic.\n *\n * - **`'frozen'`**. the pin stays at its current cell if the new shape\n * fits, otherwise clamps to the last visible cell AND **updates\n * `originCell` to the clamped position** so it never restores. Use when\n * the pin's cell should be locked to wherever it is now, regardless of\n * future shape changes (e.g. a walking-wild on MultiWays where the\n * wild's \"current cell\" IS the source of truth. restoring to a\n * pre-walk cell would undo the walk).\n */\nexport type PinMigration = 'origin' | 'frozen';\n\nexport interface CellPin {\n /** Column (reel index) this pin is anchored to. */\n readonly reel: number;\n /** Row (0 = top visible cell) this pin is anchored to. */\n readonly cell: number;\n /** Symbol id this pin forces onto its cell. */\n readonly symbolId: string;\n /**\n * Original cell at pin creation. The pin migrates back toward this value\n * across MultiWays reshapes when `migration === 'origin'` (the default).\n * For `migration === 'frozen'` this is updated on every clamp so the\n * pin never restores to a higher cell.\n *\n * Non-MultiWays: equals `cell` and never changes.\n */\n readonly originCell: number;\n /**\n * Migration policy across MultiWays reshapes. Default `'origin'`.\n * see {@link PinMigration} for semantics.\n */\n readonly migration: PinMigration;\n /**\n * Lifetime of the pin:\n * - number → counts down after each completed spin; removed at 0\n * - 'eval' → valid for one spin only; cleared at the next spin start\n * - 'permanent' → persists until `unpin()` is called explicitly\n */\n readonly turns: number | 'eval' | 'permanent';\n /** Optional per-instance data: multiplier, value, tier. game-specific. */\n payload?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Options accepted by `reelSet.pin()`.\n *\n * **Calling `pin()` mid-reshape** (from a `shape:changed`, `pin:migrated`, or\n * `adjust:start` event handler) is allowed and well-defined: the new pin is\n * placed at the cell you pass, and `originCell` defaults to the **post-reshape**\n * cell (i.e. whatever `cell` is *now*, on the new shape). If you want the pin\n * to remember a different origin, pass `originCell` explicitly. The pin\n * participates in the next migration cycle like any other pin.\n */\nexport interface CellPinOptions {\n /** Defaults to 'permanent'. */\n turns?: number | 'eval' | 'permanent';\n /** Arbitrary per-instance data. */\n payload?: Record<string, unknown>;\n /**\n * Original cell for MultiWays pin migration. Defaults to the cell at pin\n * placement. With `migration === 'origin'` (default), the pin's `cell`\n * migrates back toward this value when the shape grows enough to fit\n * it; with `migration === 'frozen'` this gets overwritten on every\n * clamp so the pin doesn't restore.\n */\n originCell?: number;\n /**\n * MultiWays migration policy. Default `'origin'`. clamp + restore.\n * Set to `'frozen'` for \"lock at current cell, never restore\" semantics.\n * See {@link PinMigration}.\n */\n migration?: PinMigration;\n}\n\n/**\n * Reason a pin expired. Fired with `pin:expired`.\n * - `'turns'` - its turn counter reached zero.\n * - `'explicit'` - removed via `unpin()`.\n * - `'eval'` - an eval callback returned false.\n * - `'collision'` - a reshape clamped it onto a cell another pin already\n * holds, so it was dropped deterministically.\n */\nexport type PinExpireReason = 'turns' | 'explicit' | 'eval' | 'collision';\n\n/** A grid coordinate. */\nexport interface CellCoord {\n reel: number;\n cell: number;\n}\n\n/** Options for `reelSet.movePin()`. flight animation tuning + lifecycle hooks. */\nexport interface MovePinOptions {\n /** Animation duration in milliseconds. Default 400. */\n duration?: number;\n /** GSAP easing string. Default 'power2.inOut'. */\n easing?: string;\n /**\n * Symbol id to use as the filler at the vacated cell. When omitted, the\n * engine picks a random symbol from its frame builder's random provider.\n */\n backfill?: string;\n /**\n * Fires after the flight symbol is acquired, positioned at `from`, and\n * added to the viewport. but before the tween begins. Use this hook to\n * drive animation state on the flight instance. For example: cast to\n * your `SpineSymbol` subclass and switch to a `run` animation for the\n * duration of the flight.\n *\n * `flight` is the pooled `ReelSymbol` instance. The type is `unknown` so\n * this module stays free of circular imports; cast in the caller.\n */\n onFlightCreated?: (flight: unknown) => void;\n /**\n * Fires after the tween completes, before the flight symbol is released\n * back to the pool. Use this hook to play a landing animation or return\n * a Spine symbol to `idle` before its instance is recycled.\n */\n onFlightCompleted?: (flight: unknown) => void;\n}\n\n/** Map key used internally and exposed by `reelSet.pins`. */\nexport function pinKey(reel: number, cell: number): string {\n return `${reel}:${cell}`;\n}\n","/**\n * The v1 -> v2 rename table (ADR 016 section 5), in one place.\n *\n * Two consumers depend on it: the builder's fail-loud guards below, and the\n * `pixi-reels-codemod` transform. The codemod is a `.cjs` in another\n * package and cannot import this file, so it carries its own copy --\n * `scripts/check-codemod-parity.mjs` fails the build if the two ever\n * disagree about a name or its replacement.\n *\n * Per CLAUDE.md's fail-loud rule there are **no deprecated aliases**. A v1\n * name either fails to compile or throws with the line below; it never\n * quietly means something subtly different.\n */\n\nexport const CODEMOD_HINT = 'run npx pixi-reels-codemod v1-to-v2';\n\n/** Renamed `ReelSetBuilder` methods. The v1 name throws on call. */\nexport const V1_BUILDER_METHODS: Readonly<Record<string, string>> = {\n visibleRows: 'visibleCells',\n visibleRowsPerReel: 'visibleCellsPerReel',\n reelPixelHeights: 'reelExtents',\n};\n\n/** Renamed keys inside builder option objects, grouped by the option they belong to. */\nexport const V1_OPTION_KEYS: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n 'bufferSymbols()': { above: 'start', below: 'end' },\n 'multiways()': {\n minRows: 'minCells',\n maxRows: 'maxCells',\n reelPixelHeight: 'reelExtent',\n },\n 'symbolData() size': { w: 'reels', h: 'cells' },\n 'tumble() fall/dropIn': { rowStagger: 'cellStagger', rowOrder: 'cellOrder' },\n 'offset() trapezoid': { topWidthFactor: 'startFactor', bottomWidthFactor: 'endFactor' },\n 'initialFrame() / setResult() column': {\n bufferAbove: 'bufferStart',\n bufferBelow: 'bufferEnd',\n },\n};\n\n/** Renamed string-literal values, grouped by the option that carries them. */\nexport const V1_OPTION_VALUES: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n 'reelAnchor()': { top: 'start', bottom: 'end' },\n 'tumble() cellOrder': { bottomToTop: 'endFirst', topToBottom: 'startFirst' },\n 'nudge() direction': { down: 'forward', up: 'reverse' },\n};\n\n/** Build the standard \"X was renamed to Y\" message for a single name. */\nexport function renamedMessage(context: string, v1: string, v2: string): string {\n return `${context}: '${v1}' was renamed to '${v2}' in v2; ${CODEMOD_HINT}.`;\n}\n\n/**\n * Throw if `value` carries any v1 key from `map`. `context` names the public\n * API surface so the message points at the caller's own call, not at engine\n * internals.\n */\nexport function assertNoV1Keys(\n value: unknown,\n map: Readonly<Record<string, string>>,\n context: string,\n): void {\n if (!value || typeof value !== 'object') return;\n for (const [v1, v2] of Object.entries(map)) {\n if (v1 in (value as Record<string, unknown>)) {\n throw new Error(renamedMessage(context, v1, v2));\n }\n }\n}\n\n/** Throw if `value` is a v1 string-literal option value. */\nexport function assertNoV1Value(\n value: unknown,\n map: Readonly<Record<string, string>>,\n context: string,\n): void {\n if (typeof value !== 'string') return;\n const v2 = map[value];\n if (v2 !== undefined) {\n throw new Error(renamedMessage(context, value, v2));\n }\n}\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type {\n ReelSetInternalConfig,\n CellBounds,\n SymbolData,\n SpinOptions,\n AnticipationStagger,\n AnticipationOptions,\n} from '../config/types.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSetEvents, SpinResult, RunCascadeResult as RunCascadeResultBase } from '../events/ReelEvents.js';\nimport { Reel, } from './Reel.js';\nimport type { NudgeOptions } from './Reel.js';\nimport type { ReelCurveInput } from './ReelCurve.js';\nimport { ReelViewport } from './ReelViewport.js';\nimport { SpinController } from '../spin/SpinController.js';\nimport { SpeedManager } from '../speed/SpeedManager.js';\nimport { SymbolSpotlight, } from '../spotlight/SymbolSpotlight.js';\nimport type { SymbolFactory } from '../symbols/SymbolFactory.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { FrameBuilder } from '../frame/FrameBuilder.js';\nimport type { PhaseFactory } from '../spin/phases/PhaseFactory.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport type { CellPin, CellPinOptions, PinExpireReason, MovePinOptions, CellCoord } from '../pins/CellPin.js';\nimport { pinKey } from '../pins/CellPin.js';\n\nimport type { FrameMiddleware } from '../frame/FrameBuilder.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport type { RandomSymbolControl } from '../frame/SymbolPool.js';\nimport { assertBufferCountsInRange, assertColumnTargets, cloneColumnTarget } from '../frame/ColumnTarget.js';\nimport { V1_OPTION_KEYS, assertNoV1Keys } from '../config/v1Renames.js';\nimport type { Cell } from '../cascade/tumbleAlgorithm.js';\n\nexport interface ReelSetParams {\n config: ReelSetInternalConfig;\n reels: Reel[];\n viewport: ReelViewport;\n symbolFactory: SymbolFactory;\n frameBuilder: FrameBuilder;\n phaseFactory: PhaseFactory;\n spinningMode: SpinningMode;\n defaultSpinMode: 'standard' | 'cascade';\n}\n\n/**\n * The runtime-mutable frame-builder pipeline exposed on `reelSet.frame`.\n * Matches `FrameBuilder.use/remove`. the internal machinery that already\n * exists; this is the ergonomic surface.\n */\nexport interface FrameAPI {\n /** Add a middleware. Sorted by `priority` on next frame build. */\n use(middleware: FrameMiddleware): void;\n /** Remove a middleware by `name`. No-op if absent. */\n remove(name: string): void;\n /** Current middleware list in registration order. */\n readonly middleware: ReadonlyArray<FrameMiddleware>;\n}\n\n/**\n * Options for {@link ReelSet.destroySymbols}. Every field is optional;\n * the defaults produce the canonical \"winners poof\" look (no stagger,\n * no viewport dim, zIndex bumped to 1000 so destroy effects render\n * above neighbouring cells).\n */\nexport interface DestroySymbolsOptions {\n /**\n * Per-cell start delay in seconds. Default `0` (every cell starts together).\n * Pass `(cell, i) => i * 0.03` for a per-cell stagger.\n */\n delay?: number | ((cell: Cell, index: number) => number);\n /**\n * zIndex applied to each cell's view for the duration of the animation\n * so destroy effects aren't clipped behind neighbours. Default `1000`.\n * The library does NOT restore the previous zIndex. the cell is\n * destroyed (alpha 0) and will be replaced on the next `refill()` /\n * `setResult()`. Pass `null` to skip the bump.\n */\n zIndex?: number | null;\n /**\n * Dim the viewport (`viewport.showDim(alpha)`) while the destroy\n * animation runs, restoring on completion. Pass `false` to skip,\n * a number for a custom alpha. Default: `false` (no dim).\n */\n dim?: boolean | number;\n /**\n * Abort signal. Aborting mid-destroy kills every in-flight\n * `playDestroy` tween and snaps the cells to their destroyed pose\n * (`alpha: 0`) without waiting for the natural end of the animation.\n * The returned promise still resolves normally. abort means\n * \"fast-forward to the destroyed state,\" not \"fail.\" Forwarded\n * automatically by `runCascade`'s own `signal`.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Summary returned by {@link ReelSet.runCascade}. Re-exported from the\n * canonical definition in `events/ReelEvents.ts` so the events module\n * stays the single source of truth for shared shapes.\n */\nexport type RunCascadeResult = RunCascadeResultBase;\n\n/**\n * Options for {@link ReelSet.refill}. The two required fields encode\n * what just happened (`winners`) and what the next visible state should\n * be (`grid`). Everything else is timing / cancellation / animation\n * flavor with sensible defaults.\n *\n * Use this directly when you're driving your own cascade loop. For the\n * common case of \"destroy → refill → check → repeat\", use\n * {@link ReelSet.runCascade} instead. it composes refill, destroySymbols,\n * and win-detection into one call with the same cancellation semantics.\n */\nexport interface RefillOptions {\n /** Winners that were just destroyed. Their cells will be refilled per gravity. */\n winners: ReadonlyArray<Cell>;\n /**\n * Target grid after refill. Convention: per reel, `winners.length` new\n * symbols sit at the gravity-ENTRY end and the survivors pack against the\n * gravity-EXIT end in their original order. Under the default\n * `gravity: 'auto'` that means the first cells are new for a forward reel\n * and the LAST cells are new for a reverse one. Same contract as the\n * `nextGrid` callback in `runCascade`.\n */\n grid: ColumnTarget[];\n /**\n * Pick the refill animation flavor.\n *\n * - `'combined'` (default). survivors and new symbols animate\n * together in one drop-in beat. The Sweet Bonanza / Sugar Rush feel.\n * - `'gravity-then-drop'`. survivors slide down to fill holes FIRST,\n * then a global pause (`gravityHoldMs` + `gravityHold`), then new\n * symbols enter from above. Useful when you want a multiplier or\n * SFX beat between the two motions.\n */\n mode?: 'combined' | 'gravity-then-drop';\n /**\n * Fixed wall-clock pause (ms) between the gravity stage and the\n * drop-in stage. Only applies when `mode === 'gravity-then-drop'`.\n * Default `250`. Combines via `Promise.all` with `gravityHold` if\n * both are provided. whichever finishes LAST gates the drop-in.\n */\n gravityHoldMs?: number;\n /**\n * Promise (or zero-arg factory) gating the drop-in stage. Only\n * applies when `mode === 'gravity-then-drop'`.\n *\n * - `Promise<void>`. pass an already-in-flight animation / SFX /\n * network call's completion handle when you want the drop-in to\n * wait for it. The promise is awaited as-is.\n * - `() => Promise<void>`. pass a factory when the *side effects*\n * of starting the promise should fire AT gravity-end, not at\n * refill-start.\n *\n * Combines via `Promise.all` with `gravityHoldMs`. pass both to\n * floor the hold to a minimum wall-clock duration even if the\n * promise resolves earlier.\n */\n gravityHold?: Promise<void> | (() => Promise<void>);\n /**\n * Awaitable callback fired AFTER `gravityHoldMs` + `gravityHold` both\n * resolve, BEFORE the drop-in stage. Only fires when\n * `mode === 'gravity-then-drop'`. Use for last-mile side effects that\n * need to read the post-hold state.\n */\n onGravityComplete?: () => Promise<void> | void;\n /**\n * Abort signal. Aborting mid-refill slams the in-flight animation so\n * the await unblocks immediately rather than waiting for the drop-in\n * to finish. The returned promise still resolves normally with\n * `wasSkipped: true`. Mirrors the abort contract on\n * {@link RunCascadeOptions.signal} and {@link DestroySymbolsOptions.signal}.\n */\n signal?: AbortSignal;\n}\n\n/**\n * Summary returned by {@link ReelSet.refill}. Symmetric with\n * {@link RunCascadeResult}. same vocabulary, narrower scope (one stage\n * vs. the full chain).\n */\nexport interface RefillResult {\n /** Number of winner cells that were refilled. Equals `winners.length`. */\n winnersRefilled: number;\n /** Visible grid after the refill landed. Matches `getVisibleGrid()`. */\n finalGrid: string[][];\n /** True if the refill was aborted via `signal` (slammed to land). */\n wasSkipped: boolean;\n /** Total refill duration in milliseconds. */\n duration: number;\n}\n\n/**\n * Options for {@link ReelSet.runCascade}. The two required callbacks\n * (`detectWinners`, `nextGrid`) encode game rules; everything else is\n * timing / cancellation / forwarded-to-destroy plumbing with sensible\n * defaults.\n */\nexport interface RunCascadeOptions {\n /**\n * Win-detection callback. Receives the current grid (a fresh copy from\n * `getVisibleGrid()`) and the chain level (0 on the first iteration).\n * Returns the cells whose symbols are \"winners\" that should be cleared\n * before the next refill. Return `[]` to end the chain. Sync or async.\n */\n detectWinners: (\n grid: string[][],\n chainLevel: number,\n ) => readonly Cell[] | Promise<readonly Cell[]>;\n /**\n * Next-grid callback. Given the post-destroy grid and the winners that\n * were cleared, return the grid the survivors + new symbols should\n * land on. This is your server-side gravity simulation (or the\n * fallback `cascadeNextGrid` from your client). Sync or async.\n *\n * Must follow the gravity convention: per reel, `winners.length` new\n * symbols at the gravity-entry end, survivors packed against the\n * gravity-exit end in their original order. `tumble({ gravity })` picks\n * which end is which; the engine animates your grid, it does not reorder\n * it. Same contract as `refill({ grid })`.\n *\n * Returns `ColumnTarget[]` -- one entry per reel, `{ visible }` plus any\n * `bufferStart` / `bufferEnd` anchors. A plain `string[][]` is not\n * accepted anywhere in the API; wrap it with\n * `grid.map((visible) => ({ visible }))`.\n */\n nextGrid: (\n grid: string[][],\n winners: readonly Cell[],\n chainLevel: number,\n ) => ColumnTarget[] | Promise<ColumnTarget[]>;\n /**\n * Win-presentation hook fired AFTER detection (`cascade:chain:start`)\n * and BEFORE `destroySymbols`. the beat where the winners are still on\n * the board. This is where a `WinPresenter` pass belongs: play the\n * authored win clip, dim the losers, await, and only then does the\n * library destroy the cells. A round's presentation order is\n * win → destroy → refill; `onCascade` remains the post-destroy hook.\n *\n * - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n * - `winners`. cells about to be destroyed. still visible.\n * - `currentGrid`. the grid as it stood at `cascade:chain:start`.\n */\n presentWinners?: (info: {\n chain: number;\n winners: readonly Cell[];\n currentGrid: string[][];\n }) => void | Promise<void>;\n /**\n * Per-cascade hook fired AFTER `destroySymbols` and BEFORE the refill\n * starts. Use it to bump multipliers, play SFX, run \"winners gone\"\n * UI animations. Return a promise to delay the refill (e.g. for a\n * number-roll animation).\n *\n * - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n * - `winners`. cells that were just destroyed.\n * - `currentGrid`. the grid as it stood at `cascade:chain:start`\n * (same reference). The symbols at `winners` are visually gone but\n * the grid array still names them. `nextGrid` will replace them.\n */\n onCascade?: (info: {\n chain: number;\n winners: readonly Cell[];\n currentGrid: string[][];\n }) => Promise<void> | void;\n /**\n * Milliseconds to wait between win-destroy completing and the next\n * refill starting. Commercial slots dial this between 150 ms (snappy)\n * and 500 ms (dramatic). Default `250`.\n */\n pauseAfterDestroyMs?: number;\n /**\n * Safety cap on cascade-chain length. Defaults to `32`. a sane\n * upper bound that protects against pathological server bugs while\n * being well above any commercial slot's natural cap. Pass `Infinity`\n * to disable.\n */\n maxChain?: number;\n /**\n * Forwarded to `destroySymbols(cells, opts)` on every cascade. Useful\n * for per-cell stagger, viewport dim, an abort signal, etc.\n */\n destroyOptions?: DestroySymbolsOptions;\n /**\n * How each refill in the chain animates.\n *\n * - `'combined'` (default). survivors and new symbols animate\n * together in one drop-in beat. The Sweet Bonanza / Sugar Rush feel.\n * - `'gravity-then-drop'`. survivors slide down to fill holes FIRST,\n * then a global pause (`gravityHoldMs`), then new symbols enter\n * from above with the per-reel stop delay applied. The Mummyland\n * Treasures / Reactoonz feel. gives space for an anticipation\n * beat between gravity and new-symbol entry.\n *\n * Per-column stagger inside the new-symbol drop is controlled by\n * `setDropOrder('ltr', stepMs)` exactly as in combined mode. when the\n * step is shorter than `dropIn.duration` you get overlapping waves;\n * when it's at least as long you get strictly sequential columns.\n */\n refillMode?: 'combined' | 'gravity-then-drop';\n /**\n * Fixed wall-clock pause between gravity end and drop-in start, in ms.\n * Only used when `refillMode === 'gravity-then-drop'`. Default `250`.\n * Combines via `Promise.all` with `gravityHold` if both are provided.\n *\n * The natural place for asymmetric anticipation visuals: register a\n * listener on `cascade:gravity:end` (one per reel) and trigger your\n * mascot / multiplier roll / SFX from there. Use `gravityHold` if you\n * already have an in-flight animation promise, or `onGravityComplete`\n * if you need a post-hold callback.\n */\n gravityHoldMs?: number;\n /**\n * Per-cascade promise-builder. Invoked once per chain stage at the\n * **gravity-end boundary** (i.e. AFTER every reel's gravity stage has\n * settled, just before the global hold begins). The returned promise\n * is awaited in parallel with `gravityHoldMs` via `Promise.all`.\n * whichever finishes LAST gates the drop-in. Only fires when\n * `refillMode === 'gravity-then-drop'`.\n *\n * Use this when each cascade starts its own anticipation animation\n * (multiplier roll, mascot reaction, anticipation SFX) and you want\n * the builder's *side effects* (e.g. `multiplier.bumpTo(chain + 1)`)\n * to fire AT gravity-end. not back when the refill args were\n * assembled. The library calls your function at the right beat and\n * awaits the promise you return.\n *\n * - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n * - `winners`. cells cleared this cascade.\n *\n * A rejection from the returned promise is surfaced via the\n * `cascade:gravity:error` event AND logged via `console.error`; the\n * engine slams the refill so the awaited promise still settles.\n */\n gravityHold?: (info: {\n chain: number;\n winners: readonly Cell[];\n }) => Promise<void>;\n /**\n * Per-cascade callback fired AFTER `gravityHoldMs` + `gravityHold` both\n * resolve, BEFORE the drop-in stage. Only fires when\n * `refillMode === 'gravity-then-drop'`. Use for last-mile side effects\n * that need to read post-hold state (e.g. snapshot the multiplier\n * value that just finished its count-up).\n *\n * - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n * - `winners`. cells cleared this cascade.\n */\n onGravityComplete?: (info: {\n chain: number;\n winners: readonly Cell[];\n }) => Promise<void> | void;\n /**\n * Abort signal for caller-driven cancellation. The loop exits at the\n * next await boundary, the in-flight refill (if any) is slammed via\n * `slamStop()`, and the resolved summary reports `wasSkipped: true`.\n *\n * Use this for \"player tapped SLAM mid-cascade\". `reelSet.skipSpin()` is\n * a no-op when called between refills (the engine is idle), so it\n * cannot end the chain from a button handler. AbortController can.\n *\n * ```ts\n * const controller = new AbortController();\n * skipButton.addEventListener('click', () => controller.abort());\n * await reelSet.runCascade({ ..., signal: controller.signal });\n * ```\n */\n signal?: AbortSignal;\n}\n\n/**\n * The whole slot board as one object.\n *\n * A `ReelSet` is a PixiJS `Container` that owns every reel, the spin\n * controller, the speed manager, and the win spotlight. You `addChild` it\n * to your stage and then drive it from the four public verbs below:\n *\n * - `spin()`. start the reels moving, returns a promise that resolves\n * when every reel has landed (or been slam-stopped)\n * - `setResult(grid)`. tell the reels what to land on; the spin\n * controller consumes this and each reel queues its target symbols\n * - `setAnticipation(reelIndices)`. slow the given reels before they\n * stop, for \"will the third scatter land?\" tension\n * - `skipSpin()` lands the in-flight spin immediately. The slam-stop button calls this.\n *\n * Everything else is subsystems: `speed`, `spotlight`, `events`, `viewport`.\n * Construction goes through {@link ReelSetBuilder}, never `new ReelSet()`\n * directly. the builder enforces that every required piece is wired.\n *\n * ```ts\n * const reelSet = new ReelSetBuilder()\n * .reels(5).visibleCells(3).symbolSize(140, 140)\n * .symbols((r) => r.register('cherry', SpriteSymbol, { textures }))\n * .ticker(app.ticker)\n * .build();\n * app.stage.addChild(reelSet);\n *\n * const spin = reelSet.spin();\n * reelSet.setResult(await server.spin());\n * await spin;\n * ```\n *\n * Teardown cascades: one `reelSet.destroy()` disposes every child.\n */\nexport class ReelSet extends Container implements Disposable {\n /**\n * zIndex applied to pin overlays so they render above the reel strip.\n *\n * **The library's z-index budget**, for reference if you author symbols\n * that need to layer above defaults:\n *\n * | Layer | zIndex | Source |\n * |---|---|---|\n * | 1×1 symbol, default | `0 * 100 + arrayIndex` (~0–10) | `symbolData.zIndex ?? 0` |\n * | 1×1 symbol, elevated (`zIndex: 1` on `symbolData`) | `1 * 100 + arrayIndex` (~100) | `symbolData.zIndex` |\n * | Big-symbol anchor, default registration | `5 * 100 + arrayIndex` (~500) | recipe convention |\n * | Pin overlay (sticky/expanding wild during spin) | `10000` | `PIN_OVERLAY_Z_INDEX` |\n *\n * The 100× multiplier on `symbolData.zIndex` leaves room for per-cell\n * stacking inside a layer (bottom cells render in front of top cells on\n * the same layer). The 10000 ceiling on pin overlays is set very high\n * so a consumer who sets `symbolData.zIndex: 50` (= 5000) still sits\n * below pins. If you need to stack ABOVE pin overlays. e.g. a win-\n * presenter symbol promotion. re-parent the symbol to\n * `viewport.spotlightContainer`, which is its own DisplayObject layer\n * above pin overlays.\n */\n private static readonly PIN_OVERLAY_Z_INDEX = 10000;\n\n private _events = new EventEmitter<ReelSetEvents>();\n private _reels: Reel[];\n private _viewport: ReelViewport;\n private _spinController: SpinController;\n private _speedManager: SpeedManager;\n private _spotlight: SymbolSpotlight;\n private _symbolFactory: SymbolFactory;\n private _frameBuilder: FrameBuilder;\n private _frameAPI: FrameAPI;\n private _isDestroyed = false;\n private _pins = new Map<string, CellPin>();\n /**\n * Visual overlays rendered above the reel viewport while a spin is in\n * motion. Each overlay is a pooled ReelSymbol sitting in the viewport's\n * unmaskedContainer at the pin's cell position. it keeps the pinned\n * symbol visible while the underlying reel scrolls. Created on\n * spin:start, destroyed on spin:allLanded. The pin is co-stored so\n * _destroyPinOverlay always has it available even after the pin is\n * removed from `_pins` (e.g. during unpin()).\n */\n private _pinOverlays = new Map<string, { pin: CellPin; overlay: ReelSymbol }>();\n\n /**\n * MultiWays: target cell counts for the next AdjustPhase. Recorded by\n * `setShape()`, consumed by `SpinController` when it builds AdjustPhase\n * configs. `null` means \"no shape change pending\".\n */\n private _targetShape: number[] | null = null;\n\n /**\n * True once `setResult()` has been called for the current spin. Reset on\n * every `spin:start`. Used to enforce the contract that `setShape()`\n * must be called BEFORE `setResult()`. calling it after corrupts the\n * cached frames (pins were applied at their pre-migration cells; a later\n * setShape would migrate them but the frames are already built).\n */\n private _resultSetForCurrentSpin = false;\n\n /** Set at construction by the builder when `.multiways(...)` was called. */\n private _isMultiWaysSlot: boolean;\n\n /**\n * Number of `nudge()` calls currently in flight. Reference-counted (not a\n * boolean) so parallel nudges across reels don't clear the guard early: the\n * first to settle would otherwise let spin/setResult/pin/setShape race a\n * still-live nudge. Used by those methods to throw while any nudge runs.\n */\n private _nudgesInFlight = 0;\n private _multiwaysMinCells = 0;\n private _multiwaysMaxCells = 0;\n private _multiwaysReelExtent = 0;\n\n /** Resolved per-symbol metadata (size, zIndex, etc). */\n private _symbolsData: Record<string, SymbolData>;\n\n /** Horizontal symbol gap (px). Used by `getBlockBounds` for big symbols. */\n\n constructor(params: ReelSetParams) {\n super();\n\n this._reels = params.reels;\n this._viewport = params.viewport;\n this._symbolFactory = params.symbolFactory;\n this._frameBuilder = params.frameBuilder;\n this._symbolsData = params.config.symbols;\n this._isMultiWaysSlot = !!params.config.grid.multiways;\n if (params.config.grid.multiways) {\n this._multiwaysMinCells = params.config.grid.multiways.minCells;\n this._multiwaysMaxCells = params.config.grid.multiways.maxCells;\n this._multiwaysReelExtent = params.config.grid.multiways.reelExtent;\n }\n\n // Wire each reel's cross-reel resolver so `Reel.getVisibleSymbols()`\n // returns the anchor's id even when the OCCUPIED cell's anchor lives\n // on a different reel. Without this, per-reel surface returns the\n // sentinel for cross-reel cells. making it inconsistent with\n // `ReelSet.getVisibleGrid()`.\n for (const reel of this._reels) {\n reel.setCrossReelResolver((reel, cell) => {\n const fp = this.getSymbolFootprint(reel, cell);\n const anchorReel = this._reels[fp.anchor.reel];\n // Anchor cell is on its OWN reel. read its symbolId directly to\n // avoid recursing back through this resolver.\n return anchorReel.symbols[anchorReel.bufferStart + fp.anchor.cell].symbolId;\n });\n }\n\n const fb = this._frameBuilder;\n this._frameAPI = {\n use(mw: FrameMiddleware): void { fb.use(mw); },\n remove(name: string): void { fb.remove(name); },\n get middleware(): ReadonlyArray<FrameMiddleware> { return fb.middleware; },\n };\n\n this._speedManager = new SpeedManager(\n params.config.speeds,\n params.config.initialSpeed,\n );\n\n this._spinController = new SpinController(\n params.reels,\n this._speedManager,\n params.frameBuilder,\n params.phaseFactory,\n this._events,\n params.config.ticker,\n params.spinningMode,\n params.defaultSpinMode,\n {\n isMultiWaysSlot: this._isMultiWaysSlot,\n symbolsData: this._symbolsData,\n peekTargetShape: () => this.peekTargetShape(),\n clearTargetShape: () => this.clearTargetShape(),\n multiwaysReelExtent: this._multiwaysReelExtent,\n getPinsOnReel: (reelIndex) => this._pinsOnReel(reelIndex),\n migratePinsForReel: (reelIndex, newCells) => this._migratePinsForReel(reelIndex, newCells),\n refreshPinOverlaysForReel: (reelIndex) => this.refreshPinOverlaysForReel(reelIndex),\n buildPinOverlayTweens: (reelIndex, targetCellMain) =>\n this._buildPinOverlayTweens(reelIndex, targetCellMain),\n },\n );\n\n this._spotlight = new SymbolSpotlight(params.reels, params.viewport, this._events);\n\n this.addChild(this._viewport);\n\n // Pin lifecycle: decrement numeric turns when the spin lands; clear 'eval'\n // pins when the next spin starts.\n this._events.on('spin:allLanded', () => this._onSpinLanded());\n this._events.on('spin:start', () => this._onSpinStart());\n }\n\n // ─── Event system ──────────────────────────────────────────\n // Uses a dedicated emitter to avoid collision with PixiJS Container events.\n\n /** The event emitter for reel-specific events. */\n get events(): EventEmitter<ReelSetEvents> {\n return this._events;\n }\n\n // ─── Spin API ─────────────────────────────────────────────\n\n /**\n * Start spinning. Returns a promise that resolves when all (non-held)\n * reels land.\n *\n * Pass `{ holdReels: [i, ...] }` to keep specific columns frozen for\n * this spin. they skip START / SPIN / STOP entirely and stay on\n * whatever symbols they're currently showing. The use cases are\n * Hold & Win respins, sticky / expanding wilds, and \"the trigger\n * column stays in place\" bonus rounds.\n *\n * Pass `{ mode: 'standard' | 'cascade' }` to override the builder-time\n * default for a single spin (e.g. classic strip-spin on the first round,\n * drop-in on the cascade waves). `'cascade'` requires `.tumble(...)`\n * on the builder.\n *\n * @example\n * // Plain spin. every reel animates.\n * await reelSet.spin();\n *\n * @example\n * // Hold reels 0 and 4; only the middle three reroll.\n * const spin = reelSet.spin({ holdReels: [0, 4] });\n * reelSet.setResult(serverGrid); // entries at 0/4 are ignored\n * await spin;\n *\n * @example\n * // Per-spin cascade override.\n * await reelSet.spin({ mode: 'cascade' });\n *\n * See {@link SpinOptions} for the full contract (event behaviour,\n * setResult interaction, setAnticipation filtering).\n */\n async spin(options?: SpinOptions): Promise<SpinResult> {\n this._assertNoNudgeInFlight('spin');\n return this._spinController.spin(options);\n }\n\n /**\n * Set the target result symbols. Triggers the stop sequence.\n *\n * One `ColumnTarget` per reel. `visible` is the visible-window target;\n * optional `bufferStart` / `bufferEnd` target cells outside it.\n *\n * If any pins are active (`reelSet.pin(...)`), their symbols are overlaid\n * onto the result before it reaches the stop sequencer, so pinned cells\n * always land on the pin's `symbolId` regardless of what the server sent.\n *\n * @example\n * reelSet.setResult([\n * { visible: ['A','B','C'] },\n * { visible: ['A','B','C'] },\n * { visible: ['A','B','C'], bufferStart: ['COIN'] },\n * { visible: ['A','B','C'] },\n * { visible: ['A','B','C'] },\n * ]);\n */\n setResult(symbols: ColumnTarget[]): void {\n this._assertNoNudgeInFlight('setResult');\n // Before anything else: a `string[][]` here used to blow up deep in the\n // frame pipeline, AFTER the reels were moving, so the spin never settled.\n this._assertGrid(symbols, 'setResult()');\n const withPins = this._applyPinsToGrid(this._cloneTargets(symbols));\n this._resultSetForCurrentSpin = true;\n this._spinController.setResult(withPins);\n }\n\n /**\n * Tumble cascade: cascade refill (Moment B). Call this AFTER you've faded\n * out the winning symbols in your own code, with the list of winner cells\n * and the next grid the server returned.\n *\n * - Untouched survivors don't animate.\n * - Survivors behind a hole slide toward the gravity-exit edge to fill it.\n * - New symbols enter from the gravity-entry edge into the\n * `winners.length` cells left at that end.\n *\n * The new grid must follow the gravity convention: per reel, the\n * `winnerCells.length` cells nearest the gravity-ENTRY edge are the new\n * symbols and the rest are survivors in their original order. On the\n * default vertical/forward reel that is the familiar \"top N are new\";\n * on a reverse reel it is the last N. This matches what server-side\n * gravity simulations emit.\n *\n * Resolves with a {@link RefillResult} (mirror of {@link RunCascadeResult}.\n * one stage's worth). Requires the builder to have been configured with\n * `.tumble(...)`.\n *\n * For the common destroy → refill → check → repeat loop, prefer\n * {@link ReelSet.runCascade}. it composes refill, destroySymbols, and\n * win-detection with the same cancellation semantics.\n *\n * @example\n * const winners = detectWins(currentGrid);\n * await reelSet.destroySymbols(winners);\n * const next = await server.cascade(winners);\n * const result = await reelSet.refill({ winners, grid: next });\n * console.log(result.finalGrid, result.wasSkipped);\n *\n * @example\n * // Abort mid-refill: slams the in-flight animation, resolves with wasSkipped.\n * const ac = new AbortController();\n * skipButton.onclick = () => ac.abort();\n * const result = await reelSet.refill({\n * winners, grid: next, signal: ac.signal,\n * });\n */\n async refill(opts: RefillOptions): Promise<RefillResult> {\n // Same gate as `setResult`. A cascade grid arrives straight off a server\n // response, so it is the LEAST type-checked input the engine takes: a\n // stale `bufferAbove` used to sail through here and get silently\n // random-filled on every stage of the chain.\n this._assertGrid(opts.grid, 'refill(): grid');\n const startTime = performance.now();\n let wasSkipped = false;\n\n const onSkip = (): void => { wasSkipped = true; };\n this._events.on('skip:requested', onSkip);\n\n const onAbort = (): void => {\n wasSkipped = true;\n if (this._spinController.isSpinning) {\n this._spinController.slamStop();\n }\n };\n if (opts.signal) {\n if (opts.signal.aborted) onAbort();\n else opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n try {\n const spinResult = await this._spinController.refill(opts);\n return {\n winnersRefilled: opts.winners.length,\n finalGrid: spinResult.symbols,\n wasSkipped: wasSkipped || spinResult.wasSkipped,\n duration: performance.now() - startTime,\n };\n } finally {\n this._events.off('skip:requested', onSkip);\n if (opts.signal) {\n opts.signal.removeEventListener('abort', onAbort);\n }\n }\n }\n\n /**\n * Destroy a batch of cells in parallel, deferring to each symbol's own\n * `playDestroy()` so subclasses (Spine, particles, custom sprites) can\n * provide art-appropriate disintegration without the spin handler caring.\n *\n * This is the canonical \"fade out the winners\" step in a cascade chain:\n * call it between win-detection and `refill()`. Every cell's view is\n * lifted with a high zIndex so the destroy animation isn't clipped by\n * neighbours. The default `playDestroy` is a brief scale/fade implode;\n * override it per symbol class for art-appropriate destruction.\n *\n * - Empty `cells` resolves immediately, no work.\n * - Out-of-range cells throw. the contract is that you've already\n * run win detection on the visible grid, so coords must be valid.\n *\n * @example\n * const winners = detectWinners(reelSet.getVisibleGrid());\n * await reelSet.destroySymbols(winners);\n * await reelSet.refill({ winners, grid: nextGrid });\n *\n * @example\n * // Per-cell stagger. disintegrate left-to-right with a 30 ms beat.\n * await reelSet.destroySymbols(winners, {\n * delay: (cell, i) => i * 0.03,\n * });\n */\n async destroySymbols(\n cells: ReadonlyArray<Cell>,\n opts?: DestroySymbolsOptions,\n ): Promise<void> {\n if (cells.length === 0) return;\n\n const resolveDelay = (cell: Cell, i: number): number => {\n const d = opts?.delay;\n if (typeof d === 'function') return d(cell, i);\n return d ?? 0;\n };\n\n // Validate up-front so partial work doesn't leave the grid in a half-\n // destroyed state. Cheap O(n) walk; fails loud with the bad coord.\n for (const cell of cells) {\n if (cell.reel < 0 || cell.reel >= this._reels.length) {\n throw new RangeError(\n `destroySymbols: cell.reel ${cell.reel} out of range [0, ${this._reels.length})`,\n );\n }\n const reel = this._reels[cell.reel];\n if (cell.cell < 0 || cell.cell >= reel.visibleCells) {\n throw new RangeError(\n `destroySymbols: cell.cell ${cell.cell} out of range [0, ${reel.visibleCells}) ` +\n `for reel ${cell.reel}`,\n );\n }\n }\n\n const dim = opts?.dim;\n if (dim) {\n this._viewport.showDim(typeof dim === 'number' ? dim : 0.35);\n }\n\n const z = opts?.zIndex === undefined ? 1000 : opts.zIndex;\n\n const signal = opts?.signal;\n this._events.emit('cascade:destroy:start', { cells });\n try {\n // allSettled (not all) so a single misbehaving playDestroy doesn't\n // strand its siblings mid-animation. Failed cells are surfaced via\n // the `failed` field on `cascade:destroy:end` so listeners can log\n // / replay-mark / alarm; the cell stays at whatever pose its tween\n // left it in (typically still visible). the next `refill()` resets\n // it via `_replaceSymbol` regardless.\n const results = await Promise.allSettled(cells.map((cell, i) => {\n const reel = this._reels[cell.reel];\n const sym = reel.getSymbolAt(cell.cell);\n // The range check above proves `cell.cell` is a legal visible cell, so\n // a miss here means the strip itself is short or holed - a torn-down\n // reel still being driven, not a bad coordinate from the caller.\n // Without this it surfaced as `Cannot read properties of undefined\n // (reading 'view')` from inside an Array.map, naming neither the cell\n // nor the reel.\n if (!sym) {\n throw new Error(\n `destroySymbols: reel ${cell.reel} has no symbol at visible cell ` +\n `${cell.cell} (strip length ${reel.symbols.length}, bufferStart ` +\n `${reel.bufferStart}, visibleCells ${reel.visibleCells}). The reel ` +\n 'was torn down or reshaped while a cascade was in flight.',\n );\n }\n if (z !== null) sym.view.zIndex = z;\n return sym.playDestroy({\n delay: resolveDelay(cell, i),\n signal,\n });\n }));\n const failed: Cell[] = [];\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === 'rejected') {\n failed.push(cells[i]);\n // eslint-disable-next-line no-console\n console.warn(\n `[pixi-reels] destroySymbols: cell (${cells[i].reel}, ${cells[i].cell}) ` +\n 'playDestroy rejected:',\n (results[i] as PromiseRejectedResult).reason,\n );\n }\n }\n this._events.emit('cascade:destroy:end',\n failed.length > 0 ? { cells, failed } : { cells });\n } finally {\n if (dim) this._viewport.hideDim();\n }\n }\n\n /**\n * Run the canonical cascade chain on top of `refill()`. Loops:\n * detect winners → destroy → pause → refill → emit. until\n * `detectWinners` returns an empty list (or `maxChain` is hit, or the\n * player slammed via `skipSpin()` / abort). Resolves with the final grid\n * and a summary.\n *\n * The orchestration is library-owned; the **game rules** (what counts\n * as a winner, how the next grid is computed) stay in your callbacks.\n * This is the cascade equivalent of `spin()` + `setResult()`. three\n * lines instead of fifteen, and the slam path is handled for you.\n *\n * Typical usage:\n *\n * ```ts\n * await reelSet.spin();\n * reelSet.setResult(await server.spin());\n * const summary = await reelSet.runCascade({\n * detectWinners: (grid) => detectClusters(grid),\n * nextGrid: async (grid, winners) => server.cascade(winners),\n * onCascade: ({ chain, winners }) => bumpMultiplier(chain),\n * });\n * console.log(summary.chainLength, summary.totalWinners);\n * ```\n *\n * Composes with everything else in the library:\n * - `setDropOrder(...)` is honoured on every refill in the chain. set\n * it before `runCascade` and the same order applies to every drop.\n * - `cascade:fall:symbol`, `cascade:place:end`, `cascade:dropIn:symbol`\n * fire on each refill.\n * - `reelSet.skipSpin()` ends the chain immediately; the returned summary\n * reports `wasSkipped: true`.\n *\n * Event order per stage with winners: `cascade:chain:start` →\n * `cascade:destroy:start` → (destroy tweens) → `cascade:destroy:end` →\n * `onCascade` → pause → refill (`cascade:place:end` +\n * `cascade:dropIn:*` per reel) → `cascade:chain:end`. The chain itself\n * is delimited by the returned `Promise`. `await` the call to know\n * when it's done.\n *\n * Requires `.tumble(...)` on the builder (same as `refill()`).\n */\n async runCascade(opts: RunCascadeOptions): Promise<RunCascadeResult> {\n const pauseMs = opts.pauseAfterDestroyMs ?? 250;\n const maxChain = opts.maxChain ?? 32;\n let wasSkipped = false;\n const onSkip = (): void => { wasSkipped = true; };\n this._events.on('skip:requested', onSkip);\n\n const onAbort = (): void => {\n wasSkipped = true;\n // If a refill is currently animating, slam it so the await unblocks\n // immediately rather than after the full drop-in. slamStop is a no-op\n // when the engine is idle (between refills), so we only need this\n // guard for in-flight cancellation.\n if (this._spinController.isSpinning) {\n this._spinController.slamStop();\n }\n };\n if (opts.signal) {\n if (opts.signal.aborted) onAbort();\n else opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n let chainLength = 0;\n let totalWinners = 0;\n let current = this.getVisibleGrid();\n\n try {\n while (chainLength < maxChain && !wasSkipped) {\n const winners = await opts.detectWinners(current, chainLength);\n if (winners.length === 0) break;\n totalWinners += winners.length;\n\n const stage = chainLength + 1;\n this._events.emit('cascade:chain:start', {\n chain: stage,\n winners,\n currentGrid: current,\n });\n\n // Win presentation FIRST. the winners are still on the board.\n // Awaited so the destroy waits for the presenter pass.\n if (opts.presentWinners) {\n await opts.presentWinners({ chain: stage, winners, currentGrid: current });\n if (wasSkipped) break;\n }\n\n // Forward the round-level abort signal into destroySymbols so a\n // mid-destroy abort kills the in-flight tweens immediately instead\n // of letting them run their full ~300 ms. The opts.destroyOptions\n // signal (if any) takes precedence to honor explicit per-batch\n // overrides; otherwise we use the cascade-level one.\n const destroyOpts = opts.destroyOptions?.signal\n ? opts.destroyOptions\n : { ...opts.destroyOptions, signal: opts.signal };\n await this.destroySymbols(winners, destroyOpts);\n if (wasSkipped) break;\n\n if (opts.onCascade) {\n await opts.onCascade({ chain: stage, winners, currentGrid: current });\n if (wasSkipped) break;\n }\n\n if (pauseMs > 0) {\n // Abort-cancellable wait. A plain `setTimeout` would run to\n // completion regardless of `signal.aborted`, adding up to\n // `pauseMs` of dead air between an abort and the loop exit.\n // We race the timer against `signal.aborted` so an abort mid-\n // pause unblocks the loop within a microtask.\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, pauseMs);\n if (!opts.signal) return;\n const onAbortPause = (): void => {\n clearTimeout(timer);\n resolve();\n };\n if (opts.signal.aborted) onAbortPause();\n else opts.signal.addEventListener('abort', onAbortPause, { once: true });\n });\n if (wasSkipped) break;\n }\n\n const next = await opts.nextGrid(current, winners, chainLength);\n if (wasSkipped) break;\n\n const refillMode = opts.refillMode ?? 'combined';\n // Wrap `opts.gravityHold` in a FACTORY so the user's builder is\n // invoked at gravity-end (inside `_refillTwoStage`), not at\n // refill-start. This matters when the builder has side effects.\n // e.g. `multiplier.bumpTo(chain + 1); return multiplier.done`.\n // that the player should see synchronized with the gravity-end\n // beat. Without the wrapping the bump would fire ~the duration\n // of the gravity stage too early.\n // Checked here so a bad `nextGrid` names ITSELF rather than surfacing\n // as a `refill()` error two frames later. `nextGrid` is the callback\n // that returns a raw server response, so it is where v1 keys arrive.\n this._assertGrid(next, 'runCascade(): nextGrid');\n await this.refill({\n winners: [...winners],\n grid: next,\n mode: refillMode,\n gravityHoldMs: opts.gravityHoldMs,\n gravityHold: opts.gravityHold\n ? () => opts.gravityHold!({ chain: stage, winners })\n : undefined,\n onGravityComplete: opts.onGravityComplete\n ? () => opts.onGravityComplete!({ chain: stage, winners })\n : undefined,\n });\n chainLength += 1;\n current = this.getVisibleGrid();\n\n this._events.emit('cascade:chain:end', {\n chain: stage,\n winners,\n nextGrid: current,\n });\n }\n } finally {\n this._events.off('skip:requested', onSkip);\n if (opts.signal) {\n opts.signal.removeEventListener('abort', onAbort);\n }\n }\n\n const summary: RunCascadeResult = {\n chainLength,\n totalWinners,\n finalGrid: current,\n wasSkipped,\n };\n return summary;\n }\n\n /**\n * Set which reels should show anticipation before stopping, and how their\n * slow-downs are spaced via `stagger`:\n *\n * - `0` (default): every anticipation reel begins slowing at once (the\n * historical parallel behaviour).\n * - `number`: reel at tease-order `k` starts its slow-down `k * stagger`\n * ms after the first, so the tease sweeps across the reels.\n * - `number[]`: explicit per-tease-order offset in ms.\n * - `'sequential'`: each reel waits until the previous anticipation reel\n * has fully landed before it starts. maximal one-at-a-time tension.\n *\n * Offsets are by tease-order (position in `reelIndices`), not raw reel\n * index. Reset at the start of every `spin()`.\n *\n * Pass a `{ stagger, slowdown, duration }` object to shape the tease more:\n * - `slowdown` interpolates across the tease sequence so each successive\n * reel slows to a lower speed (`from` → `to`) and/or holds longer\n * (`holdFrom` → `holdTo`) — the escalating \"each reel crawls slower than\n * the last\" build-up. See {@link AnticipationSlowdown}.\n * - `duration` (ms) overrides the profile's `anticipationDelay`, so the\n * tease plays even in Turbo / SuperTurbo (whose profiles use\n * `anticipationDelay: 0` and would otherwise skip anticipation).\n *\n * Listen to `anticipation:reel` ({ reelIndex, order, total }) to drive\n * per-step tension SFX / a pitch ramp, and `anticipation:reelEnd` to stop it.\n *\n * @example\n * // Classic \"2 scatters showing\" sweep across the last three reels:\n * reelSet.setResult(grid);\n * reelSet.setAnticipation([2, 3, 4], 450); // 450ms apart\n * reelSet.setAnticipation([2, 3, 4], 'sequential'); // strict one-by-one\n *\n * // Keep the tease alive in turbo (profile anticipationDelay is 0):\n * reelSet.setAnticipation([2, 3, 4], { duration: 350, stagger: 200 });\n *\n * // Escalating slow-down: later reels crawl slower and hold longer.\n * reelSet.setAnticipation([2, 3, 4], {\n * stagger: 400,\n * slowdown: { from: 0.45, to: 0.12, holdTo: 2 },\n * });\n *\n * // Drive which reels tease straight from the result grid:\n * const reels = anticipationForScatters(grid, { symbol: 'SCAT', trigger: 2 });\n * reelSet.setAnticipation(reels, { stagger: 'sequential', slowdown: { from: 0.4, to: 0.1 } });\n */\n setAnticipation(\n reelIndices: number[],\n options: AnticipationStagger | AnticipationOptions = 0,\n ): void {\n this._spinController.setAnticipation(reelIndices, options);\n }\n\n /**\n * Override the per-reel stop delay (in ms). Pass one value per reel.\n *\n * **Sticky.** The override persists indefinitely. it survives across\n * `spin()` AND `refill()` boundaries until you call `setStopDelays()`\n * (or `setDropOrder()`) again. The persistence is deliberate: cascade\n * recipes that set `setDropOrder('all')` once before `runCascade(...)`\n * want every internal `refill()` to honor it. If your rounds use\n * different patterns, re-set explicitly per round.\n *\n * Pass `null` to CLEAR the override and restore the default\n * `i * speed.stopDelay` stagger. this is distinct from `[]` / all-zeros\n * (which lands every reel simultaneously). Use it to undo a one-off\n * per-round pattern without hard-coding the default back in.\n *\n * @example\n * // Stagger the last two reels more than the default for dramatic effect:\n * reelSet.setStopDelays([0, 140, 280, 600, 1100]);\n * // ...later, go back to the profile default:\n * reelSet.setStopDelays(null);\n */\n setStopDelays(delays: number[] | null): void {\n this._spinController.setStopDelays(delays);\n }\n\n /**\n * Round-aware spin skip. The button-press entry point. The first press\n * in a round slams the current drop AND applies a round-scoped side\n * effect:\n *\n * - Standard mode: boost the active speed profile to the fastest\n * registered one (emits `skip:boosted`). Restored on the next\n * `spin()` (unless the app manually changed speed in between).\n * - Cascade/tumble mode: flag every subsequent `refill()` to\n * auto-slam with no animation. One press ends a multi-drop cascade.\n *\n * Subsequent presses also slam each current drop.\n *\n * Throws if called before `setResult()` arrives (nothing to land on:\n * slamming now would land on random spin-buffer content). The universal\n * \"spin/skip\" button pattern should call `requestSkip()` in that window\n * (or wrap `skipSpin()` in a try/catch that routes to `requestSkip()`\n * in the catch). Callers that want a slam without the round-scoped side\n * effects (tests, anti-cheat) should use `slamStop()`.\n *\n * Pairs with `skipNudge()` (skip an in-flight `nudge()`) and `slamStop()`\n * (unconditional land-now, no boost). Three distinct actions:\n *\n * - `skipSpin()` lands the in-flight spin and applies the round-scoped\n * boost / auto-slam-refills side effect.\n * - `skipNudge()` fast-forwards an in-flight `nudge()` to its landed\n * position. Spin state is unrelated.\n * - `slamStop()` lands every un-landed reel unconditionally. No boost.\n */\n skipSpin(): void {\n this._spinController.skip();\n }\n\n /**\n * Slam-stop safe before `setResult()` arrives: queues until then.\n * Bypasses the two-stage `skipSpin()` machine. An explicit slam intent.\n *\n * Note on `skipStage`: when this call queues a slam (pre-`setResult`)\n * rather than firing one, `skipStage` stays at `0` until `setResult()`\n * arrives and the queued slam actually runs. If your UI labels the\n * button off `skipStage`, expect a beat of \"Skip\" still shown while\n * the queued intent is in flight; the queued state is not exposed as\n * its own stage on purpose (kept the `0 | 1 | 2` shape stable).\n */\n requestSkip(): void {\n this._spinController.requestSkip();\n }\n\n /**\n * Hard slam-stop. Always lands every un-landed reel immediately.\n * Bypasses the two-stage `skipSpin()` machine and any speed boost.\n * For tests, anti-cheat flows, or any caller with unambiguous\n * \"end now\" intent.\n *\n * Pairs with `skipSpin()` (round-aware land + boost) and `skipNudge()`\n * (fast-forward an in-flight `nudge()`).\n */\n slamStop(): void {\n this._spinController.slamStop();\n }\n\n /**\n * Current `skipSpin()` position within the active round. `0` until the\n * player presses the slam button, `2` after. Read this to drive button\n * labels (e.g. \"Skip\" to \"Skipped\"). `1` is reserved for forward compat\n * and is not currently reachable.\n *\n * `requestSkip()` that gets queued pre-`setResult()` does NOT advance\n * the stage until the queued slam actually fires (i.e. once\n * `setResult()` arrives). If you need a \"queued\" UI state, track that\n * yourself alongside `skipStage`.\n */\n get skipStage(): 0 | 1 | 2 {\n return this._spinController.skipStage;\n }\n\n /**\n * Swap the symbol at a single grid cell in-place, at rest.\n *\n * Caller-facing wrapper over `Reel.setSymbolAt` that ALSO refuses\n * pinned cells (since `Reel` itself can't see the pin map). Use this\n * for live presentation effects. sticky-after-win, mid-feature\n * rewrites. without going through `setResult()`.\n *\n * Throws (in addition to the per-reel guards documented on\n * `Reel.setSymbolAt`) if `(reel, cell)` currently has an active pin.\n * Use `unpin(reel, cell)` first if you intentionally want to overwrite it.\n *\n * @example\n * await reelSet.spin(); // landed\n * reelSet.setSymbolAt(2, 1, 'wild'); // swap centre cell to wild\n */\n setSymbolAt(reel: number, cell: number, symbolId: string): void {\n if (reel < 0 || reel >= this._reels.length) {\n throw new RangeError(`setSymbolAt: reel ${reel} out of range [0, ${this._reels.length})`);\n }\n if (this._pins.has(pinKey(reel, cell))) {\n throw new Error(\n `setSymbolAt: cell (${reel}, ${cell}) has an active pin. Call unpin(reel, cell) ` +\n `first if you intend to overwrite it.`,\n );\n }\n this._reels[reel].setSymbolAt(cell, symbolId);\n }\n\n /**\n * Shift a single reel by `distance` positions after it has landed, revealing\n * caller-supplied symbols.\n *\n * Per-reel by design. multi-reel sync is via `Promise.all([...])` of\n * independent calls. Each call emits its own `nudge:start` / `nudge:complete`\n * pair on the ReelSet bus and `phase:enter('nudge')` / `phase:exit('nudge')`\n * on the per-reel bus.\n *\n * Big-symbol blocks on the target reel are nudged through as a unit as\n * long as they fit on the strip post-rotation. Use case: a 1xH block\n * lands with stubs in bufferEnd; nudge up to reveal it fully.\n *\n * `nudge:start` fires AFTER pre-placement so listeners observe the\n * about-to-tween state, mirroring `nudge:complete` which fires after\n * the strip has snapped. To capture the pre-nudge state, snapshot the\n * grid in your call site before awaiting.\n *\n * Throws (synchronously) if:\n * - the reel set is currently spinning (avoid races with the spin pipeline),\n * - `reel` is out of range,\n * - any visible cell on the target reel has an active pin,\n * - `Reel.nudge` itself rejects (bad distance / direction / incoming /\n * incompatible big-symbol layout).\n *\n * While `nudge()` is in flight, calling `spin()`, `setResult()`, `pin()`,\n * or `setShape()` throws. Await the returned promise before calling any\n * of those methods.\n *\n * Rejects with an `AbortError` if `options.signal` aborts or the reel\n * is destroyed mid-tween. `nudge:cancelled` fires on the bus in that case.\n *\n * @example\n * await reelSet.spin(); // landed\n * await reelSet.nudge(2, { distance: 1, direction: 'forward', incoming: ['wild'] });\n *\n * @example Parallel nudges across two reels:\n * await Promise.all([\n * reelSet.nudge(2, { distance: 1, direction: 'forward', incoming: ['wild'] }),\n * reelSet.nudge(3, { distance: 1, direction: 'forward', incoming: ['wild'] }),\n * ]);\n *\n * @example Staggered parallel via `startDelay`:\n * await Promise.all(\n * [1, 2, 3].map((reel, i) =>\n * reelSet.nudge(reel, { ...opts, startDelay: i * 80 }),\n * ),\n * );\n *\n * @example Abortable nudge:\n * const controller = new AbortController();\n * skipButton.onclick = () => controller.abort();\n * await reelSet.nudge(2, { ...opts, signal: controller.signal })\n * .catch((e) => { if (e.name !== 'AbortError') throw e; });\n */\n async nudge(reel: number, options: NudgeOptions): Promise<{ symbols: string[] }> {\n if (this._spinController.isSpinning) {\n throw new Error('nudge: cannot nudge while a spin or refill is in progress.');\n }\n if (!Number.isInteger(reel) || reel < 0 || reel >= this._reels.length) {\n throw new RangeError(`nudge: reel ${reel} out of range [0, ${this._reels.length}).`);\n }\n if (this._reels[reel].bufferEnd === 0) {\n throw new Error(\n 'nudge: requires bufferEnd >= 1. a downward nudge shifts the bottom ' +\n 'visible symbol through the below-window buffer. This reel set was ' +\n 'built with bufferSymbols({ end: 0 }) for tumble-only use.',\n );\n }\n // Pin overlap detection lives at the ReelSet layer (Reel can't see pins).\n // Nudges would shift symbols out from under a pinned cell visually but\n // leave the pin record stale: fail loudly instead.\n for (const pin of this._pins.values()) {\n if (pin.reel === reel) {\n throw new Error(\n `nudge: reel ${reel} has an active pin at cell ${pin.cell}. ` +\n `Call unpin(${reel}, ${pin.cell}) first if you intend to nudge through it.`,\n );\n }\n }\n\n this._nudgesInFlight++;\n try {\n const result = await this._reels[reel].nudge(options, () => {\n // Fires after Reel.nudge has validated, pre-placed, and snapped:\n // right before the GSAP tween starts. Now the bus event matches\n // observable state.\n this._events.emit('nudge:start', {\n reelIndex: reel,\n distance: options.distance,\n direction: options.direction,\n });\n });\n this._events.emit('nudge:complete', {\n reelIndex: reel,\n distance: options.distance,\n direction: options.direction,\n symbols: result.symbols,\n });\n return result;\n } catch (err) {\n const isAbort = err instanceof Error && err.name === 'AbortError';\n // If the ReelSet was destroyed mid-nudge, `super.destroy({children: true})`\n // has already torn down our event bus (PixiJS Container has its own\n // `_events` field that we shadow: after super.destroy ours is gone too).\n // Skip the emit; the consumer's `nudge()` await will still see the\n // AbortError via the re-throw below.\n if (isAbort && !this._isDestroyed) {\n this._events.emit('nudge:cancelled', {\n reelIndex: reel,\n distance: options.distance,\n direction: options.direction,\n reason: err.message,\n });\n }\n throw err;\n } finally {\n this._nudgesInFlight--;\n }\n }\n\n /**\n * The one gate every caller-supplied grid goes through: shape, v1 option\n * keys, and buffer counts that fit the reels. `context` names the entry\n * point so the throw points at the call the consumer actually made.\n *\n * Shared by `setResult()`, `refill()` and `runCascade()`'s `nextGrid`.\n * Those last two used to skip it entirely, which let a v1 `bufferAbove`\n * reach `columnTargetToStrip`, come back `undefined`, and get silently\n * random-filled on every stage of a cascade.\n */\n private _assertGrid(grid: ColumnTarget[], context: string): void {\n assertColumnTargets(grid, context);\n const columnKeys = V1_OPTION_KEYS['initialFrame() / setResult() column'];\n for (let i = 0; i < grid.length; i++) {\n assertNoV1Keys(grid[i], columnKeys, `${context} column ${i}`);\n }\n assertBufferCountsInRange(\n grid,\n this._reels.map((r) => r.bufferStart),\n this._reels.map((r) => r.bufferEnd),\n context,\n );\n }\n\n private _assertNoNudgeInFlight(method: string): void {\n if (this._nudgesInFlight > 0) {\n throw new Error(\n `ReelSet.${method}: cannot be called while nudge() is in flight. ` +\n `Await the nudge() promise before calling ${method}.`,\n );\n }\n }\n\n /**\n * Fast-forward an in-flight `nudge()` to its landed state. No-op if the\n * given reel is not currently nudging.\n *\n * The tween's `onComplete` fires synchronously, the strip snaps to the\n * final position, and the original `nudge()` promise resolves on the\n * next microtask. `nudge:complete` fires normally. From a listener's\n * POV the nudge just landed fast.\n *\n * Pairs with `skipSpin()` (round-aware spin land + boost) and\n * `slamStop()` (unconditional spin land-now). These three are distinct:\n * spin actions do not affect a nudge in flight, and `skipNudge` does\n * not touch spin state.\n *\n * @param reel Reel index, or `undefined` to skip all in-flight nudges.\n */\n skipNudge(reel?: number): void {\n if (reel === undefined) {\n for (const reel of this._reels) {\n if (reel.isNudging) reel.skipNudge();\n }\n return;\n }\n if (!Number.isInteger(reel) || reel < 0 || reel >= this._reels.length) {\n throw new RangeError(`skipNudge: reel ${reel} out of range [0, ${this._reels.length}).`);\n }\n this._reels[reel].skipNudge();\n }\n\n /**\n * Set the per-reel drop order for the next stop / refill sequence.\n *\n * Convenience wrapper over `setStopDelays()` for common patterns. The\n * stagger step defaults to the active speed profile's stopDelay (or\n * 150 ms if stopDelay is 0).\n *\n * **Sticky.** The override persists indefinitely. until another\n * `setDropOrder()` / `setStopDelays()` call overwrites it (a `null` /\n * absent override falls back to the default `i * speed.stopDelay`\n * stagger). It survives across `spin()` AND `refill()` boundaries by\n * design, because `runCascade(...)` calls `refill()` in a loop and the\n * order set once before the chain must apply to every iteration.\n *\n * The canonical cascade pattern resets it per phase:\n *\n * - `setDropOrder('ltr')` before `spin()`. left-to-right reveal on\n * the initial drop.\n * - `setDropOrder('all')` before `runCascade()`. every refill in the\n * chain drops all columns simultaneously (the commercial-cascade\n * pattern).\n *\n * If you leave the order set between rounds and don't re-set before the\n * next `spin()`, the previous value carries over. Re-set explicitly per\n * round if your rounds use different patterns.\n *\n * Call again with a different value to change it; the previous value\n * is replaced, not stacked.\n *\n * @example\n * reelSet.setDropOrder('ltr'); // left-to-right\n * reelSet.setDropOrder('rtl'); // right-to-left\n * reelSet.setDropOrder('all'); // all columns simultaneously\n * reelSet.setDropOrder([0, 0, 200, 200, 400]); // custom per-reel delays\n * reelSet.setDropOrder(null); // clear the override, restore the default\n */\n setDropOrder(order: 'ltr' | 'rtl' | 'all' | number[] | null, stepMs?: number): void {\n if (order === null) {\n this._spinController.setStopDelays(null);\n return;\n }\n if (Array.isArray(order)) {\n this._spinController.setStopDelays(order);\n return;\n }\n\n const n = this._reels.length;\n const step = stepMs ?? Math.max(this._speedManager.active.stopDelay, 150);\n let delays: number[];\n\n if (order === 'all') {\n delays = new Array(n).fill(0);\n } else if (order === 'ltr') {\n delays = Array.from({ length: n }, (_, i) => i * step);\n } else {\n delays = Array.from({ length: n }, (_, i) => (n - 1 - i) * step);\n }\n\n this._spinController.setStopDelays(delays);\n }\n\n get isSpinning(): boolean {\n return this._spinController.isSpinning;\n }\n\n /** Whether this slot was built with `.multiways(...)`. */\n get isMultiWaysSlot(): boolean {\n return this._isMultiWaysSlot;\n }\n\n // ─── MultiWays API ─────────────────────────────────────────\n\n /**\n * MultiWays: record the cell count each reel should land on this spin. The\n * AdjustPhase between SPIN and STOP will reshape reels (resize symbols,\n * reshape motion) before the stop sequence runs.\n *\n * Must be called between `spin()` and `setResult()`. The shape stays in\n * effect for the current spin only. call again on every spin.\n *\n * Throws if:\n * - this slot was not built with `.multiways(...)`\n * - `cellsPerReel.length !== reelCount`\n * - any entry falls outside `[multiways.minCells, multiways.maxCells]`\n */\n setShape(cellsPerReel: number[]): void {\n this._assertNoNudgeInFlight('setShape');\n if (!this._isMultiWaysSlot) {\n throw new Error('setShape(): slot was not built with .multiways(...). call ReelSetBuilder.multiways() first.');\n }\n if (this._resultSetForCurrentSpin) {\n throw new Error(\n 'setShape(): must be called BEFORE setResult() in the current spin. ' +\n 'Calling setShape after setResult corrupts the cached frames (pins were ' +\n 'overlaid at their pre-migration cells). Reorder: spin() → setShape() → setResult().',\n );\n }\n if (cellsPerReel.length !== this._reels.length) {\n throw new Error(\n `setShape(): cellsPerReel length ${cellsPerReel.length} must equal reelCount ${this._reels.length}.`,\n );\n }\n for (let i = 0; i < cellsPerReel.length; i++) {\n const r = cellsPerReel[i];\n if (r < this._multiwaysMinCells || r > this._multiwaysMaxCells) {\n throw new Error(\n `setShape(): cellsPerReel[${i}] = ${r} out of range [${this._multiwaysMinCells}, ${this._multiwaysMaxCells}].`,\n );\n }\n }\n // Fast-path: if the requested shape matches the current shape per-reel,\n // there's nothing to do. Avoids spurious `shape:changed` events and\n // pointless migration loops in defensive callers that always invoke\n // `setShape` per spin even when the shape didn't actually change.\n let isUnchanged = true;\n for (let i = 0; i < this._reels.length; i++) {\n if (this._reels[i].visibleCells !== cellsPerReel[i]) {\n isUnchanged = false;\n break;\n }\n }\n if (isUnchanged) {\n return;\n }\n\n this._targetShape = [...cellsPerReel];\n this._events.emit('shape:changed', [...cellsPerReel]);\n\n // Migrate pins to their post-reshape cells EAGERLY. before any\n // `setResult` overlay or frame build runs. Otherwise a pin at cell=4\n // on a 7-cell reel is silently dropped when setResult overlays it onto\n // a 3-cell grid (cell 4 is out of bounds for the new shape).\n //\n // AdjustPhase later commits the geometry; the pin map is already at\n // the post-migration cells by then, so AdjustPhase only needs to\n // refresh overlays + tween (when implemented).\n for (let i = 0; i < this._reels.length; i++) {\n this._migratePinsForReel(i, cellsPerReel[i]);\n }\n }\n\n /** Wired internally by SpinController. Consumers do not call this directly. */\n private peekTargetShape(): number[] | null {\n return this._targetShape;\n }\n\n /** Wired internally by SpinController. Consumers do not call this directly. */\n private clearTargetShape(): void {\n this._targetShape = null;\n }\n\n /**\n * Resolved grid, with all OCCUPIED cells (same-reel and cross-reel)\n * replaced by their anchor's symbol id. A 2×2 bonus reads as four\n * `'bonus'` cells.\n *\n * Equivalent to `reelSet.reels.map(r => r.getVisibleSymbols())` because\n * each reel has a cross-reel resolver wired in by ReelSet's constructor.\n * the per-reel surface and the grid surface are the same.\n */\n getVisibleGrid(): string[][] {\n return this._reels.map((r) => r.getVisibleSymbols());\n }\n\n /**\n * The whole board as `ColumnTarget[]` -- buffers included, big-symbol\n * anchors at their true positions, so it can be handed straight back:\n * `reelSet.setResult(reelSet.getTargets())` reproduces what is on screen.\n *\n * `getVisibleGrid()` cannot do that, and its `string[][]` type says so. It\n * reports the visible window only, so a block anchored in `bufferStart`\n * with just its tail showing reads as that id at visible cell 0; replaying\n * that re-anchors the block there and it expands over the cells below.\n * Use `getVisibleGrid()` to read the board for win logic, and this to\n * capture and replay one.\n */\n getTargets(): ColumnTarget[] {\n return this._reels.map((r) => r.getTarget());\n }\n\n /**\n * Footprint of the symbol at `(reel, cell)`.\n *\n * - 1×1 symbols: `{ anchor: { reel, cell }, size: { reels: 1, cells: 1 } }`.\n * - Big symbols: returns the anchor cell and block size.\n * - OCCUPIED cells: resolves transparently to the anchor.\n *\n * Useful for win presenters that need to highlight a whole NxM block.\n */\n getSymbolFootprint(\n reel: number,\n cell: number,\n ): { anchor: { reel: number; cell: number }; size: { reels: number; cells: number } } {\n if (reel < 0 || reel >= this._reels.length) {\n throw new RangeError(`getSymbolFootprint: reel ${reel} out of range [0, ${this._reels.length})`);\n }\n const target = this._reels[reel];\n if (cell < 0 || cell >= target.visibleCells) {\n throw new RangeError(`getSymbolFootprint: cell ${cell} out of range [0, ${target.visibleCells})`);\n }\n\n // Resolve OCCUPIED -> anchor cell on this reel. Cross-reel OCCUPIED\n // requires walking left to find the anchoring reel with size.reels > the\n // distance back to it.\n const anchorCell = target.getAnchorCell(cell);\n const anchorSym = target.getSymbolAt(cell);\n const meta = this._symbolsData[anchorSym.symbolId];\n const size = meta?.size && (meta.size.reels > 1 || meta.size.cells > 1)\n ? meta.size\n : { reels: 1, cells: 1 };\n\n // Resolve cross-reel anchor column: if the anchor symbol on THIS reel\n // is itself an OCCUPIED stub painted by a big symbol on a leftward\n // reel, walk left until we find a column where the cell matches a big\n // symbol whose width covers our column.\n let anchorReelIndex = reel;\n for (let c = reel - 1; c >= 0; c--) {\n const leftReel = this._reels[c];\n if (anchorCell >= leftReel.visibleCells) break;\n const leftAnchorCell = leftReel.getAnchorCell(anchorCell);\n const leftSym = leftReel.getSymbolAt(anchorCell);\n const leftMeta = this._symbolsData[leftSym.symbolId];\n if (leftMeta?.size && leftMeta.size.reels > reel - c) {\n anchorReelIndex = c;\n return {\n anchor: { reel: anchorReelIndex, cell: leftAnchorCell },\n size: leftMeta.size,\n };\n }\n }\n\n return { anchor: { reel: anchorReelIndex, cell: anchorCell }, size };\n }\n\n /**\n * Pixel rectangle covering a big symbol's whole `N×M` block, in\n * ReelSet-local coordinates. Returns the anchor cell's bounds for 1×1\n * symbols. Pass any cell of a block. anchor or non-anchor. and you\n * get the same rect.\n *\n * Useful for win presenters drawing an outline around a whole bonus, or\n * any overlay aligned to the visible footprint of a big symbol:\n *\n * ```ts\n * const rect = reelSet.getBlockBounds(2, 1);\n * gfx.rect(rect.x, rect.y, rect.width, rect.height)\n * .stroke({ color: 0xff6b35, width: 4 });\n * reelSet.addChild(gfx);\n * ```\n *\n * For 1×1 cells this is equivalent to `getCellBounds(reel, cell)`. For\n * big-symbol cells it multiplies width/height by the block size and\n * starts from the anchor cell's bounds.\n */\n getBlockBounds(reel: number, cell: number): CellBounds {\n const fp = this.getSymbolFootprint(reel, cell);\n const anchorReel = this._reels[fp.anchor.reel];\n const axis = anchorReel.axis;\n const slotPitch = anchorReel.motion.slotPitch;\n // `size.reels` spans the CROSS axis and `size.cells` the MAIN axis in\n // every orientation (ADR 016 section 6.7). The screen width and height\n // they map to therefore INVERT under horizontal, even though this\n // method's name and return shape do not move.\n const blockCross =\n fp.size.reels * anchorReel.cellCross + (fp.size.reels - 1) * anchorReel.crossGap;\n const blockMain =\n fp.size.cells * anchorReel.cellMain + (fp.size.cells - 1) * anchorReel.mainGap;\n\n // For anchors that sit in bufferStart (`fp.anchor.cell < 0`), the block\n // extends outside the visible window at the start edge. Coordinates are\n // derived directly from the cell offset (negative values land before\n // visible cell 0). The returned rect is the FULL block footprint,\n // including the clipped-by-mask portion. consumers drawing overlays can\n // intersect with the visible viewport themselves if they need a clipped\n // rect.\n const anchorCellOffset = fp.anchor.cell; // may be negative\n // Same treatment as `getCellBounds`, over the block's full main extent:\n // project its two main-axis edges and take the bounding box. `scaleAt` on\n // the near edge sets the cross extent, which is the widest the block gets.\n // a block spanning several cells tapers, and one rect cannot say \"taper\".\n const curve = anchorReel.curve;\n const flatMain = anchorCellOffset * slotPitch;\n const curvedMain = curve ? curve.mapMain(flatMain) : flatMain;\n const curvedBlockMain = curve\n ? curve.mapMain(flatMain + blockMain) - curvedMain\n : blockMain;\n const crossScale = curve ? curve.scaleAt(flatMain) : 1;\n const origin = axis.toScreen(\n axis.getCross(anchorReel.container) + (blockCross * (1 - crossScale)) / 2,\n anchorReel.mainOffset + curvedMain,\n );\n const size = axis.toScreen(blockCross * crossScale, curvedBlockMain);\n return {\n x: this._viewport.x + origin.x,\n y: this._viewport.y + origin.y,\n width: size.x,\n height: size.y,\n };\n }\n\n // ─── Speed API ────────────────────────────────────────────\n\n /** Speed profile manager. */\n get speed(): SpeedManager {\n return this._speedManager;\n }\n\n /** Change speed and emit event. */\n setSpeed(name: string): void {\n const { previous, current } = this._speedManager.set(name);\n this._events.emit('speed:changed', current, previous);\n // Tell the spin controller this was a user-driven change (not the\n // internal `skip()` boost), so the next `spin()`'s restore path\n // leaves the choice alone even if the name happens to match the\n // value we boosted into.\n this._spinController.notifyManualSpeedChange();\n }\n\n // ─── Spotlight API ────────────────────────────────────────\n\n get spotlight(): SymbolSpotlight {\n return this._spotlight;\n }\n\n // ─── Curve API ────────────────────────────────────────────\n\n /**\n * Re-curve the whole set at runtime, the same way `builder.curve(...)` does\n * at build time. Takes effect immediately on reels at rest and on the next\n * frame for reels in motion.\n *\n * Mostly a tuning affordance: dial the curvature live against the real art\n * instead of rebuilding the set on every guess. Pass `0` to flatten.\n *\n * @param curve one value for every reel, or one entry per reel (length must\n * equal the reel count).\n *\n * @example\n * reelSet.setCurve(0.4);\n * reelSet.setCurve([0.2, 0.35, 0.5, 0.35, 0.2]);\n */\n setCurve(curve: ReelCurveInput | ReelCurveInput[]): void {\n if (Array.isArray(curve) && curve.length !== this._reels.length) {\n throw new Error(\n `setCurve(): per-reel array length (${curve.length}) must equal the reel count (${this._reels.length}).`,\n );\n }\n for (let i = 0; i < this._reels.length; i++) {\n this._reels[i].setCurve(Array.isArray(curve) ? curve[i] : curve);\n }\n }\n\n // ─── Random symbol pools ──────────────────────────────────\n\n /**\n * Control over what the engine may draw when it fills a cell the game\n * did not name: the strip streaming past during a spin, and the buffer\n * cells parked either side of the visible window.\n *\n * ```ts\n * reelSet.randomSymbols.set({ exclude: ['EMPTY'] });\n * reelSet.randomSymbols.set({ weights: { WILD: 40 } }, { reel: 2 });\n * reelSet.randomSymbols.set({ exclude: ['COIN'] }, { slots: 'buffer' });\n * ```\n *\n * Build-time equivalent: `builder.randomSymbols(pool, scope)`.\n */\n get randomSymbols(): RandomSymbolControl {\n return this._frameBuilder.randomProvider;\n }\n\n // ─── Reel access ──────────────────────────────────────────\n\n /** Get all reels. */\n get reels(): readonly Reel[] {\n return this._reels;\n }\n\n /** Get a reel by index. */\n getReel(index: number): Reel {\n return this._reels[index];\n }\n\n /**\n * Returns the bounding box of a visible grid cell in ReelSet-local\n * coordinates (i.e. relative to this Container, before any parent\n * transforms). Row 0 is the top visible cell.\n *\n * Use this to place payline graphics, hit areas, or debug overlays\n * that must align with a specific symbol cell:\n *\n * ```ts\n * const b = reelSet.getCellBounds(2, 1);\n * gfx.rect(b.x, b.y, b.width, b.height).stroke({ color: 0xff6b35 });\n * reelSet.addChild(gfx);\n * ```\n *\n * To convert to stage / global coordinates use PixiJS:\n * ```ts\n * const global = reelSet.toGlobal({ x: b.x, y: b.y });\n * ```\n */\n getCellBounds(reel: number, cell: number): CellBounds {\n if (reel < 0 || reel >= this._reels.length) {\n throw new RangeError(`getCellBounds: reel ${reel} out of range [0, ${this._reels.length})`);\n }\n const target = this._reels[reel];\n if (cell < 0 || cell >= target.visibleCells) {\n throw new RangeError(`getCellBounds: cell ${cell} out of range [0, ${target.visibleCells})`);\n }\n // Project the cell's cross (reel marching) + main (cell along the strip)\n // coordinates to screen. For vertical this is (x = column, y = mainOffset +\n // cell * pitch), unchanged; for horizontal the axes swap.\n const axis = target.axis;\n // A curved reel bends the cell away from where the flat arithmetic puts\n // it and resizes it, so paylines and overlays have to be measured through\n // the same map the symbols were placed with. Flat reels take the `?:`\n // fallbacks and come out at exactly the pre-curve numbers.\n const flatMain = cell * target.motion.slotPitch;\n const origin = axis.toScreen(\n axis.getCross(target.container),\n axis.getMain(target.container) + flatMain,\n );\n const flat = axis.toScreen(target.cellCross, target.cellMain);\n // A projected cell is a TRAPEZOID and `CellBounds` is a rectangle, so the\n // honest answer is the quad's bounding box: the smallest axis-aligned rect\n // that still contains the whole cell. A payline drawn through its centre\n // tracks the curve; an outline drawn on it is a hair loose on the narrow\n // edge, which is the price of the shape not being a rectangle any more.\n const quad = target.curve?.quadFor(flatMain);\n if (!quad) {\n return {\n x: this._viewport.x + origin.x,\n y: this._viewport.y + origin.y,\n width: flat.x,\n height: flat.y,\n };\n }\n const minX = Math.min(quad.x0, quad.x1, quad.x2, quad.x3);\n const maxX = Math.max(quad.x0, quad.x1, quad.x2, quad.x3);\n const minY = Math.min(quad.y0, quad.y1, quad.y2, quad.y3);\n const maxY = Math.max(quad.y0, quad.y1, quad.y2, quad.y3);\n return {\n x: this._viewport.x + origin.x + minX,\n y: this._viewport.y + origin.y + minY,\n width: maxX - minX,\n height: maxY - minY,\n };\n }\n\n /**\n * The four corners of a visible cell as the drum actually draws it, in\n * ReelSet-local pixels, clockwise from top-left. `null` when the reel is\n * flat, in which case {@link ReelSet.getCellBounds} already describes it\n * exactly.\n *\n * `getCellBounds` has to return a rectangle, so on a curved reel it widens\n * to the trapezoid's bounding box. Use this instead to draw anything that\n * should sit ON the curve rather than around it - a cell outline, a payline\n * that follows the bend, a debug overlay.\n *\n * @example\n * const q = reelSet.getCellQuad(2, 0);\n * if (q) gfx.poly(q).stroke({ color: 0xff6b35 });\n */\n getCellQuad(reel: number, cell: number): { x: number; y: number }[] | null {\n if (reel < 0 || reel >= this._reels.length) {\n throw new RangeError(`getCellQuad: reel ${reel} out of range [0, ${this._reels.length})`);\n }\n const target = this._reels[reel];\n if (cell < 0 || cell >= target.visibleCells) {\n throw new RangeError(`getCellQuad: cell ${cell} out of range [0, ${target.visibleCells})`);\n }\n const flatMain = cell * target.motion.slotPitch;\n const quad = target.curve?.quadFor(flatMain);\n if (!quad) return null;\n const axis = target.axis;\n const origin = axis.toScreen(\n axis.getCross(target.container),\n axis.getMain(target.container) + flatMain,\n );\n // Quad corners are view-local; lift them into ReelSet space.\n const ox = this._viewport.x + origin.x;\n const oy = this._viewport.y + origin.y;\n return [\n { x: ox + quad.x0, y: oy + quad.y0 },\n { x: ox + quad.x1, y: oy + quad.y1 },\n { x: ox + quad.x2, y: oy + quad.y2 },\n { x: ox + quad.x3, y: oy + quad.y3 },\n ];\n }\n\n /** Get the viewport. */\n get viewport(): ReelViewport {\n return this._viewport;\n }\n\n // ─── Pins (persistent cell claims) ────────────────────────\n //\n // A `CellPin` claims a grid cell: the strip cannot overwrite it, and\n // `setResult()` overlays the pin's symbolId at that cell before the\n // stop sequence runs. Pins persist across spins according to their\n // `turns` field. See `CellPin` for the full semantics.\n\n /**\n * Pin a symbol to a grid cell. Applied immediately if the reel is idle;\n * applied at the next `setResult()` otherwise. Fires `pin:placed`.\n *\n * Passing the same `(reel, cell)` replaces the previous pin. The old one\n * is replaced silently (no `pin:expired` fires for replacement).\n *\n * Negative cells are rejected. Place buffer-cell anchors via `setResult()`\n * with `bufferStart` / `bufferEnd` on the column's `ColumnTarget`.\n *\n * @example\n * // Sticky wild for 3 spins\n * reelSet.pin(2, 1, 'wild', { turns: 3 })\n *\n * // Hold & Win coin with a payout value\n * reelSet.pin(reel, cell, 'coin', { turns: 'permanent', payload: { value: 50 } })\n *\n * // Expanding wild: fill column for the current spin's evaluation only\n * for (let r = 0; r < 3; r++) reelSet.pin(2, r, 'wild', { turns: 'eval' })\n */\n pin(reel: number, cell: number, symbolId: string, options?: CellPinOptions): CellPin {\n this._assertNoNudgeInFlight('pin');\n if (reel < 0 || reel >= this._reels.length) {\n throw new Error(`pin(): reel ${reel} out of range [0, ${this._reels.length})`);\n }\n const target = this._reels[reel];\n if (cell < 0 || cell >= target.visibleCells) {\n throw new Error(`pin(): cell ${cell} out of range [0, ${target.visibleCells})`);\n }\n\n const pin: CellPin = {\n reel,\n cell,\n originCell: options?.originCell ?? cell,\n migration: options?.migration ?? 'origin',\n symbolId,\n turns: options?.turns ?? 'permanent',\n payload: options?.payload,\n };\n\n const key = pinKey(reel, cell);\n // If we're replacing an existing pin, drop its overlay so a fresh one\n // with the new symbolId can be created.\n if (this._pins.has(key)) {\n this._destroyPinOverlay(key);\n }\n this._pins.set(key, pin);\n\n if (!this._spinController.isSpinning) {\n // Reel is idle: apply the pin visually on the reel itself so\n // `getVisibleSymbols()` matches what `pins` reports.\n this._applyPinVisually(reel, cell, symbolId);\n } else {\n // Mid-spin: create an overlay so the pinned symbol is visible\n // immediately even while the reel scrolls.\n this._ensurePinOverlay(pin);\n }\n\n this._events.emit('pin:placed', pin);\n return pin;\n }\n\n /**\n * Remove a pin at `(reel, cell)`. If no pin exists at that cell, this is a\n * no-op. Fires `pin:expired` with reason `'explicit'`.\n */\n unpin(reel: number, cell: number): void {\n const key = pinKey(reel, cell);\n const pin = this._pins.get(key);\n if (!pin) return;\n this._pins.delete(key);\n this._destroyPinOverlay(key);\n this._events.emit('pin:expired', pin, 'explicit');\n }\n\n /**\n * All active pins, keyed by `\"reel:cell\"`.\n *\n * Reads are safe at any time. during a spin the map reflects pins that\n * will apply to the NEXT `setResult()`, not the one already in flight.\n */\n get pins(): ReadonlyMap<string, CellPin> {\n return this._pins;\n }\n\n /** Convenience: get the pin at `(reel, cell)` or `undefined`. */\n getPin(reel: number, cell: number): CellPin | undefined {\n return this._pins.get(pinKey(reel, cell));\n }\n\n /**\n * Move an existing pin from one cell to another. Animates a flight symbol\n * between the two cells, updates pin state atomically, and resolves when\n * the animation completes.\n *\n * This is the engine-native replacement for ghost sprites in walking-wild\n * recipes. The flight symbol is a pooled `ReelSymbol` acquired from the\n * factory, parented briefly to the viewport's `unmaskedContainer` so it\n * can travel across reel boundaries without being clipped.\n *\n * Constraints:\n * - Only callable at rest (throws if `isSpinning === true`).\n * - `to` must be within the grid; no pin may already exist there.\n * - Calling with `from === to` is a no-op that still fires `pin:moved`.\n *\n * @example\n * // Walking wild. move the pinned wild one column left each spin\n * reelSet.events.on('spin:complete', async () => {\n * for (const pin of [...reelSet.pins.values()]) {\n * if (pin.reel > 0) {\n * await reelSet.movePin(\n * { reel: pin.reel, cell: pin.cell },\n * { reel: pin.reel - 1, cell: pin.cell },\n * );\n * } else {\n * reelSet.unpin(pin.reel, pin.cell);\n * }\n * }\n * });\n */\n async movePin(\n from: CellCoord,\n to: CellCoord,\n opts?: MovePinOptions,\n ): Promise<void> {\n if (this._spinController.isSpinning) {\n throw new Error('movePin(): cannot move pin while spinning');\n }\n\n const fromKey = pinKey(from.reel, from.cell);\n const pin = this._pins.get(fromKey);\n if (!pin) {\n throw new Error(\n `movePin(): no pin at (${from.reel}, ${from.cell})`,\n );\n }\n\n // Validate `to` bounds (same rules as pin()).\n if (to.reel < 0 || to.reel >= this._reels.length) {\n throw new Error(\n `movePin(): to reel ${to.reel} out of range [0, ${this._reels.length})`,\n );\n }\n const toReel = this._reels[to.reel];\n if (to.cell < 0 || to.cell >= toReel.visibleCells) {\n throw new Error(\n `movePin(): to cell ${to.cell} out of range [0, ${toReel.visibleCells})`,\n );\n }\n\n // No-op self-move: still fire the event so callers can treat it uniformly.\n if (from.reel === to.reel && from.cell === to.cell) {\n this._events.emit('pin:moved', pin, { reel: from.reel, cell: from.cell });\n return;\n }\n\n const toKey = pinKey(to.reel, to.cell);\n if (this._pins.has(toKey)) {\n throw new Error(\n `movePin(): a pin already exists at (${to.reel}, ${to.cell})`,\n );\n }\n\n // Update pin state first (atomic). The map now reflects the new position\n // immediately. any subsequent spin sees the pin at `to`.\n this._pins.delete(fromKey);\n const movedPin: CellPin = { ...pin, reel: to.reel, cell: to.cell, originCell: to.cell };\n this._pins.set(toKey, movedPin);\n\n // An overlay at the old cell (from a prior spin-interrupted state)\n // is no longer accurate. drop it; the flight symbol takes over.\n this._destroyPinOverlay(fromKey);\n\n // Gather viewport-local coordinates for both cells. The flight symbol\n // will be parented to `viewport.unmaskedContainer`, whose local space\n // matches `maskedContainer` (both sit at (0,0) inside viewport). so\n // `reel.container.x + symbol.view.x/y` gives us the right offset.\n const fromReel = this._reels[from.reel];\n // Viewport-space cell position from the single source of truth\n // (`_pinOverlayCellMain`), so the flight symbol - parented to\n // unmaskedContainer, i.e. viewport space - lands on the right cell for any\n // nonzero container offset and regardless of the source cell's mask state.\n // Reading `getSymbolAt(cell).view` directly would mis-place it: that is\n // reel-local for masked symbols but container-baked for unmasked ones.\n //\n // Both ends go through the axis. `_pinOverlayCellMain` returns a TRAVEL-axis\n // coordinate, which is `x` on a horizontal set, so assigning it to `.y`\n // (and the reel's main offset to `.x`) sent the flight diagonally to a\n // meaningless spot. Silent: both are numbers.\n const fromPoint = fromReel.axis.toScreen(\n fromReel.axis.getCross(fromReel.container),\n this._pinOverlayCellMain(fromReel, from.cell, fromReel.motion.slotPitch),\n );\n const toPoint = toReel.axis.toScreen(\n toReel.axis.getCross(toReel.container),\n this._pinOverlayCellMain(toReel, to.cell, toReel.motion.slotPitch),\n );\n\n // Backfill the vacated cell with a filler. Takes effect immediately.\n // the vacated cell visually swaps to the backfill while the flight\n // symbol is still in motion.\n const backfill =\n opts?.backfill ?? this._frameBuilder.randomProvider.next('spinning', from.reel);\n const fromVisible = fromReel.getVisibleSymbols();\n fromVisible[from.cell] = backfill;\n fromReel.placeSymbols({ visible: fromVisible });\n\n // Spawn the flight symbol on the unmasked container so it renders above\n // the reels and can cross column boundaries.\n const flight = this._symbolFactory.acquire(pin.symbolId);\n flight.resize(fromReel.symbolWidth, fromReel.symbolHeight);\n flight.view.x = fromPoint.x;\n flight.view.y = fromPoint.y;\n this._viewport.unmaskedContainer.addChild(flight.view);\n\n // onFlightCreated hook. fires after the flight symbol is in place but\n // before the tween begins. This is where consumers switch a Spine\n // symbol onto a `run` animation for the flight duration.\n //\n // A throw from the hook MUST NOT abort the move: the pin map is\n // already updated and the tween needs to run for the flight symbol\n // to reach its destination. leaking a flight symbol on the unmasked\n // container is worse than a noisy console.error. Log so the bug is\n // diagnosable instead of silently eaten.\n try {\n opts?.onFlightCreated?.(flight);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[pixi-reels] movePin onFlightCreated hook threw. continuing the flight to avoid leaking the flight symbol:', err);\n }\n\n const duration = (opts?.duration ?? 400) / 1000;\n const easing = opts?.easing ?? 'power2.inOut';\n await new Promise<void>((resolve) => {\n this._reels[0].gsap.to(flight.view, {\n x: toPoint.x,\n y: toPoint.y,\n duration,\n ease: easing,\n onComplete: () => resolve(),\n });\n });\n\n // onFlightCompleted hook. fires before releasing the flight symbol,\n // so consumers can return a Spine to `idle` or play a landing animation.\n //\n // A throw from the hook MUST NOT prevent the rest of the cleanup\n // (apply the pin at destination, release the flight symbol to the\n // pool). otherwise we leak a flight symbol AND leave the pin map\n // out of sync with the reels. Log so the bug is diagnosable.\n try {\n opts?.onFlightCompleted?.(flight);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[pixi-reels] movePin onFlightCompleted hook threw. continuing cleanup:', err);\n }\n\n // Apply the pin visually at the destination cell.\n const toVisible = toReel.getVisibleSymbols();\n toVisible[to.cell] = pin.symbolId;\n toReel.placeSymbols({ visible: toVisible });\n\n this._viewport.unmaskedContainer.removeChild(flight.view);\n this._symbolFactory.release(flight);\n\n this._events.emit('pin:moved', movedPin, {\n reel: from.reel,\n cell: from.cell,\n });\n }\n\n // ─── Frame pipeline (strip generation) ────────────────────\n //\n // Exposes the runtime-mutable FrameBuilder middleware pipeline on ReelSet\n // so recipes can add/remove frame middleware after build. the entry\n // point for mode-specific strip changes (feature weights, mystery\n // injection, positional overrides) without a full rebuild.\n //\n // The internal machinery was already present on FrameBuilder; this is\n // pure exposure. no behaviour change for recipes that don't call it.\n\n /**\n * Runtime-mutable middleware pipeline for symbol-frame generation.\n *\n * @example\n * // Feature entry. swap to a middleware that injects more wilds\n * reelSet.frame.use(moreWildsMiddleware);\n *\n * // Feature exit\n * reelSet.frame.remove('more-wilds');\n */\n get frame(): FrameAPI {\n return this._frameAPI;\n }\n\n // ─── Lifecycle ────────────────────────────────────────────\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._isDestroyed = true;\n\n this._spotlight.destroy();\n this._spinController.destroy();\n\n for (const reel of this._reels) {\n reel.destroy();\n }\n\n this._destroyAllPinOverlays();\n this._symbolFactory.destroy();\n this._viewport.destroy();\n this._pins.clear();\n this._events.emit('destroyed');\n this._events.removeAllListeners();\n\n super.destroy({ children: true });\n }\n\n // ─── Pin internals ────────────────────────────────────────\n\n /**\n * Overlay active pins onto `symbols`. Mutates the input in place; call\n * `_cloneTargets` first if the caller needs to keep the original\n * unmodified.\n */\n private _applyPinsToGrid(symbols: ColumnTarget[]): ColumnTarget[] {\n for (const pin of this._pins.values()) {\n const reel = symbols[pin.reel];\n if (!reel) continue;\n if (pin.cell < reel.visible.length) {\n reel.visible[pin.cell] = pin.symbolId;\n }\n }\n return symbols;\n }\n\n /**\n * Deep clone a `ColumnTarget[]` so the caller can mutate the result\n * without touching the original. Inner arrays are spread (one level deep);\n * cells are strings, so further depth is not needed.\n */\n private _cloneTargets(grid: ColumnTarget[]): ColumnTarget[] {\n return grid.map(cloneColumnTarget);\n }\n\n /** Pins on a given reel, in cell order. Used by AdjustPhase migration. */\n private _pinsOnReel(reelIndex: number): CellPin[] {\n const result: CellPin[] = [];\n for (const pin of this._pins.values()) {\n if (pin.reel === reelIndex) result.push(pin);\n }\n return result;\n }\n\n /**\n * MultiWays: relocate pins on a reel for a new visible-cell count. The new\n * cell is computed as `min(originCell, newCells - 1)`. clamped only when\n * the origin no longer fits. Returns the migrated pins so AdjustPhase\n * can build tween descriptors. Mutates the pins map in place.\n */\n private _migratePinsForReel(reelIndex: number, newCells: number): {\n pin: CellPin;\n fromCell: number;\n toCell: number;\n clamped: boolean;\n }[] {\n const migrations: {\n pin: CellPin;\n fromCell: number;\n toCell: number;\n clamped: boolean;\n }[] = [];\n\n // Process top-to-bottom so collision resolution is deterministic: the\n // first (topmost) pin to claim a clamped cell keeps it.\n const reelPins = this._pinsOnReel(reelIndex).sort((a, b) => a.cell - b.cell);\n\n // Rows that will be occupied after migration. Seed with pins that stay put\n // (a mover clamped onto one of these collides and is expired).\n const occupied = new Set<number>();\n const movers: {\n pin: CellPin;\n fromCell: number;\n target: number;\n clamped: boolean;\n nextOriginCell: number;\n }[] = [];\n\n for (const pin of reelPins) {\n const fromCell = pin.cell;\n\n // Compute target cell based on migration policy.\n // 'origin' → clamp to min(originCell, newCells - 1). Restores on grow.\n // 'frozen' → stay at current cell if it fits, else clamp to last\n // visible cell AND update originCell so future grows\n // don't restore. \"Lock at current position\" semantics.\n let target: number;\n let clamped: boolean;\n let nextOriginCell = pin.originCell;\n if (pin.migration === 'frozen') {\n if (fromCell < newCells) {\n target = fromCell;\n clamped = false;\n } else {\n target = newCells - 1;\n clamped = true;\n nextOriginCell = target; // freeze the new cell as the new \"origin\"\n }\n } else {\n // 'origin' (default)\n target = Math.min(pin.originCell, newCells - 1);\n clamped = target !== pin.originCell;\n }\n\n if (target === fromCell && nextOriginCell === pin.originCell) {\n occupied.add(fromCell); // stays put; claims its cell\n continue;\n }\n movers.push({ pin, fromCell, target, clamped, nextOriginCell });\n }\n\n for (const { pin, fromCell, target, clamped, nextOriginCell } of movers) {\n const fromKey = pinKey(pin.reel, fromCell);\n\n if (occupied.has(target)) {\n // Target cell already taken (by a stayer or an earlier mover). Expire\n // this pin deterministically instead of overwriting the other pin in\n // `_pins` and orphaning its overlay.\n this._pins.delete(fromKey);\n this._destroyPinOverlay(fromKey);\n this._events.emit('pin:expired', pin, 'collision');\n continue;\n }\n occupied.add(target);\n\n const toKey = pinKey(pin.reel, target);\n this._pins.delete(fromKey);\n const moved: CellPin = { ...pin, cell: target, originCell: nextOriginCell };\n this._pins.set(toKey, moved);\n\n // Keep overlay map keyed by the new cell.\n const overlayEntry = this._pinOverlays.get(fromKey);\n if (overlayEntry) {\n this._pinOverlays.delete(fromKey);\n this._pinOverlays.set(toKey, { pin: moved, overlay: overlayEntry.overlay });\n }\n\n migrations.push({ pin: moved, fromCell, toCell: target, clamped });\n this._events.emit('pin:migrated', moved, {\n fromCell,\n toCell: target,\n clamped,\n reelIndex,\n });\n }\n return migrations;\n }\n\n /**\n * The MAIN-axis (travel-axis) coordinate of a pin overlay sitting on cell\n * `cell` of `reel`, given the cell pitch `slotPitch`. This is `y` on a\n * vertical set and `x` on a horizontal one -- callers must route it through\n * `axis.setMain` / `axis.toScreen`, never assign it to `.y` directly.\n * Single source of truth for overlay placement;\n * `axis.getMain(getSymbolAt(cell).view)` equals `cell * slotPitch` for a snapped reel\n * (ReelMotion lays symbols at that pitch), so all overlay sites agree.\n */\n private _pinOverlayCellMain(reel: Reel, cell: number, slotPitch: number): number {\n return reel.axis.getMain(reel.container) + cell * slotPitch;\n }\n\n /**\n * Apply a pin to the idle reel's visible display immediately. Used when\n * `pin()` is called while no spin is in flight. the grid updates right\n * away so `getVisibleSymbols()` reflects the pin.\n */\n private _applyPinVisually(reel: number, cell: number, symbolId: string): void {\n const target = this._reels[reel];\n const current = target.getVisibleSymbols();\n if (current[cell] === symbolId) return; // already there\n current[cell] = symbolId;\n target.placeSymbols({ visible: current });\n }\n\n /**\n * Fires on `spin:allLanded`. Destroys visual pin overlays (the actual reel\n * cells now show the pinned symbols via setResult overlay), then\n * decrements numeric-turns pins and expires pins that hit zero.\n */\n private _onSpinLanded(): void {\n // Overlays are only needed during spin motion. destroy them all.\n this._destroyAllPinOverlays();\n\n if (this._pins.size === 0) return;\n\n const expired: CellPin[] = [];\n for (const pin of this._pins.values()) {\n if (typeof pin.turns === 'number') {\n // turns is readonly on the public interface; the engine owns the\n // mutation here. cast to the mutable internal representation.\n (pin as { turns: number }).turns -= 1;\n if (pin.turns <= 0) expired.push(pin);\n }\n }\n\n for (const pin of expired) {\n this._pins.delete(pinKey(pin.reel, pin.cell));\n this._events.emit('pin:expired', pin, 'turns' as PinExpireReason);\n }\n }\n\n /**\n * Fires on `spin:start`. Clears every `'eval'` pin from the previous spin,\n * then creates a visual overlay for every remaining pin so its symbol\n * stays visible while the reel scrolls underneath.\n */\n private _onSpinStart(): void {\n // Fresh spin. setResult hasn't been called yet, so setShape() is\n // allowed again until setResult() flips this back.\n this._resultSetForCurrentSpin = false;\n\n if (this._pins.size > 0) {\n const expired: CellPin[] = [];\n for (const pin of this._pins.values()) {\n if (pin.turns === 'eval') expired.push(pin);\n }\n\n for (const pin of expired) {\n this._pins.delete(pinKey(pin.reel, pin.cell));\n this._events.emit('pin:expired', pin, 'eval' as PinExpireReason);\n }\n }\n\n // Create overlays for all remaining pins. The overlay is what the player\n // sees during the spin motion phase. the underlying reel cell scrolls\n // normally but is visually covered.\n for (const pin of this._pins.values()) {\n this._ensurePinOverlay(pin);\n }\n }\n\n /**\n * Create an overlay ReelSymbol for a pin in the viewport's unmasked\n * container. No-op if one already exists at that cell. Fires\n * `pin:overlayCreated` after the overlay is positioned and added to the\n * display list. that's the hook consumers use to drive animation state\n * (e.g. setting a Spine track).\n */\n private _ensurePinOverlay(pin: CellPin): void {\n const key = pinKey(pin.reel, pin.cell);\n if (this._pinOverlays.has(key)) return;\n\n const reel = this._reels[pin.reel];\n const overlay = this._symbolFactory.acquire(pin.symbolId);\n overlay.resize(reel.symbolWidth, reel.symbolHeight);\n // Viewport.unmaskedContainer sits at (0,0) inside the viewport. same\n // local space as maskedContainer. The reel's cross coordinate lives on\n // its container; the symbol view's main coordinate is reel-local, and\n // jagged layouts add the reel's mainOffset so overlays line up with the\n // actual cell.\n reel.axis.setCross(overlay.view, reel.axis.getCross(reel.container));\n reel.axis.setMain(\n overlay.view,\n this._pinOverlayCellMain(reel, pin.cell, reel.motion.slotPitch),\n );\n overlay.view.zIndex = ReelSet.PIN_OVERLAY_Z_INDEX;\n this._viewport.unmaskedContainer.addChild(overlay.view);\n this._pinOverlays.set(key, { pin, overlay });\n this._events.emit('pin:overlayCreated', pin, overlay);\n }\n\n /**\n * Reposition + resize every pin overlay on the given reel.\n *\n * The engine calls this automatically after every MultiWays AdjustPhase\n * reshape (and from the skip path), so applications that just use\n * `setShape()` / `setResult()` never need to invoke it. **Call it\n * yourself only if** you mutate `Reel.symbolWidth`, `Reel.symbolHeight`,\n * or a pin's cell outside the normal MultiWays flow. e.g. a custom\n * mid-spin layout swap that bypasses `AdjustPhase`.\n *\n * No-op for reels with no active pin overlays.\n */\n refreshPinOverlaysForReel(reelIndex: number): void {\n const reel = this._reels[reelIndex];\n for (const [, entry] of this._pinOverlays) {\n if (entry.pin.reel !== reelIndex) continue;\n const { pin, overlay } = entry;\n overlay.resize(reel.symbolWidth, reel.symbolHeight);\n reel.axis.setCross(overlay.view, reel.axis.getCross(reel.container));\n reel.axis.setMain(\n overlay.view,\n this._pinOverlayCellMain(reel, pin.cell, reel.motion.slotPitch),\n );\n }\n }\n\n /**\n * Internal: build AdjustPhase pin-overlay tween descriptors for a reel.\n * Captures the overlays' CURRENT on-screen main coordinate + size as the tween's\n * `from` state, then computes the post-reshape `to` state from the\n * pin's already-migrated cell + the upcoming cell height. Called BEFORE\n * AdjustPhase commits the reshape, so the snapshot reflects what the\n * player actually sees.\n */\n private _buildPinOverlayTweens(\n reelIndex: number,\n targetCellMain: number,\n ): import('../spin/phases/AdjustPhase.js').PinOverlayTween[] {\n const reel = this._reels[reelIndex];\n const out: import('../spin/phases/AdjustPhase.js').PinOverlayTween[] = [];\n // The reel's OWN main gap, not symbolGap.y: under horizontal the strip\n // is spaced by the X gap (ADR 016 section 6.6).\n const newSlot = targetCellMain + reel.mainGap;\n for (const [, entry] of this._pinOverlays) {\n if (entry.pin.reel !== reelIndex) continue;\n const { pin, overlay } = entry;\n out.push({\n symbol: overlay,\n cellCross: reel.cellCross,\n oldCellMain: reel.cellMain,\n newCellMain: targetCellMain,\n fromMain: reel.axis.getMain(overlay.view),\n toMain: this._pinOverlayCellMain(reel, pin.cell, newSlot),\n cross: reel.axis.getCross(reel.container),\n });\n }\n return out;\n }\n\n /**\n * Destroy a single pin's overlay, if present. Fires\n * `pin:overlayDestroyed` BEFORE the overlay is released to the pool, so\n * consumers can stop animations / remove listeners on a still-valid\n * instance.\n */\n private _destroyPinOverlay(key: string): void {\n const entry = this._pinOverlays.get(key);\n if (!entry) return;\n const { pin, overlay } = entry;\n this._events.emit('pin:overlayDestroyed', pin, overlay);\n this._viewport.unmaskedContainer.removeChild(overlay.view);\n this._symbolFactory.release(overlay);\n this._pinOverlays.delete(key);\n }\n\n /** Destroy every active pin overlay. Called on spin land and on destroy. */\n private _destroyAllPinOverlays(): void {\n const keys = [...this._pinOverlays.keys()];\n for (const key of keys) this._destroyPinOverlay(key);\n }\n}\n","/** Default values applied when not explicitly set by the builder. */\nexport const DEFAULTS = {\n bufferSymbols: 1 as number,\n symbolGap: { x: 0 as number, y: 0 as number },\n initialSpeed: 'normal' as string,\n maxPoolPerKey: 20 as number,\n zIndexStep: 100 as number,\n};\n","import type { SpeedProfile } from './types.js';\n\n/**\n * Built-in speed profiles covering common slot game needs.\n *\n * Bounce values are tuned for the typical 120–200px symbol range. For larger\n * or smaller symbols, register a custom profile or override `bounceDistance`.\n * `bounceDuration` is the total time for the two-leg bounce (down then back).\n */\nexport const SpeedPresets = {\n NORMAL: {\n name: 'normal',\n spinDelay: 100,\n spinSpeed: 30,\n stopDelay: 140,\n anticipationDelay: 450,\n bounceDistance: 56,\n bounceDuration: 600,\n accelerationEase: 'power2.in',\n decelerationEase: 'power2.out',\n accelerationDuration: 300,\n minimumSpinTime: 500,\n },\n TURBO: {\n name: 'turbo',\n spinDelay: 30,\n spinSpeed: 50,\n stopDelay: 0,\n anticipationDelay: 250,\n bounceDistance: 42,\n bounceDuration: 200,\n accelerationEase: 'power2.in',\n decelerationEase: 'power2.out',\n accelerationDuration: 200,\n minimumSpinTime: 300,\n },\n SUPER_TURBO: {\n name: 'superTurbo',\n spinDelay: 0,\n spinSpeed: 80,\n stopDelay: 0,\n anticipationDelay: 0,\n bounceDistance: 14,\n bounceDuration: 120,\n accelerationEase: 'power1.in',\n decelerationEase: 'power1.out',\n accelerationDuration: 50,\n minimumSpinTime: 100,\n },\n} as const satisfies Record<string, SpeedProfile>;\n","import type { ReelSymbol } from './ReelSymbol.js';\n\ntype SymbolConstructor<T extends ReelSymbol = ReelSymbol> = new (options: any) => T;\n\ninterface RegistryEntry {\n SymbolClass: SymbolConstructor;\n options: any;\n}\n\n/**\n * Registry that maps symbolIds to their constructors and options.\n *\n * Used by the builder and SymbolFactory to create symbols on demand.\n */\nexport class SymbolRegistry {\n private _entries = new Map<string, RegistryEntry>();\n\n /**\n * Register a symbol type.\n *\n * ```ts\n * registry.register('cherry', SpriteSymbol, { textures: { cherry: tex } });\n * ```\n */\n register<T extends ReelSymbol>(\n symbolId: string,\n SymbolClass: new (options: any) => T,\n options: T extends { constructor: (options: infer O) => any } ? O : any,\n ): void {\n if (this._entries.has(symbolId)) {\n throw new Error(`Symbol '${symbolId}' is already registered.`);\n }\n this._entries.set(symbolId, { SymbolClass, options });\n }\n\n /** Create a new symbol instance for the given symbolId. */\n create(symbolId: string): ReelSymbol {\n const entry = this._entries.get(symbolId);\n if (!entry) {\n throw new Error(\n `Symbol '${symbolId}' is not registered. Available: ${[...this._entries.keys()].join(', ')}`,\n );\n }\n const symbol = new entry.SymbolClass(entry.options);\n symbol.activate(symbolId);\n return symbol;\n }\n\n has(symbolId: string): boolean {\n return this._entries.has(symbolId);\n }\n\n get symbolIds(): string[] {\n return [...this._entries.keys()];\n }\n\n get size(): number {\n return this._entries.size;\n }\n}\n","import type { Disposable } from '../utils/Disposable.js';\n\n/**\n * Generic object pool for reusing expensive-to-create objects.\n *\n * Reduces GC pressure by recycling objects instead of creating/destroying them each frame.\n * Used internally for ReelSymbol instances and available to game code for trails, particles, etc.\n *\n * @typeParam T - The type of object to pool.\n */\nexport class ObjectPool<T> implements Disposable {\n private _pools = new Map<string, T[]>();\n /** Mirror of every item currently held in a pool, for O(1) double-release detection. */\n private _pooled = new Set<T>();\n private _isDestroyed = false;\n\n constructor(\n private _factory: (key: string) => T,\n private _reset?: (item: T) => void,\n private _dispose?: (item: T) => void,\n private _maxPerKey: number = 20,\n ) {}\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * Get an object from the pool, or create a new one if the pool is empty.\n */\n acquire(key: string): T {\n if (this._isDestroyed) {\n throw new Error(\n `ObjectPool.acquire('${key}') called after destroy(). A ticker or promise ` +\n `callback is still running past teardown; cancel it before destroying the pool.`,\n );\n }\n const pool = this._pools.get(key);\n if (pool && pool.length > 0) {\n const item = pool.pop()!;\n this._pooled.delete(item);\n this._reset?.(item);\n return item;\n }\n return this._factory(key);\n }\n\n /**\n * Return an object to the pool for reuse.\n * If the pool is at capacity, the object is disposed instead.\n */\n release(key: string, item: T): void {\n // Dropping a release after destroy() avoids resurrecting (and leaking) the pool.\n if (this._isDestroyed) return;\n // Guard against double-release: pooling the same instance twice would hand it\n // to two different cells on the next two acquire() calls (silent aliasing).\n if (this._pooled.has(item)) return;\n\n let pool = this._pools.get(key);\n if (!pool) {\n pool = [];\n this._pools.set(key, pool);\n }\n if (pool.length >= this._maxPerKey) {\n this._dispose?.(item);\n return;\n }\n pool.push(item);\n this._pooled.add(item);\n }\n\n /** Get the number of pooled items for a key. */\n size(key: string): number {\n return this._pools.get(key)?.length ?? 0;\n }\n\n /** Get total pooled items across all keys. */\n get totalSize(): number {\n let total = 0;\n for (const pool of this._pools.values()) {\n total += pool.length;\n }\n return total;\n }\n\n /** Clear all pooled items, calling dispose on each. */\n clear(): void {\n if (this._dispose) {\n for (const pool of this._pools.values()) {\n for (const item of pool) {\n this._dispose(item);\n }\n }\n }\n this._pools.clear();\n this._pooled.clear();\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this.clear();\n this._isDestroyed = true;\n }\n}\n","import type { ReelSymbol } from './ReelSymbol.js';\nimport type { SymbolRegistry } from './SymbolRegistry.js';\nimport { ObjectPool } from '../pool/ObjectPool.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * Creates and pools ReelSymbol instances.\n *\n * Wraps SymbolRegistry for creation and ObjectPool for recycling.\n * Game code should not need to interact with this directly.\n * it's managed by Reel internally.\n */\nexport class SymbolFactory {\n private _pool: ObjectPool<ReelSymbol>;\n private _capacityPerKey: number;\n\n constructor(\n private _registry: SymbolRegistry,\n maxPoolPerKey: number = 20,\n gsap: Gsap = DEFAULT_GSAP,\n mainAxis: 'x' | 'y' = 'y',\n ) {\n this._capacityPerKey = maxPoolPerKey;\n this._pool = new ObjectPool<ReelSymbol>(\n // Bind at CREATE, not acquire: the factory belongs to one reel set for\n // its whole life, and a pooled symbol never crosses sets.\n (key: string) => {\n const symbol = this._registry.create(key);\n symbol.bindGsap(gsap);\n symbol.bindMainAxis(mainAxis);\n return symbol;\n },\n (item: ReelSymbol) => item.reset(),\n (item: ReelSymbol) => item.destroy(),\n maxPoolPerKey,\n );\n }\n\n /** Max recycled instances kept per symbol id before overflow is destroyed. */\n get capacityPerKey(): number {\n return this._capacityPerKey;\n }\n\n /** Get a symbol (from pool or newly created), activated with symbolId. */\n acquire(symbolId: string): ReelSymbol {\n const symbol = this._pool.acquire(symbolId);\n if (symbol.symbolId !== symbolId) {\n symbol.activate(symbolId);\n }\n return symbol;\n }\n\n /** Return a symbol to the pool. */\n release(symbol: ReelSymbol): void {\n const id = symbol.symbolId;\n symbol.deactivate();\n this._pool.release(id, symbol);\n }\n\n destroy(): void {\n this._pool.destroy();\n }\n}\n","import type { SymbolData } from '../config/types.js';\nimport type {\n RandomSymbolControl,\n SymbolPool,\n SymbolPoolScope,\n SymbolPoolSlots,\n} from './SymbolPool.js';\n\n/**\n * Which slot the engine is filling. `'buffer'` is a LAYER, never a slot: a\n * real cell is always on one side or the other.\n */\nexport type DrawSlot = 'spinning' | 'bufferStart' | 'bufferEnd';\n\n/** A compiled draw table: ids with weight > 0 plus their cumulative weights. */\ninterface DrawTable {\n ids: string[];\n cumulative: number[];\n total: number;\n}\n\n/** Global layers use this in place of a reel index. */\nconst ALL_REELS = '*';\n\n/**\n * Weighted random symbol selector using binary search on cumulative weights.\n *\n * On top of the registered weights it carries layers of `SymbolPool`\n * overrides. global and per-reel, for the spinning strip, for both buffer\n * ends at once, and for either end on its own. See `SymbolPoolScope` for\n * how they resolve.\n */\nexport class RandomSymbolProvider implements RandomSymbolControl {\n private _symbols: string[];\n private _baseWeights: Record<string, number> = {};\n /** Installed pools, keyed by `${slots}:${reel}`. */\n private _pools = new Map<string, SymbolPool>();\n /** Compiled tables, same key shape. Dropped wholesale on any mutation. */\n private _tables = new Map<string, DrawTable>();\n private _rng: () => number;\n\n /**\n * @param symbolsData - Symbol id → weight/data map.\n * @param rng - Source of randomness returning a value in [0, 1). Defaults to\n * `Math.random`. Regulated / provably-fair deployments must inject a\n * seeded, audited PRNG so the on-screen strip can be replayed from a seed.\n */\n constructor(symbolsData: Record<string, SymbolData>, rng: () => number = Math.random) {\n this._rng = rng;\n this._symbols = Object.keys(symbolsData);\n this._readBaseWeights(symbolsData);\n this._assertUsable();\n }\n\n /**\n * Get a random symbol for one slot.\n *\n * @param slot - Which slot is being filled. A buffer slot names its side,\n * so the pools for that side apply on top of the wider ones.\n * @param reelIndex - Reel the slot belongs to. Omit for a draw that\n * belongs to no particular reel; per-reel pools then don't apply.\n */\n next(slot: DrawSlot = 'spinning', reelIndex?: number): string {\n const table = this._table(slot, reelIndex);\n const rand = this._rng() * table.total;\n let lo = 0;\n let hi = table.cumulative.length - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (table.cumulative[mid] <= rand) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n return table.ids[lo];\n }\n\n /** @inheritdoc */\n set(pool: SymbolPool | null, scope: SymbolPoolScope = {}): void {\n const key = this._key(scope.slots ?? 'spinning', scope.reel);\n const previous = this._pools.get(key);\n if (pool === null) {\n this._pools.delete(key);\n } else {\n this._assertKnownIds(pool, scope);\n this._pools.set(key, {\n weights: pool.weights ? { ...pool.weights } : undefined,\n exclude: pool.exclude ? [...pool.exclude] : undefined,\n });\n }\n this._tables.clear();\n // Roll back rather than leave the set holding a pool it cannot draw from.\n try {\n this._assertDrawable();\n } catch (err) {\n if (previous === undefined) this._pools.delete(key);\n else this._pools.set(key, previous);\n this._tables.clear();\n throw err;\n }\n }\n\n /** @inheritdoc */\n clear(): void {\n this._pools.clear();\n this._tables.clear();\n }\n\n /** @inheritdoc */\n weights(scope: SymbolPoolScope = {}): Record<string, number> {\n const resolved = this._resolve(scope.slots ?? 'spinning', scope.reel);\n const out: Record<string, number> = {};\n for (const id of this._symbols) out[id] = resolved[id];\n return out;\n }\n\n /**\n * Set symbols to exclude during spinning.\n *\n * Sugar for `set({ exclude }, { slots: 'spinning' })` that leaves any\n * weight overrides on the global spinning pool alone.\n */\n setExcludeSpinning(symbolIds: string[]): void {\n this._setExclude('spinning', symbolIds);\n }\n\n /**\n * Set symbols to exclude from buffer (above/below) areas.\n *\n * Sugar for `set({ exclude }, { slots: 'buffer' })` that leaves any\n * weight overrides on the global buffer pool alone.\n */\n setExcludeBuffer(symbolIds: string[]): void {\n this._setExclude('buffer', symbolIds);\n }\n\n /** Update weights at runtime (e.g., for different game modes). */\n updateWeights(symbolsData: Record<string, SymbolData>): void {\n this._symbols = Object.keys(symbolsData);\n this._readBaseWeights(symbolsData);\n this._assertUsable();\n // Drop pool entries that reference symbols no longer present in this mode,\n // otherwise a stale exclusion from the previous game mode silently lingers.\n const present = new Set(this._symbols);\n for (const [key, pool] of this._pools) {\n this._pools.set(key, {\n weights: pool.weights\n ? Object.fromEntries(\n Object.entries(pool.weights).filter(([id]) => present.has(id)),\n )\n : undefined,\n exclude: pool.exclude?.filter((id) => present.has(id)),\n });\n }\n this._tables.clear();\n this._assertDrawable();\n }\n\n private _setExclude(slots: SymbolPoolSlots, symbolIds: string[]): void {\n const current = this._pools.get(this._key(slots, undefined));\n this.set({ weights: current?.weights, exclude: symbolIds }, { slots });\n }\n\n private _key(slots: SymbolPoolSlots, reel: number | undefined): string {\n return `${slots}:${reel ?? ALL_REELS}`;\n }\n\n private _readBaseWeights(symbolsData: Record<string, SymbolData>): void {\n this._baseWeights = {};\n for (const id of this._symbols) {\n this._baseWeights[id] = symbolsData[id].weight;\n }\n }\n\n /**\n * The layer keys that apply to one draw, widest first.\n *\n * A `'bufferStart'` cell reads the spinning layers, then the both-sides\n * buffer layers, then its own side's - so every layer only ever narrows\n * the one before it. Asking for `'buffer'` stops before the side layers:\n * that is what both sides inherit, and what `weights()` reports for it.\n */\n private _layerKeys(slots: SymbolPoolSlots, reelIndex: number | undefined): string[] {\n const perReel = (key: SymbolPoolSlots): string[] =>\n reelIndex === undefined\n ? [this._key(key, undefined)]\n : [this._key(key, undefined), this._key(key, reelIndex)];\n\n const keys = perReel('spinning');\n if (slots === 'spinning') return keys;\n keys.push(...perReel('buffer'));\n if (slots === 'buffer') return keys;\n keys.push(...perReel(slots));\n return keys;\n }\n\n /**\n * Effective weight per symbol id for one draw, excluded ids flattened to\n * `0`. See `_layerKeys` for the order.\n */\n private _resolve(\n slots: SymbolPoolSlots,\n reelIndex: number | undefined,\n ): Record<string, number> {\n const weights = { ...this._baseWeights };\n const layers = this._layerKeys(slots, reelIndex);\n const excluded = new Set<string>();\n for (const key of layers) {\n const pool = this._pools.get(key);\n if (!pool) continue;\n if (pool.weights) {\n for (const [id, weight] of Object.entries(pool.weights)) {\n if (id in weights) weights[id] = weight;\n }\n }\n if (pool.exclude) {\n for (const id of pool.exclude) excluded.add(id);\n }\n }\n // Exclusions win over every weight in the chain: a narrower layer can't\n // re-admit what a wider one banned, it can only ban more.\n for (const id of excluded) weights[id] = 0;\n return weights;\n }\n\n private _table(slots: SymbolPoolSlots, reelIndex: number | undefined): DrawTable {\n const key = this._key(slots, reelIndex);\n const cached = this._tables.get(key);\n if (cached) return cached;\n const table = this._compile(slots, reelIndex);\n if (table.total <= 0) {\n throw new Error(this._emptyPoolMessage(slots, reelIndex));\n }\n this._tables.set(key, table);\n return table;\n }\n\n private _compile(slots: SymbolPoolSlots, reelIndex: number | undefined): DrawTable {\n const weights = this._resolve(slots, reelIndex);\n const table: DrawTable = { ids: [], cumulative: [], total: 0 };\n for (const id of this._symbols) {\n const weight = weights[id];\n // Zero-weight ids are dropped rather than carried at a repeated\n // cumulative value: same draw either way, smaller table.\n if (weight <= 0) continue;\n table.total += weight;\n table.ids.push(id);\n table.cumulative.push(table.total);\n }\n return table;\n }\n\n private _assertKnownIds(pool: SymbolPool, scope: SymbolPoolScope): void {\n const ids = [...Object.keys(pool.weights ?? {}), ...(pool.exclude ?? [])];\n for (const id of ids) {\n if (this._baseWeights[id] === undefined) {\n throw new Error(\n `SymbolPool ${this._scopeName(scope.slots ?? 'spinning', scope.reel)} names symbol '${id}', ` +\n `which is not registered. Registered ids: ${this._symbols.join(', ')}.`,\n );\n }\n }\n }\n\n /**\n * Every scope a draw can reach must still have something to draw. Checked\n * on mutation so a pool that empties the strip fails at the call that\n * caused it, not mid-spin on whichever reel happens to wrap first.\n */\n private _assertDrawable(): void {\n const reels = new Set<number>();\n for (const key of this._pools.keys()) {\n const reel = key.slice(key.indexOf(':') + 1);\n if (reel !== ALL_REELS) reels.add(Number(reel));\n }\n // Widest scope first, so the message names the layer that actually\n // emptied things: a pool that clears `'buffer'` empties both sides, and\n // \"buffer cells\" is a better answer than \"buffer-start cells\".\n const drawn: SymbolPoolSlots[] = ['spinning', 'buffer', 'bufferStart', 'bufferEnd'];\n const scopes: [SymbolPoolSlots, number | undefined][] = drawn.map((slot) => [slot, undefined]);\n for (const reel of reels) {\n for (const slot of drawn) scopes.push([slot, reel]);\n }\n for (const [slot, reel] of scopes) {\n if (this._compile(slot, reel).total <= 0) {\n throw new Error(this._emptyPoolMessage(slot, reel));\n }\n }\n }\n\n private _emptyPoolMessage(slots: SymbolPoolSlots, reelIndex: number | undefined): string {\n return (\n `No symbol left to draw ${this._scopeName(slots, reelIndex)}: every registered symbol is ` +\n 'excluded or weighted 0, so the strip cannot be filled. Leave at least one symbol drawable.'\n );\n }\n\n private _scopeName(slots: SymbolPoolSlots, reelIndex: number | undefined): string {\n const where = {\n spinning: 'spinning cells',\n buffer: 'buffer cells',\n bufferStart: 'buffer-start cells',\n bufferEnd: 'buffer-end cells',\n }[slots];\n return reelIndex === undefined ? `for ${where} on every reel` : `for ${where} on reel ${reelIndex}`;\n }\n\n private _assertUsable(): void {\n if (this._symbols.length === 0) {\n throw new Error('RandomSymbolProvider requires at least one symbol.');\n }\n let total = 0;\n for (const id of this._symbols) total += Math.max(0, this._baseWeights[id]);\n if (total <= 0) {\n throw new Error(\n 'RandomSymbolProvider requires at least one symbol with weight > 0; ' +\n 'all registered symbols have weight 0, so the spinning strip cannot be filled.',\n );\n }\n }\n}\n","import type { RandomSymbolProvider } from './RandomSymbolProvider.js';\nimport { columnTargetToStrip, type ColumnTarget } from './ColumnTarget.js';\n\n/** Context passed through the middleware pipeline. */\nexport interface FrameContext {\n /** Reel column index. */\n readonly reelIndex: number;\n /** Total visible cells. */\n readonly visibleCells: number;\n /** Buffer symbols above visible area. */\n readonly bufferStart: number;\n /** Buffer symbols below visible area. */\n readonly bufferEnd: number;\n /** The symbol array being built (buffer + visible + buffer). Mutable by middleware. */\n symbols: string[];\n /**\n * This reel's target column from `setResult()` / `initialFrame()`, if\n * available. Read it with `getTargetSlot(target, cell)` (cell `0` is the\n * first visible cell; negative cells are buffer-above).\n */\n readonly target?: ColumnTarget;\n /** Whether the reel is currently spinning. */\n readonly isSpinning: boolean;\n /** Arbitrary metadata middleware can use to communicate. */\n metadata: Record<string, unknown>;\n}\n\n/** Middleware that participates in frame building. */\nexport interface FrameMiddleware {\n readonly name: string;\n /** Lower priority runs first. */\n readonly priority: number;\n process(context: FrameContext, next: () => void): void;\n}\n\n/**\n * Builds symbol frames using a middleware pipeline.\n *\n * Built-in middleware handles random fill and target placement.\n * Users can inject custom middleware for features like multiplier encoding\n * or triple-prevention.\n */\nexport class FrameBuilder {\n private _middlewares: FrameMiddleware[] = [];\n private _sorted = false;\n\n constructor(private _randomProvider: RandomSymbolProvider) {\n this.use(new RandomFillMiddleware(_randomProvider));\n this.use(new TargetPlacementMiddleware());\n }\n\n /** Add a middleware to the pipeline. */\n use(middleware: FrameMiddleware): this {\n this._middlewares.push(middleware);\n this._sorted = false;\n return this;\n }\n\n /** Remove a middleware by name. */\n remove(name: string): this {\n this._middlewares = this._middlewares.filter((m) => m.name !== name);\n return this;\n }\n\n /** Build a frame for a single reel. */\n build(\n reelIndex: number,\n visibleCells: number,\n bufferStart: number,\n bufferEnd: number,\n target?: ColumnTarget,\n isSpinning: boolean = false,\n ): string[] {\n if (!this._sorted) {\n this._middlewares.sort((a, b) => a.priority - b.priority);\n this._sorted = true;\n }\n\n const totalSlots = bufferStart + visibleCells + bufferEnd;\n const context: FrameContext = {\n reelIndex,\n visibleCells,\n bufferStart,\n bufferEnd,\n symbols: new Array<string>(totalSlots).fill(''),\n target,\n isSpinning,\n metadata: {},\n };\n\n // Run middleware chain\n let index = 0;\n const next = (): void => {\n if (index < this._middlewares.length) {\n const mw = this._middlewares[index++];\n mw.process(context, next);\n }\n };\n next();\n\n return context.symbols;\n }\n\n /** Build frames for all reels. */\n buildAll(\n reelCount: number,\n visibleCells: number,\n bufferStart: number,\n bufferEnd: number,\n targets?: ColumnTarget[],\n isSpinning: boolean = false,\n ): string[][] {\n return Array.from({ length: reelCount }, (_, reelIndex) =>\n this.build(\n reelIndex,\n visibleCells,\n bufferStart,\n bufferEnd,\n targets?.[reelIndex],\n isSpinning,\n ),\n );\n }\n\n /**\n * @internal `RandomSymbolProvider` was hidden from the package entry in\n * 1.0.0 (PR #140); this getter re-exposed the type. Middleware that needs a\n * random symbol should read it from the `FrameContext.symbols` slot it is\n * filling, or carry its own provider - weights are configured through\n * `builder.weights({...})`.\n */\n get randomProvider(): RandomSymbolProvider {\n return this._randomProvider;\n }\n\n get middleware(): ReadonlyArray<FrameMiddleware> {\n return this._middlewares;\n }\n}\n\n/** Fills empty symbol slots with random symbols. OCCUPIED cells are kept verbatim. */\nclass RandomFillMiddleware implements FrameMiddleware {\n readonly name = 'random-fill';\n readonly priority = 0;\n\n constructor(private _provider: RandomSymbolProvider) {}\n\n process(context: FrameContext, next: () => void): void {\n for (let i = 0; i < context.symbols.length; i++) {\n if (!context.symbols[i]) {\n // Each slot names the pool it belongs to: the visible window, or\n // whichever side of the buffer it sits on.\n const slot =\n i < context.bufferStart\n ? 'bufferStart'\n : i >= context.bufferStart + context.visibleCells\n ? 'bufferEnd'\n : 'spinning';\n context.symbols[i] = this._provider.next(slot, context.reelIndex);\n }\n }\n next();\n }\n}\n\n/** Places the target column (from setResult) onto the strip. */\nclass TargetPlacementMiddleware implements FrameMiddleware {\n readonly name = 'target-placement';\n readonly priority = 10;\n\n process(context: FrameContext, next: () => void): void {\n if (context.target) {\n const strip = columnTargetToStrip(context.target, context.bufferStart);\n const count = Math.min(strip.length, context.symbols.length);\n for (let i = 0; i < count; i++) {\n const id = strip[i];\n if (id) context.symbols[i] = id;\n }\n }\n next();\n }\n}\n","/**\n * Configuration for tumble cascade phases. Passed to\n * `ReelSetBuilder.tumble(config)` and baked into the three phase classes at\n * build time. Pure animation values. every callback you want is an event\n * (`reelSet.events.on('cascade:...', ...)`), never a config field.\n */\nimport type { Direction } from '../core/ReelAxis.js';\n\nexport interface TumbleFallConfig {\n /**\n * How long each symbol's fall-out tween runs, in ms. Default 300.\n */\n duration?: number;\n\n /**\n * GSAP easing string for the fall trajectory. Default `'sine.in'`\n * (gravity feel). Anything from gsap.com/docs/v3/Eases works.\n */\n ease?: string;\n\n /**\n * Delay between successive cells starting their fall, in ms. `0` makes\n * every cell fall together. Default 0.\n */\n cellStagger?: number;\n\n /**\n * Which cell of each reel begins its fall first.\n *\n * - `'auto'` (default). the cell at the gravity-EXIT end goes first, so\n * the column drains from the edge symbols are leaving by. Under the\n * usual downward gravity that is the bottom cell, which pairs with the\n * per-reel left-to-right stagger from `speed.spinDelay` to give the\n * canonical \"bottom-left falls first, top-right last\" feel of\n * commercial tumble slots. Flip gravity and the stagger flips with it.\n * - `'endFirst'`. always the cell at the larger main coordinate (bottom /\n * right) first, whichever way gravity points.\n * - `'startFirst'`. always the cell at the smaller main coordinate (top /\n * left) first. Reads as the column \"peeling\" away from that edge.\n *\n * `'endFirst'` and `'startFirst'` are geometric, like the buffers: they name\n * an end of the strip, not a direction of travel. Only `'auto'` follows\n * gravity.\n */\n cellOrder?: 'auto' | 'endFirst' | 'startFirst';\n}\n\nexport interface TumbleDropInConfig {\n /**\n * How long each symbol's drop-in tween runs, in ms. Default 600.\n */\n duration?: number;\n\n /**\n * GSAP easing string for the drop-in trajectory. Default `'power2.out'`\n *. symbols decelerate cleanly into their slot with NO overshoot, which\n * matches the canonical commercial-slot pattern: fall straight in, then\n * play a per-symbol landing spine animation. Use `'back.out(1.5)'` for a\n * soft overshoot, `'bounce.out'` for cartoon bounce, `'sine.in'` for\n * gravity, `'expo.in'` for slam.\n */\n ease?: string;\n\n /**\n * Delay between successive cells starting their drop, in ms. Default 60.\n * `0` makes every animated cell drop in simultaneously. the most common\n * choice for cascade refills.\n */\n cellStagger?: number;\n\n /**\n * Which cell lands first when `cellStagger > 0`.\n *\n * - `'auto'` (default). the cell at the gravity-EXIT end arrives first,\n * the way a settling stack fills from the floor up. Under the usual\n * downward gravity that is the bottom cell, which paired with\n * `setDropOrder('ltr')` gives the canonical \"bottom-left first,\n * top-right last\" reveal every commercial tumble slot ships with. A\n * reel that drains upward fills from the top instead, with no further\n * config.\n * - `'endFirst'`. always the cell at the larger main coordinate (bottom /\n * right) first, whichever way gravity points.\n * - `'startFirst'`. always the cell at the smaller main coordinate (top /\n * left) first.\n *\n * `'endFirst'` and `'startFirst'` are geometric, like the buffers: they name\n * an end of the strip, not a direction of travel. Only `'auto'` follows\n * gravity.\n */\n cellOrder?: 'auto' | 'endFirst' | 'startFirst';\n\n /**\n * How far symbols fall, in cells.\n *\n * - `'perHole'` (default). gravity-correct. Each symbol falls exactly\n * as far as its hole demands: new symbols from above, survivors slide\n * down the count of holes below them, untouched symbols don't move.\n * - `'auto'`. every symbol falls the full visible-cells distance. Best\n * for Moment A (initial drop, \"the entire column drops in unison\")\n * and for refills made up entirely of new symbols. For refills with\n * SURVIVORS the engine silently falls back to per-hole geometry for\n * those movers. `'auto'` would teleport a sliding survivor above\n * the viewport before dropping it back down, which reads as a flash.\n * - `number`. explicit pixel distance applied uniformly to every\n * animated symbol.\n */\n distance?: 'perHole' | 'auto' | number;\n}\n\nexport interface TumbleConfig {\n /** Fall-out animation (existing symbols leaving on `spin()` click). */\n fall?: TumbleFallConfig;\n /** Drop-in animation (new symbols arriving after `setResult` or in `refill`). */\n dropIn?: TumbleDropInConfig;\n /**\n * Which way symbols settle along the strip. Default `'auto'`.\n *\n * - `'auto'` (default). follow each reel's own travel direction, so a\n * reel built with `.direction('reverse')` cascades upward (or leftward,\n * on a horizontal set) without any further configuration. This is what\n * you want almost always.\n * - `'forward'`. always settle toward the larger main coordinate (down /\n * right), whichever way the reel spins.\n * - `'reverse'`. always settle toward the smaller main coordinate (up /\n * left).\n *\n * Gravity is independent of direction so a reel can spin one way and drop\n * the other, but the default ties them together because that is the\n * physically coherent case. Orientation never enters into it: gravity picks\n * an END of the strip, and the axis decides which screen edge that is.\n *\n * Whichever edge gravity exits by is also the edge the server must pack\n * survivors against in the grids it sends -- the engine animates the\n * result, it does not reorder it.\n */\n gravity?: 'auto' | Direction;\n}\n\n/** Resolved config with defaults applied. Internal type. */\nexport interface ResolvedTumbleConfig {\n fall: Required<TumbleFallConfig>;\n dropIn: Required<TumbleDropInConfig>;\n gravity: 'auto' | Direction;\n}\n\n/**\n * Resolve `'auto'` against a reel's own travel direction. Phases call this\n * rather than reading `axis.polarity`, because gravity and travel are\n * separable (ADR 016 section 3.6) and only coincide under the default.\n */\nexport function resolveGravity(gravity: 'auto' | Direction, direction: Direction): Direction {\n return gravity === 'auto' ? direction : gravity;\n}\n\n/** `+1` when gravity settles toward the larger main coordinate, `-1` otherwise. */\nexport function gravitySign(gravity: Direction): 1 | -1 {\n return gravity === 'forward' ? 1 : -1;\n}\n\n/**\n * Resolve `'auto'` cell order against the reel's resolved gravity. `'auto'`\n * means \"the gravity-EXIT end goes first\": the column drains from, and\n * refills toward, the edge symbols are settling against. Explicit\n * `'endFirst'` / `'startFirst'` stay geometric and pass through untouched.\n *\n * Without this, a reel draining upward still staggered from the bottom cell\n * - the one FURTHEST from the exit edge - so the cell nearest the drain\n * waited for the whole column to leave ahead of it.\n */\nexport function resolveCellOrder(\n cellOrder: 'auto' | 'endFirst' | 'startFirst',\n gravity: Direction,\n): 'endFirst' | 'startFirst' {\n if (cellOrder !== 'auto') return cellOrder;\n return gravity === 'forward' ? 'endFirst' : 'startFirst';\n}\n\nexport function resolveTumbleConfig(config: TumbleConfig | undefined): ResolvedTumbleConfig {\n return {\n gravity: config?.gravity ?? 'auto',\n fall: {\n duration: config?.fall?.duration ?? 300,\n ease: config?.fall?.ease ?? 'sine.in',\n cellStagger: config?.fall?.cellStagger ?? 0,\n cellOrder: config?.fall?.cellOrder ?? 'auto',\n },\n dropIn: {\n duration: config?.dropIn?.duration ?? 600,\n // No overshoot in the default: most commercial cascade slots have\n // symbols fall straight into their slot, then play a per-symbol\n // landing spine animation. `power2.out` is a clean decelerating\n // ease that lands without an overshoot bounce. Recipes that want\n // the springy feel can opt into `back.out(...)` explicitly.\n ease: config?.dropIn?.ease ?? 'power2.out',\n cellStagger: config?.dropIn?.cellStagger ?? 60,\n cellOrder: config?.dropIn?.cellOrder ?? 'auto',\n distance: config?.dropIn?.distance ?? 'perHole',\n },\n };\n}\n\n/**\n * Merge a partial `TumbleFallConfig` over a fully-resolved base. Used by\n * `CascadeFallPhase` at `onEnter` time to apply per-speed-profile\n * overrides without losing the build-time defaults. Returns a new object\n *. the base is never mutated.\n */\nexport function mergeFallConfig(\n base: Required<TumbleFallConfig>,\n override: TumbleFallConfig | undefined,\n): Required<TumbleFallConfig> {\n if (!override) return base;\n return {\n duration: override.duration ?? base.duration,\n ease: override.ease ?? base.ease,\n cellStagger: override.cellStagger ?? base.cellStagger,\n cellOrder: override.cellOrder ?? base.cellOrder,\n };\n}\n\n/**\n * Merge a partial `TumbleDropInConfig` over a fully-resolved base. Used by\n * `CascadeDropInPhase` at `onEnter` time to apply per-speed-profile\n * overrides without losing the build-time defaults. Returns a new object\n *. the base is never mutated.\n */\nexport function mergeDropInConfig(\n base: Required<TumbleDropInConfig>,\n override: TumbleDropInConfig | undefined,\n): Required<TumbleDropInConfig> {\n if (!override) return base;\n return {\n duration: override.duration ?? base.duration,\n ease: override.ease ?? base.ease,\n cellStagger: override.cellStagger ?? base.cellStagger,\n cellOrder: override.cellOrder ?? base.cellOrder,\n distance: override.distance ?? base.distance,\n };\n}\n","import type { gsap } from 'gsap';\nimport type { Container } from 'pixi.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { SpinningMode } from '../modes/SpinningMode.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\nimport type { EventEmitter } from '../../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../../events/ReelEvents.js';\nimport type { TumbleFallConfig } from '../../cascade/TumbleConfig.js';\nimport {\n gravitySign,\n mergeFallConfig,\n resolveCellOrder,\n resolveGravity,\n} from '../../cascade/TumbleConfig.js';\nimport type { Direction } from '../../core/ReelAxis.js';\n\nexport interface CascadeFallPhaseConfig {\n /** Required by the start-phase contract. set on the reel even though\n * tumble mode never accelerates. */\n spinningMode: SpinningMode;\n /** Per-reel delay before this column begins its fall, in ms. */\n delay?: number;\n /** Reel-set event bus, injected by SpinController so the phase can emit\n * `cascade:fall:*` events. */\n events: EventEmitter<ReelSetEvents>;\n}\n\n/**\n * Fall-out half of the tumble cascade. Replaces `StartPhase` when the\n * builder was configured with `.tumble(...)`.\n *\n * Runs at the moment the player presses spin: every currently-visible\n * symbol falls off the bottom of the viewport. The reel then sits at speed\n * zero while `SpinPhase` waits for the server result.\n *\n * Animation parameters (duration, ease, cell stagger) are baked into the\n * phase at builder time via the factory closure; the run-time config\n * carries only per-spin context (delay, event bus).\n */\nexport class CascadeFallPhase extends ReelPhase<CascadeFallPhaseConfig> {\n readonly name = 'cascade:fall';\n readonly skippable = true;\n\n private readonly _baseFall: Required<TumbleFallConfig>;\n /** Resolved at `onEnter` time by merging the active speed profile's\n * `tumble.fall` override (if any) over `_baseFall`. Lives only for the\n * duration of a single run so a `setSpeed` between phases is honoured\n * on the next entry. */\n private _fall: Required<TumbleFallConfig>;\n private _timeline: gsap.core.Timeline | null = null;\n private _delayedCall: gsap.core.Tween | null = null;\n /** Views actively being faded out. Tracked so `onSkip` can hide them\n * rather than leaving them at mid-fall position. */\n private _fallingViews: Container[] = [];\n /** Captured on enter so `onSkip` can emit the paired `cascade:fall:end`\n * without needing the config closure (which lives only inside `_beginFall`). */\n private _events: EventEmitter<ReelSetEvents> | null = null;\n /** Whether `cascade:fall:start` was emitted yet. `onSkip` emits the\n * matching `:end` ONLY when `:start` already fired. a skip during the\n * pre-fall delay window must not produce an unpaired `:end`. */\n private _startEmitted = false;\n /** Per-run abort controller exposed to listeners on `cascade:fall:symbol`\n * as `signal`. Aborts on `onSkip` so listener-scheduled tweens (squish,\n * badge fade, etc.) can clean themselves up alongside the library's\n * own timeline. Stays un-aborted on natural completion. only explicit\n * skips trigger it. */\n private _skipAbort: AbortController | null = null;\n\n /** Build-time gravity setting; `'auto'` resolves per reel at `onEnter`. */\n private readonly _gravity: 'auto' | Direction;\n\n constructor(\n reel: Reel,\n speed: SpeedProfile,\n fall: Required<TumbleFallConfig>,\n gravity: 'auto' | Direction = 'auto',\n ) {\n super(reel, speed);\n this._baseFall = fall;\n this._fall = fall;\n this._gravity = gravity;\n }\n\n protected onEnter(config: CascadeFallPhaseConfig): void {\n const reel = this._reel;\n reel.spinningMode = config.spinningMode;\n reel.speed = 0;\n reel.notifySpinStart();\n\n // Apply speed-profile tumble override. Falls back to the build-time\n // base when the profile doesn't define one.\n this._fall = mergeFallConfig(this._baseFall, this._speed.tumble?.fall);\n\n this._events = config.events;\n this._startEmitted = false;\n this._skipAbort = new AbortController();\n\n const delaySec = (config.delay ?? 0) / 1000;\n if (delaySec > 0) {\n this._delayedCall = this._reel.gsap.delayedCall(delaySec, () => this._beginFall(config.events));\n } else {\n this._beginFall(config.events);\n }\n }\n\n private _beginFall(events: EventEmitter<ReelSetEvents>): void {\n this._delayedCall = null;\n\n const reel = this._reel;\n const axis = reel.axis;\n const cellHeight = reel.motion.slotPitch;\n const visibleCells = reel.visibleCells;\n const reelIndex = reel.reelIndex;\n // Symbols fall along GRAVITY, not along travel. The two coincide under\n // the default `gravity: 'auto'`, but a reel can spin one way and drop\n // the other (ADR 016 section 3.6).\n const gravity = resolveGravity(this._gravity, axis.direction);\n const sign = gravitySign(gravity);\n\n // Distance: just past the exit-edge buffer so the symbols clear the mask.\n // The exit edge is the one gravity points AT, so a reverse-gravity reel\n // clears through `bufferStart`. Reading the wrong buffer here still\n // happened to clear the mask, but only because the old value was\n // over-generous - it is a real bug the moment this is tightened.\n const exitBuffer = sign > 0 ? reel.bufferEnd : reel.bufferStart;\n const fallDistance = (visibleCells + exitBuffer + 1) * cellHeight;\n\n const fallSec = this._fall.duration / 1000;\n const staggerSec = this._fall.cellStagger / 1000;\n\n // Snapshot views and current main positions before any tween starts.\n // Avoids reading mid-tween values if `cascade:fall:symbol` listeners\n // mutate things.\n const symbols: ReelSymbol[] = [];\n const views: Container[] = [];\n const startMains: number[] = [];\n for (let cell = 0; cell < visibleCells; cell++) {\n const sym = reel.getSymbolAt(cell);\n symbols.push(sym);\n views.push(sym.view);\n startMains.push(axis.getMain(sym.view));\n }\n this._fallingViews = views;\n\n events.emit('cascade:fall:start', { reelIndex });\n this._startEmitted = true;\n\n if (fallSec <= 0) {\n // Instant fall: hide and complete. No symbol events fire (no tween\n // to attach decoration to), so the AbortController is dropped\n // un-aborted. listeners can't have registered cleanup against it.\n for (const v of views) v.alpha = 0;\n events.emit('cascade:fall:end', { reelIndex });\n this._fallingViews = [];\n // Null the start-emitted flag so a later `forceComplete` doesn't\n // re-emit `cascade:fall:end` on a phase that already balanced its\n // start/end pair.\n this._startEmitted = false;\n this._events = null;\n this._skipAbort = null;\n this._complete();\n return;\n }\n\n const tl = this._reel.gsap.timeline({\n onComplete: () => {\n this._timeline = null;\n for (const v of views) v.alpha = 0;\n this._fallingViews = [];\n events.emit('cascade:fall:end', { reelIndex });\n this._startEmitted = false;\n this._events = null;\n // Natural completion: drop the controller un-aborted. Listener\n // tweens scheduled off `cascade:fall:symbol` are expected to\n // settle on their own timeline.\n this._skipAbort = null;\n this._complete();\n },\n });\n this._timeline = tl;\n\n // Stagger order follows GRAVITY under the default `'auto'`: the cell at\n // the exit end peels off first, so the column drains from the edge it is\n // leaving by. Explicit 'endFirst'/'startFirst' name a geometric end and\n // ignore gravity.\n const reverseOrder = resolveCellOrder(this._fall.cellOrder, gravity) === 'endFirst';\n\n for (let cell = 0; cell < visibleCells; cell++) {\n const view = views[cell];\n const symbol = symbols[cell];\n const startMain = startMains[cell];\n const orderIndex = reverseOrder ? visibleCells - 1 - cell : cell;\n const offset = orderIndex * staggerSec;\n\n // Fire the per-symbol event right before the tween starts so listeners\n // can stage parallel tweens with full knowledge of duration/ease.\n // `signal` aborts when this phase is skipped, so listener-scheduled\n // tweens (squish, badge, etc.) can be cleaned up alongside the\n // library's own timeline.\n tl.call(\n () => {\n const signal = this._skipAbort?.signal;\n if (!signal) return;\n events.emit('cascade:fall:symbol', {\n symbol,\n view,\n reelIndex,\n cellIndex: cell,\n duration: this._fall.duration,\n ease: this._fall.ease,\n distance: fallDistance,\n signal,\n });\n },\n undefined,\n offset,\n );\n\n tl.to(view, {\n [axis.mainProp]: startMain + sign * fallDistance,\n duration: fallSec,\n ease: this._fall.ease,\n }, offset);\n }\n }\n\n update(_deltaMs: number): void {}\n\n protected onSkip(): void {\n this._kill();\n for (const v of this._fallingViews) v.alpha = 0;\n this._fallingViews = [];\n // Abort BEFORE emitting `:end` so listeners registered against\n // `signal` see the cancellation in the same microtask their `:end`\n // handler would (some consumers branch on `wasSkipped`-flavoured\n // state and rely on the order).\n if (this._skipAbort && !this._skipAbort.signal.aborted) {\n this._skipAbort.abort();\n }\n this._skipAbort = null;\n // Emit the paired `cascade:fall:end` so listeners that count\n // start/end events stay balanced. Only emit if `:start` already\n // fired. a skip during the pre-fall delay window has no\n // matching `:start`, so an `:end` here would be unpaired.\n if (this._startEmitted && this._events) {\n this._events.emit('cascade:fall:end', { reelIndex: this._reel.reelIndex });\n }\n this._startEmitted = false;\n this._events = null;\n }\n\n private _kill(): void {\n if (this._delayedCall) {\n this._delayedCall.kill();\n this._delayedCall = null;\n }\n if (this._timeline) {\n this._timeline.kill();\n this._timeline = null;\n }\n }\n}\n","/**\n * Gravity-correct refill geometry for tumble cascades.\n *\n * Two distinct moments use the same algorithm with different inputs:\n *\n * - **Moment A (initial drop):** `winnerCells = []`. The entire visible\n * column is treated as \"new\". every cell falls in from above the\n * viewport. The vertical distance per cell is `visibleCells` cells, so\n * all cells arrive at their grid positions in the same beat.\n *\n * - **Moment B (cascade refill):** `winnerCells` lists the cells whose\n * symbols were removed by the most recent win. Survivors slide toward\n * the gravity-exit edge to fill the gaps; new symbols enter from the\n * gravity-entry edge into the holes left behind. The new grid follows\n * the server convention that survivors keep their relative order and\n * pack against the exit edge, with `winnerCells.length` new symbols\n * stacked behind them.\n *\n * Which edge is which comes from `gravity` (ADR 016 section 3.6), NOT from\n * the screen axis: `'forward'` settles toward the larger cell index (down on\n * a vertical set, right on a horizontal one), `'reverse'` toward the smaller.\n * The algorithm is pure index arithmetic, so orientation never reaches it -\n * only the reel's travel direction does.\n */\nimport type { Direction } from '../core/ReelAxis.js';\n\n/** A cell coordinate on the reel set. `reel` is column, `cell` is visible cell. */\nexport interface Cell {\n reel: number;\n cell: number;\n}\n\nexport interface DropOffset {\n /** Visible cell in the new grid (start-to-end, 0-indexed). */\n cell: number;\n /**\n * Where this symbol \"came from\" expressed as a virtual cell index.\n * Off-grid values name the cell the symbol enters from: under\n * `gravity: 'forward'` new symbols come from negative indices (before\n * cell 0); under `'reverse'` they come from `visibleCells` and up. An\n * index inside `[0, visibleCells)` is a survivor's OLD cell.\n *\n * Read {@link DropOffset.isNew} rather than testing the sign - the sign\n * only discriminates under forward gravity.\n */\n originalCell: number;\n /**\n * Signed cell distance this symbol travels: `cell - originalCell`.\n * Positive moves toward the end edge (forward gravity), negative toward\n * the start edge (reverse gravity). Zero means the symbol stays put and\n * must NOT be animated.\n */\n offsetCells: number;\n /**\n * True when this is a fresh symbol entering from off-grid, false for a\n * survivor that was already on the reel. The discriminator every caller\n * should branch on; `originalCell < 0` is only equivalent under forward\n * gravity.\n */\n isNew: boolean;\n}\n\n/**\n * Compute per-cell drop offsets for one reel given its winner set.\n *\n * Returns one entry per visible cell, top-to-bottom. Cells with\n * `offsetCells === 0` should NOT be animated. they're survivors that\n * didn't move.\n *\n * **Convention** (Moment B): the new grid must place new symbols at the\n * top `winnerCells.length` cells and survivors at the bottom cells in their\n * original top-to-bottom order. This matches how server-side gravity\n * simulations emit cascade results.\n *\n * @param options.initial - When `true` (Moment A. the player's first\n * spin click), every cell is treated as new regardless of `winnerCells`\n * (which is normally empty for initial spins). When `false` (Moment B\n * . cascade refill), an empty `winnerCells` means *no movement on this\n * reel*; survivor reels in a refill correctly return all-zero offsets.\n * Default `false` so callers can't accidentally trigger a full re-drop\n * on a reel that had no winners.\n */\nexport function computeDropOffsets(\n visibleCells: number,\n winnerCells: readonly number[],\n options: { initial?: boolean; gravity?: Direction } = {},\n): DropOffset[] {\n const initial = options.initial ?? false;\n const gravity = options.gravity ?? 'forward';\n // Initial: every visible cell is new (Moment A). The empty-winners case\n // in refill (Moment B) gives winCount=0 → all cells resolve to survivors\n // with originalCell === cell → offsetCells === 0 → no animation.\n const winCount = initial ? visibleCells : winnerCells.length;\n const winSet = initial ? new Set<number>() : new Set(winnerCells);\n\n // Survivor cells in the OLD grid, ascending. Indexed by survivor-position\n // so the cells nearest the gravity-exit edge can pull their original cell\n // in order.\n const nonWinnerCells: number[] = [];\n for (let r = 0; r < visibleCells; r++) {\n if (!winSet.has(r)) nonWinnerCells.push(r);\n }\n\n // Under 'forward' gravity symbols settle toward the LARGER cell index, so\n // survivors pack into the tail and new symbols occupy the head, entering\n // from negative indices. 'reverse' is the exact mirror: survivors pack\n // into the head and new symbols occupy the tail, entering from\n // `visibleCells` and beyond. Everything else - the absolute main\n // coordinate of a virtual cell, the sign of `offsetCells` - falls out of\n // the index arithmetic, which is why the phases need no second branch.\n const survivorCount = visibleCells - winCount;\n const offsets: DropOffset[] = [];\n for (let cell = 0; cell < visibleCells; cell++) {\n const isNew = gravity === 'forward' ? cell < winCount : cell >= survivorCount;\n let originalCell: number;\n if (isNew) {\n // Stack the arrivals just off the gravity-entry edge so every new\n // symbol travels the same `winCount` cells.\n originalCell = gravity === 'forward' ? cell - winCount : cell + winCount;\n } else {\n // Survivor. read its OLD cell from the precomputed survivor list.\n originalCell = gravity === 'forward'\n ? nonWinnerCells[cell - winCount]\n : nonWinnerCells[cell];\n }\n offsets.push({ cell, originalCell, offsetCells: cell - originalCell, isNew });\n }\n return offsets;\n}\n","import type { gsap } from 'gsap';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\nimport type { EventEmitter } from '../../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../../events/ReelEvents.js';\nimport { computeDropOffsets } from '../../cascade/tumbleAlgorithm.js';\nimport { resolveGravity } from '../../cascade/TumbleConfig.js';\nimport type { Direction } from '../../core/ReelAxis.js';\n\nexport interface CascadePlacePhaseConfig {\n /** Full target frame for this reel: buffer-above + visible + buffer-below. */\n targetFrame: string[];\n /** Visible cells whose old symbols were \"winners\" cleared since the last\n * placement. Empty AND `initial: false` ⇒ no movement on this reel. */\n winnerCells: number[];\n /** `true` for Moment A (initial spin); `false` for Moment B (refill). */\n initial: boolean;\n /** Per-reel delay before placement, in ms. */\n delay?: number;\n /** Reel-set event bus, injected by SpinController. */\n events: EventEmitter<ReelSetEvents>;\n}\n\n/**\n * Identity-swap half of the tumble cascade. Runs after `CascadeFallPhase`\n * (Moment A) or right at the start of `refill()` (Moment B).\n *\n * Mechanically tiny: it calls `reel.placeSymbols(visible)` to swap visible\n * symbol identities, then fires `cascade:place:end`. Listeners on that\n * event (badges, decorations, multiplier overlays) run synchronously\n * BEFORE `CascadeDropInPhase` starts the drop tweens. so anything you\n * attach to a new symbol falls WITH it, not after landing.\n */\nexport class CascadePlacePhase extends ReelPhase<CascadePlacePhaseConfig> {\n readonly name = 'cascade:place';\n readonly skippable = true;\n\n private _config: CascadePlacePhaseConfig | null = null;\n private _delayedCall: gsap.core.Tween | null = null;\n /** Build-time gravity setting; `'auto'` resolves per reel at place time. */\n private readonly _gravity: 'auto' | Direction;\n\n constructor(reel: Reel, speed: SpeedProfile, gravity: 'auto' | Direction = 'auto') {\n super(reel, speed);\n this._gravity = gravity;\n }\n\n protected onEnter(config: CascadePlacePhaseConfig): void {\n this._config = config;\n const delaySec = (config.delay ?? 0) / 1000;\n if (delaySec > 0) {\n this._delayedCall = this._reel.gsap.delayedCall(delaySec, () => this._doPlace());\n } else {\n this._doPlace();\n }\n }\n\n /**\n * Trim the positional `targetFrame` (buffer-above … visible … buffer-below)\n * to the head the cascade actually lands: buffer-above plus the visible\n * window. The buffer-above cells matter — a big-symbol anchor can live\n * there (a partial-visibility \"tail-visible\" block), and dropping it leaves\n * its visible OCCUPIED stub uncovered so the block's visible cell renders\n * empty. Buffer-below cells are deliberately left off the end for\n * `placeStrip` to random-fill: they're masked and never carry a visible\n * anchor.\n */\n private _placement(targetFrame: string[]): string[] {\n return targetFrame.slice(0, this._reel.bufferStart + this._reel.visibleCells);\n }\n\n private _doPlace(): void {\n this._delayedCall = null;\n if (!this._config) return;\n\n const reel = this._reel;\n const { targetFrame, events } = this._config;\n\n // Re-mask lifted unmask views before any placement/geometry work.\n // A pure refill never passes through StartPhase (strip spins) or\n // notifySpinStart (tumble fall), so without this a lifted symbol\n // stays in viewport.unmaskedContainer while the drop-in repositions\n // it above the viewport. rendering its whole approach outside the\n // mask. Same rule as StartPhase._launch; notifyLanded re-lifts once\n // the refill settles. Idempotent.\n reel.beginMotion();\n\n reel.placeStrip(this._placement(targetFrame));\n // Defensive: CascadeFallPhase displaces views by `fallDistance` and pool\n // reuse can leak the post-fall y onto same-id replacements when\n // `_placeSymbolView` runs BEFORE the motion snap inside placeSymbols.\n // Calling snapToGrid here guarantees every view sits at its grid Y\n // before listeners on `cascade:place:end` (or CascadeDropInPhase) read\n // them.\n reel.snapToGrid();\n reel.notifySpinEnd();\n\n // Visibility split: SURVIVORS (offsetCells === 0) become visible\n // immediately at grid Y; MOVERS stay at alpha=0 so they don't flash\n // at grid Y for a frame between PlacePhase and CascadeDropInPhase\n // moving them above the viewport. The DropIn phase reveals movers\n // AFTER repositioning view.y, which produces a flash-free drop-in.\n const offsets = computeDropOffsets(\n reel.visibleCells,\n this._config.winnerCells,\n {\n initial: this._config.initial,\n // Must match the gravity CascadeDropInPhase will animate under, or\n // the mover/survivor split computed here reveals the wrong cells.\n gravity: resolveGravity(this._gravity, reel.axis.direction),\n },\n );\n // Big symbols: every occupied cell of a block resolves to the SAME anchor\n // view. Reveal it ONCE, keyed on the first visible cell of the block\n // (top-to-bottom), so the anchor's alpha reflects whether the BLOCK moves\n // (mover ⇒ 0, stays hidden until the drop-in repositions it). Without the\n // dedup the last occupied cell wins, leaving a mover-anchor at alpha 1 at\n // its grid Y. it flashes fully-formed there for a frame before the drop-in\n // yanks it back up to fall again (\"snap then re-drop\").\n const placedSymbols: ReelSymbol[] = [];\n const handledAnchors = new Set<number>();\n for (const off of offsets) {\n const anchorCell = reel.getAnchorCell(off.cell);\n if (anchorCell !== off.cell && handledAnchors.has(anchorCell)) continue;\n handledAnchors.add(anchorCell);\n const sym = reel.getSymbolAt(off.cell);\n sym.view.visible = true;\n sym.view.alpha = off.offsetCells === 0 ? 1 : 0;\n placedSymbols.push(sym);\n }\n\n events.emit('cascade:place:end', {\n reelIndex: reel.reelIndex,\n placedSymbols,\n isInitial: this._config.initial,\n winnerCells: this._config.winnerCells,\n });\n\n this._complete();\n }\n\n update(_deltaMs: number): void {}\n\n protected onSkip(): void {\n if (this._delayedCall) {\n this._delayedCall.kill();\n this._delayedCall = null;\n }\n // If skipped before placement, force the placement so the reel lands\n // on the right identities AND every visible view is fully revealed\n // (skip == \"show me the final landed state right now\").\n if (this._config) {\n const reel = this._reel;\n reel.placeStrip(this._placement(this._config.targetFrame));\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const view = reel.getSymbolAt(cell).view;\n view.alpha = 1;\n view.visible = true;\n }\n }\n }\n}\n","import type { gsap } from 'gsap';\nimport type { Container } from 'pixi.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\nimport type { EventEmitter } from '../../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../../events/ReelEvents.js';\nimport type { TumbleDropInConfig } from '../../cascade/TumbleConfig.js';\nimport {\n gravitySign,\n mergeDropInConfig,\n resolveCellOrder,\n resolveGravity,\n} from '../../cascade/TumbleConfig.js';\nimport type { Direction } from '../../core/ReelAxis.js';\nimport { computeDropOffsets } from '../../cascade/tumbleAlgorithm.js';\n\nexport interface CascadeDropInPhaseConfig {\n /** Visible cells whose old symbols were winners. drives per-cell drop\n * geometry. Empty AND `initial: false` ⇒ no animation on this reel. */\n winnerCells: number[];\n /** `true` for Moment A (initial spin: every cell drops from above);\n * `false` for Moment B (refill: only winner-displaced cells animate). */\n initial: boolean;\n /**\n * Two-stage refill filter.\n *\n * - `'all'` (default). animate every mover: survivors-sliding-down AND\n * new-symbols-from-above. The classic single-phase refill.\n * - `'gravity'`. animate only survivors that slide down to fill holes\n * (`isNew === false` with offsetCells !== 0). New-symbol movers stay\n * repositioned above the viewport with alpha=0. invisible, awaiting\n * the second stage. Emits `cascade:gravity:*` events.\n * - `'new'`. animate only new-symbol movers (`isNew === true`).\n * Survivors are already at their grid Y from the prior gravity stage,\n * so this phase reveals them at alpha=1 and only tweens the new\n * arrivals down from above. Emits `cascade:dropIn:*` events.\n *\n * Used by `mode: 'gravity-then-drop'` on `refill()` to split one refill\n * into two animated beats with a hold in between.\n */\n role?: 'all' | 'gravity' | 'new';\n /** Reel-set event bus, injected by SpinController. */\n events: EventEmitter<ReelSetEvents>;\n}\n\ninterface DropJob {\n cell: number;\n symbol: ReelSymbol;\n view: Container;\n startMain: number;\n finalMain: number;\n offsetCells: number;\n}\n\n/**\n * Drop-in half of the tumble cascade. Animates each visible symbol from\n * its computed origin (above the viewport for new symbols, its old grid\n * cell for survivors) down to its current grid position.\n *\n * Geometry comes from `computeDropOffsets`. Symbols whose `offsetCells`\n * resolves to zero (untouched survivors) skip the tween entirely.\n *\n * Resolves when every animated tween completes, then calls\n * `reel.notifyLanded()`.\n */\nexport class CascadeDropInPhase extends ReelPhase<CascadeDropInPhaseConfig> {\n readonly name = 'cascade:dropIn';\n readonly skippable = true;\n\n private readonly _baseDrop: Required<TumbleDropInConfig>;\n /** Resolved at `onEnter` time by merging the active speed profile's\n * `tumble.dropIn` override (if any) over `_baseDrop`. Lives only for\n * the duration of a single run so a `setSpeed` between phases is\n * honoured on the next entry. */\n private _drop: Required<TumbleDropInConfig>;\n private _timeline: gsap.core.Timeline | null = null;\n private _jobs: DropJob[] = [];\n /** Captured on enter so `onSkip` can emit the paired `:end` event\n * without needing the config closure. */\n private _events: EventEmitter<ReelSetEvents> | null = null;\n private _endEvent: 'cascade:dropIn:end' | 'cascade:gravity:end' = 'cascade:dropIn:end';\n /** Per-run abort controller exposed on `cascade:dropIn:symbol` (or\n * `cascade:gravity:symbol`) as `signal`. Aborts on `onSkip` so\n * listener-scheduled tweens (landing squish, badge fade) can clean up\n * alongside the library's own timeline. Stays un-aborted on natural\n * completion. */\n private _skipAbort: AbortController | null = null;\n\n /** Build-time gravity setting; `'auto'` resolves per reel at `onEnter`. */\n private readonly _gravity: 'auto' | Direction;\n\n constructor(\n reel: Reel,\n speed: SpeedProfile,\n drop: Required<TumbleDropInConfig>,\n gravity: 'auto' | Direction = 'auto',\n ) {\n super(reel, speed);\n this._baseDrop = drop;\n this._drop = drop;\n this._gravity = gravity;\n }\n\n protected onEnter(config: CascadeDropInPhaseConfig): void {\n const reel = this._reel;\n const axis = reel.axis;\n const visible = reel.visibleCells;\n const cellHeight = reel.motion.slotPitch;\n const events = config.events;\n const reelIndex = reel.reelIndex;\n const role = config.role ?? 'all';\n\n // Re-mask lifted unmask views before building drop jobs. Movers are\n // pre-positioned above the viewport; a lifted view would render that\n // whole approach outside the mask. CascadePlacePhase already does\n // this on the standard refill path; this covers direct drop-in\n // entries (two-stage refills re-enter here after the gravity hold,\n // during which notifyLanded may have re-lifted). Idempotent.\n reel.beginMotion();\n\n // Apply speed-profile tumble override. Falls back to the build-time\n // base when the profile doesn't define one.\n this._drop = mergeDropInConfig(this._baseDrop, this._speed.tumble?.dropIn);\n this._skipAbort = new AbortController();\n\n // Pick the event triplet for this role. Gravity uses its own channel so\n // listeners can distinguish \"survivors slid into the holes\" from \"new\n // symbols entered\". 'all' and 'new' both emit `cascade:dropIn:*`. they\n // are semantically the same drop-in beat (the 'new' role is just a\n // filtered variant where survivors already landed in stage 1).\n const startEvent = role === 'gravity' ? 'cascade:gravity:start' : 'cascade:dropIn:start';\n const symbolEvent = role === 'gravity' ? 'cascade:gravity:symbol' : 'cascade:dropIn:symbol';\n const endEvent = role === 'gravity' ? 'cascade:gravity:end' : 'cascade:dropIn:end';\n\n // Capture for `onSkip`. the `:start` event was just emitted, so any\n // skip from here must produce the paired `:end` to keep listeners\n // balanced.\n this._events = events;\n this._endEvent = endEvent;\n\n events.emit(startEvent, { reelIndex });\n\n // Gravity, not travel, decides which edge symbols enter from and which\n // edge survivors pack against. `computeDropOffsets` returns absolute cell\n // indices under that gravity, so `perHole` needs no sign of its own.\n const gravity = resolveGravity(this._gravity, axis.direction);\n const sign = gravitySign(gravity);\n const offsets = computeDropOffsets(visible, config.winnerCells, {\n initial: config.initial,\n gravity,\n });\n\n // Build jobs and reset view.y to the pre-drop position. Survivors that\n // don't move (offsetCells === 0) are revealed where placeSymbols left\n // them. Movers are repositioned above the viewport, THEN revealed.\n // this avoids a single-frame flash at the grid position between\n // CascadePlacePhase (snaps view.y) and the first tween frame.\n //\n // Two-stage refill (`role === 'gravity' | 'new'`) skips a subset of\n // movers depending on origin:\n // - 'gravity' . animate survivor-shifters (isNew === false). Keep\n // new-symbol movers (isNew === true) repositioned\n // above the viewport with alpha = 0 so they're ready\n // to drop in stage 2 without a flash.\n // - 'new' . animate new-symbol movers (isNew === true).\n // Survivors that slid in stage 1 are already at\n // their grid Y; reveal them at alpha = 1.\n const jobs: DropJob[] = [];\n // Big symbols: every occupied cell of a block resolves (via getAnchorCell /\n // getSymbolAt) to the SAME anchor view. Animate that view ONCE, driven by\n // the first visible cell of the block (top-to-bottom). Without this the\n // anchor gets one job per occupied cell, so: multiple GSAP tweens fight\n // over its main position (the jitter), `finalMain` is re-read after a\n // sibling job already moved the view to its startMain (wrong landing pos), and\n // per-symbol listeners (landing squish/bounce) fire N times on one view.\n const handledAnchors = new Set<number>();\n for (const off of offsets) {\n const anchorCell = reel.getAnchorCell(off.cell);\n if (anchorCell !== off.cell && handledAnchors.has(anchorCell)) continue;\n handledAnchors.add(anchorCell);\n\n const sym = reel.getSymbolAt(off.cell);\n\n if (off.offsetCells === 0) {\n // Untouched survivor. placeSymbols left it at its final position, visible.\n sym.view.visible = true;\n sym.view.alpha = 1;\n continue;\n }\n\n // Compute the main-axis start for any mover (gravity-correct origin).\n // Grid origins (`originalCell * cellHeight`) are absolute main\n // coordinates already expressed under this gravity, so they need no\n // sign. Fall distances are directional and carry `sign`: the mover\n // always starts on the gravity-entry side and travels toward the exit.\n const finalMain = axis.getMain(sym.view);\n let startMain: number;\n switch (this._drop.distance) {\n case 'auto':\n // `'auto'` = \"every mover falls the full visible-cells distance,\"\n // which is correct for Moment A (every cell is new) and for new\n // arrivals in Moment B (isNew). For a Moment B SURVIVOR\n // (not isNew), 'auto' would teleport the symbol from its\n // actual prior cell up above the viewport, then back down. a\n // visible discontinuity. Fall back to perHole geometry for those\n // movers so the survivor really does slide from its old cell.\n if (!config.initial && !off.isNew) {\n startMain = off.originalCell * cellHeight;\n } else {\n startMain = finalMain - sign * visible * cellHeight;\n }\n break;\n case 'perHole':\n startMain = off.originalCell * cellHeight;\n break;\n default:\n startMain = finalMain - sign * this._drop.distance;\n }\n\n const isNewSymbol = off.isNew;\n const skipForRole =\n (role === 'gravity' && isNewSymbol) ||\n (role === 'new' && !isNewSymbol);\n\n if (skipForRole) {\n if (role === 'gravity' && isNewSymbol) {\n // New symbol awaiting stage 2. invisible (alpha = 0) but parked\n // at the FINAL grid position, not at startMain. placeSymbols already\n // snapped the view to grid; we leave it there so stage 2's\n // `finalMain = axis.getMain(view)` read picks up the correct landing\n // position. (Stage 2 will reposition to startMain for the drop-in tween.)\n sym.view.alpha = 0;\n sym.view.visible = true;\n } else if (role === 'new' && !isNewSymbol) {\n // Survivor already animated by the gravity stage. reveal it\n // where placeSymbols originally targeted (the final grid position).\n axis.setMain(sym.view, finalMain);\n sym.view.alpha = 1;\n sym.view.visible = true;\n }\n continue;\n }\n\n // Move FIRST, then reveal. so the symbol never appears at the grid\n // position during the place→drop handover.\n axis.setMain(sym.view, startMain);\n sym.view.alpha = 1;\n sym.view.visible = true;\n jobs.push({\n cell: off.cell,\n symbol: sym,\n view: sym.view,\n startMain,\n finalMain,\n offsetCells: off.offsetCells,\n });\n }\n this._jobs = jobs;\n\n const finish = (): void => {\n this._timeline = null;\n this._jobs = [];\n events.emit(endEvent, { reelIndex });\n // Null the stored events ref so `onSkip` (if `forceComplete` is\n // called after natural completion) doesn't re-emit `:end` and\n // double-fire on balanced listeners.\n this._events = null;\n // Natural completion: drop the controller un-aborted. Listener\n // tweens scheduled off `cascade:dropIn:symbol` (squish, bounce)\n // are expected to settle on their own timeline.\n this._skipAbort = null;\n // Only stage that lands the reel: 'all' (combined) and 'new' (final\n // stage of two-stage). The gravity stage hands off to the drop-in\n // stage; that's where `notifyLanded` belongs. Landing notification\n // is MOVERS-ONLY (this stage's job cells): untouched survivors must\n // not replay their landing animation on every cascade stage.\n // Gravity movers get their reaction the moment they settle. the\n // reel itself still lands at the final stage.\n if (role === 'gravity') {\n for (const job of jobs) job.symbol.onReelLanded();\n } else {\n reel.notifyLanded(jobs.map((j) => j.cell));\n }\n this._complete();\n };\n\n const dropSec = this._drop.duration / 1000;\n const staggerSec = this._drop.cellStagger / 1000;\n\n if (jobs.length === 0 || dropSec <= 0) {\n // Nothing to animate, or zero-duration recipe. snap and complete.\n for (const job of jobs) axis.setMain(job.view, job.finalMain);\n finish();\n return;\n }\n\n const tl = this._reel.gsap.timeline({ onComplete: finish });\n this._timeline = tl;\n\n // For 'endFirst' order: walk jobs in reverse so the bottom-cell job\n // gets staggerIndex 0 (fires first), the next one up gets 1, etc.\n // Note: `jobs` is already in cell order (top-to-bottom) because offsets\n // are built in that order, so reversing the iteration is correct.\n //\n // The default `'auto'` resolves against gravity, so the stack fills from\n // the gravity-exit end - its \"floor\" - whichever screen edge that is.\n const reverseOrder = resolveCellOrder(this._drop.cellOrder, gravity) === 'endFirst';\n\n for (let i = 0; i < jobs.length; i++) {\n const job = jobs[i];\n const staggerIndex = reverseOrder ? jobs.length - 1 - i : i;\n const offset = staggerIndex * staggerSec;\n\n tl.call(\n () => {\n const signal = this._skipAbort?.signal;\n if (!signal) return;\n events.emit(symbolEvent, {\n symbol: job.symbol,\n view: job.view,\n reelIndex,\n cellIndex: job.cell,\n duration: this._drop.duration,\n ease: this._drop.ease,\n offsetCells: job.offsetCells,\n signal,\n });\n },\n undefined,\n offset,\n );\n\n tl.to(job.view, {\n [axis.mainProp]: job.finalMain,\n duration: dropSec,\n ease: this._drop.ease,\n }, offset);\n }\n }\n\n update(_deltaMs: number): void {}\n\n protected onSkip(): void {\n const axis = this._reel.axis;\n if (this._timeline) {\n this._timeline.kill();\n this._timeline = null;\n }\n // Snap every animating view to its final grid position.\n for (const job of this._jobs) {\n axis.setMain(job.view, job.finalMain);\n job.view.alpha = 1;\n job.view.visible = true;\n }\n this._jobs = [];\n\n // Defensive reveal: the two-stage `role === 'gravity'` path parks\n // new-symbol movers off-viewport at alpha = 0, and those aren't in\n // `_jobs`. A skip during the gravity beat must still reveal the final\n // landed state, so force every visible cell to its grid Y / alpha 1.\n // Cheap belt-and-braces. for `role === 'all' | 'new'` this is a no-op\n // because non-job cells are already revealed.\n const reel = this._reel;\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const sym = reel.getSymbolAt(cell);\n sym.view.alpha = 1;\n sym.view.visible = true;\n }\n\n // Abort BEFORE emitting `:end` so listeners registered on the\n // per-symbol `signal` get the cancellation first. squish/bounce\n // tweens they scheduled off `cascade:dropIn:symbol` must die before\n // `:end` consumers run any landed-state setup that would otherwise\n // collide with mid-air tweens.\n if (this._skipAbort && !this._skipAbort.signal.aborted) {\n this._skipAbort.abort();\n }\n this._skipAbort = null;\n\n // Emit the paired `:end` event so listeners that count start/end\n // events stay balanced across skips. `:start` was already emitted at\n // the top of `onEnter`, so a skip here always has a matching `:start`\n //. no guard needed (unlike `CascadeFallPhase`, where `:start` fires\n // after a configurable delay).\n if (this._events) {\n this._events.emit(this._endEvent, { reelIndex: this._reel.reelIndex });\n this._events = null;\n }\n }\n}\n","import type { gsap } from 'gsap';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\n\nexport interface AdjustPhaseConfig {\n /**\n * Pin overlays on this reel that need to tween from their pre-reshape\n * cell to the post-reshape cell. Populated by `SpinController` BEFORE\n * the reshape commits. `fromMain` captures each overlay's on-screen main\n * coordinate at the moment the snapshot was taken, `toMain` is computed from the new\n * geometry.\n *\n * AdjustPhase no longer commits geometry. `SpinController._applyReshape`\n * does that synchronously before the phase runs. The phase's only job is\n * the tween.\n */\n pinOverlays: PinOverlayTween[];\n}\n\n/**\n * Descriptor for one pin overlay's animation across a MultiWays reshape.\n *\n * @internal. constructed by `SpinController.buildPinOverlayTweens`. Not\n * meant to be hand-built by consumers.\n */\nexport interface PinOverlayTween {\n /** The pin overlay symbol. its view is what we animate. */\n symbol: ReelSymbol;\n /** Cross-axis cell extent. unchanged by a reshape. */\n cellCross: number;\n /** Main-axis cell extent before the reshape (the overlay's current size). */\n oldCellMain: number;\n /** Main-axis cell extent after the reshape. */\n newCellMain: number;\n /** Pre-tween main coordinate, viewport-local. */\n fromMain: number;\n /** Post-tween target main coordinate, viewport-local. */\n toMain: number;\n /** Reel container cross coordinate (unchanged across reshape). */\n cross: number;\n}\n\n/**\n * Tween-only phase between SPIN and STOP for MultiWays slots.\n *\n * The geometry commit (resize symbols, reshape motion) happens in\n * `SpinController._applyReshape` before this phase runs. AdjustPhase only\n * tweens any pin overlays from their pre-reshape cell to the new cell.\n * cell symbols on the strip snap instantly because the reel is still\n * spinning at full speed when this phase runs (tweening cell scale would\n * fight the motion layer).\n *\n * Inserted into the phase chain ONLY when `builder.multiways(...)` is\n * called. Non-MultiWays slots never see this phase.\n *\n * Plays on top of whatever stop staggering you've configured; duration\n * is independent of `stopDelay`.\n */\nexport class AdjustPhase extends ReelPhase<AdjustPhaseConfig> {\n readonly name = 'adjust';\n readonly skippable = true;\n\n private _durationMs: number;\n private _ease: string;\n private _tween: gsap.core.Timeline | null = null;\n private _settle: (() => void) | null = null;\n\n constructor(\n reel: Reel,\n speed: SpeedProfile,\n opts: { durationMs: number; ease?: string },\n ) {\n super(reel, speed);\n this._durationMs = opts.durationMs;\n this._ease = opts.ease ?? 'power2.out';\n }\n\n protected onEnter(config: AdjustPhaseConfig): void {\n const overlays = config.pinOverlays;\n\n if (overlays.length === 0) {\n // SpinController shouldn't construct the phase in this case, but\n // defend in depth.\n this._complete();\n return;\n }\n\n if (this._durationMs <= 0) {\n // Instant snap path. match user's `pinMigrationDuration(0)`.\n this._snapPinOverlays(overlays);\n this._complete();\n return;\n }\n\n // Pose every overlay at its OLD cell visually so the tween starts\n // from where the player last saw it. The overlay's underlying view is\n // already at `newCellMain` after the upstream reshape; we squash the\n // main-axis scale to make it look its old size during the tween.\n const axis = this._reel.axis;\n for (const o of overlays) {\n const size = axis.toScreen(o.cellCross, o.newCellMain);\n o.symbol.resize(size.x, size.y);\n axis.setCross(o.symbol.view, o.cross);\n axis.setMain(o.symbol.view, o.fromMain);\n o.symbol.view.scale[axis.mainProp] =\n o.newCellMain > 0 ? o.oldCellMain / o.newCellMain : 1;\n o.symbol.view.scale[axis.crossProp] = 1;\n }\n\n this._settle = () => {\n for (const o of overlays) {\n o.symbol.view.scale.set(1, 1);\n axis.setMain(o.symbol.view, o.toMain);\n axis.setCross(o.symbol.view, o.cross);\n }\n };\n\n const dur = this._durationMs / 1000;\n const ease = this._ease;\n this._tween = this._reel.gsap.timeline({\n onComplete: () => {\n this._settle?.();\n this._settle = null;\n this._tween = null;\n this._complete();\n },\n });\n\n for (const o of overlays) {\n this._tween.to(o.symbol.view, { [axis.mainProp]: o.toMain, duration: dur, ease }, 0);\n this._tween.to(o.symbol.view.scale, { [axis.mainProp]: 1, duration: dur, ease }, 0);\n }\n }\n\n update(_deltaMs: number): void {\n // GSAP-driven; no per-frame work needed.\n }\n\n protected onSkip(): void {\n if (this._tween) {\n this._tween.progress(1);\n this._tween.kill();\n this._tween = null;\n }\n if (this._settle) {\n this._settle();\n this._settle = null;\n }\n }\n\n private _snapPinOverlays(overlays: PinOverlayTween[]): void {\n const axis = this._reel.axis;\n for (const o of overlays) {\n const size = axis.toScreen(o.cellCross, o.newCellMain);\n o.symbol.resize(size.x, size.y);\n axis.setCross(o.symbol.view, o.cross);\n axis.setMain(o.symbol.view, o.toMain);\n o.symbol.view.scale.set(1, 1);\n }\n }\n}\n","import type { Renderer, Ticker } from 'pixi.js';\nimport type { gsap } from 'gsap';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\nimport type {\n SpeedProfile,\n SymbolData,\n OffsetConfig,\n ReelSetInternalConfig,\n MultiWaysConfig,\n ReelAnchor,\n Stacking,\n} from '../config/types.js';\nimport type { ReelMaskRect, MaskStrategy } from './ReelViewport.js';\nimport {\n MASK_STRATEGY_VERSION,\n RectMaskStrategy,\n SharedRectMaskStrategy,\n} from './ReelViewport.js';\nimport { DEFAULTS } from '../config/defaults.js';\nimport { SpeedPresets } from '../config/SpeedPresets.js';\nimport { ReelSet, type ReelSetParams } from './ReelSet.js';\nimport { Reel, type ReelConfig } from './Reel.js';\nimport { reelAxis, type Orientation, type Direction } from './ReelAxis.js';\nimport type { ReelCurveConfig, ReelCurveInput, CurveFocus, CurveMode } from './ReelCurve.js';\nimport { CURVE_FOCUS_WEIGHT } from './ReelCurve.js';\nimport { ReelViewport } from './ReelViewport.js';\nimport { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport { SymbolFactory } from '../symbols/SymbolFactory.js';\nimport { RandomSymbolProvider } from '../frame/RandomSymbolProvider.js';\nimport type { SymbolPool, SymbolPoolScope } from '../frame/SymbolPool.js';\nimport { FrameBuilder } from '../frame/FrameBuilder.js';\nimport { PhaseFactory } from '../spin/phases/PhaseFactory.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport { StandardMode } from '../spin/modes/StandardMode.js';\nimport type { FrameMiddleware } from '../frame/FrameBuilder.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport { assertBufferCountsInRange, assertColumnTargets } from '../frame/ColumnTarget.js';\nimport {\n V1_BUILDER_METHODS,\n V1_OPTION_KEYS,\n V1_OPTION_VALUES,\n assertNoV1Keys,\n assertNoV1Value,\n renamedMessage,\n} from '../config/v1Renames.js';\nimport type { TumbleConfig, ResolvedTumbleConfig } from '../cascade/TumbleConfig.js';\nimport { resolveTumbleConfig } from '../cascade/TumbleConfig.js';\nimport { CascadeFallPhase } from '../spin/phases/CascadeFallPhase.js';\nimport { CascadePlacePhase } from '../spin/phases/CascadePlacePhase.js';\nimport { CascadeDropInPhase } from '../spin/phases/CascadeDropInPhase.js';\nimport { AdjustPhase } from '../spin/phases/AdjustPhase.js';\n\n/**\n * The configurator you call before every reel set.\n *\n * `ReelSetBuilder` is a fluent, chainable builder: every call returns the\n * builder so you can string setup onto one expression. It hides the\n * twenty-odd subsystems you would otherwise have to wire by hand, and its\n * `.build()` step validates that every required piece is present (throws\n * at construction, not at first spin).\n *\n * Required calls (in any order): `.reels(n)`, `.visibleCells(n)`,\n * `.symbolSize(w, h)`, `.symbols((registry) => ...)`, `.ticker(app.ticker)`.\n * Optional: `.symbolGap()`, `.weights()`, `.symbolData()`, `.speed()`,\n * `.bufferSymbols()`, `.offset()`, `.frameMiddleware()`, `.phases()`,\n * `.spinningMode()`.\n *\n * ```ts\n * const reelSet = new ReelSetBuilder()\n * .reels(5)\n * .visibleCells(3)\n * .symbolSize(200, 200)\n * .symbols((r) => {\n * r.register('cherry', SpriteSymbol, { textures: { cherry: tex } });\n * })\n * .weights({ cherry: 20 })\n * .ticker(app.ticker)\n * .build();\n * ```\n */\nexport class ReelSetBuilder {\n private _reelCount?: number;\n private _visibleCells?: number;\n private _symbolWidth?: number;\n private _symbolHeight?: number;\n private _symbolGap = { ...DEFAULTS.symbolGap };\n private _bufferStart = DEFAULTS.bufferSymbols;\n private _bufferEnd = DEFAULTS.bufferSymbols;\n private _symbolRegistry = new SymbolRegistry();\n private _weights: Record<string, number> = {};\n private _symbolPools: { pool: SymbolPool; scope: SymbolPoolScope }[] = [];\n private _speeds = new Map<string, SpeedProfile>();\n private _initialSpeed = DEFAULTS.initialSpeed;\n private _offset: OffsetConfig = { mode: 'none' };\n private _ticker?: Ticker;\n private _spinningMode: SpinningMode = new StandardMode();\n private _phaseFactory = new PhaseFactory();\n private _middlewares: FrameMiddleware[] = [];\n private _initialFrame?: ColumnTarget[];\n private _symbolDataOverrides: Record<string, Partial<SymbolData>> = {};\n private _tumbleConfig?: ResolvedTumbleConfig;\n private _defaultSpinMode: 'standard' | 'cascade' = 'standard';\n /** Per-reel static cell counts (jagged shapes like 3-5-5-5-3). */\n private _visibleCellsPerReel?: number[];\n /** Per-reel pixel-box heights. used for both pyramids and MultiWays. */\n private _reelExtents?: number[];\n /** Vertical alignment of short reels inside the tallest reel's box. */\n private _reelAnchor: ReelAnchor = 'center';\n /** Render order of cells inside a reel, and of reels inside the set. */\n private _cellStacking: Stacking = 'ascending';\n private _reelStacking: Stacking = 'ascending';\n private _orientation: Orientation = 'vertical';\n private _direction: Direction = 'forward';\n private _directionPerReel?: Direction[];\n private _curve?: ReelCurveInput;\n private _curvePerReel?: ReelCurveInput[];\n private _curveFocus: CurveFocus = 'reel';\n private _curveMode: CurveMode = 'symbol';\n private _curveBleed = 0;\n private _renderer?: Renderer;\n /** MultiWays configuration. Set by `.multiways(...)`. */\n private _multiways?: MultiWaysConfig;\n /** Per-reel AdjustPhase tween duration in ms (MultiWays only). */\n private _pinMigrationDuration: number | ((reelIndex: number) => number) = 200;\n /** GSAP easing string used by AdjustPhase. Default: 'power2.out'. */\n private _pinMigrationEase = 'power2.out';\n /** Mask strategy. Default: per-reel `RectMaskStrategy`. */\n private _maskStrategy: MaskStrategy = new RectMaskStrategy();\n /** True if the user explicitly set a mask strategy (no auto-pick override). */\n private _maskStrategyExplicit = false;\n\n private _gsap: Gsap = DEFAULT_GSAP;\n\n private _rng: () => number = Math.random;\n\n private _poolCapacity?: number;\n\n /**\n * @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.visibleCells}.\n *\n * TypeScript catches a v1 call at compile time, but an untyped consumer\n * would otherwise get \"x.visibleRows is not a function\", which names\n * neither the replacement nor the codemod. These stubs do.\n */\n visibleRows(_count: number): never {\n throw new Error(renamedMessage('ReelSetBuilder', 'visibleRows', V1_BUILDER_METHODS.visibleRows));\n }\n\n /** @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.visibleCellsPerReel}. */\n visibleRowsPerReel(_cells: number[]): never {\n throw new Error(\n renamedMessage('ReelSetBuilder', 'visibleRowsPerReel', V1_BUILDER_METHODS.visibleRowsPerReel),\n );\n }\n\n /** @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.reelExtents}. */\n reelPixelHeights(_heights: number[]): never {\n throw new Error(\n renamedMessage('ReelSetBuilder', 'reelPixelHeights', V1_BUILDER_METHODS.reelPixelHeights),\n );\n }\n\n /** Set number of reel columns. */\n reels(count: number): this {\n this._reelCount = count;\n return this;\n }\n\n /**\n * Number of visible cells per reel (uniform across all reels).\n * Mutually exclusive with `visibleCellsPerReel()`. calling both throws\n * at `build()`.\n *\n * @example\n * builder.reels(5).visibleCells(3) // classic 5x3\n */\n visibleCells(count: number): this {\n this._visibleCells = count;\n return this;\n }\n\n /**\n * Per-reel static cell counts. Length MUST equal `reels()`. Mutually\n * exclusive with `visibleCells()`; calling both throws at `build()`.\n *\n * @example\n * builder.reels(5).visibleCellsPerReel([3, 5, 5, 5, 3]) // pyramid\n */\n visibleCellsPerReel(cells: number[]): this {\n this._visibleCellsPerReel = [...cells];\n return this;\n }\n\n /**\n * Per-reel pixel-box heights. Length MUST equal `reels()`.\n *\n * - Pyramid: defaults to `visibleCellsPerReel[i] * symbolHeight`. Override\n * to make all reels the same height with different cell heights per\n * reel.\n * - MultiWays: every entry equals the same fixed reel height. Cell\n * height per reel is derived as `reelExtent / visibleCells[i]`.\n *\n * Precedence: when both `reelExtents` and `reelAnchor` are set,\n * `reelExtents` wins. anchor is derived from the explicit boxes.\n */\n reelExtents(heights: number[]): this {\n this._reelExtents = [...heights];\n return this;\n }\n\n /** Vertical alignment of short reels inside the tallest reel's box. Default 'center'. */\n reelAnchor(anchor: ReelAnchor): this {\n assertNoV1Value(anchor, V1_OPTION_VALUES['reelAnchor()'], 'reelAnchor()');\n this._reelAnchor = anchor;\n return this;\n }\n\n /**\n * Render order of cells inside each reel. Default `'ascending'`. the cell\n * at the larger main coordinate (bottom for vertical, right for\n * horizontal) draws in front of its neighbour.\n *\n * Geometric on purpose: `direction('reverse')` and per-spin reversal do\n * NOT flip it, so symbol art lit from above keeps overlapping the way the\n * artist drew it. Set `'descending'` if your art wants the opposite.\n */\n cellStacking(order: Stacking): this {\n this._cellStacking = order;\n return this;\n }\n\n /**\n * Render order of reels inside the set. Default `'ascending'`. the last\n * reel draws in front, which reads as \"rightmost on top\" for vertical and\n * \"bottom-most on top\" for horizontal.\n */\n reelStacking(order: Stacking): this {\n this._reelStacking = order;\n return this;\n }\n\n /**\n * Strip travel axis for the whole set. `'vertical'` (default) runs strips on\n * Y with reels marched along X; `'horizontal'` runs them on X with reels\n * marched along Y.\n *\n * Everything else is orientation-neutral: uniform grids, pyramids\n * (`visibleCellsPerReel`), MultiWays, big symbols and cascades all work on\n * either axis from the same arithmetic. `symbolSize(width, height)` stays\n * SCREEN-space, so a horizontal set gives the cell its main extent through\n * `width` where a vertical one uses `height`.\n */\n orientation(orientation: Orientation): this {\n this._orientation = orientation;\n return this;\n }\n\n /**\n * Default travel direction for every reel. `'forward'` (default) heads toward\n * the larger coordinate (down for vertical); `'reverse'` runs the other way\n * (roll-up on a vertical set).\n */\n direction(direction: Direction): this {\n this._direction = direction;\n return this;\n }\n\n /**\n * Per-reel travel direction override (length must equal `reels()`), for\n * alternating-column effects. Reels omitted fall back to `direction()`.\n */\n directionPerReel(directions: Direction[]): this {\n this._directionPerReel = directions;\n return this;\n }\n\n /**\n * Fake the curvature of the reel cylinder on every reel in the set.\n *\n * Cells bunch up and squash toward the window edges the way they would on a\n * real drum, while the middle of the window magnifies slightly because it is\n * the part facing you. It is a per-cell transform, so the art stays crisp,\n * there is no render texture or shader, and a flat set (the default) pays\n * nothing at all.\n *\n * @param curve `0` = flat, `1` = a hard barrel. Pass\n * {@link ReelCurveConfig} to also tune `depth`, the cross-axis narrowing\n * that keeps it reading as a drum rather than a squeezed flat strip.\n *\n * @example\n * builder.curve(0.35);\n * builder.curve({ amount: 0.5, depth: 0.3 });\n */\n curve(curve: ReelCurveInput): this {\n this._curve = curve;\n return this;\n }\n\n /**\n * Per-reel curvature override (length must equal `reels()`). Reels omitted\n * fall back to `curve()`.\n *\n * Use it when the reels are not all the same size, or for the common trick\n * of bending the middle reels harder than the outer ones so the board reads\n * as one wide drum rather than five identical ones.\n *\n * @example\n * builder.curvePerReel([0.2, 0.35, 0.5, 0.35, 0.2]);\n */\n curvePerReel(curves: ReelCurveInput[]): this {\n this._curvePerReel = curves;\n return this;\n }\n\n /**\n * Where the camera looking at the drum sits, across the strip.\n *\n * `'reel'` (default) puts one dead ahead of every reel, so each is its own\n * little drum. `'set'` puts a single camera in front of the middle of the\n * board: cells that rotate away also lean IN toward the centre, and the grid\n * reads as one wide cylinder instead of five identical ones. `'set-lean'` is\n * halfway, which is usually the sweet spot on a 5-wide board.\n *\n * Only has an effect alongside `curve(...)` / `curvePerReel(...)`.\n *\n * **Mask-strategy auto-pick:** leaning cells cross their own column, and the\n * default per-reel {@link RectMaskStrategy} would clip them at the boundary.\n * Anything other than `'reel'` therefore switches the default to\n * {@link SharedRectMaskStrategy}. Passing `.maskStrategy(...)` explicitly\n * always wins.\n *\n * @example\n * builder.curve(0.4).curveFocus('set-lean');\n */\n /**\n * How the curve is drawn.\n *\n * `'symbol'` (default) projects each cell on its own: crisp, free, and a real\n * keystone - but only for symbols whose content IS a texture. A `Container`\n * transform is affine, so a Spine skeleton, a `Graphics`, or a composite\n * subtree can only be displaced and scaled by it, never bent.\n *\n * `'warp'` renders each reel to a texture and draws it through a mesh whose\n * VERTICES are displaced by the projection. Everything inside the reel bends\n * identically - skeletons, atlas sprites, text, effects - and no symbol has\n * to cooperate. It costs one extra render pass per reel per frame and\n * resamples the reel once, so hairline art is marginally softer.\n *\n * `'warp'` requires {@link ReelSetBuilder.renderer}.\n *\n * @example\n * builder.curve(0.5).curveMode('warp').renderer(app.renderer);\n */\n curveMode(mode: CurveMode): this {\n if (mode !== 'symbol' && mode !== 'warp') {\n throw new Error(`curveMode(): expected 'symbol' or 'warp', got \"${mode}\".`);\n }\n this._curveMode = mode;\n return this;\n }\n\n /**\n * The renderer `curveMode('warp')` draws each reel's texture with. Required\n * for warp mode and unused otherwise.\n *\n * @example\n * builder.renderer(app.renderer)\n */\n renderer(renderer: Renderer): this {\n this._renderer = renderer;\n return this;\n }\n\n /**\n * Cross-axis room, in pixels per side, for symbols whose art is WIDER than\n * their cell - an overflowing mystery plate, leaves spilling past the tile.\n *\n * `curveMode('warp')` renders each reel into a texture the size of the reel,\n * so anything hanging over the edge is sliced off at the texture boundary.\n * This gives the texture room, and the overflow is captured, warped with\n * everything else, and sticks out over its neighbours.\n *\n * Costs texture area, so keep it to what the art actually needs. Warp mode\n * only; ignored under `curveMode('symbol')`, where symbols are real display\n * objects and overflow already draws.\n *\n * Pair it with {@link SharedRectMaskStrategy} (or a `curveFocus` other than\n * `'reel'`, which selects it for you) or the per-reel mask clips the\n * overhang straight back off.\n *\n * @example\n * builder.curve(0.45).curveMode('warp').curveBleed(40).renderer(app.renderer);\n */\n curveBleed(pixels: number): this {\n if (!Number.isFinite(pixels) || pixels < 0) {\n throw new Error(`curveBleed(): expected a non-negative number, got ${pixels}.`);\n }\n this._curveBleed = pixels;\n return this;\n }\n\n curveFocus(focus: CurveFocus): this {\n if (!(focus in CURVE_FOCUS_WEIGHT)) {\n throw new Error(\n `curveFocus(): unknown focus \"${focus}\". Expected one of ${Object.keys(CURVE_FOCUS_WEIGHT).join(', ')}.`,\n );\n }\n this._curveFocus = focus;\n return this;\n }\n\n /**\n * Custom mask strategy for the viewport. Defaults to {@link RectMaskStrategy}\n * (one clip rect per reel. clean for pyramid + uniform layouts).\n *\n * Use {@link SharedRectMaskStrategy} when reels have horizontal gaps\n * AND symbols (typically big symbols) need to overlap across reel\n * boundaries. the per-reel default would clip them at the gaps.\n *\n * Or pass any custom `MaskStrategy` for non-rectangular masks (rounded\n * frames, hexagonal grids, etc.).\n *\n * @example\n * import { SharedRectMaskStrategy } from 'pixi-reels';\n * builder.maskStrategy(new SharedRectMaskStrategy())\n */\n maskStrategy(strategy: MaskStrategy): this {\n // TS catches `null`/`undefined` for typed callers, but plain-JS callers\n // get a confusing crash deep inside `ReelViewport` later. Throw here\n // with a name they can grep.\n if (\n strategy == null ||\n typeof strategy.build !== 'function' ||\n typeof strategy.update !== 'function'\n ) {\n throw new Error(\n 'maskStrategy(): expected a MaskStrategy with build(...) and update(...) methods ' +\n '(e.g. new RectMaskStrategy() or new SharedRectMaskStrategy()).',\n );\n }\n // A v1 strategy takes positional (rects, totalWidth, totalHeight) and\n // knows nothing about the axis. Handed a MaskContext it would read\n // `rects` as an object, find no `.length`, and quietly draw a full-bleed\n // rect - a mask that clips nothing, with no error anywhere. Refuse it.\n if (strategy.version !== MASK_STRATEGY_VERSION) {\n throw new Error(\n `maskStrategy(): this strategy declares version ${String(strategy.version)}, ` +\n `but v2 requires ${MASK_STRATEGY_VERSION}. build(ctx) and update(graphics, ctx) now ` +\n 'take a single MaskContext { rects, width, height, axis } instead of positional ' +\n 'arguments, because a per-reel rect means different things on a vertical and a ' +\n 'horizontal set. Add `readonly version = MASK_STRATEGY_VERSION` and read the ' +\n 'context. See the Migrating to 2.0 guide.',\n );\n }\n this._maskStrategy = strategy;\n this._maskStrategyExplicit = true;\n return this;\n }\n\n /**\n * Configure this slot as MultiWays: per-spin cell variation. Pass minCells,\n * maxCells, and the fixed reel pixel height. After build, call\n * `reelSet.setShape(cellsPerReel)` mid-spin to set the next stop's shape.\n *\n * Mutually exclusive with big-symbol registration (`SymbolData.size`).\n * Mutually exclusive with cascade mode in v1.\n */\n multiways(config: MultiWaysConfig): this {\n assertNoV1Keys(config, V1_OPTION_KEYS['multiways()'], 'multiways()');\n this._multiways = { ...config };\n return this;\n }\n\n /**\n * AdjustPhase tween duration in ms (MultiWays only). Pass a number for a\n * uniform duration across reels, or a function `(reelIndex) => number`\n * for per-reel control. Default: 200. Pass `0` for an instant snap (no\n * tween).\n *\n * AdjustPhase plays on top of whatever stop staggering you've configured;\n * its duration is independent of `stopDelay`.\n */\n pinMigrationDuration(value: number | ((reelIndex: number) => number)): this {\n this._pinMigrationDuration = value;\n return this;\n }\n\n /**\n * GSAP easing string used by AdjustPhase tweens (MultiWays only).\n * Applied to both the cell-resize tween and any pin-overlay migration\n * tween. Defaults to `'power2.out'`. See gsap.com/docs/v3/Eases for\n * the full vocabulary.\n *\n * @example\n * builder.pinMigrationEase('back.out(1.4)') // pop-in feel\n * builder.pinMigrationEase('expo.inOut') // slow start + slow end\n */\n pinMigrationEase(ease: string): this {\n this._pinMigrationEase = ease;\n return this;\n }\n\n /** Set symbol dimensions in pixels. */\n symbolSize(width: number, height: number): this {\n this._symbolWidth = width;\n this._symbolHeight = height;\n return this;\n }\n\n /** Set gap between symbols. Default: { x: 0, y: 0 }. */\n symbolGap(x: number, y: number): this {\n this._symbolGap = { x, y };\n return this;\n }\n\n /**\n * Set number of buffer symbols either side of the visible window.\n * Default: 1.\n *\n * `start` is the edge at the smaller main coordinate (above for\n * vertical, left for horizontal) and `end` the larger one. Both are\n * geometric, not travel-relative: flipping a reel's direction never\n * moves a buffer teaser to the opposite edge.\n *\n * Buffer cells are off-screen cells the reel keeps around the visible\n * window so symbols can fade/slide in cleanly. The motion layer's wrap\n * detection assumes at least one buffer cell each side. the minimum\n * supported count is **1**. Passing `0` (or a negative number) is\n * clamped to `1` and a single console warning is printed; the builder\n * does not throw, so existing user code keeps running.\n *\n * **Tumble-only reel sets** may drop the end-window buffer entirely\n * with the object form: `bufferSymbols({ start: 1, end: 0 })`. A pure\n * tumble never scrolls the strip, so nothing ever wraps through the\n * end-window cells. they exist only to be hidden by the mask. This\n * requires `.tumble(...)` on the builder (validated at `build()`), and\n * strip spins (`spin({ mode: 'standard' })`) and `nudge()` throw on\n * such a set. `start` keeps the minimum of 1 (drop-in movers are\n * pre-positioned outside the start edge).\n */\n bufferSymbols(count: number | { start: number; end: number }): this {\n assertNoV1Keys(count, V1_OPTION_KEYS['bufferSymbols()'], 'bufferSymbols()');\n if (typeof count === 'object') {\n this._bufferStart = this._clampBufferMin1(count.start, 'bufferSymbols({ start })');\n this._bufferEnd =\n Number.isFinite(count.end) && count.end >= 0 ? count.end : 0;\n return this;\n }\n const clamped = this._clampBufferMin1(count, `bufferSymbols(${count})`);\n this._bufferStart = clamped;\n this._bufferEnd = clamped;\n return this;\n }\n\n private _clampBufferMin1(count: number, label: string): number {\n if (!Number.isFinite(count) || count < 1) {\n if (!ReelSetBuilder._bufferWarnedThisProcess) {\n ReelSetBuilder._bufferWarnedThisProcess = true;\n // eslint-disable-next-line no-console\n console.warn(\n `[pixi-reels] ${label} is below the minimum of 1; clamping to 1. ` +\n `The motion layer needs at least one buffer cell above (and, outside tumble-only sets, below) the visible window for wrap detection.`,\n );\n }\n return 1;\n }\n return count;\n }\n /** One-shot guard so we don't spam consoles when builders are constructed in a loop. */\n private static _bufferWarnedThisProcess = false;\n\n /** Configure symbols via a registry callback. */\n symbols(configurator: (registry: SymbolRegistry) => void): this {\n configurator(this._symbolRegistry);\n return this;\n }\n\n /** Set weights for random symbol generation. */\n weights(weights: Record<string, number>): this {\n this._weights = weights;\n return this;\n }\n\n /**\n * Narrow what the engine may draw when it fills a cell you did not name.\n *\n * `weights()` sets the base table for every reel; this layers pools on\n * top of it, so a symbol can be common on the strip and impossible in\n * the buffer cells, or heavy on one reel only. Call it once per scope.\n *\n * Buffer pools apply ON TOP of the spinning ones (see `SymbolPoolScope`),\n * and the same pools are reachable at run time as\n * `reelSet.randomSymbols`, which is where a game mode switch belongs.\n *\n * @example\n * .randomSymbols({ exclude: ['EMPTY'] }) // every reel\n * .randomSymbols({ exclude: ['COIN'] }, { slots: 'buffer' }) // buffers only\n * .randomSymbols({ weights: { WILD: 40 } }, { reel: 2 }) // reel 2 only\n */\n randomSymbols(pool: SymbolPool, scope: SymbolPoolScope = {}): this {\n this._symbolPools.push({ pool, scope });\n return this;\n }\n\n /**\n * Per-symbol metadata overrides (zIndex, unmask, or a custom weight that\n * replaces the one from `weights()`). Merged into the final symbolsData map;\n * any field you don't specify falls back to the default.\n *\n * `zIndex` sorts within ONE reel's container only. it can never lift a\n * symbol above the reel to its right (reels are separate containers).\n * Cross-reel and out-of-mask layering needs `unmask: true`, which is an\n * **at-rest** presentation: while the reel spins the symbol stays masked\n * like everything else; on land, visible-cell instances are lifted into\n * the viewport-wide `unmaskedContainer` (above every reel and the mask)\n * and pulled back down when the next spin starts.\n *\n * @example\n * .symbolData({\n * wild: { zIndex: 5 }, // above reel-mates (same reel only)\n * bonus: { zIndex: 10, unmask: true }, // landed: above all reels + mask\n * })\n */\n symbolData(overrides: Record<string, Partial<SymbolData>>): this {\n for (const [id, data] of Object.entries(overrides ?? {})) {\n assertNoV1Keys(data?.size, V1_OPTION_KEYS['symbolData() size'], `symbolData('${id}').size`);\n }\n this._symbolDataOverrides = { ...this._symbolDataOverrides, ...overrides };\n return this;\n }\n\n /** Add a named speed profile. */\n speed(name: string, profile: SpeedProfile): this {\n this._speeds.set(name, profile);\n return this;\n }\n\n /** Set which speed profile to use initially. Default: 'normal'. */\n initialSpeed(name: string): this {\n this._initialSpeed = name;\n return this;\n }\n\n /** Set X-axis offset config (e.g., trapezoid perspective). Default: 'none'. */\n offsetConfig(config: OffsetConfig): this {\n assertNoV1Keys(config, V1_OPTION_KEYS['offset() trapezoid'], 'offsetConfig()');\n this._offset = config;\n return this;\n }\n\n /** Set the PixiJS ticker for frame updates. */\n ticker(ticker: Ticker): this {\n this._ticker = ticker;\n return this;\n }\n\n /**\n * Inject the source of randomness used to fill the scrolling strip (buffer\n * fill, the symbols shown during SPIN before `setResult` lands, nudge\n * padding). Must return a value in [0, 1). Default: `Math.random`.\n *\n * **Why you'd set this:** server-authoritative *outcomes* do not make the\n * on-screen strip reproducible — the symbols a player sees scrolling are\n * drawn from this RNG. Injecting a seeded, audited PRNG lets you replay the\n * exact visual sequence from a seed, which provably-fair and regulated\n * real-money deployments are eventually required to produce.\n *\n * @example\n * import { ReelSetBuilder } from 'pixi-reels';\n * const seeded = mulberry32(serverSeed); // your audited PRNG\n * const reelSet = new ReelSetBuilder().reels(5).visibleCells(3)\n * .symbols(...).ticker(app.ticker).rng(seeded).build();\n */\n rng(fn: () => number): this {\n this._rng = fn;\n return this;\n }\n\n /**\n * Override the per-symbol-id recycle-pool capacity. By default the engine\n * sizes the pool to the whole strip (every visible + buffer cell), so even a\n * grid that is briefly all one symbol recycles instead of churning through\n * `destroy()` + recreate. Set this only to cap memory on very large grids, or\n * to raise headroom for unusually heavy simultaneous symbol swaps.\n */\n poolCapacity(maxPerSymbol: number): this {\n this._poolCapacity = maxPerSymbol;\n return this;\n }\n\n /**\n * Inject the GSAP instance the engine should use for tweens.\n *\n * **When you need this:** if your app already imports `gsap` and your\n * bundler resolves `gsap` to a different module instance than the one\n * `pixi-reels` resolved (common with symlinked workspaces, npm-link, or\n * misconfigured `dedupe`), every tween you start on a target the engine\n * also tweens will fight a separate timeline. Symptoms: spotlights that\n * render but never finish, animations that double-fire, tweens that\n * silently drop on hidden tabs in only one of the two instances.\n *\n * Calling `.gsap(myGsap)` binds every phase, motion tween, symbol\n * pin-flight tween, and SpriteSymbol win pulse to the GSAP you pass.\n * guaranteed to be the same instance that drives your own animations.\n *\n * Default: the `gsap` import resolved at the engine's own\n * `node_modules/gsap` path. If your app and the engine resolve to the\n * same instance (the common case in production bundles with proper\n * `dedupe`), you do NOT need to call this.\n *\n * **Per reel set, not process-wide.** v1 stored one instance in a module\n * global, so the last `.gsap()` call before any `build()` silently won for\n * every set. Each set now captures the instance at `build()` time, so a\n * composed stage can drive two sets from different instances. Pass the\n * same instance to `driveGsapWithTicker(ticker, instance)`.\n *\n * Read at `build()`. calling it afterwards does not move an existing set.\n *\n * @example\n * import { gsap } from 'gsap';\n * const reelSet = new ReelSetBuilder()\n * .reels(5).visibleCells(3).symbolSize(200, 200)\n * .symbols(...)\n * .ticker(app.ticker)\n * .gsap(gsap) // ensure engine and app share one instance\n * .build();\n */\n gsap(instance: typeof gsap): this {\n this._gsap = instance;\n return this;\n }\n\n /** Set the spinning mode. Default: StandardMode. */\n spinningMode(mode: SpinningMode): this {\n this._spinningMode = mode;\n return this;\n }\n\n /** Add custom frame middleware. */\n frameMiddleware(middleware: FrameMiddleware): this {\n this._middlewares.push(middleware);\n return this;\n }\n\n /** Override default phases. */\n phases(configurator: (factory: PhaseFactory) => void): this {\n configurator(this._phaseFactory);\n return this;\n }\n\n /**\n * Enable tumble cascade mechanics. Replaces strip-spin + bounce-stop with\n * a three-phase pipeline:\n *\n * 1. **`cascade:fall`**. on `spin()`, existing visible symbols fall\n * off the bottom of the viewport.\n * 2. **`cascade:place`**. when `setResult()` arrives, new symbol\n * identities swap into the buffer at their final grid positions.\n * 3. **`cascade:dropIn`**. new symbols animate from above (and\n * survivors slide down to fill holes) into the grid.\n *\n * For a Moment B refill after wins are cleared, call\n * `reelSet.refill({ winners, grid })`. that skips fall + wait and runs\n * `place` + `dropIn` only, with gravity-correct geometry driven by the\n * `winners` list (untouched symbols don't animate; survivors slide;\n * new symbols come from above).\n *\n * Every phase boundary fires a `cascade:*` event on\n * `reelSet.events`. per-symbol events (`cascade:fall:symbol` /\n * `cascade:dropIn:symbol`) carry the symbol, view, and the timing the\n * library is about to apply, so listeners can run parallel tweens on\n * any other property in sync with the library's `view.y` motion.\n *\n * Override any individual phase via `.phases(f => f.register('cascade:fall', MyPhase))`.\n *\n * @example\n * builder.tumble({\n * fall: { duration: 300, ease: 'sine.in', cellStagger: 60 },\n * dropIn: { duration: 600, ease: 'power2.out', cellStagger: 60, distance: 'perHole' },\n * });\n */\n tumble(config?: TumbleConfig): this {\n const tumbleKeys = V1_OPTION_KEYS['tumble() fall/dropIn'];\n assertNoV1Keys(config?.fall, tumbleKeys, 'tumble({ fall })');\n assertNoV1Keys(config?.dropIn, tumbleKeys, 'tumble({ dropIn })');\n assertNoV1Value(\n config?.fall?.cellOrder,\n V1_OPTION_VALUES['tumble() cellOrder'],\n 'tumble({ fall: { cellOrder } })',\n );\n assertNoV1Value(\n config?.dropIn?.cellOrder,\n V1_OPTION_VALUES['tumble() cellOrder'],\n 'tumble({ dropIn: { cellOrder } })',\n );\n this._tumbleConfig = resolveTumbleConfig(config);\n this._defaultSpinMode = 'cascade';\n return this;\n }\n\n /**\n * Set the initial symbol grid the reels show before the first spin.\n *\n * One `ColumnTarget` per reel. `visible` lists the symbols in the visible\n * window; optional `bufferStart` / `bufferEnd` prefill cells outside it\n * (`[0]` is the slot closest to the visible window, later indices go\n * further out).\n *\n * @example\n * builder.initialFrame([\n * { visible: ['A','B','C'] },\n * { visible: ['A','B','C'], bufferStart: ['COIN'] },\n * { visible: ['A','B','C'], bufferEnd: ['SCATTER'] },\n * ]);\n */\n initialFrame(frame: ColumnTarget[]): this {\n assertColumnTargets(frame, 'initialFrame()');\n const columnKeys = V1_OPTION_KEYS['initialFrame() / setResult() column'];\n for (let i = 0; i < (frame?.length ?? 0); i++) {\n assertNoV1Keys(frame[i], columnKeys, `initialFrame() column ${i}`);\n }\n // Stored un-materialized so `build()` can validate it against the\n // final bufferSymbols config. Builder methods are order-free, so\n // `bufferSymbols()` may not have been called yet when `initialFrame()`\n // runs.\n this._initialFrame = frame;\n return this;\n }\n\n /** Build the ReelSet. Validates configuration and assembles all internal objects. */\n build(): ReelSet {\n this._validate();\n\n if (this._directionPerReel && this._directionPerReel.length !== this._reelCount) {\n throw new Error(\n `directionPerReel() length (${this._directionPerReel.length}) must equal reels() (${this._reelCount}).`,\n );\n }\n if (this._curveMode === 'warp' && !this._renderer) {\n throw new Error(\n \"curveMode('warp') renders each reel to a texture, so it needs a renderer: \" +\n 'add .renderer(app.renderer). Use the default curveMode(\\'symbol\\') if you ' +\n 'do not have one.',\n );\n }\n if (this._curvePerReel && this._curvePerReel.length !== this._reelCount) {\n throw new Error(\n `curvePerReel() length (${this._curvePerReel.length}) must equal reels() (${this._reelCount}).`,\n );\n }\n const reelCount = this._reelCount!;\n const symbolWidth = this._symbolWidth!;\n const symbolHeight = this._symbolHeight!;\n const bufferStart = this._bufferStart;\n const bufferEnd = this._bufferEnd;\n if (bufferEnd === 0 && this._defaultSpinMode !== 'cascade') {\n throw new Error(\n 'bufferSymbols({ end: 0 }) is tumble-only: the strip machinery wraps ' +\n 'symbols through the below-window buffer. Add .tumble(...) to the ' +\n 'builder, or keep bufferEnd >= 1.',\n );\n }\n const ticker = this._ticker!;\n const isMultiWays = !!this._multiways;\n\n // Set-level axis projection. `main` is the strip travel axis (Y vertical,\n // X horizontal), `cross` is the reel-marching axis. Symbol art always sizes\n // to screen (symbolWidth, symbolHeight); only the strip/marching geometry\n // swaps. Identity for vertical.\n const vertical = this._orientation === 'vertical';\n const setAxis = reelAxis(this._orientation, 'forward');\n const mainCellSize = vertical ? symbolHeight : symbolWidth;\n const crossCellSize = vertical ? symbolWidth : symbolHeight;\n const mainGap = vertical ? this._symbolGap.y : this._symbolGap.x;\n const crossGap = vertical ? this._symbolGap.x : this._symbolGap.y;\n\n // Resolve per-reel cell counts. MultiWays: every reel starts at maxCells.\n let visibleCellsPerReel: number[];\n if (isMultiWays) {\n visibleCellsPerReel = new Array(reelCount).fill(this._multiways!.maxCells);\n } else if (this._visibleCellsPerReel) {\n visibleCellsPerReel = this._visibleCellsPerReel;\n } else {\n const v = this._visibleCells!;\n visibleCellsPerReel = new Array(reelCount).fill(v);\n }\n\n // Per-reel MAIN-AXIS extent (the strip length): pixel height for a\n // vertical set, pixel width for a horizontal one. `reelExtents([...])`\n // and `multiways({ reelExtent })` are both main-axis values, which is\n // what lets a pyramid or MultiWays set run sideways from exactly the\n // same arithmetic.\n let reelExtents: number[];\n if (isMultiWays) {\n reelExtents = new Array(reelCount).fill(this._multiways!.reelExtent);\n } else if (this._reelExtents) {\n reelExtents = this._reelExtents;\n } else {\n reelExtents = visibleCellsPerReel.map(\n (cells) => cells * mainCellSize + (cells - 1) * mainGap,\n );\n }\n const mainExtents = reelExtents;\n\n // Compute per-reel main offset and target cell height.\n // SPIN-time uniform cell height equals the configured `symbolHeight`.\n const tallest = Math.max(...mainExtents);\n const mainOffsets = mainExtents.map((h) => {\n switch (this._reelAnchor) {\n case 'start': return 0;\n case 'end': return tallest - h;\n case 'center':\n default: return (tallest - h) / 2;\n }\n });\n // Per-reel MAIN cell extent, derived by dividing the reel's extent by\n // its cell count (minus the inter-cell gaps).\n const perReelCellSize: number[] = reelExtents.map((extent, i) => {\n const cells = visibleCellsPerReel[i];\n return (extent - (cells - 1) * mainGap) / cells;\n });\n // SPIN-time uniform main cell extent. Every reel uses this while the\n // strip is scrolling, regardless of its post-AdjustPhase shape.\n const spinCellSize = mainCellSize;\n const initialCellSize = isMultiWays\n ? new Array(reelCount).fill(spinCellSize)\n : perReelCellSize;\n\n if (this._speeds.size === 0) {\n this._speeds.set('normal', SpeedPresets.NORMAL);\n }\n\n const symbolsData: Record<string, SymbolData> = {};\n const symbolIds = this._symbolRegistry.symbolIds;\n for (const id of symbolIds) {\n const override = this._symbolDataOverrides[id] ?? {};\n symbolsData[id] = {\n weight: override.weight ?? this._weights[id] ?? 10,\n zIndex: override.zIndex ?? 1,\n unmask: override.unmask,\n size: override.size,\n };\n }\n\n const config: ReelSetInternalConfig = {\n grid: {\n reelCount,\n visibleCells: this._visibleCells ?? visibleCellsPerReel[0],\n symbolWidth,\n symbolHeight,\n symbolGap: { ...this._symbolGap },\n bufferSymbols: this._bufferStart,\n bufferEnd: this._bufferEnd,\n visibleCellsPerReel,\n reelExtents,\n reelAnchor: this._reelAnchor,\n multiways: this._multiways,\n },\n symbols: symbolsData,\n speeds: this._speeds,\n initialSpeed: this._initialSpeed,\n offset: this._offset,\n ticker,\n };\n\n // Pool cap per symbol id. The worst case for a single id is the whole\n // strip (every visible + buffer cell) showing it at once, so size the pool\n // to that to avoid destroy()+recreate churn on large/MultiWays grids. A\n // floor of 20 preserves headroom for small grids; an explicit\n // .poolCapacity() overrides the derivation.\n const totalStripCells = visibleCellsPerReel.reduce(\n (sum, cells) => sum + cells + bufferStart + bufferEnd,\n 0,\n );\n const poolCapacity = this._poolCapacity ?? Math.max(20, totalStripCells);\n const symbolFactory = new SymbolFactory(\n this._symbolRegistry,\n poolCapacity,\n this._gsap,\n setAxis.mainProp,\n );\n const randomProvider = new RandomSymbolProvider(symbolsData, this._rng);\n for (const { pool, scope } of this._symbolPools) {\n randomProvider.set(pool, scope);\n }\n const frameBuilder = new FrameBuilder(randomProvider);\n\n for (const mw of this._middlewares) {\n frameBuilder.use(mw);\n }\n\n // Wire the three tumble cascade phases under their named keys. The\n // defaults registered here can be overridden via `.phases(...)` after\n // `.tumble(...)` was called. The default spin mode flips to 'cascade'\n // when `.tumble()` ran.\n if (this._tumbleConfig) {\n const fall = this._tumbleConfig.fall;\n const drop = this._tumbleConfig.dropIn;\n // Gravity stays UNRESOLVED here: `'auto'` has to be read against each\n // reel's own axis, and `directionPerReel` lets those differ inside one\n // set. The phases resolve it per reel at run time.\n const gravity = this._tumbleConfig.gravity;\n this._phaseFactory.registerFactory('cascade:fall', (reel, speed) => new CascadeFallPhase(reel, speed, fall, gravity));\n this._phaseFactory.registerFactory('cascade:place', (reel, speed) => new CascadePlacePhase(reel, speed, gravity));\n this._phaseFactory.registerFactory('cascade:dropIn', (reel, speed) => new CascadeDropInPhase(reel, speed, drop, gravity));\n }\n\n // MultiWays: wire AdjustPhase. Stay out of non-MultiWays chains entirely\n // so the default `start → spin → stop` flow is unchanged for them.\n if (isMultiWays) {\n const adjustDur = this._pinMigrationDuration;\n const pinMigrationEase = this._pinMigrationEase;\n this._phaseFactory.registerFactory('adjust', (reel, speed) => {\n const ms = typeof adjustDur === 'function' ? adjustDur(reel.reelIndex) : adjustDur;\n return new AdjustPhase(reel, speed, { durationMs: ms, ease: pinMigrationEase });\n });\n }\n\n // Create viewport. width covers all reels, height covers tallest box.\n // Viewport spans the cross axis across all reels and the main axis over the\n // tallest strip, projected to screen. Vertical: (crossSpan, mainSpan).\n const crossSpan = reelCount * (crossCellSize + crossGap) - crossGap;\n const viewportSize = setAxis.toScreen(crossSpan, tallest);\n const viewportWidth = viewportSize.x;\n const viewportHeight = viewportSize.y;\n\n // Auto-pick `SharedRectMaskStrategy` when the layout has horizontal\n // gaps AND any registered symbol needs to span across reel boundaries:\n //\n // - **big symbols** (footprint w > 1 or h > 1). the per-reel mask\n // would clip cross-reel big symbols at every column gap (visible\n // vertical strips through the symbol), so we share a single mask.\n // - **unmasked symbols** (`SymbolData.unmask: true`). these render\n // above the per-reel mask anyway, but neighboring (masked)\n // symbols still get clipped at the gap. Players see a\n // half-cropped neighbor next to the unmasked overlay. Sharing\n // one mask removes the gap stripe.\n //\n // Explicit `.maskStrategy(...)` calls always win.\n const hasBigSymbols = Object.values(symbolsData).some(\n (d) => d.size && (d.size.reels > 1 || d.size.cells > 1),\n );\n const hasUnmaskedSymbols = Object.values(symbolsData).some((d) => d.unmask);\n\n // Unmask works on jagged/pyramid layouts (non-zero reel `mainOffset`) too:\n // unmask is an at-rest presentation, so a lifted view only exists while\n // the reel is stopped, and `Reel._syncUnmaskedViewOffsets()` re-bakes\n // `container.y` after every absolute motion snap. No config-time guard.\n\n if (\n !this._maskStrategyExplicit &&\n (hasBigSymbols || hasUnmaskedSymbols) &&\n crossGap > 0\n ) {\n this._maskStrategy = new SharedRectMaskStrategy();\n // Heads-up so devs see the auto-pick in their console.\n const reason = hasBigSymbols\n ? 'big symbols are registered'\n : 'one or more symbols use `unmask: true`';\n // eslint-disable-next-line no-console\n console.info(\n `[pixi-reels] auto-selected SharedRectMaskStrategy because ${reason} ` +\n `and the cross-axis gap (symbolGap.${vertical ? 'x' : 'y'}) is > 0. ` +\n 'Pass .maskStrategy(...) explicitly to override.',\n );\n }\n\n // A set-focused curve makes receding cells LEAN toward the middle of the\n // board, which walks them out of their own column. The per-reel mask would\n // slice that overhang off at the boundary, so share one mask. Unlike the\n // cases above this does not need a cross gap - the lean crosses the column\n // edge whether or not there is a gap there.\n const curveLeans =\n this._curveFocus !== 'reel' && (this._curve !== undefined || this._curvePerReel !== undefined);\n if (!this._maskStrategyExplicit && curveLeans) {\n this._maskStrategy = new SharedRectMaskStrategy();\n // eslint-disable-next-line no-console\n console.info(\n `[pixi-reels] auto-selected SharedRectMaskStrategy because curveFocus('${this._curveFocus}') ` +\n 'leans cells across their own reel column. Pass .maskStrategy(...) explicitly to override.',\n );\n }\n\n // Warp draws each reel through a texture, so anything the engine LIFTS out\n // of a reel container is not in that texture and is not bent: it draws\n // flat, over a curved board. Unmask is the one a game asks for by name, so\n // say so rather than let it look like a curve bug.\n if (this._curveMode === 'warp' && this._curve !== undefined && hasUnmaskedSymbols) {\n // eslint-disable-next-line no-console\n console.info(\n \"[pixi-reels] curveMode('warp') does not bend symbols with `unmask: true`. \" +\n 'They are lifted into `viewport.unmaskedContainer`, outside the reel texture, ' +\n 'so they render FLAT above a curved board. The same applies to the win ' +\n 'spotlight and pin overlays. Use curveMode(\\'symbol\\') if those have to follow ' +\n 'the drum.',\n );\n }\n\n // Reel-local cross coordinate each reel's perspective converges on. At\n // weight 0 that is the reel's own centreline; at 1 it is the middle of the\n // whole board, expressed in that reel's coordinates.\n const curveFocusWeight = CURVE_FOCUS_WEIGHT[this._curveFocus];\n const setCentreCross = crossSpan / 2;\n\n const viewport = new ReelViewport(\n viewportWidth,\n viewportHeight,\n undefined,\n this._maskStrategy,\n setAxis,\n this._curveMode === 'warp' ? this._curveBleed : 0,\n );\n\n // Validate the initial frame now that buffer counts are fully resolved.\n // `initialFrame()` stores the raw `ColumnTarget[]` so the validator runs\n // against the final bufferSymbols config.\n if (this._initialFrame) {\n const bufferAboveArr = new Array(reelCount).fill(bufferStart);\n const bufferBelowArr = new Array(reelCount).fill(bufferEnd);\n assertBufferCountsInRange(\n this._initialFrame,\n bufferAboveArr,\n bufferBelowArr,\n 'initialFrame',\n );\n }\n\n // Create reels with per-reel geometry.\n const reels: Reel[] = [];\n const maskRects: ReelMaskRect[] = [];\n for (let reelIndex = 0; reelIndex < reelCount; reelIndex++) {\n const cells = visibleCellsPerReel[reelIndex];\n // Project this reel's (main, cross) cell extents back to the screen\n // pair `Reel` stores. For vertical that is (symbolWidth, cellMain) as\n // before; for horizontal the per-reel value lands on WIDTH instead,\n // which is what makes a sideways pyramid work.\n const cellScreen = setAxis.toScreen(crossCellSize, initialCellSize[reelIndex]);\n\n // Per-reel initial frame at its own visibleCells count.\n const initialFrame = frameBuilder.build(\n reelIndex,\n cells,\n bufferStart,\n bufferEnd,\n this._initialFrame?.[reelIndex],\n );\n\n const reelConfig: ReelConfig = {\n reelIndex,\n visibleCells: cells,\n bufferStart,\n bufferEnd,\n symbolWidth: cellScreen.x,\n symbolHeight: cellScreen.y,\n symbolGapX: this._symbolGap.x,\n symbolGapY: this._symbolGap.y,\n symbolsData,\n initialSymbols: initialFrame,\n mainOffset: mainOffsets[reelIndex],\n extent: reelExtents[reelIndex],\n spinCellSize,\n axis: reelAxis(this._orientation, this._directionPerReel?.[reelIndex] ?? this._direction),\n curve: this._curvePerReel?.[reelIndex] ?? this._curve,\n curveRenderer: this._curveMode === 'warp' ? this._renderer : undefined,\n curveTicker: this._curveMode === 'warp' ? ticker : undefined,\n curveBleed: this._curveBleed,\n curveFocus:\n curveFocusWeight === 0\n ? undefined\n : crossCellSize / 2 +\n curveFocusWeight *\n (setCentreCross - reelIndex * (crossCellSize + crossGap) - crossCellSize / 2),\n cellStacking: this._cellStacking,\n reelStacking: this._reelStacking,\n gsap: this._gsap,\n };\n\n const reel = new Reel(reelConfig, symbolFactory, randomProvider, viewport);\n reels.push(reel);\n // Per-reel mask rect: cross position marches the reels, main position is\n // the reel's own offset, cross size is one cell, main size is the strip.\n const rectPos = setAxis.toScreen(reelIndex * (crossCellSize + crossGap), mainOffsets[reelIndex]);\n const rectSize = setAxis.toScreen(crossCellSize, mainExtents[reelIndex]);\n maskRects.push({\n x: rectPos.x,\n y: rectPos.y,\n width: rectSize.x,\n height: rectSize.y,\n });\n }\n viewport.updateMaskSize(viewportWidth, viewportHeight, maskRects);\n\n const params: ReelSetParams = {\n config,\n reels,\n viewport,\n symbolFactory,\n frameBuilder,\n phaseFactory: this._phaseFactory,\n spinningMode: this._spinningMode,\n defaultSpinMode: this._defaultSpinMode,\n };\n\n return new ReelSet(params);\n }\n\n private _validate(): void {\n const errors: string[] = [];\n\n if (this._reelCount === undefined || this._reelCount <= 0) {\n errors.push('reels() must be called with a positive number.');\n }\n\n const hasShape = !!this._visibleCellsPerReel;\n const hasUniform = this._visibleCells !== undefined;\n const hasMega = !!this._multiways;\n\n if (!hasMega && !hasUniform && !hasShape) {\n errors.push('one of visibleCells(n) or visibleCellsPerReel([...]) or multiways({...}) must be called.');\n }\n if (hasUniform && hasShape) {\n errors.push('cannot call both visibleCells() and visibleCellsPerReel(). pick one.');\n }\n if (hasMega && hasShape) {\n errors.push('cannot combine multiways() with visibleCellsPerReel(). MultiWays shapes are server-driven.');\n }\n\n if (this._reelCount && hasShape && this._visibleCellsPerReel!.length !== this._reelCount) {\n errors.push(\n `visibleCellsPerReel length ${this._visibleCellsPerReel!.length} must equal reels(${this._reelCount}).`,\n );\n }\n if (hasShape) {\n for (let i = 0; i < this._visibleCellsPerReel!.length; i++) {\n if (this._visibleCellsPerReel![i] <= 0) {\n errors.push(`visibleCellsPerReel[${i}] = ${this._visibleCellsPerReel![i]} must be positive.`);\n break;\n }\n }\n }\n if (this._reelCount && this._reelExtents && this._reelExtents.length !== this._reelCount) {\n errors.push(\n `reelExtents length ${this._reelExtents.length} must equal reels(${this._reelCount}).`,\n );\n }\n\n if (hasMega) {\n const m = this._multiways!;\n if (m.minCells <= 0 || m.maxCells <= 0) {\n errors.push('multiways({minCells, maxCells}) must both be positive.');\n } else if (m.minCells > m.maxCells) {\n errors.push(`multiways: minCells ${m.minCells} cannot exceed maxCells ${m.maxCells}.`);\n }\n if (m.reelExtent <= 0) {\n errors.push('multiways({reelExtent}) must be positive.');\n }\n // multiways({reelExtent}) sets a uniform reel-pixel height for\n // every reel; reelExtents([...]) sets per-reel heights for\n // pyramid layouts. Setting both is ambiguous. fail loud.\n if (this._reelExtents) {\n errors.push(\n 'cannot combine multiways({reelExtent}) with reelExtents([...]). ' +\n 'multiways slots use a uniform reel pixel height. Drop reelExtents() or ' +\n 'remove the multiways() configuration.',\n );\n }\n // Big symbols are mutually exclusive with MultiWays.\n for (const id of this._symbolRegistry.symbolIds) {\n const override = this._symbolDataOverrides[id] ?? {};\n if (override.size && (override.size.reels > 1 || override.size.cells > 1)) {\n errors.push(\n `big symbol '${id}' (size ${override.size.reels}x${override.size.cells}) cannot be ` +\n 'registered on a MultiWays slot. Drop multiways() or remove the size metadata.',\n );\n break;\n }\n }\n }\n\n // Big symbols (size > 1x1) are placed by the server at anchor cells\n // only. random fill skips them in v1 (a 2x2 with a non-zero weight\n // would silently never get picked, since RandomFillMiddleware can't\n // place blocks). Throw to surface the misunderstanding.\n for (const id of this._symbolRegistry.symbolIds) {\n const override = this._symbolDataOverrides[id] ?? {};\n const size = override.size;\n if (!size || (size.reels === 1 && size.cells === 1)) continue;\n const weight = override.weight ?? this._weights[id];\n if (weight !== undefined && weight > 0) {\n errors.push(\n `big symbol '${id}' (size ${size.reels}x${size.cells}) must have weight 0. ` +\n 'big symbols are placed by the server at anchor cells only and never enter ' +\n 'random fill. Set weight to 0 (or omit it) and place the symbol via setResult().',\n );\n }\n // Cross-reel blocks vs per-reel direction (ADR 016 section 6.7). The\n // coordinator reads buffer geometry off reel 0 and paints stubs under\n // one shared \"start = above the window\" convention. With mixed\n // directions the reels a block spans can feed from opposite edges, so\n // a stub would land on the wrong side of the window on some of them.\n // Fail at build() rather than ship a block that splits at run time.\n if (size.reels > 1 && this._directionPerReel) {\n const distinct = new Set(this._directionPerReel);\n if (distinct.size > 1) {\n errors.push(\n `big symbol '${id}' spans ${size.reels} reels, which is not supported ` +\n 'together with mixed directionPerReel([...]). The cross-reel coordinator ' +\n 'assumes one shared feed edge for every reel a block covers. Use a single ' +\n 'direction() for the set, or keep blocks within one reel (size.reels === 1).',\n );\n }\n }\n }\n\n if (this._visibleCells !== undefined && this._visibleCells <= 0) {\n errors.push('visibleCells() must be called with a positive number.');\n }\n if (this._symbolWidth === undefined || this._symbolHeight === undefined) {\n errors.push('symbolSize() must be called with width and height.');\n }\n if (this._symbolRegistry.size === 0) {\n errors.push('symbols() must register at least one symbol.');\n }\n if (!this._ticker) {\n errors.push('ticker() must be called with a PixiJS Ticker.');\n }\n if (this._speeds.size > 0 && !this._speeds.has(this._initialSpeed)) {\n errors.push(\n `initialSpeed '${this._initialSpeed}' does not match any registered speed profile. ` +\n `Available: ${[...this._speeds.keys()].join(', ')}`,\n );\n }\n\n if (errors.length > 0) {\n throw new Error(`ReelSetBuilder validation failed:\\n - ${errors.join('\\n - ')}`);\n }\n }\n}\n","import { Container, Graphics, Text, Ticker } from 'pixi.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { Reel } from '../core/Reel.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { TickerRef } from '../utils/TickerRef.js';\n\n/**\n * A single visual debug layer.\n *\n * - `mask` Mask bounding box + per-reel rects.\n * - `cells` Every visible cell from `getCellBounds`, with `reel,cell` labels.\n * - `buffers` The off-window strip cells (bufferStart / bufferEnd), dimmer.\n * - `axis` One arrow per reel along the travel axis, pointing the way\n * it goes. The whole point of the v2 refactor is invisible in\n * a canvas otherwise: reverse polarity and horizontal\n * orientation become obvious instead of inferred.\n * - `feed` A marker on the strip edge new symbols enter from.\n * Confirms `feedEdge` derives from polarity rather than\n * being set twice.\n * - `thresholds` The wrap lines. a symbol crossing one wraps to the other\n * end of the array (contract law L7 / L9, watchable).\n * - `bounds` Actual `view.getBounds()` per visible symbol (spine overrun).\n * - `blocks` `getBlockBounds` outline for big symbols.\n * - `pins` Pin cells and pin-overlay positions.\n * - `hud` Per-reel text: orientation, direction, speed, phase, cells.\n */\nexport type DebugOverlayLayer =\n | 'mask'\n | 'cells'\n | 'buffers'\n | 'axis'\n | 'feed'\n | 'thresholds'\n | 'bounds'\n | 'blocks'\n | 'pins'\n | 'hud';\n\n/** Every layer, in draw order. `'all'` resolves to this list. */\nconst ALL_LAYERS: readonly DebugOverlayLayer[] = [\n 'mask',\n 'cells',\n 'buffers',\n 'thresholds',\n 'axis',\n 'feed',\n 'bounds',\n 'blocks',\n 'pins',\n 'hud',\n];\n\n/** Per-layer stroke colors. Distinct hues so overlapping layers stay legible. */\nconst COLORS: Record<DebugOverlayLayer, number> = {\n mask: 0xff3b30, // red mask box\n cells: 0x32ade6, // cyan visible cells\n buffers: 0xff9500, // amber off-window buffer cells\n axis: 0x30d158, // green travel arrow\n feed: 0x64d2ff, // sky feed edge\n thresholds: 0xff453a, // red wrap lines\n bounds: 0xff2d95, // pink real symbol bounds\n blocks: 0xffcc00, // yellow big-symbol blocks\n pins: 0xaf52de, // purple pins\n hud: 0xffffff, // white hud text\n};\n\n/** Container / layer label prefix. Used by the Pixi devtools and by tests. */\nexport const OVERLAY_LABEL = 'pixi-reels:debugOverlay';\n\n/** Mask per-reel rect color (green), separate from the red mask box. */\nconst MASK_RECT_COLOR = 0x34c759;\n/** Pin-overlay marker color (green), separate from the purple pin cell. */\nconst PIN_OVERLAY_COLOR = 0x34c759;\n\n/** hud line metrics. One line per reel, stacked inside the mask's top-left. */\nconst HUD_FONT_SIZE = 10;\nconst HUD_LINE_HEIGHT = 11;\n/** Inset from the mask's top-left corner to the first hud line. */\nconst HUD_PAD = 4;\n/** Backing plate behind the hud lines, so white text survives bright art. */\nconst HUD_BACKING_COLOR = 0x000000;\nconst HUD_BACKING_ALPHA = 0.7;\n/**\n * Advance per character, as a fraction of the font size. The plate is sized\n * from the longest line's LENGTH rather than from `Text.width`, because\n * measuring needs a canvas to rasterize against and throws in a headless\n * test. The font is monospace, so a character count is exact up to this\n * ratio, and the plate only has to be roughly right.\n */\nconst HUD_CHAR_ADVANCE = 0.62;\n\nexport interface DebugOverlayOptions {\n /**\n * Which layers to draw. An explicit list, or `'all'` for every C3 layer.\n * Defaults to `'all'`.\n */\n layers?: DebugOverlayLayer[] | 'all';\n /**\n * When `true`, the live layers (`bounds` / `blocks` / `pins` / `hud`)\n * redraw every tick. When `false` (default) the overlay draws once and\n * only updates on `redraw()` / `setLayers()` and reshape events.\n */\n live?: boolean;\n /**\n * Ticker driving the live redraw when `live: true`. Defaults to\n * `Ticker.shared`. Pass the reel set's own ticker (e.g. `app.ticker`, or a\n * `FakeTicker` in tests) to keep the overlay in lock-step with it. Ignored\n * when `live` is falsy.\n */\n ticker?: Ticker;\n}\n\n/** What the axis-family layers drew for one reel, as plain numbers. */\nexport interface DebugOverlayReelInfo {\n reel: number;\n orientation: 'vertical' | 'horizontal';\n direction: 'forward' | 'reverse';\n /** Which strip edge new symbols arrive at. Derived from polarity. */\n feedEdge: 'start' | 'end';\n /**\n * The travel arrow in reel-local MAIN coordinates. `to - from` is signed,\n * so its sign is the reel's travel direction - which a bounding box\n * cannot tell you, because a mirrored arrow has identical bounds.\n */\n axisArrow: { fromMain: number; toMain: number };\n /** Main coordinate of the feed marker. */\n feedMain: number;\n /** The two wrap lines, in main coordinates. */\n thresholds: { start: number; end: number };\n visibleCells: number;\n /** Last phase seen on this reel's bus, or 'idle'. */\n phase: string;\n}\n\n/** Serializable summary of the overlay. the text half of a visual debugger. */\nexport interface DebugOverlaySnapshot {\n layers: DebugOverlayLayer[];\n reels: DebugOverlayReelInfo[];\n}\n\n/** Handle returned by {@link debugOverlay}. Owns its display objects. */\nexport interface DebugOverlayHandle extends Disposable {\n /** Swap the active layer set and redraw. Accepts a list or `'all'`. */\n setLayers(layers: DebugOverlayLayer[] | 'all'): void;\n /** Force a full redraw (static + live layers). */\n redraw(): void;\n /**\n * Plain-JSON description of what the axis / feed / thresholds layers\n * represent, per reel. PixiJS renders to a canvas, which CLAUDE.md notes\n * AI agents and CI cannot see; this is the same information in a form\n * they (and `expect`) can read. No PixiJS types, safe to `JSON.stringify`.\n */\n describe(): DebugOverlaySnapshot;\n /** Remove the overlay from the reel set and dispose every allocation. */\n destroy(): void;\n readonly isDestroyed: boolean;\n}\n\nfunction resolveLayers(\n layers: DebugOverlayLayer[] | 'all' | undefined,\n): Set<DebugOverlayLayer> {\n if (layers === undefined || layers === 'all') return new Set(ALL_LAYERS);\n return new Set(layers);\n}\n\n/**\n * A layered visual debug overlay for a {@link ReelSet}. Draws mask, cell,\n * buffer, symbol-bounds, big-symbol-block, pin and hud layers into a\n * `Container` added to the reel set itself. because `ReelSet extends\n * Container`, that renders the overlay above the viewport (including the\n * spotlight container), unlike the older `showMask` which drew inside the\n * viewport and was covered by the spotlight.\n *\n * Dev-only. It reads engine internals through the public accessors, is not\n * semver-protected, and must not reach a production bundle.\n *\n * ```ts\n * const overlay = debugOverlay(reelSet, { layers: ['cells', 'bounds'], live: true });\n * overlay.setLayers(['cells', 'pins']);\n * overlay.redraw();\n * overlay.destroy();\n * ```\n */\nexport function debugOverlay(\n reelSet: ReelSet,\n options: DebugOverlayOptions = {},\n): DebugOverlayHandle {\n return new DebugOverlay(reelSet, options);\n}\n\nclass DebugOverlay implements DebugOverlayHandle {\n private _root = new Container();\n private _graphics = new Map<DebugOverlayLayer, Graphics>();\n private _cellLabels: Text[] = [];\n private _hudTexts: Text[] = [];\n private _active: Set<DebugOverlayLayer>;\n private _tickerRef: TickerRef | null = null;\n private _isDestroyed = false;\n\n /** Current phase name per reel, tracked off the reel bus for the hud layer. */\n private _phase: string[];\n /** Detach callbacks for the per-reel phase listeners. */\n private _reelDetach: Array<() => void> = [];\n private _onStatic = (): void => this._redrawStatic();\n\n constructor(\n private _reelSet: ReelSet,\n options: DebugOverlayOptions,\n ) {\n this._active = resolveLayers(options.layers);\n this._phase = _reelSet.reels.map(() => 'idle');\n\n // Above the viewport (and its spotlight container), never interactive.\n this._root.zIndex = 1_000_000;\n this._root.eventMode = 'none';\n this._root.label = OVERLAY_LABEL;\n _reelSet.addChild(this._root);\n\n // Track per-reel phase for the hud layer via the reel bus. There is no\n // `reel.phase` accessor. phases are only observable as events.\n _reelSet.reels.forEach((reel: Reel, i: number) => {\n const onEnter = (name: string): void => {\n this._phase[i] = name;\n };\n const onExit = (name: string): void => {\n if (this._phase[i] === name) this._phase[i] = 'idle';\n };\n reel.events.on('phase:enter', onEnter);\n reel.events.on('phase:exit', onExit);\n this._reelDetach.push(() => {\n reel.events.off('phase:enter', onEnter);\n reel.events.off('phase:exit', onExit);\n });\n });\n\n // Static layers redraw only on reshape, not per tick.\n _reelSet.events.on('shape:changed', this._onStatic);\n _reelSet.events.on('adjust:complete', this._onStatic);\n\n if (options.live) {\n const ticker = options.ticker ?? Ticker.shared;\n this._tickerRef = new TickerRef(ticker);\n this._tickerRef.add(() => this._redrawLive());\n }\n\n this.redraw();\n }\n\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n setLayers(layers: DebugOverlayLayer[] | 'all'): void {\n if (this._isDestroyed) return;\n this._active = resolveLayers(layers);\n // Clear + hide anything no longer active so stale strokes vanish.\n for (const [layer, g] of this._graphics) {\n if (!this._active.has(layer)) {\n g.clear();\n g.visible = false;\n } else {\n g.visible = true;\n }\n }\n if (!this._active.has('cells')) this._hideTextsFrom(this._cellLabels, 0);\n if (!this._active.has('hud')) this._hideTextsFrom(this._hudTexts, 0);\n this.redraw();\n }\n\n redraw(): void {\n if (this._isDestroyed) return;\n this._redrawStatic();\n this._redrawLive();\n }\n\n describe(): DebugOverlaySnapshot {\n return {\n layers: [...this._active],\n reels: this._reelSet.reels.map((reel: Reel, i: number) => {\n const axis = reel.axis;\n const arrow = this._arrowMains(reel);\n const pitch = reel.motion.slotPitch;\n return {\n reel: i,\n orientation: axis.orientation,\n direction: axis.direction,\n feedEdge: axis.feedEdge,\n axisArrow: arrow,\n feedMain: this._feedMain(reel),\n thresholds: {\n start: -(reel.bufferStart + 1) * pitch,\n end: (reel.visibleCells + reel.bufferEnd) * pitch,\n },\n visibleCells: reel.visibleCells,\n phase: this._phase[i],\n };\n }),\n };\n }\n\n destroy(): void {\n if (this._isDestroyed) return;\n this._isDestroyed = true;\n\n this._tickerRef?.destroy();\n this._tickerRef = null;\n\n this._reelSet.events.off('shape:changed', this._onStatic);\n this._reelSet.events.off('adjust:complete', this._onStatic);\n for (const detach of this._reelDetach) detach();\n this._reelDetach.length = 0;\n\n if (this._root.parent) this._root.parent.removeChild(this._root);\n // Destroys every pooled Graphics + Text child in one call.\n this._root.destroy({ children: true });\n this._graphics.clear();\n this._cellLabels.length = 0;\n this._hudTexts.length = 0;\n }\n\n // --- redraw dispatch -----------------------------------------------------\n\n /**\n * The static layers: `mask` / `cells` / `buffers` are pure geometry that\n * only shifts on a MultiWays reshape, so they redraw on `shape:changed` /\n * `adjust:complete` rather than every tick.\n */\n private _redrawStatic(): void {\n if (this._isDestroyed) return;\n if (this._active.has('mask')) this._drawMask();\n if (this._active.has('cells')) this._drawCells();\n if (this._active.has('buffers')) this._drawBuffers();\n if (this._active.has('thresholds')) this._drawThresholds();\n if (this._active.has('axis')) this._drawAxis();\n if (this._active.has('feed')) this._drawFeed();\n }\n\n /**\n * The live layers. `bounds` / `pins` / `hud` are the plan's named live\n * layers; `blocks` joins them because a big symbol's block outline tracks\n * landed content (which changes on every result), not just reshapes.\n */\n private _redrawLive(): void {\n if (this._isDestroyed) return;\n if (this._active.has('bounds')) this._drawBounds();\n if (this._active.has('blocks')) this._drawBlocks();\n if (this._active.has('pins')) this._drawPins();\n if (this._active.has('hud')) this._drawHud();\n }\n\n // --- pooling helpers -----------------------------------------------------\n\n /** One persistent Graphics per layer, created on first use, cleared per draw. */\n private _layer(layer: DebugOverlayLayer): Graphics {\n let g = this._graphics.get(layer);\n if (!g) {\n g = new Graphics();\n // Labelled so it is identifiable in the Pixi devtools tree and in\n // tests, which is the only way to assert a layer drew where it should.\n g.label = `${OVERLAY_LABEL}:${layer}`;\n this._graphics.set(layer, g);\n this._root.addChild(g);\n }\n g.visible = true;\n g.clear();\n return g;\n }\n\n /** Reuse (or lazily grow) a text pool slot. Never measured. positioned only. */\n private _text(pool: Text[], index: number, color: number, size: number): Text {\n let t = pool[index];\n if (!t) {\n t = new Text({\n text: '',\n style: { fontFamily: 'monospace', fontSize: size, fill: color },\n });\n pool[index] = t;\n this._root.addChild(t);\n }\n t.visible = true;\n return t;\n }\n\n private _hideTextsFrom(pool: Text[], from: number): void {\n for (let i = from; i < pool.length; i++) pool[i].visible = false;\n }\n\n // --- layer draws ---------------------------------------------------------\n\n private _drawMask(): void {\n const g = this._layer('mask');\n const vp = this._reelSet.viewport;\n // Cell bounds are ReelSet-local (they add viewport.x/y); mirror that here\n // since the overlay root sits in ReelSet-local space, not viewport-local.\n const vx = vp.x;\n const vy = vp.y;\n g.rect(vx, vy, vp.maskWidth, vp.maskHeight).stroke({ color: COLORS.mask, width: 2 });\n for (const rect of vp.maskRects) {\n g.rect(vx + rect.x, vy + rect.y, rect.width, rect.height).stroke({\n color: MASK_RECT_COLOR,\n width: 2,\n });\n }\n }\n\n private _drawCells(): void {\n const g = this._layer('cells');\n let labelIndex = 0;\n this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const b = this._reelSet.getCellBounds(reelIndex, cell);\n // On a curved reel outline the TRAPEZOID the drum actually draws, not\n // the bounding box `getCellBounds` has to widen to. The overlay is how\n // you check the projection landed where you think it did, so it has to\n // show the bend rather than a rectangle around it.\n const quad = this._reelSet.getCellQuad(reelIndex, cell);\n if (quad) {\n g.poly(quad).stroke({ color: COLORS.cells, width: 1 });\n } else {\n g.rect(b.x, b.y, b.width, b.height).stroke({ color: COLORS.cells, width: 1 });\n }\n const label = this._text(this._cellLabels, labelIndex++, COLORS.cells, 10);\n label.text = `${reelIndex},${cell}`;\n // Anchor the label on the quad's own leading corner so it tracks the\n // bend instead of floating off in the bounding box's dead space.\n label.x = (quad ? quad[0].x : b.x) + 3;\n label.y = (quad ? quad[0].y : b.y) + 3;\n }\n });\n this._hideTextsFrom(this._cellLabels, labelIndex);\n }\n\n private _drawBuffers(): void {\n const g = this._layer('buffers');\n for (const reel of this._reelSet.reels) {\n const pitch = reel.motion.slotPitch;\n const draw = (main: number): void => {\n // Follow the drum. A buffer box left on the flat grid sits nowhere\n // near the symbol it is labelling once the reel is curved, and the\n // buffers are exactly the cells the curve moves furthest.\n const curve = reel.curve;\n const from = curve ? curve.mapMain(main) : main;\n const to = curve ? curve.mapMain(main + reel.cellMain) : main + reel.cellMain;\n const cross = curve ? reel.cellCross * curve.scaleAt(main + reel.cellMain / 2) : reel.cellCross;\n const r = this._reelRect(reel, (reel.cellCross - cross) / 2, from, cross, to - from);\n g.rect(r.x, r.y, r.width, r.height).stroke({\n color: COLORS.buffers,\n width: 1,\n alpha: 0.45,\n });\n };\n // bufferStart cells sit at negative main offsets, before visible cell 0.\n for (let k = 1; k <= reel.bufferStart; k++) draw(-k * pitch);\n // bufferEnd cells sit past the last visible cell.\n for (let k = 0; k < reel.bufferEnd; k++) draw((reel.visibleCells + k) * pitch);\n }\n }\n\n /**\n * Project a reel-local `(cross, main)` point into overlay space.\n *\n * Every layer below goes through this rather than touching `container.x`\n * and `.y`, which is what lets the same code draw a sideways or reversed\n * reel correctly - and what makes a mistake in the projection show up on\n * screen instead of hiding in a diff.\n */\n private _reelPoint(reel: Reel, cross: number, main: number): { x: number; y: number } {\n const axis = reel.axis;\n const p = axis.toScreen(\n axis.getCross(reel.container) + cross,\n axis.getMain(reel.container) + main,\n );\n return { x: this._reelSet.viewport.x + p.x, y: this._reelSet.viewport.y + p.y };\n }\n\n /** A reel-local rect in (cross, main) space, as screen `x/y/width/height`. */\n private _reelRect(\n reel: Reel,\n cross: number,\n main: number,\n crossSize: number,\n mainSize: number,\n ): { x: number; y: number; width: number; height: number } {\n const origin = this._reelPoint(reel, cross, main);\n const size = reel.axis.toScreen(crossSize, mainSize);\n return { x: origin.x, y: origin.y, width: size.x, height: size.y };\n }\n\n /**\n * One arrow per reel, drawn along the travel axis and pointing the way the\n * strip actually moves. Reads polarity, so a `direction('reverse')` reel\n * points back at you.\n */\n /**\n * The arrow's tail and head in reel-local main coordinates. Shared by the\n * draw and by `describe()` so the picture and the numbers cannot disagree.\n */\n private _arrowMains(reel: Reel): { fromMain: number; toMain: number } {\n const span = reel.visibleCells * reel.motion.slotPitch;\n const forward = reel.axis.polarity > 0;\n return {\n fromMain: forward ? span * 0.2 : span * 0.8,\n toMain: forward ? span * 0.8 : span * 0.2,\n };\n }\n\n /** Main coordinate of the feed marker: just outside the feeding edge. */\n private _feedMain(reel: Reel): number {\n const pitch = reel.motion.slotPitch;\n return reel.axis.feedEdge === 'start'\n ? -reel.bufferStart * pitch\n : reel.visibleCells * pitch;\n }\n\n private _drawAxis(): void {\n const g = this._layer('axis');\n for (const reel of this._reelSet.reels) {\n const span = reel.visibleCells * reel.motion.slotPitch;\n const midCross = reel.cellCross / 2;\n const { fromMain: tailMain, toMain: headMain } = this._arrowMains(reel);\n const forward = reel.axis.polarity > 0;\n const tail = this._reelPoint(reel, midCross, tailMain);\n const head = this._reelPoint(reel, midCross, headMain);\n g.moveTo(tail.x, tail.y).lineTo(head.x, head.y).stroke({\n color: COLORS.axis,\n width: 3,\n });\n // Arrowhead: two barbs, each pulled back along travel and out to the\n // sides on the cross axis.\n const barb = Math.min(span * 0.12, reel.cellCross * 0.4) || 8;\n const backMain = headMain - (forward ? barb : -barb);\n for (const side of [-1, 1]) {\n const b = this._reelPoint(reel, midCross + side * barb * 0.6, backMain);\n g.moveTo(head.x, head.y).lineTo(b.x, b.y).stroke({\n color: COLORS.axis,\n width: 3,\n });\n }\n }\n }\n\n /**\n * A bar on the edge new symbols enter from. `feedEdge` is derived from\n * polarity, so this and the axis arrow must always agree; if they ever\n * disagree on screen, the derivation broke.\n */\n private _drawFeed(): void {\n const g = this._layer('feed');\n for (const reel of this._reelSet.reels) {\n const pitch = reel.motion.slotPitch;\n const r = this._reelRect(reel, 0, this._feedMain(reel), reel.cellCross, pitch * 0.18);\n g.rect(r.x, r.y, r.width, r.height).fill({ color: COLORS.feed, alpha: 0.55 });\n }\n }\n\n /**\n * The two wrap lines. A symbol that crosses one is rotated to the other\n * end of the strip array, which is contract law L7 (periodicity) and L9\n * (boundedness) made watchable: drive a spin with this layer on and no\n * symbol should ever be drawn past a line.\n */\n private _drawThresholds(): void {\n const g = this._layer('thresholds');\n for (const reel of this._reelSet.reels) {\n const pitch = reel.motion.slotPitch;\n const mains = [\n -(reel.bufferStart + 1) * pitch,\n (reel.visibleCells + reel.bufferEnd) * pitch,\n ];\n for (const main of mains) {\n const a = this._reelPoint(reel, 0, main);\n const bEnd = this._reelPoint(reel, reel.cellCross, main);\n g.moveTo(a.x, a.y).lineTo(bEnd.x, bEnd.y).stroke({\n color: COLORS.thresholds,\n width: 2,\n alpha: 0.9,\n });\n }\n }\n }\n\n private _drawBounds(): void {\n const g = this._layer('bounds');\n this._reelSet.reels.forEach((reel: Reel) => {\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const view = reel.getSymbolAt(cell).view;\n // getBounds() is world-space; map the AABB corners into overlay-local\n // (ReelSet-local) space so the rect aligns regardless of stage offset.\n const wb = view.getBounds();\n const tl = this._root.toLocal({ x: wb.x, y: wb.y });\n const br = this._root.toLocal({ x: wb.x + wb.width, y: wb.y + wb.height });\n g.rect(tl.x, tl.y, br.x - tl.x, br.y - tl.y).stroke({\n color: COLORS.bounds,\n width: 1,\n });\n }\n });\n }\n\n private _drawBlocks(): void {\n const g = this._layer('blocks');\n // Only outline each block once, at its anchor cell.\n this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const fp = this._reelSet.getSymbolFootprint(reelIndex, cell);\n if (fp.size.reels <= 1 && fp.size.cells <= 1) continue;\n if (fp.anchor.reel !== reelIndex || fp.anchor.cell !== cell) continue;\n const rect = this._reelSet.getBlockBounds(reelIndex, cell);\n g.rect(rect.x, rect.y, rect.width, rect.height).stroke({\n color: COLORS.blocks,\n width: 3,\n });\n }\n });\n }\n\n private _drawPins(): void {\n const g = this._layer('pins');\n this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n for (let cell = 0; cell < reel.visibleCells; cell++) {\n const pin = this._reelSet.getPin(reelIndex, cell);\n if (!pin) continue;\n const b = this._reelSet.getCellBounds(reelIndex, cell);\n // Pin cell outline.\n g.rect(b.x, b.y, b.width, b.height).stroke({ color: COLORS.pins, width: 3 });\n // A diagonal cross marks the pin-overlay cell, so a movePin /\n // pin-overlay disagreement (A1) shows as a cross off its cell.\n g.moveTo(b.x, b.y)\n .lineTo(b.x + b.width, b.y + b.height)\n .moveTo(b.x + b.width, b.y)\n .lineTo(b.x, b.y + b.height)\n .stroke({ color: PIN_OVERLAY_COLOR, width: 1, alpha: 0.8 });\n }\n });\n }\n\n private _drawHud(): void {\n // One Text per reel, stacked as a single left-aligned column.\n //\n // Each line used to be anchored at its own reel's top-left corner, which\n // assumed a line fits inside a reel. It does not: ~40 characters at 11px\n // monospace is ~230px against a cell that is typically ~100px wide, so on\n // any set past two reels every line overprinted its neighbours into an\n // unreadable smear -- worse the more reels you had, which is exactly when\n // you want the hud. A column reads at any reel count and in either\n // orientation; the `r<n>` prefix still ties a line to its reel, and the\n // `cells` layer labels each cell `reel,cell` on the canvas.\n //\n // Anchored INSIDE the mask's top-left, not outside it. Stacking below the\n // mask would keep the reels clear, but a host that sized its camera to the\n // reel set before the overlay existed then renders the whole block\n // off-screen, and an invisible hud is worse than a cluttered one. A debug\n // layer you opted into may cover art; drop `hud` if it is in the way.\n const g = this._layer('hud');\n const vp = this._reelSet.viewport;\n const left = vp.x + HUD_PAD;\n const top = vp.y + HUD_PAD;\n let i = 0;\n let widest = 0;\n this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n const t = this._text(this._hudTexts, i, COLORS.hud, HUD_FONT_SIZE);\n // Render at 1x rather than devicePixelRatio: at 10px the glyphs come out\n // blocky and aliased instead of grey-smeared, which is both the pixel\n // look and the more legible one over busy art. Guarded because assigning\n // resolution dirties the texture and would re-rasterize every live tick.\n if (t.resolution !== 1) t.resolution = 1;\n const axis = reel.axis;\n // Single letters keep the line short: V/H orientation, F/R direction,\n // then the runtime state.\n const o = axis.orientation === 'vertical' ? 'V' : 'H';\n const d = axis.direction === 'forward' ? 'F' : 'R';\n t.text =\n `r${reelIndex} ${o}${d} feed=${axis.feedEdge} ` +\n `spd=${reel.speed.toFixed(1)} ${this._phase[reelIndex]} cells=${reel.visibleCells}`;\n t.x = left;\n t.y = top + i * HUD_LINE_HEIGHT;\n widest = Math.max(widest, t.text.length);\n i++;\n });\n this._hideTextsFrom(this._hudTexts, i);\n // Backing plate, so white text survives bright art. `_layer` added this\n // Graphics before the pool's Texts, so child order already puts it under\n // them.\n if (i > 0) {\n g.rect(\n left - HUD_PAD,\n top - HUD_PAD,\n widest * HUD_FONT_SIZE * HUD_CHAR_ADVANCE + HUD_PAD * 2,\n i * HUD_LINE_HEIGHT + HUD_PAD * 2,\n ).fill({ color: HUD_BACKING_COLOR, alpha: HUD_BACKING_ALPHA });\n }\n }\n}\n","import { Graphics } from 'pixi.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { Reel } from '../core/Reel.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport { debugOverlay } from './debugOverlay.js';\nimport type { DebugOverlayOptions, DebugOverlayHandle } from './debugOverlay.js';\n\n/**\n * Debug snapshot. plain JSON representation of the entire reel state.\n *\n * Designed for AI agents that cannot see the canvas.\n * Returns no PixiJS display objects, only serializable data.\n *\n * **Breaking note (since v0.3):** `visibleCells` is now `number[]` (one entry\n * per reel) so jagged shapes (pyramids, MultiWays) are representable. For\n * uniform slots every entry is the same value. Adapt downstream code that\n * deep-reads the snapshot.\n */\nexport interface DebugSnapshot {\n timestamp: number;\n isSpinning: boolean;\n currentSpeed: string;\n availableSpeeds: string[];\n spotlightActive: boolean;\n reelCount: number;\n visibleCells: number[];\n reels: DebugReelSnapshot[];\n grid: string[][];\n}\n\nexport interface DebugReelSnapshot {\n index: number;\n speed: number;\n isStopping: boolean;\n /** This reel's travel projection, so a reader can interpret `main`. */\n orientation: Orientation;\n direction: Direction;\n allSymbols: {\n cell: number;\n symbolId: string;\n /**\n * Position along the reel's TRAVEL axis (screen `y` when vertical,\n * `x` when horizontal). This used to be a hard-coded `y`, which meant\n * every symbol on a horizontal set reported a constant 0 - the one\n * orientation where the field mattered most.\n */\n main: number;\n }[];\n visibleSymbols: string[];\n}\n\n/**\n * Take a plain-JSON snapshot of the entire reel set state.\n *\n * This is the primary debugging tool for AI agents. The output is\n * a serializable object with no circular references, no PixiJS types.\n *\n * ```ts\n * const state = debugSnapshot(reelSet);\n * console.log(JSON.stringify(state, null, 2));\n * ```\n */\nexport function debugSnapshot(reelSet: ReelSet): DebugSnapshot {\n const reels = reelSet.reels;\n const reelSnapshots: DebugReelSnapshot[] = reels.map((reel: Reel, i: number) => ({\n index: i,\n speed: reel.speed,\n isStopping: reel.isStopping,\n orientation: reel.axis.orientation,\n direction: reel.axis.direction,\n allSymbols: reel.symbols.map((s, cell) => ({\n cell,\n symbolId: s.symbolId,\n main: Math.round(reel.axis.getMain(s.view)),\n })),\n visibleSymbols: reel.getVisibleSymbols(),\n }));\n\n // Build the visual grid (what a player would see). Uses the ReelSet\n // resolver so cross-reel OCCUPIED cells of a big-symbol block render as\n // the anchor's id, not as the OCCUPIED sentinel.\n const grid: string[][] = reelSet.getVisibleGrid();\n\n return {\n timestamp: Date.now(),\n isSpinning: reelSet.isSpinning,\n currentSpeed: reelSet.speed.activeName,\n availableSpeeds: reelSet.speed.profileNames,\n spotlightActive: reelSet.spotlight.isActive,\n reelCount: reels.length,\n visibleCells: reels.map((r) => r.visibleCells),\n reels: reelSnapshots,\n grid,\n };\n}\n\n/**\n * Pretty-print the grid as an ASCII table.\n *\n * ```\n * ┌────────┬────────┬────────┬────────┬────────┐\n * │ cherry │ lemon │ bar │ seven │ cherry │\n * │ plum │ cherry │ wild │ lemon │ orange │\n * │ orange │ bell │ cherry │ plum │ bell │\n * └────────┴────────┴────────┴────────┴────────┘\n * ```\n */\nexport function debugGrid(reelSet: ReelSet): string {\n const snap = debugSnapshot(reelSet);\n const { grid, visibleCells } = snap;\n if (grid.length === 0) return '(empty grid)';\n\n const colWidth = 8;\n const maxCells = Math.max(...visibleCells);\n const pad = (s: string) => s.slice(0, colWidth).padEnd(colWidth);\n const empty = ' '.repeat(colWidth);\n\n const border = (left: string, mid: string, right: string) =>\n left + grid.map(() => '─'.repeat(colWidth)).join(mid) + right;\n\n const lines: string[] = [];\n lines.push(border('┌', '┬', '┐'));\n\n for (let cell = 0; cell < maxCells; cell++) {\n const cells = grid.map((reel, i) => (cell < visibleCells[i] ? pad(reel[cell] ?? '?') : empty));\n lines.push('│' + cells.join('│') + '│');\n }\n\n lines.push(border('└', '┴', '┘'));\n return lines.join('\\n');\n}\n\n/**\n * One captured frame from `startRecording()`. a `DebugSnapshot` plus the\n * tag the recording was started with and the spin event that triggered\n * the capture.\n */\nexport interface RecordedFrame {\n /** Recording tag at the time of capture. Useful for grouping multiple sessions. */\n tag: string;\n /** Reel-set event that triggered the capture (`spin:start`, `spin:allLanded`, etc). */\n trigger: string;\n /** Snapshot of `debugSnapshot(reelSet)` at the moment of capture. */\n snapshot: DebugSnapshot;\n}\n\n/**\n * Default upper bound on `_recordedFrames` length. When the buffer fills,\n * the oldest entries are dropped (rolling window). Override per session\n * via `startRecording(reelSet, tag, { maxFrames })`. A long-running debug\n * session in a browser would otherwise grow the array forever.\n */\nconst DEFAULT_MAX_FRAMES = 1000;\n\n/** All captured frames across all recording sessions in this process. */\nconst _recordedFrames: RecordedFrame[] = [];\n\n/** Effective per-process cap. Updated when a session starts with a higher value. */\nlet _maxFrames = DEFAULT_MAX_FRAMES;\n\n/**\n * Per-recording-session state: which events to listen on and how to\n * detach them later. Keyed by the `ReelSet` so two reel sets in the\n * same page can each record independently.\n */\nconst _recorders = new WeakMap<ReelSet, () => void>();\n\n/** Options for {@link startRecording}. */\nexport interface StartRecordingOptions {\n /**\n * Maximum number of frames retained across the whole process. When the\n * buffer is full the oldest frames are dropped. Default 1000.\n */\n maxFrames?: number;\n}\n\n/**\n * Start recording the reel-set's frame state at every key spin event\n * (`spin:start`, `spin:reelLanded`, `spin:allLanded`, `spin:complete`).\n * Each event captures a `DebugSnapshot` and pushes it onto a process-\n * wide rolling log readable via {@link getFrames}.\n *\n * The `tag` is freeform. use it to label multiple recording sessions\n * so you can filter `getFrames(tag)` later. Call {@link stopRecording}\n * to detach the listeners (also fires automatically when the reel set\n * emits `'destroyed'`).\n *\n * Designed for AI agents and debug harnesses. Calling `startRecording`\n * twice on the same `reelSet` replaces the prior recording (the previous\n * tag's listeners are removed before the new ones attach).\n *\n * ```ts\n * import { startRecording, stopRecording, getFrames } from 'pixi-reels';\n *\n * startRecording(reelSet, 'spin-1');\n * await reelSet.spin();\n * stopRecording(reelSet);\n * const frames = getFrames('spin-1'); // every snapshot tagged 'spin-1'\n * ```\n */\nexport function startRecording(\n reelSet: ReelSet,\n tag = 'default',\n options: StartRecordingOptions = {},\n): void {\n // Detach any prior recorder on this reel set first.\n stopRecording(reelSet);\n\n if (options.maxFrames !== undefined && options.maxFrames > 0) {\n _maxFrames = options.maxFrames;\n }\n\n const capture = (trigger: string): void => {\n _recordedFrames.push({ tag, trigger, snapshot: debugSnapshot(reelSet) });\n // Rolling window: drop oldest when over cap.\n if (_recordedFrames.length > _maxFrames) {\n _recordedFrames.splice(0, _recordedFrames.length - _maxFrames);\n }\n };\n\n const onStart = () => capture('spin:start');\n const onReelLanded = () => capture('spin:reelLanded');\n const onAllLanded = () => capture('spin:allLanded');\n const onComplete = () => capture('spin:complete');\n // Auto-detach when the reel set is destroyed. otherwise listeners hang\n // off a dead emitter and the WeakMap entry can't drop until GC.\n const onDestroyed = () => stopRecording(reelSet);\n\n reelSet.events.on('spin:start', onStart);\n reelSet.events.on('spin:reelLanded', onReelLanded);\n reelSet.events.on('spin:allLanded', onAllLanded);\n reelSet.events.on('spin:complete', onComplete);\n reelSet.events.on('destroyed', onDestroyed);\n\n _recorders.set(reelSet, () => {\n reelSet.events.off('spin:start', onStart);\n reelSet.events.off('spin:reelLanded', onReelLanded);\n reelSet.events.off('spin:allLanded', onAllLanded);\n reelSet.events.off('spin:complete', onComplete);\n reelSet.events.off('destroyed', onDestroyed);\n });\n}\n\n/** Detach the recorder previously installed by {@link startRecording}. No-op if none. */\nexport function stopRecording(reelSet: ReelSet): void {\n const detach = _recorders.get(reelSet);\n if (detach) {\n detach();\n _recorders.delete(reelSet);\n }\n}\n\n/**\n * All recorded frames in capture order. When `tag` is provided, only\n * frames tagged with it are returned. Frames are not cleared between\n * recording sessions. call {@link clearFrames} to reset.\n */\nexport function getFrames(tag?: string): readonly RecordedFrame[] {\n if (tag === undefined) return _recordedFrames.slice();\n return _recordedFrames.filter((f) => f.tag === tag);\n}\n\n/** Empty the global recording log. */\nexport function clearFrames(): void {\n _recordedFrames.length = 0;\n}\n\n/**\n * Enable debug mode: attaches debug utilities to `window.__PIXI_REELS_DEBUG`.\n *\n * After calling this, an AI agent can run in the browser console:\n * ```js\n * __PIXI_REELS_DEBUG.snapshot() // full state JSON\n * __PIXI_REELS_DEBUG.grid() // ASCII grid\n * __PIXI_REELS_DEBUG.log() // console.log the grid\n * __PIXI_REELS_DEBUG.startRecording('myTag')\n * __PIXI_REELS_DEBUG.stopRecording()\n * __PIXI_REELS_DEBUG.getFrames('myTag')\n * ```\n *\n * For a single reel set, leave `key` unset. With multiple reel sets, pass a\n * distinct `key` per call so they don't clobber each other on `window`: each is\n * reachable at `__PIXI_REELS_DEBUG_INSTANCES[key]`, and `__PIXI_REELS_DEBUG`\n * always points at the most recently enabled one for convenience.\n *\n * This attaches to `window` and logs - call it only in dev/QA builds, never in\n * a production bundle (the snapshot exposes internal state and is not\n * semver-protected, so do not wire monitoring/telemetry to it).\n */\nexport function enableDebug(reelSet: ReelSet, key?: string): void {\n if (typeof window === 'undefined') return;\n\n let maskOverlay: Graphics | null = null;\n\n const debug = {\n reelSet,\n snapshot: () => debugSnapshot(reelSet),\n grid: () => debugGrid(reelSet),\n log: () => {\n const snap = debugSnapshot(reelSet);\n console.log(`[pixi-reels debug] spinning=${snap.isSpinning} speed=${snap.currentSpeed}`);\n console.log(debugGrid(reelSet));\n return snap;\n },\n /** Log every event as it happens */\n trace: () => {\n const events = [\n 'spin:start', 'spin:allStarted', 'spin:stopping',\n 'spin:reelLanded', 'spin:allLanded', 'spin:complete',\n 'skip:requested', 'skip:completed', 'speed:changed',\n 'spotlight:start', 'spotlight:end',\n 'shape:changed', 'adjust:start', 'adjust:complete',\n 'pin:placed', 'pin:moved', 'pin:expired', 'pin:migrated',\n 'destroyed',\n ] as const;\n for (const event of events) {\n reelSet.events.on(event as any, (...args: any[]) => {\n console.log(`[pixi-reels] ${event}`, ...args);\n });\n }\n console.log('[pixi-reels debug] tracing enabled for all events');\n },\n /** Start a frame-state recording session on this reel set. */\n startRecording: (tag = 'default', options?: StartRecordingOptions) =>\n startRecording(reelSet, tag, options),\n /** Stop a recording session. paired with `startRecording`. */\n stopRecording: () => stopRecording(reelSet),\n /** Pull recorded frames; pass `tag` to filter to one session. */\n getFrames: (tag?: string) => getFrames(tag),\n /** Empty the global recording log. */\n clearFrames: () => clearFrames(),\n /**\n * Toggle a debug overlay on the unmasked container that visualizes the\n * mask shape and per-reel boxes. Useful for spotting pyramid peek and\n * confirming MultiWays box geometry.\n */\n showMask: (enabled: boolean) => {\n if (enabled) {\n if (maskOverlay) return;\n const g = new Graphics();\n g.rect(0, 0, reelSet.viewport.maskWidth, reelSet.viewport.maskHeight)\n .fill({ color: 0xff0000, alpha: 0.15 });\n for (const rect of reelSet.viewport.maskRects) {\n g.rect(rect.x, rect.y, rect.width, rect.height)\n .stroke({ color: 0x00ff00, width: 2 });\n }\n reelSet.viewport.unmaskedContainer.addChild(g);\n maskOverlay = g;\n } else if (maskOverlay) {\n reelSet.viewport.unmaskedContainer.removeChild(maskOverlay);\n maskOverlay.destroy();\n maskOverlay = null;\n }\n },\n /**\n * Create a layered visual debug overlay on this reel set (mask, cells,\n * buffers, symbol bounds, big-symbol blocks, pins, hud). Returns a handle;\n * call `.setLayers(...)` / `.redraw()` / `.destroy()` on it. Unlike\n * `showMask`, this renders above the viewport (incl. the spotlight).\n */\n overlay: (opts?: DebugOverlayOptions): DebugOverlayHandle => debugOverlay(reelSet, opts),\n };\n\n const w = window as unknown as {\n __PIXI_REELS_DEBUG?: typeof debug;\n __PIXI_REELS_DEBUG_INSTANCES?: Record<string, typeof debug>;\n };\n // Per-instance registry so multiple reel sets don't overwrite one another.\n const registry = (w.__PIXI_REELS_DEBUG_INSTANCES ??= {});\n const resolvedKey = key ?? `reelset_${Object.keys(registry).length}`;\n registry[resolvedKey] = debug;\n // Back-compat: the bare global points at the most recently enabled instance.\n w.__PIXI_REELS_DEBUG = debug;\n console.log(\n `[pixi-reels] Debug mode enabled (key \"${resolvedKey}\"). ` +\n `Use __PIXI_REELS_DEBUG.log() or __PIXI_REELS_DEBUG_INSTANCES[\"${resolvedKey}\"].`,\n );\n}\n"],"mappings":"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"}