/** * @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(); // 存活英雄: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 { 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。 * 胜利:随关卡进度增长(base + monLv × per_lv,整体 ×1.2)。 * Why: 无回合概念后 wave_win_count 每节被 resetScores 清零恒为 1, * 旧的 per_wave 增长失效,改挂怪物等级(= 大关号)随关卡推进线性增长。 * 失败:受"坚持时间 + 击杀比例"限制—— * - 坚持 < 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; } // 1. 金币 / 天赋经验 let gold: number; let talentExp: number; if (isVictory) { const mult = cfg.victory_multiplier; gold = Math.floor((cfg.gold.base + this.monLv * cfg.gold.per_lv) * mult); talentExp = Math.floor((cfg.talent_exp.base + this.monLv * cfg.talent_exp.per_lv) * 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; } // 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. 落地发放 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, 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() } }