using UnityEngine; using UnityEngine.InputSystem; using Unheimlich.Core; using Unheimlich.Features.Player.Data; using Unheimlich.Features.Dialogue; using InputManager = Unheimlich.Core.Input.InputManager; namespace Unheimlich.Features.Player { /// /// Handles camera effects: aim assist, shake, and FOV kick on sprint. /// public class CameraEffects : MonoBehaviour { [Header("Settings")] public CameraEffectsSettings Settings; private float _shakeTimer = 0f; private float _shakeInterval; private Vector3 _shakeTarget; private Vector3 _shakeOriginalPos; private float _shakeSmoothness; private Vector3 _pendingAimAssistTarget; private bool _hasAimAssistTarget; // FOV kick private float _baseFov = -1f; private float _currentFovOffset = 0f; // External FOV override — when >= 0, UpdateFovKick uses this as the base FOV // instead of the locally cached snapshot. Set by HouseOfLeavesController each frame. // Consumed once per frame — caller must re-submit every Update. private float _externalBaseFov = -1f; public bool IsAimLocked { get; private set; } public bool HasActiveLookInput { get; private set; } public bool IsUsingGamepad { get; private set; } private void Start() { if (Settings != null) _shakeInterval = 1f / Settings.shakeFrequency; if (PlayerController.Instance?.PlayerCamera != null) _baseFov = PlayerController.Instance.PlayerCamera.fieldOfView; } private void Update() { DetectInputDevice(); UpdateFovKick(); } private void LateUpdate() { if (_hasAimAssistTarget) ApplyAimAssistInternal(_pendingAimAssistTarget); } // ── FOV Kick ────────────────────────────────────────────────────────────── private void UpdateFovKick() { if (Settings == null || PlayerController.Instance?.PlayerCamera == null) return; // Use the external override if submitted this frame, otherwise fall back // to the locally cached base FOV. float effectiveBase = _externalBaseFov >= 0f ? _externalBaseFov : _baseFov; if (effectiveBase < 0f) { effectiveBase = PlayerController.Instance.PlayerCamera.fieldOfView; _baseFov = effectiveBase; } float targetOffset = PlayerController.Instance.IsRunning ? Settings.fovKickAmount : 0f; _currentFovOffset = Mathf.Lerp(_currentFovOffset, targetOffset, Time.deltaTime * Settings.fovKickSpeed); PlayerController.Instance.PlayerCamera.fieldOfView = effectiveBase + _currentFovOffset; // Consume the override — must be re-submitted next frame by the caller. _externalBaseFov = -1f; } /// /// Overrides the base FOV used by the sprint kick for this frame. /// Call every Update from HouseOfLeavesController — consumed automatically after use. /// Must be called before CameraEffects.Update runs (use Script Execution Order -10). /// public void SetBaseFovOverride(float fov) => _externalBaseFov = fov; /// /// Resets the locally cached base FOV to the camera's current fieldOfView. /// public void ResetBaseFov() { if (PlayerController.Instance?.PlayerCamera != null) _baseFov = PlayerController.Instance.PlayerCamera.fieldOfView; } // ── Input device detection ──────────────────────────────────────────────── private void DetectInputDevice() { Vector2 lookInput = InputManager.GetLookInput(); HasActiveLookInput = lookInput.magnitude > 0.1f; if (Gamepad.current != null && Gamepad.current.rightStick.ReadValue().magnitude > 0.1f) IsUsingGamepad = true; else if (Mouse.current != null && Mouse.current.delta.ReadValue().magnitude > 0.1f) IsUsingGamepad = false; } // ── Camera Shake ────────────────────────────────────────────────────────── public void ShakeCamera(float amplitude, float smoothness = 0f) { if (PlayerController.Instance == null || PlayerController.Instance.CameraPivot == null || Settings == null) return; PlayerEvents.TriggerCameraShake(amplitude); _shakeSmoothness = smoothness; _shakeTimer -= Time.deltaTime; if (_shakeTimer <= 0f) { _shakeTimer = _shakeInterval * amplitude; _shakeOriginalPos = PlayerController.Instance.CameraPivot.position; float x = Random.Range(-1f, 1f) * amplitude; float y = Random.Range(-1f, 1f) * amplitude; _shakeTarget = _shakeOriginalPos + new Vector3(x, y, 0f); } if (_shakeSmoothness > 0f) { PlayerController.Instance.CameraPivot.position = Vector3.Lerp( PlayerController.Instance.CameraPivot.position, _shakeTarget, Time.deltaTime * _shakeSmoothness); } else { PlayerController.Instance.CameraPivot.position = _shakeTarget; } } // ── Aim Assist ──────────────────────────────────────────────────────────── public void ApplyAimAssist(Vector3 targetCenter) { _pendingAimAssistTarget = targetCenter; _hasAimAssistTarget = true; } private void ApplyAimAssistInternal(Vector3 targetCenter) { if (PlayerController.Instance == null || PlayerController.Instance.CameraTransform == null || Settings == null) return; if (PlayerController.Instance.Movement == null) return; if (IsAimLocked && HasActiveLookInput) IsAimLocked = false; Vector3 directionToTarget = (targetCenter - PlayerController.Instance.CameraTransform.position).normalized; float angleError = Vector3.Angle(PlayerController.Instance.CameraTransform.forward, directionToTarget); if (IsAimLocked || angleError < Settings.aimAssistMaxAngle) { Vector3 targetDirection = directionToTarget; float targetYaw = Mathf.Atan2(targetDirection.x, targetDirection.z) * Mathf.Rad2Deg; float targetPitch = -Mathf.Asin(targetDirection.y) * Mathf.Rad2Deg; float currentYaw = PlayerController.Instance.Movement.State.Yaw; float currentPitch = PlayerController.Instance.Movement.State.Pitch; float newYaw = Mathf.LerpAngle(currentYaw, targetYaw, Settings.aimAssistStrength * Time.deltaTime); float newPitch = Mathf.Lerp(currentPitch, targetPitch, Settings.aimAssistStrength * Time.deltaTime); PlayerController.Instance.Movement.State.Yaw = newYaw; PlayerController.Instance.Movement.State.Pitch = newPitch; } _hasAimAssistTarget = false; } public void LockAim(bool locked) => IsAimLocked = locked; } }