//========= Copyright Valve Corporation, All rights reserved. ============// // tf_raid_logic.cpp // Raid game mode singleton manager // Michael Booth, November 2009 #include "cbase.h" #ifdef TF_RAID_MODE #include "team.h" #include "nav_pathfind.h" #include "tf_gamerules.h" #include "team_control_point_master.h" #include "bot/tf_bot.h" #include "nav_mesh/tf_nav_mesh.h" #include "raid/tf_raid_logic.h" #include "bot_npc/bot_npc_minion.h" #include "tf_obj_sentrygun.h" #include "filesystem.h" #include "func_respawnroom.h" #include "pathtrack.h" extern ConVar mp_teams_unbalance_limit; extern ConVar mp_autoteambalance; extern ConVar sv_alltalk; extern ConVar mp_timelimit; CRaidLogic *g_pRaidLogic = NULL; ConVar tf_debug_sniper_spots( "tf_debug_sniper_spots", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_wanderers( "tf_raid_max_wanderers", "10"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_engineers( "tf_raid_max_defense_engineers", "6"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_demomen( "tf_raid_max_defense_demomen", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_heavies( "tf_raid_max_defense_heavies", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_soldiers( "tf_raid_max_defense_soldiers", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_pyros( "tf_raid_max_defense_pyros", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_spies( "tf_raid_max_defense_spies", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_snipers( "tf_raid_max_defense_snipers", "3"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_max_defense_squads( "tf_raid_max_defense_squads", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_wandering_density( "tf_raid_wandering_density", "0.00001", FCVAR_CHEAT, "Wandering defenders per unit area" ); ConVar tf_raid_spawn_wanderers( "tf_raid_spawn_wanderers", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_defender_density( "tf_raid_defender_density", "0.000001", 0/*FCVAR_CHEAT*/, "Wandering defenders per unit area" ); ConVar tf_raid_max_defender_count( "tf_raid_max_defender_count", "18", 0/*FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_defenders( "tf_raid_spawn_defenders", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sentry_density( "tf_raid_sentry_density", "0.0000005", 0/*FCVAR_CHEAT*/, "Sentry guns per unit area" ); ConVar tf_raid_sentry_spacing( "tf_raid_sentry_spacing", "750", 0/*FCVAR_CHEAT*/, "Minimum travel distance between sentry gun spots" ); ConVar tf_raid_debug_sentry_placement( "tf_raid_debug_sentry_placement", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_sentries( "tf_raid_spawn_sentries", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_engineers( "tf_raid_spawn_engineers", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_engineer_spawn_interval( "tf_raid_engineer_spawn_interval", "20"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_mob_spawn_min_interval( "tf_raid_mob_spawn_min_interval", "60"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_mob_spawn_max_interval( "tf_raid_mob_spawn_max_interval", "90"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_mob_spawn_count( "tf_raid_mob_spawn_count", "15"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_mob_spawn_below_tolerance( "tf_raid_mob_spawn_below_tolerance", "150"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_mob_spawn_min_range( "tf_raid_mob_spawn_min_range", "1000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_mobs( "tf_raid_spawn_mobs", "1"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_mob_as_squad_chance_start( "tf_raid_spawn_mob_as_squad_chance_start", "100" ); // /*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_mob_as_squad_chance_halfway( "tf_raid_spawn_mob_as_squad_chance_halfway", "100" ); // /*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_mob_as_squad_chance_final( "tf_raid_spawn_mob_as_squad_chance_final", "100" ); // /*, FCVAR_CHEAT*/ ); ConVar tf_raid_squad_medic_intro_percent( "tf_raid_squad_medic_intro_percent", "0.5" ); // /*, FCVAR_CHEAT*/ ); ConVar tf_raid_capture_mob_interval( "tf_raid_capture_mob_interval", "20"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_special_spawn_min_interval( "tf_raid_special_spawn_min_interval", "20"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_special_spawn_max_interval( "tf_raid_special_spawn_max_interval", "30"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_specials( "tf_raid_spawn_specials", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sniper_spawn_ahead_incursion( "tf_raid_sniper_spawn_ahead_incursion", "6000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sniper_spawn_behind_incursion( "tf_raid_sniper_spawn_behind_incursion", "6000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sniper_spawn_max_range( "tf_raid_sniper_spawn_max_range", "6000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sniper_spawn_min_range( "tf_raid_sniper_spawn_min_range", "1000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_show_escape_route( "tf_raid_show_escape_route", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sentry_build_ahead_incursion( "tf_raid_sentry_build_ahead_incursion", "5000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_sentry_build_behind_incursion( "tf_raid_sentry_build_behind_incursion", "-1000"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_debug( "tf_raid_debug", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_debug_escape_route( "tf_raid_debug_escape_route", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_debug_director( "tf_raid_debug_director", "0"/*, FCVAR_CHEAT*/ ); ConVar tf_raid_spawn_enable( "tf_raid_spawn_enable", "1"/*, FCVAR_CHEAT*/ ); extern ConVar tf_populator_active_buffer_range; extern bool IsSpaceToSpawnHere( const Vector &where ); //-------------------------------------------------------------------------------------------------------- // Check actual line of sight to team bool IsPlayerVisibleToTeam( CTFPlayer *subject, int teamIndex ) { CTeam *team = GetGlobalTeam( teamIndex ); 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->IsInFieldOfView( subject->EyePosition() ) ) { if ( teamMember->IsLineOfSightClear( subject, CBaseCombatCharacter::IGNORE_ACTORS ) ) { // visible to team return true; } } } return false; } //-------------------------------------------------------------------------------------------------------------- int GetAvailableRedSpawnSlots( void ) { int available = 0; // count dead bots we can re-use CTeam *deadTeam = GetGlobalTeam( TEAM_SPECTATOR ); for( int i=0; iGetNumPlayers(); ++i ) { if ( !deadTeam->GetPlayer(i)->IsBot() ) continue; // reuse this guy ++available; } // count unused player slots int totalPlayerCount = 0; totalPlayerCount += GetGlobalTeam( TEAM_SPECTATOR )->GetNumPlayers(); totalPlayerCount += 4; // always leave room for 4 blue players totalPlayerCount += GetGlobalTeam( TF_TEAM_RED )->GetNumPlayers(); available += gpGlobals->maxClients - totalPlayerCount; return available; } //------------------------------------------------------------------------- //------------------------------------------------------------------------- BEGIN_DATADESC( CRaidLogic ) DEFINE_THINKFUNC( Update ), END_DATADESC() LINK_ENTITY_TO_CLASS( tf_logic_raid, CRaidLogic ); //------------------------------------------------------------------------- CRaidLogic::CRaidLogic() { ListenForGameEvent( "teamplay_point_captured" ); ListenForGameEvent( "teamplay_round_win" ); ListenForGameEvent( "teamplay_round_start" ); m_didFailLastTime = false; } //------------------------------------------------------------------------- CRaidLogic::~CRaidLogic() { g_pRaidLogic = NULL; } //------------------------------------------------------------------------- void CRaidLogic::Spawn( void ) { BaseClass::Spawn(); Reset(); SetThink( &CRaidLogic::Update ); SetNextThink( gpGlobals->curtime ); m_didFailLastTime = false; g_pRaidLogic = this; m_miniBossIndex = 0; } //------------------------------------------------------------------------- void CRaidLogic::Reset( void ) { m_isWaitingForRaidersToLeaveSpawnRoom = true; m_wasCapturingPoint = false; m_mobSpawnTimer.Invalidate(); m_mobLifetimeTimer.Invalidate(); m_specialSpawnTimer.Invalidate(); m_mobCountRemaining = 0; m_mobArea = NULL; m_mobClass = TF_CLASS_SCOUT; m_priorRaiderAliveCount = -1; m_farthestAlongRaider = NULL; m_farthestAlongEscapeRouteArea = NULL; m_incursionDistanceAtEnd = -1.0f; m_wandererCount = 0; m_engineerCount = 0; m_demomanCount = 0; m_heavyCount = 0; m_soldierCount = 0; m_pyroCount = 0; m_spyCount = 0; m_sniperCount = 0; m_squadCount = 0; m_miniBossIndex = 0; } #if 0 //-------------------------------------------------------------------------------------------------------- bool SpawnWanderer( const Vector &spot ) { if ( !tf_raid_spawn_wanderers.GetBool() ) return false; return SpawnRedTFBot( TF_CLASS_SCOUT, spot ) ? true : false; /* CBaseCombatCharacter *minion = static_cast< CBaseCombatCharacter * >( CreateEntityByName( "bot_npc_minion" ) ); if ( minion ) { minion->SetAbsOrigin( spot ); minion->SetOwnerEntity( NULL ); DispatchSpawn( minion ); return true; } return false; */ } #endif // 0 //------------------------------------------------------------------------- void CRaidLogic::OnRoundStart( void ) { if ( !TFGameRules() || !TFGameRules()->IsRaidMode() ) return; Reset(); // unspawn entire red team CTeam *defendingTeam = GetGlobalTeam( TF_TEAM_RED ); int i; for( i=0; iGetNumPlayers(); ++i ) { engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", defendingTeam->GetPlayer(i)->GetUserID() ) ); } // remove all minions CBaseEntity *minion = NULL; while( ( minion = gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) { UTIL_Remove( minion ); } // kick last round's NPCs CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); for( i=0; iGetNumPlayers(); ++i ) { CTFBot *bot = ToTFBot( raidingTeam->GetPlayer(i) ); if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) { engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", raidingTeam->GetPlayer(i)->GetUserID() ) ); } } BuildEscapeRoute(); // collect special areas m_sniperSpotVector.RemoveAll(); m_sentrySpotVector.RemoveAll(); m_rescueClosetVector.RemoveAll(); m_miniBossHomeVector.RemoveAll(); m_miniBossIndex = 0; float availableSentrySpotArea = 0.0f; for( i=0; iHasAttributeTF( TF_NAV_SNIPER_SPOT ) ) m_sniperSpotVector.AddToTail( area ); if ( area->HasAttributeTF( TF_NAV_SENTRY_SPOT ) ) { m_sentrySpotVector.AddToTail( area ); availableSentrySpotArea += area->GetSizeX() * area->GetSizeY(); } if ( area->HasAttributeTF( TF_NAV_RESCUE_CLOSET ) ) m_rescueClosetVector.AddToTail( area ); if ( area->HasAttributeTF( TF_NAV_RED_SETUP_GATE ) ) m_miniBossHomeVector.AddToTail( area ); } // compute total geometric area of entire nav mesh, and clear all wander counts float totalSpace = 0.0f; for( i=0; iGetSizeX() * area->GetSizeY(); area->SetWanderCount( 0 ); } #if 0 //---------------------------------------------- // fill the world with wandering defenders int totalPopulation = (int)( tf_raid_wandering_density.GetFloat() * totalSpace + 0.5f ); CUtlVector< CNavArea * > minionAreaVector; SelectSeparatedShuffleSet< CNavArea >( totalPopulation, tf_raid_sentry_spacing.GetFloat(), TheNavAreas, &minionAreaVector ); for( int i=0; i( minionAreaVector[i] )->AddToWanderCount( 1 ); // SpawnWanderer( minionAreaVector[i]->GetRandomPoint() ); } DevMsg( "RAID: Total minion population = %d\n", minionAreaVector.Count() ); //---------------------------------------------- // determine where sentry guns will be // the total sentry population is based on the total actual space, not just sentry areas totalPopulation = (int)( tf_raid_sentry_density.GetFloat() * totalSpace + 0.5f ); SelectSeparatedShuffleSet< CTFNavArea >( totalPopulation, tf_raid_sentry_spacing.GetFloat(), m_sentrySpotVector, &m_actualSentrySpotVector ); for( int i=0; iGetCenter() ); } DevMsg( "RAID: Total sentry population = %d\n", m_actualSentrySpotVector.Count() ); //---------------------------------------------- // fill the world with defending bots if ( tf_raid_spawn_defenders.GetBool() ) { const int classRosterCount = 5; int classRoster[ classRosterCount ] = { TF_CLASS_SNIPER, TF_CLASS_DEMOMAN, TF_CLASS_SNIPER, TF_CLASS_DEMOMAN, TF_CLASS_PYRO }; CUtlVector< CTFNavArea * > validSpawnAreaVector; for( int i=0; iIsValidForWanderingPopulation() && IsSpaceToSpawnHere( area->GetCenter() ) ) { validSpawnAreaVector.AddToTail( area ); } } totalPopulation = (int)( tf_raid_defender_density.GetFloat() * totalSpace + 0.5f ); totalPopulation = clamp( totalPopulation, 0, tf_raid_max_defender_count.GetInt() ); CUtlVector< CTFNavArea * > defenderAreaVector; SelectSeparatedShuffleSet< CTFNavArea >( totalPopulation, tf_raid_sentry_spacing.GetFloat(), validSpawnAreaVector, &defenderAreaVector ); for( int i=0; iGetCenter() + Vector( 0, 0, 10.0f ) ); if ( bot ) { bot->SetHomeArea( homeArea ); } else { DevMsg( "RAID: Failed to spawn defender!\n" ); } } DevMsg( "RAID: Total defender population = %d\n", defenderAreaVector.Count() ); } #endif // collect all capture point gates m_gateVector.RemoveAll(); CBaseEntity *entity = NULL; while( ( entity = gEntList.FindEntityByClassname( entity, "func_door*" ) ) != NULL ) { CBaseDoor *door = (CBaseDoor *)entity; if ( door->GetEntityName() != NULL_STRING && V_stristr( STRING( door->GetEntityName() ), "raid" ) ) { m_gateVector.AddToTail( door ); } } } //-------------------------------------------------------------------------------------------------------- void CRaidLogic::FireGameEvent( IGameEvent *event ) { const char *eventName = event->GetName(); if ( !Q_strcmp( eventName, "teamplay_point_captured" ) ) { // they just capped - give them a break and reset the mob spawner StartMobTimer( RandomFloat( tf_raid_mob_spawn_min_interval.GetFloat(), tf_raid_mob_spawn_max_interval.GetFloat() ) ); DevMsg( "RAID: %3.2f: Reset Mob timer after successful point capture\n", gpGlobals->curtime ); } else if ( !Q_strcmp( eventName, "teamplay_round_win" ) ) { if ( event->GetInt( "team" ) == TF_TEAM_RED ) { // the raiders didn't make it m_didFailLastTime = true; } } else if ( !Q_strcmp( eventName, "teamplay_round_start" ) ) { OnRoundStart(); } } //-------------------------------------------------------------------------------------------------------- int CompareIncursionDistances( CTFNavArea * const *area1, CTFNavArea * const *area2 ) { float d1 = (*area1)->GetIncursionDistance( TF_TEAM_BLUE ); float d2 = (*area2)->GetIncursionDistance( TF_TEAM_BLUE ); if ( d1 < d2 ) return -1; if ( d1 > d2 ) return 1; return 0; } #if 0 //-------------------------------------------------------------------------------------------------------- class CPopulator : public ISearchSurroundingAreasFunctor { public: CPopulator( float leaderIncursionRange, int maxWanderersToSpawn ) { m_leaderIncursionRange = leaderIncursionRange; m_spaceLeft = maxWanderersToSpawn; m_floor = FLT_MAX; CTeam *invaderTeam = GetGlobalTeam( TF_TEAM_BLUE ); for( int i=0; iGetNumPlayers(); ++i ) { if ( !invaderTeam->GetPlayer(i)->IsAlive() ) continue; if ( invaderTeam->GetPlayer(i)->GetAbsOrigin().z < m_floor ) { m_floor = invaderTeam->GetPlayer(i)->GetAbsOrigin().z; } } m_floor -= tf_raid_mob_spawn_below_tolerance.GetFloat(); } virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) { CTFNavArea *area = (CTFNavArea *)baseArea; if ( area->IsBlocked( TF_TEAM_RED ) ) return true; if ( area->HasAttributeTF( TF_NAV_NO_SPAWNING | TF_NAV_SPAWN_ROOM_BLUE ) ) return true; if ( tf_raid_debug.GetInt() > 1 ) { if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) area->DrawFilled( 255, 100, 0, 100, 1.0f ); else area->DrawFilled( 0, 100, 255, 100, 1.0f ); } // require minimum size if ( area->GetSizeX() < 45.0f && area->GetSizeY() < 45.0f ) return true; // don't use areas far below team // if ( area->GetCenter().z < m_floor ) // return true; if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) { // don't spawn wanderers in view of raiders // clear any unspawned wanderers here area->SetWanderCount( 0 ); return true; } // collect out-of-sight areas m_hiddenAreaVector.AddToTail( area ); // collect out-of-sight areas ahead of the team for special spawns const float aheadBuffer = 500.0f; if ( area->GetIncursionDistance( TF_TEAM_BLUE ) > m_leaderIncursionRange + aheadBuffer ) { m_hiddenAreaAheadVector.AddToTail( area ); } if ( m_spaceLeft <= 0 ) return true; if ( !tf_raid_spawn_wanderers.GetBool() ) { return true; } if ( TFGameRules()->GetRaidLogic()->IsMobSpawning() ) { // don't spawn wanderers if a mob is spawning to keep slots free return true; } if ( !area->IsValidForWanderingPopulation() ) { return true; } int maxSpawnCount = 5; while( area->GetWanderCount() > 0 && --maxSpawnCount && m_spaceLeft ) { // attempt to spawn a wanderer here if ( SpawnWanderer( area->GetRandomPoint() + Vector( 0, 0, StepHeight ) ) ) { area->SetWanderCount( area->GetWanderCount() - 1 ); --m_spaceLeft; } } return true; } // return true if 'adjArea' should be included in the ongoing search virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) { CTFNavArea *area = (CTFNavArea *)adjArea; float incursionDistance = area->GetIncursionDistance( TF_TEAM_BLUE ); return incursionDistance > m_leaderIncursionRange - tf_populator_active_buffer_range.GetFloat() && incursionDistance < m_leaderIncursionRange + tf_populator_active_buffer_range.GetFloat() && !adjArea->IsBlocked( TEAM_ANY ); } virtual void PostSearch( void ) { // collect the highest hidden & ahead areas float minZ = 999999.9f, maxZ = -999999.9f; int i; for( i=0; iGetCenter().z; if ( areaZ < minZ ) minZ = areaZ; if ( areaZ > maxZ ) maxZ = areaZ; } float floorZ = minZ + 0.7f * ( maxZ - minZ ); for( i=0; iGetCenter().z; if ( areaZ > floorZ ) { m_hiddenAreaAheadHighVector.AddToTail( area ); } } // sort hidden areas by incursion distance m_hiddenAreaVector.Sort( CompareIncursionDistances ); } float m_leaderIncursionRange; float m_floor; int m_spaceLeft; CUtlVector< CTFNavArea * > m_hiddenAreaVector; CUtlVector< CTFNavArea * > m_hiddenAreaAheadVector; CUtlVector< CTFNavArea * > m_hiddenAreaAheadHighVector; }; #endif // 0 //-------------------------------------------------------------------------------------------------------- bool CRaidLogic::Unspawn( CTFPlayer *who ) { if ( who->IsAlive() && who->GetTeamNumber() == TF_TEAM_RED ) { // only cull Engineers who are far behind the team if ( who->IsPlayerClass( TF_CLASS_ENGINEER ) ) { CTFNavArea *area = (CTFNavArea *)who->GetLastKnownArea(); if ( area && area->GetIncursionDistance( TF_TEAM_BLUE ) > GetMaximumRaiderIncursionDistance() - tf_populator_active_buffer_range.GetFloat() ) return false; } else if ( !who->IsPlayerClass( TF_CLASS_SCOUT ) ) { // do not unspawn these classes - they lurk at far distances return false; } } // check actual line of sight to team if ( IsPlayerVisibleToTeam( who, TF_TEAM_BLUE ) ) return false; who->ChangeTeam( TEAM_SPECTATOR, false, true ); // destroy all buildings (for relocated engineers) who->RemoveAllObjects(); return true; } //-------------------------------------------------------------------------------------------------------- CTFNavArea *CRaidLogic::FindSpawnAreaAhead( void ) { CTFPlayer *leader = GetFarthestAlongRaider(); if ( leader == NULL || m_escapeRouteVector.Count() == 0 ) return NULL; const float minTravel = 1000.0f; float minIncursion = GetMaximumRaiderIncursionDistance() + minTravel; // find first non-visible area ahead of leader along escape path beyond a minimum distance for( int i=0; iIsBlocked( TF_TEAM_RED ) ) return NULL; if ( area->HasAttributeTF( TF_NAV_NO_SPAWNING ) ) continue; if ( area->GetIncursionDistance( TF_TEAM_BLUE ) < minIncursion ) continue; if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) continue; if ( IsSpaceToSpawnHere( area->GetCenter() ) ) { // found valid squad spawn return area; } } return NULL; } //-------------------------------------------------------------------------------------------------------- CTFNavArea *CRaidLogic::FindSpawnAreaBehind( void ) { CTFPlayer *leader = GetFarthestAlongRaider(); if ( leader == NULL || m_escapeRouteVector.Count() == 0 ) return NULL; const float minTravel = 1000.0f; float maxIncursion = GetMaximumRaiderIncursionDistance() - minTravel; // find first non-visible area behind leader along escape path beyond a minimum distance for( int i=m_escapeRouteVector.Count()-1; i >= 0; --i ) { CTFNavArea *area = m_escapeRouteVector[i]; if ( area->HasAttributeTF( TF_NAV_NO_SPAWNING ) ) continue; if ( area->GetIncursionDistance( TF_TEAM_BLUE ) > maxIncursion ) continue; if ( area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) continue; if ( IsSpaceToSpawnHere( area->GetCenter() ) ) { // found valid squad spawn return area; } } return NULL; } #if 0 //-------------------------------------------------------------------------------------------------------- bool CRaidLogic::SpawnSquad( CTFNavArea *spawnArea ) { if ( spawnArea == NULL ) return false; int squadSize = 4; int freeSlots = GetAvailableRedSpawnSlots(); if ( freeSlots < squadSize ) { DevMsg( "RAID: %3.2f: *** Not enough free slots to spawn a squad\n", gpGlobals->curtime ); return false; } const int squadClassMaxCount = 4; int squadClasses[ squadClassMaxCount ]; int squadClassCount = 0; squadClasses[ squadClassCount++ ] = TF_CLASS_HEAVYWEAPONS; squadClasses[ squadClassCount++ ] = TF_CLASS_SOLDIER; squadClasses[ squadClassCount++ ] = TF_CLASS_PYRO; squadClasses[ squadClassCount++ ] = TF_CLASS_DEMOMAN; // randomly shuffle the class order int n = squadClassCount; while( n > 1 ) { int k = RandomInt( 0, n-1 ); n--; int tmp = squadClasses[n]; squadClasses[n] = squadClasses[k]; squadClasses[k] = tmp; } if ( GetMaximumRaiderIncursionDistance() > tf_raid_squad_medic_intro_percent.GetFloat() * GetIncursionDistanceAtEnd() ) { // Medics join squads farther into the raid squadClasses[0] = TF_CLASS_MEDIC; } CTFBotSquad *squad = new CTFBotSquad; CTFBot *bot; DevMsg( "RAID: %3.2f: <<<< Spawning Squad >>>>\n", gpGlobals->curtime ); for( int i=0; iGetCenter() ); if ( !bot ) return false; bot->JoinSquad( squad ); DevMsg( "RAID: %3.2f: Squad member %s(%d)\n", gpGlobals->curtime, bot->GetPlayerName(), bot->entindex() ); } IGameEvent* event = gameeventmanager->CreateEvent( "raid_spawn_squad" ); if ( event ) { gameeventmanager->FireEvent( event ); } return true; } #endif // 0 //-------------------------------------------------------------------------------------------------------- void CRaidLogic::StartMobTimer( float duration ) { if ( IsMobSpawning() ) { DevMsg( "RAID: %3.2f: Skipping mob spawn because an existing mob is still in progress\n", gpGlobals->curtime ); return; } m_mobSpawnTimer.Start( duration ); m_mobLifetimeTimer.Invalidate(); m_mobCountRemaining = 0; m_mobArea = NULL; } #if 0 //-------------------------------------------------------------------------------------------------------- CTFNavArea *CRaidLogic::SelectMobSpawn( CUtlVector< CTFNavArea * > *spawnAreaVector, RelativePositionType where ) { if ( spawnAreaVector->Count() == 0 ) { // use high-reliability locations CTFNavArea *spawnArea = FindSpawnAreaBehind(); if ( spawnArea ) return spawnArea; return NULL; // FindSpawnAreaAhead(); } const int maxRetries = 5; CTFNavArea *spawnArea = NULL; CTFPlayer *farRaider = GetFarthestAlongRaider(); if ( !farRaider ) return NULL; for( int r=0; rCount(); break; case BEHIND: // areas are sorted from behind to ahead - weight the selection to choose behind which = ( 1.0f - SkewedRandomValue() ) * spawnAreaVector->Count(); break; case ANYWHERE: // areas are sorted from behind to ahead - weight the selection to choose ahead which = RandomFloat( 0.0f, 1.0f ) * spawnAreaVector->Count(); break; } if ( which == spawnAreaVector->Count() ) --which; spawnArea = spawnAreaVector->Element( which ); if ( ( farRaider->GetAbsOrigin() - spawnArea->GetCenter() ).IsLengthGreaterThan( tf_raid_mob_spawn_min_range.GetFloat() ) ) { // well behaved spawn area return spawnArea; } } // return whatever we've found so far return spawnArea; } //-------------------------------------------------------------------------------------------------------- void CRaidLogic::SpawnMobs( CUtlVector< CTFNavArea * > *spawnAreaVector ) { if ( !tf_raid_spawn_mobs.GetBool() ) return; if ( m_mobLifetimeTimer.HasStarted() ) { if ( m_mobLifetimeTimer.IsElapsed() ) { // time is up for this mob to spawn, clear the rest DevMsg( "RAID: %3.2f: Mob spawn lifetime is up. Mob count remaining unspawned: %d\n", gpGlobals->curtime, m_mobCountRemaining ); m_mobLifetimeTimer.Invalidate(); m_mobCountRemaining = 0; m_mobArea = NULL; // start next mob m_mobSpawnTimer.Start( RandomFloat( tf_raid_mob_spawn_min_interval.GetFloat(), tf_raid_mob_spawn_max_interval.GetFloat() ) ); } // can't create another mob until this mob has expired return; } if ( m_mobSpawnTimer.HasStarted() && m_mobSpawnTimer.IsElapsed() && spawnAreaVector && spawnAreaVector->Count() > 0 ) { // chance of mob changes as we progress through the map int mobChance; float progressRatio = GetMaximumRaiderIncursionDistance() / GetIncursionDistanceAtEnd(); if ( progressRatio < 0.5f ) { mobChance = tf_raid_spawn_mob_as_squad_chance_start.GetInt() + 2.0f * progressRatio * ( tf_raid_spawn_mob_as_squad_chance_halfway.GetInt() - tf_raid_spawn_mob_as_squad_chance_start.GetInt() ); } else { mobChance = tf_raid_spawn_mob_as_squad_chance_halfway.GetInt() + 2.0f * ( progressRatio - 0.5f ) * ( tf_raid_spawn_mob_as_squad_chance_final.GetInt() - tf_raid_spawn_mob_as_squad_chance_halfway.GetInt() ); } if ( RandomInt( 1, 100 ) <= mobChance ) { // this mob is a squad CTFNavArea *spawnArea = SelectMobSpawn( spawnAreaVector, BEHIND ); // m_wasCapturingPoint ? ANYWHERE : BEHIND ); if ( SpawnSquad( spawnArea ) ) { StartMobTimer( RandomFloat( tf_raid_mob_spawn_min_interval.GetFloat(), tf_raid_mob_spawn_max_interval.GetFloat() ) ); } else { // couldn't spawn - try again soon StartMobTimer( 1.0f ); DevMsg( "RAID: %3.2f: No place to spawn Squad!\n", gpGlobals->curtime ); } return; } // time to throw in a mob rush m_mobArea = SelectMobSpawn( spawnAreaVector, m_wasCapturingPoint ? ANYWHERE : BEHIND ); if ( m_mobArea ) { const int mobClassCount = 4; static int mobClassList[ mobClassCount ] = { TF_CLASS_SCOUT, TF_CLASS_HEAVYWEAPONS, TF_CLASS_PYRO, TF_CLASS_SPY, }; m_mobLifetimeTimer.Start( 7.0f ); m_mobCountRemaining = tf_raid_mob_spawn_count.GetInt(); m_mobClass = mobClassList[ RandomInt( 0, mobClassCount-1 ) ]; DevMsg( "RAID: %3.2f: <<<< Creating mob! >>>>\n", gpGlobals->curtime ); IGameEvent* event = gameeventmanager->CreateEvent( "raid_spawn_mob" ); if ( event ) { gameeventmanager->FireEvent( event ); } } else { // couldn't spawn - try again soon StartMobTimer( 1.0f ); DevMsg( "RAID: %3.2f: No place to spawn Mob!\n", gpGlobals->curtime ); } } } #endif // 0 //-------------------------------------------------------------------------------------------------------- class CNearbyHiddenScan : public ISearchSurroundingAreasFunctor { public: CNearbyHiddenScan( void ) { m_hiddenArea = NULL; } virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) { CTFNavArea *area = (CTFNavArea *)baseArea; if ( area->GetIncursionDistance( TF_TEAM_BLUE ) < TFGameRules()->GetRaidLogic()->GetMaximumRaiderIncursionDistance() ) { // defenders can't spawn in region under raider's control return true; } if ( !area->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) && area->IsValidForWanderingPopulation() ) { // found a hidden spot m_hiddenArea = area; return false; } return true; } CTFNavArea *m_hiddenArea; }; //-------------------------------------------------------------------------------------------------------- // // Choose unpopulated sentry area nearest the invaders // CTFNavArea *CRaidLogic::SelectRaidSentryArea( void ) const { CTFNavArea *nearestSentryArea = NULL; float nearestSentryAreaIncDist = FLT_MAX; float invaderIncDist = GetMaximumRaiderIncursionDistance(); float aheadLimit = invaderIncDist + tf_raid_sentry_build_ahead_incursion.GetFloat(); float behindLimit = invaderIncDist - tf_raid_sentry_build_behind_incursion.GetFloat(); // collect a vector of alive enemies CUtlVector< CTFPlayer * > enemyVector; CollectPlayers( &enemyVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); // check for unpopulated sentry areas in the active area set for( int i=0; iGetIncursionDistance( TF_TEAM_BLUE ) > aheadLimit ) continue; if ( sentryArea->GetIncursionDistance( TF_TEAM_BLUE ) < behindLimit ) continue; // is this area 'owned' by another, active, engineer? int e; for( e=0; eIsBot() ) continue; if ( !enemyVector[e]->IsPlayerClass( TF_CLASS_ENGINEER ) ) continue; CTFBot *engineer = (CTFBot *)enemyVector[e]; if ( engineer->GetHomeArea() && engineer->GetHomeArea()->GetID() == sentryArea->GetID() ) { break; } } if ( e < enemyVector.Count() ) { // another engineer is using this area continue; } // this is an unreserved sentry area in the active area set, keep the nearest one that is ahead of the team float incDist = sentryArea->GetIncursionDistance( TF_TEAM_BLUE ); if ( incDist < nearestSentryAreaIncDist && incDist >= behindLimit ) { nearestSentryArea = sentryArea; nearestSentryAreaIncDist = incDist; } } if ( nearestSentryArea ) { // find a nearby non-visible spawn spot for the engineer to enter from CNearbyHiddenScan hide; const float hideRange = 1000.0f; SearchSurroundingAreas( nearestSentryArea, hide, hideRange ); if ( !hide.m_hiddenArea ) { DevMsg( "RAID: %3.2f: Can't find hidden area to spawn in engineer", gpGlobals->curtime ); return NULL; } // actual spawn-in, hidden area is this area's parent nearestSentryArea->SetParent( hide.m_hiddenArea ); return nearestSentryArea; } return NULL; } #if 0 //-------------------------------------------------------------------------------------------------------- void CRaidLogic::SpawnEngineers( void ) { if ( !tf_raid_spawn_engineers.GetBool() || !m_engineerSpawnTimer.IsElapsed() ) return; m_engineerSpawnTimer.Start( tf_raid_engineer_spawn_interval.GetFloat() ); int engineerCount = m_engineerCount; while( engineerCount < tf_raid_max_defense_engineers.GetInt() ) { // parent of sentry area is hidden area where engineer spawns CTFNavArea *sentryArea = SelectRaidSentryArea(); if ( !sentryArea ) { // no available areas break; } const int maxTries = 10; int tryCount; for( tryCount=0; tryCountGetParent()->GetCenter() + Vector( 0, 0, RandomFloat( 0.0f, 30.0f ) ) ); if ( bot ) { // engineer bot will move to the sentry area and build bot->SetHomeArea( sentryArea ); ++engineerCount; DevMsg( "RAID: %3.2f: Spawned engineer", gpGlobals->curtime ); break; } } if ( tryCount == maxTries ) { DevMsg( "RAID: %3.2f: Can't spawn engineer", gpGlobals->curtime ); break; } } } //-------------------------------------------------------------------------------------------------------- void CRaidLogic::SpawnSpecials( CUtlVector< CTFNavArea * > *spawnAheadVector, CUtlVector< CTFNavArea * > *spawnAnywhereVector ) { // spawn specials if ( tf_raid_spawn_specials.GetBool() && m_specialSpawnTimer.HasStarted() && m_specialSpawnTimer.IsElapsed() ) { // time to add in a "special" m_specialSpawnTimer.Start( RandomFloat( tf_raid_special_spawn_min_interval.GetFloat(), tf_raid_special_spawn_max_interval.GetFloat() ) ); DevMsg( "RAID: %3.2f: <<<< Spawning Special >>>>\n", gpGlobals->curtime ); const int specialClassCount = 8; int availableSpecialClassList[ specialClassCount ]; int availableCount = 0; if ( m_sniperCount < tf_raid_max_defense_snipers.GetInt() ) { // increased chance of a sniper availableSpecialClassList[ availableCount++ ] = TF_CLASS_SNIPER; availableSpecialClassList[ availableCount++ ] = TF_CLASS_SNIPER; } if ( m_demomanCount < tf_raid_max_defense_demomen.GetInt() ) { availableSpecialClassList[ availableCount++ ] = TF_CLASS_DEMOMAN; } if ( m_heavyCount < tf_raid_max_defense_heavies.GetInt() ) { availableSpecialClassList[ availableCount++ ] = TF_CLASS_HEAVYWEAPONS; } if ( m_soldierCount < tf_raid_max_defense_soldiers.GetInt() ) { availableSpecialClassList[ availableCount++ ] = TF_CLASS_SOLDIER; } if ( m_pyroCount < tf_raid_max_defense_pyros.GetInt() ) { availableSpecialClassList[ availableCount++ ] = TF_CLASS_PYRO; } if ( m_spyCount < tf_raid_max_defense_spies.GetInt() ) { availableSpecialClassList[ availableCount++ ] = TF_CLASS_SPY; } if ( availableCount == 0 ) { // nothing to spawn DevMsg( "RAID: %3.2f: All specials in play already.\n", gpGlobals->curtime ); return; } int whichClass = availableSpecialClassList[ RandomInt( 0, availableCount-1 ) ]; if ( whichClass == TF_CLASS_SNIPER ) { CTFNavArea *homeArea = FindSniperSpawn(); if ( homeArea ) { // actual spawn-in, hidden area is this area's parent for( int tryCount=0; tryCount<10; ++tryCount ) { CTFBot *bot = SpawnRedTFBot( whichClass, homeArea->GetParent()->GetCenter() + Vector( 0, 0, RandomFloat( 0.0f, 30.0f ) ) ); if ( bot ) { // Bot will move to his home area to do his business bot->SetHomeArea( homeArea ); return; } } } } else if ( spawnAheadVector && spawnAheadVector->Count() > 0 ) { CTFNavArea *where = spawnAheadVector->Element( RandomInt( 0, spawnAheadVector->Count()-1 ) ); CTFBot *bot = SpawnRedTFBot( whichClass, where->GetCenter() + Vector( 0, 0, StepHeight ) ); if ( bot ) { bot->SetHomeArea( where ); return; } } // failed to create special - try again soon m_specialSpawnTimer.Start( RandomFloat( 1.0f, 2.0f ) ); DevMsg( "RAID: %3.2f: Failed to spawn Special.\n", gpGlobals->curtime ); } } #endif // 0 //-------------------------------------------------------------------------------------------------------- void CRaidLogic::CullObsoleteEnemies( float minIncursion, float maxIncursion ) { return; // cull wanderers outside of the active area set - use slightly larger range to avoid thrashing CTeam *defenseTeam = GetGlobalTeam( TF_TEAM_RED ); float cullMinIncursionDistance = minIncursion - 1.1f * tf_populator_active_buffer_range.GetFloat(); float cullMaxIncursionDistance = maxIncursion + 1.1f * tf_populator_active_buffer_range.GetFloat(); for( int i=0; iGetNumPlayers(); ++i ) { if ( !defenseTeam->GetPlayer(i)->IsBot() ) continue; CTFBot *defender = (CTFBot *)defenseTeam->GetPlayer(i); if ( !defender->IsAlive() ) continue; CTFNavArea *defenderArea = (CTFNavArea *)defender->GetLastKnownArea(); if ( defenderArea == NULL ) { // bad placement, underground most likely Unspawn( defender ); } else if ( !defender->IsMoving() || defender->GetLocomotionInterface()->IsStuck() ) { if ( defenderArea->GetIncursionDistance( TF_TEAM_BLUE ) < cullMinIncursionDistance || defenderArea->GetIncursionDistance( TF_TEAM_BLUE ) > cullMaxIncursionDistance ) { if ( Unspawn( defender ) ) { if ( !defender->HasAttribute( CTFBot::AGGRESSIVE ) ) { defenderArea->AddToWanderCount( 1 ); } } } } } // aggressively cull wanderers if we need to spawn a mob and have no room if ( IsMobSpawning() && GetAvailableRedSpawnSlots() <= 0 ) { // try to make room by removing an unseen wanderer for( int i=0; iGetNumPlayers(); ++i ) { if ( !defenseTeam->GetPlayer(i)->IsBot() ) continue; CTFBot *defender = (CTFBot *)defenseTeam->GetPlayer(i); if ( !defender->IsAlive() ) continue; // only cull Scouts if ( !defender->IsPlayerClass( TF_CLASS_SCOUT ) ) continue; // don't cull mob rushers if ( defender->HasAttribute( CTFBot::AGGRESSIVE ) ) continue; // try to open up a slot if ( Unspawn( defender ) ) { if ( defender->GetLastKnownArea() ) { defender->GetLastKnownArea()->AddToWanderCount( 1 ); } break; } } } } //-------------------------------------------------------------------------------------------------------- class CShowEscapeRoute : public ISearchSurroundingAreasFunctor { public: virtual bool operator() ( CNavArea *baseArea, CNavArea *priorArea, float travelDistanceSoFar ) { CTFNavArea *area = (CTFNavArea *)baseArea; if ( area->HasAttributeTF( TF_NAV_ESCAPE_ROUTE ) ) { area->DrawFilled( 100, 255, 255, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, false ); } else if ( area->HasAttributeTF( TF_NAV_ESCAPE_ROUTE_VISIBLE ) ) { area->DrawFilled( 100, 200, 100, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER, false ); } return true; } virtual bool ShouldSearch( CNavArea *adjArea, CNavArea *currentArea, float travelDistanceSoFar ) { return travelDistanceSoFar < 3000.0f; } }; //-------------------------------------------------------------------------------------------------------- void CRaidLogic::DrawDebugDisplay( float deltaT ) { // avoid Warning() spam from UTIL_GetListenServerHost when on a dedicated server if ( engine->IsDedicatedServer() ) return; CBasePlayer *player = UTIL_GetListenServerHost(); if ( player == NULL ) return; if ( tf_raid_debug_director.GetBool() ) { NDebugOverlay::ScreenText( 0.01f, 0.5f, CFmtStr( "Mob timer: %3.2f", m_mobSpawnTimer.GetRemainingTime() ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.51f, CFmtStr( "Wanderers: %d", m_wandererCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.52f, CFmtStr( "Engineers: %d", m_engineerCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.53f, CFmtStr( "Demomen: %d", m_demomanCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.54f, CFmtStr( "Heavies: %d", m_heavyCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.55f, CFmtStr( "Soldiers: %d", m_soldierCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.56f, CFmtStr( "Pyros: %d", m_pyroCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.57f, CFmtStr( "Spies: %d", m_spyCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.58f, CFmtStr( "Snipers: %d", m_sniperCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.59f, CFmtStr( "Squads: %d", m_squadCount ), 255, 255, 0, 255, 0.33f ); NDebugOverlay::ScreenText( 0.01f, 0.60f, CFmtStr( "Raider max inc range: %3.2f", GetMaximumRaiderIncursionDistance() ), 255, 255, 0, 255, 0.33f ); } if ( tf_raid_show_escape_route.GetInt() == 2 ) { if ( m_escapeRouteVector.Count() >= 2 ) { int anchor=0; float closeRangeSq = FLT_MAX; for( int i=0; iGetCenter() - player->GetAbsOrigin() ).LengthSqr(); if ( rangeSq < closeRangeSq ) { closeRangeSq = rangeSq; anchor = i; } } int lo = MAX( 0, anchor-20 ); int hi = MIN( m_escapeRouteVector.Count(), anchor+20 ); for( int i=lo; iGetCenter(), m_escapeRouteVector[i+1]->GetCenter(), 5.0f, 255, 0, 0, 255, true, deltaT ); NDebugOverlay::Text( m_escapeRouteVector[i]->GetCenter(), CFmtStr( "%d", i ), true, deltaT ); } } } else if ( tf_raid_show_escape_route.GetInt() == 1 ) { CShowEscapeRoute show; SearchSurroundingAreas( player->GetLastKnownArea(), show ); } if ( tf_raid_debug_sentry_placement.GetBool() ) { for( int i=0; iDrawFilled( 255, 155, 0, 255, deltaT, true ); } } } //-------------------------------------------------------------------------------------------------------- void CRaidLogic::Update( void ) { VPROF_BUDGET( "CRaidLogic::Update", "Game" ); const float deltaT = 0.33f; SetNextThink( gpGlobals->curtime + deltaT ); if ( !TFGameRules()->IsRaidMode() ) return; DrawDebugDisplay( deltaT ); CTeam *raidingTeam = GetGlobalTeam( TF_TEAM_BLUE ); // total hack for testing mini-bosses if ( m_miniBossIndex == 0 && m_miniBossHomeVector.Count() > 0 ) { CTFNavArea *bossHomeArea = m_miniBossHomeVector[0]; CBaseEntity *miniBoss = CreateEntityByName( "bot_boss_mini_rockets" ); if ( miniBoss ) { miniBoss->SetAbsOrigin( bossHomeArea->GetCenter() ); miniBoss->SetOwnerEntity( NULL ); DispatchSpawn( miniBoss ); } ++m_miniBossIndex; } else if ( m_miniBossIndex == 1 && m_miniBossHomeVector.Count() > 1 ) { if ( gEntList.FindEntityByClassname( NULL, "bot_boss_mini_rockets" ) == NULL ) { CTFNavArea *bossHomeArea = m_miniBossHomeVector[1]; CBaseEntity *miniBoss = CreateEntityByName( "bot_boss_mini_nuker" ); if ( miniBoss ) { miniBoss->SetAbsOrigin( bossHomeArea->GetCenter() ); miniBoss->SetOwnerEntity( NULL ); DispatchSpawn( miniBoss ); } ++m_miniBossIndex; } } if ( IsWaitingForRaidersToLeaveSafeRoom() ) { // has anyone left? for( int i=0; iGetNumPlayers(); ++i ) { CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i); if ( !player->IsAlive() || !player->GetLastKnownArea() ) continue; // don't start until a HUMAN leaves the safe room if ( player->IsBot() ) continue; CTFNavArea *area = (CTFNavArea *)player->GetLastKnownArea(); if ( !area->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE ) ) { // this Raider has left the spawn room - game on! m_isWaitingForRaidersToLeaveSpawnRoom = false; StartMobTimer( RandomFloat( 0.5f * tf_raid_mob_spawn_min_interval.GetFloat(), tf_raid_mob_spawn_max_interval.GetFloat() ) ); m_specialSpawnTimer.Start( RandomFloat( 0.0f, tf_raid_special_spawn_min_interval.GetFloat() ) ); DevMsg( "RAID: %3.2f: Raiders left the spawn room!\n", gpGlobals->curtime ); } } } else { int aliveCount = 0; for( int i=0; iGetNumPlayers(); ++i ) { CTFPlayer *player = ToTFPlayer( raidingTeam->GetPlayer(i) ); CTFBot *bot = ToTFBot( player ); if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) continue; if ( player->IsAlive() ) { CTFNavArea *area = (CTFNavArea *)player->GetLastKnownArea(); if ( !area || !area->HasAttributeTF( TF_NAV_RESCUE_CLOSET ) ) { // only count raiders not in a rescue closet ++aliveCount; } } } if ( m_priorRaiderAliveCount < 0 ) { // just left the safe room if ( m_didFailLastTime ) { TFGameRules()->BroadcastSound( 255, "Announcer.DontFailAgain" ); } else { TFGameRules()->BroadcastSound( 255, "Announcer.AM_GameStarting04" ); // "Let the games begin!" } } else { if ( m_priorRaiderAliveCount > aliveCount ) { // somebody died, warn the team switch( aliveCount ) { case 3: TFGameRules()->BroadcastSound( 255, "Announcer.RoundEnds3seconds" ); break; case 2: TFGameRules()->BroadcastSound( 255, "Announcer.RoundEnds2seconds" ); break; case 1: TFGameRules()->BroadcastSound( 255, "Announcer.AM_LastManAlive01" ); break; } } else if ( m_priorRaiderAliveCount < aliveCount ) { // someone has joined/respawned, let the team know switch( aliveCount ) { case 4: TFGameRules()->BroadcastSound( 255, "Announcer.RoundEnds4seconds" ); break; case 3: TFGameRules()->BroadcastSound( 255, "Announcer.RoundEnds3seconds" ); break; case 2: TFGameRules()->BroadcastSound( 255, "Announcer.RoundEnds2seconds" ); break; } } } m_priorRaiderAliveCount = aliveCount; if ( TFGameRules() && !TFGameRules()->IsInWaitingForPlayers() ) { // if all of the raiders die, they lose if ( aliveCount == 0 ) { CTeamplayRoundBasedRules *pRules = dynamic_cast( GameRules() ); if ( pRules ) { pRules->SetWinningTeam( TF_TEAM_RED, WINREASON_OPPONENTS_DEAD ); } } } } // have the raiders begun capturing the next point? CTeamControlPoint *ctrlPoint = GetContestedPoint(); bool isCapturingPoint = ( ctrlPoint && ctrlPoint->GetTeamCapPercentage( TF_TEAM_BLUE ) ); // find maximum incursion distance if ( m_incursionDistanceAtEnd < 0.0f ) { for( int i=0; iGetIncursionDistance( TF_TEAM_BLUE ) > m_incursionDistanceAtEnd ) { m_incursionDistanceAtEnd = area->GetIncursionDistance( TF_TEAM_BLUE ); } } } // find incursion range that surrounds raider team float minIncursion = FLT_MAX, maxIncursion = -FLT_MAX; m_farthestAlongRaider = NULL; CTFPlayer *capturer = NULL; int i; for( i=0; iGetNumPlayers(); ++i ) { CTFPlayer *player = (CTFPlayer *)raidingTeam->GetPlayer(i); if ( !player->IsAlive() || !player->GetLastKnownArea() ) continue; CTFBot *bot = ToTFBot( player ); if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) continue; if ( player->IsCapturingPoint() ) { capturer = player; } CTFNavArea *area = (CTFNavArea *)player->GetLastKnownArea(); float myIncursion = area->GetIncursionDistance( TF_TEAM_BLUE ); if ( maxIncursion < myIncursion ) { maxIncursion = myIncursion; m_farthestAlongRaider = player; } if ( minIncursion > myIncursion ) { minIncursion = myIncursion; } } m_farthestAlongEscapeRouteArea = NULL; if ( !m_farthestAlongRaider ) return; // watch for point capture events if ( !m_wasCapturingPoint && isCapturingPoint ) { DevMsg( "RAID: %3.2f: Point capture STARTED!\n", gpGlobals->curtime ); // UTIL_CenterPrintAll( CFmtStr( "*** %s started capturing the point! ***", capturer->GetPlayerName() ) ); // spawn a mob immediately StartMobTimer( 0.1f ); } else if ( m_wasCapturingPoint && !isCapturingPoint ) { DevMsg( "RAID: %3.2f: Point capture STOPPED!\n", gpGlobals->curtime ); } m_wasCapturingPoint = isCapturingPoint; // track escape route area nearest leader for( i=0; iGetIncursionDistance( TF_TEAM_BLUE ) <= GetMaximumRaiderIncursionDistance() ) { if ( m_farthestAlongEscapeRouteArea ) { if ( m_farthestAlongEscapeRouteArea->GetIncursionDistance( TF_TEAM_BLUE ) < area->GetIncursionDistance( TF_TEAM_BLUE ) ) { m_farthestAlongEscapeRouteArea = area; } } else { m_farthestAlongEscapeRouteArea = area; } } } if ( tf_raid_debug_escape_route.GetBool() && m_farthestAlongEscapeRouteArea ) { m_farthestAlongEscapeRouteArea->DrawFilled( 255, 100, 0, 255, NDEBUG_PERSIST_TILL_NEXT_SERVER ); } CullObsoleteEnemies( minIncursion, maxIncursion ); // count defensive types CTeam *defenseTeam = GetGlobalTeam( TF_TEAM_RED ); m_wandererCount = 0; m_engineerCount = 0; m_demomanCount = 0; m_heavyCount = 0; m_soldierCount = 0; m_pyroCount = 0; m_spyCount = 0; m_sniperCount = 0; m_squadCount = 0; CUtlVector< CTFBotSquad * > m_squadVector; for( i=0; iGetNumPlayers(); ++i ) { if ( !defenseTeam->GetPlayer(i)->IsBot() ) continue; CTFBot *defender = (CTFBot *)defenseTeam->GetPlayer(i); if ( !defender->IsAlive() ) continue; if ( defender->GetSquad() ) { if ( m_squadVector.Find( defender->GetSquad() ) == m_squadVector.InvalidIndex() ) { m_squadVector.AddToTail( defender->GetSquad() ); ++m_squadCount; } continue; } if ( defender->IsPlayerClass( TF_CLASS_ENGINEER ) ) { ++m_engineerCount; } else if ( defender->IsPlayerClass( TF_CLASS_DEMOMAN ) ) { ++m_demomanCount; } else if ( defender->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) { ++m_heavyCount; } else if ( defender->IsPlayerClass( TF_CLASS_SOLDIER ) ) { ++m_soldierCount; } else if ( defender->IsPlayerClass( TF_CLASS_PYRO ) ) { ++m_pyroCount; } else if ( defender->IsPlayerClass( TF_CLASS_SPY ) ) { ++m_spyCount; } else if ( defender->IsPlayerClass( TF_CLASS_SNIPER ) ) { ++m_sniperCount; } else if ( defender->IsPlayerClass( TF_CLASS_SCOUT ) ) { if ( defender->HasAttribute( CTFBot::AGGRESSIVE ) ) continue; ++m_wandererCount; } } if ( tf_raid_spawn_enable.GetBool() == false ) return; #if 0 // populate wanderers CPopulator populator( maxIncursion, tf_raid_max_wanderers.GetInt() - m_wandererCount ); SearchSurroundingAreas( m_farthestAlongRaider->GetLastKnownArea(), populator ); // if raiders are capturing a point, spawn mobs at a faster rate if ( isCapturingPoint && m_mobSpawnTimer.GetRemainingTime() > tf_raid_capture_mob_interval.GetFloat() ) { StartMobTimer( tf_raid_capture_mob_interval.GetFloat() - 0.1f ); } SpawnMobs( &populator.m_hiddenAreaVector ); SpawnSpecials( &populator.m_hiddenAreaAheadVector, &populator.m_hiddenAreaVector ); SpawnEngineers(); // emit mob if ( IsMobSpawning() && m_mobCountRemaining && m_mobArea && tf_raid_spawn_mobs.GetBool() ) { if ( m_mobArea->HasAttributeTF( TF_NAV_NO_SPAWNING ) ) { // the mob is trying to spawn from an area that doesn't have room to spawn in bots, pick another DevMsg( "RAID: %3.2f: Mob attempting to spawn where there isn't room - retrying\n", gpGlobals->curtime ); m_mobArea = SelectMobSpawn( &populator.m_hiddenAreaVector, BEHIND ); if ( !m_mobArea ) { DevMsg( "RAID: %3.2f: Can't find a mob spawn area!\n", gpGlobals->curtime ); // try again soon StartMobTimer( 2.0f ); } } if ( m_mobArea ) { if ( SpawnRedTFBot( m_mobClass, m_mobArea->GetCenter() + Vector( 0, 0, StepHeight ), IS_MOB_RUSHER ) ) { --m_mobCountRemaining; DevMsg( "RAID: %3.2f: Spawned mob member, %d to go\n", gpGlobals->curtime, m_mobCountRemaining ); } } } #endif // 0 // block/unblock capture point gate doors // TODO: Do this more efficiently for( i=0; iForAllAreasOverlappingExtent( overlapAreas, doorExtent ); for( int b=0; bm_toggle_state == TS_AT_TOP ) { // open, not blocked area->ClearAttributeTF( TF_NAV_BLOCKED ); } else { // not open, blocked area->SetAttributeTF( TF_NAV_BLOCKED ); } } } } //-------------------------------------------------------------------------------------------------------- float CRaidLogic::GetMaximumRaiderIncursionDistance( void ) const { if ( m_farthestAlongRaider == NULL ) return 0.0f; CTFNavArea *area = (CTFNavArea *)m_farthestAlongRaider->GetLastKnownArea(); return area ? area->GetIncursionDistance( TF_TEAM_BLUE ) : 0.0f; } //-------------------------------------------------------------------------------------------------------- /** * Given a viewing area, return the earliest escape route area near the team that is visible */ CTFNavArea *CRaidLogic::FindEarliestVisibleEscapeRouteAreaNearTeam( CTFNavArea *viewArea ) const { if ( viewArea == NULL || m_farthestAlongEscapeRouteArea == NULL ) return NULL; const float nearIncursionRange = 1000.0f; const float minIncursion = GetMaximumRaiderIncursionDistance() - nearIncursionRange; const float maxIncursion = GetMaximumRaiderIncursionDistance() + nearIncursionRange; NavAreaCollector collector; viewArea->ForAllCompletelyVisibleAreas( collector ); CTFNavArea *firstArea = NULL; float firstAreaIncursion = FLT_MAX; for( int i=0; iGetIncursionDistance( TF_TEAM_BLUE ); if ( area->HasAttributeTF( TF_NAV_ESCAPE_ROUTE ) && areaIncursion > minIncursion && areaIncursion < maxIncursion ) { if ( tf_debug_sniper_spots.GetBool() ) { area->DrawFilled( 255, 0, 255, 255, 120.0f ); } /* float rangeSq = ( viewArea->GetCenter() - area->GetCenter() ).LengthSqr(); if ( firstAreaIncursion > rangeSq ) { // closest firstAreaIncursion = rangeSq; firstArea = area; } */ if ( firstAreaIncursion > areaIncursion ) { firstArea = area; firstAreaIncursion = areaIncursion; } } } return firstArea; } //-------------------------------------------------------------------------------------------------------- /** * Return area where sniper should take up a vantage point. That area's parent will be * the actual spawn-in area not currently visible to the team. */ CTFNavArea *CRaidLogic::FindSniperSpawn( void ) { CUtlVector< CTFNavArea * > validAreas; float aheadLimit = GetMaximumRaiderIncursionDistance() + tf_raid_sniper_spawn_ahead_incursion.GetFloat(); float behindLimit = GetMaximumRaiderIncursionDistance() - tf_raid_sniper_spawn_behind_incursion.GetFloat(); float tooCloseLimitSq = tf_raid_sniper_spawn_min_range.GetFloat(); tooCloseLimitSq *= tooCloseLimitSq; for( int i=0; iGetIncursionDistance( TF_TEAM_BLUE ) < 0.0f ) continue; if ( sniperArea->GetIncursionDistance( TF_TEAM_BLUE ) > aheadLimit ) continue; if ( sniperArea->GetIncursionDistance( TF_TEAM_BLUE ) < behindLimit ) continue; // make sure no Raider is too close CClosestTFPlayer close( sniperArea->GetCenter(), TF_TEAM_BLUE ); ForEachPlayer( close ); if ( close.m_closePlayer && close.m_closeRangeSq < tooCloseLimitSq ) continue; // this is often too restrictive - we really want "potentially visible to region near team" if ( !sniperArea->IsPotentiallyVisibleToTeam( TF_TEAM_BLUE ) ) { if ( tf_debug_sniper_spots.GetBool() ) sniperArea->DrawFilled( 100, 100, 100, 255, 99999.9f ); continue; } validAreas.AddToTail( sniperArea ); } if ( validAreas.Count() ) { // choose a specific sniper spot CTFNavArea *sniperArea = validAreas[ RandomInt( 0, validAreas.Count()-1 ) ]; // find a nearby non-visible spawn spot CNearbyHiddenScan hide; const float hideRange = 1000.0f; SearchSurroundingAreas( sniperArea, hide, hideRange ); if ( hide.m_hiddenArea ) { sniperArea->SetParent( hide.m_hiddenArea ); if ( tf_debug_sniper_spots.GetBool() ) { const float deltaT = 999999.9f; hide.m_hiddenArea->DrawFilled( 0, 0, 255, 255, deltaT ); NDebugOverlay::HorzArrow( hide.m_hiddenArea->GetCenter() + Vector( 0, 0, 10.0f ), sniperArea->GetCenter() + Vector( 0, 0, 10.0f ), 5.0f, 255, 255, 0, 255, true, deltaT ); sniperArea->DrawFilled( 255, 0, 0, 255, deltaT ); for( int i=0; iDrawFilled( 0, 255, 0, 255, 99999.9f ); } } return sniperArea; } } return NULL; } //-------------------------------------------------------------------------------------------------------- /** * Return a good area for a Sentry Gun, and return a hidden area for the engineer * to spawn as the parent of that area. */ CTFNavArea *CRaidLogic::FindSentryArea( void ) { CUtlVector< CTFNavArea * > validAreas; float aheadLimit = GetMaximumRaiderIncursionDistance() + tf_raid_sentry_build_ahead_incursion.GetFloat(); float behindLimit = GetMaximumRaiderIncursionDistance() - tf_raid_sentry_build_behind_incursion.GetFloat(); for( int i=0; iGetIncursionDistance( TF_TEAM_BLUE ) < 0.0f ) continue; if ( sentryArea->GetIncursionDistance( TF_TEAM_BLUE ) > aheadLimit ) continue; if ( sentryArea->GetIncursionDistance( TF_TEAM_BLUE ) < behindLimit ) continue; // don't use this area if it already has a sentry in it if ( TheTFNavMesh()->IsSentryGunHere( sentryArea ) ) continue; validAreas.AddToTail( sentryArea ); } if ( validAreas.Count() ) { // choose a specific sentry spot CTFNavArea *sentryArea = validAreas[ RandomInt( 0, validAreas.Count()-1 ) ]; // find a nearby non-visible spawn spot for the engineer CNearbyHiddenScan hide; const float hideRange = 1000.0f; SearchSurroundingAreas( sentryArea, hide, hideRange ); if ( hide.m_hiddenArea ) { sentryArea->SetParent( hide.m_hiddenArea ); return sentryArea; } } return NULL; } //--------------------------------------------------------------------------------------------- // Pick a member of the raiding (blue) team for a red defender to attack CTFPlayer *CRaidLogic::SelectRaiderToAttack( void ) { CTeam *invaderTeam = GetGlobalTeam( TF_TEAM_BLUE ); // attack point cappers first CUtlVector< CTFPlayer * > victimVector; int i; for( i=0; iGetNumPlayers(); ++i ) { CTFPlayer *player = (CTFPlayer *)invaderTeam->GetPlayer(i); if ( player->IsAlive() && player->IsCapturingPoint() ) victimVector.AddToTail( player ); } if ( victimVector.Count() == 0 ) { // pick a random living Raider for( i=0; iGetNumPlayers(); ++i ) { CTFPlayer *player = (CTFPlayer *)invaderTeam->GetPlayer(i); CTFBot *bot = ToTFBot( player ); if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) ) continue; if ( player->IsAlive() ) victimVector.AddToTail( player ); } } if ( victimVector.Count() ) { return victimVector[ RandomInt( 0, victimVector.Count()-1 ) ]; } return NULL; } //-------------------------------------------------------------------------------------------------------- // Return entity positioned within next valid rescue closet area for to respawn players in CBaseEntity *CRaidLogic::GetRescueRespawn( void ) const { if ( g_internalSpawnPoint == NULL ) { g_internalSpawnPoint = (CPopulatorInternalSpawnPoint *)CreateEntityByName( "populator_internal_spawn_point" ); g_internalSpawnPoint->Spawn(); } float limit = GetMaximumRaiderIncursionDistance() - 500.0f; CTFNavArea *rescueArea = NULL; float rescueFlow = FLT_MAX; for( int i=0; iGetIncursionDistance( TF_TEAM_BLUE ); if ( flow > limit && flow < rescueFlow ) { rescueArea = m_rescueClosetVector[i]; rescueFlow = flow; } } if ( rescueArea ) { g_internalSpawnPoint->SetAbsOrigin( rescueArea->GetCenter() ); g_internalSpawnPoint->SetLocalAngles( vec3_angle ); return g_internalSpawnPoint; } return NULL; } //-------------------------------------------------------------------------------------------------------- class CMarkEscapeRoute { public: CMarkEscapeRoute( CUtlVector< CTFNavArea * > *escapeRouteVector ) { m_escapeRouteVector = escapeRouteVector; } void operator() ( CNavArea *baseArea ) { CTFNavArea *area = (CTFNavArea *)baseArea; if ( !m_escapeRouteVector->HasElement( area ) ) { area->SetAttributeTF( TF_NAV_ESCAPE_ROUTE ); m_escapeRouteVector->AddToTail( area ); } } CUtlVector< CTFNavArea * > *m_escapeRouteVector; }; //-------------------------------------------------------------------------------------------------------- class CMarkEscapeRouteVisible { public: bool operator() ( CNavArea *baseArea ) { CTFNavArea *area = (CTFNavArea *)baseArea; area->SetAttributeTF( TF_NAV_ESCAPE_ROUTE_VISIBLE ); return true; } }; //--------------------------------------------------------------------------------------------- // Locate and store escape route void CRaidLogic::BuildEscapeRoute( void ) { // find blue spawn room CBaseEntity *entity = NULL; for ( int i=0; i( IFuncRespawnRoomAutoList::AutoList()[i] ); entity = respawnRoom; if ( respawnRoom->GetActive() && respawnRoom->GetTeamNumber() == TF_TEAM_BLUE ) { break; } } if ( entity ) { // respawn room absorigin is 0,0,0 - have to use extent Extent extent; extent.Init( entity ); Vector safeRoomPos = ( extent.lo + extent.hi ) / 2; DevMsg( "RAID: Blue spawn room at (%g, %g, %g)\n", safeRoomPos.x, safeRoomPos.y, safeRoomPos.z ); // assume path_track nearest the blue spawn is the start of the escape route // according to the mappers, previous pointers do not contain useful data CPathTrack *pathNode = NULL; CPathTrack *firstPathNode = NULL; float closeRangeSq = FLT_MAX; for( pathNode = dynamic_cast< CPathTrack * >( gEntList.FindEntityByClassname( pathNode, "path_track" ) ); pathNode; pathNode = dynamic_cast< CPathTrack * >( gEntList.FindEntityByClassname( pathNode, "path_track" ) ) ) { float rangeSq = ( pathNode->GetAbsOrigin() - safeRoomPos ).LengthSqr(); if ( rangeSq < closeRangeSq ) { closeRangeSq = rangeSq; firstPathNode = pathNode; } } if ( firstPathNode ) { CUtlVector< CTFNavArea * > pathTrackAreaVector; for( pathNode = firstPathNode; pathNode; pathNode = pathNode->GetNext() ) { CTFNavArea *pathArea = (CTFNavArea *)TheNavMesh->GetNavArea( pathNode->GetAbsOrigin() ); if ( pathArea ) { if ( !pathTrackAreaVector.HasElement( pathArea ) ) { pathTrackAreaVector.AddToTail( pathArea ); } } } if ( pathTrackAreaVector.Count() > 1 ) { // build contiguous escape route area vector m_escapeRouteVector.RemoveAll(); CMarkEscapeRoute markAsEscapeRoute( &m_escapeRouteVector ); for( int i=1; iForAllAreasAlongLine( markAsEscapeRoute, pathTrackAreaVector[i-1], pathTrackAreaVector[i] ); } // flag all areas that can see the escape route as escape route 'visible' for( int i=0; iForAllCompletelyVisibleAreas( markAsEscapeRouteVisible ); } } } } } //--------------------------------------------------------------------------------------------- // // Return the next control point that can be captured // CTeamControlPoint *CRaidLogic::GetContestedPoint( void ) const { CTeamControlPointMaster *pMaster = g_hControlPointMasters.Count() ? g_hControlPointMasters[0] : NULL; if ( pMaster ) { for( int i=0; iGetNumPoints(); ++i ) { CTeamControlPoint *point = pMaster->GetControlPoint( i ); if ( point && pMaster->IsInRound( point ) ) { if ( ObjectiveResource()->GetOwningTeam( point->GetPointIndex() ) == TF_TEAM_BLUE ) continue; // blue are the invaders if ( !TeamplayGameRules()->TeamMayCapturePoint( TF_TEAM_BLUE, point->GetPointIndex() ) ) continue; return point; } } } return NULL; } #endif // TF_RAID_MODE