OMRBotController.UtilityBrain.cs
using Sandbox;
using System;
using System.Collections.Generic;
/// <summary>
/// BOT 7D - Utility Combat Brain.
///
/// BOT 6.1/7C already supplied a large vocabulary of useful behaviours, but the
/// old coordinator was mostly a priority chain. BOT 7D makes the *intent* a
/// scored decision while deliberately reusing the proven cover/flank/search/
/// suppression/execution systems underneath it.
///
/// Hard rules (High Noon, impossible actions, empty weapons and immediate blind
/// incoming-fire reactions) remain hard rules. Everything else competes in one
/// inspectable decision frame with commitment/hysteresis so the bot does not
/// change its mind every few frames.
/// </summary>
public sealed partial class OMRBotController
{
[Property, Group( "BOT 7D / Utility Brain" )]
public bool UtilityCombatBrainEnabled { get; set; } = true;
[Property, Group( "BOT 7D / Utility Brain" )]
public float UtilityDecisionInterval { get; set; } = 0.28f;
[Property, Group( "BOT 7D / Utility Brain" )]
public float UtilityMinimumCommitTime { get; set; } = 0.65f;
[Property, Group( "BOT 7D / Utility Brain" ), Range( 0f, 0.35f )]
public float UtilitySwitchMargin { get; set; } = 0.08f;
[Property, Group( "BOT 7D / Utility Brain" ), Range( 0f, 0.35f )]
public float UtilityCurrentIntentBonus { get; set; } = 0.08f;
[Property, Group( "BOT 7D / Utility Brain" )]
public float UtilityRepositionDistance { get; set; } = 230f;
[Property, Sync( SyncFlags.FromHost )]
public string UtilityBrainStatus { get; set; } = "LEGACY";
private OMRBotIntentKind _utilitySelectedIntent = OMRBotIntentKind.None;
private float _utilitySelectedScore;
private string _utilitySelectedReason = string.Empty;
private Vector3 _utilitySelectedTarget = Vector3.Zero;
private bool _utilityHasSelectedTarget;
private float _utilityTargetChosenAt = -999f;
private float _utilitySelectedAt = -999f;
private float _utilityCommitUntil = -999f;
private float _nextUtilityDecisionAt;
private float _utilityRangeScale = 1f;
private float _utilityCoverFailureUntil = -999f;
private float _utilityFlankFailureUntil = -999f;
private float _utilityPositionFailureUntil = -999f;
private bool TryHandleTacticalPriority( OneMoreRoundWeapon weapon )
{
if ( UtilityCombatBrainEnabled )
return TryHandleUtilityCombatBrain( weapon );
UtilityBrainStatus = "LEGACY";
return TryHandleLegacyTacticalPriority( weapon );
}
private bool TryHandleUtilityCombatBrain( OneMoreRoundWeapon weapon )
{
if ( !UsesTacticalBehavior() )
return false;
bool hasVisual = _hasConfirmedSight && _perceptionTarget?.IsAlive == true;
bool recentThreat = HasRecentThreat();
if ( hasVisual && _searchPoints.Count > 0 )
ClearSearchPlan( "CONTACT" );
// Level-2 emergency from the architecture audit: being hit from an unseen
// direction gets an immediate orient/evasion window before deliberation.
if ( recentThreat && !hasVisual && Time.Now < _threatOrientUntil )
{
bool evadeNow = ShouldEvadeBlindThreatImmediately();
SetForcedUtilityDecision(
evadeNow ? OMRBotIntentKind.Disengage : OMRBotIntentKind.Reposition,
1f,
evadeNow ? "REPEATED BLIND FIRE" : "LOCATE INCOMING FIRE"
);
if ( evadeNow )
UpdateUnderFireReaction( weapon );
else
UpdateBlindThreatLocateReaction( weapon );
return true;
}
// Raw LOS is not usable combat information until BOT 2's reaction delay has
// elapsed. Let the existing reaction gate own this frame rather than allowing
// a belief/search action to accidentally bypass human-like reaction timing.
if ( PerceptionStatus == "REACTING" )
{
UtilityBrainStatus = "REACTION GATE";
return false;
}
if ( Time.Now >= _nextUtilityDecisionAt || !IsUtilityIntentStillLegal( _utilitySelectedIntent, weapon ) )
EvaluateUtilityCombatDecision( weapon );
return ExecuteUtilityCombatDecision( weapon );
}
private void EvaluateUtilityCombatDecision( OneMoreRoundWeapon weapon )
{
RefreshBrainArchitecture( weapon );
_brainDecisionFrame.Clear();
_utilityRangeScale = 1f;
float interval = MathF.Max( 0.12f, UtilityDecisionInterval );
interval *= Difficulty switch
{
OMRBotDifficulty.Easy => 1.30f,
OMRBotDifficulty.Hard => 0.82f,
_ => 1f
};
_nextUtilityDecisionAt = Time.Now + interval;
OMRBotBlackboard bb = _brainBlackboard;
OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
OMRBotMatchupContext matchup = WeaponMatchupReasoningEnabled ? ( bb.Matchup ?? OMRBotMatchupContext.Neutral ) : OMRBotMatchupContext.Neutral;
bool hasVisual = bb.HasConfirmedSight;
bool hasMemory = bb.HasVisualMemory;
bool hasAudio = bb.HasActionableAudio;
bool recentThreat = bb.HasRecentThreat;
bool hasBelief = bb.HasOpponentBelief;
bool hasEvidence = hasVisual || hasMemory || hasAudio || recentThreat || hasBelief;
bool needsReload = UtilityNeedsReload( weapon );
float health = MathX.Clamp( bb.OwnHealthFraction, 0f, 1f );
float aggression = MathX.Clamp( bb.TacticalAggression, 0f, 1f );
float caution = 1f - aggression;
float desiredRange = MathF.Max( 80f, doctrine.LegacyHoldDistance * matchup.DesiredRangeScale );
float distance = MathF.Max( 0f, bb.TargetDistance );
float rangeFit = hasVisual || hasMemory || hasAudio || hasBelief
? 1f - MathX.Clamp( MathF.Abs( distance - desiredRange ) / desiredRange, 0f, 1f )
: 0.5f;
float tooClose = hasVisual
? MathX.Clamp( ( desiredRange - distance ) / desiredRange, 0f, 1f )
: 0f;
float tooFar = hasVisual
? MathX.Clamp( ( distance - desiredRange ) / MathF.Max( desiredRange, 1f ), 0f, 1f )
: 0f;
float adaptive = MathX.Clamp( bb.OpponentModel.Confidence, 0f, 1f );
float tempo = MathX.Clamp( bb.Rule.TempoPressure, 0f, 1f );
float currentRisk = MathX.Clamp( GetSpatialMemoryRiskAt( WorldPosition ) / 8f, 0f, 1f );
int blindThreatHits = !hasVisual ? GetRecentBlindThreatHits() : 0;
float blindThreatEscalation = MathX.Clamp( ( blindThreatHits - 1 ) / 2f, 0f, 1f );
bool automaticWeapon = weapon?.IsAutomaticWeapon == true;
float engage = hasVisual
? 0.40f + aggression * 0.22f + rangeFit * 0.20f + tempo * 0.10f
: -1f;
engage += _dynamicPressureBias * 0.07f;
engage += matchup.EngageAdjustment;
if ( automaticWeapon ) engage += 0.14f;
engage -= ( 1f - health ) * 0.16f;
if ( bb.KnowsOpponentHealth )
engage += ( 1f - bb.OpponentHealthFraction ) * 0.10f;
if ( weapon?.Clip1 > 0 ) engage += 0.06f;
if ( needsReload ) engage -= 0.65f;
AddUtilityCandidate( OMRBotIntentKind.Engage, engage, hasVisual, "VISIBLE / RANGE FIT" );
float press = hasVisual
? 0.16f + doctrine.ClosePressureBias * 0.34f + tooFar * 0.32f + aggression * 0.18f + tempo * 0.14f
: -1f;
press += _dynamicPressureBias * 0.08f;
press += matchup.PressureAdjustment;
if ( automaticWeapon ) press += 0.10f;
press -= ( 1f - health ) * 0.16f;
press -= doctrine.PrecisionBias * 0.10f;
if ( needsReload ) press -= 0.75f;
AddUtilityCandidate( OMRBotIntentKind.Press, press, hasVisual, "CLOSE RANGE / PRESSURE" );
float openDistance = hasVisual
? 0.10f + doctrine.PrecisionBias * 0.28f + doctrine.CoverCycleBias * 0.12f + tooClose * 0.42f
: -1f;
if ( doctrine.PreserveSpaceAgainstRush ) openDistance += 0.10f;
openDistance += matchup.OpenDistanceAdjustment;
openDistance += MathF.Max( 0f, bb.OpponentModel.Aggression - 0.52f ) * adaptive * 0.16f;
openDistance += ( 1f - health ) * 0.10f;
AddUtilityCandidate( OMRBotIntentKind.OpenDistance, openDistance, hasVisual, "CREATE SPACE" );
float cover = hasEvidence
? 0.10f + ( 1f - health ) * 0.38f + doctrine.CoverCycleBias * 0.18f + caution * 0.14f
: 0.04f;
cover += recentThreat
? MathX.Lerp( 0.08f, 0.30f, hasVisual ? 1f : blindThreatEscalation )
: 0f;
cover += needsReload ? 0.52f : 0f;
cover += currentRisk * 0.13f;
cover += _dynamicCoverBias * 0.08f;
cover += bb.Rule.AmmoDiscipline * 0.08f;
cover += matchup.CoverAdjustment;
if ( automaticWeapon && health > 0.55f && !recentThreat && !needsReload )
cover -= 0.06f;
if ( Time.Now < _utilityCoverFailureUntil ) cover -= 0.50f;
AddUtilityCandidate( OMRBotIntentKind.TakeCover, cover, hasEvidence, needsReload ? "SAFE RELOAD" : "SURVIVAL / COVER" );
float disengage = ( hasVisual || recentThreat ) && health < 0.58f
? 0.12f + MathX.Clamp( ( 0.58f - health ) / 0.58f, 0f, 1f ) * 0.48f +
( recentThreat ? MathX.Lerp( 0.04f, 0.18f, hasVisual ? 1f : blindThreatEscalation ) : 0f )
: -1f;
disengage += doctrine.PrecisionBias * 0.10f;
disengage += matchup.OpenDistanceAdjustment * 0.45f;
disengage -= tempo * 0.16f;
AddUtilityCandidate( OMRBotIntentKind.Disengage, disengage, disengage >= 0f, "LOW HEALTH / BREAK CONTACT" );
float reposition = hasVisual
? 0.16f + ( 1f - rangeFit ) * 0.10f + currentRisk * 0.18f + doctrine.CoverCycleBias * 0.10f
: -1f;
if ( _confirmedSightStartedAt > 0f )
reposition += MathX.Clamp( ( Time.Now - _confirmedSightStartedAt - 1.5f ) / 4f, 0f, 1f ) * 0.12f;
reposition += MathF.Abs( _dynamicMovementBias ) * 0.06f;
reposition += matchup.RepositionAdjustment;
if ( automaticWeapon && rangeFit > 0.45f ) reposition -= 0.04f;
if ( Time.Now < _utilityPositionFailureUntil ) reposition -= 0.35f;
AddUtilityCandidate( OMRBotIntentKind.Reposition, reposition, hasVisual, "CHANGE ANGLE / SPACE" );
bool flankLegal =
hasVisual &&
weapon?.IsValid == true &&
!weapon.IsReloading &&
weapon.Clip1 > 0 &&
!recentThreat &&
Time.Now >= _nextFlankDecisionAt &&
Time.Now >= _utilityFlankFailureUntil &&
_confirmedSightStartedAt > 0f &&
Time.Now - _confirmedSightStartedAt >= MathF.Max( 0.65f, FlankMinimumContactTime * 0.72f );
if ( flankLegal )
{
float contact = MathX.Clamp( ( Time.Now - _confirmedSightStartedAt ) / 4f, 0f, 1f );
float flankBase =
0.14f +
aggression * 0.17f +
contact * 0.17f +
( 1f - bb.OpponentModel.Mobility ) * adaptive * 0.15f +
doctrine.ClosePressureBias * 0.07f +
_dynamicFlankBias * 0.11f +
matchup.FlankAdjustment -
( 1f - health ) * 0.12f;
Vector3 toThreat = ( _observedTargetPosition - WorldPosition ).WithZ( 0f );
Vector3 lateral = toThreat.IsNearlyZero( 0.01f )
? new Vector3( -WorldRotation.Forward.y, WorldRotation.Forward.x, 0f ).Normal
: new Vector3( -toThreat.Normal.y, toThreat.Normal.x, 0f ).Normal;
float sideProbeDistance = MathF.Max( 120f, FlankMinimumDistance );
float leftRisk = GetSpatialMemoryRiskAt( WorldPosition + lateral * sideProbeDistance );
float rightRisk = GetSpatialMemoryRiskAt( WorldPosition - lateral * sideProbeDistance );
float leftScore = flankBase - MathX.Clamp( leftRisk / 8f, 0f, 1f ) * 0.16f;
float rightScore = flankBase - MathX.Clamp( rightRisk / 8f, 0f, 1f ) * 0.16f;
if ( _flankSide < 0 ) leftScore += 0.025f; else rightScore += 0.025f;
AddUtilityCandidate( OMRBotIntentKind.FlankLeft, leftScore, true, "ANGLE CHANGE / LEFT" );
AddUtilityCandidate( OMRBotIntentKind.FlankRight, rightScore, true, "ANGLE CHANGE / RIGHT" );
}
bool suppressionLegal =
!hasVisual &&
hasMemory &&
doctrine.SupportsSuppressiveFire &&
weapon?.IsValid == true &&
weapon.Clip1 > 1 &&
!weapon.IsReloading &&
Time.Now - _lastConfirmedSeenAt <= MathF.Max( 0.35f, SuppressionMemoryWindow * 1.15f );
float suppression = suppressionLegal
? 0.24f + aggression * 0.12f + ( 1f - MathX.Clamp( bb.LastSeenAge / MathF.Max( 0.4f, SuppressionMemoryWindow ), 0f, 1f ) ) * 0.18f
: -1f;
AddUtilityCandidate( OMRBotIntentKind.Suppress, suppression, suppressionLegal, "DENY RE-PEEK" );
bool beliefEvidence = hasBelief && bb.OpponentBeliefConfidence >= 0.055f;
bool searchLegal = !hasVisual && hasEvidence;
float search = searchLegal
? 0.34f + ( hasMemory ? 0.13f : 0f ) + bb.LastHeardConfidence * 0.14f + bb.OpponentBeliefConfidence * 0.40f + tempo * 0.08f
: -1f;
if ( recentThreat )
search += !hasVisual && blindThreatHits <= 1 ? 0.15f : 0.06f;
AddUtilityCandidate( OMRBotIntentKind.Search, search, searchLegal, "FOLLOW EVIDENCE" );
bool interceptLegal =
!hasVisual &&
beliefEvidence &&
!_lastSeenDirection.WithZ( 0f ).IsNearlyZero( 0.01f ) &&
bb.OpponentBeliefConfidence >= MathF.Max( 0.06f, BeliefInterceptMinimumConfidence );
float intercept = interceptLegal
? 0.24f + bb.OpponentBeliefConfidence * 0.45f + bb.OpponentModel.Mobility * adaptive * 0.16f + aggression * 0.08f + bb.OpponentModel.RoutePredictionConfidence * 0.15f
: -1f;
AddUtilityCandidate( OMRBotIntentKind.Intercept, intercept, interceptLegal, "BELIEF / CUT ROUTE", bb.OpponentBeliefPosition, interceptLegal );
bool ambushLegal =
!hasVisual &&
beliefEvidence &&
bb.TargetDistance <= MathF.Max( 180f, desiredRange * 1.15f );
float ambush = ambushLegal
? 0.18f + doctrine.PrecisionBias * 0.18f + doctrine.CoverCycleBias * 0.14f + bb.OpponentModel.Aggression * adaptive * 0.14f + bb.OpponentBeliefConfidence * 0.25f + matchup.AmbushAdjustment + bb.OpponentModel.RepeekTendency * 0.09f + bb.OpponentModel.PushTendency * 0.06f
: -1f;
AddUtilityCandidate( OMRBotIntentKind.HoldAmbush, ambush, ambushLegal, "LIKELY APPROACH / HOLD", bb.OpponentBeliefPosition, ambushLegal );
bool reloadLegal = needsReload;
float reload = reloadLegal ? ( hasEvidence ? 0.96f : 0.76f ) + matchup.ReloadSafetyAdjustment : -1f;
AddUtilityCandidate( OMRBotIntentKind.ReloadSafely, reload, reloadLegal, "AMMO CRITICAL" );
if ( needsReload )
{
foreach ( OMRBotDecisionCandidate candidate in _brainDecisionFrame.Candidates )
{
if ( candidate.Intent != OMRBotIntentKind.ReloadSafely || !candidate.Legal )
continue;
SetSelectedUtilityDecision(
candidate.Intent,
1f,
candidate.Reason,
candidate.TargetPosition,
candidate.HasTargetPosition
);
return;
}
}
SelectUtilityCandidate();
}
private void AddUtilityCandidate(
OMRBotIntentKind intent,
float rawScore,
bool legal,
string reason,
Vector3 target = default,
bool hasTarget = false
)
{
if ( legal )
rawScore += GetCombatSequenceUtilityAdjustment( intent );
float score = legal ? MathX.Clamp( rawScore, 0f, 1f ) : 0f;
if ( legal && intent == _utilitySelectedIntent )
score = MathX.Clamp( score + MathF.Max( 0f, UtilityCurrentIntentBonus ), 0f, 1f );
OMRBotDecisionCandidate candidate = _brainDecisionFrame.RentCandidate();
candidate.Intent = intent;
candidate.Score = score;
candidate.Legal = legal;
candidate.Reason = reason ?? string.Empty;
candidate.TargetPosition = target;
candidate.HasTargetPosition = hasTarget;
}
private void SelectUtilityCandidate()
{
OMRBotDecisionCandidate best = null;
OMRBotDecisionCandidate current = null;
foreach ( OMRBotDecisionCandidate candidate in _brainDecisionFrame.Candidates )
{
if ( !candidate.Legal )
continue;
if ( candidate.Intent == _utilitySelectedIntent )
current = candidate;
if ( best is null || candidate.Score > best.Score )
best = candidate;
}
if ( best is null )
{
SetSelectedUtilityDecision( OMRBotIntentKind.None, 0f, "NO LEGAL ACTION", Vector3.Zero, false );
return;
}
if (
current is not null &&
best.Intent != current.Intent &&
Time.Now < _utilityCommitUntil &&
best.Score < current.Score + MathF.Max( 0f, UtilitySwitchMargin )
)
{
best = current;
}
SetSelectedUtilityDecision(
best.Intent,
best.Score,
best.Reason,
best.TargetPosition,
best.HasTargetPosition
);
}
private void SetSelectedUtilityDecision(
OMRBotIntentKind intent,
float score,
string reason,
Vector3 target,
bool hasTarget
)
{
bool changed = intent != _utilitySelectedIntent;
if ( changed )
{
_utilitySelectedAt = Time.Now;
_utilityCommitUntil = Time.Now + MathF.Max( 0.18f, UtilityMinimumCommitTime );
ApplyUtilityIntentTransition( _utilitySelectedIntent, intent );
}
_utilitySelectedIntent = intent;
_utilitySelectedScore = score;
_utilitySelectedReason = reason ?? string.Empty;
if ( changed || hasTarget )
{
_utilitySelectedTarget = target;
_utilityHasSelectedTarget = hasTarget;
_utilityTargetChosenAt = hasTarget ? Time.Now : -999f;
}
_brainDecisionFrame.SelectedIntent = intent;
_brainDecisionFrame.SelectedScore = score;
_brainDecisionFrame.SelectionReason = _utilitySelectedReason;
UtilityBrainStatus =
$"{intent.ToString().ToUpperInvariant()} {score * 100f:0}% · {_utilitySelectedReason}";
}
private void SetForcedUtilityDecision( OMRBotIntentKind intent, float score, string reason )
{
_brainDecisionFrame.Clear();
AddUtilityCandidate( intent, score, true, reason );
SetSelectedUtilityDecision( intent, score, reason, Vector3.Zero, false );
}
private void ApplyUtilityIntentTransition( OMRBotIntentKind from, OMRBotIntentKind to )
{
if ( from == to )
return;
bool wantsCover = to == OMRBotIntentKind.TakeCover || to == OMRBotIntentKind.ReloadSafely;
bool wantsFlank = to == OMRBotIntentKind.FlankLeft || to == OMRBotIntentKind.FlankRight;
bool wantsSearch = to == OMRBotIntentKind.Search || to == OMRBotIntentKind.Intercept || to == OMRBotIntentKind.HoldAmbush;
if ( _hasCoverTarget && !wantsCover )
ClearCoverPlan( $"UTILITY → {to.ToString().ToUpperInvariant()}" );
if ( _flankActive && !wantsFlank )
CancelFlank( $"UTILITY → {to.ToString().ToUpperInvariant()}" );
if ( _searchPoints.Count > 0 && !wantsSearch )
ClearSearchPlan( $"UTILITY → {to.ToString().ToUpperInvariant()}" );
}
private bool ExecuteUtilityCombatDecision( OneMoreRoundWeapon weapon )
{
bool hasVisual = _hasConfirmedSight && _perceptionTarget?.IsAlive == true;
OMRBotMatchupContext matchup = ActiveMatchup;
_utilityRangeScale = matchup.DesiredRangeScale;
switch ( _utilitySelectedIntent )
{
case OMRBotIntentKind.ReloadSafely:
return ExecuteUtilitySafeReload( weapon );
case OMRBotIntentKind.TakeCover:
return ExecuteUtilityCover( weapon, "UTILITY" );
case OMRBotIntentKind.FlankLeft:
return ExecuteUtilityFlank( weapon, -1 );
case OMRBotIntentKind.FlankRight:
return ExecuteUtilityFlank( weapon, 1 );
case OMRBotIntentKind.Suppress:
if ( TryHandleSuppressiveFire( weapon ) )
return true;
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
return TryHandleBeliefDrivenSearch( weapon );
case OMRBotIntentKind.Search:
return TryHandleBeliefDrivenSearch( weapon );
case OMRBotIntentKind.Intercept:
return ExecuteUtilityIntercept( weapon );
case OMRBotIntentKind.HoldAmbush:
return ExecuteUtilityAmbush( weapon );
case OMRBotIntentKind.Disengage:
if ( hasVisual )
return ExecuteUtilityTacticalMove( weapon, OMRBotTacticalQueryPurpose.Retreat, "DISENGAGE" );
return TryHandleBeliefDrivenSearch( weapon );
case OMRBotIntentKind.Reposition:
if ( hasVisual )
return ExecuteUtilityTacticalMove( weapon, OMRBotTacticalQueryPurpose.Reposition, "REPOSITION" );
return TryHandleBeliefDrivenSearch( weapon );
case OMRBotIntentKind.OpenDistance:
_utilityRangeScale = matchup.DesiredRangeScale * 1.28f;
if ( hasVisual && _brainBlackboard.TargetDistance < GetCombatHoldDistance( weapon ) * 0.92f )
{
if ( ExecuteUtilityTacticalMove( weapon, OMRBotTacticalQueryPurpose.OpenDistance, "OPEN DISTANCE" ) )
return true;
}
return false;
case OMRBotIntentKind.Press:
_utilityRangeScale = matchup.DesiredRangeScale * 0.76f;
return false;
case OMRBotIntentKind.Engage:
default:
_utilityRangeScale = matchup.DesiredRangeScale;
return false;
}
}
private bool ExecuteUtilitySafeReload( OneMoreRoundWeapon weapon )
{
if ( weapon?.IsValid != true )
return false;
bool hasEvidence =
_hasConfirmedSight || HasFreshMemory() || HasRecentThreat() || HasFreshHearing() || HasOpponentBelief();
if ( hasEvidence )
{
if ( ExecuteUtilityCover( weapon, "RELOAD" ) )
return true;
}
StopMovementOnly();
ReleaseBotWeaponIntent( weapon );
if ( !weapon.IsReloading )
weapon.BotTryReload();
CombatStatus = weapon.IsReloading ? "UTILITY / SAFE RELOAD" : "UTILITY / RELOAD";
TacticalStatus = "RELOAD / NO COVER NEEDED";
return true;
}
private bool ExecuteUtilityCover( OneMoreRoundWeapon weapon, string reason )
{
if ( _hasCoverTarget )
return UpdateActiveCoverPlan( weapon );
if ( Time.Now < _utilityCoverFailureUntil || Time.Now < _nextCoverDecisionAt )
return false;
if (
!TryResolveThreatReference( out Vector3 threatReference, out Vector3 threatDirection ) ||
!TryChooseCoverPosition( weapon, threatReference, threatDirection, out OMRCoverChoice cover )
)
{
_utilityCoverFailureUntil = Time.Now + 0.75f;
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
CoverStatus = "UTILITY / NO COVER";
return false;
}
_hasCoverTarget = true;
_coverTarget = cover.Anchor;
_coverReason = reason;
_coverHoldUntil = -1f;
_coverPosture = cover.Posture;
_coverLeftPeek = cover.LeftPeek;
_coverRightPeek = cover.RightPeek;
_coverHasLeftPeek = cover.HasLeftPeek;
_coverHasRightPeek = cover.HasRightPeek;
_coverPeekActive = false;
_coverPeekReturning = false;
_coverPeekUntil = -1f;
_nextCoverPeekAt = Time.Now + 0.12f;
_nextCoverDecisionAt = Time.Now + MathF.Max( 0.5f, CoverDecisionCooldown );
ClearSearchPlan( "UTILITY COVER" );
CancelFlank( "UTILITY COVER" );
TacticalStatus = $"UTILITY COVER / {reason}";
CoverStatus = $"SEEKING {reason}";
return UpdateActiveCoverPlan( weapon );
}
private bool ExecuteUtilityFlank( OneMoreRoundWeapon weapon, int side )
{
if ( _flankActive )
return TryHandleFlankPlan( weapon );
if ( Time.Now < _nextFlankDecisionAt )
{
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
return false;
}
if ( Time.Now < _utilityFlankFailureUntil )
return false;
if ( !TryChooseFlankPositionForSide( weapon, side, out Vector3 flank ) )
{
// One tactical side can legitimately be sealed by geometry. Try the other
// before abandoning the intent; position quality still comes from BOT 7C.
if ( !TryChooseFlankPositionForSide( weapon, -side, out flank ) )
{
_utilityFlankFailureUntil = Time.Now + 1.15f;
_nextFlankDecisionAt = Time.Now + 1.35f;
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
FlankStatus = "UTILITY / NO ROUTE";
return false;
}
side = -side;
}
_flankActive = true;
_flankTarget = flank;
_flankSide = -side;
_nextFlankDecisionAt = Time.Now + MathF.Max( 1.6f, FlankDecisionCooldown );
FlankStatus = side < 0 ? "UTILITY / LEFT" : "UTILITY / RIGHT";
ClearSearchPlan( "UTILITY FLANK" );
return TryHandleFlankPlan( weapon );
}
private bool TryChooseFlankPositionForSide( OneMoreRoundWeapon weapon, int side, out Vector3 best )
{
best = Vector3.Zero;
if ( Scene?.NavMesh is null || _perceptionTarget?.IsAlive != true )
return false;
Vector3 toTarget = ( _observedTargetPosition - WorldPosition ).WithZ( 0f );
if ( toTarget.IsNearlyZero( 0.01f ) )
return false;
Vector3 forward = toTarget.Normal;
Vector3 lateral = new Vector3( -forward.y, forward.x, 0f ).Normal;
float hold = GetCombatHoldDistance( weapon );
float minDistance = MathF.Max( 140f, FlankMinimumDistance );
float maxDistance = MathF.Max( minDistance + 60f, FlankMaximumDistance );
float[] distances = { minDistance, ( minDistance + maxDistance ) * 0.5f, maxDistance };
List<Vector3> candidates = new();
foreach ( float distance in distances )
{
candidates.Add(
WorldPosition +
lateral * side * distance +
forward * MathF.Min( 90f, distance * 0.22f )
);
}
AddAuthoredTacticalCandidates(
candidates,
OMRBotTacticalQueryPurpose.Flank,
WorldPosition,
maxDistance * 1.55f
);
OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
Vector3 threatReference = _observedTargetAimPoint.IsNearlyZero( 0.01f )
? _observedTargetPosition + Vector3.Up * 46f
: _observedTargetAimPoint;
float bestScore = float.MinValue;
OMRBotTacticalPositionEvaluation bestEvaluation = default;
foreach ( Vector3 raw in candidates )
{
Vector3 candidateDirection = ( raw - WorldPosition ).WithZ( 0f );
if ( !candidateDirection.IsNearlyZero( 0.01f ) )
{
float sideDot = candidateDirection.Normal.Dot( lateral ) * side;
if ( sideDot < -0.20f )
continue;
}
if (
!TryEvaluateTacticalPosition(
raw,
OMRBotTacticalQueryPurpose.Flank,
doctrine,
threatReference,
true,
_observedTargetPosition,
hold,
out OMRBotTacticalPositionEvaluation evaluation
)
)
continue;
float score = evaluation.Score;
if ( score <= bestScore )
continue;
bestScore = score;
best = evaluation.Position;
bestEvaluation = evaluation;
}
if ( bestScore == float.MinValue )
return false;
SetTacticalQueryDiagnostic( OMRBotTacticalQueryPurpose.Flank, bestEvaluation );
return true;
}
private bool ExecuteUtilityTacticalMove(
OneMoreRoundWeapon weapon,
OMRBotTacticalQueryPurpose purpose,
string label
)
{
if ( !_hasConfirmedSight || _perceptionTarget?.IsAlive != true )
return false;
if ( Time.Now < _utilityPositionFailureUntil )
return false;
OMRBotTacticalPositionEvaluation evaluation = default;
if (
_utilityHasSelectedTarget &&
( _utilitySelectedTarget - WorldPosition ).WithZ( 0f ).Length <= 42f
)
{
_utilityHasSelectedTarget = false;
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
return false;
}
bool needTarget =
!_utilityHasSelectedTarget ||
Time.Now - _utilityTargetChosenAt > 1.25f;
if ( needTarget )
{
if ( !TryChooseUtilityTacticalPosition( weapon, purpose, out evaluation ) )
{
_utilityPositionFailureUntil = Time.Now + 0.65f;
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
_utilityHasSelectedTarget = false;
return false;
}
_utilitySelectedTarget = evaluation.Position;
_utilityHasSelectedTarget = true;
_utilityTargetChosenAt = Time.Now;
SetTacticalQueryDiagnostic( purpose, evaluation );
}
float enemyDistance = ( _observedTargetPosition - WorldPosition ).WithZ( 0f ).Length;
float travelDistance = ( _utilitySelectedTarget - WorldPosition ).WithZ( 0f ).Length;
UpdateCombatAim( _observedTargetAimPoint );
bool wantsAds = ShouldUseAds( weapon, enemyDistance );
CombatWantsAim = wantsAds;
weapon?.SetBotAimIntent( wantsAds );
NavigateToWorldPosition(
_utilitySelectedTarget,
$"UTILITY {label}",
48f,
$"UTILITY / {label}",
allowSprint: false,
faceMovement: false,
advancedMovement: true,
movementWeapon: weapon,
visibleCombatTarget: true
);
UpdateVisibleWeaponAttack( weapon, enemyDistance, wantsAds );
TacticalStatus = $"UTILITY / {label}";
return true;
}
private bool TryChooseUtilityTacticalPosition(
OneMoreRoundWeapon weapon,
OMRBotTacticalQueryPurpose purpose,
out OMRBotTacticalPositionEvaluation best
)
{
best = default;
if ( Scene?.NavMesh is null || !_hasConfirmedSight )
return false;
OMRBotWeaponDoctrine doctrine = GetWeaponDoctrine( weapon );
Vector3 threatReference = _observedTargetAimPoint.IsNearlyZero( 0.01f )
? _observedTargetPosition + Vector3.Up * 46f
: _observedTargetAimPoint;
float desiredRange = MathF.Max( 90f, doctrine.LegacyHoldDistance );
float baseDistance = MathF.Max( 110f, UtilityRepositionDistance );
List<Vector3> candidates = new();
Vector3 fromThreat = ( WorldPosition - _observedTargetPosition ).WithZ( 0f );
if ( fromThreat.IsNearlyZero( 0.01f ) )
fromThreat = -WorldRotation.Forward.WithZ( 0f );
fromThreat = fromThreat.Normal;
Vector3 lateral = new Vector3( -fromThreat.y, fromThreat.x, 0f ).Normal;
Vector3[] directions =
{
fromThreat,
-fromThreat,
lateral,
-lateral,
( fromThreat + lateral ).Normal,
( fromThreat - lateral ).Normal,
( -fromThreat + lateral ).Normal,
( -fromThreat - lateral ).Normal
};
float[] radii = { baseDistance * 0.72f, baseDistance, baseDistance * 1.35f };
foreach ( Vector3 direction in directions )
{
foreach ( float radius in radii )
candidates.Add( WorldPosition + direction * radius );
}
AddAuthoredTacticalCandidates(
candidates,
purpose,
WorldPosition,
baseDistance * 2.25f
);
float bestScore = float.MinValue;
foreach ( Vector3 candidate in candidates )
{
if (
!TryEvaluateTacticalPosition(
candidate,
purpose,
doctrine,
threatReference,
true,
_observedTargetPosition,
desiredRange,
out OMRBotTacticalPositionEvaluation evaluation
)
)
continue;
if ( evaluation.Score <= bestScore )
continue;
bestScore = evaluation.Score;
best = evaluation;
}
return bestScore != float.MinValue;
}
private bool ExecuteUtilityIntercept( OneMoreRoundWeapon weapon )
{
if ( _hasConfirmedSight )
return false;
Vector3 target = _utilityHasSelectedTarget ? _utilitySelectedTarget : Vector3.Zero;
if ( !TryGetBeliefInterceptTarget( out Vector3 liveIntercept, out float confidence ) )
{
if ( target.IsNearlyZero( 0.01f ) )
return TryHandleBeliefDrivenSearch( weapon );
}
else
{
target = liveIntercept;
}
float distance = ( target - WorldPosition ).WithZ( 0f ).Length;
if ( distance <= 58f )
{
MarkOpponentBeliefAreaCleared( target );
_nextUtilityDecisionAt = MathF.Min( _nextUtilityDecisionAt, Time.Now + 0.12f );
return TryHandleBeliefDrivenSearch( weapon );
}
ReleaseBotWeaponIntent( weapon );
NavigateToWorldPosition(
target,
"BELIEF INTERCEPT",
58f,
"UTILITY / INTERCEPT",
allowSprint: distance > 250f,
faceMovement: true,
advancedMovement: true,
movementWeapon: weapon,
visibleCombatTarget: false
);
TacticalStatus = $"INTERCEPT / BELIEF {confidence * 100f:0}%";
SearchStatus = "INTERCEPTING LIKELY ROUTE";
return true;
}
private bool ExecuteUtilityAmbush( OneMoreRoundWeapon weapon )
{
if ( _hasConfirmedSight )
return false;
StopMovementOnly();
ReleaseBotWeaponIntent( weapon );
if ( TryGetBestOpponentBelief( out Vector3 beliefPosition, out float confidence ) )
{
LookTowardWorldPoint( beliefPosition + Vector3.Up * 46f, 6.5f );
SearchStatus = $"HOLD / BELIEF {confidence * 100f:0}%";
TacticalStatus = "AMBUSH / HOLD LIKELY APPROACH";
}
else
{
SweepLook( IdleSweepSpeed * 0.55f );
SearchStatus = "HOLD / SCAN";
}
return true;
}
private void LookTowardWorldPoint( Vector3 point, float followSpeed )
{
if ( _controller?.IsValid != true )
return;
Vector3 direction = ( point - _controller.EyePosition ).Normal;
if ( direction.IsNearlyZero( 0.001f ) )
return;
Rotation desired = Rotation.LookAt( direction, Vector3.Up );
float follow = 1f - MathF.Exp( -MathF.Max( 0.5f, followSpeed ) * MathF.Max( Time.Delta, 0f ) );
_controller.EyeAngles = Rotation.Slerp(
_controller.EyeAngles.ToRotation(),
desired,
MathX.Clamp( follow, 0f, 1f )
).Angles();
}
private bool UtilityNeedsReload( OneMoreRoundWeapon weapon )
{
if ( weapon?.IsValid != true || !weapon.UsesPrimaryClip )
return false;
if ( weapon.IsReloading || weapon.Clip1 <= 0 )
return true;
if (
_brainRuleContext.AmmoDiscipline >= 0.75f &&
weapon.PrimaryClipSize > 0 &&
weapon.Ammo1 > 0
)
{
return weapon.Clip1 <= Math.Max( 1, (int)MathF.Floor( weapon.PrimaryClipSize * 0.24f ) );
}
return false;
}
private bool IsUtilityIntentStillLegal( OMRBotIntentKind intent, OneMoreRoundWeapon weapon )
{
bool hasVisual = _hasConfirmedSight && _perceptionTarget?.IsAlive == true;
return intent switch
{
OMRBotIntentKind.None => false,
OMRBotIntentKind.Engage => hasVisual,
OMRBotIntentKind.Press => hasVisual,
OMRBotIntentKind.OpenDistance => hasVisual,
OMRBotIntentKind.Reposition => hasVisual,
OMRBotIntentKind.FlankLeft => hasVisual && weapon?.Clip1 > 0,
OMRBotIntentKind.FlankRight => hasVisual && weapon?.Clip1 > 0,
OMRBotIntentKind.Suppress => !hasVisual && HasFreshMemory(),
OMRBotIntentKind.ReloadSafely => UtilityNeedsReload( weapon ),
OMRBotIntentKind.TakeCover => HasRecentThreat() || hasVisual || HasFreshMemory() || HasFreshHearing(),
OMRBotIntentKind.Search => !hasVisual,
OMRBotIntentKind.Intercept => !hasVisual && HasOpponentBelief(),
OMRBotIntentKind.HoldAmbush => !hasVisual && HasOpponentBelief(),
OMRBotIntentKind.Disengage => hasVisual || HasRecentThreat(),
_ => true
};
}
private float GetUtilityRangeScale()
{
return UtilityCombatBrainEnabled
? MathX.Clamp( _utilityRangeScale, 0.60f, 1.55f )
: 1f;
}
private void ResetUtilityBrainState()
{
_utilitySelectedIntent = OMRBotIntentKind.None;
_utilitySelectedScore = 0f;
_utilitySelectedReason = string.Empty;
_utilitySelectedTarget = Vector3.Zero;
_utilityHasSelectedTarget = false;
_utilityTargetChosenAt = -999f;
_utilitySelectedAt = -999f;
_utilityCommitUntil = -999f;
_nextUtilityDecisionAt = 0f;
_utilityRangeScale = 1f;
_utilityCoverFailureUntil = -999f;
_utilityFlankFailureUntil = -999f;
_utilityPositionFailureUntil = -999f;
UtilityBrainStatus = UtilityCombatBrainEnabled ? "READY" : "LEGACY";
_brainDecisionFrame.Clear();
}
}