diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 0f652f9a8..718d9bfe6 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1457,10 +1457,18 @@ target_sources_grouped( neo/bot/behavior/neo_bot_ctg_escort.h neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp 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.cpp 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.cpp 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..b0647673b 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 5be1709aa..1793ee00d 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" @@ -368,9 +369,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 cca00ee16..1d22b728e 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,41 +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; -} - //--------------------------------------------------------------------------------------------- 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_vecDropThreatPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; - m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; - m_hPursueTarget = nullptr; - return Continue(); } @@ -44,323 +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 *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 ( !me->Weapon_GetSlot( 0 ) ) - { - return SuspendFor( new CNEOBotSeekWeapon(), "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 we see a weapon nearby, drop the ghost and take it - CBaseEntity *pNearbyWeapon = FindNearestPrimaryWeapon( me->GetAbsOrigin(), true ); - if ( pNearbyWeapon ) - { - CBaseCombatWeapon *pGhostWep = me->Weapon_GetSlot( 0 ); - if ( pGhostWep ) - { - if ( me->GetActiveWeapon() != pGhostWep ) - { - me->Weapon_Switch( pGhostWep ); - return Continue(); - } - - me->PressDropButton( 0.1f ); - return ChangeTo( new CNEOBotSeekWeapon(), "Dropping ghost to scavenge nearby weapon" ); - } - } - - CBaseCombatWeapon *pActiveWeapon = me->GetActiveWeapon(); - CBaseCombatWeapon *pGhostWep = me->Weapon_GetSlot( 0 ); - - // 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 ( pGhostWep ) - { - 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 ) { @@ -371,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(); } @@ -401,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 ) -{ - 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; - } - - bool operator() ( CNavArea *baseArea ) - { - m_collection->AddToTail( baseArea ); - return true; - } + if ( !me ) + return CNEO_Player::VECTOR_INVALID_WAYPOINT; - 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 6458c1dc2..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 @@ -6,7 +6,7 @@ class CNEOBotCtgLoneWolf : public Action< CNEOBot > { public: - CNEOBotCtgLoneWolf( void ); + CNEOBotCtgLoneWolf( void ) = default; virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; @@ -14,28 +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; +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..5be56b0aa --- /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 000000000..ef343f378 --- /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 000000000..65bb4ae17 --- /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 000000000..3f0c91ba4 --- /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 000000000..1ebb6173d --- /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 000000000..ebd1b6abb --- /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 959326b76..a39bb96a3 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" @@ -25,6 +26,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" ); @@ -72,6 +76,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 ) @@ -293,6 +413,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 6beb096d2..2bd3a0a31 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 @@ -26,6 +26,8 @@ class CNEOBotTacticalMonitor : public Action< CNEOBot > CountdownTimer m_acknowledgeRetryTimer; CountdownTimer m_attentionTimer; + 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 f5659eb24..7625455a5 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); @@ -1571,6 +1573,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) @@ -2826,5 +2862,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 cc392021a..c54173bc4 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 PushRequiredWeapon(CNEOBaseCombatWeapon* weapon); // force us to equip and use this weapon until popped off the required stack void PopRequiredWeapon(void); // pop top required weapon off of stack and discard @@ -169,6 +170,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 9fc72c47a..c87a6ca8f 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -1224,6 +1224,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) @@ -2807,6 +2810,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 8feb445ea..714f013b5 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 8228525a9..510cdc1ef 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 0a7938c6c..e46d6442d 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; }