Files
pixelheros/assets/script/game/map/VictoryComp.ts
walkpan 6281c0f1b2 feat(结算): 添加最高分记录判定与UI显示
在胜利结算时,增加最高分记录判定逻辑。当当前局分数超过历史最高分时,更新存储并标记为新记录。同时,在总分UI旁显示"new"标识以提示玩家打破了记录。
2026-04-26 09:05:50 +08:00

607 lines
22 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 { 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);
// 判定是否打破历史最高分记录
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
});
}
/**
* 渲染得分条与亮点标签
* 依赖各维度对应的UI节点需要在Cocos Creator中拖入绑定
*/
private renderScores() {
const s = smc.vmdata.scores;
// 渲染总分
if (this.total_score_label) {
this.total_score_label.string = `${s.score}`;
// 判定是否是新记录,如果是则激活 new 节点
const isNewRecord = (s as any).isNewRecord === true;
const newNode = this.total_score_label.node.getChildByName("new");
if (newNode) {
newNode.active = isNewRecord;
}
}
// 通用渲染单个维度的函数
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()
}
}