refactor(hero): 重构额外属性加成系统,统一为bonus字段
1. 将原ap_bonus/hp_bonus/def_bonus三个字段合并为通用的bonus数组结构 2. 新增多属性同时配置能力,支持hp/ap/def百分比加成与crt/frz/stun/para固定百分点加成 3. 更新所有前端编辑器、服务端渲染与游戏运行时逻辑适配新配置格式 4. 添加数据兼容迁移逻辑,自动处理旧版英雄配置数据
This commit is contained in:
@@ -417,9 +417,7 @@ export interface heroInfo {
|
||||
dodge?: number; // 闪避率(百分点,缺省由初始化处兜底为全局默认值)
|
||||
accuracy?: number; // 命中率(百分点,缺省由初始化处兜底为全局默认值)
|
||||
skills: Record<number, HSkillInfo>; // 携带技能ID列表
|
||||
ap_bonus?: LvBonusEntry[]; // 额外攻击加成(按 lv 升序,运行时累加 ≤英雄等级 的所有档)
|
||||
hp_bonus?: LvBonusEntry[]; // 额外生命加成(按 lv 升序,运行时累加 ≤英雄等级 的所有档)
|
||||
def_bonus?: LvBonusEntry[]; // 额外防御加成(按 lv 升序,运行时累加 ≤英雄等级 的所有档)
|
||||
bonus?: LvBonusAttrsEntry[]; // 额外属性加成(按 lv 升序,运行时累加所有 lv ≤ 英雄等级 的档位 attrs;hp/ap/def 按"基础×品质+成长"快照百分比加成,crt/frz/stun/para 为固定百分点累加)
|
||||
info: string; // 描述文案
|
||||
/**
|
||||
* @deprecated 已废弃:无任何 UI/运行时读取。技能提升说明由 HeroSkillText 依据
|
||||
@@ -526,11 +524,15 @@ export interface LvFieldEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 额外属性加成按等级叠加:运行时累加所有 ≤英雄等级 的档位。
|
||||
* 额外属性加成按等级叠加:运行时累加所有 ≤英雄等级 的档位 attrs。
|
||||
* 口径约定:hp/ap/def 为百分比(基数取"基础×品质+成长"快照,多档同键加法叠加不滚雪球);
|
||||
* crt/frz/stun/para 为固定百分点(直接累加到对应概率属性)。
|
||||
*/
|
||||
export interface LvBonusEntry {
|
||||
export type BonusAttrKey = 'hp' | 'ap' | 'def' | 'crt' | 'frz' | 'stun' | 'para';
|
||||
|
||||
export interface LvBonusAttrsEntry {
|
||||
lv: number;
|
||||
value: number;
|
||||
attrs: Partial<Record<BonusAttrKey, number>>;
|
||||
}
|
||||
/*
|
||||
*=============英雄配置列表================
|
||||
@@ -1089,7 +1091,7 @@ export function resolveFieldByLv(field: LvFieldEntry[] | undefined, lv: number):
|
||||
return pick ? pick.uuids : [];
|
||||
}
|
||||
|
||||
/** 计算目标等级下额外属性加成(hp/ap/def_bonus 一次性奖励 + 成长类型每级递增) */
|
||||
/** 计算目标等级下额外属性加成(成长类型每级递增 + bonus 档位累加;bonus 中 hp/ap/def 为百分比,基数取"基础+成长"快照,多档加法叠加) */
|
||||
function getEvolveBonuses(hero: heroInfo, targetLv: number): { hp: number, ap: number, def: number } {
|
||||
const lv = Math.max(1, targetLv);
|
||||
// 成长类型线性增量(仅英雄生效,怪物无 grow_type 时走 Warrior 基准但不影响平衡,因怪物通常不填)
|
||||
@@ -1097,20 +1099,22 @@ function getEvolveBonuses(hero: heroInfo, targetLv: number): { hp: number, ap: n
|
||||
let hp = grow.hp;
|
||||
let ap = grow.ap;
|
||||
let def = grow.def;
|
||||
if (hero.hp_bonus) {
|
||||
for (const e of hero.hp_bonus) {
|
||||
if (e.lv <= lv) hp += e.value;
|
||||
}
|
||||
}
|
||||
if (hero.ap_bonus) {
|
||||
for (const e of hero.ap_bonus) {
|
||||
if (e.lv <= lv) ap += e.value;
|
||||
}
|
||||
}
|
||||
if (hero.def_bonus) {
|
||||
for (const e of hero.def_bonus) {
|
||||
if (e.lv <= lv) def += e.value;
|
||||
if (hero.bonus) {
|
||||
// 百分比基数:基础值 + 成长快照(不含 bonus 自身,避免滚雪球)
|
||||
const snapHp = hero.hp + grow.hp;
|
||||
const snapAp = hero.ap + grow.ap;
|
||||
const snapDef = hero.def + grow.def;
|
||||
let hpP = 0, apP = 0, defP = 0;
|
||||
for (const e of hero.bonus) {
|
||||
if (e.lv > lv) continue;
|
||||
hpP += e.attrs.hp ?? 0;
|
||||
apP += e.attrs.ap ?? 0;
|
||||
defP += e.attrs.def ?? 0;
|
||||
// crt/frz/stun/para 为固定百分点,由运行时直接累加到概率属性,此处不结算
|
||||
}
|
||||
hp += Math.round(snapHp * hpP / 100);
|
||||
ap += Math.round(snapAp * apP / 100);
|
||||
def += Math.round(snapDef * defP / 100);
|
||||
}
|
||||
return { hp, ap, def };
|
||||
}
|
||||
|
||||
@@ -137,9 +137,7 @@ export class Hero extends ecs.Entity {
|
||||
model.atking = hero.atking;
|
||||
model.atked = hero.atked;
|
||||
model.field = hero.field;
|
||||
model.ap_bonus = hero.ap_bonus;
|
||||
model.hp_bonus = hero.hp_bonus;
|
||||
model.def_bonus = hero.def_bonus;
|
||||
model.bonus = hero.bonus;
|
||||
|
||||
// 基础属性:固定基础值 × 品质系数(相邻品质差 5%),成长由 grow_type 线性递增提供
|
||||
// TODO: 局外升级机制落地后,此处基础值将与存档强化累加重算
|
||||
@@ -184,26 +182,30 @@ export class Hero extends ecs.Entity {
|
||||
model.runtime_field = resolveFieldByLv(model.field, hero_lv);
|
||||
// 复活已纳入 dead 触发组,无需单独解析 revive
|
||||
|
||||
// 应用额外属性加成(成长类型每级递增 + hp/ap/def_bonus 一次性奖励累加)
|
||||
// 应用额外属性加成(成长类型每级递增 + bonus 档位累加)
|
||||
// 成长增量:仅英雄生效(model.fac 已为 HERO),按 grow_type 递增曲线累计
|
||||
const grow = calcGrowBonus(hero, hero_lv);
|
||||
model.hp += grow.hp; model.hp_max += grow.hp;
|
||||
model.ap += grow.ap;
|
||||
model.defense += grow.def;
|
||||
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;
|
||||
}
|
||||
if (model.def_bonus) {
|
||||
let defExtra = 0;
|
||||
for (const e of model.def_bonus) { if (e.lv <= hero_lv) defExtra += e.value; }
|
||||
model.defense += defExtra;
|
||||
if (model.bonus) {
|
||||
// 百分比基数:基础×品质 + 成长快照(不含 bonus 自身,多档加法叠加不滚雪球)
|
||||
const snapHp = model.hp_max, snapAp = model.ap, snapDef = model.defense;
|
||||
let hpP = 0, apP = 0, defP = 0;
|
||||
for (const e of model.bonus) {
|
||||
if (e.lv > hero_lv) continue;
|
||||
const a = e.attrs;
|
||||
hpP += a.hp ?? 0; apP += a.ap ?? 0; defP += a.def ?? 0;
|
||||
// 概率类:固定百分点直接累加
|
||||
model.critical += a.crt ?? 0;
|
||||
model.freeze_chance += a.frz ?? 0;
|
||||
model.stun_chance += a.stun ?? 0;
|
||||
model.paralyze_chance += a.para ?? 0;
|
||||
}
|
||||
const hpAdd = Math.round(snapHp * hpP / 100);
|
||||
model.hp += hpAdd; model.hp_max += hpAdd;
|
||||
model.ap += Math.round(snapAp * apP / 100);
|
||||
model.defense += Math.round(snapDef * defP / 100);
|
||||
}
|
||||
|
||||
// lv12 职业属性专项:达到 12 级时按职业对单项基础属性 +50%
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||||
import { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType, TriggerGrouped, LvFieldEntry, LvBonusEntry, FieldEntry, HRole } from "../common/config/heroSet";
|
||||
import { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType, TriggerGrouped, LvFieldEntry, LvBonusAttrsEntry, FieldEntry, HRole } from "../common/config/heroSet";
|
||||
import { mLogger } from "../common/Logger";
|
||||
import { FacSet, FightSet } from "../common/config/GameSet";
|
||||
import { FieldSkillSet, FieldSkillType, SkillOverrides } from "../common/config/SkillSet";
|
||||
@@ -53,10 +53,8 @@ export class HeroAttrsComp extends ecs.Comp {
|
||||
[SkillTriggerType.Atked]?: TriggerGrouped;
|
||||
/** 驻场光环(按 lv 解锁,存活期间全局生效) */
|
||||
[SkillTriggerType.Field]?: LvFieldEntry[];
|
||||
/** 额外攻击/生命/防御加成(按 lv 累加) */
|
||||
ap_bonus?: LvBonusEntry[];
|
||||
hp_bonus?: LvBonusEntry[];
|
||||
def_bonus?: LvBonusEntry[];
|
||||
/** 额外属性加成(按 lv 累加:hp/ap/def 百分比、 crt/frz/stun/para 固定百分点) */
|
||||
bonus?: LvBonusAttrsEntry[];
|
||||
|
||||
// ==================== 运行时解析缓存(按当前等级 resolve 后的有效列表) ====================
|
||||
/** 当前等级下生效的攻击触发条目(供 SkillTriggerHelper 消费) */
|
||||
@@ -704,9 +702,7 @@ export class HeroAttrsComp extends ecs.Comp {
|
||||
this.atking = undefined;
|
||||
this.atked = undefined;
|
||||
this.field = undefined;
|
||||
this.ap_bonus = undefined;
|
||||
this.hp_bonus = undefined;
|
||||
this.def_bonus = undefined;
|
||||
this.bonus = undefined;
|
||||
this.runtime_atking = undefined;
|
||||
this.runtime_atked = undefined;
|
||||
this.runtime_call = undefined;
|
||||
|
||||
@@ -215,8 +215,8 @@ export class HeroBoxComp extends CCComp {
|
||||
// ---- 属性成长(固定基础值 + 成长类型指数递增 + 一次性加成) ----
|
||||
const attrLine = (lv: number): string => {
|
||||
const grow = calcGrowBonus(hero, lv);
|
||||
const ap = Math.floor(hero.ap + grow.ap + HeroBoxComp.sumBonus(hero.ap_bonus, lv));
|
||||
const hp = Math.floor(hero.hp + grow.hp + HeroBoxComp.sumBonus(hero.hp_bonus, lv));
|
||||
const ap = Math.floor(hero.ap + grow.ap + HeroBoxComp.sumBonusPct(hero.ap + grow.ap, hero.bonus, 'ap', lv));
|
||||
const hp = Math.floor(hero.hp + grow.hp + HeroBoxComp.sumBonusPct(hero.hp + grow.hp, hero.bonus, 'hp', lv));
|
||||
return `攻击 <color=#27AE60>${ap}</color> 生命 <color=#27AE60>${hp}</color>`;
|
||||
};
|
||||
|
||||
@@ -237,11 +237,12 @@ export class HeroBoxComp extends CCComp {
|
||||
}
|
||||
|
||||
/** 累加等级额外加成(≤目标等级的所有档位) */
|
||||
private static sumBonus(entries: { lv: number; value: number }[] | undefined, lv: number): number {
|
||||
/** 累加 bonus 中某百分比键的档位总量,并按"基础+成长"快照换算为固定增量(≤目标等级的所有档位) */
|
||||
private static sumBonusPct(snap: number, entries: HeroInfo[number]['bonus'], key: 'hp' | 'ap' | 'def', lv: number): number {
|
||||
if (!entries) return 0;
|
||||
let total = 0;
|
||||
for (const e of entries) { if (e.lv <= lv) total += e.value; }
|
||||
return total;
|
||||
let pct = 0;
|
||||
for (const e of entries) { if (e.lv <= lv) pct += e.attrs[key] ?? 0; }
|
||||
return Math.round(snap * pct / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,18 +544,18 @@ export class HeroBoxComp extends CCComp {
|
||||
}
|
||||
|
||||
// ---- AP / HP ----
|
||||
// plus(附加值)= 最终值 - 当前等级基础值;基础值 = 1 级原始 base + 等级成长 + ap/hp_bonus 一次性奖励。
|
||||
// plus(附加值)= 最终值 - 当前等级基础值;基础值 = 1 级原始 base + 等级成长 + bonus 百分比加成。
|
||||
// Why: base_ap/base_hp 是不含任何等级增长的原始值,直接用 final-base 会把升级带来的
|
||||
// 属性增长也计入增量。这里补齐等级增长作为基准,使 plus 仅反映 buff/光环等临时增益。
|
||||
const lv = Math.max(1, Math.floor(this.model.lv ?? 1));
|
||||
const grow = hero ? calcGrowBonus(hero, lv) : { ap: 0, hp: 0 };
|
||||
|
||||
const finalAp = Math.max(0, Math.floor(this.model.getFinalAp()));
|
||||
const lvBaseAp = Math.max(0, Math.floor((this.model.base_ap ?? 0) + grow.ap + HeroBoxComp.sumBonus(hero?.ap_bonus, lv)));
|
||||
const lvBaseAp = Math.max(0, Math.floor((this.model.base_ap ?? 0) + grow.ap + HeroBoxComp.sumBonusPct((this.model.base_ap ?? 0) + grow.ap, hero?.bonus, 'ap', lv)));
|
||||
this.setStatRow(this.ap_all_label, this.ap_plus_label, finalAp, finalAp - lvBaseAp);
|
||||
|
||||
const finalHp = Math.max(0, Math.floor(this.model.getFinalHpMax()));
|
||||
const lvBaseHp = Math.max(0, Math.floor((this.model.base_hp ?? 0) + grow.hp + HeroBoxComp.sumBonus(hero?.hp_bonus, lv)));
|
||||
const lvBaseHp = Math.max(0, Math.floor((this.model.base_hp ?? 0) + grow.hp + HeroBoxComp.sumBonusPct((this.model.base_hp ?? 0) + grow.hp, hero?.bonus, 'hp', lv)));
|
||||
this.setStatRow(this.hp_all_label, this.hp_plus_label, finalHp, finalHp - lvBaseHp);
|
||||
|
||||
// ---- 图标(UUID 变化时重新加载首帧动画) ----
|
||||
|
||||
@@ -2142,7 +2142,7 @@ export class MissionCardComp extends CCComp {
|
||||
* 1. 同步刷新 base_ap/base_hp 快照,下游 UI/复活读到当前 lv 的基础值
|
||||
* 2. 走 getRuntimeAp/getRuntimeHp 应用驻场光环乘区
|
||||
* 3. 重新解析 runtime_* 触发缓存与光环/复活,避免升级后停在旧等级快照
|
||||
* 4. 按 ≤nextLv 累加 hp_bonus/ap_bonus
|
||||
* 4. 按 ≤nextLv 累加 bonus 档位(hp/ap/def 百分比 + crt/frz/stun/para 百分点)
|
||||
*/
|
||||
public applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
|
||||
const hero = HeroInfo[model.hero_uuid];
|
||||
@@ -2167,28 +2167,38 @@ export class MissionCardComp extends CCComp {
|
||||
model.hp_max = base_hp;
|
||||
}
|
||||
|
||||
// 按 ≤nextLv 累加:成长类型每级递增 + hp/ap/def_bonus 一次性奖励(沿用 Hero.load 的加法区规则)
|
||||
// 按 ≤nextLv 累加:成长类型每级递增 + bonus 档位(与 Hero.load 同口径:hp/ap 百分比、基数取"基础+成长"快照)
|
||||
const grow = calcGrowBonus(hero, nextLv);
|
||||
model.hp_max += grow.hp;
|
||||
model.ap += grow.ap;
|
||||
if (model.hp_bonus) {
|
||||
let hpExtra = 0;
|
||||
for (const e of model.hp_bonus) { if (e.lv <= nextLv) hpExtra += e.value; }
|
||||
model.hp_max += hpExtra;
|
||||
}
|
||||
if (model.ap_bonus) {
|
||||
let apExtra = 0;
|
||||
for (const e of model.ap_bonus) { if (e.lv <= nextLv) apExtra += e.value; }
|
||||
model.ap += apExtra;
|
||||
let bonusHpP = 0, bonusApP = 0, bonusDefP = 0;
|
||||
if (model.bonus) {
|
||||
for (const e of model.bonus) {
|
||||
if (e.lv > nextLv) continue;
|
||||
bonusHpP += e.attrs.hp ?? 0;
|
||||
bonusApP += e.attrs.ap ?? 0;
|
||||
bonusDefP += e.attrs.def ?? 0;
|
||||
}
|
||||
model.hp_max += Math.round((base_hp + grow.hp) * bonusHpP / 100);
|
||||
model.ap += Math.round((base_ap + grow.ap) * bonusApP / 100);
|
||||
}
|
||||
|
||||
// 天赋加成层(与 Hero.load 对齐):先重置天赋可能修改的字段,避免升级重算时重复叠加
|
||||
// Why: ap/hp_max 已由上方公式从 base 重算(天然幂等),但 defense 与概率类字段是累加语义,必须先归零再注入
|
||||
if (model.fac === FacSet.HERO) {
|
||||
// defense 从品质系数后的基础值重算,叠加 def 成长 + def_bonus + lv12 专项,幂等
|
||||
// defense 从品质系数后的基础值重算,叠加 def 成长 + def 百分比 bonus + lv12 专项,幂等
|
||||
let base_def = Math.round(hero.def * qualityCoef) + grow.def;
|
||||
if (model.def_bonus) {
|
||||
for (const e of model.def_bonus) { if (e.lv <= nextLv) base_def += e.value; }
|
||||
base_def += Math.round(base_def * bonusDefP / 100);
|
||||
// bonus 概率类:固定百分点累加(在下方归零后注入,避免重复叠加)
|
||||
let bonusCrt = 0, bonusFrz = 0, bonusStun = 0, bonusPara = 0;
|
||||
if (model.bonus) {
|
||||
for (const e of model.bonus) {
|
||||
if (e.lv > nextLv) continue;
|
||||
bonusCrt += e.attrs.crt ?? 0;
|
||||
bonusFrz += e.attrs.frz ?? 0;
|
||||
bonusStun += e.attrs.stun ?? 0;
|
||||
bonusPara += e.attrs.para ?? 0;
|
||||
}
|
||||
}
|
||||
// lv12 职业属性专项:达到 12 级时按职业对单项基础属性 +50%(基数为 lv11 有效值)
|
||||
if (nextLv >= HERO_LV12_SPECIAL_LV && model.role !== undefined) {
|
||||
@@ -2199,10 +2209,11 @@ export class MissionCardComp extends CCComp {
|
||||
else if (attr === "ap") model.ap += Math.round(lv11.ap * 0.5);
|
||||
}
|
||||
model.defense = base_def;
|
||||
model.critical = 0;
|
||||
model.critical = bonusCrt;
|
||||
model.critical_damage = 0;
|
||||
model.stun_chance = 0;
|
||||
model.freeze_chance = 0;
|
||||
model.stun_chance = bonusStun;
|
||||
model.freeze_chance = bonusFrz;
|
||||
model.paralyze_chance = bonusPara;
|
||||
// 风怒机制已停用: 升级重算不再重置风怒
|
||||
// model.wfuny = 0;
|
||||
model.puncture_chance = 0;
|
||||
|
||||
Reference in New Issue
Block a user