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:
@@ -240,4 +240,17 @@ export const VICTORY_REWARD_SET = {
|
|||||||
},
|
},
|
||||||
/** 胜利加成系数(失败 1.0) */
|
/** 胜利加成系数(失败 1.0) */
|
||||||
victory_multiplier: 1.2,
|
victory_multiplier: 1.2,
|
||||||
|
/**
|
||||||
|
* 失败奖励限制(不影响胜利):
|
||||||
|
* 坚持 < defeat_time_threshold 秒或本节一只怪未杀 → 无任何奖励;
|
||||||
|
* 达标后 = 坚持奖励(固定)+ 击杀比例奖励(基数 × 本节击杀比例)
|
||||||
|
*/
|
||||||
|
defeat: {
|
||||||
|
/** 坚持时间门槛(秒) */
|
||||||
|
time_threshold: 15,
|
||||||
|
/** 超门槛固定奖励("超 15 秒奖励") */
|
||||||
|
survive: { gold: 50, talent_exp: 10 },
|
||||||
|
/** 击杀比例奖励基数(本节怪物全灭时全额发放) */
|
||||||
|
kill: { gold: 150, talent_exp: 30 },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
|||||||
import { FieldSkillType } from "../common/config/SkillSet";
|
import { FieldSkillType } from "../common/config/SkillSet";
|
||||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||||
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
|
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
|
||||||
|
import { MissionMonCompComp } from "./MissionMonComp";
|
||||||
import {
|
import {
|
||||||
getProgressText,
|
getProgressText,
|
||||||
} from "../common/config/StageSet";
|
} from "../common/config/StageSet";
|
||||||
@@ -637,6 +638,11 @@ export class MissionComp extends CCComp {
|
|||||||
// 结算类型:小节胜利显示"下一节",末关通关显示"确认退出" + 通关标题
|
// 结算类型:小节胜利显示"下一节",末关通关显示"确认退出" + 通关标题
|
||||||
settle_type: this._settleType,
|
settle_type: this._settleType,
|
||||||
settle_title: this._settleTitle,
|
settle_title: this._settleTitle,
|
||||||
|
// 失败奖励判定数据:本节坚持时间 + 击杀统计(kills=已刷出-存活)
|
||||||
|
defeat_stats: {
|
||||||
|
clear_time: this.clearTime,
|
||||||
|
...this.querySectionKillStats(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 如果 play 已经是 false,说明是通过 fight_end 进来的
|
// 如果 play 已经是 false,说明是通过 fight_end 进来的
|
||||||
@@ -1080,6 +1086,17 @@ export class MissionComp extends CCComp {
|
|||||||
return hasHero && smc.mission.dead_heroes.length === 0;
|
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 当前存活英雄数量
|
* @param heroCount 当前存活英雄数量
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/O
|
|||||||
import { Monster } from "../hero/Mon";
|
import { Monster } from "../hero/Mon";
|
||||||
import { smc } from "../common/SingletonModuleComp";
|
import { smc } from "../common/SingletonModuleComp";
|
||||||
import { GameEvent } from "../common/config/GameEvent";
|
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 { MonType } from "../common/config/heroSet";
|
||||||
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
|
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
|
||||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
@@ -254,6 +254,25 @@ 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,6 +177,9 @@ export class VictoryComp extends CCComp {
|
|||||||
/** 是否胜利场(驱动主按钮文案与行为) */
|
/** 是否胜利场(驱动主按钮文案与行为) */
|
||||||
private isVictory: boolean = false;
|
private isVictory: boolean = false;
|
||||||
|
|
||||||
|
/** 失败奖励判定数据(MissionComp 传入:本节坚持时间 + 击杀统计) */
|
||||||
|
private defeatStats: { clear_time: number; kills: number; total: number } | null = null;
|
||||||
|
|
||||||
/** 加载时隐藏 loading 遮罩并绑定按钮事件 */
|
/** 加载时隐藏 loading 遮罩并绑定按钮事件 */
|
||||||
protected onLoad(): void {
|
protected onLoad(): void {
|
||||||
this.node.getChildByName("loading").active = false
|
this.node.getChildByName("loading").active = false
|
||||||
@@ -221,6 +224,7 @@ export class VictoryComp extends CCComp {
|
|||||||
// 结算类型与胜负状态:缺省按失败处理
|
// 结算类型与胜负状态:缺省按失败处理
|
||||||
this.settleType = args.settle_type ?? "defeat";
|
this.settleType = args.settle_type ?? "defeat";
|
||||||
this.isVictory = !!args.victory;
|
this.isVictory = !!args.victory;
|
||||||
|
this.defeatStats = args.defeat_stats ?? null;
|
||||||
|
|
||||||
// 根据是否可复活决定按钮显示
|
// 根据是否可复活决定按钮显示
|
||||||
this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
|
this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
|
||||||
@@ -604,34 +608,66 @@ export class VictoryComp extends CCComp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算并发放结算奖励,结果存入 _rewardResult。
|
* 计算并发放结算奖励,结果存入 _rewardResult。
|
||||||
* @param isVictory 是否胜利(胜利整体 ×1.2)
|
* 胜利:原公式不变(base + wave × per_wave,整体 ×1.2)。
|
||||||
|
* 失败:受"坚持时间 + 击杀比例"限制——
|
||||||
|
* - 坚持 < 15s 或本节一只怪未杀 → 无任何奖励;
|
||||||
|
* - 达标 → 坚持奖励(固定)+ 击杀比例奖励(基数 × 本节击杀比例),
|
||||||
|
* 银币/钻石/碎片同样受此门槛限制(不达标不发,达标正常发放)。
|
||||||
|
* @param isVictory 是否胜利
|
||||||
*/
|
*/
|
||||||
private grantRewards(isVictory: boolean) {
|
private grantRewards(isVictory: boolean) {
|
||||||
const s = smc.vmdata.scores;
|
const s = smc.vmdata.scores;
|
||||||
const cfg = VICTORY_REWARD_SET;
|
const cfg = VICTORY_REWARD_SET;
|
||||||
const mult = isVictory ? cfg.victory_multiplier : 1.0;
|
|
||||||
const wave = Math.max(0, s.wave_win_count || 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);
|
let defeatQualified = false;
|
||||||
const talentExp = Math.floor((cfg.talent_exp.base + wave * cfg.talent_exp.per_wave) * mult);
|
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] 内随机(不吃胜利加成,避免与固定区间重复放大)
|
// 1. 金币 / 天赋经验
|
||||||
const silver = cfg.silver;
|
let gold: number;
|
||||||
const dRange = isVictory ? cfg.diamond.victory : cfg.diamond.defeat;
|
let talentExp: number;
|
||||||
const diamond = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
|
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 个随机英雄
|
// 1.5 银币固定发放;钻石按输赢在 [min, max] 内随机(失败不达标时同样清零)
|
||||||
const playedUuids = this.collectPlayedHeroUuids();
|
const silver = !isVictory && !defeatQualified ? 0 : cfg.silver;
|
||||||
const playedSet = new Set(playedUuids);
|
let diamond = 0;
|
||||||
const fragments: FragmentReward[] = playedUuids.map(uuid => ({
|
if (isVictory || defeatQualified) {
|
||||||
uuid,
|
const dRange = isVictory ? cfg.diamond.victory : cfg.diamond.defeat;
|
||||||
count: cfg.played_fragment,
|
diamond = dRange.min + Math.floor(Math.random() * (dRange.max - dRange.min + 1));
|
||||||
isPlayed: true,
|
}
|
||||||
}));
|
|
||||||
const randomUuid = this.pickRandomHeroUuid(playedSet);
|
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄(失败不达标时不发)
|
||||||
if (randomUuid > 0) {
|
const fragments: FragmentReward[] = [];
|
||||||
fragments.push({ uuid: randomUuid, count: cfg.random_fragment, isPlayed: false });
|
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. 落地发放
|
// 3. 落地发放
|
||||||
@@ -655,6 +691,7 @@ export class VictoryComp extends CCComp {
|
|||||||
|
|
||||||
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
|
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
|
||||||
isVictory, wave, gold, talentExp, silver, diamond, fragments,
|
isVictory, wave, gold, talentExp, silver, diamond, fragments,
|
||||||
|
defeatStats: this.defeatStats, defeatQualified, killRatio,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user