diff options
Diffstat (limited to 'game/server/cstrike/bot/cs_bot_chatter.cpp')
| -rw-r--r-- | game/server/cstrike/bot/cs_bot_chatter.cpp | 2582 |
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 ); +} + + |