- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가 - 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가 - DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
222 lines
8.3 KiB
C#
222 lines
8.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace Colosseum.Combat.Simulation
|
|
{
|
|
/// <summary>
|
|
/// 결과 미리보기/추출 포맷입니다.
|
|
/// </summary>
|
|
public enum SimulationReportFormat
|
|
{
|
|
DetailText,
|
|
Markdown,
|
|
Csv,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 허수아비 계산 시뮬레이터 결과를 외부 공유용 문자열로 변환합니다.
|
|
/// </summary>
|
|
public static class SimulationReportUtility
|
|
{
|
|
/// <summary>
|
|
/// 선택한 포맷으로 결과 문자열을 생성합니다.
|
|
/// </summary>
|
|
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,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 마크다운 리포트를 생성합니다.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// CSV 리포트를 생성합니다.
|
|
/// </summary>
|
|
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<SimulationSkillBreakdown> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 저장에 적합한 기본 파일 이름을 생성합니다.
|
|
/// </summary>
|
|
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(' ', '_');
|
|
}
|
|
}
|
|
}
|