Skip to content
Open
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
1 change: 1 addition & 0 deletions src/game/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,7 @@ set(UNITY_SOURCE_NEO_BOT
neo/bot/behavior/neo_bot_pause.cpp
neo/bot/behavior/neo_bot_retreat_to_cover.cpp
neo/bot/behavior/neo_bot_retreat_from_grenade.cpp
neo/bot/behavior/neo_bot_retreat_from_hazard_area.cpp
neo/bot/behavior/neo_bot_scenario_monitor.cpp
neo/bot/behavior/neo_bot_seek_and_destroy.cpp
neo/bot/behavior/neo_bot_seek_weapon.cpp
Expand Down
29 changes: 29 additions & 0 deletions src/game/server/NextBot/NextBotVisionInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include "neo_player.h"
#include "neo_smokelineofsightblocker.h"
#include "bot/neo_bot.h"
#include "bot/neo_bot_path_reservation.h"
#include "nav_mesh.h"
#endif

#include "tier0/vprof.h"
Expand Down Expand Up @@ -794,6 +796,33 @@ bool IVision::IsLineOfSightClearToEntity( const CBaseEntity *subject, Vector *vi
UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), firstPosition, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );
if ( result.DidHit() )
{
// Check if first center traceline hit any blocking smoke
if (neo_bot_path_reservation_enable.GetBool()
&& result.m_pEnt
&& FStrEq(result.m_pEnt->GetClassname(), SMOKELINEOFSIGHTBLOCKER_ENTITYNAME))
{
auto pSmoke = static_cast<CNEOSmokeLineOfSightBlocker*>(result.m_pEnt);
// Assume that threat could be hiding inside navarea where smoke blocked sight
if (neoBot)
{
int team = neoBot->GetTeamNumber();

CNavArea *smokeArea = TheNavMesh->GetNearestNavArea(result.endpos);
if (smokeArea)
{
// Register smoke sightlines for team to avoid as hazardous
CNEOBotPathReservations()->AddSmokeHazard(smokeArea->GetID(), pSmoke->GetNextThink(), team);
}

CNavArea *myArea = neoBot->GetLastKnownArea();
if (myArea)
{
// Also register the area I saw the smoke from as hazardous
CNEOBotPathReservations()->AddSmokeHazard(myArea->GetID(), pSmoke->GetNextThink(), team, false);
}
}
}

UTIL_TraceLine( GetBot()->GetBodyInterface()->GetEyePosition(), secondPosition, MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE, &filter, &result );

if ( result.DidHit() )
Expand Down
21 changes: 13 additions & 8 deletions src/game/server/neo/bot/behavior/neo_bot_attack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor
m_threatArea = threat->GetLastKnownArea();
m_goalArea = goalArea ? goalArea : m_threatArea; // prioritize movement towards input goal area or threat
m_myDistToGoalSq = m_goalArea ? ( m_goalArea->GetCenter() - m_me->GetAbsOrigin() ).LengthSqr() : 0;
m_onStuckPenalty = neo_bot_path_reservation_onstuck_penalty.GetFloat();
}

virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar )
Expand All @@ -79,10 +80,18 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor
return true; // skip our starting area
}

if ( neo_bot_path_reservation_enable.GetBool() &&
( CNEOBotPathReservations()->GetAreaAvoidPenalty(area->GetID()) > 0 ) )
// Skip areas that are hazardous or where bots get stuck
if ( neo_bot_path_reservation_enable.GetBool() )
{
return true; // skip areas that have had navigation hiccups
int navAreaId = area->GetID();
if (CNEOBotPathReservations()->IsAreaHazardous(navAreaId, m_me))
{
return true;
}
if (CNEOBotPathReservations()->GetAreaAvoidPenalty(navAreaId) >= m_onStuckPenalty)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot I added avoidance penalties for areas where a bot died, so I made the threshold under which a bot got stuck in a location, allowing to minor penalty for death areas to be used as candidates if under the OnStuck threshold.

{
return true;
}
}

if ( m_attackCoverArea )
Expand Down Expand Up @@ -199,6 +208,7 @@ class CSearchForAttackCover : public ISearchSurroundingAreasFunctor
const CNavArea *m_myArea; // reference point of myself
const CNavArea *m_threatArea; // reference point of the threat
float m_myDistToGoalSq; // the bot's current distance to the threat
float m_onStuckPenalty; // cache onstuck penalty
};


Expand Down Expand Up @@ -452,11 +462,6 @@ QueryResultType CNEOBotAttack::ShouldRetreat( const INextBot *me ) const
return ANSWER_UNDEFINED;
}

if ( m_goalArea && m_attackCoverArea )
{
return ANSWER_NO;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While preventing the bots from retreating when they are rushing for a cover area was okay for proving out #2000, it made the bots a bit suicidal when their chosen advancing cover was too exposed, so I think it's better to retreat when there's any incoming fire.

This also encourages more grenade throw attempts which is helpful for viewing the smoke and grenade avoidance behavior.

}

