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
};
}
}