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

767 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { sys, resources, SpriteAtlas } from "cc";
import { VM } from "../../../../extensions/oops-plugin-framework/assets/libs/model-view/ViewModel";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Initialize } from "../initialize/Initialize";
import { GameMap } from "../map/GameMap";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { GameEvent } from "./config/GameEvent";
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, isTalentPrereqMet, isTalentTierUnlocked } from "./config/TalentSet";
import { ScoreRankList, ScoreTier, getRankByScore } from "./config/ScoreRankSet";
import { RANK_REWARDS, STAGE_REWARDS, RewardItem, pickRandomHeroUuid } from "./config/RewardSet";
/**
* 用远程数据覆盖本地数据(统一方法)
* @param remoteData 远程数据(云端或本地调试)
*/
export interface GameDate {
openid?: string,
score?: number,
mission?: number,
diamond?: number,
gold?: number,
/** 银币:局外持久货币,用于局内刷新英雄卡牌,仅通过日常任务等途径积攒 */
silver?: number,
task?: number,
noStop?: boolean,
showInfo?: boolean,
tip_done?: {
start: boolean,
hero: boolean,
equip: boolean,
skill: boolean,
shop: boolean,
},
switch_effect?: boolean, // 音效开关
switch_music?: boolean, // 音乐开关
timestamp?: number, // 用于比对本地与云端数据的最新状态
/** 英雄局外成长数据:key 为英雄 uuid,value 为成长记录 */
hero_growth?: Record<number, HeroGrowthRecord>,
/** 天赋等级表:key 为天赋 uuid,value 为当前等级(未记录视为 0 级) */
talents?: Record<number, number>,
/** 当前可用天赋点数(升级天赋消耗,经验/等级系统接入后由其产出) */
talent_points?: number,
/** 天赋当前等级(0 起步,区间 0~29,共 30 级) */
talent_lv?: number,
/** 天赋当前等级已累计的经验值 */
talent_exp?: number,
/** 关卡进度存档:当前章节-关-节 */
stage_progress?: {
chapter: number,
stage: number,
section: number,
},
/** 奖励领取状态:段位/闯关首达奖励的已领取标记(玩家点击领取) */
reward_claimed?: {
/** 已领取的段位(ScoreTier 值) */
rank: string[],
/** 已领取的大关号 */
stage: number[],
},
/** 已通关大关的显式标记(覆盖存档停留场景,如第 10 关通关后进度停留 1-10-10) */
passed_stages?: number[],
}
export interface CloudData {
openid: string;
data: GameDate;
}
/** 英雄局外成长记录 */
export interface HeroGrowthRecord {
/** 英雄模板 uuid(对应 heroSet.HeroInfo 的 key) */
uuid: number;
/** 当前局外等级(从 1 开始) */
lv: number;
/** 当前拥有的英雄碎片数量 */
fragments: number;
}
/**
* 死亡英雄快照:英雄真实死亡时记录,供后续复活机制(重新召唤)消费。
* 仅记录静态/成长数据,不含战斗内临时状态(buff、CD 等)。
*/
export interface DeadHeroRecord {
/** 英雄模板 uuid(决定重新召唤哪个英雄) */
uuid: number;
/** 死亡时的英雄等级(复活后继承) */
lv: number;
/** 卡牌等级(驱动 bg_node 颜色) */
rarity: number;
/** 死亡时占用的站位索引(复活时优先归位,-1 表示未分配) */
posIndex: number;
/** 死亡时的回合数(用于统计/按回合清理) */
wave: number;
}
/** 游戏模块 */
@ecs.register('SingletonModule')
export class SingletonModuleComp extends ecs.Comp {
private debugMode: boolean = false;
/** 游戏初始化模块 */
initialize: Initialize = null!;
/** 游戏地图 */
map: GameMap = null!;
/** 全局缓存的通用图集 */
uiconsAtlas: SpriteAtlas | null = null;
/** 全局缓存的技能图标图集(gui/ui7,SinfoComp/TalentItemComp/SSkillBoxComp 专用) */
skillIconsAtlas: SpriteAtlas | null = null;
mission: any = {
status: 0, //0:未开始 1:进行中 2:胜利 3:失败
play: false,
pause: false,
in_select: false,
in_fight: false,
stop_mon_action: false,
heroGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
/** 本局真实死亡的英雄快照列表,供后续复活机制消费;整局结束(MissionEnd)时清空 */
dead_heroes: [] as DeadHeroRecord[],
};
data: any = {
openid: '',
score: 0,
mission: 1,
diamond: 100, //商店购买 及 双倍奖励资源
gold: 1000,
/** 银币:刷新英雄卡牌专用,每局不重置,仅日常积攒 */
silver: 0,
task: 0,
noStop: false,
showInfo: true,
/** 新手指引完成标记:false=需要提醒,true=已提醒过(仅提醒 1 次) */
tip_done: {
start: false, // 主页开始按钮
hero: false, // 召唤英雄面板按钮
equip: false, // 购买装备面板按钮
skill: false, // 购买技能卷轴面板按钮
shop: false, // 购买药品面板按钮
},
/** 英雄局外成长数据:记录每个英雄的局外等级与碎片数量 */
hero_growth: {} as Record<number, HeroGrowthRecord>,
/** 天赋等级表:key 为天赋 uuid,value 为当前等级 */
talents: {} as Record<number, number>,
/** 可用天赋点数 */
talent_points: 0,
/** 天赋等级(0 起步) */
talent_lv: 0,
/** 当前等级已累计经验 */
talent_exp: 0,
/** 关卡进度存档:默认从 1-1-1 开始 */
stage_progress: {
chapter: 1,
stage: 1,
section: 1,
},
/** 奖励领取状态(玩家点击领取,跨局保留) */
reward_claimed: {
rank: [],
stage: [],
},
/** 已通关大关的显式标记 */
passed_stages: [],
}
vmdata: any = {
game_over: false,
game_pause: false,
mission_data: {
mon_num: 0,//怪物数量
pending_mon_num: 0,//待刷出怪物数量(MissionMonComp 每帧写入,供回合结束检测与 HUD 进度)
wave_early_skip: 0,//本回合清场加速累计节省的秒数(MissionComp 用于还原 DDA 判定口径)
hero_num: 0,//英雄数量
wave_time_num: 0,//回合时间
in_fight: false,
fight_time: 0,//战斗时间
level: 1,//关卡等级
max_mission: 4,//最大关卡
coin: 0,
time: 15 * 60,//游戏时间
},
scores: {
score: 0, // 基础得分
// 战斗统计
crt_count: 0, // 暴击次数
// 风怒机制已停用: 风怒次数统计已移除
// wf_count: 0, // 风怒次数
dod_count: 0, // 闪避次数
back_count: 0, // 击退次数
stun_count: 0, // 击晕次数
freeze_count: 0, // 冰冻次数
// 伤害统计
total_dmg: 0, // 总伤害
atk_count: 0, // 攻击次数
avg_dmg: 0, // 平均伤害
thorns_dmg: 0, // 反伤伤害
crit_dmg_total: 0, // 暴击伤害总额
// 生存统计
heal_total: 0, // 治疗总量
lifesteal_total: 0, // 吸血总量
shield_block_count: 0,
dead_trigger_count: 0,
// 资源统计
exp_total: 0, // 经验总数
gold_total: 0, // 金币总数
gold_earned: 0,
gold_spent: 0,
refresh_count: 0,
refresh_hit_count: 0,
// 击杀统计
melee_kill_count: 0, // 近战怪击杀数量
remote_kill_count: 0, // 远程怪击杀数量
elite_kill_count: 0, // 精英怪击杀数量
boss_kill_count: 0, // Boss击杀数
// 战绩统计
wave_win_count: 0,
wave_remain_monsters: 0,
wave_all_alive_count: 0,
passed_wave_20: false,
highest_dmg: 0,
achieved_highlights: [],
} as GameScoreStats,
gold: 0, // 金币数据(MVVM绑定字段,镜像 data.gold)
diamond: 0, // 钻石数据(MVVM绑定字段,镜像 data.diamond)
silver: 0, // 银币数据(MVVM绑定字段,镜像 data.silver,刷新卡牌消耗)
switch_effect: true, // 音效开关(MVVM绑定字段)
switch_music: true, // 音乐开关(MVVM绑定字段)
/** 关卡进度(MVVM绑定字段,镜像 data.stage_progress,供 HUD 实时显示) */
stage_progress: {
chapter: 1,
stage: 1,
section: 1,
},
};
vmAdd() {
VM.add(this.vmdata, "data");
// mLogger.log(this.debugMode, 'SMC', "[MissionComp]局内数据初始化",smc.vmdata.mission_data)
}
reset() {
for (var key in this.vmdata) {
delete this.vmdata[key];
}
}
/**
* 重置单局评分数据
* 在每次新战斗开始时调用,确保上一局的得分不会带入新一局
*/
resetScores() {
this.vmdata.scores = {
score: 0,
crt_count: 0,
// 风怒机制已停用: 风怒次数统计已移除
// wf_count: 0,
dod_count: 0,
back_count: 0,
stun_count: 0,
freeze_count: 0,
total_dmg: 0,
atk_count: 0,
avg_dmg: 0,
thorns_dmg: 0,
crit_dmg_total: 0,
heal_total: 0,
lifesteal_total: 0,
shield_block_count: 0,
dead_trigger_count: 0,
exp_total: 0,
gold_total: 0,
gold_earned: 0,
gold_spent: 0,
refresh_count: 0,
refresh_hit_count: 0,
melee_kill_count: 0,
remote_kill_count: 0,
elite_kill_count: 0,
boss_kill_count: 0,
wave_win_count: 0,
wave_remain_monsters: 0,
wave_all_alive_count: 0,
passed_wave_20: false,
highest_dmg: 0,
achieved_highlights: [],
} as GameScoreStats;
}
// ==================== 数据管理方法 ====================
/**
* 判断是否为微信客户端
*/
isWxClient(): boolean {
return gameDataSync.isWxClient();
}
updateCloudData() {
return gameDataSync.updateCloudData();
}
getCloudData() {
gameDataSync.syncFromCloud();
}
public async overrideLocalDataWithRemote(cloudData: any) {
try {
// 直接覆盖基础游戏数据
if (cloudData.openid) {
this.data.openid = cloudData.openid;
}
// 直接覆盖出战英雄配置
if (cloudData.data) {
Object.assign(this.data, cloudData.data);
}
// 触发UI更新(同步镜像层 + 事件)
this.syncCurrencyToView();
this.syncStageProgressToView();
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE);
oops.message.dispatchEvent(GameEvent.STAGE_PROGRESS_UPDATE);
} catch (error) {
mLogger.error(this.debugMode, 'SMC', `[SMC]: 数据覆盖失败:`, error);
}
}
/**
* 将 data 层的货币镜像到 vmdata 视图层(MVVM 绑定字段)。
* Why: data 为云端持久权威源,vmdata.gold/diamond 仅供界面绑定展示;
* 登录覆盖、奖励发放后都必须调用本方法保持镜像一致。
*/
public syncCurrencyToView(): void {
this.vmdata.gold = this.data.gold ?? 0;
this.vmdata.diamond = this.data.diamond ?? 0;
this.vmdata.silver = this.data.silver ?? 0;
}
/**
* 将 data 层的关卡进度镜像到 vmdata 视图层(MVVM 绑定字段)。
* Why: data.stage_progress 为云端持久权威源,vmdata.stage_progress 仅供 HUD 绑定展示;
* 登录覆盖、关卡推进后都必须调用本方法保持镜像一致。
*/
public syncStageProgressToView(): void {
const p = this.data.stage_progress;
// 就地更新字段而非整体替换对象:oops VM(JsonOb)只通知被赋值属性的精确路径,
// 整体替换仅派发 VC:data.stage_progress,不会触发 HUD 绑定的
// data.stage_progress.section 等嵌套末端路径刷新,导致进度显示停留在老节
const view = this.vmdata.stage_progress;
view.chapter = p?.chapter ?? 1;
view.stage = p?.stage ?? 1;
view.section = p?.section ?? 1;
}
/**
* 更新关卡进度(权威源 data + 视图镜像 vmdata),并派发事件刷新 UI。
* Why: 替代直接赋值 smc.data.stage_progress,保证视图层实时同步。
* @param progress 新的关卡进度(章节/大关/小节均从 1 开始)
* @param is_sync 是否标记脏并触发云端/本地同步
*/
public updateStageProgress(progress: { chapter: number; stage: number; section: number }, is_sync: boolean = true): void {
this.data.stage_progress = { ...progress };
this.syncStageProgressToView();
if (is_sync) {
gameDataSync.markDataDirty();
}
oops.message.dispatchEvent(GameEvent.STAGE_PROGRESS_UPDATE);
}
// ==================== 奖励领取(段位 / 闯关) ====================
/** 查询当前已达、未领取的段位列表(升序;跨档时可连领多份) */
public getClaimableRankRewards(): ScoreTier[] {
const current = getRankByScore(this.data.score ?? 0);
const curIdx = ScoreRankList.findIndex(r => r.tier === current.tier);
const claimed = this.data.reward_claimed?.rank ?? [];
const result: ScoreTier[] = [];
// 段位只涨不跌:当前段位即历史最高,低位段位全部视为已达
for (let i = 0; i <= curIdx; i++) {
const tier = ScoreRankList[i].tier;
if (!claimed.includes(tier)) {
result.push(tier);
}
}
return result;
}
/** 查询已通关、未领取的大关号列表(升序) */
public getClaimableStageRewards(): number[] {
const claimed = this.data.reward_claimed?.stage ?? [];
const result: number[] = [];
for (const stage of Object.keys(STAGE_REWARDS).map(Number)) {
if (!claimed.includes(stage) && this.isStagePassed(stage)) {
result.push(stage);
}
}
return result.sort((a, b) => a - b);
}
/** 是否存在待领取奖励(UI 红点总开关) */
public hasUnclaimedRewards(): boolean {
return this.getClaimableRankRewards().length > 0
|| this.getClaimableStageRewards().length > 0;
}
/**
* 领取段位奖励(玩家点击触发)
* @param tier 目标段位
* @returns 实际发放的奖励列表;段位未达/已领取/配置缺失时返回 null
*/
public claimRankReward(tier: ScoreTier): RewardItem[] | null {
if (!this.getClaimableRankRewards().includes(tier)) return null;
const rewards = RANK_REWARDS[tier];
if (!rewards || rewards.length === 0) return null;
this.grantRewardItems(rewards);
if (!this.data.reward_claimed) this.data.reward_claimed = { rank: [], stage: [] };
this.data.reward_claimed.rank.push(tier);
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.REWARD_CLAIMED);
return rewards;
}
/**
* 领取闯关奖励(玩家点击触发)
* @param stage 目标大关号(1~10)
* @returns 实际发放的奖励列表;未通关/已领取/配置缺失时返回 null
*/
public claimStageReward(stage: number): RewardItem[] | null {
if (!this.getClaimableStageRewards().includes(stage)) return null;
const rewards = STAGE_REWARDS[stage];
if (!rewards || rewards.length === 0) return null;
this.grantRewardItems(rewards);
if (!this.data.reward_claimed) this.data.reward_claimed = { rank: [], stage: [] };
this.data.reward_claimed.stage.push(stage);
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.REWARD_CLAIMED);
return rewards;
}
/**
* 标记大关已通关(通关点调用,覆盖存档停留场景)
* Why: 第 10 关通关后存档停留 1-10-10,无法从 stage_progress 推导已通状态,
* 需在通关时显式写入。
* @param stage 大关号
*/
public markStagePassed(stage: number): void {
if (!this.data.passed_stages) this.data.passed_stages = [];
if (!this.data.passed_stages.includes(stage)) {
this.data.passed_stages.push(stage);
gameDataSync.markDataDirty();
}
}
/** 判定大关是否已通关:显式标记优先,存档推进兜底(已进入后续关即已通关) */
private isStagePassed(stage: number): boolean {
if ((this.data.passed_stages ?? []).includes(stage)) return true;
return (this.data.stage_progress?.stage ?? 1) > stage;
}
/**
* 统一发放奖励条目(复用现有货币/成长入口,自动落 data 持久层并标脏)
* @param items 奖励条目列表
*/
private grantRewardItems(items: RewardItem[]): void {
for (const item of items) {
switch (item.type) {
case 'gold':
if (item.value > 0) this.updateGold(item.value);
break;
case 'silver':
if (item.value > 0) this.updateSilver(item.value);
break;
case 'diamond':
if (item.value > 0) this.updateDiamond(item.value);
break;
case 'talent_exp':
if (item.value > 0) this.addTalentExp(item.value);
break;
case 'fragment': {
const uuid = item.heroUuid ?? pickRandomHeroUuid();
if (item.value > 0 && uuid > 0) {
this.addHeroFragments(uuid, item.value);
}
break;
}
}
}
}
getGameDate() {
let data: GameDate = {
...this.data,
timestamp: Date.now() // 每次获取当前数据结构时都附带最新的时间戳
};
return data;
}
/**
* 变更金币(正数获得 / 负数消耗),以 data 为权威源并镜像到 vmdata。
* @param gold 变动量
* @param is_sync 是否标记脏并触发云端/本地同步
*/
updateGold(gold: number, is_sync: boolean = true) {
this.data.gold = Math.max(0, (this.data.gold ?? 0) + gold);
this.vmdata.gold = this.data.gold;
if (is_sync) {
gameDataSync.markDataDirty();
}
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE)
return true
}
/**
* 变更钻石(正数获得 / 负数消耗),以 data 为权威源并镜像到 vmdata。
* @param diamond 变动量
* @param is_sync 是否标记脏并触发云端/本地同步
*/
updateDiamond(diamond: number, is_sync: boolean = true) {
this.data.diamond = Math.max(0, (this.data.diamond ?? 0) + diamond);
this.vmdata.diamond = this.data.diamond;
if (is_sync) {
gameDataSync.markDataDirty();
}
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE)
return true
}
/**
* 变更银币(正数获得 / 负数消耗),以 data 为权威源并镜像到 vmdata。
* Why: 银币为局外持久货币,仅用于刷新英雄卡牌,日常任务等途径积攒。
* @param silver 变动量
* @param is_sync 是否标记脏并触发云端/本地同步
*/
updateSilver(silver: number, is_sync: boolean = true) {
this.data.silver = Math.max(0, (this.data.silver ?? 0) + silver);
this.vmdata.silver = this.data.silver;
if (is_sync) {
gameDataSync.markDataDirty();
}
oops.message.dispatchEvent(GameEvent.SILVER_UPDATE)
return true
}
updateSwitchEffect(isOpen: boolean, is_sync: boolean = true) {
this.vmdata.switch_effect = isOpen;
if (oops.audio) oops.audio.switchEffect = isOpen;
if (is_sync) {
gameDataSync.markDataDirty();
}
}
updateSwitchMusic(isOpen: boolean, is_sync: boolean = true) {
this.vmdata.switch_music = isOpen;
if (oops.audio) oops.audio.switchMusic = isOpen;
if (is_sync) {
gameDataSync.markDataDirty();
}
}
// ==================== 英雄局外成长 ====================
/**
* 获取英雄局外成长记录;若不存在则初始化并返回
* @param uuid 英雄模板 uuid
*/
getHeroGrowth(uuid: number): HeroGrowthRecord {
if (!this.data.hero_growth) this.data.hero_growth = {};
if (!this.data.hero_growth[uuid]) {
this.data.hero_growth[uuid] = { uuid, lv: 1, fragments: 0 };
}
return this.data.hero_growth[uuid];
}
/**
* 获取英雄局外等级(未初始化时返回 1)
* @param uuid 英雄模板 uuid
*/
getHeroOuterLv(uuid: number): number {
return this.getHeroGrowth(uuid).lv;
}
/**
* 增加英雄碎片
* @param uuid 英雄模板 uuid
* @param count 增加的碎片数量(正数)
*/
addHeroFragments(uuid: number, count: number): void {
const growth = this.getHeroGrowth(uuid);
growth.fragments = Math.max(0, growth.fragments + count);
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.HERO_GROWTH_UPDATE);
}
/**
* 消耗碎片提升英雄局外等级(内部查表决定所需碎片数)
* @param uuid 英雄模板 uuid
* @returns 是否升级成功(已到满级或碎片不足时返回 false)
*/
upgradeHero(uuid: number): boolean {
const growth = this.getHeroGrowth(uuid);
const needFragments = HERO_UPGRADE_NEED_FRAGMENTS[growth.lv + 1];
// 配置表中查不到下一级 = 已到满级
if (needFragments === undefined) return false;
if (growth.fragments < needFragments) return false;
growth.fragments -= needFragments;
growth.lv += 1;
gameDataSync.markDataDirty();
oops.message.dispatchEvent(GameEvent.HERO_GROWTH_UPDATE);
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);
}
/**
* 获取天赋等级(0 起步,区间 0~29,共 30 级)
*/
getTalentLevel(): number {
return this.data.talent_lv ?? 0;
}
/**
* 获取当前等级已累计经验
*/
getTalentExp(): number {
return this.data.talent_exp ?? 0;
}
/**
* 增加天赋经验并自动升级(可连升多级;每升 1 级产出 1 点天赋点)
* Why: 升级阈值查 TALENT_LEVEL_EXP 配置表,满级(30 级)后不再累计经验。
* @param count 增加的经验值(正数)
*/
addTalentExp(count: number): void {
if (count <= 0) return;
let lv = this.getTalentLevel();
if (lv >= TALENT_LEVEL_MAX) return;
let exp = this.getTalentExp() + count;
// 支持一次加经验连升多级
while (lv < TALENT_LEVEL_MAX) {
const need = getTalentLevelExp(lv);
if (need < 0 || exp < need) break;
exp -= need;
lv += 1;
this.addTalentPoints(1);
}
// 满级后清空经验,进度条显示为满
if (lv >= TALENT_LEVEL_MAX) exp = 0;
this.data.talent_lv = lv;
this.data.talent_exp = exp;
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;
// 层位解锁:第 N 层天赋要求第 N-1 层累计投入点数达标(第 1 层恒解锁)
if (!isTalentTierUnlocked(this.data.talents, cfg.tier)) return false;
// 软前置:前置天赋等级需达标(仅限制加点)
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);
return true;
}
/**
* 重置所有天赋等级并退还全部已消耗天赋点(看广告入口)
* Why: 统一 1 点/级后退还点数 = 等级总和,与升级消耗严格对账,避免点数膨胀/缩水。
*/
resetTalents(): void {
if (!this.data.talents) this.data.talents = {};
let refund = 0;
for (const key in this.data.talents) {
refund += this.data.talents[key];
}
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 ?? {};
}
/**
* 在游戏载入早期调用,预加载常用图集
*/
preloadCommonAssets() {
resources.load("gui/ui6", SpriteAtlas, (err, atlas) => {
if (!err && atlas) {
// 增加引用计数,防止图集被引擎自动垃圾回收(GC)导致底层 spriteFrames 为 null
atlas.addRef();
this.uiconsAtlas = atlas;
} else {
mLogger.error(this.debugMode, 'SMC', "预加载 gui/ui6 图集失败:", err);
}
});
resources.load("gui/ui7", SpriteAtlas, (err, atlas) => {
if (!err && atlas) {
// 增加引用计数,防止图集被引擎自动垃圾回收(GC)导致底层 spriteFrames 为 null
atlas.addRef();
this.skillIconsAtlas = atlas;
} else {
mLogger.error(this.debugMode, 'SMC', "预加载 gui/ui7 技能图标图集失败:", err);
}
});
}
}
export var smc: SingletonModuleComp = ecs.getSingleton(SingletonModuleComp);