using System.Collections.Generic;
using Colosseum.Passives;
using Colosseum.Skills;
namespace Colosseum.Combat.Simulation
{
///
/// 스킬/젬/패시브 자산 풀에서 유효한 빌드 조합을 생성합니다.
///
public static class SimulationCombinationGenerator
{
///
/// 조합 조건에 따라 실제 시뮬레이션 입력 빌드를 생성합니다.
///
public static List GenerateBuilds(
BuildSimulationInput template,
SimulationCombinationSpec spec,
IReadOnlyList skillPool,
IReadOnlyList gemPool,
IReadOnlyList passiveNodePool,
List warnings,
out bool truncated)
{
warnings ??= new List();
truncated = false;
BuildSimulationInput baseTemplate = template != null ? template.CreateCopy() : new BuildSimulationInput();
List results = new List();
SimulationCombinationSpec safeSpec = spec ?? new SimulationCombinationSpec();
List> passiveSelections = BuildPassiveSelections(baseTemplate, safeSpec, passiveNodePool, warnings);
List activeSlots = BuildActiveSlots(safeSpec);
Dictionary> loadoutCache = new Dictionary>();
SkillLoadoutEntry[] workingSlots = new SkillLoadoutEntry[7];
for (int i = 0; i < workingSlots.Length; i++)
{
IReadOnlyList 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 passiveSelection = passiveSelections[passiveIndex];
GenerateSkillAssignments(
baseTemplate,
safeSpec,
skillPool,
gemPool,
activeSlots,
loadoutCache,
workingSlots,
0,
new HashSet(),
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 skillPool,
IReadOnlyList gemPool,
IReadOnlyList activeSlots,
Dictionary> loadoutCache,
SkillLoadoutEntry[] workingSlots,
int slotCursor,
HashSet usedSkills,
IReadOnlyList passiveSelection,
List results,
List 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 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 gemPool,
IReadOnlyList activeSlots,
Dictionary> loadoutCache,
SkillLoadoutEntry[] workingSlots,
int slotCursor,
IReadOnlyList passiveSelection,
List results,
List warnings,
ref int buildIndex,
ref bool truncated)
{
int slotIndex = activeSlots[slotCursor];
IReadOnlyList 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 variants = spec.CombineGems
? GetLoadoutVariants(skill, spec, gemPool, loadoutCache)
: new List { 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(),
passiveSelection,
results,
warnings,
ref buildIndex,
ref truncated);
if (truncated)
return;
}
}
private static List GetLoadoutVariants(
SkillData skill,
SimulationCombinationSpec spec,
IReadOnlyList gemPool,
Dictionary> loadoutCache)
{
if (loadoutCache.TryGetValue(skill, out List cachedVariants))
return cachedVariants;
List variants = new List();
int gemSlotCount = skill != null ? skill.MaxGemSlotCount : 0;
if (skill == null)
return variants;
List compatibleGems = new List();
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 selected = new List();
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 compatibleGems,
List selected,
int startIndex,
int maxGemCount,
List 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> BuildPassiveSelections(
BuildSimulationInput template,
SimulationCombinationSpec spec,
IReadOnlyList passiveNodePool,
List warnings)
{
List> selections = new List>();
if (!spec.CombinePassives)
{
List currentSelection = new List();
IReadOnlyList 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());
return selections;
}
List candidates = new List();
IReadOnlyList 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 selected = new List();
if (spec.IncludeEmptyPassiveSelection)
selections.Add(new List());
CollectPassiveSelections(tree, spec, candidates, selected, 0, selections);
if (selections.Count == 0)
selections.Add(new List());
return selections;
}
private static void CollectPassiveSelections(
PassiveTreeData tree,
SimulationCombinationSpec spec,
IReadOnlyList candidates,
List selected,
int startIndex,
List> 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(selected));
CollectPassiveSelections(tree, spec, candidates, selected, i + 1, results);
}
selected.RemoveAt(selected.Count - 1);
}
}
private static bool TryValidatePassiveSelection(PassiveTreeData tree, IReadOnlyList selected)
{
List ids = new List(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 BuildActiveSlots(SimulationCombinationSpec spec)
{
List activeSlots = new List();
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;
}
}
}