feat: 完成天赋系统重做与UI界面优化

1.  新增天赋等级配置与经验升级逻辑,支持连升多级
2.  重构天赋UI界面,添加等级进度条与页签状态显示
3.  优化技能卡片UI,新增技能名称显示与等级格式调整
4.  调整英雄界面布局,修复UI错位问题
5.  新增页签背景预制件与天赋道具UI样式
This commit is contained in:
panFD
2026-08-26 08:50:54 +08:00
parent 93a3c69d22
commit 5b965bba60
13 changed files with 9888 additions and 6078 deletions

View File

@@ -9,7 +9,7 @@ import { GameScoreStats } from "./config/HeroAttrs";
import { mLogger } from "./Logger";
import { gameDataSync } from "./GameDataSync";
import { HERO_UPGRADE_NEED_FRAGMENTS } from "./config/GameSet";
import { TalentSet, getTalentUpgradeCost, isTalentTierUnlocked } from "./config/TalentSet";
import { TalentSet, TALENT_LEVEL_MAX, getTalentLevelExp, getTalentUpgradeCost, isTalentTierUnlocked } from "./config/TalentSet";
/**
* 用远程数据覆盖本地数据(统一方法)
@@ -40,6 +40,10 @@ export interface GameDate {
talents?: Record<number, number>,
/** 当前可用天赋点数(升级天赋消耗,经验/等级系统接入后由其产出) */
talent_points?: number,
/** 天赋当前等级(0 起步,区间 0~29,共 30 级) */
talent_lv?: number,
/** 天赋当前等级已累计的经验值 */
talent_exp?: number,
}
export interface CloudData {
openid: string;
@@ -130,6 +134,10 @@ export class SingletonModuleComp extends ecs.Comp {
talents: {} as Record<number, number>,
/** 可用天赋点数 */
talent_points: 0,
/** 天赋等级(0 起步) */
talent_lv: 0,
/** 当前等级已累计经验 */
talent_exp: 0,
}
vmdata: any = {
@@ -415,6 +423,46 @@ export class SingletonModuleComp extends ecs.Comp {
oops.message.dispatchEvent(GameEvent.TALENT_UPDATE);
}
/**
* 获取天赋等级(0 起步,区间 0~29,共 30 级)
*/
getTalentLevel(): number {
return this.data.talent_lv ?? 0;
}
/**
* 获取当前等级已累计经验
*/
getTalentExp(): number {
return this.data.talent_exp ?? 0;
}
/**
* 增加天赋经验并自动升级(可连升多级;每升 1 级产出 1 点天赋点)
* Why: 升级阈值查 TALENT_LEVEL_EXP 配置表,满级(30 级)后不再累计经验。
* @param count 增加的经验值(正数)
*/
addTalentExp(count: number): void {
if (count <= 0) return;
let lv = this.getTalentLevel();
if (lv >= TALENT_LEVEL_MAX) return;
let exp = this.getTalentExp() + count;
// 支持一次加经验连升多级
while (lv < TALENT_LEVEL_MAX) {
const need = getTalentLevelExp(lv);
if (need < 0 || exp < need) break;
exp -= need;
lv += 1;
this.addTalentPoints(1);
}
// 满级后清空经验,进度条显示为满
if (lv >= TALENT_LEVEL_MAX) exp = 0;
this.data.talent_lv = lv;
this.data.talent_exp = exp;
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.TALENT_UPDATE);
}
/**
* 消耗天赋点升级天赋(升级即激活,下一局生效)
* @param uuid 天赋 uuid

View File

@@ -285,6 +285,26 @@ export function getTalentUpgradeCost(curLv: number): number {
return TALENT_UPGRADE_COST[curLv];
}
/** 天赋等级上限(不含):等级从 0 起步,区间 0~29,共 30 级;每升 1 级产出 1 点天赋点 */
export const TALENT_LEVEL_MAX = 30;
/** 天赋升级经验表:索引为当前等级(0~28),值为升到下一级所需经验 */
export const TALENT_LEVEL_EXP: number[] = [
100, 120, 140, 170, 200, 240, 280, 330, 380, 440,
510, 580, 660, 750, 850, 960, 1080, 1210, 1350, 1500,
1670, 1850, 2050, 2270, 2510, 2770, 3060, 3380, 3730,
];
/**
* 查询天赋升到下一级所需经验
* @param curLv 当前等级(0 起步)
* @returns 升到下一级所需经验;已满级或非法等级返回 -1
*/
export function getTalentLevelExp(curLv: number): number {
if (curLv < 0 || curLv >= TALENT_LEVEL_EXP.length) return -1;
return TALENT_LEVEL_EXP[curLv];
}
/** 阶位解锁门槛:前一阶累计投入 ≥ 该点数才能升级下一阶天赋 */
export const TALENT_TIER_UNLOCK_THRESHOLD = 10;

View File

@@ -30,6 +30,10 @@ export class SSkillBoxComp extends CCComp {
@property(Label)
private lv_label: Label = null;
/** 技能名称标签 */
@property(Label)
private name_label: Label = null;
/** 锁定遮罩节点(active 时表示技能未激活) */
@property({ type: Node })
private locked: Node = null;
@@ -40,6 +44,8 @@ export class SSkillBoxComp extends CCComp {
private card_lv: number = 1;
/** 卡牌自定义图标ID(优先级最高,未定义则按 trigger_type 自动取) */
private card_icon?: string;
/** 技能名称(装备卡取卡牌名,英雄图鉴取技能名) */
private skill_name: string = "";
/** 触发类型(默认即时,保持向后兼容) */
private trigger_type: CardTriggerType = CardTriggerType.Instant;
/** 驻场技能词条列表(uuid 指向 FieldSkillSet 底座,value 为生效数值) */
@@ -64,8 +70,10 @@ export class SSkillBoxComp extends CCComp {
this.s_uuid = config.skill ?? uuid; // 获取实际的技能 UUID
this.card_icon = config.icon; // 保存卡牌自定义图标(优先级最高)
this.trigger_type = config.trigger_type ?? CardTriggerType.Instant;
this.skill_name = config.name ?? "";
} else {
this.s_uuid = uuid;
this.skill_name = "";
}
this.updateUI();
@@ -89,13 +97,16 @@ export class SSkillBoxComp extends CCComp {
/**
* 设置技能激活状态:
* - locked = true → 显示锁定遮罩,技能未激活
* - locked = false → 隐藏锁定遮罩,技能已激活
* - locked = true → 显示锁定遮罩,技能未激活(隐藏等级标签)
* - locked = false → 隐藏锁定遮罩,技能已激活(恢复等级标签)
*/
setLocked(locked: boolean) {
if (this.locked) {
this.locked.active = locked;
}
if (this.lv_label) {
this.lv_label.node.active = !locked;
}
}
/**
@@ -119,11 +130,34 @@ export class SSkillBoxComp extends CCComp {
});
}
/**
* 解析技能名称:
* 优先取显式设置的卡牌名;否则按图标来源从 SkillSet / FieldSkillSet 取技能名。
*/
private resolveSkillName(): string {
if (this.skill_name) return this.skill_name;
if (this.hero_icon_source === "field") {
return this.s_uuid ? (FieldSkillSet[this.s_uuid]?.name ?? "") : "";
}
if (this.hero_icon_source === "skill") {
return this.s_uuid ? (SkillSet[this.s_uuid]?.name ?? "") : "";
}
if (this.trigger_type === CardTriggerType.Interval) {
return this.s_uuid ? (SkillSet[this.s_uuid]?.name ?? "") : "";
}
if (this.trigger_type === CardTriggerType.Field) {
const fieldUuid = this.field?.[0]?.uuid;
return fieldUuid ? (FieldSkillSet[fieldUuid]?.name ?? "") : "";
}
return "";
}
/**
* 更新 UI:
* - 背景:根据技能等级切换对应颜色子节点显示
* - 图标:从 uicons 图集获取
* - 等级标签:显示技能卡等级
* - 名称标签:显示技能名称
* - 等级标签:显示技能等级(Lv.x 样式)
*/
updateUI() {
// 按技能等级切换背景颜色节点(与 getLvColor 等级配色一致)
@@ -158,9 +192,14 @@ export class SSkillBoxComp extends CCComp {
}
}
// 显示技能等级
// 显示技能名称
if (this.name_label) {
this.name_label.string = this.resolveSkillName();
}
// 显示技能等级(Lv.x 样式)
if (this.lv_label) {
this.lv_label.string = `${this.card_lv}`;
this.lv_label.string = `Lv.${this.card_lv}`;
}
}

View File

@@ -13,13 +13,13 @@
* - TalentItemComp —— 单张迷你升级卡
* - smc —— 天赋等级表/点数/升级/重置
*/
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, UITransform } from "cc";
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, ProgressBar, UITransform } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import {
TalentSet, TalentTier,
TALENT_TIER_UNLOCK_THRESHOLD,
getTalentTierInvestedPoints, getTalentUpgradeCost, isTalentTierUnlocked,
getTalentLevelExp, getTalentTierInvestedPoints, getTalentUpgradeCost, isTalentTierUnlocked,
} from "../common/config/TalentSet";
import { GameEvent } from "../common/config/GameEvent";
import { UIID } from "../common/config/GameUIConfig";
@@ -39,6 +39,12 @@ const TIER_TAB_TEXT: Record<number, { title: string; lockName: string }> = {
/** 阶位页签选中高亮子节点名(可选,编辑器侧搭建) */
const TAB_SELECTED_CHILD = "sp_on";
/** 阶位页签锁定图标子节点名(可选;阶位未解锁时显示) */
const TAB_LOCKED_CHILD = "locked";
/** 阶位页签未选中态子节点名(可选;非当前选中页签时显示) */
const TAB_INACTIVE_CHILD = "noactive";
/** TalentsComp —— 天赋加点页组件(阶位 Tab 单屏聚焦) */
@ccclass('TalentsComp')
@ecs.register('Talents', false)
@@ -53,6 +59,12 @@ export class TalentsComp extends CCComp {
@property({ type: Label, tooltip: "页头可用天赋点" })
lbl_points: Label = null!;
@property({ type: Label, tooltip: "天赋等级文案(Lv.x 样式)" })
lbl_lv: Label = null!;
@property({ type: ProgressBar, tooltip: "当前等级经验进度条" })
progress_exp: ProgressBar = null!;
@property({ type: Node, tooltip: "一阶页签节点" })
tab_basic: Node = null!;
@@ -129,7 +141,18 @@ export class TalentsComp extends CCComp {
if (!this.talents_content || !this.prefab_talent_item) return;
const levels = smc.getTalentLevels();
if (this.lbl_points) this.lbl_points.string = `天赋点 ${smc.getTalentPoints()}`;
if (this.lbl_points) this.lbl_points.string = `${smc.getTalentPoints()}`;
// 天赋等级(Lv.x)与当前等级经验进度
const talentLv = smc.getTalentLevel();
if (this.lbl_lv) this.lbl_lv.string = `Lv.${talentLv}`;
if (this.progress_exp) {
const needExp = getTalentLevelExp(talentLv);
// 满级后进度条显示为满
this.progress_exp.progress = needExp > 0
? Math.min(1, smc.getTalentExp() / needExp)
: 1;
}
// 仅首次进入时默认选中已解锁的最高阶,后续刷新保持玩家当前选择
if (!this.inited) {
@@ -142,21 +165,30 @@ export class TalentsComp extends CCComp {
this.refreshGrid(levels);
}
/** 页签文案 + 解锁进度 + 选中高亮(sp_on 子节点可选) */
/** 页签文案 + 解锁进度 + 选中高亮(sp_on)+ 锁定图标(locked)+ 未选中态(noactive) */
private refreshTabs(levels: Record<number, number>): void {
for (const [tier, tab] of this.tabNodes) {
const text = TIER_TAB_TEXT[tier];
const unlocked = isTalentTierUnlocked(levels, tier);
if (tab.label) {
if (tier === TalentTier.Basic) {
if (unlocked) {
tab.label.string = text.title;
} else {
const unlocked = isTalentTierUnlocked(levels, tier);
const invested = getTalentTierInvestedPoints(levels, (tier - 1) as TalentTier);
tab.label.string = unlocked ? text.title : `${text.title} ${invested}/${TALENT_TIER_UNLOCK_THRESHOLD}`;
tab.label.string = `${text.title} ${invested}/${TALENT_TIER_UNLOCK_THRESHOLD}`;
}
}
// 锁定图标:该阶未解锁时显示,解锁后隐藏(一阶恒解锁,节点可缺省)
const locked = tab.node?.getChildByName(TAB_LOCKED_CHILD);
if (locked) locked.active = !unlocked;
const selected = tier === this.curTier;
// 未选中态节点:非当前激活页签时显示(与 sp_on 选中高亮互斥)
const inactive = tab.node?.getChildByName(TAB_INACTIVE_CHILD);
if (inactive) inactive.active = !selected;
tab.node?.getChildByName(TAB_SELECTED_CHILD)?.setPosition(0, 0);
tab.node?.getChildByName(TAB_SELECTED_CHILD) && (tab.node.getChildByName(TAB_SELECTED_CHILD)!.active = tier === this.curTier);
tab.node?.getChildByName(TAB_SELECTED_CHILD) && (tab.node.getChildByName(TAB_SELECTED_CHILD)!.active = selected);
}
}