mirror of
https://github.com/s1lentq/ReGameDLL_CS.git
synced 2025-04-10 11:40:02 +03:00
Friendly fire fixes Bot Chatter: Bots will react to enemy snipers presence previously unused chatter on both 1.6 and czero but is used on cs source Probably fixed where teammate that use sniperrifles looking at bots that don't use sniperrifles got warned
2273 lines
53 KiB
C++
2273 lines
53 KiB
C++
/*
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at
|
|
* your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*
|
|
* In addition, as a special exception, the author gives permission to
|
|
* link the code of this program with the Half-Life Game Engine ("HL
|
|
* Engine") and Modified Game Libraries ("MODs") developed by Valve,
|
|
* L.L.C ("Valve"). You must obey the GNU General Public License in all
|
|
* respects for all of the code used other than the HL Engine and MODs
|
|
* from Valve. If you modify this file, you may extend this exception
|
|
* to your version of the file, but you are not obligated to do so. If
|
|
* you do not wish to do so, delete this exception statement from your
|
|
* version.
|
|
*
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
#include <algorithm>
|
|
|
|
BotPhraseManager *TheBotPhrases = nullptr;
|
|
CountdownTimer BotChatterInterface::m_encourageTimer;
|
|
IntervalTimer BotChatterInterface::m_radioSilenceInterval[2];
|
|
|
|
const Vector *GetRandomSpotAtPlace(Place place)
|
|
{
|
|
int count = 0;
|
|
int which;
|
|
|
|
for (auto area : TheNavAreaList)
|
|
{
|
|
if (area->GetPlace() == place)
|
|
count++;
|
|
}
|
|
|
|
if (count == 0)
|
|
return nullptr;
|
|
|
|
which = RANDOM_LONG(0, count - 1);
|
|
|
|
for (auto area : TheNavAreaList)
|
|
{
|
|
if (area->GetPlace() == place && which == 0)
|
|
return area->GetCenter();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Transmit meme to other bots
|
|
void BotMeme::Transmit(CCSBot *pSender) const
|
|
{
|
|
for (int i = 1; i <= gpGlobals->maxClients; i++)
|
|
{
|
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex(i);
|
|
|
|
if (!UTIL_IsValidPlayer(pPlayer))
|
|
continue;
|
|
|
|
if (FStrEq(STRING(pPlayer->pev->netname), ""))
|
|
continue;
|
|
|
|
// skip self
|
|
if (pSender == pPlayer)
|
|
continue;
|
|
|
|
// ignore dead humans
|
|
if (!pPlayer->IsBot() && !pPlayer->IsAlive())
|
|
continue;
|
|
|
|
// ignore enemies, since we can't hear them talk
|
|
if (pSender->BotRelationship(pPlayer) == CCSBot::BOT_ENEMY)
|
|
continue;
|
|
|
|
// if not a bot, fail the test
|
|
if (!pPlayer->IsBot())
|
|
continue;
|
|
|
|
// allow bot to interpret our meme
|
|
Interpret(pSender, (CCSBot *)pPlayer);
|
|
}
|
|
}
|
|
|
|
// A teammate called for help - respond
|
|
void BotHelpMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
const float maxHelpRange = 3000.0f; // 2000
|
|
pReceiver->RespondToHelpRequest(pSender, m_place, maxHelpRange);
|
|
}
|
|
|
|
// A teammate reported information about a bombsite
|
|
void BotBombsiteStatusMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
// remember this bombsite's status
|
|
if (m_status == CLEAR)
|
|
pReceiver->GetGameState()->ClearBombsite(m_zoneIndex);
|
|
else
|
|
pReceiver->GetGameState()->MarkBombsiteAsPlanted(m_zoneIndex);
|
|
|
|
// if we were heading to the just-cleared bombsite, pick another one to search
|
|
// if our target bombsite wasn't cleared, will will continue going to it,
|
|
// because GetNextBombsiteToSearch() will return the same zone (since its not cleared)
|
|
// if the bomb was planted, we will head to that bombsite
|
|
if (pReceiver->GetTask() == CCSBot::FIND_TICKING_BOMB)
|
|
{
|
|
pReceiver->Idle();
|
|
pReceiver->GetChatter()->Affirmative();
|
|
}
|
|
}
|
|
|
|
// A teammate reported information about the bomb
|
|
void BotBombStatusMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
// update our gamestate based on teammate's report
|
|
switch (m_state)
|
|
{
|
|
case CSGameState::MOVING:
|
|
{
|
|
pReceiver->GetGameState()->UpdateBomber(&m_pos);
|
|
|
|
// if we are hunting and see no enemies, respond
|
|
if (!pReceiver->IsRogue() && pReceiver->IsHunting() && pReceiver->GetNearbyEnemyCount() == 0)
|
|
pReceiver->RespondToHelpRequest(pSender, TheNavAreaGrid.GetPlace(&m_pos));
|
|
|
|
break;
|
|
}
|
|
case CSGameState::LOOSE:
|
|
{
|
|
pReceiver->GetGameState()->UpdateLooseBomb(&m_pos);
|
|
|
|
if (pReceiver->GetTask() == CCSBot::GUARD_BOMB_ZONE)
|
|
{
|
|
pReceiver->Idle();
|
|
pReceiver->GetChatter()->Affirmative();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// A teammate has asked that we follow him
|
|
void BotFollowMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
if (pReceiver->IsRogue())
|
|
return;
|
|
|
|
// if we're busy, ignore
|
|
if (pReceiver->IsBusy())
|
|
return;
|
|
|
|
PathCost pathCost(pReceiver);
|
|
float travelDistance = NavAreaTravelDistance(pReceiver->GetLastKnownArea(), TheNavAreaGrid.GetNearestNavArea(&pSender->pev->origin), pathCost);
|
|
if (travelDistance < 0.0f)
|
|
return;
|
|
|
|
const float tooFar = 1000.0f;
|
|
if (travelDistance > tooFar)
|
|
return;
|
|
|
|
// begin following
|
|
pReceiver->Follow(pSender);
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("CoveringFriend");
|
|
}
|
|
|
|
// A teammate has asked us to defend a place
|
|
void BotDefendHereMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
if (pReceiver->IsRogue())
|
|
return;
|
|
|
|
// if we're busy, ignore
|
|
if (pReceiver->IsBusy())
|
|
return;
|
|
|
|
Place place = TheNavAreaGrid.GetPlace(&m_pos);
|
|
if (place != UNDEFINED_PLACE)
|
|
{
|
|
// pick a random hiding spot in this place
|
|
const Vector *spot = FindRandomHidingSpot(pReceiver, place, pReceiver->IsSniper());
|
|
if (spot)
|
|
{
|
|
pReceiver->SetTask(CCSBot::HOLD_POSITION);
|
|
pReceiver->Hide(spot);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// hide nearby
|
|
pReceiver->SetTask(CCSBot::HOLD_POSITION);
|
|
pReceiver->Hide(TheNavAreaGrid.GetNearestNavArea(&m_pos));
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("Affirmative");
|
|
}
|
|
|
|
// A teammate has asked where the bomb is planted
|
|
void BotWhereBombMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
int zone = pReceiver->GetGameState()->GetPlantedBombsite();
|
|
if (zone != CSGameState::UNKNOWN)
|
|
{
|
|
pReceiver->GetChatter()->FoundPlantedBomb(zone);
|
|
}
|
|
}
|
|
|
|
// A teammate has asked us to report in
|
|
void BotRequestReportMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
pReceiver->GetChatter()->ReportingIn();
|
|
}
|
|
|
|
// A teammate told us all the hostages are gone
|
|
void BotAllHostagesGoneMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
pReceiver->GetGameState()->AllHostagesGone();
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("Affirmative");
|
|
}
|
|
|
|
// A teammate told us a CT is talking to a hostage
|
|
void BotHostageBeingTakenMeme::Interpret(CCSBot *pSender, CCSBot *pReceiver) const
|
|
{
|
|
pReceiver->GetGameState()->HostageWasTaken();
|
|
|
|
// if we're busy, ignore
|
|
if (pReceiver->IsBusy())
|
|
return;
|
|
|
|
pReceiver->Idle();
|
|
|
|
// acknowledge
|
|
pReceiver->GetChatter()->Say("Affirmative");
|
|
}
|
|
|
|
#ifdef REGAMEDLL_ADD
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate warned about snipers, so we shouldn't warn again for awhile
|
|
*/
|
|
void BotWarnSniperMeme::Interpret(CCSBot* sender, CCSBot* receiver) const
|
|
{
|
|
receiver->GetChatter()->FriendSpottedSniper();
|
|
}
|
|
#endif
|
|
|
|
BotSpeakable::BotSpeakable()
|
|
{
|
|
m_phrase = nullptr;
|
|
}
|
|
|
|
BotSpeakable::~BotSpeakable()
|
|
{
|
|
if (m_phrase)
|
|
{
|
|
delete[] m_phrase;
|
|
m_phrase = nullptr;
|
|
}
|
|
}
|
|
|
|
BotPhrase::BotPhrase(unsigned int id, bool isPlace)
|
|
{
|
|
m_name = nullptr;
|
|
m_id = id;
|
|
m_isPlace = isPlace;
|
|
m_radioEvent = EVENT_INVALID;
|
|
m_isImportant = false;
|
|
|
|
ClearCriteria();
|
|
|
|
m_numVoiceBanks = 0;
|
|
InitVoiceBank(0);
|
|
}
|
|
|
|
BotPhrase::~BotPhrase()
|
|
{
|
|
for (size_t bank = 0; bank < m_voiceBank.size(); bank++)
|
|
{
|
|
for (size_t speakable = 0; speakable < m_voiceBank[bank]->size(); speakable++)
|
|
{
|
|
delete (*m_voiceBank[bank])[speakable];
|
|
}
|
|
|
|
delete m_voiceBank[bank];
|
|
}
|
|
|
|
if (m_name)
|
|
{
|
|
delete[] m_name;
|
|
m_name = nullptr;
|
|
}
|
|
}
|
|
|
|
void BotPhrase::InitVoiceBank(int bankIndex)
|
|
{
|
|
while (m_numVoiceBanks <= bankIndex)
|
|
{
|
|
m_count.push_back(0);
|
|
m_index.push_back(0);
|
|
m_voiceBank.push_back(new BotSpeakableVector);
|
|
m_numVoiceBanks++;
|
|
}
|
|
}
|
|
|
|
// Return a random speakable - avoid repeating
|
|
char *BotPhrase::GetSpeakable(int bankIndex, float *duration) const
|
|
{
|
|
if (bankIndex < 0 || bankIndex >= m_numVoiceBanks || m_count[bankIndex] == 0)
|
|
{
|
|
if (duration)
|
|
*duration = 0.0f;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// find phrase that meets the current criteria
|
|
int start = m_index[bankIndex];
|
|
while (true)
|
|
{
|
|
BotSpeakableVector *speakables = m_voiceBank[bankIndex];
|
|
int &index = m_index[bankIndex];
|
|
|
|
const BotSpeakable *speak = (*speakables)[index++];
|
|
|
|
if (m_index[bankIndex] >= m_count[bankIndex])
|
|
m_index[bankIndex] = 0;
|
|
|
|
// check place criteria
|
|
// if this speakable has a place criteria, it must match to be used
|
|
// speakables with Place of ANY will match any place
|
|
// speakables with a specific Place will only be used if Place matches
|
|
// speakables with Place of UNDEFINED only match Place of UNDEFINED
|
|
if (speak->m_place == ANY_PLACE || speak->m_place == m_placeCriteria)
|
|
{
|
|
// check count criteria
|
|
// if this speakable has a count criteria, it must match to be used
|
|
// if this speakable does not have a count criteria, we dont care what the count is set to
|
|
if (speak->m_count == UNDEFINED_COUNT || speak->m_count == Q_min(m_countCriteria, (CountCriteria)COUNT_MANY))
|
|
{
|
|
if (duration)
|
|
*duration = speak->m_duration;
|
|
|
|
return speak->m_phrase;
|
|
}
|
|
}
|
|
|
|
// check if we exhausted all speakables
|
|
if (m_index[bankIndex] == start)
|
|
{
|
|
if (duration)
|
|
*duration = 0.0f;
|
|
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Randomly shuffle the speakable order
|
|
void BotPhrase::Randomize()
|
|
{
|
|
for (size_t i = 0; i < m_voiceBank.size(); i++)
|
|
{
|
|
std::random_shuffle(m_voiceBank[i]->begin(), m_voiceBank[i]->end());
|
|
}
|
|
}
|
|
|
|
BotPhraseManager::BotPhraseManager()
|
|
{
|
|
for (int i = 0; i < MAX_PLACES_PER_MAP; i++)
|
|
m_placeStatementHistory[i].timer.Invalidate();
|
|
|
|
m_placeCount = 0;
|
|
}
|
|
|
|
// Invoked when map changes
|
|
void BotPhraseManager::OnMapChange()
|
|
{
|
|
m_placeCount = 0;
|
|
}
|
|
|
|
// Invoked when the round resets
|
|
void BotPhraseManager::OnRoundRestart()
|
|
{
|
|
// effectively reset all interval timers
|
|
m_placeCount = 0;
|
|
|
|
// shuffle all the speakables
|
|
for (auto phrase : m_placeList)
|
|
phrase->Randomize();
|
|
|
|
for (auto phrase : m_list)
|
|
phrase->Randomize();
|
|
}
|
|
|
|
// Initialize phrase system from database file
|
|
bool BotPhraseManager::Initialize(const char *filename, int bankIndex)
|
|
{
|
|
bool isDefault = (bankIndex == 0);
|
|
int phraseDataLength;
|
|
char *phraseDataFile = (char *)LOAD_FILE_FOR_ME((char *)filename, &phraseDataLength);
|
|
|
|
if (!phraseDataFile)
|
|
{
|
|
if (AreBotsAllowed())
|
|
{
|
|
CONSOLE_ECHO("WARNING: Cannot access bot phrase database '%s'\n", filename);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
char *token;
|
|
char *phraseData = phraseDataFile;
|
|
unsigned int nextID = 1;
|
|
|
|
// wav filenames need to be shorter than this to go over the net anyway.
|
|
const int RadioPathLen = 128;
|
|
char baseDir[RadioPathLen] = "";
|
|
char compositeFilename[RadioPathLen];
|
|
|
|
#ifdef REGAMEDLL_ADD
|
|
char filePath[MAX_PATH];
|
|
#endif
|
|
|
|
// Parse the BotChatter.db into BotPhrase collections
|
|
while (true)
|
|
{
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
break;
|
|
|
|
token = SharedGetToken();
|
|
if (!Q_stricmp(token, "BaseDir"))
|
|
{
|
|
// get name of this output device
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO("Error parsing '%s' - expected identifier\n", filename);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
Q_strncpy(baseDir, token, RadioPathLen);
|
|
baseDir[RadioPathLen - 1] = '\0';
|
|
}
|
|
else if (!Q_stricmp(token, "Place") || !Q_stricmp(token, "Chatter"))
|
|
{
|
|
bool isPlace = (Q_stricmp(token, "Place") == 0);
|
|
|
|
// encountered a new phrase collection
|
|
BotPhrase *phrase = nullptr;
|
|
if (isDefault)
|
|
{
|
|
phrase = new BotPhrase(nextID++, isPlace);
|
|
}
|
|
|
|
// get name of this phrase
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
{
|
|
if (phrase) delete phrase;
|
|
CONSOLE_ECHO("Error parsing '%s' - expected identifier\n", filename);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
if (isDefault)
|
|
{
|
|
phrase->m_name = CloneString(token);
|
|
}
|
|
// look up the existing phrase
|
|
else
|
|
{
|
|
if (isPlace)
|
|
{
|
|
phrase = const_cast<BotPhrase *>(GetPlace(token));
|
|
}
|
|
else
|
|
{
|
|
phrase = const_cast<BotPhrase *>(GetPhrase(token));
|
|
}
|
|
|
|
if (!phrase)
|
|
{
|
|
CONSOLE_ECHO("Error parsing '%s' - phrase '%s' is invalid\n", filename, token);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
phrase->InitVoiceBank(bankIndex);
|
|
|
|
PlaceCriteria placeCriteria = ANY_PLACE;
|
|
CountCriteria countCriteria = UNDEFINED_COUNT;
|
|
GameEventType radioEvent = EVENT_INVALID;
|
|
bool isImportant = false;
|
|
|
|
// read attributes of this phrase
|
|
while (true)
|
|
{
|
|
// get next token
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO("Error parsing %s - expected 'End'\n", filename);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
|
|
// check for Place criteria
|
|
if (!Q_stricmp(token, "Place"))
|
|
{
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO("Error parsing %s - expected Place name\n", filename);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
|
|
// update place criteria for subsequent speak lines
|
|
// NOTE: this assumes places must be first in the chatter database
|
|
|
|
// check for special identifiers
|
|
if (!Q_stricmp("ANY", token))
|
|
placeCriteria = ANY_PLACE;
|
|
else if (!Q_stricmp("UNDEFINED", token))
|
|
placeCriteria = UNDEFINED_PLACE;
|
|
else
|
|
{
|
|
placeCriteria = TheBotPhrases->NameToID(token);
|
|
|
|
if (!TheBotPhrases->IsValid() && placeCriteria == UNDEFINED_PLACE)
|
|
placeCriteria = TheNavAreaGrid.NameToID(token);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// check for Count criteria
|
|
if (!Q_stricmp(token, "Count"))
|
|
{
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO("Error parsing %s - expected Count value\n", filename);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
|
|
// update count criteria for subsequent speak lines
|
|
if (!Q_stricmp(token, "Many"))
|
|
countCriteria = COUNT_MANY;
|
|
else
|
|
countCriteria = Q_atoi(token);
|
|
|
|
continue;
|
|
}
|
|
|
|
// check for radio equivalent
|
|
if (!Q_stricmp(token, "Radio"))
|
|
{
|
|
phraseData = SharedParse(phraseData);
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO("Error parsing %s - expected radio event\n", filename);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
GameEventType event = NameToGameEvent(token);
|
|
if (event <= EVENT_START_RADIO_1 || event >= EVENT_END_RADIO)
|
|
{
|
|
CONSOLE_ECHO("Error parsing %s - invalid radio event '%s'\n", filename, token);
|
|
FREE_FILE(phraseDataFile);
|
|
return false;
|
|
}
|
|
|
|
radioEvent = event;
|
|
continue;
|
|
}
|
|
|
|
// check for "important" flag
|
|
if (!Q_stricmp(token, "Important"))
|
|
{
|
|
isImportant = true;
|
|
continue;
|
|
}
|
|
|
|
// check for End delimiter
|
|
if (!Q_stricmp(token, "End"))
|
|
break;
|
|
|
|
#ifdef REGAMEDLL_ADD
|
|
Q_snprintf(filePath, sizeof(filePath), "sound\\%s%s", baseDir, token);
|
|
|
|
if (!g_pFileSystem->FileExists(filePath))
|
|
continue;
|
|
#endif
|
|
|
|
// found a phrase - add it to the collection
|
|
BotSpeakable *speak = new BotSpeakable;
|
|
if (baseDir[0])
|
|
{
|
|
Q_snprintf(compositeFilename, RadioPathLen, "%s%s", baseDir, token);
|
|
speak->m_phrase = CloneString(compositeFilename);
|
|
}
|
|
else
|
|
{
|
|
speak->m_phrase = CloneString(token);
|
|
}
|
|
|
|
speak->m_place = placeCriteria;
|
|
speak->m_count = countCriteria;
|
|
|
|
Q_snprintf(compositeFilename, RadioPathLen, "sound\\%s", speak->m_phrase);
|
|
speak->m_duration = (double)GET_APPROX_WAVE_PLAY_LEN(compositeFilename) / 1000.0f;
|
|
|
|
if (speak->m_duration <= 0.0f)
|
|
{
|
|
CONSOLE_ECHO("Warning: Couldn't get duration of phrase '%s'\n", compositeFilename);
|
|
speak->m_duration = 1.0f;
|
|
}
|
|
|
|
BotSpeakableVector *speakables = phrase->m_voiceBank[bankIndex];
|
|
speakables->push_back(speak);
|
|
|
|
phrase->m_count[bankIndex]++;
|
|
}
|
|
|
|
if (isDefault)
|
|
{
|
|
phrase->m_radioEvent = radioEvent;
|
|
phrase->m_isImportant = isImportant;
|
|
}
|
|
|
|
// add phrase collection to the appropriate master list
|
|
if (isPlace)
|
|
m_placeList.push_back(phrase);
|
|
else
|
|
m_list.push_back(phrase);
|
|
}
|
|
}
|
|
|
|
FREE_FILE(phraseDataFile);
|
|
return true;
|
|
}
|
|
|
|
BotPhraseManager::~BotPhraseManager()
|
|
{
|
|
for (auto phrase : m_list)
|
|
delete phrase;
|
|
|
|
for (auto phrase : m_placeList)
|
|
delete phrase;
|
|
|
|
m_list.clear();
|
|
m_placeList.clear();
|
|
}
|
|
|
|
Place BotPhraseManager::NameToID(const char *name) const
|
|
{
|
|
for (auto phrase : m_placeList)
|
|
{
|
|
if (!Q_stricmp(phrase->m_name, name))
|
|
return phrase->m_id;
|
|
}
|
|
|
|
for (auto phrase : m_list)
|
|
{
|
|
if (!Q_stricmp(phrase->m_name, name))
|
|
return phrase->m_id;
|
|
}
|
|
|
|
return UNDEFINED_PLACE;
|
|
}
|
|
|
|
const char *BotPhraseManager::IDToName(Place id) const
|
|
{
|
|
for (auto phrase : m_placeList)
|
|
{
|
|
if (phrase->m_id == id)
|
|
return phrase->m_name;
|
|
}
|
|
|
|
for (auto phrase : m_list)
|
|
{
|
|
if (phrase->m_id == id)
|
|
return phrase->m_name;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Given a name, return the associated phrase collection
|
|
const BotPhrase *BotPhraseManager::GetPhrase(const char *name) const
|
|
{
|
|
for (auto phrase : m_list)
|
|
{
|
|
if (!Q_stricmp(phrase->m_name, name))
|
|
return phrase;
|
|
}
|
|
|
|
//CONSOLE_ECHO("GetPhrase: ERROR - Invalid phrase '%s'\n", name);
|
|
return nullptr;
|
|
}
|
|
|
|
// Given an id, return the associated phrase collection
|
|
// TODO: Store phrases in a vector to make this fast
|
|
const BotPhrase *BotPhraseManager::GetPhrase(unsigned int id) const
|
|
{
|
|
for (auto phrase : m_list)
|
|
{
|
|
if (phrase->m_id == id)
|
|
return phrase;
|
|
}
|
|
|
|
CONSOLE_ECHO("GetPhrase: ERROR - Invalid phrase id #%d\n", id);
|
|
return nullptr;
|
|
}
|
|
|
|
// Given a name, return the associated Place phrase collection
|
|
const BotPhrase *BotPhraseManager::GetPlace(const char *name) const
|
|
{
|
|
if (!name)
|
|
return nullptr;
|
|
|
|
for (auto phrase : m_placeList)
|
|
{
|
|
if (!Q_stricmp(phrase->m_name, name))
|
|
return phrase;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Given a place, return the associated Place phrase collection
|
|
const BotPhrase *BotPhraseManager::GetPlace(PlaceCriteria place) const
|
|
{
|
|
if (place == UNDEFINED_PLACE)
|
|
return nullptr;
|
|
|
|
for (auto phrase : m_placeList)
|
|
{
|
|
if (phrase->m_id == place)
|
|
return phrase;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
BotStatement::BotStatement(BotChatterInterface *chatter, BotStatementType type, float expireDuration)
|
|
{
|
|
m_chatter = chatter;
|
|
|
|
m_prev = m_next = nullptr;
|
|
m_timestamp = gpGlobals->time;
|
|
m_speakTimestamp = 0.0f;
|
|
|
|
m_type = type;
|
|
m_subject = UNDEFINED_SUBJECT;
|
|
m_place = UNDEFINED_PLACE;
|
|
m_meme = nullptr;
|
|
|
|
m_startTime = gpGlobals->time;
|
|
m_expireTime = gpGlobals->time + expireDuration;
|
|
m_isSpeaking = false;
|
|
|
|
m_nextTime = 0.0f;
|
|
m_index = -1;
|
|
m_count = 0;
|
|
|
|
m_conditionCount = 0;
|
|
}
|
|
|
|
BotStatement::~BotStatement()
|
|
{
|
|
if (m_meme)
|
|
{
|
|
delete m_meme;
|
|
m_meme = nullptr;
|
|
}
|
|
}
|
|
|
|
CCSBot *BotStatement::GetOwner() const
|
|
{
|
|
return m_chatter->GetOwner();
|
|
}
|
|
|
|
// Attach a meme to this statement, to be transmitted to other friendly bots when spoken
|
|
void BotStatement::AttachMeme(BotMeme *meme)
|
|
{
|
|
m_meme = meme;
|
|
}
|
|
|
|
// Add a conditions that must be true for the statement to be spoken
|
|
void BotStatement::AddCondition(ConditionType condition)
|
|
{
|
|
if (m_conditionCount < MAX_BOT_CONDITIONS)
|
|
m_condition[m_conditionCount++] = condition;
|
|
}
|
|
|
|
// Return true if this statement is "important" and not personality chatter
|
|
bool BotStatement::IsImportant() const
|
|
{
|
|
// if a statement contains any important phrases, it is important
|
|
for (int i = 0; i < m_count; i++)
|
|
{
|
|
if (m_statement[i].isPhrase && m_statement[i].phrase->IsImportant())
|
|
return true;
|
|
|
|
// hack for now - phrases with enemy counts are important
|
|
if (!m_statement[i].isPhrase && m_statement[i].context == BotStatement::CURRENT_ENEMY_COUNT)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Verify all attached conditions
|
|
bool BotStatement::IsValid() const
|
|
{
|
|
for (int i = 0; i < m_conditionCount; i++)
|
|
{
|
|
switch (m_condition[i])
|
|
{
|
|
case IS_IN_COMBAT:
|
|
{
|
|
if (!GetOwner()->IsAttacking())
|
|
return false;
|
|
break;
|
|
}
|
|
/*case RADIO_SILENCE:
|
|
{
|
|
if (GetOwner()->GetChatter()->GetRadioSilenceDuration() < 10.0f)
|
|
return false;
|
|
break;
|
|
}*/
|
|
case ENEMIES_REMAINING:
|
|
{
|
|
if (GetOwner()->GetEnemiesRemaining() == 0)
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Return true if this statement is essentially the same as the given one
|
|
bool BotStatement::IsRedundant(const BotStatement *say) const
|
|
{
|
|
// special cases
|
|
if (GetType() == REPORT_MY_PLAN ||
|
|
GetType() == REPORT_REQUEST_HELP ||
|
|
GetType() == REPORT_CRITICAL_EVENT ||
|
|
GetType() == REPORT_ACKNOWLEDGE)
|
|
return false;
|
|
|
|
// check if topics are different
|
|
if (say->GetType() != GetType())
|
|
return false;
|
|
|
|
if (!say->HasPlace() && !HasPlace() && !say->HasSubject() && !HasSubject())
|
|
{
|
|
// neither has place or subject, so they are the same
|
|
return true;
|
|
}
|
|
|
|
// check if subject matter is the same
|
|
if (say->HasPlace() && HasPlace() && say->GetPlace() == GetPlace())
|
|
{
|
|
// talking about the same place
|
|
return true;
|
|
}
|
|
|
|
if (say->HasSubject() && HasSubject() && say->GetSubject() == GetSubject())
|
|
{
|
|
// talking about the same player
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Return true if this statement is no longer appropriate to say
|
|
bool BotStatement::IsObsolete() const
|
|
{
|
|
// if the round is over, the only things we should say are emotes
|
|
if (GetOwner()->GetGameState()->IsRoundOver())
|
|
{
|
|
if (m_type != REPORT_EMOTE)
|
|
return true;
|
|
}
|
|
|
|
#if 0
|
|
// If we're wanting to say "I lost him" but we've spotted another enemy,
|
|
// we no longer need to report losing someone.
|
|
if (GetOwner()->GetChatter()->SeesAtLeastOneEnemy() && m_type == REPORT_ENEMY_LOST)
|
|
{
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// check if statement lifetime has expired
|
|
return (gpGlobals->time > m_expireTime);
|
|
}
|
|
|
|
// Possibly change what were going to say base on what teammate is saying
|
|
void BotStatement::Convert(const BotStatement *say)
|
|
{
|
|
if (GetType() == REPORT_MY_PLAN && say->GetType() == REPORT_MY_PLAN)
|
|
{
|
|
static const BotPhrase *meToo = TheBotPhrases->GetPhrase("AgreeWithPlan");
|
|
|
|
// don't reconvert
|
|
if (m_statement[0].phrase == meToo)
|
|
return;
|
|
|
|
// if our plans are the same, change our statement to "me too"
|
|
if (m_statement[0].phrase == say->m_statement[0].phrase)
|
|
{
|
|
if (m_place == say->m_place)
|
|
{
|
|
// same plan at the same place - convert to "me too"
|
|
m_statement[0].phrase = meToo;
|
|
m_startTime = gpGlobals->time + RANDOM_FLOAT(0.5f, 1.0f);
|
|
}
|
|
else
|
|
{
|
|
// same plan at different place - wait a bit to allow others to respond "me too"
|
|
m_startTime = gpGlobals->time + RANDOM_FLOAT(3.0f, 4.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotStatement::AppendPhrase(const BotPhrase *phrase)
|
|
{
|
|
if (!phrase)
|
|
return;
|
|
|
|
if (m_count < MAX_BOT_PHRASES)
|
|
{
|
|
m_statement[m_count].isPhrase = true;
|
|
m_statement[m_count].phrase = phrase;
|
|
m_count++;
|
|
}
|
|
}
|
|
|
|
// Special phrases that depend on the context
|
|
void BotStatement::AppendPhrase(ContextType contextPhrase)
|
|
{
|
|
if (m_count < MAX_BOT_PHRASES)
|
|
{
|
|
m_statement[m_count].isPhrase = false;
|
|
m_statement[m_count].context = contextPhrase;
|
|
m_count++;
|
|
}
|
|
}
|
|
|
|
// Say our statement
|
|
// m_index refers to the phrase currently being spoken, or -1 if we havent started yet
|
|
bool BotStatement::Update()
|
|
{
|
|
CCSBot *me = GetOwner();
|
|
|
|
// if all of our teammates are dead, the only non-redundant statements are emotes
|
|
if (me->GetFriendsRemaining() == 0 && GetType() != REPORT_EMOTE)
|
|
return false;
|
|
|
|
if (!m_isSpeaking)
|
|
{
|
|
m_isSpeaking = true;
|
|
m_speakTimestamp = gpGlobals->time;
|
|
}
|
|
|
|
// special case - context dependent delay
|
|
if (m_index >= 0 && m_statement[m_index].context == ACCUMULATE_ENEMIES_DELAY)
|
|
{
|
|
// report if we see a lot of enemies, or if enough time has passed
|
|
const float reportTime = 2.0f;
|
|
if (me->GetNearbyEnemyCount() > 3 || gpGlobals->time - m_speakTimestamp > reportTime)
|
|
{
|
|
// enough enemies have accumulated to expire this delay
|
|
m_nextTime = 0.0f;
|
|
}
|
|
}
|
|
|
|
if (gpGlobals->time > m_nextTime)
|
|
{
|
|
// check for end of statement
|
|
if (++m_index == m_count)
|
|
{
|
|
// transmit any memes carried in this statement to our teammates
|
|
if (m_meme)
|
|
{
|
|
m_meme->Transmit(me);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// start next part of statement
|
|
float duration = 0.0f;
|
|
const BotPhrase *phrase = nullptr;
|
|
|
|
if (m_statement[m_index].isPhrase)
|
|
{
|
|
// normal phrase
|
|
phrase = m_statement[m_index].phrase;
|
|
}
|
|
else
|
|
{
|
|
// context-dependant phrase
|
|
switch (m_statement[m_index].context)
|
|
{
|
|
case CURRENT_ENEMY_COUNT:
|
|
{
|
|
int enemyCount = me->GetNearbyEnemyCount();
|
|
|
|
// if we are outnumbered, ask for help
|
|
if (enemyCount - 1 > me->GetNearbyFriendCount())
|
|
{
|
|
phrase = TheBotPhrases->GetPhrase("Help");
|
|
AttachMeme(new BotHelpMeme());
|
|
}
|
|
else if (enemyCount > 1)
|
|
{
|
|
phrase = TheBotPhrases->GetPhrase("EnemySpotted");
|
|
#ifdef REGAMEDLL_FIXES
|
|
if (phrase)
|
|
#endif
|
|
{
|
|
phrase->SetCountCriteria(enemyCount);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case REMAINING_ENEMY_COUNT:
|
|
{
|
|
static const char *speak[] =
|
|
{
|
|
"NoEnemiesLeft", "OneEnemyLeft", "TwoEnemiesLeft", "ThreeEnemiesLeft"
|
|
};
|
|
|
|
int enemyCount = me->GetEnemiesRemaining();
|
|
|
|
// dont report if there are lots of enemies left
|
|
if (enemyCount < 0 || enemyCount > 3)
|
|
{
|
|
phrase = nullptr;
|
|
}
|
|
else
|
|
{
|
|
phrase = TheBotPhrases->GetPhrase(speak[enemyCount]);
|
|
}
|
|
break;
|
|
}
|
|
case SHORT_DELAY:
|
|
{
|
|
m_nextTime = gpGlobals->time + RANDOM_FLOAT(0.1f, 0.5f);
|
|
return true;
|
|
}
|
|
case LONG_DELAY:
|
|
{
|
|
m_nextTime = gpGlobals->time + RANDOM_FLOAT(1.0f, 2.0f);
|
|
return true;
|
|
}
|
|
case ACCUMULATE_ENEMIES_DELAY:
|
|
{
|
|
// wait until test becomes true
|
|
m_nextTime = 99999999.9f;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (phrase)
|
|
{
|
|
// if chatter system is in "standard radio" mode, send the equivalent radio command
|
|
if (me->GetChatter()->GetVerbosity() == BotChatterInterface::RADIO)
|
|
{
|
|
GameEventType radioEvent = phrase->GetRadioEquivalent();
|
|
if (radioEvent == EVENT_INVALID)
|
|
{
|
|
// skip directly to the next phrase
|
|
m_nextTime = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// use the standard radio
|
|
me->GetChatter()->ResetRadioSilenceDuration();
|
|
me->SendRadioMessage(radioEvent);
|
|
duration = 2.0f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// set place criteria
|
|
phrase->SetPlaceCriteria(m_place);
|
|
|
|
const char *filename = phrase->GetSpeakable(me->GetProfile()->GetVoiceBank(), &duration);
|
|
// CONSOLE_ECHO("%s: Radio('%s')\n", STRING(me->pev->netname), filename);
|
|
|
|
bool sayIt = true;
|
|
if (phrase->IsPlace())
|
|
{
|
|
// don't repeat the place if someone just mentioned it not too long ago
|
|
float timeSince = TheBotPhrases->GetPlaceStatementInterval(phrase->GetID());
|
|
const float minRepeatTime = 20.0f;
|
|
|
|
if (timeSince < minRepeatTime)
|
|
{
|
|
sayIt = false;
|
|
}
|
|
else
|
|
{
|
|
TheBotPhrases->ResetPlaceStatementInterval(phrase->GetID());
|
|
}
|
|
}
|
|
|
|
if (sayIt)
|
|
{
|
|
if (!filename)
|
|
{
|
|
GameEventType radioEvent = phrase->GetRadioEquivalent();
|
|
if (radioEvent == EVENT_INVALID)
|
|
{
|
|
// skip directly to the next phrase
|
|
m_nextTime = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
me->SendRadioMessage(radioEvent);
|
|
me->GetChatter()->ResetRadioSilenceDuration();
|
|
duration = 2.0f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
me->Radio(filename, nullptr, me->GetProfile()->GetVoicePitch(), false);
|
|
me->GetChatter()->ResetRadioSilenceDuration();
|
|
me->StartVoiceFeedback(duration + 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
const float gap = 0.1f;
|
|
m_nextTime = gpGlobals->time + duration + gap;
|
|
}
|
|
else
|
|
{
|
|
// skip directly to the next phrase
|
|
m_nextTime = 0.0f;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// If this statement refers to a specific place, return that place
|
|
// Places can be implicit in the statement, or explicitly defined
|
|
Place BotStatement::GetPlace() const
|
|
{
|
|
// return any explicitly set place if we have one
|
|
if (m_place != UNDEFINED_PLACE)
|
|
return m_place;
|
|
|
|
// look for an implicit place in our statement
|
|
for (int i = 0; i < m_count; i++)
|
|
{
|
|
if (m_statement[i].isPhrase && m_statement[i].phrase->IsPlace())
|
|
return m_statement[i].phrase->GetID();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Return true if this statement has an associated count
|
|
bool BotStatement::HasCount() const
|
|
{
|
|
for (int i = 0; i < m_count; i++)
|
|
{
|
|
if (!m_statement[i].isPhrase && m_statement[i].context == CURRENT_ENEMY_COUNT)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
enum PitchHack { P_HI, P_NORMAL, P_LOW };
|
|
|
|
static int nextPitch = P_HI;
|
|
|
|
BotChatterInterface::BotChatterInterface(CCSBot *me)
|
|
{
|
|
m_me = me;
|
|
m_statementList = nullptr;
|
|
|
|
switch (nextPitch)
|
|
{
|
|
case P_HI:
|
|
m_pitch = RANDOM_LONG(105, 110);
|
|
break;
|
|
case P_NORMAL:
|
|
m_pitch = RANDOM_LONG(95, 105);
|
|
break;
|
|
case P_LOW:
|
|
m_pitch = RANDOM_LONG(85, 95);
|
|
break;
|
|
}
|
|
|
|
nextPitch = (nextPitch + 1) % 3;
|
|
Reset();
|
|
}
|
|
|
|
BotChatterInterface::~BotChatterInterface()
|
|
{
|
|
// free pending statements
|
|
BotStatement *next;
|
|
for (BotStatement *msg = m_statementList; msg; msg = next)
|
|
{
|
|
next = msg->m_next;
|
|
delete msg;
|
|
}
|
|
}
|
|
|
|
// Reset to initial state
|
|
void BotChatterInterface::Reset()
|
|
{
|
|
BotStatement *msg, *nextMsg;
|
|
|
|
// removing pending statements - except for those about the round results
|
|
for (msg = m_statementList; msg; msg = nextMsg)
|
|
{
|
|
nextMsg = msg->m_next;
|
|
|
|
if (msg->GetType() != REPORT_ROUND_END)
|
|
RemoveStatement(msg);
|
|
}
|
|
|
|
m_seeAtLeastOneEnemy = false;
|
|
m_timeWhenSawFirstEnemy = 0.0f;
|
|
m_reportedEnemies = false;
|
|
m_requestedBombLocation = false;
|
|
|
|
ResetRadioSilenceDuration();
|
|
|
|
m_needBackupInterval.Invalidate();
|
|
m_spottedBomberInterval.Invalidate();
|
|
m_spottedLooseBombTimer.Invalidate();
|
|
m_heardNoiseTimer.Invalidate();
|
|
m_scaredInterval.Invalidate();
|
|
m_planInterval.Invalidate();
|
|
m_encourageTimer.Invalidate();
|
|
m_escortingHostageTimer.Invalidate();
|
|
#ifdef REGAMEDLL_ADD
|
|
m_warnSniperTimer.Invalidate();
|
|
#endif
|
|
}
|
|
|
|
// Register a statement for speaking
|
|
void BotChatterInterface::AddStatement(BotStatement *statement, bool mustAdd)
|
|
{
|
|
// don't add statements if bot chatter is shut off
|
|
if (GetVerbosity() == OFF)
|
|
{
|
|
delete statement;
|
|
return;
|
|
}
|
|
|
|
// if we only want mission-critical radio chatter, ignore non-important phrases
|
|
if (GetVerbosity() == MINIMAL && !statement->IsImportant())
|
|
{
|
|
delete statement;
|
|
return;
|
|
}
|
|
|
|
// don't add statements if we're dead
|
|
if (!m_me->IsAlive() && !mustAdd)
|
|
{
|
|
delete statement;
|
|
return;
|
|
}
|
|
|
|
// don't add empty statements
|
|
if (statement->m_count == 0)
|
|
{
|
|
delete statement;
|
|
return;
|
|
}
|
|
|
|
// don't add statements that are redundant with something we're already waiting to say
|
|
BotStatement *s;
|
|
for (s = m_statementList; s; s = s->m_next)
|
|
{
|
|
if (statement->IsRedundant(s))
|
|
{
|
|
m_me->PrintIfWatched("I tried to say something I'm already saying.\n");
|
|
delete statement;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// keep statements in order of start time
|
|
|
|
// check list is empty
|
|
if (!m_statementList)
|
|
{
|
|
statement->m_next = nullptr;
|
|
statement->m_prev = nullptr;
|
|
m_statementList = statement;
|
|
return;
|
|
}
|
|
|
|
// list has at least one statement on it
|
|
|
|
// insert into list in order
|
|
BotStatement *earlier = nullptr;
|
|
for (s = m_statementList; s; s = s->m_next)
|
|
{
|
|
if (s->GetStartTime() > statement->GetStartTime())
|
|
break;
|
|
|
|
earlier = s;
|
|
}
|
|
|
|
// insert just after "earlier"
|
|
if (earlier)
|
|
{
|
|
if (earlier->m_next)
|
|
earlier->m_next->m_prev = statement;
|
|
|
|
statement->m_next = earlier->m_next;
|
|
|
|
earlier->m_next = statement;
|
|
statement->m_prev = earlier;
|
|
}
|
|
else
|
|
{
|
|
// insert at head
|
|
statement->m_prev = nullptr;
|
|
statement->m_next = m_statementList;
|
|
m_statementList->m_prev = statement;
|
|
m_statementList = statement;
|
|
}
|
|
}
|
|
|
|
// Remove a statement
|
|
void BotChatterInterface::RemoveStatement(BotStatement *statement)
|
|
{
|
|
if (statement->m_next)
|
|
statement->m_next->m_prev = statement->m_prev;
|
|
|
|
if (statement->m_prev)
|
|
statement->m_prev->m_next = statement->m_next;
|
|
else
|
|
m_statementList = statement->m_next;
|
|
|
|
delete statement;
|
|
}
|
|
|
|
// Track nearby enemy count and report enemy activity
|
|
void BotChatterInterface::ReportEnemies()
|
|
{
|
|
if (!m_me->IsAlive())
|
|
return;
|
|
|
|
if (m_me->GetNearbyEnemyCount() == 0)
|
|
{
|
|
m_seeAtLeastOneEnemy = false;
|
|
m_reportedEnemies = false;
|
|
}
|
|
else if (!m_seeAtLeastOneEnemy)
|
|
{
|
|
m_seeAtLeastOneEnemy = true;
|
|
m_timeWhenSawFirstEnemy = gpGlobals->time;
|
|
}
|
|
|
|
// determine whether we should report enemy activity
|
|
if (!m_reportedEnemies && m_seeAtLeastOneEnemy)
|
|
{
|
|
// request backup if we're outnumbered
|
|
if (m_me->IsOutnumbered() && NeedBackup())
|
|
{
|
|
m_reportedEnemies = true;
|
|
return;
|
|
}
|
|
|
|
m_me->GetChatter()->EnemySpotted();
|
|
m_reportedEnemies = true;
|
|
}
|
|
}
|
|
|
|
void BotChatterInterface::OnEvent(GameEventType event, CBaseEntity *pEntity, CBaseEntity *pOther)
|
|
{
|
|
;
|
|
}
|
|
|
|
// Invoked when we die
|
|
void BotChatterInterface::OnDeath()
|
|
{
|
|
if (IsTalking())
|
|
{
|
|
if (m_me->GetChatter()->GetVerbosity() == BotChatterInterface::MINIMAL
|
|
|| m_me->GetChatter()->GetVerbosity() == BotChatterInterface::NORMAL)
|
|
{
|
|
// we've died mid-sentance - emit a gargle of pain
|
|
static const BotPhrase *pain = TheBotPhrases->GetPhrase("pain");
|
|
|
|
if (pain)
|
|
{
|
|
m_me->Radio(pain->GetSpeakable(m_me->GetProfile()->GetVoiceBank()), nullptr, m_me->GetProfile()->GetVoicePitch());
|
|
m_me->GetChatter()->ResetRadioSilenceDuration();
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove all of our statements
|
|
Reset();
|
|
}
|
|
|
|
// Process ongoing chatter for this bot
|
|
void BotChatterInterface::Update()
|
|
{
|
|
// report enemy activity
|
|
ReportEnemies();
|
|
|
|
// ask team to report in if we havent heard anything in awhile
|
|
if (ShouldSpeak())
|
|
{
|
|
const float longTime = 30.0f;
|
|
if (m_me->GetEnemiesRemaining() > 0 && GetRadioSilenceDuration() > longTime)
|
|
{
|
|
ReportIn();
|
|
}
|
|
}
|
|
|
|
// speak if it is our turn
|
|
BotStatement *say = GetActiveStatement();
|
|
if (say)
|
|
{
|
|
// if our statement is active, speak it
|
|
if (say->GetOwner() == m_me)
|
|
{
|
|
if (say->Update() == false)
|
|
{
|
|
// this statement is complete - destroy it
|
|
RemoveStatement(say);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process active statements.
|
|
// Removed expired statements, re-order statements according to their relavence and importance
|
|
// Remove redundant statements (ie: our teammates already said them)
|
|
const BotStatement *friendSay = GetActiveStatement();
|
|
if (friendSay && friendSay->GetOwner() == m_me)
|
|
friendSay = nullptr;
|
|
|
|
BotStatement *nextSay;
|
|
for (say = m_statementList; say; say = nextSay)
|
|
{
|
|
nextSay = say->m_next;
|
|
|
|
// check statement conditions
|
|
if (!say->IsValid())
|
|
{
|
|
RemoveStatement(say);
|
|
continue;
|
|
}
|
|
|
|
// don't interrupt ourselves
|
|
if (say->IsSpeaking())
|
|
continue;
|
|
|
|
// check for obsolete statements
|
|
if (say->IsObsolete())
|
|
{
|
|
m_me->PrintIfWatched("Statement obsolete - removing.\n");
|
|
RemoveStatement(say);
|
|
continue;
|
|
}
|
|
|
|
// if a teammate is saying what we were going to say, dont repeat it
|
|
if (friendSay)
|
|
{
|
|
// convert what we're about to say based on what our teammate is currently saying
|
|
say->Convert(friendSay);
|
|
|
|
// don't say things our teammates have just said
|
|
if (say->IsRedundant(friendSay))
|
|
{
|
|
// thie statement is redundant - destroy it
|
|
m_me->PrintIfWatched("Teammate said what I was going to say - shutting up.\n");
|
|
RemoveStatement(say);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns the statement that is being spoken, or is next to be spoken if no-one is speaking now
|
|
BotStatement *BotChatterInterface::GetActiveStatement()
|
|
{
|
|
// keep track of statement waiting longest to be spoken - it is next
|
|
BotStatement *earliest = nullptr;
|
|
float earlyTime = 999999999.9f;
|
|
|
|
for (int i = 1; i <= gpGlobals->maxClients; i++)
|
|
{
|
|
CBasePlayer *pPlayer = UTIL_PlayerByIndex(i);
|
|
|
|
if (!UTIL_IsValidPlayer(pPlayer))
|
|
continue;
|
|
|
|
if (FStrEq(STRING(pPlayer->pev->netname), ""))
|
|
continue;
|
|
|
|
// ignore dead humans
|
|
if (!pPlayer->IsBot() && !pPlayer->IsAlive())
|
|
continue;
|
|
|
|
// ignore enemies, since we can't hear them talk
|
|
if (m_me->BotRelationship(pPlayer) == CCSBot::BOT_ENEMY)
|
|
continue;
|
|
|
|
// if not a bot, fail the test
|
|
// TODO: Check if human is currently talking
|
|
if (!pPlayer->IsBot())
|
|
continue;
|
|
|
|
CCSBot *pBot = static_cast<CCSBot *>(pPlayer);
|
|
auto say = pBot->GetChatter()->m_statementList;
|
|
while (say)
|
|
{
|
|
// if this statement is currently being spoken, return it
|
|
if (say->IsSpeaking())
|
|
return say;
|
|
|
|
// keep track of statement that has been waiting longest to be spoken of anyone on our team
|
|
if (say->GetStartTime() < earlyTime)
|
|
{
|
|
earlyTime = say->GetTimestamp();
|
|
earliest = say;
|
|
}
|
|
|
|
say = say->m_next;
|
|
}
|
|
}
|
|
|
|
// make sure it is time to start this statement
|
|
if (earliest && earliest->GetStartTime() > gpGlobals->time)
|
|
return nullptr;
|
|
|
|
return earliest;
|
|
}
|
|
|
|
// Return true if we speaking makes sense now
|
|
bool BotChatterInterface::ShouldSpeak() const
|
|
{
|
|
// don't talk to non-existent friends
|
|
if (m_me->GetFriendsRemaining() == 0)
|
|
return false;
|
|
|
|
// if everyone is together, no need to tell them what's going on
|
|
if (m_me->GetNearbyFriendCount() == m_me->GetFriendsRemaining())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
float BotChatterInterface::GetRadioSilenceDuration()
|
|
{
|
|
#ifdef REGAMEDLL_FIXES
|
|
if (m_me->m_iTeam != CT && m_me->m_iTeam != TERRORIST)
|
|
return 0;
|
|
#endif
|
|
|
|
return m_radioSilenceInterval[m_me->m_iTeam - 1].GetElapsedTime();
|
|
}
|
|
|
|
void BotChatterInterface::ResetRadioSilenceDuration()
|
|
{
|
|
#ifdef REGAMEDLL_FIXES
|
|
if (m_me->m_iTeam != CT && m_me->m_iTeam != TERRORIST)
|
|
return;
|
|
#endif
|
|
|
|
m_radioSilenceInterval[m_me->m_iTeam - 1].Reset();
|
|
}
|
|
|
|
inline void SayWhere(BotStatement *say, Place place)
|
|
{
|
|
say->AppendPhrase(TheBotPhrases->GetPlace(place));
|
|
}
|
|
|
|
// Report enemy sightings
|
|
void BotChatterInterface::EnemySpotted()
|
|
{
|
|
// NOTE: This could be a few seconds out of date (enemy is in an adjacent place)
|
|
Place place = m_me->GetEnemyPlace();
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_VISIBLE_ENEMIES, 10.0f);
|
|
|
|
// where are the enemies
|
|
say->AppendPhrase(TheBotPhrases->GetPlace(place));
|
|
|
|
// how many are there
|
|
say->AppendPhrase(BotStatement::ACCUMULATE_ENEMIES_DELAY);
|
|
say->AppendPhrase(BotStatement::CURRENT_ENEMY_COUNT);
|
|
say->AddCondition(BotStatement::IS_IN_COMBAT);
|
|
|
|
AddStatement(say);
|
|
}
|
|
|
|
#ifdef REGAMEDLL_ADD
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* If a friend warned of snipers, don't warn again for awhile
|
|
*/
|
|
void BotChatterInterface::FriendSpottedSniper(void)
|
|
{
|
|
m_warnSniperTimer.Start(60.0f);
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Warn of an enemy sniper
|
|
*/
|
|
void BotChatterInterface::SpottedSniper(void)
|
|
{
|
|
if (!m_warnSniperTimer.IsElapsed())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_me->GetFriendsRemaining() == 0)
|
|
{
|
|
// no-one to warn
|
|
return;
|
|
}
|
|
|
|
BotStatement* say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("SniperWarning"));
|
|
say->AttachMeme(new BotWarnSniperMeme());
|
|
|
|
AddStatement(say);
|
|
}
|
|
#endif
|
|
|
|
NOXREF void BotChatterInterface::Clear(Place place)
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
SayWhere(say, place);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("Clear"));
|
|
AddStatement(say);
|
|
}
|
|
|
|
// Request enemy activity report
|
|
void BotChatterInterface::ReportIn()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_REQUEST_INFORMATION, 10.0f);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("RequestReport"));
|
|
say->AddCondition(BotStatement::RADIO_SILENCE);
|
|
say->AttachMeme(new BotRequestReportMeme());
|
|
AddStatement(say);
|
|
}
|
|
|
|
// Report our situtation
|
|
void BotChatterInterface::ReportingIn()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
// where are we
|
|
Place place = m_me->GetPlace();
|
|
SayWhere(say, place);
|
|
|
|
// what are we doing
|
|
switch (m_me->GetTask())
|
|
{
|
|
case CCSBot::PLANT_BOMB:
|
|
{
|
|
m_me->GetChatter()->GoingToPlantTheBomb(UNDEFINED_PLACE);
|
|
break;
|
|
}
|
|
case CCSBot::DEFUSE_BOMB:
|
|
{
|
|
m_me->GetChatter()->Say("DefusingBomb");
|
|
break;
|
|
}
|
|
case CCSBot::GUARD_LOOSE_BOMB:
|
|
{
|
|
if (TheCSBots()->GetLooseBomb())
|
|
{
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("GuardingLooseBomb"));
|
|
say->AttachMeme(new BotBombStatusMeme(CSGameState::LOOSE, TheCSBots()->GetLooseBomb()->pev->origin));
|
|
}
|
|
break;
|
|
}
|
|
case CCSBot::GUARD_HOSTAGES:
|
|
{
|
|
m_me->GetChatter()->GuardingHostages(UNDEFINED_PLACE, !m_me->IsAtHidingSpot());
|
|
break;
|
|
}
|
|
case CCSBot::GUARD_HOSTAGE_RESCUE_ZONE:
|
|
{
|
|
m_me->GetChatter()->GuardingHostageEscapeZone(!m_me->IsAtHidingSpot());
|
|
break;
|
|
}
|
|
case CCSBot::COLLECT_HOSTAGES:
|
|
{
|
|
break;
|
|
}
|
|
case CCSBot::RESCUE_HOSTAGES:
|
|
{
|
|
m_me->GetChatter()->EscortingHostages();
|
|
break;
|
|
}
|
|
case CCSBot::GUARD_VIP_ESCAPE_ZONE:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// what do we see
|
|
if (m_me->IsAttacking())
|
|
{
|
|
if (m_me->IsOutnumbered())
|
|
{
|
|
// in trouble in a firefight
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("Help"));
|
|
say->AttachMeme(new BotHelpMeme(place));
|
|
}
|
|
else
|
|
{
|
|
// battling enemies
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("InCombat"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// not in combat, start our report a little later
|
|
say->SetStartTime(gpGlobals->time + 2.0f);
|
|
|
|
const float recentTime = 10.0f;
|
|
if (m_me->GetEnemyDeathTimestamp() < recentTime && m_me->GetEnemyDeathTimestamp() >= m_me->GetTimeSinceLastSawEnemy() + 0.5f)
|
|
{
|
|
// recently saw an enemy die
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("EnemyDown"));
|
|
}
|
|
else if (m_me->GetTimeSinceLastSawEnemy() < recentTime)
|
|
{
|
|
// recently saw an enemy
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("EnemySpotted"));
|
|
}
|
|
else
|
|
{
|
|
// haven't seen enemies
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("Clear"));
|
|
}
|
|
}
|
|
|
|
AddStatement(say);
|
|
}
|
|
|
|
bool BotChatterInterface::NeedBackup()
|
|
{
|
|
const float minRequestInterval = 10.0f;
|
|
if (m_needBackupInterval.IsLessThen(minRequestInterval))
|
|
return false;
|
|
|
|
m_needBackupInterval.Reset();
|
|
|
|
if (m_me->GetFriendsRemaining() == 0)
|
|
{
|
|
// we're all alone...
|
|
Scared();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// ask friends for help
|
|
BotStatement *say = new BotStatement(this, REPORT_REQUEST_HELP, 10.0f);
|
|
|
|
// where are we
|
|
Place place = m_me->GetPlace();
|
|
SayWhere(say, place);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("Help"));
|
|
say->AttachMeme(new BotHelpMeme(place));
|
|
AddStatement(say);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void BotChatterInterface::PinnedDown()
|
|
{
|
|
// this is a form of "need backup"
|
|
const float minRequestInterval = 10.0f;
|
|
if (m_needBackupInterval.IsLessThen(minRequestInterval))
|
|
return;
|
|
|
|
m_needBackupInterval.Reset();
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_REQUEST_HELP, 10.0f);
|
|
|
|
// where are we
|
|
Place place = m_me->GetPlace();
|
|
SayWhere(say, place);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("PinnedDown"));
|
|
say->AttachMeme(new BotHelpMeme(place));
|
|
say->AddCondition(BotStatement::IS_IN_COMBAT);
|
|
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::HeardNoise(const Vector *pos)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
if (m_heardNoiseTimer.IsElapsed())
|
|
{
|
|
// throttle frequency
|
|
m_heardNoiseTimer.Start(20.0f);
|
|
|
|
// make rare, since many teammates may try to say this
|
|
if (RANDOM_FLOAT(0.0f, 100.0f) < 33.0f)
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 5.0f);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("HeardNoise"));
|
|
say->SetPlace(TheNavAreaGrid.GetPlace(pos));
|
|
|
|
AddStatement(say);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotChatterInterface::KilledMyEnemy(int victimID)
|
|
{
|
|
// only report if we killed the last enemy in the area
|
|
if (m_me->GetNearbyEnemyCount() <= 1)
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_ENEMY_ACTION, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("KilledMyEnemy"));
|
|
say->SetSubject(victimID);
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::EnemiesRemaining()
|
|
{
|
|
// only report if we killed the last enemy in the area
|
|
if (m_me->GetNearbyEnemyCount() > 1)
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_ENEMIES_REMAINING, 5.0f);
|
|
say->AppendPhrase(BotStatement::REMAINING_ENEMY_COUNT);
|
|
say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(2.0f, 4.0f));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::Affirmative()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_ACKNOWLEDGE, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("Affirmative"));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::Negative()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_ACKNOWLEDGE, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("Negative"));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::GoingToPlantTheBomb(Place place)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
const float minInterval = 10.0f; // 20.0f
|
|
if (m_planInterval.IsLessThen(minInterval))
|
|
return;
|
|
|
|
m_planInterval.Reset();
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_CRITICAL_EVENT, 10.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("GoingToPlantBomb"));
|
|
say->SetPlace(place);
|
|
say->AttachMeme(new BotFollowMeme());
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::PlantingTheBomb(Place place)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_CRITICAL_EVENT, 10.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("PlantingBomb"));
|
|
say->SetPlace(place);
|
|
say->AttachMeme(new BotDefendHereMeme(m_me->pev->origin));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::TheyPickedUpTheBomb()
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
// if we already know the bomb is not loose, this is old news
|
|
if (!m_me->GetGameState()->IsBombLoose())
|
|
return;
|
|
|
|
// update our gamestate - use our own position for now
|
|
m_me->GetGameState()->UpdateBomber(&m_me->pev->origin);
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("TheyPickedUpTheBomb"));
|
|
say->AttachMeme(new BotBombStatusMeme(CSGameState::MOVING, m_me->pev->origin));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::SpottedBomber(CBasePlayer *bomber)
|
|
{
|
|
if (m_me->GetGameState()->IsBombMoving())
|
|
{
|
|
// if we knew where the bomber was, this is old news
|
|
const Vector *bomberPos = m_me->GetGameState()->GetBombPosition();
|
|
const float closeRangeSq = 1000.0f * 1000.0f;
|
|
if (bomberPos && (bomber->pev->origin - *bomberPos).LengthSquared() < closeRangeSq)
|
|
return;
|
|
}
|
|
|
|
// update our gamestate
|
|
m_me->GetGameState()->UpdateBomber(&bomber->pev->origin);
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
// where is the bomber
|
|
Place place = TheNavAreaGrid.GetPlace(&bomber->pev->origin);
|
|
SayWhere(say, place);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("SpottedBomber"));
|
|
say->SetSubject(bomber->entindex());
|
|
|
|
//say->AttachMeme(new BotHelpMeme(place));
|
|
say->AttachMeme(new BotBombStatusMeme(CSGameState::MOVING, bomber->pev->origin));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::SpottedLooseBomb(CBaseEntity *bomb)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
// if we already know the bomb is loose, this is old news
|
|
if (m_me->GetGameState()->IsBombLoose())
|
|
return;
|
|
|
|
// update our gamestate
|
|
m_me->GetGameState()->UpdateLooseBomb(&bomb->pev->origin);
|
|
|
|
if (m_spottedLooseBombTimer.IsElapsed())
|
|
{
|
|
// throttle frequency
|
|
m_spottedLooseBombTimer.Start(10.0f);
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
// where is the bomb
|
|
Place place = TheNavAreaGrid.GetPlace(&bomb->pev->origin);
|
|
SayWhere(say, place);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("SpottedLooseBomb"));
|
|
|
|
if (TheCSBots()->GetLooseBomb())
|
|
say->AttachMeme(new BotBombStatusMeme(CSGameState::LOOSE, bomb->pev->origin));
|
|
|
|
AddStatement(say);
|
|
}
|
|
}
|
|
|
|
NOXREF void BotChatterInterface::GuardingLooseBomb(CBaseEntity *bomb)
|
|
{
|
|
if (TheCSBots()->IsRoundOver() || !bomb)
|
|
return;
|
|
|
|
#ifdef REGAMEDLL_FIXES
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen(minInterval))
|
|
return;
|
|
|
|
m_planInterval.Reset();
|
|
#endif
|
|
|
|
// update our gamestate
|
|
m_me->GetGameState()->UpdateLooseBomb(&bomb->pev->origin);
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
// where is the bomb
|
|
Place place = TheNavAreaGrid.GetPlace(&bomb->pev->origin);
|
|
SayWhere(say, place);
|
|
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("GuardingLooseBomb"));
|
|
|
|
if (TheCSBots()->GetLooseBomb())
|
|
say->AttachMeme(new BotBombStatusMeme(CSGameState::LOOSE, bomb->pev->origin));
|
|
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::RequestBombLocation()
|
|
{
|
|
// only ask once per round
|
|
if (m_requestedBombLocation)
|
|
return;
|
|
|
|
m_requestedBombLocation = true;
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement(this, REPORT_REQUEST_INFORMATION, 10.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("WhereIsTheBomb"));
|
|
say->AttachMeme(new BotWhereBombMeme());
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::BombsiteClear(int zoneIndex)
|
|
{
|
|
const CCSBotManager::Zone *zone = TheCSBots()->GetZone(zoneIndex);
|
|
if (!zone)
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 10.0f);
|
|
|
|
SayWhere(say, TheNavAreaGrid.GetPlace(&zone->m_center));
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("BombsiteClear"));
|
|
say->AttachMeme(new BotBombsiteStatusMeme(zoneIndex, BotBombsiteStatusMeme::CLEAR));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::FoundPlantedBomb(int zoneIndex)
|
|
{
|
|
const CCSBotManager::Zone *zone = TheCSBots()->GetZone(zoneIndex);
|
|
if (!zone)
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("PlantedBombPlace"));
|
|
say->SetPlace(TheNavAreaGrid.GetPlace(&zone->m_center));
|
|
say->AttachMeme(new BotBombsiteStatusMeme(zoneIndex, BotBombsiteStatusMeme::PLANTED));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::Scared()
|
|
{
|
|
const float minInterval = 10.0f;
|
|
if (m_scaredInterval.IsLessThen(minInterval))
|
|
return;
|
|
|
|
m_scaredInterval.Reset();
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_EMOTE, 1.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("ScaredEmote"));
|
|
say->AddCondition(BotStatement::IS_IN_COMBAT);
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::CelebrateWin()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_EMOTE, 15.0f);
|
|
|
|
// wait a bit before speaking
|
|
say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(2.0f, 5.0f));
|
|
|
|
const float quickRound = 45.0f;
|
|
|
|
if (m_me->GetFriendsRemaining() == 0)
|
|
{
|
|
// we were the last man standing
|
|
if (TheCSBots()->GetElapsedRoundTime() < quickRound)
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("WonRoundQuickly"));
|
|
else if (RANDOM_FLOAT(0.0f, 100.0f) < 33.3f)
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("LastManStanding"));
|
|
}
|
|
else
|
|
{
|
|
if (TheCSBots()->GetElapsedRoundTime() < quickRound)
|
|
{
|
|
if (RANDOM_FLOAT(0.0f, 100.0f) < 33.3f)
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("WonRoundQuickly"));
|
|
}
|
|
else if (RANDOM_FLOAT(0.0f, 100.0f) < 10.0f)
|
|
{
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("WonRound"));
|
|
}
|
|
}
|
|
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::AnnouncePlan(const char *phraseName, Place place)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_MY_PLAN, 10.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase(phraseName));
|
|
say->SetPlace(place);
|
|
|
|
// wait at least a short time after round start
|
|
say->SetStartTime(TheCSBots()->GetRoundStartTime() + RANDOM_FLOAT(2.0, 3.0f));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::GuardingHostages(Place place, bool isPlan)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen(minInterval))
|
|
return;
|
|
|
|
#ifdef REGAMEDLL_FIXES
|
|
m_planInterval.Reset();
|
|
#endif
|
|
|
|
if (isPlan)
|
|
AnnouncePlan("GoingToGuardHostages", place);
|
|
else
|
|
Say("GuardingHostages");
|
|
}
|
|
|
|
void BotChatterInterface::GuardingHostageEscapeZone(bool isPlan)
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen(minInterval))
|
|
return;
|
|
|
|
#ifdef REGAMEDLL_FIXES
|
|
m_planInterval.Reset();
|
|
#endif
|
|
|
|
if (isPlan)
|
|
AnnouncePlan("GoingToGuardHostageEscapeZone", UNDEFINED_PLACE);
|
|
else
|
|
Say("GuardingHostageEscapeZone");
|
|
}
|
|
|
|
void BotChatterInterface::HostagesBeingTaken()
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("HostagesBeingTaken"));
|
|
say->AttachMeme(new BotHostageBeingTakenMeme());
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::HostagesTaken()
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("HostagesTaken"));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::TalkingToHostages()
|
|
{
|
|
;
|
|
}
|
|
|
|
void BotChatterInterface::EscortingHostages()
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
if (m_escortingHostageTimer.IsElapsed())
|
|
{
|
|
// throttle frequency
|
|
m_escortingHostageTimer.Start(10.0f);
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_MY_PLAN, 5.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("EscortingHostages"));
|
|
AddStatement(say);
|
|
}
|
|
}
|
|
|
|
NOXREF void BotChatterInterface::HostageDown()
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement(this, REPORT_INFORMATION, 3.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("HostageDown"));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::Encourage(const char *phraseName, float repeatInterval, float lifetime)
|
|
{
|
|
if (m_encourageTimer.IsElapsed())
|
|
{
|
|
Say(phraseName, lifetime);
|
|
m_encourageTimer.Start(repeatInterval);
|
|
}
|
|
}
|
|
|
|
void BotChatterInterface::KilledFriend()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_KILLED_FRIEND, 2.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("KilledFriend"));
|
|
|
|
// give them time to react
|
|
say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(0.5f, 1.0f));
|
|
AddStatement(say);
|
|
}
|
|
|
|
void BotChatterInterface::FriendlyFire()
|
|
{
|
|
BotStatement *say = new BotStatement(this, REPORT_FRIENDLY_FIRE, 1.0f);
|
|
say->AppendPhrase(TheBotPhrases->GetPhrase("FriendlyFire"));
|
|
|
|
// give them time to react
|
|
say->SetStartTime(gpGlobals->time + RANDOM_FLOAT(0.3f, 0.5f));
|
|
AddStatement(say);
|
|
}
|