Files
FablishRPG/Assets/Scripts/MeleeCombatLayerController.cs

307 lines
9.0 KiB
C#
Raw Normal View History

2026-09-28 20:46:57 +01:00
using UnityEngine;
using UnityEngine.InputSystem;
using Synty.AnimationBaseLocomotion.Samples.InputSystem;
public class MeleeCombatLayerController : MonoBehaviour
{
[Header("References")]
[Tooltip("The InputReader on your character - same one the player controller uses")]
[SerializeField] private InputReader _inputReader;
[Tooltip("The Animator on your character")]
[SerializeField] private Animator _animator;
[Tooltip("Your main camera's transform - used to know where you're looking")]
[SerializeField] private Transform _cameraTransform;
[Header("Weapon Management")]
[Tooltip("The active weapon used for damage and stats.")]
[SerializeField] private Weapon _currentWeapon;
[Tooltip("The Transform socket where the weapon is held when equipped (e.g., Prop_R)")]
[SerializeField] private Transform _equippedSocket;
[Tooltip("The Transform socket where the weapon rests when put away (e.g., Spine_03 or a Back Socket)")]
[SerializeField] private Transform _unequippedSocket;
[Header("Layer Settings")]
[Tooltip("The name of the Animator layer containing the melee override (e.g. 'Melee')")]
[SerializeField] private string _meleeLayerName = "Melee";
[Tooltip("How fast the melee layer blends in and out when equipping. Higher = snappier.")]
[SerializeField] private float _equipBlendSpeed = 5f;
[Header("Combat Settings")]
[Tooltip("Layers to consider as targets when attacking")]
[SerializeField] private LayerMask _targetLayers;
[Header("Input Actions")]
[Tooltip("Input action for attacking")]
[SerializeField] private InputAction _attackAction = new InputAction("Attack", binding: "<Mouse>/leftButton");
[Tooltip("Input action for blocking")]
[SerializeField] private InputAction _blockAction = new InputAction("Block", binding: "<Mouse>/rightButton");
// Animation Hashes
private readonly int _powerUpHash = Animator.StringToHash("PowerUp");
private readonly int _attackHash = Animator.StringToHash("Attack");
private readonly int _isBlockingHash = Animator.StringToHash("IsBlocking");
private readonly int _takeDamageHash = Animator.StringToHash("TakeDamage");
// Runtime state
private bool _isEquipped;
private bool _isBlocking;
private bool _isPoweringUp;
private float _equipWeight;
private int _meleeLayerIndex;
private void Start()
{
_meleeLayerIndex = _animator.GetLayerIndex(_meleeLayerName);
// If the referenced weapon is a prefab asset (not in the scene), instantiate it
if (_currentWeapon != null && !_currentWeapon.gameObject.scene.IsValid())
{
_currentWeapon = Instantiate(_currentWeapon);
}
// Start with weapon in correct socket
UpdateWeaponSocket();
// Try getting the main camera if not set
if (_cameraTransform == null && Camera.main != null)
{
_cameraTransform = Camera.main.transform;
}
}
private void OnEnable()
{
_attackAction.started += OnAttackStarted;
_attackAction.canceled += OnAttackCanceled;
_blockAction.started += OnBlockStarted;
_blockAction.canceled += OnBlockCanceled;
_attackAction.Enable();
_blockAction.Enable();
}
private void OnDisable()
{
_attackAction.started -= OnAttackStarted;
_attackAction.canceled -= OnAttackCanceled;
_blockAction.started -= OnBlockStarted;
_blockAction.canceled -= OnBlockCanceled;
_attackAction.Disable();
_blockAction.Disable();
}
private void Update()
{
// Blend the melee layer weight based on equip state
float equipTarget = _isEquipped ? 1f : 0f;
_equipWeight = Mathf.MoveTowards(_equipWeight, equipTarget, _equipBlendSpeed * Time.deltaTime);
if (_meleeLayerIndex != -1)
{
_animator.SetLayerWeight(_meleeLayerIndex, _equipWeight);
}
// Send parameters to animator
if (_animator != null)
{
_animator.SetBool(_isBlockingHash, _isBlocking);
}
}
public bool IsEquipped => _isEquipped;
public System.Collections.IEnumerator EquipWeapon()
{
if (_isEquipped) yield break;
_isEquipped = true;
if (_meleeLayerIndex != -1)
{
_animator.Play("Equip", _meleeLayerIndex, 0f);
}
// Wait for the hand to reach the weapon on the back before snapping it to the hand
yield return new WaitForSeconds(0.3f);
UpdateWeaponSocket();
}
public System.Collections.IEnumerator UnequipWeapon()
{
if (!_isEquipped) yield break;
// Cancel block and power up
if (_isBlocking)
{
_isBlocking = false;
_inputReader.onWalkToggled?.Invoke();
}
if (_isPoweringUp)
{
_isPoweringUp = false;
if (_animator != null) _animator.SetBool(_powerUpHash, false);
}
// Play the Unequip animation
if (_meleeLayerIndex != -1)
{
_animator.Play("Unequip", _meleeLayerIndex, 0f);
}
// Wait for the unequip animation to finish before snapping to the back
// Adjust this 0.5f duration to match the length of your actual unequip animation!
yield return new WaitForSeconds(0.5f);
_isEquipped = false;
UpdateWeaponSocket();
}
private void UpdateWeaponSocket()
{
if (_currentWeapon == null) return;
Transform targetSocket = _isEquipped ? _equippedSocket : _unequippedSocket;
if (targetSocket != null)
{
_currentWeapon.transform.SetParent(targetSocket, false);
if (_isEquipped)
{
_currentWeapon.transform.localPosition = _currentWeapon.equippedPositionOffset;
_currentWeapon.transform.localRotation = Quaternion.Euler(_currentWeapon.equippedRotationOffset);
}
else
{
_currentWeapon.transform.localPosition = _currentWeapon.unequippedPositionOffset;
_currentWeapon.transform.localRotation = Quaternion.Euler(_currentWeapon.unequippedRotationOffset);
}
}
}
#region Input Handlers
private void OnAttackStarted(InputAction.CallbackContext context)
{
if (!_isEquipped || _isBlocking) return;
_isPoweringUp = true;
if (_animator != null)
{
_animator.SetBool(_powerUpHash, true);
}
}
private void OnAttackCanceled(InputAction.CallbackContext context)
{
if (_isPoweringUp)
{
_isPoweringUp = false;
if (_animator != null)
{
_animator.SetBool(_powerUpHash, false);
}
Attack();
}
}
private void OnBlockStarted(InputAction.CallbackContext context)
{
// Don't allow blocking if the weapon is put away
if (!_isEquipped) return;
_isBlocking = true;
// Slow down to walk speed while blocking
_inputReader.onWalkToggled?.Invoke();
}
private void OnBlockCanceled(InputAction.CallbackContext context)
{
if (_isBlocking)
{
_isBlocking = false;
// Return to normal speed
_inputReader.onWalkToggled?.Invoke();
}
}
#endregion
#region Combat Actions
public void Attack()
{
if (_animator != null)
{
_animator.SetTrigger(_attackHash);
}
}
/// <summary>
/// Performs hit detection. Call this from an Animation Event on the Attack animation.
/// </summary>
/// <summary>
/// Alias for Synty's built-in "Hit" animation event.
/// </summary>
public void Hit()
{
DealDamage();
}
public void DealDamage()
{
if (_currentWeapon == null)
{
Debug.LogWarning("No weapon assigned to deal damage!");
return;
}
Collider[] hitColliders;
if (_currentWeapon.hitShape == Weapon.HitShape.Sphere)
{
hitColliders = Physics.OverlapSphere(_currentWeapon.GetHitCenter(), _currentWeapon.attackRadius, _targetLayers);
}
else
{
hitColliders = Physics.OverlapBox(_currentWeapon.GetHitCenter(), _currentWeapon.hitBoxHalfExtents, _currentWeapon.GetHitRotation(), _targetLayers);
}
foreach (Collider hit in hitColliders)
{
Debug.Log($"Melee hit {hit.name} for {_currentWeapon.damage} damage!");
hit.GetComponent<WorldObject>()?.TakeDamage(_currentWeapon.damage);
}
}
public void TakeDamage(float amount)
{
if (_isBlocking)
{
Debug.Log($"Blocked {amount} damage!");
return;
}
Debug.Log($"Took {amount} damage!");
if (_animator != null)
{
_animator.SetTrigger(_takeDamageHash);
}
}
#endregion
}