1. 重构天赋系统架构:将天赋加成与触发修正统一收敛到 HeroAttrsComp.applyTalent 2. 新增天赋触发修正字段:talent_t_num_reduce 与 talent_extra_trigger_chance 3. 重构天赋加点逻辑:统一 1 点/级,新增软前置校验 4. 优化天赋UI组件:改为编辑器静态配置驱动,重构层位展示逻辑 5. 修复技能触发判定:在 SkillTriggerHelper 中应用天赋触发修正 6. 调整天赋配置结构:改为 5 层递进解锁,新增 L4/L5 专属触发修正配置
767 lines
28 KiB
TypeScript
767 lines
28 KiB
TypeScript
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);
|