/** * @file VictoryComp.ts * @description 战斗结算弹窗组件(UI 视图层) * * 通过 oops.gui.open(UIID.Victory, args) 打开。 * 展示段位徽章、总分滚动动画、段位进度与亮点成就,并提供重开 / 退出操作。 */ import { _decorator, instantiate, Label, Prefab, Node, Sprite, ProgressBar, Tween, tween, v3, UIOpacity } 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: 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; /** * 结算类型(由 MissionComp 传入,驱动主按钮行为): * section - 小节胜利(主按钮"下一节",派发 NextSection) * 其余 - 末关通关 / 失败(主按钮确认退出,走原结算退出流程) */ private settleType: string = "defeat"; /** 加载时隐藏 loading 遮罩 */ protected onLoad(): void { this.node.getChildByName("loading").active = false } /** * 弹窗打开时的回调:接收战斗结果参数。 * * @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.node.getChildByName("btns").getChildByName("next").active = !args.can_revive // 下一节按钮:仅小节胜利时显示 this.updateNextButtonState(); // 通关标题:仅末关通关时显示 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(); // 存活英雄: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。 * @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); }); } // ======================== 操作入口 ======================== /** * 下一节按钮显隐:仅小节胜利(settleType === "section")时显示。 * 该按钮为 @property 绑定节点,编辑器中已设计好样式与位置。 */ private updateNextButtonState() { if (this.next_section_btn && this.next_section_btn.isValid) { this.next_section_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; } } /** * 下一节按钮点击(编辑器 Click Events 绑定): * 派发 NextSection,由 MissionComp 关窗并推进下一小节。 */ on_next_section() { oops.message.dispatchEvent(GameEvent.NextSection); } /** * 主按钮点击(编辑器 Click Events 绑定,btns/next): * 确认结算退出(原有退出流程)。 * 注:下一节按钮为独立节点,不经过本方法。 */ 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() { } /** * 重新开始: * 1. 清理数据。 * 2. 触发 MissionEnd 事件重置状态。 * 3. 显示 loading 遮罩,延迟 0.5 秒后触发 MissionStart。 * 4. 关闭弹窗。 */ restart() { this.clear_data() oops.message.dispatchEvent(GameEvent.MissionEnd) this.node.getChildByName("loading").active = true this.scheduleOnce(() => { oops.gui.open(UIID.Mission) this.node.getChildByName("loading").active = false oops.gui.removeByNode(this.node) }, 0.5) } /** 物品展示回调(预留) */ 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() } }