ReGameDLL_CS/regamedll/dlls/bot/cs_bot_chatter.cpp
Huga a7395b054d
Bot Chatter: Bots will react to enemy snipers presence (#1055)
Friendly fire fixes
Bot Chatter: Bots will react to enemy snipers presence
previously unused chatter on both 1.6 and czero but is used on cs source
Probably fixed where teammate that use sniperrifles looking at bots that don't use sniperrifles got warned
2025-03-28 05:06:17 +07:00

2273 lines
53 KiB
C++

/*
*
* 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 <algorithm>
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 (!UTIL_IsValidPlayer(pPlayer))
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");
}
#ifdef REGAMEDLL_ADD
//---------------------------------------------------------------------------------------------------------------
/**
* A teammate warned about snipers, so we shouldn't warn again for awhile
*/
void BotWarnSniperMeme::Interpret(CCSBot* sender, CCSBot* receiver) const
{
receiver->GetChatter()->FriendSpottedSniper();
}
#endif
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)
{
if (phrase) delete phrase;
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<BotPhrase *>(GetPlace(token));
}
else
{
phrase = const_cast<BotPhrase *>(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);
if (!TheBotPhrases->IsValid() && placeCriteria == UNDEFINED_PLACE)
placeCriteria = TheNavAreaGrid.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();
#ifdef REGAMEDLL_ADD
m_warnSniperTimer.Invalidate();
#endif
}
// 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 (!UTIL_IsValidPlayer(pPlayer))
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<CCSBot *>(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);
}
#ifdef REGAMEDLL_ADD
//---------------------------------------------------------------------------------------------------------------
/**
* 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);
}
#endif
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);
}