import { Node, sp, Tween } from 'cc'; import { GBundle } from '../../game/ConfigRes'; import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import type { AttackRangeBuffFxPreview, BattleBuffSkillDropTarget, CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { TextGrid } from '../../battle/view/TextGrid'; /** 千里眼预制体名 */ const ATTACK_RANGE_FX_PREFAB_NAME = '千里眼'; /** 千里眼 Spine 动画名 */ const ATTACK_RANGE_FX_ANIM_NAME = 'animation'; /** * 千里眼主动 buff 施法表现:字块中心播千里眼动画,与 buff 逻辑并行。 */ export class BattleGameAttackRangeFxPresenter { private _bagView: CocosBagBoardView | null = null; private readonly _activeFxNodes: Node[] = []; /** 绑定背包视图 */ bind(bagView: CocosBagBoardView | null, _flyLayer: Node | null | undefined): void { this.unbind(); this._bagView = bagView; } /** 注销并中断全部进行中的表现 */ unbind(): void { this._abortAllFx(); this._bagView = null; } /** * 在目标字块中心播放千里眼动画(纯表现,不阻塞 buff 生效)。 * @returns 是否成功起播 */ tryPlay(target: BattleBuffSkillDropTarget, preview: AttackRangeBuffFxPreview | null = null): boolean { const bagView = this._bagView; if (!bagView) return false; if (preview) { bagView.playAttackRangeBuffRangeTween(preview); } const grid = bagView.getTextGridForDropTarget(target); if (!grid?.node?.isValid) { console.log(`[AttackRangeFx] 目标字块视图缺失 ${target.kind}`); return false; } const fx = this._spawnFxAtTextCenter(grid); if (!fx) { console.log('[AttackRangeFx] 千里眼预制体加载失败'); return false; } const skel = fx.getComponent(sp.Skeleton) ?? fx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[AttackRangeFx] 千里眼缺少 sp.Skeleton'); this._releaseFx(fx); return false; } BattleSpinePlayback.play(skel, ATTACK_RANGE_FX_ANIM_NAME, { loop: false, onComplete: this._onAnimComplete.bind(this, fx, skel), }); return true; } /** 挂到字块根节点;多格组合字时锚点对齐两格中心 */ private _spawnFxAtTextCenter(grid: TextGrid): Node | null { if (!grid?.node?.isValid) return null; let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, ATTACK_RANGE_FX_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[AttackRangeFx] 千里眼加载异常', String(e)); return null; } if (!node?.isValid) return null; grid.attachBattleBuffFx(node, 'under'); this._activeFxNodes.push(node); return node; } /** 动画播完:回收单个预制体 */ private _onAnimComplete(fx: Node, skel: sp.Skeleton): void { if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); this._releaseFx(fx); } /** 回收千里眼节点 */ private _releaseFx(node: Node | null): void { if (!node?.isValid) return; const idx = this._activeFxNodes.indexOf(node); if (idx >= 0) this._activeFxNodes.splice(idx, 1); Tween.stopAllByTarget(node); if (node['resKey']) gg.res.putNode(node); else node.destroy(); } /** 中断并清理全部表现节点 */ private _abortAllFx(): void { const pending = this._activeFxNodes.slice(); this._activeFxNodes.length = 0; for (let i = 0; i < pending.length; i++) { const node = pending[i]; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); this._releaseFx(node); } } }