//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// // Author: Michael S. Booth (mike@turtlerockstudios.com), 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( 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( 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; bankCount(); ++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; iRandomize(); for( i=0; iRandomize(); } 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(GetPlace( SharedGetToken() )); } else { phrase = const_cast(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; im_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; im_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; im_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; iIsImportant()) 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; iIsAttacking()) 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; iIsPlace()) 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; im_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( 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(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 ); }