OMRBotController.Brain.cs
using Sandbox;
using System;

/// <summary>
/// BOT 7A architectural bridge.
///
/// This partial owns normalized inputs consumed by the active utility controller:
/// blackboard,
/// weapon doctrine, rule context and a utility decision frame.
/// </summary>
public sealed partial class OMRBotController
{
	private readonly OMRBotBlackboard _brainBlackboard = new();
	private readonly OMRBotRuleContext _brainRuleContext = new();
	private readonly OMRBotDecisionFrame _brainDecisionFrame = new();
	private OMRBotWeaponDoctrine _brainWeaponDoctrine = OMRBotWeaponDoctrine.Empty;
	private OneMoreRoundWeapon _brainDoctrineWeapon;
	private int _brainDoctrineRevision = -1;
	private float _brainDoctrineSightRange = -1f;
	private bool _brainDoctrineAllowAim;
	private bool _brainDoctrineRoundAllowsAim;
	private OMRBotWeaponDoctrine _brainOpponentWeaponDoctrine = OMRBotWeaponDoctrine.Empty;
	private OneMoreRoundWeapon _brainOpponentDoctrineWeapon;
	private int _brainOpponentDoctrineRevision = -1;
	private readonly OMRBotMatchupContext _brainMatchupContext = new();

	/// <summary>
	/// Runtime-only read access for diagnostics/tests. These values are host
	/// AI state and are not network-authoritative gameplay properties.
	/// </summary>
	public OMRBotBlackboard BrainBlackboard => _brainBlackboard;
	public OMRBotRuleContext BrainRuleContext => _brainRuleContext;
	public OMRBotWeaponDoctrine BrainWeaponDoctrine => _brainWeaponDoctrine;
	public OMRBotDecisionFrame BrainDecisionFrame => _brainDecisionFrame;
	public OMRBotMatchupContext BrainMatchupContext => _brainMatchupContext;

	private OMRBotWeaponDoctrine GetWeaponDoctrine( OneMoreRoundWeapon weapon )
	{
		if ( weapon?.IsValid != true )
		{
			_brainDoctrineWeapon = null;
			_brainDoctrineRevision = -1;
			_brainDoctrineSightRange = SightRange;
			_brainDoctrineAllowAim = false;
			_brainDoctrineRoundAllowsAim = false;
			_brainWeaponDoctrine = OMRBotWeaponDoctrine.Empty;
			return _brainWeaponDoctrine;
		}

		bool needsRefresh =
			_brainDoctrineWeapon != weapon ||
			_brainDoctrineRevision != weapon.StatRevision ||
			MathF.Abs( _brainDoctrineSightRange - SightRange ) > 0.01f ||
			_brainDoctrineAllowAim != weapon.AllowAim ||
			_brainDoctrineRoundAllowsAim != weapon.RoundAllowsAim;

		if ( needsRefresh )
		{
			_brainDoctrineWeapon = weapon;
			_brainDoctrineRevision = weapon.StatRevision;
			_brainDoctrineSightRange = SightRange;
			_brainDoctrineAllowAim = weapon.AllowAim;
			_brainDoctrineRoundAllowsAim = weapon.RoundAllowsAim;
			_brainWeaponDoctrine = OMRBotWeaponDoctrine.FromWeapon( weapon, SightRange );
		}

		return _brainWeaponDoctrine;
	}

	private OMRBotWeaponDoctrine GetOpponentWeaponDoctrine(
		PlayerState target,
		bool hasVisual,
		OMRBotWeaponDoctrine ownDoctrine,
		out bool knowsWeapon,
		out string source
	)
	{
		knowsWeapon = false;
		source = "UNKNOWN";

		// OMR competitive rounds currently use a shared round loadout. That loadout
		// is public match information, so using our own equipped doctrine as the
		// opponent doctrine does not reveal hidden runtime state.
		if ( _roundManager?.Mode == SessionMode.Match && ownDoctrine != OMRBotWeaponDoctrine.Empty )
		{
			knowsWeapon = true;
			source = "SHARED ROUND LOADOUT";
			return ownDoctrine;
		}

		// Sandbox loadouts are explicit developer/test configuration. Treat them as
		// known information so matchup testing does not depend on line of sight.
		bool sandboxPublic = _roundManager?.Mode == SessionMode.Sandbox;
		if ( target?.IsValid == true && ( hasVisual || sandboxPublic ) )
		{
			OneMoreRoundWeapon observed = ResolveActiveWeaponFor( target );
			if ( observed?.IsValid == true )
			{
				if (
					_brainOpponentDoctrineWeapon != observed ||
					_brainOpponentDoctrineRevision != observed.StatRevision
				)
				{
					_brainOpponentDoctrineWeapon = observed;
					_brainOpponentDoctrineRevision = observed.StatRevision;
					_brainOpponentWeaponDoctrine = OMRBotWeaponDoctrine.FromWeapon( observed, SightRange );
				}

				knowsWeapon = true;
				source = sandboxPublic ? "SANDBOX LOADOUT" : "OBSERVED";
				return _brainOpponentWeaponDoctrine;
			}
		}

		return OMRBotWeaponDoctrine.Empty;
	}

