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/BattleGameBaseThunderPresen...

176 lines
6.5 KiB

3 weeks ago
import { Node, sp, Tween } from 'cc';
import type { EntityId } from 'db://assets/mx/module/ecs';
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 { CocosBattleMonsterPresenter } from '../../battle/view/CocosBattleMonsterPresenter';
/** 九霄神雷预制体名 */
const BASE_THUNDER_FX_PREFAB_NAME = '九霄神雷';
/** 九霄神雷 Spine 动画名 */
const BASE_THUNDER_FX_ANIM_NAME = '攻击';
/** 挂载特效后至造成伤害的延迟(逻辑秒) */
const BASE_THUNDER_DAMAGE_DELAY_SEC = 0.2;
/** 相邻目标特效起播间隔(逻辑秒) */
const BASE_THUNDER_STAGGER_SEC = 0.2;
/** 落雷相对怪物世界坐标 Y 偏移(像素) */
const BASE_THUNDER_WORLD_OFFSET_Y = 50;
/** 单目标落雷表现条目 */
type ThunderFxEntry = {
/** 特效节点 */
node: Node;
/** Spine 组件 */
skel: sp.Skeleton;
};
/**
* 九霄神雷被动表现:在目标怪物上挂特效播「攻击」,挂载后固定延迟结算伤害。
*/
export class BattleGameBaseThunderPresenter {
private _monsterPresenter: CocosBattleMonsterPresenter | null = null;
/** 落雷特效父节点(地图层;与怪物节点解耦,避免击杀回收打断动画) */
private _fxLayer: Node | null = null;
/** 并行调度世代(unbind 时递增以作废待触发回调) */
private _scheduleGen = 0;
/** 本轮进行中的特效节点 */
private readonly _activeFx: ThunderFxEntry[] = [];
/** 绑定怪物表现层与地图特效层 */
bind(monsterPresenter: CocosBattleMonsterPresenter | null, fxLayer: Node | null | undefined): void {
this.unbind();
this._monsterPresenter = monsterPresenter;
this._fxLayer = fxLayer?.isValid ? fxLayer : null;
}
/** 注销并中断全部进行中的表现 */
unbind(): void {
this._scheduleGen++;
this._abortAllFx();
this._monsterPresenter = null;
this._fxLayer = null;
}
/**
* 逐目标播放落雷:各目标 Spine 错开 0.2s 起播,挂载后 0.2s 结算伤害(不等动画播完)。
* @param targets 按进路顺序排列的受击实体
* @param onDamage 单目标伤害回调
* @param onComplete 全部目标伤害已结算完成
*/
playStrike(
targets: readonly EntityId[],
onDamage: (entityId: EntityId) => void,
onComplete: () => void,
): void {
if (targets.length <= 0) {
onComplete();
return;
}
const scheduleGen = this._scheduleGen;
let pending = targets.length;
const finishOne = (): void => {
pending--;
if (pending <= 0) onComplete();
};
for (let i = 0; i < targets.length; i++) {
const entityId = targets[i];
const fxDelay = i * BASE_THUNDER_STAGGER_SEC;
schedulePresentation(fxDelay, () => {
if (scheduleGen !== this._scheduleGen) {
finishOne();
return;
}
this._spawnFxOnMonster(entityId);
schedulePresentation(BASE_THUNDER_DAMAGE_DELAY_SEC, () => {
if (scheduleGen !== this._scheduleGen) {
finishOne();
return;
}
onDamage(entityId);
finishOne();
});
});
}
}
/** 在怪物世界坐标挂九霄神雷并播攻击动画(挂地图层,击杀不带走特效) */
private _spawnFxOnMonster(entityId: EntityId): void {
const presenter = this._monsterPresenter;
const fxLayer = this._fxLayer;
if (!presenter || !fxLayer?.isValid) {
console.log('[BaseThunder] 落雷特效层未绑定');
return;
}
const hostNode = presenter.getEntityNodeMap().get(entityId);
if (!hostNode?.isValid) {
console.log('[BaseThunder] 怪物节点缺失,跳过特效', entityId);
return;
}
let fxNode: Node | null = null;
try {
fxNode = gg.res.getNode(BATTLE_HERO_BUFF_PATH, BASE_THUNDER_FX_PREFAB_NAME, GBundle.BattleHero);
} catch (e) {
console.log('[BaseThunder] 九霄神雷预制体加载异常', String(e));
return;
}
if (!fxNode?.isValid) {
console.log('[BaseThunder] 九霄神雷预制体缺失', BASE_THUNDER_FX_PREFAB_NAME);
return;
}
const worldPos = hostNode.worldPosition.clone();
worldPos.y += BASE_THUNDER_WORLD_OFFSET_Y;
fxNode.parent = fxLayer;
fxNode.setWorldPosition(worldPos);
fxNode.setSiblingIndex(fxLayer.children.length - 1);
fxNode.active = true;
const skel = fxNode.getComponent(sp.Skeleton) ?? fxNode.getComponentInChildren(sp.Skeleton);
if (!skel?.isValid) {
console.log('[BaseThunder] 九霄神雷缺少 sp.Skeleton');
this._releaseFx(fxNode, null);
return;
}
this._activeFx.push({ node: fxNode, skel });
BattleSpinePlayback.play(skel, BASE_THUNDER_FX_ANIM_NAME, {
loop: false,
onComplete: this._onAnimComplete.bind(this, fxNode, skel),
});
}
/** 动画播完回收特效节点 */
private _onAnimComplete(fxNode: Node, skel: sp.Skeleton): void {
if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel);
this._releaseFx(fxNode, skel);
}
/** 回收单个特效节点 */
private _releaseFx(node: Node, skel: sp.Skeleton | null): void {
if (!node?.isValid) return;
for (let i = 0; i < this._activeFx.length; i++) {
const entry = this._activeFx[i];
if (entry.node === node) {
this._activeFx.splice(i, 1);
break;
}
}
if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel);
Tween.stopAllByTarget(node);
if (node['resKey']) gg.res.putNode(node);
else node.destroy();
}
/** 中断并清理全部表现 */
private _abortAllFx(): void {
const pending = this._activeFx.slice();
this._activeFx.length = 0;
for (let i = 0; i < pending.length; i++) {
this._releaseFx(pending[i].node, pending[i].skel);
}
}
}