Files
pixelheros/assets/script/game/hero/SCastSystem.ts
panw 20aa067c9c fix(战斗): 修复英雄移动和施法逻辑
- 移动系统现在会在需要保持距离时也执行移动,避免过于靠近敌人
- 施法系统重构目标选择逻辑,确保在射程内寻找最近敌人
- 添加近战施法距离常量,根据英雄类型动态计算最大施法范围
- 移除不必要的攻击状态检查,优化施法条件判断
2026-03-17 17:00:01 +08:00

231 lines
10 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 } 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";
import { HType } from "../common/config/heroSet";
/**
* ==================== 自动施法系统 ====================
*
* 职责:
* 1. 检测可施放的技能
* 2. 根据策略自动施法AI
* 3. 选择目标
* 4. 添加施法请求标记
*
* 设计理念:
* - 负责"何时施法"的决策
*/
@ecs.register('SCastSystem')
export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
debugMode: boolean = false; // 是否启用调试模式
private readonly emptyCastPlan = { skillId: 0, targets: [] as HeroViewComp[], isFriendly: false };
private readonly meleeCastRange = 64;
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);
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[]; isFriendly: boolean } {
const { target, inRange } = this.resolveCastTarget(heroAttrs, heroView);
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;
if (this.isFriendlySkill(config.TGroup)) {
return { skillId: s_uuid, targets: [heroView], isFriendly: true };
}
if (!target || !inRange) continue;
return { skillId: s_uuid, targets: [target], isFriendly: false };
}
return this.emptyCastPlan;
}
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;
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;
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);
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 isFriendlySkill(group: TGroup): boolean {
return group === TGroup.Self || group === TGroup.Team || group === TGroup.Ally;
}
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;
}
private resolveCastTarget(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { target: HeroViewComp | null; inRange: boolean } {
const combatTarget = this.resolveCombatTarget(heroAttrs);
if (combatTarget && this.isEnemyInCastRange(heroAttrs, heroView, combatTarget)) {
return { target: combatTarget, inRange: true };
}
const nearestInRange = this.findNearestEnemy(heroAttrs, heroView, true);
if (nearestInRange) {
return { target: nearestInRange, inRange: true };
}
const fallback = combatTarget ?? this.findNearestEnemy(heroAttrs, heroView, false);
if (!fallback) return { target: null, inRange: false };
return { target: fallback, inRange: this.isEnemyInCastRange(heroAttrs, heroView, fallback) };
}
private findNearestEnemy(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, requireInRange: boolean): HeroViewComp | null {
if (!heroView.node) return null;
const currentX = heroView.node.position.x;
let nearest: HeroViewComp | null = null;
let minDist = Infinity;
ecs.query(this.filter()).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view?.node) return;
if (attrs.fac === heroAttrs.fac) return;
if (attrs.is_dead || attrs.is_reviving) return;
const dist = Math.abs(currentX - view.node.position.x);
if (requireInRange && !this.isEnemyInCastRange(heroAttrs, heroView, view)) return;
if (dist >= minDist) return;
minDist = dist;
nearest = view;
});
return nearest;
}
private isEnemyInCastRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, target: HeroViewComp): boolean {
if (!heroView.node || !target.node) return false;
const dist = Math.abs(heroView.node.position.x - target.node.position.x);
const type = heroAttrs.type as HType;
const maxRange = this.resolveMaxCastRange(heroAttrs, type);
return dist <= maxRange;
}
private resolveMaxCastRange(heroAttrs: HeroAttrsComp, type: HType): number {
const cached = heroAttrs.getCachedMaxSkillDistance();
if (cached > 0) return cached;
if (type === HType.Long) return 720;
if (type === HType.Mid) return 360;
return this.meleeCastRange;
}
}