5 Commits

Author SHA1 Message Date
walkpan
b97ea5027d feat(victory): 添加得分条和亮点标签的渲染逻辑
在 VictoryComp 中新增 renderScores 和 renderHighlights 方法,用于在结算界面展示各维度得分进度条和基于游戏数据的成就标签。同时添加了对应的 highlight.prefab 资源作为标签的 UI 模板。

- renderScores 方法渲染总分及各维度(战斗、输出、防御等)的得分条。
- renderHighlights 方法根据本局数据(如暴击次数、死亡触发次数等)匹配并生成最多3个亮点成就标签。
- 新增 highlight.prefab 作为标签的 UI 预制体,包含图标和文本。
2026-04-25 22:34:25 +08:00
walkpan
b588fd06a0 feat(评分系统): 实现多维度游戏评分统计与结算
- 扩展 GameScoreStats 数据结构,新增战绩、输出、防御、构建和效率五个维度的统计字段
- 在战斗、治疗、购卡、刷新等关键节点实时采集评分数据
- 实现评分数据重置机制,确保每局数据独立
- 重构总分计算逻辑,采用五维加权评分模型
- 新增初始金币收入统计,完善资源利用效率评估
2026-04-25 21:52:59 +08:00
walkpan
83d5792b48 docs: 添加评分系统设计草案文档
添加 Draftmaster Arena 评分系统设计草案,包含五个评分维度的详细规则、流派评分关系以及亮点成就系统。
2026-04-25 15:45:36 +08:00
walkpan
c0166f9d03 feat(结算界面): 添加MVP英雄展示功能
在战斗结算界面中,根据英雄等级和攻击力计算MVP(最厉害英雄),并渲染展示其卡牌信息。实现包括:
- 新增MVP英雄评选逻辑
- 复用卡牌放大显示UI组件
- 加载并播放英雄闲置动画
- 动态调整卡牌尺寸和布局
2026-04-25 15:40:38 +08:00
walkpan
afe6fb1bc0 feat(地图): 为VictoryComp组件添加mvp_node属性
添加mvp_node属性以支持在胜利界面显示MVP相关节点,为后续功能扩展提供基础。
2026-04-24 22:34:26 +08:00
13 changed files with 15676 additions and 7147 deletions

View File

