using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using Colosseum.Combat.Simulation;
using Colosseum.Passives;
using Colosseum.Skills;
namespace Colosseum.Editor
{
///
/// 허수아비 계산 시뮬레이터의 전체 조합 배치 실행 창입니다.
///
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("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 warnings = new List();
List skillPool = combinationSpec.CombineSkills
? LoadAssetsInFolder(skillSearchFolder)
: new List();
List gemPool = combinationSpec.CombineGems
? LoadAssetsInFolder(gemSearchFolder)
: new List();
List passivePool = combinationSpec.CombinePassives
? LoadAssetsInFolder(passiveNodeSearchFolder)
: new List();
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 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 LoadAssetsInFolder(string folderPath) where T : UnityEngine.Object
{
List assets = new List();
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(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);
}
}
}