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.
1890 lines
47 KiB
1890 lines
47 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: tf_populator_spawners |
|
// Implementations of NPC Spawning Code for PvE related game modes (MvM) |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
|
|
#include "tf_population_manager.h" |
|
#include "tf_team.h" |
|
#include "tf_obj_sentrygun.h" |
|
#include "tf_weapon_medigun.h" |
|
#include "tf_tank_boss.h" |
|
#include "bot/behavior/engineer/mvm_engineer/tf_bot_mvm_engineer_idle.h" |
|
|
|
#include "etwprof.h" |
|
|
|
extern ConVar tf_mvm_skill; |
|
extern ConVar tf_mm_trusted; |
|
|
|
extern ConVar tf_populator_debug; |
|
extern ConVar tf_populator_active_buffer_range; |
|
extern ConVar tf_mvm_miniboss_scale; |
|
|
|
ConVar tf_debug_placement_failure( "tf_debug_placement_failure", "0", FCVAR_CHEAT ); |
|
|
|
LINK_ENTITY_TO_CLASS( populator_internal_spawn_point, CPopulatorInternalSpawnPoint ); |
|
CHandle< CPopulatorInternalSpawnPoint > g_internalSpawnPoint = NULL; |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if a player has room to spawn at the given position |
|
*/ |
|
bool IsSpaceToSpawnHere( const Vector &where ) |
|
{ |
|
// make sure a player will fit here |
|
trace_t result; |
|
float bloat = 5.0f; |
|
UTIL_TraceHull( where, where, VEC_HULL_MIN - Vector( bloat, bloat, 0 ), VEC_HULL_MAX + Vector( bloat, bloat, bloat ), MASK_SOLID | CONTENTS_PLAYERCLIP, NULL, COLLISION_GROUP_PLAYER_MOVEMENT, &result ); |
|
|
|
if ( tf_debug_placement_failure.GetBool() && result.fraction < 1.0f ) |
|
{ |
|
NDebugOverlay::Cross3D( where, 5.0f, 255, 100, 0, true, 99999.9f ); |
|
} |
|
|
|
return result.fraction >= 1.0; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
//----------------------------------------------------------------------- |
|
/*static*/ IPopulationSpawner *IPopulationSpawner::ParseSpawner( IPopulator *populator, KeyValues *data ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
if ( !Q_stricmp( name, "TFBot" ) ) |
|
{ |
|
CTFBotSpawner *botSpawner = new CTFBotSpawner( populator ); |
|
|
|
if ( botSpawner->Parse( data ) == false ) |
|
{ |
|
Warning( "Warning reading TFBot spawner definition\n" ); |
|
delete botSpawner; |
|
return NULL; |
|
} |
|
|
|
return botSpawner; |
|
} |
|
else if ( !Q_stricmp( name, "Tank" ) ) |
|
{ |
|
CTankSpawner *tankSpawner = new CTankSpawner( populator ); |
|
|
|
if ( tankSpawner->Parse( data ) == false ) |
|
{ |
|
Warning( "Warning reading Tank spawner definition\n" ); |
|
delete tankSpawner; |
|
return NULL; |
|
} |
|
|
|
return tankSpawner; |
|
} |
|
else if ( !Q_stricmp( name, "SentryGun" ) ) |
|
{ |
|
CSentryGunSpawner *sentrySpawner = new CSentryGunSpawner( populator ); |
|
|
|
if ( sentrySpawner->Parse( data ) == false ) |
|
{ |
|
Warning( "Warning reading SentryGun spawner definition\n" ); |
|
delete sentrySpawner; |
|
return NULL; |
|
} |
|
|
|
return sentrySpawner; |
|
} |
|
else if ( !Q_stricmp( name, "Squad" ) ) |
|
{ |
|
CSquadSpawner *squadSpawner = new CSquadSpawner( populator ); |
|
|
|
if ( squadSpawner->Parse( data ) == false ) |
|
{ |
|
Warning( "Warning reading Squad spawner definition\n" ); |
|
delete squadSpawner; |
|
return NULL; |
|
} |
|
|
|
return squadSpawner; |
|
} |
|
else if ( !Q_stricmp( name, "Mob" ) ) |
|
{ |
|
CMobSpawner *mobSpawner = new CMobSpawner( populator ); |
|
|
|
if ( mobSpawner->Parse( data ) == false ) |
|
{ |
|
Warning( "Warning reading Mob spawner definition\n" ); |
|
delete mobSpawner; |
|
return NULL; |
|
} |
|
|
|
return mobSpawner; |
|
} |
|
else if ( !Q_stricmp( name, "RandomChoice" ) ) |
|
{ |
|
CRandomChoiceSpawner *randomSpawner = new CRandomChoiceSpawner( populator ); |
|
|
|
if ( randomSpawner->Parse( data ) == false ) |
|
{ |
|
Warning( "Warning reading RandomChoice spawner definition\n" ); |
|
delete randomSpawner; |
|
return NULL; |
|
} |
|
|
|
return randomSpawner; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
//----------------------------------------------------------------------- |
|
static EventInfo *ParseEvent( KeyValues *values ) |
|
{ |
|
EventInfo *eventInfo = new EventInfo; |
|
|
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( !Q_stricmp( name, "Target" ) ) |
|
{ |
|
eventInfo->m_target.sprintf( "%s", data->GetString() ); |
|
} |
|
else if ( !Q_stricmp( name, "Action" ) ) |
|
{ |
|
eventInfo->m_action.sprintf( "%s", data->GetString() ); |
|
} |
|
else |
|
{ |
|
Warning( "Unknown field '%s' in WaveSpawn event definition.\n", data->GetString() ); |
|
delete eventInfo; |
|
return NULL; |
|
} |
|
} |
|
|
|
return eventInfo; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
// CRandomChoiceSpawner |
|
//----------------------------------------------------------------------- |
|
CRandomChoiceSpawner::CRandomChoiceSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) |
|
{ |
|
m_spawnerVector.RemoveAll(); |
|
m_nRandomPickDecision.RemoveAll(); |
|
m_nNumSpawned = 0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
CRandomChoiceSpawner::~CRandomChoiceSpawner() |
|
{ |
|
for( int i=0; i<m_spawnerVector.Count(); ++i ) |
|
{ |
|
delete m_spawnerVector[i]; |
|
} |
|
m_spawnerVector.RemoveAll(); |
|
m_nRandomPickDecision.RemoveAll(); |
|
m_nNumSpawned = 0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CRandomChoiceSpawner::Parse( KeyValues *values ) |
|
{ |
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
IPopulationSpawner *spawner = ParseSpawner( m_populator, data ); |
|
|
|
if ( spawner == NULL ) |
|
{ |
|
Warning( "Unknown attribute '%s' in RandomChoice definition.\n", name ); |
|
} |
|
else |
|
{ |
|
m_spawnerVector.AddToTail( spawner ); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CRandomChoiceSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) |
|
{ |
|
if ( m_spawnerVector.Count() < 1 ) |
|
return false; |
|
|
|
int nCurrentCount = m_nRandomPickDecision.Count(); |
|
int nNumToAdd = ( m_nNumSpawned + 1 ) - nCurrentCount; |
|
|
|
if ( nNumToAdd > 0 ) |
|
{ |
|
m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); |
|
|
|
for ( int i = nCurrentCount; i < m_nNumSpawned + 1; ++i ) |
|
{ |
|
m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); |
|
} |
|
} |
|
|
|
bool bResult = m_spawnerVector[ m_nRandomPickDecision[ m_nNumSpawned ] ]->Spawn( here, result ); |
|
|
|
m_nNumSpawned++; |
|
|
|
return bResult; |
|
} |
|
|
|
int CRandomChoiceSpawner::GetClass( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 ) |
|
{ |
|
return TF_CLASS_UNDEFINED; |
|
} |
|
|
|
int nCurrentCount = m_nRandomPickDecision.Count(); |
|
int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; |
|
|
|
if ( nNumToAdd > 0 ) |
|
{ |
|
m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); |
|
|
|
for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) |
|
{ |
|
m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); |
|
} |
|
} |
|
|
|
if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) |
|
{ |
|
return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->GetClass(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return TF_CLASS_UNDEFINED; |
|
} |
|
} |
|
|
|
string_t CRandomChoiceSpawner::GetClassIcon( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 ) |
|
{ |
|
return NULL_STRING; |
|
} |
|
|
|
int nCurrentCount = m_nRandomPickDecision.Count(); |
|
int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; |
|
|
|
if ( nNumToAdd > 0 ) |
|
{ |
|
m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); |
|
|
|
for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) |
|
{ |
|
m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); |
|
} |
|
} |
|
|
|
if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) |
|
{ |
|
return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->GetClassIcon(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return NULL_STRING; |
|
} |
|
} |
|
|
|
int CRandomChoiceSpawner::GetHealth( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 ) |
|
{ |
|
return 0; |
|
} |
|
|
|
int nCurrentCount = m_nRandomPickDecision.Count(); |
|
int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; |
|
|
|
if ( nNumToAdd > 0 ) |
|
{ |
|
m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); |
|
|
|
for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) |
|
{ |
|
m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); |
|
} |
|
} |
|
|
|
if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) |
|
{ |
|
return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->GetHealth(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return 0; |
|
} |
|
} |
|
|
|
|
|
bool CRandomChoiceSpawner::IsMiniBoss( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 ) |
|
{ |
|
return false; |
|
} |
|
|
|
int nCurrentCount = m_nRandomPickDecision.Count(); |
|
int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; |
|
|
|
if ( nNumToAdd > 0 ) |
|
{ |
|
m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); |
|
|
|
for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) |
|
{ |
|
m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); |
|
} |
|
} |
|
|
|
if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) |
|
{ |
|
return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsMiniBoss(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return false; |
|
} |
|
} |
|
|
|
|
|
bool CRandomChoiceSpawner::HasAttribute( CTFBot::AttributeType type, int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 ) |
|
{ |
|
return false; |
|
} |
|
|
|
int nCurrentCount = m_nRandomPickDecision.Count(); |
|
int nNumToAdd = ( nSpawnNum + 1 ) - nCurrentCount; |
|
|
|
if ( nNumToAdd > 0 ) |
|
{ |
|
m_nRandomPickDecision.AddMultipleToTail( nNumToAdd ); |
|
|
|
for ( int i = nCurrentCount; i < nSpawnNum + 1; ++i ) |
|
{ |
|
m_nRandomPickDecision[ i ] = RandomInt( 0, m_spawnerVector.Count() - 1 ); |
|
} |
|
} |
|
|
|
if ( !m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->IsVarious() ) |
|
{ |
|
return m_spawnerVector[ m_nRandomPickDecision[ nSpawnNum ] ]->HasAttribute( type, nSpawnNum ); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return false; |
|
} |
|
} |
|
|
|
|
|
bool CRandomChoiceSpawner::HasEventChangeAttributes( const char* pszEventName ) const |
|
{ |
|
for ( int i=0; i<m_spawnerVector.Count(); ++i ) |
|
{ |
|
m_spawnerVector[i]->HasEventChangeAttributes( pszEventName ); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
//----------------------------------------------------------------------- |
|
CTFBotSpawner::CTFBotSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) |
|
{ |
|
m_class = TF_CLASS_UNDEFINED; |
|
m_iszClassIcon = NULL_STRING; |
|
|
|
m_health = -1; // default health |
|
m_scale = -1.0f; // default scale |
|
m_flAutoJumpMin = m_flAutoJumpMax = 0.f; // default AutoJumpMin/Max |
|
|
|
m_defaultAttributes.Reset(); |
|
} |
|
|
|
static void ParseCharacterAttributes( CTFBot::EventChangeAttributes_t& event, KeyValues *data ) |
|
{ |
|
CUtlVector<static_attrib_t> vecStaticAttribs; |
|
|
|
FOR_EACH_SUBKEY( data, pKVAttribute ) |
|
{ |
|
CUtlVector<CUtlString> vecErrors; |
|
|
|
static_attrib_t staticAttrib; |
|
if ( !staticAttrib.BInitFromKV_SingleLine( "CharacterAttributes", pKVAttribute, &vecErrors ) ) |
|
{ |
|
FOR_EACH_VEC( vecErrors, i ) |
|
{ |
|
Warning( "TFBotSpawner: attribute error: '%s'\n", vecErrors[i].Get() ); |
|
} |
|
continue; |
|
} |
|
|
|
vecStaticAttribs.AddToTail( staticAttrib ); |
|
} |
|
|
|
// found an existing entry for this name -- stomp attribute values if present or add |
|
// new entries if not |
|
FOR_EACH_VEC( vecStaticAttribs, iNewAttribute ) |
|
{ |
|
const static_attrib_t& newStaticAttrib = vecStaticAttribs[iNewAttribute]; |
|
bool bAdd = true; |
|
|
|
int iExistingAttribute; |
|
for ( iExistingAttribute = 0; iExistingAttribute < event.m_characterAttributes.Count(); iExistingAttribute++ ) |
|
{ |
|
// matching existing attribute? stomp value |
|
if ( event.m_characterAttributes[iExistingAttribute].iDefIndex == newStaticAttrib.iDefIndex ) |
|
{ |
|
// stomp value |
|
event.m_characterAttributes[iExistingAttribute].m_value = newStaticAttrib.m_value; |
|
bAdd = false; |
|
// move on to next new attribute |
|
break; |
|
} |
|
} |
|
|
|
if ( bAdd ) |
|
{ |
|
// couldn't find? add new attribute entry |
|
event.m_characterAttributes.AddToTail( newStaticAttrib ); |
|
} |
|
} |
|
} |
|
|
|
static void ParseItemAttributes( CTFBot::EventChangeAttributes_t& event, KeyValues *data ) |
|
{ |
|
const char *pszItemName = NULL; |
|
CUtlVector<static_attrib_t> vecStaticAttribs; |
|
|
|
FOR_EACH_SUBKEY( data, pKVSubkey ) |
|
{ |
|
if ( !Q_stricmp( pKVSubkey->GetName(), "ItemName" ) ) |
|
{ |
|
if ( pszItemName ) |
|
{ |
|
Warning( "TFBotSpawner: \"ItemName\" field specified multiple times ('%s' / '%s').\n", pszItemName, pKVSubkey->GetString() ); |
|
} |
|
|
|
pszItemName = pKVSubkey->GetString(); |
|
} |
|
else |
|
{ |
|
CUtlVector<CUtlString> vecErrors; |
|
|
|
static_attrib_t staticAttrib; |
|
if ( !staticAttrib.BInitFromKV_SingleLine( "ItemAttributes", pKVSubkey, &vecErrors ) ) |
|
{ |
|
FOR_EACH_VEC( vecErrors, i ) |
|
{ |
|
Warning( "TFBotSpawner: attribute error: '%s'\n", vecErrors[i].Get() ); |
|
} |
|
continue; |
|
} |
|
|
|
vecStaticAttribs.AddToTail( staticAttrib ); |
|
} |
|
} |
|
|
|
if ( !pszItemName ) |
|
{ |
|
Warning( "TFBotSpawner: need to specify ItemName in ItemAttributes.\n" ); |
|
return; |
|
} |
|
|
|
// check if the item name is already on the list |
|
FOR_EACH_VEC( event.m_itemsAttributes, i ) |
|
{ |
|
CTFBot::EventChangeAttributes_t::item_attributes_t& botItemAttrs = event.m_itemsAttributes[i]; |
|
|
|
if ( !Q_stricmp( botItemAttrs.m_itemName, pszItemName ) ) |
|
{ |
|
// found an existing entry for this name -- stomp attribute values if present or add |
|
// new entries if not |
|
FOR_EACH_VEC( vecStaticAttribs, iNewAttribute ) |
|
{ |
|
const static_attrib_t& newStaticAttrib = vecStaticAttribs[iNewAttribute]; |
|
|
|
int iExistingAttribute; |
|
for ( iExistingAttribute = 0; iExistingAttribute < botItemAttrs.m_attributes.Count(); iExistingAttribute++ ) |
|
{ |
|
// matching existing attribute? stomp value |
|
if ( botItemAttrs.m_attributes[iExistingAttribute].iDefIndex == newStaticAttrib.iDefIndex ) |
|
{ |
|
// stomp value |
|
botItemAttrs.m_attributes[iExistingAttribute].m_value = newStaticAttrib.m_value; |
|
|
|
// move on to next new attribute |
|
break; |
|
} |
|
} |
|
|
|
// couldn't find? add new attribute entry |
|
botItemAttrs.m_attributes.AddToTail( newStaticAttrib ); |
|
} |
|
|
|
// only one entry expected -- done here, before we add a new one |
|
return; |
|
} |
|
} |
|
|
|
// new item |
|
CTFBot::EventChangeAttributes_t::item_attributes_t botItemAttributes; |
|
botItemAttributes.m_itemName = pszItemName; |
|
botItemAttributes.m_attributes.AddVectorToTail( vecStaticAttribs ); |
|
|
|
event.m_itemsAttributes.AddToTail( botItemAttributes ); |
|
} |
|
|
|
static bool ParseDynamicAttributes( CTFBot::EventChangeAttributes_t& event, KeyValues* data ) |
|
{ |
|
const char *name = data->GetName(); |
|
const char *value = data->GetString(); |
|
|
|
if ( !Q_stricmp( name, "Skill" ) ) |
|
{ |
|
if ( !Q_stricmp( value, "Easy" ) ) |
|
{ |
|
event.m_skill = CTFBot::EASY; |
|
} |
|
else if ( !Q_stricmp( value, "Normal" ) ) |
|
{ |
|
event.m_skill = CTFBot::NORMAL; |
|
} |
|
else if ( !Q_stricmp( value, "Hard" ) ) |
|
{ |
|
event.m_skill = CTFBot::HARD; |
|
} |
|
else if ( !Q_stricmp( value, "Expert" ) ) |
|
{ |
|
event.m_skill = CTFBot::EXPERT; |
|
} |
|
else |
|
{ |
|
Warning( "TFBotSpawner: Invalid skill '%s'\n", value ); |
|
return false; |
|
} |
|
} |
|
else if ( !Q_stricmp( name, "WeaponRestrictions" ) ) |
|
{ |
|
if ( !Q_stricmp( value, "MeleeOnly" ) ) |
|
{ |
|
event.m_weaponRestriction = CTFBot::MELEE_ONLY; |
|
} |
|
else if ( !Q_stricmp( value, "PrimaryOnly" ) ) |
|
{ |
|
event.m_weaponRestriction = CTFBot::PRIMARY_ONLY; |
|
} |
|
else if ( !Q_stricmp( value, "SecondaryOnly" ) ) |
|
{ |
|
event.m_weaponRestriction = CTFBot::SECONDARY_ONLY; |
|
} |
|
else |
|
{ |
|
Warning( "TFBotSpawner: Invalid weapon restriction '%s'\n", value ); |
|
return false; |
|
} |
|
} |
|
else if ( !Q_stricmp( name, "BehaviorModifiers" ) ) |
|
{ |
|
// modifying bot attribute flags here due to legacy code |
|
if ( !Q_stricmp( value, "Mobber" ) || !Q_stricmp( value, "Push" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::AGGRESSIVE; |
|
} |
|
else |
|
{ |
|
Warning( "TFBotSpawner: Invalid behavior modifier '%s'\n", value ); |
|
return false; |
|
} |
|
} |
|
else if ( !Q_stricmp( name, "Attributes" ) ) |
|
{ |
|
if ( !Q_stricmp( value, "RemoveOnDeath" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::REMOVE_ON_DEATH; |
|
} |
|
else if ( !Q_stricmp( value, "Aggressive" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::AGGRESSIVE; |
|
} |
|
else if ( !Q_stricmp( value, "SuppressFire" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::SUPPRESS_FIRE; |
|
} |
|
else if ( !Q_stricmp( value, "DisableDodge" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::DISABLE_DODGE; |
|
} |
|
else if ( !Q_stricmp( value, "BecomeSpectatorOnDeath" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::BECOME_SPECTATOR_ON_DEATH; |
|
} |
|
else if ( !Q_stricmp( value, "RetainBuildings" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::RETAIN_BUILDINGS; |
|
} |
|
else if ( !Q_stricmp( value, "SpawnWithFullCharge" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::SPAWN_WITH_FULL_CHARGE; |
|
} |
|
else if ( !Q_stricmp( value, "AlwaysCrit" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::ALWAYS_CRIT; |
|
} |
|
else if ( !Q_stricmp( value, "IgnoreEnemies" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::IGNORE_ENEMIES; |
|
} |
|
else if ( !Q_stricmp( value, "HoldFireUntilFullReload" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::HOLD_FIRE_UNTIL_FULL_RELOAD; |
|
} |
|
else if ( !Q_stricmp( value, "AlwaysFireWeapon" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::ALWAYS_FIRE_WEAPON; |
|
} |
|
else if ( !Q_stricmp( value, "TeleportToHint" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::TELEPORT_TO_HINT; |
|
} |
|
else if ( !Q_stricmp( value, "MiniBoss" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::MINIBOSS; |
|
} |
|
else if ( !Q_stricmp( value, "UseBossHealthBar" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::USE_BOSS_HEALTH_BAR; |
|
} |
|
else if ( !Q_stricmp( value, "IgnoreFlag" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::IGNORE_FLAG; |
|
} |
|
else if ( !Q_stricmp( value, "AutoJump" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::AUTO_JUMP; |
|
} |
|
else if ( !Q_stricmp( value, "AirChargeOnly" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::AIR_CHARGE_ONLY; |
|
} |
|
else if( !Q_stricmp( value, "VaccinatorBullets" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::PREFER_VACCINATOR_BULLETS; |
|
} |
|
else if( !Q_stricmp( value, "VaccinatorBlast" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::PREFER_VACCINATOR_BLAST; |
|
} |
|
else if( !Q_stricmp( value, "VaccinatorFire" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::PREFER_VACCINATOR_FIRE; |
|
} |
|
else if( !Q_stricmp( value, "BulletImmune" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::BULLET_IMMUNE; |
|
} |
|
else if( !Q_stricmp( value, "BlastImmune" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::BLAST_IMMUNE; |
|
} |
|
else if( !Q_stricmp( value, "FireImmune" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::FIRE_IMMUNE; |
|
} |
|
else if ( !Q_stricmp( value, "Parachute" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::PARACHUTE; |
|
} |
|
else if ( !Q_stricmp( value, "ProjectileShield" ) ) |
|
{ |
|
event.m_attributeFlags |= CTFBot::PROJECTILE_SHIELD; |
|
} |
|
else |
|
{ |
|
Warning( "TFBotSpawner: Invalid attribute '%s'\n", value ); |
|
return false; |
|
} |
|
} |
|
else if ( !Q_stricmp( name, "MaxVisionRange" ) ) |
|
{ |
|
event.m_maxVisionRange = data->GetFloat(); |
|
} |
|
else if ( !Q_stricmp( name, "Item" ) ) |
|
{ |
|
event.m_items.CopyAndAddToTail( value ); |
|
} |
|
else if ( !Q_stricmp( name, "ItemAttributes" ) ) |
|
{ |
|
ParseItemAttributes( event, data ); |
|
} |
|
else if ( !Q_stricmp( name, "CharacterAttributes" ) ) |
|
{ |
|
ParseCharacterAttributes( event, data ); |
|
} |
|
else if ( !Q_stricmp( name, "Tag" ) ) |
|
{ |
|
event.m_tags.CopyAndAddToTail( value ); |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
bool CTFBotSpawner::Parse( KeyValues *values ) |
|
{ |
|
// Reset All values |
|
m_class = TF_CLASS_UNDEFINED; |
|
m_iszClassIcon = NULL_STRING; |
|
|
|
m_health = -1; // default health |
|
m_scale = -1.0f; // default scale |
|
m_flAutoJumpMin = m_flAutoJumpMax = 0.f; // default AutoJumpMin/Max |
|
|
|
m_defaultAttributes.Reset(); |
|
m_eventChangeAttributes.RemoveAll(); |
|
|
|
// First, see if we have any Template key |
|
KeyValues *pTemplate = values->FindKey("Template"); |
|
if ( pTemplate ) |
|
{ |
|
KeyValues *pTemplateKV = GetPopulator()->GetManager()->GetTemplate( pTemplate->GetString() ); |
|
if ( pTemplateKV ) |
|
{ |
|
// Pump all the keys into ourself now |
|
if ( Parse( pTemplateKV ) == false ) |
|
{ |
|
return false; |
|
} |
|
} |
|
else |
|
{ |
|
Warning( "Unknown Template '%s' in TFBotSpawner definition\n", pTemplate->GetString() ); |
|
} |
|
} |
|
|
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
const char *value = data->GetString(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
// Skip templates when looping through the rest of the keys |
|
if ( !Q_stricmp( name, "Template" ) ) |
|
continue; |
|
|
|
if ( !Q_stricmp( name, "Class" ) ) |
|
{ |
|
m_class = GetClassIndexFromString( value ); |
|
if ( m_class == TF_CLASS_UNDEFINED ) |
|
{ |
|
Warning( "TFBotSpawner: Invalid class '%s'\n", value ); |
|
return false; |
|
} |
|
if ( m_name.IsEmpty() ) |
|
{ |
|
m_name = value; |
|
} |
|
} |
|
else if ( !Q_stricmp( name, "ClassIcon" ) ) |
|
{ |
|
m_iszClassIcon = AllocPooledString( value ); |
|
} |
|
else if ( !Q_stricmp( name, "Health" ) ) |
|
{ |
|
m_health = data->GetInt(); |
|
} |
|
else if ( !Q_stricmp( name, "Scale" ) ) |
|
{ |
|
m_scale = data->GetFloat(); |
|
} |
|
else if ( !Q_stricmp( name, "Name" ) ) |
|
{ |
|
m_name = value; |
|
} |
|
else if ( !Q_stricmp( name, "TeleportWhere" ) ) |
|
{ |
|
m_teleportWhereName.CopyAndAddToTail( data->GetString() ); |
|
} |
|
else if ( !Q_stricmp( name, "AutoJumpMin" ) ) |
|
{ |
|
m_flAutoJumpMin = data->GetFloat(); |
|
} |
|
else if ( !Q_stricmp( name, "AutoJumpMax" ) ) |
|
{ |
|
m_flAutoJumpMax = data->GetFloat(); |
|
} |
|
else if ( !Q_stricmp( name, "EventChangeAttributes" ) ) |
|
{ |
|
if ( !ParseEventChangeAttributes( data ) ) |
|
{ |
|
Warning( "TFBotSpawner: Failed to parse EventChangeAttributes\n" ); |
|
return false; |
|
} |
|
} |
|
else if ( ParseDynamicAttributes( m_defaultAttributes, data ) ) |
|
{ |
|
// Do nothing on success |
|
} |
|
else |
|
{ |
|
Warning( "TFBotSpawner: Unknown field '%s'\n", name ); |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CTFBotSpawner::ParseEventChangeAttributes( KeyValues *data ) |
|
{ |
|
for ( KeyValues *pKVEvent = data->GetFirstSubKey(); pKVEvent != NULL; pKVEvent = pKVEvent->GetNextKey() ) |
|
{ |
|
const char* pszEventName = pKVEvent->GetName(); |
|
|
|
// Add new event |
|
int index = m_eventChangeAttributes.AddToTail(); |
|
CTFBot::EventChangeAttributes_t& event = m_eventChangeAttributes[index]; |
|
event.m_eventName = pszEventName; |
|
|
|
for ( KeyValues *pAttr=pKVEvent->GetFirstSubKey(); pAttr != NULL; pAttr = pAttr->GetNextKey() ) |
|
{ |
|
if ( !ParseDynamicAttributes( event, pAttr ) ) |
|
{ |
|
Warning( "TFBotSpawner EventChangeAttributes: Failed to parse event '%s' with unknown attribute '%s'\n", pKVEvent->GetName(), pAttr->GetName() ); |
|
return false; |
|
} |
|
} |
|
|
|
// should override default attr? |
|
if ( !Q_stricmp( pszEventName, "default" ) ) |
|
{ |
|
m_defaultAttributes = event; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CTFBotSpawner::Spawn( const Vector &rawHere, EntityHandleVector_t *result ) |
|
{ |
|
CETWScope timer( "CTFBotSpawner::Spawn" ); |
|
VPROF_BUDGET( "CTFBotSpawner::Spawn", "NextBot" ); |
|
|
|
CTFBot *newBot = NULL; |
|
|
|
Vector here = rawHere; |
|
|
|
CTFNavArea *area = (CTFNavArea *)TheTFNavMesh()->GetNavArea( here ); |
|
if ( area && area->HasAttributeTF( TF_NAV_NO_SPAWNING ) ) |
|
{ |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTFBotSpawner: %3.2f: *** Tried to spawn in a NO_SPAWNING area at (%f, %f, %f)\n", gpGlobals->curtime, here.x, here.y, here.z ); |
|
} |
|
return false; |
|
} |
|
|
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
// Only spawn bots while the round is running in MVM mode |
|
if ( TFGameRules()->State_Get() != GR_STATE_RND_RUNNING ) |
|
return false; |
|
} |
|
|
|
// the ground may be variable here, try a few heights |
|
float z; |
|
for( z = 0.0f; z<StepHeight; z += 4.0f ) |
|
{ |
|
here.z = rawHere.z + StepHeight; |
|
|
|
if ( IsSpaceToSpawnHere( here ) ) |
|
{ |
|
break; |
|
} |
|
} |
|
|
|
if ( z >= StepHeight ) |
|
{ |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTFBotSpawner: %3.2f: *** No space to spawn at (%f, %f, %f)\n", gpGlobals->curtime, here.x, here.y, here.z ); |
|
} |
|
return false; |
|
} |
|
|
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
if ( m_class == TF_CLASS_ENGINEER && m_defaultAttributes.m_attributeFlags & CTFBot::TELEPORT_TO_HINT && CTFBotMvMEngineerHintFinder::FindHint( true, false ) == false ) |
|
{ |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTFBotSpawner: %3.2f: *** No teleporter hint for engineer\n", gpGlobals->curtime ); |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
|
|
|
|
// find dead bot we can re-use |
|
CTeam *deadTeam = GetGlobalTeam( TEAM_SPECTATOR ); |
|
for( int i=0; i<deadTeam->GetNumPlayers(); ++i ) |
|
{ |
|
if ( !deadTeam->GetPlayer(i)->IsBot() ) |
|
continue; |
|
|
|
// reuse this guy |
|
newBot = (CTFBot *)deadTeam->GetPlayer(i); |
|
newBot->ClearAllAttributes(); |
|
break; |
|
} |
|
|
|
if ( newBot == NULL ) |
|
{ |
|
//AssertMsg( 0, "Bots should be preallocated. This block of code should never get called.\n" ); |
|
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
int nNumEnemyBots = 0; |
|
|
|
CUtlVector<CTFPlayer *> botVector; |
|
CPopulationManager::CollectMvMBots( &botVector ); |
|
|
|
nNumEnemyBots = botVector.Count(); |
|
|
|
if ( nNumEnemyBots >= CPopulationManager::MVM_INVADERS_TEAM_SIZE ) |
|
{ |
|
// no room for more |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTFBotSpawner: %3.2f: *** Can't spawn. Max number invaders already spawned.\n", gpGlobals->curtime ); |
|
} |
|
|
|
// extra guard if we're over full on bots |
|
if ( nNumEnemyBots > CPopulationManager::MVM_INVADERS_TEAM_SIZE ) |
|
{ |
|
// Kick bots until we are at the proper number starting with spectator bots |
|
int iNumberToKick = nNumEnemyBots - CPopulationManager::MVM_INVADERS_TEAM_SIZE; |
|
int iKickedBots = 0; |
|
|
|
// loop through spectators and invaders in that order |
|
// Kick Spectators first |
|
CUtlVector<CTFPlayer *> botsToKick; |
|
for ( int iTeam = 0; iTeam < 2; iTeam++ ) |
|
{ |
|
int targetTeam = TEAM_SPECTATOR; |
|
if ( iTeam == 1 ) |
|
{ |
|
targetTeam = TF_TEAM_PVE_INVADERS; |
|
} |
|
|
|
FOR_EACH_VEC( botVector, iBot ) |
|
{ |
|
if ( iKickedBots >= iNumberToKick ) |
|
break; |
|
|
|
if ( botVector[iBot]->GetTeamNumber() == targetTeam ) |
|
{ |
|
botsToKick.AddToTail( botVector[iBot] ); |
|
iKickedBots++; |
|
} |
|
} |
|
} |
|
|
|
FOR_EACH_VEC ( botsToKick, iKick ) |
|
{ |
|
engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", botsToKick[iKick]->GetUserID() ) ); |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
else |
|
{ |
|
// need to add another bot - is there room? |
|
int totalPlayerCount = 0; |
|
totalPlayerCount += GetGlobalTeam( TEAM_SPECTATOR )->GetNumPlayers(); |
|
totalPlayerCount += GetGlobalTeam( TF_TEAM_BLUE )->GetNumPlayers(); |
|
totalPlayerCount += GetGlobalTeam( TF_TEAM_RED )->GetNumPlayers(); |
|
|
|
if ( totalPlayerCount >= gpGlobals->maxClients ) |
|
{ |
|
// no room for more |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTFBotSpawner: %3.2f: *** Can't spawn. No free player slot.\n", gpGlobals->curtime ); |
|
} |
|
return false; |
|
} |
|
} |
|
|
|
newBot = NextBotCreatePlayerBot< CTFBot >( "TFBot", false ); |
|
} |
|
|
|
if ( newBot ) |
|
{ |
|
// remove any player attributes |
|
newBot->RemovePlayerAttributes( false ); |
|
|
|
// clear any old TeleportWhere settings |
|
newBot->ClearTeleportWhere(); |
|
|
|
if ( g_internalSpawnPoint == NULL ) |
|
{ |
|
g_internalSpawnPoint = (CPopulatorInternalSpawnPoint *)CreateEntityByName( "populator_internal_spawn_point" ); |
|
g_internalSpawnPoint->Spawn(); |
|
} |
|
|
|
// set name |
|
engine->SetFakeClientConVarValue( newBot->edict(), "name", m_name.IsEmpty() ? "TFBot" : m_name.Get() ); |
|
|
|
g_internalSpawnPoint->SetAbsOrigin( here ); |
|
g_internalSpawnPoint->SetLocalAngles( vec3_angle ); |
|
newBot->SetSpawnPoint( g_internalSpawnPoint ); |
|
|
|
int team = TF_TEAM_RED; |
|
|
|
if ( TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
team = TF_TEAM_PVE_INVADERS; |
|
} |
|
|
|
newBot->ChangeTeam( team, false, true ); |
|
|
|
newBot->AllowInstantSpawn(); |
|
newBot->HandleCommand_JoinClass( GetPlayerClassData( m_class )->m_szClassName ); |
|
newBot->GetPlayerClass()->SetClassIconName( GetClassIcon() ); |
|
|
|
newBot->ClearEventChangeAttributes(); |
|
for ( int i=0; i<m_eventChangeAttributes.Count(); ++i ) |
|
{ |
|
newBot->AddEventChangeAttributes( &m_eventChangeAttributes[i] ); |
|
} |
|
|
|
// Request to Add in Endless |
|
if ( g_pPopulationManager->IsInEndlessWaves() ) |
|
{ |
|
g_pPopulationManager->EndlessSetAttributesForBot( newBot ); |
|
} |
|
|
|
newBot->SetTeleportWhere( m_teleportWhereName ); |
|
|
|
if ( m_defaultAttributes.m_attributeFlags & CTFBot::MINIBOSS ) |
|
{ |
|
newBot->SetIsMiniBoss( true ); |
|
} |
|
|
|
if ( m_defaultAttributes.m_attributeFlags & CTFBot::USE_BOSS_HEALTH_BAR ) |
|
{ |
|
newBot->SetUseBossHealthBar( true ); |
|
} |
|
|
|
if ( m_defaultAttributes.m_attributeFlags & CTFBot::AUTO_JUMP ) |
|
{ |
|
newBot->SetAutoJump( m_flAutoJumpMin, m_flAutoJumpMax ); |
|
} |
|
|
|
if( m_defaultAttributes.m_attributeFlags & CTFBot::BULLET_IMMUNE ) |
|
{ |
|
newBot->m_Shared.AddCond( TF_COND_BULLET_IMMUNE ); |
|
} |
|
|
|
if( m_defaultAttributes.m_attributeFlags & CTFBot::BLAST_IMMUNE ) |
|
{ |
|
newBot->m_Shared.AddCond( TF_COND_BLAST_IMMUNE ); |
|
} |
|
|
|
if( m_defaultAttributes.m_attributeFlags & CTFBot::FIRE_IMMUNE ) |
|
{ |
|
newBot->m_Shared.AddCond( TF_COND_FIRE_IMMUNE ); |
|
} |
|
|
|
if ( TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
// initialize currency to be dropped on death to zero |
|
newBot->SetCurrency( 0 ); |
|
|
|
// announce Spies |
|
if ( m_class == TF_CLASS_SPY ) |
|
{ |
|
CUtlVector< CTFPlayer * > playerVector; |
|
CollectPlayers( &playerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); |
|
|
|
int spyCount = 0; |
|
for( int i=0; i<playerVector.Count(); ++i ) |
|
{ |
|
if ( playerVector[i]->IsPlayerClass( TF_CLASS_SPY ) ) |
|
{ |
|
++spyCount; |
|
} |
|
} |
|
|
|
IGameEvent *event = gameeventmanager->CreateEvent( "mvm_mission_update" ); |
|
if ( event ) |
|
{ |
|
event->SetInt( "class", TF_CLASS_SPY ); |
|
event->SetInt( "count", spyCount ); |
|
gameeventmanager->FireEvent( event ); |
|
} |
|
} |
|
} |
|
|
|
newBot->SetScaleOverride( m_scale ); |
|
|
|
int nHealth = m_health; |
|
|
|
if ( nHealth <= 0.0f ) |
|
{ |
|
nHealth = newBot->GetMaxHealth(); |
|
} |
|
|
|
nHealth *= g_pPopulationManager->GetHealthMultiplier( false ); |
|
newBot->ModifyMaxHealth( nHealth ); |
|
|
|
newBot->StartIdleSound(); |
|
|
|
// Add our items first, they'll get replaced below by the normal MvM items if any are needed |
|
if ( TFGameRules()->IsMannVsMachineMode() && ( newBot->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) ) |
|
{ |
|
// Apply the Rome 2 promo items to each bot. They'll be |
|
// filtered out for clients that do not have Romevision. |
|
CMissionPopulator *pMission = dynamic_cast< CMissionPopulator* >( GetPopulator() ); |
|
if ( pMission && ( pMission->GetMissionType() == CTFBot::MISSION_DESTROY_SENTRIES ) ) |
|
{ |
|
newBot->AddItem( "tw_sentrybuster" ); |
|
} |
|
else |
|
{ |
|
newBot->AddItem( g_szRomePromoItems_Hat[m_class] ); |
|
newBot->AddItem( g_szRomePromoItems_Misc[m_class] ); |
|
} |
|
} |
|
|
|
// apply default attributes |
|
const CTFBot::EventChangeAttributes_t* pEventChangeAttributes = newBot->GetEventChangeAttributes( g_pPopulationManager->GetDefaultEventChangeAttributesName() ); |
|
if ( !pEventChangeAttributes ) |
|
{ |
|
pEventChangeAttributes = &m_defaultAttributes; |
|
} |
|
newBot->OnEventChangeAttributes( pEventChangeAttributes ); |
|
|
|
CCaptureFlag *pFlag = newBot->GetFlagToFetch(); |
|
if ( pFlag ) |
|
{ |
|
newBot->SetFlagTarget( pFlag ); |
|
} |
|
|
|
if ( newBot->HasAttribute( CTFBot::SPAWN_WITH_FULL_CHARGE ) ) |
|
{ |
|
// charge up our weapons |
|
|
|
// Medigun Ubercharge |
|
CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( newBot->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY ) ); |
|
if ( medigun ) |
|
{ |
|
medigun->AddCharge( 1.0f ); |
|
} |
|
|
|
newBot->m_Shared.SetRageMeter( 100.0f ); |
|
} |
|
|
|
int nClassIndex = ( newBot->GetPlayerClass() ? newBot->GetPlayerClass()->GetClassIndex() : TF_CLASS_UNDEFINED ); |
|
|
|
if ( GetPopulator()->GetManager()->IsPopFileEventType( MVM_EVENT_POPFILE_HALLOWEEN ) ) |
|
{ |
|
// zombies use the original player models |
|
newBot->m_nSkin = 4; |
|
|
|
const char *name = g_aRawPlayerClassNamesShort[ nClassIndex ]; |
|
|
|
newBot->AddItem( CFmtStr( "Zombie %s", name ) ); |
|
} |
|
else |
|
{ |
|
// use the nifty new robot model |
|
if ( nClassIndex >= TF_CLASS_SCOUT && nClassIndex <= TF_CLASS_ENGINEER ) |
|
{ |
|
if ( ( m_scale >= tf_mvm_miniboss_scale.GetFloat() || newBot->IsMiniBoss() ) && g_pFullFileSystem->FileExists( g_szBotBossModels[ nClassIndex ] ) ) |
|
{ |
|
newBot->GetPlayerClass()->SetCustomModel( g_szBotBossModels[ nClassIndex ], USE_CLASS_ANIMATIONS ); |
|
newBot->UpdateModel(); |
|
newBot->SetBloodColor( DONT_BLEED ); |
|
} |
|
else if ( g_pFullFileSystem->FileExists( g_szBotModels[ nClassIndex ] ) ) |
|
{ |
|
newBot->GetPlayerClass()->SetCustomModel( g_szBotModels[ nClassIndex ], USE_CLASS_ANIMATIONS ); |
|
newBot->UpdateModel(); |
|
newBot->SetBloodColor( DONT_BLEED ); |
|
} |
|
} |
|
} |
|
|
|
if ( result ) |
|
{ |
|
result->AddToTail( newBot ); |
|
} |
|
|
|
if ( TFGameRules()->IsMannVsMachineMode() ) |
|
{ |
|
if ( newBot->IsMiniBoss() ) |
|
{ |
|
TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_GIANT_CALLOUT, TF_TEAM_PVE_DEFENDERS ); |
|
} |
|
} |
|
|
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "%3.2f: Spawned TFBot '%s'\n", gpGlobals->curtime, m_name.IsEmpty() ? newBot->GetPlayerClass()->GetName() : m_name.Get() ); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTFBotSpawner: %3.2f: *** Can't create TFBot to spawn.\n", gpGlobals->curtime ); |
|
} |
|
|
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
int CTFBotSpawner::GetClass( int nSpawnNum /*= -1*/ ) |
|
{ |
|
return m_class; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
string_t CTFBotSpawner::GetClassIcon( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( m_iszClassIcon != NULL_STRING ) |
|
return m_iszClassIcon; |
|
|
|
return AllocPooledString( g_aRawPlayerClassNamesShort[ m_class ] ); |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
int CTFBotSpawner::GetHealth( int nSpawnNum /*= -1*/ ) |
|
{ |
|
return m_health; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
bool CTFBotSpawner::IsMiniBoss( int nSpawnNum /*= -1*/ ) |
|
{ |
|
return ( m_defaultAttributes.m_attributeFlags & CTFBot::MINIBOSS ) != 0; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
bool CTFBotSpawner::HasAttribute( CTFBot::AttributeType type, int nSpawnNum /*= -1*/ ) |
|
{ |
|
return ( m_defaultAttributes.m_attributeFlags & type ) != 0; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
bool CTFBotSpawner::HasEventChangeAttributes( const char* pszEventName ) const |
|
{ |
|
for ( int i=0; i<m_eventChangeAttributes.Count(); ++i ) |
|
{ |
|
if ( FStrEq( pszEventName, m_eventChangeAttributes[i].m_eventName ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
// CTankSpawner |
|
//----------------------------------------------------------------------- |
|
CTankSpawner::CTankSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) |
|
{ |
|
m_health = 50000; |
|
m_speed = 75; |
|
m_name = "Tank"; |
|
m_skin = 0; |
|
m_startingPathTrackNodeName = NULL; |
|
m_onKilledOutput = NULL; |
|
m_onBombDroppedOutput = NULL; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CTankSpawner::Parse( KeyValues *values ) |
|
{ |
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( !Q_stricmp( name, "Health" ) ) |
|
{ |
|
m_health = data->GetInt(); |
|
} |
|
else if ( !Q_stricmp( name, "Speed" ) ) |
|
{ |
|
m_speed = data->GetFloat(); |
|
} |
|
else if ( !Q_stricmp( name, "Name" ) ) |
|
{ |
|
m_name = data->GetString(); |
|
} |
|
else if ( !Q_stricmp( name, "Skin" ) ) |
|
{ |
|
m_skin = data->GetInt(); |
|
} |
|
else if ( !Q_stricmp( name, "StartingPathTrackNode" ) ) |
|
{ |
|
m_startingPathTrackNodeName = data->GetString(); |
|
} |
|
else if ( !Q_stricmp( name, "OnKilledOutput" ) ) |
|
{ |
|
m_onKilledOutput = ParseEvent( data ); |
|
} |
|
else if ( !Q_stricmp( name, "OnBombDroppedOutput" ) ) |
|
{ |
|
m_onBombDroppedOutput = ParseEvent( data ); |
|
} |
|
else |
|
{ |
|
Warning( "Invalid attribute '%s' in Tank definition\n", name ); |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CTankSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) |
|
{ |
|
CTFTankBoss *tank = (CTFTankBoss *)CreateEntityByName( "tank_boss" ); |
|
if ( tank ) |
|
{ |
|
tank->SetAbsOrigin( here ); |
|
tank->SetAbsAngles( vec3_angle ); |
|
|
|
int nHealth = m_health * g_pPopulationManager->GetHealthMultiplier( true ); |
|
tank->SetInitialHealth( nHealth ); |
|
|
|
tank->SetMaxSpeed( m_speed ); |
|
tank->SetName( MAKE_STRING( m_name ) ); |
|
tank->SetSkin( m_skin ); |
|
tank->SetStartingPathTrackNode( m_startingPathTrackNodeName.GetForModify() ); |
|
|
|
tank->Spawn(); |
|
|
|
tank->DefineOnKilledOutput( m_onKilledOutput ); |
|
tank->DefineOnBombDroppedOutput( m_onBombDroppedOutput ); |
|
|
|
if ( result ) |
|
{ |
|
result->AddToTail( tank ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CTankSpawner: %3.2f: Failed to create base_boss\n", gpGlobals->curtime ); |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
//----------------------------------------------------------------------- |
|
CSentryGunSpawner::CSentryGunSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) |
|
{ |
|
m_level = 0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CSentryGunSpawner::Parse( KeyValues *values ) |
|
{ |
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( !Q_stricmp( name, "Level" ) ) |
|
{ |
|
m_level = data->GetInt(); |
|
} |
|
else |
|
{ |
|
Warning( "Invalid attribute '%s' in SentryGun definition\n", name ); |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CSentryGunSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) |
|
{ |
|
// directly create a sentry gun at the precise position and orientation desired |
|
CObjectSentrygun *sentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" ); |
|
if ( sentry ) |
|
{ |
|
sentry->SetAbsOrigin( here ); |
|
sentry->SetAbsAngles( vec3_angle ); |
|
|
|
sentry->Spawn(); |
|
sentry->ChangeTeam( TF_TEAM_RED ); |
|
|
|
sentry->m_nDefaultUpgradeLevel = m_level+1; |
|
|
|
sentry->InitializeMapPlacedObject(); |
|
|
|
if ( result ) |
|
{ |
|
result->AddToTail( sentry ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CSentryGunSpawner: %3.2f: Failed to create obj_sentrygun\n", gpGlobals->curtime ); |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
//----------------------------------------------------------------------- |
|
CSquadSpawner::CSquadSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) |
|
{ |
|
m_memberSpawnerVector.RemoveAll(); |
|
m_formationSize = -1.0f; |
|
m_bShouldPreserveSquad = false; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
CSquadSpawner::~CSquadSpawner() |
|
{ |
|
for( int i=0; i<m_memberSpawnerVector.Count(); ++i ) |
|
{ |
|
delete m_memberSpawnerVector[i]; |
|
} |
|
m_memberSpawnerVector.RemoveAll(); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CSquadSpawner::Parse( KeyValues *values ) |
|
{ |
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( V_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( !V_stricmp( name, "FormationSize" ) ) |
|
{ |
|
m_formationSize = data->GetFloat(); |
|
continue; |
|
} |
|
else if ( !V_stricmp( name, "ShouldPreserveSquad" ) ) |
|
{ |
|
m_bShouldPreserveSquad = data->GetBool(); |
|
continue; |
|
} |
|
|
|
// NOTE: It doesn't make sense for Squads to contain SentryGun or Mobs, but this |
|
// allows for interesting trees of RandomChoice, etc. |
|
IPopulationSpawner *spawner = ParseSpawner( GetPopulator(), data ); |
|
|
|
if ( spawner == NULL ) |
|
{ |
|
Warning( "Unknown attribute '%s' in Squad definition.\n", name ); |
|
} |
|
else |
|
{ |
|
m_memberSpawnerVector.AddToTail( spawner ); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CSquadSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) |
|
{ |
|
VPROF_BUDGET( "CSquadSpawner::Spawn", "NextBot" ); |
|
|
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "CSquadSpawner: %3.2f: <<<< Spawning Squad >>>>\n", gpGlobals->curtime ); |
|
} |
|
|
|
// Is there enough slots to spawn? |
|
CTeam *deadTeam = GetGlobalTeam( TEAM_SPECTATOR ); |
|
if ( deadTeam->GetNumPlayers() < m_memberSpawnerVector.Count() ) |
|
{ |
|
return false; |
|
} |
|
|
|
// spawn all of the squad members |
|
bool isComplete = true; |
|
EntityHandleVector_t squadVector; |
|
for( int i=0; i<m_memberSpawnerVector.Count(); ++i ) |
|
{ |
|
if ( m_memberSpawnerVector[i]->Spawn( here, &squadVector ) == false ) |
|
{ |
|
isComplete = false; |
|
break; |
|
} |
|
} |
|
|
|
if ( !isComplete ) |
|
{ |
|
// unable to spawn entire squad |
|
if ( tf_populator_debug.GetBool() ) |
|
{ |
|
DevMsg( "%3.2f: CSquadSpawner: Unable to spawn entire squad\n", gpGlobals->curtime ); |
|
} |
|
|
|
// unspawn partial squad |
|
// TODO: Respect TFBot attributes |
|
for( int i=0; i<squadVector.Count(); ++i ) |
|
{ |
|
CTFPlayer *player = ToTFPlayer( squadVector[i] ); |
|
|
|
if ( player ) |
|
{ |
|
player->ChangeTeam( TEAM_SPECTATOR, false, true ); |
|
} |
|
else |
|
{ |
|
UTIL_Remove( squadVector[i] ); |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// create the squad |
|
CTFBotSquad *squad = new CTFBotSquad; |
|
if ( squad ) |
|
{ |
|
squad->SetFormationSize( m_formationSize ); |
|
squad->SetShouldPreserveSquad( m_bShouldPreserveSquad ); |
|
|
|
for( int i=0; i<squadVector.Count(); ++i ) |
|
{ |
|
CTFBot *bot = ToTFBot( squadVector[i] ); |
|
if ( bot ) |
|
{ |
|
bot->JoinSquad( squad ); |
|
} |
|
} |
|
} |
|
|
|
if ( result ) |
|
{ |
|
result->AddVectorToTail( squadVector ); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
int CSquadSpawner::GetClass( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) |
|
{ |
|
return TF_CLASS_UNDEFINED; |
|
} |
|
|
|
int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); |
|
|
|
if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) |
|
{ |
|
return m_memberSpawnerVector[ nSpawner ]->GetClass(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return TF_CLASS_UNDEFINED; |
|
} |
|
} |
|
|
|
string_t CSquadSpawner::GetClassIcon( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) |
|
{ |
|
return NULL_STRING; |
|
} |
|
|
|
int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); |
|
|
|
if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) |
|
{ |
|
return m_memberSpawnerVector[ nSpawner ]->GetClassIcon(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return NULL_STRING; |
|
} |
|
} |
|
|
|
int CSquadSpawner::GetHealth( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) |
|
{ |
|
return 0; |
|
} |
|
|
|
int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); |
|
|
|
if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) |
|
{ |
|
return m_memberSpawnerVector[ nSpawner ]->GetHealth(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return 0; |
|
} |
|
} |
|
|
|
bool CSquadSpawner::IsMiniBoss( int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) |
|
{ |
|
return false; |
|
} |
|
|
|
int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); |
|
|
|
if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) |
|
{ |
|
return m_memberSpawnerVector[ nSpawner ]->IsMiniBoss(); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return false; |
|
} |
|
} |
|
|
|
bool CSquadSpawner::HasAttribute( CTFBot::AttributeType type, int nSpawnNum /*= -1*/ ) |
|
{ |
|
if ( nSpawnNum < 0 || m_memberSpawnerVector.Count() == 0 ) |
|
{ |
|
return false; |
|
} |
|
|
|
int nSpawner = nSpawnNum % m_memberSpawnerVector.Count(); |
|
|
|
if ( !m_memberSpawnerVector[ nSpawner ]->IsVarious() ) |
|
{ |
|
return m_memberSpawnerVector[ nSpawner ]->HasAttribute( type, nSpawnNum ); |
|
} |
|
else |
|
{ |
|
// FIXME: Nested complex spawner types... need a method for counting these. |
|
Assert( 1 ); |
|
DevWarning( "Nested complex spawner types... need a method for counting these." ); |
|
return false; |
|
} |
|
} |
|
|
|
bool CSquadSpawner::HasEventChangeAttributes( const char* pszEventName ) const |
|
{ |
|
for ( int i=0; i<m_memberSpawnerVector.Count(); ++i ) |
|
{ |
|
if ( m_memberSpawnerVector[i]->HasEventChangeAttributes( pszEventName ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------- |
|
//----------------------------------------------------------------------- |
|
CMobSpawner::CMobSpawner( IPopulator *populator ) : IPopulationSpawner( populator ) |
|
{ |
|
m_count = 0; |
|
m_spawner = NULL; |
|
|
|
m_mobSpawnTimer.Invalidate(); |
|
m_mobLifetimeTimer.Invalidate(); |
|
m_mobArea = NULL; |
|
m_mobCountRemaining = 0; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
CMobSpawner::~CMobSpawner() |
|
{ |
|
if ( m_spawner ) |
|
{ |
|
delete m_spawner; |
|
} |
|
m_spawner = NULL; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CMobSpawner::Parse( KeyValues *values ) |
|
{ |
|
for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) |
|
{ |
|
const char *name = data->GetName(); |
|
|
|
if ( Q_strlen( name ) <= 0 ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( !Q_stricmp( name, "Count" ) ) |
|
{ |
|
m_count = data->GetInt(); |
|
} |
|
else |
|
{ |
|
// NOTE: It doesn't make sense for Mobs to contain SentryGuns, but this |
|
// allows for interesting trees of RandomChoice, etc. |
|
IPopulationSpawner *spawner = ParseSpawner( GetPopulator(), data ); |
|
|
|
if ( spawner && m_spawner ) |
|
{ |
|
Warning( "CMobSpawner: Duplicate spawner encountered - discarding!\n" ); |
|
delete spawner; |
|
} |
|
else if ( spawner == NULL ) |
|
{ |
|
Warning( "Unknown attribute '%s' in Mob definition.\n", name ); |
|
} |
|
else |
|
{ |
|
m_spawner = spawner; |
|
} |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CMobSpawner::Spawn( const Vector &here, EntityHandleVector_t *result ) |
|
{ |
|
if ( m_spawner == NULL ) |
|
return false; |
|
|
|
// spawn the mob |
|
for( int i=0; i<m_count; ++i ) |
|
{ |
|
if ( m_spawner->Spawn( here, result ) == false ) |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------- |
|
bool CMobSpawner::HasEventChangeAttributes( const char* pszEventName ) const |
|
{ |
|
if ( m_spawner == NULL ) |
|
return false; |
|
|
|
return m_spawner->HasEventChangeAttributes( pszEventName ); |
|
}
|
|
|