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