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; [Tooltip("The player's stats script for stamina usage")] [SerializeField] private PlayerStats _playerStats; [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 Transform _spineBone; private void Start() { _meleeLayerIndex = _animator.GetLayerIndex(_meleeLayerName); _spineBone = _animator.GetBoneTransform(HumanBodyBones.Spine); // 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 bool _isAttacking; private void Update() { // 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; _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; if (_currentWeapon == null) { Debug.LogWarning("Cannot equip! No melee weapon is assigned."); 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; if (_playerStats != null) { _playerStats.staminaRegenMultiplier = 0.5f; // Half regen while blocking } // Slow down to walk speed while blocking _inputReader.onWalkToggled?.Invoke(); } private void OnBlockCanceled(InputAction.CallbackContext context) { if (_isBlocking) { _isBlocking = false; if (_playerStats != null) { _playerStats.staminaRegenMultiplier = 1f; // Restore regen rate } // Return to normal speed _inputReader.onWalkToggled?.Invoke(); } } #endregion #region Combat Actions public void Attack() { if (_animator != null) { // 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 } _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() { Debug.Log("Animation Event 'Hit' triggered!"); DealDamage(); } public void DealDamage() { Debug.Log("DealDamage called!"); 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, QueryTriggerInteraction.Ignore); } else { hitColliders = Physics.OverlapBox(_currentWeapon.GetHitCenter(), _currentWeapon.hitBoxHalfExtents, _currentWeapon.GetHitRotation(), _targetLayers, QueryTriggerInteraction.Ignore); } foreach (Collider hit in hitColliders) { Debug.Log($"Melee hit {hit.name} for {_currentWeapon.damage} damage!"); hit.GetComponent()?.TakeDamage(_currentWeapon.damage); hit.GetComponent()?.TakeDamage(_currentWeapon.damage); hit.GetComponent()?.TakeDamage(_currentWeapon.damage); } } public void TakeDamage(float amount) { if (_isBlocking) { 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; } } Debug.Log($"Took {amount} damage!"); if (_playerStats != null) { _playerStats.TakeDamage(amount); } if (_animator != null) { _animator.SetTrigger(_takeDamageHash); } } #endregion 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; } }