feat(battle): 重构技能施放与战斗距离系统

- 新增技能距离缓存机制,根据英雄类型动态计算最小和最大攻击范围
- 重构SCastSystem实现完整的技能施放逻辑,支持伤害、治疗、护盾和buff技能
- 在Hero和Monster初始化时调用updateSkillDistanceCache预计算技能距离
- 修改HeroMoveSystem和MonMoveSystem使用动态战斗范围,支持撤退逻辑
- 优化Skill实体创建,增加对象池支持
- 添加技能CD触发方法和状态检查方法
This commit is contained in:
walkpan
2026-03-12 09:13:28 +08:00
parent ce2cd05ba9
commit 5d09b3361e
7 changed files with 194 additions and 35 deletions

View File

@@ -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}`);
}
}
}
}

View File

@@ -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;
}
/**

View File

@@ -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 {

View File

@@ -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}`);
}
}
}
}

View File

@@ -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);

View File

@@ -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);
}
}
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);
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);
}
}