feat: 허수아비 계산 시뮬레이터 추가
- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가 - 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가 - DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
This commit is contained in:
287
Assets/_Game/Scripts/Editor/BuildSimulationBatchWindow.cs
Normal file
287
Assets/_Game/Scripts/Editor/BuildSimulationBatchWindow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user