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 { schedulePresentation } from '../../battle/skill/BattlePresentationClock'; import type { BattleBuffSkillDropTarget, CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { TextGrid } from '../../battle/view/TextGrid'; /** 授仙箓上层预制体(字块上方) */ const RANDOM_LEVEL_UPPER_PREFAB_NAME = '授仙箓上层'; /** 授仙箓预制体(字块下方) */ const RANDOM_LEVEL_LOWER_PREFAB_NAME = '授仙箓'; /** 上层 Spine 动画 */ const RANDOM_LEVEL_UPPER_ANIM_NAME = '授仙箓'; /** 下层升级 Spine 动画 */ const RANDOM_LEVEL_UP_ANIM_NAME = '升级效果'; /** 下层降级 Spine 动画 */ const RANDOM_LEVEL_DOWN_ANIM_NAME = '降级效果'; /** 下层相对上层延迟(逻辑秒) */ const LOWER_FX_DELAY_SEC = 1; /** 授仙箓特效相对锚点 Y 偏移 */ const RANDOM_LEVEL_FX_OFFSET_Y = 20; import type { TextGridBattleBuffFxLayer } from '../../battle/view/TextGrid'; /** * 授仙箓(functiontype=7):上层播授仙箓,播完随机升/降级(合成武将升级补剩余经验);下层延迟 1s 起播升/降级效果。 */ export class BattleGameRandomLevelFxPresenter { private _bagView: CocosBagBoardView | null = null; private readonly _activeFxNodes: Node[] = []; private _playing = false; /** 延迟下层特效世代(unbind 时递增以作废待触发回调) */ private _scheduleGen = 0; private _lowerDelayPassed = false; private _upperDone = false; private _pendingLevelDelta: number | null = null; private _pendingLowerGrid: TextGrid | null = null; /** 绑定背包视图 */ bind(bagView: CocosBagBoardView | null, _flyLayer: Node | null | undefined): void { this.unbind(); this._bagView = bagView; } /** 注销并中断全部进行中的表现 */ unbind(): void { this._scheduleGen++; this._abortAllFx(); this._bagView = null; } /** * 上层播完后随机升/降级;下层在距上层开始满 1s 且已有结果后播升/降级效果。 * @param target 地图格 / 背包槽 * @param levelUpProbsWan 授仙箓概率配置 * @param onApplied 升/降级成功并进入 CD */ tryPlay(target: BattleBuffSkillDropTarget, levelUpProbsWan: readonly number[], onApplied: () => void): boolean { if (this._playing) return false; const bagView = this._bagView; if (!bagView || levelUpProbsWan.length < 4) return false; if (!bagView.canApplyBattleBuffRandomLevelTarget(target)) return false; const grid = bagView.getTextGridForDropTarget(target); if (!grid?.node?.isValid) { console.log(`[RandomLevelFx] 目标字块视图缺失 ${target.kind}`); return false; } const upperFx = this._spawnFxOnText(grid, RANDOM_LEVEL_UPPER_PREFAB_NAME, 'over'); if (!upperFx) { console.log('[RandomLevelFx] 授仙箓上层预制体加载失败'); return false; } const skel = upperFx.getComponent(sp.Skeleton) ?? upperFx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[RandomLevelFx] 授仙箓上层缺少 sp.Skeleton'); this._releaseFx(upperFx); return false; } this._playing = true; this._lowerDelayPassed = false; this._upperDone = false; this._pendingLevelDelta = null; this._pendingLowerGrid = grid; const scheduleGen = ++this._scheduleGen; schedulePresentation(LOWER_FX_DELAY_SEC, () => { if (scheduleGen !== this._scheduleGen) return; this._lowerDelayPassed = true; this._tryPlayLowerFx(); }); BattleSpinePlayback.play(skel, RANDOM_LEVEL_UPPER_ANIM_NAME, { loop: false, onComplete: this._onUpperAnimComplete.bind(this, upperFx, skel, target, levelUpProbsWan, onApplied), }); return true; } /** 上层播完:回收上层 → 随机升/降级 */ private _onUpperAnimComplete( upperFx: Node, skel: sp.Skeleton, target: BattleBuffSkillDropTarget, levelUpProbsWan: readonly number[], onApplied: () => void, ): void { if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel); this._releaseFx(upperFx); const bagView = this._bagView; if (!bagView) { this._resetPlayState(); return; } if (!bagView.canApplyBattleBuffRandomLevelTarget(target)) { console.log(`[RandomLevelFx] 上层动画结束时目标已不可施放 ${target.kind}`); this._resetPlayState(); return; } const levelDelta = bagView.applyBattleBuffRandomLevelTarget(target, levelUpProbsWan); if (levelDelta == null) { console.log(`[RandomLevelFx] 随机升/降级失败 ${target.kind}`); this._resetPlayState(); return; } onApplied(); this._pendingLevelDelta = levelDelta; this._upperDone = true; this._tryPlayLowerFx(); } /** 下层:距上层开始满 1s 且升/降级结果已出时播放 */ private _tryPlayLowerFx(): void { if (!this._upperDone || !this._lowerDelayPassed) return; const levelDelta = this._pendingLevelDelta; const grid = this._pendingLowerGrid; this._resetPlayState(); if (levelDelta == null || levelDelta === 0 || !grid?.node?.isValid) return; const animName = levelDelta > 0 ? RANDOM_LEVEL_UP_ANIM_NAME : RANDOM_LEVEL_DOWN_ANIM_NAME; const lowerFx = this._spawnFxOnText(grid, RANDOM_LEVEL_LOWER_PREFAB_NAME, 'under'); if (!lowerFx) { console.log('[RandomLevelFx] 授仙箓预制体加载失败'); return; } const skel = lowerFx.getComponent(sp.Skeleton) ?? lowerFx.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[RandomLevelFx] 授仙箓缺少 sp.Skeleton'); this._releaseFx(lowerFx); return; } BattleSpinePlayback.play(skel, animName, { 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('[RandomLevelFx] 预制体加载异常', prefabName, String(e)); return null; } if (!node?.isValid) return null; grid.attachBattleBuffFx(node, layer); const pos = node.position; node.setPosition(pos.x, pos.y + RANDOM_LEVEL_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 _resetPlayState(): void { this._playing = false; this._lowerDelayPassed = false; this._upperDone = false; this._pendingLevelDelta = null; this._pendingLowerGrid = null; } /** 中断并清理全部表现节点 */ 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._resetPlayState(); } }