Files
pixelheros/assets/script/game/map/VictoryComp.ts
walkpan c0166f9d03 feat(结算界面): 添加MVP英雄展示功能
在战斗结算界面中,根据英雄等级和攻击力计算MVP(最厉害英雄),并渲染展示其卡牌信息。实现包括:
- 新增MVP英雄评选逻辑
- 复用卡牌放大显示UI组件
- 加载并播放英雄闲置动画
- 动态调整卡牌尺寸和布局
2026-04-25 15:40:38 +08:00

427 lines
15 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 } 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!
/** 调试日志开关 */
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();
// 显示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;
let totalScore = 0;
// 1. 战斗行为分
totalScore += s.crt_count * ScoreWeights.CRT_KILL;
totalScore += s.wf_count * ScoreWeights.WF_TRIGGER;
totalScore += s.dod_count * ScoreWeights.DODGE_SUCCESS;
totalScore += s.back_count * ScoreWeights.BACK_SUCCESS;
totalScore += s.stun_count * ScoreWeights.STUN_SUCCESS;
totalScore += s.freeze_count * ScoreWeights.FREEZE_SUCCESS;
// 2. 伤害转化分
totalScore += s.total_dmg * ScoreWeights.DMG_FACTOR;
totalScore += s.avg_dmg * ScoreWeights.AVG_DMG_FACTOR;
totalScore += s.thorns_dmg * ScoreWeights.THORNS_DMG_FACTOR;
totalScore += s.crit_dmg_total * ScoreWeights.CRIT_DMG_FACTOR;
// 3. 击杀得分
totalScore += s.melee_kill_count * ScoreWeights.KILL_MELEE;
totalScore += s.remote_kill_count * ScoreWeights.KILL_REMOTE;
totalScore += s.elite_kill_count * ScoreWeights.KILL_ELITE;
totalScore += s.boss_kill_count * ScoreWeights.KILL_BOSS;
// 4. 生存得分
totalScore += s.heal_total * ScoreWeights.HEAL_FACTOR;
totalScore += s.lifesteal_total * ScoreWeights.LIFESTEAL_FACTOR;
// 5. 资源得分
totalScore += s.exp_total * ScoreWeights.EXP_FACTOR;
totalScore += s.gold_total * ScoreWeights.GOLD_FACTOR;
// 取整并存储
s.score = Math.floor(totalScore);
mLogger.log(this.debugMode, 'VictoryComp', `[VictoryComp] 结算总分: ${s.score}`);
}
// ======================== 操作入口 ========================
/** 退出战斗:清理数据 → 触发任务结束 → 关闭弹窗 */
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()
}
}