From 82a0401282ed1da14f5b035a2e2d0cfb1bbc33e0 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 12 Dec 2025 00:31:27 -0700 Subject: [PATCH 1/7] Bots throw grenades from cover --- src/game/server/CMakeLists.txt | 8 + .../neo/bot/behavior/neo_bot_attack.cpp | 13 ++ .../server/neo/bot/behavior/neo_bot_attack.h | 1 + .../bot/behavior/neo_bot_grenade_dispatch.cpp | 119 +++++++++++ .../bot/behavior/neo_bot_grenade_dispatch.h | 18 ++ .../bot/behavior/neo_bot_grenade_throw.cpp | 186 ++++++++++++++++++ .../neo/bot/behavior/neo_bot_grenade_throw.h | 46 +++++ .../behavior/neo_bot_grenade_throw_frag.cpp | 112 +++++++++++ .../bot/behavior/neo_bot_grenade_throw_frag.h | 25 +++ .../behavior/neo_bot_grenade_throw_smoke.cpp | 54 +++++ .../behavior/neo_bot_grenade_throw_smoke.h | 25 +++ .../bot/behavior/neo_bot_retreat_to_cover.cpp | 41 +++- .../bot/behavior/neo_bot_retreat_to_cover.h | 1 + 13 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 8aa3b870b..3157a814f 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -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 diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp index e2b47ad69..ef9169266 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -4,12 +4,14 @@ #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" extern ConVar neo_bot_path_lookahead_range; extern ConVar neo_bot_offense_must_push_time; +extern ConVar sv_neo_smoke_bloom_duration; ConVar neo_bot_aggressive( "neo_bot_aggressive", "0", FCVAR_NONE ); @@ -99,6 +101,17 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) return Done( "I lost my target!" ); } + if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) + { + m_grenadeThrowCooldownTimer.Start( sv_neo_smoke_bloom_duration.GetFloat() / 2.0f ); + + Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); + if ( pGrenadeBehavior ) + { + return SuspendFor( pGrenadeBehavior, "Throwing grenade before chasing threat!" ); + } + } + // look where we last saw him as we approach if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), me->GetMaxAttackRange() ) ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.h b/src/game/server/neo/bot/behavior/neo_bot_attack.h index 11ca7c205..a8890d07e 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.h +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.h @@ -25,5 +25,6 @@ class CNEOBotAttack : public Action< CNEOBot > private: PathFollower m_path; ChasePath m_chasePath; + CountdownTimer m_grenadeThrowCooldownTimer; CountdownTimer m_repathTimer; }; diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp new file mode 100644 index 000000000..c375fbb45 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp @@ -0,0 +1,119 @@ +#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" + +extern ConVar sv_neo_grenade_blast_radius; +extern ConVar sv_neo_grenade_fuse_timer; +extern ConVar sv_neo_grenade_throw_intensity; + +//--------------------------------------------------------------------------------------------- +Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot *me, const CKnownEntity *threat ) +{ + if ( !threat || !threat->GetEntity() || !threat->GetEntity()->IsPlayer() || !threat->GetEntity()->IsAlive() ) + { + return nullptr; + } + + CNEO_Player *pNEOPlayer = ToNEOPlayer( me->GetEntity() ); + if ( !pNEOPlayer ) + { + return nullptr; + } + + CWeaponGrenade *pFragGrenade = nullptr; + CWeaponSmokeGrenade *pSmokeGrenade = nullptr; + + for ( int i=0; iWeaponCount(); ++i ) + { + CBaseCombatWeapon *pWep = pNEOPlayer->GetWeapon( i ); + if ( !pWep ) + { + continue; + } + + if ( ( pFragGrenade = dynamic_cast< CWeaponGrenade * >( pWep ) ) ) + { + if ( pSmokeGrenade ) + { + break; // found both + } + } + else if ( ( pSmokeGrenade = dynamic_cast< CWeaponSmokeGrenade * >( pWep ) ) ) + { + if ( pFragGrenade ) + { + break; // found both + } + } + } + + if ( !pFragGrenade && !pSmokeGrenade ) + { + return nullptr; + } + + Vector vecThreatPos = threat->GetLastKnownPosition(); + + // Should I toss a frag grenade? + if ( pFragGrenade ) + { + float flDistToThreatSqr = me->GetAbsOrigin().DistToSqr( vecThreatPos ); + float flSafeRadius = sv_neo_grenade_blast_radius.GetFloat(); + float flSafeRadiusSqr = flSafeRadius * flSafeRadius; + + if ( flDistToThreatSqr < flSafeRadiusSqr ) + { + return nullptr; + } + + float flMaxThrowDist = sv_neo_grenade_throw_intensity.GetFloat() * sv_neo_grenade_fuse_timer.GetFloat(); + if ( flDistToThreatSqr > ( flMaxThrowDist * flMaxThrowDist ) ) + { + return nullptr; + } + + if ( NEORules()->IsTeamplay() ) + { + bool bTeammateTooClose = false; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer != pNEOPlayer && pPlayer->InSameTeam(me) ) + { + if ( pPlayer->GetAbsOrigin().DistToSqr( vecThreatPos ) < flSafeRadiusSqr ) + { + bTeammateTooClose = true; + break; + } + } + } + + if ( bTeammateTooClose ) + { + return nullptr; + } + } + + return new CNEOBotGrenadeThrowFrag( pFragGrenade, threat ); + } + + // Should I toss a smoke grenade? + if ( pSmokeGrenade ) + { + CNEO_Player *pThreatPlayer = ToNEOPlayer( threat->GetEntity() ); + // Only smoke if threat is NOT support (supports have thermal vision) + if ( pThreatPlayer && pThreatPlayer->GetClass() != NEO_CLASS_SUPPORT ) + { + return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); + } + } + + return nullptr; +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h new file mode 100644 index 000000000..b7d64b26f --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h @@ -0,0 +1,18 @@ +#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; + +class CNEOBotGrenadeDispatch +{ +public: + static Action< CNEOBot > *ChooseGrenadeThrowBehavior( CNEOBot *me, const CKnownEntity *threat ); +}; + +#endif // NEO_BOT_GRENADE_DISPATCH_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp new file mode 100644 index 000000000..1fe779cad --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp @@ -0,0 +1,186 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_grenade_throw.h" +#include "weapon_neobasecombatweapon.h" +#include "nav_mesh.h" +#include "nav_pathfind.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotGrenadeThrow::CNEOBotGrenadeThrow( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ) +{ + m_hGrenadeWeapon = pWeapon; + m_vecTarget = vec3_invalid; + + if ( threat ) + { + m_hThreatGrenadeTarget = threat->GetEntity(); + m_vecThreatLastKnownPos = threat->GetLastKnownPosition(); + } + else + { + m_hThreatGrenadeTarget = nullptr; + m_vecThreatLastKnownPos = vec3_invalid; + } +} + +//--------------------------------------------------------------------------------------------- +// Used to anticipate the emergence point from cover ahead of a path +// Is calculated in reverse of path to find the farthest point from bot +// (assuming "familiar" position is closer to the bot than the "obscured" position) +Vector CNEOBotGrenadeThrow::FindEmergencePointAlongPath( CNEOBot *me, const Vector &familiarPos, const Vector &obscuredPos ) +{ + CNavArea *familiarArea = TheNavMesh->GetNavArea( familiarPos ); + CNavArea *obscuredArea = TheNavMesh->GetNavArea( obscuredPos ); + + if ( familiarArea && obscuredArea ) + { + ShortestPathCost cost; + Vector vecGoal = obscuredPos; + if ( NavAreaBuildPath( familiarArea, obscuredArea, &vecGoal, cost ) ) + { + // search backwards from obscured position to find the first point visible to me + for ( CNavArea *area = obscuredArea; area; area = area->GetParent() ) + { + Vector vecTest = area->GetCenter(); + + if ( me->IsLineOfFireClear( vecTest, CNEOBot::LINE_OF_FIRE_FLAGS_SHOTGUN ) ) + { + return vecTest; + } + + if ( area == familiarArea ) + { + break; + } + } + } + } + + return vec3_invalid; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotGrenadeThrow::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + if ( !m_hThreatGrenadeTarget.Get() ) + { + return Done( "Targeted Threat is null" ); + } + + if ( !m_hThreatGrenadeTarget->IsAlive() ) + { + return Done( "Targeted threat is dead" ); + } + + if ( m_hGrenadeWeapon.Get() ) + { + me->PushRequiredWeapon( m_hGrenadeWeapon ); + } + + m_giveUpTimer.Start( 5.0f ); + m_bPinPulled = false; + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval ) +{ + CNEOBaseCombatWeapon *pWep = m_hGrenadeWeapon.Get(); + if ( !pWep ) + { + return Done( "Do not have grenade" ); + } + + if ( m_vecThreatLastKnownPos == vec3_invalid ) + { + return Done( "Targeted threat position invalid" ); + } + + if ( !m_hThreatGrenadeTarget.Get() ) + { + return Done( "Targeted threat is null" ); + } + + if ( !m_hThreatGrenadeTarget->IsAlive() ) + { + return Done( "Targeted threat is dead" ); + } + + if ( m_giveUpTimer.IsElapsed() ) + { + return Done( "Grenade throw timed out" ); + } + + if ( me->GetActiveWeapon() != pWep ) + { + // Still waiting to switch + me->Weapon_Switch( pWep ); + return Continue(); + } + + // Wait for weapon switch to complete fully + if ( pWep->m_flNextPrimaryAttack > gpGlobals->curtime ) + { + return Continue(); + } + + // NEOJANK: The bots struggle throw to grenades with PressFireButton due to control quirks + // As a workaround, we decompose the action into different phases of the bot behavior + // Part 1: Primary attack which actually only pulls the pin + // Part 2: See below ItemPostFrame() which throws the grenade and triggers throw animation + if ( !m_bPinPulled ) + { + // Initiate pull pin animation + pWep->PrimaryAttack(); + m_pinPullTimer.Start( 0.5f ); // RETHROW_DELAY + m_bPinPulled = true; + } + + if ( !m_pinPullTimer.IsElapsed() ) + { + return Continue(); + } + + // Continue to call UpdateGrenadeTargeting after THROW_TARGET_READY result + // as subclasses may decide in the last second that a throw is too dangerous (CANCEL) + switch ( UpdateGrenadeTargeting( me, pWep ) ) + { + case THROW_TARGET_CANCEL: + return Done( "Grenade throw aborted" ); + + case THROW_TARGET_WAIT: + return Continue(); + + case THROW_TARGET_READY: + // Wait until we are aiming at the target + if ( m_vecTarget == vec3_invalid ) + { + return Done( "Invalid target coordinate" ); + } + me->GetBodyInterface()->AimHeadTowards( m_vecTarget, IBody::MANDATORY, 0.2f, nullptr, "Aiming grenade" ); + + Vector vecForward; + me->EyeVectors( &vecForward ); + Vector vecToTarget = m_vecTarget - me->GetEntity()->EyePosition(); + vecToTarget.NormalizeInPlace(); + if ( vecForward.Dot( vecToTarget ) < 0.99f ) + { + return Continue(); + } + + // NEOJANK Part 2: Throw the grenade with ItemPostFrame + pWep->ItemPostFrame(); // includes ThrowGrenade() among other triggers like animation + return Done( "Grenade throw sequence finished" ); + default: + Assert( false ); + return Done( "Unknown grenade throw outcome" ); + } +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotGrenadeThrow::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + me->PopRequiredWeapon(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h new file mode 100644 index 000000000..1767ec27b --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h @@ -0,0 +1,46 @@ +#ifndef NEO_BOT_GRENADE_THROW_H +#define NEO_BOT_GRENADE_THROW_H +#ifdef _WIN32 +#pragma once +#endif + +#include "neo_bot_behavior.h" +#include "NextBot/Path/NextBotPathFollow.h" + +class CNEOBaseCombatWeapon; +class CKnownEntity; + +class CNEOBotGrenadeThrow : public Action< CNEOBot > +{ +public: + CNEOBotGrenadeThrow( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ); + virtual ~CNEOBotGrenadeThrow() override { } + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) override; + +protected: + Vector m_vecTarget; // caches target to aim at during throw action in implementation classes + Vector m_vecThreatLastKnownPos; + CHandle< CNEOBaseCombatWeapon > m_hGrenadeWeapon; + CHandle< CBaseEntity > m_hThreatGrenadeTarget; + CountdownTimer m_giveUpTimer; + CountdownTimer m_scanTimer; + CountdownTimer m_pinPullTimer; + bool m_bPinPulled; + + enum ThrowTargetResult + { + THROW_TARGET_CANCEL = -1, + THROW_TARGET_READY = 0, + THROW_TARGET_WAIT = 1, + }; + + static Vector FindEmergencePointAlongPath( CNEOBot *me, const Vector &familiarPos, const Vector &obscuredPos ); + + virtual ThrowTargetResult UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) = 0; + +}; + +#endif // NEO_BOT_GRENADE_THROW_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp new file mode 100644 index 000000000..eb65112d7 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp @@ -0,0 +1,112 @@ +#include "cbase.h" +#include "bot/neo_bot.h" +#include "bot/neo_bot_path_compute.h" +#include "bot/behavior/neo_bot_grenade_throw_frag.h" +#include "neo_gamerules.h" +#include "neo_player.h" +#include "weapon_neobasecombatweapon.h" + +#include "nav_pathfind.h" + +extern ConVar sv_neo_grenade_blast_radius; + +CNEOBotGrenadeThrowFrag::CNEOBotGrenadeThrowFrag( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ) + : CNEOBotGrenadeThrow( pWeapon, threat ) +{ +} + +ActionResult< CNEOBot > CNEOBotGrenadeThrowFrag::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + ActionResult< CNEOBot > result = CNEOBotGrenadeThrow::OnStart( me, priorAction ); + if ( result.IsDone() ) + { + return result; + } + + m_PathFollower.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + if ( !CNEOBotPathCompute( me, m_PathFollower, m_vecThreatLastKnownPos, FASTEST_ROUTE ) ) + { + return Done( "Path to threat last known position unavailable" ); + } + + return Continue(); +} + +CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) +{ + // Should be checked by CNEOBotGrenadeThrow::Update + Assert( m_hThreatGrenadeTarget.Get() && m_vecThreatLastKnownPos != vec3_invalid ); + + const float flSafeRadius = sv_neo_grenade_blast_radius.GetFloat(); + + // Check if there is a more immediate threat interrupting my grenade throw + const CKnownEntity* pPrimaryThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + // Not using LINE_OF_FIRE_FLAGS_SHOTGUN because we want to abort grenade throw if threat could shoot us from behind glass + if (pPrimaryThreat && pPrimaryThreat->GetEntity() && me->IsLineOfFireClear(pPrimaryThreat->GetEntity(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT)) + { + // consider panic throwing the grenade at the immediate threat + m_vecTarget = pPrimaryThreat->GetLastKnownPosition(); + } + else if (m_vecTarget == vec3_invalid) + { + if (me->IsLineOfSightClear(m_vecThreatLastKnownPos)) + { + // See the last known location, but don't see threat + // Infer where the threat could have gone + // CHEAT: calculate a path from the last known position to the actual threat position + // and throw at the furthest visible point along that path + if (m_hThreatGrenadeTarget.Get()) + { + if ( m_scanTimer.IsElapsed() ) + { + Vector vecThrowTarget = FindEmergencePointAlongPath(me, m_vecThreatLastKnownPos, m_hThreatGrenadeTarget->GetAbsOrigin()); + + if (vecThrowTarget != vec3_invalid) + { + m_vecTarget = vecThrowTarget; + } + else + { + m_scanTimer.Start( 0.2f ); + } + } + } + } + + if ( m_vecTarget == vec3_invalid ) + { + // continue investigating last known position + m_PathFollower.Update(me); + return THROW_TARGET_WAIT; + } + } + Assert( m_vecTarget != vec3_invalid ); + + // Safety checks + if ( !me->IsLineOfFireClear( m_vecTarget, CNEOBot::LINE_OF_FIRE_FLAGS_SHOTGUN ) ) + { + return THROW_TARGET_CANCEL; // risk of grenade bouncing back at me + } + + if ( NEORules()->IsTeamplay() ) + { + const float flSafeRadiusSqr = flSafeRadius * flSafeRadius; + const CNEO_Player *pMePlayer = ToNEOPlayer( me->GetEntity() ); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + // Also happen to check my distance, as I am on the same team + if ( pPlayer && pPlayer->IsAlive() && pPlayer->InSameTeam( pMePlayer ) ) + { + if ( pPlayer->GetAbsOrigin().DistToSqr( m_vecTarget ) < flSafeRadiusSqr ) + { + return THROW_TARGET_CANCEL; // risk of friendly fire + } + } + } + } + + return THROW_TARGET_READY; +} + diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h new file mode 100644 index 000000000..8ecaf58ef --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h @@ -0,0 +1,25 @@ +#ifndef NEO_BOT_GRENADE_THROW_FRAG_H +#define NEO_BOT_GRENADE_THROW_FRAG_H +#ifdef _WIN32 +#pragma once +#endif + +#include "neo_bot_grenade_throw.h" + +class CNEOBotGrenadeThrowFrag : public CNEOBotGrenadeThrow +{ +public: + CNEOBotGrenadeThrowFrag( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ); + virtual ~CNEOBotGrenadeThrowFrag() override { } + + virtual const char *GetName( void ) const override { return "GrenadeThrowFrag"; } + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + +protected: + virtual ThrowTargetResult UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) override; + + PathFollower m_PathFollower; +}; + +#endif // NEO_BOT_GRENADE_THROW_FRAG_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.cpp new file mode 100644 index 000000000..c09922c48 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.cpp @@ -0,0 +1,54 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_grenade_throw_smoke.h" +#include "bot/neo_bot_path_compute.h" +#include "weapon_neobasecombatweapon.h" + +#include "nav_pathfind.h" + +CNEOBotGrenadeThrowSmoke::CNEOBotGrenadeThrowSmoke( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ) + : CNEOBotGrenadeThrow( pWeapon, threat ) +{ +} + +ActionResult< CNEOBot > CNEOBotGrenadeThrowSmoke::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + ActionResult< CNEOBot > result = CNEOBotGrenadeThrow::OnStart( me, priorAction ); + if ( result.IsDone() ) + { + return result; + } + + m_PathFollower.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + if ( !CNEOBotPathCompute( me, m_PathFollower, m_vecThreatLastKnownPos, FASTEST_ROUTE ) ) + { + return Done( "Path to threat last known position unavailable" ); + } + + return Continue(); +} + +CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowSmoke::UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) +{ + Assert( m_vecThreatLastKnownPos != vec3_invalid ); + + if (m_vecTarget == vec3_invalid) + { + Vector vecThrowTarget = FindEmergencePointAlongPath(me, me->GetAbsOrigin(), m_vecThreatLastKnownPos); + + if (vecThrowTarget != vec3_invalid) + { + m_vecTarget = vecThrowTarget; + } + else + { + // throw smoke at feet as fallback + m_vecTarget = me->GetAbsOrigin(); + } + } + + Assert( m_vecTarget != vec3_invalid ); + return THROW_TARGET_READY; +} + diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.h new file mode 100644 index 000000000..56c212245 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_smoke.h @@ -0,0 +1,25 @@ +#ifndef NEO_BOT_GRENADE_THROW_SMOKE_H +#define NEO_BOT_GRENADE_THROW_SMOKE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "neo_bot_grenade_throw.h" + +class CNEOBotGrenadeThrowSmoke : public CNEOBotGrenadeThrow +{ +public: + CNEOBotGrenadeThrowSmoke( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ); + virtual ~CNEOBotGrenadeThrowSmoke() override { } + + virtual const char *GetName( void ) const override { return "GrenadeThrowSmoke"; } + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + +protected: + virtual ThrowTargetResult UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) override; + + PathFollower m_PathFollower; +}; + +#endif // NEO_BOT_GRENADE_THROW_SMOKE_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index 57736ded6..ba4d390e8 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -2,11 +2,14 @@ #include "cbase.h" #include "neo_player.h" +#include "neo_smokelineofsightblocker.h" #include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_grenade_dispatch.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/neo_bot_path_compute.h" extern ConVar neo_bot_path_lookahead_range; +extern ConVar sv_neo_smoke_bloom_duration; ConVar neo_bot_retreat_to_cover_range( "neo_bot_retreat_to_cover_range", "1000", FCVAR_CHEAT ); ConVar neo_bot_debug_retreat_to_cover( "neo_bot_debug_retreat_to_cover", "0", FCVAR_CHEAT ); ConVar neo_bot_wait_in_cover_min_time( "neo_bot_wait_in_cover_min_time", "1", FCVAR_CHEAT ); @@ -53,7 +56,33 @@ class CTestAreaAgainstThreats : public IVision::IForEachKnownEntity { // is area visible by known threat if ( m_area->IsPotentiallyVisible( threatArea ) ) - ++m_exposedThreatCount; + { + // Is there smoke in this area that I can use for concealment? + bool bObscuredBySmoke = false; + CNEO_Player *pThreatPlayer = ToNEOPlayer( known.GetEntity() ); + // Support class can see through smoke + if ( pThreatPlayer && (pThreatPlayer->GetClass() != NEO_CLASS_SUPPORT) ) + { + ScopedSmokeLOS smokeScope( false ); + + Vector vecThreatEye = known.GetLastKnownPosition() + pThreatPlayer->GetViewOffset(); + Vector vecCandidateArea = m_area->GetCenter() + m_me->GetViewOffset(); + + trace_t tr; + CTraceFilterSimple filter( known.GetEntity(), COLLISION_GROUP_NONE); + UTIL_TraceLine( vecThreatEye, vecCandidateArea, MASK_BLOCKLOS, &filter, &tr ); + + if ( tr.fraction < 1.0f ) + { + bObscuredBySmoke = true; + } + } + + if ( !bObscuredBySmoke ) + { + ++m_exposedThreatCount; + } + } } } @@ -214,6 +243,16 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv if ( threat ) { + if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) + { + m_grenadeThrowCooldownTimer.Start( sv_neo_smoke_bloom_duration.GetFloat() / 2.0f ); + Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); + if ( pGrenadeBehavior ) + { + return SuspendFor( pGrenadeBehavior, "Throwing grenade from cover!" ); + } + } + // threats are still visible - find new cover m_coverArea = FindCoverArea( me ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h index 615571dfb..f437d41f4 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.h @@ -25,6 +25,7 @@ class CNEOBotRetreatToCover : public Action< CNEOBot > CountdownTimer m_repathTimer; CNavArea *m_coverArea; + CountdownTimer m_grenadeThrowCooldownTimer; CountdownTimer m_waitInCoverTimer; CNavArea *FindCoverArea( CNEOBot *me ); From 1a7e2907af8bfe448c45c9cea762247800544da8 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Feb 2026 02:29:07 -0700 Subject: [PATCH 2/7] ConVar options to disable bots throwing nades --- .../neo/bot/behavior/neo_bot_grenade_dispatch.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp index c375fbb45..965a0cdff 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp @@ -13,6 +13,9 @@ extern ConVar sv_neo_grenade_blast_radius; extern ConVar sv_neo_grenade_fuse_timer; extern ConVar sv_neo_grenade_throw_intensity; +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); + //--------------------------------------------------------------------------------------------- Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot *me, const CKnownEntity *threat ) { @@ -27,6 +30,11 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * return nullptr; } + if (!sv_neo_bot_grenade_use_frag.GetBool() && !sv_neo_bot_grenade_use_smoke.GetBool()) + { + return nullptr; + } + CWeaponGrenade *pFragGrenade = nullptr; CWeaponSmokeGrenade *pSmokeGrenade = nullptr; @@ -38,14 +46,14 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * continue; } - if ( ( pFragGrenade = dynamic_cast< CWeaponGrenade * >( pWep ) ) ) + if ( sv_neo_bot_grenade_use_frag.GetBool() && ( pFragGrenade = dynamic_cast< CWeaponGrenade * >( pWep ) ) ) { if ( pSmokeGrenade ) { break; // found both } } - else if ( ( pSmokeGrenade = dynamic_cast< CWeaponSmokeGrenade * >( pWep ) ) ) + else if ( sv_neo_bot_grenade_use_smoke.GetBool() && ( pSmokeGrenade = dynamic_cast< CWeaponSmokeGrenade * >( pWep ) ) ) { if ( pFragGrenade ) { From 961bf0a9664950a738faa5556efe3c44fe1dbc6b Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Feb 2026 02:33:35 -0700 Subject: [PATCH 3/7] Fix jump to case label error caused by variable initialization in switch --- src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp index 1fe779cad..1f9c9129b 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp @@ -154,6 +154,7 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval return Continue(); case THROW_TARGET_READY: + { // Wait until we are aiming at the target if ( m_vecTarget == vec3_invalid ) { @@ -173,6 +174,8 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval // NEOJANK Part 2: Throw the grenade with ItemPostFrame pWep->ItemPostFrame(); // includes ThrowGrenade() among other triggers like animation return Done( "Grenade throw sequence finished" ); + } + default: Assert( false ); return Done( "Unknown grenade throw outcome" ); From 63f3f0b24ca30bb246ebeac8cc1dc55234c46f5d Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Feb 2026 12:30:35 -0700 Subject: [PATCH 4/7] Safety related tweaks --- .../bot/behavior/neo_bot_grenade_dispatch.cpp | 46 ++++++++----- .../bot/behavior/neo_bot_grenade_throw.cpp | 68 ++++++++++--------- .../neo/bot/behavior/neo_bot_grenade_throw.h | 1 - .../behavior/neo_bot_grenade_throw_frag.cpp | 12 +++- .../bot/behavior/neo_bot_grenade_throw_frag.h | 2 + .../behavior/neo_bot_retreat_from_grenade.cpp | 5 +- .../bot/behavior/neo_bot_retreat_to_cover.cpp | 25 ++++--- 7 files changed, 95 insertions(+), 64 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp index 965a0cdff..8254b8e88 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp @@ -19,18 +19,18 @@ ConVar sv_neo_bot_grenade_use_smoke("sv_neo_bot_grenade_use_smoke", "1", FCVAR_N //--------------------------------------------------------------------------------------------- Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot *me, const CKnownEntity *threat ) { - if ( !threat || !threat->GetEntity() || !threat->GetEntity()->IsPlayer() || !threat->GetEntity()->IsAlive() ) + if (!sv_neo_bot_grenade_use_frag.GetBool() && !sv_neo_bot_grenade_use_smoke.GetBool()) { return nullptr; } - CNEO_Player *pNEOPlayer = ToNEOPlayer( me->GetEntity() ); - if ( !pNEOPlayer ) + if ( !threat || !threat->GetEntity() || !threat->GetEntity()->IsPlayer() || !threat->GetEntity()->IsAlive() ) { return nullptr; } - if (!sv_neo_bot_grenade_use_frag.GetBool() && !sv_neo_bot_grenade_use_smoke.GetBool()) + CNEO_Player *pNEOPlayer = ToNEOPlayer( me->GetEntity() ); + if ( !pNEOPlayer ) { return nullptr; } @@ -69,11 +69,36 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * Vector vecThreatPos = threat->GetLastKnownPosition(); + // Should I toss a smoke grenade? + if ( pSmokeGrenade ) + { + if ( pNEOPlayer->GetClass() == NEO_CLASS_SUPPORT ) + { + // I can see through smoke + return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); + } + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetClass() == NEO_CLASS_SUPPORT ) + { + if ( !pPlayer->InSameTeam(pNEOPlayer) ) + { + return nullptr; // Enemy support could see through smoke + } + } + } + + // Enemy team does not have Support players in the field + return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); + } + // Should I toss a frag grenade? if ( pFragGrenade ) { float flDistToThreatSqr = me->GetAbsOrigin().DistToSqr( vecThreatPos ); - float flSafeRadius = sv_neo_grenade_blast_radius.GetFloat(); + float flSafeRadius = CNEOBotGrenadeThrowFrag::GetFragSafetyRadius(); float flSafeRadiusSqr = flSafeRadius * flSafeRadius; if ( flDistToThreatSqr < flSafeRadiusSqr ) @@ -112,16 +137,5 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * return new CNEOBotGrenadeThrowFrag( pFragGrenade, threat ); } - // Should I toss a smoke grenade? - if ( pSmokeGrenade ) - { - CNEO_Player *pThreatPlayer = ToNEOPlayer( threat->GetEntity() ); - // Only smoke if threat is NOT support (supports have thermal vision) - if ( pThreatPlayer && pThreatPlayer->GetClass() != NEO_CLASS_SUPPORT ) - { - return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); - } - } - return nullptr; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp index 1f9c9129b..cee388c00 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp @@ -126,64 +126,68 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval return Continue(); } - // NEOJANK: The bots struggle throw to grenades with PressFireButton due to control quirks - // As a workaround, we decompose the action into different phases of the bot behavior - // Part 1: Primary attack which actually only pulls the pin - // Part 2: See below ItemPostFrame() which throws the grenade and triggers throw animation - if ( !m_bPinPulled ) - { - // Initiate pull pin animation - pWep->PrimaryAttack(); - m_pinPullTimer.Start( 0.5f ); // RETHROW_DELAY - m_bPinPulled = true; - } - - if ( !m_pinPullTimer.IsElapsed() ) - { - return Continue(); - } + ThrowTargetResult result = UpdateGrenadeTargeting( me, pWep ); - // Continue to call UpdateGrenadeTargeting after THROW_TARGET_READY result - // as subclasses may decide in the last second that a throw is too dangerous (CANCEL) - switch ( UpdateGrenadeTargeting( me, pWep ) ) + // Subclasses may decide in the last second that a throw is too dangerous (CANCEL) + if ( result == THROW_TARGET_CANCEL ) { - case THROW_TARGET_CANCEL: return Done( "Grenade throw aborted" ); + } - case THROW_TARGET_WAIT: - return Continue(); + bool bAimOnTarget = false; - case THROW_TARGET_READY: + if ( result == THROW_TARGET_READY ) { - // Wait until we are aiming at the target if ( m_vecTarget == vec3_invalid ) { return Done( "Invalid target coordinate" ); } + me->GetBodyInterface()->AimHeadTowards( m_vecTarget, IBody::MANDATORY, 0.2f, nullptr, "Aiming grenade" ); + // NEOJANK: The bots struggle to throw grenades with PressFireButton due to control quirks. + // As a workaround, we decompose the action into different phases of the bot behavior. + // This also allows us to run aiming logic in parallel with the pin-pull animation. + + // PrimaryAttack which actually only pulls the pin + if (!m_bPinPulled) + { + // Initiate pull pin animation + pWep->PrimaryAttack(); + m_bPinPulled = true; + } + Vector vecForward; me->EyeVectors( &vecForward ); Vector vecToTarget = m_vecTarget - me->GetEntity()->EyePosition(); vecToTarget.NormalizeInPlace(); - if ( vecForward.Dot( vecToTarget ) < 0.99f ) + + if ( vecForward.Dot( vecToTarget ) >= 0.95f ) { - return Continue(); + bAimOnTarget = true; } + } - // NEOJANK Part 2: Throw the grenade with ItemPostFrame + // Check Weapon Readiness + // m_flNextPrimaryAttack is set by PrimaryAttack() to block firing during the pin-pull anim. + bool bWeaponReady = ( pWep->m_flNextPrimaryAttack <= gpGlobals->curtime ); + + // Execute Throw + // Only throw if both aimed correctly AND the weapon ready animation has finished. + if ( bWeaponReady && bAimOnTarget ) + { + // Throw the grenade with ItemPostFrame pWep->ItemPostFrame(); // includes ThrowGrenade() among other triggers like animation return Done( "Grenade throw sequence finished" ); } - - default: - Assert( false ); - return Done( "Unknown grenade throw outcome" ); - } + + return Continue(); } //--------------------------------------------------------------------------------------------- void CNEOBotGrenadeThrow::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) { me->PopRequiredWeapon(); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + me->EquipBestWeaponForThreat( threat ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h index 1767ec27b..0ba625c83 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.h @@ -27,7 +27,6 @@ class CNEOBotGrenadeThrow : public Action< CNEOBot > CHandle< CBaseEntity > m_hThreatGrenadeTarget; CountdownTimer m_giveUpTimer; CountdownTimer m_scanTimer; - CountdownTimer m_pinPullTimer; bool m_bPinPulled; enum ThrowTargetResult diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp index eb65112d7..ef98e70ff 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp @@ -10,6 +10,9 @@ extern ConVar sv_neo_grenade_blast_radius; +ConVar sv_neo_bot_grenade_frag_safety_range_multiplier("sv_neo_bot_grenade_frag_safety_range_multiplier", "1.2", + FCVAR_NONE, "Multiplier for frag grenade blast radius safety check", true, 0.1, false, 0); + CNEOBotGrenadeThrowFrag::CNEOBotGrenadeThrowFrag( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ) : CNEOBotGrenadeThrow( pWeapon, threat ) { @@ -37,8 +40,6 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar // Should be checked by CNEOBotGrenadeThrow::Update Assert( m_hThreatGrenadeTarget.Get() && m_vecThreatLastKnownPos != vec3_invalid ); - const float flSafeRadius = sv_neo_grenade_blast_radius.GetFloat(); - // Check if there is a more immediate threat interrupting my grenade throw const CKnownEntity* pPrimaryThreat = me->GetVisionInterface()->GetPrimaryKnownThreat(); // Not using LINE_OF_FIRE_FLAGS_SHOTGUN because we want to abort grenade throw if threat could shoot us from behind glass @@ -90,6 +91,8 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar if ( NEORules()->IsTeamplay() ) { + + const float flSafeRadius = GetFragSafetyRadius(); const float flSafeRadiusSqr = flSafeRadius * flSafeRadius; const CNEO_Player *pMePlayer = ToNEOPlayer( me->GetEntity() ); @@ -110,3 +113,8 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar return THROW_TARGET_READY; } +float CNEOBotGrenadeThrowFrag::GetFragSafetyRadius() +{ + return sv_neo_grenade_blast_radius.GetFloat() * sv_neo_bot_grenade_frag_safety_range_multiplier.GetFloat(); +} + diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h index 8ecaf58ef..6b46e484a 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h @@ -16,6 +16,8 @@ class CNEOBotGrenadeThrowFrag : public CNEOBotGrenadeThrow virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + static float GetFragSafetyRadius(); + protected: virtual ThrowTargetResult UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) override; diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp index f0512ec6f..ffa668165 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_from_grenade.cpp @@ -10,7 +10,8 @@ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" -ConVar neo_bot_retreat_from_grenade_range( "neo_bot_retreat_from_grenade_range", "1000", FCVAR_CHEAT ); +extern ConVar sv_neo_bot_grenade_frag_safety_range_multiplier; +ConVar neo_bot_retreat_from_grenade_range( "neo_bot_retreat_from_grenade_range", "2000", FCVAR_CHEAT ); ConVar neo_bot_debug_retreat_from_grenade( "neo_bot_debug_retreat_from_grenade", "0", FCVAR_CHEAT ); @@ -32,7 +33,7 @@ class CSearchForCoverFromGrenade : public ISearchSurroundingAreasFunctor m_me = me; m_grenade = grenade; m_pGrenadeStats = dynamic_cast( grenade ); - m_safeRadiusSqr = m_pGrenadeStats ? Square(m_pGrenadeStats->m_DmgRadius * 2.0f) : 0.0f; + m_safeRadiusSqr = m_pGrenadeStats ? Square(m_pGrenadeStats->m_DmgRadius * sv_neo_bot_grenade_frag_safety_range_multiplier.GetFloat()) : 0.0f; if ( neo_bot_debug_retreat_from_grenade.GetBool() ) TheNavMesh->ClearSelectedSet(); diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index ba4d390e8..6b95f71f8 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -9,7 +9,6 @@ #include "bot/neo_bot_path_compute.h" extern ConVar neo_bot_path_lookahead_range; -extern ConVar sv_neo_smoke_bloom_duration; ConVar neo_bot_retreat_to_cover_range( "neo_bot_retreat_to_cover_range", "1000", FCVAR_CHEAT ); ConVar neo_bot_debug_retreat_to_cover( "neo_bot_debug_retreat_to_cover", "0", FCVAR_CHEAT ); ConVar neo_bot_wait_in_cover_min_time( "neo_bot_wait_in_cover_min_time", "1", FCVAR_CHEAT ); @@ -236,6 +235,20 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv } #endif + // If line of fire broken, consider throwing a grenade + if ( threat && !me->IsLineOfFireClear( threat->GetEntity(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) + { + m_grenadeThrowCooldownTimer.Start( 10.0f ); + Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); + if ( pGrenadeBehavior ) + { + return SuspendFor( pGrenadeBehavior, "Throwing grenade while taking cover!" ); + } + } + } + // move to cover, or stop if we've found opportunistic cover (no visible threats right now) if ( me->GetLastKnownArea() == m_coverArea || !threat ) { @@ -243,16 +256,6 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv if ( threat ) { - if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) - { - m_grenadeThrowCooldownTimer.Start( sv_neo_smoke_bloom_duration.GetFloat() / 2.0f ); - Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); - if ( pGrenadeBehavior ) - { - return SuspendFor( pGrenadeBehavior, "Throwing grenade from cover!" ); - } - } - // threats are still visible - find new cover m_coverArea = FindCoverArea( me ); From 8834cd9a7ed0df3218d9a2ee1817affabae5d325 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sun, 8 Feb 2026 13:54:37 -0700 Subject: [PATCH 5/7] Weekend self review session --- .../neo/bot/behavior/neo_bot_attack.cpp | 23 +++-- .../bot/behavior/neo_bot_grenade_dispatch.cpp | 76 +++++---------- .../bot/behavior/neo_bot_grenade_dispatch.h | 2 + .../bot/behavior/neo_bot_grenade_throw.cpp | 63 +++++++----- .../behavior/neo_bot_grenade_throw_frag.cpp | 97 ++++++++++++------- .../bot/behavior/neo_bot_grenade_throw_frag.h | 1 + .../bot/behavior/neo_bot_retreat_to_cover.cpp | 2 +- 7 files changed, 142 insertions(+), 122 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp index ef9169266..d93c86ea1 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -11,7 +11,6 @@ extern ConVar neo_bot_path_lookahead_range; extern ConVar neo_bot_offense_must_push_time; -extern ConVar sv_neo_smoke_bloom_duration; ConVar neo_bot_aggressive( "neo_bot_aggressive", "0", FCVAR_NONE ); @@ -81,6 +80,17 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) // pre-cloak needs more thermoptic budget when chasing threats me->EnableCloak(6.0f); + if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) + { + m_grenadeThrowCooldownTimer.Start( sv_neo_bot_grenade_throw_cooldown.GetFloat() ); + + Action *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 ); @@ -101,17 +111,6 @@ ActionResult< CNEOBot > CNEOBotAttack::Update( CNEOBot *me, float interval ) return Done( "I lost my target!" ); } - if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) - { - m_grenadeThrowCooldownTimer.Start( sv_neo_smoke_bloom_duration.GetFloat() / 2.0f ); - - Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); - if ( pGrenadeBehavior ) - { - return SuspendFor( pGrenadeBehavior, "Throwing grenade before chasing threat!" ); - } - } - // look where we last saw him as we approach if ( me->IsRangeLessThan( threat->GetLastKnownPosition(), me->GetMaxAttackRange() ) ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp index 8254b8e88..f3467e55b 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp @@ -9,12 +9,9 @@ #include "weapon_grenade.h" #include "weapon_smokegrenade.h" -extern ConVar sv_neo_grenade_blast_radius; -extern ConVar sv_neo_grenade_fuse_timer; -extern ConVar sv_neo_grenade_throw_intensity; - 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 ) @@ -38,7 +35,8 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * CWeaponGrenade *pFragGrenade = nullptr; CWeaponSmokeGrenade *pSmokeGrenade = nullptr; - for ( int i=0; iWeaponCount(); ++i ) + int iWeaponCount = pNEOPlayer->WeaponCount(); + for ( int i=0; iGetWeapon( i ); if ( !pWep ) @@ -46,18 +44,25 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * continue; } - if ( sv_neo_bot_grenade_use_frag.GetBool() && ( pFragGrenade = dynamic_cast< CWeaponGrenade * >( pWep ) ) ) + CNEOBaseCombatWeapon *pNeoWep = static_cast< CNEOBaseCombatWeapon * >( pWep ); + if ( pNeoWep ) { - if ( pSmokeGrenade ) + auto bits = pNeoWep->GetNeoWepBits(); + if ( sv_neo_bot_grenade_use_frag.GetBool() && (bits & NEO_WEP_FRAG_GRENADE) ) { - break; // found both + pFragGrenade = static_cast< CWeaponGrenade * >( pNeoWep ); + if ( pSmokeGrenade ) + { + break; // found both + } } - } - else if ( sv_neo_bot_grenade_use_smoke.GetBool() && ( pSmokeGrenade = dynamic_cast< CWeaponSmokeGrenade * >( pWep ) ) ) - { - if ( pFragGrenade ) + else if ( sv_neo_bot_grenade_use_smoke.GetBool() && (bits & NEO_WEP_SMOKE_GRENADE) ) { - break; // found both + pSmokeGrenade = static_cast< CWeaponSmokeGrenade * >( pNeoWep ); + if ( pFragGrenade ) + { + break; // found both + } } } } @@ -67,8 +72,6 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * return nullptr; } - Vector vecThreatPos = threat->GetLastKnownPosition(); - // Should I toss a smoke grenade? if ( pSmokeGrenade ) { @@ -78,6 +81,7 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); } + bool bEnemySupportInField = false; for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); @@ -85,55 +89,27 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * { if ( !pPlayer->InSameTeam(pNEOPlayer) ) { - return nullptr; // Enemy support could see through smoke + bEnemySupportInField = true; // Enemy support could see through smoke + break; } } } // Enemy team does not have Support players in the field - return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); + if (!bEnemySupportInField) + { + return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); + } } // Should I toss a frag grenade? if ( pFragGrenade ) { - float flDistToThreatSqr = me->GetAbsOrigin().DistToSqr( vecThreatPos ); - float flSafeRadius = CNEOBotGrenadeThrowFrag::GetFragSafetyRadius(); - float flSafeRadiusSqr = flSafeRadius * flSafeRadius; - - if ( flDistToThreatSqr < flSafeRadiusSqr ) - { - return nullptr; - } - - float flMaxThrowDist = sv_neo_grenade_throw_intensity.GetFloat() * sv_neo_grenade_fuse_timer.GetFloat(); - if ( flDistToThreatSqr > ( flMaxThrowDist * flMaxThrowDist ) ) + if ( !CNEOBotGrenadeThrowFrag::IsFragSafe( me, threat->GetLastKnownPosition() ) ) { return nullptr; } - if ( NEORules()->IsTeamplay() ) - { - bool bTeammateTooClose = false; - for ( int i = 1; i <= gpGlobals->maxClients; i++ ) - { - CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( pPlayer && pPlayer->IsAlive() && pPlayer != pNEOPlayer && pPlayer->InSameTeam(me) ) - { - if ( pPlayer->GetAbsOrigin().DistToSqr( vecThreatPos ) < flSafeRadiusSqr ) - { - bTeammateTooClose = true; - break; - } - } - } - - if ( bTeammateTooClose ) - { - return nullptr; - } - } - return new CNEOBotGrenadeThrowFrag( pFragGrenade, threat ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h index b7d64b26f..1bf4f03d0 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.h @@ -9,6 +9,8 @@ class CNEOBot; class CKnownEntity; +extern ConVar sv_neo_bot_grenade_throw_cooldown; + class CNEOBotGrenadeDispatch { public: diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp index cee388c00..e7e8e33d9 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp @@ -3,6 +3,8 @@ #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_grenade_throw.h" #include "weapon_neobasecombatweapon.h" +#include "weapon_grenade.h" +#include "weapon_smokegrenade.h" #include "nav_mesh.h" #include "nav_pathfind.h" @@ -51,7 +53,7 @@ Vector CNEOBotGrenadeThrow::FindEmergencePointAlongPath( CNEOBot *me, const Vect if ( area == familiarArea ) { - break; + return vec3_invalid; } } } @@ -113,6 +115,8 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval return Done( "Grenade throw timed out" ); } + me->EnableCloak(3.0f); + if ( me->GetActiveWeapon() != pWep ) { // Still waiting to switch @@ -126,9 +130,21 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval return Continue(); } + // NEOJANK: The bots struggle to throw grenades with PressFireButton due to control quirks. + // As a workaround, we decompose the action into different phases of the bot behavior. + // This also allows us to run aiming logic in parallel with the pin-pull animation. + if (!m_bPinPulled) + { + // Just play the animation. Do NOT call PrimaryAttack, as that sets up the weapon to + // auto-throw via ItemPostFrame, which we want to control manually here. + pWep->SendWeaponAnim( ACT_VM_PULLPIN ); + pWep->m_flTimeWeaponIdle = FLT_MAX; // Don't let idle anims interrupt us + m_bPinPulled = true; + } + ThrowTargetResult result = UpdateGrenadeTargeting( me, pWep ); - // Subclasses may decide in the last second that a throw is too dangerous (CANCEL) + // Subclasses may decide in the last second that a throw is too dangerous if ( result == THROW_TARGET_CANCEL ) { return Done( "Grenade throw aborted" ); @@ -145,18 +161,6 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval me->GetBodyInterface()->AimHeadTowards( m_vecTarget, IBody::MANDATORY, 0.2f, nullptr, "Aiming grenade" ); - // NEOJANK: The bots struggle to throw grenades with PressFireButton due to control quirks. - // As a workaround, we decompose the action into different phases of the bot behavior. - // This also allows us to run aiming logic in parallel with the pin-pull animation. - - // PrimaryAttack which actually only pulls the pin - if (!m_bPinPulled) - { - // Initiate pull pin animation - pWep->PrimaryAttack(); - m_bPinPulled = true; - } - Vector vecForward; me->EyeVectors( &vecForward ); Vector vecToTarget = m_vecTarget - me->GetEntity()->EyePosition(); @@ -168,16 +172,29 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval } } - // Check Weapon Readiness - // m_flNextPrimaryAttack is set by PrimaryAttack() to block firing during the pin-pull anim. - bool bWeaponReady = ( pWep->m_flNextPrimaryAttack <= gpGlobals->curtime ); - - // Execute Throw - // Only throw if both aimed correctly AND the weapon ready animation has finished. - if ( bWeaponReady && bAimOnTarget ) + // NEO JANK: Force the throwing phase if aimed. + // We ignore bWeaponReady/m_flNextPrimaryAttack because we are manually controlling the throw + // and want to bypass the weapon's internal scheduled throw to ensure it happens NOW. + if ( bAimOnTarget ) { - // Throw the grenade with ItemPostFrame - pWep->ItemPostFrame(); // includes ThrowGrenade() among other triggers like animation + // Play first person throw animation + pWep->SendWeaponAnim( ACT_VM_THROW ); + + CNEO_Player* pPlayer = ToNEOPlayer(me->GetEntity()); + if ( pWep->GetNeoWepBits() & NEO_WEP_FRAG_GRENADE ) + { + static_cast(pWep)->ThrowGrenade( pPlayer ); + } + else if ( pWep->GetNeoWepBits() & NEO_WEP_SMOKE_GRENADE ) + { + static_cast(pWep)->ThrowGrenade( pPlayer ); + } + else + { + DevMsg( "CNEOBotGrenadeThrow: Unknown grenade type!\n" ); + Assert(0); + } + return Done( "Grenade throw sequence finished" ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp index ef98e70ff..009c0734d 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp @@ -9,6 +9,8 @@ #include "nav_pathfind.h" extern ConVar sv_neo_grenade_blast_radius; +extern ConVar sv_neo_grenade_fuse_timer; +extern ConVar sv_neo_grenade_throw_intensity; ConVar sv_neo_bot_grenade_frag_safety_range_multiplier("sv_neo_bot_grenade_frag_safety_range_multiplier", "1.2", FCVAR_NONE, "Multiplier for frag grenade blast radius safety check", true, 0.1, false, 0); @@ -37,7 +39,7 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrowFrag::OnStart( CNEOBot *me, Action< C CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) { - // Should be checked by CNEOBotGrenadeThrow::Update + // Should be checked by CNEOBotGrenadeThrow::Update by this point Assert( m_hThreatGrenadeTarget.Get() && m_vecThreatLastKnownPos != vec3_invalid ); // Check if there is a more immediate threat interrupting my grenade throw @@ -50,27 +52,21 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar } else if (m_vecTarget == vec3_invalid) { - if (me->IsLineOfSightClear(m_vecThreatLastKnownPos)) + // Last known location in view, but don't see threat + // Infer where the threat could have gone + // CHEAT: calculate a path from the last known position to the actual threat position + // and throw at the furthest visible point along that path + if ( me->IsLineOfSightClear( m_vecThreatLastKnownPos ) && m_scanTimer.IsElapsed() ) { - // See the last known location, but don't see threat - // Infer where the threat could have gone - // CHEAT: calculate a path from the last known position to the actual threat position - // and throw at the furthest visible point along that path - if (m_hThreatGrenadeTarget.Get()) + Vector vecThrowTarget = FindEmergencePointAlongPath( me, m_vecThreatLastKnownPos, m_hThreatGrenadeTarget->GetAbsOrigin() ); + + if ( vecThrowTarget != vec3_invalid ) { - if ( m_scanTimer.IsElapsed() ) - { - Vector vecThrowTarget = FindEmergencePointAlongPath(me, m_vecThreatLastKnownPos, m_hThreatGrenadeTarget->GetAbsOrigin()); - - if (vecThrowTarget != vec3_invalid) - { - m_vecTarget = vecThrowTarget; - } - else - { - m_scanTimer.Start( 0.2f ); - } - } + m_vecTarget = vecThrowTarget; + } + else + { + m_scanTimer.Start( 0.2f ); } } @@ -89,32 +85,61 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar return THROW_TARGET_CANCEL; // risk of grenade bouncing back at me } - if ( NEORules()->IsTeamplay() ) + if ( !IsFragSafe( me, m_vecTarget ) ) { + return THROW_TARGET_CANCEL; // risk of friendly fire + } - const float flSafeRadius = GetFragSafetyRadius(); - const float flSafeRadiusSqr = flSafeRadius * flSafeRadius; - const CNEO_Player *pMePlayer = ToNEOPlayer( me->GetEntity() ); + return THROW_TARGET_READY; +} - for ( int i = 1; i <= gpGlobals->maxClients; i++ ) +float CNEOBotGrenadeThrowFrag::GetFragSafetyRadius() +{ + return sv_neo_grenade_blast_radius.GetFloat() * sv_neo_bot_grenade_frag_safety_range_multiplier.GetFloat(); +} + +bool CNEOBotGrenadeThrowFrag::IsFragSafe( CNEOBot *me, const Vector &vecTarget ) +{ + const float flDistToTargetSqr = me->GetAbsOrigin().DistToSqr( vecTarget ); + const float flSafeRadius = GetFragSafetyRadius(); + const float flSafeRadiusSqr = flSafeRadius * flSafeRadius; + + if ( flDistToTargetSqr < flSafeRadiusSqr ) + { + return false; // Too close to self to safely throw + } + + const float flMaxThrowDist = sv_neo_grenade_throw_intensity.GetFloat() * sv_neo_grenade_fuse_timer.GetFloat(); + if ( flDistToTargetSqr > ( flMaxThrowDist * flMaxThrowDist ) ) + { + return false; // Too far to throw + } + + if ( !NEORules()->IsTeamplay() ) + { + return true; + } + + const CNEO_Player *pMePlayer = ToNEOPlayer( me->GetEntity() ); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->InSameTeam( pMePlayer ) ) { - CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - // Also happen to check my distance, as I am on the same team - if ( pPlayer && pPlayer->IsAlive() && pPlayer->InSameTeam( pMePlayer ) ) + if ( pPlayer->GetAbsOrigin().DistToSqr( vecTarget ) < flSafeRadiusSqr ) { - if ( pPlayer->GetAbsOrigin().DistToSqr( m_vecTarget ) < flSafeRadiusSqr ) + trace_t tr; + UTIL_TraceLine( vecTarget, pPlayer->WorldSpaceCenter(), MASK_SHOT_HULL, nullptr, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction == 1.0f || tr.m_pEnt == pPlayer ) { - return THROW_TARGET_CANCEL; // risk of friendly fire + return false; // potentially in blast radius } } } } - return THROW_TARGET_READY; -} - -float CNEOBotGrenadeThrowFrag::GetFragSafetyRadius() -{ - return sv_neo_grenade_blast_radius.GetFloat() * sv_neo_bot_grenade_frag_safety_range_multiplier.GetFloat(); + return true; } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h index 6b46e484a..2f3fcaadb 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.h @@ -17,6 +17,7 @@ class CNEOBotGrenadeThrowFrag : public CNEOBotGrenadeThrow virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; static float GetFragSafetyRadius(); + static bool IsFragSafe( CNEOBot *me, const Vector &vecTarget ); protected: virtual ThrowTargetResult UpdateGrenadeTargeting( CNEOBot *me, CNEOBaseCombatWeapon *pWeapon ) override; diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index 6b95f71f8..e4f29f3f8 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -240,7 +240,7 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv { if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) { - m_grenadeThrowCooldownTimer.Start( 10.0f ); + m_grenadeThrowCooldownTimer.Start( sv_neo_bot_grenade_throw_cooldown.GetFloat() ); Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); if ( pGrenadeBehavior ) { From db218d454f7620e4de8c12dd743d30aad922271d Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Mon, 9 Feb 2026 02:48:56 -0700 Subject: [PATCH 6/7] Debug overlay for throw targeting --- .../bot/behavior/neo_bot_grenade_throw.cpp | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp index e7e8e33d9..e0d099d25 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp @@ -8,6 +8,9 @@ #include "nav_mesh.h" #include "nav_pathfind.h" +ConVar sv_neo_grenade_debug_behavior("sv_neo_grenade_debug_behavior", "0", FCVAR_CHEAT, + "Draw debug overlays for bot grenade behavior", true, 0, true, 1); + //--------------------------------------------------------------------------------------------- CNEOBotGrenadeThrow::CNEOBotGrenadeThrow( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ) { @@ -44,10 +47,29 @@ Vector CNEOBotGrenadeThrow::FindEmergencePointAlongPath( CNEOBot *me, const Vect // search backwards from obscured position to find the first point visible to me for ( CNavArea *area = obscuredArea; area; area = area->GetParent() ) { + // DEBUG: Draw emergence path + // Color: Yellow (255, 255, 0) to distinguish path analysis + if ( sv_neo_grenade_debug_behavior.GetBool() ) + { + if ( area->GetParent() ) + { + NDebugOverlay::HorzArrow( area->GetCenter(), area->GetParent()->GetCenter(), 2.0f, 255, 255, 0, 255, true, 2.0f ); + } + else + { + NDebugOverlay::Cross3D( area->GetCenter(), 16.0f, 255, 255, 0, true, 2.0f ); + } + } + Vector vecTest = area->GetCenter(); if ( me->IsLineOfFireClear( vecTest, CNEOBot::LINE_OF_FIRE_FLAGS_SHOTGUN ) ) { + // DEBUG: Draw emergence point + if ( sv_neo_grenade_debug_behavior.GetBool() ) + { + NDebugOverlay::Box( vecTest, Vector(-16,-16,-16), Vector(16,16,16), 255, 255, 0, 50, 2.0f ); + } return vecTest; } @@ -115,6 +137,26 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval return Done( "Grenade throw timed out" ); } + if ( sv_neo_grenade_debug_behavior.GetBool() && m_hThreatGrenadeTarget.Get() ) + { + // DEBUG Colors: Red = Frag, Gray = Smoke, Purple = Unknown + Vector vecStart = me->GetEntity()->WorldSpaceCenter(); + Vector vecVictim = m_hThreatGrenadeTarget->GetAbsOrigin(); + int r = 255, g = 0, b = 255; // Default purple just in case + if ( pWep->GetNeoWepBits() & NEO_WEP_FRAG_GRENADE ) + { + r = 255; g = 0; b = 0; // Red + } + else if ( pWep->GetNeoWepBits() & NEO_WEP_SMOKE_GRENADE ) + { + r = 128; g = 128; b = 128; // Gray + } + // Draw box around the bot that is considering the throw + NDebugOverlay::Box( me->GetEntity()->GetAbsOrigin(), me->GetEntity()->WorldAlignMins(), me->GetEntity()->WorldAlignMaxs(), r, g, b, 30, 0.1f ); + // Arrow from bot to threat being targeted + NDebugOverlay::HorzArrow( vecStart, vecVictim, 2.0f, r, g, b, 255, true, 0.1f ); + } + me->EnableCloak(3.0f); if ( me->GetActiveWeapon() != pWep ) @@ -171,6 +213,18 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval bAimOnTarget = true; } } + + // DEBUG: Targeting decisions + if ( sv_neo_grenade_debug_behavior.GetBool() ) + { + Vector vecStart = me->GetEntity()->WorldSpaceCenter(); + // Color: Orange (255, 128, 0) is the final ballistics target + if ( m_vecTarget != vec3_invalid ) + { + NDebugOverlay::HorzArrow( vecStart, m_vecTarget, 2.0f, 255, 128, 0, 255, true, 1.0f ); + NDebugOverlay::Box( m_vecTarget, Vector(-16,-16,-16), Vector(16,16,16), 255, 128, 0, 30, 1.0f ); + } + } // NEO JANK: Force the throwing phase if aimed. // We ignore bWeaponReady/m_flNextPrimaryAttack because we are manually controlling the throw From d0d9fc1f4996ae573ae50e25d97dabf6ff674962 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 12 Feb 2026 01:03:35 -0700 Subject: [PATCH 7/7] Reduce team obstructing use of smokes --- .../neo/bot/behavior/neo_bot_attack.cpp | 1 + .../bot/behavior/neo_bot_grenade_dispatch.cpp | 41 +++++++++++-------- .../bot/behavior/neo_bot_grenade_throw.cpp | 6 +-- .../behavior/neo_bot_grenade_throw_frag.cpp | 12 +++++- .../bot/behavior/neo_bot_retreat_to_cover.cpp | 15 +++---- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp index d93c86ea1..657bb6090 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_attack.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_attack.cpp @@ -80,6 +80,7 @@ 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() ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp index f3467e55b..e0af6cc12 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_dispatch.cpp @@ -26,6 +26,12 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * 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 ) { @@ -75,31 +81,34 @@ Action< CNEOBot > *CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( CNEOBot * // Should I toss a smoke grenade? if ( pSmokeGrenade ) { - if ( pNEOPlayer->GetClass() == NEO_CLASS_SUPPORT ) - { - // I can see through smoke - return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); - } - - bool bEnemySupportInField = false; for ( int i = 1; i <= gpGlobals->maxClients; i++ ) { CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetClass() == NEO_CLASS_SUPPORT ) + 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->InSameTeam(pNEOPlayer) ) + if ( pPlayer->GetClass() == NEO_CLASS_SUPPORT ) { - bEnemySupportInField = true; // Enemy support could see through smoke - break; + // Avoid giving an enemy with thermal vision a free smoke screen + return nullptr; } } } - // Enemy team does not have Support players in the field - if (!bEnemySupportInField) - { - return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); - } + return new CNEOBotGrenadeThrowSmoke( pSmokeGrenade, threat ); } // Should I toss a frag grenade? diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp index e0d099d25..377d682b2 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw.cpp @@ -102,7 +102,7 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::OnStart( CNEOBot *me, Action< CNEOB me->PushRequiredWeapon( m_hGrenadeWeapon ); } - m_giveUpTimer.Start( 5.0f ); + m_giveUpTimer.Start( 3.0f ); m_bPinPulled = false; return Continue(); @@ -221,8 +221,8 @@ ActionResult< CNEOBot > CNEOBotGrenadeThrow::Update( CNEOBot *me, float interval // Color: Orange (255, 128, 0) is the final ballistics target if ( m_vecTarget != vec3_invalid ) { - NDebugOverlay::HorzArrow( vecStart, m_vecTarget, 2.0f, 255, 128, 0, 255, true, 1.0f ); - NDebugOverlay::Box( m_vecTarget, Vector(-16,-16,-16), Vector(16,16,16), 255, 128, 0, 30, 1.0f ); + NDebugOverlay::HorzArrow( vecStart, m_vecTarget, 2.0f, 255, 128, 0, 255, true, 2.0f ); + NDebugOverlay::Box( m_vecTarget, Vector(-16,-16,-16), Vector(16,16,16), 255, 128, 0, 30, 2.0f ); } } diff --git a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp index 009c0734d..bbc574c68 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_grenade_throw_frag.cpp @@ -15,6 +15,9 @@ extern ConVar sv_neo_grenade_throw_intensity; ConVar sv_neo_bot_grenade_frag_safety_range_multiplier("sv_neo_bot_grenade_frag_safety_range_multiplier", "1.2", FCVAR_NONE, "Multiplier for frag grenade blast radius safety check", true, 0.1, false, 0); +ConVar sv_neo_bot_grenade_frag_max_range_ratio("sv_neo_bot_grenade_frag_max_range_ratio", "0.7", + FCVAR_NONE, "Ratio of max frag grenade throw distance to account for slowing factors", true, 0.1, true, 1); + CNEOBotGrenadeThrowFrag::CNEOBotGrenadeThrowFrag( CNEOBaseCombatWeapon *pWeapon, const CKnownEntity *threat ) : CNEOBotGrenadeThrow( pWeapon, threat ) { @@ -48,7 +51,12 @@ CNEOBotGrenadeThrow::ThrowTargetResult CNEOBotGrenadeThrowFrag::UpdateGrenadeTar if (pPrimaryThreat && pPrimaryThreat->GetEntity() && me->IsLineOfFireClear(pPrimaryThreat->GetEntity(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT)) { // consider panic throwing the grenade at the immediate threat - m_vecTarget = pPrimaryThreat->GetLastKnownPosition(); + if ( m_scanTimer.IsElapsed() ) + { + m_vecTarget = pPrimaryThreat->GetLastKnownPosition(); + CNEOBotPathCompute( me, m_PathFollower, m_vecTarget, FASTEST_ROUTE ); + m_scanTimer.Start( 0.2f ); + } } else if (m_vecTarget == vec3_invalid) { @@ -109,7 +117,7 @@ bool CNEOBotGrenadeThrowFrag::IsFragSafe( CNEOBot *me, const Vector &vecTarget ) return false; // Too close to self to safely throw } - const float flMaxThrowDist = sv_neo_grenade_throw_intensity.GetFloat() * sv_neo_grenade_fuse_timer.GetFloat(); + const float flMaxThrowDist = sv_neo_grenade_throw_intensity.GetFloat() * sv_neo_grenade_fuse_timer.GetFloat() * sv_neo_bot_grenade_frag_max_range_ratio.GetFloat(); if ( flDistToTargetSqr > ( flMaxThrowDist * flMaxThrowDist ) ) { return false; // Too far to throw diff --git a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp index e4f29f3f8..59b7f2e4f 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_retreat_to_cover.cpp @@ -235,17 +235,14 @@ ActionResult< CNEOBot > CNEOBotRetreatToCover::Update( CNEOBot *me, float interv } #endif - // If line of fire broken, consider throwing a grenade - if ( threat && !me->IsLineOfFireClear( threat->GetEntity(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + // Consider throwing a grenade + if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) { - if ( !m_grenadeThrowCooldownTimer.HasStarted() || m_grenadeThrowCooldownTimer.IsElapsed() ) + m_grenadeThrowCooldownTimer.Start( sv_neo_bot_grenade_throw_cooldown.GetFloat() ); + Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); + if ( pGrenadeBehavior ) { - m_grenadeThrowCooldownTimer.Start( sv_neo_bot_grenade_throw_cooldown.GetFloat() ); - Action *pGrenadeBehavior = CNEOBotGrenadeDispatch::ChooseGrenadeThrowBehavior( me, threat ); - if ( pGrenadeBehavior ) - { - return SuspendFor( pGrenadeBehavior, "Throwing grenade while taking cover!" ); - } + return SuspendFor( pGrenadeBehavior, "Throwing grenade while taking cover!" ); } }