//========= 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 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()[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 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()[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; iIsDisabled() ) 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; tGetNumPlayers(); ++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; iIsTFMarked() ) { 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= 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()[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( 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; kSetFlagTarget( 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[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( livePlayerVector[iLiveBot] ); if ( pBot && pBot->IsPlayerClass( TF_CLASS_SNIPER ) ) { nSniperCount++; } } // dispatch mission members for( int iDesiredCount=0; iDesiredCountSpawn( vSpawnPosition, &spawnedVector ) ) { // success for( int iSpawn=0; iSpawnSetFlagTarget( 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; iHasAttributeTF( m_navAreaFilter ) ) { candidateAreaVector.AddToTail( area ); } } CUtlVector< CTFNavArea * > selectedAreaVector; SelectSeparatedShuffleSet< CTFNavArea >( m_count, m_minSeparation, candidateAreaVector, &selectedAreaVector ); if ( m_spawner ) { for( i=0; iSpawn( 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; iChangeTeam( 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; iIsAlive() ) { ++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; iOnPlayerKilled( corpse ); } } //------------------------------------------------------------------------- bool CWave::HasEventChangeAttributes( const char* pszEventName ) const { for ( int i=0; iHasEventChangeAttributes( 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 //-------------------------------------------------------------------------