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;
}
if (!agent.isOnNavMesh)
{
pendingInteractable = null;
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;
}
if (!agent.isOnNavMesh)
{
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()
{
// isOnNavMesh, not just a null check: the agent lives in the Systems scene
// and the NavMesh is baked into the level, so between those two scenes
// loading there is a window where the agent exists but is not on a mesh.
// ResetPath and isStopped both log an error in that state.
if (agent != null && agent.isOnNavMesh)
{
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;
}
}
}