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); } } }