OMRBotWeaponDoctrine.cs
using Sandbox;
using System;

/// <summary>
/// Stable tactical interpretation of one weapon's resolved gameplay data.
///
/// The resolved OMRWeaponStats remain the mechanical source of truth. Doctrine is
/// intentionally derived data for AI reasoning: role, movement penalties and the
/// legacy BOT 6 distance thresholds required for behavior-preserving migration.
/// </summary>
public sealed class OMRBotWeaponDoctrine
{
	public static OMRBotWeaponDoctrine Empty { get; } = new();

	public string WeaponId { get; private set; } = string.Empty;
	public string DisplayName { get; private set; } = "UNARMED";
	public OMRWeaponArchetype Archetype { get; private set; } = OMRWeaponArchetype.Unknown;
	public int StatRevision { get; private set; } = -1;

	public bool IsAutomatic { get; private set; }
	public bool IsPelletWeapon { get; private set; }
	public bool AllowsAim { get; private set; }
	public bool RoundAllowsAim { get; private set; }
	public bool UsesMagazine { get; private set; }

	public float Damage { get; private set; }
	public int Pellets { get; private set; } = 1;
	public float MechanicalRange { get; private set; }
	public float FireInterval { get; private set; }
	public int MagazineSize { get; private set; }
	public float ReloadTime { get; private set; }

	public float MovingSpreadMultiplier { get; private set; } = 1f;
	public float CrouchedSpreadMultiplier { get; private set; } = 1f;
	public float AirborneSpreadMultiplier { get; private set; } = 1f;
	public float SlidingSpreadMultiplier { get; private set; } = 1f;
	public float AdsSpreadMultiplier { get; private set; } = 1f;

	// Normalized tactical traits. They are descriptive in BOT 7A and do not yet
	// alter behavior.
	public float ClosePressureBias { get; private set; }
	public float PrecisionBias { get; private set; }
	public float MovementFireTolerance { get; private set; }
	public float AirFireTolerance { get; private set; }
	public float CoverCycleBias { get; private set; }

	// BOT 8 combat-execution traits. These do not change weapon mechanics; they
	// describe how a human-like bot should exploit those mechanics.
	public float SustainedFireBias { get; private set; }
	public float BurstDisciplineBias { get; private set; }
	public float PostShotResetBias { get; private set; }
	public float AggressiveCommitBias { get; private set; }
	public float CombatStabilityBias { get; private set; }
	public float ReacquirePersistence { get; private set; }

	// Behavior-preserving semantic flags extracted from BOT 6.1's weapon-id
	// branches. These are intentionally centralized here before BOT 7 replaces
	// more of the legacy arbitration with data-driven utility considerations.
	public bool CanUseCombatSlide { get; private set; } = true;
	public bool IsPrecisionMovementWeapon { get; private set; }
	public bool SupportsSuppressiveFire { get; private set; }
	public bool PreserveSpaceAgainstRush { get; private set; }
	public bool CloseDistanceAgainstRangedOpponent { get; private set; }

	// Compatibility values reproduce the exact BOT 6.1 weapon tables. Later phases
	// can migrate callers to richer doctrine without changing the live bot in 7A.
	public float LegacyHoldDistance { get; private set; } = 280f;
	public float LegacyMaximumFireDistance { get; private set; } = 900f;
	public float LegacyAdsStartDistance { get; private set; } = 220f;
	public float LegacyHeadshotAdsStartDistance { get; private set; } = 105f;
	public bool LegacyDisableAds { get; private set; }

	public static OMRBotWeaponDoctrine FromPrefab( GameObject prefab, float sightRange )
	{
		return FromWeapon( OMRWeaponUtility.GetWeapon( prefab ), sightRange );
	}

