Files
pixelheros/assets/script/game/hero/SCastSystem.ts
walkpan ae3231156d fix(技能): 修正技能目标筛选和触发类型配置
- 移除未使用的反伤技能配置 (5000)
- 交换技能 6001 和 6002 的 EType 配置 (animationEnd/collision),使空挥技能在动画结束时触发,电击技能在碰撞时触发
- 将绿箭 (6006) 和红箭 (6007) 的 EType 从 animationEnd 改为 collision,使其在碰撞时触发
- 重构 SCastSystem 的目标查找逻辑,将候选目标收集与筛选分离,提高性能并修复可能的目标查找错误
2026-03-16 19:43:11 +08:00

212 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}