254 lines
7.6 KiB
C#
254 lines
7.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Smoothly rotates the player towards a valid enemy target when an attack animation is triggered.
|
|
/// </summary>
|
|
[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<string> assistedAnimationNames = new List<string>
|
|
{
|
|
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<string> assistedLookup;
|
|
|
|
private void Reset()
|
|
{
|
|
targetDetector = GetComponent<EnemyTargetDetector>();
|
|
lookInputRouter = GetComponent<LookInputRouter>();
|
|
characterAttacks = GetComponent<CharacterAttacks>();
|
|
playerScript = GetComponent<PlayerScript>();
|
|
animator = GetComponent<Animator>();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
if (targetDetector == null)
|
|
targetDetector = GetComponent<EnemyTargetDetector>();
|
|
if (lookInputRouter == null)
|
|
lookInputRouter = GetComponent<LookInputRouter>();
|
|
if (characterAttacks == null)
|
|
characterAttacks = GetComponent<CharacterAttacks>();
|
|
if (playerScript == null)
|
|
playerScript = GetComponent<PlayerScript>();
|
|
if (animator == null)
|
|
animator = GetComponent<Animator>();
|
|
|
|
assistedLookup = assistedAnimationNames != null && assistedAnimationNames.Count > 0
|
|
? new HashSet<string>(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);
|
|
}
|
|
}
|
|
}
|