	public static OMRBotWeaponDoctrine FromWeapon(
		OneMoreRoundWeapon weapon,
		float sightRange
	)
	{
		if ( weapon?.IsValid != true )
			return Empty;

		OMRWeaponStats stats = weapon.ResolvedStats ?? OMRWeaponStats.Capture( weapon );
		if ( stats is null )
			return Empty;

		string sourceId = string.IsNullOrWhiteSpace( stats.WeaponId )
			? weapon.WeaponId
			: stats.WeaponId;
		string id = ( sourceId ?? string.Empty ).Trim().ToLowerInvariant();

		OMRWeaponArchetype archetype = stats.Archetype != OMRWeaponArchetype.Unknown
			? stats.Archetype
			: weapon.WeaponArchetype;

		OMRBotWeaponDoctrine doctrine = new()
		{
			WeaponId = id,
			DisplayName = string.IsNullOrWhiteSpace( stats.DisplayName )
				? ( string.IsNullOrWhiteSpace( weapon.DisplayName ) ? id.ToUpperInvariant() : weapon.DisplayName )
				: stats.DisplayName,
			Archetype = archetype,
			StatRevision = weapon.StatRevision,
			IsAutomatic = stats.PrimaryAutomatic,
			IsPelletWeapon = stats.Pellets > 1,
			AllowsAim = weapon.AllowAim,
			RoundAllowsAim = weapon.RoundAllowsAim,
			UsesMagazine = weapon.UsesPrimaryClip,
			Damage = MathF.Max( 0f, stats.Damage ),
			Pellets = Math.Max( 1, stats.Pellets ),
			MechanicalRange = MathF.Max( 0f, stats.Range ),
			FireInterval = MathF.Max( 0.001f, stats.PrimaryDelay ),
			MagazineSize = Math.Max( 0, stats.ClipMaxSize ),
			ReloadTime = MathF.Max( 0f, stats.ReloadTime ),
			MovingSpreadMultiplier = MathF.Max( 0.01f, stats.MovingSpreadMultiplier ),
			CrouchedSpreadMultiplier = MathF.Max( 0.01f, stats.CrouchedSpreadMultiplier ),
			AirborneSpreadMultiplier = MathF.Max( 0.01f, stats.AirborneSpreadMultiplier ),
			SlidingSpreadMultiplier = MathF.Max( 0.01f, stats.SlidingSpreadMultiplier ),
			AdsSpreadMultiplier = MathF.Max( 0f, stats.AimSpreadMultiplier )
		};

		doctrine.ApplyLegacyCompatibility( sightRange );
		doctrine.DeriveTacticalTraits();
		return doctrine;
	}

	private void ApplyLegacyCompatibility( float sightRange )
	{
		LegacyHoldDistance = WeaponId switch
		{
			"spm4" => 115f,
			"mp5" => 210f,
			"m4a1" => 330f,
			"usp" => 290f,
			"revolver" => 330f,
			"m700" => 520f,
			_ => 280f
		};

		LegacyMaximumFireDistance = WeaponId switch
		{
			"spm4" => 300f,
			"mp5" => 760f,
			"m4a1" => 1250f,
			"usp" => 950f,
			"revolver" => 1000f,
			"m700" => sightRange,
			_ => 900f
		};

		LegacyAdsStartDistance = WeaponId switch
		{
			"spm4" => float.PositiveInfinity,
			"mp5" => 260f,
			"m4a1" => 190f,
			"usp" => 150f,
			"revolver" => 175f,
			"m700" => 90f,
			_ => 220f
		};

		LegacyHeadshotAdsStartDistance = WeaponId switch
		{
			"spm4" => float.PositiveInfinity,
			"m700" => 70f,
			_ => 105f
		};

		LegacyDisableAds = WeaponId == "spm4";

		IsPrecisionMovementWeapon = WeaponId == "m700" || WeaponId == "revolver";
		CanUseCombatSlide = !IsPrecisionMovementWeapon;
		SupportsSuppressiveFire = WeaponId == "mp5" || WeaponId == "m4a1";
		PreserveSpaceAgainstRush =
			WeaponId == "m700" ||
			WeaponId == "revolver" ||
			WeaponId == "m4a1" ||
			WeaponId == "usp";
		CloseDistanceAgainstRangedOpponent = WeaponId == "spm4" || WeaponId == "mp5";
	}

