diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index da026598c..a3e0fdb23 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1525,6 +1525,7 @@ set(UNITY_SOURCE_NEO_BOT neo/bot/behavior/neo_bot_ctg_capture.cpp neo/bot/behavior/neo_bot_ctg_carrier.cpp neo/bot/behavior/neo_bot_ctg_enemy.cpp + neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.cpp neo/bot/behavior/neo_bot_ctg_escort.cpp neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp neo/bot/behavior/neo_bot_ctg_seek.cpp @@ -1585,6 +1586,7 @@ target_sources_grouped( neo/bot/behavior/neo_bot_ctg_capture.h neo/bot/behavior/neo_bot_ctg_carrier.h neo/bot/behavior/neo_bot_ctg_enemy.h + neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.h neo/bot/behavior/neo_bot_ctg_escort.h neo/bot/behavior/neo_bot_ctg_lone_wolf.h neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.cpp diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.cpp new file mode 100644 index 000000000..688c88a0e --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.cpp @@ -0,0 +1,214 @@ +#include "cbase.h" +#include "neo_player.h" +#include "neo_gamerules.h" +#include "neo_ghost_cap_point.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.h" +#include "bot/behavior/neo_bot_ctg_enemy.h" +#include "bot/neo_bot_path_compute.h" +#include "nav_pathfind.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEnemyInterceptCapPath::CNEOBotCtgEnemyInterceptCapPath( void ) + : m_targetCapPos( vec3_origin ), + m_targetCapArea( nullptr ) +{ +} + +//--------------------------------------------------------------------------------------------- +bool CNEOBotCtgEnemyInterceptCapPath::RecomputeTargetCapPoint( CNEOBot *me ) +{ + m_targetCapPos = vec3_origin; + m_targetCapArea = nullptr; + + if ( !NEORules()->GhostExists() ) + { + return false; + } + + const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + if ( iGhosterPlayer <= 0 || iGhosterPlayer > gpGlobals->maxClients ) + { + return false; + } + + CNEO_Player *pGhostCarrier = ToNEOPlayer( UTIL_PlayerByIndex( iGhosterPlayer ) ); + if ( !pGhostCarrier || !pGhostCarrier->IsAlive() || pGhostCarrier->GetTeamNumber() == me->GetTeamNumber() ) + { + return false; + } + + float flMinDistSq = FLT_MAX; + CNEOGhostCapturePoint *pBestCap = nullptr; + + for ( int i = 0; i < NEORules()->m_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast< CNEOGhostCapturePoint* >( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if ( !pCapPoint ) + { + continue; + } + + if ( pCapPoint->owningTeamAlternate() != me->GetTeamNumber() ) + { + const float distSq = pGhostCarrier->GetAbsOrigin().DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( distSq < flMinDistSq ) + { + flMinDistSq = distSq; + pBestCap = pCapPoint; + } + } + } + + if ( !pBestCap ) + { + return false; + } + + const Vector vecCapPos = pBestCap->GetAbsOrigin(); + CNavArea *pCapArea = TheNavMesh->GetNearestNavArea( vecCapPos ); + if ( !pCapArea ) + { + return false; + } + + CNavArea *pCarrierArea = pGhostCarrier->GetLastKnownArea(); + if ( !pCarrierArea ) + { + return false; + } + + // Default fallback to the capture zone position and area + m_targetCapPos = vecCapPos; + m_targetCapArea = pCapArea; + + // Predict hypothetical fastest path for the enemy ghost carrier to their target cap zone + if ( pCarrierArea && pCarrierArea != pCapArea ) + { + ShortestPathCost cost; + if ( NavAreaBuildPath( pCarrierArea, pCapArea, &vecCapPos, cost ) ) + { + CUtlVector< CNavArea * > pathAreas; + for ( CNavArea *area = pCapArea; area; area = area->GetParent() ) + { + pathAreas.AddToTail( area ); + } + + // Find the nav area along the predicted carrier path closest to this bot + CNavArea *pBestInterceptArea = nullptr; + float flMinDistSqToBot = FLT_MAX; + const Vector vecBotPos = me->GetAbsOrigin(); + + for ( int i = 0; i < pathAreas.Count(); ++i ) + { + CNavArea *pArea = pathAreas[i]; + if ( !pArea ) + { + continue; + } + + const float distSq = vecBotPos.DistToSqr( pArea->GetCenter() ); + if ( distSq < flMinDistSqToBot ) + { + flMinDistSqToBot = distSq; + pBestInterceptArea = pArea; + } + } + + if ( pBestInterceptArea ) + { + m_targetCapArea = pBestInterceptArea; + m_targetCapPos = pBestInterceptArea->GetCenter(); + } + } + } + + return ( m_targetCapArea != nullptr ); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemyInterceptCapPath::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_visibilityCheckTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + m_recomputeTimer.Start( RandomFloat( 2.0f, 5.0f ) ); + + if ( !RecomputeTargetCapPoint( me ) ) + { + return ChangeTo( new CNEOBotCtgEnemy, "No enemy capture point available" ); + } + + if ( !CNEOBotPathCompute( me, m_path, m_targetCapPos, FASTEST_ROUTE ) ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Initial path compute failed" ); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemyInterceptCapPath::Update( CNEOBot *me, float interval ) +{ + if ( NEORules()->GetGameType() != NEO_GAME_TYPE_CTG ) + { + return Done( "Game mode is no longer CTG" ); + } + + if ( !NEORules()->GhostExists() ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Ghost does not exist" ); + } + + const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + if ( iGhosterPlayer <= 0 || iGhosterPlayer > gpGlobals->maxClients ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Ghost carrier invalid" ); + } + + CNEO_Player *pGhostCarrier = ToNEOPlayer( UTIL_PlayerByIndex( iGhosterPlayer ) ); + if ( !pGhostCarrier || !pGhostCarrier->IsAlive() || pGhostCarrier->GetTeamNumber() == me->GetTeamNumber() ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Ghost carrier dead or friendly" ); + } + + // Check if we need to reroute + if ( m_recomputeTimer.IsElapsed() ) + { + m_recomputeTimer.Start( RandomFloat( 2.0f, 5.0f ) ); + const Vector vecOldTargetPos = m_targetCapPos; + if ( RecomputeTargetCapPoint( me ) ) + { + if ( m_targetCapPos != vecOldTargetPos ) + { + if ( !CNEOBotPathCompute( me, m_path, m_targetCapPos, FASTEST_ROUTE ) ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Path failed on recompute" ); + } + } + } + else + { + return ChangeTo( new CNEOBotCtgEnemy, "No capture point found on recompute" ); + } + } + + // Visibility / arrival stop-condition check (every 1-3s) + if ( m_visibilityCheckTimer.IsElapsed() ) + { + m_visibilityCheckTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + CNavArea *botArea = me->GetLastKnownArea(); + if ( botArea && m_targetCapArea && ( botArea == m_targetCapArea || botArea->IsPotentiallyVisible( m_targetCapArea ) ) ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Reached interception visibility" ); + } + } + + m_path.Update( me ); + if ( !m_path.IsValid() ) + { + return ChangeTo( new CNEOBotCtgEnemy, "Path failed" ); + } + + return Continue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.h new file mode 100644 index 000000000..847e3128b --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy_intercept_cap_path.h @@ -0,0 +1,24 @@ +#pragma once + +#include "bot/neo_bot.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgEnemyInterceptCapPath : public Action< CNEOBot > +{ +public: + CNEOBotCtgEnemyInterceptCapPath( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + + virtual const char *GetName( void ) const override { return "ctgEnemyInterceptCapPath"; } + +private: + bool RecomputeTargetCapPoint( CNEOBot *me ); + + CNavArea *m_targetCapArea; + CountdownTimer m_visibilityCheckTimer; + CountdownTimer m_recomputeTimer; + PathFollower m_path; + Vector m_targetCapPos; +}; 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 e7134f5fb..99d468d05 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 @@ -1,11 +1,13 @@ #include "cbase.h" #include "neo_player.h" #include "neo_gamerules.h" +#include "neo_ghost_cap_point.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_enemy_intercept_cap_path.h" #include "bot/behavior/neo_bot_ctg_carrier.h" #include "bot/behavior/neo_bot_ctg_capture.h" #include "bot/neo_bot_path_compute.h" @@ -25,47 +27,65 @@ ActionResult< CNEOBot > CNEOBotCtgSeek::Update( CNEOBot *me, float interval ) return result; } - int team_members = 0; - for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + if (!NEORules()->GhostExists()) { - CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() == me->GetTeamNumber() ) - { - team_members++; - } + return Done("Ghost does not exist"); } - if ( team_members == 1 ) + if (me->IsCarryingGhost()) { - return SuspendFor( new CNEOBotCtgLoneWolf, "I'm the last one on my team!" ); + return SuspendFor(new CNEOBotCtgCarrier, "I am the ghost carrier!"); } - if (NEORules()->GhostExists()) + int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + if (iGhosterPlayer > 0 && iGhosterPlayer <= gpGlobals->maxClients) { - int iGhosterPlayer = NEORules()->GetGhosterPlayer(); - if (iGhosterPlayer > 0 && iGhosterPlayer <= gpGlobals->maxClients) + CNEO_Player* pGhostCarrier = ToNEOPlayer(UTIL_PlayerByIndex(iGhosterPlayer)); + if (pGhostCarrier && pGhostCarrier != me) { - CNEO_Player* pGhostCarrier = ToNEOPlayer(UTIL_PlayerByIndex(iGhosterPlayer)); - if (pGhostCarrier && pGhostCarrier != me) + if (pGhostCarrier->GetTeamNumber() == me->GetTeamNumber()) { - if (pGhostCarrier->GetTeamNumber() == me->GetTeamNumber()) - { - return SuspendFor(new CNEOBotCtgEscort, "Protecting the ghost carrier!"); - } - else + return SuspendFor(new CNEOBotCtgEscort, "Protecting the ghost carrier!"); + } + + bool bHasEnemyCap = false; + for ( int i = 0; i < NEORules()->m_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast< CNEOGhostCapturePoint* >( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if ( pCapPoint && pCapPoint->owningTeamAlternate() != me->GetTeamNumber() ) { - return SuspendFor(new CNEOBotCtgEnemy, "Stopping the ghost carrier!"); + bHasEnemyCap = true; + break; } } - // If I have the ghost, switch to ghost behavior - if (me->IsCarryingGhost()) + if ( bHasEnemyCap ) + { + return SuspendFor( new CNEOBotCtgEnemyInterceptCapPath, "Intercepting capture zone" ); + } + else { - return SuspendFor(new CNEOBotCtgCarrier, "I am the ghost carrier!"); + return SuspendFor( new CNEOBotCtgEnemy, "Stopping the ghost carrier!" ); } } } + 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!" ); + } + + // Ghost capture logic if (m_bGoingToTargetEntity && m_hTargetEntity) { diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 7d3045be3..34241e5b1 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -491,11 +491,13 @@ class CNEORules : public CHL2MPRules, public CGameEventListener // For looking up capture zone locations friend class CNEOBotCtgCarrier; + friend class CNEOBotCtgEnemyInterceptCapPath; friend class CNEOBotCtgEscort; friend class CNEOBotCtgLoneWolf; friend class CNEOBotCtgLoneWolfAmbush; friend class CNEOBotCtgLoneWolfDetpack; friend class CNEOBotCtgLoneWolfSeek; + friend class CNEOBotCtgSeek; friend class CNEOBotTacticalMonitor; friend class CNEOBotSeekAndDestroy;