feat(战斗): 优化自动施法目标选择逻辑

- 在 HeroAttrsComp 中新增 combat_target_eid 和 enemy_in_cast_range 字段,用于跟踪当前战斗目标
- 修改 MoveSystem 在移动时同步更新战斗目标状态,并清理无效目标
- 重构 SCastSystem 的自动施法逻辑,优先使用已锁定的战斗目标而非重新搜索
- 调整技能 6005 和 6006 的 hit_count 参数,分别改为 2 和 3 次打击
- 为友方技能施法添加事件派发机制,通知其他系统技能释放
This commit is contained in:
walkpan
2026-03-16 20:30:28 +08:00
parent ae3231156d
commit f63f5c6656
4 changed files with 75 additions and 64 deletions

View File

@@ -211,13 +211,13 @@ export const SkillSet: Record<number, SkillConfig> = {
},
6005: {
uuid:6005,name:"蓝箭",sp_name:"arrow_blue",icon:"1135",TGroup:TGroup.Enemy,TType:TType.Frontline,readyAnm:"",endAnm:"",act:"atk",DTType:DTType.single,
ap:100,hit_count:1,hitcd:0.2,speed:720,with:0,
ap:100,hit_count:2,hitcd:0.2,speed:720,with:0,
ready:0,EAnm:0,DAnm:9001,RType:RType.linear,EType:EType.collision,
buffs:[],debuffs:[],info:"对前方单个目标造成100%攻击的伤害",
},
6006: {
uuid:6006,name:"绿箭",sp_name:"arrow_green",icon:"1135",TGroup:TGroup.Enemy,TType:TType.Frontline,readyAnm:"",endAnm:"",act:"atk",DTType:DTType.single,
ap:100,hit_count:1,hitcd:0.2,speed:720,with:0,
ap:100,hit_count:3,hitcd:0.2,speed:720,with:0,
ready:0,EAnm:0,DAnm:9001,RType:RType.linear,EType:EType.collision,
buffs:[],debuffs:[],info:"对前方单个目标造成100%攻击的伤害",
},

View File

