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(' ', '_'); } } }