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

/// <summary>
/// Plain runtime data representing the effective authored/gameplay values of a
/// weapon. Instances are copied before modifiers are applied, so the source
/// OMRWeaponDefinition is never mutated during gameplay.
/// </summary>
public sealed class OMRWeaponStats
{
	// Identity is carried with the snapshot for diagnostics and filtering. It is
	// not intended to be changed by ordinary stat modifiers.
	public string WeaponId { get; set; } = string.Empty;
	public string DisplayName { get; set; } = string.Empty;
	public OMRWeaponArchetype Archetype { get; set; } = OMRWeaponArchetype.Unknown;

	// Fire / ballistics
	public float Damage { get; set; }
	public int Pellets { get; set; }
	public float Range { get; set; }
	public float Radius { get; set; }
	public float Force { get; set; }
	public Vector2 SpreadBase { get; set; }
	public Vector2 SpreadGrowth { get; set; }
	public float SpreadRecovery { get; set; }
	public bool PrimaryAutomatic { get; set; }
	public float PrimaryDelay { get; set; }
	public int ClipMaxSize { get; set; }
	public float ReloadTime { get; set; }
	public float DeployTime { get; set; }
	public bool AutoReload { get; set; }
	public bool CanCancelReload { get; set; }

	// Handling
	public float IronSightsFireScale { get; set; }
	public float AimFovScale { get; set; }
	public float AimFovTransitionTime { get; set; }
	public float AimAnimationSpeed { get; set; }
	public float SprintToFireDelay { get; set; }
	public float SprintFirePoseHoldTime { get; set; }

	// Accuracy
	public OMRMovementAccuracyModel MovementAccuracyModel { get; set; }
	public float MovingSpeedThreshold { get; set; }
	public float MovementAccuracyStartFraction { get; set; }
	public float MovementAccuracyFullFraction { get; set; }
	public float MovingSpreadMultiplier { get; set; }
	public float CrouchedSpreadMultiplier { get; set; }
	public float AirborneSpreadMultiplier { get; set; }
	public float SlidingSpreadMultiplier { get; set; }
	public float AimSpreadMultiplier { get; set; }

	// Recoil
	public OMRGameplayRecoilModel RecoilModel { get; set; }
	public Vector2 RecoilPitch { get; set; }
	public Vector2 RecoilYaw { get; set; }
	public OMRRecoilPattern RecoilPatternAsset { get; set; }
	public Vector2 RecoilPatternScale { get; set; } = Vector2.One;
	public List<Vector2> RecoilPattern { get; set; } = new();
	public Vector2 RecoilPatternNoise { get; set; }
	public float RecoilPatternResetDelay { get; set; }
	public bool RecoilPatternLoop { get; set; }
	public bool RecoilRecoveryEnabled { get; set; }
	public float RecoilRecoveryFraction { get; set; }
	public float RecoilRecoveryDelay { get; set; }
	public float RecoilRecoveryPitchSpeed { get; set; }
	public float RecoilRecoveryYawSpeed { get; set; }
	public float CameraRecoilRoll { get; set; }
	public float CameraRecoilFrequency { get; set; }
	public float CameraRecoilDuration { get; set; }
	public float AdsCameraRecoilScale { get; set; }

	// Crosshair
	public float CrosshairSpreadVisualScale { get; set; }
	public float CrosshairMinimumGap { get; set; }
	public float CrosshairMaximumGap { get; set; }
	public float CrosshairLineLength { get; set; }
	public float CrosshairLineWidth { get; set; }
	public Color CrosshairColor { get; set; }

