/* * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * In addition, as a special exception, the author gives permission to * link the code of this program with the Half-Life Game Engine ("HL * Engine") and Modified Game Libraries ("MODs") developed by Valve, * L.L.C ("Valve"). You must obey the GNU General Public License in all * respects for all of the code used other than the HL Engine and MODs * from Valve. If you modify this file, you may extend this exception * to your version of the file, but you are not obligated to do so. If * you do not wish to do so, delete this exception statement from your * version. * */ #include "precompiled.h" #include BotPhraseManager *TheBotPhrases = nullptr; CountdownTimer BotChatterInterface::m_encourageTimer; IntervalTimer BotChatterInterface::m_radioSilenceInterval[2]; const Vector *GetRandomSpotAtPlace(Place place) { int count = 0; int which; for (auto area : TheNavAreaList) { if (area->GetPlace() == place) count++; } if (count == 0) return nullptr; which = RANDOM_LONG(0, count - 1); for (auto area : TheNavAreaList) { if (area->GetPlace() == place && which == 0) return area->GetCenter(); } return nullptr; } // Transmit meme to other bots void BotMeme::Transmit(CCSBot *pSender) const { for (int i = 1; i <= gpGlobals->maxClients; i++) { CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); if (!pPlayer) continue; if (FNullEnt(pPlayer->pev)) continue; if (FStrEq(STRING(pPlayer->pev->netname), "")) continue; // skip self if (pSender == pPlayer) continue; // ignore dead humans if (!pPlayer->IsBot() && !pPlayer->IsAlive()) continue; // ignore enemies, since we can't hear them talk if (pSender->BotRelationship(pPlayer) == CCSBot::BOT_ENEMY) continue; // if not a bot, fail the test if (!pPlayer->IsBot()) continue; // allow bot to interpret our meme Interpret(pSender, (CCSBot *)pPlayer); } } // A teammate called for help - respond void BotHelpMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { const float maxHelpRange = 3000.0f; // 2000 pReceiver->RespondToHelpRequest(pSender, m_place, maxHelpRange); } // A teammate reported information about a bombsite void BotBombsiteStatusMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { // remember this bombsite's status if (m_status == CLEAR) pReceiver->GetGameState()->ClearBombsite(m_zoneIndex); else pReceiver->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 (pReceiver->GetTask() == CCSBot::FIND_TICKING_BOMB) { pReceiver->Idle(); pReceiver->GetChatter()->Affirmative(); } } // A teammate reported information about the bomb void BotBombStatusMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { // update our gamestate based on teammate's report switch (m_state) { case CSGameState::MOVING: { pReceiver->GetGameState()->UpdateBomber(&m_pos); // if we are hunting and see no enemies, respond if (!pReceiver->IsRogue() && pReceiver->IsHunting() && pReceiver->GetNearbyEnemyCount() == 0) pReceiver->RespondToHelpRequest(pSender, TheNavAreaGrid.GetPlace(&m_pos)); break; } case CSGameState::LOOSE: { pReceiver->GetGameState()->UpdateLooseBomb(&m_pos); if (pReceiver->GetTask() == CCSBot::GUARD_BOMB_ZONE) { pReceiver->Idle(); pReceiver->GetChatter()->Affirmative(); } break; } } } // A teammate has asked that we follow him void BotFollowMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { if (pReceiver->IsRogue()) return; // if we're busy, ignore if (pReceiver->IsBusy()) return; PathCost pathCost(pReceiver); float travelDistance = NavAreaTravelDistance(pReceiver->GetLastKnownArea(), TheNavAreaGrid.GetNearestNavArea(&pSender->pev->origin), pathCost); if (travelDistance < 0.0f) return; const float tooFar = 1000.0f; if (travelDistance > tooFar) return; // begin following pReceiver->Follow(pSender); // acknowledge pReceiver->GetChatter()->Say("CoveringFriend"); } // A teammate has asked us to defend a place void BotDefendHereMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { if (pReceiver->IsRogue()) return; // if we're busy, ignore if (pReceiver->IsBusy()) return; Place place = TheNavAreaGrid.GetPlace(&m_pos); if (place != UNDEFINED_PLACE) { // pick a random hiding spot in this place const Vector *spot = FindRandomHidingSpot(pReceiver, place, pReceiver->IsSniper()); if (spot) { pReceiver->SetTask(CCSBot::HOLD_POSITION); pReceiver->Hide(spot); return; } } // hide nearby pReceiver->SetTask(CCSBot::HOLD_POSITION); pReceiver->Hide(TheNavAreaGrid.GetNearestNavArea(&m_pos)); // acknowledge pReceiver->GetChatter()->Say("Affirmative"); } // A teammate has asked where the bomb is planted void BotWhereBombMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { int zone = pReceiver->GetGameState()->GetPlantedBombsite(); if (zone != CSGameState::UNKNOWN) { pReceiver->GetChatter()->FoundPlantedBomb(zone); } } // A teammate has asked us to report in void BotRequestReportMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { pReceiver->GetChatter()->ReportingIn(); } // A teammate told us all the hostages are gone void BotAllHostagesGoneMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { pReceiver->GetGameState()->AllHostagesGone(); // acknowledge pReceiver->GetChatter()->Say("Affirmative"); } // A teammate told us a CT is talking to a hostage void BotHostageBeingTakenMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const { pReceiver->GetGameState()->HostageWasTaken(); // if we're busy, ignore if (pReceiver->IsBusy()) return; pReceiver->Idle(); // acknowledge pReceiver->GetChatter()->Say("Affirmative"); } BotSpeakable::BotSpeakable() { m_phrase = nullptr; } BotSpeakable::~BotSpeakable() { if (m_phrase) { delete[] m_phrase; m_phrase = nullptr; } } BotPhrase::BotPhrase(unsigned int id, bool isPlace) { m_name = nullptr; m_id = id; m_isPlace = isPlace; m_radioEvent = EVENT_INVALID; m_isImportant = false; ClearCriteria(); m_numVoiceBanks = 0; InitVoiceBank(0); } BotPhrase::~BotPhrase() { for (size_t bank = 0; bank < m_voiceBank.size(); bank++) { for (size_t speakable = 0; speakable < m_voiceBank[bank]->size(); speakable++) { delete (*m_voiceBank[bank])[speakable]; } delete m_voiceBank[bank]; } if (m_name) { delete[] m_name; m_name = nullptr; } } void BotPhrase::InitVoiceBank(int bankIndex) { while (m_numVoiceBanks <= bankIndex) { m_count.push_back(0); m_index.push_back(0); m_voiceBank.push_back(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 nullptr; } // 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 == Q_min(m_countCriteria, (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 nullptr; } } return nullptr; } // Randomly shuffle the speakable order void BotPhrase::Randomize() { for (size_t i = 0; i < m_voiceBank.size(); i++) { std::random_shuffle(m_voiceBank[i]->begin(), m_voiceBank[i]->end()); } } BotPhraseManager::BotPhraseManager() { for (int i = 0; i < MAX_PLACES_PER_MAP; i++) m_placeStatementHistory[i].timer.Invalidate(); m_placeCount = 0; } // Invoked when map changes void BotPhraseManager::OnMapChange() { m_placeCount = 0; } // Invoked when the round resets void BotPhraseManager::OnRoundRestart() { // effectively reset all interval timers m_placeCount = 0; // shuffle all the speakables for (auto phrase : m_placeList) phrase->Randomize(); for (auto phrase : m_list) phrase->Randomize(); } // Initialize phrase system from database file bool BotPhraseManager::Initialize(const char *filename, int bankIndex) { bool isDefault = (bankIndex == 0); int phraseDataLength; char *phraseDataFile = (char *)LOAD_FILE_FOR_ME((char *)filename, &phraseDataLength); if (!phraseDataFile) { if (AreBotsAllowed()) { CONSOLE_ECHO("WARNING: Cannot access bot phrase database '%s'\n", filename); } return false; } char *token; char *phraseData = phraseDataFile; unsigned int nextID = 1; // wav filenames need to be shorter than this to go over the net anyway. const int RadioPathLen = 128; char baseDir[RadioPathLen] = ""; char compositeFilename[RadioPathLen]; #ifdef REGAMEDLL_ADD char filePath[MAX_PATH]; #endif // Parse the BotChatter.db into BotPhrase collections while (true) { phraseData = SharedParse(phraseData); if (!phraseData) break; token = SharedGetToken(); if (!Q_stricmp(token, "BaseDir")) { // get name of this output device phraseData = SharedParse(phraseData); if (!phraseData) { CONSOLE_ECHO("Error parsing '%s' - expected identifier\n", filename); FREE_FILE(phraseDataFile); return false; } token = SharedGetToken(); Q_strncpy(baseDir, token, RadioPathLen); baseDir[RadioPathLen - 1] = '\0'; } else if (!Q_stricmp(token, "Place") || !Q_stricmp(token, "Chatter")) { bool isPlace = (Q_stricmp(token, "Place") == 0); // encountered a new phrase collection BotPhrase *phrase = nullptr; if (isDefault) { phrase = new BotPhrase(nextID++, isPlace); } // get name of this phrase phraseData = SharedParse(phraseData); if (!phraseData) { CONSOLE_ECHO("Error parsing '%s' - expected identifier\n", filename); FREE_FILE(phraseDataFile); return false; } token = SharedGetToken(); if (isDefault) { phrase->m_name = CloneString(token); } // look up the existing phrase else { if (isPlace) { phrase = const_cast(GetPlace(token)); } else { phrase = const_cast(GetPhrase(token)); } if (!phrase) { CONSOLE_ECHO("Error parsing '%s' - phrase '%s' is invalid\n", filename, token); FREE_FILE(phraseDataFile); return false; } } phrase->InitVoiceBank(bankIndex); PlaceCriteria placeCriteria = ANY_PLACE; CountCriteria countCriteria = UNDEFINED_COUNT; GameEventType radioEvent = EVENT_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); FREE_FILE(phraseDataFile); return false; } token = SharedGetToken(); // check for Place criteria if (!Q_stricmp(token, "Place")) { phraseData = SharedParse(phraseData); if (!phraseData) { CONSOLE_ECHO("Error parsing %s - expected Place name\n", filename); FREE_FILE(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 (!Q_stricmp("ANY", token)) placeCriteria = ANY_PLACE; else if (!Q_stricmp("UNDEFINED", token)) placeCriteria = UNDEFINED_PLACE; else placeCriteria = TheBotPhrases->NameToID(token); continue; } // check for Count criteria if (!Q_stricmp(token, "Count")) { phraseData = SharedParse(phraseData); if (!phraseData) { CONSOLE_ECHO("Error parsing %s - expected Count value\n", filename); FREE_FILE(phraseDataFile); return false; } token = SharedGetToken(); // update count criteria for subsequent speak lines if (!Q_stricmp(token, "Many")) countCriteria = COUNT_MANY; else countCriteria = Q_atoi(token); continue; } // check for radio equivalent if (!Q_stricmp(token, "Radio")) { phraseData = SharedParse(phraseData); if (!phraseData) { CONSOLE_ECHO("Error parsing %s - expected radio event\n", filename); FREE_FILE(phraseDataFile); return false; } token = SharedGetToken(); GameEventType event = NameToGameEvent(token); if (event <= EVENT_START_RADIO_1 || event >= EVENT_END_RADIO) { CONSOLE_ECHO("Error parsing %s - invalid radio event '%s'\n", filename, token); FREE_FILE(phraseDataFile); return false; } radioEvent = event; continue; } // check for "important" flag if (!Q_stricmp(token, "Important")) { isImportant = true; continue; } // check for End delimiter if (!Q_stricmp(token, "End")) break; #ifdef REGAMEDLL_ADD Q_snprintf(filePath, sizeof(filePath), "sound\\%s%s", baseDir, token); if (!g_pFileSystem->FileExists(filePath)) continue; #endif // 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; Q_snprintf(compositeFilename, RadioPathLen, "sound\\%s", speak->m_phrase); speak->m_duration = (double)GET_APPROX_WAVE_PLAY_LEN(compositeFilename) / 1000.0f; if (speak->m_duration <= 0.0f) { CONSOLE_ECHO("Warning: Couldn't get duration of phrase '%s'\n", compositeFilename); speak->m_duration = 1.0f; } BotSpeakableVector *speakables = phrase->m_voiceBank[bankIndex]; speakables->push_back(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.push_back(phrase); else m_list.push_back(phrase); } } FREE_FILE(phraseDataFile); return true; } BotPhraseManager::~BotPhraseManager() { for (auto phrase : m_list) delete phrase; for (auto phrase : m_placeList) delete phrase; m_list.clear(); m_placeList.clear(); } Place BotPhraseManager::NameToID(const char *name) const { for (auto phrase : m_placeList) { if (!Q_stricmp(phrase->m_name, name)) return phrase->m_id; } for (auto phrase : m_list) { if (!Q_stricmp(phrase->m_name, name)) return phrase->m_id; } return UNDEFINED_PLACE; } const char *BotPhraseManager::IDToName(Place id) const { for (auto phrase : m_placeList) { if (phrase->m_id == id) return phrase->m_name; } for (auto phrase : m_list) { if (phrase->m_id == id) return phrase->m_name; } return nullptr; } // Given a name, return the associated phrase collection const BotPhrase *BotPhraseManager::GetPhrase(const char *name) const { for (auto phrase : m_list) { if (!Q_stricmp(phrase->m_name, name)) return phrase; } //CONSOLE_ECHO("GetPhrase: ERROR - Invalid phrase '%s'\n", name); return nullptr; } // Given an id, return the associated phrase collection // TODO: Store phrases in a vector to make this fast const BotPhrase *BotPhraseManager::GetPhrase(unsigned int id) const { for (auto phrase : m_list) { if (phrase->m_id == id) return phrase; } CONSOLE_ECHO("GetPhrase: ERROR - Invalid phrase id #%d\n", id); return nullptr; } // Given a name, return the associated Place phrase collection const BotPhrase *BotPhraseManager::GetPlace(const char *name) const { if (!name) return nullptr; for (auto phrase : m_placeList) { if (!Q_stricmp(phrase->m_name, name)) return phrase; } return nullptr; } // Given a place, return the associated Place phrase collection const BotPhrase *BotPhraseManager::GetPlace(PlaceCriteria place) const { if (place == UNDEFINED_PLACE) return nullptr; for (auto phrase : m_placeList) { if (phrase->m_id == place) return phrase; } return nullptr; } BotStatement::BotStatement(BotChatterInterface *chatter, BotStatementType type, float expireDuration) { m_chatter = chatter; m_prev = m_next = nullptr; m_timestamp = gpGlobals->time; m_speakTimestamp = 0.0f; m_type = type; m_subject = UNDEFINED_SUBJECT; m_place = UNDEFINED_PLACE; m_meme = nullptr; m_startTime = gpGlobals->time; m_expireTime = gpGlobals->time + 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; m_meme = nullptr; } } CCSBot *BotStatement::GetOwner() 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() 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() 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() 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 0 // 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; } #endif // check if statement lifetime has expired return (gpGlobals->time > 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) { static const BotPhrase *meToo = TheBotPhrases->GetPhrase("AgreeWithPlan"); // 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->time + RANDOM_FLOAT(0.5f, 1.0f); } else { // same plan at different place - wait a bit to allow others to respond "me too" m_startTime = gpGlobals->time + RANDOM_FLOAT(3.0f, 4.0f); } } } } void BotStatement::AppendPhrase(const BotPhrase *phrase) { if (!phrase) return; if (m_count < MAX_BOT_PHRASES) { m_statement[m_count].isPhrase = true; m_statement[m_count].phrase = phrase; m_count++; } } // 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; m_count++; } } // Say our statement // m_index refers to the phrase currently being spoken, or -1 if we havent started yet bool BotStatement::Update() { 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->time; } // 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; if (me->GetNearbyEnemyCount() > 3 || gpGlobals->time - m_speakTimestamp > reportTime) { // enough enemies have accumulated to expire this delay m_nextTime = 0.0f; } } if (gpGlobals->time > 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 = nullptr; 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"); #ifdef REGAMEDLL_FIXES if (phrase) #endif { 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 = nullptr; } else { phrase = TheBotPhrases->GetPhrase(speak[enemyCount]); } break; } case SHORT_DELAY: { m_nextTime = gpGlobals->time + RANDOM_FLOAT(0.1f, 0.5f); return true; } case LONG_DELAY: { m_nextTime = gpGlobals->time + RANDOM_FLOAT(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) { GameEventType radioEvent = phrase->GetRadioEquivalent(); if (radioEvent == EVENT_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->GetID()); const float minRepeatTime = 20.0f; if (timeSince < minRepeatTime) { sayIt = false; } else { TheBotPhrases->ResetPlaceStatementInterval(phrase->GetID()); } } if (sayIt) { if (!filename) { GameEventType radioEvent = phrase->GetRadioEquivalent(); if (radioEvent == EVENT_INVALID) { // skip directly to the next phrase m_nextTime = 0.0f; } else { me->SendRadioMessage(radioEvent); me->GetChatter()->ResetRadioSilenceDuration(); duration = 2.0f; } } else { me->Radio(filename, nullptr, me->GetProfile()->GetVoicePitch(), false); me->GetChatter()->ResetRadioSilenceDuration(); me->StartVoiceFeedback(duration + 1.0f); } } } const float gap = 0.1f; m_nextTime = gpGlobals->time + 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 Place BotStatement::GetPlace() 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->GetID(); } return 0; } // Return true if this statement has an associated count bool BotStatement::HasCount() 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; } enum PitchHack { P_HI, P_NORMAL, P_LOW }; static int nextPitch = P_HI; BotChatterInterface::BotChatterInterface(CCSBot *me) { m_me = me; m_statementList = nullptr; switch (nextPitch) { case P_HI: m_pitch = RANDOM_LONG(105, 110); break; case P_NORMAL: m_pitch = RANDOM_LONG(95, 105); break; case P_LOW: m_pitch = RANDOM_LONG(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() { 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(); } // 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) { statement->m_next = nullptr; statement->m_prev = nullptr; m_statementList = statement; return; } // list has at least one statement on it // insert into list in order BotStatement *earlier = nullptr; 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 = nullptr; 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() { 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->time; } // 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; } } void BotChatterInterface::OnEvent(GameEventType event, CBaseEntity *pEntity, CBaseEntity *pOther) { ; } // Invoked when we die void BotChatterInterface::OnDeath() { 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 static const BotPhrase *pain = TheBotPhrases->GetPhrase("pain"); if (pain) { m_me->Radio(pain->GetSpeakable(m_me->GetProfile()->GetVoiceBank()), nullptr, m_me->GetProfile()->GetVoicePitch()); m_me->GetChatter()->ResetRadioSilenceDuration(); } } } // remove all of our statements Reset(); } // Process ongoing chatter for this bot void BotChatterInterface::Update() { // 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 = nullptr; 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"); RemoveStatement(say); } } } } // Returns the statement that is being spoken, or is next to be spoken if no-one is speaking now BotStatement *BotChatterInterface::GetActiveStatement() { // keep track of statement waiting longest to be spoken - it is next BotStatement *earliest = nullptr; float earlyTime = 999999999.9f; for (int i = 1; i <= gpGlobals->maxClients; i++) { CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); if (!pPlayer) continue; if (FNullEnt(pPlayer->pev)) continue; if (FStrEq(STRING(pPlayer->pev->netname), "")) continue; // ignore dead humans if (!pPlayer->IsBot() && !pPlayer->IsAlive()) continue; // ignore enemies, since we can't hear them talk if (m_me->BotRelationship(pPlayer) == CCSBot::BOT_ENEMY) continue; // if not a bot, fail the test // TODO: Check if human is currently talking if (!pPlayer->IsBot()) continue; CCSBot *pBot = static_cast(pPlayer); auto say = pBot->GetChatter()->m_statementList; while (say) { // 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; } say = say->m_next; } } // make sure it is time to start this statement if (earliest && earliest->GetStartTime() > gpGlobals->time) return nullptr; return earliest; } // Return true if we speaking makes sense now bool BotChatterInterface::ShouldSpeak() 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() { #ifdef REGAMEDLL_FIXES if (m_me->m_iTeam != CT && m_me->m_iTeam != TERRORIST) return 0; #endif return m_radioSilenceInterval[m_me->m_iTeam - 1].GetElapsedTime(); } void BotChatterInterface::ResetRadioSilenceDuration() { #ifdef REGAMEDLL_FIXES if (m_me->m_iTeam != CT && m_me->m_iTeam != TERRORIST) return; #endif m_radioSilenceInterval[m_me->m_iTeam - 1].Reset(); } inline void SayWhere(BotStatement *say, Place place) { say->AppendPhrase(TheBotPhrases->GetPlace(place)); } // Report enemy sightings void BotChatterInterface::EnemySpotted() { // 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); } NOXREF 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() { 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() { 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()->pev->origin)); } 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->time + 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() { 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() { // 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); } 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 (RANDOM_FLOAT(0.0f, 100.0f) < 33.0f) { BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 5.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("HeardNoise")); say->SetPlace(TheNavAreaGrid.GetPlace(pos)); 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() { // 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->time + RANDOM_FLOAT(2.0f, 4.0f)); AddStatement(say); } void BotChatterInterface::Affirmative() { BotStatement *say = new BotStatement(this, REPORT_ACKNOWLEDGE, 3.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("Affirmative")); AddStatement(say); } void BotChatterInterface::Negative() { 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 = 10.0f; // 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); say->AttachMeme(new BotDefendHereMeme(m_me->pev->origin)); AddStatement(say); } void BotChatterInterface::TheyPickedUpTheBomb() { 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 m_me->GetGameState()->UpdateBomber(&m_me->pev->origin); // tell our teammates BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("TheyPickedUpTheBomb")); say->AttachMeme(new BotBombStatusMeme(CSGameState::MOVING, m_me->pev->origin)); AddStatement(say); } void BotChatterInterface::SpottedBomber(CBasePlayer *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 && (bomber->pev->origin - *bomberPos).LengthSquared() < closeRangeSq) return; } // update our gamestate m_me->GetGameState()->UpdateBomber(&bomber->pev->origin); // tell our teammates BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f); // where is the bomber Place place = TheNavAreaGrid.GetPlace(&bomber->pev->origin); SayWhere(say, place); say->AppendPhrase(TheBotPhrases->GetPhrase("SpottedBomber")); say->SetSubject(bomber->entindex()); //say->AttachMeme(new BotHelpMeme(place)); say->AttachMeme(new BotBombStatusMeme(CSGameState::MOVING, bomber->pev->origin)); 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->pev->origin); 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 = TheNavAreaGrid.GetPlace(&bomb->pev->origin); SayWhere(say, place); say->AppendPhrase(TheBotPhrases->GetPhrase("SpottedLooseBomb")); if (TheCSBots()->GetLooseBomb()) say->AttachMeme(new BotBombStatusMeme(CSGameState::LOOSE, bomb->pev->origin)); AddStatement(say); } } NOXREF void BotChatterInterface::GuardingLooseBomb(CBaseEntity *bomb) { if (TheCSBots()->IsRoundOver() || !bomb) return; #ifdef REGAMEDLL_FIXES const float minInterval = 20.0f; if (m_planInterval.IsLessThen(minInterval)) return; m_planInterval.Reset(); #endif // update our gamestate m_me->GetGameState()->UpdateLooseBomb(&bomb->pev->origin); // tell our teammates BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f); // where is the bomb Place place = TheNavAreaGrid.GetPlace(&bomb->pev->origin); SayWhere(say, place); say->AppendPhrase(TheBotPhrases->GetPhrase("GuardingLooseBomb")); if (TheCSBots()->GetLooseBomb()) say->AttachMeme(new BotBombStatusMeme(CSGameState::LOOSE, bomb->pev->origin)); AddStatement(say); } void BotChatterInterface::RequestBombLocation() { // 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) return; BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f); SayWhere(say, TheNavAreaGrid.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) return; BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 3.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("PlantedBombPlace")); say->SetPlace(TheNavAreaGrid.GetPlace(&zone->m_center)); say->AttachMeme(new BotBombsiteStatusMeme(zoneIndex, BotBombsiteStatusMeme::PLANTED)); AddStatement(say); } void BotChatterInterface::Scared() { 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() { BotStatement *say = new BotStatement(this, REPORT_EMOTE, 15.0f); // wait a bit before speaking say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(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 (RANDOM_FLOAT(0.0f, 100.0f) < 33.3f) say->AppendPhrase(TheBotPhrases->GetPhrase("LastManStanding")); } else { if (TheCSBots()->GetElapsedRoundTime() < quickRound) { if (RANDOM_FLOAT(0.0f, 100.0f) < 33.3f) say->AppendPhrase(TheBotPhrases->GetPhrase("WonRoundQuickly")); } else if (RANDOM_FLOAT(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() + RANDOM_FLOAT(2.0, 3.0f)); AddStatement(say); } void BotChatterInterface::GuardingHostages(Place place, bool isPlan) { if (TheCSBots()->IsRoundOver()) return; const float minInterval = 20.0f; if (m_planInterval.IsLessThen(minInterval)) return; #ifdef REGAMEDLL_FIXES m_planInterval.Reset(); #endif 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; #ifdef REGAMEDLL_FIXES m_planInterval.Reset(); #endif if (isPlan) AnnouncePlan("GoingToGuardHostageEscapeZone", UNDEFINED_PLACE); else Say("GuardingHostageEscapeZone"); } void BotChatterInterface::HostagesBeingTaken() { 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() { if (TheCSBots()->IsRoundOver()) return; BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 3.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("HostagesTaken")); AddStatement(say); } void BotChatterInterface::TalkingToHostages() { ; } void BotChatterInterface::EscortingHostages() { 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); } } NOXREF void BotChatterInterface::HostageDown() { 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() { BotStatement *say = new BotStatement(this, REPORT_KILLED_FRIEND, 2.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("KilledFriend")); // give them time to react say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(0.5f, 1.0f)); AddStatement(say); } void BotChatterInterface::FriendlyFire() { BotStatement *say = new BotStatement(this, REPORT_FRIENDLY_FIRE, 1.0f); say->AppendPhrase(TheBotPhrases->GetPhrase("FriendlyFire")); // give them time to react say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(0.3f, 0.5f)); AddStatement(say); }