From 88ac1337a4db8004dfe8a25a8ddf00307666a8d4 Mon Sep 17 00:00:00 2001 From: pan Date: Fri, 11 Sep 2026 17:08:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=AE=B5=E4=BD=8D?= =?UTF-8?q?=E5=92=8C=E9=97=AF=E5=85=B3=E5=A5=96=E5=8A=B1=E9=A2=86=E5=8F=96?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增REWARD_CLAIMED游戏事件用于通知奖励领取完成 2. 新建RewardSet配置文件,定义段位和闯关奖励数据结构与具体配置 3. 在SingletonModuleComp中新增奖励领取相关逻辑: - 新增奖励领取状态存档字段 - 实现可领取奖励查询、奖励领取、通关标记等方法 - 新增统一的奖励发放工具方法 --- .../script/game/common/SingletonModuleComp.ts | 141 ++++++++++++++++++ assets/script/game/common/config/GameEvent.ts | 2 + assets/script/game/common/config/RewardSet.ts | 76 ++++++++++ 3 files changed, 219 insertions(+) create mode 100644 assets/script/game/common/config/RewardSet.ts diff --git a/assets/script/game/common/SingletonModuleComp.ts b/assets/script/game/common/SingletonModuleComp.ts index 9168ebb9..e780524b 100644 --- a/assets/script/game/common/SingletonModuleComp.ts +++ b/assets/script/game/common/SingletonModuleComp.ts @@ -10,6 +10,8 @@ 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 { ScoreRankList, ScoreTier, getRankByScore } from "./config/ScoreRankSet"; +import { RANK_REWARDS, STAGE_REWARDS, RewardItem, pickRandomHeroUuid } from "./config/RewardSet"; /** * 用远程数据覆盖本地数据(统一方法) @@ -52,6 +54,15 @@ export interface GameDate { stage: number, section: number, }, + /** 奖励领取状态:段位/闯关首达奖励的已领取标记(玩家点击领取) */ + reward_claimed?: { + /** 已领取的段位(ScoreTier 值) */ + rank: string[], + /** 已领取的大关号 */ + stage: number[], + }, + /** 已通关大关的显式标记(覆盖存档停留场景,如第 10 关通关后进度停留 1-10-10) */ + passed_stages?: number[], } export interface CloudData { openid: string; @@ -157,6 +168,13 @@ export class SingletonModuleComp extends ecs.Comp { stage: 1, section: 1, }, + /** 奖励领取状态(玩家点击领取,跨局保留) */ + reward_claimed: { + rank: [], + stage: [], + }, + /** 已通关大关的显式标记 */ + passed_stages: [], } vmdata: any = { @@ -370,6 +388,129 @@ export class SingletonModuleComp extends ecs.Comp { } 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, diff --git a/assets/script/game/common/config/GameEvent.ts b/assets/script/game/common/config/GameEvent.ts index 2349b2f9..c5ca5414 100644 --- a/assets/script/game/common/config/GameEvent.ts +++ b/assets/script/game/common/config/GameEvent.ts @@ -102,6 +102,8 @@ export enum GameEvent { TALENT_UPDATE = "TALENT_UPDATE", /** 关卡进度变更(payload 为空,UI 重新读取 smc.vmdata.stage_progress) */ STAGE_PROGRESS_UPDATE = "STAGE_PROGRESS_UPDATE", + /** 奖励领取完成(payload 为空,UI 刷新待领取列表/红点) */ + REWARD_CLAIMED = "REWARD_CLAIMED", /** 小节开始(payload: { chapter, stage, section },MissionMonComp 据此生成怪物) */ SectionStart = "SectionStart", /** 小节结束(payload: { chapter, stage, section },玩家点击"下一节"后推进) */ diff --git a/assets/script/game/common/config/RewardSet.ts b/assets/script/game/common/config/RewardSet.ts new file mode 100644 index 00000000..a17d5052 --- /dev/null +++ b/assets/script/game/common/config/RewardSet.ts @@ -0,0 +1,76 @@ +/** + * @file RewardSet.ts + * @description 段位奖励 + 闯关奖励配置(纯数据,无逻辑) + * + * 定位差异化: + * - 段位奖励(RANK_REWARDS):稀有向——钻石 + 英雄碎片,荣誉回报 + * - 闯关奖励(STAGE_REWARDS):养成向——金币 + 天赋经验,进程回报(精英关/Boss 关加成) + * + * 领取模式:玩家点击领取(SMC.claimRankReward / claimStageReward), + * 状态持久化于 smc.data.reward_claimed,不领不丢、跨局保留。 + */ + +import { HeroInfo } from "./heroSet"; +import { FacSet } from "./GameSet"; +import { ScoreTier } from "./ScoreRankSet"; + +/** 奖励资源类型 */ +export type RewardType = 'gold' | 'silver' | 'diamond' | 'talent_exp' | 'fragment'; + +/** 单条奖励 */ +export interface RewardItem { + /** 资源类型 */ + type: RewardType; + /** 数量 */ + value: number; + /** 英雄碎片专用:指定英雄 uuid,缺省随机抽取 */ + heroUuid?: number; +} + +/** + * 段位首达奖励(每档一份,跨档可补领) + * 阈值与段位定义见 ScoreRankSet.ScoreRankList + */ +export const RANK_REWARDS: Record = { + [ScoreTier.BRONZE]: [{ type: 'diamond', value: 10 }], + [ScoreTier.SILVER]: [{ type: 'diamond', value: 20 }, { type: 'fragment', value: 3 }], + [ScoreTier.GOLD]: [{ type: 'diamond', value: 30 }, { type: 'fragment', value: 5 }], + [ScoreTier.PLATINUM]: [{ type: 'diamond', value: 50 }, { type: 'fragment', value: 8 }], + [ScoreTier.DIAMOND]: [{ type: 'diamond', value: 80 }, { type: 'fragment', value: 10 }], + [ScoreTier.MASTER]: [{ type: 'diamond', value: 120 }, { type: 'fragment', value: 15 }], + [ScoreTier.LEGEND]: [{ type: 'diamond', value: 200 }, { type: 'fragment', value: 20 }], +}; + +/** + * 闯关奖励(每大关首通一份,共 10 份) + * 养成向:金币 + 天赋经验;第 5 关(精英)/第 10 关(Boss)额外附赠银币 + */ +export const STAGE_REWARDS: Record = { + 1: [{ type: 'gold', value: 200 }, { type: 'talent_exp', value: 20 }], + 2: [{ type: 'gold', value: 300 }, { type: 'talent_exp', value: 30 }], + 3: [{ type: 'gold', value: 400 }, { type: 'talent_exp', value: 40 }], + 4: [{ type: 'gold', value: 500 }, { type: 'talent_exp', value: 50 }], + 5: [{ type: 'gold', value: 700 }, { type: 'talent_exp', value: 70 }, { type: 'silver', value: 10 }], + 6: [{ type: 'gold', value: 600 }, { type: 'talent_exp', value: 60 }], + 7: [{ type: 'gold', value: 700 }, { type: 'talent_exp', value: 70 }], + 8: [{ type: 'gold', value: 800 }, { type: 'talent_exp', value: 80 }], + 9: [{ type: 'gold', value: 900 }, { type: 'talent_exp', value: 90 }], + 10: [{ type: 'gold', value: 1500 }, { type: 'talent_exp', value: 150 }, { type: 'silver', value: 20 }], +}; + +/** + * 从全英雄池随机抽取 1 个英雄 uuid(排除指定集合,池空时兜底返回 0) + * @param exclude 需排除的 uuid 集合(可空) + * @returns 随机英雄 uuid;英雄池为空返回 0 + */ +export function pickRandomHeroUuid(exclude?: Set): number { + const excludeSet = exclude ?? new Set(); + // 仅英雄池参与抽取(HeroInfo 同时含怪物条目,碎片属英雄局外成长) + const all = Object.keys(HeroInfo) + .map(k => Number(k)) + .filter(u => u > 0 && HeroInfo[u]?.fac === FacSet.HERO); + const candidates = all.filter(u => !excludeSet.has(u)); + const pool = candidates.length > 0 ? candidates : all; + if (pool.length === 0) return 0; + return pool[Math.floor(Math.random() * pool.length)]; +}