feat(hero-config): 新增英雄额外属性加成档位的图鉴展示支持

本次变更完成了英雄bonus属性加成系统的图鉴可视化改造:
1. 扩展LvBonusAttrsEntry类型,新增name/icon/info配置字段
2. 服务端渲染兼容新增字段,仅在配置存在时输出
3. 更新英雄配置文档,补充bonus字段的新参数说明
4. 新增属性加成编辑器表单,支持配置名称、图标和自定义描述
5. 实现英雄图鉴的bonus档位图标渲染逻辑,支持锁定状态和点击弹窗
6. 新增bonus效果文案生成工具,支持自定义模板和属性自动生成
7. 为技能弹窗新增bonus专属渲染逻辑,适配属性加成档位的展示
8. 为部分测试英雄配置了bonus档位的名称属性
This commit is contained in:
panFD
2026-09-07 23:58:44 +08:00
parent 771b0aad26
commit dbc013a20f
9 changed files with 183 additions and 19 deletions

View File

@@ -32,6 +32,7 @@ import {
HeroInfo, heroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry, FieldEntry, HeroInfo, heroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry, FieldEntry,
TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv, TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv,
getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName, getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName,
LvBonusAttrsEntry, BonusAttrKey,
} from "./config/heroSet"; } from "./config/heroSet";
import { SkillSet, mergeSkillParams, SkillConfig, SkillKind, TGroup, FieldSkillSet } from "./config/SkillSet"; import { SkillSet, mergeSkillParams, SkillConfig, SkillKind, TGroup, FieldSkillSet } from "./config/SkillSet";
import { BuffList } from "./config/BuffSet"; import { BuffList } from "./config/BuffSet";
@@ -714,3 +715,35 @@ function resolveTriggerSkillName(hero: heroInfo, skillUuid: number): string {
} }
return SkillSet[skillUuid]?.name ?? ""; return SkillSet[skillUuid]?.name ?? "";
} }
/* ==================== bonus(额外属性加成)展示文案 ==================== */
/** bonus 属性键 → 中文展示名与单位(hp/ap/def 为百分比,概率类为固定百分点) */
const BONUS_ATTR_LABEL: Record<BonusAttrKey, { name: string; suffix: string }> = {
hp: { name: "生命", suffix: "%" },
ap: { name: "攻击", suffix: "%" },
def: { name: "防御", suffix: "%" },
crt: { name: "暴击率", suffix: "" },
frz: { name: "冰冻", suffix: "" },
stun: { name: "击晕", suffix: "" },
para: { name: "麻痹", suffix: "" },
};
/**
* 生成单个 bonus 档位的效果描述(富文本)。
* entry.info 显式配置时优先渲染(占位符 {lv} 替换为解锁等级);否则按 attrs 键自动生成。
* @param entry bonus 档位配置
* @returns 富文本效果串(不含名称与解锁等级,由调用方拼接)
*/
export function buildBonusEffectDesc(entry: LvBonusAttrsEntry): string {
if (entry.info) return entry.info.replace(/\{lv\}/g, `${entry.lv}`);
const parts: string[] = [];
for (const key of Object.keys(entry.attrs) as BonusAttrKey[]) {
const v = entry.attrs[key];
if (v === undefined || v === 0) continue;
const meta = BONUS_ATTR_LABEL[key];
const sign = v > 0 ? "+" : "";
parts.push(`${meta.name} ${valueRich(`${sign}${v}${meta.suffix}`)}`);
}
return parts.join(",") || "无效果";
}

View File

