diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/player_vs_environment/tf_populators.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/tf/player_vs_environment/tf_populators.cpp')
| -rw-r--r-- | game/server/tf/player_vs_environment/tf_populators.cpp | 2324 |
1 files changed, 2324 insertions, 0 deletions
diff --git a/game/server/tf/player_vs_environment/tf_populators.cpp b/game/server/tf/player_vs_environment/tf_populators.cpp new file mode 100644 index 0000000..3e0c5e8 --- /dev/null +++ b/game/server/tf/player_vs_environment/tf_populators.cpp @@ -0,0 +1,2324 @@ +//========= 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_populators.h" +#include "tf_populator_spawners.h" +#include "tf_team.h" +#include "tf_obj_sentrygun.h" +#include "tf_objective_resource.h" +#include "eventqueue.h" +#include "tf_tank_boss.h" +#include "tf_gc_server.h" +#include "tf_gamerules.h" +#include "etwprof.h" +#include "team_control_point_master.h" + +extern ConVar tf_populator_debug; +extern ConVar tf_populator_active_buffer_range; + +ConVar tf_mvm_engineer_teleporter_uber_duration( "tf_mvm_engineer_teleporter_uber_duration", "5.f", FCVAR_CHEAT ); +ConVar tf_mvm_currency_bonus_ratio_min( "tf_mvm_currency_bonus_ratio_min", "0.95f", FCVAR_HIDDEN, "The minimum percentage of wave money players must collect in order to qualify for min bonus - 0.1 to 1.0. Half the bonus amount will be awarded when reaching min ratio, and half when reaching max.", true, 0.1, true, 1.0 ); +ConVar tf_mvm_currency_bonus_ratio_max( "tf_mvm_currency_bonus_ratio_max", "1.f", FCVAR_HIDDEN, "The highest percentage of wave money players must collect in order to qualify for max bonus - 0.1 to 1.0. Half the bonus amount will be awarded when reaching min ratio, and half when reaching max.", true, 0.1, true, 1.0 ); + +//----------------------------------------------------------------------- +static void FireEvent( EventInfo *eventInfo, const char *eventName ) +{ + if ( eventInfo ) + { + CBaseEntity *targetEntity = gEntList.FindEntityByName( NULL, eventInfo->m_target ); + if ( !targetEntity ) + { + Warning( "WaveSpawnPopulator: Can't find target entity '%s' for %s\n", eventInfo->m_target.Access(), eventName ); + } + else + { + g_EventQueue.AddEvent( targetEntity, eventInfo->m_action, 0.0f, NULL, 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; +} + +static CHandle<CBaseEntity> s_lastTeleporter = NULL; +static float s_flLastTeleportTime = -1; + +//----------------------------------------------------------------------- +// Given a named entity, select a random invader teleporter with the same name and +// return it's WorldSpaceCenter. +SpawnLocationResult DoTeleporterOverride( CBaseEntity *spawnEnt, Vector& vSpawnPosition, bool bClosestPointOnNav ) +{ + CUtlVector< CBaseEntity * > teleporterVector; + + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject *pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->GetType() != OBJ_TELEPORTER ) + continue; + + if ( pObj->GetTeamNumber() != TF_TEAM_PVE_INVADERS ) + continue; + + if ( pObj->IsBuilding() ) + continue; + + if ( pObj->HasSapper() ) + continue; + + if ( pObj->IsPlasmaDisabled() ) + continue; + + CObjectTeleporter *teleporter = assert_cast< CObjectTeleporter* >( pObj ); + const CUtlStringList& teleportWhereNames = teleporter->GetTeleportWhere(); + + const char* pszSpawnPointName = STRING( spawnEnt->GetEntityName() ); + for ( int iTelePoints =0; iTelePoints<teleportWhereNames.Count(); ++iTelePoints ) + { + // check if this teleporter can replace the original spawn point + if ( FStrEq( teleportWhereNames[iTelePoints], pszSpawnPointName ) ) + { + teleporterVector.AddToTail( teleporter ); + break; + } + } + } + + if ( teleporterVector.Count() > 0 ) + { + int which = RandomInt( 0, teleporterVector.Count()-1 ); + vSpawnPosition = teleporterVector[ which ]->WorldSpaceCenter(); + s_lastTeleporter = teleporterVector[ which ]; + return SPAWN_LOCATION_TELEPORTER; + } + + CTFNavArea *pNav = (CTFNavArea *)TheNavMesh->GetNearestNavArea( spawnEnt->WorldSpaceCenter() ); + if ( !pNav ) + return SPAWN_LOCATION_NOT_FOUND; + + if ( bClosestPointOnNav ) + { + pNav->GetClosestPointOnArea( spawnEnt->WorldSpaceCenter(), &vSpawnPosition ); + } + else + { + vSpawnPosition = pNav->GetCenter(); + } + + return SPAWN_LOCATION_NAV; +} + +//----------------------------------------------------------------------- +void OnBotTeleported( CTFBot* bot ) +{ + const Vector& origin = s_lastTeleporter->GetAbsOrigin(); + + // don't too many sound and effect when lots of bots teleporting in short time. + if ( gpGlobals->curtime - s_flLastTeleportTime > 0.1f ) + { + CPVSFilter filter( origin ); +#if 0 + // These are pretty, but they're chewing into our particle budget (1000 particles each!) + // They're also basically invisible because bots spawn in ubered. + + TE_TFParticleEffect( filter, 0.0, "teleported_blue", origin, vec3_angle ); + TE_TFParticleEffect( filter, 0.0, "player_sparkles_blue", origin, vec3_angle ); +#endif + s_lastTeleporter->EmitSound( "MVM.Robot_Teleporter_Deliver" ); + + s_flLastTeleportTime = gpGlobals->curtime; + } + + // force bot to face in the direction specified by the teleporter + Vector vForward; + AngleVectors( s_lastTeleporter->GetAbsAngles(), &vForward, NULL, NULL ); + bot->GetLocomotionInterface()->FaceTowards( bot->GetAbsOrigin() + 50 * vForward ); + + // spy shouldn't get any effect from the teleporter + if ( !bot->IsPlayerClass( TF_CLASS_SPY ) ) + { + bot->TeleportEffect(); + + // invading bots get uber while they leave their spawn so they don't drop their cash where players can't pick it up + float flUberTime = tf_mvm_engineer_teleporter_uber_duration.GetFloat(); + bot->m_Shared.AddCond( TF_COND_INVULNERABLE, flUberTime ); + bot->m_Shared.AddCond( TF_COND_INVULNERABLE_WEARINGOFF, flUberTime ); + } +} + +//----------------------------------------------------------------------- +// CSpawnLocation +//----------------------------------------------------------------------- +CSpawnLocation::CSpawnLocation() +{ + m_relative = UNDEFINED; + m_teamSpawnVector.RemoveAll(); + m_nSpawnCount = 0; + m_nRandomSeed = RandomInt( 0, 9999 ); + m_bClosestPointOnNav = false; +} + +//----------------------------------------------------------------------- +// Return true if we successfully parse a "Where" clause +bool CSpawnLocation::Parse( KeyValues *data ) +{ + const char *name = data->GetName(); + const char *value = data->GetString(); + + if ( Q_strlen( name ) <= 0 ) + { + return false; + } + + if ( FStrEq( name, "Where" ) || FStrEq( name, "ClosestPoint" ) ) + { + if ( FStrEq( value, "Ahead" ) ) + { + m_relative = AHEAD; + } + else if ( FStrEq( value, "Behind" ) ) + { + m_relative = BEHIND; + } + else if ( FStrEq( value, "Anywhere" ) ) + { + m_relative = ANYWHERE; + } + else + { + m_bClosestPointOnNav = FStrEq( name, "ClosestPoint" ); + + // collect entities with given name + bool bFound = false; + for ( int i=0; i<ITFTeamSpawnAutoList::AutoList().Count(); ++i ) + { + CTFTeamSpawn* pTeamSpawn = static_cast< CTFTeamSpawn* >( ITFTeamSpawnAutoList::AutoList()[i] ); + if ( FStrEq( STRING( pTeamSpawn->GetEntityName() ), value ) ) + { + m_teamSpawnVector.AddToTail( pTeamSpawn ); + bFound = true; + } + } + + if ( !bFound ) + { + Warning( "Invalid Where argument '%s'\n", value ); + return false; + } + } + + return true; + } + + return false; +} + +//----------------------------------------------------------------------- +SpawnLocationResult CSpawnLocation::FindSpawnLocation( Vector& vSpawnPosition ) +{ + TFTeamSpawnVector_t activeSpawn; + for ( int i=0; i<m_teamSpawnVector.Count(); ++i ) + { + if ( m_teamSpawnVector[i]->IsDisabled() ) + continue; + + activeSpawn.AddToTail( m_teamSpawnVector[i] ); + } + + // treat spawn points as deck of cards. shuffle it when we run out + if ( m_nSpawnCount >= activeSpawn.Count() ) + { + m_nRandomSeed = RandomInt( 0, 9999 ); + m_nSpawnCount = 0; + } + CUniformRandomStream randomSpawn; + randomSpawn.SetSeed( m_nRandomSeed ); + activeSpawn.Shuffle( &randomSpawn ); + + if ( activeSpawn.Count() > 0 ) + { + // if any invading teleporters exist with this name, use them instead + SpawnLocationResult result = DoTeleporterOverride( activeSpawn[ m_nSpawnCount ], vSpawnPosition, m_bClosestPointOnNav ); + if ( result != SPAWN_LOCATION_NOT_FOUND ) + { + m_nSpawnCount++; + return result; + } + } + + CTFNavArea *spawnArea = SelectSpawnArea(); + if ( spawnArea ) + { + vSpawnPosition = spawnArea->GetCenter(); + return SPAWN_LOCATION_NAV; + } + + return SPAWN_LOCATION_NOT_FOUND; +} + +//----------------------------------------------------------------------- +CTFNavArea *CSpawnLocation::SelectSpawnArea( void ) const +{ + VPROF_BUDGET( "CSpawnLocation::SelectSpawnArea", "NextBot" ); + + if ( m_relative == UNDEFINED ) + { + return NULL; + } + +#ifdef TF_RAID_MODE + CTFPlayer *farRaider = g_pRaidLogic->GetFarthestAlongRaider(); + + if ( !farRaider ) + { + return NULL; + } +#endif // TF_RAID_MODE + + // + // Collect all areas surrounding the invading team and + // build a vector sorted by increasing incursion distance + // + CUtlSortVector< CTFNavArea *, CTFNavAreaIncursionLess > theaterAreaVector; + + CTFNavArea::MakeNewTFMarker(); + + CTeam *team = GetGlobalTeam( TF_TEAM_BLUE ); + for( int t=0; t<team->GetNumPlayers(); ++t ) + { + CTFPlayer *teamMember = (CTFPlayer *)team->GetPlayer(t); + + if ( !teamMember->IsAlive() ) + continue; + + CTFBot *bot = ToTFBot( teamMember ); + if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) + continue; + + if ( teamMember->GetLastKnownArea() == NULL ) + continue; + + // collect areas surrounding this invader + CUtlVector< CNavArea * > nearbyAreaVector; + CollectSurroundingAreas( &nearbyAreaVector, teamMember->GetLastKnownArea(), tf_populator_active_buffer_range.GetFloat() ); + + for( int i=0; i<nearbyAreaVector.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)nearbyAreaVector[i]; + + if ( !area->IsTFMarked() ) + { + area->TFMark(); + + if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) + continue; + + if ( !area->IsValidForWanderingPopulation() ) + continue; + + theaterAreaVector.Insert( area ); + + if ( tf_populator_debug.GetBool() ) + { + TheTFNavMesh()->AddToSelectedSet( area ); + } + } + } + } + + if ( theaterAreaVector.Count() == 0 ) + { + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: SelectSpawnArea: Empty theater!\n", gpGlobals->curtime ); + } + return NULL; + } + + const int maxRetries = 5; + CTFNavArea *spawnArea = NULL; + + for( int r=0; r<maxRetries; ++r ) + { + int which = 0; + + switch( m_relative ) + { + case AHEAD: + // areas are sorted from behind to ahead - weight the selection to choose ahead + which = SkewedRandomValue() * theaterAreaVector.Count(); + break; + + case BEHIND: + // areas are sorted from behind to ahead - weight the selection to choose behind + which = ( 1.0f - SkewedRandomValue() ) * theaterAreaVector.Count(); + break; + + case ANYWHERE: + // choose any valid area at random + which = RandomFloat( 0.0f, 1.0f ) * theaterAreaVector.Count(); + break; + } + + if ( which >= theaterAreaVector.Count() ) + which = theaterAreaVector.Count()-1; + + spawnArea = theaterAreaVector[ which ]; + + // well behaved spawn area + return spawnArea; + + } + + return NULL; +} + +//----------------------------------------------------------------------- +// CMissionPopulator +//----------------------------------------------------------------------- +CMissionPopulator::CMissionPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_mission = CTFBot::NO_MISSION; + m_initialCooldown = 0.0f; + m_cooldownDuration = 0.0f; + m_desiredCount = 0; + m_beginAtWaveIndex = 0; + m_stopAtWaveIndex = 99999; + m_state = NOT_STARTED; +} + + +//----------------------------------------------------------------------- +bool CMissionPopulator::Parse( KeyValues *values ) +{ + int waveDuration = 99999; + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( m_where.Parse( data ) ) + { + continue; + } + + if ( !Q_stricmp( name, "Objective" ) ) + { + if ( !Q_stricmp( data->GetString(), "DestroySentries" ) ) + { + m_mission = CTFBot::MISSION_DESTROY_SENTRIES; + } + else if ( !Q_stricmp( data->GetString(), "Sniper" ) ) + { + m_mission = CTFBot::MISSION_SNIPER; + } + else if ( !Q_stricmp( data->GetString(), "Spy" ) ) + { + m_mission = CTFBot::MISSION_SPY; + } + else if ( !Q_stricmp( data->GetString(), "Engineer" ) ) + { + m_mission = CTFBot::MISSION_ENGINEER; + } + else if ( !Q_stricmp( data->GetString(), "SeekAndDestroy" ) ) + { + m_mission = CTFBot::MISSION_DESTROY_SENTRIES; + } + else + { + Warning( "Invalid mission '%s'\n", data->GetString() ); + return false; + } + } + else if ( !Q_stricmp( name, "InitialCooldown" ) ) + { + m_initialCooldown = data->GetFloat(); + } + else if ( !Q_stricmp( name, "CooldownTime" ) ) + { + m_cooldownDuration = data->GetFloat(); + } + else if ( !Q_stricmp( name, "BeginAtWave" ) ) + { + m_beginAtWaveIndex = data->GetInt() - 1; // internally counts from 0 + } + else if ( !Q_stricmp( name, "RunForThisManyWaves" ) ) + { + waveDuration = data->GetInt(); + } + else if ( !Q_stricmp( name, "DesiredCount" ) ) + { + m_desiredCount = data->GetInt(); + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in Mission definition.\n", name ); + } + } + } + + m_stopAtWaveIndex = m_beginAtWaveIndex + waveDuration; + + return true; +} + + +//-------------------------------------------------------------------------------------------------------- +// Dispatch sentry killer squads +bool CMissionPopulator::UpdateMissionDestroySentries( void ) +{ + VPROF_BUDGET( "CMissionPopulator::UpdateMissionDestroySentries", "NextBot" ); + + if ( !m_cooldownTimer.IsElapsed() ) + { + return false; + } + + if ( !m_checkForDangerousSentriesTimer.IsElapsed() ) + { + return false; + } + + if( g_pPopulationManager->IsSpawningPaused() ) + { + return false; + } + + m_checkForDangerousSentriesTimer.Start( RandomFloat( 5.0f, 10.0f ) ); + + // collect all of the dangerous sentries + CUtlVector< CObjectSentrygun * > dangerousSentryVector; + + int nDmgLimit = 0; + int nKillLimit = 0; + GetManager()->GetSentryBusterDamageAndKillThreshold( nDmgLimit, nKillLimit ); + + for ( int i=0; i<IBaseObjectAutoList::AutoList().Count(); ++i ) + { + CBaseObject* pObj = static_cast< CBaseObject* >( IBaseObjectAutoList::AutoList()[i] ); + if ( pObj->ObjectType() == OBJ_SENTRYGUN ) + { + // Disposable sentries are not valid targets + if ( pObj->IsDisposableBuilding() ) + continue; + + if ( pObj->GetTeamNumber() == TF_TEAM_PVE_DEFENDERS ) + { + CTFPlayer *sentryOwner = pObj->GetOwner(); + if ( sentryOwner ) + { + int nDmgDone = sentryOwner->GetAccumulatedSentryGunDamageDealt(); + int nKillsMade = sentryOwner->GetAccumulatedSentryGunKillCount(); + + if ( nDmgDone >= nDmgLimit || nKillsMade >= nKillLimit ) + { + dangerousSentryVector.AddToTail( static_cast< CObjectSentrygun* >( pObj ) ); + } + } + } + } + } + + CUtlVector< CTFPlayer * > livePlayerVector; + CollectPlayers( &livePlayerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + // dispatch a sentry busting squad for each dangerous sentry + bool didSpawn = false; + + for( int i=0; i<dangerousSentryVector.Count(); ++i ) + { + CObjectSentrygun *targetSentry = dangerousSentryVector[i]; + + // if there is already a squad out there destroying this sentry, don't spawn another one + int j; + for( j=0; j<livePlayerVector.Count(); ++j ) + { + CTFBot *bot = dynamic_cast<CTFBot *>( livePlayerVector[j] ); + if ( bot && bot->HasMission( CTFBot::MISSION_DESTROY_SENTRIES ) && bot->GetMissionTarget() == targetSentry ) + { + // there is already a sentry busting squad active for this sentry + break; + } + } + + if ( j < livePlayerVector.Count() ) + { + continue; + } + + // spawn a sentry buster squad to destroy this sentry + Vector vSpawnPosition; + SpawnLocationResult spawnLocationResult = m_where.FindSpawnLocation( vSpawnPosition ); + if ( spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + EntityHandleVector_t spawnVector; + + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &spawnVector ) ) + { + // success + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "MANN VS MACHINE: %3.2f: <<<< Spawning Sentry Busting Mission >>>>\n", gpGlobals->curtime ); + } + + for( int k=0; k<spawnVector.Count(); ++k ) + { + CTFBot *bot = ToTFBot( spawnVector[k] ); + if ( bot ) + { + bot->SetFlagTarget( NULL ); + bot->SetMission( CTFBot::MISSION_DESTROY_SENTRIES ); + bot->SetMissionTarget( targetSentry ); + + // force an update to start the behavior so we can set the sentry + bot->Update(); + + bot->MarkAsMissionEnemy(); + + didSpawn = true; + + bot->GetPlayerClass()->SetCustomModel( g_szBotBossSentryBusterModel, USE_CLASS_ANIMATIONS ); + bot->UpdateModel(); + bot->SetBloodColor( DONT_BLEED ); + + if ( TFObjectiveResource() ) + { + unsigned int iFlags = MVM_CLASS_FLAG_MISSION; + if ( bot->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( bot->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + TFObjectiveResource()->IncrementMannVsMachineWaveClassCount( m_spawner->GetClassIcon( k ), iFlags ); + } + + if ( TFGameRules() ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SENTRY_BUSTER, TF_TEAM_PVE_DEFENDERS ); + } + + // what bot should do after spawning at teleporter exit + if ( spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + OnBotTeleported( bot ); + } + } + } + } + } + else if ( tf_populator_debug.GetBool() ) + { + Warning( "MissionPopulator: %3.2f: Can't find a place to spawn a sentry destroying squad\n", gpGlobals->curtime ); + } + } + + if ( didSpawn ) + { + float flCoolDown = m_cooldownDuration; + + CWave *pWave = GetManager()->GetCurrentWave(); + if ( pWave ) + { + pWave->IncrementSentryBustersSpawned(); + + if ( TFGameRules() ) + { + if ( pWave->NumSentryBustersSpawned() > 1 ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Sentry_Buster_Alert_Another" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Sentry_Buster_Alert" ); + } + } + + flCoolDown = m_cooldownDuration + pWave->NumSentryBustersKilled() * m_cooldownDuration; + + pWave->ResetSentryBustersKilled(); + } + + m_cooldownTimer.Start( flCoolDown ); + } + + return didSpawn; +} + + +//----------------------------------------------------------------------- +bool CMissionPopulator::UpdateMission( CTFBot::MissionType mission ) +{ + VPROF_BUDGET( "CMissionPopulator::UpdateMission", "NextBot" ); + + int activeMissionMembers = 0; + + CUtlVector< CTFPlayer * > livePlayerVector; + CollectPlayers( &livePlayerVector, TF_TEAM_PVE_INVADERS, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<livePlayerVector.Count(); ++i ) + { + CTFBot *pBot = dynamic_cast<CTFBot *>( livePlayerVector[i] ); + if ( pBot && pBot ->HasMission( mission ) ) + { + ++activeMissionMembers; + } + } + + if( g_pPopulationManager->IsSpawningPaused() ) + { + return false; + } + + if ( activeMissionMembers > 0 ) + { + // wait until prior mission is dead + + // cooldown is time after death of last mission member + m_cooldownTimer.Start( m_cooldownDuration ); + + return false; + } + + if ( !m_cooldownTimer.IsElapsed() ) + { + return false; + } + + // are there enough free slots? + int currentEnemyCount = GetGlobalTeam( TF_TEAM_PVE_INVADERS )->GetNumPlayers(); + + if ( currentEnemyCount + m_desiredCount > CPopulationManager::MVM_INVADERS_TEAM_SIZE ) + { + // not enough slots yet + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "MANN VS MACHINE: %3.2f: Waiting for slots to spawn mission.\n", gpGlobals->curtime ); + } + + return false; + } + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "MANN VS MACHINE: %3.2f: <<<< Spawning Mission >>>>\n", gpGlobals->curtime ); + } + + int nSniperCount = 0; + FOR_EACH_VEC( livePlayerVector, iLiveBot ) + { + CTFBot *pBot = dynamic_cast<CTFBot *>( livePlayerVector[iLiveBot] ); + if ( pBot && pBot->IsPlayerClass( TF_CLASS_SNIPER ) ) + { + nSniperCount++; + } + } + + // dispatch mission members + for( int iDesiredCount=0; iDesiredCount<m_desiredCount; ++iDesiredCount ) + { + Vector vSpawnPosition; + SpawnLocationResult spawnLocationResult = m_where.FindSpawnLocation( vSpawnPosition ); + if ( spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + EntityHandleVector_t spawnedVector; + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &spawnedVector ) ) + { + // success + for( int iSpawn=0; iSpawn<spawnedVector.Count(); ++iSpawn ) + { + CTFBot *bot = ToTFBot( spawnedVector[iSpawn] ); + if ( bot ) + { + bot->SetFlagTarget( NULL ); + bot->SetMission( mission ); + //bot->SetMissionString( "" ); + bot->MarkAsMissionEnemy(); + + if ( TFObjectiveResource() ) + { + unsigned int iFlags = MVM_CLASS_FLAG_MISSION; + if ( bot->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( bot->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + TFObjectiveResource()->IncrementMannVsMachineWaveClassCount( bot->GetPlayerClass()->GetClassIconName(), iFlags ); + } + + // Response rules stuff for MvM + if ( TFGameRules()->IsMannVsMachineMode() ) + { + // Only have defenders announce the arrival of the first enemy Sniper + if ( bot->HasMission( CTFBot::MISSION_SNIPER ) ) + { + nSniperCount++; + + if ( nSniperCount == 1 ) + { + TFGameRules()->HaveAllPlayersSpeakConceptIfAllowed( MP_CONCEPT_MVM_SNIPER_CALLOUT, TF_TEAM_PVE_DEFENDERS ); + } + } + } + + // what bot should do after spawning at teleporter exit + if ( spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + OnBotTeleported( bot ); + } + } + } + } + } + else + { + if ( tf_populator_debug.GetBool() ) + { + Warning( "MissionPopulator: %3.2f: Skipped a member - can't find a place to spawn\n", gpGlobals->curtime ); + } + } + } + + m_cooldownTimer.Start( m_cooldownDuration ); + + return true; +} + + +//----------------------------------------------------------------------- +void CMissionPopulator::Update( void ) +{ + VPROF_BUDGET( "CMissionPopulator::Update", "NextBot" ); + + if ( TFGameRules()->InSetup() || + GetManager()->GetWaveNumber() < m_beginAtWaveIndex || + GetManager()->GetWaveNumber() >= m_stopAtWaveIndex || + TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() ) + { + m_state = NOT_STARTED; + return; + } + + if ( m_state == NOT_STARTED ) + { + if ( m_initialCooldown > 0.0f ) + { + m_state = INITIAL_COOLDOWN; + m_cooldownTimer.Start( m_initialCooldown ); + return; + } + + m_state = RUNNING; + m_cooldownTimer.Invalidate(); + } + else if ( m_state == INITIAL_COOLDOWN ) + { + if ( !m_cooldownTimer.IsElapsed() ) + { + return; + } + + m_state = RUNNING; + m_cooldownTimer.Invalidate(); + } + + switch( m_mission ) + { + case CTFBot::MISSION_SEEK_AND_DESTROY: + break; + + case CTFBot::MISSION_DESTROY_SENTRIES: + UpdateMissionDestroySentries(); + break; + + case CTFBot::MISSION_SNIPER: + case CTFBot::MISSION_SPY: + case CTFBot::MISSION_ENGINEER: + UpdateMission( m_mission ); + break; + } +} + + +void CMissionPopulator::UnpauseSpawning( void ) +{ + m_cooldownTimer.Start( m_cooldownDuration ); + m_checkForDangerousSentriesTimer.Start( RandomFloat( 5.0f, 10.0f ) ); +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CRandomPlacementPopulator::CRandomPlacementPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_count = 0; + m_minSeparation = 0.0f; + m_navAreaFilter = 0xFFFFFFFF; +} + + +//----------------------------------------------------------------------- +bool CRandomPlacementPopulator::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 if ( !Q_stricmp( name, "MinimumSeparation" ) ) + { + m_minSeparation = data->GetFloat(); + } + else if ( !Q_stricmp( name, "NavAreaFilter" ) ) + { + if ( !Q_stricmp( data->GetString(), "SENTRY_SPOT" ) ) + { + m_navAreaFilter = TF_NAV_SENTRY_SPOT; + } + else if ( !Q_stricmp( data->GetString(), "SNIPER_SPOT" ) ) + { + m_navAreaFilter = TF_NAV_SNIPER_SPOT; + } + else + { + Warning( "Unknown NavAreaFilter value '%s'\n", data->GetString() ); + } + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in RandomPlacement definition.\n", name ); + } + } + } + + return true; +} + + +//----------------------------------------------------------------------- +// Create initial population at start of scenario +void CRandomPlacementPopulator::PostInitialize( void ) +{ + int i; + CUtlVector< CTFNavArea * > candidateAreaVector; + + for( i=0; i<TheNavAreas.Count(); ++i ) + { + CTFNavArea *area = (CTFNavArea *)TheNavAreas[i]; + + if ( area->HasAttributeTF( m_navAreaFilter ) ) + { + candidateAreaVector.AddToTail( area ); + } + } + + CUtlVector< CTFNavArea * > selectedAreaVector; + SelectSeparatedShuffleSet< CTFNavArea >( m_count, m_minSeparation, candidateAreaVector, &selectedAreaVector ); + + if ( m_spawner ) + { + for( i=0; i<selectedAreaVector.Count(); ++i ) + { + m_spawner->Spawn( selectedAreaVector[i]->GetCenter() ); + } + } +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- +CPeriodicSpawnPopulator::CPeriodicSpawnPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_minInterval = 30.0f; + m_maxInterval = 30.0f; +} + + +//----------------------------------------------------------------------- +bool CPeriodicSpawnPopulator::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 ( m_where.Parse( data ) ) + { + continue; + } + + if ( !Q_stricmp( name, "When" ) ) + { + if ( data->GetFirstSubKey() ) + { + for ( KeyValues *whenData = data->GetFirstSubKey(); whenData != NULL; whenData = whenData->GetNextKey() ) + { + if ( !Q_stricmp( whenData->GetName(), "MinInterval" ) ) + { + m_minInterval = whenData->GetFloat(); + } + else if ( !Q_stricmp( whenData->GetName(), "MaxInterval" ) ) + { + m_maxInterval = whenData->GetFloat(); + } + else + { + Warning( "Invalid field '%s' encountered in When\n", whenData->GetName() ); + return false; + } + } + } + else + { + // single constant value + m_minInterval = data->GetFloat(); + m_maxInterval = m_minInterval; + } + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in PeriodicSpawn definition.\n", name ); + } + } + } + + return true; +} + + +//----------------------------------------------------------------------- +// Create initial population at start of scenario +void CPeriodicSpawnPopulator::PostInitialize( void ) +{ + m_timer.Start( RandomFloat( m_minInterval, m_maxInterval ) ); +} + + + +//----------------------------------------------------------------------- +// Continuously invoked to modify population over time +void CPeriodicSpawnPopulator::Update( void ) +{ + if ( m_timer.IsElapsed() && !g_pPopulationManager->IsSpawningPaused() ) + { + m_timer.Start( RandomFloat( m_minInterval, m_maxInterval ) ); + + Vector vSpawnPosition; + SpawnLocationResult spawnLocationResult = m_where.FindSpawnLocation( vSpawnPosition ); + if ( spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + EntityHandleVector_t spawnedVector; + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &spawnedVector ) ) + { + // success + for( int i=0; i<spawnedVector.Count(); ++i ) + { + CTFBot *bot = ToTFBot( spawnedVector[i] ); + if ( bot ) + { + // what bot should do after spawning at teleporter exit + if ( spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + OnBotTeleported( bot ); + } + } + } + + return; + } + } + + // retry soon, in the hopes constraints have changed + m_timer.Start( 2.0f ); + } +} + +void CPeriodicSpawnPopulator::UnpauseSpawning( void ) +{ + m_timer.Start( RandomFloat( m_minInterval, m_maxInterval ) ); +} + + +//----------------------------------------------------------------------- +//----------------------------------------------------------------------- + +int CWaveSpawnPopulator::m_reservedPlayerSlotCount = 0; + +CWaveSpawnPopulator::CWaveSpawnPopulator( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_totalCount = 0; + m_maxActive = 999; + m_spawnCount = 1; + m_waitBeforeStarting = 0.0f; + m_waitBetweenSpawns = 0.0f; + m_bWaitBetweenSpawnAfterDeath = false; + m_totalCurrency = -1; + SetState( PENDING ); + + m_startWaveOutput = NULL; + m_firstSpawnOutput = NULL; + m_lastSpawnOutput = NULL; + m_doneOutput = NULL; + + m_bSupportWave = false; + m_bLimitedSupport = false; + m_pParent = NULL; + + m_bRandomSpawn = false; + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; +} + +//----------------------------------------------------------------------- +CWaveSpawnPopulator::~CWaveSpawnPopulator() +{ + delete m_startWaveOutput; + delete m_firstSpawnOutput; + delete m_lastSpawnOutput; + delete m_doneOutput; +} + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::ForceFinish() +{ + if ( m_state < WAIT_FOR_ALL_DEAD ) + { + SetState( WAIT_FOR_ALL_DEAD ); + } + else if ( m_state != WAIT_FOR_ALL_DEAD ) + { + SetState( DONE ); + } + + FOR_EACH_VEC( m_activeVector, i ) + { + // Move bots over to spectator + CTFBot *pBot = ToTFBot( m_activeVector[i] ); + if ( pBot ) + { + pBot->ChangeTeam( TEAM_SPECTATOR, false, true ); + } + else // Other things just get removed. (ie. Tanks) + { + m_activeVector[i]->Remove(); + } + } + + m_activeVector.Purge(); +} + + +//----------------------------------------------------------------------- +bool CWaveSpawnPopulator::Parse( KeyValues *values ) +{ + // First, see if we have any Template keys + KeyValues *pTemplate = values->FindKey( "Template" ); + if ( pTemplate ) + { + KeyValues *pTemplateKV = 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 WaveSpawn definition\n", pTemplate->GetString() ); + } + } + + for ( KeyValues *data = values->GetFirstSubKey(); data != NULL; data = data->GetNextKey() ) + { + const char *name = data->GetName(); + + if ( Q_strlen( name ) <= 0 ) + { + continue; + } + + if ( m_where.Parse( data ) ) + { + continue; + } + + // Skip templates when looping through the rest of the keys + if ( !Q_stricmp( name, "Template" ) ) + continue; + + if ( !Q_stricmp( name, "TotalCount" ) ) + { + m_totalCount = data->GetInt(); + } + else if ( !Q_stricmp( name, "MaxActive" ) ) + { + m_maxActive = data->GetInt(); + } + else if ( !Q_stricmp( name, "SpawnCount" ) ) + { + m_spawnCount = data->GetInt(); + } + else if ( !Q_stricmp( name, "WaitBeforeStarting" ) ) + { + m_waitBeforeStarting = data->GetFloat(); + } + else if ( !Q_stricmp( name, "WaitBetweenSpawns" ) ) + { + if ( m_waitBetweenSpawns != 0.f && m_bWaitBetweenSpawnAfterDeath ) + { + Warning( "Already specified WaitBetweenSpawnsAfterDeath time, WaitBetweenSpawns won't be used\n" ); + continue; + } + + m_waitBetweenSpawns = data->GetFloat(); + } + else if ( !Q_stricmp( name, "WaitBetweenSpawnsAfterDeath" ) ) + { + if ( m_waitBetweenSpawns != 0.f ) + { + Warning( "Already specified WaitBetweenSpawns time, WaitBetweenSpawnsAfterDeath won't be used\n" ); + continue; + } + + m_bWaitBetweenSpawnAfterDeath = true; + m_waitBetweenSpawns = data->GetFloat(); + } + else if ( !Q_stricmp( name, "StartWaveWarningSound" ) ) + { + m_startWaveWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "StartWaveOutput" ) ) + { + m_startWaveOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "FirstSpawnWarningSound" ) ) + { + m_firstSpawnWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "FirstSpawnOutput" ) ) + { + m_firstSpawnOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "LastSpawnWarningSound" ) ) + { + m_lastSpawnWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "LastSpawnOutput" ) ) + { + m_lastSpawnOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "DoneWarningSound" ) ) + { + m_doneWarningSound.sprintf( "%s", data->GetString() ); + } + else if ( !Q_stricmp( name, "DoneOutput" ) ) + { + m_doneOutput = ParseEvent( data ); + } + else if ( !Q_stricmp( name, "TotalCurrency" ) ) + { + m_totalCurrency = data->GetInt(); + } + else if ( !Q_stricmp( name, "Name" ) ) + { + m_name = data->GetString(); + } + else if ( !Q_stricmp( name, "WaitForAllSpawned" ) ) + { + m_waitForAllSpawned = data->GetString(); + } + else if ( !Q_stricmp( name, "WaitForAllDead" ) ) + { + m_waitForAllDead = data->GetString(); + } + else if ( !Q_stricmp( name, "Support" ) ) + { + m_bLimitedSupport = !Q_stricmp( data->GetString(), "Limited" ); + m_bSupportWave = true; + } + else if ( !Q_stricmp( name, "RandomSpawn" ) ) + { + m_bRandomSpawn = data->GetBool(); + } + else + { + m_spawner = IPopulationSpawner::ParseSpawner( this, data ); + + if ( m_spawner == NULL ) + { + Warning( "Unknown attribute '%s' in WaveSpawn definition.\n", name ); + } + } + + // These allow us to avoid rounding errors later when divvying money to bots + m_unallocatedCurrency = m_totalCurrency; + m_remainingCount = m_totalCount; + } + + return true; +} + + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::OnPlayerKilled( CTFPlayer *corpse ) +{ + m_activeVector.FindAndFastRemove( corpse ); +} + + +//----------------------------------------------------------------------- +bool CWaveSpawnPopulator::IsFinishedSpawning( void ) +{ + if ( m_bSupportWave && !m_bLimitedSupport ) + { + // support waves are never done spawning until + // we get OnNonSupportWavesDone called + return false; + } + + return ( m_countSpawnedSoFar >= m_totalCount ); +} + + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::SetState( InternalStateType eState ) +{ + m_state = eState; + + switch( m_state ) + { + case PENDING: + case PRE_SPAWN_DELAY: + case SPAWNING: + break; + case WAIT_FOR_ALL_DEAD: + // last spawn has occurred + if ( m_lastSpawnWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_lastSpawnWarningSound ); + } + + FireEvent( m_lastSpawnOutput, "LastSpawnOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) started WAIT_FOR_ALL_DEAD\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + break; + case DONE: + if ( m_doneWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_doneWarningSound ); + } + + FireEvent( m_doneOutput, "DoneOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) DONE\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + break; + } +} + + +//----------------------------------------------------------------------- +void CWaveSpawnPopulator::OnNonSupportWavesDone( void ) +{ + if ( m_bSupportWave ) + { + switch( m_state ) + { + case PENDING: + case PRE_SPAWN_DELAY: + SetState( DONE ); + break; + case SPAWNING: + case WAIT_FOR_ALL_DEAD: + if ( TFGameRules() && ( m_unallocatedCurrency > 0 ) ) + { + TFGameRules()->DistributeCurrencyAmount( m_unallocatedCurrency, NULL, true, true ); + m_unallocatedCurrency = 0; + } + SetState( WAIT_FOR_ALL_DEAD ); + case DONE: + break; + } + } +} + + +//----------------------------------------------------------------------- +int CWaveSpawnPopulator::GetCurrencyAmountPerDeath( void ) +{ + int nCurrency = 0; + + if ( m_bSupportWave ) + { + if ( m_state == WAIT_FOR_ALL_DEAD ) + { + // we're still in the m_ActiveVector at this point so the number of players + // in the vector is the division of the money since we're done spawning + m_remainingCount = m_activeVector.Count(); + } + } + + if ( m_unallocatedCurrency > 0 ) + { + // We shouldn't be back in here if our remaining count is 0 + Assert ( m_remainingCount > 0 ); + + // Band-aid for playtest + m_remainingCount = m_remainingCount <= 0 ? 1 : m_remainingCount; + + nCurrency = m_unallocatedCurrency / m_remainingCount; + m_unallocatedCurrency -= nCurrency; + m_remainingCount--; + } + + return nCurrency; +} + + +//----------------------------------------------------------------------- +// Continuously invoked to modify population over time +void CWaveSpawnPopulator::Update( void ) +{ + VPROF_BUDGET( "CWaveSpawnPopulator::Update", "NextBot" ); + + switch( m_state ) + { + case DONE: + return; + + case PENDING: + m_timer.Start( m_waitBeforeStarting ); + SetState( PRE_SPAWN_DELAY ); + + // zero this here to ensure it is cleared between Waves + // since all WaveSpawns start at the same time at the beginning of a Wave + m_reservedPlayerSlotCount = 0; + + if ( m_startWaveWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_startWaveWarningSound ); + } + + FireEvent( m_startWaveOutput, "StartWaveOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) started PRE_SPAWN_DELAY\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + break; + + case PRE_SPAWN_DELAY: + if ( m_timer.IsElapsed() ) + { + m_countSpawnedSoFar = 0; + m_myReservedSlotCount = 0; + SetState( SPAWNING ); + + if ( m_firstSpawnWarningSound.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_firstSpawnWarningSound ); + } + + FireEvent( m_firstSpawnOutput, "FirstSpawnOutput" ); + + if ( tf_populator_debug.GetBool() ) + { + DevMsg( "%3.2f: WaveSpawn(%s) started SPAWNING\n", gpGlobals->curtime, m_name.IsEmpty() ? "" : m_name.Get() ); + } + } + break; + + case SPAWNING: + if ( m_timer.IsElapsed() ) + { + if( g_pPopulationManager->IsSpawningPaused() ) + { + return; + } + + if ( !m_spawner ) + { + Warning( "Invalid spawner\n" ); + SetState( DONE ); + return; + } + + // count up how many entities we've spawned are still active + int currentActive = 0; + for( int i=0; i<m_activeVector.Count(); ++i ) + { + if ( m_activeVector[i] != NULL && m_activeVector[i]->IsAlive() ) + { + ++currentActive; + } + } + + if ( m_bWaitBetweenSpawnAfterDeath ) + { + if ( currentActive == 0 ) + { + if ( m_spawnLocationResult != SPAWN_LOCATION_NOT_FOUND ) + { + // free up the current spawn area so we select a new one for the next group + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; + + if ( m_waitBetweenSpawns > 0.0f ) + { + // start delay + m_timer.Start( m_waitBetweenSpawns ); + } + + // wait for the timer + return; + } + } + else + { + // wait until all current actives are dead + return; + } + } + + if ( currentActive >= m_maxActive ) + { + // we've reached our allowed cap + return; + } + + if ( m_myReservedSlotCount <= 0 ) + { + // are there enough free slots? + if ( ( m_maxActive - currentActive ) < m_spawnCount ) + { + // not enough room to spawn a group so wait + return; + } + + int currentEnemyCount = GetGlobalTeam( TF_TEAM_PVE_INVADERS )->GetNumPlayers(); + + if ( currentEnemyCount + m_spawnCount + m_reservedPlayerSlotCount > CPopulationManager::MVM_INVADERS_TEAM_SIZE ) + { + // no space right now + return; + } + + // there is room - reserve our slots to ensure another concurrent WaveSpawn doesn't consume them + m_reservedPlayerSlotCount += m_spawnCount; + m_myReservedSlotCount = m_spawnCount; + } + + bool bTeleported = ( m_spawnLocationResult == SPAWN_LOCATION_TELEPORTER ); + + Vector vSpawnPosition = vec3_origin; + if ( m_spawner && m_spawner->IsWhereRequired() ) + { + // try to look for a spawn point or a new teleport location + if ( m_spawnLocationResult == SPAWN_LOCATION_NOT_FOUND || m_spawnLocationResult == SPAWN_LOCATION_TELEPORTER ) + { + m_spawnLocationResult = m_where.FindSpawnLocation( m_vSpawnPosition ); + if ( m_spawnLocationResult == SPAWN_LOCATION_NOT_FOUND ) + { + // try again + return; + } + } + + vSpawnPosition = m_vSpawnPosition; + bTeleported = ( m_spawnLocationResult == SPAWN_LOCATION_TELEPORTER ); + + // reset m_pCurrentSpawnArea if we want to pick a new spawn area for the next bot to spawn + if ( m_bRandomSpawn ) + { + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; + } + } + + EntityHandleVector_t m_justSpawnedVector; + if ( m_spawner && m_spawner->Spawn( vSpawnPosition, &m_justSpawnedVector ) ) + { + // successfully spawned + + FOR_EACH_VEC( m_justSpawnedVector, i ) + { + if ( m_justSpawnedVector[i].Get() == NULL ) + continue; + + CTFBot *bot = ToTFBot( m_justSpawnedVector[i] ); + if ( bot ) + { + bot->SetCustomCurrencyWorth( 0 ); + bot->SetWaveSpawnPopulator( this ); + + // Allows client UI to know if a specific spawner is active + TFObjectiveResource()->SetMannVsMachineWaveClassActive( bot->GetPlayerClass()->GetClassIconName() ); + + if ( IsLimitedSupportWave() ) + { + bot->MarkAsLimitedSupportEnemy(); + } + + // what bot should do after spawning at teleporter exit + if ( bTeleported ) + { + OnBotTeleported( bot ); + } + } + else + { + CTFTankBoss *tank = dynamic_cast< CTFTankBoss * >( m_justSpawnedVector[i].Get() ); + if ( tank ) + { + tank->SetCurrencyValue( 0 ); + tank->SetWaveSpawnPopulator( this ); + + m_pParent->IncrementTanksSpawned(); + } + } + } + + int justSpawnedCount = m_justSpawnedVector.Count(); + + m_countSpawnedSoFar += justSpawnedCount; + + // release our reserved slots + int slotsToReleaseCount = ( justSpawnedCount <= m_myReservedSlotCount ) ? justSpawnedCount : m_myReservedSlotCount; + m_myReservedSlotCount -= slotsToReleaseCount; + m_reservedPlayerSlotCount -= slotsToReleaseCount; + + // somehow, duplicate entries can end up in m_activeVector if we just AddVectorToTail() - look into this + for( int i = 0 ; i < m_justSpawnedVector.Count() ; ++i ) + { + CBaseEntity *newEntity = m_justSpawnedVector[i]; + + for( int j = 0 ; j < m_activeVector.Count() ; ++j ) + { + if ( m_activeVector[j] == NULL ) + continue; + + if ( m_activeVector[j]->entindex() == newEntity->entindex() ) + { + Warning( "WaveSpawn duplicate entry in active vector\n" ); + continue; + } + } + + m_activeVector.AddToTail( newEntity ); + } + + if ( IsFinishedSpawning() ) + { + SetState( WAIT_FOR_ALL_DEAD ); + return; + } + + // successfully spawned a group of SpawnCount entities + if ( m_myReservedSlotCount <= 0 && !m_bWaitBetweenSpawnAfterDeath ) + { + // free up the current spawn area so we select a new one for the next group + m_spawnLocationResult = SPAWN_LOCATION_NOT_FOUND; + + if ( m_waitBetweenSpawns > 0.0f ) + { + // start delay + m_timer.Start( m_waitBetweenSpawns ); + } + } + + // not done yet + return; + } + + // couldn't spawn - retry soon + m_timer.Start( 1.0f ); + } + break; + + case WAIT_FOR_ALL_DEAD: + FOR_EACH_VEC( m_activeVector, i ) + { + if ( m_activeVector[i] != NULL && m_activeVector[i]->IsAlive() ) + { + // not done yet + return; + } + } + + // everyone we spawned is dead + SetState( DONE ); + break; + + } // switch + + // not done yet +} + + +//------------------------------------------------------------------------- +// End CWaveSpawnPopulator +//------------------------------------------------------------------------- + +//------------------------------------------------------------------------- +// CWave +//------------------------------------------------------------------------- +CWave::CWave( CPopulationManager *manager ) : IPopulator( manager ) +{ + m_iEnemyCount = 0; + m_nTanksSpawned = 0; + m_nSentryBustersSpawned = 0; + m_nNumEngineersTeleportSpawned = 0; + m_nNumSentryBustersKilled = 0; + m_totalCurrency = 0; + m_waitWhenDone = 0.0f; + m_isStarted = false; + m_bFiredInitWaveOutput = false; + m_startOutput = NULL; + m_doneOutput = NULL; + m_initOutput = NULL; + m_bCheckBonusCreditsMin = true; + m_bCheckBonusCreditsMax = true; + m_bPlayedUpgradeAlert = false; + m_flBonusCreditsTime = 0; + m_isEveryContainedWaveSpawnDone = false; + m_flStartTime = 0; + + m_doneTimer.Invalidate(); +} + +//------------------------------------------------------------------------- +CWave::~CWave() +{ + delete m_startOutput; + delete m_doneOutput; + delete m_initOutput; + m_waveSpawnVector.PurgeAndDeleteElements(); +} + +//------------------------------------------------------------------------- +bool CWave::Parse( KeyValues *data ) +{ + m_iEnemyCount = 0; + m_nWaveClassCounts.RemoveAll(); + m_totalCurrency = 0; + + FOR_EACH_SUBKEY( data, kvWave ) + { + if ( !Q_stricmp( kvWave->GetName(), "WaveSpawn" ) ) + { + CWaveSpawnPopulator *wavePopulator = new CWaveSpawnPopulator( GetManager() ); + + if ( wavePopulator->Parse( kvWave ) == false ) + { + Warning( "Error reading WaveSpawn definition\n" ); + return false; + } + + m_waveSpawnVector.AddToTail( wavePopulator ); + + if ( !wavePopulator->IsSupportWave() ) + { + // this is a total of all enemies we have to fight that are NOT support enemies + m_iEnemyCount += wavePopulator->m_totalCount; + } + m_totalCurrency += wavePopulator->m_totalCurrency; + + wavePopulator->SetParent( this ); + + if ( wavePopulator->m_spawner ) + { + if ( wavePopulator->m_spawner->IsVarious() ) + { + for ( int i = 0; i < wavePopulator->m_totalCount; ++i ) + { + unsigned int iFlags = wavePopulator->IsSupportWave() ? MVM_CLASS_FLAG_SUPPORT : MVM_CLASS_FLAG_NORMAL; + if ( wavePopulator->m_spawner->IsMiniBoss( i ) ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( wavePopulator->m_spawner->HasAttribute( CTFBot::ALWAYS_CRIT, i ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + if ( wavePopulator->IsLimitedSupportWave() ) + { + iFlags |= MVM_CLASS_FLAG_SUPPORT_LIMITED; + } + AddClassType( wavePopulator->m_spawner->GetClassIcon( i ), 1, iFlags ); + } + } + else + { + unsigned int iFlags = wavePopulator->IsSupportWave() ? MVM_CLASS_FLAG_SUPPORT : MVM_CLASS_FLAG_NORMAL; + if ( wavePopulator->m_spawner->IsMiniBoss() ) + { + iFlags |= MVM_CLASS_FLAG_MINIBOSS; + } + if ( wavePopulator->m_spawner->HasAttribute( CTFBot::ALWAYS_CRIT ) ) + { + iFlags |= MVM_CLASS_FLAG_ALWAYSCRIT; + } + if ( wavePopulator->IsLimitedSupportWave() ) + { + iFlags |= MVM_CLASS_FLAG_SUPPORT_LIMITED; + } + AddClassType( wavePopulator->m_spawner->GetClassIcon(), wavePopulator->m_totalCount, iFlags ); + } + } + } + else if ( !Q_stricmp( kvWave->GetName(), "Sound" ) ) + { + m_soundName.sprintf( "%s", kvWave->GetString() ); + } + else if ( !Q_stricmp( kvWave->GetName(), "Description" ) ) + { + m_description.sprintf( "%s", kvWave->GetString() ); + } + else if ( !Q_stricmp( kvWave->GetName(), "WaitWhenDone" ) ) + { + m_waitWhenDone = kvWave->GetFloat(); + } + else if ( !Q_stricmp( kvWave->GetName(), "Checkpoint" ) ) + { + //m_isCheckpoint = true; + } + else if ( !Q_stricmp( kvWave->GetName(), "StartWaveOutput" ) ) + { + m_startOutput = ParseEvent( kvWave ); + } + else if ( !Q_stricmp( kvWave->GetName(), "DoneOutput" ) ) + { + m_doneOutput = ParseEvent( kvWave ); + } + else if ( !Q_stricmp( kvWave->GetName(), "InitWaveOutput" ) ) + { + m_initOutput = ParseEvent( kvWave ); + } + else + { + Warning( "Unknown attribute '%s' in Wave definition.\n", kvWave->GetName() ); + } + } + + return true; +} +//------------------------------------------------------------------------- +// If we are the currently active wave, update all contained WaveSpawns. +void CWave::Update( void ) +{ + VPROF_BUDGET( "CWave::Update", "NextBot" ); + + if ( TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + ActiveWaveUpdate(); + } + else if ( TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS || TFGameRules()->State_Get() == GR_STATE_TEAM_WIN ) + { + WaveIntermissionUpdate(); + } + + // is the wave done? + if ( m_isEveryContainedWaveSpawnDone && TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) + { + if ( GetManager()->IsBonusRound() && GetManager()->GetBonusBoss() && GetManager()->GetBonusBoss()->IsAlive() ) + { + return; + } + WaveCompleteUpdate(); + } +} + + +//------------------------------------------------------------------------- +void CWave::OnPlayerKilled( CTFPlayer *corpse ) +{ + for( int i=0; i<m_waveSpawnVector.Count(); ++i ) + { + m_waveSpawnVector[i]->OnPlayerKilled( corpse ); + } +} + + +//------------------------------------------------------------------------- +bool CWave::HasEventChangeAttributes( const char* pszEventName ) const +{ + for ( int i=0; i<m_waveSpawnVector.Count(); ++i ) + { + if ( m_waveSpawnVector[i]->HasEventChangeAttributes( pszEventName ) ) + { + return true; + } + } + + return false; +} + + +//------------------------------------------------------------------------- +void CWave::ForceFinish() +{ + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + m_waveSpawnVector[i]->ForceFinish(); + } +} + + +//------------------------------------------------------------------------- +void CWave::ForceReset() +{ + m_isStarted = false; + m_bFiredInitWaveOutput = false; + m_flBonusCreditsTime = 0; + m_isEveryContainedWaveSpawnDone = false; + m_flStartTime = 0; + + m_doneTimer.Invalidate(); + + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + m_waveSpawnVector[i]->ForceReset(); + } +} + +//------------------------------------------------------------------------- +CWaveSpawnPopulator *CWave::FindWaveSpawnPopulator( const char *name ) +{ + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + if ( !Q_stricmp( waveSpawnPopulator->m_name.Get(), name ) ) + { + return waveSpawnPopulator; + } + } + + return NULL; +} + +//------------------------------------------------------------------------- +void CWave::AddClassType( string_t iszClassIconName, int nCount, unsigned int iFlags ) +{ + int nIndex = -1; + for ( int nClass = 0; nClass < m_nWaveClassCounts.Count(); ++nClass ) + { + if ( ( m_nWaveClassCounts[ nClass ].iszClassIconName == iszClassIconName ) && ( m_nWaveClassCounts[ nClass ].iFlags & iFlags ) ) + { + nIndex = nClass; + break; + } + } + + if ( nIndex == -1 ) + { + nIndex = m_nWaveClassCounts.AddToTail(); + m_nWaveClassCounts[ nIndex ].iszClassIconName = iszClassIconName; + m_nWaveClassCounts[ nIndex ].nClassCount = 0; + m_nWaveClassCounts[ nIndex ].iFlags = MVM_CLASS_FLAG_NONE; + } + + m_nWaveClassCounts[ nIndex ].nClassCount += nCount; + m_nWaveClassCounts[ nIndex ].iFlags |= iFlags; +} + +//------------------------------------------------------------------------- +// Private +//------------------------------------------------------------------------- +bool CWave::IsDoneWithNonSupportWaves( void ) +{ + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + if ( waveSpawnPopulator ) + { + if ( !waveSpawnPopulator->IsSupportWave() && !waveSpawnPopulator->IsDone() ) + return false; + } + } + + return true; +} + +//------------------------------------------------------------------------- +void CWave::ActiveWaveUpdate( void ) +{ + VPROF_BUDGET( "CWave::ActiveWaveUpdate", "NextBot" ); + + if ( !m_isStarted ) + { + // Delay the start of the next wave + if ( GetManager()->IsInEndlessWaves() && m_flStartTime > gpGlobals->curtime ) + return; + + // wave just started + m_isStarted = true; + + FireEvent( m_startOutput, "StartWaveOutput" ); + + if ( m_soundName.Length() > 0 ) + { + TFGameRules()->BroadcastSound( 255, m_soundName ); + } + + GetManager()->AdjustMinPlayerSpawnTime(); + } + + m_isEveryContainedWaveSpawnDone = true; + + if ( GetManager()->IsBonusRound() ) + { + return; + } + + // update each contained WaveSpawn + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + bool bWaiting = false; + + // check if this WaveSpawn is waiting for another WaveSpawn to be done spawning players + if ( !waveSpawnPopulator->m_waitForAllSpawned.IsEmpty() ) + { + char *name = waveSpawnPopulator->m_waitForAllSpawned.GetForModify(); + FOR_EACH_VEC( m_waveSpawnVector, j ) + { + CWaveSpawnPopulator *predecessor = m_waveSpawnVector[j]; + if ( predecessor && !Q_stricmp( predecessor->m_name.Get(), name ) ) + { + if ( !predecessor->IsDoneSpawningBots() ) + { + bWaiting = true; + break; + } + } + } + } + + if ( !bWaiting ) + { + // check if this WaveSpawn is waiting for another WaveSpawn's players to all be dead + if ( !waveSpawnPopulator->m_waitForAllDead.IsEmpty() ) + { + const char *name = waveSpawnPopulator->m_waitForAllDead.Get(); + FOR_EACH_VEC( m_waveSpawnVector, j ) + { + CWaveSpawnPopulator *predecessor = m_waveSpawnVector[j]; + if ( predecessor && !Q_stricmp( predecessor->m_name.Get(), name ) ) + { + if ( !predecessor->IsDone() ) + { + bWaiting = true; + break; + } + } + } + } + } + + if ( bWaiting ) + { + continue; + } + + waveSpawnPopulator->Update(); + + m_isEveryContainedWaveSpawnDone &= waveSpawnPopulator->IsDone(); + } + + if ( IsDoneWithNonSupportWaves() ) + { + // Loop through and tell all the WaveSpawns + FOR_EACH_VEC( m_waveSpawnVector, i ) + { + CWaveSpawnPopulator *waveSpawnPopulator = m_waveSpawnVector[i]; + waveSpawnPopulator->OnNonSupportWavesDone(); + } + + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + // Now let's kill everyone left on the attacking team + CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); + if ( pPlayer && pPlayer->IsAlive() && + ( ( pPlayer->GetTeamNumber() == TF_TEAM_PVE_INVADERS ) || pPlayer->m_Shared.InCond( TF_COND_REPROGRAMMED ) ) ) + { + pPlayer->CommitSuicide( true, false ); + } + } + } +} + +//------------------------------------------------------------------------- +void CWave::WaveCompleteUpdate( void ) +{ + bool bHasTank = NumTanksSpawned() >= 1; + + FireEvent( m_doneOutput, "DoneOutput" ); + + bool bLastWave = ( GetManager()->GetWaveNumber() + 1 ) >= GetManager()->GetTotalWaveCount(); + bool bMidWave = ( GetManager()->GetWaveNumber() + 1 ) >= ( GetManager()->GetTotalWaveCount() / 2 ); + bool bAdvancedPopfile = ( g_pPopulationManager ? g_pPopulationManager->IsAdvancedPopFile() : false ); + + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_wave_complete" ); + if ( event ) + { + event->SetBool( "advanced", bAdvancedPopfile ); + gameeventmanager->FireEvent( event ); + } + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + if ( bAdvancedPopfile ) + { + CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; + if ( pMaster && ( pMaster->GetNumPoints() > 0 ) ) + { + if ( pMaster->GetNumPointsOwnedByTeam( TF_TEAM_PVE_DEFENDERS ) == pMaster->GetNumPoints() ) + { + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_adv_wave_complete_no_gates" ); + if ( event ) + { + event->SetInt( "index", GetManager()->GetWaveNumber() ); + gameeventmanager->FireEvent( event ); + } + } + } + } + } + + if ( bLastWave && !GetManager()->IsInEndlessWaves() ) + { + GetManager()->MvMVictory(); + + if ( TFGameRules() ) + { + if ( GTFGCClientSystem()->GetMatch() && GTFGCClientSystem()->GetMatch()->m_eMatchGroup == k_nMatchGroup_MvM_MannUp ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Manned_Up_Wave_End" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Final_Wave_End" ); + } + + TFGameRules()->BroadcastSound( 255, "music.mvm_end_last_wave" ); + } + + event = gameeventmanager->CreateEvent( "mvm_mission_complete" ); + if ( event ) + { + event->SetString( "mission", GetManager()->GetPopulationFilename() ); + gameeventmanager->FireEvent( event ); + } + } + else + { + if ( TFGameRules() ) + { + TFGameRules()->BroadcastSound( 255,"Announcer.MVM_Wave_End" ); + + if( bHasTank ) + { + TFGameRules()->BroadcastSound( 255, "music.mvm_end_tank_wave" ); + } + else if( bMidWave ) + { + TFGameRules()->BroadcastSound( 255, "music.mvm_end_mid_wave" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "music.mvm_end_wave" ); + } + } + } + + CBroadcastRecipientFilter filter; + filter.MakeReliable(); + UserMessageBegin( filter, "MVMAnnouncement" ); + WRITE_CHAR( TF_MVM_ANNOUNCEMENT_WAVE_COMPLETE ); + WRITE_CHAR( GetManager()->GetWaveNumber() ); + MessageEnd(); + + if ( TFObjectiveResource() ) + { + // if we're using a timer between waves... + if ( !g_pPopulationManager->GetWavesUseReadyBetween() ) + { + if ( !m_doneTimer.HasStarted() ) + { + m_doneTimer.Start( m_waitWhenDone ); + } + + TFObjectiveResource()->SetMannVsMachineNextWaveTime( gpGlobals->curtime + m_waitWhenDone ); + } + + // Force respawn dead defenders + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_PVE_DEFENDERS ); + FOR_EACH_VEC( playerVector, i ) + { + if ( !playerVector[i]->IsAlive() ) + { + playerVector[i]->ForceRespawn(); + } + + // clear player's accumulated sentry damage + playerVector[i]->ResetAccumulatedSentryGunDamageDealt(); + playerVector[i]->ResetAccumulatedSentryGunKillCount(); + } + } + + GetManager()->WaveEnd( true ); +} + +//------------------------------------------------------------------------- +void CWave::WaveIntermissionUpdate ( void ) +{ + if ( !m_bFiredInitWaveOutput ) + { + FireEvent( m_initOutput, "InitWaveOutput" ); + + m_bFiredInitWaveOutput = true; + } + + if ( m_GetUpgradesAlertTimer.HasStarted() && m_GetUpgradesAlertTimer.IsElapsed() ) + { + // Monitor for full wave currency collection bonus + if ( ( m_bCheckBonusCreditsMin || m_bCheckBonusCreditsMax ) && gpGlobals->curtime > m_flBonusCreditsTime ) + { + int nWaveNum = GetManager()->GetWaveNumber() - 1; + int nDropped = MannVsMachineStats_GetDroppedCredits( nWaveNum ); + int nAcquired = MannVsMachineStats_GetAcquiredCredits( nWaveNum, false ); + float flRatioCollected = clamp( ( (float)nAcquired / (float)nDropped ), 0.1f, 1.f ); + + float flMinBonus = tf_mvm_currency_bonus_ratio_min.GetFloat(); + float flMaxBonus = tf_mvm_currency_bonus_ratio_max.GetFloat(); + + Assert( flMinBonus <= flMaxBonus ); + if ( flMinBonus > flMaxBonus ) + flMinBonus = flMaxBonus; + + // Max bonus + if ( m_bCheckBonusCreditsMax && nDropped > 0 && flRatioCollected >= flMaxBonus ) + { + int nAmount = (float)TFGameRules()->CalculateCurrencyAmount_ByType( TF_CURRENCY_WAVE_COLLECTION_BONUS ) * 0.5f; + TFGameRules()->DistributeCurrencyAmount( nAmount, NULL, true, false, true ); + + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Bonus" ); + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_creditbonus_wave" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + m_bCheckBonusCreditsMax = false; + m_GetUpgradesAlertTimer.Reset(); + } + // Min bonus + if ( m_bCheckBonusCreditsMin && nDropped > 0 && flRatioCollected >= flMinBonus ) + { + int nAmount = (float)TFGameRules()->CalculateCurrencyAmount_ByType( TF_CURRENCY_WAVE_COLLECTION_BONUS ) * 0.5f; + TFGameRules()->DistributeCurrencyAmount( nAmount, NULL, true, false, true ); + + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Bonus" ); + IGameEvent *event = gameeventmanager->CreateEvent( "mvm_creditbonus_wave" ); + if ( event ) + { + gameeventmanager->FireEvent( event ); + } + + m_bCheckBonusCreditsMin = false; + } + else if ( !m_bPlayedUpgradeAlert ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.MVM_Get_To_Upgrade" ); + + m_bPlayedUpgradeAlert = true; + m_GetUpgradesAlertTimer.Reset(); + } + + m_flBonusCreditsTime = gpGlobals->curtime + 0.25f; + } + } + + // When we use a timer between waves, start it here + if ( m_doneTimer.HasStarted() && m_doneTimer.IsElapsed() ) + { + m_doneTimer.Invalidate(); + GetManager()->StartCurrentWave(); + } +} +//------------------------------------------------------------------------- +// End CWave +//------------------------------------------------------------------------- |