From 5d09b3361e8a15d514b8b997b1af6d3ea470263b Mon Sep 17 00:00:00 2001 From: walkpan Date: Thu, 12 Mar 2026 09:13:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(battle):=20=E9=87=8D=E6=9E=84=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E6=96=BD=E6=94=BE=E4=B8=8E=E6=88=98=E6=96=97=E8=B7=9D?= =?UTF-8?q?=E7=A6=BB=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增技能距离缓存机制,根据英雄类型动态计算最小和最大攻击范围 - 重构SCastSystem实现完整的技能施放逻辑,支持伤害、治疗、护盾和buff技能 - 在Hero和Monster初始化时调用updateSkillDistanceCache预计算技能距离 - 修改HeroMoveSystem和MonMoveSystem使用动态战斗范围,支持撤退逻辑 - 优化Skill实体创建,增加对象池支持 - 添加技能CD触发方法和状态检查方法 --- assets/script/game/hero/Hero.ts | 3 +- assets/script/game/hero/HeroAttrsComp.ts | 48 ++++++--- assets/script/game/hero/HeroMove.ts | 20 ++-- assets/script/game/hero/Mon.ts | 3 +- assets/script/game/hero/MonMove.ts | 20 ++-- assets/script/game/hero/SCastSystem.ts | 129 +++++++++++++++++++++-- assets/script/game/skill/Skill.ts | 6 +- 7 files changed, 194 insertions(+), 35 deletions(-) diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index 629b0a74..23422c38 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -91,6 +91,7 @@ export class Hero extends ecs.Entity { if(hero.skills[1]) { model.skill_id=hero.skills[1] } + model.updateSkillDistanceCache(model.skill_id || model.atk_id); // 初始化 buff/debuff 系统 model.initAttrs(); @@ -143,4 +144,4 @@ export class HeroLifecycleSystem extends ecs.ComblockSystem mLogger.log(true, 'HeroLifecycle', `英雄离开世界: 实体ID ${e.eid}`); } } -} \ No newline at end of file +} diff --git a/assets/script/game/hero/HeroAttrsComp.ts b/assets/script/game/hero/HeroAttrsComp.ts index 874a4fe2..cda1ca7e 100644 --- a/assets/script/game/hero/HeroAttrsComp.ts +++ b/assets/script/game/hero/HeroAttrsComp.ts @@ -2,8 +2,8 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { GameEvent } from "../common/config/GameEvent"; import { Attrs, BType } from "../common/config/HeroAttrs"; -import { BuffConf, SkillDisVal, SkillRange, SkillSet } from "../common/config/SkillSet"; -import { HeroInfo } from "../common/config/heroSet"; +import { BuffConf, SkillDisVal, SkillRange } from "../common/config/SkillSet"; +import { HeroInfo, HType } from "../common/config/heroSet"; import { mLogger } from "../common/Logger"; import { _decorator } from "cc"; @@ -267,11 +267,25 @@ export class HeroAttrsComp extends ecs.Comp { this.a_cd+=dt if(this.a_cd >= this.a_cd_max) this.can_atk = true } - if(this.skill_id !=0&&this.can_skill){ + if(this.skill_id !=0&&!this.can_skill){ this.s_cd+=dt if(this.s_cd >= this.s_cd_max) this.can_skill = true } } + isStun(): boolean { + return this.in_stun; + } + isFrost(): boolean { + return this.in_frost; + } + triggerAtkCD() { + this.a_cd = 0; + this.can_atk = false; + } + triggerSkillCD() { + this.s_cd = 0; + this.can_skill = false; + } // ==================== 临时 BUFF/DEBUFF 更新 ==================== /** * 更新临时 buff/debuff 的剩余时间 @@ -342,16 +356,26 @@ export class HeroAttrsComp extends ecs.Comp { * @param skillsComp 技能组件 */ public updateSkillDistanceCache(skill_id:number): void { - let skillConf=SkillSet[skill_id]; - if (!skillConf) { - this.maxSkillDistance = 0; - this.minSkillDistance = 0; - return; + void skill_id; + let rangeType = this.rangeType; + if (rangeType === undefined || rangeType === null) { + if (this.type === HType.remote) { + rangeType = SkillRange.Long; + } else if (this.type === HType.mage || this.type === HType.support) { + rangeType = SkillRange.Mid; + } else { + rangeType = SkillRange.Melee; + } } - // 最远距离使用当前MP可施放的技能 - this.maxSkillDistance = SkillDisVal[skillConf.dis]; - // 最近距离使用所有技能中的最小距离,不考虑MP限制,用于停止位置判断 - this.minSkillDistance = SkillDisVal[skillConf.dis]; + const maxRange = SkillDisVal[rangeType]; + let minRange = 0; + if (rangeType === SkillRange.Mid) { + minRange = SkillDisVal[SkillRange.Melee]; + } else if (rangeType === SkillRange.Long) { + minRange = SkillDisVal[SkillRange.Mid]; + } + this.maxSkillDistance = maxRange; + this.minSkillDistance = minRange; } /** diff --git a/assets/script/game/hero/HeroMove.ts b/assets/script/game/hero/HeroMove.ts index 07e6da45..dc77c0a5 100644 --- a/assets/script/game/hero/HeroMove.ts +++ b/assets/script/game/hero/HeroMove.ts @@ -110,11 +110,13 @@ export class HeroMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpd const currentX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); - const attackRange = 75; // 保持原有的近战判定 + const [minRange, maxRange] = this.resolveCombatRange(model, 0, 75); move.direction = enemyX > currentX ? 1 : -1; - if (dist <= attackRange) { + if (dist < minRange) { + this.performRetreat(view, move, model, currentX); + } else if (dist <= maxRange) { view.status_change("idle"); model.is_atking = true; } else { @@ -134,8 +136,7 @@ export class HeroMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpd const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); - const minRange = 120; - const maxRange = 360; + const [minRange, maxRange] = this.resolveCombatRange(model, 120, 360); move.direction = enemyX > currentX ? 1 : -1; @@ -164,8 +165,7 @@ export class HeroMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpd const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); - const minRange = 360; - const maxRange = 720; + const [minRange, maxRange] = this.resolveCombatRange(model, 360, 720); move.direction = enemyX > currentX ? 1 : -1; @@ -245,6 +245,14 @@ export class HeroMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpd } } + private resolveCombatRange(model: HeroAttrsComp, defaultMin: number, defaultMax: number): [number, number] { + const minRange = model.getCachedMinSkillDistance(); + const maxRange = model.getCachedMaxSkillDistance(); + if (maxRange <= 0) return [defaultMin, defaultMax]; + const safeMin = Math.max(0, Math.min(minRange, maxRange - 20)); + return [safeMin, maxRange]; + } + // --- 辅助方法 --- private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null { diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 9997547f..0a76a702 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -125,6 +125,7 @@ export class Monster extends ecs.Entity { // ✅ 初始化技能数据(迁移到 HeroSkillsComp) if(hero.skills[0]) model.atk_id=hero.skills[0] if(hero.skills[1]) model.skill_id=hero.skills[1] + model.updateSkillDistanceCache(model.skill_id || model.atk_id); this.add(view); @@ -178,4 +179,4 @@ export class MonLifecycleSystem extends ecs.ComblockSystem mLogger.log(this.debugMode, 'MonLifecycleSystem', `怪物离开世界: 实体ID ${e.eid}`); } } -} \ No newline at end of file +} diff --git a/assets/script/game/hero/MonMove.ts b/assets/script/game/hero/MonMove.ts index 7bfe0fd4..39f5afb0 100644 --- a/assets/script/game/hero/MonMove.ts +++ b/assets/script/game/hero/MonMove.ts @@ -127,11 +127,13 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda const currentX = view.node.position.x; const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); - const attackRange = 75; // 保持原有的近战判定 + const [minRange, maxRange] = this.resolveCombatRange(model, 0, 75); move.direction = enemyX > currentX ? 1 : -1; - if (dist <= attackRange) { + if (dist < minRange) { + this.performRetreat(view, move, model, currentX); + } else if (dist <= maxRange) { view.status_change("idle"); model.is_atking = true; } else { @@ -151,8 +153,7 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); - const minRange = 120; - const maxRange = 360; + const [minRange, maxRange] = this.resolveCombatRange(model, 120, 360); move.direction = enemyX > currentX ? 1 : -1; @@ -181,8 +182,7 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda const enemyX = enemy.node.position.x; const dist = Math.abs(currentX - enemyX); - const minRange = 360; - const maxRange = 720; + const [minRange, maxRange] = this.resolveCombatRange(model, 360, 720); move.direction = enemyX > currentX ? 1 : -1; @@ -263,6 +263,14 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda } } + private resolveCombatRange(model: HeroAttrsComp, defaultMin: number, defaultMax: number): [number, number] { + const minRange = model.getCachedMinSkillDistance(); + const maxRange = model.getCachedMaxSkillDistance(); + if (maxRange <= 0) return [defaultMin, defaultMax]; + const safeMin = Math.max(0, Math.min(minRange, maxRange - 20)); + return [safeMin, maxRange]; + } + /** 检查并设置y轴目标位置 */ private checkAndSetTargetY(entity: ecs.Entity): void { const move = entity.get(MonMoveComp); diff --git a/assets/script/game/hero/SCastSystem.ts b/assets/script/game/hero/SCastSystem.ts index de64d8fd..c66fb3b8 100644 --- a/assets/script/game/hero/SCastSystem.ts +++ b/assets/script/game/hero/SCastSystem.ts @@ -1,13 +1,11 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; -import { Vec3, v3 } from "cc"; +import { Vec3 } from "cc"; import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; -import { HSSet, SkillSet, SType, TGroup, SkillConfig } from "../common/config/SkillSet"; +import { BuffsList, SkillConfig, SkillSet, SType, TGroup } from "../common/config/SkillSet"; import { Skill } from "../skill/Skill"; import { smc } from "../common/SingletonModuleComp"; -import { BoxSet, FacSet } from "../common/config/GameSet"; import { GameConst } from "../common/config/GameConst"; -import { Attrs } from "../common/config/HeroAttrs"; import { mLogger } from "../common/Logger"; /** @@ -36,7 +34,124 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate update(e: ecs.Entity): void { if(!smc.mission.play ) return; if(smc.mission.pause) 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.isStun() || heroAttrs.isFrost()) return; + heroAttrs.updateCD(this.dt); + if (!heroAttrs.is_atking) return; + const castSkillId = this.pickCastSkill(heroAttrs, heroView); + if (castSkillId === 0) return; + this.castSkill(e, castSkillId, heroAttrs, heroView); } - -} \ No newline at end of file + + private pickCastSkill(heroAttrs: HeroAttrsComp, heroView: HeroViewComp): number { + const skillCandidates = [heroAttrs.skill_id, heroAttrs.atk_id]; + for (const s_uuid of skillCandidates) { + if (!s_uuid) continue; + const config = SkillSet[s_uuid]; + if (!config) continue; + const isMainSkill = s_uuid === heroAttrs.skill_id; + if (isMainSkill && !heroAttrs.can_skill) continue; + if (!isMainSkill && !heroAttrs.can_atk) continue; + const targets = this.findTargets(heroView, heroAttrs, config); + if (targets.length === 0) continue; + return s_uuid; + } + return 0; + } + + private castSkill(entity: ecs.Entity, s_uuid: number, heroAttrs: HeroAttrsComp, heroView: HeroViewComp) { + const config = SkillSet[s_uuid]; + if (!config) return; + const targets = this.findTargets(heroView, heroAttrs, config); + if (targets.length === 0) return; + heroView.playSkillEffect(s_uuid); + const isMainSkill = s_uuid === heroAttrs.skill_id; + const delay = GameConst.Battle.SKILL_CAST_DELAY; + heroView.scheduleOnce(() => { + if (!heroView.node || !heroView.node.isValid || heroAttrs.is_dead) return; + if (config.SType === SType.damage) { + this.createSkillEntity(s_uuid, heroView, targets[0].node.position); + } else { + this.applySupportSkill(entity, config, targets); + } + }, delay); + if (isMainSkill) { + heroAttrs.triggerSkillCD(); + } else { + heroAttrs.triggerAtkCD(); + } + } + + private createSkillEntity(s_uuid: number, caster: HeroViewComp, targetPos: Vec3) { + if (!caster.node || !caster.node.isValid) return; + const parent = caster.node.parent; + if (!parent) return; + const skill = ecs.getEntity(Skill); + skill.load(caster.node.position.clone(), parent, s_uuid, targetPos.clone(), caster, 0); + } + + private applySupportSkill(casterEntity: ecs.Entity, config: SkillConfig, targets: HeroViewComp[]) { + const casterAttrs = casterEntity.get(HeroAttrsComp); + if (!casterAttrs) return; + const ratio = Number(config.ap ?? 0) / 100; + for (const target of targets) { + if (!target.ent) continue; + const model = target.ent.get(HeroAttrsComp); + if (!model || model.is_dead) continue; + if (config.SType === SType.heal) { + const amount = model.hp_max * ratio; + model.add_hp(amount, true); + target.health(amount); + continue; + } + if (config.SType === SType.shield) { + const amount = model.hp_max * ratio; + model.shield_max = Math.max(model.shield_max, amount); + model.add_shield(amount, true); + continue; + } + if (config.SType === SType.buff) { + for (const buffId of config.buffs) { + const buffConf = BuffsList[buffId]; + if (buffConf) { + model.addBuff(buffConf); + } + } + } + } + mLogger.log(this.debugMode, "SCastSystem", `[SCastSystem] ${casterAttrs.hero_name} 施放 ${config.name}`); + } + + private findTargets(caster: HeroViewComp, casterAttrs: HeroAttrsComp, config: SkillConfig): HeroViewComp[] { + const range = Number(config.dis ?? GameConst.Battle.DEFAULT_SEARCH_RANGE); + const isEnemy = config.TGroup === TGroup.Enemy; + const isSelf = config.TGroup === TGroup.Self; + const isTeam = config.TGroup === TGroup.Team || config.TGroup === TGroup.Ally; + const isAll = config.TGroup === TGroup.All; + if (isSelf) return [caster]; + const currentPos = caster.node.position; + const list: { view: HeroViewComp; dis: number; lane: number }[] = []; + ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).forEach(ent => { + const targetAttrs = ent.get(HeroAttrsComp); + const targetView = ent.get(HeroViewComp); + if (!targetAttrs || !targetView || !targetView.node || targetAttrs.is_dead) return; + if (targetView === caster) return; + const isSameFac = targetAttrs.fac === casterAttrs.fac; + if (isEnemy && isSameFac) return; + if (isTeam && !isSameFac) return; + if (!isEnemy && !isTeam && !isAll) return; + const dis = Math.abs(currentPos.x - targetView.node.position.x); + if (dis > range) return; + const lane = Math.abs(currentPos.y - targetView.node.position.y); + list.push({ view: targetView, dis, lane }); + }); + list.sort((a, b) => { + if (a.lane !== b.lane) return a.lane - b.lane; + return a.dis - b.dis; + }); + const maxTargets = Math.max(GameConst.Skill.MIN_TARGET_COUNT, config.t_num || 1); + return list.slice(0, maxTargets).map(item => item.view); + } +} diff --git a/assets/script/game/skill/Skill.ts b/assets/script/game/skill/Skill.ts index f037d0c4..0e505dde 100644 --- a/assets/script/game/skill/Skill.ts +++ b/assets/script/game/skill/Skill.ts @@ -71,9 +71,11 @@ export class Skill extends ecs.Entity { mLogger.error(this.debugMode, 'Skill', "[Skill] 预制体加载失败:", path); return; } - const node: Node = instantiate(prefab); + const node: Node = Skill.getFromPool(path) || instantiate(prefab); + this.prefabPath = path; + this.skillNode = node; var scene = smc.map.MapView.scene; - node.parent = scene.entityLayer!.node!.getChildByName("SKILL")!; + node.parent = scene.entityLayer!.node!.getChildByName("SKILL") || parent; // 设置节点属性 let face=caster.node.scale.x < 0 ? -1 : 1 node.setScale(v3(node.scale.x*face,node.scale.y,1))