Skip to content
Merged
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
117 changes: 96 additions & 21 deletions src/game/server/neo/bot/behavior/neo_bot_seek_weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,42 @@
static constexpr int BOT_WEP_PREF_RANK_UNPREFERRED = -1;
static constexpr int BOT_WEP_PREF_RANK_EMPTY = -2;

// A weapon lies on a nav area when it is over the area, or at most half a player's width past its edge
// (nav_generate areas often stop short of wall boundaries),
static constexpr float BOT_WEP_MAX_OUTSIDE_AREA = HalfHumanWidth;
// and at most this far above the area's surface (such as a desk or crate surface)
static constexpr float BOT_WEP_MAX_HEIGHT_ABOVE_AREA = 32.0f;
// or this far below it (a nav area's plane is rarely an exact flush fit with a stairway's steps)
static constexpr float BOT_WEP_MAX_DEPTH_BELOW_AREA = 8.0f;
static constexpr float BOT_WEP_AREA_SEARCH_RANGE = 64.0f;

//---------------------------------------------------------------------------------------------
// Weapons can come to rest where no nav area exists: in a pit, behind a counter, out of a window
static CNavArea *GetWeaponNavArea( CBaseEntity *pWeapon )
{
const Vector vecWeapon = pWeapon->WorldSpaceCenter();
CNavArea *pArea = TheNavMesh->GetNearestNavArea( vecWeapon, false, BOT_WEP_AREA_SEARCH_RANGE, true, false );
if ( !pArea )
{
return nullptr;
}

Vector vecClosest;
pArea->GetClosestPointOnArea( vecWeapon, &vecClosest );
if ( ( vecWeapon - vecClosest ).Length2D() > BOT_WEP_MAX_OUTSIDE_AREA )
{
return nullptr;
}

const float flHeightAboveArea = vecWeapon.z - vecClosest.z;
if ( flHeightAboveArea > BOT_WEP_MAX_HEIGHT_ABOVE_AREA || flHeightAboveArea < -BOT_WEP_MAX_DEPTH_BELOW_AREA )
{
return nullptr;
}

return pArea;
}

