feat: 新增天赋系统,实现局外天赋养成与战斗属性加成

- 新增天赋配置表与相关枚举、数据结构
- 新增全局单例的天赋数据管理接口,支持升级、重置、点数管理
- 新增天赋属性加成结算逻辑,支持全局天赋与职业专精天赋
- 新增治疗强化、buff强化属性与对应天赋支持
- 新增天赋更新事件,同步UI与战斗数据
- 在英雄加载、升级流程中接入天赋加成计算
This commit is contained in:
panFD
2026-08-24 22:58:27 +08:00
parent 61e33ff754
commit 2fc085c3aa
9 changed files with 416 additions and 3 deletions

View File

@@ -9,6 +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";
/**
* 用远程数据覆盖本地数据(统一方法)
@@ -35,6 +36,10 @@ export interface GameDate {
timestamp?: number, // 用于比对本地与云端数据的最新状态
/** 英雄局外成长数据:key 为英雄 uuid,value 为成长记录 */
hero_growth?: Record<number, HeroGrowthRecord>,
/** 天赋等级表:key 为天赋 uuid,value 为当前等级(未记录视为 0 级) */
talents?: Record<number, number>,
/** 当前可用天赋点数(升级天赋消耗,经验/等级系统接入后由其产出) */
talent_points?: number,
}
export interface CloudData {
openid: string;
@@ -121,6 +126,10 @@ export class SingletonModuleComp extends ecs.Comp {
// 已满级:验证满级后 upgradeHero 返回 false
5004: { uuid: 5004, lv: 6, fragments: 999 },
} as Record<number, HeroGrowthRecord>,
/** 天赋等级表:key 为天赋 uuid,value 为当前等级 */
talents: {} as Record<number, number>,
/** 可用天赋点数 */
talent_points: 0,
}
vmdata: any = {
@@ -376,6 +385,87 @@ export class SingletonModuleComp extends ecs.Comp {
return true;
}
// ==================== 天赋系统 ====================
/**
* 获取天赋当前等级(未升级返回 0)
* @param uuid 天赋 uuid(TalentSet key)
*/
getTalentLv(uuid: number): number {
return this.data.talents?.[uuid] ?? 0;
}
/**
* 获取当前可用天赋点数
*/
getTalentPoints(): number {
return this.data.talent_points ?? 0;
}
/**
* 增加天赋点(经验/等级系统接入后由其调用)
* @param count 增加的点数(正数)
*/
addTalentPoints(count: number): void {
if (!this.data.talents) this.data.talents = {};
this.data.talent_points = Math.max(0, (this.data.talent_points ?? 0) + count);
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.TALENT_UPDATE);
}
/**
* 消耗天赋点升级天赋(升级即激活,下一局生效)
* @param uuid 天赋 uuid
* @returns 是否升级成功(天赋不存在/已满级/前置未满足/点数不足时返回 false)
*/
upgradeTalent(uuid: number): boolean {
const cfg = TalentSet[uuid];
if (!cfg) return false;
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;
}
}
const cost = getTalentUpgradeCost(curLv);
if (cost < 0 || this.getTalentPoints() < cost) return false;
this.data.talent_points -= cost;
this.data.talents[uuid] = curLv + 1;
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.TALENT_UPDATE);
return true;
}
/**
* 重置所有天赋等级并退还全部已消耗天赋点(看广告入口)
* Why: 退还点数按"升到当前等级的累计消耗"计算,与升级消耗表严格对账,避免点数膨胀/缩水。
*/
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;
}
}
this.data.talents = {};
this.data.talent_points = (this.data.talent_points ?? 0) + refund;
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.TALENT_UPDATE);
}
/**
* 获取天赋等级表(战斗加载时供 Hero.load / applyHeroLevel 聚合加成)
*/
getTalentLevels(): Record<number, number> {
return this.data.talents ?? {};
}
/**
* 在游戏载入早期调用,预加载常用图集
*/

View File

@@ -98,5 +98,7 @@ export enum GameEvent {
ComboReach = "ComboReach",
/** 英雄局外成长数据变更(碎片/等级更新,payload 为空,UI 重新读取 smc.data.hero_growth) */
HERO_GROWTH_UPDATE = "HERO_GROWTH_UPDATE",
/** 天赋数据变更(等级/点数更新,payload 为空,UI 重新读取 smc.data.talents / talent_points) */
TALENT_UPDATE = "TALENT_UPDATE",
}

