import { Node, Sorting2D, sp } from 'cc'; import type { BattleEvent, EntityId } from 'db://assets/mx/module/battle'; import { BATTLE_EVENT_ALL, BattleEventType } from 'db://assets/mx/module/battle'; import { GBundle } from '../../game/ConfigRes'; import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import type { CocosBattleMonsterPresenter } from '../../battle/view/CocosBattleMonsterPresenter'; import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../../battle/view/IBattleMonsterView'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; /** 碎法帝钟预制体(battleHero/prefab/buff/碎法帝钟) */ const BOSS_INTERRUPT_FX_PREFAB_NAME = '碎法帝钟'; /** 碎法帝钟 Spine 动画名 */ const BOSS_INTERRUPT_FX_ANIM_NAME = 'animation'; /** 碎法帝钟相对血条锚点 bloodPos 的 Y 偏移(本地坐标) */ const BOSS_INTERRUPT_FX_OFFSET_Y = 40; /** 碎法帝钟 Sorting2D(须高于怪物 skin) */ const BOSS_INTERRUPT_FX_SORT_LAYER = 3; const BOSS_INTERRUPT_FX_SORT_ORDER = 12; /** 单 Boss 上的碎法帝钟实例 */ type BossInterruptFxEntry = { /** 受击 Boss */ entityId: EntityId; /** 特效节点 */ node: Node; /** Spine 组件 */ skel: sp.Skeleton; }; /** * 碎法帝钟(functiontype=21):Boss 施法被打断时挂 bloodPos(血条锚点)Y+40 播 animation,播完回收;本机/AI 共用。 */ export class BattleGameBossInterruptFxPresenter { private _battle: ProjectBattleHandle | null = null; private _monsterPresenter: CocosBattleMonsterPresenter | null = null; private readonly _activeFx: BossInterruptFxEntry[] = []; private _battleListener: ((e: BattleEvent) => void) | null = null; /** 打断 pending 完成回调 */ private _fxGen = 0; /** * 绑定战斗与怪物表现层。 * @param battle 战斗句柄 * @param monsterPresenter 怪物节点映射 */ bind(battle: ProjectBattleHandle | null, monsterPresenter: CocosBattleMonsterPresenter | null): void { this.unbind(); this._battle = battle; this._monsterPresenter = monsterPresenter; if (battle) { this._battleListener = (e) => this._onBattleEvent(e); battle.on(BATTLE_EVENT_ALL, this._battleListener); } } /** 注销并中断全部碎法帝钟表现 */ unbind(): void { if (this._battle && this._battleListener) { this._battle.off(BATTLE_EVENT_ALL, this._battleListener); } this._battleListener = null; this._fxGen++; for (let i = this._activeFx.length - 1; i >= 0; i--) { this._releaseFxEntry(this._activeFx[i]); } this._activeFx.length = 0; this._battle = null; this._monsterPresenter = null; } /** * Boss 打断成功时在实体上播放碎法帝钟。 * @param entityId 敌方 Boss 实体 */ playOnBoss(entityId: EntityId): void { if (!(entityId > 0)) return; this._releaseFxForEntity(entityId); const parent = this._resolveMonsterRoot(entityId); if (!parent?.isValid) { console.log('[BossInterruptFx] Boss 挂载点缺失 entity=', entityId); return; } const fxNode = this._spawnFx(parent, entityId); if (!fxNode) return; const skel = fxNode.getComponent(sp.Skeleton) ?? fxNode.getComponentInChildren(sp.Skeleton); if (!skel?.isValid) { console.log('[BossInterruptFx] 碎法帝钟缺少 sp.Skeleton'); this._releaseFxNode(fxNode); return; } const entry: BossInterruptFxEntry = { entityId, node: fxNode, skel }; this._activeFx.push(entry); const gen = this._fxGen; const started = BattleSpinePlayback.play(skel, BOSS_INTERRUPT_FX_ANIM_NAME, { loop: false, onComplete: this._onAnimComplete.bind(this, entry, gen), }); if (!started) { console.log('[BossInterruptFx] 碎法帝钟动画缺失', BOSS_INTERRUPT_FX_ANIM_NAME); this._releaseFxEntry(entry); } } /** 动画播完回收 */ private _onAnimComplete(entry: BossInterruptFxEntry, gen: number): void { if (gen !== this._fxGen) return; if (entry.skel?.isValid) BattleSpinePlayback.clearPendingComplete(entry.skel); this._releaseFxEntry(entry); } private _onBattleEvent(event: BattleEvent): void { switch (event.type) { case BattleEventType.EntityKilled: this._releaseFxForEntity(event.victim); break; case BattleEventType.UnitDespawned: this._releaseFxForEntity(event.entity); break; default: break; } } /** Boss 根节点 */ private _resolveMonsterRoot(entityId: EntityId): Node | null { const root = this._monsterPresenter?.getEntityNodeMap().get(entityId); return root?.isValid ? root : null; } /** * 解析碎法帝钟挂点:优先 bloodPos(血条锚点)+ Y 偏移。 * @param monsterRoot Boss 表现根节点 * @param entityId 实体 id(日志) */ private _resolveFxAttach(monsterRoot: Node, entityId: EntityId): { parent: Node; localY: number } { const view = monsterRoot.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null; view?.syncBloodPosToSkin?.(); view?.ensureBloodBar?.(); if (view?.bloodPos?.isValid) { return { parent: view.bloodPos, localY: BOSS_INTERRUPT_FX_OFFSET_Y }; } console.log('[BossInterruptFx] Boss 血条锚点缺失,挂根节点', entityId); return { parent: monsterRoot, localY: BOSS_INTERRUPT_FX_OFFSET_Y }; } /** 加载并挂到 Boss 血条锚点(bloodPos + Y 偏移) */ private _spawnFx(monsterRoot: Node, entityId: EntityId): Node | null { let node: Node | null = null; try { node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, BOSS_INTERRUPT_FX_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[BossInterruptFx] 碎法帝钟预制体加载失败', String(e)); return null; } if (!node?.isValid) { console.log('[BossInterruptFx] 碎法帝钟预制体缺失', BOSS_INTERRUPT_FX_PREFAB_NAME); return null; } const attach = this._resolveFxAttach(monsterRoot, entityId); node.layer = attach.parent.layer; node.setParent(attach.parent); node.setPosition(0, attach.localY, 0); node.active = true; node.setSiblingIndex(attach.parent.children.length - 1); for (const sk of node.getComponentsInChildren(sp.Skeleton)) { if (!sk?.node?.isValid) continue; sk.node.layer = attach.parent.layer; sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME); let sorting = sk.node.getComponent(Sorting2D); if (!sorting) sorting = sk.node.addComponent(Sorting2D); sorting.sortingLayer = BOSS_INTERRUPT_FX_SORT_LAYER; sorting.sortingOrder = BOSS_INTERRUPT_FX_SORT_ORDER; } return node; } /** 回收指定 Boss 上的碎法帝钟 */ private _releaseFxForEntity(entityId: EntityId): void { for (let i = this._activeFx.length - 1; i >= 0; i--) { if (this._activeFx[i].entityId !== entityId) continue; this._releaseFxEntry(this._activeFx[i]); } } /** 从活动列表移除并回收节点 */ private _releaseFxEntry(entry: BossInterruptFxEntry): void { const idx = this._activeFx.indexOf(entry); if (idx >= 0) this._activeFx.splice(idx, 1); if (entry.skel?.isValid) BattleSpinePlayback.clearPendingComplete(entry.skel); this._releaseFxNode(entry.node); } /** 回收特效节点 */ private _releaseFxNode(node: Node | null | undefined): void { if (!node?.isValid) return; if (node['resKey']) { gg.res.putNode(node); } else { node.destroy(); } } }