summaryrefslogtreecommitdiff
path: root/game/server/cstrike/bot/cs_bot_chatter.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/cstrike/bot/cs_bot_chatter.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/cstrike/bot/cs_bot_chatter.cpp')
-rw-r--r--game/server/cstrike/bot/cs_bot_chatter.cpp2582
1 files changed, 2582 insertions, 0 deletions
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 );
+}
+
+