	public OMRWeaponStats Clone()
	{
		return new OMRWeaponStats
		{
			WeaponId = WeaponId,
			DisplayName = DisplayName,
			Archetype = Archetype,

			Damage = Damage,
			Pellets = Pellets,
			Range = Range,
			Radius = Radius,
			Force = Force,
			SpreadBase = SpreadBase,
			SpreadGrowth = SpreadGrowth,
			SpreadRecovery = SpreadRecovery,
			PrimaryAutomatic = PrimaryAutomatic,
			PrimaryDelay = PrimaryDelay,
			ClipMaxSize = ClipMaxSize,
			ReloadTime = ReloadTime,
			DeployTime = DeployTime,
			AutoReload = AutoReload,
			CanCancelReload = CanCancelReload,

			IronSightsFireScale = IronSightsFireScale,
			AimFovScale = AimFovScale,
			AimFovTransitionTime = AimFovTransitionTime,
			AimAnimationSpeed = AimAnimationSpeed,
			SprintToFireDelay = SprintToFireDelay,
			SprintFirePoseHoldTime = SprintFirePoseHoldTime,

			MovementAccuracyModel = MovementAccuracyModel,
			MovingSpeedThreshold = MovingSpeedThreshold,
			MovementAccuracyStartFraction = MovementAccuracyStartFraction,
			MovementAccuracyFullFraction = MovementAccuracyFullFraction,
			MovingSpreadMultiplier = MovingSpreadMultiplier,
			CrouchedSpreadMultiplier = CrouchedSpreadMultiplier,
			AirborneSpreadMultiplier = AirborneSpreadMultiplier,
			SlidingSpreadMultiplier = SlidingSpreadMultiplier,
			AimSpreadMultiplier = AimSpreadMultiplier,

			RecoilModel = RecoilModel,
			RecoilPitch = RecoilPitch,
			RecoilYaw = RecoilYaw,
			RecoilPatternAsset = RecoilPatternAsset,
			RecoilPatternScale = RecoilPatternScale,
			RecoilPattern = RecoilPattern is null
				? new List<Vector2>()
				: new List<Vector2>( RecoilPattern ),
			RecoilPatternNoise = RecoilPatternNoise,
			RecoilPatternResetDelay = RecoilPatternResetDelay,
			RecoilPatternLoop = RecoilPatternLoop,
			RecoilRecoveryEnabled = RecoilRecoveryEnabled,
			RecoilRecoveryFraction = RecoilRecoveryFraction,
			RecoilRecoveryDelay = RecoilRecoveryDelay,
			RecoilRecoveryPitchSpeed = RecoilRecoveryPitchSpeed,
			RecoilRecoveryYawSpeed = RecoilRecoveryYawSpeed,
			CameraRecoilRoll = CameraRecoilRoll,
			CameraRecoilFrequency = CameraRecoilFrequency,
			CameraRecoilDuration = CameraRecoilDuration,
			AdsCameraRecoilScale = AdsCameraRecoilScale,

			CrosshairSpreadVisualScale = CrosshairSpreadVisualScale,
			CrosshairMinimumGap = CrosshairMinimumGap,
			CrosshairMaximumGap = CrosshairMaximumGap,
			CrosshairLineLength = CrosshairLineLength,
			CrosshairLineWidth = CrosshairLineWidth,
			CrosshairColor = CrosshairColor
		};
	}

