refactor(mission): 重构关卡结算与刷怪统计逻辑
1. 新增整局跨回合刷怪统计与重置能力,替换原小节统计 2. 修改奖励配置为整局5回合累计结算逻辑 3. 调整失败奖励判定为整局维度统计 4. 重构胜利/失败奖励计算逻辑,适配多回合关卡
This commit is contained in:
@@ -219,38 +219,40 @@ export function getLvColor(lv: number): Color {
|
|||||||
export const RARITY_BG_COLORS: string[] = ["green", "blue", "purple", "yellow", "red"];
|
export const RARITY_BG_COLORS: string[] = ["green", "blue", "purple", "yellow", "red"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 战斗结算奖励配置(每节结束后发放:英雄碎片 + 金币 + 天赋经验)。
|
* 战斗结算奖励配置(一关 5 回合累计,整关结算时一次性发放:英雄碎片 + 金币 + 天赋经验)。
|
||||||
* 胜利奖励随关卡进度(怪物等级 = 大关号)线性增长,整体乘以 victory_multiplier。
|
* 通关奖励 = 每回合奖励 × 胜利回合数(wave_win_count),全员存活回合再 × per_wave 加成。
|
||||||
|
* Why: 一关一局 5 回合后,通关不再是单场,改按"5 回合累计"发放,
|
||||||
|
* 每回合基础奖励随关卡进度(怪物等级 = 大关号)增长。
|
||||||
*/
|
*/
|
||||||
export const VICTORY_REWARD_SET = {
|
export const VICTORY_REWARD_SET = {
|
||||||
/** 金币:base + monLv × per_lv */
|
/** 金币:每回合 base + monLv × per_lv,× 胜利回合数累计 */
|
||||||
gold: { base: 100, per_lv: 50 },
|
gold: { base: 100, per_lv: 50 },
|
||||||
/** 天赋经验:base + monLv × per_lv */
|
/** 天赋经验:每回合 base + monLv × per_lv,× 胜利回合数累计 */
|
||||||
talent_exp: { base: 20, per_lv: 10 },
|
talent_exp: { base: 20, per_lv: 10 },
|
||||||
/** 每个出场英雄保底碎片数 */
|
/** 每个出场英雄保底碎片数(× 胜利回合数累计) */
|
||||||
played_fragment: 1,
|
played_fragment: 1,
|
||||||
/** 随机英雄碎片数量(发放给随机抽中的 1 个英雄) */
|
/** 随机英雄碎片数量(发放给随机抽中的 1 个英雄,× 胜利回合数累计) */
|
||||||
random_fragment: 2,
|
random_fragment: 2,
|
||||||
/** 银币:固定发放,不分输赢 */
|
/** 银币:固定发放,不分输赢(通关时 × 胜利回合数累计) */
|
||||||
silver: 3,
|
silver: 3,
|
||||||
/** 钻石:按输赢在 [min, max] 闭区间内随机(不参与 victory_multiplier) */
|
/** 钻石:按输赢在 [min, max] 闭区间内随机(通关时 × 胜利回合数累计,不参与 per_wave 加成) */
|
||||||
diamond: {
|
diamond: {
|
||||||
victory: { min: 5, max: 10 },
|
victory: { min: 5, max: 10 },
|
||||||
defeat: { min: 1, max: 3 },
|
defeat: { min: 1, max: 3 },
|
||||||
},
|
},
|
||||||
/** 胜利加成系数(失败 1.0) */
|
/** 全员存活回合加成系数:每个全员存活回合额外 +20%(乘法叠乘于累计基础之上) */
|
||||||
victory_multiplier: 1.2,
|
per_wave_bonus: 0.2,
|
||||||
/**
|
/**
|
||||||
* 失败奖励限制(不影响胜利):
|
* 失败奖励限制(不影响胜利):
|
||||||
* 坚持 < defeat_time_threshold 秒或本节一只怪未杀 → 无任何奖励;
|
* 坚持 < defeat_time_threshold 秒或本回合一只怪未杀 → 无任何奖励;
|
||||||
* 达标后 = 坚持奖励(固定)+ 击杀比例奖励(基数 × 本节击杀比例)
|
* 达标后 = 坚持奖励(固定)+ 击杀比例奖励(基数 × 本回合击杀比例)
|
||||||
*/
|
*/
|
||||||
defeat: {
|
defeat: {
|
||||||
/** 坚持时间门槛(秒) */
|
/** 坚持时间门槛(秒) */
|
||||||
time_threshold: 15,
|
time_threshold: 15,
|
||||||
/** 超门槛固定奖励("超 15 秒奖励") */
|
/** 超门槛固定奖励("超 15 秒奖励") */
|
||||||
survive: { gold: 50, talent_exp: 10 },
|
survive: { gold: 50, talent_exp: 10 },
|
||||||
/** 击杀比例奖励基数(本节怪物全灭时全额发放) */
|
/** 击杀比例奖励基数(本回合怪物全灭时全额发放) */
|
||||||
kill: { gold: 150, talent_exp: 30 },
|
kill: { gold: 150, talent_exp: 30 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -629,11 +629,8 @@ export class MissionComp extends CCComp {
|
|||||||
settle_title: this._settleTitle,
|
settle_title: this._settleTitle,
|
||||||
// 关卡难度度量(= 大关号,驱动胜利奖励随进度增长)
|
// 关卡难度度量(= 大关号,驱动胜利奖励随进度增长)
|
||||||
mon_lv: getMonsterLv(this.currentStage),
|
mon_lv: getMonsterLv(this.currentStage),
|
||||||
// 失败奖励判定数据:本关坚持时间 + 击杀统计(kills=已刷出-存活)
|
// 失败奖励判定数据:整局坚持时间 + 整局击杀统计(kills=跨回合累计击杀,total=整局已刷出)
|
||||||
defeat_stats: {
|
defeat_stats: this.queryMissionKillStats(),
|
||||||
clear_time: this.clearTime,
|
|
||||||
...this.querySectionKillStats(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// play 已为 false(fight_end 强制结束)→ 直接清理
|
// play 已为 false(fight_end 强制结束)→ 直接清理
|
||||||
@@ -838,6 +835,8 @@ export class MissionComp extends CCComp {
|
|||||||
|
|
||||||
// 重置所有的战局得分数据,防止上一局的数据污染
|
// 重置所有的战局得分数据,防止上一局的数据污染
|
||||||
smc.resetScores();
|
smc.resetScores();
|
||||||
|
// 重置整局刷怪统计(跨回合累计的已刷出总数),回合间不重置
|
||||||
|
ecs.query(ecs.allOf(MissionMonCompComp)).forEach(e => e.get(MissionMonCompComp).resetMissionStats());
|
||||||
|
|
||||||
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(FightSet.INIT_COIN));
|
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(FightSet.INIT_COIN));
|
||||||
// 银币为局外持久货币,每局不重置,仅通过日常积攒,无需初始化
|
// 银币为局外持久货币,每局不重置,仅通过日常积攒,无需初始化
|
||||||
@@ -1023,14 +1022,24 @@ export class MissionComp extends CCComp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询本节击杀统计(转交结算弹窗做失败奖励判定)
|
* 查询整局(整关 5 回合)击杀统计与坚持时间,供结算弹窗做失败奖励判定。
|
||||||
* @returns { kills, total },查询不到 MissionMonComp 时返回 0/0
|
* Why: 一关一局后,失败奖励按整局累计口径计算,不再只看当前回合——
|
||||||
|
* clear_time 用整关战斗总时长 fight_time(跨回合累计,整关重置一次);
|
||||||
|
* kills 用 scores 跨回合累计的分类击杀数之和(melee/remote/elite/boss);
|
||||||
|
* total 用整局已刷出怪物总数(MissionMonComp 跨回合累计)。
|
||||||
*/
|
*/
|
||||||
private querySectionKillStats(): { kills: number; total: number } {
|
private queryMissionKillStats(): { clear_time: number; kills: number; total: number } {
|
||||||
|
const s = smc.vmdata.scores;
|
||||||
|
const kills = (s.melee_kill_count || 0) + (s.remote_kill_count || 0)
|
||||||
|
+ (s.elite_kill_count || 0) + (s.boss_kill_count || 0);
|
||||||
const entities = ecs.query(ecs.allOf(MissionMonCompComp));
|
const entities = ecs.query(ecs.allOf(MissionMonCompComp));
|
||||||
const comp = entities[0]?.get(MissionMonCompComp);
|
const comp = entities[0]?.get(MissionMonCompComp);
|
||||||
if (!comp) return { kills: 0, total: 0 };
|
const total = comp ? comp.getTotalSpawnedCount() : kills;
|
||||||
return comp.getSectionKillStats();
|
return {
|
||||||
|
clear_time: smc.vmdata.mission_data.fight_time,
|
||||||
|
kills,
|
||||||
|
total: Math.max(total, kills),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
private sectionTargetCount: number = 0;
|
private sectionTargetCount: number = 0;
|
||||||
/** 当前小节已刷出怪物数量 */
|
/** 当前小节已刷出怪物数量 */
|
||||||
private sectionSpawnedCount: number = 0;
|
private sectionSpawnedCount: number = 0;
|
||||||
|
/** 整局(整关 5 回合)累计刷出怪物总数:跨回合累计,仅在整关重置(由 MissionComp.data_init 触发) */
|
||||||
|
private missionSpawnedCount: number = 0;
|
||||||
/** 待刷出队列(batch=1 的怪物) */
|
/** 待刷出队列(batch=1 的怪物) */
|
||||||
private pendingQueue: GeneratedMonster[] = [];
|
private pendingQueue: GeneratedMonster[] = [];
|
||||||
/** 刷怪定时器已累积时间 */
|
/** 刷怪定时器已累积时间 */
|
||||||
@@ -241,6 +243,17 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sectionSpawnedCount++;
|
this.sectionSpawnedCount++;
|
||||||
|
this.missionSpawnedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整局累计已刷出怪物总数(跨回合,整关重置):供 MissionComp 整局击杀统计 */
|
||||||
|
public getTotalSpawnedCount(): number {
|
||||||
|
return this.missionSpawnedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整关重置刷怪统计(由 MissionComp.data_init 在整关开始/重开时调用,回合间不重置) */
|
||||||
|
public resetMissionStats(): void {
|
||||||
|
this.missionSpawnedCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,25 +270,6 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
return FightSet.MON_ROW_BACK_X;
|
return FightSet.MON_ROW_BACK_X;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 本节击杀统计(结算奖励消费):
|
|
||||||
* kills = 已刷出数 - 场上存活数(实时查询,不依赖 MissionComp 的 0.2s 降频同步)
|
|
||||||
* total = 本节目标怪物总数(含未刷出)
|
|
||||||
*/
|
|
||||||
public getSectionKillStats(): { kills: number; total: number } {
|
|
||||||
let alive = 0;
|
|
||||||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(entity => {
|
|
||||||
const attrs = entity.get(HeroAttrsComp);
|
|
||||||
if (attrs && !attrs.is_dead && attrs.fac === FacSet.MON) {
|
|
||||||
alive++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
kills: Math.max(0, this.sectionSpawnedCount - alive),
|
|
||||||
total: this.sectionTargetCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ECS 组件移除时触发 */
|
/** ECS 组件移除时触发 */
|
||||||
reset() { }
|
reset() { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export class VictoryComp extends CCComp {
|
|||||||
/** 是否胜利场(驱动主按钮文案与行为) */
|
/** 是否胜利场(驱动主按钮文案与行为) */
|
||||||
private isVictory: boolean = false;
|
private isVictory: boolean = false;
|
||||||
|
|
||||||
/** 失败奖励判定数据(MissionComp 传入:本节坚持时间 + 击杀统计) */
|
/** 失败奖励判定数据(MissionComp 传入:整局坚持时间 + 整局击杀统计) */
|
||||||
private defeatStats: { clear_time: number; kills: number; total: number } | null = null;
|
private defeatStats: { clear_time: number; kills: number; total: number } | null = null;
|
||||||
|
|
||||||
/** 本节怪物等级(= 大关号,驱动胜利奖励随关卡进度增长) */
|
/** 本节怪物等级(= 大关号,驱动胜利奖励随关卡进度增长) */
|
||||||
@@ -585,12 +585,14 @@ export class VictoryComp extends CCComp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算并发放结算奖励,结果存入 _rewardResult。
|
* 计算并发放结算奖励,结果存入 _rewardResult。
|
||||||
* 胜利:随关卡进度增长(base + monLv × per_lv,整体 ×1.2)。
|
* 通关:按 5 回合累计发放——
|
||||||
* Why: 无回合概念后 wave_win_count 每节被 resetScores 清零恒为 1,
|
* 每回合基础奖励(随 monLv 增长)× 胜利回合数(wave_win_count),
|
||||||
* 旧的 per_wave 增长失效,改挂怪物等级(= 大关号)随关卡推进线性增长。
|
* 再按全员存活回合数 × per_wave_bonus 加成。
|
||||||
|
* Why: 一关一局 5 回合后,通关奖励不再是单场,改按"5 回合累计"发放;
|
||||||
|
* scores 统计在整关间保留(data_init 仅在整关开始/重开重置),故可直接累计。
|
||||||
* 失败:受"坚持时间 + 击杀比例"限制——
|
* 失败:受"坚持时间 + 击杀比例"限制——
|
||||||
* - 坚持 < 15s 或本节一只怪未杀 → 无任何奖励;
|
* - 坚持 < 15s 或本回合一只怪未杀 → 无任何奖励;
|
||||||
* - 达标 → 坚持奖励(固定)+ 击杀比例奖励(基数 × 本节击杀比例),
|
* - 达标 → 坚持奖励(固定)+ 击杀比例奖励(基数 × 本回合击杀比例),
|
||||||
* 银币/钻石/碎片同样受此门槛限制(不达标不发,达标正常发放)。
|
* 银币/钻石/碎片同样受此门槛限制(不达标不发,达标正常发放)。
|
||||||
* @param isVictory 是否胜利
|
* @param isVictory 是否胜利
|
||||||
*/
|
*/
|
||||||
@@ -609,13 +611,19 @@ export class VictoryComp extends CCComp {
|
|||||||
killRatio = total > 0 ? Math.min(1, kills / total) : 0;
|
killRatio = total > 0 ? Math.min(1, kills / total) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5 回合累计:胜利回合数与全员存活回合数(scores 跨回合累计,整关重置)
|
||||||
|
const winWaves = Math.max(1, Math.floor(smc.vmdata.scores.wave_win_count || 0));
|
||||||
|
const aliveWaves = Math.max(0, Math.floor(smc.vmdata.scores.wave_all_alive_count || 0));
|
||||||
|
// 全员存活回合加成:每个全员存活回合额外 +per_wave_bonus(线性叠加于累计基础)
|
||||||
|
const aliveBonus = 1 + aliveWaves * cfg.per_wave_bonus;
|
||||||
|
|
||||||
// 1. 金币 / 天赋经验
|
// 1. 金币 / 天赋经验
|
||||||
let gold: number;
|
let gold: number;
|
||||||
let talentExp: number;
|
let talentExp: number;
|
||||||
if (isVictory) {
|
if (isVictory) {
|
||||||
const mult = cfg.victory_multiplier;
|
// 通关:每回合基础 × 胜利回合数 × 全员存活加成
|
||||||
gold = Math.floor((cfg.gold.base + this.monLv * cfg.gold.per_lv) * mult);
|
gold = Math.floor((cfg.gold.base + this.monLv * cfg.gold.per_lv) * winWaves * aliveBonus);
|
||||||
talentExp = Math.floor((cfg.talent_exp.base + this.monLv * cfg.talent_exp.per_lv) * mult);
|
talentExp = Math.floor((cfg.talent_exp.base + this.monLv * cfg.talent_exp.per_lv) * winWaves * aliveBonus);
|
||||||
} else if (defeatQualified) {
|
} else if (defeatQualified) {
|
||||||
// 失败达标 = 坚持奖励(固定)+ 击杀比例奖励
|
// 失败达标 = 坚持奖励(固定)+ 击杀比例奖励
|
||||||
gold = Math.floor(cfg.defeat.survive.gold + cfg.defeat.kill.gold * killRatio);
|
gold = Math.floor(cfg.defeat.survive.gold + cfg.defeat.kill.gold * killRatio);
|
||||||
@@ -625,25 +633,31 @@ export class VictoryComp extends CCComp {
|
|||||||
talentExp = 0;
|
talentExp = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.5 银币固定发放;钻石按输赢在 [min, max] 内随机(失败不达标时同样清零)
|
// 1.5 银币 / 钻石:通关按胜利回合数累计;失败不达标清零
|
||||||
const silver = !isVictory && !defeatQualified ? 0 : cfg.silver;
|
const silverBase = !isVictory && !defeatQualified ? 0 : cfg.silver;
|
||||||
|
const silver = isVictory ? Math.floor(silverBase * winWaves) : silverBase;
|
||||||
let diamond = 0;
|
let diamond = 0;
|
||||||
if (isVictory || defeatQualified) {
|
if (isVictory) {
|
||||||
const dRange = isVictory ? cfg.diamond.victory : cfg.diamond.defeat;
|
const dRange = cfg.diamond.victory;
|
||||||
|
const perWave = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
|
||||||
|
diamond = perWave * winWaves;
|
||||||
|
} else if (defeatQualified) {
|
||||||
|
const dRange = cfg.diamond.defeat;
|
||||||
diamond = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
|
diamond = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄(失败不达标时不发)
|
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄(通关 × 胜利回合数累计;失败不达标时不发)
|
||||||
const fragments: FragmentReward[] = [];
|
const fragments: FragmentReward[] = [];
|
||||||
if (isVictory || defeatQualified) {
|
if (isVictory || defeatQualified) {
|
||||||
const playedUuids = this.collectPlayedHeroUuids();
|
const playedUuids = this.collectPlayedHeroUuids();
|
||||||
const playedSet = new Set(playedUuids);
|
const playedSet = new Set(playedUuids);
|
||||||
|
const fragMult = isVictory ? winWaves : 1;
|
||||||
for (const uuid of playedUuids) {
|
for (const uuid of playedUuids) {
|
||||||
fragments.push({ uuid, count: cfg.played_fragment, isPlayed: true });
|
fragments.push({ uuid, count: cfg.played_fragment * fragMult, isPlayed: true });
|
||||||
}
|
}
|
||||||
const randomUuid = this.pickRandomHeroUuid(playedSet);
|
const randomUuid = this.pickRandomHeroUuid(playedSet);
|
||||||
if (randomUuid > 0) {
|
if (randomUuid > 0) {
|
||||||
fragments.push({ uuid: randomUuid, count: cfg.random_fragment, isPlayed: false });
|
fragments.push({ uuid: randomUuid, count: cfg.random_fragment * fragMult, isPlayed: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,7 +681,8 @@ export class VictoryComp extends CCComp {
|
|||||||
this._rewardResult = { gold, talentExp, silver, diamond, fragments };
|
this._rewardResult = { gold, talentExp, silver, diamond, fragments };
|
||||||
|
|
||||||
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
|
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
|
||||||
isVictory, monLv: this.monLv, gold, talentExp, silver, diamond, fragments,
|
isVictory, monLv: this.monLv, winWaves, aliveWaves, aliveBonus,
|
||||||
|
gold, talentExp, silver, diamond, fragments,
|
||||||
defeatStats: this.defeatStats, defeatQualified, killRatio,
|
defeatStats: this.defeatStats, defeatQualified, killRatio,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user