feat(评分系统): 实现多维度游戏评分统计与结算

- 扩展 GameScoreStats 数据结构,新增战绩、输出、防御、构建和效率五个维度的统计字段
- 在战斗、治疗、购卡、刷新等关键节点实时采集评分数据
- 实现评分数据重置机制,确保每局数据独立
- 重构总分计算逻辑,采用五维加权评分模型
- 新增初始金币收入统计,完善资源利用效率评估
This commit is contained in:
walkpan
2026-04-25 21:52:59 +08:00
parent 83d5792b48
commit b588fd06a0
9 changed files with 220 additions and 28 deletions

View File

@@ -92,16 +92,35 @@ export class SingletonModuleComp extends ecs.Comp {
// 生存统计 // 生存统计
heal_total: 0, // 治疗总量 heal_total: 0, // 治疗总量
lifesteal_total: 0, // 吸血总量 lifesteal_total: 0, // 吸血总量
shield_block_count: 0,
dead_trigger_count: 0,
// 资源统计 // 资源统计
exp_total: 0, // 经验总数 exp_total: 0, // 经验总数
gold_total: 0, // 金币总数 gold_total: 0, // 金币总数
gold_earned: 0,
gold_spent: 0,
refresh_count: 0,
refresh_hit_count: 0,
// 击杀统计 // 击杀统计
melee_kill_count: 0, // 近战怪击杀数量 melee_kill_count: 0, // 近战怪击杀数量
remote_kill_count: 0, // 远程怪击杀数量 remote_kill_count: 0, // 远程怪击杀数量
elite_kill_count: 0, // 精英怪击杀数量 elite_kill_count: 0, // 精英怪击杀数量
boss_kill_count: 0, // Boss击杀数 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, } as GameScoreStats,
gold: 0, // 金币数据MVVM绑定字段 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; // 治疗总量 heal_total: number; // 治疗总量
lifesteal_total: number;// 吸血总量 lifesteal_total: number;// 吸血总量
shield_block_count: number; // 格挡次数
dead_trigger_count: number; // 死亡触发次数
// 资源统计 // 资源统计
exp_total: number; // 经验总数 exp_total: number; // 经验总数
gold_total: number; // 金币总数 gold_total: number; // 金币总数
gold_earned: number; // 总收入金币
gold_spent: number; // 消耗金币
refresh_count: number; // 刷新次数
refresh_hit_count: number; // 刷新命中次数(刷新后选中卡)
// 击杀统计 // 击杀统计
melee_kill_count: number; // 近战怪击杀数量 melee_kill_count: number; // 近战怪击杀数量
remote_kill_count: number; // 远程怪击杀数量 remote_kill_count: number; // 远程怪击杀数量
elite_kill_count: number; // 精英怪击杀数量 elite_kill_count: number; // 精英怪击杀数量
boss_kill_count: number; // Boss击杀数 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,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) { if (isCrit) {
damage = Math.floor(damage * (1 + FightSet.CRIT_DAMAGE / 100)); damage = Math.floor(damage * (1 + FightSet.CRIT_DAMAGE / 100));
reDate.isCrit=true; 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) mLogger.log(this.debugMode, 'HeroAtkSystem', " after crit",damage)
// 护盾吸收 // 护盾吸收
const shieldResult = this.absorbShield(TAttrsComp, damage); const shieldResult = this.absorbShield(TAttrsComp, damage);
damage = shieldResult.remainingDamage; damage = shieldResult.remainingDamage;
if (shieldResult.absorbedDamage > 0) {
// 【评分系统 - 防御分】统计护盾成功抵挡伤害的次数
smc.vmdata.scores.shield_block_count += shieldResult.absorbedDamage;
}
mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield",damage) mLogger.log(this.debugMode, 'HeroAtkSystem', " after shield",damage)
// 显示护盾吸收飘字 // 显示护盾吸收飘字
if (shieldResult.absorbedDamage > 0 && targetView) { if (shieldResult.absorbedDamage > 0 && targetView) {
@@ -160,6 +168,14 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
// TAttrsComp.hp -= damage; // 应用伤害到数据层 // TAttrsComp.hp -= damage; // 应用伤害到数据层
TAttrsComp.add_hp(-damage); // 使用 add_hp 以触发 dirty_hp 和 UI 更新 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 技能 // 增加受击计数并触发 atked 技能
TAttrsComp.atked_count++; TAttrsComp.atked_count++;
this.checkAndTriggerAtkedSkills(TAttrsComp, targetView); this.checkAndTriggerAtkedSkills(TAttrsComp, targetView);
@@ -301,6 +317,10 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
for (let i = 0; i < triggerCount; i++) { for (let i = 0; i < triggerCount; i++) {
TAttrsComp.dead.forEach((uuid: number) => { TAttrsComp.dead.forEach((uuid: number) => {
if (TAttrsComp.fac === FacSet.HERO) {
// 【评分系统 - 防御分】统计死亡触发技能的生效次数
smc.vmdata.scores.dead_trigger_count++;
}
oops.message.dispatchEvent(GameEvent.TriggerSkill, { oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: uuid, s_uuid: uuid,
heroAttrs: TAttrsComp, 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); const addHp = Math.floor(sAp*_cAttrsComp.ap/100);
model.add_hp(addHp); model.add_hp(addHp);
target.health(addHp); target.health(addHp);
if (_cAttrsComp.fac === FacSet.HERO) {
// 【评分系统 - 防御分】统计团队造成的总治疗量
smc.vmdata.scores.heal_total += addHp;
}
} else if (kind === SkillKind.Shield && sAp !== 0) { } else if (kind === SkillKind.Shield && sAp !== 0) {
const addShield = Math.max(0, Math.floor(sAp)); const addShield = Math.max(0, Math.floor(sAp));
model.add_shield(addShield); model.add_shield(addShield);

View File

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

View File

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

View File

@@ -445,6 +445,33 @@ export class MissionComp extends CCComp {
smc.mission.in_fight = false; smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false; smc.vmdata.mission_data.in_fight = false;
smc.mission.stop_spawn_mon = true; 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 // 触发战斗结束技能fend
this.triggerHeroBattleSkills(false); this.triggerHeroBattleSkills(false);
@@ -682,7 +709,13 @@ export class MissionComp extends CCComp {
this.heapTrendBaseMB = -1; this.heapTrendBaseMB = -1;
this.monsterCountSyncTimer = 0; this.monsterCountSyncTimer = 0;
this.lastPrepareCoinWave = 0; this.lastPrepareCoinWave = 0;
// 重置所有的战局得分数据,防止上一局的数据污染
smc.resetScores();
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins)); 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; return;
} }
smc.vmdata.mission_data.coin = Math.max(0, Math.floor((smc.vmdata.mission_data.coin ?? 0) + reward)); 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; this.lastPrepareCoinWave = wave;
oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: reward, syncOnly: true }); oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: reward, syncOnly: true });

View File

@@ -48,6 +48,32 @@ export class VictoryComp extends CCComp {
@property(Node) @property(Node)
mvp_node=null! 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; debugMode: boolean = false;
/** 奖励等级(预留) */ /** 奖励等级(预留) */
@@ -334,39 +360,41 @@ export class VictoryComp extends CCComp {
*/ */
private calculateTotalScore() { private calculateTotalScore() {
const s = smc.vmdata.scores; const s = smc.vmdata.scores;
let totalScore = 0;
// 1. 战斗行为分 // 1. 战绩分:衡量生存能力——活几回合、赢几场。
totalScore += s.crt_count * ScoreWeights.CRT_KILL; s.score_combat = (s.wave_win_count * 100)
totalScore += s.wf_count * ScoreWeights.WF_TRIGGER; - (s.wave_remain_monsters * 15)
totalScore += s.dod_count * ScoreWeights.DODGE_SUCCESS; + (s.wave_all_alive_count * 50)
totalScore += s.back_count * ScoreWeights.BACK_SUCCESS; + (s.passed_wave_20 ? 500 : 0);
totalScore += s.stun_count * ScoreWeights.STUN_SUCCESS;
totalScore += s.freeze_count * ScoreWeights.FREEZE_SUCCESS;
// 2. 伤害转化分 // 2. 输出分:衡量伤害能力——团队火力如何。
totalScore += s.total_dmg * ScoreWeights.DMG_FACTOR; s.score_output = Math.floor(s.total_dmg / 100) * 10
totalScore += s.avg_dmg * ScoreWeights.AVG_DMG_FACTOR; + (s.crt_count * 5)
totalScore += s.thorns_dmg * ScoreWeights.THORNS_DMG_FACTOR; + (s.wf_count * 5)
totalScore += s.crit_dmg_total * ScoreWeights.CRIT_DMG_FACTOR; + (s.highest_dmg * 2);
// 3. 击杀得分 // 3. 防御分:衡量生存能力——团队多能扛。
totalScore += s.melee_kill_count * ScoreWeights.KILL_MELEE; s.score_defense = (s.shield_block_count * 5)
totalScore += s.remote_kill_count * ScoreWeights.KILL_REMOTE; + Math.floor(s.heal_total / 50) * 10
totalScore += s.elite_kill_count * ScoreWeights.KILL_ELITE; + (s.dead_trigger_count * 8);
totalScore += s.boss_kill_count * ScoreWeights.KILL_BOSS;
// 4. 生存得分 // 4. 构建分:衡量阵容构建质量——团队配合程度。
totalScore += s.heal_total * ScoreWeights.HEAL_FACTOR; s.score_build = 0; // 待完善
totalScore += s.lifesteal_total * ScoreWeights.LIFESTEAL_FACTOR;
// 5. 资源得分 // 5. 效率分:衡量资源利用——金币花得值不值。
totalScore += s.exp_total * ScoreWeights.EXP_FACTOR; const goldRatio = s.gold_earned > 0 ? (s.gold_spent / s.gold_earned) : 0;
totalScore += s.gold_total * ScoreWeights.GOLD_FACTOR; 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); 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}`); 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
});
} }
// ======================== 操作入口 ======================== // ======================== 操作入口 ========================