@@ -122,6 +122,8 @@ export class HeroAttrsComp extends ecs.Comp {
skill_id:number=0; //技能攻击技能id
can_atk=false
can_skill=false
combat_target_eid: number = -1;
enemy_in_cast_range: boolean = false;
start(){
}
// ==================== BUFF 系统初始化 ====================
@@ -548,6 +550,8 @@ export class HeroAttrsComp extends ecs.Comp {
this.skill_id = 0;
this.can_atk=false
this.can_skill=false
this.combat_target_eid = -1;
this.enemy_in_cast_range = false;
// 重置脏标签
this.dirty_hp = false;
this.dirty_shield = false;

View File

@@ -88,10 +88,12 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if (model.fac !== FacSet.HERO && model.fac !== FacSet.MON) return;
if (!move.moving) return;
if (model.fac === FacSet.MON && smc.mission.stop_mon_action) {
this.clearCombatTarget(model);
view.status_change("idle");
return;
}
if (model.is_stop || model.is_dead || model.is_reviving || model.in_stun || model.in_frost) {
this.clearCombatTarget(model);
if (!model.is_reviving) view.status_change("idle");
return;
}
@@ -103,13 +105,46 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const nearestEnemy = this.findNearestEnemy(e);
if (nearestEnemy) {
this.processCombatLogic(e, move, view, model, nearestEnemy);
this.syncCombatTarget(model, view, nearestEnemy);
} else {
this.clearCombatTarget(model);
move.targetY = 0;
this.processReturnFormation(e, move, view, model);
model.is_atking = false;
}
}
private clearCombatTarget(model: HeroAttrsComp): void {
model.combat_target_eid = -1;
model.enemy_in_cast_range = false;
}
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
if (!enemyView || !enemyView.node || !enemyView.ent) {
this.clearCombatTarget(model);
return;
}
const enemyAttrs = enemyView.ent.get(HeroAttrsComp);
if (!enemyAttrs || enemyAttrs.is_dead || enemyAttrs.is_reviving || enemyAttrs.fac === model.fac) {
this.clearCombatTarget(model);
return;
}
model.combat_target_eid = enemyView.ent.eid;
model.enemy_in_cast_range = this.isEnemyInAttackRange(model, selfView.node.position.x, enemyView.node.position.x);
}
private isEnemyInAttackRange(model: HeroAttrsComp, selfX: number, enemyX: number): boolean {
const dist = Math.abs(selfX - enemyX);
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
if (rangeType === HType.Long) {
const [, maxRange] = this.resolveCombatRange(model, 360, 720);
return dist <= maxRange;
}
const [, maxRange] = this.resolveCombatRange(model, 120, 360);
return dist <= maxRange;
}
private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
switch (rangeType) {

View File

@@ -2,10 +2,12 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { Vec3 } from "cc";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { BuffsList, SkillConfig, SkillKind, SkillSet, TGroup, TType } from "../common/config/SkillSet";
import { BuffsList, SkillConfig, SkillKind, SkillSet, TGroup } from "../common/config/SkillSet";
import { Skill } from "../skill/Skill";
import { smc } from "../common/SingletonModuleComp";
import { GameConst } from "../common/config/GameConst";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
/**
* ==================== 自动施法系统 ====================
@@ -25,8 +27,7 @@ import { GameConst } from "../common/config/GameConst";
@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 }[] = [];
private readonly emptyCastPlan = { skillId: 0, targets: [] as HeroViewComp[], isFriendly: false };
filter(): ecs.IMatcher {
return ecs.allOf(HeroAttrsComp, HeroViewComp);
@@ -46,9 +47,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
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);
private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { skillId: number; targets: HeroViewComp[]; isFriendly: boolean } {
const skillCandidates = [heroAttrs.skill_id, heroAttrs.atk_id];
for (const s_uuid of skillCandidates) {
if (!s_uuid) continue;
@@ -57,14 +56,18 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
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 };
if (this.isFriendlySkill(config.TGroup)) {
return { skillId: s_uuid, targets: [heroView], isFriendly: true };
}
if (!heroAttrs.enemy_in_cast_range) continue;
const target = this.resolveCombatTarget(heroAttrs);
if (!target) continue;
return { skillId: s_uuid, targets: [target], isFriendly: false };
}
return this.emptyCastPlan;
}
private castSkill(entity: ecs.Entity, castPlan: { skillId: number; targets: HeroViewComp[] }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) {
private castSkill(entity: ecs.Entity, castPlan: { skillId: number; targets: HeroViewComp[]; isFriendly: boolean }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) {
const s_uuid = castPlan.skillId;
const config = SkillSet[s_uuid];
if (!config) return;
@@ -78,6 +81,15 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
heroView.scheduleOnce(() => {
if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return;
if (castPlan.isFriendly) {
oops.message.dispatchEvent(GameEvent.CastHeroSkill, {
casterEid: entity.eid,
s_uuid,
fac: heroAttrs.fac,
targetEids: castPlan.targets.map(target => target.ent?.eid).filter((eid): eid is number => typeof eid === "number")
});
return;
}
const validTargets = this.filterValidTargets(castPlan.targets);
if (validTargets.length === 0) return;
this.applyPrimaryEffect(entity, s_uuid, config, heroView, validTargets);
@@ -153,59 +165,19 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
});
}
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 isFriendlySkill(group: TGroup): boolean {
return group === TGroup.Self || group === TGroup.Team || group === TGroup.Ally;
}
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);
private resolveCombatTarget(heroAttrs: HeroAttrsComp): HeroViewComp | null {
if (heroAttrs.combat_target_eid <= 0) return null;
const targetEntity = ecs.getEntityByEid(heroAttrs.combat_target_eid);
if (!targetEntity) return null;
const targetAttrs = targetEntity.get(HeroAttrsComp);
const targetView = targetEntity.get(HeroViewComp);
if (!targetAttrs || !targetView || !targetView.node || !targetView.node.isValid) return null;
if (targetAttrs.is_dead || targetAttrs.is_reviving) return null;
if (targetAttrs.fac === heroAttrs.fac) return null;
return targetView;
}
}