/** * @file TalentsComp.ts * @description 天赋加点页组件(UI 视图层,最小容器) * * 职责: * 1. 页头展示:可用天赋点 / 天赋等级(Lv.x)/ 当前等级经验进度。 * 2. 收集子树内全部 TalentItemComp(编辑器静态摆放),TALENT_UPDATE 事件驱动全量刷新。 * 3. 卡片点击统一校验(层位解锁 → 软前置 → 满级 → 点数),不满足走 smalltip 气泡; * 满足则弹出 CommonPrompt 确认加点(smc.upgradeTalent)。 * 4. 看广告重置入口:CommonPrompt 二次确认后调 smc.resetTalents() 退还全部点数。 * * 依赖: * - TalentSet(TalentTier/isTalentTierUnlocked/isTalentPrereqMet/getTalentTierInvestedPoints)—— 配置与层位校验 * - TalentItemComp —— 单张迷你升级卡(编辑器静态摆放,按 uuid 自驱动渲染) * - smc —— 天赋等级表/点数/升级/重置 */ import { _decorator, Label, Node, NodeEventType, ProgressBar } 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, TALENT_TIER_UNLOCK_THRESHOLD, getTalentDisplayText, getTalentLevelExp, getTalentTierInvestedPoints, getTalentUpgradeCost, isTalentPrereqMet, 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; /** 层位序号展示文案(提示文案用) */ const TIER_NAMES: Record = { 1: "第 1 层", 2: "第 2 层", 3: "第 3 层", 4: "第 4 层", 5: "第 5 层", }; /** 页签/卡片上的 smalltip 气泡节点名 */ const SMALL_TIP_CHILD = "smalltip"; /** TalentsComp —— 天赋加点页组件(最小容器:页头 + 编辑器静态卡片收集) */ @ccclass('TalentsComp') @ecs.register('Talents', false) 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: Label, tooltip: "当前等级经验数值(当前经验/升级所需经验)" }) lbl_exp: Label = null!; @property({ type: Node, tooltip: "看广告重置按钮" }) btn_reset: Node = null!; /** 页内全部天赋卡(onLoad 时自动收集子树内所有 TalentItemComp,无需编辑器拖引) */ private talent_items: TalentItemComp[] = []; protected onLoad(): void { this.talent_items = this.node.getComponentsInChildren(TalentItemComp); for (const item of this.talent_items) { item.onTalentTap = (uuid: number) => this.onTalentTap(uuid); } 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 { this.btn_reset?.targetOff(this); } /** 全量刷新:页头点数 + 天赋等级/经验 + 全部静态卡片 */ public refreshUI(): void { const levels = smc.getTalentLevels(); if (this.lbl_points) { // 剩余/总点数:总点数 = 剩余 + 各层已投入 const remaining = smc.getTalentPoints(); let invested = 0; for (let tier = 1; tier <= 5; tier++) { invested += getTalentTierInvestedPoints(levels, tier); } this.lbl_points.string = `${remaining}/${remaining + invested}`; } // 天赋等级(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.lbl_exp) { const needExp = getTalentLevelExp(talentLv); // 满级后显示为 0/0,避免分母为 0 this.lbl_exp.string = needExp > 0 ? `${smc.getTalentExp()}/${needExp}` : `0/0`; } for (const item of this.talent_items) { item.updateItem(); } } /** * 获取挂载点下的 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); } /** * 按 uuid 找到对应的天赋卡节点(气泡锚点用) * @param uuid 天赋 uuid */ private findItemNode(uuid: number): Node | null { for (const item of this.talent_items) { if (item.talent_uuid === uuid) return item.node; } return null; } /** 卡片点击统一入口:层位未解锁/前置未满足/已满级/点数不足走 smalltip 气泡;其余弹出 CommonPrompt 确认加点,成功后事件驱动刷新 */ private onTalentTap(uuid: number): void { const cfg = TalentSet[uuid]; if (!cfg) return; const levels = smc.getTalentLevels(); const anchor = this.findItemNode(uuid); // 层位解锁:第 N 层要求第 N-1 层累计投入 ≥ 阈值 if (!isTalentTierUnlocked(levels, cfg.tier)) { this.showSmallTip(anchor, `${TIER_NAMES[cfg.tier - 1]}投入${TALENT_TIER_UNLOCK_THRESHOLD} 点解锁`); return; } // 软前置:前置天赋等级需达标 if (!isTalentPrereqMet(levels, cfg)) { const preCfg = TalentSet[cfg.prereq_uuid!]; const preName = preCfg ? preCfg.name : `天赋${cfg.prereq_uuid}`; this.showSmallTip(anchor, `需「${preName}」达到 Lv.${cfg.prereq_lv}`); return; } const lv = smc.getTalentLv(uuid); if (lv >= cfg.max_lv) { this.showSmallTip(anchor, "该天赋已满级"); return; } const cost = getTalentUpgradeCost(cfg.tier); if (cost <= 0) return; if (smc.getTalentPoints() < cost) { this.showSmallTip(anchor, "天赋点不足"); return; } // CommonPrompt 确认加点(UIID.Window) oops.gui.open(UIID.Window, { title: "天赋加点", content: `${cfg.name} Lv.${lv} → Lv.${lv + 1}\n消耗 ${cost} 点天赋点,获得 ${getTalentDisplayText(cfg, 1)}。是否加点?`, okWord: "加点", cancelWord: "取消", needCancel: true, maskClose: true, okFunc: () => { if (!smc.upgradeTalent(uuid)) { this.showSmallTip(anchor, "天赋点不足"); } }, }); } /** 看广告重置入口:CommonPrompt 二次确认(广告接入前先直接确认重置) */ private onResetTap(): void { oops.gui.open(UIID.Window, { title: "重置天赋", content: "观看广告后重置所有天赋等级,并全额退还已消耗的天赋点。是否继续?", okWord: "重置", cancelWord: "取消", needCancel: true, okFunc: () => smc.resetTalents(), }); } /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } }