Files
pixelheros/assets/script/game/map/VictoryComp.ts
panFD 4b0c0f00f9 feat(mission): 添加结算页再来一次功能
1. 新增GameEvent.RestartMission事件用于重开本关
2. 重构结算弹窗按钮逻辑,适配失败关卡重试需求
3. 新增restartCurrentMission方法实现关卡重置逻辑
4. 调整胜利prefab的缩放和控件绑定
2026-09-10 23:48:13 +08:00

876 lines
32 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;
/** 结算类型:section=小节胜利, victory=末关通关, defeat=失败 */
private settleType: string = "defeat";
/** 是否胜利场(驱动主按钮文案与行为) */
private isVictory: boolean = false;
/** 加载时隐藏 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 结算类型:section=小节胜利(下一节按钮),其余=确认退出
* @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.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.vmdata.scores.score。
* 同时刷新 smc.data.score 历史最高分。
*
* @param isVictory 是否胜利(胜利 +10 分,失败不加分)
* @param allAlive 是否全英雄存活获胜(额外 +5 分)
*/
private calculateTotalScore(isVictory: boolean, allAlive: boolean) {
const s = smc.vmdata.scores;
// 0. 胜负分:胜利 +10,全英雄存活获胜额外 +5,失败不加分
let score_result = 0;
if (isVictory) {
score_result = 10;
if (allAlive) {
score_result += 5;
}
}
// 1. 战绩分:衡量生存能力——活几回合、赢几场。
s.score_combat = (s.wave_win_count * 100)
- (s.wave_remain_monsters * 15)
+ (s.wave_all_alive_count * 50)
+ (s.passed_wave_20 ? 500 : 0);
// 2. 输出分:衡量伤害能力——团队火力如何。
s.score_output = Math.floor(s.total_dmg / 100) * 10
+ (s.crt_count * 5)
// 风怒机制已停用: 风怒次数不再计入输出分
// + (s.wf_count * 5)
+ (s.highest_dmg * 2);
// 3. 防御分:衡量生存能力——团队多能扛。
s.score_defense = (s.shield_block_count * 5)
+ Math.floor(s.heal_total / 50) * 10
+ (s.dead_trigger_count * 8);
// 4. 构建分:衡量阵容构建质量——团队配合程度。
s.score_build = 0; // 待完善
// 5. 效率分:衡量资源利用——金币花得值不值。
const goldRatio = s.gold_earned > 0 ? (s.gold_spent / s.gold_earned) : 0;
const refreshRatio = s.refresh_count > 0 ? (s.refresh_hit_count / s.refresh_count) : 0;
s.score_efficiency = Math.floor(goldRatio * 100) + Math.floor(refreshRatio * 50);
// 6. 亮点成就额外加分 (按等级叠加)
const achieved = this.getAchievedHighlights(s);
s.achieved_highlights = achieved; // 记录已达成的亮点信息
let highlightBonus = 0;
for (const item of achieved) {
highlightBonus += item.config.scoreBonus;
}
// 取整并存储当前局分数
s.score = Math.floor(score_result + s.score_combat + s.score_output + s.score_defense + s.score_build + s.score_efficiency + highlightBonus);
// 记录滚动前的历史最高分(动画起始值)
this._prevBestScore = smc.data.score;
this._finalScore = s.score;
// 判定是否打破历史最高分记录
let isNewRecord = false;
if (s.score > smc.data.score) {
smc.data.score = s.score;
isNewRecord = true;
// 更新云端/本地存储,保存新记录
if (typeof smc.updateCloudData === "function") {
smc.updateCloudData();
}
}
// 借用 scores 对象传递新记录标记,供 UI 渲染使用
(s as any).isNewRecord = isNewRecord;
mLogger.log(this.debugMode, 'VictoryComp', `[VictoryComp] 结算总分: ${s.score}`, {
combat: s.score_combat,
output: s.score_output,
defense: s.score_defense,
build: s.score_build,
efficiency: s.score_efficiency,
highlightBonus: highlightBonus,
achievedHighlights: achieved,
isNewRecord: isNewRecord,
prevBest: 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 isNewRecord = (s as any).isNewRecord === true;
const gain = this._finalScore - this._prevBestScore;
if (!isNewRecord || gain <= 0) {
this.gain_label.node.active = false;
return;
}
this.gain_label.string = `+${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。
* @param isVictory 是否胜利(胜利整体 ×1.2)
*/
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);
// 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));
// 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 });
}
// 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, wave, gold, talentExp, silver, diamond, fragments,
});
}
/**
* 渲染结算奖励条目(金币 / 天赋经验 / 英雄碎片),逐个弹跳入场。
*/
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);
});
}
// ======================== 操作入口 ========================
/**
* 统一更新按钮显隐与文案:
* section(小节胜利)→ next="下一节",btn 隐藏
* victory(末关通关)→ 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 = this.settleType !== "section";
const label = btnNode.getChildByName("Label")?.getComponent(Label);
if (label) label.string = "确认";
}
// next 按钮:小节胜利→"下一节",失败→"再来一次",末关通关隐藏
if (nextNode) {
nextNode.active = this.settleType !== "victory";
const label = nextNode.getChildByName("Label")?.getComponent(Label);
if (label) {
label.string = this.settleType === "section" ? "下一节" : "再来一次";
}
}
// 属性绑定的确认按钮同步显隐
if (this.confirm_btn && this.confirm_btn.isValid) {
this.confirm_btn.active = this.settleType !== "section";
}
}
/**
* 通关标题显隐与文案:仅末关通关时显示("本关通关"/"本章通关")。
* 该 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 按钮点击分发:
* 小节胜利 → 派发 NextSection 推进下一节
* 失败场 → 派发 RestartMission 重开本关
*/
on_next_btn_click() {
if (this.settleType === "section") {
oops.message.dispatchEvent(GameEvent.NextSection);
} else {
oops.message.dispatchEvent(GameEvent.RestartMission);
}
}
/**
* 确认按钮点击(代码绑定,btns/btn):
* 清理数据 → 派发 MissionEnd → 关闭结算弹窗
*/
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()
}
}