feat(battle): 重构技能施放与战斗距离系统
- 新增技能距离缓存机制,根据英雄类型动态计算最小和最大攻击范围 - 重构SCastSystem实现完整的技能施放逻辑,支持伤害、治疗、护盾和buff技能 - 在Hero和Monster初始化时调用updateSkillDistanceCache预计算技能距离 - 修改HeroMoveSystem和MonMoveSystem使用动态战斗范围,支持撤退逻辑 - 优化Skill实体创建,增加对象池支持 - 添加技能CD触发方法和状态检查方法
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user