mirror of
https://github.com/s1lentq/ReGameDLL_CS.git
synced 2024-12-27 23:25:41 +03:00
7372573c89
Ignore dormant players Minor refactoring
2222 lines
52 KiB
C++
2222 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 (!UTIL_IsValidPlayer(pPlayer))
|
|
continue;
|
|
|
|
if (FStrEq(STRING(pPlayer->pev->netname), ""))
|
|
continue;
|
|
|
|
// skip self
|
|
if (pSender == pPlayer)
|
|
continue;
|
|
|
|
// ignore dead humans
|
|
if (!pPlayer->IsBot() && !pPlayer->IsAlive())
|
|
continue;
|
|
|
|
// ignore enemies, since we can't hear them talk
|
|
if (pSender->BotRelationship(pPlayer) == CCSBot::BOT_ENEMY)
|
|
continue;
|
|
|
|
// if not a bot, fail the test
|
|
if (!pPlayer->IsBot())
|
|
continue;
|
|
|
|
// allow bot to interpret our meme
|
|
Interpret(pSender, (CCSBot *)pPlayer);
|
|
}
|
|
}
|
|
|
|
// A teammate called for help - respond
|
|
void BotHelpMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
const float maxHelpRange = 3000.0f; // 2000
|
|
pReceiver->RespondToHelpRequest(pSender, m_place, maxHelpRange);
|
|
}
|
|
|
|
// A teammate reported information about a bombsite
|
|
void BotBombsiteStatusMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
// remember this bombsite's status
|
|
if (m_status == CLEAR)
|
|
pReceiver->GetGameState()->ClearBombsite(m_zoneIndex);
|
|
else
|
|
pReceiver->GetGameState()->MarkBombsiteAsPlanted(m_zoneIndex);
|
|
|
|
// if we were heading to the just-cleared bombsite, pick another one to search
|
|
// if our target bombsite wasn't cleared, will will continue going to it,
|
|
// because GetNextBombsiteToSearch() will return the same zone (since its not cleared)
|
|
// if the bomb was planted, we will head to that bombsite
|
|
if (pReceiver->GetTask() == CCSBot::FIND_TICKING_BOMB)
|
|
{
|
|
pReceiver->Idle();
|
|
pReceiver->GetChatter()->Affirmative();
|
|
}
|
|
}
|
|
|
|
// A teammate reported information about the bomb
|
|
void BotBombStatusMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
// update our gamestate based on teammate's report
|
|
switch (m_state)
|
|
{
|
|
case CSGameState::MOVING:
|
|
{
|
|
pReceiver->GetGameState()->UpdateBomber(&m_pos);
|
|
|
|
// if we are hunting and see no enemies, respond
|
|
if (!pReceiver->IsRogue() && pReceiver->IsHunting() && pReceiver->GetNearbyEnemyCount() == 0)
|
|
pReceiver->RespondToHelpRequest(pSender, TheNavAreaGrid.GetPlace(&m_pos));
|
|
|
|
break;
|
|
}
|
|
case CSGameState::LOOSE:
|
|
{
|
|
pReceiver->GetGameState()->UpdateLooseBomb(&m_pos);
|
|
|
|
if (pReceiver->GetTask() == CCSBot::GUARD_BOMB_ZONE)
|
|
{
|
|
pReceiver->Idle();
|
|
pReceiver->GetChatter()->Affirmative();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// A teammate has asked that we follow him
|
|
void BotFollowMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
if (pReceiver->IsRogue())
|
|
return;
|
|
|
|
// if we're busy, ignore
|
|
if (pReceiver->IsBusy())
|
|
return;
|
|
|
|
PathCost pathCost(pReceiver);
|
|
float travelDistance = NavAreaTravelDistance(pReceiver->GetLastKnownArea(), TheNavAreaGrid.GetNearestNavArea(&pSender->pev->origin), pathCost);
|
|
if (travelDistance < 0.0f)
|
|
return;
|
|
|
|
const float tooFar = 1000.0f;
|
|
if (travelDistance > tooFar)
|
|
return;
|
|
|
|
// begin following
|
|
pReceiver->Follow(pSender);
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("CoveringFriend");
|
|
}
|
|
|
|
// A teammate has asked us to defend a place
|
|
void BotDefendHereMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
if (pReceiver->IsRogue())
|
|
return;
|
|
|
|
// if we're busy, ignore
|
|
if (pReceiver->IsBusy())
|
|
return;
|
|
|
|
Place place = TheNavAreaGrid.GetPlace(&m_pos);
|
|
if (place != UNDEFINED_PLACE)
|
|
{
|
|
// pick a random hiding spot in this place
|
|
const Vector *spot = FindRandomHidingSpot(pReceiver, place, pReceiver->IsSniper());
|
|
if (spot)
|
|
{
|
|
pReceiver->SetTask(CCSBot::HOLD_POSITION);
|
|
pReceiver->Hide(spot);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// hide nearby
|
|
pReceiver->SetTask(CCSBot::HOLD_POSITION);
|
|
pReceiver->Hide(TheNavAreaGrid.GetNearestNavArea(&m_pos));
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("Affirmative");
|
|
}
|
|
|
|
// A teammate has asked where the bomb is planted
|
|
void BotWhereBombMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
int zone = pReceiver->GetGameState()->GetPlantedBombsite();
|
|
if (zone != CSGameState::UNKNOWN)
|
|
{
|
|
pReceiver->GetChatter()->FoundPlantedBomb(zone);
|
|
}
|
|
}
|
|
|
|
// A teammate has asked us to report in
|
|
void BotRequestReportMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
pReceiver->GetChatter()->ReportingIn();
|
|
}
|
|
|
|
// A teammate told us all the hostages are gone
|
|
void BotAllHostagesGoneMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
pReceiver->GetGameState()->AllHostagesGone();
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("Affirmative");
|
|
}
|
|
|
|
// A teammate told us a CT is talking to a hostage
|
|
void BotHostageBeingTakenMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
pReceiver->GetGameState()->HostageWasTaken();
|
|
|
|
// if we're busy, ignore
|
|
if (pReceiver->IsBusy())
|
|
return;
|
|
|
|
pReceiver->Idle();
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("Affirmative");
|
|
}
|
|
|
|
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 (!UTIL_IsValidPlayer(pPlayer))
|
|
continue;
|
|
|
|
if (FStrEq(STRING(pPlayer->pev->netname), ""))
|
|
continue;
|
|
|
|
// ignore dead humans
|
|
if (!pPlayer->IsBot() && !pPlayer->IsAlive())
|
|
continue;
|
|
|
|
// ignore enemies, since we can't hear them talk
|
|
if (m_me->BotRelationship(pPlayer) == CCSBot::BOT_ENEMY)
|
|
continue;
|
|
|
|
// if not a bot, fail the test
|
|
// TODO: Check if human is currently talking
|
|
if (!pPlayer->IsBot())
|
|
continue;
|
|
|
|
CCSBot *pBot = static_cast<CCSBot *>(pPlayer);
|
|
auto say = pBot->GetChatter()->m_statementList;
|
|
while (say)
|
|
{
|
|
// if this statement is currently being spoken, return it
|
|
if (say->IsSpeaking())
|
|
return say;
|
|
|
|
// keep track of statement that has been waiting longest to be spoken of anyone on our team
|
|
if (say->GetStartTime() < earlyTime)
|
|
{
|
|
earlyTime = say->GetTimestamp();
|
|
earliest = say;
|
|
}
|
|
|
|
say = say->m_next;
|
|
}
|
|
}
|
|
|
|
// make sure it is time to start this statement
|
|
if (earliest && earliest->GetStartTime() > gpGlobals->time)
|
|
return nullptr;
|
|
|
|
return earliest;
|
|
}
|
|
|
|
// Return true if we speaking makes sense now
|
|
bool BotChatterInterface::ShouldSpeak() const
|
|
{
|
|
// don't talk to non-existent friends
|
|
if (m_me->GetFriendsRemaining() == 0)
|
|
return false;
|
|
|
|
// if everyone is together, no need to tell them what's going on
|
|
if (m_me->GetNearbyFriendCount() == m_me->GetFriendsRemaining())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
float BotChatterInterface::GetRadioSilenceDuration()
|
|
{
|
|
#ifdef REGAMEDLL_FIXES
|
|
if (m_me->m_iTeam != CT && m_me->m_iTeam != TERRORIST)
|
|
return 0;
|
|
#endif
|
|
|
|
return m_radioSilenceInterval[m_me->m_iTeam - 1].GetElapsedTime();
|
|
}
|
|
|
|
void BotChatterInterface::ResetRadioSilenceDuration()
|
|
{
|
|
#ifdef REGAMEDLL_FIXES
|
|
if (m_me->m_iTeam != CT && m_me->m_iTeam != TERRORIST)
|
|
return;
|
|
#endif
|
|
|
|
m_radioSilenceInterval[m_me->m_iTeam - 1].Reset();
|
|
}
|
|
|
|
inline void SayWhere(BotStatement *say, Place place)
|
|
{
|
|
say->AppendPhrase(TheBotPhrases->GetPlace(place));
|
|
}
|
|
|
|
// Report enemy sightings
|
|
void BotChatterInterface::EnemySpotted()
|
|
{
|
|
// NOTE: This could be a few seconds out of date (enemy is in an adjacent place)
|
|
Place place = m_me->GetEnemyPlace();
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_VISIBLE_ENEMIES, 10.0f);
|
|
|
|
// where are the enemies
|
|
say->AppendPhrase(TheBotPhrases->GetPlace(place));
|
|
|
|
// how many are there
|
|
say->AppendPhrase(BotStatement::ACCUMULATE_ENEMIES_DELAY);
|
|
say->AppendPhrase(BotStatement::CURRENT_ENEMY_COUNT);
|
|
say->AddCondition(BotStatement::IS_IN_COMBAT);
|
|
|
|
AddStatement(say);
|
|
}
|
|
|
|
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);
|
|
}
|