using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.XR.Interaction.Toolkit; using UnityEngine.XR.Interaction.Toolkit.Interactables; public class ClamBiteDetector : MonoBehaviour { [Header("References")] [SerializeField] private ClamOpenClose clam; [SerializeField] private RaftHealth health; [SerializeField] private MemoryFragmentReset memoryFragment; [Header("Bite Damage")] [SerializeField] private int biteDamage = 20; [Header("Bite Zone")] [SerializeField] private Collider biteZoneCollider; [Tooltip("조개 미션이 시작되었을 때만 물림 판정을 합니다.")] [SerializeField] private bool missionActiveOnStart = false; [Tooltip("조개가 닫히는 동안 이미 한 번 물렸으면 추가 판정을 막습니다.")] [SerializeField] private bool biteOncePerClose = true; [Tooltip("미션 중 BiteZone Collider를 계속 켜두어 조각이 빠져나갔는지 추적합니다.")] [SerializeField] private bool keepBiteZoneEnabledDuringMission = true; [Header("Fragment Rule")] [Tooltip("기억의 조각을 잡은 뒤 BiteZone을 빠져나가지 못한 상태에서 조개가 닫히면 물림 처리합니다.")] [SerializeField] private bool biteIfGrabbedFragmentDidNotExitBiteZone = true; [Tooltip("기억의 조각을 잡고 있을 때만 조각 물림 판정을 합니다.")] [SerializeField] private bool biteFragmentOnlyWhenGrabbed = true; [Header("Hand Rule")] [Tooltip("손 콜라이더가 BiteZone 안에 있으면 조개가 닫힐 때 물림 처리합니다.")] [SerializeField] private bool biteHandInsideZone = true; [Header("Target Detection")] [SerializeField] private string handTag = "PlayerHand"; [SerializeField] private string fragmentTag = "MemoryFragment"; [Header("Clear Event")] [Tooltip("기억의 조각을 잡은 상태로 BiteZone 밖으로 빼냈을 때 한 번만 성공 처리합니다.")] [SerializeField] private bool clearOnce = true; [Tooltip("성공하면 조개 물림 미션을 끕니다.")] [SerializeField] private bool stopMissionOnClear = true; public UnityEvent onMemoryFragmentEscaped; [Header("Debug")] [SerializeField] private bool showDebugLog = true; private bool missionActive; private bool biteWindowOpen; private bool hasBittenThisClose; private bool fragmentGrabbed; private bool fragmentInsideBiteZone; private bool fragmentGrabStartedInsideBiteZone; private bool clearTriggered; private XRGrabInteractable memoryGrabInteractable; private Collider[] memoryFragmentColliders; private readonly HashSet collidersInside = new(); private void Awake() { if (biteZoneCollider == null) biteZoneCollider = GetComponent(); if (biteZoneCollider != null) { biteZoneCollider.isTrigger = true; biteZoneCollider.enabled = false; } if (clam == null) clam = GetComponentInParent(); if (health == null) health = FindFirstObjectByType(); if (memoryFragment == null) memoryFragment = FindFirstObjectByType(); if (memoryFragment != null) { memoryGrabInteractable = memoryFragment.GetComponent(); memoryFragmentColliders = memoryFragment.GetComponentsInChildren(true); } missionActive = missionActiveOnStart; } private void Start() { if (missionActive && keepBiteZoneEnabledDuringMission) { SetBiteZoneEnabled(true); RefreshInitialOverlapState(); } } private void Update() { if (!missionActive || !fragmentGrabbed || !fragmentGrabStartedInsideBiteZone || clearTriggered) return; UpdateFragmentOverlapState(); } private void OnEnable() { if (clam != null) { clam.onCloseStarted.AddListener(OnClamCloseStarted); clam.onClosed.AddListener(OnClamClosed); clam.onOpened.AddListener(OnClamOpened); } if (memoryGrabInteractable != null) { memoryGrabInteractable.selectEntered.AddListener(OnFragmentGrabbed); memoryGrabInteractable.selectExited.AddListener(OnFragmentReleased); } } private void OnDisable() { if (clam != null) { clam.onCloseStarted.RemoveListener(OnClamCloseStarted); clam.onClosed.RemoveListener(OnClamClosed); clam.onOpened.RemoveListener(OnClamOpened); } if (memoryGrabInteractable != null) { memoryGrabInteractable.selectEntered.RemoveListener(OnFragmentGrabbed); memoryGrabInteractable.selectExited.RemoveListener(OnFragmentReleased); } } private void OnTriggerEnter(Collider other) { collidersInside.Add(other); if (IsMemoryFragmentCollider(other)) { fragmentInsideBiteZone = true; } if (biteWindowOpen) { TryBiteByCollider(other); } } private void OnTriggerStay(Collider other) { collidersInside.Add(other); if (IsMemoryFragmentCollider(other)) { fragmentInsideBiteZone = true; } if (biteWindowOpen) { TryBiteByCollider(other); } } private void OnTriggerExit(Collider other) { collidersInside.Remove(other); if (IsMemoryFragmentCollider(other)) { UpdateFragmentOverlapState(); } } public void StartBiteMission() { missionActive = true; biteWindowOpen = false; hasBittenThisClose = false; clearTriggered = false; collidersInside.Clear(); SetBiteZoneEnabled(true); RefreshInitialOverlapState(); if (showDebugLog) Debug.Log("[ClamBiteDetector] 조개 미션 시작. BiteZone 추적 ON.", this); } public void StopBiteMission() { missionActive = false; biteWindowOpen = false; hasBittenThisClose = false; fragmentGrabbed = false; fragmentInsideBiteZone = false; fragmentGrabStartedInsideBiteZone = false; collidersInside.Clear(); SetBiteZoneEnabled(false); if (showDebugLog) Debug.Log("[ClamBiteDetector] 조개 미션 정지. BiteZone 추적 OFF.", this); } private void OnClamOpened() { hasBittenThisClose = false; biteWindowOpen = false; if (!missionActive) return; SetBiteZoneEnabled(true); RefreshInitialOverlapState(); } private void OnClamCloseStarted() { if (!missionActive) return; hasBittenThisClose = false; biteWindowOpen = true; SetBiteZoneEnabled(true); RefreshInitialOverlapState(); if (showDebugLog) Debug.Log("[ClamBiteDetector] 조개 닫힘 시작. 물림 판정 체크.", this); TryBiteGrabbedFragmentIfStillInside(); TryBiteCurrentHandsInside(); } private void OnClamClosed() { biteWindowOpen = false; hasBittenThisClose = false; collidersInside.Clear(); if (!missionActive) return; if (keepBiteZoneEnabledDuringMission) { SetBiteZoneEnabled(true); RefreshInitialOverlapState(); } else { SetBiteZoneEnabled(false); } if (showDebugLog) Debug.Log("[ClamBiteDetector] 조개 닫힘 완료. 물림 판정 종료.", this); } private void OnFragmentGrabbed(SelectEnterEventArgs args) { fragmentGrabbed = true; fragmentInsideBiteZone = IsFragmentOverlappingBiteZone(); fragmentGrabStartedInsideBiteZone = fragmentInsideBiteZone; RefreshInitialOverlapState(); if (showDebugLog) Debug.Log("[ClamBiteDetector] 기억의 조각을 잡았습니다.", this); } private void OnFragmentReleased(SelectExitEventArgs args) { fragmentGrabbed = false; fragmentGrabStartedInsideBiteZone = false; if (showDebugLog) Debug.Log("[ClamBiteDetector] 기억의 조각을 놓았습니다.", this); } private void TryBiteGrabbedFragmentIfStillInside() { if (!biteIfGrabbedFragmentDidNotExitBiteZone) return; if (memoryFragment == null) return; if (biteOncePerClose && hasBittenThisClose) return; bool isSelected = IsFragmentGrabbed(); if (biteFragmentOnlyWhenGrabbed && !isSelected) return; bool shouldBite = isSelected && IsFragmentOverlappingBiteZone(); if (!shouldBite) return; BiteNow("기억의 조각을 잡은 뒤 BiteZone 밖으로 빼내지 못함"); } private void TryBiteCurrentHandsInside() { if (!biteHandInsideZone) return; foreach (Collider col in collidersInside) { if (col == null) continue; if (IsHandCollider(col)) { BiteNow("손이 BiteZone 안에 있음"); return; } } } private void TryBiteByCollider(Collider other) { if (!missionActive) return; if (!biteWindowOpen) return; if (biteOncePerClose && hasBittenThisClose) return; if (biteHandInsideZone && IsHandCollider(other)) { BiteNow("손이 BiteZone 안에 있음"); return; } if (IsMemoryFragmentCollider(other)) { if (memoryGrabInteractable != null && memoryGrabInteractable.isSelected) { BiteNow("기억의 조각이 BiteZone 안에 있음"); } } } private void BiteNow(string reason) { if (biteOncePerClose && hasBittenThisClose) return; hasBittenThisClose = true; biteWindowOpen = false; if (health != null) health.TakeDamage(biteDamage); if (memoryFragment != null) memoryFragment.ResetFragment(); fragmentGrabbed = false; fragmentInsideBiteZone = false; fragmentGrabStartedInsideBiteZone = false; if (showDebugLog) { Debug.Log($"[ClamBiteDetector] 조개에게 물림. 이유: {reason}. 데미지 {biteDamage}, 기억의 조각 리셋", this); } } private void TriggerMemoryFragmentEscaped() { if (clearOnce && clearTriggered) return; clearTriggered = true; if (showDebugLog) Debug.Log("[ClamBiteDetector] 기억의 조각 꺼내기 성공. 클리어 이벤트 실행.", this); onMemoryFragmentEscaped?.Invoke(); if (stopMissionOnClear) { StopBiteMission(); } } private void RefreshInitialOverlapState() { if (biteZoneCollider == null || !biteZoneCollider.enabled) return; collidersInside.Clear(); fragmentInsideBiteZone = false; Bounds bounds = biteZoneCollider.bounds; Collider[] hits = Physics.OverlapBox( bounds.center, bounds.extents, biteZoneCollider.transform.rotation, ~0, QueryTriggerInteraction.Collide ); foreach (Collider hit in hits) { if (hit == null) continue; collidersInside.Add(hit); if (IsMemoryFragmentCollider(hit)) fragmentInsideBiteZone = true; } } private void UpdateFragmentOverlapState() { bool wasInside = fragmentInsideBiteZone; fragmentInsideBiteZone = IsFragmentOverlappingBiteZone(); if (!wasInside || fragmentInsideBiteZone) return; if (!fragmentGrabbed || !missionActive || !fragmentGrabStartedInsideBiteZone) return; if (showDebugLog) Debug.Log("[ClamBiteDetector] 기억의 조각이 BiteZone 밖으로 빠져나갔습니다.", this); TriggerMemoryFragmentEscaped(); } private bool IsFragmentOverlappingBiteZone() { if (biteZoneCollider == null || memoryFragment == null) return false; if (memoryFragmentColliders == null || memoryFragmentColliders.Length == 0) memoryFragmentColliders = memoryFragment.GetComponentsInChildren(true); foreach (Collider fragmentCollider in memoryFragmentColliders) { if (fragmentCollider == null || fragmentCollider == biteZoneCollider) continue; if (Physics.ComputePenetration( biteZoneCollider, biteZoneCollider.transform.position, biteZoneCollider.transform.rotation, fragmentCollider, fragmentCollider.transform.position, fragmentCollider.transform.rotation, out _, out _)) { return true; } if (biteZoneCollider.bounds.Intersects(fragmentCollider.bounds)) { return true; } } return false; } private bool IsFragmentGrabbed() { if (memoryGrabInteractable != null) return memoryGrabInteractable.isSelected; return fragmentGrabbed; } private void SetBiteZoneEnabled(bool enabled) { if (biteZoneCollider == null) return; biteZoneCollider.enabled = enabled; } private bool IsMemoryFragmentCollider(Collider other) { if (other == null) return false; if (other.CompareTag(fragmentTag)) return true; MemoryFragmentReset fragment = other.GetComponentInParent(); return fragment != null; } private bool IsHandCollider(Collider other) { if (other == null) return false; if (other.CompareTag(handTag)) return true; XRHandMarker marker = other.GetComponentInParent(); return marker != null; } }