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.
211 lines
8.1 KiB
211 lines
8.1 KiB
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();
|
|
}
|
|
}
|
|
}
|
|
|