From c6fad9ba9af485577484cd062388587dcae70329 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Sat, 7 Mar 2026 09:34:27 -0700 Subject: [PATCH 1/2] Lone bots deploy detpack ambush at ghost Overhaul lone wolf behaviors based around search Fix drop weapon to capture and immediately cancel loop Code review feedback pass Equip firearm when investigating --- src/game/server/CMakeLists.txt | 10 + .../neo/bot/behavior/neo_bot_ctg_capture.cpp | 27 + .../neo/bot/behavior/neo_bot_ctg_carrier.cpp | 20 +- .../bot/behavior/neo_bot_ctg_lone_wolf.cpp | 485 ++++-------------- .../neo/bot/behavior/neo_bot_ctg_lone_wolf.h | 30 +- .../behavior/neo_bot_ctg_lone_wolf_ambush.cpp | 275 ++++++++++ .../behavior/neo_bot_ctg_lone_wolf_ambush.h | 39 ++ .../behavior/neo_bot_ctg_lone_wolf_seek.cpp | 354 +++++++++++++ .../bot/behavior/neo_bot_ctg_lone_wolf_seek.h | 39 ++ .../bot/behavior/neo_bot_detpack_deploy.cpp | 218 ++++++++ .../neo/bot/behavior/neo_bot_detpack_deploy.h | 35 ++ .../bot/behavior/neo_bot_detpack_trigger.cpp | 82 +++ .../bot/behavior/neo_bot_detpack_trigger.h | 26 + .../bot/behavior/neo_bot_tactical_monitor.cpp | 126 +++++ .../bot/behavior/neo_bot_tactical_monitor.h | 2 + src/game/server/neo/bot/neo_bot.cpp | 111 ++++ src/game/server/neo/bot/neo_bot.h | 2 + src/game/server/neo/neo_player.cpp | 5 + src/game/shared/neo/neo_gamerules.h | 8 + .../shared/neo/weapons/weapon_detpack.cpp | 7 +- src/game/shared/neo/weapons/weapon_detpack.h | 6 + 21 files changed, 1496 insertions(+), 411 deletions(-) create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.h create mode 100644 src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.cpp create mode 100644 src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.h diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index a58b193f86..ab44d8d31f 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1580,8 +1580,18 @@ target_sources_grouped( neo/bot/behavior/neo_bot_ctg_enemy.h neo/bot/behavior/neo_bot_ctg_escort.h neo/bot/behavior/neo_bot_ctg_lone_wolf.h + neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.cpp + neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.h + neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp + neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h + neo/bot/behavior/neo_bot_ctg_seek.cpp neo/bot/behavior/neo_bot_ctg_seek.h neo/bot/behavior/neo_bot_dead.h + neo/bot/behavior/neo_bot_detpack_deploy.cpp + neo/bot/behavior/neo_bot_detpack_deploy.h + neo/bot/behavior/neo_bot_detpack_trigger.cpp + neo/bot/behavior/neo_bot_detpack_trigger.h + neo/bot/behavior/neo_bot_grenade_dispatch.cpp neo/bot/behavior/neo_bot_grenade_dispatch.h neo/bot/behavior/neo_bot_grenade_throw.h neo/bot/behavior/neo_bot_grenade_throw_frag.h diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp index c7626d4dfd..b0647673bc 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp @@ -1,7 +1,9 @@ #include "cbase.h" #include "bot/behavior/neo_bot_ctg_capture.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_seek.h" #include "bot/behavior/neo_bot_seek_weapon.h" #include "bot/neo_bot_path_compute.h" +#include "neo_detpack.h" #include "weapon_ghost.h" @@ -67,6 +69,31 @@ ActionResult CNEOBotCtgCapture::Update( CNEOBot *me, float interval ) m_captureAttemptTimer.Start( 3.0f ); } + // Check if there is a detpack that risks exploding if I pick up the ghost + // NEO Jank: It may be more proper to check for line of sight, + // but triggering an entity search at the last moment may involve fewer overall calculations + // with the lampshade explanation as the bot being able to notice the detpack along the path + // even if we didn't check constantly along the same path + CBaseEntity *pEnts[256]; + int numEnts = UTIL_EntitiesInSphere( pEnts, 256, me->GetAbsOrigin(), NEO_DETPACK_DAMAGE_RADIUS, 0 ); + bool bDetpackNear = false; + for ( int i = 0; i < numEnts; ++i ) + { + if ( pEnts[i] && FClassnameIs( pEnts[i], "neo_deployed_detpack" ) ) + { + bDetpackNear = true; + break; + } + } + + if ( bDetpackNear ) + { + // NEO JANK: Putting the bot into seek mode will have it search the map for enemies for the rest of the round + // but for now this could be fine as it may indicate an entrenched enemy + // or a friendly that is setting up an ambush, where either scenario indicates ghost capture is too dangerous + return ChangeTo( new CNEOBotCtgLoneWolfSeek(), "Found detpack: skipping ghost capture to search for enemies" ); + } + CBaseCombatWeapon *pPrimary = me->Weapon_GetSlot( 0 ); if ( pPrimary ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp index ebe85f7315..7a8f79a149 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp @@ -4,6 +4,7 @@ #include "bot/behavior/neo_bot_ctg_carrier.h" #include "bot/behavior/neo_bot_ctg_lone_wolf.h" #include "bot/neo_bot_path_compute.h" +#include "nav_mesh.h" #include "neo_gamerules.h" #include "neo_ghost_cap_point.h" #include "debugoverlay_shared.h" @@ -384,9 +385,26 @@ ActionResult< CNEOBot > CNEOBotCtgCarrier::Update( CNEOBot *me, float interval ) m_teammates.RemoveAll(); CollectPlayers( me, &m_teammates ); + // Check if bot should transition into lone wolf behavior if ( m_teammates.Count() == 0 ) { - return SuspendFor( new CNEOBotCtgLoneWolf, "I'm the last one!" ); + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + if ( threat && threat->GetEntity() && threat->GetEntity()->IsAlive() ) + { + CNavArea *destArea = TheNavMesh->GetNearestNavArea( m_closestCapturePoint ); + CNavArea *myArea = me->GetLastKnownArea(); + + if ( !destArea || !myArea || !destArea->IsPotentiallyVisible( myArea ) ) + { + return SuspendFor( new CNEOBotCtgLoneWolf, "Last one standing and blocked from capturing!" ); + } + } + + // Lone wolf will drop the ghost and go into enemy seeking behavior + if ( m_closestCapturePoint == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + return SuspendFor( new CNEOBotCtgLoneWolf, "Looking for enemy since there is no capture point" ); + } } UpdateFollowPath( me, m_teammates ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp index 1e6b03c571..1d22b728e0 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp @@ -2,47 +2,21 @@ #include "neo_player.h" #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_attack.h" -#include "bot/behavior/neo_bot_ctg_capture.h" #include "bot/behavior/neo_bot_ctg_lone_wolf.h" -#include "bot/behavior/neo_bot_seek_weapon.h" -#include "bot/behavior/neo_bot_retreat_to_cover.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_ambush.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_seek.h" +#include "bot/behavior/neo_bot_detpack_deploy.h" #include "bot/neo_bot_path_compute.h" #include "neo_gamerules.h" #include "neo_ghost_cap_point.h" +#include "weapon_detpack.h" #include "weapon_ghost.h" -//--------------------------------------------------------------------------------------------- -CNEOBotCtgLoneWolf::CNEOBotCtgLoneWolf( void ) -{ - m_hGhost = nullptr; - m_bPursuingDropThreat = false; - m_bHasRetreatedFromGhost = false; - m_vecDropThreatPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; - m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; - m_pIgnoredWeapons = std::make_unique(); -} - -//--------------------------------------------------------------------------------------------- -CNEOBotCtgLoneWolf::~CNEOBotCtgLoneWolf() = default; - //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) { - m_hGhost = nullptr; - m_bHasRetreatedFromGhost = false; - m_bPursuingDropThreat = false; - m_useAttemptTimer.Invalidate(); - m_lookAroundTimer.Invalidate(); m_repathTimer.Invalidate(); - m_stalemateTimer.Invalidate(); - m_capPointUpdateTimer.Invalidate(); - m_scavengeTimer.Invalidate(); - m_pIgnoredWeapons->Reset(); - m_vecDropThreatPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; - m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; - m_hPursueTarget = nullptr; - return Continue(); } @@ -50,313 +24,129 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnStart( CNEOBot *me, Action< CNEOBo //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval ) { - const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); - CBaseCombatWeapon *pGhostWep = me->Weapon_GetSlot( 0 ); - - CBaseCombatWeapon *pWeapon = me->GetActiveWeapon(); - if ( !threat && pWeapon ) + if ( me->DropGhost() ) { - // Aggressively reload due to lack of backup - me->ReloadIfLowClip(true); // force reload true + return Continue(); // ghost drop in progress } - // We dropped the ghost to hunt a threat. - if ( m_bPursuingDropThreat ) + ActionResult< CNEOBot > interceptionResult = ConsiderGhostInterception( me ); + if ( interceptionResult.IsRequestingChange() ) { - // First, ensure we have a weapon. - if ( !pGhostWep ) - { - return SuspendFor( new CNEOBotSeekWeapon(nullptr, m_pIgnoredWeapons.get()), "Scavenging for weapon to hunt threat" ); - } - - // We have a weapon. Investigate the last known location. - float flDistSq = me->GetAbsOrigin().DistToSqr( m_vecDropThreatPos ); - if ( flDistSq < Square( 100.0f ) || m_vecDropThreatPos == CNEO_Player::VECTOR_INVALID_WAYPOINT ) - { - // We arrived at threat's last known position, but didn't find them. - m_bPursuingDropThreat = false; - } - else - { - // Move to investigate - if ( threat && threat->GetEntity() && me->GetVisionInterface()->IsAbleToSee( threat->GetEntity(), CNEOBotVision::DISREGARD_FOV, nullptr ) ) - { - return SuspendFor( new CNEOBotAttack, "Found the threat I was hunting!" ); - } + return interceptionResult; + } - CNEOBotPathCompute( me, m_path, m_vecDropThreatPos, FASTEST_ROUTE ); - m_path.Update( me ); - return Continue(); - } + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + if ( threat && threat->GetEntity() ) + { + return ChangeTo( new CNEOBotAttack(), "Engaging enemy" ); } - // Always need to find the ghost to act on it - if (!m_hGhost) + if ( !threat && me->GetActiveWeapon() ) { - m_hGhost = dynamic_cast( gEntList.FindEntityByClassname(nullptr, "weapon_ghost") ); + // Aggressively reload due to lack of backup + me->ReloadIfLowClip(true); // force reload true } - if (!m_hGhost) + CWeaponDetpack *const pDetpackWeapon = assert_cast( me->Weapon_OwnsThisType( "weapon_remotedet" ) ); + + if ( pDetpackWeapon && pDetpackWeapon->m_bThisDetpackHasBeenThrown && !pDetpackWeapon->m_bRemoteHasBeenTriggered ) { - return Done( "Ghost not found" ); + return ChangeTo( new CNEOBotCtgLoneWolfAmbush(), "Detpack deployed, transitioning to ambush" ); } - // Occasionally reconsider which cap zone is our goal - if ( !m_capPointUpdateTimer.HasStarted() || m_capPointUpdateTimer.IsElapsed() ) + CNavArea *const ghostArea = TheNavMesh->GetNearestNavArea( NEORules()->GetGhostPos() ); + CNavArea *const myArea = me->GetLastKnownArea(); + if ( ghostArea && myArea && ghostArea->IsPotentiallyVisible( myArea ) ) { - m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; - float flNearestCapDistSq = FLT_MAX; - - if ( NEORules()->m_pGhostCaps.Count() > 0 ) + if ( pDetpackWeapon && !pDetpackWeapon->m_bThisDetpackHasBeenThrown && NEORules()->m_pGhost ) { - const Vector& vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); - - for( int i=0; im_pGhostCaps.Count(); ++i ) - { - CNEOGhostCapturePoint *pCapPoint = dynamic_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); - if ( !pCapPoint ) continue; - - if ( pCapPoint->owningTeamAlternate() == me->GetTeamNumber() ) - { - float distSq = vecStart.DistToSqr( pCapPoint->GetAbsOrigin() ); - if ( distSq < flNearestCapDistSq ) - { - flNearestCapDistSq = distSq; - m_closestCapturePoint = pCapPoint->GetAbsOrigin(); - } - } - } + return ChangeTo( new CNEOBotDetpackDeploy( NEORules()->GetGhostPos(), new CNEOBotCtgLoneWolfAmbush() ), "Moving to plant detpack" ); } - m_capPointUpdateTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + return ChangeTo( new CNEOBotCtgLoneWolfAmbush(), "Waiting in ambush near ghost" ); } - float flDistGhostToGoal = FLT_MAX; - if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + return ConsiderGhostVisualCheck( me ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::ConsiderGhostInterception( CNEOBot *me, const CBaseCombatCharacter *pGhostOwner ) +{ + if ( !pGhostOwner ) { - const Vector& vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); - flDistGhostToGoal = vecStart.DistTo( m_closestCapturePoint ); + CWeaponGhost *pGhostWeapon = NEORules()->m_pGhost; + pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; } - // Safe to cap: We are closer to the goal than the nearest enemy is to the goal. - // NEO Jank Cheat: We're intentionally cheating here compared to the neo_bot_ctg_carrier behavior by not checking if the ghost is booted. - // The reason is that we want to avoid spectators getting frustrated with bots choosing to ambush at the ghost instead of capping it, - // when it's apparent that the enemy is too far behind to catch up (and ambushing would give them the opportunity to do so). - // Our bots so far have poor intuition about where unseen enemies could come from, - // so it's easier to cheat with distance checks than to anticipate where enemies are. - float flMyTotalDist = flDistGhostToGoal; - if ( !me->IsCarryingGhost() ) + bool bGhostHeldByEnemy = ( pGhostOwner && !me->InSameTeam( pGhostOwner ) ); + if ( !bGhostHeldByEnemy ) { - flMyTotalDist += me->GetAbsOrigin().DistTo( m_hGhost->GetAbsOrigin() ); + return Continue(); } - // Count enemies and find if one is closer to our goal - int iEnemyTeamCount = 0; - float flClosestEnemyDistToGoalSq = FLT_MAX; - float flMyTotalDistSq = ( flMyTotalDist >= FLT_MAX ) ? FLT_MAX : ( flMyTotalDist * flMyTotalDist ); - - for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + // intercept enemy carrier + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + if ( threat && threat->GetEntity() == pGhostOwner && me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) { - CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) - { - iEnemyTeamCount++; - if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) - { - float distSq = pPlayer->GetAbsOrigin().DistToSqr( m_closestCapturePoint ); - if ( distSq < flClosestEnemyDistToGoalSq ) - { - flClosestEnemyDistToGoalSq = distSq; - if ( iEnemyTeamCount > 1 && flClosestEnemyDistToGoalSq < flMyTotalDistSq ) - { - // We already know it's not a 1v1 (count > 1) - // And we know it's not safe to cap (enemy closer than us) - // So we can stop checking. - break; - } - } - } - } + me->EnableCloak( 3.0f ); + return SuspendFor( new CNEOBotAttack, "Attacking the ghost carrier!" ); } - - // Tie breaker: If it's a 1v1, it's boring for human observers to wait forever - // Just try to grab the ghost, even if it might not be the best tactic - bool bIs1v1 = (iEnemyTeamCount == 1); - - bool bSafeToCap = ((m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT) && (flMyTotalDistSq < flClosestEnemyDistToGoalSq)); - - CWeaponGhost *pGhostWeapon = m_hGhost.Get(); - CBaseCombatCharacter *pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; - bool bGhostHeldByEnemy = (pGhostOwner && pGhostOwner->GetTeamNumber() != me->GetTeamNumber()); - // Consider next action - if ( me->IsCarryingGhost() ) + Vector vecInterceptGoal = NEORules()->GetGhostPos(); + if ( vecInterceptGoal != CNEO_Player::VECTOR_INVALID_WAYPOINT ) { - if ( bSafeToCap ) + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) { - if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) - { - if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) - { - CNEOBotPathCompute( me, m_path, m_closestCapturePoint, SAFEST_ROUTE ); - m_path.Update( me ); - m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); - } - else - { - m_path.Update( me ); - } - } - return Continue(); + CNEOBotPathCompute( me, m_path, vecInterceptGoal, FASTEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.3f, 1.0f ) ); } else { - // Enemy is closer to goal (blocking us) or gaining on us. - if ( m_scavengeTimer.IsElapsed() ) - { - m_scavengeTimer.Start( RandomFloat( 0.5f, 1.0f ) ); - - // If we see a weapon nearby, drop the ghost and take it - CBaseEntity *pNearbyWeapon = FindNearestPrimaryWeapon( me, true, m_pIgnoredWeapons.get() ); - if ( pNearbyWeapon ) - { - return SuspendFor( new CNEOBotSeekWeapon( pNearbyWeapon, m_pIgnoredWeapons.get() ), "Dropping ghost to scavenge nearby weapon" ); - } - } - - CBaseCombatWeapon *pActiveWeapon = me->GetActiveWeapon(); - - // If we know where the threat is, drop and hunt. - if ( threat && threat->GetLastKnownPosition() != CNEO_Player::VECTOR_INVALID_WAYPOINT ) - { - m_vecDropThreatPos = threat->GetLastKnownPosition(); - m_bPursuingDropThreat = true; - m_hPursueTarget = threat->GetEntity(); - - if ( pActiveWeapon != pGhostWep ) - { - me->Weapon_Switch( pGhostWep ); - } - else - { - me->EnableCloak( 3.0f ); - me->PressDropButton( 0.1f ); - } - return Continue(); - } - - // Else continue moving ghost towards goal - if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) - { - if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) - { - CNEOBotPathCompute( me, m_path, m_closestCapturePoint, SAFEST_ROUTE ); - m_path.Update( me ); - m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); - } - else - { - m_path.Update( me ); - } - } - return Continue(); + m_path.Update( me ); } } - else if ( bGhostHeldByEnemy ) + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::ConsiderGhostVisualCheck( CNEOBot *me ) +{ + if ( me->IsCarryingGhost() ) { - // intercept enemy carrier - if ( threat && threat->GetEntity() == pGhostOwner && me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT )) - { - me->EnableCloak( 3.0f ); - return SuspendFor(new CNEOBotAttack, "Attacking the ghost carrier!"); - } + return Continue(); + } - if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) - { - CNEOBotPathCompute(me, m_path, m_hGhost->GetAbsOrigin(), FASTEST_ROUTE); - m_path.Update(me); - m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); - } - else - { - m_path.Update(me); - } + CWeaponGhost *pGhostWeapon = NEORules()->m_pGhost; + CBaseCombatCharacter *pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; + bool bGhostHeldByEnemy = ( pGhostOwner && !me->InSameTeam( pGhostOwner ) ); + + if ( bGhostHeldByEnemy ) + { return Continue(); } - else + + // Move to ghost's location to gain visual contact + Vector vecAcquireGoal = NEORules()->GetGhostPos(); + if ( vecAcquireGoal != CNEO_Player::VECTOR_INVALID_WAYPOINT ) { - // Ghost is free for taking - if ( bSafeToCap || (bIs1v1 && m_stalemateTimer.HasStarted() && m_stalemateTimer.IsElapsed()) ) + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) { - // Try to cap before enemy can stop us. - float flDistToGhostSq = me->GetAbsOrigin().DistToSqr(m_hGhost->GetAbsOrigin()); - if ( flDistToGhostSq < 100.0f * 100.0f ) - { - return SuspendFor(new CNEOBotCtgCapture(m_hGhost.Get()), "Picking up ghost to make a run for it!"); - } - - if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) - { - CNEOBotPathCompute(me, m_path, m_hGhost->GetAbsOrigin(), FASTEST_ROUTE); - m_path.Update(me); - m_repathTimer.Start( RandomFloat( 0.2f, 0.5f ) ); - } - else - { - m_path.Update(me); - } - return Continue(); + CNEOBotPathCompute( me, m_path, vecAcquireGoal, FASTEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.3f, 1.0f ) ); } else { - // Not safe. Enemy is closer to goal or blocking. - // Try to ambush them - - if ( bIs1v1 && !m_stalemateTimer.HasStarted() ) - { - m_stalemateTimer.Start( RandomFloat( 10.0f, 20.0f ) ); - } - - if ( m_bHasRetreatedFromGhost ) - { - // Waiting in ambush/cover - if (threat && me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT )) - { - me->EnableCloak( 3.0f ); - return SuspendFor(new CNEOBotAttack, "Ambushing enemy near ghost!"); - } - return UpdateLookAround( me, m_hGhost->GetAbsOrigin() ); - } - else - { - // Hide out of sight of ghost to ambush anyone that picks up the ghost - float flDistToGhostSq = me->GetAbsOrigin().DistToSqr(m_hGhost->GetAbsOrigin()); - if (flDistToGhostSq < 300.0f * 300.0f) - { - m_bHasRetreatedFromGhost = true; - return SuspendFor(new CNEOBotRetreatToCover(), "Finding a hiding spot near the ghost"); - } - else - { - // Get near the ghost first before surveying hiding spots - if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) - { - CNEOBotPathCompute(me, m_path, m_hGhost->GetAbsOrigin(), FASTEST_ROUTE); - m_path.Update(me); - m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); - } - else - { - m_path.Update(me); - } - return Continue(); - } - } + m_path.Update( me ); } } - + return Continue(); } + //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnSuspend( CNEOBot *me, Action< CNEOBot > *interruptingAction ) { @@ -367,25 +157,6 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnSuspend( CNEOBot *me, Action< CNEO //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) { - if ( m_bPursuingDropThreat && m_hPursueTarget.Get() ) - { - if ( !m_hPursueTarget->IsAlive() ) - { - // Target dead, stop pursuit - m_bPursuingDropThreat = false; - m_hPursueTarget = nullptr; - } - else - { - // Remember where we last saw the threat - const CKnownEntity *known = me->GetVisionInterface()->GetKnown( m_hPursueTarget ); - if ( known ) - { - m_vecDropThreatPos = known->GetLastKnownPosition(); - } - } - } - return Continue(); } @@ -397,99 +168,41 @@ EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnStuck( CNEOBot *me ) return TryContinue(); } -//--------------------------------------------------------------------------------------------- -EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnMoveToSuccess( CNEOBot *me, const Path *path ) -{ - return TryContinue(); -} //--------------------------------------------------------------------------------------------- -EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +Vector CNEOBotCtgLoneWolf::GetNearestEnemyCapPoint( CNEOBot *me ) { - m_path.Invalidate(); - return TryContinue(); -} + if ( !me ) + return CNEO_Player::VECTOR_INVALID_WAYPOINT; + const int iEnemyTeam = NEORules()->GetOpposingTeam( me->GetTeamNumber() ); -// Helper for "UpdateLookAround" - inspired from how CNavArea CollectPotentiallyVisibleAreas works -class CCollectPotentiallyVisibleAreas -{ -public: - CCollectPotentiallyVisibleAreas( CUtlVector< CNavArea * > *collection ) + if ( NEORules()->m_pGhostCaps.Count() > 0 ) { - m_collection = collection; - } - - bool operator() ( CNavArea *baseArea ) - { - m_collection->AddToTail( baseArea ); - return true; - } - - CUtlVector< CNavArea * > *m_collection; -}; - -//--------------------------------------------------------------------------------------------- -ActionResult< CNEOBot > CNEOBotCtgLoneWolf::UpdateLookAround( CNEOBot *me, const Vector &anchorPos ) -{ - if ( !m_lookAroundTimer.HasStarted() || m_lookAroundTimer.IsElapsed() ) - { - // NEO Jank Cheat: Bots don't have a good intuition for where to look for threats - // So the compromise is to have them retreat from a threat when the latter shows up - // The looking around logic below is performative for spectators to justify why a bot might incidentally turn to see threat - for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + Vector bestPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + float flNearestSq = FLT_MAX; + for ( int i = 0; i < NEORules()->m_pGhostCaps.Count(); ++i ) { - CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); - if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + CNEOGhostCapturePoint *pCapPoint = assert_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if ( !pCapPoint ) { - if ( me->IsLineOfFireClear( pPlayer->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) - { - me->GetVisionInterface()->AddKnownEntity( pPlayer ); - me->GetBodyInterface()->AimHeadTowards( pPlayer->WorldSpaceCenter(), IBody::CRITICAL, 0.2f, nullptr, "Ambush Cheat: Reacting to enemy in LOF" ); - return SuspendFor( new CNEOBotRetreatToCover(), "Ambush Prep: Retreating from sensed enemy" ); - } + continue; } - } - - m_lookAroundTimer.Start( 0.2f ); - - // Logic inspired from neo_bot.cpp UpdateLookingAroundForIncomingPlayers - // Update our view to watch where enemies might be coming from - CNavArea *myArea = me->GetLastKnownArea(); - if ( myArea ) - { - m_visibleAreas.RemoveAll(); - CCollectPotentiallyVisibleAreas collect( &m_visibleAreas ); - myArea->ForAllPotentiallyVisibleAreas( collect ); - if ( m_visibleAreas.Count() > 0 ) + int iCapTeam = pCapPoint->owningTeamAlternate(); + if ( iCapTeam == iEnemyTeam || iCapTeam == TEAM_ANY ) { - // Pick a random area - int which = RandomInt( 0, m_visibleAreas.Count()-1 ); - CNavArea *area = m_visibleAreas[ which ]; - - // Look at a spot in it - int retryCount = 5; - for( int i=0; iGetAbsOrigin().DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( distSq < flNearestSq ) { - Vector spot = area->GetRandomPoint() + Vector( 0, 0, HumanEyeHeight * 0.75f ); - - // Ensure we can see it - if ( me->GetVisionInterface()->IsLineOfSightClear( spot ) ) - { - me->GetBodyInterface()->AimHeadTowards( spot, IBody::IMPORTANT, 1.0f, nullptr, "Ambush: Scanning area" ); - - const float maxLookInterval = 2.0f; - m_lookAroundTimer.Start(RandomFloat(0.5f, maxLookInterval)); - return Continue(); - } + flNearestSq = distSq; + bestPos = pCapPoint->GetAbsOrigin(); } } } - - // Fallback scanning delay if we failed to find a spot - m_lookAroundTimer.Start(RandomFloat(0.3f, 1.0f)); + return bestPos; } - return Continue(); + return CNEO_Player::VECTOR_INVALID_WAYPOINT; } + diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h index 5cb1c8c0d4..dc2a079e4c 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h @@ -9,8 +9,7 @@ class CNEOIgnoredWeaponsCache; class CNEOBotCtgLoneWolf : public Action< CNEOBot > { public: - CNEOBotCtgLoneWolf( void ); - virtual ~CNEOBotCtgLoneWolf(); + CNEOBotCtgLoneWolf( void ) = default; virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; @@ -18,30 +17,15 @@ class CNEOBotCtgLoneWolf : public Action< CNEOBot > virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; - virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; - virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; virtual const char *GetName( void ) const override { return "ctgLoneWolf"; } -private: - PathFollower m_path; - CHandle m_hGhost; - CountdownTimer m_repathTimer; - CountdownTimer m_useAttemptTimer; - bool m_bHasRetreatedFromGhost; - - Vector m_vecDropThreatPos; - CHandle m_hPursueTarget; - bool m_bPursuingDropThreat; - std::unique_ptr m_pIgnoredWeapons; - CountdownTimer m_scavengeTimer; +protected: + virtual ActionResult< CNEOBot > ConsiderGhostInterception( CNEOBot *me, const CBaseCombatCharacter *pGhostOwner = nullptr ); + virtual ActionResult< CNEOBot > ConsiderGhostVisualCheck( CNEOBot *me ); - ActionResult< CNEOBot > UpdateLookAround( CNEOBot *me, const Vector &anchorPos ); - CountdownTimer m_lookAroundTimer; - CountdownTimer m_stalemateTimer; + Vector GetNearestEnemyCapPoint( CNEOBot *me ); - CountdownTimer m_capPointUpdateTimer; - Vector m_closestCapturePoint; - - CUtlVector< CNavArea * > m_visibleAreas; + CountdownTimer m_repathTimer; + PathFollower m_path; }; diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.cpp new file mode 100644 index 0000000000..7e0707a029 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.cpp @@ -0,0 +1,275 @@ +#include "cbase.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_ambush.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_seek.h" +#include "bot/behavior/neo_bot_retreat_to_cover.h" +#include "bot/neo_bot_path_compute.h" +#include "nav_mesh.h" +#include "neo_detpack.h" +#include "neo_gamerules.h" +#include "neo_player.h" +#include "weapon_detpack.h" +#include "weapon_ghost.h" + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolfAmbush::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + BaseClass::OnStart( me, priorAction ); + m_lookAroundTimer.Invalidate(); + m_vecAmbushGoal = GetNearestEnemyCapPoint( me ); + + m_bIs1v1 = false; + m_1v1Timer.Invalidate(); + m_1v1TransitionTimer.Start( RandomFloat( 5.0f, 30.0f ) ); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolfAmbush::Update( CNEOBot *me, float interval ) +{ + CWeaponGhost *pGhost = NEORules()->m_pGhost; + if ( !pGhost ) + { + return Done( "Ghost not found" ); + } + + if ( me->DropGhost() ) + { + return Continue(); // ghost drop in progress + } + + const CBaseCombatCharacter *const pGhostOwner = pGhost->GetOwner(); + ActionResult< CNEOBot > ghostAction = ConsiderGhostInterception( me, pGhostOwner ); + if ( !ghostAction.IsContinue() ) + { + return ghostAction; + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(true); + if ( threat && threat->GetEntity() ) + { + return ChangeTo( new CNEOBotAttack(), "Engaging enemy from ambush" ); + } + + if ( !threat && me->GetActiveWeapon() ) + { + // Aggressively reload due to lack of backup + me->ReloadIfLowClip(true); // force reload true + } + + if ( pGhostOwner && !me->InSameTeam( pGhostOwner ) ) + { + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + DevMsg( "Lone Wolf Ambush: Intercepting enemy ghost carrier\n" ); + } + // Don't interrupt enemy chasing with ambush pathing + return Continue(); + } + + if ( m_1v1TransitionTimer.IsElapsed() && Is1v1( me ) ) + { + return ChangeTo( new CNEOBotCtgLoneWolfSeek(), "Searching for other lone wolf" ); + } + + // Wait far enough from the ghost and out of sight, but not too far away that it's hard to intercept + const float flDistToGoalSq = ( m_vecAmbushGoal != CNEO_Player::VECTOR_INVALID_WAYPOINT ) ? me->GetAbsOrigin().DistToSqr( m_vecAmbushGoal ) : FLT_MAX; + const Vector vecGhostPos = NEORules()->GetGhostPos(); + const float flDistToGhostSq = me->GetAbsOrigin().DistToSqr( vecGhostPos ); + + bool bShouldHoldPosition = ( flDistToGoalSq < Square( 200.0f ) ); + if ( !bShouldHoldPosition ) + { + const float flMinSafeDistSq = Square( NEO_DETPACK_DAMAGE_RADIUS * 2.0f ); + const float flMaxLurkDistSq = Square( NEO_DETPACK_DAMAGE_RADIUS * 3.0f ); + + if ( flDistToGhostSq > flMinSafeDistSq && flDistToGhostSq < flMaxLurkDistSq ) + { + CNavArea *ghostArea = TheNavMesh->GetNearestNavArea( vecGhostPos ); + CNavArea *myArea = me->GetLastKnownArea(); + bShouldHoldPosition = ( !ghostArea || !myArea || !myArea->IsPotentiallyVisible( ghostArea ) ); + } + } + + // Wait here in ambush by invalidating path to nearest enemy cap zone + if ( bShouldHoldPosition ) + { + if ( m_path.IsValid() && me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + DevMsg( "Lone Wolf Ambush: Holding position at %f %f %f\n", m_vecAmbushGoal.x, m_vecAmbushGoal.y, m_vecAmbushGoal.z ); + } + m_path.Invalidate(); + me->GetLocomotionInterface()->Stop(); + me->PressCrouchButton( 0.3f ); + return UpdateLookAround( me ); + } + + if ( m_vecAmbushGoal == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + return Done( "No ambush spot found" ); + } + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() || !m_path.IsValid() ) + { + CNEOBotPathCompute( me, m_path, m_vecAmbushGoal, SAFEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.5f, 1.5f ) ); + } + else + { + m_path.Update( me ); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotCtgLoneWolfAmbush::OnSuspend( CNEOBot *me, Action *interruptingAction ) +{ + m_path.Invalidate(); + BaseClass::OnSuspend( me, interruptingAction ); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotCtgLoneWolfAmbush::OnResume( CNEOBot *me, Action *interruptingAction ) +{ + BaseClass::OnResume( me, interruptingAction ); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolfAmbush::OnStuck( CNEOBot *me ) +{ + m_path.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolfAmbush::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolfAmbush::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +// Helper for "UpdateLookAround" - inspired from how CNavArea CollectPotentiallyVisibleAreas works +class CCollectPotentiallyVisibleAreas +{ +public: + CCollectPotentiallyVisibleAreas( CUtlVector< CNavArea * > *collection ) + { + m_collection = collection; + } + + bool operator() ( CNavArea *baseArea ) + { + m_collection->AddToTail( baseArea ); + return true; + } + + CUtlVector< CNavArea * > *m_collection; +}; + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolfAmbush::UpdateLookAround( CNEOBot *me ) +{ + if ( !m_lookAroundTimer.HasStarted() || m_lookAroundTimer.IsElapsed() ) + { + // NEO Jank Cheat: Bots don't have a good intuition for where to look for threats + // So the compromise is to have them retreat from a threat when the latter shows up + // The looking around logic below is performative for spectators to justify why a bot might incidentally turn to see threat + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && !me->InSameTeam( pPlayer ) ) + { + if ( me->IsLineOfFireClear( pPlayer->WorldSpaceCenter(), CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + me->GetVisionInterface()->AddKnownEntity( pPlayer ); + me->GetBodyInterface()->AimHeadTowards( pPlayer->WorldSpaceCenter(), IBody::CRITICAL, 0.2f, nullptr, "Ambush Cheat: Reacting to enemy in LOF" ); + return SuspendFor( new CNEOBotRetreatToCover(), "Ambush Prep: Retreating from sensed enemy" ); + } + } + } + + m_lookAroundTimer.Start( 0.2f ); + + // Logic inspired from neo_bot.cpp UpdateLookingAroundForIncomingPlayers + // Update our view to watch where enemies might be coming from + CNavArea *myArea = me->GetLastKnownArea(); + if ( myArea ) + { + m_visibleAreas.RemoveAll(); + CCollectPotentiallyVisibleAreas collect( &m_visibleAreas ); + myArea->ForAllPotentiallyVisibleAreas( collect ); + + if ( m_visibleAreas.Count() > 0 ) + { + // Pick a random area + int which = RandomInt( 0, m_visibleAreas.Count()-1 ); + CNavArea *area = m_visibleAreas[ which ]; + + // Look at a spot in it + int retryCount = 5; + for( int i=0; iGetRandomPoint() + Vector( 0, 0, HumanEyeHeight * 0.75f ); + + // Ensure we can see it + if ( me->GetVisionInterface()->IsLineOfSightClear( spot ) ) + { + me->GetBodyInterface()->AimHeadTowards( spot, IBody::IMPORTANT, 1.0f, nullptr, "Ambush: Scanning area" ); + m_lookAroundTimer.Start(RandomFloat(0.5f, 2.0f)); + return Continue(); + } + } + } + } + + // Fallback scanning delay if we failed to find a spot + m_lookAroundTimer.Start(RandomFloat(0.3f, 1.0f)); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +bool CNEOBotCtgLoneWolfAmbush::Is1v1( CNEOBot *me ) +{ + if ( m_bIs1v1 ) + { + return true; + } + + // NEO JANK: Assume I have no teammates given that + // I entered this function because my teammates are dead + if ( !m_1v1Timer.HasStarted() || m_1v1Timer.IsElapsed() ) + { + int iAliveEnemyCount = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && !me->InSameTeam( pPlayer ) ) + { + if ( ++iAliveEnemyCount > 1 ) + { + break; + } + } + } + m_bIs1v1 = ( iAliveEnemyCount == 1 ); + m_1v1Timer.Start( 2.0f ); + } + + return m_bIs1v1; +} + diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.h new file mode 100644 index 0000000000..2947ad00a2 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_ambush.h @@ -0,0 +1,39 @@ +#pragma once + +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf.h" + +class CWeaponDetpack; + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgLoneWolfAmbush : public CNEOBotCtgLoneWolf +{ +public: + typedef CNEOBotCtgLoneWolf BaseClass; + CNEOBotCtgLoneWolfAmbush( void ) = default; + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult OnSuspend( CNEOBot *me, Action *interruptingAction ) override; + virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "ctgLoneWolfAmbush"; } + +protected: + ActionResult< CNEOBot > UpdateLookAround( CNEOBot *me ); + bool Is1v1( CNEOBot *me ); + +private: + CountdownTimer m_lookAroundTimer; + + bool m_bIs1v1{ false }; + CountdownTimer m_1v1Timer; + CountdownTimer m_1v1TransitionTimer; + + CUtlVector< CNavArea * > m_visibleAreas; + Vector m_vecAmbushGoal; +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp new file mode 100644 index 0000000000..5be56b0aa5 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp @@ -0,0 +1,354 @@ +#include "cbase.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_seek.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" +#include "neo_player.h" + +//--------------------------------------------------------------------------------------------- +class CSearchForUnexplored : public ISearchSurroundingAreasFunctor +{ +public: + static constexpr int DEFAULT_AREA_LIMIT = 1000; + static constexpr int CANDIDATE_LIMIT = 10; + + CSearchForUnexplored( CNEOBot *me, CUtlMap &exploredAreaIds, int areaLimit = DEFAULT_AREA_LIMIT ) + : m_me( me ), m_exploredAreaIds( exploredAreaIds ), m_iAreaCount( 0 ), m_iAreaLimit( areaLimit ) + { + } + + // return true to keep searching, return false to stop searching + virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) override + { + if ( m_exploredAreaIds.Find( (int)baseArea->GetID() ) == m_exploredAreaIds.InvalidIndex() ) + { + m_candidateAreas.AddToTail( baseArea ); + } + + if ( m_candidateAreas.Count() >= CANDIDATE_LIMIT ) + { + return false; + } + + return true; + } + + // return true if 'adjArea' should be included in the ongoing search + virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) override + { + if ( m_candidateAreas.Count() >= CANDIDATE_LIMIT ) + { + return false; + } + + // hit the max search limit + if ( ++m_iAreaCount > m_iAreaLimit ) + { + return false; + } + + // don't want to jump or drop + const float heightChange = currentArea->ComputeAdjacentConnectionHeightChange( adjArea ); + if ( fabs( heightChange ) > m_me->GetLocomotionInterface()->GetStepHeight() ) + { + return false; + } + + return true; + } + + CNavArea *GetRandomCandidate() const + { + if ( m_candidateAreas.IsEmpty() ) + { + return nullptr; + } + int which = RandomInt( 0, m_candidateAreas.Count() - 1 ); + return m_candidateAreas[ which ]; + } + + CNEOBot *m_me; + CUtlMap &m_exploredAreaIds; + CUtlVector m_candidateAreas; + int m_iAreaCount; + int m_iAreaLimit; +}; + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolfSeek::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + BaseClass::OnStart( me, priorAction ); + + SetDefLessFunc( m_exploredAreaIds ); + m_exploredAreaIds.RemoveAll(); + m_iExplorationTargetId = -1; + + m_vecLastGhostPos = NEORules()->GetGhostPos(); + m_pCachedGhostArea = TheNavMesh->GetNearestNavArea( m_vecLastGhostPos ); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolfSeek::Update( CNEOBot *me, float interval ) +{ + me->PressCrouchButton( 0.2f ); // Keep a lower profile + + if ( !NEORules()->GhostExists() || !NEORules()->m_pGhost ) + { + return Done( "Ghost not found" ); + } + + if ( me->DropGhost() ) + { + return Continue(); // ghost drop in progress + } + + ActionResult< CNEOBot > interceptionResult = ConsiderGhostInterception( me ); + if ( interceptionResult.IsRequestingChange() ) + { + return interceptionResult; + } + + CWeaponGhost *pGhostWeapon = NEORules()->m_pGhost; + const CBaseCombatCharacter *pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; + + if ( pGhostOwner && !me->InSameTeam( pGhostOwner ) ) + { + // Don't interrupt enemy carrier pursuit with search pathing + return Continue(); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && threat->GetEntity() ) + { + me->ReleaseCrouchButton(); // move faster + return ChangeTo( new CNEOBotAttack(), "Engaging enemy from seek" ); + } + + if ( !threat && me->GetActiveWeapon() ) + { + // Aggressively reload due to lack of backup + me->ReloadIfLowClip(true); // force reload true + } + + Vector vecSoundPos = me->GetAudibleEnemySoundPos(); + if ( vecSoundPos != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + // Don't veer path for sound if waypoint is not that far off + if ( m_vecSearchWaypoint.DistToSqr( vecSoundPos ) > Square( 200.0f ) ) + { + m_vecSearchWaypoint = vecSoundPos; + m_path.Invalidate(); + m_repathTimer.Invalidate(); // path to sound next tick + + CNavArea *soundArea = TheNavMesh->GetNearestNavArea( vecSoundPos ); + if ( soundArea ) + { + m_iExplorationTargetId = (int)soundArea->GetID(); + // Mark sound area as not explored + m_exploredAreaIds.Remove( m_iExplorationTargetId ); + } + } + } + + const Vector currentGhostPos = NEORules()->GetGhostPos(); + if ( !m_pCachedGhostArea || currentGhostPos.DistToSqr( m_vecLastGhostPos ) > Square( 64.0f ) ) + { + CNavArea *pLastGhostArea = m_pCachedGhostArea; + m_pCachedGhostArea = TheNavMesh->GetNearestNavArea( currentGhostPos ); + m_vecLastGhostPos = currentGhostPos; + + if ( m_pCachedGhostArea != pLastGhostArea ) + { + m_iExplorationTargetId = -1; + m_vecSearchWaypoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; + + // Restart search, as most likely someone is moving the ghost + m_path.Invalidate(); + m_exploredAreaIds.RemoveAll(); + + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + DevMsg( "Lone Wolf Seek: Ghost moved, re-calculating exploration targets locally\n" ); + } + } + } + + CNavArea *ghostArea = m_pCachedGhostArea; + + const bool bReachedSearchTarget = ( m_vecSearchWaypoint != CNEO_Player::VECTOR_INVALID_WAYPOINT && me->GetAbsOrigin().DistToSqr( m_vecSearchWaypoint ) < Square( SEARCH_WAYPOINT_REACHED_DIST ) ); + + if ( bReachedSearchTarget ) + { + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + DevMsg( "Lone Wolf Seek: Search area reached, looking for new search area\n" ); + } + + m_iExplorationTargetId = -1; + m_vecSearchWaypoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_path.Invalidate(); + m_repathTimer.Invalidate(); + } + + if ( m_vecSearchWaypoint == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + if ( !me->IsCarryingGhost() ) + { + if ( ghostArea ) + { + // Before searching, mark what we can see from our current position as explored, + CNavArea *currentArea = me->GetLastKnownArea(); + if ( currentArea ) + { + MarkVisibleAreasAsExplored( me, currentArea, ghostArea ); + } + + CSearchForUnexplored search( me, m_exploredAreaIds ); + SearchSurroundingAreas( ghostArea, search ); + + if ( search.m_candidateAreas.IsEmpty() ) + { + // Track already explored areas around the ghost + auto searchFromVisible = [&]( CNavArea *visibleArea ) -> bool + { + if ( search.m_candidateAreas.Count() >= CSearchForUnexplored::CANDIDATE_LIMIT || search.m_iAreaCount >= search.m_iAreaLimit ) + { + return false; + } + SearchSurroundingAreas( visibleArea, search ); + return search.m_candidateAreas.Count() < CSearchForUnexplored::CANDIDATE_LIMIT; + }; + ghostArea->ForAllPotentiallyVisibleAreas( searchFromVisible ); + } + + if ( m_iExplorationTargetId == -1 && !search.m_candidateAreas.IsEmpty() ) + { + CNavArea *target = search.GetRandomCandidate(); + m_iExplorationTargetId = (int)target->GetID(); + m_exploredAreaIds.InsertOrReplace( m_iExplorationTargetId, true ); + m_vecSearchWaypoint = target->GetCenter(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + target->DrawFilled( 255, 255, 0, 128, DEBUG_OVERLAY_DURATION ); + } + } + else if ( m_iExplorationTargetId == -1 ) + { + // All nearby areas explored, or search failed to find new search area. + // Reset search to restart patrol + m_exploredAreaIds.RemoveAll(); + + // Fallback: move towards the ghost itself while we search + m_path.Invalidate(); + m_repathTimer.Invalidate(); + + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + DevMsg( "Lone Wolf Seek: Searched all areas around ghost, resetting seen tracking\n" ); + } + } + } + } + } + + if ( m_vecSearchWaypoint == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + m_vecSearchWaypoint = NEORules()->GetGhostPos(); + } + + if ( me->GetAbsOrigin().DistToSqr( m_vecSearchWaypoint ) > Square( PATH_RECOMPUTE_DIST ) ) + { + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, m_vecSearchWaypoint, FASTEST_ROUTE ); + m_repathTimer.Start( RandomFloat( 0.3f, 1.0f ) ); + } + } + + m_path.Update( me ); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolfSeek::OnStuck( CNEOBot *me ) +{ + if ( m_iExplorationTargetId != -1 ) + { + m_exploredAreaIds.InsertOrReplace( m_iExplorationTargetId, true ); + } + else if ( m_vecSearchWaypoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( m_vecSearchWaypoint ); + if ( area ) + { + m_exploredAreaIds.InsertOrReplace( (int)area->GetID(), true ); + } + } + + if ( me->IsDebugging( NEXTBOT_BEHAVIOR ) ) + { + DevMsg( "Lone Wolf Seek: Bot stuck going to search area, marking as explored and finding new target\n" ); + } + + m_iExplorationTargetId = -1; + m_vecSearchWaypoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_path.Invalidate(); + + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolfSeek::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolfSeek::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + m_path.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgLoneWolfSeek::MarkVisibleAreasAsExplored( CNEOBot *me, CNavArea *currentArea, CNavArea *ghostArea ) +{ + if ( !currentArea ) + { + return; + } + + // Mark the currently occupied area explored + if ( m_exploredAreaIds.InsertOrReplace( (int)currentArea->GetID(), true ) != m_exploredAreaIds.InvalidIndex() ) + { + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + currentArea->DrawFilled( 0, 255, 0, 128, DEBUG_OVERLAY_DURATION ); + } + } + + // Mark all potentially visible areas + auto markVisible = [this, me, ghostArea]( CNavArea *area ) -> bool + { + if ( area && area != ghostArea ) + { + if ( m_exploredAreaIds.InsertOrReplace( (int)area->GetID(), true ) != m_exploredAreaIds.InvalidIndex() ) + { + if ( me->IsDebugging( NEXTBOT_PATH ) ) + { + area->DrawFilled( 0, 255, 0, 128, DEBUG_OVERLAY_DURATION ); + } + } + } + return true; + }; + currentArea->ForAllPotentiallyVisibleAreas( markVisible ); +} + diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h new file mode 100644 index 0000000000..ef343f3788 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h @@ -0,0 +1,39 @@ +#pragma once + +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf.h" +#include "utlmap.h" + +class CWeaponDetpack; + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgLoneWolfSeek : public CNEOBotCtgLoneWolf +{ +public: + typedef CNEOBotCtgLoneWolf BaseClass; + CNEOBotCtgLoneWolfSeek( void ) = default; + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "ctgLoneWolfSeek"; } + +private: + static constexpr float DEBUG_OVERLAY_DURATION = 3.0f; + static constexpr float ENEMY_LAST_KNOWN_DIST = 100.0f; + static constexpr float PATH_RECOMPUTE_DIST = 64.0f; + static constexpr float SEARCH_WAYPOINT_REACHED_DIST = 200.0f; + + Vector m_vecLastGhostPos{ CNEO_Player::VECTOR_INVALID_WAYPOINT }; + CNavArea *m_pCachedGhostArea{ nullptr }; + + CUtlMap m_exploredAreaIds; + int m_iExplorationTargetId{-1}; + Vector m_vecSearchWaypoint{CNEO_Player::VECTOR_INVALID_WAYPOINT}; + + void MarkVisibleAreasAsExplored( CNEOBot *me, CNavArea *currentArea, CNavArea *ghostArea = nullptr ); +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.cpp b/src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.cpp new file mode 100644 index 0000000000..65bb4ae17c --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.cpp @@ -0,0 +1,218 @@ +#include "cbase.h" +#include "bot/neo_bot.h" +#include "bot/neo_bot_path_compute.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf_seek.h" +#include "bot/behavior/neo_bot_detpack_deploy.h" +#include "neo_detpack.h" +#include "weapon_detpack.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotDetpackDeploy::CNEOBotDetpackDeploy( const Vector &targetPos, Action< CNEOBot > *nextAction ) + : m_targetPos( targetPos ), m_nextAction( nextAction ), m_flDeployDistSq( 0.0f ) +{ + m_bPushedWeapon = false; + m_losTimer.Invalidate(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotDetpackDeploy::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_hDetpackWeapon = assert_cast< CWeaponDetpack* >( me->Weapon_OwnsThisType( "weapon_remotedet" ) ); + if ( !m_hDetpackWeapon ) + { + if (m_nextAction != nullptr) + { + return ChangeTo( m_nextAction, "No detpack weapon, transitioning to next action" ); + } + return Done( "No detpack weapon found" ); + } + + me->PushRequiredWeapon( m_hDetpackWeapon ); + m_bPushedWeapon = true; + + // Ignore enemy to prevent weapon handling interference with detpack deployment + me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + + m_expiryTimer.Start( 10.0f ); + m_repathTimer.Invalidate(); + + m_flDeployDistSq = Square( MAX( 100.0f, CWeaponDetpack::GetArmingTime() * me->GetNormSpeed() ) ); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotDetpackDeploy::Update( CNEOBot *me, float interval ) +{ + if ( !m_hDetpackWeapon ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "No detpack weapon, transitioning to next action" ); + } + return Done( "No detpack weapon" ); + } + + if ( m_expiryTimer.IsElapsed() ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "Detpack deploy timer expired, transitioning to next action" ); + } + return Done( "Detpack deploy timer expired" ); + } + + if ( m_hDetpackWeapon->m_bThisDetpackHasBeenThrown ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "Detpack deployed, transitioning to next action" ); + } + return Done( "Detpack deployed" ); + } + + const CKnownEntity* threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + if ( threat && threat->GetEntity() ) + { + if (m_nextAction != nullptr) + { + return ChangeTo( m_nextAction, "Interrupting detpack deploy to let next action handle enemy" ); + } + return ChangeTo( new CNEOBotAttack(), "Engaging enemy encountered while deploying detpack" ); + } + + float flDistToTargetSq = me->GetAbsOrigin().DistToSqr( m_targetPos ); + if ( flDistToTargetSq < m_flDeployDistSq ) + { + if ( me->GetActiveWeapon() == m_hDetpackWeapon ) + { + if ( !m_losTimer.HasStarted() || m_losTimer.IsElapsed() ) + { + if ( me->GetVisionInterface()->IsLineOfSightClear( m_targetPos ) ) + { + CBaseEntity *pEnts[256]; + int numEnts = UTIL_EntitiesInSphere( pEnts, 256, me->GetAbsOrigin(), NEO_DETPACK_DAMAGE_RADIUS, 0 ); + bool bDetpackNear = false; + for ( int i = 0; i < numEnts; ++i ) + { + if ( pEnts[i] && FClassnameIs( pEnts[i], "neo_deployed_detpack" ) ) + { + bDetpackNear = true; + break; + } + } + + if ( bDetpackNear ) + { + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "Skipping detpack deploy: in blast radius of another detpack" ); + } + return Done( "Aborting detpack deploy: in blast radius of another detpack" ); + } + + me->PressFireButton(); + + if ( flDistToTargetSq < Square( 64.0f ) ) + { + m_path.Invalidate(); + m_repathTimer.Start( 10.0f ); + } + } + + m_losTimer.Start( RandomFloat( 0.2f, 0.4f ) ); + } + } + } + + if ( !m_path.IsValid() ) + { + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, m_targetPos, FASTEST_ROUTE ); + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + } + else + { + m_path.Update( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CNEOBotDetpackDeploy::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + if ( m_bPushedWeapon ) + { + me->PopRequiredWeapon(); + m_bPushedWeapon = false; + } + me->StartLookingAroundForEnemies(); + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); + + if ( m_hDetpackWeapon && m_hDetpackWeapon->m_bThisDetpackHasBeenThrown ) + { + me->EquipBestWeaponForThreat( me->GetVisionInterface()->GetPrimaryKnownThreat( true ) ); + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotDetpackDeploy::OnSuspend( CNEOBot *me, Action *interruptingAction ) +{ + if (m_nextAction != nullptr) + { + return ChangeTo( m_nextAction, "Detpack deploy suspend cancelled, transitioning to next action" ); + } + + return Done( "Detpack deploy suspended, situation will likely become stale." ); + // OnEnd will get called after Done +} + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotDetpackDeploy::OnResume( CNEOBot *me, Action *interruptingAction ) +{ + if (m_nextAction != nullptr) + { + return ChangeTo( m_nextAction, "Detpack deploy resume cancelled, transitioning to next action" ); + } + + return Done( "Detpack deploy resumed, situation is likely stale." ); + // OnEnd will get called after Done +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotDetpackDeploy::OnStuck( CNEOBot *me ) +{ + if (m_nextAction != nullptr) + { + return TryChangeTo( m_nextAction, RESULT_CRITICAL, "Detpack deploy stuck, transitioning to next action" ); + } + + return TryDone( RESULT_CRITICAL, "Detpack deploy stuck, situation will likely become stale." ); + // OnEnd will get called after Done +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotDetpackDeploy::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotDetpackDeploy::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + if (m_nextAction != nullptr) + { + return TryChangeTo( m_nextAction, RESULT_CRITICAL, "Detpack deploy move to failure, transitioning to next action" ); + } + + return TryDone( RESULT_CRITICAL, "Detpack deploy move to failure, situation will likely become stale." ); + // OnEnd will get called after Done +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.h b/src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.h new file mode 100644 index 0000000000..3f0c91ba42 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_detpack_deploy.h @@ -0,0 +1,35 @@ +#pragma once + +#include "bot/neo_bot.h" + +class CWeaponDetpack; + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotDetpackDeploy : public Action< CNEOBot > +{ +public: + CNEOBotDetpackDeploy( const Vector &targetPos, Action< CNEOBot > *nextAction = nullptr ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual ActionResult< CNEOBot > OnSuspend( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + virtual ActionResult< CNEOBot > OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) override; + virtual void OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) override; + + virtual EventDesiredResult< CNEOBot > OnStuck( CNEOBot *me ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToSuccess( CNEOBot *me, const Path *path ) override; + virtual EventDesiredResult< CNEOBot > OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) override; + + virtual const char *GetName( void ) const override { return "DetpackDeploy"; } + +private: + bool m_bPushedWeapon; + float m_flDeployDistSq; + Action< CNEOBot > *m_nextAction; + CHandle< CWeaponDetpack > m_hDetpackWeapon; + CountdownTimer m_expiryTimer; + CountdownTimer m_losTimer; + CountdownTimer m_repathTimer; + PathFollower m_path; + Vector m_targetPos; +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.cpp b/src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.cpp new file mode 100644 index 0000000000..1ebb6173d4 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.cpp @@ -0,0 +1,82 @@ +#include "cbase.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_detpack_trigger.h" +#include "weapon_detpack.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotDetpackTrigger::CNEOBotDetpackTrigger( void ) +{ + m_hDetpackWeapon = nullptr; + m_bPushedWeapon = false; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotDetpackTrigger::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_hDetpackWeapon = assert_cast( me->Weapon_OwnsThisType( "weapon_remotedet" ) ); + if ( m_hDetpackWeapon ) + { + me->PushRequiredWeapon( m_hDetpackWeapon ); + m_bPushedWeapon = true; + } + else + { + return Done( "No detpack weapon found" ); + } + + // Ignore enemy to prevent weapon handling interference with detpack trigger + me->StopLookingAroundForEnemies(); + me->SetAttribute( CNEOBot::IGNORE_ENEMIES ); + + m_expiryTimer.Start( 5.0f ); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotDetpackTrigger::Update( CNEOBot *me, float interval ) +{ + if ( m_expiryTimer.IsElapsed() ) + { + return Done( "Detpack trigger timer expired" ); + } + + if ( !m_hDetpackWeapon || !m_hDetpackWeapon->m_bThisDetpackHasBeenThrown || m_hDetpackWeapon->m_bRemoteHasBeenTriggered ) + { + return Done( "Detpack triggered or invalid" ); + } + + if ( me->GetActiveWeapon() == m_hDetpackWeapon && gpGlobals->curtime >= m_hDetpackWeapon->m_flNextPrimaryAttack ) + { + me->PressFireButton(); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotDetpackTrigger::OnEnd( CNEOBot *me, Action< CNEOBot > *nextAction ) +{ + // Restore looking and weapon handling behaviors + if ( m_bPushedWeapon ) + { + me->PopRequiredWeapon(); + m_bPushedWeapon = false; + } + me->StartLookingAroundForEnemies(); + me->ClearAttribute( CNEOBot::IGNORE_ENEMIES ); +} + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotDetpackTrigger::OnSuspend( CNEOBot *me, Action *interruptingAction ) +{ + return Done( "OnSuspend: Cancel out of detpack trigger behavior, situation will likely become stale." ); + // OnEnd will get called after Done +} + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotDetpackTrigger::OnResume( CNEOBot *me, Action *interruptingAction ) +{ + return Done( "OnResume: Cancel out of detpack trigger behavior, situation is likely stale." ); + // OnEnd will get called after Done +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.h b/src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.h new file mode 100644 index 0000000000..ebd1b6abb2 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_detpack_trigger.h @@ -0,0 +1,26 @@ +#pragma once + +#include "bot/neo_bot.h" + +class CWeaponDetpack; + +//----------------------------------------------------------------------------- +class CNEOBotDetpackTrigger : public Action< CNEOBot > +{ +public: + CNEOBotDetpackTrigger( void ); + + 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 ActionResult OnSuspend( CNEOBot *me, Action *interruptingAction ) override; + virtual ActionResult OnResume( CNEOBot *me, Action *interruptingAction ) override; + + virtual const char *GetName( void ) const override { return "DetpackTrigger"; } + +private: + bool m_bPushedWeapon; + CHandle< CWeaponDetpack > m_hDetpackWeapon; + CountdownTimer m_expiryTimer; +}; 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 78e8be610a..cded9861b5 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 @@ -8,6 +8,7 @@ #include "bot/neo_bot.h" #include "bot/neo_bot_manager.h" +#include "bot/behavior/neo_bot_detpack_trigger.h" #include "bot/behavior/neo_bot_tactical_monitor.h" #include "bot/behavior/neo_bot_scenario_monitor.h" @@ -26,6 +27,9 @@ #include "bot/behavior/nav_entities/neo_bot_nav_ent_move_to.h" #include "bot/behavior/nav_entities/neo_bot_nav_ent_wait.h" #include "neo/neo_player_shared.h" +#include "neo_detpack.h" +#include "weapon_detpack.h" +#include "weapon_ghost.h" #include "nav_mesh.h" ConVar neo_bot_force_jump( "neo_bot_force_jump", "0", FCVAR_CHEAT, "Force bots to continuously jump" ); @@ -85,6 +89,122 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::OnStart( CNEOBot *me, Action< CN } +//----------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotTacticalMonitor::MonitorArmedDetpack( CNEOBot *me ) +{ + if ( !m_detpackCheckTimer.IsElapsed() ) + { + return Continue(); + } + m_detpackCheckTimer.Start( 0.2f ); + + CWeaponDetpack *pDetWeapon = assert_cast( me->Weapon_OwnsThisType( "weapon_remotedet" ) ); + if ( !pDetWeapon || !pDetWeapon->m_bThisDetpackHasBeenThrown || pDetWeapon->m_bRemoteHasBeenTriggered ) + { + return Continue(); + } + + CBaseEntity *pDetpackEnt = pDetWeapon->GetDetpackEntity(); + if ( !pDetpackEnt ) + { + return Continue(); + } + + const Vector vecDetpackPos = pDetpackEnt->GetAbsOrigin(); + + // Check if I am too close to the detpack + if ( me->GetAbsOrigin().DistToSqr( vecDetpackPos ) <= Square( NEO_DETPACK_DAMAGE_RADIUS ) ) + { + return Continue(); + } + + float flThresholdMultiplier; + switch ( me->GetDifficulty() ) + { + case CNEOBot::EASY: + flThresholdMultiplier = 1.05f; + break; + case CNEOBot::NORMAL: + flThresholdMultiplier = 0.95f; + break; + case CNEOBot::HARD: + flThresholdMultiplier = 0.85f; + break; + case CNEOBot::EXPERT: + flThresholdMultiplier = 0.75f; + break; + default: + flThresholdMultiplier = 0.95f; + break; + } + + const float flMaxRadiusSq = Square( NEO_DETPACK_DAMAGE_RADIUS * flThresholdMultiplier ); + bool bShouldDetonate = false; + + // Check if any known threat or teammate is in range + CUtlVector< CKnownEntity > knownVector; + me->GetVisionInterface()->CollectKnownEntities( &knownVector ); + bool bIsTeamplay = NEORules()->IsTeamplay(); + + for ( int i = 0; i < knownVector.Count(); ++i ) + { + if ( knownVector[i].IsObsolete() ) + { + continue; + } + + CBaseEntity *pEntity = knownVector[i].GetEntity(); + if ( !pEntity ) + { + continue; + } + + if ( vecDetpackPos.DistToSqr( knownVector[i].GetLastKnownPosition() ) <= flMaxRadiusSq ) + { + if ( bIsTeamplay && me->InSameTeam( pEntity ) ) + { + // Teammate in blast radius + return Continue(); + } + + // Enemy in range. + bShouldDetonate = true; + } + } + + // Check if ghost carrier is in range + if ( !bShouldDetonate ) + { + if ( CWeaponGhost *pGhost = NEORules()->m_pGhost ) + { + CBaseCombatCharacter *pGhostOwner = pGhost->GetOwner(); + if ( pGhostOwner && !me->InSameTeam( pGhostOwner ) ) + { + if ( vecDetpackPos.DistToSqr( pGhostOwner->GetAbsOrigin() ) <= flMaxRadiusSq ) + { + bShouldDetonate = true; + } + } + } + } + + if ( !bShouldDetonate ) + { + if ( me->GetAudibleEnemySoundPos( vecDetpackPos, flMaxRadiusSq ) != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + bShouldDetonate = true; + } + } + + if ( bShouldDetonate ) + { + return SuspendFor( new CNEOBotDetpackTrigger(), "Triggering detpack!" ); + } + + return Continue(); +} + + #ifndef NEO // NEO TODO (Adam) Monitor the remote detpack //----------------------------------------------------------------------------------------- void CNEOBotTacticalMonitor::MonitorArmedStickyBombs( CNEOBot *me ) @@ -311,6 +431,12 @@ ActionResult< CNEOBot > CNEOBotTacticalMonitor::Update( CNEOBot *me, float inter } #endif + ActionResult< CNEOBot > detpackResult = MonitorArmedDetpack( me ); + if ( detpackResult.IsRequestingChange() ) + { + return detpackResult; + } + #if 0 // NEO TODO (Adam) detonate remote detpacks // detonate sticky bomb traps when victims are near MonitorArmedStickyBombs( me ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h index 83634acfcf..0cfe08a0d2 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h +++ b/src/game/server/neo/bot/behavior/neo_bot_tactical_monitor.h @@ -33,6 +33,8 @@ class CNEOBotTacticalMonitor : public Action< CNEOBot > CountdownTimer m_attentionTimer; std::unique_ptr m_pIgnoredWeapons; + CountdownTimer m_detpackCheckTimer; + ActionResult< CNEOBot > MonitorArmedDetpack(CNEOBot *me); #if 0 CountdownTimer m_stickyBombCheckTimer; void MonitorArmedStickyBombs(CNEOBot* me); diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 91d06de787..54dacc8dd9 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -21,6 +21,8 @@ #include "neo_weapon_loadout.h" #include "behavior/neo_bot_behavior.h" #include "neo_crosshair.h" +#include "recipientfilter.h" +#include "soundent.h" ConVar neo_bot_notice_gunfire_range("neo_bot_notice_gunfire_range", "3000", FCVAR_GAMEDLL); ConVar neo_bot_notice_quiet_gunfire_range("neo_bot_notice_quiet_gunfire_range", "500", FCVAR_GAMEDLL); @@ -1588,6 +1590,40 @@ void CNEOBot::EquipBestWeaponForThreat(const CKnownEntity* threat, const bool bN } +//----------------------------------------------------------------------------------------------------- +bool CNEOBot::DropGhost() +{ + if ( !IsCarryingGhost() ) + { + return false; + } + + CBaseCombatWeapon *pGhost = Weapon_GetSlot( 0 ); + if ( pGhost ) + { + if ( GetActiveWeapon() != pGhost ) + { + Weapon_Switch( pGhost ); + } + else + { + // Look behind where we are moving + Vector moveDir = GetLocomotionInterface()->GetMotionVector(); + Vector lookDir = -moveDir; + GetBodyInterface()->AimHeadTowards( EyePosition() + lookDir * 100.0f, IBody::IMPORTANT, 0.2f, nullptr, "Preparing to drop ghost away from path" ); + + // Drop the ghost if we are looking anywhere but the front + Vector viewDir = GetBodyInterface()->GetViewVector(); + if ( moveDir.Dot( viewDir ) < 0.4f ) + { + PressDropButton(); + } + } + } + + return true; +} + //----------------------------------------------------------------------------------------------------- // Reload the active weapon if it makes sense for the situation void CNEOBot::ReloadIfLowClip(bool bForceReload) @@ -2843,5 +2879,80 @@ QueryResultType CNEOBotBehavior::ShouldAim(const CNEOBot *me, const bool bWepHas return result; } +//--------------------------------------------------------------------------------------------- +Vector CNEOBot::GetAudibleEnemySoundPos(const Vector& vecReferencePos, float flMaxRangeSq) const +{ + CSound *pSound = nullptr; + for ( int iSound = CSoundEnt::ActiveList(); iSound != SOUNDLIST_EMPTY; iSound = pSound->NextSound() ) + { + pSound = CSoundEnt::SoundPointerForIndex( iSound ); + if ( !pSound ) + { + break; + } + + // If a reference position and range are provided, check distance first + if ( vecReferencePos != CNEO_Player::VECTOR_INVALID_WAYPOINT && flMaxRangeSq > 0.0f ) + { + if ( pSound->GetSoundOrigin().DistToSqr( vecReferencePos ) > flMaxRangeSq ) + { + continue; + } + } + + if ( ( pSound->SoundType() & ( SOUND_COMBAT | SOUND_PLAYER ) ) == 0 ) + { + continue; + } + + CBaseEntity *pOwner = pSound->m_hOwner.Get(); + + // Ignore non-player sounds and sounds I was responsible for + if ( !pOwner || !pOwner->IsPlayer() || pOwner == GetEntity() ) + { + continue; + } + + // Only care about sounds from the enemy + if ( InSameTeam( pOwner ) ) + { + continue; + } + + // Check if I can hear the sound + bool bCanHearEnemy = false; + CPASFilter soundFilter( pSound->GetSoundOrigin() ); + for ( int i = 0; i < soundFilter.GetRecipientCount(); ++i ) + { + if ( soundFilter.GetRecipientIndex( i ) == entindex() ) + { + bCanHearEnemy = true; + break; + } + } + + if ( !bCanHearEnemy ) + { + // Check if I can hear the shooter + CPASFilter shooterFilter( pOwner->GetAbsOrigin() ); + for ( int i = 0; i < shooterFilter.GetRecipientCount(); ++i ) + { + if ( shooterFilter.GetRecipientIndex( i ) == entindex() ) + { + bCanHearEnemy = true; + break; + } + } + } + + if ( bCanHearEnemy ) + { + return pSound->GetSoundOrigin(); + } + } + + return CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + diff --git a/src/game/server/neo/bot/neo_bot.h b/src/game/server/neo/bot/neo_bot.h index f2af7450ef..8a432c8375 100644 --- a/src/game/server/neo/bot/neo_bot.h +++ b/src/game/server/neo/bot/neo_bot.h @@ -157,6 +157,7 @@ class CNEOBot : public NextBotPlayer< CNEO_Player >, public CGameEventListener bool EquipRequiredWeapon(void); // if we're required to equip a specific weapon, do it. void EquipBestWeaponForThreat(const CKnownEntity* threat, const bool bNotPrimary = false); // equip the best weapon we have to attack the given threat void ReloadIfLowClip(bool bForceReload = false); + bool DropGhost(); void DropPrimaryWeapon(void); @@ -171,6 +172,7 @@ class CNEOBot : public NextBotPlayer< CNEO_Player >, public CGameEventListener bool IsQuietWeapon(CNEOBaseCombatWeapon* weapon) const; // return true if given weapon doesn't make much sound when used (ie: spy knife, etc) bool IsEnvironmentNoisy(void) const; // return true if there are/have been loud noises (ie: non-quiet weapons) nearby very recently + Vector GetAudibleEnemySoundPos(const Vector& vecReferencePos = CNEO_Player::VECTOR_INVALID_WAYPOINT, float flMaxRangeSq = -1.0f) const; bool IsEnemy(const CBaseEntity* them) const OVERRIDE; diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 12884ea267..f1a82ddfa3 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -1231,6 +1231,9 @@ void CNEO_Player::PlayCloakSound(bool removeLocalPlayer) // effect lasts 0.5 seconds, but allow 200-300ms leeway with GetFogObscuredRatio cache window m_botThermOpticCamoDisruptedTimer.Start(0.2f); } + + // For bots to notice cloak sound + CSoundEnt::InsertSound(SOUND_COMBAT, GetAbsOrigin(), 600, 0.2, this); } void CNEO_Player::SetCloakState(bool state) @@ -2827,6 +2830,8 @@ void CNEO_Player::PickupObject( CBaseEntity *pObject, void CNEO_Player::PlayStepSound( Vector &vecOrigin, surfacedata_t *psurface, float fvol, bool force ) { + // For bots to hear footsteps + CSoundEnt::InsertSound(SOUND_PLAYER, GetAbsOrigin(), 150, 0.1, this); BaseClass::PlayStepSound(vecOrigin, psurface, fvol, force); } diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 5602e0b267..f48e35f56a 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -83,6 +83,10 @@ class NEOViewVectors : public HL2MPViewVectors class CNEOGhostCapturePoint; class CNEO_Player; class CWeaponGhost; +class CNEOBotCtgLoneWolf; +class CNEOBotCtgLoneWolfAmbush; +class CNEOBotCtgLoneWolfDetpack; +class CNEOBotCtgLoneWolfSeek; class CNEOBotSeekAndDestroy; extern ConVar sv_neo_mirror_teamdamage_multiplier; @@ -472,6 +476,10 @@ class CNEORules : public CHL2MPRules, public CGameEventListener friend class CNEOBotCtgCarrier; friend class CNEOBotCtgEscort; friend class CNEOBotCtgLoneWolf; + friend class CNEOBotCtgLoneWolfAmbush; + friend class CNEOBotCtgLoneWolfDetpack; + friend class CNEOBotCtgLoneWolfSeek; + friend class CNEOBotTacticalMonitor; friend class CNEOBotSeekAndDestroy; CUtlVector m_pGhostCaps; diff --git a/src/game/shared/neo/weapons/weapon_detpack.cpp b/src/game/shared/neo/weapons/weapon_detpack.cpp index 8228525a9e..510cdc1ef7 100644 --- a/src/game/shared/neo/weapons/weapon_detpack.cpp +++ b/src/game/shared/neo/weapons/weapon_detpack.cpp @@ -19,6 +19,7 @@ #include "te_effect_dispatch.h" #include "grenade_frag.h" #include "eventqueue.h" +#include "soundent.h" #endif #include "effect_dispatch_data.h" @@ -251,7 +252,7 @@ void CWeaponDetpack::ItemPostFrame(void) pOwner->DoAnimationEvent(PLAYERANIMEVENT_ATTACK_PRIMARY); // NEO NOTE (Rain): Why 0.9? Because we want the explosion to occur after 2.666... seconds of detpack arming, - // plus 1.333... seconds of the trigger, for a total of 4 seconds delay. And we just happen to need 0.9 seconds + // plus ~1.333... seconds of the trigger, for a total of 4 seconds delay. And we just happen to need 0.9 seconds // here to reach that. There's probably some nicer way to arrive at these values, but that's the explanation // for this magic value. m_flNextPrimaryAttack = gpGlobals->curtime + 0.9; @@ -304,6 +305,10 @@ void CWeaponDetpack::TossDetpack(CBasePlayer* pPlayer) { Assert(false); } + + // Notify bots after pressing keypad since reacting immediately to press fire is unforgiving + // especially since the sound clip has some silence at the beginning + CSoundEnt::InsertSound( SOUND_COMBAT, pPlayer->GetAbsOrigin(), 256, 0.2, pPlayer, SOUNDENT_CHANNEL_WEAPON ); #endif if (GetOwner()->IsPlayer()) // NEO NOTE (Adam) if else taken from CBaseCombatWeapon::Equip, this must be what was fixing the viewmodel previously after dropping and picking up the detremote { diff --git a/src/game/shared/neo/weapons/weapon_detpack.h b/src/game/shared/neo/weapons/weapon_detpack.h index 0a7938c6ce..e46d6442d6 100644 --- a/src/game/shared/neo/weapons/weapon_detpack.h +++ b/src/game/shared/neo/weapons/weapon_detpack.h @@ -53,6 +53,12 @@ class CWeaponDetpack : public CNEOBaseProjectile virtual float GetSpeedScale(void) const OVERRIDE { return 0.85f; } +#ifdef GAME_DLL + CNEODeployedDetpack* GetDetpackEntity() const { return m_pDetpack; } +#endif + + static float GetArmingTime() { return 2.66f; } // see Rain's comment in DecrementAmmo + bool CanDrop(void) OVERRIDE; virtual bool CanAim() final { return false; } virtual bool CanPerformSecondaryAttack() const override final { return false; } From 5b94e3c0c343bbe8fc166ede1a516994a414d124 Mon Sep 17 00:00:00 2001 From: Alan Shen Date: Thu, 18 Jun 2026 00:02:29 -0600 Subject: [PATCH 2/2] Look for better weapons when searching for enemy --- .../neo/bot/behavior/neo_bot_ctg_lone_wolf.h | 3 --- .../behavior/neo_bot_ctg_lone_wolf_seek.cpp | 25 +++++++++++++++++++ .../bot/behavior/neo_bot_ctg_lone_wolf_seek.h | 8 +++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h index dc2a079e4c..e6629e7977 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h @@ -1,9 +1,6 @@ #pragma once #include "bot/neo_bot.h" -#include - -class CNEOIgnoredWeaponsCache; //-------------------------------------------------------------------------------------------------------- class CNEOBotCtgLoneWolf : public Action< CNEOBot > diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp index 5be56b0aa5..3ec84b4d33 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp @@ -2,6 +2,7 @@ #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_ctg_lone_wolf_seek.h" +#include "bot/behavior/neo_bot_seek_weapon.h" #include "bot/neo_bot_path_compute.h" #include "neo_gamerules.h" #include "neo_player.h" @@ -75,6 +76,15 @@ class CSearchForUnexplored : public ISearchSurroundingAreasFunctor int m_iAreaLimit; }; +//--------------------------------------------------------------------------------------------- +CNEOBotCtgLoneWolfSeek::CNEOBotCtgLoneWolfSeek( void ) +{ + m_pIgnoredWeapons = std::make_unique(); +} + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgLoneWolfSeek::~CNEOBotCtgLoneWolfSeek() = default; + //--------------------------------------------------------------------------------------------- ActionResult< CNEOBot > CNEOBotCtgLoneWolfSeek::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) { @@ -87,6 +97,9 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolfSeek::OnStart( CNEOBot *me, Action< CN m_vecLastGhostPos = NEORules()->GetGhostPos(); m_pCachedGhostArea = TheNavMesh->GetNearestNavArea( m_vecLastGhostPos ); + m_scavengeTimer.Invalidate(); + m_pIgnoredWeapons->Reset(); + return Continue(); } @@ -133,6 +146,18 @@ ActionResult< CNEOBot > CNEOBotCtgLoneWolfSeek::Update( CNEOBot *me, float inter me->ReloadIfLowClip(true); // force reload true } + // Periodically look for better weapons + if ( ( !m_scavengeTimer.HasStarted() || m_scavengeTimer.IsElapsed() ) ) + { + m_scavengeTimer.Start( RandomFloat( 3.0f, 6.0f ) ); + + CBaseEntity *pNearbyWeapon = FindNearestPrimaryWeapon( me, true, m_pIgnoredWeapons.get() ); + if ( pNearbyWeapon ) + { + return SuspendFor( new CNEOBotSeekWeapon( pNearbyWeapon, m_pIgnoredWeapons.get() ), "Scavenging for nearby weapon" ); + } + } + Vector vecSoundPos = me->GetAudibleEnemySoundPos(); if ( vecSoundPos != CNEO_Player::VECTOR_INVALID_WAYPOINT ) { diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h index ef343f3788..afe7683131 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h @@ -3,15 +3,18 @@ #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_ctg_lone_wolf.h" #include "utlmap.h" +#include class CWeaponDetpack; +class CNEOIgnoredWeaponsCache; //-------------------------------------------------------------------------------------------------------- class CNEOBotCtgLoneWolfSeek : public CNEOBotCtgLoneWolf { public: typedef CNEOBotCtgLoneWolf BaseClass; - CNEOBotCtgLoneWolfSeek( void ) = default; + CNEOBotCtgLoneWolfSeek( void ); + virtual ~CNEOBotCtgLoneWolfSeek(); virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; @@ -35,5 +38,8 @@ class CNEOBotCtgLoneWolfSeek : public CNEOBotCtgLoneWolf int m_iExplorationTargetId{-1}; Vector m_vecSearchWaypoint{CNEO_Player::VECTOR_INVALID_WAYPOINT}; + std::unique_ptr m_pIgnoredWeapons; + CountdownTimer m_scavengeTimer; + void MarkVisibleAreasAsExplored( CNEOBot *me, CNavArea *currentArea, CNavArea *ghostArea = nullptr ); };