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 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(true) : GetComponentInChildren(true); if (rewardSystem == null) rewardSystem = searchRoot != null ? searchRoot.GetComponentInChildren(true) : GetComponentInChildren(true); if (haptic == null) haptic = searchRoot != null ? searchRoot.GetComponentInChildren(true) : GetComponentInChildren(true); if (centerIconRotate == null) { Transform centerIconTransform = FindTransformRecursive(searchRoot, "CenterIcon"); if (centerIconTransform != null) centerIconRotate = centerIconTransform.GetComponent(); if (centerIconRotate == null && searchRoot != null) centerIconRotate = searchRoot.GetComponentInChildren(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 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 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(); } }