Files
pixelheros/assets/script/game/hero/SCastSystem.ts
pan 747b6d17cf feat(skill-system): 新增网格AOE技能的目标选择逻辑
新增DTType.aoe_grid枚举类型用于标识3*3网格范围攻击技能
实现该类型技能的目标位置解析逻辑,区分敌我单位的中路列选择
调整6201至6206号技能的类型为aoe_grid
2026-06-17 10:46:36 +08:00

765 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>(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>(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<Vec3>, 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;
}
}