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

@@ -28,14 +28,14 @@
"_active": true,
"_components": [
{
"__id__": 42
"__id__": 30
},
{
"__id__": 44
"__id__": 32
}
],
"_prefab": {
"__id__": 47
"__id__": 35
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -436,8 +436,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 240,
"height": 110
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -473,7 +473,7 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@65cbe",
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@62d7f",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
@@ -488,7 +488,10 @@
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_atlas": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
@@ -509,10 +512,10 @@
},
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_left": 70,
"_right": 70,
"_top": 5,
"_bottom": 5,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
@@ -552,219 +555,26 @@
"_parent": {
"__id__": 1
},
"_children": [
"_children": [],
"_active": true,
"_components": [
{
"__id__": 23
},
{
"__id__": 29
}
],
"_active": false,
"_components": [
{
"__id__": 35
"__id__": 25
},
{
"__id__": 37
},
{
"__id__": 39
"__id__": 27
}
],
"_prefab": {
"__id__": 41
"__id__": 29
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "icon",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 24
},
{
"__id__": 26
}
],
"_prefab": {
"__id__": 28
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 15,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.6,
"y": 0.6,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 23
},
"_enabled": true,
"__prefab": {
"__id__": 25
},
"_contentSize": {
"__type__": "cc.Size",
"width": 106,
"height": 96
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "6eBP1g6s5Goai//DXwnDla"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 23
},
"_enabled": true,
"__prefab": {
"__id__": 27
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@24ff5",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "caZzAfi9ZLqZS4Wd5Rqm3r"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "ce9MKPS+FJx4Y+XyFAZSyN",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 30
},
{
"__id__": 32
}
],
"_prefab": {
"__id__": 34
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -30,
"y": 55,
"z": 0
},
"_lrot": {
@@ -790,115 +600,6 @@
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 29
},
"_enabled": true,
"__prefab": {
"__id__": 31
},
"_contentSize": {
"__type__": "cc.Size",
"width": 64,
"height": 54.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "fb/sGndAVJK7moJI1+G/0t"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 29
},
"_enabled": true,
"__prefab": {
"__id__": 33
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_string": "挑战",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 30,
"_fontSize": 30,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": true,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "a9bThvxy9LiriOUrI/zZUU"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "b8MZ0xfLVCQ7mTzo5vkCgp",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@@ -909,12 +610,12 @@
},
"_enabled": true,
"__prefab": {
"__id__": 36
"__id__": 24
},
"_contentSize": {
"__type__": "cc.Size",
"width": 180,
"height": 110
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -937,7 +638,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 38
"__id__": 26
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -949,10 +650,7 @@
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@7eac7",
"__expectedType__": "cc.SpriteFrame"
},
"_spriteFrame": null,
"_type": 1,
"_fillType": 0,
"_sizeMode": 0,
@@ -982,14 +680,14 @@
},
"_enabled": true,
"__prefab": {
"__id__": 40
"__id__": 28
},
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_left": 70,
"_right": 70,
"_top": 5,
"_bottom": 5,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
@@ -1031,7 +729,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 43
"__id__": 31
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1059,11 +757,11 @@
},
"_enabled": true,
"__prefab": {
"__id__": 45
"__id__": 33
},
"clickEvents": [
{
"__id__": 46
"__id__": 34
}
],
"_interactable": true,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -32,6 +32,8 @@ export enum FightSet {
HERO_MAX_NUM = 3,//英雄最大数量
/** 英雄可升级到的最高等级(通过升级卡提升) */
HERO_MAX_LV = 6,
/** 英雄技能等级上限(无限升级下技能 5 级封顶,超出等级只长属性) */
HERO_SKILL_MAX_LV = 5,
/** 英雄属性随等级成长的倍率ap/hp = base * MULTIPLIER^(lv-1) */
HERO_LV_MULTIPLIER = 2,
// BACK_RANG=30,//后退范围

View File

@@ -1,4 +1,4 @@
/**
/**
* @file HeroSkillDesc.ts
* @description 英雄技能/能力描述的【唯一文字表达标准层】
*
@@ -28,7 +28,7 @@
* - SkillDescSet:结构化描述模板
* - FieldSkillSet驻场技能、BuffList计时 buff 默认值兜底、Attrs属性枚举
*/
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvTriggerEntry, LvFieldEntry, LvReviveEntry, LvBonusEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, getTriggerSkillName } from "./heroSet";
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvTriggerEntry, LvFieldEntry, LvReviveEntry, LvBonusEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, getTriggerSkillName, HGrowType, calcGrowBonus } from "./heroSet";
import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet";
import { FieldDescSet, FieldValueFmt, SkillDescSet } from "./SkillDescSet";
import { BuffList } from "./BuffSet";
@@ -53,6 +53,14 @@ export interface ISkillDescSource {
/** 额外攻击/生命加成(按 lv 累加,用于成长文案) */
ap_bonus?: LvBonusEntry[];
hp_bonus?: LvBonusEntry[];
/** 成长类型(决定每级 hp/ap 标准递增增量) */
grow_type?: HGrowType;
/** 基础攻击/生命(成长基数,线性公式 = 基础 × 系数 × (lv-1) */
ap?: number;
hp?: number;
/** 原始基础攻击/生命HeroAttrsComp 场景优先用作成长基数,避免运行时 hp/ap 重复计成长) */
base_ap?: number;
base_hp?: number;
}
/**
@@ -315,11 +323,16 @@ function buildReviveLines(source: ISkillDescSource, showName: boolean = true, sh
return lines;
}
/** 额外属性加成ap/hp累计结构化行高亮累计数值 */
/** 额外属性加成ap/hp累计结构化行成长递增 + 一次性奖励,高亮累计数值 */
function buildEvolveBonusLines(source: ISkillDescSource): ISkillLine[] {
const lv = Math.max(1, source.lv ?? 1);
let apTotal = 0;
let hpTotal = 0;
// 成长线性增量优先用原始基础值base_*HeroAttrsComp 场景),否则用配置 hp/ap
// 缺基础值时按 0 处理(如纯触发技能描述场景),避免运行时 hp/ap 重复计成长
const baseHp = source.base_hp ?? source.hp ?? 0;
const baseAp = source.base_ap ?? source.ap ?? 0;
const grow = calcGrowBonus({ hp: baseHp, ap: baseAp, grow_type: source.grow_type }, lv);
let apTotal = grow.ap;
let hpTotal = grow.hp;
if (source.ap_bonus) {
for (const e of source.ap_bonus) { if (e.lv <= lv) apTotal += e.value; }
}
@@ -327,8 +340,8 @@ function buildEvolveBonusLines(source: ISkillDescSource): ISkillLine[] {
for (const e of source.hp_bonus) { if (e.lv <= lv) hpTotal += e.value; }
}
const lines: ISkillLine[] = [];
if (apTotal !== 0) lines.push({ prefix: "成长:额外", value: `+${apTotal}`, suffix: ATTR_NAME[Attrs.ap] ?? "攻击力" });
if (hpTotal !== 0) lines.push({ prefix: "成长:额外", value: `+${hpTotal}`, suffix: ATTR_NAME[Attrs.hp_max] ?? "最大生命" });
if (apTotal !== 0) lines.push({ prefix: "成长:", value: `+${apTotal}`, suffix: ATTR_NAME[Attrs.ap] ?? "攻击力" });
if (hpTotal !== 0) lines.push({ prefix: "成长:", value: `+${hpTotal}`, suffix: ATTR_NAME[Attrs.hp_max] ?? "最大生命" });
return lines;
}

