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