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: "/leftButton"); [Tooltip("Input action for blocking")] [SerializeField] private InputAction _blockAction = new InputAction("Block", binding: "/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); } } /// /// Performs hit detection. Call this from an Animation Event on the Attack animation. /// /// /// Alias for Synty's built-in "Hit" animation event. /// 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()?.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 }