using System.Collections.Generic; using System.Text; using UnityEngine; using Colosseum.Passives; using Colosseum.Skills; using Colosseum.Weapons; namespace Colosseum.Combat.Simulation { /// /// 허수아비 계산 시뮬레이터가 사용할 완성형 빌드 입력값입니다. /// 프리셋 없이 직접 구성한 빌드와 기존 프리셋 기반 입력을 모두 지원합니다. /// [System.Serializable] public class BuildSimulationInput { private const int DefaultSlotCount = 7; [Header("Label")] [SerializeField] private string buildName = "새 빌드"; [Header("Stats")] [Min(0f)] [SerializeField] private float strength = 10f; [Min(0f)] [SerializeField] private float dexterity = 10f; [Min(0f)] [SerializeField] private float intelligence = 10f; [Min(0f)] [SerializeField] private float vitality = 10f; [Min(0f)] [SerializeField] private float wisdom = 10f; [Min(0f)] [SerializeField] private float spirit = 10f; [Header("Build Assets")] [SerializeField] private WeaponData weapon; [Header("Direct Build")] [Tooltip("직접 구성한 스킬/젬 슬롯입니다. 비어 있으면 로드아웃 프리셋을 사용합니다.")] [SerializeField] private SkillLoadoutEntry[] directSkillSlots = new SkillLoadoutEntry[DefaultSlotCount]; [Tooltip("직접 구성한 패시브 트리입니다. 비어 있으면 패시브 프리셋의 트리를 사용합니다.")] [SerializeField] private PassiveTreeData passiveTree; [Tooltip("직접 선택한 패시브 노드 목록입니다. 비어 있으면 패시브 프리셋의 선택 노드를 사용합니다.")] [SerializeField] private List selectedPassiveNodes = new List(); [Header("Preset Fallback")] [SerializeField] private PassivePresetData passivePreset; [SerializeField] private PlayerLoadoutPreset loadoutPreset; public string BuildName => string.IsNullOrWhiteSpace(buildName) ? "새 빌드" : buildName.Trim(); public float Strength => strength; public float Dexterity => dexterity; public float Intelligence => intelligence; public float Vitality => vitality; public float Wisdom => wisdom; public float Spirit => spirit; public WeaponData Weapon => weapon; public PassiveTreeData PassiveTree => passiveTree; public IReadOnlyList SelectedPassiveNodes => selectedPassiveNodes; public PassivePresetData PassivePreset => passivePreset; public PlayerLoadoutPreset LoadoutPreset => loadoutPreset; /// /// 입력 상태를 읽기 쉬운 라벨로 구성합니다. /// public string BuildLabel { get { string passiveName = ResolvePassiveLabel(); string loadoutName = ResolveLoadoutLabel(); return $"{BuildName} | Passive={passiveName} | Loadout={loadoutName}"; } } /// /// 직접 구성 슬롯과 프리셋을 합쳐 실제 계산에 사용할 슬롯 배열을 반환합니다. /// public IReadOnlyList ResolveLoadoutEntries() { EnsureDirectSlotCapacity(); if (HasDirectSkillSetup()) return directSkillSlots; return loadoutPreset != null ? loadoutPreset.Slots : System.Array.Empty(); } /// /// 실제 계산 가능한 기반 스킬이 하나 이상 있는지 반환합니다. /// public bool HasAnyResolvedSkill() { IReadOnlyList entries = ResolveLoadoutEntries(); for (int i = 0; i < entries.Count; i++) { if (entries[i] != null && entries[i].BaseSkill != null) return true; } return false; } /// /// 실제 계산에 사용할 패시브 트리를 반환합니다. /// public PassiveTreeData ResolvePassiveTree() { if (passiveTree != null) return passiveTree; return passivePreset != null ? passivePreset.Tree : null; } /// /// 실제 계산에 사용할 패시브 노드 ID 목록을 반환합니다. /// public List BuildSelectedPassiveNodeIdList() { List nodeIds = new List(); IReadOnlyList resolvedNodes = ResolveSelectedPassiveNodes(); for (int i = 0; i < resolvedNodes.Count; i++) { PassiveNodeData node = resolvedNodes[i]; if (node == null || string.IsNullOrWhiteSpace(node.NodeId)) continue; nodeIds.Add(node.NodeId); } return nodeIds; } /// /// 실제 계산에 사용할 패시브 노드 목록을 반환합니다. /// public IReadOnlyList ResolveSelectedPassiveNodes() { if (selectedPassiveNodes != null && selectedPassiveNodes.Count > 0) return selectedPassiveNodes; return passivePreset != null ? passivePreset.SelectedNodes : System.Array.Empty(); } /// /// 직접 구성 슬롯을 덮어씁니다. /// public void SetDirectSkillSlots(IReadOnlyList entries) { EnsureDirectSlotCapacity(); for (int i = 0; i < directSkillSlots.Length; i++) { directSkillSlots[i] = null; } if (entries == null) return; int copyCount = Mathf.Min(entries.Count, directSkillSlots.Length); for (int i = 0; i < copyCount; i++) { SkillLoadoutEntry entry = entries[i]; directSkillSlots[i] = entry != null ? entry.CreateCopy() : null; } } /// /// 직접 구성 패시브를 덮어씁니다. /// public void SetDirectPassiveSelection(PassiveTreeData tree, IReadOnlyList nodes) { passiveTree = tree; selectedPassiveNodes.Clear(); if (nodes == null) return; for (int i = 0; i < nodes.Count; i++) { if (nodes[i] != null) selectedPassiveNodes.Add(nodes[i]); } } /// /// 빌드 이름을 갱신합니다. /// public void SetBuildName(string value) { buildName = value ?? string.Empty; } /// /// 현재 입력값의 복사본을 생성합니다. /// public BuildSimulationInput CreateCopy() { EnsureDirectSlotCapacity(); BuildSimulationInput copy = new BuildSimulationInput { buildName = buildName, strength = strength, dexterity = dexterity, intelligence = intelligence, vitality = vitality, wisdom = wisdom, spirit = spirit, weapon = weapon, passiveTree = passiveTree, passivePreset = passivePreset, loadoutPreset = loadoutPreset, }; copy.directSkillSlots = new SkillLoadoutEntry[directSkillSlots.Length]; for (int i = 0; i < directSkillSlots.Length; i++) { copy.directSkillSlots[i] = directSkillSlots[i] != null ? directSkillSlots[i].CreateCopy() : null; } copy.selectedPassiveNodes = new List(selectedPassiveNodes.Count); for (int i = 0; i < selectedPassiveNodes.Count; i++) { if (selectedPassiveNodes[i] != null) copy.selectedPassiveNodes.Add(selectedPassiveNodes[i]); } return copy; } private bool HasDirectSkillSetup() { if (directSkillSlots == null) return false; for (int i = 0; i < directSkillSlots.Length; i++) { if (directSkillSlots[i] != null && directSkillSlots[i].BaseSkill != null) return true; } return false; } private void EnsureDirectSlotCapacity() { if (directSkillSlots != null && directSkillSlots.Length == DefaultSlotCount) return; SkillLoadoutEntry[] resized = new SkillLoadoutEntry[DefaultSlotCount]; if (directSkillSlots != null) { int copyCount = Mathf.Min(directSkillSlots.Length, resized.Length); for (int i = 0; i < copyCount; i++) { resized[i] = directSkillSlots[i]; } } directSkillSlots = resized; } private string ResolvePassiveLabel() { if (selectedPassiveNodes != null && selectedPassiveNodes.Count > 0) return BuildPassiveNodeSummary(selectedPassiveNodes); if (passivePreset != null && !string.IsNullOrWhiteSpace(passivePreset.PresetName)) return passivePreset.PresetName; return "패시브 없음"; } private string ResolveLoadoutLabel() { if (HasDirectSkillSetup()) return BuildLoadoutSummary(directSkillSlots); if (loadoutPreset != null && !string.IsNullOrWhiteSpace(loadoutPreset.PresetName)) return loadoutPreset.PresetName; return "로드아웃 없음"; } private static string BuildPassiveNodeSummary(IReadOnlyList nodes) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < nodes.Count; i++) { PassiveNodeData node = nodes[i]; if (node == null) continue; if (builder.Length > 0) builder.Append('+'); builder.Append(string.IsNullOrWhiteSpace(node.DisplayName) ? node.name : node.DisplayName); } return builder.Length > 0 ? builder.ToString() : "패시브 없음"; } private static string BuildLoadoutSummary(IReadOnlyList entries) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < entries.Count; i++) { SkillLoadoutEntry entry = entries[i]; if (entry == null || entry.BaseSkill == null) continue; if (builder.Length > 0) builder.Append(" / "); builder.Append('S'); builder.Append(i); builder.Append('='); builder.Append(entry.BaseSkill.SkillName); bool hasGem = false; IReadOnlyList gems = entry.SocketedGems; for (int gemIndex = 0; gemIndex < gems.Count; gemIndex++) { SkillGemData gem = gems[gemIndex]; if (gem == null) continue; builder.Append(hasGem ? '+' : '['); builder.Append(gem.GemName); hasGem = true; } if (hasGem) builder.Append(']'); } return builder.Length > 0 ? builder.ToString() : "직접구성"; } } }