- 移除未使用的反伤技能配置 (5000) - 交换技能 6001 和 6002 的 EType 配置 (animationEnd/collision),使空挥技能在动画结束时触发,电击技能在碰撞时触发 - 将绿箭 (6006) 和红箭 (6007) 的 EType 从 animationEnd 改为 collision,使其在碰撞时触发 - 重构 SCastSystem 的目标查找逻辑,将候选目标收集与筛选分离,提高性能并修复可能的目标查找错误
212 lines
9.5 KiB
TypeScript
212 lines
9.5 KiB
TypeScript
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||
import { Vec3 } from "cc";
|
||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||
import { HeroViewComp } from "./HeroViewComp";
|
||
import { BuffsList, SkillConfig, SkillKind, SkillSet, TGroup, TType } from "../common/config/SkillSet";
|
||
import { Skill } from "../skill/Skill";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
import { GameConst } from "../common/config/GameConst";
|
||
|
||
/**
|
||
* ==================== 自动施法系统 ====================
|
||
*
|
||
* 职责:
|
||
* 1. 检测可施放的技能
|
||
* 2. 根据策略自动施法(AI)
|
||
* 3. 选择目标
|
||
* 4. 添加施法请求标记
|
||
*
|
||
* 设计理念:
|
||
* - 负责"何时施法"的决策
|
||
* - 通过添加 CSRequestComp 触发施法
|
||
* - 可被玩家输入系统或AI系统复用
|
||
* - 支持多种AI策略
|
||
*/
|
||
@ecs.register('SCastSystem')
|
||
export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||
debugMode: boolean = false; // 是否启用调试模式
|
||
private readonly emptyCastPlan = { skillId: 0, targets: [] as HeroViewComp[] };
|
||
private readonly emptyCandidates: { view: HeroViewComp; attrs: HeroAttrsComp; dis: number; lane: number; isSameFac: boolean }[] = [];
|
||
|
||
filter(): ecs.IMatcher {
|
||
return ecs.allOf(HeroAttrsComp, HeroViewComp);
|
||
}
|
||
|
||
update(e: ecs.Entity): void {
|
||
if(!smc.mission.play ) return;
|
||
if(smc.mission.pause) return
|
||
const heroAttrs = e.get(HeroAttrsComp);
|
||
const heroView = e.get(HeroViewComp);
|
||
if (!heroAttrs || !heroView || !heroView.node) return;
|
||
if (heroAttrs.is_dead || heroAttrs.is_reviving || heroAttrs.isStun() || heroAttrs.isFrost()) return;
|
||
heroAttrs.updateCD(this.dt);
|
||
if (!heroAttrs.is_atking) return;
|
||
const castPlan = this.pickCastSkill(heroAttrs, heroView);
|
||
if (castPlan.skillId === 0 || castPlan.targets.length === 0) return;
|
||
this.castSkill(e, castPlan, heroAttrs, heroView);
|
||
}
|
||
|
||
private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { skillId: number; targets: HeroViewComp[] } {
|
||
const range = heroAttrs.getCachedMaxSkillDistance() || GameConst.Battle.DEFAULT_SEARCH_RANGE;
|
||
const candidates = this.collectCandidates(heroView, heroAttrs, range);
|
||
const skillCandidates = [heroAttrs.skill_id, heroAttrs.atk_id];
|
||
for (const s_uuid of skillCandidates) {
|
||
if (!s_uuid) continue;
|
||
const config = SkillSet[s_uuid];
|
||
if (!config) continue;
|
||
const isMainSkill = s_uuid === heroAttrs.skill_id;
|
||
if (isMainSkill && !heroAttrs.can_skill) continue;
|
||
if (!isMainSkill && !heroAttrs.can_atk) continue;
|
||
const targets = this.findTargetsByCandidates(heroView, config, candidates);
|
||
if (targets.length === 0) continue;
|
||
return { skillId: s_uuid, targets };
|
||
}
|
||
return this.emptyCastPlan;
|
||
}
|
||
|
||
private castSkill(entity: ecs.Entity, castPlan: { skillId: number; targets: HeroViewComp[] }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) {
|
||
const s_uuid = castPlan.skillId;
|
||
const config = SkillSet[s_uuid];
|
||
if (!config) return;
|
||
heroView.playSkillEffect(s_uuid);
|
||
const isMainSkill = s_uuid === heroAttrs.skill_id;
|
||
|
||
// 优先使用技能配置的前摇时间,否则使用全局默认值
|
||
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。
|
||
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
|
||
const delay = config.ready > 0 ? config.ready : GameConst.Battle.SKILL_CAST_DELAY;
|
||
|
||
heroView.scheduleOnce(() => {
|
||
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
|
||
const validTargets = this.filterValidTargets(castPlan.targets);
|
||
if (validTargets.length === 0) return;
|
||
this.applyPrimaryEffect(entity, s_uuid, config, heroView, validTargets);
|
||
this.applyExtraEffects(config, validTargets);
|
||
}, delay);
|
||
if (isMainSkill) {
|
||
heroAttrs.triggerSkillCD();
|
||
} else {
|
||
heroAttrs.triggerAtkCD();
|
||
}
|
||
}
|
||
|
||
private createSkillEntity(s_uuid: number, caster: HeroViewComp, targetPos: Vec3) {
|
||
if (!caster.node || !caster.node.isValid) return;
|
||
const parent = caster.node.parent;
|
||
if (!parent) return;
|
||
const skill = ecs.getEntity<Skill>(Skill);
|
||
skill.load(caster.node.position.clone(), parent, s_uuid, targetPos.clone(), caster, 0);
|
||
}
|
||
|
||
private applyPrimaryEffect(casterEntity: ecs.Entity, s_uuid: number, config: SkillConfig, heroView: HeroViewComp, targets: HeroViewComp[]) {
|
||
const kind = config.kind ?? SkillKind.Damage;
|
||
if (kind === SkillKind.Damage) {
|
||
if (config.ap > 0) {
|
||
this.createSkillEntity(s_uuid, heroView, targets[0].node.position);
|
||
}
|
||
return;
|
||
}
|
||
for (const target of targets) {
|
||
if (!target.ent) continue;
|
||
const model = target.ent.get(HeroAttrsComp);
|
||
if (!model || model.is_dead) continue;
|
||
if (kind === SkillKind.Heal && config.ap !== 0) {
|
||
model.add_hp(config.ap, false);
|
||
} else if (kind === SkillKind.Shield && config.ap !== 0) {
|
||
model.add_shield(config.ap, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
private applyExtraEffects(config: SkillConfig, targets: HeroViewComp[]) {
|
||
for (const target of targets) {
|
||
if (!target.ent) continue;
|
||
const model = target.ent.get(HeroAttrsComp);
|
||
if (!model || model.is_dead) continue;
|
||
if (config.buffs) {
|
||
for (const buffId of config.buffs) {
|
||
const buffConf = BuffsList[buffId];
|
||
if (buffConf) {
|
||
model.addBuff(buffConf);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (config.debuffs) {
|
||
for (const buffId of config.debuffs) {
|
||
const buffConf = BuffsList[buffId];
|
||
if (buffConf) {
|
||
model.addBuff(buffConf);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private filterValidTargets(targets: HeroViewComp[]): HeroViewComp[] {
|
||
return targets.filter(target => {
|
||
if (!target || !target.node || !target.node.isValid) return false;
|
||
if (!target.ent) return false;
|
||
const model = target.ent.get(HeroAttrsComp);
|
||
if (!model || model.is_dead || model.is_reviving) return false;
|
||
return true;
|
||
});
|
||
}
|
||
|
||
private collectCandidates(caster: HeroViewComp, casterAttrs: HeroAttrsComp, range: number): { view: HeroViewComp; attrs: HeroAttrsComp; dis: number; lane: number; isSameFac: boolean }[] {
|
||
if (!caster || !caster.node || !caster.node.isValid) return this.emptyCandidates;
|
||
const currentPos = caster.node.position;
|
||
const list: { view: HeroViewComp; attrs: HeroAttrsComp; dis: number; lane: number; isSameFac: boolean }[] = [];
|
||
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).forEach(ent => {
|
||
const targetAttrs = ent.get(HeroAttrsComp);
|
||
const targetView = ent.get(HeroViewComp);
|
||
if (!targetAttrs || !targetView || !targetView.node || targetAttrs.is_dead) return;
|
||
if (targetView === caster) return;
|
||
const dis = Math.abs(currentPos.x - targetView.node.position.x);
|
||
if (dis > range) return;
|
||
const lane = Math.abs(currentPos.y - targetView.node.position.y);
|
||
const isSameFac = targetAttrs.fac === casterAttrs.fac;
|
||
list.push({ view: targetView, attrs: targetAttrs, dis, lane, isSameFac });
|
||
});
|
||
return list;
|
||
}
|
||
|
||
private findTargetsByCandidates(caster: HeroViewComp, config: SkillConfig, candidates: { view: HeroViewComp; attrs: HeroAttrsComp; dis: number; lane: number; isSameFac: boolean }[]): HeroViewComp[] {
|
||
const isEnemy = config.TGroup === TGroup.Enemy;
|
||
const isSelf = config.TGroup === TGroup.Self;
|
||
const isTeam = config.TGroup === TGroup.Team || config.TGroup === TGroup.Ally;
|
||
const isAll = config.TGroup === TGroup.All;
|
||
if (isSelf) return [caster];
|
||
if (!isEnemy && !isTeam && !isAll) return this.emptyCastPlan.targets;
|
||
const list = candidates.filter(item => {
|
||
if (isEnemy && item.isSameFac) return false;
|
||
if (isTeam && !item.isSameFac) return false;
|
||
return true;
|
||
});
|
||
list.sort((a, b) => {
|
||
const type = config.TType ?? TType.Frontline;
|
||
switch (type) {
|
||
case TType.Backline:
|
||
if (a.lane !== b.lane) return a.lane - b.lane;
|
||
return b.dis - a.dis;
|
||
case TType.LowestHP:
|
||
if (a.attrs.hp !== b.attrs.hp) return a.attrs.hp - b.attrs.hp;
|
||
return a.dis - b.dis;
|
||
case TType.HighestHP:
|
||
if (a.attrs.hp !== b.attrs.hp) return b.attrs.hp - a.attrs.hp;
|
||
return a.dis - b.dis;
|
||
case TType.HighestAP:
|
||
if (a.attrs.ap !== b.attrs.ap) return b.attrs.ap - a.attrs.ap;
|
||
return a.dis - b.dis;
|
||
case TType.Frontline:
|
||
default:
|
||
if (a.lane !== b.lane) return a.lane - b.lane;
|
||
return a.dis - b.dis;
|
||
}
|
||
});
|
||
|
||
const maxTargets = Math.max(GameConst.Skill.MIN_TARGET_COUNT, 1);
|
||
return list.slice(0, maxTargets).map(item => item.view);
|
||
}
|
||
}
|