PlayerCameraFeel.cs
using Sandbox;
using System;
using System.Collections.Generic;

/// <summary>
/// Stable owner-local first-person camera presentation.
///
/// Architectural boundary:
/// PlayerController builds the base eye camera first (order 0), OMR adds
/// locomotion feel here (order 150), and the active weapon applies its ADS FOV
/// afterwards. This component never repositions the weapon viewmodel.
///
/// All user-facing feel values live in gameplay/default.omrfp. The hidden
/// properties retained below exist only so older player.prefab snapshots keep
/// deserializing cleanly while Revision 21 moves tuning out of the networked
/// prefab.
/// </summary>
public sealed class PlayerCameraFeel :
	Component,
	ICameraModifier,
	PlayerController.IEvents
{
	// =========================================================
	// CAMERA ORDER
	// =========================================================

	int ICameraModifier.CameraOrder => 150;

	// =========================================================
	// REFERENCES
	// =========================================================

	[Property]
	public PlayerController Controller { get; set; }

	[Property]
	public PlayerSprint Sprint { get; set; }

	[Property]
	public PlayerSlide Slide { get; set; }

	[Property]
	public BaseInventoryComponent Inventory { get; set; }

	// =========================================================
	// LEGACY PREFAB DATA (REVISION 16-20 COMPATIBILITY)
	// =========================================================
	// These are intentionally no longer used for runtime tuning. Keeping them
	// lets the existing player.prefab deserialize without a structural edit.

	[Property, Hide] public bool EnableMovementBob { get; set; } = true;
	[Property, Hide] public float BobFrequency { get; set; } = 2f;
	[Property, Hide] public float BobVerticalAmount { get; set; } = 1.2f;
	[Property, Hide] public float BobHorizontalAmount { get; set; } = 0.8f;
	[Property, Hide] public bool EnableStrafeRoll { get; set; } = true;
	[Property, Hide] public float MaxStrafeRoll { get; set; } = 1.5f;
	[Property, Hide] public bool EnableSpeedFov { get; set; } = true;
	[Property, Hide] public float MaxMovementFovBoost { get; set; } = 7f;
	[Property, Hide] public float SprintFovBoost { get; set; } = 4f;
	[Property, Hide] public float SprintBobMultiplier { get; set; } = 1.25f;
	[Property, Hide] public bool EnableSlideCamera { get; set; } = true;
	[Property, Hide] public float SlideFovBoost { get; set; } = 2.5f;
	[Property, Hide] public float SlideCameraDrop { get; set; } = 1.3f;
	[Property, Hide] public float SlideSwayAmount { get; set; } = 0.5f;
	[Property, Hide] public float SlideSwayFrequency { get; set; } = 0.85f;
	[Property, Hide] public float SlideStrafeRollReduction { get; set; } = 0.65f;
	[Property, Hide] public float SlideSmoothing { get; set; } = 0.07f;
	[Property, Hide] public float LandingSlidePunchMultiplier { get; set; } = 0.35f;
	[Property, Hide] public bool EnableAdsCameraStabilization { get; set; } = true;
	[Property, Hide] public float AdsCameraSmoothing { get; set; } = 0.08f;
	[Property, Hide] public float AdsBobScale { get; set; } = 0.18f;
	[Property, Hide] public float AdsStrafeRollScale { get; set; } = 0.20f;
	[Property, Hide] public float AdsMovementFovScale { get; set; } = 0f;
	[Property, Hide] public float AdsSlideCameraScale { get; set; } = 0.35f;
	[Property, Hide] public bool EnableZoomMatchedSensitivity { get; set; } = true;
	[Property, Hide] public float MinimumZoomSensitivityScale { get; set; } = 0.20f;
	[Property, Hide] public float SensitivitySmoothing { get; set; } = 0.04f;
	[Property, Hide] public float MovementSmoothing { get; set; } = 0.08f;
	[Property, Hide] public float SprintSmoothing { get; set; } = 0.12f;
	[Property, Hide] public bool EnableLandingResponse { get; set; } = true;
	[Property, Hide] public float LandingPitchAmount { get; set; } = 2f;

	// =========================================================
	// RUNTIME STATE
	// =========================================================

	private float _bobPhase;
	private float _slidePhase;
	private float _smoothedMovement;
	private float _smoothedRoll;
	private float _smoothedSprint;
	private float _smoothedSlide;
	private float _smoothedAim;
	private float _smoothedSensitivityScale = 1f;
	private float _baseLookSensitivity = 1f;
	private bool _wasSliding;
	private bool _cameraHookConfirmed;
	private float _nextCameraDiagnosticAt;

	// Owner-local full-body shadow proxy.
	private GameObject _firstPersonShadowObject;
	private SkinnedModelRenderer _firstPersonShadowRenderer;
	private SkinnedModelRenderer _shadowSourceRenderer;
	private GameObject _shadowedViewModel;
	private readonly Dictionary<ModelRenderer, ModelRenderer.ShadowRenderType>
		_originalViewModelShadowModes = new();
	private bool _presentationSetupLogged;

	// =========================================================
	// LIFECYCLE
	// =========================================================

	protected override void OnStart()
	{
		Controller ??= Components.Get<PlayerController>();
		Sprint ??= Components.Get<PlayerSprint>();
		Slide ??= Components.Get<PlayerSlide>();
		Inventory ??= Components.Get<BaseInventoryComponent>();

		if ( Controller is null )
		{
			Log.Warning( "PlayerCameraFeel could not find PlayerController." );
			return;
		}

		_baseLookSensitivity = Controller.LookSensitivity;
		_smoothedSensitivityScale = 1f;
		_smoothedAim = 0f;
		_smoothedMovement = 0f;
		_smoothedSprint = 0f;
		_smoothedSlide = 0f;
		_smoothedRoll = 0f;

		OMRFirstPersonTuning tuning = OMRFirstPersonResources.DefaultTuning;
		if ( tuning?.LogPresentationSetup == true )
		{
			Log.Info(
				$"[OMR FP] PlayerCameraFeel active | Proxy:{IsProxy} | CameraOrder:150"
			);
		}
	}

	protected override void OnUpdate()
	{
		if ( IsProxy || Components.Get<PlayerState>()?.IsBot == true || Controller is null )
			return;

		UpdateAimBlend();
		UpdateLookSensitivity();
	}

	protected override void OnDisabled()
	{
		RestoreLookSensitivity();
		RestoreViewModelShadows();
		DestroyFirstPersonShadowProxy();
		base.OnDisabled();
	}

	protected override void OnPreRender()
	{
		base.OnPreRender();

		if ( IsProxy || Components.Get<PlayerState>()?.IsBot == true || Controller is null )
			return;

		UpdateFirstPersonShadowPresentation();
	}

	// =========================================================
	// CAMERA MODIFIER
	// =========================================================

	public void ModifyCamera(
		CameraComponent camera,
		ref CameraView view
	)
	{
		if ( IsProxy || Components.Get<PlayerState>()?.IsBot == true || Controller is null || camera is null )
			return;

		if ( Scene.Camera is not null && camera != Scene.Camera )
			return;

		if ( !_cameraHookConfirmed )
		{
			_cameraHookConfirmed = true;

			OMRFirstPersonTuning hookTuning = OMRFirstPersonResources.DefaultTuning;
			if ( hookTuning?.LogPresentationSetup == true )
			{
				Log.Info( "[OMR FP] Camera modifier hook confirmed | CameraOrder:150" );
			}
		}

		// OMR's local gameplay FOV preference is the base for all subsequent
		// movement and weapon modifiers.
		view.FieldOfView = OMRPlayerSettings.FieldOfView;

		OMRFirstPersonTuning tuning = OMRFirstPersonResources.DefaultTuning;
		if ( tuning is null || !tuning.EnableCameraFeel )
			return;

		bool isSliding =
			Slide is not null &&
			Slide.Enabled &&
			Slide.IsSliding;

		if ( isSliding && !_wasSliding )
			_slidePhase = 0f;

		_wasSliding = isSliding;

		Vector3 horizontalVelocity = Controller.Velocity.WithZ( 0f );
		float horizontalSpeed = horizontalVelocity.Length;
		float referenceSpeed = GetReferenceMoveSpeed();

		float movementTarget = MathX.Clamp(
			horizontalSpeed / referenceSpeed,
			0f,
			1f
		);

		// Sprint FOV deliberately uses a discrete physically-resolved state.
		// This avoids the old ambiguity where both walk and sprint collapsed into
		// the same normalized speed band.
		bool physicallySprinting = IsPhysicallySprinting(
			horizontalSpeed,
			referenceSpeed
		);

		float sprintTarget = physicallySprinting ? 1f : 0f;
		float slideTarget = isSliding ? 1f : 0f;

		_smoothedMovement = MathX.ExponentialDecay(
			_smoothedMovement,
			movementTarget,
			MathF.Max( tuning.MovementSmoothing, 0.001f ),
			Time.Delta
		);

		_smoothedSprint = MathX.ExponentialDecay(
			_smoothedSprint,
			sprintTarget,
			MathF.Max( tuning.SprintSmoothing, 0.001f ),
			Time.Delta
		);

		_smoothedSlide = MathX.ExponentialDecay(
			_smoothedSlide,
			slideTarget,
			MathF.Max( tuning.SlideSmoothing, 0.001f ),
			Time.Delta
		);

		ApplyMovementFov( tuning, ref view );
		ApplyMovementBob( tuning, ref view );
		ApplySlideCamera( tuning, ref view );
		ApplyStrafeRoll(
			tuning,
			ref view,
			horizontalVelocity,
			referenceSpeed
		);

		if (
			tuning.LogCameraState &&
			RealTime.Now >= _nextCameraDiagnosticAt
		)
		{
			_nextCameraDiagnosticAt = RealTime.Now + 1f;

			float adsFovScale = GetAdsScale(
				tuning,
				tuning.AdsMovementFovScale
			);

			float movementBoost =
				tuning.MovementFovBoost * _smoothedMovement;
			float sprintBoost =
				tuning.SprintFovBoost * _smoothedSprint * (1f - _smoothedSlide);
			float slideBoost =
				tuning.EnableSlideCamera
					? tuning.SlideFovBoost * _smoothedSlide
					: 0f;

			Log.Info(
				$"[OMR FP CAM] Speed:{horizontalSpeed:0.0}/{referenceSpeed:0.0} Move:{_smoothedMovement:0.00} SprintState:{physicallySprinting} Sprint:{_smoothedSprint:0.00} Slide:{_smoothedSlide:0.00} ADS:{_smoothedAim:0.00} FovBoost:{(movementBoost + sprintBoost + slideBoost) * adsFovScale:0.00}"
			);
		}
	}

	// =========================================================
	// STATE RESOLUTION
	// =========================================================

	private float GetReferenceMoveSpeed()
	{
		if ( Sprint is not null && Sprint.Enabled )
			return MathF.Max( Sprint.BaseMoveSpeed, 1f );

		return MathF.Max( Controller.RunSpeed, 1f );
	}

	private bool IsPhysicallySprinting(
		float horizontalSpeed,
		float referenceSpeed
	)
	{
		if ( Sprint is null || !Sprint.Enabled )
			return false;

		if ( Sprint.IsSprinting )
			return true;

		// Ordering-safe fallback. PlayerLocomotion raises WalkSpeed and RunSpeed
		// to SprintSpeed when sprint is physically resolved, so presentation can
		// still identify the state even if IsSprinting is observed one tick late.
		return
			Controller.WalkSpeed > referenceSpeed + 1f &&
			horizontalSpeed > 10f;
	}

	private void UpdateAimBlend()
	{
		OMRFirstPersonTuning tuning = OMRFirstPersonResources.DefaultTuning;
		if ( tuning is null || !tuning.EnableAdsCameraStabilization )
		{
			_smoothedAim = 0f;
			return;
		}

		float target = GetActiveWeaponAimState()?.IsAiming == true ? 1f : 0f;

		_smoothedAim = MathX.ExponentialDecay(
			_smoothedAim,
			target,
			MathF.Max( tuning.AdsCameraSmoothing, 0.001f ),
			Time.Delta
		);
	}

	private float GetAdsScale(
		OMRFirstPersonTuning tuning,
		float fullAdsValue
	)
	{
		if ( !tuning.EnableAdsCameraStabilization )
			return 1f;

		return MathX.Lerp(
			1f,
			fullAdsValue,
			_smoothedAim
		);
	}

	// =========================================================
	// MOVEMENT FOV
	// =========================================================

	private void ApplyMovementFov(
		OMRFirstPersonTuning tuning,
		ref CameraView view
	)
	{
		if ( !tuning.EnableMovementFov )
			return;

		float movementBoost =
			tuning.MovementFovBoost *
			_smoothedMovement;

		float sprintBoost =
			tuning.SprintFovBoost *
			_smoothedSprint *
			(1f - _smoothedSlide);

		float slideBoost =
			tuning.EnableSlideCamera
				? tuning.SlideFovBoost * _smoothedSlide
				: 0f;

		float adsScale = GetAdsScale(
			tuning,
			tuning.AdsMovementFovScale
		);

		view.FieldOfView +=
			(movementBoost + sprintBoost + slideBoost) *
			adsScale;
	}

	// =========================================================
	// CAMERA BOB
	// =========================================================

	private void ApplyMovementBob(
		OMRFirstPersonTuning tuning,
		ref CameraView view
	)
	{
		if ( !tuning.EnableCameraBob )
			return;

		if ( _smoothedMovement <= 0.02f || !Controller.IsOnGround )
			return;

		float sprintBobScale = MathX.Lerp(
			1f,
			tuning.CameraSprintBobMultiplier,
			_smoothedSprint
		);

		float slideBobScale =
			tuning.EnableSlideCamera
				? 1f - _smoothedSlide
				: 1f;

		float adsBobScale = GetAdsScale(
			tuning,
			tuning.AdsCameraBobScale
		);

		float frequency =
			tuning.CameraBobFrequency *
			(0.35f + _smoothedMovement * 0.65f) *
			sprintBobScale;

		_bobPhase +=
			Time.Delta *
			frequency *
			MathF.PI *
			2f;

		float verticalBob =
			MathF.Sin( _bobPhase * 2f ) *
			tuning.CameraBobVerticalAmount *
			_smoothedMovement *
			sprintBobScale *
			slideBobScale *
			adsBobScale;

		float horizontalBob =
			MathF.Sin( _bobPhase ) *
			tuning.CameraBobHorizontalAmount *
			_smoothedMovement *
			sprintBobScale *
			slideBobScale *
			adsBobScale;

		view.Position += view.Rotation.Up * verticalBob;
		view.Position += view.Rotation.Right * horizontalBob;
	}

	// =========================================================
	// SLIDE CAMERA
	// =========================================================

	private void ApplySlideCamera(
		OMRFirstPersonTuning tuning,
		ref CameraView view
	)
	{
		if ( !tuning.EnableSlideCamera || _smoothedSlide <= 0.001f )
			return;

		float adsScale = GetAdsScale(
			tuning,
			tuning.AdsSlideCameraScale
		);

		view.Position +=
			view.Rotation.Up *
			(-tuning.SlideCameraDrop * _smoothedSlide * adsScale);

		_slidePhase +=
			Time.Delta *
			tuning.SlideSwayFrequency *
			MathF.PI *
			2f;

		float slideSway =
			MathF.Sin( _slidePhase ) *
			tuning.SlideSwayAmount *
			_smoothedSlide *
			adsScale;

		view.Position += view.Rotation.Right * slideSway;
	}

	// =========================================================
	// STRAFE ROLL
	// =========================================================

	private void ApplyStrafeRoll(
		OMRFirstPersonTuning tuning,
		ref CameraView view,
		Vector3 horizontalVelocity,
		float referenceSpeed
	)
	{
		if ( !tuning.EnableStrafeRoll )
			return;

		float lateralVelocity = Vector3.Dot(
			horizontalVelocity,
			view.Rotation.Right
		);

		float lateralFraction = MathX.Clamp(
			lateralVelocity / referenceSpeed,
			-1f,
			1f
		);

		float slideScale = tuning.EnableSlideCamera
			? 1f - tuning.SlideStrafeRollReduction * _smoothedSlide
			: 1f;

		float adsScale = GetAdsScale(
			tuning,
			tuning.AdsStrafeRollScale
		);

		float targetRoll =
			-lateralFraction *
			tuning.MaximumStrafeRoll *
			slideScale *
			adsScale;

		_smoothedRoll = MathX.ExponentialDecay(
			_smoothedRoll,
			targetRoll,
			MathF.Max( tuning.MovementSmoothing, 0.001f ),
			Time.Delta
		);

		view.Rotation *= Rotation.FromRoll( _smoothedRoll );
	}

	// =========================================================
	// ZOOM-MATCHED SENSITIVITY
	// =========================================================

	private void UpdateLookSensitivity()
	{
		if ( Controller is null || IsProxy || Components.Get<PlayerState>()?.IsBot == true )
			return;

		OMRFirstPersonTuning tuning = OMRFirstPersonResources.DefaultTuning;
		float userSensitivity = OMRPlayerSettings.Sensitivity;

		IWeaponOpticState opticState = GetActiveWeaponOpticState();
		float opticScale =
			opticState?.CurrentOpticSensitivityScale ?? 1f;

		opticScale = MathX.Clamp( opticScale, 0.1f, 2f );

		if ( tuning is null || !tuning.EnableZoomMatchedSensitivity )
		{
			_smoothedSensitivityScale = opticScale;
			Controller.LookSensitivity =
				_baseLookSensitivity *
				userSensitivity *
				opticScale;
			return;
		}

		CameraComponent camera = Scene.Camera;
		float targetScale = 1f;
		float baseFov = OMRPlayerSettings.FieldOfView;

		if (
			camera is not null &&
			baseFov > 1f &&
			camera.View.FieldOfView > 1f
		)
		{
			float sensitivityFov = camera.View.FieldOfView;

			// PiP optics may now apply a small presentation-only zoom to the ordinary
			// peripheral camera. Sensitivity matching should still be based on the
			// authored optic projection rather than becoming accidentally slower just
			// because the surrounding world was also narrowed a little. Recover the
			// pre-peripheral FOV first, then resolve the scope's sensitivity FOV.
			OMROpticProfile profile = opticState?.ActiveOpticProfile;
			if (
				profile is not null &&
				profile.IsValid &&
				profile.Mode == OMROpticMode.MagnifiedScope &&
				(
					profile.PresentationMode == OMROpticPresentationMode.PictureInPicture ||
					profile.PresentationMode == OMROpticPresentationMode.ModelLensPictureInPicture
				)
			)
			{
				float peripheralScale = MathX.Clamp(
					opticState.CurrentPeripheralAimFovScale,
					0.5f,
					1.5f
				);

				float opticalReferenceFov = MathX.Clamp(
					sensitivityFov / MathF.Max( peripheralScale, 0.0001f ),
					1f,
					179f
				);

				sensitivityFov = OMROpticMath.ResolveSensitivityFieldOfView(
					opticalReferenceFov,
					profile,
					opticState.AimFraction
				);
			}

			targetScale = CalculateZoomSensitivityScale(
				baseFov,
				sensitivityFov,
				tuning.MinimumZoomSensitivityScale
			);
		}

		targetScale *= opticScale;
		targetScale = MathX.Clamp( targetScale, 0.05f, 2f );

		_smoothedSensitivityScale = MathX.ExponentialDecay(
			_smoothedSensitivityScale,
			targetScale,
			MathF.Max( tuning.SensitivitySmoothing, 0.001f ),
			Time.Delta
		);

		Controller.LookSensitivity =
			_baseLookSensitivity *
			userSensitivity *
			_smoothedSensitivityScale;
	}

	private float CalculateZoomSensitivityScale(
		float baseFov,
		float composedFov,
		float minimumScale
	)
	{
		if ( composedFov >= baseFov )
			return 1f;

		float baseRadians = baseFov * (MathF.PI / 180f);
		float composedRadians = composedFov * (MathF.PI / 180f);
		float baseTangent = MathF.Tan( baseRadians * 0.5f );
		float composedTangent = MathF.Tan( composedRadians * 0.5f );

		if ( baseTangent <= 0.0001f )
			return 1f;

		return MathX.Clamp(
			composedTangent / baseTangent,
			minimumScale,
			1f
		);
	}

	private void RestoreLookSensitivity()
	{
		if ( Controller is null || IsProxy || Components.Get<PlayerState>()?.IsBot == true )
			return;

		Controller.LookSensitivity = _baseLookSensitivity;
		_smoothedSensitivityScale = 1f;
	}

	// =========================================================
	// LANDING RESPONSE
	// =========================================================

	public void OnLanded(
		float distance,
		Vector3 impactVelocity
	)
	{
		if ( IsProxy || Components.Get<PlayerState>()?.IsBot == true )
			return;

		OMRFirstPersonTuning tuning = OMRFirstPersonResources.DefaultTuning;
		if (
			tuning is null ||
			!tuning.EnableCameraFeel ||
			!tuning.EnableLandingResponse
		)
		{
			return;
		}

		CameraComponent camera = Scene.Camera;
		if ( camera is null )
			return;

		float impactSpeed = MathF.Abs( impactVelocity.z );
		float strength = MathX.Clamp(
			MathX.Remap( impactSpeed, 120f, 600f, 0f, 1f ),
			0f,
			1f
		);

		if ( strength <= 0.05f )
			return;

		bool landingIntoSlide =
			Slide is not null &&
			(Slide.IsSliding || Slide.LandingSlideQueued);

		if ( landingIntoSlide )
			strength *= tuning.LandingSlidePunchMultiplier;

		camera.AddPunch(
			new Angles(
				tuning.LandingPitchAmount * strength,
				0f,
				0f
			)
		);
	}

	// =========================================================
	// FIRST-PERSON BODY / VIEWMODEL SHADOWS
	// =========================================================

	private void UpdateFirstPersonShadowPresentation()
	{
		OMRFirstPersonTuning tuning = OMRFirstPersonResources.DefaultTuning;

		if ( tuning is null )
		{
			RestoreViewModelShadows();
			if ( _firstPersonShadowRenderer is not null )
				_firstPersonShadowRenderer.Enabled = false;
			return;
		}

		bool firstPerson = !Controller.ThirdPerson;

		if ( tuning.UseFullBodyShadow && firstPerson )
		{
			EnsureFirstPersonShadowProxy();

			if ( _firstPersonShadowRenderer is not null )
			{
				_firstPersonShadowRenderer.Enabled = true;
				_firstPersonShadowRenderer.RenderType =
					ModelRenderer.ShadowRenderType.ShadowsOnly;
			}
		}
		else if ( _firstPersonShadowRenderer is not null )
		{
			_firstPersonShadowRenderer.Enabled = false;
		}

		UpdateViewModelShadowMode(
			tuning.DisableViewModelShadows && firstPerson
		);

		if ( tuning.LogPresentationSetup && !_presentationSetupLogged )
		{
			_presentationSetupLogged = true;
			Log.Info(
				$"[OMR FP] Presentation | FullBodyShadow:{tuning.UseFullBodyShadow} ViewModelShadowsOff:{tuning.DisableViewModelShadows} RootViewModelMotion:false"
			);
		}
	}

	private void EnsureFirstPersonShadowProxy()
	{
		SkinnedModelRenderer body = Controller.Renderer;

		if ( body is null || body.Model is null )
			return;

		if (
			_firstPersonShadowRenderer is not null &&
			_shadowSourceRenderer == body &&
			_firstPersonShadowRenderer.Model == body.Model
		)
		{
			_firstPersonShadowRenderer.BodyGroups = body.BodyGroups;
			return;
		}

		DestroyFirstPersonShadowProxy();

		_firstPersonShadowObject = new GameObject( "_OMR First Person Shadow" );
		_firstPersonShadowObject.NetworkMode = NetworkMode.Never;
		_firstPersonShadowObject.Flags |=
			GameObjectFlags.NotNetworked |
			GameObjectFlags.NotSaved |
			GameObjectFlags.Hidden;
		_firstPersonShadowObject.SetParent( GameObject, false );

		_firstPersonShadowRenderer =
			_firstPersonShadowObject.AddComponent<SkinnedModelRenderer>();

		_firstPersonShadowRenderer.CopyFrom( body );
		_firstPersonShadowRenderer.Model = body.Model;
		_firstPersonShadowRenderer.BodyGroups = body.BodyGroups;
		_firstPersonShadowRenderer.BoneMergeTarget = body;
		_firstPersonShadowRenderer.UseAnimGraph = false;
		_firstPersonShadowRenderer.CreateBoneObjects = false;
		_firstPersonShadowRenderer.RenderType =
			ModelRenderer.ShadowRenderType.ShadowsOnly;

		_shadowSourceRenderer = body;
	}

	private void DestroyFirstPersonShadowProxy()
	{
		if (
			_firstPersonShadowObject is not null &&
			_firstPersonShadowObject.IsValid
		)
		{
			_firstPersonShadowObject.Destroy();
		}

		_firstPersonShadowObject = null;
		_firstPersonShadowRenderer = null;
		_shadowSourceRenderer = null;
	}

	private void UpdateViewModelShadowMode( bool disableShadows )
	{
		GameObject viewModel = GetActiveCombatWeapon()?.ViewModel;

		if ( viewModel != _shadowedViewModel )
		{
			RestoreViewModelShadows();
			_shadowedViewModel = viewModel;
		}

		if ( viewModel is null || !viewModel.IsValid )
			return;

		if ( !disableShadows )
		{
			RestoreViewModelShadows();
			_shadowedViewModel = viewModel;
			return;
		}

		foreach (
			ModelRenderer renderer in
			viewModel.Components.GetAll<ModelRenderer>(
				FindMode.InSelf | FindMode.InDescendants
			)
		)
		{
			if ( !_originalViewModelShadowModes.ContainsKey( renderer ) )
			{
				_originalViewModelShadowModes[renderer] = renderer.RenderType;
			}

			renderer.RenderType = ModelRenderer.ShadowRenderType.Off;
		}
	}

	private void RestoreViewModelShadows()
	{
		foreach (
			KeyValuePair<ModelRenderer, ModelRenderer.ShadowRenderType> pair in
			_originalViewModelShadowModes
		)
		{
			ModelRenderer renderer = pair.Key;
			if (
				renderer is null ||
				renderer.GameObject is null ||
				!renderer.GameObject.IsValid
			)
			{
				continue;
			}

			renderer.RenderType = pair.Value;
		}

		_originalViewModelShadowModes.Clear();
		_shadowedViewModel = null;
	}

	// =========================================================
	// ACTIVE WEAPON
	// =========================================================

	private IWeaponAimState GetActiveWeaponAimState()
	{
		Inventory ??= Components.Get<BaseInventoryComponent>();
		return Inventory?.ActiveItem as IWeaponAimState;
	}

	private IWeaponOpticState GetActiveWeaponOpticState()
	{
		Inventory ??= Components.Get<BaseInventoryComponent>();
		return Inventory?.ActiveItem as IWeaponOpticState;
	}

	private BaseCombatWeapon GetActiveCombatWeapon()
	{
		Inventory ??= Components.Get<BaseInventoryComponent>();
		return Inventory?.ActiveItem as BaseCombatWeapon;
	}
}