#include "cbase.h" #include "asw_spawn_manager.h" #include "asw_marine.h" #include "asw_marine_resource.h" #include "asw_game_resource.h" #include "iasw_spawnable_npc.h" #include "asw_player.h" #include "ai_network.h" #include "ai_waypoint.h" #include "ai_node.h" #include "asw_director.h" #include "asw_util_shared.h" #include "asw_path_utils.h" #include "asw_trace_filter_doors.h" #include "asw_objective_escape.h" #include "triggers.h" #include "datacache/imdlcache.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" CASW_Spawn_Manager g_Spawn_Manager; CASW_Spawn_Manager* ASWSpawnManager() { return &g_Spawn_Manager; } #define CANDIDATE_ALIEN_HULL 11 // TODO: have this use the hull of the alien type we're spawning a horde of? #define MARINE_NEAR_DISTANCE 740.0f extern ConVar asw_director_debug; ConVar asw_horde_min_distance("asw_horde_min_distance", "800", FCVAR_CHEAT, "Minimum distance away from the marines the horde can spawn" ); ConVar asw_horde_max_distance("asw_horde_max_distance", "1500", FCVAR_CHEAT, "Maximum distance away from the marines the horde can spawn" ); ConVar asw_max_alien_batch("asw_max_alien_batch", "10", FCVAR_CHEAT, "Max number of aliens spawned in a horde batch" ); ConVar asw_batch_interval("asw_batch_interval", "5", FCVAR_CHEAT, "Time between successive batches spawning in the same spot"); ConVar asw_candidate_interval("asw_candidate_interval", "1.0", FCVAR_CHEAT, "Interval between updating candidate spawning nodes"); ConVar asw_horde_class( "asw_horde_class", "asw_drone", FCVAR_CHEAT, "Alien class used when spawning hordes" ); // TODO: Notify the director when a horde is all killed? // - currently you could try to spawn a 2nd horde while the first is still sitting out in the world CASW_Spawn_Manager::CASW_Spawn_Manager() { } CASW_Spawn_Manager::~CASW_Spawn_Manager() { } // ================================== // == Master list of alien classes == // ================================== // NOTE: If you add new entries to this list, update the asw_spawner choices in swarm.fgd. // Do not rearrange the order or you will be changing what spawns in all the maps. ASW_Alien_Class_Entry g_Aliens[]= { ASW_Alien_Class_Entry( "asw_drone", HULL_MEDIUMBIG ), ASW_Alien_Class_Entry( "asw_buzzer", HULL_TINY_CENTERED ), ASW_Alien_Class_Entry( "asw_parasite", HULL_TINY ), ASW_Alien_Class_Entry( "asw_shieldbug", HULL_LARGE ), ASW_Alien_Class_Entry( "asw_grub", HULL_TINY ), ASW_Alien_Class_Entry( "asw_drone_jumper", HULL_MEDIUMBIG ), ASW_Alien_Class_Entry( "asw_harvester", HULL_HUMAN ), ASW_Alien_Class_Entry( "asw_parasite_defanged", HULL_TINY ), ASW_Alien_Class_Entry( "asw_queen", HULL_TINY ), ASW_Alien_Class_Entry( "asw_boomer", HULL_HUMAN ), ASW_Alien_Class_Entry( "asw_ranger", HULL_HUMAN ), ASW_Alien_Class_Entry( "asw_mortarbug", HULL_LARGE ), ASW_Alien_Class_Entry( "asw_shaman", HULL_LARGE ), }; // Array indices of drones. Used by carnage mode. const int g_nDroneClassEntry = 0; const int g_nDroneJumperClassEntry = 5; int CASW_Spawn_Manager::GetNumAlienClasses() { return NELEMS( g_Aliens ); } ASW_Alien_Class_Entry* CASW_Spawn_Manager::GetAlienClass( int i ) { Assert( i >= 0 && i < NELEMS( g_Aliens ) ); return &g_Aliens[ i ]; } void CASW_Spawn_Manager::LevelInitPreEntity() { // init alien classes for ( int i = 0; i < GetNumAlienClasses(); i++ ) { GetAlienClass( i )->m_iszAlienClass = AllocPooledString( GetAlienClass( i )->m_pszAlienClass ); } } void CASW_Spawn_Manager::LevelInitPostEntity() { m_vecHordePosition = vec3_origin; m_angHordeAngle = vec3_angle; m_batchInterval.Invalidate(); m_CandidateUpdateTimer.Invalidate(); m_iHordeToSpawn = 0; m_iAliensToSpawn = 0; m_northCandidateNodes.Purge(); m_southCandidateNodes.Purge(); FindEscapeTriggers(); } // finds all trigger_multiples linked to asw_objective_escape entities void CASW_Spawn_Manager::FindEscapeTriggers() { m_EscapeTriggers.Purge(); // go through all escape objectives CBaseEntity* pEntity = NULL; while ( (pEntity = gEntList.FindEntityByClassname( pEntity, "asw_objective_escape" )) != NULL ) { CASW_Objective_Escape* pObjective = dynamic_cast(pEntity); if ( !pObjective ) continue; const char *pszEscapeTargetName = STRING( pObjective->GetEntityName() ); CBaseEntity* pOtherEntity = NULL; while ( (pOtherEntity = gEntList.FindEntityByClassname( pOtherEntity, "trigger_multiple" )) != NULL ) { CTriggerMultiple *pTrigger = dynamic_cast( pOtherEntity ); if ( !pTrigger ) continue; bool bAdded = false; CBaseEntityOutput *pOutput = pTrigger->FindNamedOutput( "OnTrigger" ); if ( pOutput ) { CEventAction *pAction = pOutput->GetFirstAction(); while ( pAction ) { if ( !Q_stricmp( STRING( pAction->m_iTarget ), pszEscapeTargetName ) ) { bAdded = true; m_EscapeTriggers.AddToTail( pTrigger ); break; } pAction = pAction->m_pNext; } } if ( !bAdded ) { pOutput = pTrigger->FindNamedOutput( "OnStartTouch" ); if ( pOutput ) { CEventAction *pAction = pOutput->GetFirstAction(); while ( pAction ) { if ( !Q_stricmp( STRING( pAction->m_iTarget ), pszEscapeTargetName ) ) { bAdded = true; m_EscapeTriggers.AddToTail( pTrigger ); break; } pAction = pAction->m_pNext; } } } } } Msg("Spawn manager found %d escape triggers\n", m_EscapeTriggers.Count() ); } void CASW_Spawn_Manager::Update() { if ( m_iHordeToSpawn > 0 ) { if ( m_vecHordePosition != vec3_origin && ( !m_batchInterval.HasStarted() || m_batchInterval.IsElapsed() ) ) { int iToSpawn = MIN( m_iHordeToSpawn, asw_max_alien_batch.GetInt() ); int iSpawned = SpawnAlienBatch( asw_horde_class.GetString(), iToSpawn, m_vecHordePosition, m_angHordeAngle, MARINE_NEAR_DISTANCE ); m_iHordeToSpawn -= iSpawned; if ( m_iHordeToSpawn <= 0 ) { ASWDirector()->OnHordeFinishedSpawning(); m_vecHordePosition = vec3_origin; } else if ( iSpawned == 0 ) // if we failed to spawn any aliens, then try to find a new horde location { if ( !FindHordePosition() ) // if we failed to find a new location, just abort this horde { m_iHordeToSpawn = 0; ASWDirector()->OnHordeFinishedSpawning(); m_vecHordePosition = vec3_origin; } } m_batchInterval.Start( asw_batch_interval.GetFloat() ); } } if ( m_iAliensToSpawn > 0 ) { if ( SpawnAlientAtRandomNode() ) m_iAliensToSpawn--; } } void CASW_Spawn_Manager::AddAlien() { m_iAliensToSpawn++; } bool CASW_Spawn_Manager::SpawnAlientAtRandomNode() { UpdateCandidateNodes(); // decide if the alien is going to come from behind or in front bool bNorth = RandomFloat() < 0.7f; if ( m_northCandidateNodes.Count() <= 0 ) { bNorth = false; } else if ( m_southCandidateNodes.Count() <= 0 ) { bNorth = true; } CUtlVector &candidateNodes = bNorth ? m_northCandidateNodes : m_southCandidateNodes; if ( candidateNodes.Count() <= 0 ) return false; const char *szAlienClass = "asw_drone"; Vector vecMins, vecMaxs; GetAlienBounds( szAlienClass, vecMins, vecMaxs ); int iMaxTries = 10; for ( int i=0 ; iGetNode( candidateNodes[iChosen] ); if ( !pNode ) continue; float flDistance = 0; CASW_Marine *pMarine = dynamic_cast(UTIL_ASW_NearestMarine( pNode->GetPosition( CANDIDATE_ALIEN_HULL ), flDistance )); if ( !pMarine ) return false; // check if there's a route from this node to the marine(s) AI_Waypoint_t *pRoute = ASWPathUtils()->BuildRoute( pNode->GetPosition( CANDIDATE_ALIEN_HULL ), pMarine->GetAbsOrigin(), NULL, 100 ); if ( !pRoute ) continue; if ( bNorth && UTIL_ASW_DoorBlockingRoute( pRoute, true ) ) { DeleteRoute( pRoute ); continue; } Vector vecSpawnPos = pNode->GetPosition( CANDIDATE_ALIEN_HULL ) + Vector( 0, 0, 32 ); if ( ValidSpawnPoint( vecSpawnPos, vecMins, vecMaxs, true, MARINE_NEAR_DISTANCE ) ) { if ( SpawnAlienAt( szAlienClass, vecSpawnPos, vec3_angle ) ) { return true; } } DeleteRoute( pRoute ); } return false; } bool CASW_Spawn_Manager::AddHorde( int iHordeSize ) { m_iHordeToSpawn = iHordeSize; if ( m_vecHordePosition == vec3_origin ) { if ( !FindHordePosition() ) { Msg("Error: Failed to find horde position\n"); return false; } else { if ( asw_director_debug.GetBool() ) { NDebugOverlay::Cross3D( m_vecHordePosition, 50.0f, 255, 128, 0, true, 40.0f ); } } } return true; } CAI_Network* CASW_Spawn_Manager::GetNetwork() { return g_pBigAINet; } void CASW_Spawn_Manager::UpdateCandidateNodes() { // don't update too frequently if ( m_CandidateUpdateTimer.HasStarted() && !m_CandidateUpdateTimer.IsElapsed() ) return; m_CandidateUpdateTimer.Start( asw_candidate_interval.GetFloat() ); if ( !GetNetwork() || !GetNetwork()->NumNodes() ) { m_vecHordePosition = vec3_origin; Msg("Error: Can't spawn hordes as this map has no node network\n"); return; } CASW_Game_Resource *pGameResource = ASWGameResource(); if ( !pGameResource ) return; Vector vecSouthMarine = vec3_origin; Vector vecNorthMarine = vec3_origin; for ( int i=0;iGetMaxMarineResources();i++ ) { CASW_Marine_Resource *pMR = pGameResource->GetMarineResource(i); if ( !pMR ) continue; CASW_Marine *pMarine = pMR->GetMarineEntity(); if ( !pMarine || pMarine->GetHealth() <= 0 ) continue; if ( vecSouthMarine == vec3_origin || vecSouthMarine.y > pMarine->GetAbsOrigin().y ) { vecSouthMarine = pMarine->GetAbsOrigin(); } if ( vecNorthMarine == vec3_origin || vecNorthMarine.y < pMarine->GetAbsOrigin().y ) { vecNorthMarine = pMarine->GetAbsOrigin(); } } if ( vecSouthMarine == vec3_origin || vecNorthMarine == vec3_origin ) // no live marines return; int iNumNodes = GetNetwork()->NumNodes(); m_northCandidateNodes.Purge(); m_southCandidateNodes.Purge(); for ( int i=0 ; iGetNode( i ); if ( !pNode || pNode->GetType() != NODE_GROUND ) continue; Vector vecPos = pNode->GetPosition( CANDIDATE_ALIEN_HULL ); // find the nearest marine to this node float flDistance = 0; CASW_Marine *pMarine = dynamic_cast(UTIL_ASW_NearestMarine( vecPos, flDistance )); if ( !pMarine ) return; if ( flDistance > asw_horde_max_distance.GetFloat() || flDistance < asw_horde_min_distance.GetFloat() ) continue; // check node isn't in an exit trigger bool bInsideEscapeArea = false; for ( int d=0; dCollisionProp()->IsPointInBounds( vecPos ) ) { bInsideEscapeArea = true; break; } } if ( bInsideEscapeArea ) continue; if ( vecPos.y >= vecSouthMarine.y ) { m_northCandidateNodes.AddToTail( i ); } if ( vecPos.y <= vecNorthMarine.y ) { m_southCandidateNodes.AddToTail( i ); } } } bool CASW_Spawn_Manager::FindHordePosition() { // need to find a suitable place from which to spawn a horde // this place should: // - be far enough away from the marines so the whole horde can spawn before the marines get there // - should have a clear path to the marines UpdateCandidateNodes(); // decide if the horde is going to come from behind or in front bool bNorth = RandomFloat() < 0.7f; if ( m_northCandidateNodes.Count() <= 0 ) { bNorth = false; } else if ( m_southCandidateNodes.Count() <= 0 ) { bNorth = true; } CUtlVector &candidateNodes = bNorth ? m_northCandidateNodes : m_southCandidateNodes; if ( candidateNodes.Count() <= 0 ) return false; int iMaxTries = 10; for ( int i=0 ; iGetNode( candidateNodes[iChosen] ); if ( !pNode ) continue; float flDistance = 0; CASW_Marine *pMarine = dynamic_cast(UTIL_ASW_NearestMarine( pNode->GetPosition( CANDIDATE_ALIEN_HULL ), flDistance )); if ( !pMarine ) return false; // check if there's a route from this node to the marine(s) AI_Waypoint_t *pRoute = ASWPathUtils()->BuildRoute( pNode->GetPosition( CANDIDATE_ALIEN_HULL ), pMarine->GetAbsOrigin(), NULL, 100 ); if ( !pRoute ) continue; if ( bNorth && UTIL_ASW_DoorBlockingRoute( pRoute, true ) ) { DeleteRoute( pRoute ); continue; } m_vecHordePosition = pNode->GetPosition( CANDIDATE_ALIEN_HULL ) + Vector( 0, 0, 32 ); DeleteRoute( pRoute ); return true; } return false; } bool CASW_Spawn_Manager::LineBlockedByGeometry( const Vector &vecSrc, const Vector &vecEnd ) { trace_t tr; UTIL_TraceLine( vecSrc, vecEnd, MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr ); return ( tr.fraction != 1.0f ); } bool CASW_Spawn_Manager::GetAlienBounds( const char *szAlienClass, Vector &vecMins, Vector &vecMaxs ) { int nCount = GetNumAlienClasses(); for ( int i = 0 ; i < nCount; i++ ) { if ( !Q_stricmp( szAlienClass, GetAlienClass( i )->m_pszAlienClass ) ) { vecMins = NAI_Hull::Mins( GetAlienClass( i )->m_nHullType ); vecMaxs = NAI_Hull::Maxs (GetAlienClass( i )->m_nHullType ); return true; } } return false; } bool CASW_Spawn_Manager::GetAlienBounds( string_t iszAlienClass, Vector &vecMins, Vector &vecMaxs ) { int nCount = GetNumAlienClasses(); for ( int i = 0 ; i < nCount; i++ ) { if ( iszAlienClass == GetAlienClass( i )->m_iszAlienClass ) { vecMins = NAI_Hull::Mins( GetAlienClass( i )->m_nHullType ); vecMaxs = NAI_Hull::Maxs (GetAlienClass( i )->m_nHullType ); return true; } } return false; } // spawn a group of aliens at the target point int CASW_Spawn_Manager::SpawnAlienBatch( const char* szAlienClass, int iNumAliens, const Vector &vecPosition, const QAngle &angle, float flMarinesBeyondDist ) { int iSpawned = 0; bool bCheckGround = true; Vector vecMins = NAI_Hull::Mins(HULL_MEDIUMBIG); Vector vecMaxs = NAI_Hull::Maxs(HULL_MEDIUMBIG); GetAlienBounds( szAlienClass, vecMins, vecMaxs ); float flAlienWidth = vecMaxs.x - vecMins.x; float flAlienDepth = vecMaxs.y - vecMins.y; // spawn one in the middle if ( ValidSpawnPoint( vecPosition, vecMins, vecMaxs, bCheckGround, flMarinesBeyondDist ) ) { if ( SpawnAlienAt( szAlienClass, vecPosition, angle ) ) iSpawned++; } // try to spawn a 5x5 grid of aliens, starting at the centre and expanding outwards Vector vecNewPos = vecPosition; for ( int i=1; i<=5 && iSpawned < iNumAliens; i++ ) { // spawn aliens along top of box for ( int x=-i; x<=i && iSpawned < iNumAliens; x++ ) { vecNewPos = vecPosition; vecNewPos.x += x * flAlienWidth; vecNewPos.y -= i * flAlienDepth; if ( !LineBlockedByGeometry( vecPosition, vecNewPos) && ValidSpawnPoint( vecNewPos, vecMins, vecMaxs, bCheckGround, flMarinesBeyondDist ) ) { if ( SpawnAlienAt( szAlienClass, vecNewPos, angle ) ) iSpawned++; } } // spawn aliens along bottom of box for ( int x=-i; x<=i && iSpawned < iNumAliens; x++ ) { vecNewPos = vecPosition; vecNewPos.x += x * flAlienWidth; vecNewPos.y += i * flAlienDepth; if ( !LineBlockedByGeometry( vecPosition, vecNewPos) && ValidSpawnPoint( vecNewPos, vecMins, vecMaxs, bCheckGround, flMarinesBeyondDist ) ) { if ( SpawnAlienAt( szAlienClass, vecNewPos, angle ) ) iSpawned++; } } // spawn aliens along left of box for ( int y=-i + 1; y(pEntity); if ( pNPC ) { pNPC->AddSpawnFlags( SF_NPC_FALL_TO_GROUND ); } // Strip pitch and roll from the spawner's angles. Pass only yaw to the spawned NPC. QAngle angles = angle; angles.x = 0.0; angles.z = 0.0; pEntity->SetAbsOrigin( vecPos ); pEntity->SetAbsAngles( angles ); UTIL_DropToFloor( pEntity, MASK_SOLID ); IASW_Spawnable_NPC* pSpawnable = dynamic_cast(pEntity); ASSERT(pSpawnable); if ( !pSpawnable ) { Warning("NULL Spawnable Ent in CASW_Spawn_Manager::SpawnAlienAt! AlienClass = %s\n", szAlienClass); UTIL_Remove(pEntity); return NULL; } DispatchSpawn( pEntity ); pEntity->Activate(); // give our aliens the orders pSpawnable->SetAlienOrders(AOT_MoveToNearestMarine, vec3_origin, NULL); return pEntity; } bool CASW_Spawn_Manager::ValidSpawnPoint( const Vector &vecPosition, const Vector &vecMins, const Vector &vecMaxs, bool bCheckGround, float flMarineNearDistance ) { // check if we can fit there trace_t tr; UTIL_TraceHull( vecPosition, vecPosition + Vector( 0, 0, 1 ), vecMins, vecMaxs, MASK_NPCSOLID, NULL, COLLISION_GROUP_NONE, &tr ); if( tr.fraction != 1.0 ) return false; // check there's ground underneath this point if ( bCheckGround ) { UTIL_TraceHull( vecPosition + Vector( 0, 0, 1 ), vecPosition - Vector( 0, 0, 64 ), vecMins, vecMaxs, MASK_NPCSOLID, NULL, COLLISION_GROUP_NONE, &tr ); if( tr.fraction == 1.0 ) return false; } if ( flMarineNearDistance > 0 ) { CASW_Game_Resource* pGameResource = ASWGameResource(); float distance = 0.0f; for ( int i=0 ; i < pGameResource->GetMaxMarineResources() ; i++ ) { CASW_Marine_Resource* pMR = pGameResource->GetMarineResource(i); if ( pMR && pMR->GetMarineEntity() && pMR->GetMarineEntity()->GetHealth() > 0 ) { distance = pMR->GetMarineEntity()->GetAbsOrigin().DistTo( vecPosition ); if ( distance < flMarineNearDistance ) { return false; } } } } return true; } void CASW_Spawn_Manager::DeleteRoute( AI_Waypoint_t *pWaypointList ) { while ( pWaypointList ) { AI_Waypoint_t *pPrevWaypoint = pWaypointList; pWaypointList = pWaypointList->GetNext(); delete pPrevWaypoint; } } // creates a batch of aliens at the mouse cursor void asw_alien_batch_f( const CCommand& args ) { MDLCACHE_CRITICAL_SECTION(); bool allowPrecache = CBaseEntity::IsPrecacheAllowed(); CBaseEntity::SetAllowPrecache( true ); // find spawn point CASW_Player* pPlayer = ToASW_Player(UTIL_GetCommandClient()); if (!pPlayer) return; CASW_Marine *pMarine = pPlayer->GetMarine(); if (!pMarine) return; trace_t tr; Vector forward; AngleVectors( pMarine->EyeAngles(), &forward ); UTIL_TraceLine(pMarine->EyePosition(), pMarine->EyePosition() + forward * 300.0f,MASK_SOLID, pMarine, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction != 0.0 ) { // trace to the floor from this spot Vector vecSrc = tr.endpos; tr.endpos.z += 12; UTIL_TraceLine( vecSrc + Vector(0, 0, 12), vecSrc - Vector( 0, 0, 512 ) ,MASK_SOLID, pMarine, COLLISION_GROUP_NONE, &tr ); ASWSpawnManager()->SpawnAlienBatch( "asw_parasite", 25, tr.endpos, vec3_angle ); } CBaseEntity::SetAllowPrecache( allowPrecache ); } static ConCommand asw_alien_batch("asw_alien_batch", asw_alien_batch_f, "Creates a batch of aliens at the cursor", FCVAR_GAMEDLL | FCVAR_CHEAT); void asw_alien_horde_f( const CCommand& args ) { if ( args.ArgC() < 2 ) { Msg("supply horde size!\n"); return; } if ( !ASWSpawnManager()->AddHorde( atoi(args[1]) ) ) { Msg("Failed to add horde\n"); } } static ConCommand asw_alien_horde("asw_alien_horde", asw_alien_horde_f, "Creates a horde of aliens somewhere nearby", FCVAR_GAMEDLL | FCVAR_CHEAT);