Files
WhaleAdventure_VR_Diet/Assets/My project/Fishing Scripts/UI/FishingGaugeUI.cs
2026-06-29 17:43:17 +09:00

1283 lines
45 KiB
C#

using System;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
[Serializable]
public class FishingItemIconData
{
public FishingItemType itemType = FishingItemType.Fish;
public Sprite icon;
[TextArea] public string hintText;
}
/// <summary>
/// Reel-only fishing UI.
///
/// Removed from the old mixed version:
/// - circular timing gauge references
/// - pointer / success zone rendering
/// - Perfect / Good / Miss result type helpers
/// - debug catch button handling
///
/// Keeps:
/// - reel progress gauge
/// - line length text
/// - pond cleanup progress
/// - caught item panel
/// - final result panel
/// - slot highlight / simple UI effects
/// </summary>
public class FishingGaugeUI : MonoBehaviour
{
[Header("Auto Bind")]
[SerializeField] private bool autoBindMissingReferences = true;
[Header("Root / Main Panels")]
[SerializeField] private GameObject backgroundPanel;
[SerializeField] private GameObject counterPanel;
[SerializeField] private GameObject itemSlotPanel;
[SerializeField] private GameObject caughtItemPanel;
[SerializeField] private GameObject controllerGuidePanel;
[SerializeField] private GameObject memoryPieceNoticePanel;
[SerializeField] private GameObject finalResultPanel;
[Header("Top Texts")]
[SerializeField] private TMP_Text titleText;
[SerializeField] private TMP_Text pondStateText;
[SerializeField] private TMP_Text objectiveText;
[Header("Counter Panel Texts")]
[SerializeField] private TMP_Text catchCountText;
[SerializeField] private TMP_Text cleanupProgressText;
[Header("Cleanup Gauge")]
[Tooltip("CleanupGauge/CleanupGaugeFill. Image Type = Filled, Fill Method = Horizontal, Fill Origin = Left")]
[SerializeField] private Image cleanupGaugeFill;
[SerializeField] private TMP_Text cleanupPercentText;
[Header("Simple Reel UI")]
[Tooltip("릴 감기 진행도 게이지 루트입니다. 없으면 텍스트만 업데이트됩니다.")]
[SerializeField] private GameObject reelProgressGaugeRoot;
[Tooltip("릴 감기 진행도 Fill Image입니다. Image Type = Filled / Horizontal / Left 추천.")]
[SerializeField] private Image reelProgressGaugeFill;
[Tooltip("현재 줄 길이를 보여줄 텍스트입니다. CounterPanel 안에 LineLengthText를 추가해서 연결하세요.")]
[SerializeField] private TMP_Text lineLengthText;
[SerializeField] private string simpleReelObjectiveText = "릴을 감아 끌어올려라";
[SerializeField] private string simpleReelIdleText = "릴 손잡이를 잡고 돌려라";
[SerializeField] private string simpleReelReelingText = "좋다, 계속 감아라!";
[Header("Result UI")]
[SerializeField] private TMP_Text resultText;
[Header("Caught Item UI")]
[SerializeField] private Image itemIcon;
[SerializeField] private TMP_Text caughtItemText;
[SerializeField] private TMP_Text itemHintText;
[Header("Item Slot UI - Fish / Other")]
[SerializeField] private GameObject fishSlot;
[SerializeField] private Image fishSlotBackground;
[SerializeField] private Image fishIcon;
[SerializeField] private TMP_Text fishCountText;
[Header("Item Slot UI - Trash")]
[SerializeField] private GameObject trashSlot;
[SerializeField] private Image trashSlotBackground;
[SerializeField] private Image trashIcon;
[SerializeField] private TMP_Text trashCountText;
[Header("Item Slot UI - Memory")]
[SerializeField] private GameObject memorySlot;
[SerializeField] private Image memorySlotBackground;
[SerializeField] private Image memoryIcon;
[SerializeField] private TMP_Text memoryPieceCountText;
[Header("Item Slot UI - Optional Compass")]
[SerializeField] private GameObject compassSlot;
[SerializeField] private Image compassSlotBackground;
[SerializeField] private Image compassIcon;
[SerializeField] private TMP_Text compassCountText;
[Header("Panel Canvas Groups")]
[SerializeField] private CanvasGroup caughtItemCanvasGroup;
[SerializeField] private CanvasGroup controllerGuideCanvasGroup;
[SerializeField] private CanvasGroup memoryPieceNoticeCanvasGroup;
[SerializeField] private CanvasGroup finalResultCanvasGroup;
[Header("Effects")]
[SerializeField] private FishingUIEffects effects;
[SerializeField] private bool autoCreateEffectsComponent = true;
[SerializeField] private bool animateCleanupGauge = true;
[SerializeField] private float cleanupFillTime = 0.45f;
[SerializeField] private float resultShowTime = 0.85f;
[SerializeField] private float resultPopScale = 1.18f;
[SerializeField] private float resultPopTime = 0.18f;
[SerializeField] private float caughtItemShowTime = 1.6f;
[SerializeField] private float panelFadeTime = 0.16f;
[SerializeField] private float panelPopScale = 1.06f;
[SerializeField] private float panelPopTime = 0.18f;
[SerializeField] private float noticeShowTime = 2.2f;
[SerializeField] private float finalPanelPopScale = 1.08f;
[SerializeField] private float finalPanelPopTime = 0.22f;
[Header("Slot Visual Settings")]
[Range(0f, 1f)] [SerializeField] private float emptyIconAlpha = 0.35f;
[Range(0f, 1f)] [SerializeField] private float filledIconAlpha = 1f;
[Range(0f, 1f)] [SerializeField] private float emptySlotAlpha = 0.55f;
[Range(0f, 1f)] [SerializeField] private float filledSlotAlpha = 1f;
[SerializeField] private float slotHighlightTime = 0.32f;
[SerializeField] private float slotHighlightScale = 1.08f;
[SerializeField] private Color slotHighlightColor = new Color(0.35f, 1f, 0.9f, 1f);
[Header("Notice UI")]
[SerializeField] private TMP_Text noticeText;
[Header("Final Result UI")]
[SerializeField] private TMP_Text finalResultText;
[Header("Final Result Only Mode")]
[Tooltip("게임 클리어 최종 결과가 뜰 때 진행 UI를 전부 숨기고 FinalResultPanel만 보이게 합니다.")]
[SerializeField] private bool showOnlyFinalResult = true;
[Tooltip("FinalResultPanel을 숨길 때 진행 UI를 다시 켤지 결정합니다. 클리어 후 UI를 완전히 닫는 구조면 Off로 두세요.")]
[SerializeField] private bool restoreGameplayUIAfterFinalResult = true;
[Tooltip("BackgroundPanel. 최종 결과창만 보이고 싶으면 자동 연결하거나 직접 연결하세요.")]
[SerializeField] private GameObject gameplayBackgroundRoot;
[Tooltip("CleanupGauge 루트 오브젝트입니다.")]
[SerializeField] private GameObject cleanupGaugeRoot;
[Header("Item Icons")]
[SerializeField] private FishingItemIconData[] itemIcons;
[Header("Caught Item Random Texts")]
[Tooltip("물고기를 낚았을 때 랜덤으로 표시할 제목 문구입니다.")]
[SerializeField] private string[] fishCaughtMessages =
{
"물고기를 낚았다!",
"작은 물고기가 걸렸다!",
"연못에서 물고기가 올라왔다.",
"평범한 물고기를 낚았다.",
"힘차게 팔딱이는 물고기다!",
"낚싯줄 끝에서 물고기가 반짝였다."
};
[Tooltip("쓰레기를 낚았을 때 랜덤으로 표시할 제목 문구입니다.")]
[SerializeField] private string[] trashCaughtMessages =
{
"쓰레기를 건져냈다!",
"낡은 쓰레기가 걸렸다.",
"연못 속 오염물을 꺼냈다.",
"물고기는 아니지만, 연못은 조금 깨끗해졌다.",
"버려진 쓰레기를 낚아 올렸다.",
"찌 아래에 숨어 있던 쓰레기를 끌어냈다."
};
[Tooltip("기억의 조각을 낚았을 때 랜덤으로 표시할 제목 문구입니다.")]
[SerializeField] private string[] memoryPieceCaughtMessages =
{
"기억의 조각을 되찾았다!",
"빛나는 기억의 조각이 떠올랐다.",
"잊고 있던 기억이 손에 닿았다.",
"연못 속에서 기억의 조각을 발견했다.",
"희미한 빛이 기억의 조각으로 모였다."
};
[Tooltip("물고기를 낚았을 때 랜덤으로 표시할 설명 문구입니다.")]
[SerializeField] private string[] fishHintMessages =
{
"평범한 물고기다.",
"연못에 아직 생명이 남아 있다.",
"작지만 건강해 보인다.",
"특별하진 않지만 나쁘지 않은 수확이다.",
"조용한 연못에서 살아남은 물고기다."
};
[Tooltip("쓰레기를 낚았을 때 랜덤으로 표시할 설명 문구입니다.")]
[SerializeField] private string[] trashHintMessages =
{
"연못이 조금 맑아졌다.",
"이런 것들이 연못을 더럽히고 있었다.",
"하나씩 치우면 물이 맑아질 것이다.",
"기억의 조각에 한 걸음 가까워졌다.",
"연못 아래의 탁한 기운이 조금 걷혔다."
};
[Tooltip("기억의 조각을 낚았을 때 랜덤으로 표시할 설명 문구입니다.")]
[SerializeField] private string[] memoryPieceHintMessages =
{
"잃어버린 기억의 일부다.",
"연못이 맑아지자 모습을 드러냈다.",
"오래전의 기억이 희미하게 느껴진다.",
"이 조각이 무언가를 떠올리게 한다.",
"빛이 손끝에서 조용히 흔들린다."
};
[Header("Guide UI")]
[SerializeField] private bool showControllerGuideOnInitialize = true;
[Header("Text Settings")]
[SerializeField] private string titleDefaultText = "기묘한 낚시터";
[SerializeField] private string objectiveDirtyText = "쓰레기를 건져 연못을 맑게 하자";
[SerializeField] private string objectiveCleanText = "기억의 조각을 찾아보자";
[SerializeField] private string objectiveClearText = "기억의 조각을 획득했다";
[Header("VR Follow")]
[Tooltip("켜면 FishingCanvas가 플레이어 머리 앞을 따라갑니다.")]
[SerializeField] private bool followPlayerHead = true;
[Tooltip("비워두면 Camera.main을 자동으로 사용합니다. XR Origin의 Main Camera를 넣는 것을 추천합니다.")]
[SerializeField] private Transform playerHead;
[Tooltip("실제로 이동시킬 UI 루트입니다. 비워두면 이 FishingGaugeUI가 붙은 오브젝트를 이동합니다.")]
[SerializeField] private Transform followRoot;
[SerializeField] private float followDistance = 1.6f;
[SerializeField] private float followHeightOffset = -0.15f;
[SerializeField] private float followSideOffset = 0f;
[Tooltip("고개를 위아래로 숙여도 UI는 수평으로 유지합니다. VR에서는 켜는 것을 추천합니다.")]
[SerializeField] private bool followYawOnly = true;
[SerializeField] private float followPositionSpeed = 8f;
[SerializeField] private float followRotationSpeed = 10f;
[Tooltip("UI가 카메라와 같은 방향을 바라보게 합니다. 글자가 뒤집히면 이 값을 바꾸거나 Canvas Y Rotation을 180도로 돌리세요.")]
[SerializeField] private bool faceSameDirectionAsHead = true;
private Coroutine resultRoutine;
private Coroutine caughtItemRoutine;
private Coroutine noticeRoutine;
private float lastCleanupFill = -1f;
private void Awake()
{
if (autoBindMissingReferences)
AutoBindMissingReferences();
EnsureEffectReferences();
InitializeFishingUI();
}
private void OnDisable()
{
StopRunningRoutines();
}
private void LateUpdate()
{
UpdateFollowPlayerHead();
}
private void OnValidate()
{
cleanupFillTime = Mathf.Max(0f, cleanupFillTime);
resultShowTime = Mathf.Max(0f, resultShowTime);
resultPopScale = Mathf.Max(1f, resultPopScale);
resultPopTime = Mathf.Max(0.01f, resultPopTime);
caughtItemShowTime = Mathf.Max(0f, caughtItemShowTime);
panelFadeTime = Mathf.Max(0f, panelFadeTime);
panelPopScale = Mathf.Max(1f, panelPopScale);
panelPopTime = Mathf.Max(0.01f, panelPopTime);
noticeShowTime = Mathf.Max(0f, noticeShowTime);
finalPanelPopScale = Mathf.Max(1f, finalPanelPopScale);
finalPanelPopTime = Mathf.Max(0.01f, finalPanelPopTime);
slotHighlightTime = Mathf.Max(0.01f, slotHighlightTime);
slotHighlightScale = Mathf.Max(1f, slotHighlightScale);
emptyIconAlpha = Mathf.Clamp01(emptyIconAlpha);
filledIconAlpha = Mathf.Clamp01(filledIconAlpha);
emptySlotAlpha = Mathf.Clamp01(emptySlotAlpha);
filledSlotAlpha = Mathf.Clamp01(filledSlotAlpha);
followDistance = Mathf.Max(0.2f, followDistance);
followPositionSpeed = Mathf.Max(0.01f, followPositionSpeed);
followRotationSpeed = Mathf.Max(0.01f, followRotationSpeed);
}
private void UpdateFollowPlayerHead()
{
if (!followPlayerHead)
return;
Transform root = followRoot != null ? followRoot : transform;
if (playerHead == null)
{
Camera mainCamera = Camera.main;
if (mainCamera == null)
return;
playerHead = mainCamera.transform;
}
Vector3 forward = playerHead.forward;
Vector3 right = playerHead.right;
if (followYawOnly)
{
forward.y = 0f;
if (forward.sqrMagnitude < 0.0001f)
forward = root.forward;
forward.Normalize();
right = Vector3.Cross(Vector3.up, forward).normalized;
}
Vector3 targetPosition =
playerHead.position +
forward * followDistance +
Vector3.up * followHeightOffset +
right * followSideOffset;
float positionT = 1f - Mathf.Exp(-followPositionSpeed * Time.deltaTime);
root.position = Vector3.Lerp(root.position, targetPosition, positionT);
Quaternion targetRotation;
if (faceSameDirectionAsHead)
{
targetRotation = Quaternion.LookRotation(forward, Vector3.up);
}
else
{
Vector3 lookDirection = root.position - playerHead.position;
if (followYawOnly)
lookDirection.y = 0f;
if (lookDirection.sqrMagnitude < 0.0001f)
lookDirection = forward;
targetRotation = Quaternion.LookRotation(lookDirection.normalized, Vector3.up);
}
float rotationT = 1f - Mathf.Exp(-followRotationSpeed * Time.deltaTime);
root.rotation = Quaternion.Slerp(root.rotation, targetRotation, rotationT);
}
private void EnsureEffectReferences()
{
if (effects == null)
effects = GetComponent<FishingUIEffects>();
if (effects == null && autoCreateEffectsComponent)
effects = gameObject.AddComponent<FishingUIEffects>();
if (effects == null)
return;
if (caughtItemCanvasGroup == null && caughtItemPanel != null)
caughtItemCanvasGroup = effects.EnsureCanvasGroup(caughtItemPanel);
if (controllerGuideCanvasGroup == null && controllerGuidePanel != null)
controllerGuideCanvasGroup = effects.EnsureCanvasGroup(controllerGuidePanel);
if (memoryPieceNoticeCanvasGroup == null && memoryPieceNoticePanel != null)
memoryPieceNoticeCanvasGroup = effects.EnsureCanvasGroup(memoryPieceNoticePanel);
if (finalResultCanvasGroup == null && finalResultPanel != null)
finalResultCanvasGroup = effects.EnsureCanvasGroup(finalResultPanel);
}
[ContextMenu("Auto Bind Reel UI References")]
public void AutoBindMissingReferences()
{
if (backgroundPanel == null) backgroundPanel = FindGameObject("BackgroundPanel");
if (gameplayBackgroundRoot == null) gameplayBackgroundRoot = backgroundPanel;
if (cleanupGaugeRoot == null) cleanupGaugeRoot = FindGameObject("CleanupGauge");
if (counterPanel == null) counterPanel = FindGameObject("CounterPanel");
if (itemSlotPanel == null) itemSlotPanel = FindGameObject("ItemSlotPanel");
if (caughtItemPanel == null) caughtItemPanel = FindGameObject("CaughtItemPanel");
if (controllerGuidePanel == null) controllerGuidePanel = FindGameObject("ControllerGuidePanel");
if (memoryPieceNoticePanel == null) memoryPieceNoticePanel = FindGameObject("MemoryPieceNoticePanel");
if (finalResultPanel == null) finalResultPanel = FindGameObject("FinalResultPanel");
if (titleText == null) titleText = FindComponentByName<TMP_Text>("TitleText");
if (pondStateText == null) pondStateText = FindComponentByName<TMP_Text>("PondStateText");
if (objectiveText == null) objectiveText = FindComponentByName<TMP_Text>("ObjectiveText");
if (catchCountText == null) catchCountText = FindComponentByName<TMP_Text>("CatchCountText", "SuccessText");
if (cleanupProgressText == null) cleanupProgressText = FindComponentByName<TMP_Text>("CleanupProgressText");
if (cleanupGaugeFill == null) cleanupGaugeFill = FindComponentByName<Image>("CleanupGaugeFill", "CleanupGaugeFillImage");
if (cleanupPercentText == null) cleanupPercentText = FindComponentByName<TMP_Text>("CleanupPercentText", "CleanupGaugePercentText");
if (reelProgressGaugeRoot == null) reelProgressGaugeRoot = FindGameObject("ReelProgressGauge", "ReelProgressGroup", "ReelFightProgressGauge");
if (reelProgressGaugeFill == null) reelProgressGaugeFill = FindComponentByName<Image>("ReelProgressGaugeFill", "ReelProgressFill", "ReelFightProgressFill");
if (lineLengthText == null) lineLengthText = FindComponentByName<TMP_Text>("LineLengthText", "Line Length Text", "LineText");
if (resultText == null) resultText = FindComponentByName<TMP_Text>("ResultText");
if (itemIcon == null) itemIcon = FindComponentByName<Image>("ItemIcon");
if (caughtItemText == null) caughtItemText = FindComponentByName<TMP_Text>("CaughtItemText");
if (itemHintText == null) itemHintText = FindComponentByName<TMP_Text>("ItemHintText");
if (fishSlot == null) fishSlot = FindGameObject("FishSlot");
if (fishSlotBackground == null && fishSlot != null) fishSlotBackground = fishSlot.GetComponent<Image>();
if (fishIcon == null) fishIcon = FindComponentByName<Image>("FishIcon");
if (fishCountText == null) fishCountText = FindComponentByName<TMP_Text>("FishCountText");
if (trashSlot == null) trashSlot = FindGameObject("trash Slot", "TrashSlot");
if (trashSlotBackground == null && trashSlot != null) trashSlotBackground = trashSlot.GetComponent<Image>();
if (trashIcon == null) trashIcon = FindComponentByName<Image>("trashIcon", "TrashIcon");
if (trashCountText == null) trashCountText = FindComponentByName<TMP_Text>("trashCountText", "TrashCountText", "CleanupCountText");
if (memorySlot == null) memorySlot = FindGameObject("Memory Slot", "MemorySlot", "MemoryPieceSlot");
if (memorySlotBackground == null && memorySlot != null) memorySlotBackground = memorySlot.GetComponent<Image>();
if (memoryIcon == null) memoryIcon = FindComponentByName<Image>("Memory Icon", "MemoryIcon", "MemoryPieceIcon");
if (memoryPieceCountText == null) memoryPieceCountText = FindComponentByName<TMP_Text>("Memory CountText", "MemoryCountText", "MemoryPieceCountText");
if (compassSlot == null) compassSlot = FindGameObject("CompassSlot", "Compass Slot");
if (compassSlotBackground == null && compassSlot != null) compassSlotBackground = compassSlot.GetComponent<Image>();
if (compassIcon == null) compassIcon = FindComponentByName<Image>("CompassIcon", "Compass Icon");
if (compassCountText == null) compassCountText = FindComponentByName<TMP_Text>("CompassCountText", "Compass CountText");
if (finalResultText == null) finalResultText = FindComponentByName<TMP_Text>("FinalResultText");
if (noticeText == null) noticeText = FindComponentByName<TMP_Text>("NoticeText");
if (caughtItemCanvasGroup == null && caughtItemPanel != null) caughtItemCanvasGroup = caughtItemPanel.GetComponent<CanvasGroup>();
if (controllerGuideCanvasGroup == null && controllerGuidePanel != null) controllerGuideCanvasGroup = controllerGuidePanel.GetComponent<CanvasGroup>();
if (memoryPieceNoticeCanvasGroup == null && memoryPieceNoticePanel != null) memoryPieceNoticeCanvasGroup = memoryPieceNoticePanel.GetComponent<CanvasGroup>();
if (finalResultCanvasGroup == null && finalResultPanel != null) finalResultCanvasGroup = finalResultPanel.GetComponent<CanvasGroup>();
}
public void InitializeFishingUI()
{
StopRunningRoutines();
EnsureEffectReferences();
SetGameplayUIVisible(true);
if (titleText != null && !string.IsNullOrWhiteSpace(titleDefaultText))
titleText.text = titleDefaultText;
ShowCounter();
SetItemSlotPanelVisible(true);
SetControllerGuideVisible(showControllerGuideOnInitialize, true);
HideRoundResult();
HideCaughtItem(true);
HideNotice(true);
HideFinalResult(false, true);
UpdateInventoryUI(0, 0, 0);
lastCleanupFill = -1f;
SetCleanupFill(0f, true);
SetSimpleReelUIVisible(false);
}
private void StopRunningRoutines()
{
if (resultRoutine != null)
{
StopCoroutine(resultRoutine);
resultRoutine = null;
}
if (caughtItemRoutine != null)
{
StopCoroutine(caughtItemRoutine);
caughtItemRoutine = null;
}
if (noticeRoutine != null)
{
StopCoroutine(noticeRoutine);
noticeRoutine = null;
}
}
public void SetSimpleReelUIVisible(bool visible)
{
if (reelProgressGaugeRoot != null)
reelProgressGaugeRoot.SetActive(visible);
if (lineLengthText != null)
lineLengthText.gameObject.SetActive(visible);
}
public void UpdateSimpleReelUI(float progress, float lineLength, bool isReeling)
{
progress = Mathf.Clamp01(progress);
if (reelProgressGaugeFill != null)
reelProgressGaugeFill.fillAmount = progress;
if (lineLengthText != null)
{
string stateText = isReeling ? simpleReelReelingText : simpleReelIdleText;
lineLengthText.text = $"줄 길이 {lineLength:0.00}m\n진행도 {Mathf.RoundToInt(progress * 100f)}%\n{stateText}";
}
if (objectiveText != null && !string.IsNullOrWhiteSpace(simpleReelObjectiveText))
objectiveText.text = simpleReelObjectiveText;
}
public void UpdateFishingProgress(int cleanupItemCount, int cleanupTarget, bool pondCleaned, bool memoryPieceCollected, int totalCaughtItems)
{
cleanupTarget = Mathf.Max(1, cleanupTarget);
float progress = Mathf.Clamp01((float)cleanupItemCount / cleanupTarget);
if (catchCountText != null)
catchCountText.text = $"획득 {totalCaughtItems}";
if (cleanupProgressText != null)
cleanupProgressText.text = pondCleaned ? "연못 정화 완료" : $"정화 {cleanupItemCount}/{cleanupTarget}";
if (pondStateText != null)
{
if (memoryPieceCollected)
pondStateText.text = "기억의 조각을 찾았다";
else if (pondCleaned)
pondStateText.text = "맑아진 연못";
else
pondStateText.text = "오염된 연못";
}
if (objectiveText != null)
{
if (memoryPieceCollected)
objectiveText.text = objectiveClearText;
else if (pondCleaned)
objectiveText.text = objectiveCleanText;
else
objectiveText.text = objectiveDirtyText;
}
SetCleanupFill(progress, false);
}
private void SetCleanupFill(float progress, bool instant)
{
progress = Mathf.Clamp01(progress);
if (cleanupGaugeFill != null)
{
if (instant || !animateCleanupGauge || effects == null || lastCleanupFill < 0f)
cleanupGaugeFill.fillAmount = progress;
else if (!Mathf.Approximately(lastCleanupFill, progress))
effects.FillImage(cleanupGaugeFill, progress, cleanupFillTime);
}
if (cleanupPercentText != null)
cleanupPercentText.text = $"{Mathf.RoundToInt(progress * 100f)}%";
lastCleanupFill = progress;
}
public void UpdateInventoryUI(int fishCount, int trashCount, int memoryPieceCount)
{
if (fishCountText != null) fishCountText.text = fishCount.ToString();
if (trashCountText != null) trashCountText.text = trashCount.ToString();
if (memoryPieceCountText != null) memoryPieceCountText.text = memoryPieceCount.ToString();
ApplySlotState(fishSlot, fishSlotBackground, fishIcon, fishCount > 0);
ApplySlotState(trashSlot, trashSlotBackground, trashIcon, trashCount > 0);
ApplySlotState(memorySlot, memorySlotBackground, memoryIcon, memoryPieceCount > 0);
// 이제 낚시 아이템은 물고기 / 쓰레기 / 기억의 조각 3종만 사용합니다.
// 기존 Compass 슬롯이 하이어라키에 남아 있으면 자동으로 숨깁니다.
if (compassSlot != null)
compassSlot.SetActive(false);
}
public void HighlightSlotForItem(FishingItemType itemType)
{
switch (itemType)
{
case FishingItemType.Fish:
StartSlotHighlight(fishSlotBackground, fishIcon);
break;
case FishingItemType.Trash:
StartSlotHighlight(trashSlotBackground, trashIcon);
break;
case FishingItemType.MemoryPiece:
StartSlotHighlight(memorySlotBackground, memoryIcon);
break;
}
}
private void StartSlotHighlight(Image background, Image icon)
{
if (background != null)
StartCoroutine(SimpleSlotHighlightRoutine(background, icon));
else if (icon != null && effects != null)
effects.Pop(icon.transform, slotHighlightScale, slotHighlightTime);
}
private IEnumerator SimpleSlotHighlightRoutine(Image background, Image icon)
{
Color originalBackgroundColor = background.color;
Color originalIconColor = icon != null ? icon.color : Color.white;
background.color = slotHighlightColor;
if (icon != null)
{
Color highlightedIcon = icon.color;
highlightedIcon.a = filledIconAlpha;
icon.color = highlightedIcon;
}
if (effects != null)
{
effects.Pop(background.transform, slotHighlightScale, slotHighlightTime);
if (icon != null)
effects.Pop(icon.transform, slotHighlightScale, slotHighlightTime);
}
yield return new WaitForSeconds(slotHighlightTime);
background.color = originalBackgroundColor;
if (icon != null)
icon.color = originalIconColor;
}
private void ApplySlotState(GameObject slotRoot, Image slotBackground, Image icon, bool hasItem)
{
if (slotRoot != null)
slotRoot.SetActive(true);
SetImageAlpha(slotBackground, hasItem ? filledSlotAlpha : emptySlotAlpha);
SetImageAlpha(icon, hasItem ? filledIconAlpha : emptyIconAlpha);
}
private void SetImageAlpha(Image image, float alpha)
{
if (image == null)
return;
Color color = image.color;
color.a = Mathf.Clamp01(alpha);
image.color = color;
}
public void ShowCounter()
{
SetCounterVisible(true);
}
public void HideCounter()
{
SetCounterVisible(false);
}
public void SetCounterVisible(bool visible)
{
if (counterPanel != null)
counterPanel.SetActive(visible);
}
public void SetItemSlotPanelVisible(bool visible)
{
if (itemSlotPanel != null)
itemSlotPanel.SetActive(visible);
}
public void ShowResult(string text)
{
ShowResult(text, Color.white);
}
public void ShowResult(string text, Color color)
{
if (resultRoutine != null)
StopCoroutine(resultRoutine);
resultRoutine = StartCoroutine(ResultRoutine(text, color));
}
public void ShowPersistentResult(string text)
{
ShowPersistentResult(text, Color.white);
}
public void ShowPersistentResult(string text, Color color)
{
if (resultRoutine != null)
{
StopCoroutine(resultRoutine);
resultRoutine = null;
}
if (resultText != null)
{
resultText.gameObject.SetActive(true);
resultText.text = text;
resultText.color = color;
if (effects != null)
effects.Pop(resultText.transform, resultPopScale, resultPopTime);
}
}
private IEnumerator ResultRoutine(string text, Color color)
{
if (resultText == null)
yield break;
resultText.gameObject.SetActive(true);
resultText.text = text;
resultText.color = color;
if (effects != null)
effects.Pop(resultText.transform, resultPopScale, resultPopTime);
if (resultShowTime > 0f)
yield return new WaitForSeconds(resultShowTime);
else
yield return null;
resultText.gameObject.SetActive(false);
resultRoutine = null;
}
public void ShowCaughtItem(string text)
{
ShowCaughtItem(FishingItemType.None, text, null);
}
public void ShowCaughtItem(FishingItemType itemType, string displayName, bool countsAsCleanupItem, string extraMessage = null)
{
string caughtMessage = GetCaughtMessage(itemType, displayName);
string defaultHint = GetDefaultHint(itemType, countsAsCleanupItem);
string hintMessage = CombineHintMessage(defaultHint, extraMessage);
ShowCaughtItem(itemType, caughtMessage, hintMessage);
}
private void ShowCaughtItem(FishingItemType itemType, string caughtMessage, string hintMessage)
{
if (caughtItemText == null && resultText != null)
{
ShowResult(caughtMessage);
return;
}
if (caughtItemRoutine != null)
StopCoroutine(caughtItemRoutine);
caughtItemRoutine = StartCoroutine(CaughtItemRoutine(itemType, caughtMessage, hintMessage));
}
private IEnumerator CaughtItemRoutine(FishingItemType itemType, string caughtMessage, string hintMessage)
{
if (caughtItemPanel != null)
caughtItemPanel.SetActive(true);
if (caughtItemCanvasGroup != null)
{
caughtItemCanvasGroup.alpha = 0f;
caughtItemCanvasGroup.interactable = false;
caughtItemCanvasGroup.blocksRaycasts = false;
}
if (caughtItemText != null)
{
caughtItemText.gameObject.SetActive(true);
caughtItemText.text = caughtMessage;
}
ApplyItemIcon(itemType);
if (itemHintText != null)
{
itemHintText.gameObject.SetActive(!string.IsNullOrWhiteSpace(hintMessage));
itemHintText.text = hintMessage ?? string.Empty;
}
if (effects != null)
{
effects.FadeCanvasGroup(caughtItemPanel, caughtItemCanvasGroup, true, panelFadeTime);
if (caughtItemPanel != null)
effects.Pop(caughtItemPanel.transform, panelPopScale, panelPopTime);
}
else if (caughtItemCanvasGroup != null)
{
caughtItemCanvasGroup.alpha = 1f;
}
if (caughtItemShowTime > 0f)
yield return new WaitForSeconds(caughtItemShowTime);
else
yield return null;
if (effects != null)
effects.FadeCanvasGroup(caughtItemPanel, caughtItemCanvasGroup, false, panelFadeTime);
else if (caughtItemPanel != null)
caughtItemPanel.SetActive(false);
if (caughtItemText != null)
caughtItemText.gameObject.SetActive(false);
if (itemHintText != null)
itemHintText.gameObject.SetActive(false);
caughtItemRoutine = null;
}
public void HideCaughtItem()
{
HideCaughtItem(false);
}
private void HideCaughtItem(bool instant)
{
if (caughtItemRoutine != null)
{
StopCoroutine(caughtItemRoutine);
caughtItemRoutine = null;
}
if (instant || effects == null)
{
if (caughtItemPanel != null)
caughtItemPanel.SetActive(false);
if (caughtItemCanvasGroup != null)
{
caughtItemCanvasGroup.alpha = 0f;
caughtItemCanvasGroup.interactable = false;
caughtItemCanvasGroup.blocksRaycasts = false;
}
}
else
{
effects.FadeCanvasGroup(caughtItemPanel, caughtItemCanvasGroup, false, panelFadeTime);
}
if (caughtItemText != null)
caughtItemText.gameObject.SetActive(false);
if (itemHintText != null)
itemHintText.gameObject.SetActive(false);
}
private void ApplyItemIcon(FishingItemType itemType)
{
if (itemIcon == null)
return;
FishingItemIconData iconData = GetIconData(itemType);
itemIcon.sprite = iconData != null ? iconData.icon : null;
itemIcon.enabled = itemIcon.sprite != null;
if (itemIcon.enabled && effects != null)
effects.Pop(itemIcon.transform, 1.12f, 0.18f);
}
private FishingItemIconData GetIconData(FishingItemType itemType)
{
if (itemIcons == null)
return null;
for (int i = 0; i < itemIcons.Length; i++)
{
if (itemIcons[i] != null && itemIcons[i].itemType == itemType)
return itemIcons[i];
}
return null;
}
private string GetCaughtMessage(FishingItemType itemType, string displayName)
{
switch (itemType)
{
case FishingItemType.Fish:
return PickRandomText(fishCaughtMessages, "물고기를 낚았다!");
case FishingItemType.Trash:
return PickRandomText(trashCaughtMessages, "쓰레기를 건져냈다!");
case FishingItemType.MemoryPiece:
return PickRandomText(memoryPieceCaughtMessages, "기억의 조각을 되찾았다!");
case FishingItemType.None:
default:
return string.IsNullOrWhiteSpace(displayName) ? "무언가를 낚았다!" : displayName;
}
}
private string GetDefaultHint(FishingItemType itemType, bool countsAsCleanupItem)
{
FishingItemIconData iconData = GetIconData(itemType);
string iconHint = iconData != null ? iconData.hintText : null;
switch (itemType)
{
case FishingItemType.Fish:
return PickRandomText(fishHintMessages, string.IsNullOrWhiteSpace(iconHint) ? "평범한 물고기다." : iconHint);
case FishingItemType.Trash:
return PickRandomText(trashHintMessages, string.IsNullOrWhiteSpace(iconHint) ? "연못이 조금 맑아졌다." : iconHint);
case FishingItemType.MemoryPiece:
return PickRandomText(memoryPieceHintMessages, string.IsNullOrWhiteSpace(iconHint) ? "잃어버린 기억의 일부다." : iconHint);
default:
if (!string.IsNullOrWhiteSpace(iconHint))
return iconHint;
return countsAsCleanupItem ? "연못이 조금 맑아졌다." : string.Empty;
}
}
private string CombineHintMessage(string defaultHint, string extraMessage)
{
bool hasDefault = !string.IsNullOrWhiteSpace(defaultHint);
bool hasExtra = !string.IsNullOrWhiteSpace(extraMessage);
if (hasDefault && hasExtra)
return $"{defaultHint}\n{extraMessage}";
if (hasDefault)
return defaultHint;
if (hasExtra)
return extraMessage;
return string.Empty;
}
private string PickRandomText(string[] messages, string fallback)
{
if (messages == null || messages.Length == 0)
return fallback;
int validCount = 0;
for (int i = 0; i < messages.Length; i++)
{
if (!string.IsNullOrWhiteSpace(messages[i]))
validCount++;
}
if (validCount == 0)
return fallback;
int targetIndex = UnityEngine.Random.Range(0, validCount);
int currentIndex = 0;
for (int i = 0; i < messages.Length; i++)
{
if (string.IsNullOrWhiteSpace(messages[i]))
continue;
if (currentIndex == targetIndex)
return messages[i];
currentIndex++;
}
return fallback;
}
public void ShowNotice(string message)
{
if (noticeRoutine != null)
StopCoroutine(noticeRoutine);
noticeRoutine = StartCoroutine(NoticeRoutine(message));
}
private IEnumerator NoticeRoutine(string message)
{
if (memoryPieceNoticePanel != null)
memoryPieceNoticePanel.SetActive(true);
if (memoryPieceNoticeCanvasGroup != null)
{
memoryPieceNoticeCanvasGroup.alpha = 0f;
memoryPieceNoticeCanvasGroup.interactable = false;
memoryPieceNoticeCanvasGroup.blocksRaycasts = false;
}
if (noticeText != null)
{
noticeText.gameObject.SetActive(true);
noticeText.text = message;
}
if (effects != null)
{
effects.FadeCanvasGroup(memoryPieceNoticePanel, memoryPieceNoticeCanvasGroup, true, panelFadeTime);
if (memoryPieceNoticePanel != null)
effects.Pop(memoryPieceNoticePanel.transform, panelPopScale, panelPopTime);
}
else if (memoryPieceNoticeCanvasGroup != null)
{
memoryPieceNoticeCanvasGroup.alpha = 1f;
}
if (noticeShowTime > 0f)
yield return new WaitForSeconds(noticeShowTime);
else
yield return null;
if (effects != null)
effects.FadeCanvasGroup(memoryPieceNoticePanel, memoryPieceNoticeCanvasGroup, false, panelFadeTime);
else if (memoryPieceNoticePanel != null)
memoryPieceNoticePanel.SetActive(false);
if (noticeText != null)
noticeText.gameObject.SetActive(false);
noticeRoutine = null;
}
public void HideNotice()
{
HideNotice(false);
}
private void HideNotice(bool instant)
{
if (noticeRoutine != null)
{
StopCoroutine(noticeRoutine);
noticeRoutine = null;
}
if (instant || effects == null)
{
if (memoryPieceNoticePanel != null)
memoryPieceNoticePanel.SetActive(false);
if (memoryPieceNoticeCanvasGroup != null)
{
memoryPieceNoticeCanvasGroup.alpha = 0f;
memoryPieceNoticeCanvasGroup.interactable = false;
memoryPieceNoticeCanvasGroup.blocksRaycasts = false;
}
}
else
{
effects.FadeCanvasGroup(memoryPieceNoticePanel, memoryPieceNoticeCanvasGroup, false, panelFadeTime);
}
if (noticeText != null)
noticeText.gameObject.SetActive(false);
}
public void HideControllerGuide()
{
SetControllerGuideVisible(false, false);
}
public void SetControllerGuideVisible(bool visible)
{
SetControllerGuideVisible(visible, false);
}
private void SetControllerGuideVisible(bool visible, bool instant)
{
if (effects != null && !instant)
{
effects.FadeCanvasGroup(controllerGuidePanel, controllerGuideCanvasGroup, visible, panelFadeTime);
}
else
{
if (controllerGuidePanel != null)
controllerGuidePanel.SetActive(visible);
if (controllerGuideCanvasGroup != null)
{
controllerGuideCanvasGroup.alpha = visible ? 1f : 0f;
controllerGuideCanvasGroup.interactable = visible;
controllerGuideCanvasGroup.blocksRaycasts = visible;
}
}
}
public void HideRoundResult()
{
if (resultRoutine != null)
{
StopCoroutine(resultRoutine);
resultRoutine = null;
}
if (resultText != null)
resultText.gameObject.SetActive(false);
}
private void SetGameplayUIVisible(bool visible)
{
SetGameObjectVisible(gameplayBackgroundRoot, visible);
if (titleText != null)
titleText.gameObject.SetActive(visible);
if (pondStateText != null)
pondStateText.gameObject.SetActive(visible);
if (objectiveText != null)
objectiveText.gameObject.SetActive(visible);
SetGameObjectVisible(counterPanel, visible);
SetGameObjectVisible(itemSlotPanel, visible);
SetGameObjectVisible(cleanupGaugeRoot, visible);
SetSimpleReelUIVisible(visible);
SetControllerGuideVisible(visible && showControllerGuideOnInitialize, true);
if (!visible)
{
HideRoundResult();
HideCaughtItem(true);
HideNotice(true);
}
}
private void SetGameObjectVisible(GameObject target, bool visible)
{
if (target != null)
target.SetActive(visible);
}
public void ShowFinalResult(string text)
{
HideRoundResult();
HideCaughtItem(true);
HideNotice(true);
if (showOnlyFinalResult)
SetGameplayUIVisible(false);
else
SetControllerGuideVisible(false, true);
if (finalResultPanel != null)
finalResultPanel.SetActive(true);
if (finalResultCanvasGroup != null)
{
finalResultCanvasGroup.alpha = 0f;
finalResultCanvasGroup.interactable = true;
finalResultCanvasGroup.blocksRaycasts = true;
}
if (finalResultText != null)
{
finalResultText.gameObject.SetActive(true);
finalResultText.text = text;
}
if (effects != null)
{
effects.FadeCanvasGroup(finalResultPanel, finalResultCanvasGroup, true, panelFadeTime);
if (finalResultPanel != null)
effects.Pop(finalResultPanel.transform, finalPanelPopScale, finalPanelPopTime);
}
else if (finalResultCanvasGroup != null)
{
finalResultCanvasGroup.alpha = 1f;
}
}
public void HideFinalResult()
{
HideFinalResult(true, false);
}
public void HideFinalResult(bool showGameplayAfterHide)
{
HideFinalResult(showGameplayAfterHide, false);
}
private void HideFinalResult(bool showGameplayAfterHide, bool instant)
{
if (instant || effects == null)
{
if (finalResultPanel != null)
finalResultPanel.SetActive(false);
if (finalResultCanvasGroup != null)
{
finalResultCanvasGroup.alpha = 0f;
finalResultCanvasGroup.interactable = false;
finalResultCanvasGroup.blocksRaycasts = false;
}
}
else
{
effects.FadeCanvasGroup(finalResultPanel, finalResultCanvasGroup, false, panelFadeTime);
}
if (finalResultText != null)
finalResultText.gameObject.SetActive(false);
if (showGameplayAfterHide && showOnlyFinalResult && restoreGameplayUIAfterFinalResult)
SetGameplayUIVisible(true);
}
private GameObject FindGameObject(params string[] names)
{
Transform found = FindTransformByName(transform, names);
return found != null ? found.gameObject : null;
}
private T FindComponentByName<T>(params string[] names) where T : Component
{
Transform found = FindTransformByName(transform, names);
return found != null ? found.GetComponent<T>() : null;
}
private Transform FindTransformByName(Transform root, params string[] names)
{
if (root == null || names == null)
return null;
for (int i = 0; i < names.Length; i++)
{
Transform exact = FindTransformRecursive(root, names[i], false);
if (exact != null)
return exact;
}
for (int i = 0; i < names.Length; i++)
{
Transform normalized = FindTransformRecursive(root, names[i], true);
if (normalized != null)
return normalized;
}
return null;
}
private Transform FindTransformRecursive(Transform current, string targetName, bool normalize)
{
if (current == null || string.IsNullOrEmpty(targetName))
return null;
string currentName = normalize ? NormalizeName(current.name) : current.name;
string target = normalize ? NormalizeName(targetName) : targetName;
if (currentName == target)
return current;
for (int i = 0; i < current.childCount; i++)
{
Transform found = FindTransformRecursive(current.GetChild(i), targetName, normalize);
if (found != null)
return found;
}
return null;
}
private string NormalizeName(string value)
{
return value.Replace(" ", string.Empty)
.Replace("_", string.Empty)
.Replace("-", string.Empty)
.ToLowerInvariant();
}
}