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
8 changes: 8 additions & 0 deletions src/game/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,14 @@ 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_grenade_dispatch.cpp
neo/bot/behavior/neo_bot_grenade_dispatch.h
neo/bot/behavior/neo_bot_grenade_throw.cpp
neo/bot/behavior/neo_bot_grenade_throw.h
neo/bot/behavior/neo_bot_grenade_throw_frag.cpp
neo/bot/behavior/neo_bot_grenade_throw_frag.h
neo/bot/behavior/neo_bot_grenade_throw_smoke.cpp
neo/bot/behavior/neo_bot_grenade_throw_smoke.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
Expand Down
13 changes: 13 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_attack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "team_control_point_master.h"
#include "bot/neo_bot.h"
#include "bot/behavior/neo_bot_attack.h"
#include "bot/behavior/neo_bot_grenade_dispatch.h"
#include "bot/neo_bot_path_compute.h"

#include "nav_mesh.h"
Expand Down Expand Up @@ -79,6 +80,18 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval )
// pre-cloak needs more thermoptic budget when chasing threats
me->EnableCloak(6.0f);

// Consider throwing a grenade
if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() )
{
m_grenadeThrowCooldownTimer.Start( sv_neo_bot_grenade_throw_cooldown.GetFloat() );

Action<CNEOBot> *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat );
if ( pGrenadeBehavior )
{
return SuspendFor( pGrenadeBehavior, "Throwing grenade before chasing threat!" );
}
}

if ( isUsingCloseRangeWeapon )
{
CNEOBotPathUpdateChase( me, m_chasePath, threat->GetEntity(), FASTEST_ROUTE );
Expand Down
1 change: 1 addition & 0 deletions src/game/server/neo/bot/behavior/neo_bot_attack.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ class CNEOBotAttack : public Action< CNEOBot >
private:
PathFollower m_path;
ChasePath m_chasePath;
CountdownTimer m_grenadeThrowCooldownTimer;
CountdownTimer m_repathTimer;
};
126 changes: 126 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include "cbase.h"
#include "neo_gamerules.h"
#include "neo_player.h"
#include "bot/neo_bot.h"
#include "bot/behavior/neo_bot_grenade_dispatch.h"
#include "bot/behavior/neo_bot_grenade_throw_frag.h"
#include "bot/behavior/neo_bot_grenade_throw_smoke.h"
#include "weapon_neobasecombatweapon.h"
#include "weapon_grenade.h"
#include "weapon_smokegrenade.h"

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");

