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.
386 lines
14 KiB
386 lines
14 KiB
import { Node, sp } from 'cc';
|
|
import {
|
|
AttributeComp,
|
|
HealthComp,
|
|
MovementComp,
|
|
TeamComp,
|
|
UnitMetaComp,
|
|
BattleTeamId,
|
|
type BattleSession,
|
|
} from 'db://assets/mx/module/battle';
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
import { GBundle } from '../../game/ConfigRes';
|
|
import { BATTLE_HERO_BUFF_PATH } from '../res/BattleHeroResCollector';
|
|
import { parseLegacyMonsterId, parseMonsterUnitDefId } from '../config/MonsterHpCalculator';
|
|
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
|
|
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
|
|
import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../view/IBattleMonsterView';
|
|
import { getMonsterStatusText } from '../view/MonsterStatusText';
|
|
import { BattleSpinePlayback } from './BattleSpinePlayback';
|
|
|
|
/** 加速特效预制体名 */
|
|
const FX_SPEED = '怪_加速';
|
|
/** 移速 modifier 分组 */
|
|
const SPEED_MOD = 'ally_skill_extra_speed';
|
|
/** 特效缺时长时的回收秒(已 warn) */
|
|
const FX_WAIT_FALLBACK = 2;
|
|
|
|
/** 挂在友军怪上的特效 */
|
|
interface AllyFx {
|
|
/** 目标实体 */
|
|
entity: EntityId;
|
|
/** 特效节点 */
|
|
node: Node;
|
|
/** 剩余秒 */
|
|
wait: number;
|
|
/** 循环(加速);刷新时先回收 */
|
|
loop: boolean;
|
|
/** 朝向用:上一帧怪物世界 X */
|
|
lastX?: number;
|
|
/** 朝向用:上一帧怪物世界 Y */
|
|
lastY?: number;
|
|
}
|
|
|
|
let _host: AllySkillExtraHost | null = null;
|
|
|
|
/** Skill.skill 类型 3/4:友军加速 / 加血 */
|
|
export function getAllySkillExtraHost(): AllySkillExtraHost {
|
|
if (!_host) _host = new AllySkillExtraHost();
|
|
return _host;
|
|
}
|
|
|
|
/** 主动技额外 3/4:对友军怪物加速或加血并播特效 */
|
|
export class AllySkillExtraHost {
|
|
private readonly _fx: AllyFx[] = [];
|
|
private readonly _allyIds: EntityId[] = [];
|
|
private _scanSession: BattleSession | null = null;
|
|
private _scanCaster: EntityId = 0;
|
|
|
|
/**
|
|
* 英雄/武器主动释放时应用。普攻不触发。
|
|
* @param ctx 表现上下文
|
|
*/
|
|
onSkillCast(ctx: SkillPresentationContext): void {
|
|
if (!ctx.isAlive) return;
|
|
const kind = String(ctx.params.skillKind ?? '');
|
|
if (kind !== 'hero' && kind !== 'weapon') return;
|
|
const cue = String(ctx.cueId ?? '').toLowerCase();
|
|
if (cue !== 'skill' && cue !== 'weaponskill') return;
|
|
const raw = String(ctx.params.extraSkill ?? ctx.params.skillRaw ?? '');
|
|
const parts = raw.split(',').map((s) => Number(String(s).trim()));
|
|
const head = parts[0] ?? 0;
|
|
if (head === 3) {
|
|
this._applySpeed(ctx, parts);
|
|
return;
|
|
}
|
|
if (head === 4) {
|
|
this._applyHeal(ctx, parts);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* atk_buff type3 正移速:身上循环加速特效,时长到回收。
|
|
* @param entity 目标
|
|
* @param duration 秒
|
|
*/
|
|
playSpeedFx(entity: EntityId, duration: number): void {
|
|
if (!(duration > 0)) return;
|
|
this._playFx(entity, FX_SPEED, duration, true);
|
|
}
|
|
|
|
/**
|
|
* 推进特效计时。
|
|
* @param dt 逻辑秒
|
|
*/
|
|
tick(dt: number): void {
|
|
if (!(dt > 0)) return;
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
for (let i = this._fx.length - 1; i >= 0; i--) {
|
|
const fx = this._fx[i];
|
|
fx.wait -= dt;
|
|
const dead = !fx.node.isValid || (session != null && !session.world.isAlive(fx.entity));
|
|
if (fx.wait > 0 && !dead) {
|
|
if (fx.loop) this._orientLoopFxToMove(fx);
|
|
continue;
|
|
}
|
|
this._fx.splice(i, 1);
|
|
this._recycleFx(fx.node);
|
|
}
|
|
}
|
|
|
|
/** 循环加速特效按怪物位移转向行进方向(预制体默认朝 +X) */
|
|
private _orientLoopFxToMove(fx: AllyFx): void {
|
|
if (!fx.node.isValid) return;
|
|
const monsterNode = SkillPresentationEnv.getMonsterNode(fx.entity);
|
|
if (!monsterNode?.isValid) return;
|
|
const wp = monsterNode.worldPosition;
|
|
const x = wp.x;
|
|
const y = wp.y;
|
|
if (fx.lastX == null || fx.lastY == null || !Number.isFinite(fx.lastX) || !Number.isFinite(fx.lastY)) {
|
|
fx.lastX = x;
|
|
fx.lastY = y;
|
|
return;
|
|
}
|
|
const dx = x - fx.lastX;
|
|
const dy = y - fx.lastY;
|
|
fx.lastX = x;
|
|
fx.lastY = y;
|
|
if (dx * dx + dy * dy < 1e-4) return;
|
|
fx.node.angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
}
|
|
|
|
/** 离场回收 */
|
|
abortAll(): void {
|
|
for (let i = 0; i < this._fx.length; i++) {
|
|
this._recycleFx(this._fx[i].node);
|
|
}
|
|
this._fx.length = 0;
|
|
}
|
|
|
|
/**
|
|
* 3,加速系数万分比,加速时间秒。
|
|
* @param ctx 表现上下文
|
|
* @param parts skill 分段
|
|
*/
|
|
private _applySpeed(ctx: SkillPresentationContext, parts: number[]): void {
|
|
const wan = parts[1];
|
|
const duration = parts[2];
|
|
if (!(wan > 0) || !(duration > 0)) {
|
|
console.log('[AllySkillExtra] skill 类型3参数无效', parts.join(','));
|
|
return;
|
|
}
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (!session) {
|
|
console.log('[AllySkillExtra] 无 session');
|
|
return;
|
|
}
|
|
this._fillAllies(session, ctx.caster);
|
|
const mul = 1 + wan / 10000;
|
|
for (let i = 0; i < this._allyIds.length; i++) {
|
|
this._buffSpeed(session, this._allyIds[i], mul, duration);
|
|
this._playFx(this._allyIds[i], FX_SPEED, duration, true);
|
|
getMonsterStatusText().play(this._allyIds[i], '加速');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 4,加血比例万分比。满血或未实际加血也播 `怪物加血`。
|
|
* @param ctx 表现上下文
|
|
* @param parts skill 分段
|
|
*/
|
|
private _applyHeal(ctx: SkillPresentationContext, parts: number[]): void {
|
|
const wan = parts[1];
|
|
if (!(wan > 0)) {
|
|
console.log('[AllySkillExtra] skill 类型4参数无效', parts.join(','));
|
|
return;
|
|
}
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (!session) {
|
|
console.log('[AllySkillExtra] 无 session');
|
|
return;
|
|
}
|
|
this._fillAllies(session, ctx.caster);
|
|
for (let i = 0; i < this._allyIds.length; i++) {
|
|
const id = this._allyIds[i];
|
|
this._healOne(session, id, wan);
|
|
const view = this._monsterView(id);
|
|
if (!view?.playHealEffect) {
|
|
console.log('[AllySkillExtra] 友军怪无法播加血特效', id);
|
|
continue;
|
|
}
|
|
view.playHealEffect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 收集友军怪物。
|
|
* @param session 会话
|
|
* @param caster 施法者
|
|
*/
|
|
private _fillAllies(session: BattleSession, caster: EntityId): void {
|
|
this._scanSession = session;
|
|
this._scanCaster = caster;
|
|
this._allyIds.length = 0;
|
|
session.world.forEachAlive(this._collectAlly);
|
|
this._scanSession = null;
|
|
}
|
|
|
|
/** 扫描回调:友军怪物入列 */
|
|
private readonly _collectAlly = (id: EntityId): void => {
|
|
const session = this._scanSession;
|
|
if (!session) return;
|
|
if (!this._isAllyMonster(session, this._scanCaster, id)) return;
|
|
this._allyIds.push(id);
|
|
};
|
|
|
|
/**
|
|
* 是否施法者友军怪物:己方出怪口行军怪,或友方召唤。不含攻向自己的敌方来路怪。
|
|
* @param session 会话
|
|
* @param caster 施法者
|
|
* @param id 候选
|
|
*/
|
|
private _isAllyMonster(session: BattleSession, caster: EntityId, id: EntityId): boolean {
|
|
if (id === caster) return false;
|
|
const world = session.world;
|
|
if (!world.isAlive(id)) return false;
|
|
const hp = world.getComponent(id, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) return false;
|
|
const meta = world.getComponent(id, UnitMetaComp);
|
|
if (!meta?.unitDefId) return false;
|
|
const parsed = parseMonsterUnitDefId(meta.unitDefId);
|
|
const monsterId = parsed?.monsterId ?? parseLegacyMonsterId(meta.unitDefId);
|
|
if (!(monsterId > 0)) return false;
|
|
const casterTeam = world.getComponent(caster, TeamComp);
|
|
const team = world.getComponent(id, TeamComp);
|
|
if (BattleTeamId.isFriendly(casterTeam, team)) return true;
|
|
const allyPort = (casterTeam?.ownerId ?? 0) === 0 ? 2 : 1;
|
|
return (meta.spawnPoint ?? 0) === allyPort;
|
|
}
|
|
|
|
/**
|
|
* 写入限时移速。MovementSystem 每帧按 modifier 刷新 speed。
|
|
* @param session 会话
|
|
* @param id 目标
|
|
* @param mul 倍率
|
|
* @param duration 秒
|
|
*/
|
|
private _buffSpeed(session: BattleSession, id: EntityId, mul: number, duration: number): void {
|
|
const world = session.world;
|
|
if (!world.getComponent(id, AttributeComp)) {
|
|
world.addComponent(id, new AttributeComp());
|
|
}
|
|
session.attributes.addModifier(id, {
|
|
attr: 'move_speed_mul',
|
|
op: 'Mul',
|
|
value: mul,
|
|
source: SPEED_MOD,
|
|
group: SPEED_MOD,
|
|
remain: duration,
|
|
});
|
|
const move = world.getComponent(id, MovementComp);
|
|
if (move) {
|
|
move.speed = Math.max(0, (move.baseSpeed || move.speed) * mul);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 按最大生命万分比加血。
|
|
* @param session 会话
|
|
* @param id 目标
|
|
* @param wan 万分比
|
|
*/
|
|
private _healOne(session: BattleSession, id: EntityId, wan: number): void {
|
|
const health = session.world.getComponent(id, HealthComp);
|
|
if (!health || !(health.maxHp > 0)) return;
|
|
const add = Math.max(1, Math.floor(health.maxHp * wan / 10000));
|
|
health.hp = Math.min(health.maxHp, health.hp + add);
|
|
const view = this._monsterView(id);
|
|
view?.syncMxHp?.(health.hp, health.maxHp);
|
|
}
|
|
|
|
/**
|
|
* 在怪物节点上播 buff 特效(加速等)。
|
|
* @param entity 目标
|
|
* @param prefab 预制体名
|
|
* @param duration 持续秒;≤0 时用动画时长或兜底
|
|
* @param loop 是否循环
|
|
*/
|
|
private _playFx(entity: EntityId, prefab: string, duration: number, loop: boolean): void {
|
|
const parent = this._fxParent(entity);
|
|
if (!parent?.isValid) {
|
|
console.log('[AllySkillExtra] 友军怪无节点', entity, prefab);
|
|
return;
|
|
}
|
|
if (loop) this._dropLoopOnEntity(entity);
|
|
const node = this._loadFxNode(prefab);
|
|
if (!node) return;
|
|
parent.addChild(node);
|
|
node.active = true;
|
|
node.setPosition(0, 0, 0);
|
|
const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
|
|
let wait = duration;
|
|
if (sk?.isValid) {
|
|
const typed = sk as sp.Skeleton & { defaultAnimation?: string };
|
|
const anim = String(typed.defaultAnimation || sk.animation || '').trim();
|
|
if (!anim) {
|
|
console.log('[AllySkillExtra] 特效无默认动画', prefab);
|
|
} else {
|
|
BattleSpinePlayback.play(sk, anim, { loop });
|
|
if (!loop) {
|
|
const dur = BattleSpinePlayback.getAnimDuration(sk, anim);
|
|
wait = dur != null && dur > 0 ? dur : FX_WAIT_FALLBACK;
|
|
if (dur == null || !(dur > 0)) {
|
|
console.log('[AllySkillExtra] 特效动画时长无效', prefab, anim);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.log('[AllySkillExtra] 特效缺少 Skeleton', prefab);
|
|
}
|
|
if (!(wait > 0)) wait = FX_WAIT_FALLBACK;
|
|
this._fx.push({ entity, node, wait, loop });
|
|
}
|
|
|
|
/**
|
|
* 取 buff 预制体:优先 battleHero/prefab/buff,否则 battleGameMonster/prefab。
|
|
* @param name 预制体名
|
|
*/
|
|
private _loadFxNode(name: string): Node | null {
|
|
const heroNode = gg.res.getNode(BATTLE_HERO_BUFF_PATH, name, GBundle.BattleHero);
|
|
if (heroNode?.isValid) return heroNode;
|
|
const monsterNode = gg.res.getNode('prefab/', name, GBundle.BattleGameMonster);
|
|
if (monsterNode?.isValid) {
|
|
console.log('[AllySkillExtra] battleHero/prefab/buff 无预制体,改用 battleGameMonster', name);
|
|
return monsterNode;
|
|
}
|
|
console.log('[AllySkillExtra] 特效预制体缺失', name);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 特效挂点(upEff 优先)。
|
|
* @param entity 目标
|
|
*/
|
|
private _fxParent(entity: EntityId): Node | null {
|
|
const node = SkillPresentationEnv.getMonsterNode(entity);
|
|
if (!node?.isValid) return null;
|
|
const view = this._monsterView(entity);
|
|
const up = view?.upEff;
|
|
return up?.isValid ? up : node;
|
|
}
|
|
|
|
/**
|
|
* 怪物视图。
|
|
* @param entity 目标
|
|
*/
|
|
private _monsterView(entity: EntityId): IBattleMonsterView | null {
|
|
const node = SkillPresentationEnv.getMonsterNode(entity);
|
|
if (!node?.isValid) return null;
|
|
return node.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null;
|
|
}
|
|
|
|
/**
|
|
* 去掉该实体已有循环特效(加速刷新)。
|
|
* @param entity 目标
|
|
*/
|
|
private _dropLoopOnEntity(entity: EntityId): void {
|
|
for (let i = this._fx.length - 1; i >= 0; i--) {
|
|
const fx = this._fx[i];
|
|
if (fx.entity !== entity || !fx.loop) continue;
|
|
this._fx.splice(i, 1);
|
|
this._recycleFx(fx.node);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 回池特效。
|
|
* @param node 节点
|
|
*/
|
|
private _recycleFx(node: Node): void {
|
|
if (!node.isValid) return;
|
|
const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
|
|
BattleSpinePlayback.clearPendingComplete(sk);
|
|
BattleSpinePlayback.resetTracks(sk);
|
|
gg.res.putNode(node);
|
|
}
|
|
}
|
|
|