diff --git a/assets/script/game/common/config/GameSet.ts b/assets/script/game/common/config/GameSet.ts index 480b20d4..7b3ccb45 100644 --- a/assets/script/game/common/config/GameSet.ts +++ b/assets/script/game/common/config/GameSet.ts @@ -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 }, + }, }; diff --git a/assets/script/game/map/MissionComp.ts b/assets/script/game/map/MissionComp.ts index 8221259c..9d37bf5f 100644 --- a/assets/script/game/map/MissionComp.ts +++ b/assets/script/game/map/MissionComp.ts @@ -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 当前存活英雄数量 diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index 03af4d15..bbaa22d6 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -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() { } } diff --git a/assets/script/game/map/VictoryComp.ts b/assets/script/game/map/VictoryComp.ts index 4c22fa30..cf09e5b4 100644 --- a/assets/script/game/map/VictoryComp.ts +++ b/assets/script/game/map/VictoryComp.ts @@ -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, }); }