Files
Colosseum/Assets/_Game/Scripts/Combat/CombatBalanceTracker.cs
dal4segno 8d1e97d01a feat: 패시브 트리 프로토타입 구현
- 패시브 트리/노드/프리셋 데이터와 카탈로그 참조 구조를 추가하고 Resources 의존을 Data/Passives 자산 구조로 정리
- 플레이어 런타임, 전투 계수, 프리셋 적용, 멀티플레이 동기화 경로에 패시브 적용 로직 연결
- 프리팹 기반 패시브 트리 UI와 노드 아이콘/프리셋/상세 패널 흐름을 추가하고 HUD에 연동
- 패시브 디버그/부트스트랩 메뉴와 UI 프리팹 재생성 경로를 추가
2026-03-26 22:59:39 +09:00

343 lines
12 KiB
C#

using System.Collections.Generic;
using System.Text;
using Unity.Netcode;
using UnityEngine;
using Colosseum.Enemy;
using Colosseum.Passives;
using Colosseum.Skills;
namespace Colosseum.Combat
{
/// <summary>
/// 전투 밸런싱 검증을 위한 런타임 계측기입니다.
/// 플레이어/보스 기준으로 대미지, 치유, 보호막, 위협, 패턴 사용량을 누적합니다.
/// </summary>
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<string, float> damageBySkill = new Dictionary<string, float>();
public readonly Dictionary<string, float> healBySkill = new Dictionary<string, float>();
public readonly Dictionary<string, float> shieldBySkill = new Dictionary<string, float>();
public readonly Dictionary<string, float> threatBySkill = new Dictionary<string, float>();
}
private static readonly Dictionary<string, ActorMetrics> actorMetrics = new Dictionary<string, ActorMetrics>();
private static readonly Dictionary<string, int> bossPatternCounts = new Dictionary<string, int>();
private static readonly Dictionary<string, int> bossEventCounts = new Dictionary<string, int>();
private static float combatStartTime = -1f;
private static float lastEventTime = -1f;
/// <summary>
/// 누적된 전투 계측 데이터를 초기화합니다.
/// </summary>
public static void Reset()
{
actorMetrics.Clear();
bossPatternCounts.Clear();
bossEventCounts.Clear();
combatStartTime = -1f;
lastEventTime = -1f;
}
/// <summary>
/// 실제 적용된 대미지를 기록합니다.
/// </summary>
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<BossEnemy>() != null || target.GetComponentInParent<BossEnemy>() != null)
{
sourceMetrics.bossDamageDealt += actualDamage;
}
AddSkillValue(sourceMetrics.damageBySkill, ResolveSkillLabel(source), actualDamage);
}
/// <summary>
/// 실제 적용된 회복량을 기록합니다.
/// </summary>
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);
}
/// <summary>
/// 실제 적용된 보호막 수치를 기록합니다.
/// </summary>
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);
}
/// <summary>
/// 실제 적용된 위협 증가량을 기록합니다.
/// </summary>
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);
}
/// <summary>
/// 보스 패턴 사용 횟수를 기록합니다.
/// </summary>
public static void RecordBossPattern(string patternName)
{
if (string.IsNullOrWhiteSpace(patternName))
return;
MarkCombatEvent();
AddCount(bossPatternCounts, patternName);
}
/// <summary>
/// 시그니처 성공/실패 같은 보스 전투 이벤트를 기록합니다.
/// </summary>
public static void RecordBossEvent(string eventName)
{
if (string.IsNullOrWhiteSpace(eventName))
return;
MarkCombatEvent();
AddCount(bossEventCounts, eventName);
}
/// <summary>
/// 현재 누적 계측 데이터를 보기 좋은 문자열로 반환합니다.
/// </summary>
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<string, ActorMetrics> 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<NetworkObject>() ?? actor.GetComponentInParent<NetworkObject>();
if (networkObject != null)
{
string roleLabel = actor.GetComponent<BossEnemy>() != null || actor.GetComponentInParent<BossEnemy>() != 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<SkillController>() ?? source.GetComponentInParent<SkillController>();
if (skillController != null && skillController.CurrentSkill != null)
return skillController.CurrentSkill.SkillName;
return "Unknown";
}
private static void AddSkillValue(Dictionary<string, float> 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<string, int> dictionary, string key)
{
dictionary.TryGetValue(key, out int currentValue);
dictionary[key] = currentValue + 1;
}
private static void AppendCountSummary(StringBuilder builder, Dictionary<string, int> dictionary)
{
bool first = true;
foreach (KeyValuePair<string, int> 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<string, float> dictionary)
{
if (dictionary == null || dictionary.Count == 0)
return;
builder.Append(" | ");
builder.Append(label);
builder.Append('=');
bool first = true;
foreach (KeyValuePair<string, float> pair in dictionary)
{
if (!first)
builder.Append(", ");
builder.Append(pair.Key);
builder.Append(':');
builder.Append(pair.Value.ToString("0.##"));
first = false;
}
}
}
}