- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가 - 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가 - DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
425 lines
16 KiB
C#
425 lines
16 KiB
C#
using System.Collections.Generic;
|
|
|
|
using Colosseum.Passives;
|
|
using Colosseum.Skills;
|
|
|
|
namespace Colosseum.Combat.Simulation
|
|
{
|
|
/// <summary>
|
|
/// 스킬/젬/패시브 자산 풀에서 유효한 빌드 조합을 생성합니다.
|
|
/// </summary>
|
|
public static class SimulationCombinationGenerator
|
|
{
|
|
/// <summary>
|
|
/// 조합 조건에 따라 실제 시뮬레이션 입력 빌드를 생성합니다.
|
|
/// </summary>
|
|
public static List<BuildSimulationInput> GenerateBuilds(
|
|
BuildSimulationInput template,
|
|
SimulationCombinationSpec spec,
|
|
IReadOnlyList<SkillData> skillPool,
|
|
IReadOnlyList<SkillGemData> gemPool,
|
|
IReadOnlyList<PassiveNodeData> passiveNodePool,
|
|
List<string> warnings,
|
|
out bool truncated)
|
|
{
|
|
warnings ??= new List<string>();
|
|
truncated = false;
|
|
|
|
BuildSimulationInput baseTemplate = template != null ? template.CreateCopy() : new BuildSimulationInput();
|
|
List<BuildSimulationInput> results = new List<BuildSimulationInput>();
|
|
SimulationCombinationSpec safeSpec = spec ?? new SimulationCombinationSpec();
|
|
|
|
List<List<PassiveNodeData>> passiveSelections = BuildPassiveSelections(baseTemplate, safeSpec, passiveNodePool, warnings);
|
|
List<int> activeSlots = BuildActiveSlots(safeSpec);
|
|
Dictionary<SkillData, List<SkillLoadoutEntry>> loadoutCache = new Dictionary<SkillData, List<SkillLoadoutEntry>>();
|
|
SkillLoadoutEntry[] workingSlots = new SkillLoadoutEntry[7];
|
|
|
|
for (int i = 0; i < workingSlots.Length; i++)
|
|
{
|
|
IReadOnlyList<SkillLoadoutEntry> templateSlots = baseTemplate.ResolveLoadoutEntries();
|
|
if (i < templateSlots.Count && templateSlots[i] != null)
|
|
workingSlots[i] = templateSlots[i].CreateCopy();
|
|
}
|
|
|
|
int buildIndex = 0;
|
|
for (int passiveIndex = 0; passiveIndex < passiveSelections.Count; passiveIndex++)
|
|
{
|
|
if (results.Count >= safeSpec.MaxBuildCount)
|
|
{
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
List<PassiveNodeData> passiveSelection = passiveSelections[passiveIndex];
|
|
GenerateSkillAssignments(
|
|
baseTemplate,
|
|
safeSpec,
|
|
skillPool,
|
|
gemPool,
|
|
activeSlots,
|
|
loadoutCache,
|
|
workingSlots,
|
|
0,
|
|
new HashSet<SkillData>(),
|
|
passiveSelection,
|
|
results,
|
|
warnings,
|
|
ref buildIndex,
|
|
ref truncated);
|
|
}
|
|
|
|
if (truncated)
|
|
warnings.Add($"조합 생성이 상한에 도달해 중단되었습니다. MaxBuildCount={safeSpec.MaxBuildCount}");
|
|
|
|
return results;
|
|
}
|
|
|
|
private static void GenerateSkillAssignments(
|
|
BuildSimulationInput template,
|
|
SimulationCombinationSpec spec,
|
|
IReadOnlyList<SkillData> skillPool,
|
|
IReadOnlyList<SkillGemData> gemPool,
|
|
IReadOnlyList<int> activeSlots,
|
|
Dictionary<SkillData, List<SkillLoadoutEntry>> loadoutCache,
|
|
SkillLoadoutEntry[] workingSlots,
|
|
int slotCursor,
|
|
HashSet<SkillData> usedSkills,
|
|
IReadOnlyList<PassiveNodeData> passiveSelection,
|
|
List<BuildSimulationInput> results,
|
|
List<string> warnings,
|
|
ref int buildIndex,
|
|
ref bool truncated)
|
|
{
|
|
if (truncated || results.Count >= spec.MaxBuildCount)
|
|
{
|
|
truncated = true;
|
|
return;
|
|
}
|
|
|
|
if (slotCursor >= activeSlots.Count)
|
|
{
|
|
BuildSimulationInput variant = template.CreateCopy();
|
|
variant.SetDirectSkillSlots(workingSlots);
|
|
variant.SetDirectPassiveSelection(spec.CombinePassives ? spec.PassiveTree : template.ResolvePassiveTree(), passiveSelection);
|
|
variant.SetBuildName($"{spec.BatchName}_{buildIndex + 1:D4}");
|
|
results.Add(variant);
|
|
buildIndex++;
|
|
return;
|
|
}
|
|
|
|
int slotIndex = activeSlots[slotCursor];
|
|
if (!spec.CombineSkills)
|
|
{
|
|
GenerateFromTemplateSlot(
|
|
template,
|
|
spec,
|
|
gemPool,
|
|
activeSlots,
|
|
loadoutCache,
|
|
workingSlots,
|
|
slotCursor,
|
|
passiveSelection,
|
|
results,
|
|
warnings,
|
|
ref buildIndex,
|
|
ref truncated);
|
|
return;
|
|
}
|
|
|
|
if (skillPool == null || skillPool.Count == 0)
|
|
{
|
|
warnings.Add("스킬 풀에 자산이 없어 조합 생성을 진행하지 못했습니다.");
|
|
truncated = true;
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < skillPool.Count; i++)
|
|
{
|
|
SkillData skill = skillPool[i];
|
|
if (skill == null)
|
|
continue;
|
|
|
|
if (!spec.AllowDuplicateSkills && usedSkills.Contains(skill))
|
|
continue;
|
|
|
|
List<SkillLoadoutEntry> variants = GetLoadoutVariants(skill, spec, gemPool, loadoutCache);
|
|
for (int variantIndex = 0; variantIndex < variants.Count; variantIndex++)
|
|
{
|
|
workingSlots[slotIndex] = variants[variantIndex].CreateCopy();
|
|
|
|
bool added = false;
|
|
if (!spec.AllowDuplicateSkills)
|
|
{
|
|
added = usedSkills.Add(skill);
|
|
}
|
|
|
|
GenerateSkillAssignments(
|
|
template,
|
|
spec,
|
|
skillPool,
|
|
gemPool,
|
|
activeSlots,
|
|
loadoutCache,
|
|
workingSlots,
|
|
slotCursor + 1,
|
|
usedSkills,
|
|
passiveSelection,
|
|
results,
|
|
warnings,
|
|
ref buildIndex,
|
|
ref truncated);
|
|
|
|
if (added)
|
|
usedSkills.Remove(skill);
|
|
|
|
if (truncated)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void GenerateFromTemplateSlot(
|
|
BuildSimulationInput template,
|
|
SimulationCombinationSpec spec,
|
|
IReadOnlyList<SkillGemData> gemPool,
|
|
IReadOnlyList<int> activeSlots,
|
|
Dictionary<SkillData, List<SkillLoadoutEntry>> loadoutCache,
|
|
SkillLoadoutEntry[] workingSlots,
|
|
int slotCursor,
|
|
IReadOnlyList<PassiveNodeData> passiveSelection,
|
|
List<BuildSimulationInput> results,
|
|
List<string> warnings,
|
|
ref int buildIndex,
|
|
ref bool truncated)
|
|
{
|
|
int slotIndex = activeSlots[slotCursor];
|
|
IReadOnlyList<SkillLoadoutEntry> templateSlots = template.ResolveLoadoutEntries();
|
|
SkillLoadoutEntry templateEntry = slotIndex < templateSlots.Count ? templateSlots[slotIndex] : null;
|
|
SkillData skill = templateEntry != null ? templateEntry.BaseSkill : null;
|
|
if (skill == null)
|
|
{
|
|
warnings.Add($"템플릿 슬롯 {slotIndex}에 기반 스킬이 없어 배치 생성이 중단되었습니다.");
|
|
truncated = true;
|
|
return;
|
|
}
|
|
|
|
List<SkillLoadoutEntry> variants = spec.CombineGems
|
|
? GetLoadoutVariants(skill, spec, gemPool, loadoutCache)
|
|
: new List<SkillLoadoutEntry> { templateEntry.CreateCopy() };
|
|
|
|
for (int i = 0; i < variants.Count; i++)
|
|
{
|
|
workingSlots[slotIndex] = variants[i].CreateCopy();
|
|
GenerateSkillAssignments(
|
|
template,
|
|
spec,
|
|
null,
|
|
gemPool,
|
|
activeSlots,
|
|
loadoutCache,
|
|
workingSlots,
|
|
slotCursor + 1,
|
|
new HashSet<SkillData>(),
|
|
passiveSelection,
|
|
results,
|
|
warnings,
|
|
ref buildIndex,
|
|
ref truncated);
|
|
|
|
if (truncated)
|
|
return;
|
|
}
|
|
}
|
|
|
|
private static List<SkillLoadoutEntry> GetLoadoutVariants(
|
|
SkillData skill,
|
|
SimulationCombinationSpec spec,
|
|
IReadOnlyList<SkillGemData> gemPool,
|
|
Dictionary<SkillData, List<SkillLoadoutEntry>> loadoutCache)
|
|
{
|
|
if (loadoutCache.TryGetValue(skill, out List<SkillLoadoutEntry> cachedVariants))
|
|
return cachedVariants;
|
|
|
|
List<SkillLoadoutEntry> variants = new List<SkillLoadoutEntry>();
|
|
int gemSlotCount = skill != null ? skill.MaxGemSlotCount : 0;
|
|
|
|
if (skill == null)
|
|
return variants;
|
|
|
|
List<SkillGemData> compatibleGems = new List<SkillGemData>();
|
|
if (gemPool != null)
|
|
{
|
|
for (int i = 0; i < gemPool.Count; i++)
|
|
{
|
|
SkillGemData gem = gemPool[i];
|
|
if (gem != null && gem.CanAttachToSkill(skill))
|
|
compatibleGems.Add(gem);
|
|
}
|
|
}
|
|
|
|
if (!spec.CombineGems || gemSlotCount <= 0 || compatibleGems.Count == 0)
|
|
{
|
|
variants.Add(SkillLoadoutEntry.CreateTemporary(skill));
|
|
loadoutCache.Add(skill, variants);
|
|
return variants;
|
|
}
|
|
|
|
List<SkillGemData> selected = new List<SkillGemData>();
|
|
if (spec.IncludeEmptyGemSet)
|
|
variants.Add(SkillLoadoutEntry.CreateTemporary(skill));
|
|
|
|
CollectGemVariants(skill, compatibleGems, selected, 0, gemSlotCount, variants);
|
|
|
|
if (variants.Count == 0)
|
|
variants.Add(SkillLoadoutEntry.CreateTemporary(skill));
|
|
|
|
loadoutCache.Add(skill, variants);
|
|
return variants;
|
|
}
|
|
|
|
private static void CollectGemVariants(
|
|
SkillData skill,
|
|
IReadOnlyList<SkillGemData> compatibleGems,
|
|
List<SkillGemData> selected,
|
|
int startIndex,
|
|
int maxGemCount,
|
|
List<SkillLoadoutEntry> variants)
|
|
{
|
|
if (selected.Count > 0)
|
|
{
|
|
SkillLoadoutEntry entry = SkillLoadoutEntry.CreateTemporary(skill);
|
|
bool valid = true;
|
|
for (int i = 0; i < selected.Count; i++)
|
|
{
|
|
if (!entry.TrySetGem(i, selected[i], out _))
|
|
{
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (valid)
|
|
variants.Add(entry.CreateCopy());
|
|
}
|
|
|
|
if (selected.Count >= maxGemCount)
|
|
return;
|
|
|
|
for (int i = startIndex; i < compatibleGems.Count; i++)
|
|
{
|
|
selected.Add(compatibleGems[i]);
|
|
CollectGemVariants(skill, compatibleGems, selected, i + 1, maxGemCount, variants);
|
|
selected.RemoveAt(selected.Count - 1);
|
|
}
|
|
}
|
|
|
|
private static List<List<PassiveNodeData>> BuildPassiveSelections(
|
|
BuildSimulationInput template,
|
|
SimulationCombinationSpec spec,
|
|
IReadOnlyList<PassiveNodeData> passiveNodePool,
|
|
List<string> warnings)
|
|
{
|
|
List<List<PassiveNodeData>> selections = new List<List<PassiveNodeData>>();
|
|
|
|
if (!spec.CombinePassives)
|
|
{
|
|
List<PassiveNodeData> currentSelection = new List<PassiveNodeData>();
|
|
IReadOnlyList<PassiveNodeData> templateNodes = template.ResolveSelectedPassiveNodes();
|
|
for (int i = 0; i < templateNodes.Count; i++)
|
|
{
|
|
if (templateNodes[i] != null)
|
|
currentSelection.Add(templateNodes[i]);
|
|
}
|
|
|
|
selections.Add(currentSelection);
|
|
return selections;
|
|
}
|
|
|
|
PassiveTreeData tree = spec.PassiveTree;
|
|
if (tree == null)
|
|
{
|
|
warnings.Add("패시브 트리가 없어 패시브 조합 생성은 빈 선택만 사용합니다.");
|
|
selections.Add(new List<PassiveNodeData>());
|
|
return selections;
|
|
}
|
|
|
|
List<PassiveNodeData> candidates = new List<PassiveNodeData>();
|
|
IReadOnlyList<PassiveNodeData> sourcePool = passiveNodePool != null && passiveNodePool.Count > 0 ? passiveNodePool : tree.Nodes;
|
|
for (int i = 0; i < sourcePool.Count; i++)
|
|
{
|
|
PassiveNodeData node = sourcePool[i];
|
|
if (node != null && tree.GetNodeById(node.NodeId) != null)
|
|
candidates.Add(node);
|
|
}
|
|
|
|
List<PassiveNodeData> selected = new List<PassiveNodeData>();
|
|
if (spec.IncludeEmptyPassiveSelection)
|
|
selections.Add(new List<PassiveNodeData>());
|
|
|
|
CollectPassiveSelections(tree, spec, candidates, selected, 0, selections);
|
|
|
|
if (selections.Count == 0)
|
|
selections.Add(new List<PassiveNodeData>());
|
|
|
|
return selections;
|
|
}
|
|
|
|
private static void CollectPassiveSelections(
|
|
PassiveTreeData tree,
|
|
SimulationCombinationSpec spec,
|
|
IReadOnlyList<PassiveNodeData> candidates,
|
|
List<PassiveNodeData> selected,
|
|
int startIndex,
|
|
List<List<PassiveNodeData>> results)
|
|
{
|
|
if (spec.MaxPassiveNodeCount > 0 && selected.Count >= spec.MaxPassiveNodeCount)
|
|
return;
|
|
|
|
for (int i = startIndex; i < candidates.Count; i++)
|
|
{
|
|
PassiveNodeData node = candidates[i];
|
|
selected.Add(node);
|
|
|
|
if (TryValidatePassiveSelection(tree, selected))
|
|
{
|
|
results.Add(new List<PassiveNodeData>(selected));
|
|
CollectPassiveSelections(tree, spec, candidates, selected, i + 1, results);
|
|
}
|
|
|
|
selected.RemoveAt(selected.Count - 1);
|
|
}
|
|
}
|
|
|
|
private static bool TryValidatePassiveSelection(PassiveTreeData tree, IReadOnlyList<PassiveNodeData> selected)
|
|
{
|
|
List<string> ids = new List<string>(selected.Count);
|
|
for (int i = 0; i < selected.Count; i++)
|
|
{
|
|
if (selected[i] != null && !string.IsNullOrWhiteSpace(selected[i].NodeId))
|
|
ids.Add(selected[i].NodeId);
|
|
}
|
|
|
|
return tree.TryResolveSelection(ids, out _, out _);
|
|
}
|
|
|
|
private static List<int> BuildActiveSlots(SimulationCombinationSpec spec)
|
|
{
|
|
List<int> activeSlots = new List<int>();
|
|
int[] slotIndices = spec.ActiveSlotIndices;
|
|
for (int i = 0; i < slotIndices.Length; i++)
|
|
{
|
|
int slotIndex = slotIndices[i];
|
|
if (slotIndex < 0 || slotIndex >= 7 || activeSlots.Contains(slotIndex))
|
|
continue;
|
|
|
|
activeSlots.Add(slotIndex);
|
|
}
|
|
|
|
if (activeSlots.Count == 0)
|
|
activeSlots.Add(0);
|
|
|
|
return activeSlots;
|
|
}
|
|
}
|
|
}
|