//---------------------------------------------------------------------------------------------
bool IsUndroppablePrimary( CBaseCombatWeapon *pPrimary )
{
Expand Down Expand Up @@ -174,14 +210,17 @@ CBaseEntity *FindNearestPrimaryWeapon( const CNEOBot *me, bool bAllowDropGhost,

if ( bBetterFound )
{
// A path to an off-mesh weapon ends with a straight walk to it, over whatever lies between
CNavArea *pWepArea = GetWeaponNavArea( pEntity );
if ( !pWepArea )
{
continue;
}

// Check if weapon candidate is in PVS of me
if ( pMyArea )
if ( pMyArea && !pMyArea->IsPotentiallyVisible( pWepArea ) )
{
CNavArea *pWepArea = TheNavMesh->GetNavArea( pEntity->WorldSpaceCenter() );
if ( pWepArea && !pMyArea->IsPotentiallyVisible( pWepArea ) )
{
continue;
}
continue;
}

flClosestDistSq = flDistSq;
Expand All @@ -201,6 +240,36 @@ CNEOBotSeekWeapon::CNEOBotSeekWeapon( CBaseEntity *pTargetWeapon, CNEOIgnoredWea
m_pIgnoredWeapons = pIgnoredWeapons;
}

//---------------------------------------------------------------------------------------------
// Remember a weapon the bot failed to reach, so the next scavenge does not pick it again
void CNEOBotSeekWeapon::IgnoreTargetWeapon( void )
{
if ( !m_hTargetWeapon || !m_pIgnoredWeapons || m_pIgnoredWeapons->Has( m_hTargetWeapon ) )
{
return;
}

m_pIgnoredWeapons->Add( m_hTargetWeapon );
}

//---------------------------------------------------------------------------------------------
// Only a full path to the weapon's nav area: a partial one ends with a straight walk from the edge of the mesh
bool CNEOBotSeekWeapon::PathToTargetWeapon( CNEOBot *me )
{
// The weapon may have been knocked off the mesh since it was chosen
const CNavArea *pWepArea = GetWeaponNavArea( m_hTargetWeapon );
if ( !pWepArea )
{
m_path.Invalidate();
return false;
}

// Stay on the area: from its edge a bot still touches a weapon lying against the wall beyond it
Vector vecGoal;
pWepArea->GetClosestPointOnArea( m_hTargetWeapon->WorldSpaceCenter(), &vecGoal );
return CNEOBotPathCompute( me, m_path, vecGoal, FASTEST_ROUTE, PATH_NO_LENGTH_LIMIT, PATH_TRUNCATE_INCOMPLETE_PATH );
}

//---------------------------------------------------------------------------------------------
CBaseEntity *CNEOBotSeekWeapon::FindAndPathToWeapon( CNEOBot *me )
{
Expand All @@ -209,19 +278,16 @@ CBaseEntity *CNEOBotSeekWeapon::FindAndPathToWeapon( CNEOBot *me )
m_hTargetWeapon = FindNearestPrimaryWeapon( me, false, m_pIgnoredWeapons );
}

if ( m_hTargetWeapon )
{
if ( !CNEOBotPathCompute( me, m_path, m_hTargetWeapon->GetAbsOrigin(), FASTEST_ROUTE ) || !m_path.IsValid() )
{
m_hTargetWeapon = nullptr;
m_path.Invalidate();
}
}
else
if ( !m_hTargetWeapon )
{
// no weapon found
m_path.Invalidate();
}
else if ( !PathToTargetWeapon( me ) )
{
IgnoreTargetWeapon();
m_hTargetWeapon = nullptr;
}

return m_hTargetWeapon;
}
Expand Down Expand Up @@ -250,6 +316,14 @@ ActionResult< CNEOBot > CNEOBotSeekWeapon::OnStart( CNEOBot *me, Action< CNEOBot
return Done("No valid replacement primary found");
}

// Check the path before a ghost carrier drops the ghost for this weapon
if ( !PathToTargetWeapon( me ) )
{
IgnoreTargetWeapon();
return Done( "No full path to the weapon" );
}
m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );

auto *pNeoPrimary = assert_cast<CNEOBaseCombatWeapon *>( pPrimary );
if ( pNeoPrimary && ( pNeoPrimary->GetNeoWepBits() & NEO_WEP_GHOST ) )
{
Expand All @@ -271,14 +345,16 @@ ActionResult< CNEOBot > CNEOBotSeekWeapon::Update( CNEOBot *me, float interval )

if ( m_giveUpTimer.IsElapsed() )
{
IgnoreTargetWeapon();
return Done("Gave up seeking weapon");
}

if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() )
{
if ( !CNEOBotPathCompute( me, m_path, m_hTargetWeapon->GetAbsOrigin(), FASTEST_ROUTE ) )
if ( !PathToTargetWeapon( me ) )
{
return Done("Unable to find a path to the nearest primary weapon");
IgnoreTargetWeapon();
return Done( "No full path to the weapon" );
}
m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
}
Expand All @@ -292,10 +368,7 @@ ActionResult< CNEOBot > CNEOBotSeekWeapon::Update( CNEOBot *me, float interval )
const float flMaxEndpointDistSqr = 150.0f * 150.0f;
if ( m_path.GetEndPosition().DistToSqr( m_hTargetWeapon->GetAbsOrigin() ) > flMaxEndpointDistSqr )
{
if ( m_pIgnoredWeapons && !m_pIgnoredWeapons->Has( m_hTargetWeapon ) )
{
m_pIgnoredWeapons->Add( m_hTargetWeapon );
}
IgnoreTargetWeapon();
return Done("Weapon is unreachable (path doesn't get close enough)");
}

Expand Down Expand Up @@ -354,6 +427,7 @@ ActionResult< CNEOBot > CNEOBotSeekWeapon::OnResume( CNEOBot *me, Action< CNEOBo
//---------------------------------------------------------------------------------------------
EventDesiredResult< CNEOBot > CNEOBotSeekWeapon::OnStuck( CNEOBot *me )
{
IgnoreTargetWeapon();
m_hTargetWeapon = nullptr;
m_repathTimer.Invalidate();
FindAndPathToWeapon(me);
Expand All @@ -369,6 +443,7 @@ EventDesiredResult< CNEOBot > CNEOBotSeekWeapon::OnMoveToSuccess( CNEOBot *me, c
//---------------------------------------------------------------------------------------------
EventDesiredResult< CNEOBot > CNEOBotSeekWeapon::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason )
{
IgnoreTargetWeapon();
m_hTargetWeapon = nullptr;
m_repathTimer.Invalidate();
FindAndPathToWeapon(me);
Expand Down
2 changes: 2 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_seek_weapon.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ class CNEOBotSeekWeapon : public Action< CNEOBot >
CountdownTimer m_repathTimer;
CountdownTimer m_giveUpTimer;

bool PathToTargetWeapon( CNEOBot *me );
CBaseEntity *FindAndPathToWeapon( CNEOBot *me );
void IgnoreTargetWeapon( void );
};
16 changes: 12 additions & 4 deletions src/game/server/neo/bot/neo_bot_path_compute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ static void CNEOBotReservePath(CNEOBot* me, PathFollower& path)
}
}

// A path that stops short of the goal is only wanted if the caller asked for partial paths
static bool IsPathUsable(const PathFollower& path, bool reachedGoal, bool includeGoalIfPathFails)
{
return path.IsValid() && (reachedGoal || includeGoalIfPathFails);
}

// By default, assumes that we would prefer to have partial paths even if we can't make a full path
// to accomodate scenarios like getting as close as possible to a hazard area but not into it
// Set includeGoalIfPathFails to false if you need to get the full path (or nothing on failure)
Expand All @@ -42,22 +48,24 @@ bool CNEOBotPathCompute(CNEOBot* bot, PathFollower& path, const Vector& goal, Ro
Assert(goal.IsValid());

CNEOBotPathCost cost_with_reservations(bot, route);
path.Compute(bot, goal, cost_with_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea);
if (path.IsValid())
bool reachedGoal = path.Compute(bot, goal, cost_with_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea);
if (IsPathUsable(path, reachedGoal, includeGoalIfPathFails))
{
CNEOBotReservePath(bot, path);
return true;
}

CNEOBotPathCost cost_without_reservations(bot, FASTEST_ROUTE);
cost_without_reservations.m_bIgnoreReservations = true;
path.Compute(bot, goal, cost_without_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea);
if (path.IsValid())
reachedGoal = path.Compute(bot, goal, cost_without_reservations, maxPathLength, includeGoalIfPathFails, requireGoalArea);
if (IsPathUsable(path, reachedGoal, includeGoalIfPathFails))
{
CNEOBotReservePath(bot, path);
return true;
}

// Path::Compute leaves a partial path valid even when told not to append the goal
path.Invalidate();
return false;
}

Expand Down
Loading