	public static OMRWeaponStats FromDefinition( OMRWeaponDefinition definition )
	{
		if ( definition is null || !definition.IsValid )
			return null;

		OMROpticProfile optic =
			definition.OpticProfile is not null &&
			definition.OpticProfile.IsValid
				? definition.OpticProfile
				: null;

		return new OMRWeaponStats
		{
			WeaponId = definition.WeaponId?.Trim() ?? string.Empty,
			DisplayName = definition.DisplayName?.Trim() ?? string.Empty,
			Archetype = definition.Archetype,

			Damage = definition.Damage,
			Pellets = definition.Pellets,
			Range = definition.Range,
			Radius = definition.Radius,
			Force = definition.Force,
			SpreadBase = definition.SpreadBase,
			SpreadGrowth = definition.SpreadGrowth,
			SpreadRecovery = definition.SpreadRecovery,
			PrimaryAutomatic = definition.PrimaryAutomatic,
			PrimaryDelay = definition.PrimaryDelay,
			ClipMaxSize = definition.ClipMaxSize,
			ReloadTime = definition.ReloadTime,
			DeployTime = definition.DeployTime,
			AutoReload = definition.AutoReload,
			CanCancelReload = definition.CanCancelReload,

			IronSightsFireScale = definition.IronSightsFireScale,
			AimFovScale = optic?.AimFovScale ?? definition.AimFovScale,
			AimFovTransitionTime = optic?.AimTransitionTime ?? definition.AimFovTransitionTime,
			AimAnimationSpeed = optic?.AimAnimationSpeed ?? definition.AimAnimationSpeed,
			SprintToFireDelay = definition.SprintToFireDelay,
			SprintFirePoseHoldTime = definition.SprintFirePoseHoldTime,

			MovementAccuracyModel = definition.MovementAccuracyModel,
			MovingSpeedThreshold = definition.MovingSpeedThreshold,
			MovementAccuracyStartFraction = definition.MovementAccuracyStartFraction,
			MovementAccuracyFullFraction = definition.MovementAccuracyFullFraction,
			MovingSpreadMultiplier = definition.MovingSpreadMultiplier,
			CrouchedSpreadMultiplier = definition.CrouchedSpreadMultiplier,
			AirborneSpreadMultiplier = definition.AirborneSpreadMultiplier,
			SlidingSpreadMultiplier = definition.SlidingSpreadMultiplier,
			AimSpreadMultiplier = definition.AimSpreadMultiplier,

			RecoilModel = definition.RecoilModel,
			RecoilPitch = definition.RecoilPitch,
			RecoilYaw = definition.RecoilYaw,
			RecoilPatternAsset = definition.RecoilPatternAsset,
			RecoilPatternScale = definition.RecoilPatternScale,
			RecoilPattern = definition.RecoilPattern is null
				? new List<Vector2>()
				: new List<Vector2>( definition.RecoilPattern ),
			RecoilPatternNoise = definition.RecoilPatternNoise,
			RecoilPatternResetDelay = definition.RecoilPatternResetDelay,
			RecoilPatternLoop = definition.RecoilPatternLoop,
			RecoilRecoveryEnabled = definition.RecoilRecoveryEnabled,
			RecoilRecoveryFraction = definition.RecoilRecoveryFraction,
			RecoilRecoveryDelay = definition.RecoilRecoveryDelay,
			RecoilRecoveryPitchSpeed = definition.RecoilRecoveryPitchSpeed,
			RecoilRecoveryYawSpeed = definition.RecoilRecoveryYawSpeed,
			CameraRecoilRoll = definition.CameraRecoilRoll,
			CameraRecoilFrequency = definition.CameraRecoilFrequency,
			CameraRecoilDuration = definition.CameraRecoilDuration,
			AdsCameraRecoilScale = definition.AdsCameraRecoilScale,

			CrosshairSpreadVisualScale = definition.CrosshairSpreadVisualScale,
			CrosshairMinimumGap = definition.CrosshairMinimumGap,
			CrosshairMaximumGap = definition.CrosshairMaximumGap,
			CrosshairLineLength = definition.CrosshairLineLength,
			CrosshairLineWidth = definition.CrosshairLineWidth,
			CrosshairColor = definition.CrosshairColor
		};
	}