return ANSWER_UNDEFINED;
}

Expand Down
2 changes: 1 addition & 1 deletion src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval )
m_chasePath.Invalidate();

CNEOBotPathCompute( me, m_path, vecMoveTarget, SAFEST_ROUTE );
m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );
m_repathTimer.Start( RandomFloat( 5.0f, 10.0f ) );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excessively short repath timers can cause bots to indecisively fail to move towards a position, so I increased these timers to allow some movement towards a goal.

}
m_path.Update( me );
}
Expand Down
46 changes: 27 additions & 19 deletions src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include "weapon_grenade.h"
#include "weapon_smokegrenade.h"

ConVar sv_neo_bot_grenade_polite_frag("sv_neo_bot_grenade_polite_frag", "1", FCVAR_NONE, "Force bots to consider teammates when throwing frag grenades.", true, 0, true, 1);
ConVar sv_neo_bot_grenade_polite_smoke("sv_neo_bot_grenade_polite_smoke", "1", FCVAR_NONE, "Force bots to consider teammates when throwing smoke grenades.", true, 0, true, 1);

@sunzenshen sunzenshen Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be rare to see a smoke deployment with the default bot deployment decisions, so I added a ConVar to induce bots to throw grenades more readily. This helps observe the hazard avoidance behavior more frequently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing logic below should be mostly the same despite the indentation changes that caused many diff line changes.

ConVar sv_neo_bot_grenade_use_frag("sv_neo_bot_grenade_use_frag", "1", FCVAR_NONE, "Allow bots to use frag grenades", true, 0, true, 1);
ConVar sv_neo_bot_grenade_use_smoke("sv_neo_bot_grenade_use_smoke", "1", FCVAR_NONE, "Allow bots to use smoke grenades", true, 0, true, 1);
ConVar sv_neo_bot_grenade_throw_cooldown("sv_neo_bot_grenade_throw_cooldown", "10", FCVAR_NONE, "Cooldown in seconds between grenade throws for bots");
Expand Down Expand Up @@ -78,29 +80,32 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( const CNE
// Should I toss a smoke grenade?
if ( pSmokeGrenade && (me->GetClass() == NEO_CLASS_SUPPORT) )
{
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
if ( sv_neo_bot_grenade_polite_smoke.GetBool() )
{
CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) );
if ( !pPlayer || !pPlayer->IsAlive() || pPlayer == me )
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
{
continue;
}
CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) );
if ( !pPlayer || !pPlayer->IsAlive() || pPlayer == me )
{
continue;
}

if ( pPlayer->InSameTeam( me ) )
{
if ( !pPlayer->IsBot() && pPlayer->GetClass() != NEO_CLASS_SUPPORT )
if ( pPlayer->InSameTeam( me ) )
{
// Avoid blocking the vision of a friendly human
// (Bots benefit from concealment without the disorientation)
return nullptr;
if ( !pPlayer->IsBot() && pPlayer->GetClass() != NEO_CLASS_SUPPORT )
{
// Avoid blocking the vision of a friendly human
// (Bots benefit from concealment without the disorientation)
return nullptr;
}
}
}
else
{
if ( pPlayer->GetClass() == NEO_CLASS_SUPPORT )
else
{
// Avoid giving an enemy with thermal vision a free smoke screen
return nullptr;
if ( pPlayer->GetClass() == NEO_CLASS_SUPPORT )
{
// Avoid giving an enemy with thermal vision a free smoke screen
return nullptr;
}
}
}
}
Expand All @@ -111,9 +116,12 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( const CNE
// Should I toss a frag grenade?
if ( pFragGrenade )
{
if ( !CNEOBotGrenadeThrowFrag::IsFragSafe( me, threat->GetLastKnownPosition() ) )
if ( sv_neo_bot_grenade_polite_frag.GetBool() )
{
return nullptr;
if ( !CNEOBotGrenadeThrowFrag::IsFragSafe( me, threat->GetLastKnownPosition() ) )
{
return nullptr;
}
}

return new CNEOBotGrenadeThrowFrag( pFragGrenade, threat );
Expand Down
11 changes: 11 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "cbase.h"
#include "bot/neo_bot.h"
#include "bot/neo_bot_path_compute.h"
#include "bot/neo_bot_path_reservation.h"
#include "bot/behavior/neo_bot_grenade_throw_frag.h"
#include "neo_gamerules.h"
#include "neo_player.h"
#include "weapon_neobasecombatweapon.h"

#include "nav_mesh.h"
#include "nav_pathfind.h"

extern ConVar sv_neo_grenade_blast_radius;
Expand Down Expand Up @@ -106,6 +108,15 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar
return THROW_TARGET_CANCEL; // risk of friendly fire
}

// Register explosive hazard at the target position
// so the throwing bot's team can avoid the trajectory and landing areas
CNavArea *area = TheNavMesh->GetNearestNavArea(m_vecTarget);
if (area)
{
int team = me->GetTeamNumber();
CNEOBotPathReservations()->AddFragHazard(area->GetID(), gpGlobals->curtime + sv_neo_grenade_fuse_timer.GetFloat(), team);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanding the hazard area around grenades by adjacent Potentially Visible Set is intended as a shorthand for avoid the area along the line through the target, as well as avoid the radiating circle around the idea detonation zone, with some leeway for the trajectory to be off as it usually is.

}

return THROW_TARGET_READY;
}

