summaryrefslogtreecommitdiff
path: root/game/server/cstrike/bot
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/cstrike/bot')
-rw-r--r--game/server/cstrike/bot/cs_bot.cpp1117
-rw-r--r--game/server/cstrike/bot/cs_bot.h2004
-rw-r--r--game/server/cstrike/bot/cs_bot_chatter.cpp2582
-rw-r--r--game/server/cstrike/bot/cs_bot_chatter.h656
-rw-r--r--game/server/cstrike/bot/cs_bot_event.cpp427
-rw-r--r--game/server/cstrike/bot/cs_bot_event_bomb.cpp158
-rw-r--r--game/server/cstrike/bot/cs_bot_event_player.cpp227
-rw-r--r--game/server/cstrike/bot/cs_bot_event_weapon.cpp167
-rw-r--r--game/server/cstrike/bot/cs_bot_init.cpp359
-rw-r--r--game/server/cstrike/bot/cs_bot_listen.cpp230
-rw-r--r--game/server/cstrike/bot/cs_bot_manager.cpp2390
-rw-r--r--game/server/cstrike/bot/cs_bot_manager.h400
-rw-r--r--game/server/cstrike/bot/cs_bot_nav.cpp963
-rw-r--r--game/server/cstrike/bot/cs_bot_pathfind.cpp2075
-rw-r--r--game/server/cstrike/bot/cs_bot_radio.cpp346
-rw-r--r--game/server/cstrike/bot/cs_bot_statemachine.cpp693
-rw-r--r--game/server/cstrike/bot/cs_bot_update.cpp1211
-rw-r--r--game/server/cstrike/bot/cs_bot_vision.cpp1834
-rw-r--r--game/server/cstrike/bot/cs_bot_weapon.cpp1363
-rw-r--r--game/server/cstrike/bot/cs_bot_weapon_id.cpp22
-rw-r--r--game/server/cstrike/bot/cs_gamestate.cpp767
-rw-r--r--game/server/cstrike/bot/cs_gamestate.h151
-rw-r--r--game/server/cstrike/bot/states/cs_bot_attack.cpp710
-rw-r--r--game/server/cstrike/bot/states/cs_bot_buy.cpp690
-rw-r--r--game/server/cstrike/bot/states/cs_bot_defuse_bomb.cpp82
-rw-r--r--game/server/cstrike/bot/states/cs_bot_escape_from_bomb.cpp67
-rw-r--r--game/server/cstrike/bot/states/cs_bot_fetch_bomb.cpp69
-rw-r--r--game/server/cstrike/bot/states/cs_bot_follow.cpp366
-rw-r--r--game/server/cstrike/bot/states/cs_bot_hide.cpp549
-rw-r--r--game/server/cstrike/bot/states/cs_bot_hunt.cpp234
-rw-r--r--game/server/cstrike/bot/states/cs_bot_idle.cpp887
-rw-r--r--game/server/cstrike/bot/states/cs_bot_investigate_noise.cpp139
-rw-r--r--game/server/cstrike/bot/states/cs_bot_move_to.cpp366
-rw-r--r--game/server/cstrike/bot/states/cs_bot_open_door.cpp94
-rw-r--r--game/server/cstrike/bot/states/cs_bot_plant_bomb.cpp79
-rw-r--r--game/server/cstrike/bot/states/cs_bot_use_entity.cpp63
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();
+}
+
+
+