ReGameDLL_CS/regamedll/dlls/bot/cs_bot_chatter.cpp

2228 lines
52 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 (!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<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();
}
// 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<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);
}
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);
}