@@ -519,6 +519,12 @@ export type BonusAttrKey = 'hp' | 'ap' | 'def' | 'crt' | 'frz' | 'stun' | 'para'
export interface LvBonusAttrsEntry { export interface LvBonusAttrsEntry {
lv: number; lv: number;
attrs: Partial<Record<BonusAttrKey, number>>; attrs: Partial<Record<BonusAttrKey, number>>;
/** 展示名(图鉴技能图标/弹窗用,如「钢铁壁垒」);缺省不出图标 */
name?: string;
/** 图标帧名(gui/ui7 图集);缺省该档不在图鉴出图标 */
icon?: string;
/** 自定义描述模板(可选);缺省由 attrs 自动生成效果文案 */
info?: string;
} }
/* /*
*=============英雄配置列表================ *=============英雄配置列表================

View File

@@ -22,7 +22,7 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "db://oops-framework/core/Oops"; import { oops } from "db://oops-framework/core/Oops";
import { mLogger } from "../common/Logger"; import { mLogger } from "../common/Logger";
import { HeroInfo, HeroList, SkillTriggerType, LvFieldEntry, LvTriggerEntry, TriggerGrouped, HSkillInfo, getHeroDisplayStats } from "../common/config/heroSet"; import { HeroInfo, HeroList, SkillTriggerType, LvFieldEntry, LvTriggerEntry, TriggerGrouped, HSkillInfo, getHeroDisplayStats, LvBonusAttrsEntry } from "../common/config/heroSet";
import { buildHeroSkillDesc } from "../common/HeroSkillText"; import { buildHeroSkillDesc } from "../common/HeroSkillText";
import { GameEvent } from "../common/config/GameEvent"; import { GameEvent } from "../common/config/GameEvent";
import { HERO_UPGRADE_NEED_FRAGMENTS, RARITY_BG_COLORS } from "../common/config/GameSet"; import { HERO_UPGRADE_NEED_FRAGMENTS, RARITY_BG_COLORS } from "../common/config/GameSet";
@@ -537,6 +537,26 @@ export class HerosListComp extends CCComp {
this.addTriggerGroupIcon(hero[type] as TriggerGrouped | undefined, lv, type) this.addTriggerGroupIcon(hero[type] as TriggerGrouped | undefined, lv, type)
} }
} }
// ---- bonus 档位:配置了 icon 的档位在技能列表最末各出一个图标 ----
this.addBonusIcons(hero.bonus, lv)
}
/**
* bonus 档位图标:配置了 icon 的档位各出一个图标(追加在技能列表最末)。
* 激活判定:英雄局外等级 ≥ 档位 lv;未激活显示锁定遮罩。
* 点击弹窗走 SinfoComp bonus 专属渲染(名称/图标取档位配置,主说明为 attrs 效果)。
*/
private addBonusIcons(bonus: LvBonusAttrsEntry[] | undefined, lv: number) {
if (!bonus?.length) return
for (const entry of bonus) {
if (!entry.icon) continue
const node = instantiate(this.skill_icon_prefab)
node.name = `bonus_lv${entry.lv}`
const comp = node.getComponent(SSkillBoxComp) || node.addComponent(SSkillBoxComp)
comp.initBonus(this.huuid, entry, lv < entry.lv)
this.skills_node.addChild(node)
}
} }
/** /**

View File

@@ -7,6 +7,7 @@ import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import { import {
FieldEntry, HeroInfo, TriggerGrouped, SkillTriggerType, FieldEntry, HeroInfo, TriggerGrouped, SkillTriggerType,
getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName, getHeroSkillIcon, getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName, getHeroSkillIcon,
LvBonusAttrsEntry,
} from "../common/config/heroSet"; } from "../common/config/heroSet";
import { UIID } from "../common/config/GameUIConfig"; import { UIID } from "../common/config/GameUIConfig";
import { smc } from "../common/SingletonModuleComp"; import { smc } from "../common/SingletonModuleComp";
@@ -63,6 +64,8 @@ export class SSkillBoxComp extends CCComp {
public field: FieldEntry[] = []; public field: FieldEntry[] = [];
/** 英雄图鉴模式下的图标来源:skill=SkillSet,field=FieldSkillSet;undefined 表示装备卡模式 */ /** 英雄图鉴模式下的图标来源:skill=SkillSet,field=FieldSkillSet;undefined 表示装备卡模式 */
private hero_icon_source?: "skill" | "field"; private hero_icon_source?: "skill" | "field";
/** bonus 档位模式:该图标代表英雄额外属性加成的某一档(图鉴展示,无技能底座 uuid) */
private bonus_entry?: LvBonusAttrsEntry;
/** 点击事件是否已注册(防止重复绑定) */ /** 点击事件是否已注册(防止重复绑定) */
private _clickRegistered: boolean = false; private _clickRegistered: boolean = false;
@@ -79,6 +82,16 @@ export class SSkillBoxComp extends CCComp {
/** 点击:弹出技能信息框 */ /** 点击:弹出技能信息框 */
private onClick() { private onClick() {
oops.audio.playEffect("music/button"); oops.audio.playEffect("music/button");
// bonus 档位模式:专属弹窗参数(无技能底座 uuid,直接透传档位数据)
if (this.bonus_entry) {
oops.gui.open(UIID.SInfo, {
uuid: 0,
skill_lv: 1,
hero_uuid: this.hero_uuid || undefined,
bonus_entry: this.bonus_entry,
});
return;
}
oops.gui.open(UIID.SInfo, { oops.gui.open(UIID.SInfo, {
uuid: this.s_uuid, uuid: this.s_uuid,
skill_lv: this.rarity, skill_lv: this.rarity,
@@ -108,6 +121,24 @@ export class SSkillBoxComp extends CCComp {
} }
} }
/**
* 初始化 bonus 档位图标(英雄图鉴面板专用):
* 图标/名称直接取档位配置的 icon/name,无技能底座 uuid;点击弹窗走 bonus 专属渲染。
* @param heroUuid 所属英雄 UUID(激活判定用局外等级)
* @param entry bonus 档位配置(含 lv/attrs/name/icon/info)
* @param locked 当前英雄等级未达档位解锁等级时显示锁定遮罩
*/
initBonus(heroUuid: number, entry: LvBonusAttrsEntry, locked: boolean) {
this.hero_uuid = heroUuid;
this.bonus_entry = entry;
// 等级标签展示解锁所需英雄等级(Lv.n),便于玩家直观看到解锁条件
this.rarity = entry.lv;
this.card_icon = entry.icon;
this.skill_name = entry.name ?? "";
this.updateUI();
this.setLocked(locked);
}
/** /**
* 初始化英雄技能图标(英雄图鉴面板专用): * 初始化英雄技能图标(英雄图鉴面板专用):
* 图标按 s_uuid 直接取 SkillSet / FieldSkillSet 配置,不走 EquipPoolList。 * 图标按 s_uuid 直接取 SkillSet / FieldSkillSet 配置,不走 EquipPoolList。

View File

@@ -35,9 +35,9 @@ import { oops } from "db://oops-framework/core/Oops";
import { CardTriggerType, CardConfig } from "../common/config/CardSet"; import { CardTriggerType, CardConfig } from "../common/config/CardSet";
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet"; import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import { SkillCardList } from "../common/config/SCardSet"; import { SkillCardList } from "../common/config/SCardSet";
import { FieldEntry, SkillTriggerType, HeroInfo, getHeroSkillIcon } from "../common/config/heroSet"; import { FieldEntry, SkillTriggerType, HeroInfo, getHeroSkillIcon, LvBonusAttrsEntry } from "../common/config/heroSet";
import { buildCardDescRich } from "../common/config/CardSkillText"; import { buildCardDescRich } from "../common/config/CardSkillText";
import { buildHeroSkillTierLines, buildHeroGroupTierLines, HeroSkillTierLine } from "../common/HeroSkillText"; import { buildHeroSkillTierLines, buildHeroGroupTierLines, HeroSkillTierLine, buildBonusEffectDesc } from "../common/HeroSkillText";
import { smc } from "../common/SingletonModuleComp"; import { smc } from "../common/SingletonModuleComp";
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@@ -67,6 +67,9 @@ export interface SInfoParams {
/** 普攻 lv2+ 强化档专属模式(英雄图鉴):名称/图标取 lv1,只展示 lv2+ 档位说明 /** 普攻 lv2+ 强化档专属模式(英雄图鉴):名称/图标取 lv1,只展示 lv2+ 档位说明
* (裁剪 lv1 档,隐藏 2/3 级面板);等级显示偏移一档(配置 Lv2 → 显示 Lv1,Lv3 → Lv2) */ * (裁剪 lv1 档,隐藏 2/3 级面板);等级显示偏移一档(配置 Lv2 → 显示 Lv1,Lv3 → Lv2) */
hero_normal_lv2?: boolean; hero_normal_lv2?: boolean;
/** bonus 档位模式(英雄图鉴):直接透传额外属性加成档位,弹窗走专属渲染
* (名称/图标取档位配置,主说明为 attrs 自动生成或 info 模板,无分档面板) */
bonus_entry?: LvBonusAttrsEntry;
} }
/** 等级提升信息(用于 2 级 / 3 级面板) */ /** 等级提升信息(用于 2 级 / 3 级面板) */
@@ -311,6 +314,42 @@ export class SinfoComp extends CCComp {
} }
} }
/**
* bonus 档位模式渲染:展示额外属性加成某一档位的名称/图标/效果/解锁状态。
* 名称取 entry.name,图标取 entry.icon,主说明为 attrs 自动生成或 info 模板;
* 隐藏 2/3 级面板(bonus 档位各自独立出图标,无需分档对比)。
*/
private renderBonus(p: SInfoParams) {
const entry = p.bonus_entry!;
// 图标:直接用档位配置的 icon(不经底座解析)
if (this.icon_node && this.icon_node.isValid) {
let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite);
if (smc.skillIconsAtlas && entry.icon) {
sprite.spriteFrame = smc.skillIconsAtlas.getSpriteFrame(entry.icon) || null;
} else {
sprite.spriteFrame = null;
}
}
this.setLabel(this.name_label, entry.name ?? "属性强化");
// 等级标签展示解锁所需英雄等级
this.setLabel(this.lv_label, `Lv.${entry.lv}`);
// 主说明:attrs 自动生成效果文案(或 info 模板)
if (this.main_info && this.main_info.isValid) {
this.main_info.string = buildBonusEffectDesc(entry);
}
// 激活状态:英雄局外等级对比档位解锁等级
const heroLv = p.hero_uuid ? Math.max(1, smc.getHeroOuterLv(p.hero_uuid)) : 1;
const unlocked = heroLv >= entry.lv;
if (this.main_status && this.main_status.isValid) {
this.main_status.string = unlocked ? "已激活" : `${entry.lv}级解锁`;
this.main_status.color = unlocked ? new Color(0, 255, 10, 255) : new Color(150, 150, 150, 255);
}
this.updateInactiveBg(unlocked);
// 隐藏 2/3 级面板
this.setPanelActive(this.lv2_node, false);
this.setPanelActive(this.lv3_node, false);
}
/** 按档位渲染单等级面板:激活状态按英雄等级对比档位解锁等级 */ /** 按档位渲染单等级面板:激活状态按英雄等级对比档位解锁等级 */
private renderLvPanelFromTier(tier: HeroSkillTierLine | undefined, heroLv: number, statusLabel: Label | null, descRich: RichText | null) { private renderLvPanelFromTier(tier: HeroSkillTierLine | undefined, heroLv: number, statusLabel: Label | null, descRich: RichText | null) {
if (statusLabel && statusLabel.isValid) { if (statusLabel && statusLabel.isValid) {
@@ -331,6 +370,12 @@ export class SinfoComp extends CCComp {
if (!this.params) return; if (!this.params) return;
const p = this.params; const p = this.params;
// ---- bonus 档位模式:专属渲染(名称/图标取档位配置,无分档面板) ----
if (p.bonus_entry) {
this.renderBonus(p);
return;
}
// ---- 英雄技能模式:按英雄语境渲染分档描述(文本二) ---- // ---- 英雄技能模式:按英雄语境渲染分档描述(文本二) ----
if (p.hero_uuid && this.renderHeroSkill(p)) return; if (p.hero_uuid && this.renderHeroSkill(p)) return;

View File

@@ -100,7 +100,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"def": 50 "def": 50
} },
"name": "钢铁壁垒"
} }
] ]
}, },
@@ -200,7 +201,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"hp": 50 "hp": 50
} },
"name": "不屈战意"
} }
] ]
}, },
@@ -297,7 +299,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"hp": 50 "hp": 50
} },
"name": "不屈战意"
} }
] ]
}, },
@@ -396,7 +399,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"ap": 50 "ap": 50
} },
"name": "致命本能"
} }
] ]
}, },
@@ -496,7 +500,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"ap": 50 "ap": 50
} },
"name": "致命本能"
} }
] ]
}, },
@@ -594,7 +599,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"ap": 50 "ap": 50
} },
"name": "鹰眼锁定"
} }
] ]
}, },
@@ -692,7 +698,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"ap": 50 "ap": 50
} },
"name": "奥术共鸣"
} }
] ]
}, },
@@ -787,7 +794,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"hp": 50 "hp": 50
} },
"name": "守护之心"
} }
] ]
}, },
@@ -889,7 +897,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"def": 50 "def": 50
} },
"name": "钢铁壁垒"
} }
] ]
}, },
@@ -989,7 +998,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"ap": 50 "ap": 50
} },
"name": "鹰眼锁定"
} }
] ]
}, },
@@ -1092,7 +1102,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"def": 50 "def": 50
} },
"name": "钢铁壁垒"
} }
] ]
}, },
@@ -1214,7 +1225,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"ap": 50 "ap": 50
} },
"name": "奥术共鸣"
} }
] ]
}, },
@@ -1321,7 +1333,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"hp": 50 "hp": 50
} },
"name": "守护之心"
} }
] ]
}, },
@@ -1426,7 +1439,8 @@
"lv": 12, "lv": 12,
"attrs": { "attrs": {
"hp": 50 "hp": 50
} },
"name": "不屈战意"
} }
] ]
} }

