using System; using UnityEngine; using Ashwild.Inventory; namespace Ashwild.Player { [DisallowMultipleComponent] public class PlayerToolHolder : MonoBehaviour { // ============================================================ // Tunables // ============================================================ [Header("References")] [SerializeField] private Transform toolHolder; [Header("Weapon bob")] [SerializeField] private bool enableWeaponBob = true; [SerializeField] private float bobFrequency = 8f; [SerializeField] private float bobAmplitudeY = 0.04f; [SerializeField] private float bobAmplitudeX = 0.03f; [SerializeField] private float bobSmooth = 8f; [SerializeField] private float sprintBobMultiplier = 1.4f; [SerializeField] private float crouchBobMultiplier = 0.5f; [Header("Weapon sway")] [SerializeField] private bool enableWeaponSway = true; [SerializeField] private float swayAmount = 0.02f; [SerializeField] private float swaySmooth = 6f; [SerializeField] private float swayMaxAmount = 0.06f; [Header("Idle tilt")] [SerializeField] private bool enableIdleTilt = true; [SerializeField] private float idleTiltFrequency = 0.4f; [SerializeField] private float idleTiltAmplitudeZ = 1.5f; [SerializeField] private float idleTiltAmplitudeX = 0.8f; [SerializeField] private float idleTiltSmooth = 4f; [Header("Equip / Unequip")] [SerializeField] private float equipDropOffset = -0.5f; [SerializeField] private float equipTiltAngle = -15f; [SerializeField] private float equipDuration = 0.25f; [SerializeField] private float unequipDuration = 0.12f; // ============================================================ // Wiring // ============================================================ // ============================================================ // Runtime // ============================================================ private Vector2 lookInput; private float currentSpeed; private bool isSprinting; private bool isCrouching; private bool isGrounded = true; private Vector3 toolHolderBasePos; private Quaternion toolHolderBaseRot; private Vector3 currentSway; private float bobTimer; private float idleTimer; private float currentBobIntensity; private float equipAnimProgress = 1f; private float equipAnimTimer; private bool isUnequipping; private float unequipAnimProgress = 1f; private float unequipAnimTimer; private Action onUnequipComplete; // ============================================================ // Public API (consumed by HotbarController) // ============================================================ public void PlayEquipAnimation() { equipAnimProgress = 0f; equipAnimTimer = 0f; } public void PlayUnequipAnimation(Action onComplete) { isUnequipping = true; unequipAnimProgress = 0f; unequipAnimTimer = 0f; onUnequipComplete = onComplete; } // ============================================================ // Lifecycle // ============================================================ private void Awake() { if (toolHolder != null) { toolHolderBasePos = toolHolder.localPosition; toolHolderBaseRot = toolHolder.localRotation; } } private void OnEnable() { PlayerEvents.LookInput += OnLookInput; PlayerEvents.VelocityChanged += OnVelocityChanged; PlayerEvents.SprintChanged += OnSprintChanged; PlayerEvents.CrouchChanged += OnCrouchChanged; PlayerEvents.GroundedChanged += OnGroundedChanged; } private void OnDisable() { PlayerEvents.LookInput -= OnLookInput; PlayerEvents.VelocityChanged -= OnVelocityChanged; PlayerEvents.SprintChanged -= OnSprintChanged; PlayerEvents.CrouchChanged -= OnCrouchChanged; PlayerEvents.GroundedChanged -= OnGroundedChanged; } // ============================================================ // Event handlers // ============================================================ private void OnLookInput(Vector2 v) => lookInput = v; private void OnVelocityChanged(Vector3 v) => currentSpeed = v.magnitude; private void OnSprintChanged(bool b) => isSprinting = b; private void OnCrouchChanged(bool b) => isCrouching = b; private void OnGroundedChanged(bool grounded, float _) => isGrounded = grounded; // ============================================================ // Main loop // ============================================================ private void LateUpdate() { if (toolHolder == null) return; bool dead = PlayerEvents.IsDead; if (dead) { currentBobIntensity = 0f; currentSway = Vector3.zero; toolHolder.localPosition = Vector3.Lerp(toolHolder.localPosition, toolHolderBasePos, Time.deltaTime * bobSmooth); toolHolder.localRotation = Quaternion.Slerp(toolHolder.localRotation, toolHolderBaseRot, Time.deltaTime * idleTiltSmooth); return; } UpdateBobTimers(); UpdateSway(); UpdateEquipAnimations(); ApplyToolHolderPose(); } private void UpdateBobTimers() { float dt = Time.deltaTime; float target = (isGrounded && currentSpeed > 0.1f) ? 1f : 0f; currentBobIntensity = Mathf.Lerp(currentBobIntensity, target, dt * 4f); if (isGrounded && currentSpeed > 0.1f) bobTimer += dt * bobFrequency * (currentSpeed / 5f); if (currentBobIntensity < 0.05f) idleTimer += dt; else idleTimer = 0f; } private void UpdateSway() { if (enableWeaponSway) { Vector3 target = new Vector3(-lookInput.x * swayAmount, -lookInput.y * swayAmount, 0f); target.x = Mathf.Clamp(target.x, -swayMaxAmount, swayMaxAmount); target.y = Mathf.Clamp(target.y, -swayMaxAmount, swayMaxAmount); currentSway = Vector3.Lerp(currentSway, target, Time.deltaTime * swaySmooth); } else { currentSway = Vector3.Lerp(currentSway, Vector3.zero, Time.deltaTime * swaySmooth); } } private void UpdateEquipAnimations() { if (isUnequipping) { unequipAnimTimer += Time.deltaTime; float t = Mathf.Clamp01(unequipAnimTimer / unequipDuration); unequipAnimProgress = t * t; if (t >= 1f) { isUnequipping = false; PlayerEvents.RaiseUnequipAnimComplete(); onUnequipComplete?.Invoke(); onUnequipComplete = null; } } if (equipAnimTimer < equipDuration) { equipAnimTimer += Time.deltaTime; float t = Mathf.Clamp01(equipAnimTimer / equipDuration); const float c1 = 1.70158f; const float c3 = c1 + 1f; equipAnimProgress = 1f + c3 * Mathf.Pow(t - 1f, 3f) + c1 * Mathf.Pow(t - 1f, 2f); if (t >= 1f) PlayerEvents.RaiseEquipAnimComplete(); } else { equipAnimProgress = 1f; } } private void ApplyToolHolderPose() { Vector3 targetPos = toolHolderBasePos; float mult = isCrouching ? crouchBobMultiplier : (isSprinting ? sprintBobMultiplier : 1f); if (enableWeaponBob) { targetPos.y += Mathf.Sin(bobTimer) * bobAmplitudeY * mult * currentBobIntensity; targetPos.x += Mathf.Cos(bobTimer * 0.5f) * bobAmplitudeX * mult * currentBobIntensity; } targetPos += currentSway; bool isAnimating = isUnequipping || equipAnimProgress < 1f; float animFactor = isUnequipping ? unequipAnimProgress : (1f - equipAnimProgress); if (isAnimating) { targetPos.y += animFactor * equipDropOffset; toolHolder.localPosition = targetPos; } else { toolHolder.localPosition = Vector3.Lerp(toolHolder.localPosition, targetPos, Time.deltaTime * bobSmooth); } if (enableIdleTilt || isAnimating) { float idleWeight = 1f - currentBobIntensity; float phase = idleTimer * idleTiltFrequency * Mathf.PI * 2f; float tiltZ = Mathf.Sin(phase) * idleTiltAmplitudeZ * idleWeight; float tiltX = Mathf.Cos(phase * 0.6f) * idleTiltAmplitudeX * idleWeight; if (isAnimating) tiltZ += animFactor * equipTiltAngle; Quaternion targetTilt = toolHolderBaseRot * Quaternion.Euler(tiltX, 0f, tiltZ); if (isAnimating) toolHolder.localRotation = targetTilt; else toolHolder.localRotation = Quaternion.Slerp(toolHolder.localRotation, targetTilt, Time.deltaTime * idleTiltSmooth); } } } }