You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2679 lines
68 KiB
2679 lines
68 KiB
//========= Copyright 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
// $NoKeywords: $
|
|
//=============================================================================//
|
|
|
|
// Author: Michael S. Booth ([email protected]), 2003
|
|
|
|
#include "cbase.h"
|
|
#include "cs_gamerules.h"
|
|
#include "cs_player.h"
|
|
#include "shared_util.h"
|
|
#include "engine/IEngineSound.h"
|
|
#include "keyvalues.h"
|
|
|
|
#include "bot.h"
|
|
#include "bot_util.h"
|
|
#include "cs_bot.h"
|
|
#include "cs_bot_chatter.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
// Use to toggle between old bot chatter system and response rules
|
|
extern ConVar bot_chatter_use_rr;
|
|
|
|
/**
|
|
* @todo Fix this
|
|
*/
|
|
|
|
bool GetRandomSpotAtPlace( Place place, Vector *pPos )
|
|
{
|
|
int count = 0;
|
|
|
|
FOR_EACH_VEC( TheNavAreas, it )
|
|
{
|
|
CNavArea *area = TheNavAreas[ it ];
|
|
|
|
if (area->GetPlace() == place)
|
|
++count;
|
|
}
|
|
|
|
if (count == 0)
|
|
return false;
|
|
|
|
int which = RandomInt( 0, count-1 );
|
|
|
|
FOR_EACH_VEC( TheNavAreas, rit )
|
|
{
|
|
CNavArea *area = TheNavAreas[ rit ];
|
|
|
|
if (area->GetPlace() == place && which == 0)
|
|
{
|
|
*pPos = area->GetCenter();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Transmit meme to other bots
|
|
*/
|
|
void BotMeme::Transmit( CCSBot *sender ) const
|
|
{
|
|
for( int i = 1; i <= gpGlobals->maxClients; i++ )
|
|
{
|
|
CCSPlayer *player = static_cast<CCSPlayer *>( UTIL_PlayerByIndex( i ) );
|
|
|
|
if (player == NULL)
|
|
continue;
|
|
|
|
// if (FNullEnt( player->pev ))
|
|
// continue;
|
|
|
|
// if (FStrEq( STRING( player->pev->netname ), "" ))
|
|
// continue;
|
|
|
|
// skip self
|
|
if (sender == player)
|
|
continue;
|
|
|
|
// ignore dead humans
|
|
if (!player->IsBot() && !player->IsAlive())
|
|
continue;
|
|
|
|
// ignore enemies, since we can't hear them talk
|
|
if (sender && sender->IsOtherEnemy( player ))
|
|
continue;
|
|
|
|
// if not a bot, fail the test
|
|
if (!player->IsBot())
|
|
continue;
|
|
|
|
CCSBot *bot = dynamic_cast<CCSBot *>( player );
|
|
|
|
if ( !bot )
|
|
continue;
|
|
|
|
// allow bot to interpret our meme
|
|
Interpret( sender, bot );
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate called for help - respond
|
|
*/
|
|
void BotHelpMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
const float maxHelpRange = 3000.0f; // 2000
|
|
receiver->RespondToHelpRequest( sender, m_place, maxHelpRange );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate reported information about a bombsite
|
|
*/
|
|
void BotBombsiteStatusMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
// remember this bombsite's status
|
|
if (m_status == CLEAR)
|
|
receiver->GetGameState()->ClearBombsite( m_zoneIndex );
|
|
else
|
|
receiver->GetGameState()->MarkBombsiteAsPlanted( m_zoneIndex );
|
|
|
|
// if we were heading to the just-cleared bombsite, pick another one to search
|
|
// if our target bombsite wasn't cleared, will will continue going to it,
|
|
// because GetNextBombsiteToSearch() will return the same zone (since its not cleared)
|
|
// if the bomb was planted, we will head to that bombsite
|
|
if (receiver->GetTask() == CCSBot::FIND_TICKING_BOMB)
|
|
{
|
|
receiver->Idle();
|
|
receiver->GetChatter()->Affirmative();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate reported information about the bomb
|
|
*/
|
|
void BotBombStatusMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
// update our gamestate based on teammate's report
|
|
switch( m_state )
|
|
{
|
|
case CSGameState::MOVING:
|
|
receiver->GetGameState()->UpdateBomber( m_pos );
|
|
|
|
// if we are hunting and see no enemies, respond
|
|
if (!receiver->IsRogue() && receiver->IsHunting() && receiver->GetNearbyEnemyCount() == 0)
|
|
receiver->RespondToHelpRequest( sender, TheNavMesh->GetPlace( m_pos ) );
|
|
|
|
break;
|
|
|
|
case CSGameState::LOOSE:
|
|
receiver->GetGameState()->UpdateLooseBomb( m_pos );
|
|
|
|
if (receiver->GetTask() == CCSBot::GUARD_BOMB_ZONE)
|
|
{
|
|
receiver->Idle();
|
|
receiver->GetChatter()->Affirmative();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate has asked that we follow him
|
|
*/
|
|
void BotFollowMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
if (receiver->IsRogue())
|
|
return;
|
|
|
|
// if we're busy, ignore
|
|
if (receiver->IsBusy())
|
|
return;
|
|
|
|
// if we are too far away, ignore
|
|
// compute actual travel distance
|
|
Vector senderOrigin = GetCentroid( sender );
|
|
PathCost cost( receiver );
|
|
float travelDistance = NavAreaTravelDistance( receiver->GetLastKnownArea(),
|
|
TheNavMesh->GetNearestNavArea( senderOrigin ),
|
|
cost );
|
|
if (travelDistance < 0.0f)
|
|
return;
|
|
|
|
const float tooFar = 1000.0f;
|
|
if (travelDistance > tooFar)
|
|
return;
|
|
|
|
// begin following
|
|
receiver->Follow( sender );
|
|
|
|
// acknowledge
|
|
receiver->GetChatter()->Say( "CoveringFriend" );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate has asked us to defend a place
|
|
*/
|
|
void BotDefendHereMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
if (receiver->IsRogue())
|
|
return;
|
|
|
|
// if we're busy, ignore
|
|
if (receiver->IsBusy())
|
|
return;
|
|
|
|
Place place = TheNavMesh->GetPlace( m_pos );
|
|
if (place != UNDEFINED_PLACE)
|
|
{
|
|
// pick a random hiding spot in this place
|
|
const Vector *spot = FindRandomHidingSpot( receiver, place, receiver->IsSniper() );
|
|
if (spot)
|
|
{
|
|
receiver->SetTask( CCSBot::HOLD_POSITION );
|
|
receiver->Hide( *spot );
|
|
return;
|
|
}
|
|
}
|
|
|
|
// hide nearby
|
|
receiver->SetTask( CCSBot::HOLD_POSITION );
|
|
receiver->Hide( TheNavMesh->GetNearestNavArea( m_pos ) );
|
|
|
|
// acknowledge
|
|
receiver->GetChatter()->Say( "Affirmative" );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate has asked where the bomb is planted
|
|
*/
|
|
void BotWhereBombMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
int zone = receiver->GetGameState()->GetPlantedBombsite();
|
|
|
|
if (zone != CSGameState::UNKNOWN)
|
|
receiver->GetChatter()->FoundPlantedBomb( zone );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate has asked us to report in
|
|
*/
|
|
void BotRequestReportMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
receiver->GetChatter()->ReportingIn();
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate told us all the hostages are gone
|
|
*/
|
|
void BotAllHostagesGoneMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
receiver->GetGameState()->AllHostagesGone();
|
|
|
|
// acknowledge
|
|
receiver->GetChatter()->Say( "Affirmative" );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate told us a CT is talking to a hostage
|
|
*/
|
|
void BotHostageBeingTakenMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
receiver->GetGameState()->HostageWasTaken();
|
|
|
|
// if we're busy, ignore
|
|
if (receiver->IsBusy())
|
|
return;
|
|
|
|
receiver->Idle();
|
|
|
|
// acknowledge
|
|
receiver->GetChatter()->Say( "Affirmative" );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate heard a noise, so we shouldn't report noises for a while
|
|
*/
|
|
void BotHeardNoiseMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
receiver->GetChatter()->FriendHeardNoise();
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* A teammate warned about snipers, so we shouldn't warn again for awhile
|
|
*/
|
|
void BotWarnSniperMeme::Interpret( CCSBot *sender, CCSBot *receiver ) const
|
|
{
|
|
receiver->GetChatter()->FriendSpottedSniper();
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
BotSpeakable::BotSpeakable()
|
|
{
|
|
m_phrase = NULL;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
BotSpeakable::~BotSpeakable()
|
|
{
|
|
if ( m_phrase )
|
|
{
|
|
delete[] m_phrase;
|
|
m_phrase = NULL;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
|
|
BotPhrase::BotPhrase( bool isPlace )
|
|
{
|
|
m_name = NULL;
|
|
m_place = UNDEFINED_PLACE;
|
|
m_isPlace = isPlace;
|
|
m_radioEvent = RADIO_INVALID;
|
|
m_isImportant = false;
|
|
ClearCriteria();
|
|
m_numVoiceBanks = 0;
|
|
InitVoiceBank( 0 );
|
|
}
|
|
|
|
BotPhrase::~BotPhrase()
|
|
{
|
|
for( int bank=0; bank<m_voiceBank.Count(); ++bank )
|
|
{
|
|
for( int speakable=0; speakable<m_voiceBank[bank]->Count(); ++speakable )
|
|
{
|
|
delete (*m_voiceBank[bank])[speakable];
|
|
}
|
|
delete m_voiceBank[bank];
|
|
}
|
|
|
|
if ( m_name )
|
|
delete [] m_name;
|
|
}
|
|
|
|
void BotPhrase::InitVoiceBank( int bankIndex )
|
|
{
|
|
while ( m_numVoiceBanks <= bankIndex )
|
|
{
|
|
m_count.AddToTail(0);
|
|
m_index.AddToTail(0);
|
|
m_voiceBank.AddToTail( new BotSpeakableVector );
|
|
++m_numVoiceBanks;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a random speakable - avoid repeating
|
|
*/
|
|
char *BotPhrase::GetSpeakable( int bankIndex, float *duration ) const
|
|
{
|
|
if (bankIndex < 0 || bankIndex >= m_numVoiceBanks || m_count[bankIndex] == 0)
|
|
{
|
|
if (duration)
|
|
*duration = 0.0f;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// find phrase that meets the current criteria
|
|
int start = m_index[bankIndex];
|
|
while(true)
|
|
{
|
|
BotSpeakableVector *speakables = m_voiceBank[bankIndex];
|
|
int& index = m_index[bankIndex];
|
|
const BotSpeakable *speak = (*speakables)[index++];
|
|
|
|
if (m_index[bankIndex] >= m_count[bankIndex])
|
|
m_index[bankIndex] = 0;
|
|
|
|
// check place criteria
|
|
// if this speakable has a place criteria, it must match to be used
|
|
// speakables with Place of ANY will match any place
|
|
// speakables with a specific Place will only be used if Place matches
|
|
// speakables with Place of UNDEFINED only match Place of UNDEFINED
|
|
if (speak->m_place == ANY_PLACE || speak->m_place == m_placeCriteria)
|
|
{
|
|
// check count criteria
|
|
// if this speakable has a count criteria, it must match to be used
|
|
// if this speakable does not have a count criteria, we dont care what the count is set to
|
|
if (speak->m_count == UNDEFINED_COUNT || speak->m_count == MIN( m_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 NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Randomly shuffle the speakable order
|
|
*/
|
|
void BotPhrase::Randomize( void )
|
|
{
|
|
for ( int bank = 0; bank < m_voiceBank.Count(); ++bank )
|
|
{
|
|
BotSpeakableVector *speakables = m_voiceBank[bank];
|
|
if ( speakables->Count() == 1 )
|
|
continue;
|
|
|
|
// A simple shuffle: for each array index, swap it with a random index
|
|
for ( int index = 0; index < speakables->Count(); ++index )
|
|
{
|
|
int newIndex = RandomInt( 0, speakables->Count()-1 );
|
|
|
|
BotSpeakable *speakable = (*speakables)[index];
|
|
(*speakables)[index] = (*speakables)[newIndex];
|
|
(*speakables)[newIndex] = speakable;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
|
|
BotPhraseManager *TheBotPhrases = NULL;
|
|
|
|
BotPhraseManager::BotPhraseManager( void )
|
|
{
|
|
m_placeCount = 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Invoked when map changes
|
|
*/
|
|
void BotPhraseManager::OnMapChange( void )
|
|
{
|
|
m_placeCount = 0;
|
|
}
|
|
|
|
/**
|
|
* Removes everything from memory
|
|
*/
|
|
void BotPhraseManager::Reset( void )
|
|
{
|
|
int i;
|
|
|
|
// free phrase resources
|
|
for( i=0; i<m_list.Count(); ++i )
|
|
{
|
|
delete m_list[i];
|
|
}
|
|
|
|
for( i=0; i<m_placeList.Count(); ++i )
|
|
{
|
|
delete m_placeList[i];
|
|
}
|
|
|
|
m_list.RemoveAll();
|
|
m_placeList.RemoveAll();
|
|
|
|
m_painPhrase = NULL;
|
|
m_agreeWithPlanPhrase = NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Invoked when the round resets
|
|
*/
|
|
void BotPhraseManager::OnRoundRestart( void )
|
|
{
|
|
// effectively reset all interval timers
|
|
m_placeCount = 0;
|
|
|
|
// shuffle all the speakables
|
|
int i;
|
|
for( i=0; i<m_placeList.Count(); ++i )
|
|
m_placeList[i]->Randomize();
|
|
|
|
for( i=0; i<m_list.Count(); ++i )
|
|
m_list[i]->Randomize();
|
|
}
|
|
|
|
BotChatterOutputType BotPhraseManager::GetOutputType( int voiceBank ) const
|
|
{
|
|
if ( voiceBank >= 0 && voiceBank < m_output.Count() )
|
|
{
|
|
return m_output[voiceBank];
|
|
}
|
|
return BOT_CHATTER_RADIO;
|
|
}
|
|
|
|
/**
|
|
* Initialize phrase system from database file
|
|
*/
|
|
bool BotPhraseManager::Initialize( const char *filename, int bankIndex )
|
|
{
|
|
bool isDefault = (bankIndex == 0);
|
|
|
|
FileHandle_t file = filesystem->Open( filename, "r" );
|
|
if (!file)
|
|
{
|
|
CONSOLE_ECHO( "WARNING: Cannot access bot phrase database '%s'\n", filename );
|
|
return false;
|
|
}
|
|
|
|
// BOTPORT: Redo file reading to avoid loading whole file into memory at once
|
|
int phraseDataLength = filesystem->Size( filename );
|
|
char *phraseDataFile = new char[ phraseDataLength ];
|
|
|
|
int dataReadLength = filesystem->Read( phraseDataFile, phraseDataLength, file );
|
|
|
|
filesystem->Close( file );
|
|
|
|
if ( dataReadLength > 0 )
|
|
{
|
|
// NULL-terminate based on the length read in, since Read() can transform \r\n to \n and
|
|
// return fewer bytes than we were expecting.
|
|
phraseDataFile[ dataReadLength - 1 ] = 0;
|
|
}
|
|
|
|
const char *phraseData = phraseDataFile;
|
|
|
|
|
|
const int RadioPathLen = 128; // wav filenames need to be shorter than this to go over the net anyway.
|
|
char baseDir[RadioPathLen] = "";
|
|
char compositeFilename[RadioPathLen];
|
|
|
|
//
|
|
// Parse the BotChatter.db into BotPhrase collections
|
|
//
|
|
while( true )
|
|
{
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
break;
|
|
|
|
char *token = SharedGetToken();
|
|
|
|
if ( !stricmp( token, "Output" ) )
|
|
{
|
|
// get name of this output device
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
|
|
while ( m_output.Count() <= bankIndex )
|
|
{
|
|
m_output.AddToTail(BOT_CHATTER_RADIO);
|
|
}
|
|
|
|
char *token = SharedGetToken();
|
|
if ( !stricmp( token, "Voice" ) )
|
|
{
|
|
m_output[bankIndex] = BOT_CHATTER_VOICE;
|
|
}
|
|
}
|
|
else if ( !stricmp( token, "BaseDir" ) )
|
|
{
|
|
// get name of this output device
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
char *token = SharedGetToken();
|
|
Q_strncpy( baseDir, token, RadioPathLen );
|
|
Q_strncat( baseDir, "\\", RadioPathLen, -1 );
|
|
baseDir[RadioPathLen-1] = 0;
|
|
}
|
|
else if (!stricmp( token, "Place" ) || !stricmp( token, "Chatter" ))
|
|
{
|
|
bool isPlace = (stricmp( token, "Place" )) ? false : true;
|
|
|
|
// encountered a new phrase collection
|
|
BotPhrase *phrase = NULL;
|
|
if ( isDefault )
|
|
{
|
|
phrase = new BotPhrase( isPlace );
|
|
}
|
|
|
|
// get name of this phrase
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing '%s' - expected identifier\n", filename );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
if ( isDefault )
|
|
{
|
|
phrase->m_name = CloneString( SharedGetToken() );
|
|
|
|
phrase->m_place = (isPlace) ? TheNavMesh->NameToPlace( phrase->m_name ) : UNDEFINED_PLACE;
|
|
}
|
|
else // look up the existing phrase
|
|
{
|
|
if ( isPlace )
|
|
{
|
|
phrase = const_cast<BotPhrase *>(GetPlace( SharedGetToken() ));
|
|
}
|
|
else
|
|
{
|
|
phrase = const_cast<BotPhrase *>(GetPhrase( SharedGetToken() ));
|
|
}
|
|
|
|
if ( !phrase )
|
|
{
|
|
CONSOLE_ECHO( "Error parsing '%s' - phrase '%s' is invalid\n", filename, SharedGetToken() );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
}
|
|
phrase->InitVoiceBank( bankIndex );
|
|
|
|
PlaceCriteria placeCriteria = ANY_PLACE;
|
|
CountCriteria countCriteria = UNDEFINED_COUNT;
|
|
RadioType radioEvent = RADIO_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 );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
token = SharedGetToken();
|
|
|
|
// check for Place criteria
|
|
if (!stricmp( token, "Place" ))
|
|
{
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected Place name\n", filename );
|
|
delete [] 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 (!stricmp( "ANY", token ))
|
|
placeCriteria = ANY_PLACE;
|
|
else if (!stricmp( "UNDEFINED", token ))
|
|
placeCriteria = UNDEFINED_PLACE;
|
|
else
|
|
placeCriteria = TheNavMesh->NameToPlace( token );
|
|
|
|
continue;
|
|
}
|
|
|
|
// check for Count criteria
|
|
if (!stricmp( token, "Count" ))
|
|
{
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected Count value\n", filename );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
token = SharedGetToken();
|
|
|
|
// update count criteria for subsequent speak lines
|
|
if (!stricmp( token, "Many" ))
|
|
countCriteria = COUNT_MANY;
|
|
else
|
|
countCriteria = atoi( token );
|
|
|
|
continue;
|
|
}
|
|
|
|
// check for radio equivalent
|
|
if (!stricmp( token, "Radio" ))
|
|
{
|
|
phraseData = SharedParse( phraseData );
|
|
if (!phraseData)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected radio event\n", filename );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
token = SharedGetToken();
|
|
|
|
RadioType event = NameToRadioEvent( token );
|
|
if (event <= RADIO_START_1 || event >= RADIO_END)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - invalid radio event '%s'\n", filename, token );
|
|
delete [] phraseDataFile;
|
|
return false;
|
|
}
|
|
|
|
radioEvent = event;
|
|
|
|
continue;
|
|
}
|
|
|
|
// check for "important" flag
|
|
if (!stricmp( token, "Important" ))
|
|
{
|
|
isImportant = true;
|
|
continue;
|
|
}
|
|
|
|
// check for End delimiter
|
|
if (!stricmp( token, "End" ))
|
|
break;
|
|
|
|
// found a phrase - add it to the collection
|
|
BotSpeakable *speak = new BotSpeakable;
|
|
if ( baseDir[0] )
|
|
{
|
|
Q_snprintf( compositeFilename, RadioPathLen, "%s%s", baseDir, token );
|
|
speak->m_phrase = CloneString( compositeFilename );
|
|
}
|
|
else
|
|
{
|
|
speak->m_phrase = CloneString( token );
|
|
}
|
|
speak->m_place = placeCriteria;
|
|
speak->m_count = countCriteria;
|
|
#ifdef POSIX
|
|
Q_FixSlashes( speak->m_phrase );
|
|
Q_strlower( speak->m_phrase );
|
|
#endif
|
|
|
|
#if defined( CSTRIKE15 )
|
|
// cstrike15 doesn't use bot chatter this way, the system redirects to RR, so no need to do anything other than slam this
|
|
speak->m_duration = 1.0f;
|
|
#else
|
|
speak->m_duration = enginesound->GetSoundDuration( speak->m_phrase );
|
|
if (speak->m_duration <= 0.0f)
|
|
{
|
|
if ( !engine->IsDedicatedServer() )
|
|
{
|
|
DevMsg( "Warning: Couldn't get duration of phrase '%s'\n", speak->m_phrase );
|
|
}
|
|
speak->m_duration = 1.0f;
|
|
}
|
|
#endif
|
|
BotSpeakableVector * speakables = phrase->m_voiceBank[ bankIndex ];
|
|
speakables->AddToTail( speak );
|
|
|
|
++phrase->m_count[ bankIndex ];
|
|
}
|
|
|
|
if ( isDefault )
|
|
{
|
|
phrase->m_radioEvent = radioEvent;
|
|
phrase->m_isImportant = isImportant;
|
|
}
|
|
|
|
// add phrase collection to the appropriate master list
|
|
if (isPlace)
|
|
m_placeList.AddToTail( phrase );
|
|
else
|
|
m_list.AddToTail( phrase );
|
|
}
|
|
}
|
|
|
|
delete [] phraseDataFile;
|
|
|
|
m_painPhrase = GetPhrase( "Pain" );
|
|
m_agreeWithPlanPhrase = GetPhrase( "AgreeWithPlan" );
|
|
|
|
return true;
|
|
}
|
|
|
|
BotPhraseManager::~BotPhraseManager()
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
/**
|
|
* Given a name, return the associated phrase collection
|
|
*/
|
|
const BotPhrase *BotPhraseManager::GetPhrase( const char *name ) const
|
|
{
|
|
for( int i=0; i<m_list.Count(); ++i )
|
|
{
|
|
if (!stricmp( m_list[i]->m_name, name ))
|
|
return m_list[i];
|
|
}
|
|
|
|
//CONSOLE_ECHO( "GetPhrase: ERROR - Invalid phrase '%s'\n", name );
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Given an id, return the associated phrase collection
|
|
* @todo Store phrases in a vector to make this fast
|
|
*/
|
|
/*
|
|
const BotPhrase *BotPhraseManager::GetPhrase( unsigned int place ) const
|
|
{
|
|
for( BotPhraseList::const_iterator iter = m_list.begin(); iter != m_list.end(); ++iter )
|
|
{
|
|
const BotPhrase *phrase = *iter;
|
|
if (phrase->m_place == id)
|
|
return phrase;
|
|
}
|
|
|
|
CONSOLE_ECHO( "GetPhrase: ERROR - Invalid phrase id #%d\n", id );
|
|
return NULL;
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Given a name, return the associated Place phrase collection
|
|
*/
|
|
const BotPhrase *BotPhraseManager::GetPlace( const char *name ) const
|
|
{
|
|
if (name == NULL)
|
|
return NULL;
|
|
|
|
for( int i=0; i<m_placeList.Count(); ++i )
|
|
{
|
|
if (!stricmp( m_placeList[i]->m_name, name ))
|
|
return m_placeList[i];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Given a name, return the associated Place phrase collection
|
|
*/
|
|
const BotPhrase *BotPhraseManager::GetPlace( PlaceCriteria place ) const
|
|
{
|
|
if (place == UNDEFINED_PLACE)
|
|
return NULL;
|
|
|
|
for( int i=0; i<m_placeList.Count(); ++i )
|
|
{
|
|
if (m_placeList[i]->m_place == place)
|
|
return m_placeList[i];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
|
|
BotStatement::BotStatement( BotChatterInterface *chatter, BotStatementType type, float expireDuration )
|
|
{
|
|
m_chatter = chatter;
|
|
|
|
m_next = NULL;
|
|
m_prev = NULL;
|
|
m_timestamp = gpGlobals->curtime;
|
|
m_speakTimestamp = 0.0f;
|
|
|
|
m_type = type;
|
|
m_subject = UNDEFINED_SUBJECT;
|
|
m_place = UNDEFINED_PLACE;
|
|
m_meme = NULL;
|
|
|
|
m_startTime = gpGlobals->curtime;
|
|
m_expireTime = gpGlobals->curtime + 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;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
CCSBot *BotStatement::GetOwner( void ) 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( void ) 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( void ) const
|
|
{
|
|
for( int i=0; i<m_conditionCount; ++i )
|
|
{
|
|
switch( m_condition[i] )
|
|
{
|
|
case IS_IN_COMBAT:
|
|
{
|
|
// Don't speak in combat, unless we're playing a coop mission
|
|
if ( !GetOwner()->IsAttacking() && !CSGameRules()->IsPlayingCoopMission() )
|
|
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( void ) 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 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;
|
|
}
|
|
|
|
// check if statement lifetime has expired
|
|
return (gpGlobals->curtime > 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)
|
|
{
|
|
const BotPhrase *meToo = TheBotPhrases->GetAgreeWithPlanPhrase();
|
|
|
|
// 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->curtime + RandomFloat( 0.5f, 1.0f );
|
|
}
|
|
else
|
|
{
|
|
// same plan at different place - wait a bit to allow others to respond "me too"
|
|
m_startTime = gpGlobals->curtime + RandomFloat( 3.0f, 4.0f );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotStatement::AppendPhrase( const BotPhrase *phrase )
|
|
{
|
|
if (phrase == NULL)
|
|
return;
|
|
|
|
if (m_count < MAX_BOT_PHRASES)
|
|
{
|
|
m_statement[ m_count ].isPhrase = true;
|
|
m_statement[ m_count++ ].phrase = phrase;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Special phrases that depend on the context
|
|
*/
|
|
void BotStatement::AppendPhrase( ContextType contextPhrase )
|
|
{
|
|
if (m_count < MAX_BOT_PHRASES)
|
|
{
|
|
m_statement[ m_count ].isPhrase = false;
|
|
m_statement[ m_count++ ].context = contextPhrase;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Say our statement
|
|
* m_index refers to the phrase currently being spoken, or -1 if we havent started yet
|
|
*/
|
|
bool BotStatement::Update( void )
|
|
{
|
|
CCSBot *me = GetOwner();
|
|
|
|
// if all of our teammates are dead, the only non-redundant statements are emotes
|
|
if (me->GetFriendsRemaining() == 0 && GetType() != REPORT_EMOTE && !me->HasHeavyArmor())
|
|
return false;
|
|
|
|
if (!m_isSpeaking)
|
|
{
|
|
m_isSpeaking = true;
|
|
m_speakTimestamp = gpGlobals->curtime;
|
|
}
|
|
|
|
const int nMinEnemiesToNote = CSGameRules()->IsPlayingCoopMission() ? 1 : 3;
|
|
|
|
// 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; // 1
|
|
if ( me->GetNearbyEnemyCount() > nMinEnemiesToNote || gpGlobals->curtime - m_speakTimestamp > reportTime )
|
|
{
|
|
// enough enemies have accumulated to expire this delay
|
|
m_nextTime = 0.0f;
|
|
}
|
|
}
|
|
|
|
|
|
if (gpGlobals->curtime > 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 = NULL;
|
|
|
|
if (m_statement[ m_index ].isPhrase)
|
|
{
|
|
// normal phrase
|
|
phrase = m_statement[ m_index ].phrase;
|
|
}
|
|
else
|
|
{
|
|
// context-dependant phrase
|
|
switch( m_statement[ m_index ].context )
|
|
{
|
|
case CURRENT_ENEMY_COUNT:
|
|
{
|
|
if ( me->HasHeavyArmor() )
|
|
break;
|
|
|
|
int enemyCount = me->GetNearbyEnemyCount();
|
|
const int nEnemySpottedThreshold = CSGameRules()->IsPlayingCoopMission() ? 0 : 1;
|
|
|
|
// if we are outnumbered, ask for help
|
|
if (enemyCount-1 > me->GetNearbyFriendCount())
|
|
{
|
|
phrase = TheBotPhrases->GetPhrase( "Help" );
|
|
AttachMeme( new BotHelpMeme() );
|
|
}
|
|
else if ( enemyCount > nEnemySpottedThreshold )
|
|
{
|
|
phrase = TheBotPhrases->GetPhrase( "EnemySpotted" );
|
|
phrase->SetCountCriteria( enemyCount );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case REMAINING_ENEMY_COUNT:
|
|
{
|
|
if ( me->HasHeavyArmor() )
|
|
break;
|
|
|
|
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 > nMinEnemiesToNote )
|
|
{
|
|
phrase = NULL;
|
|
}
|
|
else
|
|
{
|
|
phrase = TheBotPhrases->GetPhrase( speak[ enemyCount ] );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SHORT_DELAY:
|
|
{
|
|
m_nextTime = gpGlobals->curtime + RandomFloat( 0.1f, 0.5f );
|
|
return true;
|
|
}
|
|
|
|
case LONG_DELAY:
|
|
{
|
|
m_nextTime = gpGlobals->curtime + RandomFloat( 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)
|
|
{
|
|
RadioType radioEvent = phrase->GetRadioEquivalent();
|
|
if (radioEvent == RADIO_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 );
|
|
|
|
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->GetPlace() );
|
|
const float minRepeatTime = 20.0f; // 30
|
|
if (timeSince < minRepeatTime)
|
|
{
|
|
sayIt = false;
|
|
}
|
|
else
|
|
{
|
|
TheBotPhrases->ResetPlaceStatementInterval( phrase->GetPlace() );
|
|
}
|
|
}
|
|
|
|
if (sayIt)
|
|
{
|
|
bool bSpoke = false;
|
|
if ( bot_chatter_use_rr.GetBool() )
|
|
{
|
|
// Pipe the bot chatter through the response rules system
|
|
int nPlace = phrase->IsPlace() ? phrase->GetPlace() : phrase->GetPlaceCriteria();
|
|
const char *place = TheNavMesh->PlaceToName( nPlace );
|
|
CountCriteria count = phrase->GetCountCriteria();
|
|
|
|
AI_CriteriaSet &botCriteria = phrase->GetCriteriaSet();
|
|
if ( count != UNDEFINED_COUNT )
|
|
{
|
|
botCriteria.AppendCriteria( "count", count );
|
|
}
|
|
if ( place )
|
|
{
|
|
botCriteria.AppendCriteria( "place", place );
|
|
}
|
|
if ( CSGameRules()->IsPlayingCoopMission() )
|
|
{
|
|
botCriteria.AppendCriteria( "gamemode", "coop" );
|
|
}
|
|
if ( me->HasHeavyArmor() )
|
|
{
|
|
botCriteria.AppendCriteria( "isheavy", 1 );
|
|
}
|
|
|
|
// TODO: Need voice pitch as a criteria?
|
|
// me->GetProfile()->GetVoicePitch()
|
|
|
|
bSpoke = me->SpeakAudioResponseRules( phrase->GetName(), &botCriteria, duration + 1.0f );
|
|
}
|
|
else
|
|
{
|
|
const char *filename = phrase->GetSpeakable( me->GetProfile()->GetVoiceBank(), &duration );
|
|
// CONSOLE_ECHO( "%s: Radio( '%s' )\n", STRING( me->pev->netname ), filename );
|
|
if ( filename )
|
|
{
|
|
me->SpeakAudio( filename, duration + 1.0f, me->GetProfile()->GetVoicePitch() );
|
|
bSpoke = true;
|
|
}
|
|
}
|
|
|
|
if ( !bSpoke )
|
|
{
|
|
RadioType radioEvent = phrase->GetRadioEquivalent();
|
|
if (radioEvent == RADIO_INVALID)
|
|
{
|
|
// skip directly to the next phrase
|
|
m_nextTime = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// use the standard radio
|
|
me->SendRadioMessage( radioEvent );
|
|
me->GetChatter()->ResetRadioSilenceDuration();
|
|
duration = 2.0f;
|
|
}
|
|
}
|
|
/* BOTPORT: Wire up bot voice over IP
|
|
else if ( g_engfuncs.pfnPlayClientVoice && TheBotPhrases->GetOutputType( me->GetProfile()->GetVoiceBank() ) == BOT_CHATTER_VOICE )
|
|
{
|
|
me->GetChatter()->ResetRadioSilenceDuration();
|
|
g_engfuncs.pfnPlayClientVoice( me->entindex() - 1, filename );
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
const float gap = 0.1f;
|
|
m_nextTime = gpGlobals->curtime + 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
|
|
*/
|
|
unsigned int BotStatement::GetPlace( void ) 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->GetPlace();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return true if this statement has an associated count
|
|
*/
|
|
bool BotStatement::HasCount( void ) 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;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
|
|
CountdownTimer BotChatterInterface::m_encourageTimer;
|
|
IntervalTimer BotChatterInterface::m_radioSilenceInterval[ 2 ];
|
|
|
|
|
|
enum PitchHack
|
|
{
|
|
P_HI,
|
|
P_NORMAL,
|
|
P_LOW
|
|
};
|
|
|
|
static int nextPitch = P_HI;
|
|
|
|
BotChatterInterface::BotChatterInterface( CCSBot *me )
|
|
{
|
|
m_me = me;
|
|
m_statementList = NULL;
|
|
|
|
switch( nextPitch )
|
|
{
|
|
case P_HI:
|
|
m_pitch = RandomInt( 105, 110 );
|
|
break;
|
|
|
|
case P_NORMAL:
|
|
m_pitch = RandomInt( 95, 105 );
|
|
break;
|
|
|
|
case P_LOW:
|
|
m_pitch = RandomInt( 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( void )
|
|
{
|
|
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();
|
|
m_warnSniperTimer.Invalidate();
|
|
m_heavyTauntTimer.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 == NULL)
|
|
{
|
|
statement->m_next = NULL;
|
|
statement->m_prev = NULL;
|
|
m_statementList = statement;
|
|
return;
|
|
}
|
|
|
|
// list has at least one statement on it
|
|
|
|
// insert into list in order
|
|
BotStatement *earlier = NULL;
|
|
for( s=m_statementList; s; 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 = NULL;
|
|
statement->m_next = m_statementList;
|
|
m_statementList->m_prev = statement;
|
|
m_statementList = statement;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Remove a statement
|
|
*/
|
|
void BotChatterInterface::RemoveStatement( BotStatement *statement )
|
|
{
|
|
if (statement->m_next)
|
|
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( void )
|
|
{
|
|
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->curtime;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Invoked when we die
|
|
*/
|
|
void BotChatterInterface::OnDeath( void )
|
|
{
|
|
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
|
|
const BotPhrase *pain = TheBotPhrases->GetPainPhrase();
|
|
if (pain)
|
|
{
|
|
/*
|
|
if ( g_engfuncs.pfnPlayClientVoice && TheBotPhrases->GetOutputType( m_me->GetProfile()->GetVoiceBank() ) == BOT_CHATTER_VOICE )
|
|
{
|
|
g_engfuncs.pfnPlayClientVoice( m_me->entindex() - 1, pain->GetSpeakable(m_me->GetProfile()->GetVoiceBank()) );
|
|
m_me->GetChatter()->ResetRadioSilenceDuration();
|
|
}
|
|
else
|
|
*/
|
|
{
|
|
if ( bot_chatter_use_rr.GetBool() )
|
|
{
|
|
AI_CriteriaSet botCriteria;
|
|
if ( CSGameRules()->IsPlayingCoopMission() )
|
|
{
|
|
botCriteria.AppendCriteria( "gamemode", "coop" );
|
|
}
|
|
if ( m_me->HasHeavyArmor() )
|
|
{
|
|
botCriteria.AppendCriteria( "isheavy", 1 );
|
|
}
|
|
|
|
m_me->SpeakAudioResponseRules( "DeathCry", &botCriteria, 0.0f );
|
|
}
|
|
else
|
|
{
|
|
m_me->SpeakAudio( pain->GetSpeakable( m_me->GetProfile()->GetVoiceBank() ), 0.0f, m_me->GetProfile()->GetVoicePitch() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove all of our statements
|
|
Reset();
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Process ongoing chatter for this bot
|
|
*/
|
|
void BotChatterInterface::Update( void )
|
|
{
|
|
// 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 = NULL;
|
|
|
|
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" );
|
|
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( void )
|
|
{
|
|
// keep track of statement waiting longest to be spoken - it is next
|
|
BotStatement *earliest = NULL;
|
|
float earlyTime = 999999999.9f;
|
|
|
|
for( int i = 1; i <= gpGlobals->maxClients; i++ )
|
|
{
|
|
CCSPlayer *player = static_cast<CCSPlayer *>( UTIL_PlayerByIndex( i ) );
|
|
|
|
if (player == NULL)
|
|
continue;
|
|
|
|
// ignore dead humans
|
|
if (!player->IsBot() && !player->IsAlive())
|
|
continue;
|
|
|
|
// ignore enemies, since we can't hear them talk
|
|
if (player->IsOtherEnemy( m_me->entindex() ))
|
|
continue;
|
|
|
|
CCSBot *bot = dynamic_cast<CCSBot *>(player);
|
|
|
|
// if not a bot, fail the test
|
|
/// @todo Check if human is currently talking
|
|
if (!bot)
|
|
continue;
|
|
|
|
for( BotStatement *say = bot->GetChatter()->m_statementList; say; say = say->m_next )
|
|
{
|
|
// if this statement is currently being spoken, return it
|
|
if (say->IsSpeaking())
|
|
return say;
|
|
|
|
// keep track of statement that has been waiting longest to be spoken of anyone on our team
|
|
if (say->GetStartTime() < earlyTime)
|
|
{
|
|
earlyTime = say->GetTimestamp();
|
|
earliest = say;
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure it is time to start this statement
|
|
if (earliest && earliest->GetStartTime() > gpGlobals->curtime)
|
|
return NULL;
|
|
|
|
return earliest;
|
|
}
|
|
|
|
/**
|
|
* Return true if we speaking makes sense now
|
|
*/
|
|
bool BotChatterInterface::ShouldSpeak( void ) 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( void )
|
|
{
|
|
return m_radioSilenceInterval[ m_me->GetTeamNumber() % 2 ].GetElapsedTime();
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::ResetRadioSilenceDuration( void )
|
|
{
|
|
m_radioSilenceInterval[ m_me->GetTeamNumber() % 2 ].Reset();
|
|
}
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
inline const BotPhrase *GetPlacePhrase( CCSBot *me )
|
|
{
|
|
Place place = me->GetPlace();
|
|
if (place != UNDEFINED_PLACE)
|
|
return TheBotPhrases->GetPlace( place );
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
inline void SayWhere( BotStatement *say, Place place )
|
|
{
|
|
say->AppendPhrase( TheBotPhrases->GetPlace( place ) );
|
|
}
|
|
|
|
/**
|
|
* Report enemy sightings
|
|
*/
|
|
void BotChatterInterface::EnemySpotted( void )
|
|
{
|
|
// 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 );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* 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 );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
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( void )
|
|
{
|
|
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( void )
|
|
{
|
|
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()->GetAbsOrigin() ) );
|
|
}
|
|
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->curtime + 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( void )
|
|
{
|
|
const float minRequestInterval = 10.0f;
|
|
if (m_needBackupInterval.IsLessThen( minRequestInterval ))
|
|
return false;
|
|
|
|
if ( m_me->HasHeavyArmor() )
|
|
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( void )
|
|
{
|
|
// this is a form of "need backup"
|
|
const float minRequestInterval = 10.0f;
|
|
if (m_needBackupInterval.IsLessThen( minRequestInterval ))
|
|
return;
|
|
|
|
if ( m_me->HasHeavyArmor() )
|
|
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 );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* If a friend said that they heard something, we don't want to say something similar
|
|
* for a while.
|
|
*/
|
|
void BotChatterInterface::FriendHeardNoise( void )
|
|
{
|
|
m_heardNoiseTimer.Start( 20.0f );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
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 (RandomFloat( 0, 100 ) < 33)
|
|
{
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 5.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "HeardNoise" ) );
|
|
say->SetPlace( TheNavMesh->GetPlace( pos ) );
|
|
say->AttachMeme( new BotHeardNoiseMeme() );
|
|
|
|
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( void )
|
|
{
|
|
// 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->curtime + RandomFloat( 2.0f, 4.0f ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::Affirmative( void )
|
|
{
|
|
BotStatement *say = new BotStatement( this, REPORT_ACKNOWLEDGE, 3.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "Affirmative" ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::Negative( void )
|
|
{
|
|
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 = 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 );
|
|
|
|
Vector myOrigin = GetCentroid( m_me );
|
|
say->AttachMeme( new BotDefendHereMeme( myOrigin ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::TheyPickedUpTheBomb( void )
|
|
{
|
|
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
|
|
const Vector &myOrigin = GetCentroid( m_me );
|
|
m_me->GetGameState()->UpdateBomber( myOrigin );
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "TheyPickedUpTheBomb" ) );
|
|
|
|
say->AttachMeme( new BotBombStatusMeme( CSGameState::MOVING, myOrigin ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::SpottedBomber( CBasePlayer *bomber )
|
|
{
|
|
const Vector &bomberOrigin = GetCentroid( 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 && (bomberOrigin - *bomberPos).LengthSqr() < closeRangeSq)
|
|
return;
|
|
}
|
|
|
|
// update our gamestate
|
|
m_me->GetGameState()->UpdateBomber( bomberOrigin );
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f );
|
|
|
|
// where is the bomber
|
|
Place place = TheNavMesh->GetPlace( bomberOrigin );
|
|
SayWhere( say, place );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "SpottedBomber" ) );
|
|
|
|
say->SetSubject( bomber->entindex() );
|
|
|
|
//say->AttachMeme( new BotHelpMeme( place ) );
|
|
say->AttachMeme( new BotBombStatusMeme( CSGameState::MOVING, bomberOrigin ) );
|
|
|
|
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->GetAbsOrigin() );
|
|
|
|
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 = TheNavMesh->GetPlace( bomb->GetAbsOrigin() );
|
|
SayWhere( say, place );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "SpottedLooseBomb" ) );
|
|
|
|
if (TheCSBots()->GetLooseBomb())
|
|
say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, bomb->GetAbsOrigin() ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::GuardingLooseBomb( CBaseEntity *bomb )
|
|
{
|
|
// if we already know the bomb is loose, this is old news
|
|
// if (m_me->GetGameState()->IsBombLoose())
|
|
// return;
|
|
|
|
if (TheCSBots()->IsRoundOver() || !bomb)
|
|
return;
|
|
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen( minInterval ))
|
|
return;
|
|
|
|
m_planInterval.Reset();
|
|
|
|
// update our gamestate
|
|
m_me->GetGameState()->UpdateLooseBomb( bomb->GetAbsOrigin() );
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f );
|
|
|
|
// where is the bomb
|
|
Place place = TheNavMesh->GetPlace( bomb->GetAbsOrigin() );
|
|
SayWhere( say, place );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "GuardingLooseBomb" ) );
|
|
|
|
if (TheCSBots()->GetLooseBomb())
|
|
say->AttachMeme( new BotBombStatusMeme( CSGameState::LOOSE, bomb->GetAbsOrigin() ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::RequestBombLocation( void )
|
|
{
|
|
// only ask once per round
|
|
if (m_requestedBombLocation)
|
|
return;
|
|
|
|
m_requestedBombLocation = true;
|
|
|
|
// tell our teammates
|
|
BotStatement *say = new BotStatement( this, REPORT_REQUEST_INFORMATION, 10.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "WhereIsTheBomb" ) );
|
|
|
|
say->AttachMeme( new BotWhereBombMeme() );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::BombsiteClear( int zoneIndex )
|
|
{
|
|
const CCSBotManager::Zone *zone = TheCSBots()->GetZone( zoneIndex );
|
|
if (zone == NULL)
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 10.0f );
|
|
|
|
SayWhere( say, TheNavMesh->GetPlace( zone->m_center ) );
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "BombsiteClear" ) );
|
|
|
|
say->AttachMeme( new BotBombsiteStatusMeme( zoneIndex, BotBombsiteStatusMeme::CLEAR ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::FoundPlantedBomb( int zoneIndex )
|
|
{
|
|
const CCSBotManager::Zone *zone = TheCSBots()->GetZone( zoneIndex );
|
|
if (zone == NULL)
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "PlantedBombPlace" ) );
|
|
say->SetPlace( TheNavMesh->GetPlace( zone->m_center ) );
|
|
|
|
say->AttachMeme( new BotBombsiteStatusMeme( zoneIndex, BotBombsiteStatusMeme::PLANTED ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::Scared( void )
|
|
{
|
|
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( void )
|
|
{
|
|
BotStatement *say = new BotStatement( this, REPORT_EMOTE, 15.0f );
|
|
|
|
// wait a bit before speaking
|
|
say->SetStartTime( gpGlobals->curtime + RandomFloat( 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 (RandomFloat( 0.0f, 100.0f ) < 33.3f)
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "LastManStanding" ) );
|
|
}
|
|
else
|
|
{
|
|
if (TheCSBots()->GetElapsedRoundTime() < quickRound)
|
|
{
|
|
if (RandomFloat( 0.0f, 100.0f ) < 33.3f)
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "WonRoundQuickly" ) );
|
|
}
|
|
else if (RandomFloat( 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() + RandomFloat( 2.0, 3.0f ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::GuardingBombsite( Place place )
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen( minInterval ))
|
|
return;
|
|
|
|
m_planInterval.Reset();
|
|
|
|
AnnouncePlan( "GoingToDefendBombsite", place );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::GuardingHostages( Place place, bool isPlan )
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen( minInterval ))
|
|
return;
|
|
|
|
m_planInterval.Reset();
|
|
|
|
if (isPlan)
|
|
AnnouncePlan( "GoingToGuardHostages", place );
|
|
else
|
|
Say( "GuardingHostages" );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::GuardingHostageEscapeZone( bool isPlan )
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
const float minInterval = 20.0f;
|
|
if (m_planInterval.IsLessThen( minInterval ))
|
|
return;
|
|
|
|
m_planInterval.Reset();
|
|
|
|
if (isPlan)
|
|
AnnouncePlan( "GoingToGuardHostageEscapeZone", UNDEFINED_PLACE );
|
|
else
|
|
Say( "GuardingHostageEscapeZone" );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::HostagesBeingTaken( void )
|
|
{
|
|
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( void )
|
|
{
|
|
if (TheCSBots()->IsRoundOver())
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement( this, REPORT_INFORMATION, 3.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "HostagesTaken" ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::TalkingToHostages( void )
|
|
{
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::EscortingHostages( void )
|
|
{
|
|
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 );
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::HostageDown( void )
|
|
{
|
|
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( void )
|
|
{
|
|
BotStatement *say = new BotStatement( this, REPORT_KILLED_FRIEND, 2.0f );
|
|
|
|
say->AppendPhrase( TheBotPhrases->GetPhrase( "KilledFriend" ) );
|
|
|
|
// give them time to react
|
|
say->SetStartTime( gpGlobals->curtime + RandomFloat( 0.5f, 1.0f ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::FriendlyFire( const char *pDmgType )
|
|
{
|
|
if ( !mp_friendlyfire.GetBool() )
|
|
return;
|
|
|
|
BotStatement *say = new BotStatement( this, REPORT_FRIENDLY_FIRE, 1.0f );
|
|
|
|
AI_CriteriaSet botCriteria;
|
|
if ( pDmgType )
|
|
{
|
|
botCriteria.AppendCriteria( "damagetype", pDmgType );
|
|
}
|
|
|
|
const BotPhrase *pPhrase = TheBotPhrases->GetPhrase( "FriendlyFire" );
|
|
pPhrase->SetCriteriaSet( botCriteria );
|
|
|
|
say->AppendPhrase( pPhrase );
|
|
|
|
// give them time to react
|
|
say->SetStartTime( gpGlobals->curtime + RandomFloat( 0.3f, 0.5f ) );
|
|
|
|
AddStatement( say );
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
void BotChatterInterface::DoPhoenixHeavyWakeTaunt( void )
|
|
{
|
|
if ( m_heavyTauntTimer.IsElapsed() )
|
|
{
|
|
m_heavyTauntTimer.Start( RandomInt( 9, 15 ) );
|
|
|
|
Say( "HeavyWakeTaunt" );
|
|
}
|
|
}
|