using System; using System.Collections; using UnityEngine; using UnityEngine.InputSystem; public class FishingGameManager : MonoBehaviour { public enum ResultType { Perfect, Good, Miss } public enum FishingControlMode { TimingGauge, SimpleReel } [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("Simple Reel Fishing")] [Tooltip("TimingGauge는 기존 원형 타이밍 게이지 방식, SimpleReel은 릴을 감아 줄 길이를 줄이면 성공하는 방식입니다.")] [SerializeField] private FishingControlMode fishingControlMode = FishingControlMode.SimpleReel; [Tooltip("낚싯대 / 찌 / 릴 제어 스크립트입니다. SimpleReel 모드에서 줄 길이를 읽습니다.")] [SerializeField] private FishingRodVRController rodController; [Tooltip("현재 줄 길이가 이 값 이하가 되면 낚기 성공으로 처리합니다.")] [SerializeField] private float catchCompleteLineLength = 0.7f; [Tooltip("릴 낚시 시작 시 줄 길이가 성공 길이보다 너무 짧으면 이 거리만큼 최소 시작 길이를 보정합니다.")] [SerializeField] private float simpleReelMinimumPullDistance = 0.35f; [Tooltip("릴 낚기 성공 후 찌를 낚싯대 쪽 대기 위치로 회수합니다.")] [SerializeField] private bool returnBobberAfterSimpleReelCatch = true; [Tooltip("SimpleReel 모드에서 기존 타이밍 게이지 UI를 숨기고 진행도/줄 길이 중심으로 표시합니다.")] [SerializeField] private bool hideTimingGaugeInSimpleReelMode = true; [Tooltip("SimpleReel 모드에서는 StartFishing이 여러 번 호출됩니다. 켜면 첫 시작 때만 연못 상태를 초기화합니다.")] [SerializeField] private bool resetPondOnlyOnFirstSimpleReelStart = true; [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; [Tooltip("낚시 세션이 진행 중일 때 StartFishing이 중복 호출되는 것을 막습니다.")] [SerializeField] private bool preventRestartWhileSessionRunning = 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 fishingSessionRunning; private bool pondCleaned; private bool memoryPieceCollected; private bool simpleReelCatchActive; private bool simpleReelPondInitialized; private float simpleReelStartLineLength; private float simpleReelProgress; private bool submitActionWasEnabled; private bool resetActionWasEnabled; private Coroutine nextRoundRoutine; private Coroutine nextCatchRoutine; private Coroutine finalResultRoutine; public bool IsActiveGame => activeGame; public bool IsFishingSessionRunning => fishingSessionRunning; 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 FishingControlMode ControlMode => fishingControlMode; public bool IsSimpleReelCatchActive => simpleReelCatchActive; public float SimpleReelProgress => simpleReelProgress; 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 (fishingControlMode == FishingControlMode.SimpleReel) { UpdateSimpleReelCatch(); 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 (rodController == null) rodController = 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); catchCompleteLineLength = Mathf.Max(0.05f, catchCompleteLineLength); simpleReelMinimumPullDistance = Mathf.Max(0.01f, simpleReelMinimumPullDistance); 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); ui.SetSimpleReelUIVisible(false); ui.SetTimingGaugeVisible(fishingControlMode == FishingControlMode.TimingGauge); } if (centerIconRotate != null) centerIconRotate.StopRotate(); } public void StartFishing() { StartFishing(false); } public void StartFishing(bool forceRestart) { ValidateRuntimeSettings(); if (preventRestartWhileSessionRunning && fishingSessionRunning && !forceRestart) { if (showDebugLog) Debug.Log("낚시 세션이 이미 진행 중이라 StartFishing 호출을 무시했습니다."); return; } StopRunningRoutines(); if (uiRoot != null) uiRoot.SetActive(true); bool shouldResetPondState = forceRestart || resetPondStateOnStart; if (fishingControlMode == FishingControlMode.SimpleReel && resetPondOnlyOnFirstSimpleReelStart) shouldResetPondState = forceRestart || (resetPondStateOnStart && !simpleReelPondInitialized); if (shouldResetPondState) { ResetPondState(); if (fishingControlMode == FishingControlMode.SimpleReel) simpleReelPondInitialized = true; } fishingSessionRunning = true; if (ui != null) ui.InitializeFishingUI(); if (fishingControlMode == FishingControlMode.SimpleReel) StartSimpleReelCatch(); else StartNewCatchAttempt(); if (showDebugLog) Debug.Log(fishingControlMode == FishingControlMode.SimpleReel ? "릴 감기 낚시 시작" : "기묘한 낚시터 시작"); } public void StopFishing(bool hideUI = false) { StopRunningRoutines(); activeGame = false; inputLocked = true; fishingSessionRunning = false; simpleReelCatchActive = false; if (centerIconRotate != null) centerIconRotate.StopRotate(); if (hideUI && uiRoot != null) uiRoot.SetActive(false); } public void ResetFishing() { fishingSessionRunning = false; simpleReelCatchActive = false; simpleReelPondInitialized = false; StartFishing(true); } public void ResetPondState() { successCount = 0; failCount = 0; cleanupItemCount = 0; totalCaughtItems = 0; sessionFishCount = 0; sessionTrashCount = 0; sessionMemoryPieceCount = 0; sessionCompassCount = 0; pondCleaned = false; memoryPieceCollected = false; simpleReelCatchActive = false; simpleReelProgress = 0f; simpleReelStartLineLength = 0f; } private void StartSimpleReelCatch() { if (rodController == null && autoBindMissingReferences) AutoBindMissingReferences(); if (rodController == null) { Debug.LogWarning("[FishingGameManager] SimpleReel 모드에는 FishingRodVRController 참조가 필요합니다.", this); activeGame = false; inputLocked = true; fishingSessionRunning = false; return; } successCount = 0; failCount = 0; inputLocked = false; activeGame = true; simpleReelCatchActive = true; float currentLineLength = Mathf.Max(rodController.CurrentLineLength, catchCompleteLineLength + simpleReelMinimumPullDistance); simpleReelStartLineLength = currentLineLength; simpleReelProgress = 0f; if (centerIconRotate != null) { centerIconRotate.ResetRotation(); centerIconRotate.StopRotate(); } if (ui != null) { ui.SetSimpleReelUIVisible(true); if (hideTimingGaugeInSimpleReelMode) ui.SetTimingGaugeVisible(false); ui.UpdateSimpleReelUI(simpleReelProgress, rodController.CurrentLineLength, rodController.IsReeling); ui.ShowPersistentResult("릴을 감아 끌어올려라!"); } UpdateUI(); } private void UpdateSimpleReelCatch() { if (!simpleReelCatchActive || rodController == null) return; float currentLineLength = rodController.CurrentLineLength; float totalPullDistance = Mathf.Max(0.001f, simpleReelStartLineLength - catchCompleteLineLength); float pulledDistance = simpleReelStartLineLength - currentLineLength; simpleReelProgress = Mathf.Clamp01(pulledDistance / totalPullDistance); if (ui != null) ui.UpdateSimpleReelUI(simpleReelProgress, currentLineLength, rodController.IsReeling); if (currentLineLength <= catchCompleteLineLength || simpleReelProgress >= 1f) { simpleReelCatchActive = false; inputLocked = true; if (haptic != null) haptic.Perfect(); if (ui != null) { ui.UpdateSimpleReelUI(1f, currentLineLength, false); ui.ShowPersistentResult("낚았다!"); } CatchItemSuccess(); } } private void StartNewCatchAttempt() { successCount = 0; failCount = 0; inputLocked = false; activeGame = true; if (resetDifficultyEachCatch) { pointerAngle = 0f; pointerSpeed = minSpeed; zoneSize = startZoneSize; } if (randomDirectionEachRound) 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 (fishingControlMode == FishingControlMode.SimpleReel) return; 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; } if (fishingControlMode == FishingControlMode.SimpleReel) { fishingSessionRunning = false; simpleReelCatchActive = false; if (returnBobberAfterSimpleReelCatch && rodController != null) rodController.ReturnBobberToRest(); 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; if (fishingControlMode == FishingControlMode.SimpleReel) { ui.SetSimpleReelUIVisible(activeGame || simpleReelCatchActive); if (hideTimingGaugeInSimpleReelMode) ui.SetTimingGaugeVisible(false); ui.UpdateFishingProgress(cleanupItemCount, requiredCleanupItems, pondCleaned, memoryPieceCollected, totalCaughtItems); ui.UpdateInventoryUI(sessionFishCount, sessionTrashCount, sessionMemoryPieceCount, sessionCompassCount); float currentLineLength = rodController != null ? rodController.CurrentLineLength : 0f; bool isReeling = rodController != null && rodController.IsReeling; ui.UpdateSimpleReelUI(simpleReelProgress, currentLineLength, isReeling); return; } ui.SetSimpleReelUIVisible(false); ui.SetTimingGaugeVisible(true); 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; fishingSessionRunning = false; simpleReelCatchActive = false; if (centerIconRotate != null) centerIconRotate.StopRotate(); UpdateUI(); if (ui != null) ui.ShowFinalResult("기억의 조각 획득!\n기묘한 낚시터 클리어!"); if (showDebugLog) Debug.Log("기묘한 낚시터 클리어"); BeginFinalResultRoutineIfNeeded(); } private void FishingFail() { activeGame = false; inputLocked = true; fishingSessionRunning = false; simpleReelCatchActive = false; 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(); } }