Files
pixelheros/assets/script/game/map/VictoryComp.ts
walkpan 7427419670 fix: 修复合成规则默认值和进度条组件类型错误
- 将 MissionCardComp 中的合成规则默认值改为从 FightSet 配置读取,避免硬编码
- 修复 VictoryComp 中进度条组件类型错误,将 Sprite 改为 ProgressBar 并更新属性设置
2026-04-25 23:57:38 +08:00

543 lines
20 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 视图层)
*
* 职责:
* 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 —— 全局战斗统计数据
* - ScoreWeightsScoreSet—— 得分权重配置
* - 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 { 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");
});
}
// ======================== 得分计算 ========================
/**
* 计算单局总分并更新到 smc.vmdata.scores.score。
*
* 得分维度:
* 1. 战斗行为分 —— 暴击次数、连击触发、闪避、格挡、眩晕、冻结
* 2. 伤害转化分 —— 总伤害、平均伤害、反伤、暴击伤害
* 3. 击杀得分 —— 近战击杀、远程击杀、精英击杀、Boss 击杀
* 4. 生存得分 —— 治疗量、吸血量
* 5. 资源得分 —— 经验获取、金币获取
*/
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);
// 取整并存储
s.score = Math.floor(s.score_combat + s.score_output + s.score_defense + s.score_build + s.score_efficiency);
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
});
}
/**
* 渲染得分条与亮点标签
* 依赖各维度对应的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 tags: { name: string, desc: string }[] = [];
// 按照 scoring-system.md 设计的触发条件判定
if (s.crt_count >= 40) tags.push({ name: "🔥 暴击大师", desc: `暴击${s.crt_count}` });
if (s.dead_trigger_count >= 25) tags.push({ name: "💀 送死达人", desc: `死亡触发${s.dead_trigger_count}` });
if (s.shield_block_count >= 30) tags.push({ name: "🛡️ 铁壁铜墙", desc: `格挡${s.shield_block_count}` });
if (s.wf_count >= 20) tags.push({ name: "⚡ 风暴之王", desc: `风怒${s.wf_count}` });
if (s.highest_dmg >= 200) tags.push({ name: "🎯 一击必杀", desc: `单次伤害${Math.floor(s.highest_dmg)}` });
if (s.heal_total >= 500) tags.push({ name: "💊 治愈之光", desc: `治疗总量${Math.floor(s.heal_total)}` });
// 🏆 完美通关通关且20回合全员存活
if (s.passed_wave_20 && s.wave_all_alive_count >= 20) {
tags.push({ name: "🏆 完美通关", desc: "20回合全胜" });
}
// 效率类判定
const refreshRatio = s.refresh_count > 0 ? (s.refresh_hit_count / s.refresh_count) : 0;
if (refreshRatio >= 0.8) {
tags.push({ name: "🎲 欧皇附体", desc: `刷新命中率${Math.floor(refreshRatio * 100)}%` });
}
const goldRatio = s.gold_earned > 0 ? (s.gold_spent / s.gold_earned) : 0;
if (goldRatio >= 0.95) {
tags.push({ name: "💰 节俭玩家", desc: `金币使用率${Math.floor(goldRatio * 100)}%` });
}
// 最多显示前3个亮点
const displayTags = tags.slice(0, 3);
displayTags.forEach(tag => {
const tagNode = instantiate(this.highlight_prefab);
// 尝试获取自身或者名为 "label" 的子节点上的 Label 组件
const lab = tagNode.getComponent(Label) || tagNode.getChildByName("label")?.getComponent(Label);
if (lab) {
lab.string = `${tag.name} (${tag.desc})`;
}
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()
}
}