Files
pixelheros/assets/script/game/map/VictoryComp.ts
panFD da2caa6576 refactor(mission): 重构关卡结算与刷怪统计逻辑
1. 新增整局跨回合刷怪统计与重置能力,替换原小节统计
2. 修改奖励配置为整局5回合累计结算逻辑
3. 调整失败奖励判定为整局维度统计
4. 重构胜利/失败奖励计算逻辑,适配多回合关卡
2026-09-13 10:28:55 +08:00

904 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file VictoryComp.ts
* @description 战斗结算弹窗组件(UI 视图层)
*
* 通过 oops.gui.open(UIID.Victory, args) 打开。
* 展示段位徽章、总分滚动动画、段位进度与亮点成就,并提供重开 / 退出操作。
*/
import { _decorator, instantiate, Label, Prefab, Node, Sprite, ProgressBar, Tween, tween, v3, UIOpacity, Button } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { HighlightSet, HighlightType, HighlightLevel } from "../common/config/HighlightSet";
import { getRankByScore, getNextRank, getRankProgress, ScoreRankInfo } from "../common/config/ScoreRankSet";
import { LangPrefix, lang, langf } from "../common/LangUtil";
import { mLogger } from "../common/Logger";
import { UIID } from "../common/config/GameUIConfig";
import { VICTORY_REWARD_SET } from "../common/config/GameSet";
import { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { ChipComp } from "./ChipComp";
import { RewardItemComp } from "./RewardItemComp";
const { ccclass, property } = _decorator;
/** 总分滚动动画时长(秒) */
const SCORE_TWEEN_DURATION = 1.6;
/** 段位晋升动效时长(秒) */
const RANK_UP_DURATION = 0.5;
/** 奖励条目逐个入场的间隔(秒) */
const REWARD_ENTER_INTERVAL = 0.12;
/** 单个英雄碎片奖励 */
interface FragmentReward {
/** 英雄模板 uuid */
uuid: number;
/** 碎片数量 */
count: number;
/** 是否为本局出场英雄(否则为随机英雄) */
isPlayed: boolean;
}
/** 结算奖励结果 */
interface RewardResult {
/** 发放的金币数量 */
gold: number;
/** 发放的天赋经验数量 */
talentExp: number;
/** 发放的银币数量 */
silver: number;
/** 发放的钻石数量 */
diamond: number;
/** 英雄碎片奖励列表 */
fragments: FragmentReward[];
}
/**
* VictoryComp —— 战斗结算弹窗视图组件
*
* 布局(自上而下):
* rank_icon 段位徽章
* total 总分(score_label 滚动数字)
* rank_progress 段位内进度条
* next_rank_label 下一段位提示
* gain_label +N 涨幅标签(仅破纪录时显示)
* highlights 亮点成就标签容器
* btns 操作按钮
*/
@ccclass('VictoryComp')
@ecs.register('Victory', false)
export class VictoryComp extends CCComp {
// ======================== 结算 UI 绑定 ========================
@property({ type: Sprite, tooltip: "段位徽章图标(rank_icon 节点上的 Sprite)" })
rank_icon: Sprite = null!;
@property({ type: Node, tooltip: "撒花装饰节点(fireworks,循环缩放弹跳)" })
fireworks: Node = null!;
@property({ type: Node, tooltip: "光晕装饰节点(halo,持续旋转)" })
halo: Node = null!;
@property({ type: Label, tooltip: "总分文本(total/score_label)" })
total_score_label: Label = null!;
@property({ type: ProgressBar, tooltip: "段位进度条(rank_progress)" })
rank_progress: ProgressBar = null!;
@property({ type: Label, tooltip: "下一段位提示文本(next_rank_label)" })
next_rank_label: Label = null!;
@property({ type: Label, tooltip: "涨幅标签(gain_label,仅破纪录时显示)" })
gain_label: Label = null!;
@property({ type: Node, tooltip: "亮点成就标签的容器" })
highlights_container: Node = null!;
@property({ type: Prefab, tooltip: "亮点成就标签预制体" })
highlight_prefab: Prefab = null!;
// ======================== 结算奖励绑定 ========================
@property({ type: Node, tooltip: "结算奖励条目容器(rewards_container,横向 Layout)" })
rewards_container: Node = null!;
@property({ type: Prefab, tooltip: "英雄碎片条目预制体(clips.prefab,挂 ChipComp,num 子节点显示数量)" })
clip_prefab: Prefab = null!;
@property({ type: Prefab, tooltip: "金币奖励条目预制体(reward_item_gold.prefab,挂 RewardItemComp)" })
reward_gold_prefab: Prefab = null!;
@property({ type: Prefab, tooltip: "天赋经验奖励条目预制体(reward_item_exp.prefab,挂 RewardItemComp)" })
reward_exp_prefab: Prefab = null!;
@property({ type: Prefab, tooltip: "银币奖励条目预制体(reward_item_silver.prefab,挂 RewardItemComp)" })
reward_silver_prefab: Prefab = null!;
@property({ type: Prefab, tooltip: "钻石奖励条目预制体(reward_item_diamond.prefab,挂 RewardItemComp)" })
reward_diamond_prefab: Prefab = null!;
// ======================== 关卡推进绑定 ========================
/** 下一节按钮(小节胜利时显示,末关通关/失败时隐藏) */
@property({ type: Node, tooltip: "下一节按钮(小节胜利时显示)" })
next_section_btn: Node = null!;
/** 确认按钮(末关通关/失败时显示,点击确认退出结算) */
@property({ type: Node, tooltip: "确认按钮(末关通关/失败时显示,点击确认退出结算)" })
confirm_btn: Node = null!;
/** 通关标题(末关通关时显示"本关通关/本章通关") */
@property({ type: Label, tooltip: "通关标题 Label(末关通关时显示,默认隐藏)" })
settle_title: Label = null!;
/** 调试日志开关 */
debugMode: boolean = false;
/** 奖励等级(预留) */
reward_lv: number = 1
/** 奖励数量(预留) */
reward_num: number = 2
/** 掉落奖励列表 */
rewards: any[] = []
/** 累计游戏数据(经验 / 金币 / 钻石) */
game_data: any = {
exp: 0,
gold: 0,
diamond: 0
}
// ======================== 结算奖励(运行时) ========================
/** 本次结算实际发放的奖励结果(供渲染) */
private _rewardResult: RewardResult = null!;
// ======================== 动画状态 ========================
/** 动画期间实时显示的分数(tween 驱动) */
private _displayScore: number = 0;
/** 滚动前的生涯累计分(动画起始值) */
private _prevBestScore: number = 0;
/** 滚动后的生涯累计分(动画结束值) */
private _finalScore: number = 0;
/** 滚动起始段位(用于检测跨段) */
private _startRank: ScoreRankInfo = null!;
/** 动画期间是否已触发过段位晋升动效 */
private _rankUpPlayed: boolean = false;
// ======================== 复活相关 ========================
/** 是否可以复活(由 MissionComp 传入,取决于剩余复活次数) */
private canRevive: boolean = false;
/** 结算类型:final=通关(确认退出), defeat=失败(再来一次/退出) */
private settleType: string = "defeat";
/** 是否胜利场(驱动主按钮文案与行为) */
private isVictory: boolean = false;
/** 失败奖励判定数据(MissionComp 传入:整局坚持时间 + 整局击杀统计) */
private defeatStats: { clear_time: number; kills: number; total: number } | null = null;
/** 本节怪物等级(= 大关号,驱动胜利奖励随关卡进度增长) */
private monLv: number = 1;
/** 加载时隐藏 loading 遮罩并绑定按钮事件 */
protected onLoad(): void {
this.node.getChildByName("loading").active = false
// 代码绑定按钮点击事件,替代编辑器 Click Events 配置
// next 按钮按状态分发:失败→再来一次(一关一局无节间"下一节")
this.bindButtonClick(this.next_section_btn, this.on_next_btn_click);
this.bindButtonClick(this.confirm_btn, this.victory_end);
}
/**
* 为按钮节点绑定点击事件(无则自动添加 Button 组件)
* @param node 按钮节点
* @param handler 点击回调
*/
private bindButtonClick(node: Node, handler: () => void) {
if (!node || !node.isValid) return;
let btn = node.getComponent(Button);
if (!btn) {
btn = node.addComponent(Button);
}
node.on(Button.EventType.CLICK, handler, this);
}
/**
* 弹窗打开时的回调:接收战斗结果参数。
*
* @param args.victory 是否胜利(当前仅用于标识)
* @param args.rewards 掉落奖励列表
* @param args.game_data 累计数据 { exp, gold, diamond }
* @param args.can_revive 是否可复活
* @param args.settle_type 结算类型:final=通关(确认退出),defeat=失败(再来一次/退出)
* @param args.settle_title 通关标题(如"本关通关"),非空时在结算页顶部展示
*/
onAdded(args: any) {
this.node.getChildByName("loading").active = false
mLogger.log(this.debugMode, 'VictoryComp', "[VictoryComp] onAdded", args)
if (args.game_data) {
this.game_data = args.game_data
}
// 结算类型与胜负状态:缺省按失败处理
this.settleType = args.settle_type ?? "defeat";
this.isVictory = !!args.victory;
this.defeatStats = args.defeat_stats ?? null;
this.monLv = Math.max(1, Number(args.mon_lv) || 1);
// 根据是否可复活决定按钮显示
this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
// 按钮显隐与文案:再来一次 / 确认退出(通关隐藏再来一次)
this.updateButtonStates();
// 通关标题:仅通关时显示
this.updateSettleTitle(args.settle_title ? String(args.settle_title) : "");
// 计算总分并写入历史纪录
this.calculateTotalScore(!!args.victory, !!args.all_alive);
// 计算并发放结算奖励(英雄碎片 + 金币 + 天赋经验)
this.grantRewards(!!args.victory);
// 每次战斗结束都标记数据脏并触发云端同步(防抖 3 秒后自动上传)
smc.updateCloudData();
// 播放结算动画
this.playSettleAnimation();
// 渲染亮点标签
this.renderHighlights();
// 渲染结算奖励条目
this.renderRewards();
}
// ======================== 分数计算(逻辑层,保持不变) ========================
/**
* 获取满足条件的最高等级的亮点成就
* @param type 亮点类型
* @param value 玩家实际达成的值
* @returns 达成的最高等级配置,未达成返回 null
*/
private getHighestHighlightLevel(type: HighlightType, value: number): HighlightLevel | null {
const config = HighlightSet[type];
if (!config || !config.levels) return null;
let highest: HighlightLevel | null = null;
for (const levelConfig of config.levels) {
if (value >= levelConfig.threshold) {
highest = levelConfig;
}
}
return highest;
}
/**
* 计算并获取所有达成的最高亮点配置数组,用于加分和UI展示
*/
private getAchievedHighlights(s: any): { type: HighlightType, config: HighlightLevel, value: number }[] {
const achieved: { type: HighlightType, config: HighlightLevel, value: number }[] = [];
// 计算辅助比例
const refreshRatio = s.refresh_count > 0 ? (s.refresh_hit_count / s.refresh_count) : 0;
const goldRatio = s.gold_earned > 0 ? (s.gold_spent / s.gold_earned) : 0;
// 判定表:每个维度对应的值
const checkList: { type: HighlightType, value: number }[] = [
{ type: HighlightType.CritMaster, value: s.crt_count },
{ type: HighlightType.DeathExpert, value: s.dead_trigger_count },
{ type: HighlightType.IronWall, value: s.shield_block_count },
// 风怒机制已停用: WindStorm 成就判定已移除
// { type: HighlightType.WindStorm, value: s.wf_count },
{ type: HighlightType.OneHitKill, value: Math.floor(s.highest_dmg) },
{ type: HighlightType.HealingLight, value: Math.floor(s.heal_total) },
{ type: HighlightType.PerfectClear, value: (s.passed_wave_20 && s.wave_all_alive_count >= 20) ? 1 : 0 },
{ type: HighlightType.LuckyKing, value: refreshRatio },
{ type: HighlightType.ThriftyPlayer, value: goldRatio }
];
for (const item of checkList) {
const levelConfig = this.getHighestHighlightLevel(item.type, item.value);
if (levelConfig) {
achieved.push({ type: item.type, config: levelConfig, value: item.value });
}
}
return achieved;
}
/**
* 计算本局得分并累加到生涯总分(smc.data.score 为累计分,驱动段位)。
*
* 积分规则:
* - 单局:胜利 +10(全队存活再 +5)/ 失败 -10;暴击/闪避/击晕/冰冻每次 +1、单项封顶 5
* - 累计:生涯分 += max(0, 本局分),负分局不回退(不掉段),累计下限钳 0
*
* @param isVictory 是否胜利
* @param allAlive 是否全队存活
*/
private calculateTotalScore(isVictory: boolean, allAlive: boolean) {
const s = smc.vmdata.scores;
// 0. 胜负分:胜利 +10(全队存活再 +5),失败 -10
let base = 0;
if (isVictory) {
base = 10;
if (allAlive) {
base += 5;
}
} else {
base = -10;
}
// 1. 战斗行为分:暴击/闪避/击晕/冰冻每次 +1,单项封顶 5
const critPts = Math.min(5, s.crt_count);
const dodgePts = Math.min(5, s.dod_count);
const stunPts = Math.min(5, s.stun_count);
const freezePts = Math.min(5, s.freeze_count);
// 本局得分:失败局可为负(结算页如实展示)
s.score = Math.floor(base + critPts + dodgePts + stunPts + freezePts);
// 亮点成就判定:仅作结算页亮点标签展示,不计入总分
s.achieved_highlights = this.getAchievedHighlights(s);
// 累计生涯分:正贡献累加、负分不回退(声望式不掉段),下限钳 0
this._prevBestScore = smc.data.score;
const gain = Math.max(0, s.score);
smc.data.score = Math.max(0, this._prevBestScore + gain);
this._finalScore = smc.data.score;
// 持久化生涯分(云端/本地同步)
if (typeof smc.updateCloudData === "function") {
smc.updateCloudData();
}
// 借用 scores 对象传递标记:本局有正贡献(供涨幅标签展示)
(s as any).isNewRecord = gain > 0;
mLogger.log(this.debugMode, 'VictoryComp', `[VictoryComp] 本局得分: ${s.score}`, {
base,
critPts, dodgePts, stunPts, freezePts,
gain,
careerTotal: this._finalScore,
prevTotal: this._prevBestScore
});
}
// ======================== 结算动画 ========================
/**
* 播放结算动画:入场 → 总分滚动 → 定格涨幅
*/
private playSettleAnimation() {
// 初始化动画状态
this._displayScore = this._prevBestScore;
this._startRank = getRankByScore(this._prevBestScore);
this._rankUpPlayed = false;
// 初始渲染:旧纪录分数与段位
this.renderScore(this._prevBestScore);
this.renderRank(this._prevBestScore);
// 隐藏涨幅标签
if (this.gain_label) {
this.gain_label.node.active = false;
}
// 若本局分数未超过旧纪录,直接定格,无需滚动
if (this._finalScore <= this._prevBestScore) {
this.showGainLabel();
return;
}
// 滚动动画:从旧纪录涨到本局分数
const self = this;
const proxy = { value: this._prevBestScore };
tween(proxy)
.to(SCORE_TWEEN_DURATION, { value: this._finalScore }, {
easing: 'quadOut',
onUpdate(target) {
self.onScoreTweenUpdate(Math.floor(target!.value));
}
})
.call(() => {
self.onScoreTweenEnd();
})
.start();
}
/**
* 总分滚动过程中的每一帧更新
* @param score 当前插值分数
*/
private onScoreTweenUpdate(score: number) {
this._displayScore = score;
this.renderScore(score);
this.renderRank(score);
// 跨段检测:当前分数已超过起始段位的下一段门槛,触发晋升动效
const currentRank = getRankByScore(score);
if (!this._rankUpPlayed && currentRank.tier !== this._startRank.tier) {
this._rankUpPlayed = true;
this.playRankUpEffect();
}
}
/**
* 总分滚动结束
*/
private onScoreTweenEnd() {
this._displayScore = this._finalScore;
this.renderScore(this._finalScore);
this.renderRank(this._finalScore);
this.showGainLabel();
}
/**
* 渲染总分数字
*/
private renderScore(score: number) {
if (this.total_score_label) {
this.total_score_label.string = `${score}`;
}
}
/**
* 渲染段位徽章、进度条与下一段位提示
* @param score 当前用于判定段位的分数
*/
private renderRank(score: number) {
const rank = getRankByScore(score);
const next = getNextRank(score);
// 段位徽章:从全局图集取帧
if (this.rank_icon && smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(rank.icon);
if (frame) {
this.rank_icon.spriteFrame = frame;
}
}
// 段位进度条
if (this.rank_progress) {
this.rank_progress.progress = getRankProgress(score);
}
// 下一段位提示
if (this.next_rank_label) {
this.next_rank_label.string = next
? `下一段位:${next.name}(${next.minScore} 分)`
: `已达最高段位`;
}
}
/**
* 段位晋升动效:徽章放大回弹 + 闪白
*/
private playRankUpEffect() {
if (!this.rank_icon) return;
const node = this.rank_icon.node;
// 停止徽章上残留的 tween,避免叠加
Tween.stopAllByTarget(node);
node.setScale(1, 1, 1);
tween(node)
.to(RANK_UP_DURATION * 0.4, { scale: v3(1.4, 1.4, 1) }, { easing: 'quadOut' })
.to(RANK_UP_DURATION * 0.6, { scale: v3(1, 1, 1) }, { easing: 'backOut' })
.start();
// 若有 UIOpacity 组件则做一次闪白
const op = node.getComponent(UIOpacity);
if (op) {
Tween.stopAllByTarget(op);
tween(op)
.to(RANK_UP_DURATION * 0.3, { opacity: 120 })
.to(RANK_UP_DURATION * 0.7, { opacity: 255 })
.start();
}
}
/**
* 滚动定格后显示 +N 涨幅标签(本局有正贡献时;跨段位时附加升段文案)
*/
private showGainLabel() {
if (!this.gain_label) return;
const s = smc.vmdata.scores;
const hasGain = (s as any).isNewRecord === true;
const gain = this._finalScore - this._prevBestScore;
if (!hasGain || gain <= 0) {
this.gain_label.node.active = false;
return;
}
// 跨段位判定:滚动前后段位不一致即为升段(累计分只涨不跌,不存在降段)
const rankUp = getRankByScore(this._finalScore).tier !== getRankByScore(this._prevBestScore).tier;
this.gain_label.string = rankUp ? `+${gain} 升段!` : `+${gain}`;
this.gain_label.node.active = true;
// 浮现动效:从 0 缩放淡入
const node = this.gain_label.node;
Tween.stopAllByTarget(node);
node.setScale(0, 0, 1);
let op = node.getComponent(UIOpacity);
if (!op) {
op = node.addComponent(UIOpacity);
}
op.opacity = 0;
tween(node)
.to(0.35, { scale: v3(1, 1, 1) }, { easing: 'backOut' })
.start();
tween(op)
.to(0.35, { opacity: 255 })
.start();
}
// ======================== 结算奖励 ========================
/**
* 收集本局出场英雄的 uuid 列表(去重)。
* 来源 = 存活英雄(heroGrid 中实体的 hero_uuid)+ 死亡英雄快照(dead_heroes)。
*/
private collectPlayedHeroUuids(): number[] {
const uuidSet = new Set<number>();
// 存活英雄:heroGrid 存 eid,反查实体属性
for (const eid of smc.mission.heroGrid) {
if (eid < 0) continue;
const entity = ecs.getEntityByEid(eid);
const attrs = entity?.get(HeroAttrsComp);
if (attrs && attrs.fac === 0) {
uuidSet.add(attrs.hero_uuid);
}
}
// 死亡英雄快照
const deadList = smc.mission.dead_heroes || [];
for (const rec of deadList) {
if (rec && rec.uuid > 0) uuidSet.add(rec.uuid);
}
return [...uuidSet];
}
/**
* 从全英雄池中随机抽取 1 个英雄 uuid(优先未出场,避免与出场碎片重复)。
*/
private pickRandomHeroUuid(exclude: Set<number>): number {
const all = Object.keys(HeroInfo).map(k => Number(k)).filter(u => u > 0);
const candidates = all.filter(u => !exclude.has(u));
const pool = candidates.length > 0 ? candidates : all;
if (pool.length === 0) return 0;
return pool[Math.floor(Math.random() * pool.length)];
}
/**
* 计算并发放结算奖励,结果存入 _rewardResult。
* 通关:按 5 回合累计发放——
* 每回合基础奖励(随 monLv 增长)× 胜利回合数(wave_win_count),
* 再按全员存活回合数 × per_wave_bonus 加成。
* Why: 一关一局 5 回合后,通关奖励不再是单场,改按"5 回合累计"发放;
* scores 统计在整关间保留(data_init 仅在整关开始/重开重置),故可直接累计。
* 失败:受"坚持时间 + 击杀比例"限制——
* - 坚持 < 15s 或本回合一只怪未杀 → 无任何奖励;
* - 达标 → 坚持奖励(固定)+ 击杀比例奖励(基数 × 本回合击杀比例),
* 银币/钻石/碎片同样受此门槛限制(不达标不发,达标正常发放)。
* @param isVictory 是否胜利
*/
private grantRewards(isVictory: boolean) {
const cfg = VICTORY_REWARD_SET;
// 失败奖励资格判定:坚持时间与击杀数不达标则整场无奖励
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;
}
// 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. 金币 / 天赋经验
let gold: number;
let talentExp: number;
if (isVictory) {
// 通关:每回合基础 × 胜利回合数 × 全员存活加成
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) * winWaves * aliveBonus);
} 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;
}
// 1.5 银币 / 钻石:通关按胜利回合数累计;失败不达标清零
const silverBase = !isVictory && !defeatQualified ? 0 : cfg.silver;
const silver = isVictory ? Math.floor(silverBase * winWaves) : silverBase;
let diamond = 0;
if (isVictory) {
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));
}
// 2. 英雄碎片:出场英雄各保底 + 1 个随机英雄(通关 × 胜利回合数累计;失败不达标时不发)
const fragments: FragmentReward[] = [];
if (isVictory || defeatQualified) {
const playedUuids = this.collectPlayedHeroUuids();
const playedSet = new Set(playedUuids);
const fragMult = isVictory ? winWaves : 1;
for (const uuid of playedUuids) {
fragments.push({ uuid, count: cfg.played_fragment * fragMult, isPlayed: true });
}
const randomUuid = this.pickRandomHeroUuid(playedSet);
if (randomUuid > 0) {
fragments.push({ uuid: randomUuid, count: cfg.random_fragment * fragMult, isPlayed: false });
}
}
// 3. 落地发放
if (gold > 0) {
smc.updateGold(gold); // 统一入口:改 data 持久层 + 镜像 vmdata + 标脏同步
}
if (silver > 0) {
smc.updateSilver(silver);
}
if (diamond > 0) {
smc.updateDiamond(diamond);
}
if (talentExp > 0) {
smc.addTalentExp(talentExp);
}
for (const f of fragments) {
smc.addHeroFragments(f.uuid, f.count);
}
this._rewardResult = { gold, talentExp, silver, diamond, fragments };
mLogger.log(this.debugMode, 'VictoryComp', '[VictoryComp] 结算奖励发放', {
isVictory, monLv: this.monLv, winWaves, aliveWaves, aliveBonus,
gold, talentExp, silver, diamond, fragments,
defeatStats: this.defeatStats, defeatQualified, killRatio,
});
}
/**
* 渲染结算奖励条目(金币 / 天赋经验 / 英雄碎片),逐个弹跳入场。
*/
private renderRewards() {
if (!this.rewards_container || !this._rewardResult) return;
this.rewards_container.removeAllChildren();
const items: Node[] = [];
const r = this._rewardResult;
// 金币条目
if (this.reward_gold_prefab && r.gold > 0) {
const node = instantiate(this.reward_gold_prefab);
node.getComponent(RewardItemComp)?.setReward(r.gold);
items.push(node);
}
// 天赋经验条目
if (this.reward_exp_prefab && r.talentExp > 0) {
const node = instantiate(this.reward_exp_prefab);
node.getComponent(RewardItemComp)?.setReward(r.talentExp);
items.push(node);
}
// 银币条目
if (this.reward_silver_prefab && r.silver > 0) {
const node = instantiate(this.reward_silver_prefab);
node.getComponent(RewardItemComp)?.setReward(r.silver);
items.push(node);
}
// 钻石条目
if (this.reward_diamond_prefab && r.diamond > 0) {
const node = instantiate(this.reward_diamond_prefab);
node.getComponent(RewardItemComp)?.setReward(r.diamond);
items.push(node);
}
// 英雄碎片条目(复用 clips.prefab + ChipComp,num 子节点显示数量)
if (this.clip_prefab) {
for (const f of r.fragments) {
const node = instantiate(this.clip_prefab);
node.getComponent(ChipComp)?.setHeroByUuid(f.uuid);
const numLabel = node.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${f.count}`;
numLabel.node.active = true;
}
items.push(node);
}
}
// 逐个入场(初始隐藏,间隔弹出)
items.forEach((node, i) => {
node.active = false;
this.rewards_container.addChild(node);
this.scheduleOnce(() => {
node.active = true;
// 碎片条目没有 RewardItemComp,手动播一次弹跳
if (!node.getComponent(RewardItemComp)) {
Tween.stopAllByTarget(node);
node.setScale(0, 0, 1);
tween(node).to(0.35, { scale: v3(1, 1, 1) }, { easing: 'backOut' }).start();
}
}, REWARD_ENTER_INTERVAL * (i + 1));
});
}
// ======================== 亮点成就 ========================
/**
* 根据当前局数据匹配并生成对应的亮点标签(成就)
*/
private renderHighlights() {
if (!this.highlights_container || !this.highlight_prefab) return;
// 先清空原有的标签
this.highlights_container.removeAllChildren();
const s = smc.vmdata.scores;
// 获取所有已达成的亮点(包含对应等级的信息)
const achievedList = s.achieved_highlights || [];
// 最多显示前3个亮点(如有优先级需求,可在截取前对 achievedList 进行排序)
const displayTags = achievedList.slice(0, 3);
displayTags.forEach(item => {
const tagNode = instantiate(this.highlight_prefab);
const lab = tagNode.getComponent(Label) || tagNode.getChildByName("label")?.getComponent(Label);
if (lab) {
const typeConfig = HighlightSet[item.type];
const levelConfig = item.config;
const uuid = levelConfig.uuid;
const titleStr = lang(LangPrefix.hl_title, uuid);
const descStr = langf(LangPrefix.hl_desc, uuid, levelConfig.threshold);
lab.string = `${typeConfig.icon} ${titleStr} (${descStr})`;
}
this.highlights_container.addChild(tagNode);
});
}
// ======================== 操作入口 ========================
/**
* 统一更新按钮显隐与文案(一关一局,无节间"下一节"):
* final(通关) → btn="返回",next 隐藏
* defeat(失败) → btn="退出",next="再来一次"
*/
private updateButtonStates() {
const btns = this.node.getChildByName("btns");
if (!btns) return;
const btnNode = btns.getChildByName("btn");
const nextNode = btns.getChildByName("next");
// btn 确认/退出按钮:所有结算类型都显示
if (btnNode) {
btnNode.active = true;
const label = btnNode.getChildByName("Label")?.getComponent(Label);
if (label) {
label.string = this.settleType === "final" ? "返回" : "退出";
}
}
// next 按钮:失败→"再来一次",通关隐藏
if (nextNode) {
nextNode.active = this.settleType !== "final";
const label = nextNode.getChildByName("Label")?.getComponent(Label);
if (label) {
label.string = "再来一次";
}
}
// 属性绑定的确认按钮同步显隐
if (this.confirm_btn && this.confirm_btn.isValid) {
this.confirm_btn.active = true;
}
}
/**
* 通关标题显隐与文案:仅通关时显示("本关通关"/"本章通关")。
* 该 Label 为 @property 绑定节点,编辑器中默认隐藏。
* @param title 文案,空串时隐藏
*/
private updateSettleTitle(title: string) {
if (this.settle_title && this.settle_title.isValid) {
this.settle_title.string = title;
this.settle_title.node.active = !!title;
}
}
/**
* next 按钮点击分发:失败场 → 派发 RestartMission 重开本关
* (一关一局:通关场 next 隐藏,无"下一节")
*/
on_next_btn_click() {
oops.message.dispatchEvent(GameEvent.RestartMission);
}
/**
* 确认按钮点击(代码绑定,btns/btn):
* 通关 / 失败统一 → 清理数据 → 派发 MissionEnd → 关闭结算弹窗
*
* Why: 一关一局,存档只在 MissionComp 通关时推进到下一关(section 恒为 1),
* 此处不再做 section 推进,直接结束本局返回。
*/
victory_end() {
this.clear_data()
oops.message.dispatchEvent(GameEvent.MissionEnd)
oops.gui.removeByNode(this.node)
}
/** 清理运行时数据:解除暂停标志 */
clear_data() {
smc.mission.pause = false
}
/** 看广告双倍奖励(预留) */
watch_ad() {
return true
}
/** 双倍奖励发放(预留) */
double_reward() {
}
/** 再来一次(已迁移至 MissionComp.restartCurrentMission,保留空实现兼容旧引用) */
restart() {
oops.message.dispatchEvent(GameEvent.RestartMission);
}
/** 物品展示回调(预留) */
item_show(e: any, val: any) {
mLogger.log(this.debugMode, 'VictoryComp', "item_show", val)
}
protected onDestroy(): void {
// 停止所有动画与延迟回调,防止节点销毁后 tween/scheduleOnce 访问无效对象
if (this.rank_icon) Tween.stopAllByTarget(this.rank_icon.node);
if (this.gain_label) Tween.stopAllByTarget(this.gain_label.node);
this.unscheduleAllCallbacks();
super.onDestroy();
mLogger.log(this.debugMode, 'VictoryComp', "释放胜利界面");
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy()
}
}