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.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); } }