using System; using NightclubArcadia.Interaction; using NightclubArcadia.UI; using UnityEngine; using UnityEngine.EventSystems; namespace NightclubArcadia.Player { /// /// Converts pointer clicks into click-to-move destinations or interactable /// approach targets. /// public sealed class PointerNavigationInput : MonoBehaviour { const float DoubleClickWindow = 0.35f; const float DoubleClickPixelThreshold = 40f; [SerializeField] Camera brainCamera; [SerializeField] LayerMask clickMask = ~0; [SerializeField] PlayerPointerInput pointerInput; [SerializeField] ClickToMoveController clickToMove; [SerializeField] PlayerMotionArbiter motionArbiter; float lastClickTime = -1f; Vector2 lastClickPosition; bool pendingClick; Vector2 pendingClickPosition; void Awake() { if (pointerInput == null) { pointerInput = GetComponent(); } if (clickToMove == null) { clickToMove = GetComponent(); } if (motionArbiter == null) { motionArbiter = GetComponent(); } if (brainCamera == null) { brainCamera = Camera.main; } if (clickMask.value == ~0) { clickMask = LayerMask.GetMask("Default", "Ground", "Interactable"); } } void OnEnable() { if (pointerInput != null) { pointerInput.Clicked += OnClicked; } } void OnDisable() { if (pointerInput != null) { pointerInput.Clicked -= OnClicked; } pendingClick = false; } // Input System callbacks fire during event processing, where // IsPointerOverGameObject() is invalid (queries last frame's UI). // Queue the click and resolve it in Update instead. void OnClicked() { if (pointerInput == null) { return; } pendingClick = true; pendingClickPosition = pointerInput.ScreenPosition; } void Update() { if (!pendingClick) { return; } pendingClick = false; ProcessClick(pendingClickPosition); } void ProcessClick(Vector2 screenPos) { if (!CanNavigate()) { return; } if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) { return; } if (brainCamera == null) { brainCamera = Camera.main; } if (brainCamera == null || clickToMove == null) { return; } var ray = brainCamera.ScreenPointToRay(screenPos); var hits = Physics.RaycastAll(ray, 200f, clickMask, QueryTriggerInteraction.Collide); Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); var isDoubleClick = IsDoubleClick(screenPos); lastClickTime = Time.unscaledTime; lastClickPosition = screenPos; foreach (var hit in hits) { var interactable = FindInteractable(hit.collider); if (interactable != null) { if (isDoubleClick) { clickToMove.RequestRun(); } if (clickToMove.SetInteractionTarget(interactable)) { return; } } } foreach (var hit in hits) { if (isDoubleClick) { clickToMove.RequestRun(); } if (clickToMove.SetDestination(hit.point)) { return; } } } bool CanNavigate() { if (GetComponent() is { IsLocked: true }) { return false; } if (UIStateController.Instance == null) { return true; } return !UIStateController.Instance.IsDialogueActive && !UIStateController.Instance.IsMenuOpen; } bool IsDoubleClick(Vector2 screenPos) { if (lastClickTime < 0f) { return false; } var withinTime = Time.unscaledTime - lastClickTime <= DoubleClickWindow; var withinDistance = Vector2.Distance(screenPos, lastClickPosition) <= DoubleClickPixelThreshold; return withinTime && withinDistance; } static IClickInteractable FindInteractable(Collider collider) { var behaviours = collider.GetComponentsInParent(); foreach (var behaviour in behaviours) { if (behaviour is IClickInteractable interactable) { return interactable; } } return null; } } }