using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// /// 전체 인벤토리 UI를 갱신하는 스크립트입니다. /// 기능: 패널 열기/닫기, 슬롯 갱신, 획득 팝업, 부족 안내 메시지, 확인창, 상세보기, 획득 로그, 기억의 조각 진행도, 간단한 손목/타겟 추적. /// InventoryUI가 붙은 오브젝트는 항상 켜두고, 실제 보이는 inventoryPanel만 켜고 끄는 구조를 권장합니다. /// [DisallowMultipleComponent] public class InventoryUI : MonoBehaviour { [Header("References")] [SerializeField] private InventoryManager inventoryManager; [Tooltip("실제로 보이거나 숨겨질 자식 패널입니다. InventoryUI가 붙은 자기 자신을 넣지 않는 것을 권장합니다.")] [SerializeField] private GameObject inventoryPanel; [SerializeField] private InventorySlotUI[] slots; [Header("Panel Settings")] [SerializeField] private bool autoFindManager = true; [SerializeField] private bool autoFindPanelChild = true; [SerializeField] private string autoPanelChildName = "InventoryPanel"; [SerializeField] private bool visibleOnStart = false; [SerializeField] private bool refreshOnEnable = true; [SerializeField] private bool showNewBadgeOnIncrease = true; [SerializeField] private bool preventDisablingSelf = true; [Header("Filter")] [SerializeField] private InventoryItemCategory currentFilter = InventoryItemCategory.All; [SerializeField] private bool hideSlotsOutsideFilter = false; [Header("Acquisition Popup")] [SerializeField] private GameObject acquisitionPopupPanel; [SerializeField] private CanvasGroup acquisitionPopupCanvasGroup; [SerializeField] private Image acquisitionIconImage; [SerializeField] private TMP_Text acquisitionText; [SerializeField] private float acquisitionPopupTime = 1.4f; [SerializeField] private string acquisitionFormat = "{0} x{1} 획득!"; [Header("Message Popup")] [SerializeField] private GameObject messagePanel; [SerializeField] private CanvasGroup messageCanvasGroup; [SerializeField] private TMP_Text messageText; [SerializeField] private float messageShowTime = 1.5f; [Header("Confirmation UI")] [SerializeField] private GameObject confirmationPanel; [SerializeField] private TMP_Text confirmationTitleText; [SerializeField] private TMP_Text confirmationBodyText; [SerializeField] private Button confirmationYesButton; [SerializeField] private Button confirmationNoButton; [Header("Detail UI")] [SerializeField] private GameObject detailPanel; [SerializeField] private Image detailIconImage; [SerializeField] private TMP_Text detailTitleText; [SerializeField] private TMP_Text detailDescriptionText; [SerializeField] private TMP_Text detailCountText; [SerializeField] private TMP_Text detailGoalText; [SerializeField] private Button detailUseButton; [Header("Recent Log UI")] [SerializeField] private TMP_Text[] recentLogTexts; [Header("Memory Piece UI")] [SerializeField] private TMP_Text memoryProgressText; [SerializeField] private Slider memoryProgressSlider; [SerializeField] private string memoryProgressFormat = "기억의 조각 {0} / {1}"; [Header("Audio")] [SerializeField] private AudioSource uiAudioSource; [SerializeField] private AudioClip defaultAcquisitionClip; [SerializeField] private AudioClip defaultUseClip; [SerializeField] private AudioClip defaultErrorClip; [Header("Optional Follow Target / Wrist UI")] [SerializeField] private bool followTarget = false; [SerializeField] private Transform targetToFollow; [SerializeField] private Vector3 localPositionOffset = new Vector3(0f, 0.08f, 0.12f); [SerializeField] private Vector3 localEulerOffset = Vector3.zero; [SerializeField] private bool faceMainCamera = false; [Header("Editor Test Toggle")] [SerializeField] private bool enableKeyboardToggleForTesting = false; [SerializeField] private KeyCode keyboardToggleKey = KeyCode.I; private bool subscribed; private Coroutine acquisitionRoutine; private Coroutine messageRoutine; private InventoryItemType pendingUseItemType; private int pendingUseAmount = 1; private bool hasPendingUse; private InventoryItemType currentDetailItemType; private bool hasDetailItem; private void Awake() { if (inventoryManager == null && InventoryManager.Instance != null) inventoryManager = InventoryManager.Instance; if (inventoryManager == null && autoFindManager) inventoryManager = FindFirstObjectByType(); if (inventoryPanel == null && autoFindPanelChild) { Transform panelTransform = transform.Find(autoPanelChildName); if (panelTransform != null) inventoryPanel = panelTransform.gameObject; } if (uiAudioSource == null) uiAudioSource = GetComponent(); if (acquisitionPopupPanel != null) acquisitionPopupPanel.SetActive(false); if (messagePanel != null) messagePanel.SetActive(false); if (confirmationPanel != null) confirmationPanel.SetActive(false); if (detailPanel != null) detailPanel.SetActive(false); if (confirmationYesButton != null) { confirmationYesButton.onClick.RemoveListener(ConfirmPendingUse); confirmationYesButton.onClick.AddListener(ConfirmPendingUse); } if (confirmationNoButton != null) { confirmationNoButton.onClick.RemoveListener(CancelPendingUse); confirmationNoButton.onClick.AddListener(CancelPendingUse); } if (detailUseButton != null) { detailUseButton.onClick.RemoveListener(UseCurrentDetailItem); detailUseButton.onClick.AddListener(UseCurrentDetailItem); } ApplyItemDataToSlots(); } private void Start() { SetVisible(visibleOnStart); RefreshAllSlots(); RefreshRecentLogs(); RefreshMemoryProgress(); } private void OnEnable() { Subscribe(); if (refreshOnEnable) RefreshAllSlots(); } private void OnDisable() { Unsubscribe(); } private void Update() { if (enableKeyboardToggleForTesting && Input.GetKeyDown(keyboardToggleKey)) ToggleVisible(); } private void LateUpdate() { if (!followTarget || targetToFollow == null) return; transform.position = targetToFollow.TransformPoint(localPositionOffset); transform.rotation = targetToFollow.rotation * Quaternion.Euler(localEulerOffset); if (faceMainCamera && Camera.main != null) { Vector3 direction = transform.position - Camera.main.transform.position; if (direction.sqrMagnitude > 0.0001f) transform.rotation = Quaternion.LookRotation(direction.normalized, Vector3.up); } } public void SetVisible(bool visible) { if (inventoryPanel == null) return; if (preventDisablingSelf && inventoryPanel == gameObject) { Debug.LogWarning("[InventoryUI] InventoryPanel에 자기 자신이 들어가 있습니다. InventoryRoot는 항상 켜두고 자식 InventoryPanel만 연결하세요.", this); return; } inventoryPanel.SetActive(visible); if (visible) RefreshAllSlots(); } public void ToggleVisible() { if (inventoryPanel != null) SetVisible(!inventoryPanel.activeSelf); } public void ShowInventory() => SetVisible(true); public void HideInventory() => SetVisible(false); public void SetManager(InventoryManager manager) { if (inventoryManager == manager) return; Unsubscribe(); inventoryManager = manager; Subscribe(); ApplyItemDataToSlots(); RefreshAllSlots(); } public void SetFilterAll() => SetFilter(InventoryItemCategory.All); public void SetFilterConsumable() => SetFilter(InventoryItemCategory.Consumable); public void SetFilterQuest() => SetFilter(InventoryItemCategory.Quest); public void SetFilterKeyItem() => SetFilter(InventoryItemCategory.KeyItem); public void SetFilterMaterial() => SetFilter(InventoryItemCategory.Material); public void SetFilter(InventoryItemCategory category) { currentFilter = category; ApplyFilterToSlots(); } public void RefreshAllSlots() { if (inventoryManager == null || slots == null) return; for (int i = 0; i < slots.Length; i++) { InventorySlotUI slot = slots[i]; if (slot == null) continue; InventoryItemDefinition definition = inventoryManager.GetDefinition(slot.ItemType); slot.SetInventoryUI(this); slot.SetDefinition(definition); int count = inventoryManager.GetItemCount(slot.ItemType); int maxCount = inventoryManager.GetMaxCount(slot.ItemType); slot.SetCount(count, false, maxCount); } ApplyFilterToSlots(); } private void RefreshSlot(InventoryItemType itemType, int count) { if (slots == null) return; int maxCount = inventoryManager != null ? inventoryManager.GetMaxCount(itemType) : 0; for (int i = 0; i < slots.Length; i++) { InventorySlotUI slot = slots[i]; if (slot == null || slot.ItemType != itemType) continue; slot.SetCount(count, showNewBadgeOnIncrease, maxCount); } if (hasDetailItem && currentDetailItemType == itemType) ShowItemDetail(itemType); } private void ApplyItemDataToSlots() { if (inventoryManager == null || slots == null) return; for (int i = 0; i < slots.Length; i++) { InventorySlotUI slot = slots[i]; if (slot == null) continue; slot.SetInventoryUI(this); slot.SetDefinition(inventoryManager.GetDefinition(slot.ItemType)); } } private void ApplyFilterToSlots() { if (slots == null) return; for (int i = 0; i < slots.Length; i++) { InventorySlotUI slot = slots[i]; if (slot == null) continue; bool visible = currentFilter == InventoryItemCategory.All || slot.Category == currentFilter; if (hideSlotsOutsideFilter) slot.gameObject.SetActive(visible); else slot.SetFilteredOut(!visible); } } private void Subscribe() { if (subscribed || inventoryManager == null) return; inventoryManager.ItemCountChanged += RefreshSlot; inventoryManager.ItemAdded += HandleItemAdded; inventoryManager.ItemUsed += HandleItemUsed; inventoryManager.MessageRequested += ShowMessage; inventoryManager.LogAdded += HandleLogAdded; inventoryManager.MemoryPieceProgressChanged += HandleMemoryPieceProgressChanged; subscribed = true; } private void Unsubscribe() { if (!subscribed || inventoryManager == null) return; inventoryManager.ItemCountChanged -= RefreshSlot; inventoryManager.ItemAdded -= HandleItemAdded; inventoryManager.ItemUsed -= HandleItemUsed; inventoryManager.MessageRequested -= ShowMessage; inventoryManager.LogAdded -= HandleLogAdded; inventoryManager.MemoryPieceProgressChanged -= HandleMemoryPieceProgressChanged; subscribed = false; } private void HandleItemAdded(InventoryItemType itemType, int addedAmount, int totalCount) { ShowAcquisitionPopup(itemType, addedAmount); } private void HandleItemUsed(InventoryItemType itemType, int count) { AudioClip clip = inventoryManager != null ? inventoryManager.GetUseClip(itemType) : null; PlayUIClip(clip != null ? clip : defaultUseClip); } private void HandleLogAdded(InventoryLogEntry entry) { RefreshRecentLogs(); } private void HandleMemoryPieceProgressChanged(int current, int target) { RefreshMemoryProgress(current, target); } public void ShowAcquisitionPopup(InventoryItemType itemType, int amount) { if (inventoryManager == null) return; string displayName = inventoryManager.GetDisplayName(itemType); Sprite icon = inventoryManager.GetIcon(itemType); if (acquisitionIconImage != null) { acquisitionIconImage.sprite = icon; acquisitionIconImage.enabled = icon != null; } if (acquisitionText != null) acquisitionText.text = string.Format(acquisitionFormat, displayName, Mathf.Max(1, amount)); AudioClip clip = inventoryManager.GetAcquisitionClip(itemType); PlayUIClip(clip != null ? clip : defaultAcquisitionClip); if (acquisitionRoutine != null) StopCoroutine(acquisitionRoutine); acquisitionRoutine = StartCoroutine(ShowPanelRoutine(acquisitionPopupPanel, acquisitionPopupCanvasGroup, acquisitionPopupTime)); } public void ShowMessage(string message) { if (string.IsNullOrWhiteSpace(message)) return; if (messageText != null) messageText.text = message; if (messageRoutine != null) StopCoroutine(messageRoutine); messageRoutine = StartCoroutine(ShowPanelRoutine(messagePanel, messageCanvasGroup, messageShowTime)); } private IEnumerator ShowPanelRoutine(GameObject panel, CanvasGroup canvasGroup, float showTime) { if (panel == null) yield break; panel.SetActive(true); if (canvasGroup != null) canvasGroup.alpha = 1f; yield return new WaitForSecondsRealtime(Mathf.Max(0.05f, showTime)); if (canvasGroup != null) canvasGroup.alpha = 0f; panel.SetActive(false); } public void RequestUseItem(InventoryItemType itemType) { RequestUseItem(itemType, 1); } public void RequestUseItem(InventoryItemType itemType, int amount) { if (inventoryManager == null) return; if (!inventoryManager.IsUsable(itemType)) { ShowMessage($"{inventoryManager.GetDisplayName(itemType)}은(는) 지금 사용할 수 없습니다."); PlayUIClip(defaultErrorClip); return; } if (!inventoryManager.HasItem(itemType, amount)) { ShowMessage(inventoryManager.GetInsufficientMessage(itemType, amount)); PlayUIClip(defaultErrorClip); return; } if (inventoryManager.RequiresUseConfirmation(itemType)) { pendingUseItemType = itemType; pendingUseAmount = Mathf.Max(1, amount); ShowConfirmation(itemType, amount); } else { bool result = inventoryManager.UseItem(itemType, amount); if (!result) PlayUIClip(defaultErrorClip); } } private void ShowConfirmation(InventoryItemType itemType, int amount) { pendingUseItemType = itemType; pendingUseAmount = Mathf.Max(1, amount); hasPendingUse = true; if (confirmationPanel == null) { ConfirmPendingUse(); return; } string displayName = inventoryManager != null ? inventoryManager.GetDisplayName(itemType) : itemType.ToString(); if (confirmationTitleText != null) confirmationTitleText.text = "아이템 사용"; if (confirmationBodyText != null) confirmationBodyText.text = $"{displayName}을(를) 사용하시겠습니까?"; confirmationPanel.SetActive(true); } public void ConfirmPendingUse() { if (confirmationPanel != null) confirmationPanel.SetActive(false); if (!hasPendingUse) return; InventoryItemType itemType = pendingUseItemType; int amount = Mathf.Max(1, pendingUseAmount); hasPendingUse = false; if (inventoryManager != null) { bool result = inventoryManager.UseItem(itemType, amount); if (!result) PlayUIClip(defaultErrorClip); } } public void CancelPendingUse() { hasPendingUse = false; if (confirmationPanel != null) confirmationPanel.SetActive(false); } public void ShowItemDetail(InventoryItemType itemType) { hasDetailItem = true; currentDetailItemType = itemType; if (inventoryManager == null || detailPanel == null) return; InventoryItemDefinition definition = inventoryManager.GetDefinition(itemType); int count = inventoryManager.GetItemCount(itemType); int maxCount = inventoryManager.GetMaxCount(itemType); Sprite icon = inventoryManager.GetIcon(itemType); if (detailIconImage != null) { detailIconImage.sprite = icon; detailIconImage.enabled = icon != null; } if (detailTitleText != null) detailTitleText.text = inventoryManager.GetDisplayName(itemType); if (detailDescriptionText != null) detailDescriptionText.text = inventoryManager.GetDescription(itemType); if (detailCountText != null) detailCountText.text = maxCount > 0 ? $"보유: x{count} / {maxCount}" : $"보유: x{count}"; if (detailGoalText != null) detailGoalText.text = inventoryManager.GetGoalHint(itemType); if (detailUseButton != null) { bool usable = definition != null && definition.usable && count > 0; detailUseButton.gameObject.SetActive(definition != null && definition.usable); detailUseButton.interactable = usable; } detailPanel.SetActive(true); } public void HideItemDetail() { hasDetailItem = false; if (detailPanel != null) detailPanel.SetActive(false); } public void UseCurrentDetailItem() { if (!hasDetailItem) return; RequestUseItem(currentDetailItemType, 1); } private void RefreshRecentLogs() { if (recentLogTexts == null || recentLogTexts.Length == 0 || inventoryManager == null) return; IReadOnlyList logs = inventoryManager.GetRecentLogs(); for (int i = 0; i < recentLogTexts.Length; i++) { if (recentLogTexts[i] == null) continue; recentLogTexts[i].text = i < logs.Count && logs[i] != null ? logs[i].ToString() : string.Empty; } } private void RefreshMemoryProgress() { if (inventoryManager == null) return; RefreshMemoryProgress(inventoryManager.GetItemCount(inventoryManager.MemoryPieceItemType), inventoryManager.MemoryPieceTargetCount); } private void RefreshMemoryProgress(int current, int target) { target = Mathf.Max(1, target); current = Mathf.Clamp(current, 0, target); if (memoryProgressText != null) memoryProgressText.text = string.Format(memoryProgressFormat, current, target); if (memoryProgressSlider != null) { memoryProgressSlider.minValue = 0f; memoryProgressSlider.maxValue = target; memoryProgressSlider.value = current; } } private void PlayUIClip(AudioClip clip) { if (clip == null) return; if (uiAudioSource != null) uiAudioSource.PlayOneShot(clip); else AudioSource.PlayClipAtPoint(clip, transform.position); } public void CheckVRUISetup() { Canvas canvas = GetComponentInParent(); if (canvas == null) { Debug.LogWarning("[InventoryUI] Canvas가 없습니다.", this); return; } if (canvas.renderMode != RenderMode.WorldSpace) Debug.LogWarning("[InventoryUI] VR UI에서는 Canvas Render Mode를 World Space로 권장합니다.", canvas); if (EventSystem.current == null) Debug.LogWarning("[InventoryUI] EventSystem이 없습니다. VR 포인터 UI가 동작하지 않을 수 있습니다.", this); GraphicRaycaster graphicRaycaster = canvas.GetComponent(); if (graphicRaycaster == null) Debug.LogWarning("[InventoryUI] Canvas에 GraphicRaycaster 또는 Tracked Device Graphic Raycaster가 필요합니다.", canvas); } }