feat: 허수아비 계산 시뮬레이터 추가

- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가
- 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가
- DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
This commit is contained in:
2026-03-28 15:07:09 +09:00
parent 29cb132d5d
commit 285da31047
30 changed files with 2909 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Colosseum.Combat.Simulation
{
/// <summary>
/// 스킬별 기여도 요약입니다.
/// </summary>
[System.Serializable]
public sealed class SimulationSkillBreakdown
{
[SerializeField] private string skillName;
[Min(0)] [SerializeField] private int castCount;
[Min(0f)] [SerializeField] private float totalDamage;
public string SkillName => skillName;
public int CastCount => castCount;
public float TotalDamage => totalDamage;
public SimulationSkillBreakdown(string skillName, int castCount, float totalDamage)
{
this.skillName = skillName ?? "Unknown";
this.castCount = Mathf.Max(0, castCount);
this.totalDamage = Mathf.Max(0f, totalDamage);
}
}
/// <summary>
/// 허수아비 계산 시뮬레이터 결과입니다.
/// </summary>
[System.Serializable]
public sealed class SimulationResult
{
[SerializeField] private string summaryLine = string.Empty;
[TextArea(8, 30)]
[SerializeField] private string detailText = string.Empty;
[SerializeField] private string buildLabel = string.Empty;
[SerializeField] private string ruleName = string.Empty;
[SerializeField] private string rotationName = string.Empty;
[Min(0f)] [SerializeField] private float durationSeconds;
[Min(0f)] [SerializeField] private float totalDamage;
[Min(0f)] [SerializeField] private float averageDps;
[Min(0f)] [SerializeField] private float totalManaUsed;
[Min(0f)] [SerializeField] private float averageManaPerSecond;
[SerializeField] private float firstCycleEndTime = -1f;
[SerializeField] private List<SimulationSkillBreakdown> skillBreakdowns = new List<SimulationSkillBreakdown>();
[SerializeField] private List<string> warnings = new List<string>();
public string SummaryLine => summaryLine;
public string DetailText => detailText;
public string BuildLabel => buildLabel;
public string RuleName => ruleName;
public string RotationName => rotationName;
public float DurationSeconds => durationSeconds;
public float TotalDamage => totalDamage;
public float AverageDps => averageDps;
public float TotalManaUsed => totalManaUsed;
public float AverageManaPerSecond => averageManaPerSecond;
public float FirstCycleEndTime => firstCycleEndTime;
public IReadOnlyList<SimulationSkillBreakdown> SkillBreakdowns => skillBreakdowns;
public IReadOnlyList<string> Warnings => warnings;
public void FinalizeResult(
string buildLabel,
string ruleName,
string rotationName,
float durationSeconds,
float totalDamage,
float totalManaUsed,
float firstCycleEndTime,
List<SimulationSkillBreakdown> breakdowns,
List<string> warnings)
{
this.buildLabel = buildLabel ?? string.Empty;
this.ruleName = ruleName ?? string.Empty;
this.rotationName = rotationName ?? string.Empty;
this.durationSeconds = Mathf.Max(0f, durationSeconds);
this.totalDamage = Mathf.Max(0f, totalDamage);
averageDps = this.durationSeconds > 0f ? this.totalDamage / this.durationSeconds : 0f;
this.totalManaUsed = Mathf.Max(0f, totalManaUsed);
averageManaPerSecond = this.durationSeconds > 0f ? this.totalManaUsed / this.durationSeconds : 0f;
this.firstCycleEndTime = firstCycleEndTime;
skillBreakdowns = breakdowns ?? new List<SimulationSkillBreakdown>();
this.warnings = warnings ?? new List<string>();
summaryLine = BuildSummaryLine();
detailText = BuildDetailText();
}
private string BuildSummaryLine()
{
StringBuilder builder = new StringBuilder();
builder.Append("[BuildSimulation] ");
builder.Append(buildLabel);
builder.Append(" | Rule=");
builder.Append(ruleName);
builder.Append(" | Rotation=");
builder.Append(rotationName);
builder.Append(" | Dmg=");
builder.Append(totalDamage.ToString("0.##"));
builder.Append(" | DPS=");
builder.Append(averageDps.ToString("0.##"));
builder.Append(" | Mana=");
builder.Append(totalManaUsed.ToString("0.##"));
builder.Append(" | Cycle=");
builder.Append(firstCycleEndTime >= 0f ? firstCycleEndTime.ToString("0.##") + "s" : "미완료");
return builder.ToString();
}
private string BuildDetailText()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine(summaryLine);
builder.Append("Duration=");
builder.Append(durationSeconds.ToString("0.##"));
builder.Append("s | ManaPerSec=");
builder.Append(averageManaPerSecond.ToString("0.##"));
builder.AppendLine();
if (skillBreakdowns.Count > 0)
{
builder.AppendLine("Skill Breakdown");
for (int i = 0; i < skillBreakdowns.Count; i++)
{
SimulationSkillBreakdown entry = skillBreakdowns[i];
builder.Append("- ");
builder.Append(entry.SkillName);
builder.Append(" | Cast=");
builder.Append(entry.CastCount);
builder.Append(" | Dmg=");
builder.Append(entry.TotalDamage.ToString("0.##"));
builder.AppendLine();
}
}
if (warnings.Count > 0)
{
builder.AppendLine("Warnings");
for (int i = 0; i < warnings.Count; i++)
{
builder.Append("- ");
builder.Append(warnings[i]);
builder.AppendLine();
}
}
return builder.ToString().TrimEnd();
}
}
}