View File

@@ -107,6 +107,45 @@ export enum MonStart {
START_I = 90, //x轴间隔
}
/**
* 英雄成长类型(决定每级 hp/ap 标准增量系数)。
* Why: 不同定位英雄的成长曲线差异化,避免"一刀切"导致坦克/刺客同强度。
*/
export enum HGrowType {
Tank = 0, // 坦克:高 hp 低 ap
Warrior = 1, // 战士:均衡
Assassin = 2, // 刺客:低 hp 高 ap
Mage = 3, // 法师:中 hp 高 ap
Support = 4, // 辅助:中 hp 低 ap
}
/** 成长类型系数表(每级增量 = 英雄基础属性 × 对应系数) */
export const HERO_GROW_COEF: Record<HGrowType, { hp: number; ap: number }> = {
[HGrowType.Tank]: { hp: 1.5, ap: 0.6 },
[HGrowType.Warrior]: { hp: 1.0, ap: 1.0 },
[HGrowType.Assassin]: { hp: 0.7, ap: 1.4 },
[HGrowType.Mage]: { hp: 0.9, ap: 1.3 },
[HGrowType.Support]: { hp: 1.1, ap: 0.7 },
};
/**
* 计算英雄在目标等级下的累计属性成长增量(线性:每级加一份"基础属性×系数")。
* 公式:累计 = 基础属性 × 类型系数 × (lv - 1)。
* Why: 以英雄自身基础值为基数,自动适配低/高费卡的成长占比,避免全局固定值失衡;
* 线性曲线在无限升级下不会指数爆炸,强度平滑可控。
* @param hero 英雄配置(取 hp/ap 基础值与 grow_type
* @param targetLv 目标等级≥1
* @returns 累计 hp/ap 增量(已取整)
*/
export function calcGrowBonus(hero: Pick<heroInfo, "hp" | "ap" | "grow_type">, targetLv: number): { hp: number; ap: number } {
const coef = HERO_GROW_COEF[hero.grow_type ?? HGrowType.Warrior];
const steps = Math.max(0, Math.floor(targetLv) - 1);
return {
hp: Math.floor(hero.hp * coef.hp * steps),
ap: Math.floor(hero.ap * coef.ap * steps),
};
}
/**
@@ -248,6 +287,7 @@ export interface heroInfo {
lv: number; // 英雄等级
card_lv?: number; // 卡牌等级(驱动 bg_node 颜色1=green 2=blue 3=purple 4=red 5=yellow
type: HType; // 攻击定位(近战/中程/远程)
grow_type?: HGrowType; // 成长类型(决定每级 hp/ap 标准增量,默认 Warrior
monType?: MonType; // 怪物专属类型
hp: number; // 生命值上限
ap: number; // 攻击力
@@ -340,19 +380,18 @@ export interface LvBonusEntry {
export const HeroInfo: Record<number, heroInfo> = {
// ========== atked 类(战士 · 自身强化) ==========
5011: {
uuid: 5011, name: "小铁卫", path: "hk1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, hp: 300, ap: 28,
uuid: 5011, name: "铁壁", path: "hk1", fac: FacSet.HERO, card_lv: 1, lv: 1, type: HType.Melee, grow_type: HGrowType.Tank, hp: 300, ap: 28,
skills: { 6001: { uuid: 6001, lv: 1, cd: AtkSpeedSet[AtkSpeedLv.Slow3].cd, ccd: 0 } },
// 成长主线:护盾层数 4→5→6等效减伤 57%→67%
// 成长主线:受击5次凝聚护盾层数 2→3→4→5每级按 Tank 成长类型递增 hp/ap
atked: {
6301: [
{ lv: 1, s_lv: SkillLv.Lv1, t_num: 3, overrides: { TGroup: TGroup.Self, ap: 4 } },
{ lv: 2, s_lv: SkillLv.Lv2, t_num: 3, overrides: { TGroup: TGroup.Self, ap: 5 } },
{ lv: 4, s_lv: SkillLv.Lv3, t_num: 3, overrides: { TGroup: TGroup.Self, ap: 6 } },
{ lv: 1, s_lv: SkillLv.Lv1, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 2 } },
{ lv: 3, s_lv: SkillLv.Lv2, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 3 } },
{ lv: 5, s_lv: SkillLv.Lv3, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 4 } },
{ lv: 6, s_lv: SkillLv.Lv4, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 5 } },
],
},
hp_bonus: [{ lv: 3, value: 150 }, { lv: 5, value: 150 }],
ap_bonus: [{ lv: 5, value: 10 }],
info: "<color=#27AE60>✧</color>受伤获得护盾",
info: "<color=#27AE60>✧</color>受击5次凝聚护盾",
},
5012: {
uuid: 5012, name: "不死小强", path: "hk2", fac: FacSet.HERO, card_lv: 2, lv: 1, type: HType.Melee, hp: 600, ap: 57,
@@ -368,7 +407,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 1, s_lv: SkillLv.Lv1, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 2 } },
],
},
hp_bonus: [{ lv: 3, value: 150 }],
info: "<color=#27AE60>✧</color>受伤恢复生命",
},
5013: {
@@ -385,7 +423,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 1, s_lv: SkillLv.Lv1, t_num: 5, overrides: { TGroup: TGroup.Self, ap: 2 } },
],
},
hp_bonus: [{ lv: 3, value: 150 }],
info: "<color=#27AE60>✧</color>受伤获得护盾与生命",
},
5014: {
@@ -402,7 +439,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 4, s_lv: SkillLv.Lv3, t_num: 5, overrides: { TGroup: TGroup.Self, timed_buff_id: 7012, buff_value: 10, buff_duration: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 150 }],
info: "<color=#27AE60>✧</color>受伤提升攻击与暴击",
},
5015: {
@@ -421,7 +457,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 4, s_lv: SkillLv.Lv3, t_num: 5, overrides: { TGroup: TGroup.Self, timed_buff_id: 7020, buff_value: 10, buff_duration: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 200 }],
info: "<color=#27AE60>✧</color>受伤获得护盾与暴伤",
},
5016: {
@@ -438,8 +473,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 4, s_lv: SkillLv.Lv3, t_num: 5, overrides: { TGroup: TGroup.Self, timed_buff_id: 7017, buff_value: 10, buff_duration: 6 } },
],
},
ap_bonus: [{ lv: 3, value: 20 }],
hp_bonus: [{ lv: 3, value: 150 }],
info: "<color=#27AE60>✧</color>受伤提升攻击与击晕",
},
// 以下英雄暂时注释,保留待后续扩展
@@ -481,8 +514,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv2, t_num: 4, overrides: { TGroup: TGroup.Self, ap: 1 } },
],
},
ap_bonus: [{ lv: 3, value: 20 }],
hp_bonus: [{ lv: 3, value: 80 }],
info: "<color=#27AE60>✧</color>攻击永久提升攻击",
},
5022: {
@@ -499,8 +530,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 1, s_lv: SkillLv.Lv1, t_num: 7, overrides: { TGroup: TGroup.Self, ap: 1 } },
],
},
ap_bonus: [{ lv: 3, value: 20 }],
hp_bonus: [{ lv: 3, value: 80 }],
info: "<color=#27AE60>✧</color>攻击限时提升暴击",
},
5023: {
@@ -517,7 +546,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 4, s_lv: SkillLv.Lv3, t_num: 5, overrides: { TGroup: TGroup.Self, timed_buff_id: 7012, buff_value: 10, buff_duration: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 100 }],
info: "<color=#27AE60>✧</color>攻击提升攻击与暴击",
},
// 5022: {
@@ -550,7 +578,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv2, t_num: 5, overrides: { TGroup: TGroup.Team, hit_count: 2, ap: 1 } },
],
},
hp_bonus: [{ lv: 3, value: 60 }],
info: "<color=#27AE60>✧</color>攻击提升队友攻击",
},
5032: {
@@ -568,7 +595,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 4, s_lv: SkillLv.Lv2, t_num: 7, overrides: { TGroup: TGroup.Team, hit_count: 4, ap: 1 } },
],
},
hp_bonus: [{ lv: 3, value: 80 }],
info: "<color=#27AE60>✧</color>攻击限时提升队友暴击",
},
// 5032: {
@@ -592,7 +618,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 4, s_lv: SkillLv.Lv3, t_num: 7, overrides: { TGroup: TGroup.Team, hit_count: 4, timed_buff_id: 7020, buff_value: 10, buff_duration: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 100 }],
info: "<color=#27AE60>✧</color>攻击提升队友攻击与暴伤",
},
// 5033: {
@@ -659,7 +684,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv4, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 100 }],
info: "<color=#27AE60>✧</color>死亡全队获得护盾",
},
5042: {
@@ -697,7 +721,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv3, t_num: 1, overrides: { TGroup: TGroup.Team, timed_buff_id: 7012, buff_value: 10, buff_duration: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 100 }],
info: "<color=#27AE60>✧</color>死亡全队击晕与暴击",
},
5044: {
@@ -715,7 +738,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv3, t_num: 1, overrides: { TGroup: TGroup.Team, timed_buff_id: 7020, buff_value: 10, buff_duration: 6 } },
],
},
hp_bonus: [{ lv: 3, value: 150 }],
info: "<color=#27AE60>✧</color>死亡全队暴击与暴伤",
},
@@ -759,7 +781,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv3, t_num: 1, overrides: { TGroup: TGroup.Team, timed_buff_id: 7111, buff_value: 90, buff_duration: 10 } },
],
},
hp_bonus: [{ lv: 2, value: 80 }, { lv: 4, value: 80 }],
info: "<color=#27AE60>✧</color>回合开始全队提升攻击",
},
5052: {
@@ -776,7 +797,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 1, s_lv: SkillLv.Lv1, t_num: 1, overrides: { TGroup: TGroup.Team, ap: 2 } },
],
},
hp_bonus: [{ lv: 3, value: 80 }],
info: "<color=#27AE60>✧</color>回合开始全队获得护盾",
},
// 5052: {
@@ -832,7 +852,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 2, uuids: [7202, 7006] },
{ lv: 4, uuids: [7202, 7206] },
],
hp_bonus: [{ lv: 3, value: 80 }],
info: "<color=#27AE60>✧</color>光环全队提升攻击\n<color=#9B59B6>✦</color>追加提升攻速",
},
@@ -849,7 +868,6 @@ export const HeroInfo: Record<number, heroInfo> = {
{ lv: 5, s_lv: SkillLv.Lv4, t_num: 5, overrides: { TGroup: TGroup.Team, hit_count: 2, ap: 300 } },
],
},
hp_bonus: [{ lv: 3, value: 80 }],
info: "<color=#27AE60>✧</color>攻击治疗低血队友",
},
// 5072: {
@@ -1058,11 +1076,13 @@ export function resolveReviveByLv(revive: LvReviveEntry[] | undefined, lv: numbe
return pick ? { s_uuid: pick.s_uuid, r_num: pick.r_num, upr: pick.upr } : undefined;
}
/** 计算目标等级下额外属性加成hp_bonus/ap_bonus 按 lv 累加 */
/** 计算目标等级下额外属性加成hp_bonus/ap_bonus 一次性奖励 + 成长类型每级递增 */
function getEvolveBonuses(hero: heroInfo, targetLv: number): { hp: number, ap: number } {
let hp = 0;
let ap = 0;
const lv = Math.max(1, targetLv);
// 成长类型线性增量(仅英雄生效,怪物无 grow_type 时走 Warrior 基准但不影响平衡,因怪物通常不填)
const grow = hero.fac === FacSet.HERO ? calcGrowBonus(hero, lv) : { hp: 0, ap: 0 };
let hp = grow.hp;
let ap = grow.ap;
if (hero.hp_bonus) {
for (const e of hero.hp_bonus) {
if (e.lv <= lv) hp += e.value;
@@ -1076,14 +1096,12 @@ function getEvolveBonuses(hero: heroInfo, targetLv: number): { hp: number, ap: n
return { hp, ap };
}
/** 计算目标等级下的有效 HP/AP含等级倍率与额外加成) */
/** 计算目标等级下的有效 HP/AP固定基础值 + 成长递增与一次性加成) */
function getEffectiveStats(hero: heroInfo, targetLv: number): { hp: number, ap: number } {
const bonuses = getEvolveBonuses(hero, targetLv);
const lv = Math.max(1, targetLv);
const multiplier = Math.pow(FightSet.HERO_LV_MULTIPLIER, lv - 1);
return {
hp: (hero.hp + bonuses.hp) * multiplier,
ap: (hero.ap + bonuses.ap) * multiplier,
hp: hero.hp + bonuses.hp,
ap: hero.ap + bonuses.ap,
};
}

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, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv } from "../common/config/heroSet";
import { HeroInfo, HeroPos, resolveFormationTargetX, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, calcGrowBonus } from "../common/config/heroSet";
import { GameEvent } from "../common/config/GameEvent";
import { SkillTriggerType } from "../common/config/heroSet";
import { SkillTriggerHelper } from "./SkillTriggerHelper";
@@ -119,6 +119,7 @@ export class Hero extends ecs.Entity {
model.card_lv = card_lv;
model.base_card_lv = hero.card_lv ?? 1;
model.type = hero.type;
model.grow_type = hero.grow_type;
model.fac = FacSet.HERO;
model.dis = hero.dis ?? 720;
model.posIndex = posIndex;
@@ -138,10 +139,9 @@ export class Hero extends ecs.Entity {
model.ap_bonus = hero.ap_bonus;
model.hp_bonus = hero.hp_bonus;
// 基础属性按等级倍率初始化
// 使用指数增长公式:等级 2 时为原来的 HERO_LV_MULTIPLIER 倍,等级 3 时为 HERO_LV_MULTIPLIER^2 倍
let base_ap = hero.ap * Math.pow(FightSet.HERO_LV_MULTIPLIER, model.lv - 1);
let base_hp = hero.hp * Math.pow(FightSet.HERO_LV_MULTIPLIER, model.lv - 1);
// 基础属性:固定基础值,成长完全由 grow_type 指数递增提供(替代旧 ×2^(lv-1) 翻倍)
let base_ap = hero.ap;
let base_hp = hero.hp;
model.base_ap = base_ap;
model.base_hp = base_hp;
@@ -162,8 +162,8 @@ export class Hero extends ecs.Entity {
for (const key in hero.skills) {
const skill = hero.skills[key];
if (!skill) continue;
// 最终技能等级 = 初始技能等级 + 英雄等级增量,下限 0
model.skills[skill.uuid] = { ...skill, lv: Math.max(0, skill.lv + hero_lv - 2), ccd: 0 };
// 最终技能等级 = 初始技能等级 + 英雄等级增量,下限 0、上限封顶(无限升级下技能不涨过上限)
model.skills[skill.uuid] = { ...skill, lv: Math.min(FightSet.HERO_SKILL_MAX_LV, Math.max(0, skill.lv + hero_lv - 2)), ccd: 0 };
}
// 按当前英雄等级 resolve 触发技能/光环/复活到运行时缓存,供 SkillTriggerHelper 消费
@@ -176,7 +176,11 @@ export class Hero extends ecs.Entity {
model.runtime_field = resolveFieldByLv(model.field, hero_lv);
model.runtime_revive = resolveReviveByLv(model.revive, hero_lv);
// 应用额外属性加成hp_bonus/ap_bonus 按 ≤英雄等级 累加)
// 应用额外属性加成(成长类型每级递增 + hp_bonus/ap_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;
if (model.hp_bonus) {
let hpExtra = 0;
for (const e of model.hp_bonus) { if (e.lv <= hero_lv) hpExtra += e.value; }

View File

@@ -28,6 +28,7 @@ export class HeroAttrsComp extends ecs.Comp {
base_card_lv: number = 1;
type: number = 0; // 0近战 1远程 2辅助
fac: number = 0; // 0:hero 1:monster
grow_type?: number; // 成长类型HGrowType驱动每级 hp/ap 线性成长
// ==================== 基础属性(有初始值) ====================
base_ap: number = 0; // 原始基础攻击(无任何加成)
base_hp: number = 0; // 原始基础血量(无任何加成)

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;