refactor(英雄技能): 重构技能系统以支持多技能和独立冷却

- 将 HeroAttrsComp 中的单一攻击/技能ID重构为技能数组,支持多个技能
- 为每个技能添加独立的冷却计时和最大冷却时间
- 修改 SCastSystem 以支持多技能选择和冷却检查
- 更新 HeroViewComp 显示当前展示技能的冷却进度
- 统一英雄和怪物初始化技能的方式,使用 setSkills 方法
- 移除 heroSet 配置中的 as/ss 字段,改为 cds 数组
- 修改 Skill 实体加载,传递 HeroAttrsComp 用于技能伤害计算
This commit is contained in:
walkpan
2026-03-22 13:35:25 +08:00
parent 218703be6b
commit be4884d28a
7 changed files with 134 additions and 113 deletions

View File

@@ -79,15 +79,9 @@ export class Hero extends ecs.Entity {
model.ap = hero.ap*model.lv;
model.hp= model.hp_max = hero.hp*model.lv;
model.speed = hero.speed;
model.a_cd_max=hero.as
model.s_cd_max=hero.ss
// 初始化技能信息数组
if(hero.skills[0]) model.atk_id=hero.skills[0]*model.lv
let s_lv=hero.skills[model.lv]?model.lv:1
if(hero.skills[s_lv]) {
model.skill_id=hero.skills[s_lv]
}
model.updateSkillDistanceCache(model.skill_id || model.atk_id);
model.setSkills(hero.skills, hero.cds);
model.skill_lvs=hero.slvs
model.updateSkillDistanceCache();
// 初始化 buff/debuff 系统
model.initAttrs();

View File

@@ -26,10 +26,10 @@ export class HeroAttrsComp extends ecs.Comp {
shield_max: number = 0; // 最大护盾值
// ==================== 攻击属性 (补充) ====================
a_cd: number = 0; // 攻击计时
s_cd: number = 0; // 技能计时
a_cd_max: number = 0; // 攻击CD
s_cd_max: number = 0; // 技能CD
skills: number[] = [];
skill_max_cds: Record<number, number> = {};
skill_cds: Record<number, number> = {};
skill_lvs:Record<number, number> = {};
// ==================== 特殊属性 ====================
critical: number = 0; // 暴击率
freeze_chance: number = 0; // 冰冻概率
@@ -69,10 +69,6 @@ export class HeroAttrsComp extends ecs.Comp {
atk_count: number = 0; // 攻击次数
atked_count: number = 0; // 被攻击次数
killed_count:number=0;
atk_id:number=0; //普通攻击技能id
skill_id:number=0; //技能攻击技能id
can_atk=false
can_skill=false
combat_target_eid: number = -1;
enemy_in_cast_range: boolean = false;
start(){
@@ -162,27 +158,74 @@ export class HeroAttrsComp extends ecs.Comp {
this.shield = Math.max(0, Math.min(this.shield + value, this.shield_max));
this.dirty_shield = true;
}
//======更新cd========//
updateCD(dt: number){
if(this.atk_id !=0&&!this.can_atk){
this.a_cd+=dt
if(this.a_cd >= this.a_cd_max) this.can_atk = true
}
if(this.skill_id !=0&&!this.can_skill){
this.s_cd+=dt
if(this.s_cd >= this.s_cd_max) this.can_skill = true
for (const skillId of this.skills) {
const maxCd = this.skill_max_cds[skillId] ?? 0;
if (maxCd <= 0) {
this.skill_cds[skillId] = 0;
continue;
}
const currentCd = this.skill_cds[skillId] ?? maxCd;
if (currentCd >= maxCd) {
this.skill_cds[skillId] = maxCd;
continue;
}
this.skill_cds[skillId] = Math.min(maxCd, currentCd + dt);
}
}
isFrost(): boolean {
return this.frost_end_time > 0
}
triggerAtkCD() {
this.a_cd = 0;
this.can_atk = false;
setSkills(skills: number[], cds: number[]) {
this.skills = [];
this.skill_max_cds = {};
this.skill_cds = {};
if (!skills) return;
const len = skills.length;
for (let i = 0; i < len; i++) {
const skillId = skills[i];
if (!skillId) continue;
const cd = cds[i] ?? cds[0] ?? 0;
const maxCd = Math.max(0, cd);
this.skills.push(skillId);
this.skill_max_cds[skillId] = maxCd;
this.skill_cds[skillId] = maxCd;
}
}
triggerSkillCD() {
this.s_cd = 0;
this.can_skill = false;
getSkillIds(): number[] {
return [...this.skills];
}
isSkillReady(skillId: number): boolean {
if (!skillId) return false;
const maxCd = this.skill_max_cds[skillId] ?? 0;
if (maxCd <= 0) return true;
const currentCd = this.skill_cds[skillId] ?? maxCd;
return currentCd >= maxCd;
}
triggerSkillCD(skillId: number) {
if (!skillId) return;
const maxCd = this.skill_max_cds[skillId] ?? 0;
if (maxCd <= 0) {
this.skill_cds[skillId] = 0;
return;
}
this.skill_cds[skillId] = 0;
}
getSkillCdProgress(skillId: number): number {
if (!skillId) return 1;
const maxCd = this.skill_max_cds[skillId] ?? 0;
if (maxCd <= 0) return 1;
const currentCd = this.skill_cds[skillId] ?? maxCd;
return Math.max(0, Math.min(1, currentCd / maxCd));
}
getDisplaySkillCdProgress(): number {
const displaySkillId = this.skills[1] ?? this.skills[0] ?? 0;
return this.getSkillCdProgress(displaySkillId);
}
@@ -194,8 +237,7 @@ export class HeroAttrsComp extends ecs.Comp {
* 在技能初始化、新增技能、MP变化时调用
* @param skillsComp 技能组件
*/
public updateSkillDistanceCache(skill_id:number): void {
void skill_id;
public updateSkillDistanceCache(): void {
const rangeType = this.type as HType.Melee | HType.Mid | HType.Long;
const maxRange = HeroDisVal[rangeType];
let minRange = 0;
@@ -240,10 +282,9 @@ export class HeroAttrsComp extends ecs.Comp {
this.shield_max = 0;
// 重置新增属性
this.a_cd = 0;
this.s_cd = 0;
this.a_cd_max = 0;
this.s_cd_max = 0;
this.skills = [];
this.skill_max_cds = {};
this.skill_cds = {};
this.critical = 0;
this.freeze_chance = 0;
this.back_chance = 0;
@@ -273,10 +314,6 @@ export class HeroAttrsComp extends ecs.Comp {
this.atk_count = 0;
this.atked_count = 0;
this.killed_count =0;
this.atk_id = 0;
this.skill_id = 0;
this.can_atk=false
this.can_skill=false
this.combat_target_eid = -1;
this.enemy_in_cast_range = false;
// 重置脏标签

View File

@@ -164,7 +164,7 @@ export class HeroViewComp extends CCComp {
}
public cd_show(){
this.top_node.getChildByName("cd").getComponent(ProgressBar).progress = this.model.s_cd/this.model.s_cd_max;
this.top_node.getChildByName("cd").getComponent(ProgressBar).progress = this.model.getDisplaySkillCdProgress();
}
/** 显示护盾 */

View File

@@ -141,12 +141,8 @@ export class Monster extends ecs.Entity {
if(!model.is_boss){
model.is_kalami = true;
}
model.a_cd_max=hero.as
model.s_cd_max=hero.ss
// ✅ 初始化技能数据(迁移到 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);
model.setSkills(hero.skills, hero.cds);
model.updateSkillDistanceCache();
//根据刷怪控制脚本对ap和hp进行加强

View File

@@ -56,15 +56,13 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const type = heroAttrs.type as HType;
const maxRange = this.resolveMaxCastRange(heroAttrs, type);
const target = this.findNearestEnemyInRange(heroAttrs, heroView, maxRange);
const skillCandidates = [heroAttrs.skill_id, heroAttrs.atk_id];
const skillCandidates = this.buildSkillCandidates(heroAttrs.getSkillIds());
const selfEid = heroView.ent?.eid;
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;
if (!heroAttrs.isSkillReady(s_uuid)) continue;
if (this.isSelfSkill(config.TGroup)) {
if (typeof selfEid !== "number") continue;
return { skillId: s_uuid, isFriendly: true, targetPos: null, targetEids: [selfEid] };
@@ -91,8 +89,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
heroView.playReady(config.readyAnm);
//播放角色攻击动画
heroView.playSkillAnm(config.act);
const isMainSkill = s_uuid === heroAttrs.skill_id;
// 优先使用技能配置的前摇时间,否则使用全局默认值
// 注意:这里仍然是基于时间的延迟,受帧率波动影响。
// 若需精确同步,建议在动画中添加帧事件并在 HeroViewComp 中监听。
@@ -103,32 +100,34 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if (castPlan.isFriendly) {
const friendlyTargets = this.resolveFriendlyTargets(castPlan.targetEids, heroAttrs.fac);
if (friendlyTargets.length === 0) return;
this.applyPrimaryEffect(s_uuid, config, heroView, friendlyTargets, null);
this.applyPrimaryEffect(s_uuid, config, heroView,heroAttrs, friendlyTargets, null);
this.applyExtraEffects(config, friendlyTargets);
return;
}
this.applyPrimaryEffect(s_uuid, config, heroView, [], castPlan.targetPos);
this.applyPrimaryEffect(s_uuid, config, heroView,heroAttrs, [], castPlan.targetPos);
}, delay);
if (isMainSkill) {
heroAttrs.triggerSkillCD();
} else {
heroAttrs.triggerAtkCD();
}
heroAttrs.triggerSkillCD(s_uuid);
}
private createSkillEntity(s_uuid: number, caster: HeroViewComp, targetPos: Vec3) {
private buildSkillCandidates(skillIds: number[]): number[] {
if (!skillIds || skillIds.length === 0) return [];
if (skillIds.length === 1) return [skillIds[0]];
return [...skillIds.slice(1), skillIds[0]];
}
private createSkillEntity(s_uuid: number, caster: HeroViewComp,cAttrsComp: HeroAttrsComp, 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);
skill.load(caster.node.position.clone(), parent, s_uuid, targetPos.clone(), caster,cAttrsComp, 0);
}
private applyPrimaryEffect(s_uuid: number, config: SkillConfig, heroView: HeroViewComp, targets: HeroViewComp[], targetPos: Vec3 | null) {
private applyPrimaryEffect(s_uuid: number, config: SkillConfig, heroView: HeroViewComp, cAttrsComp: HeroAttrsComp,targets: HeroViewComp[], targetPos: Vec3 | null) {
const kind = config.kind ?? SkillKind.Damage;
if (kind === SkillKind.Damage) {
if (config.ap <= 0 || !targetPos) return;
this.createSkillEntity(s_uuid, heroView, targetPos);
this.createSkillEntity(s_uuid, heroView,cAttrsComp, targetPos);
return;
}
for (const target of targets) {