feat: 添加段位和闯关奖励领取系统
1. 新增REWARD_CLAIMED游戏事件用于通知奖励领取完成 2. 新建RewardSet配置文件,定义段位和闯关奖励数据结构与具体配置 3. 在SingletonModuleComp中新增奖励领取相关逻辑: - 新增奖励领取状态存档字段 - 实现可领取奖励查询、奖励领取、通关标记等方法 - 新增统一的奖励发放工具方法
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 },玩家点击"下一节"后推进) */
|
||||
|
||||
76
assets/script/game/common/config/RewardSet.ts
Normal file
76
assets/script/game/common/config/RewardSet.ts
Normal file
@@ -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, RewardItem[]> = {
|
||||
[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<number, RewardItem[]> = {
|
||||
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>): number {
|
||||
const excludeSet = exclude ?? new Set<number>();
|
||||
// 仅英雄池参与抽取(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)];
|
||||
}
|
||||
Reference in New Issue
Block a user