View File

@@ -44,7 +44,7 @@
| `skills` | array | **恰好 2 个**:`[普攻, 大招]`,结构见 §3 | | `skills` | array | **恰好 2 个**:`[普攻, 大招]`,结构见 §3 |
| `atking`/`atked`/`fstart`/`dead` | object | 触发组,结构见 §4 | | `atking`/`atked`/`fstart`/`dead` | object | 触发组,结构见 §4 |
| `field` | array | 驻场光环档位,结构见 §5 | | `field` | array | 驻场光环档位,结构见 §5 |
| `bonus` | array | 额外属性加成 `[{lv,attrs:{...}}]`,运行时累加所有 lv ≤ 英雄等级 的档位;`hp`/`ap`/`def` 为百分比(基数取"基础×品质+成长"快照,多档加法叠加),`crt`/`frz`/`stun`/`para` 为固定百分点;常规英雄留空 | | `bonus` | array | 额外属性加成 `[{lv,attrs:{...},name?,icon?,info?}]`,运行时累加所有 lv ≤ 英雄等级 的档位;`hp`/`ap`/`def` 为百分比(基数取"基础×品质+成长"快照,多档加法叠加),`crt`/`frz`/`stun`/`para` 为固定百分点;配了 `icon` 的档位在图鉴技能列表最末出图标(点击弹窗展示 name/效果,描述缺省按 attrs 自动生成,`info` 可自定义模板支持 `{lv}` 占位);常规英雄留空 |
| `info` | string | 描述文案,可空 | | `info` | string | 描述文案,可空 |
| `revive` | — | **已废弃**:复活改在 `dead` 组 overrides 里配 `revive_hp`/`revive_count`,不要写独立字段 | | `revive` | — | **已废弃**:复活改在 `dead` 组 overrides 里配 `revive_hp`/`revive_count`,不要写独立字段 |

