using System.Collections.Generic; using System.Text; using Unity.Netcode; using UnityEngine; using Colosseum.Enemy; using Colosseum.Passives; using Colosseum.Skills; namespace Colosseum.Combat { /// /// 전투 밸런싱 검증을 위한 런타임 계측기입니다. /// 플레이어/보스 기준으로 대미지, 치유, 보호막, 위협, 패턴 사용량을 누적합니다. /// public static class CombatBalanceTracker { private sealed class ActorMetrics { public string label; public string passivePresetName; public float totalDamageDealt; public float bossDamageDealt; public float damageTaken; public float healDone; public float healReceived; public float shieldApplied; public float shieldReceived; public float threatGenerated; public readonly Dictionary damageBySkill = new Dictionary(); public readonly Dictionary healBySkill = new Dictionary(); public readonly Dictionary shieldBySkill = new Dictionary(); public readonly Dictionary threatBySkill = new Dictionary(); } private static readonly Dictionary actorMetrics = new Dictionary(); private static readonly Dictionary bossPatternCounts = new Dictionary(); private static readonly Dictionary bossEventCounts = new Dictionary(); private static float combatStartTime = -1f; private static float lastEventTime = -1f; /// /// 누적된 전투 계측 데이터를 초기화합니다. /// public static void Reset() { actorMetrics.Clear(); bossPatternCounts.Clear(); bossEventCounts.Clear(); combatStartTime = -1f; lastEventTime = -1f; } /// /// 실제 적용된 대미지를 기록합니다. /// public static void RecordDamage(GameObject source, GameObject target, float actualDamage) { if (actualDamage <= 0f || target == null) return; MarkCombatEvent(); ActorMetrics targetMetrics = GetMetrics(target); targetMetrics.damageTaken += actualDamage; if (source == null) return; ActorMetrics sourceMetrics = GetMetrics(source); sourceMetrics.totalDamageDealt += actualDamage; if (target.GetComponent() != null || target.GetComponentInParent() != null) { sourceMetrics.bossDamageDealt += actualDamage; } AddSkillValue(sourceMetrics.damageBySkill, ResolveSkillLabel(source), actualDamage); } /// /// 실제 적용된 회복량을 기록합니다. /// public static void RecordHeal(GameObject source, GameObject target, float actualHeal) { if (actualHeal <= 0f || target == null) return; MarkCombatEvent(); ActorMetrics targetMetrics = GetMetrics(target); targetMetrics.healReceived += actualHeal; if (source == null) return; ActorMetrics sourceMetrics = GetMetrics(source); sourceMetrics.healDone += actualHeal; AddSkillValue(sourceMetrics.healBySkill, ResolveSkillLabel(source), actualHeal); } /// /// 실제 적용된 보호막 수치를 기록합니다. /// public static void RecordShield(GameObject source, GameObject target, float actualShield) { if (actualShield <= 0f || target == null) return; MarkCombatEvent(); ActorMetrics targetMetrics = GetMetrics(target); targetMetrics.shieldReceived += actualShield; if (source == null) return; ActorMetrics sourceMetrics = GetMetrics(source); sourceMetrics.shieldApplied += actualShield; AddSkillValue(sourceMetrics.shieldBySkill, ResolveSkillLabel(source), actualShield); } /// /// 실제 적용된 위협 증가량을 기록합니다. /// public static void RecordThreat(GameObject source, float threatAmount) { if (threatAmount <= 0f || source == null) return; MarkCombatEvent(); ActorMetrics sourceMetrics = GetMetrics(source); sourceMetrics.threatGenerated += threatAmount; AddSkillValue(sourceMetrics.threatBySkill, ResolveSkillLabel(source), threatAmount); } /// /// 보스 패턴 사용 횟수를 기록합니다. /// public static void RecordBossPattern(string patternName) { if (string.IsNullOrWhiteSpace(patternName)) return; MarkCombatEvent(); AddCount(bossPatternCounts, patternName); } /// /// 시그니처 성공/실패 같은 보스 전투 이벤트를 기록합니다. /// public static void RecordBossEvent(string eventName) { if (string.IsNullOrWhiteSpace(eventName)) return; MarkCombatEvent(); AddCount(bossEventCounts, eventName); } /// /// 현재 누적 계측 데이터를 보기 좋은 문자열로 반환합니다. /// public static string BuildSummary() { StringBuilder builder = new StringBuilder(); builder.Append("[Balance] 전투 요약"); if (combatStartTime >= 0f && lastEventTime >= combatStartTime) { builder.Append(" | Duration="); builder.Append((lastEventTime - combatStartTime).ToString("0.00")); builder.Append("s"); } if (bossPatternCounts.Count > 0) { builder.AppendLine(); builder.Append("보스 패턴: "); AppendCountSummary(builder, bossPatternCounts); } if (bossEventCounts.Count > 0) { builder.AppendLine(); builder.Append("보스 이벤트: "); AppendCountSummary(builder, bossEventCounts); } foreach (KeyValuePair pair in actorMetrics) { ActorMetrics metrics = pair.Value; builder.AppendLine(); builder.Append("- "); builder.Append(metrics.label); if (!string.IsNullOrWhiteSpace(metrics.passivePresetName)) { builder.Append(" [Passive="); builder.Append(metrics.passivePresetName); builder.Append(']'); } builder.Append(" | BossDmg="); builder.Append(metrics.bossDamageDealt.ToString("0.##")); builder.Append(" | TotalDmg="); builder.Append(metrics.totalDamageDealt.ToString("0.##")); builder.Append(" | Taken="); builder.Append(metrics.damageTaken.ToString("0.##")); builder.Append(" | Heal="); builder.Append(metrics.healDone.ToString("0.##")); builder.Append(" | HealRecv="); builder.Append(metrics.healReceived.ToString("0.##")); builder.Append(" | Shield="); builder.Append(metrics.shieldApplied.ToString("0.##")); builder.Append(" | ShieldRecv="); builder.Append(metrics.shieldReceived.ToString("0.##")); builder.Append(" | Threat="); builder.Append(metrics.threatGenerated.ToString("0.##")); AppendSkillBreakdown(builder, "DmgBySkill", metrics.damageBySkill); AppendSkillBreakdown(builder, "HealBySkill", metrics.healBySkill); AppendSkillBreakdown(builder, "ShieldBySkill", metrics.shieldBySkill); AppendSkillBreakdown(builder, "ThreatBySkill", metrics.threatBySkill); } return builder.ToString(); } private static void MarkCombatEvent() { if (!Application.isPlaying) return; float now = Time.time; if (combatStartTime < 0f) combatStartTime = now; lastEventTime = now; } private static ActorMetrics GetMetrics(GameObject actor) { string actorLabel = ResolveActorLabel(actor); if (!actorMetrics.TryGetValue(actorLabel, out ActorMetrics metrics)) { metrics = new ActorMetrics { label = actorLabel, }; actorMetrics.Add(actorLabel, metrics); } metrics.passivePresetName = PassiveRuntimeModifierUtility.GetCurrentPresetName(actor); return metrics; } private static string ResolveActorLabel(GameObject actor) { if (actor == null) return "Unknown"; NetworkObject networkObject = actor.GetComponent() ?? actor.GetComponentInParent(); if (networkObject != null) { string roleLabel = actor.GetComponent() != null || actor.GetComponentInParent() != null ? "Boss" : "Actor"; return $"{roleLabel}:{actor.name}(Owner={networkObject.OwnerClientId})"; } return actor.name; } private static string ResolveSkillLabel(GameObject source) { if (source == null) return "Unknown"; SkillController skillController = source.GetComponent() ?? source.GetComponentInParent(); if (skillController != null && skillController.CurrentSkill != null) return skillController.CurrentSkill.SkillName; return "Unknown"; } private static void AddSkillValue(Dictionary dictionary, string key, float value) { if (dictionary == null || string.IsNullOrWhiteSpace(key) || value <= 0f) return; dictionary.TryGetValue(key, out float currentValue); dictionary[key] = currentValue + value; } private static void AddCount(Dictionary dictionary, string key) { dictionary.TryGetValue(key, out int currentValue); dictionary[key] = currentValue + 1; } private static void AppendCountSummary(StringBuilder builder, Dictionary dictionary) { bool first = true; foreach (KeyValuePair pair in dictionary) { if (!first) builder.Append(" | "); builder.Append(pair.Key); builder.Append('='); builder.Append(pair.Value); first = false; } } private static void AppendSkillBreakdown(StringBuilder builder, string label, Dictionary dictionary) { if (dictionary == null || dictionary.Count == 0) return; builder.Append(" | "); builder.Append(label); builder.Append('='); bool first = true; foreach (KeyValuePair pair in dictionary) { if (!first) builder.Append(", "); builder.Append(pair.Key); builder.Append(':'); builder.Append(pair.Value.ToString("0.##")); first = false; } } } }