You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/ui/BattleGame/BattleGameBossInterruptFxPr...

212 lines
8.1 KiB

3 weeks ago
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();
}
}
}