/** * @file VictoryComp.ts * @description 战斗结算弹窗组件(UI 视图层) * * 职责: * 1. 在战斗结束时弹出,展示结算信息(得分、奖励)。 * 2. 根据传入参数判断是否可复活,切换"下一步"或"复活"按钮。 * 3. 计算单局总分并存储到 smc.vmdata.scores.score。 * 4. 提供"重新开始"和"退出"两个操作入口。 * * 关键设计: * - onAdded(args) 接收战斗结果参数(victory / rewards / game_data / can_revive)。 * - calculateTotalScore() 根据 ScoreWeights 配置加权计算各项得分。 * - restart() 和 victory_end() 通过分发 MissionEnd / MissionStart 事件驱动游戏状态切换。 * * 依赖: * - smc.vmdata.scores —— 全局战斗统计数据 * - ScoreWeights(ScoreSet)—— 得分权重配置 * - GameEvent.MissionEnd / MissionStart —— 游戏生命周期事件 */ import { _decorator, instantiate, Label ,Prefab,Node, Sprite, Animation, AnimationClip, resources, UITransform, Widget, ProgressBar } 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 { it } from "node:test"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { HeroViewComp } from "../hero/HeroViewComp"; import { FacSet } from "../common/config/GameSet"; import { Attrs } from "../common/config/HeroAttrs"; import { HeroInfo } from "../common/config/heroSet"; import { CKind } from "../common/config/CardSet"; import { ScoreWeights } from "../common/config/ScoreSet"; import { HighlightSet, HighlightType, HighlightLevel } from "../common/config/HighlightSet"; import { mLogger } from "../common/Logger"; const { ccclass, property } = _decorator; /** * VictoryComp —— 战斗结算弹窗视图组件 * * 通过 oops.gui.open(UIID.Victory, args) 打开。 * 展示战斗结果,计算总分,并提供重开 / 退出操作。 */ @ccclass('VictoryComp') @ecs.register('Victory', false) export class VictoryComp extends CCComp { @property(Node) mvp_node=null! // ======================== 结算 UI 绑定 ======================== @property({ type: Label, tooltip: "总分文本" }) total_score_label: Label = null!; @property({ type: Node, tooltip: "战绩分节点 (需包含 score_label 和 progress_bar 子节点)" }) combat_node: Node = null!; @property({ type: Node, tooltip: "输出分节点 (需包含 score_label 和 progress_bar 子节点)" }) output_node: Node = null!; @property({ type: Node, tooltip: "防御分节点 (需包含 score_label 和 progress_bar 子节点)" }) defense_node: Node = null!; @property({ type: Node, tooltip: "构建分节点 (需包含 score_label 和 progress_bar 子节点)" }) build_node: Node = null!; @property({ type: Node, tooltip: "效率分节点 (需包含 score_label 和 progress_bar 子节点)" }) efficiency_node: Node = null!; @property({ type: Node, tooltip: "亮点成就标签的容器" }) highlights_container: Node = null!; @property({ type: Prefab, tooltip: "亮点成就标签预制体" }) highlight_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 } // ======================== 复活相关 ======================== /** 是否可以复活(由 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(); // 渲染分数UI和亮点标签 this.renderScores(); // 显示MVP英雄 const mvp = this.getMVPHero(); this.renderMVPHero(mvp); } // ======================== MVP 英雄 ======================== /** * 获取战斗中最厉害的英雄(根据等级和攻击力) */ private getMVPHero(): HeroAttrsComp | null { let mvp: HeroAttrsComp | null = null; ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => { const model = entity.get(HeroAttrsComp); if (!model || model.fac !== FacSet.HERO) return; if (!mvp) { mvp = model; } else { if (model.lv > mvp.lv) { mvp = model; } else if (model.lv === mvp.lv && model.ap > mvp.ap) { mvp = model; } } }); return mvp; } /** * 渲染 MVP 英雄,逻辑参考 CardComp 长按放大的 UI 显示 */ private renderMVPHero(mvp: HeroAttrsComp | null) { if (!this.mvp_node) return; if (!mvp) { this.mvp_node.active = false; return; } this.mvp_node.active = true; const uuid = mvp.hero_uuid; const hero = HeroInfo[uuid]; if (!hero) return; const kindName = CKind[CKind.Hero]; // 节点查找 const BG_node = this.mvp_node.getChildByName("BG"); const HF_node = this.mvp_node.getChildByName("HF"); const NF_node = this.mvp_node.getChildByName("NF"); const lv_node = this.mvp_node.getChildByName("lv"); const name_node = this.mvp_node.getChildByName("name"); const ap_node = this.mvp_node.getChildByName("ap"); const hp_node = this.mvp_node.getChildByName("hp"); const oinfo_node = this.mvp_node.getChildByName("oinfo"); const icon_node = this.mvp_node.getChildByName("icon"); const hbNode = this.mvp_node.getChildByName("HB"); const cost_node = this.mvp_node.getChildByName("cost"); // ---- 背景与边框 ---- if (BG_node) { BG_node.children.forEach(child => { child.active = (child.name === kindName); }); } if (HF_node) { HF_node.active = true; // HF_node.children.forEach(child => { // child.active = (child.name === kindName); // }); } if (NF_node) { NF_node.active = false; } if (hbNode) hbNode.active = false; // ---- 卡牌等级标识 ---- const cardLvStr = `lv${mvp.pool_lv || 1}`; if (lv_node) { lv_node.children.forEach(child => { if (child.name === "light") { child.active = false; } else if (child.name === "bg") { child.active = true; } else { child.active = (child.name === cardLvStr); } }); } // ---- 调整尺寸 (模拟放大状态) ---- const isEnlarged = true; // 结算界面的卡牌默认处于放大显示状态 const uiTrans = this.mvp_node.getComponent(UITransform); if (uiTrans) { uiTrans.setContentSize(isEnlarged ? 230 : 170, isEnlarged ? 340 : 230); const widget = this.mvp_node.getComponent(Widget); if (widget) widget.updateAlignment(); } if (BG_node) { const bgTrans = BG_node.getComponent(UITransform); if (bgTrans) { bgTrans.setContentSize(isEnlarged ? 230 : 170, isEnlarged ? 340 : 230); const widget = BG_node.getComponent(Widget); if (widget) widget.updateAlignment(); } BG_node.children.forEach(child => { const childTrans = child.getComponent(UITransform); if (childTrans) { childTrans.setContentSize(isEnlarged ? 230 : 170, isEnlarged ? 340 : 230); const widget = child.getComponent(Widget); if (widget) widget.updateAlignment(); } }); } if (HF_node) { const hfTrans = HF_node.getComponent(UITransform); if (hfTrans) { hfTrans.setContentSize(isEnlarged ? 230 : 170, isEnlarged ? 340 : 230); const widget = HF_node.getComponent(Widget); if (widget) widget.updateAlignment(); } HF_node.children.forEach(child => { const childTrans = child.getComponent(UITransform); if (childTrans) { childTrans.setContentSize(isEnlarged ? 230 : 170, isEnlarged ? 340 : 230); const widget = child.getComponent(Widget); if (widget) widget.updateAlignment(); } }); } this.mvp_node.children.forEach(child => { const widget = child.getComponent(Widget); if (widget) widget.updateAlignment(); child.children.forEach(subChild => { const subWidget = subChild.getComponent(Widget); if (subWidget) subWidget.updateAlignment(); }); }); // ---- 文本信息 ---- const heroLv = mvp.lv || 1; const suffix = heroLv >= 2 ? "★".repeat(heroLv - 1) : ""; if (name_node) { const label = name_node.getComponent(Label); if (label) label.string = `${suffix}${hero.name || ""}${suffix}`; const currentPos = name_node.position; name_node.setPosition(currentPos.x, isEnlarged ? 8 : -70, currentPos.z); } if (ap_node) { ap_node.active = true; const valNode = ap_node.getChildByName("val"); if (valNode) { const label = valNode.getComponent(Label); if (label) label.string = `${Math.floor(mvp.ap)}`; } } if (hp_node) { hp_node.active = true; const valNode = hp_node.getChildByName("val"); if (valNode) { const label = valNode.getComponent(Label); if (label) label.string = `${Math.floor(mvp.hp_max)}`; } } if (oinfo_node) { oinfo_node.active = isEnlarged; const infoLabel = oinfo_node.getChildByName("info")?.getComponent(Label); if (infoLabel) infoLabel.string = `${hero.info || ""}`; } if (cost_node) { cost_node.active = false; // 结算时不显示金币费用 } // ---- 图标动画 ---- if (icon_node) { this.updateHeroAnimation(icon_node, uuid); } } private updateHeroAnimation(node: Node, uuid: number) { const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; const hero = HeroInfo[uuid]; if (!hero) return; const anim = node.getComponent(Animation) || node.addComponent(Animation); // clear animation clips const clips = anim.clips; for (let i = clips.length - 1; i >= 0; i--) { const clip = clips[i]; if (clip) anim.removeClip(clip); } const path = `game/heros/hero/${hero.path}/idle`; resources.load(path, AnimationClip, (err, clip) => { if (err || !clip) { mLogger.log(this.debugMode, "VictoryComp", `load hero animation failed ${uuid}`, err); return; } // avoid state conflict if (!this.node.isValid || !this.mvp_node || !this.mvp_node.active) return; const currentClips = anim.clips; for (let i = currentClips.length - 1; i >= 0; i--) { const c = currentClips[i]; if (c) anim.removeClip(c); } anim.addClip(clip); anim.play("idle"); }); } /** * 获取满足条件的最高等级的亮点成就 * @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 }, { 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。 */ private calculateTotalScore() { const s = smc.vmdata.scores; // 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(s.score_combat + s.score_output + s.score_defense + s.score_build + s.score_efficiency + highlightBonus); 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 }); } /** * 渲染得分条与亮点标签 * 依赖各维度对应的UI节点(需要在Cocos Creator中拖入绑定) */ private renderScores() { const s = smc.vmdata.scores; // 渲染总分 if (this.total_score_label) { this.total_score_label.string = `${s.score}`; } // 通用渲染单个维度的函数 const renderDim = (node: Node, score: number, maxScore: number) => { if (!node) return; const lab = node.getChildByName("score_label")?.getComponent(Label); if (lab) lab.string = `${score}`; const bar = node.getChildByName("progress_bar")?.getComponent(ProgressBar); if (bar) { // 根据该维度得分占“预期满分”的比例设置进度条(fillRange) bar.progress = Math.min(1, Math.max(0, score / maxScore)); } }; // TODO: 进度条的最大值可按设计期望自行调整,目前为占位预估值 renderDim(this.combat_node, s.score_combat, 3000); renderDim(this.output_node, s.score_output, 3000); renderDim(this.defense_node, s.score_defense, 1500); renderDim(this.build_node, s.score_build, 1000); renderDim(this.efficiency_node, s.score_efficiency, 200); // 渲染成就亮点标签 this.renderHighlights(); } /** * 根据当前局数据匹配并生成对应的亮点标签(成就) */ 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; // 格式化描述(替换占位符 {0} 为实际所需达成的阈值或当前值) let descStr = levelConfig.desc.replace("{0}", levelConfig.threshold.toString()); // 显示:[图标] 称号 (描述) lab.string = `${typeConfig.icon} ${levelConfig.title} (${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.message.dispatchEvent(GameEvent.MissionStart) 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 { mLogger.log(this.debugMode, 'VictoryComp', "释放胜利界面"); } /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy() } }