import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { Vec3, Prefab, instantiate, tween, Node } from "cc"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { DTType, RType, SkillConfig, SkillKind, SkillSet, SkillUpList, TGroup, SkillOverrides, mergeSkillParams } from "../common/config/SkillSet"; import { Skill } from "../skill/Skill"; import { smc } from "../common/SingletonModuleComp"; import { HeroDisVal, HeroInfo, HType } from "../common/config/heroSet"; import { Attrs } from "../common/config/HeroAttrs"; import { BoxSet, FacSet, FightSet } from "../common/config/GameSet"; import { oops } from "db://oops-framework/core/Oops"; import { GameEvent } from "../common/config/GameEvent"; import { SkillTriggerType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; import { MissionEconomy } from "../map/MissionEconomy"; import { MissionMonCompComp } from "../map/MissionMonComp"; import { MissionHeroComp } from "../map/MissionHeroComp"; /** * ==================== 自动施法系统 ==================== * * 职责: * 1. 检测可施放的技能 * 2. 根据策略自动施法(AI) * 3. 选择目标 * 4. 添加施法请求标记 * * 设计理念: * - 负责"何时施法"的决策 */ @ecs.register('SCastSystem') export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate { static instance: SCastSystem | null = null; debugMode: boolean = false; // 是否启用调试模式 constructor() { super(); SCastSystem.instance = this; // 监听触发技能事件 oops.message.on(GameEvent.TriggerSkill, this.onTriggerSkill, this); } /** 系统被销毁或重置时,必须注销全局事件监听,避免内存泄漏与重复触发 */ onDestroy() { oops.message.off(GameEvent.TriggerSkill, this.onTriggerSkill, this); } private onTriggerSkill(event: string, args: { s_uuid: number, heroAttrs?: HeroAttrsComp, heroView?: HeroViewComp, triggerType?: string, isCardSkill?: boolean, card_lv?: number, targetPos?: Vec3, overrides?: any }) { if (!args || !args.s_uuid) return; // 卡牌技能直接触发 if (args.isCardSkill) { this.forceCastCardSkill(args.s_uuid, args.card_lv || 1, args.targetPos || new Vec3(FightSet.CSKILL_START_X, FightSet.CSKILL_START_Y, 0), args.overrides); return; } // 常规英雄技能触发 if (!args.heroAttrs || !args.heroView) return; this.forceCastTriggerSkill(args.s_uuid, args.heroAttrs, args.heroView, args.triggerType, args.overrides); } /** * 强制执行卡牌技能 * 卡牌技能没有施法者主体,直接从指定坐标释放,或者对全体/随机友方生效 */ public forceCastCardSkill(s_uuid: number, cardLv: number, spawnPos: Vec3, overrides?: SkillOverrides) { let config = SkillSet[s_uuid]; if (!config) return; config = mergeSkillParams(config, overrides); // 如果是敌方目标,没有战斗时不释放 const isEnemyTarget = !this.isSelfSkill(config.TGroup) && !this.isFriendlySkill(config.TGroup); if (isEnemyTarget && !smc.mission.in_fight) return; let isFriendly = false; let targetEids: number[] = []; if (this.isFriendlySkill(config.TGroup) || this.isSelfSkill(config.TGroup)) { isFriendly = true; targetEids = this.collectFriendlyTargetEids(FacSet.HERO, undefined, true); // 获取所有英雄阵营的目标 } const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001]; const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0))); const castTimes = 1 + cNum; // 构造一个模拟的 HeroAttrsComp 用于数值计算,只包含基础卡牌伤害计算所需的属性 const mockAttrs = new HeroAttrsComp(); // 动态计算卡牌的虚拟攻击力: // 1. 根据卡牌等级给予基础成长(同英雄升级公式,基准设为 100) let baseAp = 100 * Math.pow(FightSet.MERGE_NEED, cardLv - 1); let highestAp = baseAp; // 2. 获取场上最高攻击力的英雄,保证后期奶量/增益绝对够用 for (const eid of smc.mission.heroGrid) { if (eid >= 0) { const entity = ecs.getEntityByEid(eid); if (entity) { const attr = entity.get(HeroAttrsComp); if (attr && !attr.is_dead && attr.ap > highestAp) { highestAp = attr.ap; } } } } mockAttrs.ap = highestAp; mockAttrs.critical = 0; mockAttrs.freeze_chance = 0; mockAttrs.stun_chance = 0; mockAttrs.puncture_chance = 0; mockAttrs.fac = FacSet.HERO; mockAttrs.type = HType.Long; // 假定为远程,拥有较长索敌范围 mockAttrs.dis = 2000; // 给予全屏以上的索敌范围 let targetPos: Vec3 | null = null; if (!isFriendly) { // 伪造一个 view 供找敌逻辑使用,位置为 spawnPos const mockView = { node: { position: spawnPos } } as any; // 获取全屏索敌范围 const maxRange = this.resolveMaxCastRange(mockAttrs, mockAttrs.type as HType); const target = this.findNearestEnemyInRange(mockAttrs, mockView, maxRange); if (target && target.node) { targetPos = this.resolveEnemyCastTargetPos(config, mockAttrs, mockView, target, maxRange); } // 如果全屏都没找到敌人,直接放弃释放伤害技能 if (!targetPos) { console.log("[SCastSystem] forceCastCardSkill: no enemy found for skill", s_uuid); return; } } console.log("[SCastSystem] forceCastCardSkill: casting skill", s_uuid, "castTimes", castTimes, "targetPos", targetPos); for (let i = 0; i < castTimes; i++) { if (isFriendly) { const friendlyTargets = this.resolveFriendlyTargets(targetEids, FacSet.HERO); if (friendlyTargets.length === 0) continue; this.applyFriendlySkillEffects(s_uuid, cardLv, config, null as any, mockAttrs, friendlyTargets, spawnPos); } else { const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i); this.createSkillEntityForCard(s_uuid, cardLv, mockAttrs, spawnPos, enemyTargetPos, i, overrides); } } } /** 专用于卡牌施放的技能实体生成 */ private createSkillEntityForCard(s_uuid: number, skillLv: number, mockAttrs: HeroAttrsComp, startPos: Vec3, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides) { const scene = smc.map.MapView.scene; const parent = scene.entityLayer?.node?.getChildByName("SKILL"); if (!parent || !targetPos) { console.log("[SCastSystem] createSkillEntityForCard failed: parent or targetPos missing", !!parent, !!targetPos); return; } const skill = ecs.getEntity(Skill); const actualStartPos = this.resolveRepeatCastStartPos(startPos, castIndex); // 伪造一个简单的 heroView 供 Skill 初始化使用,只包含方向信息 const mockView = { node: { scale: new Vec3(1, 1, 1), position: actualStartPos }, ent: { eid: -1 }, box_group: BoxSet.HERO } as any; skill.load(actualStartPos, parent, s_uuid, targetPos.clone(), mockView, mockAttrs, skillLv, 0, overrides); console.log("[SCastSystem] createSkillEntityForCard success for skill", s_uuid); } /** 空施法计划:用于“当前无可施法技能”时的统一返回 */ private readonly emptyCastPlan = { skillId: 0, skillLv: 1, isFriendly: false, targetPos: null as Vec3 | null, targetEids: [] as number[], overrides: undefined as SkillOverrides | undefined }; /** 查询缓存:避免每帧重复创建 matcher */ private heroMatcher: ecs.IMatcher | null = null; /** 获取英雄查询条件(包含属性与视图组件) */ private getHeroMatcher(): ecs.IMatcher { if (!this.heroMatcher) { this.heroMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp); } return this.heroMatcher; } private isOutOfBattleBounds(x: number): boolean { return x < BoxSet.LETF_END || x > BoxSet.RIGHT_END; } /** 系统过滤器:仅处理英雄实体 */ 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 if(!smc.mission.in_fight) return const heroAttrs = e.get(HeroAttrsComp); const heroView = e.get(HeroViewComp); if (!heroAttrs || !heroView || !heroView.node) return; if (this.isOutOfBattleBounds(heroView.node.position.x)) 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、状态、动画前摇,直接生效 */ public forceCastTriggerSkill(s_uuid: number, heroAttrs: HeroAttrsComp, heroView: HeroViewComp, triggerType?: string, overrides?: SkillOverrides) { // 播放相应的触发动画 if (triggerType === 'call') { heroView.playReady("yellow"); } else if (triggerType === 'dead') { heroView.playOther("dead"); }else{ heroView.playOther('yellow') } // 播放特殊技能触发音效 oops.audio.playEffect("music/flash"); // 如果是敌方攻击技能,必须在战斗中才能释放;友方增益/护盾则允许在非战斗中释放 let config = SkillSet[s_uuid]; if (!config) return; config = mergeSkillParams(config, overrides); const isEnemyTarget = !this.isSelfSkill(config.TGroup) && !this.isFriendlySkill(config.TGroup); if (isEnemyTarget && !smc.mission.in_fight) return; const skillLv = heroAttrs.getSkillLevel(s_uuid) || 1; let isFriendly = false; let targetPos: Vec3 | null = null; let targetEids: number[] = []; const selfEid = heroView.ent?.eid; const type = heroAttrs.type as HType; const maxRange = this.resolveMaxCastRange(heroAttrs, type); if (this.isSelfSkill(config.TGroup)) { isFriendly = true; if (typeof selfEid === "number") targetEids = [selfEid]; } else if (this.isFriendlySkill(config.TGroup)) { isFriendly = true; const includeSelf = config.TGroup === TGroup.Ally; targetEids = this.collectFriendlyTargetEids(heroAttrs.fac, selfEid, includeSelf); } else { const target = this.findNearestEnemyInRange(heroAttrs, heroView, maxRange); if (target && target.node) { targetPos = this.resolveEnemyCastTargetPos(config, heroAttrs, heroView, target, maxRange); } } const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid] : SkillUpList[1001]; const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0))); const castTimes = 1 + cNum; let val="" if(castTimes >1){ val = "*"+castTimes.toString } heroView.skill_name(val,s_uuid) for (let i = 0; i < castTimes; i++) { if (!heroView.node || !heroView.node.isValid) return; if (isFriendly) { const friendlyTargets = this.resolveFriendlyTargets(targetEids, heroAttrs.fac); if (friendlyTargets.length === 0) continue; this.applyFriendlySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, friendlyTargets, null); } else { const enemyTargetPos = this.resolveRepeatCastTargetPos(targetPos, i); this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides); } } } /** * 选择当前应释放的技能。 * 选择顺序:技能候选列表顺序 + 条件过滤(CD、目标可达、目标类型匹配)。 * 返回内容同时包含“对敌施法”和“友方施法”两种执行所需数据。 */ private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[]; overrides?: SkillOverrides } { 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; let config = SkillSet[s_uuid]; if (!config) continue; if (!heroAttrs.isSkillReady(s_uuid)) continue; const skillLv = heroAttrs.getSkillLevel(s_uuid); const overrides = heroAttrs.skills[s_uuid]?.overrides; config = mergeSkillParams(config, overrides); if (this.isSelfSkill(config.TGroup)) { if (typeof selfEid !== "number") continue; return { skillId: s_uuid, skillLv, isFriendly: true, targetPos: null, targetEids: [selfEid], overrides }; } 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, overrides }; } 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: [], overrides }; } return this.emptyCastPlan; } /** * 执行施法: * - 播放前摇与技能动作 * - 延迟到出手时机后,按技能目标类型分发到对敌/友方效果处理 * - 触发技能CD */ private castSkill(castPlan: { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[]; overrides?: SkillOverrides }, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) { if (!smc.mission.in_fight) return; const s_uuid = castPlan.skillId; const skillLv = castPlan.skillLv; const overrides = castPlan.overrides; let config = SkillSet[s_uuid]; const sUp = SkillUpList[s_uuid] ? SkillUpList[s_uuid]:SkillUpList[1001]; const cNum = Math.min(2, Math.max(0, Math.floor(sUp.num ?? 0))); if (!config) return; config = mergeSkillParams(config, overrides); //播放前摇技能动画 heroView.playReady(config.readyAnm); //播放角色攻击动画 heroView.playSkillAnm(config.act); // 因为 castSkill 只由 update 循环调用,处理的必然是 heroAttrs.skills 中的普通技能 // 特殊触发技能(call/dead/atking等)走的是 forceCastTriggerSkill,不会调用 castSkill const skillIds = heroAttrs.getSkillIds(); if (skillIds.includes(s_uuid)) { heroAttrs.atk_count++; this.checkAndTriggerAtkingSkills(heroAttrs, heroView); } // 优先使用技能配置的前摇时间,否则使用全局默认值 // 注意:这里仍然是基于时间的延迟,受帧率波动影响。 // 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。 const delay = config.ready > 0 ? config.ready : FightSet.SKILL_CAST_DELAY; heroView.scheduleOnce(() => { if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return; if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return; const castTimes = 1 + cNum; for (let i = 0; i < castTimes; i++) { if (!smc.mission.play || smc.mission.pause || !smc.mission.in_fight) return; 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); continue; } const enemyTargetPos = this.resolveRepeatCastTargetPos(castPlan.targetPos, i); this.applyEnemySkillEffects(s_uuid, skillLv, config, heroView, heroAttrs, enemyTargetPos, i, overrides); } }, delay); heroAttrs.triggerSkillCD(s_uuid); } /** 检查并触发攻击附加技能 (atking) */ private checkAndTriggerAtkingSkills(heroAttrs: HeroAttrsComp, heroView: HeroViewComp) { SkillTriggerHelper.trigger(SkillTriggerType.Atking, heroAttrs, heroView); } private resolveRepeatCastTargetPos(targetPos: Vec3 | null, castIndex: number): Vec3 | null { if (!targetPos) return null; if (castIndex === 1) return new Vec3(targetPos.x, targetPos.y + 15, targetPos.z); if (castIndex === 2) return new Vec3(targetPos.x, targetPos.y - 15, targetPos.z); return targetPos; } /** 构建技能尝试顺序:优先主动技能,其次普攻 */ 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, castIndex: number = 0, overrides?: SkillOverrides) { if (!caster.node || !caster.node.isValid) return; const parent = caster.node.parent; if (!parent) return; const skill = ecs.getEntity(Skill); const startPos = this.resolveRepeatCastStartPos(caster.node.position, castIndex); skill.load(startPos, parent, s_uuid, targetPos.clone(), caster, cAttrsComp, skillLv, 0, overrides); } /** * 对敌技能效果处理。 * 当前职责:仅处理伤害技能并创建技能实体。 */ private applyEnemySkillEffects(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3 | null, castIndex: number = 0, overrides?: SkillOverrides) { 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, castIndex, overrides); } private resolveRepeatCastStartPos(startPos: Readonly, castIndex: number): Vec3 { if (castIndex === 1) return new Vec3(startPos.x, startPos.y + 15, startPos.z); if (castIndex === 2) return new Vec3(startPos.x, startPos.y - 15, startPos.z); return startPos.clone(); } /** * 友方技能效果处理。 * 当前职责: * 1. 处理治疗与护盾 * 2. 处理 buffs 配置追加 * 3. 保留完整施法信息参数,便于后续扩展更多友方效果 */ private applyFriendlySkillEffects(_s_uuid: number, _skillLv: number, config: SkillConfig, _heroView: HeroViewComp, _cAttrsComp: HeroAttrsComp, targets: HeroViewComp[], _targetPos: Vec3 | null, isCardSkill: boolean = false) { const kind = config.kind ?? SkillKind.Support; const sUp = SkillUpList[_s_uuid] ?? SkillUpList[1001]; const sAp =config.ap+sUp.ap*_skillLv; const sHit=config.hit_count+sUp.hit_count*_skillLv; const applyTargets = kind === SkillKind.Heal ? this.pickHealTargetsByMostMissingHp(targets, sHit) : this.pickRandomFriendlyTargets(targets, sHit); for (const target of applyTargets) { this.applyActualFriendlyEffect(target, kind, sAp, _cAttrsComp, config, sUp, _skillLv); } } private applyActualFriendlyEffect(target: HeroViewComp, kind: SkillKind, sAp: number, _cAttrsComp: HeroAttrsComp, config: SkillConfig, sUp: any, _skillLv: number = 1) { if (!target.ent) return; const model = target.ent.get(HeroAttrsComp); if (!model || model.is_dead) return; if (config.endAnm && config.endAnm !== "") { target.playEnd(config.endAnm); } if (kind === SkillKind.Heal && sAp !== 0) { const addHp = Math.floor(sAp*_cAttrsComp.ap/100); model.add_hp(addHp); target.health(addHp); if (_cAttrsComp.fac === FacSet.HERO) { // 【评分系统 - 防御分】统计团队造成的总治疗量 smc.vmdata.scores.heal_total += addHp; } } else if (kind === SkillKind.Shield && sAp !== 0) { const addShield = Math.max(0, Math.floor(sAp)); model.add_shield(addShield); } else if (kind === SkillKind.Gold) { const baseGold = config.gold ?? config.ap; const addGold = baseGold + (sUp.ap * _skillLv); if (addGold > 0) { MissionEconomy.addCoin(addGold); } } if (config.buff_type !== undefined) { const baseValue = config.ap; let upgradeValue = 0; // 根据 buff 类型选择对应的升级加成 if (config.buff_type === Attrs.ap) upgradeValue = sUp.buff_ap || 0; else if (config.buff_type === Attrs.hp_max) upgradeValue = sUp.buff_hp || 0; else if (config.buff_type === Attrs.critical) upgradeValue = sUp.crt || 0; // 如果后续有冰冻、击晕等,在这里加上对应的 sUp 字段即可,如 sUp.frz / sUp.stun const totalBuffValue = baseValue + upgradeValue; switch (config.buff_type){ case Attrs.ap: model.add_ap(totalBuffValue); break; case Attrs.hp_max: model.add_hp_max(totalBuffValue); break; default: // 除了 hp_max 和 ap,其他固定属性走统一的 add_special_attr 方法 model.add_special_attr(config.buff_type, totalBuffValue); break; } } } private pickRandomFriendlyTargets(targets: HeroViewComp[], hitCount: number): HeroViewComp[] { if (!targets || targets.length === 0) return []; const validHitCount = Math.max(1, Math.floor(hitCount)); if (validHitCount >= targets.length) return [...targets]; const pool = [...targets]; const selected: HeroViewComp[] = []; while (selected.length < validHitCount && pool.length > 0) { const index = Math.floor(Math.random() * pool.length); const [target] = pool.splice(index, 1); if (!target) continue; selected.push(target); } return selected; } private pickHealTargetsByMostMissingHp(targets: HeroViewComp[], hitCount: number): HeroViewComp[] { if (!targets || targets.length === 0) return []; const validHitCount = Math.max(1, Math.floor(hitCount)); const sortedTargets = this.sortTargetsByMostMissingHp(targets); return sortedTargets.slice(0, Math.min(validHitCount, sortedTargets.length)); } private sortTargetsByMostMissingHp(targets: HeroViewComp[]): HeroViewComp[] { return [...targets].sort((a, b) => { const aModel = a.ent?.get(HeroAttrsComp); const bModel = b.ent?.get(HeroAttrsComp); const aMissingHp = aModel && aModel.hp_max > 0 ? Math.max(0, aModel.hp_max - aModel.hp) : -1; const bMissingHp = bModel && bModel.hp_max > 0 ? Math.max(0, bModel.hp_max - bModel.hp) : -1; if (aMissingHp !== bMissingHp) return bMissingHp - aMissingHp; const aRatio = aModel && aModel.hp_max > 0 ? aModel.hp / aModel.hp_max : 1; const bRatio = bModel && bModel.hp_max > 0 ? bModel.hp / bModel.hp_max : 1; return aRatio - bRatio; }); } /** 根据目标 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[] = []; const grid = fac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid; for (const eid of grid) { if (eid >= 0) { if (!includeSelf && typeof selfEid === "number" && eid === selfEid) continue; const entity = ecs.getEntityByEid(eid); if (entity) { const model = entity.get(HeroAttrsComp); if (model && !model.is_dead && !model.is_reviving) { eids.push(eid); } } } } return eids; } /** 判定施法计划是否具备有效目标 */ private hasCastTarget(castPlan: { skillId: number; skillLv: number; isFriendly: boolean; targetPos: Vec3 | null; targetEids: number[]; overrides?: SkillOverrides }): 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; } /** * 根据网格快速查找最近的敌人 * 英雄查找怪物:按列从前往后(列0 -> 列3),列内优先同排 * 怪物查找英雄:按列从前往后(列0 -> 列1),列内优先中路或同排 */ private findNearestEnemyByGrid(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null { if (!heroView.node) return null; const isHero = heroAttrs.fac === FacSet.HERO; const myPosIndex = heroAttrs.posIndex; let myRow = myPosIndex >= 0 ? myPosIndex % 3 : 1; // 默认中路 const currentX = heroView.node.position.x; let targetView: HeroViewComp | null = null; let minCol = -1; if (isHero) { // 英雄找怪物 for (let col = 0; col < 4; col++) { // 列内顺序:优先同排,其次中路,再次其他 const rowOrder = myRow === 1 ? [1, 0, 2] : [myRow, 1, myRow === 0 ? 2 : 0]; for (const row of rowOrder) { const idx = col * 3 + row; const eid = smc.mission.monGrid[idx]; if (eid >= 0) { const target = ecs.getEntityByEid(eid); if (target) { const tModel = target.get(HeroAttrsComp); const tView = target.get(HeroViewComp); if (tModel && !tModel.is_dead && !tModel.is_reviving && tView && tView.node) { const dist = Math.abs(currentX - tView.node.position.x); if (dist <= maxRange) { return tView; // 找到列内最优且在射程内的目标,直接返回 } } } } } } } else { // 怪物找英雄 for (let col = 0; col < 2; col++) { // 列内顺序:怪物配置优先中路 const rowOrder = [1, myRow, myRow === 0 ? 2 : 0]; for (const row of rowOrder) { const idx = col * 3 + row; const eid = smc.mission.heroGrid[idx]; if (eid >= 0) { const target = ecs.getEntityByEid(eid); if (target) { const tModel = target.get(HeroAttrsComp); const tView = target.get(HeroViewComp); if (tModel && !tModel.is_dead && !tModel.is_reviving && tView && tView.node) { const dist = Math.abs(currentX - tView.node.position.x); if (dist <= maxRange) { return tView; } } } } } } } return null; } /** * 在施法距离内查找最近敌人。 * 替换为网格化查找,大幅提升性能并解决同排优先问题。 */ private findNearestEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null { return this.findNearestEnemyByGrid(heroAttrs, heroView, maxRange); } /** * 在施法距离内查找“最前排”敌人。 * 依据网格排布,列0天然是最前排,因此直接复用网格查找即可。 */ private findFrontEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number, nearestEnemy: HeroViewComp): HeroViewComp | null { return this.findNearestEnemyByGrid(heroAttrs, heroView, maxRange); } /** * 解析对敌技能目标点: * - 非范围技能:按施法方向生成目标点 * - range 范围技能:优先前排敌人位置 * - aoe_grid 范围技能:以中路第2或第3列为目标(若存在敌人优先第2列,否则第3列;这里简化为直接返回第2列中路的固定坐标,或者根据情况处理) */ private resolveEnemyCastTargetPos(config: SkillConfig, heroAttrs: HeroAttrsComp, heroView: HeroViewComp, nearestEnemy: HeroViewComp, maxRange: number): Vec3 | null { if (config.DTType === DTType.aoe_grid) { const isHero = heroAttrs.fac === FacSet.HERO; if (isHero) { // 英雄打怪物,目标为怪物网格的第2列(idx 1)中路(row 1)或第3列(idx 2)中路 // MissionMonComp.MON_POSITIONS 顺序: // 列1: 0,1,2 (X=60) // 列2: 3,4,5 (X=140) -> 第2列中路是 index 4 // 列3: 6,7,8 (X=220) -> 第3列中路是 index 7 // 简单起见,可以固定取第2列中路(index 4)或第3列中路(index 7) // 若最近的敌人在更后排,也可以取那个敌人的列。 // 需求:三类 需要以中路 第 2、 3列为目标。 // 我们直接返回第2列中路坐标(X=140, Y=GAME_LINE)或者根据最近敌人判断。 // 需求说明为第 2 或 第 3 列,这里我们可以根据目标位置,如果目标在第3列及以后,打第3列,否则打第2列。 let targetCol = 1; // 0-based, so 1 is 2nd col if (nearestEnemy && nearestEnemy.node) { // 判断目标所在的列 const ex = nearestEnemy.node.position.x; // X=60(列1), X=140(列2), X=220(列3), X=300(列4) if (ex >= 200) targetCol = 2; // 第3列 } const targetIdx = targetCol * 3 + 1; // 1是中路 return MissionMonCompComp.MON_POSITIONS[targetIdx].clone(); } else { // 怪物打英雄,英雄网格: // 列1: 0,1,2 (X=-210) // 列2: 3,4,5 (X=-300) // 英雄只有2列,第2列中路是 index 4 return MissionHeroComp.HERO_POSITIONS[4].clone(); } } if (config.RType === RType.bezier) { return nearestEnemy.node?.position.clone() ?? 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; return heroAttrs.dis; } /** 生成沿目标方向的施法目标坐标 */ private buildEnemyCastTargetPos(caster: HeroViewComp, target: HeroViewComp, castRange: number): Vec3 { // 直接返回目标的真实坐标,保留其 Y 轴信息,确保能向目标真实所在位置发射 // 考虑到目前角色的 y 坐标都是脚底(碰撞体底部),为了命中身体中心,给目标 y 加上高度的一半 let halfHeight = 0; if (target.node) { const transform = target.node.getComponent('cc.UITransform') as any; if (transform) { halfHeight = transform.height / 2; } else { halfHeight = 40; // 如果没有 UITransform,给一个默认高度偏移 } } const pos = target.node.position.clone(); pos.y += halfHeight; // 至于最终投射物是否要飞出屏幕(例如线性弹道延长至 +-500),由 SMoveSystem 统一处理 return pos; } }