Files
pixelheros/assets/script/game/map/VictoryComp.ts
pan ecd6be7a84 feat(battle): add silver and diamond reward system
1. 新增银币、钻石奖励配置到游戏结算配置表
2. 添加银币、钻石奖励的预制体与组件绑定
3. 实现结算逻辑:固定发放银币,按胜负区间随机发放钻石
4. 新增奖励UI展示条目,完整展示新增奖励类型
2026-09-10 15:36:43 +08:00

777 lines
28 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 } 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!;
/** 调试日志开关 */
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;
/** 加载时隐藏 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 是否可复活
*/
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.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
// 计算总分并写入历史纪录
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);
});
}
// ======================== 操作入口 ========================
/** 退出战斗:清理数据 → 触发任务结束 → 关闭弹窗 */
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()
}
}