	public static OMRWeaponStats Capture( OneMoreRoundWeapon weapon )
	{
		if ( weapon is null )
			return null;

		OMROpticProfile optic =
			weapon.OpticProfile is not null &&
			weapon.OpticProfile.IsValid
				? weapon.OpticProfile
				: null;

		return new OMRWeaponStats
		{
			WeaponId = weapon.WeaponId?.Trim() ?? string.Empty,
			DisplayName = weapon.DisplayName?.Trim() ?? string.Empty,
			Archetype = weapon.WeaponArchetype,

			Damage = weapon.Ballistics.Damage,
			Pellets = weapon.Ballistics.Pellets,
			Range = weapon.Ballistics.Range,
			Radius = weapon.Ballistics.Radius,
			Force = weapon.Ballistics.Force,
			SpreadBase = weapon.Ballistics.SpreadBase,
			SpreadGrowth = weapon.Ballistics.SpreadGrowth,
			SpreadRecovery = weapon.Ballistics.SpreadRecovery,
			PrimaryAutomatic = weapon.PrimaryAutomatic,
			PrimaryDelay = weapon.PrimaryDelay,
			ClipMaxSize = weapon.ClipMaxSize,
			ReloadTime = weapon.ReloadTime,
			DeployTime = weapon.DeployTime,
			AutoReload = weapon.AutoReload,
			CanCancelReload = weapon.CanCancelReload,

			IronSightsFireScale = weapon.IronSightsFireScale,
			AimFovScale = optic?.AimFovScale ?? weapon.AimFovScale,
			AimFovTransitionTime = optic?.AimTransitionTime ?? weapon.AimFovTransitionTime,
			AimAnimationSpeed = optic?.AimAnimationSpeed ?? weapon.AimAnimationSpeed,
			SprintToFireDelay = weapon.SprintToFireDelay,
			SprintFirePoseHoldTime = weapon.SprintFirePoseHoldTime,

			MovementAccuracyModel = weapon.MovementAccuracyModel,
			MovingSpeedThreshold = weapon.MovingSpeedThreshold,
			MovementAccuracyStartFraction = weapon.MovementAccuracyStartFraction,
			MovementAccuracyFullFraction = weapon.MovementAccuracyFullFraction,
			MovingSpreadMultiplier = weapon.MovingSpreadMultiplier,
			CrouchedSpreadMultiplier = weapon.CrouchedSpreadMultiplier,
			AirborneSpreadMultiplier = weapon.AirborneSpreadMultiplier,
			SlidingSpreadMultiplier = weapon.SlidingSpreadMultiplier,
			AimSpreadMultiplier = weapon.AimSpreadMultiplier,

			RecoilModel = weapon.RecoilModel,
			RecoilPitch = weapon.RecoilPitch,
			RecoilYaw = weapon.RecoilYaw,
			RecoilPatternAsset = weapon.RecoilPatternAsset,
			RecoilPatternScale = weapon.RecoilPatternScale,
			RecoilPattern = weapon.RecoilPattern is null
				? new List<Vector2>()
				: new List<Vector2>( weapon.RecoilPattern ),
			RecoilPatternNoise = weapon.RecoilPatternNoise,
			RecoilPatternResetDelay = weapon.RecoilPatternResetDelay,
			RecoilPatternLoop = weapon.RecoilPatternLoop,
			RecoilRecoveryEnabled = weapon.RecoilRecoveryEnabled,
			RecoilRecoveryFraction = weapon.RecoilRecoveryFraction,
			RecoilRecoveryDelay = weapon.RecoilRecoveryDelay,
			RecoilRecoveryPitchSpeed = weapon.RecoilRecoveryPitchSpeed,
			RecoilRecoveryYawSpeed = weapon.RecoilRecoveryYawSpeed,
			CameraRecoilRoll = weapon.CameraRecoilRoll,
			CameraRecoilFrequency = weapon.CameraRecoilFrequency,
			CameraRecoilDuration = weapon.CameraRecoilDuration,
			AdsCameraRecoilScale = weapon.AdsCameraRecoilScale,

			CrosshairSpreadVisualScale = weapon.CrosshairSpreadVisualScale,
			CrosshairMinimumGap = weapon.CrosshairMinimumGap,
			CrosshairMaximumGap = weapon.CrosshairMaximumGap,
			CrosshairLineLength = weapon.CrosshairLineLength,
			CrosshairLineWidth = weapon.CrosshairLineWidth,
			CrosshairColor = weapon.CrosshairColor
		};
	}

