OMRBotMatchupContext.cs
using Sandbox;
using System;
/// <summary>
/// BOT 7F - compositional combat interpretation of the current weapon matchup
/// and round rule. This contains preferences only; it never grants movement,
/// accuracy, health or information unavailable to a human player.
/// </summary>
public sealed class OMRBotMatchupContext
{
public static OMRBotMatchupContext Neutral { get; } = new();
public OMRBotWeaponDoctrine OwnWeapon { get; private set; } = OMRBotWeaponDoctrine.Empty;
public OMRBotWeaponDoctrine OpponentWeapon { get; private set; } = OMRBotWeaponDoctrine.Empty;
public bool KnowsOpponentWeapon { get; private set; }
public string KnowledgeSource { get; private set; } = "UNKNOWN";
public float DesiredRangeScale { get; private set; } = 1f;
public float EngageAdjustment { get; private set; }
public float PressureAdjustment { get; private set; }
public float OpenDistanceAdjustment { get; private set; }
public float CoverAdjustment { get; private set; }
public float RepositionAdjustment { get; private set; }
public float FlankAdjustment { get; private set; }
public float AmbushAdjustment { get; private set; }
public float MovementAdjustment { get; private set; }
public float AirActionAdjustment { get; private set; }
public float ReloadSafetyAdjustment { get; private set; }
public string Summary { get; private set; } = "NEUTRAL";
public void Refresh(
OMRBotWeaponDoctrine own,
OMRBotWeaponDoctrine opponent,
bool knowsOpponentWeapon,
string knowledgeSource,
OMRBotRuleContext rule
)
{
OwnWeapon = own ?? OMRBotWeaponDoctrine.Empty;
OpponentWeapon = opponent ?? OMRBotWeaponDoctrine.Empty;
KnowsOpponentWeapon = knowsOpponentWeapon && OpponentWeapon != OMRBotWeaponDoctrine.Empty;
KnowledgeSource = string.IsNullOrWhiteSpace( knowledgeSource ) ? "UNKNOWN" : knowledgeSource;
DesiredRangeScale = 1f;
EngageAdjustment = 0f;
PressureAdjustment = 0f;
OpenDistanceAdjustment = 0f;
CoverAdjustment = 0f;
RepositionAdjustment = 0f;
FlankAdjustment = 0f;
AmbushAdjustment = 0f;
MovementAdjustment = 0f;
AirActionAdjustment = 0f;
ReloadSafetyAdjustment = 0f;
ApplyWeaponMatchup();
ApplyRuleComposition( rule ?? OMRBotRuleContext.Standard );
Clamp();
UpdateSummary( rule ?? OMRBotRuleContext.Standard );
}
private void ApplyWeaponMatchup()
{
if ( OwnWeapon == OMRBotWeaponDoctrine.Empty )
return;
if ( !KnowsOpponentWeapon )
{
// Even without opponent weapon knowledge, the bot can reason from its own
// weapon role without inventing anything about the other player.
PressureAdjustment += OwnWeapon.ClosePressureBias * 0.05f;
CoverAdjustment += OwnWeapon.CoverCycleBias * 0.04f;
MovementAdjustment += ( OwnWeapon.MovementFireTolerance - 0.5f ) * 0.08f;
return;
}
float ownRange = MathF.Max( 80f, OwnWeapon.LegacyHoldDistance );
float opponentRange = MathF.Max( 80f, OpponentWeapon.LegacyHoldDistance );
float rangeRatio = opponentRange / ownRange;
// If the opponent's weapon wants a longer fight than ours, closing space is
// valuable. If ours wants more distance, preserving/opening space is valuable.
if ( rangeRatio >= 1.18f )
{
DesiredRangeScale *= 0.90f;
PressureAdjustment += 0.10f;
FlankAdjustment += 0.09f;
MovementAdjustment += 0.04f;
}
else if ( rangeRatio <= 0.84f )
{
DesiredRangeScale *= 1.12f;
OpenDistanceAdjustment += 0.13f;
CoverAdjustment += 0.07f;
RepositionAdjustment += 0.07f;
}
if ( OwnWeapon.ClosePressureBias >= 0.72f && OpponentWeapon.PrecisionBias >= 0.55f )
{
PressureAdjustment += 0.08f;
FlankAdjustment += 0.08f;
DesiredRangeScale *= 0.94f;
}
if ( OwnWeapon.PrecisionBias >= 0.55f && OpponentWeapon.ClosePressureBias >= 0.68f )
{
OpenDistanceAdjustment += 0.10f;
CoverAdjustment += 0.08f;
AmbushAdjustment += 0.05f;
DesiredRangeScale *= 1.06f;
}
// Fast mobile weapons can exploit a slow precision opponent with angle changes.
float movementEdge = OwnWeapon.MovementFireTolerance - OpponentWeapon.MovementFireTolerance;
MovementAdjustment += movementEdge * 0.10f;
RepositionAdjustment += MathF.Max( 0f, movementEdge ) * 0.07f;
// Long-cycle weapons become more attractive to pressure after a miss, while
// they themselves value cover/peek cadence more strongly.
float opponentCycle = MathX.Clamp( OpponentWeapon.FireInterval / 0.85f, 0f, 1f );
PressureAdjustment += opponentCycle * OwnWeapon.ClosePressureBias * 0.07f;
CoverAdjustment += OwnWeapon.CoverCycleBias * 0.05f;
}
private void ApplyRuleComposition( OMRBotRuleContext rule )
{
float tempo = MathX.Clamp( rule.TempoPressure, 0f, 1f );
float air = MathX.Clamp( rule.AirMobility, 0f, 1f );
float precision = MathX.Clamp( rule.PrecisionPriority, 0f, 1f );
float ammo = MathX.Clamp( rule.AmmoDiscipline, 0f, 1f );
PressureAdjustment += tempo * 0.09f;
RepositionAdjustment += tempo * 0.05f;
CoverAdjustment -= tempo * 0.04f;
// Headshot-only rewards stable, deliberate exposures rather than generic body
// pressure. Precision-oriented weapons gain more from that rule.
float precisionFit = MathX.Clamp( OwnWeapon.PrecisionBias * 0.7f + OwnWeapon.CoverCycleBias * 0.3f, 0f, 1f );
EngageAdjustment += precision * precisionFit * 0.07f;
CoverAdjustment += precision * precisionFit * 0.08f;
AmbushAdjustment += precision * precisionFit * 0.08f;
MovementAdjustment -= precision * ( 1f - OwnWeapon.MovementFireTolerance ) * 0.08f;
ReloadSafetyAdjustment += ammo * 0.14f;
CoverAdjustment += ammo * 0.06f;
PressureAdjustment -= ammo * 0.05f;
// Extra air mobility is only tactically useful for shooting if the weapon can
// tolerate airborne spread. Otherwise it remains traversal/reposition value.
float airFireFit = MathX.Clamp( OwnWeapon.AirFireTolerance, 0f, 1f );
AirActionAdjustment += air * ( airFireFit - 0.35f ) * 0.18f;
MovementAdjustment += air * 0.06f;
RepositionAdjustment += air * 0.05f;
if ( rule.IsHipfireOnly )
{
// Hipfire-only naturally favors mobile/close doctrines and devalues long
// precision holds without changing the actual weapon stats.
PressureAdjustment += OwnWeapon.ClosePressureBias * 0.08f;
MovementAdjustment += OwnWeapon.MovementFireTolerance * 0.05f;
AmbushAdjustment -= OwnWeapon.PrecisionBias * 0.04f;
}
if ( rule.IsLowGravity )
RepositionAdjustment += 0.03f + airFireFit * 0.03f;
}
private void Clamp()
{
DesiredRangeScale = MathX.Clamp( DesiredRangeScale, 0.76f, 1.28f );
EngageAdjustment = MathX.Clamp( EngageAdjustment, -0.25f, 0.25f );
PressureAdjustment = MathX.Clamp( PressureAdjustment, -0.30f, 0.30f );
OpenDistanceAdjustment = MathX.Clamp( OpenDistanceAdjustment, -0.30f, 0.30f );
CoverAdjustment = MathX.Clamp( CoverAdjustment, -0.30f, 0.30f );
RepositionAdjustment = MathX.Clamp( RepositionAdjustment, -0.30f, 0.30f );
FlankAdjustment = MathX.Clamp( FlankAdjustment, -0.30f, 0.30f );
AmbushAdjustment = MathX.Clamp( AmbushAdjustment, -0.30f, 0.30f );
MovementAdjustment = MathX.Clamp( MovementAdjustment, -0.25f, 0.25f );
AirActionAdjustment = MathX.Clamp( AirActionAdjustment, -0.25f, 0.25f );
ReloadSafetyAdjustment = MathX.Clamp( ReloadSafetyAdjustment, 0f, 0.30f );
}
private void UpdateSummary( OMRBotRuleContext rule )
{
string opponent = KnowsOpponentWeapon ? OpponentWeapon.DisplayName : "?";
Summary = $"{OwnWeapon.DisplayName} vs {opponent} · RANGE x{DesiredRangeScale:0.00} · P {PressureAdjustment:+0.00;-0.00;0.00} · C {CoverAdjustment:+0.00;-0.00;0.00} · {rule.DisplayName}";
}
}