View File

@@ -206,7 +206,11 @@ function renderSingleHero(h) {
if (bonusList.length) { if (bonusList.length) {
push(` bonus: [`) push(` bonus: [`)
for (const b of bonusList) { for (const b of bonusList) {
push(` { lv: ${b.lv}, attrs: ${JSON.stringify(b.attrs)} },`) // 展示字段(name/icon/info)非空才输出,保持底座纯净
const nameSeg = b.name ? ` name: ${JSON.stringify(b.name)},` : ''
const iconSeg = b.icon ? ` icon: ${JSON.stringify(b.icon)},` : ''
const infoSeg = b.info ? ` info: ${JSON.stringify(b.info)},` : ''
push(` { lv: ${b.lv}, attrs: ${JSON.stringify(b.attrs)},${nameSeg}${iconSeg}${infoSeg} },`)
} }
push(` ],`) push(` ],`)
} }

View File

@@ -9,7 +9,18 @@
<el-form-item label="等级 lv"> <el-form-item label="等级 lv">
<el-input-number v-model="b.lv" :min="1" :max="20" :controls="false" style="width: 100%" /> <el-input-number v-model="b.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item label="名称 name">
<el-input v-model="b.name" placeholder="如 钢铁壁垒(缺省不出图标)" />
</el-form-item>
<el-form-item label="图标 icon">
<el-select v-model="b.icon" clearable filterable allow-create placeholder="gui/ui7 帧名(缺省该档不出图标)">
<el-option v-for="opt in SKILL_ICON_OPTIONS" :key="opt" :label="opt" :value="opt" />
</el-select>
</el-form-item>
</div> </div>
<el-form-item label="自定义描述 info">
<el-input v-model="b.info" type="textarea" :rows="1" placeholder="可选;占位符 {lv} 替换为解锁等级;缺省按属性自动生成效果文案" />
</el-form-item>
<el-form-item label="属性加成"> <el-form-item label="属性加成">
<div style="width: 100%"> <div style="width: 100%">
<div v-for="(a, i) in attrList(b)" :key="i" style="display: flex; gap: 8px; margin-bottom: 6px"> <div v-for="(a, i) in attrList(b)" :key="i" style="display: flex; gap: 8px; margin-bottom: 6px">