Files
pixelheros/assets/script/game/map/TalentsComp.ts
walkpan 2ee8eb097e feat(talent): 引入天赋碎片系统替换通用天赋点
- 新增 TalentFragmentType 枚举和 TalentFragmentInfo 接口定义碎片类型
- 在 SingletonModuleComp 中新增 talent_fragments 字段存储碎片库存,talent_points 改为可选字段以兼容旧存档
- 为每个天赋配置 fragmentType 指定升级所需的具体碎片类型
- 修改 TalentsComp 升级逻辑,从消耗天赋点改为扣除对应类型的碎片
- 重置天赋功能现在返还已消耗的碎片而非天赋点
- 更新界面显示,展示碎片库存摘要和具体消耗
2026-05-08 08:38:01 +08:00

353 lines
14 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. 根据传入参数判断是否可复活,切换"下一步"或"复活"按钮。
* 3. 计算单局总分并存储到 smc.vmdata.scores.score。
* 4. 提供"重新开始"和"退出"两个操作入口。
*
* 关键设计:
* - onAdded(args) 接收战斗结果参数Talents / rewards / game_data / can_revive
* - calculateTotalScore() 根据 ScoreWeights 配置加权计算各项得分。
* - restart() 和 Talents_end() 通过分发 MissionEnd / MissionStart 事件驱动游戏状态切换。
*
* 依赖:
* - smc.vmdata.scores —— 全局战斗统计数据
* - ScoreWeightsScoreSet—— 得分权重配置
* - GameEvent.MissionEnd / MissionStart —— 游戏生命周期事件
*/
import { _decorator, Node, Label, Button, ProgressBar, instantiate, Prefab } 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 { mLogger } from "../common/Logger";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { TalentConfig, TalentFragmentInfo, TalentFragmentType, TalentInfo, TalentType } from "../common/config/TalentSet";
const { ccclass, property } = _decorator;
/**
* TalentsComp —— 天赋系统界面组件
*
* 职责:
* 1. 展示玩家等级、当前经验、进度条、碎片库存。
* 2. 展示天赋列表及每个天赋的当前等级。
* 3. 处理天赋升级点击事件,扣除碎片并保存。
* 4. 处理重置天赋(看广告)功能。
*/
@ccclass('TalentsComp')
@ecs.register('Talents', false)
export class TalentsComp extends CCComp {
@property({ type: Node, tooltip: "标题节点" })
title_node: Node = null!;
@property({ type: Label, tooltip: "玩家等级文本,例如 'Lv.12'" })
lbl_level: Label = null!;
@property({ type: Label, tooltip: "经验文本,例如 '150/200'" })
lbl_exp: Label = null!;
@property({ type: ProgressBar, tooltip: "经验进度条" })
pb_exp: ProgressBar = null!;
@property({ type: Label, tooltip: "当前碎片库存摘要文本" })
lbl_points: Label = null!;
@property({ type: Node, tooltip: "天赋列表容器,用于动态添加天赋项" })
talents_content: Node = null!;
@property({ type: Prefab, tooltip: "天赋项预制体\n预制体结构要求:\n- 根节点\n - lbl_name (Label): 天赋名称\n - lbl_desc (Label): 天赋描述\n - lbl_level (Label): 当前等级\n - lbl_cost (Label): 升级消耗\n - btn_upgrade (Button): 升级按钮\n - pb_level (ProgressBar或一组节点): 等级进度展示(可选)" })
prefab_talent_item: Prefab = null!;
@property({ type: Button, tooltip: "看广告重置天赋按钮" })
btn_reset: Button = null!;
@property({ type: Button, tooltip: "返回按钮" })
btn_close: Button = null!;
/** 调试日志开关 */
debugMode: boolean = false;
/** 最大玩家等级 */
private readonly MAX_PLAYER_LEVEL = 30;
protected onLoad(): void {
// 绑定按钮事件
if (this.btn_reset) {
this.btn_reset.node.on(Button.EventType.CLICK, this.onResetClicked, this);
}
if (this.btn_close) {
this.btn_close.node.on(Button.EventType.CLICK, this.onCloseClicked, this);
}
}
onAdded(args: any) {
this.refreshUI();
}
/** 刷新整体界面 */
private refreshUI() {
this.updatePlayerInfo();
this.updateTalentList();
}
/** 更新玩家等级、经验、碎片信息 */
private updatePlayerInfo() {
const collection = smc.collection;
let level = collection.player_level || 1;
let exp = collection.player_exp || 0;
const fragmentSummary = this.getFragmentSummaryText();
// 限制最大等级
if (level > this.MAX_PLAYER_LEVEL) {
level = this.MAX_PLAYER_LEVEL;
}
if (this.lbl_level) this.lbl_level.string = `Lv.${level}`;
if (this.lbl_points) this.lbl_points.string = fragmentSummary;
// 计算当前等级升级所需经验
let expRequired = this.getExpRequirement(level);
if (level >= this.MAX_PLAYER_LEVEL) {
if (this.lbl_exp) this.lbl_exp.string = "已满级";
if (this.pb_exp) this.pb_exp.progress = 1;
} else {
if (this.lbl_exp) this.lbl_exp.string = `${exp}/${expRequired}`;
if (this.pb_exp) this.pb_exp.progress = exp / expRequired;
}
}
/** 获取对应等级的升级所需经验 */
private getExpRequirement(level: number): number {
for (let config of TalentConfig.expRequirements) {
if (level <= config.maxLevel) {
return config.expPerLevel;
}
}
return TalentConfig.expRequirements[TalentConfig.expRequirements.length - 1].expPerLevel;
}
/** 动态生成或更新天赋列表 */
private updateTalentList() {
if (!this.talents_content || !this.prefab_talent_item) return;
const collection = smc.collection;
if (!collection.talents) collection.talents = {};
// 如果内容为空,则实例化预制体
if (this.talents_content.children.length === 0) {
TalentConfig.talents.forEach(talentInfo => {
let itemNode = instantiate(this.prefab_talent_item);
this.talents_content.addChild(itemNode);
this.updateTalentItem(itemNode, talentInfo, collection.talents[talentInfo.id as TalentType] || 0);
});
} else {
// 否则直接更新现有节点
TalentConfig.talents.forEach((talentInfo, index) => {
let itemNode = this.talents_content.children[index];
if (itemNode) {
this.updateTalentItem(itemNode, talentInfo, collection.talents[talentInfo.id as TalentType] || 0);
}
});
}
}
/** 更新单个天赋项的显示 */
private updateTalentItem(itemNode: Node, talentInfo: TalentInfo, currentLevel: number) {
let lblName = itemNode.getChildByName("lbl_name")?.getComponent(Label);
let lblDesc = itemNode.getChildByName("lbl_desc")?.getComponent(Label);
let lblLevel = itemNode.getChildByName("lbl_level")?.getComponent(Label);
let lblCost = itemNode.getChildByName("lbl_cost")?.getComponent(Label);
let btnUpgradeNode = itemNode.getChildByName("btn_upgrade");
let btnUpgrade = btnUpgradeNode?.getComponent(Button);
if (lblName) {
lblName.string = (talentInfo.icon ? `${talentInfo.icon} ` : '') + talentInfo.name;
}
if (lblDesc) {
let currentVal = currentLevel === 0 ? 0 : talentInfo.values[currentLevel - 1];
lblDesc.string = talentInfo.desc.replace('{value}', currentVal.toString());
}
if (lblLevel) lblLevel.string = `Lv.${currentLevel}`;
let isMax = currentLevel >= talentInfo.maxLevel;
const levelCost = isMax ? null : this.getLevelFragmentCost(talentInfo, currentLevel);
const canUpgrade = !!levelCost && this.hasEnoughFragments(levelCost);
if (lblCost) {
lblCost.string = isMax ? "已满级" : `消耗: ${this.buildFragmentCostText(levelCost!)}`;
}
if (btnUpgrade) {
btnUpgrade.interactable = !isMax && canUpgrade;
// 清除旧的监听,避免重复绑定
btnUpgradeNode?.off(Button.EventType.CLICK);
btnUpgradeNode?.on(Button.EventType.CLICK, () => {
this.onUpgradeClicked(talentInfo.id, currentLevel);
}, this);
}
}
/** 点击升级按钮 */
private onUpgradeClicked(talentId: TalentType, currentLevel: number) {
const collection = smc.collection;
const talentInfo = TalentConfig.talents.find(t => t.id === talentId);
if (!talentInfo) {
oops.gui.toast("天赋配置不存在");
return;
}
if (currentLevel >= talentInfo.maxLevel) {
oops.gui.toast("该天赋已满级");
return;
}
const levelCost = this.getLevelFragmentCost(talentInfo, currentLevel);
if (!levelCost) {
oops.gui.toast("天赋消耗配置异常");
return;
}
if (this.hasEnoughFragments(levelCost)) {
// 1. 扣除单碎片消耗
this.consumeFragments(levelCost);
// 2. 更新等级
collection.talents[talentId] = currentLevel + 1;
// 3. 同步数据(通过 SingletonModuleComp 新增的机制,这里会触发标记脏数据并自动尝试云端同步)
smc.updateCloudData();
// 4. 刷新 UI
this.refreshUI();
oops.gui.toast("天赋升级成功");
} else {
oops.gui.toast("碎片不足或已满级");
}
}
/** 点击重置按钮 */
private onResetClicked() {
// 看广告回调(预留)
this.watch_ad().then(success => {
if (success) {
const collection = smc.collection;
// 计算已消耗碎片并返还
const refundedFragments: Partial<Record<TalentFragmentType, number>> = {};
for (let id in collection.talents) {
let talentId = Number(id) as TalentType;
let level = collection.talents[talentId] || 0;
let talentInfo = TalentConfig.talents.find(t => t.id === talentId);
if (talentInfo) {
for (let i = 0; i < level; i++) {
const levelCost = this.getLevelFragmentCost(talentInfo, i);
if (!levelCost) continue;
refundedFragments[levelCost.type] = (refundedFragments[levelCost.type] || 0) + levelCost.amount;
}
}
}
// 重置天赋等级并返还碎片
collection.talents = {};
if (!collection.talent_fragments) {
collection.talent_fragments = {};
}
for (const fragmentType in refundedFragments) {
const key = Number(fragmentType) as TalentFragmentType;
const amount = refundedFragments[key] || 0;
if (amount > 0) {
collection.talent_fragments[key] = (collection.talent_fragments[key] || 0) + amount;
}
}
// 同步到云端
smc.updateCloudData();
// 刷新界面
this.refreshUI();
oops.gui.toast("天赋已重置,碎片已返还");
} else {
oops.gui.toast("广告观看失败,无法重置");
}
});
}
/** 获取升级到下一等级所需的单碎片消耗 */
private getLevelFragmentCost(talentInfo: TalentInfo, currentLevel: number): { type: TalentFragmentType; amount: number } | null {
if (talentInfo.fragmentType === undefined) {
return null;
}
return {
type: talentInfo.fragmentType,
amount: talentInfo.costs[currentLevel] ?? 0
};
}
/** 判断当前库存是否满足一次升级 */
private hasEnoughFragments(cost: { type: TalentFragmentType; amount: number }): boolean {
const bag = smc.collection.talent_fragments || {};
return (bag[cost.type] || 0) >= cost.amount;
}
/** 执行碎片扣除 */
private consumeFragments(cost: { type: TalentFragmentType; amount: number }) {
const bag = smc.collection.talent_fragments || (smc.collection.talent_fragments = {});
bag[cost.type] = Math.max(0, (bag[cost.type] || 0) - cost.amount);
}
/** 生成单次升级消耗文案 */
private buildFragmentCostText(cost: { type: TalentFragmentType; amount: number }): string {
const info = this.getFragmentInfo(cost.type);
const own = smc.collection.talent_fragments?.[cost.type] || 0;
const fragmentName = info ? info.name : `碎片${cost.type}`;
return `${fragmentName} ${own}/${cost.amount}`;
}
/** 顶部碎片库存摘要 */
private getFragmentSummaryText(): string {
const bag = smc.collection.talent_fragments || {};
const summary = TalentConfig.fragments
.filter(fragment => (bag[fragment.id] || 0) > 0)
.map(fragment => `${fragment.name}:${bag[fragment.id] || 0}`)
.join(" ");
return summary ? `碎片: ${summary}` : "碎片: 暂无";
}
/** 按碎片类型查找碎片配置 */
private getFragmentInfo(type: TalentFragmentType): TalentFragmentInfo | undefined {
return TalentConfig.fragments.find(fragment => fragment.id === type);
}
/** 模拟看广告回调实际项目中需要替换为真实的广告SDK调用 */
private watch_ad(): Promise<boolean> {
return new Promise((resolve) => {
// 模拟广告播放延迟
setTimeout(() => {
resolve(true);
}, 500);
});
}
/** 点击返回按钮 */
private onCloseClicked() {
oops.gui.removeByNode(this.node); //
}
protected onDestroy(): void {
mLogger.log(this.debugMode, 'TalentsComp', "释放界面");
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();
}
}