diff --git a/assets/script/game/hero/SACastSystem.ts b/assets/script/game/hero/SACastSystem.ts new file mode 100644 index 00000000..99a4da25 --- /dev/null +++ b/assets/script/game/hero/SACastSystem.ts @@ -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); + + // 获取施法者位置作为起始位置 + 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; + } + +} \ No newline at end of file