OneMoreRoundWeapon.Optics.cs
using Sandbox;
using System;

/// <summary>
/// Generic optic/ADS capability state. This owns no scope rendering; it exposes
/// a stable mechanical contract that a simple overlay or future PiP renderer can
/// consume without changing the weapon core again.
/// </summary>
public partial class OneMoreRoundWeapon : IWeaponOpticState
{
	[Property, Group( "Optics" ), Description( "Optional reusable optic profile. When a Weapon Definition is assigned, its profile becomes canonical and overwrites this legacy/prefab field during initialization." )]
	public OMROpticProfile OpticProfile { get; set; }

	public bool HasOpticProfile =>
		OpticProfile is not null &&
		OpticProfile.IsValid;

	public OMROpticProfile ActiveOpticProfile =>
		HasOpticProfile ? OpticProfile : null;

	/// <summary>
	/// 0 = hip, 1 = fully aimed. Unlike IsAiming this reflects the authored ADS
	/// transition and is therefore suitable for precision/scope thresholds.
	/// </summary>
	public float AimFraction =>
		MathX.Clamp( _aimFovBlend, 0f, 1f );

	/// <summary>
	/// Accuracy contribution of ADS. Existing weapons without an optic profile
	/// preserve the original immediate IsAiming behavior exactly.
	/// </summary>
	public float AimAccuracyBlend =>
		ResolveAimAccuracyBlend();

	/// <summary>
	/// Presentation threshold for magnified optics. Phase 5C/5D can consume this
	/// to show a simple scope or PiP lens without duplicating ADS timing logic.
	/// </summary>
	public bool IsScopePresentationActive
	{
		get
		{
			if ( !HasOpticProfile )
				return false;

			if ( OpticProfile.Mode != OMROpticMode.MagnifiedScope )
				return false;

			float threshold = MathX.Clamp(
				OpticProfile.ScopePresentationThreshold,
				0f,
				1f
			);

			return IsAiming && AimFraction >= threshold;
		}
	}

	/// <summary>
	/// Current additional sensitivity contribution from this optic. FOV-matched
	/// sensitivity remains owned by PlayerCameraFeel; this scale composes on top.
	/// </summary>
	public float CurrentOpticSensitivityScale
	{
		get
		{
			if ( !HasOpticProfile )
				return 1f;

			float fullScale = MathX.Clamp(
				OpticProfile.ScopedSensitivityMultiplier,
				0.1f,
				2f
			);

			return MathX.Lerp(
				1f,
				fullScale,
				AimFraction
			);
		}
	}

	/// <summary>
	/// Presentation-only FOV scale for the ordinary gameplay/peripheral camera
	/// while a PiP scope is aimed. Non-PiP optics keep their existing AimFovScale
	/// path and therefore always report 1 here.
	/// </summary>
	public float CurrentPeripheralAimFovScale
	{
		get
		{
			if ( !UsesPictureInPictureScope )
				return 1f;

			float fullScale = MathX.Clamp(
				OpticProfile.PeripheralAimFovScale,
				0.5f,
				1.5f
			);

			return MathX.Lerp(
				1f,
				fullScale,
				AimFraction
			);
		}
	}

	/// <summary>
	/// Additional FOV scale for OMR's dedicated viewmodel camera. The authored
	/// value blends with the same smooth start/full window used by physical
	/// viewmodel aim alignment so depth offset and projection can move together.
	/// </summary>
	public float CurrentViewModelAimFovScale
	{
		get
		{
			if ( !HasOpticProfile )
				return 1f;

			float fullScale = MathX.Clamp(
				OpticProfile.ViewModelAimFovScale,
				0.5f,
				1.5f
			);

			return MathX.Lerp(
				1f,
				fullScale,
				ResolveViewModelAimFovBlend()
			);
		}
	}

	public bool HideCrosshairWhileAiming =>
		!HasOpticProfile ||
		OpticProfile.HideCrosshairWhileAiming;

	/// <summary>
	/// True when this optic is using the real picture-in-picture presentation.
	/// Optical magnification belongs exclusively to OMRScopePiPCamera; the normal
	/// gameplay camera may optionally receive only the separate peripheral-focus
	/// FOV scale authored by the optic profile.
	/// </summary>
	public bool UsesPictureInPictureScope =>
		HasOpticProfile &&
		OpticProfile.Mode == OMROpticMode.MagnifiedScope &&
		(
			OpticProfile.PresentationMode == OMROpticPresentationMode.PictureInPicture ||
			OpticProfile.PresentationMode == OMROpticPresentationMode.ModelLensPictureInPicture
		);

	private float ResolveViewModelAimFovBlend()
	{
		if ( !HasOpticProfile )
			return AimFraction;

		float start = MathX.Clamp(
			OpticProfile.ViewModelAimAlignmentStart,
			0f,
			1f
		);

		float full = MathX.Clamp(
			OpticProfile.ViewModelAimAlignmentFull,
			start,
			1f
		);

		float t;

		if ( full <= start + 0.0001f )
		{
			t = AimFraction >= full ? 1f : 0f;
		}
		else
		{
			t = MathX.Clamp(
				(AimFraction - start) / (full - start),
				0f,
				1f
			);
		}

		return t * t * (3f - 2f * t);
	}

	private void UpdateAimTransitionState()
	{
		float target = IsAiming ? 1f : 0f;
		float transitionTime = MathF.Max( AimFovTransitionTime, 0f );

		if ( transitionTime <= 0f )
		{
			_aimFovBlend = target;
			return;
		}

		float direction = IsAiming ? 1f : -1f;
		_aimFovBlend += direction * Time.Delta / transitionTime;
		_aimFovBlend = MathX.Clamp( _aimFovBlend, 0f, 1f );
	}

	private float ResolveAimAccuracyBlend()
	{
		if ( !IsAiming )
			return 0f;

		if (
			!HasOpticProfile ||
			!OpticProfile.UseAimProgressForAccuracy
		)
		{
			return 1f;
		}

		float start = MathX.Clamp(
			OpticProfile.AccuracyBlendStart,
			0f,
			1f
		);

		float full = MathX.Clamp(
			OpticProfile.AccuracyFullThreshold,
			start,
			1f
		);

		if ( AimFraction <= start )
			return 0f;

		if ( full <= start + 0.0001f )
			return 1f;

		return MathX.Clamp(
			(AimFraction - start) / (full - start),
			0f,
			1f
		);
	}
}