From 2057419aedcfec23012c24cfa091e814da774869 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 15 Mar 2026 15:33:09 -0600 Subject: [PATCH] Improve bot JGR capture reliability --- .../neo/bot/behavior/neo_bot_jgr_capture.cpp | 141 ++++++++++++++---- .../neo/bot/behavior/neo_bot_jgr_capture.h | 5 + .../neo/bot/behavior/neo_bot_jgr_seek.cpp | 31 +++- .../bot/behavior/neo_bot_tactical_monitor.cpp | 2 +- src/game/shared/neo/neo_juggernaut.h | 2 + 5 files changed, 151 insertions(+), 30 deletions(-) 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 b9847b82ab..49146977ff 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 @@ -1,10 +1,28 @@ #include "cbase.h" #include "bot/behavior/neo_bot_jgr_capture.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" +#include "bot/behavior/neo_bot_attack.h" #include "bot/neo_bot_path_compute.h" #include "neo_gamerules.h" #include "neo_juggernaut.h" +static const float JGR_CAPTURE_FACING_DOT = 0.9f; +static const float JGR_CAPTURE_LOCKED_RETREAT_TIME = 2.0f; +static const float JGR_CAPTURE_MAX_REPATH_DELAY = 5.0f; +static const float JGR_CAPTURE_MIN_REPATH_DELAY = 1.0f; +static const float JGR_CAPTURE_REPOSITION_RATIO = 0.7f; +static const float JGR_CAPTURE_TIMER_BUFFER = 1.0f; + +//--------------------------------------------------------------------------------------------- +void CNEOBotJgrCapture::StopMoving( CNEOBot *me ) +{ + m_path.Invalidate(); + me->ReleaseForwardButton(); + me->ReleaseBackwardButton(); + me->ReleaseLeftButton(); + me->ReleaseRightButton(); +} + //--------------------------------------------------------------------------------------------- CNEOBotJgrCapture::CNEOBotJgrCapture( CNEO_Juggernaut *pObjective ) { @@ -17,6 +35,7 @@ ActionResult CNEOBotJgrCapture::OnStart( CNEOBot *me, Action * m_useAttemptTimer.Invalidate(); m_path.Invalidate(); m_repathTimer.Invalidate(); + m_bStrafeRight = ( RandomInt( 0, 1 ) == 1 ); if ( !m_hObjective ) { @@ -35,6 +54,7 @@ ActionResult CNEOBotJgrCapture::OnStart( CNEOBot *me, Action * // Ignore enemies while capturing juggernaut me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); me->ReloadIfLowClip(); // might as well as we're preoccupied return Continue(); } @@ -44,24 +64,21 @@ void CNEOBotJgrCapture::OnEnd( CNEOBot *me, Action *nextAction ) { me->ReleaseUseButton(); me->StartLookingAroundForEnemies(); + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); } //--------------------------------------------------------------------------------------------- ActionResult CNEOBotJgrCapture::OnSuspend( CNEOBot *me, Action *interruptingAction ) { - // CNEOBotTacticalMonitor -> CNEOBotRetreatToCover will handle reacting to enemies if under fire - // Debatably, maybe bots should just ignore enemies, but that would require a change to CNEOBotTacticalMonitor - // Also it might be more fun for humans if they can interrupt bots from taking the Juggernaut. - me->ReleaseUseButton(); - me->StartLookingAroundForEnemies(); - return Continue(); + // Situation around juggernaut is possibly stale, reevaluate + return Done( "OnSuspend: Reevaluating capture situation" ); } //--------------------------------------------------------------------------------------------- ActionResult CNEOBotJgrCapture::OnResume( CNEOBot *me, Action *interruptingAction ) { - me->StopLookingAroundForEnemies(); - return Continue(); + // Situation around juggernaut is possibly stale, reevaluate + return Done( "OnResume: Reevaluating capture situation" ); } //--------------------------------------------------------------------------------------------- @@ -92,6 +109,29 @@ ActionResult CNEOBotJgrCapture::Update( CNEOBot *me, float interval ) } } + CBasePlayer *pActivatingPlayer = m_hObjective->GetActivatingPlayer(); + if ( pActivatingPlayer ) + { + 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" ); + } + + // Look away from the juggernaut to watch for threats + CNEOBotJgrCapture::LookAwayFrom( me, m_hObjective ); + + me->ReleaseUseButton(); + m_useAttemptTimer.Invalidate(); + return Continue(); + } + } + if ( me->GetAbsOrigin().DistToSqr( m_hObjective->GetAbsOrigin() ) < CNEO_Juggernaut::GetUseDistanceSquared() ) { if ( NEORules()->IsJuggernautLocked() ) @@ -101,47 +141,76 @@ ActionResult CNEOBotJgrCapture::Update( CNEOBot *me, float interval ) Assert( NEORules()->IsJuggernautLocked() == (pJuggernaut && pJuggernaut->m_bLocked) ); #endif me->ReleaseUseButton(); - return SuspendFor( new CNEOBotRetreatToCover( 2.0f ), "Juggernaut is locked, taking cover to wait for it to unlock" ); + m_useAttemptTimer.Invalidate(); + + // Look away from the juggernaut while it's locked to watch for threats + CNEOBotJgrCapture::LookAwayFrom( me, m_hObjective ); } - - // Stop moving while using - m_path.Invalidate(); - me->ReleaseForwardButton(); - me->ReleaseBackwardButton(); - me->ReleaseLeftButton(); - me->ReleaseRightButton(); const Vector vecObjectiveCenter = m_hObjective->WorldSpaceCenter(); + me->GetBodyInterface()->AimHeadTowards( vecObjectiveCenter, IBody::MANDATORY, 0.1f, nullptr, "Focusing on Juggernaut objective" ); + + // Check if we are facing juggernaut and have a clear line of sight Vector vecToTargetDir = vecObjectiveCenter - me->EyePosition(); vecToTargetDir.NormalizeInPlace(); - // Ensure we are facing the target before attempting to use Vector vecEyeDirection; me->EyeVectors( &vecEyeDirection ); const float flDot = vecEyeDirection.Dot( vecToTargetDir ); - const bool bIsFacing = flDot > 0.9f; + const bool bIsFacing = flDot > JGR_CAPTURE_FACING_DOT; - me->GetBodyInterface()->AimHeadTowards( vecObjectiveCenter, IBody::CRITICAL, 0.1f, NULL, "Looking at Juggernaut objective to use" ); + trace_t trace; + UTIL_TraceLine( me->EyePosition(), vecObjectiveCenter, MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &trace ); - if ( m_useAttemptTimer.HasStarted() ) + if ( trace.m_pEnt == m_hObjective ) { - if ( m_useAttemptTimer.IsElapsed() ) + if ( bIsFacing && me->GetBodyInterface()->IsHeadAimingOnTarget() ) { - return Done( "Use timer elapsed, failed to capture" ); + if ( !m_useAttemptTimer.HasStarted() ) + { + m_useAttemptTimer.Start( CNEO_Juggernaut::GetUseDuration() + JGR_CAPTURE_TIMER_BUFFER ); + } + + me->PressUseButton(); + StopMoving( me ); + + if ( m_useAttemptTimer.IsElapsed() ) + { + return Done( "Activation attempt expired" ); + } } } - else if ( bIsFacing && me->GetBodyInterface()->IsHeadAimingOnTarget() ) + else { - m_useAttemptTimer.Start( CNEO_Juggernaut::GetUseDuration() + 1.0f ); - me->PressUseButton( CNEO_Juggernaut::GetUseDuration() + 1.0f ); + // Trace blocked (usually by teammate), reposition to get a better angle + me->ReleaseUseButton(); + m_useAttemptTimer.Invalidate(); + m_path.Invalidate(); + + const float flRepositionDistSqr = JGR_CAPTURE_REPOSITION_RATIO * CNEO_Juggernaut::GetUseDistanceSquared(); + if ( me->GetAbsOrigin().DistToSqr( m_hObjective->GetAbsOrigin() ) > flRepositionDistSqr ) + { + me->PressForwardButton( 0.2f ); + } + + if ( m_bStrafeRight ) + { + me->PressRightButton( 1.0f ); + me->ReleaseLeftButton(); + } + else + { + me->PressLeftButton( 1.0f ); + me->ReleaseRightButton(); + } } } else { - // Objective is farther than we can reach for cpature - if ( m_repathTimer.IsElapsed() ) + // Objective is farther than we can reach for capture + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) { - m_repathTimer.Start( RandomFloat( 1.0f, 5.0f ) ); + m_repathTimer.Start( RandomFloat( JGR_CAPTURE_MIN_REPATH_DELAY, JGR_CAPTURE_MAX_REPATH_DELAY ) ); if ( !CNEOBotPathCompute( me, m_path, m_hObjective->GetAbsOrigin(), FASTEST_ROUTE ) ) { return Done( "Unable to find a path to the Juggernaut objective" ); @@ -152,3 +221,19 @@ ActionResult CNEOBotJgrCapture::Update( CNEOBot *me, float interval ) return Continue(); } + +//--------------------------------------------------------------------------------------------- +void CNEOBotJgrCapture::LookAwayFrom( CNEOBot *me, CBaseEntity *pTarget ) +{ + if ( !pTarget ) + { + return; + } + + Vector vecAwayFromTarget = me->GetAbsOrigin() - pTarget->GetAbsOrigin(); + Vector2D vecLookDir2D = vecAwayFromTarget.AsVector2D(); + vecLookDir2D.NormalizeInPlace(); + + const Vector vecLookPos = me->EyePosition() + Vector( vecLookDir2D.x, vecLookDir2D.y, 0.0f ) * 500.0f; + me->GetBodyInterface()->AimHeadTowards( vecLookPos, IBody::IMPORTANT, 0.1f, nullptr, "Facing away from target to watch for threats" ); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.h b/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.h index 984697a65a..9d12b1cd72 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.h +++ b/src/game/server/neo/bot/behavior/neo_bot_jgr_capture.h @@ -19,7 +19,12 @@ class CNEOBotJgrCapture : public Action virtual ActionResult OnSuspend( CNEOBot *me, Action *interruptingAction ) override; virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; + static void LookAwayFrom( CNEOBot *me, CBaseEntity *pTarget ); + private: + void StopMoving( CNEOBot *me ); + + bool m_bStrafeRight = false; CHandle m_hObjective; CountdownTimer m_useAttemptTimer; PathFollower m_path; 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 2bcdb19b8f..15535cd318 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 @@ -3,6 +3,7 @@ #include "neo_gamerules.h" #include "bot/neo_bot.h" #include "bot/neo_bot_path_compute.h" +#include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_jgr_seek.h" #include "bot/behavior/neo_bot_jgr_escort.h" #include "bot/behavior/neo_bot_jgr_enemy.h" @@ -65,7 +66,24 @@ ActionResult< CNEOBot > CNEOBotJgrSeek::Update( CNEOBot *me, float interval ) if ( FStrEq( classname, "neo_juggernaut" ) ) { - return SuspendFor( new CNEOBotJgrCapture( static_cast(m_hTargetEntity.Get()) ), "Capturing Juggernaut" ); + CNEO_Juggernaut *pJgr = static_cast( m_hTargetEntity.Get() ); + if ( pJgr ) + { + CBasePlayer *pActivatingPlayer = pJgr->GetActivatingPlayer(); + if ( NEORules()->IsJuggernautLocked() + || ( pActivatingPlayer && me->InSameTeam( pActivatingPlayer ) && pActivatingPlayer != me ) ) + { + CNEOBotJgrCapture::LookAwayFrom( me, pJgr ); + m_path.Invalidate(); // wait at juggernaut + if ( me->GetVisionInterface()->GetPrimaryKnownThreat() ) + { + return SuspendFor( new CNEOBotAttack, "Intercepting enemies near juggernaut" ); + } + return Continue(); + } + } + + return SuspendFor( new CNEOBotJgrCapture( pJgr ), "Capturing Juggernaut" ); } } } @@ -98,6 +116,17 @@ void CNEOBotJgrSeek::RecomputeSeekPath( CNEOBot *me ) CBaseEntity* pJuggernaut = gEntList.FindEntityByClassname(NULL, "neo_juggernaut"); if (pJuggernaut) { + const float useRangeSq = CNEO_Juggernaut::GetUseDistanceSquared() * 0.8f; + if ( me->GetAbsOrigin().DistToSqr( pJuggernaut->GetAbsOrigin() ) < useRangeSq ) + { + // We're already at the goal, no need to path. + m_vGoalPos = pJuggernaut->WorldSpaceCenter(); + m_bGoingToTargetEntity = true; + m_hTargetEntity = pJuggernaut; + m_path.Invalidate(); + return; + } + m_vGoalPos = pJuggernaut->WorldSpaceCenter(); m_bGoingToTargetEntity = true; m_hTargetEntity = pJuggernaut; diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp index a1229e7f1d..5f299aadef 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp @@ -430,7 +430,7 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter #endif CNEO_Player* pBotPlayer = ToNEOPlayer( me->GetEntity() ); - if ( pBotPlayer && !(pBotPlayer->m_nButtons & (IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT)) ) + if ( pBotPlayer && !(pBotPlayer->m_nButtons & (IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT | IN_USE)) ) { AvoidBumpingFriends( me ); } diff --git a/src/game/shared/neo/neo_juggernaut.h b/src/game/shared/neo/neo_juggernaut.h index 8c0029206e..c1740eba7d 100644 --- a/src/game/shared/neo/neo_juggernaut.h +++ b/src/game/shared/neo/neo_juggernaut.h @@ -33,6 +33,8 @@ class CNEO_Juggernaut : public CBaseAnimating virtual int ObjectCaps(void) { return BaseClass::ObjectCaps() | FCAP_ONOFF_USE; } virtual int UpdateTransmitState() override; + CNEO_Player* GetActivatingPlayer() const { return m_hHoldingPlayer.Get(); } + bool IsBeingActivated() const { return m_bIsHolding; } const bool IsBeingActivatedByLosingTeam(); bool m_bPostDeath = false;