using System; using NightclubArcadia.Interaction; using NightclubArcadia.Player.Math; using StarterAssets; using UnityEngine; using UnityEngine.AI; namespace NightclubArcadia.Player { /// /// NavMeshAgent in oracle mode: computes desired move input for the arbiter /// without moving the CharacterController directly. /// [DefaultExecutionOrder(-60)] [RequireComponent(typeof(NavMeshAgent))] public sealed class ClickToMoveController : MonoBehaviour { const float SampleRadius = 1.5f; const float StallWindow = 0.4f; const float StallDistance = 0.05f; [SerializeField] Camera brainCamera; NavMeshAgent agent; ThirdPersonController locomotion; IClickInteractable pendingInteractable; bool forceRunForPath; [SerializeField] float runDistanceThreshold = 6f; Vector3 lastPosition; float stallTimer; bool hasPath; public bool HasPath => hasPath; public Vector3 Destination { get; private set; } public Vector2 DesiredMoveInput { get; private set; } public float RemainingDistance => agent != null ? agent.remainingDistance : 0f; public bool WantsSprint => hasPath && (forceRunForPath || agent.remainingDistance > runDistanceThreshold); public event Action PathFinished; public event Action RunRequested; void Awake() { agent = GetComponent(); locomotion = GetComponent(); agent.updatePosition = false; agent.updateRotation = false; agent.updateUpAxis = false; agent.angularSpeed = 0f; agent.acceleration = 1000f; agent.autoBraking = true; agent.stoppingDistance = 0.15f; agent.radius = 0.3f; agent.height = 1.8f; if (brainCamera == null) { brainCamera = Camera.main; } SyncAgentSpeed(false); } void Update() { if (!hasPath) { DesiredMoveInput = Vector2.zero; return; } SyncAgentSpeed(forceRunForPath || agent.remainingDistance > runDistanceThreshold); if (brainCamera == null) { brainCamera = Camera.main; } var cameraYaw = brainCamera != null ? brainCamera.transform.eulerAngles.y : 0f; DesiredMoveInput = MotionInputMath.WorldToCameraRelativeInput( agent.desiredVelocity, agent.speed, cameraYaw); CheckStall(); CheckArrival(); } void LateUpdate() { if (agent != null) { agent.nextPosition = transform.position; } } public void RequestRun() { forceRunForPath = true; RunRequested?.Invoke(); } public bool SetDestination(Vector3 worldPoint) { pendingInteractable = null; if (!TrySampleNavMesh(worldPoint, out var hit)) { return false; } agent.stoppingDistance = 0.15f; agent.isStopped = false; agent.SetDestination(hit.position); Destination = hit.position; hasPath = true; ResetStallTracking(); return true; } public bool SetInteractionTarget(IClickInteractable interactable) { if (interactable == null || interactable.ApproachPoint == null) { return false; } pendingInteractable = interactable; var approach = interactable.ApproachPoint.position; if (!TrySampleNavMesh(approach, out var hit)) { pendingInteractable = null; return false; } agent.stoppingDistance = Mathf.Max(0.15f, interactable.ApproachRadius); agent.isStopped = false; agent.SetDestination(hit.position); Destination = hit.position; hasPath = true; ResetStallTracking(); return true; } public void Cancel() { if (agent != null) { agent.ResetPath(); agent.isStopped = true; } hasPath = false; forceRunForPath = false; pendingInteractable = null; DesiredMoveInput = Vector2.zero; ResetStallTracking(); } bool TrySampleNavMesh(Vector3 point, out NavMeshHit hit) { return NavMesh.SamplePosition(point, out hit, SampleRadius, NavMesh.AllAreas); } void CheckArrival() { if (!hasPath || agent.pathPending) { return; } if (agent.remainingDistance > agent.stoppingDistance) { return; } var success = agent.pathStatus != NavMeshPathStatus.PathInvalid; hasPath = false; agent.ResetPath(); var interactable = pendingInteractable; pendingInteractable = null; PathFinished?.Invoke(success); if (success && interactable != null) { interactable.OnApproached(); } } void CheckStall() { var displacement = Vector3.Distance( new Vector3(transform.position.x, 0f, transform.position.z), new Vector3(lastPosition.x, 0f, lastPosition.z)); if (displacement < StallDistance) { stallTimer += Time.deltaTime; } else { stallTimer = 0f; lastPosition = transform.position; } if (stallTimer >= StallWindow && agent.remainingDistance > agent.stoppingDistance) { Cancel(); PathFinished?.Invoke(false); } } void ResetStallTracking() { lastPosition = transform.position; stallTimer = 0f; } void SyncAgentSpeed(bool sprint) { if (locomotion == null) { return; } agent.speed = sprint ? locomotion.SprintSpeed : locomotion.MoveSpeed; } } }