	private void DeriveTacticalTraits()
	{
		MovementFireTolerance = MathX.Clamp( 1f / MovingSpreadMultiplier, 0f, 1f );
		AirFireTolerance = MathX.Clamp( 1f / AirborneSpreadMultiplier, 0f, 1f );
		PrecisionBias = MathX.Clamp( 1f - AdsSpreadMultiplier, 0f, 1f );

		ClosePressureBias = Archetype switch
		{
			OMRWeaponArchetype.Shotgun => 1f,
			OMRWeaponArchetype.SubmachineGun => 0.82f,
			OMRWeaponArchetype.Pistol => 0.48f,
			OMRWeaponArchetype.AssaultRifle => 0.40f,
			OMRWeaponArchetype.Revolver => 0.30f,
			OMRWeaponArchetype.SniperRifle => 0.08f,
			_ => 0.42f
		};

		CoverCycleBias = Archetype switch
		{
			OMRWeaponArchetype.SniperRifle => 0.96f,
			OMRWeaponArchetype.Revolver => 0.80f,
			OMRWeaponArchetype.Pistol => 0.58f,
			OMRWeaponArchetype.AssaultRifle => 0.52f,
			OMRWeaponArchetype.Shotgun => 0.36f,
			OMRWeaponArchetype.SubmachineGun => 0.28f,
			_ => 0.50f
		};

		SustainedFireBias = Archetype switch
		{
			OMRWeaponArchetype.SubmachineGun => 0.96f,
			OMRWeaponArchetype.AssaultRifle => 0.82f,
			_ => IsAutomatic ? 0.68f : 0.08f
		};

		BurstDisciplineBias = Archetype switch
		{
			OMRWeaponArchetype.AssaultRifle => 0.72f,
			OMRWeaponArchetype.SubmachineGun => 0.34f,
			_ => IsAutomatic ? 0.50f : 0.20f
		};

		PostShotResetBias = Archetype switch
		{
			OMRWeaponArchetype.SniperRifle => 1.00f,
			OMRWeaponArchetype.Revolver => 0.82f,
			OMRWeaponArchetype.Pistol => 0.38f,
			OMRWeaponArchetype.Shotgun => 0.28f,
			OMRWeaponArchetype.AssaultRifle => 0.16f,
			OMRWeaponArchetype.SubmachineGun => 0.08f,
			_ => 0.30f
		};

		AggressiveCommitBias = Archetype switch
		{
			OMRWeaponArchetype.Shotgun => 1.00f,
			OMRWeaponArchetype.SubmachineGun => 0.88f,
			OMRWeaponArchetype.Pistol => 0.46f,
			OMRWeaponArchetype.AssaultRifle => 0.42f,
			OMRWeaponArchetype.Revolver => 0.24f,
			OMRWeaponArchetype.SniperRifle => 0.08f,
			_ => 0.40f
		};

		CombatStabilityBias = Archetype switch
		{
			OMRWeaponArchetype.SniperRifle => 1.00f,
			OMRWeaponArchetype.Revolver => 0.88f,
			OMRWeaponArchetype.Pistol => 0.62f,
			OMRWeaponArchetype.AssaultRifle => 0.58f,
			OMRWeaponArchetype.Shotgun => 0.34f,
			OMRWeaponArchetype.SubmachineGun => 0.20f,
			_ => 0.48f
		};

		ReacquirePersistence = Archetype switch
		{
			OMRWeaponArchetype.SubmachineGun => 0.92f,
			OMRWeaponArchetype.AssaultRifle => 0.84f,
			OMRWeaponArchetype.Pistol => 0.62f,
			OMRWeaponArchetype.Revolver => 0.56f,
			OMRWeaponArchetype.Shotgun => 0.54f,
			OMRWeaponArchetype.SniperRifle => 0.42f,
			_ => 0.52f
		};
	}
}