{"version":3,"file":"ReelSymbol-0kVAZr4S.cjs","names":[],"sources":["../src/utils/gsap.ts","../src/symbols/ReelSymbol.ts"],"sourcesContent":["import { gsap as resolvedGsap } from 'gsap';\n\n/** The gsap namespace type, as the engine passes it around. */\nexport type Gsap = typeof resolvedGsap;\n\n/**\n * The gsap instance resolved at lib-load time. Used by any `ReelSet` whose\n * builder did not call `.gsap(...)`, and by any symbol not yet bound to a\n * set.\n *\n * **Why the engine passes gsap around at all:** under tools that resolve\n * modules through symlinked workspaces (vite + a locally-linked pixi-reels,\n * pnpm dev setups, esbuild plugin chains), the gsap import inside the lib's\n * compiled `dist/index.js` and the gsap import in the consumer's source can\n * resolve to *different module instances*. each with its own root timeline.\n * The consumer drives one, the lib's tweens live on the other, and reels\n * stall at progress 0. `ReelSetBuilder.gsap(myGsap)` hands the engine the\n * consumer's instance so both live on the same timeline.\n *\n * Held PER REEL SET, not process-wide: two sets on one stage can be driven\n * by different gsap instances, which is what a composed stage (a banner reel\n * above a main grid, a bonus board beside it) needs.\n */\nexport const DEFAULT_GSAP: Gsap = resolvedGsap;\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\n\n/**\n * One visible cell on a reel. the thing that actually draws.\n *\n * `ReelSymbol` is the abstract base class. Subclass it to pick a rendering\n * technology (`SpriteSymbol`, `AnimatedSpriteSymbol`, `SpineSymbol`, or a\n * custom class of your own). The reel set pools instances aggressively:\n * one instance is reused many times as it scrolls off one identity and on\n * to another, so implementations must never assume \"I was just created\".\n *\n * Required lifecycle hooks:\n *\n *   - `onActivate(symbolId)`. the pool just handed me a new identity. Swap\n *     texture, restart animations, bring myself out of any \"ended\" pose.\n *   - `onDeactivate()`. I am about to be pooled. Pause animations, clear\n *     listeners, leave myself in a clean state for the next activation.\n *   - `playWin()`. the spotlight is celebrating me. Return a promise that\n *     resolves when the one-shot animation is done.\n *   - `stopAnimation()`. spotlight is over, return to idle.\n *   - `resize(w, h)`. the reel's cell size changed (on every symbol swap).\n *     Store the dimensions and reposition internal children. Forgetting\n *     this is the single most common \"why do my symbols scatter\" bug.\n *\n * ```\n * create → activate(symbolId) → [playWin / stopAnimation]\n *                             → deactivate\n *                             → activate(newId) → ...\n * ```\n *\n * There's no hidden GC. Hold resources? Override `onDestroy()`.\n */\nexport abstract class ReelSymbol implements Disposable {\n  /** The PixiJS container that holds this symbol's visual. */\n  public readonly view: Container;\n\n  private _symbolId: string = '';\n  private _isDestroyed = false;\n\n  private _gsap: Gsap = DEFAULT_GSAP;\n  private _mainAxis: 'x' | 'y' = 'y';\n\n  constructor() {\n    this.view = new Container();\n  }\n\n  /**\n   * The gsap instance this symbol should animate on. Use it instead of\n   * importing `gsap` in a subclass: under a symlinked-workspace module\n   * resolution your import and the engine's can be different instances, and\n   * only this one is on the timeline the reel set actually drives.\n   *\n   * Bound to the owning set by `SymbolFactory`; falls back to the instance\n   * resolved at lib-load time for a symbol built outside a set.\n   */\n  protected get gsap(): Gsap {\n    return this._gsap;\n  }\n\n  /**\n   * @internal. Called by `SymbolFactory` when the symbol is created, so a\n   * pooled symbol animates on its own set's gsap rather than whichever set\n   * happened to build last.\n   */\n  bindGsap(instance: Gsap): void {\n    this._gsap = instance;\n  }\n\n  /**\n   * The screen axis the owning set's strips travel along: `'y'` for a\n   * vertical set, `'x'` for a horizontal one.\n   *\n   * Symbols are otherwise orientation-agnostic - `resize(width, height)` is\n   * screen-space and always will be. This exists for the few effects that\n   * genuinely follow travel, motion blur being the one in the box.\n   */\n  protected get mainAxis(): 'x' | 'y' {\n    return this._mainAxis;\n  }\n\n  /** @internal. Bound by `SymbolFactory` from the set's orientation. */\n  bindMainAxis(prop: 'x' | 'y'): void {\n    this._mainAxis = prop;\n  }\n\n  get symbolId(): string {\n    return this._symbolId;\n  }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  /**\n   * Activate the symbol with a new identity. Called when the symbol enters\n   * the visible reel or is recycled from the pool. Resets container\n   * transform / filter state for parity with deactivate().\n   */\n  activate(symbolId: string): void {\n    this._symbolId = symbolId;\n    this.view.visible = true;\n    this.view.alpha = 1;\n    this.view.scale.set(1, 1);\n    this.view.rotation = 0;\n    this.view.filters = null;\n    this.view.zIndex = 0;\n    this.onActivate(symbolId);\n  }\n\n  /**\n   * Deactivate the symbol before returning it to the pool. Stops\n   * animations, hides the view, and resets container transform / filter\n   * state so subclass decorations don't leak across recycles.\n   */\n  deactivate(): void {\n    this.stopAnimation();\n    this.onDeactivate();\n    this._symbolId = '';\n    this.view.visible = false;\n    this.view.alpha = 1;\n    this.view.scale.set(1, 1);\n    this.view.rotation = 0;\n    this.view.filters = null;\n    this.view.zIndex = 0;\n  }\n\n  /** Pool reset. aliases deactivate. */\n  reset(): void {\n    this.deactivate();\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this.stopAnimation();\n    this.onDeactivate();\n    this.onDestroy();\n    if (!this.view.destroyed) this.view.destroy({ children: true });\n    this._isDestroyed = true;\n  }\n\n  /** Subclass hook: set up visuals for the given symbolId. */\n  protected abstract onActivate(symbolId: string): void;\n\n  /** Subclass hook: clean up visuals. */\n  protected abstract onDeactivate(): void;\n\n  /** Subclass hook: additional cleanup on destroy. */\n  protected onDestroy(): void {}\n\n  /** Play the win/highlight animation for this symbol. Resolves when complete. */\n  abstract playWin(): Promise<void>;\n\n  /** Immediately stop any running animation and return to idle. */\n  abstract stopAnimation(): void;\n\n  /** Resize the symbol's visual to fit the given dimensions. */\n  abstract resize(width: number, height: number): void;\n\n  /**\n   * Play the cascade-destruction animation for this symbol. Called by\n   * consumers (typically via `reelSet.destroySymbols(...)`) to disintegrate\n   * a winning cell before the next cascade refill drops fresh symbols in.\n   *\n   * Override in subclasses for art-appropriate destruction, e.g. a Spine\n   * symbol can play its `disintegration` track here, or a sprite symbol can\n   * swap to a shatter atlas. The promise must resolve when the symbol is no\n   * longer visible.\n   *\n   * Default: a snappy \"poof\" centered on the symbol's bounds regardless of\n   * the view's anchor. Tiny anticipation pop (~60 ms) then a fast implode to\n   * `scale: 0` + `alpha: 0` (~140 ms), ~200 ms total, no rotation. Reads\n   * cleanly under win-cluster pacing without competing with the win\n   * presenter. The view is left at `alpha: 0` (destroyed); position / pivot\n   * are restored so pool reuse via `_replaceSymbol`'s same-id fast path\n   * doesn't inherit a stale pivot offset.\n   *\n   * `opts.delay`. seconds to wait before the animation starts. Use to\n   * stagger a cluster of winners (e.g. `i * 0.015`).\n   * `opts.signal`. abort signal. If aborted (now or mid-animation), the\n   * tween is killed and the view is snapped to its destroyed pose\n   * (`alpha: 0`, transform restored). The promise resolves normally. abort\n   * means \"skip to the end,\" not \"fail\". Subclasses that override this\n   * method MUST honor the signal or document why they can't (e.g. a Spine\n   * `disintegration` track is uninterruptible).\n   */\n  async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n    const view = this.view;\n    // Capture original transform so pool reuse sees a clean state.\n    const originalPivotX = view.pivot.x;\n    const originalPivotY = view.pivot.y;\n    const originalX = view.x;\n    const originalY = view.y;\n\n    // Pivot to bounds-center so the scale collapses around the visual\n    // centre instead of the view's (0,0) corner. and compensate position\n    // so the symbol doesn't visibly jump when the pivot moves.\n    const bounds = view.getLocalBounds();\n    const cx = bounds.x + bounds.width / 2;\n    const cy = bounds.y + bounds.height / 2;\n    // Moving the pivot moves the view by `delta * scale`, not by `delta`: a\n    // container renders a local point at `position + (point - pivot) * scale`.\n    // Scale is 1 on a plain reel, which is why dropping it went unnoticed, but\n    // a curved reel scales every cell and the symbol would jump on destroy.\n    view.pivot.set(cx, cy);\n    view.x = originalX + (cx - originalPivotX) * view.scale.x;\n    view.y = originalY + (cy - originalPivotY) * view.scale.y;\n\n    const delay = opts?.delay ?? 0;\n    const signal = opts?.signal;\n\n    const snapDestroyed = (): void => {\n      view.alpha = 0;\n      view.scale.set(0, 0);\n    };\n\n    // Pre-abort: skip the tween entirely and snap to the destroyed pose.\n    if (signal?.aborted) {\n      snapDestroyed();\n      view.pivot.set(originalPivotX, originalPivotY);\n      view.x = originalX;\n      view.y = originalY;\n      view.scale.set(1, 1);\n      view.alpha = 0;\n      return;\n    }\n\n    await new Promise<void>((resolve) => {\n      const tl = this.gsap\n        .timeline({ onComplete: () => {\n          if (signal) signal.removeEventListener('abort', onAbort);\n          resolve();\n        }, delay })\n        // Brief anticipation pop. small upscale, ~60 ms, with overshoot\n        // so the implode reads as a release. No rotation.\n        .to(view.scale, { x: 1.1, y: 1.1, duration: 0.06, ease: 'back.out(2.5)' })\n        // Snap implode. scale -> 0 + alpha -> 0 together, snappy ease-in\n        // so the symbol collapses into the cell centre and is gone.\n        .to(view.scale, { x: 0, y: 0, duration: 0.14, ease: 'power3.in' }, '<+=0.04')\n        .to(view, { alpha: 0, duration: 0.14, ease: 'power3.in' }, '<');\n\n      const onAbort = (): void => {\n        tl.kill();\n        snapDestroyed();\n        resolve();\n      };\n      if (signal) signal.addEventListener('abort', onAbort, { once: true });\n    });\n\n    // Restore transform. alpha stays 0 (the symbol IS destroyed). Scale\n    // restored to 1 so pool reuse via `_replaceSymbol`'s same-id fast path\n    // doesn't inherit a stale 0× scale; _replaceSymbol also resets scale\n    // explicitly but a defensive restore here makes the destroyed cell\n    // observably \"ready to be re-skinned\" between calls.\n    view.pivot.set(originalPivotX, originalPivotY);\n    view.x = originalX;\n    view.y = originalY;\n    view.scale.set(1, 1);\n  }\n\n  /**\n   * The owning reel is curved: render into this projected quad instead of the\n   * flat cell rectangle. `null` means the reel is flat again.\n   *\n   * The quad's corners are SCREEN-space and local to this view's own origin,\n   * clockwise from top-left, and `width` / `height` give the flat cell box the\n   * quad replaces. Called on every frame of motion and on every placement, so\n   * it must be cheap and idempotent.\n   *\n   * **The default is an approximation.** A `Container` transform is affine, so\n   * it cannot express a trapezoid; the base class fits the quad as closely as\n   * an affine transform can - a UNIFORM scale about the quad's centre. Uniform\n   * on purpose: symbol art is usually smaller than its cell and has a shape a\n   * player recognises, and squashing one axis turns a `7` into a squashed `7`\n   * rather than a `7` seen at an angle.\n   *\n   * Override this to render the real perspective. {@link SpriteSymbol} and\n   * {@link AnimatedSpriteSymbol} do, by drawing their texture through a\n   * `PerspectiveMesh`, which costs no extra render pass because the content is\n   * already a texture. A symbol whose content is an arbitrary subtree (Spine, a\n   * composite of sprites and text) cannot do that without rendering itself to a\n   * texture every frame, so it keeps the affine fit.\n   *\n   * Whatever you do here, do NOT move `view.position`: the reel reads that\n   * coordinate back to work out which slot this symbol is in.\n   */\n  /**\n   * The part of its cell this symbol's art actually covers, or `null` (the\n   * default) for \"all of it\".\n   *\n   * Slot art is usually smaller than its cell - a trimmed atlas frame is a\n   * shape floating in a much bigger transparent box - and the reel needs to\n   * know that to project the rectangle the art is really in. Overriding this\n   * is what stops a small symbol being inflated to the cell's edges and given\n   * the cell's keystone instead of its own, milder one.\n   *\n   * Read once per projection, so it may change with the symbol's identity.\n   */\n  get cellInset(): ReelCellInset | null {\n    return null;\n  }\n\n  applyCellQuad(quad: ReelCellQuad | null): void {\n    const view = this.view;\n    if (quad === null) {\n      view.scale.set(1, 1);\n      view.pivot.set(0, 0);\n      return;\n    }\n    // Measure the trapezoid: the mean of its two parallel edges, and the\n    // distance between their midpoints.\n    const nearWidth = Math.hypot(quad.x1 - quad.x0, quad.y1 - quad.y0);\n    const farWidth = Math.hypot(quad.x2 - quad.x3, quad.y2 - quad.y3);\n    const across = (nearWidth + farWidth) / 2;\n    const along = Math.hypot(\n      (quad.x3 + quad.x2) / 2 - (quad.x0 + quad.x1) / 2,\n      (quad.y3 + quad.y2) / 2 - (quad.y0 + quad.y1) / 2,\n    );\n    // CONTAIN, not cover. A quad in the middle of the window is TALLER than\n    // the flat cell (that is the drum magnifying what faces you), so a scale\n    // picked to fill it would also make the symbol wider than its column and\n    // overlap its neighbours - very visible on art that fills its cell\n    // edge-to-edge. Taking the smaller ratio keeps every symbol inside its own\n    // projected footprint at the cost of a little slack on one axis.\n    const scale =\n      quad.width > 0 && quad.height > 0\n        ? Math.min(across / quad.width, along / quad.height)\n        : 1;\n\n    const cx = (quad.x0 + quad.x1 + quad.x2 + quad.x3) / 4;\n    const cy = (quad.y0 + quad.y1 + quad.y2 + quad.y3) / 4;\n    view.scale.set(scale, scale);\n    // A container renders a local point at `position + (point - pivot) * scale`\n    // and `position` must not move, so putting the flat box's centre on the\n    // quad's centre has to be paid for entirely out of the pivot.\n    view.pivot.set(\n      quad.x + quad.width / 2 - cx / scale,\n      quad.y + quad.height / 2 - cy / scale,\n    );\n  }\n\n  /**\n   * Lifecycle hook: the owning reel is spinning.\n   * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayBlur,\n   * StaticSpinSymbol) to swap to a spin presentation automatically.\n   *\n   * Fired on every strip symbol (visible AND buffer cells) when the reel\n   * enters the spin phase, and again with `joinedMidSpin: true` on each\n   * symbol freshly installed while the reel is already spinning (pool\n   * recycling wipes symbol state, so a wrapped-in symbol can't know the\n   * reel is moving without this). Implementations MUST be idempotent.\n   * the same instance can be notified more than once per spin.\n   *\n   * @param joinedMidSpin true when this symbol was installed into a reel\n   * already at speed (skip start-of-spin transitions like blur ramps).\n   */\n  onReelSpinStart(joinedMidSpin?: boolean): void {}\n\n  /**\n   * Lifecycle hook: the owning reel is about to stop (just before bounce).\n   * Default: no-op.\n   */\n  onReelSpinEnd(): void {}\n\n  /**\n   * Lifecycle hook: the owning reel entered its anticipation (tease) phase.\n   * it is still spinning, but slowed enough that the strip is readable.\n   * Spin presentations that obscure symbols (blur textures, smear\n   * animations) should relax so the player can follow the tease. Also\n   * fired on symbols installed while the reel is anticipating.\n   * Implementations MUST be idempotent. Default: no-op.\n   */\n  onReelAnticipationStart(): void {}\n\n  /**\n   * Lifecycle hook: the owning reel has landed on its final symbols.\n   * Default: no-op. Override (e.g. SpineReelSymbol.autoPlayLanding) to fire\n   * a landing animation concurrently with the bounce.\n   */\n  onReelLanded(): void {}\n}\n"],"mappings":"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"}