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 { 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, isFriendly: false, targetPos: null as Vec3 | null, targetEids: [] as number[] }; private readonly meleeCastRange = 64; private heroMatcher: ecs.IMatcher | null = null; private getHeroMatcher(): ecs.IMatcher { if (!this.heroMatcher) { this.heroMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp); } return this.heroMatcher; } filter(): ecs.IMatcher { return this.getHeroMatcher(); } 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.isFrost()) return; heroAttrs.updateCD(this.dt); heroView.cd_show(); const castPlan = this.pickCastSkill(heroAttrs, heroView); if (!this.hasCastTarget(castPlan)) return; this.castSkill(castPlan, heroAttrs, heroView); } private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { skillId: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] } { const type = heroAttrs.type as HType; const maxRange = this.resolveMaxCastRange(heroAttrs, type); const target = this.findNearestEnemyInRange(heroAttrs, heroView, maxRange); const skillCandidates = [heroAttrs.skill_id, heroAttrs.atk_id]; const selfEid = heroView.ent?.eid; 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.isSelfSkill(config.TGroup)) { if (typeof selfEid !== "number") continue; return { skillId: s_uuid, isFriendly: true, targetPos: null, targetEids: [selfEid] }; } if (this.isFriendlySkill(config.TGroup)) { const includeSelf = config.TGroup === TGroup.Ally; const friendlyEids = this.collectFriendlyTargetEids(heroAttrs.fac, selfEid, includeSelf); if (friendlyEids.length === 0) continue; return { skillId: s_uuid, isFriendly: true, targetPos: null, targetEids: friendlyEids }; } if (!target || !heroView.node || !target.node) continue; const targetPos = this.buildEnemyCastTargetPos(heroView, target, maxRange); return { skillId: s_uuid, isFriendly: false, targetPos, targetEids: [] }; } return this.emptyCastPlan; } private castSkill(castPlan: { skillId: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) { const s_uuid = castPlan.skillId; const config = SkillSet[s_uuid]; if (!config) return; //播放前摇技能动画 heroView.playReady(config.readyAnm); //播放角色攻击动画 heroView.playSkillAnm(config.act); 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) { const friendlyTargets = this.resolveFriendlyTargets(castPlan.targetEids, heroAttrs.fac); if (friendlyTargets.length === 0) return; this.applyPrimaryEffect(s_uuid, config, heroView, friendlyTargets, null); this.applyExtraEffects(config, friendlyTargets); return; } this.applyPrimaryEffect(s_uuid, config, heroView, [], castPlan.targetPos); }, 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(s_uuid: number, config: SkillConfig, heroView: HeroViewComp, targets: HeroViewComp[], targetPos: Vec3 | null) { const kind = config.kind ?? SkillKind.Damage; if (kind === SkillKind.Damage) { if (config.ap <= 0 || !targetPos) return; this.createSkillEntity(s_uuid, heroView, targetPos); 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) { let addHp = model.add_hp(config.ap, false); target.health(addHp); } 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); } } } } } private resolveFriendlyTargets(targetEids: number[], fac: number): HeroViewComp[] { const targets: HeroViewComp[] = []; for (const eid of targetEids) { const entity = ecs.getEntityByEid(eid); if (!entity) continue; const model = entity.get(HeroAttrsComp); const view = entity.get(HeroViewComp); if (!model || !view?.node) continue; if (model.fac !== fac) continue; if (model.is_dead || model.is_reviving) continue; targets.push(view); } return targets; } private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] { const eids: number[] = []; ecs.query(this.getHeroMatcher()).forEach(entity => { const model = entity.get(HeroAttrsComp); const view = entity.get(HeroViewComp); if (!model || !view?.node || !view.ent) return; if (model.fac !== fac) return; if (model.is_dead || model.is_reviving) return; const eid = view.ent.eid; if (!includeSelf && typeof selfEid === "number" && eid === selfEid) return; eids.push(eid); }); return eids; } private hasCastTarget(castPlan: { skillId: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] }): boolean { if (castPlan.skillId === 0) return false; if (castPlan.isFriendly) return castPlan.targetEids.length > 0; return !!castPlan.targetPos; } private isFriendlySkill(group: TGroup): boolean { return group === TGroup.Team || group === TGroup.Ally; } private isSelfSkill(group: TGroup): boolean { return group === TGroup.Self; } private findNearestEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null { if (!heroView.node) return null; const currentX = heroView.node.position.x; let nearest: HeroViewComp | null = null; let minDist = Infinity; ecs.query(this.getHeroMatcher()).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 (dist > maxRange) return; if (dist >= minDist) return; minDist = dist; nearest = view; }); return nearest; } 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; } private buildEnemyCastTargetPos(caster: HeroViewComp, target: HeroViewComp, castRange: number): Vec3 { const casterPos = caster.node.position; const targetPos = target.node.position; const direction = targetPos.x >= casterPos.x ? 1 : -1; return new Vec3(casterPos.x + direction * castRange, casterPos.y, casterPos.z); } }