View File

@@ -43,6 +43,8 @@ export enum Attrs {
wfuny = "wfuny", // 风怒
vulnerable_res = "vulnerable_res", // 易伤抗性(百分比减免易伤附加伤害)
paralyze_power = "paralyze_power", // 麻痹强化(百分点;提升麻痹对技能 CD 的减速量,累计达 100 则 CD 停止增长)
heal_power = "heal_power", // 治疗强化(百分点;放大该英雄施放技能的治疗量,辅助专精天赋写入)
buff_power = "buff_power", // buff 强化(百分点;放大该英雄施加的友方 buff 数值,辅助专精天赋写入)
}
/**

View File

@@ -0,0 +1,269 @@
import { Attrs } from "./HeroAttrs";
import { HRole } from "./heroSet";
/**
* 天赋系统配置(局外成长:升级即激活,战斗中对英雄属性常驻加成)。
*
* 双层结构:
* - 基础天赋:全局生效,对所有英雄叠加;
* - 专精天赋:按 HRole 职业过滤,仅对匹配职业的英雄生效,且要求前置天赋达到指定等级。
*
* 结算口径(与项目 buff 规范一致):
* - percent 模式:按英雄基础值(base_ap/base_hp/hero.def)相对放大;
* - flat 模式:概率/抗性类按百分点累加,defense 按固定点数累加。
*/
/** 天赋数值模式:percent=按基础值百分比放大;flat=固定值/百分点累加 */
export enum TalentValueMode {
Percent = 0,
Flat = 1,
}
/** 单条属性加成词条 */
export interface TalentAttrBonus {
/** 目标属性(Attrs 枚举键) */
attr: Attrs;
/** 数值模式 */
mode: TalentValueMode;
/** 每级数值(percent 模式为百分比;flat 模式为百分点/固定值) */
per_lv: number;
}
/** 天赋配置 */
export interface TalentConfig {
/** 天赋 uuid(段位见 TalentSet 注释) */
uuid: number;
/** 天赋名称 */
name: string;
/** 描述(UI 展示) */
info: string;
/** 属性加成词条列表(每级叠加) */
bonuses: TalentAttrBonus[];
/** 最大等级 */
max_lv: number;
/** 专精天赋:仅对该职业英雄生效;基础天赋省略此字段 */
role?: HRole;
/** 专精天赋:前置要求(前置天赋 uuid + 所需等级),全部满足才生效 */
requires?: { uuid: number; lv: number }[];
}
/** 单条已解析的加成(等级已乘算) */
export interface TalentResolvedBonus {
attr: Attrs;
mode: TalentValueMode;
/** 总值 = per_lv × 天赋等级 */
value: number;
}
/**
* 天赋加成作用目标的最小结构接口(TS 结构化类型,HeroAttrsComp 天然满足)。
* Why: 用结构化接口替代直接 import HeroAttrsComp,避免 config → hero → SingletonModuleComp → config 的循环依赖。
*/
export interface ITalentTarget {
ap: number;
hp: number;
hp_max: number;
defense: number;
/** 基础攻击快照(percent 模式的基数) */
base_ap: number;
/** 基础血量快照(percent 模式的基数) */
base_hp: number;
/** 防御基础值(percent 模式的基数,等于 hero.def) */
[key: string]: any;
}
/**
* 天赋配置表。
* uuid 段位:9001-9099 基础天赋(全局);9101-9199 专精天赋(按职业过滤)。
*/
export const TalentSet: Record<number, TalentConfig> = {
// ==================== 基础天赋(9001-9099,全局生效) ====================
9001: {
uuid: 9001, name: "攻击强化", info: "所有英雄攻击 +{value}%",
bonuses: [{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 3 }],
max_lv: 5,
},
9002: {
uuid: 9002, name: "生命强化", info: "所有英雄生命 +{value}%",
bonuses: [{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 5 }],
max_lv: 5,
},
9003: {
uuid: 9003, name: "防御强化", info: "所有英雄防御 +{value} 点",
bonuses: [{ attr: Attrs.defense, mode: TalentValueMode.Flat, per_lv: 8 }],
max_lv: 5,
},
9004: {
uuid: 9004, name: "暴击强化", info: "所有英雄暴击率 +{value}%",
bonuses: [{ attr: Attrs.critical, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9005: {
uuid: 9005, name: "击晕强化", info: "所有英雄击晕率 +{value}%",
bonuses: [{ attr: Attrs.stun_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9006: {
uuid: 9006, name: "冰冻强化", info: "所有英雄冰冻率 +{value}%",
bonuses: [{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9007: {
uuid: 9007, name: "击退强化", info: "所有英雄击退距离 +{value}",
bonuses: [{ attr: Attrs.knockback_distance, mode: TalentValueMode.Flat, per_lv: 10 }],
max_lv: 5,
},
9008: {
uuid: 9008, name: "风怒强化", info: "所有英雄风怒率 +{value}%",
bonuses: [{ attr: Attrs.wfuny, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
// ==================== 专精天赋(9101-9199,按职业过滤 + 前置要求) ====================
// 坦克专精:前置 防御强化 Lv3 → 坦克大量防御 + 生命
9101: {
uuid: 9101, name: "坦克专精", info: "坦克防御 +{value}%、生命 +{value2}%",
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}%",
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}%",
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}%",
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}%",
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}%",
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 }],
},
};
/** 天赋升级消耗:索引为当前等级(0 表示升到 1 级),返回所需天赋点数 */
export const TALENT_UPGRADE_COST: number[] = [1, 1, 2, 2, 3];
/**
* 查询升级所需天赋点数
* @param curLv 当前等级
* @returns 升到下一级所需点数;已满级返回 -1
*/
export function getTalentUpgradeCost(curLv: number): number {
if (curLv < 0 || curLv >= TALENT_UPGRADE_COST.length) return -1;
return TALENT_UPGRADE_COST[curLv];
}
/**
* 聚合指定职业英雄生效的全部天赋加成。
* Why: 基础天赋全局生效,专精天赋按 role 过滤且要求前置天赋达标;
* 战斗加载时(Hero.load / applyHeroLevel)调用本函数一次性取出本英雄的天赋加成区。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param role 英雄职业(专精过滤依据;无 role 时只应用基础天赋)
* @returns 已解析的加成列表(等级已乘算进 value)
*/
export function getTalentBonusesForRole(levels: Record<number, number>, role?: HRole): TalentResolvedBonus[] {
const result: TalentResolvedBonus[] = [];
if (!levels) return result;
for (const key in levels) {
const lv = levels[key];
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 });
}
}
return result;
}
/**
* 将天赋加成写入英雄属性模型(在 Hero.load / MissionCardComp.applyHeroLevel 的属性组装段调用)。
*
* 结算规则:
* - Percent 模式:以基础快照为基数(ap→base_ap,hp_max→base_hp,defense→def_base),按百分比放大后累加;
* - Flat 模式:直接按属性名动态累加(概率/抗性类为百分点,defense 为固定点数)。
*
* Why: percent 必须用基础快照而非当前 ap/hp_max 作基数——当前值已含驻场光环与成长增量,
* 若以当前值为基数会与光环乘区叠加成乘法,导致数值膨胀失控。
*
* @param model 英雄属性模型(满足 ITalentTarget 结构)
* @param levels 天赋等级表(smc.getTalentLevels())
* @param role 英雄职业(专精过滤;无 role 的怪物由调用方跳过)
*/
export function applyTalentBonuses(model: ITalentTarget, levels: Record<number, number>, role?: HRole): void {
const bonuses = getTalentBonusesForRole(levels, role);
if (bonuses.length === 0) return;
// percent 模式的 defense 基数:取调用方传入的当前 defense(Hero.load/applyHeroLevel 已在调用前重置为 hero.def)
const defBase = model.defense;
for (const b of bonuses) {
if (b.mode === TalentValueMode.Percent) {
switch (b.attr) {
case Attrs.ap:
model.ap += Math.floor(model.base_ap * b.value / 100);
break;
case Attrs.hp_max:
model.hp_max += Math.floor(model.base_hp * b.value / 100);
model.hp += Math.floor(model.base_hp * b.value / 100);
break;
case Attrs.defense:
model.defense += Math.floor(defBase * b.value / 100);
break;
default:
model[b.attr] = (typeof model[b.attr] === "number" ? model[b.attr] : 0) + b.value;
break;
}
} else {
// flat:概率/抗性类百分点累加,defense 固定点数累加
model[b.attr] = (typeof model[b.attr] === "number" ? model[b.attr] : 0) + b.value;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "0f2652aa-4d80-43ee-a5d4-35c3334520cc",
"files": [],
"subMetas": {},
"userData": {}
}