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.
176 lines
6.5 KiB
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|