feat: 重构英雄成长系统,支持差异化成长曲线与无限升级

1. 新增英雄成长类型枚举与系数表,实现坦克/战士/刺客等差异化成长
2. 替换旧的固定指数成长逻辑,改用线性累计成长公式
3. 移除英雄等级上限,改为技能等级封顶,调整升级/出售金币计算逻辑
4. 重构属性计算与UI展示逻辑,统一成长计算入口
5. 优化技能文案展示与英雄配置格式
This commit is contained in:
panFD
2026-08-10 23:33:06 +08:00
parent d3ee664b00
commit 4eba1c8ad8
14 changed files with 3085 additions and 2624 deletions

View File

@@ -483,6 +483,9 @@ export class CHeroComp extends CCComp {
atked: hero.atked,
field: hero.field,
revive: hero.revive,
grow_type: hero.grow_type,
ap: hero.ap,
hp: hero.hp,
};
}

View File

@@ -18,7 +18,7 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { HeroInfo } from "../common/config/heroSet";
import { HeroInfo, calcGrowBonus } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { Hero } from "../hero/Hero";
import { FacSet, FightSet, getLvColor, NumberFormatter } from "../common/config/GameSet";
@@ -165,11 +165,7 @@ export class HeroBoxComp extends CCComp {
private onUpgradeHeroClick() {
if (!this.eid || !this.model) return;
const heroLv = Math.max(1, Math.floor(this.model.lv ?? 1));
// 已满级:气泡提示,复用英雄栏 smalltip 挂载点
if (heroLv >= FightSet.HERO_MAX_LV) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, { type: "hero_full", text: "已达最高等级" });
return;
}
// 无限升级:不再拦截满级
oops.audio.playEffect("music/button");
const nextLv = heroLv + 1;
@@ -226,13 +222,16 @@ export class HeroBoxComp extends CCComp {
atked: hero.atked,
field: hero.field,
revive: hero.revive,
grow_type: hero.grow_type,
ap: hero.ap,
hp: hero.hp,
});
// ---- 属性成长(按 HERO_LV_MULTIPLIER 幂成长 + 等级额外加成) ----
// ---- 属性成长(固定基础值 + 成长类型指数递增 + 一次性加成) ----
const attrLine = (lv: number): string => {
const mult = Math.pow(FightSet.HERO_LV_MULTIPLIER, lv - 1);
const ap = Math.floor(hero.ap * mult + HeroBoxComp.sumBonus(hero.ap_bonus, lv));
const hp = Math.floor(hero.hp * mult + HeroBoxComp.sumBonus(hero.hp_bonus, lv));
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));
return `攻击 <color=#27AE60>${ap}</color> 生命 <color=#27AE60>${hp}</color>`;
};
@@ -548,17 +547,13 @@ export class HeroBoxComp extends CCComp {
this.sell_price_label.string = `${MissionEconomy.getSellGold(lv)}`;
}
// ---- 升级费用(满级隐藏;金币不足时标红提示) ----
// ---- 升级费用(无限升级:始终显示;金币不足时标红提示) ----
if (this.upgrade_price_label && this.upgrade_price_label.isValid) {
const lv = Math.max(1, Math.floor(this.model.lv ?? 1));
if (lv >= FightSet.HERO_MAX_LV) {
this.upgrade_price_label.string = "";
} else {
const cost = MissionEconomy.getUpgradeCost(lv);
this.upgrade_price_label.string = `${cost}`;
// 金币不足标红,足够恢复默认色,给玩家直观的可升级反馈
this.upgrade_price_label.color = MissionEconomy.getCoin() >= cost ? Color.WHITE : HeroBoxComp.INSUFFICIENT_COLOR;
}
const cost = MissionEconomy.getUpgradeCost(lv);
this.upgrade_price_label.string = `${cost}`;
// 金币不足标红,足够恢复默认色,给玩家直观的可升级反馈
this.upgrade_price_label.color = MissionEconomy.getCoin() >= cost ? Color.WHITE : HeroBoxComp.INSUFFICIENT_COLOR;
}
// ---- AP / HP ----

View File

