From 3343de8895bffabd19a6a24b41b4a7616aad7aa3 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 22 Jan 2026 22:00:24 -0700 Subject: [PATCH 1/2] Bots trade weapons with commander --- src/game/server/CMakeLists.txt | 2 + .../bot/behavior/neo_bot_command_follow.cpp | 90 +++++++++++++++++++ .../neo/bot/behavior/neo_bot_command_follow.h | 4 + .../bot/behavior/neo_bot_scenario_monitor.cpp | 18 ++++ .../bot/behavior/neo_bot_tactical_monitor.cpp | 22 +---- .../neo_bot_throw_weapon_at_player.cpp | 63 +++++++++++++ .../behavior/neo_bot_throw_weapon_at_player.h | 26 ++++++ 7 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index b1f84ef28b..c53b05f0cd 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_seek_weapon.h neo/bot/behavior/neo_bot_tactical_monitor.cpp neo/bot/behavior/neo_bot_tactical_monitor.h + neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp + neo/bot/behavior/neo_bot_throw_weapon_at_player.h neo/bot/behavior/nav_entities/neo_bot_nav_ent_destroy_entity.cpp neo/bot/behavior/nav_entities/neo_bot_nav_ent_destroy_entity.h neo/bot/behavior/nav_entities/neo_bot_nav_ent_move_to.cpp diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index 7c125e1af1..0e76b1bd0d 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -2,6 +2,7 @@ #include "neo_player.h" #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_command_follow.h" +#include "bot/behavior/neo_bot_throw_weapon_at_player.h" #include "nav_mesh.h" // memdbgon must be the last include file in a .cpp file!!! @@ -36,6 +37,8 @@ CNEOBotCommandFollow::CNEOBotCommandFollow() : m_vGoalPos( CNEO_Player::VECTOR_I ActionResult< CNEOBot > CNEOBotCommandFollow::OnStart(CNEOBot *me, Action< CNEOBot > *priorAction) { m_path.SetMinLookAheadDistance(me->GetDesiredPathLookAheadRange()); + m_commanderLookingAtMeTimer.Invalidate(); + m_bWasCommanderLookingAtMe = false; if (!FollowCommandChain(me)) { @@ -53,12 +56,99 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval return Done("Lost commander or released"); } + ActionResult weaponRequestResult = CheckCommanderWeaponRequest(me); + if (weaponRequestResult.IsRequestingChange()) + { + return weaponRequestResult; + } + m_path.Update(me); m_ghostEquipmentHandler.Update( me ); return Continue(); } + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCommandFollow::CheckCommanderWeaponRequest(CNEOBot *me) +{ + // Can I even drop my primary? (e.g. Juggernaut's BALC) + CNEOBaseCombatWeapon *pMyPrimary = dynamic_cast(me->Weapon_GetSlot(0)); + if (!pMyPrimary || !pMyPrimary->CanDrop()) + { + return Continue(); + } + + CNEO_Player* pCommander = me->m_hCommandingPlayer.Get(); + + if (!pCommander || !pCommander->IsAlive()) + { + return Continue(); + } + + // Check if commander has dropped primary + if (pCommander->Weapon_GetSlot(0)) + { + return Continue(); + } + + // Can the commander even pick my primary up? (e.g. Recon can't pick up PZ) + if (!pMyPrimary->CanBePickedUpByClass(pCommander->GetClass())) + { + return Continue(); + } + + // Check if looking at me + Vector vecToBot = me->EyePosition() - pCommander->EyePosition(); + vecToBot.NormalizeInPlace(); + Vector vecCmdrFacing; + pCommander->EyeVectors(&vecCmdrFacing); + + if (vecCmdrFacing.Dot(vecToBot) <= 0.99f) + { + m_commanderLookingAtMeTimer.Invalidate(); + m_bWasCommanderLookingAtMe = false; + return Continue(); + } + + if (!m_bWasCommanderLookingAtMe) + { + // Wait to see if it wasn't just a momentary glance + m_commanderLookingAtMeTimer.Start(); + m_bWasCommanderLookingAtMe = true; + return Continue(); + } + + if (m_commanderLookingAtMeTimer.GetElapsedTime() > 0.2f) + { + me->GetBodyInterface()->AimHeadTowards(pCommander->EyePosition(), IBody::CRITICAL, 0.2f, NULL, "Noticed commander looking at me without a primary"); + } + + if (m_commanderLookingAtMeTimer.GetElapsedTime() > 1.0f) + { + // Sanity check that commander is really looking at me with more expensive traceline + // e.g. for edge case where I am behind the player the commander is looking at + trace_t tr; + const float estimatedReasonableThrowDistance = 300.0f; + Vector traceEnd = pCommander->EyePosition() + vecCmdrFacing * estimatedReasonableThrowDistance; + UTIL_TraceLine(pCommander->EyePosition(), traceEnd, MASK_SHOT_HULL, pCommander, COLLISION_GROUP_NONE, &tr); + + if (tr.DidHit() && tr.m_pEnt == me) + { + m_bWasCommanderLookingAtMe = false; // Reset state + return SuspendFor(new CNEOBotThrowWeaponAtPlayer(pCommander), "Donating weapon to commander"); + } + else + { + // Line of sight blocked + m_commanderLookingAtMeTimer.Invalidate(); + m_bWasCommanderLookingAtMe = false; + } + } + + return Continue(); +} + //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCommandFollow::OnResume(CNEOBot *me, Action< CNEOBot > *interruptingAction) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h index 3920b908fb..fe2afab06c 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h @@ -22,11 +22,15 @@ class CNEOBotCommandFollow : public Action< CNEOBot > private: bool FollowCommandChain( CNEOBot *me ); bool FanOutAndCover( CNEOBot *me, Vector &movementTarget, bool bMoveToSeparate = true, float flArrivalZoneSizeSq = -1.0f ); + ActionResult< CNEOBot > CheckCommanderWeaponRequest( CNEOBot *me ); PathFollower m_path; CountdownTimer m_repathTimer; CNEOBotGhostEquipmentHandler m_ghostEquipmentHandler; + IntervalTimer m_commanderLookingAtMeTimer; + bool m_bWasCommanderLookingAtMe = false; + EHANDLE m_hTargetEntity; bool m_bGoingToTargetEntity = false; Vector m_vGoalPos; diff --git a/src/game/server/neo/bot/behavior/neo_bot_scenario_monitor.cpp b/src/game/server/neo/bot/behavior/neo_bot_scenario_monitor.cpp index 2ef7bf96e7..cb0b348e02 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_scenario_monitor.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_scenario_monitor.cpp @@ -13,6 +13,8 @@ #include "bot/behavior/neo_bot_retreat_to_cover.h" #include "bot/behavior/neo_bot_get_health.h" #include "bot/behavior/neo_bot_get_ammo.h" +#include "bot/behavior/neo_bot_command_follow.h" +#include "bot/behavior/neo_bot_pause.h" #include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_seek_and_destroy.h" @@ -64,10 +66,26 @@ ActionResult< CNEOBot > CNEOBotScenarioMonitor::OnStart( CNEOBot *me, Action< CN ConVar neo_bot_fetch_lost_flag_time( "neo_bot_fetch_lost_flag_time", "10", FCVAR_CHEAT, "How long busy NEOBots will ignore the dropped flag before they give up what they are doing and go after it" ); ConVar neo_bot_flag_kill_on_touch( "neo_bot_flag_kill_on_touch", "0", FCVAR_CHEAT, "If nonzero, any bot that picks up the flag dies. For testing." ); +extern ConVar sv_neo_bot_cmdr_enable; +extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; + //----------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotScenarioMonitor::Update( CNEOBot *me, float interval ) { + if (sv_neo_bot_cmdr_enable.GetBool()) + { + if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) + { + return SuspendFor(new CNEOBotCommandFollow, "Following commander"); + } + + if (sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) + { + return SuspendFor(new CNEOBotPause, "Paused by debug convar sv_neo_bot_cmdr_debug_pause_uncommanded"); + } + } + return Continue(); } 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 ae5db90b3a..ef8a397630 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 @@ -10,7 +10,6 @@ #include "bot/behavior/neo_bot_tactical_monitor.h" #include "bot/behavior/neo_bot_scenario_monitor.h" -#include "bot/behavior/neo_bot_command_follow.h" #include "bot/behavior/neo_bot_seek_and_destroy.h" #include "bot/behavior/neo_bot_seek_weapon.h" #include "bot/behavior/neo_bot_retreat_to_cover.h" @@ -26,8 +25,6 @@ ConVar neo_bot_force_jump( "neo_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" ); ConVar neo_bot_grenade_check_radius( "neo_bot_grenade_check_radius", "500", FCVAR_CHEAT ); -extern ConVar sv_neo_bot_cmdr_enable; -extern ConVar sv_neo_bot_cmdr_debug_pause_uncommanded; //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -279,23 +276,6 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter return result; } - if (sv_neo_bot_cmdr_enable.GetBool()) - { - if (me->m_hLeadingPlayer.Get() || me->m_hCommandingPlayer.Get()) - { - return SuspendFor(new CNEOBotCommandFollow, "Following commander"); - } - - if (sv_neo_bot_cmdr_debug_pause_uncommanded.GetBool()) - { - return SuspendFor( new CNEOBotPause, "Paused by debug convar sv_neo_bot_cmdr_debug_pause_uncommanded" ); - } - } - -#if 0 - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); -#endif - // check if we need to get to cover QueryResultType shouldRetreat = me->GetIntentionInterface()->ShouldRetreat( me ); @@ -372,7 +352,7 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::ScavengeForPrimaryWeapon( CNEOBo { return Continue(); } - m_maintainTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + m_maintainTimer.Start( 1.0f ); // Look for any one valid primary weapon, then dispatch into behavior for more optimal search // true parameter: short-circuit the search if any valid primary weapon is found diff --git a/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp b/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp new file mode 100644 index 0000000000..00b4425982 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp @@ -0,0 +1,63 @@ +#include "cbase.h" +#include "bot/neo_bot.h" +#include "neo_player.h" +#include "bot/behavior/neo_bot_throw_weapon_at_player.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotThrowWeaponAtPlayer::CNEOBotThrowWeaponAtPlayer( CNEO_Player *pTargetPlayer ) +{ + m_hTargetPlayer = pTargetPlayer; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotThrowWeaponAtPlayer::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + if ( !m_hTargetPlayer ) + { + return Done( "No target player to throw weapon at" ); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotThrowWeaponAtPlayer::Update( CNEOBot *me, float interval ) +{ + CNEO_Player *pTarget = m_hTargetPlayer.Get(); + if ( !pTarget || !pTarget->IsAlive() ) + { + return Done( "Target player lost or died" ); + } + + me->GetBodyInterface()->AimHeadTowards( pTarget->EyePosition(), IBody::CRITICAL, 0.2f, NULL, "Aiming at player to throw weapon" ); + + CBaseCombatWeapon *pPrimary = me->Weapon_GetSlot( 0 ); + if ( pPrimary ) + { + if ( me->GetActiveWeapon() != pPrimary ) + { + me->Weapon_Switch( pPrimary ); + return Continue(); // Wait for switch + } + } + else + { + return Done( "No primary weapon to throw" ); + } + + if ( me->GetBodyInterface()->IsHeadAimingOnTarget() ) + { + me->PressDropButton(); + return Done("Weapon dropped"); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotThrowWeaponAtPlayer::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.h b/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.h new file mode 100644 index 0000000000..53e23cd93a --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.h @@ -0,0 +1,26 @@ +#ifndef NEO_BOT_THROW_WEAPON_AT_PLAYER_H +#define NEO_BOT_THROW_WEAPON_AT_PLAYER_H +#ifdef _WIN32 +#pragma once +#endif + +#include "NextBotBehavior.h" + +class CNEO_Player; + +class CNEOBotThrowWeaponAtPlayer : public Action< CNEOBot > +{ +public: + CNEOBotThrowWeaponAtPlayer( CNEO_Player *pTargetPlayer ); + + 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 "ThrowWeaponAtPlayer"; }; + +private: + CHandle m_hTargetPlayer; +}; + +#endif // NEO_BOT_THROW_WEAPON_AT_PLAYER_H From 1ee86093b331d0e1b04cd4742aa11b832b142a82 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Fri, 20 Feb 2026 22:16:25 -0700 Subject: [PATCH 2/2] Aim at feet to avoid overthrowing --- .../server/neo/bot/behavior/neo_bot_command_follow.cpp | 7 ++++--- .../neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index 0e76b1bd0d..5832827155 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -73,7 +73,7 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval ActionResult< CNEOBot > CNEOBotCommandFollow::CheckCommanderWeaponRequest(CNEOBot *me) { // Can I even drop my primary? (e.g. Juggernaut's BALC) - CNEOBaseCombatWeapon *pMyPrimary = dynamic_cast(me->Weapon_GetSlot(0)); + CNEOBaseCombatWeapon *pMyPrimary = assert_cast(me->Weapon_GetSlot(0)); if (!pMyPrimary || !pMyPrimary->CanDrop()) { return Continue(); @@ -104,7 +104,7 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::CheckCommanderWeaponRequest(CNEOBo Vector vecCmdrFacing; pCommander->EyeVectors(&vecCmdrFacing); - if (vecCmdrFacing.Dot(vecToBot) <= 0.99f) + if (vecCmdrFacing.Dot(vecToBot) <= 0.95f) { m_commanderLookingAtMeTimer.Invalidate(); m_bWasCommanderLookingAtMe = false; @@ -121,7 +121,8 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::CheckCommanderWeaponRequest(CNEOBo if (m_commanderLookingAtMeTimer.GetElapsedTime() > 0.2f) { - me->GetBodyInterface()->AimHeadTowards(pCommander->EyePosition(), IBody::CRITICAL, 0.2f, NULL, "Noticed commander looking at me without a primary"); + // The throw behavior triggers drop when the bot is looking at the commander, so practively aim at feet to avoid overthrowing + me->GetBodyInterface()->AimHeadTowards(pCommander->GetAbsOrigin(), IBody::IMPORTANT, 0.2f, NULL, "Noticed commander looking at me without a primary"); } if (m_commanderLookingAtMeTimer.GetElapsedTime() > 1.0f) diff --git a/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp b/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp index 00b4425982..db94c88011 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_throw_weapon_at_player.cpp @@ -32,7 +32,7 @@ ActionResult< CNEOBot > CNEOBotThrowWeaponAtPlayer::Update( CNEOBot *me, float i return Done( "Target player lost or died" ); } - me->GetBodyInterface()->AimHeadTowards( pTarget->EyePosition(), IBody::CRITICAL, 0.2f, NULL, "Aiming at player to throw weapon" ); + me->GetBodyInterface()->AimHeadTowards( pTarget->GetAbsOrigin(), IBody::MANDATORY, 0.1f, NULL, "Aiming at player's feet to throw weapon" ); CBaseCombatWeapon *pPrimary = me->Weapon_GetSlot( 0 ); if ( pPrimary )