Files
pixelheros/assets/script/game/map/TIBoxComp.ts
pan 1634f6a528 feat(talent-ui): 优化天赋界面展示逻辑与数据显示
1.  天赋总览页新增各层已投入点数展示
2.  天赋详情弹窗重构前置与层位校验UI:
    - 拆分层位需求与软前置展示模块
    - 优化进度显示逻辑,避免数值歧义
    - 修正前置等级展示为当前/需求格式
3.  更新相关注释与文档说明
2026-09-20 16:04:09 +08:00

248 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file TIBoxComp.ts
* @description 天赋详情弹窗组件(UI 视图层,TIBox.prefab 载体)
*
* 职责:
* 1. 展示天赋图标 / 名称 / 当前等级 / 条幅说明(getTalentDisplayText)。
* 2. 展示层位需求行:前置层位名称(如:1 级天赋)、投入进度(如:5/5)、解锁勾图标(解锁亮 On,未解锁亮 Off)。
* 3. 展示软前置行:前置天赋名称、等级进度(当前/需求,如:0/5)、前置已达成勾图标(达成亮 On,未达成亮 Off)。
* 4. 加点按钮:仅在可加点(层位已解锁 + 前置满足 + 未满级 + 点数足够)时显示;
* 不满足条件或已满级时隐藏,弹窗仍可正常打开查看。
* 5. 加点成功后通过 TALENT_UPDATE 事件驱动 TalentsComp 全量刷新,并同步刷新本弹窗。
*
* 使用(oops.gui 模块):
* oops.gui.open(UIID.TIBox, { uuid: 9001 });
*
* 依赖:
* - TalentSet(TalentConfig 配置与校验:isTalentTierUnlocked/isTalentPrereqMet/getTalentUpgradeCost)
* - smc —— 天赋等级表 / 点数 / upgradeTalent / 图标图集
* - outlineRich —— RichText 条幅描边
*/
import { _decorator, EventTouch, Label, Node, NodeEventType, RichText, Sprite, SpriteFrame } 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, TalentConfig, TalentTier, TALENT_TIER_UNLOCK_THRESHOLD, getTalentTierInvestedPoints, getTalentUpgradeCost, isTalentPrereqMet, isTalentTierUnlocked } from "../common/config/TalentSet";
import { GameEvent } from "../common/config/GameEvent";
import { HRoleName } from "../common/config/heroSet";
import { outlineRich } from "../common/RichOutline";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "db://oops-framework/core/Oops";
const { ccclass, property } = _decorator;
/** TIBox 弹窗初始化参数 */
export interface TIBoxParams {
/** 天赋 uuid(TalentSet key) */
uuid: number;
}
/** TIBoxComp —— 天赋详情弹窗(任何状态均可点开,不可加点时隐藏加点按钮) */
@ccclass('TIBoxComp')
@ecs.register('TIBoxComp', false)
export class TIBoxComp extends CCComp {
@property({ type: Sprite, tooltip: "天赋图标" })
icon: Sprite = null!;
@property({ type: Label, tooltip: "天赋名称" })
lbl_name: Label = null!;
@property({ type: Label, tooltip: "当前天赋等级(当前/满级)" })
lbl_level: Label = null!;
@property({ type: Label, tooltip: "前置天赋名称(无前置时隐藏前置行)" })
lbl_prereq_name: Label = null!;
@property({ type: Label, tooltip: "前置天赋等级进度(当前/需求,如:0/5)" })
lbl_prereq_lv: Label = null!;
@property({ type: Node, tooltip: "前置已达成勾图标(On)" })
node_prereq_check_on: Node = null!;
@property({ type: Node, tooltip: "前置未达成勾图标(Off)" })
node_prereq_check_off: Node = null!;
@property({ type: Node, tooltip: "前置行容器(无前置天赋时整体隐藏)" })
node_prereq: Node = null!;
@property({ type: Node, tooltip: "层位需求行容器(第 1 层天赋无层位需求,整体隐藏)" })
node_tier: Node = null!;
@property({ type: Label, tooltip: "前置层位名称(如:1 级天赋)" })
lbl_tier_name: Label = null!;
@property({ type: Label, tooltip: "前一层投入进度(已投入/门槛,如:5/5)" })
lbl_tier_progress: Label = null!;
@property({ type: Node, tooltip: "层位已解锁勾图标(On)" })
node_tier_check_on: Node = null!;
@property({ type: Node, tooltip: "层位未解锁勾图标(Off)" })
node_tier_check_off: Node = null!;
@property({ type: RichText, tooltip: "条幅说明(当前已生效数值 / 每级引导)" })
richtext_info: RichText = null!;
@property({ type: Node, tooltip: "加点按钮(不可加点或满级时隐藏)" })
node_add: Node = null!;
@property({ type: Node, tooltip: "关闭遮罩/关闭按钮(点击关闭弹窗)" })
node_close: Node = null!;
/** 当前展示的天赋 uuid */
private talent_uuid: number = 0;
protected onLoad(): void {
this.node_add?.on(NodeEventType.TOUCH_END, this.onAddTap, this);
this.node_close?.on(NodeEventType.TOUCH_END, this.onCloseTap, this);
}
protected onDestroy(): void {
this.node_add?.targetOff(this);
this.node_close?.targetOff(this);
oops.message.off(GameEvent.TALENT_UPDATE, this.refresh, this);
}
/**
* oops.gui 添加到舞台回调(框架约定入口)
* @param params 见 TIBoxParams
*/
onAdded(params: TIBoxParams): void {
this.talent_uuid = params?.uuid ?? 0;
// 加点成功后同步刷新本弹窗(等级/按钮状态),与 TalentsComp 同一事件源
oops.message.on(GameEvent.TALENT_UPDATE, this.refresh, this);
this.refresh();
}
/** 全量刷新弹窗内容 */
private refresh(): void {
const cfg = TalentSet[this.talent_uuid];
if (!cfg) return;
const levels = smc.getTalentLevels();
const lv = levels[this.talent_uuid] ?? 0;
// 图标:技能图标图集(gui/ui7)优先,资源目录回退(与 TalentItemComp 口径一致)
if (this.icon && cfg.icon) {
let frame: SpriteFrame | null = null;
if (smc.skillIconsAtlas) {
frame = smc.skillIconsAtlas.getSpriteFrame(cfg.icon);
}
if (!frame) {
frame = oops.res.get("game/heros/cards/" + cfg.icon, SpriteFrame) as SpriteFrame;
}
if (frame) this.icon.spriteFrame = frame;
}
if (this.lbl_name) this.lbl_name.string = cfg.name;
if (this.lbl_level) this.lbl_level.string = `${lv}/${cfg.max_lv}`;
// 条幅说明:已加点显示当前生效数值,未加点显示每级引导
if (this.richtext_info) this.richtext_info.string = outlineRich(this.buildDetailText(cfg, lv));
// 层位需求行:第 1 层恒解锁无需求,整体隐藏;其余层展示前一层名称与投入进度(如:1 级天赋 5/5)
const hasTierNeed = cfg.tier > TalentTier.L1;
if (this.node_tier) this.node_tier.active = hasTierNeed;
if (hasTierNeed) {
const prevTier = (cfg.tier - 1) as TalentTier;
const invested = getTalentTierInvestedPoints(levels, prevTier);
const tierMet = isTalentTierUnlocked(levels, cfg.tier);
if (this.lbl_tier_name) this.lbl_tier_name.string = `${prevTier} 级天赋`;
// 进度超门槛时截断显示(如已投入 7 点仍显示 5/5,避免 7/5 的歧义观感)
const shown = Math.min(invested, TALENT_TIER_UNLOCK_THRESHOLD);
if (this.lbl_tier_progress) this.lbl_tier_progress.string = `${shown}/${TALENT_TIER_UNLOCK_THRESHOLD}`;
if (this.node_tier_check_on) this.node_tier_check_on.active = tierMet;
if (this.node_tier_check_off) this.node_tier_check_off.active = !tierMet;
}
// 软前置行:无前置整体隐藏;有前置展示名称/需求等级/达成勾
const hasPrereq = !!(cfg.prereq_uuid && cfg.prereq_lv);
if (this.node_prereq) this.node_prereq.active = hasPrereq;
if (hasPrereq) {
const preCfg = TalentSet[cfg.prereq_uuid!];
const preMet = isTalentPrereqMet(levels, cfg);
if (this.lbl_prereq_name) this.lbl_prereq_name.string = preCfg ? preCfg.name : `天赋${cfg.prereq_uuid}`;
// 前置等级进度(当前/需求,如:0/5);超需求时截断显示,避免 7/5 的歧义观感
const preLv = Math.min(levels?.[cfg.prereq_uuid!] ?? 0, cfg.prereq_lv!);
if (this.lbl_prereq_lv) this.lbl_prereq_lv.string = `${preLv}/${cfg.prereq_lv}`;
if (this.node_prereq_check_on) this.node_prereq_check_on.active = preMet;
if (this.node_prereq_check_off) this.node_prereq_check_off.active = !preMet;
}
// 加点按钮:层位解锁 + 前置满足 + 未满级 + 点数足够时才显示
if (this.node_add) this.node_add.active = this.canUpgrade(lv, cfg.tier, levels);
}
/**
* 按等级精准渲染天赋 info 模板({value}/{value2} 占位替换为实际数值)。
* Lv0 时按 1 级数值预览(引导加点)。
* @param cfg 天赋配置
* @param lv 当前等级
* @returns 替换占位后的完整描述文本
*/
private buildPreciseText(cfg: TalentConfig, lv: number): string {
const mult = lv > 0 ? lv : 1;
const v1 = cfg.bonuses[0]?.per_lv ?? cfg.t_num_reduce_per_lv ?? cfg.extra_trigger_chance_per_lv ?? 0;
const v2 = cfg.bonuses[1]?.per_lv ?? 0;
return cfg.info
.replace("{value}", `${v1 * mult}`)
.replace("{value2}", `${v2 * mult}`);
}
/**
* 构建天赋详情条幅文本:精准描述 + 下一级预览 + 满级提示
* @param cfg 天赋配置
* @param lv 当前等级
*/
private buildDetailText(cfg: TalentConfig, lv: number): string {
const lines: string[] = [];
// 精准效果描述:使用 cfg.info 模板按当前等级替换数值占位
lines.push(this.buildPreciseText(cfg, lv));
// 未满级:追加下一级预览
if (lv < cfg.max_lv) {
lines.push(`<color=#E0A93D>下一级:${this.buildPreciseText(cfg, lv + 1)}</color>`);
} else {
lines.push(`<color=#FFD700>已满级</color>`);
}
// 职业限定提示
if (cfg.role !== undefined) {
lines.push(`<color=#AAAAAA>仅${HRoleName[cfg.role]}生效</color>`);
}
return lines.join("\n");
}
/**
* 当前是否可加点(与 smc.upgradeTalent 校验口径一致,仅做 UI 预判定)
* @param lv 当前等级
* @param tier 所属层位
* @param levels 天赋等级表
*/
private canUpgrade(lv: number, tier: number, levels: Record<number, number>): boolean {
const cfg = TalentSet[this.talent_uuid];
if (!cfg) return false;
if (lv >= cfg.max_lv) return false;
if (!isTalentTierUnlocked(levels, tier)) return false;
if (!isTalentPrereqMet(levels, cfg)) return false;
return smc.getTalentPoints() >= getTalentUpgradeCost(tier);
}
/** 加点按钮点击:升级成功后由 TALENT_UPDATE 事件驱动刷新(含本弹窗) */
private onAddTap(event?: EventTouch): void {
if (event) event.propagationStopped = true;
oops.audio.playEffect("music/button");
smc.upgradeTalent(this.talent_uuid);
}
/** 点击关闭 */
private onCloseTap(event?: EventTouch): void {
if (event) event.propagationStopped = true;
oops.audio.playEffect("music/button");
oops.gui.removeByNode(this.node);
}
/** ECS 组件移除时销毁节点(CCComp 抽象方法实现) */
reset(): void {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}