using System; using System.Collections.Generic; using System.Text; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; using Colosseum.Passives; using Colosseum.Player; namespace Colosseum.UI { /// /// 비전투 중 전체 화면으로 여는 패시브 트리 UI입니다. /// public class PassiveTreeUI : MonoBehaviour { private sealed class NodeVisual { public RectTransform RectTransform; public Button Button; public Image Image; public Image FillImage; public Image InnerImage; public Outline Outline; } [Header("Toggle")] [Tooltip("패시브 UI를 여닫는 키")] [SerializeField] private Key toggleKey = Key.P; [Tooltip("토글 버튼에 표시할 텍스트")] [SerializeField] private string toggleButtonLabel = "패시브"; [Tooltip("토글 버튼의 캔버스 기준 위치")] [SerializeField] private Vector2 toggleButtonAnchoredPosition = new Vector2(-132f, 48f); [Header("Debug")] [Tooltip("플레이 모드 시작 시 패널을 자동으로 엽니다.")] [SerializeField] private bool openOnStartInPlayMode = false; [Header("Prefabs")] [Tooltip("패시브 트리 메인 뷰 프리팹")] [SerializeField] private PassiveTreeViewReferences viewPrefab; [Tooltip("패시브 노드 프리팹")] [SerializeField] private PassiveTreeNodeView nodePrefab; [Tooltip("프로토타입 패시브 카탈로그")] [SerializeField] private PassivePrototypeCatalogData passivePrototypeCatalog; [Tooltip("일반 패시브 노드에 사용할 아이콘")] [SerializeField] private Sprite normalNodeSprite; [Tooltip("허브/브릿지/완성 패시브 노드에 사용할 아이콘")] [SerializeField] private Sprite specialNodeSprite; [Tooltip("노드 안쪽에 겹쳐서 사용할 보조 아이콘")] [SerializeField] private Sprite innerNodeSprite; [Header("Style")] [SerializeField] private Color panelBackgroundColor = new Color(0.07f, 0.07f, 0.1f, 0.98f); [SerializeField] private Color sectionBackgroundColor = new Color(0.12f, 0.12f, 0.16f, 0.96f); [SerializeField] private Color sectionOverlayColor = new Color(0f, 0f, 0f, 0.12f); [SerializeField] private Color attackColor = new Color(0.78f, 0.3f, 0.22f, 1f); [SerializeField] private Color defenseColor = new Color(0.2f, 0.56f, 0.6f, 1f); [SerializeField] private Color supportColor = new Color(0.56f, 0.67f, 0.24f, 1f); [SerializeField] private Color bridgeColor = new Color(0.58f, 0.47f, 0.23f, 1f); [SerializeField] private Color lockedNodeColor = new Color(0.15f, 0.15f, 0.17f, 0.96f); [SerializeField] private Color selectedNodeTint = new Color(1f, 0.92f, 0.68f, 1f); [SerializeField] private Color focusedOutlineColor = new Color(0.95f, 0.92f, 0.82f, 1f); [SerializeField] private Color normalTextColor = new Color(0.9f, 0.88f, 0.82f, 1f); [SerializeField] private Color mutedTextColor = new Color(0.72f, 0.71f, 0.66f, 0.95f); [SerializeField] private Color statusErrorColor = new Color(1f, 0.52f, 0.45f, 1f); [SerializeField] private Color lineColor = new Color(0.72f, 0.67f, 0.53f, 0.4f); [SerializeField] private Color activeLineColor = new Color(0.98f, 0.9f, 0.7f, 0.92f); [SerializeField] private float graphCenterYOffset = -118f; private const float LeftPanelWidth = 292f; private const float RightPanelWidth = 308f; private const float GraphPadding = 24f; private const float ConnectionThickness = 7f; private readonly Dictionary nodeVisuals = new(); private PlayerNetworkController targetPlayer; private PlayerMovement playerMovement; private PlayerSkillInput playerSkillInput; private Canvas parentCanvas; private RectTransform canvasRectTransform; private PassiveTreeViewReferences viewInstance; private GameObject overlayRoot; private RectTransform panelRectTransform; private RectTransform graphRectTransform; private RectTransform connectionLayer; private RectTransform nodeLayer; private RectTransform detailContent; private TextMeshProUGUI pointsSummaryText; private TextMeshProUGUI selectionSummaryText; private TextMeshProUGUI detailText; private TextMeshProUGUI statusText; private Button nonePresetButton; private Button defensePresetButton; private Button supportPresetButton; private Button attackPresetButton; private Button clearButton; private Button selectNodeButton; private TextMeshProUGUI selectNodeButtonLabel; private bool isPanelVisible; private bool uiInitialized; private bool previousCursorVisible; private CursorLockMode previousCursorLockState; private PassiveNodeData focusedNode; private string lastStatusMessage = string.Empty; private bool lastStatusIsError; private void Awake() { EnsureUi(); HidePanelImmediate(); } private void Start() { EnsureUi(); FindLocalPlayer(); RefreshAll(); if (Application.isPlaying && openOnStartInPlayMode) { SetPanelVisible(true); } } private void Update() { if (targetPlayer == null) { FindLocalPlayer(); } if (Keyboard.current == null) return; if (Keyboard.current[toggleKey].wasPressedThisFrame) { TogglePanelVisibility(); } else if (isPanelVisible && Keyboard.current.escapeKey.wasPressedThisFrame) { SetPanelVisible(false); } if (isPanelVisible) { UpdatePanelSize(); } } private void OnDestroy() { UnsubscribeFromPlayer(); SetGameplayInputBlocked(false); RestoreCursorState(); } private void FindLocalPlayer() { PlayerNetworkController[] players = FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < players.Length; i++) { if (!players[i].IsOwner) continue; if (targetPlayer == players[i]) return; SetTarget(players[i]); return; } } private void SetTarget(PlayerNetworkController target) { UnsubscribeFromPlayer(); targetPlayer = target; playerMovement = targetPlayer != null ? targetPlayer.GetComponent() : null; playerSkillInput = targetPlayer != null ? targetPlayer.GetComponent() : null; if (targetPlayer != null) { targetPlayer.OnPassiveSelectionChanged += HandlePassiveSelectionChanged; } if (isPanelVisible) { SetGameplayInputBlocked(true); } RefreshAll(); } private void UnsubscribeFromPlayer() { if (targetPlayer != null) { targetPlayer.OnPassiveSelectionChanged -= HandlePassiveSelectionChanged; } } private void HandlePassiveSelectionChanged() { RefreshAll(); } private void TogglePanelVisibility() { SetPanelVisible(!isPanelVisible); } private void SetPanelVisible(bool visible) { EnsureUi(); isPanelVisible = visible; UpdatePanelSize(); if (overlayRoot != null) { overlayRoot.SetActive(visible); } if (visible) { RememberCursorState(); Cursor.visible = true; Cursor.lockState = CursorLockMode.None; SetGameplayInputBlocked(true); RefreshAll(); } else { SetGameplayInputBlocked(false); RestoreCursorState(); } } private void HidePanelImmediate() { isPanelVisible = false; if (overlayRoot != null) { overlayRoot.SetActive(false); } } private void RememberCursorState() { previousCursorVisible = Cursor.visible; previousCursorLockState = Cursor.lockState; } private void RestoreCursorState() { Cursor.visible = previousCursorVisible; Cursor.lockState = previousCursorLockState; } private void SetGameplayInputBlocked(bool blocked) { if (playerMovement != null) { playerMovement.SetGameplayInputEnabled(!blocked); } if (playerSkillInput != null) { playerSkillInput.SetGameplayInputEnabled(!blocked); } } private void EnsureUi() { if (uiInitialized) return; parentCanvas = GetComponentInParent(); canvasRectTransform = parentCanvas != null ? parentCanvas.GetComponent() : null; if (parentCanvas == null || canvasRectTransform == null) { Debug.LogWarning("[PassiveTreeUI] Canvas를 찾지 못했습니다."); return; } if (!EnsureViewInstance()) return; BindViewReferences(); UpdatePanelSize(); uiInitialized = overlayRoot != null && panelRectTransform != null && graphRectTransform != null; } /// /// 런타임에 사용할 패시브 트리 뷰 프리팹을 외부에서 주입합니다. /// public void ConfigurePrefabReferences(PassiveTreeViewReferences configuredViewPrefab, PassiveTreeNodeView configuredNodePrefab) { viewPrefab = configuredViewPrefab; nodePrefab = configuredNodePrefab; } private bool EnsureViewInstance() { if (viewInstance != null) return true; viewInstance = GetComponentInChildren(true); if (viewInstance != null) return true; if (viewPrefab == null) { Debug.LogWarning("[PassiveTreeUI] 메인 뷰 프리팹이 지정되지 않았습니다."); return false; } viewInstance = Instantiate(viewPrefab, canvasRectTransform); viewInstance.name = viewPrefab.name; return viewInstance != null; } private void BindViewReferences() { if (viewInstance == null) return; overlayRoot = viewInstance.OverlayRoot; panelRectTransform = viewInstance.PanelRect; graphRectTransform = viewInstance.GraphRect; connectionLayer = viewInstance.ConnectionLayer; nodeLayer = viewInstance.NodeLayer; detailContent = viewInstance.DetailContent; pointsSummaryText = viewInstance.PointsSummaryText; selectionSummaryText = viewInstance.SelectionSummaryText; detailText = viewInstance.DetailText; statusText = viewInstance.StatusText; nonePresetButton = viewInstance.NonePresetButton; defensePresetButton = viewInstance.DefensePresetButton; supportPresetButton = viewInstance.SupportPresetButton; attackPresetButton = viewInstance.AttackPresetButton; clearButton = viewInstance.ClearButton; selectNodeButton = viewInstance.SelectNodeButton; selectNodeButtonLabel = viewInstance.SelectNodeButtonLabel; ApplyStaticViewSettings(); if (statusText != null) { statusText.textWrappingMode = TextWrappingModes.NoWrap; statusText.overflowMode = TextOverflowModes.Ellipsis; } BindButton(viewInstance.ToggleButton, TogglePanelVisibility); BindButton(viewInstance.CloseButton, () => SetPanelVisible(false)); BindButton(nonePresetButton, () => TryApplyPreset(PassivePrototypePresetKind.None)); BindButton(defensePresetButton, () => TryApplyPreset(PassivePrototypePresetKind.Defense)); BindButton(supportPresetButton, () => TryApplyPreset(PassivePrototypePresetKind.Support)); BindButton(attackPresetButton, () => TryApplyPreset(PassivePrototypePresetKind.Attack)); BindButton(clearButton, TryClearSelection); BindButton(selectNodeButton, TrySelectFocusedNode); } private void ApplyStaticViewSettings() { if (viewInstance?.ToggleButtonLabel != null) { viewInstance.ToggleButtonLabel.text = toggleButtonLabel; } if (viewInstance?.ToggleButton != null) { RectTransform toggleRect = viewInstance.ToggleButton.GetComponent(); if (toggleRect != null) { toggleRect.anchoredPosition = toggleButtonAnchoredPosition; } } } private static void BindButton(Button button, Action callback) { if (button == null || callback == null) return; button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => callback()); } private void UpdatePanelSize() { if (panelRectTransform == null || canvasRectTransform == null) return; Rect canvasRect = canvasRectTransform.rect; panelRectTransform.sizeDelta = new Vector2( Mathf.Max(1160f, canvasRect.width - 24f), Mathf.Max(720f, canvasRect.height - 24f)); } private void CreateToggleButton() { GameObject buttonObject = CreateUiObject("Button_PassiveTree", canvasRectTransform); RectTransform buttonRect = buttonObject.AddComponent(); buttonRect.anchorMin = new Vector2(1f, 0f); buttonRect.anchorMax = new Vector2(1f, 0f); buttonRect.pivot = new Vector2(1f, 0f); buttonRect.anchoredPosition = toggleButtonAnchoredPosition; buttonRect.sizeDelta = new Vector2(110f, 40f); Image buttonImage = buttonObject.AddComponent(); buttonImage.color = new Color(0.18f, 0.21f, 0.16f, 0.96f); Button button = buttonObject.AddComponent