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 { BattlePhase } from 'db://assets/mx/module/battle'; import type { CharmTarget } from '../../battle/mechanism/MechanismServices'; import { composeKey } from '../../battle/runtime/ComposeUnitKey'; import type { CocosBagBoardView } from '../../battle/view/CocosBagBoardView'; import type { HeroSkin } from '../../battle/view/HeroSkin'; import { TextGrid, type TextGridBattleBuffFxLayer } from '../../battle/view/TextGrid'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; /** 施法幻魂镜预制体(battleHero/prefab/buff/幻魂镜) */ const CONFUSE_HERO_CAST_PREFAB_NAME = '幻魂镜'; /** 幻魂镜下层预制体 */ const CONFUSE_HERO_LOWER_PREFAB_NAME = '幻魂镜下层'; /** 幻魂镜上层预制体 */ const CONFUSE_HERO_UPPER_PREFAB_NAME = '幻魂镜上层'; /** 施法 Spine 动画名 */ const CONFUSE_HERO_CAST_ANIM_NAME = '施法效果'; /** 下层循环 Spine 动画名 */ const CONFUSE_HERO_LOWER_ANIM_NAME = '技能效果'; /** 上层循环 Spine 动画名 */ const CONFUSE_HERO_UPPER_ANIM_NAME = '技能效果2'; /** 施法后延迟添加循环层(逻辑秒) */ const LOOP_FX_DELAY_SEC = 1; /** * 幻魂镜(functiontype=22):施法效果 → 1s 后上下层循环技能效果,同步施加迷惑。 */ export class BattleGameConfuseHeroFxPresenter { private _battle: ProjectBattleHandle | null = null; private _bagView: CocosBagBoardView | null = null; private _playing = false; private readonly _activeFxNodes: Node[] = []; /** 延迟回调世代(unbind 时递增以作废待触发回调) */ private _scheduleGen = 0; /** 当前施法幻魂镜节点(1s 后回收) */ private _castFxNode: Node | null = null; /** 本次施法目标 composeKey(合成变阵时用于中断表现) */ private _playTargetKey: string | null = null; /** 施法鬼手挂点字块 */ private _playGrid: TextGrid | null = null; /** 循环层挂点皮肤 */ private _playSkin: HeroSkin | null = null; /** 是否正在播放幻魂镜全流程 */ get isPlaying(): boolean { return this._playing; } /** * 绑定战斗与背包视图。 * @param battle 战斗句柄 * @param bagView 背包视图 * @param _flyLayer 保留参数(历史兼容,特效改挂字块) */ bind(battle: ProjectBattleHandle | null, bagView: CocosBagBoardView | null, _flyLayer: Node | null | undefined): void { this.unbind(); this._battle = battle; this._bagView = bagView; } /** 注销并中断进行中的表现 */ unbind(): void { this._scheduleGen++; this._abortAllFx(); this._battle = null; this._bagView = null; } /** * 合成变阵或魅惑解除:中断仍指向该 composeKey 的幻魂镜序列,并剥离残留鬼手。 * @param composeKey 合成体键 */ abortForComposeKey(composeKey: string): void { if (!composeKey) return; if (this._playTargetKey === composeKey) { this._scheduleGen++; this._abortAllFx(); } this._stripComposeVisuals(composeKey); } /** * 玩家拖到敌方神将:播施法 → 1s 后循环上下层并施加迷惑。 * @param target 敌方神将 * @param durationSec 迷幻时长(functionpar 第二段) * @param onApplied 施放成功并进入 CD */ tryPlay(target: CharmTarget, durationSec: number, onApplied: () => void): boolean { const bagView = this._bagView; if (!bagView || !bagView.canApplyConfuseHeroTarget(target)) return false; return this._beginPlay(target, durationSec, onApplied); } /** * AI 对玩家神将施放:跳过敌方阵营校验,其余表现与玩家一致。 * @param target 神将目标 * @param durationSec 迷幻时长 */ tryPlayFromAi(target: CharmTarget, durationSec: number): boolean { const battle = this._battle; if (!battle || !this._validateCharmTarget(battle, target)) return false; return this._beginPlay(target, durationSec, null); } /** * 启动幻魂镜表现序列。 * @param target 神将目标 * @param durationSec 迷幻时长 * @param onApplied 进入 CD 回调;AI 可为 null */ private _beginPlay(target: CharmTarget, durationSec: number, onApplied: (() => void) | null): boolean { if (this._playing) return false; if (durationSec <= 0) return false; if (this._activeFxNodes.length > 0 || this._castFxNode) this._abortAllFx(); const battle = this._battle; const bagView = this._bagView; if (!battle || !bagView) return false; const textGrid = this._resolveTargetTextGrid(bagView, target); if (!textGrid?.node?.isValid) { console.log('[ConfuseHeroFx] 目标字块视图缺失', target.composeKey); return false; } const castFx = this._spawnFxOnTextGrid(textGrid, CONFUSE_HERO_CAST_PREFAB_NAME, 'over'); if (!castFx) return false; const castSkel = castFx.getComponent(sp.Skeleton) ?? castFx.getComponentInChildren(sp.Skeleton); if (!castSkel?.isValid) { console.log('[ConfuseHeroFx] 幻魂镜缺少 sp.Skeleton'); this._releaseFx(castFx); return false; } onApplied?.(); this._playing = true; this._playTargetKey = target.composeKey; this._playGrid = textGrid; this._castFxNode = castFx; const scheduleGen = ++this._scheduleGen; const totalSec = LOOP_FX_DELAY_SEC + durationSec; const started = BattleSpinePlayback.play(castSkel, CONFUSE_HERO_CAST_ANIM_NAME, { loop: false, onComplete: this._onCastAnimComplete.bind(this, castFx, castSkel, scheduleGen), }); if (!started) { this._abortAllFx(); return false; } schedulePresentation(LOOP_FX_DELAY_SEC, () => { if (scheduleGen !== this._scheduleGen) return; this._startLoopPhase(target, durationSec, scheduleGen); }); schedulePresentation(totalSec, () => { if (scheduleGen !== this._scheduleGen) return; this._finishPlaySequence(); }); return true; } /** 施法幻魂镜播完:回收施法预制体 */ private _onCastAnimComplete(castFx: Node, castSkel: sp.Skeleton, scheduleGen: number): void { if (scheduleGen !== this._scheduleGen) return; if (castSkel?.isValid) BattleSpinePlayback.clearPendingComplete(castSkel); if (this._castFxNode === castFx) this._castFxNode = null; this._releaseFx(castFx); } /** * 1s 后:回收施法鬼手,添加上下层循环特效并施加迷惑。 * @param target 神将目标 * @param durationSec 迷幻时长 * @param scheduleGen 回调世代 */ private _startLoopPhase(target: CharmTarget, durationSec: number, scheduleGen: number): void { if (scheduleGen !== this._scheduleGen) return; const battle = this._battle; const bagView = this._bagView; if (!battle || !bagView) { this._finishPlaySequence(); return; } if (!this._validateCharmTarget(battle, target)) { console.log('[ConfuseHeroFx] 循环层启动时目标已不可迷惑', target.composeKey); this._finishPlaySequence(); return; } this._recycleCastFx(); const heroSkin = bagView.resolveConfuseHeroVisualSkin(target); if (!heroSkin?.displayNode?.isValid) { console.log('[ConfuseHeroFx] 神将合成预制体未就绪', target.composeKey); this._finishPlaySequence(); return; } this._playSkin = heroSkin; const lowerFx = this._spawnLoopFxOnHeroSkin(heroSkin, CONFUSE_HERO_LOWER_PREFAB_NAME, 'lower'); const upperFx = this._spawnLoopFxOnHeroSkin(heroSkin, CONFUSE_HERO_UPPER_PREFAB_NAME, 'upper'); if (!lowerFx || !upperFx) { console.log('[ConfuseHeroFx] 幻魂镜上下层预制体加载失败'); this._releaseFx(lowerFx); this._releaseFx(upperFx); this._finishPlaySequence(); return; } const lowerSkel = lowerFx.getComponent(sp.Skeleton) ?? lowerFx.getComponentInChildren(sp.Skeleton); const upperSkel = upperFx.getComponent(sp.Skeleton) ?? upperFx.getComponentInChildren(sp.Skeleton); if (!lowerSkel?.isValid || !upperSkel?.isValid) { console.log('[ConfuseHeroFx] 幻魂镜上下层缺少 sp.Skeleton'); this._releaseFx(lowerFx); this._releaseFx(upperFx); this._finishPlaySequence(); return; } BattleSpinePlayback.play(lowerSkel, CONFUSE_HERO_LOWER_ANIM_NAME, { loop: true }); BattleSpinePlayback.play(upperSkel, CONFUSE_HERO_UPPER_ANIM_NAME, { loop: true }); battle.charmController.applyCharm(target, durationSec, null); } /** 迷幻时长结束:回收循环层并结束占用 */ private _finishPlaySequence(): void { this._recycleCastFx(); const pending = this._activeFxNodes.slice(); this._activeFxNodes.length = 0; for (let i = 0; i < pending.length; i++) { this._releaseFx(pending[i]); } this._stripPlayVisuals(); if (this._playTargetKey) this._stripComposeVisuals(this._playTargetKey); this._resetPlayState(); } /** 校验目标仍可被迷惑(不含敌方阵营校验) */ private _validateCharmTarget(battle: ProjectBattleHandle, target: CharmTarget): boolean { if (battle.session.ended || battle.session.phase !== BattlePhase.COMBAT) return false; if (battle.charmController.isComposeKeyCharmed(target.composeKey)) return false; if (!battle.session.world.isAlive(target.entityId)) return false; return true; } /** 解析组合神将任一格的字块视图(含敌方 AiMapTextHost) */ private _resolveTargetTextGrid(bagView: CocosBagBoardView, target: CharmTarget): TextGrid | null { for (let i = 0; i < target.gridIndices.length; i++) { const grid = bagView.getTextGridForConfuseHeroTarget(target.gridIndices[i]); if (grid?.node?.isValid) return grid; } return bagView.getTextGridForConfuseHeroTarget(target.anchorGridIndex); } /** * 循环层:挂到 battleHero 合成预制体(下层在品质框上,上层在根节点顶)。 * @param heroSkin 神将皮肤 * @param prefabName 预制体名 * @param layer 上下层 */ private _spawnLoopFxOnHeroSkin(heroSkin: HeroSkin, prefabName: string, layer: 'lower' | 'upper'): Node | null { let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, prefabName, GBundle.BattleHero); } catch (e) { console.log('[ConfuseHeroFx] 预制体加载异常', prefabName, String(e)); return null; } if (!node?.isValid) { console.log('[ConfuseHeroFx] 预制体缺失', prefabName); return null; } const attached = layer === 'lower' ? heroSkin.attachConfuseMirrorLowerFx(node) : heroSkin.attachConfuseMirrorUpperFx(node); if (!attached) { this._releaseFx(node); return null; } this._activeFxNodes.push(node); return node; } /** * 施法层:挂到字块根节点(多格锚点对齐占格中心)。 * @param grid 字块 * @param prefabName 预制体名 * @param layer 上下层 */ private _spawnFxOnTextGrid(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('[ConfuseHeroFx] 预制体加载异常', prefabName, String(e)); return null; } if (!node?.isValid) { console.log('[ConfuseHeroFx] 预制体缺失', prefabName); return null; } grid.attachBattleBuffFx(node, layer); this._activeFxNodes.push(node); return node; } /** 回收特效节点 */ private _releaseFx(node: Node | null): void { if (!node) return; const idx = this._activeFxNodes.indexOf(node); if (idx >= 0) this._activeFxNodes.splice(idx, 1); if (!node.isValid) return; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (skel?.isValid) { BattleSpinePlayback.clearPendingComplete(skel); BattleSpinePlayback.resetTracks(skel); } const grid = node.parent?.getComponent(TextGrid); if (grid) { grid.releaseAttachedBattleBuffFx(node); return; } Tween.stopAllByTarget(node); if (node['resKey']) gg.res.putNode(node); else node.destroy(); } /** 回收施法鬼手预制体 */ private _recycleCastFx(): void { const castFx = this._castFxNode; this._castFxNode = null; this._releaseFx(castFx); } /** 剥离本次挂点上跟丢的幻魂镜节点 */ private _stripPlayVisuals(): void { const grid = this._playGrid; this._playGrid = null; grid?.releaseAttachedBattleBuffFxByName(CONFUSE_HERO_CAST_PREFAB_NAME); const skin = this._playSkin; this._playSkin = null; skin?.detachConfuseMirrorFx(); } /** * 按合成键剥离场上残留鬼手(Presenter 跟丢时仍能清)。 * @param key 合成体键 */ private _stripComposeVisuals(key: string): void { const bagView = this._bagView; if (!bagView || !key) return; bagView.resolveConfuseHeroVisualSkin({ composeKey: key, entityId: 0, gridIndices: [], anchorGridIndex: -1, })?.detachConfuseMirrorFx(); const battle = this._battle; if (!battle) return; for (let playerId = 0; playerId <= 1; playerId++) { const comps = battle.getPlayer(playerId)?.compositions; if (!comps) continue; for (let i = 0; i < comps.length; i++) { const comp = comps[i]; if (!comp || composeKey(comp) !== key) continue; for (let g = 0; g < comp.gridIndices.length; g++) { bagView.getTextGridForConfuseHeroTarget(comp.gridIndices[g]) ?.releaseAttachedBattleBuffFxByName(CONFUSE_HERO_CAST_PREFAB_NAME); } } } } /** 重置单次施法占用态 */ private _resetPlayState(): void { this._playing = false; this._castFxNode = null; this._playTargetKey = null; this._playGrid = null; this._playSkin = null; } /** 中断并清理全部表现 */ private _abortAllFx(): void { this._recycleCastFx(); const pending = this._activeFxNodes.slice(); this._activeFxNodes.length = 0; for (let i = 0; i < pending.length; i++) { this._releaseFx(pending[i]); } this._stripPlayVisuals(); this._resetPlayState(); } }