using System; using System.Collections.Generic; using System.Text; using UnityEngine; using Colosseum.Skills; using Colosseum.Stats; namespace Colosseum.Passives { /// /// 선택된 패시브 노드를 실제 전투 수치에 적용합니다. /// [DisallowMultipleComponent] public class PassiveRuntimeController : MonoBehaviour { [Header("References")] [Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")] [SerializeField] private CharacterStats characterStats; [Header("Debug")] [Tooltip("현재 적용된 패시브 프리셋 이름")] [SerializeField] private string currentPresetName = string.Empty; [Tooltip("현재 적용된 노드 ID 목록")] [SerializeField] private string currentSelectionSummary = string.Empty; [Tooltip("현재 사용한 패시브 포인트")] [Min(0)] [SerializeField] private int usedPoints = 0; private readonly List activeEffects = new List(); private readonly List selectedNodeIds = new List(); private PassiveTreeData currentTree; public string CurrentPresetName => currentPresetName; public int UsedPoints => usedPoints; public int RemainingPoints => currentTree != null ? Mathf.Max(0, currentTree.InitialPoints - usedPoints) : 0; public IReadOnlyList SelectedNodeIds => selectedNodeIds; private void Awake() { if (characterStats == null) { characterStats = GetComponent(); } } /// /// 외부에서 참조를 보정합니다. /// public void Initialize(CharacterStats stats) { if (stats != null) { characterStats = stats; } else if (characterStats == null) { characterStats = GetComponent(); } } /// /// 선택된 노드 구성을 적용합니다. /// public bool TryApplySelection(PassiveTreeData tree, IReadOnlyList nodeIds, string presetName, out string reason) { Initialize(characterStats); ClearAppliedState(); currentTree = tree; currentPresetName = presetName ?? string.Empty; if (currentTree == null) { reason = "패시브 트리 데이터가 없습니다."; return false; } if (!currentTree.TryResolveSelection(nodeIds, out List resolvedNodes, out reason)) { return false; } usedPoints = currentTree.CalculateUsedPoints(resolvedNodes); for (int i = 0; i < resolvedNodes.Count; i++) { PassiveNodeData node = resolvedNodes[i]; if (node == null) continue; selectedNodeIds.Add(node.NodeId); IReadOnlyList effects = node.Effects; if (effects == null) continue; for (int j = 0; j < effects.Count; j++) { PassiveEffectEntry effect = effects[j]; if (effect == null) continue; ApplyEffect(effect); } } currentSelectionSummary = BuildSelectionSummary(); reason = string.Empty; return true; } /// /// 현재 선택 상태를 해제합니다. /// public void ClearSelection() { ClearAppliedState(); currentTree = null; currentPresetName = string.Empty; currentSelectionSummary = string.Empty; } public float GetDamageMultiplier(SkillData skill = null) { return GetScalarMultiplier(PassiveEffectType.DamageMultiplier, skill); } public float GetHealMultiplier(SkillData skill = null) { return GetScalarMultiplier(PassiveEffectType.HealMultiplier, skill); } public float GetShieldDoneMultiplier(SkillData skill = null) { return GetScalarMultiplier(PassiveEffectType.ShieldDoneMultiplier, skill); } public float GetShieldReceivedMultiplier() { return GetScalarMultiplier(PassiveEffectType.ShieldReceivedMultiplier, null); } public float GetThreatGeneratedMultiplier() { return GetScalarMultiplier(PassiveEffectType.ThreatGeneratedMultiplier, null); } public float GetIncomingDamageMultiplier() { return GetScalarMultiplier(PassiveEffectType.IncomingDamageMultiplier, null); } public float GetManaCostMultiplier(SkillData skill = null) { return GetScalarMultiplier(PassiveEffectType.ManaCostMultiplier, skill); } public string BuildSummary() { StringBuilder builder = new StringBuilder(); builder.Append("[Passive] "); builder.Append(string.IsNullOrWhiteSpace(currentPresetName) ? "미적용" : currentPresetName); builder.Append(" | Used="); builder.Append(usedPoints); if (currentTree != null) { builder.Append('/'); builder.Append(currentTree.InitialPoints); } if (selectedNodeIds.Count > 0) { builder.Append(" | Nodes="); builder.Append(currentSelectionSummary); } return builder.ToString(); } private void ApplyEffect(PassiveEffectEntry effect) { if (effect.EffectType == PassiveEffectType.StatModifier) { ApplyStatModifier(effect); return; } activeEffects.Add(effect); } private void ApplyStatModifier(PassiveEffectEntry effect) { if (characterStats == null || effect == null) return; CharacterStat stat = characterStats.GetStat(effect.StatType); if (stat == null) return; stat.AddModifier(new StatModifier(effect.Value, effect.ModType, this)); } private float GetScalarMultiplier(PassiveEffectType effectType, SkillData skill) { float result = 1f; for (int i = 0; i < activeEffects.Count; i++) { PassiveEffectEntry effect = activeEffects[i]; if (effect == null || effect.EffectType != effectType) continue; if (!effect.AppliesToSkill(skill)) continue; result *= Mathf.Max(0f, effect.Value); } return result; } private void ClearAppliedState() { RemoveAllPassiveStatModifiers(); activeEffects.Clear(); selectedNodeIds.Clear(); usedPoints = 0; } private void RemoveAllPassiveStatModifiers() { if (characterStats == null) return; foreach (StatType statType in Enum.GetValues(typeof(StatType))) { CharacterStat stat = characterStats.GetStat(statType); stat?.RemoveAllModifiersFromSource(this); } } private string BuildSelectionSummary() { if (selectedNodeIds.Count <= 0) return string.Empty; return string.Join(", ", selectedNodeIds); } } /// /// 패시브 전투 배율을 안전하게 조회하는 유틸리티입니다. /// public static class PassiveRuntimeModifierUtility { public static float GetDamageMultiplier(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetDamageMultiplier(GetCurrentSkill(actor)) : 1f; } public static float GetHealMultiplier(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetHealMultiplier(GetCurrentSkill(actor)) : 1f; } public static float GetShieldDoneMultiplier(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetShieldDoneMultiplier(GetCurrentSkill(actor)) : 1f; } public static float GetShieldReceivedMultiplier(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetShieldReceivedMultiplier() : 1f; } public static float GetThreatGeneratedMultiplier(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetThreatGeneratedMultiplier() : 1f; } public static float GetIncomingDamageMultiplier(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetIncomingDamageMultiplier() : 1f; } public static float GetManaCostMultiplier(GameObject actor, SkillData skill) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.GetManaCostMultiplier(skill) : 1f; } public static string GetCurrentPresetName(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.CurrentPresetName : string.Empty; } public static string BuildSummary(GameObject actor) { PassiveRuntimeController controller = GetController(actor); return controller != null ? controller.BuildSummary() : "[Passive] 미적용"; } private static PassiveRuntimeController GetController(GameObject actor) { if (actor == null) return null; return actor.GetComponent() ?? actor.GetComponentInParent(); } private static SkillData GetCurrentSkill(GameObject actor) { if (actor == null) return null; SkillController skillController = actor.GetComponent() ?? actor.GetComponentInParent(); return skillController != null ? skillController.CurrentSkill : null; } } }