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 { BattleBuffSkillDropTarget, CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { TextGrid } from '../../battle/view/TextGrid'; /** 封神榜上层预制体(字块上方) */ const UNIT_LEVEL_UP_UPPER_PREFAB_NAME = '封神榜上层'; /** 封神榜预制体(字块下方) */ const UNIT_LEVEL_UP_LOWER_PREFAB_NAME = '封神榜'; /** 上层 Spine 动画 */ const UNIT_LEVEL_UP_UPPER_ANIM_NAME = '授仙箓'; /** 下层 Spine 动画 */ const UNIT_LEVEL_UP_LOWER_ANIM_NAME = '升级效果'; /** 封神榜特效相对锚点 Y 偏移 */ const UNIT_LEVEL_UP_FX_OFFSET_Y = 20; import type { TextGridBattleBuffFxLayer } from '../../battle/view/TextGrid'; /** * 封神榜(functiontype=2):先播上层授仙箓,播完再升级(合成武将补剩余升级经验)并播下层升级效果。 */ export class BattleGameUnitLevelUpFxPresenter { private _bagView: CocosBagBoardView | null = null; private readonly _activeFxNodes: Node[] = []; private _playing = false; /** 绑定背包视图 */ bind(bagView: CocosBagBoardView | null, _flyLayer: Node | null | undefined): void { this.unbind(); this._bagView = bagView; } /** 注销并中断全部进行中的表现 */ unbind(): void { this._abortAllFx(); this._bagView = null; } /** * 上层动画播完后升级,并播下层升级效果。 * @param target 地图格 / 背包槽 * @param levelDelta functionpar 等级增量 * @param onApplied 升级成功并进入 CD */ tryPlay(target: BattleBuffSkillDropTarget, levelDelta: number, onApplied: () => void): boolean { if (this._playing) return false; const bagView = this._bagView; if (!bagView) return false; if (!bagView.canApplyBattleBuffLevelUpTarget(target, levelDelta)) return false; const grid = bagView.getTextGridForDropTarget(target); if (!grid?.node?.isValid) { console.log(`[UnitLevelUpFx] 目标字块视图缺失 ${target.kind}`); return false; } const upperFx = this._spawnFxOnText(grid, UNIT_LEVEL_UP_UPPER_PREFAB_NAME, 'over'); if (!upperFx) { console.log('[UnitLevelUpFx] 封神榜上层预制体加载失败'); return false; } const skel = upperFx.getComponent(sp.Skeleton) ?? upperFx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[UnitLevelUpFx] 封神榜上层缺少 sp.Skeleton'); this._releaseFx(upperFx); return false; } this._playing = true; BattleSpinePlayback.play(skel, UNIT_LEVEL_UP_UPPER_ANIM_NAME, { loop: false, onComplete: this._onUpperAnimComplete.bind(this, upperFx, skel, target, levelDelta, grid, onApplied), }); return true; } /** 上层播完:回收上层 → 升级 → 播下层升级效果 */ private _onUpperAnimComplete( upperFx: Node, skel: sp.Skeleton, target: BattleBuffSkillDropTarget, levelDelta: number, grid: TextGrid, onApplied: () => void, ): void { if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); this._releaseFx(upperFx); const bagView = this._bagView; if (!bagView) { this._playing = false; return; } if (!bagView.canApplyBattleBuffLevelUpTarget(target, levelDelta)) { console.log(`[UnitLevelUpFx] 上层动画结束时目标已不可升级 ${target.kind}`); this._playing = false; return; } if (!bagView.applyBattleBuffLevelUpTarget(target, levelDelta)) { console.log(`[UnitLevelUpFx] 升级失败 ${target.kind}`); this._playing = false; return; } onApplied(); this._playLowerFx(grid); this._playing = false; } /** 播放下层封神榜升级效果 */ private _playLowerFx(grid: TextGrid): void { if (!grid.node?.isValid) return; const lowerFx = this._spawnFxOnText(grid, UNIT_LEVEL_UP_LOWER_PREFAB_NAME, 'under'); if (!lowerFx) { console.log('[UnitLevelUpFx] 封神榜预制体加载失败'); return; } const skel = lowerFx.getComponent(sp.Skeleton) ?? lowerFx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[UnitLevelUpFx] 封神榜缺少 sp.Skeleton'); this._releaseFx(lowerFx); return; } BattleSpinePlayback.play(skel, UNIT_LEVEL_UP_LOWER_ANIM_NAME, { loop: false, onComplete: this._onLowerAnimComplete.bind(this, lowerFx, skel), }); } /** 下层播完:回收 */ private _onLowerAnimComplete(fx: Node, skel: sp.Skeleton): void { if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); this._releaseFx(fx); } /** 挂到字块根节点并按层级插入 */ private _spawnFxOnText(grid: TextGrid, prefabName: string, layer: TextGridBattleBuffFxLayer): Node | null { if (!grid?.node?.isValid) return null; let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, prefabName, GBundle.BattleHero); } catch (e) { console.log('[UnitLevelUpFx] 预制体加载异常', prefabName, String(e)); return null; } if (!node?.isValid) return null; grid.attachBattleBuffFx(node, layer); const pos = node.position; node.setPosition(pos.x, pos.y + UNIT_LEVEL_UP_FX_OFFSET_Y, pos.z); this._activeFxNodes.push(node); return node; } /** 回收特效节点 */ 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); } this._playing = false; } }