Files
pixelheros/assets/script/game/map/TalentsComp.ts
panFD fcb9f293dc refactor(talents): 优化天赋页签展示与投入点数显示
1.  更新阶位页签文案配置,调整锁提示文本
2.  新增各阶已投入点数标签的序列化与映射
3.  重构页签刷新逻辑,分离文案和投入点数展示
4.  简化未解锁阶位的提示文案
2026-09-04 23:07:43 +08:00

303 lines
13 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 TalentsComp.ts
* @description 天赋加点页组件(UI 视图层)
*
* 职责:
* 1. 三阶页签(Ⅰ 基础 / Ⅱ 专精 / Ⅲ 超强)状态机:单屏聚焦当前阶位的迷你天赋卡。
* 2. 页签常显解锁进度(前阶投入 x/10),点击天赋卡弹出 CommonPrompt 确认加点(smc.upgradeTalent)。
* 3. 监听 GameEvent.TALENT_UPDATE 全量重渲染(23 条卡片,无需虚拟列表)。
* 4. 看广告重置入口:CommonPrompt 二次确认后调 smc.resetTalents() 退还全部点数。
*
* 依赖:
* - TalentSet(TalentSet/TalentTier/isTalentTierUnlocked/getTalentTierInvestedPoints)—— 配置与阶位校验
* - TalentItemComp —— 单张迷你升级卡
* - smc —— 天赋等级表/点数/升级/重置
*/
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,
getTalentDisplayText, getTalentLevelExp, getTalentTierInvestedPoints, getTalentUpgradeCost, isTalentTierUnlocked,
} from "../common/config/TalentSet";
import { GameEvent } from "../common/config/GameEvent";
import { UIID } from "../common/config/GameUIConfig";
import { SmallTipComp } from "./SmallTipComp";
import { TalentItemComp } from "./TalentItemComp";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "db://oops-framework/core/Oops";
const { ccclass, property } = _decorator;
/** 阶位页签展示文案(lockName 用于未解锁提示) */
const TIER_TAB_TEXT: Record<number, { title: string; lockName: string }> = {
[TalentTier.Basic]: { title: "基础天赋", lockName: "基础天赋" },
[TalentTier.Specialized]: { title: "专精天赋", lockName: "基础天赋" },
[TalentTier.Super]: { title: "传说天赋", lockName: "专精天赋" },
};
/** 阶位页签选中高亮子节点名(可选,编辑器侧搭建) */
const TAB_SELECTED_CHILD = "sp_on";
/** 阶位页签锁定图标子节点名(可选;阶位未解锁时显示) */
const TAB_LOCKED_CHILD = "locked";
/** 阶位页签未选中态子节点名(可选;非当前选中页签时显示) */
const TAB_INACTIVE_CHILD = "noactive";
/** 页签/卡片上的 smalltip 气泡节点名 */
const SMALL_TIP_CHILD = "smalltip";
/** TalentsComp —— 天赋加点页组件(阶位 Tab 单屏聚焦) */
@ccclass('TalentsComp')
@ecs.register('Talents', false)
export class TalentsComp extends CCComp {
@property({ type: Node, tooltip: "当前阶位卡片列表容器(ScrollView content,Grid 三列)" })
talents_content: Node = null!;
@property({ type: Prefab, tooltip: "迷你天赋卡预制(TalentItemComp)" })
prefab_talent_item: Prefab = null!;
@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!;
@property({ type: Node, tooltip: "二阶页签节点" })
tab_special: Node = null!;
@property({ type: Node, tooltip: "三阶页签节点" })
tab_super: Node = null!;
@property({ type: Label, tooltip: "一阶页签文案" })
lbl_tab_basic: Label = null!;
@property({ type: Label, tooltip: "二阶页签文案" })
lbl_tab_special: Label = null!;
@property({ type: Label, tooltip: "三阶页签文案" })
lbl_tab_super: Label = null!;
@property({ type: Label, tooltip: "一阶已投入点数" })
lbl_invest_basic: Label = null!;
@property({ type: Label, tooltip: "二阶已投入点数" })
lbl_invest_special: Label = null!;
@property({ type: Label, tooltip: "三阶已投入点数" })
lbl_invest_super: Label = null!;
@property({ type: Node, tooltip: "看广告重置按钮" })
btn_reset: Node = null!;
/** 当前选中的阶位(首次进入默认为已解锁的最高阶,之后由玩家切换) */
private curTier: TalentTier = TalentTier.Basic;
/** 是否已完成首次默认阶位选择 */
private inited: boolean = false;
/** 阶位 → 页签节点/文案/投入点数/气泡 映射(onLoad 时装配) */
private tabNodes: Map<TalentTier, { node: Node | null; label: Label | null; invest: Label | null; tip: SmallTipComp | null }> = new Map();
protected onLoad(): void {
this.tabNodes.set(TalentTier.Basic, { node: this.tab_basic, label: this.lbl_tab_basic, invest: this.lbl_invest_basic, tip: this.getSmallTip(this.tab_basic) });
this.tabNodes.set(TalentTier.Specialized, { node: this.tab_special, label: this.lbl_tab_special, invest: this.lbl_invest_special, tip: this.getSmallTip(this.tab_special) });
this.tabNodes.set(TalentTier.Super, { node: this.tab_super, label: this.lbl_tab_super, invest: this.lbl_invest_super, tip: this.getSmallTip(this.tab_super) });
for (const [tier, tab] of this.tabNodes) {
tab.node?.on(NodeEventType.TOUCH_END, () => this.selectTier(tier), this);
}
this.btn_reset?.on(NodeEventType.TOUCH_END, this.onResetTap, this);
}
protected onEnable(): void {
// 升级/重置/加点变动统一走 TALENT_UPDATE 事件驱动全量刷新
oops.message.on(GameEvent.TALENT_UPDATE, this.refreshUI, this);
this.refreshUI();
}
protected onDisable(): void {
oops.message.off(GameEvent.TALENT_UPDATE, this.refreshUI, this);
}
protected onDestroy(): void {
for (const [, tab] of this.tabNodes) {
tab.node?.targetOff(this);
}
this.btn_reset?.targetOff(this);
}
/** 切换阶位页签:锁定阶允许查看(卡片呈锁定态),不弹解锁提示 */
private selectTier(tier: TalentTier): void {
if (tier === this.curTier) return;
this.curTier = tier;
this.refreshUI();
}
/** 全量刷新:页头点数 + 页签解锁进度 + 当前阶位卡片列表 */
public refreshUI(): void {
if (!this.talents_content || !this.prefab_talent_item) return;
const levels = smc.getTalentLevels();
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) {
this.inited = true;
if (isTalentTierUnlocked(levels, TalentTier.Super)) this.curTier = TalentTier.Super;
else if (isTalentTierUnlocked(levels, TalentTier.Specialized)) this.curTier = TalentTier.Specialized;
}
this.refreshTabs(levels);
this.refreshGrid(levels);
}
/** 页签文案 + 各阶已投入点数 + 选中高亮(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) tab.label.string = text.title;
// 各阶已投入点数(该阶所有天赋等级之和)
if (tab.invest) tab.invest.string = `${getTalentTierInvestedPoints(levels, tier)}`;
// 锁定图标:该阶未解锁时显示,解锁后隐藏(一阶恒解锁,节点可缺省)
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 = selected);
}
}
/** 重建当前阶位的卡片列表(每阶 ≤10 条,直接全量实例化) */
private refreshGrid(levels: Record<number, number>): void {
this.talents_content.removeAllChildren();
const unlocked = isTalentTierUnlocked(levels, this.curTier);
const cfgs = Object.values(TalentSet)
.filter(cfg => cfg.tier === this.curTier)
.sort((a, b) => a.uuid - b.uuid);
let cardHeight = 0;
for (const cfg of cfgs) {
const lv = levels[cfg.uuid] ?? 0;
const itemNode = instantiate(this.prefab_talent_item);
this.talents_content.addChild(itemNode);
const comp = itemNode.getComponent(TalentItemComp);
if (!comp) continue;
comp.updateItem(cfg, lv, unlocked);
comp.onTalentTap = (uuid: number) => this.onTalentTap(uuid);
if (!cardHeight) cardHeight = itemNode.getComponent(UITransform)?.height ?? 0;
}
// Grid 三列手算容器高度(与旧版图鉴页口径一致,spacing 15 / 上下 padding 15)
const rows = Math.ceil(cfgs.length / 3);
if (cardHeight > 0 && rows > 0) {
const contentHeight = rows * cardHeight + (rows - 1) * 15 + 30;
const uiTransform = this.talents_content.getComponent(UITransform);
if (uiTransform) uiTransform.height = contentHeight;
}
}
/**
* 获取挂载点下的 smalltip 气泡组件(节点缺失或组件未挂时返回 null 并静默跳过提示)
* @param parent 气泡挂载的父节点
*/
private getSmallTip(parent: Node | null): SmallTipComp | null {
const tipNode = parent?.getChildByName(SMALL_TIP_CHILD);
return tipNode ? (tipNode.getComponent(SmallTipComp) ?? tipNode.addComponent(SmallTipComp)) : null;
}
/**
* 在指定节点下弹出 smalltip 气泡提示
* @param anchor 气泡挂载的父节点(优先页签,卡片点击时传卡片节点)
* @param text 提示文案
*/
private showSmallTip(anchor: Node | null, text: string): void {
const tip = this.getSmallTip(anchor);
tip?.show(text);
}
/** 卡片点击统一入口:未解锁/已满级/点数不足走 smalltip 气泡;其余弹出 CommonPrompt 确认加点,成功后事件驱动刷新 */
private onTalentTap(uuid: number): void {
const cfg = TalentSet[uuid];
if (!cfg) return;
const levels = smc.getTalentLevels();
const tab = this.tabNodes.get(cfg.tier);
if (!isTalentTierUnlocked(levels, cfg.tier)) {
const invested = getTalentTierInvestedPoints(levels, (cfg.tier - 1) as TalentTier);
this.showSmallTip(tab?.node ?? null, `${TIER_TAB_TEXT[cfg.tier].lockName}投入${TALENT_TIER_UNLOCK_THRESHOLD} 点解锁`);
return;
}
const lv = smc.getTalentLv(uuid);
if (lv >= cfg.max_lv) {
this.showSmallTip(tab?.node ?? null, "该天赋已满级");
return;
}
const cost = getTalentUpgradeCost(lv);
if (cost <= 0) return;
if (smc.getTalentPoints() < cost) {
this.showSmallTip(tab?.node ?? null, "天赋点不足");
return;
}
// CommonPrompt 确认加点(UIID.Window)
oops.gui.open(UIID.Window, {
title: "天赋加点",
content: `${cfg.name} Lv.${lv} → Lv.${lv + 1}\n消耗 <color=#E0A93D>${cost}</color> 点天赋点,获得 ${getTalentDisplayText(cfg, 1)}。是否加点?`,
okWord: "加点",
cancelWord: "取消",
needCancel: true,
maskClose: true,
okFunc: () => {
if (!smc.upgradeTalent(uuid)) {
this.showSmallTip(this.tabNodes.get(cfg.tier)?.node ?? null, "天赋点不足");
}
},
});
}
/** 看广告重置入口:CommonPrompt 二次确认(广告接入前先直接确认重置) */
private onResetTap(): void {
oops.gui.open(UIID.Window, {
title: "重置天赋",
content: "观看广告后重置所有天赋等级,并全额退还已消耗的天赋点。是否继续?",
okWord: "重置",
cancelWord: "取消",
needCancel: true,
okFunc: () => smc.resetTalents(),
});
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();
}
}