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"; /** * ==================== 自动施法系统 ==================== * * 职责: * 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[], isFriendly: false }; 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[]; isFriendly: boolean } { 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 (!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[]; 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.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; } }