You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2258 lines
52 KiB
2258 lines
52 KiB
#include "bot_common.h" |
|
|
|
/* |
|
* Globals initialization |
|
*/ |
|
BotPhraseManager *TheBotPhrases = NULL; |
|
CountdownTimer BotChatterInterface::m_encourageTimer; |
|
IntervalTimer BotChatterInterface::m_radioSilenceInterval[ 2 ]; |
|
|
|
const Vector *GetRandomSpotAtPlace(Place place) |
|
{ |
|
int count = 0; |
|
int which; |
|
|
|
FOR_EACH_LL (TheNavAreaList, it) |
|
{ |
|
CNavArea *area = TheNavAreaList[it]; |
|
|
|
if (area->GetPlace() == place) |
|
++count; |
|
} |
|
|
|
if (count == 0) |
|
return NULL; |
|
|
|
which = RANDOM_LONG(0, count - 1); |
|
|
|
FOR_EACH_LL (TheNavAreaList, it) |
|
{ |
|
CNavArea *area = TheNavAreaList[it]; |
|
|
|
if (area->GetPlace() == place && which == 0) |
|
return area->GetCenter(); |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
// Transmit meme to other bots |
|
|
|
void BotMeme::Transmit(CCSBot *sender) const |
|
{ |
|
for (int i = 1; i <= gpGlobals->maxClients; ++i) |
|
{ |
|
CBasePlayer *player = static_cast<CBasePlayer *>(UTIL_PlayerByIndex(i)); |
|
|
|
if (player == NULL) |
|
continue; |
|
|
|
if (FNullEnt(player->pev)) |
|
continue; |
|
|
|
if (FStrEq(STRING(player->pev->netname), "")) |
|
continue; |
|
|
|
// skip self |
|
if (sender == player) |
|
continue; |
|
|
|
// ignore dead humans |
|
if (!player->IsBot() && !player->IsAlive()) |
|
continue; |
|
|
|
// if not a bot, fail the test |
|
if (!player->IsBot()) |
|
continue; |
|
|
|
CCSBot *bot = dynamic_cast<CCSBot *>(player); |
|
|
|
if (!bot) |
|
continue; |
|
|
|
// allow bot to interpret our meme |
|
Interpret(sender, bot); |
|
} |
|
} |
|
|
|
// A teammate called for help - respond |
|
|
|
void BotHelpMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
const float maxHelpRange = 3000.0f; // 2000 |
|
// receiver->RespondToHelpRequest(sender, m_place, maxHelpRange); |
|
} |
|
|
|
// A teammate reported information about a bombsite |
|
|
|
void BotBombsiteStatusMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
// remember this bombsite's status |
|
if (m_status == CLEAR) |
|
receiver->GetGameState()->ClearBombsite(m_zoneIndex); |
|
else |
|
receiver->GetGameState()->MarkBombsiteAsPlanted(m_zoneIndex); |
|
|
|
// if we were heading to the just-cleared bombsite, pick another one to search |
|
// if our target bombsite wasn't cleared, will will continue going to it, |
|
// because GetNextBombsiteToSearch() will return the same zone (since its not cleared) |
|
// if the bomb was planted, we will head to that bombsite |
|
if (receiver->GetTask() == CCSBot::FIND_TICKING_BOMB) |
|
{ |
|
receiver->Idle(); |
|
receiver->GetChatter()->Affirmative(); |
|
} |
|
} |
|
|
|
// A teammate reported information about the bomb |
|
|
|
void BotBombStatusMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
// update our gamestate based on teammate's report |
|
switch (m_state) |
|
{ |
|
case CSGameState::MOVING: |
|
{ |
|
receiver->GetGameState()->UpdateBomber(&m_pos); |
|
|
|
// if we are hunting and see no enemies, respond |
|
if (!receiver->IsRogue() && receiver->IsHunting() && receiver->GetNearbyEnemyCount() == 0) |
|
// receiver->RespondToHelpRequest(sender, TheNavAreaGrid.GetPlace(&m_pos)); |
|
|
|
break; |
|
} |
|
case CSGameState::LOOSE: |
|
{ |
|
receiver->GetGameState()->UpdateLooseBomb(&m_pos); |
|
|
|
if (receiver->GetTask() == CCSBot::GUARD_BOMB_ZONE) |
|
{ |
|
receiver->Idle(); |
|
receiver->GetChatter()->Affirmative(); |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// A teammate has asked that we follow him |
|
|
|
void BotFollowMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
if (receiver->IsRogue()) |
|
return; |
|
|
|
// if we're busy, ignore |
|
if (receiver->IsBusy()) |
|
return; |
|
|
|
PathCost pathCost(receiver); |
|
float travelDistance = NavAreaTravelDistance(receiver->GetLastKnownArea(), TheNavAreaGrid.GetNearestNavArea(&sender->pev->origin), pathCost); |
|
if (travelDistance < 0.0f) |
|
return; |
|
|
|
const float tooFar = 1000.0f; |
|
if (travelDistance > tooFar) |
|
return; |
|
|
|
// begin following |
|
receiver->Follow(sender); |
|
|
|
// acknowledge |
|
receiver->GetChatter()->Say("CoveringFriend"); |
|
} |
|
|
|
// A teammate has asked us to defend a place |
|
|
|
void BotDefendHereMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
if (receiver->IsRogue()) |
|
return; |
|
|
|
// if we're busy, ignore |
|
if (receiver->IsBusy()) |
|
return; |
|
|
|
Place place = TheNavAreaGrid.GetPlace(&m_pos); |
|
if (place != UNDEFINED_PLACE) |
|
{ |
|
// pick a random hiding spot in this place |
|
const Vector *spot = FindRandomHidingSpot(receiver, place, receiver->IsSniper()); |
|
if (spot != NULL) |
|
{ |
|
receiver->SetTask(CCSBot::HOLD_POSITION); |
|
receiver->Hide(spot); |
|
return; |
|
} |
|
} |
|
|
|
// hide nearby |
|
receiver->SetTask(CCSBot::HOLD_POSITION); |
|
receiver->Hide(TheNavAreaGrid.GetNearestNavArea(&m_pos)); |
|
|
|
// acknowledge |
|
receiver->GetChatter()->Say("Affirmative"); |
|
} |
|
|
|
// A teammate has asked where the bomb is planted |
|
|
|
void BotWhereBombMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
int zone = receiver->GetGameState()->GetPlantedBombsite(); |
|
|
|
if (zone != CSGameState::UNKNOWN) |
|
receiver->GetChatter()->FoundPlantedBomb(zone); |
|
} |
|
|
|
// A teammate has asked us to report in |
|
|
|
void BotRequestReportMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
receiver->GetChatter()->ReportingIn(); |
|
} |
|
|
|
// A teammate told us all the hostages are gone |
|
|
|
void BotAllHostagesGoneMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
// receiver->GetGameState()->AllHostagesGone(); |
|
|
|
// acknowledge |
|
receiver->GetChatter()->Say("Affirmative"); |
|
} |
|
|
|
// A teammate told us a CT is talking to a hostage |
|
|
|
void BotHostageBeingTakenMeme::Interpret(CCSBot *sender, CCSBot *receiver) const |
|
{ |
|
// receiver->GetGameState()->HostageWasTaken(); |
|
|
|
// if we're busy, ignore |
|
if (receiver->IsBusy()) |
|
return; |
|
|
|
receiver->Idle(); |
|
|
|
// acknowledge |
|
receiver->GetChatter()->Say("Affirmative"); |
|
} |
|
|
|
BotSpeakable::BotSpeakable() |
|
{ |
|
m_phrase = NULL; |
|
} |
|
|
|
BotSpeakable::~BotSpeakable() |
|
{ |
|
if (m_phrase != NULL) |
|
{ |
|
delete[] m_phrase; |
|
m_phrase = NULL; |
|
} |
|
} |
|
|
|
BotPhrase::BotPhrase(unsigned int id, bool isPlace) |
|
{ |
|
m_name = NULL; |
|
m_id = id; |
|
m_isPlace = isPlace; |
|
m_radioEvent = EVENT_INVALID; |
|
m_isImportant = false; |
|
|
|
ClearCriteria(); |
|
|
|
m_numVoiceBanks = 0; |
|
InitVoiceBank(0); |
|
} |
|
|
|
BotPhrase::~BotPhrase () |
|
{ |
|
for (int bank = 0; bank < m_voiceBank.Count (); ++bank) |
|
{ |
|
for (int speakable = 0; speakable < m_voiceBank[bank]->Count (); ++speakable) |
|
{ |
|
delete (*m_voiceBank[bank])[speakable]; |
|
} |
|
delete m_voiceBank[bank]; |
|
} |
|
|
|
if (m_name) |
|
delete[] m_name; |
|
} |
|
|
|
void BotPhrase::InitVoiceBank(int bankIndex) |
|
{ |
|
while (m_numVoiceBanks <= bankIndex) |
|
{ |
|
m_count.AddToTail (0); |
|
m_index.AddToTail (0); |
|
m_voiceBank.AddToTail (new BotSpeakableVector); |
|
++m_numVoiceBanks; |
|
} |
|
} |
|
|
|
// Return a random speakable - avoid repeating |
|
|
|
char *BotPhrase::GetSpeakable(int bankIndex, float *duration) const |
|
{ |
|
if (bankIndex < 0 || bankIndex >= m_numVoiceBanks || m_count[bankIndex] == 0) |
|
{ |
|
if (duration != NULL) |
|
*duration = 0.0f; |
|
|
|
return NULL; |
|
} |
|
|
|
// find phrase that meets the current criteria |
|
int start = m_index[ bankIndex ]; |
|
while (true) |
|
{ |
|
BotSpeakableVector *speakables = m_voiceBank[ bankIndex ]; |
|
int &index = m_index[ bankIndex ]; |
|
|
|
const BotSpeakable *speak = (*speakables)[index++]; |
|
|
|
if (m_index[ bankIndex ] >= m_count[ bankIndex ]) |
|
m_index[ bankIndex ] = 0; |
|
|
|
// check place criteria |
|
// if this speakable has a place criteria, it must match to be used |
|
// speakables with Place of ANY will match any place |
|
// speakables with a specific Place will only be used if Place matches |
|
// speakables with Place of UNDEFINED only match Place of UNDEFINED |
|
if (speak->m_place == ANY_PLACE || speak->m_place == m_placeCriteria) |
|
{ |
|
// check count criteria |
|
// if this speakable has a count criteria, it must match to be used |
|
// if this speakable does not have a count criteria, we dont care what the count is set to |
|
if (speak->m_count == UNDEFINED_COUNT || speak->m_count == 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 != NULL) |
|
*duration = 0.0f; |
|
|
|
return NULL; |
|
} |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
// Randomly shuffle the speakable order |
|
|
|
void BotPhrase::Randomize() |
|
{ |
|
for (int bank = 0; bank < m_voiceBank.Count (); ++bank) |
|
{ |
|
BotSpeakableVector *speakables = m_voiceBank[bank]; |
|
if (speakables->Count () == 1) |
|
continue; |
|
|
|
// A simple shuffle: for each array index, swap it with a random index |
|
for (int index = 0; index < speakables->Count (); ++index) |
|
{ |
|
int newIndex = RANDOM_LONG (0, speakables->Count () - 1); |
|
|
|
BotSpeakable *speakable = (*speakables)[index]; |
|
(*speakables)[index] = (*speakables)[newIndex]; |
|
(*speakables)[newIndex] = speakable; |
|
} |
|
} |
|
} |
|
|
|
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_EACH_LL (m_placeList, it) |
|
{ |
|
m_placeList[it]->Randomize(); |
|
} |
|
|
|
FOR_EACH_LL (m_list, it) |
|
{ |
|
m_list[it]->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 == NULL) |
|
{ |
|
if (true) |
|
{ |
|
CONSOLE_ECHO("WARNING: Cannot access bot phrase database '%s'\n", filename); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
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]; |
|
|
|
// Parse the BotChatter.db into BotPhrase collections |
|
while (true) |
|
{ |
|
phraseData = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
break; |
|
|
|
char *token = MP_COM_GetToken(); |
|
|
|
if (!Q_stricmp(token, "BaseDir")) |
|
{ |
|
// get name of this output device |
|
phraseData = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
{ |
|
CONSOLE_ECHO("Error parsing '%s' - expected identifier\n", filename); |
|
FREE_FILE(phraseDataFile); |
|
return false; |
|
} |
|
|
|
char *token = MP_COM_GetToken(); |
|
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 = NULL; |
|
if (isDefault) |
|
{ |
|
phrase = new BotPhrase(nextID++, isPlace); |
|
} |
|
|
|
// get name of this phrase |
|
phraseData = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
{ |
|
CONSOLE_ECHO("Error parsing '%s' - expected identifier\n", filename); |
|
FREE_FILE(phraseDataFile); |
|
return false; |
|
} |
|
|
|
if (isDefault) |
|
{ |
|
phrase->m_name = CloneString(MP_COM_GetToken()); |
|
} |
|
// look up the existing phrase |
|
else |
|
{ |
|
if (isPlace) |
|
{ |
|
phrase = const_cast<BotPhrase *>(GetPlace(MP_COM_GetToken())); |
|
} |
|
else |
|
{ |
|
phrase = const_cast<BotPhrase *>(GetPhrase(MP_COM_GetToken())); |
|
} |
|
|
|
if (!phrase) |
|
{ |
|
CONSOLE_ECHO("Error parsing '%s' - phrase '%s' is invalid\n", filename, MP_COM_GetToken()); |
|
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 = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
{ |
|
CONSOLE_ECHO("Error parsing %s - expected 'End'\n", filename); |
|
FREE_FILE(phraseDataFile); |
|
return false; |
|
} |
|
|
|
token = MP_COM_GetToken(); |
|
|
|
// check for Place criteria |
|
if (!Q_stricmp(token, "Place")) |
|
{ |
|
phraseData = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
{ |
|
CONSOLE_ECHO("Error parsing %s - expected Place name\n", filename); |
|
FREE_FILE(phraseDataFile); |
|
return false; |
|
} |
|
|
|
token = MP_COM_GetToken(); |
|
|
|
// update place criteria for subsequent speak lines |
|
// NOTE: this assumes places must be first in the chatter database |
|
|
|
// check for special identifiers |
|
if (!Q_stricmp("ANY", token)) |
|
placeCriteria = ANY_PLACE; |
|
else if (!Q_stricmp("UNDEFINED", token)) |
|
placeCriteria = UNDEFINED_PLACE; |
|
else |
|
placeCriteria = TheBotPhrases->NameToID(token); |
|
|
|
continue; |
|
} |
|
|
|
// check for Count criteria |
|
if (!Q_stricmp(token, "Count")) |
|
{ |
|
phraseData = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
{ |
|
CONSOLE_ECHO("Error parsing %s - expected Count value\n", filename); |
|
FREE_FILE(phraseDataFile); |
|
return false; |
|
} |
|
|
|
token = MP_COM_GetToken(); |
|
|
|
// 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 = MP_COM_Parse(phraseData); |
|
if (!phraseData) |
|
{ |
|
CONSOLE_ECHO("Error parsing %s - expected radio event\n", filename); |
|
FREE_FILE(phraseDataFile); |
|
return false; |
|
} |
|
token = MP_COM_GetToken(); |
|
|
|
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; |
|
|
|
// 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 = 1;//(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->AddToTail(speak); |
|
|
|
++phrase->m_count[ bankIndex ]; |
|
} |
|
|
|
if (isDefault) |
|
{ |
|
phrase->m_radioEvent = radioEvent; |
|
phrase->m_isImportant = isImportant; |
|
} |
|
|
|
// add phrase collection to the appropriate master list |
|
if (isPlace) |
|
m_placeList.AddToTail (phrase); |
|
else |
|
m_list.AddToTail (phrase); |
|
} |
|
} |
|
|
|
FREE_FILE(phraseDataFile); |
|
|
|
return true; |
|
} |
|
|
|
BotPhraseManager::~BotPhraseManager() |
|
{ |
|
int i; |
|
|
|
// free phrase resources |
|
for (i = 0; i < m_list.Count (); ++i) |
|
{ |
|
delete m_list[i]; |
|
} |
|
|
|
for (i = 0; i < m_placeList.Count (); ++i) |
|
{ |
|
delete m_placeList[i]; |
|
} |
|
|
|
m_list.RemoveAll (); |
|
m_placeList.RemoveAll (); |
|
} |
|
|
|
Place BotPhraseManager::NameToID(const char *name) const |
|
{ |
|
FOR_EACH_LL (m_placeList, it) |
|
{ |
|
const BotPhrase *phrase = m_placeList[it]; |
|
|
|
if (!Q_stricmp(phrase->m_name, name)) |
|
return phrase->m_id; |
|
} |
|
|
|
FOR_EACH_LL (m_list, it) |
|
{ |
|
const BotPhrase *phrase = m_list[it]; |
|
|
|
if (!Q_stricmp(phrase->m_name, name)) |
|
return phrase->m_id; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
const char *BotPhraseManager::IDToName(Place id) const |
|
{ |
|
FOR_EACH_LL (m_placeList, it) |
|
{ |
|
const BotPhrase *phrase = m_placeList[it]; |
|
|
|
if (phrase->m_id == id) |
|
return phrase->m_name; |
|
} |
|
|
|
FOR_EACH_LL (m_list, it) |
|
{ |
|
const BotPhrase *phrase = m_list[it]; |
|
|
|
if (phrase->m_id == id) |
|
return phrase->m_name; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
// Given a name, return the associated phrase collection |
|
|
|
const BotPhrase *BotPhraseManager::GetPhrase(const char *name) const |
|
{ |
|
FOR_EACH_LL (m_list, it) |
|
{ |
|
const BotPhrase *phrase = m_list[it]; |
|
|
|
if (!Q_stricmp(phrase->m_name, name)) |
|
return phrase; |
|
} |
|
|
|
//CONSOLE_ECHO("GetPhrase: ERROR - Invalid phrase '%s'\n", name); |
|
return NULL; |
|
} |
|
|
|
// Given an id, return the associated phrase collection |
|
// TODO: Store phrases in a vector to make this fast |
|
|
|
/* |
|
const BotPhrase *BotPhraseManager::GetPhrase(unsigned int id) const |
|
{ |
|
for (BotPhraseList::const_iterator iter = m_list.begin(); iter != m_list.end(); ++iter) |
|
{ |
|
const BotPhrase *phrase = *iter; |
|
|
|
if (phrase->m_id == id) |
|
return phrase; |
|
} |
|
|
|
CONSOLE_ECHO("GetPhrase: ERROR - Invalid phrase id #%d\n", id); |
|
return NULL; |
|
} |
|
*/ |
|
|
|
// Given a name, return the associated Place phrase collection |
|
|
|
const BotPhrase *BotPhraseManager::GetPlace(const char *name) const |
|
{ |
|
if (name == NULL) |
|
return NULL; |
|
|
|
FOR_EACH_LL (m_placeList, it) |
|
{ |
|
const BotPhrase *phrase = m_placeList[it]; |
|
|
|
if (!Q_stricmp(phrase->m_name, name)) |
|
return phrase; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
// Given a place, return the associated Place phrase collection |
|
|
|
const BotPhrase *BotPhraseManager::GetPlace(PlaceCriteria place) const |
|
{ |
|
if (place == UNDEFINED_PLACE) |
|
return NULL; |
|
|
|
FOR_EACH_LL (m_placeList, it) |
|
{ |
|
const BotPhrase *phrase = m_placeList[it]; |
|
|
|
if (phrase->m_id == place) |
|
return phrase; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
BotStatement::BotStatement(BotChatterInterface *chatter, BotStatementType type, float expireDuration) |
|
{ |
|
m_chatter = chatter; |
|
|
|
m_prev = m_next = NULL; |
|
m_timestamp = gpGlobals->time; |
|
m_speakTimestamp = 0.0f; |
|
|
|
m_type = type; |
|
m_subject = UNDEFINED_SUBJECT; |
|
m_place = UNDEFINED_PLACE; |
|
m_meme = NULL; |
|
|
|
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 != NULL) |
|
{ |
|
delete m_meme; |
|
m_meme = NULL; |
|
} |
|
} |
|
|
|
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 == NULL) |
|
return; |
|
|
|
if (m_count < MAX_BOT_PHRASES) |
|
{ |
|
m_statement[ m_count ].isPhrase = true; |
|
m_statement[ m_count++ ].phrase = phrase; |
|
} |
|
} |
|
|
|
// Special phrases that depend on the context |
|
|
|
void BotStatement::AppendPhrase(ContextType contextPhrase) |
|
{ |
|
if (m_count < MAX_BOT_PHRASES) |
|
{ |
|
m_statement[ m_count ].isPhrase = false; |
|
m_statement[ m_count++ ].context = contextPhrase; |
|
} |
|
} |
|
|
|
// Say our statement |
|
// m_index refers to the phrase currently being spoken, or -1 if we havent started yet |
|
extern int gmsgSayText; |
|
|
|
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 != NULL) |
|
{ |
|
m_meme->Transmit(me); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// start next part of statement |
|
float duration = 0.0f; |
|
const BotPhrase *phrase = NULL; |
|
|
|
if (m_statement[ m_index ].isPhrase) |
|
{ |
|
// normal phrase |
|
phrase = m_statement[ m_index ].phrase; |
|
} |
|
else |
|
{ |
|
// context-dependant phrase |
|
switch (m_statement[ m_index ].context) |
|
{ |
|
case CURRENT_ENEMY_COUNT: |
|
{ |
|
int enemyCount = me->GetNearbyEnemyCount(); |
|
|
|
// if we are outnumbered, ask for help |
|
if (enemyCount - 1 > me->GetNearbyFriendCount()) |
|
{ |
|
phrase = TheBotPhrases->GetPhrase("Help"); |
|
AttachMeme(new BotHelpMeme()); |
|
} |
|
else if (enemyCount > 1) |
|
{ |
|
phrase = TheBotPhrases->GetPhrase("EnemySpotted"); |
|
phrase->SetCountCriteria(enemyCount); |
|
} |
|
break; |
|
} |
|
case REMAINING_ENEMY_COUNT: |
|
{ |
|
static const char *speak[] = |
|
{ |
|
"NoEnemiesLeft", "OneEnemyLeft", "TwoEnemiesLeft", "ThreeEnemiesLeft" |
|
}; |
|
|
|
int enemyCount = me->GetEnemiesRemaining(); |
|
|
|
// dont report if there are lots of enemies left |
|
if (enemyCount < 0 || enemyCount > 3) |
|
{ |
|
phrase = NULL; |
|
} |
|
else |
|
{ |
|
phrase = TheBotPhrases->GetPhrase(speak[ enemyCount ]); |
|
} |
|
break; |
|
} |
|
case SHORT_DELAY: |
|
{ |
|
m_nextTime = gpGlobals->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 != NULL) |
|
{ |
|
// 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); |
|
TheBots->OnEvent( radioEvent, me ); |
|
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 == NULL) |
|
{ |
|
GameEventType radioEvent = phrase->GetRadioEquivalent(); |
|
|
|
if (radioEvent == EVENT_INVALID) |
|
{ |
|
// skip directly to the next phrase |
|
m_nextTime = 0.0f; |
|
} |
|
else |
|
{ |
|
// me->SendRadioMessage(radioEvent); |
|
TheBots->OnEvent( radioEvent, me ); |
|
|
|
//me->GetChatter()->ResetRadioSilenceDuration(); |
|
duration = 2.0f; |
|
} |
|
} |
|
else |
|
{ |
|
//me->Radio(filename, NULL, me->GetProfile()->GetVoicePitch(), false); |
|
me->GetChatter()->ResetRadioSilenceDuration(); |
|
// me->StartVoiceFeedback(duration + 1.0f); |
|
} |
|
// print to the sending client |
|
char message[256]; |
|
Q_snprintf( message, 256, "[bot] %s (radio): %s", STRING( me->pev->netname), phrase->GetName() ); |
|
MESSAGE_BEGIN( MSG_ALL, gmsgSayText, NULL ); |
|
WRITE_BYTE( ENTINDEX(me->edict()) ); |
|
WRITE_STRING( message ); |
|
MESSAGE_END(); |
|
} |
|
} |
|
|
|
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 = NULL; |
|
|
|
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 != NULL; 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 != NULL; s = s->m_next) |
|
{ |
|
if (statement->IsRedundant(s)) |
|
{ |
|
m_me->PrintIfWatched("I tried to say something I'm already saying.\n"); |
|
delete statement; |
|
return; |
|
} |
|
} |
|
|
|
// keep statements in order of start time |
|
|
|
// check list is empty |
|
if (m_statementList == NULL) |
|
{ |
|
statement->m_next = NULL; |
|
statement->m_prev = NULL; |
|
m_statementList = statement; |
|
return; |
|
} |
|
|
|
// list has at least one statement on it |
|
|
|
// insert into list in order |
|
BotStatement *earlier = NULL; |
|
for (s = m_statementList; s != NULL; s = s->m_next) |
|
{ |
|
if (s->GetStartTime() > statement->GetStartTime()) |
|
break; |
|
|
|
earlier = s; |
|
} |
|
|
|
// insert just after "earlier" |
|
if (earlier != NULL) |
|
{ |
|
if (earlier->m_next != NULL) |
|
earlier->m_next->m_prev = statement; |
|
|
|
statement->m_next = earlier->m_next; |
|
|
|
earlier->m_next = statement; |
|
statement->m_prev = earlier; |
|
} |
|
else |
|
{ |
|
// insert at head |
|
statement->m_prev = NULL; |
|
statement->m_next = m_statementList; |
|
m_statementList->m_prev = statement; |
|
m_statementList = statement; |
|
} |
|
} |
|
|
|
// Remove a statement |
|
|
|
void BotChatterInterface::RemoveStatement(BotStatement *statement) |
|
{ |
|
if (statement->m_next != NULL) |
|
statement->m_next->m_prev = statement->m_prev; |
|
|
|
if (statement->m_prev != NULL) |
|
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 *entity, CBaseEntity *other) |
|
{ |
|
; |
|
} |
|
|
|
// 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 != NULL) |
|
{ |
|
//m_me->Radio(pain->GetSpeakable(m_me->GetProfile()->GetVoiceBank()), NULL, 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 != NULL) |
|
{ |
|
// 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 != NULL && friendSay->GetOwner() == m_me) |
|
friendSay = NULL; |
|
|
|
BotStatement *nextSay; |
|
for (say = m_statementList; say != NULL; 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 != NULL) |
|
{ |
|
// 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 = NULL; |
|
float earlyTime = 999999999.9f; |
|
|
|
for (int i = 1; i <= gpGlobals->maxClients; ++i) |
|
{ |
|
CBasePlayer *player = static_cast<CBasePlayer *>(UTIL_PlayerByIndex(i)); |
|
|
|
if (player == NULL) |
|
continue; |
|
|
|
if (FNullEnt(player->pev)) |
|
continue; |
|
|
|
if (FStrEq(STRING(player->pev->netname), "")) |
|
continue; |
|
|
|
// ignore dead humans |
|
if (!player->IsBot() && !player->IsAlive()) |
|
continue; |
|
|
|
// ignore enemies, since we can't hear them talk |
|
// if (m_me->m_iTeam != player->m_iTeam) |
|
// continue; |
|
|
|
CCSBot *bot = dynamic_cast<CCSBot *>(player); |
|
|
|
// if not a bot, fail the test |
|
// TODO: Check if human is currently talking |
|
if (!bot) |
|
continue; |
|
|
|
for (BotStatement *say = bot->GetChatter()->m_statementList; say != NULL; say = say->m_next) |
|
{ |
|
// if this statement is currently being spoken, return it |
|
if (say->IsSpeaking()) |
|
return say; |
|
|
|
// keep track of statement that has been waiting longest to be spoken of anyone on our team |
|
if (say->GetStartTime() < earlyTime) |
|
{ |
|
earlyTime = say->GetTimestamp(); |
|
earliest = say; |
|
} |
|
} |
|
} |
|
|
|
// make sure it is time to start this statement |
|
if (earliest != NULL && earliest->GetStartTime() > gpGlobals->time) |
|
return NULL; |
|
|
|
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() |
|
{ |
|
return 0;// m_radioSilenceInterval[ m_me->m_iTeam - 1 ].GetElapsedTime(); |
|
} |
|
|
|
void BotChatterInterface::ResetRadioSilenceDuration() |
|
{ |
|
// m_radioSilenceInterval[0].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() |
|
{ |
|
CCSBotManager *ctrl = TheCSBots(); |
|
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 (ctrl->GetLooseBomb()) |
|
{ |
|
say->AppendPhrase(TheBotPhrases->GetPhrase("GuardingLooseBomb")); |
|
say->AttachMeme(new BotBombStatusMeme(CSGameState::LOOSE, ctrl->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 != NULL && (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; |
|
|
|
const float minInterval = 20.0f; |
|
if (m_planInterval.IsLessThen(minInterval)) |
|
return; |
|
|
|
m_planInterval.Reset(); |
|
|
|
// 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 == NULL) |
|
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 == NULL) |
|
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; |
|
CCSBotManager *ctrl = TheCSBots(); |
|
|
|
if (m_me->GetFriendsRemaining() == 0) |
|
{ |
|
// we were the last man standing |
|
if (ctrl->GetElapsedRoundTime() < quickRound) |
|
say->AppendPhrase(TheBotPhrases->GetPhrase("WonRoundQuickly")); |
|
else if (RANDOM_FLOAT(0.0f, 100.0f) < 33.3f) |
|
say->AppendPhrase(TheBotPhrases->GetPhrase("LastManStanding")); |
|
} |
|
else |
|
{ |
|
if (ctrl->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) |
|
{ |
|
CCSBotManager *ctrl = TheCSBots(); |
|
if (ctrl->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(ctrl->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; |
|
|
|
m_planInterval.Reset(); |
|
|
|
if (isPlan) |
|
AnnouncePlan("GoingToGuardHostages", place); |
|
else |
|
Say("GuardingHostages"); |
|
} |
|
|
|
void BotChatterInterface::GuardingHostageEscapeZone(bool isPlan) |
|
{ |
|
if (TheCSBots()->IsRoundOver()) |
|
return; |
|
|
|
const float minInterval = 20.0f; |
|
if (m_planInterval.IsLessThen(minInterval)) |
|
return; |
|
|
|
m_planInterval.Reset(); |
|
|
|
if (isPlan) |
|
AnnouncePlan("GoingToGuardHostageEscapeZone", UNDEFINED_PLACE); |
|
else |
|
Say("GuardingHostageEscapeZone"); |
|
} |
|
|
|
void BotChatterInterface::HostagesBeingTaken() |
|
{ |
|
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); |
|
}
|
|
|