diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index a58b193f8..1986a0d79 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1580,6 +1580,11 @@ 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_grenade_dispatch.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 c7626d4df..d6ee9d542 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,5 +1,6 @@ #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 "weapon_ghost.h" 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 ebe85f731..7a8f79a14 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 1e6b03c57..9bb964582 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,20 @@ #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/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 +23,125 @@ 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!" ); - } - - CNEOBotPathCompute( me, m_path, m_vecDropThreatPos, FASTEST_ROUTE ); - m_path.Update( me ); - return Continue(); - } + return interceptionResult; } - // Always need to find the ghost to act on it - if (!m_hGhost) + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + if ( threat && threat->GetEntity() ) { - m_hGhost = dynamic_cast( gEntList.FindEntityByClassname(nullptr, "weapon_ghost") ); + return ChangeTo( new CNEOBotAttack(), "Engaging enemy" ); } - if (!m_hGhost) + if ( !threat && me->GetActiveWeapon() ) { - return Done( "Ghost not found" ); + // Aggressively reload due to lack of backup + me->ReloadIfLowClip(true); // force reload true } - // Occasionally reconsider which cap zone is our goal - if ( !m_capPointUpdateTimer.HasStarted() || m_capPointUpdateTimer.IsElapsed() ) + CWeaponDetpack *const pDetpackWeapon = assert_cast( me->Weapon_OwnsThisType( "weapon_remotedet" ) ); + + if ( pDetpackWeapon && pDetpackWeapon->m_bThisDetpackHasBeenThrown && !pDetpackWeapon->m_bRemoteHasBeenTriggered ) { - m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; - float flNearestCapDistSq = FLT_MAX; - - if ( NEORules()->m_pGhostCaps.Count() > 0 ) - { - 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; + return ChangeTo( new CNEOBotCtgLoneWolfAmbush(), "Detpack deployed, transitioning to ambush" ); + } - if ( pCapPoint->owningTeamAlternate() == me->GetTeamNumber() ) - { - float distSq = vecStart.DistToSqr( pCapPoint->GetAbsOrigin() ); - if ( distSq < flNearestCapDistSq ) - { - flNearestCapDistSq = distSq; - m_closestCapturePoint = pCapPoint->GetAbsOrigin(); - } - } - } - } - m_capPointUpdateTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + CNavArea *const ghostArea = TheNavMesh->GetNearestNavArea( NEORules()->GetGhostPos() ); + CNavArea *const myArea = me->GetLastKnownArea(); + if ( ghostArea && myArea && ghostArea->IsPotentiallyVisible( myArea ) ) + { + 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 +152,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 +163,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 ) -{ - m_path.Invalidate(); - return TryContinue(); -} - - -// Helper for "UpdateLookAround" - inspired from how CNavArea CollectPotentiallyVisibleAreas works -class CCollectPotentiallyVisibleAreas +Vector CNEOBotCtgLoneWolf::GetNearestEnemyCapPoint( CNEOBot *me ) { -public: - CCollectPotentiallyVisibleAreas( CUtlVector< CNavArea * > *collection ) - { - m_collection = collection; - } + if ( !me ) + return CNEO_Player::VECTOR_INVALID_WAYPOINT; - bool operator() ( CNavArea *baseArea ) - { - m_collection->AddToTail( baseArea ); - return true; - } - - CUtlVector< CNavArea * > *m_collection; -}; + const int iEnemyTeam = NEORules()->GetOpposingTeam( me->GetTeamNumber() ); -//--------------------------------------------------------------------------------------------- -ActionResult< CNEOBot > CNEOBotCtgLoneWolf::UpdateLookAround( CNEOBot *me, const Vector &anchorPos ) -{ - if ( !m_lookAroundTimer.HasStarted() || m_lookAroundTimer.IsElapsed() ) + if ( NEORules()->m_pGhostCaps.Count() > 0 ) { - // 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 5cb1c8c0d..e6629e797 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,16 +1,12 @@ #pragma once #include "bot/neo_bot.h" -#include - -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 +14,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 000000000..7e0707a02 --- /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 000000000..2947ad00a --- /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 000000000..47e888e59 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.cpp @@ -0,0 +1,359 @@ +#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/behavior/neo_bot_seek_weapon.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; +}; + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgLoneWolfSeek::CNEOBotCtgLoneWolfSeek( void ) +{ + m_pIgnoredWeapons = std::make_unique(); +} + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgLoneWolfSeek::~CNEOBotCtgLoneWolfSeek() = default; + +//--------------------------------------------------------------------------------------------- +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 ); + + m_scavengeTimer.Invalidate(); + m_pIgnoredWeapons->Reset(); + + 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 + } + + // 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" ); + } + } + + 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 000000000..afe768313 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf_seek.h @@ -0,0 +1,45 @@ +#pragma once + +#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 ); + virtual ~CNEOBotCtgLoneWolfSeek(); + + 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}; + + std::unique_ptr m_pIgnoredWeapons; + CountdownTimer m_scavengeTimer; + + void MarkVisibleAreasAsExplored( CNEOBot *me, CNavArea *currentArea, CNavArea *ghostArea = nullptr ); +}; diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 91d06de78..baaecd6b2 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -1588,6 +1588,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) diff --git a/src/game/server/neo/bot/neo_bot.h b/src/game/server/neo/bot/neo_bot.h index f2af7450e..c7258e3cf 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); diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 5602e0b26..eb8f0e246 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -83,6 +83,9 @@ class NEOViewVectors : public HL2MPViewVectors class CNEOGhostCapturePoint; class CNEO_Player; class CWeaponGhost; +class CNEOBotCtgLoneWolf; +class CNEOBotCtgLoneWolfAmbush; +class CNEOBotCtgLoneWolfSeek; class CNEOBotSeekAndDestroy; extern ConVar sv_neo_mirror_teamdamage_multiplier; @@ -472,6 +475,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;