diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt index 386a7225e7..fbf697cbd2 100644 --- a/src/game/server/CMakeLists.txt +++ b/src/game/server/CMakeLists.txt @@ -1440,6 +1440,18 @@ target_sources_grouped( neo/bot/behavior/neo_bot_command_follow.h neo/bot/behavior/neo_bot_dead.cpp neo/bot/behavior/neo_bot_dead.h + neo/bot/behavior/neo_bot_ctg_capture.cpp + neo/bot/behavior/neo_bot_ctg_capture.h + neo/bot/behavior/neo_bot_ctg_carrier.cpp + neo/bot/behavior/neo_bot_ctg_carrier.h + neo/bot/behavior/neo_bot_ctg_enemy.cpp + neo/bot/behavior/neo_bot_ctg_enemy.h + neo/bot/behavior/neo_bot_ctg_escort.cpp + 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_seek.cpp + neo/bot/behavior/neo_bot_ctg_seek.h neo/bot/behavior/neo_bot_jgr_capture.cpp neo/bot/behavior/neo_bot_jgr_capture.h neo/bot/behavior/neo_bot_jgr_enemy.cpp diff --git a/src/game/server/NextBot/Player/NextBotPlayer.h b/src/game/server/NextBot/Player/NextBotPlayer.h index 2a3c846325..d729256085 100644 --- a/src/game/server/NextBot/Player/NextBotPlayer.h +++ b/src/game/server/NextBot/Player/NextBotPlayer.h @@ -106,6 +106,9 @@ class INextBotPlayerInput virtual void PressReloadButton( float duration = -1.0f ) = 0; virtual void ReleaseReloadButton( void ) = 0; + virtual void PressDropButton( float duration = -1.0f ) = 0; + virtual void ReleaseDropButton( void ) = 0; + virtual void PressForwardButton( float duration = -1.0f ) = 0; virtual void ReleaseForwardButton( void ) = 0; @@ -212,6 +215,9 @@ class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerI virtual void PressReloadButton( float duration = -1.0f ); virtual void ReleaseReloadButton( void ); + virtual void PressDropButton( float duration = -1.0f ); + virtual void ReleaseDropButton( void ); + virtual void PressForwardButton( float duration = -1.0f ); virtual void ReleaseForwardButton( void ); @@ -277,6 +283,7 @@ class NextBotPlayer : public PlayerType, public INextBot, public INextBotPlayerI CountdownTimer m_specialFireButtonTimer; CountdownTimer m_useButtonTimer; CountdownTimer m_reloadButtonTimer; + CountdownTimer m_dropButtonTimer; CountdownTimer m_forwardButtonTimer; CountdownTimer m_backwardButtonTimer; CountdownTimer m_leftButtonTimer; @@ -430,6 +437,20 @@ inline void NextBotPlayer< PlayerType >::ReleaseReloadButton( void ) m_reloadButtonTimer.Invalidate(); } +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::PressDropButton( float duration ) +{ + m_inputButtons |= IN_DROP; + m_dropButtonTimer.Start( duration ); +} + +template < typename PlayerType > +inline void NextBotPlayer< PlayerType >::ReleaseDropButton( void ) +{ + m_inputButtons &= ~IN_DROP; + m_dropButtonTimer.Invalidate(); +} + template < typename PlayerType > inline void NextBotPlayer< PlayerType >::PressJumpButton( float duration ) { @@ -631,6 +652,7 @@ inline void NextBotPlayer< PlayerType >::Spawn( void ) m_specialFireButtonTimer.Invalidate(); m_useButtonTimer.Invalidate(); m_reloadButtonTimer.Invalidate(); + m_dropButtonTimer.Invalidate(); m_forwardButtonTimer.Invalidate(); m_backwardButtonTimer.Invalidate(); m_leftButtonTimer.Invalidate(); @@ -758,6 +780,9 @@ inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void ) if ( !m_reloadButtonTimer.IsElapsed() ) m_inputButtons |= IN_RELOAD; + if ( !m_dropButtonTimer.IsElapsed() ) + m_inputButtons |= IN_DROP; + if ( !m_forwardButtonTimer.IsElapsed() ) m_inputButtons |= IN_FORWARD; diff --git a/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp b/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp index 40336fdbd7..855162fd41 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_behavior.cpp @@ -90,6 +90,13 @@ ActionResult< CNEOBot > CNEOBotMainAction::Update( CNEOBot *me, float interval ) // make sure our vision FOV matches the player's me->GetVisionInterface()->SetFieldOfView( me->GetFOV() ); + if (me->IsCarryingGhost()) + { + // Don't waste cloak power + // Incidentally flashing cloak is fine, everyone can see you anyway + me->DisableCloak(); + } + // track aim velocity ourselves, since body aim "steady" is too loose float deltaYaw = me->EyeAngles().y - m_priorYaw; m_yawRate = fabs( deltaYaw / ( interval + 0.0001f ) ); diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp index adaa7545a1..7c125e1af1 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.cpp @@ -54,6 +54,7 @@ ActionResult< CNEOBot > CNEOBotCommandFollow::Update(CNEOBot *me, float interval } m_path.Update(me); + m_ghostEquipmentHandler.Update( me ); return Continue(); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h index 49035ca12a..3920b908fb 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_command_follow.h +++ b/src/game/server/neo/bot/behavior/neo_bot_command_follow.h @@ -5,6 +5,7 @@ #endif #include "NextBotBehavior.h" +#include "bot/behavior/neo_bot_ctg_carrier.h" class CNEOBotCommandFollow : public Action< CNEOBot > { @@ -24,6 +25,7 @@ class CNEOBotCommandFollow : public Action< CNEOBot > PathFollower m_path; CountdownTimer m_repathTimer; + CNEOBotGhostEquipmentHandler m_ghostEquipmentHandler; EHANDLE m_hTargetEntity; bool m_bGoingToTargetEntity = false; 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 new file mode 100644 index 0000000000..c7626d4dfd --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.cpp @@ -0,0 +1,90 @@ +#include "cbase.h" +#include "bot/behavior/neo_bot_ctg_capture.h" +#include "bot/behavior/neo_bot_seek_weapon.h" +#include "bot/neo_bot_path_compute.h" +#include "weapon_ghost.h" + + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgCapture::CNEOBotCtgCapture( CWeaponGhost *pObjective ) +{ + m_hObjective = pObjective; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotCtgCapture::OnStart( CNEOBot *me, Action *priorAction ) +{ + m_captureAttemptTimer.Invalidate(); + m_repathTimer.Invalidate(); + m_path.Invalidate(); + + if ( !m_hObjective ) + { + return Done( "No ghost capture target specified." ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult CNEOBotCtgCapture::Update( CNEOBot *me, float interval ) +{ + if ( me->IsDead() ) + { + return Done( "I died before I could capture the ghost" ); + } + + if ( !m_hObjective ) + { + return Done( "Ghost capture target lost" ); + } + + if ( me->IsCarryingGhost() ) + { + return Done( "Captured ghost" ); + } + + if ( m_hObjective->GetOwner() ) + { + return Done( "Ghost was taken by someone else" ); + } + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + if ( !CNEOBotPathCompute( me, m_path, m_hObjective->GetAbsOrigin(), FASTEST_ROUTE ) ) + { + return Done( "Unable to find a path to the ghost capture target" ); + } + m_repathTimer.Start( RandomFloat( 0.3f, 0.6f ) ); + } + m_path.Update( me ); + + if ( !m_captureAttemptTimer.HasStarted() ) + { + // If this timer expires, give up + m_captureAttemptTimer.Start( 3.0f ); + } + + CBaseCombatWeapon *pPrimary = me->Weapon_GetSlot( 0 ); + if ( pPrimary ) + { + // Switch to primary weapon to drop it, if not already active + if ( me->GetActiveWeapon() != pPrimary ) + { + me->Weapon_Switch( pPrimary ); + } + else + { + me->PressDropButton( 0.1f ); + } + } + + if ( m_captureAttemptTimer.IsElapsed() ) + { + return ChangeTo( new CNEOBotSeekWeapon, "Failed to capture ghost in time" ); + } + + return Continue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h new file mode 100644 index 0000000000..324450081e --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_capture.h @@ -0,0 +1,26 @@ +#ifndef NEO_BOT_CTG_CAPTURE_H +#define NEO_BOT_CTG_CAPTURE_H + +#include "NextBotBehavior.h" +#include "bot/neo_bot.h" + +//---------------------------------------------------------------------------------------------------------------- +class CNEOBotCtgCapture : public Action +{ +public: + CNEOBotCtgCapture( CWeaponGhost *pObjective ); + virtual ~CNEOBotCtgCapture() { } + + virtual const char *GetName() const override { return "ctgCapture"; } + + virtual ActionResult OnStart( CNEOBot *me, Action *priorAction ) override; + virtual ActionResult Update( CNEOBot *me, float interval ) override; + +private: + CHandle m_hObjective; + CountdownTimer m_captureAttemptTimer; + CountdownTimer m_repathTimer; + PathFollower m_path; +}; + +#endif // NEO_BOT_CTG_CAPTURE_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 new file mode 100644 index 0000000000..ad8d6c8c88 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.cpp @@ -0,0 +1,653 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#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 "neo_gamerules.h" +#include "neo_ghost_cap_point.h" +#include "debugoverlay_shared.h" +#include "weapon_ghost.h" + +ConVar neo_debug_ghost_carrier( "neo_debug_ghost_carrier", "0", FCVAR_CHEAT ); + + +//--------------------------------------------------------------------------------------------- +static void CollectPlayers( CNEOBot *me, CUtlVector *pOutTeammates, CUtlVector *pOutEnemies = nullptr ) +{ + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer || !pPlayer->IsAlive() ) + { + continue; + } + + if ( pPlayer->GetTeamNumber() == me->GetTeamNumber() ) + { + if ( pPlayer != me && pOutTeammates ) + { + pOutTeammates->AddToTail( pPlayer ); + } + } + else if ( pOutEnemies ) + { + pOutEnemies->AddToTail( pPlayer ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +CNEOBotGhostEquipmentHandler::CNEOBotGhostEquipmentHandler() +{ + m_hCurrentFocusEnemy = nullptr; + m_enemyUpdateTimer.Invalidate(); + + for ( int i = 0; i < MAX_PLAYERS_ARRAY_SAFE; ++i ) + { + m_enemyLastPos[i] = CNEO_Player::VECTOR_INVALID_WAYPOINT; + } +} + +void CNEOBotGhostEquipmentHandler::Update( CNEOBot *me ) +{ + if ( !me->IsAlive() ) + { + return; + } + + if ( !me->IsCarryingGhost() ) + { + return; + } + + if ( !m_enemyUpdateTimer.HasStarted() ) + { + m_enemyUpdateTimer.Start( GetUpdateInterval( me ) ); + } + + EquipBestWeaponForGhoster( me ); + + bool bUpdateCallout = m_enemyUpdateTimer.IsElapsed(); + if ( bUpdateCallout ) + { + m_enemies.RemoveAll(); + CollectPlayers( me, nullptr, &m_enemies ); + UpdateGhostCarrierCallout( me, m_enemies ); + m_enemyUpdateTimer.Start( GetUpdateInterval( me ) ); + } + + // Debug: Highlight the location of the enemy a bot ghost carrier is calling out + if ( neo_debug_ghost_carrier.GetBool() ) + { + CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); + if ( pFocus && pFocus->IsPlayer() && pFocus->IsAlive() ) + { + NDebugOverlay::Cross3D( pFocus->GetAbsOrigin(), 20.0f, 255, 0, 0, true, 0.1f ); + } + } + + CBaseEntity *pFocus = m_hCurrentFocusEnemy.Get(); + if ( pFocus && pFocus->IsAlive() ) + { + // Notify teammates to look at the enemy + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pTeammate = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pTeammate || !pTeammate->IsAlive() || pTeammate == me || pTeammate->GetTeamNumber() != me->GetTeamNumber() ) + { + continue; + } + + CNEOBot *pBot = ToNEOBot( pTeammate ); + if ( pBot ) + { + if ( pBot->IsLineOfFireClear( pFocus, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + // NEO Jank: Urge relevant teammate bots look at the enemy + pBot->GetBodyInterface()->AimHeadTowards( pFocus, IBody::IMPORTANT, 0.5f, nullptr, "Ghost carrier teammate look override" ); + } + else + { + // Force updates to known but not visible entity by forgetting them first + pBot->GetVisionInterface()->ForgetEntity( pFocus ); + } + pBot->GetVisionInterface()->AddKnownEntity( pFocus ); // keep after ForgetEntity + } + } + + me->GetBodyInterface()->AimHeadTowards( pFocus->WorldSpaceCenter(), IBody::IMPORTANT, 1.0f, nullptr, "Ghost carrier focus look" ); + } +} + +void CNEOBotGhostEquipmentHandler::EquipBestWeaponForGhoster( CNEOBot *me ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + + CNEOBaseCombatWeapon *pActive = dynamic_cast( me->GetActiveWeapon() ); + CNEOBaseCombatWeapon *pGhost = dynamic_cast( me->Weapon_GetSlot( 0 ) ); + CNEOBaseCombatWeapon *pSecondary = dynamic_cast( me->Weapon_GetSlot( 1 ) ); + + // Sanity check: if we don't have the ghost, we shouldn't be in this behavior, but handle gracefully + if ( !pGhost ) + { + // Fallback to normal behavior if ghost is missing + me->EquipBestWeaponForThreat( threat ); + return; + } + + // See if we need to defend ourselves + bool bHasThreat = ( threat && threat->GetEntity() && threat->GetEntity()->IsAlive() ); + if ( bHasThreat ) + { + bool bCanSeeThreat = me->GetVisionInterface()->IsLineOfSightClear( threat->GetEntity()->WorldSpaceCenter() ); + if ( bCanSeeThreat ) + { + if ( pSecondary && pSecondary->HasAmmo() ) + { + // We can still call out enemies the old fashioned way ;) + me->Weapon_Switch( pSecondary ); + return; + } + } + } + + // Equip ghost to call out enemies behind walls + if ( pActive != pGhost ) + { + me->Weapon_Switch( pGhost ); + } +} + +float CNEOBotGhostEquipmentHandler::GetUpdateInterval( CNEOBot *me ) const +{ + switch ( me->GetDifficulty() ) + { + case CNEOBot::EASY: return 2.5f; + case CNEOBot::NORMAL: return 2.0f; + case CNEOBot::HARD: return 1.5f; + case CNEOBot::EXPERT: return 1.0f; + } + + return 3.0f; +} + +void CNEOBotGhostEquipmentHandler::UpdateGhostCarrierCallout( CNEOBot *me, const CUtlVector &enemies ) +{ + CNEO_Player *pBestCallout = nullptr; + float flBestCalloutMoved = -1.0f; + float flBestCalloutDist = FLT_MAX; + bool bBestCalloutIsNew = false; + + bool bConsideringOnlyLoSEnemies = false; + + const Vector vecMyPos = me->GetAbsOrigin(); + + for ( int i = 0; i < enemies.Count(); ++i ) + { + CNEO_Player *pPlayer = enemies[i]; + + if ( pPlayer->IsEffectActive( EF_NODRAW ) ) + { + continue; + } + + int idx = pPlayer->entindex(); + if ( !IsIndexIntoPlayerArrayValid( idx ) ) + { + continue; + } + + // IsAbleToSee already checks if ghost is booted up to see enemies behind walls + if ( !me->GetVisionInterface()->IsAbleToSee( pPlayer, IVision::DISREGARD_FOV ) ) + { + continue; + } + + float flDistToMe = vecMyPos.DistTo( pPlayer->GetAbsOrigin() ); + + // Also take into account how much the enemy has moved since we last reported them + Vector vecLast = m_enemyLastPos[ idx ]; + float flMoved = 0.0f; + bool bIsNew = ( vecLast == CNEO_Player::VECTOR_INVALID_WAYPOINT ); + + if ( !bIsNew ) + { + flMoved = ( pPlayer->GetAbsOrigin() - vecLast ).Length(); + } + + // Any enemies in line of sight have the top priority + if ( me->GetVisionInterface()->IsLineOfSightClear( pPlayer->WorldSpaceCenter() ) ) + { + if ( !bConsideringOnlyLoSEnemies ) + { + bConsideringOnlyLoSEnemies = true; + pBestCallout = pPlayer; + bBestCalloutIsNew = bIsNew; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + else + { + if ( flDistToMe < flBestCalloutDist ) + { + pBestCallout = pPlayer; + bBestCalloutIsNew = bIsNew; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + } + continue; + } + + if ( bConsideringOnlyLoSEnemies ) + { + // we don't need to consider other criteria anymore if we are only considering LoS enemies + continue; + } + + if ( !pBestCallout ) + { + pBestCallout = pPlayer; + bBestCalloutIsNew = bIsNew; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + continue; + } + + // Consider an enemy we haven't called out yet + if ( bIsNew && !bBestCalloutIsNew ) + { + pBestCallout = pPlayer; + bBestCalloutIsNew = true; + flBestCalloutDist = flDistToMe; + flBestCalloutMoved = 0; + continue; + } + + if ( !bIsNew && bBestCalloutIsNew ) + { + // Existing candidate is new, current is old -> ignore current + continue; + } + + // Tie breakers between candidates + if ( bIsNew ) + { + // Both New: Tie breaker is distance to me (closest first) + if ( flDistToMe < flBestCalloutDist ) + { + pBestCallout = pPlayer; + flBestCalloutDist = flDistToMe; + } + } + else + { + // Both Old: Prioritize one that has moved more since last callout + float diff = flMoved - flBestCalloutMoved; + + // Check absolute difference to see if it is significant or not + if ( FloatMakePositive( diff ) < 100.0f ) + { + // Roughly same movement, tie breaker is closest distance + if ( flDistToMe < flBestCalloutDist ) + { + pBestCallout = pPlayer; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + } + else if ( diff > 0.0f ) + { + // Distinctly moved more + pBestCallout = pPlayer; + flBestCalloutMoved = flMoved; + flBestCalloutDist = flDistToMe; + } + } + } + + if ( pBestCallout ) + { + // NEO Jank: Ideally we could detect the enemy in the middle of the bot's screen, but they tend to look erratically + // It's easier to just set an enemy handle to appromimate a human focusing on calling out one enemy + m_hCurrentFocusEnemy = pBestCallout; + + // Update cache for this enemy so we know how much they moved next time + int idx = pBestCallout->entindex(); + if ( IsIndexIntoPlayerArrayValid( idx ) ) + { + m_enemyLastPos[ idx ] = pBestCallout->GetAbsOrigin(); + } + } +} + + + +//--------------------------------------------------------------------------------------------- +// CNEOBotCtgCarrier Implementation +//--------------------------------------------------------------------------------------------- + +CNEOBotCtgCarrier::CNEOBotCtgCarrier( void ) +{ + m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgCarrier::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_chasePath.Invalidate(); + m_aloneTimer.Invalidate(); + m_repathTimer.Invalidate(); + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + + m_closestCapturePoint = GetNearestCapPoint( me ); + + UpdateFollowPath( me, m_teammates ); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgCarrier::Update( CNEOBot *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done( "I am dead" ); + } + + if ( !me->IsCarryingGhost() ) + { + return Done( "No longer carrying the ghost" ); + } + + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + + if ( m_teammates.Count() == 0 ) + { + return SuspendFor( new CNEOBotCtgLoneWolf, "I'm the last one!" ); + } + + UpdateFollowPath( me, m_teammates ); + + m_ghostEquipmentHandler.Update( me ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +Vector CNEOBotCtgCarrier::GetNearestCapPoint( const CNEOBot *me ) const +{ + if ( !me ) + return CNEO_Player::VECTOR_INVALID_WAYPOINT; + + const int iMyTeam = me->GetTeamNumber(); + + if ( NEORules()->m_pGhostCaps.Count() > 0 ) + { + Vector bestPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + float flNearestCapDistSq = FLT_MAX; + for( int i=0; im_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if (!pCapPoint) + { + continue; + } + + int iCapTeam = pCapPoint->owningTeamAlternate(); + if ( iCapTeam == iMyTeam ) + { + float distanceToCap = me->GetAbsOrigin().DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( distanceToCap < flNearestCapDistSq ) + { + flNearestCapDistSq = distanceToCap; + bestPos = pCapPoint->GetAbsOrigin(); + } + } + } + return bestPos; + } + + return CNEO_Player::VECTOR_INVALID_WAYPOINT; +} + + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgCarrier::UpdateFollowPath( CNEOBot *me, const CUtlVector &teammates ) +{ + // Strategy: + // 1. Identify valid goal (Capture Point). + // 2. Scan all teammates: + // - Track "Nearest Spatial Teammate" (fallback if no LOS). + // - Track "Teammate Closest to Goal" (primary target if LOS exists). + // 3. Decision: + // - If I see any teammate -> Chase the one closest to the goal. + // - If I see NO teammates -> Chase the one spatially closest to me (regroup). + // - If no teammates exist -> Fallback to Goal (though Update() should catch this as a transition to LoneWolf). + + // Identify the goal first + Vector vecGoalPos = m_closestCapturePoint; + bool bFoundGoal = ( vecGoalPos != CNEO_Player::VECTOR_INVALID_WAYPOINT ); + + if ( vecGoalPos == CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + bFoundGoal = false; + } + + CNEO_Player *pTeammateNearestToMe = nullptr; + float flTeammateNearestToMeDistSq = FLT_MAX; + + CNEO_Player *pBestGoalTeammate = nullptr; + float flBestGoalTeammateDistSq = FLT_MAX; + + for ( int i = 0; i < teammates.Count(); i++ ) + { + CNEO_Player *pPlayer = teammates[i]; + + // Find the teammate nearest to me + float distanceToMe = pPlayer->GetAbsOrigin().DistToSqr( me->GetAbsOrigin() ); + if ( distanceToMe < flTeammateNearestToMeDistSq ) + { + flTeammateNearestToMeDistSq = distanceToMe; + pTeammateNearestToMe = pPlayer; + } + + // Find the teammate closest to the goal + if ( bFoundGoal ) + { + float distanceToGoal = pPlayer->GetAbsOrigin().DistToSqr( vecGoalPos ); + if ( distanceToGoal < flBestGoalTeammateDistSq ) + { + flBestGoalTeammateDistSq = distanceToGoal; + pBestGoalTeammate = pPlayer; + } + } + } + + if ( !bFoundGoal ) + { + // If we have no goal (defending team on asymmetric map), we should just stay put and call out targets. + m_chasePath.Invalidate(); + m_path.Invalidate(); + return; + } + + // If we are safe to cap (closer than enemies), just go for it! + if ( bFoundGoal ) + { + // We need to know where enemies are to determine if we are safe to cap + CWeaponGhost *pGhost = dynamic_cast( me->Weapon_GetSlot( 0 ) ); + if ( pGhost && pGhost->IsGhost() && pGhost->IsBootupCompleted() ) + { + float flDistMeToGoalSq = me->GetAbsOrigin().DistToSqr( vecGoalPos ); + + bool bAnyEnemyCloser = false; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + { + float dSq = pPlayer->GetAbsOrigin().DistToSqr( vecGoalPos ); + if ( dSq <= flDistMeToGoalSq ) + { + bAnyEnemyCloser = true; + break; + } + } + } + + if ( !bAnyEnemyCloser ) + { + m_chasePath.Invalidate(); + m_path.Invalidate(); + + if ( !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, vecGoalPos, FASTEST_ROUTE ); + m_path.Update( me ); + m_repathTimer.Start( RandomFloat( 0.3f, 0.5f ) ); + } + else + { + m_path.Update( me ); + } + return; + } + } + } + + // Choose which teammate to follow + CNEO_Player *pTargetTeammate = nullptr; + + // Check if we are relatively protected by teammates before running after the vanguard + bool bIsNearEscort = false; + if ( pTeammateNearestToMe ) + { + if ( me->GetVisionInterface()->IsLineOfSightClear( pTeammateNearestToMe->WorldSpaceCenter() ) ) + { + bIsNearEscort = true; + } + } + + if ( bIsNearEscort ) + { + m_aloneTimer.Invalidate(); + + // If we are near a teammate, advance towards goal with vanguard + if ( pBestGoalTeammate ) + { + pTargetTeammate = pBestGoalTeammate; + } + else + { + // Fallback to nearest if no goal teammate + // (unlikely if we are near a teammate, but in case there's a logic bug) + pTargetTeammate = pTeammateNearestToMe; + } + } + else + { + // We are isolated from our teammates + if ( !m_aloneTimer.HasStarted() ) + { + m_aloneTimer.Start( 3.0f ); + } + + if ( !m_aloneTimer.IsElapsed() ) + { + // grace period: keep running towards the vanguard in case we can catch up with them + // helps keep following vanguard in case they round a corner or some other visual obstacle + if ( pBestGoalTeammate ) + { + pTargetTeammate = pBestGoalTeammate; + } + else + { + pTargetTeammate = pTeammateNearestToMe; + } + } + else + { + // regroup with nearest friendlies if alone for too long + pTargetTeammate = pTeammateNearestToMe; + } + } + + if ( pTargetTeammate ) + { + float flDistToTeammate = me->GetAbsOrigin().DistTo( pTargetTeammate->GetAbsOrigin() ); + if ( flDistToTeammate < 100.0f ) + { + m_chasePath.Invalidate(); + return; + } + + m_path.Invalidate(); + + // chase after the chosen teammate + CNEOBotPathUpdateChase( me, m_chasePath, pTargetTeammate, SAFEST_ROUTE ); + return; + } + + // No teammates alive? + // Update() should handle the LastStand transition next tick, but just in case we fall through here: + m_chasePath.Invalidate(); + + if ( bFoundGoal ) + { + if ( !m_path.IsValid() || !m_repathTimer.HasStarted() || m_repathTimer.IsElapsed() ) + { + CNEOBotPathCompute( me, m_path, vecGoalPos, SAFEST_ROUTE ); + m_repathTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + } + m_path.Update( me ); + } + else + { + // Usually defending the ghost with no capture point + m_path.Invalidate(); + } +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgCarrier::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + + // Re-evaluate nearest cap point on resume (in case we moved significantly while interrupted) + m_closestCapturePoint = GetNearestCapPoint( me ); + + m_repathTimer.Invalidate(); + UpdateFollowPath( me, m_teammates ); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgCarrier::OnStuck( CNEOBot *me ) +{ + m_teammates.RemoveAll(); + CollectPlayers( me, &m_teammates ); + UpdateFollowPath( me, m_teammates ); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgCarrier::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgCarrier::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h new file mode 100644 index 0000000000..d857c3aa2c --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_carrier.h @@ -0,0 +1,59 @@ +#ifndef NEO_BOT_CTG_CARRIER_H +#define NEO_BOT_CTG_CARRIER_H + +#include "bot/neo_bot.h" +#include "Path/NextBotChasePath.h" + +class CNEO_Player; + +//-------------------------------------------------------------------------------------------------------- +// Common ghost equipment usage routines. Also used by CNEOBotCommandFollow. +class CNEOBotGhostEquipmentHandler +{ +public: + CNEOBotGhostEquipmentHandler(); + void Update( CNEOBot *me ); + +private: + void EquipBestWeaponForGhoster( CNEOBot *me ); + float GetUpdateInterval( CNEOBot *me ) const; + void UpdateGhostCarrierCallout( CNEOBot *me, const CUtlVector &enemies ); + + EHANDLE m_hCurrentFocusEnemy{nullptr}; + CountdownTimer m_enemyUpdateTimer; + Vector m_enemyLastPos[MAX_PLAYERS_ARRAY_SAFE]; + CUtlVector m_enemies; +}; + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgCarrier : public Action< CNEOBot > +{ +public: + CNEOBotCtgCarrier( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + 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 "ctgGhostCarrier"; } + +private: + PathFollower m_path; + ChasePath m_chasePath; + CountdownTimer m_aloneTimer; + CountdownTimer m_repathTimer; + + CNEOBotGhostEquipmentHandler m_ghostEquipmentHandler; + + Vector m_closestCapturePoint; + CUtlVector m_teammates; + + Vector GetNearestCapPoint( const CNEOBot *me ) const; + void UpdateFollowPath( CNEOBot *me, const CUtlVector &teammates ); +}; + +#endif // NEO_BOT_CTG_CARRIER_H \ No newline at end of file diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp new file mode 100644 index 0000000000..5429f4989b --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.cpp @@ -0,0 +1,84 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_enemy.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" + + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEnemy::CNEOBotCtgEnemy( void ) +{ +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemy::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() ); + m_repathTimer.Invalidate(); + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemy::Update( CNEOBot *me, float interval ) +{ + if ( !NEORules()->GhostExists() ) + { + return Done( "Ghost does not exist" ); + } + + if ( NEORules()->GetGhosterPlayer() <= 0 ) + { + return Done( "No ghost carrier" ); + } + + CNEO_Player* pGhostCarrier = ToNEOPlayer( UTIL_PlayerByIndex( NEORules()->GetGhosterPlayer() ) ); + if ( !pGhostCarrier || pGhostCarrier->GetTeamNumber() == me->GetTeamNumber() ) + { + return Done( "Ghost carrier is friendly" ); + } + + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( threat && !threat->IsObsolete() && me->GetIntentionInterface()->ShouldAttack( me, threat ) ) + { + return SuspendFor( new CNEOBotAttack, "Attacking ghoster team" ); + } + + // chase the ghost carrier + if ( m_repathTimer.IsElapsed() ) + { + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, DEFAULT_ROUTE ); + m_repathTimer.Start( RandomFloat( 0.2f, 1.0f ) ); + } + m_path.Invalidate(); + + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEnemy::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEnemy::OnStuck( CNEOBot *me ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEnemy::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEnemy::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + return TryContinue(); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h new file mode 100644 index 0000000000..50513980fb --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_enemy.h @@ -0,0 +1,29 @@ +#ifndef NEO_BOT_CTG_ENEMY_H +#define NEO_BOT_CTG_ENEMY_H + +#include "bot/neo_bot.h" +#include "Path/NextBotChasePath.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgEnemy : public Action< CNEOBot > +{ +public: + CNEOBotCtgEnemy( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + 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 "ctgEnemy"; } + +private: + ChasePath m_chasePath; + PathFollower m_path; + CountdownTimer m_repathTimer; +}; + +#endif // NEO_BOT_CTG_ENEMY_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp new file mode 100644 index 0000000000..662141c0e8 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.cpp @@ -0,0 +1,349 @@ +#include "cbase.h" +#include "neo_player.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_escort.h" +#include "bot/behavior/neo_bot_attack.h" +#include "bot/neo_bot_path_compute.h" +#include "neo_gamerules.h" +#include "neo_ghost_cap_point.h" +#include "weapons/weapon_ghost.h" + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEscort::CNEOBotCtgEscort( void ) : + m_role( ROLE_SCREEN ), + m_bHasGoal( false ) +{ + m_vecGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_repathTimer.Invalidate(); + m_lostSightOfCarrierTimer.Invalidate(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEscort::OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) +{ + m_chasePath.Invalidate(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + m_lostSightOfCarrierTimer.Invalidate(); + + m_role = ROLE_SCREEN; + m_vecGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_bHasGoal = false; + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEscort::Update( CNEOBot *me, float interval ) +{ + if ( !NEORules()->GhostExists() ) + { + return Done( "Ghost does not exist" ); + } + + if ( NEORules()->GetGhosterPlayer() <= 0 ) + { + return Done( "Ghost is not held by a player" ); + } + + CNEO_Player* pGhostCarrier = ToNEOPlayer( UTIL_PlayerByIndex( NEORules()->GetGhosterPlayer() ) ); + if ( !pGhostCarrier || pGhostCarrier->GetTeamNumber() != me->GetTeamNumber() || pGhostCarrier == me ) + { + return Done( "Ghost carrier is not a teammate anymore" ); + } + + // Check if we can assist the Ghost Carrier (if they are a bot) + CNEOBot *pBotGhostCarrier = ToNEOBot( pGhostCarrier ); + if ( pBotGhostCarrier ) + { + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( carrierThreat ) + { + // Check if the threat has a clear shot at our friend + if ( me->IsLineOfFireClear( carrierThreat->GetLastKnownPosition(), pGhostCarrier, CNEOBot::LINE_OF_FIRE_FLAGS_DEFAULT ) ) + { + return SuspendFor( new CNEOBotAttack, "Assisting Ghost Carrier with their threat" ); + } + } + } + + if ( m_repathTimer.IsElapsed() ) + { + UpdateGoalPosition( me, pGhostCarrier ); + + if ( m_bHasGoal ) + { + m_role = UpdateRoleAssignment( me, pGhostCarrier, m_vecGoalPos ); + } + else + { + m_role = ROLE_SCREEN; + } + } + + bool bCanSeeCarrier = me->GetVisionInterface()->IsLineOfSightClear( pGhostCarrier->WorldSpaceCenter() ); + if ( bCanSeeCarrier ) + { + m_lostSightOfCarrierTimer.Invalidate(); + + if ( !m_bHasGoal ) + { + // Asymmetric defense: No goal cap zone, so defend the carrier. + // Look away from the carrier to cover their blind spots + Vector vecFromCarrier = me->GetAbsOrigin() - pGhostCarrier->GetAbsOrigin(); + vecFromCarrier.z = 0.0f; // Bias towards horizontal scanning + if ( VectorNormalize( vecFromCarrier ) > 0.1f ) + { + // Look at a point far away in the opposite direction of the carrier + Vector vecLookTarget = me->EyePosition() + ( vecFromCarrier * 500.0f ); + me->GetBodyInterface()->AimHeadTowards( vecLookTarget, IBody::IMPORTANT, 0.2f, nullptr, "Escort: Scanning away from carrier" ); + } + } + } + else + { + // If we can't see them, check if we are in grace period + if ( !m_lostSightOfCarrierTimer.HasStarted() ) + { + // JUST lost sight + m_lostSightOfCarrierTimer.Start( 3.0f ); + // Treat as visible for now + bCanSeeCarrier = true; + } + else if ( !m_lostSightOfCarrierTimer.IsElapsed() ) + { + // Still in grace period + bCanSeeCarrier = true; + } + else + { + // Timer elapsed, truly lost sight + bCanSeeCarrier = false; + } + } + + // Check again based on grace period checks above + if ( !bCanSeeCarrier ) + { + m_path.Invalidate(); + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); + } + else + { + if ( m_role == ROLE_BODYGUARD ) + { + // Dont crowd the carrier + if ( me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ) < ( 100.0f * 100.0f ) ) + { + m_path.Invalidate(); + m_chasePath.Invalidate(); + return Continue(); + } + + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); + return Continue(); + } + + if ( !m_bHasGoal ) + { + // Asymmetric defense: No goal cap zone, so defend the carrier. + if ( pBotGhostCarrier ) + { + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( carrierThreat && carrierThreat->GetEntity() && carrierThreat->GetEntity()->IsAlive() ) + { + me->GetVisionInterface()->AddKnownEntity( carrierThreat->GetEntity() ); + return SuspendFor( new CNEOBotAttack, "Attacking enemy during asymmetric defense" ); + } + } + + // No active threats to carrier + float flDistToCarrierSq = me->GetAbsOrigin().DistToSqr( pGhostCarrier->GetAbsOrigin() ); + constexpr float regroupDistanceSq = 300.0f * 300.0f; + if ( flDistToCarrierSq > regroupDistanceSq ) + { + // Regroup + CNEOBotPathUpdateChase( me, m_chasePath, pGhostCarrier, SAFEST_ROUTE ); + } + else + { + // Hold position + m_chasePath.Invalidate(); + m_path.Invalidate(); + } + return Continue(); + } + + m_chasePath.Invalidate(); + + Vector vecMoveTarget = m_vecGoalPos; + + + if ( m_role == ROLE_SCREEN && pBotGhostCarrier ) + { + CWeaponGhost *pGhost = dynamic_cast( pBotGhostCarrier->Weapon_GetSlot( 0 ) ); + // Only screen the threat if the ghost is actually booted up and ready to reveal enemies + // Otherwise just stick to the goal path + if ( pGhost && pGhost->IsBootupCompleted() ) + { + const CKnownEntity *carrierThreat = pBotGhostCarrier->GetVisionInterface()->GetPrimaryKnownThreat(); + if ( carrierThreat && carrierThreat->GetEntity() && carrierThreat->GetEntity()->IsAlive() ) + { + vecMoveTarget = carrierThreat->GetLastKnownPosition(); + + } + } + } + + // Move to Target (Goal or Threat) + // Throttling repath to avoid excessive computations + if ( m_repathTimer.IsElapsed() ) + { + m_chasePath.Invalidate(); + m_path.Invalidate(); + + CNEOBotPathCompute( me, m_path, vecMoveTarget, SAFEST_ROUTE ); + m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + m_path.Update( me ); + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgEscort::OnResume( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + m_chasePath.Invalidate(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEscort::OnStuck( CNEOBot *me ) +{ + m_chasePath.Invalidate(); + m_path.Invalidate(); + m_repathTimer.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEscort::OnMoveToSuccess( CNEOBot *me, const Path *path ) +{ + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgEscort::OnMoveToFailure( CNEOBot *me, const Path *path, MoveToFailureType reason ) +{ + m_repathTimer.Invalidate(); + return TryContinue(); +} + +//--------------------------------------------------------------------------------------------- +CNEOBotCtgEscort::EscortRole CNEOBotCtgEscort::UpdateRoleAssignment( CNEOBot *me, CNEO_Player *pGhostCarrier, const Vector &vecGoalPos ) +{ + // Roles: + // 1. Scout/Vanguard: Closest to Goal. + // 2. Bodyguard: Closest to Carrier. + // 3. Wall/Screen: Everyone else. + + CNEO_Player* pScout = nullptr; + CNEO_Player* pBodyguard = nullptr; + const int iMyTeam = me->GetTeamNumber(); + + // Collect candidates and process roles in one pass + float flBestDistToGoal = FLT_MAX; + float flBestDistToCarrier = FLT_MAX; + float flSecondBestDistToCarrier = FLT_MAX; + CNEO_Player* pBestToCarrier = nullptr; + CNEO_Player* pSecondBestToCarrier = nullptr; + + Vector vecCarrierOrigin = pGhostCarrier->GetAbsOrigin(); + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( !pPlayer || !pPlayer->IsAlive() || pPlayer->GetTeamNumber() != iMyTeam || pPlayer == pGhostCarrier ) + { + continue; + } + + Vector vecPlayerOrigin = pPlayer->GetAbsOrigin(); + + // Check for Scout (Best dist to goal) + float goalDist = vecPlayerOrigin.DistToSqr( vecGoalPos ); + if ( goalDist < flBestDistToGoal ) + { + flBestDistToGoal = goalDist; + pScout = pPlayer; + } + + // Check for Bodyguard candidates (Dist to carrier) + float carrierDist = vecPlayerOrigin.DistToSqr( vecCarrierOrigin ); + if ( carrierDist < flBestDistToCarrier ) + { + flSecondBestDistToCarrier = flBestDistToCarrier; + pSecondBestToCarrier = pBestToCarrier; + + flBestDistToCarrier = carrierDist; + pBestToCarrier = pPlayer; + } + else if ( carrierDist < flSecondBestDistToCarrier ) + { + flSecondBestDistToCarrier = carrierDist; + pSecondBestToCarrier = pPlayer; + } + } + + // If the best bodyguard candidate is the scout, take the second best. + if ( pBestToCarrier && pBestToCarrier == pScout ) + { + pBodyguard = pSecondBestToCarrier; + } + else + { + pBodyguard = pBestToCarrier; + } + + if ( me == pScout ) + { + return ROLE_SCOUT; + } + else if ( me == pBodyguard ) + { + return ROLE_BODYGUARD; + } + else + { + return ROLE_SCREEN; + } +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgEscort::UpdateGoalPosition( CNEOBot *me, CNEO_Player *pGhostCarrier ) +{ + m_vecGoalPos = CNEO_Player::VECTOR_INVALID_WAYPOINT; + m_bHasGoal = false; + + float flNearestCapDistSq = FLT_MAX; + const int iMyTeam = me->GetTeamNumber(); + + for( int i=0; im_pGhostCaps.Count(); ++i ) + { + CNEOGhostCapturePoint *pCapPoint = dynamic_cast( UTIL_EntityByIndex( NEORules()->m_pGhostCaps[i] ) ); + if ( !pCapPoint ) continue; + if ( pCapPoint->owningTeamAlternate() == iMyTeam ) + { + float d = pGhostCarrier->GetAbsOrigin().DistToSqr( pCapPoint->GetAbsOrigin() ); + if ( d < flNearestCapDistSq ) + { + flNearestCapDistSq = d; + m_vecGoalPos = pCapPoint->GetAbsOrigin(); + m_bHasGoal = true; + } + } + } +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h new file mode 100644 index 0000000000..d8e932c5cf --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_escort.h @@ -0,0 +1,44 @@ +#ifndef NEO_BOT_CTG_ESCORT_H +#define NEO_BOT_CTG_ESCORT_H + +#include "bot/neo_bot.h" +#include "Path/NextBotChasePath.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgEscort : public Action< CNEOBot > +{ +public: + CNEOBotCtgEscort( void ); + + virtual ActionResult< CNEOBot > OnStart( CNEOBot *me, Action< CNEOBot > *priorAction ) override; + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + 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 "ctgEscort"; } + +private: + PathFollower m_path; + ChasePath m_chasePath; + CountdownTimer m_repathTimer; + CountdownTimer m_lostSightOfCarrierTimer; + + enum EscortRole + { + ROLE_SCREEN, + ROLE_SCOUT, + ROLE_BODYGUARD, + }; + EscortRole m_role; + + EscortRole UpdateRoleAssignment( CNEOBot *me, CNEO_Player *pGhostCarrier, const Vector &vecGoalPos ); + void UpdateGoalPosition( CNEOBot *me, CNEO_Player *pGhostCarrier ); + + Vector m_vecGoalPos; + bool m_bHasGoal; +}; + +#endif // NEO_BOT_CTG_ESCORT_H 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 new file mode 100644 index 0000000000..03f4658bb5 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.cpp @@ -0,0 +1,501 @@ +#include "cbase.h" +#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/neo_bot_path_compute.h" +#include "neo_gamerules.h" +#include "neo_ghost_cap_point.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(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::Update( CNEOBot *me, float interval ) +{ + const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat( true ); + + CBaseCombatWeapon *pWeapon = me->GetActiveWeapon(); + if ( !threat && pWeapon && pWeapon->UsesClipsForAmmo1() ) + { + if ( pWeapon->Clip1() < pWeapon->GetMaxClip1() && me->GetAmmoCount( pWeapon->GetPrimaryAmmoType() ) > 0 ) + { + // Aggressively reload due to lack of backup + me->PressReloadButton(); + } + } + + // We dropped the ghost to hunt a threat. + if ( m_bPursuingDropThreat ) + { + // 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!" ); + } + + CNEOBotPathCompute( me, m_path, m_vecDropThreatPos, FASTEST_ROUTE ); + m_path.Update( me ); + return Continue(); + } + } + + // Always need to find the ghost to act on it + if (!m_hGhost) + { + m_hGhost = dynamic_cast( gEntList.FindEntityByClassname(nullptr, "weapon_ghost") ); + } + + if (!m_hGhost) + { + return Done( "Ghost not found" ); + } + + // Occasionally reconsider which cap zone is our goal + if ( !m_capPointUpdateTimer.HasStarted() || m_capPointUpdateTimer.IsElapsed() ) + { + m_closestCapturePoint = CNEO_Player::VECTOR_INVALID_WAYPOINT; + float flNearestCapDist = FLT_MAX; + + if ( NEORules()->m_pGhostCaps.Count() > 0 ) + { + 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 d = vecStart.DistTo( pCapPoint->GetAbsOrigin() ); + if ( d < flNearestCapDist ) + { + flNearestCapDist = d; + m_closestCapturePoint = pCapPoint->GetAbsOrigin(); + } + } + } + } + m_capPointUpdateTimer.Start( RandomFloat( 0.5f, 1.0f ) ); + } + + float flDistGhostToGoal = FLT_MAX; + if ( m_closestCapturePoint != CNEO_Player::VECTOR_INVALID_WAYPOINT ) + { + Vector vecStart = me->IsCarryingGhost() ? me->GetAbsOrigin() : m_hGhost->GetAbsOrigin(); + flDistGhostToGoal = vecStart.DistTo( m_closestCapturePoint ); + } + + // 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() ) + { + flMyTotalDist += me->GetAbsOrigin().DistTo( m_hGhost->GetAbsOrigin() ); + } + + // Count enemies and find if one is closer to our goal + int iEnemyTeamCount = 0; + float flClosestEnemyDistToGoal = FLT_MAX; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + 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 d = pPlayer->GetAbsOrigin().DistTo( m_closestCapturePoint ); + if ( d < flClosestEnemyDistToGoal ) + { + flClosestEnemyDistToGoal = d; + if ( iEnemyTeamCount > 1 && flClosestEnemyDistToGoal < flMyTotalDist ) + { + // 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; + } + } + } + } + } + + // 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) && (flMyTotalDist < flClosestEnemyDistToGoal)); + + CWeaponGhost *pGhostWeapon = m_hGhost.Get(); + CBaseCombatCharacter *pGhostOwner = pGhostWeapon ? pGhostWeapon->GetOwner() : nullptr; + bool bGhostHeldByEnemy = (pGhostOwner && pGhostOwner->GetTeamNumber() != me->GetTeamNumber()); + + // Consider next action + if ( me->IsCarryingGhost() ) + { + if ( bSafeToCap ) + { + 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(); + } + 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(); + } + } + else if ( bGhostHeldByEnemy ) + { + // 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!"); + } + + 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); + } + return Continue(); + } + else + { + // Ghost is free for taking + if ( bSafeToCap || (bIs1v1 && m_stalemateTimer.HasStarted() && m_stalemateTimer.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(); + } + 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(); + } + } + } + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::OnSuspend( CNEOBot *me, Action< CNEOBot > *interruptingAction ) +{ + m_path.Invalidate(); + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +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(); +} + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CNEOBot > CNEOBotCtgLoneWolf::OnStuck( CNEOBot *me ) +{ + m_path.Invalidate(); + me->GetLocomotionInterface()->Jump(); + 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 +{ +public: + CCollectPotentiallyVisibleAreas( CUtlVector< CNavArea * > *collection ) + { + m_collection = collection; + } + + bool operator() ( CNavArea *baseArea ) + { + m_collection->AddToTail( baseArea ); + return true; + } + + CUtlVector< CNavArea * > *m_collection; +}; + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgLoneWolf::UpdateLookAround( CNEOBot *me, const Vector &anchorPos ) +{ + if ( !m_lookAroundTimer.HasStarted() || m_lookAroundTimer.IsElapsed() ) + { + // NEO Jank Cheat: Bots don't have a good intuition for where to look for threats + // So the compromise is to have them retreat from a threat when the latter shows up + // The looking around logic below is performative for spectators to justify why a bot might incidentally turn to see threat + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() != me->GetTeamNumber() ) + { + 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" ); + + const float maxLookInterval = 2.0f; + m_lookAroundTimer.Start(RandomFloat(0.5f, maxLookInterval)); + return Continue(); + } + } + } + } + + // Fallback scanning delay if we failed to find a spot + m_lookAroundTimer.Start(RandomFloat(0.3f, 1.0f)); + } + + return Continue(); +} 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 new file mode 100644 index 0000000000..ef9bf545c3 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_lone_wolf.h @@ -0,0 +1,44 @@ +#ifndef NEO_BOT_CTG_LONE_WOLF_H +#define NEO_BOT_CTG_LONE_WOLF_H + +#include "bot/neo_bot.h" + +//-------------------------------------------------------------------------------------------------------- +class CNEOBotCtgLoneWolf : public Action< CNEOBot > +{ +public: + CNEOBotCtgLoneWolf( void ); + + 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 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; + + ActionResult< CNEOBot > UpdateLookAround( CNEOBot *me, const Vector &anchorPos ); + CountdownTimer m_lookAroundTimer; + CountdownTimer m_stalemateTimer; + + CountdownTimer m_capPointUpdateTimer; + Vector m_closestCapturePoint; + + CUtlVector< CNavArea * > m_visibleAreas; +}; + +#endif // NEO_BOT_CTG_LAST_STAND_H diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp new file mode 100644 index 0000000000..00d25a4248 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.cpp @@ -0,0 +1,144 @@ +#include "cbase.h" +#include "neo_player.h" +#include "neo_gamerules.h" +#include "bot/neo_bot.h" +#include "bot/behavior/neo_bot_ctg_seek.h" +#include "bot/behavior/neo_bot_ctg_lone_wolf.h" +#include "bot/behavior/neo_bot_ctg_escort.h" +#include "bot/behavior/neo_bot_ctg_enemy.h" +#include "bot/behavior/neo_bot_ctg_carrier.h" +#include "bot/behavior/neo_bot_ctg_capture.h" +#include "bot/neo_bot_path_compute.h" +#include "weapon_ghost.h" + +//--------------------------------------------------------------------------------------------- +ActionResult< CNEOBot > CNEOBotCtgSeek::Update( CNEOBot *me, float interval ) +{ + if (NEORules()->GetGameType() != NEO_GAME_TYPE_CTG) + { + return Done( "Game mode is no longer CTG" ); + } + + ActionResult< CNEOBot > result = UpdateCommon( me, interval ); + if ( result.IsRequestingChange() || result.IsDone() ) + { + return result; + } + + int team_members = 0; + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CNEO_Player *pPlayer = ToNEOPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && pPlayer->GetTeamNumber() == me->GetTeamNumber() ) + { + team_members++; + } + } + + if ( team_members == 1 ) + { + return SuspendFor( new CNEOBotCtgLoneWolf, "I'm the last one on my team!" ); + } + + if (NEORules()->GhostExists()) + { + int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + if (iGhosterPlayer > 0 && iGhosterPlayer <= gpGlobals->maxClients) + { + CNEO_Player* pGhostCarrier = ToNEOPlayer(UTIL_PlayerByIndex(iGhosterPlayer)); + if (pGhostCarrier && pGhostCarrier != me) + { + if (pGhostCarrier->GetTeamNumber() == me->GetTeamNumber()) + { + return SuspendFor(new CNEOBotCtgEscort, "Protecting the ghost carrier!"); + } + else + { + return SuspendFor(new CNEOBotCtgEnemy, "Stopping the ghost carrier!"); + } + } + + // If I have the ghost, switch to ghost behavior + if (me->IsCarryingGhost()) + { + return SuspendFor(new CNEOBotCtgCarrier, "I am the ghost carrier!"); + } + } + } + + // Ghost capture logic + if (m_bGoingToTargetEntity && m_hTargetEntity) + { + const float useRangeSq = 100.0f * 100.0f; + if ( me->GetAbsOrigin().DistToSqr( m_hTargetEntity->GetAbsOrigin() ) < useRangeSq ) + { + if ( !m_hTargetEntity->IsPlayer() ) + { + if ( me->IsLineOfSightClear( m_hTargetEntity, CBaseCombatCharacter::IGNORE_ACTORS ) ) + { + const char *classname = m_hTargetEntity->GetClassname(); + if ( FStrEq( classname, "weapon_ghost" ) ) + { + CBaseCombatWeapon* pWeapon = m_hTargetEntity->MyCombatWeaponPointer(); + if ( pWeapon && !pWeapon->GetOwner() ) + { + CWeaponGhost *pGhost = dynamic_cast( m_hTargetEntity.Get() ); + return SuspendFor( new CNEOBotCtgCapture( pGhost ), "Capturing Ghost" ); + } + } + } + else + { + return Done("Capture target was not a ghost"); + } + } + } + } + + return Continue(); +} + +//--------------------------------------------------------------------------------------------- +void CNEOBotCtgSeek::RecomputeSeekPath( CNEOBot *me ) +{ + if (NEORules()->GetGameType() != NEO_GAME_TYPE_CTG) + { + // Wait until next tick to exit behavior + return; + } + + m_hTargetEntity = nullptr; + m_bGoingToTargetEntity = false; + m_vGoalPos = vec3_origin; + + if (NEORules()->GhostExists()) + { + const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); + + if (iGhosterPlayer > 0) + { + // Get ready to transition into CTG specific role next tick + m_path.Invalidate(); + return; + } + else + { + // Search for ghost on the ground + CBaseEntity* pGhost = gEntList.FindEntityByClassname( nullptr, "weapon_ghost" ); + if ( pGhost ) + { + m_vGoalPos = pGhost->WorldSpaceCenter(); + m_bGoingToTargetEntity = true; + m_hTargetEntity = pGhost; + + if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + { + return; + } + } + } + } + + // Fallback to base behavior (roaming spawn points) + CNEOBotSeekAndDestroy::RecomputeSeekPath( me ); +} diff --git a/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h new file mode 100644 index 0000000000..a09b305177 --- /dev/null +++ b/src/game/server/neo/bot/behavior/neo_bot_ctg_seek.h @@ -0,0 +1,18 @@ +#pragma once + +#include "bot/behavior/neo_bot_seek_and_destroy.h" + +// +// CTG game mode behavior dispatcher +// +class CNEOBotCtgSeek : public CNEOBotSeekAndDestroy +{ +public: + CNEOBotCtgSeek( float duration = -1.0f ) : CNEOBotSeekAndDestroy( duration ) { } + + virtual ActionResult< CNEOBot > Update( CNEOBot *me, float interval ) override; + virtual const char *GetName( void ) const override { return "ctgSeek"; }; + +protected: + virtual void RecomputeSeekPath( CNEOBot *me ) override; +}; diff --git a/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp b/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp index 0b37e748bb..2f1d9eb8da 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_get_health.cpp @@ -175,7 +175,7 @@ ActionResult< CNEOBot > CNEOBotGetHealth::OnStart( CNEOBot *me, Action< CNEOBot m_healthKit = s_possibleHealth; m_isGoalCharger = m_healthKit->ClassMatches( "*charger*" ); - if (!CNEOBotPathCompute(me, m_path, m_healthKit->WorldSpaceCenter(), SAFEST_ROUTE)) + if (!CNEOBotPathCompute(me, m_path, m_healthKit->WorldSpaceCenter(), DEFAULT_ROUTE)) { return Done( "No path to health!" ); } @@ -211,7 +211,7 @@ ActionResult< CNEOBot > CNEOBotGetHealth::Update( CNEOBot *me, float interval ) { // this can occur if we overshoot the health kit's location // because it is momentarily gone - if ( !CNEOBotPathCompute( me, m_path, m_healthKit->WorldSpaceCenter(), SAFEST_ROUTE ) ) + if ( !CNEOBotPathCompute( me, m_path, m_healthKit->WorldSpaceCenter(), DEFAULT_ROUTE ) ) { return Done( "No path to health!" ); } diff --git a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp index 8094c5ec3b..d7af5cc12f 100644 --- a/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp +++ b/src/game/server/neo/bot/behavior/neo_bot_seek_and_destroy.cpp @@ -5,10 +5,10 @@ #include "bot/neo_bot.h" #include "bot/behavior/neo_bot_attack.h" #include "bot/behavior/neo_bot_seek_and_destroy.h" +#include "bot/behavior/neo_bot_ctg_seek.h" #include "bot/behavior/neo_bot_jgr_seek.h" #include "bot/neo_bot_path_compute.h" #include "nav_mesh.h" -#include "neo_ghost_cap_point.h" extern ConVar neo_bot_path_lookahead_range; extern ConVar neo_bot_offense_must_push_time; @@ -58,7 +58,11 @@ ActionResult< CNEOBot > CNEOBotSeekAndDestroy::Update( CNEOBot *me, float interv return result; // Check for Game Type Specific behaviors and suspend for them - if (NEORules()->GetGameType() == NEO_GAME_TYPE_JGR) + if (NEORules()->GetGameType() == NEO_GAME_TYPE_CTG) + { + return SuspendFor( new CNEOBotCtgSeek, "Switching to Ghost-related Seek and Destroy" ); + } + else if (NEORules()->GetGameType() == NEO_GAME_TYPE_JGR) { return SuspendFor( new CNEOBotJgrSeek, "Switching to Juggernaut-related Seek and Destroy" ); } @@ -353,126 +357,12 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) m_hTargetEntity = pClosestWeapon; m_bGoingToTargetEntity = true; m_vGoalPos = pClosestWeapon->WorldSpaceCenter(); - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, SAFEST_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) return; } } } #endif - - if (NEORules()->GhostExists()) - { - const Vector vGhostPos = NEORules()->GetGhostPos(); - const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); - - const int iMyTeam = me->GetTeamNumber(); - const int iGhosterTeam = NEORules()->GetGhosterTeam(); - - bool bGoToGoalPos = true; - bool bGetCloserToGhoster = false; - bool bQuickToGoalPos = false; - - if (iGhosterPlayer > 0) - { - const int iTargetCapTeam = (iGhosterTeam == iMyTeam) ? iMyTeam : iGhosterTeam; - - // If there's a player playing ghost, turn toward cap zones that's - // closest to the ghoster player - Vector vrTargetCapPos; - int iMinCapGhostLength = INT_MAX; - - // Enemy team is carrying the ghost - try to defend the cap zone - // You or friendly team is carrying the ghost - go towards the cap point - - for (int i = 0; i < NEORules()->m_pGhostCaps.Count(); i++) - { - auto pGhostCap = dynamic_cast( - UTIL_EntityByIndex(NEORules()->m_pGhostCaps[i])); - if (!pGhostCap) - { - continue; - } - - const Vector vCapPos = pGhostCap->GetAbsOrigin(); - const Vector vGhostCapDist = vGhostPos - vCapPos; - const int iGhostCapLength = static_cast(vGhostCapDist.Length()); - const int iCapTeam = pGhostCap->owningTeamAlternate(); - - if (iCapTeam == iTargetCapTeam && iGhostCapLength < iMinCapGhostLength) - { - vrTargetCapPos = vCapPos; - iMinCapGhostLength = iGhostCapLength; - } - } - - if (!me->IsCarryingGhost()) - { - // If a ghoster player carrying and nearby, get close to them - // Friendly - get closer and assists, enemy - get closer and attack - const float flGhosterMeters = METERS_PER_INCH * me->GetAbsOrigin().DistTo(vGhostPos); - const float flMinCapMeters = METERS_PER_INCH * iMinCapGhostLength; - static const constexpr float FL_NEARBY_FOLLOW_METERS = 26.0f; - static const constexpr float FL_NEARBY_CAPZONE_METERS = 18.0f; - const bool bGhosterNearby = flGhosterMeters < FL_NEARBY_FOLLOW_METERS; - const bool bCapzoneNearby = flMinCapMeters < FL_NEARBY_CAPZONE_METERS; - // But a nearby capzone overrides a nearby ghoster - bGetCloserToGhoster = !bCapzoneNearby && bGhosterNearby && flMinCapMeters > flGhosterMeters; - } - - if (bGetCloserToGhoster) - { - m_vGoalPos = vGhostPos; - bQuickToGoalPos = true; - } - else - { - // iMinCapGhostLength == INT_MAX should never happen, just disable going to target - Assert(iMinCapGhostLength < INT_MAX); - bGoToGoalPos = (iMinCapGhostLength < INT_MAX); - - m_vGoalPos = vrTargetCapPos; - bQuickToGoalPos = (iGhosterTeam != iMyTeam); - } - } - else - { - // If the ghost exists, go to the ghost - m_vGoalPos = vGhostPos; - // NEO TODO (nullsystem): More sophisticated on handling non-ghost playing scenario, - // although it kind of already prefer hunting down players when they're in view, but - // just going towards ghost isn't something that always happens in general. - } - - if (bGoToGoalPos) - { - if (bGetCloserToGhoster) - { - const int iDistSqrConsidered = (iGhosterTeam == iMyTeam) ? 50000 : 5000; - if (m_vGoalPos.DistToSqr(me->GetAbsOrigin()) < iDistSqrConsidered) - { - // Don't stop targeting entity even when near enough - return; - } - } - else - { - constexpr int DISTANCE_CONSIDERED_ARRIVED_SQUARED = 10000; - if (m_vGoalPos.DistToSqr(me->GetAbsOrigin()) < DISTANCE_CONSIDERED_ARRIVED_SQUARED) - { - constexpr float RECHECK_TIME = 30.f; - m_repathTimer.Start(RECHECK_TIME); - m_bGoingToTargetEntity = false; - return; - } - } - m_bGoingToTargetEntity = true; - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, bQuickToGoalPos ? FASTEST_ROUTE : SAFEST_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) - { - return; - } - } - } - // Fallback and roam random spawn points if we have all weapons. { CNextSpawnFilter spawnFilter( me, 128.0f ); @@ -494,7 +384,7 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) m_hTargetEntity = pSpawns[RandomInt( 0, pSpawns.Size() - 1 )]; m_bGoingToTargetEntity = true; m_vGoalPos = m_hTargetEntity->WorldSpaceCenter(); - if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, SAFEST_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) + if ( CNEOBotPathCompute( me, m_path, m_vGoalPos, DEFAULT_ROUTE ) && m_path.IsValid() && m_path.GetResult() == Path::COMPLETE_PATH ) return; } } @@ -506,7 +396,7 @@ void CNEOBotSeekAndDestroy::RecomputeSeekPath( CNEOBot *me ) Vector vWanderPoint = TheNavAreas[RandomInt( 0, TheNavAreas.Size() - 1 )]->GetCenter(); m_vGoalPos = vWanderPoint; - if ( CNEOBotPathCompute( me, m_path, vWanderPoint, SAFEST_ROUTE ) ) + if ( CNEOBotPathCompute( me, m_path, vWanderPoint, DEFAULT_ROUTE ) ) return; } @@ -541,7 +431,7 @@ EventDesiredResult< CNEOBot > CNEOBotSeekAndDestroy::OnCommandApproach( CNEOBot* m_bOverrideApproach = true; m_vOverrideApproach = pos; - CNEOBotPathCompute( me, m_path, m_vOverrideApproach, SAFEST_ROUTE ); + CNEOBotPathCompute( me, m_path, m_vOverrideApproach, DEFAULT_ROUTE ); return TryContinue(); } diff --git a/src/game/server/neo/bot/neo_bot.cpp b/src/game/server/neo/bot/neo_bot.cpp index 2acda92e56..227c6400d8 100644 --- a/src/game/server/neo/bot/neo_bot.cpp +++ b/src/game/server/neo/bot/neo_bot.cpp @@ -1480,7 +1480,8 @@ void CNEOBot::EquipBestWeaponForThreat(const CKnownEntity* threat, const bool bN // We do not care about slugs if (bNotPrimary || (primaryWeapon && - (!primaryWeapon->m_iPrimaryAmmoType || + ((primaryWeapon->GetNeoWepBits() & NEO_WEP_GHOST) || + !primaryWeapon->m_iPrimaryAmmoType || (primaryWeapon->Clip1() + primaryWeapon->m_iPrimaryAmmoCount) <= 0))) { primaryWeapon = NULL; @@ -2001,7 +2002,7 @@ void CNEOBot::RepathIfFriendlyBlockingLineOfFire() { Vector goal = pPath->GetEndPosition(); - CNEOBotPathCost cost(this, SAFEST_ROUTE); + CNEOBotPathCost cost(this, DEFAULT_ROUTE); if (m_repathAroundFriendlyFollower.Compute(this, goal, cost)) { if (m_repathAroundFriendlyFollower.IsValid()) diff --git a/src/game/server/neo/bot/neo_bot_path_cost.cpp b/src/game/server/neo/bot/neo_bot_path_cost.cpp index 604f146b7c..32e3cbf6f9 100644 --- a/src/game/server/neo/bot/neo_bot_path_cost.cpp +++ b/src/game/server/neo/bot/neo_bot_path_cost.cpp @@ -117,6 +117,15 @@ float CNEOBotPathCost::operator()(CNavArea* baseArea, CNavArea* fromArea, const && (m_routeType != FASTEST_ROUTE) ) { cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), m_me->GetTeamNumber()) * neo_bot_path_reservation_penalty.GetFloat(); + + if (m_routeType == SAFEST_ROUTE) + { + // NEO Jank Cheat: Incorporate enemy bot paths so that we don't run directly into their line of fire + // Intended for use by ghost carrier team, to emulate a team that knows where enemies are likely to ambush + // Compensates for bots' lack of meta knowledge by making them prefer routes not reserved by enemies + // Adheres to cheat against bots but not against humans philosophy by not considering human players' positions + cost += CNEOBotPathReservations()->GetPredictedFriendlyPathCount(area->GetID(), GetEnemyTeam(m_me->GetTeamNumber())) * neo_bot_path_reservation_penalty.GetFloat() * 2; + } } // ------------------------------------------------------------------------------------------------ diff --git a/src/game/server/neo/bot/neo_bot_vision.cpp b/src/game/server/neo/bot/neo_bot_vision.cpp index de7b7e86ad..e4160959ff 100644 --- a/src/game/server/neo/bot/neo_bot_vision.cpp +++ b/src/game/server/neo/bot/neo_bot_vision.cpp @@ -5,6 +5,7 @@ #include "neo_bot_vision.h" #include "neo_player.h" #include "neo_gamerules.h" +#include "neo/weapons/weapon_ghost.h" ConVar neo_bot_choose_target_interval( "neo_bot_choose_target_interval", "0.3f", FCVAR_CHEAT, "How often, in seconds, a NEOBot can reselect his target" ); ConVar neo_bot_sniper_choose_target_interval( "neo_bot_sniper_choose_target_interval", "3.0f", FCVAR_CHEAT, "How often, in seconds, a zoomed-in Sniper can reselect his target" ); @@ -178,6 +179,19 @@ bool CNEOBotVision::IsInFieldOfView( CBaseEntity *subject ) const bool CNEOBotVision::IsAbleToSee(CBaseEntity *subject, FieldOfViewCheckType checkFOV, Vector *visibleSpot) const { + CNEOBot *me = (CNEOBot *)GetBot()->GetEntity(); + if (me && me->IsCarryingGhost()) + { + auto *pGhost = dynamic_cast(me->GetActiveWeapon()); + if (pGhost && pGhost->IsGhost() && pGhost->IsBootupCompleted()) + { + if (me->GetAbsOrigin().DistToSqr(subject->GetAbsOrigin()) < Square(CWeaponGhost::GetGhostRangeInHammerUnits())) + { + return true; + } + } + } + const int iGhosterPlayer = NEORules()->GetGhosterPlayer(); if (iGhosterPlayer > 0) { diff --git a/src/game/shared/neo/neo_gamerules.h b/src/game/shared/neo/neo_gamerules.h index 7a788e885e..aa9a1ebe4f 100644 --- a/src/game/shared/neo/neo_gamerules.h +++ b/src/game/shared/neo/neo_gamerules.h @@ -431,6 +431,11 @@ class CNEORules : public CHL2MPRules, public CGameEventListener float m_flJuggernautDeathTime = 0.0f; int m_iLastJuggernautTeam = TEAM_INVALID; + // For looking up capture zone locations + friend class CNEOBotCtgCarrier; + friend class CNEOBotCtgEscort; + friend class CNEOBotCtgLoneWolf; + friend class CNEOBotSeekAndDestroy; CUtlVector m_pGhostCaps; CWeaponGhost *m_pGhost = nullptr;