OMRBotRuleContext.cs
using Sandbox;
using System;
/// <summary>
/// Normalized tactical view of the currently active OMR round rule.
///
/// BOT 6 branches directly on RoundModifier in many places. BOT 7A keeps that
/// behavior intact while establishing a single context object that later phases
/// can consume compositionally instead of scattering rule checks everywhere.
/// </summary>
public sealed class OMRBotRuleContext
{
public static OMRBotRuleContext Standard => new();
public RoundModifier Modifier { get; private set; } = RoundModifier.Standard;
public string DisplayName { get; private set; } = "STANDARD";
public OMRRoundSpecialFlow SpecialFlow { get; private set; } = OMRRoundSpecialFlow.None;
public OMRRoundWeaponPolicy WeaponPolicy { get; private set; } = OMRRoundWeaponPolicy.CompetitiveShuffle;
public float GravityScale { get; private set; } = 1f;
public int AdditionalAirJumps { get; private set; }
public bool AllowsAim { get; private set; } = true;
public bool HeadshotOnly { get; private set; }
public float FightDurationOverride { get; private set; }
public bool IsSandbox { get; private set; }
public bool IsMatch { get; private set; }
public bool IsFightActive { get; private set; }
public float FightTimeRemaining { get; private set; } = -1f;
// Transitional semantic flags. They centralize BOT 6's modifier meaning now;
// later phases can progressively replace enum-specific behavior with the more
// compositional numeric traits below.
public bool IsHighNoon => Modifier == RoundModifier.HighNoon || SpecialFlow == OMRRoundSpecialFlow.HighNoon;
public bool IsLowGravity => Modifier == RoundModifier.LowGravity;
public bool IsDoubleJump => Modifier == RoundModifier.DoubleJump || AdditionalAirJumps > 0;
public bool IsHipfireOnly => Modifier == RoundModifier.HipfireOnly || !AllowsAim;
public bool IsLargeCaliber => Modifier == RoundModifier.LargeCaliber;
public bool IsBlitz => Modifier == RoundModifier.Blitz;
/// <summary>0 = ordinary tempo, 1 = extreme urgency.</summary>
public float TempoPressure { get; private set; }
/// <summary>0 = ordinary grounded mobility, 1 = strongly air-enabled.</summary>
public float AirMobility { get; private set; }
/// <summary>0 = ordinary target selection, 1 = head placement is mandatory.</summary>
public float PrecisionPriority { get; private set; }
/// <summary>0 = ordinary magazine freedom, 1 = strong conserve/reload pressure.</summary>
public float AmmoDiscipline { get; private set; }
public void Refresh(
RoundModifier modifier,
OMRRoundRuleProfile profile,
RoundManager manager
)
{
Modifier = modifier;
DisplayName = profile?.GetDisplayName() ?? modifier.ToString().ToUpperInvariant();
SpecialFlow = profile?.SpecialFlow ?? OMRRoundSpecialFlow.None;
WeaponPolicy = profile?.WeaponPolicy ?? OMRRoundWeaponPolicy.CompetitiveShuffle;
GravityScale = MathX.Clamp( profile?.GravityScale ?? 1f, 0.05f, 3f );
AdditionalAirJumps = Math.Clamp( profile?.AdditionalAirJumps ?? 0, 0, 3 );
AllowsAim = profile?.AllowAim ?? true;
HeadshotOnly = profile?.HeadshotOnly ?? modifier == RoundModifier.HeadshotOnly;
FightDurationOverride = MathF.Max( profile?.FightDurationOverride ?? 0f, 0f );
IsSandbox = manager?.Mode == SessionMode.Sandbox;
IsMatch = manager?.Mode == SessionMode.Match;
IsFightActive = IsMatch && manager?.Phase == RoundPhase.Fighting;
FightTimeRemaining =
IsFightActive && manager.FightEndsAt > 0f
? MathF.Max( 0f, manager.FightEndsAt - Time.Now )
: -1f;
float gravityAirBonus = MathX.Clamp( ( 1f - GravityScale ) / 0.75f, 0f, 1f );
float jumpAirBonus = MathX.Clamp( AdditionalAirJumps / 2f, 0f, 1f );
AirMobility = MathX.Clamp( gravityAirBonus * 0.65f + jumpAirBonus * 0.75f, 0f, 1f );
PrecisionPriority = HeadshotOnly ? 1f : 0f;
AmmoDiscipline = IsLargeCaliber ? 1f : 0f;
TempoPressure = IsBlitz ? 1f : 0f;
if ( FightTimeRemaining >= 0f )
{
float urgency = 1f - MathX.Clamp( FightTimeRemaining / 12f, 0f, 1f );
TempoPressure = MathF.Max( TempoPressure, urgency );
}
}
}