	private void RefreshBrainArchitecture( OneMoreRoundWeapon weapon )
	{
		_brainRuleContext.Refresh(
			_activeModifier,
			_activeRoundRule,
			_roundManager
		);

		OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
		PlayerState target = ResolveHumanTarget( _perceptionTarget );
		bool targetAlive = target?.IsAlive == true;
		bool hasVisual = _hasConfirmedSight && targetAlive;
		OMRBotWeaponDoctrine opponentDoctrine = GetOpponentWeaponDoctrine(
			target,
			hasVisual,
			doctrine,
			out bool knowsOpponentWeapon,
			out string opponentWeaponSource
		);
		_brainMatchupContext.Refresh(
			doctrine,
			opponentDoctrine,
			knowsOpponentWeapon,
			opponentWeaponSource,
			_brainRuleContext
		);
		MatchupStatus = _brainMatchupContext.Summary;

		bool hasMemory = HasFreshMemory();
		bool hasAudio = HasActionableHearing();
		bool recentThreat = HasRecentThreat();

		float opponentHealth = 1f;
		// BOT 7 utility decisions only use opponent health while the opponent is
		// actually visible. The legacy system can still read the replicated value,
		// but the new blackboard does not turn scoreboard state into hidden combat
		// knowledge.
		bool knowsOpponentHealth = hasVisual && TryGetOpponentHealthFraction( out opponentHealth );

		bool hasBelief = TryGetBestOpponentBelief( out Vector3 beliefPosition, out float beliefConfidence );

		Vector3 velocity =
			_controller?.IsValid == true
				? _controller.Velocity
				: Vector3.Zero;

		float targetDistance = 0f;
		if ( hasVisual )
		{
			targetDistance = ( _observedTargetPosition - WorldPosition ).WithZ( 0f ).Length;
		}
		else if ( hasMemory )
		{
			targetDistance = ( LastKnownPosition - WorldPosition ).WithZ( 0f ).Length;
		}
		else if ( hasAudio )
		{
			targetDistance = ( _lastHeardPosition - WorldPosition ).WithZ( 0f ).Length;
		}
		else if ( hasBelief )
		{
			targetDistance = ( beliefPosition - WorldPosition ).WithZ( 0f ).Length;
		}

		OMRBotContactState contactState = OMRBotContactState.None;
		if ( PerceptionStatus == "REACTING" )
			contactState = OMRBotContactState.Reacting;
		else if ( hasVisual )
			contactState = OMRBotContactState.Visual;
		else if ( hasMemory )
			contactState = OMRBotContactState.VisualMemory;
		else if ( hasAudio )
			contactState = OMRBotContactState.Audio;
		else if ( recentThreat )
			contactState = OMRBotContactState.ThreatDirection;
		else if ( hasBelief )
			contactState = OMRBotContactState.Belief;

		_brainBlackboard.UpdatedAt = Time.Now;
		_brainBlackboard.Difficulty = Difficulty;
		_brainBlackboard.BehaviorMode = BehaviorMode;
		_brainBlackboard.BotPosition = WorldPosition;
		_brainBlackboard.BotVelocity = velocity;
		_brainBlackboard.OwnHealthFraction = GetOwnHealthFraction();
		_brainBlackboard.OpponentHealthFraction = opponentHealth;
		_brainBlackboard.KnowsOpponentHealth = knowsOpponentHealth;
		_brainBlackboard.ContactState = contactState;
		_brainBlackboard.HasConfirmedSight = hasVisual;
		_brainBlackboard.HasVisualMemory = hasMemory;
		_brainBlackboard.HasActionableAudio = hasAudio;
		_brainBlackboard.HasRecentThreat = recentThreat;
		_brainBlackboard.HasOpponentBelief = hasBelief;
		_brainBlackboard.OpponentBeliefPosition = hasBelief ? beliefPosition : Vector3.Zero;
		_brainBlackboard.OpponentBeliefConfidence = hasBelief ? beliefConfidence : 0f;
		_brainBlackboard.TargetAlive = targetAlive;
		_brainBlackboard.TargetName = target?.DisplayName ?? "NONE";
		_brainBlackboard.TargetDistance = targetDistance;
		_brainBlackboard.ObservedTargetPosition = _observedTargetPosition;
		_brainBlackboard.ObservedTargetAimPoint = _observedTargetAimPoint;
		_brainBlackboard.LastKnownPosition = LastKnownPosition;
		_brainBlackboard.LastSeenAge = hasMemory
			? MathF.Max( 0f, Time.Now - _lastConfirmedSeenAt )
			: -1f;
		_brainBlackboard.LastSeenVelocity = _lastSeenVelocity;
		_brainBlackboard.LastSeenDirection = _lastSeenDirection;
		_brainBlackboard.LastHeardPosition = _lastHeardPosition;
		_brainBlackboard.LastHeardAge = HasFreshHearing()
			? MathF.Max( 0f, Time.Now - _lastHeardAt )
			: -1f;
		_brainBlackboard.LastHeardConfidence = _lastHeardConfidence;
		_brainBlackboard.LastHeardKind = _lastHeardKind;
		_brainBlackboard.LastThreatDirection = _lastThreatDirection;
		_brainBlackboard.ThreatAge = recentThreat
			? MathF.Max( 0f, Time.Now - _lastThreatAt )
			: -1f;
		_brainBlackboard.HasCoverPlan = _hasCoverTarget;
		_brainBlackboard.HasFlankPlan = _flankActive;
		_brainBlackboard.HasSearchPlan = _searchPoints.Count > 0;
		_brainBlackboard.IsSuppressing = _suppressionShotsRemaining > 0 || Time.Now < _suppressionUntil;
		_brainBlackboard.TacticalAggression = TacticalAggression;
		_brainBlackboard.LegacyIntent = CurrentIntent ?? string.Empty;
		_brainBlackboard.Weapon = doctrine;
		_brainBlackboard.OpponentWeapon = opponentDoctrine;
		_brainBlackboard.KnowsOpponentWeapon = knowsOpponentWeapon;
		_brainBlackboard.Matchup = _brainMatchupContext;
		_brainBlackboard.Rule = _brainRuleContext;

		OMRBotOpponentModelSnapshot opponent = _brainBlackboard.OpponentModel;
		opponent.Samples = _adaptiveSampleCount;
		opponent.Confidence = GetAdaptiveConfidenceWeight();
		opponent.Aggression = _adaptiveOpponentAggression;
		opponent.Mobility = _adaptiveOpponentMobility;
		opponent.AirUse = _adaptiveOpponentAirUse;
		opponent.CrouchUse = _adaptiveOpponentCrouchUse;
		opponent.PreferredDistance = _adaptivePreferredDistance;
		opponent.RoutePredictionConfidence = PlayerRoutePredictionConfidence;
		opponent.RepeekTendency = PlayerRepeekTendency;
		opponent.PushTendency = PlayerPushTendency;
		opponent.Summary = AdaptationStatus ?? "LEARNING";

		// BOT 7D owns this frame while enabled. Keeping the previous decision between
		// utility ticks makes the debug view stable and preserves commitment/hysteresis.
		if ( !UtilityCombatBrainEnabled )
			_brainDecisionFrame.Clear();
	}

	private void ResetBrainArchitecture()
	{
		_brainDoctrineWeapon = null;
		_brainDoctrineRevision = -1;
		_brainDoctrineSightRange = -1f;
		_brainDoctrineAllowAim = false;
		_brainDoctrineRoundAllowsAim = false;
		_brainWeaponDoctrine = OMRBotWeaponDoctrine.Empty;
		_brainOpponentWeaponDoctrine = OMRBotWeaponDoctrine.Empty;
		_brainOpponentDoctrineWeapon = null;
		_brainOpponentDoctrineRevision = -1;
		_brainMatchupContext.Refresh( OMRBotWeaponDoctrine.Empty, OMRBotWeaponDoctrine.Empty, false, "RESET", OMRBotRuleContext.Standard );
		MatchupStatus = "NEUTRAL";
		_brainBlackboard.Reset();
		_brainRuleContext.Refresh( RoundModifier.Standard, null, null );
		_brainDecisionFrame.Clear();
	}
}