RoundManager.BotMatch.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// BOT 7H - full competitive match integration for one host-owned bot.
/// RoundManager remains authoritative for scores, timers, loadouts and rules;
/// this partial only supplies the bot's decisions when the bot owns that choice.
/// </summary>
public sealed partial class RoundManager
{
	[Property, Sync( SyncFlags.FromHost ), Group( "Match / Bot" )]
	public bool MatchBotEnabled { get; set; }

	[Property, Sync( SyncFlags.FromHost ), Group( "Match / Bot" )]
	public OMRBotDifficulty MatchBotDifficulty { get; set; } = OMRBotDifficulty.Hard;

	[Property, Sync( SyncFlags.FromHost ), Group( "Match / Bot" )]
	public string MatchBotName { get; set; } = "ROOK";

	[Property, Group( "Match / Bot" ), Range( 0.15f, 2.5f )]
	public float MatchBotDecisionDelay { get; set; } = 0.65f;

	private RoundPhase _botDecisionPhase = (RoundPhase)(-1);
	private int _botDecisionRound = -1;
	private float _botDecisionAt = -1f;

	public void SetMatchBotDifficulty( OMRBotDifficulty difficulty )
	{
		if ( !Networking.IsHost )
			return;

		MatchBotDifficulty = difficulty;
		GetBotManager()?.SyncNow();
	}

	public void SetMatchBotEnabled( bool enabled )
	{
		if ( !Networking.IsHost )
			return;
		MatchBotEnabled = enabled;
		ResetBotMatchDecisionState();
		GetBotManager()?.SyncNow();
	}

	private void ResetBotMatchDecisionState()
	{
		_botDecisionPhase = (RoundPhase)(-1);
		_botDecisionRound = -1;
		_botDecisionAt = -1f;
	}

	private bool TryHandleBotWinnerDecision( List<PlayerState> players )
	{
		if ( !MatchBotEnabled || PotOwnerTeam < 0 )
			return false;
		PlayerState bot = players.FirstOrDefault( x => x.IsBot && x.TeamId == PotOwnerTeam );
		if ( bot is null )
			return false;
		OMRBotController controller = bot.Components.Get<OMRBotController>( true );
		if ( controller is null )
			return false;

		if ( !IsBotDecisionReady( RoundPhase.WinnerDecision ) )
			return true;

		OMRRoundRuleProfile ruleA = ResolveRoundRule( ModifierOptionA );
		OMRRoundRuleProfile ruleB = ResolveRoundRule( ModifierOptionB );
		GameObject weaponA = ResolveModifierWeaponPreviewPrefabHost( ModifierOptionA );
		GameObject weaponB = ResolveModifierWeaponPreviewPrefabHost( ModifierOptionB );
		int opponentTeam = GetOpponentTeam( PotOwnerTeam );
		bool bank = controller.DecideBankForMatch(
			GetBankedScore( PotOwnerTeam ),
			GetBankedScore( opponentTeam ),
			CurrentPot,
			GetNextRiskPot(),
			TargetScore,
			ModifierOptionA, ruleA, weaponA,
			ModifierOptionB, ruleB, weaponB
		);
		_botDecisionAt = float.PositiveInfinity;
		HandleWinnerDecision( PotOwnerTeam, bank, players );
		return true;
	}

	private bool TryHandleBotModifierDecision( List<PlayerState> players )
	{
		if ( !MatchBotEnabled || PotOwnerTeam < 0 )
			return false;
		int chooserTeam = GetOpponentTeam( PotOwnerTeam );
		PlayerState bot = players.FirstOrDefault( x => x.IsBot && x.TeamId == chooserTeam );
		if ( bot is null )
			return false;
		OMRBotController controller = bot.Components.Get<OMRBotController>( true );
		if ( controller is null )
			return false;

		if ( !IsBotDecisionReady( RoundPhase.ModifierDecision ) )
			return true;

		OMRRoundRuleProfile ruleA = ResolveRoundRule( ModifierOptionA );
		OMRRoundRuleProfile ruleB = ResolveRoundRule( ModifierOptionB );
		GameObject weaponA = ResolveModifierWeaponPreviewPrefabHost( ModifierOptionA );
		GameObject weaponB = ResolveModifierWeaponPreviewPrefabHost( ModifierOptionB );
		RoundModifier choice = controller.ChooseModifierForMatch(
			ModifierOptionA, ruleA, weaponA,
			ModifierOptionB, ruleB, weaponB
		);
		_botDecisionAt = float.PositiveInfinity;
		HandleModifierDecision( chooserTeam, choice, players );
		return true;
	}

	private bool IsBotDecisionReady( RoundPhase phase )
	{
		if ( _botDecisionPhase != phase || _botDecisionRound != RoundNumber )
		{
			_botDecisionPhase = phase;
			_botDecisionRound = RoundNumber;
			float delay = MathF.Max( 0.15f, MatchBotDecisionDelay );
			delay *= MatchBotDifficulty switch
			{
				OMRBotDifficulty.Easy => 1.25f,
				OMRBotDifficulty.Hard => 0.82f,
				_ => 1f
			};
			_botDecisionAt = Time.Now + delay + Game.Random.Float( 0.05f, 0.22f );
			return false;
		}
		return Time.Now >= _botDecisionAt || ( DecisionEndsAt > 0f && Time.Now >= DecisionEndsAt - 0.10f );
	}

	private GameObject ResolveModifierWeaponPreviewPrefabHost( RoundModifier modifier )
	{
		OMRRoundRuleProfile rule = ResolveRoundRule( modifier );
		OMRRoundLoadoutDirector loadouts = GetRoundLoadoutDirector();
		GameObject preview = loadouts?.PreviewRoundWeapon( rule );
		if ( preview is not null && preview.IsValid )
			return preview;
		return rule?.ResolveFixedWeaponPrefab();
	}
}