diff options
Diffstat (limited to 'game/server/cstrike/bot')
36 files changed, 24537 insertions, 0 deletions
diff --git a/game/server/cstrike/bot/cs_bot.cpp b/game/server/cstrike/bot/cs_bot.cpp new file mode 100644 index 0000000..d3d8235 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot.cpp @@ -0,0 +1,1117 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_gamerules.h" +#include "func_breakablesurf.h" +#include "obstacle_pushaway.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +LINK_ENTITY_TO_CLASS( cs_bot, CCSBot ); + +BEGIN_DATADESC( CCSBot ) + +END_DATADESC() + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the number of bots following the given player + */ +int GetBotFollowCount( CCSPlayer *leader ) +{ + int count = 0; + + for( int i=1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *entity = UTIL_PlayerByIndex( i ); + + if (entity == NULL) + continue; + + CBasePlayer *player = static_cast<CBasePlayer *>( entity ); + + if (!player->IsBot()) + continue; + + if (!player->IsAlive()) + continue; + + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + if (bot && bot->GetFollowLeader() == leader) + ++count; + } + + return count; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Change movement speed to walking + */ +void CCSBot::Walk( void ) +{ + if (m_mustRunTimer.IsElapsed()) + { + BaseClass::Walk(); + } + else + { + // must run + Run(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if jump was started. + * This is extended from the base jump to disallow jumping when in a crouch area. + */ +bool CCSBot::Jump( bool mustJump ) +{ + // prevent jumping if we're crouched, unless we're in a crouchjump area - jump wins + bool inCrouchJumpArea = (m_lastKnownArea && + (m_lastKnownArea->GetAttributes() & NAV_MESH_CROUCH) && + (m_lastKnownArea->GetAttributes() & NAV_MESH_JUMP)); + + if ( !IsUsingLadder() && IsDucked() && !inCrouchJumpArea ) + { + return false; + } + + return BaseClass::Jump( mustJump ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when injured by something + * NOTE: We dont want to directly call Attack() here, or the bots will have super-human reaction times when injured + */ +int CCSBot::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CBaseEntity *attacker = info.GetInflictor(); + + // getting hurt makes us alert + BecomeAlert(); + StopWaiting(); + + // if we were attacked by a teammate, rebuke + if (attacker->IsPlayer()) + { + CCSPlayer *player = static_cast<CCSPlayer *>( attacker ); + + if (InSameTeam( player ) && !player->IsBot()) + GetChatter()->FriendlyFire(); + } + + if (attacker->IsPlayer() && IsEnemy( attacker )) + { + // Track previous attacker so we don't try to panic multiple times for a shotgun blast + CCSPlayer *lastAttacker = m_attacker; + float lastAttackedTimestamp = m_attackedTimestamp; + + // keep track of our last attacker + m_attacker = reinterpret_cast<CCSPlayer *>( attacker ); + m_attackedTimestamp = gpGlobals->curtime; + + // no longer safe + AdjustSafeTime(); + + if ( !IsSurprised() && (m_attacker != lastAttacker || m_attackedTimestamp != lastAttackedTimestamp) ) + { + CCSPlayer *enemy = static_cast<CCSPlayer *>( attacker ); + + // being hurt by an enemy we can't see causes panic + if (!IsVisible( enemy, CHECK_FOV )) + { + // if not attacking anything, look around to try to find attacker + if (!IsAttacking()) + { + Panic(); + } + else // we are attacking + { + if (!IsEnemyVisible()) + { + // can't see our current enemy, panic to acquire new attacker + Panic(); + } + } + } + } + } + + // extend + return BaseClass::OnTakeDamage( info ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when killed + */ +void CCSBot::Event_Killed( const CTakeDamageInfo &info ) +{ +// PrintIfWatched( "Killed( attacker = %s )\n", STRING(pevAttacker->netname) ); + + GetChatter()->OnDeath(); + + // increase the danger where we died + const float deathDanger = 1.0f; + const float deathDangerRadius = 500.0f; + TheNavMesh->IncreaseDangerNearby( GetTeamNumber(), deathDanger, m_lastKnownArea, GetAbsOrigin(), deathDangerRadius ); + + // end voice feedback + m_voiceEndTimestamp = 0.0f; + + // extend + BaseClass::Event_Killed( info ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if line segment intersects rectagular volume + */ +#define HI_X 0x01 +#define LO_X 0x02 +#define HI_Y 0x04 +#define LO_Y 0x08 +#define HI_Z 0x10 +#define LO_Z 0x20 + +inline bool IsIntersectingBox( const Vector& start, const Vector& end, const Vector& boxMin, const Vector& boxMax ) +{ + unsigned char startFlags = 0; + unsigned char endFlags = 0; + + // classify start point + if (start.x < boxMin.x) + startFlags |= LO_X; + if (start.x > boxMax.x) + startFlags |= HI_X; + + if (start.y < boxMin.y) + startFlags |= LO_Y; + if (start.y > boxMax.y) + startFlags |= HI_Y; + + if (start.z < boxMin.z) + startFlags |= LO_Z; + if (start.z > boxMax.z) + startFlags |= HI_Z; + + // classify end point + if (end.x < boxMin.x) + endFlags |= LO_X; + if (end.x > boxMax.x) + endFlags |= HI_X; + + if (end.y < boxMin.y) + endFlags |= LO_Y; + if (end.y > boxMax.y) + endFlags |= HI_Y; + + if (end.z < boxMin.z) + endFlags |= LO_Z; + if (end.z > boxMax.z) + endFlags |= HI_Z; + + // trivial reject + if (startFlags & endFlags) + return false; + + /// @todo Do exact line/box intersection check + + return true; +} + + +extern void UTIL_DrawBox( Extent *extent, int lifetime, int red, int green, int blue ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * When bot is touched by another entity. + */ +void CCSBot::Touch( CBaseEntity *other ) +{ + // EXTEND + BaseClass::Touch( other ); + + // if we have touched a higher-priority player, make way + /// @todo Need to account for reaction time, etc. + if (other->IsPlayer()) + { + // if we are defusing a bomb, don't move + if (IsDefusingBomb()) + return; + + // if we are on a ladder, don't move + if (IsUsingLadder()) + return; + + CCSPlayer *player = static_cast<CCSPlayer *>( other ); + + // get priority of other player + unsigned int otherPri = TheCSBots()->GetPlayerPriority( player ); + + // get our priority + unsigned int myPri = TheCSBots()->GetPlayerPriority( this ); + + // if our priority is better, don't budge + if (myPri < otherPri) + return; + + // they are higher priority - make way, unless we're already making way for someone more important + if (m_avoid != NULL) + { + unsigned int avoidPri = TheCSBots()->GetPlayerPriority( static_cast<CBasePlayer *>( static_cast<CBaseEntity *>( m_avoid ) ) ); + if (avoidPri < otherPri) + { + // ignore 'other' because we're already avoiding someone better + return; + } + } + + m_avoid = other; + m_avoidTimestamp = gpGlobals->curtime; + } + + // Check for breakables we're actually touching + // If we're not stuck or crouched, we don't care + if ( !m_isStuck && !IsCrouching() && !IsOnLadder() ) + return; + + // See if it's breakable + if ( IsBreakableEntity( other ) ) + { + // it's breakable - try to shoot it. + SetLookAt( "Breakable", other->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are busy doing something important + */ +bool CCSBot::IsBusy( void ) const +{ + if (IsAttacking() || + IsBuying() || + IsDefusingBomb() || + GetTask() == PLANT_BOMB || + GetTask() == RESCUE_HOSTAGES || + IsSniping()) + { + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::BotDeathThink( void ) +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Try to join the given team + */ +void CCSBot::TryToJoinTeam( int team ) +{ + m_desiredTeam = team; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Assign given player as our current enemy to attack + */ +void CCSBot::SetBotEnemy( CCSPlayer *enemy ) +{ + if (m_enemy != enemy) + { + m_enemy = enemy; + m_currentEnemyAcquireTimestamp = gpGlobals->curtime; + + PrintIfWatched( "SetBotEnemy: %s\n", (enemy) ? enemy->GetPlayerName() : "(NULL)" ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * If we are not on the navigation mesh (m_currentArea == NULL), + * move towards last known area. + * Return false if off mesh. + */ +bool CCSBot::StayOnNavMesh( void ) +{ + if (m_currentArea == NULL) + { + // move back onto the area map + + // if we have no lastKnownArea, we probably started off + // of the nav mesh - find the closest nav area and use it + CNavArea *goalArea; + if (!m_currentArea && !m_lastKnownArea) + { + goalArea = TheNavMesh->GetNearestNavArea( GetCentroid( this ) ); + PrintIfWatched( "Started off the nav mesh - moving to closest nav area...\n" ); + } + else + { + goalArea = m_lastKnownArea; + PrintIfWatched( "Getting out of NULL area...\n" ); + } + + if (goalArea) + { + Vector pos; + goalArea->GetClosestPointOnArea( GetCentroid( this ), &pos ); + + // move point into area + Vector to = pos - GetCentroid( this ); + to.NormalizeInPlace(); + + const float stepInDist = 5.0f; // how far to "step into" an area - must be less than min area size + pos = pos + (stepInDist * to); + + MoveTowardsPosition( pos ); + } + + // if we're stuck, try to get un-stuck + // do stuck movements last, so they override normal movement + if (m_isStuck) + Wiggle(); + + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we will do scenario-related tasks + */ +bool CCSBot::IsDoingScenario( void ) const +{ + // if we are deferring to humans, and there is a live human on our team, don't do the scenario + if (cv_bot_defer_to_human.GetBool()) + { + if (UTIL_HumansOnTeam( GetTeamNumber(), IS_ALIVE )) + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we noticed the bomb on the ground or on the radar (for T's only) + */ +bool CCSBot::NoticeLooseBomb( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetScenario() != CCSBotManager::SCENARIO_DEFUSE_BOMB) + return false; + + CBaseEntity *bomb = ctrl->GetLooseBomb(); + + if (bomb) + { + // T's can always see bomb on their radar + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if can see the bomb lying on the ground + */ +bool CCSBot::CanSeeLooseBomb( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetScenario() != CCSBotManager::SCENARIO_DEFUSE_BOMB) + return false; + + CBaseEntity *bomb = ctrl->GetLooseBomb(); + + if (bomb) + { + if (IsVisible( bomb->GetAbsOrigin(), CHECK_FOV )) + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if can see the planted bomb + */ +bool CCSBot::CanSeePlantedBomb( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetScenario() != CCSBotManager::SCENARIO_DEFUSE_BOMB) + return false; + + if (!GetGameState()->IsBombPlanted()) + return false; + + const Vector *bombPos = GetGameState()->GetBombPosition(); + + if (bombPos && IsVisible( *bombPos, CHECK_FOV )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return last enemy that hurt us + */ +CCSPlayer *CCSBot::GetAttacker( void ) const +{ + if (m_attacker && m_attacker->IsAlive()) + return m_attacker; + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Immediately jump off of our ladder, if we're on one + */ +void CCSBot::GetOffLadder( void ) +{ + if (IsUsingLadder()) + { + Jump( MUST_JUMP ); + DestroyPath(); + } +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return time when given spot was last checked + */ +float CCSBot::GetHidingSpotCheckTimestamp( HidingSpot *spot ) const +{ + for( int i=0; i<m_checkedHidingSpotCount; ++i ) + if (m_checkedHidingSpot[i].spot->GetID() == spot->GetID()) + return m_checkedHidingSpot[i].timestamp; + + return -999999.9f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the timestamp of the given spot to now. + * If the spot is not in the set, overwrite the least recently checked spot. + */ +void CCSBot::SetHidingSpotCheckTimestamp( HidingSpot *spot ) +{ + int leastRecent = 0; + float leastRecentTime = gpGlobals->curtime + 1.0f; + + for( int i=0; i<m_checkedHidingSpotCount; ++i ) + { + // if spot is in the set, just update its timestamp + if (m_checkedHidingSpot[i].spot->GetID() == spot->GetID()) + { + m_checkedHidingSpot[i].timestamp = gpGlobals->curtime; + return; + } + + // keep track of least recent spot + if (m_checkedHidingSpot[i].timestamp < leastRecentTime) + { + leastRecentTime = m_checkedHidingSpot[i].timestamp; + leastRecent = i; + } + } + + // if there is room for more spots, append this one + if (m_checkedHidingSpotCount < MAX_CHECKED_SPOTS) + { + m_checkedHidingSpot[ m_checkedHidingSpotCount ].spot = spot; + m_checkedHidingSpot[ m_checkedHidingSpotCount ].timestamp = gpGlobals->curtime; + ++m_checkedHidingSpotCount; + } + else + { + // replace the least recent spot + m_checkedHidingSpot[ leastRecent ].spot = spot; + m_checkedHidingSpot[ leastRecent ].timestamp = gpGlobals->curtime; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Periodic check of hostage count in case we lost some + */ +void CCSBot::UpdateHostageEscortCount( void ) +{ + const float updateInterval = 1.0f; + if (m_hostageEscortCount == 0 || gpGlobals->curtime - m_hostageEscortCountTimestamp < updateInterval) + return; + + m_hostageEscortCountTimestamp = gpGlobals->curtime; + + // recount the hostages in case we lost some + m_hostageEscortCount = 0; + + for( int i=0; i<g_Hostages.Count(); ++i ) + { + CHostage *hostage = g_Hostages[i]; + + // skip dead or rescued hostages + if ( !hostage->IsValid() || !hostage->IsAlive() ) + continue; + + // check if hostage has targeted us, and is following + if ( hostage->IsFollowing( this ) ) + ++m_hostageEscortCount; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are outnumbered by enemies + */ +bool CCSBot::IsOutnumbered( void ) const +{ + return (GetNearbyFriendCount() < GetNearbyEnemyCount()-1) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return number of enemies we are outnumbered by + */ +int CCSBot::OutnumberedCount( void ) const +{ + if (IsOutnumbered()) + return (GetNearbyEnemyCount()-1) - GetNearbyFriendCount(); + + return 0; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest "important" enemy for the given scenario (bomb carrier, VIP, hostage escorter) + */ +CCSPlayer *CCSBot::GetImportantEnemy( bool checkVisibility ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + CCSPlayer *nearEnemy = NULL; + float nearDist = 999999999.9f; + + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBaseEntity *entity = UTIL_PlayerByIndex( i ); + + if (entity == NULL) + continue; + +// if (FNullEnt( entity->pev )) +// continue; + +// if (FStrEq( STRING( entity->pev->netname ), "" )) +// continue; + + // is it a player? + if (!entity->IsPlayer()) + continue; + + CCSPlayer *player = static_cast<CCSPlayer *>( entity ); + + // is it alive? + if (!player->IsAlive()) + continue; + + // skip friends + if (InSameTeam( player )) + continue; + + // is it "important" + if (!ctrl->IsImportantPlayer( player )) + continue; + + // is it closest? + Vector d = GetAbsOrigin() - player->GetAbsOrigin(); + float distSq = d.x*d.x + d.y*d.y + d.z*d.z; + if (distSq < nearDist) + { + if (checkVisibility && !IsVisible( player, CHECK_FOV )) + continue; + + nearEnemy = player; + nearDist = distSq; + } + } + + return nearEnemy; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Sets our current disposition + */ +void CCSBot::SetDisposition( DispositionType disposition ) +{ + m_disposition = disposition; + + if (m_disposition != IGNORE_ENEMIES) + m_ignoreEnemiesTimer.Invalidate(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return our current disposition + */ +CCSBot::DispositionType CCSBot::GetDisposition( void ) const +{ + if (!m_ignoreEnemiesTimer.IsElapsed()) + return IGNORE_ENEMIES; + + return m_disposition; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Ignore enemies for a short durationy + */ +void CCSBot::IgnoreEnemies( float duration ) +{ + m_ignoreEnemiesTimer.Start( duration ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Increase morale one step + */ +void CCSBot::IncreaseMorale( void ) +{ + if (m_morale < EXCELLENT) + m_morale = static_cast<MoraleType>( m_morale + 1 ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Decrease morale one step + */ +void CCSBot::DecreaseMorale( void ) +{ + if (m_morale > TERRIBLE) + m_morale = static_cast<MoraleType>( m_morale - 1 ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are acting like a rogue (not listening to teammates, not doing scenario goals) + * @todo Account for morale + */ +bool CCSBot::IsRogue( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + if (!ctrl->AllowRogues()) + return false; + + // periodically re-evaluate our rogue status + if (m_rogueTimer.IsElapsed()) + { + m_rogueTimer.Start( RandomFloat( 10.0f, 30.0f ) ); + + // our chance of going rogue is inversely proportional to our teamwork attribute + const float rogueChance = 100.0f * (1.0f - GetProfile()->GetTeamwork()); + + m_isRogue = (RandomFloat( 0, 100 ) < rogueChance); + } + + return m_isRogue; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are in a hurry + */ +bool CCSBot::IsHurrying( void ) const +{ + if (!m_hurryTimer.IsElapsed()) + return true; + + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + // if the bomb has been planted, we are in a hurry, CT or T (they could be defusing it!) + if (ctrl->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB && ctrl->IsBombPlanted()) + return true; + + // if we are a T and hostages are being rescued, we are in a hurry + if (ctrl->GetScenario() == CCSBotManager::SCENARIO_RESCUE_HOSTAGES && + GetTeamNumber() == TEAM_TERRORIST && + GetGameState()->AreAllHostagesBeingRescued()) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if it is the early, "safe", part of the round + */ +bool CCSBot::IsSafe( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetElapsedRoundTime() < m_safeTime) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if it is well past the early, "safe", part of the round + */ +bool CCSBot::IsWellPastSafe( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + if (ctrl->GetElapsedRoundTime() > 2.0f * m_safeTime) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we were in the safe time last update, but not now + */ +bool CCSBot::IsEndOfSafeTime( void ) const +{ + return m_wasSafe && !IsSafe(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the amount of "safe time" we have left + */ +float CCSBot::GetSafeTimeRemaining( void ) const +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + return m_safeTime - ctrl->GetElapsedRoundTime(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when enemy seen to adjust safe time for this round + */ +void CCSBot::AdjustSafeTime( void ) +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + // if we spotted an enemy sooner than we thought possible, adjust our notion of "safe" time + if (ctrl->GetElapsedRoundTime() < m_safeTime) + { + // since right now is not safe, adjust safe time to be a few seconds ago + m_safeTime = ctrl->GetElapsedRoundTime() - 2.0f; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we haven't seen an enemy for "a long time" + */ +bool CCSBot::HasNotSeenEnemyForLongTime( void ) const +{ + const float longTime = 30.0f; + return (GetTimeSinceLastSawEnemy() > longTime); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Pick a random zone and hide near it + */ +bool CCSBot::GuardRandomZone( float range ) +{ + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheCSBots() ); + + const CCSBotManager::Zone *zone = ctrl->GetRandomZone(); + if (zone) + { + CNavArea *rescueArea = ctrl->GetRandomAreaInZone( zone ); + if (rescueArea) + { + Hide( rescueArea, -1.0f, range ); + return true; + } + } + + return false; +} + + + +//-------------------------------------------------------------------------------------------------------------- +class CollectRetreatSpotsFunctor +{ +public: + CollectRetreatSpotsFunctor( CCSBot *me, float range ) + { + m_me = me; + m_count = 0; + m_range = range; + } + + enum { MAX_SPOTS = 256 }; + + bool operator() ( CNavArea *area ) + { + // collect all the hiding spots in this area + const HidingSpotVector *pSpots = area->GetHidingSpots(); + + FOR_EACH_VEC( (*pSpots), it ) + { + const HidingSpot *spot = (*pSpots)[ it ]; + + if (m_count >= MAX_SPOTS) + break; + + // make sure hiding spot is in range + if (m_range > 0.0f) + if ((spot->GetPosition() - GetCentroid( m_me )).IsLengthGreaterThan( m_range )) + continue; + + // if a Player is using this hiding spot, don't consider it + if (IsSpotOccupied( m_me, spot->GetPosition() )) + { + // player is in hiding spot + /// @todo Check if player is moving or sitting still + continue; + } + + // don't select spot if an enemy can see it + if (UTIL_IsVisibleToTeam( spot->GetPosition() + Vector( 0, 0, HalfHumanHeight ), OtherTeam( m_me->GetTeamNumber() ) )) + continue; + + // don't select spot if it is closest to an enemy + CBasePlayer *owner = UTIL_GetClosestPlayer( spot->GetPosition() ); + if (owner && !m_me->InSameTeam( owner )) + continue; + + m_spot[ m_count++ ] = &spot->GetPosition(); + } + + // if we've filled up, stop searching + if (m_count == MAX_SPOTS) + return false; + + return true; + } + + CCSBot *m_me; + float m_range; + + const Vector *m_spot[ MAX_SPOTS ]; + int m_count; +}; + + +/** + * Do a breadth-first search to find a good retreat spot. + * Don't pick a spot that a Player is currently occupying. + */ +const Vector *FindNearbyRetreatSpot( CCSBot *me, float maxRange ) +{ + CNavArea *area = me->GetLastKnownArea(); + if (area == NULL) + return NULL; + + // collect spots that enemies cannot see + CollectRetreatSpotsFunctor collector( me, maxRange ); + SearchSurroundingAreas( area, GetCentroid( me ), collector, maxRange ); + + if (collector.m_count == 0) + return NULL; + + // select a hiding spot at random + int which = RandomInt( 0, collector.m_count-1 ); + return collector.m_spot[ which ]; +} + +//-------------------------------------------------------------------------------------------------------------- +class FarthestHostage +{ +public: + FarthestHostage( const CCSBot *me ) + { + m_me = me; + m_farRange = -1.0f; + } + + bool operator() ( CHostage *hostage ) + { + if (hostage->IsFollowing( m_me )) + { + float range = (hostage->GetAbsOrigin() - m_me->GetAbsOrigin()).Length(); + if (range > m_farRange) + { + m_farRange = range; + } + } + + return true; + } + + const CCSBot *m_me; + float m_farRange; +}; + +/** + * Return euclidean distance to farthest escorted hostage. + * Return -1 if no hostage is following us. + */ +float CCSBot::GetRangeToFarthestEscortedHostage( void ) const +{ + FarthestHostage away( this ); + + ForEachHostage( away ); + + return away.m_farRange; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return string describing current task + * NOTE: This MUST be kept in sync with the CCSBot::TaskType enum + */ +const char *CCSBot::GetTaskName( void ) const +{ + static const char *name[ NUM_TASKS ] = + { + "SEEK_AND_DESTROY", + "PLANT_BOMB", + "FIND_TICKING_BOMB", + "DEFUSE_BOMB", + "GUARD_TICKING_BOMB", + "GUARD_BOMB_DEFUSER", + "GUARD_LOOSE_BOMB", + "GUARD_BOMB_ZONE", + "GUARD_INITIAL_ENCOUNTER", + "ESCAPE_FROM_BOMB", + "HOLD_POSITION", + "FOLLOW", + "VIP_ESCAPE", + "GUARD_VIP_ESCAPE_ZONE", + "COLLECT_HOSTAGES", + "RESCUE_HOSTAGES", + "GUARD_HOSTAGES", + "GUARD_HOSTAGE_RESCUE_ZONE", + "MOVE_TO_LAST_KNOWN_ENEMY_POSITION", + "MOVE_TO_SNIPER_SPOT", + "SNIPING", + }; + + return name[ (int)GetTask() ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return string describing current disposition + * NOTE: This MUST be kept in sync with the CCSBot::DispositionType enum + */ +const char *CCSBot::GetDispositionName( void ) const +{ + static const char *name[ NUM_DISPOSITIONS ] = + { + "ENGAGE_AND_INVESTIGATE", + "OPPORTUNITY_FIRE", + "SELF_DEFENSE", + "IGNORE_ENEMIES" + }; + + return name[ (int)GetDisposition() ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return string describing current morale + * NOTE: This MUST be kept in sync with the CCSBot::MoraleType enum + */ +const char *CCSBot::GetMoraleName( void ) const +{ + static const char *name[ EXCELLENT - TERRIBLE + 1 ] = + { + "TERRIBLE", + "BAD", + "NEGATIVE", + "NEUTRAL", + "POSITIVE", + "GOOD", + "EXCELLENT" + }; + + return name[ (int)GetMorale() + 3 ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Fill in a CUserCmd with our data + */ +void CCSBot::BuildUserCmd( CUserCmd& cmd, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ) +{ + Q_memset( &cmd, 0, sizeof( cmd ) ); + if ( !RunMimicCommand( cmd ) ) + { + // Don't walk when ducked - it's painfully slow + if ( m_Local.m_bDucked || m_Local.m_bDucking ) + { + buttons &= ~IN_SPEED; + } + + cmd.command_number = gpGlobals->tickcount; + cmd.forwardmove = forwardmove; + cmd.sidemove = sidemove; + cmd.upmove = upmove; + cmd.buttons = buttons; + cmd.impulse = impulse; + + VectorCopy( viewangles, cmd.viewangles ); + cmd.random_seed = random->RandomInt( 0, 0x7fffffff ); + } +} + +//-------------------------------------------------------------------------------------------------------------- diff --git a/game/server/cstrike/bot/cs_bot.h b/game/server/cstrike/bot/cs_bot.h new file mode 100644 index 0000000..e9530d5 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot.h @@ -0,0 +1,2004 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// +// Author: Michael S. Booth ([email protected]), 2003 +// +// NOTE: The CS Bot code uses Doxygen-style comments. If you run Doxygen over this code, it will +// auto-generate documentation. Visit www.doxygen.org to download the system for free. +// + +#ifndef _CS_BOT_H_ +#define _CS_BOT_H_ + +#include "bot/bot.h" +#include "bot/cs_bot_manager.h" +#include "bot/cs_bot_chatter.h" +#include "cs_gamestate.h" +#include "cs_player.h" +#include "weapon_csbase.h" +#include "cs_nav_pathfind.h" +#include "cs_nav_area.h" + +class CBaseDoor; +class CBasePropDoor; +class CCSBot; +class CPushAwayEnumerator; + +//-------------------------------------------------------------------------------------------------------------- +/** + * For use with player->m_rgpPlayerItems[] + */ +enum InventorySlotType +{ + PRIMARY_WEAPON_SLOT = 1, + PISTOL_SLOT, + KNIFE_SLOT, + GRENADE_SLOT, + C4_SLOT +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The definition of a bot's behavior state. One or more finite state machines + * using these states implement a bot's behaviors. + */ +class BotState +{ +public: + virtual void OnEnter( CCSBot *bot ) { } ///< when state is entered + virtual void OnUpdate( CCSBot *bot ) { } ///< state behavior + virtual void OnExit( CCSBot *bot ) { } ///< when state exited + virtual const char *GetName( void ) const = 0; ///< return state name +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * The state is invoked when a bot has nothing to do, or has finished what it was doing. + * A bot never stays in this state - it is the main action selection mechanism. + */ +class IdleState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Idle"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is actively searching for an enemy. + */ +class HuntState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Hunt"; } + + void ClearHuntArea( void ) { m_huntArea = NULL; } + +private: + CNavArea *m_huntArea; ///< "far away" area we are moving to +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot has an enemy and is attempting to kill it + */ +class AttackState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Attack"; } + + void SetCrouchAndHold( bool crouch ) { m_crouchAndHold = crouch; } + +protected: + enum DodgeStateType + { + STEADY_ON, + SLIDE_LEFT, + SLIDE_RIGHT, + JUMP, + + NUM_ATTACK_STATES + }; + DodgeStateType m_dodgeState; + float m_nextDodgeStateTimestamp; + + CountdownTimer m_repathTimer; + float m_scopeTimestamp; + + bool m_haveSeenEnemy; ///< false if we haven't yet seen the enemy since we started this attack (told by a friend, etc) + bool m_isEnemyHidden; ///< true we if we have lost line-of-sight to our enemy + float m_reacquireTimestamp; ///< time when we can fire again, after losing enemy behind cover + float m_shieldToggleTimestamp; ///< time to toggle shield deploy state + bool m_shieldForceOpen; ///< if true, open up and shoot even if in danger + + float m_pinnedDownTimestamp; ///< time when we'll consider ourselves "pinned down" by the enemy + + bool m_crouchAndHold; + bool m_didAmbushCheck; + bool m_shouldDodge; + bool m_firstDodge; + + bool m_isCoward; ///< if true, we'll retreat if outnumbered during this fight + CountdownTimer m_retreatTimer; + + void StopAttacking( CCSBot *bot ); + void Dodge( CCSBot *bot ); ///< do dodge behavior +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot has heard an enemy noise and is moving to find out what it was. + */ +class InvestigateNoiseState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "InvestigateNoise"; } + +private: + void AttendCurrentNoise( CCSBot *bot ); ///< move towards currently heard noise + Vector m_checkNoisePosition; ///< the position of the noise we're investigating + CountdownTimer m_minTimer; ///< minimum time we will investigate our current noise +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is buying equipment at the start of a round. + */ +class BuyState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Buy"; } + +private: + bool m_isInitialDelay; + int m_prefRetries; ///< for retrying buying preferred weapon at current index + int m_prefIndex; ///< where are we in our list of preferred weapons + + int m_retries; + bool m_doneBuying; + bool m_buyDefuseKit; + bool m_buyGrenade; + bool m_buyShield; + bool m_buyPistol; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is moving to a potentially far away position in the world. + */ +class MoveToState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "MoveTo"; } + void SetGoalPosition( const Vector &pos ) { m_goalPosition = pos; } + void SetRouteType( RouteType route ) { m_routeType = route; } + +private: + Vector m_goalPosition; ///< goal position of move + RouteType m_routeType; ///< the kind of route to build + bool m_radioedPlan; + bool m_askedForCover; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a Terrorist bot is moving to pick up a dropped bomb. + */ +class FetchBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual const char *GetName( void ) const { return "FetchBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a Terrorist bot is actually planting the bomb. + */ +class PlantBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "PlantBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a CT bot is actually defusing a live bomb. + */ +class DefuseBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "DefuseBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is hiding in a corner. + * NOTE: This state also includes MOVING TO that hiding spot, which may be all the way + * across the map! + */ +class HideState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Hide"; } + + void SetHidingSpot( const Vector &pos ) { m_hidingSpot = pos; } + const Vector &GetHidingSpot( void ) const { return m_hidingSpot; } + + void SetSearchArea( CNavArea *area ) { m_searchFromArea = area; } + void SetSearchRange( float range ) { m_range = range; } + void SetDuration( float time ) { m_duration = time; } + void SetHoldPosition( bool hold ) { m_isHoldingPosition = hold; } + + bool IsAtSpot( void ) const { return m_isAtSpot; } + + float GetHideTime( void ) const + { + if (IsAtSpot()) + { + return m_duration - m_hideTimer.GetRemainingTime(); + } + + return 0.0f; + } + +private: + CNavArea *m_searchFromArea; + float m_range; + + Vector m_hidingSpot; + bool m_isLookingOutward; + bool m_isAtSpot; + float m_duration; + CountdownTimer m_hideTimer; ///< how long to hide + + bool m_isHoldingPosition; + float m_holdPositionTime; ///< how long to hold our position after we hear nearby enemy noise + + bool m_heardEnemy; ///< set to true when we first hear an enemy + float m_firstHeardEnemyTime; ///< when we first heard the enemy + + int m_retry; ///< counter for retrying hiding spot + + Vector m_leaderAnchorPos; ///< the position of our follow leader when we decided to hide + + bool m_isPaused; ///< if true, we have paused in our retreat for a moment + CountdownTimer m_pauseTimer; ///< for stoppping and starting our pauses while we retreat +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is attempting to flee from a bomb that is about to explode. + */ +class EscapeFromBombState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "EscapeFromBomb"; } +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is following another player. + */ +class FollowState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "Follow"; } + + void SetLeader( CCSPlayer *player ) { m_leader = player; } + +private: + CHandle< CCSPlayer > m_leader; ///< the player we are following + Vector m_lastLeaderPos; ///< where the leader was when we computed our follow path + bool m_isStopped; + float m_stoppedTimestamp; + + enum LeaderMotionStateType + { + INVALID, + STOPPED, + WALKING, + RUNNING + }; + LeaderMotionStateType m_leaderMotionState; + IntervalTimer m_leaderMotionStateTime; + + bool m_isSneaking; + float m_lastSawLeaderTime; + CountdownTimer m_repathInterval; + + IntervalTimer m_walkTime; + bool m_isAtWalkSpeed; + + float m_waitTime; + CountdownTimer m_idleTimer; + + void ComputeLeaderMotionState( float leaderSpeed ); +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is actually using another entity (ie: facing towards it and pressing the use key) + */ +class UseEntityState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "UseEntity"; } + + void SetEntity( CBaseEntity *entity ) { m_entity = entity; } + +private: + EHANDLE m_entity; ///< the entity we will use +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * When a bot is opening a door + */ +class OpenDoorState : public BotState +{ +public: + virtual void OnEnter( CCSBot *bot ); + virtual void OnUpdate( CCSBot *bot ); + virtual void OnExit( CCSBot *bot ); + virtual const char *GetName( void ) const { return "OpenDoor"; } + + void SetDoor( CBaseEntity *door ); + + bool IsDone( void ) const { return m_isDone; } ///< return true if behavior is done + +private: + CHandle< CBaseDoor > m_funcDoor; ///< the func_door we are opening + CHandle< CBasePropDoor > m_propDoor; ///< the prop_door we are opening + bool m_isDone; + CountdownTimer m_timeout; +}; + + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +/** + * The Counter-strike Bot + */ +class CCSBot : public CBot< CCSPlayer > +{ +public: + DECLARE_CLASS( CCSBot, CBot< CCSPlayer > ); + DECLARE_DATADESC(); + + CCSBot( void ); ///< constructor initializes all values to zero + virtual ~CCSBot(); + virtual bool Initialize( const BotProfile *profile, int team ); ///< (EXTEND) prepare bot for action + + virtual void Spawn( void ); ///< (EXTEND) spawn the bot into the game + virtual void Touch( CBaseEntity *other ); ///< (EXTEND) when touched by another entity + + virtual void Upkeep( void ); ///< lightweight maintenance, invoked frequently + virtual void Update( void ); ///< heavyweight algorithms, invoked less often + virtual void BuildUserCmd( CUserCmd& cmd, const QAngle& viewangles, float forwardmove, float sidemove, float upmove, int buttons, byte impulse ); + virtual float GetMoveSpeed( void ); ///< returns current movement speed (for walk/run) + + virtual void Walk( void ); + virtual bool Jump( bool mustJump = false ); ///< returns true if jump was started + + //- behavior properties ------------------------------------------------------------------------------------------ + float GetCombatRange( void ) const; + bool IsRogue( void ) const; ///< return true if we dont listen to teammates or pursue scenario goals + void SetRogue( bool rogue ); + bool IsHurrying( void ) const; ///< return true if we are in a hurry + void Hurry( float duration ); ///< force bot to hurry + bool IsSafe( void ) const; ///< return true if we are in a safe region + bool IsWellPastSafe( void ) const; ///< return true if it is well past the early, "safe", part of the round + bool IsEndOfSafeTime( void ) const; ///< return true if we were in the safe time last update, but not now + float GetSafeTimeRemaining( void ) const; ///< return the amount of "safe time" we have left + float GetSafeTime( void ) const; ///< return what we think the total "safe time" for this map is + virtual void Blind( float holdTime, float fadeTime, float startingAlpha = 255 ); // player blinded by a flashbang + bool IsUnhealthy( void ) const; ///< returns true if bot is low on health + + bool IsAlert( void ) const; ///< return true if bot is in heightened "alert" mode + void BecomeAlert( void ); ///< bot becomes "alert" for immediately nearby enemies + + bool IsSneaking( void ) const; ///< return true if bot is sneaking + void Sneak( float duration ); ///< sneak for given duration + + //- behaviors --------------------------------------------------------------------------------------------------- + void Idle( void ); + + void Hide( CNavArea *searchFromArea = NULL, float duration = -1.0f, float hideRange = 750.0f, bool holdPosition = false ); ///< DEPRECATED: Use TryToHide() instead + #define USE_NEAREST true + bool TryToHide( CNavArea *searchFromArea = NULL, float duration = -1.0f, float hideRange = 750.0f, bool holdPosition = false, bool useNearest = false ); ///< try to hide nearby, return false if cannot + void Hide( const Vector &hidingSpot, float duration = -1.0f, bool holdPosition = false ); ///< move to the given hiding place + bool IsHiding( void ) const; ///< returns true if bot is currently hiding + bool IsAtHidingSpot( void ) const; ///< return true if we are hiding and at our hiding spot + float GetHidingTime( void ) const; ///< return number of seconds we have been at our current hiding spot + + bool MoveToInitialEncounter( void ); ///< move to a hiding spot and wait for initial encounter with enemy team (return false if no spots are available) + + bool TryToRetreat( float maxRange = 1000.0f, float duration = -1.0f ); ///< retreat to a nearby hiding spot, away from enemies + + void Hunt( void ); + bool IsHunting( void ) const; ///< returns true if bot is currently hunting + + void Attack( CCSPlayer *victim ); + void FireWeaponAtEnemy( void ); ///< fire our active weapon towards our current enemy + void StopAttacking( void ); + bool IsAttacking( void ) const; ///< returns true if bot is currently engaging a target + + void MoveTo( const Vector &pos, RouteType route = SAFEST_ROUTE ); ///< move to potentially distant position + bool IsMovingTo( void ) const; ///< return true if we are in the MoveTo state + + void PlantBomb( void ); + + void FetchBomb( void ); ///< bomb has been dropped - go get it + bool NoticeLooseBomb( void ) const; ///< return true if we noticed the bomb on the ground or on radar + bool CanSeeLooseBomb( void ) const; ///< return true if we directly see the loose bomb + + void DefuseBomb( void ); + bool IsDefusingBomb( void ) const; ///< returns true if bot is currently defusing the bomb + bool CanSeePlantedBomb( void ) const; ///< return true if we directly see the planted bomb + + void EscapeFromBomb( void ); + bool IsEscapingFromBomb( void ) const; ///< return true if we are escaping from the bomb + + void RescueHostages( void ); ///< begin process of rescuing hostages + + void UseEntity( CBaseEntity *entity ); ///< use the entity + + void OpenDoor( CBaseEntity *door ); ///< open the door (assumes we are right in front of it) + bool IsOpeningDoor( void ) const; ///< return true if we are in the process of opening a door + + void Buy( void ); ///< enter the buy state + bool IsBuying( void ) const; + + void Panic( void ); ///< look around in panic + bool IsPanicking( void ) const; ///< return true if bot is panicked + void StopPanicking( void ); ///< end our panic + void UpdatePanicLookAround( void ); ///< do panic behavior + + void TryToJoinTeam( int team ); ///< try to join the given team + + void Follow( CCSPlayer *player ); ///< begin following given Player + void ContinueFollowing( void ); ///< continue following our leader after finishing what we were doing + void StopFollowing( void ); ///< stop following + bool IsFollowing( void ) const; ///< return true if we are following someone (not necessarily in the follow state) + CCSPlayer *GetFollowLeader( void ) const; ///< return the leader we are following + float GetFollowDuration( void ) const; ///< return how long we've been following our leader + bool CanAutoFollow( void ) const; ///< return true if we can auto-follow + + bool IsNotMoving( float minDuration = 0.0f ) const; ///< return true if we are currently standing still and have been for minDuration + + void AimAtEnemy( void ); ///< point our weapon towards our enemy + void StopAiming( void ); ///< stop aiming at enemy + bool IsAimingAtEnemy( void ) const; ///< returns true if we are trying to aim at an enemy + + float GetStateTimestamp( void ) const; ///< get time current state was entered + + bool IsDoingScenario( void ) const; ///< return true if we will do scenario-related tasks + + //- scenario / gamestate ----------------------------------------------------------------------------------------- + CSGameState *GetGameState( void ); ///< return an interface to this bot's gamestate + const CSGameState *GetGameState( void ) const; ///< return an interface to this bot's gamestate + + bool IsAtBombsite( void ); ///< return true if we are in a bomb planting zone + bool GuardRandomZone( float range = 500.0f ); ///< pick a random zone and hide near it + + bool IsBusy( void ) const; ///< return true if we are busy doing something important + + //- high-level tasks --------------------------------------------------------------------------------------------- + enum TaskType + { + SEEK_AND_DESTROY, + PLANT_BOMB, + FIND_TICKING_BOMB, + DEFUSE_BOMB, + GUARD_TICKING_BOMB, + GUARD_BOMB_DEFUSER, + GUARD_LOOSE_BOMB, + GUARD_BOMB_ZONE, + GUARD_INITIAL_ENCOUNTER, + ESCAPE_FROM_BOMB, + HOLD_POSITION, + FOLLOW, + VIP_ESCAPE, + GUARD_VIP_ESCAPE_ZONE, + COLLECT_HOSTAGES, + RESCUE_HOSTAGES, + GUARD_HOSTAGES, + GUARD_HOSTAGE_RESCUE_ZONE, + MOVE_TO_LAST_KNOWN_ENEMY_POSITION, + MOVE_TO_SNIPER_SPOT, + SNIPING, + + NUM_TASKS + }; + void SetTask( TaskType task, CBaseEntity *entity = NULL ); ///< set our current "task" + TaskType GetTask( void ) const; + CBaseEntity *GetTaskEntity( void ); + const char *GetTaskName( void ) const; ///< return string describing current task + + //- behavior modifiers ------------------------------------------------------------------------------------------ + enum DispositionType + { + ENGAGE_AND_INVESTIGATE, ///< engage enemies on sight and investigate enemy noises + OPPORTUNITY_FIRE, ///< engage enemies on sight, but only look towards enemy noises, dont investigate + SELF_DEFENSE, ///< only engage if fired on, or very close to enemy + IGNORE_ENEMIES, ///< ignore all enemies - useful for ducking around corners, running away, etc + + NUM_DISPOSITIONS + }; + void SetDisposition( DispositionType disposition ); ///< define how we react to enemies + DispositionType GetDisposition( void ) const; + const char *GetDispositionName( void ) const; ///< return string describing current disposition + + void IgnoreEnemies( float duration ); ///< ignore enemies for a short duration + + enum MoraleType + { + TERRIBLE = -3, + BAD = -2, + NEGATIVE = -1, + NEUTRAL = 0, + POSITIVE = 1, + GOOD = 2, + EXCELLENT = 3, + }; + MoraleType GetMorale( void ) const; + const char *GetMoraleName( void ) const; ///< return string describing current morale + void IncreaseMorale( void ); + void DecreaseMorale( void ); + + void Surprise( float duration ); ///< become "surprised" - can't attack + bool IsSurprised( void ) const; ///< return true if we are "surprised" + + + //- listening for noises ---------------------------------------------------------------------------------------- + bool IsNoiseHeard( void ) const; ///< return true if we have heard a noise + bool HeardInterestingNoise( void ); ///< return true if we heard an enemy noise worth checking in to + void InvestigateNoise( void ); ///< investigate recent enemy noise + bool IsInvestigatingNoise( void ) const; ///< return true if we are investigating a noise + const Vector *GetNoisePosition( void ) const; ///< return position of last heard noise, or NULL if none heard + CNavArea *GetNoiseArea( void ) const; ///< return area where noise was heard + void ForgetNoise( void ); ///< clear the last heard noise + bool CanSeeNoisePosition( void ) const; ///< return true if we directly see where we think the noise came from + float GetNoiseRange( void ) const; ///< return approximate distance to last noise heard + + bool CanHearNearbyEnemyGunfire( float range = -1.0f ) const;///< return true if we hear nearby threatening enemy gunfire within given range (-1 == infinite) + PriorityType GetNoisePriority( void ) const; ///< return priority of last heard noise + + //- radio and chatter-------------------------------------------------------------------------------------------- + void SendRadioMessage( RadioType event ); ///< send a radio message + void SpeakAudio( const char *voiceFilename, float duration, int pitch ); ///< send voice chatter + BotChatterInterface *GetChatter( void ); ///< return an interface to this bot's chatter system + bool RespondToHelpRequest( CCSPlayer *player, Place place, float maxRange = -1.0f ); ///< decide if we should move to help the player, return true if we will + bool IsUsingVoice() const; ///< new-style "voice" chatter gets voice feedback + + + //- enemies ------------------------------------------------------------------------------------------------------ + // BOTPORT: GetEnemy() collides with GetEnemy() in CBaseEntity - need to use different nomenclature + void SetBotEnemy( CCSPlayer *enemy ); ///< set given player as our current enemy + CCSPlayer *GetBotEnemy( void ) const; + int GetNearbyEnemyCount( void ) const; ///< return max number of nearby enemies we've seen recently + unsigned int GetEnemyPlace( void ) const; ///< return location where we see the majority of our enemies + bool CanSeeBomber( void ) const; ///< return true if we can see the bomb carrier + CCSPlayer *GetBomber( void ) const; + + int GetNearbyFriendCount( void ) const; ///< return number of nearby teammates + CCSPlayer *GetClosestVisibleFriend( void ) const; ///< return the closest friend that we can see + CCSPlayer *GetClosestVisibleHumanFriend( void ) const; ///< return the closest human friend that we can see + + bool IsOutnumbered( void ) const; ///< return true if we are outnumbered by enemies + int OutnumberedCount( void ) const; ///< return number of enemies we are outnumbered by + + #define ONLY_VISIBLE_ENEMIES true + CCSPlayer *GetImportantEnemy( bool checkVisibility = false ) const; ///< return the closest "important" enemy for the given scenario (bomb carrier, VIP, hostage escorter) + + void UpdateReactionQueue( void ); ///< update our reaction time queue + CCSPlayer *GetRecognizedEnemy( void ); ///< return the most dangerous threat we are "conscious" of + bool IsRecognizedEnemyReloading( void ); ///< return true if the enemy we are "conscious" of is reloading + bool IsRecognizedEnemyProtectedByShield( void ); ///< return true if the enemy we are "conscious" of is hiding behind a shield + float GetRangeToNearestRecognizedEnemy( void ); ///< return distance to closest enemy we are "conscious" of + + CCSPlayer *GetAttacker( void ) const; ///< return last enemy that hurt us + float GetTimeSinceAttacked( void ) const; ///< return duration since we were last injured by an attacker + float GetFirstSawEnemyTimestamp( void ) const; ///< time since we saw any enemies + float GetLastSawEnemyTimestamp( void ) const; + float GetTimeSinceLastSawEnemy( void ) const; + float GetTimeSinceAcquiredCurrentEnemy( void ) const; + bool HasNotSeenEnemyForLongTime( void ) const; ///< return true if we haven't seen an enemy for "a long time" + const Vector &GetLastKnownEnemyPosition( void ) const; + bool IsEnemyVisible( void ) const; ///< is our current enemy visible + float GetEnemyDeathTimestamp( void ) const; + bool IsFriendInLineOfFire( void ); ///< return true if a friend is in our weapon's way + bool IsAwareOfEnemyDeath( void ) const; ///< return true if we *noticed* that our enemy died + int GetLastVictimID( void ) const; ///< return the ID (entindex) of the last victim we killed, or zero + + bool CanSeeSniper( void ) const; ///< return true if we can see an enemy sniper + bool HasSeenSniperRecently( void ) const; ///< return true if we have seen a sniper recently + + float GetTravelDistanceToPlayer( CCSPlayer *player ) const; ///< return shortest path travel distance to this player + bool DidPlayerJustFireWeapon( const CCSPlayer *player ) const; ///< return true if the given player just fired their weapon + + //- navigation -------------------------------------------------------------------------------------------------- + bool HasPath( void ) const; + void DestroyPath( void ); + + float GetFeetZ( void ) const; ///< return Z of bottom of feet + + enum PathResult + { + PROGRESSING, ///< we are moving along the path + END_OF_PATH, ///< we reached the end of the path + PATH_FAILURE ///< we failed to reach the end of the path + }; + #define NO_SPEED_CHANGE false + PathResult UpdatePathMovement( bool allowSpeedChange = true ); ///< move along our computed path - if allowSpeedChange is true, bot will walk when near goal to ensure accuracy + + //bool AStarSearch( CNavArea *startArea, CNavArea *goalArea ); ///< find shortest path from startArea to goalArea - don't actually buid the path + bool ComputePath( const Vector &goal, RouteType route = SAFEST_ROUTE ); ///< compute path to goal position + bool StayOnNavMesh( void ); + CNavArea *GetLastKnownArea( void ) const; ///< return the last area we know we were inside of + const Vector &GetPathEndpoint( void ) const; ///< return final position of our current path + float GetPathDistanceRemaining( void ) const; ///< return estimated distance left to travel along path + void ResetStuckMonitor( void ); + bool IsAreaVisible( const CNavArea *area ) const; ///< is any portion of the area visible to this bot + const Vector &GetPathPosition( int index ) const; + bool GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal = NULL ); ///< find "simple" ground height, treating current nav area as part of the floor + void BreakablesCheck( void ); + void DoorCheck( void ); ///< Check for any doors along our path that need opening + + virtual void PushawayTouch( CBaseEntity *pOther ); + + Place GetPlace( void ) const; ///< get our current radio chatter place + + bool IsUsingLadder( void ) const; ///< returns true if we are in the process of negotiating a ladder + void GetOffLadder( void ); ///< immediately jump off of our ladder, if we're on one + + void SetGoalEntity( CBaseEntity *entity ); + CBaseEntity *GetGoalEntity( void ); + + bool IsNearJump( void ) const; ///< return true if nearing a jump in the path + float GetApproximateFallDamage( float height ) const; ///< return how much damage will will take from the given fall height + + void ForceRun( float duration ); ///< force the bot to run if it moves for the given duration + virtual bool IsRunning( void ) const; + + void Wait( float duration ); ///< wait where we are for the given duration + bool IsWaiting( void ) const; ///< return true if we are waiting + void StopWaiting( void ); ///< stop waiting + + void Wiggle( void ); ///< random movement, for getting un-stuck + + bool IsFriendInTheWay( const Vector &goalPos ); ///< return true if a friend is between us and the given position + void FeelerReflexAdjustment( Vector *goalPosition ); ///< do reflex avoidance movements if our "feelers" are touched + + bool HasVisitedEnemySpawn( void ) const; ///< return true if we have visited enemy spawn at least once + bool IsAtEnemySpawn( void ) const; ///< return true if we are at the/an enemy spawn right now + + //- looking around ---------------------------------------------------------------------------------------------- + + // BOTPORT: EVIL VILE HACK - why is EyePosition() not const?!?!? + const Vector &EyePositionConst( void ) const; + + void SetLookAngles( float yaw, float pitch ); ///< set our desired look angles + void UpdateLookAngles( void ); ///< move actual view angles towards desired ones + void UpdateLookAround( bool updateNow = false ); ///< update "looking around" mechanism + void InhibitLookAround( float duration ); ///< block all "look at" and "looking around" behavior for given duration - just look ahead + + /// @todo Clean up notion of "forward angle" and "look ahead angle" + void SetForwardAngle( float angle ); ///< define our forward facing + void SetLookAheadAngle( float angle ); ///< define default look ahead angle + + /// look at the given point in space for the given duration (-1 means forever) + void SetLookAt( const char *desc, const Vector &pos, PriorityType pri, float duration = -1.0f, bool clearIfClose = false, float angleTolerance = 5.0f, bool attack = false ); + void ClearLookAt( void ); ///< stop looking at a point in space and just look ahead + bool IsLookingAtSpot( PriorityType pri = PRIORITY_LOW ) const; ///< return true if we are looking at spot with equal or higher priority + bool IsViewMoving( float angleVelThreshold = 1.0f ) const; ///< returns true if bot's view angles are rotating (not still) + bool HasViewBeenSteady( float duration ) const; ///< how long has our view been "steady" (ie: not moving) for given duration + + bool HasLookAtTarget( void ) const; ///< return true if we are in the process of looking at a target + + enum VisiblePartType + { + NONE = 0x00, + GUT = 0x01, + HEAD = 0x02, + LEFT_SIDE = 0x04, ///< the left side of the object from our point of view (not their left side) + RIGHT_SIDE = 0x08, ///< the right side of the object from our point of view (not their right side) + FEET = 0x10 + }; + + #define CHECK_FOV true + bool IsVisible( const Vector &pos, bool testFOV = false, const CBaseEntity *ignore = NULL ) const; ///< return true if we can see the point + bool IsVisible( CCSPlayer *player, bool testFOV = false, unsigned char *visParts = NULL ) const; ///< return true if we can see any part of the player + + bool IsNoticable( const CCSPlayer *player, unsigned char visibleParts ) const; ///< return true if we "notice" given player + + bool IsEnemyPartVisible( VisiblePartType part ) const; ///< if enemy is visible, return the part we see for our current enemy + const Vector &GetPartPosition( CCSPlayer *player, VisiblePartType part ) const; ///< return world space position of given part on player + + float ComputeWeaponSightRange( void ); ///< return line-of-sight distance to obstacle along weapon fire ray + + bool IsAnyVisibleEnemyLookingAtMe( bool testFOV = false ) const;///< return true if any enemy I have LOS to is looking directly at me + + bool IsSignificantlyCloser( const CCSPlayer *testPlayer, const CCSPlayer *referencePlayer ) const; ///< return true if testPlayer is significantly closer than referencePlayer + + //- approach points --------------------------------------------------------------------------------------------- + void ComputeApproachPoints( void ); ///< determine the set of "approach points" representing where the enemy can enter this region + void UpdateApproachPoints( void ); ///< recompute the approach point set if we have moved far enough to invalidate the current ones + void ClearApproachPoints( void ); + void DrawApproachPoints( void ) const; ///< for debugging + float GetHidingSpotCheckTimestamp( HidingSpot *spot ) const; ///< return time when given spot was last checked + void SetHidingSpotCheckTimestamp( HidingSpot *spot ); ///< set the timestamp of the given spot to now + + const CNavArea *GetInitialEncounterArea( void ) const; ///< return area where we think we will first meet the enemy + void SetInitialEncounterArea( const CNavArea *area ); + + //- weapon query and equip -------------------------------------------------------------------------------------- + #define MUST_EQUIP true + void EquipBestWeapon( bool mustEquip = false ); ///< equip the best weapon we are carrying that has ammo + void EquipPistol( void ); ///< equip our pistol + void EquipKnife( void ); ///< equip the knife + + #define DONT_USE_SMOKE_GRENADE true + bool EquipGrenade( bool noSmoke = false ); ///< equip a grenade, return false if we cant + + bool IsUsingKnife( void ) const; ///< returns true if we have knife equipped + bool IsUsingPistol( void ) const; ///< returns true if we have pistol equipped + bool IsUsingGrenade( void ) const; ///< returns true if we have grenade equipped + bool IsUsingSniperRifle( void ) const; ///< returns true if using a "sniper" rifle + bool IsUsing( CSWeaponID weapon ) const; ///< returns true if using the specific weapon + bool IsSniper( void ) const; ///< return true if we have a sniper rifle in our inventory + bool IsSniping( void ) const; ///< return true if we are actively sniping (moving to sniper spot or settled in) + bool IsUsingShotgun( void ) const; ///< returns true if using a shotgun + bool IsUsingMachinegun( void ) const; ///< returns true if using the big 'ol machinegun + void ThrowGrenade( const Vector &target ); ///< begin the process of throwing the grenade + bool IsThrowingGrenade( void ) const; ///< return true if we are in the process of throwing a grenade + bool HasGrenade( void ) const; ///< return true if we have a grenade in our inventory + void AvoidEnemyGrenades( void ); ///< react to enemy grenades we see + bool IsAvoidingGrenade( void ) const; ///< return true if we are in the act of avoiding a grenade + bool DoesActiveWeaponHaveSilencer( void ) const; ///< returns true if we are using a weapon with a removable silencer + bool CanActiveWeaponFire( void ) const; ///< returns true if our current weapon can attack + CWeaponCSBase *GetActiveCSWeapon( void ) const; ///< get our current Counter-Strike weapon + + void GiveWeapon( const char *weaponAlias ); ///< Debug command to give a named weapon + + virtual void PrimaryAttack( void ); ///< presses the fire button, unless we're holding a pistol that can't fire yet (so we can just always call PrimaryAttack()) + + enum ZoomType { NO_ZOOM, LOW_ZOOM, HIGH_ZOOM }; + ZoomType GetZoomLevel( void ); ///< return the current zoom level of our weapon + + bool AdjustZoom( float range ); ///< change our zoom level to be appropriate for the given range + bool IsWaitingForZoom( void ) const; ///< return true if we are reacquiring after our zoom + + bool IsPrimaryWeaponEmpty( void ) const; ///< return true if primary weapon doesn't exist or is totally out of ammo + bool IsPistolEmpty( void ) const; ///< return true if pistol doesn't exist or is totally out of ammo + + int GetHostageEscortCount( void ) const; ///< return the number of hostages following me + void IncreaseHostageEscortCount( void ); + float GetRangeToFarthestEscortedHostage( void ) const; ///< return euclidean distance to farthest escorted hostage + void ResetWaitForHostagePatience( void ); + + //------------------------------------------------------------------------------------ + // Event hooks + // + + /// invoked when injured by something (EXTEND) - returns the amount of damage inflicted + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + + /// invoked when killed (EXTEND) + virtual void Event_Killed( const CTakeDamageInfo &info ); + + virtual bool BumpWeapon( CBaseCombatWeapon *pWeapon ); ///< invoked when in contact with a CWeaponBox + + + /// invoked when event occurs in the game (some events have NULL entity) + void OnPlayerFootstep( IGameEvent *event ); + void OnPlayerRadio( IGameEvent *event ); + void OnPlayerDeath( IGameEvent *event ); + void OnPlayerFallDamage( IGameEvent *event ); + + void OnBombPickedUp( IGameEvent *event ); + void OnBombPlanted( IGameEvent *event ); + void OnBombBeep( IGameEvent *event ); + void OnBombDefuseBegin( IGameEvent *event ); + void OnBombDefused( IGameEvent *event ); + void OnBombDefuseAbort( IGameEvent *event ); + void OnBombExploded( IGameEvent *event ); + + void OnRoundEnd( IGameEvent *event ); + void OnRoundStart( IGameEvent *event ); + + void OnDoorMoving( IGameEvent *event ); + + void OnBreakProp( IGameEvent *event ); + void OnBreakBreakable( IGameEvent *event ); + + void OnHostageFollows( IGameEvent *event ); + void OnHostageRescuedAll( IGameEvent *event ); + + void OnWeaponFire( IGameEvent *event ); + void OnWeaponFireOnEmpty( IGameEvent *event ); + void OnWeaponReload( IGameEvent *event ); + void OnWeaponZoom( IGameEvent *event ); + + void OnBulletImpact( IGameEvent *event ); + + void OnHEGrenadeDetonate( IGameEvent *event ); + void OnFlashbangDetonate( IGameEvent *event ); + void OnSmokeGrenadeDetonate( IGameEvent *event ); + void OnGrenadeBounce( IGameEvent *event ); + + void OnNavBlocked( IGameEvent *event ); + + void OnEnteredNavArea( CNavArea *newArea ); ///< invoked when bot enters a nav area + +private: + #define IS_FOOTSTEP true + void OnAudibleEvent( IGameEvent *event, CBasePlayer *player, float range, PriorityType priority, bool isHostile, bool isFootstep = false, const Vector *actualOrigin = NULL ); ///< Checks if the bot can hear the event + +private: + friend class CCSBotManager; + + /// @todo Get rid of these + friend class AttackState; + friend class BuyState; + + // BOTPORT: Remove this vile hack + Vector m_eyePosition; + + void ResetValues( void ); ///< reset internal data to initial state + void BotDeathThink( void ); + + char m_name[64]; ///< copied from STRING(pev->netname) for debugging + void DebugDisplay( void ) const; ///< render bot debug info + + //- behavior properties ------------------------------------------------------------------------------------------ + float m_combatRange; ///< desired distance between us and them during gunplay + mutable bool m_isRogue; ///< if true, the bot is a "rogue" and listens to no-one + mutable CountdownTimer m_rogueTimer; + MoraleType m_morale; ///< our current morale, based on our win/loss history + bool m_diedLastRound; ///< true if we died last round + float m_safeTime; ///< duration at the beginning of the round where we feel "safe" + bool m_wasSafe; ///< true if we were in the safe time last update + void AdjustSafeTime( void ); ///< called when enemy seen to adjust safe time for this round + NavRelativeDirType m_blindMoveDir; ///< which way to move when we're blind + bool m_blindFire; ///< if true, fire weapon while blinded + CountdownTimer m_surpriseTimer; ///< when we were surprised + + bool m_isFollowing; ///< true if we are following someone + CHandle< CCSPlayer > m_leader; ///< the ID of who we are following + float m_followTimestamp; ///< when we started following + float m_allowAutoFollowTime; ///< time when we can auto follow + + CountdownTimer m_hurryTimer; ///< if valid, bot is in a hurry + CountdownTimer m_alertTimer; ///< if valid, bot is alert + CountdownTimer m_sneakTimer; ///< if valid, bot is sneaking + CountdownTimer m_panicTimer; ///< if valid, bot is panicking + + + // instances of each possible behavior state, to avoid dynamic memory allocation during runtime + IdleState m_idleState; + HuntState m_huntState; + AttackState m_attackState; + InvestigateNoiseState m_investigateNoiseState; + BuyState m_buyState; + MoveToState m_moveToState; + FetchBombState m_fetchBombState; + PlantBombState m_plantBombState; + DefuseBombState m_defuseBombState; + HideState m_hideState; + EscapeFromBombState m_escapeFromBombState; + FollowState m_followState; + UseEntityState m_useEntityState; + OpenDoorState m_openDoorState; + + /// @todo Allow multiple simultaneous state machines (look around, etc) + void SetState( BotState *state ); ///< set the current behavior state + BotState *m_state; ///< current behavior state + float m_stateTimestamp; ///< time state was entered + bool m_isAttacking; ///< if true, special Attack state is overriding the state machine + bool m_isOpeningDoor; ///< if true, special OpenDoor state is overriding the state machine + + TaskType m_task; ///< our current task + EHANDLE m_taskEntity; ///< an entity used for our task + + //- navigation --------------------------------------------------------------------------------------------------- + Vector m_goalPosition; + EHANDLE m_goalEntity; + void MoveTowardsPosition( const Vector &pos ); ///< move towards position, independant of view angle + void MoveAwayFromPosition( const Vector &pos ); ///< move away from position, independant of view angle + void StrafeAwayFromPosition( const Vector &pos ); ///< strafe (sidestep) away from position, independant of view angle + void StuckCheck( void ); ///< check if we have become stuck + CCSNavArea *m_currentArea; ///< the nav area we are standing on + CCSNavArea *m_lastKnownArea; ///< the last area we were in + EHANDLE m_avoid; ///< higher priority player we need to make way for + float m_avoidTimestamp; + bool m_isStopping; ///< true if we're trying to stop because we entered a 'stop' nav area + bool m_hasVisitedEnemySpawn; ///< true if we have been at the enemy spawn + IntervalTimer m_stillTimer; ///< how long we have been not moving + + //- path navigation data ---------------------------------------------------------------------------------------- + enum { MAX_PATH_LENGTH = 256 }; + struct ConnectInfo + { + CNavArea *area; ///< the area along the path + NavTraverseType how; ///< how to enter this area from the previous one + Vector pos; ///< our movement goal position at this point in the path + const CNavLadder *ladder; ///< if "how" refers to a ladder, this is it + } + m_path[ MAX_PATH_LENGTH ]; + int m_pathLength; + int m_pathIndex; ///< index of next area on path + float m_areaEnteredTimestamp; + void BuildTrivialPath( const Vector &goal ); ///< build trivial path to goal, assuming we are already in the same area + + CountdownTimer m_repathTimer; ///< must have elapsed before bot can pathfind again + + bool ComputePathPositions( void ); ///< determine actual path positions bot will move between along the path + void SetupLadderMovement( void ); + void SetPathIndex( int index ); ///< set the current index along the path + void DrawPath( void ); + int FindOurPositionOnPath( Vector *close, bool local = false ) const; ///< compute the closest point to our current position on our path + int FindPathPoint( float aheadRange, Vector *point, int *prevIndex = NULL ); ///< compute a point a fixed distance ahead along our path. + bool FindClosestPointOnPath( const Vector &pos, int startIndex, int endIndex, Vector *close ) const; ///< compute closest point on path to given point + bool IsStraightLinePathWalkable( const Vector &goal ) const; ///< test for un-jumpable height change, or unrecoverable fall + void ComputeLadderAngles( float *yaw, float *pitch ); ///< computes ideal yaw/pitch for traversing the current ladder on our path + + mutable CountdownTimer m_avoidFriendTimer; ///< used to throttle how often we check for friends in our path + mutable bool m_isFriendInTheWay; ///< true if a friend is blocking our path + CountdownTimer m_politeTimer; ///< we'll wait for friend to move until this runs out + bool m_isWaitingBehindFriend; ///< true if we are waiting for a friend to move + + #define ONLY_JUMP_DOWN true + bool DiscontinuityJump( float ground, bool onlyJumpDown = false, bool mustJump = false ); ///< check if we need to jump due to height change + + enum LadderNavState + { + APPROACH_ASCENDING_LADDER, ///< prepare to scale a ladder + APPROACH_DESCENDING_LADDER, ///< prepare to go down ladder + FACE_ASCENDING_LADDER, + FACE_DESCENDING_LADDER, + MOUNT_ASCENDING_LADDER, ///< move toward ladder until "on" it + MOUNT_DESCENDING_LADDER, ///< move toward ladder until "on" it + ASCEND_LADDER, ///< go up the ladder + DESCEND_LADDER, ///< go down the ladder + DISMOUNT_ASCENDING_LADDER, ///< get off of the ladder + DISMOUNT_DESCENDING_LADDER, ///< get off of the ladder + MOVE_TO_DESTINATION, ///< dismount ladder and move to destination area + } + m_pathLadderState; + bool m_pathLadderFaceIn; ///< if true, face towards ladder, otherwise face away + const CNavLadder *m_pathLadder; ///< the ladder we need to use to reach the next area + bool UpdateLadderMovement( void ); ///< called by UpdatePathMovement() + NavRelativeDirType m_pathLadderDismountDir; ///< which way to dismount + float m_pathLadderDismountTimestamp; ///< time when dismount started + float m_pathLadderEnd; ///< if ascending, z of top, if descending z of bottom + void ComputeLadderEndpoint( bool ascending ); + float m_pathLadderTimestamp; ///< time when we started using ladder - for timeout check + + CountdownTimer m_mustRunTimer; ///< if nonzero, bot cannot walk + CountdownTimer m_waitTimer; ///< if nonzero, we are waiting where we are + + void UpdateTravelDistanceToAllPlayers( void ); ///< periodically compute shortest path distance to each player + CountdownTimer m_updateTravelDistanceTimer; ///< for throttling travel distance computations + float m_playerTravelDistance[ MAX_PLAYERS ]; ///< current distance from this bot to each player + unsigned char m_travelDistancePhase; ///< a counter for optimizing when to compute travel distance + + //- game scenario mechanisms ------------------------------------------------------------------------------------- + CSGameState m_gameState; ///< our current knowledge about the state of the scenario + + byte m_hostageEscortCount; ///< the number of hostages we're currently escorting + void UpdateHostageEscortCount( void ); ///< periodic check of hostage count in case we lost some + float m_hostageEscortCountTimestamp; + + int m_desiredTeam; ///< the team we want to be on + bool m_hasJoined; ///< true if bot has actually joined the game + + bool m_isWaitingForHostage; + CountdownTimer m_inhibitWaitingForHostageTimer; ///< if active, inhibits us waiting for lagging hostages + CountdownTimer m_waitForHostageTimer; ///< stops us waiting too long + + //- listening mechanism ------------------------------------------------------------------------------------------ + Vector m_noisePosition; ///< position we last heard non-friendly noise + float m_noiseTravelDistance; ///< the travel distance to the noise + float m_noiseTimestamp; ///< when we heard it (can get zeroed) + CNavArea *m_noiseArea; ///< the nav area containing the noise + PriorityType m_noisePriority; ///< priority of currently heard noise + bool UpdateLookAtNoise( void ); ///< return true if we decided to look towards the most recent noise source + CountdownTimer m_noiseBendTimer; ///< for throttling how often we bend our line of sight to the noise location + Vector m_bentNoisePosition; ///< the last computed bent line of sight + bool m_bendNoisePositionValid; + + //- "looking around" mechanism ----------------------------------------------------------------------------------- + float m_lookAroundStateTimestamp; ///< time of next state change + float m_lookAheadAngle; ///< our desired forward look angle + float m_forwardAngle; ///< our current forward facing direction + float m_inhibitLookAroundTimestamp; ///< time when we can look around again + + enum LookAtSpotState + { + NOT_LOOKING_AT_SPOT, ///< not currently looking at a point in space + LOOK_TOWARDS_SPOT, ///< in the process of aiming at m_lookAtSpot + LOOK_AT_SPOT, ///< looking at m_lookAtSpot + NUM_LOOK_AT_SPOT_STATES + } + m_lookAtSpotState; + Vector m_lookAtSpot; ///< the spot we're currently looking at + PriorityType m_lookAtSpotPriority; + float m_lookAtSpotDuration; ///< how long we need to look at the spot + float m_lookAtSpotTimestamp; ///< when we actually began looking at the spot + float m_lookAtSpotAngleTolerance; ///< how exactly we must look at the spot + bool m_lookAtSpotClearIfClose; ///< if true, the look at spot is cleared if it gets close to us + bool m_lookAtSpotAttack; ///< if true, the look at spot should be attacked + const char *m_lookAtDesc; ///< for debugging + void UpdateLookAt( void ); + void UpdatePeripheralVision(); ///< update enounter spot timestamps, etc + float m_peripheralTimestamp; + + enum { MAX_APPROACH_POINTS = 16 }; + struct ApproachPoint + { + Vector m_pos; + CNavArea *m_area; + }; + + ApproachPoint m_approachPoint[ MAX_APPROACH_POINTS ]; + unsigned char m_approachPointCount; + Vector m_approachPointViewPosition; ///< the position used when computing current approachPoint set + + CBaseEntity * FindEntitiesOnPath( float distance, CPushAwayEnumerator *enumerator, bool checkStuck ); + + IntervalTimer m_viewSteadyTimer; ///< how long has our view been "steady" (ie: not moving) + + bool BendLineOfSight( const Vector &eye, const Vector &target, Vector *bend, float angleLimit = 135.0f ) const; ///< "bend" our line of sight until we can see the target point. Return bend point, false if cant bend. + bool FindApproachPointNearestPath( Vector *pos ); ///< find the approach point that is nearest to our current path, ahead of us + bool FindGrenadeTossPathTarget( Vector *pos ); ///< find spot to throw grenade ahead of us and "around the corner" along our path + enum GrenadeTossState + { + NOT_THROWING, ///< not yet throwing + START_THROW, ///< lining up throw + THROW_LINED_UP, ///< pause for a moment when on-line + FINISH_THROW, ///< throwing + }; + GrenadeTossState m_grenadeTossState; + CountdownTimer m_tossGrenadeTimer; ///< timeout timer for grenade tossing + const CNavArea *m_initialEncounterArea; ///< area where we think we will initially encounter the enemy + void LookForGrenadeTargets( void ); ///< look for grenade throw targets and throw our grenade at them + void UpdateGrenadeThrow( void ); ///< process grenade throwing + CountdownTimer m_isAvoidingGrenade; ///< if nonzero we are in the act of avoiding a grenade + + + SpotEncounter *m_spotEncounter; ///< the spots we will encounter as we move thru our current area + float m_spotCheckTimestamp; ///< when to check next encounter spot + + /// @todo Add timestamp for each possible client to hiding spots + enum { MAX_CHECKED_SPOTS = 64 }; + struct HidingSpotCheckInfo + { + HidingSpot *spot; + float timestamp; + } + m_checkedHidingSpot[ MAX_CHECKED_SPOTS ]; + int m_checkedHidingSpotCount; + + //- view angle mechanism ----------------------------------------------------------------------------------------- + float m_lookPitch; ///< our desired look pitch angle + float m_lookPitchVel; + float m_lookYaw; ///< our desired look yaw angle + float m_lookYawVel; + + //- aim angle mechanism ----------------------------------------------------------------------------------------- + Vector m_aimOffset; ///< current error added to victim's position to get actual aim spot + Vector m_aimOffsetGoal; ///< desired aim offset + float m_aimOffsetTimestamp; ///< time of next offset adjustment + float m_aimSpreadTimestamp; ///< time used to determine max spread as it begins to tighten up + void SetAimOffset( float accuracy ); ///< set the current aim offset + void UpdateAimOffset( void ); ///< wiggle aim error based on m_accuracy + Vector m_aimSpot; ///< the spot we are currently aiming to fire at + + struct PartInfo + { + Vector m_headPos; ///< current head position + Vector m_gutPos; ///< current gut position + Vector m_feetPos; ///< current feet position + Vector m_leftSidePos; ///< current left side position + Vector m_rightSidePos; ///< current right side position + int m_validFrame; ///< frame of last computation (for lazy evaluation) + }; + static PartInfo m_partInfo[ MAX_PLAYERS ]; ///< part positions for each player + void ComputePartPositions( CCSPlayer *player ); ///< compute part positions from bone location + + //- attack state data -------------------------------------------------------------------------------------------- + DispositionType m_disposition; ///< how we will react to enemies + CountdownTimer m_ignoreEnemiesTimer; ///< how long will we ignore enemies + mutable CHandle< CCSPlayer > m_enemy; ///< our current enemy + bool m_isEnemyVisible; ///< result of last visibility test on enemy + unsigned char m_visibleEnemyParts; ///< which parts of the visible enemy do we see + Vector m_lastEnemyPosition; ///< last place we saw the enemy + float m_lastSawEnemyTimestamp; + float m_firstSawEnemyTimestamp; + float m_currentEnemyAcquireTimestamp; + float m_enemyDeathTimestamp; ///< if m_enemy is dead, this is when he died + float m_friendDeathTimestamp; ///< time since we saw a friend die + bool m_isLastEnemyDead; ///< true if we killed or saw our last enemy die + int m_nearbyEnemyCount; ///< max number of enemies we've seen recently + unsigned int m_enemyPlace; ///< the location where we saw most of our enemies + + struct WatchInfo + { + float timestamp; ///< time we last saw this player, zero if never seen + bool isEnemy; + } + m_watchInfo[ MAX_PLAYERS ]; + mutable CHandle< CCSPlayer > m_bomber; ///< points to bomber if we can see him + + int m_nearbyFriendCount; ///< number of nearby teammates + mutable CHandle< CCSPlayer > m_closestVisibleFriend; ///< the closest friend we can see + mutable CHandle< CCSPlayer > m_closestVisibleHumanFriend; ///< the closest human friend we can see + + IntervalTimer m_attentionInterval; ///< time between attention checks + + mutable CHandle< CCSPlayer > m_attacker; ///< last enemy that hurt us (may not be same as m_enemy) + float m_attackedTimestamp; ///< when we were hurt by the m_attacker + + int m_lastVictimID; ///< the entindex of the last victim we killed, or zero + bool m_isAimingAtEnemy; ///< if true, we are trying to aim at our enemy + bool m_isRapidFiring; ///< if true, RunUpkeep() will toggle our primary attack as fast as it can + IntervalTimer m_equipTimer; ///< how long have we had our current weapon equipped + CountdownTimer m_zoomTimer; ///< for delaying firing immediately after zoom + bool DoEquip( CWeaponCSBase *gun ); ///< equip the given item + + void ReloadCheck( void ); ///< reload our weapon if we must + void SilencerCheck( void ); ///< use silencer + + float m_fireWeaponTimestamp; + + bool m_isEnemySniperVisible; ///< do we see an enemy sniper right now + CountdownTimer m_sawEnemySniperTimer; ///< tracking time since saw enemy sniper + + //- reaction time system ----------------------------------------------------------------------------------------- + enum { MAX_ENEMY_QUEUE = 20 }; + struct ReactionState + { + // NOTE: player position & orientation is not currently stored separately + CHandle<CCSPlayer> player; + bool isReloading; + bool isProtectedByShield; + } + m_enemyQueue[ MAX_ENEMY_QUEUE ]; ///< round-robin queue for simulating reaction times + byte m_enemyQueueIndex; + byte m_enemyQueueCount; + byte m_enemyQueueAttendIndex; ///< index of the timeframe we are "conscious" of + + CCSPlayer *FindMostDangerousThreat( void ); ///< return most dangerous threat in my field of view (feeds into reaction time queue) + + + //- stuck detection --------------------------------------------------------------------------------------------- + bool m_isStuck; + float m_stuckTimestamp; ///< time when we got stuck + Vector m_stuckSpot; ///< the location where we became stuck + NavRelativeDirType m_wiggleDirection; + CountdownTimer m_wiggleTimer; + CountdownTimer m_stuckJumpTimer; ///< time for next jump when stuck + + enum { MAX_VEL_SAMPLES = 10 }; + float m_avgVel[ MAX_VEL_SAMPLES ]; + int m_avgVelIndex; + int m_avgVelCount; + Vector m_lastOrigin; + + //- radio -------------------------------------------------------------------------------------------------------- + RadioType m_lastRadioCommand; ///< last radio command we recieved + float m_lastRadioRecievedTimestamp; ///< time we recieved a radio message + float m_lastRadioSentTimestamp; ///< time when we send a radio message + CHandle< CCSPlayer > m_radioSubject; ///< who issued the radio message + Vector m_radioPosition; ///< position referred to in radio message + void RespondToRadioCommands( void ); + bool IsRadioCommand( RadioType event ) const; ///< returns true if the radio message is an order to do something + + /// new-style "voice" chatter gets voice feedback + float m_voiceEndTimestamp; + + BotChatterInterface m_chatter; ///< chatter mechanism +}; + + +// +// Inlines +// + +inline float CCSBot::GetFeetZ( void ) const +{ + return GetAbsOrigin().z; +} + +inline const Vector *CCSBot::GetNoisePosition( void ) const +{ + if (m_noiseTimestamp > 0.0f) + return &m_noisePosition; + + return NULL; +} + +inline bool CCSBot::IsAwareOfEnemyDeath( void ) const +{ + if (GetEnemyDeathTimestamp() == 0.0f) + return false; + + if (m_enemy == NULL) + return true; + + if (!m_enemy->IsAlive() && gpGlobals->curtime - GetEnemyDeathTimestamp() > (1.0f - 0.8f * GetProfile()->GetSkill())) + return true; + + return false; +} + +inline void CCSBot::Panic( void ) +{ + // we are stunned for a moment + Surprise( RandomFloat( 0.2f, 0.3f ) ); + + const float panicTime = 3.0f; + m_panicTimer.Start( panicTime ); + + const float panicRetreatRange = 300.0f; + TryToRetreat( panicRetreatRange, 0.0f ); + + PrintIfWatched( "*** PANIC ***\n" ); +} + +inline bool CCSBot::IsPanicking( void ) const +{ + return !m_panicTimer.IsElapsed(); +} + +inline void CCSBot::StopPanicking( void ) +{ + m_panicTimer.Invalidate(); +} + +inline bool CCSBot::IsNotMoving( float minDuration ) const +{ + return (m_stillTimer.HasStarted() && m_stillTimer.GetElapsedTime() >= minDuration); +} + +inline CWeaponCSBase *CCSBot::GetActiveCSWeapon( void ) const +{ + return reinterpret_cast<CWeaponCSBase *>( GetActiveWeapon() ); +} + + +inline float CCSBot::GetCombatRange( void ) const +{ + return m_combatRange; +} + +inline void CCSBot::SetRogue( bool rogue ) +{ + m_isRogue = rogue; +} + +inline void CCSBot::Hurry( float duration ) +{ + m_hurryTimer.Start( duration ); +} + +inline float CCSBot::GetSafeTime( void ) const +{ + return m_safeTime; +} + +inline bool CCSBot::IsUnhealthy( void ) const +{ + return (GetHealth() <= 40); +} + +inline bool CCSBot::IsAlert( void ) const +{ + return !m_alertTimer.IsElapsed(); +} + +inline void CCSBot::BecomeAlert( void ) +{ + const float alertCooldownTime = 10.0f; + m_alertTimer.Start( alertCooldownTime ); +} + +inline bool CCSBot::IsSneaking( void ) const +{ + return !m_sneakTimer.IsElapsed(); +} + +inline void CCSBot::Sneak( float duration ) +{ + m_sneakTimer.Start( duration ); +} + +inline bool CCSBot::IsFollowing( void ) const +{ + return m_isFollowing; +} + +inline CCSPlayer *CCSBot::GetFollowLeader( void ) const +{ + return m_leader; +} + +inline float CCSBot::GetFollowDuration( void ) const +{ + return gpGlobals->curtime - m_followTimestamp; +} + +inline bool CCSBot::CanAutoFollow( void ) const +{ + return (gpGlobals->curtime > m_allowAutoFollowTime); +} + +inline void CCSBot::AimAtEnemy( void ) +{ + m_isAimingAtEnemy = true; +} + +inline void CCSBot::StopAiming( void ) +{ + m_isAimingAtEnemy = false; +} + +inline bool CCSBot::IsAimingAtEnemy( void ) const +{ + return m_isAimingAtEnemy; +} + +inline float CCSBot::GetStateTimestamp( void ) const +{ + return m_stateTimestamp; +} + +inline CSGameState *CCSBot::GetGameState( void ) +{ + return &m_gameState; +} + +inline const CSGameState *CCSBot::GetGameState( void ) const +{ + return &m_gameState; +} + +inline bool CCSBot::IsAtBombsite( void ) +{ + return m_bInBombZone; +} + +inline void CCSBot::SetTask( TaskType task, CBaseEntity *entity ) +{ + m_task = task; + m_taskEntity = entity; +} + +inline CCSBot::TaskType CCSBot::GetTask( void ) const +{ + return m_task; +} + +inline CBaseEntity *CCSBot::GetTaskEntity( void ) +{ + return static_cast<CBaseEntity *>( m_taskEntity ); +} + +inline CCSBot::MoraleType CCSBot::GetMorale( void ) const +{ + return m_morale; +} + +inline void CCSBot::Surprise( float duration ) +{ + m_surpriseTimer.Start( duration ); +} + +inline bool CCSBot::IsSurprised( void ) const +{ + return !m_surpriseTimer.IsElapsed(); +} + +inline CNavArea *CCSBot::GetNoiseArea( void ) const +{ + return m_noiseArea; +} + +inline void CCSBot::ForgetNoise( void ) +{ + m_noiseTimestamp = 0.0f; +} + +inline float CCSBot::GetNoiseRange( void ) const +{ + if (IsNoiseHeard()) + return m_noiseTravelDistance; + + return 999999999.9f; +} + +inline PriorityType CCSBot::GetNoisePriority( void ) const +{ + return m_noisePriority; +} + +inline BotChatterInterface *CCSBot::GetChatter( void ) +{ + return &m_chatter; +} + +inline CCSPlayer *CCSBot::GetBotEnemy( void ) const +{ + return m_enemy; +} + +inline int CCSBot::GetNearbyEnemyCount( void ) const +{ + return MIN( GetEnemiesRemaining(), m_nearbyEnemyCount ); +} + +inline unsigned int CCSBot::GetEnemyPlace( void ) const +{ + return m_enemyPlace; +} + +inline bool CCSBot::CanSeeBomber( void ) const +{ + return (m_bomber == NULL) ? false : true; +} + +inline CCSPlayer *CCSBot::GetBomber( void ) const +{ + return m_bomber; +} + +inline int CCSBot::GetNearbyFriendCount( void ) const +{ + return MIN( GetFriendsRemaining(), m_nearbyFriendCount ); +} + +inline CCSPlayer *CCSBot::GetClosestVisibleFriend( void ) const +{ + return m_closestVisibleFriend; +} + +inline CCSPlayer *CCSBot::GetClosestVisibleHumanFriend( void ) const +{ + return m_closestVisibleHumanFriend; +} + +inline float CCSBot::GetTimeSinceAttacked( void ) const +{ + return gpGlobals->curtime - m_attackedTimestamp; +} + +inline float CCSBot::GetFirstSawEnemyTimestamp( void ) const +{ + return m_firstSawEnemyTimestamp; +} + +inline float CCSBot::GetLastSawEnemyTimestamp( void ) const +{ + return m_lastSawEnemyTimestamp; +} + +inline float CCSBot::GetTimeSinceLastSawEnemy( void ) const +{ + return gpGlobals->curtime - m_lastSawEnemyTimestamp; +} + +inline float CCSBot::GetTimeSinceAcquiredCurrentEnemy( void ) const +{ + return gpGlobals->curtime - m_currentEnemyAcquireTimestamp; +} + +inline const Vector &CCSBot::GetLastKnownEnemyPosition( void ) const +{ + return m_lastEnemyPosition; +} + +inline bool CCSBot::IsEnemyVisible( void ) const +{ + return m_isEnemyVisible; +} + +inline float CCSBot::GetEnemyDeathTimestamp( void ) const +{ + return m_enemyDeathTimestamp; +} + +inline int CCSBot::GetLastVictimID( void ) const +{ + return m_lastVictimID; +} + +inline bool CCSBot::CanSeeSniper( void ) const +{ + return m_isEnemySniperVisible; +} + +inline bool CCSBot::HasSeenSniperRecently( void ) const +{ + return !m_sawEnemySniperTimer.IsElapsed(); +} + +inline float CCSBot::GetTravelDistanceToPlayer( CCSPlayer *player ) const +{ + if (player == NULL) + return -1.0f; + + if (!player->IsAlive()) + return -1.0f; + + return m_playerTravelDistance[ player->entindex() % MAX_PLAYERS ]; +} + +inline bool CCSBot::HasPath( void ) const +{ + return (m_pathLength) ? true : false; +} + +inline void CCSBot::DestroyPath( void ) +{ + m_isStopping = false; + m_pathLength = 0; + m_pathLadder = NULL; +} + +inline CNavArea *CCSBot::GetLastKnownArea( void ) const +{ + return m_lastKnownArea; +} + +inline const Vector &CCSBot::GetPathEndpoint( void ) const +{ + return m_path[ m_pathLength-1 ].pos; +} + +inline const Vector &CCSBot::GetPathPosition( int index ) const +{ + return m_path[ index ].pos; +} + +inline bool CCSBot::IsUsingLadder( void ) const +{ + return (m_pathLadder) ? true : false; +} + +inline void CCSBot::SetGoalEntity( CBaseEntity *entity ) +{ + m_goalEntity = entity; +} + +inline CBaseEntity *CCSBot::GetGoalEntity( void ) +{ + return m_goalEntity; +} + +inline void CCSBot::ForceRun( float duration ) +{ + Run(); + m_mustRunTimer.Start( duration ); +} + +inline void CCSBot::Wait( float duration ) +{ + m_waitTimer.Start( duration ); +} + +inline bool CCSBot::IsWaiting( void ) const +{ + return !m_waitTimer.IsElapsed(); +} + +inline void CCSBot::StopWaiting( void ) +{ + m_waitTimer.Invalidate(); +} + +inline bool CCSBot::HasVisitedEnemySpawn( void ) const +{ + return m_hasVisitedEnemySpawn; +} + +inline const Vector &CCSBot::EyePositionConst( void ) const +{ + return m_eyePosition; +} + +inline void CCSBot::SetLookAngles( float yaw, float pitch ) +{ + m_lookYaw = yaw; + m_lookPitch = pitch; +} + +inline void CCSBot::SetForwardAngle( float angle ) +{ + m_forwardAngle = angle; +} + +inline void CCSBot::SetLookAheadAngle( float angle ) +{ + m_lookAheadAngle = angle; +} + +inline void CCSBot::ClearLookAt( void ) +{ + //PrintIfWatched( "ClearLookAt()\n" ); + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + m_lookAtDesc = NULL; +} + +inline bool CCSBot::IsLookingAtSpot( PriorityType pri ) const +{ + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && m_lookAtSpotPriority >= pri) + return true; + + return false; +} + +inline bool CCSBot::IsViewMoving( float angleVelThreshold ) const +{ + if (m_lookYawVel < angleVelThreshold && m_lookYawVel > -angleVelThreshold && + m_lookPitchVel < angleVelThreshold && m_lookPitchVel > -angleVelThreshold) + { + return false; + } + + return true; +} + +inline bool CCSBot::HasViewBeenSteady( float duration ) const +{ + return (m_viewSteadyTimer.GetElapsedTime() > duration); +} + +inline bool CCSBot::HasLookAtTarget( void ) const +{ + return (m_lookAtSpotState != NOT_LOOKING_AT_SPOT); +} + +inline bool CCSBot::IsEnemyPartVisible( VisiblePartType part ) const +{ + VPROF_BUDGET( "CCSBot::IsEnemyPartVisible", VPROF_BUDGETGROUP_NPCS ); + + if (!IsEnemyVisible()) + return false; + + return (m_visibleEnemyParts & part) ? true : false; +} + +inline bool CCSBot::IsSignificantlyCloser( const CCSPlayer *testPlayer, const CCSPlayer *referencePlayer ) const +{ + if ( !referencePlayer ) + return true; + + if ( !testPlayer ) + return false; + + float testDist = ( GetAbsOrigin() - testPlayer->GetAbsOrigin() ).Length(); + float referenceDist = ( GetAbsOrigin() - referencePlayer->GetAbsOrigin() ).Length(); + + const float significantRangeFraction = 0.7f; + if ( testDist < referenceDist * significantRangeFraction ) + return true; + + return false; +} + +inline void CCSBot::ClearApproachPoints( void ) +{ + m_approachPointCount = 0; +} + +inline const CNavArea *CCSBot::GetInitialEncounterArea( void ) const +{ + return m_initialEncounterArea; +} + +inline void CCSBot::SetInitialEncounterArea( const CNavArea *area ) +{ + m_initialEncounterArea = area; +} + +inline bool CCSBot::IsThrowingGrenade( void ) const +{ + return m_grenadeTossState != NOT_THROWING; +} + +inline bool CCSBot::IsAvoidingGrenade( void ) const +{ + return !m_isAvoidingGrenade.IsElapsed(); +} + +inline void CCSBot::PrimaryAttack( void ) +{ + if ( IsUsingPistol() && !CanActiveWeaponFire() ) + return; + + BaseClass::PrimaryAttack(); +} + +inline CCSBot::ZoomType CCSBot::GetZoomLevel( void ) +{ + if (GetFOV() > 60.0f) + return NO_ZOOM; + if (GetFOV() > 25.0f) + return LOW_ZOOM; + return HIGH_ZOOM; +} + +inline bool CCSBot::IsWaitingForZoom( void ) const +{ + return !m_zoomTimer.IsElapsed(); +} + +inline int CCSBot::GetHostageEscortCount( void ) const +{ + return m_hostageEscortCount; +} + +inline void CCSBot::IncreaseHostageEscortCount( void ) +{ + ++m_hostageEscortCount; +} + +inline void CCSBot::ResetWaitForHostagePatience( void ) +{ + m_isWaitingForHostage = false; + m_inhibitWaitingForHostageTimer.Invalidate(); +} + + +inline bool CCSBot::IsUsingVoice() const +{ + return m_voiceEndTimestamp > gpGlobals->curtime; +} + +inline bool CCSBot::IsOpeningDoor( void ) const +{ + return m_isOpeningDoor; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given weapon is a sniper rifle + */ +inline bool IsSniperRifle( CWeaponCSBase *weapon ) +{ + if (weapon == NULL) + return false; + + return weapon->IsKindOf(WEAPONTYPE_SNIPER_RIFLE); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor used with NavAreaBuildPath() + */ +class PathCost +{ +public: + PathCost( CCSBot *bot, RouteType route = SAFEST_ROUTE ) + { + m_bot = bot; + m_route = route; + } + + // HPE_TODO[pmf]: check that these new parameters are okay to be ignored + float operator() ( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) + { + float baseDangerFactor = 100.0f; // 100 + + // respond to the danger modulated by our aggression (even super-aggressives pay SOME attention to danger) + float dangerFactor = (1.0f - (0.95f * m_bot->GetProfile()->GetAggression())) * baseDangerFactor; + + if (fromArea == NULL) + { + if (m_route == FASTEST_ROUTE) + return 0.0f; + + // first area in path, cost is just danger + return dangerFactor * area->GetDanger( m_bot->GetTeamNumber() ); + } + else if ((fromArea->GetAttributes() & NAV_MESH_JUMP) && (area->GetAttributes() & NAV_MESH_JUMP)) + { + // cannot actually walk in jump areas - disallow moving from jump area to jump area + return -1.0f; + } + if ( area->GetAttributes() & NAV_MESH_NO_HOSTAGES && m_bot->GetHostageEscortCount() ) + { + // if we're leading hostages, don't try to go where they can't + return -1.0f; + } + else + { + // compute distance from previous area to this area + float dist; + if (ladder) + { + // ladders are slow to use + const float ladderPenalty = 1.0f; // 3.0f; + dist = ladderPenalty * ladder->m_length; + + // if we are currently escorting hostages, avoid ladders (hostages are confused by them) + //if (m_bot->GetHostageEscortCount()) + // dist *= 100.0f; + } + else + { + dist = (area->GetCenter() - fromArea->GetCenter()).Length(); + } + + // compute distance travelled along path so far + float cost = dist + fromArea->GetCostSoFar(); + + // zombies ignore all path penalties + if (cv_bot_zombie.GetBool()) + return cost; + + // add cost of "jump down" pain unless we're jumping into water + if (!area->IsUnderwater() && area->IsConnected( fromArea, NUM_DIRECTIONS ) == false) + { + // this is a "jump down" (one way drop) transition - estimate damage we will take to traverse it + float fallDistance = -fromArea->ComputeGroundHeightChange( area ); + + // if it's a drop-down ladder, estimate height from the bottom of the ladder to the lower area + if ( ladder && ladder->m_bottom.z < fromArea->GetCenter().z && ladder->m_bottom.z > area->GetCenter().z ) + { + fallDistance = ladder->m_bottom.z - area->GetCenter().z; + } + + float fallDamage = m_bot->GetApproximateFallDamage( fallDistance ); + + if (fallDamage > 0.0f) + { + // if the fall would kill us, don't use it + const float deathFallMargin = 10.0f; + if (fallDamage + deathFallMargin >= m_bot->GetHealth()) + return -1.0f; + + // if we need to get there in a hurry, ignore minor pain + const float painTolerance = 15.0f * m_bot->GetProfile()->GetAggression() + 10.0f; + if (m_route != FASTEST_ROUTE || fallDamage > painTolerance) + { + // cost is proportional to how much it hurts when we fall + // 10 points - not a big deal, 50 points - ouch! + cost += 100.0f * fallDamage * fallDamage; + } + } + } + + // if this is a "crouch" or "walk" area, add penalty + if (area->GetAttributes() & (NAV_MESH_CROUCH | NAV_MESH_WALK)) + { + // these areas are very slow to move through + float penalty = (m_route == FASTEST_ROUTE) ? 20.0f : 5.0f; + + // avoid crouch areas if we are rescuing hostages + if ((area->GetAttributes() & NAV_MESH_CROUCH) && m_bot->GetHostageEscortCount()) + { + penalty *= 3.0f; + } + + cost += penalty * dist; + } + + // if this is a "jump" area, add penalty + if (area->GetAttributes() & NAV_MESH_JUMP) + { + // jumping can slow you down + //const float jumpPenalty = (m_route == FASTEST_ROUTE) ? 100.0f : 0.5f; + const float jumpPenalty = 1.0f; + cost += jumpPenalty * dist; + } + + // if this is an area to avoid, add penalty + if (area->GetAttributes() & NAV_MESH_AVOID) + { + const float avoidPenalty = 20.0f; + cost += avoidPenalty * dist; + } + + if (m_route == SAFEST_ROUTE) + { + // add in the danger of this path - danger is per unit length travelled + cost += dist * dangerFactor * area->GetDanger( m_bot->GetTeamNumber() ); + } + + if (!m_bot->IsAttacking()) + { + // add in cost of teammates in the way + + // approximate density of teammates based on area + float size = (area->GetSizeX() + area->GetSizeY())/2.0f; + + // degenerate check + if (size >= 1.0f) + { + // cost is proportional to the density of teammates in this area + const float costPerFriendPerUnit = 50000.0f; + cost += costPerFriendPerUnit * (float)area->GetPlayerCount( m_bot->GetTeamNumber() ) / size; + } + } + + return cost; + } + } + +private: + CCSBot *m_bot; + RouteType m_route; +}; + + +//-------------------------------------------------------------------------------------------------------------- +// +// Prototypes +// +extern int GetBotFollowCount( CCSPlayer *leader ); +extern const Vector *FindNearbyRetreatSpot( CCSBot *me, float maxRange = 250.0f ); +extern const HidingSpot *FindInitialEncounterSpot( CBaseEntity *me, const Vector &searchOrigin, float enemyArriveTime, float maxRange, bool isSniper ); + + +#endif // _CS_BOT_H_ + diff --git a/game/server/cstrike/bot/cs_bot_chatter.cpp b/game/server/cstrike/bot/cs_bot_chatter.cpp new file mode 100644 index 0000000..4d8b4dc --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_chatter.cpp @@ -0,0 +1,2582 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_player.h" +#include "shared_util.h" +#include "engine/IEngineSound.h" +#include "KeyValues.h" + +#include "bot.h" +#include "bot_util.h" +#include "cs_bot.h" +#include "cs_bot_chatter.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +/** + * @todo Fix this + */ +const Vector *GetRandomSpotAtPlace( Place place ) +{ + int count = 0; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if (area->GetPlace() == place) + ++count; + } + + if (count == 0) + return NULL; + + int which = RandomInt( 0, count-1 ); + + FOR_EACH_VEC( TheNavAreas, rit ) + { + CNavArea *area = TheNavAreas[ rit ]; + + if (area->GetPlace() == place && which == 0) + return &area->GetCenter(); + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Transmit meme to other bots + */ +void BotMeme::Transmit( CCSBot *sender ) const +{ + for( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + +// if (FNullEnt( player->pev )) +// continue; + +// if (FStrEq( STRING( player->pev->netname ), "" )) +// continue; + + // skip self + if (sender == player) + continue; + + // ignore dead humans + if (!player->IsBot() && !player->IsAlive()) + continue; + + // ignore enemies, since we can't hear them talk + if (!player->InSameTeam( sender )) + continue; + + // if not a bot, fail the test + if (!player->IsBot()) + continue; + + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + + if ( !bot ) + continue; + + // allow bot to interpret our meme + Interpret( sender, bot ); + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate called for help - respond + */ +void BotHelpMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + const float maxHelpRange = 3000.0f; // 2000 + receiver->RespondToHelpRequest( sender, m_place, maxHelpRange ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate reported information about a bombsite + */ +void BotBombsiteStatusMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + // remember this bombsite's status + if (m_status == CLEAR) + receiver->GetGameState()->ClearBombsite( m_zoneIndex ); + else + receiver->GetGameState()->MarkBombsiteAsPlanted( m_zoneIndex ); + + // if we were heading to the just-cleared bombsite, pick another one to search + // if our target bombsite wasn't cleared, will will continue going to it, + // because GetNextBombsiteToSearch() will return the same zone (since its not cleared) + // if the bomb was planted, we will head to that bombsite + if (receiver->GetTask() == CCSBot::FIND_TICKING_BOMB) + { + receiver->Idle(); + receiver->GetChatter()->Affirmative(); + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate reported information about the bomb + */ +void BotBombStatusMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + // update our gamestate based on teammate's report + switch( m_state ) + { + case CSGameState::MOVING: + receiver->GetGameState()->UpdateBomber( m_pos ); + + // if we are hunting and see no enemies, respond + if (!receiver->IsRogue() && receiver->IsHunting() && receiver->GetNearbyEnemyCount() == 0) + receiver->RespondToHelpRequest( sender, TheNavMesh->GetPlace( m_pos ) ); + + break; + + case CSGameState::LOOSE: + receiver->GetGameState()->UpdateLooseBomb( m_pos ); + + if (receiver->GetTask() == CCSBot::GUARD_BOMB_ZONE) + { + receiver->Idle(); + receiver->GetChatter()->Affirmative(); + } + break; + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked that we follow him + */ +void BotFollowMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + if (receiver->IsRogue()) + return; + + // if we're busy, ignore + if (receiver->IsBusy()) + return; + + // if we are too far away, ignore + // compute actual travel distance + Vector senderOrigin = GetCentroid( sender ); + PathCost cost( receiver ); + float travelDistance = NavAreaTravelDistance( receiver->GetLastKnownArea(), + TheNavMesh->GetNearestNavArea( senderOrigin ), + cost ); + if (travelDistance < 0.0f) + return; + + const float tooFar = 1000.0f; + if (travelDistance > tooFar) + return; + + // begin following + receiver->Follow( sender ); + + // acknowledge + receiver->GetChatter()->Say( "CoveringFriend" ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked us to defend a place + */ +void BotDefendHereMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + if (receiver->IsRogue()) + return; + + // if we're busy, ignore + if (receiver->IsBusy()) + return; + + Place place = TheNavMesh->GetPlace( m_pos ); + if (place != UNDEFINED_PLACE) + { + // pick a random hiding spot in this place + const Vector *spot = FindRandomHidingSpot( receiver, place, receiver->IsSniper() ); + if (spot) + { + receiver->SetTask( CCSBot::HOLD_POSITION ); + receiver->Hide( *spot ); + return; + } + } + + // hide nearby + receiver->SetTask( CCSBot::HOLD_POSITION ); + receiver->Hide( TheNavMesh->GetNearestNavArea( m_pos ) ); + + // acknowledge + receiver->GetChatter()->Say( "Affirmative" ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked where the bomb is planted + */ +void BotWhereBombMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + int zone = receiver->GetGameState()->GetPlantedBombsite(); + + if (zone != CSGameState::UNKNOWN) + receiver->GetChatter()->FoundPlantedBomb( zone ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate has asked us to report in + */ +void BotRequestReportMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetChatter()->ReportingIn(); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate told us all the hostages are gone + */ +void BotAllHostagesGoneMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetGameState()->AllHostagesGone(); + + // acknowledge + receiver->GetChatter()->Say( "Affirmative" ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate told us a CT is talking to a hostage + */ +void BotHostageBeingTakenMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetGameState()->HostageWasTaken(); + + // if we're busy, ignore + if (receiver->IsBusy()) + return; + + receiver->Idle(); + + // acknowledge + receiver->GetChatter()->Say( "Affirmative" ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate heard a noise, so we shouldn't report noises for a while + */ +void BotHeardNoiseMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetChatter()->FriendHeardNoise(); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * A teammate warned about snipers, so we shouldn't warn again for awhile + */ +void BotWarnSniperMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const +{ + receiver->GetChatter()->FriendSpottedSniper(); +} + + +//--------------------------------------------------------------------------------------------------------------- +BotSpeakable::BotSpeakable() +{ + m_phrase = NULL; +} + +//--------------------------------------------------------------------------------------------------------------- +BotSpeakable::~BotSpeakable() +{ + if ( m_phrase ) + { + delete[] m_phrase; + m_phrase = NULL; + } +} + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +BotPhrase::BotPhrase( bool isPlace ) +{ + m_name = NULL; + m_place = UNDEFINED_PLACE; + m_isPlace = isPlace; + m_radioEvent = RADIO_INVALID; + m_isImportant = false; + ClearCriteria(); + m_numVoiceBanks = 0; + InitVoiceBank( 0 ); +} + +BotPhrase::~BotPhrase() +{ + for( int bank=0; bank<m_voiceBank.Count(); ++bank ) + { + for( int speakable=0; speakable<m_voiceBank[bank]->Count(); ++speakable ) + { + delete (*m_voiceBank[bank])[speakable]; + } + delete m_voiceBank[bank]; + } + + if ( m_name ) + delete [] m_name; +} + +void BotPhrase::InitVoiceBank( int bankIndex ) +{ + while ( m_numVoiceBanks <= bankIndex ) + { + m_count.AddToTail(0); + m_index.AddToTail(0); + m_voiceBank.AddToTail( new BotSpeakableVector ); + ++m_numVoiceBanks; + } +} + +/** + * Return a random speakable - avoid repeating + */ +char *BotPhrase::GetSpeakable( int bankIndex, float *duration ) const +{ + if (bankIndex < 0 || bankIndex >= m_numVoiceBanks || m_count[bankIndex] == 0) + { + if (duration) + *duration = 0.0f; + + return NULL; + } + + // find phrase that meets the current criteria + int start = m_index[bankIndex]; + while(true) + { + BotSpeakableVector *speakables = m_voiceBank[bankIndex]; + int& index = m_index[bankIndex]; + const BotSpeakable *speak = (*speakables)[index++]; + + if (m_index[bankIndex] >= m_count[bankIndex]) + m_index[bankIndex] = 0; + + // check place criteria + // if this speakable has a place criteria, it must match to be used + // speakables with Place of ANY will match any place + // speakables with a specific Place will only be used if Place matches + // speakables with Place of UNDEFINED only match Place of UNDEFINED + if (speak->m_place == ANY_PLACE || speak->m_place == m_placeCriteria) + { + // check count criteria + // if this speakable has a count criteria, it must match to be used + // if this speakable does not have a count criteria, we dont care what the count is set to + if (speak->m_count == UNDEFINED_COUNT || speak->m_count == MIN( m_countCriteria, COUNT_MANY )) + { + if (duration) + *duration = speak->m_duration; + + return speak->m_phrase; + } + } + + // check if we exhausted all speakables + if (m_index[bankIndex] == start) + { + if (duration) + *duration = 0.0f; + + return NULL; + } + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Randomly shuffle the speakable order + */ +void BotPhrase::Randomize( void ) +{ + for ( int bank = 0; bank < m_voiceBank.Count(); ++bank ) + { + BotSpeakableVector *speakables = m_voiceBank[bank]; + if ( speakables->Count() == 1 ) + continue; + + // A simple shuffle: for each array index, swap it with a random index + for ( int index = 0; index < speakables->Count(); ++index ) + { + int newIndex = RandomInt( 0, speakables->Count()-1 ); + + BotSpeakable *speakable = (*speakables)[index]; + (*speakables)[index] = (*speakables)[newIndex]; + (*speakables)[newIndex] = speakable; + } + } +} + + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +BotPhraseManager *TheBotPhrases = NULL; + +BotPhraseManager::BotPhraseManager( void ) +{ + m_placeCount = 0; +} + + +/** + * Invoked when map changes + */ +void BotPhraseManager::OnMapChange( void ) +{ + m_placeCount = 0; +} + +/** + * Removes everything from memory + */ +void BotPhraseManager::Reset( void ) +{ + int i; + + // free phrase resources + for( i=0; i<m_list.Count(); ++i ) + { + delete m_list[i]; + } + + for( i=0; i<m_placeList.Count(); ++i ) + { + delete m_placeList[i]; + } + + m_list.RemoveAll(); + m_placeList.RemoveAll(); + + m_painPhrase = NULL; + m_agreeWithPlanPhrase = NULL; +} + + +/** + * Invoked when the round resets + */ +void BotPhraseManager::OnRoundRestart( void ) +{ + // effectively reset all interval timers + m_placeCount = 0; + + // shuffle all the speakables + int i; + for( i=0; i<m_placeList.Count(); ++i ) + m_placeList[i]->Randomize(); + + for( i=0; i<m_list.Count(); ++i ) + m_list[i]->Randomize(); +} + +BotChatterOutputType BotPhraseManager::GetOutputType( int voiceBank ) const +{ + if ( voiceBank >= 0 && voiceBank < m_output.Count() ) + { + return m_output[voiceBank]; + } + return BOT_CHATTER_RADIO; +} + +/** + * Initialize phrase system from database file + */ +bool BotPhraseManager::Initialize( const char *filename, int bankIndex ) +{ + bool isDefault = (bankIndex == 0); + + FileHandle_t file = filesystem->Open( filename, "r" ); + if (!file) + { + CONSOLE_ECHO( "WARNING: Cannot access bot phrase database '%s'\n", filename ); + return false; + } + + // BOTPORT: Redo file reading to avoid loading whole file into memory at once + int phraseDataLength = filesystem->Size( filename ); + char *phraseDataFile = new char[ phraseDataLength ]; + + int dataReadLength = filesystem->Read( phraseDataFile, phraseDataLength, file ); + + filesystem->Close( file ); + + if ( dataReadLength > 0 ) + { + // NULL-terminate based on the length read in, since Read() can transform \r\n to \n and + // return fewer bytes than we were expecting. + phraseDataFile[ dataReadLength - 1 ] = 0; + } + + const char *phraseData = phraseDataFile; + + + const int RadioPathLen = 128; // wav filenames need to be shorter than this to go over the net anyway. + char baseDir[RadioPathLen] = ""; + char compositeFilename[RadioPathLen]; + + // + // Parse the BotChatter.db into BotPhrase collections + // + while( true ) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + break; + + char *token = SharedGetToken(); + + if ( !stricmp( token, "Output" ) ) + { + // get name of this output device + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename ); + delete [] phraseDataFile; + return false; + } + + while ( m_output.Count() <= bankIndex ) + { + m_output.AddToTail(BOT_CHATTER_RADIO); + } + + char *token = SharedGetToken(); + if ( !stricmp( token, "Voice" ) ) + { + m_output[bankIndex] = BOT_CHATTER_VOICE; + } + } + else if ( !stricmp( token, "BaseDir" ) ) + { + // get name of this output device + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename ); + delete [] phraseDataFile; + return false; + } + char *token = SharedGetToken(); + Q_strncpy( baseDir, token, RadioPathLen ); + Q_strncat( baseDir, "\\", RadioPathLen, -1 ); + baseDir[RadioPathLen-1] = 0; + } + else if (!stricmp( token, "Place" ) || !stricmp( token, "Chatter" )) + { + bool isPlace = (stricmp( token, "Place" )) ? false : true; + + // encountered a new phrase collection + BotPhrase *phrase = NULL; + if ( isDefault ) + { + phrase = new BotPhrase( isPlace ); + } + + // get name of this phrase + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename ); + delete [] phraseDataFile; + return false; + } + if ( isDefault ) + { + phrase->m_name = CloneString( SharedGetToken() ); + + phrase->m_place = (isPlace) ? TheNavMesh->NameToPlace( phrase->m_name ) : UNDEFINED_PLACE; + } + else // look up the existing phrase + { + if ( isPlace ) + { + phrase = const_cast<BotPhrase *>(GetPlace( SharedGetToken() )); + } + else + { + phrase = const_cast<BotPhrase *>(GetPhrase( SharedGetToken() )); + } + + if ( !phrase ) + { + CONSOLE_ECHO( "Error parsing '%s' - phrase '%s' is invalid\n", filename, SharedGetToken() ); + delete [] phraseDataFile; + return false; + } + } + phrase->InitVoiceBank( bankIndex ); + + PlaceCriteria placeCriteria = ANY_PLACE; + CountCriteria countCriteria = UNDEFINED_COUNT; + RadioType radioEvent = RADIO_INVALID; + bool isImportant = false; + + // read attributes of this phrase + while( true ) + { + // get next token + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + // check for Place criteria + if (!stricmp( token, "Place" )) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected Place name\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + // update place criteria for subsequent speak lines + // NOTE: this assumes places must be first in the chatter database + + // check for special identifiers + if (!stricmp( "ANY", token )) + placeCriteria = ANY_PLACE; + else if (!stricmp( "UNDEFINED", token )) + placeCriteria = UNDEFINED_PLACE; + else + placeCriteria = TheNavMesh->NameToPlace( token ); + + continue; + } + + // check for Count criteria + if (!stricmp( token, "Count" )) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected Count value\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + // update count criteria for subsequent speak lines + if (!stricmp( token, "Many" )) + countCriteria = COUNT_MANY; + else + countCriteria = atoi( token ); + + continue; + } + + // check for radio equivalent + if (!stricmp( token, "Radio" )) + { + phraseData = SharedParse( phraseData ); + if (!phraseData) + { + CONSOLE_ECHO( "Error parsing %s - expected radio event\n", filename ); + delete [] phraseDataFile; + return false; + } + token = SharedGetToken(); + + RadioType event = NameToRadioEvent( token ); + if (event <= RADIO_START_1 || event >= RADIO_END) + { + CONSOLE_ECHO( "Error parsing %s - invalid radio event '%s'\n", filename, token ); + delete [] phraseDataFile; + return false; + } + + radioEvent = event; + + continue; + } + + // check for "important" flag + if (!stricmp( token, "Important" )) + { + isImportant = true; + continue; + } + + // check for End delimiter + if (!stricmp( token, "End" )) + break; + + // found a phrase - add it to the collection + BotSpeakable *speak = new BotSpeakable; + if ( baseDir[0] ) + { + Q_snprintf( compositeFilename, RadioPathLen, "%s%s", baseDir, token ); + speak->m_phrase = CloneString( compositeFilename ); + } + else + { + speak->m_phrase = CloneString( token ); + } + speak->m_place = placeCriteria; + speak->m_count = countCriteria; +#ifdef POSIX + Q_FixSlashes( speak->m_phrase ); + Q_strlower( speak->m_phrase ); +#endif + + speak->m_duration = enginesound->GetSoundDuration( speak->m_phrase ); + + if (speak->m_duration <= 0.0f) + { + if ( !engine->IsDedicatedServer() ) + { + DevMsg( "Warning: Couldn't get duration of phrase '%s'\n", speak->m_phrase ); + } + speak->m_duration = 1.0f; + } + + BotSpeakableVector * speakables = phrase->m_voiceBank[ bankIndex ]; + speakables->AddToTail( speak ); + + ++phrase->m_count[ bankIndex ]; + } + + if ( isDefault ) + { + phrase->m_radioEvent = radioEvent; + phrase->m_isImportant = isImportant; + } + + // add phrase collection to the appropriate master list + if (isPlace) + m_placeList.AddToTail( phrase ); + else + m_list.AddToTail( phrase ); + } + } + + delete [] phraseDataFile; + + m_painPhrase = GetPhrase( "Pain" ); + m_agreeWithPlanPhrase = GetPhrase( "AgreeWithPlan" ); + + return true; +} + +BotPhraseManager::~BotPhraseManager() +{ + Reset(); +} + +/** + * Given a name, return the associated phrase collection + */ +const BotPhrase *BotPhraseManager::GetPhrase( const char *name ) const +{ + for( int i=0; i<m_list.Count(); ++i ) + { + if (!stricmp( m_list[i]->m_name, name )) + return m_list[i]; + } + + //CONSOLE_ECHO( "GetPhrase: ERROR - Invalid phrase '%s'\n", name ); + return NULL; +} + +/** + * Given an id, return the associated phrase collection + * @todo Store phrases in a vector to make this fast + */ +/* +const BotPhrase *BotPhraseManager::GetPhrase( unsigned int place ) const +{ + for( BotPhraseList::const_iterator iter = m_list.begin(); iter != m_list.end(); ++iter ) + { + const BotPhrase *phrase = *iter; + if (phrase->m_place == id) + return phrase; + } + + CONSOLE_ECHO( "GetPhrase: ERROR - Invalid phrase id #%d\n", id ); + return NULL; +} +*/ + +/** + * Given a name, return the associated Place phrase collection + */ +const BotPhrase *BotPhraseManager::GetPlace( const char *name ) const +{ + if (name == NULL) + return NULL; + + for( int i=0; i<m_placeList.Count(); ++i ) + { + if (!stricmp( m_placeList[i]->m_name, name )) + return m_placeList[i]; + } + + return NULL; +} + +/** + * Given a name, return the associated Place phrase collection + */ +const BotPhrase *BotPhraseManager::GetPlace( PlaceCriteria place ) const +{ + if (place == UNDEFINED_PLACE) + return NULL; + + for( int i=0; i<m_placeList.Count(); ++i ) + { + if (m_placeList[i]->m_place == place) + return m_placeList[i]; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +BotStatement::BotStatement( BotChatterInterface *chatter, BotStatementType type, float expireDuration ) +{ + m_chatter = chatter; + + m_next = NULL; + m_prev = NULL; + m_timestamp = gpGlobals->curtime; + m_speakTimestamp = 0.0f; + + m_type = type; + m_subject = UNDEFINED_SUBJECT; + m_place = UNDEFINED_PLACE; + m_meme = NULL; + + m_startTime = gpGlobals->curtime; + m_expireTime = gpGlobals->curtime + expireDuration; + m_isSpeaking = false; + + m_nextTime = 0.0f; + m_index = -1; + m_count = 0; + + m_conditionCount = 0; +} + +BotStatement::~BotStatement() +{ + if (m_meme) + delete m_meme; +} + + +//--------------------------------------------------------------------------------------------------------------- +CCSBot *BotStatement::GetOwner( void ) const +{ + return m_chatter->GetOwner(); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Attach a meme to this statement, to be transmitted to other friendly bots when spoken + */ +void BotStatement::AttachMeme( BotMeme *meme ) +{ + m_meme = meme; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Add a conditions that must be true for the statement to be spoken + */ +void BotStatement::AddCondition( ConditionType condition ) +{ + if (m_conditionCount < MAX_BOT_CONDITIONS) + m_condition[ m_conditionCount++ ] = condition; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Return true if this statement is "important" and not personality chatter + */ +bool BotStatement::IsImportant( void ) const +{ + // if a statement contains any important phrases, it is important + for( int i=0; i<m_count; ++i ) + { + if (m_statement[i].isPhrase && m_statement[i].phrase->IsImportant()) + return true; + + // hack for now - phrases with enemy counts are important + if (!m_statement[i].isPhrase && m_statement[i].context == BotStatement::CURRENT_ENEMY_COUNT) + return true; + } + + return false; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Verify all attached conditions + */ +bool BotStatement::IsValid( void ) const +{ + for( int i=0; i<m_conditionCount; ++i ) + { + switch( m_condition[i] ) + { + case IS_IN_COMBAT: + { + if (!GetOwner()->IsAttacking()) + return false; + break; + } + +/* + case RADIO_SILENCE: + { + if (GetOwner()->GetChatter()->GetRadioSilenceDuration() < 10.0f) + return false; + break; + } +*/ + + case ENEMIES_REMAINING: + { + if (GetOwner()->GetEnemiesRemaining() == 0) + return false; + break; + } + } + } + + return true; +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Return true if this statement is essentially the same as the given one + */ +bool BotStatement::IsRedundant( const BotStatement *say ) const +{ + // special cases + if (GetType() == REPORT_MY_PLAN || + GetType() == REPORT_REQUEST_HELP || + GetType() == REPORT_CRITICAL_EVENT || + GetType() == REPORT_ACKNOWLEDGE) + return false; + + // check if topics are different + if (say->GetType() != GetType()) + return false; + + if (!say->HasPlace() && !HasPlace() && !say->HasSubject() && !HasSubject()) + { + // neither has place or subject, so they are the same + return true; + } + + // check if subject matter is the same + if (say->HasPlace() && HasPlace() && say->GetPlace() == GetPlace()) + { + // talking about the same place + return true; + } + + if (say->HasSubject() && HasSubject() && say->GetSubject() == GetSubject()) + { + // talking about the same player + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Return true if this statement is no longer appropriate to say + */ +bool BotStatement::IsObsolete( void ) const +{ + // if the round is over, the only things we should say are emotes + if (GetOwner()->GetGameState()->IsRoundOver()) + { + if (m_type != REPORT_EMOTE) + return true; + } + + // If we're wanting to say "I lost him" but we've spotted another enemy, + // we no longer need to report losing someone. + if ( GetOwner()->GetChatter()->SeesAtLeastOneEnemy() && m_type == REPORT_ENEMY_LOST ) + { + return true; + } + + // check if statement lifetime has expired + return (gpGlobals->curtime > m_expireTime); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Possibly change what were going to say base on what teammate is saying + */ +void BotStatement::Convert( const BotStatement *say ) +{ + if (GetType() == REPORT_MY_PLAN && say->GetType() == REPORT_MY_PLAN) + { + const BotPhrase *meToo = TheBotPhrases->GetAgreeWithPlanPhrase(); + + // don't reconvert + if (m_statement[0].phrase == meToo) + return; + + // if our plans are the same, change our statement to "me too" + if (m_statement[0].phrase == say->m_statement[0].phrase) + { + if (m_place == say->m_place) + { + // same plan at the same place - convert to "me too" + m_statement[0].phrase = meToo; + m_startTime = gpGlobals->curtime + RandomFloat( 0.5f, 1.0f ); + } + else + { + // same plan at different place - wait a bit to allow others to respond "me too" + m_startTime = gpGlobals->curtime + RandomFloat( 3.0f, 4.0f ); + } + } + } +} + +//--------------------------------------------------------------------------------------------------------------- +void BotStatement::AppendPhrase( const BotPhrase *phrase ) +{ + if (phrase == NULL) + return; + + if (m_count < MAX_BOT_PHRASES) + { + m_statement[ m_count ].isPhrase = true; + m_statement[ m_count++ ].phrase = phrase; + } +} + +/** + * Special phrases that depend on the context + */ +void BotStatement::AppendPhrase( ContextType contextPhrase ) +{ + if (m_count < MAX_BOT_PHRASES) + { + m_statement[ m_count ].isPhrase = false; + m_statement[ m_count++ ].context = contextPhrase; + } +} + +/** + * Say our statement + * m_index refers to the phrase currently being spoken, or -1 if we havent started yet + */ +bool BotStatement::Update( void ) +{ + CCSBot *me = GetOwner(); + + // if all of our teammates are dead, the only non-redundant statements are emotes + if (me->GetFriendsRemaining() == 0 && GetType() != REPORT_EMOTE) + return false; + + if (!m_isSpeaking) + { + m_isSpeaking = true; + m_speakTimestamp = gpGlobals->curtime; + } + + // special case - context dependent delay + if (m_index >= 0 && m_statement[ m_index ].context == ACCUMULATE_ENEMIES_DELAY) + { + // report if we see a lot of enemies, or if enough time has passed + const float reportTime = 2.0f; // 1 + if (me->GetNearbyEnemyCount() > 3 || gpGlobals->curtime - m_speakTimestamp > reportTime) + { + // enough enemies have accumulated to expire this delay + m_nextTime = 0.0f; + } + } + + + if (gpGlobals->curtime > m_nextTime) + { + // check for end of statement + if (++m_index == m_count) + { + // transmit any memes carried in this statement to our teammates + if (m_meme) + m_meme->Transmit( me ); + + return false; + } + + // start next part of statement + float duration = 0.0f; + const BotPhrase *phrase = NULL; + + if (m_statement[ m_index ].isPhrase) + { + // normal phrase + phrase = m_statement[ m_index ].phrase; + } + else + { + // context-dependant phrase + switch( m_statement[ m_index ].context ) + { + case CURRENT_ENEMY_COUNT: + { + int enemyCount = me->GetNearbyEnemyCount(); + + // if we are outnumbered, ask for help + if (enemyCount-1 > me->GetNearbyFriendCount()) + { + phrase = TheBotPhrases->GetPhrase( "Help" ); + AttachMeme( new BotHelpMeme() ); + } + else if (enemyCount > 1) + { + phrase = TheBotPhrases->GetPhrase( "EnemySpotted" ); + phrase->SetCountCriteria( enemyCount ); + } + break; + } + + case REMAINING_ENEMY_COUNT: + { + static const char *speak[] = + { + "NoEnemiesLeft", "OneEnemyLeft", "TwoEnemiesLeft", "ThreeEnemiesLeft" + }; + + int enemyCount = me->GetEnemiesRemaining(); + + // dont report if there are lots of enemies left + if (enemyCount < 0 || enemyCount > 3) + { + phrase = NULL; + } + else + { + phrase = TheBotPhrases->GetPhrase( speak[ enemyCount ] ); + } + break; + } + + case SHORT_DELAY: + { + m_nextTime = gpGlobals->curtime + RandomFloat( 0.1f, 0.5f ); + return true; + } + + case LONG_DELAY: + { + m_nextTime = gpGlobals->curtime + RandomFloat( 1.0f, 2.0f ); + return true; + } + + case ACCUMULATE_ENEMIES_DELAY: + { + // wait until test becomes true + m_nextTime = 99999999.9f; + return true; + } + } + } + + if (phrase) + { + // if chatter system is in "standard radio" mode, send the equivalent radio command + if (me->GetChatter()->GetVerbosity() == BotChatterInterface::RADIO) + { + RadioType radioEvent = phrase->GetRadioEquivalent(); + if (radioEvent == RADIO_INVALID) + { + // skip directly to the next phrase + m_nextTime = 0.0f; + } + else + { + // use the standard radio + me->GetChatter()->ResetRadioSilenceDuration(); + me->SendRadioMessage( radioEvent ); + duration = 2.0f; + } + } + else + { + // set place criteria + phrase->SetPlaceCriteria( m_place ); + + const char *filename = phrase->GetSpeakable( me->GetProfile()->GetVoiceBank(), &duration ); + // CONSOLE_ECHO( "%s: Radio( '%s' )\n", STRING( me->pev->netname ), filename ); + + bool sayIt = true; + + if (phrase->IsPlace()) + { + // don't repeat the place if someone just mentioned it not too long ago + float timeSince = TheBotPhrases->GetPlaceStatementInterval( phrase->GetPlace() ); + const float minRepeatTime = 20.0f; // 30 + if (timeSince < minRepeatTime) + { + sayIt = false; + } + else + { + TheBotPhrases->ResetPlaceStatementInterval( phrase->GetPlace() ); + } + } + + if (sayIt) + { + if ( !filename ) + { + RadioType radioEvent = phrase->GetRadioEquivalent(); + if (radioEvent == RADIO_INVALID) + { + // skip directly to the next phrase + m_nextTime = 0.0f; + } + else + { + // use the standard radio + me->SendRadioMessage( radioEvent ); + me->GetChatter()->ResetRadioSilenceDuration(); + duration = 2.0f; + } + } + /* BOTPORT: Wire up bot voice over IP + else if ( g_engfuncs.pfnPlayClientVoice && TheBotPhrases->GetOutputType( me->GetProfile()->GetVoiceBank() ) == BOT_CHATTER_VOICE ) + { + me->GetChatter()->ResetRadioSilenceDuration(); + g_engfuncs.pfnPlayClientVoice( me->entindex() - 1, filename ); + } + */ + else + { + me->SpeakAudio( filename, duration + 1.0f, me->GetProfile()->GetVoicePitch() ); + } + } + } + + const float gap = 0.1f; + m_nextTime = gpGlobals->curtime + duration + gap; + } + else + { + // skip directly to the next phrase + m_nextTime = 0.0f; + } + } + + return true; +} + +/** + * If this statement refers to a specific place, return that place + * Places can be implicit in the statement, or explicitly defined + */ +unsigned int BotStatement::GetPlace( void ) const +{ + // return any explicitly set place if we have one + if (m_place != UNDEFINED_PLACE) + return m_place; + + // look for an implicit place in our statement + for( int i=0; i<m_count; ++i ) + if (m_statement[i].isPhrase && m_statement[i].phrase->IsPlace()) + return m_statement[i].phrase->GetPlace(); + + return 0; +} + +/** + * Return true if this statement has an associated count + */ +bool BotStatement::HasCount( void ) const +{ + for( int i=0; i<m_count; ++i ) + if (!m_statement[i].isPhrase && m_statement[i].context == CURRENT_ENEMY_COUNT) + return true; + + return false; +} + +//--------------------------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------------------------- + +CountdownTimer BotChatterInterface::m_encourageTimer; +IntervalTimer BotChatterInterface::m_radioSilenceInterval[ 2 ]; + + +enum PitchHack +{ + P_HI, + P_NORMAL, + P_LOW +}; + +static int nextPitch = P_HI; + +BotChatterInterface::BotChatterInterface( CCSBot *me ) +{ + m_me = me; + m_statementList = NULL; + + switch( nextPitch ) + { + case P_HI: + m_pitch = RandomInt( 105, 110 ); + break; + + case P_NORMAL: + m_pitch = RandomInt( 95, 105 ); + break; + + case P_LOW: + m_pitch = RandomInt( 85, 95 ); + break; + } + + nextPitch = (nextPitch + 1) % 3; + + Reset(); +} + +//--------------------------------------------------------------------------------------------------------------- +BotChatterInterface::~BotChatterInterface() +{ + // free pending statements + BotStatement *next; + for( BotStatement *msg = m_statementList; msg; msg = next ) + { + next = msg->m_next; + delete msg; + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Reset to initial state + */ +void BotChatterInterface::Reset( void ) +{ + BotStatement *msg, *nextMsg; + + // removing pending statements - except for those about the round results + for( msg = m_statementList; msg; msg = nextMsg ) + { + nextMsg = msg->m_next; + + if (msg->GetType() != REPORT_ROUND_END) + RemoveStatement( msg ); + } + + m_seeAtLeastOneEnemy = false; + m_timeWhenSawFirstEnemy = 0.0f; + m_reportedEnemies = false; + m_requestedBombLocation = false; + + ResetRadioSilenceDuration(); + + m_needBackupInterval.Invalidate(); + m_spottedBomberInterval.Invalidate(); + m_spottedLooseBombTimer.Invalidate(); + m_heardNoiseTimer.Invalidate(); + m_scaredInterval.Invalidate(); + m_planInterval.Invalidate(); + m_encourageTimer.Invalidate(); + m_escortingHostageTimer.Invalidate(); + m_warnSniperTimer.Invalidate(); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Register a statement for speaking + */ +void BotChatterInterface::AddStatement( BotStatement *statement, bool mustAdd ) +{ + // don't add statements if bot chatter is shut off + if (GetVerbosity() == OFF) + { + delete statement; + return; + } + + // if we only want mission-critical radio chatter, ignore non-important phrases + if (GetVerbosity() == MINIMAL && !statement->IsImportant()) + { + delete statement; + return; + } + + // don't add statements if we're dead + if (!m_me->IsAlive() && !mustAdd) + { + delete statement; + return; + } + + // don't add empty statements + if (statement->m_count == 0) + { + delete statement; + return; + } + + // don't add statements that are redundant with something we're already waiting to say + BotStatement *s; + for( s=m_statementList; s; s = s->m_next ) + { + if (statement->IsRedundant( s )) + { + m_me->PrintIfWatched( "I tried to say something I'm already saying.\n" ); + delete statement; + return; + } + } + + // keep statements in order of start time + + // check list is empty + if (m_statementList == NULL) + { + statement->m_next = NULL; + statement->m_prev = NULL; + m_statementList = statement; + return; + } + + // list has at least one statement on it + + // insert into list in order + BotStatement *earlier = NULL; + for( s=m_statementList; s; s = s->m_next ) + { + if (s->GetStartTime() > statement->GetStartTime()) + break; + + earlier = s; + } + + // insert just after "earlier" + if (earlier) + { + if (earlier->m_next) + earlier->m_next->m_prev = statement; + + statement->m_next = earlier->m_next; + + earlier->m_next = statement; + statement->m_prev = earlier; + } + else + { + // insert at head + statement->m_prev = NULL; + statement->m_next = m_statementList; + m_statementList->m_prev = statement; + m_statementList = statement; + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Remove a statement + */ +void BotChatterInterface::RemoveStatement( BotStatement *statement ) +{ + if (statement->m_next) + statement->m_next->m_prev = statement->m_prev; + + if (statement->m_prev) + statement->m_prev->m_next = statement->m_next; + else + m_statementList = statement->m_next; + + delete statement; +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Track nearby enemy count and report enemy activity + */ +void BotChatterInterface::ReportEnemies( void ) +{ + if (!m_me->IsAlive()) + return; + + if (m_me->GetNearbyEnemyCount() == 0) + { + m_seeAtLeastOneEnemy = false; + m_reportedEnemies = false; + } + else if (!m_seeAtLeastOneEnemy) + { + m_seeAtLeastOneEnemy = true; + m_timeWhenSawFirstEnemy = gpGlobals->curtime; + } + + // determine whether we should report enemy activity + if (!m_reportedEnemies && m_seeAtLeastOneEnemy) + { + // request backup if we're outnumbered + if (m_me->IsOutnumbered() && NeedBackup()) + { + m_reportedEnemies = true; + return; + } + + m_me->GetChatter()->EnemySpotted(); + m_reportedEnemies = true; + } +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * Invoked when we die + */ +void BotChatterInterface::OnDeath( void ) +{ + if (IsTalking()) + { + if (m_me->GetChatter()->GetVerbosity() == BotChatterInterface::MINIMAL || + m_me->GetChatter()->GetVerbosity() == BotChatterInterface::NORMAL) + { + // we've died mid-sentance - emit a gargle of pain + const BotPhrase *pain = TheBotPhrases->GetPainPhrase(); + if (pain) + { + /* + if ( g_engfuncs.pfnPlayClientVoice && TheBotPhrases->GetOutputType( m_me->GetProfile()->GetVoiceBank() ) == BOT_CHATTER_VOICE ) + { + g_engfuncs.pfnPlayClientVoice( m_me->entindex() - 1, pain->GetSpeakable(m_me->GetProfile()->GetVoiceBank()) ); + m_me->GetChatter()->ResetRadioSilenceDuration(); + } + else + */ + { + m_me->SpeakAudio( pain->GetSpeakable( m_me->GetProfile()->GetVoiceBank() ), 0.0f, m_me->GetProfile()->GetVoicePitch() ); + } + } + } + } + + // remove all of our statements + Reset(); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Process ongoing chatter for this bot + */ +void BotChatterInterface::Update( void ) +{ + // report enemy activity + ReportEnemies(); + + // ask team to report in if we havent heard anything in awhile + if (ShouldSpeak()) + { + const float longTime = 30.0f; + if (m_me->GetEnemiesRemaining() > 0 && GetRadioSilenceDuration() > longTime) + { + ReportIn(); + } + } + + // speak if it is our turn + BotStatement *say = GetActiveStatement(); + + if (say) + { + // if our statement is active, speak it + if (say->GetOwner() == m_me) + { + if (say->Update() == false) + { + // this statement is complete - destroy it + RemoveStatement( say ); + } + } + } + + + // + // Process active statements. + // Removed expired statements, re-order statements according to their relavence and importance + // Remove redundant statements (ie: our teammates already said them) + // + const BotStatement *friendSay = GetActiveStatement(); + if (friendSay && friendSay->GetOwner() == m_me) + friendSay = NULL; + + BotStatement *nextSay; + for( say = m_statementList; say; say = nextSay ) + { + nextSay = say->m_next; + + // check statement conditions + if (!say->IsValid()) + { + RemoveStatement( say ); + continue; + } + + // don't interrupt ourselves + if (say->IsSpeaking()) + continue; + + // check for obsolete statements + if (say->IsObsolete()) + { + m_me->PrintIfWatched( "Statement obsolete - removing.\n" ); + RemoveStatement( say ); + continue; + } + + // if a teammate is saying what we were going to say, dont repeat it + if (friendSay) + { + // convert what we're about to say based on what our teammate is currently saying + say->Convert( friendSay ); + + // don't say things our teammates have just said + if (say->IsRedundant( friendSay )) + { + // thie statement is redundant - destroy it + //m_me->PrintIfWatched( "Teammate said what I was going to say - shutting up.\n" ); + m_me->PrintIfWatched( "Teammate said what I was going to say - shutting up.\n" ); + RemoveStatement( say ); + } + } + } +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Returns the statement that is being spoken, or is next to be spoken if no-one is speaking now + */ +BotStatement *BotChatterInterface::GetActiveStatement( void ) +{ + // keep track of statement waiting longest to be spoken - it is next + BotStatement *earliest = NULL; + float earlyTime = 999999999.9f; + + for( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + // ignore dead humans + if (!player->IsBot() && !player->IsAlive()) + continue; + + // ignore enemies, since we can't hear them talk + if (!m_me->InSameTeam( player )) + continue; + + CCSBot *bot = dynamic_cast<CCSBot *>(player); + + // if not a bot, fail the test + /// @todo Check if human is currently talking + if (!bot) + continue; + + for( BotStatement *say = bot->GetChatter()->m_statementList; say; say = say->m_next ) + { + // if this statement is currently being spoken, return it + if (say->IsSpeaking()) + return say; + + // keep track of statement that has been waiting longest to be spoken of anyone on our team + if (say->GetStartTime() < earlyTime) + { + earlyTime = say->GetTimestamp(); + earliest = say; + } + } + } + + // make sure it is time to start this statement + if (earliest && earliest->GetStartTime() > gpGlobals->curtime) + return NULL; + + return earliest; +} + +/** + * Return true if we speaking makes sense now + */ +bool BotChatterInterface::ShouldSpeak( void ) const +{ + // don't talk to non-existent friends + if (m_me->GetFriendsRemaining() == 0) + return false; + + // if everyone is together, no need to tell them what's going on + if (m_me->GetNearbyFriendCount() == m_me->GetFriendsRemaining()) + return false; + + return true; +} + +//--------------------------------------------------------------------------------------------------------------- +float BotChatterInterface::GetRadioSilenceDuration( void ) +{ + return m_radioSilenceInterval[ m_me->GetTeamNumber() % 2 ].GetElapsedTime(); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::ResetRadioSilenceDuration( void ) +{ + m_radioSilenceInterval[ m_me->GetTeamNumber() % 2 ].Reset(); +} + + + +//--------------------------------------------------------------------------------------------------------------- +inline const BotPhrase *GetPlacePhrase( CCSBot *me ) +{ + Place place = me->GetPlace(); + if (place != UNDEFINED_PLACE) + return TheBotPhrases->GetPlace( place ); + + return NULL; +} + + +inline void SayWhere( BotStatement *say, Place place ) +{ + say->AppendPhrase( TheBotPhrases->GetPlace( place ) ); +} + +/** + * Report enemy sightings + */ +void BotChatterInterface::EnemySpotted( void ) +{ + // NOTE: This could be a few seconds out of date (enemy is in an adjacent place) + Place place = m_me->GetEnemyPlace(); + + BotStatement *say = new BotStatement( this, REPORT_VISIBLE_ENEMIES, 10.0f ); + + // where are the enemies + say->AppendPhrase( TheBotPhrases->GetPlace( place ) ); + + // how many are there + say->AppendPhrase( BotStatement::ACCUMULATE_ENEMIES_DELAY ); + say->AppendPhrase( BotStatement::CURRENT_ENEMY_COUNT ); + say->AddCondition( BotStatement::IS_IN_COMBAT ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * If a friend warned of snipers, don't warn again for awhile + */ +void BotChatterInterface::FriendSpottedSniper( void ) +{ + m_warnSniperTimer.Start( 60.0f ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Warn of an enemy sniper + */ +void BotChatterInterface::SpottedSniper( void ) +{ + if (!m_warnSniperTimer.IsElapsed()) + { + return; + } + + if (m_me->GetFriendsRemaining() == 0) + { + // no-one to warn + return; + } + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "SniperWarning" ) ); + say->AttachMeme( new BotWarnSniperMeme() ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Clear( Place place ) +{ + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + SayWhere( say, place ); + say->AppendPhrase( TheBotPhrases->GetPhrase( "Clear" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Request enemy activity report + */ +void BotChatterInterface::ReportIn( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_REQUEST_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "RequestReport" ) ); + say->AddCondition( BotStatement::RADIO_SILENCE ); + say->AttachMeme( new BotRequestReportMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +/** + * Report our situtation + */ +void BotChatterInterface::ReportingIn( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where are we + Place place = m_me->GetPlace(); + SayWhere( say, place ); + + // what are we doing + switch( m_me->GetTask() ) + { + case CCSBot::PLANT_BOMB: + { + m_me->GetChatter()->GoingToPlantTheBomb( UNDEFINED_PLACE ); + break; + } + + case CCSBot::DEFUSE_BOMB: + { + m_me->GetChatter()->Say( "DefusingBomb" ); + break; + } + + case CCSBot::GUARD_LOOSE_BOMB: + { + if (TheCSBots()->GetLooseBomb()) + { + say->AppendPhrase( TheBotPhrases->GetPhrase( "GuardingLooseBomb" ) ); + say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, TheCSBots()->GetLooseBomb()->GetAbsOrigin() ) ); + } + break; + } + + case CCSBot::GUARD_HOSTAGES: + { + m_me->GetChatter()->GuardingHostages( UNDEFINED_PLACE, !m_me->IsAtHidingSpot() ); + break; + } + + case CCSBot::GUARD_HOSTAGE_RESCUE_ZONE: + { + m_me->GetChatter()->GuardingHostageEscapeZone( !m_me->IsAtHidingSpot() ); + break; + } + + case CCSBot::COLLECT_HOSTAGES: + { + break; + } + + case CCSBot::RESCUE_HOSTAGES: + { + m_me->GetChatter()->EscortingHostages(); + break; + } + + case CCSBot::GUARD_VIP_ESCAPE_ZONE: + { + break; + } + + } + + + // what do we see + if (m_me->IsAttacking()) + { + if (m_me->IsOutnumbered()) + { + // in trouble in a firefight + say->AppendPhrase( TheBotPhrases->GetPhrase( "Help" ) ); + say->AttachMeme( new BotHelpMeme( place ) ); + } + else + { + // battling enemies + say->AppendPhrase( TheBotPhrases->GetPhrase( "InCombat" ) ); + } + } + else + { + // not in combat, start our report a little later + say->SetStartTime( gpGlobals->curtime + 2.0f ); + + const float recentTime = 10.0f; + if (m_me->GetEnemyDeathTimestamp() < recentTime && m_me->GetEnemyDeathTimestamp() >= m_me->GetTimeSinceLastSawEnemy() + 0.5f) + { + // recently saw an enemy die + say->AppendPhrase( TheBotPhrases->GetPhrase( "EnemyDown" ) ); + } + else if (m_me->GetTimeSinceLastSawEnemy() < recentTime) + { + // recently saw an enemy + say->AppendPhrase( TheBotPhrases->GetPhrase( "EnemySpotted" ) ); + } + else + { + // haven't seen enemies + say->AppendPhrase( TheBotPhrases->GetPhrase( "Clear" ) ); + } + } + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +bool BotChatterInterface::NeedBackup( void ) +{ + const float minRequestInterval = 10.0f; + if (m_needBackupInterval.IsLessThen( minRequestInterval )) + return false; + + m_needBackupInterval.Reset(); + + if (m_me->GetFriendsRemaining() == 0) + { + // we're all alone... + Scared(); + return true; + } + else + { + // ask friends for help + BotStatement *say = new BotStatement( this, REPORT_REQUEST_HELP, 10.0f ); + + // where are we + Place place = m_me->GetPlace(); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "Help" ) ); + say->AttachMeme( new BotHelpMeme( place ) ); + + AddStatement( say ); + } + + return true; +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::PinnedDown( void ) +{ + // this is a form of "need backup" + const float minRequestInterval = 10.0f; + if (m_needBackupInterval.IsLessThen( minRequestInterval )) + return; + + m_needBackupInterval.Reset(); + + BotStatement *say = new BotStatement( this, REPORT_REQUEST_HELP, 10.0f ); + + // where are we + Place place = m_me->GetPlace(); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "PinnedDown" ) ); + say->AttachMeme( new BotHelpMeme( place ) ); + say->AddCondition( BotStatement::IS_IN_COMBAT ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +/** + * If a friend said that they heard something, we don't want to say something similar + * for a while. + */ +void BotChatterInterface::FriendHeardNoise( void ) +{ + m_heardNoiseTimer.Start( 20.0f ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HeardNoise( const Vector &pos ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + if (m_heardNoiseTimer.IsElapsed()) + { + // throttle frequency + m_heardNoiseTimer.Start( 20.0f ); + + // make rare, since many teammates may try to say this + if (RandomFloat( 0, 100 ) < 33) + { + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 5.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HeardNoise" ) ); + say->SetPlace( TheNavMesh->GetPlace( pos ) ); + say->AttachMeme( new BotHeardNoiseMeme() ); + + AddStatement( say ); + } + } +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::KilledMyEnemy( int victimID ) +{ + // only report if we killed the last enemy in the area + if (m_me->GetNearbyEnemyCount() <= 1) + return; + + BotStatement *say = new BotStatement( this, REPORT_ENEMY_ACTION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "KilledMyEnemy" ) ); + say->SetSubject( victimID ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::EnemiesRemaining( void ) +{ + // only report if we killed the last enemy in the area + if (m_me->GetNearbyEnemyCount() > 1) + return; + + BotStatement *say = new BotStatement( this, REPORT_ENEMIES_REMAINING, 5.0f ); + say->AppendPhrase( BotStatement::REMAINING_ENEMY_COUNT ); + say->SetStartTime( gpGlobals->curtime + RandomFloat( 2.0f, 4.0f ) ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Affirmative( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_ACKNOWLEDGE, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "Affirmative" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Negative( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_ACKNOWLEDGE, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "Negative" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GoingToPlantTheBomb( Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + BotStatement *say = new BotStatement( this, REPORT_CRITICAL_EVENT, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "GoingToPlantBomb" ) ); + say->SetPlace( place ); + say->AttachMeme( new BotFollowMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::PlantingTheBomb( Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_CRITICAL_EVENT, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "PlantingBomb" ) ); + say->SetPlace( place ); + + Vector myOrigin = GetCentroid( m_me ); + say->AttachMeme( new BotDefendHereMeme( myOrigin ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::TheyPickedUpTheBomb( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + // if we already know the bomb is not loose, this is old news + if (!m_me->GetGameState()->IsBombLoose()) + return; + + // update our gamestate - use our own position for now + const Vector &myOrigin = GetCentroid( m_me ); + m_me->GetGameState()->UpdateBomber( myOrigin ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "TheyPickedUpTheBomb" ) ); + + say->AttachMeme( new BotBombStatusMeme( CSGameState::MOVING, myOrigin ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::SpottedBomber( CBasePlayer *bomber ) +{ + const Vector &bomberOrigin = GetCentroid( bomber ); + + if (m_me->GetGameState()->IsBombMoving()) + { + // if we knew where the bomber was, this is old news + const Vector *bomberPos = m_me->GetGameState()->GetBombPosition(); + const float closeRangeSq = 1000.0f * 1000.0f; + if (bomberPos && (bomberOrigin - *bomberPos).LengthSqr() < closeRangeSq) + return; + } + + // update our gamestate + m_me->GetGameState()->UpdateBomber( bomberOrigin ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where is the bomber + Place place = TheNavMesh->GetPlace( bomberOrigin ); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "SpottedBomber" ) ); + + say->SetSubject( bomber->entindex() ); + + //say->AttachMeme( new BotHelpMeme( place ) ); + say->AttachMeme( new BotBombStatusMeme( CSGameState::MOVING, bomberOrigin ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::SpottedLooseBomb( CBaseEntity *bomb ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + // if we already know the bomb is loose, this is old news + if (m_me->GetGameState()->IsBombLoose()) + return; + + // update our gamestate + m_me->GetGameState()->UpdateLooseBomb( bomb->GetAbsOrigin() ); + + if (m_spottedLooseBombTimer.IsElapsed()) + { + // throttle frequency + m_spottedLooseBombTimer.Start( 10.0f ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where is the bomb + Place place = TheNavMesh->GetPlace( bomb->GetAbsOrigin() ); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "SpottedLooseBomb" ) ); + + if (TheCSBots()->GetLooseBomb()) + say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, bomb->GetAbsOrigin() ) ); + + AddStatement( say ); + } +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingLooseBomb( CBaseEntity *bomb ) +{ + // if we already know the bomb is loose, this is old news +// if (m_me->GetGameState()->IsBombLoose()) +// return; + + if (TheCSBots()->IsRoundOver() || !bomb) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + // update our gamestate + m_me->GetGameState()->UpdateLooseBomb( bomb->GetAbsOrigin() ); + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + // where is the bomb + Place place = TheNavMesh->GetPlace( bomb->GetAbsOrigin() ); + SayWhere( say, place ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "GuardingLooseBomb" ) ); + + if (TheCSBots()->GetLooseBomb()) + say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, bomb->GetAbsOrigin() ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::RequestBombLocation( void ) +{ + // only ask once per round + if (m_requestedBombLocation) + return; + + m_requestedBombLocation = true; + + // tell our teammates + BotStatement *say = new BotStatement( this, REPORT_REQUEST_INFORMATION, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "WhereIsTheBomb" ) ); + + say->AttachMeme( new BotWhereBombMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::BombsiteClear( int zoneIndex ) +{ + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( zoneIndex ); + if (zone == NULL) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f ); + + SayWhere( say, TheNavMesh->GetPlace( zone->m_center ) ); + say->AppendPhrase( TheBotPhrases->GetPhrase( "BombsiteClear" ) ); + + say->AttachMeme( new BotBombsiteStatusMeme( zoneIndex, BotBombsiteStatusMeme::CLEAR ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::FoundPlantedBomb( int zoneIndex ) +{ + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( zoneIndex ); + if (zone == NULL) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "PlantedBombPlace" ) ); + say->SetPlace( TheNavMesh->GetPlace( zone->m_center ) ); + + say->AttachMeme( new BotBombsiteStatusMeme( zoneIndex, BotBombsiteStatusMeme::PLANTED ) ); + + AddStatement( say ); +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Scared( void ) +{ + const float minInterval = 10.0f; + if (m_scaredInterval.IsLessThen( minInterval )) + return; + + m_scaredInterval.Reset(); + + BotStatement *say = new BotStatement( this, REPORT_EMOTE, 1.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "ScaredEmote" ) ); + say->AddCondition( BotStatement::IS_IN_COMBAT ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::CelebrateWin( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_EMOTE, 15.0f ); + + // wait a bit before speaking + say->SetStartTime( gpGlobals->curtime + RandomFloat( 2.0f, 5.0f ) ); + + const float quickRound = 45.0f; + + if (m_me->GetFriendsRemaining() == 0) + { + // we were the last man standing + if (TheCSBots()->GetElapsedRoundTime() < quickRound) + say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRoundQuickly" ) ); + else if (RandomFloat( 0.0f, 100.0f ) < 33.3f) + say->AppendPhrase( TheBotPhrases->GetPhrase( "LastManStanding" ) ); + } + else + { + if (TheCSBots()->GetElapsedRoundTime() < quickRound) + { + if (RandomFloat( 0.0f, 100.0f ) < 33.3f) + say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRoundQuickly" ) ); + } + else if (RandomFloat( 0.0f, 100.0f ) < 10.0f) + { + say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRound" ) ); + } + } + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::AnnouncePlan( const char *phraseName, Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_MY_PLAN, 10.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( phraseName ) ); + say->SetPlace( place ); + + // wait at least a short time after round start + say->SetStartTime( TheCSBots()->GetRoundStartTime() + RandomFloat( 2.0, 3.0f ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingBombsite( Place place ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + AnnouncePlan( "GoingToDefendBombsite", place ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingHostages( Place place, bool isPlan ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + if (isPlan) + AnnouncePlan( "GoingToGuardHostages", place ); + else + Say( "GuardingHostages" ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::GuardingHostageEscapeZone( bool isPlan ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + const float minInterval = 20.0f; + if (m_planInterval.IsLessThen( minInterval )) + return; + + m_planInterval.Reset(); + + if (isPlan) + AnnouncePlan( "GoingToGuardHostageEscapeZone", UNDEFINED_PLACE ); + else + Say( "GuardingHostageEscapeZone" ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HostagesBeingTaken( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HostagesBeingTaken" ) ); + say->AttachMeme( new BotHostageBeingTakenMeme() ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HostagesTaken( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HostagesTaken" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::TalkingToHostages( void ) +{ +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::EscortingHostages( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + if (m_escortingHostageTimer.IsElapsed()) + { + // throttle frequency + m_escortingHostageTimer.Start( 10.0f ); + + BotStatement *say = new BotStatement( this, REPORT_MY_PLAN, 5.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "EscortingHostages" ) ); + + AddStatement( say ); + } +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::HostageDown( void ) +{ + if (TheCSBots()->IsRoundOver()) + return; + + BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "HostageDown" ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::Encourage( const char *phraseName, float repeatInterval, float lifetime ) +{ + if (m_encourageTimer.IsElapsed()) + { + Say( phraseName, lifetime ); + m_encourageTimer.Start( repeatInterval ); + } +} + + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::KilledFriend( void ) +{ + BotStatement *say = new BotStatement( this, REPORT_KILLED_FRIEND, 2.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "KilledFriend" ) ); + + // give them time to react + say->SetStartTime( gpGlobals->curtime + RandomFloat( 0.5f, 1.0f ) ); + + AddStatement( say ); +} + +//--------------------------------------------------------------------------------------------------------------- +void BotChatterInterface::FriendlyFire( void ) +{ + if ( !friendlyfire.GetBool() ) + return; + + BotStatement *say = new BotStatement( this, REPORT_FRIENDLY_FIRE, 1.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "FriendlyFire" ) ); + + // give them time to react + say->SetStartTime( gpGlobals->curtime + RandomFloat( 0.3f, 0.5f ) ); + + AddStatement( say ); +} + + diff --git a/game/server/cstrike/bot/cs_bot_chatter.h b/game/server/cstrike/bot/cs_bot_chatter.h new file mode 100644 index 0000000..47ba802 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_chatter.h @@ -0,0 +1,656 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Bot radio chatter system +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef CS_BOT_CHATTER_H +#define CS_BOT_CHATTER_H + +#pragma warning( disable : 4786 ) // long STL names get truncated in browse info. + +#include "nav_mesh.h" +#include "cs_gamestate.h" + +class CCSBot; +class BotChatterInterface; + +#define MAX_PLACES_PER_MAP 64 + +typedef unsigned int PlaceCriteria; + +typedef unsigned int CountCriteria; +#define UNDEFINED_COUNT 0xFFFF +#define COUNT_CURRENT_ENEMIES 0xFF // use the number of enemies we see right when we speak +#define COUNT_MANY 4 // equal to or greater than this is "many" + +#define UNDEFINED_SUBJECT (-1) + +/// @todo Make Place a class with member fuctions for this +const Vector *GetRandomSpotAtPlace( Place place ); + +//---------------------------------------------------------------------------------------------------- +/** + * A meme is a unit information that bots use to + * transmit information to each other via the radio + */ +class BotMeme +{ +public: + void Transmit( CCSBot *sender ) const; ///< transmit meme to other bots + // It is a best practice to always have a virtual destructor in an interface + // class. Otherwise if the derived classes have destructors they will not be + // called. + virtual ~BotMeme() {} + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const = 0; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotHelpMeme : public BotMeme +{ +public: + BotHelpMeme( Place place = UNDEFINED_PLACE ) + { + m_place = place; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + Place m_place; ///< where the help is needed +}; + +//---------------------------------------------------------------------------------------------------- +class BotBombsiteStatusMeme : public BotMeme +{ +public: + enum StatusType { CLEAR, PLANTED }; + + BotBombsiteStatusMeme( int zoneIndex, StatusType status ) + { + m_zoneIndex = zoneIndex; + m_status = status; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + int m_zoneIndex; ///< the bombsite + StatusType m_status; ///< whether it is cleared or the bomb is there (planted) +}; + +//---------------------------------------------------------------------------------------------------- +class BotBombStatusMeme : public BotMeme +{ +public: + BotBombStatusMeme( CSGameState::BombState state, const Vector &pos ) + { + m_state = state; + m_pos = pos; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + CSGameState::BombState m_state; + Vector m_pos; +}; + +//---------------------------------------------------------------------------------------------------- +class BotFollowMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotDefendHereMeme : public BotMeme +{ +public: + BotDefendHereMeme( const Vector &pos ) + { + m_pos = pos; + } + + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme + +private: + Vector m_pos; +}; + +//---------------------------------------------------------------------------------------------------- +class BotWhereBombMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotRequestReportMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotAllHostagesGoneMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotHostageBeingTakenMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotHeardNoiseMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +class BotWarnSniperMeme : public BotMeme +{ +public: + virtual void Interpret( CCSBot *sender, CCSBot *receiver ) const; ///< cause the given bot to act on this meme +}; + +//---------------------------------------------------------------------------------------------------- +enum BotStatementType +{ + REPORT_VISIBLE_ENEMIES, + REPORT_ENEMY_ACTION, + REPORT_MY_CURRENT_TASK, + REPORT_MY_INTENTION, + REPORT_CRITICAL_EVENT, + REPORT_REQUEST_HELP, + REPORT_REQUEST_INFORMATION, + REPORT_ROUND_END, + REPORT_MY_PLAN, + REPORT_INFORMATION, + REPORT_EMOTE, + REPORT_ACKNOWLEDGE, ///< affirmative or negative + REPORT_ENEMIES_REMAINING, + REPORT_FRIENDLY_FIRE, + REPORT_KILLED_FRIEND, + REPORT_ENEMY_LOST, + + NUM_BOT_STATEMENT_TYPES +}; + +//---------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------- +/** + * BotSpeakables are the smallest unit of bot chatter. + * They represent a specific wav file of a phrase, and the criteria for which it is useful + */ +class BotSpeakable +{ +public: + BotSpeakable(); + ~BotSpeakable(); + char *m_phrase; + float m_duration; + PlaceCriteria m_place; + CountCriteria m_count; +}; +typedef CUtlVector< BotSpeakable * > BotSpeakableVector; +typedef CUtlVector< BotSpeakableVector * > BotVoiceBankVector; + + +//---------------------------------------------------------------------------------------------------- +/** + * The BotPhrase class is a collection of Speakables associated with a name, ID, and criteria + */ +class BotPhrase +{ +public: + char *GetSpeakable( int bankIndex, float *duration = NULL ) const; ///< return a random speakable and its duration in seconds that meets the current criteria + + // NOTE: Criteria must be set just before the GetSpeakable() call, since they are shared among all bots + void ClearCriteria( void ) const; + void SetPlaceCriteria( PlaceCriteria place ) const; ///< all returned phrases must have this place criteria + void SetCountCriteria( CountCriteria count ) const; ///< all returned phrases must have this count criteria + + const char *GetName( void ) const { return m_name; } + const unsigned int GetPlace( void ) const { return m_place; } + RadioType GetRadioEquivalent( void ) const { return m_radioEvent; } ///< return equivalent "standard radio" event + bool IsImportant( void ) const { return m_isImportant; } ///< return true if this phrase is part of an important statement + + bool IsPlace( void ) const { return m_isPlace; } + + void Randomize( void ); ///< randomly shuffle the speakable order + +private: + friend class BotPhraseManager; + BotPhrase( bool isPlace ); + ~BotPhrase(); + + char *m_name; + Place m_place; + bool m_isPlace; ///< true if this is a Place phrase + RadioType m_radioEvent; ///< equivalent radio event + bool m_isImportant; ///< mission-critical statement + + mutable BotVoiceBankVector m_voiceBank; ///< array of voice banks (arrays of speakables) + CUtlVector< int > m_count; ///< number of speakables + mutable CUtlVector< int > m_index; ///< index of next speakable to return + int m_numVoiceBanks; ///< number of voice banks that have been initialized + void InitVoiceBank( int bankIndex ); ///< sets up the vector of voice banks for the first bankIndex voice banks + + mutable PlaceCriteria m_placeCriteria; + mutable CountCriteria m_countCriteria; +}; +typedef CUtlVector<BotPhrase *> BotPhraseList; + +inline void BotPhrase::ClearCriteria( void ) const +{ + m_placeCriteria = ANY_PLACE; + m_countCriteria = UNDEFINED_COUNT; +} + +inline void BotPhrase::SetPlaceCriteria( PlaceCriteria place ) const +{ + m_placeCriteria = place; +} + +inline void BotPhrase::SetCountCriteria( CountCriteria count ) const +{ + m_countCriteria = count; +} + +enum BotChatterOutputType +{ + BOT_CHATTER_RADIO, + BOT_CHATTER_VOICE +}; +typedef CUtlVector<BotChatterOutputType> BotOutputList; + +//---------------------------------------------------------------------------------------------------- +/** + * The BotPhraseManager is a singleton that provides an interface to all BotPhrase collections + */ +class BotPhraseManager +{ +public: + BotPhraseManager( void ); + ~BotPhraseManager(); + + bool Initialize( const char *filename, int bankIndex ); ///< initialize phrase system from database file for a specific voice bank (0 is the default voice bank) + + void OnRoundRestart( void ); ///< invoked when round resets + void OnMapChange( void ); ///< invoked when map changes + void Reset( void ); + + const BotPhrase *GetPhrase( const char *name ) const; ///< given a name, return the associated phrase collection + const BotPhrase *GetPainPhrase( void ) const { return m_painPhrase; } ///< optimization, replaces a static pointer to the phrase + const BotPhrase *GetAgreeWithPlanPhrase( void ) const { return m_agreeWithPlanPhrase; } ///< optimization, replaces a static pointer to the phrase + + const BotPhrase *GetPlace( const char *name ) const; ///< given a name, return the associated Place phrase collection + const BotPhrase *GetPlace( unsigned int id ) const; ///< given an id, return the associated Place phrase collection + + const BotPhraseList *GetPlaceList( void ) const { return &m_placeList; } + + float GetPlaceStatementInterval( Place where ) const; ///< return time last statement of given type was emitted by a teammate for the given place + void ResetPlaceStatementInterval( Place where ); ///< set time of last statement of given type was emitted by a teammate for the given place + + BotChatterOutputType GetOutputType( int voiceBank ) const; + +private: + BotPhraseList m_list; ///< master list of all phrase collections + BotPhraseList m_placeList; ///< master list of all Place phrases + + BotOutputList m_output; + + const BotPhrase *m_painPhrase; + const BotPhrase *m_agreeWithPlanPhrase; + + struct PlaceTimeInfo + { + Place placeID; + IntervalTimer timer; + }; + mutable PlaceTimeInfo m_placeStatementHistory[ MAX_PLACES_PER_MAP ]; + mutable int m_placeCount; + int FindPlaceIndex( Place where ) const; +}; + +inline int BotPhraseManager::FindPlaceIndex( Place where ) const +{ + for( int i=0; i<m_placeCount; ++i ) + if (m_placeStatementHistory[i].placeID == where) + return i; + + // no such place - allocate it + if (m_placeCount < MAX_PLACES_PER_MAP) + { + m_placeStatementHistory[ ++m_placeCount ].placeID = where; + m_placeStatementHistory[ ++m_placeCount ].timer.Invalidate(); + return m_placeCount-1; + } + + // place directory is full + return -1; +} + +/** + * Return time last statement of given type was emitted by a teammate for the given place + */ +inline float BotPhraseManager::GetPlaceStatementInterval( Place place ) const +{ + int index = FindPlaceIndex( place ); + + if (index < 0) + return 999999.9f; + + if (index >= m_placeCount) + return 999999.9f; + + return m_placeStatementHistory[ index ].timer.GetElapsedTime(); +} + +/** + * Set time of last statement of given type was emitted by a teammate for the given place + */ +inline void BotPhraseManager::ResetPlaceStatementInterval( Place place ) +{ + int index = FindPlaceIndex( place ); + + if (index < 0) + return; + + if (index >= m_placeCount) + return; + + // update entry + m_placeStatementHistory[ index ].timer.Reset(); +} + +extern BotPhraseManager *TheBotPhrases; + + + +//---------------------------------------------------------------------------------------------------- +/** + * Statements are meaningful collections of phrases + */ +class BotStatement +{ +public: + BotStatement( BotChatterInterface *chatter, BotStatementType type, float expireDuration ); + ~BotStatement(); + + BotChatterInterface *GetChatter( void ) const { return m_chatter; } + CCSBot *GetOwner( void ) const; + + BotStatementType GetType( void ) const { return m_type; } ///< return the type of statement this is + bool IsImportant( void ) const; ///< return true if this statement is "important" and not personality chatter + + bool HasSubject( void ) const { return (m_subject == UNDEFINED_SUBJECT) ? false : true; } + void SetSubject( int playerID ) { m_subject = playerID; } ///< who this statement is about + int GetSubject( void ) const { return m_subject; } ///< who this statement is about + + bool HasPlace( void ) const { return (GetPlace()) ? true : false; } + Place GetPlace( void ) const; ///< if this statement refers to a specific place, return that place + void SetPlace( Place where ) { m_place = where; } ///< explicitly set place + + bool HasCount( void ) const; ///< return true if this statement has an associated count + + bool IsRedundant( const BotStatement *say ) const; ///< return true if this statement is the same as the given one + bool IsObsolete( void ) const; ///< return true if this statement is no longer appropriate to say + void Convert( const BotStatement *say ); ///< possibly change what were going to say base on what teammate is saying + + void AppendPhrase( const BotPhrase *phrase ); + + void SetStartTime( float timestamp ) { m_startTime = timestamp; } ///< define the earliest time this statement can be spoken + float GetStartTime( void ) const { return m_startTime; } + + enum ConditionType + { + IS_IN_COMBAT, + RADIO_SILENCE, + ENEMIES_REMAINING, + + NUM_CONDITIONS + }; + + void AddCondition( ConditionType condition ); ///< conditions must be true for the statement to be spoken + bool IsValid( void ) const; ///< verify all attached conditions + + enum ContextType + { + CURRENT_ENEMY_COUNT, + REMAINING_ENEMY_COUNT, + SHORT_DELAY, + LONG_DELAY, + ACCUMULATE_ENEMIES_DELAY + }; + void AppendPhrase( ContextType contextPhrase ); ///< special phrases that depend on the context + + bool Update( void ); ///< emit statement over time, return false if statement is done + bool IsSpeaking( void ) const { return m_isSpeaking; } ///< return true if this statement is currently being spoken + float GetTimestamp( void ) const { return m_timestamp; } ///< get time statement was created (but not necessarily started talking) + + void AttachMeme( BotMeme *meme ); ///< attach a meme to this statement, to be transmitted to other friendly bots when spoken + +private: + friend class BotChatterInterface; + + BotChatterInterface *m_chatter; ///< the chatter system this statement is part of + + BotStatement *m_next, *m_prev; ///< linked list hooks + + BotStatementType m_type; ///< what kind of statement this is + int m_subject; ///< who this subject is about + Place m_place; ///< explicit place - note some phrases have implicit places as well + BotMeme *m_meme; ///< a statement can only have a single meme for now + + float m_timestamp; ///< time when message was created + float m_startTime; ///< the earliest time this statement can be spoken + float m_expireTime; ///< time when this statement is no longer valid + float m_speakTimestamp; ///< time when message began being spoken + bool m_isSpeaking; ///< true if this statement is current being spoken + + float m_nextTime; ///< time for next phrase to begin + + enum { MAX_BOT_PHRASES = 4 }; + struct + { + bool isPhrase; + union + { + const BotPhrase *phrase; + ContextType context; + }; + } + m_statement[ MAX_BOT_PHRASES ]; + + enum { MAX_BOT_CONDITIONS = 4 }; + ConditionType m_condition[ MAX_BOT_CONDITIONS ]; ///< conditions that must be true for the statement to be said + int m_conditionCount; + + int m_index; ///< m_index refers to the phrase currently being spoken, or -1 if we havent started yet + int m_count; +}; + +//---------------------------------------------------------------------------------------------------- +/** + * This class defines the interface to the bot radio chatter system + */ +class BotChatterInterface +{ +public: + BotChatterInterface( CCSBot *me ); + ~BotChatterInterface( ); + + void Reset( void ); ///< reset to initial state + void Update( void ); ///< process ongoing chatter + + /// invoked when event occurs in the game (some events have NULL entities) + void OnDeath( void ); ///< invoked when we die + + enum VerbosityType + { + NORMAL, ///< full chatter + MINIMAL, ///< only scenario-critical events + RADIO, ///< use the standard radio instead + OFF ///< no chatter at all + }; + VerbosityType GetVerbosity( void ) const; ///< return our current level of verbosity + + CCSBot *GetOwner( void ) const { return m_me; } + + bool IsTalking( void ) const; ///< return true if we are currently talking + float GetRadioSilenceDuration( void ); ///< return time since any teammate said anything + void ResetRadioSilenceDuration( void ); + + enum { MUST_ADD = 1 }; + void AddStatement( BotStatement *statement, bool mustAdd = false ); ///< register a statement for speaking + void RemoveStatement( BotStatement *statement ); ///< remove a statement + + BotStatement *GetActiveStatement( void ); ///< returns the statement that is being spoken, or is next to be spoken if no-one is speaking now + BotStatement *GetStatement( void ) const; ///< returns our current statement, or NULL if we aren't speaking + + int GetPitch( void ) const { return m_pitch; } + + + //-- things the bots can say --------------------------------------------------------------------- + void Say( const char *phraseName, float lifetime = 3.0f, float delay = 0.0f ); + + void AnnouncePlan( const char *phraseName, Place where ); + void Affirmative( void ); + void Negative( void ); + + void EnemySpotted( void ); ///< report enemy sightings + void KilledMyEnemy( int victimID ); + void EnemiesRemaining( void ); + + void SpottedSniper( void ); + void FriendSpottedSniper( void ); + + void Clear( Place where ); + + void ReportIn( void ); ///< ask for current situation + void ReportingIn( void ); ///< report current situation + + bool NeedBackup( void ); + void PinnedDown( void ); + void Scared( void ); + void HeardNoise( const Vector &pos ); + void FriendHeardNoise( void ); + + void TheyPickedUpTheBomb( void ); + void GoingToPlantTheBomb( Place where ); + void BombsiteClear( int zoneIndex ); + void FoundPlantedBomb( int zoneIndex ); + void PlantingTheBomb( Place where ); + void SpottedBomber( CBasePlayer *bomber ); + void SpottedLooseBomb( CBaseEntity *bomb ); + void GuardingLooseBomb( CBaseEntity *bomb ); + void RequestBombLocation( void ); + + #define IS_PLAN true + void GuardingHostages( Place where, bool isPlan = false ); + void GuardingHostageEscapeZone( bool isPlan = false ); + void HostagesBeingTaken( void ); + void HostagesTaken( void ); + void TalkingToHostages( void ); + void EscortingHostages( void ); + void HostageDown( void ); + void GuardingBombsite( Place where ); + + void CelebrateWin( void ); + + void Encourage( const char *phraseName, float repeatInterval = 10.0f, float lifetime = 3.0f ); ///< "encourage" the player to do the scenario + + void KilledFriend( void ); + void FriendlyFire( void ); + + bool SeesAtLeastOneEnemy( void ) const { return m_seeAtLeastOneEnemy; } + +private: + BotStatement *m_statementList; ///< list of all active/pending messages for this bot + + void ReportEnemies( void ); ///< track nearby enemy count and generate enemy activity statements + bool ShouldSpeak( void ) const; ///< return true if we speaking makes sense now + + CCSBot *m_me; ///< the bot this chatter is for + + bool m_seeAtLeastOneEnemy; + float m_timeWhenSawFirstEnemy; + bool m_reportedEnemies; + bool m_requestedBombLocation; ///< true if we already asked where the bomb has been planted + + int m_pitch; + + static IntervalTimer m_radioSilenceInterval[ 2 ]; ///< one timer for each team + + IntervalTimer m_needBackupInterval; + IntervalTimer m_spottedBomberInterval; + IntervalTimer m_scaredInterval; + IntervalTimer m_planInterval; + CountdownTimer m_spottedLooseBombTimer; + CountdownTimer m_heardNoiseTimer; + CountdownTimer m_escortingHostageTimer; + CountdownTimer m_warnSniperTimer; + static CountdownTimer m_encourageTimer; ///< timer to know when we can "encourage" the human player again - shared by all bots +}; + +inline BotChatterInterface::VerbosityType BotChatterInterface::GetVerbosity( void ) const +{ + const char *string = cv_bot_chatter.GetString(); + + if (string == NULL) + return NORMAL; + + if (string[0] == 'm' || string[0] == 'M') + return MINIMAL; + + if (string[0] == 'r' || string[0] == 'R') + return RADIO; + + if (string[0] == 'o' || string[0] == 'O') + return OFF; + + return NORMAL; +} + + +inline bool BotChatterInterface::IsTalking( void ) const +{ + if (m_statementList) + return m_statementList->IsSpeaking(); + + return false; +} + +inline BotStatement *BotChatterInterface::GetStatement( void ) const +{ + return m_statementList; +} + + +inline void BotChatterInterface::Say( const char *phraseName, float lifetime, float delay ) +{ + BotStatement *say = new BotStatement( this, REPORT_MY_INTENTION, lifetime ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( phraseName ) ); + + if (delay > 0.0f) + say->SetStartTime( gpGlobals->curtime + delay ); + + AddStatement( say ); +} + + + + +#endif // CS_BOT_CHATTER_H diff --git a/game/server/cstrike/bot/cs_bot_event.cpp b/game/server/cstrike/bot/cs_bot_event.cpp new file mode 100644 index 0000000..24bdae4 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event.cpp @@ -0,0 +1,427 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Checks if the bot can hear the event + */ +void CCSBot::OnAudibleEvent( IGameEvent *event, CBasePlayer *player, float range, PriorityType priority, bool isHostile, bool isFootstep, const Vector *actualOrigin ) +{ + /// @todo Listen to non-player sounds + if (player == NULL) + return; + + // don't pay attention to noise that friends make + if (!IsEnemy( player )) + return; + + Vector playerOrigin = GetCentroid( player ); + Vector myOrigin = GetCentroid( this ); + + // If the event occurs far from the triggering player, it may override the origin + if ( actualOrigin ) + { + playerOrigin = *actualOrigin; + } + + // check if noise is close enough for us to hear + const Vector *newNoisePosition = &playerOrigin; + float newNoiseDist = (myOrigin - *newNoisePosition).Length(); + if (newNoiseDist < range) + { + // we heard the sound + if ((IsLocalPlayerWatchingMe() && cv_bot_debug.GetInt() == 3) || cv_bot_debug.GetInt() == 4) + { + PrintIfWatched( "Heard noise (%s from %s, pri %s, time %3.1f)\n", + (FStrEq( "weapon_fire", event->GetName() )) ? "Weapon fire " : "", + (player) ? player->GetPlayerName() : "NULL", + (priority == PRIORITY_HIGH) ? "HIGH" : ((priority == PRIORITY_MEDIUM) ? "MEDIUM" : "LOW"), + gpGlobals->curtime ); + } + + // should we pay attention to it + // if noise timestamp is zero, there is no prior noise + if (m_noiseTimestamp > 0.0f) + { + // only overwrite recent sound if we are louder (closer), or more important - if old noise was long ago, its faded + const float shortTermMemoryTime = 3.0f; + if (gpGlobals->curtime - m_noiseTimestamp < shortTermMemoryTime) + { + // prior noise is more important - ignore new one + if (priority < m_noisePriority) + return; + + float oldNoiseDist = (myOrigin - m_noisePosition).Length(); + if (newNoiseDist >= oldNoiseDist) + return; + } + } + + // find the area in which the noise occured + /// @todo Better handle when noise occurs off the nav mesh + /// @todo Make sure noise area is not through a wall or ceiling from source of noise + /// @todo Change GetNavTravelTime to better deal with NULL destination areas + CNavArea *noiseArea = TheNavMesh->GetNearestNavArea( *newNoisePosition ); + if (noiseArea == NULL) + { + PrintIfWatched( " *** Noise occurred off the nav mesh - ignoring!\n" ); + return; + } + + m_noiseArea = noiseArea; + + // remember noise priority + m_noisePriority = priority; + + // randomize noise position in the area a bit - hearing isn't very accurate + // the closer the noise is, the more accurate our placement + /// @todo Make sure not to pick a position on the opposite side of ourselves. + const float maxErrorRadius = 400.0f; + const float maxHearingRange = 2000.0f; + float errorRadius = maxErrorRadius * newNoiseDist/maxHearingRange; + + m_noisePosition.x = newNoisePosition->x + RandomFloat( -errorRadius, errorRadius ); + m_noisePosition.y = newNoisePosition->y + RandomFloat( -errorRadius, errorRadius ); + + // note the *travel distance* to the noise + m_noiseTravelDistance = GetTravelDistanceToPlayer( (CCSPlayer *)player ); + + // make sure noise position remains in the same area + m_noiseArea->GetClosestPointOnArea( m_noisePosition, &m_noisePosition ); + + // note when we heard the noise + m_noiseTimestamp = gpGlobals->curtime; + + // if we hear a nearby enemy, become alert + const float nearbyNoiseRange = 1000.0f; + if (m_noiseTravelDistance < nearbyNoiseRange && m_noiseTravelDistance > 0.0f) + { + BecomeAlert(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnHEGrenadeDetonate( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 99999.0f, PRIORITY_HIGH, true ); // hegrenade_detonate +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnFlashbangDetonate( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1000.0f, PRIORITY_LOW, true ); // flashbang_detonate +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnSmokeGrenadeDetonate( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1000.0f, PRIORITY_LOW, true ); // smokegrenade_detonate +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnGrenadeBounce( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 500.0f, PRIORITY_LOW, true ); // grenade_bounce +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBulletImpact( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // Construct an origin for the sound, since it can be far from the originating player + Vector actualOrigin; + actualOrigin.x = event->GetFloat( "x", 0.0f ); + actualOrigin.y = event->GetFloat( "y", 0.0f ); + actualOrigin.z = event->GetFloat( "z", 0.0f ); + + /// @todo Ignoring bullet impact events for now - we dont want bots to look directly at them! + //OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, true, false, &actualOrigin ); // bullet_impact +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBreakProp( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, true ); // break_prop +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBreakBreakable( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, true ); // break_glass +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnDoorMoving( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_MEDIUM, false ); // door_moving +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnHostageFollows( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // player_follows needs a player + if (player == NULL) + return; + + // don't pay attention to noise that friends make + if (!IsEnemy( player )) + return; + + Vector playerOrigin = GetCentroid( player ); + Vector myOrigin = GetCentroid( this ); + const float range = 1200.0f; + + // this is here so T's not only act on the noise, but look at it, too + if (GetTeamNumber() == TEAM_TERRORIST) + { + // make sure we can hear the noise + if ((playerOrigin - myOrigin).IsLengthGreaterThan( range )) + return; + + // tell our teammates that the hostages are being taken + GetChatter()->HostagesBeingTaken(); + + // only move if we hear them being rescued and can't see any hostages + if (GetGameState()->GetNearestVisibleFreeHostage() == NULL) + { + // since we are guarding the hostages, presumably we know where they are + // if we're close enough to "hear" this event, either go to where the event occured, + // or head for an escape zone to head them off + if (GetTask() != CCSBot::GUARD_HOSTAGE_RESCUE_ZONE) + { + //const float headOffChance = 33.3f; + if (true) // || RandomFloat( 0, 100 ) < headOffChance) + { + // head them off at a rescue zone + if (GuardRandomZone()) + { + SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + PrintIfWatched( "Trying to beat them to an escape zone!\n" ); + } + } + else + { + SetTask( SEEK_AND_DESTROY ); + StandUp(); + Run(); + MoveTo( playerOrigin, FASTEST_ROUTE ); + } + } + } + } + else + { + // CT's don't care about this noise + return; + } + + OnAudibleEvent( event, player, range, PRIORITY_MEDIUM, false ); // hostage_follows +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnRoundEnd( IGameEvent *event ) +{ + // Morale adjustments happen even for dead players + int winner = event->GetInt( "winner" ); + switch ( winner ) + { + case WINNER_TER: + if (GetTeamNumber() == TEAM_CT) + { + DecreaseMorale(); + } + else + { + IncreaseMorale(); + } + break; + + case WINNER_CT: + if (GetTeamNumber() == TEAM_CT) + { + IncreaseMorale(); + } + else + { + DecreaseMorale(); + } + break; + + default: + break; + } + + m_gameState.OnRoundEnd( event ); + + if ( !IsAlive() ) + return; + + if ( event->GetInt( "winner" ) == WINNER_TER ) + { + if (GetTeamNumber() == TEAM_TERRORIST) + GetChatter()->CelebrateWin(); + } + else if ( event->GetInt( "winner" ) == WINNER_CT ) + { + if (GetTeamNumber() == TEAM_CT) + GetChatter()->CelebrateWin(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnRoundStart( IGameEvent *event ) +{ + m_gameState.OnRoundStart( event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnHostageRescuedAll( IGameEvent *event ) +{ + m_gameState.OnHostageRescuedAll( event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnNavBlocked( IGameEvent *event ) +{ + if ( event->GetBool( "blocked" ) ) + { + unsigned int areaID = event->GetInt( "area" ); + if ( areaID ) + { + // An area was blocked off. Reset our path if it has this area on it. + for( int i=0; i<m_pathLength; ++i ) + { + const ConnectInfo *info = &m_path[ i ]; + if ( info->area && info->area->GetID() == areaID ) + { + DestroyPath(); + return; + } + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when bot enters a nav area + */ +void CCSBot::OnEnteredNavArea( CNavArea *newArea ) +{ + // assume that we "clear" an area of enemies when we enter it + newArea->SetClearedTimestamp( GetTeamNumber()-1 ); + + // if we just entered a 'stop' area, set the flag + if ( newArea->GetAttributes() & NAV_MESH_STOP ) + { + m_isStopping = true; + } + + /// @todo Flag these areas as spawn areas during load + if (IsAtEnemySpawn()) + { + m_hasVisitedEnemySpawn = true; + } +} diff --git a/game/server/cstrike/bot/cs_bot_event_bomb.cpp b/game/server/cstrike/bot/cs_bot_event_bomb.cpp new file mode 100644 index 0000000..b1dd75d --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event_bomb.cpp @@ -0,0 +1,158 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombPickedUp( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + if (GetTeamNumber() == TEAM_CT && player) + { + // check if we're close enough to hear it + const float bombPickupHearRangeSq = 1000.0f * 1000.0f; + Vector myOrigin = GetCentroid( this ); + + if ((myOrigin - player->GetAbsOrigin()).LengthSqr() < bombPickupHearRangeSq) + { + GetChatter()->TheyPickedUpTheBomb(); + GetGameState()->UpdateBomber( player->GetAbsOrigin() ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombPlanted( IGameEvent *event ) +{ + m_gameState.OnBombPlanted( event ); + + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // if we're a TEAM_CT, forget what we're doing and go after the bomb + if (GetTeamNumber() == TEAM_CT) + { + Idle(); + } + + // if we are following someone, stop following + if (IsFollowing()) + { + StopFollowing(); + Idle(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombBeep( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + CBaseEntity *entity = UTIL_EntityByIndex( event->GetInt( "entindex" ) ); + Vector myOrigin = GetCentroid( this ); + + // if we don't know where the bomb is, but heard it beep, we've discovered it + if (GetGameState()->IsPlantedBombLocationKnown() == false && entity) + { + // check if we're close enough to hear it + const float bombBeepHearRangeSq = 1500.0f * 1500.0f; + if ((myOrigin - entity->GetAbsOrigin()).LengthSqr() < bombBeepHearRangeSq) + { + // radio the news to our team + if (GetTeamNumber() == TEAM_CT && GetGameState()->GetPlantedBombsite() == CSGameState::UNKNOWN) + { + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( entity->GetAbsOrigin() ); + if (zone) + GetChatter()->FoundPlantedBomb( zone->m_index ); + } + + // remember where the bomb is + GetGameState()->UpdatePlantedBomb( entity->GetAbsOrigin() ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombDefuseBegin( IGameEvent *event ) +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombDefused( IGameEvent *event ) +{ + m_gameState.OnBombDefused( event ); + + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + if (GetTeamNumber() == TEAM_CT) + { + if (TheCSBots()->GetBombTimeLeft() < 2.0f) + GetChatter()->Say( "BarelyDefused" ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombDefuseAbort( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + PrintIfWatched( "BOMB DEFUSE ABORTED\n" ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnBombExploded( IGameEvent *event ) +{ + m_gameState.OnBombExploded( event ); +} + + + diff --git a/game/server/cstrike/bot/cs_bot_event_player.cpp b/game/server/cstrike/bot/cs_bot_event_player.cpp new file mode 100644 index 0000000..d5734ab --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event_player.cpp @@ -0,0 +1,227 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerDeath( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + Vector playerOrigin = (player) ? GetCentroid( player ) : Vector( 0, 0, 0 ); + + CBasePlayer *other = UTIL_PlayerByUserId( event->GetInt( "attacker" ) ); + CBasePlayer *victim = player; + + CBasePlayer *killer = (other && other->IsPlayer()) ? static_cast<CBasePlayer *>( other ) : NULL; + + // if the human player died in the single player game, tell the team + if (CSGameRules()->IsCareer() && victim && !victim->IsBot() && victim->GetTeamNumber() == GetTeamNumber()) + { + GetChatter()->Say( "CommanderDown", 20.0f ); + } + + // keep track of the last player we killed + if (killer == this) + { + m_lastVictimID = victim ? victim->entindex() : 0; + } + + // react to teammate death + if (victim && victim->GetTeamNumber() == GetTeamNumber()) + { + // note time of death + m_friendDeathTimestamp = gpGlobals->curtime; + + // chastise friendly fire from humans + if (killer && !killer->IsBot() && killer->GetTeamNumber() == GetTeamNumber() && killer != this) + { + GetChatter()->KilledFriend(); + } + + if (IsAttacking()) + { + if (GetTimeSinceLastSawEnemy() > 0.4f) + { + PrintIfWatched( "Rethinking my attack due to teammate death\n" ); + + // allow us to sneak past windows, doors, etc + IgnoreEnemies( 1.0f ); + + // move to last known position of enemy - this could cause us to flank if + // the danger has changed due to our teammate's recent death + SetTask( MOVE_TO_LAST_KNOWN_ENEMY_POSITION, GetBotEnemy() ); + MoveTo( GetLastKnownEnemyPosition() ); + return; + } + } + else // not attacking + { + // + // If we just saw a nearby friend die, and we haven't yet acquired an enemy + // automatically acquire our dead friend's killer + // + if (GetDisposition() == ENGAGE_AND_INVESTIGATE || GetDisposition() == OPPORTUNITY_FIRE) + { + CBasePlayer *other = UTIL_PlayerByUserId( event->GetInt( "attacker" ) ); + + // check that attacker is an enemy (for friendly fire, etc) + if (other && other->IsPlayer()) + { + CCSPlayer *killer = static_cast<CCSPlayer *>( other ); + if (killer->GetTeamNumber() != GetTeamNumber()) + { + // check if we saw our friend die - dont check FOV - assume we're aware of our surroundings in combat + // snipers stay put + if (!IsSniper() && IsVisible( playerOrigin )) + { + // people are dying - we should hurry + Hurry( RandomFloat( 10.0f, 15.0f ) ); + + // if we're hiding with only our knife, be a little more cautious + const float knifeAmbushChance = 50.0f; + if (!IsHiding() || !IsUsingKnife() || RandomFloat( 0, 100 ) < knifeAmbushChance) + { + PrintIfWatched( "Attacking our friend's killer!\n" ); + Attack( killer ); + return; + } + } + + // if friend was far away and we haven't seen an enemy in awhile, go to where our friend was killed + const float longHidingTime = 20.0f; + if (IsHunting() || IsInvestigatingNoise() || (IsHiding() && GetTask() != FOLLOW && GetHidingTime() > longHidingTime)) + { + const float someTime = 10.0f; + const float farAway = 750.0f; + if (GetTimeSinceLastSawEnemy() > someTime && (playerOrigin - GetAbsOrigin()).IsLengthGreaterThan( farAway )) + { + PrintIfWatched( "Checking out where our friend was killed\n" ); + MoveTo( playerOrigin, FASTEST_ROUTE ); + return; + } + } + } + } + } + } + } + else // an enemy was killed + { + // forget our current noise - it may have come from the now dead enemy + ForgetNoise(); + + if (killer && killer->GetTeamNumber() == GetTeamNumber()) + { + // only chatter about enemy kills if we see them occur, and they were the last one we see + if (GetNearbyEnemyCount() <= 1) + { + // report if number of enemies left is few and we killed the last one we saw locally + GetChatter()->EnemiesRemaining(); + + Vector victimOrigin = (victim) ? GetCentroid( victim ) : Vector( 0, 0, 0 ); + if (IsVisible( victimOrigin, CHECK_FOV )) + { + // congratulate teammates on their kills + if (killer && killer != this) + { + float delay = RandomFloat( 2.0f, 3.0f ); + if (killer->IsBot()) + { + if (RandomFloat( 0.0f, 100.0f ) < 40.0f) + GetChatter()->Say( "NiceShot", 3.0f, delay ); + } + else + { + // humans get the honorific + if (CSGameRules()->IsCareer()) + GetChatter()->Say( "NiceShotCommander", 3.0f, delay ); + else + GetChatter()->Say( "NiceShotSir", 3.0f, delay ); + } + } + } + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerRadio( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CCSPlayer *player = ToCSPlayer( UTIL_PlayerByUserId( event->GetInt( "userid" ) ) ); + if ( player == this ) + return; + + // + // Process radio events from our team + // + if (player && player->GetTeamNumber() == GetTeamNumber() ) + { + /// @todo Distinguish between radio commands and responses + RadioType radioEvent = (RadioType)event->GetInt( "slot" ); + + if (radioEvent != RADIO_INVALID && radioEvent != RADIO_AFFIRMATIVE && radioEvent != RADIO_NEGATIVE && radioEvent != RADIO_REPORTING_IN) + { + m_lastRadioCommand = radioEvent; + m_lastRadioRecievedTimestamp = gpGlobals->curtime; + m_radioSubject = player; + m_radioPosition = GetCentroid( player ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerFallDamage( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // player_falldamage +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnPlayerFootstep( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false, IS_FOOTSTEP ); // player_footstep +} + + diff --git a/game/server/cstrike/bot/cs_bot_event_weapon.cpp b/game/server/cstrike/bot/cs_bot_event_weapon.cpp new file mode 100644 index 0000000..90f8b9f --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_event_weapon.cpp @@ -0,0 +1,167 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "KeyValues.h" + +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponFire( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // for knife fighting - if our victim is attacking or reloading, rush him + /// @todo Propagate events into active state + if (GetEnemy() == player && IsUsingKnife()) + { + ForceRun( 5.0f ); + } + + const float ShortRange = 1000.0f; + const float NormalRange = 2000.0f; + + float range; + + /// @todo Check weapon type (knives are pretty quiet) + /// @todo Use actual volume, account for silencers, etc. + CWeaponCSBase *weapon = (CWeaponCSBase *)((player)?player->GetActiveWeapon():NULL); + + if (weapon == NULL) + return; + + switch( weapon->GetWeaponID() ) + { + // silent "firing" + case WEAPON_HEGRENADE: + case WEAPON_SMOKEGRENADE: + case WEAPON_FLASHBANG: + case WEAPON_SHIELDGUN: + case WEAPON_C4: + return; + + // quiet + case WEAPON_KNIFE: + case WEAPON_TMP: + range = ShortRange; + break; + + // M4A1 - check for silencer + case WEAPON_M4A1: + { + if (weapon->IsSilenced()) + { + range = ShortRange; + } + else + { + range = NormalRange; + } + break; + } + + // USP - check for silencer + case WEAPON_USP: + { + if (weapon->IsSilenced()) + { + range = ShortRange; + } + else + { + range = NormalRange; + } + break; + } + + // loud + case WEAPON_AWP: + range = 99999.0f; + break; + + // normal + default: + range = NormalRange; + break; + } + + OnAudibleEvent( event, player, range, PRIORITY_HIGH, true ); // weapon_fire +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponFireOnEmpty( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // for knife fighting - if our victim is attacking or reloading, rush him + /// @todo Propagate events into active state + if (GetEnemy() == player && IsUsingKnife()) + { + ForceRun( 5.0f ); + } + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // weapon_fire_on_empty +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponReload( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + // for knife fighting - if our victim is attacking or reloading, rush him + /// @todo Propagate events into active state + if (GetEnemy() == player && IsUsingKnife()) + { + ForceRun( 5.0f ); + } + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // weapon_reload +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::OnWeaponZoom( IGameEvent *event ) +{ + if ( !IsAlive() ) + return; + + // don't react to our own events + CBasePlayer *player = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + if ( player == this ) + return; + + OnAudibleEvent( event, player, 1100.0f, PRIORITY_LOW, false ); // weapon_zoom +} + + + diff --git a/game/server/cstrike/bot/cs_bot_init.cpp b/game/server/cstrike/bot/cs_bot_init.cpp new file mode 100644 index 0000000..d6a5c77 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_init.cpp @@ -0,0 +1,359 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "cs_shareddefs.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#pragma warning( disable : 4355 ) // warning 'this' used in base member initializer list - we're using it safely + + +//-------------------------------------------------------------------------------------------------------------- +static void PrefixChanged( IConVar *c, const char *oldPrefix, float flOldValue ) +{ + if ( TheCSBots() && TheCSBots()->IsServerActive() ) + { + for( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if ( !player ) + continue; + + if ( !player->IsBot() || !IsEntityValid( player ) ) + continue; + + CCSBot *bot = dynamic_cast< CCSBot * >( player ); + + if ( !bot ) + continue; + + // set the bot's name + char botName[MAX_PLAYER_NAME_LENGTH]; + UTIL_ConstructBotNetName( botName, MAX_PLAYER_NAME_LENGTH, bot->GetProfile() ); + + engine->SetFakeClientConVarValue( bot->edict(), "name", botName ); + } + } +} + + +ConVar cv_bot_traceview( "bot_traceview", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_stop( "bot_stop", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, immediately stops all bot processing." ); +ConVar cv_bot_show_nav( "bot_show_nav", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_walk( "bot_walk", "0", FCVAR_REPLICATED, "If nonzero, bots can only walk, not run." ); +ConVar cv_bot_difficulty( "bot_difficulty", "1", FCVAR_REPLICATED, "Defines the skill of bots joining the game. Values are: 0=easy, 1=normal, 2=hard, 3=expert." ); +ConVar cv_bot_debug( "bot_debug", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_debug_target( "bot_debug_target", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "For internal testing purposes." ); +ConVar cv_bot_quota( "bot_quota", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Determines the total number of bots in the game." ); +ConVar cv_bot_quota_mode( "bot_quota_mode", "normal", FCVAR_REPLICATED, "Determines the type of quota.\nAllowed values: 'normal', 'fill', and 'match'.\nIf 'fill', the server will adjust bots to keep N players in the game, where N is bot_quota.\nIf 'match', the server will maintain a 1:N ratio of humans to bots, where N is bot_quota." ); +ConVar cv_bot_prefix( "bot_prefix", "", FCVAR_REPLICATED, "This string is prefixed to the name of all bots that join the game.\n<difficulty> will be replaced with the bot's difficulty.\n<weaponclass> will be replaced with the bot's desired weapon class.\n<skill> will be replaced with a 0-100 representation of the bot's skill.", PrefixChanged ); +ConVar cv_bot_allow_rogues( "bot_allow_rogues", "1", FCVAR_REPLICATED, "If nonzero, bots may occasionally go 'rogue'. Rogue bots do not obey radio commands, nor pursue scenario goals." ); +ConVar cv_bot_allow_pistols( "bot_allow_pistols", "1", FCVAR_REPLICATED, "If nonzero, bots may use pistols." ); +ConVar cv_bot_allow_shotguns( "bot_allow_shotguns", "1", FCVAR_REPLICATED, "If nonzero, bots may use shotguns." ); +ConVar cv_bot_allow_sub_machine_guns( "bot_allow_sub_machine_guns", "1", FCVAR_REPLICATED, "If nonzero, bots may use sub-machine guns." ); +ConVar cv_bot_allow_rifles( "bot_allow_rifles", "1", FCVAR_REPLICATED, "If nonzero, bots may use rifles." ); +ConVar cv_bot_allow_machine_guns( "bot_allow_machine_guns", "1", FCVAR_REPLICATED, "If nonzero, bots may use the machine gun." ); +ConVar cv_bot_allow_grenades( "bot_allow_grenades", "1", FCVAR_REPLICATED, "If nonzero, bots may use grenades." ); +ConVar cv_bot_allow_snipers( "bot_allow_snipers", "1", FCVAR_REPLICATED, "If nonzero, bots may use sniper rifles." ); +#ifdef CS_SHIELD_ENABLED +ConVar cv_bot_allow_shield( "bot_allow_shield", "1", FCVAR_REPLICATED ); +#endif // CS_SHIELD_ENABLED +ConVar cv_bot_join_team( "bot_join_team", "any", FCVAR_REPLICATED, "Determines the team bots will join into. Allowed values: 'any', 'T', or 'CT'." ); +ConVar cv_bot_join_after_player( "bot_join_after_player", "1", FCVAR_REPLICATED, "If nonzero, bots wait until a player joins before entering the game." ); +ConVar cv_bot_auto_vacate( "bot_auto_vacate", "1", FCVAR_REPLICATED, "If nonzero, bots will automatically leave to make room for human players." ); +ConVar cv_bot_zombie( "bot_zombie", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, bots will stay in idle mode and not attack." ); +ConVar cv_bot_defer_to_human( "bot_defer_to_human", "0", FCVAR_REPLICATED, "If nonzero and there is a human on the team, the bots will not do the scenario tasks." ); +ConVar cv_bot_chatter( "bot_chatter", "normal", FCVAR_REPLICATED, "Control how bots talk. Allowed values: 'off', 'radio', 'minimal', or 'normal'." ); +ConVar cv_bot_profile_db( "bot_profile_db", "BotProfile.db", FCVAR_REPLICATED, "The filename from which bot profiles will be read." ); +ConVar cv_bot_dont_shoot( "bot_dont_shoot", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, bots will not fire weapons (for debugging)." ); +ConVar cv_bot_eco_limit( "bot_eco_limit", "2000", FCVAR_REPLICATED, "If nonzero, bots will not buy if their money falls below this amount." ); +ConVar cv_bot_auto_follow( "bot_auto_follow", "0", FCVAR_REPLICATED, "If nonzero, bots with high co-op may automatically follow a nearby human player." ); +ConVar cv_bot_flipout( "bot_flipout", "0", FCVAR_REPLICATED | FCVAR_CHEAT, "If nonzero, bots use no CPU for AI. Instead, they run around randomly." ); + + +extern void FinishClientPutInServer( CCSPlayer *pPlayer ); + + +//-------------------------------------------------------------------------------------------------------------- +// Engine callback for custom server commands +void Bot_ServerCommand( void ) +{ +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Constructor + */ +CCSBot::CCSBot( void ) : m_chatter( this ), m_gameState( this ) +{ + m_hasJoined = false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Destructor + */ +CCSBot::~CCSBot() +{ +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Prepare bot for action + */ +bool CCSBot::Initialize( const BotProfile *profile, int team ) +{ + // extend + BaseClass::Initialize( profile, team ); + + // CS bot initialization + m_diedLastRound = false; + m_morale = POSITIVE; // starting a new round makes everyone a little happy + + m_combatRange = RandomFloat( 325.0f, 425.0f ); + + // set initial safe time guess for this map + m_safeTime = 15.0f + 5.0f * GetProfile()->GetAggression(); + + m_name[0] = '\000'; + + ResetValues(); + + m_desiredTeam = team; + + if (GetTeamNumber() == 0) + { + HandleCommand_JoinTeam( m_desiredTeam ); + int desiredClass = GetProfile()->GetSkin(); + if ( m_desiredTeam == TEAM_CT && desiredClass ) + { + desiredClass = FIRST_CT_CLASS + desiredClass - 1; + } + else if ( m_desiredTeam == TEAM_TERRORIST && desiredClass ) + { + desiredClass = FIRST_T_CLASS + desiredClass - 1; + } + HandleCommand_JoinClass( desiredClass ); + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset internal data to initial state + */ +void CCSBot::ResetValues( void ) +{ + m_chatter.Reset(); + m_gameState.Reset(); + + m_avoid = NULL; + m_avoidTimestamp = 0.0f; + + m_hurryTimer.Invalidate(); + m_alertTimer.Invalidate(); + m_sneakTimer.Invalidate(); + m_noiseBendTimer.Invalidate(); + m_bendNoisePositionValid = false; + + m_isStuck = false; + m_stuckTimestamp = 0.0f; + m_wiggleTimer.Invalidate(); + m_stuckJumpTimer.Invalidate(); + + m_pathLength = 0; + m_pathIndex = 0; + m_areaEnteredTimestamp = 0.0f; + m_currentArea = NULL; + m_lastKnownArea = NULL; + m_isStopping = false; + + m_avoidFriendTimer.Invalidate(); + m_isFriendInTheWay = false; + m_isWaitingBehindFriend = false; + m_isAvoidingGrenade.Invalidate(); + + StopPanicking(); + + m_disposition = ENGAGE_AND_INVESTIGATE; + + m_enemy = NULL; + + m_grenadeTossState = NOT_THROWING; + m_initialEncounterArea = NULL; + + m_wasSafe = true; + + m_nearbyEnemyCount = 0; + m_enemyPlace = 0; + m_nearbyFriendCount = 0; + m_closestVisibleFriend = NULL; + m_closestVisibleHumanFriend = NULL; + + for( int w=0; w<MAX_PLAYERS; ++w ) + { + m_watchInfo[w].timestamp = 0.0f; + m_watchInfo[w].isEnemy = false; + + m_playerTravelDistance[ w ] = -1.0f; + } + + // randomly offset each bot's timer to spread computation out + m_updateTravelDistanceTimer.Start( RandomFloat( 0.0f, 0.9f ) ); + m_travelDistancePhase = 0; + + m_isEnemyVisible = false; + m_visibleEnemyParts = NONE; + m_lastSawEnemyTimestamp = -999.9f; + m_firstSawEnemyTimestamp = 0.0f; + m_currentEnemyAcquireTimestamp = 0.0f; + m_isLastEnemyDead = true; + m_attacker = NULL; + m_attackedTimestamp = 0.0f; + m_enemyDeathTimestamp = 0.0f; + m_friendDeathTimestamp = 0.0f; + m_lastVictimID = 0; + m_isAimingAtEnemy = false; + m_fireWeaponTimestamp = 0.0f; + m_equipTimer.Invalidate(); + m_zoomTimer.Invalidate(); + + m_isFollowing = false; + m_leader = NULL; + m_followTimestamp = 0.0f; + m_allowAutoFollowTime = 0.0f; + + m_enemyQueueIndex = 0; + m_enemyQueueCount = 0; + m_enemyQueueAttendIndex = 0; + m_bomber = NULL; + + m_isEnemySniperVisible = false; + m_sawEnemySniperTimer.Invalidate(); + + m_lookAroundStateTimestamp = 0.0f; + m_inhibitLookAroundTimestamp = 0.0f; + + m_lookPitch = 0.0f; + m_lookPitchVel = 0.0f; + m_lookYaw = 0.0f; + m_lookYawVel = 0.0f; + + m_aimOffsetTimestamp = 0.0f; + m_aimSpreadTimestamp = 0.0f; + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + + for( int p=0; p<MAX_PLAYERS; ++p ) + { + m_partInfo[p].m_validFrame = 0; + } + + m_spotEncounter = NULL; + m_spotCheckTimestamp = 0.0f; + m_peripheralTimestamp = 0.0f; + + m_avgVelIndex = 0; + m_avgVelCount = 0; + + m_lastOrigin = GetCentroid( this ); + + m_lastRadioCommand = RADIO_INVALID; + m_lastRadioRecievedTimestamp = 0.0f; + m_lastRadioSentTimestamp = 0.0f; + m_radioSubject = NULL; + m_voiceEndTimestamp = 0.0f; + + m_hostageEscortCount = 0; + m_hostageEscortCountTimestamp = 0.0f; + + m_noisePosition = Vector( 0, 0, 0 ); + m_noiseTimestamp = 0.0f; + + m_stateTimestamp = 0.0f; + m_task = SEEK_AND_DESTROY; + m_taskEntity = NULL; + + m_approachPointCount = 0; + m_approachPointViewPosition.x = 99999999999.9f; + m_approachPointViewPosition.y = 0.0f; + m_approachPointViewPosition.z = 0.0f; + + m_checkedHidingSpotCount = 0; + + StandUp(); + Run(); + m_mustRunTimer.Invalidate(); + m_waitTimer.Invalidate(); + m_pathLadder = NULL; + + m_repathTimer.Invalidate(); + + m_huntState.ClearHuntArea(); + m_hasVisitedEnemySpawn = false; + m_stillTimer.Invalidate(); + + // adjust morale - if we died, our morale decreased, + // but if we live, no adjustement (round win/loss also adjusts morale) + if (m_diedLastRound) + DecreaseMorale(); + + m_diedLastRound = false; + + + // IsRogue() randomly changes this + m_isRogue = false; + + m_surpriseTimer.Invalidate(); + + // even though these are EHANDLEs, they need to be NULL-ed + m_goalEntity = NULL; + m_avoid = NULL; + m_enemy = NULL; + + for ( int i=0; i<MAX_ENEMY_QUEUE; ++i ) + { + m_enemyQueue[i].player = NULL; + m_enemyQueue[i].isReloading = false; + m_enemyQueue[i].isProtectedByShield = false; + } + + // start in idle state + m_isOpeningDoor = false; + StopAttacking(); + Idle(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called when bot is placed in map, and when bots are reset after a round ends. + * NOTE: For some reason, this can be called twice when a bot is added. + */ +void CCSBot::Spawn( void ) +{ + // do the normal player spawn process + BaseClass::Spawn(); + + ResetValues(); + + V_strcpy_safe( m_name, GetPlayerName() ); + + Buy(); +} + diff --git a/game/server/cstrike/bot/cs_bot_listen.cpp b/game/server/cstrike/bot/cs_bot_listen.cpp new file mode 100644 index 0000000..54e47e2 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_listen.cpp @@ -0,0 +1,230 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +bool CCSBot::IsNoiseHeard( void ) const +{ + if (m_noiseTimestamp <= 0.0f) + return false; + + // primitive reaction time simulation - cannot "hear" noise until reaction time has elapsed + if (gpGlobals->curtime - m_noiseTimestamp >= GetProfile()->GetReactionTime()) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Listen for enemy noises, and determine if we should react to them. + * Returns true if heard a noise and should move to investigate. + */ +bool CCSBot::HeardInterestingNoise( void ) +{ + if (IsBlind()) + return false; + + // don't investigate noises during safe time + if (!IsWellPastSafe()) + return false; + + // if our disposition is not to investigate, dont investigate + if (GetDisposition() != ENGAGE_AND_INVESTIGATE) + return false; + + // listen for enemy noises + if (IsNoiseHeard()) + { + // if we are hiding, only react to noises very nearby, depending on how aggressive we are + if (IsAtHidingSpot() && GetNoiseRange() > 100.0f + 400.0f * GetProfile()->GetAggression()) + return false; + + // chance of investigating is inversely proportional to distance + const float maxNoiseDist = 3000.0f; + float chance = 100.0f * (1.0f - (GetNoiseRange()/maxNoiseDist)); + + // modify chance by number of friends remaining + // if we have lots of friends, presumably one of them is closer and will check it out + if (GetFriendsRemaining() >= 3) + { + float friendFactor = 5.0f * GetFriendsRemaining(); + if (friendFactor > 50.0f) + friendFactor = 50.0f; + + chance -= friendFactor; + } + + if (RandomFloat( 0.0f, 100.0f ) <= chance) + { + return true; + } + } + + return false; +} + + + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we hear nearby threatening enemy gunfire within given range + * -1 == infinite range + */ +bool CCSBot::CanHearNearbyEnemyGunfire( float range ) const +{ + Vector myOrigin = GetCentroid( this ); + + // only attend to noise if it just happened + if (gpGlobals->curtime - m_noiseTimestamp > 0.5f) + return false; + + // gunfire is high priority + if (m_noisePriority < PRIORITY_HIGH) + return false; + + // check noise range + if (range > 0.0f && (myOrigin - m_noisePosition).IsLengthGreaterThan( range )) + return false; + + // if we dont have line of sight, it's not threatening (cant get shot) + if (!CanSeeNoisePosition()) + return false; + + if (IsAttacking() && m_enemy != NULL && GetTimeSinceLastSawEnemy() < 1.0f) + { + // gunfire is only threatening if it is closer than our current enemy + float gunfireDistSq = (m_noisePosition - myOrigin).LengthSqr(); + float enemyDistSq = (GetCentroid( m_enemy ) - myOrigin).LengthSqr(); + const float muchCloserSq = 100.0f * 100.0f; + if (gunfireDistSq > enemyDistSq - muchCloserSq) + return false; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we directly see where we think the noise came from + * NOTE: Dont check FOV, since this is used to determine if we should turn our head to look at the noise + * NOTE: Dont use IsVisible(), because smoke shouldnt cause us to not look toward noises + */ +bool CCSBot::CanSeeNoisePosition( void ) const +{ + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); + UTIL_TraceLine( EyePositionConst(), m_noisePosition + Vector( 0, 0, HalfHumanHeight ), MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + if (result.fraction == 1.0f) + { + // we can see the source of the noise + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we decided to look towards the most recent noise source + * Assumes m_noisePosition is valid. + */ +bool CCSBot::UpdateLookAtNoise( void ) +{ + // make sure a noise exists + if (!IsNoiseHeard()) + { + return false; + } + + Vector spot; + + // if we have clear line of sight to noise position, look directly at it + if (CanSeeNoisePosition()) + { + /// @todo adjust noise Z to keep consistent with current height while fighting + spot = m_noisePosition + Vector( 0, 0, HalfHumanHeight ); + + // since we can see the noise spot, forget about it + ForgetNoise(); + } + else + { + // line of sight is blocked, bend it + + // the bending algorithm is very expensive, throttle how often it is done + if (m_noiseBendTimer.IsElapsed()) + { + const float noiseBendLOSInterval = RandomFloat( 0.2f, 0.3f ); + m_noiseBendTimer.Start( noiseBendLOSInterval ); + + // line of sight is blocked, bend it + if (BendLineOfSight( EyePosition(), m_noisePosition, &spot ) == false) + { + m_bendNoisePositionValid = false; + return false; + } + + m_bentNoisePosition = spot; + m_bendNoisePositionValid = true; + } + else if (m_bendNoisePositionValid) + { + // use result of prior bend computation + spot = m_bentNoisePosition; + } + else + { + // prior bend failed + return false; + } + } + + // it's always important to look at enemy noises, because they come from ... enemies! + PriorityType pri = PRIORITY_HIGH; + + // look longer if we're hiding + if (IsAtHidingSpot()) + { + // if there is only one enemy left, look for a long time + if (GetEnemiesRemaining() == 1) + { + SetLookAt( "Noise", spot, pri, RandomFloat( 5.0f, 15.0f ), true ); + } + else + { + SetLookAt( "Noise", spot, pri, RandomFloat( 3.0f, 5.0f ), true ); + } + } + else + { + const float closeRange = 500.0f; + if (GetNoiseRange() < closeRange) + { + // look at nearby enemy noises for a longer time + SetLookAt( "Noise", spot, pri, RandomFloat( 3.0f, 5.0f ), true ); + } + else + { + SetLookAt( "Noise", spot, pri, RandomFloat( 1.0f, 2.0f ), true ); + } + } + + return true; +} + diff --git a/game/server/cstrike/bot/cs_bot_manager.cpp b/game/server/cstrike/bot/cs_bot_manager.cpp new file mode 100644 index 0000000..aec0ef3 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_manager.cpp @@ -0,0 +1,2390 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#pragma warning( disable : 4530 ) // STL uses exceptions, but we are not compiling with them - ignore warning + +#include "cbase.h" + +#include "cs_bot.h" +#include "nav_area.h" +#include "cs_gamerules.h" +#include "shared_util.h" +#include "KeyValues.h" +#include "tier0/icommandline.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef _WIN32 +#pragma warning (disable:4701) // disable warning that variable *may* not be initialized +#endif + +CBotManager *TheBots = NULL; + +bool CCSBotManager::m_isMapDataLoaded = false; + +int g_nClientPutInServerOverrides = 0; + + +void DrawOccupyTime( void ); +ConVar bot_show_occupy_time( "bot_show_occupy_time", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show when each nav area can first be reached by each team." ); + +void DrawBattlefront( void ); +ConVar bot_show_battlefront( "bot_show_battlefront", "0", FCVAR_GAMEDLL | FCVAR_CHEAT, "Show areas where rushing players will initially meet." ); + +int UTIL_CSSBotsInGame( void ); + +ConVar bot_join_delay( "bot_join_delay", "0", FCVAR_GAMEDLL, "Prevents bots from joining the server for this many seconds after a map change." ); + +/** + * Determine whether bots can be used or not + */ +inline bool AreBotsAllowed() +{ + // If they pass in -nobots, don't allow bots. This is for people who host servers, to + // allow them to disallow bots to enforce CPU limits. + const char *nobots = CommandLine()->CheckParm( "-nobots" ); + if ( nobots ) + { + return false; + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +void InstallBotControl( void ) +{ + if ( TheBots != NULL ) + delete TheBots; + + TheBots = new CCSBotManager; +} + + +//-------------------------------------------------------------------------------------------------------------- +void RemoveBotControl( void ) +{ + if ( TheBots != NULL ) + delete TheBots; + + TheBots = NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +CBasePlayer* ClientPutInServerOverride_Bot( edict_t *pEdict, const char *playername ) +{ + CBasePlayer *pPlayer = TheBots->AllocateAndBindBotEntity( pEdict ); + if ( pPlayer ) + { + pPlayer->SetPlayerName( playername ); + } + ++g_nClientPutInServerOverrides; + + return pPlayer; +} + +//-------------------------------------------------------------------------------------------------------------- +// Constructor +CCSBotManager::CCSBotManager() +{ + m_zoneCount = 0; + SetLooseBomb( NULL ); + m_serverActive = false; + + m_isBombPlanted = false; + m_bombDefuser = NULL; + m_roundStartTimestamp = 0.0f; + + m_eventListenersEnabled = true; + m_commonEventListeners.AddToTail( &m_PlayerFootstepEvent ); + m_commonEventListeners.AddToTail( &m_PlayerRadioEvent ); + m_commonEventListeners.AddToTail( &m_PlayerFallDamageEvent ); + m_commonEventListeners.AddToTail( &m_BombBeepEvent ); + m_commonEventListeners.AddToTail( &m_DoorMovingEvent ); + m_commonEventListeners.AddToTail( &m_BreakPropEvent ); + m_commonEventListeners.AddToTail( &m_BreakBreakableEvent ); + m_commonEventListeners.AddToTail( &m_WeaponFireEvent ); + m_commonEventListeners.AddToTail( &m_WeaponFireOnEmptyEvent ); + m_commonEventListeners.AddToTail( &m_WeaponReloadEvent ); + m_commonEventListeners.AddToTail( &m_WeaponZoomEvent ); + m_commonEventListeners.AddToTail( &m_BulletImpactEvent ); + m_commonEventListeners.AddToTail( &m_GrenadeBounceEvent ); + m_commonEventListeners.AddToTail( &m_NavBlockedEvent ); + + TheBotPhrases = new BotPhraseManager; + TheBotProfiles = new BotProfileManager; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when a new round begins + */ +void CCSBotManager::RestartRound( void ) +{ + // extend + CBotManager::RestartRound(); + + SetLooseBomb( NULL ); + m_isBombPlanted = false; + m_earliestBombPlantTimestamp = gpGlobals->curtime + RandomFloat( 10.0f, 30.0f ); // 60 + m_bombDefuser = NULL; + + ResetRadioMessageTimestamps(); + + m_lastSeenEnemyTimestamp = -9999.9f; + + m_roundStartTimestamp = gpGlobals->curtime + mp_freezetime.GetFloat(); + + // randomly decide if defensive team wants to "rush" as a whole + const float defenseRushChance = 33.3f; // 25.0f; + m_isDefenseRushing = (RandomFloat( 0.0f, 100.0f ) <= defenseRushChance) ? true : false; + + TheBotPhrases->OnRoundRestart(); + + m_isRoundOver = false; +} + +//-------------------------------------------------------------------------------------------------------------- + +void UTIL_DrawBox( Extent *extent, int lifetime, int red, int green, int blue ) +{ + int darkRed = red/2; + int darkGreen = green/2; + int darkBlue = blue/2; + + Vector v[8]; + v[0].x = extent->lo.x; v[0].y = extent->lo.y; v[0].z = extent->lo.z; + v[1].x = extent->hi.x; v[1].y = extent->lo.y; v[1].z = extent->lo.z; + v[2].x = extent->hi.x; v[2].y = extent->hi.y; v[2].z = extent->lo.z; + v[3].x = extent->lo.x; v[3].y = extent->hi.y; v[3].z = extent->lo.z; + v[4].x = extent->lo.x; v[4].y = extent->lo.y; v[4].z = extent->hi.z; + v[5].x = extent->hi.x; v[5].y = extent->lo.y; v[5].z = extent->hi.z; + v[6].x = extent->hi.x; v[6].y = extent->hi.y; v[6].z = extent->hi.z; + v[7].x = extent->lo.x; v[7].y = extent->hi.y; v[7].z = extent->hi.z; + + static int edge[] = + { + 1, 2, 3, 4, -1, + 5, 6, 7, 8, -5, + 1, -5, + 2, -6, + 3, -7, + 4, -8, + 0 + }; + + Vector from, to; + bool restart = true; + for( int i=0; edge[i] != 0; ++i ) + { + if (restart) + { + to = v[ edge[i]-1 ]; + restart = false; + continue; + } + + from = to; + + int index = edge[i]; + if (index < 0) + { + restart = true; + index = -index; + } + + to = v[ index-1 ]; + + NDebugOverlay::Line( from, to, darkRed, darkGreen, darkBlue, true, 0.1f ); + NDebugOverlay::Line( from, to, red, green, blue, false, 0.15f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::EnableEventListeners( bool enable ) +{ + if ( m_eventListenersEnabled == enable ) + { + return; + } + + m_eventListenersEnabled = enable; + + // enable/disable the most frequent event listeners, to improve performance when no bots are present. + for ( int i=0; i<m_commonEventListeners.Count(); ++i ) + { + if ( enable ) + { + gameeventmanager->AddListener( m_commonEventListeners[i], m_commonEventListeners[i]->GetEventName(), true ); + } + else + { + gameeventmanager->RemoveListener( m_commonEventListeners[i] ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Called each frame + */ +void CCSBotManager::StartFrame( void ) +{ + if ( !AreBotsAllowed() ) + { + EnableEventListeners( false ); + return; + } + + // EXTEND + CBotManager::StartFrame(); + + MaintainBotQuota(); + EnableEventListeners( UTIL_CSSBotsInGame() > 0 ); + + // debug zone extent visualization + if (cv_bot_debug.GetInt() == 5) + { + for( int z=0; z<m_zoneCount; ++z ) + { + Zone *zone = &m_zone[z]; + + if ( zone->m_isBlocked ) + { + UTIL_DrawBox( &zone->m_extent, 1, 255, 0, 200 ); + } + else + { + UTIL_DrawBox( &zone->m_extent, 1, 255, 100, 0 ); + } + } + } + + if (bot_show_occupy_time.GetBool()) + { + DrawOccupyTime(); + } + + if (bot_show_battlefront.GetBool()) + { + DrawBattlefront(); + } + + if ( m_checkTransientAreasTimer.IsElapsed() && !nav_edit.GetBool() ) + { + CUtlVector< CNavArea * >& transientAreas = TheNavMesh->GetTransientAreas(); + for ( int i=0; i<transientAreas.Count(); ++i ) + { + CNavArea *area = transientAreas[i]; + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + area->UpdateBlocked(); + } + } + + m_checkTransientAreasTimer.Start( 2.0f ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the bot can use this weapon + */ +bool CCSBotManager::IsWeaponUseable( const CWeaponCSBase *weapon ) const +{ + if (weapon == NULL) + return false; + + if (weapon->IsA( WEAPON_C4 )) + return true; + + if ((!AllowShotguns() && weapon->IsKindOf( WEAPONTYPE_SHOTGUN )) || + (!AllowMachineGuns() && weapon->IsKindOf( WEAPONTYPE_MACHINEGUN )) || + (!AllowRifles() && weapon->IsKindOf( WEAPONTYPE_RIFLE )) || + (!AllowShotguns() && weapon->IsKindOf( WEAPONTYPE_SHOTGUN )) || + (!AllowSnipers() && weapon->IsKindOf( WEAPONTYPE_SNIPER_RIFLE )) || + (!AllowSubMachineGuns() && weapon->IsKindOf( WEAPONTYPE_SUBMACHINEGUN )) || + (!AllowPistols() && weapon->IsKindOf( WEAPONTYPE_PISTOL )) || + (!AllowGrenades() && weapon->IsKindOf( WEAPONTYPE_GRENADE ))) + { + return false; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this player is on "defense" + */ +bool CCSBotManager::IsOnDefense( const CCSPlayer *player ) const +{ + switch (GetScenario()) + { + case SCENARIO_DEFUSE_BOMB: + return (player->GetTeamNumber() == TEAM_CT); + + case SCENARIO_RESCUE_HOSTAGES: + return (player->GetTeamNumber() == TEAM_TERRORIST); + + case SCENARIO_ESCORT_VIP: + return (player->GetTeamNumber() == TEAM_TERRORIST); + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if this player is on "offense" + */ +bool CCSBotManager::IsOnOffense( const CCSPlayer *player ) const +{ + return !IsOnDefense( player ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when a map has just been loaded + */ +void CCSBotManager::ServerActivate( void ) +{ + m_isMapDataLoaded = false; + + // load the database of bot radio chatter + TheBotPhrases->Reset(); + TheBotPhrases->Initialize( "BotChatter.db", 0 ); + + TheBotProfiles->Reset(); + TheBotProfiles->FindVoiceBankIndex( "BotChatter.db" ); // make sure default voice bank is first + const char *filename; + if ( false ) // g_engfuncs.pfnIsCareerMatch() ) + { + filename = "MissionPacks/BotPackList.db"; + } + else + { + filename = "BotPackList.db"; + } + + // read in the list of bot profile DBs + FileHandle_t file = filesystem->Open( filename, "r" ); + + if ( !file ) + { + TheBotProfiles->Init( "BotProfile.db" ); + } + else + { + int dataLength = filesystem->Size( filename ); + char *dataPointer = new char[ dataLength ]; + + filesystem->Read( dataPointer, dataLength, file ); + filesystem->Close( file ); + + const char *dataFile = SharedParse( dataPointer ); + const char *token; + + while ( dataFile ) + { + token = SharedGetToken(); + char *clone = CloneString( token ); + TheBotProfiles->Init( clone ); + delete[] clone; + dataFile = SharedParse( dataFile ); + } + + delete [] dataPointer; + } + + // Now that we've parsed all the profiles, we have a list of the voice banks they're using. + // Go back and parse the custom voice speakables. + const BotProfileManager::VoiceBankList *voiceBanks = TheBotProfiles->GetVoiceBanks(); + for ( int i=1; i<voiceBanks->Count(); ++i ) + { + TheBotPhrases->Initialize( (*voiceBanks)[i], i ); + } + + // tell the Navigation Mesh system what CS spawn points are named + TheNavMesh->SetPlayerSpawnName( "info_player_terrorist" ); + + ExtractScenarioData(); + + RestartRound(); + + TheBotPhrases->OnMapChange(); + + m_serverActive = true; +} + + +void CCSBotManager::ServerDeactivate( void ) +{ + m_serverActive = false; +} + +void CCSBotManager::ClientDisconnect( CBaseEntity *entity ) +{ +/* + if ( FBitSet( entity->GetFlags(), FL_FAKECLIENT ) ) + { + FREE_PRIVATE( entity ); + } +*/ + + /* + // make sure voice feedback is turned off + CBasePlayer *pPlayer = (CBasePlayer *)CBaseEntity::Instance( pEntity ); + if ( pPlayer && pPlayer->IsBot() ) + { + CCSBot *pBot = static_cast<CCSBot *>(pPlayer); + if (pBot) + { + pBot->EndVoiceFeedback( true ); + } + } + */ +} + + + +//-------------------------------------------------------------------------------------------------------------- +/** +* Parses out bot name/template/etc params from the current ConCommand +*/ +void BotArgumentsFromArgv( const CCommand &args, const char **name, CSWeaponType *weaponType, BotDifficultyType *difficulty, int *team = NULL, bool *all = NULL ) +{ + static char s_name[MAX_PLAYER_NAME_LENGTH]; + + s_name[0] = 0; + *name = s_name; + *difficulty = NUM_DIFFICULTY_LEVELS; + if ( team ) + { + *team = TEAM_UNASSIGNED; + } + if ( all ) + { + *all = false; + } + + *weaponType = WEAPONTYPE_UNKNOWN; + + for ( int arg=1; arg<args.ArgC(); ++arg ) + { + bool found = false; + + const char *token = args[arg]; + if ( all && FStrEq( token, "all" ) ) + { + *all = true; + found = true; + } + else if ( team && FStrEq( token, "t" ) ) + { + *team = TEAM_TERRORIST; + found = true; + } + else if ( team && FStrEq( token, "ct" ) ) + { + *team = TEAM_CT; + found = true; + } + + for( int i=0; i<NUM_DIFFICULTY_LEVELS && !found; ++i ) + { + if (!stricmp( BotDifficultyName[i], token )) + { + *difficulty = (BotDifficultyType)i; + found = true; + } + } + + if ( !found ) + { + *weaponType = WeaponClassFromString( token ); + if ( *weaponType != WEAPONTYPE_UNKNOWN ) + { + found = true; + } + } + + if ( !found ) + { + Q_strncpy( s_name, token, sizeof( s_name ) ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_add, "bot_add <t|ct> <type> <difficulty> <name> - Adds a bot matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + int team; + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty, &team ); + TheCSBots()->BotAddCommand( team, FROM_CONSOLE, name, weaponType, difficulty ); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_add_t, "bot_add_t <type> <difficulty> <name> - Adds a terrorist bot matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty ); + TheCSBots()->BotAddCommand( TEAM_TERRORIST, FROM_CONSOLE, name, weaponType, difficulty ); +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_add_ct, "bot_add_ct <type> <difficulty> <name> - Adds a Counter-Terrorist bot matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty ); + TheCSBots()->BotAddCommand( TEAM_CT, FROM_CONSOLE, name, weaponType, difficulty ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Collects all bots matching the given criteria (player name, profile template name, difficulty, and team) + */ +class CollectBots +{ +public: + CollectBots( const char *name, CSWeaponType weaponType, BotDifficultyType difficulty, int team ) + { + m_name = name; + m_difficulty = difficulty; + m_team = team; + m_weaponType = weaponType; + } + + bool operator() ( CBasePlayer *player ) + { + if ( !player->IsBot() ) + { + return true; + } + + CCSBot *bot = dynamic_cast< CCSBot * >(player); + if ( !bot || !bot->GetProfile() ) + { + return true; + } + + if ( m_name && *m_name ) + { + // accept based on name + if ( FStrEq( m_name, bot->GetProfile()->GetName() ) ) + { + m_bots.RemoveAll(); + m_bots.AddToTail( bot ); + return false; + } + + // Reject based on profile template name + if ( !bot->GetProfile()->InheritsFrom( m_name ) ) + { + return true; + } + } + + // reject based on difficulty + if ( m_difficulty != NUM_DIFFICULTY_LEVELS ) + { + if ( !bot->GetProfile()->IsDifficulty( m_difficulty ) ) + { + return true; + } + } + + // reject based on team + if ( m_team == TEAM_CT || m_team == TEAM_TERRORIST ) + { + if ( bot->GetTeamNumber() != m_team ) + { + return true; + } + } + + // reject based on weapon preference + if ( m_weaponType != WEAPONTYPE_UNKNOWN ) + { + if ( !bot->GetProfile()->GetWeaponPreferenceCount() ) + { + return true; + } + + if ( m_weaponType != WeaponClassFromWeaponID( (CSWeaponID)bot->GetProfile()->GetWeaponPreference( 0 ) ) ) + { + return true; + } + } + + // A match! + m_bots.AddToTail( bot ); + + return true; + } + + CUtlVector< CCSBot * > m_bots; + +private: + const char *m_name; + CSWeaponType m_weaponType; + BotDifficultyType m_difficulty; + int m_team; +}; + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_kill, "bot_kill <all> <t|ct> <type> <difficulty> <name> - Kills a specific bot, or all bots, matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + int team; + bool all; + + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty, &team, &all ); + if ( (!name || !*name) && team == TEAM_UNASSIGNED && difficulty == NUM_DIFFICULTY_LEVELS ) + { + all = true; + } + + CollectBots collector( name, weaponType, difficulty, team ); + ForEachPlayer( collector ); + + for ( int i=0; i<collector.m_bots.Count(); ++i ) + { + CCSBot *bot = collector.m_bots[i]; + if ( !bot->IsAlive() ) + continue; + + bot->CommitSuicide(); + + if ( !all ) + { + return; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_kick, "bot_kick <all> <t|ct> <type> <difficulty> <name> - Kicks a specific bot, or all bots, matching the given criteria.", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + const char *name; + BotDifficultyType difficulty; + CSWeaponType weaponType; + int team; + bool all; + + BotArgumentsFromArgv( args, &name, &weaponType, &difficulty, &team, &all ); + if ( (!name || !*name) && team == TEAM_UNASSIGNED && difficulty == NUM_DIFFICULTY_LEVELS ) + { + all = true; + } + + CollectBots collector( name, weaponType, difficulty, team ); + ForEachPlayer( collector ); + + for ( int i=0; i<collector.m_bots.Count(); ++i ) + { + CCSBot *bot = collector.m_bots[i]; + engine->ServerCommand( UTIL_VarArgs( "kick \"%s\"\n", bot->GetPlayerName() ) ); + if ( !all ) + { + // adjust bot quota so kicked bot is not immediately added back in + int newQuota = cv_bot_quota.GetInt() - 1; + cv_bot_quota.SetValue( clamp( newQuota, 0, cv_bot_quota.GetInt() ) ); + return; + } + } + + // adjust bot quota so kicked bot is not immediately added back in + if ( all && (!name || !*name) && team == TEAM_UNASSIGNED && difficulty == NUM_DIFFICULTY_LEVELS ) + { + cv_bot_quota.SetValue( 0 ); + } + else + { + int newQuota = cv_bot_quota.GetInt() - collector.m_bots.Count(); + cv_bot_quota.SetValue( clamp( newQuota, 0, cv_bot_quota.GetInt() ) ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_knives_only, "Restricts the bots to only using knives", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 0 ); + cv_bot_allow_shotguns.SetValue( 0 ); + cv_bot_allow_sub_machine_guns.SetValue( 0 ); + cv_bot_allow_rifles.SetValue( 0 ); + cv_bot_allow_machine_guns.SetValue( 0 ); + cv_bot_allow_grenades.SetValue( 0 ); + cv_bot_allow_snipers.SetValue( 0 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 0 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_pistols_only, "Restricts the bots to only using pistols", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 1 ); + cv_bot_allow_shotguns.SetValue( 0 ); + cv_bot_allow_sub_machine_guns.SetValue( 0 ); + cv_bot_allow_rifles.SetValue( 0 ); + cv_bot_allow_machine_guns.SetValue( 0 ); + cv_bot_allow_grenades.SetValue( 0 ); + cv_bot_allow_snipers.SetValue( 0 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 0 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_snipers_only, "Restricts the bots to only using sniper rifles", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 0 ); + cv_bot_allow_shotguns.SetValue( 0 ); + cv_bot_allow_sub_machine_guns.SetValue( 0 ); + cv_bot_allow_rifles.SetValue( 0 ); + cv_bot_allow_machine_guns.SetValue( 0 ); + cv_bot_allow_grenades.SetValue( 0 ); + cv_bot_allow_snipers.SetValue( 1 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 0 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_all_weapons, "Allows the bots to use all weapons", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + cv_bot_allow_pistols.SetValue( 1 ); + cv_bot_allow_shotguns.SetValue( 1 ); + cv_bot_allow_sub_machine_guns.SetValue( 1 ); + cv_bot_allow_rifles.SetValue( 1 ); + cv_bot_allow_machine_guns.SetValue( 1 ); + cv_bot_allow_grenades.SetValue( 1 ); + cv_bot_allow_snipers.SetValue( 1 ); +#ifdef CS_SHIELD_ENABLED + cv_bot_allow_shield.SetValue( 1 ); +#endif // CS_SHIELD_ENABLED +} + + +//-------------------------------------------------------------------------------------------------------------- +CON_COMMAND_F( bot_goto_mark, "Sends a bot to the selected nav area (useful for testing navigation meshes)", FCVAR_GAMEDLL | FCVAR_CHEAT ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + // tell the first bot we find to go to our marked area + CNavArea *area = TheNavMesh->GetMarkedArea(); + if (area) + { + for ( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBasePlayer *player = static_cast<CBasePlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (player->IsBot()) + { + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + + if ( bot ) + { + bot->MoveTo( area->GetCenter(), FASTEST_ROUTE ); + } + + break; + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +#if 0 +CON_COMMAND_F( bot_memory_usage, "Reports on the bots' memory usage", FCVAR_GAMEDLL ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + Msg( "Memory usage:\n" ); + + Msg( " %d bytes per bot\n", sizeof(CCSBot) ); + + Msg( " %d Navigation Areas @ %d bytes each = %d bytes\n", + TheNavMesh->GetNavAreaCount(), + sizeof( CNavArea ), + TheNavMesh->GetNavAreaCount() * sizeof( CNavArea ) ); + + Msg( " %d Hiding Spots @ %d bytes each = %d bytes\n", + TheHidingSpotList.Count(), + sizeof( HidingSpot ), + TheHidingSpotList.Count() * sizeof( HidingSpot ) ); + +/* + unsigned int encounterMem = 0; + FOR_EACH_LL( TheNavAreaList, it ) + { + CNavArea *area = TheNavAreaList[ it ]; + + FOR_EACH_LL( area->m_spotEncounterList, it ) + { + SpotEncounter *se = area->m_spotEncounterList[ it ]; + + encounterMem += sizeof( SpotEncounter ); + encounterMem += se->spotList.Count() * sizeof( SpotOrder ); + } + } + + Msg( " Encounter Spot data = %d bytes\n", encounterMem ); +*/ +} +#endif + + +bool CCSBotManager::ServerCommand( const char *cmd ) +{ + return false; +} + + +bool CCSBotManager::ClientCommand( CBasePlayer *player, const CCommand &args ) +{ + return false; +} + + +/** + * Process the "bot_add" console command + */ +bool CCSBotManager::BotAddCommand( int team, bool isFromConsole, const char *profileName, CSWeaponType weaponType, BotDifficultyType difficulty ) +{ + if ( !TheNavMesh->IsLoaded() ) + { + // If there isn't a Navigation Mesh in memory, create one + if ( !TheNavMesh->IsGenerating() ) + { + if ( !m_isMapDataLoaded ) + { + TheNavMesh->BeginGeneration(); + m_isMapDataLoaded = true; + } + return false; + } + } + + // dont allow bots to join if the Navigation Mesh is being generated + if (TheNavMesh->IsGenerating()) + return false; + + const BotProfile *profile = NULL; + + if ( !isFromConsole ) + { + profileName = NULL; + difficulty = GetDifficultyLevel(); + } + else + { + if ( difficulty == NUM_DIFFICULTY_LEVELS ) + { + difficulty = GetDifficultyLevel(); + } + + // if team not specified, check bot_join_team cvar for preference + if (team == TEAM_UNASSIGNED) + { + if (!stricmp( cv_bot_join_team.GetString(), "T" )) + team = TEAM_TERRORIST; + else if (!stricmp( cv_bot_join_team.GetString(), "CT" )) + team = TEAM_CT; + else + team = CSGameRules()->SelectDefaultTeam(); + } + } + + if ( profileName && *profileName ) + { + // in career, ignore humans, since we want to add anyway + bool ignoreHumans = CSGameRules()->IsCareer(); + if (UTIL_IsNameTaken( profileName, ignoreHumans )) + { + if ( isFromConsole ) + { + Msg( "Error - %s is already in the game.\n", profileName ); + } + return true; + } + + // try to add a bot by name + profile = TheBotProfiles->GetProfile( profileName, team ); + if ( !profile ) + { + // try to add a bot by template + profile = TheBotProfiles->GetProfileMatchingTemplate( profileName, team, difficulty ); + if ( !profile ) + { + if ( isFromConsole ) + { + Msg( "Error - no profile for '%s' exists.\n", profileName ); + } + return true; + } + } + } + else + { + // if team not specified, check bot_join_team cvar for preference + if (team == TEAM_UNASSIGNED) + { + if (!stricmp( cv_bot_join_team.GetString(), "T" )) + team = TEAM_TERRORIST; + else if (!stricmp( cv_bot_join_team.GetString(), "CT" )) + team = TEAM_CT; + else + team = CSGameRules()->SelectDefaultTeam(); + } + + profile = TheBotProfiles->GetRandomProfile( difficulty, team, weaponType ); + if (profile == NULL) + { + if ( isFromConsole ) + { + Msg( "All bot profiles at this difficulty level are in use.\n" ); + } + return true; + } + } + + if (team == TEAM_UNASSIGNED || team == TEAM_SPECTATOR) + { + if ( isFromConsole ) + { + Msg( "Could not add bot to the game: The game is full\n" ); + } + return false; + } + + if (CSGameRules()->TeamFull( team )) + { + if ( isFromConsole ) + { + Msg( "Could not add bot to the game: Team is full\n" ); + } + return false; + } + + if (CSGameRules()->TeamStacked( team, TEAM_UNASSIGNED )) + { + if ( isFromConsole ) + { + Msg( "Could not add bot to the game: Team is stacked (to disable this check, set mp_autoteambalance to zero, increase mp_limitteams, and restart the round).\n" ); + } + return false; + } + + // create the actual bot + CCSBot *bot = CreateBot<CCSBot>( profile, team ); + + if (bot == NULL) + { + if ( isFromConsole ) + { + Msg( "Error: CreateBot() failed.\n" ); + } + return false; + } + + if (isFromConsole) + { + // increase the bot quota to account for manually added bot + cv_bot_quota.SetValue( cv_bot_quota.GetInt() + 1 ); + } + + return true; +} + +int UTIL_CSSBotsInGame() +{ + int count = 0; + + for (int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSBot *player = dynamic_cast<CCSBot *>(UTIL_PlayerByIndex( i )); + + if ( player == NULL ) + continue; + + count++; + } + + return count; +} + +bool UTIL_CSSKickBotFromTeam( int kickTeam ) +{ + int i; + + // try to kick a dead bot first + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSBot *player = dynamic_cast<CCSBot *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (!player->IsAlive() && player->GetTeamNumber() == kickTeam) + { + // its a bot on the right team - kick it + engine->ServerCommand( UTIL_VarArgs( "kick \"%s\"\n", player->GetPlayerName() ) ); + + return true; + } + } + + // no dead bots, kick any bot on the given team + for ( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSBot *player = dynamic_cast<CCSBot *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (player->GetTeamNumber() == kickTeam) + { + // its a bot on the right team - kick it + engine->ServerCommand( UTIL_VarArgs( "kick \"%s\"\n", player->GetPlayerName() ) ); + + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Keep a minimum quota of bots in the game + */ +void CCSBotManager::MaintainBotQuota( void ) +{ + if ( !AreBotsAllowed() ) + return; + + if (TheNavMesh->IsGenerating()) + return; + + int totalHumansInGame = UTIL_HumansInGame(); + int humanPlayersInGame = UTIL_HumansInGame( IGNORE_SPECTATORS ); + + // don't add bots until local player has been registered, to make sure he's player ID #1 + if (!engine->IsDedicatedServer() && totalHumansInGame == 0) + return; + + // new players can't spawn immediately after the round has been going for some time + if ( !CSGameRules() || !TheCSBots() ) + { + return; + } + + int desiredBotCount = cv_bot_quota.GetInt(); + int botsInGame = UTIL_CSSBotsInGame(); + + /// isRoundInProgress is true if the round has progressed far enough that new players will join as dead. + bool isRoundInProgress = CSGameRules()->m_bFirstConnected && + !TheCSBots()->IsRoundOver() && + ( CSGameRules()->GetRoundElapsedTime() >= 20.0f ); + + if ( FStrEq( cv_bot_quota_mode.GetString(), "fill" ) ) + { + // If bot_quota_mode is 'fill', we want the number of bots and humans together to equal bot_quota + // unless the round is already in progress, in which case we play with what we've been dealt + if ( !isRoundInProgress ) + { + desiredBotCount = MAX( 0, desiredBotCount - humanPlayersInGame ); + } + else + { + desiredBotCount = botsInGame; + } + } + else if ( FStrEq( cv_bot_quota_mode.GetString(), "match" ) ) + { + // If bot_quota_mode is 'match', we want the number of bots to be bot_quota * total humans + // unless the round is already in progress, in which case we play with what we've been dealt + if ( !isRoundInProgress ) + { + desiredBotCount = (int)MAX( 0, cv_bot_quota.GetFloat() * humanPlayersInGame ); + } + else + { + desiredBotCount = botsInGame; + } + } + + // wait for a player to join, if necessary + if (cv_bot_join_after_player.GetBool()) + { + if (humanPlayersInGame == 0) + desiredBotCount = 0; + } + + // wait until the map has been loaded for a bit, to allow players to transition across + // the transition without missing the pistol round + if ( bot_join_delay.GetInt() > CSGameRules()->GetMapElapsedTime() ) + { + desiredBotCount = 0; + } + + // if bots will auto-vacate, we need to keep one slot open to allow players to join + if (cv_bot_auto_vacate.GetBool()) + desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - (humanPlayersInGame + 1) ); + else + desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - humanPlayersInGame ); + + // Try to balance teams, if we are in the first 20 seconds of a round and bots can join either team. + if ( botsInGame > 0 && desiredBotCount == botsInGame && CSGameRules()->m_bFirstConnected ) + { + if ( CSGameRules()->GetRoundElapsedTime() < 20.0f ) // new bots can still spawn during this time + { + if ( mp_autoteambalance.GetBool() ) + { + int numAliveTerrorist; + int numAliveCT; + int numDeadTerrorist; + int numDeadCT; + CSGameRules()->InitializePlayerCounts( numAliveTerrorist, numAliveCT, numDeadTerrorist, numDeadCT ); + + if ( !FStrEq( cv_bot_join_team.GetString(), "T" ) && + !FStrEq( cv_bot_join_team.GetString(), "CT" ) ) + { + if ( numAliveTerrorist > CSGameRules()->m_iNumCT + 1 ) + { + if ( UTIL_KickBotFromTeam( TEAM_TERRORIST ) ) + return; + } + else if ( numAliveCT > CSGameRules()->m_iNumTerrorist + 1 ) + { + if ( UTIL_KickBotFromTeam( TEAM_CT ) ) + return; + } + } + } + } + } + + // add bots if necessary + if (desiredBotCount > botsInGame) + { + // don't try to add a bot if all teams are full + if (!CSGameRules()->TeamFull( TEAM_TERRORIST ) || !CSGameRules()->TeamFull( TEAM_CT )) + TheCSBots()->BotAddCommand( TEAM_UNASSIGNED ); + } + else if (desiredBotCount < botsInGame) + { + // kick a bot to maintain quota + + // first remove any unassigned bots + if (UTIL_CSSKickBotFromTeam( TEAM_UNASSIGNED )) + return; + + int kickTeam; + + // remove from the team that has more players + if (CSGameRules()->m_iNumTerrorist > CSGameRules()->m_iNumCT) + { + kickTeam = TEAM_TERRORIST; + } + else if (CSGameRules()->m_iNumTerrorist < CSGameRules()->m_iNumCT) + { + kickTeam = TEAM_CT; + } + + // remove from the team that's winning + else if (CSGameRules()->m_iNumTerroristWins > CSGameRules()->m_iNumCTWins) + { + kickTeam = TEAM_TERRORIST; + } + else if (CSGameRules()->m_iNumCTWins > CSGameRules()->m_iNumTerroristWins) + { + kickTeam = TEAM_CT; + } + else + { + // teams and scores are equal, pick a team at random + kickTeam = (RandomInt( 0, 1 ) == 0) ? TEAM_CT : TEAM_TERRORIST; + } + + // attempt to kick a bot from the given team + if (UTIL_CSSKickBotFromTeam( kickTeam )) + return; + + // if there were no bots on the team, kick a bot from the other team + if (kickTeam == TEAM_TERRORIST) + UTIL_CSSKickBotFromTeam( TEAM_CT ); + else + UTIL_CSSKickBotFromTeam( TEAM_TERRORIST ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Collect all nav areas that overlap the given zone + */ +class CollectOverlappingAreas +{ +public: + CollectOverlappingAreas( CCSBotManager::Zone *zone ) + { + m_zone = zone; + + zone->m_areaCount = 0; + } + + bool operator() ( CNavArea *area ) + { + Extent areaExtent; + area->GetExtent(&areaExtent); + + if (areaExtent.hi.x >= m_zone->m_extent.lo.x && areaExtent.lo.x <= m_zone->m_extent.hi.x && + areaExtent.hi.y >= m_zone->m_extent.lo.y && areaExtent.lo.y <= m_zone->m_extent.hi.y && + areaExtent.hi.z >= m_zone->m_extent.lo.z && areaExtent.lo.z <= m_zone->m_extent.hi.z) + { + // area overlaps m_zone + m_zone->m_area[ m_zone->m_areaCount++ ] = area; + if (m_zone->m_areaCount == CCSBotManager::MAX_ZONE_NAV_AREAS) + { + return false; + } + } + + return true; + } + +private: + CCSBotManager::Zone *m_zone; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Search the map entities to determine the game scenario and define important zones. + */ +void CCSBotManager::ExtractScenarioData( void ) +{ + if (!TheNavMesh->IsLoaded()) + return; + + m_zoneCount = 0; + m_gameScenario = SCENARIO_DEATHMATCH; + + + // + // Search all entities in the map and set the game type and + // store all zones (bomb target, etc). + // + CBaseEntity *entity; + int i; + for( i=1; i<gpGlobals->maxEntities; ++i ) + { + entity = CBaseEntity::Instance( engine->PEntityOfEntIndex( i ) ); + + if (entity == NULL) + continue; + + bool found = false; + bool isLegacy = false; + + if (FClassnameIs( entity, "func_bomb_target" )) + { + m_gameScenario = SCENARIO_DEFUSE_BOMB; + found = true; + isLegacy = false; + } + else if (FClassnameIs( entity, "info_bomb_target" )) + { + m_gameScenario = SCENARIO_DEFUSE_BOMB; + found = true; + isLegacy = true; + } + else if (FClassnameIs( entity, "func_hostage_rescue" )) + { + m_gameScenario = SCENARIO_RESCUE_HOSTAGES; + found = true; + isLegacy = false; + } + else if (FClassnameIs( entity, "info_hostage_rescue" )) + { + m_gameScenario = SCENARIO_RESCUE_HOSTAGES; + found = true; + isLegacy = true; + } + else if (FClassnameIs( entity, "hostage_entity" )) + { + // some very old maps (ie: cs_assault) use info_player_start + // as rescue zones, so set the scenario if there are hostages + // in the map + m_gameScenario = SCENARIO_RESCUE_HOSTAGES; + } + else if (FClassnameIs( entity, "func_vip_safetyzone" )) + { + m_gameScenario = SCENARIO_ESCORT_VIP; + found = true; + isLegacy = false; + } + + if (found) + { + if (m_zoneCount < MAX_ZONES) + { + Vector absmin, absmax; + entity->CollisionProp()->WorldSpaceAABB( &absmin, &absmax ); + + m_zone[ m_zoneCount ].m_isBlocked = false; + m_zone[ m_zoneCount ].m_center = (isLegacy) ? entity->GetAbsOrigin() : (absmin + absmax)/2.0f; + m_zone[ m_zoneCount ].m_isLegacy = isLegacy; + m_zone[ m_zoneCount ].m_index = m_zoneCount; + m_zone[ m_zoneCount++ ].m_entity = entity; + } + else + Msg( "Warning: Too many zones, some will be ignored.\n" ); + } + } + + // + // If there are no zones and the scenario is hostage rescue, + // use the info_player_start entities as rescue zones. + // + if (m_zoneCount == 0 && m_gameScenario == SCENARIO_RESCUE_HOSTAGES) + { + for( entity = gEntList.FindEntityByClassname( NULL, "info_player_start" ); + entity && !FNullEnt( entity->edict() ); + entity = gEntList.FindEntityByClassname( entity, "info_player_start" ) ) + { + if (m_zoneCount < MAX_ZONES) + { + m_zone[ m_zoneCount ].m_isBlocked = false; + m_zone[ m_zoneCount ].m_center = entity->GetAbsOrigin(); + m_zone[ m_zoneCount ].m_isLegacy = true; + m_zone[ m_zoneCount ].m_index = m_zoneCount; + m_zone[ m_zoneCount++ ].m_entity = entity; + } + else + { + Msg( "Warning: Too many zones, some will be ignored.\n" ); + } + } + } + + // + // Collect nav areas that overlap each zone + // + for( i=0; i<m_zoneCount; ++i ) + { + Zone *zone = &m_zone[i]; + + if (zone->m_isLegacy) + { + const float legacyRange = 256.0f; + zone->m_extent.lo.x = zone->m_center.x - legacyRange; + zone->m_extent.lo.y = zone->m_center.y - legacyRange; + zone->m_extent.lo.z = zone->m_center.z - legacyRange; + zone->m_extent.hi.x = zone->m_center.x + legacyRange; + zone->m_extent.hi.y = zone->m_center.y + legacyRange; + zone->m_extent.hi.z = zone->m_center.z + legacyRange; + } + else + { + Vector absmin, absmax; + zone->m_entity->CollisionProp()->WorldSpaceAABB( &absmin, &absmax ); + + zone->m_extent.lo = absmin; + zone->m_extent.hi = absmax; + } + + // ensure Z overlap + const float zFudge = 50.0f; + zone->m_extent.lo.z -= zFudge; + zone->m_extent.hi.z += zFudge; + + // build a list of nav areas that overlap this zone + CollectOverlappingAreas collector( zone ); + TheNavMesh->ForAllAreas( collector ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the zone that contains the given position + */ +const CCSBotManager::Zone *CCSBotManager::GetZone( const Vector &pos ) const +{ + for( int z=0; z<m_zoneCount; ++z ) + { + if (m_zone[z].m_extent.Contains( pos )) + { + return &m_zone[z]; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest zone to the given position + */ +const CCSBotManager::Zone *CCSBotManager::GetClosestZone( const Vector &pos ) const +{ + const Zone *close = NULL; + float closeRangeSq = 999999999.9f; + + for( int z=0; z<m_zoneCount; ++z ) + { + if ( m_zone[z].m_isBlocked ) + continue; + + float rangeSq = (m_zone[z].m_center - pos).LengthSqr(); + + if (rangeSq < closeRangeSq) + { + closeRangeSq = rangeSq; + close = &m_zone[z]; + } + } + + return close; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return a random position inside the given zone + */ +const Vector *CCSBotManager::GetRandomPositionInZone( const Zone *zone ) const +{ + static Vector pos; + + if (zone == NULL) + return NULL; + + if (zone->m_areaCount == 0) + return NULL; + + // pick a random overlapping area + CNavArea *area = GetRandomAreaInZone(zone); + + // pick a location inside both the nav area and the zone + /// @todo Randomize this + + if (zone->m_isLegacy) + { + /// @todo It is possible that the radius might not overlap this area at all... + area->GetClosestPointOnArea( zone->m_center, &pos ); + } + else + { + Extent areaExtent; + area->GetExtent(&areaExtent); + Extent overlap; + overlap.lo.x = MAX( areaExtent.lo.x, zone->m_extent.lo.x ); + overlap.lo.y = MAX( areaExtent.lo.y, zone->m_extent.lo.y ); + overlap.hi.x = MIN( areaExtent.hi.x, zone->m_extent.hi.x ); + overlap.hi.y = MIN( areaExtent.hi.y, zone->m_extent.hi.y ); + + pos.x = (overlap.lo.x + overlap.hi.x)/2.0f; + pos.y = (overlap.lo.y + overlap.hi.y)/2.0f; + pos.z = area->GetZ( pos ); + } + + return &pos; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return a random area inside the given zone + */ +CNavArea *CCSBotManager::GetRandomAreaInZone( const Zone *zone ) const +{ + int areaCount = zone->m_areaCount; + if( areaCount == 0 ) + { + assert( false && "CCSBotManager::GetRandomAreaInZone: No areas for this zone" ); + return NULL; + } + + // Random, but weighted. Jump areas score zero, since you aren't ever meant to stop on one of those. + // Avoid areas score 1 to a normal area's 20 because pathfinding treats Avoid as a 20x penalty. + int totalWeight = 0; + for( int areaIndex = 0; areaIndex < areaCount; areaIndex++ ) + { + CNavArea *currentArea = zone->m_area[areaIndex]; + if( currentArea->GetAttributes() & NAV_MESH_JUMP ) + totalWeight += 0; + else if( currentArea->GetAttributes() & NAV_MESH_AVOID ) + totalWeight += 1; + else + totalWeight += 20; + } + + if( totalWeight == 0 ) + { + assert( false && "CCSBotManager::GetRandomAreaInZone: No real areas for this zone" ); + return NULL; + } + + int randomPick = RandomInt( 1, totalWeight ); + + for( int areaIndex = 0; areaIndex < areaCount; areaIndex++ ) + { + CNavArea *currentArea = zone->m_area[areaIndex]; + if( currentArea->GetAttributes() & NAV_MESH_JUMP ) + randomPick -= 0; + else if( currentArea->GetAttributes() & NAV_MESH_AVOID ) + randomPick -= 1; + else + randomPick -= 20; + + if( randomPick <= 0 ) + return currentArea; + } + + // Won't ever get here, but the compiler will cry without it. + return zone->m_area[0]; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnServerShutdown( IGameEvent *event ) +{ + if ( !engine->IsDedicatedServer() ) + { + // Since we're a listenserver, save some config info for the next time we start up + static const char *botVars[] = + { + "bot_quota", + "bot_difficulty", + "bot_chatter", + "bot_prefix", + "bot_join_team", + "bot_defer_to_human", +#ifdef CS_SHIELD_ENABLED + "bot_allow_shield", +#endif // CS_SHIELD_ENABLED + "bot_join_after_player", + "bot_allow_rogues", + "bot_allow_pistols", + "bot_allow_shotguns", + "bot_allow_sub_machine_guns", + "bot_allow_machine_guns", + "bot_allow_rifles", + "bot_allow_snipers", + "bot_allow_grenades" + }; + + KeyValues *data = new KeyValues( "ServerConfig" ); + + // load the config data + if (data) + { + data->LoadFromFile( filesystem, "ServerConfig.vdf", "GAME" ); + for ( int i=0; i<sizeof(botVars)/sizeof(botVars[0]); ++i ) + { + const char *varName = botVars[i]; + if ( varName ) + { + ConVar *var = cvar->FindVar( varName ); + if ( var ) + { + data->SetString( varName, var->GetString() ); + } + } + } + data->SaveToFile( filesystem, "ServerConfig.vdf", "GAME" ); + data->deleteThis(); + } + return; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerFootstep( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerFootstep, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerRadio( IGameEvent *event ) +{ + // if it's an Enemy Spotted radio, update our enemy spotted timestamp + if ( event->GetInt( "slot" ) == RADIO_ENEMY_SPOTTED ) + { + // to have some idea of when a human Player has seen an enemy + SetLastSeenEnemyTimestamp(); + } + + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerRadio, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerDeath( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerDeath, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnPlayerFallDamage( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnPlayerFallDamage, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombPickedUp( IGameEvent *event ) +{ + // bomb no longer loose + SetLooseBomb( NULL ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBombPickedUp, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombPlanted( IGameEvent *event ) +{ + m_isBombPlanted = true; + m_bombPlantTimestamp = gpGlobals->curtime; + + CCSBOTMANAGER_ITERATE_BOTS( OnBombPlanted, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombBeep( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnBombBeep, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombDefuseBegin( IGameEvent *event ) +{ + m_bombDefuser = static_cast<CCSPlayer *>( UTIL_PlayerByUserId( event->GetInt( "userid" ) ) ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBombDefuseBegin, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombDefused( IGameEvent *event ) +{ + m_isBombPlanted = false; + m_bombDefuser = NULL; + + CCSBOTMANAGER_ITERATE_BOTS( OnBombDefused, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombDefuseAbort( IGameEvent *event ) +{ + m_bombDefuser = NULL; + + CCSBOTMANAGER_ITERATE_BOTS( OnBombDefuseAbort, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBombExploded( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnBombExploded, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnRoundEnd( IGameEvent *event ) +{ + m_isRoundOver = true; + + CCSBOTMANAGER_ITERATE_BOTS( OnRoundEnd, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnRoundStart( IGameEvent *event ) +{ + RestartRound(); + + CCSBOTMANAGER_ITERATE_BOTS( OnRoundStart, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +static CBaseEntity * SelectSpawnSpot( const char *pEntClassName ) +{ + CBaseEntity* pSpot = NULL; + + // Find the next spawn spot. + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + if ( pSpot == NULL ) // skip over the null point + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + + CBaseEntity *pFirstSpot = pSpot; + do + { + if ( pSpot ) + { + // check if pSpot is valid + if ( pSpot->GetAbsOrigin() == Vector( 0, 0, 0 ) ) + { + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + continue; + } + + // if so, go to pSpot + return pSpot; + } + // increment pSpot + pSpot = gEntList.FindEntityByClassname( pSpot, pEntClassName ); + } while ( pSpot != pFirstSpot ); // loop if we're not back to the start + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Pathfind from each zone to a spawn point to ensure it is valid. Assumes that every spawn can pathfind to + * every other spawn. + */ +void CCSBotManager::CheckForBlockedZones( void ) +{ + CBaseEntity *pSpot = SelectSpawnSpot( "info_player_counterterrorist" ); + if ( !pSpot ) + pSpot = SelectSpawnSpot( "info_player_terrorist" ); + + if ( !pSpot ) + return; + + Vector spawnPos = pSpot->GetAbsOrigin(); + CNavArea *spawnArea = TheNavMesh->GetNearestNavArea( spawnPos ); + if ( !spawnArea ) + return; + + ShortestPathCost costFunc; + + for( int i=0; i<m_zoneCount; ++i ) + { + if (m_zone[i].m_areaCount == 0) + continue; + + // just use the first overlapping nav area as a reasonable approximation + float dist = NavAreaTravelDistance( spawnArea, m_zone[i].m_area[0], costFunc ); + m_zone[i].m_isBlocked = (dist < 0.0f ); + + if ( cv_bot_debug.GetInt() == 5 ) + { + if ( m_zone[i].m_isBlocked ) + DevMsg( "%.1f: Zone %d, area %d (%.0f %.0f %.0f) is blocked from spawn area %d (%.0f %.0f %.0f)\n", + gpGlobals->curtime, i, m_zone[i].m_area[0]->GetID(), + m_zone[i].m_area[0]->GetCenter().x, m_zone[i].m_area[0]->GetCenter().y, m_zone[i].m_area[0]->GetCenter().z, + spawnArea->GetID(), + spawnPos.x, spawnPos.y, spawnPos.z ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnRoundFreezeEnd( IGameEvent *event ) +{ + bool reenableEvents = m_NavBlockedEvent.IsEnabled(); + + m_NavBlockedEvent.Enable( false ); // don't listen to nav_blocked events - there could be several, and we don't have bots pathing + CUtlVector< CNavArea * >& transientAreas = TheNavMesh->GetTransientAreas(); + for ( int i=0; i<transientAreas.Count(); ++i ) + { + CNavArea *area = transientAreas[i]; + if ( area->GetAttributes() & NAV_MESH_TRANSIENT ) + { + area->UpdateBlocked(); + } + } + if ( reenableEvents ) + { + m_NavBlockedEvent.Enable( true ); + } + + CheckForBlockedZones(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnNavBlocked( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnNavBlocked, event ); + CheckForBlockedZones(); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnDoorMoving( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnDoorMoving, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check all nav areas inside the breakable's extent to see if players would now fall through + */ +class CheckAreasOverlappingBreakable +{ +public: + CheckAreasOverlappingBreakable( CBaseEntity *breakable ) + { + m_breakable = breakable; + ICollideable *collideable = breakable->GetCollideable(); + collideable->WorldSpaceSurroundingBounds( &m_breakableExtent.lo, &m_breakableExtent.hi ); + + const float expand = 10.0f; + m_breakableExtent.lo += Vector( -expand, -expand, -expand ); + m_breakableExtent.hi += Vector( expand, expand, expand ); + } + + bool operator() ( CNavArea *area ) + { + Extent areaExtent; + area->GetExtent(&areaExtent); + + if (areaExtent.hi.x >= m_breakableExtent.lo.x && areaExtent.lo.x <= m_breakableExtent.hi.x && + areaExtent.hi.y >= m_breakableExtent.lo.y && areaExtent.lo.y <= m_breakableExtent.hi.y && + areaExtent.hi.z >= m_breakableExtent.lo.z && areaExtent.lo.z <= m_breakableExtent.hi.z) + { + // area overlaps the breakable + area->CheckFloor( m_breakable ); + } + + return true; + } + +private: + Extent m_breakableExtent; + CBaseEntity *m_breakable; +}; + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBreakBreakable( IGameEvent *event ) +{ + CheckAreasOverlappingBreakable collector( UTIL_EntityByIndex( event->GetInt( "entindex" ) ) ); + TheNavMesh->ForAllAreas( collector ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBreakBreakable, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBreakProp( IGameEvent *event ) +{ + CheckAreasOverlappingBreakable collector( UTIL_EntityByIndex( event->GetInt( "entindex" ) ) ); + TheNavMesh->ForAllAreas( collector ); + + CCSBOTMANAGER_ITERATE_BOTS( OnBreakProp, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnHostageFollows( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnHostageFollows, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnHostageRescuedAll( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnHostageRescuedAll, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponFire( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponFire, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponFireOnEmpty( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponFireOnEmpty, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponReload( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponReload, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnWeaponZoom( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnWeaponZoom, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnBulletImpact( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnBulletImpact, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnHEGrenadeDetonate( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnHEGrenadeDetonate, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnFlashbangDetonate( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnFlashbangDetonate, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnSmokeGrenadeDetonate( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnSmokeGrenadeDetonate, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::OnGrenadeBounce( IGameEvent *event ) +{ + CCSBOTMANAGER_ITERATE_BOTS( OnGrenadeBounce, event ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Get the time remaining before the planted bomb explodes + */ +float CCSBotManager::GetBombTimeLeft( void ) const +{ + return (mp_c4timer.GetFloat() - (gpGlobals->curtime - m_bombPlantTimestamp)); +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBotManager::SetLooseBomb( CBaseEntity *bomb ) +{ + m_looseBomb = bomb; + + if (bomb) + { + m_looseBombArea = TheNavMesh->GetNearestNavArea( bomb->GetAbsOrigin() ); + } + else + { + m_looseBombArea = NULL; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if player is important to scenario (VIP, bomb carrier, etc) + */ +bool CCSBotManager::IsImportantPlayer( CCSPlayer *player ) const +{ + switch (GetScenario()) + { + case SCENARIO_DEFUSE_BOMB: + { + if (player->GetTeamNumber() == TEAM_TERRORIST && player->HasC4()) + return true; + + /// @todo TEAM_CT's defusing the bomb are important + + return false; + } + + case SCENARIO_ESCORT_VIP: + { + if (player->GetTeamNumber() == TEAM_CT && player->IsVIP()) + return true; + + return false; + } + + case SCENARIO_RESCUE_HOSTAGES: + { + /// @todo TEAM_CT's escorting hostages are important + return false; + } + } + + // everyone is equally important in a deathmatch + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return priority of player (0 = max pri) + */ +unsigned int CCSBotManager::GetPlayerPriority( CBasePlayer *player ) const +{ + const unsigned int lowestPriority = 0xFFFFFFFF; + + if (!player->IsPlayer()) + return lowestPriority; + + // human players have highest priority + if (!player->IsBot()) + return 0; + + CCSBot *bot = dynamic_cast<CCSBot *>( player ); + + if ( !bot ) + return 0; + + // bots doing something important for the current scenario have high priority + switch (GetScenario()) + { + case SCENARIO_DEFUSE_BOMB: + { + // the bomb carrier has high priority + if (bot->GetTeamNumber() == TEAM_TERRORIST && bot->HasC4()) + return 1; + + break; + } + + case SCENARIO_ESCORT_VIP: + { + // the VIP has high priority + if (bot->GetTeamNumber() == TEAM_CT && bot->m_bIsVIP) + return 1; + + break; + } + + case SCENARIO_RESCUE_HOSTAGES: + { + // TEAM_CT's rescuing hostages have high priority + if (bot->GetTeamNumber() == TEAM_CT && bot->GetHostageEscortCount()) + return 1; + + break; + } + } + + // everyone else is ranked by their unique ID (which cannot be zero) + return 1 + bot->GetID(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns a random spawn point for the given team (no arg means use both team spawnpoints) + */ +CBaseEntity *CCSBotManager::GetRandomSpawn( int team ) const +{ + CUtlVector< CBaseEntity * > spawnSet; + CBaseEntity *spot; + + if (team == TEAM_TERRORIST || team == TEAM_MAXCOUNT) + { + // collect T spawns + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_terrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_terrorist" ) ) + { + spawnSet.AddToTail( spot ); + } + } + + if (team == TEAM_CT || team == TEAM_MAXCOUNT) + { + // collect CT spawns + for( spot = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); + spot; + spot = gEntList.FindEntityByClassname( spot, "info_player_counterterrorist" ) ) + { + spawnSet.AddToTail( spot ); + } + } + + if (spawnSet.Count() == 0) + { + return NULL; + } + + // select one at random + int which = RandomInt( 0, spawnSet.Count()-1 ); + return spawnSet[ which ]; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the last time the given radio message was sent for given team + * 'teamID' can be TEAM_CT or TEAM_TERRORIST + */ +float CCSBotManager::GetRadioMessageTimestamp( RadioType event, int teamID ) const +{ + int i = (teamID == TEAM_TERRORIST) ? 0 : 1; + + if (event > RADIO_START_1 && event < RADIO_END) + return m_radioMsgTimestamp[ event - RADIO_START_1 ][ i ]; + + return 0.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the interval since the last time this message was sent + */ +float CCSBotManager::GetRadioMessageInterval( RadioType event, int teamID ) const +{ + int i = (teamID == TEAM_TERRORIST) ? 0 : 1; + + if (event > RADIO_START_1 && event < RADIO_END) + return gpGlobals->curtime - m_radioMsgTimestamp[ event - RADIO_START_1 ][ i ]; + + return 99999999.9f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the given radio message timestamp. + * 'teamID' can be TEAM_CT or TEAM_TERRORIST + */ +void CCSBotManager::SetRadioMessageTimestamp( RadioType event, int teamID ) +{ + int i = (teamID == TEAM_TERRORIST) ? 0 : 1; + + if (event > RADIO_START_1 && event < RADIO_END) + m_radioMsgTimestamp[ event - RADIO_START_1 ][ i ] = gpGlobals->curtime; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset all radio message timestamps + */ +void CCSBotManager::ResetRadioMessageTimestamps( void ) +{ + for( int t=0; t<2; ++t ) + { + for( int m=0; m<(RADIO_END - RADIO_START_1); ++m ) + m_radioMsgTimestamp[ m ][ t ] = 0.0f; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Display nav areas as they become reachable by each team + */ +void DrawOccupyTime( void ) +{ + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + int r, g, b; + + if (TheCSBots()->GetElapsedRoundTime() > area->GetEarliestOccupyTime( TEAM_TERRORIST )) + { + if (TheCSBots()->GetElapsedRoundTime() > area->GetEarliestOccupyTime( TEAM_CT )) + { + r = 255; g = 0; b = 255; + } + else + { + r = 255; g = 0; b = 0; + } + } + else if (TheCSBots()->GetElapsedRoundTime() > area->GetEarliestOccupyTime( TEAM_CT )) + { + r = 0; g = 0; b = 255; + } + else + { + continue; + } + + const Vector &nw = area->GetCorner( NORTH_WEST ); + const Vector &ne = area->GetCorner( NORTH_EAST ); + const Vector &sw = area->GetCorner( SOUTH_WEST ); + const Vector &se = area->GetCorner( SOUTH_EAST ); + + NDebugOverlay::Line( nw, ne, r, g, b, true, 0.1f ); + NDebugOverlay::Line( nw, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, ne, r, g, b, true, 0.1f ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Display areas where players will likely have initial battles + */ +void DrawBattlefront( void ) +{ + const float epsilon = 1.0f; + int r = 255, g = 50, b = 0; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + if ( fabs(area->GetEarliestOccupyTime( TEAM_TERRORIST ) - area->GetEarliestOccupyTime( TEAM_CT )) > epsilon ) + { + continue; + } + + + const Vector &nw = area->GetCorner( NORTH_WEST ); + const Vector &ne = area->GetCorner( NORTH_EAST ); + const Vector &sw = area->GetCorner( SOUTH_WEST ); + const Vector &se = area->GetCorner( SOUTH_EAST ); + + NDebugOverlay::Line( nw, ne, r, g, b, true, 0.1f ); + NDebugOverlay::Line( nw, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, sw, r, g, b, true, 0.1f ); + NDebugOverlay::Line( se, ne, r, g, b, true, 0.1f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +static bool CheckAreaAgainstAllZoneAreas(CNavArea *queryArea) +{ + // A marked area means they just want to double check this one spot + int goalZoneCount = TheCSBots()->GetZoneCount(); + + for( int zoneIndex = 0; zoneIndex < goalZoneCount; zoneIndex++ ) + { + const CCSBotManager::Zone *currentZone = TheCSBots()->GetZone(zoneIndex); + + int zoneAreaCount = currentZone->m_areaCount; + for( int areaIndex = 0; areaIndex < zoneAreaCount; areaIndex++ ) + { + CNavArea *zoneArea = currentZone->m_area[areaIndex]; + // We need to be connected to every area in the zone, since we don't know what other code might pick for an area + ShortestPathCost cost; + if( NavAreaTravelDistance(queryArea, zoneArea, cost) == -1.0f ) + { + Msg( "Area #%d is disconnected from goal area #%d.\n", + queryArea->GetID(), + zoneArea->GetID() + ); + return false; + } + } + + } + return true; +} + +CON_COMMAND_F( nav_check_connectivity, "Checks to be sure every (or just the marked) nav area can get to every goal area for the map (hostages or bomb site).", FCVAR_CHEAT ) +{ + //Nav command in here since very CS specific. + + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + if ( TheNavMesh->GetMarkedArea() ) + { + CNavArea *markedArea = TheNavMesh->GetMarkedArea(); + bool fine = CheckAreaAgainstAllZoneAreas( markedArea ); + if( fine ) + { + Msg( "Area #%d is connected to all goal areas.\n", markedArea->GetID() ); + } + } + else + { + // Otherwise, loop through every area, and make sure they can all get to the goal. + float start = engine->Time(); + FOR_EACH_VEC( TheNavAreas, nit ) + { + CheckAreaAgainstAllZoneAreas(TheNavAreas[ nit ]); + } + + float end = engine->Time(); + float time = (end - start) * 1000.0f; + Msg( "nav_check_connectivity took %2.2f ms\n", time ); + } +} + + + + diff --git a/game/server/cstrike/bot/cs_bot_manager.h b/game/server/cstrike/bot/cs_bot_manager.h new file mode 100644 index 0000000..3f1da76 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_manager.h @@ -0,0 +1,400 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef CS_CONTROL_H +#define CS_CONTROL_H + + +#include "bot_manager.h" +#include "nav_area.h" +#include "bot_util.h" +#include "bot_profile.h" +#include "cs_shareddefs.h" +#include "cs_player.h" + +extern ConVar friendlyfire; + +class CBasePlayerWeapon; + +/** + * Given one team, return the other + */ +inline int OtherTeam( int team ) +{ + return (team == TEAM_TERRORIST) ? TEAM_CT : TEAM_TERRORIST; +} + +class CCSBotManager; + +// accessor for CS-specific bots +inline CCSBotManager *TheCSBots( void ) +{ + return reinterpret_cast< CCSBotManager * >( TheBots ); +} + +//-------------------------------------------------------------------------------------------------------------- +class BotEventInterface : public IGameEventListener2 +{ +public: + virtual const char *GetEventName( void ) const = 0; +}; + +//-------------------------------------------------------------------------------------------------------------- +//-------------------------------------------------------------------------------------------------------------- +/** + * Macro to set up an OnEventClass() in TheCSBots. + */ +#define DECLARE_BOTMANAGER_EVENT_LISTENER( BotManagerSingleton, EventClass, EventName ) \ + public: \ + virtual void On##EventClass( IGameEvent *data ); \ + private: \ + class EventClass##Event : public BotEventInterface \ + { \ + bool m_enabled; \ + public: \ + EventClass##Event( void ) \ + { \ + gameeventmanager->AddListener( this, #EventName, true ); \ + m_enabled = true; \ + } \ + ~EventClass##Event( void ) \ + { \ + if ( m_enabled ) gameeventmanager->RemoveListener( this ); \ + } \ + virtual const char *GetEventName( void ) const \ + { \ + return #EventName; \ + } \ + void Enable( bool enable ) \ + { \ + m_enabled = enable; \ + if ( enable ) \ + gameeventmanager->AddListener( this, #EventName, true ); \ + else \ + gameeventmanager->RemoveListener( this ); \ + } \ + bool IsEnabled( void ) const { return m_enabled; } \ + void FireGameEvent( IGameEvent *event ) \ + { \ + BotManagerSingleton()->On##EventClass( event ); \ + } \ + }; \ + EventClass##Event m_##EventClass##Event; + + +//-------------------------------------------------------------------------------------------------------------- +#define DECLARE_CSBOTMANAGER_EVENT_LISTENER( EventClass, EventName ) DECLARE_BOTMANAGER_EVENT_LISTENER( TheCSBots, EventClass, EventName ) + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Macro to propogate an event from the bot manager to all bots + */ +#define CCSBOTMANAGER_ITERATE_BOTS( Callback, arg1 ) \ + { \ + for ( int idx = 1; idx <= gpGlobals->maxClients; ++idx ) \ + { \ + CBasePlayer *player = UTIL_PlayerByIndex( idx ); \ + if (player == NULL) continue; \ + if (!player->IsBot()) continue; \ + CCSBot *bot = dynamic_cast< CCSBot * >(player); \ + if ( !bot ) continue; \ + bot->Callback( arg1 ); \ + } \ + } + + +//-------------------------------------------------------------------------------------------------------------- +// +// The manager for Counter-Strike specific bots +// +class CCSBotManager : public CBotManager +{ +public: + CCSBotManager(); + + virtual CBasePlayer *AllocateBotEntity( void ); ///< factory method to allocate the appropriate entity for the bot + + virtual void ClientDisconnect( CBaseEntity *entity ); + virtual bool ClientCommand( CBasePlayer *player, const CCommand &args ); + + virtual void ServerActivate( void ); + virtual void ServerDeactivate( void ); + virtual bool ServerCommand( const char *cmd ); + bool IsServerActive( void ) const { return m_serverActive; } + + virtual void RestartRound( void ); ///< (EXTEND) invoked when a new round begins + virtual void StartFrame( void ); ///< (EXTEND) called each frame + + virtual unsigned int GetPlayerPriority( CBasePlayer *player ) const; ///< return priority of player (0 = max pri) + virtual bool IsImportantPlayer( CCSPlayer *player ) const; ///< return true if player is important to scenario (VIP, bomb carrier, etc) + + void ExtractScenarioData( void ); ///< search the map entities to determine the game scenario and define important zones + + // difficulty levels ----------------------------------------------------------------------------------------- + static BotDifficultyType GetDifficultyLevel( void ) + { + if (cv_bot_difficulty.GetFloat() < 0.9f) + return BOT_EASY; + if (cv_bot_difficulty.GetFloat() < 1.9f) + return BOT_NORMAL; + if (cv_bot_difficulty.GetFloat() < 2.9f) + return BOT_HARD; + + return BOT_EXPERT; + } + + // the supported game scenarios ------------------------------------------------------------------------------ + enum GameScenarioType + { + SCENARIO_DEATHMATCH, + SCENARIO_DEFUSE_BOMB, + SCENARIO_RESCUE_HOSTAGES, + SCENARIO_ESCORT_VIP + }; + GameScenarioType GetScenario( void ) const { return m_gameScenario; } + + // "zones" --------------------------------------------------------------------------------------------------- + // depending on the game mode, these are bomb zones, rescue zones, etc. + + enum { MAX_ZONES = 4 }; ///< max # of zones in a map + enum { MAX_ZONE_NAV_AREAS = 16 }; ///< max # of nav areas in a zone + struct Zone + { + CBaseEntity *m_entity; ///< the map entity + CNavArea *m_area[ MAX_ZONE_NAV_AREAS ]; ///< nav areas that overlap this zone + int m_areaCount; + Vector m_center; + bool m_isLegacy; ///< if true, use pev->origin and 256 unit radius as zone + int m_index; + bool m_isBlocked; + Extent m_extent; + }; + + const Zone *GetZone( int i ) const { return &m_zone[i]; } + const Zone *GetZone( const Vector &pos ) const; ///< return the zone that contains the given position + const Zone *GetClosestZone( const Vector &pos ) const; ///< return the closest zone to the given position + const Zone *GetClosestZone( const CBaseEntity *entity ) const; ///< return the closest zone to the given entity + int GetZoneCount( void ) const { return m_zoneCount; } + void CheckForBlockedZones( void ); + + + const Vector *GetRandomPositionInZone( const Zone *zone ) const; ///< return a random position inside the given zone + CNavArea *GetRandomAreaInZone( const Zone *zone ) const; ///< return a random area inside the given zone + + /** + * Return the zone closest to the given position, using the given cost heuristic + */ + template< typename CostFunctor > + const Zone *GetClosestZone( CNavArea *startArea, CostFunctor costFunc, float *travelDistance = NULL ) const + { + const Zone *closeZone = NULL; + float closeDist = 99999999.9f; + + if (startArea == NULL) + return NULL; + + for( int i=0; i<m_zoneCount; ++i ) + { + if (m_zone[i].m_areaCount == 0) + continue; + + if ( m_zone[i].m_isBlocked ) + continue; + + // just use the first overlapping nav area as a reasonable approximation + float dist = NavAreaTravelDistance( startArea, m_zone[i].m_area[0], costFunc ); + + if (dist >= 0.0f && dist < closeDist) + { + closeZone = &m_zone[i]; + closeDist = dist; + } + } + + if (travelDistance) + *travelDistance = closeDist; + + return closeZone; + } + + /// pick a zone at random and return it + const Zone *GetRandomZone( void ) const + { + if (m_zoneCount == 0) + return NULL; + + int i; + CUtlVector< const Zone * > unblockedZones; + for ( i=0; i<m_zoneCount; ++i ) + { + if ( m_zone[i].m_isBlocked ) + continue; + + unblockedZones.AddToTail( &(m_zone[i]) ); + } + + if ( unblockedZones.Count() == 0 ) + return NULL; + + return unblockedZones[ RandomInt( 0, unblockedZones.Count()-1 ) ]; + } + + + /// returns a random spawn point for the given team (no arg means use both team spawnpoints) + CBaseEntity *GetRandomSpawn( int team = TEAM_MAXCOUNT ) const; + + + bool IsBombPlanted( void ) const { return m_isBombPlanted; } ///< returns true if bomb has been planted + float GetBombPlantTimestamp( void ) const { return m_bombPlantTimestamp; } ///< return time bomb was planted + bool IsTimeToPlantBomb( void ) const; ///< return true if it's ok to try to plant bomb + CCSPlayer *GetBombDefuser( void ) const { return m_bombDefuser; } ///< return the player currently defusing the bomb, or NULL + float GetBombTimeLeft( void ) const; ///< get the time remaining before the planted bomb explodes + CBaseEntity *GetLooseBomb( void ) { return m_looseBomb; } ///< return the bomb if it is loose on the ground + CNavArea *GetLooseBombArea( void ) const { return m_looseBombArea; } ///< return area that bomb is in/near + void SetLooseBomb( CBaseEntity *bomb ); + + + float GetRadioMessageTimestamp( RadioType event, int teamID ) const; ///< return the last time the given radio message was sent for given team + float GetRadioMessageInterval( RadioType event, int teamID ) const; ///< return the interval since the last time this message was sent + void SetRadioMessageTimestamp( RadioType event, int teamID ); + void ResetRadioMessageTimestamps( void ); + + float GetLastSeenEnemyTimestamp( void ) const { return m_lastSeenEnemyTimestamp; } ///< return the last time anyone has seen an enemy + void SetLastSeenEnemyTimestamp( void ) { m_lastSeenEnemyTimestamp = gpGlobals->curtime; } + + float GetRoundStartTime( void ) const { return m_roundStartTimestamp; } + float GetElapsedRoundTime( void ) const { return gpGlobals->curtime - m_roundStartTimestamp; } ///< return the elapsed time since the current round began + + bool AllowRogues( void ) const { return cv_bot_allow_rogues.GetBool(); } + bool AllowPistols( void ) const { return cv_bot_allow_pistols.GetBool(); } + bool AllowShotguns( void ) const { return cv_bot_allow_shotguns.GetBool(); } + bool AllowSubMachineGuns( void ) const { return cv_bot_allow_sub_machine_guns.GetBool(); } + bool AllowRifles( void ) const { return cv_bot_allow_rifles.GetBool(); } + bool AllowMachineGuns( void ) const { return cv_bot_allow_machine_guns.GetBool(); } + bool AllowGrenades( void ) const { return cv_bot_allow_grenades.GetBool(); } + bool AllowSnipers( void ) const { return cv_bot_allow_snipers.GetBool(); } +#ifdef CS_SHIELD_ENABLED + bool AllowTacticalShield( void ) const { return cv_bot_allow_shield.GetBool(); } +#else + bool AllowTacticalShield( void ) const { return false; } +#endif // CS_SHIELD_ENABLED + + bool AllowFriendlyFireDamage( void ) const { return friendlyfire.GetBool(); } + + bool IsWeaponUseable( const CWeaponCSBase *weapon ) const; ///< return true if the bot can use this weapon + + bool IsDefenseRushing( void ) const { return m_isDefenseRushing; } ///< returns true if defense team has "decided" to rush this round + bool IsOnDefense( const CCSPlayer *player ) const; ///< return true if this player is on "defense" + bool IsOnOffense( const CCSPlayer *player ) const; ///< return true if this player is on "offense" + + bool IsRoundOver( void ) const { return m_isRoundOver; } ///< return true if the round has ended + + #define FROM_CONSOLE true + bool BotAddCommand( int team, bool isFromConsole = false, const char *profileName = NULL, CSWeaponType weaponType = WEAPONTYPE_UNKNOWN, BotDifficultyType difficulty = NUM_DIFFICULTY_LEVELS ); ///< process the "bot_add" console command + +private: + enum SkillType { LOW, AVERAGE, HIGH, RANDOM }; + + void MaintainBotQuota( void ); + + static bool m_isMapDataLoaded; ///< true if we've attempted to load map data + bool m_serverActive; ///< true between ServerActivate() and ServerDeactivate() + + GameScenarioType m_gameScenario; ///< what kind of game are we playing + + Zone m_zone[ MAX_ZONES ]; + int m_zoneCount; + + bool m_isBombPlanted; ///< true if bomb has been planted + float m_bombPlantTimestamp; ///< time bomb was planted + float m_earliestBombPlantTimestamp; ///< don't allow planting until after this time has elapsed + CCSPlayer *m_bombDefuser; ///< the player currently defusing a bomb + EHANDLE m_looseBomb; ///< will be non-NULL if bomb is loose on the ground + CNavArea *m_looseBombArea; ///< area that bomb is is/near + + bool m_isRoundOver; ///< true if the round has ended + + CountdownTimer m_checkTransientAreasTimer; ///< when elapsed, all transient nav areas should be checked for blockage + + float m_radioMsgTimestamp[ RADIO_END - RADIO_START_1 ][ 2 ]; + + float m_lastSeenEnemyTimestamp; + float m_roundStartTimestamp; ///< the time when the current round began + + bool m_isDefenseRushing; ///< whether defensive team is rushing this round or not + + // Event Handlers -------------------------------------------------------------------------------------------- + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerFootstep, player_footstep ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerRadio, player_radio ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerDeath, player_death ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( PlayerFallDamage, player_falldamage ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombPickedUp, bomb_pickup ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombPlanted, bomb_planted ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombBeep, bomb_beep ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombDefuseBegin, bomb_begindefuse ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombDefused, bomb_defused ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombDefuseAbort, bomb_abortdefuse ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BombExploded, bomb_exploded ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( RoundEnd, round_end ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( RoundStart, round_start ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( RoundFreezeEnd, round_freeze_end ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( DoorMoving, door_moving ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BreakProp, break_prop ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BreakBreakable, break_breakable ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( HostageFollows, hostage_follows ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( HostageRescuedAll, hostage_rescued_all ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponFire, weapon_fire ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponFireOnEmpty, weapon_fire_on_empty ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponReload, weapon_reload ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( WeaponZoom, weapon_zoom ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( BulletImpact, bullet_impact ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( HEGrenadeDetonate, hegrenade_detonate ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( FlashbangDetonate, flashbang_detonate ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( SmokeGrenadeDetonate, smokegrenade_detonate ) + DECLARE_CSBOTMANAGER_EVENT_LISTENER( GrenadeBounce, grenade_bounce ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( NavBlocked, nav_blocked ) + + DECLARE_CSBOTMANAGER_EVENT_LISTENER( ServerShutdown, server_shutdown ) + + CUtlVector< BotEventInterface * > m_commonEventListeners; // These event listeners fire often, and can be disabled for performance gains when no bots are present. + bool m_eventListenersEnabled; + void EnableEventListeners( bool enable ); +}; + +inline CBasePlayer *CCSBotManager::AllocateBotEntity( void ) +{ + return static_cast<CBasePlayer *>( CreateEntityByName( "cs_bot" ) ); +} + +inline bool CCSBotManager::IsTimeToPlantBomb( void ) const +{ + return (gpGlobals->curtime >= m_earliestBombPlantTimestamp); +} + +inline const CCSBotManager::Zone *CCSBotManager::GetClosestZone( const CBaseEntity *entity ) const +{ + if (entity == NULL) + return NULL; + + Vector centroid = entity->GetAbsOrigin(); + centroid.z += HalfHumanHeight; + return GetClosestZone( centroid ); +} + +#endif diff --git a/game/server/cstrike/bot/cs_bot_nav.cpp b/game/server/cstrike/bot/cs_bot_nav.cpp new file mode 100644 index 0000000..8406101 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_nav.cpp @@ -0,0 +1,963 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "obstacle_pushaway.h" +#include "fmtstr.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const float NearBreakableCheckDist = 20.0f; +const float FarBreakableCheckDist = 300.0f; + +#define DEBUG_BREAKABLES 0 +#define DEBUG_DOORS 0 + +//-------------------------------------------------------------------------------------------------------------- +#if DEBUG_BREAKABLES +static void DrawOutlinedQuad( const Vector &p1, + const Vector &p2, + const Vector &p3, + const Vector &p4, + int r, int g, int b, + float duration ) +{ + NDebugOverlay::Triangle( p1, p2, p3, r, g, b, 20, false, duration ); + NDebugOverlay::Triangle( p3, p4, p1, r, g, b, 20, false, duration ); + NDebugOverlay::Line( p1, p2, r, g, b, false, duration ); + NDebugOverlay::Line( p2, p3, r, g, b, false, duration ); + NDebugOverlay::Line( p3, p4, r, g, b, false, duration ); + NDebugOverlay::Line( p4, p1, r, g, b, false, duration ); +} +ConVar bot_debug_breakable_duration( "bot_debug_breakable_duration", "30" ); +#endif // DEBUG_BREAKABLES + + +//-------------------------------------------------------------------------------------------------------------- +CBaseEntity * CheckForEntitiesAlongSegment( const Vector &start, const Vector &end, const Vector &mins, const Vector &maxs, CPushAwayEnumerator *enumerator ) +{ + CBaseEntity *entity = NULL; + + Ray_t ray; + ray.Init( start, end, mins, maxs ); + + partition->EnumerateElementsAlongRay( PARTITION_ENGINE_SOLID_EDICTS, ray, false, enumerator ); + if ( enumerator->m_nAlreadyHit > 0 ) + { + entity = enumerator->m_AlreadyHit[0]; + } + +#if DEBUG_BREAKABLES + if ( entity ) + { + DrawOutlinedQuad( start + mins, start + maxs, end + maxs, end + mins, 255, 0, 0, bot_debug_breakable_duration.GetFloat() ); + } + else + { + DrawOutlinedQuad( start + mins, start + maxs, end + maxs, end + mins, 0, 255, 0, 0.1 ); + } +#endif // DEBUG_BREAKABLES + + return entity; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Look up to 'distance' units ahead on the bot's path for entities. Returns the closest one. + */ +CBaseEntity * CCSBot::FindEntitiesOnPath( float distance, CPushAwayEnumerator *enumerator, bool checkStuck ) +{ + Vector goal; + + int pathIndex = FindPathPoint( distance, &goal, NULL ); + bool isDegeneratePath = ( pathIndex == m_pathLength ); + if ( isDegeneratePath ) + { + goal = m_goalPosition; + } + goal.z += HalfHumanHeight; + + Vector mins, maxs; + mins = Vector( 0, 0, -HalfHumanWidth ); + maxs = Vector( 0, 0, HalfHumanHeight ); + + if ( distance <= NearBreakableCheckDist && m_isStuck && checkStuck ) + { + mins = Vector( -HalfHumanWidth, -HalfHumanWidth, -HalfHumanWidth ); + maxs = Vector( HalfHumanWidth, HalfHumanWidth, HalfHumanHeight ); + } + + CBaseEntity *entity = NULL; + if ( isDegeneratePath ) + { + entity = CheckForEntitiesAlongSegment( WorldSpaceCenter(), m_goalPosition + Vector( 0, 0, HalfHumanHeight ), mins, maxs, enumerator ); +#if DEBUG_BREAKABLES + if ( entity ) + { + NDebugOverlay::HorzArrow( WorldSpaceCenter(), m_goalPosition, 6, 0, 0, 255, 255, true, bot_debug_breakable_duration.GetFloat() ); + } +#endif // DEBUG_BREAKABLES + } + else + { + int startIndex = MAX( 0, m_pathIndex ); + float distanceLeft = distance; + // HACK: start with an index one lower than normal, so we can trace from the bot's location to the + // start of the path nodes. + for( int i=startIndex-1; i<m_pathLength-1; ++i ) + { + Vector start, end; + if ( i == startIndex - 1 ) + { + start = GetAbsOrigin(); + end = m_path[i+1].pos; + } + else + { + start = m_path[i].pos; + end = m_path[i+1].pos; + + if ( m_path[i+1].how == GO_LADDER_UP ) + { + // Need two checks. First we'll check along the ladder + start = m_path[i].pos; + end = m_path[i+1].ladder->m_top; + } + else if ( m_path[i].how == GO_LADDER_UP ) + { + start = m_path[i].ladder->m_top; + } + else if ( m_path[i+1].how == GO_LADDER_DOWN ) + { + // Need two checks. First we'll check along the ladder + start = m_path[i].pos; + end = m_path[i+1].ladder->m_bottom; + } + else if ( m_path[i].how == GO_LADDER_DOWN ) + { + start = m_path[i].ladder->m_bottom; + } + } + + float segmentLength = (start - end).Length(); + if ( distanceLeft - segmentLength < 0 ) + { + // scale our segment back so we don't look too far + Vector direction = end - start; + direction.NormalizeInPlace(); + + end = start + direction * distanceLeft; + } + entity = CheckForEntitiesAlongSegment( start + Vector( 0, 0, HalfHumanHeight ), end + Vector( 0, 0, HalfHumanHeight ), mins, maxs, enumerator ); + if ( entity ) + { +#if DEBUG_BREAKABLES + NDebugOverlay::HorzArrow( start, end, 4, 0, 255, 0, 255, true, bot_debug_breakable_duration.GetFloat() ); +#endif // DEBUG_BREAKABLES + break; + } + + if ( m_path[i].ladder && !IsOnLadder() && distance > NearBreakableCheckDist ) // don't try to break breakables on the other end of a ladder + break; + + distanceLeft -= segmentLength; + if ( distanceLeft < 0 ) + break; + + if ( i != startIndex - 1 && m_path[i+1].ladder ) + { + // Now we'll check from the ladder out to the endpoint + start = ( m_path[i+1].how == GO_LADDER_DOWN ) ? m_path[i+1].ladder->m_bottom : m_path[i+1].ladder->m_top; + end = m_path[i+1].pos; + + entity = CheckForEntitiesAlongSegment( start + Vector( 0, 0, HalfHumanHeight ), end + Vector( 0, 0, HalfHumanHeight ), mins, maxs, enumerator ); + if ( entity ) + { +#if DEBUG_BREAKABLES + NDebugOverlay::HorzArrow( start, end, 4, 0, 255, 0, 255, true, bot_debug_breakable_duration.GetFloat() ); +#endif // DEBUG_BREAKABLES + break; + } + } + } + } + + if ( entity && !IsVisible( entity->WorldSpaceCenter(), false, entity ) ) + return NULL; + + return entity; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::PushawayTouch( CBaseEntity *pOther ) +{ +#if DEBUG_BREAKABLES + NDebugOverlay::EntityBounds( pOther, 255, 0, 0, 127, 0.1f ); +#endif // DEBUG_BREAKABLES + + // if we're not stuck or crouched, we don't care + if ( !m_isStuck && !IsCrouching() ) + return; + + // See if it's breakable + CBaseEntity *props[1]; + CBotBreakableEnumerator enumerator( props, ARRAYSIZE( props ) ); + enumerator.EnumElement( pOther ); + + if ( enumerator.m_nAlreadyHit == 1 ) + { + // it's breakable - try to shoot it. + SetLookAt( "Breakable", pOther->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check for breakable physics props and other breakable entities. We do this here instead of catching them + * in OnTouch() because players don't collide with physics props, so OnTouch() doesn't get called. Also, + * looking ahead like this lets us anticipate when we'll need to break something, and do it before being + * stopped by it. + */ +void CCSBot::BreakablesCheck( void ) +{ +#if DEBUG_BREAKABLES + /* + // Debug code to visually mark all breakables near us + { + Ray_t ray; + Vector origin = WorldSpaceCenter(); + Vector mins( -400, -400, -400 ); + Vector maxs( 400, 400, 400 ); + ray.Init( origin, origin, mins, maxs ); + + CBaseEntity *props[40]; + CBotBreakableEnumerator enumerator( props, ARRAYSIZE( props ) ); + partition->EnumerateElementsAlongRay( PARTITION_ENGINE_SOLID_EDICTS, ray, false, &enumerator ); + for ( int i=0; i<enumerator.m_nAlreadyHit; ++i ) + { + CBaseEntity *prop = props[i]; + if ( prop && prop->m_takedamage == DAMAGE_YES ) + { + CFmtStr msg; + const char *text = msg.sprintf( "%s, %d health", prop->GetClassname(), prop->m_iHealth ); + if ( prop->m_iHealth > 200 ) + { + NDebugOverlay::EntityBounds( prop, 255, 0, 0, 10, 0.2f ); + prop->EntityText( 0, text, 0.2f, 255, 0, 0, 255 ); + } + else + { + NDebugOverlay::EntityBounds( prop, 0, 255, 0, 10, 0.2f ); + prop->EntityText( 0, text, 0.2f, 0, 255, 0, 255 ); + } + } + } + } + */ +#endif // DEBUG_BREAKABLES + + if ( IsAttacking() ) + { + // make sure we aren't running into a breakable trying to knife an enemy + if ( IsUsingKnife() && m_enemy != NULL ) + { + CBaseEntity *breakables[1]; + CBotBreakableEnumerator enumerator( breakables, ARRAYSIZE( breakables ) ); + + CBaseEntity *breakable = NULL; + Vector mins = Vector( -HalfHumanWidth, -HalfHumanWidth, -HalfHumanWidth ); + Vector maxs = Vector( HalfHumanWidth, HalfHumanWidth, HalfHumanHeight ); + breakable = CheckForEntitiesAlongSegment( WorldSpaceCenter(), m_enemy->WorldSpaceCenter(), mins, maxs, &enumerator ); + if ( breakable ) + { +#if DEBUG_BREAKABLES + NDebugOverlay::HorzArrow( WorldSpaceCenter(), m_enemy->WorldSpaceCenter(), 6, 0, 0, 255, 255, true, bot_debug_breakable_duration.GetFloat() ); +#endif // DEBUG_BREAKABLES + + // look at it (chances are we'll already be looking at it, since it's between us and our enemy) + SetLookAt( "Breakable", breakable->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + + // break it (again, don't wait: we don't have ammo, since we're using the knife, and we're looking mostly at it anyway) + PrimaryAttack(); + } + } + return; + } + + if ( !HasPath() ) + return; + + bool isNear = true; + + // Check just in front of us on the path + CBaseEntity *breakables[4]; + CBotBreakableEnumerator enumerator( breakables, ARRAYSIZE( breakables ) ); + CBaseEntity *breakable = FindEntitiesOnPath( NearBreakableCheckDist, &enumerator, true ); + + // If we don't have an object right in front of us, check a ways out + if ( !breakable ) + { + breakable = FindEntitiesOnPath( FarBreakableCheckDist, &enumerator, false ); + isNear = false; + } + + // Try to shoot a breakable we know about + if ( breakable ) + { + // look at it + SetLookAt( "Breakable", breakable->WorldSpaceCenter(), PRIORITY_HIGH, 0.1f, false, 5.0f, true ); + } + + // break it + if ( IsLookingAtSpot( PRIORITY_HIGH ) && m_lookAtSpotAttack ) + { + if ( IsUsingGrenade() || ( !isNear && IsUsingKnife() ) ) + { + EquipBestWeapon( MUST_EQUIP ); + } + else if ( GetActiveWeapon() && GetActiveWeapon()->m_flNextPrimaryAttack <= gpGlobals->curtime ) + { + bool shouldShoot = IsLookingAtPosition( m_lookAtSpot, 10.0f ); + + if ( !shouldShoot ) + { + CBaseEntity *breakables[1]; + CBotBreakableEnumerator LOSbreakable( breakables, ARRAYSIZE( breakables ) ); + + // compute the unit vector along our view + Vector aimDir = GetViewVector(); + + // trace the potential bullet's path + trace_t result; + UTIL_TraceLine( EyePosition(), EyePosition() + FarBreakableCheckDist * aimDir, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + if ( result.DidHitNonWorldEntity() ) + { + LOSbreakable.EnumElement( result.m_pEnt ); + if ( LOSbreakable.m_nAlreadyHit == 1 && LOSbreakable.m_AlreadyHit[0] == breakable ) + { + shouldShoot = true; + } + } + } + + shouldShoot = shouldShoot && !IsFriendInLineOfFire(); + + if ( shouldShoot ) + { + PrimaryAttack(); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check for doors that need +use to open. + */ +void CCSBot::DoorCheck( void ) +{ + if ( IsAttacking() && !IsUsingKnife() ) + { + // If we're attacking with a gun or nade, don't bother with doors. If we're trying to + // knife someone, we might need to open a door. + m_isOpeningDoor = false; + return; + } + + if ( !HasPath() ) + return; + + // Find any doors that need a +use to open just in front of us along the path. + CBaseEntity *doors[4]; + CBotDoorEnumerator enumerator( doors, ARRAYSIZE( doors ) ); + CBaseEntity *door = FindEntitiesOnPath( NearBreakableCheckDist, &enumerator, false ); + + if ( door ) + { + if ( !IsLookingAtSpot( PRIORITY_HIGH ) ) + { + if ( !IsOpeningDoor() ) + { + OpenDoor( door ); + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset the stuck-checker. + */ +void CCSBot::ResetStuckMonitor( void ) +{ + if (m_isStuck) + { + if (IsLocalPlayerWatchingMe() && cv_bot_debug.GetBool() && UTIL_GetListenServerHost()) + { + CBasePlayer *localPlayer = UTIL_GetListenServerHost(); + CSingleUserRecipientFilter filter( localPlayer ); + EmitSound( filter, localPlayer->entindex(), "Bot.StuckSound" ); + } + } + + m_isStuck = false; + m_stuckTimestamp = 0.0f; + m_stuckJumpTimer.Invalidate(); + m_avgVelIndex = 0; + m_avgVelCount = 0; + + m_areaEnteredTimestamp = gpGlobals->curtime; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Test if we have become stuck + */ +void CCSBot::StuckCheck( void ) +{ + if (m_isStuck) + { + // we are stuck - see if we have moved far enough to be considered unstuck + Vector delta = GetAbsOrigin() - m_stuckSpot; + + const float unstuckRange = 75.0f; + if (delta.IsLengthGreaterThan( unstuckRange )) + { + // we are no longer stuck + ResetStuckMonitor(); + PrintIfWatched( "UN-STUCK\n" ); + } + } + else + { + // check if we are stuck + + // compute average velocity over a short period (for stuck check) + Vector vel = GetAbsOrigin() - m_lastOrigin; + + // if we are jumping, ignore Z + if (IsJumping()) + vel.z = 0.0f; + + // cannot be Length2D, or will break ladder movement (they are only Z) + float moveDist = vel.Length(); + + float deltaT = g_BotUpdateInterval; + + m_avgVel[ m_avgVelIndex++ ] = moveDist/deltaT; + + if (m_avgVelIndex == MAX_VEL_SAMPLES) + m_avgVelIndex = 0; + + if (m_avgVelCount < MAX_VEL_SAMPLES) + { + m_avgVelCount++; + } + else + { + // we have enough samples to know if we're stuck + + float avgVel = 0.0f; + for( int t=0; t<m_avgVelCount; ++t ) + avgVel += m_avgVel[t]; + + avgVel /= m_avgVelCount; + + // cannot make this velocity too high, or bots will get "stuck" when going down ladders + float stuckVel = (IsUsingLadder()) ? 10.0f : 20.0f; + + if (avgVel < stuckVel) + { + // we are stuck - note when and where we initially become stuck + m_stuckTimestamp = gpGlobals->curtime; + m_stuckSpot = GetAbsOrigin(); + m_stuckJumpTimer.Start( RandomFloat( 0.3f, 0.75f ) ); // 1.0 + + PrintIfWatched( "STUCK\n" ); + if (IsLocalPlayerWatchingMe() && cv_bot_debug.GetInt() > 0.0f && UTIL_GetListenServerHost()) + { + CBasePlayer *localPlayer = UTIL_GetListenServerHost(); + CSingleUserRecipientFilter filter( localPlayer ); + EmitSound( filter, localPlayer->entindex(), "Bot.StuckStart" ); + } + + m_isStuck = true; + } + } + } + + // always need to track this + m_lastOrigin = GetAbsOrigin(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Check if we need to jump due to height change + */ +bool CCSBot::DiscontinuityJump( float ground, bool onlyJumpDown, bool mustJump ) +{ + // Don't try to jump if in the air. + if( !(GetFlags() & FL_ONGROUND) ) + { + return false; + } + + float dz = ground - GetFeetZ(); + + if (dz > StepHeight && !onlyJumpDown) + { + // dont restrict jump time when going up + if (Jump( MUST_JUMP )) + { + return true; + } + } + else if (!IsUsingLadder() && dz < -JumpHeight) + { + if (Jump( mustJump )) + { + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find "simple" ground height, treating current nav area as part of the floor + */ +bool CCSBot::GetSimpleGroundHeightWithFloor( const Vector &pos, float *height, Vector *normal ) +{ + if (TheNavMesh->GetSimpleGroundHeight( pos, height, normal )) + { + // our current nav area also serves as a ground polygon + if (m_lastKnownArea && m_lastKnownArea->IsOverlapping( pos )) + *height = MAX( (*height), m_lastKnownArea->GetZ( pos ) ); + + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Get our current radio chatter place + */ +Place CCSBot::GetPlace( void ) const +{ + if (m_lastKnownArea) + return m_lastKnownArea->GetPlace(); + + return UNDEFINED_PLACE; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move towards position, independant of view angle + */ +void CCSBot::MoveTowardsPosition( const Vector &pos ) +{ + Vector myOrigin = GetCentroid( this ); + + // + // Jump up on ledges + // Because we may not be able to get to our goal position and enter the next + // area because our extent collides with a nearby vertical ledge, make sure + // we look far enough ahead to avoid this situation. + // Can't look too far ahead, or bots will try to jump up slopes. + // + // NOTE: We need to do this frequently to catch edges at the right time + // @todo Look ahead *along path* instead of straight line + // + if ((m_lastKnownArea == NULL || !(m_lastKnownArea->GetAttributes() & NAV_MESH_NO_JUMP)) && + !IsOnLadder()) + { + float ground; + Vector aheadRay( pos.x - myOrigin.x, pos.y - myOrigin.y, 0 ); + aheadRay.NormalizeInPlace(); + + // look far ahead to allow us to smoothly jump over gaps, ledges, etc + // only jump if ground is flat at lookahead spot to avoid jumping up slopes + bool jumped = false; + if (IsRunning()) + { + const float farLookAheadRange = 80.0f; // 60 + Vector normal; + Vector stepAhead = myOrigin + farLookAheadRange * aheadRay; + stepAhead.z += HalfHumanHeight; + + if (GetSimpleGroundHeightWithFloor( stepAhead, &ground, &normal )) + { + if (normal.z > 0.9f) + jumped = DiscontinuityJump( ground, ONLY_JUMP_DOWN ); + } + } + + if (!jumped) + { + // close up jumping + const float lookAheadRange = 30.0f; // cant be less or will miss jumps over low walls + Vector stepAhead = myOrigin + lookAheadRange * aheadRay; + stepAhead.z += HalfHumanHeight; + if (GetSimpleGroundHeightWithFloor( stepAhead, &ground )) + { + jumped = DiscontinuityJump( ground ); + } + } + + if (!jumped) + { + // about to fall gap-jumping + const float lookAheadRange = 10.0f; + Vector stepAhead = myOrigin + lookAheadRange * aheadRay; + stepAhead.z += HalfHumanHeight; + if (GetSimpleGroundHeightWithFloor( stepAhead, &ground )) + { + jumped = DiscontinuityJump( ground, ONLY_JUMP_DOWN, MUST_JUMP ); + } + } + } + + + // compute our current forward and lateral vectors + float angle = EyeAngles().y; + + Vector2D dir( BotCOS(angle), BotSIN(angle) ); + Vector2D lat( -dir.y, dir.x ); + + // compute unit vector to goal position + Vector2D to( pos.x - myOrigin.x, pos.y - myOrigin.y ); + to.NormalizeInPlace(); + + // move towards the position independant of our view direction + float toProj = to.x * dir.x + to.y * dir.y; + float latProj = to.x * lat.x + to.y * lat.y; + + const float c = 0.25f; // 0.5 + if (toProj > c) + MoveForward(); + else if (toProj < -c) + MoveBackward(); + + // if we are avoiding someone via strafing, don't override + if (m_avoid != NULL) + return; + + if (latProj >= c) + StrafeLeft(); + else if (latProj <= -c) + StrafeRight(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move away from position, independant of view angle + */ +void CCSBot::MoveAwayFromPosition( const Vector &pos ) +{ + // compute our current forward and lateral vectors + float angle = EyeAngles().y; + + Vector2D dir( BotCOS(angle), BotSIN(angle) ); + Vector2D lat( -dir.y, dir.x ); + + // compute unit vector to goal position + Vector2D to( pos.x - GetAbsOrigin().x, pos.y - GetAbsOrigin().y ); + to.NormalizeInPlace(); + + // move away from the position independant of our view direction + float toProj = to.x * dir.x + to.y * dir.y; + float latProj = to.x * lat.x + to.y * lat.y; + + const float c = 0.5f; + if (toProj > c) + MoveBackward(); + else if (toProj < -c) + MoveForward(); + + if (latProj >= c) + StrafeRight(); + else if (latProj <= -c) + StrafeLeft(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Strafe (sidestep) away from position, independant of view angle + */ +void CCSBot::StrafeAwayFromPosition( const Vector &pos ) +{ + // compute our current forward and lateral vectors + float angle = EyeAngles().y; + + Vector2D dir( BotCOS(angle), BotSIN(angle) ); + Vector2D lat( -dir.y, dir.x ); + + // compute unit vector to goal position + Vector2D to( pos.x - GetAbsOrigin().x, pos.y - GetAbsOrigin().y ); + to.NormalizeInPlace(); + + float latProj = to.x * lat.x + to.y * lat.y; + + if (latProj >= 0.0f) + StrafeRight(); + else + StrafeLeft(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * For getting un-stuck + */ +void CCSBot::Wiggle( void ) +{ + if (IsCrouching()) + { + return; + } + + // for wiggling + if (m_wiggleTimer.IsElapsed()) + { + m_wiggleDirection = (NavRelativeDirType)RandomInt( 0, 3 ); + m_wiggleTimer.Start( RandomFloat( 0.3f, 0.5f ) ); // 0.3, 0.5 + } + + Vector forward, right; + EyeVectors( &forward, &right ); + + const float lookAheadRange = (m_lastKnownArea && (m_lastKnownArea->GetAttributes() & NAV_MESH_WALK)) ? 5.0f : 30.0f; + float ground; + + switch( m_wiggleDirection ) + { + case LEFT: + { + // don't move left if we will fall + Vector pos = GetAbsOrigin() - (lookAheadRange * right); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + StrafeLeft(); + } + } + break; + } + + case RIGHT: + { + // don't move right if we will fall + Vector pos = GetAbsOrigin() + (lookAheadRange * right); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + StrafeRight(); + } + } + break; + } + + case FORWARD: + { + // don't move forward if we will fall + Vector pos = GetAbsOrigin() + (lookAheadRange * forward); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + MoveForward(); + } + } + break; + } + + case BACKWARD: + { + // don't move backward if we will fall + Vector pos = GetAbsOrigin() - (lookAheadRange * forward); + + if (GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (GetAbsOrigin().z - ground < StepHeight) + { + MoveBackward(); + } + } + break; + } + } + + if (m_stuckJumpTimer.IsElapsed() && m_lastKnownArea && !(m_lastKnownArea->GetAttributes() & NAV_MESH_NO_JUMP)) + { + if (Jump()) + { + m_stuckJumpTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine approach points from eye position and approach areas of current area + */ +void CCSBot::ComputeApproachPoints( void ) +{ + m_approachPointCount = 0; + + if (m_lastKnownArea == NULL) + { + return; + } + + // assume we're crouching for now + Vector eye = GetCentroid( this ); // + pev->view_ofs; // eye position + + Vector ap; + float halfWidth; + for( int i=0; i<m_lastKnownArea->GetApproachInfoCount() && m_approachPointCount < MAX_APPROACH_POINTS; ++i ) + { + const CCSNavArea::ApproachInfo *info = m_lastKnownArea->GetApproachInfo( i ); + + if (info->here.area == NULL || info->prev.area == NULL) + { + continue; + } + + // compute approach point (approach area is "info->here") + if (info->prevToHereHow <= GO_WEST) + { + info->prev.area->ComputePortal( info->here.area, (NavDirType)info->prevToHereHow, &ap, &halfWidth ); + ap.z = info->here.area->GetZ( ap ); + } + else + { + // use the area's center as an approach point + ap = info->here.area->GetCenter(); + } + + // "bend" our line of sight around corners until we can see the approach point + Vector bendPoint; + if (BendLineOfSight( eye, ap + Vector( 0, 0, HalfHumanHeight ), &bendPoint )) + { + // put point on the ground + if (TheNavMesh->GetGroundHeight( bendPoint, &bendPoint.z ) == false) + { + bendPoint.z = ap.z; + } + + m_approachPoint[ m_approachPointCount ].m_pos = bendPoint; + m_approachPoint[ m_approachPointCount ].m_area = info->here.area; + ++m_approachPointCount; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::DrawApproachPoints( void ) const +{ + for( int i=0; i<m_approachPointCount; ++i ) + { + if (TheCSBots()->GetElapsedRoundTime() >= m_approachPoint[i].m_area->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) + NDebugOverlay::Cross3D( m_approachPoint[i].m_pos, 10.0f, 255, 0, 255, true, 0.1f ); + else + NDebugOverlay::Cross3D( m_approachPoint[i].m_pos, 10.0f, 100, 100, 100, true, 0.1f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find the approach point that is nearest to our current path, ahead of us + */ +bool CCSBot::FindApproachPointNearestPath( Vector *pos ) +{ + if (!HasPath()) + return false; + + // make sure approach points are accurate + ComputeApproachPoints(); + + if (m_approachPointCount == 0) + return false; + + Vector target = Vector( 0, 0, 0 ), close; + float targetRangeSq = 0.0f; + bool found = false; + + int start = m_pathIndex; + int end = m_pathLength; + + // + // We dont want the strictly closest point, but the farthest approach point + // from us that is near our path + // + const float nearPathSq = 10000.0f; // (100) + + for( int i=0; i<m_approachPointCount; ++i ) + { + if (FindClosestPointOnPath( m_approachPoint[i].m_pos, start, end, &close ) == false) + continue; + + float rangeSq = (m_approachPoint[i].m_pos - close).LengthSqr(); + if (rangeSq > nearPathSq) + continue; + + if (rangeSq > targetRangeSq) + { + target = close; + targetRangeSq = rangeSq; + found = true; + } + } + + if (found) + { + *pos = target + Vector( 0, 0, HalfHumanHeight ); + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are at the/an enemy spawn right now + */ +bool CCSBot::IsAtEnemySpawn( void ) const +{ + CBaseEntity *spot; + const char *spawnName = (GetTeamNumber() == TEAM_TERRORIST) ? "info_player_counterterrorist" : "info_player_terrorist"; + + // check if we are at any of the enemy's spawn points + for( spot = gEntList.FindEntityByClassname( NULL, spawnName ); spot; spot = gEntList.FindEntityByClassname( spot, spawnName ) ) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( spot->WorldSpaceCenter() ); + if (area && GetLastKnownArea() == area) + { + return true; + } + } + + return false; +} + diff --git a/game/server/cstrike/bot/cs_bot_pathfind.cpp b/game/server/cstrike/bot/cs_bot_pathfind.cpp new file mode 100644 index 0000000..8604f9b --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_pathfind.cpp @@ -0,0 +1,2075 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef _WIN32 +#pragma warning (disable:4701) // disable warning that variable *may* not be initialized +#endif + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Finds a point from which we can approach a descending ladder. First it tries behind the ladder, + * then in front of ladder, based on LOS. Once we know the direction, we snap to the aproaching nav + * area. Returns true if we're approaching from behind the ladder. + */ +static bool FindDescendingLadderApproachPoint( const CNavLadder *ladder, const CNavArea *area, Vector *pos ) +{ + *pos = ladder->m_top - ladder->GetNormal() * 2.0f * HalfHumanWidth; + + trace_t result; + UTIL_TraceLine( ladder->m_top, *pos, MASK_PLAYERSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + if (result.fraction < 1.0f) + { + *pos = ladder->m_top + ladder->GetNormal() * 2.0f * HalfHumanWidth; + + area->GetClosestPointOnArea( *pos, pos ); + } + + // Use a cross product to determine which side of the ladder 'pos' is on + Vector posToLadder = *pos - ladder->m_top; + float dot = posToLadder.Dot( ladder->GetNormal() ); + return ( dot < 0.0f ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine actual path positions bot will move between along the path + */ +bool CCSBot::ComputePathPositions( void ) +{ + if (m_pathLength == 0) + return false; + + // start in first area's center + m_path[0].pos = m_path[0].area->GetCenter(); + m_path[0].ladder = NULL; + m_path[0].how = NUM_TRAVERSE_TYPES; + + for( int i=1; i<m_pathLength; ++i ) + { + const ConnectInfo *from = &m_path[ i-1 ]; + ConnectInfo *to = &m_path[ i ]; + + if (to->how <= GO_WEST) // walk along the floor to the next area + { + to->ladder = NULL; + + // compute next point, keeping path as straight as possible + from->area->ComputeClosestPointInPortal( to->area, (NavDirType)to->how, from->pos, &to->pos ); + + // move goal position into the goal area a bit + const float stepInDist = 5.0f; // how far to "step into" an area - must be less than min area size + AddDirectionVector( &to->pos, (NavDirType)to->how, stepInDist ); + + // we need to walk out of "from" area, so keep Z where we can reach it + to->pos.z = from->area->GetZ( to->pos ); + + // if this is a "jump down" connection, we must insert an additional point on the path + if (to->area->IsConnected( from->area, NUM_DIRECTIONS ) == false) + { + // this is a "jump down" link + + // compute direction of path just prior to "jump down" + Vector2D dir; + DirectionToVector2D( (NavDirType)to->how, &dir ); + + // shift top of "jump down" out a bit to "get over the ledge" + const float pushDist = 75.0f; // 25.0f; + to->pos.x += pushDist * dir.x; + to->pos.y += pushDist * dir.y; + + // insert a duplicate node to represent the bottom of the fall + if (m_pathLength < MAX_PATH_LENGTH-1) + { + // copy nodes down + for( int j=m_pathLength; j>i; --j ) + m_path[j] = m_path[j-1]; + + // path is one node longer + ++m_pathLength; + + // move index ahead into the new node we just duplicated + ++i; + + m_path[i].pos.x = to->pos.x; + m_path[i].pos.y = to->pos.y; + + // put this one at the bottom of the fall + m_path[i].pos.z = to->area->GetZ( m_path[i].pos ); + } + } + } + else if (to->how == GO_LADDER_UP) // to get to next area, must go up a ladder + { + // find our ladder + const NavLadderConnectVector *pLadders = from->area->GetLadders( CNavLadder::LADDER_UP ); + int it; + for ( it = 0; it < pLadders->Count(); ++it) + { + CNavLadder *ladder = (*pLadders)[ it ].ladder; + + // can't use "behind" area when ascending... + if (ladder->m_topForwardArea == to->area || + ladder->m_topLeftArea == to->area || + ladder->m_topRightArea == to->area) + { + to->ladder = ladder; + to->pos = ladder->m_bottom + ladder->GetNormal() * 2.0f * HalfHumanWidth; + break; + } + } + + if (it == pLadders->Count()) + { + PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + else if (to->how == GO_LADDER_DOWN) // to get to next area, must go down a ladder + { + // find our ladder + const NavLadderConnectVector *pLadders = from->area->GetLadders( CNavLadder::LADDER_DOWN ); + int it; + for ( it = 0; it < pLadders->Count(); ++it) + { + CNavLadder *ladder = (*pLadders)[ it ].ladder; + + if (ladder->m_bottomArea == to->area) + { + to->ladder = ladder; + + FindDescendingLadderApproachPoint( to->ladder, from->area, &to->pos ); + break; + } + } + + if (it == pLadders->Count()) + { + PrintIfWatched( "ERROR: Can't find ladder in path\n" ); + return false; + } + } + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * If next step of path uses a ladder, prepare to traverse it + */ +void CCSBot::SetupLadderMovement( void ) +{ + if (m_pathIndex < 1 || m_pathLength == 0) + return; + + const ConnectInfo *to = &m_path[ m_pathIndex ]; + const ConnectInfo *from = &m_path[ m_pathIndex - 1 ]; + + if (to->ladder) + { + m_spotEncounter = NULL; + m_areaEnteredTimestamp = gpGlobals->curtime; + + m_pathLadder = to->ladder; + m_pathLadderTimestamp = gpGlobals->curtime; + + QAngle ladderAngles; + VectorAngles( m_pathLadder->GetNormal(), ladderAngles ); + + // to get to next area, we must traverse a ladder + if (to->how == GO_LADDER_UP) + { + m_pathLadderState = APPROACH_ASCENDING_LADDER; + m_pathLadderFaceIn = true; + PrintIfWatched( "APPROACH_ASCENDING_LADDER\n" ); + m_goalPosition = m_pathLadder->m_bottom + m_pathLadder->GetNormal() * 2.0f * HalfHumanWidth; + m_lookAheadAngle = AngleNormalizePositive( ladderAngles[ YAW ] + 180.0f ); + } + else + { + // try to mount ladder "face out" first + bool behind = FindDescendingLadderApproachPoint( m_pathLadder, from->area, &m_goalPosition ); + + if ( behind ) + { + PrintIfWatched( "APPROACH_DESCENDING_LADDER (face out)\n" ); + m_pathLadderState = APPROACH_DESCENDING_LADDER; + m_pathLadderFaceIn = false; + m_lookAheadAngle = ladderAngles[ YAW ]; + } + else + { + PrintIfWatched( "APPROACH_DESCENDING_LADDER (face in)\n" ); + m_pathLadderState = APPROACH_DESCENDING_LADDER; + m_pathLadderFaceIn = true; + m_lookAheadAngle = AngleNormalizePositive( ladderAngles[ YAW ] + 180.0f ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/// @todo What about ladders whose top AND bottom are messed up? +void CCSBot::ComputeLadderEndpoint( bool isAscending ) +{ + trace_t result; + Vector from, to; + + if (isAscending) + { + // find actual top in case m_pathLadder penetrates the ceiling + // trace from our chest height at m_pathLadder base + from = m_pathLadder->m_bottom + m_pathLadder->GetNormal() * HalfHumanWidth; + from.z = GetAbsOrigin().z + HalfHumanHeight; + to = m_pathLadder->m_top; + } + else + { + // find actual bottom in case m_pathLadder penetrates the floor + // trace from our chest height at m_pathLadder top + from = m_pathLadder->m_top + m_pathLadder->GetNormal() * HalfHumanWidth; + from.z = GetAbsOrigin().z + HalfHumanHeight; + to = m_pathLadder->m_bottom; + } + + UTIL_TraceLine( from, m_pathLadder->m_bottom, MASK_PLAYERSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f) + m_pathLadderEnd = to.z; + else + m_pathLadderEnd = from.z + result.fraction * (to.z - from.z); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Navigate our current ladder. Return true if we are doing ladder navigation. + * @todo Need Push() and Pop() for run/walk context to keep ladder speed contained. + */ +bool CCSBot::UpdateLadderMovement( void ) +{ + if (m_pathLadder == NULL) + return false; + + bool giveUp = false; + + // check for timeout + const float ladderTimeoutDuration = 10.0f; + if (gpGlobals->curtime - m_pathLadderTimestamp > ladderTimeoutDuration && !cv_bot_debug.GetBool()) + { + PrintIfWatched( "Ladder timeout!\n" ); + giveUp = true; + } + else if (m_pathLadderState == APPROACH_ASCENDING_LADDER || + m_pathLadderState == APPROACH_DESCENDING_LADDER || + m_pathLadderState == ASCEND_LADDER || + m_pathLadderState == DESCEND_LADDER || + m_pathLadderState == DISMOUNT_ASCENDING_LADDER || + m_pathLadderState == MOVE_TO_DESTINATION) + { + if (m_isStuck) + { + PrintIfWatched( "Giving up ladder - stuck\n" ); + giveUp = true; + } + } + + if (giveUp) + { + // jump off ladder and give up + Jump( MUST_JUMP ); + Wiggle(); + ResetStuckMonitor(); + DestroyPath(); + Run(); + return false; + } + else + { + ResetStuckMonitor(); + } + + Vector myOrigin = GetCentroid( this ); + + // check if somehow we totally missed the ladder + switch( m_pathLadderState ) + { + case MOUNT_ASCENDING_LADDER: + case MOUNT_DESCENDING_LADDER: + case ASCEND_LADDER: + case DESCEND_LADDER: + { + const float farAway = 200.0f; + const Vector &ladderPos = (m_pathLadderState == MOUNT_ASCENDING_LADDER || + m_pathLadderState == ASCEND_LADDER) ? m_pathLadder->m_bottom : m_pathLadder->m_top; + if ((ladderPos.AsVector2D() - myOrigin.AsVector2D()).IsLengthGreaterThan( farAway )) + { + PrintIfWatched( "Missed ladder\n" ); + Jump( MUST_JUMP ); + DestroyPath(); + Run(); + return false; + } + break; + } + } + + + m_areaEnteredTimestamp = gpGlobals->curtime; + + const float tolerance = 10.0f; + const float closeToGoal = 25.0f; + + switch( m_pathLadderState ) + { + case APPROACH_ASCENDING_LADDER: + { + bool approached = false; + + Vector2D d( myOrigin.x - m_goalPosition.x, myOrigin.y - m_goalPosition.y ); + + if (d.x * m_pathLadder->GetNormal().x + d.y * m_pathLadder->GetNormal().y < 0.0f) + { + Vector2D perp( -m_pathLadder->GetNormal().y, m_pathLadder->GetNormal().x ); + + if (fabs(d.x * perp.x + d.y * perp.y) < tolerance && d.Length() < closeToGoal) + approached = true; + } + + // small radius will just slow them down a little for more accuracy in hitting their spot + const float walkRange = 50.0f; + if (d.IsLengthLessThan( walkRange )) + { + Walk(); + StandUp(); + } + + if ( d.IsLengthLessThan( 100.0f ) ) + { + if ( !IsOnLadder() && (m_pathLadder->m_bottom.z - GetAbsOrigin().z > JumpCrouchHeight ) ) + { + // find yaw to directly aim at ladder + QAngle idealAngle; + VectorAngles( GetAbsVelocity(), idealAngle ); + const float angleTolerance = 15.0f; + if (AnglesAreEqual( EyeAngles().y, idealAngle.y, angleTolerance )) + { + Jump(); + } + } + } + + /// @todo Check that we are on the ladder we think we are + if (IsOnLadder()) + { + m_pathLadderState = ASCEND_LADDER; + PrintIfWatched( "ASCEND_LADDER\n" ); + + // find actual top in case m_pathLadder penetrates the ceiling + ComputeLadderEndpoint( true ); + } + else if (approached) + { + // face the m_pathLadder + m_pathLadderState = FACE_ASCENDING_LADDER; + PrintIfWatched( "FACE_ASCENDING_LADDER\n" ); + } + else + { + // move toward ladder mount point + MoveTowardsPosition( m_goalPosition ); + } + break; + } + + case APPROACH_DESCENDING_LADDER: + { + // fall check + if (GetFeetZ() <= m_pathLadder->m_bottom.z + HalfHumanHeight) + { + PrintIfWatched( "Fell from ladder.\n" ); + + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( m_pathLadder->m_bottom, &m_goalPosition ); + m_goalPosition += m_pathLadder->GetNormal() * HalfHumanWidth; + + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + else + { + bool approached = false; + + Vector2D d( myOrigin.x - m_goalPosition.x, myOrigin.y - m_goalPosition.y ); + + if (d.x * m_pathLadder->GetNormal().x + d.y * m_pathLadder->GetNormal().y > 0.0f) + { + Vector2D perp( -m_pathLadder->GetNormal().y, m_pathLadder->GetNormal().x ); + + if (fabs(d.x * perp.x + d.y * perp.y) < tolerance && d.Length() < closeToGoal) + approached = true; + } + + // if approaching ladder from the side or "ahead", walk + if (m_pathLadder->m_topBehindArea != m_lastKnownArea) + { + const float walkRange = 150.0f; + if (!IsCrouching() && d.IsLengthLessThan( walkRange )) + Walk(); + } + + /// @todo Check that we are on the ladder we think we are + if (IsOnLadder()) + { + // we slipped onto the ladder - climb it + m_pathLadderState = DESCEND_LADDER; + Run(); + PrintIfWatched( "DESCEND_LADDER\n" ); + + // find actual bottom in case m_pathLadder penetrates the floor + ComputeLadderEndpoint( false ); + } + else if (approached) + { + // face the ladder + m_pathLadderState = FACE_DESCENDING_LADDER; + PrintIfWatched( "FACE_DESCENDING_LADDER\n" ); + } + else + { + // move toward ladder mount point + MoveTowardsPosition( m_goalPosition ); + } + } + break; + } + + case FACE_ASCENDING_LADDER: + { + // find yaw to directly aim at ladder + Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + if (m_path[ m_pathIndex ].area == m_pathLadder->m_topForwardArea) + { + m_pathLadderDismountDir = FORWARD; + } + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topLeftArea) + { + m_pathLadderDismountDir = LEFT; + idealAngle[ YAW ] = AngleNormalizePositive( idealAngle[ YAW ] + 90.0f ); + } + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topRightArea) + { + m_pathLadderDismountDir = RIGHT; + idealAngle[ YAW ] = AngleNormalizePositive( idealAngle[ YAW ] - 90.0f ); + } + + const float angleTolerance = 5.0f; + if (AnglesAreEqual( EyeAngles().y, idealAngle.y, angleTolerance )) + { + // move toward ladder until we become "on" it + Run(); + ResetStuckMonitor(); + m_pathLadderState = MOUNT_ASCENDING_LADDER; + switch (m_pathLadderDismountDir) + { + case LEFT: PrintIfWatched( "MOUNT_ASCENDING_LADDER LEFT\n" ); break; + case RIGHT: PrintIfWatched( "MOUNT_ASCENDING_LADDER RIGHT\n" ); break; + default: PrintIfWatched( "MOUNT_ASCENDING_LADDER FORWARD\n" ); break; + } + } + break; + } + + case FACE_DESCENDING_LADDER: + { + // find yaw to directly aim at ladder + Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + const float angleTolerance = 5.0f; + if (AnglesAreEqual( EyeAngles().y, idealAngle.y, angleTolerance )) + { + // move toward ladder until we become "on" it + m_pathLadderState = MOUNT_DESCENDING_LADDER; + ResetStuckMonitor(); + PrintIfWatched( "MOUNT_DESCENDING_LADDER\n" ); + } + break; + } + + case MOUNT_ASCENDING_LADDER: + if (IsOnLadder()) + { + m_pathLadderState = ASCEND_LADDER; + PrintIfWatched( "ASCEND_LADDER\n" ); + + // find actual top in case m_pathLadder penetrates the ceiling + ComputeLadderEndpoint( true ); + } + + // move toward ladder mount point + if ( !IsOnLadder() && (m_pathLadder->m_bottom.z - GetAbsOrigin().z > JumpCrouchHeight ) ) + { + Jump(); + } + + switch( m_pathLadderDismountDir ) + { + case RIGHT: StrafeLeft(); break; + case LEFT: StrafeRight(); break; + default: MoveForward(); break; + } + break; + + case MOUNT_DESCENDING_LADDER: + // fall check + if (GetFeetZ() <= m_pathLadder->m_bottom.z + HalfHumanHeight) + { + PrintIfWatched( "Fell from ladder.\n" ); + + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( m_pathLadder->m_bottom, &m_goalPosition ); + m_goalPosition += m_pathLadder->GetNormal() * HalfHumanWidth; + + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + else + { + if (IsOnLadder()) + { + m_pathLadderState = DESCEND_LADDER; + PrintIfWatched( "DESCEND_LADDER\n" ); + + // find actual bottom in case m_pathLadder penetrates the floor + ComputeLadderEndpoint( false ); + } + + // move toward ladder mount point + MoveForward(); + } + break; + + case ASCEND_LADDER: + // run, so we can make our dismount jump to the side, if necessary + Run(); + + // if our destination area requires us to crouch, do it + if (m_path[ m_pathIndex ].area->GetAttributes() & NAV_MESH_CROUCH) + Crouch(); + + // did we reach the top? + if (GetFeetZ() >= m_pathLadderEnd) + { + // we reached the top - dismount + m_pathLadderState = DISMOUNT_ASCENDING_LADDER; + PrintIfWatched( "DISMOUNT_ASCENDING_LADDER\n" ); + + if (m_path[ m_pathIndex ].area == m_pathLadder->m_topForwardArea) + m_pathLadderDismountDir = FORWARD; + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topLeftArea) + m_pathLadderDismountDir = LEFT; + else if (m_path[ m_pathIndex ].area == m_pathLadder->m_topRightArea) + m_pathLadderDismountDir = RIGHT; + + m_pathLadderDismountTimestamp = gpGlobals->curtime; + } + else if (!IsOnLadder()) + { + // we fall off the ladder, repath + DestroyPath(); + return false; + } + + // move up ladder + switch( m_pathLadderDismountDir ) + { + case RIGHT: StrafeLeft(); break; + case LEFT: StrafeRight(); break; + default: MoveForward(); break; + } + break; + + case DESCEND_LADDER: + { + Run(); + float destHeight = m_pathLadderEnd; + if ( (m_path[ m_pathIndex ].area->GetAttributes() & NAV_MESH_NO_JUMP) == 0 ) + { + destHeight += HalfHumanHeight; + } + if ( !IsOnLadder() || GetFeetZ() <= destHeight ) + { + // we reached the bottom, or we fell off - dismount + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( m_pathLadder->m_bottom, &m_goalPosition ); + m_goalPosition += m_pathLadder->GetNormal() * HalfHumanWidth; + + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + + // Move down ladder + MoveForward(); + + break; + } + + case DISMOUNT_ASCENDING_LADDER: + { + if (gpGlobals->curtime - m_pathLadderDismountTimestamp >= 0.4f) + { + m_pathLadderState = MOVE_TO_DESTINATION; + m_path[ m_pathIndex ].area->GetClosestPointOnArea( myOrigin, &m_goalPosition ); + PrintIfWatched( "MOVE_TO_DESTINATION\n" ); + } + + // We should already be facing the dismount point + MoveForward(); + break; + } + + case MOVE_TO_DESTINATION: + if (m_path[ m_pathIndex ].area->Contains( myOrigin )) + { + // successfully traversed ladder and reached destination area + // exit ladder state machine + PrintIfWatched( "Ladder traversed.\n" ); + m_pathLadder = NULL; + + // incrememnt path index to next step beyond this ladder + SetPathIndex( m_pathIndex+1 ); + + ClearLookAt(); + + return false; + } + + MoveTowardsPosition( m_goalPosition ); + break; + } + + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + DrawPath(); + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute closest point on path to given point + * NOTE: This does not do line-of-sight tests, so closest point may be thru the floor, etc + */ +bool CCSBot::FindClosestPointOnPath( const Vector &worldPos, int startIndex, int endIndex, Vector *close ) const +{ + if (!HasPath() || close == NULL) + return false; + + Vector along, toWorldPos; + Vector pos; + const Vector *from, *to; + float length; + float closeLength; + float closeDistSq = 9999999999.9; + float distSq; + + for( int i=startIndex; i<=endIndex; ++i ) + { + from = &m_path[i-1].pos; + to = &m_path[i].pos; + + // compute ray along this path segment + along = *to - *from; + + // make it a unit vector along the path + length = along.NormalizeInPlace(); + + // compute vector from start of segment to our point + toWorldPos = worldPos - *from; + + // find distance of closest point on ray + closeLength = DotProduct( toWorldPos, along ); + + // constrain point to be on path segment + if (closeLength <= 0.0f) + pos = *from; + else if (closeLength >= length) + pos = *to; + else + pos = *from + closeLength * along; + + distSq = (pos - worldPos).LengthSqr(); + + // keep the closest point so far + if (distSq < closeDistSq) + { + closeDistSq = distSq; + *close = pos; + } + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest point to our current position on our current path + * If "local" is true, only check the portion of the path surrounding m_pathIndex. + */ +int CCSBot::FindOurPositionOnPath( Vector *close, bool local ) const +{ + if (!HasPath()) + return -1; + + Vector along, toFeet; + Vector feet = GetAbsOrigin(); + Vector eyes = feet + Vector( 0, 0, HalfHumanHeight ); // in case we're crouching + Vector pos; + const Vector *from, *to; + float length; + float closeLength; + float closeDistSq = 9999999999.9; + int closeIndex = -1; + float distSq; + + int start, end; + + if (local) + { + start = m_pathIndex - 3; + if (start < 1) + start = 1; + + end = m_pathIndex + 3; + if (end > m_pathLength) + end = m_pathLength; + } + else + { + start = 1; + end = m_pathLength; + } + + for( int i=start; i<end; ++i ) + { + from = &m_path[i-1].pos; + to = &m_path[i].pos; + + // compute ray along this path segment + along = *to - *from; + + // make it a unit vector along the path + length = along.NormalizeInPlace(); + + // compute vector from start of segment to our point + toFeet = feet - *from; + + // find distance of closest point on ray + closeLength = DotProduct( toFeet, along ); + + // constrain point to be on path segment + if (closeLength <= 0.0f) + pos = *from; + else if (closeLength >= length) + pos = *to; + else + pos = *from + closeLength * along; + + distSq = (pos - feet).LengthSqr(); + + // keep the closest point so far + if (distSq < closeDistSq) + { + // don't use points we cant see + Vector probe = pos + Vector( 0, 0, HalfHumanHeight ); + if (!IsWalkableTraceLineClear( eyes, probe, WALK_THRU_DOORS | WALK_THRU_BREAKABLES )) + continue; + + // don't use points we cant reach + if (!IsStraightLinePathWalkable( pos )) + continue; + + closeDistSq = distSq; + if (close) + *close = pos; + closeIndex = i-1; + } + } + + return closeIndex; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Test for un-jumpable height change, or unrecoverable fall + */ +bool CCSBot::IsStraightLinePathWalkable( const Vector &goal ) const +{ +// this is causing hang-up problems when crawling thru ducts/windows that drop off into rooms (they fail the "falling" check) +return true; + + const float inc = GenerationStepSize; + + Vector feet = GetAbsOrigin(); + Vector dir = goal - feet; + float length = dir.NormalizeInPlace(); + + float lastGround; + //if (!GetSimpleGroundHeight( &pev->origin, &lastGround )) + // return false; + lastGround = feet.z; + + + float along=0.0f; + Vector pos; + float ground; + bool done = false; + while( !done ) + { + along += inc; + if (along > length) + { + along = length; + done = true; + } + + // compute step along path + pos = feet + along * dir; + + pos.z += HalfHumanHeight; + + if (!TheNavMesh->GetSimpleGroundHeight( pos, &ground )) + return false; + + // check for falling + if (ground - lastGround < -StepHeight) + return false; + + // check for unreachable jump + // use slightly shorter jump limit, to allow for some fudge room + if (ground - lastGround > JumpHeight) + return false; + + lastGround = ground; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute a point a fixed distance ahead along our path. + * Returns path index just after point. + */ +int CCSBot::FindPathPoint( float aheadRange, Vector *point, int *prevIndex ) +{ + Vector myOrigin = GetCentroid( this ); + + // find path index just past aheadRange + int afterIndex; + + // finds the closest point on local area of path, and returns the path index just prior to it + Vector close; + int startIndex = FindOurPositionOnPath( &close, true ); + + if (prevIndex) + *prevIndex = startIndex; + + if (startIndex <= 0) + { + // went off the end of the path + // or next point in path is unwalkable (ie: jump-down) + // keep same point + return m_pathIndex; + } + + // if we are crouching, just follow the path exactly + if (IsCrouching()) + { + // we want to move to the immediately next point along the path from where we are now + int index = startIndex+1; + if (index >= m_pathLength) + index = m_pathLength-1; + + *point = m_path[ index ].pos; + + // if we are very close to the next point in the path, skip ahead to the next one to avoid wiggling + // we must do a 2D check here, in case the goal point is floating in space due to jump down, etc + const float closeEpsilon = 20.0f; // 10 + while ((*point - close).AsVector2D().IsLengthLessThan( closeEpsilon )) + { + ++index; + + if (index >= m_pathLength) + { + index = m_pathLength-1; + break; + } + + *point = m_path[ index ].pos; + } + + return index; + } + + // make sure we use a node a minimum distance ahead of us, to avoid wiggling + while (startIndex < m_pathLength-1) + { + Vector pos = m_path[ startIndex+1 ].pos; + + // we must do a 2D check here, in case the goal point is floating in space due to jump down, etc + const float closeEpsilon = 20.0f; + if ((pos - close).AsVector2D().IsLengthLessThan( closeEpsilon )) + { + ++startIndex; + } + else + { + break; + } + } + + // if we hit a ladder, stop, or jump area, must stop (dont use ladder behind us) + if (startIndex > m_pathIndex && startIndex < m_pathLength && + (m_path[ startIndex ].ladder || m_path[ startIndex ].area->GetAttributes() & (NAV_MESH_JUMP | NAV_MESH_STOP))) + { + *point = m_path[ startIndex ].pos; + return startIndex; + } + + // we need the point just *ahead* of us + ++startIndex; + if (startIndex >= m_pathLength) + startIndex = m_pathLength-1; + + // if we hit a ladder, stop, or jump area, must stop + if (startIndex < m_pathLength && + (m_path[ startIndex ].ladder || m_path[ startIndex ].area->GetAttributes() & (NAV_MESH_JUMP | NAV_MESH_STOP))) + { + *point = m_path[ startIndex ].pos; + return startIndex; + } + + // note direction of path segment we are standing on + Vector initDir = m_path[ startIndex ].pos - m_path[ startIndex-1 ].pos; + initDir.NormalizeInPlace(); + + Vector feet = GetAbsOrigin(); + Vector eyes = feet + Vector( 0, 0, HalfHumanHeight ); + float rangeSoFar = 0; + + // this flag is true if our ahead point is visible + bool visible = true; + + Vector prevDir = initDir; + + // step along the path until we pass aheadRange + bool isCorner = false; + int i; + for( i=startIndex; i<m_pathLength; ++i ) + { + Vector pos = m_path[i].pos; + Vector to = pos - m_path[i-1].pos; + Vector dir = to; + dir.NormalizeInPlace(); + + // don't allow path to double-back from our starting direction (going upstairs, down curved passages, etc) + if (DotProduct( dir, initDir ) < 0.0f) // -0.25f + { + --i; + break; + } + + // if the path turns a corner, we want to move towards the corner, not into the wall/stairs/etc + if (DotProduct( dir, prevDir ) < 0.5f) + { + isCorner = true; + --i; + break; + } + prevDir = dir; + + // don't use points we cant see + Vector probe = pos + Vector( 0, 0, HalfHumanHeight ); + if (!IsWalkableTraceLineClear( eyes, probe, WALK_THRU_BREAKABLES )) + { + // presumably, the previous point is visible, so we will interpolate + visible = false; + break; + } + + // if we encounter a ladder or jump area, we must stop + if (i < m_pathLength && + (m_path[ i ].ladder || m_path[ i ].area->GetAttributes() & NAV_MESH_JUMP)) + break; + + // Check straight-line path from our current position to this position + // Test for un-jumpable height change, or unrecoverable fall + if (!IsStraightLinePathWalkable( pos )) + { + --i; + break; + } + + Vector along = (i == startIndex) ? (pos - feet) : (pos - m_path[i-1].pos); + rangeSoFar += along.Length2D(); + + // stop if we have gone farther than aheadRange + if (rangeSoFar >= aheadRange) + break; + } + + if (i < startIndex) + afterIndex = startIndex; + else if (i < m_pathLength) + afterIndex = i; + else + afterIndex = m_pathLength-1; + + + // compute point on the path at aheadRange + if (afterIndex == 0) + { + *point = m_path[0].pos; + } + else + { + // interpolate point along path segment + const Vector *afterPoint = &m_path[ afterIndex ].pos; + const Vector *beforePoint = &m_path[ afterIndex-1 ].pos; + + Vector to = *afterPoint - *beforePoint; + float length = to.Length2D(); + + float t = 1.0f - ((rangeSoFar - aheadRange) / length); + + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + + *point = *beforePoint + t * to; + + // if afterPoint wasn't visible, slide point backwards towards beforePoint until it is + if (!visible) + { + const float sightStepSize = 25.0f; + float dt = sightStepSize / length; + + Vector probe = *point + Vector( 0, 0, HalfHumanHeight ); + while( t > 0.0f && !IsWalkableTraceLineClear( eyes, probe, WALK_THRU_BREAKABLES ) ) + { + t -= dt; + *point = *beforePoint + t * to; + } + + if (t <= 0.0f) + *point = *beforePoint; + } + } + + // if position found is too close to us, or behind us, force it farther down the path so we don't stop and wiggle + if (!isCorner) + { + const float epsilon = 50.0f; + Vector2D toPoint; + toPoint.x = point->x - myOrigin.x; + toPoint.y = point->y - myOrigin.y; + if (DotProduct2D( toPoint, initDir.AsVector2D() ) < 0.0f || toPoint.IsLengthLessThan( epsilon )) + { + int i; + for( i=startIndex; i<m_pathLength; ++i ) + { + toPoint.x = m_path[i].pos.x - myOrigin.x; + toPoint.y = m_path[i].pos.y - myOrigin.y; + if (m_path[i].ladder || m_path[i].area->GetAttributes() & NAV_MESH_JUMP || toPoint.IsLengthGreaterThan( epsilon )) + { + *point = m_path[i].pos; + startIndex = i; + break; + } + } + + if (i == m_pathLength) + { + *point = GetPathEndpoint(); + startIndex = m_pathLength-1; + } + } + } + + // m_pathIndex should always be the next point on the path, even if we're not moving directly towards it + return startIndex; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the current index along the path + */ +void CCSBot::SetPathIndex( int newIndex ) +{ + m_pathIndex = MIN( newIndex, m_pathLength-1 ); + m_areaEnteredTimestamp = gpGlobals->curtime; + + if (m_path[ m_pathIndex ].ladder) + { + SetupLadderMovement(); + } + else + { + // get our "encounter spots" for this leg of the path + if (m_pathIndex < m_pathLength && m_pathIndex >= 2) + m_spotEncounter = m_path[ m_pathIndex-1 ].area->GetSpotEncounter( m_path[ m_pathIndex-2 ].area, m_path[ m_pathIndex ].area ); + else + m_spotEncounter = NULL; + + m_pathLadder = NULL; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if nearing a jump in the path + */ +bool CCSBot::IsNearJump( void ) const +{ + if (m_pathIndex == 0 || m_pathIndex >= m_pathLength) + return false; + + for( int i=m_pathIndex-1; i<m_pathIndex; ++i ) + { + if (m_path[ i ].area->GetAttributes() & NAV_MESH_JUMP) + { + float dz = m_path[ i+1 ].pos.z - m_path[ i ].pos.z; + + if (dz > 0.0f) + return true; + } + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return approximately how much damage will will take from the given fall height + */ +float CCSBot::GetApproximateFallDamage( float height ) const +{ + // empirically discovered height values + const float slope = 0.2178f; + const float intercept = 26.0f; + + float damage = slope * height - intercept; + + if (damage < 0.0f) + return 0.0f; + + return damage; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if a friend is between us and the given position + */ +bool CCSBot::IsFriendInTheWay( const Vector &goalPos ) +{ + // do this check less often to ease CPU burden + if (!m_avoidFriendTimer.IsElapsed()) + { + return m_isFriendInTheWay; + } + + const float avoidFriendInterval = 0.5f; + m_avoidFriendTimer.Start( avoidFriendInterval ); + + // compute ray along intended path + Vector myOrigin = GetCentroid( this ); + Vector moveDir = goalPos - myOrigin; + + // make it a unit vector + float length = moveDir.NormalizeInPlace(); + + m_isFriendInTheWay = false; + + // check if any friends are overlapping this linear path + for( int i = 1; i <= gpGlobals->maxClients; ++i ) + { + CCSPlayer *player = static_cast<CCSPlayer *>( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (!player->IsAlive()) + continue; + + if (!player->InSameTeam( this )) + continue; + + if (player->entindex() == entindex()) + continue; + + // compute vector from us to our friend + Vector toFriend = player->GetAbsOrigin() - GetAbsOrigin(); + + // check if friend is in our "personal space" + const float personalSpace = 100.0f; + if (toFriend.IsLengthGreaterThan( personalSpace )) + continue; + + // find distance of friend along our movement path + float friendDistAlong = DotProduct( toFriend, moveDir ); + + // if friend is behind us, ignore him + if (friendDistAlong <= 0.0f) + continue; + + // constrain point to be on path segment + Vector pos; + if (friendDistAlong >= length) + pos = goalPos; + else + pos = myOrigin + friendDistAlong * moveDir; + + // check if friend overlaps our intended line of movement + const float friendRadius = 30.0f; + if ((pos - GetCentroid( player )).IsLengthLessThan( friendRadius )) + { + // friend is in our personal space and overlaps our intended line of movement + m_isFriendInTheWay = true; + break; + } + } + + return m_isFriendInTheWay; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do reflex avoidance movements if our "feelers" are touched + */ +void CCSBot::FeelerReflexAdjustment( Vector *goalPosition ) +{ + // if we are in a "precise" area, do not do feeler adjustments + if (m_lastKnownArea && m_lastKnownArea->GetAttributes() & NAV_MESH_PRECISE) + return; + + Vector dir( BotCOS( m_forwardAngle ), BotSIN( m_forwardAngle ), 0.0f ); + Vector lat( -dir.y, dir.x, 0.0f ); + + const float feelerOffset = (IsCrouching()) ? 15.0f : 20.0f; + const float feelerLengthRun = 50.0f; // 100 - too long for tight hallways (cs_747) + const float feelerLengthWalk = 30.0f; + const float feelerHeight = StepHeight + 0.1f; // if obstacle is lower than StepHeight, we'll walk right over it + + float feelerLength = (IsRunning()) ? feelerLengthRun : feelerLengthWalk; + + feelerLength = (IsCrouching()) ? 20.0f : feelerLength; + + // + // Feelers must follow floor slope + // + float ground; + Vector normal; + Vector eye = EyePosition(); + if (GetSimpleGroundHeightWithFloor( eye, &ground, &normal ) == false) + return; + + // get forward vector along floor + dir = CrossProduct( lat, normal ); + + // correct the sideways vector + lat = CrossProduct( dir, normal ); + + + Vector feet = GetAbsOrigin(); + feet.z += feelerHeight; + + Vector from = feet + feelerOffset * lat; + Vector to = from + feelerLength * dir; + + bool leftClear = IsWalkableTraceLineClear( from, to, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); + + // avoid ledges, too + // use 'from' so it doesn't interfere with legitimate gap jumping (its at our feet) + /// @todo Rethink this - it causes lots of wiggling when bots jump down from vents, etc +/* + float ground; + if (GetSimpleGroundHeightWithFloor( &from, &ground )) + { + if (GetFeetZ() - ground > JumpHeight) + leftClear = false; + } +*/ + + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + if (leftClear) + UTIL_DrawBeamPoints( from, to, 1, 0, 255, 0 ); + else + UTIL_DrawBeamPoints( from, to, 1, 255, 0, 0 ); + } + + from = feet - feelerOffset * lat; + to = from + feelerLength * dir; + + bool rightClear = IsWalkableTraceLineClear( from, to, WALK_THRU_DOORS | WALK_THRU_BREAKABLES ); + +/* + // avoid ledges, too + if (GetSimpleGroundHeightWithFloor( &from, &ground )) + { + if (GetFeetZ() - ground > JumpHeight) + rightClear = false; + } +*/ + + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + if (rightClear) + UTIL_DrawBeamPoints( from, to, 1, 0, 255, 0 ); + else + UTIL_DrawBeamPoints( from, to, 1, 255, 0, 0 ); + } + + const float avoidRange = (IsCrouching()) ? 150.0f : 300.0f; // 50, 300 + + if (!rightClear) + { + if (leftClear) + { + // right hit, left clear - veer left + *goalPosition = *goalPosition + avoidRange * lat; + } + } + else if (!leftClear) + { + // right clear, left hit - veer right + *goalPosition = *goalPosition - avoidRange * lat; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Allows the current nav area to make us run/walk without messing with our state + */ +bool CCSBot::IsRunning( void ) const +{ + // if we've forced running, go with it + if ( !m_mustRunTimer.IsElapsed() ) + { + return BaseClass::IsRunning(); + } + + if ( m_lastKnownArea && m_lastKnownArea->GetAttributes() & NAV_MESH_RUN ) + { + return true; + } + + if ( m_lastKnownArea && m_lastKnownArea->GetAttributes() & NAV_MESH_WALK ) + { + return false; + } + + return BaseClass::IsRunning(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move along the path. Return false if end of path reached. + */ +CCSBot::PathResult CCSBot::UpdatePathMovement( bool allowSpeedChange ) +{ + VPROF_BUDGET( "CCSBot::UpdatePathMovement", VPROF_BUDGETGROUP_NPCS ); + + if (m_pathLength == 0) + return PATH_FAILURE; + + if (cv_bot_walk.GetBool()) + Walk(); + + // + // If we are navigating a ladder, it overrides all other path movement until complete + // + if (UpdateLadderMovement()) + return PROGRESSING; + + // ladder failure can destroy the path + if (m_pathLength == 0) + return PATH_FAILURE; + + + // we are not supposed to be on a ladder - if we are, jump off + if (IsOnLadder()) + Jump( MUST_JUMP ); + + + assert( m_pathIndex < m_pathLength ); + + // + // Stop path attribute + // + if (!IsUsingLadder()) + { + // if the m_isStopping flag is set, clear our movement + // if the m_isStopping flag is set and movement is stopped, clear m_isStopping + if ( m_lastKnownArea && m_isStopping ) + { + ResetStuckMonitor(); + ClearMovement(); + + if ( GetAbsVelocity().LengthSqr() < 0.1f ) + { + m_isStopping = false; + } + else + { + return PROGRESSING; + } + } + } // end stop logic + + + // + // Check if reached the end of the path + // + bool nearEndOfPath = false; + if (m_pathIndex >= m_pathLength-1) + { + Vector toEnd = GetPathEndpoint() - GetAbsOrigin(); + Vector d = toEnd; // can't use 2D because path end may be below us (jump down) + + const float walkRange = 200.0f; + + // walk as we get close to the goal position to ensure we hit it + if (d.IsLengthLessThan( walkRange )) + { + // don't walk if crouching - too slow + if (allowSpeedChange && !IsCrouching()) + Walk(); + + // note if we are near the end of the path + const float nearEndRange = 50.0f; + if (d.IsLengthLessThan( nearEndRange )) + nearEndOfPath = true; + + const float closeEpsilon = 20.0f; + if (d.IsLengthLessThan( closeEpsilon )) + { + // reached goal position - path complete + DestroyPath(); + + /// @todo We should push and pop walk state here, in case we want to continue walking after reaching goal + if (allowSpeedChange) + Run(); + + return END_OF_PATH; + } + } + } + + + // + // To keep us moving smoothly, we will move towards + // a point farther ahead of us down our path. + // + int prevIndex = 0; // closest index on path just prior to where we are now + const float aheadRange = 300.0f; + int newIndex = FindPathPoint( aheadRange, &m_goalPosition, &prevIndex ); + + // BOTPORT: Why is prevIndex sometimes -1? + if (prevIndex < 0) + prevIndex = 0; + + // if goal position is near to us, we must be about to go around a corner - so look ahead! + Vector myOrigin = GetCentroid( this ); + const float nearCornerRange = 100.0f; + if (m_pathIndex < m_pathLength-1 && (m_goalPosition - myOrigin).IsLengthLessThan( nearCornerRange )) + { + if (!IsLookingAtSpot( PRIORITY_HIGH )) + { + ClearLookAt(); + InhibitLookAround( 0.5f ); + } + } + + // if we moved to a new node on the path, setup movement + if (newIndex > m_pathIndex) + { + SetPathIndex( newIndex ); + } + + // + // Crouching + // + if (!IsUsingLadder()) + { + // if we are approaching a crouch area, crouch + // if there are no crouch areas coming up, stand + const float crouchRange = 50.0f; + bool didCrouch = false; + for( int i=prevIndex; i<m_pathLength; ++i ) + { + const CNavArea *to = m_path[i].area; + + // if there is a jump area on the way to the crouch area, don't crouch as it messes up the jump + // unless we are already higher than the jump area - we must've jumped already but not moved into next area + if (to->GetAttributes() & NAV_MESH_JUMP && to->GetCenter().z > GetFeetZ()) + break; + + Vector close; + to->GetClosestPointOnArea( myOrigin, &close ); + + if ((close - myOrigin).AsVector2D().IsLengthGreaterThan( crouchRange )) + break; + + if (to->GetAttributes() & NAV_MESH_CROUCH) + { + Crouch(); + didCrouch = true; + ResetStuckMonitor(); + break; + } + } + + if (!didCrouch && !IsJumping()) + { + // no crouch areas coming up + StandUp(); + } + + } // end crouching logic + + + // compute our forward facing angle + m_forwardAngle = UTIL_VecToYaw( m_goalPosition - myOrigin ); + + // + // Look farther down the path to "lead" our view around corners + // + Vector toGoal; + bool isWaitingForLadder = false; + + // if we are crouching, look towards where we are moving to negotiate tight corners + if (IsCrouching()) + { + m_lookAheadAngle = m_forwardAngle; + } + else + { + if (m_pathIndex == 0) + { + toGoal = m_path[1].pos; + } + else if (m_pathIndex < m_pathLength) + { + toGoal = m_path[ m_pathIndex ].pos - myOrigin; + + // actually aim our view farther down the path + const float lookAheadRange = 500.0f; + if (!m_path[ m_pathIndex ].ladder && + !IsNearJump() && + toGoal.AsVector2D().IsLengthLessThan( lookAheadRange )) + { + float along = toGoal.Length2D(); + int i; + for( i=m_pathIndex+1; i<m_pathLength; ++i ) + { + Vector delta = m_path[i].pos - m_path[i-1].pos; + float segmentLength = delta.Length2D(); + + if (along + segmentLength >= lookAheadRange) + { + // interpolate between points to keep look ahead point at fixed distance + float t = (lookAheadRange - along) / (segmentLength + along); + Vector target; + + if (t <= 0.0f) + target = m_path[i-1].pos; + else if (t >= 1.0f) + target = m_path[i].pos; + else + target = m_path[i-1].pos + t * delta; + + toGoal = target - myOrigin; + break; + } + + // if we are coming up to a ladder or a jump, look at it + if (m_path[i].ladder || + (m_path[i].area->GetAttributes() & NAV_MESH_JUMP) || + (m_path[i].area->GetAttributes() & NAV_MESH_PRECISE) || + (m_path[i].area->GetAttributes() & NAV_MESH_STOP)) + { + toGoal = m_path[i].pos - myOrigin; + + // if anyone is on the ladder, wait + if (m_path[i].ladder && m_path[i].ladder->IsInUse( this )) + { + isWaitingForLadder = true; + ResetStuckMonitor(); + + // if we are too close to the ladder, back off a bit + const float tooCloseRange = 100.0f; + Vector2D delta( m_path[i].ladder->m_top.x - myOrigin.x, + m_path[i].ladder->m_top.y - myOrigin.y ); + if (delta.IsLengthLessThan( tooCloseRange )) + { + MoveAwayFromPosition( m_path[i].ladder->m_top ); + } + } + + break; + } + + along += segmentLength; + } + + if (i == m_pathLength) + toGoal = GetPathEndpoint() - myOrigin; + } + } + else + { + toGoal = GetPathEndpoint() - myOrigin; + } + + m_lookAheadAngle = UTIL_VecToYaw( toGoal ); + } + + // initialize "adjusted" goal to current goal + Vector adjustedGoal = m_goalPosition; + + // + // Use short "feelers" to veer away from close-range obstacles + // Feelers come from our ankles, just above StepHeight, so we avoid short walls, too + // Don't use feelers if very near the end of the path, or about to jump + // + /// @todo Consider having feelers at several heights to deal with overhangs, etc. + if (!nearEndOfPath && !IsNearJump() && !IsJumping()) + { + FeelerReflexAdjustment( &adjustedGoal ); + } + + // draw debug visualization + if ( ( cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.GetInt() == 10 ) + { + DrawPath(); + + const Vector *pos = &m_path[ m_pathIndex ].pos; + UTIL_DrawBeamPoints( *pos, *pos + Vector( 0, 0, 50 ), 1, 255, 255, 0 ); + + UTIL_DrawBeamPoints( adjustedGoal, adjustedGoal + Vector( 0, 0, 50 ), 1, 255, 0, 255 ); + UTIL_DrawBeamPoints( myOrigin, adjustedGoal + Vector( 0, 0, 50 ), 1, 255, 0, 255 ); + } + + // dont use adjustedGoal, as it can vary wildly from the feeler adjustment + if (!IsAttacking() && IsFriendInTheWay( m_goalPosition )) + { + if (!m_isWaitingBehindFriend) + { + m_isWaitingBehindFriend = true; + + const float politeDuration = 5.0f - 3.0f * GetProfile()->GetAggression(); + m_politeTimer.Start( politeDuration ); + } + else if (m_politeTimer.IsElapsed()) + { + // we have run out of patience + m_isWaitingBehindFriend = false; + ResetStuckMonitor(); + + // repath to avoid clump of friends in the way + DestroyPath(); + } + } + else if (m_isWaitingBehindFriend) + { + // we're done waiting for our friend to move + m_isWaitingBehindFriend = false; + ResetStuckMonitor(); + } + + // + // Move along our path if there are no friends blocking our way, + // or we have run out of patience + // + if (!isWaitingForLadder && (!m_isWaitingBehindFriend || m_politeTimer.IsElapsed())) + { + // + // Move along path + // + MoveTowardsPosition( adjustedGoal ); + + // + // Stuck check + // + if (m_isStuck && !IsJumping()) + { + Wiggle(); + } + } + + // if our goal is high above us, we must have fallen + bool didFall = false; + if (m_goalPosition.z - GetFeetZ() > JumpCrouchHeight) + { + const float closeRange = 75.0f; + Vector2D to( myOrigin.x - m_goalPosition.x, myOrigin.y - m_goalPosition.y ); + if (to.IsLengthLessThan( closeRange )) + { + // we can't reach the goal position + // check if we can reach the next node, in case this was a "jump down" situation + if (m_pathIndex < m_pathLength-1) + { + if (m_path[ m_pathIndex+1 ].pos.z - GetFeetZ() > JumpCrouchHeight) + { + // the next node is too high, too - we really did fall of the path + didFall = true; + + for ( int i=m_pathIndex; i<=m_pathIndex+1; ++i ) + { + if ( m_path[i].how == GO_LADDER_UP ) + { + // if we're going up a ladder, and we're within reach of the ladder bottom, we haven't fallen + if ( m_path[i].pos.z - GetFeetZ() <= JumpCrouchHeight ) + { + didFall = false; + break; + } + } + } + } + } + else + { + // fell trying to get to the last node in the path + didFall = true; + } + } + } + + // + // This timeout check is needed if the bot somehow slips way off + // of its path and cannot progress, but also moves around + // enough that it never becomes "stuck" + // + const float giveUpDuration = 4.0f; + if (didFall || gpGlobals->curtime - m_areaEnteredTimestamp > giveUpDuration) + { + if (didFall) + { + PrintIfWatched( "I fell off!\n" ); + if (IsLocalPlayerWatchingMe() && cv_bot_debug.GetBool() && UTIL_GetListenServerHost()) + { + CBasePlayer *localPlayer = UTIL_GetListenServerHost(); + CSingleUserRecipientFilter filter( localPlayer ); + EmitSound( filter, localPlayer->entindex(), "Bot.FellOff" ); + } + } + + // if we havent made any progress in a long time, give up + if (m_pathIndex < m_pathLength-1) + { + PrintIfWatched( "Giving up trying to get to area #%d\n", m_path[ m_pathIndex ].area->GetID() ); + } + else + { + PrintIfWatched( "Giving up trying to get to end of path\n" ); + } + + Run(); + StandUp(); + DestroyPath(); + ClearLookAt(); + + // See if we should be on a different nav area + CNavArea *area = TheNavMesh->GetNearestNavArea( GetAbsOrigin(), false, 500.0f, true ); + if (area && area != m_lastNavArea) + { + if (m_lastNavArea) + { + m_lastNavArea->DecrementPlayerCount( GetTeamNumber(), entindex() ); + } + + area->IncrementPlayerCount( GetTeamNumber(), entindex() ); + + m_lastNavArea = area; + if ( area->GetPlace() != UNDEFINED_PLACE ) + { + const char *placeName = TheNavMesh->PlaceToName( area->GetPlace() ); + if ( placeName && *placeName ) + { + Q_strncpy( m_szLastPlaceName.GetForModify(), placeName, MAX_PLACE_NAME_LENGTH ); + } + } + + // generate event + //KeyValues *event = new KeyValues( "player_entered_area" ); + //event->SetInt( "userid", GetUserID() ); + //event->SetInt( "areaid", area->GetID() ); + //gameeventmanager->FireEvent( event ); + } + + return PATH_FAILURE; + } + + return PROGRESSING; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Build trivial path to goal, assuming we are already in the same area + */ +void CCSBot::BuildTrivialPath( const Vector &goal ) +{ + Vector myOrigin = GetCentroid( this ); + + m_pathIndex = 1; + m_pathLength = 2; + + m_path[0].area = m_lastKnownArea; + m_path[0].pos = myOrigin; + m_path[0].pos.z = m_lastKnownArea->GetZ( myOrigin ); + m_path[0].ladder = NULL; + m_path[0].how = NUM_TRAVERSE_TYPES; + + m_path[1].area = m_lastKnownArea; + m_path[1].pos = goal; + m_path[1].pos.z = m_lastKnownArea->GetZ( goal ); + m_path[1].ladder = NULL; + m_path[1].how = NUM_TRAVERSE_TYPES; + + m_areaEnteredTimestamp = gpGlobals->curtime; + m_spotEncounter = NULL; + m_pathLadder = NULL; + + m_goalPosition = goal; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute shortest path to goal position via A* algorithm + * If 'goalArea' is NULL, path will get as close as it can. + */ +bool CCSBot::ComputePath( const Vector &goal, RouteType route ) +{ + VPROF_BUDGET( "CCSBot::ComputePath", VPROF_BUDGETGROUP_NPCS ); + + // + // Throttle re-pathing + // + if (!m_repathTimer.IsElapsed()) + return false; + + // randomize to distribute CPU load + m_repathTimer.Start( RandomFloat( 0.4f, 0.6f ) ); + + + DestroyPath(); + + m_pathLadder = NULL; + + CNavArea *goalArea = TheNavMesh->GetNearestNavArea( goal ); + + CNavArea *startArea = m_lastKnownArea; + if (startArea == NULL) + return false; + + // if we fell off a ledge onto an area off the mesh, we will path from the + // ledge above our heads, resulting in a path we can't follow. + Vector close; + startArea->GetClosestPointOnArea( EyePosition(), &close ); + if (close.z - GetAbsOrigin().z > JumpCrouchHeight) + { + // we can't reach our last known area - find nearest area to us + PrintIfWatched( "Last known area is above my head - resetting to nearest area.\n" ); + m_lastKnownArea = (CCSNavArea*)TheNavMesh->GetNearestNavArea( GetAbsOrigin(), false, 500.0f, true ); + if (m_lastKnownArea == NULL) + { + return false; + } + + startArea = m_lastKnownArea; + } + + // note final specific position + Vector pathEndPosition = goal; + + // make sure path end position is on the ground + if (goalArea) + pathEndPosition.z = goalArea->GetZ( pathEndPosition ); + else + TheNavMesh->GetGroundHeight( pathEndPosition, &pathEndPosition.z ); + + // if we are already in the goal area, build trivial path + if (startArea == goalArea) + { + BuildTrivialPath( pathEndPosition ); + return true; + } + + // + // Compute shortest path to goal + // + CNavArea *closestArea = NULL; + PathCost cost( this, route ); + bool pathToGoalExists = NavAreaBuildPath( startArea, goalArea, &goal, cost, &closestArea ); + + CNavArea *effectiveGoalArea = (pathToGoalExists) ? goalArea : closestArea; + + // + // Build path by following parent links + // + + // get count + int count = 0; + CNavArea *area; + for( area = effectiveGoalArea; area; area = area->GetParent() ) + ++count; + + // save room for endpoint + if (count > MAX_PATH_LENGTH-1) + count = MAX_PATH_LENGTH-1; + + if (count == 0) + return false; + + if (count == 1) + { + BuildTrivialPath( pathEndPosition ); + return true; + } + + // build path + m_pathLength = count; + for( area = effectiveGoalArea; count && area; area = area->GetParent() ) + { + --count; + m_path[ count ].area = area; + m_path[ count ].how = area->GetParentHow(); + } + + // compute path positions + if (ComputePathPositions() == false) + { + PrintIfWatched( "Error building path\n" ); + DestroyPath(); + return false; + } + + // append path end position + m_path[ m_pathLength ].area = effectiveGoalArea; + m_path[ m_pathLength ].pos = pathEndPosition; + m_path[ m_pathLength ].ladder = NULL; + m_path[ m_pathLength ].how = NUM_TRAVERSE_TYPES; + ++m_pathLength; + + // do movement setup + m_pathIndex = 1; + m_areaEnteredTimestamp = gpGlobals->curtime; + m_spotEncounter = NULL; + m_goalPosition = m_path[1].pos; + + if (m_path[1].ladder) + SetupLadderMovement(); + else + m_pathLadder = NULL; + + // find initial encounter area along this path, if we are in the early part of the round + if (IsSafe()) + { + int myTeam = GetTeamNumber(); + int enemyTeam = OtherTeam( myTeam ); + int i; + + for( i=0; i<m_pathLength; ++i ) + { + if (m_path[i].area->GetEarliestOccupyTime( myTeam ) > m_path[i].area->GetEarliestOccupyTime( enemyTeam )) + { + break; + } + } + + if (i < m_pathLength) + { + SetInitialEncounterArea( m_path[i].area ); + } + else + { + SetInitialEncounterArea( NULL ); + } + } + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return estimated distance left to travel along path + */ +float CCSBot::GetPathDistanceRemaining( void ) const +{ + if (!HasPath()) + return -1.0f; + + int idx = (m_pathIndex < m_pathLength) ? m_pathIndex : m_pathLength-1; + + float dist = 0.0f; + Vector prevCenter = m_path[m_pathIndex].area->GetCenter(); + + for( int i=idx+1; i<m_pathLength; ++i ) + { + dist += (m_path[i].area->GetCenter() - prevCenter).Length(); + prevCenter = m_path[i].area->GetCenter(); + } + + return dist; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Draw a portion of our current path for debugging. + */ +void CCSBot::DrawPath( void ) +{ + if (!HasPath()) + return; + + for( int i=1; i<m_pathLength; ++i ) + { + UTIL_DrawBeamPoints( m_path[i-1].pos, m_path[i].pos, 2, 255, 75, 0 ); + } + + Vector close; + if (FindOurPositionOnPath( &close, true ) >= 0) + { + UTIL_DrawBeamPoints( close + Vector( 0, 0, 25 ), close, 1, 0, 255, 0 ); + UTIL_DrawBeamPoints( close + Vector( 25, 0, 0 ), close + Vector( -25, 0, 0 ), 1, 0, 255, 0 ); + UTIL_DrawBeamPoints( close + Vector( 0, 25, 0 ), close + Vector( 0, -25, 0 ), 1, 0, 255, 0 ); + } +} + diff --git a/game/server/cstrike/bot/cs_bot_radio.cpp b/game/server/cstrike/bot/cs_bot_radio.cpp new file mode 100644 index 0000000..08acb4e --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_radio.cpp @@ -0,0 +1,346 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern int gmsgBotVoice; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if the radio message is an order to do something + * NOTE: "Report in" is not considered a "command" because it doesnt ask the bot to go somewhere, or change its mind + */ +bool CCSBot::IsRadioCommand( RadioType event ) const +{ + if (event == RADIO_AFFIRMATIVE || + event == RADIO_NEGATIVE || + event == RADIO_ENEMY_SPOTTED || + event == RADIO_SECTOR_CLEAR || + event == RADIO_REPORTING_IN || + event == RADIO_REPORT_IN_TEAM || + event == RADIO_ENEMY_DOWN || + event == RADIO_INVALID ) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Respond to radio commands from HUMAN players + */ +void CCSBot::RespondToRadioCommands( void ) +{ + // bots use the chatter system to respond to each other + if (m_radioSubject != NULL && m_radioSubject->IsPlayer()) + { + CCSPlayer *player = m_radioSubject; + if (player->IsBot()) + { + m_lastRadioCommand = RADIO_INVALID; + return; + } + } + + if (m_lastRadioCommand == RADIO_INVALID) + return; + + // a human player has issued a radio command + GetChatter()->ResetRadioSilenceDuration(); + + + // if we are doing something important, ignore the radio + // unless it is a "report in" request - we can do that while we continue to do other things + /// @todo Create "uninterruptable" flag + if (m_lastRadioCommand != RADIO_REPORT_IN_TEAM) + { + if (IsBusy()) + { + // consume command + m_lastRadioCommand = RADIO_INVALID; + return; + } + } + + // wait for reaction time before responding + // delay needs to be long enough for the radio message we're responding to to finish + float respondTime = 1.0f + 2.0f * GetProfile()->GetReactionTime(); + if (IsRogue()) + respondTime += 2.0f; + + if (gpGlobals->curtime - m_lastRadioRecievedTimestamp < respondTime) + return; + + // rogues won't follow commands, unless already following the player + if (!IsFollowing() && IsRogue()) + { + if (IsRadioCommand( m_lastRadioCommand )) + { + GetChatter()->Negative(); + } + + // consume command + m_lastRadioCommand = RADIO_INVALID; + return; + } + + CCSPlayer *player = m_radioSubject; + if (player == NULL) + return; + + // respond to command + bool canDo = false; + const float inhibitAutoFollowDuration = 60.0f; + switch( m_lastRadioCommand ) + { + case RADIO_REPORT_IN_TEAM: + { + GetChatter()->ReportingIn(); + break; + } + + case RADIO_FOLLOW_ME: + case RADIO_COVER_ME: + case RADIO_STICK_TOGETHER_TEAM: + case RADIO_REGROUP_TEAM: + { + if (!IsFollowing()) + { + Follow( player ); + player->AllowAutoFollow(); + canDo = true; + } + break; + } + + case RADIO_ENEMY_SPOTTED: + case RADIO_NEED_BACKUP: + case RADIO_TAKING_FIRE: + if (!IsFollowing()) + { + Follow( player ); + GetChatter()->Say( "OnMyWay" ); + player->AllowAutoFollow(); + canDo = false; + } + break; + + case RADIO_TEAM_FALL_BACK: + { + if (TryToRetreat()) + canDo = true; + break; + } + + case RADIO_HOLD_THIS_POSITION: + { + // find the leader's area + SetTask( HOLD_POSITION ); + StopFollowing(); + player->InhibitAutoFollow( inhibitAutoFollowDuration ); + Hide( TheNavMesh->GetNearestNavArea( m_radioPosition ) ); + canDo = true; + break; + } + + case RADIO_GO_GO_GO: + case RADIO_STORM_THE_FRONT: + StopFollowing(); + Hunt(); + canDo = true; + player->InhibitAutoFollow( inhibitAutoFollowDuration ); + break; + + case RADIO_GET_OUT_OF_THERE: + if (TheCSBots()->IsBombPlanted()) + { + EscapeFromBomb(); + player->InhibitAutoFollow( inhibitAutoFollowDuration ); + canDo = true; + } + break; + + case RADIO_SECTOR_CLEAR: + { + // if this is a defusal scenario, and the bomb is planted, + // and a human player cleared a bombsite, check it off our list too + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB) + { + if (GetTeamNumber() == TEAM_CT && TheCSBots()->IsBombPlanted()) + { + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( player ); + + if (zone) + { + GetGameState()->ClearBombsite( zone->m_index ); + + // if we are huting for the planted bomb, re-select bombsite + if (GetTask() == FIND_TICKING_BOMB) + Idle(); + + canDo = true; + } + } + } + break; + } + + default: + // ignore all other radio commands for now + return; + } + + if (canDo) + { + // affirmative + GetChatter()->Affirmative(); + + // if we agreed to follow a new command, put away our grenade + if (IsRadioCommand( m_lastRadioCommand ) && IsUsingGrenade()) + { + EquipBestWeapon(); + } + } + + // consume command + m_lastRadioCommand = RADIO_INVALID; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Decide if we should move to help the player, return true if we will + */ +bool CCSBot::RespondToHelpRequest( CCSPlayer *them, Place place, float maxRange ) +{ + if (IsRogue()) + return false; + + // if we're busy, ignore + if (IsBusy()) + return false; + + Vector themOrigin = GetCentroid( them ); + + // if we are too far away, ignore + if (maxRange > 0.0f) + { + // compute actual travel distance + PathCost cost(this); + float travelDistance = NavAreaTravelDistance( m_lastKnownArea, TheNavMesh->GetNearestNavArea( themOrigin ), cost ); + if (travelDistance < 0.0f) + return false; + + if (travelDistance > maxRange) + return false; + } + + + if (place == UNDEFINED_PLACE) + { + // if we have no "place" identifier, go directly to them + + // if we are already there, ignore + float rangeSq = (them->GetAbsOrigin() - GetAbsOrigin()).LengthSqr(); + const float close = 750.0f * 750.0f; + if (rangeSq < close) + return true; + + MoveTo( themOrigin, FASTEST_ROUTE ); + } + else + { + // if we are already there, ignore + if (GetPlace() == place) + return true; + + // go to where help is needed + const Vector *pos = GetRandomSpotAtPlace( place ); + if (pos) + { + MoveTo( *pos, FASTEST_ROUTE ); + } + else + { + MoveTo( themOrigin, FASTEST_ROUTE ); + } + } + + // acknowledge + GetChatter()->Say( "OnMyWay" ); + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Send a radio message + */ +void CCSBot::SendRadioMessage( RadioType event ) +{ + // make sure this is a radio event + if (event <= RADIO_START_1 || event >= RADIO_END) + return; + + PrintIfWatched( "%3.1f: SendRadioMessage( %s )\n", gpGlobals->curtime, RadioEventName[ event ] ); + + // note the time the message was sent + TheCSBots()->SetRadioMessageTimestamp( event, GetTeamNumber() ); + + m_lastRadioSentTimestamp = gpGlobals->curtime; + + char slot[2]; + slot[1] = '\000'; + + if (event > RADIO_START_1 && event < RADIO_START_2) + { + HandleMenu_Radio1( event - RADIO_START_1 ); + } + else if (event > RADIO_START_2 && event < RADIO_START_3) + { + HandleMenu_Radio2( event - RADIO_START_2 ); + } + else + { + HandleMenu_Radio3( event - RADIO_START_3 ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Send voice chatter. Also sends the entindex and duration for voice feedback. + */ +void CCSBot::SpeakAudio( const char *voiceFilename, float duration, int pitch ) +{ + if( !IsAlive() ) + return; + + if ( IsObserver() ) + return; + + CRecipientFilter filter; + ConstructRadioFilter( filter ); + + UserMessageBegin ( filter, "RawAudio" ); + WRITE_BYTE( pitch ); + WRITE_BYTE( entindex() ); + WRITE_FLOAT( duration ); + WRITE_STRING( voiceFilename ); + MessageEnd(); + + GetChatter()->ResetRadioSilenceDuration(); + + m_voiceEndTimestamp = gpGlobals->curtime + duration; +} + diff --git a/game/server/cstrike/bot/cs_bot_statemachine.cpp b/game/server/cstrike/bot/cs_bot_statemachine.cpp new file mode 100644 index 0000000..8cbf9aa --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_statemachine.cpp @@ -0,0 +1,693 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "cs_nav_path.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +/** + * This method is the ONLY legal way to change a bot's current state + */ +void CCSBot::SetState( BotState *state ) +{ + PrintIfWatched( "%s: SetState: %s -> %s\n", GetPlayerName(), (m_state) ? m_state->GetName() : "NULL", state->GetName() ); + + /* + if ( IsDefusingBomb() ) + { + const Vector *bombPos = GetGameState()->GetBombPosition(); + if ( bombPos != NULL ) + { + if ( TheCSBots()->GetBombDefuser() == this ) + { + if ( TheCSBots()->IsBombPlanted() ) + { + Msg( "Bot %s is switching from defusing the bomb to %s\n", + GetPlayerName(), state->GetName() ); + } + } + } + } + */ + + // if we changed state from within the special Attack state, we are no longer attacking + if (m_isAttacking) + StopAttacking(); + + if (m_state) + m_state->OnExit( this ); + + state->OnEnter( this ); + + m_state = state; + m_stateTimestamp = gpGlobals->curtime; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Idle( void ) +{ + SetTask( SEEK_AND_DESTROY ); + SetState( &m_idleState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::EscapeFromBomb( void ) +{ + SetTask( ESCAPE_FROM_BOMB ); + SetState( &m_escapeFromBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Follow( CCSPlayer *player ) +{ + if (player == NULL) + return; + + // note when we began following + if (!m_isFollowing || m_leader != player) + m_followTimestamp = gpGlobals->curtime; + + m_isFollowing = true; + m_leader = player; + + SetTask( FOLLOW ); + m_followState.SetLeader( player ); + SetState( &m_followState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Continue following our leader after finishing what we were doing + */ +void CCSBot::ContinueFollowing( void ) +{ + SetTask( FOLLOW ); + + m_followState.SetLeader( m_leader ); + + SetState( &m_followState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Stop following + */ +void CCSBot::StopFollowing( void ) +{ + m_isFollowing = false; + m_leader = NULL; + m_allowAutoFollowTime = gpGlobals->curtime + 10.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin process of rescuing hostages + */ +void CCSBot::RescueHostages( void ) +{ + SetTask( RESCUE_HOSTAGES ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Use the entity + */ +void CCSBot::UseEntity( CBaseEntity *entity ) +{ + m_useEntityState.SetEntity( entity ); + SetState( &m_useEntityState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Open the door. + * This assumes the bot is directly in front of the door with no obstructions. + * NOTE: This state is special, like Attack, in that it suspends the current behavior and returns to it when done. + */ +void CCSBot::OpenDoor( CBaseEntity *door ) +{ + m_openDoorState.SetDoor( door ); + m_isOpeningDoor = true; + m_openDoorState.OnEnter( this ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * DEPRECATED: Use TryToHide() instead. + * Move to a hiding place. + * If 'searchFromArea' is non-NULL, hiding spots are looked for from that area first. + */ +void CCSBot::Hide( CNavArea *searchFromArea, float duration, float hideRange, bool holdPosition ) +{ + DestroyPath(); + + CNavArea *source; + Vector sourcePos; + if (searchFromArea) + { + source = searchFromArea; + sourcePos = searchFromArea->GetCenter(); + } + else + { + source = m_lastKnownArea; + sourcePos = GetCentroid( this ); + } + + if (source == NULL) + { + PrintIfWatched( "Hide from area is NULL.\n" ); + Idle(); + return; + } + + m_hideState.SetSearchArea( source ); + m_hideState.SetSearchRange( hideRange ); + m_hideState.SetDuration( duration ); + m_hideState.SetHoldPosition( holdPosition ); + + // search around source area for a good hiding spot + Vector useSpot; + + const Vector *pos = FindNearbyHidingSpot( this, sourcePos, hideRange, IsSniper() ); + if (pos == NULL) + { + PrintIfWatched( "No available hiding spots.\n" ); + // hide at our current position + useSpot = GetCentroid( this ); + } + else + { + useSpot = *pos; + } + + m_hideState.SetHidingSpot( useSpot ); + + // build a path to our new hiding spot + if (ComputePath( useSpot, FASTEST_ROUTE ) == false) + { + PrintIfWatched( "Can't pathfind to hiding spot\n" ); + Idle(); + return; + } + + SetState( &m_hideState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to the given hiding place + */ +void CCSBot::Hide( const Vector &hidingSpot, float duration, bool holdPosition ) +{ + CNavArea *hideArea = TheNavMesh->GetNearestNavArea( hidingSpot ); + if (hideArea == NULL) + { + PrintIfWatched( "Hiding spot off nav mesh\n" ); + Idle(); + return; + } + + DestroyPath(); + + m_hideState.SetSearchArea( hideArea ); + m_hideState.SetSearchRange( 750.0f ); + m_hideState.SetDuration( duration ); + m_hideState.SetHoldPosition( holdPosition ); + m_hideState.SetHidingSpot( hidingSpot ); + + // build a path to our new hiding spot + if (ComputePath( hidingSpot, FASTEST_ROUTE ) == false) + { + PrintIfWatched( "Can't pathfind to hiding spot\n" ); + Idle(); + return; + } + + SetState( &m_hideState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Try to hide nearby. Return true if hiding, false if can't hide here. + * If 'searchFromArea' is non-NULL, hiding spots are looked for from that area first. + */ +bool CCSBot::TryToHide( CNavArea *searchFromArea, float duration, float hideRange, bool holdPosition, bool useNearest ) +{ + CNavArea *source; + Vector sourcePos; + if (searchFromArea) + { + source = searchFromArea; + sourcePos = searchFromArea->GetCenter(); + } + else + { + source = m_lastKnownArea; + sourcePos = GetCentroid( this ); + } + + if (source == NULL) + { + PrintIfWatched( "Hide from area is NULL.\n" ); + return false; + } + + m_hideState.SetSearchArea( source ); + m_hideState.SetSearchRange( hideRange ); + m_hideState.SetDuration( duration ); + m_hideState.SetHoldPosition( holdPosition ); + + // search around source area for a good hiding spot + const Vector *pos = FindNearbyHidingSpot( this, sourcePos, hideRange, IsSniper(), useNearest ); + if (pos == NULL) + { + PrintIfWatched( "No available hiding spots.\n" ); + return false; + } + + m_hideState.SetHidingSpot( *pos ); + + // build a path to our new hiding spot + if (ComputePath( *pos, FASTEST_ROUTE ) == false) + { + PrintIfWatched( "Can't pathfind to hiding spot\n" ); + return false; + } + + SetState( &m_hideState ); + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Retreat to a nearby hiding spot, away from enemies + */ +bool CCSBot::TryToRetreat( float maxRange, float duration ) +{ + const Vector *spot = FindNearbyRetreatSpot( this, maxRange ); + if (spot) + { + // ignore enemies for a second to give us time to hide + // reaching our hiding spot clears our disposition + IgnoreEnemies( 10.0f ); + + if (duration < 0.0f) + { + duration = RandomFloat( 3.0f, 15.0f ); + } + + StandUp(); + Run(); + Hide( *spot, duration ); + + PrintIfWatched( "Retreating to a safe spot!\n" ); + + return true; + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Hunt( void ) +{ + SetState( &m_huntState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Attack our the given victim + * NOTE: Attacking does not change our task. + */ +void CCSBot::Attack( CCSPlayer *victim ) +{ + if (victim == NULL) + return; + + // zombies never attack + if (cv_bot_zombie.GetBool()) + return; + + // cannot attack if we are reloading + if (IsReloading()) + return; + + // change enemy + SetBotEnemy( victim ); + + // + // Do not "re-enter" the attack state if we are already attacking + // + if (IsAttacking()) + return; + + // if we're holding a grenade, throw it at the victim + if (IsUsingGrenade()) + { + // throw towards their feet + ThrowGrenade( victim->GetAbsOrigin() ); + return; + } + + + // if we are currently hiding, increase our chances of crouching and holding position + if (IsAtHidingSpot()) + m_attackState.SetCrouchAndHold( (RandomFloat( 0.0f, 100.0f ) < 60.0f) ? true : false ); + else + m_attackState.SetCrouchAndHold( false ); + + //SetState( &m_attackState ); + //PrintIfWatched( "ATTACK BEGIN (reaction time = %g (+ update time), surprise time = %g, attack delay = %g)\n", + // GetProfile()->GetReactionTime(), m_surpriseDelay, GetProfile()->GetAttackDelay() ); + m_isAttacking = true; + m_attackState.OnEnter( this ); + + + Vector victimOrigin = GetCentroid( victim ); + + // cheat a bit and give the bot the initial location of its victim + m_lastEnemyPosition = victimOrigin; + m_lastSawEnemyTimestamp = gpGlobals->curtime; + m_aimSpreadTimestamp = gpGlobals->curtime; + + // compute the angle difference between where are looking, and where we need to look + Vector toEnemy = victimOrigin - GetCentroid( this ); + + QAngle idealAngle; + VectorAngles( toEnemy, idealAngle ); + + float deltaYaw = (float)fabs(m_lookYaw - idealAngle.y); + + while( deltaYaw > 180.0f ) + deltaYaw -= 360.0f; + + if (deltaYaw < 0.0f) + deltaYaw = -deltaYaw; + + // immediately aim at enemy - accuracy penalty depending on how far we must turn to aim + // accuracy is halved if we have to turn 180 degrees + float turn = deltaYaw / 180.0f; + float accuracy = GetProfile()->GetSkill() / (1.0f + turn); + + SetAimOffset( accuracy ); + + // define time when aim offset will automatically be updated + // longer time the more we had to turn (surprise) + m_aimOffsetTimestamp = gpGlobals->curtime + RandomFloat( 0.25f + turn, 1.5f ); + + // forget any look at targets we have + ClearLookAt(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Exit the Attack state + */ +void CCSBot::StopAttacking( void ) +{ + PrintIfWatched( "ATTACK END\n" ); + m_attackState.OnExit( this ); + m_isAttacking = false; + + // if we are following someone, go to the Idle state after the attack to decide whether we still want to follow + if (IsFollowing()) + { + Idle(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CCSBot::IsAttacking( void ) const +{ + return m_isAttacking; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are escaping from the bomb + */ +bool CCSBot::IsEscapingFromBomb( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_escapeFromBombState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are defusing the bomb + */ +bool CCSBot::IsDefusingBomb( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_defuseBombState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are hiding + */ +bool CCSBot::IsHiding( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_hideState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are hiding and at our hiding spot + */ +bool CCSBot::IsAtHidingSpot( void ) const +{ + if (!IsHiding()) + return false; + + return m_hideState.IsAtSpot(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return number of seconds we have been at our current hiding spot + */ +float CCSBot::GetHidingTime( void ) const +{ + if (IsHiding()) + { + return m_hideState.GetHideTime(); + } + + return 0.0f; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are huting + */ +bool CCSBot::IsHunting( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_huntState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are in the MoveTo state + */ +bool CCSBot::IsMovingTo( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_moveToState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are buying + */ +bool CCSBot::IsBuying( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_buyState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +bool CCSBot::IsInvestigatingNoise( void ) const +{ + if (m_state == static_cast<const BotState *>( &m_investigateNoiseState )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to potentially distant position + */ +void CCSBot::MoveTo( const Vector &pos, RouteType route ) +{ + m_moveToState.SetGoalPosition( pos ); + m_moveToState.SetRouteType( route ); + SetState( &m_moveToState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::PlantBomb( void ) +{ + SetState( &m_plantBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Bomb has been dropped - go get it + */ +void CCSBot::FetchBomb( void ) +{ + SetState( &m_fetchBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::DefuseBomb( void ) +{ + SetState( &m_defuseBombState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Investigate recent enemy noise + */ +void CCSBot::InvestigateNoise( void ) +{ + SetState( &m_investigateNoiseState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::Buy( void ) +{ + SetState( &m_buyState ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a hiding spot and wait for initial encounter with enemy team. + * Return false if can't do this behavior (ie: no hiding spots available). + */ +bool CCSBot::MoveToInitialEncounter( void ) +{ + int myTeam = GetTeamNumber(); + int enemyTeam = OtherTeam( myTeam ); + + // build a path to an enemy spawn point + CBaseEntity *enemySpawn = TheCSBots()->GetRandomSpawn( enemyTeam ); + + if (enemySpawn == NULL) + { + PrintIfWatched( "MoveToInitialEncounter: No enemy spawn points?\n" ); + return false; + } + + // build a path from us to the enemy spawn + CCSNavPath path; + PathCost cost( this, FASTEST_ROUTE ); + path.Compute( WorldSpaceCenter(), enemySpawn->GetAbsOrigin(), cost ); + + if (!path.IsValid()) + { + PrintIfWatched( "MoveToInitialEncounter: Pathfind failed.\n" ); + return false; + } + + // find battlefront area where teams will first meet along this path + int i; + for( i=0; i<path.GetSegmentCount(); ++i ) + { + if (path[i]->area->GetEarliestOccupyTime( myTeam ) > path[i]->area->GetEarliestOccupyTime( enemyTeam )) + { + break; + } + } + + if (i == path.GetSegmentCount()) + { + PrintIfWatched( "MoveToInitialEncounter: Can't find battlefront!\n" ); + return false; + } + + /// @todo Remove this evil side-effect + SetInitialEncounterArea( path[i]->area ); + + // find a hiding spot on our side of the battlefront that has LOS to it + const float maxRange = 1500.0f; + const HidingSpot *spot = FindInitialEncounterSpot( this, path[i]->area->GetCenter(), path[i]->area->GetEarliestOccupyTime( enemyTeam ), maxRange, IsSniper() ); + + if (spot == NULL) + { + PrintIfWatched( "MoveToInitialEncounter: Can't find a hiding spot\n" ); + return false; + } + + float timeToWait = path[i]->area->GetEarliestOccupyTime( enemyTeam ) - spot->GetArea()->GetEarliestOccupyTime( myTeam ); + float minWaitTime = 4.0f * GetProfile()->GetAggression() + 3.0f; + if (timeToWait < minWaitTime) + { + timeToWait = minWaitTime; + } + + Hide( spot->GetPosition(), timeToWait ); + + return true; +} + diff --git a/game/server/cstrike/bot/cs_bot_update.cpp b/game/server/cstrike/bot/cs_bot_update.cpp new file mode 100644 index 0000000..d57af0a --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_update.cpp @@ -0,0 +1,1211 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_bot.h" +#include "fmtstr.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------------------------------------- +float CCSBot::GetMoveSpeed( void ) +{ + return 250.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Lightweight maintenance, invoked frequently + */ +void CCSBot::Upkeep( void ) +{ + VPROF_BUDGET( "CCSBot::Upkeep", VPROF_BUDGETGROUP_NPCS ); + + if (TheNavMesh->IsGenerating() || !IsAlive()) + return; + + // If bot_flipout is on, then generate some random commands. + if ( cv_bot_flipout.GetBool() ) + { + int val = RandomInt( 0, 2 ); + if ( val == 0 ) + MoveForward(); + else if ( val == 1 ) + MoveBackward(); + + val = RandomInt( 0, 2 ); + if ( val == 0 ) + StrafeLeft(); + else if ( val == 1 ) + StrafeRight(); + + if ( RandomInt( 0, 5 ) == 0 ) + Jump( true ); + + val = RandomInt( 0, 2 ); + if ( val == 0 ) + Crouch(); + else ( val == 1 ); + StandUp(); + + return; + } + + // BOTPORT: Remove this nasty hack + m_eyePosition = EyePosition(); + + Vector myOrigin = GetCentroid( this ); + + // aiming must be smooth - update often + if (IsAimingAtEnemy()) + { + UpdateAimOffset(); + + // aim at enemy, if he's still alive + if (m_enemy != NULL && m_enemy->IsAlive()) + { + Vector enemyOrigin = GetCentroid( m_enemy ); + + if (m_isEnemyVisible) + { + // + // Enemy is visible - determine which part of him to shoot at + // + const float sharpshooter = 0.8f; + VisiblePartType aimAtPart; + + if (IsUsingMachinegun()) + { + // spray the big machinegun at the enemy's gut + aimAtPart = GUT; + } + else if (IsUsing( WEAPON_AWP ) || IsUsingShotgun()) + { + // these weapons are best aimed at the chest + aimAtPart = GUT; + } + else if (GetProfile()->GetSkill() > 0.5f && IsActiveWeaponRecoilHigh() ) + { + // sprayin' and prayin' - aim at the gut since we're not going to be accurate + aimAtPart = GUT; + } + else if (GetProfile()->GetSkill() < sharpshooter) + { + // low skill bots don't go for headshots + aimAtPart = GUT; + } + else + { + // high skill - aim for the head + aimAtPart = HEAD; + } + + if (IsEnemyPartVisible( aimAtPart )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), aimAtPart ); + } + else + { + // desired part is blocked - aim at whatever part is visible + if (IsEnemyPartVisible( GUT )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), GUT ); + } + else if (IsEnemyPartVisible( HEAD )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), HEAD ); + } + else if (IsEnemyPartVisible( LEFT_SIDE )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), LEFT_SIDE ); + } + else if (IsEnemyPartVisible( RIGHT_SIDE )) + { + m_aimSpot = GetPartPosition( GetBotEnemy(), RIGHT_SIDE ); + } + else // FEET + { + m_aimSpot = GetPartPosition( GetBotEnemy(), FEET ); + } + } + + // high skill bots lead the target a little to compensate for update tick latency + /* + if (false && GetProfile()->GetSkill() > 0.5f) + { + const float k = 1.0f; + m_aimSpot += k * g_flBotCommandInterval * (m_enemy->GetAbsVelocity() - GetAbsVelocity()); + } + */ + + } + else + { + // aim where we last saw enemy - but bend the ray so we dont point directly into walls + // if we put this back, make sure you only bend the ray ONCE and keep the bent spot - dont continually recompute + //BendLineOfSight( m_eyePosition, m_lastEnemyPosition, &m_aimSpot ); + m_aimSpot = m_lastEnemyPosition; + } + + // add in aim error + m_aimSpot.x += m_aimOffset.x; + m_aimSpot.y += m_aimOffset.y; + m_aimSpot.z += m_aimOffset.z; + + Vector to = m_aimSpot - EyePositionConst(); + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + // adjust aim angle for recoil, based on bot skill + const QAngle &punchAngles = GetPunchAngle(); + idealAngle -= punchAngles * GetProfile()->GetSkill(); + + SetLookAngles( idealAngle.y, idealAngle.x ); + } + } + else + { + if (m_lookAtSpotClearIfClose) + { + // dont look at spots just in front of our face - it causes erratic view rotation + const float tooCloseRange = 100.0f; + if ((m_lookAtSpot - myOrigin).IsLengthLessThan( tooCloseRange )) + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + } + + switch( m_lookAtSpotState ) + { + case NOT_LOOKING_AT_SPOT: + { + // look ahead + SetLookAngles( m_lookAheadAngle, 0.0f ); + break; + } + + case LOOK_TOWARDS_SPOT: + { + UpdateLookAt(); + if (IsLookingAtPosition( m_lookAtSpot, m_lookAtSpotAngleTolerance )) + { + m_lookAtSpotState = LOOK_AT_SPOT; + m_lookAtSpotTimestamp = gpGlobals->curtime; + } + break; + } + + case LOOK_AT_SPOT: + { + UpdateLookAt(); + + if (m_lookAtSpotDuration >= 0.0f && gpGlobals->curtime - m_lookAtSpotTimestamp > m_lookAtSpotDuration) + { + m_lookAtSpotState = NOT_LOOKING_AT_SPOT; + m_lookAtSpotDuration = 0.0f; + } + break; + } + } + + // have view "drift" very slowly, so view looks "alive" + if (!IsUsingSniperRifle()) + { + float driftAmplitude = 2.0f; + if (IsBlind()) + { + driftAmplitude = 5.0f; + } + + m_lookYaw += driftAmplitude * BotCOS( 33.0f * gpGlobals->curtime ); + m_lookPitch += driftAmplitude * BotSIN( 13.0f * gpGlobals->curtime ); + } + } + + // view angles can change quickly + UpdateLookAngles(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Heavyweight processing, invoked less often + */ +void CCSBot::Update( void ) +{ + VPROF_BUDGET( "CCSBot::Update", VPROF_BUDGETGROUP_NPCS ); + + // If bot_flipout is on, then we only do stuff in Upkeep(). + if ( cv_bot_flipout.GetBool() ) + return; + + Vector myOrigin = GetCentroid( this ); + + // if we are spectating, get into the game + if (GetTeamNumber() == 0) + { + HandleCommand_JoinTeam( m_desiredTeam ); + int desiredClass = GetProfile()->GetSkin(); + if ( m_desiredTeam == TEAM_CT && desiredClass ) + { + desiredClass = FIRST_CT_CLASS + desiredClass - 1; + } + else if ( m_desiredTeam == TEAM_TERRORIST && desiredClass ) + { + desiredClass = FIRST_T_CLASS + desiredClass - 1; + } + HandleCommand_JoinClass( desiredClass ); + return; + } + + + // update our radio chatter + // need to allow bots to finish their chatter even if they are dead + GetChatter()->Update(); + + // check if we are dead + if (!IsAlive()) + { + // remember that we died + m_diedLastRound = true; + + BotDeathThink(); + return; + } + + // the bot is alive and in the game at this point + m_hasJoined = true; + + // + // Debug beam rendering + // + + if (cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + DebugDisplay(); + } + + if (cv_bot_stop.GetBool()) + return; + + // check if we are stuck + StuckCheck(); + + // Check for physics props and other breakables in our way that we can break + BreakablesCheck(); + + // Check for useable doors in our way that we need to open + DoorCheck(); + + // update travel distance to all players (this is an optimization) + UpdateTravelDistanceToAllPlayers(); + + // if our current 'noise' was heard a long time ago, forget it + const float rememberNoiseDuration = 20.0f; + if (m_noiseTimestamp > 0.0f && gpGlobals->curtime - m_noiseTimestamp > rememberNoiseDuration) + { + ForgetNoise(); + } + + // where are we + if (!m_currentArea || !m_currentArea->Contains( myOrigin )) + { + m_currentArea = (CCSNavArea *)TheNavMesh->GetNavArea( myOrigin ); + } + + // track the last known area we were in + if (m_currentArea && m_currentArea != m_lastKnownArea) + { + m_lastKnownArea = m_currentArea; + + OnEnteredNavArea( m_currentArea ); + } + + // keep track of how long we have been motionless + const float stillSpeed = 10.0f; + if (GetAbsVelocity().IsLengthLessThan( stillSpeed )) + { + m_stillTimer.Start(); + } + else + { + m_stillTimer.Invalidate(); + } + + // if we're blind, retreat! + if (IsBlind()) + { + if (m_blindFire) + { + PrimaryAttack(); + } + } + + UpdatePanicLookAround(); + + // + // Enemy acquisition and attack initiation + // + + // take a snapshot and update our reaction time queue + UpdateReactionQueue(); + + // "threat" may be the same as our current enemy + CCSPlayer *threat = GetRecognizedEnemy(); + if (threat) + { + Vector threatOrigin = GetCentroid( threat ); + + // adjust our personal "safe" time + AdjustSafeTime(); + + BecomeAlert(); + + const float selfDefenseRange = 500.0f; // 750.0f; + const float farAwayRange = 2000.0f; + + // + // Decide if we should attack + // + bool doAttack = false; + switch( GetDisposition() ) + { + case IGNORE_ENEMIES: + { + // never attack + doAttack = false; + break; + } + + case SELF_DEFENSE: + { + // attack if fired on + doAttack = (IsPlayerLookingAtMe( threat, 0.99f ) && DidPlayerJustFireWeapon( threat )); + + // attack if enemy very close + if (!doAttack) + { + doAttack = (myOrigin - threatOrigin).IsLengthLessThan( selfDefenseRange ); + } + + break; + } + + case ENGAGE_AND_INVESTIGATE: + case OPPORTUNITY_FIRE: + { + if ((myOrigin - threatOrigin).IsLengthGreaterThan( farAwayRange )) + { + // enemy is very far away - wait to take our shot until he is closer + // unless we are a sniper or he is shooting at us + if (IsSniper()) + { + // snipers love far away targets + doAttack = true; + } + else + { + // attack if fired on + doAttack = (IsPlayerLookingAtMe( threat, 0.99f ) && DidPlayerJustFireWeapon( threat )); + } + } + else + { + // normal combat range + doAttack = true; + } + + break; + } + } + + // if we aren't attacking but we are being attacked, retaliate + if (!doAttack && !IsAttacking() && GetDisposition() != IGNORE_ENEMIES) + { + const float recentAttackDuration = 1.0f; + if (GetTimeSinceAttacked() < recentAttackDuration) + { + // we may not be attacking our attacker, but at least we're not just taking it + // (since m_attacker isn't reaction-time delayed, we can't directly use it) + doAttack = true; + PrintIfWatched( "Ouch! Retaliating!\n" ); + } + } + + if (doAttack) + { + if (!IsAttacking() || threat != GetBotEnemy()) + { + if (IsUsingKnife() && IsHiding()) + { + // if hiding with a knife, wait until threat is close + const float knifeAttackRange = 250.0f; + if ((GetAbsOrigin() - threat->GetAbsOrigin()).IsLengthLessThan( knifeAttackRange )) + { + Attack( threat ); + } + } + else + { + Attack( threat ); + } + } + } + else + { + // dont attack, but keep track of nearby enemies + SetBotEnemy( threat ); + m_isEnemyVisible = true; + } + + TheCSBots()->SetLastSeenEnemyTimestamp(); + } + + // + // Validate existing enemy, if any + // + if (m_enemy != NULL) + { + if (IsAwareOfEnemyDeath()) + { + // we have noticed that our enemy has died + m_enemy = NULL; + m_isEnemyVisible = false; + } + else + { + // check LOS to current enemy (chest & head), in case he's dead (GetNearestEnemy() only returns live players) + // note we're not checking FOV - once we've acquired an enemy (which does check FOV), assume we know roughly where he is + if (IsVisible( m_enemy, false, &m_visibleEnemyParts )) + { + m_isEnemyVisible = true; + m_lastSawEnemyTimestamp = gpGlobals->curtime; + m_lastEnemyPosition = GetCentroid( m_enemy ); + } + else + { + m_isEnemyVisible = false; + } + + // check if enemy died + if (m_enemy->IsAlive()) + { + m_enemyDeathTimestamp = 0.0f; + m_isLastEnemyDead = false; + } + else if (m_enemyDeathTimestamp == 0.0f) + { + // note time of death (to allow bots to overshoot for a time) + m_enemyDeathTimestamp = gpGlobals->curtime; + m_isLastEnemyDead = true; + } + } + } + else + { + m_isEnemyVisible = false; + } + + + // if we have seen an enemy recently, keep an eye on him if we can + if (!IsBlind() && !IsLookingAtSpot(PRIORITY_UNINTERRUPTABLE) ) + { + const float seenRecentTime = 3.0f; + if (m_enemy != NULL && GetTimeSinceLastSawEnemy() < seenRecentTime) + { + AimAtEnemy(); + } + else + { + StopAiming(); + } + } + else if( IsAimingAtEnemy() ) + { + StopAiming(); + } + + // + // Hack to fire while retreating + /// @todo Encapsulate aiming and firing on enemies separately from current task + // + if (GetDisposition() == IGNORE_ENEMIES) + { + FireWeaponAtEnemy(); + } + + // toss grenades + LookForGrenadeTargets(); + + // process grenade throw state machine + UpdateGrenadeThrow(); + + // avoid enemy grenades + AvoidEnemyGrenades(); + + + // check if our weapon is totally out of ammo + // or if we no longer feel "safe", equip our weapon + if (!IsSafe() && !IsUsingGrenade() && IsActiveWeaponOutOfAmmo()) + { + EquipBestWeapon(); + } + + /// @todo This doesn't work if we are restricted to just knives and sniper rifles because we cant use the rifle at close range + if (!IsSafe() && !IsUsingGrenade() && IsUsingKnife() && !IsEscapingFromBomb()) + { + EquipBestWeapon(); + } + + // if we haven't seen an enemy in awhile, and we switched to our pistol during combat, + // switch back to our primary weapon (if it still has ammo left) + const float safeRearmTime = 5.0f; + if (!IsReloading() && IsUsingPistol() && !IsPrimaryWeaponEmpty() && GetTimeSinceLastSawEnemy() > safeRearmTime) + { + EquipBestWeapon(); + } + + // reload our weapon if we must + ReloadCheck(); + + // equip silencer + SilencerCheck(); + + // listen to the radio + RespondToRadioCommands(); + + // make way + const float avoidTime = 0.33f; + if (gpGlobals->curtime - m_avoidTimestamp < avoidTime && m_avoid != NULL) + { + StrafeAwayFromPosition( GetCentroid( m_avoid ) ); + } + else + { + m_avoid = NULL; + } + + // if we're using a sniper rifle and are no longer attacking, stop looking thru scope + if (!IsAtHidingSpot() && !IsAttacking() && IsUsingSniperRifle() && IsUsingScope()) + { + SecondaryAttack(); + } + + if (!IsBlind()) + { + // check encounter spots + UpdatePeripheralVision(); + + // watch for snipers + if (CanSeeSniper() && !HasSeenSniperRecently()) + { + GetChatter()->SpottedSniper(); + + const float sniperRecentInterval = 20.0f; + m_sawEnemySniperTimer.Start( sniperRecentInterval ); + } + + // + // Update gamestate + // + if (m_bomber != NULL) + GetChatter()->SpottedBomber( GetBomber() ); + + if (CanSeeLooseBomb()) + GetChatter()->SpottedLooseBomb( TheCSBots()->GetLooseBomb() ); + } + + // + // Scenario interrupts + // + switch (TheCSBots()->GetScenario()) + { + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + // flee if the bomb is ready to blow and we aren't defusing it or attacking and we know where the bomb is + // (aggressive players wait until its almost too late) + float gonnaBlowTime = 8.0f - (2.0f * GetProfile()->GetAggression()); + + // if we have a defuse kit, can wait longer + if (m_bHasDefuser) + gonnaBlowTime *= 0.66f; + + if (!IsEscapingFromBomb() && // we aren't already escaping the bomb + TheCSBots()->IsBombPlanted() && // is the bomb planted + GetGameState()->IsPlantedBombLocationKnown() && // we know where the bomb is + TheCSBots()->GetBombTimeLeft() < gonnaBlowTime && // is the bomb about to explode + !IsDefusingBomb() && // we aren't defusing the bomb + !IsAttacking()) // we aren't in the midst of a firefight + { + EscapeFromBomb(); + break; + } + + break; + } + + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + if (GetTeamNumber() == TEAM_CT) + { + UpdateHostageEscortCount(); + } + else + { + // Terrorists have imperfect information on status of hostages + unsigned char status = GetGameState()->ValidateHostagePositions(); + + if (status & CSGameState::HOSTAGES_ALL_GONE) + { + GetChatter()->HostagesTaken(); + Idle(); + } + else if (status & CSGameState::HOSTAGE_GONE) + { + GetGameState()->HostageWasTaken(); + Idle(); + } + } + break; + } + } + + + // + // Follow nearby humans if our co-op is high and we have nothing else to do + // If we were just following someone, don't auto-follow again for a short while to + // give us a chance to do something else. + // + const float earliestAutoFollowTime = 5.0f; + const float minAutoFollowTeamwork = 0.4f; + if (cv_bot_auto_follow.GetBool() && + TheCSBots()->GetElapsedRoundTime() > earliestAutoFollowTime && + GetProfile()->GetTeamwork() > minAutoFollowTeamwork && + CanAutoFollow() && + !IsBusy() && + !IsFollowing() && + !IsBlind() && + !GetGameState()->IsAtPlantedBombsite()) + { + + // chance of following is proportional to teamwork attribute + if (GetProfile()->GetTeamwork() > RandomFloat( 0.0f, 1.0f )) + { + CCSPlayer *leader = GetClosestVisibleHumanFriend(); + if (leader && leader->IsAutoFollowAllowed()) + { + // count how many bots are already following this player + const float maxFollowCount = 2; + if (GetBotFollowCount( leader ) < maxFollowCount) + { + const float autoFollowRange = 300.0f; + Vector leaderOrigin = GetCentroid( leader ); + if ((leaderOrigin - myOrigin).IsLengthLessThan( autoFollowRange )) + { + CNavArea *leaderArea = TheNavMesh->GetNavArea( leaderOrigin ); + if (leaderArea) + { + PathCost cost( this, FASTEST_ROUTE ); + float travelRange = NavAreaTravelDistance( GetLastKnownArea(), leaderArea, cost ); + if (travelRange >= 0.0f && travelRange < autoFollowRange) + { + // follow this human + Follow( leader ); + PrintIfWatched( "Auto-Following %s\n", leader->GetPlayerName() ); + + if (CSGameRules()->IsCareer()) + { + GetChatter()->Say( "FollowingCommander", 10.0f ); + } + else + { + GetChatter()->Say( "FollowingSir", 10.0f ); + } + } + } + } + } + } + } + else + { + // we decided not to follow, don't re-check for a duration + m_allowAutoFollowTime = gpGlobals->curtime + 15.0f + (1.0f - GetProfile()->GetTeamwork()) * 30.0f; + } + } + + if (IsFollowing()) + { + // if we are following someone, make sure they are still alive + CBaseEntity *leader = m_leader; + if (leader == NULL || !leader->IsAlive()) + { + StopFollowing(); + } + + // decide whether to continue following them + const float highTeamwork = 0.85f; + if (GetProfile()->GetTeamwork() < highTeamwork) + { + float minFollowDuration = 15.0f; + if (GetFollowDuration() > minFollowDuration + 40.0f * GetProfile()->GetTeamwork()) + { + // we are bored of following our leader + StopFollowing(); + PrintIfWatched( "Stopping following - bored\n" ); + } + } + } + + + // + // Execute state machine + // + if (m_isOpeningDoor) + { + // opening doors takes precedence over attacking because DoorCheck() will stop opening doors if + // we don't have a knife out. + m_openDoorState.OnUpdate( this ); + + if (m_openDoorState.IsDone()) + { + // open door behavior is finished, return to previous behavior context + m_openDoorState.OnExit( this ); + m_isOpeningDoor = false; + } + } + else if (m_isAttacking) + { + m_attackState.OnUpdate( this ); + } + else + { + m_state->OnUpdate( this ); + } + + // do wait behavior + if (!IsAttacking() && IsWaiting()) + { + ResetStuckMonitor(); + ClearMovement(); + } + + // don't move while reloading unless we see an enemy + if (IsReloading() && !m_isEnemyVisible) + { + ResetStuckMonitor(); + ClearMovement(); + } + + // if we get too far ahead of the hostages we are escorting, wait for them + if (!IsAttacking() && m_inhibitWaitingForHostageTimer.IsElapsed()) + { + const float waitForHostageRange = 500.0f; + if ((GetTask() == COLLECT_HOSTAGES || GetTask() == RESCUE_HOSTAGES) && GetRangeToFarthestEscortedHostage() > waitForHostageRange) + { + if (!m_isWaitingForHostage) + { + // just started waiting + m_isWaitingForHostage = true; + m_waitForHostageTimer.Start( 10.0f ); + } + else + { + // we've been waiting + if (m_waitForHostageTimer.IsElapsed()) + { + // give up waiting for awhile + m_isWaitingForHostage = false; + m_inhibitWaitingForHostageTimer.Start( 3.0f ); + } + else + { + // keep waiting + ResetStuckMonitor(); + ClearMovement(); + } + } + } + } + + // remember our prior safe time status + m_wasSafe = IsSafe(); +} + + +//-------------------------------------------------------------------------------------------------------------- +class DrawTravelTime +{ +public: + DrawTravelTime( const CCSBot *me ) + { + m_me = me; + } + + bool operator() ( CBasePlayer *player ) + { + if (player->IsAlive() && !m_me->InSameTeam( player )) + { + CFmtStr msg; + player->EntityText( 0, + msg.sprintf( "%3.0f", m_me->GetTravelDistanceToPlayer( (CCSPlayer *)player ) ), + 0.1f ); + + + if (m_me->DidPlayerJustFireWeapon( ToCSPlayer( player ) )) + { + player->EntityText( 1, "BANG!", 0.1f ); + } + } + + return true; + } + + const CCSBot *m_me; +}; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Render bot debug info + */ +void CCSBot::DebugDisplay( void ) const +{ + const float duration = 0.15f; + CFmtStr msg; + + NDebugOverlay::ScreenText( 0.5f, 0.34f, msg.sprintf( "Skill: %d%%", (int)(100.0f * GetProfile()->GetSkill()) ), 255, 255, 255, 150, duration ); + + if ( m_pathLadder ) + { + NDebugOverlay::ScreenText( 0.5f, 0.36f, msg.sprintf( "Ladder: %d", m_pathLadder->GetID() ), 255, 255, 255, 150, duration ); + } + + // show safe time + float safeTime = GetSafeTimeRemaining(); + if (safeTime > 0.0f) + { + NDebugOverlay::ScreenText( 0.5f, 0.38f, msg.sprintf( "SafeTime: %3.2f", safeTime ), 255, 255, 255, 150, duration ); + } + + // show if blind + if (IsBlind()) + { + NDebugOverlay::ScreenText( 0.5f, 0.38f, msg.sprintf( "<<< BLIND >>>" ), 255, 255, 255, 255, duration ); + } + + // show if alert + if (IsAlert()) + { + NDebugOverlay::ScreenText( 0.5f, 0.38f, msg.sprintf( "ALERT" ), 255, 0, 0, 255, duration ); + } + + // show if panicked + if (IsPanicking()) + { + NDebugOverlay::ScreenText( 0.5f, 0.36f, msg.sprintf( "PANIC" ), 255, 255, 0, 255, duration ); + } + + // show behavior variables + if (m_isAttacking) + { + NDebugOverlay::ScreenText( 0.5f, 0.4f, msg.sprintf( "ATTACKING: %s", GetBotEnemy()->GetPlayerName() ), 255, 0, 0, 255, duration ); + } + else + { + NDebugOverlay::ScreenText( 0.5f, 0.4f, msg.sprintf( "State: %s", m_state->GetName() ), 255, 255, 0, 255, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.42f, msg.sprintf( "Task: %s", GetTaskName() ), 0, 255, 0, 255, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.44f, msg.sprintf( "Disposition: %s", GetDispositionName() ), 100, 100, 255, 255, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.46f, msg.sprintf( "Morale: %s", GetMoraleName() ), 0, 200, 200, 255, duration ); + } + + // show look at status + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT) + { + const char *string = msg.sprintf( "LookAt: %s (%s)", m_lookAtDesc, (m_lookAtSpotState == LOOK_TOWARDS_SPOT) ? "LOOK_TOWARDS_SPOT" : "LOOK_AT_SPOT" ); + + NDebugOverlay::ScreenText( 0.5f, 0.60f, string, 255, 255, 0, 150, duration ); + } + + NDebugOverlay::ScreenText( 0.5f, 0.62f, msg.sprintf( "Steady view = %s", HasViewBeenSteady( 0.2f ) ? "YES" : "NO" ), 255, 255, 0, 150, duration ); + + + // show friend/foes I know of + NDebugOverlay::ScreenText( 0.5f, 0.64f, msg.sprintf( "Nearby friends = %d", m_nearbyFriendCount ), 100, 255, 100, 150, duration ); + NDebugOverlay::ScreenText( 0.5f, 0.66f, msg.sprintf( "Nearby enemies = %d", m_nearbyEnemyCount ), 255, 100, 100, 150, duration ); + + if ( m_lastNavArea ) + { + NDebugOverlay::ScreenText( 0.5f, 0.68f, msg.sprintf( "Nav Area: %d (%s)", m_lastNavArea->GetID(), m_szLastPlaceName.Get() ), 255, 255, 255, 150, duration ); + /* + if ( cv_bot_traceview.GetBool() ) + { + if ( m_currentArea ) + { + NDebugOverlay::Line( GetAbsOrigin(), m_currentArea->GetCenter(), 0, 255, 0, true, 0.1f ); + } + else if ( m_lastKnownArea ) + { + NDebugOverlay::Line( GetAbsOrigin(), m_lastKnownArea->GetCenter(), 255, 0, 0, true, 0.1f ); + } + else if ( m_lastNavArea ) + { + NDebugOverlay::Line( GetAbsOrigin(), m_lastNavArea->GetCenter(), 0, 0, 255, true, 0.1f ); + } + } + */ + } + + // show debug message history + float y = 0.8f; + const float lineHeight = 0.02f; + const float fadeAge = 7.0f; + const float maxAge = 10.0f; + for( int i=0; i<TheBots->GetDebugMessageCount(); ++i ) + { + const CBotManager::DebugMessage *msg = TheBots->GetDebugMessage( i ); + + if (msg->m_age.GetElapsedTime() < maxAge) + { + int alpha = 255; + + if (msg->m_age.GetElapsedTime() > fadeAge) + { + alpha *= (1.0f - (msg->m_age.GetElapsedTime() - fadeAge) / (maxAge - fadeAge)); + } + + NDebugOverlay::ScreenText( 0.5f, y, msg->m_string, 255, 255, 255, alpha, duration ); + y += lineHeight; + } + } + + // show noises + const Vector *noisePos = GetNoisePosition(); + if (noisePos) + { + const float size = 25.0f; + NDebugOverlay::VertArrow( *noisePos + Vector( 0, 0, size ), *noisePos, size/4.0f, 255, 255, 0, 0, true, duration ); + } + + // show aim spot + if (IsAimingAtEnemy()) + { + NDebugOverlay::Cross3D( m_aimSpot, 5.0f, 255, 0, 0, true, duration ); + } + + + + if (IsHiding()) + { + // show approach points + DrawApproachPoints(); + } + else + { + // show encounter spot data + if (false && m_spotEncounter) + { + NDebugOverlay::Line( m_spotEncounter->path.from, m_spotEncounter->path.to, 0, 150, 150, true, 0.1f ); + + Vector dir = m_spotEncounter->path.to - m_spotEncounter->path.from; + float length = dir.NormalizeInPlace(); + + const SpotOrder *order; + Vector along; + + FOR_EACH_VEC( m_spotEncounter->spots, it ) + { + order = &m_spotEncounter->spots[ it ]; + + // ignore spots the enemy could not have possibly reached yet + if (order->spot->GetArea()) + { + if (TheCSBots()->GetElapsedRoundTime() < order->spot->GetArea()->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) + { + continue; + } + } + + along = m_spotEncounter->path.from + order->t * length * dir; + + NDebugOverlay::Line( along, order->spot->GetPosition(), 0, 255, 255, true, 0.1f ); + } + } + } + + // show aim targets + if (false) + { + CStudioHdr *pStudioHdr = const_cast< CCSBot *>( this )->GetModelPtr(); + if ( !pStudioHdr ) + return; + + mstudiohitboxset_t *set = pStudioHdr->pHitboxSet( const_cast< CCSBot *>( this )->GetHitboxSet() ); + if ( !set ) + return; + + Vector position, forward, right, up; + QAngle angles; + char buffer[16]; + + for ( int i = 0; i < set->numhitboxes; i++ ) + { + mstudiobbox_t *pbox = set->pHitbox( i ); + + const_cast< CCSBot *>( this )->GetBonePosition( pbox->bone, position, angles ); + + AngleVectors( angles, &forward, &right, &up ); + + NDebugOverlay::BoxAngles( position, pbox->bbmin, pbox->bbmax, angles, 255, 0, 0, 0, 0.1f ); + + const float size = 5.0f; + NDebugOverlay::Line( position, position + size * forward, 255, 255, 0, true, 0.1f ); + NDebugOverlay::Line( position, position + size * right, 255, 0, 0, true, 0.1f ); + NDebugOverlay::Line( position, position + size * up, 0, 255, 0, true, 0.1f ); + + Q_snprintf( buffer, 16, "%d", i ); + if (i == 12) + { + // in local bone space + const float headForwardOffset = 4.0f; + const float headRightOffset = 2.0f; + position += headForwardOffset * forward + headRightOffset * right; + } + NDebugOverlay::Text( position, buffer, true, 0.1f ); + } + } + + + /* + const QAngle &angles = const_cast< CCSBot *>( this )->GetPunchAngle(); + NDebugOverlay::ScreenText( 0.3f, 0.66f, msg.sprintf( "Punch angle pitch = %3.2f", angles.x ), 255, 255, 0, 150, duration ); + */ + + DrawTravelTime drawTravelTime( this ); + ForEachPlayer( drawTravelTime ); + +/* + // show line of fire + if ((cv_bot_traceview.GetInt() == 100 && IsLocalPlayerWatchingMe()) || cv_bot_traceview.GetInt() == 101) + { + if (!IsFriendInLineOfFire()) + { + Vector vecAiming = GetViewVector(); + Vector vecSrc = EyePositionConst(); + + if (GetTeamNumber() == TEAM_TERRORIST) + UTIL_DrawBeamPoints( vecSrc, vecSrc + 2000.0f * vecAiming, 1, 255, 0, 0 ); + else + UTIL_DrawBeamPoints( vecSrc, vecSrc + 2000.0f * vecAiming, 1, 0, 50, 255 ); + } + } + + // show path navigation data + if (cv_bot_traceview.GetInt() == 1 && IsLocalPlayerWatchingMe()) + { + Vector from = EyePositionConst(); + + const float size = 50.0f; + //Vector arrow( size * cos( m_forwardAngle * M_PI/180.0f ), size * sin( m_forwardAngle * M_PI/180.0f ), 0.0f ); + Vector arrow( size * (float)cos( m_lookAheadAngle * M_PI/180.0f ), + size * (float)sin( m_lookAheadAngle * M_PI/180.0f ), + 0.0f ); + + UTIL_DrawBeamPoints( from, from + arrow, 1, 0, 255, 255 ); + } + + if (cv_bot_show_nav.GetBool() && m_lastKnownArea) + { + m_lastKnownArea->DrawConnectedAreas(); + } + */ + + + if (IsAttacking()) + { + const float crossSize = 2.0f; + CCSPlayer *enemy = GetBotEnemy(); + if (enemy) + { + NDebugOverlay::Cross3D( GetPartPosition( enemy, GUT ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, HEAD ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, FEET ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, LEFT_SIDE ), crossSize, 0, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetPartPosition( enemy, RIGHT_SIDE ), crossSize, 0, 255, 0, true, 0.1f ); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Periodically compute shortest path distance to each player. + * NOTE: Travel distance is NOT symmetric between players A and B. Each much be computed separately. + */ +void CCSBot::UpdateTravelDistanceToAllPlayers( void ) +{ + const unsigned char numPhases = 3; + + if (m_updateTravelDistanceTimer.IsElapsed()) + { + ShortestPathCost pathCost; + + for( int i=1; i<=gpGlobals->maxClients; ++i ) + { + CCSPlayer *player = static_cast< CCSPlayer * >( UTIL_PlayerByIndex( i ) ); + + if (player == NULL) + continue; + + if (FNullEnt( player->edict() )) + continue; + + if (!player->IsPlayer()) + continue; + + if (!player->IsAlive()) + continue; + + // skip friends for efficiency + if (player->InSameTeam( this )) + continue; + + int which = player->entindex() % MAX_PLAYERS; + + // if player is very far away, update every third time (on phase 0) + const float veryFarAway = 4000.0f; + if (m_playerTravelDistance[ which ] < 0.0f || m_playerTravelDistance[ which ] > veryFarAway) + { + if (m_travelDistancePhase != 0) + continue; + } + else + { + // if player is far away, update two out of three times (on phases 1 and 2) + const float farAway = 2000.0f; + if (m_playerTravelDistance[ which ] > farAway && m_travelDistancePhase == 0) + continue; + } + + // if player is fairly close, update often + m_playerTravelDistance[ which ] = NavAreaTravelDistance( EyePosition(), player->EyePosition(), pathCost ); + } + + // throttle the computation frequency + const float checkInterval = 1.0f; + m_updateTravelDistanceTimer.Start( checkInterval ); + + // round-robin the phases + ++m_travelDistancePhase; + if (m_travelDistancePhase >= numPhases) + { + m_travelDistancePhase = 0; + } + } +} diff --git a/game/server/cstrike/bot/cs_bot_vision.cpp b/game/server/cstrike/bot/cs_bot_vision.cpp new file mode 100644 index 0000000..a7e8500 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_vision.cpp @@ -0,0 +1,1834 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "datacache/imdlcache.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Used to update view angles to stay on a ladder + */ +inline float StayOnLadderLine( CCSBot *me, const CNavLadder *ladder ) +{ + // determine our facing + NavDirType faceDir = AngleToDirection( me->EyeAngles().y ); + + const float stiffness = 1.0f; + + // move toward ladder mount point + switch( faceDir ) + { + case NORTH: + return stiffness * (ladder->m_top.x - me->GetAbsOrigin().x); + + case SOUTH: + return -stiffness * (ladder->m_top.x - me->GetAbsOrigin().x); + + case WEST: + return -stiffness * (ladder->m_top.y - me->GetAbsOrigin().y); + + case EAST: + return stiffness * (ladder->m_top.y - me->GetAbsOrigin().y); + } + + return 0.0f; +} + +//-------------------------------------------------------------------------------------------------------------- +void CCSBot::ComputeLadderAngles( float *yaw, float *pitch ) +{ + if ( !yaw || !pitch ) + return; + + Vector myOrigin = GetCentroid( this ); + + // set yaw to aim at ladder + Vector to = m_pathLadder->GetPosAtHeight(myOrigin.z) - myOrigin; + float idealYaw = UTIL_VecToYaw( to ); + + Vector faceDir = (m_pathLadderFaceIn) ? -m_pathLadder->GetNormal() : m_pathLadder->GetNormal(); + QAngle faceAngles; + VectorAngles( faceDir, faceAngles ); + + const float lookAlongLadderRange = 50.0f; + const float ladderPitchUpApproach = -30.0f; + const float ladderPitchUpTraverse = -60.0f; // -80 + const float ladderPitchDownApproach = 0.0f; + const float ladderPitchDownTraverse = 80.0f; + + // adjust pitch to look up/down ladder as we ascend/descend + switch( m_pathLadderState ) + { + case APPROACH_ASCENDING_LADDER: + { + Vector to = m_goalPosition - myOrigin; + *yaw = idealYaw; + + if (to.IsLengthLessThan( lookAlongLadderRange )) + *pitch = ladderPitchUpApproach; + break; + } + + case APPROACH_DESCENDING_LADDER: + { + Vector to = m_goalPosition - myOrigin; + *yaw = idealYaw; + + if (to.IsLengthLessThan( lookAlongLadderRange )) + *pitch = ladderPitchDownApproach; + break; + } + + case FACE_ASCENDING_LADDER: + if ( m_pathLadderDismountDir == LEFT ) + { + *yaw = AngleNormalizePositive( idealYaw + 90.0f ); + } + else if ( m_pathLadderDismountDir == RIGHT ) + { + *yaw = AngleNormalizePositive( idealYaw - 90.0f ); + } + else + { + *yaw = idealYaw; + } + *pitch = ladderPitchUpApproach; + break; + + case FACE_DESCENDING_LADDER: + *yaw = idealYaw; + *pitch = ladderPitchDownApproach; + break; + + case MOUNT_ASCENDING_LADDER: + case ASCEND_LADDER: + if ( m_pathLadderDismountDir == LEFT ) + { + *yaw = AngleNormalizePositive( idealYaw + 90.0f ); + } + else if ( m_pathLadderDismountDir == RIGHT ) + { + *yaw = AngleNormalizePositive( idealYaw - 90.0f ); + } + else + { + *yaw = faceAngles[ YAW ] + StayOnLadderLine( this, m_pathLadder ); + } + *pitch = ( m_pathLadderState == ASCEND_LADDER ) ? ladderPitchUpTraverse : ladderPitchUpApproach; + break; + + case MOUNT_DESCENDING_LADDER: + case DESCEND_LADDER: + *yaw = faceAngles[ YAW ] + StayOnLadderLine( this, m_pathLadder ); + *pitch = ( m_pathLadderState == DESCEND_LADDER ) ? ladderPitchDownTraverse : ladderPitchDownApproach; + break; + + case DISMOUNT_ASCENDING_LADDER: + if ( m_pathLadderDismountDir == LEFT ) + { + *yaw = AngleNormalizePositive( faceAngles[ YAW ] + 90.0f ); + } + else if ( m_pathLadderDismountDir == RIGHT ) + { + *yaw = AngleNormalizePositive( faceAngles[ YAW ] - 90.0f ); + } + else + { + *yaw = faceAngles[ YAW ]; + } + break; + + case DISMOUNT_DESCENDING_LADDER: + *yaw = faceAngles[ YAW ]; + break; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move actual view angles towards desired ones. + * This is the only place v_angle is altered. + * @todo Make stiffness and turn rate constants timestep invariant. + */ +void CCSBot::UpdateLookAngles( void ) +{ + VPROF_BUDGET( "CCSBot::UpdateLookAngles", VPROF_BUDGETGROUP_NPCS ); + + const float deltaT = g_BotUpkeepInterval; + float maxAccel; + float stiffness; + float damping; + + // If mimicing the player, don't modify the view angles. + if ( bot_mimic.GetInt() ) + return; + + // springs are stiffer when attacking, so we can track and move between targets better + if (IsAttacking()) + { + stiffness = 300.0f; + damping = 30.0f; // 20 + maxAccel = 3000.0f; // 4000 + } + else + { + stiffness = 200.0f; + damping = 25.0f; + maxAccel = 3000.0f; + } + + // these may be overridden by ladder logic + float useYaw = m_lookYaw; + float usePitch = m_lookPitch; + + // + // Ladders require precise movement, therefore we need to look at the + // ladder as we approach and ascend/descend it. + // If we are on a ladder, we need to look up or down to traverse it - override pitch in this case. + // + // If we're trying to break something, though, we actually need to look at it before we can + // look at the ladder + // + if ( IsUsingLadder() && !(IsLookingAtSpot( PRIORITY_HIGH ) && m_lookAtSpotAttack) ) + { + ComputeLadderAngles( &useYaw, &usePitch ); + } + + // get current view angles + QAngle viewAngles = EyeAngles(); + + // + // Yaw + // + float angleDiff = AngleNormalize( useYaw - viewAngles.y ); + + /* + * m_forwardAngle is unreliable. Need to simulate mouse sliding & centering + if (!IsAttacking()) + { + // do not allow rotation through our reverse facing angle - go the "long" way instead + float toCurrent = AngleNormalize( pev->v_angle.y - m_forwardAngle ); + float toDesired = AngleNormalize( useYaw - m_forwardAngle ); + + // if angle differences are different signs, they cross the forward facing + if (toCurrent * toDesired < 0.0f) + { + // if the sum of the angles is greater than 180, turn the "long" way around + if (abs( toCurrent - toDesired ) >= 180.0f) + { + if (angleDiff > 0.0f) + angleDiff -= 360.0f; + else + angleDiff += 360.0f; + } + } + } + */ + + // if almost at target angle, snap to it + const float onTargetTolerance = 1.0f; // 3 + if (angleDiff < onTargetTolerance && angleDiff > -onTargetTolerance) + { + m_lookYawVel = 0.0f; + viewAngles.y = useYaw; + } + else + { + // simple angular spring/damper + float accel = stiffness * angleDiff - damping * m_lookYawVel; + + // limit rate + if (accel > maxAccel) + accel = maxAccel; + else if (accel < -maxAccel) + accel = -maxAccel; + + m_lookYawVel += deltaT * accel; + viewAngles.y += deltaT * m_lookYawVel; + + // keep track of how long our view remains steady + const float steadyYaw = 1000.0f; + if (fabs( accel ) > steadyYaw) + { + m_viewSteadyTimer.Start(); + } + } + + // + // Pitch + // Actually, this is negative pitch. + // + angleDiff = usePitch - viewAngles.x; + + angleDiff = AngleNormalize( angleDiff ); + + + if (false && angleDiff < onTargetTolerance && angleDiff > -onTargetTolerance) + { + m_lookPitchVel = 0.0f; + viewAngles.x = usePitch; + } + else + { + // simple angular spring/damper + // double the stiffness since pitch is only +/- 90 and yaw is +/- 180 + float accel = 2.0f * stiffness * angleDiff - damping * m_lookPitchVel; + + // limit rate + if (accel > maxAccel) + accel = maxAccel; + else if (accel < -maxAccel) + accel = -maxAccel; + + m_lookPitchVel += deltaT * accel; + viewAngles.x += deltaT * m_lookPitchVel; + + // keep track of how long our view remains steady + const float steadyPitch = 1000.0f; + if (fabs( accel ) > steadyPitch) + { + m_viewSteadyTimer.Start(); + } + } + + //PrintIfWatched( "yawVel = %g, pitchVel = %g\n", m_lookYawVel, m_lookPitchVel ); + + // limit range - avoid gimbal lock + if (viewAngles.x < -89.0f) + viewAngles.x = -89.0f; + else if (viewAngles.x > 89.0f) + viewAngles.x = 89.0f; + + // update view angles + SnapEyeAngles( viewAngles ); + + // if our weapon is zooming, our view is not steady + if (IsWaitingForZoom()) + { + m_viewSteadyTimer.Start(); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we can see the point + */ +bool CCSBot::IsVisible( const Vector &pos, bool testFOV, const CBaseEntity *ignore ) const +{ + VPROF_BUDGET( "CCSBot::IsVisible( pos )", VPROF_BUDGETGROUP_NPCS ); + + // we can't see anything if we're blind + if (IsBlind()) + return false; + + // is it in my general viewcone? + if (testFOV && !(const_cast<CCSBot *>(this)->FInViewCone( pos ))) + return false; + + // check line of sight against smoke + if (TheCSBots()->IsLineBlockedBySmoke( EyePositionConst(), pos )) + return false; + + // check line of sight + // Must include CONTENTS_MONSTER to pick up all non-brush objects like barrels + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( ignore, COLLISION_GROUP_NONE ); + UTIL_TraceLine( EyePositionConst(), pos, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + if (result.fraction != 1.0f) + return false; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we can see any part of the player + * Check parts in order of importance. Return the first part seen in "visPart" if it is non-NULL. + */ +bool CCSBot::IsVisible( CCSPlayer *player, bool testFOV, unsigned char *visParts ) const +{ + VPROF_BUDGET( "CCSBot::IsVisible( player )", VPROF_BUDGETGROUP_NPCS ); + + // optimization - assume if center is not in FOV, nothing is + // we're using WorldSpaceCenter instead of GUT so we can skip GetPartPosition below - that's + // the most expensive part of this, and if we can skip it, so much the better. + if (testFOV && !(const_cast<CCSBot *>(this)->FInViewCone( player->WorldSpaceCenter() ))) + { + return false; + } + + unsigned char testVisParts = NONE; + + // check gut + Vector partPos = GetPartPosition( player, GUT ); + + // finish gut check + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= GUT; + } + + + // check top of head + partPos = GetPartPosition( player, HEAD ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= HEAD; + } + + // check feet + partPos = GetPartPosition( player, FEET ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= FEET; + } + + // check "edges" + partPos = GetPartPosition( player, LEFT_SIDE ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= LEFT_SIDE; + } + + partPos = GetPartPosition( player, RIGHT_SIDE ); + if (IsVisible( partPos, testFOV )) + { + if (visParts == NULL) + return true; + + testVisParts |= RIGHT_SIDE; + } + + if (visParts) + *visParts = testVisParts; + + if (testVisParts) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Interesting part positions + */ +CCSBot::PartInfo CCSBot::m_partInfo[ MAX_PLAYERS ]; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Compute part positions from bone location. + */ +void CCSBot::ComputePartPositions( CCSPlayer *player ) +{ + const int headBox = 12; + const int gutBox = 9; + const int leftElbowBox = 14; + const int rightElbowBox = 17; + //const int hipBox = 0; + //const int leftFootBox = 4; + //const int rightFootBox = 8; + const int maxBoxIndex = rightElbowBox; + + VPROF_BUDGET( "CCSBot::ComputePartPositions", VPROF_BUDGETGROUP_NPCS ); + + // which PartInfo corresponds to the given player + PartInfo *info = &m_partInfo[ player->entindex() % MAX_PLAYERS ]; + + // always compute feet, since it doesn't rely on bones + info->m_feetPos = player->GetAbsOrigin(); + info->m_feetPos.z += 5.0f; + + // get bone positions for interesting points on the player + MDLCACHE_CRITICAL_SECTION(); + CStudioHdr *studioHdr = player->GetModelPtr(); + if (studioHdr) + { + mstudiohitboxset_t *set = studioHdr->pHitboxSet( player->GetHitboxSet() ); + if (set && maxBoxIndex < set->numhitboxes) + { + QAngle angles; + mstudiobbox_t *box; + + // gut + box = set->pHitbox( gutBox ); + player->GetBonePosition( box->bone, info->m_gutPos, angles ); + + // head + box = set->pHitbox( headBox ); + player->GetBonePosition( box->bone, info->m_headPos, angles ); + + Vector forward, right; + AngleVectors( angles, &forward, &right, NULL ); + + // in local bone space + const float headForwardOffset = 4.0f; + const float headRightOffset = 2.0f; + info->m_headPos += headForwardOffset * forward + headRightOffset * right; + + /// @todo Fix this hack - lower the head target because it's a bit too high for the current T model + info->m_headPos.z -= 2.0f; + + + // left side + box = set->pHitbox( leftElbowBox ); + player->GetBonePosition( box->bone, info->m_leftSidePos, angles ); + + // right side + box = set->pHitbox( rightElbowBox ); + player->GetBonePosition( box->bone, info->m_rightSidePos, angles ); + + return; + } + } + + + // default values if bones are not available + info->m_headPos = GetCentroid( player ); + info->m_gutPos = info->m_headPos; + info->m_leftSidePos = info->m_headPos; + info->m_rightSidePos = info->m_headPos; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return world space position of given part on player. + * Uses hitboxes to get accurate positions. + * @todo Optimize by computing once for each player and storing. + */ +const Vector &CCSBot::GetPartPosition( CCSPlayer *player, VisiblePartType part ) const +{ + VPROF_BUDGET( "CCSBot::GetPartPosition", VPROF_BUDGETGROUP_NPCS ); + + // which PartInfo corresponds to the given player + PartInfo *info = &m_partInfo[ player->entindex() % MAX_PLAYERS ]; + + if (gpGlobals->framecount > info->m_validFrame) + { + // update part positions + const_cast< CCSBot * >( this )->ComputePartPositions( player ); + info->m_validFrame = gpGlobals->framecount; + } + + // return requested part position + switch( part ) + { + default: + { + AssertMsg( false, "GetPartPosition: Invalid part" ); + // fall thru to GUT + } + + case GUT: + return info->m_gutPos; + + case HEAD: + return info->m_headPos; + + case FEET: + return info->m_feetPos; + + case LEFT_SIDE: + return info->m_leftSidePos; + + case RIGHT_SIDE: + return info->m_rightSidePos; + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update desired view angles to point towards m_lookAtSpot + */ +void CCSBot::UpdateLookAt( void ) +{ + Vector to = m_lookAtSpot - EyePositionConst(); + + QAngle idealAngle; + VectorAngles( to, idealAngle ); + + //Vector idealAngle = UTIL_VecToAngles( to ); + //idealAngle.x = 360.0f - idealAngle.x; + + SetLookAngles( idealAngle.y, idealAngle.x ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Look at the given point in space for the given duration (-1 means forever) + */ +void CCSBot::SetLookAt( const char *desc, const Vector &pos, PriorityType pri, float duration, bool clearIfClose, float angleTolerance, bool attack ) +{ + if (IsBlind()) + return; + + // if currently looking at a point in space with higher priority, ignore this request + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && m_lookAtSpotPriority > pri) + return; + + // if already looking at this spot, just extend the time + const float tolerance = 10.0f; + if (m_lookAtSpotState != NOT_LOOKING_AT_SPOT && VectorsAreEqual( pos, m_lookAtSpot, tolerance )) + { + m_lookAtSpotDuration = duration; + + if (m_lookAtSpotPriority < pri) + m_lookAtSpotPriority = pri; + } + else + { + // look at new spot + m_lookAtSpot = pos; + m_lookAtSpotState = LOOK_TOWARDS_SPOT; + m_lookAtSpotDuration = duration; + m_lookAtSpotPriority = pri; + } + + m_lookAtSpotAngleTolerance = angleTolerance; + m_lookAtSpotClearIfClose = clearIfClose; + m_lookAtDesc = desc; + m_lookAtSpotAttack = attack; + + PrintIfWatched( "%3.1f SetLookAt( %s ), duration = %f\n", gpGlobals->curtime, desc, duration ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Block all "look at" and "look around" behavior for given duration - just look ahead + */ +void CCSBot::InhibitLookAround( float duration ) +{ + m_inhibitLookAroundTimestamp = gpGlobals->curtime + duration; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update enounter spot timestamps, etc + */ +void CCSBot::UpdatePeripheralVision() +{ + VPROF_BUDGET( "CCSBot::UpdatePeripheralVision", VPROF_BUDGETGROUP_NPCS ); + + const float peripheralUpdateInterval = 0.29f; // if we update at 10Hz, this ensures we test once every three + if (gpGlobals->curtime - m_peripheralTimestamp < peripheralUpdateInterval) + return; + + m_peripheralTimestamp = gpGlobals->curtime; + + if (m_spotEncounter) + { + // check LOS to all spots in case we see them with our "peripheral vision" + const SpotOrder *spotOrder; + Vector pos; + + FOR_EACH_VEC( m_spotEncounter->spots, it ) + { + spotOrder = &m_spotEncounter->spots[ it ]; + + const Vector &spotPos = spotOrder->spot->GetPosition(); + + pos.x = spotPos.x; + pos.y = spotPos.y; + pos.z = spotPos.z + HalfHumanHeight; + + if (!IsVisible( pos, CHECK_FOV )) + continue; + + // can see hiding spot, remember when we saw it last + SetHidingSpotCheckTimestamp( spotOrder->spot ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update the "looking around" behavior. + */ +void CCSBot::UpdateLookAround( bool updateNow ) +{ + VPROF_BUDGET( "CCSBot::UpdateLookAround", VPROF_BUDGETGROUP_NPCS ); + + // + // If we recently saw an enemy, look towards where we last saw them + // Unless we can hear them moving, in which case look towards the noise + // + const float closeRange = 500.0f; + if (!IsNoiseHeard() || GetNoiseRange() > closeRange) + { + const float recentThreatTime = 1.0f; // 0.25f; + if (!IsLookingAtSpot( PRIORITY_MEDIUM ) && gpGlobals->curtime - m_lastSawEnemyTimestamp < recentThreatTime) + { + ClearLookAt(); + + Vector spot = m_lastEnemyPosition; + + // find enemy position on the ground + if (TheNavMesh->GetSimpleGroundHeight( m_lastEnemyPosition, &spot.z )) + { + spot.z += HalfHumanHeight; + SetLookAt( "Last Enemy Position", spot, PRIORITY_MEDIUM, RandomFloat( 2.0f, 3.0f ), true ); + return; + } + } + } + + // + // Look at nearby enemy noises + // + if (UpdateLookAtNoise()) + return; + + + // check if looking around has been inhibited + // Moved inhibit to allow high priority enemy lookats to still occur + if (gpGlobals->curtime < m_inhibitLookAroundTimestamp) + return; + + // + // If we are hiding (or otherwise standing still), watch all approach points leading into this region + // + const float minStillTime = 2.0f; + if (IsAtHidingSpot() || IsNotMoving( minStillTime )) + { + // update approach points + const float recomputeApproachPointTolerance = 50.0f; + if ((m_approachPointViewPosition - GetAbsOrigin()).IsLengthGreaterThan( recomputeApproachPointTolerance )) + { + ComputeApproachPoints(); + m_approachPointViewPosition = GetAbsOrigin(); + } + + // if we're sniping, zoom in to watch our approach points + if (IsUsingSniperRifle()) + { + // low skill bots don't pre-zoom + if (GetProfile()->GetSkill() > 0.4f) + { + if (!IsViewMoving()) + { + float range = ComputeWeaponSightRange(); + AdjustZoom( range ); + } + else + { + // zoom out + if (GetZoomLevel() != NO_ZOOM) + SecondaryAttack(); + } + } + } + + if (m_lastKnownArea == NULL) + return; + + if (gpGlobals->curtime < m_lookAroundStateTimestamp) + return; + + // if we're sniping, switch look-at spots less often + if (IsUsingSniperRifle()) + m_lookAroundStateTimestamp = gpGlobals->curtime + RandomFloat( 5.0f, 10.0f ); + else + m_lookAroundStateTimestamp = gpGlobals->curtime + RandomFloat( 1.0f, 2.0f ); // 0.5, 1.0 + + + #define MAX_APPROACHES 16 + Vector validSpot[ MAX_APPROACHES ]; + int validSpotCount = 0; + + Vector *earlySpot = NULL; + float earliest = 999999.9f; + + for( int i=0; i<m_approachPointCount; ++i ) + { + float spotTime = m_approachPoint[i].m_area->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) ); + + // ignore approach areas the enemy could not have possibly reached yet + if (TheCSBots()->GetElapsedRoundTime() >= spotTime) + { + validSpot[ validSpotCount++ ] = m_approachPoint[i].m_pos; + } + else + { + // keep track of earliest spot we can see in case we get there very early + if (spotTime < earliest) + { + earlySpot = &m_approachPoint[i].m_pos; + earliest = spotTime; + } + } + } + + Vector spot; + + if (validSpotCount) + { + int which = RandomInt( 0, validSpotCount-1 ); + spot = validSpot[ which ]; + } + else if (earlySpot) + { + // all of the spots we can see can't be reached yet by the enemy - look at the earliest spot + spot = *earlySpot; + } + else + { + return; + } + + // don't look at the floor, look roughly at chest level + /// @todo If this approach point is very near, this will cause us to aim up in the air if were crouching + spot.z += HalfHumanHeight; + + SetLookAt( "Approach Point (Hiding)", spot, PRIORITY_LOW ); + + return; + } + + // + // Glance at "encouter spots" as we move past them + // + if (m_spotEncounter) + { + // + // Check encounter spots + // + if (!IsSafe() && !IsLookingAtSpot( PRIORITY_LOW )) + { + // allow a short time to look where we're going + if (gpGlobals->curtime < m_spotCheckTimestamp) + return; + + /// @todo Use skill parameter instead of accuracy + + // lower skills have exponentially longer delays + float asleep = (1.0f - GetProfile()->GetSkill()); + asleep *= asleep; + asleep *= asleep; + + m_spotCheckTimestamp = gpGlobals->curtime + asleep * RandomFloat( 10.0f, 30.0f ); + + + // figure out how far along the path segment we are + Vector delta = m_spotEncounter->path.to - m_spotEncounter->path.from; + float length = delta.Length(); + float adx = (float)fabs(delta.x); + float ady = (float)fabs(delta.y); + float t; + Vector myOrigin = GetCentroid( this ); + + if (adx > ady) + t = (myOrigin.x - m_spotEncounter->path.from.x) / delta.x; + else + t = (myOrigin.y - m_spotEncounter->path.from.y) / delta.y; + + // advance parameter a bit so we "lead" our checks + const float leadCheckRange = 50.0f; + t += leadCheckRange / length; + + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + + // collect the unchecked spots so far + #define MAX_DANGER_SPOTS 16 + HidingSpot *dangerSpot[MAX_DANGER_SPOTS]; + int dangerSpotCount = 0; + int dangerIndex = 0; + + const float checkTime = 10.0f; + const SpotOrder *spotOrder; + FOR_EACH_VEC( m_spotEncounter->spots, it ) + { + spotOrder = &(m_spotEncounter->spots[ it ]); + + // if we have seen this spot recently, we don't need to look at it + if (gpGlobals->curtime - GetHidingSpotCheckTimestamp( spotOrder->spot ) <= checkTime) + continue; + + if (spotOrder->t > t) + break; + + // ignore spots the enemy could not have possibly reached yet + if (spotOrder->spot->GetArea()) + { + if (TheCSBots()->GetElapsedRoundTime() < spotOrder->spot->GetArea()->GetEarliestOccupyTime( OtherTeam( GetTeamNumber() ) )) + { + continue; + } + } + + dangerSpot[ dangerIndex++ ] = spotOrder->spot; + if (dangerIndex >= MAX_DANGER_SPOTS) + dangerIndex = 0; + if (dangerSpotCount < MAX_DANGER_SPOTS) + ++dangerSpotCount; + } + + if (dangerSpotCount) + { + // pick one of the spots at random + int which = RandomInt( 0, dangerSpotCount-1 ); + + // glance at the spot for minimum time + SetLookAt( "Encounter Spot", dangerSpot[which]->GetPosition() + Vector( 0, 0, HalfHumanHeight ), PRIORITY_LOW, 0.2f, true, 10.0f ); + + // immediately mark it as "checked", so we don't check it again + // if we get distracted before we check it - that's the way it goes + SetHidingSpotCheckTimestamp( dangerSpot[which] ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * "Bend" our line of sight around corners until we can "see" the point. + */ +bool CCSBot::BendLineOfSight( const Vector &eye, const Vector &target, Vector *bend, float angleLimit ) const +{ + VPROF_BUDGET( "CCSBot::BendLineOfSight", VPROF_BUDGETGROUP_NPCS ); + + bool doDebug = false; + const float debugDuration = 0.04f; + if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + NDebugOverlay::Line( eye, target, 255, 255, 255, true, debugDuration ); + + // if we can directly see the point, use it + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); + UTIL_TraceLine( eye, target, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + if (result.fraction == 1.0f && !result.startsolid) + { + // can directly see point, no bending needed + *bend = target; + return true; + } + + // "bend" our line of sight until we can see the approach point + Vector to = target - eye; + float startAngle = UTIL_VecToYaw( to ); + float length = to.Length2D(); + to.NormalizeInPlace(); + + struct Color3 + { + int r, g, b; + }; + const int colorCount = 6; + Color3 colorSet[ colorCount ] = + { + { 255, 0, 0 }, + { 0, 255, 0 }, + { 0, 0, 255 }, + { 255, 255, 0 }, + { 0, 255, 255 }, + { 255, 0, 255 }, + }; + + int color = 0; + + // optiming assumption - previous rays cast "shadow" on subsequent rays since they already + // enumerated visible space along their length. + // We should do a dot product and compute the exact length, but since the angular changes + // are incremental, using the direct length should be close enough. + float priorVisibleLength[2] = { 0.0f, 0.0f }; + + float angleInc = 5.0f; + for( float angle = angleInc; angle <= angleLimit; angle += angleInc ) + { + // check both sides at this angle offset + for( int side=0; side<2; ++side ) + { + float actualAngle = (side) ? (startAngle + angle) : (startAngle - angle); + + float dx = cos( 3.141592f * actualAngle / 180.0f ); + float dy = sin( 3.141592f * actualAngle / 180.0f ); + + // compute rotated point ray endpoint + Vector rotPoint( eye.x + length * dx, eye.y + length * dy, target.z ); + + // check LOS to find length to test along ray + UTIL_TraceLine( eye, rotPoint, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + + // if this ray started in an obstacle, skip it + if (result.startsolid) + { + continue; + } + + Vector ray = rotPoint - eye; + float rayLength = ray.NormalizeInPlace(); + float visibleLength = rayLength * result.fraction; + + if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + NDebugOverlay::Line( eye, eye + visibleLength * ray, colorSet[color].r, colorSet[color].g, colorSet[color].b, true, debugDuration ); + } + + // step along ray, checking if point is visible from ray point + const float bendStepSize = 50.0f; + + // start from point that prior rays couldn't see + float startLength = priorVisibleLength[ side ]; + + for( float bendLength=startLength; bendLength <= visibleLength; bendLength += bendStepSize ) + { + // compute point along ray + Vector bendPoint = eye + bendLength * ray; + + // check if we can see approach point from this bend point + UTIL_TraceLine( bendPoint, target, MASK_VISIBLE_AND_NPCS, &traceFilter, &result ); + + if (doDebug && cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + NDebugOverlay::Line( bendPoint, result.endpos, colorSet[color].r/2, colorSet[color].g/2, colorSet[color].b/2, true, debugDuration ); + } + + if (result.fraction == 1.0f && !result.startsolid) + { + // target is visible from this bend point on the ray - use this point on the ray as our point + + // keep "bent" point at correct height along line of sight + bendPoint.z = eye.z + bendLength * to.z; + + *bend = bendPoint; + + return true; + } + } + + priorVisibleLength[ side ] = visibleLength; + + ++color; + if (color >= colorCount) + { + color = 0; + } + } // side + } + + // bending rays didn't help - still can't see the point + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we "notice" given player + * @todo Increase chance if player is rotating + * @todo Decrease chance as nears edge of FOV + */ +bool CCSBot::IsNoticable( const CCSPlayer *player, unsigned char visParts ) const +{ + // if this player has just fired his weapon, we notice him + if (DidPlayerJustFireWeapon( player )) + { + return true; + } + + float deltaT = m_attentionInterval.GetElapsedTime(); + + // all chances are specified in terms of a standard "quantum" of time + // in which a normal person would notice something + const float noticeQuantum = 0.25f; + + // determine percentage of player that is visible + float coverRatio = 0.0f; + + if (visParts & GUT) + { + const float chance = 40.0f; + coverRatio += chance; + } + + if (visParts & HEAD) + { + const float chance = 10.0f; + coverRatio += chance; + } + + if (visParts & LEFT_SIDE) + { + const float chance = 20.0f; + coverRatio += chance; + } + + if (visParts & RIGHT_SIDE) + { + const float chance = 20.0f; + coverRatio += chance; + } + + if (visParts & FEET) + { + const float chance = 10.0f; + coverRatio += chance; + } + + + // compute range modifier - farther away players are harder to notice, depeding on what they are doing + float range = (player->GetAbsOrigin() - GetAbsOrigin()).Length(); + const float closeRange = 300.0f; + const float farRange = 1000.0f; + + float rangeModifier; + if (range < closeRange) + { + rangeModifier = 0.0f; + } + else if (range > farRange) + { + rangeModifier = 1.0f; + } + else + { + rangeModifier = (range - closeRange)/(farRange - closeRange); + } + + + // harder to notice when crouched + bool isCrouching = (player->GetFlags() & FL_DUCKING); + + + // moving players are easier to spot + float playerSpeedSq = player->GetAbsVelocity().LengthSqr(); + const float runSpeed = 200.0f; + const float walkSpeed = 30.0f; + float farChance, closeChance; + if (playerSpeedSq > runSpeed * runSpeed) + { + // running players are always easy to spot (must be standing to run) + return true; + } + else if (playerSpeedSq > walkSpeed * walkSpeed) + { + // walking players are less noticable far away + if (isCrouching) + { + closeChance = 90.0f; + farChance = 60.0f; + } + else // standing + { + closeChance = 100.0f; + farChance = 75.0f; + } + } + else + { + // motionless players are hard to notice + if (isCrouching) + { + // crouching and motionless - very tough to notice + closeChance = 80.0f; + farChance = 5.0f; // takes about three seconds to notice (50% chance) + } + else // standing + { + closeChance = 100.0f; + farChance = 10.0f; + } + } + + // combine posture, speed, and range chances + float dispositionChance = closeChance + (farChance - closeChance) * rangeModifier; + + // determine actual chance of noticing player + float noticeChance = dispositionChance * coverRatio/100.0f; + + // scale by skill level + noticeChance *= (0.5f + 0.5f * GetProfile()->GetSkill()); + + // if we are alert, our chance of noticing is much higher + if (IsAlert()) + { + const float alertBonus = 50.0f; + noticeChance += alertBonus; + } + + // scale by time quantum + noticeChance *= deltaT / noticeQuantum; + + // there must always be a chance of detecting the enemy + const float minChance = 0.1f; + if (noticeChance < minChance) + { + noticeChance = minChance; + } + + //PrintIfWatched( "Notice chance = %3.2f\n", noticeChance ); + + return (RandomFloat( 0.0f, 100.0f ) < noticeChance); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return most dangerous threat in my field of view (feeds into reaction time queue). + * @todo Account for lighting levels, cover, and distance to see if we notice enemy + */ +CCSPlayer *CCSBot::FindMostDangerousThreat( void ) +{ + VPROF_BUDGET( "CCSBot::FindMostDangerousThreat", VPROF_BUDGETGROUP_NPCS ); + + if (IsBlind()) + { + return NULL; + } + + enum { MAX_THREATS = 16 }; // maximum number of simulataneously attendable threats + struct CloseInfo + { + CCSPlayer *enemy; + float range; + } + threat[ MAX_THREATS ]; + int threatCount = 0; + + int prevIndex = m_enemyQueueIndex - 1; + if ( prevIndex < 0 ) + prevIndex = MAX_ENEMY_QUEUE - 1; + CCSPlayer *currentThreat = m_enemyQueue[ prevIndex ].player; + + m_bomber = NULL; + m_isEnemySniperVisible = false; + + m_closestVisibleFriend = NULL; + float closeFriendRange = 99999999999.9f; + + m_closestVisibleHumanFriend = NULL; + float closeHumanFriendRange = 99999999999.9f; + + CCSPlayer *sniperThreat = NULL; + float sniperThreatRange = 99999999999.9f; + bool sniperThreatIsFacingMe = false; + + const float lookingAtMeTolerance = 0.7071f; + + int i; + + { + VPROF_BUDGET( "CCSBot::Collect Threats", VPROF_BUDGETGROUP_NPCS ); + + for( i = 1; i <= gpGlobals->maxClients; ++i ) + { + CBaseEntity *entity = UTIL_PlayerByIndex( i ); + + if (entity == NULL) + continue; + + // is it a player? + if (!entity->IsPlayer()) + continue; + + CCSPlayer *player = static_cast<CCSPlayer *>( entity ); + + // ignore self + if (player->entindex() == entindex()) + continue; + + // is it alive? + if (!player->IsAlive()) + continue; + + // is it an enemy? + if (player->InSameTeam( this )) + { + // keep track of nearby friends - use less exact visibility check + if (IsVisible( entity->WorldSpaceCenter(), false, this )) + { + // update watch timestamp + int idx = player->entindex(); + m_watchInfo[idx].timestamp = gpGlobals->curtime; + m_watchInfo[idx].isEnemy = false; + + // keep track of our closest friend + Vector to = GetAbsOrigin() - player->GetAbsOrigin(); + float rangeSq = to.LengthSqr(); + if (rangeSq < closeFriendRange) + { + m_closestVisibleFriend = player; + closeFriendRange = rangeSq; + } + + // keep track of our closest human friend + if (!player->IsBot() && rangeSq < closeHumanFriendRange) + { + m_closestVisibleHumanFriend = player; + closeHumanFriendRange = rangeSq; + } + } + + continue; + } + + // check if this enemy is fully or partially visible + unsigned char visParts; + if (!IsVisible( player, CHECK_FOV, &visParts )) + continue; + + // do we notice this enemy? (always notice current enemy) + if (player != currentThreat) + { + if (!IsNoticable( player, visParts )) + { + continue; + } + } + + // update watch timestamp + int idx = player->entindex(); + m_watchInfo[idx].timestamp = gpGlobals->curtime; + m_watchInfo[idx].isEnemy = true; + + // note if we see the bomber + if (player->HasC4()) + { + m_bomber = player; + } + + // keep track of all visible threats + Vector d = GetAbsOrigin() - player->GetAbsOrigin(); + float distSq = d.LengthSqr(); + + // track enemy sniper threats + if (IsSniperRifle( player->GetActiveCSWeapon() )) + { + m_isEnemySniperVisible = true; + + // keep track of the most dangerous sniper we see + if (sniperThreat) + { + if (IsPlayerLookingAtMe( player, lookingAtMeTolerance )) + { + if (sniperThreatIsFacingMe) + { + // several snipers are facing us - keep closest + if (distSq < sniperThreatRange) + { + sniperThreat = player; + sniperThreatRange = distSq; + sniperThreatIsFacingMe = true; + } + } + else + { + // even if this sniper is farther away, keep it because he's aiming at us + sniperThreat = player; + sniperThreatRange = distSq; + sniperThreatIsFacingMe = true; + } + } + else + { + // this sniper is not looking at us, only consider it if we dont have a sniper facing us + if (!sniperThreatIsFacingMe && distSq < sniperThreatRange) + { + sniperThreat = player; + sniperThreatRange = distSq; + } + } + } + else + { + // first sniper we see + sniperThreat = player; + sniperThreatRange = distSq; + sniperThreatIsFacingMe = IsPlayerLookingAtMe( player, lookingAtMeTolerance ); + } + } + + + { + VPROF_BUDGET( "CCSBot::Sort Threats", VPROF_BUDGETGROUP_NPCS ); + + // maintain set of visible threats, sorted by increasing distance + if (threatCount == 0) + { + threat[0].enemy = player; + threat[0].range = distSq; + threatCount = 1; + } + else + { + // find insertion point + int j; + for( j=0; j<threatCount; ++j ) + { + if (distSq < threat[j].range) + break; + } + + // shift lower half down a notch + for( int k=threatCount-1; k>=j; --k ) + threat[k+1] = threat[k]; + + // insert threat into sorted list + threat[j].enemy = player; + threat[j].range = distSq; + + if (threatCount < MAX_THREATS) + ++threatCount; + } + } + } + } + + + { + VPROF_BUDGET( "CCSBot::Count nearby Friends & Enemies", VPROF_BUDGETGROUP_NPCS ); + + // track the maximum enemy and friend counts we've seen recently + int prevEnemies = m_nearbyEnemyCount; + m_nearbyEnemyCount = 0; + m_nearbyFriendCount = 0; + for( i=0; i<MAX_PLAYERS; ++i ) + { + if (m_watchInfo[i].timestamp <= 0.0f) + continue; + + const float recentTime = 3.0f; + if (gpGlobals->curtime - m_watchInfo[i].timestamp < recentTime) + { + if (m_watchInfo[i].isEnemy) + ++m_nearbyEnemyCount; + else + ++m_nearbyFriendCount; + } + } + + // note when we saw this batch of enemies + if (prevEnemies == 0 && m_nearbyEnemyCount > 0) + { + m_firstSawEnemyTimestamp = gpGlobals->curtime; + } + } + + + { + VPROF_BUDGET( "CCSBot::Track enemy Place", VPROF_BUDGETGROUP_NPCS ); + + // + // Track the place where we saw most of our enemies + // + struct PlaceRank + { + unsigned int place; + int count; + }; + static PlaceRank placeRank[ MAX_PLACES_PER_MAP ]; + int locCount = 0; + + PlaceRank common; + common.place = 0; + common.count = 0; + + for( i=0; i<threatCount; ++i ) + { + // find the area the player/bot is standing on + CNavArea *area; + CCSBot *bot = dynamic_cast<CCSBot *>(threat[i].enemy); + if (bot && bot->IsBot()) + { + area = bot->GetLastKnownArea(); + } + else + { + Vector enemyOrigin = GetCentroid( threat[i].enemy ); + area = TheNavMesh->GetNearestNavArea( enemyOrigin ); + } + + if (area == NULL) + continue; + + unsigned int threatLoc = area->GetPlace(); + if (!threatLoc) + continue; + + // if place is already in set, increment count + int j; + for( j=0; j<locCount; ++j ) + if (placeRank[j].place == threatLoc) + break; + + if (j == locCount) + { + // new place + if (locCount < MAX_PLACES_PER_MAP) + { + placeRank[ locCount ].place = threatLoc; + placeRank[ locCount ].count = 1; + + if (common.count == 0) + common = placeRank[locCount]; + + ++locCount; + } + } + else + { + // others are in that place, increment + ++placeRank[j].count; + + // keep track of the most common place + if (placeRank[j].count > common.count) + common = placeRank[j]; + } + } + + // remember most common place + m_enemyPlace = common.place; + } + + + { + VPROF_BUDGET( "CCSBot::Select Threat", VPROF_BUDGETGROUP_NPCS ); + + if (threatCount == 0) + return NULL; + + // if we can still see our current threat, keep it + // unless a new one is much closer + bool sawCloserThreat = false; + bool sawCurrentThreat = false; + int t; + for( t=0; t<threatCount; ++t ) + { + if ( threat[t].enemy == currentThreat ) + { + sawCurrentThreat = true; + } + else if ( threat[t].enemy != currentThreat && + IsSignificantlyCloser( threat[t].enemy, currentThreat ) ) + { + sawCloserThreat = true; + } + } + + if ( sawCurrentThreat && !sawCloserThreat ) + { + return currentThreat; + } + + // if we are a sniper and we see a sniper threat, attack it unless + // there are other close enemies facing me + if (IsSniper() && sniperThreat) + { + const float closeCombatRange = 500.0f; + + for( t=0; t<threatCount; ++t ) + { + if (threat[t].range < closeCombatRange && IsPlayerLookingAtMe( threat[t].enemy, lookingAtMeTolerance )) + { + return threat[t].enemy; + } + } + + return sniperThreat; + } + + // otherwise, find the closest threat that is looking at me + for( t=0; t<threatCount; ++t ) + { + if (IsPlayerLookingAtMe( threat[t].enemy, lookingAtMeTolerance )) + { + return threat[t].enemy; + } + } + } + + + // return closest threat + return threat[0].enemy; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update our reaction time queue + */ +void CCSBot::UpdateReactionQueue( void ) +{ + VPROF_BUDGET( "CCSBot::UpdateReactionQueue", VPROF_BUDGETGROUP_NPCS ); + + // zombies dont see any threats + if (cv_bot_zombie.GetBool()) + return; + + // find biggest threat at this instant + CCSPlayer *threat = FindMostDangerousThreat(); + + // reset timer + m_attentionInterval.Start(); + + + int now = m_enemyQueueIndex; + + // store a snapshot of its state at the end of the reaction time queue + if (threat) + { + m_enemyQueue[ now ].player = threat; + m_enemyQueue[ now ].isReloading = threat->IsReloading(); + m_enemyQueue[ now ].isProtectedByShield = threat->IsProtectedByShield(); + } + else + { + m_enemyQueue[ now ].player = NULL; + m_enemyQueue[ now ].isReloading = false; + m_enemyQueue[ now ].isProtectedByShield = false; + } + + // queue is round-robin + ++m_enemyQueueIndex; + if (m_enemyQueueIndex >= MAX_ENEMY_QUEUE) + m_enemyQueueIndex = 0; + + if (m_enemyQueueCount < MAX_ENEMY_QUEUE) + ++m_enemyQueueCount; + + // clamp reaction time to enemy queue size + float reactionTime = GetProfile()->GetReactionTime() - g_BotUpdateInterval; + float maxReactionTime = (MAX_ENEMY_QUEUE * g_BotUpdateInterval) - 0.01f; + if (reactionTime > maxReactionTime) + reactionTime = maxReactionTime; + + // "rewind" time back to our reaction time + int reactionTimeSteps = (int)((reactionTime / g_BotUpdateInterval) + 0.5f); + + int i = now - reactionTimeSteps; + if (i < 0) + i += MAX_ENEMY_QUEUE; + + m_enemyQueueAttendIndex = (unsigned char)i; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the most dangerous threat we are "conscious" of + */ +CCSPlayer *CCSBot::GetRecognizedEnemy( void ) +{ + if (m_enemyQueueAttendIndex >= m_enemyQueueCount || IsBlind()) + { + return NULL; + } + + return m_enemyQueue[ m_enemyQueueAttendIndex ].player; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the enemy we are "conscious" of is reloading + */ +bool CCSBot::IsRecognizedEnemyReloading( void ) +{ + if (m_enemyQueueAttendIndex >= m_enemyQueueCount) + return false; + + return m_enemyQueue[ m_enemyQueueAttendIndex ].isReloading; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the enemy we are "conscious" of is hiding behind a shield + */ +bool CCSBot::IsRecognizedEnemyProtectedByShield( void ) +{ + if (m_enemyQueueAttendIndex >= m_enemyQueueCount) + return false; + + return m_enemyQueue[ m_enemyQueueAttendIndex ].isProtectedByShield; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return distance to closest enemy we are "conscious" of + */ +float CCSBot::GetRangeToNearestRecognizedEnemy( void ) +{ + const CCSPlayer *enemy = GetRecognizedEnemy(); + + if (enemy) + return (GetAbsOrigin() - enemy->GetAbsOrigin()).Length(); + + return 99999999.9f; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Blind the bot for the given duration + */ +void CCSBot::Blind( float holdTime, float fadeTime, float startingAlpha ) +{ + PrintIfWatched( "Blinded: holdTime = %3.2f, fadeTime = %3.2f, alpha = %3.2f\n", holdTime, fadeTime, startingAlpha ); + + // if we were only blinded a little bit, shake it off + const float mildBlindTime = 3.0f; + if (holdTime < mildBlindTime) + { + Wait( 0.75f * holdTime ); + BecomeAlert(); + BaseClass::Blind( holdTime, fadeTime, startingAlpha ); + return; + } + + + // if blinded while in combat - then spray and pray! + m_blindFire = IsAttacking(); + + // retreat + // do this first, so spot selection happens before IsBlind() is set + const float hideRange = 400.0f; + TryToRetreat( hideRange ); + + PrintIfWatched( "I'm blind!\n" ); + + if (RandomFloat( 0.0f, 100.0f ) < 33.3f) + { + GetChatter()->Say( "Blinded", 1.0f ); + } + + // no longer safe + AdjustSafeTime(); + + // decide which way to move while blind + m_blindMoveDir = static_cast<NavRelativeDirType>( RandomInt( 1, NUM_RELATIVE_DIRECTIONS-1 ) ); + + // if we're defusing, don't give up + if (IsDefusingBomb()) + { + return; + } + + // can't see to aim at enemy + StopAiming(); + + // dont override "facing away" behavior unless we are going to spray and pray + if (m_blindFire) + { + ClearLookAt(); + + // just look straight ahead while blind + Vector forward; + EyeVectors( &forward ); + SetLookAt( "Blind", EyePosition() + 10000.0f * forward, PRIORITY_UNINTERRUPTABLE, holdTime + 0.5f * fadeTime ); + } + + StopWaiting(); + BecomeAlert(); + + BaseClass::Blind( holdTime, fadeTime, startingAlpha ); +} + + +//-------------------------------------------------------------------------------------------------------------- +class CheckLookAt +{ +public: + CheckLookAt( const CCSBot *me, bool testFOV ) + { + m_me = me; + m_testFOV = testFOV; + } + + bool operator() ( CBasePlayer *player ) + { + if (!m_me->IsEnemy( player )) + return true; + + if (m_testFOV && !(const_cast< CCSBot * >(m_me)->FInViewCone( player->WorldSpaceCenter() ))) + return true; + + if (!m_me->IsPlayerLookingAtMe( player )) + return true; + + if (m_me->IsVisible( (CCSPlayer *)player )) + return false; + + return true; + } + + const CCSBot *m_me; + bool m_testFOV; +}; + +/** + * Return true if any enemy I have LOS to is looking directly at me + * @todo Use reaction time pipeline + */ +bool CCSBot::IsAnyVisibleEnemyLookingAtMe( bool testFOV ) const +{ + CheckLookAt checkLookAt( this, testFOV ); + return (ForEachPlayer( checkLookAt ) == false) ? true : false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do panic behavior + */ +void CCSBot::UpdatePanicLookAround( void ) +{ + if (m_panicTimer.IsElapsed()) + { + return; + } + + if (IsEnemyVisible()) + { + StopPanicking(); + return; + } + + if (HasLookAtTarget()) + { + // wait until we finish our current look at + return; + } + + // select a spot somewhere behind us to look at as we search for our attacker + const QAngle &eyeAngles = EyeAngles(); + + QAngle newAngles; + newAngles.x = RandomFloat( -30.0f, 30.0f ); + + // Look directly behind at a random offset in a 90 window. + float yaw = RandomFloat( 135.0f, 225.0f ); + newAngles.y = eyeAngles.y + yaw; + newAngles.z = 0.0f; + + Vector forward; + AngleVectors( newAngles, &forward ); + + Vector spot; + spot = EyePosition() + 1000.0f * forward; + + SetLookAt( "Panic", spot, PRIORITY_HIGH, 0.0f ); + PrintIfWatched( "Panic yaw angle = %3.2f\n", newAngles.y ); +} diff --git a/game/server/cstrike/bot/cs_bot_weapon.cpp b/game/server/cstrike/bot/cs_bot_weapon.cpp new file mode 100644 index 0000000..4c737f5 --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_weapon.cpp @@ -0,0 +1,1363 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" +#include "basecsgrenade_projectile.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Fire our active weapon towards our current enemy + * NOTE: Aiming our weapon is handled in RunBotUpkeep() + */ +void CCSBot::FireWeaponAtEnemy( void ) +{ + if (cv_bot_dont_shoot.GetBool()) + { + return; + } + + CBasePlayer *enemy = GetBotEnemy(); + if (enemy == NULL) + { + return; + } + + Vector myOrigin = GetCentroid( this ); + + if (IsUsingSniperRifle()) + { + // if we're using a sniper rifle, don't fire until we are standing still, are zoomed in, and not rapidly moving our view + if (!IsNotMoving() || IsWaitingForZoom() || !HasViewBeenSteady( GetProfile()->GetReactionTime() ) ) + { + return; + } + } + + if (gpGlobals->curtime > m_fireWeaponTimestamp && + GetTimeSinceAcquiredCurrentEnemy() >= GetProfile()->GetAttackDelay() && + !IsSurprised()) + { + if (!(IsRecognizedEnemyProtectedByShield() && IsPlayerFacingMe( enemy )) && // don't shoot at enemies behind shields + !IsReloading() && + !IsActiveWeaponClipEmpty() && + //gpGlobals->curtime > m_reacquireTimestamp && + IsEnemyVisible()) + { + // we have a clear shot - pull trigger if we are aiming at enemy + Vector toAimSpot = m_aimSpot - EyePosition(); + float rangeToEnemy = toAimSpot.NormalizeInPlace(); + + if ( IsUsingSniperRifle() ) + { + // check our accuracy versus our target distance + float fProjectedSpread = rangeToEnemy * GetActiveCSWeapon()->GetInaccuracy(); + float fRequiredSpread = IsUsing( WEAPON_AWP ) ? 50.0f : 25.0f; // AWP will kill with any hit + if ( fProjectedSpread > fRequiredSpread ) + return; + } + + // get actual view direction vector + Vector aimDir = GetViewVector(); + + float onTarget = DotProduct( toAimSpot, aimDir ); + + // aim more precisely with a sniper rifle + // because rifles' bullets spray, don't have to be very precise + const float halfSize = (IsUsingSniperRifle()) ? HalfHumanWidth : 2.0f * HalfHumanWidth; + + // aiming tolerance depends on how close the target is - closer targets subtend larger angles + float aimTolerance = (float)cos( atan( halfSize / rangeToEnemy ) ); + + if (onTarget > aimTolerance) + { + bool doAttack; + + // if friendly fire is on, don't fire if a teammate is blocking our line of fire + if (TheCSBots()->AllowFriendlyFireDamage()) + { + if (IsFriendInLineOfFire()) + doAttack = false; + else + doAttack = true; + } + else + { + // fire freely + doAttack = true; + } + + if (doAttack) + { + // if we are using a knife, only swing it if we're close + if (IsUsingKnife()) + { + const float knifeRange = 75.0f; // 50 + if (rangeToEnemy < knifeRange) + { + // since we've given ourselves away - run! + ForceRun( 5.0f ); + + // if our prey is facing away, backstab him! + if (!IsPlayerFacingMe( enemy )) + { + SecondaryAttack(); + } + else + { + // randomly choose primary and secondary attacks with knife + const float knifeStabChance = 33.3f; + if (RandomFloat( 0, 100 ) < knifeStabChance) + SecondaryAttack(); + else + PrimaryAttack(); + } + } + } + else + { + PrimaryAttack(); + } + } + + if (IsUsingPistol()) + { + // high-skill bots fire their pistols quickly at close range + const float closePistolRange = 360.0f; + if (GetProfile()->GetSkill() > 0.75f && rangeToEnemy < closePistolRange) + { + // fire as fast as possible + m_fireWeaponTimestamp = 0.0f; + } + else + { + // fire somewhat quickly + m_fireWeaponTimestamp = RandomFloat( 0.15f, 0.4f ); + } + } + else // not using a pistol + { + const float sprayRange = 400.0f; + if (GetProfile()->GetSkill() < 0.5f || rangeToEnemy < sprayRange || IsUsingMachinegun()) + { + // spray 'n pray if enemy is close, or we're not that good, or we're using the big machinegun + m_fireWeaponTimestamp = 0.0f; + } + else + { + const float distantTargetRange = 800.0f; + if (!IsUsingSniperRifle() && rangeToEnemy > distantTargetRange) + { + // if very far away, fire slowly for better accuracy + m_fireWeaponTimestamp = RandomFloat( 0.3f, 0.7f ); + } + else + { + // fire short bursts for accuracy + m_fireWeaponTimestamp = RandomFloat( 0.15f, 0.25f ); // 0.15, 0.5 + } + } + } + + // subtract system latency + m_fireWeaponTimestamp -= g_BotUpdateInterval; + + m_fireWeaponTimestamp += gpGlobals->curtime; + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Set the current aim offset using given accuracy (1.0 = perfect aim, 0.0f = terrible aim) + */ +void CCSBot::SetAimOffset( float accuracy ) +{ + // if our accuracy is less than perfect, it will improve as we "focus in" while not rotating our view + if (accuracy < 1.0f) + { + // if we moved our view, reset our "focus" mechanism + if (IsViewMoving( 100.0f )) + m_aimSpreadTimestamp = gpGlobals->curtime; + + // focusTime is the time it takes for a bot to "focus in" for very good aim, from 2 to 5 seconds + const float focusTime = MAX( 5.0f * (1.0f - accuracy), 2.0f ); + float focusInterval = gpGlobals->curtime - m_aimSpreadTimestamp; + + float focusAccuracy = focusInterval / focusTime; + + // limit how much "focus" will help + const float maxFocusAccuracy = 0.75f; + if (focusAccuracy > maxFocusAccuracy) + focusAccuracy = maxFocusAccuracy; + + accuracy = MAX( accuracy, focusAccuracy ); + } + + //PrintIfWatched( "Accuracy = %4.3f\n", accuracy ); + + // aim error increases with distance, such that actual crosshair error stays about the same + float range = (m_lastEnemyPosition - EyePosition()).Length(); + float maxOffset = (GetFOV()/GetDefaultFOV()) * 0.05f * range; // 0.1 + float error = maxOffset * (1.0f - accuracy); + + m_aimOffsetGoal.x = RandomFloat( -error, error ); + m_aimOffsetGoal.y = RandomFloat( -error, error ); + m_aimOffsetGoal.z = RandomFloat( -error, error ); + + // define time when aim offset will automatically be updated + m_aimOffsetTimestamp = gpGlobals->curtime + RandomFloat( 0.25f, 1.0f ); // 0.25, 1.5f +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Wiggle aim error based on GetProfile()->GetSkill() + */ +void CCSBot::UpdateAimOffset( void ) +{ + if (gpGlobals->curtime >= m_aimOffsetTimestamp) + { + SetAimOffset( GetProfile()->GetSkill() ); + } + + // move current offset towards goal offset + Vector d = m_aimOffsetGoal - m_aimOffset; + const float stiffness = 0.1f; + m_aimOffset.x += stiffness * d.x; + m_aimOffset.y += stiffness * d.y; + m_aimOffset.z += stiffness * d.z; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Change our zoom level to be appropriate for the given range. + * Return true if the zoom level changed. + */ +bool CCSBot::AdjustZoom( float range ) +{ + bool adjustZoom = false; + + if (IsUsingSniperRifle()) + { + const float sniperZoomRange = 150.0f; // NOTE: This must be less than sniperMinRange in AttackState + const float sniperFarZoomRange = 1500.0f; + + // if range is too close, don't zoom + if (range <= sniperZoomRange) + { + // zoom out + if (GetZoomLevel() != NO_ZOOM) + { + adjustZoom = true; + } + } + else if (range < sniperFarZoomRange) + { + // maintain low zoom + if (GetZoomLevel() != LOW_ZOOM) + { + adjustZoom = true; + } + } + else + { + // maintain high zoom + if (GetZoomLevel() != HIGH_ZOOM) + { + adjustZoom = true; + } + } + } + else + { + // zoom out + if (GetZoomLevel() != NO_ZOOM) + { + adjustZoom = true; + } + } + + if (adjustZoom) + { + SecondaryAttack(); + + // pause after zoom to allow "eyes" to refocus +// m_zoomTimer.Start( 0.25f + (1.0f - GetProfile()->GetSkill()) ); + m_zoomTimer.Start( 0.25f ); + } + + return adjustZoom; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if using the specific weapon + */ +bool CCSBot::IsUsing( CSWeaponID weaponID ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon == NULL) + return false; + + if (weapon->IsA( weaponID )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we are using a weapon with a removable silencer + */ +bool CCSBot::DoesActiveWeaponHaveSilencer( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon == NULL) + return false; + + if (weapon->IsA( WEAPON_M4A1 ) || weapon->IsA( WEAPON_USP )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are using a sniper rifle + */ +bool CCSBot::IsUsingSniperRifle( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && IsSniperRifle( weapon )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we have a sniper rifle in our inventory + */ +bool CCSBot::IsSniper( void ) const +{ + CWeaponCSBase *weapon = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + + if (weapon && IsSniperRifle( weapon )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are actively sniping (moving to sniper spot or settled in) + */ +bool CCSBot::IsSniping( void ) const +{ + if (GetTask() == MOVE_TO_SNIPER_SPOT || GetTask() == SNIPING) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are using a shotgun + */ +bool CCSBot::IsUsingShotgun( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon == NULL) + return false; + + return weapon->IsKindOf(WEAPONTYPE_SHOTGUN); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if using the big 'ol machinegun + */ +bool CCSBot::IsUsingMachinegun( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && weapon->IsA( WEAPON_M249 )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if primary weapon doesn't exist or is totally out of ammo + */ +bool CCSBot::IsPrimaryWeaponEmpty( void ) const +{ + CWeaponCSBase *weapon = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + + if (weapon == NULL) + return true; + + // check if gun has any ammo left + if (weapon->HasAnyAmmo()) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if pistol doesn't exist or is totally out of ammo + */ +bool CCSBot::IsPistolEmpty( void ) const +{ + CWeaponCSBase *weapon = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_PISTOL ) ); + + if (weapon == NULL) + return true; + + // check if gun has any ammo left + if (weapon->HasAnyAmmo()) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip the given item + */ +bool CCSBot::DoEquip( CWeaponCSBase *weapon ) +{ + if (weapon == NULL) + return false; + + // check if weapon has any ammo left + if (!weapon->HasAnyAmmo()) + return false; + + // equip it + SelectItem( weapon->GetClassname() ); + m_equipTimer.Start(); + + return true; +} + + +// throttle how often equipping is allowed +const float minEquipInterval = 5.0f; + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip the best weapon we are carrying that has ammo + */ +void CCSBot::EquipBestWeapon( bool mustEquip ) +{ + // throttle how often equipping is allowed + if (!mustEquip && m_equipTimer.GetElapsedTime() < minEquipInterval) + return; + + CCSBotManager *ctrl = static_cast<CCSBotManager *>( TheBots ); + + CWeaponCSBase *primary = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + if (primary) + { + CSWeaponType weaponClass = primary->GetCSWpnData().m_WeaponType; + + if ((ctrl->AllowShotguns() && weaponClass == WEAPONTYPE_SHOTGUN) || + (ctrl->AllowMachineGuns() && weaponClass == WEAPONTYPE_MACHINEGUN) || + (ctrl->AllowRifles() && weaponClass == WEAPONTYPE_RIFLE) || + (ctrl->AllowShotguns() && weaponClass == WEAPONTYPE_SHOTGUN) || + (ctrl->AllowSnipers() && weaponClass == WEAPONTYPE_SNIPER_RIFLE) || + (ctrl->AllowSubMachineGuns() && weaponClass == WEAPONTYPE_SUBMACHINEGUN)) + { + if (DoEquip( primary )) + return; + } + } + + if (ctrl->AllowPistols()) + { + if (DoEquip( static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_PISTOL ) ) )) + return; + } + + // always have a knife + EquipKnife(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip our pistol + */ +void CCSBot::EquipPistol( void ) +{ + // throttle how often equipping is allowed + if (m_equipTimer.GetElapsedTime() < minEquipInterval) + return; + + if (TheCSBots()->AllowPistols() && !IsUsingPistol()) + { + CWeaponCSBase *pistol = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_PISTOL ) ); + DoEquip( pistol ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip the knife + */ +void CCSBot::EquipKnife( void ) +{ + if (!IsUsingKnife()) + { + SelectItem( "weapon_knife" ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we have a grenade in our inventory + */ +bool CCSBot::HasGrenade( void ) const +{ + CWeaponCSBase *grenade = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_GRENADES ) ); + return (grenade) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Equip a grenade, return false if we cant + */ +bool CCSBot::EquipGrenade( bool noSmoke ) +{ + // snipers don't use grenades + if (IsSniper()) + return false; + + if (IsUsingGrenade()) + return true; + + if (HasGrenade()) + { + CWeaponCSBase *grenade = static_cast<CWeaponCSBase *>( Weapon_GetSlot( WEAPON_SLOT_GRENADES ) ); + + if (noSmoke && grenade->IsA( WEAPON_SMOKEGRENADE )) + return false; + + SelectItem( grenade->GetClassname() ); + + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we have knife equipped + */ +bool CCSBot::IsUsingKnife( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && weapon->IsA( WEAPON_KNIFE )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we have pistol equipped + */ +bool CCSBot::IsUsingPistol( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (weapon && weapon->IsPistol()) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if we have a grenade equipped + */ +bool CCSBot::IsUsingGrenade( void ) const +{ + CWeaponCSBase *weapon = GetActiveCSWeapon(); + + if (!weapon) + return false; + + if (weapon->IsA( WEAPON_FLASHBANG ) || + weapon->IsA( WEAPON_SMOKEGRENADE ) || + weapon->IsA( WEAPON_HEGRENADE )) + return true; + + return false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin the process of throwing the grenade + */ +void CCSBot::ThrowGrenade( const Vector &target ) +{ + if (IsUsingGrenade() && m_grenadeTossState == NOT_THROWING && !IsOnLadder()) + { + m_grenadeTossState = START_THROW; + m_tossGrenadeTimer.Start( 2.0f ); + + const float angleTolerance = 3.0f; + SetLookAt( "GrenadeThrow", target, PRIORITY_UNINTERRUPTABLE, 4.0f, false, angleTolerance ); + + Wait( RandomFloat( 2.0f, 4.0f ) ); + + if (cv_bot_debug.GetBool() && IsLocalPlayerWatchingMe()) + { + NDebugOverlay::Cross3D( target, 25.0f, 255, 125, 0, true, 3.0f ); + } + + PrintIfWatched( "%3.2f: Grenade: START_THROW\n", gpGlobals->curtime ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns true if our weapon can attack + */ +bool CCSBot::CanActiveWeaponFire( void ) const +{ + return ( GetActiveWeapon() && GetActiveWeapon()->m_flNextPrimaryAttack <= gpGlobals->curtime ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Find spot to throw grenade ahead of us and "around the corner" along our path + */ +bool CCSBot::FindGrenadeTossPathTarget( Vector *pos ) +{ + if (!HasPath()) + return false; + + // find farthest point we can see on the path + int i; + for( i=m_pathIndex; i<m_pathLength; ++i ) + { + if (!FVisible( m_path[i].pos + Vector( 0, 0, HalfHumanHeight ) )) + break; + } + + if (i == m_pathIndex) + return false; + + // find exact spot where we lose sight + Vector dir = m_path[i].pos - m_path[i-1].pos; + float length = dir.NormalizeInPlace(); + + const float inc = 25.0f; + Vector p; + Vector visibleSpot = m_path[i-1].pos; + for( float t = 0.0f; t<length; t += inc ) + { + p = m_path[i-1].pos + t * dir; + p.z += HalfHumanHeight; + if (!FVisible( p )) + break; + + visibleSpot = p; + } + + // massage the location a bit + visibleSpot.z += 10.0f; + + const float bufferRange = 50.0f; + + trace_t result; + Vector check; + + // check +X + check = visibleSpot + Vector( 999.9f, 0, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = result.endpos.x - visibleSpot.x; + if (range < bufferRange) + { + visibleSpot.x = result.endpos.x - bufferRange; + } + } + + // check -X + check = visibleSpot + Vector( -999.9f, 0, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = visibleSpot.x - result.endpos.x; + if (range < bufferRange) + { + visibleSpot.x = result.endpos.x + bufferRange; + } + } + + // check +Y + check = visibleSpot + Vector( 0, 999.9f, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = result.endpos.y - visibleSpot.y; + if (range < bufferRange) + { + visibleSpot.y = result.endpos.y - bufferRange; + } + } + + // check -Y + check = visibleSpot + Vector( 0, -999.9f, 0 ); + UTIL_TraceLine( visibleSpot, check, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.fraction < 1.0f) + { + float range = visibleSpot.y - result.endpos.y; + if (range < bufferRange) + { + visibleSpot.y = result.endpos.y + bufferRange; + } + } + + *pos = visibleSpot; + return true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Look for grenade throw targets and throw the grenade + */ +void CCSBot::LookForGrenadeTargets( void ) +{ + if (!IsUsingGrenade() || IsThrowingGrenade()) + { + return; + } + + const CNavArea *tossArea = GetInitialEncounterArea(); + if (tossArea == NULL) + { + return; + } + + int enemyTeam = OtherTeam( GetTeamNumber() ); + + // check if we should put our grenade away + if (tossArea->GetEarliestOccupyTime( enemyTeam ) > gpGlobals->curtime) + { + EquipBestWeapon( MUST_EQUIP ); + return; + } + + // throw grenades at initial encounter area + Vector tossTarget = Vector( 0, 0, 0 ); + if (!tossArea->IsVisible( EyePosition(), &tossTarget )) + { + return; + } + + + CWeaponCSBase *weapon = GetActiveCSWeapon(); + if (weapon && weapon->IsA( WEAPON_SMOKEGRENADE )) + { + // don't worry so much about smokes + ThrowGrenade( tossTarget ); + PrintIfWatched( "Throwing smoke grenade!" ); + SetInitialEncounterArea( NULL ); + return; + } + else // explosive and flashbang grenades + { + // initial encounter area is visible, wait to throw until timing is right + + const float leadTime = 1.5f; + float enemyTime = tossArea->GetEarliestOccupyTime( enemyTeam ); + if (enemyTime - TheCSBots()->GetElapsedRoundTime() > leadTime) + { + // don't throw yet + return; + } + + + Vector to = tossTarget - EyePosition(); + float range = to.Length(); + + const float slope = 0.2f; // 0.25f; + float tossHeight = slope * range; + + trace_t result; + CTraceFilterNoNPCsOrPlayer traceFilter( this, COLLISION_GROUP_NONE ); + + const float heightInc = tossHeight / 10.0f; + Vector target; + float safeSpace = tossHeight / 2.0f; + + // Build a box to sweep along the ray when looking for obstacles + const Vector& eyePosition = EyePosition(); + Vector mins = VEC_HULL_MIN; + Vector maxs = VEC_HULL_MAX; + mins.z = 0; + maxs.z = heightInc; + + + // find low and high bounds of toss window + float low = 0.0f; + float high = tossHeight + safeSpace; + bool gotLow = false; + float lastH = 0.0f; + for( float h = 0.0f; h < 3.0f * tossHeight; h += heightInc ) + { + target = tossTarget + Vector( 0, 0, h ); + + // make sure toss line is clear + + QAngle angles( 0, 0, 0 ); + Ray_t ray; + ray.Init( eyePosition, target, mins, maxs ); + enginetrace->TraceRay( ray, MASK_VISIBLE_AND_NPCS | CONTENTS_GRATE, &traceFilter, &result ); + if (result.fraction == 1.0f) + { + //NDebugOverlay::SweptBox( eyePosition, target, mins, maxs, angles, 0, 0, 255, 40, 10.0f ); + + // line is clear + if (!gotLow) + { + low = h; + gotLow = true; + } + } + else + { + //NDebugOverlay::SweptBox( eyePosition, target, mins, maxs, angles, 255, 0, 0, 5, 10.0f ); + + // line is blocked + if (gotLow) + { + high = lastH; + break; + } + } + + lastH = h; + } + + if (gotLow) + { + // throw grenade into toss window + if (tossHeight < low) + { + if (low + safeSpace > high) + { + // narrow window + tossHeight = (high + low)/2.0f; + } + else + { + tossHeight = low + safeSpace; + } + } + else if (tossHeight > high - safeSpace) + { + if (high - safeSpace < low) + { + // narrow window + tossHeight = (high + low)/2.0f; + } + else + { + tossHeight = high - safeSpace; + } + } + + ThrowGrenade( tossTarget + Vector( 0, 0, tossHeight ) ); + SetInitialEncounterArea( NULL ); + return; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +class FOVClearOfFriends +{ +public: + FOVClearOfFriends( CCSBot *me ) + { + m_me = me; + } + + bool operator() ( CBasePlayer *player ) + { + if (player == m_me || !player->IsAlive()) + return true; + + if (m_me->InSameTeam( player )) + { + Vector to = player->EyePosition() - m_me->EyePosition(); + to.NormalizeInPlace(); + + Vector forward; + m_me->EyeVectors( &forward ); + + if (DotProduct( to, forward ) > 0.95f) + { + if (m_me->IsVisible( (CCSPlayer *)player )) + { + // we see a friend in our FOV + return false; + } + } + } + + return true; + } + + CCSBot *m_me; +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Process the grenade throw state machine + */ +void CCSBot::UpdateGrenadeThrow( void ) +{ + switch( m_grenadeTossState ) + { + case START_THROW: + { + if (m_tossGrenadeTimer.IsElapsed()) + { + // something prevented the throw - give up + EquipBestWeapon( MUST_EQUIP ); + ClearLookAt(); + m_grenadeTossState = NOT_THROWING; + PrintIfWatched( "%3.2f: Grenade: THROW FAILED\n", gpGlobals->curtime ); + return; + } + + if (m_lookAtSpotState == LOOK_AT_SPOT) + { + // don't throw if there are friends ahead of us + FOVClearOfFriends fovClear( this ); + if (ForEachPlayer( fovClear )) + { + m_grenadeTossState = FINISH_THROW; + m_tossGrenadeTimer.Start( 1.0f ); + PrintIfWatched( "%3.2f: Grenade: FINISH_THROW\n", gpGlobals->curtime ); + } + else + { + PrintIfWatched( "%3.2f: Grenade: Friend is in the way...\n", gpGlobals->curtime ); + } + } + + // hold in the trigger and be ready to throw + PrimaryAttack(); + + break; + } + + case FINISH_THROW: + { + // throw the grenade and hold our aiming line for a moment + if (m_tossGrenadeTimer.IsElapsed()) + { + ClearLookAt(); + + m_grenadeTossState = NOT_THROWING; + PrintIfWatched( "%3.2f: Grenade: THROW COMPLETE\n", gpGlobals->curtime ); + } + break; + } + + default: + { + if (IsUsingGrenade()) + { + // pull the pin + PrimaryAttack(); + } + break; + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +class GrenadeResponse +{ +public: + GrenadeResponse( CCSBot *me ) + { + m_me = me; + } + + bool operator() ( ActiveGrenade *ag ) const + { + const float retreatRange = 300.0f; + const float hideTime = 1.0f; + + // do we see this grenade + if (m_me->IsVisible( ag->GetPosition(), CHECK_FOV, (CBaseEntity *)ag->GetEntity() )) + { + // we see it + if (ag->IsSmoke()) + { + // ignore smokes + return true; + } + + Vector velDir = ag->GetEntity()->GetAbsVelocity(); + float grenadeSpeed = velDir.NormalizeInPlace(); + const float atRestSpeed = 50.0f; + + const float aboutToBlow = 0.5f; + if (ag->IsFlashbang() && ag->GetEntity()->m_flDetonateTime - gpGlobals->curtime < aboutToBlow) + { + // turn away from flashbangs about to explode + QAngle eyeAngles = m_me->EyeAngles(); + + float yaw = RandomFloat( 100.0f, 135.0f ); + eyeAngles.y += (RandomFloat( -1.0f, 1.0f ) < 0.0f) ? (-yaw) : yaw; + + Vector forward; + AngleVectors( eyeAngles, &forward ); + + Vector away = m_me->EyePosition() - 1000.0f * forward; + const float duration = 2.0f; + + m_me->ClearLookAt(); + m_me->SetLookAt( "Avoid Flashbang", away, PRIORITY_UNINTERRUPTABLE, duration ); + + m_me->StopAiming(); + + return false; + } + + + // flee from grenades if close by or thrown towards us + const float throwDangerRange = 750.0f; + const float nearDangerRange = 300.0f; + Vector to = ag->GetPosition() - m_me->GetAbsOrigin(); + float range = to.NormalizeInPlace(); + if (range > throwDangerRange) + { + return true; + } + + if (grenadeSpeed > atRestSpeed) + { + // grenade is moving + if (DotProduct( to, velDir ) >= -0.5f) + { + // going away from us + return true; + } + + m_me->PrintIfWatched( "Retreating from a grenade thrown towards me!\n" ); + } + else if (range < nearDangerRange) + { + // grenade has come to rest near us + m_me->PrintIfWatched( "Retreating from a grenade that landed near me!\n" ); + } + + // retreat! + m_me->TryToRetreat( retreatRange, hideTime ); + + return false; + } + + return true; + } + + CCSBot *m_me; +}; + +/** + * React to enemy grenades we see + */ +void CCSBot::AvoidEnemyGrenades( void ) +{ + // low skill bots dont avoid grenades + if (GetProfile()->GetSkill() < 0.5) + { + return; + } + + if (IsAvoidingGrenade()) + { + // already avoiding one + return; + } + + // low skill bots don't avoid grenades + if (GetProfile()->GetSkill() < 0.6f) + { + return; + } + + GrenadeResponse respond( this ); + if (TheBots->ForEachGrenade( respond ) == false) + { + const float avoidTime = 4.0f; + m_isAvoidingGrenade.Start( avoidTime ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reload our weapon if we must + */ +void CCSBot::ReloadCheck( void ) +{ + const float safeReloadWaitTime = 3.0f; + const float reloadAmmoRatio = 0.6f; + + // don't bother to reload if there are no enemies left + if (GetEnemiesRemaining() == 0) + return; + + if (IsDefusingBomb() || IsReloading()) + return; + + if (IsActiveWeaponClipEmpty()) + { + // high-skill players switch to pistol instead of reloading during combat + if (GetProfile()->GetSkill() > 0.5f && IsAttacking()) + { + if (!GetActiveCSWeapon()->IsPistol() && !IsPistolEmpty()) + { + // switch to pistol instead of reloading + EquipPistol(); + return; + } + } + } + else if (GetTimeSinceLastSawEnemy() > safeReloadWaitTime && GetActiveWeaponAmmoRatio() <= reloadAmmoRatio) + { + // high-skill players use all their ammo and switch to pistol instead of reloading during combat + if (GetProfile()->GetSkill() > 0.5f && IsAttacking()) + return; + } + else + { + // do not need to reload + return; + } + + // don't reload the AWP until it is totally out of ammo + if (IsUsing( WEAPON_AWP ) && !IsActiveWeaponClipEmpty()) + return; + + Reload(); + + // move to cover to reload if there are enemies nearby + if (GetNearbyEnemyCount()) + { + // avoid enemies while reloading (above 0.75 skill always hide to reload) + const float hideChance = 25.0f + 100.0f * GetProfile()->GetSkill(); + + if (!IsHiding() && RandomFloat( 0, 100 ) < hideChance) + { + const float safeTime = 5.0f; + if (GetTimeSinceLastSawEnemy() < safeTime) + { + PrintIfWatched( "Retreating to a safe spot to reload!\n" ); + const Vector *spot = FindNearbyRetreatSpot( this, 1000.0f ); + if (spot) + { + // ignore enemies for a second to give us time to hide + // reaching our hiding spot clears our disposition + IgnoreEnemies( 10.0f ); + + Run(); + StandUp(); + Hide( *spot, 0.0f ); + } + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Silence/unsilence our weapon if we must + */ +void CCSBot::SilencerCheck( void ) +{ + const float safeSilencerWaitTime = 3.5f; // longer than reload check because reloading should take precedence + + if (IsDefusingBomb() || IsReloading() || IsAttacking()) + return; + + // M4A1 and USP are the only weapons with removable silencers + if (!DoesActiveWeaponHaveSilencer()) + return; + + if (GetTimeSinceLastSawEnemy() < safeSilencerWaitTime) + return; + + // don't touch the silencer if there are enemies nearby + if (GetNearbyEnemyCount() == 0) + { + CWeaponCSBase *weapon = GetActiveCSWeapon(); + if (weapon == NULL) + return; + + bool isSilencerOn = weapon->IsSilenced(); + + if ( weapon->m_flNextSecondaryAttack >= gpGlobals->curtime ) + return; + + // equip silencer if we want to and we don't have a shield. + if ( isSilencerOn != (GetProfile()->PrefersSilencer() || GetProfile()->GetSkill() > 0.7f) && !HasShield() ) + { + PrintIfWatched( "%s silencer!\n", (isSilencerOn) ? "Unequipping" : "Equipping" ); + weapon->SecondaryAttack(); + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Invoked when in contact with a CBaseCombatWeapon + */ +bool CCSBot::BumpWeapon( CBaseCombatWeapon *pWeapon ) +{ + CWeaponCSBase *droppedGun = dynamic_cast< CWeaponCSBase* >( pWeapon ); + + // right now we only care about primary weapons on the ground + if ( droppedGun && droppedGun->GetSlot() == WEAPON_SLOT_RIFLE ) + { + CWeaponCSBase *myGun = dynamic_cast< CWeaponCSBase* >( Weapon_GetSlot( WEAPON_SLOT_RIFLE ) ); + + // if the gun on the ground is the same one we have, dont bother + if ( myGun && droppedGun->GetWeaponID() != myGun->GetWeaponID() ) + { + // if we don't have a weapon preference, give up + if ( GetProfile()->HasPrimaryPreference() ) + { + // don't change weapons if we've seen enemies recently + const float safeTime = 2.5f; + if ( GetTimeSinceLastSawEnemy() >= safeTime ) + { + // we have a primary weapon - drop it if the one on the ground is better + for( int i = 0; i < GetProfile()->GetWeaponPreferenceCount(); ++i ) + { + CSWeaponID prefID = GetProfile()->GetWeaponPreference( i ); + + if (!IsPrimaryWeapon( prefID )) + continue; + + // if the gun we are using is more desirable, give up + if ( prefID == myGun->GetWeaponID() ) + break; + + if ( prefID == droppedGun->GetWeaponID() ) + { + // the gun on the ground is better than the one we have - drop our gun + DropRifle(); + break; + } + } + } + } + } + } + + return BaseClass::BumpWeapon( droppedGun ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if a friend is in our weapon's way + * @todo Check more rays for safety. + */ +bool CCSBot::IsFriendInLineOfFire( void ) +{ + // compute the unit vector along our view + Vector aimDir = GetViewVector(); + + // trace the bullet's path + trace_t result; + UTIL_TraceLine( EyePosition(), EyePosition() + 10000.0f * aimDir, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + if (result.DidHitNonWorldEntity()) + { + CBaseEntity *victim = result.m_pEnt; + + if (victim && victim->IsPlayer() && victim->IsAlive()) + { + CBasePlayer *player = static_cast<CBasePlayer *>( victim ); + + if (player->InSameTeam( this )) + return true; + } + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return line-of-sight distance to obstacle along weapon fire ray + * @todo Re-use this computation with IsFriendInLineOfFire() + */ +float CCSBot::ComputeWeaponSightRange( void ) +{ + // compute the unit vector along our view + Vector aimDir = GetViewVector(); + + // trace the bullet's path + trace_t result; + UTIL_TraceLine( EyePosition(), EyePosition() + 10000.0f * aimDir, MASK_PLAYERSOLID, this, COLLISION_GROUP_NONE, &result ); + + return (EyePosition() - result.endpos).Length(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if the given player just fired their weapon + */ +bool CCSBot::DidPlayerJustFireWeapon( const CCSPlayer *player ) const +{ + // if this player has just fired his weapon, we notice him + CWeaponCSBase *weapon = player->GetActiveCSWeapon(); + return (weapon && !weapon->IsSilenced() && weapon->m_flNextPrimaryAttack > gpGlobals->curtime); +} + diff --git a/game/server/cstrike/bot/cs_bot_weapon_id.cpp b/game/server/cstrike/bot/cs_bot_weapon_id.cpp new file mode 100644 index 0000000..21c423e --- /dev/null +++ b/game/server/cstrike/bot/cs_bot_weapon_id.cpp @@ -0,0 +1,22 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael Booth ([email protected]), 2003 +// Author: Matthew D. Campbell ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------- +// +// Temporary solution until we have time to build something more elegant +// Very nasty - need to keep in sync with the buy aliases +// NOTE: Array must be NULL terminated +// diff --git a/game/server/cstrike/bot/cs_gamestate.cpp b/game/server/cstrike/bot/cs_gamestate.cpp new file mode 100644 index 0000000..61f537b --- /dev/null +++ b/game/server/cstrike/bot/cs_gamestate.cpp @@ -0,0 +1,767 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Encapsulation of the current scenario/game state. Allows each bot imperfect knowledge. +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "KeyValues.h" + +#include "cs_bot.h" +#include "cs_gamestate.h" +#include "cs_simple_hostage.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +CSGameState::CSGameState( CCSBot *owner ) +{ + m_owner = owner; + m_isRoundOver = false; + + m_bombState = MOVING; + m_lastSawBomber.Invalidate(); + m_lastSawLooseBomb.Invalidate(); + m_isPlantedBombPosKnown = false; + m_plantedBombsite = UNKNOWN; + + m_bombsiteCount = 0; + m_bombsiteSearchIndex = 0; + + for( int i=0; i<MAX_HOSTAGES; ++i ) + { + m_hostage[i].hostage = NULL; + m_hostage[i].isValid = false; + m_hostage[i].isAlive = false; + m_hostage[i].isFree = true; + m_hostage[i].knownPos = Vector( 0, 0, 0 ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Reset at round start + */ +void CSGameState::Reset( void ) +{ + m_isRoundOver = false; + + // bomb ----------------------------------------------------------------------- + m_bombState = MOVING; + m_lastSawBomber.Invalidate(); + m_lastSawLooseBomb.Invalidate(); + m_isPlantedBombPosKnown = false; + m_plantedBombsite = UNKNOWN; + + m_bombsiteCount = TheCSBots()->GetZoneCount(); + + int i; + for( i=0; i<m_bombsiteCount; ++i ) + { + m_isBombsiteClear[i] = false; + m_bombsiteSearchOrder[i] = i; + } + + // shuffle the bombsite search order + // allows T's to plant at random site, and TEAM_CT's to search in a random order + // NOTE: VS6 std::random_shuffle() doesn't work well with an array of two elements (most maps) + for( i=0; i < m_bombsiteCount; ++i ) + { + int swap = m_bombsiteSearchOrder[i]; + int rnd = RandomInt( i, m_bombsiteCount-1 ); + m_bombsiteSearchOrder[i] = m_bombsiteSearchOrder[ rnd ]; + m_bombsiteSearchOrder[ rnd ] = swap; + } + + m_bombsiteSearchIndex = 0; + + // hostage --------------------------------------------------------------------- + InitializeHostageInfo(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnHostageRescuedAll( IGameEvent *event ) +{ + m_allHostagesRescued = true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnRoundEnd( IGameEvent *event ) +{ + m_isRoundOver = true; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnRoundStart( IGameEvent *event ) +{ + Reset(); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnBombPlanted( IGameEvent *event ) +{ + // change state - the event is announced to everyone + SetBombState( PLANTED ); + + CBasePlayer *plantingPlayer = UTIL_PlayerByUserId( event->GetInt( "userid" ) ); + + // Terrorists always know where the bomb is + if (m_owner->GetTeamNumber() == TEAM_TERRORIST && plantingPlayer) + { + UpdatePlantedBomb( plantingPlayer->GetAbsOrigin() ); + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnBombDefused( IGameEvent *event ) +{ + // change state - the event is announced to everyone + SetBombState( DEFUSED ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Update game state based on events we have received + */ +void CSGameState::OnBombExploded( IGameEvent *event ) +{ + // change state - the event is announced to everyone + SetBombState( EXPLODED ); +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * True if round has been won or lost (but not yet reset) + */ +bool CSGameState::IsRoundOver( void ) const +{ + return m_isRoundOver; +} + +//-------------------------------------------------------------------------------------------------------------- +void CSGameState::SetBombState( BombState state ) +{ + // if state changed, reset "last seen" timestamps + if (m_bombState != state) + { + m_bombState = state; + } +} + +//-------------------------------------------------------------------------------------------------------------- +void CSGameState::UpdateLooseBomb( const Vector &pos ) +{ + m_looseBombPos = pos; + m_lastSawLooseBomb.Reset(); + + // we saw the loose bomb, update our state + SetBombState( LOOSE ); +} + +//-------------------------------------------------------------------------------------------------------------- +float CSGameState::TimeSinceLastSawLooseBomb( void ) const +{ + return m_lastSawLooseBomb.GetElapsedTime(); +} + +//-------------------------------------------------------------------------------------------------------------- +bool CSGameState::IsLooseBombLocationKnown( void ) const +{ + if (m_bombState != LOOSE) + return false; + + return (m_lastSawLooseBomb.HasStarted()) ? true : false; +} + +//-------------------------------------------------------------------------------------------------------------- +void CSGameState::UpdateBomber( const Vector &pos ) +{ + m_bomberPos = pos; + m_lastSawBomber.Reset(); + + // we saw the bomber, update our state + SetBombState( MOVING ); +} + +//-------------------------------------------------------------------------------------------------------------- +float CSGameState::TimeSinceLastSawBomber( void ) const +{ + return m_lastSawBomber.GetElapsedTime(); +} + +//-------------------------------------------------------------------------------------------------------------- +bool CSGameState::IsPlantedBombLocationKnown( void ) const +{ + if (m_bombState != PLANTED) + return false; + + return m_isPlantedBombPosKnown; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the zone index of the planted bombsite, or UNKNOWN + */ +int CSGameState::GetPlantedBombsite( void ) const +{ + if (m_bombState != PLANTED) + return UNKNOWN; + + return m_plantedBombsite; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if we are currently in the bombsite where the bomb is planted + */ +bool CSGameState::IsAtPlantedBombsite( void ) const +{ + if (m_bombState != PLANTED) + return false; + + Vector myOrigin = GetCentroid( m_owner ); + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( myOrigin ); + + if (zone) + { + return (m_plantedBombsite == zone->m_index); + } + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the zone index of the next bombsite to search + */ +int CSGameState::GetNextBombsiteToSearch( void ) +{ + if (m_bombsiteCount <= 0) + return 0; + + int i; + + // return next non-cleared bombsite index + for( i=m_bombsiteSearchIndex; i<m_bombsiteCount; ++i ) + { + int z = m_bombsiteSearchOrder[i]; + if (!m_isBombsiteClear[z]) + { + m_bombsiteSearchIndex = i; + return z; + } + } + + // all the bombsites are clear, someone must have been mistaken - start search over + for( i=0; i<m_bombsiteCount; ++i ) + m_isBombsiteClear[i] = false; + m_bombsiteSearchIndex = 0; + + return GetNextBombsiteToSearch(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Returns position of bomb in its various states (moving, loose, planted), + * or NULL if we don't know where the bomb is + */ +const Vector *CSGameState::GetBombPosition( void ) const +{ + switch( m_bombState ) + { + case MOVING: + { + if (!m_lastSawBomber.HasStarted()) + return NULL; + + return &m_bomberPos; + } + + case LOOSE: + { + if (IsLooseBombLocationKnown()) + return &m_looseBombPos; + + return NULL; + } + + case PLANTED: + { + if (IsPlantedBombLocationKnown()) + return &m_plantedBombPos; + + return NULL; + } + } + + return NULL; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * We see the planted bomb at 'pos' + */ +void CSGameState::UpdatePlantedBomb( const Vector &pos ) +{ + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( pos ); + + if (zone == NULL) + { + CONSOLE_ECHO( "ERROR: Bomb planted outside of a zone!\n" ); + m_plantedBombsite = UNKNOWN; + } + else + { + m_plantedBombsite = zone->m_index; + } + + m_plantedBombPos = pos; + m_isPlantedBombPosKnown = true; + SetBombState( PLANTED ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Someone told us where the bomb is planted + */ +void CSGameState::MarkBombsiteAsPlanted( int zoneIndex ) +{ + m_plantedBombsite = zoneIndex; + SetBombState( PLANTED ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Someone told us a bombsite is clear + */ +void CSGameState::ClearBombsite( int zoneIndex ) +{ + if (zoneIndex >= 0 && zoneIndex < m_bombsiteCount) + m_isBombsiteClear[ zoneIndex ] = true; +} + +//-------------------------------------------------------------------------------------------------------------- +bool CSGameState::IsBombsiteClear( int zoneIndex ) const +{ + if (zoneIndex >= 0 && zoneIndex < m_bombsiteCount) + return m_isBombsiteClear[ zoneIndex ]; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Initialize our knowledge of the number and location of hostages + */ +void CSGameState::InitializeHostageInfo( void ) +{ + m_hostageCount = 0; + m_allHostagesRescued = false; + m_haveSomeHostagesBeenTaken = false; + + for( int i=0; i<g_Hostages.Count(); ++i ) + { + m_hostage[ m_hostageCount ].hostage = g_Hostages[i]; + m_hostage[ m_hostageCount ].knownPos = g_Hostages[i]->GetAbsOrigin(); + m_hostage[ m_hostageCount ].isValid = true; + m_hostage[ m_hostageCount ].isAlive = true; + m_hostage[ m_hostageCount ].isFree = true; + ++m_hostageCount; + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the closest free and live hostage + * If we are a CT this information is perfect. + * Otherwise, this is based on our individual memory of the game state. + * If NULL is returned, we don't think there are any hostages left, or we dont know where they are. + * NOTE: a T can remember a hostage who has died. knowPos will be filled in, but NULL will be + * returned, since CHostages get deleted when they die. + */ +CHostage *CSGameState::GetNearestFreeHostage( Vector *knowPos ) const +{ + if (m_owner == NULL) + return NULL; + + CNavArea *startArea = m_owner->GetLastKnownArea(); + if (startArea == NULL) + return NULL; + + CHostage *close = NULL; + Vector closePos( 0, 0, 0 ); + float closeDistance = 9999999999.9f; + + for( int i=0; i<m_hostageCount; ++i ) + { + CHostage *hostage = m_hostage[i].hostage; + Vector hostagePos; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // we know exactly where the hostages are, and if they are alive + if (!m_hostage[i].hostage || !m_hostage[i].hostage->IsValid()) + continue; + + if (m_hostage[i].hostage->IsFollowingSomeone()) + continue; + + hostagePos = m_hostage[i].hostage->GetAbsOrigin(); + } + else + { + // use our memory of where we think the hostages are + if (m_hostage[i].isValid == false) + continue; + + hostagePos = m_hostage[i].knownPos; + } + + CNavArea *hostageArea = TheNavMesh->GetNearestNavArea( hostagePos ); + if (hostageArea) + { + ShortestPathCost cost; + float travelDistance = NavAreaTravelDistance( startArea, hostageArea, cost ); + + if (travelDistance >= 0.0f && travelDistance < closeDistance) + { + closeDistance = travelDistance; + closePos = hostagePos; + close = hostage; + } + } + } + + // return where we think the hostage is + if (knowPos && close) + *knowPos = closePos; + + return close; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the location of a "free" hostage, or NULL if we dont know of any + */ +const Vector *CSGameState::GetRandomFreeHostagePosition( void ) const +{ + if (m_owner == NULL) + return NULL; + + static Vector freePos[ MAX_HOSTAGES ]; + int freeCount = 0; + + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // we know exactly where the hostages are, and if they are alive + if (!info->hostage || !info->hostage->IsAlive()) + continue; + + // escorted hostages are not "free" + if (info->hostage->IsFollowingSomeone()) + continue; + + freePos[ freeCount++ ] = info->hostage->GetAbsOrigin(); + } + else + { + // use our memory of where we think the hostages are + if (info->isValid == false) + continue; + + freePos[ freeCount++ ] = info->knownPos; + } + } + + if (freeCount) + { + return &freePos[ RandomInt( 0, freeCount-1 ) ]; + } + + return NULL; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * If we can see any of the positions where we think a hostage is, validate it + * Return status of any changes (a hostage died or was moved) + */ +unsigned char CSGameState::ValidateHostagePositions( void ) +{ + // limit how often we validate + if (!m_validateInterval.IsElapsed()) + return NO_CHANGE; + + const float validateInterval = 0.5f; + m_validateInterval.Start( validateInterval ); + + + // check the status of hostages + unsigned char status = NO_CHANGE; + + int i; + int startValidCount = 0; + for( i=0; i<m_hostageCount; ++i ) + if (m_hostage[i].isValid) + ++startValidCount; + + for( i=0; i<m_hostageCount; ++i ) + { + HostageInfo *info = &m_hostage[i]; + + if (!info->hostage ) + continue; + + // if we can see a hostage, update our knowledge of it + Vector pos = info->hostage->GetAbsOrigin() + Vector( 0, 0, HalfHumanHeight ); + if (m_owner->IsVisible( pos, CHECK_FOV )) + { + if (info->hostage->IsAlive()) + { + // live hostage + + // if hostage is being escorted by a CT, we don't "see" it, we see the CT + if (info->hostage->IsFollowingSomeone()) + { + info->isValid = false; + } + else + { + info->knownPos = info->hostage->GetAbsOrigin(); + info->isValid = true; + } + } + else + { + // dead hostage + + // if we thought it was alive, this is news to us + if (info->isAlive) + status |= HOSTAGE_DIED; + + info->isAlive = false; + info->isValid = false; + } + + continue; + } + + // if we dont know where this hostage is, nothing to validate + if (!info->isValid) + continue; + + // can't directly see this hostage + // check line of sight to where we think this hostage is, to see if we noticed that is has moved + pos = info->knownPos + Vector( 0, 0, HalfHumanHeight ); + if (m_owner->IsVisible( pos, CHECK_FOV )) + { + // we can see where we thought the hostage was - verify it is still there and alive + + if (!info->hostage->IsValid()) + { + // since we have line of sight to an invalid hostage, it must be dead + // discovered that hostage has been killed + status |= HOSTAGE_DIED; + info->isAlive = false; + info->isValid = false; + continue; + } + + if (info->hostage->IsFollowingSomeone()) + { + // discovered the hostage has been taken + status |= HOSTAGE_GONE; + info->isValid = false; + continue; + } + + const float tolerance = 50.0f; + if ((info->hostage->GetAbsOrigin() - info->knownPos).IsLengthGreaterThan( tolerance )) + { + // discovered that hostage has been moved + status |= HOSTAGE_GONE; + info->isValid = false; + continue; + } + } + } + + int endValidCount = 0; + for( i=0; i<m_hostageCount; ++i ) + if (m_hostage[i].isValid) + ++endValidCount; + + if (endValidCount == 0 && startValidCount > 0) + { + // we discovered all the hostages are gone + status &= ~HOSTAGE_GONE; + status |= HOSTAGES_ALL_GONE; + } + + return status; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return the nearest visible free hostage + * Since we can actually see any hostage we return, we know its actual position + */ +CHostage *CSGameState::GetNearestVisibleFreeHostage( void ) const +{ + CHostage *close = NULL; + float closeRangeSq = 999999999.9f; + float rangeSq; + + Vector pos; + Vector myOrigin = GetCentroid( m_owner ); + + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if ( !info->hostage ) + continue; + + // if the hostage is dead or rescued, its not free + if (!info->hostage->IsAlive()) + continue; + + // if this hostage is following someone, its not free + if (info->hostage->IsFollowingSomeone()) + continue; + + /// @todo Use travel distance here + pos = info->hostage->GetAbsOrigin(); + rangeSq = (pos - myOrigin).LengthSqr(); + + if (rangeSq < closeRangeSq) + { + if (!m_owner->IsVisible( pos )) + continue; + + close = info->hostage; + closeRangeSq = rangeSq; + } + } + + return close; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Return true if there are no free hostages + */ +bool CSGameState::AreAllHostagesBeingRescued( void ) const +{ + // if the hostages have all been rescued, they are not being rescued any longer + if (m_allHostagesRescued) + return false; + + bool isAllDead = true; + + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // CT's have perfect knowledge via their radar + if (info->hostage && info->hostage->IsValid()) + { + if (!info->hostage->IsFollowingSomeone()) + return false; + + isAllDead = false; + } + } + else + { + if (info->isValid && info->isAlive) + return false; + + if (info->isAlive) + isAllDead = false; + } + } + + // if all of the remaining hostages are dead, they arent being rescued + if (isAllDead) + return false; + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * All hostages have been rescued or are dead + */ +bool CSGameState::AreAllHostagesGone( void ) const +{ + if (m_allHostagesRescued) + return true; + + // do we know that all the hostages are dead + for( int i=0; i<m_hostageCount; ++i ) + { + const HostageInfo *info = &m_hostage[i]; + + if (m_owner->GetTeamNumber() == TEAM_CT) + { + // CT's have perfect knowledge via their radar + if (info->hostage && info->hostage->IsAlive()) + return false; + } + else + { + if (info->isValid && info->isAlive) + return false; + } + } + + return true; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Someone told us all the hostages are gone + */ +void CSGameState::AllHostagesGone( void ) +{ + for( int i=0; i<m_hostageCount; ++i ) + m_hostage[i].isValid = false; +} + diff --git a/game/server/cstrike/bot/cs_gamestate.h b/game/server/cstrike/bot/cs_gamestate.h new file mode 100644 index 0000000..79bb8d2 --- /dev/null +++ b/game/server/cstrike/bot/cs_gamestate.h @@ -0,0 +1,151 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#ifndef _GAME_STATE_H_ +#define _GAME_STATE_H_ + + +#include "bot_util.h" + + +class CHostage; +class CCSBot; + +/** + * This class represents the game state as known by a particular bot + */ +class CSGameState +{ +public: + CSGameState( CCSBot *owner ); + + void Reset( void ); + + // Event handling + void OnHostageRescuedAll( IGameEvent *event ); + void OnRoundEnd( IGameEvent *event ); + void OnRoundStart( IGameEvent *event ); + void OnBombPlanted( IGameEvent *event ); + void OnBombDefused( IGameEvent *event ); + void OnBombExploded( IGameEvent *event ); + + bool IsRoundOver( void ) const; ///< true if round has been won or lost (but not yet reset) + + // bomb defuse scenario ----------------------------------------------------------------------------- + + enum BombState + { + MOVING, ///< being carried by a Terrorist + LOOSE, ///< loose on the ground somewhere + PLANTED, ///< planted and ticking + DEFUSED, ///< the bomb has been defused + EXPLODED ///< the bomb has exploded + }; + + bool IsBombMoving( void ) const { return (m_bombState == MOVING); } + bool IsBombLoose( void ) const { return (m_bombState == LOOSE); } + bool IsBombPlanted( void ) const { return (m_bombState == PLANTED); } + bool IsBombDefused( void ) const { return (m_bombState == DEFUSED); } + bool IsBombExploded( void ) const { return (m_bombState == EXPLODED); } + + void UpdateLooseBomb( const Vector &pos ); ///< we see the loose bomb + float TimeSinceLastSawLooseBomb( void ) const; ///< how long has is been since we saw the loose bomb + bool IsLooseBombLocationKnown( void ) const; ///< do we know where the loose bomb is + + void UpdateBomber( const Vector &pos ); ///< we see the bomber + float TimeSinceLastSawBomber( void ) const; ///< how long has is been since we saw the bomber + + void UpdatePlantedBomb( const Vector &pos ); ///< we see the planted bomb + bool IsPlantedBombLocationKnown( void ) const; ///< do we know where the bomb was planted + void MarkBombsiteAsPlanted( int zoneIndex ); ///< mark bombsite as the location of the planted bomb + + enum { UNKNOWN = -1 }; + int GetPlantedBombsite( void ) const; ///< return the zone index of the planted bombsite, or UNKNOWN + bool IsAtPlantedBombsite( void ) const; ///< return true if we are currently in the bombsite where the bomb is planted + + int GetNextBombsiteToSearch( void ); ///< return the zone index of the next bombsite to search + bool IsBombsiteClear( int zoneIndex ) const; ///< return true if given bombsite has been cleared + void ClearBombsite( int zoneIndex ); ///< mark bombsite as clear + + const Vector *GetBombPosition( void ) const; ///< return where we think the bomb is, or NULL if we don't know + + // hostage rescue scenario ------------------------------------------------------------------------ + CHostage *GetNearestFreeHostage( Vector *knowPos = NULL ) const; ///< return the closest free hostage, and where we think it is (knowPos) + const Vector *GetRandomFreeHostagePosition( void ) const; + bool AreAllHostagesBeingRescued( void ) const; ///< return true if there are no free hostages + bool AreAllHostagesGone( void ) const; ///< all hostages have been rescued or are dead + void AllHostagesGone( void ); ///< someone told us all the hostages are gone + bool HaveSomeHostagesBeenTaken( void ) const ///< return true if one or more hostages have been moved by the CT's + { + return m_haveSomeHostagesBeenTaken; + } + void HostageWasTaken( void ) ///< someone told us a CT is talking to a hostage + { + m_haveSomeHostagesBeenTaken = true; + } + + CHostage *GetNearestVisibleFreeHostage( void ) const; + + enum ValidateStatusType + { + NO_CHANGE = 0x00, + HOSTAGE_DIED = 0x01, + HOSTAGE_GONE = 0x02, + HOSTAGES_ALL_GONE = 0x04 + }; + unsigned char ValidateHostagePositions( void ); ///< update our knowledge with what we currently see - returns bitflag events + +private: + CCSBot *m_owner; ///< who owns this gamestate + + bool m_isRoundOver; ///< true if round is over, but no yet reset + + // bomb defuse scenario --------------------------------------------------------------------------- + void SetBombState( BombState state ); + BombState GetBombState( void ) const { return m_bombState; } + + BombState m_bombState; ///< what we think the bomb is doing + + IntervalTimer m_lastSawBomber; + Vector m_bomberPos; + + IntervalTimer m_lastSawLooseBomb; + Vector m_looseBombPos; + + bool m_isBombsiteClear[ CCSBotManager::MAX_ZONES ]; ///< corresponds to zone indices in CCSBotManager + int m_bombsiteSearchOrder[ CCSBotManager::MAX_ZONES ]; ///< randomized order of bombsites to search + int m_bombsiteCount; + int m_bombsiteSearchIndex; ///< the next step in the search + + int m_plantedBombsite; ///< zone index of the bombsite where the planted bomb is + + bool m_isPlantedBombPosKnown; ///< if true, we know the exact location of the bomb + Vector m_plantedBombPos; + + // hostage rescue scenario ------------------------------------------------------------------------ + struct HostageInfo + { + CHandle<CHostage> hostage; + Vector knownPos; + bool isValid; + bool isAlive; + bool isFree; ///< not being escorted by a CT + } + m_hostage[ MAX_HOSTAGES ]; + int m_hostageCount; ///< number of hostages left in map + CountdownTimer m_validateInterval; + + CBaseEntity *GetNearestHostage( void ) const; ///< return the closest live hostage + void InitializeHostageInfo( void ); ///< initialize our knowledge of the number and location of hostages + + bool m_allHostagesRescued; + bool m_haveSomeHostagesBeenTaken; ///< true if a hostage has been moved by a CT (and we've seen it) +}; + +#endif // _GAME_STATE_ diff --git a/game/server/cstrike/bot/states/cs_bot_attack.cpp b/game/server/cstrike/bot/states/cs_bot_attack.cpp new file mode 100644 index 0000000..1f44b6c --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_attack.cpp @@ -0,0 +1,710 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin attacking + */ +void AttackState::OnEnter( CCSBot *me ) +{ + CBasePlayer *enemy = me->GetBotEnemy(); + + // store our posture when the attack began + me->PushPostureContext(); + + me->DestroyPath(); + + // if we are using a knife, try to sneak up on the enemy + if (enemy && me->IsUsingKnife() && !me->IsPlayerFacingMe( enemy )) + me->Walk(); + else + me->Run(); + + me->GetOffLadder(); + me->ResetStuckMonitor(); + + m_repathTimer.Invalidate(); + m_haveSeenEnemy = me->IsEnemyVisible(); + m_nextDodgeStateTimestamp = 0.0f; + m_firstDodge = true; + m_isEnemyHidden = false; + m_reacquireTimestamp = 0.0f; + + m_pinnedDownTimestamp = gpGlobals->curtime + RandomFloat( 7.0f, 10.0f ); + + m_shieldToggleTimestamp = gpGlobals->curtime + RandomFloat( 2.0f, 10.0f ); + m_shieldForceOpen = false; + + // if we encountered someone while escaping, grab our weapon and fight! + if (me->IsEscapingFromBomb()) + me->EquipBestWeapon(); + + if (me->IsUsingKnife()) + { + // can't crouch and hold with a knife + m_crouchAndHold = false; + me->StandUp(); + } + else if (me->CanSeeSniper() && !me->IsSniper()) + { + // don't sit still if we see a sniper! + m_crouchAndHold = false; + me->StandUp(); + } + else + { + // decide whether to crouch where we are, or run and gun (if we havent already - see CCSBot::Attack()) + if (!m_crouchAndHold) + { + if (enemy) + { + const float crouchFarRange = 750.0f; + float crouchChance; + + // more likely to crouch if using sniper rifle or if enemy is far away + if (me->IsUsingSniperRifle()) + crouchChance = 50.0f; + else if ((GetCentroid( me ) - GetCentroid( enemy )).IsLengthGreaterThan( crouchFarRange )) + crouchChance = 50.0f; + else + crouchChance = 20.0f * (1.0f - me->GetProfile()->GetAggression()); + + if (RandomFloat( 0.0f, 100.0f ) < crouchChance) + { + // make sure we can still see if we crouch + trace_t result; + + Vector origin = GetCentroid( me ); + if (!me->IsCrouching()) + { + // we are standing, adjust for lower crouch origin + origin.z -= 20.0f; + } + + UTIL_TraceLine( origin, enemy->EyePosition(), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &result ); + + if (result.fraction == 1.0f) + { + m_crouchAndHold = true; + } + } + } + } + + if (m_crouchAndHold) + { + me->Crouch(); + me->PrintIfWatched( "Crouch and hold attack!\n" ); + } + } + + m_scopeTimestamp = 0; + m_didAmbushCheck = false; + + float skill = me->GetProfile()->GetSkill(); + + // tendency to dodge is proportional to skill + float dodgeChance = 80.0f * skill; + + // high skill bots always dodge if outnumbered, or they see a sniper + if (skill > 0.5f && (me->IsOutnumbered() || me->CanSeeSniper())) + { + dodgeChance = 100.0f; + } + + m_shouldDodge = (RandomFloat( 0, 100 ) <= dodgeChance); + + + // decide whether we might bail out of this fight + m_isCoward = (RandomFloat( 0, 100 ) > 100.0f * me->GetProfile()->GetAggression()); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * When we are done attacking, this is invoked + */ +void AttackState::StopAttacking( CCSBot *me ) +{ + if (me->GetTask() == CCSBot::SNIPING) + { + // stay in our hiding spot + me->Hide( me->GetLastKnownArea(), -1.0f, 50.0f ); + } + else + { + me->StopAttacking(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Do dodge behavior + */ +void AttackState::Dodge( CCSBot *me ) +{ + // + // Dodge. + // If sniping or crouching, stand still. + // + if (m_shouldDodge && !me->IsUsingSniperRifle() && !m_crouchAndHold) + { + CBasePlayer *enemy = me->GetBotEnemy(); + if (enemy == NULL) + { + return; + } + + Vector toEnemy = enemy->GetAbsOrigin() - me->GetAbsOrigin(); + float range = toEnemy.Length(); + + const float hysterisRange = 125.0f; // (+/-) m_combatRange + + float minRange = me->GetCombatRange() - hysterisRange; + float maxRange = me->GetCombatRange() + hysterisRange; + + if (me->IsUsingKnife()) + { + // dodge when far away if armed only with a knife + maxRange = 999999.9f; + } + + // move towards (or away from) enemy if we are using a knife, behind a corner, or we aren't very skilled + if (me->GetProfile()->GetSkill() < 0.66f || !me->IsEnemyVisible()) + { + if (range > maxRange) + me->MoveForward(); + else if (range < minRange) + me->MoveBackward(); + } + + // don't dodge if enemy is facing away + const float dodgeRange = 2000.0f; + if (!me->CanSeeSniper() && (range > dodgeRange || !me->IsPlayerFacingMe( enemy ))) + { + m_dodgeState = STEADY_ON; + m_nextDodgeStateTimestamp = 0.0f; + } + else if (gpGlobals->curtime >= m_nextDodgeStateTimestamp) + { + int next; + + // high-skill bots keep moving and don't jump if they see a sniper + if (me->GetProfile()->GetSkill() > 0.5f && me->CanSeeSniper()) + { + // juke back and forth + if (m_firstDodge) + { + next = (RandomInt( 0, 100 ) < 50) ? SLIDE_RIGHT : SLIDE_LEFT; + } + else + { + next = (m_dodgeState == SLIDE_LEFT) ? SLIDE_RIGHT : SLIDE_LEFT; + } + } + else + { + // select next dodge state that is different that our current one + do + { + // low-skill bots may jump when first engaging the enemy (if they are moving) + const float jumpChance = 33.3f; + if (m_firstDodge && me->GetProfile()->GetSkill() < 0.5f && RandomFloat( 0, 100 ) < jumpChance && !me->IsNotMoving()) + next = RandomInt( 0, NUM_ATTACK_STATES-1 ); + else + next = RandomInt( 0, NUM_ATTACK_STATES-2 ); + } + while( !m_firstDodge && next == m_dodgeState ); + } + + m_dodgeState = (DodgeStateType)next; + m_nextDodgeStateTimestamp = gpGlobals->curtime + RandomFloat( 0.3f, 1.0f ); + m_firstDodge = false; + } + + + Vector forward, right; + me->EyeVectors( &forward, &right ); + + const float lookAheadRange = 30.0f; + float ground; + + switch( m_dodgeState ) + { + case STEADY_ON: + { + break; + } + + case SLIDE_LEFT: + { + // don't move left if we will fall + Vector pos = me->GetAbsOrigin() - (lookAheadRange * right); + + if (me->GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (me->GetAbsOrigin().z - ground < StepHeight) + { + me->StrafeLeft(); + } + } + break; + } + + case SLIDE_RIGHT: + { + // don't move left if we will fall + Vector pos = me->GetAbsOrigin() + (lookAheadRange * right); + + if (me->GetSimpleGroundHeightWithFloor( pos, &ground )) + { + if (me->GetAbsOrigin().z - ground < StepHeight) + { + me->StrafeRight(); + } + } + break; + } + + case JUMP: + { + if (me->m_isEnemyVisible) + { + me->Jump(); + } + break; + } + } + } +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Perform attack behavior + */ +void AttackState::OnUpdate( CCSBot *me ) +{ + // can't be stuck while attacking + me->ResetStuckMonitor(); + + // if we somehow ended up with the C4 or a grenade in our hands, grab our weapon! + CWeaponCSBase *weapon = me->GetActiveCSWeapon(); + if (weapon) + { + if (weapon->GetWeaponID() == WEAPON_C4 || + weapon->GetWeaponID() == WEAPON_HEGRENADE || + weapon->GetWeaponID() == WEAPON_FLASHBANG || + weapon->GetWeaponID() == WEAPON_SMOKEGRENADE) + { + me->EquipBestWeapon(); + } + } + + CBasePlayer *enemy = me->GetBotEnemy(); + if (enemy == NULL) + { + StopAttacking( me ); + return; + } + + Vector myOrigin = GetCentroid( me ); + Vector enemyOrigin = GetCentroid( enemy ); + + // keep track of whether we have seen our enemy at least once yet + if (!m_haveSeenEnemy) + m_haveSeenEnemy = me->IsEnemyVisible(); + + + // + // Retreat check + // Do not retreat if the enemy is too close + // + if (m_retreatTimer.IsElapsed()) + { + // If we've been fighting this battle for awhile, we're "pinned down" and + // need to do something else. + // If we are outnumbered, retreat. + // If we see a sniper and we aren't a sniper, retreat. + + bool isPinnedDown = (gpGlobals->curtime > m_pinnedDownTimestamp); + + if (isPinnedDown || + (me->CanSeeSniper() && !me->IsSniper()) || + (me->IsOutnumbered() && m_isCoward) || + (me->OutnumberedCount() >= 2 && me->GetProfile()->GetAggression() < 1.0f)) + { + // only retreat if at least one of them is aiming at me + if (me->IsAnyVisibleEnemyLookingAtMe( CHECK_FOV )) + { + // tell our teammates our plight + if (isPinnedDown) + me->GetChatter()->PinnedDown(); + else if (!me->CanSeeSniper()) + me->GetChatter()->Scared(); + + m_retreatTimer.Start( RandomFloat( 3.0f, 15.0f ) ); + + // try to retreat + if (me->TryToRetreat()) + { + // if we are a sniper, equip our pistol so we can fire while retreating + /* + if (me->IsUsingSniperRifle()) + { + // wait a moment to allow one last shot + me->Wait( 0.5f ); + //me->EquipPistol(); + } + */ + + // request backup if outnumbered + if (me->IsOutnumbered()) + { + me->GetChatter()->NeedBackup(); + } + } + else + { + me->PrintIfWatched( "I want to retreat, but no safe spots nearby!\n" ); + } + } + } + } + + // + // Knife fighting + // We need to pathfind right to the enemy to cut him + // + if (me->IsUsingKnife()) + { + // can't crouch and hold with a knife + m_crouchAndHold = false; + me->StandUp(); + + // if we are using a knife and our prey is looking towards us, run at him + if (me->IsPlayerFacingMe( enemy )) + { + me->ForceRun( 5.0f ); + me->Hurry( 10.0f ); + } + + // slash our victim + me->FireWeaponAtEnemy(); + + // if toe to toe with our enemy, don't dodge, just slash + const float slashRange = 70.0f; + if ((enemy->GetAbsOrigin() - me->GetAbsOrigin()).IsLengthGreaterThan( slashRange )) + { + const float repathInterval = 0.5f; + + // if our victim has moved, repath + bool repath = false; + if (me->HasPath()) + { + const float repathRange = 100.0f; // 50 + if ((me->GetPathEndpoint() - enemy->GetAbsOrigin()).IsLengthGreaterThan( repathRange )) + { + repath = true; + } + } + else + { + repath = true; + } + + if (repath && m_repathTimer.IsElapsed()) + { + Vector enemyPos = enemy->GetAbsOrigin() + Vector( 0, 0, HalfHumanHeight ); + me->ComputePath( enemyPos, FASTEST_ROUTE ); + m_repathTimer.Start( repathInterval ); + } + + // move towards victim + if (me->UpdatePathMovement( NO_SPEED_CHANGE ) != CCSBot::PROGRESSING) + { + me->DestroyPath(); + } + } + + return; + } + + + + // + // Simple shield usage + // + if (me->HasShield()) + { + if (me->IsEnemyVisible() && !m_shieldForceOpen) + { + if (!me->IsRecognizedEnemyReloading() && !me->IsReloading() && me->IsPlayerLookingAtMe( enemy )) + { + // close up - enemy is pointing his gun at us + if (!me->IsProtectedByShield()) + me->SecondaryAttack(); + } + else + { + // enemy looking away or reloading his weapon - open up and shoot him + if (me->IsProtectedByShield()) + me->SecondaryAttack(); + } + } + else + { + // can't see enemy, open up + if (me->IsProtectedByShield()) + me->SecondaryAttack(); + } + + if (gpGlobals->curtime > m_shieldToggleTimestamp) + { + m_shieldToggleTimestamp = gpGlobals->curtime + RandomFloat( 0.5, 2.0f ); + + // toggle shield force open + m_shieldForceOpen = !m_shieldForceOpen; + } + } + + + // check if our weapon range is bad and we should switch to pistol + if (me->IsUsingSniperRifle()) + { + // if we have a sniper rifle and our enemy is too close, switch to pistol + const float sniperMinRange = 160.0f; // NOTE: Must be larger than NO_ZOOM range in AdjustZoom() + if ((enemyOrigin - myOrigin).IsLengthLessThan( sniperMinRange )) + me->EquipPistol(); + } + else if (me->IsUsingShotgun()) + { + // if we have a shotgun equipped and enemy is too far away, switch to pistol + const float shotgunMaxRange = 600.0f; + if ((enemyOrigin - myOrigin).IsLengthGreaterThan( shotgunMaxRange )) + me->EquipPistol(); + } + + // if we're sniping, look through the scope - need to do this here in case a reload resets our scope + if (me->IsUsingSniperRifle()) + { + // for Scouts and AWPs, we need to wait for zoom to resume + if (me->m_bResumeZoom) + { + m_scopeTimestamp = gpGlobals->curtime; + return; + } + + Vector toAimSpot3D = me->m_aimSpot - myOrigin; + float targetRange = toAimSpot3D.Length(); + + // dont adjust zoom level if we're already zoomed in - just fire + if (me->GetZoomLevel() == CCSBot::NO_ZOOM && me->AdjustZoom( targetRange )) + m_scopeTimestamp = gpGlobals->curtime; + + const float waitScopeTime = 0.3f + me->GetProfile()->GetReactionTime(); + if (gpGlobals->curtime - m_scopeTimestamp < waitScopeTime) + { + // force us to wait until zoomed in before firing + return; + } + } + + // see if we "notice" that our prey is dead + if (me->IsAwareOfEnemyDeath()) + { + // let team know if we killed the last enemy + if (me->GetLastVictimID() == enemy->entindex() && me->GetNearbyEnemyCount() <= 1) + { + me->GetChatter()->KilledMyEnemy( enemy->entindex() ); + + // if there are other enemies left, wait a moment - they usually come in groups + if (me->GetEnemiesRemaining()) + { + me->Wait( RandomFloat( 1.0f, 3.0f ) ); + } + } + + StopAttacking( me ); + return; + } + + float notSeenEnemyTime = gpGlobals->curtime - me->GetLastSawEnemyTimestamp(); + + // if we haven't seen our enemy for a moment, continue on if we dont want to fight, or decide to ambush if we do + if (!me->IsEnemyVisible()) + { + // attend to nearby enemy gunfire + if (notSeenEnemyTime > 0.5f && me->CanHearNearbyEnemyGunfire()) + { + // give up the attack, since we didn't want it in the first place + StopAttacking( me ); + + const Vector *pos = me->GetNoisePosition(); + if (pos) + { + me->SetLookAt( "Nearby enemy gunfire", *pos, PRIORITY_HIGH, 0.0f ); + me->PrintIfWatched( "Checking nearby threatening enemy gunfire!\n" ); + return; + } + } + + // check if we have lost track of our enemy during combat + if (notSeenEnemyTime > 0.25f) + { + m_isEnemyHidden = true; + } + + + if (notSeenEnemyTime > 0.1f) + { + if (me->GetDisposition() == CCSBot::ENGAGE_AND_INVESTIGATE) + { + // decide whether we should hide and "ambush" our enemy + if (m_haveSeenEnemy && !m_didAmbushCheck) + { + float hideChance = 33.3f; + + if (RandomFloat( 0.0, 100.0f ) < hideChance) + { + float ambushTime = RandomFloat( 3.0f, 15.0f ); + + // hide in ambush nearby + /// @todo look towards where we know enemy is + const Vector *spot = FindNearbyRetreatSpot( me, 200.0f ); + if (spot) + { + me->IgnoreEnemies( 1.0f ); + + me->Run(); + me->StandUp(); + me->Hide( *spot, ambushTime, true ); + return; + } + } + + // don't check again + m_didAmbushCheck = true; + } + } + else + { + // give up the attack, since we didn't want it in the first place + StopAttacking( me ); + return; + } + } + } + else + { + // we can see the enemy again - reset our ambush check + m_didAmbushCheck = false; + + // if the enemy is coming out of hiding, we need time to react + if (m_isEnemyHidden) + { + m_reacquireTimestamp = gpGlobals->curtime + me->GetProfile()->GetReactionTime(); + m_isEnemyHidden = false; + } + } + + + // if we haven't seen our enemy for a long time, chase after them + float chaseTime = 2.0f + 2.0f * (1.0f - me->GetProfile()->GetAggression()); + + // if we are sniping, be very patient + if (me->IsUsingSniperRifle()) + chaseTime += 3.0f; + else if (me->IsCrouching()) // if we are crouching, be a little patient + chaseTime += 1.0f; + + // if we can't see the enemy, and have either seen him but currently lost sight of him, + // or haven't yet seen him, chase after him (unless we are a sniper) + if (!me->IsEnemyVisible() && (notSeenEnemyTime > chaseTime || !m_haveSeenEnemy)) + { + // snipers don't chase their prey - they wait for their prey to come to them + if (me->GetTask() == CCSBot::SNIPING) + { + StopAttacking( me ); + return; + } + else + { + // move to last known position of enemy + me->SetTask( CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION, enemy ); + me->MoveTo( me->GetLastKnownEnemyPosition() ); + return; + } + } + + + // if we can't see our enemy at the moment, and were shot by + // a different visible enemy, engage them instead + const float hurtRecentlyTime = 3.0f; + if (!me->IsEnemyVisible() && + me->GetTimeSinceAttacked() < hurtRecentlyTime && + me->GetAttacker() && + me->GetAttacker() != me->GetBotEnemy()) + { + // if we can see them, attack, otherwise panic + if (me->IsVisible( me->GetAttacker(), CHECK_FOV )) + { + me->Attack( me->GetAttacker() ); + me->PrintIfWatched( "Switching targets to retaliate against new attacker!\n" ); + } + /* + * Rethink this + else + { + me->Panic( me->GetAttacker() ); + me->PrintIfWatched( "Panicking from crossfire while attacking!\n" ); + } + */ + + return; + } + + if (true || gpGlobals->curtime > m_reacquireTimestamp) + me->FireWeaponAtEnemy(); + + + // do dodge behavior + Dodge( me ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Finish attack + */ +void AttackState::OnExit( CCSBot *me ) +{ + me->PrintIfWatched( "AttackState:OnExit()\n" ); + + m_crouchAndHold = false; + + // clear any noises we heard during battle + me->ForgetNoise(); + me->ResetStuckMonitor(); + + // resume our original posture + me->PopPostureContext(); + + // put shield away + if (me->IsProtectedByShield()) + me->SecondaryAttack(); + + + //me->StopAiming(); +} + diff --git a/game/server/cstrike/bot/states/cs_bot_buy.cpp b/game/server/cstrike/bot/states/cs_bot_buy.cpp new file mode 100644 index 0000000..de06e8b --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_buy.cpp @@ -0,0 +1,690 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_gamerules.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +ConVar bot_loadout( "bot_loadout", "", FCVAR_CHEAT, "bots are given these items at round start" ); +ConVar bot_randombuy( "bot_randombuy", "0", FCVAR_CHEAT, "should bots ignore their prefered weapons and just buy weapons at random?" ); + +//-------------------------------------------------------------------------------------------------------------- +/** + * Debug command to give a named weapon + */ +void CCSBot::GiveWeapon( const char *weaponAlias ) +{ + const char *translatedAlias = GetTranslatedWeaponAlias( weaponAlias ); + + char wpnName[128]; + Q_snprintf( wpnName, sizeof( wpnName ), "weapon_%s", translatedAlias ); + WEAPON_FILE_INFO_HANDLE hWpnInfo = LookupWeaponInfoSlot( wpnName ); + if ( hWpnInfo == GetInvalidWeaponInfoHandle() ) + { + return; + } + + CCSWeaponInfo *pWeaponInfo = dynamic_cast< CCSWeaponInfo* >( GetFileWeaponInfoFromHandle( hWpnInfo ) ); + if ( !pWeaponInfo ) + { + return; + } + + if ( !Weapon_OwnsThisType( wpnName ) ) + { + CBaseCombatWeapon *pWeapon = Weapon_GetSlot( pWeaponInfo->iSlot ); + if ( pWeapon ) + { + if ( pWeaponInfo->iSlot == WEAPON_SLOT_PISTOL ) + { + DropPistol(); + } + else if ( pWeaponInfo->iSlot == WEAPON_SLOT_RIFLE ) + { + DropRifle(); + } + } + } + + GiveNamedItem( wpnName ); +} + +//-------------------------------------------------------------------------------------------------------------- +static bool HasDefaultPistol( CCSBot *me ) +{ + CWeaponCSBase *pistol = (CWeaponCSBase *)me->Weapon_GetSlot( WEAPON_SLOT_PISTOL ); + + if (pistol == NULL) + return false; + + if (me->GetTeamNumber() == TEAM_TERRORIST && pistol->IsA( WEAPON_GLOCK )) + return true; + + if (me->GetTeamNumber() == TEAM_CT && pistol->IsA( WEAPON_USP )) + return true; + + return false; +} + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Buy weapons, armor, etc. + */ +void BuyState::OnEnter( CCSBot *me ) +{ + m_retries = 0; + m_prefRetries = 0; + m_prefIndex = 0; + + const char *cheatWeaponString = bot_loadout.GetString(); + if ( cheatWeaponString && *cheatWeaponString ) + { + m_doneBuying = false; // we're going to be given weapons - ignore the eco limit + } + else + { + // check if we are saving money for the next round + if (me->m_iAccount < cv_bot_eco_limit.GetFloat()) + { + me->PrintIfWatched( "Saving money for next round.\n" ); + m_doneBuying = true; + } + else + { + m_doneBuying = false; + } + } + + m_isInitialDelay = true; + + // this will force us to stop holding live grenade + me->EquipBestWeapon( MUST_EQUIP ); + + m_buyDefuseKit = false; + m_buyShield = false; + + if (me->GetTeamNumber() == TEAM_CT) + { + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB) + { + // CT's sometimes buy defuse kits in the bomb scenario (except in career mode, where the player should defuse) + if (CSGameRules()->IsCareer() == false) + { + const float buyDefuseKitChance = 100.0f * (me->GetProfile()->GetSkill() + 0.2f); + if (RandomFloat( 0.0f, 100.0f ) < buyDefuseKitChance) + { + m_buyDefuseKit = true; + } + } + } + + // determine if we want a tactical shield + if (!me->HasPrimaryWeapon() && TheCSBots()->AllowTacticalShield()) + { + if (me->m_iAccount > 2500) + { + if (me->m_iAccount < 4000) + m_buyShield = (RandomFloat( 0, 100.0f ) < 33.3f) ? true : false; + else + m_buyShield = (RandomFloat( 0, 100.0f ) < 10.0f) ? true : false; + } + } + } + + if (TheCSBots()->AllowGrenades()) + { + m_buyGrenade = (RandomFloat( 0.0f, 100.0f ) < 33.3f) ? true : false; + } + else + { + m_buyGrenade = false; + } + + + m_buyPistol = false; + if (TheCSBots()->AllowPistols()) + { + // check if we have a pistol + if (me->Weapon_GetSlot( WEAPON_SLOT_PISTOL )) + { + // if we have our default pistol, think about buying a different one + if (HasDefaultPistol( me )) + { + // if everything other than pistols is disallowed, buy a pistol + if (TheCSBots()->AllowShotguns() == false && + TheCSBots()->AllowSubMachineGuns() == false && + TheCSBots()->AllowRifles() == false && + TheCSBots()->AllowMachineGuns() == false && + TheCSBots()->AllowTacticalShield() == false && + TheCSBots()->AllowSnipers() == false) + { + m_buyPistol = (RandomFloat( 0, 100 ) < 75.0f); + } + else if (me->m_iAccount < 1000) + { + // if we're low on cash, buy a pistol + m_buyPistol = (RandomFloat( 0, 100 ) < 75.0f); + } + else + { + m_buyPistol = (RandomFloat( 0, 100 ) < 33.3f); + } + } + } + else + { + // we dont have a pistol - buy one + m_buyPistol = true; + } + } +} + + +enum WeaponType +{ + PISTOL, + SHOTGUN, + SUB_MACHINE_GUN, + RIFLE, + MACHINE_GUN, + SNIPER_RIFLE, + GRENADE, + + NUM_WEAPON_TYPES +}; + +struct BuyInfo +{ + WeaponType type; + bool preferred; ///< more challenging bots prefer these weapons + const char *buyAlias; ///< the buy alias for this equipment +}; + +#define PRIMARY_WEAPON_BUY_COUNT 13 +#define SECONDARY_WEAPON_BUY_COUNT 3 + +/** + * These tables MUST be kept in sync with the CT and T buy aliases + */ + +static BuyInfo primaryWeaponBuyInfoCT[ PRIMARY_WEAPON_BUY_COUNT ] = +{ + { SHOTGUN, false, "m3" }, // WEAPON_M3 + { SHOTGUN, false, "xm1014" }, // WEAPON_XM1014 + { SUB_MACHINE_GUN, false, "tmp" }, // WEAPON_TMP + { SUB_MACHINE_GUN, false, "mp5navy" }, // WEAPON_MP5N + { SUB_MACHINE_GUN, false, "ump45" }, // WEAPON_UMP45 + { SUB_MACHINE_GUN, false, "p90" }, // WEAPON_P90 + { RIFLE, true, "famas" }, // WEAPON_FAMAS + { SNIPER_RIFLE, false, "scout" }, // WEAPON_SCOUT + { RIFLE, true, "m4a1" }, // WEAPON_M4A1 + { RIFLE, false, "aug" }, // WEAPON_AUG + { SNIPER_RIFLE, true, "sg550" }, // WEAPON_SG550 + { SNIPER_RIFLE, true, "awp" }, // WEAPON_AWP + { MACHINE_GUN, false, "m249" } // WEAPON_M249 +}; + +static BuyInfo secondaryWeaponBuyInfoCT[ SECONDARY_WEAPON_BUY_COUNT ] = +{ +// { PISTOL, false, "glock" }, +// { PISTOL, false, "usp" }, + { PISTOL, true, "p228" }, + { PISTOL, true, "deagle" }, + { PISTOL, true, "fn57" } +}; + + +static BuyInfo primaryWeaponBuyInfoT[ PRIMARY_WEAPON_BUY_COUNT ] = +{ + { SHOTGUN, false, "m3" }, // WEAPON_M3 + { SHOTGUN, false, "xm1014" }, // WEAPON_XM1014 + { SUB_MACHINE_GUN, false, "mac10" }, // WEAPON_MAC10 + { SUB_MACHINE_GUN, false, "mp5navy" }, // WEAPON_MP5N + { SUB_MACHINE_GUN, false, "ump45" }, // WEAPON_UMP45 + { SUB_MACHINE_GUN, false, "p90" }, // WEAPON_P90 + { RIFLE, true, "galil" }, // WEAPON_GALIL + { RIFLE, true, "ak47" }, // WEAPON_AK47 + { SNIPER_RIFLE, false, "scout" }, // WEAPON_SCOUT + { RIFLE, true, "sg552" }, // WEAPON_SG552 + { SNIPER_RIFLE, true, "awp" }, // WEAPON_AWP + { SNIPER_RIFLE, true, "g3sg1" }, // WEAPON_G3SG1 + { MACHINE_GUN, false, "m249" } // WEAPON_M249 +}; + +static BuyInfo secondaryWeaponBuyInfoT[ SECONDARY_WEAPON_BUY_COUNT ] = +{ +// { PISTOL, false, "glock" }, +// { PISTOL, false, "usp" }, + { PISTOL, true, "p228" }, + { PISTOL, true, "deagle" }, + { PISTOL, true, "elites" } +}; + +/** + * Given a weapon alias, return the kind of weapon it is + */ +inline WeaponType GetWeaponType( const char *alias ) +{ + int i; + + for( i=0; i<PRIMARY_WEAPON_BUY_COUNT; ++i ) + { + if (!stricmp( alias, primaryWeaponBuyInfoCT[i].buyAlias )) + return primaryWeaponBuyInfoCT[i].type; + + if (!stricmp( alias, primaryWeaponBuyInfoT[i].buyAlias )) + return primaryWeaponBuyInfoT[i].type; + } + + for( i=0; i<SECONDARY_WEAPON_BUY_COUNT; ++i ) + { + if (!stricmp( alias, secondaryWeaponBuyInfoCT[i].buyAlias )) + return secondaryWeaponBuyInfoCT[i].type; + + if (!stricmp( alias, secondaryWeaponBuyInfoT[i].buyAlias )) + return secondaryWeaponBuyInfoT[i].type; + } + + return NUM_WEAPON_TYPES; +} + + + + +//-------------------------------------------------------------------------------------------------------------- +void BuyState::OnUpdate( CCSBot *me ) +{ + char cmdBuffer[256]; + + // wait for a Navigation Mesh + if (!TheNavMesh->IsLoaded()) + return; + + // apparently we cant buy things in the first few seconds, so wait a bit + if (m_isInitialDelay) + { + const float waitToBuyTime = 0.25f; + if (gpGlobals->curtime - me->GetStateTimestamp() < waitToBuyTime) + return; + + m_isInitialDelay = false; + } + + // if we're done buying and still in the freeze period, wait + if (m_doneBuying) + { + if (CSGameRules()->IsMultiplayer() && CSGameRules()->IsFreezePeriod()) + { + // make sure we're locked and loaded + me->EquipBestWeapon( MUST_EQUIP ); + me->Reload(); + me->ResetStuckMonitor(); + return; + } + + me->Idle(); + return; + } + + // If we're supposed to buy a specific weapon for debugging, do so and then bail + const char *cheatWeaponString = bot_loadout.GetString(); + if ( cheatWeaponString && *cheatWeaponString ) + { + CUtlVector<char*, CUtlMemory<char*> > loadout; + Q_SplitString( cheatWeaponString, " ", loadout ); + for ( int i=0; i<loadout.Count(); ++i ) + { + const char *item = loadout[i]; + if ( FStrEq( item, "vest" ) ) + { + me->GiveNamedItem( "item_kevlar" ); + } + else if ( FStrEq( item, "vesthelm" ) ) + { + me->GiveNamedItem( "item_assaultsuit" ); + } + else if ( FStrEq( item, "defuser" ) ) + { + if ( me->GetTeamNumber() == TEAM_CT ) + { + me->GiveDefuser(); + } + } + else if ( FStrEq( item, "nvgs" ) ) + { + me->m_bHasNightVision = true; + } + else if ( FStrEq( item, "primammo" ) ) + { + me->AttemptToBuyAmmo( 0 ); + } + else if ( FStrEq( item, "secammo" ) ) + { + me->AttemptToBuyAmmo( 1 ); + } + else + { + me->GiveWeapon( item ); + } + } + m_doneBuying = true; + return; + } + + + if (!me->IsInBuyZone()) + { + m_doneBuying = true; + CONSOLE_ECHO( "%s bot spawned outside of a buy zone (%d, %d, %d)\n", + (me->GetTeamNumber() == TEAM_CT) ? "CT" : "Terrorist", + (int)me->GetAbsOrigin().x, + (int)me->GetAbsOrigin().y, + (int)me->GetAbsOrigin().z ); + return; + } + + // try to buy some weapons + const float buyInterval = 0.02f; + if (gpGlobals->curtime - me->GetStateTimestamp() > buyInterval) + { + me->m_stateTimestamp = gpGlobals->curtime; + + bool isPreferredAllDisallowed = true; + + // try to buy our preferred weapons first + if (m_prefIndex < me->GetProfile()->GetWeaponPreferenceCount() && bot_randombuy.GetBool() == false ) + { + // need to retry because sometimes first buy fails?? + const int maxPrefRetries = 2; + if (m_prefRetries >= maxPrefRetries) + { + // try to buy next preferred weapon + ++m_prefIndex; + m_prefRetries = 0; + return; + } + + int weaponPreference = me->GetProfile()->GetWeaponPreference( m_prefIndex ); + + // don't buy it again if we still have one from last round + char weaponPreferenceName[32]; + Q_snprintf( weaponPreferenceName, sizeof(weaponPreferenceName), "weapon_%s", me->GetProfile()->GetWeaponPreferenceAsString( m_prefIndex ) ); + if( me->Weapon_OwnsThisType(weaponPreferenceName) )//Prefs and buyalias use the short version, this uses the long + { + // done with buying preferred weapon + m_prefIndex = 9999; + return; + } + + if (me->HasShield() && weaponPreference == WEAPON_SHIELDGUN) + { + // done with buying preferred weapon + m_prefIndex = 9999; + return; + } + + const char *buyAlias = NULL; + + if (weaponPreference == WEAPON_SHIELDGUN) + { + if (TheCSBots()->AllowTacticalShield()) + buyAlias = "shield"; + } + else + { + buyAlias = WeaponIDToAlias( weaponPreference ); + WeaponType type = GetWeaponType( buyAlias ); + switch( type ) + { + case PISTOL: + if (!TheCSBots()->AllowPistols()) + buyAlias = NULL; + break; + + case SHOTGUN: + if (!TheCSBots()->AllowShotguns()) + buyAlias = NULL; + break; + + case SUB_MACHINE_GUN: + if (!TheCSBots()->AllowSubMachineGuns()) + buyAlias = NULL; + break; + + case RIFLE: + if (!TheCSBots()->AllowRifles()) + buyAlias = NULL; + break; + + case MACHINE_GUN: + if (!TheCSBots()->AllowMachineGuns()) + buyAlias = NULL; + break; + + case SNIPER_RIFLE: + if (!TheCSBots()->AllowSnipers()) + buyAlias = NULL; + break; + } + } + + if (buyAlias) + { + Q_snprintf( cmdBuffer, 256, "buy %s\n", buyAlias ); + + CCommand args; + args.Tokenize( cmdBuffer ); + me->ClientCommand( args ); + + me->PrintIfWatched( "Tried to buy preferred weapon %s.\n", buyAlias ); + isPreferredAllDisallowed = false; + } + + ++m_prefRetries; + + // bail out so we dont waste money on other equipment + // unless everything we prefer has been disallowed, then buy at random + if (isPreferredAllDisallowed == false) + return; + } + + // if we have no preferred primary weapon (or everything we want is disallowed), buy at random + if (!me->HasPrimaryWeapon() && (isPreferredAllDisallowed || !me->GetProfile()->HasPrimaryPreference())) + { + if (m_buyShield) + { + // buy a shield + CCommand args; + args.Tokenize( "buy shield" ); + me->ClientCommand( args ); + + me->PrintIfWatched( "Tried to buy a shield.\n" ); + } + else + { + // build list of allowable weapons to buy + BuyInfo *masterPrimary = (me->GetTeamNumber() == TEAM_TERRORIST) ? primaryWeaponBuyInfoT : primaryWeaponBuyInfoCT; + BuyInfo *stockPrimary[ PRIMARY_WEAPON_BUY_COUNT ]; + int stockPrimaryCount = 0; + + // dont choose sniper rifles as often + const float sniperRifleChance = 50.0f; + bool wantSniper = (RandomFloat( 0, 100 ) < sniperRifleChance) ? true : false; + + if ( bot_randombuy.GetBool() ) + { + wantSniper = true; + } + + for( int i=0; i<PRIMARY_WEAPON_BUY_COUNT; ++i ) + { + if ((masterPrimary[i].type == SHOTGUN && TheCSBots()->AllowShotguns()) || + (masterPrimary[i].type == SUB_MACHINE_GUN && TheCSBots()->AllowSubMachineGuns()) || + (masterPrimary[i].type == RIFLE && TheCSBots()->AllowRifles()) || + (masterPrimary[i].type == SNIPER_RIFLE && TheCSBots()->AllowSnipers() && wantSniper) || + (masterPrimary[i].type == MACHINE_GUN && TheCSBots()->AllowMachineGuns())) + { + stockPrimary[ stockPrimaryCount++ ] = &masterPrimary[i]; + } + } + + if (stockPrimaryCount) + { + // buy primary weapon if we don't have one + int which; + + // on hard difficulty levels, bots try to buy preferred weapons on the first pass + if (m_retries == 0 && TheCSBots()->GetDifficultyLevel() >= BOT_HARD && bot_randombuy.GetBool() == false ) + { + // count up available preferred weapons + int prefCount = 0; + for( which=0; which<stockPrimaryCount; ++which ) + if (stockPrimary[which]->preferred) + ++prefCount; + + if (prefCount) + { + int whichPref = RandomInt( 0, prefCount-1 ); + for( which=0; which<stockPrimaryCount; ++which ) + if (stockPrimary[which]->preferred && whichPref-- == 0) + break; + } + else + { + // no preferred weapons available, just pick randomly + which = RandomInt( 0, stockPrimaryCount-1 ); + } + } + else + { + which = RandomInt( 0, stockPrimaryCount-1 ); + } + + Q_snprintf( cmdBuffer, 256, "buy %s\n", stockPrimary[ which ]->buyAlias ); + + CCommand args; + args.Tokenize( cmdBuffer ); + me->ClientCommand( args ); + + me->PrintIfWatched( "Tried to buy %s.\n", stockPrimary[ which ]->buyAlias ); + } + } + } + + + // + // If we now have a weapon, or have tried for too long, we're done + // + if (me->HasPrimaryWeapon() || m_retries++ > 5) + { + // primary ammo + CCommand args; + if (me->HasPrimaryWeapon()) + { + args.Tokenize( "buy primammo" ); + me->ClientCommand( args ); + } + + // buy armor last, to make sure we bought a weapon first + args.Tokenize( "buy vesthelm" ); + me->ClientCommand( args ); + args.Tokenize( "buy vest" ); + me->ClientCommand( args ); + + // pistols - if we have no preferred pistol, buy at random + if (TheCSBots()->AllowPistols() && !me->GetProfile()->HasPistolPreference()) + { + if (m_buyPistol) + { + int which = RandomInt( 0, SECONDARY_WEAPON_BUY_COUNT-1 ); + + const char *what = NULL; + + if (me->GetTeamNumber() == TEAM_TERRORIST) + what = secondaryWeaponBuyInfoT[ which ].buyAlias; + else + what = secondaryWeaponBuyInfoCT[ which ].buyAlias; + + Q_snprintf( cmdBuffer, 256, "buy %s\n", what ); + args.Tokenize( cmdBuffer ); + me->ClientCommand( args ); + + + // only buy one pistol + m_buyPistol = false; + } + + // make sure we have enough pistol ammo + args.Tokenize( "buy secammo" ); + me->ClientCommand( args ); + } + + // buy a grenade if we wish, and we don't already have one + if (m_buyGrenade && !me->HasGrenade()) + { + if (UTIL_IsTeamAllBots( me->GetTeamNumber() )) + { + // only allow Flashbangs if everyone on the team is a bot (dont want to blind our friendly humans) + float rnd = RandomFloat( 0, 100 ); + + if (rnd < 10) + { + args.Tokenize( "buy smokegrenade" ); + me->ClientCommand( args ); // smoke grenade + } + else if (rnd < 35) + { + args.Tokenize( "buy flashbang" ); + me->ClientCommand( args ); // flashbang + } + else + { + args.Tokenize( "buy hegrenade" ); + me->ClientCommand( args ); // he grenade + } + } + else + { + if (RandomFloat( 0, 100 ) < 10) + { + args.Tokenize( "buy smokegrenade" ); // smoke grenade + me->ClientCommand( args ); + } + else + { + args.Tokenize( "buy hegrenade" ); // he grenade + me->ClientCommand( args ); + } + } + } + + if (m_buyDefuseKit) + { + args.Tokenize( "buy defuser" ); + me->ClientCommand( args ); + } + + m_doneBuying = true; + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void BuyState::OnExit( CCSBot *me ) +{ + me->ResetStuckMonitor(); + me->EquipBestWeapon(); +} + diff --git a/game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp new file mode 100644 index 0000000..3614bcb --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp @@ -0,0 +1,82 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin defusing the bomb + */ +void DefuseBombState::OnEnter( CCSBot *me ) +{ + me->Crouch(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + me->GetChatter()->Say( "DefusingBomb" ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Defuse the bomb + */ +void DefuseBombState::OnUpdate( CCSBot *me ) +{ + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + + if (bombPos == NULL) + { + me->PrintIfWatched( "In Defuse state, but don't know where the bomb is!\n" ); + me->Idle(); + return; + } + + // look at the bomb + me->SetLookAt( "Defuse bomb", *bombPos, PRIORITY_HIGH ); + + // defuse... + me->UseEnvironment(); + + if (gpGlobals->curtime - me->GetStateTimestamp() > 1.0f) + { + // if we missed starting the defuse, give up + if (TheCSBots()->GetBombDefuser() == NULL) + { + me->PrintIfWatched( "Failed to start defuse, giving up\n" ); + me->Idle(); + return; + } + else if (TheCSBots()->GetBombDefuser() != me) + { + // if someone else got the defuse, give up + me->PrintIfWatched( "Someone else started defusing, giving up\n" ); + me->Idle(); + return; + } + } + + // if bomb has been defused, give up + if (!TheCSBots()->IsBombPlanted()) + { + me->Idle(); + return; + } +} + +//-------------------------------------------------------------------------------------------------------------- +void DefuseBombState::OnExit( CCSBot *me ) +{ + me->StandUp(); + me->ResetStuckMonitor(); + me->SetTask( CCSBot::SEEK_AND_DESTROY ); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + me->ClearLookAt(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp new file mode 100644 index 0000000..9abeb2b --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp @@ -0,0 +1,67 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Escape from the bomb. + */ +void EscapeFromBombState::OnEnter( CCSBot *me ) +{ + me->StandUp(); + me->Run(); + me->DestroyPath(); + me->EquipKnife(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Escape from the bomb. + */ +void EscapeFromBombState::OnUpdate( CCSBot *me ) +{ + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + + // if we don't know where the bomb is, we shouldn't be in this state + if (bombPos == NULL) + { + me->Idle(); + return; + } + + // grab our knife to move quickly + me->EquipKnife(); + + // look around + me->UpdateLookAround(); + + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + // we have no path, or reached the end of one - create a new path far away from the bomb + FarAwayFromPositionFunctor func( *bombPos ); + CNavArea *goalArea = FindMinimumCostArea( me->GetLastKnownArea(), func ); + + // if this fails, we'll try again next time + me->ComputePath( goalArea->GetCenter(), FASTEST_ROUTE ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Escape from the bomb. + */ +void EscapeFromBombState::OnExit( CCSBot *me ) +{ + me->EquipBestWeapon(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp new file mode 100644 index 0000000..6e78019 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp @@ -0,0 +1,69 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to the bomb on the floor and pick it up + */ +void FetchBombState::OnEnter( CCSBot *me ) +{ + me->DestroyPath(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to the bomb on the floor and pick it up + */ +void FetchBombState::OnUpdate( CCSBot *me ) +{ + if (me->HasC4()) + { + me->PrintIfWatched( "I picked up the bomb\n" ); + me->Idle(); + return; + } + + + CBaseEntity *bomb = TheCSBots()->GetLooseBomb(); + if (bomb) + { + if (!me->HasPath()) + { + // build a path to the bomb + if (me->ComputePath( bomb->GetAbsOrigin() ) == false) + { + me->PrintIfWatched( "Fetch bomb pathfind failed\n" ); + + // go Hunt instead of Idle to prevent continuous re-pathing to inaccessible bomb + me->Hunt(); + return; + } + } + } + else + { + // someone picked up the bomb + me->PrintIfWatched( "Someone else picked up the bomb.\n" ); + me->Idle(); + return; + } + + // look around + me->UpdateLookAround(); + + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + me->Idle(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_follow.cpp b/game/server/cstrike/bot/states/cs_bot_follow.cpp new file mode 100644 index 0000000..c8d4976 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_follow.cpp @@ -0,0 +1,366 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Follow our leader + */ +void FollowState::OnEnter( CCSBot *me ) +{ + me->StandUp(); + me->Run(); + me->DestroyPath(); + + m_isStopped = false; + m_stoppedTimestamp = 0.0f; + + // to force immediate repath + m_lastLeaderPos.x = -99999999.9f; + m_lastLeaderPos.y = -99999999.9f; + m_lastLeaderPos.z = -99999999.9f; + + m_lastSawLeaderTime = 0; + + // set re-pathing frequency + m_repathInterval.Invalidate(); + + m_isSneaking = false; + + m_walkTime.Invalidate(); + m_isAtWalkSpeed = false; + + m_leaderMotionState = INVALID; + m_idleTimer.Start( RandomFloat( 2.0f, 5.0f ) ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine the leader's motion state by tracking his speed + */ +void FollowState::ComputeLeaderMotionState( float leaderSpeed ) +{ + // walk = 130, run = 250 + const float runWalkThreshold = 140.0f; + const float walkStopThreshold = 10.0f; // 120.0f; + LeaderMotionStateType prevState = m_leaderMotionState; + if (leaderSpeed > runWalkThreshold) + { + m_leaderMotionState = RUNNING; + m_isAtWalkSpeed = false; + } + else if (leaderSpeed > walkStopThreshold) + { + // track when began to walk + if (!m_isAtWalkSpeed) + { + m_walkTime.Start(); + m_isAtWalkSpeed = true; + } + + const float minWalkTime = 0.25f; + if (m_walkTime.GetElapsedTime() > minWalkTime) + { + m_leaderMotionState = WALKING; + } + } + else + { + m_leaderMotionState = STOPPED; + m_isAtWalkSpeed = false; + } + + // track time spent in this motion state + if (prevState != m_leaderMotionState) + { + m_leaderMotionStateTime.Start(); + m_waitTime = RandomFloat( 1.0f, 3.0f ); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Functor to collect all areas in the forward direction of the given player within a radius + */ +class FollowTargetCollector +{ +public: + FollowTargetCollector( CBasePlayer *player ) + { + m_player = player; + + Vector playerVel = player->GetAbsVelocity(); + m_forward.x = playerVel.x; + m_forward.y = playerVel.y; + float speed = m_forward.NormalizeInPlace(); + + Vector playerOrigin = GetCentroid( player ); + + const float walkSpeed = 100.0f; + if (speed < walkSpeed) + { + m_cutoff.x = playerOrigin.x; + m_cutoff.y = playerOrigin.y; + m_forward.x = 0.0f; + m_forward.y = 0.0f; + } + else + { + const float k = 1.5f; // 2.0f; + float trimSpeed = MIN( speed, 200.0f ); + m_cutoff.x = playerOrigin.x + k * trimSpeed * m_forward.x; + m_cutoff.y = playerOrigin.y + k * trimSpeed * m_forward.y; + } + + m_targetAreaCount = 0; + } + + enum { MAX_TARGET_AREAS = 128 }; + + bool operator() ( CNavArea *area ) + { + if (m_targetAreaCount >= MAX_TARGET_AREAS) + return false; + + // only use two-way connections + if (!area->GetParent() || area->IsConnected( area->GetParent(), NUM_DIRECTIONS )) + { + if (m_forward.IsZero()) + { + m_targetArea[ m_targetAreaCount++ ] = area; + } + else + { + // collect areas in the direction of the player's forward motion + Vector2D to( area->GetCenter().x - m_cutoff.x, area->GetCenter().y - m_cutoff.y ); + to.NormalizeInPlace(); + + //if (DotProduct( to, m_forward ) > 0.7071f) + if ((to.x * m_forward.x + to.y * m_forward.y) > 0.7071f) + m_targetArea[ m_targetAreaCount++ ] = area; + } + } + + return (m_targetAreaCount < MAX_TARGET_AREAS); + } + + + CBasePlayer *m_player; + Vector2D m_forward; + Vector2D m_cutoff; + + CNavArea *m_targetArea[ MAX_TARGET_AREAS ]; + int m_targetAreaCount; +}; + +//-------------------------------------------------------------------------------------------------------------- +/** + * Follow our leader + * @todo Clean up this nasty mess + */ +void FollowState::OnUpdate( CCSBot *me ) +{ + // if we lost our leader, give up + if (m_leader == NULL || !m_leader->IsAlive()) + { + me->Idle(); + return; + } + + // if we are carrying the bomb and at a bombsite, plant + if (me->HasC4() && me->IsAtBombsite()) + { + // plant it + me->SetTask( CCSBot::PLANT_BOMB ); + me->PlantBomb(); + + // radio to the team + me->GetChatter()->PlantingTheBomb( me->GetPlace() ); + + return; + } + + // look around + me->UpdateLookAround(); + + // if we are moving, we are not idle + if (me->IsNotMoving() == false) + m_idleTimer.Start( RandomFloat( 2.0f, 5.0f ) ); + + // compute the leader's speed + Vector leaderVel = m_leader->GetAbsVelocity(); + float leaderSpeed = Vector2D( leaderVel.x, leaderVel.y ).Length(); + + // determine our leader's movement state + ComputeLeaderMotionState( leaderSpeed ); + + // track whether we can see the leader + bool isLeaderVisible; + Vector leaderOrigin = GetCentroid( m_leader ); + if (me->IsVisible( leaderOrigin )) + { + m_lastSawLeaderTime = gpGlobals->curtime; + isLeaderVisible = true; + } + else + { + isLeaderVisible = false; + } + + + // determine whether we should sneak or not + const float farAwayRange = 750.0f; + Vector myOrigin = GetCentroid( me ); + if ((leaderOrigin - myOrigin).IsLengthGreaterThan( farAwayRange )) + { + // far away from leader - run to catch up + m_isSneaking = false; + } + else if (isLeaderVisible) + { + // if we see leader walking and we are nearby, walk + if (m_leaderMotionState == WALKING) + m_isSneaking = true; + + // if we are sneaking and our leader starts running, stop sneaking + if (m_isSneaking && m_leaderMotionState == RUNNING) + m_isSneaking = false; + } + + // if we haven't seen the leader for a long time, run + const float longTime = 20.0f; + if (gpGlobals->curtime - m_lastSawLeaderTime > longTime) + m_isSneaking = false; + + if (m_isSneaking) + me->Walk(); + else + me->Run(); + + + bool repath = false; + + // if the leader has stopped, hide nearby + const float nearLeaderRange = 250.0f; + if (!me->HasPath() && m_leaderMotionState == STOPPED && m_leaderMotionStateTime.GetElapsedTime() > m_waitTime) + { + // throttle how often this check occurs + m_waitTime += RandomFloat( 1.0f, 3.0f ); + + // the leader has stopped - if we are close to him, take up a hiding spot + if ((leaderOrigin - myOrigin).IsLengthLessThan( nearLeaderRange )) + { + const float hideRange = 250.0f; + if (me->TryToHide( NULL, -1.0f, hideRange, false, USE_NEAREST )) + { + me->ResetStuckMonitor(); + return; + } + } + } + + // if we have been idle for awhile, move + if (m_idleTimer.IsElapsed()) + { + repath = true; + + // always walk when we move such a short distance + m_isSneaking = true; + } + + // if our leader has moved, repath (don't repath if leading is stopping) + if (leaderSpeed > 100.0f && m_leaderMotionState != STOPPED) + { + repath = true; + } + + // move along our path + if (me->UpdatePathMovement( NO_SPEED_CHANGE ) != CCSBot::PROGRESSING) + { + me->DestroyPath(); + } + + // recompute our path if necessary + if (repath && m_repathInterval.IsElapsed() && !me->IsOnLadder()) + { + // recompute our path to keep us near our leader + m_lastLeaderPos = leaderOrigin; + + me->ResetStuckMonitor(); + + const float runSpeed = 200.0f; + + const float collectRange = (leaderSpeed > runSpeed) ? 600.0f : 400.0f; // 400, 200 + FollowTargetCollector collector( m_leader ); + SearchSurroundingAreas( TheNavMesh->GetNearestNavArea( m_lastLeaderPos ), m_lastLeaderPos, collector, collectRange ); + + if (cv_bot_debug.GetBool()) + { + for( int i=0; i<collector.m_targetAreaCount; ++i ) + collector.m_targetArea[i]->Draw( /*255, 0, 0, 2*/ ); + } + + // move to one of the collected areas + if (collector.m_targetAreaCount) + { + CNavArea *target = NULL; + Vector targetPos; + + // if we are idle, pick a random area + if (m_idleTimer.IsElapsed()) + { + target = collector.m_targetArea[ RandomInt( 0, collector.m_targetAreaCount-1 ) ]; + targetPos = target->GetCenter(); + me->PrintIfWatched( "%4.1f: Bored. Repathing to a new nearby area\n", gpGlobals->curtime ); + } + else + { + me->PrintIfWatched( "%4.1f: Repathing to stay with leader.\n", gpGlobals->curtime ); + + // find closest area to where we are + CNavArea *area; + float closeRangeSq = 9999999999.9f; + Vector close; + + for( int a=0; a<collector.m_targetAreaCount; ++a ) + { + area = collector.m_targetArea[a]; + + area->GetClosestPointOnArea( myOrigin, &close ); + + float rangeSq = (myOrigin - close).LengthSqr(); + if (rangeSq < closeRangeSq) + { + target = area; + targetPos = close; + closeRangeSq = rangeSq; + } + } + } + + if (target == NULL || me->ComputePath( target->GetCenter(), FASTEST_ROUTE ) == false) + me->PrintIfWatched( "Pathfind to leader failed.\n" ); + + // throttle how often we repath + m_repathInterval.Start( 0.5f ); + + m_idleTimer.Reset(); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void FollowState::OnExit( CCSBot *me ) +{ +} diff --git a/game/server/cstrike/bot/states/cs_bot_hide.cpp b/game/server/cstrike/bot/states/cs_bot_hide.cpp new file mode 100644 index 0000000..9afd1e8 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_hide.cpp @@ -0,0 +1,549 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin moving to a nearby hidey-hole. + * NOTE: Do not forget this state may include a very long "move-to" time to get to our hidey spot! + */ +void HideState::OnEnter( CCSBot *me ) +{ + m_isAtSpot = false; + m_isLookingOutward = false; + + // if duration is "infinite", set it to a reasonably long time to prevent infinite camping + if (m_duration < 0.0f) + { + m_duration = RandomFloat( 30.0f, 60.0f ); + } + + // decide whether to "ambush" or not - never set to false so as not to override external setting + if (RandomFloat( 0.0f, 100.0f ) < 50.0f) + { + m_isHoldingPosition = true; + } + + // if we are holding position, decide for how long + if (m_isHoldingPosition) + { + m_holdPositionTime = RandomFloat( 3.0f, 10.0f ); + } + else + { + m_holdPositionTime = 0.0f; + } + + m_heardEnemy = false; + m_firstHeardEnemyTime = 0.0f; + m_retry = 0; + + if (me->IsFollowing()) + { + m_leaderAnchorPos = GetCentroid( me->GetFollowLeader() ); + } + + // if we are a sniper, we need to periodically pause while we retreat to squeeze off a shot or two + if (me->IsSniper()) + { + // start off paused to allow a final shot before retreating + m_isPaused = false; + m_pauseTimer.Invalidate(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a nearby hidey-hole. + * NOTE: Do not forget this state may include a very long "move-to" time to get to our hidey spot! + */ +void HideState::OnUpdate( CCSBot *me ) +{ + Vector myOrigin = GetCentroid( me ); + + // wait until finished reloading to leave hide state + if (!me->IsReloading()) + { + // if we are momentarily hiding while following someone, check to see if he has moved on + if (me->IsFollowing()) + { + CCSPlayer *leader = static_cast<CCSPlayer *>( static_cast<CBaseEntity *>( me->GetFollowLeader() ) ); + Vector leaderOrigin = GetCentroid( leader ); + + // BOTPORT: Determine walk/run velocity thresholds + float runThreshold = 200.0f; + if (leader->GetAbsVelocity().IsLengthGreaterThan( runThreshold )) + { + // leader is running, stay with him + me->Follow( leader ); + return; + } + + // if leader has moved, stay with him + const float followRange = 250.0f; + if ((m_leaderAnchorPos - leaderOrigin).IsLengthGreaterThan( followRange )) + { + me->Follow( leader ); + return; + } + } + + // if we see a nearby buddy in combat, join him + /// @todo - Perhaps tie in to TakeDamage(), so it works for human players, too + + // + // Scenario logic + // + switch( TheCSBots()->GetScenario() ) + { + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + if (me->GetTeamNumber() == TEAM_CT) + { + // if we are just holding position (due to a radio order) and the bomb has just planted, go defuse it + if (me->GetTask() == CCSBot::HOLD_POSITION && + TheCSBots()->IsBombPlanted() && + TheCSBots()->GetBombPlantTimestamp() > me->GetStateTimestamp()) + { + me->Idle(); + return; + } + + // if we are guarding the defuser and he dies/gives up, stop hiding (to choose another defuser) + if (me->GetTask() == CCSBot::GUARD_BOMB_DEFUSER && TheCSBots()->GetBombDefuser() == NULL) + { + me->Idle(); + return; + } + + // if we are guarding the loose bomb and it is picked up, stop hiding + if (me->GetTask() == CCSBot::GUARD_LOOSE_BOMB && TheCSBots()->GetLooseBomb() == NULL) + { + me->GetChatter()->TheyPickedUpTheBomb(); + me->Idle(); + return; + } + + // if we are guarding a bombsite and the bomb is dropped and we hear about it, stop guarding + if (me->GetTask() == CCSBot::GUARD_BOMB_ZONE && me->GetGameState()->IsLooseBombLocationKnown()) + { + me->Idle(); + return; + } + + // if we are guarding (bombsite, initial encounter, etc) and the bomb is planted, go defuse it + if (me->IsDoingScenario() && me->GetTask() != CCSBot::GUARD_BOMB_DEFUSER && TheCSBots()->IsBombPlanted()) + { + me->Idle(); + return; + } + + } + else // TERRORIST + { + // if we are near the ticking bomb and someone starts defusing it, attack! + if (TheCSBots()->GetBombDefuser()) + { + Vector defuserOrigin = GetCentroid( TheCSBots()->GetBombDefuser() ); + Vector toDefuser = defuserOrigin - myOrigin; + + const float hearDefuseRange = 2000.0f; + if (toDefuser.IsLengthLessThan( hearDefuseRange )) + { + // if we are nearby, attack, otherwise move to the bomb (which will cause us to attack when we see defuser) + if (me->CanSeePlantedBomb()) + { + me->Attack( TheCSBots()->GetBombDefuser() ); + } + else + { + me->MoveTo( defuserOrigin, FASTEST_ROUTE ); + me->InhibitLookAround( 10.0f ); + } + + return; + } + } + } + break; + } + + //-------------------------------------------------------------------------------------------------- + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + // if we're guarding the hostages and they all die or are taken, do something else + if (me->GetTask() == CCSBot::GUARD_HOSTAGES) + { + if (me->GetGameState()->AreAllHostagesBeingRescued() || me->GetGameState()->AreAllHostagesGone()) + { + me->Idle(); + return; + } + } + else if (me->GetTask() == CCSBot::GUARD_HOSTAGE_RESCUE_ZONE) + { + // if we stumble across a hostage, guard it + CHostage *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + // we see a free hostage, guard it + Vector hostageOrigin = GetCentroid( hostage ); + CNavArea *area = TheNavMesh->GetNearestNavArea( hostageOrigin ); + if (area) + { + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm guarding hostages I found\n" ); + // don't chatter here - he'll tell us when he's in his hiding spot + return; + } + } + } + } + } + + + bool isSettledInSniper = (me->IsSniper() && m_isAtSpot) ? true : false; + + // only investigate noises if we are initiating attacks, and we aren't a "settled in" sniper + // dont investigate noises if we are reloading + if (!me->IsReloading() && + !isSettledInSniper && + me->GetDisposition() == CCSBot::ENGAGE_AND_INVESTIGATE) + { + // if we are holding position, and have heard the enemy nearby, investigate after our hold time is up + if (m_isHoldingPosition && m_heardEnemy && (gpGlobals->curtime - m_firstHeardEnemyTime > m_holdPositionTime)) + { + /// @todo We might need to remember specific location of last enemy noise here + me->InvestigateNoise(); + return; + } + + // investigate nearby enemy noises + if (me->HeardInterestingNoise()) + { + // if we are holding position, check if enough time has elapsed since we first heard the enemy + if (m_isAtSpot && m_isHoldingPosition) + { + if (!m_heardEnemy) + { + // first time we heard the enemy + m_heardEnemy = true; + m_firstHeardEnemyTime = gpGlobals->curtime; + me->PrintIfWatched( "Heard enemy, holding position for %f2.1 seconds...\n", m_holdPositionTime ); + } + } + else + { + // not holding position - investigate enemy noise + me->InvestigateNoise(); + return; + } + } + } + } // end reloading check + + // look around + me->UpdateLookAround(); + + // if we are at our hiding spot, crouch and wait + if (m_isAtSpot) + { + me->ResetStuckMonitor(); + + CNavArea *area = TheNavMesh->GetNavArea( m_hidingSpot ); + if ( !area || !( area->GetAttributes() & NAV_MESH_STAND ) ) + { + me->Crouch(); + } + + // check if duration has expired + if (m_hideTimer.IsElapsed()) + { + if (me->GetTask() == CCSBot::GUARD_LOOSE_BOMB) + { + // if we're guarding the loose bomb, continue to guard it but pick a new spot + me->Hide( TheCSBots()->GetLooseBombArea() ); + return; + } + else if (me->GetTask() == CCSBot::GUARD_BOMB_ZONE) + { + // if we're guarding a bombsite, continue to guard it but pick a new spot + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( myOrigin ); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + me->Hide( area ); + return; + } + } + } + else if (me->GetTask() == CCSBot::GUARD_HOSTAGE_RESCUE_ZONE) + { + // if we're guarding a rescue zone, continue to guard this or another rescue zone + if (me->GuardRandomZone()) + { + me->SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + me->PrintIfWatched( "Continuing to guard hostage rescue zones\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->GetChatter()->GuardingHostageEscapeZone( IS_PLAN ); + return; + } + } + + me->Idle(); + return; + } + +/* + // if we are watching for an approaching noisy enemy, anticipate and fire before they round the corner + /// @todo Need to check if we are looking at an ENEMY_NOISE here + const float veryCloseNoise = 250.0f; + if (me->IsLookingAtSpot() && me->GetNoiseRange() < veryCloseNoise) + { + // fire! + me->PrimaryAttack(); + me->PrintIfWatched( "Firing at anticipated enemy coming around the corner!\n" ); + } +*/ + + // if we have a shield, hide behind it + if (me->HasShield() && !me->IsProtectedByShield()) + me->SecondaryAttack(); + + // while sitting at our hiding spot, if we are being attacked but can't see our attacker, move somewhere else + const float hurtRecentlyTime = 1.0f; + if (!me->IsEnemyVisible() && me->GetTimeSinceAttacked() < hurtRecentlyTime) + { + me->Idle(); + return; + } + + // encourage the human player + if (!me->IsDoingScenario()) + { + if (me->GetTeamNumber() == TEAM_CT) + { + if (me->GetTask() == CCSBot::GUARD_BOMB_ZONE && + me->IsAtHidingSpot() && + TheCSBots()->IsBombPlanted()) + { + if (me->GetNearbyEnemyCount() == 0) + { + const float someTime = 30.0f; + const float littleTime = 11.0; + + if (TheCSBots()->GetBombTimeLeft() > someTime) + me->GetChatter()->Encourage( "BombsiteSecure", RandomFloat( 10.0f, 15.0f ) ); + else if (TheCSBots()->GetBombTimeLeft() > littleTime) + me->GetChatter()->Encourage( "WaitingForHumanToDefuseBomb", RandomFloat( 5.0f, 8.0f ) ); + else + me->GetChatter()->Encourage( "WaitingForHumanToDefuseBombPanic", RandomFloat( 3.0f, 4.0f ) ); + } + } + + if (me->GetTask() == CCSBot::GUARD_HOSTAGES && me->IsAtHidingSpot()) + { + if (me->GetNearbyEnemyCount() == 0) + { + CHostage *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + me->GetChatter()->Encourage( "WaitingForHumanToRescueHostages", RandomFloat( 10.0f, 15.0f ) ); + } + } + } + } + } + } + else + { + // we are moving to our hiding spot + + // snipers periodically pause and fire while retreating + if (me->IsSniper() && me->IsEnemyVisible()) + { + if (m_isPaused) + { + if (m_pauseTimer.IsElapsed()) + { + // get moving + m_isPaused = false; + m_pauseTimer.Start( RandomFloat( 1.0f, 3.0f ) ); + } + else + { + me->Wait( 0.2f ); + } + } + else + { + if (m_pauseTimer.IsElapsed()) + { + // pause for a moment + m_isPaused = true; + m_pauseTimer.Start( RandomFloat( 0.5f, 1.5f ) ); + } + } + } + + // if a Player is using this hiding spot, give up + float range; + CCSPlayer *camper = static_cast<CCSPlayer *>( UTIL_GetClosestPlayer( m_hidingSpot, &range ) ); + + const float closeRange = 75.0f; + if (camper && camper != me && range < closeRange && me->IsVisible( camper, CHECK_FOV )) + { + // player is in our hiding spot + me->PrintIfWatched( "Someone's in my hiding spot - picking another...\n" ); + + const int maxRetries = 3; + if (m_retry++ >= maxRetries) + { + me->PrintIfWatched( "Can't find a free hiding spot, giving up.\n" ); + me->Idle(); + return; + } + + // pick another hiding spot near where we were planning on hiding + me->Hide( TheNavMesh->GetNavArea( m_hidingSpot ) ); + + return; + } + + Vector toSpot; + toSpot.x = m_hidingSpot.x - myOrigin.x; + toSpot.y = m_hidingSpot.y - myOrigin.y; + toSpot.z = m_hidingSpot.z - me->GetFeetZ(); // use feet location + range = toSpot.Length(); + + // look outwards as we get close to our hiding spot + if (!me->IsEnemyVisible() && !m_isLookingOutward) + { + const float lookOutwardRange = 200.0f; + const float nearSpotRange = 10.0f; + if (range < lookOutwardRange && range > nearSpotRange) + { + m_isLookingOutward = true; + + toSpot.x /= range; + toSpot.y /= range; + toSpot.z /= range; + + me->SetLookAt( "Face outward", me->EyePosition() - 1000.0f * toSpot, PRIORITY_HIGH, 3.0f ); + } + } + + const float atDist = 20.0f; + if (range < atDist) + { + //------------------------------------- + // Just reached our hiding spot + // + m_isAtSpot = true; + m_hideTimer.Start( m_duration ); + + // make sure our approach points are valid, since we'll be watching them + me->ComputeApproachPoints(); + me->ClearLookAt(); + + // ready our weapon and prepare to attack + me->EquipBestWeapon( me->IsUsingGrenade() ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + + // if we are a sniper, update our task + if (me->GetTask() == CCSBot::MOVE_TO_SNIPER_SPOT) + { + me->SetTask( CCSBot::SNIPING ); + } + else if (me->GetTask() == CCSBot::GUARD_INITIAL_ENCOUNTER) + { + const float campChatterChance = 20.0f; + if (RandomFloat( 0, 100 ) < campChatterChance) + { + me->GetChatter()->Say( "WaitingHere" ); + } + } + + + // determine which way to look + trace_t result; + float outAngle = 0.0f; + float outAngleRange = 0.0f; + for( float angle = 0.0f; angle < 360.0f; angle += 45.0f ) + { + UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 1000.0f * Vector( BotCOS(angle), BotSIN(angle), 0.0f ), MASK_PLAYERSOLID, me, COLLISION_GROUP_NONE, &result ); + + if (result.fraction > outAngleRange) + { + outAngle = angle; + outAngleRange = result.fraction; + } + } + + me->SetLookAheadAngle( outAngle ); + + } + + // move to hiding spot + if (me->UpdatePathMovement() != CCSBot::PROGRESSING && !m_isAtSpot) + { + // we couldn't get to our hiding spot - pick another + me->PrintIfWatched( "Can't get to my hiding spot - finding another...\n" ); + + // search from hiding spot, since we know it was valid + const Vector *pos = FindNearbyHidingSpot( me, m_hidingSpot, m_range, me->IsSniper() ); + if (pos == NULL) + { + // no available hiding spots + me->PrintIfWatched( "No available hiding spots - hiding where I'm at.\n" ); + + // hide where we are + m_hidingSpot.x = myOrigin.x; + m_hidingSpot.x = myOrigin.y; + m_hidingSpot.z = me->GetFeetZ(); + } + else + { + m_hidingSpot = *pos; + } + + // build a path to our new hiding spot + if (me->ComputePath( m_hidingSpot, FASTEST_ROUTE ) == false) + { + me->PrintIfWatched( "Can't pathfind to hiding spot\n" ); + me->Idle(); + return; + } + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +void HideState::OnExit( CCSBot *me ) +{ + m_isHoldingPosition = false; + + me->StandUp(); + me->ResetStuckMonitor(); + //me->ClearLookAt(); + me->ClearApproachPoints(); + + // if we have a shield, put it away + if (me->HasShield() && me->IsProtectedByShield()) + me->SecondaryAttack(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_hunt.cpp b/game/server/cstrike/bot/states/cs_bot_hunt.cpp new file mode 100644 index 0000000..9ca6c90 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_hunt.cpp @@ -0,0 +1,234 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Begin the hunt + */ +void HuntState::OnEnter( CCSBot *me ) +{ + // lurking death + if (me->IsUsingKnife() && me->IsWellPastSafe() && !me->IsHurrying()) + me->Walk(); + else + me->Run(); + + + me->StandUp(); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + me->SetTask( CCSBot::SEEK_AND_DESTROY ); + + me->DestroyPath(); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Hunt down our enemies + */ +void HuntState::OnUpdate( CCSBot *me ) +{ + // if we've been hunting for a long time, drop into Idle for a moment to + // select something else to do + const float huntingTooLongTime = 30.0f; + if (gpGlobals->curtime - me->GetStateTimestamp() > huntingTooLongTime) + { + // stop being a rogue and do the scenario, since there must not be many enemies left to hunt + me->PrintIfWatched( "Giving up hunting.\n" ); + me->SetRogue( false ); + me->Idle(); + return; + } + + // scenario logic + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_DEFUSE_BOMB) + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + // if we have the bomb and it's time to plant, or we happen to be in a bombsite and it seems safe, do it + if (me->HasC4()) + { + const float safeTime = 3.0f; + + if (TheCSBots()->IsTimeToPlantBomb() || + (me->IsAtBombsite() && gpGlobals->curtime - me->GetLastSawEnemyTimestamp() > safeTime)) + { + me->Idle(); + return; + } + } + + // if we notice the bomb lying on the ground, go get it + if (me->NoticeLooseBomb()) + { + me->FetchBomb(); + return; + } + + // if bomb has been planted, and we hear it, move to a hiding spot near the bomb and watch it + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (!me->IsRogue() && me->GetGameState()->IsBombPlanted() && bombPos) + { + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + return; + } + } + else // CT + { + if (!me->IsRogue() && me->CanSeeLooseBomb()) + { + // if we are near the loose bomb and can see it, hide nearby and guard it + me->SetTask( CCSBot::GUARD_LOOSE_BOMB ); + me->Hide( TheCSBots()->GetLooseBombArea() ); + me->GetChatter()->GuardingLooseBomb( TheCSBots()->GetLooseBomb() ); + return; + } + else if (TheCSBots()->IsBombPlanted()) + { + // rogues will defuse a bomb, but not guard the defuser + if (!me->IsRogue() || !TheCSBots()->GetBombDefuser()) + { + // search for the planted bomb to defuse + me->Idle(); + return; + } + } + } + } + else if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_RESCUE_HOSTAGES) + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + if (me->GetGameState()->AreAllHostagesBeingRescued()) + { + // all hostages are being rescued, head them off at the escape zones + if (me->GuardRandomZone()) + { + me->SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + me->PrintIfWatched( "Trying to beat them to an escape zone!\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->GetChatter()->GuardingHostageEscapeZone( IS_PLAN ); + return; + } + } + + // if safe time is up, and we stumble across a hostage, guard it + if (!me->IsRogue() && !me->IsSafe()) + { + CHostage *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ); + if (area) + { + // we see a free hostage, guard it + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm guarding hostages\n" ); + me->GetChatter()->GuardingHostages( area->GetPlace(), IS_PLAN ); + return; + } + } + } + } + } + + // listen for enemy noises + if (me->HeardInterestingNoise()) + { + me->InvestigateNoise(); + return; + } + + // look around + me->UpdateLookAround(); + + // if we have reached our destination area, pick a new one + // if our path fails, pick a new one + if (me->GetLastKnownArea() == m_huntArea || me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + // pick a new hunt area + const float earlyGameTime = 45.0f; + if (TheCSBots()->GetElapsedRoundTime() < earlyGameTime && !me->HasVisitedEnemySpawn()) + { + // in the early game, rush the enemy spawn + CBaseEntity *enemySpawn = TheCSBots()->GetRandomSpawn( OtherTeam( me->GetTeamNumber() ) ); + + //ADRIAN: REVISIT + if ( enemySpawn ) + { + m_huntArea = TheNavMesh->GetNavArea( enemySpawn->WorldSpaceCenter() ); + } + } + else + { + m_huntArea = NULL; + float oldest = 0.0f; + + int areaCount = 0; + const float minSize = 150.0f; + + FOR_EACH_VEC( TheNavAreas, it ) + { + CNavArea *area = TheNavAreas[ it ]; + + ++areaCount; + + // skip the small areas + Extent extent; + area->GetExtent(&extent); + if (extent.hi.x - extent.lo.x < minSize || extent.hi.y - extent.lo.y < minSize) + continue; + + // keep track of the least recently cleared area + float age = gpGlobals->curtime - area->GetClearedTimestamp( me->GetTeamNumber()-1 ); + if (age > oldest) + { + oldest = age; + m_huntArea = area; + } + } + + // if all the areas were too small, pick one at random + int which = RandomInt( 0, areaCount-1 ); + + areaCount = 0; + FOR_EACH_VEC( TheNavAreas, hit ) + { + m_huntArea = TheNavAreas[ hit ]; + + if (which == areaCount) + break; + + --which; + } + } + + if (m_huntArea) + { + // create a new path to a far away area of the map + me->ComputePath( m_huntArea->GetCenter() ); + } + } +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Done hunting + */ +void HuntState::OnExit( CCSBot *me ) +{ +} diff --git a/game/server/cstrike/bot/states/cs_bot_idle.cpp b/game/server/cstrike/bot/states/cs_bot_idle.cpp new file mode 100644 index 0000000..64e14d2 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_idle.cpp @@ -0,0 +1,887 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// range for snipers to select a hiding spot +const float sniperHideRange = 2000.0f; + +//-------------------------------------------------------------------------------------------------------------- +/** + * The Idle state. + * We never stay in the Idle state - it is a "home base" for the state machine that + * does various checks to determine what we should do next. + */ +void IdleState::OnEnter( CCSBot *me ) +{ + me->DestroyPath(); + me->SetBotEnemy( NULL ); + + // lurking death + if (me->IsUsingKnife() && me->IsWellPastSafe() && !me->IsHurrying()) + me->Walk(); + + // + // Since Idle assigns tasks, we assume that coming back to Idle means our task is complete + // + me->SetTask( CCSBot::SEEK_AND_DESTROY ); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Determine what we should do next + */ +void IdleState::OnUpdate( CCSBot *me ) +{ + // all other states assume GetLastKnownArea() is valid, ensure that it is + if (me->GetLastKnownArea() == NULL && me->StayOnNavMesh() == false) + return; + + // zombies never leave the Idle state + if (cv_bot_zombie.GetBool()) + { + me->ResetStuckMonitor(); + return; + } + + // if we are in the early "safe" time, grab a knife or grenade + if (me->IsSafe()) + { + // if we have a grenade, use it + if (!me->EquipGrenade()) + { + // high-skill bots run with the knife, unless using the Scout (which moves faster) + if (me->GetProfile()->GetSkill() > 0.33f && !me->IsUsing( WEAPON_SCOUT )) + { + me->EquipKnife(); + } + } + } + + // if round is over, hunt + if (me->GetGameState()->IsRoundOver()) + { + // if we are escorting hostages, try to get to the rescue zone + if (me->GetHostageEscortCount()) + { + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me, FASTEST_ROUTE ) ); + const Vector *zonePos = TheCSBots()->GetRandomPositionInZone( zone ); + + if (zonePos) + { + me->SetTask( CCSBot::RESCUE_HOSTAGES ); + me->Run(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + me->MoveTo( *zonePos, FASTEST_ROUTE ); + me->PrintIfWatched( "Trying to rescue hostages at the end of the round\n" ); + return; + } + } + + me->Hunt(); + return; + } + + const float defenseSniperCampChance = 75.0f; + const float offenseSniperCampChance = 10.0f; + + // if we were following someone, continue following them + if (me->IsFollowing()) + { + me->ContinueFollowing(); + return; + } + + // + // Scenario logic + // + switch (TheCSBots()->GetScenario()) + { + //====================================================================================================== + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + // if this is a bomb game and we have the bomb, go plant it + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + if (me->GetGameState()->IsBombPlanted()) + { + if (me->GetGameState()->GetPlantedBombsite() != CSGameState::UNKNOWN) + { + // T's always know where the bomb is - go defend it + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( me->GetGameState()->GetPlantedBombsite() ); + if (zone) + { + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + + Place place = TheNavMesh->GetPlace( zone->m_center ); + if (place != UNDEFINED_PLACE) + { + // pick a random hiding spot in this place + const Vector *spot = FindRandomHidingSpot( me, place, me->IsSniper() ); + if (spot) + { + me->Hide( *spot ); + return; + } + } + + // hide nearby + me->Hide( TheNavMesh->GetNearestNavArea( zone->m_center ) ); + return; + } + } + else + { + // ask our teammates where the bomb is + me->GetChatter()->RequestBombLocation(); + + // we dont know where the bomb is - we must search the bombsites + int zoneIndex = me->GetGameState()->GetNextBombsiteToSearch(); + + // move to bombsite - if we reach it, we'll update its cleared status, causing us to select another + const Vector *pos = TheCSBots()->GetRandomPositionInZone( TheCSBots()->GetZone( zoneIndex ) ); + if (pos) + { + me->SetTask( CCSBot::FIND_TICKING_BOMB ); + me->MoveTo( *pos ); + return; + } + } + } + else if (me->HasC4()) + { + // if we're at a bomb site, plant the bomb + if (me->IsAtBombsite()) + { + // plant it + me->SetTask( CCSBot::PLANT_BOMB ); + me->PlantBomb(); + + // radio to the team + me->GetChatter()->PlantingTheBomb( me->GetPlace() ); + + return; + } + else if (TheCSBots()->IsTimeToPlantBomb()) + { + // move to the closest bomb site + const CCSBotManager::Zone *zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me ) ); + if (zone) + { + // pick a random spot within the bomb zone + const Vector *pos = TheCSBots()->GetRandomPositionInZone( zone ); + if (pos) + { + // move to bombsite + me->SetTask( CCSBot::PLANT_BOMB ); + me->Run(); + me->MoveTo( *pos ); + + return; + } + } + } + } + else + { + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // small chance of sniper camping on offense, if we aren't carrying the bomb + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + + // if the bomb is loose (on the ground), go get it + if (me->NoticeLooseBomb()) + { + me->FetchBomb(); + return; + } + + // if bomb has been planted, and we hear it, move to a hiding spot near the bomb and guard it + if (!me->IsRogue() && me->GetGameState()->IsBombPlanted() && me->GetGameState()->GetBombPosition()) + { + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + + if (bombPos) + { + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + return; + } + } + } + } + else // CT ------------------------------------------------------------------------------------------ + { + if (me->GetGameState()->IsBombPlanted()) + { + // if the bomb has been planted, attempt to defuse it + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (bombPos) + { + // if someone is defusing the bomb, guard them + if (TheCSBots()->GetBombDefuser()) + { + if (!me->IsRogue()) + { + me->SetTask( CCSBot::GUARD_BOMB_DEFUSER ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + return; + } + } + else if (me->IsDoingScenario()) + { + // move to the bomb and defuse it + me->SetTask( CCSBot::DEFUSE_BOMB ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->MoveTo( *bombPos ); + return; + } + else + { + // we're not allowed to defuse, guard the bomb zone + me->SetTask( CCSBot::GUARD_BOMB_ZONE ); + me->Hide( TheNavMesh->GetNavArea( *bombPos ) ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + else if (me->GetGameState()->GetPlantedBombsite() != CSGameState::UNKNOWN) + { + // we know which bombsite, but not exactly where the bomb is, go there + const CCSBotManager::Zone *zone = TheCSBots()->GetZone( me->GetGameState()->GetPlantedBombsite() ); + if (zone) + { + if (me->IsDoingScenario()) + { + me->SetTask( CCSBot::DEFUSE_BOMB ); + me->MoveTo( zone->m_center ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + else + { + // we're not allowed to defuse, guard the bomb zone + me->SetTask( CCSBot::GUARD_BOMB_ZONE ); + me->Hide( TheNavMesh->GetNavArea( zone->m_center ) ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + else + { + // we dont know where the bomb is - we must search the bombsites + + // find closest un-cleared bombsite + const CCSBotManager::Zone *zone = NULL; + float travelDistance = 9999999.9f; + + for( int z=0; z<TheCSBots()->GetZoneCount(); ++z ) + { + if (TheCSBots()->GetZone(z)->m_areaCount == 0) + continue; + + // don't check bombsites that have been cleared + if (me->GetGameState()->IsBombsiteClear( z )) + continue; + + // just use the first overlapping nav area as a reasonable approximation + ShortestPathCost cost = ShortestPathCost(); + float dist = NavAreaTravelDistance( me->GetLastKnownArea(), + TheNavMesh->GetNearestNavArea( TheCSBots()->GetZone(z)->m_center ), + cost ); + + if (dist >= 0.0f && dist < travelDistance) + { + zone = TheCSBots()->GetZone(z); + travelDistance = dist; + } + } + + + if (zone) + { + const float farAwayRange = 2000.0f; + if (travelDistance > farAwayRange) + { + zone = NULL; + } + } + + // if closest bombsite is "far away", pick one at random + if (zone == NULL) + { + int zoneIndex = me->GetGameState()->GetNextBombsiteToSearch(); + zone = TheCSBots()->GetZone( zoneIndex ); + } + + // move to bombsite - if we reach it, we'll update its cleared status, causing us to select another + if (zone) + { + const Vector *pos = TheCSBots()->GetRandomPositionInZone( zone ); + if (pos) + { + me->SetTask( CCSBot::FIND_TICKING_BOMB ); + me->MoveTo( *pos ); + return; + } + } + } + AssertMsg( 0, "A CT bot doesn't know what to do while the bomb is planted!\n" ); + } + + + // if we have a sniper rifle, we like to camp, whether rogue or not + if (me->IsSniper() && !me->IsSafe()) + { + if (RandomFloat( 0, 100 ) <= defenseSniperCampChance) + { + CNavArea *snipingArea = NULL; + + // if the bomb is loose, snipe near it + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (me->GetGameState()->IsLooseBombLocationKnown() && bombPos) + { + snipingArea = TheNavMesh->GetNearestNavArea( *bombPos ); + me->PrintIfWatched( "Sniping near loose bomb\n" ); + } + else + { + // snipe bomb zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + snipingArea = TheCSBots()->GetRandomAreaInZone( zone ); + me->PrintIfWatched( "Sniping near bombsite\n" ); + } + } + + if (snipingArea) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( snipingArea, -1.0, sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // rogues just hunt, unless they want to snipe + // if the whole team has decided to rush, hunt + // if we know the bomb is dropped, hunt for enemies and the loose bomb + if (me->IsRogue() || TheCSBots()->IsDefenseRushing() || me->GetGameState()->IsLooseBombLocationKnown()) + { + me->Hunt(); + return; + } + + // the lower our morale gets, the more we want to camp the bomb zone(s) + // only decide to camp at the start of the round, or if we haven't seen anything for a long time + if (me->IsSafe() || me->HasNotSeenEnemyForLongTime()) + { + float guardBombsiteChance = -34.0f * me->GetMorale(); + + if (RandomFloat( 0.0f, 100.0f ) < guardBombsiteChance) + { + float guardRange = 500.0f + 100.0f * (me->GetMorale() + 3); + + // guard bomb zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + me->PrintIfWatched( "I'm guarding a bombsite\n" ); + me->GetChatter()->GuardingBombsite( area->GetPlace() ); + me->SetTask( CCSBot::GUARD_BOMB_ZONE ); + me->Hide( area, -1.0, guardRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + } + } + + break; + } + + //====================================================================================================== + case CCSBotManager::SCENARIO_ESCORT_VIP: + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + // if we have a sniper rifle, we like to camp, whether rogue or not + if (me->IsSniper()) + { + if (RandomFloat( 0, 100 ) <= defenseSniperCampChance) + { + // snipe escape zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( area, -1.0, sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping near escape zone\n" ); + return; + } + } + } + } + + // rogues just hunt, unless they want to snipe + // if the whole team has decided to rush, hunt + if (me->IsRogue() || TheCSBots()->IsDefenseRushing()) + break; + + // the lower our morale gets, the more we want to camp the escape zone(s) + float guardEscapeZoneChance = -34.0f * me->GetMorale(); + + if (RandomFloat( 0.0f, 100.0f ) < guardEscapeZoneChance) + { + // guard escape zone(s) + const CCSBotManager::Zone *zone = TheCSBots()->GetRandomZone(); + if (zone) + { + CNavArea *area = TheCSBots()->GetRandomAreaInZone( zone ); + if (area) + { + // guard the escape zone - stay closer if our morale is low + me->SetTask( CCSBot::GUARD_VIP_ESCAPE_ZONE ); + me->PrintIfWatched( "I'm guarding an escape zone\n" ); + + float escapeGuardRange = 750.0f + 250.0f * (me->GetMorale() + 3); + me->Hide( area, -1.0, escapeGuardRange ); + + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + } + else // CT + { + if (me->m_bIsVIP) + { + // if early in round, pick a random zone, otherwise pick closest zone + const float earlyTime = 20.0f; + const CCSBotManager::Zone *zone = NULL; + + if (TheCSBots()->GetElapsedRoundTime() < earlyTime) + { + // pick random zone + zone = TheCSBots()->GetRandomZone(); + } + else + { + // pick closest zone + zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me ) ); + } + + if (zone) + { + // pick a random spot within the escape zone + const Vector *pos = TheCSBots()->GetRandomPositionInZone( zone ); + if (pos) + { + // move to escape zone + me->SetTask( CCSBot::VIP_ESCAPE ); + me->Run(); + me->MoveTo( *pos ); + + // tell team to follow + const float repeatTime = 30.0f; + if (me->GetFriendsRemaining() && + TheCSBots()->GetRadioMessageInterval( RADIO_FOLLOW_ME, me->GetTeamNumber() ) > repeatTime) + me->SendRadioMessage( RADIO_FOLLOW_ME ); + return; + } + } + } + else + { + // small chance of sniper camping on offense, if we aren't VIP + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + } + } + break; + } + + //====================================================================================================== + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + if (me->GetTeamNumber() == TEAM_TERRORIST) + { + bool campHostages; + + // if we are in early game, camp the hostages + if (me->IsSafe()) + { + campHostages = true; + } + else if (me->GetGameState()->HaveSomeHostagesBeenTaken() || me->GetGameState()->AreAllHostagesBeingRescued()) + { + campHostages = false; + } + else + { + // later in the game, camp either hostages or escape zone + const float campZoneChance = 100.0f * (TheCSBots()->GetElapsedRoundTime() - me->GetSafeTime())/120.0f; + + campHostages = (RandomFloat( 0, 100 ) > campZoneChance) ? true : false; + } + + + // if we have a sniper rifle, we like to camp, whether rogue or not + if (me->IsSniper()) + { + // the at start of the round, snipe the initial rush + if (me->IsSafe()) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm sniping an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + + if (RandomFloat( 0, 100 ) <= defenseSniperCampChance) + { + const Vector *hostagePos = me->GetGameState()->GetRandomFreeHostagePosition(); + if (hostagePos && campHostages) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->PrintIfWatched( "Sniping near hostages\n" ); + me->Hide( TheNavMesh->GetNearestNavArea( *hostagePos ), -1.0, sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + else + { + // camp the escape zone(s) + if (me->GuardRandomZone( sniperHideRange )) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->PrintIfWatched( "Sniping near a rescue zone\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + } + + // if safe time is up, and we stumble across a hostage, guard it + if (!me->IsSafe() && !me->IsRogue()) + { + CBaseEntity *hostage = me->GetGameState()->GetNearestVisibleFreeHostage(); + if (hostage) + { + // we see a free hostage, guard it + CNavArea *area = TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ); + if (area) + { + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm guarding hostages I found\n" ); + // don't chatter here - he'll tell us when he's in his hiding spot + return; + } + } + } + + + // decide if we want to hunt, or guard + const float huntChance = 70.0f + 25.0f * me->GetMorale(); + + // rogues just hunt, unless they want to snipe + // if the whole team has decided to rush, hunt + if (me->GetFriendsRemaining()) + { + if (me->IsRogue() || TheCSBots()->IsDefenseRushing() || RandomFloat( 0, 100 ) < huntChance) + { + me->Hunt(); + return; + } + } + + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + + // decide whether to camp the hostages or the escape zones + const Vector *hostagePos = me->GetGameState()->GetRandomFreeHostagePosition(); + if (hostagePos && campHostages) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( *hostagePos ); + if (area) + { + // guard the hostages - stay closer to hostages if our morale is low + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->PrintIfWatched( "I'm guarding hostages\n" ); + + float hostageGuardRange = 750.0f + 250.0f * (me->GetMorale() + 3); // 2000 + me->Hide( area, -1.0, hostageGuardRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + + if (RandomFloat( 0, 100 ) < 50) + me->GetChatter()->GuardingHostages( area->GetPlace(), IS_PLAN ); + + return; + } + } + + // guard rescue zone(s) + if (me->GuardRandomZone()) + { + me->SetTask( CCSBot::GUARD_HOSTAGE_RESCUE_ZONE ); + me->PrintIfWatched( "I'm guarding a rescue zone\n" ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->GetChatter()->GuardingHostageEscapeZone( IS_PLAN ); + return; + } + } + else // CT --------------------------------------------------------------------------------- + { + // only decide to do something else if we aren't already rescuing hostages + if (!me->GetHostageEscortCount()) + { + // small chance of sniper camping on offense + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + + if (me->GetFriendsRemaining() && !me->GetHostageEscortCount()) + { + // rogues just hunt, unless all friends are dead + // if we have friends left, we might go hunting instead of hostage rescuing + const float huntChance = 33.3f; + if (me->IsRogue() || RandomFloat( 0.0f, 100.0f ) < huntChance) + { + me->Hunt(); + return; + } + } + } + + // at the start of the round, we may decide to defend "initial encounter" areas + // where we will first meet the enemy rush + if (me->IsSafe()) + { + float defendRushChance = -17.0f * (me->GetMorale() - 2); + + if (me->IsSniper() || RandomFloat( 0.0f, 100.0f ) < defendRushChance) + { + if (me->MoveToInitialEncounter()) + { + me->PrintIfWatched( "I'm guarding an initial encounter area\n" ); + me->SetTask( CCSBot::GUARD_INITIAL_ENCOUNTER ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + return; + } + } + } + + // look for free hostages - CT's have radar so they know where hostages are at all times + CHostage *hostage = me->GetGameState()->GetNearestFreeHostage(); + + // if we are not allowed to do the scenario, guard the hostages to clear the area for the human(s) + if (!me->IsDoingScenario()) + { + if (hostage) + { + CNavArea *area = TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ); + if (area) + { + me->SetTask( CCSBot::GUARD_HOSTAGES ); + me->Hide( area ); + me->PrintIfWatched( "I'm securing the hostages for a human to rescue\n" ); + return; + } + } + + me->Hunt(); + return; + } + + + bool fetchHostages = false; + bool rescueHostages = false; + const CCSBotManager::Zone *zone = NULL; + me->SetGoalEntity( NULL ); + + // if we are escorting hostages, determine where to take them + if (me->GetHostageEscortCount()) + zone = TheCSBots()->GetClosestZone( me->GetLastKnownArea(), PathCost( me, FASTEST_ROUTE ) ); + + // if we are escorting hostages and there are more hostages to rescue, + // determine whether it's faster to rescue the ones we have, or go get the remaining ones + if (hostage) + { + Vector hostageOrigin = GetCentroid( hostage ); + + if (zone) + { + PathCost cost( me, FASTEST_ROUTE ); + float toZone = NavAreaTravelDistance( me->GetLastKnownArea(), zone->m_area[0], cost ); + float toHostage = NavAreaTravelDistance( me->GetLastKnownArea(), TheNavMesh->GetNearestNavArea( GetCentroid( hostage ) ), cost ); + + if (toHostage < 0.0f) + { + rescueHostages = true; + } + else + { + if (toZone < toHostage) + rescueHostages = true; + else + fetchHostages = true; + } + } + else + { + fetchHostages = true; + } + } + else if (zone) + { + rescueHostages = true; + } + + + if (fetchHostages) + { + // go get hostages + me->SetTask( CCSBot::COLLECT_HOSTAGES ); + me->Run(); + me->SetGoalEntity( hostage ); + me->ResetWaitForHostagePatience(); + + // if we already have some hostages, move to the others by the quickest route + RouteType route = (me->GetHostageEscortCount()) ? FASTEST_ROUTE : SAFEST_ROUTE; + me->MoveTo( GetCentroid( hostage ), route ); + + me->PrintIfWatched( "I'm collecting hostages\n" ); + return; + } + + const Vector *zonePos = TheCSBots()->GetRandomPositionInZone( zone ); + if (rescueHostages && zonePos) + { + me->SetTask( CCSBot::RESCUE_HOSTAGES ); + me->Run(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + me->MoveTo( *zonePos, FASTEST_ROUTE ); + me->PrintIfWatched( "I'm rescuing hostages\n" ); + me->GetChatter()->EscortingHostages(); + return; + } + } + break; + } + + default: // deathmatch + { + // sniping check + if (me->GetFriendsRemaining() && me->IsSniper() && RandomFloat( 0, 100.0f ) < offenseSniperCampChance) + { + me->SetTask( CCSBot::MOVE_TO_SNIPER_SPOT ); + me->Hide( me->GetLastKnownArea(), RandomFloat( 10.0f, 30.0f ), sniperHideRange ); + me->SetDisposition( CCSBot::OPPORTUNITY_FIRE ); + me->PrintIfWatched( "Sniping!\n" ); + return; + } + break; + } + } + + // if we have nothing special to do, go hunting for enemies + me->Hunt(); +} + diff --git a/game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp b/game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp new file mode 100644 index 0000000..9aa3dd0 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp @@ -0,0 +1,139 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move towards currently heard noise + */ +void InvestigateNoiseState::AttendCurrentNoise( CCSBot *me ) +{ + if (!me->IsNoiseHeard() && me->GetNoisePosition()) + return; + + // remember where the noise we heard was + m_checkNoisePosition = *me->GetNoisePosition(); + + // tell our teammates (unless the noise is obvious, like gunfire) + if (me->IsWellPastSafe() && me->HasNotSeenEnemyForLongTime() && me->GetNoisePriority() != PRIORITY_HIGH) + me->GetChatter()->HeardNoise( *me->GetNoisePosition() ); + + // figure out how to get to the noise + me->PrintIfWatched( "Attending to noise...\n" ); + me->ComputePath( m_checkNoisePosition, FASTEST_ROUTE ); + + const float minAttendTime = 3.0f; + const float maxAttendTime = 10.0f; + m_minTimer.Start( RandomFloat( minAttendTime, maxAttendTime ) ); + + // consume the noise + me->ForgetNoise(); +} + +//-------------------------------------------------------------------------------------------------------------- +void InvestigateNoiseState::OnEnter( CCSBot *me ) +{ + AttendCurrentNoise( me ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * @todo Use TravelDistance instead of distance... + */ +void InvestigateNoiseState::OnUpdate( CCSBot *me ) +{ + Vector myOrigin = GetCentroid( me ); + + // keep an ear out for closer noises... + if (m_minTimer.IsElapsed()) + { + const float nearbyRange = 500.0f; + if (me->HeardInterestingNoise() && me->GetNoiseRange() < nearbyRange) + { + // new sound is closer + AttendCurrentNoise( me ); + } + } + + + // if the pathfind fails, give up + if (!me->HasPath()) + { + me->Idle(); + return; + } + + // look around + me->UpdateLookAround(); + + // get distance remaining on our path until we reach the source of the noise + float range = me->GetPathDistanceRemaining(); + + if (me->IsUsingKnife()) + { + if (me->IsHurrying()) + me->Run(); + else + me->Walk(); + } + else + { + const float closeToNoiseRange = 1500.0f; + if (range < closeToNoiseRange) + { + // if we dont have many friends left, or we are alone, and we are near noise source, sneak quietly + if ((me->GetNearbyFriendCount() == 0 || me->GetFriendsRemaining() <= 2) && !me->IsHurrying()) + { + me->Walk(); + } + else + { + me->Run(); + } + } + else + { + me->Run(); + } + } + + + // if we can see the noise position and we're close enough to it and looking at it, + // we don't need to actually move there (it's checked enough) + const float closeRange = 500.0f; + if (range < closeRange) + { + if (me->IsVisible( m_checkNoisePosition, CHECK_FOV )) + { + // can see noise position + me->PrintIfWatched( "Noise location is clear.\n" ); + me->ForgetNoise(); + me->Idle(); + return; + } + } + + // move towards noise + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + me->Idle(); + } +} + +//-------------------------------------------------------------------------------------------------------------- +void InvestigateNoiseState::OnExit( CCSBot *me ) +{ + // reset to run mode in case we were sneaking about + me->Run(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_move_to.cpp b/game/server/cstrike/bot/states/cs_bot_move_to.cpp new file mode 100644 index 0000000..eafba40 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_move_to.cpp @@ -0,0 +1,366 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_simple_hostage.h" +#include "cs_bot.h" +#include "cs_gamerules.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a potentially far away position. + */ +void MoveToState::OnEnter( CCSBot *me ) +{ + if (me->IsUsingKnife() && me->IsWellPastSafe() && !me->IsHurrying()) + { + me->Walk(); + } + else + { + me->Run(); + } + + + // if we need to find the bomb, get there as quick as we can + RouteType route; + switch (me->GetTask()) + { + case CCSBot::FIND_TICKING_BOMB: + case CCSBot::DEFUSE_BOMB: + case CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION: + route = FASTEST_ROUTE; + break; + + default: + route = SAFEST_ROUTE; + break; + } + + // build path to, or nearly to, goal position + me->ComputePath( m_goalPosition, route ); + + m_radioedPlan = false; + m_askedForCover = false; +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Move to a potentially far away position. + */ +void MoveToState::OnUpdate( CCSBot *me ) +{ + Vector myOrigin = GetCentroid( me ); + + // assume that we are paying attention and close enough to know our enemy died + if (me->GetTask() == CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION) + { + /// @todo Account for reaction time so we take some time to realized the enemy is dead + CBasePlayer *victim = static_cast<CBasePlayer *>( me->GetTaskEntity() ); + if (victim == NULL || !victim->IsAlive()) + { + me->PrintIfWatched( "The enemy I was chasing was killed - giving up.\n" ); + me->Idle(); + return; + } + } + + // look around + me->UpdateLookAround(); + + // + // Scenario logic + // + switch (TheCSBots()->GetScenario()) + { + case CCSBotManager::SCENARIO_DEFUSE_BOMB: + { + // if the bomb has been planted, find it + // NOTE: This task is used by both CT and T's to find the bomb + if (me->GetTask() == CCSBot::FIND_TICKING_BOMB) + { + if (!me->GetGameState()->IsBombPlanted()) + { + // the bomb is not planted - give up this task + me->Idle(); + return; + } + + if (me->GetGameState()->GetPlantedBombsite() != CSGameState::UNKNOWN) + { + // we know where the bomb is planted, stop searching + me->Idle(); + return; + } + + // check off bombsites that we explore or happen to stumble into + for( int z=0; z<TheCSBots()->GetZoneCount(); ++z ) + { + // don't re-check zones + if (me->GetGameState()->IsBombsiteClear( z )) + continue; + + if (TheCSBots()->GetZone(z)->m_extent.Contains( myOrigin )) + { + // note this bombsite is clear + me->GetGameState()->ClearBombsite( z ); + + if (me->GetTeamNumber() == TEAM_CT) + { + // tell teammates this bombsite is clear + me->GetChatter()->BombsiteClear( z ); + } + + // find another zone to check + me->Idle(); + + return; + } + } + + // move to a bombsite + break; + } + + + if (me->GetTeamNumber() == TEAM_CT) + { + if (me->GetGameState()->IsBombPlanted()) + { + switch( me->GetTask() ) + { + case CCSBot::DEFUSE_BOMB: + { + // if we are near the bombsite and there is time left, sneak in (unless all enemies are dead) + if (me->GetEnemiesRemaining()) + { + const float plentyOfTime = 15.0f; + if (TheCSBots()->GetBombTimeLeft() > plentyOfTime) + { + // get distance remaining on our path until we reach the bombsite + float range = me->GetPathDistanceRemaining(); + + const float closeRange = 1500.0f; + if (range < closeRange) + { + me->Walk(); + } + else + { + me->Run(); + } + } + } + else + { + // everyone is dead - run! + me->Run(); + } + + // if we are trying to defuse the bomb, and someone has started defusing, guard them instead + if (me->CanSeePlantedBomb() && TheCSBots()->GetBombDefuser()) + { + me->GetChatter()->Say( "CoveringFriend" ); + me->Idle(); + return; + } + + + // if we are near the bomb, defuse it (if we are reloading, don't try to defuse until we finish) + const Vector *bombPos = me->GetGameState()->GetBombPosition(); + if (bombPos && !me->IsReloading()) + { + const float defuseRange = 100.0f; // 50 + if ((*bombPos - me->EyePosition()).IsLengthLessThan( defuseRange )) + { + // make sure we can see the bomb + if (me->IsVisible( *bombPos )) + { + me->DefuseBomb(); + return; + } + } + } + + break; + } + + default: + { + // we need to find the bomb + me->Idle(); + return; + } + } + } + } + else // TERRORIST + { + if (me->GetTask() == CCSBot::PLANT_BOMB ) + { + if ( me->GetFriendsRemaining() ) + { + // if we are about to plant, radio for cover + if (!m_askedForCover) + { + const float nearPlantSite = 50.0f; + if (me->IsAtBombsite() && me->GetPathDistanceRemaining() < nearPlantSite) + { + // radio to the team + me->GetChatter()->PlantingTheBomb( me->GetPlace() ); + m_askedForCover = true; + } + + // after we have started to move to the bombsite, tell team we're going to plant, and where + // don't do this if we have already radioed that we are starting to plant + if (!m_radioedPlan) + { + const float radioTime = 2.0f; + if (gpGlobals->curtime - me->GetStateTimestamp() > radioTime) + { + // radio to the team if we're more than 10 seconds (2400 units) out + const float nearPlantSite = 2400.0f; + if ( me->GetPathDistanceRemaining() >= nearPlantSite ) + { + me->GetChatter()->GoingToPlantTheBomb( TheNavMesh->GetPlace( m_goalPosition ) ); + } + m_radioedPlan = true; + } + } + } + } + } + } + break; + } + + //-------------------------------------------------------------------------------------------------- + case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: + { + if (me->GetTask() == CCSBot::COLLECT_HOSTAGES) + { + // + // Since CT's have a radar, they can directly look at the actual hostage state + // + + // check if someone else collected our hostage, or the hostage died or was rescued + CHostage *hostage = static_cast<CHostage *>( me->GetGoalEntity() ); + if (hostage == NULL || !hostage->IsValid() || hostage->IsFollowingSomeone()) + { + me->Idle(); + return; + } + + Vector hostageOrigin = GetCentroid( hostage ); + + // if our hostage has moved, repath + const float repathToleranceSq = 75.0f * 75.0f; + float error = (hostageOrigin - m_goalPosition).LengthSqr(); + if (error > repathToleranceSq) + { + m_goalPosition = hostageOrigin; + me->ComputePath( m_goalPosition, SAFEST_ROUTE ); + } + + /// @todo Generalize ladder priorities over other tasks + if (!me->IsUsingLadder()) + { + Vector pos = hostage->EyePosition(); + Vector to = pos - me->EyePosition(); // "Use" checks from eye position, so we should too + + // look at the hostage as we approach + const float watchHostageRange = 100.0f; + if (to.IsLengthLessThan( watchHostageRange )) + { + me->SetLookAt( "Hostage", pos, PRIORITY_LOW, 0.5f ); + + // randomly move just a bit to avoid infinite use loops from bad hostage placement + NavRelativeDirType dir = (NavRelativeDirType)RandomInt( 0, 3 ); + switch( dir ) + { + case LEFT: me->StrafeLeft(); break; + case RIGHT: me->StrafeRight(); break; + case FORWARD: me->MoveForward(); break; + case BACKWARD: me->MoveBackward(); break; + } + + // check if we are close enough to the hostage to talk to him + const float useRange = PLAYER_USE_RADIUS - 10.0f; // shave off a fudge factor to make sure we're within range + if (to.IsLengthLessThan( useRange )) + { + me->UseEntity( me->GetGoalEntity() ); + return; + } + } + } + } + else if (me->GetTask() == CCSBot::RESCUE_HOSTAGES) + { + // periodically check if we lost all our hostages + if (me->GetHostageEscortCount() == 0) + { + // lost our hostages - go get 'em + me->Idle(); + return; + } + } + + break; + } + } + + + if (me->UpdatePathMovement() != CCSBot::PROGRESSING) + { + // reached destination + switch( me->GetTask() ) + { + case CCSBot::PLANT_BOMB: + // if we are at bombsite with the bomb, plant it + if (me->IsAtBombsite() && me->HasC4()) + { + me->PlantBomb(); + return; + } + break; + + case CCSBot::MOVE_TO_LAST_KNOWN_ENEMY_POSITION: + { + CBasePlayer *victim = static_cast<CBasePlayer *>( me->GetTaskEntity() ); + if (victim && victim->IsAlive()) + { + // if we got here and haven't re-acquired the enemy, we lost him + BotStatement *say = new BotStatement( me->GetChatter(), REPORT_ENEMY_LOST, 8.0f ); + + say->AppendPhrase( TheBotPhrases->GetPhrase( "LostEnemy" ) ); + say->SetStartTime( gpGlobals->curtime + RandomFloat( 3.0f, 5.0f ) ); + + me->GetChatter()->AddStatement( say ); + } + break; + } + } + + // default behavior when destination is reached + me->Idle(); + return; + } +} + +//-------------------------------------------------------------------------------------------------------------- +void MoveToState::OnExit( CCSBot *me ) +{ + // reset to run in case we were walking near our goal position + me->Run(); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + //me->StopAiming(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_open_door.cpp b/game/server/cstrike/bot/states/cs_bot_open_door.cpp new file mode 100644 index 0000000..a945a6c --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_open_door.cpp @@ -0,0 +1,94 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), April 2005 + +#include "cbase.h" +#include "cs_bot.h" +#include "BasePropDoor.h" +#include "doors.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +//------------------------------------------------------------------------------------------------- +/** + * Face the door and open it. + * NOTE: This state assumes we are standing in range of the door to be opened, with no obstructions. + */ +void OpenDoorState::OnEnter( CCSBot *me ) +{ + m_isDone = false; + m_timeout.Start( 1.0f ); +} + + +//------------------------------------------------------------------------------------------------- +void OpenDoorState::SetDoor( CBaseEntity *door ) +{ + CBaseDoor *funcDoor = dynamic_cast< CBaseDoor * >(door); + if ( funcDoor ) + { + m_funcDoor = funcDoor; + return; + } + + CBasePropDoor *propDoor = dynamic_cast< CBasePropDoor * >(door); + if ( propDoor ) + { + m_propDoor = propDoor; + return; + } +} + + +//------------------------------------------------------------------------------------------------- +void OpenDoorState::OnUpdate( CCSBot *me ) +{ + me->ResetStuckMonitor(); + + // wait for door to swing open before leaving state + if (m_timeout.IsElapsed()) + { + m_isDone = true; + return; + } + + // look at the door + Vector pos; + bool isDoorMoving = false; + if ( m_funcDoor ) + { + pos = m_funcDoor->WorldSpaceCenter(); + isDoorMoving = m_funcDoor->m_toggle_state == TS_GOING_UP || m_funcDoor->m_toggle_state == TS_GOING_DOWN; + } + else + { + pos = m_propDoor->WorldSpaceCenter(); + isDoorMoving = m_propDoor->IsDoorOpening() || m_propDoor->IsDoorClosing(); + } + + me->SetLookAt( "Open door", pos, PRIORITY_HIGH ); + + // if we are looking at the door, "use" it and exit + if (me->IsLookingAtPosition( pos )) + { + me->UseEnvironment(); + } +} + + +//------------------------------------------------------------------------------------------------- +void OpenDoorState::OnExit( CCSBot *me ) +{ + me->ClearLookAt(); + me->ResetStuckMonitor(); +} + + + diff --git a/game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp b/game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp new file mode 100644 index 0000000..fe45a3d --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp @@ -0,0 +1,79 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//-------------------------------------------------------------------------------------------------------------- +/** + * Plant the bomb. + */ +void PlantBombState::OnEnter( CCSBot *me ) +{ + me->Crouch(); + me->SetDisposition( CCSBot::SELF_DEFENSE ); + + // look at the floor +// Vector down( myOrigin.x, myOrigin.y, -1000.0f ); + + float yaw = me->EyeAngles().y; + Vector2D dir( BotCOS(yaw), BotSIN(yaw) ); + Vector myOrigin = GetCentroid( me ); + + Vector down( myOrigin.x + 10.0f * dir.x, myOrigin.y + 10.0f * dir.y, me->GetFeetZ() ); + me->SetLookAt( "Plant bomb on floor", down, PRIORITY_HIGH ); +} + +//-------------------------------------------------------------------------------------------------------------- +/** + * Plant the bomb. + */ +void PlantBombState::OnUpdate( CCSBot *me ) +{ + CBaseCombatWeapon *gun = me->GetActiveWeapon(); + bool holdingC4 = false; + if (gun) + { + if (FStrEq( gun->GetClassname(), "weapon_c4" )) + holdingC4 = true; + } + + // if we aren't holding the C4, grab it, otherwise plant it + if (holdingC4) + me->PrimaryAttack(); + else + me->SelectItem( "weapon_c4" ); + + // if we no longer have the C4, we've successfully planted + if (!me->HasC4()) + { + // move to a hiding spot and watch the bomb + me->SetTask( CCSBot::GUARD_TICKING_BOMB ); + me->Hide(); + } + + // if we time out, it's because we slipped into a non-plantable area + const float timeout = 5.0f; + if (gpGlobals->curtime - me->GetStateTimestamp() > timeout) + me->Idle(); +} + +//-------------------------------------------------------------------------------------------------------------- +void PlantBombState::OnExit( CCSBot *me ) +{ + // equip our rifle (in case we were interrupted while holding C4) + me->EquipBestWeapon(); + me->StandUp(); + me->ResetStuckMonitor(); + me->SetDisposition( CCSBot::ENGAGE_AND_INVESTIGATE ); + me->ClearLookAt(); +} diff --git a/game/server/cstrike/bot/states/cs_bot_use_entity.cpp b/game/server/cstrike/bot/states/cs_bot_use_entity.cpp new file mode 100644 index 0000000..591b364 --- /dev/null +++ b/game/server/cstrike/bot/states/cs_bot_use_entity.cpp @@ -0,0 +1,63 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +// Author: Michael S. Booth ([email protected]), 2003 + +#include "cbase.h" +#include "cs_bot.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +/** + * Face the entity and "use" it + * NOTE: This state assumes we are standing in range of the entity to be used, with no obstructions. + */ +void UseEntityState::OnEnter( CCSBot *me ) +{ +} + +void UseEntityState::OnUpdate( CCSBot *me ) +{ + // in the very rare situation where two or more bots "used" a hostage at the same time, + // one bot will fail and needs to time out of this state + const float useTimeout = 5.0f; + if (me->GetStateTimestamp() - gpGlobals->curtime > useTimeout) + { + me->Idle(); + return; + } + + // look at the entity + Vector pos = m_entity->EyePosition(); + me->SetLookAt( "Use entity", pos, PRIORITY_HIGH ); + + // if we are looking at the entity, "use" it and exit + if (me->IsLookingAtPosition( pos )) + { + if (TheCSBots()->GetScenario() == CCSBotManager::SCENARIO_RESCUE_HOSTAGES && + me->GetTeamNumber() == TEAM_CT && + me->GetTask() == CCSBot::COLLECT_HOSTAGES) + { + // we are collecting a hostage, assume we were successful - the update check will correct us if we weren't + me->IncreaseHostageEscortCount(); + } + + me->UseEnvironment(); + me->Idle(); + } +} + +void UseEntityState::OnExit( CCSBot *me ) +{ + me->ClearLookAt(); + me->ResetStuckMonitor(); +} + + + |