403 lines
18 KiB
TypeScript
403 lines
18 KiB
TypeScript
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";
|
||
import { Attrs } from "../common/config/HeroAttrs";
|
||
|
||
/**
|
||
* ==================== 自动施法系统 ====================
|
||
*
|
||
* 职责:
|
||
* 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
|
||
if(!smc.mission.in_fight) 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) {
|
||
if (!smc.mission.in_fight) return;
|
||
const s_uuid = castPlan.skillId;
|
||
const skillLv = castPlan.skillLv;
|
||
const 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;
|
||
//播放前摇技能动画
|
||
heroView.playReady(config.readyAnm);
|
||
//播放角色攻击动画
|
||
heroView.playSkillAnm(config.act);
|
||
|
||
// 优先使用技能配置的前摇时间,否则使用全局默认值
|
||
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。
|
||
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
|
||
const delay = config.ready > 0 ? config.ready : GameConst.Battle.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);
|
||
}
|
||
}, delay);
|
||
heroAttrs.triggerSkillCD(s_uuid);
|
||
}
|
||
|
||
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) {
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 对敌技能效果处理。
|
||
* 当前职责:仅处理伤害技能并创建技能实体。
|
||
*/
|
||
private applyEnemySkillEffects(s_uuid: number, skillLv: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp, targetPos: Vec3 | null, castIndex: number = 0) {
|
||
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);
|
||
}
|
||
|
||
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) {
|
||
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 selectedTargets = this.pickRandomFriendlyTargets(targets, sHit);
|
||
const applyTargets = kind === SkillKind.Heal ? this.sortTargetsByLowestHp(selectedTargets) : selectedTargets;
|
||
for (const target of applyTargets) {
|
||
if (!target.ent) continue;
|
||
const model = target.ent.get(HeroAttrsComp);
|
||
if (!model || model.is_dead) continue;
|
||
if (kind === SkillKind.Heal && sAp !== 0) {
|
||
const addHp = Math.floor(sAp*_cAttrsComp.ap/100);//技能的ap是百分值 需要/100
|
||
model.add_hp(addHp);
|
||
target.health(addHp);
|
||
} else if (kind === SkillKind.Shield && sAp !== 0) {
|
||
const addShield = Math.max(0, Math.floor(sAp));
|
||
model.add_shield(addShield);
|
||
}
|
||
if (!config.buffs || config.buffs.length === 0) continue;
|
||
for (const buffConf of config.buffs) {
|
||
if (!buffConf) continue;
|
||
const sBuffAp=buffConf.value+sUp.buff_ap
|
||
const sBuffHp=buffConf.value+sUp.buff_hp
|
||
switch (buffConf.buff){
|
||
case Attrs.ap:
|
||
model.add_ap(sBuffAp)
|
||
//加工动画
|
||
break
|
||
case Attrs.hp_max:
|
||
model.add_hp_max(sBuffHp)
|
||
//加最大生命值动画
|
||
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 sortTargetsByLowestHp(targets: HeroViewComp[]): HeroViewComp[] {
|
||
return [...targets].sort((a, b) => {
|
||
const aModel = a.ent?.get(HeroAttrsComp);
|
||
const bModel = b.ent?.get(HeroAttrsComp);
|
||
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;
|
||
if (aRatio !== bRatio) return aRatio - bRatio;
|
||
const aHp = aModel?.hp ?? Number.MAX_SAFE_INTEGER;
|
||
const bHp = bModel?.hp ?? Number.MAX_SAFE_INTEGER;
|
||
return aHp - bHp;
|
||
});
|
||
}
|
||
|
||
/** 根据目标 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);
|
||
}
|
||
}
|