From e191f60cd1e074f29b9c0c3a076f8461b426766e Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 31 Dec 2025 20:25:18 -0800 Subject: [PATCH 1/2] Bot CTG gameplay behaviors Disable bots always seeing ghoster Instead, rely on neo_bot_ctg_enemy behavior to guide enemies to ghost carrier by pathing using the ghoster's position. Reduce team allocation for path reservation Also rename ConVar enabling the path reservation system to be more generic Label NEO-specific Drop Button --- src/game/server/CMakeLists.txt | 12 + .../server/NextBot/Player/NextBotPlayer.h | 25 + .../neo/bot/behavior/neo_bot_behavior.cpp | 7 + .../bot/behavior/neo_bot_command_follow.cpp | 1 + .../neo/bot/behavior/neo_bot_command_follow.h | 2 + .../neo/bot/behavior/neo_bot_ctg_capture.cpp | 90 +++ .../neo/bot/behavior/neo_bot_ctg_capture.h | 26 + .../neo/bot/behavior/neo_bot_ctg_carrier.cpp | 653 ++++++++++++++++++ .../neo/bot/behavior/neo_bot_ctg_carrier.h | 59 ++ .../neo/bot/behavior/neo_bot_ctg_enemy.cpp | 84 +++ .../neo/bot/behavior/neo_bot_ctg_enemy.h | 27 + .../neo/bot/behavior/neo_bot_ctg_escort.cpp | 349 ++++++++++ .../neo/bot/behavior/neo_bot_ctg_escort.h | 44 ++ .../bot/behavior/neo_bot_ctg_lone_wolf.cpp | 501 ++++++++++++++ .../neo/bot/behavior/neo_bot_ctg_lone_wolf.h | 44 ++ .../neo/bot/behavior/neo_bot_ctg_seek.cpp | 144 ++++ .../neo/bot/behavior/neo_bot_ctg_seek.h | 18 + .../neo/bot/behavior/neo_bot_get_health.cpp | 4 +- .../bot/behavior/neo_bot_seek_and_destroy.cpp | 137 +--- src/game/server/neo/bot/neo_bot.cpp | 5 +- src/game/server/neo/bot/neo_bot_path_cost.cpp | 9 + src/game/server/neo/bot/neo_bot_vision.cpp | 25 +- src/game/shared/neo/neo_gamerules.h | 5 + 23 files changed, 2124 insertions(+), 147 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 8aa3b870ba..57a69beab9 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1443,6 +1443,18 @@ target_sources_grouped( neo/bot/behavior/neo_bot_command_follow.h neo/bot/behavior/neo_bot_dead.cpp neo/bot/behavior/neo_bot_dead.h + neo/bot/behavior/neo_bot_ctg_capture.cpp + neo/bot/behavior/neo_bot_ctg_capture.h + neo/bot/behavior/neo_bot_ctg_carrier.cpp + neo/bot/behavior/neo_bot_ctg_carrier.h + neo/bot/behavior/neo_bot_ctg_enemy.cpp + neo/bot/behavior/neo_bot_ctg_enemy.h + neo/bot/behavior/neo_bot_ctg_escort.cpp + neo/bot/behavior/neo_bot_ctg_escort.h + neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp + neo/bot/behavior/neo_bot_ctg_lone_wolf.h + neo/bot/behavior/neo_bot_ctg_seek.cpp + neo/bot/behavior/neo_bot_ctg_seek.h neo/bot/behavior/neo_bot_jgr_capture.cpp neo/bot/behavior/neo_bot_jgr_capture.h neo/bot/behavior/neo_bot_jgr_enemy.cpp diff --git a/src/game/server/NextBot/Player/NextBotPlayer.h b/src/game/server/NextBot/Player/NextBotPlayer.h index 2a3c846325..d12df0fe5e 100644 --- a/src/game/server/NextBot/Player/NextBotPlayer.h +++ b/src/game/server/NextBot/Player/NextBotPlayer.h @@ -122,6 +122,9 @@ class INextBotPlayerInput virtual void ReleaseJumpButton( void ) = 0; #ifdef NEO + virtual void PressDropButton( float duration = -1.0f ) = 0; + virtual void ReleaseDropButton( void ) = 0; + virtual void PressThermopticButton( float duration = -1.0f ) = 0; virtual void ReleaseThermopticButton( void ) = 0; #endif // NEO @@ -202,6 +205,9 @@ class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerI virtual void ReleaseSpecialFireButton( void ); #ifdef NEO + virtual void PressDropButton( float duration = -1.0f ); + virtual void ReleaseDropButton( void ); + virtual void PressThermopticButton( float duration = -1.0f ); virtual void ReleaseThermopticButton( void ); #endif @@ -286,6 +292,7 @@ class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerI CountdownTimer m_walkButtonTimer; CountdownTimer m_buttonScaleTimer; #ifdef NEO + CountdownTimer m_dropButtonTimer; CountdownTimer m_thermopticButtonTimer; CountdownTimer m_leanLeftButtonTimer; CountdownTimer m_leanRightButtonTimer; @@ -450,6 +457,20 @@ inline void NextBotPlayer< PlayerType >::ReleaseJumpButton( void ) } #ifdef NEO +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressDropButton( float duration ) +{ + m_inputButtons |= IN_DROP; + m_dropButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseDropButton( void ) +{ + m_inputButtons &= ~IN_DROP; + m_dropButtonTimer.Invalidate(); +} + template < typename PlayerType > inline void NextBotPlayer< PlayerType >::PressThermopticButton( float duration ) { @@ -642,6 +663,7 @@ inline void NextBotPlayer< PlayerType >::Spawn( void ) m_forwardScale = m_rightScale = 0.04; m_burningTimer.Invalidate(); #ifdef NEO + m_dropButtonTimer.Invalidate(); m_thermopticButtonTimer.Invalidate(); m_leanLeftButtonTimer.Invalidate(); m_leanRightButtonTimer.Invalidate(); @@ -780,6 +802,9 @@ inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void ) m_inputButtons |= IN_SPEED; #ifdef NEO + if ( !m_dropButtonTimer.IsElapsed() ) + m_inputButtons |= IN_DROP; + if ( !m_leanLeftButtonTimer.IsElapsed() ) m_inputButtons |= IN_LEAN_LEFT; diff --git a/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp b/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp index aa3e52deb4..3ce6465845 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp @@ -92,6 +92,13 @@ ActionResult< CNEOBot > CNEOBotMainAction::Update( CNEOBot *me, float interval ) // make sure our vision FOV matches the player's me->GetVisionInterface()->SetFieldOfView( me->GetFOV() ); + if (me->IsCarryingGhost()) + { + // Don't waste cloak power + // Incidentally flashing cloak is fine, everyone can see you anyway + me->DisableCloak(); + } + // track aim velocity ourselves, since body aim "steady" is too loose float deltaYaw = me->EyeAngles().y - m_priorYaw; m_yawRate = fabs( deltaYaw / ( interval + 0.0001f ) ); 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 adaa7545a1..7c125e1af1 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 @@ -54,6 +54,7 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval } m_path.Update(me); + m_ghostEquipmentHandler.Update( me ); return Continue(); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h index 49035ca12a..3920b908fb 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h @@ -5,6 +5,7 @@ #endif #include "NextBotBehavior.h" +#include "bot/behavior/neo_bot_ctg_carrier.h" class CNEOBotCommandFollow : public Action< CNEOBot > { @@ -24,6 +25,7 @@ class CNEOBotCommandFollow : public Action< CNEOBot > PathFollower m_path; CountdownTimer m_repathTimer; + CNEOBotGhostEquipmentHandler m_ghostEquipmentHandler; EHANDLE m_hTargetEntity; bool m_bGoingToTargetEntity = false; diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp new file mode 100644 index 0000000000..c7626d4dfd --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp @@ -0,0 +1,90 @@ +#include "cbase.h" +#include "bot/behavior/neo_bot_ctg_capture.h" +#include "bot/behavior/neo_bot_seek_weapon.h" +#include "bot/neo_bot_path_compute.h" +#include "weapon_ghost.h" + + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgCapture::CNEOBotCtgCapture( CWeaponGhost *pObjective ) +{ + m_hObjective = pObjective; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotCtgCapture::OnStart( CNEOBot *me, Action *priorAction ) +{ + m_captureAttemptTimer.Invalidate(); + m_repathTimer.Invalidate(); + m_path.Invalidate(); + + if ( !m_hObjective ) + { + return Done( "No ghost capture target specified." ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotCtgCapture::Update( CNEOBot *me, float interval ) +{ + if ( me->IsDead() ) + { + return Done( "I died before I could capture the ghost" ); + } + + if ( !m_hObjective ) + { + return Done( "Ghost capture target lost" ); + } + + if ( me->IsCarryingGhost() ) + { + return Done( "Captured ghost" ); + } + + if ( m_hObjective->GetOwner() ) + { + return Done( "Ghost was taken by someone else" ); + } + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + if ( !CNEOBotPathCompute( me, m_path, m_hObjective->GetAbsOrigin(), FASTEST_ROUTE ) ) + { + return Done( "Unable to find a path to the ghost capture target" ); + } + m_repathTimer.Start( RandomFloat( 0.3f, 0.6f ) ); + } + m_path.Update( me ); + + if ( !m_captureAttemptTimer.HasStarted() ) + { + // If this timer expires, give up + m_captureAttemptTimer.Start( 3.0f ); + } + + CBaseCombatWeapon *pPrimary = me->Weapon_GetSlot( 0 ); + if ( pPrimary ) + { + // Switch to primary weapon to drop it, if not already active + if ( me->GetActiveWeapon() != pPrimary ) + { + me->Weapon_Switch( pPrimary ); + } + else + { + me->PressDropButton( 0.1f ); + } + } + + if ( m_captureAttemptTimer.IsElapsed() ) + { + return ChangeTo( new CNEOBotSeekWeapon, "Failed to capture ghost in time" ); + } + + return Continue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h new file mode 100644 index 0000000000..324450081e --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h @@ -0,0 +1,26 @@ +#ifndef NEO_BOT_CTG_CAPTURE_H +#define NEO_BOT_CTG_CAPTURE_H + +#include "NextBotBehavior.h" +#include "bot/neo_bot.h" + +//---------------------------------------------------------------------------------------------------------------- +class CNEOBotCtgCapture : public Action +{ +public: + CNEOBotCtgCapture( CWeaponGhost *pObjective ); + virtual ~CNEOBotCtgCapture() { } + + virtual const char *GetName() const override { return "ctgCapture"; } + + virtual ActionResult OnStart( CNEOBot *me, Action *priorAction ) override; + virtual ActionResult Update( CNEOBot *me, float interval ) override; + +private: + CHandle m_hObjective; + CountdownTimer m_captureAttemptTimer; + CountdownTimer m_repathTimer; + PathFollower m_path; +}; + +#endif // NEO_BOT_CTG_CAPTURE_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp new file mode 100644 index 0000000000..ad8d6c8c88 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp @@ -0,0 +1,653 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_carrier.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" +#include "neo_ghost_cap_point.h" +#include "debugoverlay_shared.h" +#include "weapon_ghost.h" + +ConVar neo_debug_ghost_carrier( "neo_debug_ghost_carrier", "0", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +static void CollectPlayers( CNEOBot *me, CUtlVector *pOutTeammates, CUtlVector *pOutEnemies = nullptr ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer || !pPlayer->IsAlive() ) + { + continue; + } + + if ( pPlayer->GetTeamNumber() == me->GetTeamNumber() ) + { + if ( pPlayer != me && pOutTeammates ) + { + pOutTeammates->AddToTail( pPlayer ); + } + } + else if ( pOutEnemies ) + { + pOutEnemies->AddToTail( pPlayer ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +CNEOBotGhostEquipmentHandler::CNEOBotGhostEquipmentHandler() +{ + m_hCurrentFocusEnemy = nullptr; + m_enemyUpdateTimer.Invalidate(); + + for ( int i = 0; i < MAX_PLAYERS_ARRAY_SAFE; ++i ) + { + m_enemyLastPos[i] = CNEO_Player::VECTOR_INVALID_WAYPOINT; + } +} + +void CNEOBotGhostEquipmentHandler::Update( CNEOBot *me ) +{ + if ( !me->IsAlive() ) + { + return; + } + + if ( !me->IsCarryingGhost() ) + { + return; + } + + if ( !m_enemyUpdateTimer.HasStarted() ) + { + m_enemyUpdateTimer.Start( GetUpdateInterval( me ) ); + } + + EquipBestWeaponForGhoster( me ); + + bool bUpdateCallout = m_enemyUpdateTimer.IsElapsed(); + if ( bUpdateCallout ) + { + m_enemies.RemoveAll(); + CollectPlayers( me, nullptr, &m_enemies ); + UpdateGhostCarrierCallout( me, m_enemies ); + m_enemyUpdateTimer.Start( GetUpdateInterval( me ) ); + } + + // Debug: Highlight the location of the enemy a bot ghost carrier is calling out + if ( neo_debug_ghost_carrier.GetBool() ) + { + CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); + if ( pFocus && pFocus->IsPlayer() && pFocus->IsAlive() ) + { + NDebugOverlay::Cross3D( pFocus->GetAbsOrigin(), 20.0f, 255, 0, 0, true, 0.1f ); + } + } + + CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); + if ( pFocus && pFocus->IsAlive() ) + { + // Notify teammates to look at the enemy + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pTeammate = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pTeammate || !pTeammate->IsAlive() || pTeammate == me || pTeammate->GetTeamNumber() != me->GetTeamNumber() ) + { + continue; + } + + CNEOBot *pBot = ToNEOBot( pTeammate ); + if ( pBot ) + { + if ( pBot->IsLineOfFireClear( pFocus, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + // NEO Jank: Urge relevant teammate bots look at the enemy + pBot->GetBodyInterface()->AimHeadTowards( pFocus, IBody::IMPORTANT, 0.5f, nullptr, "Ghost carrier teammate look override" ); + } + else + { + // Force updates to known but not visible entity by forgetting them first + pBot->GetVisionInterface()->ForgetEntity( pFocus ); + } + pBot->GetVisionInterface()->AddKnownEntity( pFocus ); // keep after ForgetEntity + } + } + + me->GetBodyInterface()->AimHeadTowards( pFocus->WorldSpaceCenter(), IBody::IMPORTANT, 1.0f, nullptr, "Ghost carrier focus look" ); + } +} + +void CNEOBotGhostEquipmentHandler::EquipBestWeaponForGhoster( CNEOBot *me ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + CNEOBaseCombatWeapon *pActive = dynamic_cast( me->GetActiveWeapon() ); + CNEOBaseCombatWeapon *pGhost = dynamic_cast( me->Weapon_GetSlot( 0 ) ); + CNEOBaseCombatWeapon *pSecondary = dynamic_cast( me->Weapon_GetSlot( 1 ) ); + + // Sanity check: if we don't have the ghost, we shouldn't be in this behavior, but handle gracefully + if ( !pGhost ) + { + // Fallback to normal behavior if ghost is missing + me->EquipBestWeaponForThreat( threat ); + return; + } + + // See if we need to defend ourselves + bool bHasThreat = ( threat && threat->GetEntity() && threat->GetEntity()->IsAlive() ); + if ( bHasThreat ) + { + bool bCanSeeThreat = me->GetVisionInterface()->IsLineOfSightClear( threat->GetEntity()->WorldSpaceCenter() ); + if ( bCanSeeThreat ) + { + if ( pSecondary && pSecondary->HasAmmo() ) + { + // We can still call out enemies the old fashioned way ;) + me->Weapon_Switch( pSecondary ); + return; + } + } + } + + // Equip ghost to call out enemies behind walls + if ( pActive != pGhost ) + { + me->Weapon_Switch( pGhost ); + } +} + +float CNEOBotGhostEquipmentHandler::GetUpdateInterval( CNEOBot *me ) const +{ + switch ( me->GetDifficulty() ) + { + case CNEOBot::EASY: return 2.5f; + case CNEOBot::NORMAL: return 2.0f; + case CNEOBot::HARD: return 1.5f; + case CNEOBot::EXPERT: return 1.0f; + } + + return 3.0f; +} + +void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const CUtlVector &enemies ) +{ + CNEO_Player *pBestCallout = nullptr; + float flBestCalloutMoved = -1.0f; + float flBestCalloutDist = FLT_MAX; + bool bBestCalloutIsNew = false; + + bool bConsideringOnlyLoSEnemies = false; + + const Vector vecMyPos = me->GetAbsOrigin(); + + for ( int i = 0; i < enemies.Count(); ++i ) + { + CNEO_Player *pPlayer = enemies[i]; + + if ( pPlayer->IsEffectActive( EF_NODRAW ) ) + { + continue; + } + + int idx = pPlayer->entindex(); + if ( !IsIndexIntoPlayerArrayValid( idx ) ) + { + continue; + } + + // IsAbleToSee already checks if ghost is booted up to see enemies behind walls + if ( !me->GetVisionInterface()->IsAbleToSee( pPlayer, IVision::DISREGARD_FOV ) ) + { + continue; + } + + float flDistToMe = vecMyPos.DistTo( pPlayer->GetAbsOrigin() ); + + // Also take into account how much the enemy has moved since we last reported them + Vector vecLast = m_enemyLastPos[ idx ]; + float flMoved = 0.0f; + bool bIsNew = ( vecLast == CNEO_Player::VECTOR_INVALID_WAYPOINT ); + + if ( !bIsNew ) + { + flMoved = ( pPlayer->GetAbsOrigin() - vecLast ).Length(); + } + + // Any enemies in line of sight have the top priority + if ( me->GetVisionInterface()->IsLineOfSightClear( pPlayer->WorldSpaceCenter() ) ) + { + if ( !bConsideringOnlyLoSEnemies ) + { + bConsideringOnlyLoSEnemies = true; + pBestCallout = pPlayer; + bBestCalloutIsNew = bIsNew; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + else + { + if ( flDistToMe < flBestCalloutDist ) + { + pBestCallout = pPlayer; + bBestCalloutIsNew = bIsNew; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + } + continue; + } + + if ( bConsideringOnlyLoSEnemies ) + { + // we don't need to consider other criteria anymore if we are only considering LoS enemies + continue; + } + + if ( !pBestCallout ) + { + pBestCallout = pPlayer; + bBestCalloutIsNew = bIsNew; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + continue; + } + + // Consider an enemy we haven't called out yet + if ( bIsNew && !bBestCalloutIsNew ) + { + pBestCallout = pPlayer; + bBestCalloutIsNew = true; + flBestCalloutDist = flDistToMe; + flBestCalloutMoved = 0; + continue; + } + + if ( !bIsNew && bBestCalloutIsNew ) + { + // Existing candidate is new, current is old -> ignore current + continue; + } + + // Tie breakers between candidates + if ( bIsNew ) + { + // Both New: Tie breaker is distance to me (closest first) + if ( flDistToMe < flBestCalloutDist ) + { + pBestCallout = pPlayer; + flBestCalloutDist = flDistToMe; + } + } + else + { + // Both Old: Prioritize one that has moved more since last callout + float diff = flMoved - flBestCalloutMoved; + + // Check absolute difference to see if it is significant or not + if ( FloatMakePositive( diff ) < 100.0f ) + { + // Roughly same movement, tie breaker is closest distance + if ( flDistToMe < flBestCalloutDist ) + { + pBestCallout = pPlayer; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + } + else if ( diff > 0.0f ) + { + // Distinctly moved more + pBestCallout = pPlayer; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + } + } + + if ( pBestCallout ) + { + // NEO Jank: Ideally we could detect the enemy in the middle of the bot's screen, but they tend to look erratically + // It's easier to just set an enemy handle to appromimate a human focusing on calling out one enemy + m_hCurrentFocusEnemy = pBestCallout; + + // Update cache for this enemy so we know how much they moved next time + int idx = pBestCallout->entindex(); + if ( IsIndexIntoPlayerArrayValid( idx ) ) + { + m_enemyLastPos[ idx ] = pBestCallout->GetAbsOrigin(); + } + } +} + + + +//--------------------------------------------------------------------------------------------- +// CNEOBotCtgCarrier Implementation +//--------------------------------------------------------------------------------------------- + +CNEOBotCtgCarrier::CNEOBotCtgCarrier( void ) +{ + m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgCarrier::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_chasePath.Invalidate(); + m_aloneTimer.Invalidate(); + m_repathTimer.Invalidate(); + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + + m_closestCapturePoint = GetNearestCapPoint( me ); + + UpdateFollowPath( me, m_teammates ); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgCarrier::Update( CNEOBot *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done( "I am dead" ); + } + + if ( !me->IsCarryingGhost() ) + { + return Done( "No longer carrying the ghost" ); + } + + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + + if ( m_teammates.Count() == 0 ) + { + return SuspendFor( new CNEOBotCtgLoneWolf, "I'm the last one!" ); + } + + UpdateFollowPath( me, m_teammates ); + + m_ghostEquipmentHandler.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +Vector CNEOBotCtgCarrier::GetNearestCapPoint( const CNEOBot *me ) const +{ + if ( !me ) + return CNEO_Player::VECTOR_INVALID_WAYPOINT; + + const int iMyTeam = me->GetTeamNumber(); + + if ( NEORules()->m_pGhostCaps.Count() > 0 ) + { + Vector bestPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + float flNearestCapDistSq = FLT_MAX; + for( int i=0; im_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if (!pCapPoint) + { + continue; + } + + int iCapTeam = pCapPoint->owningTeamAlternate(); + if ( iCapTeam == iMyTeam ) + { + float distanceToCap = me->GetAbsOrigin().DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( distanceToCap < flNearestCapDistSq ) + { + flNearestCapDistSq = distanceToCap; + bestPos = pCapPoint->GetAbsOrigin(); + } + } + } + return bestPos; + } + + return CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgCarrier::UpdateFollowPath( CNEOBot *me, const CUtlVector &teammates ) +{ + // Strategy: + // 1. Identify valid goal (Capture Point). + // 2. Scan all teammates: + // - Track "Nearest Spatial Teammate" (fallback if no LOS). + // - Track "Teammate Closest to Goal" (primary target if LOS exists). + // 3. Decision: + // - If I see any teammate -> Chase the one closest to the goal. + // - If I see NO teammates -> Chase the one spatially closest to me (regroup). + // - If no teammates exist -> Fallback to Goal (though Update() should catch this as a transition to LoneWolf). + + // Identify the goal first + Vector vecGoalPos = m_closestCapturePoint; + bool bFoundGoal = ( vecGoalPos != CNEO_Player::VECTOR_INVALID_WAYPOINT ); + + if ( vecGoalPos == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + bFoundGoal = false; + } + + CNEO_Player *pTeammateNearestToMe = nullptr; + float flTeammateNearestToMeDistSq = FLT_MAX; + + CNEO_Player *pBestGoalTeammate = nullptr; + float flBestGoalTeammateDistSq = FLT_MAX; + + for ( int i = 0; i < teammates.Count(); i++ ) + { + CNEO_Player *pPlayer = teammates[i]; + + // Find the teammate nearest to me + float distanceToMe = pPlayer->GetAbsOrigin().DistToSqr( me->GetAbsOrigin() ); + if ( distanceToMe < flTeammateNearestToMeDistSq ) + { + flTeammateNearestToMeDistSq = distanceToMe; + pTeammateNearestToMe = pPlayer; + } + + // Find the teammate closest to the goal + if ( bFoundGoal ) + { + float distanceToGoal = pPlayer->GetAbsOrigin().DistToSqr( vecGoalPos ); + if ( distanceToGoal < flBestGoalTeammateDistSq ) + { + flBestGoalTeammateDistSq = distanceToGoal; + pBestGoalTeammate = pPlayer; + } + } + } + + if ( !bFoundGoal ) + { + // If we have no goal (defending team on asymmetric map), we should just stay put and call out targets. + m_chasePath.Invalidate(); + m_path.Invalidate(); + return; + } + + // If we are safe to cap (closer than enemies), just go for it! + if ( bFoundGoal ) + { + // We need to know where enemies are to determine if we are safe to cap + CWeaponGhost *pGhost = dynamic_cast( me->Weapon_GetSlot( 0 ) ); + if ( pGhost && pGhost->IsGhost() && pGhost->IsBootupCompleted() ) + { + float flDistMeToGoalSq = me->GetAbsOrigin().DistToSqr( vecGoalPos ); + + bool bAnyEnemyCloser = false; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + { + float dSq = pPlayer->GetAbsOrigin().DistToSqr( vecGoalPos ); + if ( dSq <= flDistMeToGoalSq ) + { + bAnyEnemyCloser = true; + break; + } + } + } + + if ( !bAnyEnemyCloser ) + { + m_chasePath.Invalidate(); + m_path.Invalidate(); + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, vecGoalPos, FASTEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + } + else + { + m_path.Update( me ); + } + return; + } + } + } + + // Choose which teammate to follow + CNEO_Player *pTargetTeammate = nullptr; + + // Check if we are relatively protected by teammates before running after the vanguard + bool bIsNearEscort = false; + if ( pTeammateNearestToMe ) + { + if ( me->GetVisionInterface()->IsLineOfSightClear( pTeammateNearestToMe->WorldSpaceCenter() ) ) + { + bIsNearEscort = true; + } + } + + if ( bIsNearEscort ) + { + m_aloneTimer.Invalidate(); + + // If we are near a teammate, advance towards goal with vanguard + if ( pBestGoalTeammate ) + { + pTargetTeammate = pBestGoalTeammate; + } + else + { + // Fallback to nearest if no goal teammate + // (unlikely if we are near a teammate, but in case there's a logic bug) + pTargetTeammate = pTeammateNearestToMe; + } + } + else + { + // We are isolated from our teammates + if ( !m_aloneTimer.HasStarted() ) + { + m_aloneTimer.Start( 3.0f ); + } + + if ( !m_aloneTimer.IsElapsed() ) + { + // grace period: keep running towards the vanguard in case we can catch up with them + // helps keep following vanguard in case they round a corner or some other visual obstacle + if ( pBestGoalTeammate ) + { + pTargetTeammate = pBestGoalTeammate; + } + else + { + pTargetTeammate = pTeammateNearestToMe; + } + } + else + { + // regroup with nearest friendlies if alone for too long + pTargetTeammate = pTeammateNearestToMe; + } + } + + if ( pTargetTeammate ) + { + float flDistToTeammate = me->GetAbsOrigin().DistTo( pTargetTeammate->GetAbsOrigin() ); + if ( flDistToTeammate < 100.0f ) + { + m_chasePath.Invalidate(); + return; + } + + m_path.Invalidate(); + + // chase after the chosen teammate + CNEOBotPathUpdateChase( me, m_chasePath, pTargetTeammate, SAFEST_ROUTE ); + return; + } + + // No teammates alive? + // Update() should handle the LastStand transition next tick, but just in case we fall through here: + m_chasePath.Invalidate(); + + if ( bFoundGoal ) + { + if ( !m_path.IsValid() || !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, vecGoalPos, SAFEST_ROUTE ); + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + } + m_path.Update( me ); + } + else + { + // Usually defending the ghost with no capture point + m_path.Invalidate(); + } +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgCarrier::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + + // Re-evaluate nearest cap point on resume (in case we moved significantly while interrupted) + m_closestCapturePoint = GetNearestCapPoint( me ); + + m_repathTimer.Invalidate(); + UpdateFollowPath( me, m_teammates ); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgCarrier::OnStuck( CNEOBot *me ) +{ + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + UpdateFollowPath( me, m_teammates ); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgCarrier::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgCarrier::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h new file mode 100644 index 0000000000..d857c3aa2c --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h @@ -0,0 +1,59 @@ +#ifndef NEO_BOT_CTG_CARRIER_H +#define NEO_BOT_CTG_CARRIER_H + +#include "bot/neo_bot.h" +#include "Path/NextBotChasePath.h" + +class CNEO_Player; + +//-------------------------------------------------------------------------------------------------------- +// Common ghost equipment usage routines. Also used by CNEOBotCommandFollow. +class CNEOBotGhostEquipmentHandler +{ +public: + CNEOBotGhostEquipmentHandler(); + void Update( CNEOBot *me ); + +private: + void EquipBestWeaponForGhoster( CNEOBot *me ); + float GetUpdateInterval( CNEOBot *me ) const; + void UpdateGhostCarrierCallout( CNEOBot *me, const CUtlVector &enemies ); + + EHANDLE m_hCurrentFocusEnemy{nullptr}; + CountdownTimer m_enemyUpdateTimer; + Vector m_enemyLastPos[MAX_PLAYERS_ARRAY_SAFE]; + CUtlVector m_enemies; +}; + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgCarrier : public Action< CNEOBot > +{ +public: + CNEOBotCtgCarrier( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "ctgGhostCarrier"; } + +private: + PathFollower m_path; + ChasePath m_chasePath; + CountdownTimer m_aloneTimer; + CountdownTimer m_repathTimer; + + CNEOBotGhostEquipmentHandler m_ghostEquipmentHandler; + + Vector m_closestCapturePoint; + CUtlVector m_teammates; + + Vector GetNearestCapPoint( const CNEOBot *me ) const; + void UpdateFollowPath( CNEOBot *me, const CUtlVector &teammates ); +}; + +#endif // NEO_BOT_CTG_CARRIER_H \ No newline at end of file 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 new file mode 100644 index 0000000000..c103251b5a --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp @@ -0,0 +1,84 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_enemy.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" + + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEnemy::CNEOBotCtgEnemy( void ) +{ +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemy::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_repathTimer.Invalidate(); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemy::Update( CNEOBot *me, float interval ) +{ + if ( !NEORules()->GhostExists() ) + { + return Done( "Ghost does not exist" ); + } + + if ( NEORules()->GetGhosterPlayer() <= 0 ) + { + return Done( "No ghost carrier" ); + } + + CNEO_Player* pGhostCarrier = ToNEOPlayer( UTIL_PlayerByIndex( NEORules()->GetGhosterPlayer() ) ); + if ( !pGhostCarrier || pGhostCarrier->GetTeamNumber() == me->GetTeamNumber() ) + { + return Done( "Ghost carrier is friendly" ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && !threat->IsObsolete() && me->GetIntentionInterface()->ShouldAttack( me, threat ) ) + { + return SuspendFor( new CNEOBotAttack, "Attacking ghoster team" ); + } + + // Investigate the ghost carrier's position + if ( m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, pGhostCarrier->GetAbsOrigin(), DEFAULT_ROUTE ); + m_repathTimer.Start( RandomFloat( 0.2f, 1.0f ) ); + } + m_path.Update( me ); + + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemy::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEnemy::OnStuck( CNEOBot *me ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEnemy::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEnemy::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} 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 new file mode 100644 index 0000000000..c6a1ff77a0 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h @@ -0,0 +1,27 @@ +#ifndef NEO_BOT_CTG_ENEMY_H +#define NEO_BOT_CTG_ENEMY_H + +#include "bot/neo_bot.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgEnemy : public Action< CNEOBot > +{ +public: + CNEOBotCtgEnemy( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "ctgEnemy"; } + +private: + PathFollower m_path; + CountdownTimer m_repathTimer; +}; + +#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 new file mode 100644 index 0000000000..662141c0e8 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp @@ -0,0 +1,349 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_escort.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" +#include "neo_ghost_cap_point.h" +#include "weapons/weapon_ghost.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEscort::CNEOBotCtgEscort( void ) : + m_role( ROLE_SCREEN ), + m_bHasGoal( false ) +{ + m_vecGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_repathTimer.Invalidate(); + m_lostSightOfCarrierTimer.Invalidate(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEscort::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_chasePath.Invalidate(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + m_lostSightOfCarrierTimer.Invalidate(); + + m_role = ROLE_SCREEN; + m_vecGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_bHasGoal = false; + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) +{ + if ( !NEORules()->GhostExists() ) + { + return Done( "Ghost does not exist" ); + } + + if ( NEORules()->GetGhosterPlayer() <= 0 ) + { + return Done( "Ghost is not held by a player" ); + } + + CNEO_Player* pGhostCarrier = ToNEOPlayer( UTIL_PlayerByIndex( NEORules()->GetGhosterPlayer() ) ); + if ( !pGhostCarrier || pGhostCarrier->GetTeamNumber() != me->GetTeamNumber() || pGhostCarrier == me ) + { + return Done( "Ghost carrier is not a teammate anymore" ); + } + + // Check if we can assist the Ghost Carrier (if they are a bot) + CNEOBot *pBotGhostCarrier = ToNEOBot( pGhostCarrier ); + if ( pBotGhostCarrier ) + { + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( carrierThreat ) + { + // Check if the threat has a clear shot at our friend + if ( me->IsLineOfFireClear( carrierThreat->GetLastKnownPosition(), pGhostCarrier, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + return SuspendFor( new CNEOBotAttack, "Assisting Ghost Carrier with their threat" ); + } + } + } + + if ( m_repathTimer.IsElapsed() ) + { + UpdateGoalPosition( me, pGhostCarrier ); + + if ( m_bHasGoal ) + { + m_role = UpdateRoleAssignment( me, pGhostCarrier, m_vecGoalPos ); + } + else + { + m_role = ROLE_SCREEN; + } + } + + bool bCanSeeCarrier = me->GetVisionInterface()->IsLineOfSightClear( pGhostCarrier->WorldSpaceCenter() ); + if ( bCanSeeCarrier ) + { + m_lostSightOfCarrierTimer.Invalidate(); + + if ( !m_bHasGoal ) + { + // Asymmetric defense: No goal cap zone, so defend the carrier. + // Look away from the carrier to cover their blind spots + Vector vecFromCarrier = me->GetAbsOrigin() - pGhostCarrier->GetAbsOrigin(); + vecFromCarrier.z = 0.0f; // Bias towards horizontal scanning + if ( VectorNormalize( vecFromCarrier ) > 0.1f ) + { + // Look at a point far away in the opposite direction of the carrier + Vector vecLookTarget = me->EyePosition() + ( vecFromCarrier * 500.0f ); + me->GetBodyInterface()->AimHeadTowards( vecLookTarget, IBody::IMPORTANT, 0.2f, nullptr, "Escort: Scanning away from carrier" ); + } + } + } + else + { + // If we can't see them, check if we are in grace period + if ( !m_lostSightOfCarrierTimer.HasStarted() ) + { + // JUST lost sight + m_lostSightOfCarrierTimer.Start( 3.0f ); + // Treat as visible for now + bCanSeeCarrier = true; + } + else if ( !m_lostSightOfCarrierTimer.IsElapsed() ) + { + // Still in grace period + bCanSeeCarrier = true; + } + else + { + // Timer elapsed, truly lost sight + bCanSeeCarrier = false; + } + } + + // Check again based on grace period checks above + if ( !bCanSeeCarrier ) + { + m_path.Invalidate(); + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); + } + else + { + if ( m_role == ROLE_BODYGUARD ) + { + // Dont crowd the carrier + if ( me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ) < ( 100.0f * 100.0f ) ) + { + m_path.Invalidate(); + m_chasePath.Invalidate(); + return Continue(); + } + + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); + return Continue(); + } + + if ( !m_bHasGoal ) + { + // Asymmetric defense: No goal cap zone, so defend the carrier. + if ( pBotGhostCarrier ) + { + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( carrierThreat && carrierThreat->GetEntity() && carrierThreat->GetEntity()->IsAlive() ) + { + me->GetVisionInterface()->AddKnownEntity( carrierThreat->GetEntity() ); + return SuspendFor( new CNEOBotAttack, "Attacking enemy during asymmetric defense" ); + } + } + + // No active threats to carrier + float flDistToCarrierSq = me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ); + constexpr float regroupDistanceSq = 300.0f * 300.0f; + if ( flDistToCarrierSq > regroupDistanceSq ) + { + // Regroup + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); + } + else + { + // Hold position + m_chasePath.Invalidate(); + m_path.Invalidate(); + } + return Continue(); + } + + m_chasePath.Invalidate(); + + Vector vecMoveTarget = m_vecGoalPos; + + + if ( m_role == ROLE_SCREEN && pBotGhostCarrier ) + { + CWeaponGhost *pGhost = dynamic_cast( pBotGhostCarrier->Weapon_GetSlot( 0 ) ); + // Only screen the threat if the ghost is actually booted up and ready to reveal enemies + // Otherwise just stick to the goal path + if ( pGhost && pGhost->IsBootupCompleted() ) + { + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( carrierThreat && carrierThreat->GetEntity() && carrierThreat->GetEntity()->IsAlive() ) + { + vecMoveTarget = carrierThreat->GetLastKnownPosition(); + + } + } + } + + // Move to Target (Goal or Threat) + // Throttling repath to avoid excessive computations + if ( m_repathTimer.IsElapsed() ) + { + m_chasePath.Invalidate(); + m_path.Invalidate(); + + CNEOBotPathCompute( me, m_path, vecMoveTarget, SAFEST_ROUTE ); + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + m_path.Update( me ); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEscort::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + m_chasePath.Invalidate(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEscort::OnStuck( CNEOBot *me ) +{ + m_chasePath.Invalidate(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEscort::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEscort::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + m_repathTimer.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEscort::EscortRole CNEOBotCtgEscort::UpdateRoleAssignment( CNEOBot *me, CNEO_Player *pGhostCarrier, const Vector &vecGoalPos ) +{ + // Roles: + // 1. Scout/Vanguard: Closest to Goal. + // 2. Bodyguard: Closest to Carrier. + // 3. Wall/Screen: Everyone else. + + CNEO_Player* pScout = nullptr; + CNEO_Player* pBodyguard = nullptr; + const int iMyTeam = me->GetTeamNumber(); + + // Collect candidates and process roles in one pass + float flBestDistToGoal = FLT_MAX; + float flBestDistToCarrier = FLT_MAX; + float flSecondBestDistToCarrier = FLT_MAX; + CNEO_Player* pBestToCarrier = nullptr; + CNEO_Player* pSecondBestToCarrier = nullptr; + + Vector vecCarrierOrigin = pGhostCarrier->GetAbsOrigin(); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer || !pPlayer->IsAlive() || pPlayer->GetTeamNumber() != iMyTeam || pPlayer == pGhostCarrier ) + { + continue; + } + + Vector vecPlayerOrigin = pPlayer->GetAbsOrigin(); + + // Check for Scout (Best dist to goal) + float goalDist = vecPlayerOrigin.DistToSqr( vecGoalPos ); + if ( goalDist < flBestDistToGoal ) + { + flBestDistToGoal = goalDist; + pScout = pPlayer; + } + + // Check for Bodyguard candidates (Dist to carrier) + float carrierDist = vecPlayerOrigin.DistToSqr( vecCarrierOrigin ); + if ( carrierDist < flBestDistToCarrier ) + { + flSecondBestDistToCarrier = flBestDistToCarrier; + pSecondBestToCarrier = pBestToCarrier; + + flBestDistToCarrier = carrierDist; + pBestToCarrier = pPlayer; + } + else if ( carrierDist < flSecondBestDistToCarrier ) + { + flSecondBestDistToCarrier = carrierDist; + pSecondBestToCarrier = pPlayer; + } + } + + // If the best bodyguard candidate is the scout, take the second best. + if ( pBestToCarrier && pBestToCarrier == pScout ) + { + pBodyguard = pSecondBestToCarrier; + } + else + { + pBodyguard = pBestToCarrier; + } + + if ( me == pScout ) + { + return ROLE_SCOUT; + } + else if ( me == pBodyguard ) + { + return ROLE_BODYGUARD; + } + else + { + return ROLE_SCREEN; + } +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgEscort::UpdateGoalPosition( CNEOBot *me, CNEO_Player *pGhostCarrier ) +{ + m_vecGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_bHasGoal = false; + + float flNearestCapDistSq = FLT_MAX; + const int iMyTeam = me->GetTeamNumber(); + + for( int i=0; im_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if ( !pCapPoint ) continue; + if ( pCapPoint->owningTeamAlternate() == iMyTeam ) + { + float d = pGhostCarrier->GetAbsOrigin().DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( d < flNearestCapDistSq ) + { + flNearestCapDistSq = d; + m_vecGoalPos = pCapPoint->GetAbsOrigin(); + m_bHasGoal = true; + } + } + } +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h new file mode 100644 index 0000000000..d8e932c5cf --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h @@ -0,0 +1,44 @@ +#ifndef NEO_BOT_CTG_ESCORT_H +#define NEO_BOT_CTG_ESCORT_H + +#include "bot/neo_bot.h" +#include "Path/NextBotChasePath.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgEscort : public Action< CNEOBot > +{ +public: + CNEOBotCtgEscort( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "ctgEscort"; } + +private: + PathFollower m_path; + ChasePath m_chasePath; + CountdownTimer m_repathTimer; + CountdownTimer m_lostSightOfCarrierTimer; + + enum EscortRole + { + ROLE_SCREEN, + ROLE_SCOUT, + ROLE_BODYGUARD, + }; + EscortRole m_role; + + EscortRole UpdateRoleAssignment( CNEOBot *me, CNEO_Player *pGhostCarrier, const Vector &vecGoalPos ); + void UpdateGoalPosition( CNEOBot *me, CNEO_Player *pGhostCarrier ); + + Vector m_vecGoalPos; + bool m_bHasGoal; +}; + +#endif // NEO_BOT_CTG_ESCORT_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp new file mode 100644 index 0000000000..03f4658bb5 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp @@ -0,0 +1,501 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/behavior/neo_bot_ctg_capture.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf.h" +#include "bot/behavior/neo_bot_seek_weapon.h" +#include "bot/behavior/neo_bot_retreat_to_cover.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" +#include "neo_ghost_cap_point.h" +#include "weapon_ghost.h" + + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgLoneWolf::CNEOBotCtgLoneWolf( void ) +{ + m_hGhost = nullptr; + m_bPursuingDropThreat = false; + m_bHasRetreatedFromGhost = false; + m_vecDropThreatPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_hGhost = nullptr; + m_bHasRetreatedFromGhost = false; + m_bPursuingDropThreat = false; + m_useAttemptTimer.Invalidate(); + m_lookAroundTimer.Invalidate(); + m_repathTimer.Invalidate(); + m_stalemateTimer.Invalidate(); + m_capPointUpdateTimer.Invalidate(); + m_vecDropThreatPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_hPursueTarget = nullptr; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + + CBaseCombatWeapon *pWeapon = me->GetActiveWeapon(); + if ( !threat && pWeapon && pWeapon->UsesClipsForAmmo1() ) + { + if ( pWeapon->Clip1() < pWeapon->GetMaxClip1() && me->GetAmmoCount( pWeapon->GetPrimaryAmmoType() ) > 0 ) + { + // Aggressively reload due to lack of backup + me->PressReloadButton(); + } + } + + // We dropped the ghost to hunt a threat. + if ( m_bPursuingDropThreat ) + { + // First, ensure we have a weapon. + if ( !me->Weapon_GetSlot( 0 ) ) + { + return SuspendFor( new CNEOBotSeekWeapon(), "Scavenging for weapon to hunt threat" ); + } + + // We have a weapon. Investigate the last known location. + float flDistSq = me->GetAbsOrigin().DistToSqr( m_vecDropThreatPos ); + if ( flDistSq < Square( 100.0f ) || m_vecDropThreatPos == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + // We arrived at threat's last known position, but didn't find them. + m_bPursuingDropThreat = false; + } + else + { + // Move to investigate + if ( threat && threat->GetEntity() && me->GetVisionInterface()->IsAbleToSee( threat->GetEntity(), CNEOBotVision::DISREGARD_FOV, nullptr ) ) + { + return SuspendFor( new CNEOBotAttack, "Found the threat I was hunting!" ); + } + + CNEOBotPathCompute( me, m_path, m_vecDropThreatPos, FASTEST_ROUTE ); + m_path.Update( me ); + return Continue(); + } + } + + // Always need to find the ghost to act on it + if (!m_hGhost) + { + m_hGhost = dynamic_cast( gEntList.FindEntityByClassname(nullptr, "weapon_ghost") ); + } + + if (!m_hGhost) + { + return Done( "Ghost not found" ); + } + + // Occasionally reconsider which cap zone is our goal + if ( !m_capPointUpdateTimer.HasStarted() || m_capPointUpdateTimer.IsElapsed() ) + { + m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; + float flNearestCapDist = FLT_MAX; + + if ( NEORules()->m_pGhostCaps.Count() > 0 ) + { + Vector vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); + + for( int i=0; im_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if ( !pCapPoint ) continue; + + if ( pCapPoint->owningTeamAlternate() == me->GetTeamNumber() ) + { + float d = vecStart.DistTo( pCapPoint->GetAbsOrigin() ); + if ( d < flNearestCapDist ) + { + flNearestCapDist = d; + m_closestCapturePoint = pCapPoint->GetAbsOrigin(); + } + } + } + } + m_capPointUpdateTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + } + + float flDistGhostToGoal = FLT_MAX; + if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + Vector vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); + flDistGhostToGoal = vecStart.DistTo( m_closestCapturePoint ); + } + + // Safe to cap: We are closer to the goal than the nearest enemy is to the goal. + // NEO Jank Cheat: We're intentionally cheating here compared to the neo_bot_ctg_carrier behavior by not checking if the ghost is booted. + // The reason is that we want to avoid spectators getting frustrated with bots choosing to ambush at the ghost instead of capping it, + // when it's apparent that the enemy is too far behind to catch up (and ambushing would give them the opportunity to do so). + // Our bots so far have poor intuition about where unseen enemies could come from, + // so it's easier to cheat with distance checks than to anticipate where enemies are. + float flMyTotalDist = flDistGhostToGoal; + if ( !me->IsCarryingGhost() ) + { + flMyTotalDist += me->GetAbsOrigin().DistTo( m_hGhost->GetAbsOrigin() ); + } + + // Count enemies and find if one is closer to our goal + int iEnemyTeamCount = 0; + float flClosestEnemyDistToGoal = FLT_MAX; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + { + iEnemyTeamCount++; + if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + float d = pPlayer->GetAbsOrigin().DistTo( m_closestCapturePoint ); + if ( d < flClosestEnemyDistToGoal ) + { + flClosestEnemyDistToGoal = d; + if ( iEnemyTeamCount > 1 && flClosestEnemyDistToGoal < flMyTotalDist ) + { + // We already know it's not a 1v1 (count > 1) + // And we know it's not safe to cap (enemy closer than us) + // So we can stop checking. + break; + } + } + } + } + } + + // Tie breaker: If it's a 1v1, it's boring for human observers to wait forever + // Just try to grab the ghost, even if it might not be the best tactic + bool bIs1v1 = (iEnemyTeamCount == 1); + + bool bSafeToCap = ((m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT) && (flMyTotalDist < flClosestEnemyDistToGoal)); + + CWeaponGhost *pGhostWeapon = m_hGhost.Get(); + CBaseCombatCharacter *pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; + bool bGhostHeldByEnemy = (pGhostOwner && pGhostOwner->GetTeamNumber() != me->GetTeamNumber()); + + // Consider next action + if ( me->IsCarryingGhost() ) + { + if ( bSafeToCap ) + { + if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, m_closestCapturePoint, SAFEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + } + else + { + m_path.Update( me ); + } + } + return Continue(); + } + else + { + // Enemy is closer to goal (blocking us) or gaining on us. + + // If we see a weapon nearby, drop the ghost and take it + CBaseEntity *pNearbyWeapon = FindNearestPrimaryWeapon( me->GetAbsOrigin(), true ); + if ( pNearbyWeapon ) + { + CBaseCombatWeapon *pGhostWep = me->Weapon_GetSlot( 0 ); + if ( pGhostWep ) + { + if ( me->GetActiveWeapon() != pGhostWep ) + { + me->Weapon_Switch( pGhostWep ); + return Continue(); + } + + me->PressDropButton( 0.1f ); + return ChangeTo( new CNEOBotSeekWeapon(), "Dropping ghost to scavenge nearby weapon" ); + } + } + + CBaseCombatWeapon *pActiveWeapon = me->GetActiveWeapon(); + CBaseCombatWeapon *pGhostWep = me->Weapon_GetSlot( 0 ); + + // If we know where the threat is, drop and hunt. + if ( threat && threat->GetLastKnownPosition() != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + m_vecDropThreatPos = threat->GetLastKnownPosition(); + m_bPursuingDropThreat = true; + m_hPursueTarget = threat->GetEntity(); + + if ( pGhostWep ) + { + if ( pActiveWeapon != pGhostWep ) + { + me->Weapon_Switch( pGhostWep ); + } + else + { + me->EnableCloak( 3.0f ); + me->PressDropButton( 0.1f ); + } + } + return Continue(); + } + + // Else continue moving ghost towards goal + if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, m_closestCapturePoint, SAFEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + } + else + { + m_path.Update( me ); + } + } + return Continue(); + } + } + else if ( bGhostHeldByEnemy ) + { + // intercept enemy carrier + if ( threat && threat->GetEntity() == pGhostOwner && me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT )) + { + me->EnableCloak( 3.0f ); + return SuspendFor(new CNEOBotAttack, "Attacking the ghost carrier!"); + } + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute(me, m_path, m_hGhost->GetAbsOrigin(), FASTEST_ROUTE); + m_path.Update(me); + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + } + else + { + m_path.Update(me); + } + return Continue(); + } + else + { + // Ghost is free for taking + if ( bSafeToCap || (bIs1v1 && m_stalemateTimer.HasStarted() && m_stalemateTimer.IsElapsed()) ) + { + // Try to cap before enemy can stop us. + float flDistToGhostSq = me->GetAbsOrigin().DistToSqr(m_hGhost->GetAbsOrigin()); + if ( flDistToGhostSq < 100.0f * 100.0f ) + { + return SuspendFor(new CNEOBotCtgCapture(m_hGhost.Get()), "Picking up ghost to make a run for it!"); + } + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute(me, m_path, m_hGhost->GetAbsOrigin(), FASTEST_ROUTE); + m_path.Update(me); + m_repathTimer.Start( RandomFloat( 0.2f, 0.5f ) ); + } + else + { + m_path.Update(me); + } + return Continue(); + } + else + { + // Not safe. Enemy is closer to goal or blocking. + // Try to ambush them + + if ( bIs1v1 && !m_stalemateTimer.HasStarted() ) + { + m_stalemateTimer.Start( RandomFloat( 10.0f, 20.0f ) ); + } + + if ( m_bHasRetreatedFromGhost ) + { + // Waiting in ambush/cover + if (threat && me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT )) + { + me->EnableCloak( 3.0f ); + return SuspendFor(new CNEOBotAttack, "Ambushing enemy near ghost!"); + } + return UpdateLookAround( me, m_hGhost->GetAbsOrigin() ); + } + else + { + // Hide out of sight of ghost to ambush anyone that picks up the ghost + float flDistToGhostSq = me->GetAbsOrigin().DistToSqr(m_hGhost->GetAbsOrigin()); + if (flDistToGhostSq < 300.0f * 300.0f) + { + m_bHasRetreatedFromGhost = true; + return SuspendFor(new CNEOBotRetreatToCover(), "Finding a hiding spot near the ghost"); + } + else + { + // Get near the ghost first before surveying hiding spots + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute(me, m_path, m_hGhost->GetAbsOrigin(), FASTEST_ROUTE); + m_path.Update(me); + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + } + else + { + m_path.Update(me); + } + return Continue(); + } + } + } + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnSuspend( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + m_path.Invalidate(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + if ( m_bPursuingDropThreat && m_hPursueTarget.Get() ) + { + if ( !m_hPursueTarget->IsAlive() ) + { + // Target dead, stop pursuit + m_bPursuingDropThreat = false; + m_hPursueTarget = nullptr; + } + else + { + // Remember where we last saw the threat + const CKnownEntity *known = me->GetVisionInterface()->GetKnown( m_hPursueTarget ); + if ( known ) + { + m_vecDropThreatPos = known->GetLastKnownPosition(); + } + } + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnStuck( CNEOBot *me ) +{ + m_path.Invalidate(); + me->GetLocomotionInterface()->Jump(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + return TryContinue(); +} + + +// Helper for "UpdateLookAround" - inspired from how CNavArea CollectPotentiallyVisibleAreas works +class CCollectPotentiallyVisibleAreas +{ +public: + CCollectPotentiallyVisibleAreas( CUtlVector< CNavArea * > *collection ) + { + m_collection = collection; + } + + bool operator() ( CNavArea *baseArea ) + { + m_collection->AddToTail( baseArea ); + return true; + } + + CUtlVector< CNavArea * > *m_collection; +}; + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::UpdateLookAround( CNEOBot *me, const Vector &anchorPos ) +{ + if ( !m_lookAroundTimer.HasStarted() || m_lookAroundTimer.IsElapsed() ) + { + // NEO Jank Cheat: Bots don't have a good intuition for where to look for threats + // So the compromise is to have them retreat from a threat when the latter shows up + // The looking around logic below is performative for spectators to justify why a bot might incidentally turn to see threat + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + { + if ( me->IsLineOfFireClear( pPlayer->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + me->GetVisionInterface()->AddKnownEntity( pPlayer ); + me->GetBodyInterface()->AimHeadTowards( pPlayer->WorldSpaceCenter(), IBody::CRITICAL, 0.2f, nullptr, "Ambush Cheat: Reacting to enemy in LOF" ); + return SuspendFor( new CNEOBotRetreatToCover(), "Ambush Prep: Retreating from sensed enemy" ); + } + } + } + + m_lookAroundTimer.Start( 0.2f ); + + // Logic inspired from neo_bot.cpp UpdateLookingAroundForIncomingPlayers + // Update our view to watch where enemies might be coming from + CNavArea *myArea = me->GetLastKnownArea(); + if ( myArea ) + { + m_visibleAreas.RemoveAll(); + CCollectPotentiallyVisibleAreas collect( &m_visibleAreas ); + myArea->ForAllPotentiallyVisibleAreas( collect ); + + if ( m_visibleAreas.Count() > 0 ) + { + // Pick a random area + int which = RandomInt( 0, m_visibleAreas.Count()-1 ); + CNavArea *area = m_visibleAreas[ which ]; + + // Look at a spot in it + int retryCount = 5; + for( int i=0; iGetRandomPoint() + Vector( 0, 0, HumanEyeHeight * 0.75f ); + + // Ensure we can see it + if ( me->GetVisionInterface()->IsLineOfSightClear( spot ) ) + { + me->GetBodyInterface()->AimHeadTowards( spot, IBody::IMPORTANT, 1.0f, nullptr, "Ambush: Scanning area" ); + + const float maxLookInterval = 2.0f; + m_lookAroundTimer.Start(RandomFloat(0.5f, maxLookInterval)); + return Continue(); + } + } + } + } + + // Fallback scanning delay if we failed to find a spot + m_lookAroundTimer.Start(RandomFloat(0.3f, 1.0f)); + } + + return Continue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h new file mode 100644 index 0000000000..ef9bf545c3 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h @@ -0,0 +1,44 @@ +#ifndef NEO_BOT_CTG_LONE_WOLF_H +#define NEO_BOT_CTG_LONE_WOLF_H + +#include "bot/neo_bot.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgLoneWolf : public Action< CNEOBot > +{ +public: + CNEOBotCtgLoneWolf( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnSuspend( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "ctgLoneWolf"; } + +private: + PathFollower m_path; + CHandle m_hGhost; + CountdownTimer m_repathTimer; + CountdownTimer m_useAttemptTimer; + bool m_bHasRetreatedFromGhost; + + Vector m_vecDropThreatPos; + CHandle m_hPursueTarget; + bool m_bPursuingDropThreat; + + ActionResult< CNEOBot > UpdateLookAround( CNEOBot *me, const Vector &anchorPos ); + CountdownTimer m_lookAroundTimer; + CountdownTimer m_stalemateTimer; + + CountdownTimer m_capPointUpdateTimer; + Vector m_closestCapturePoint; + + CUtlVector< CNavArea * > m_visibleAreas; +}; + +#endif // NEO_BOT_CTG_LAST_STAND_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp new file mode 100644 index 0000000000..00d25a4248 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp @@ -0,0 +1,144 @@ +#include "cbase.h" +#include "neo_player.h" +#include "neo_gamerules.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_seek.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf.h" +#include "bot/behavior/neo_bot_ctg_escort.h" +#include "bot/behavior/neo_bot_ctg_enemy.h" +#include "bot/behavior/neo_bot_ctg_carrier.h" +#include "bot/behavior/neo_bot_ctg_capture.h" +#include "bot/neo_bot_path_compute.h" +#include "weapon_ghost.h" + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgSeek::Update( CNEOBot *me, float interval ) +{ + if (NEORules()->GetGameType() != NEO_GAME_TYPE_CTG) + { + return Done( "Game mode is no longer CTG" ); + } + + ActionResult< CNEOBot > result = UpdateCommon( me, interval ); + if ( result.IsRequestingChange() || result.IsDone() ) + { + return result; + } + + int team_members = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() == me->GetTeamNumber() ) + { + team_members++; + } + } + + if ( team_members == 1 ) + { + return SuspendFor( new CNEOBotCtgLoneWolf, "I'm the last one on my team!" ); + } + + if (NEORules()->GhostExists()) + { + int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + if (iGhosterPlayer > 0 && iGhosterPlayer <= gpGlobals->maxClients) + { + CNEO_Player* pGhostCarrier = ToNEOPlayer(UTIL_PlayerByIndex(iGhosterPlayer)); + if (pGhostCarrier && pGhostCarrier != me) + { + if (pGhostCarrier->GetTeamNumber() == me->GetTeamNumber()) + { + return SuspendFor(new CNEOBotCtgEscort, "Protecting the ghost carrier!"); + } + else + { + return SuspendFor(new CNEOBotCtgEnemy, "Stopping the ghost carrier!"); + } + } + + // If I have the ghost, switch to ghost behavior + if (me->IsCarryingGhost()) + { + return SuspendFor(new CNEOBotCtgCarrier, "I am the ghost carrier!"); + } + } + } + + // Ghost capture logic + if (m_bGoingToTargetEntity && m_hTargetEntity) + { + const float useRangeSq = 100.0f * 100.0f; + if ( me->GetAbsOrigin().DistToSqr( m_hTargetEntity->GetAbsOrigin() ) < useRangeSq ) + { + if ( !m_hTargetEntity->IsPlayer() ) + { + if ( me->IsLineOfSightClear( m_hTargetEntity, CBaseCombatCharacter::IGNORE_ACTORS ) ) + { + const char *classname = m_hTargetEntity->GetClassname(); + if ( FStrEq( classname, "weapon_ghost" ) ) + { + CBaseCombatWeapon* pWeapon = m_hTargetEntity->MyCombatWeaponPointer(); + if ( pWeapon && !pWeapon->GetOwner() ) + { + CWeaponGhost *pGhost = dynamic_cast( m_hTargetEntity.Get() ); + return SuspendFor( new CNEOBotCtgCapture( pGhost ), "Capturing Ghost" ); + } + } + } + else + { + return Done("Capture target was not a ghost"); + } + } + } + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgSeek::RecomputeSeekPath( CNEOBot *me ) +{ + if (NEORules()->GetGameType() != NEO_GAME_TYPE_CTG) + { + // Wait until next tick to exit behavior + return; + } + + m_hTargetEntity = nullptr; + m_bGoingToTargetEntity = false; + m_vGoalPos = vec3_origin; + + if (NEORules()->GhostExists()) + { + const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + + if (iGhosterPlayer > 0) + { + // Get ready to transition into CTG specific role next tick + m_path.Invalidate(); + return; + } + else + { + // Search for ghost on the ground + CBaseEntity* pGhost = gEntList.FindEntityByClassname( nullptr, "weapon_ghost" ); + if ( pGhost ) + { + m_vGoalPos = pGhost->WorldSpaceCenter(); + m_bGoingToTargetEntity = true; + m_hTargetEntity = pGhost; + + if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + { + return; + } + } + } + } + + // Fallback to base behavior (roaming spawn points) + CNEOBotSeekAndDestroy::RecomputeSeekPath( me ); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h new file mode 100644 index 0000000000..a09b305177 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h @@ -0,0 +1,18 @@ +#pragma once + +#include "bot/behavior/neo_bot_seek_and_destroy.h" + +// +// CTG game mode behavior dispatcher +// +class CNEOBotCtgSeek : public CNEOBotSeekAndDestroy +{ +public: + CNEOBotCtgSeek( float duration = -1.0f ) : CNEOBotSeekAndDestroy( duration ) { } + + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual const char *GetName( void ) const override { return "ctgSeek"; }; + +protected: + virtual void RecomputeSeekPath( CNEOBot *me ) override; +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp b/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp index 0b37e748bb..2f1d9eb8da 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp @@ -175,7 +175,7 @@ ActionResult< CNEOBot > CNEOBotGetHealth::OnStart( CNEOBot *me, Action< CNEOBot m_healthKit = s_possibleHealth; m_isGoalCharger = m_healthKit->ClassMatches( "*charger*" ); - if (!CNEOBotPathCompute(me, m_path, m_healthKit->WorldSpaceCenter(), SAFEST_ROUTE)) + if (!CNEOBotPathCompute(me, m_path, m_healthKit->WorldSpaceCenter(), DEFAULT_ROUTE)) { return Done( "No path to health!" ); } @@ -211,7 +211,7 @@ ActionResult< CNEOBot > CNEOBotGetHealth::Update( CNEOBot *me, float interval ) { // this can occur if we overshoot the health kit's location // because it is momentarily gone - if ( !CNEOBotPathCompute( me, m_path, m_healthKit->WorldSpaceCenter(), SAFEST_ROUTE ) ) + if ( !CNEOBotPathCompute( me, m_path, m_healthKit->WorldSpaceCenter(), DEFAULT_ROUTE ) ) { return Done( "No path to health!" ); } 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 b284a7c668..d7af5cc12f 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 @@ -5,10 +5,10 @@ #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_seek_and_destroy.h" +#include "bot/behavior/neo_bot_ctg_seek.h" #include "bot/behavior/neo_bot_jgr_seek.h" #include "bot/neo_bot_path_compute.h" #include "nav_mesh.h" -#include "neo_ghost_cap_point.h" extern ConVar neo_bot_path_lookahead_range; extern ConVar neo_bot_offense_must_push_time; @@ -58,7 +58,11 @@ ActionResult< CNEOBot > CNEOBotSeekAndDestroy::Update( CNEOBot *me, float interv return result; // Check for Game Type Specific behaviors and suspend for them - if (NEORules()->GetGameType() == NEO_GAME_TYPE_JGR) + if (NEORules()->GetGameType() == NEO_GAME_TYPE_CTG) + { + return SuspendFor( new CNEOBotCtgSeek, "Switching to Ghost-related Seek and Destroy" ); + } + else if (NEORules()->GetGameType() == NEO_GAME_TYPE_JGR) { return SuspendFor( new CNEOBotJgrSeek, "Switching to Juggernaut-related Seek and Destroy" ); } @@ -353,133 +357,12 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) m_hTargetEntity = pClosestWeapon; m_bGoingToTargetEntity = true; m_vGoalPos = pClosestWeapon->WorldSpaceCenter(); - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, SAFEST_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) return; } } } #endif - - if (NEORules()->GhostExists()) - { - const Vector vGhostPos = NEORules()->GetGhostPos(); - const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); - - const int iMyTeam = me->GetTeamNumber(); - const int iGhosterTeam = NEORules()->GetGhosterTeam(); - - bool bGoToGoalPos = true; - bool bGetCloserToGhoster = false; - bool bQuickToGoalPos = false; - - if (iGhosterPlayer > 0) - { - const int iTargetCapTeam = (iGhosterTeam == iMyTeam) ? iMyTeam : iGhosterTeam; - - // If there's a player playing ghost, turn toward cap zones that's - // closest to the ghoster player - Vector vrTargetCapPos = vec3_invalid; - int iMinCapGhostLength = INT_MAX; - - // Enemy team is carrying the ghost - try to defend the cap zone - // You or friendly team is carrying the ghost - go towards the cap point - - for (int i = 0; i < NEORules()->m_pGhostCaps.Count(); i++) - { - auto pGhostCap = dynamic_cast( - UTIL_EntityByIndex(NEORules()->m_pGhostCaps[i])); - if (!pGhostCap) - { - continue; - } - - const Vector vCapPos = pGhostCap->GetAbsOrigin(); - const Vector vGhostCapDist = vGhostPos - vCapPos; - const int iGhostCapLength = static_cast(vGhostCapDist.Length()); - const int iCapTeam = pGhostCap->owningTeamAlternate(); - - if (iCapTeam == iTargetCapTeam && iGhostCapLength < iMinCapGhostLength) - { - vrTargetCapPos = vCapPos; - iMinCapGhostLength = iGhostCapLength; - } - } - - if (!me->IsCarryingGhost()) - { - // If a ghoster player carrying and nearby, get close to them - // Friendly - get closer and assists, enemy - get closer and attack - const float flGhosterMeters = METERS_PER_INCH * me->GetAbsOrigin().DistTo(vGhostPos); - const float flMinCapMeters = METERS_PER_INCH * iMinCapGhostLength; - static const constexpr float FL_NEARBY_FOLLOW_METERS = 26.0f; - static const constexpr float FL_NEARBY_CAPZONE_METERS = 18.0f; - const bool bGhosterNearby = flGhosterMeters < FL_NEARBY_FOLLOW_METERS; - const bool bCapzoneNearby = flMinCapMeters < FL_NEARBY_CAPZONE_METERS; - // But a nearby capzone overrides a nearby ghoster - bGetCloserToGhoster = !bCapzoneNearby && bGhosterNearby && flMinCapMeters > flGhosterMeters; - } - - if (bGetCloserToGhoster) - { - Assert(vGhostPos.IsValid()); - m_vGoalPos = vGhostPos; - bQuickToGoalPos = true; - } - else - { - // iMinCapGhostLength == INT_MAX should never happen, just disable going to target - Assert(iMinCapGhostLength < INT_MAX); - bGoToGoalPos = (iMinCapGhostLength < INT_MAX); - if (bGoToGoalPos) // else, vrTargetCapPos may be uninitialized - { - Assert(vrTargetCapPos.IsValid()); - m_vGoalPos = vrTargetCapPos; - } - - bQuickToGoalPos = (iGhosterTeam != iMyTeam); - } - } - else - { - // If the ghost exists, go to the ghost - Assert(vGhostPos.IsValid()); - m_vGoalPos = vGhostPos; - // NEO TODO (nullsystem): More sophisticated on handling non-ghost playing scenario, - // although it kind of already prefer hunting down players when they're in view, but - // just going towards ghost isn't something that always happens in general. - } - - if (bGoToGoalPos) - { - Assert(m_vGoalPos.IsValid()); - if (bGetCloserToGhoster) - { - const int iDistSqrConsidered = (iGhosterTeam == iMyTeam) ? 50000 : 5000; - if (m_vGoalPos.DistToSqr(me->GetAbsOrigin()) < iDistSqrConsidered) - { - // Don't stop targeting entity even when near enough - return; - } - } - else - { - constexpr int DISTANCE_CONSIDERED_ARRIVED_SQUARED = 10000; - if (m_vGoalPos.DistToSqr(me->GetAbsOrigin()) < DISTANCE_CONSIDERED_ARRIVED_SQUARED) - { - constexpr float RECHECK_TIME = 30.f; - m_repathTimer.Start(RECHECK_TIME); - m_bGoingToTargetEntity = false; - return; - } - } - m_bGoingToTargetEntity = true; - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, bQuickToGoalPos ? FASTEST_ROUTE : SAFEST_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) - { - return; - } - } - } - // Fallback and roam random spawn points if we have all weapons. { CNextSpawnFilter spawnFilter( me, 128.0f ); @@ -501,7 +384,7 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) m_hTargetEntity = pSpawns[RandomInt( 0, pSpawns.Size() - 1 )]; m_bGoingToTargetEntity = true; m_vGoalPos = m_hTargetEntity->WorldSpaceCenter(); - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, SAFEST_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) return; } } @@ -513,7 +396,7 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) Vector vWanderPoint = TheNavAreas[RandomInt( 0, TheNavAreas.Size() - 1 )]->GetCenter(); m_vGoalPos = vWanderPoint; - if ( CNEOBotPathCompute( me, m_path, vWanderPoint, SAFEST_ROUTE ) ) + if ( CNEOBotPathCompute( me, m_path, vWanderPoint, DEFAULT_ROUTE ) ) return; } @@ -548,7 +431,7 @@ EventDesiredResult< CNEOBot > CNEOBotSeekAndDestroy::OnCommandApproach( CNEOBot* m_bOverrideApproach = true; m_vOverrideApproach = pos; - CNEOBotPathCompute( me, m_path, m_vOverrideApproach, SAFEST_ROUTE ); + CNEOBotPathCompute( me, m_path, m_vOverrideApproach, DEFAULT_ROUTE ); return TryContinue(); } diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 355e3c40b4..1ebd5aa83b 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -1479,7 +1479,8 @@ void CNEOBot::EquipBestWeaponForThreat(const CKnownEntity* threat, const bool bN // We do not care about slugs if (bNotPrimary || (primaryWeapon && - (!primaryWeapon->m_iPrimaryAmmoType || + ((primaryWeapon->GetNeoWepBits() & NEO_WEP_GHOST) || + !primaryWeapon->m_iPrimaryAmmoType || (primaryWeapon->Clip1() + primaryWeapon->m_iPrimaryAmmoCount) <= 0))) { primaryWeapon = NULL; @@ -2000,7 +2001,7 @@ void CNEOBot::RepathIfFriendlyBlockingLineOfFire() { Vector goal = pPath->GetEndPosition(); - CNEOBotPathCost cost(this, SAFEST_ROUTE); + CNEOBotPathCost cost(this, DEFAULT_ROUTE); if (m_repathAroundFriendlyFollower.Compute(this, goal, cost)) { if (m_repathAroundFriendlyFollower.IsValid()) diff --git a/src/game/server/neo/bot/neo_bot_path_cost.cpp b/src/game/server/neo/bot/neo_bot_path_cost.cpp index 5816fc26c1..d1741e3c26 100644 --- a/src/game/server/neo/bot/neo_bot_path_cost.cpp +++ b/src/game/server/neo/bot/neo_bot_path_cost.cpp @@ -115,6 +115,15 @@ float CNEOBotPathCost::operator()(CNavArea* baseArea, CNavArea* fromArea, const { cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), m_me->GetTeamNumber()) * neo_bot_path_reservation_penalty.GetFloat(); cost += CNEOBotPathReservations()->GetAreaStuckPenalty(area->GetID()); + + if (m_routeType == SAFEST_ROUTE) + { + // NEO Jank Cheat: Incorporate enemy bot paths so that we don't run directly into their line of fire + // Intended for use by ghost carrier team, to emulate a team that knows where enemies are likely to ambush + // Compensates for bots' lack of meta knowledge by making them prefer routes not reserved by enemies + // Adheres to cheat against bots but not against humans philosophy by not considering human players' positions + cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), GetEnemyTeam(m_me->GetTeamNumber())) * neo_bot_path_reservation_penalty.GetFloat() * 2; + } } // ------------------------------------------------------------------------------------------------ diff --git a/src/game/server/neo/bot/neo_bot_vision.cpp b/src/game/server/neo/bot/neo_bot_vision.cpp index de7b7e86ad..75d240feb5 100644 --- a/src/game/server/neo/bot/neo_bot_vision.cpp +++ b/src/game/server/neo/bot/neo_bot_vision.cpp @@ -5,6 +5,7 @@ #include "neo_bot_vision.h" #include "neo_player.h" #include "neo_gamerules.h" +#include "neo/weapons/weapon_ghost.h" ConVar neo_bot_choose_target_interval( "neo_bot_choose_target_interval", "0.3f", FCVAR_CHEAT, "How often, in seconds, a NEOBot can reselect his target" ); ConVar neo_bot_sniper_choose_target_interval( "neo_bot_sniper_choose_target_interval", "3.0f", FCVAR_CHEAT, "How often, in seconds, a zoomed-in Sniper can reselect his target" ); @@ -162,29 +163,21 @@ float CNEOBotVision::GetMaxVisionRange( void ) const bool CNEOBotVision::IsInFieldOfView( CBaseEntity *subject ) const { - // Ghoster is always in FOV of everyone - const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); - if (iGhosterPlayer > 0) - { - auto *pNEOPlayer = ToNEOPlayer(subject); - if (pNEOPlayer && pNEOPlayer->IsCarryingGhost()) - { - return true; - } - } - return IVision::IsInFieldOfView(subject); } bool CNEOBotVision::IsAbleToSee(CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot) const { - const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); - if (iGhosterPlayer > 0) + CNEOBot *me = (CNEOBot *)GetBot()->GetEntity(); + if (me && me->IsCarryingGhost()) { - auto *pNEOPlayer = ToNEOPlayer(subject); - if (pNEOPlayer && pNEOPlayer->IsCarryingGhost()) + auto *pGhost = dynamic_cast(me->GetActiveWeapon()); + if (pGhost && pGhost->IsGhost() && pGhost->IsBootupCompleted()) { - return true; + if (me->GetAbsOrigin().DistToSqr(subject->GetAbsOrigin()) < Square(CWeaponGhost::GetGhostRangeInHammerUnits())) + { + return true; + } } } diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index b43cb6f80a..f2e2327403 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -440,6 +440,11 @@ class CNEORules : public CHL2MPRules, public CGameEventListener float m_flJuggernautDeathTime = 0.0f; int m_iLastJuggernautTeam = TEAM_INVALID; + // For looking up capture zone locations + friend class CNEOBotCtgCarrier; + friend class CNEOBotCtgEscort; + friend class CNEOBotCtgLoneWolf; + friend class CNEOBotSeekAndDestroy; CUtlVector m_pGhostCaps; CWeaponGhost *m_pGhost = nullptr; From b12de89c1fb54ba2481cdb46f5e31de7fcc1b8ee Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Wed, 11 Feb 2026 22:52:54 -0700 Subject: [PATCH 2/2] Prefer square units and avoid vector copies --- src/game/server/CMakeLists.txt | 4 +-- .../neo/bot/behavior/neo_bot_ctg_carrier.cpp | 36 +++++++++---------- .../neo/bot/behavior/neo_bot_ctg_escort.cpp | 9 ++--- .../bot/behavior/neo_bot_ctg_lone_wolf.cpp | 25 ++++++------- .../neo/bot/behavior/neo_bot_ctg_lone_wolf.h | 5 +-- .../neo/bot/behavior/neo_bot_ctg_seek.cpp | 2 +- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 57a69beab9..b1f84ef28b 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1441,8 +1441,6 @@ target_sources_grouped( neo/bot/behavior/neo_bot_behavior.h neo/bot/behavior/neo_bot_command_follow.cpp neo/bot/behavior/neo_bot_command_follow.h - neo/bot/behavior/neo_bot_dead.cpp - neo/bot/behavior/neo_bot_dead.h neo/bot/behavior/neo_bot_ctg_capture.cpp neo/bot/behavior/neo_bot_ctg_capture.h neo/bot/behavior/neo_bot_ctg_carrier.cpp @@ -1455,6 +1453,8 @@ target_sources_grouped( neo/bot/behavior/neo_bot_ctg_lone_wolf.h neo/bot/behavior/neo_bot_ctg_seek.cpp neo/bot/behavior/neo_bot_ctg_seek.h + neo/bot/behavior/neo_bot_dead.cpp + neo/bot/behavior/neo_bot_dead.h neo/bot/behavior/neo_bot_jgr_capture.cpp neo/bot/behavior/neo_bot_jgr_capture.h neo/bot/behavior/neo_bot_jgr_enemy.cpp diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp index ad8d6c8c88..7c2bb664db 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp @@ -125,9 +125,9 @@ void CNEOBotGhostEquipmentHandler::EquipBestWeaponForGhoster( CNEOBot *me ) { const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); - CNEOBaseCombatWeapon *pActive = dynamic_cast( me->GetActiveWeapon() ); - CNEOBaseCombatWeapon *pGhost = dynamic_cast( me->Weapon_GetSlot( 0 ) ); - CNEOBaseCombatWeapon *pSecondary = dynamic_cast( me->Weapon_GetSlot( 1 ) ); + CNEOBaseCombatWeapon *pActive = assert_cast( me->GetActiveWeapon() ); + CNEOBaseCombatWeapon *pGhost = assert_cast( me->Weapon_GetSlot( 0 ) ); + CNEOBaseCombatWeapon *pSecondary = assert_cast( me->Weapon_GetSlot( 1 ) ); // Sanity check: if we don't have the ghost, we shouldn't be in this behavior, but handle gracefully if ( !pGhost ) @@ -177,12 +177,12 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const { CNEO_Player *pBestCallout = nullptr; float flBestCalloutMoved = -1.0f; - float flBestCalloutDist = FLT_MAX; + float flBestCalloutDistSq = FLT_MAX; bool bBestCalloutIsNew = false; bool bConsideringOnlyLoSEnemies = false; - const Vector vecMyPos = me->GetAbsOrigin(); + const Vector& vecMyPos = me->GetAbsOrigin(); for ( int i = 0; i < enemies.Count(); ++i ) { @@ -205,7 +205,7 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const continue; } - float flDistToMe = vecMyPos.DistTo( pPlayer->GetAbsOrigin() ); + float flDistToMeSq = vecMyPos.DistToSqr( pPlayer->GetAbsOrigin() ); // Also take into account how much the enemy has moved since we last reported them Vector vecLast = m_enemyLastPos[ idx ]; @@ -226,16 +226,16 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const pBestCallout = pPlayer; bBestCalloutIsNew = bIsNew; flBestCalloutMoved = flMoved; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; } else { - if ( flDistToMe < flBestCalloutDist ) + if ( flDistToMeSq < flBestCalloutDistSq ) { pBestCallout = pPlayer; bBestCalloutIsNew = bIsNew; flBestCalloutMoved = flMoved; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; } } continue; @@ -252,7 +252,7 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const pBestCallout = pPlayer; bBestCalloutIsNew = bIsNew; flBestCalloutMoved = flMoved; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; continue; } @@ -261,7 +261,7 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const { pBestCallout = pPlayer; bBestCalloutIsNew = true; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; flBestCalloutMoved = 0; continue; } @@ -276,10 +276,10 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const if ( bIsNew ) { // Both New: Tie breaker is distance to me (closest first) - if ( flDistToMe < flBestCalloutDist ) + if ( flDistToMeSq < flBestCalloutDistSq ) { pBestCallout = pPlayer; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; } } else @@ -291,11 +291,11 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const if ( FloatMakePositive( diff ) < 100.0f ) { // Roughly same movement, tie breaker is closest distance - if ( flDistToMe < flBestCalloutDist ) + if ( flDistToMeSq < flBestCalloutDistSq ) { pBestCallout = pPlayer; flBestCalloutMoved = flMoved; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; } } else if ( diff > 0.0f ) @@ -303,7 +303,7 @@ void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const // Distinctly moved more pBestCallout = pPlayer; flBestCalloutMoved = flMoved; - flBestCalloutDist = flDistToMe; + flBestCalloutDistSq = flDistToMeSq; } } } @@ -583,8 +583,8 @@ void CNEOBotCtgCarrier::UpdateFollowPath( CNEOBot *me, const CUtlVectorGetAbsOrigin().DistTo( pTargetTeammate->GetAbsOrigin() ); - if ( flDistToTeammate < 100.0f ) + float flDistToTeammateSq = me->GetAbsOrigin().DistToSqr( pTargetTeammate->GetAbsOrigin() ); + if ( flDistToTeammateSq < ( 100.0f * 100.0f ) ) { m_chasePath.Invalidate(); return; 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 662141c0e8..1608185be9 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 @@ -92,7 +92,8 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) // Look away from the carrier to cover their blind spots Vector vecFromCarrier = me->GetAbsOrigin() - pGhostCarrier->GetAbsOrigin(); vecFromCarrier.z = 0.0f; // Bias towards horizontal scanning - if ( VectorNormalize( vecFromCarrier ) > 0.1f ) + // CNEOBotTacticalMonitor::AvoidBumpingFriends handles <32hu distance + if ( VectorNormalize( vecFromCarrier ) > 32.0f ) { // Look at a point far away in the opposite direction of the carrier Vector vecLookTarget = me->EyePosition() + ( vecFromCarrier * 500.0f ); @@ -176,7 +177,7 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) m_chasePath.Invalidate(); - Vector vecMoveTarget = m_vecGoalPos; + Vector& vecMoveTarget = m_vecGoalPos; if ( m_role == ROLE_SCREEN && pBotGhostCarrier ) @@ -261,7 +262,7 @@ CNEOBotCtgEscort::EscortRole CNEOBotCtgEscort::UpdateRoleAssignment( CNEOBot *me CNEO_Player* pBestToCarrier = nullptr; CNEO_Player* pSecondBestToCarrier = nullptr; - Vector vecCarrierOrigin = pGhostCarrier->GetAbsOrigin(); + const Vector& vecCarrierOrigin = pGhostCarrier->GetAbsOrigin(); for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { @@ -271,7 +272,7 @@ CNEOBotCtgEscort::EscortRole CNEOBotCtgEscort::UpdateRoleAssignment( CNEOBot *me continue; } - Vector vecPlayerOrigin = pPlayer->GetAbsOrigin(); + const Vector& vecPlayerOrigin = pPlayer->GetAbsOrigin(); // Check for Scout (Best dist to goal) float goalDist = vecPlayerOrigin.DistToSqr( vecGoalPos ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp index 03f4658bb5..c561f50326 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp @@ -101,11 +101,11 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval if ( !m_capPointUpdateTimer.HasStarted() || m_capPointUpdateTimer.IsElapsed() ) { m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; - float flNearestCapDist = FLT_MAX; + float flNearestCapDistSq = FLT_MAX; if ( NEORules()->m_pGhostCaps.Count() > 0 ) { - Vector vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); + const Vector& vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); for( int i=0; im_pGhostCaps.Count(); ++i ) { @@ -114,10 +114,10 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval if ( pCapPoint->owningTeamAlternate() == me->GetTeamNumber() ) { - float d = vecStart.DistTo( pCapPoint->GetAbsOrigin() ); - if ( d < flNearestCapDist ) + float distSq = vecStart.DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( distSq < flNearestCapDistSq ) { - flNearestCapDist = d; + flNearestCapDistSq = distSq; m_closestCapturePoint = pCapPoint->GetAbsOrigin(); } } @@ -129,7 +129,7 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval float flDistGhostToGoal = FLT_MAX; if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) { - Vector vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); + const Vector& vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); flDistGhostToGoal = vecStart.DistTo( m_closestCapturePoint ); } @@ -147,7 +147,8 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval // Count enemies and find if one is closer to our goal int iEnemyTeamCount = 0; - float flClosestEnemyDistToGoal = FLT_MAX; + float flClosestEnemyDistToGoalSq = FLT_MAX; + float flMyTotalDistSq = ( flMyTotalDist >= FLT_MAX ) ? FLT_MAX : ( flMyTotalDist * flMyTotalDist ); for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { @@ -157,11 +158,11 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval iEnemyTeamCount++; if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) { - float d = pPlayer->GetAbsOrigin().DistTo( m_closestCapturePoint ); - if ( d < flClosestEnemyDistToGoal ) + float distSq = pPlayer->GetAbsOrigin().DistToSqr( m_closestCapturePoint ); + if ( distSq < flClosestEnemyDistToGoalSq ) { - flClosestEnemyDistToGoal = d; - if ( iEnemyTeamCount > 1 && flClosestEnemyDistToGoal < flMyTotalDist ) + flClosestEnemyDistToGoalSq = distSq; + if ( iEnemyTeamCount > 1 && flClosestEnemyDistToGoalSq < flMyTotalDistSq ) { // We already know it's not a 1v1 (count > 1) // And we know it's not safe to cap (enemy closer than us) @@ -177,7 +178,7 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval // Just try to grab the ghost, even if it might not be the best tactic bool bIs1v1 = (iEnemyTeamCount == 1); - bool bSafeToCap = ((m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT) && (flMyTotalDist < flClosestEnemyDistToGoal)); + bool bSafeToCap = ((m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT) && (flMyTotalDistSq < flClosestEnemyDistToGoalSq)); CWeaponGhost *pGhostWeapon = m_hGhost.Get(); CBaseCombatCharacter *pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h index ef9bf545c3..6458c1dc28 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h @@ -1,5 +1,4 @@ -#ifndef NEO_BOT_CTG_LONE_WOLF_H -#define NEO_BOT_CTG_LONE_WOLF_H +#pragma once #include "bot/neo_bot.h" @@ -40,5 +39,3 @@ class CNEOBotCtgLoneWolf : public Action< CNEOBot > CUtlVector< CNavArea * > m_visibleAreas; }; - -#endif // NEO_BOT_CTG_LAST_STAND_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp index 00d25a4248..e7134f5fbb 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp @@ -82,7 +82,7 @@ ActionResult< CNEOBot > CNEOBotCtgSeek::Update( CNEOBot *me, float interval ) CBaseCombatWeapon* pWeapon = m_hTargetEntity->MyCombatWeaponPointer(); if ( pWeapon && !pWeapon->GetOwner() ) { - CWeaponGhost *pGhost = dynamic_cast( m_hTargetEntity.Get() ); + CWeaponGhost *pGhost = assert_cast( m_hTargetEntity.Get() ); return SuspendFor( new CNEOBotCtgCapture( pGhost ), "Capturing Ghost" ); } }