feat(hero): 添加自动施法系统,支持技能AI决策与执行

实现英雄技能自动施法系统,包含以下功能:
- 根据技能类型(伤害/治疗/护盾/Buff)自动检测可施放技能
- 支持多种AI策略,包括目标选择、距离检查和CD管理
- 提供手动施法接口,支持普通攻击、技能和必杀技
- 集成天赋系统,处理风怒、双技能等天赋效果
- 实现治疗、护盾和Buff技能的目标选择与效果应用
- 添加调试日志支持,便于系统行为追踪
This commit is contained in:
walkpan
2026-03-11 23:15:47 +08:00
parent a544f65d73
commit 87b21864b1

View File

@@ -0,0 +1,621 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Vec3, v3 } from "cc";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { HSSet, SkillSet, SType, TGroup, SkillConfig } from "../common/config/SkillSet";
import { HeroSkillsComp, SkillSlot } from "./HeroSkills";
import { Skill } from "../skill/Skill";
import { smc } from "../common/SingletonModuleComp";
import { TalComp } from "./TalComp";
import { TalEffet, TriType } from "../common/config/TalSet";
import { BoxSet, FacSet } from "../common/config/GameSet";
import { GameConst } from "../common/config/GameConst";
import { Attrs } from "../common/config/HeroAttrs";
import { mLogger } from "../common/Logger";
/**
* ==================== 自动施法系统 ====================
*
* 职责:
* 1. 检测可施放的技能
* 2. 根据策略自动施法AI
* 3. 选择目标
* 4. 添加施法请求标记
*
* 设计理念:
* - 负责"何时施法"的决策
* - 通过添加 CSRequestComp 触发施法
* - 可被玩家输入系统或AI系统复用
* - 支持多种AI策略
*/
@ecs.register('SACastSystem')
export class SACastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
debugMode: boolean = false; // 是否启用调试模式
filter(): ecs.IMatcher {
return ecs.allOf(HeroSkillsComp, HeroAttrsComp, HeroViewComp);
}
update(e: ecs.Entity): void {
if(!smc.mission.play ) return;
if(smc.mission.pause) return
const skills = e.get(HeroSkillsComp);
if (!skills) return;
// AI 降频每0.2秒执行一次
skills.ai_timer += this.dt;
if (skills.ai_timer < GameConst.Battle.AI_CHECK_INTERVAL) return;
skills.ai_timer = 0;
const heroAttrs = e.get(HeroAttrsComp);
const heroView = e.get(HeroViewComp);
if (!heroAttrs || !heroView) return;
// 检查基本条件
if (heroAttrs.is_dead || heroAttrs.is_reviving || heroAttrs.isStun() || heroAttrs.isFrost()) return;
// 移除 is_atking 检查实现只要距离和CD满足即施法
// if (!heroAttrs.is_atking) return;
const readySkills = skills.getReadySkills();
if (readySkills.length === 0) return;
// 选择第一个可施放的技能(支持伤害/治疗/护盾)
for (const s_uuid of readySkills) {
const skill = skills.getSkill(s_uuid);
if (!skill) continue;
if (skill.hset === HSSet.max && !skills.max_auto) continue;
const config = SkillSet[skill.s_uuid];
if (!config) continue;
// 根据技能类型检查目标
if (config.SType === SType.damage) {
if (!this.hasEnemyInSkillRange(heroView, heroAttrs, skill.dis)) continue;
} else if (config.SType === SType.heal || config.SType === SType.shield) {
if (!this.hasTeamInSkillRange(heroView, heroAttrs, skill.dis)) continue;
} else if (config.SType === SType.buff) {
if (!this.hasBuffTarget(heroView, heroAttrs, skill.dis, config.TGroup)) continue;
}
// ✅ 开始执行施法
this.startCast(e, skill, skill.hset);
// 一次只施放一个技能
break;
}
}
private startCast(e: ecs.Entity,skill:SkillSlot,hset:HSSet): boolean {
if (!skill||!e) return false
const skills = e.get(HeroSkillsComp);
const heroAttrs = e.get(HeroAttrsComp);
const heroView = e.get(HeroViewComp);
// 3. 检查施法条件
if (!this.checkCastConditions(skills, heroAttrs, skill.s_uuid)) return false
// 4. 执行施法
const castSucess = this.executeCast(e, skill.s_uuid, heroView,hset);
// 5. 扣除资源和重置CD
if (castSucess) {
// 🔥 怪物不消耗蓝
if (heroAttrs.fac !== FacSet.MON) {
// 手动更新技能距离缓存
heroAttrs.updateSkillDistanceCache(skills);
}
skills.resetCD(skill.s_uuid);
}
return castSucess;
}
public manualCast(e: ecs.Entity, s_uuid: number): boolean {
if (!e) return false
const skills = e.get(HeroSkillsComp)
const heroAttrs = e.get(HeroAttrsComp)
const heroView = e.get(HeroViewComp)
if (!skills || !heroAttrs || !heroView) return false
const slot = skills.getSkill(s_uuid)
if (!slot) return false
return this.startCast(e, slot, slot.hset)
}
public manualCastMax(e: ecs.Entity): boolean {
const skills = e.get(HeroSkillsComp)
if (!skills) return false
for (const key in skills.skills) {
const s_uuid = Number(key)
const slot = skills.getSkill(s_uuid)
if (slot && slot.hset === HSSet.max) {
return this.manualCast(e, s_uuid)
}
}
return false
}
/**
* 检查施法条件
*/
private checkCastConditions(skills: HeroSkillsComp, heroAttrs: HeroAttrsComp, s_uuid: number): boolean {
// 检查角色状态
if (heroAttrs.is_dead) {
return false;
}
// 检查控制状态(眩晕、冰冻)
if (heroAttrs.isStun() || heroAttrs.isFrost()) {
return false;
}
// 检查CD
if (!skills.canCast(s_uuid)) {
return false;
}
return true;
}
/**
* 执行施法
*/
private executeCast(casterEntity: ecs.Entity, s_uuid: number, heroView: HeroViewComp,hset:HSSet): boolean {
const heroAttrs=casterEntity.get(HeroAttrsComp)
const config = SkillSet[s_uuid];
if (!config) {
mLogger.error(this.debugMode, 'SACastSystem', "[SACastSystem] 技能配置不存在:", s_uuid);
return false;
}
// 1. 播放施法动画
heroView.playSkillEffect(s_uuid);
/**********************天赋处理*************************************************************************/
// 2. 更新攻击类型的天赋触发值,技能和必杀级
if(casterEntity.has(TalComp)){
const talComp = casterEntity.get(TalComp);
if (hset === HSSet.atk) talComp.updateCur(TriType.ATK);
if (hset === HSSet.skill) talComp.updateCur(TriType.SKILL);
}
/**********************天赋处理*************************************************************************/
// 根据技能类型执行不同逻辑
if (config.SType === SType.heal) {
return this.executeHealSkill(casterEntity, s_uuid, heroView, hset);
} else if (config.SType === SType.shield) {
return this.executeShieldSkill(casterEntity, s_uuid, heroView, hset);
} else if (config.SType === SType.buff) {
return this.executeBuffSkill(casterEntity, s_uuid, heroView, hset);
}
// 获取目标位置(伤害技能)
let targets = this.sTargets(heroView, s_uuid);
if (targets.length === 0) {
mLogger.warn(this.debugMode, 'SACastSystem', "[SACastSystem] 没有找到有效目标");
return false;
}
// 2.1 普通攻击逻辑
if (hset === HSSet.atk){
let delay = GameConst.Battle.SKILL_CAST_DELAY
let ext_dmg = heroAttrs.useCountValTal(TalEffet.ATK_DMG);
heroView.scheduleOnce(() => {
this.createSkill(s_uuid, heroView,targets,ext_dmg);
}, delay);
//风怒wfuny 只针对 普通攻击起效
if (heroAttrs.useCountTal(TalEffet.WFUNY)){
let ext2_dmg = heroAttrs.useCountValTal(TalEffet.ATK_DMG);
let delay = GameConst.Battle.SKILL_CAST_DELAY
heroView.playSkillEffect(s_uuid);
//需要再添加 风怒动画
heroView.scheduleOnce(() => {
this.createSkill(s_uuid, heroView,targets,ext2_dmg);
},delay);
}
}
// 2.2 技能攻击逻辑
if(hset === HSSet.skill){
let delay = GameConst.Battle.SKILL_CAST_DELAY
let ext_dmg = heroAttrs.useCountValTal(TalEffet.SKILL_DMG);
heroView.scheduleOnce(() => {
this.createSkill(s_uuid, heroView,targets,ext_dmg);
}, delay);
// 双技能 只针对 技能起效
if(heroAttrs.useCountTal(TalEffet.D_SKILL)){
let ext2_dmg = heroAttrs.useCountValTal(TalEffet.SKILL_DMG);
let delay = GameConst.Battle.SKILL_CAST_DELAY
heroView.playSkillEffect(s_uuid);
//需要再添加 双技能动画
heroView.scheduleOnce(() => {
this.createSkill(s_uuid, heroView,targets,ext2_dmg);
},delay);
}
}
// 2.3 必杀技能逻辑
if(hset === HSSet.max){
let delay = GameConst.Battle.SKILL_CAST_DELAY
heroView.playSkillEffect(s_uuid);
//需要再添加 最大伤害动画
heroView.scheduleOnce(() => {
this.createSkill(s_uuid, heroView,targets);
},delay);
}
return true;
}
/**
* 创建技能实体
*/
private createSkill(s_uuid: number, caster: HeroViewComp,targets:Vec3[]=[],ext_dmg:number=0) {
// 检查节点有效性
if (!caster.node || !caster.node.isValid) {
mLogger.warn(this.debugMode, 'SACastSystem', "[SACastSystem] 施法者节点无效");
return;
}
// 获取场景节点
const parent = caster.node.parent;
if (!parent) {
mLogger.warn(this.debugMode, 'SACastSystem', "[SACastSystem] 场景节点无效");
return;
}
// 创建技能实体
const skill = ecs.getEntity<Skill>(Skill);
// 获取施法者位置作为起始位置
const startPos = caster.node.position.clone();
const targetPos = targets[0]; // 使用第一个目标位置
// mLogger.log(this.debugMode, 'SACastSystem', `[SACastSystem]: ${s_uuid}, 起始位置: ${startPos}, 目标位置: ${targetPos}`);
// 加载技能实体(包括预制体、组件初始化等)
skill.load(startPos, parent, s_uuid, targetPos, caster,ext_dmg);
}
/**
* 选择目标位置
*/
private sTargets(caster: HeroViewComp, s_uuid: number): Vec3[] {
const heroAttrs = caster.ent.get(HeroAttrsComp);
if (!heroAttrs) return [];
const config = SkillSet[s_uuid];
if (!config) return this.sDefaultTargets(caster, heroAttrs.fac);
const maxTargets = Math.max(GameConst.Skill.MIN_TARGET_COUNT, config.t_num ?? 1);
const targets = this.sDamageTargets(caster, config, maxTargets);
if (targets.length === 0) {
targets.push(...this.sDefaultTargets(caster, heroAttrs.fac));
}
return targets;
}
/**
* 选择伤害技能目标
*/
private sDamageTargets(caster: HeroViewComp, config: SkillConfig, maxTargets: number): Vec3[] {
const targets: Vec3[] = [];
const heroAttrs = caster.ent.get(HeroAttrsComp);
if (!heroAttrs) return targets;
const range = Number(config.dis ?? GameConst.Battle.DEFAULT_SEARCH_RANGE);
const enemyPositions = this.findNearbyEnemies(caster, heroAttrs.fac, range);
// 选择最多maxTargets个目标
for (let i = 0; i < Math.min(maxTargets, enemyPositions.length); i++) {
targets.push(enemyPositions[i]);
}
// 如果没有找到敌人,使用默认位置
if (targets.length === 0) {
targets.push(...this.sDefaultTargets(caster, heroAttrs.fac));
}
return targets;
}
/**
* 选择默认目标
*/
private sDefaultTargets(caster: HeroViewComp, fac: number): Vec3[] {
const targets: Vec3[] = [];
const defaultX = fac === 0 ? GameConst.Battle.DEFAULT_TARGET_X_RIGHT : GameConst.Battle.DEFAULT_TARGET_X_LEFT;
targets.push(v3(defaultX, BoxSet.GAME_LINE, GameConst.Battle.DEFAULT_TARGET_Z));
return targets;
}
/**
* 查找附近的敌人
*/
private findNearbyEnemies(caster: HeroViewComp, fac: number, range: number): Vec3[] {
const enemies: Vec3[] = [];
if (!caster || !caster.node) return enemies;
const currentPos = caster.node.position;
const results: { pos: Vec3; dist: number; laneBias: number }[] = [];
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).some(e => {
const model = e.get(HeroAttrsComp);
const view = e.get(HeroViewComp);
if (!model || !view || !view.node) return false;
if (model.is_dead) return false;
if (model.fac === fac) return false;
const pos = view.node.position.clone();
pos.y += GameConst.Battle.SEARCH_Y_OFFSET;
const dist = Math.abs(currentPos.x - pos.x);
if (dist <= range) {
const laneBias = Math.abs(currentPos.y - pos.y);
results.push({ pos: pos, dist, laneBias });
}
return false;
});
results.sort((a, b) => {
if (a.laneBias !== b.laneBias) return a.laneBias - b.laneBias;
return a.dist - b.dist;
});
for (const r of results) enemies.push(r.pos);
return enemies;
}
/**
* 检查技能攻击范围内是否有敌人
*/
private hasEnemyInSkillRange(heroView: HeroViewComp, heroAttrs: HeroAttrsComp, skillDistance: number): boolean {
if (!heroView || !heroView.node) return false;
const currentPos = heroView.node.position;
const team = heroAttrs.fac;
let found = false;
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).some(e => {
const model = e.get(HeroAttrsComp);
const view = e.get(HeroViewComp);
if (!view || !view.node) return false;
const distance = Math.abs(currentPos.x - view.node.position.x);
if (model.fac !== team && !model.is_dead) {
if (distance <= skillDistance) {
found = true;
return true;
}
}
});
return found;
}
/**
* 检查技能范围内是否有友军
*/
private hasTeamInSkillRange(heroView: HeroViewComp, heroAttrs: HeroAttrsComp, skillDistance: number): boolean {
if (!heroView || !heroView.node) return false;
const currentPos = heroView.node.position;
const team = heroAttrs.fac;
let found = false;
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).some(e => {
const model = e.get(HeroAttrsComp);
const view = e.get(HeroViewComp);
if (!view || !view.node) return false;
const distance = Math.abs(currentPos.x - view.node.position.x);
if (model.fac === team && !model.is_dead) {
if (distance <= skillDistance) {
found = true;
return true;
}
}
});
return found;
}
/**
* 检查Buff技能是否有目标
*/
private hasBuffTarget(heroView: HeroViewComp, heroAttrs: HeroAttrsComp, skillDistance: number, tGroup: TGroup): boolean {
if (tGroup === TGroup.Self) return true; // 自身Buff总是可以释放
// 如果是团队Buff检查范围内是否有队友
if (tGroup === TGroup.Team || tGroup === TGroup.Ally) {
return this.hasTeamInSkillRange(heroView, heroAttrs, skillDistance);
}
return false;
}
/**
* 执行Buff技能
*/
private executeBuffSkill(casterEntity: ecs.Entity, s_uuid: number, heroView: HeroViewComp, hset: HSSet): boolean {
const hAttrsCom = casterEntity.get(HeroAttrsComp);
const config = SkillSet[s_uuid];
if (!config || !config.buffs || config.buffs.length === 0) return false;
const targets = this.sBuffTargets(casterEntity, heroView, hAttrsCom, config);
if (targets.length === 0) return false;
const delay = GameConst.Battle.SKILL_CAST_DELAY;
heroView.scheduleOnce(() => {
for (const targetEntity of targets) {
const targetAttrs = targetEntity.get(HeroAttrsComp);
if (!targetAttrs) continue;
// 应用所有配置的Buff
for (const buffConf of config.buffs) {
// 检查概率
if (buffConf.chance >= 1 || Math.random() < buffConf.chance) {
targetAttrs.addBuff(buffConf);
mLogger.log(this.debugMode, 'SACastSystem', `[SACastSystem] Buff生效: 施法者=${casterEntity.get(HeroAttrsComp)?.hero_name}, 技能=${config.name}, 目标=${targetAttrs.hero_name}, Buff类型=${buffConf.buff}, 值=${buffConf.value}`);
}
}
}
}, delay);
return true;
}
/**
* 选择Buff目标
*/
private sBuffTargets(casterEntity: ecs.Entity, casterView: HeroViewComp, heroAttrs: HeroAttrsComp, config: SkillConfig): ecs.Entity[] {
const targets: ecs.Entity[] = [];
const tGroup = config.TGroup;
// 1. 自身
if (tGroup === TGroup.Self) {
targets.push(casterEntity);
return targets;
}
// 2. 团队/友军
if (tGroup === TGroup.Team || tGroup === TGroup.Ally) {
const maxTargets = Math.max(GameConst.Skill.MIN_TARGET_COUNT, Number(config.t_num ?? 1));
const range = Number(config.dis ?? GameConst.Battle.DEFAULT_SEARCH_RANGE);
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).forEach(e => {
const model = e.get(HeroAttrsComp);
const view = e.get(HeroViewComp);
if (!model || !view || !view.node) return;
if (model.fac !== heroAttrs.fac) return; // 必须是同阵营
if (model.is_dead) return;
const distance = Math.abs(casterView.node.position.x - view.node.position.x);
if (distance <= range) {
targets.push(e);
}
});
return targets.slice(0, maxTargets);
}
return targets;
}
/**
* 执行治疗技能
*/
private executeHealSkill(casterEntity: ecs.Entity, s_uuid: number, heroView: HeroViewComp, hset: HSSet): boolean {
const hAttrsCom = casterEntity.get(HeroAttrsComp);
const config = SkillSet[s_uuid];
if (!config) return false;
const targets = this.sHealTargets(heroView, hAttrsCom, config);
if (targets.length === 0) return false;
const healAmount = config.ap * hAttrsCom.Attrs[Attrs.HP_MAX]/100;
const delay = GameConst.Battle.SKILL_CAST_DELAY;
heroView.scheduleOnce(() => {
for (const targetEntity of targets) {
const targetAttrs = targetEntity.get(HeroAttrsComp);
const targetView = targetEntity.get(HeroViewComp);
if (!targetAttrs || !targetView) continue;
targetAttrs.add_hp(healAmount, true);
targetView.health(healAmount);
mLogger.log(this.debugMode, 'SACastSystem', `[SACastSystem] 治疗生效: 施法者=${casterEntity.get(HeroAttrsComp)?.hero_name}, 技能=${config.name}, 目标=${targetAttrs.hero_name}, 治疗量=${healAmount}`);
}
}, delay);
return true;
}
/**
* 执行护盾技能
*/
private executeShieldSkill(casterEntity: ecs.Entity, s_uuid: number, heroView: HeroViewComp, hset: HSSet): boolean {
const hAttrsCom = casterEntity.get(HeroAttrsComp);
const config = SkillSet[s_uuid];
if (!config) return false;
const targets = this.sShieldTargets(heroView, hAttrsCom, config);
if (targets.length === 0) return false;
const shieldAmount = config.ap * hAttrsCom.Attrs[Attrs.HP_MAX]/100;
const delay = GameConst.Battle.SKILL_CAST_DELAY;
heroView.scheduleOnce(() => {
for (const targetEntity of targets) {
const targetAttrs = targetEntity.get(HeroAttrsComp);
const targetView = targetEntity.get(HeroViewComp);
if (!targetAttrs || !targetView) continue;
targetAttrs.add_shield(shieldAmount, true);
targetView.add_shield(shieldAmount);
mLogger.log(this.debugMode, 'SACastSystem', `[SACastSystem] 护盾生效: 施法者=${casterEntity.get(HeroAttrsComp)?.hero_name}, 技能=${config.name}, 目标=${targetAttrs.hero_name}, 护盾量=${shieldAmount}`);
}
}, delay);
return true;
}
/**
* 选择治疗目标
*/
private sHealTargets(caster: HeroViewComp, heroAttrs: HeroAttrsComp, config: SkillConfig): ecs.Entity[] {
const targets: ecs.Entity[] = [];
const maxTargets = Math.max(GameConst.Skill.MIN_TARGET_COUNT, Number(config.t_num ?? 1));
const range = Number(config.dis ?? GameConst.Battle.DEFAULT_SEARCH_RANGE);
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).forEach(e => {
const model = e.get(HeroAttrsComp);
const view = e.get(HeroViewComp);
if (!model || !view || !view.node) return;
if (model.fac !== heroAttrs.fac) return;
if (model.is_dead) return;
const distance = Math.abs(caster.node.position.x - view.node.position.x);
if (distance <= range) {
targets.push(e);
}
});
targets.sort((a, b) => {
const attrsA = a.get(HeroAttrsComp);
const attrsB = b.get(HeroAttrsComp);
if (!attrsA || !attrsB) return 0;
return attrsA.hp - attrsB.hp;
});
return targets.slice(0, maxTargets);
}
/**
* 选择护盾目标
*/
private sShieldTargets(caster: HeroViewComp, heroAttrs: HeroAttrsComp, config: SkillConfig): ecs.Entity[] {
const targets: ecs.Entity[] = [];
const maxTargets = Math.max(GameConst.Skill.MIN_TARGET_COUNT, Number(config.t_num ?? 1));
const range = Number(config.dis ?? GameConst.Battle.DEFAULT_SEARCH_RANGE);
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).forEach(e => {
const model = e.get(HeroAttrsComp);
const view = e.get(HeroViewComp);
if (!model || !view || !view.node) return;
if (model.fac !== heroAttrs.fac) return;
if (model.is_dead) return;
const distance = Math.abs(caster.node.position.x - view.node.position.x);
if (distance <= range) {
targets.push(e);
}
});
return targets.slice(0, maxTargets);
}
/**
* 根据位置查找实体
*/
private findEntityAtPosition(pos: Vec3): ecs.Entity | null {
let foundEntity: ecs.Entity | null = null;
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).some(e => {
const view = e.get(HeroViewComp);
if (!view || !view.node) return false;
const distance = Vec3.distance(pos, view.node.position);
if (distance < 50) {
foundEntity = e;
return true;
}
return false;
});
return foundEntity;
}
}