From 5b788a9e6af25d51cea499d44137abeb6061bdbc Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 8 Mar 2026 22:51:44 -0600 Subject: [PATCH] Bots leapfrog between cover areas when attacking --- .../neo/bot/behavior/neo_bot_attack.cpp | 150 ++++++++++++++++++ .../server/neo/bot/behavior/neo_bot_attack.h | 2 + .../neo/bot/behavior/neo_bot_ctg_escort.cpp | 16 +- .../neo/bot/behavior/neo_bot_jgr_escort.cpp | 12 +- 4 files changed, 172 insertions(+), 8 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 b9a3f463e0..d091155bd8 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -1,6 +1,7 @@ #include "cbase.h" #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" @@ -17,6 +18,7 @@ ConVar neo_bot_aggressive( "neo_bot_aggressive", "0", FCVAR_NONE ); //--------------------------------------------------------------------------------------------- CNEOBotAttack::CNEOBotAttack( void ) : m_chasePath( ChasePath::LEAD_SUBJECT ) { + m_attackCoverArea = nullptr; } @@ -24,11 +26,110 @@ 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 +class CSearchForAttackCover : public ISearchSurroundingAreasFunctor +{ +public: + CSearchForAttackCover( CNEOBot *me, const CKnownEntity *threat ) : m_me( me ), m_threat( threat ) + { + m_attackCoverArea = nullptr; + m_threatArea = threat->GetLastKnownArea(); + m_distToThreatSq = ( threat->GetLastKnownPosition() - me->GetAbsOrigin() ).LengthSqr(); + } + + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) + { + // return true to keep searching, false when suitable cover is found + CNavArea *area = static_cast(baseArea); + + if ( !m_threatArea ) + { + return false; // threat area is unknown + } + + constexpr float distanceThresholdRatio = 1.2f; + float candidateDistFromMeSq = ( m_me->GetAbsOrigin() - area->GetCenter() ).LengthSqr(); + if ( candidateDistFromMeSq > m_distToThreatSq * distanceThresholdRatio ) + { + return true; // not close enough to justify rerouting + } + + if ( area->IsPotentiallyVisible( m_threatArea ) ) + { + // Even if area does not have hard cover, see if Support can use smoke concealment + if ( m_me->GetClass() == NEO_CLASS_SUPPORT ) + { + CNEO_Player *pThreatPlayer = ToNEOPlayer( m_threat->GetEntity() ); + if ( pThreatPlayer && ( pThreatPlayer->GetClass() != NEO_CLASS_SUPPORT ) ) + { + ScopedSmokeLOS smokeScope( false ); + + 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 ); + + if ( tr.fraction < 1.0f ) + { + m_attackCoverArea = area; + return false; // found smoke as concealment + } + } + } + + return true; // not cover + } + + // found hard cover + m_attackCoverArea = area; + return false; // found suitable cover + } + + virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) + { + if ( travelDistanceSoFar > 1000.0f ) + { + return false; + } + + // For considering areas off to the side of current area + constexpr float distanceThresholdRatio = 0.9f; + + // 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 ) + { + return false; // Candidate adjacent area veers farther from threat + } + + // The adjacent area to search should not be beyond the threat + if ( adjThreatDistance > m_distToThreatSq ) + { + return false; // Candidate adjacent area is beyond threat distance + } + + // Don't consider areas that require jumping when engaging enemy + return ( currentArea->ComputeAdjacentConnectionHeightChange( adjArea ) < m_me->GetLocomotionInterface()->GetStepHeight() ); + } + + CNEOBot *m_me; + const CKnownEntity *m_threat; + CNavArea *m_attackCoverArea; + const CNavArea *m_threatArea; // reference point of the threat + float m_distToThreatSq; // the bot's current distance to the threat +}; + + //--------------------------------------------------------------------------------------------- // head aiming and weapon firing is handled elsewhere - we just need to get into position to fight ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) @@ -90,6 +191,55 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) } } + // Look for opportunities to leap frog from cover to cover + if ( m_attackCoverTimer.IsElapsed() ) + { + 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 ); 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 a8890d07eb..d2a74dcd3b 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.h +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.h @@ -25,6 +25,8 @@ class CNEOBotAttack : public Action< CNEOBot > private: 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_ctg_escort.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp index 13f1acd5aa..2b918fe360 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 @@ -8,6 +8,8 @@ #include "neo_ghost_cap_point.h" #include "weapons/weapon_ghost.h" +extern ConVar sv_neo_grenade_blast_radius; + //--------------------------------------------------------------------------------------------- CNEOBotCtgEscort::CNEOBotCtgEscort( void ) : m_role( ROLE_SCREEN ), @@ -86,6 +88,12 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) { 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. @@ -131,10 +139,13 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) } else { + const float flSafeRadius = sv_neo_grenade_blast_radius.GetFloat(); + const float flSafeRadiusSq = flSafeRadius * flSafeRadius; + if ( m_role == ROLE_BODYGUARD ) { // Dont crowd the carrier - if ( me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ) < ( 100.0f * 100.0f ) ) + if ( me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ) < flSafeRadiusSq ) { m_path.Invalidate(); m_chasePath.Invalidate(); @@ -160,8 +171,7 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) // No active threats to carrier float flDistToCarrierSq = me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ); - constexpr float regroupDistanceSq = 300.0f * 300.0f; - if ( flDistToCarrierSq > regroupDistanceSq ) + if ( flDistToCarrierSq > flSafeRadiusSq ) { // Regroup CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); 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 d01d86dd94..7fb7c271ce 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 @@ -3,10 +3,11 @@ #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_jgr_escort.h" -#include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/neo_bot_path_compute.h" #include "neo_gamerules.h" +extern ConVar sv_neo_grenade_blast_radius; + //--------------------------------------------------------------------------------------------- CNEOBotJgrEscort::CNEOBotJgrEscort( void ) { @@ -63,17 +64,18 @@ ActionResult< CNEOBot > CNEOBotJgrEscort::Update( CNEOBot *me, float interval ) // Check for own threats const CKnownEntity *myThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(); - if ( myThreat ) + if ( myThreat && myThreat->GetEntity() && myThreat->GetEntity()->IsAlive() ) { - return SuspendFor( new CNEOBotRetreatToCover, "Retreating to let Juggernaut get the kill" ); + return SuspendFor( new CNEOBotAttack, "Breaking away from Juggernaut to engage threat" ); } // Just follow the Juggernaut // If too close, stop moving to avoid crowding - constexpr float flMinFollowDistSq = 200.0f * 200.0f; float flDistSq = me->GetAbsOrigin().DistToSqr( pJuggernaut->GetAbsOrigin() ); + float flSafeRadius = sv_neo_grenade_blast_radius.GetFloat(); + float flSafeRadiusSq = flSafeRadius * flSafeRadius; - if ( flDistSq < flMinFollowDistSq ) + if ( flDistSq < flSafeRadiusSq ) { // Too close, stop moving if ( m_chasePath.IsValid() )