@@ -0,0 +1,400 @@
[
{
"__type__": "cc.Prefab",
"_name": "highlight",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "highlight",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 8
}
],
"_active": true,
"_components": [
{
"__id__": 14
}
],
"_prefab": {
"__id__": 16
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "icon",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -49.62,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 32,
"height": 32
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "5fJ3wyWeRFN7kPc9OuAXmV"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@846cc",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d6OnY0P9tJC4k9RdVsxNpm"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "0eDkgu/69LuKtcD32qwUbN",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 9
},
{
"__id__": 11
}
],
"_prefab": {
"__id__": 13
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -24.655,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": {
"__id__": 10
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 50.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "99pcVUFvRDqbDRk7G8XlSo"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_string": "暴击大师",
"_horizontalAlign": 0,
"_verticalAlign": 1,
"_actualFontSize": 21,
"_fontSize": 20,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 2,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": true,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "14kI2KPGNNkr7zcLFslGP+"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "33KfRxefJMI47rEEufEkMV",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 15
},
"_contentSize": {
"__type__": "cc.Size",
"width": 100,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "0aAqXsadlAkrim8Re1CWIN"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "3aBcBjdQdI9LIyPDmuxuPb",
"targetOverrides": null
}
]

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "12df63de-cda7-4be2-8e23-ea387fbba6f5",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "highlight"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -92,16 +92,35 @@ export class SingletonModuleComp extends ecs.Comp {
// 生存统计
heal_total: 0, // 治疗总量
lifesteal_total: 0, // 吸血总量
shield_block_count: 0,
dead_trigger_count: 0,
// 资源统计
exp_total: 0, // 经验总数
gold_total: 0, // 金币总数
gold_earned: 0,
gold_spent: 0,
refresh_count: 0,
refresh_hit_count: 0,
// 击杀统计
melee_kill_count: 0, // 近战怪击杀数量
remote_kill_count: 0, // 远程怪击杀数量
elite_kill_count: 0, // 精英怪击杀数量
boss_kill_count: 0, // Boss击杀数
// 战绩统计
wave_win_count: 0,
wave_remain_monsters: 0,
wave_all_alive_count: 0,
passed_wave_20: false,
highest_dmg: 0,
score_combat: 0,
score_output: 0,
score_defense: 0,
score_build: 0,
score_efficiency: 0,
} as GameScoreStats,
gold: 0, // 金币数据MVVM绑定字段
@@ -120,6 +139,51 @@ export class SingletonModuleComp extends ecs.Comp {
}
}
/**
* 重置单局评分数据
* 在每次新战斗开始时调用,确保上一局的得分不会带入新一局
*/
resetScores() {
this.vmdata.scores = {
score: 0,
crt_count: 0,
wf_count: 0,
dod_count: 0,
back_count: 0,
stun_count: 0,
freeze_count: 0,
total_dmg: 0,
atk_count: 0,
avg_dmg: 0,
thorns_dmg: 0,
crit_dmg_total: 0,
heal_total: 0,
lifesteal_total: 0,
shield_block_count: 0,
dead_trigger_count: 0,
exp_total: 0,
gold_total: 0,
gold_earned: 0,
gold_spent: 0,
refresh_count: 0,
refresh_hit_count: 0,
melee_kill_count: 0,
remote_kill_count: 0,
elite_kill_count: 0,
boss_kill_count: 0,
wave_win_count: 0,
wave_remain_monsters: 0,
wave_all_alive_count: 0,
passed_wave_20: false,
highest_dmg: 0,
score_combat: 0,
score_output: 0,
score_defense: 0,
score_build: 0,
score_efficiency: 0,
} as GameScoreStats;
}
// ==================== 数据管理方法 ====================
/**

View File

@@ -54,16 +54,36 @@ export interface GameScoreStats {
// 生存统计
heal_total: number; // 治疗总量
lifesteal_total: number;// 吸血总量
shield_block_count: number; // 格挡次数
dead_trigger_count: number; // 死亡触发次数
// 资源统计
exp_total: number; // 经验总数
gold_total: number; // 金币总数
gold_earned: number; // 总收入金币
gold_spent: number; // 消耗金币
refresh_count: number; // 刷新次数
refresh_hit_count: number; // 刷新命中次数(刷新后选中卡)
// 击杀统计
melee_kill_count: number; // 近战怪击杀数量
remote_kill_count: number; // 远程怪击杀数量
elite_kill_count: number; // 精英怪击杀数量
boss_kill_count: number; // Boss击杀数
// 战绩统计
wave_win_count: number; // 回合胜利次数
wave_remain_monsters: number; // 累计每回合留存敌人数量
wave_all_alive_count: number; // 全员存活胜利次数
passed_wave_20: boolean; // 是否通过第20回合
highest_dmg: number; // 单次最高伤害
// 最终结算分
score_combat: number;
score_output: number;
score_defense: number;
score_build: number;
score_efficiency: number;
}

View File

@@ -0,0 +1,119 @@
# 评分系统设计 — Draftmaster Arena
*Created: 2026-04-25*
*Status: Draft*
---
## 概述
每局游戏20回合结束后或进入无限模式 失败后 计算总分从5个维度综合评估玩家表现。不同流派在不同维度各有优势没有唯一最佳刷分流派。
---
## 维度1战绩分
衡量生存能力——活几回合、赢几场。
| 事件 | 分值 |
|------|------|
| 每回合胜利 | +100分 |
| 每回合每留存1个敌人 | -15分 |
| 全员存活胜利 | +50分额外 |
| 通过第20回合 | +500分通关奖励 |
---
## 维度2输出分
衡量伤害能力——团队火力如何。
| 事件 | 分值 |
|------|------|
| 团队伤害总量 | 每100伤害 = +10分 |
| 暴击次数 | 每次 +5分 |
| 风怒次数 | 每次 +5分 |
| 单次最高伤害 | 记录值 × 2分 |
---
## 维度3防御分
衡量生存能力——团队多能扛。
| 事件 | 分值 |
|------|------|
| 格挡次数(护盾低于次数) | 每次 +5分 |
| 治疗总量 | 每50治疗 = +10分 |
| 死亡触发次数 | 每次 +8分 |
---
## 维度4构建分
衡量阵容构建质量——团队配合程度。
待完善
---
## 维度5效率分
衡量资源利用——金币花得值不值。
| 事件 | 分值 |
|------|------|
| 金币使用率 | 使用金币 / 总收入 × 100分 |
| 刷新命中率 | 刷新后选中卡 / 刷新次数 × 50分 |
---
## 评分结算信息
│ 🏆 战绩 2,350 ████ │
│ ⚔️ 输出 1,080 ███ │
│ 🛡️ 防御 520 ██ │
│ 🃏 构建 280 █ │
│ 💰 效率 50 ▏ │
│ │
│ 亮点: │
│ 🔥 暴击大师 (暴击47次) │
│ 💀 送死达人 (死亡32次) │
│ 🎯 通关奖励 (+500) │
## 流派与评分的关系
| 流派 | 高分维度 | 低分维度 | 策略 |
|------|---------|---------|------|
| 暴击流 | 输出↑↑ | 防御↓ | 火力碾压 |
| 护盾流 | 防御↑↑ | 输出↓ | 持久消耗 |
| 攻击流 | 输出↑ | 构建→ | 纯伤害堆叠 |
| 受伤流 | 防御↑ | 战绩→ | 越打越硬 |
| 风怒流 | 输出↑↑ | 防御↓ | 连击爆发 |
| 死亡流 | 防御↑ 构建↑ | 战绩↓ | 死亡也是得分 |
| 召唤流 | 构建↑↑ 防御↑ | 输出→ | 配合丰富 |
混合流派得分最均衡——暴击+死亡 = 高输出 + 高防御分。
---
## 亮点系统(成就标签)
每局结束后根据表现自动授予1-3个亮点标签
| 亮点 | 触发条件 |
|------|---------|
| 🔥 暴击大师 | 暴击次数 ≥ 40 |
| 💀 送死达人 | 死亡触发 ≥ 25次 |
| 🛡️ 铁壁铜墙 | 格挡次数 ≥ 30 |
| ⚡ 风暴之王 | 风怒次数 ≥ 20 |
| 🎯 一击必杀 | 单次伤害 ≥ 200 |
| 💊 治愈之光 | 治疗总量 ≥ 500 |
| 🃏 流派大师 | 完成3个流派构建 |
| ♻️ 合成狂人 | 合成次数 ≥ 8 |
| 🏆 完美通关 | 20回合全胜 |
| 🎲 欧皇附体 | 刷新命中率 ≥ 80% |
| 💰 节俭玩家 | 金币使用率 ≥ 95% |

View File

@@ -0,0 +1,11 @@
{
"ver": "1.0.1",
"importer": "text",
"imported": true,
"uuid": "8127fa12-7890-4bad-9dff-eeb9e1e4d6c9",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -144,12 +144,20 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
if (isCrit) {
damage = Math.floor(damage * (1 + FightSet.CRIT_DAMAGE / 100));
reDate.isCrit=true;
if (damageEvent.Attrs.fac === FacSet.HERO) {
// 【评分系统 - 输出分】统计暴击次数与暴击造成的总伤害
smc.vmdata.scores.crt_count++;
smc.vmdata.scores.crit_dmg_total += damage;
}
}
mLogger.log(this.debugMode, 'HeroAtkSystem', " after crit",damage)
// 护盾吸收
const shieldResult = this.absorbShield(TAttrsComp, damage);
damage = shieldResult.remainingDamage;
if (shieldResult.absorbedDamage > 0) {
// 【评分系统 - 防御分】统计护盾成功抵挡伤害的次数
smc.vmdata.scores.shield_block_count += shieldResult.absorbedDamage;
}
mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield",damage)
// 显示护盾吸收飘字
if (shieldResult.absorbedDamage > 0 && targetView) {
@@ -160,6 +168,14 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
// TAttrsComp.hp -= damage; // 应用伤害到数据层
TAttrsComp.add_hp(-damage); // 使用 add_hp 以触发 dirty_hp 和 UI 更新
if (damageEvent.Attrs.fac === FacSet.HERO) {
// 【评分系统 - 输出分】统计团队造成的总伤害以及单次最高伤害记录
smc.vmdata.scores.total_dmg += damage;
if (damage > smc.vmdata.scores.highest_dmg) {
smc.vmdata.scores.highest_dmg = damage;
}
}
// 增加受击计数并触发 atked 技能
TAttrsComp.atked_count++;
this.checkAndTriggerAtkedSkills(TAttrsComp, targetView);
@@ -301,6 +317,10 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
for (let i = 0; i < triggerCount; i++) {
TAttrsComp.dead.forEach((uuid: number) => {
if (TAttrsComp.fac === FacSet.HERO) {
// 【评分系统 - 防御分】统计死亡触发技能的生效次数
smc.vmdata.scores.dead_trigger_count++;
}
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: uuid,
heroAttrs: TAttrsComp,

View File

@@ -429,6 +429,10 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
const addHp = Math.floor(sAp*_cAttrsComp.ap/100);
model.add_hp(addHp);
target.health(addHp);
if (_cAttrsComp.fac === FacSet.HERO) {
// 【评分系统 - 防御分】统计团队造成的总治疗量
smc.vmdata.scores.heal_total += addHp;
}
} else if (kind === SkillKind.Shield && sAp !== 0) {
const addShield = Math.max(0, Math.floor(sAp));
model.add_shield(addShield);

View File

@@ -331,6 +331,10 @@ export class CardComp extends CCComp {
}
// 扣除金币
this.setMissionCoin(currentCoin - cardCost);
// 【评分系统 - 效率分】记录购卡消耗的金币,以及刷新后的选中卡次数(命中率分子)
smc.vmdata.scores.gold_spent += cardCost;
smc.vmdata.scores.refresh_hit_count++;
oops.message.dispatchEvent(GameEvent.CoinAdd, {
syncOnly: true,
delta: -cardCost

View File

@@ -298,6 +298,9 @@ export class MissionCardComp extends CCComp {
const v = typeof payload === 'number' ? payload : (payload?.delta ?? payload?.value ?? 0);
if (v === 0) return;
this.setMissionCoin(this.getMissionCoin() + v);
// 【评分系统 - 效率分】精确统计整局游戏的总收入与额外支出(如卖卡、技能扣费等)
if (v > 0) smc.vmdata.scores.gold_earned += v;
else smc.vmdata.scores.gold_spent -= v;
this.updateCoinAndCostUI();
this.playCoinChangeAnim(v > 0);
}
@@ -554,6 +557,9 @@ export class MissionCardComp extends CCComp {
return;
}
this.setMissionCoin(currentCoin - cost);
// 【评分系统 - 效率分】记录因刷新卡池消耗的金币,以及刷新次数
smc.vmdata.scores.gold_spent += cost;
smc.vmdata.scores.refresh_count++;
this.playCoinChangeAnim(false);
this.updateCoinAndCostUI();
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {

View File

@@ -445,6 +445,33 @@ export class MissionComp extends CCComp {
smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false;
smc.mission.stop_spawn_mon = true;
if (smc.mission.play && !smc.mission.pause) {
// 【评分系统 - 战绩分】每回合胜利加分
smc.vmdata.scores.wave_win_count++;
// 【评分系统 - 战绩分】记录每回合结束时场上留存的敌人数量(扣分项)
smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num;
let allAlive = true;
let hasHero = false;
ecs.query(this.heroAttrsMatcher).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.HERO) {
hasHero = true;
if (attrs.is_dead) allAlive = false;
}
});
// 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分)
if (hasHero && allAlive) {
smc.vmdata.scores.wave_all_alive_count++;
}
// 【评分系统 - 战绩分】判断是否通过最后一关第20回合
if (this.currentWave === 20) {
smc.vmdata.scores.passed_wave_20 = true;
}
}
// 触发战斗结束技能fend
this.triggerHeroBattleSkills(false);
@@ -682,7 +709,13 @@ export class MissionComp extends CCComp {
this.heapTrendBaseMB = -1;
this.monsterCountSyncTimer = 0;
this.lastPrepareCoinWave = 0;
// 重置所有的战局得分数据,防止上一局的数据污染
smc.resetScores();
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins));
// 【评分系统 - 效率分】记录初始获得的金币收入
smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin;
}
// ======================== 波次管理 ========================
@@ -761,6 +794,8 @@ export class MissionComp extends CCComp {
return;
}
smc.vmdata.mission_data.coin = Math.max(0, Math.floor((smc.vmdata.mission_data.coin ?? 0) + reward));
// 【评分系统 - 效率分】记录每波次发放的金币奖励收入
smc.vmdata.scores.gold_earned += reward;
this.lastPrepareCoinWave = wave;
oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: reward, syncOnly: true });

View File

@@ -18,7 +18,7 @@
* - ScoreWeightsScoreSet—— 得分权重配置
* - GameEvent.MissionEnd / MissionStart —— 游戏生命周期事件
*/
import { _decorator, instantiate, Label ,Prefab,Node} from "cc";
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";
@@ -29,6 +29,8 @@ 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";
@@ -43,6 +45,35 @@ const { ccclass, property } = _decorator;
@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;
/** 奖励等级(预留) */
@@ -88,6 +119,234 @@ export class VictoryComp extends CCComp {
// 计算总分
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");
});
}
// ======================== 得分计算 ========================
@@ -104,39 +363,126 @@ export class VictoryComp extends CCComp {
*/
private calculateTotalScore() {
const s = smc.vmdata.scores;
let totalScore = 0;
// 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);
// 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. 输出分:衡量伤害能力——团队火力如何。
s.score_output = Math.floor(s.total_dmg / 100) * 10
+ (s.crt_count * 5)
+ (s.wf_count * 5)
+ (s.highest_dmg * 2);
// 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. 防御分:衡量生存能力——团队多能扛。
s.score_defense = (s.shield_block_count * 5)
+ Math.floor(s.heal_total / 50) * 10
+ (s.dead_trigger_count * 8);
// 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. 构建分:衡量阵容构建质量——团队配合程度。
s.score_build = 0; // 待完善
// 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;
// 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(totalScore);
mLogger.log(this.debugMode, 'VictoryComp', `[VictoryComp] 结算总分: ${s.score}`);
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(Sprite);
if (bar) {
// 根据该维度得分占“预期满分”的比例设置进度条fillRange
bar.fillRange = 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);
});
}
// ======================== 操作入口 ========================