Files
FablishRPG/Assets/Scripts/MeleeCombatLayerController.cs

437 lines
14 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;
2026-09-29 16:11:54 +01:00
[Tooltip("The player's stats script for stamina usage")]
[SerializeField] private PlayerStats _playerStats;
2026-09-28 20:46:57 +01:00
[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;
2026-09-29 16:11:54 +01:00
private Transform _spineBone;
2026-09-28 20:46:57 +01:00
private void Start()
{
_meleeLayerIndex = _animator.GetLayerIndex(_meleeLayerName);
2026-09-29 16:11:54 +01:00
_spineBone = _animator.GetBoneTransform(HumanBodyBones.Spine);
2026-09-28 20:46:57 +01:00
// 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();
}
2026-09-29 16:11:54 +01:00
private bool _isAttacking;
2026-09-28 20:46:57 +01:00
private void Update()
{
2026-09-29 16:11:54 +01:00
// We only want this layer to be active if we are actively attacking, blocking, or equipping.
bool isLayerActive = false;
_isAttacking = false;
if (_animator != null)
{
// Check all layers for the Attack state
for (int i = 0; i < _animator.layerCount; i++)
{
AnimatorStateInfo stateInfo = _animator.GetCurrentAnimatorStateInfo(i);
if (stateInfo.IsTag("Attack") ||
stateInfo.IsName("Attack") ||
stateInfo.IsName("Attack1") ||
stateInfo.IsName("Attack2") ||
stateInfo.IsName("Attack3") ||
stateInfo.IsName("A_MOD_SWD_Attack_03"))
{
_isAttacking = true;
break;
}
}
if (_meleeLayerIndex != -1)
{
AnimatorStateInfo stateInfo = _animator.GetCurrentAnimatorStateInfo(_meleeLayerIndex);
bool isPlayingCombatAnim = stateInfo.IsName("Equip") ||
stateInfo.IsName("Unequip");
// If attacking on ANY layer, or blocking/equipping, keep the combat state active
if (_isPoweringUp || _isBlocking || isPlayingCombatAnim)
{
isLayerActive = true;
}
// If the attack is actually on the melee layer (old setup), keep weight 1
if (stateInfo.IsName("Attack"))
{
isLayerActive = true;
}
}
}
// Blend the melee layer weight based on if it's currently active
float equipTarget = isLayerActive ? 1f : 0f;
2026-09-28 20:46:57 +01:00
_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;
2026-09-29 16:11:54 +01:00
if (_currentWeapon == null)
{
Debug.LogWarning("Cannot equip! No melee weapon is assigned.");
yield break;
}
2026-09-28 20:46:57 +01:00
_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;
2026-09-29 16:11:54 +01:00
if (_playerStats != null)
{
_playerStats.staminaRegenMultiplier = 0.5f; // Half regen while blocking
}
2026-09-28 20:46:57 +01:00
// Slow down to walk speed while blocking
_inputReader.onWalkToggled?.Invoke();
}
private void OnBlockCanceled(InputAction.CallbackContext context)
{
if (_isBlocking)
{
_isBlocking = false;
2026-09-29 16:11:54 +01:00
if (_playerStats != null)
{
_playerStats.staminaRegenMultiplier = 1f; // Restore regen rate
}
2026-09-28 20:46:57 +01:00
// Return to normal speed
_inputReader.onWalkToggled?.Invoke();
}
}
#endregion
#region Combat Actions
public void Attack()
{
if (_animator != null)
{
2026-09-29 16:11:54 +01:00
// Optional: You can check if there's enough stamina to attack here
// e.g. if (_playerStats != null && !_playerStats.HasStaminaPercentage(0.1f)) return;
if (_playerStats != null)
{
_playerStats.UseStamina(15f); // Use 15 stamina per swing
}
2026-09-28 20:46:57 +01:00
_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()
{
2026-09-29 16:11:54 +01:00
Debug.Log("Animation Event 'Hit' triggered!");
2026-09-28 20:46:57 +01:00
DealDamage();
}
public void DealDamage()
{
2026-09-29 16:11:54 +01:00
Debug.Log("DealDamage called!");
2026-09-28 20:46:57 +01:00
if (_currentWeapon == null)
{
Debug.LogWarning("No weapon assigned to deal damage!");
return;
}
Collider[] hitColliders;
if (_currentWeapon.hitShape == Weapon.HitShape.Sphere)
{
2026-09-29 16:11:54 +01:00
hitColliders = Physics.OverlapSphere(_currentWeapon.GetHitCenter(), _currentWeapon.attackRadius, _targetLayers, QueryTriggerInteraction.Ignore);
2026-09-28 20:46:57 +01:00
}
else
{
2026-09-29 16:11:54 +01:00
hitColliders = Physics.OverlapBox(_currentWeapon.GetHitCenter(), _currentWeapon.hitBoxHalfExtents, _currentWeapon.GetHitRotation(), _targetLayers, QueryTriggerInteraction.Ignore);
2026-09-28 20:46:57 +01:00
}
foreach (Collider hit in hitColliders)
{
Debug.Log($"Melee hit {hit.name} for {_currentWeapon.damage} damage!");
hit.GetComponent<WorldObject>()?.TakeDamage(_currentWeapon.damage);
2026-09-29 16:11:54 +01:00
hit.GetComponent<NPCController>()?.TakeDamage(_currentWeapon.damage);
hit.GetComponent<PlayerStats>()?.TakeDamage(_currentWeapon.damage);
2026-09-28 20:46:57 +01:00
}
}
public void TakeDamage(float amount)
{
if (_isBlocking)
{
2026-09-29 16:11:54 +01:00
if (_playerStats != null)
{
if (_playerStats.currentStamina >= amount)
{
Debug.Log($"Blocked {amount} damage! Cost {amount} stamina.");
_playerStats.UseStamina(amount);
return;
}
else
{
Debug.Log($"Block broken! Took {amount} damage.");
_isBlocking = false;
_playerStats.staminaRegenMultiplier = 1f;
_playerStats.UseStamina(_playerStats.currentStamina); // Drain rest of stamina
_playerStats.PauseStaminaRegen(3f); // Pause regen for 3 seconds
_inputReader.onWalkToggled?.Invoke(); // Stop walking slow
// Optionally, play a guard break animation here if you have one
// _animator.SetTrigger("GuardBreak");
}
}
else
{
Debug.Log($"Blocked {amount} damage!");
return;
}
2026-09-28 20:46:57 +01:00
}
Debug.Log($"Took {amount} damage!");
2026-09-29 16:11:54 +01:00
if (_playerStats != null)
{
_playerStats.TakeDamage(amount);
}
2026-09-28 20:46:57 +01:00
if (_animator != null)
{
_animator.SetTrigger(_takeDamageHash);
}
}
#endregion
2026-09-29 16:11:54 +01:00
private void LateUpdate()
{
// If we are not attacking and the equip weight is 0, don't bend the spine
if (_spineBone == null || _cameraTransform == null) return;
if (_equipWeight <= 0f && !_isAttacking) return;
// Calculate the camera's pitch (up/down rotation)
// We use the camera's forward vector compared to the horizontal plane
Vector3 cameraForward = _cameraTransform.forward;
Vector3 horizontalForward = new Vector3(cameraForward.x, 0, cameraForward.z).normalized;
float pitch = Vector3.SignedAngle(horizontalForward, cameraForward, _cameraTransform.right);
// Clamp the pitch so we don't break the spine backward
pitch = Mathf.Clamp(pitch, -45f, 45f);
// Apply pitch to the spine bone.
// If we are doing a full-body attack, force the blend weight to 1.0!
float blendWeight = _isAttacking ? 1.0f : _equipWeight;
// We rotate around the character's right axis to lean forward/backward
_spineBone.rotation = Quaternion.AngleAxis(pitch * blendWeight, transform.right) * _spineBone.rotation;
}
2026-09-28 20:46:57 +01:00
}