Files
2026-06-22 16:18:34 +02:00

255 lines
9.6 KiB
C#

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);
}
}
}
}