feat(victory): add defeat reward gate by clear time and kill ratio

Defeat rewards now require surviving 15s and killing at least one
monster. Qualified defeats grant a fixed survive reward plus a
kill-ratio-scaled bonus; unqualified defeats yield nothing.
Victory rewards remain unchanged.
This commit is contained in:
pan
2026-09-11 15:57:21 +08:00
parent a6dbda13b1
commit 5fe961846f
4 changed files with 107 additions and 21 deletions

View File

@@ -240,4 +240,17 @@ export const VICTORY_REWARD_SET = {
},
/** 胜利加成系数(失败 1.0) */
victory_multiplier: 1.2,
/**
* 失败奖励限制(不影响胜利):
* 坚持 < defeat_time_threshold 秒或本节一只怪未杀 → 无任何奖励;
* 达标后 = 坚持奖励(固定)+ 击杀比例奖励(基数 × 本节击杀比例)
*/
defeat: {
/** 坚持时间门槛(秒) */
time_threshold: 15,
/** 超门槛固定奖励("超 15 秒奖励") */
survive: { gold: 50, talent_exp: 10 },
/** 击杀比例奖励基数(本节怪物全灭时全额发放) */
kill: { gold: 150, talent_exp: 30 },
},
};

View File

@@ -46,6 +46,7 @@ import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
import { MissionMonCompComp } from "./MissionMonComp";
import {
getProgressText,
} from "../common/config/StageSet";
@@ -637,6 +638,11 @@ export class MissionComp extends CCComp {
// 结算类型:小节胜利显示"下一节",末关通关显示"确认退出" + 通关标题
settle_type: this._settleType,
settle_title: this._settleTitle,
// 失败奖励判定数据:本节坚持时间 + 击杀统计(kills=已刷出-存活)
defeat_stats: {
clear_time: this.clearTime,
...this.querySectionKillStats(),
},
});
} else {
// 如果 play 已经是 false,说明是通过 fight_end 进来的
@@ -1080,6 +1086,17 @@ export class MissionComp extends CCComp {
return hasHero && smc.mission.dead_heroes.length === 0;
}
/**
* 查询本节击杀统计(转交结算弹窗做失败奖励判定)
* @returns { kills, total },查询不到 MissionMonComp 时返回 0/0
*/
private querySectionKillStats(): { kills: number; total: number } {
const entities = ecs.query(ecs.allOf(MissionMonCompComp));
const comp = entities[0]?.get(MissionMonCompComp);
if (!comp) return { kills: 0, total: 0 };
return comp.getSectionKillStats();
}
/**
* 英雄全灭检测:若场上无存活英雄且处于战斗中,预约延迟结算(等待死亡动画播完再弹窗)。
* @param heroCount 当前存活英雄数量

View File

@@ -22,7 +22,7 @@ import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/O
import { Monster } from "../hero/Mon";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BoxSet, FightSet } from "../common/config/GameSet";
import { BoxSet, FacSet, FightSet } from "../common/config/GameSet";
import { MonType } from "../common/config/heroSet";
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
@@ -254,6 +254,25 @@ export class MissionMonCompComp extends CCComp {
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 组件移除时触发 */
reset() { }
}

View File

