Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/game/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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;
};
66 changes: 43 additions & 23 deletions src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
{
Expand Down
2 changes: 2 additions & 0 deletions src/game/shared/neo/neo_gamerules.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down