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,176 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using Colosseum.Combat.Simulation;
using Colosseum.Passives;
using Colosseum.Skills;
namespace Colosseum.Editor
{
/// <summary>
/// 허수아비 계산 시뮬레이터의 배치 조사 실행 메뉴입니다.
/// </summary>
public static class BuildSimulationBatchCommands
{
private const string PlayerSkillFolder = "Assets/_Game/Data/Skills";
private const string PlayerGemFolder = "Assets/_Game/Data/SkillGems";
private const string PlayerPassiveFolder = "Assets/_Game/Data/Passives/Nodes";
private const string PlayerPassiveTreePath = "Assets/_Game/Data/Passives/Data_PassiveTree_Player_Prototype.asset";
private const string ReportFolder = "BuildSimulationReports";
private static readonly HashSet<string> DisabledPlayerSkillPaths = new HashSet<string>
{
"Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
};
/// <summary>
/// 현재 기준 플레이어 단일 슬롯 데미지 전수 조사를 실행합니다.
/// </summary>
[MenuItem("Tools/Colosseum/Simulation/Run Player Damage Sweep")]
private static void RunPlayerDamageSweep()
{
PassiveTreeData passiveTree = AssetDatabase.LoadAssetAtPath<PassiveTreeData>(PlayerPassiveTreePath);
if (passiveTree == null)
{
Debug.LogError($"[BuildSimulationBatch] 패시브 트리를 찾지 못했습니다. | Path={PlayerPassiveTreePath}");
return;
}
BuildSimulationInput template = new BuildSimulationInput();
template.SetBuildName("플레이어_단일슬롯_데미지전수");
SimulationRuleSet ruleSet = new SimulationRuleSet();
ruleSet.Configure("PlayerDamageSweep10s", 10f, 1, 0f, 0f);
RotationPolicy rotationPolicy = new RotationPolicy();
rotationPolicy.Configure("Slot0Only", new[] { 0 }, false, 0, false, 5, 0f);
SimulationCombinationSpec combinationSpec = new SimulationCombinationSpec();
combinationSpec.Configure(
"PlayerDamageSweep",
combineSkills: true,
combineGems: true,
combinePassives: true,
activeSlotIndices: new[] { 0 },
allowDuplicateSkills: false,
includeEmptyGemSet: true,
passiveTree: passiveTree,
includeEmptyPassiveSelection: true,
maxPassiveNodeCount: 0,
maxBuildCount: 50000);
List<string> warnings = new List<string>();
List<SkillData> skills = LoadPlayerSkills(warnings);
List<SkillGemData> gems = LoadAssetsInFolder<SkillGemData>(PlayerGemFolder);
List<PassiveNodeData> passiveNodes = LoadAssetsInFolder<PassiveNodeData>(PlayerPassiveFolder);
List<BuildSimulationInput> builds = SimulationCombinationGenerator.GenerateBuilds(
template,
combinationSpec,
skills,
gems,
passiveNodes,
warnings,
out bool truncated);
SimulationBatchResult result = SimulationBatchRunner.Run(
combinationSpec.BatchName,
builds,
ruleSet,
rotationPolicy,
warnings,
truncated);
string reportDirectory = Path.Combine(Path.GetDirectoryName(Application.dataPath) ?? Application.dataPath, ReportFolder);
Directory.CreateDirectory(reportDirectory);
string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
string markdownPath = Path.Combine(reportDirectory, $"PlayerDamageSweep_{timestamp}.md");
string csvPath = Path.Combine(reportDirectory, $"PlayerDamageSweep_{timestamp}.csv");
File.WriteAllText(markdownPath, SimulationBatchReportUtility.BuildMarkdown(result), Encoding.UTF8);
File.WriteAllText(csvPath, SimulationBatchReportUtility.BuildCsv(result), Encoding.UTF8);
Debug.Log(BuildSummary(result, markdownPath, csvPath));
}
private static List<SkillData> LoadPlayerSkills(List<string> warnings)
{
List<SkillData> skills = LoadAssetsInFolder<SkillData>(PlayerSkillFolder);
List<SkillData> filtered = new List<SkillData>();
for (int i = 0; i < skills.Count; i++)
{
SkillData skill = skills[i];
if (skill == null || !skill.name.StartsWith("Data_Skill_Player_", System.StringComparison.Ordinal))
continue;
string assetPath = AssetDatabase.GetAssetPath(skill);
if (DisabledPlayerSkillPaths.Contains(assetPath))
{
warnings?.Add($"애니메이션 미구현 스킬 제외: {skill.SkillName}");
continue;
}
filtered.Add(skill);
}
return filtered;
}
private static List<T> LoadAssetsInFolder<T>(string folderPath) where T : Object
{
List<T> assets = new List<T>();
string[] guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}", new[] { folderPath });
for (int i = 0; i < guids.Length; i++)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
if (asset != null)
assets.Add(asset);
}
return assets;
}
private static string BuildSummary(SimulationBatchResult result, string markdownPath, string csvPath)
{
StringBuilder builder = new StringBuilder();
builder.Append("[BuildSimulationBatch] 플레이어 단일 슬롯 데미지 전수 조사 완료");
builder.Append(" | Builds=");
builder.Append(result.GeneratedBuildCount);
builder.Append(" | Truncated=");
builder.Append(result.Truncated);
builder.Append(" | Markdown=");
builder.Append(markdownPath);
builder.Append(" | CSV=");
builder.Append(csvPath);
int topCount = Mathf.Min(10, result.Entries.Count);
for (int i = 0; i < topCount; i++)
{
SimulationBatchEntry entry = result.Entries[i];
SimulationResult simulation = entry != null ? entry.Result : null;
if (simulation == null)
continue;
builder.AppendLine();
builder.Append('#');
builder.Append(i + 1);
builder.Append(' ');
builder.Append(entry.BuildLabel);
builder.Append(" | DPS=");
builder.Append(simulation.AverageDps.ToString("0.##"));
builder.Append(" | Dmg=");
builder.Append(simulation.TotalDamage.ToString("0.##"));
builder.Append(" | Mana=");
builder.Append(simulation.TotalManaUsed.ToString("0.##"));
}
return builder.ToString();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 82f1fba75c8e4f040ad82e2aa0096063

View File

@@ -0,0 +1,287 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using Colosseum.Combat.Simulation;
using Colosseum.Passives;
using Colosseum.Skills;
namespace Colosseum.Editor
{
/// <summary>
/// 허수아비 계산 시뮬레이터의 전체 조합 배치 실행 창입니다.
/// </summary>
public sealed class BuildSimulationBatchWindow : EditorWindow
{
[SerializeField] private BuildSimulationInput templateInput = new BuildSimulationInput();
[SerializeField] private SimulationRuleSet ruleSet = new SimulationRuleSet();
[SerializeField] private RotationPolicy rotationPolicy = new RotationPolicy();
[SerializeField] private SimulationCombinationSpec combinationSpec = new SimulationCombinationSpec();
[SerializeField] private string skillSearchFolder = "Assets/_Game/Data/Skills";
[SerializeField] private string gemSearchFolder = "Assets/_Game/Data/SkillGems";
[SerializeField] private string passiveNodeSearchFolder = "Assets/_Game/Data/Passives";
[SerializeField] private SimulationBatchResult lastBatchResult;
[SerializeField] private bool previewAsCsv;
private Vector2 scrollPosition;
[MenuItem("Tools/Colosseum/Simulation/Build Simulation Batch Window")]
private static void Open()
{
BuildSimulationBatchWindow window = GetWindow<BuildSimulationBatchWindow>("Build Simulation Batch");
window.minSize = new Vector2(620f, 720f);
window.Show();
}
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
DrawTemplateSection();
EditorGUILayout.Space(12f);
DrawRuleSection();
EditorGUILayout.Space(12f);
DrawRotationSection();
EditorGUILayout.Space(12f);
DrawCombinationSection();
EditorGUILayout.Space(12f);
DrawRunSection();
EditorGUILayout.Space(12f);
DrawExportSection();
EditorGUILayout.Space(12f);
DrawResultSection();
EditorGUILayout.EndScrollView();
}
private void DrawTemplateSection()
{
EditorGUILayout.LabelField("Template", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("조합 생성에서 비활성화한 축은 이 템플릿 입력을 사용합니다.", MessageType.None);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty buildProperty = serializedWindow.FindProperty(nameof(templateInput));
DrawProperty(buildProperty, "buildName");
DrawProperty(buildProperty, "strength");
DrawProperty(buildProperty, "dexterity");
DrawProperty(buildProperty, "intelligence");
DrawProperty(buildProperty, "vitality");
DrawProperty(buildProperty, "wisdom");
DrawProperty(buildProperty, "spirit");
DrawProperty(buildProperty, "weapon");
DrawProperty(buildProperty, "directSkillSlots", true);
DrawProperty(buildProperty, "passiveTree");
DrawProperty(buildProperty, "selectedPassiveNodes", true);
DrawProperty(buildProperty, "passivePreset");
DrawProperty(buildProperty, "loadoutPreset");
serializedWindow.ApplyModifiedProperties();
}
private void DrawRuleSection()
{
EditorGUILayout.LabelField("Simulation", EditorStyles.boldLabel);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty ruleProperty = serializedWindow.FindProperty(nameof(ruleSet));
DrawProperty(ruleProperty, "ruleName");
DrawProperty(ruleProperty, "durationSeconds");
DrawProperty(ruleProperty, "targetCount");
DrawProperty(ruleProperty, "movementLossSecondsPerCast");
DrawProperty(ruleProperty, "manaRegenPerSecond");
serializedWindow.ApplyModifiedProperties();
}
private void DrawRotationSection()
{
EditorGUILayout.LabelField("Rotation", EditorStyles.boldLabel);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty rotationProperty = serializedWindow.FindProperty(nameof(rotationPolicy));
DrawProperty(rotationProperty, "policyName");
DrawProperty(rotationProperty, "prioritySlots", true);
DrawProperty(rotationProperty, "useFallbackSlot");
DrawProperty(rotationProperty, "fallbackSlotIndex");
DrawProperty(rotationProperty, "delayHighPowerSkillUntilTime");
if (rotationPolicy.DelayHighPowerSkillUntilTime)
{
DrawProperty(rotationProperty, "highPowerSlotIndex");
DrawProperty(rotationProperty, "highPowerFirstUseTime");
}
serializedWindow.ApplyModifiedProperties();
}
private void DrawCombinationSection()
{
EditorGUILayout.LabelField("Combination", EditorStyles.boldLabel);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty specProperty = serializedWindow.FindProperty(nameof(combinationSpec));
DrawProperty(specProperty, "batchName");
DrawProperty(specProperty, "combineSkills");
DrawProperty(specProperty, "combineGems");
DrawProperty(specProperty, "combinePassives");
DrawProperty(specProperty, "activeSlotIndices", true);
DrawProperty(specProperty, "allowDuplicateSkills");
DrawProperty(specProperty, "includeEmptyGemSet");
DrawProperty(specProperty, "passiveTree");
DrawProperty(specProperty, "includeEmptyPassiveSelection");
DrawProperty(specProperty, "maxPassiveNodeCount");
DrawProperty(specProperty, "maxBuildCount");
skillSearchFolder = EditorGUILayout.TextField("Skill Folder", skillSearchFolder);
gemSearchFolder = EditorGUILayout.TextField("Gem Folder", gemSearchFolder);
passiveNodeSearchFolder = EditorGUILayout.TextField("Passive Folder", passiveNodeSearchFolder);
serializedWindow.ApplyModifiedProperties();
}
private void DrawRunSection()
{
EditorGUILayout.LabelField("Run", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("전수 조합은 매우 빠르게 폭증합니다. 폴더 범위를 줄이고 Max Build Count를 적절히 설정하는 편이 안전합니다.", MessageType.Warning);
if (GUILayout.Button("Run Batch Simulation", GUILayout.Height(32f)))
{
RunBatchSimulation();
}
}
private void DrawExportSection()
{
EditorGUILayout.LabelField("Export", EditorStyles.boldLabel);
if (lastBatchResult == null)
{
EditorGUILayout.HelpBox("배치 실행 후 Markdown/CSV로 복사하거나 저장할 수 있습니다.", MessageType.None);
return;
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Copy Markdown"))
EditorGUIUtility.systemCopyBuffer = SimulationBatchReportUtility.BuildMarkdown(lastBatchResult);
if (GUILayout.Button("Copy CSV"))
EditorGUIUtility.systemCopyBuffer = SimulationBatchReportUtility.BuildCsv(lastBatchResult);
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Save Markdown"))
SaveBatchReport(false);
if (GUILayout.Button("Save CSV"))
SaveBatchReport(true);
}
}
private void DrawResultSection()
{
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
if (lastBatchResult == null)
{
EditorGUILayout.HelpBox("아직 배치 결과가 없습니다.", MessageType.None);
return;
}
EditorGUILayout.LabelField("Generated", lastBatchResult.GeneratedBuildCount.ToString());
EditorGUILayout.LabelField("Truncated", lastBatchResult.Truncated ? "Yes" : "No");
previewAsCsv = EditorGUILayout.Toggle("Preview CSV", previewAsCsv);
string previewText = previewAsCsv
? SimulationBatchReportUtility.BuildCsv(lastBatchResult)
: SimulationBatchReportUtility.BuildMarkdown(lastBatchResult);
EditorGUILayout.TextArea(previewText, GUILayout.MinHeight(320f));
}
private void RunBatchSimulation()
{
List<string> warnings = new List<string>();
List<SkillData> skillPool = combinationSpec.CombineSkills
? LoadAssetsInFolder<SkillData>(skillSearchFolder)
: new List<SkillData>();
List<SkillGemData> gemPool = combinationSpec.CombineGems
? LoadAssetsInFolder<SkillGemData>(gemSearchFolder)
: new List<SkillGemData>();
List<PassiveNodeData> passivePool = combinationSpec.CombinePassives
? LoadAssetsInFolder<PassiveNodeData>(passiveNodeSearchFolder)
: new List<PassiveNodeData>();
if (combinationSpec.CombineSkills && skillPool.Count == 0)
warnings.Add($"스킬 폴더에서 SkillData를 찾지 못했습니다: {skillSearchFolder}");
if (combinationSpec.CombineGems && gemPool.Count == 0)
warnings.Add($"젬 폴더에서 SkillGemData를 찾지 못했습니다: {gemSearchFolder}");
if (combinationSpec.CombinePassives && passivePool.Count == 0 && combinationSpec.PassiveTree != null)
warnings.Add($"패시브 폴더에서 PassiveNodeData를 찾지 못했습니다: {passiveNodeSearchFolder}");
List<BuildSimulationInput> builds = SimulationCombinationGenerator.GenerateBuilds(
templateInput,
combinationSpec,
skillPool,
gemPool,
passivePool,
warnings,
out bool truncated);
lastBatchResult = SimulationBatchRunner.Run(
combinationSpec.BatchName,
builds,
ruleSet,
rotationPolicy,
warnings,
truncated);
Debug.Log($"[BuildSimulationBatch] 배치 실행 완료 | Builds={lastBatchResult.GeneratedBuildCount} | Truncated={lastBatchResult.Truncated}");
}
private void SaveBatchReport(bool csv)
{
string defaultName = SimulationBatchReportUtility.BuildDefaultFileName(lastBatchResult, csv);
string path = EditorUtility.SaveFilePanel(
"배치 시뮬레이션 결과 저장",
Application.dataPath,
defaultName,
csv ? "csv" : "md");
if (string.IsNullOrWhiteSpace(path))
return;
string contents = csv
? SimulationBatchReportUtility.BuildCsv(lastBatchResult)
: SimulationBatchReportUtility.BuildMarkdown(lastBatchResult);
File.WriteAllText(path, contents);
Debug.Log($"[BuildSimulationBatch] 결과 파일을 저장했습니다. | Path={path}");
}
private static List<T> LoadAssetsInFolder<T>(string folderPath) where T : UnityEngine.Object
{
List<T> assets = new List<T>();
if (string.IsNullOrWhiteSpace(folderPath))
return assets;
string[] guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}", new[] { folderPath });
for (int i = 0; i < guids.Length; i++)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
if (asset != null)
assets.Add(asset);
}
return assets;
}
private static void DrawProperty(SerializedProperty root, string relativePath, bool includeChildren = false)
{
SerializedProperty property = root.FindPropertyRelative(relativePath);
if (property != null)
EditorGUILayout.PropertyField(property, includeChildren);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0ee99b72045222a418924f50d0e5f4ab

View File

@@ -0,0 +1,219 @@
using System.IO;
using UnityEditor;
using UnityEngine;
using Colosseum.Combat.Simulation;
namespace Colosseum.Editor
{
/// <summary>
/// 허수아비 계산 시뮬레이터 실행 창입니다.
/// </summary>
public sealed class BuildSimulationWindow : EditorWindow
{
[SerializeField] private BuildSimulationInput buildInput = new BuildSimulationInput();
[SerializeField] private SimulationRuleSet ruleSet = new SimulationRuleSet();
[SerializeField] private RotationPolicy rotationPolicy = new RotationPolicy();
[SerializeField] private SimulationResult lastResult;
[SerializeField] private SimulationReportFormat previewFormat = SimulationReportFormat.DetailText;
private Vector2 scrollPosition;
[MenuItem("Tools/Colosseum/Simulation/Build Simulation Window")]
private static void Open()
{
BuildSimulationWindow window = GetWindow<BuildSimulationWindow>("Build Simulation");
window.minSize = new Vector2(520f, 640f);
window.Show();
}
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
DrawBuildSection();
EditorGUILayout.Space(12f);
DrawRuleSection();
EditorGUILayout.Space(12f);
DrawRotationSection();
EditorGUILayout.Space(12f);
DrawRunSection();
EditorGUILayout.Space(12f);
DrawExportSection();
EditorGUILayout.Space(12f);
DrawResultSection();
EditorGUILayout.EndScrollView();
}
private void DrawBuildSection()
{
EditorGUILayout.LabelField("Build", EditorStyles.boldLabel);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty buildProperty = serializedWindow.FindProperty(nameof(buildInput));
DrawProperty(buildProperty, "buildName");
DrawProperty(buildProperty, "strength");
DrawProperty(buildProperty, "dexterity");
DrawProperty(buildProperty, "intelligence");
DrawProperty(buildProperty, "vitality");
DrawProperty(buildProperty, "wisdom");
DrawProperty(buildProperty, "spirit");
DrawProperty(buildProperty, "weapon");
DrawProperty(buildProperty, "directSkillSlots", true);
DrawProperty(buildProperty, "passiveTree");
DrawProperty(buildProperty, "selectedPassiveNodes", true);
DrawProperty(buildProperty, "passivePreset");
DrawProperty(buildProperty, "loadoutPreset");
serializedWindow.ApplyModifiedProperties();
}
private void DrawRuleSection()
{
EditorGUILayout.LabelField("Simulation", EditorStyles.boldLabel);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty ruleProperty = serializedWindow.FindProperty(nameof(ruleSet));
DrawProperty(ruleProperty, "ruleName");
DrawProperty(ruleProperty, "durationSeconds");
DrawProperty(ruleProperty, "targetCount");
DrawProperty(ruleProperty, "movementLossSecondsPerCast");
DrawProperty(ruleProperty, "manaRegenPerSecond");
serializedWindow.ApplyModifiedProperties();
}
private void DrawRotationSection()
{
EditorGUILayout.LabelField("Rotation", EditorStyles.boldLabel);
SerializedObject serializedWindow = new SerializedObject(this);
SerializedProperty rotationProperty = serializedWindow.FindProperty(nameof(rotationPolicy));
DrawProperty(rotationProperty, "policyName");
DrawProperty(rotationProperty, "prioritySlots", true);
DrawProperty(rotationProperty, "useFallbackSlot");
DrawProperty(rotationProperty, "fallbackSlotIndex");
DrawProperty(rotationProperty, "delayHighPowerSkillUntilTime");
if (rotationPolicy.DelayHighPowerSkillUntilTime)
{
DrawProperty(rotationProperty, "highPowerSlotIndex");
DrawProperty(rotationProperty, "highPowerFirstUseTime");
}
serializedWindow.ApplyModifiedProperties();
}
private void DrawRunSection()
{
EditorGUILayout.LabelField("Run", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("MVP 범위는 단일 대상 피해 계산입니다. 회복/보호막/위협과 버프성 효과는 이후 단계에서 확장합니다.", MessageType.Info);
using (new EditorGUI.DisabledScope(buildInput == null || buildInput.LoadoutPreset == null))
{
if (GUILayout.Button("Run Simulation", GUILayout.Height(32f)))
{
lastResult = BuildSimulationEngine.Run(buildInput, ruleSet, rotationPolicy);
if (lastResult != null && !string.IsNullOrWhiteSpace(lastResult.DetailText))
{
Debug.Log(lastResult.DetailText);
}
}
}
}
private void DrawResultSection()
{
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
if (lastResult == null)
{
EditorGUILayout.HelpBox("아직 실행 결과가 없습니다.", MessageType.None);
return;
}
EditorGUILayout.SelectableLabel(lastResult.SummaryLine, EditorStyles.textField, GUILayout.Height(36f));
previewFormat = (SimulationReportFormat)EditorGUILayout.EnumPopup("Format", previewFormat);
string previewText = SimulationReportUtility.BuildReport(lastResult, previewFormat);
EditorGUILayout.TextArea(previewText, GUILayout.MinHeight(260f));
}
private static void DrawProperty(SerializedProperty root, string relativePath, bool includeChildren = false)
{
SerializedProperty property = root.FindPropertyRelative(relativePath);
if (property != null)
{
EditorGUILayout.PropertyField(property, includeChildren);
}
}
private void DrawExportSection()
{
EditorGUILayout.LabelField("Export", EditorStyles.boldLabel);
if (lastResult == null)
{
EditorGUILayout.HelpBox("실행 결과가 생기면 복사와 저장 버튼이 활성화됩니다.", MessageType.None);
return;
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Copy Detail"))
{
CopyReport(SimulationReportFormat.DetailText);
}
if (GUILayout.Button("Copy Markdown"))
{
CopyReport(SimulationReportFormat.Markdown);
}
if (GUILayout.Button("Copy CSV"))
{
CopyReport(SimulationReportFormat.Csv);
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Save Markdown"))
{
SaveReport(SimulationReportFormat.Markdown);
}
if (GUILayout.Button("Save CSV"))
{
SaveReport(SimulationReportFormat.Csv);
}
}
}
private void CopyReport(SimulationReportFormat format)
{
string report = SimulationReportUtility.BuildReport(lastResult, format);
EditorGUIUtility.systemCopyBuffer = report;
Debug.Log($"[BuildSimulation] 결과를 클립보드에 복사했습니다. | Format={format}");
}
private void SaveReport(SimulationReportFormat format)
{
string extension = format == SimulationReportFormat.Csv ? "csv" : "md";
string defaultFileName = SimulationReportUtility.BuildDefaultFileName(lastResult, format);
string path = EditorUtility.SaveFilePanel(
"시뮬레이션 결과 저장",
Application.dataPath,
defaultFileName,
extension);
if (string.IsNullOrWhiteSpace(path))
return;
string report = SimulationReportUtility.BuildReport(lastResult, format);
File.WriteAllText(path, report);
Debug.Log($"[BuildSimulation] 결과 파일을 저장했습니다. | Path={path}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fc6991eb0ed991a439747282181ff086