//---------------------------------------------------------------------------------------------
Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot *me, const CKnownEntity *threat )
{
if (!sv_neo_bot_grenade_use_frag.GetBool() && !sv_neo_bot_grenade_use_smoke.GetBool())
{
return nullptr;
}

if ( !threat || !threat->GetEntity() || !threat->GetEntity()->IsPlayer() || !threat->GetEntity()->IsAlive() )
{
return nullptr;
}

// Prefer shooting if we have a clear line of fire
if ( me->IsLineOfFireClear( threat->GetEntity(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) )
{
return nullptr;
}

CNEO_Player *pNEOPlayer = ToNEOPlayer( me->GetEntity() );
if ( !pNEOPlayer )
{
return nullptr;
}

CWeaponGrenade *pFragGrenade = nullptr;
CWeaponSmokeGrenade *pSmokeGrenade = nullptr;

int iWeaponCount = pNEOPlayer->WeaponCount();
for ( int i=0; i<iWeaponCount; ++i )
{
CBaseCombatWeapon *pWep = pNEOPlayer->GetWeapon( i );
if ( !pWep )
{
continue;
}

CNEOBaseCombatWeapon *pNeoWep = static_cast< CNEOBaseCombatWeapon * >( pWep );
if ( pNeoWep )
{
auto bits = pNeoWep->GetNeoWepBits();
if ( sv_neo_bot_grenade_use_frag.GetBool() && (bits & NEO_WEP_FRAG_GRENADE) )
{
pFragGrenade = static_cast< CWeaponGrenade * >( pNeoWep );
if ( pSmokeGrenade )
{
break; // found both
}
}
else if ( sv_neo_bot_grenade_use_smoke.GetBool() && (bits & NEO_WEP_SMOKE_GRENADE) )
{
pSmokeGrenade = static_cast< CWeaponSmokeGrenade * >( pNeoWep );
if ( pFragGrenade )
{
break; // found both
}
}
}
}

if ( !pFragGrenade && !pSmokeGrenade )
{
return nullptr;
}

// Should I toss a smoke grenade?
if ( pSmokeGrenade )
{
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To start the conversation, I wonder if we should check if there are any human non-support players on the bot's team before throwing smoke. Bots don't have too much trouble navigating smoked areas, but I imagine humans could struggle to navigate on maps with fewer routes like bullet.

Copy link
Collaborator

@Rainyan Rainyan Feb 11, 2026

Choose a reason for hiding this comment

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

One fuzzy measure could be calculating a numeric value for how "useful" the smoke would be, based on number of nearby supports, and whether they are friendly/enemy or bot/human, and try to make it such that smokes that may hinder human friendlies are to be avoided, even if they were marginally useful for some bots. And likewise avoid smokes that give advantage to human enemies based on some metric like that.

Copy link
Contributor Author

@sunzenshen sunzenshen Feb 12, 2026

Choose a reason for hiding this comment

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

After watching a friend try the game the other evening, I decided that for the scope of this review, I'd like to make the use of smokes somewhat conservative, avoiding disoriented humans and enemies that could exploit the smoke screen. (As one may guess, the very first thing they did was walk into a smoke cloud and then immediately became lost for the duration. 🥲 )

Then another PR could explore a more sophisticated handling of smokes. For example, maybe we could calculate a path to the smoke target, estimate the movement time needed, and figure out if a player could realistically reach and view the smoke based on their class movement.

Copy link
Collaborator

@Rainyan Rainyan Feb 12, 2026

Choose a reason for hiding this comment

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

We've definitely entered the bikeshed, but: it might be interesting to try a kind of VR Chaperone style effect inside smokes, to represent the in-smoke fumbling player having tactile feedback of physically hitting/brushing against the wall (it's basically solving for the same thing IRL):

chaperone

I imagine this might be able to reuse the friendly HUD X-ray shader stuff, but I'm not too familiar with that code.


Then another PR could explore a more sophisticated handling of smokes. For example, maybe we could calculate a path the the smoke target, estimate the movement time needed, and figure out if a player could realistically reach and view the smoke based on their class movement.

Definitely, I think we should implement some kind of code to calculate the full flight path of a grenade ahead of time, and that could be used for stuff like these bot throws, but also for training modes to practice nade throws etc like in CS (the smaller PiP screens show where that nade is going to land):

cs2_grenprev

{
CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) );
if ( !pPlayer || !pPlayer->IsAlive() || pPlayer == pNEOPlayer )
{
continue;
}

if ( pPlayer->InSameTeam( pNEOPlayer ) )
{
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 )
{
// Avoid giving an enemy with thermal vision a free smoke screen
return nullptr;
}
}
}

return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat );
}

// Should I toss a frag grenade?
if ( pFragGrenade )
{
if ( !CNEOBotGrenadeThrowFrag::IsFragSafe( me, threat->GetLastKnownPosition() ) )
{
return nullptr;
}

return new CNEOBotGrenadeThrowFrag( pFragGrenade, threat );
}

return nullptr;
}
20 changes: 20 additions & 0 deletions src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef NEO_BOT_GRENADE_DISPATCH_H
#define NEO_BOT_GRENADE_DISPATCH_H
#ifdef _WIN32
#pragma once
#endif

#include "neo_bot_behavior.h"

class CNEOBot;
class CKnownEntity;

extern ConVar sv_neo_bot_grenade_throw_cooldown;

class CNEOBotGrenadeDispatch
{
public:
static Action< CNEOBot > *ChooseGrenadeThrowBehavior( CNEOBot *me, const CKnownEntity *threat );
};

#endif // NEO_BOT_GRENADE_DISPATCH_H
Loading