chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
253
Assets/Scripts/Combat/FaceTargetOnAttack.cs
Normal file
253
Assets/Scripts/Combat/FaceTargetOnAttack.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user