feat(talent): 重构天赋系统为三阶结构并实现全新UI

1.  重构天赋系统为一阶基础/二阶专精/三阶超强三阶结构,移除前置天赋要求改为阶位累计点数解锁
2.  新增阶位解锁校验逻辑与天赋点投入统计
3.  重做天赋UI为三阶Tab页签+迷你升级卡样式,支持状态展示与一键升级
4.  补充新的天赋配置与属性加成,更新天赋系统文档
5.  修复属性计算问题,新增暴击伤害与穿刺伤害加成支持
This commit is contained in:
pan
2026-08-25 16:29:11 +08:00
parent ac6fc21fb6
commit df519fdc4e
12 changed files with 3595 additions and 1165 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3246,7 +3246,7 @@
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_useGrayscale": true,
"_atlas": null,
"_id": ""
},

View File

@@ -1537,6 +1537,14 @@
"__uuid__": "a0cdb31c-0abb-4e12-9dfe-0bd9e8cd7272",
"__expectedType__": "cc.Prefab"
},
"lbl_points": null,
"tab_basic": null,
"tab_special": null,
"tab_super": null,
"lbl_tab_basic": null,
"lbl_tab_special": null,
"lbl_tab_super": null,
"btn_reset": null,
"_id": ""
},
{

View File

@@ -9,7 +9,7 @@ import { GameScoreStats } from "./config/HeroAttrs";
import { mLogger } from "./Logger";
import { gameDataSync } from "./GameDataSync";
import { HERO_UPGRADE_NEED_FRAGMENTS } from "./config/GameSet";
import { TalentSet, getTalentUpgradeCost } from "./config/TalentSet";
import { TalentSet, getTalentUpgradeCost, isTalentTierUnlocked } from "./config/TalentSet";
/**
* 用远程数据覆盖本地数据(统一方法)
@@ -418,7 +418,7 @@ export class SingletonModuleComp extends ecs.Comp {
/**
* 消耗天赋点升级天赋(升级即激活,下一局生效)
* @param uuid 天赋 uuid
* @returns 是否升级成功(天赋不存在/已满级/前置未满足/点数不足时返回 false)
* @returns 是否升级成功(天赋不存在/已满级/阶位未解锁/点数不足时返回 false)
*/
upgradeTalent(uuid: number): boolean {
const cfg = TalentSet[uuid];
@@ -426,12 +426,8 @@ export class SingletonModuleComp extends ecs.Comp {
if (!this.data.talents) this.data.talents = {};
const curLv = this.getTalentLv(uuid);
if (curLv >= cfg.max_lv) return false;
// 专精天赋的前置要求:前置天赋等级不足则禁止升级
if (cfg.requires) {
for (const req of cfg.requires) {
if (this.getTalentLv(req.uuid) < req.lv) return false;
}
}
// 阶位解锁:二/三阶天赋要求前一阶累计投入点数达标(一阶恒解锁)
if (!isTalentTierUnlocked(this.data.talents, cfg.tier)) return false;
const cost = getTalentUpgradeCost(curLv);
if (cost < 0 || this.getTalentPoints() < cost) return false;
this.data.talent_points -= cost;

View File

@@ -4,9 +4,13 @@ import { HRole } from "./heroSet";
/**
* 天赋系统配置(局外成长:升级即激活,战斗中对英雄属性常驻加成)。
*
* 双层结构:
* - 基础天赋:全局生效,对所有英雄叠加;
* - 专精天赋:按 HRole 职业过滤,仅对匹配职业的英雄生效,且要求前置天赋达到指定等级。
* 三阶结构(阶位逐级解锁,门槛见 TALENT_TIER_UNLOCK_THRESHOLD):
* - 一阶·基础天赋(9001-9099):全局生效,对所有英雄叠加;
* - 二阶·专精天赋(9101-9199):按 HRole 职业过滤,仅对匹配职业的英雄生效;
* - 三阶·超强天赋(9201-9219):全局(9201-9209)+ 职业(9211-9219)混合,数值幅度约为前两阶的 2 倍。
*
* 解锁规则:一阶恒可升级;二阶需一阶累计投入 ≥10 点;三阶需二阶累计投入 ≥10 点。
* 专精/超强职业天赋不再有独立前置天赋,仅受阶位解锁约束。
*
* 结算口径(与项目 buff 规范一致):
* - percent 模式:按英雄基础值(base_ap/base_hp/hero.def)相对放大;
@@ -19,6 +23,16 @@ export enum TalentValueMode {
Flat = 1,
}
/** 天赋阶位:一阶基础 / 二阶专精 / 三阶超强 */
export enum TalentTier {
/** 一阶:基础天赋,全局生效 */
Basic = 1,
/** 二阶:专精天赋,按职业过滤 */
Specialized = 2,
/** 三阶:超强天赋,全局 + 职业混合 */
Super = 3,
}
/** 单条属性加成词条 */
export interface TalentAttrBonus {
/** 目标属性(Attrs 枚举键) */
@@ -33,18 +47,20 @@ export interface TalentAttrBonus {
export interface TalentConfig {
/** 天赋 uuid(段位见 TalentSet 注释) */
uuid: number;
/** 所属阶位(升级校验:升下一阶天赋需前一阶累计投入点数达标) */
tier: TalentTier;
/** 天赋名称 */
name: string;
/** 描述(UI 展示) */
/** 描述(UI 展示,{value}/{value2} 为按当前等级计算后的数值占位) */
info: string;
/** 图标(uiconsAtlas 帧名,缺省时 UI 端按资源目录回退) */
icon?: string;
/** 属性加成词条列表(每级叠加) */
bonuses: TalentAttrBonus[];
/** 最大等级 */
max_lv: number;
/** 专精天赋:仅对该职业英雄生效;基础天赋省略此字段 */
/** 专精/超强职业天赋:仅对该职业英雄生效;全局天赋省略此字段 */
role?: HRole;
/** 专精天赋:前置要求(前置天赋 uuid + 所需等级),全部满足才生效 */
requires?: { uuid: number; lv: number }[];
}
/** 单条已解析的加成(等级已乘算) */
@@ -74,111 +90,185 @@ export interface ITalentTarget {
/**
* 天赋配置表。
* uuid 段位:9001-9099 基础天赋(全局);9101-9199 专精天赋(按职业过滤)。
* uuid 段位:9001-9099 一阶基础(全局);9101-9199 二阶专精(按职业);
* 9201-9209 三阶超强·全局;9211-9219 三阶超强·职业。
* icon 复用 SkillSet/uiconsAtlas 既有帧名,后续可在编辑器侧替换为专属图标。
*/
export const TalentSet: Record<number, TalentConfig> = {
// ==================== 基础天赋(9001-9099,全局生效) ====================
// ==================== 一阶·基础天赋(9001-9099,全局生效) ====================
9001: {
uuid: 9001, name: "攻击强化", info: "所有英雄攻击 +{value}%",
uuid: 9001, tier: TalentTier.Basic, name: "攻击强化", info: "所有英雄攻击 +{value}%", icon: "Stat_Attack_03",
bonuses: [{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 3 }],
max_lv: 5,
},
9002: {
uuid: 9002, name: "生命强化", info: "所有英雄生命 +{value}%",
uuid: 9002, tier: TalentTier.Basic, name: "生命强化", info: "所有英雄生命 +{value}%", icon: "Stat_Hp_02",
bonuses: [{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 5 }],
max_lv: 5,
},
9003: {
uuid: 9003, name: "防御强化", info: "所有英雄防御 +{value} 点",
uuid: 9003, tier: TalentTier.Basic, name: "防御强化", info: "所有英雄防御 +{value} 点", icon: "Stat_Defense",
bonuses: [{ attr: Attrs.defense, mode: TalentValueMode.Flat, per_lv: 8 }],
max_lv: 5,
},
9004: {
uuid: 9004, name: "暴击强化", info: "所有英雄暴击率 +{value}%",
uuid: 9004, tier: TalentTier.Basic, name: "暴击强化", info: "所有英雄暴击率 +{value}%", icon: "Stat_CriticalChance_02",
bonuses: [{ attr: Attrs.critical, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9005: {
uuid: 9005, name: "击晕强化", info: "所有英雄击晕率 +{value}%",
uuid: 9005, tier: TalentTier.Basic, name: "击晕强化", info: "所有英雄击晕率 +{value}%", icon: "Stat_Stun_01",
bonuses: [{ attr: Attrs.stun_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9006: {
uuid: 9006, name: "冰冻强化", info: "所有英雄冰冻率 +{value}%",
uuid: 9006, tier: TalentTier.Basic, name: "冰冻强化", info: "所有英雄冰冻率 +{value}%", icon: "Stat_Freeze",
bonuses: [{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9007: {
uuid: 9007, name: "击退强化", info: "所有英雄击退距离 +{value}",
uuid: 9007, tier: TalentTier.Basic, name: "击退强化", info: "所有英雄击退距离 +{value}", icon: "Stat_Stun_01",
bonuses: [{ attr: Attrs.knockback_distance, mode: TalentValueMode.Flat, per_lv: 10 }],
max_lv: 5,
},
// 风怒机制已停用: 9008 风怒强化天赋已移除
/*
9008: {
uuid: 9008, name: "风怒强化", info: "所有英雄风怒率 +{value}%",
uuid: 9008, tier: TalentTier.Basic, name: "风怒强化", info: "所有英雄风怒率 +{value}%", icon: "Stat_CriticalComboChance",
bonuses: [{ attr: Attrs.wfuny, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
*/
// ==================== 专精天赋(9101-9199,按职业过滤 + 前置要求) ====================
// 坦克专精:前置 防御强化 Lv3 → 坦克大量防御 + 生命
// ==================== 二阶·专精天赋(9101-9199,按职业过滤;一阶投入 10 点解锁) ====================
9101: {
uuid: 9101, name: "坦克专精", info: "坦克防御 +{value}%、生命 +{value2}%",
uuid: 9101, tier: TalentTier.Specialized, name: "坦克专精", info: "坦克防御 +{value}%、生命 +{value2}%", icon: "Stat_Armor_01",
bonuses: [
{ attr: Attrs.defense, mode: TalentValueMode.Percent, per_lv: 30 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 5 },
],
max_lv: 5, role: HRole.Tank,
requires: [{ uuid: 9003, lv: 3 }],
},
// 战士专精:前置 生命强化 Lv3 → 战士大量生命
9102: {
uuid: 9102, name: "战士专精", info: "战士生命 +{value}%",
uuid: 9102, tier: TalentTier.Specialized, name: "战士专精", info: "战士生命 +{value}%", icon: "Sword-FA_WP_Main_Sword_003_PurpleSilver",
bonuses: [{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 12 }],
max_lv: 5, role: HRole.Warrior,
requires: [{ uuid: 9002, lv: 3 }],
},
// 刺客专精:前置 暴击强化 Lv3 → 刺客攻击 + 暴击
9103: {
uuid: 9103, name: "刺客专精", info: "刺客攻击 +{value}%、暴击率 +{value2}%",
uuid: 9103, tier: TalentTier.Specialized, name: "刺客专精", info: "刺客攻击 +{value}%、暴击率 +{value2}%", icon: "Sword-FA_WP_Main_Sword_019_WoodSilver",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.critical, mode: TalentValueMode.Flat, per_lv: 2 },
],
max_lv: 5, role: HRole.Assassin,
requires: [{ uuid: 9004, lv: 3 }],
},
// 射手专精:前置 攻击强化 Lv3 → 射手攻击 + 穿透
9104: {
uuid: 9104, name: "射手专精", info: "射手攻击 +{value}%、穿透率 +{value2}%",
uuid: 9104, tier: TalentTier.Specialized, name: "射手专精", info: "射手攻击 +{value}%、穿透率 +{value2}%", icon: "Stat_RangedAttack",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.puncture_chance, mode: TalentValueMode.Flat, per_lv: 2 },
],
max_lv: 5, role: HRole.Archer,
requires: [{ uuid: 9001, lv: 3 }],
},
// 法师专精:前置 冰冻强化 Lv3 → 法师攻击 + 冰冻率
9105: {
uuid: 9105, name: "法师专精", info: "法师攻击 +{value}%、冰冻率 +{value2}%",
uuid: 9105, tier: TalentTier.Specialized, name: "法师专精", info: "法师攻击 +{value}%、冰冻率 +{value2}%", icon: "Stat_Mana",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 2 },
],
max_lv: 5, role: HRole.Mage,
requires: [{ uuid: 9006, lv: 3 }],
},
// 辅助专精:前置 生命强化 Lv3 → 治疗量提升 + 施加的友方 buff 效果放大
9106: {
uuid: 9106, name: "辅助专精", info: "辅助治疗量 +{value}%、施加的增益效果 +{value2}%",
uuid: 9106, tier: TalentTier.Specialized, name: "辅助专精", info: "辅助治疗量 +{value}%、施加的增益效果 +{value2}%", icon: "Stat_PotionBoost",
bonuses: [
{ attr: Attrs.heal_power, mode: TalentValueMode.Flat, per_lv: 10 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 8 },
],
max_lv: 5, role: HRole.Support,
requires: [{ uuid: 9002, lv: 3 }],
},
// ==================== 三阶·超强天赋(9201-9219,二阶投入 10 点解锁;幅度约为前两阶 2 倍) ====================
// ---- 全局超强(9201-9209) ----
9201: {
uuid: 9201, tier: TalentTier.Super, name: "神威降临", info: "所有英雄攻击 +{value}%、生命 +{value2}%", icon: "Stat_HolyDamage",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 8 },
],
max_lv: 5,
},
9202: {
uuid: 9202, tier: TalentTier.Super, name: "致命终结", info: "所有英雄暴击率 +{value}%、暴击伤害 +{value2}%", icon: "Stat_Critical_01",
bonuses: [
{ attr: Attrs.critical, mode: TalentValueMode.Flat, per_lv: 3 },
{ attr: Attrs.critical_damage, mode: TalentValueMode.Flat, per_lv: 15 },
],
max_lv: 5,
},
9203: {
uuid: 9203, tier: TalentTier.Super, name: "破甲重创", info: "所有英雄穿透率 +{value}%、穿透伤害 +{value2}%", icon: "Stat_Tripleshot",
bonuses: [
{ attr: Attrs.puncture_chance, mode: TalentValueMode.Flat, per_lv: 3 },
{ attr: Attrs.puncture_dmg_bonus, mode: TalentValueMode.Flat, per_lv: 10 },
],
max_lv: 5,
},
9204: {
uuid: 9204, tier: TalentTier.Super, name: "钢铁之躯", info: "所有英雄防御 +{value}%、生命 +{value2}%", icon: "Stat_Defense",
bonuses: [
{ attr: Attrs.defense, mode: TalentValueMode.Percent, per_lv: 20 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 6 },
],
max_lv: 5,
},
// ---- 职业超强(9211-9219,按 HRole 过滤) ----
9211: {
uuid: 9211, tier: TalentTier.Super, name: "不朽壁垒", info: "坦克防御 +{value}%、生命 +{value2}%", icon: "Stat_Armor_01",
bonuses: [
{ attr: Attrs.defense, mode: TalentValueMode.Percent, per_lv: 40 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 8 },
],
max_lv: 5, role: HRole.Tank,
},
9212: {
uuid: 9212, tier: TalentTier.Super, name: "狂血战魂", info: "战士攻击 +{value}%、生命 +{value2}%", icon: "Stat_Attack_03",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 10 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 8 },
],
max_lv: 5, role: HRole.Warrior,
},
9213: {
uuid: 9213, tier: TalentTier.Super, name: "影刃绝杀", info: "刺客攻击 +{value}%、暴击伤害 +{value2}%", icon: "Sword-FA_WP_Main_Sword_019_WoodSilver",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 12 },
{ attr: Attrs.critical_damage, mode: TalentValueMode.Flat, per_lv: 12 },
],
max_lv: 5, role: HRole.Assassin,
},
9214: {
uuid: 9214, tier: TalentTier.Super, name: "穿云贯日", info: "射手攻击 +{value}%、穿透伤害 +{value2}%", icon: "Stat_RangedAttack",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 12 },
{ attr: Attrs.puncture_dmg_bonus, mode: TalentValueMode.Flat, per_lv: 8 },
],
max_lv: 5, role: HRole.Archer,
},
9215: {
uuid: 9215, tier: TalentTier.Super, name: "元素主宰", info: "法师攻击 +{value}%、冰冻率 +{value2}%", icon: "Sub_Item-FA_WP_Sub_MagicBall_001_Blue",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 12 },
{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 3 },
],
max_lv: 5, role: HRole.Mage,
},
9216: {
uuid: 9216, tier: TalentTier.Super, name: "圣光普照", info: "辅助治疗量 +{value}%、施加的增益效果 +{value2}%", icon: "Stat_HolyDamage",
bonuses: [
{ attr: Attrs.heal_power, mode: TalentValueMode.Flat, per_lv: 15 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 12 },
],
max_lv: 5, role: HRole.Support,
},
};
@@ -195,12 +285,91 @@ export function getTalentUpgradeCost(curLv: number): number {
return TALENT_UPGRADE_COST[curLv];
}
/** 阶位解锁门槛:前一阶累计投入 ≥ 该点数才能升级下一阶天赋 */
export const TALENT_TIER_UNLOCK_THRESHOLD = 10;
/**
* 计算指定阶位已投入的天赋点数(按升级消耗表逐级累加)。
* Why: 阶位解锁按"实际消耗点数"而非"等级总和"口径——与玩家直觉(投入了多少点)
* 及重置退款口径(resetTalents 同表对账)保持一致。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param tier 目标阶位
* @returns 该阶位全部天赋已投入的点数总和
*/
export function getTalentTierInvestedPoints(levels: Record<number, number>, tier: TalentTier): number {
if (!levels) return 0;
let invested = 0;
for (const key in levels) {
const cfg = TalentSet[key];
if (!cfg || cfg.tier !== tier) continue;
for (let i = 0; i < levels[key]; i++) {
const c = getTalentUpgradeCost(i);
if (c > 0) invested += c;
}
}
return invested;
}
/**
* 判断指定阶位是否已解锁(加点入口校验用;战斗结算无需关心阶位——已投入的等级恒生效)。
* 规则:一阶恒解锁;二阶要求一阶累计投入 ≥ TALENT_TIER_UNLOCK_THRESHOLD;三阶要求二阶累计投入达标。
* @param levels 天赋等级表
* @param tier 目标阶位
*/
export function isTalentTierUnlocked(levels: Record<number, number>, tier: TalentTier): boolean {
if (tier === TalentTier.Basic) return true;
const prevTier = (tier - 1) as TalentTier;
return getTalentTierInvestedPoints(levels, prevTier) >= TALENT_TIER_UNLOCK_THRESHOLD;
}
/** 天赋属性的卡面显示名(迷你卡空间有限,用短标签) */
const TALENT_ATTR_LABELS: Partial<Record<Attrs, string>> = {
[Attrs.ap]: "攻击",
[Attrs.hp_max]: "生命",
[Attrs.defense]: "防御",
[Attrs.critical]: "暴击",
[Attrs.critical_damage]: "暴伤",
[Attrs.stun_chance]: "击晕",
[Attrs.freeze_chance]: "冰冻",
[Attrs.knockback_distance]: "击退",
[Attrs.puncture_chance]: "穿透",
[Attrs.puncture_dmg_bonus]: "穿透伤",
[Attrs.heal_power]: "治疗",
[Attrs.buff_power]: "增益",
};
/** 天赋属性的单位后缀:概率/治疗/增益类为百分点(%),防御为"点",击退距离为纯数值 */
const TALENT_ATTR_SUFFIX: Partial<Record<Attrs, string>> = {
[Attrs.defense]: "点",
[Attrs.knockback_distance]: "",
};
/**
* 生成天赋迷你卡的卡面文案(自包含:不显示满级效果)。
* 规则:已加点(lv>0)显示当前已生效数值,如「攻击 +9%」;
* 未加点(lv=0)显示每级数值引导,如「攻击 +3%/级」。
* Why: 迷你点选卡没有二级详情入口,文案必须一眼看懂当前收益。
* @param cfg 天赋配置
* @param lv 当前等级
*/
export function getTalentDisplayText(cfg: TalentConfig, lv: number): string {
const mult = lv > 0 ? lv : 1;
const parts: string[] = [];
for (const b of cfg.bonuses) {
const label = TALENT_ATTR_LABELS[b.attr] ?? String(b.attr);
const suffix = TALENT_ATTR_SUFFIX[b.attr] ?? "%";
parts.push(`${label} +${b.per_lv * mult}${suffix}`);
}
const text = parts.join("、");
return lv > 0 ? text : `${text}/级`;
}
/**
* 聚合指定职业英雄生效的全部天赋加成。
* Why: 基础天赋全局生效,专精天赋按 role 过滤且要求前置天赋达标;
* 战斗加载时(Hero.load / applyHeroLevel)调用本函数一次性取出本英雄的天赋加成区。
* Why: 基础/超强全局天赋全局生效,专精与超强职业天赋按 role 过滤;
* 阶位解锁只约束加点阶段,战斗加载时(Hero.load / applyHeroLevel)按当前等级直接取加成。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param role 英雄职业(专精过滤依据;无 role 时只应用基础天赋)
* @param role 英雄职业(职业过滤依据;无 role 时只应用全局天赋)
* @returns 已解析的加成列表(等级已乘算进 value)
*/
export function getTalentBonusesForRole(levels: Record<number, number>, role?: HRole): TalentResolvedBonus[] {
@@ -211,16 +380,8 @@ export function getTalentBonusesForRole(levels: Record<number, number>, role?: H
if (lv <= 0) continue;
const cfg = TalentSet[key];
if (!cfg) continue;
// 专精天赋:职业不匹配则跳过
// 职业天赋:职业不匹配则跳过
if (cfg.role !== undefined && cfg.role !== role) continue;
// 前置要求:任一前置天赋等级不足则不生效
if (cfg.requires) {
let ok = true;
for (const req of cfg.requires) {
if ((levels[req.uuid] ?? 0) < req.lv) { ok = false; break; }
}
if (!ok) continue;
}
for (const b of cfg.bonuses) {
result.push({ attr: b.attr, mode: b.mode, value: b.per_lv * lv });
}
@@ -240,7 +401,7 @@ export function getTalentBonusesForRole(levels: Record<number, number>, role?: H
*
* @param model 英雄属性模型(满足 ITalentTarget 结构)
* @param levels 天赋等级表(smc.getTalentLevels())
* @param role 英雄职业(专精过滤;无 role 的怪物由调用方跳过)
* @param role 英雄职业(职业过滤;无 role 的怪物由调用方跳过)
*/
export function applyTalentBonuses(model: ITalentTarget, levels: Record<number, number>, role?: HRole): void {
const bonuses = getTalentBonusesForRole(levels, role);

View File

@@ -1,7 +1,7 @@
# 天赋系统设计 — Draftmaster Arena
*Created: 2026-04-26*
*Updated: 2026-04-26*
*Updated: 2026-08-25*
*Status: Draft*
---
@@ -10,7 +10,15 @@
局外成长系统。通过每局获取的经验值提升玩家等级,获得天赋点数,用于升级天赋。升级即激活,无需单独激活步骤。看广告可重置所有天赋等级并退还天赋点。
**设计原则**:天赋提供小幅加成,不改变核心策略体验。高评分 = 更多经验 = 更快升级,但天赋不是胜利的决定性因素。
**三阶结构**:
| 阶位 | 名称 | 生效范围 | uuid 段位 |
|------|------|---------|----------|
| 一阶 | 基础天赋 | 全局(所有英雄) | 9001-9099 |
| 二阶 | 专精天赋 | 按职业过滤 | 9101-9199 |
| 三阶 | 超强天赋 | 全局 + 职业混合 | 9201-9219 |
**设计原则**:天赋提供小幅加成,不改变核心策略体验。高评分 = 更多经验 = 更快升级,但天赋不是胜利的决定性因素。三阶超强天赋幅度约为前两阶的 2 倍,作为长线成长的目标感奖励。
**成长闭环**:评分 → 经验 → 等级 → 天赋点 → 天赋升级 → 下一局更强 → 更高评分
@@ -52,25 +60,54 @@
## 天赋列表
### 战斗属性类(全局加成)
### 一阶·基础天赋(全局加成,9001-9099)
| 天赋 | 每级效果 | 5级总效果 |
|------|---------|----------|
| **攻击强化** | 所有英雄 ATK +3% | +15% |
| **生命强化** | 所有英雄 HP +5% | +25% |
| **防御强化** | 所有英雄防御 +8 点 | +40 点 |
| **暴击强化** | 所有英雄暴击率 +2% | +10% |
| **风怒强化** | 所有英雄风怒率 +2% | +10% |
| **击晕强化** | 所有英雄击晕率 +2% | +10% |
| **冰冻强化** | 所有英雄冰冻率 +2% | +10% |
| **穿刺强化** | 所有英雄穿刺 +0.2 | +1 |
| **护盾强化** | 所有护盾效果 +5% | +25% |
| **击退强化** | 所有英雄击退距离 +10 | +50 |
### 经济类(金币减免/补贴)
> 风怒强化(原 9008)已随风怒机制停用移除。
### 二阶·专精天赋(按职业过滤,9101-9199)
| 天赋 | 职业 | 每级效果 | 5级总效果 |
|------|------|---------|----------|
| **坦克专精** | 坦克 | 防御 +30%、生命 +5% | +150%、+25% |
| **战士专精** | 战士 | 生命 +12% | +60% |
| **刺客专精** | 刺客 | ATK +8%、暴击率 +2% | +40%、+10% |
| **射手专精** | 射手 | ATK +8%、穿透率 +2% | +40%、+10% |
| **法师专精** | 法师 | ATK +8%、冰冻率 +2% | +40%、+10% |
| **辅助专精** | 辅助 | 治疗量 +10%、增益效果 +8% | +50%、+40% |
> 专精天赋不再有独立前置天赋,仅受阶位解锁约束(见核心规则)。
### 三阶·超强天赋(全局 + 职业混合,9201-9219)
**全局超强(9201-9204)**:
| 天赋 | 每级效果 | 5级总效果 |
|------|---------|----------|
| **采购优惠** | 购买英雄 -1金 | -5金(10→5) |
| **刷新优惠** | 刷新重抽 -0.5金 | -2.5金(3→0.5) |
| **出售补贴** | 出售英雄返还 +10%金币 | +50% |
| **神威降临** | 所有英雄 ATK +8%、HP +8% | +40%、+40% |
| **致命终结** | 所有英雄暴击率 +3%、暴击伤害 +15% | +15%、+75% |
| **破甲重创** | 所有英雄穿透率 +3%、穿透伤害 +10% | +15%、+50% |
| **钢铁之躯** | 所有英雄防御 +20%、HP +6% | +100%、+30% |
**职业超强(9211-9216)**:
| 天赋 | 职业 | 每级效果 | 5级总效果 |
|------|------|---------|----------|
| **不朽壁垒** | 坦克 | 防御 +40%、生命 +8% | +200%、+40% |
| **狂血战魂** | 战士 | ATK +10%、HP +8% | +50%、+40% |
| **影刃绝杀** | 刺客 | ATK +12%、暴击伤害 +12% | +60%、+60% |
| **穿云贯日** | 射手 | ATK +12%、穿透伤害 +8% | +60%、+40% |
| **元素主宰** | 法师 | ATK +12%、冰冻率 +3% | +60%、+15% |
| **圣光普照** | 辅助 | 治疗量 +15%、增益效果 +12% | +75%、+60% |
---
@@ -84,17 +121,20 @@
| 4级 | 2点 | 6 |
| 5级 | 3点 | 9 |
**一个天赋升满 = 9点**
30个天赋点的分配选择:
- 专精:升满3个天赋(27点)+ 3点分散
- 均衡:6-7个天赋各1-2级
- 经济:采购+刷新+出售全满(27点)
**一个天赋升满 = 9点**(各阶位升级费用相同)
---
## 核心规则
### 阶位解锁
- **一阶**:恒可升级
- **二阶**:一阶天赋累计投入 **≥10 点** 后解锁
- **三阶**:二阶天赋累计投入 **≥10 点** 后解锁
- 投入点数按实际消耗计算(非等级总和),与重置退款口径一致
- 专精/超强职业天赋没有独立前置天赋,仅受阶位解锁约束
### 升级即激活
- 消耗天赋点升级天赋 → 立即生效
@@ -104,7 +144,7 @@
### 重置(看广告)
- 观看广告 → **所有天赋等级归零**
- 观看广告 → **所有天赋等级归零**(阶位解锁状态随之重置)
- **退还全部天赋点**(30点内已消耗的)
- 玩家等级不变(经验永久累积)
- 重新自由分配天赋点
@@ -114,67 +154,83 @@
## 分配策略示例
**暴击流专精**(27点):
30 点总预算下:10 点开二阶 + 10 点开三阶 = 20 点门槛,剩余约 10 点自由分配。
**满配输出流**(30点):
```
暴击强化 Lv5(9) + 攻击强化 Lv5(9) + 采购优惠 Lv5(9) = 27点
→ 全局暴击+10%, ATK+15%, 买卡-5金
一阶: 攻击强化 Lv4(6) + 暴击强化 Lv2(2) + 生命强化 Lv2(2) = 10点 → 解锁二阶
二阶: 刺客专精 Lv4(6) + 战士专精 Lv2(2) + 坦克专精 Lv1(1) + 射手专精 Lv1(1) = 10点 → 解锁三阶
三阶: 致命终结 Lv4(6) + 影刃绝杀 Lv1(1) = 7点 + 3点备用
→ 全局暴击+15%/暴伤+75%,刺客 ATK+40%+60%、暴伤+60%+12%
```
**护盾流专精**(27点):
**生存流**(29点):
```
护盾强化 Lv5(9) + 生命强化 Lv5(9) + 冰冻强化 Lv5(9) = 27点
→ 护盾+25%, HP+25%, 冰冻+10%
一阶: 生命强化 Lv5(9) + 防御强化 Lv1(1) = 10点
二阶: 坦克专精 Lv5(9) + 战士专精 Lv1(1) = 10点
三阶: 钢铁之躯 Lv5(9) = 9点
→ 全局 HP+55%/防御+100%,坦克 HP+65%/防御+250%
```
**经济流**(27点):
**均衡新手**(20点):
```
采购优惠 Lv5(9) + 刷新优惠 Lv5(9) + 出售补贴 Lv5(9) = 27点
→ 买卡5金, 刷新0.5金, 出售+50%返还
```
**均衡新手**(26点):
```
攻击 Lv3(4) + 生命 Lv3(4) + 采购 Lv3(4) +
暴击 Lv2(2) + 护盾 Lv2(2) + 刷新 Lv2(2) +
风怒 Lv2(2) + 冰冻 Lv2(2) = 22点 + 4点备用
一阶: 攻击 Lv3(4) + 生命 Lv3(4) + 暴击 Lv1(1) + 击晕 Lv1(1) = 10点
二阶: 按主力阵容选 2 个专精各 Lv3(8) + 1 个 Lv1(1) ≈ 10点(刚开三阶)
三阶: 留待后续点数
```
---
## 界面布局
## 界面设计(阶位 Tab 单屏聚焦 · 迷你点选卡)
```
┌─────────────────────────┐
│ Lv.12 ████████░░ 150/200│
│ 天赋点: 4/30 │
├─────────────────────────┤
│ │
│ ⚔️ 攻击强化 Lv.3 ███░░ │
│ ATK +9% [+1点] │
│ │
│ ❤️ 生命强化 Lv.2 ██░░░ │
│ HP +10% [+2点] │
│ │
│ 🔥 暴击强化 Lv.1 █░░░░ │
│ 暴击 +2% [+1点] │
│ │
│ ⚡ 风怒强化 Lv.0 ░░░░░ │
│ 风怒 +0% [+1点] │
│ │
│ ❄️ 冰冻强化 Lv.0 ░░░░░ │
│ 冰冻 +0% [+1点] │
│ │
│ 🛡️ 护盾强化 Lv.0 ░░░░░ │
│ 🗡️ 穿刺强化 Lv.0 ░░░░░ │
│ 🛒 采购优惠 Lv.2 ██░░░ │
│ 🔄 刷新优惠 Lv.0 ░░░░░ │
│ 💰 出售补贴 Lv.0 ░░░░░ │
│ │
│ [🎬 看广告重置天赋] │
│ [← 返回] │
└─────────────────────────┘
┌─────────────────────────────────┐
│ 天赋点: 4 │
│ [Ⅰ 基础] [Ⅱ 专精 6/10🔒] [Ⅲ 超强 0/10🔒] │
├─────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ ⚔️ │ │ ❤️ │ │ 🛡️ │ │
│ │ 攻击强化│ │ 生命强化│ │ 防御强化│ │
│ │攻击 +9%│ │生命+10%│ │防御+8点│ │
│ │ 3/5│ │ 2/5│ │ 1/5│ │
│ │ [+]│ │ [+]│ │ [+]│ │
│ └──────┘ └──────┘ └──────┘ │
│ ……(Grid 三列,单屏聚焦当前阶) │
│ │
│ [🎬 看广告重置天赋] │
└─────────────────────────────────┘
```
### 卡片规格(迷你点选卡,自包含)
- **布局**:图标 / 名称 / 当前已生效数值 / 右下「当前/满级」数字角标;右上 `+` 升级徽标
- **卡面数值**:已加点显示当前生效值(如「攻击 +9%」);Lv0 显示每级引导(如「攻击 +3%/级」);**不显示满级效果**(`getTalentDisplayText`)
- **阶位底框**:复用 cardbg.prefab + CardBgComp,Ⅰ→蓝(2) / Ⅱ→紫(3) / Ⅲ→金(4)
- **状态机**:可升级(`+` 徽标绿光呼吸)|满级(MAX 金角标)|阶未解锁(半透明 110 + 🔒,无 `+`)
### 交互规则
- **点击 `+` 徽标 / 卡体** → 统一上抛 uuid,由 TalentsComp 校验:
- 阶未解锁 → toast「一阶投入 x/10 点解锁本阶」
- 已满级 → toast「该天赋已满级」
- 点数不足 → toast「天赋点不足」
- 校验通过 → `smc.upgradeTalent(uuid)`,成功由 `TALENT_UPDATE` 事件驱动全量刷新
- **阶位 Tab**:三 Tab 常显;锁定 Tab 显示前阶投入进度(`x/10`);点击锁定 Tab 允许查看(卡片呈锁定态)并 toast 解锁条件;首次进入默认选中已解锁的最高阶
- **重置**:底部按钮 → CommonPrompt 二次确认 → `smc.resetTalents()`(广告 SDK 接入前先直接确认重置)
### 编辑器搭建清单(prefab)
- `talents.prefab`(复用现有骨架改造):ScrollView + Grid 三列 content 挂 `talents_content`;新增页头 `lbl_points`、三个 Tab 节点(挂 `tab_basic/tab_special/tab_super` + 各自 Label、可选子节点 `sp_on` 选中高亮)、底部 `btn_reset`
- `prefab_talent_item.prefab`(复用改造):`icon` / `lbl_name` / `lbl_desc`→`lbl_info` / `lbl_level`→`lbl_lv` 四个既有节点绑定沿用;新增 `node_plus`(+ 徽标)、`node_max`(MAX 角标)、`node_lock`(锁遮罩)
- 图标:TalentSet 配置 `icon` 字段(uiconsAtlas 帧名),暂复用技能图标素材
---
## 实现映射
- 配置与工具函数:`TalentSet.ts`(`TalentTier` 阶位枚举、`isTalentTierUnlocked` / `getTalentTierInvestedPoints` 解锁校验、`getTalentDisplayText` 卡面文案、`applyTalentBonuses` 战斗注入)
- 存储/升级/重置:`SingletonModuleComp.ts`(`upgradeTalent` 内做阶位解锁校验)
- 战斗注入点:`Hero.load`(召唤加载)与 `MissionCardComp.applyHeroLevel`(局内升级重算,累加语义字段先归零再注入)
- 加点 UI:`TalentsComp.ts`(阶位 Tab 页容器)+ `TalentItemComp.ts`(迷你升级卡),挂主界面现有"天赋"页签(MissionHomeComp.showPage 切换)
*与评分系统联动:评分 → 经验 → 等级 → 天赋点 → 天赋 → 下一局更强 → 更高评分 → 更多经验*

View File

@@ -195,7 +195,7 @@ export class Hero extends ecs.Entity {
model.ap += apExtra;
}
// 天赋加成层:基础天赋全局生效,专精天赋按职业过滤(仅英雄生效,怪物跳过)
// 天赋加成层:全局天赋(一阶基础/三阶超强全局)全局生效,职业天赋(二阶专精/三阶超强职业)按 role 过滤(仅英雄生效,怪物跳过)
// Why: 放在成长/bonus 之后、光环乘区之后,以基础快照为基数叠加,避免与乘区叠加成乘法膨胀
if (model.fac === FacSet.HERO) {
applyTalentBonuses(model, smc.getTalentLevels(), model.role);

View File

@@ -2189,11 +2189,13 @@ export class MissionCardComp extends CCComp {
if (model.fac === FacSet.HERO) {
model.defense = hero.def;
model.critical = 0;
model.critical_damage = 0;
model.stun_chance = 0;
model.freeze_chance = 0;
// 风怒机制已停用: 升级重算不再重置风怒
// model.wfuny = 0;
model.puncture_chance = 0;
model.puncture_dmg_bonus = 0;
model.knockback_distance = 0;
model.heal_power = 0;
model.buff_power = 0;

View File

@@ -1,28 +1,36 @@
/**
* @file TalentItemComp.ts
* @description 天赋图鉴项组件(UI 视图层)
* @description 天赋迷你升级卡组件(UI 视图层)
*
* 职责:
* 1. 接收 TalentsComp 下发的天赋卡配置(技能卡)。
* 2. 渲染名称、描述、图标和背景。
* 3. 背景颜色根据 wave 映射为对应的 poolLv。
* 1. 接收 TalentsComp 下发的天赋配置 + 当前等级 + 阶位解锁态,渲染迷你点选卡。
* 2. 卡面自包含:图标 / 名称 / 当前已生效数值(Lv0 显示每级数值引导)/ 右下「当前/满级」角标。
* 3. 状态机:可升级(右上 + 徽标,绿光呼吸)/ 满级(MAX 金角标)/ 阶未解锁(半透明 + 锁)。
* 4. 点击回调上抛(含 + 徽标与卡体),升级校验与 toast 统一由 TalentsComp 处理(单一决策点)。
*
* 依赖:
* - CardSet.CardConfig —— 单条卡牌配置
* - SkillSet —— 获取技能图标
* - TalentSet.TalentConfig / TalentTier / getTalentDisplayText —— 配置与卡面文案
* - CardBgComp —— 阶位品质底框(Ⅰ蓝 / Ⅱ紫 / Ⅲ金)
* - smc.uiconsAtlas —— 图标解析(缺省回退资源目录)
*/
import { _decorator, Label, Sprite, SpriteFrame } from "cc";
import { _decorator, Label, Node, NodeEventType, Sprite, SpriteFrame, UIOpacity } 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 { CardConfig } from "../common/config/CardSet";
import { FieldSkillSet, SkillSet } from "../common/config/SkillSet";
import { TalentConfig, TalentTier, getTalentDisplayText } from "../common/config/TalentSet";
import { CardBgComp } from "./CardBgComp";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "db://oops-framework/core/Oops";
const { ccclass, property } = _decorator;
/** TalentItemComp —— 天赋图鉴项组件 */
/** 阶位 → 品质底框 card_lv(CARD_LV_BG_COLORS:2=blue、3=purple、4=yellow) */
const TIER_TO_BG_LV: Record<number, number> = {
[TalentTier.Basic]: 2,
[TalentTier.Specialized]: 3,
[TalentTier.Super]: 4,
};
/** TalentItemComp —— 天赋迷你升级卡组件 */
@ccclass('TalentItemComp')
@ecs.register('TalentItem', false)
export class TalentItemComp extends CCComp {
@@ -30,53 +38,86 @@ export class TalentItemComp extends CCComp {
@property({ type: Label, tooltip: "天赋名称" })
lbl_name: Label = null!;
@property({ type: Label, tooltip: "天赋说明" })
@property({ type: Label, tooltip: "当前已生效数值(Lv0 显示每级引导)" })
lbl_info: Label = null!;
@property({ type: Label, tooltip: "等级角标(当前/满级)" })
lbl_lv: Label = null!;
@property({ type: Sprite, tooltip: "天赋图标" })
icon: Sprite = null!;
@property({ type: CardBgComp, tooltip: "卡牌背景组件" })
@property({ type: CardBgComp, tooltip: "卡牌背景组件(阶位品质色)" })
bg: CardBgComp = null!;
@property({ type: Node, tooltip: "右上 + 升级徽标(可升级时显示)" })
node_plus: Node = null!;
@property({ type: Node, tooltip: "MAX 满级角标" })
node_max: Node = null!;
@property({ type: Node, tooltip: "阶位未解锁遮罩/锁图标" })
node_lock: Node = null!;
/** 点击回调(卡体与 + 徽标同源上抛),由父级统一校验升级 */
public onTalentTap: ((uuid: number) => void) | null = null;
/** 当前展示的天赋 uuid(回调携带) */
private curUuid: number = 0;
protected onLoad(): void {
this.node.on(NodeEventType.TOUCH_END, this.onTap, this);
}
protected onDestroy(): void {
this.node.off(NodeEventType.TOUCH_END, this.onTap, this);
}
/**
* 刷新单条天赋展示
* @param config CardPoolList 中的天赋卡(技能卡)配置
* 刷新迷你卡展示
* @param cfg 天赋配置(TalentSet 条目)
* @param lv 当前等级
* @param tierUnlocked 所属阶位是否已解锁
* @param canUp 父级预判的"可升级"(阶位解锁 + 未满级 + 点数足够)
*/
public updateItem(config: CardConfig): void {
if (!config) return;
public updateItem(cfg: TalentConfig, lv: number, tierUnlocked: boolean, canUp: boolean): void {
if (!cfg) return;
this.curUuid = cfg.uuid;
if (this.lbl_name) this.lbl_name.string = config.name ?? "";
if (this.lbl_info) this.lbl_info.string = config.info ?? "";
if (this.lbl_name) this.lbl_name.string = cfg.name;
// 卡面自包含:已加点显示当前生效数值,未加点显示每级数值引导
if (this.lbl_info) this.lbl_info.string = getTalentDisplayText(cfg, lv);
if (this.lbl_lv) this.lbl_lv.string = `${lv}/${cfg.max_lv}`;
// 直接复用 CardSet 中已映射好的 card_lv,保证与技能卡牌背景一致
// CardSet 通过 waveToCardLv[wave] 由 SKILL_CARD_WAVES 索引推导(wave 1→1, 5→2, 8→3)
if (this.bg) {
this.bg.apply(config.card_lv || 1);
// 阶位品质底框:Ⅰ蓝 / Ⅱ紫 / Ⅲ金
if (this.bg) this.bg.apply(TIER_TO_BG_LV[cfg.tier] ?? 1);
// 图标:uiconsAtlas 优先,资源目录回退(与 TalentItemComp 旧版/SCardComp 口径一致)
if (this.icon && cfg.icon) {
let frame: SpriteFrame | null = null;
if (smc.uiconsAtlas) {
frame = smc.uiconsAtlas.getSpriteFrame(cfg.icon);
}
if (!frame) {
frame = oops.res.get("game/heros/cards/" + cfg.icon, SpriteFrame) as SpriteFrame;
}
if (frame) this.icon.spriteFrame = frame;
}
// 设置图标:驻场卡(skill=undefined 但有 field)走 FieldSkillSet,否则走 SkillSet
// 与 SCardComp 保持一致,避免驻场卡无 icon 显示
if (this.icon) {
let iconId: string | undefined;
if (!config.skill && config.field && config.field.length > 0) {
// 驻场卡:用 FieldSkillSet[field[0]].icon
const fieldUuid = config.field[0].uuid;
iconId = FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`;
} else if (config.skill) {
// 技能卡:用 SkillSet[skill].icon
iconId = SkillSet[config.skill]?.icon;
}
if (iconId) {
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
if (frame) this.icon.spriteFrame = frame;
} else {
const sf = oops.res.get("game/heros/cards/" + iconId, SpriteFrame) as SpriteFrame;
if (sf) this.icon.spriteFrame = sf;
}
}
}
// 状态徽标:满级 MAX / 可升级 + / 阶位锁定 🔒
const maxed = lv >= cfg.max_lv;
if (this.node_max) this.node_max.active = maxed;
if (this.node_plus) this.node_plus.active = !maxed && tierUnlocked && canUp;
if (this.node_lock) this.node_lock.active = !tierUnlocked;
// 阶位锁定整卡压暗(UIOpacity:Cocos 3.x 节点透明度组件)
const uo = this.node.getComponent(UIOpacity) ?? this.node.addComponent(UIOpacity);
uo.opacity = tierUnlocked ? 255 : 110;
}
/** 卡体/+ 徽标点击:上抛 uuid,升级校验与提示由 TalentsComp 统一处理 */
private onTap(): void {
this.onTalentTap?.(this.curUuid);
}
/** ECS 组件移除时销毁节点(CCComp 抽象方法实现) */

View File

@@ -1,106 +1,233 @@
/**
* @file TalentsComp.ts
* @description 天赋图鉴页组件(UI 视图层)
* @description 天赋加点页组件(UI 视图层)
*
* 职责:
* 1. 展示当前所有天赋卡(技能卡)的图鉴。
* 2. 从 CardPoolList 获取所有的 type=Skill 卡牌。
* 3. 兼容旧的 `@ecs.register('Talents')` 资源引用。
* 1. 三阶页签(Ⅰ 基础 / Ⅱ 专精 / Ⅲ 超强)状态机:单屏聚焦当前阶位的迷你天赋卡。
* 2. 页签常显解锁进度(前阶投入 x/10),点击卡/+ 徽标升级(smc.upgradeTalent)。
* 3. 监听 GameEvent.TALENT_UPDATE 全量重渲染(23 条卡片,无需虚拟列表)。
* 4. 看广告重置入口:CommonPrompt 二次确认后调 smc.resetTalents() 退还全部点数。
*
* 依赖:
* - CardPoolList(CardSet)—— 获取技能卡配置
* - TalentItemComp —— 单条天赋图鉴项视图
* - TalentSet(TalentSet/TalentTier/isTalentTierUnlocked/getTalentTierInvestedPoints)—— 配置与阶位校验
* - TalentItemComp —— 单张迷你升级卡
* - smc —— 天赋等级表/点数/升级/重置
*/
import { _decorator, instantiate, Node, Prefab, UITransform } from "cc";
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 { mLogger } from "../common/Logger";
import { CardConfig } from "../common/config/CardSet";
import { SkillCardList } from "../common/config/SCardSet";
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;
/** TalentsComp —— 天赋图鉴信息页组件 */
/** 阶位页签展示文案(锁定态追加投入进度) */
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: "天赋技能列表容器" })
@property({ type: Node, tooltip: "当前阶位卡片列表容器(ScrollView content,Grid 三列)" })
talents_content: Node = null!;
@property({ type: Prefab, tooltip: "单条天赋项预制" })
@property({ type: Prefab, tooltip: "迷你天赋卡预制(TalentItemComp)" })
prefab_talent_item: Prefab = null!;
/** 调试日志开关 */
debugMode: boolean = false;
@property({ type: Label, tooltip: "页头可用天赋点" })
lbl_points: Label = null!;
/** 首次实例化缓存 */
private rendered: boolean = false;
@property({ type: Node, tooltip: "一阶页签节点" })
tab_basic: Node = null!;
/** 缓存的稳定配置顺序,避免重复渲染时列表抖动 */
private cachedConfigs: CardConfig[] = [];
@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.rendered) {
// 获取所有天赋(技能卡)并按 wave 和 uuid 排序
this.cachedConfigs = [...SkillCardList]
.sort((a, b) => {
const waveA = a.wave || 1;
const waveB = b.wave || 1;
if (waveA !== waveB) return waveA - waveB;
return a.uuid - b.uuid;
});
if (this.lbl_points) this.lbl_points.string = `天赋点 ${smc.getTalentPoints()}`;
this.cachedConfigs.forEach((cfg) => {
const itemNode = instantiate(this.prefab_talent_item);
this.talents_content.addChild(itemNode);
const comp = itemNode.getComponent(TalentItemComp);
if (comp) {
comp.updateItem(cfg);
}
});
this.rendered = true;
// 动态调整容器高度
// 默认底高 1000,每行 3 个天赋信息卡,每张卡高 270,行间距 15
const totalCount = this.cachedConfigs.length;
const rows = Math.ceil(totalCount / 3);
const contentHeight = Math.max(1000, rows * 270 + (rows > 0 ? (rows - 1) * 15 : 0 + 50));
const uiTransform = this.talents_content.getComponent(UITransform);
if (uiTransform) {
uiTransform.height = contentHeight;
}
} else {
// 如果已渲染,则仅更新数据(图鉴一般不会变动,这里做个兜底更新)
this.cachedConfigs.forEach((cfg, index) => {
const child = this.talents_content.children[index];
if (!child) return;
const comp = child.getComponent(TalentItemComp);
if (!comp) return;
comp.updateItem(cfg);
});
// 仅首次进入时默认选中已解锁的最高阶,后续刷新保持玩家当前选择
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.rendered = false;
this.cachedConfigs = [];
this.node.destroy();
}
protected onDestroy(): void {
super.onDestroy();
mLogger.log(this.debugMode, 'TalentsComp', "释放界面");
}
}