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