	/// <summary>
	/// Applies only sanity constraints required for the weapon to remain valid.
	/// This deliberately avoids imposing balance opinions on the resolved values.
	/// </summary>
	public void Normalize()
	{
		WeaponId = WeaponId?.Trim() ?? string.Empty;
		DisplayName = DisplayName?.Trim() ?? string.Empty;

		Damage = MathF.Max( Damage, 0f );
		Pellets = Math.Max( Pellets, 1 );
		Range = MathF.Max( Range, 0.01f );
		Radius = MathF.Max( Radius, 0f );
		Force = MathF.Max( Force, 0f );
		SpreadBase = ClampVectorMinimum( SpreadBase, 0f );
		SpreadGrowth = ClampVectorMinimum( SpreadGrowth, 0f );
		SpreadRecovery = MathF.Max( SpreadRecovery, 0f );
		PrimaryDelay = MathF.Max( PrimaryDelay, 0.001f );
		ClipMaxSize = Math.Max( ClipMaxSize, 1 );
		ReloadTime = MathF.Max( ReloadTime, 0f );
		DeployTime = MathF.Max( DeployTime, 0f );

		IronSightsFireScale = MathF.Max( IronSightsFireScale, 0f );
		AimFovScale = MathF.Max( AimFovScale, 0.01f );
		AimFovTransitionTime = MathF.Max( AimFovTransitionTime, 0f );
		AimAnimationSpeed = MathF.Max( AimAnimationSpeed, 0.01f );
		SprintToFireDelay = MathF.Max( SprintToFireDelay, 0f );
		SprintFirePoseHoldTime = MathF.Max( SprintFirePoseHoldTime, 0f );

		MovingSpeedThreshold = MathF.Max( MovingSpeedThreshold, 0f );
		MovementAccuracyStartFraction = MathX.Clamp( MovementAccuracyStartFraction, 0f, 1f );
		MovementAccuracyFullFraction = MathX.Clamp( MovementAccuracyFullFraction, MovementAccuracyStartFraction, 1f );
		MovingSpreadMultiplier = MathF.Max( MovingSpreadMultiplier, 0f );
		CrouchedSpreadMultiplier = MathF.Max( CrouchedSpreadMultiplier, 0f );
		AirborneSpreadMultiplier = MathF.Max( AirborneSpreadMultiplier, 0f );
		SlidingSpreadMultiplier = MathF.Max( SlidingSpreadMultiplier, 0f );
		AimSpreadMultiplier = MathF.Max( AimSpreadMultiplier, 0f );

		RecoilPattern ??= new List<Vector2>();
		RecoilPatternNoise = new Vector2( MathF.Abs( RecoilPatternNoise.x ), MathF.Abs( RecoilPatternNoise.y ) );
		RecoilPatternResetDelay = MathF.Max( RecoilPatternResetDelay, 0f );
		RecoilRecoveryFraction = MathX.Clamp( RecoilRecoveryFraction, 0f, 1f );
		RecoilRecoveryDelay = MathF.Max( RecoilRecoveryDelay, 0f );
		RecoilRecoveryPitchSpeed = MathF.Max( RecoilRecoveryPitchSpeed, 0f );
		RecoilRecoveryYawSpeed = MathF.Max( RecoilRecoveryYawSpeed, 0f );

		CameraRecoilRoll = MathF.Max( CameraRecoilRoll, 0f );
		CameraRecoilFrequency = MathF.Max( CameraRecoilFrequency, 0f );
		CameraRecoilDuration = MathF.Max( CameraRecoilDuration, 0f );
		AdsCameraRecoilScale = MathF.Max( AdsCameraRecoilScale, 0f );

		CrosshairSpreadVisualScale = MathF.Max( CrosshairSpreadVisualScale, 0f );
		CrosshairMinimumGap = MathF.Max( CrosshairMinimumGap, 0f );
		CrosshairMaximumGap = MathF.Max( CrosshairMaximumGap, CrosshairMinimumGap );
		CrosshairLineLength = MathF.Max( CrosshairLineLength, 0f );
		CrosshairLineWidth = MathF.Max( CrosshairLineWidth, 0f );
	}

