Files
pixelheros/assets/script/game/hero/SCastSystem.ts
pan b6b2dff986 refactor(battle): 重构战斗目标查找与位置管理逻辑
新增全局位置网格系统,用于按索引存储敌我单位实体ID:
-  在SingletonModuleComp添加heroGrid与monGrid数组
-  为HeroAttrsComp新增posIndex字段记录位置索引并初始化

优化战斗核心流程:
-  重构MissionHeroComp的位置选择逻辑,拆分方法返回位置索引而非直接坐标,优化位置占用检测
-  重构SCastSystem的目标查找与收集逻辑,改用网格遍历替代全量实体查询,大幅提升性能
-  统一三路单位的查找优先级,简化代码提升可维护性
-  完善Hero与Monster的创建销毁流程,同步更新网格的单位注册与注销信息
2026-06-17 09:45:46 +08:00

731 lines
33 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";
/**
* ==================== 自动施法系统 ====================
*
* 职责:
* 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);
}
/**
* 解析对敌技能目标点:
* - 非范围技能:按施法方向生成目标点
* - 范围技能:优先前排敌人位置
*/
private resolveEnemyCastTargetPos(config: SkillConfig, heroAttrs: HeroAttrsComp, heroView: HeroViewComp, nearestEnemy: HeroViewComp, maxRange: number): Vec3 | null {
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;
}
}