Files
pixelheros/assets/script/game/map/TalentsComp.ts
pan df519fdc4e feat(talent): 重构天赋系统为三阶结构并实现全新UI
1.  重构天赋系统为一阶基础/二阶专精/三阶超强三阶结构,移除前置天赋要求改为阶位累计点数解锁
2.  新增阶位解锁校验逻辑与天赋点投入统计
3.  重做天赋UI为三阶Tab页签+迷你升级卡样式,支持状态展示与一键升级
4.  补充新的天赋配置与属性加成,更新天赋系统文档
5.  修复属性计算问题,新增暴击伤害与穿刺伤害加成支持
2026-08-25 16:29:11 +08:00

234 lines
9.9 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),点击卡/+ 徽标升级(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, 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,
} from "../common/config/TalentSet";
import { GameEvent } from "../common/config/GameEvent";
import { UIID } from "../common/config/GameUIConfig";
import { TalentItemComp } from "./TalentItemComp";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "db://oops-framework/core/Oops";
const { ccclass, property } = _decorator;
/** 阶位页签展示文案(锁定态追加投入进度) */
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";
/** 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: 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: 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 }> = new Map();
protected onLoad(): void {
this.tabNodes.set(TalentTier.Basic, { node: this.tab_basic, label: this.lbl_tab_basic });
this.tabNodes.set(TalentTier.Specialized, { node: this.tab_special, label: this.lbl_tab_special });
this.tabNodes.set(TalentTier.Super, { node: this.tab_super, label: this.lbl_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;
const levels = smc.getTalentLevels();
if (!isTalentTierUnlocked(levels, tier)) {
const prev = tier - 1;
const invested = getTalentTierInvestedPoints(levels, prev);
oops.gui.toast(`${TIER_TAB_TEXT[tier].lockName}投入 ${invested}/${TALENT_TIER_UNLOCK_THRESHOLD} 点解锁本阶`);
}
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()}`;
// 仅首次进入时默认选中已解锁的最高阶,后续刷新保持玩家当前选择
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 子节点可选) */
private refreshTabs(levels: Record<number, number>): void {
for (const [tier, tab] of this.tabNodes) {
const text = TIER_TAB_TEXT[tier];
if (tab.label) {
if (tier === TalentTier.Basic) {
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.node?.getChildByName(TAB_SELECTED_CHILD)?.setPosition(0, 0);
tab.node?.getChildByName(TAB_SELECTED_CHILD) && (tab.node.getChildByName(TAB_SELECTED_CHILD)!.active = tier === this.curTier);
}
}
/** 重建当前阶位的卡片列表(每阶 ≤10 条,直接全量实例化) */
private refreshGrid(levels: Record<number, number>): void {
this.talents_content.removeAllChildren();
const unlocked = isTalentTierUnlocked(levels, this.curTier);
const points = smc.getTalentPoints();
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 cost = getTalentUpgradeCost(lv);
// 可升级 = 阶位解锁 + 未满级 + 点数足够(+ 徽标显示依据)
const canUp = unlocked && lv < cfg.max_lv && cost > 0 && points >= cost;
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, canUp);
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;
}
}
/** 卡片点击统一入口:阶位未解锁/已满级/点数不足分别 toast,成功升级由事件驱动刷新 */
private onTalentTap(uuid: number): void {
const cfg = TalentSet[uuid];
if (!cfg) return;
const levels = smc.getTalentLevels();
if (!isTalentTierUnlocked(levels, cfg.tier)) {
const invested = getTalentTierInvestedPoints(levels, (cfg.tier - 1) as TalentTier);
oops.gui.toast(`${TIER_TAB_TEXT[cfg.tier].lockName}投入 ${invested}/${TALENT_TIER_UNLOCK_THRESHOLD} 点解锁本阶`);
return;
}
if (smc.getTalentLv(uuid) >= cfg.max_lv) {
oops.gui.toast("该天赋已满级");
return;
}
if (!smc.upgradeTalent(uuid)) {
oops.gui.toast("天赋点不足");
}
}
/** 看广告重置入口:CommonPrompt 二次确认(广告接入前先直接确认重置) */
private onResetTap(): void {
oops.gui.open(UIID.Window, {
title: "重置天赋",
content: "观看广告后重置所有天赋等级,并全额退还已消耗的天赋点。是否继续?",
okWord: "重置",
cancelWord: "取消",
needCancel: true,
okFunc: () => smc.resetTalents(),
});
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();
}
}