	public void ApplyTo( OneMoreRoundWeapon weapon, bool clampLoadedAmmoToMagazine )
	{
		if ( weapon is null )
			return;

		WeaponId = WeaponId?.Trim() ?? string.Empty;
		DisplayName = DisplayName?.Trim() ?? string.Empty;

		weapon.WeaponId = WeaponId;
		weapon.DisplayName = DisplayName;
		weapon.WeaponArchetype = Archetype;

		var ballistics = weapon.Ballistics;
		ballistics.Damage = Damage;
		ballistics.Pellets = Pellets;
		ballistics.Range = Range;
		ballistics.Radius = Radius;
		ballistics.Force = Force;
		ballistics.SpreadBase = SpreadBase;
		ballistics.SpreadGrowth = SpreadGrowth;
		ballistics.SpreadRecovery = SpreadRecovery;
		weapon.Ballistics = ballistics;

		weapon.PrimaryAutomatic = PrimaryAutomatic;
		weapon.PrimaryDelay = PrimaryDelay;
		weapon.ClipMaxSize = ClipMaxSize;
		weapon.ReloadTime = ReloadTime;
		weapon.DeployTime = DeployTime;
		weapon.AutoReload = AutoReload;
		weapon.CanCancelReload = CanCancelReload;

		weapon.IronSightsFireScale = IronSightsFireScale;
		weapon.AimFovScale = AimFovScale;
		weapon.AimFovTransitionTime = AimFovTransitionTime;
		weapon.AimAnimationSpeed = AimAnimationSpeed;
		weapon.SprintToFireDelay = SprintToFireDelay;
		weapon.SprintFirePoseHoldTime = SprintFirePoseHoldTime;

		weapon.MovementAccuracyModel = MovementAccuracyModel;
		weapon.MovingSpeedThreshold = MovingSpeedThreshold;
		weapon.MovementAccuracyStartFraction = MovementAccuracyStartFraction;
		weapon.MovementAccuracyFullFraction = MovementAccuracyFullFraction;
		weapon.MovingSpreadMultiplier = MovingSpreadMultiplier;
		weapon.CrouchedSpreadMultiplier = CrouchedSpreadMultiplier;
		weapon.AirborneSpreadMultiplier = AirborneSpreadMultiplier;
		weapon.SlidingSpreadMultiplier = SlidingSpreadMultiplier;
		weapon.AimSpreadMultiplier = AimSpreadMultiplier;

		weapon.RecoilModel = RecoilModel;
		weapon.RecoilPitch = RecoilPitch;
		weapon.RecoilYaw = RecoilYaw;
		weapon.RecoilPatternAsset = RecoilPatternAsset;
		weapon.RecoilPatternScale = RecoilPatternScale;
		weapon.RecoilPattern = RecoilPattern is null
			? new List<Vector2>()
			: new List<Vector2>( RecoilPattern );
		weapon.RecoilPatternNoise = RecoilPatternNoise;
		weapon.RecoilPatternResetDelay = RecoilPatternResetDelay;
		weapon.RecoilPatternLoop = RecoilPatternLoop;
		weapon.RecoilRecoveryEnabled = RecoilRecoveryEnabled;
		weapon.RecoilRecoveryFraction = RecoilRecoveryFraction;
		weapon.RecoilRecoveryDelay = RecoilRecoveryDelay;
		weapon.RecoilRecoveryPitchSpeed = RecoilRecoveryPitchSpeed;
		weapon.RecoilRecoveryYawSpeed = RecoilRecoveryYawSpeed;
		weapon.CameraRecoilRoll = CameraRecoilRoll;
		weapon.CameraRecoilFrequency = CameraRecoilFrequency;
		weapon.CameraRecoilDuration = CameraRecoilDuration;
		weapon.AdsCameraRecoilScale = AdsCameraRecoilScale;

		weapon.CrosshairSpreadVisualScale = CrosshairSpreadVisualScale;
		weapon.CrosshairMinimumGap = CrosshairMinimumGap;
		weapon.CrosshairMaximumGap = CrosshairMaximumGap;
		weapon.CrosshairLineLength = CrosshairLineLength;
		weapon.CrosshairLineWidth = CrosshairLineWidth;
		weapon.CrosshairColor = CrosshairColor;

		// Runtime magazine-size reductions must not leave a physically impossible
		// loaded count. Increasing capacity deliberately does not conjure ammo.
		if ( clampLoadedAmmoToMagazine && weapon.Clip1 > weapon.ClipMaxSize )
		{
			weapon.Clip1 = weapon.ClipMaxSize;
		}
	}

	private static Vector2 ClampVectorMinimum( Vector2 value, float minimum )
	{
		return new Vector2(
			MathF.Max( value.x, minimum ),
			MathF.Max( value.y, minimum )
		);
	}
}