using System;
using System.Collections.Generic;
using System.Text;
namespace Colosseum.Combat.Simulation
{
///
/// 결과 미리보기/추출 포맷입니다.
///
public enum SimulationReportFormat
{
DetailText,
Markdown,
Csv,
}
///
/// 허수아비 계산 시뮬레이터 결과를 외부 공유용 문자열로 변환합니다.
///
public static class SimulationReportUtility
{
///
/// 선택한 포맷으로 결과 문자열을 생성합니다.
///
public static string BuildReport(SimulationResult result, SimulationReportFormat format)
{
if (result == null)
return string.Empty;
return format switch
{
SimulationReportFormat.Markdown => BuildMarkdown(result),
SimulationReportFormat.Csv => BuildCsv(result),
_ => result.DetailText,
};
}
///
/// 마크다운 리포트를 생성합니다.
///
public static string BuildMarkdown(SimulationResult result)
{
if (result == null)
return string.Empty;
StringBuilder builder = new StringBuilder();
builder.Append("# 허수아비 계산 시뮬레이션 결과");
builder.AppendLine();
builder.AppendLine();
builder.Append("- Build: ");
builder.Append(result.BuildLabel);
builder.AppendLine();
builder.Append("- Rule: ");
builder.Append(result.RuleName);
builder.AppendLine();
builder.Append("- Rotation: ");
builder.Append(result.RotationName);
builder.AppendLine();
builder.AppendLine();
builder.AppendLine("| 항목 | 값 |");
builder.AppendLine("| --- | --- |");
builder.Append("| Duration | ");
builder.Append(result.DurationSeconds.ToString("0.##"));
builder.AppendLine("s |");
builder.Append("| Total Damage | ");
builder.Append(result.TotalDamage.ToString("0.##"));
builder.AppendLine(" |");
builder.Append("| DPS | ");
builder.Append(result.AverageDps.ToString("0.##"));
builder.AppendLine(" |");
builder.Append("| Total Mana | ");
builder.Append(result.TotalManaUsed.ToString("0.##"));
builder.AppendLine(" |");
builder.Append("| Mana / Sec | ");
builder.Append(result.AverageManaPerSecond.ToString("0.##"));
builder.AppendLine(" |");
builder.Append("| First Cycle End | ");
builder.Append(result.FirstCycleEndTime >= 0f ? result.FirstCycleEndTime.ToString("0.##") + "s" : "미완료");
builder.AppendLine(" |");
if (result.SkillBreakdowns.Count > 0)
{
builder.AppendLine();
builder.AppendLine("## 스킬 기여도");
builder.AppendLine();
builder.AppendLine("| 스킬 | 사용 횟수 | 누적 피해 |");
builder.AppendLine("| --- | ---: | ---: |");
for (int i = 0; i < result.SkillBreakdowns.Count; i++)
{
SimulationSkillBreakdown entry = result.SkillBreakdowns[i];
builder.Append("| ");
builder.Append(entry.SkillName);
builder.Append(" | ");
builder.Append(entry.CastCount);
builder.Append(" | ");
builder.Append(entry.TotalDamage.ToString("0.##"));
builder.AppendLine(" |");
}
}
if (result.Warnings.Count > 0)
{
builder.AppendLine();
builder.AppendLine("## 경고");
builder.AppendLine();
for (int i = 0; i < result.Warnings.Count; i++)
{
builder.Append("- ");
builder.Append(result.Warnings[i]);
builder.AppendLine();
}
}
return builder.ToString().TrimEnd();
}
///
/// CSV 리포트를 생성합니다.
///
public static string BuildCsv(SimulationResult result)
{
if (result == null)
return string.Empty;
StringBuilder builder = new StringBuilder();
builder.AppendLine("BuildLabel,RuleName,RotationName,DurationSeconds,TotalDamage,AverageDps,TotalManaUsed,AverageManaPerSecond,FirstCycleEndTime,SkillName,CastCount,SkillDamage,Warnings");
string warnings = string.Join(" / ", result.Warnings);
IReadOnlyList breakdowns = result.SkillBreakdowns;
if (breakdowns.Count == 0)
{
AppendCsvRow(builder, result, null, warnings);
return builder.ToString().TrimEnd();
}
for (int i = 0; i < breakdowns.Count; i++)
{
AppendCsvRow(builder, result, breakdowns[i], warnings);
}
return builder.ToString().TrimEnd();
}
///
/// 저장에 적합한 기본 파일 이름을 생성합니다.
///
public static string BuildDefaultFileName(SimulationResult result, SimulationReportFormat format)
{
string baseName = result != null
? $"{result.BuildLabel}_{result.RuleName}_{result.RotationName}"
: "BuildSimulation";
string extension = format == SimulationReportFormat.Csv ? "csv" : "md";
return $"{SanitizeFileName(baseName)}.{extension}";
}
private static void AppendCsvRow(
StringBuilder builder,
SimulationResult result,
SimulationSkillBreakdown breakdown,
string warnings)
{
builder.Append(EscapeCsv(result.BuildLabel));
builder.Append(',');
builder.Append(EscapeCsv(result.RuleName));
builder.Append(',');
builder.Append(EscapeCsv(result.RotationName));
builder.Append(',');
builder.Append(result.DurationSeconds.ToString("0.##"));
builder.Append(',');
builder.Append(result.TotalDamage.ToString("0.##"));
builder.Append(',');
builder.Append(result.AverageDps.ToString("0.##"));
builder.Append(',');
builder.Append(result.TotalManaUsed.ToString("0.##"));
builder.Append(',');
builder.Append(result.AverageManaPerSecond.ToString("0.##"));
builder.Append(',');
builder.Append(result.FirstCycleEndTime >= 0f ? result.FirstCycleEndTime.ToString("0.##") : string.Empty);
builder.Append(',');
builder.Append(EscapeCsv(breakdown != null ? breakdown.SkillName : string.Empty));
builder.Append(',');
builder.Append(breakdown != null ? breakdown.CastCount.ToString() : string.Empty);
builder.Append(',');
builder.Append(breakdown != null ? breakdown.TotalDamage.ToString("0.##") : string.Empty);
builder.Append(',');
builder.Append(EscapeCsv(warnings));
builder.AppendLine();
}
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
bool needsQuotes = value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r");
if (!needsQuotes)
return value;
return $"\"{value.Replace("\"", "\"\"")}\"";
}
private static string SanitizeFileName(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "BuildSimulation";
string sanitized = value;
char[] invalidChars = System.IO.Path.GetInvalidFileNameChars();
for (int i = 0; i < invalidChars.Length; i++)
{
sanitized = sanitized.Replace(invalidChars[i], '_');
}
return sanitized.Replace(' ', '_');
}
}
}