Files
pixelheros/assets/script/game/hero/SCastSystem.ts
walkpan a634b33f6b refactor(技能系统): 移除未使用属性并优化目标选择逻辑
- 移除 Skill.ts 中未使用的 wfuny 属性赋值
- 将 SCastSystem 中的目标选择逻辑重构为统一方法
- 在施放技能前增加目标有效性检查,避免无效操作
- 移除 HeroAtkSystem 中未使用的导入和接口字段
- 调整 SkillSet 中技能 6008 的 ready 参数值
2026-03-16 19:33:24 +08:00

213 lines
9.4 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[] };
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 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.findTargets(heroView, heroAttrs, config);
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 findTargets(caster: HeroViewComp, casterAttrs: HeroAttrsComp, config: SkillConfig): HeroViewComp[] {
const range = casterAttrs.getCachedMaxSkillDistance() || GameConst.Battle.DEFAULT_SEARCH_RANGE;
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];
const currentPos = caster.node.position;
const list: { view: HeroViewComp; attrs: HeroAttrsComp; dis: number; lane: number }[] = [];
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 isSameFac = targetAttrs.fac === casterAttrs.fac;
if (isEnemy && isSameFac) return;
if (isTeam && !isSameFac) return;
if (!isEnemy && !isTeam && !isAll) 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);
list.push({ view: targetView, attrs: targetAttrs, dis, lane });
});
list.sort((a, b) => {
// 优先检查是否在同一行 (除了特殊目标类型)
// 如果是寻找特殊目标(如最低血量),通常忽略行优先,但在范围内全搜索
// 但如果设计要求"最近的优先",则通常还是先看行。
// 这里假设 TType 优先级高于 Lane 优先级,或者在 TType 相同情况下比较 Lane
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);
}
}