refactor(hero): 重构技能触发与属性系统,使用运行时解析缓存

1. 重构英雄属性组件,将静态配置与运行时缓存分离,新增runtime_前缀的运行时解析字段
2. 移除旧的evolve进化配置处理逻辑,改为通过等级解析器动态获取当前等级生效的技能与属性
3. 统一怪物与英雄的技能触发逻辑,均使用运行时缓存字段
4. 更新测试模式配置结构,适配新的分组+等级数组配置格式
5. 优化技能描述生成逻辑,支持新的等级配置结构
This commit is contained in:
pan
2026-08-04 16:46:59 +08:00
parent d0971ff72c
commit 57c7934571
10 changed files with 561 additions and 489 deletions

View File

@@ -1,6 +1,5 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { FacSet } from "../common/config/GameSet";
import { HeroInfo } from "../common/config/heroSet";
import { FieldSkillSet, FieldSkillType } from "../common/config/SkillSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { SkillBoxComp } from "../map/SkillBoxComp";
@@ -19,12 +18,11 @@ export class FieldSkillHelper {
let total = 0;
// 1. 统计英雄带来的驻场技能加成
// 读 model.field运行时优先使用 evolve 覆盖后的驻场配置,未升级时回退到 heroInfo 基础配置
// 读 model.runtime_field已按当前等级 resolve 的 uuid 列表)
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (!model || model.is_dead || model.fac !== FacSet.HERO) return;
const heroConfig = HeroInfo[model.hero_uuid];
const fields = model.field ?? heroConfig?.field;
const fields = model.runtime_field;
if (fields) {
for (const skillUuid of fields) {
const skillConfig = FieldSkillSet[skillUuid];

View File

@@ -5,7 +5,7 @@ import { smc } from "../common/SingletonModuleComp";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
import { HeroInfo, HeroPos, resolveFormationTargetX } from "../common/config/heroSet";
import { HeroInfo, HeroPos, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv } from "../common/config/heroSet";
import { GameEvent } from "../common/config/GameEvent";
import { SkillTriggerType } from "../common/config/heroSet";
import { SkillTriggerHelper } from "./SkillTriggerHelper";
@@ -126,7 +126,7 @@ export class Hero extends ecs.Entity {
smc.mission.heroGrid[posIndex] = this.eid;
}
// 复制触发技能配置
// 复制触发技能配置(新结构:按 s_uuid 分组 + lv 数组)
model.call = hero.call;
model.dead = hero.dead;
model.fstart = hero.fstart;
@@ -135,6 +135,8 @@ export class Hero extends ecs.Entity {
model.atked = hero.atked;
model.field = hero.field;
model.revive = hero.revive;
model.ap_bonus = hero.ap_bonus;
model.hp_bonus = hero.hp_bonus;
// 基础属性按等级倍率初始化
// 使用指数增长公式:等级 2 时为原来的 HERO_LV_MULTIPLIER 倍,等级 3 时为 HERO_LV_MULTIPLIER^2 倍
@@ -164,38 +166,26 @@ export class Hero extends ecs.Entity {
model.skills[skill.uuid] = { ...skill, lv: Math.max(0, skill.lv + hero_lv - 2), ccd: 0 };
}
// 应用等级进化配置(从 lv2 开始逐级覆盖)
if (hero.evolve) {
// 记录 evolve 配置快照(只读引用),供 UI 计算 ap/hp 额外加成文案
model.evolve = hero.evolve;
for (let elv = 2; elv <= hero_lv; elv++) {
const evo = hero.evolve[elv];
if (!evo) continue;
// 替换普攻技能
if (evo.skill) {
const oldSkill = Object.values(model.skills)[0];
model.skills = {};
model.skills[evo.skill.s_uuid] = {
uuid: evo.skill.s_uuid,
lv: Math.max(0, 1 + hero_lv - 2),
cd: evo.skill.cd ?? oldSkill?.cd ?? 1,
ccd: 0,
overrides: evo.skill.overrides,
};
}
// 覆盖触发技能
if (evo.atking) model.atking = evo.atking;
if (evo.atked) model.atked = evo.atked;
if (evo.dead) model.dead = evo.dead;
if (evo.fstart) model.fstart = evo.fstart;
if (evo.fend) model.fend = evo.fend;
// 覆盖驻场光环(存活期间全局生效)
if (evo.field) model.field = evo.field;
if (evo.revive) model.revive = evo.revive;
// 额外属性加成
if (evo.ap_bonus) model.ap += evo.ap_bonus;
if (evo.hp_bonus) { model.hp += evo.hp_bonus; model.hp_max += evo.hp_bonus; }
}
// 按当前英雄等级 resolve 触发技能/光环/复活到运行时缓存,供 SkillTriggerHelper 消费
model.runtime_call = resolveTriggerByLv(model.call, hero_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, hero_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, hero_lv);
model.runtime_fend = resolveTriggerByLv(model.fend, hero_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, hero_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, hero_lv);
model.runtime_field = resolveFieldByLv(model.field, hero_lv);
model.runtime_revive = resolveReviveByLv(model.revive, hero_lv);
// 应用额外属性加成hp_bonus/ap_bonus 按 ≤英雄等级 累加)
if (model.hp_bonus) {
let hpExtra = 0;
for (const e of model.hp_bonus) { if (e.lv <= hero_lv) hpExtra += e.value; }
model.hp += hpExtra; model.hp_max += hpExtra;
}
if (model.ap_bonus) {
let apExtra = 0;
for (const e of model.ap_bonus) { if (e.lv <= hero_lv) apExtra += e.value; }
model.ap += apExtra;
}
// 缓存技能射程等派生数据,减少战斗帧内重复计算

View File

@@ -228,8 +228,8 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
let maxReviveCount = 0;
let reviveHpPercent = 50; // 默认恢复50%
let reviveSkillConf: any = null; // 提取出来的复活技能配置
if (TAttrsComp.revive) {
const reviveConf = TAttrsComp.revive;
if (TAttrsComp.runtime_revive) {
const reviveConf = TAttrsComp.runtime_revive;
maxReviveCount = reviveConf.r_num + Math.floor((TAttrsComp.lv - 1) * reviveConf.upr);
if (TAttrsComp.revived_count < maxReviveCount) {
canRevive = true;

View File

@@ -1,5 +1,5 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType, HeroEvolve } from "../common/config/heroSet";
import { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType, TriggerGrouped, LvFieldEntry, LvReviveEntry, LvBonusEntry } from "../common/config/heroSet";
import { mLogger } from "../common/Logger";
import { FacSet, FightSet } from "../common/config/GameSet";
import { FieldSkillSet, FieldSkillType, SkillOverrides } from "../common/config/SkillSet";
@@ -41,19 +41,37 @@ export class HeroAttrsComp extends ecs.Comp {
// ==================== 攻击属性 (补充) ====================
skills: Record<number, HSkillInfo> = {};
// ==================== 触发类技能 ====================
[SkillTriggerType.Call]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Dead]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.FStart]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.FEnd]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Atking]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
[SkillTriggerType.Atked]?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 驻场光环(FieldSkillSet uuid 列表,随 evolve 覆盖,存活期间全局生效) */
[SkillTriggerType.Field]?: number[];
[SkillTriggerType.Revive]?: { s_uuid: number, r_num: number, upr: number };
// ==================== 触发类技能(静态配置,按 s_uuid 分组 + lv 数组) ====================
[SkillTriggerType.Call]?: TriggerGrouped;
[SkillTriggerType.Dead]?: TriggerGrouped;
[SkillTriggerType.FStart]?: TriggerGrouped;
[SkillTriggerType.FEnd]?: TriggerGrouped;
[SkillTriggerType.Atking]?: TriggerGrouped;
[SkillTriggerType.Atked]?: TriggerGrouped;
/** 驻场光环(按 lv 解锁,存活期间全局生效) */
[SkillTriggerType.Field]?: LvFieldEntry[];
[SkillTriggerType.Revive]?: LvReviveEntry[];
/** 额外攻击/生命加成(按 lv 累加) */
ap_bonus?: LvBonusEntry[];
hp_bonus?: LvBonusEntry[];
/** 等级进化配置快照(引用 heroInfo.evolve只读供 UI 计算 ap/hp 额外加成文案) */
evolve?: Record<number, HeroEvolve>;
// ==================== 运行时解析缓存(按当前等级 resolve 后的有效列表) ====================
/** 当前等级下生效的攻击触发条目(供 SkillTriggerHelper 消费) */
runtime_atking?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的受击触发条目 */
runtime_atked?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的召唤触发条目 */
runtime_call?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的死亡触发条目 */
runtime_dead?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的战前触发条目 */
runtime_fstart?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的战后触发条目 */
runtime_fend?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
/** 当前等级下生效的驻场光环 uuid 列表 */
runtime_field?: number[];
/** 当前等级下生效的复活配置 */
runtime_revive?: { s_uuid: number; r_num: number; upr: number };
// ==================== 特殊属性 ====================
critical: number = 0; // 暴击率
@@ -547,8 +565,18 @@ export class HeroAttrsComp extends ecs.Comp {
this.fend = undefined;
this.atking = undefined;
this.atked = undefined;
this.field = undefined;
this.revive = undefined;
this.evolve = undefined;
this.ap_bonus = undefined;
this.hp_bonus = undefined;
this.runtime_atking = undefined;
this.runtime_atked = undefined;
this.runtime_call = undefined;
this.runtime_dead = undefined;
this.runtime_fstart = undefined;
this.runtime_fend = undefined;
this.runtime_field = undefined;
this.runtime_revive = undefined;
this.critical = 0;
this.critical_res = 0;
this.freeze_chance = 0;

View File

@@ -489,7 +489,7 @@ export class HeroViewComp extends CCComp {
}
// 如果角色带有死亡触发技能,则由 SCastSystem 播放 playReady("dead")
if (this.model && this.model.dead && this.model.dead.length > 0) {
if (this.model && this.model.runtime_dead && this.model.runtime_dead.length > 0) {
// SCastSystem will handle the "dead" ready animation
} else {
// 播放默认死亡特效

View File

@@ -1,9 +1,9 @@
import { instantiate, Node, Prefab, Vec3 ,v3,resources,SpriteFrame,Sprite,SpriteAtlas, BoxCollider2D, NodePool, tween, Tween} from "cc";
import { instantiate, Node, Prefab, Vec3, v3, resources, SpriteFrame, Sprite, SpriteAtlas, BoxCollider2D, NodePool, tween, Tween } from "cc";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { smc } from "../common/SingletonModuleComp";
import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
import { HeroInfo } from "../common/config/heroSet";
import { HeroInfo, resolveTriggerByLv, resolveReviveByLv } from "../common/config/heroSet";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { HeroViewComp } from "./HeroViewComp";
import { MoveComp } from "./MoveComp";
@@ -109,10 +109,10 @@ export class Monster extends ecs.Entity {
const model = this.get(HeroAttrsComp);
const view = this.get(HeroViewComp);
if (model && view && view.node && view.node.isValid) {
const path = "game/heros/" + HeroInfo[model.hero_uuid].path;
Monster.putToPool(path, view.node);
const path = "game/heros/" + HeroInfo[model.hero_uuid].path;
Monster.putToPool(path, view.node);
}
if (model && model.posIndex >= 0) {
if (smc.mission.monGrid[model.posIndex] === this.eid) {
smc.mission.monGrid[model.posIndex] = -1;
@@ -131,15 +131,15 @@ export class Monster extends ecs.Entity {
* 2) 初始化表现、属性、技能与阵营
* 3) 播放下落入场并在落地后启用碰撞与移动
*/
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1, laneIndex:number = 0) {
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, is_boss: boolean = false, dropToY: number = pos.y, mon_lv: number = 1, laneIndex: number = 0) {
// 怪物默认朝左,表现缩放固定为负向
scale=-1
scale = -1
// 当前怪物尺寸固定,保留变量便于后续扩展
let size=1.15
let size = 1.15
var scene = smc.map.MapView.scene;
// 根据配置读取怪物预制体路径
var path = "game/heros/"+HeroInfo[uuid].path;
var path = "game/heros/" + HeroInfo[uuid].path;
// 优先从对象池取节点,未命中时再实例化
let node = Monster.getFromPool(path);
if (!node) {
@@ -157,7 +157,7 @@ export class Monster extends ecs.Entity {
if (collider) {
collider.enabled = false;
}
let baseScale = (node as any)._baseScale || v3(1, 1, 1);
node.setScale(size * baseScale.x, size * baseScale.y);
node.setPosition(pos)
@@ -191,8 +191,8 @@ export class Monster extends ecs.Entity {
model.revive = hero.revive;
// 标记是否 Boss非 Boss 默认记作杂兵
model.is_boss =is_boss
if(!model.is_boss){
model.is_boss = is_boss
if (!model.is_boss) {
model.is_kalami = true;
}
@@ -202,7 +202,7 @@ export class Monster extends ecs.Entity {
const skill = hero.skills[key];
if (!skill) continue;
// 最终技能等级 = 初始技能等级 + 怪物等级增量,且下限为 0
model.skills[skill.uuid] = { ...skill, lv: Math.max(0,skill.lv + mon_lv - 2), ccd: 0 };
model.skills[skill.uuid] = { ...skill, lv: Math.max(0, skill.lv + mon_lv - 2), ccd: 0 };
}
// 【测试模式专属】如果有覆盖技能配置则应用
const testSkills = (this as any)._testSkills;
@@ -227,15 +227,24 @@ export class Monster extends ecs.Entity {
if (testSkills.fend !== undefined) model.fend = testSkills.fend;
}
// 按怪物等级 resolve 触发技能/复活到运行时缓存(怪物无 lv 成长,恒用 mon_lv
model.runtime_call = resolveTriggerByLv(model.call, mon_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, mon_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, mon_lv);
model.runtime_fend = resolveTriggerByLv(model.fend, mon_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, mon_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, mon_lv);
model.runtime_revive = resolveReviveByLv(model.revive, mon_lv);
// 缓存技能射程等派生数据,减少战斗帧内重复计算
model.updateSkillDistanceCache();
// 注册视图组件并重置对象池复用状态
this.add(view);
view.init();
view.as.idle();
// 广播怪物加载事件,供刷怪与战斗系统联动
oops.message.dispatchEvent("monster_load",this)
oops.message.dispatchEvent("monster_load", this)
// 初始化移动参数:方向、目标 X、站位基准 Y
const move = this.get(MonMoveComp);
move.reset();
@@ -246,7 +255,7 @@ export class Monster extends ecs.Entity {
// 依据下落距离自适应入场时长,确保观感一致
const dropDistance = Math.abs(pos.y - dropToY);
const onDropComplete = () => {
if (!node || !node.isValid) return;
// 落地后锁定最终位置,切换到落地完成状态
@@ -271,7 +280,7 @@ export class Monster extends ecs.Entity {
// 停止旧动画后执行下落 tween避免复用节点时动画叠加
Tween.stopAllByTarget(node);
if (dropDistance > 0) {
const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200));
tween(node)
@@ -287,10 +296,10 @@ export class Monster extends ecs.Entity {
smc.vmdata.mission_data.mon_num++
}
/** 重置入口:复用 destroy 的释放流程 */
reset() {
super.destroy();
}
}

View File

@@ -42,10 +42,10 @@ export class SkillTriggerHelper {
this.handleDead(model, view);
break;
case SkillTriggerType.FStart:
this.handleArrayTrigger(model.fstart, model, view, type);
this.handleArrayTrigger(model.runtime_fstart, model, view, type);
break;
case SkillTriggerType.FEnd:
this.handleArrayTrigger(model.fend, model, view, type);
this.handleArrayTrigger(model.runtime_fend, model, view, type);
break;
case SkillTriggerType.Atking:
this.handleAtking(model, view);
@@ -61,17 +61,17 @@ export class SkillTriggerHelper {
* 支持受【召唤强化羁绊 (SummonCount)】的多次触发加成。
*/
private static handleCall(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.call || model.call.length === 0) return;
if (!model.runtime_call || model.runtime_call.length === 0) return;
let triggerCount = 1;
// 仅英雄享受加成,怪物始终只触发 1 次
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.SummonCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount)); // 确保最少触发 1 次
for (let i = 0; i < triggerCount; i++) {
this.dispatchArray(model.call, model, view, SkillTriggerType.Call);
this.dispatchArray(model.runtime_call, model, view, SkillTriggerType.Call);
}
}
@@ -80,18 +80,18 @@ export class SkillTriggerHelper {
* 支持受【亡语强化羁绊 (DeadCount)】的多次触发加成。
*/
private static handleDead(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.dead || model.dead.length === 0) return;
if (!model.runtime_dead || model.runtime_dead.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.DeadCount);
// 【局内战绩评分系统】统计死亡触发技能生效次数(用于局后防守评分结算)
smc.vmdata.scores.dead_trigger_count += model.dead.length;
smc.vmdata.scores.dead_trigger_count += model.runtime_dead.length;
}
triggerCount = Math.max(1, Math.floor(triggerCount));
for (let i = 0; i < triggerCount; i++) {
this.dispatchArray(model.dead, model, view, SkillTriggerType.Dead);
this.dispatchArray(model.runtime_dead, model, view, SkillTriggerType.Dead);
}
}
@@ -100,15 +100,15 @@ export class SkillTriggerHelper {
* 判定逻辑:依据配置中的 t_num触发所需普攻次数对比当前的普攻累计次数取模判定。
*/
private static handleAtking(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.atking || model.atking.length === 0) return;
if (!model.runtime_atking || model.runtime_atking.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.AtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
model.atking.forEach(atkConfig => {
model.runtime_atking.forEach(atkConfig => {
// atk_count 代表已进行的普攻次数。当其余数刚好整除配置阈值时触发。
if (model.atk_count > 0 && model.atk_count % atkConfig.t_num === 0) {
for (let i = 0; i < triggerCount; i++) {
@@ -123,15 +123,15 @@ export class SkillTriggerHelper {
* 判定逻辑:依据配置中的 t_num触发所需受击次数对比当前的受击累计次数取模判定。
*/
private static handleAtked(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.atked || model.atked.length === 0) return;
if (!model.runtime_atked || model.runtime_atked.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.BeAtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
model.atked.forEach(atkConfig => {
model.runtime_atked.forEach(atkConfig => {
// atked_count 代表已承受的受击次数。当其余数刚好整除配置阈值时触发。
if (model.atked_count > 0 && model.atked_count % atkConfig.t_num === 0) {
for (let i = 0; i < triggerCount; i++) {
@@ -144,7 +144,7 @@ export class SkillTriggerHelper {
/**
* 通用的数组型触发器处理(适用于 FStart / FEnd 等无需额外判定的简单列表触发)
*/
private static handleArrayTrigger(configs: {s_uuid: number, t_num: number, overrides?: SkillOverrides}[] | undefined, model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType) {
private static handleArrayTrigger(configs: { s_uuid: number, t_num: number, overrides?: SkillOverrides }[] | undefined, model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType) {
if (!configs || configs.length === 0) return;
this.dispatchArray(configs, model, view, type);
}
@@ -152,7 +152,7 @@ export class SkillTriggerHelper {
/**
* 批量派发技能事件
*/
private static dispatchArray(configs: {s_uuid: number, t_num?: number, overrides?: SkillOverrides}[], model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType) {
private static dispatchArray(configs: { s_uuid: number, t_num?: number, overrides?: SkillOverrides }[], model: HeroAttrsComp, view: HeroViewComp, type: SkillTriggerType) {
configs.forEach(config => this.dispatchSingle(config.s_uuid, model, view, type, config.overrides));
}