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/battle/presentation/AllySkillExtraHost.ts

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);
}
}