import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { Vec3 } from "cc"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { DTType, SkillConfig, SkillKind, SkillSet, SkillUpList, 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, skillLv: 1, isFriendly: false, targetPos: null as Vec3 | null, targetEids: [] as number[] }; /** 近战英雄默认施法射程 */ private readonly meleeCastRange = 64; /** 查询缓存:避免每帧重复创建 matcher */ 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(); } /** * 每帧更新: * 1. 战斗状态校验 * 2. 英雄施法CD更新与显示 * 3. 选取本帧可施放技能并执行施法 */ 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); } /** * 选择当前应释放的技能。 * 选择顺序:技能候选列表顺序 + 条件过滤(CD、目标可达、目标类型匹配)。 * 返回内容同时包含“对敌施法”和“友方施法”两种执行所需数据。 */ private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { skillId: number; skillLv: 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 = this.buildSkillCandidates(heroAttrs.getSkillIds()); const selfEid = heroView.ent?.eid; for (const s_uuid of skillCandidates) { if (!s_uuid) continue; const config = SkillSet[s_uuid]; if (!config) continue; if (!heroAttrs.isSkillReady(s_uuid)) continue; const skillLv = heroAttrs.getSkillLevel(s_uuid); if (this.isSelfSkill(config.TGroup)) { if (typeof selfEid !== "number") continue; return { skillId: s_uuid, skillLv, 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, skillLv, isFriendly: true, targetPos: null, targetEids: friendlyEids }; } if (!target || !heroView.node || !target.node) continue; const targetPos = this.resolveEnemyCastTargetPos(config, heroAttrs, heroView, target, maxRange); if (!targetPos) continue; return { skillId: s_uuid, skillLv, isFriendly: false, targetPos, targetEids: [] }; } return this.emptyCastPlan; } /** * 执行施法: * - 播放前摇与技能动作 * - 延迟到出手时机后,按技能目标类型分发到对敌/友方效果处理 * - 触发技能CD */ private castSkill(castPlan: { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[] }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) { const s_uuid = castPlan.skillId; const skillLv = castPlan.skillLv; const config = SkillSet[s_uuid]; if (!config) return; //播放前摇技能动画 heroView.playReady(config.readyAnm); //播放角色攻击动画 heroView.playSkillAnm(config.act); // 优先使用技能配置的前摇时间,否则使用全局默认值 // 注意:这里仍然是基于时间的延迟,受帧率波动影响。 // 若需精确同步,建议在动画中添加帧事件并在 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.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null); return; } this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, castPlan.targetPos); }, delay); heroAttrs.triggerSkillCD(s_uuid); } /** 构建技能尝试顺序:优先主动技能,其次普攻 */ private buildSkillCandidates(skillIds: number[]): number[] { if (!skillIds || skillIds.length === 0) return []; if (skillIds.length === 1) return [skillIds[0]]; return [...skillIds.slice(1), skillIds[0]]; } /** * 创建技能实体(投射物/范围体等)。 * 仅用于对敌伤害技能的实体化表现与碰撞伤害分发。 */ private createSkillEntity(s_uuid: number, skillLv: number, caster: HeroViewComp,cAttrsComp: HeroAttrsComp, targetPos: Vec3) { if (!caster.node || !caster.node.isValid) return; const parent = caster.node.parent; if (!parent) return; const skill = ecs.getEntity(Skill); const sNum= Math.floor(SkillUpList[s_uuid].num*skillLv) skill.load(caster.node.position.clone(), parent, s_uuid, targetPos.clone(), caster, cAttrsComp, skillLv, 0); } /** * 对敌技能效果处理。 * 当前职责:仅处理伤害技能并创建技能实体。 */ private applyEnemySkillEffects(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3 | null) { const kind = config.kind ?? SkillKind.Damage; if (kind !== SkillKind.Damage) return; if (config.ap <= 0 || !targetPos) return; this.createSkillEntity(s_uuid, skillLv, heroView, cAttrsComp, targetPos); } /** * 友方技能效果处理。 * 当前职责: * 1. 处理治疗与护盾 * 2. 处理 buffs 配置追加 * 3. 保留完整施法信息参数,便于后续扩展更多友方效果 */ private applyFriendlySkillEffects(_s_uuid: number, _skillLv: number, config: SkillConfig, _heroView: HeroViewComp, _cAttrsComp: HeroAttrsComp, targets: HeroViewComp[], _targetPos: Vec3 | null) { const kind = config.kind ?? SkillKind.Support; 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) { const addHp = model.add_hp(config.ap, false); target.health(addHp); } else if (kind === SkillKind.Shield && config.ap !== 0) { model.add_shield(config.ap, false); } if (!config.buffs || config.buffs.length === 0) continue; for (const buffConf of config.buffs) { if (!buffConf) continue; model.addBuff(buffConf); } } } /** 根据目标 eid 列表解析出有效友方视图目标 */ 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; } /** * 收集可作为友方技能目标的实体 eid。 * includeSelf 控制是否包含施法者自身。 */ 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; skillLv: 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; } /** * 在施法距离内查找“最前排”敌人。 * 依据施法者面向方向选择 x 轴上更前的目标。 */ private findFrontEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number, nearestEnemy: HeroViewComp): HeroViewComp | null { if (!heroView.node || !nearestEnemy.node) return null; const currentX = heroView.node.position.x; const direction = nearestEnemy.node.position.x >= currentX ? 1 : -1; let frontEnemy: HeroViewComp | null = null; let edgeX = direction > 0 ? Infinity : -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 enemyX = view.node.position.x; const dist = Math.abs(currentX - enemyX); if (dist > maxRange) return; if (direction > 0) { if (enemyX >= edgeX) return; edgeX = enemyX; } else { if (enemyX <= edgeX) return; edgeX = enemyX; } frontEnemy = view; }); return frontEnemy; } /** * 解析对敌技能目标点: * - 非范围技能:按施法方向生成目标点 * - 范围技能:优先前排敌人位置 */ private resolveEnemyCastTargetPos(config: SkillConfig, heroAttrs: HeroAttrsComp, heroView: HeroViewComp, nearestEnemy: HeroViewComp, maxRange: number): Vec3 | null { if (config.DTType !== DTType.range) { return this.buildEnemyCastTargetPos(heroView, nearestEnemy, maxRange); } const frontEnemy = this.findFrontEnemyInRange(heroAttrs, heroView, maxRange, nearestEnemy); if (!frontEnemy?.node) return null; return frontEnemy.node.position.clone(); } /** 计算英雄最大施法距离(优先缓存,其次按职业类型) */ 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); } }