{"version":3,"file":"spine.cjs","names":[],"sources":["../src/spine/SpineReelSymbol.ts"],"sourcesContent":["import { Spine, type TrackEntry } from '@esotericsoftware/spine-pixi-v8';\nimport { ReelSymbol } from '../symbols/ReelSymbol.js';\n\n/**\n * Per-symbol overrides so a skeleton with unusual animation names still works.\n *\n * Example: Bonanza's `low_1` has a typo (`ide` instead of `idle`).\n * `{ low1: { idle: 'ide' } }` fixes it without touching the asset.\n */\nexport type SymbolAnimOverrides = Record<\n  string,\n  Partial<Record<'idle' | 'landing' | 'win' | 'out' | 'blur', string>>\n>;\n\n/** One spine source: a skeleton/atlas alias pair plus an optional skin. */\nexport interface SpineSymbolSource {\n  skeleton: string;\n  atlas: string;\n  /**\n   * Skin applied when the instance is created. Lets several symbolIds share\n   * one multi-skin skeleton (e.g. a `lowSymbols` skeleton with skins\n   * `low1`..`low5`) instead of shipping one skeleton per symbol.\n   */\n  skin?: string;\n}\n\nexport interface SpineReelSymbolOptions {\n  /** Map of symbolId -> spine source. */\n  spineMap: Record<string, SpineSymbolSource>;\n  /** Default idle animation name. Default: 'idle'. */\n  idleAnimation?: string;\n  /** Default win animation name. Default: 'win'. */\n  winAnimation?: string;\n  /** Default landing (one-shot) animation name. Default: 'landing'. */\n  landingAnimation?: string;\n  /**\n   * Default \"exit\" animation name. used as the cascade pop / disintegrate.\n   * If the skeleton doesn't have it, callers should fall back to an alpha tween.\n   * Default: 'disintegration'.\n   */\n  outAnimation?: string;\n  /** Default \"blur\" (fast spin) animation name. Default: 'blur'. */\n  blurAnimation?: string;\n  /** Per-symbol overrides (see SymbolAnimOverrides). */\n  animations?: SymbolAnimOverrides;\n  /** Scale for spine instances. Default: 1. */\n  scale?: number;\n  /**\n   * If true, automatically plays the `blur` animation when the owning reel\n   * enters the spin phase, and reverts to idle when it lands. Requires the\n   * skeleton to have a `blur` animation (or the overridden name). Default: false.\n   */\n  autoPlayBlur?: boolean;\n  /**\n   * If true, automatically plays the `landing` animation concurrently with\n   * the stop-phase bounce. Requires the skeleton to have a `landing`\n   * animation (or the overridden name). Default: false.\n   */\n  autoPlayLanding?: boolean;\n}\n\n/**\n * ReelSymbol implementation using Spine 2D skeletons.\n *\n * Caches one Spine instance per symbolId for instant swapping, plays idle on\n * activate, and exposes the canonical set of one-shot animations (`landing`,\n * `win`, `out`, reactions). Modeled on the Bonanza / Hold & Win slot-game\n * conventions. drop in any skeleton that follows the same vocabulary.\n *\n * Import from the `pixi-reels/spine` subpath so non-Spine consumers can\n * tree-shake this module and `@esotericsoftware/spine-pixi-v8` out of their\n * production bundle:\n *\n * ```ts\n * import { SpineReelSymbol } from 'pixi-reels/spine';\n * ```\n */\nexport class SpineReelSymbol extends ReelSymbol {\n  private _spines = new Map<string, Spine>();\n  private _currentSpine: Spine | null = null;\n  private _spineMap: Record<string, SpineSymbolSource>;\n  private _defaultAnims: { idle: string; win: string; landing: string; out: string; blur: string };\n  private _overrides: SymbolAnimOverrides;\n  private _scale: number;\n  private _autoPlayBlur: boolean;\n  private _autoPlayLanding: boolean;\n  private _oneShotResolve: (() => void) | null = null;\n  private _cellWidth = 0;\n  private _cellHeight = 0;\n\n  constructor(options: SpineReelSymbolOptions) {\n    super();\n    this._spineMap = options.spineMap;\n    this._defaultAnims = {\n      idle: options.idleAnimation ?? 'idle',\n      win: options.winAnimation ?? 'win',\n      landing: options.landingAnimation ?? 'landing',\n      out: options.outAnimation ?? 'disintegration',\n      blur: options.blurAnimation ?? 'blur',\n    };\n    this._overrides = options.animations ?? {};\n    this._scale = options.scale ?? 1;\n    this._autoPlayBlur = options.autoPlayBlur ?? false;\n    this._autoPlayLanding = options.autoPlayLanding ?? false;\n  }\n\n  override onReelSpinStart(): void {\n    if (this._autoPlayBlur) this.playBlur();\n  }\n\n  override onReelSpinEnd(): void {\n    if (this._autoPlayBlur) this.stopAnimation();\n  }\n\n  override onReelLanded(): void {\n    if (this._autoPlayLanding) {\n      void this.playLanding();\n    }\n  }\n\n  /** Resolve an animation name for the current symbol, respecting overrides. */\n  private _animNameFor(role: 'idle' | 'landing' | 'win' | 'out' | 'blur'): string {\n    const override = this._overrides[this.symbolId]?.[role];\n    return override ?? this._defaultAnims[role];\n  }\n\n  protected onActivate(symbolId: string): void {\n    if (this._currentSpine) this._currentSpine.visible = false;\n\n    let spine = this._spines.get(symbolId);\n    if (!spine) {\n      const cfg = this._spineMap[symbolId];\n      if (!cfg) return;\n      spine = this._createSpine(cfg);\n      this._spines.set(symbolId, spine);\n    }\n\n    this._positionSpine(spine);\n    spine.visible = true;\n    this._currentSpine = spine;\n\n    // Settle any lingering one-shot resolve from a prior pool use so the\n    // caller's `await playWin()` doesn't dangle when the symbol is\n    // recycled mid-animation.\n    this._resolveOneShot();\n\n    const idleName = this._animNameFor('idle');\n    if (spine.skeleton.data.findAnimation(idleName)) {\n      spine.state.setAnimation(0, idleName, true);\n    }\n    // Apply the pose NOW. a freshly created (or setup-pose-reset) skeleton\n    // has no applied state until its next ticker update, so anything that\n    // renders it synchronously after activation. a same-frame first paint,\n    // or a SpinTextureCache snapshot capture. would see nothing.\n    spine.update(0);\n  }\n\n  /** Instantiate a spine from a source config: scale, optional skin, attach. */\n  private _createSpine(cfg: SpineSymbolSource): Spine {\n    const spine = Spine.from({ skeleton: cfg.skeleton, atlas: cfg.atlas });\n    spine.scale.set(this._scale);\n    if (cfg.skin) {\n      spine.skeleton.setSkinByName(cfg.skin);\n      spine.skeleton.setSlotsToSetupPose();\n    }\n    this.view.addChild(spine);\n    return spine;\n  }\n\n  protected onDeactivate(): void {\n    if (this._currentSpine) {\n      this._currentSpine.state.clearTracks();\n      this._currentSpine.state.removeListener(this._currentListener);\n      // Reset the skeleton to its setup pose, otherwise a symbol that ends on\n      // an invisible \"out\" frame (e.g. after `playOut()` / disintegrate) is\n      // still invisible when the pool reassigns it on the next spin.\n      this._currentSpine.skeleton.setToSetupPose();\n      this._currentSpine.visible = false;\n      this._currentSpine = null;\n    }\n    this._resolveOneShot();\n  }\n\n  // -- Canonical one-shot animations ---------------------------------------\n\n  /** Play the win animation on track 0. Returns when it completes. */\n  async playWin(): Promise<void> {\n    return this._playOneShot(this._animNameFor('win'), 0, true);\n  }\n\n  /**\n   * Play the landing animation (one-shot). Call this when the reel settles.\n   * typically inside a `spin:reelLanded` listener.\n   */\n  async playLanding(): Promise<void> {\n    return this._playOneShot(this._animNameFor('landing'), 0, true);\n  }\n\n  /**\n   * Play the exit / disintegrate animation. Returns a promise that resolves\n   * when it completes. Use in cascades instead of the default alpha fade.\n   */\n  async playOut(): Promise<void> {\n    return this._playOneShot(this._animNameFor('out'), 0, false);\n  }\n\n  /**\n   * Cascade-destruction override. If the skeleton has the configured\n   * `out` (disintegration) animation, play it. Otherwise fall back to the\n   * base class's GSAP scale-and-fade so a partial skeleton still cascades\n   * cleanly. `opts.delay` is honored (seconds, mirrors the GSAP version)\n   * so callers can stagger a winning cluster. `opts.signal` aborts\n   * the (pre-delay or in-flight) animation early. the spine state is\n   * snapped to the next track entry and the resolve fires immediately;\n   * the view is left at `alpha: 0` for parity with the GSAP fallback so\n   * the destroyed pose is consistent across symbol kinds.\n   */\n  override async playDestroy(opts?: { delay?: number; signal?: AbortSignal }): Promise<void> {\n    const outName = this._animNameFor('out');\n    const hasOut = !!this._currentSpine?.skeleton.data.findAnimation(outName);\n    if (!hasOut) return super.playDestroy(opts);\n\n    const signal = opts?.signal;\n    if (signal?.aborted) {\n      this.view.alpha = 0;\n      return;\n    }\n\n    const delay = opts?.delay ?? 0;\n    if (delay > 0) {\n      // Honor abort during the pre-delay window. skip the disintegrate\n      // entirely if the player slammed before it could start.\n      const aborted = await new Promise<boolean>((resolve) => {\n        const t = setTimeout(() => {\n          if (signal) signal.removeEventListener('abort', onAbort);\n          resolve(false);\n        }, delay * 1000);\n        const onAbort = (): void => {\n          clearTimeout(t);\n          resolve(true);\n        };\n        if (signal) signal.addEventListener('abort', onAbort, { once: true });\n      });\n      if (aborted) {\n        this.view.alpha = 0;\n        return;\n      }\n    }\n\n    // Race the play against abort. On abort, settle the one-shot promise\n    // (which resolves the awaiter) and snap the view to the destroyed pose\n    //. the underlying spine track ticks until the next track set, but\n    // alpha=0 keeps it invisible.\n    if (signal) {\n      const onAbort = (): void => {\n        this._resolveOneShot();\n        this.view.alpha = 0;\n      };\n      if (signal.aborted) {\n        onAbort();\n        return;\n      }\n      signal.addEventListener('abort', onAbort, { once: true });\n      try {\n        await this.playOut();\n      } finally {\n        signal.removeEventListener('abort', onAbort);\n      }\n      return;\n    }\n    return this.playOut();\n  }\n\n  /**\n   * Swap the primary track to the blur animation for the SPIN phase. Reverts\n   * to idle automatically on `stopAnimation()` or next activate.\n   *\n   * If a one-shot promise (`playWin` / `playLanding` / `playOut`) is in\n   * flight on the same track, settle it first so the caller's `await`\n   * doesn't dangle when its track is hijacked.\n   */\n  playBlur(): void {\n    if (!this._currentSpine) return;\n    const name = this._animNameFor('blur');\n    if (!this._currentSpine.skeleton.data.findAnimation(name)) return;\n    // Idempotent: the reel re-notifies spin state on mid-spin symbol\n    // installs; restarting the loop each time would snap it to frame 0.\n    if (this._currentSpine.state.getCurrent(0)?.animation?.name === name) return;\n    this._resolveOneShot();\n    this._currentSpine.state.setAnimation(0, name, true);\n  }\n\n  /** Play an arbitrary animation on a given track. Non-blocking. */\n  playOnTrack(track: number, animName: string, loop = false): TrackEntry | null {\n    if (!this._currentSpine) return null;\n    if (!this._currentSpine.skeleton.data.findAnimation(animName)) return null;\n    return this._currentSpine.state.setAnimation(track, animName, loop);\n  }\n\n  stopAnimation(): void {\n    if (!this._currentSpine) return;\n    this._resolveOneShot();\n    const idleName = this._animNameFor('idle');\n    if (this._currentSpine.skeleton.data.findAnimation(idleName)) {\n      this._currentSpine.state.setAnimation(0, idleName, true);\n    }\n  }\n\n  /** Access the underlying Spine. for advanced needs (reactions, events). */\n  get spine(): Spine | null {\n    return this._currentSpine;\n  }\n\n  // -- Internals -----------------------------------------------------------\n\n  private _currentListener: {\n    complete?: (entry: TrackEntry) => void;\n  } = {};\n\n  /**\n   * Settle any one-shot promise currently in flight and remove its\n   * listener. Safe to call when no one-shot is pending. both fields are\n   * cleared.\n   *\n   * Called by `onActivate`, `onDeactivate`, `stopAnimation`, `playBlur`,\n   * and the start of every new `_playOneShot` so that:\n   *   - a `playWin()` promise resolves rather than hangs when the symbol\n   *     is recycled mid-animation,\n   *   - calling `playOut` while `playWin` is in flight resolves the\n   *     `playWin` promise (its track was hijacked),\n   *   - the prior listener is detached so spine state doesn't leak\n   *     listeners across consecutive one-shots.\n   */\n  private _resolveOneShot(): void {\n    if (this._currentSpine) {\n      this._currentSpine.state.removeListener(this._currentListener);\n    }\n    this._currentListener = {};\n    if (this._oneShotResolve) {\n      const fn = this._oneShotResolve;\n      this._oneShotResolve = null;\n      fn();\n    }\n  }\n\n  private async _playOneShot(\n    animName: string,\n    track: number,\n    returnToIdle: boolean,\n  ): Promise<void> {\n    if (!this._currentSpine) return;\n    const spine = this._currentSpine;\n    if (!spine.skeleton.data.findAnimation(animName)) return;\n\n    // Settle any prior one-shot before starting a new one. without this,\n    // back-to-back `playWin()` / `playOut()` calls would silently abandon\n    // the first promise.\n    this._resolveOneShot();\n\n    return new Promise<void>((resolve) => {\n      this._oneShotResolve = resolve;\n      const entry = spine.state.setAnimation(track, animName, false);\n      const listener = {\n        complete: (done: TrackEntry) => {\n          // Track-entry guard: ignore completes from earlier or unrelated\n          // entries on the same track (e.g. an idle loop that happens to\n          // wrap while we're queued). Only resolve when our exact entry\n          // finishes.\n          if (done !== entry) return;\n          spine.state.removeListener(this._currentListener);\n          this._currentListener = {};\n          if (returnToIdle) {\n            const idleName = this._animNameFor('idle');\n            if (spine.skeleton.data.findAnimation(idleName)) {\n              spine.state.setAnimation(track, idleName, true);\n            }\n          }\n          if (this._oneShotResolve) {\n            const fn = this._oneShotResolve;\n            this._oneShotResolve = null;\n            fn();\n          }\n        },\n      };\n      this._currentListener = listener;\n      spine.state.addListener(listener);\n    });\n  }\n\n  resize(width: number, height: number): void {\n    this._cellWidth = width;\n    this._cellHeight = height;\n    for (const [, spine] of this._spines) this._positionSpine(spine);\n  }\n\n  private _positionSpine(spine: Spine): void {\n    spine.x = this._cellWidth / 2;\n    spine.y = this._cellHeight / 2;\n  }\n\n  protected override onDestroy(): void {\n    for (const [, spine] of this._spines) {\n      spine.state.clearListeners();\n      spine.destroy();\n    }\n    this._spines.clear();\n  }\n}\n"],"mappings":"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"}