@@ -177,6 +177,9 @@ export class VictoryComp extends CCComp {
/** 是否胜利场(驱动主按钮文案与行为) */
private isVictory: boolean = false;
/** 失败奖励判定数据(MissionComp 传入:本节坚持时间 + 击杀统计) */
private defeatStats: { clear_time: number; kills: number; total: number } | null = null;
/** 加载时隐藏 loading 遮罩并绑定按钮事件 */
protected onLoad(): void {
this.node.getChildByName("loading").active = false
@@ -221,6 +224,7 @@ export class VictoryComp extends CCComp {
// 结算类型与胜负状态:缺省按失败处理
this.settleType = args.settle_type ?? "defeat";
this.isVictory = !!args.victory;
this.defeatStats = args.defeat_stats ?? null;
// 根据是否可复活决定按钮显示
this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
@@ -604,34 +608,66 @@ export class VictoryComp extends CCComp {
/**
* 计算并发放结算奖励,结果存入 _rewardResult。
* @param isVictory 是否胜利(胜利整体 ×1.2)
* 胜利:原公式不变(base + wave × per_wave,整体 ×1.2)。
* 失败:受"坚持时间 + 击杀比例"限制——
* - 坚持 < 15s 或本节一只怪未杀 → 无任何奖励;
* - 达标 → 坚持奖励(固定)+ 击杀比例奖励(基数 × 本节击杀比例),
* 银币/钻石/碎片同样受此门槛限制(不达标不发,达标正常发放)。
* @param isVictory 是否胜利
*/
private grantRewards(isVictory: boolean) {
const s = smc.vmdata.scores;
const cfg = VICTORY_REWARD_SET;
const mult = isVictory ? cfg.victory_multiplier : 1.0;
const wave = Math.max(0, s.wave_win_count || 0);
// 1. 金币 / 天赋经验(按波次计算,胜利加成)
const gold = Math.floor((cfg.gold.base + wave * cfg.gold.per_wave) * mult);
const talentExp = Math.floor((cfg.talent_exp.base + wave * cfg.talent_exp.per_wave) * mult);
// 失败奖励资格判定:坚持时间与击杀数不达标则整场无奖励
let defeatQualified = false;
let killRatio = 0;
if (!isVictory) {
const stats = this.defeatStats;
const clearTime = stats?.clear_time ?? 0;
const kills = stats?.kills ?? 0;
const total = stats?.total ?? 0;
defeatQualified = clearTime >= cfg.defeat.time_threshold && kills > 0;
killRatio = total > 0 ? Math.min(1, kills / total) : 0;
}
// 1.5 银币固定发放;钻石按输赢在 [min, max] 内随机(不吃胜利加成,避免与固定区间重复放大)
const silver = cfg.silver;
const dRange = isVictory ? cfg.diamond.victory : cfg.diamond.defeat;
const diamond = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
// 1. 金币 / 天赋经验
let gold: number;
let talentExp: number;
if (isVictory) {
const mult = cfg.victory_multiplier;
gold = Math.floor((cfg.gold.base + wave * cfg.gold.per_wave) * mult);
talentExp = Math.floor((cfg.talent_exp.base + wave * cfg.talent_exp.per_wave) * mult);
} else if (defeatQualified) {
// 失败达标 = 坚持奖励(固定)+ 击杀比例奖励
gold = Math.floor(cfg.defeat.survive.gold + cfg.defeat.kill.gold * killRatio);
talentExp = Math.floor(cfg.defeat.survive.talent_exp + cfg.defeat.kill.talent_exp * killRatio);
} else {
gold = 0;
talentExp = 0;
}
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄
const playedUuids = this.collectPlayedHeroUuids();
const playedSet = new Set(playedUuids);
const fragments: FragmentReward[] = playedUuids.map(uuid => ({
uuid,
count: cfg.played_fragment,
isPlayed: true,
}));
const randomUuid = this.pickRandomHeroUuid(playedSet);
if (randomUuid > 0) {
fragments.push({ uuid: randomUuid, count: cfg.random_fragment, isPlayed: false });
// 1.5 银币固定发放;钻石按输赢在 [min, max] 内随机(失败不达标时同样清零)
const silver = !isVictory && !defeatQualified ? 0 : cfg.silver;
let diamond = 0;
if (isVictory || defeatQualified) {
const dRange = isVictory ? cfg.diamond.victory : cfg.diamond.defeat;
diamond = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
}
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄(失败不达标时不发)
const fragments: FragmentReward[] = [];
if (isVictory || defeatQualified) {
const playedUuids = this.collectPlayedHeroUuids();
const playedSet = new Set(playedUuids);
for (const uuid of playedUuids) {
fragments.push({ uuid, count: cfg.played_fragment, isPlayed: true });
}
const randomUuid = this.pickRandomHeroUuid(playedSet);
if (randomUuid > 0) {
fragments.push({ uuid: randomUuid, count: cfg.random_fragment, isPlayed: false });
}
}
// 3. 落地发放
@@ -655,6 +691,7 @@ export class VictoryComp extends CCComp {
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
isVictory, wave, gold, talentExp, silver, diamond, fragments,
defeatStats: this.defeatStats, defeatQualified, killRatio,
});
}