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); } } }