Files
WhaleAdventure_VR/Assets/My project/Fishing Scripts/UI/FishingGameManager.cs
2026-06-25 17:39:42 +09:00

766 lines
22 KiB
C#

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class FishingGameManager : MonoBehaviour
{
public enum ResultType
{
Perfect,
Good,
Miss
}
[Header("Auto Bind")]
[Tooltip("현재 만든 Prefab 구조 기준으로 비어 있는 참조를 자동 연결합니다.")]
[SerializeField] private bool autoBindMissingReferences = true;
[Header("References")]
[Tooltip("낚시 UI 전체 루트입니다. UI를 자동으로 켜고 끄고 싶을 때만 연결하세요.")]
[SerializeField] private GameObject uiRoot;
[SerializeField] private FishingGaugeUI ui;
[SerializeField] private FishingRewardSystem rewardSystem;
[SerializeField] private FishingHapticManager haptic;
[SerializeField] private RotateUI centerIconRotate;
[Header("XR Controller Input")]
[Tooltip("낚시 판정을 실행할 컨트롤러 입력입니다. 예: XRI Right Interaction / Select")]
[SerializeField] private InputActionReference submitAction;
[Tooltip("낚시를 다시 시작할 컨트롤러 입력입니다. 필요 없으면 비워둬도 됩니다.")]
[SerializeField] private InputActionReference resetAction;
[Tooltip("씬에 Input Action Manager가 없거나 액션이 자동 활성화되지 않으면 켜두세요.")]
[SerializeField] private bool enableInputActionsManually = true;
[Header("Pointer")]
[SerializeField] private float pointerAngle;
[SerializeField] private float pointerSpeed = 180f;
[SerializeField] private float minSpeed = 180f;
[SerializeField] private float maxSpeed = 450f;
[Header("Zone")]
[SerializeField] private float startZoneSize = 90f;
[SerializeField] private float zoneSize = 90f;
[SerializeField] private float minZoneSize = 30f;
[SerializeField] private float maxZoneSize = 120f;
[SerializeField] private float zoneCenter;
[Header("Catch Rules")]
[Tooltip("아이템 1개를 낚기 위해 필요한 성공 횟수입니다.")]
[SerializeField] private int requiredSuccesses = 3;
[Tooltip("아이템 1개를 낚는 동안 허용되는 실패 횟수입니다. 초과하면 낚싯줄이 끊어진 것으로 처리합니다.")]
[SerializeField] private int allowedFails = 3;
[Header("Pond Cleanup Rules")]
[Tooltip("연못을 맑게 만들기 위해 필요한 쓰레기성 아이템 수입니다. 쓰레기, 병, 봉지가 여기에 포함됩니다.")]
[SerializeField] private int requiredCleanupItems = 3;
[Tooltip("연못이 맑아진 뒤 다음 성공 낚시에서 기억의 조각을 확정으로 낚습니다.")]
[SerializeField] private bool guaranteeMemoryPieceAfterCleaned = true;
[Tooltip("StartFishing을 호출할 때 쓰레기 수거/연못 상태를 초기화합니다.")]
[SerializeField] private bool resetPondStateOnStart = true;
[Header("Round Settings")]
[SerializeField] private float nextRoundDelay = 0.35f;
[SerializeField] private float nextCatchDelay = 2.0f;
[SerializeField] private bool randomDirectionEachRound = true;
[SerializeField] private bool resetDifficultyEachCatch = true;
[SerializeField] private bool startOnAwake = true;
[Header("Final Result Settings")]
[Tooltip("최종 결과를 잠깐 보여준 뒤 uiRoot를 자동으로 끕니다. uiRoot가 비어 있으면 동작하지 않습니다.")]
[SerializeField] private bool hideUIRootAfterFinalResult = false;
[SerializeField] private float finalResultShowTime = 1.5f;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private int successCount;
private int failCount;
private int cleanupItemCount;
private int totalCaughtItems;
// 임시 낚시 세션 카운트입니다. 나중에 공용 인벤토리와 연결할 때는 저장용으로 쓰지 말고 UI 표시용으로만 쓰세요.
private int sessionFishCount;
private int sessionTrashCount;
private int sessionMemoryPieceCount;
private int sessionCompassCount;
private bool clockwise = true;
private bool activeGame;
private bool inputLocked;
private bool pondCleaned;
private bool memoryPieceCollected;
private bool submitActionWasEnabled;
private bool resetActionWasEnabled;
private Coroutine nextRoundRoutine;
private Coroutine nextCatchRoutine;
private Coroutine finalResultRoutine;
public bool IsActiveGame => activeGame;
public bool IsPondCleaned => pondCleaned;
public bool IsMemoryPieceCollected => memoryPieceCollected;
public int SuccessCount => successCount;
public int FailCount => failCount;
public int CleanupItemCount => cleanupItemCount;
public int RequiredCleanupItems => requiredCleanupItems;
public int TotalCaughtItems => totalCaughtItems;
public int SessionFishCount => sessionFishCount;
public int SessionTrashCount => sessionTrashCount;
public int SessionMemoryPieceCount => sessionMemoryPieceCount;
public int SessionCompassCount => sessionCompassCount;
public event Action<FishingItemType, int> ItemCaught;
private void OnEnable()
{
RegisterInputAction(submitAction, OnSubmitAction, ref submitActionWasEnabled);
RegisterInputAction(resetAction, OnResetAction, ref resetActionWasEnabled);
}
private void OnDisable()
{
UnregisterInputAction(submitAction, OnSubmitAction, submitActionWasEnabled);
UnregisterInputAction(resetAction, OnResetAction, resetActionWasEnabled);
}
private void Start()
{
if (autoBindMissingReferences)
AutoBindMissingReferences();
ValidateRuntimeSettings();
if (startOnAwake)
{
StartFishing();
}
else
{
InitializeIdleUI();
}
}
private void Update()
{
if (!activeGame)
return;
if (inputLocked)
return;
RotatePointer();
}
private void OnValidate()
{
ValidateRuntimeSettings();
}
[ContextMenu("Auto Bind Fishing References")]
public void AutoBindMissingReferences()
{
Transform searchRoot = GetSearchRoot();
if (uiRoot == null)
{
Transform canvasTransform = FindTransformRecursive(searchRoot, "FishingCanvas");
if (canvasTransform != null)
uiRoot = canvasTransform.gameObject;
}
if (ui == null)
ui = searchRoot != null ? searchRoot.GetComponentInChildren<FishingGaugeUI>(true) : GetComponentInChildren<FishingGaugeUI>(true);
if (rewardSystem == null)
rewardSystem = searchRoot != null ? searchRoot.GetComponentInChildren<FishingRewardSystem>(true) : GetComponentInChildren<FishingRewardSystem>(true);
if (haptic == null)
haptic = searchRoot != null ? searchRoot.GetComponentInChildren<FishingHapticManager>(true) : GetComponentInChildren<FishingHapticManager>(true);
if (centerIconRotate == null)
{
Transform centerIconTransform = FindTransformRecursive(searchRoot, "CenterIcon");
if (centerIconTransform != null)
centerIconRotate = centerIconTransform.GetComponent<RotateUI>();
if (centerIconRotate == null && searchRoot != null)
centerIconRotate = searchRoot.GetComponentInChildren<RotateUI>(true);
}
}
private Transform GetSearchRoot()
{
Transform current = transform;
while (current.parent != null)
current = current.parent;
return current;
}
private Transform FindTransformRecursive(Transform current, string targetName)
{
if (current == null || string.IsNullOrEmpty(targetName))
return null;
if (NormalizeName(current.name) == NormalizeName(targetName))
return current;
for (int i = 0; i < current.childCount; i++)
{
Transform found = FindTransformRecursive(current.GetChild(i), targetName);
if (found != null)
return found;
}
return null;
}
private string NormalizeName(string value)
{
return value.Replace(" ", string.Empty)
.Replace("_", string.Empty)
.Replace("-", string.Empty)
.ToLowerInvariant();
}
private void RegisterInputAction(InputActionReference actionReference, System.Action<InputAction.CallbackContext> callback, ref bool wasEnabled)
{
if (actionReference == null || actionReference.action == null)
return;
wasEnabled = actionReference.action.enabled;
actionReference.action.performed += callback;
if (enableInputActionsManually && !wasEnabled)
actionReference.action.Enable();
}
private void UnregisterInputAction(InputActionReference actionReference, System.Action<InputAction.CallbackContext> callback, bool wasEnabled)
{
if (actionReference == null || actionReference.action == null)
return;
actionReference.action.performed -= callback;
if (enableInputActionsManually && !wasEnabled)
actionReference.action.Disable();
}
private void ValidateRuntimeSettings()
{
requiredSuccesses = Mathf.Max(1, requiredSuccesses);
allowedFails = Mathf.Max(1, allowedFails);
requiredCleanupItems = Mathf.Max(1, requiredCleanupItems);
minSpeed = Mathf.Max(1f, minSpeed);
maxSpeed = Mathf.Max(minSpeed, maxSpeed);
pointerSpeed = Mathf.Clamp(pointerSpeed, minSpeed, maxSpeed);
minZoneSize = Mathf.Clamp(minZoneSize, 1f, 360f);
maxZoneSize = Mathf.Clamp(maxZoneSize, minZoneSize, 360f);
startZoneSize = Mathf.Clamp(startZoneSize, minZoneSize, maxZoneSize);
zoneSize = Mathf.Clamp(zoneSize, minZoneSize, maxZoneSize);
nextRoundDelay = Mathf.Max(0f, nextRoundDelay);
nextCatchDelay = Mathf.Max(0f, nextCatchDelay);
finalResultShowTime = Mathf.Max(0f, finalResultShowTime);
pointerAngle = Normalize(pointerAngle);
zoneCenter = Normalize(zoneCenter);
cleanupItemCount = Mathf.Max(0, cleanupItemCount);
totalCaughtItems = Mathf.Max(0, totalCaughtItems);
}
private void InitializeIdleUI()
{
if (ui != null)
{
ui.InitializeFishingUI();
ui.UpdateCounter(0, requiredSuccesses, 0, allowedFails);
ui.UpdateFishingProgress(cleanupItemCount, requiredCleanupItems, pondCleaned, memoryPieceCollected, totalCaughtItems);
ui.UpdateInventoryUI(sessionFishCount, sessionTrashCount, sessionMemoryPieceCount, sessionCompassCount);
ui.SetPointerRotation(0f);
ui.SetZone(0f, startZoneSize);
}
if (centerIconRotate != null)
centerIconRotate.StopRotate();
}
public void StartFishing()
{
ValidateRuntimeSettings();
StopRunningRoutines();
if (uiRoot != null)
uiRoot.SetActive(true);
if (resetPondStateOnStart)
ResetPondState();
if (ui != null)
ui.InitializeFishingUI();
StartNewCatchAttempt();
if (showDebugLog)
Debug.Log("기묘한 낚시터 시작");
}
public void StopFishing(bool hideUI = false)
{
StopRunningRoutines();
activeGame = false;
inputLocked = true;
if (centerIconRotate != null)
centerIconRotate.StopRotate();
if (hideUI && uiRoot != null)
uiRoot.SetActive(false);
}
public void ResetFishing()
{
StartFishing();
}
public void ResetPondState()
{
successCount = 0;
failCount = 0;
cleanupItemCount = 0;
totalCaughtItems = 0;
sessionFishCount = 0;
sessionTrashCount = 0;
sessionMemoryPieceCount = 0;
sessionCompassCount = 0;
pondCleaned = false;
memoryPieceCollected = false;
}
private void StartNewCatchAttempt()
{
successCount = 0;
failCount = 0;
inputLocked = false;
activeGame = true;
if (resetDifficultyEachCatch)
{
pointerAngle = 0f;
pointerSpeed = minSpeed;
zoneSize = startZoneSize;
}
clockwise = UnityEngine.Random.value > 0.5f;
if (centerIconRotate != null)
{
centerIconRotate.ResetRotation();
centerIconRotate.StartRotate();
}
RandomizeZone();
UpdateUI();
}
private void StopRunningRoutines()
{
if (nextRoundRoutine != null)
{
StopCoroutine(nextRoundRoutine);
nextRoundRoutine = null;
}
if (nextCatchRoutine != null)
{
StopCoroutine(nextCatchRoutine);
nextCatchRoutine = null;
}
if (finalResultRoutine != null)
{
StopCoroutine(finalResultRoutine);
finalResultRoutine = null;
}
}
private void RotatePointer()
{
float dir = clockwise ? 1f : -1f;
pointerAngle += dir * pointerSpeed * Time.deltaTime;
pointerAngle = Normalize(pointerAngle);
if (ui != null)
ui.SetPointerRotation(pointerAngle);
}
public void SubmitAttempt()
{
if (!activeGame)
return;
if (inputLocked)
return;
inputLocked = true;
if (ui != null)
ui.HideControllerGuide();
ResultType result = EvaluateResult();
if (ui != null)
{
ui.ShowResultForType(result);
ui.FlashFeedbackForType(result);
}
switch (result)
{
case ResultType.Perfect:
OnPerfect();
break;
case ResultType.Good:
OnGood();
break;
case ResultType.Miss:
OnMiss();
break;
}
if (successCount >= requiredSuccesses)
{
CatchItemSuccess();
return;
}
if (failCount >= allowedFails)
{
FishingFail();
return;
}
nextRoundRoutine = StartCoroutine(NextRoundRoutine());
}
private ResultType EvaluateResult()
{
float delta = Mathf.Abs(Mathf.DeltaAngle(pointerAngle, zoneCenter));
float perfectRange = zoneSize * 0.2f;
float goodRange = zoneSize * 0.5f;
if (delta <= perfectRange)
return ResultType.Perfect;
if (delta <= goodRange)
return ResultType.Good;
return ResultType.Miss;
}
private void OnPerfect()
{
successCount++;
zoneSize *= 0.9f;
pointerSpeed *= 1.05f;
if (haptic != null)
haptic.Perfect();
if (showDebugLog)
Debug.Log("낚시 판정: Perfect");
ClampDifficulty();
UpdateUI();
}
private void OnGood()
{
successCount++;
zoneSize *= 0.95f;
pointerSpeed *= 1.02f;
if (haptic != null)
haptic.Good();
if (showDebugLog)
Debug.Log("낚시 판정: Good");
ClampDifficulty();
UpdateUI();
}
private void OnMiss()
{
failCount++;
zoneSize *= 1.05f;
pointerSpeed *= 0.95f;
if (haptic != null)
haptic.Miss();
if (showDebugLog)
Debug.Log("낚시 판정: Miss");
ClampDifficulty();
UpdateUI();
}
private IEnumerator NextRoundRoutine()
{
yield return new WaitForSeconds(nextRoundDelay);
if (!activeGame)
{
nextRoundRoutine = null;
yield break;
}
if (randomDirectionEachRound)
clockwise = UnityEngine.Random.value > 0.5f;
RandomizeZone();
UpdateUI();
inputLocked = false;
nextRoundRoutine = null;
}
private void CatchItemSuccess()
{
activeGame = false;
inputLocked = true;
if (centerIconRotate != null)
centerIconRotate.StopRotate();
bool forceMemoryPiece = pondCleaned && guaranteeMemoryPieceAfterCleaned && !memoryPieceCollected;
FishingRewardSystem.CatchResult catchResult;
if (rewardSystem != null)
catchResult = rewardSystem.RollCatch(pondCleaned, forceMemoryPiece);
else
catchResult = new FishingRewardSystem.CatchResult(FishingItemType.Fish, "생선", false, false);
ApplyCatchResult(catchResult);
}
private void ApplyCatchResult(FishingRewardSystem.CatchResult catchResult)
{
totalCaughtItems++;
AddToSessionCounts(catchResult.ItemType);
ItemCaught?.Invoke(catchResult.ItemType, 1);
bool pondJustCleaned = false;
string extraHint = string.Empty;
if (catchResult.CountsAsCleanupItem)
{
cleanupItemCount = Mathf.Min(cleanupItemCount + 1, requiredCleanupItems);
extraHint = $"연못 정화 {cleanupItemCount}/{requiredCleanupItems}";
if (!pondCleaned && cleanupItemCount >= requiredCleanupItems)
{
pondCleaned = true;
pondJustCleaned = true;
extraHint = "연못이 맑아졌다. 기억의 조각이 모습을 드러낸다.";
}
}
if (catchResult.IsMemoryPiece)
{
memoryPieceCollected = true;
extraHint = "잃어버린 기억의 일부다.";
}
UpdateUI();
if (ui != null)
{
ui.HighlightSlotForItem(catchResult.ItemType);
if (!catchResult.IsMemoryPiece)
ui.ShowCaughtItem(catchResult.ItemType, catchResult.DisplayName, catchResult.CountsAsCleanupItem, extraHint);
if (pondJustCleaned)
ui.ShowNotice("연못이 맑아졌다!\n기억의 조각이 모습을 드러냈다!");
// 기억의 조각 획득은 FinalResultPanel에서 크게 보여줍니다.
// NoticePanel까지 동시에 띄우면 ShowFinalResult에서 바로 숨겨져 연출이 겹칠 수 있습니다.
}
if (showDebugLog)
Debug.Log($"{catchResult.DisplayName}을(를) 낚았다!");
if (catchResult.IsMemoryPiece)
{
FishingSuccess();
return;
}
nextCatchRoutine = StartCoroutine(NextCatchRoutine());
}
private IEnumerator NextCatchRoutine()
{
yield return new WaitForSeconds(nextCatchDelay);
if (memoryPieceCollected)
{
nextCatchRoutine = null;
yield break;
}
StartNewCatchAttempt();
nextCatchRoutine = null;
}
private void AddToSessionCounts(FishingItemType itemType)
{
switch (itemType)
{
case FishingItemType.Fish:
sessionFishCount++;
break;
case FishingItemType.Trash:
case FishingItemType.Bottle:
case FishingItemType.PlasticBag:
sessionTrashCount++;
break;
case FishingItemType.MemoryPiece:
sessionMemoryPieceCount++;
break;
case FishingItemType.OldCompass:
sessionCompassCount++;
break;
}
}
private void ClampDifficulty()
{
zoneSize = Mathf.Clamp(zoneSize, minZoneSize, maxZoneSize);
pointerSpeed = Mathf.Clamp(pointerSpeed, minSpeed, maxSpeed);
}
private void RandomizeZone()
{
zoneCenter = UnityEngine.Random.Range(0f, 360f);
}
private void UpdateUI()
{
if (ui == null)
return;
ui.SetZone(zoneCenter, zoneSize);
ui.UpdateCounter(successCount, requiredSuccesses, failCount, allowedFails);
ui.UpdateFishingProgress(cleanupItemCount, requiredCleanupItems, pondCleaned, memoryPieceCollected, totalCaughtItems);
ui.UpdateInventoryUI(sessionFishCount, sessionTrashCount, sessionMemoryPieceCount, sessionCompassCount);
}
private void FishingSuccess()
{
activeGame = false;
inputLocked = true;
if (centerIconRotate != null)
centerIconRotate.StopRotate();
UpdateUI();
if (ui != null)
ui.ShowFinalResult("기억의 조각 획득!\n기묘한 낚시터 클리어!");
if (showDebugLog)
Debug.Log("기묘한 낚시터 클리어");
BeginFinalResultRoutineIfNeeded();
}
private void FishingFail()
{
activeGame = false;
inputLocked = true;
if (centerIconRotate != null)
centerIconRotate.StopRotate();
if (ui != null)
ui.ShowFinalResult("낚싯줄이 끊어졌다!\n다시 천천히 낚아보자.");
if (showDebugLog)
Debug.Log("낚시 실패: 낚싯줄 끊어짐");
BeginFinalResultRoutineIfNeeded();
}
private void BeginFinalResultRoutineIfNeeded()
{
if (!hideUIRootAfterFinalResult || uiRoot == null)
return;
if (finalResultRoutine != null)
StopCoroutine(finalResultRoutine);
finalResultRoutine = StartCoroutine(HideUIRootAfterFinalResultRoutine());
}
private IEnumerator HideUIRootAfterFinalResultRoutine()
{
yield return new WaitForSeconds(finalResultShowTime);
if (uiRoot != null)
uiRoot.SetActive(false);
finalResultRoutine = null;
}
private float Normalize(float angle)
{
angle %= 360f;
if (angle < 0f)
angle += 360f;
return angle;
}
private void OnSubmitAction(InputAction.CallbackContext context)
{
SubmitAttempt();
}
private void OnResetAction(InputAction.CallbackContext context)
{
ResetFishing();
}
}