@@ -52,7 +52,7 @@ import { MissSkillsComp } from "./MissSkillsComp";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo, HType, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv } from "../common/config/heroSet";
import { HeroInfo, HType, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus } from "../common/config/heroSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FacSet, FightSet } from "../common/config/GameSet";
import { MissionEconomy } from "./MissionEconomy";
@@ -771,10 +771,7 @@ export class MissionCardComp extends CCComp {
if (!eid) return;
const actor = this.queryAliveHeroActors().find(item => item.eid === eid);
if (!actor) return;
if (actor.model.lv >= FightSet.HERO_MAX_LV) {
oops.gui.toast(`已达最高等级`);
return;
}
// 无限升级:不再拦截满级,仅校验金币
// 统一经济管理入口:按当前等级扣费
if (!MissionEconomy.executeUpgradeCost(actor.model.lv)) {
oops.gui.toast(`金币不足`);
@@ -2192,8 +2189,7 @@ export class MissionCardComp extends CCComp {
if (!heroEid) return false;
const actor = this.queryAliveHeroActors().find(item => item.eid === heroEid);
if (!actor) return false;
if (actor.model.lv >= FightSet.HERO_MAX_LV) return false;
const nextLv = Math.min(FightSet.HERO_MAX_LV, actor.model.lv + 1);
const nextLv = actor.model.lv + 1;
this.applyHeroLevel(actor.model, nextLv);
if (actor.view) {
actor.view.playBuff("buff_lvup");
@@ -2202,8 +2198,8 @@ export class MissionCardComp extends CCComp {
}
/**
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
* 应用英雄等级变化(属性 = 固定基础值 + grow_type 指数递增重新计算)。
* 沿用旧版本 SkillLvUp 的技能等级映射规则(技能按 HERO_SKILL_MAX_LV 封顶)
*
* 与 Hero.load 保持一致:
* 1. 同步刷新 base_ap/base_hp 快照,下游 UI/复活读到当前 lv 的基础值
@@ -2214,13 +2210,13 @@ export class MissionCardComp extends CCComp {
public applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
const hero = HeroInfo[model.hero_uuid];
if (!hero) return;
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));
const nextLv = Math.max(1, Math.floor(targetLv));
const hpRate = model.hp_max > 0 ? model.hp / model.hp_max : 1;
model.lv = nextLv;
// 同步 base 快照(与 Hero.load 同公式),保证 UI/复活等下游读到当前 lv 的基础值
const base_ap = hero.ap * Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
const base_hp = hero.hp * Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
// 同步 base 快照(与 Hero.load 同公式:固定基础值,成长由 grow_type 指数递增提供)
const base_ap = hero.ap;
const base_hp = hero.hp;
model.base_ap = base_ap;
model.base_hp = base_hp;
@@ -2233,7 +2229,10 @@ export class MissionCardComp extends CCComp {
model.hp_max = base_hp;
}
// 按 ≤nextLv 累加 hp_bonus/ap_bonus沿用 Hero.load 的加法区规则)
// 按 ≤nextLv 累加:成长类型每级递增 + hp_bonus/ap_bonus 一次性奖励(沿用 Hero.load 的加法区规则)
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; }
@@ -2262,7 +2261,7 @@ export class MissionCardComp extends CCComp {
for (const key in hero.skills) {
const skill = hero.skills[key];
if (!skill) continue;
model.skills[skill.uuid] = { ...skill, lv: Math.max(0, skill.lv + nextLv - 2), ccd: 0 };
model.skills[skill.uuid] = { ...skill, lv: Math.min(FightSet.HERO_SKILL_MAX_LV, Math.max(0, skill.lv + nextLv - 2)), ccd: 0 };
}
model.updateSkillDistanceCache();
model.dirty_hp = true;

View File

@@ -110,12 +110,12 @@ export class MissionEconomy {
}
/**
* 计算英雄升级金币消耗(按当前等级线性缩放Lv×5
* 计算英雄升级金币消耗(指数递增5×2^(lv-1),无限升级下指数控速
* @param heroLevel 当前英雄等级(升级前)
*/
static getUpgradeCost(heroLevel: number = 1): number {
const lv = Math.max(1, Math.floor(heroLevel));
return lv * 5;
return Math.floor(5 * Math.pow(2, lv - 1));
}
/**
@@ -128,11 +128,14 @@ export class MissionEconomy {
}
/**
* 计算英雄出售金币(按英雄等级缩放)
* 计算英雄出售金币(按累计升级投入的 50% 折算,无限升级自动缩放)
* 累计投入 = 等比数列求和 Σ5×2^(i-1)i=1..lv-1= 5×(2^(lv-1) - 1)。
*/
static getSellGold(heroLevel: number = 1): number {
const sellByLevel: Record<number, number> = { 1: 3, 2: 10, 3: 25 };
const baseSellGold = sellByLevel[heroLevel] || 3;
const lv = Math.max(1, Math.floor(heroLevel));
// 基础出售价 3保底招募残值+ 累计升级投入的 50%
const investTotal = 5 * (Math.pow(2, lv - 1) - 1);
const baseSellGold = 3 + Math.floor(investTotal * 0.5);
// 驻场英雄带来的"出售金币"加成(包含原 SellBonus 语义,统一在 SellGold 中)
const goldBoost = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.SellGold);
const totalSellGold = baseSellGold + goldBoost;