using System;
using System.Collections.Generic;
using UnityEngine;
///
/// Smoothly rotates the player towards a valid enemy target when an attack animation is triggered.
///
[DisallowMultipleComponent]
public sealed class FaceTargetOnAttack : MonoBehaviour
{
private const float MinAssistDuration = 0.05f;
private const float MinCancelTime = 0.05f;
[Header("Assist Toggle")]
[SerializeField] private bool enableAutoFaceOnAttack = true;
[SerializeField] private bool reEvaluateDuringWindow = true;
[Header("Rotation")]
[SerializeField] private float rotationSpeedDegPerSec = 720f;
[SerializeField] private float assistWindowSeconds = 0.25f;
[SerializeField] private float manualOverrideThreshold = 0.15f;
[SerializeField] private float targetLoseDistanceTolerance = 0.25f;
[Header("Dependencies")]
[SerializeField] private EnemyTargetDetector targetDetector;
[SerializeField] private LookInputRouter lookInputRouter;
[SerializeField] private CharacterAttacks characterAttacks;
[SerializeField] private PlayerScript playerScript;
[SerializeField] private Animator animator;
[Header("Animator Output")]
[SerializeField] private bool driveAnimatorTurnSpeed = false;
[SerializeField] private string turnSpeedParameter = "TurnSpeed";
[SerializeField] private float turnSpeedScale = 1f;
[Header("Attack Filtering")]
[Tooltip("Animations that should trigger auto facing. Leave empty to allow all.")]
[SerializeField] private List assistedAnimationNames = new List
{
AnimationNames.Jab,
AnimationNames.Haymaker,
AnimationNames.SpinningBackfist,
AnimationNames.ElbowSmash,
AnimationNames.AirPunch,
AnimationNames.LowKick,
AnimationNames.BasicKick,
AnimationNames.SumoSlap
};
private float assistTimer;
private Transform currentTarget;
private Vector3 currentTargetPosition;
private Quaternion lastFrameRotation;
private bool assistActive;
private bool waitingForCancelWindow;
private HashSet assistedLookup;
private void Reset()
{
targetDetector = GetComponent();
lookInputRouter = GetComponent();
characterAttacks = GetComponent();
playerScript = GetComponent();
animator = GetComponent();
}
private void Awake()
{
if (targetDetector == null)
targetDetector = GetComponent();
if (lookInputRouter == null)
lookInputRouter = GetComponent();
if (characterAttacks == null)
characterAttacks = GetComponent();
if (playerScript == null)
playerScript = GetComponent();
if (animator == null)
animator = GetComponent();
assistedLookup = assistedAnimationNames != null && assistedAnimationNames.Count > 0
? new HashSet(assistedAnimationNames)
: null;
if (characterAttacks != null)
{
characterAttacks.AnimationAboutToPlay += OnAnimationAboutToPlay;
}
}
private void OnDestroy()
{
if (characterAttacks != null)
{
characterAttacks.AnimationAboutToPlay -= OnAnimationAboutToPlay;
}
}
private void Update()
{
if (!assistActive)
return;
if (!enableAutoFaceOnAttack)
{
CancelAssist();
return;
}
if (playerScript != null && (!playerScript.attack || playerScript.dodge || playerScript.block))
{
CancelAssist();
return;
}
assistTimer -= Time.deltaTime;
if (assistTimer <= 0f)
{
CancelAssist();
return;
}
if (lookInputRouter != null && lookInputRouter.TryGetLookDelta(out var lookDelta))
{
if (waitingForCancelWindow)
{
if (assistWindowSeconds - assistTimer > MinCancelTime && lookDelta.magnitude >= manualOverrideThreshold)
{
CancelAssist();
return;
}
}
else if (lookDelta.magnitude >= manualOverrideThreshold)
{
CancelAssist();
return;
}
}
if (!ValidateOrRefreshTarget())
{
CancelAssist();
return;
}
Vector3 direction = currentTargetPosition - transform.position;
direction.y = 0f;
if (direction.sqrMagnitude < 0.0001f)
{
CancelAssist();
return;
}
Quaternion desiredRotation = Quaternion.LookRotation(direction, Vector3.up);
lastFrameRotation = transform.rotation;
transform.rotation = Quaternion.RotateTowards(transform.rotation, desiredRotation, rotationSpeedDegPerSec * Time.deltaTime);
waitingForCancelWindow = false;
if (driveAnimatorTurnSpeed && animator != null)
{
float angleDelta = Quaternion.Angle(lastFrameRotation, transform.rotation);
float turnSpeed = (angleDelta / Time.deltaTime) * turnSpeedScale;
animator.SetFloat(turnSpeedParameter, turnSpeed);
}
}
private void OnAnimationAboutToPlay(string animationName)
{
if (!enableAutoFaceOnAttack)
return;
if (!ShouldAssistForAnimation(animationName))
return;
if (targetDetector == null)
return;
if (assistWindowSeconds < MinAssistDuration)
assistWindowSeconds = MinAssistDuration;
if (targetDetector.TryFindBestTarget(out var target, out var position))
{
BeginAssist(target, position);
}
}
private bool ShouldAssistForAnimation(string animationName)
{
if (string.IsNullOrEmpty(animationName))
return false;
if (assistedLookup == null)
return true;
return assistedLookup.Contains(animationName);
}
private void BeginAssist(Transform target, Vector3 position)
{
currentTarget = target;
currentTargetPosition = position;
assistTimer = assistWindowSeconds;
assistActive = true;
waitingForCancelWindow = true;
lastFrameRotation = transform.rotation;
if (lookInputRouter != null)
{
lookInputRouter.ResetLookDelta();
}
}
private bool ValidateOrRefreshTarget()
{
if (currentTarget == null || !currentTarget.gameObject.activeInHierarchy)
return false;
if (targetDetector == null)
return false;
float sqrDistance = (currentTargetPosition - transform.position).sqrMagnitude;
float maxDistance = targetDetector.AssistRadius + targetLoseDistanceTolerance;
if (sqrDistance > maxDistance * maxDistance)
return false;
if (reEvaluateDuringWindow && targetDetector.TryFindBestTarget(out var newTarget, out var newPosition))
{
currentTarget = newTarget;
currentTargetPosition = newPosition;
}
else
{
currentTargetPosition = currentTarget.position;
}
return true;
}
private void CancelAssist()
{
assistActive = false;
assistTimer = 0f;
currentTarget = null;
currentTargetPosition = Vector3.zero;
waitingForCancelWindow = false;
if (driveAnimatorTurnSpeed && animator != null)
{
animator.SetFloat(turnSpeedParameter, 0f);
}
}
}