Expand Down
35 changes: 35 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "bot/behavior/neo_bot_retreat_from_grenade.h"
#include "bot/behavior/neo_bot_retreat_to_cover.h"
#include "bot/neo_bot_path_compute.h"
#include "bot/neo_bot_path_reservation.h"
#include "nav_mesh.h"
#include "sdk/sdk_basegrenade_projectile.h"

// memdbgon must be the last include file in a .cpp file!!!
Expand Down Expand Up @@ -72,6 +74,7 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor
{
m_me = me;
m_grenade = grenade;
m_onStuckPenalty = neo_bot_path_reservation_onstuck_penalty.GetFloat();
m_pGrenadeStats = dynamic_cast<CBaseGrenadeProjectile *>( grenade );
m_safeRadiusSqr = m_pGrenadeStats ? Square(m_pGrenadeStats->m_DmgRadius * sv_neo_bot_grenade_frag_safety_range_multiplier.GetFloat()) : 0.0f;

Expand All @@ -91,6 +94,20 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor
return false; // can't search if there's no threat source
}

// Skip areas that are hazardous or where bots get stuck
if ( neo_bot_path_reservation_enable.GetBool() )
{
int navAreaId = area->GetID();
if (CNEOBotPathReservations()->IsAreaHazardous(navAreaId, m_me))
{
return true;
}
if (CNEOBotPathReservations()->GetAreaAvoidPenalty(navAreaId) >= m_onStuckPenalty)
{
return true;
}
}

if ( m_pGrenadeStats )
{
if ( ( area->GetCenter() - m_pGrenadeStats->GetAbsOrigin() ).LengthSqr() < m_safeRadiusSqr * 2 )
Expand Down Expand Up @@ -138,6 +155,7 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor
CNEOBot *m_me;
CBaseEntity *m_grenade;
CBaseGrenadeProjectile *m_pGrenadeStats;
float m_onStuckPenalty;
float m_safeRadiusSqr;
CUtlVector< CNavArea * > m_coverAreaVector;
};
Expand Down Expand Up @@ -181,6 +199,19 @@ ActionResult< CNEOBot > CNEOBotRetreatFromGrenade::OnStart( CNEOBot *me, Action<
m_grenade = FindDangerousGrenade( me );
}

if ( !m_grenade ) // not a duplicate, check FindDangerousGrenade result
{
return Done("No grenade found");
}

// Register explosive hazard for the bot's team at the grenade's initial position
CNavArea *grenadeArea = TheNavMesh->GetNearestNavArea( m_grenade->GetAbsOrigin() );
if (grenadeArea)
{
int team = me->GetTeamNumber();
CNEOBotPathReservations()->AddFragHazard(grenadeArea->GetID(), gpGlobals->curtime + sv_neo_grenade_fuse_timer.GetFloat(), team);
}

// Sometimes grenades can be in a bad limbo state, so force exit eventually
m_expiryTimer.Start( sv_neo_grenade_fuse_timer.GetFloat() );

Expand Down Expand Up @@ -247,6 +278,8 @@ ActionResult< CNEOBot > CNEOBotRetreatFromGrenade::Update( CNEOBot *me, float in
//---------------------------------------------------------------------------------------------
EventDesiredResult< CNEOBot > CNEOBotRetreatFromGrenade::OnStuck( CNEOBot *me )
{
m_coverArea = FindCoverArea( me );
CNEOBotPathCompute(me, m_path, m_coverArea->GetCenter(), FASTEST_ROUTE);
return TryContinue();
}

Expand All @@ -261,6 +294,8 @@ EventDesiredResult< CNEOBot > CNEOBotRetreatFromGrenade::OnMoveToSuccess( CNEOBo
//---------------------------------------------------------------------------------------------
EventDesiredResult< CNEOBot > CNEOBotRetreatFromGrenade::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason )
{
m_coverArea = FindCoverArea( me );
CNEOBotPathCompute(me, m_path, m_coverArea->GetCenter(), FASTEST_ROUTE);
return TryContinue();
}

Expand Down
Loading