feat: 패시브 트리 프로토타입 구현
- 패시브 트리/노드/프리셋 데이터와 카탈로그 참조 구조를 추가하고 Resources 의존을 Data/Passives 자산 구조로 정리 - 플레이어 런타임, 전투 계수, 프리셋 적용, 멀티플레이 동기화 경로에 패시브 적용 로직 연결 - 프리팹 기반 패시브 트리 UI와 노드 아이콘/프리셋/상세 패널 흐름을 추가하고 HUD에 연동 - 패시브 디버그/부트스트랩 메뉴와 UI 프리팹 재생성 경로를 추가
This commit is contained in:
328
Assets/_Game/Scripts/Passives/PassiveRuntimeController.cs
Normal file
328
Assets/_Game/Scripts/Passives/PassiveRuntimeController.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Stats;
|
||||
|
||||
namespace Colosseum.Passives
|
||||
{
|
||||
/// <summary>
|
||||
/// 선택된 패시브 노드를 실제 전투 수치에 적용합니다.
|
||||
/// </summary>
|
||||
[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<PassiveEffectEntry> activeEffects = new List<PassiveEffectEntry>();
|
||||
private readonly List<string> selectedNodeIds = new List<string>();
|
||||
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<string> SelectedNodeIds => selectedNodeIds;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (characterStats == null)
|
||||
{
|
||||
characterStats = GetComponent<CharacterStats>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외부에서 참조를 보정합니다.
|
||||
/// </summary>
|
||||
public void Initialize(CharacterStats stats)
|
||||
{
|
||||
if (stats != null)
|
||||
{
|
||||
characterStats = stats;
|
||||
}
|
||||
else if (characterStats == null)
|
||||
{
|
||||
characterStats = GetComponent<CharacterStats>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 노드 구성을 적용합니다.
|
||||
/// </summary>
|
||||
public bool TryApplySelection(PassiveTreeData tree, IReadOnlyList<string> 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<PassiveNodeData> 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<PassiveEffectEntry> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택 상태를 해제합니다.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패시브 전투 배율을 안전하게 조회하는 유틸리티입니다.
|
||||
/// </summary>
|
||||
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<PassiveRuntimeController>() ?? actor.GetComponentInParent<PassiveRuntimeController>();
|
||||
}
|
||||
|
||||
private static SkillData GetCurrentSkill(GameObject actor)
|
||||
{
|
||||
if (actor == null)
|
||||
return null;
|
||||
|
||||
SkillController skillController = actor.GetComponent<SkillController>() ?? actor.GetComponentInParent<SkillController>();
|
||||
return skillController != null ? skillController.CurrentSkill : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user