using UnityEngine;
using Unheimlich.Core.Interaction;
using Unheimlich.World.CameraSystem;
namespace Unheimlich.Features.Player
{
///
/// Simulates camera head bobbing motion during player movement and idle states.
/// Smoothly transitions between idle, walk and run bob parameters.
/// Adds a subtle Z-axis roll to the bob and a strafe tilt applied in LateUpdate.
/// Both rolls are combined additively to avoid asymmetric drift.
///
public class CameraHeadBob : MonoBehaviour
{
private float _timer = 0f;
private Vector3 _startLocalPos;
private float _currentBobSpeed = 0f;
private float _currentBobAmplitude = 0f;
private float _currentBobRoll = 0f;
private float _currentTilt = 0f;
private CameraEffects.HeadBobMode _overrideMode = CameraEffects.HeadBobMode.Normal;
private bool _hasOverride = false;
private Coroutine _resetCoroutine;
private void Start()
{
_startLocalPos = transform.localPosition;
}
private void Update()
{
if (OctoCore.Components.OctoPlayer.Instance == null || PlayerController.Instance == null || PlayerController.Instance.CharController == null)
return;
var settings = PlayerController.Instance.CameraEffects?.Settings;
if (settings == null) return;
CameraEffects.HeadBobMode currentMode = _hasOverride ? _overrideMode : CameraEffects.HeadBobMode.Normal;
if (currentMode == CameraEffects.HeadBobMode.Disabled)
{
SetBobDirect(settings.idleBobSpeed, 0f);
_currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
if (currentMode == CameraEffects.HeadBobMode.Idle)
{
SetBobDirect(settings.idleBobSpeed, settings.idleBobAmplitude);
_currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
bool isGrounded = PlayerController.Instance.CharController.isGrounded;
if (!isGrounded)
{
SetBobDirect(settings.idleBobSpeed, 0f);
_currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude);
return;
}
float horizontalSpeed = new Vector3(
PlayerController.Instance.CharController.velocity.x,
0f,
PlayerController.Instance.CharController.velocity.z
).magnitude;
bool isRunning = OctoCore.Components.OctoPlayer.Instance.State.IsRunning;
const float WalkSpeedMs = 10f / 3.6f;
const float RunSpeedMs = 20f / 3.6f;
float maxSpeed = isRunning ? RunSpeedMs : WalkSpeedMs;
float t = Mathf.Clamp01(horizontalSpeed / maxSpeed);
float targetSpeed = Mathf.Lerp(settings.idleBobSpeed, isRunning ? settings.runBobSpeed : settings.walkBobSpeed, t);
float targetAmplitude = Mathf.Lerp(settings.idleBobAmplitude, isRunning ? settings.runBobAmplitude : settings.walkBobAmplitude, t);
SetBobDirect(targetSpeed, targetAmplitude);
_currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude);
}
private void LateUpdate()
{
if (PlayerController.Instance == null || PlayerController.Instance.CharController == null)
return;
var settings = PlayerController.Instance.CameraEffects?.Settings;
if (settings == null) return;
Vector3 velocity = PlayerController.Instance.CharController.velocity;
float strafeAmount = Vector3.Dot(velocity, PlayerController.Instance.transform.right);
float runSpeedMs = PlayerController.Instance.Movement != null ? 20f / 3.6f : 5.5f;
float strafeFactor = Mathf.Clamp(strafeAmount / runSpeedMs, -1f, 1f);
float targetTilt = -strafeFactor * settings.strafeTiltAngle;
_currentTilt = Mathf.Lerp(_currentTilt, targetTilt, Time.deltaTime * settings.strafeTiltSpeed);
// Combine bob roll + strafe tilt additively
transform.localRotation = Quaternion.Euler(0f, 0f, _currentBobRoll + _currentTilt);
}
// ── Public API ────────────────────────────────────────────────────────────
public void SetMode(CameraEffects.HeadBobMode mode)
{
_overrideMode = mode;
_hasOverride = true;
}
public void ClearOverride()
{
_hasOverride = false;
if (_resetCoroutine != null)
{
StopCoroutine(_resetCoroutine);
_resetCoroutine = null;
}
}
public void LerpToZero(float duration)
{
if (_resetCoroutine != null)
StopCoroutine(_resetCoroutine);
_resetCoroutine = StartCoroutine(LerpToZeroCoroutine(duration));
}
// ── Private helpers ───────────────────────────────────────────────────────
private void SetBobDirect(float speed, float amplitude)
{
_currentBobSpeed = speed;
_currentBobAmplitude = amplitude;
}
///
/// Applies bob position and returns the Z roll value (degrees).
/// Rotation is NOT set here — LateUpdate combines it with strafe tilt.
///
private float ApplySmoothedBob(float rollAmplitude)
{
_timer += Time.deltaTime * _currentBobSpeed;
float x = Mathf.Sin(_timer * 0.5f) * _currentBobAmplitude * 0.6f;
float y = Mathf.Cos(_timer) * _currentBobAmplitude;
float roll = Mathf.Sin(_timer * 0.5f + Mathf.PI * 0.25f) * rollAmplitude * (_currentBobAmplitude / Mathf.Max(0.001f, 0.03f));
Vector3 newPos = _startLocalPos;
newPos.x += x;
newPos.y += y;
transform.localPosition = newPos;
return roll;
}
private System.Collections.IEnumerator LerpToZeroCoroutine(float duration)
{
Vector3 startPos = transform.localPosition;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
transform.localPosition = Vector3.Lerp(startPos, _startLocalPos, t);
yield return null;
}
transform.localPosition = _startLocalPos;
_resetCoroutine = null;
}
}
}