From 0fa4d8c9cdd8490c0afd02f9bdf180230b32c638 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Mon, 22 Jun 2026 00:45:49 -0600 Subject: [PATCH] Bots can attack toward an optional coordinate --- .../neo/bot/behavior/neo_bot_attack.cpp | 406 ++++++++++++------ .../server/neo/bot/behavior/neo_bot_attack.h | 7 +- .../bot/behavior/neo_bot_command_follow.cpp | 13 + .../neo/bot/behavior/neo_bot_ctg_enemy.cpp | 15 +- .../neo/bot/behavior/neo_bot_ctg_enemy.h | 3 +- .../neo/bot/behavior/neo_bot_ctg_escort.cpp | 27 +- .../neo/bot/behavior/neo_bot_jgr_capture.cpp | 29 +- .../neo/bot/behavior/neo_bot_jgr_enemy.cpp | 4 +- .../neo/bot/behavior/neo_bot_jgr_escort.cpp | 19 +- .../neo/bot/behavior/neo_bot_jgr_seek.cpp | 9 +- .../behavior/neo_bot_retreat_from_grenade.cpp | 6 +- .../bot/behavior/neo_bot_retreat_to_cover.cpp | 2 +- .../bot/behavior/neo_bot_seek_and_destroy.cpp | 52 ++- src/game/server/neo/neo_player.cpp | 20 +- 14 files changed, 397 insertions(+), 215 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp index d091155bd8..cc13574b3e 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -2,23 +2,43 @@ #include "neo_player.h" #include "neo_gamerules.h" #include "neo_smokelineofsightblocker.h" -#include "team_control_point_master.h" #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_grenade_dispatch.h" +#include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/neo_bot_path_compute.h" #include "nav_mesh.h" +#include "debugoverlay_shared.h" -extern ConVar neo_bot_path_lookahead_range; -extern ConVar neo_bot_offense_must_push_time; +ConVar sv_neo_bot_attack_debug_cover("sv_neo_bot_attack_debug_cover", "0", FCVAR_CHEAT, + "Draw debug overlays for bot attack/cover behavior", true, 0, true, 1); -ConVar neo_bot_aggressive( "neo_bot_aggressive", "0", FCVAR_NONE ); //--------------------------------------------------------------------------------------------- +// Enter the Attack behavior to chase and destroy primary known threat. CNEOBotAttack::CNEOBotAttack( void ) : m_chasePath( ChasePath::LEAD_SUBJECT ) { + m_bSawEnemySinceLastPathCompute = true; // force first path calculation m_attackCoverArea = nullptr; + m_goalArea = nullptr; +} + + +//--------------------------------------------------------------------------------------------- +// Enter the Attack behavior with a goal position to move towards while engaging enemies. +// Bot will disengage from threat when advancement towards the goal is uncontested. +// +// When using GetPrimaryKnownThreat(true), the true parameter must be used +// such that the threat must be visible to the bot before entering attack behavior, +// because the m_goalArea terminates this behavior when the enemy is out of sight. +// When GetPrimaryKnownThreat(false) is used, this behavior may redundantly ping pong with prior state, +// such as in cases where the enemy is not visible, and is known behind a wall, while being unreachable. +CNEOBotAttack::CNEOBotAttack( const Vector &goalPosition ) : m_chasePath( ChasePath::LEAD_SUBJECT ) +{ + m_bSawEnemySinceLastPathCompute = true; // force first path calculation + m_attackCoverArea = nullptr; + m_goalArea = TheNavMesh->GetNearestNavArea( goalPosition ); } @@ -26,46 +46,83 @@ CNEOBotAttack::CNEOBotAttack( void ) : m_chasePath( ChasePath::LEAD_SUBJECT ) ActionResult< CNEOBot > CNEOBotAttack::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) { m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); - m_attackCoverTimer.Invalidate(); - return Continue(); } //--------------------------------------------------------------------------------------------- -// for finding cover closer to our threat +// for finding cover closer to our attack destination class CSearchForAttackCover : public ISearchSurroundingAreasFunctor { public: - CSearchForAttackCover( CNEOBot *me, const CKnownEntity *threat ) : m_me( me ), m_threat( threat ) + CSearchForAttackCover( CNEOBot *me, const CKnownEntity *threat, const CNavArea *goalArea = nullptr ) : m_me( me ), m_threat( threat ) { m_attackCoverArea = nullptr; + m_myArea = m_me->GetLastKnownArea(); m_threatArea = threat->GetLastKnownArea(); - m_distToThreatSq = ( threat->GetLastKnownPosition() - me->GetAbsOrigin() ).LengthSqr(); + m_goalArea = goalArea ? goalArea : m_threatArea; // prioritize movement towards input goal area or threat + m_myDistToGoalSq = m_goalArea ? ( m_goalArea->GetCenter() - m_me->GetAbsOrigin() ).LengthSqr() : 0; } virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) { - // return true to keep searching, false when suitable cover is found + // return true to keep searching, false ends search usually implying that suitable cover was found + + if ( !m_goalArea || !m_threatArea || !m_myArea ) + { + return false; // don't have enough context to search + } + CNavArea *area = static_cast(baseArea); + if ( area == m_myArea ) + { + return true; // skip our starting area + } - if ( !m_threatArea ) + if ( neo_bot_path_reservation_enable.GetBool() && + ( CNEOBotPathReservations()->GetAreaAvoidPenalty(area->GetID()) > 0 ) ) { - return false; // threat area is unknown + return true; // skip areas that have had navigation hiccups } - constexpr float distanceThresholdRatio = 1.2f; - float candidateDistFromMeSq = ( m_me->GetAbsOrigin() - area->GetCenter() ).LengthSqr(); - if ( candidateDistFromMeSq > m_distToThreatSq * distanceThresholdRatio ) + if ( m_attackCoverArea ) { - return true; // not close enough to justify rerouting + // If we already have a previous candidate cover area, + // only consider this new candidate area if it's an improvement + // as we assume earlier breadth first search nodes are closer to bot + // and thus faster to reach for safety. + if ( neo_bot_path_reservation_enable.GetBool() ) + { + int candidateReservations = CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), m_me->GetTeamNumber()); + int previousReservations = CNEOBotPathReservations()->GetPredictedFriendlyPathCount(m_attackCoverArea->GetID(), m_me->GetTeamNumber()); + if (candidateReservations >= previousReservations) + { + return true; // skip areas that have been reserved relatively more or equal by friendly bots + } + } + // Fallback in case the path reservation system is disabled + else if (area->GetPotentiallyVisibleAreaCount() >= m_attackCoverArea->GetPotentiallyVisibleAreaCount()) + { + // Use potentially visible area count as a rough proxy for how exposed the area is + // The downsides of this approach are: + // * It doesn't consider whether the nav area is actually reachable + // * It doesn't do anything to discourage friendlies from bunching up in the same area + return true; // skip areas that are relatively more exposed + } } - if ( area->IsPotentiallyVisible( m_threatArea ) ) + float goalAreaDistanceSq = ( m_goalArea->GetCenter() - area->GetCenter() ).LengthSqr(); + if (goalAreaDistanceSq >= m_myDistToGoalSq) + { + // skip as this candidate area doesn't get us closer to destination + return true; + } + + if ( m_threatArea->IsPotentiallyVisible( area ) ) { - // Even if area does not have hard cover, see if Support can use smoke concealment if ( m_me->GetClass() == NEO_CLASS_SUPPORT ) { + // Even if area does not have hard cover, see if Support can use smoke concealment CNEO_Player *pThreatPlayer = ToNEOPlayer( m_threat->GetEntity() ); if ( pThreatPlayer && ( pThreatPlayer->GetClass() != NEO_CLASS_SUPPORT ) ) { @@ -74,19 +131,25 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor Vector vecThreatEye = m_threat->GetLastKnownPosition() + pThreatPlayer->GetViewOffset(); Vector vecCandidateArea = area->GetCenter() + m_me->GetViewOffset(); - trace_t tr; CTraceFilterSimple filter( m_threat->GetEntity(), COLLISION_GROUP_NONE ); - UTIL_TraceLine( vecThreatEye, vecCandidateArea, MASK_BLOCKLOS, &filter, &tr ); + trace_t trSmoke; + UTIL_TraceLine( vecThreatEye, vecCandidateArea, MASK_BLOCKLOS, &filter, &trSmoke ); + trace_t trNormal; + UTIL_TraceLine( vecThreatEye, vecCandidateArea, MASK_SOLID, &filter, &trNormal ); - if ( tr.fraction < 1.0f ) + if ( trSmoke.fraction < trNormal.fraction ) { m_attackCoverArea = area; return false; // found smoke as concealment } } } + else if (!m_threatArea->IsCompletelyVisible(area)) + { + m_attackCoverArea = area; + } - return true; // not cover + return true; // search for potentially better cover } // found hard cover @@ -101,21 +164,28 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor return false; } - // For considering areas off to the side of current area - constexpr float distanceThresholdRatio = 0.9f; + if (!m_goalArea || !adjArea) + { + return false; + } + + // The adjacent area to search should not be farther from the attack goal + float adjGoalDistance = ( m_goalArea->GetCenter() - adjArea->GetCenter() ).LengthSqr(); + if ( adjGoalDistance > m_myDistToGoalSq ) + { + return false; // Candidate adjacent area veers farther from goal + } - // The adjacent area to search should not be farther from the threat - float adjThreatDistance = ( m_threatArea->GetCenter() - adjArea->GetCenter() ).LengthSqr(); - float curThreatDistance = ( m_threatArea->GetCenter() - currentArea->GetCenter() ).LengthSqr(); - if ( adjThreatDistance * distanceThresholdRatio > curThreatDistance ) + // The adjacent area to search should not be beyond the attack goal + float adjMeDistance = ( m_me->GetAbsOrigin() - adjArea->GetCenter() ).LengthSqr(); + if ( adjMeDistance > m_myDistToGoalSq ) { - return false; // Candidate adjacent area veers farther from threat + return false; // Candidate adjacent area is beyond goal distance } - // The adjacent area to search should not be beyond the threat - if ( adjThreatDistance > m_distToThreatSq ) + if (!currentArea) { - return false; // Candidate adjacent area is beyond threat distance + return false; } // Don't consider areas that require jumping when engaging enemy @@ -124,9 +194,11 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor CNEOBot *m_me; const CKnownEntity *m_threat; - CNavArea *m_attackCoverArea; + const CNavArea *m_attackCoverArea; + const CNavArea *m_goalArea; // reference point of the optional goal direction + const CNavArea *m_myArea; // reference point of myself const CNavArea *m_threatArea; // reference point of the threat - float m_distToThreatSq; // the bot's current distance to the threat + float m_myDistToGoalSq; // the bot's current distance to the threat }; @@ -136,14 +208,41 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) { const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); - if ( threat == NULL || threat->IsObsolete() || !me->GetIntentionInterface()->ShouldAttack( me, threat ) ) + if ( threat == nullptr || threat->IsObsolete() || !me->GetIntentionInterface()->ShouldAttack( me, threat ) ) { return Done( "No threat" ); } + const Vector& threatLastKnownPos = threat->GetLastKnownPosition(); + + if ( sv_neo_bot_attack_debug_cover.GetBool() ) + { + // red - last known position of the threat + NDebugOverlay::HorzArrow( me->GetAbsOrigin(), threatLastKnownPos, 2.0f, 255, 0, 0, 255, true, 0.1f ); + NDebugOverlay::Box( threatLastKnownPos, Vector( -16, -16, 0 ), Vector( 16, 16, 2 ), 255, 0, 0, 30, 0.1f ); + + if ( m_attackCoverArea ) // yellow - cover area that bot has selected to attempt moving towards + { + const Vector& coverPos = m_attackCoverArea->GetCenter(); + NDebugOverlay::HorzArrow( me->GetAbsOrigin(), coverPos, 2.0f, 255, 255, 0, 255, true, 0.1f ); + m_attackCoverArea->DrawFilled( 255, 255, 0, 30, 0.1f, true ); + } + + if (m_goalArea) // green - the goal direction the bot is trying to advance towards + { + const Vector& goalPos = m_goalArea->GetCenter(); + NDebugOverlay::HorzArrow( me->GetAbsOrigin(), goalPos, 2.0f, 0, 255, 0, 255, true, 0.1f ); + m_goalArea->DrawFilled( 0, 255, 0, 30, 0.1f, true ); + } + } + + CNEO_Player *pThreatPlayer = ToNEOPlayer( threat->GetEntity() ); + CNEOBaseCombatWeapon* myWeapon = static_cast( me->GetActiveWeapon() ); bool isUsingCloseRangeWeapon = me->IsCloseRange( myWeapon ); - if ( isUsingCloseRangeWeapon && threat->IsVisibleRecently() && me->IsRangeLessThan( threat->GetLastKnownPosition(), 1.1f * me->GetDesiredAttackRange() ) ) + + if (!m_attackCoverArea // don't slow movement to cover with strafing + && isUsingCloseRangeWeapon && threat->IsVisibleRecently() && me->IsRangeLessThan( threatLastKnownPos, 1.1f * me->GetDesiredAttackRange() ) ) { // circle around our victim if ( me->TransientlyConsistentRandomValue( 3.0f ) < 0.5f ) @@ -156,30 +255,55 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) } } - bool bHasRangedWeapon = me->IsRanged( myWeapon ); + if ( !me->IsRanged(myWeapon) || ( pThreatPlayer && pThreatPlayer->IsCarryingGhost() ) ) + { + m_path.Invalidate(); + CNEOBotPathUpdateChase( me, m_chasePath, threat->GetEntity(), FASTEST_ROUTE ); + return Continue(); + } + + if ( me->GetVisionInterface()->IsAbleToSee( threat->GetEntity(), CNEOBotVision::DISREGARD_FOV ) ) + { + m_bSawEnemySinceLastPathCompute = true; - // Go after them! - bool bAggressive = neo_bot_aggressive.GetBool() && - !bHasRangedWeapon && - me->GetDifficulty() > CNEOBot::EASY; + me->EnableCloak(3.0f); - // pursue the threat. if not visible, go to the last known position - if ( bAggressive || - !threat->IsVisibleRecently() || - me->IsRangeGreaterThan( threat->GetEntity()->GetAbsOrigin(), me->GetDesiredAttackRange() ) || - !me->IsLineOfFireClear( threat->GetEntity()->EyePosition(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + if ( me->GetLastKnownArea() == m_goalArea ) + { + return ChangeTo( new CNEOBotRetreatToCover(), "Taking cover as enemy is still present at my goal" ); + } + else if ( myWeapon && (myWeapon->GetNeoWepBits() & NEO_WEP_SCOPEDWEAPON) && + me->IsRangeLessThan( threatLastKnownPos, me->GetDesiredAttackRange() ) ) + { + return SuspendFor( new CNEOBotRetreatToCover(), "Retreating to cover to prepare next sniper shot" ); + } + } + else { // SUPA7 reload can be interrupted so proactively reload - if (myWeapon && (myWeapon->GetNeoWepBits() & NEO_WEP_SUPA7) && (myWeapon->Clip1() < myWeapon->GetMaxClip1())) + if (!m_bSawEnemySinceLastPathCompute // don't interrupt shooting with reloads if seen enemy on this path + && myWeapon && (myWeapon->GetNeoWepBits() & NEO_WEP_SUPA7) && (myWeapon->Clip1() < myWeapon->GetMaxClip1())) { me->ReloadIfLowClip(true); } + + if ( me->GetLastKnownArea() == threat->GetLastKnownArea() + || me->IsRangeLessThan( threatLastKnownPos, 64.0f ) ) + { + me->GetVisionInterface()->ForgetEntity( threat->GetEntity() ); + return Done( "I lost my target!" ); + } + else if ( m_goalArea && !m_bSawEnemySinceLastPathCompute && + ( me->GetLastKnownArea() == m_attackCoverArea + || me->GetLastKnownArea() == m_goalArea ) ) + { + return Done( "Disengaging from threat to pursue goal." ); + } if ( threat->IsVisibleRecently() ) { - // pre-cloak needs more thermoptic budget when chasing threats - me->EnableCloak(6.0f); - + me->EnableCloak(3.0f); + // Consider throwing a grenade if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) { @@ -191,97 +315,85 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) } } - // Look for opportunities to leap frog from cover to cover - if ( m_attackCoverTimer.IsElapsed() ) + // look where we last saw threat as we approach + if ( me->IsRangeLessThan( threatLastKnownPos, me->GetMaxAttackRange() ) ) { - m_attackCoverTimer.Start( 5.0f ); - - CSearchForAttackCover search( me, threat ); - SearchSurroundingAreas( me->GetLastKnownArea(), search ); - - if ( search.m_attackCoverArea ) - { - m_attackCoverArea = search.m_attackCoverArea; - m_path.Invalidate(); // recompute path - m_chasePath.Invalidate(); - } - } - - if ( m_attackCoverArea ) - { - if ( me->GetLastKnownArea() == m_attackCoverArea ) - { - // Immediately look for new cover - m_attackCoverArea = nullptr; - m_attackCoverTimer.Invalidate(); - } - else - { - if ( !m_path.IsValid() ) - { - if ( !CNEOBotPathCompute( me, m_path, m_attackCoverArea->GetCenter(), DEFAULT_ROUTE ) ) - { - // If no valid path, fallback to chasing threat - m_attackCoverArea = nullptr; - m_path.Invalidate(); - } - } - - if ( m_attackCoverArea ) - { - m_path.Update( me ); - return Continue(); - } - } - } - else - { - m_path.Invalidate(); - } - - // Fallback when there is no advancing cover - if ( isUsingCloseRangeWeapon ) - { - CNEOBotPathUpdateChase( me, m_chasePath, threat->GetEntity(), FASTEST_ROUTE ); - } - else - { - CNEOBotPathUpdateChase( me, m_chasePath, threat->GetEntity(), DEFAULT_ROUTE ); + me->GetBodyInterface()->AimHeadTowards( threatLastKnownPos + Vector( 0, 0, HumanEyeHeight ), IBody::IMPORTANT, 0.1f, nullptr, "Looking towards where we lost sight of our victim" ); } } else { - // if we're at the threat's last known position and he's still not visible, we lost him - m_chasePath.Invalidate(); + // pre-cloak needs more thermoptic budget when chasing threats + me->EnableCloak(6.0f); + } + } - if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), 20.0f ) ) - { - me->GetVisionInterface()->ForgetEntity( threat->GetEntity() ); - return Done( "I lost my target!" ); - } + // Consider if there is cover between me and goal to leapfrog to + if ( m_bSawEnemySinceLastPathCompute && + (!m_attackCoverArea || (me->GetLastKnownArea() == m_attackCoverArea)) ) + { + m_bSawEnemySinceLastPathCompute = false; + CSearchForAttackCover search( me, threat, m_goalArea ); + SearchSurroundingAreas( me->GetLastKnownArea(), search ); - // look where we last saw him as we approach - if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), me->GetMaxAttackRange() ) ) - { - me->GetBodyInterface()->AimHeadTowards( threat->GetLastKnownPosition() + Vector( 0, 0, HumanEyeHeight ), IBody::IMPORTANT, 0.2f, NULL, "Looking towards where we lost sight of our victim" ); - } + if ( search.m_attackCoverArea ) + { + m_attackCoverArea = search.m_attackCoverArea; + m_chasePath.Invalidate(); + } + else if (m_goalArea) + { + // Even if we bounce back to Attack, goal position may get refreshed in prior behavior + return Done( "Reconsidering goal: Failed to find cover towards goal." ); + } + else + { + m_attackCoverArea = nullptr; + } + m_path.Invalidate(); + } - m_path.Update( me ); + if ( m_attackCoverArea && !m_path.IsValid() ) + { + if ( CNEOBotPathCompute( me, m_path, m_attackCoverArea->GetCenter(), DEFAULT_ROUTE ) ) + { + // Disable chase follower to move to cover area + m_chasePath.Invalidate(); + } + else + { + // If no valid path, fallback to chasing threat + m_attackCoverArea = nullptr; + m_path.Invalidate(); + } + } - if ( m_repathTimer.IsElapsed() ) - { - //m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); - m_repathTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + if (m_path.IsValid()) + { + m_path.Update(me); - if ( isUsingCloseRangeWeapon ) - { - CNEOBotPathCompute( me, m_path, threat->GetLastKnownPosition(), FASTEST_ROUTE ); - } - else - { - CNEOBotPathCompute( me, m_path, threat->GetLastKnownPosition(), DEFAULT_ROUTE ); - } - } + if (m_path.GetEndPosition().DistTo(me->GetAbsOrigin()) <= 64.0f) + { + m_path.Invalidate(); + m_attackCoverArea = nullptr; + } + } + else if (m_goalArea) + { + return Done( "Reconsidering goal: No path to cover found." ); + } + else + { + // Directly chase threat if no cover was found or haven't seen enemy since last path compute + m_attackCoverArea = nullptr; + m_bSawEnemySinceLastPathCompute = false; // delay next cover search + if ( isUsingCloseRangeWeapon ) + { + CNEOBotPathUpdateChase( me, m_chasePath, threat->GetEntity(), FASTEST_ROUTE ); + } + else + { + CNEOBotPathUpdateChase( me, m_chasePath, threat->GetEntity(), DEFAULT_ROUTE ); } } @@ -292,6 +404,9 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) //--------------------------------------------------------------------------------------------- EventDesiredResult< CNEOBot > CNEOBotAttack::OnStuck( CNEOBot *me ) { + m_path.Invalidate(); + m_attackCoverArea = nullptr; + CNEOBotPathReservations()->IncrementAreaAvoidPenalty(me->GetLastKnownArea()->GetID(), neo_bot_path_reservation_onstuck_penalty.GetFloat()); return TryContinue(); } @@ -306,6 +421,9 @@ EventDesiredResult< CNEOBot > CNEOBotAttack::OnMoveToSuccess( CNEOBot *me, const //--------------------------------------------------------------------------------------------- EventDesiredResult< CNEOBot > CNEOBotAttack::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) { + m_path.Invalidate(); + m_attackCoverArea = nullptr; + CNEOBotPathReservations()->IncrementAreaAvoidPenalty(me->GetLastKnownArea()->GetID(), neo_bot_path_reservation_onstuck_penalty.GetFloat()); return TryContinue(); } @@ -313,6 +431,32 @@ EventDesiredResult< CNEOBot > CNEOBotAttack::OnMoveToFailure( CNEOBot *me, const //--------------------------------------------------------------------------------------------- QueryResultType CNEOBotAttack::ShouldRetreat( const INextBot *me ) const { + + const CNEOBot *meNeoBot = static_cast(me); + CNEOBaseCombatWeapon* myWeapon = static_cast( meNeoBot->GetActiveWeapon() ); + if ( !meNeoBot->IsRanged(myWeapon) ) + { + return ANSWER_NO; + } + + if (myWeapon) + { + if (myWeapon->m_bInReload || (myWeapon->Clip1() <= 1)) + { + return ANSWER_YES; + } + } + + if (!m_bSawEnemySinceLastPathCompute) + { + return ANSWER_UNDEFINED; + } + + if ( m_goalArea && m_attackCoverArea ) + { + return ANSWER_NO; + } + return ANSWER_UNDEFINED; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.h b/src/game/server/neo/bot/behavior/neo_bot_attack.h index bb57642e45..52ec61eeb7 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.h +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.h @@ -11,6 +11,7 @@ class CNEOBotAttack : public Action< CNEOBot > { public: CNEOBotAttack( void ); + CNEOBotAttack( const Vector &goalPosition ); virtual ~CNEOBotAttack() { } virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ); @@ -26,10 +27,10 @@ class CNEOBotAttack : public Action< CNEOBot > virtual const char *GetName( void ) const { return "Attack"; }; private: + bool m_bSawEnemySinceLastPathCompute; // throttles m_attackCoverArea search + const CNavArea *m_attackCoverArea; // attempting to advance towards this cover area + const CNavArea *m_goalArea; // if set, engage enemies while moving towards this destination PathFollower m_path; ChasePath m_chasePath; - CountdownTimer m_attackCoverTimer; CountdownTimer m_grenadeThrowCooldownTimer; - CountdownTimer m_repathTimer; - CNavArea *m_attackCoverArea; }; diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index 16a8c8ac89..f68019d247 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -57,6 +57,19 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval return Done("Lost commander or released"); } + if (const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true) ) + { + if ( CNEO_Player *pCommander = me->m_hCommandingPlayer.Get() ) + { + const Vector& vWaypointPingLocation = pCommander->m_vLastPingByStar.Get(me->GetStar()); + return SuspendFor( new CNEOBotAttack(vWaypointPingLocation), "Engaging enemy en route to waypoint" ); + } + else + { + return SuspendFor( new CNEOBotAttack, "Breaking formation to engage enemy" ); + } + } + ActionResult weaponRequestResult = CheckCommanderWeaponRequest(me); if (weaponRequestResult.IsRequestingChange()) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp index 752ca593e2..9ad40bf0a8 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp @@ -15,8 +15,7 @@ CNEOBotCtgEnemy::CNEOBotCtgEnemy( void ) //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCtgEnemy::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) { - m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); - m_repathTimer.Invalidate(); + m_chasePath.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); return Continue(); } @@ -40,20 +39,14 @@ ActionResult< CNEOBot > CNEOBotCtgEnemy::Update( CNEOBot *me, float interval ) return Done( "Ghost carrier is friendly" ); } - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( threat && !threat->IsObsolete() && me->GetIntentionInterface()->ShouldAttack( me, threat ) ) { - return SuspendFor( new CNEOBotAttack, "Attacking ghoster team" ); + return SuspendFor( new CNEOBotAttack(pGhostCarrier->GetAbsOrigin()), "Attacking ghoster team" ); } // Investigate the ghost carrier's position - if ( m_repathTimer.IsElapsed() ) - { - // FASTEST_ROUTE: don't waste chase time looking for cover - CNEOBotPathCompute( me, m_path, pGhostCarrier->GetAbsOrigin(), FASTEST_ROUTE ); - m_repathTimer.Start( RandomFloat( 0.2f, 1.0f ) ); - } - m_path.Update( me ); + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, DEFAULT_ROUTE ); return Continue(); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h index c6a1ff77a0..152cad9457 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h @@ -20,8 +20,7 @@ class CNEOBotCtgEnemy : public Action< CNEOBot > virtual const char *GetName( void ) const override { return "ctgEnemy"; } private: - PathFollower m_path; - CountdownTimer m_repathTimer; + ChasePath m_chasePath; }; #endif // NEO_BOT_CTG_ENEMY_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp index 2b918fe360..146af9d136 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp @@ -58,13 +58,14 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) CNEOBot *pBotGhostCarrier = ToNEOBot( pGhostCarrier ); if ( pBotGhostCarrier ) { - const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( carrierThreat ) { + const Vector& carrierThreatPos = carrierThreat->GetLastKnownPosition(); // Check if the threat has a clear shot at our friend - if ( me->IsLineOfFireClear( carrierThreat->GetLastKnownPosition(), pGhostCarrier, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + if ( me->IsLineOfFireClear( carrierThreatPos, pGhostCarrier, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) { - return SuspendFor( new CNEOBotAttack, "Assisting Ghost Carrier with their threat" ); + return SuspendFor( new CNEOBotAttack( carrierThreatPos ), "Assisting Ghost Carrier with their threat" ); } } } @@ -83,17 +84,25 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) } } + if ( const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true) ) + { + const Vector& ghosterPos = pGhostCarrier->GetAbsOrigin(); + const Vector& threatPos = threat->GetLastKnownPosition(); + if ( me->GetAbsOrigin().DistToSqr( threatPos ) > ghosterPos.DistToSqr( threatPos ) ) + { + return SuspendFor( new CNEOBotAttack( ghosterPos ), "Engaging threats while regrouping with ghoster" ); + } + else + { + return SuspendFor( new CNEOBotAttack(), "Intercepting threat while protecting ghoster" ); + } + } + bool bCanSeeCarrier = me->GetVisionInterface()->IsLineOfSightClear( pGhostCarrier->WorldSpaceCenter() ); if ( bCanSeeCarrier ) { m_lostSightOfCarrierTimer.Invalidate(); - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); - if ( threat && threat->GetEntity() && threat->GetEntity()->IsAlive() ) - { - return SuspendFor( new CNEOBotAttack, "Breaking away from ghoster to engage threat" ); - } - if ( !m_bHasGoal ) { // Asymmetric defense: No goal cap zone, so defend the carrier. diff --git a/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.cpp b/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.cpp index 49146977ff..4cdcc57ca9 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.cpp @@ -109,27 +109,20 @@ ActionResult CNEOBotJgrCapture::Update( CNEOBot *me, float interval ) } } - CBasePlayer *pActivatingPlayer = m_hObjective->GetActivatingPlayer(); - if ( pActivatingPlayer ) + if ( me->GetVisionInterface()->GetPrimaryKnownThreat() ) { - if ( !me->InSameTeam( pActivatingPlayer ) ) - { - return SuspendFor( new CNEOBotAttack, "Attacking enemy capturing the juggernaut" ); - } - else if ( pActivatingPlayer != me ) - { - if ( me->GetVisionInterface()->GetPrimaryKnownThreat() ) - { - return SuspendFor( new CNEOBotAttack, "Defending teammate capturing the juggernaut" ); - } + return SuspendFor( new CNEOBotAttack, "Engaging enemy near juggernaut capture zone" ); + } - // Look away from the juggernaut to watch for threats - CNEOBotJgrCapture::LookAwayFrom( me, m_hObjective ); + CBasePlayer *pActivatingPlayer = m_hObjective->GetActivatingPlayer(); + if ( pActivatingPlayer && me->InSameTeam( pActivatingPlayer ) && ( pActivatingPlayer != me ) ) + { + // Look away from the juggernaut to watch for threats + CNEOBotJgrCapture::LookAwayFrom( me, m_hObjective ); - me->ReleaseUseButton(); - m_useAttemptTimer.Invalidate(); - return Continue(); - } + me->ReleaseUseButton(); + m_useAttemptTimer.Invalidate(); + return Continue(); } if ( me->GetAbsOrigin().DistToSqr( m_hObjective->GetAbsOrigin() ) < CNEO_Juggernaut::GetUseDistanceSquared() ) diff --git a/src/game/server/neo/bot/behavior/neo_bot_jgr_enemy.cpp b/src/game/server/neo/bot/behavior/neo_bot_jgr_enemy.cpp index 38c1e0ef44..5e1c18867f 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_jgr_enemy.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_jgr_enemy.cpp @@ -42,10 +42,10 @@ ActionResult< CNEOBot > CNEOBotJgrEnemy::Update( CNEOBot *me, float interval ) return Done( "Juggernaut is friendly" ); } - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( threat && !threat->IsObsolete() && me->GetIntentionInterface()->ShouldAttack( me, threat ) ) { - return SuspendFor( new CNEOBotAttack, "Attacking threat" ); + return SuspendFor( new CNEOBotAttack( pJuggernaut->GetAbsOrigin() ), "Attacking threat en route to juggernaut" ); } // Chase the Juggernaut diff --git a/src/game/server/neo/bot/behavior/neo_bot_jgr_escort.cpp b/src/game/server/neo/bot/behavior/neo_bot_jgr_escort.cpp index 7fb7c271ce..bfdd18919a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_jgr_escort.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_jgr_escort.cpp @@ -47,26 +47,11 @@ ActionResult< CNEOBot > CNEOBotJgrEscort::Update( CNEOBot *me, float interval ) return Done( "Juggernaut is not friendly" ); } - // Check if we can assist the Juggernaut (if they are a bot) - CNEOBot *pBotJuggernaut = ToNEOBot( pJuggernaut ); - if ( pBotJuggernaut ) - { - const CKnownEntity *juggernautThreat = pBotJuggernaut->GetVisionInterface()->GetPrimaryKnownThreat(); - if ( juggernautThreat ) - { - // Check if the threat has a clear shot at our friend - if ( me->IsLineOfFireClear( juggernautThreat->GetLastKnownPosition(), pJuggernaut, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) - { - return SuspendFor( new CNEOBotAttack, "Assisting Juggernaut with their threat" ); - } - } - } - // Check for own threats - const CKnownEntity *myThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + const CKnownEntity *myThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( myThreat && myThreat->GetEntity() && myThreat->GetEntity()->IsAlive() ) { - return SuspendFor( new CNEOBotAttack, "Breaking away from Juggernaut to engage threat" ); + return SuspendFor( new CNEOBotAttack( pJuggernaut->GetAbsOrigin() ), "Engaging enemy to lure them to Juggernaut" ); } // Just follow the Juggernaut diff --git a/src/game/server/neo/bot/behavior/neo_bot_jgr_seek.cpp b/src/game/server/neo/bot/behavior/neo_bot_jgr_seek.cpp index 15535cd318..fb44636051 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_jgr_seek.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_jgr_seek.cpp @@ -55,6 +55,11 @@ ActionResult< CNEOBot > CNEOBotJgrSeek::Update( CNEOBot *me, float interval ) // Juggernaut objective capture logic if (m_bGoingToTargetEntity && m_hTargetEntity) { + if ( me->GetVisionInterface()->GetPrimaryKnownThreat(true) ) + { + return SuspendFor( new CNEOBotAttack( m_hTargetEntity->GetAbsOrigin() ), "Engaging enemies en route to juggernaut" ); + } + const float useRangeSq = CNEO_Juggernaut::GetUseDistanceSquared() * 0.8f; if ( me->GetAbsOrigin().DistToSqr( m_hTargetEntity->GetAbsOrigin() ) < useRangeSq ) { @@ -75,10 +80,6 @@ ActionResult< CNEOBot > CNEOBotJgrSeek::Update( CNEOBot *me, float interval ) { CNEOBotJgrCapture::LookAwayFrom( me, pJgr ); m_path.Invalidate(); // wait at juggernaut - if ( me->GetVisionInterface()->GetPrimaryKnownThreat() ) - { - return SuspendFor( new CNEOBotAttack, "Intercepting enemies near juggernaut" ); - } return Continue(); } } diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp index 479397f94d..e98035eaf6 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp @@ -103,7 +103,7 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor const CNavArea *grenadeArea = TheNavMesh->GetNavArea( m_grenade->GetAbsOrigin() ); if ( grenadeArea ) { - if ( area->IsPotentiallyVisible( grenadeArea ) ) + if ( grenadeArea->IsPotentiallyVisible( area ) ) { // area is exposed to grenade line of sight return true; @@ -212,14 +212,14 @@ ActionResult< CNEOBot > CNEOBotRetreatFromGrenade::Update( CNEOBot *me, float in bool bIsExposed = false; if ( grenadeArea && me->GetLastKnownArea() ) { - if ( me->GetLastKnownArea()->IsPotentiallyVisible( grenadeArea ) ) + if ( grenadeArea->IsPotentiallyVisible( me->GetLastKnownArea() ) ) { bIsExposed = true; } } // track projectile and relation to escape destination every update - if ( !m_coverArea || ( grenadeArea && m_coverArea->IsPotentiallyVisible( grenadeArea ) ) ) + if ( !m_coverArea || ( grenadeArea && grenadeArea->IsPotentiallyVisible( m_coverArea ) ) ) { m_coverArea = FindCoverArea( me ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index 62cf2785be..b31b8a5ae4 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -60,7 +60,7 @@ class CTestAreaAgainstThreats : public IVision::IForEachKnownEntity return true; // Can't test area if we don't know last area of threat } - if ( !m_area->IsPotentiallyVisible( threatArea ) ) + if ( !threatArea->IsPotentiallyVisible( m_area ) ) { return true; // Candidate area is not visible by threat } diff --git a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp index abcddc491b..00419171c3 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp @@ -183,7 +183,7 @@ ActionResult< CNEOBot > CNEOBotSeekAndDestroy::UpdateCommon( CNEOBot *me, float return Done( "Disabled." ); } - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); if ( threat ) { @@ -193,11 +193,55 @@ ActionResult< CNEOBot > CNEOBotSeekAndDestroy::UpdateCommon( CNEOBot *me, float const bool bDontSuspendForGhoster = (neoThreat && neoThreat->IsCarryingGhost()); if (!bDontSuspendForGhoster) { - const float engageRange = 1000.0f; - if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), engageRange ) ) + const Vector& threatLastKnownPos = threat->GetLastKnownPosition(); + // fall back to nearest teammate for backup if I am the closest contact to enemy + if ( NEORules()->IsTeamplay() ) { - return SuspendFor( new CNEOBotAttack, "Going after an enemy" ); + bool bAnyTeammatesCloserToEnemy = false; + CNEO_Player* pNearestTeammate = nullptr; + float distToNearestTeammateSqr = FLT_MAX; + const Vector& myPos = me->GetAbsOrigin(); + const float distMeToThreatSqr = myPos.DistToSqr(threatLastKnownPos); + + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); + if (!pPlayer) + { + continue; + } + + CNEO_Player *pNeoCandidate = ToNEOPlayer(pPlayer); + if (!pNeoCandidate) + { + continue; + } + + if (pNeoCandidate->InSameTeam(me) && (me != pNeoCandidate)) + { + const Vector& candidatePos = pNeoCandidate->GetAbsOrigin(); + if (threatLastKnownPos.DistToSqr(candidatePos) < distMeToThreatSqr) + { + bAnyTeammatesCloserToEnemy = true; + break; + } + + float distCandidateSqr = myPos.DistToSqr(candidatePos); + if (distCandidateSqr < distToNearestTeammateSqr) + { + distToNearestTeammateSqr = distCandidateSqr; + pNearestTeammate = pNeoCandidate; + } + } + } + + if (!bAnyTeammatesCloserToEnemy && pNearestTeammate) + { + return SuspendFor( new CNEOBotAttack( pNearestTeammate->GetAbsOrigin() ), "Kiting enemy toward teammate for backup" ); + } } + + return SuspendFor( new CNEOBotAttack, "Going after an enemy" ); } } else diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 5a3536e136..9ad8ba9e54 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -192,38 +192,38 @@ ConVar sv_neo_bot_cloak_detection_bonus_assault_motion_vision("sv_neo_bot_cloak_ "Bot cloak detection bonus for assault class detecting movement with motion vision", true, 0, true, 100); // Support has difficulty seeing cloak in thermal vision -ConVar sv_neo_bot_cloak_detection_bonus_non_support("sv_neo_bot_cloak_detection_bonus_non_support", "1", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_non_support("sv_neo_bot_cloak_detection_bonus_non_support", "5", FCVAR_NONE, "Bot cloak detection bonus for non-support classes", true, 0, true, 100); // 0.7 dot product is about a 45 degree half hangle for a 90 degree cone ConVar sv_neo_bot_cloak_detection_aim_bonus_dot_threshold("sv_neo_bot_cloak_detection_aim_bonus_dot_threshold", "0.3", FCVAR_NONE, "Bot cloak detection bonus minimum dot product threshold for aim bonus", true, 0.01, true, 0.7); -ConVar sv_neo_bot_cloak_detection_bonus_observer_stationary("sv_neo_bot_cloak_detection_bonus_observer_stationary", "2", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_observer_stationary("sv_neo_bot_cloak_detection_bonus_observer_stationary", "10", FCVAR_NONE, "Bot cloak detection bonus for observer being stationary", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_observer_walking("sv_neo_bot_cloak_detection_bonus_observer_walking", "1", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_observer_walking("sv_neo_bot_cloak_detection_bonus_observer_walking", "5", FCVAR_NONE, "Bot cloak detection bonus for observer walking", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_target_running("sv_neo_bot_cloak_detection_bonus_target_running", "2", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_target_running("sv_neo_bot_cloak_detection_bonus_target_running", "10", FCVAR_NONE, "Bot cloak detection bonus for target running", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_target_moving("sv_neo_bot_cloak_detection_bonus_target_moving", "1", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_target_moving("sv_neo_bot_cloak_detection_bonus_target_moving", "5", FCVAR_NONE, "Bot cloak detection bonus for target moving", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_target_standing("sv_neo_bot_cloak_detection_bonus_target_standing", "1", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_target_standing("sv_neo_bot_cloak_detection_bonus_target_standing", "5", FCVAR_NONE, "Bot cloak detection bonus for target standing", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_scope_range("sv_neo_bot_cloak_detection_bonus_scope_range", "1", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_scope_range("sv_neo_bot_cloak_detection_bonus_scope_range", "10", FCVAR_NONE, "Bot cloak detection bonus for being in scope range", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_shotgun_range("sv_neo_bot_cloak_detection_bonus_shotgun_range", "5", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_shotgun_range("sv_neo_bot_cloak_detection_bonus_shotgun_range", "60", FCVAR_NONE, "Bot cloak detection bonus for being in shotgun range", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_melee_range("sv_neo_bot_cloak_detection_bonus_melee_range", "50", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_melee_range("sv_neo_bot_cloak_detection_bonus_melee_range", "80", FCVAR_NONE, "Bot cloak detection bonus for being in melee range", true, 0, true, 100); -ConVar sv_neo_bot_cloak_detection_bonus_per_injury("sv_neo_bot_cloak_detection_bonus_per_injury", "1", FCVAR_NONE, +ConVar sv_neo_bot_cloak_detection_bonus_per_injury("sv_neo_bot_cloak_detection_bonus_per_injury", "5", FCVAR_NONE, "Bot cloak detection bonus per injury event", true, 0, true, 100); // TODO: Lighting information is not yet baked into NavAreas, so we would need to implement that for bots to detect based on lighting