Files
pixelheros/assets/script/game/map/TIBoxComp.ts
panFD 45e3e8712b feat(ui): 新增天赋详情弹窗功能
重构天赋加点逻辑:将卡片点击校验与加点弹窗逻辑内聚到TIBoxComp中,TalentsComp仅负责打开弹窗。新增天赋详情弹窗,支持展示天赋信息、前置条件校验,并根据玩家状态动态控制加点按钮显隐。
2026-09-19 22:19:12 +08:00

199 lines
8.3 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. 展示软前置行:前置天赋名称、需求等级、前置已达成勾图标(达成亮 On,未达成亮 Off)。
* 3. 加点按钮:仅在可加点(层位已解锁 + 前置满足 + 未满级 + 点数足够)时显示;
* 不满足条件或已满级时隐藏,弹窗仍可正常打开查看。
* 4. 加点成功后通过 TALENT_UPDATE 事件驱动 TalentsComp 全量刷新,并同步刷新本弹窗。
*
* 使用(oops.gui 模块):
* oops.gui.open(UIID.TIBox, { uuid: 9001 });
*
* 依赖:
* - TalentSet(配置与校验:isTalentTierUnlocked/isTalentPrereqMet/getTalentUpgradeCost/getTalentDisplayText)
* - 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, getTalentDisplayText, 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: "前置天赋需求等级" })
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: 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));
// 软前置行:无前置整体隐藏;有前置展示名称/需求等级/达成勾
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}`;
if (this.lbl_prereq_lv) this.lbl_prereq_lv.string = `${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);
}
/**
* 构建天赋详情条幅文本:当前生效数值 + 下一级预览 + 满级提示
* @param cfg 天赋配置
* @param lv 当前等级
*/
private buildDetailText(cfg: typeof TalentSet[number], lv: number): string {
const lines: string[] = [];
// 当前已生效数值(Lv0 显示每级引导)
lines.push(getTalentDisplayText(cfg, lv));
// 未满级:追加下一级预览
if (lv < cfg.max_lv) {
lines.push(`<color=#E0A93D>下一级:${getTalentDisplayText(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();
}
}
}