From bd36b71b396a3014d4f6820049577bc84becee12 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Mon, 9 Mar 2026 23:41:44 -0600 Subject: [PATCH] Move bot breakables clearing to tactical monitor - Behavior tweaks to minimize friendly fire when attacking breakables --- src/game/server/CMakeLists.txt | 2 + .../neo/bot/behavior/neo_bot_behavior.cpp | 67 ---- .../neo/bot/behavior/neo_bot_behavior.h | 3 - .../behavior/neo_bot_path_clear_breakable.cpp | 287 ++++++++++++++++++ .../behavior/neo_bot_path_clear_breakable.h | 32 ++ .../bot/behavior/neo_bot_tactical_monitor.cpp | 8 +- src/game/server/neo/bot/neo_bot.cpp | 1 - .../server/neo/bot/neo_bot_locomotion.cpp | 10 +- src/game/server/neo/bot/neo_bot_locomotion.h | 2 - 9 files changed, 329 insertions(+), 83 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 16dfcf289f..55b5a35c4b 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1483,6 +1483,8 @@ target_sources_grouped( neo/bot/behavior/neo_bot_melee_attack.h neo/bot/behavior/neo_bot_move_to_vantage_point.cpp neo/bot/behavior/neo_bot_move_to_vantage_point.h + neo/bot/behavior/neo_bot_path_clear_breakable.cpp + neo/bot/behavior/neo_bot_path_clear_breakable.h neo/bot/behavior/neo_bot_pause.cpp neo/bot/behavior/neo_bot_pause.h neo/bot/behavior/neo_bot_retreat_to_cover.cpp diff --git a/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp b/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp index 3a9ea7e19a..9b2ac61def 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp @@ -20,7 +20,6 @@ ConVar neo_bot_path_lookahead_range( "neo_bot_path_lookahead_range", "300" ); ConVar neo_bot_sniper_aim_error( "neo_bot_sniper_aim_error", "0.01", FCVAR_CHEAT ); ConVar neo_bot_sniper_aim_steady_rate( "neo_bot_sniper_aim_steady_rate", "10", FCVAR_CHEAT ); ConVar neo_bot_debug_sniper( "neo_bot_debug_sniper", "0", FCVAR_CHEAT ); -ConVar neo_bot_fire_at_breakable_weapon_min_time( "neo_bot_fire_at_breakable_weapon_min_time", "0.4", FCVAR_CHEAT, "Minimum time to fire at breakables", true, 0.0f, true, 60.0f ); ConVar neo_bot_notice_backstab_chance( "neo_bot_notice_backstab_chance", "25", FCVAR_CHEAT ); ConVar neo_bot_notice_backstab_min_range( "neo_bot_notice_backstab_min_range", "100", FCVAR_CHEAT ); @@ -650,78 +649,12 @@ void CNEOBotMainAction::FireWeaponAtEnemy( CNEOBot *me ) const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); const bool bIgnoreThreat = (threat == nullptr || !threat->GetEntity() || !threat->IsVisibleRecently()); - if (me->GetNeoFlags() & NEO_FL_FREEZETIME) - { - me->GetLocomotionInterface()->m_bBreakBreakableInPath = false; - } - - if (me->GetLocomotionInterface()->m_bBreakBreakableInPath) - { - if (me->GetDifficulty() >= CNEOBot::HARD && !m_bPrevBreakBreakableInPath && bIgnoreThreat) - { - auto *secondaryWep = static_cast(me->Weapon_GetSlot(1)); - if (secondaryWep != myWeapon && secondaryWep && - ((secondaryWep->Clip1() + secondaryWep->m_iPrimaryAmmoCount) > 0)) - { - me->Weapon_Switch(secondaryWep); - } - } - - if (myWeapon->Clip1() <= 0) - { - me->ReloadIfLowClip(true); - m_isWaitingForFullReload = true; - } - - if (m_isWaitingForFullReload) - { - m_isWaitingForFullReload = (myWeapon->Clip1() < myWeapon->GetMaxClip1()); - } - - if (!m_isWaitingForFullReload) - { - if (me->IsContinuousFireWeapon(myWeapon)) - { - me->PressFireButton(neo_bot_fire_at_breakable_weapon_min_time.GetFloat()); - } - else - { - if (me->m_nButtons & IN_ATTACK) - { - me->ReleaseFireButton(); - } - else - { - me->PressFireButton(); - } - } - } - m_bPrevBreakBreakableInPath = true; - return; - } - // ignore non-visible threats here so we don't force a premature weapon switch if we're doing something else if (bIgnoreThreat) { return; } - // After bIgnoreThreat so it doesn't go at a continous cycle switching weapons - if (!me->GetLocomotionInterface()->m_bBreakBreakableInPath && m_bPrevBreakBreakableInPath) - { - if (me->GetDifficulty() >= CNEOBot::HARD && - (!myWeapon || - (myWeapon->GetNeoWepBits() & (NEO_WEP_MILSO | NEO_WEP_TACHI | NEO_WEP_KYLA | NEO_WEP_KNIFE | NEO_WEP_THROWABLE)))) - { - auto *primaryWeapon = static_cast(me->Weapon_GetSlot(0)); - if (primaryWeapon && (primaryWeapon->Clip1() + primaryWeapon->m_iPrimaryAmmoCount) > 0) - { - me->Weapon_Switch(primaryWeapon); - } - } - m_bPrevBreakBreakableInPath = false; - } - CNEOBot::LineOfFireFlags lofFlags = CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT; auto *neoThreat = ToNEOPlayer(threat->GetEntity()); diff --git a/src/game/server/neo/bot/behavior/neo_bot_behavior.h b/src/game/server/neo/bot/behavior/neo_bot_behavior.h index 26b271744c..9a0a4ce9c2 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_behavior.h +++ b/src/game/server/neo/bot/behavior/neo_bot_behavior.h @@ -69,7 +69,4 @@ class CNEOBotMainAction : public Action< CNEOBot >, public CNEOBotContextualQuer void Dodge( CNEOBot *me ); IntervalTimer m_undergroundTimer; - - CountdownTimer m_reevaluateClassTimer; - bool m_bPrevBreakBreakableInPath = false; }; diff --git a/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp b/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp new file mode 100644 index 0000000000..03276cf9f8 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.cpp @@ -0,0 +1,287 @@ +#include "cbase.h" + +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_path_clear_breakable.h" +#include "neo_gamerules.h" + +extern ConVar neo_bot_fire_weapon_allowed; + +ConVar neo_bot_fire_at_breakable_weapon_min_time( "neo_bot_fire_at_breakable_weapon_min_time", "0.2", FCVAR_CHEAT, + "Minimum time to fire at breakables", true, 0.0f, true, 60.0f ); + +//-------------------------------------------------------------------------------------------------------- +CBaseEntity *CNEOBotPathClearBreakable::GetBreakableInPath( CNEOBot *me ) +{ + if ( !me || !me->IsAlive() ) + { + return nullptr; + } + + if ( me->GetNeoFlags() & NEO_FL_FREEZETIME ) + { + return nullptr; + } + + if ( !neo_bot_fire_weapon_allowed.GetBool() ) + { + return nullptr; + } + + // Only enter this behavior if there is no visible threat + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->GetEntity() && threat->IsVisibleRecently() ) + { + return nullptr; + } + + const PathFollower *path = me->GetCurrentPath(); + if ( !path || !path->IsValid() ) + { + return nullptr; + } + + const Path::Segment *goal = path->GetCurrentGoal(); + if ( !goal ) + { + return nullptr; + } + + // Trace forward along the path to look for breakables + // We use a hull trace to match the bot's collision size + trace_t tr; + UTIL_TraceHull( me->GetAbsOrigin() + Vector( 0, 0, 10 ), + goal->pos + Vector( 0, 0, 10 ), + me->GetBodyInterface()->GetHullMins(), + me->GetBodyInterface()->GetHullMaxs(), + MASK_NPCSOLID, + me->GetEntity(), + COLLISION_GROUP_NONE, + &tr ); + + if ( tr.DidHit() && tr.m_pEnt ) + { + if ( me->IsAbleToBreak( tr.m_pEnt ) ) + { + return tr.m_pEnt; + } + } + + return nullptr; +} + + +//-------------------------------------------------------------------------------------------------------- +CNEOBotPathClearBreakable::CNEOBotPathClearBreakable( CBaseEntity *breakable ) +{ + m_hBreakable = breakable; +} + + +//-------------------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotPathClearBreakable::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_isWaitingForFullReload = false; + m_bDidSwitchWeapon = false; + m_bSkipKnife = false; + m_bStrafeRight = RandomInt( 0, 1 ) == 0; + m_giveUpTimer.Start( 10.0f ); + m_meleeTimeoutTimer.Start( 2.0f ); + + if ( !m_hBreakable.Get() ) + { + return Done( "No breakable target" ); + } + + // Prioritize weapons for clearing breakables: + // 1. Knife (Slot 2) + // 2. Secondary (Slot 1) if it has ammo + // 3. Current/Primary (Slot 0) as fallback + + auto *myWeapon = static_cast( me->GetActiveWeapon() ); + auto *knife = static_cast( me->Weapon_GetSlot( 2 ) ); + auto *secondaryWep = static_cast( me->Weapon_GetSlot( 1 ) ); + + if ( !m_bSkipKnife && knife && knife != myWeapon ) + { + me->Weapon_Switch( knife ); + m_bDidSwitchWeapon = true; + } + else if ( secondaryWep && secondaryWep != myWeapon && + ( ( secondaryWep->Clip1() + secondaryWep->m_iPrimaryAmmoCount ) > 0 ) ) + { + me->Weapon_Switch( secondaryWep ); + m_bDidSwitchWeapon = true; + } + + return Continue(); +} + + +//-------------------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotPathClearBreakable::Update( CNEOBot *me, float interval ) +{ + CBaseEntity *breakable = m_hBreakable.Get(); + + // If the breakable is destroyed or invalid, we're done + if ( !breakable || !breakable->IsAlive() || breakable->GetHealth() <= 0 ) + { + return Done( "Path is clear" ); + } + + if ( m_giveUpTimer.IsElapsed() ) + { + return Done( "Give up timer elapsed" ); + } + + // Double check distance - if we've moved too far away or the breakable is too far, stop + if ( me->GetRangeSquaredTo( breakable ) > Square( 500.0f ) ) + { + return Done( "Breakable is too far" ); + } + + // If freeze time started, bail out + if ( me->GetNeoFlags() & NEO_FL_FREEZETIME ) + { + return Done( "Freeze time" ); + } + + // If a visible threat appeared, bail out and let the normal combat system handle it + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->GetEntity() && threat->IsVisibleRecently() ) + { + return Done( "Threat appeared" ); + } + + auto *myWeapon = static_cast( me->GetActiveWeapon() ); + if ( !myWeapon ) + { + return Done( "No weapon" ); + } + + // Aim at the breakable + me->GetBodyInterface()->AimHeadTowards( + breakable->WorldSpaceCenter(), IBody::MANDATORY, 1.0f, nullptr, + "Attacking breakable in path" ); + + // Handle reloading if clip is empty + bool bUsesClips = myWeapon->UsesClipsForAmmo1(); + if ( bUsesClips && myWeapon->Clip1() <= 0 ) + { + me->ReleaseFireButton(); + me->PressReloadButton(); + m_isWaitingForFullReload = true; + } + + if ( m_isWaitingForFullReload ) + { + m_isWaitingForFullReload = ( bUsesClips && myWeapon->Clip1() < myWeapon->GetMaxClip1() ); + } + + // Fire at the breakable if aim is ready + if ( !m_isWaitingForFullReload && me->GetBodyInterface()->IsHeadAimingOnTarget() ) + { + // First check if friendly in line of fire + trace_t aimTr; + Vector vForward; + me->EyeVectors( &vForward ); + Vector vStart = me->EyePosition(); + Vector vEnd = vStart + vForward * 500.0f; + UTIL_TraceLine( vStart, vEnd, MASK_SHOT_HULL, me->GetEntity(), COLLISION_GROUP_NONE, &aimTr ); + + if ( aimTr.m_pEnt ) + { + if ( aimTr.m_pEnt->IsPlayer() ) + { + CNEO_Player *pPlayer = ToNEOPlayer( aimTr.m_pEnt ); + if ( pPlayer && pPlayer->InSameTeam( me->GetEntity() ) ) + { + // Avoid friendly and allow them to break breakable if same path + me->ReleaseFireButton(); + me->ReleaseForwardButton(); + me->PressBackwardButton(); + if ( m_bStrafeRight ) + { + me->PressRightButton(); + } + else + { + me->PressLeftButton(); + } + + return Continue(); + } + } + + // Distance check for knife prioritization + if ( aimTr.m_pEnt == breakable ) + { + float flDist = ( aimTr.endpos - vStart ).Length(); + if ( flDist > 50.0f ) // NEO_WEP_KNIFE_RANGE + { + m_bSkipKnife = true; + } + } + } + + // If we've been trying to melee for too long, or the target is too far for a knife + if ( ( m_meleeTimeoutTimer.IsElapsed() || ( m_bSkipKnife && ( myWeapon->GetNeoWepBits() & NEO_WEP_KNIFE ) ) ) && + ( myWeapon->GetNeoWepBits() & NEO_WEP_KNIFE ) ) + { + auto *secondaryWep = static_cast( me->Weapon_GetSlot( 1 ) ); + auto *primaryWeapon = static_cast( me->Weapon_GetSlot( 0 ) ); + + if ( secondaryWep && ( ( secondaryWep->Clip1() + secondaryWep->m_iPrimaryAmmoCount ) > 0 ) ) + { + me->Weapon_Switch( secondaryWep ); + m_bDidSwitchWeapon = true; + } + else if ( primaryWeapon && ( ( primaryWeapon->Clip1() + primaryWeapon->m_iPrimaryAmmoCount ) > 0 ) ) + { + me->Weapon_Switch( primaryWeapon ); + m_bDidSwitchWeapon = true; + } + } + + // Prevent bot from missing breakable because of leaning + me->ReleaseLeanLeftButton(); + me->ReleaseLeanRightButton(); + + if ( me->IsContinuousFireWeapon( myWeapon ) ) + { + me->PressFireButton( neo_bot_fire_at_breakable_weapon_min_time.GetFloat() ); + } + else + { + if ( me->m_nButtons & IN_ATTACK ) + { + me->ReleaseFireButton(); + } + else + { + me->PressFireButton(); + } + } + } + + return Continue(); +} + + +//-------------------------------------------------------------------------------------------------------- +void CNEOBotPathClearBreakable::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + // If we switched weapon, try to switch back to primary + if ( m_bDidSwitchWeapon ) + { + auto *myWeapon = static_cast( me->GetActiveWeapon() ); + if ( !myWeapon || + ( myWeapon->GetNeoWepBits() & ( NEO_WEP_MILSO | NEO_WEP_TACHI | NEO_WEP_KYLA | NEO_WEP_KNIFE | NEO_WEP_THROWABLE ) ) ) + { + auto *primaryWeapon = static_cast( me->Weapon_GetSlot( 0 ) ); + if ( primaryWeapon && ( primaryWeapon->Clip1() + primaryWeapon->m_iPrimaryAmmoCount ) > 0 ) + { + me->Weapon_Switch( primaryWeapon ); + } + } + } +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.h b/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.h new file mode 100644 index 0000000000..dcf75628f0 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_path_clear_breakable.h @@ -0,0 +1,32 @@ +#ifndef NEO_BOT_PATH_CLEAR_BREAKABLE_H +#define NEO_BOT_PATH_CLEAR_BREAKABLE_H +#pragma once + +#include "bot/neo_bot.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotPathClearBreakable : public Action< CNEOBot > +{ +public: + // Returns the breakable entity if one is blocking the bot's path + static CBaseEntity *GetBreakableInPath( CNEOBot *me ); + + CNEOBotPathClearBreakable( CBaseEntity *breakable ); + + 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; + + virtual const char *GetName( void ) const override { return "PathClearBreakable"; } + +private: + CHandle< CBaseEntity > m_hBreakable; + bool m_isWaitingForFullReload = false; + bool m_bDidSwitchWeapon = false; + bool m_bSkipKnife = false; + bool m_bStrafeRight = false; + CountdownTimer m_giveUpTimer; + CountdownTimer m_meleeTimeoutTimer; +}; + +#endif // NEO_BOT_PATH_CLEAR_BREAKABLE_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp index a1229e7f1d..ce47151e25 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.cpp @@ -16,6 +16,7 @@ #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/behavior/neo_bot_retreat_from_grenade.h" #include "bot/behavior/neo_bot_ladder_approach.h" +#include "bot/behavior/neo_bot_path_clear_breakable.h" #include "bot/behavior/neo_bot_pause.h" #if 0 // NEO TODO (Adam) Fix picking up weapons, search for dropped weapons to pick up ammo #include "bot/behavior/neo_bot_get_ammo.h" @@ -400,6 +401,11 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter } } + if ( CBaseEntity *breakable = CNEOBotPathClearBreakable::GetBreakableInPath( me ) ) + { + return SuspendFor( new CNEOBotPathClearBreakable( breakable ), "Clearing breakable in path" ); + } + ActionResult< CNEOBot > scavengeResult = ScavengeForPrimaryWeapon( me ); if ( scavengeResult.IsRequestingChange() ) { @@ -432,7 +438,7 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter CNEO_Player* pBotPlayer = ToNEOPlayer( me->GetEntity() ); if ( pBotPlayer && !(pBotPlayer->m_nButtons & (IN_FORWARD | IN_BACK | IN_MOVELEFT | IN_MOVERIGHT)) ) { - AvoidBumpingFriends( me ); + AvoidBumpingFriends(me); } const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 791f62c2ba..301f63a7de 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -632,7 +632,6 @@ void CNEOBot::Spawn() m_didReselectClass = false; m_isLookingAroundForEnemies = true; m_attentionFocusEntity = NULL; - GetLocomotionInterface()->m_bBreakBreakableInPath = false; m_delayedNoticeVector.RemoveAll(); diff --git a/src/game/server/neo/bot/neo_bot_locomotion.cpp b/src/game/server/neo/bot/neo_bot_locomotion.cpp index 8ef6c85108..929c22d3f5 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.cpp +++ b/src/game/server/neo/bot/neo_bot_locomotion.cpp @@ -9,8 +9,6 @@ extern ConVar falldamage; //----------------------------------------------------------------------------------------- void CNEOBotLocomotion::Update( void ) { - m_bBreakBreakableInPath = false; - CNEOBot* me = ToNEOBot( GetBot()->GetEntity() ); if ( !me || me->GetNeoFlags() & NEO_FL_FREEZETIME) { @@ -110,8 +108,6 @@ bool CNEOBotLocomotion::IsAreaTraversable( const CNavArea* area ) const //----------------------------------------------------------------------------------------- bool CNEOBotLocomotion::IsEntityTraversable( CBaseEntity* obstacle, TraverseWhenType when ) const { - m_bBreakBreakableInPath = false; - // assume all players are "traversable" in that they will move or can be killed if ( obstacle && obstacle->IsPlayer() ) { @@ -142,9 +138,5 @@ bool CNEOBotLocomotion::IsEntityTraversable( CBaseEntity* obstacle, TraverseWhen } const bool bIsTraversable = PlayerLocomotion::IsEntityTraversable( obstacle, when ); - if (bIsTraversable) - { - m_bBreakBreakableInPath = GetBot()->IsAbleToBreak(obstacle); - } - return bIsTraversable; + return bIsTraversable || GetBot()->IsAbleToBreak(obstacle); } diff --git a/src/game/server/neo/bot/neo_bot_locomotion.h b/src/game/server/neo/bot/neo_bot_locomotion.h index fc6379e3a9..8d3e02a222 100644 --- a/src/game/server/neo/bot/neo_bot_locomotion.h +++ b/src/game/server/neo/bot/neo_bot_locomotion.h @@ -30,8 +30,6 @@ class CNEOBotLocomotion : public PlayerLocomotion virtual bool IsAreaTraversable( const CNavArea *baseArea ) const; // return true if given area can be used for navigation virtual bool IsEntityTraversable( CBaseEntity *obstacle, TraverseWhenType when = EVENTUALLY ) const; - mutable bool m_bBreakBreakableInPath = false; - protected: virtual void AdjustPosture( const Vector &moveGoal ) { } // never crouch to navigate };