refactor: 重构天赋系统与技能触发逻辑

1.  重构天赋系统架构:将天赋加成与触发修正统一收敛到 HeroAttrsComp.applyTalent
2.  新增天赋触发修正字段:talent_t_num_reduce 与 talent_extra_trigger_chance
3.  重构天赋加点逻辑:统一 1 点/级,新增软前置校验
4.  优化天赋UI组件:改为编辑器静态配置驱动,重构层位展示逻辑
5.  修复技能触发判定:在 SkillTriggerHelper 中应用天赋触发修正
6.  调整天赋配置结构:改为 5 层递进解锁,新增 L4/L5 专属触发修正配置
This commit is contained in:
panFD
2026-09-19 11:20:55 +08:00
parent f53c4293df
commit 707e25e73f
11 changed files with 12833 additions and 7245 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9180,8 +9180,8 @@
"rawHeight": 52,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"borderLeft": 43.5,
"borderRight": 43.5,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,

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, TALENT_LEVEL_MAX, getTalentLevelExp, getTalentUpgradeCost, isTalentTierUnlocked } from "./config/TalentSet";
import { TalentSet, TALENT_LEVEL_MAX, getTalentLevelExp, isTalentPrereqMet, isTalentTierUnlocked } from "./config/TalentSet";
import { ScoreRankList, ScoreTier, getRankByScore } from "./config/ScoreRankSet";
import { RANK_REWARDS, STAGE_REWARDS, RewardItem, pickRandomHeroUuid } from "./config/RewardSet";
@@ -694,7 +694,7 @@ export class SingletonModuleComp extends ecs.Comp {
/**
* 消耗天赋点升级天赋(升级即激活,下一局生效)
* @param uuid 天赋 uuid
* @returns 是否升级成功(天赋不存在/已满级/阶位未解锁/点数不足时返回 false)
* @returns 是否升级成功(天赋不存在/已满级/层位未解锁/前置未满足/点数不足时返回 false)
*/
upgradeTalent(uuid: number): boolean {
const cfg = TalentSet[uuid];
@@ -702,11 +702,13 @@ 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;
// 阶位解锁:二/三阶天赋要求前一阶累计投入点数达标(一阶恒解锁)
// 层位解锁:第 N 层天赋要求第 N-1 层累计投入点数达标(第 1 层恒解锁)
if (!isTalentTierUnlocked(this.data.talents, cfg.tier)) return false;
const cost = getTalentUpgradeCost(cfg.tier);
if (cost < 0 || this.getTalentPoints() < cost) return false;
this.data.talent_points -= cost;
// 软前置:前置天赋等级需达标(仅限制加点)
if (!isTalentPrereqMet(this.data.talents, cfg)) return false;
// 统一 1 点/级
if (this.getTalentPoints() < 1) return false;
this.data.talent_points -= 1;
this.data.talents[uuid] = curLv + 1;
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.TALENT_UPDATE);
@@ -715,17 +717,13 @@ export class SingletonModuleComp extends ecs.Comp {
/**
* 重置所有天赋等级并退还全部已消耗天赋点(看广告入口)
* Why: 退还点数按"升到当前等级的累计消耗"计算,与升级消耗表严格对账,避免点数膨胀/缩水。
* Why: 统一 1 点/级后退还点数 = 等级总和,与升级消耗严格对账,避免点数膨胀/缩水。
*/
resetTalents(): void {
if (!this.data.talents) this.data.talents = {};
let refund = 0;
for (const key in this.data.talents) {
const lv = this.data.talents[key];
for (let i = 0; i < lv; i++) {
const c = getTalentUpgradeCost(i);
if (c > 0) refund += c;
}
refund += this.data.talents[key];
}
this.data.talents = {};
this.data.talent_points = (this.data.talent_points ?? 0) + refund;

View File

@@ -4,13 +4,15 @@ import { HRole } from "./heroSet";
/**
* 天赋系统配置(局外成长:升级即激活,战斗中对英雄属性常驻加成)。
*
* 三阶结构(阶位逐级解锁,门槛见 TALENT_TIER_UNLOCK_THRESHOLD):
* - 一阶·基础天赋(9001-9099):全局生效,对所有英雄叠加;
* - 二阶·专精天赋(9101-9199):按 HRole 职业过滤,仅对匹配职业的英雄生效;
* - 三阶·超强天赋(9201-9219):全局(9201-9209)+ 职业(9211-9219)混合,数值幅度约为前两阶的 2 倍。
* 五层递进结构(逐层解锁,门槛见 TALENT_TIER_UNLOCK_THRESHOLD):
* - 第 1 层(9001-9099):恒解锁,建议放置全局基础属性;
* - 第 2 层(9101-9199):第 1 层累计投入 ≥5 点解锁;
* - 第 3 层(9201-9299):第 2 层累计投入 ≥5 点解锁;
* - 第 4 层(9301-9399):第 3 层累计投入 ≥5 点解锁;
* - 第 5 层(9401-9499):第 4 层累计投入 ≥5 点解锁。
*
* 解锁规则:一阶恒可升级;二阶需一阶累计投入 ≥10 点;三阶需二阶累计投入 ≥10 点。
* 专精/超强职业天赋不再有独立前置天赋,仅受阶位解锁约束。
* 解锁规则:第 1 层恒可升级;第 N 层需第 N-1 层累计投入 ≥5 点。
* 前置天赋(prereq_uuid/prereq_lv)为软前置:仅限制加点,不影响已有点数的战斗结算。
*
* 结算口径(与项目 buff 规范一致):
* - percent 模式:按英雄基础值(base_ap/base_hp/hero.def)相对放大;
@@ -23,14 +25,18 @@ export enum TalentValueMode {
Flat = 1,
}
/** 天赋阶位:一阶基础 / 二阶专精 / 三阶超强 */
/** 天赋层位:第 1 层 ~ 第 5 层递进解锁 */
export enum TalentTier {
/** 一阶:基础天赋,全局生效 */
Basic = 1,
/** 二阶:专精天赋,按职业过滤 */
Specialized = 2,
/** 三阶:超强天赋,全局 + 职业混合 */
Super = 3,
/** 第 1 层:恒解锁 */
L1 = 1,
/** 第 2 层:第 1 层投入 5 点解锁 */
L2 = 2,
/** 第 3 层:第 2 层投入 5 点解锁 */
L3 = 3,
/** 第 4 层:第 3 层投入 5 点解锁 */
L4 = 4,
/** 第 5 层:第 4 层投入 5 点解锁 */
L5 = 5,
}
/** 单条属性加成词条 */
@@ -47,7 +53,7 @@ export interface TalentAttrBonus {
export interface TalentConfig {
/** 天赋 uuid(段位见 TalentSet 注释) */
uuid: number;
/** 所属阶位(升级校验:升下一阶天赋需前一阶累计投入点数达标) */
/** 所属层位(升级校验:升下一层天赋需前一层累计投入点数达标) */
tier: TalentTier;
/** 天赋名称 */
name: string;
@@ -59,8 +65,16 @@ export interface TalentConfig {
bonuses: TalentAttrBonus[];
/** 最大等级 */
max_lv: number;
/** 专精/超强职业天赋:仅对该职业英雄生效;全局天赋省略此字段 */
/** 专精/职业天赋:仅对该职业英雄生效;全局天赋省略此字段 */
role?: HRole;
/** 软前置:前置天赋 uuid(与 prereq_lv 成对出现;缺省视为无前置) */
prereq_uuid?: number;
/** 软前置:前置天赋要求达到的等级(仅限制加点,不影响已有点数的结算) */
prereq_lv?: number;
/** L4 触发修正:触发类技能计数阈值减少量(每级,下限 1;与 bonuses 互斥) */
t_num_reduce_per_lv?: number;
/** L5 触发修正:触发类技能额外触发 1 次的概率(每级百分点;与 bonuses 互斥) */
extra_trigger_chance_per_lv?: number;
}
/** 单条已解析的加成(等级已乘算) */
@@ -84,208 +98,201 @@ export interface ITalentTarget {
base_ap: number;
/** 基础血量快照(percent 模式的基数) */
base_hp: number;
/** L4 写入:触发类技能计数阈值减少量 */
talent_t_num_reduce: number;
/** L5 写入:触发类技能额外触发 1 次的概率(百分点) */
talent_extra_trigger_chance: number;
/** 防御基础值(percent 模式的基数,等于 hero.def) */
[key: string]: any;
}
/**
* 天赋配置表。
* uuid 段位:9001-9099 一阶基础(全局);9101-9199 二阶专精(按职业);
* 9201-9209 三阶超强·全局;9211-9219 三阶超强·职业。
* uuid 段位:9001-9099 第 1 层;9101-9199 第 2 层;9201-9299 第 3 层;
* 9301-9399 第 4 层;9401-9499 第 5 层。
* 每层建议放置 4-6 个天赋;前置天赋用 prereq_uuid + prereq_lv 声明(软前置,仅限制加点)。
* icon 复用 SkillSet/uiconsAtlas 既有帧名,后续可在编辑器侧替换为专属图标。
*/
export const TalentSet: Record<number, TalentConfig> = {
// ==================== 一阶·基础天赋(9001-9099,全局生效) ====================
// ==================== 第 1 层(9001-9099,恒解锁,基础属性) ====================
9001: {
uuid: 9001, tier: TalentTier.Basic, name: "攻击强化", info: "所有英雄攻击 +{value}%", icon: "3-Aggression",
uuid: 9001, tier: TalentTier.L1, name: "攻击强化", info: "所有英雄攻击 +{value}%", icon: "3-Aggression",
bonuses: [{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 3 }],
max_lv: 5,
},
9002: {
uuid: 9002, tier: TalentTier.Basic, name: "生命强化", info: "所有英雄生命 +{value}%", icon: "29-Endure",
uuid: 9002, tier: TalentTier.L1, name: "生命强化", info: "所有英雄生命 +{value}%", icon: "29-Endure",
bonuses: [{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 5 }],
max_lv: 5,
},
9003: {
uuid: 9003, tier: TalentTier.Basic, name: "防御强化", info: "所有英雄防御 +{value} 点", icon: "46-Iron-Clad",
uuid: 9003, tier: TalentTier.L1, name: "防御强化", info: "所有英雄防御 +{value} 点", icon: "46-Iron-Clad",
bonuses: [{ attr: Attrs.defense, mode: TalentValueMode.Flat, per_lv: 8 }],
max_lv: 5,
},
9004: {
uuid: 9004, tier: TalentTier.Basic, name: "暴击强化", info: "所有英雄暴击率 +{value}%", icon: "27-Hawkeye",
uuid: 9004, tier: TalentTier.L1, name: "反伤荆棘", info: "所有英雄反伤 +{value} 点", icon: "45-Induration",
bonuses: [{ attr: Attrs.thorns_dmg, mode: TalentValueMode.Flat, per_lv: 5 }],
max_lv: 5,
},
// ==================== 第 2 层(9101-9199,第 1 层投入 5 点解锁,概率属性) ====================
9101: {
uuid: 9101, tier: TalentTier.L2, name: "暴击强化", info: "所有英雄暴击率 +{value}%", icon: "27-Hawkeye",
bonuses: [{ attr: Attrs.critical, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9005: {
uuid: 9005, tier: TalentTier.Basic, name: "击晕强化", info: "所有英雄击晕率 +{value}%", icon: "13-Concussion",
bonuses: [{ attr: Attrs.stun_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9006: {
uuid: 9006, tier: TalentTier.Basic, name: "冰冻强化", info: "所有英雄冰冻率 +{value}%", icon: "92-Frost-Bite",
bonuses: [{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9007: {
uuid: 9007, tier: TalentTier.Basic, name: "击退强化", info: "所有英雄击退距离 +{value}", icon: "34-Force-Push",
9102: {
uuid: 9102, tier: TalentTier.L2, name: "击退强化", info: "所有英雄击退距离 +{value}", icon: "34-Force-Push",
bonuses: [{ attr: Attrs.knockback_distance, mode: TalentValueMode.Flat, per_lv: 10 }],
max_lv: 5,
},
// 风怒机制已停用: 9008 风怒强化天赋已移除
/*
9008: {
uuid: 9008, tier: TalentTier.Basic, name: "风怒强化", info: "所有英雄风怒率 +{value}%", icon: "98-Enchant-Wind",
bonuses: [{ attr: Attrs.wfuny, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
*/
// ==================== 二阶·专精天赋(9101-9199,按职业过滤;一阶投入 10 点解锁) ====================
9101: {
uuid: 9101, tier: TalentTier.Specialized, name: "坦克专精", info: "坦克防御 +{value}%、生命 +{value2}%", icon: "36-Guardian-Stance",
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,
},
9102: {
uuid: 9102, tier: TalentTier.Specialized, name: "战士专精", info: "战士生命 +{value}%", icon: "1-Battle-Roar",
bonuses: [{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 12 }],
max_lv: 5, role: HRole.Warrior,
},
9103: {
uuid: 9103, tier: TalentTier.Specialized, name: "刺客专精", info: "刺客攻击 +{value}%、暴击率 +{value2}%", icon: "127-Hide",
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,
uuid: 9103, tier: TalentTier.L2, name: "冰冻强化", info: "所有英雄冰冻率 +{value}%", icon: "92-Frost-Bite",
bonuses: [{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9104: {
uuid: 9104, tier: TalentTier.Specialized, name: "射手专精", info: "射手攻击 +{value}%、穿透率 +{value2}%", icon: "56-Pierce-Shot",
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,
uuid: 9104, tier: TalentTier.L2, name: "击晕强化", info: "所有英雄击晕率 +{value}%", icon: "13-Concussion",
bonuses: [{ attr: Attrs.stun_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9105: {
uuid: 9105, tier: TalentTier.Specialized, name: "法师专精", info: "法师攻击 +{value}%、冰冻率 +{value2}%", icon: "145-Electromancy",
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,
uuid: 9105, tier: TalentTier.L2, name: "穿透强化", info: "所有英雄穿透率 +{value}%", icon: "56-Pierce-Shot",
bonuses: [{ attr: Attrs.puncture_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9106: {
uuid: 9106, tier: TalentTier.Specialized, name: "辅助专精", info: "辅助治疗量 +{value}%、施加的增益效果 +{value2}%", icon: "38-Healing-Light",
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,
uuid: 9106, tier: TalentTier.L2, name: "暴伤强化", info: "所有英雄暴击伤害 +{value}%", icon: "37-Head-Shot",
bonuses: [{ attr: Attrs.critical_damage, mode: TalentValueMode.Flat, per_lv: 5 }],
max_lv: 5,
},
// ==================== 三阶·超强天赋(9201-9219,二阶投入 10 点解锁;幅度约为前两阶 2 倍) ====================
// ---- 全局超强(9201-9209) ----
// ==================== 第 3 层(9201-9299,第 2 层投入 5 点解锁,职业加成) ====================
9201: {
uuid: 9201, tier: TalentTier.Super, name: "神威降临", info: "所有英雄攻击 +{value}%、生命 +{value2}%", icon: "52-Might-of-Heaven",
uuid: 9201, tier: TalentTier.L3, name: "坦克专精", info: "坦克生命 +{value}%、防御 +{value2} 点", icon: "36-Guardian-Stance",
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: "37-Head-Shot",
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: "76-Shove-Stab",
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: "45-Induration",
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: "36-Guardian-Stance",
bonuses: [
{ attr: Attrs.defense, mode: TalentValueMode.Percent, per_lv: 40 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.defense, mode: TalentValueMode.Flat, per_lv: 10 },
],
max_lv: 5, role: HRole.Tank,
},
9212: {
uuid: 9212, tier: TalentTier.Super, name: "狂血战魂", info: "战士攻击 +{value}%、生命 +{value2}%", icon: "113-Rage",
9202: {
uuid: 9202, tier: TalentTier.L3, name: "战士专精", info: "战士攻击 +{value}%、生命 +{value2}%", icon: "1-Battle-Roar",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 10 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 6 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 6 },
],
max_lv: 5, role: HRole.Warrior,
},
9213: {
uuid: 9213, tier: TalentTier.Super, name: "影刃绝杀", info: "刺客攻击 +{value}%、暴击伤害 +{value2}%", icon: "136-Heart-Rip",
9203: {
uuid: 9203, tier: TalentTier.L3, name: "刺客专精", info: "刺客攻击 +{value}%、攻速 +{value2}%", icon: "127-Hide",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 12 },
{ attr: Attrs.critical_damage, mode: TalentValueMode.Flat, per_lv: 12 },
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.speed, mode: TalentValueMode.Flat, per_lv: 3 },
],
max_lv: 5, role: HRole.Assassin,
},
9214: {
uuid: 9214, tier: TalentTier.Super, name: "穿云贯日", info: "射手攻击 +{value}%、穿透伤害 +{value2}%", icon: "37-Head-Shot",
9204: {
uuid: 9204, tier: TalentTier.L3, name: "射手专精", info: "射手攻击 +{value}%、攻速 +{value2}%", icon: "56-Pierce-Shot",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 12 },
{ attr: Attrs.puncture_dmg_bonus, mode: TalentValueMode.Flat, per_lv: 8 },
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.speed, mode: TalentValueMode.Flat, per_lv: 3 },
],
max_lv: 5, role: HRole.Archer,
},
9215: {
uuid: 9215, tier: TalentTier.Super, name: "元素主宰", info: "法师攻击 +{value}%、冰冻率 +{value2}%", icon: "144-Cryomancy",
9205: {
uuid: 9205, tier: TalentTier.L3, name: "法师专精", info: "法师攻击 +{value}%、技能增益效果 +{value2}%", icon: "145-Electromancy",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 12 },
{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 3 },
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 6 },
],
max_lv: 5, role: HRole.Mage,
},
9216: {
uuid: 9216, tier: TalentTier.Super, name: "圣光普照", info: "辅助治疗量 +{value}%、施加的增益效果 +{value2}%", icon: "40-Holy",
9206: {
uuid: 9206, tier: TalentTier.L3, name: "辅助专精", info: "辅助治疗量 +{value}%、增益效果 +{value2}%", icon: "38-Healing-Light",
bonuses: [
{ attr: Attrs.heal_power, mode: TalentValueMode.Flat, per_lv: 15 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 12 },
{ attr: Attrs.heal_power, mode: TalentValueMode.Flat, per_lv: 10 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 6 },
],
max_lv: 5, role: HRole.Support,
},
// ==================== 第 4 层(9301-9399,第 3 层投入 5 点解锁,触发条件减少,按职业) ====================
9301: {
uuid: 9301, tier: TalentTier.L4, name: "坦克·蓄势", info: "坦克触发类技能所需次数 -{value}", icon: "36-Guardian-Stance",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Tank,
},
9302: {
uuid: 9302, tier: TalentTier.L4, name: "战士·蓄势", info: "战士触发类技能所需次数 -{value}", icon: "113-Rage",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Warrior,
},
9303: {
uuid: 9303, tier: TalentTier.L4, name: "刺客·蓄势", info: "刺客触发类技能所需次数 -{value}", icon: "136-Heart-Rip",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Assassin,
},
9304: {
uuid: 9304, tier: TalentTier.L4, name: "射手·蓄势", info: "射手触发类技能所需次数 -{value}", icon: "74-Satisfaction",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Archer,
},
9305: {
uuid: 9305, tier: TalentTier.L4, name: "法师·蓄势", info: "法师触发类技能所需次数 -{value}", icon: "144-Cryomancy",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Mage,
},
9306: {
uuid: 9306, tier: TalentTier.L4, name: "辅助·蓄势", info: "辅助触发类技能所需次数 -{value}", icon: "40-Holy",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Support,
},
// ==================== 第 5 层(9401-9499,第 4 层投入 5 点解锁,概率额外触发,按职业) ====================
9401: {
uuid: 9401, tier: TalentTier.L5, name: "坦克·余震", info: "坦克触发类技能 +{value}% 概率额外触发 1 次", icon: "36-Guardian-Stance",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Tank, prereq_uuid: 9301, prereq_lv: 1,
},
9402: {
uuid: 9402, tier: TalentTier.L5, name: "战士·余威", info: "战士触发类技能 +{value}% 概率额外触发 1 次", icon: "113-Rage",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Warrior, prereq_uuid: 9302, prereq_lv: 1,
},
9403: {
uuid: 9403, tier: TalentTier.L5, name: "刺客·残影", info: "刺客触发类技能 +{value}% 概率额外触发 1 次", icon: "136-Heart-Rip",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Assassin, prereq_uuid: 9303, prereq_lv: 1,
},
9404: {
uuid: 9404, tier: TalentTier.L5, name: "射手·连珠", info: "射手触发类技能 +{value}% 概率额外触发 1 次", icon: "74-Satisfaction",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Archer, prereq_uuid: 9304, prereq_lv: 1,
},
9405: {
uuid: 9405, tier: TalentTier.L5, name: "法师·回响", info: "法师触发类技能 +{value}% 概率额外触发 1 次", icon: "144-Cryomancy",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Mage, prereq_uuid: 9305, prereq_lv: 1,
},
9406: {
uuid: 9406, tier: TalentTier.L5, name: "辅助·福音", info: "辅助触发类技能 +{value}% 概率额外触发 1 次", icon: "40-Holy",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Support, prereq_uuid: 9306, prereq_lv: 1,
},
};
/** 天赋升级消耗:按阶位统一消耗,同一阶内每级消耗相同 */
export const TALENT_UPGRADE_COST: Record<number, number> = {
[TalentTier.Basic]: 1, // 基础天赋:1 点升 1 级
[TalentTier.Specialized]: 2, // 专精天赋:2 点升 1 级
[TalentTier.Super]: 2, // 传说天赋:2 点升 1 级
};
/** 天赋升级消耗:全部天赋统一 1 点/级 */
export const TALENT_UPGRADE_COST = 1;
/**
* 查询升级所需天赋点数
* @param tier 天赋所属阶位
* @returns 升 1 级所需点数;非法阶位返回 -1
* @param _tier 天赋所属层位(保留参数以兼容旧调用;统一点价后不再按层区分)
* @returns 升 1 级所需点数(恒为 1)
*/
export function getTalentUpgradeCost(tier: TalentTier): number {
return TALENT_UPGRADE_COST[tier] ?? -1;
export function getTalentUpgradeCost(_tier: TalentTier): number {
return TALENT_UPGRADE_COST;
}
/** 天赋等级上限(不含):等级从 0 起步,区间 0~29,共 30 级;每升 1 级产出 1 点天赋点 */
@@ -308,42 +315,51 @@ export function getTalentLevelExp(curLv: number): number {
return TALENT_LEVEL_EXP[curLv];
}
/** 阶位解锁门槛:前一阶累计投入 ≥ 该点数才能升级下一阶天赋 */
export const TALENT_TIER_UNLOCK_THRESHOLD = 10;
/** 层位解锁门槛:前一层累计投入 ≥ 该点数才能升级下一层天赋 */
export const TALENT_TIER_UNLOCK_THRESHOLD = 5;
/**
* 计算指定阶位已投入的天赋点数(按该阶统一消耗 × 等级累加)。
* Why: 阶位解锁按"实际消耗点数"而非"等级总和"口径——与玩家直觉(投入了多少点)
* 计算指定层位已投入的天赋点数(统一点价 × 等级累加)。
* Why: 层位解锁按"实际消耗点数"而非"等级总和"口径——与玩家直觉(投入了多少点)
* 及重置退款口径(resetTalents 同表对账)保持一致。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param tier 目标阶位
* @returns 该阶位全部天赋已投入的点数总和
* @param tier 目标层位
* @returns 该层位全部天赋已投入的点数总和
*/
export function getTalentTierInvestedPoints(levels: Record<number, number>, tier: TalentTier): number {
if (!levels) return 0;
const cost = getTalentUpgradeCost(tier);
if (cost <= 0) return 0;
let invested = 0;
for (const key in levels) {
const cfg = TalentSet[key];
if (!cfg || cfg.tier !== tier) continue;
invested += levels[key] * cost;
invested += levels[key] * TALENT_UPGRADE_COST;
}
return invested;
}
/**
* 判断指定阶位是否已解锁(加点入口校验用;战斗结算无需关心阶位——已投入的等级恒生效)。
* 规则:一阶恒解锁;二阶要求一阶累计投入 ≥ TALENT_TIER_UNLOCK_THRESHOLD;三阶要求二阶累计投入达标。
* 判断指定层位是否已解锁(加点入口校验用;战斗结算无需关心层位——已投入的等级恒生效)。
* 规则:第 1 层恒解锁;第 N 层要求第 N-1 层累计投入 ≥ TALENT_TIER_UNLOCK_THRESHOLD。
* @param levels 天赋等级表
* @param tier 目标阶位
* @param tier 目标层位
*/
export function isTalentTierUnlocked(levels: Record<number, number>, tier: TalentTier): boolean {
if (tier === TalentTier.Basic) return true;
if (tier === TalentTier.L1) return true;
const prevTier = (tier - 1) as TalentTier;
return getTalentTierInvestedPoints(levels, prevTier) >= TALENT_TIER_UNLOCK_THRESHOLD;
}
/**
* 判断天赋的软前置是否已满足(加点入口校验用;未满足仅限制加点,不影响已有点数的结算)。
* @param levels 天赋等级表
* @param cfg 天赋配置
* @returns 无前置声明或前置天赋等级达标时返回 true
*/
export function isTalentPrereqMet(levels: Record<number, number>, cfg: TalentConfig): boolean {
if (!cfg.prereq_uuid || !cfg.prereq_lv) return true;
return (levels?.[cfg.prereq_uuid] ?? 0) >= cfg.prereq_lv;
}
/** 天赋属性的卡面显示名(迷你卡空间有限,用短标签) */
const TALENT_ATTR_LABELS: Partial<Record<Attrs, string>> = {
[Attrs.ap]: "攻击",
@@ -376,6 +392,15 @@ const TALENT_ATTR_SUFFIX: Partial<Record<Attrs, string>> = {
*/
export function getTalentDisplayText(cfg: TalentConfig, lv: number): string {
const mult = lv > 0 ? lv : 1;
// L4/L5 效果词条:不走 bonuses,按专属字段生成文案
if (cfg.t_num_reduce_per_lv) {
const text = `触发次数 -${cfg.t_num_reduce_per_lv * mult}`;
return lv > 0 ? text : `${text}/级`;
}
if (cfg.extra_trigger_chance_per_lv) {
const text = `额外触发 +${cfg.extra_trigger_chance_per_lv * mult}%`;
return lv > 0 ? text : `${text}/级`;
}
const parts: string[] = [];
for (const b of cfg.bonuses) {
const label = TALENT_ATTR_LABELS[b.attr] ?? String(b.attr);
@@ -388,8 +413,8 @@ export function getTalentDisplayText(cfg: TalentConfig, lv: number): string {
/**
* 聚合指定职业英雄生效的全部天赋加成。
* Why: 基础/超强全局天赋全局生效,专精与超强职业天赋按 role 过滤;
* 阶位解锁只约束加点阶段,战斗加载时(Hero.load / applyHeroLevel)按当前等级直接取加成。
* Why: 全局天赋全局生效,职业天赋按 role 过滤;
* 层位解锁与软前置只约束加点阶段,战斗加载时(Hero.load / applyHeroLevel)按当前等级直接取加成。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param role 英雄职业(职业过滤依据;无 role 时只应用全局天赋)
* @returns 已解析的加成列表(等级已乘算进 value)
@@ -412,7 +437,47 @@ export function getTalentBonusesForRole(levels: Record<number, number>, role?: H
}
/**
* 将天赋加成写入英雄属性模型(在 Hero.load / MissionCardComp.applyHeroLevel 的属性组装段调用)。
* 聚合指定职业英雄的 L4 触发阈值修正量。
* @param levels 天赋等级表
* @param role 英雄职业
* @returns 触发计数阈值的总减少量(SkillTriggerHelper 端再 clamp 下限 1)
*/
export function getTalentTriggerNumReduce(levels: Record<number, number>, role?: HRole): number {
let total = 0;
if (!levels) return total;
for (const key in levels) {
const lv = levels[key];
if (lv <= 0) continue;
const cfg = TalentSet[key];
if (!cfg || !cfg.t_num_reduce_per_lv) continue;
if (cfg.role !== undefined && cfg.role !== role) continue;
total += cfg.t_num_reduce_per_lv * lv;
}
return total;
}
/**
* 聚合指定职业英雄的 L5 额外触发概率(百分点)。
* @param levels 天赋等级表
* @param role 英雄职业
* @returns 额外触发 1 次的概率(百分点,可超过 100 由消费端自行 clamp)
*/
export function getTalentExtraTriggerChance(levels: Record<number, number>, role?: HRole): number {
let total = 0;
if (!levels) return total;
for (const key in levels) {
const lv = levels[key];
if (lv <= 0) continue;
const cfg = TalentSet[key];
if (!cfg || !cfg.extra_trigger_chance_per_lv) continue;
if (cfg.role !== undefined && cfg.role !== role) continue;
total += cfg.extra_trigger_chance_per_lv * lv;
}
return total;
}
/**
* 将天赋加成写入英雄属性模型(统一由 HeroAttrsComp.applyTalent 调用)。
*
* 结算规则:
* - Percent 模式:以基础快照为基数(ap→base_ap,hp_max→base_hp,defense→def_base),按百分比放大后累加;

View File

@@ -13,7 +13,6 @@ import { Attrs } from "../common/config/HeroAttrs";
import { MoveComp } from "./MoveComp";
import { mLogger } from "../common/Logger";
import { FieldSkillType } from "../common/config/SkillSet";
import { applyTalentBonuses } from "../common/config/TalentSet";
/** 英雄实体:负责英雄节点创建、属性初始化、入场动画与销毁流程 */
@ecs.register(`Hero`)
@@ -173,16 +172,6 @@ export class Hero extends ecs.Entity {
model.skills[skill.uuid] = { ...skill, lv: Math.min(FightSet.HERO_SKILL_MAX_LV, Math.max(0, skill.lv + hero_lv - 2)), ccd: 0, overrides: resolveSkillOverrides(skill, hero_lv) };
}
// 按当前英雄等级 resolve 触发技能/光环到运行时缓存,供 SkillTriggerHelper 消费
model.runtime_call = resolveTriggerByLv(model.call, hero_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, hero_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, hero_lv);
model.runtime_fend = resolveTriggerByLv(model.fend, hero_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, hero_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, hero_lv);
model.runtime_field = resolveFieldByLv(model.field, hero_lv);
// 复活已纳入 dead 触发组,无需单独解析 revive
// 应用额外属性加成(成长类型每级递增 + bonus 档位累加)
// 成长增量:仅英雄生效(model.fac 已为 HERO),按 grow_type 递增曲线累计
const grow = calcGrowBonus(hero, hero_lv);
@@ -211,12 +200,23 @@ export class Hero extends ecs.Entity {
// lv12 职业专项已移除:单项属性强化统一由 bonus 配置(如 attrs: { ap: 50 })按等级注入
// 天赋加成层:全局天赋(一阶基础/三阶超强全局)全局生效,职业天赋(二阶专精/三阶超强职业)按 role 过滤(仅英雄生效,怪物跳过)
// Why: 放在成长/bonus 之后、光环乘区之后,以基础快照为基数叠加,避免与乘区叠加成乘法膨胀
// 天赋加成层:全局天赋(L1/L2)全局生效,职业天赋(L3-L5)按 role 过滤(仅英雄生效,怪物跳过)
// Why: 放在成长/bonus 之后、runtime_* resolve 之前——触发修正需先写入模型,L4/L5 才能影响阈值判定;
// 属性加成以基础快照为基数叠加,避免与光环乘区叠加成乘法膨胀
if (model.fac === FacSet.HERO) {
applyTalentBonuses(model, smc.getTalentLevels(), model.role);
model.applyTalent();
}
// 按当前英雄等级 resolve 触发技能/光环到运行时缓存,供 SkillTriggerHelper 消费
model.runtime_call = resolveTriggerByLv(model.call, hero_lv);
model.runtime_dead = resolveTriggerByLv(model.dead, hero_lv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, hero_lv);
model.runtime_fend = resolveTriggerByLv(model.fend, hero_lv);
model.runtime_atking = resolveTriggerByLv(model.atking, hero_lv);
model.runtime_atked = resolveTriggerByLv(model.atked, hero_lv);
model.runtime_field = resolveFieldByLv(model.field, hero_lv);
// 复活已纳入 dead 触发组,无需单独解析 revive
// 缓存技能射程等派生数据,减少战斗帧内重复计算
model.updateSkillDistanceCache();

View File

@@ -5,6 +5,7 @@ import { FacSet, FightSet } from "../common/config/GameSet";
import { FieldSkillSet, FieldSkillType, SkillOverrides } from "../common/config/SkillSet";
import { smc } from "../common/SingletonModuleComp";
import { Attrs } from "../common/config/HeroAttrs";
import { applyTalentBonuses, getTalentExtraTriggerChance, getTalentTriggerNumReduce } from "../common/config/TalentSet";
import { FieldSkillHelper } from "./FieldSkillHelper";
import { BuffComp } from "./BuffComp";
import { BuffList, ModOp } from "../common/config/BuffSet";
@@ -101,6 +102,10 @@ export class HeroAttrsComp extends ecs.Comp {
thorns_dmg: number = 0; // 反伤伤害(固定值,受到攻击时对攻击者造成等量伤害)
thorns_rate: number = 0; // 反伤比例(百分点;反伤值 = 实际受到伤害 * thorns_rate / 100 + thorns_dmg)
// ==================== 天赋触发修正(L4/L5 写入,SkillTriggerHelper 消费) ====================
talent_t_num_reduce: number = 0; // L4:触发类技能计数阈值减少量(下限 1 由消费端 clamp)
talent_extra_trigger_chance: number = 0; // L5:触发类技能额外触发 1 次的概率(百分点)
revived_count: number = 0; // 已复活次数(从 dead 触发配置的 revive_count 限制)
death_count: number = 0; // 本回合死亡次数(用于 dead 触发统计)
invincible_time: number = 0;// 无敌时间
@@ -154,6 +159,20 @@ export class HeroAttrsComp extends ecs.Comp {
*/
initAttrs() {
}
/**
* 天赋统一应用入口:写入属性加成(L1-L3)与触发修正(L4/L5)。
* Why: 收敛到属性组件单点调用,Hero.load 与局内升级重算(applyHeroLevel)不再各自 import TalentSet;
* 触发修正必须在 runtime_* 触发缓存 resolve 之前写入,才能影响 SkillTriggerHelper 的阈值判定。
* 调用前请确保触发修正字段已归零(升级重算路径由调用方归零,保证幂等)。
*/
applyTalent(): void {
if (this.fac !== FacSet.HERO) return;
const levels = smc.getTalentLevels();
this.talent_t_num_reduce = getTalentTriggerNumReduce(levels, this.role);
this.talent_extra_trigger_chance = getTalentExtraTriggerChance(levels, this.role);
applyTalentBonuses(this, levels, this.role);
}
/*******************基础属性管理********************/
add_hp(value: number) {
@@ -765,6 +784,8 @@ export class HeroAttrsComp extends ecs.Comp {
this.buff_power = 0;
this.thorns_dmg = 0;
this.thorns_rate = 0;
this.talent_t_num_reduce = 0;
this.talent_extra_trigger_chance = 0;
this.boom = false;
// 重置技能距离缓存

View File

@@ -98,6 +98,7 @@ export class SkillTriggerHelper {
/**
* 处理攻击附加技能
* 判定逻辑:依据配置中的 t_num(触发所需普攻次数),对比当前的普攻累计次数取模判定。
* 天赋修正:L4 减少 t_num 阈值(下限 1);L5 按概率额外触发 1 次。
*/
private static handleAtking(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.runtime_atking || model.runtime_atking.length === 0) return;
@@ -107,10 +108,17 @@ export class SkillTriggerHelper {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.AtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
// L5 天赋:概率额外触发 1 次(与羁绊次数加成独立乘算)
if (model.talent_extra_trigger_chance > 0 && Math.random() * 100 < model.talent_extra_trigger_chance) {
triggerCount += 1;
}
// L4 天赋:减少触发所需次数(下限 1)
const tNumReduce = model.talent_t_num_reduce;
model.runtime_atking.forEach(atkConfig => {
// atk_count 代表已进行的普攻次数。当其余数刚好整除配置阈值时触发。
if (model.atk_count > 0 && model.atk_count % atkConfig.t_num === 0) {
const tNum = Math.max(1, atkConfig.t_num - tNumReduce);
if (model.atk_count > 0 && model.atk_count % tNum === 0) {
for (let i = 0; i < triggerCount; i++) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atking, atkConfig.overrides);
}
@@ -121,6 +129,7 @@ export class SkillTriggerHelper {
/**
* 处理受击附加技能
* 判定逻辑:依据配置中的 t_num(触发所需受击次数),对比当前的受击累计次数取模判定。
* 天赋修正:L4 减少 t_num 阈值(下限 1);L5 按概率额外触发 1 次。
*/
private static handleAtked(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.runtime_atked || model.runtime_atked.length === 0) return;
@@ -130,10 +139,17 @@ export class SkillTriggerHelper {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.BeAtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
// L5 天赋:概率额外触发 1 次(与羁绊次数加成独立乘算)
if (model.talent_extra_trigger_chance > 0 && Math.random() * 100 < model.talent_extra_trigger_chance) {
triggerCount += 1;
}
// L4 天赋:减少触发所需次数(下限 1)
const tNumReduce = model.talent_t_num_reduce;
model.runtime_atked.forEach(atkConfig => {
// atked_count 代表已承受的受击次数。当其余数刚好整除配置阈值时触发。
if (model.atked_count > 0 && model.atked_count % atkConfig.t_num === 0) {
const tNum = Math.max(1, atkConfig.t_num - tNumReduce);
if (model.atked_count > 0 && model.atked_count % tNum === 0) {
for (let i = 0; i < triggerCount; i++) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atked, atkConfig.overrides);
}

View File

@@ -54,7 +54,6 @@ import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo, HType, resolveTriggerByLv, resolveFieldByLv, resolveSkillOverrides, calcGrowBonus, getHeroQualityCoef } from "../common/config/heroSet";
import { applyTalentBonuses } from "../common/config/TalentSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FacSet, FightSet } from "../common/config/GameSet";
import { MissionEconomy } from "./MissionEconomy";
@@ -2094,13 +2093,16 @@ export class MissionCardComp extends CCComp {
model.knockback_distance = 0;
model.heal_power = 0;
model.buff_power = 0;
applyTalentBonuses(model, smc.getTalentLevels(), model.role);
// 天赋触发修正归零(applyTalent 内部重算,保证升级重算幂等)
model.talent_t_num_reduce = 0;
model.talent_extra_trigger_chance = 0;
model.applyTalent();
}
// 血量按当前血线比例映射到新上限,保留升级前战斗状态
model.hp = Math.max(1, Math.floor(model.hp_max * Math.max(0, Math.min(1, hpRate))));
// 重新解析触发技能/驻场光环/复活到运行时缓存(与 Hero.load 对齐)
// 重新解析触发技能/驻场光环/复活到运行时缓存(在 applyTalent 之后,确保 L4/L5 触发修正已写入模型)
model.runtime_call = resolveTriggerByLv(model.call, nextLv);
model.runtime_dead = resolveTriggerByLv(model.dead, nextLv);
model.runtime_fstart = resolveTriggerByLv(model.fstart, nextLv);

View File

@@ -1,40 +1,45 @@
/**
* @file TalentItemComp.ts
* @description 天赋迷你升级卡组件(UI 视图层)
* @description 天赋迷你升级卡组件(UI 视图层,编辑器静态摆放)
*
* 职责:
* 1. 接收 TalentsComp 下发的天赋配置 + 当前等级 + 阶位解锁态,渲染迷你点选卡。
* 1. 通过编辑器配置的 uuid 自查询天赋配置 + 当前等级 + 层位解锁态,渲染迷你点选卡。
* 2. 卡面自包含:图标 / 名称 / 当前已生效数值(Lv0 显示每级数值引导)/ 右下「当前/满级」角标。
* 3. 阶位背景:普通背景节点下挂 blue / red / yellow 三个颜色子节点,按阶位激活其一(Ⅰ蓝 / Ⅱ红 / Ⅲ金)。
* 4. 状态机:满级(MAX 金角标)/ 未激活(左上锁角标,不再做灰阶处理)。
* 3. 层位背景:普通背景节点下挂 blue / red / yellow 三个颜色子节点,按层位映射激活其一(L1蓝 / L2红 / L3金,L4/L5 复用金)。
* 4. 状态机:满级(MAX 金角标)/ 未激活(左上锁角标:未加点 / 层位未解锁 / 前置未满足,不做灰阶处理)。
* 5. 点击回调上抛卡体点击,加点确认弹窗与升级校验统一由 TalentsComp 处理(单一决策点)。
*
* 依赖:
* - TalentSet.TalentConfig / TalentTier / getTalentDisplayText —— 配置与卡面文案
* - TalentSet(TalentConfig/TalentTier/getTalentDisplayText/isTalentTierUnlocked/isTalentPrereqMet)—— 配置与校验
* - 背景三颜色子节点(blue / red / yellow,编辑器侧搭建)
* - smc.uiconsAtlas —— 图标解析(缺省回退资源目录)
* - smc —— 天赋等级表与图标图集(缺省回退资源目录)
*/
import { _decorator, Label, Node, NodeEventType, Sprite, SpriteFrame } from "cc";
import { _decorator, CCInteger, Label, Node, NodeEventType, Sprite, SpriteFrame } 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 { TalentConfig, TalentTier, getTalentDisplayText } from "../common/config/TalentSet";
import { TalentSet, TalentTier, getTalentDisplayText, isTalentPrereqMet, isTalentTierUnlocked } from "../common/config/TalentSet";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "db://oops-framework/core/Oops";
const { ccclass, property } = _decorator;
/** 阶位 → 背景节点下的颜色子节点名(Ⅰ蓝 / Ⅱ红 / Ⅲ金) */
/** 层位 → 背景节点下的颜色子节点名(L1蓝 / L2红 / L3金;L4/L5 复用金) */
const TIER_BG_CHILD_NAMES: Record<number, string> = {
[TalentTier.Basic]: "blue",
[TalentTier.Specialized]: "red",
[TalentTier.Super]: "yellow",
[TalentTier.L1]: "blue",
[TalentTier.L2]: "red",
[TalentTier.L3]: "yellow",
[TalentTier.L4]: "yellow",
[TalentTier.L5]: "yellow",
};
/** TalentItemComp —— 天赋迷你升级卡组件 */
/** TalentItemComp —— 天赋迷你升级卡组件(编辑器静态摆放,按 uuid 自驱动渲染) */
@ccclass('TalentItemComp')
@ecs.register('TalentItem', false)
export class TalentItemComp extends CCComp {
@property({ type: CCInteger, tooltip: "天赋 uuid(TalentSet key,编辑器侧配置)" })
talent_uuid: number = 0;
@property({ type: Label, tooltip: "天赋名称" })
lbl_name: Label = null!;
@@ -47,21 +52,18 @@ export class TalentItemComp extends CCComp {
@property({ type: Sprite, tooltip: "天赋图标" })
icon: Sprite = null!;
@property({ type: Node, tooltip: "卡牌背景节点(下挂 blue / red / yellow 三颜色子节点,按阶位激活其一)" })
@property({ type: Node, tooltip: "卡牌背景节点(下挂 blue / red / yellow 三颜色子节点,按层位激活其一)" })
bg: Node = null!;
@property({ type: Node, tooltip: "MAX 满级角标" })
node_max: Node = null!;
@property({ type: Node, tooltip: "未激活锁遮罩/锁图标(未加点或阶位未解锁)" })
@property({ type: Node, tooltip: "未激活锁遮罩/锁图标(未加点、层位未解锁或前置未满足)" })
node_lock: Node = null!;
/** 点击回调(卡体与 + 徽标同源上抛),由父级统一校验升级 */
/** 点击回调(卡体点击上抛),由 TalentsComp 统一校验升级 */
public onTalentTap: ((uuid: number) => void) | null = null;
/** 当前展示的天赋 uuid(回调携带) */
private curUuid: number = 0;
protected onLoad(): void {
this.node.on(NodeEventType.TOUCH_END, this.onTap, this);
}
@@ -70,25 +72,24 @@ export class TalentItemComp extends CCComp {
this.node.off(NodeEventType.TOUCH_END, this.onTap, this);
}
/**
* 刷新迷你卡展示
* @param cfg 天赋配置(TalentSet 条目)
* @param lv 当前等级
* @param tierUnlocked 所属阶位是否已解锁
*/
public updateItem(cfg: TalentConfig, lv: number, tierUnlocked: boolean): void {
/** 刷新迷你卡展示:按编辑器配置的 uuid 自查询配置与等级,层位/前置校验内部完成 */
public updateItem(): void {
const cfg = TalentSet[this.talent_uuid];
if (!cfg) return;
this.curUuid = cfg.uuid;
const levels = smc.getTalentLevels();
const lv = levels[this.talent_uuid] ?? 0;
const tierUnlocked = isTalentTierUnlocked(levels, cfg.tier);
const prereqMet = isTalentPrereqMet(levels, cfg);
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}`;
// 未激活(Lv0 或阶位未解锁):仅显示 node_lock 锁遮罩,不做灰阶处理
const inactive = lv <= 0 || !tierUnlocked;
// 未激活(Lv0 / 层位未解锁 / 前置未满足):仅显示 node_lock 锁遮罩,不做灰阶处理
const inactive = lv <= 0 || !tierUnlocked || !prereqMet;
// 阶位背景:激活对应颜色子节点(Ⅰ蓝 / Ⅱ红 / Ⅲ金)
// 层位背景:激活对应颜色子节点(L1蓝 / L2红 / L3金)
if (this.bg) this.applyTierBg(cfg.tier);
// 图标:技能图标图集(gui/ui7)优先,资源目录回退(与 SCardComp 口径一致)
@@ -103,12 +104,12 @@ export class TalentItemComp extends CCComp {
if (frame) this.icon.spriteFrame = frame;
}
// 状态角标:满级 MAX / 未激活锁角标(未加点或阶位未解锁,置于左上角)
// 状态角标:满级 MAX / 未激活锁角标(置于左上角)
if (this.node_max) this.node_max.active = lv >= cfg.max_lv;
if (this.node_lock) this.node_lock.active = inactive;
}
/** 按阶位激活 bg 下对应颜色子节点,其余隐藏 */
/** 按层位激活 bg 下对应颜色子节点,其余隐藏 */
private applyTierBg(tier: TalentTier): void {
const curName = TIER_BG_CHILD_NAMES[tier];
for (const [, name] of Object.entries(TIER_BG_CHILD_NAMES)) {
@@ -120,7 +121,7 @@ export class TalentItemComp extends CCComp {
/** 卡体点击上抛 uuid,加点确认与升级校验由 TalentsComp 处理 */
private onTap(): void {
this.onTalentTap?.(this.curUuid);
this.onTalentTap?.(this.talent_uuid);
}
/** ECS 组件移除时销毁节点(CCComp 抽象方法实现) */

View File

@@ -1,25 +1,27 @@
/**
* @file TalentsComp.ts
* @description 天赋加点页组件(UI 视图层)
* @description 天赋加点页组件(UI 视图层,最小容器)
*
* 职责:
* 1. 三阶页签(Ⅰ 基础 / Ⅱ 专精 / Ⅲ 超强)状态机:单屏聚焦当前阶位的迷你天赋卡。
* 2. 页签常显解锁进度(前阶投入 x/10),点击天赋卡弹出 CommonPrompt 确认加点(smc.upgradeTalent)。
* 3. 监听 GameEvent.TALENT_UPDATE 全量重渲染(23 条卡片,无需虚拟列表)。
* 1. 页头展示:可用天赋点 / 天赋等级(Lv.x)/ 当前等级经验进度。
* 2. 收集子树内全部 TalentItemComp(编辑器静态摆放),TALENT_UPDATE 事件驱动全量刷新。
* 3. 卡片点击统一校验(层位解锁 → 软前置 → 满级 → 点数),不满足走 smalltip 气泡;
* 满足则弹出 CommonPrompt 确认加点(smc.upgradeTalent)。
* 4. 看广告重置入口:CommonPrompt 二次确认后调 smc.resetTalents() 退还全部点数。
*
* 依赖:
* - TalentSet(TalentSet/TalentTier/isTalentTierUnlocked/getTalentTierInvestedPoints)—— 配置与阶位校验
* - TalentItemComp —— 单张迷你升级卡
* - TalentSet(TalentTier/isTalentTierUnlocked/isTalentPrereqMet/getTalentTierInvestedPoints)—— 配置与层位校验
* - TalentItemComp —— 单张迷你升级卡(编辑器静态摆放,按 uuid 自驱动渲染)
* - smc —— 天赋等级表/点数/升级/重置
*/
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, ProgressBar, UITransform } from "cc";
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, TalentTier,
TalentSet,
TALENT_TIER_UNLOCK_THRESHOLD,
getTalentDisplayText, getTalentLevelExp, getTalentTierInvestedPoints, getTalentUpgradeCost, isTalentTierUnlocked,
getTalentDisplayText, getTalentLevelExp, getTalentTierInvestedPoints, getTalentUpgradeCost,
isTalentPrereqMet, isTalentTierUnlocked,
} from "../common/config/TalentSet";
import { GameEvent } from "../common/config/GameEvent";
import { UIID } from "../common/config/GameUIConfig";
@@ -30,36 +32,19 @@ 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 TIER_NAMES: Record<number, string> = {
1: "第 1 层", 2: "第 2 层", 3: "第 3 层", 4: "第 4 层", 5: "第 5 层",
};
/** 阶位页签选中高亮子节点名(可选,编辑器侧搭建) */
const TAB_SELECTED_CHILD = "sp_on";
/** 阶位页签锁定图标子节点名(可选;阶位未解锁时显示) */
const TAB_LOCKED_CHILD = "locked";
/** 阶位页签未选中态子节点名(可选;非当前选中页签时显示) */
const TAB_INACTIVE_CHILD = "noactive";
/** 页签/卡片上的 smalltip 气泡节点名 */
const SMALL_TIP_CHILD = "smalltip";
/** TalentsComp —— 天赋加点页组件(阶位 Tab 单屏聚焦) */
/** TalentsComp —— 天赋加点页组件(最小容器:页头 + 编辑器静态卡片收集) */
@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!;
@@ -72,52 +57,16 @@ export class TalentsComp extends CCComp {
@property({ type: Label, tooltip: "当前等级经验数值(当前经验/升级所需经验)" })
lbl_exp: 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: 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();
/** 页内全部天赋卡(onLoad 时自动收集子树内所有 TalentItemComp,无需编辑器拖引) */
private talent_items: TalentItemComp[] = [];
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.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);
}
@@ -133,30 +82,20 @@ export class TalentsComp extends CCComp {
}
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) {
// 剩余/总点数:总点数 = 剩余 + 各阶已投入
// 剩余/总点数:总点数 = 剩余 + 各层已投入
const remaining = smc.getTalentPoints();
const invested = getTalentTierInvestedPoints(levels, TalentTier.Basic)
+ getTalentTierInvestedPoints(levels, TalentTier.Specialized)
+ getTalentTierInvestedPoints(levels, TalentTier.Super);
let invested = 0;
for (let tier = 1; tier <= 5; tier++) {
invested += getTalentTierInvestedPoints(levels, tier);
}
this.lbl_points.string = `${remaining}/${remaining + invested}`;
}
@@ -178,66 +117,8 @@ export class TalentsComp extends CCComp {
: `0/0`;
}
// 仅首次进入时默认选中已解锁的最高阶,后续刷新保持玩家当前选择
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;
for (const item of this.talent_items) {
item.updateItem();
}
}
@@ -252,7 +133,7 @@ export class TalentsComp extends CCComp {
/**
* 在指定节点下弹出 smalltip 气泡提示
* @param anchor 气泡挂载的父节点(优先页签,卡片点击时传卡片节点)
* @param anchor 气泡挂载的父节点
* @param text 提示文案
*/
private showSmallTip(anchor: Node | null, text: string): void {
@@ -260,27 +141,45 @@ export class TalentsComp extends CCComp {
tip?.show(text);
}
/** 卡片点击统一入口:未解锁/已满级/点数不足走 smalltip 气泡;其余弹出 CommonPrompt 确认加点,成功后事件驱动刷新 */
/**
* 按 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);
const tab = this.tabNodes.get(cfg.tier);
// 层位解锁:第 N 层要求第 N-1 层累计投入 ≥ 阈值
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} 点解锁`);
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(tab?.node ?? null, "该天赋已满级");
this.showSmallTip(anchor, "该天赋已满级");
return;
}
const cost = getTalentUpgradeCost(cfg.tier);
if (cost <= 0) return;
if (smc.getTalentPoints() < cost) {
this.showSmallTip(tab?.node ?? null, "天赋点不足");
this.showSmallTip(anchor, "天赋点不足");
return;
}
@@ -294,7 +193,7 @@ export class TalentsComp extends CCComp {
maskClose: true,
okFunc: () => {
if (!smc.upgradeTalent(uuid)) {
this.showSmallTip(this.tabNodes.get(cfg.tier)?.node ?? null, "天赋点不足");
this.showSmallTip(anchor, "天赋点不足");
}
},
});