//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #include "cbase.h" #include "ai_behavior_assault.h" #include "ai_navigator.h" #include "ai_memory.h" #include "ai_squad.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ConVar ai_debug_assault("ai_debug_assault", "0"); CGameString g_AssaultPointString( "assault_assaultpoint" ); CGameString g_RallyPointString( "assault_rallypoint" ); BEGIN_DATADESC( CRallyPoint ) DEFINE_KEYFIELD( m_AssaultPointName, FIELD_STRING, "assaultpoint" ), DEFINE_KEYFIELD( m_RallySequenceName, FIELD_STRING, "rallysequence" ), DEFINE_KEYFIELD( m_flAssaultDelay, FIELD_FLOAT, "assaultdelay" ), DEFINE_KEYFIELD( m_iPriority, FIELD_INTEGER, "priority" ), DEFINE_KEYFIELD( m_iStrictness, FIELD_INTEGER, "strict" ), DEFINE_KEYFIELD( m_bForceCrouch, FIELD_BOOLEAN, "forcecrouch" ), DEFINE_KEYFIELD( m_bIsUrgent, FIELD_BOOLEAN, "urgent" ), DEFINE_KEYFIELD( m_bShouldLock, FIELD_BOOLEAN, "lockpoint" ), DEFINE_FIELD( m_hLockedBy, FIELD_EHANDLE ), DEFINE_FIELD( m_sExclusivity, FIELD_SHORT ), DEFINE_OUTPUT( m_OnArrival, "OnArrival" ), END_DATADESC(); //--------------------------------------------------------- // Purpose: Communicate exclusivity //--------------------------------------------------------- int CRallyPoint::DrawDebugTextOverlays() { int offset; offset = BaseClass::DrawDebugTextOverlays(); if ( (m_debugOverlays & OVERLAY_TEXT_BIT) ) { switch( m_sExclusivity ) { case RALLY_EXCLUSIVE_NOT_EVALUATED: EntityText( offset, "Exclusive: Not Evaluated", 0 ); break; case RALLY_EXCLUSIVE_YES: EntityText( offset, "Exclusive: YES", 0 ); break; case RALLY_EXCLUSIVE_NO: EntityText( offset, "Exclusive: NO", 0 ); break; default: EntityText( offset, "Exclusive: !?INVALID?!", 0 ); break; } offset++; if( IsLocked() ) EntityText( offset, "LOCKED.", 0 ); else EntityText( offset, "Available", 0 ); offset++; } return offset; } //--------------------------------------------------------- // Purpose: If a rally point is 'exclusive' that means that // anytime an NPC is anywhere on the assault chain that // begins with this rally point, the assault is considered // 'exclusive' and no other NPCs will be allowed to use it // until the current NPC clears the entire assault chain // or dies. // // If exclusivity has not been determined the first time // this function is called, it will be computed and cached //--------------------------------------------------------- bool CRallyPoint::IsExclusive() { #ifndef HL2_EPISODIC // IF NOT EPISODIC // This 'exclusivity' concept is new to EP2. We're only willing to // risk causing problems in EP1, so emulate the old behavior if // we are not EPISODIC. We must do this by setting m_sExclusivity // so that ent_text will properly report the state. m_sExclusivity = RALLY_EXCLUSIVE_NO; #else if( m_sExclusivity == RALLY_EXCLUSIVE_NOT_EVALUATED ) { // We need to evaluate! Walk the chain of assault points // and if *ANY* assault points on this assault chain // are set to Never Time Out then set this rally point to // be exclusive to stop other NPC's walking down the chain // and ending up clumped up at the infinite rally point. CAssaultPoint *pAssaultEnt = (CAssaultPoint *)gEntList.FindEntityByName( NULL, m_AssaultPointName ); if( !pAssaultEnt ) { // Well, this is awkward. Leave it up to other assault code to tattle on the missing assault point. // We will just assume this assault is not exclusive. m_sExclusivity = RALLY_EXCLUSIVE_NO; return false; } // Otherwise, we start by assuming this assault chain is not exclusive. m_sExclusivity = RALLY_EXCLUSIVE_NO; if( pAssaultEnt ) { CAssaultPoint *pFirstAssaultEnt = pAssaultEnt; //some assault chains are circularly linked do { if( pAssaultEnt->m_bNeverTimeout ) { // We found a never timeout assault point! That makes this whole chain exclusive. m_sExclusivity = RALLY_EXCLUSIVE_YES; break; } pAssaultEnt = (CAssaultPoint *)gEntList.FindEntityByName( NULL, pAssaultEnt->m_NextAssaultPointName ); if ( pAssaultEnt && !pAssaultEnt->ClassMatchesExact( g_AssaultPointString ) ) { pAssaultEnt = NULL; } } while( (pAssaultEnt != NULL) && (pAssaultEnt != pFirstAssaultEnt) ); } } #endif// HL2_EPISODIC return (m_sExclusivity == RALLY_EXCLUSIVE_YES); } BEGIN_DATADESC( CAssaultPoint ) DEFINE_KEYFIELD( m_AssaultHintGroup, FIELD_STRING, "assaultgroup" ), DEFINE_KEYFIELD( m_NextAssaultPointName, FIELD_STRING, "nextassaultpoint" ), DEFINE_KEYFIELD( m_flAssaultTimeout, FIELD_FLOAT, "assaulttimeout" ), DEFINE_KEYFIELD( m_bClearOnContact, FIELD_BOOLEAN, "clearoncontact" ), DEFINE_KEYFIELD( m_bAllowDiversion, FIELD_BOOLEAN, "allowdiversion" ), DEFINE_KEYFIELD( m_flAllowDiversionRadius, FIELD_FLOAT, "allowdiversionradius" ), DEFINE_KEYFIELD( m_bNeverTimeout, FIELD_BOOLEAN, "nevertimeout" ), DEFINE_KEYFIELD( m_iStrictness, FIELD_INTEGER, "strict" ), DEFINE_KEYFIELD( m_bForceCrouch, FIELD_BOOLEAN, "forcecrouch" ), DEFINE_KEYFIELD( m_bIsUrgent, FIELD_BOOLEAN, "urgent" ), DEFINE_FIELD( m_bInputForcedClear, FIELD_BOOLEAN ), DEFINE_KEYFIELD( m_flAssaultPointTolerance, FIELD_FLOAT, "assaulttolerance" ), DEFINE_FIELD( m_flTimeLastUsed, FIELD_TIME ), // Inputs DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetClearOnContact", InputSetClearOnContact ), DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetAllowDiversion", InputSetAllowDiversion ), DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetForceClear", InputSetForceClear ), // Outputs DEFINE_OUTPUT( m_OnArrival, "OnArrival" ), DEFINE_OUTPUT( m_OnAssaultClear, "OnAssaultClear" ), END_DATADESC(); LINK_ENTITY_TO_CLASS( assault_rallypoint, CRallyPoint ); // just a copy of info_target for now LINK_ENTITY_TO_CLASS( assault_assaultpoint, CAssaultPoint ); // has its own class because it needs the entity I/O BEGIN_DATADESC( CAI_AssaultBehavior ) DEFINE_FIELD( m_hAssaultPoint, FIELD_EHANDLE ), DEFINE_FIELD( m_hRallyPoint, FIELD_EHANDLE ), DEFINE_FIELD( m_AssaultCue, FIELD_INTEGER ), DEFINE_FIELD( m_ReceivedAssaultCue, FIELD_INTEGER ), DEFINE_FIELD( m_bHitRallyPoint, FIELD_BOOLEAN ), DEFINE_FIELD( m_bHitAssaultPoint, FIELD_BOOLEAN ), DEFINE_FIELD( m_bDiverting, FIELD_BOOLEAN ), DEFINE_FIELD( m_flLastSawAnEnemyAt, FIELD_FLOAT ), DEFINE_FIELD( m_flTimeDeferScheduleSelection, FIELD_TIME ), DEFINE_FIELD( m_AssaultPointName, FIELD_STRING ), DEFINE_FIELD( m_hGoal, FIELD_EHANDLE ) END_DATADESC(); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::CanRunAScriptedNPCInteraction( bool bForced ) { if ( m_AssaultCue == CUE_NO_ASSAULT ) { // It's OK with the assault behavior, so leave it up to the base class. return BaseClass::CanRunAScriptedNPCInteraction( bForced ); } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CAI_AssaultBehavior::CAI_AssaultBehavior() { m_AssaultCue = CUE_NO_ASSAULT; } //----------------------------------------------------------------------------- // Purpose: Draw any text overlays // Input : Previous text offset from the top // Output : Current text offset from the top //----------------------------------------------------------------------------- int CAI_AssaultBehavior::DrawDebugTextOverlays( int text_offset ) { char tempstr[ 512 ]; int offset; offset = BaseClass::DrawDebugTextOverlays( text_offset ); if ( GetOuter()->m_debugOverlays & OVERLAY_TEXT_BIT ) { Q_snprintf( tempstr, sizeof(tempstr), "Assault Point: %s %s", STRING( m_AssaultPointName ), VecToString( m_hAssaultPoint->GetAbsOrigin() ) ); GetOuter()->EntityText( offset, tempstr, 0 ); offset++; } return offset; } //----------------------------------------------------------------------------- // Purpose: // Input : cue - //----------------------------------------------------------------------------- void CAI_AssaultBehavior::ReceiveAssaultCue( AssaultCue_t cue ) { if ( GetOuter() ) GetOuter()->ForceDecisionThink(); m_ReceivedAssaultCue = cue; SetCondition( COND_PROVOKED ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::AssaultHasBegun() { if( m_AssaultCue == CUE_DONT_WAIT && IsRunning() && m_bHitRallyPoint ) { return true; } return m_ReceivedAssaultCue == m_AssaultCue; } //----------------------------------------------------------------------------- // Purpose: Find an assaultpoint matching the iszAssaultPointName. // If more than one assault point of this type is found, randomly // use any of them EXCEPT the one most recently used. //----------------------------------------------------------------------------- CAssaultPoint *CAI_AssaultBehavior::FindAssaultPoint( string_t iszAssaultPointName ) { CUtlVectorpAssaultPoints; CUtlVectorpClearAssaultPoints; CBaseEntity *pEnt = gEntList.FindEntityByName( NULL, iszAssaultPointName ); while( pEnt != NULL ) { CAssaultPoint *pAssaultEnt; if ( !pEnt->Downcast( g_AssaultPointString, &pAssaultEnt) ) { if ( !pEnt->ClassMatchesExact( g_RallyPointString ) ) { DevMsg( "**ERROR: Entity %s being used as an assault_assaultpoint, but is actually a %s!\n", pEnt->GetDebugName(), pEnt->GetClassname() ); } } else { pAssaultPoints.AddToTail( pAssaultEnt ); } pEnt = gEntList.FindEntityByName( pEnt, iszAssaultPointName ); } // Didn't find any?! if( pAssaultPoints.Count() < 1 ) return NULL; // Only found one, just return it. if( pAssaultPoints.Count() == 1 ) return pAssaultPoints[0]; // Throw out any nodes that I cannot fit my bounding box on. for( int i = 0 ; i < pAssaultPoints.Count() ; i++ ) { trace_t tr; CAI_BaseNPC *pNPC = GetOuter(); CAssaultPoint *pAssaultPoint = pAssaultPoints[i]; AI_TraceHull ( pAssaultPoint->GetAbsOrigin(), pAssaultPoint->GetAbsOrigin(), pNPC->WorldAlignMins(), pNPC->WorldAlignMaxs(), MASK_SOLID, pNPC, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction == 1.0 ) { // Copy this into the list of clear points. pClearAssaultPoints.AddToTail(pAssaultPoint); } } // Only one clear assault point left! if( pClearAssaultPoints.Count() == 1 ) return pClearAssaultPoints[0]; // NONE left. Just return a random assault point, knowing that it's blocked. This is the old behavior, anyway. if( pClearAssaultPoints.Count() < 1 ) return pAssaultPoints[ random->RandomInt(0, (pAssaultPoints.Count() - 1)) ]; // We found several! First throw out the one most recently used. // This prevents picking the same point at this branch twice in a row. float flMostRecentTime = -1.0f; // Impossibly old int iMostRecentIndex = -1; for( int i = 0 ; i < pClearAssaultPoints.Count() ; i++ ) { if( pClearAssaultPoints[i]->m_flTimeLastUsed > flMostRecentTime ) { flMostRecentTime = pClearAssaultPoints[i]->m_flTimeLastUsed; iMostRecentIndex = i; } } Assert( iMostRecentIndex > -1 ); // Remove the most recently used pClearAssaultPoints.Remove( iMostRecentIndex ); if ( !m_hGoal || m_hGoal->m_BranchMethod == BRANCH_RANDOM ) { return pClearAssaultPoints[ random->RandomInt(0, (pClearAssaultPoints.Count() - 1)) ]; } CAssaultPoint *pBest = NULL; Vector vStart = GetOuter()->GetAbsOrigin(); float distBest, distCur; if ( m_hGoal->m_BranchMethod == BRANCH_CLOSEST ) { distBest = FLT_MAX; for( int i = 0 ; i < pClearAssaultPoints.Count() ; i++ ) { distCur = ( pClearAssaultPoints[i]->GetAbsOrigin() - vStart ).LengthSqr(); if ( distCur < distBest ) { pBest = pClearAssaultPoints[i]; distBest = distCur; } } } else { distBest = 0; for( int i = 0 ; i < pClearAssaultPoints.Count() ; i++ ) { distCur = ( pClearAssaultPoints[i]->GetAbsOrigin() - vStart ).LengthSqr(); if ( distCur > distBest ) { pBest = pClearAssaultPoints[i]; distBest = distCur; } } } return pBest; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_AssaultBehavior::SetAssaultPoint( CAssaultPoint *pAssaultPoint ) { Assert( pAssaultPoint != NULL ); m_hAssaultPoint = pAssaultPoint; pAssaultPoint->m_flTimeLastUsed = gpGlobals->curtime; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::ClearAssaultPoint( void ) { // To announce clear means that this NPC hasn't seen any live targets in // the assault area for the length of time the level designer has // specified that they should be vigilant. This effectively deactivates // the assault behavior. // This can also be happen if an assault point has ClearOnContact set, and // an NPC assaulting to this point has seen an enemy. Assert( m_hAssaultPoint != NULL ); if ( m_hAssaultPoint == NULL ) { DevMsg("**ERROR: ClearAssaultPoint called with no assault point\n" ); // Bomb out of assault behavior. m_AssaultCue = CUE_NO_ASSAULT; ClearSchedule( "No assault point" ); return; } // keep track of the name of the assault point m_AssaultPointName = m_hAssaultPoint->m_NextAssaultPointName; // Do we need to move to another assault point? if( m_hAssaultPoint->m_NextAssaultPointName != NULL_STRING ) { CAssaultPoint *pNextPoint = FindAssaultPoint( m_hAssaultPoint->m_NextAssaultPointName ); if( pNextPoint ) { SetAssaultPoint( pNextPoint ); // Send our NPC to the next assault point! m_bHitAssaultPoint = false; return; } else { CBaseEntity *pNextRally = gEntList.FindEntityByName( NULL, m_hAssaultPoint->m_NextAssaultPointName ); if ( pNextRally->ClassMatchesExact( g_RallyPointString ) && m_hGoal && m_hGoal->IsActive() ) { SetParameters( m_hAssaultPoint->m_NextAssaultPointName, (AssaultCue_t)m_hGoal->m_AssaultCue, m_hGoal->m_RallySelectMethod ); return; } else { DevMsg("**ERROR: Can't find next assault point: %s\n", STRING(m_hAssaultPoint->m_NextAssaultPointName) ); // Bomb out of assault behavior. m_AssaultCue = CUE_NO_ASSAULT; ClearSchedule( "Can't find next assault point" ); return; } } } // Just set the cue back to NO_ASSAULT. This disables the behavior. m_AssaultCue = CUE_NO_ASSAULT; // Exclusive or not, we unlock here. The assault is done. UnlockRallyPoint(); // If this assault behavior has changed the NPC's hint group, // slam that NPC's hint group back to null. // !!!TODO: if the NPC had a different hint group before the // assault began, we're slamming that, too! We might want // to cache it off if this becomes a problem (sjb) if( m_hAssaultPoint->m_AssaultHintGroup != NULL_STRING ) { GetOuter()->SetHintGroup( NULL_STRING ); } m_hAssaultPoint->m_OnAssaultClear.FireOutput( GetOuter(), GetOuter(), 0 ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_AssaultBehavior::OnHitAssaultPoint( void ) { GetOuter()->SpeakSentence( ASSAULT_SENTENCE_HIT_ASSAULT_POINT ); m_bHitAssaultPoint = true; // dvs: This was unprotected. Is a NULL assault point valid here? Assert( m_hAssaultPoint != NULL ); if ( m_hAssaultPoint ) { m_hAssaultPoint->m_OnArrival.FireOutput( GetOuter(), m_hAssaultPoint, 0 ); // Set the assault hint group if ( m_hAssaultPoint->m_AssaultHintGroup != NULL_STRING ) { SetHintGroup( m_hAssaultPoint->m_AssaultHintGroup ); } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_AssaultBehavior::GatherConditions( void ) { BaseClass::GatherConditions(); // If this NPC is moving towards an assault point which // a) Has a Next Assault Point, and // b) Is flagged to Clear On Arrival, // then hit and clear the assault point (fire all entity I/O) and move on to the next one without // interrupting the NPC's schedule. This provides a more fluid movement from point to point. if( IsCurSchedule( SCHED_MOVE_TO_ASSAULT_POINT ) && hl2_episodic.GetBool() ) { if( m_hAssaultPoint && m_hAssaultPoint->HasSpawnFlags(SF_ASSAULTPOINT_CLEARONARRIVAL) && m_hAssaultPoint->m_NextAssaultPointName != NULL_STRING ) { float flDist = GetAbsOrigin().DistTo( m_hAssaultPoint->GetAbsOrigin() ); if( flDist <= GetOuter()->GetMotor()->MinStoppingDist() ) { OnHitAssaultPoint(); ClearAssaultPoint(); if ( m_hAssaultPoint ) { AI_NavGoal_t goal( m_hAssaultPoint->GetAbsOrigin() ); goal.pTarget = m_hAssaultPoint; if ( GetNavigator()->SetGoal( goal ) == false ) { TaskFail( "Can't refresh assault path" ); } } else { // Bomb out of assault behavior. m_AssaultCue = CUE_NO_ASSAULT; ClearSchedule( "Can't find next assault point" ); } } } } if ( IsForcingCrouch() && GetOuter()->IsCrouching() ) { ClearCondition( COND_HEAR_BULLET_IMPACT ); } if( OnStrictAssault() ) { // Don't get distracted. Die trying if you have to. ClearCondition( COND_HEAR_DANGER ); } } //----------------------------------------------------------------------------- // Purpose: // Input : *pTask - //----------------------------------------------------------------------------- void CAI_AssaultBehavior::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_RANGE_ATTACK1: BaseClass::StartTask( pTask ); break; case TASK_ASSAULT_DEFER_SCHEDULE_SELECTION: m_flTimeDeferScheduleSelection = gpGlobals->curtime + pTask->flTaskData; TaskComplete(); break; case TASK_ASSAULT_MOVE_AWAY_PATH: break; case TASK_ANNOUNCE_CLEAR: { // If we're at an assault point that can never be cleared, keep waiting forever (if it's the last point in the assault) if ( m_hAssaultPoint && !m_hAssaultPoint->HasSpawnFlags( SF_ASSAULTPOINT_CLEARONARRIVAL ) && m_hAssaultPoint->m_bNeverTimeout && m_hAssaultPoint->m_NextAssaultPointName == NULL_STRING ) { TaskComplete(); return; } ClearAssaultPoint(); TaskComplete(); } break; case TASK_WAIT_ASSAULT_DELAY: { if( m_hRallyPoint ) { GetOuter()->SetWait( m_hRallyPoint->m_flAssaultDelay ); } else { TaskComplete(); } } break; case TASK_AWAIT_ASSAULT_TIMEOUT: // Maintain vigil for as long as the level designer has asked. Wait // and look for targets. GetOuter()->SetWait( m_hAssaultPoint->m_flAssaultTimeout ); break; case TASK_GET_PATH_TO_RALLY_POINT: { AI_NavGoal_t goal( m_hRallyPoint->GetAbsOrigin() ); goal.pTarget = m_hRallyPoint; if ( GetNavigator()->SetGoal( goal ) == false ) { // Try and get as close as possible otherwise AI_NavGoal_t nearGoal( GOALTYPE_LOCATION_NEAREST_NODE, m_hRallyPoint->GetAbsOrigin(), AIN_DEF_ACTIVITY, 256 ); if ( GetNavigator()->SetGoal( nearGoal, AIN_CLEAR_PREVIOUS_STATE ) ) { //FIXME: HACK! The internal pathfinding is setting this without our consent, so override it! ClearCondition( COND_TASK_FAILED ); GetNavigator()->SetArrivalDirection( m_hRallyPoint->GetAbsAngles() ); TaskComplete(); return; } } GetNavigator()->SetArrivalDirection( m_hRallyPoint->GetAbsAngles() ); } break; case TASK_FACE_RALLY_POINT: { UpdateForceCrouch(); GetMotor()->SetIdealYaw( m_hRallyPoint->GetAbsAngles().y ); GetOuter()->SetTurnActivity(); } break; case TASK_GET_PATH_TO_ASSAULT_POINT: { AI_NavGoal_t goal( m_hAssaultPoint->GetAbsOrigin() ); goal.pTarget = m_hAssaultPoint; if ( GetNavigator()->SetGoal( goal ) == false ) { // Try and get as close as possible otherwise AI_NavGoal_t nearGoal( GOALTYPE_LOCATION_NEAREST_NODE, m_hAssaultPoint->GetAbsOrigin(), AIN_DEF_ACTIVITY, 256 ); if ( GetNavigator()->SetGoal( nearGoal, AIN_CLEAR_PREVIOUS_STATE ) ) { //FIXME: HACK! The internal pathfinding is setting this without our consent, so override it! ClearCondition( COND_TASK_FAILED ); GetNavigator()->SetArrivalDirection( m_hAssaultPoint->GetAbsAngles() ); TaskComplete(); return; } } GetNavigator()->SetArrivalDirection( m_hAssaultPoint->GetAbsAngles() ); } break; case TASK_FACE_ASSAULT_POINT: { UpdateForceCrouch(); if( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) { // If I can already fight when I arrive, don't bother running any facing code. Let // The combat AI do that. Turning here will only make the NPC look dumb in a combat // situation because it will take time to turn before attacking. TaskComplete(); } else { GetMotor()->SetIdealYaw( m_hAssaultPoint->GetAbsAngles().y ); GetOuter()->SetTurnActivity(); } } break; case TASK_HIT_ASSAULT_POINT: OnHitAssaultPoint(); TaskComplete(); break; case TASK_HIT_RALLY_POINT: // Once we're stading on it and facing the correct direction, // we have arrived at rally point. GetOuter()->SpeakSentence( ASSAULT_SENTENCE_HIT_RALLY_POINT ); m_bHitRallyPoint = true; m_hRallyPoint->m_OnArrival.FireOutput( GetOuter(), m_hRallyPoint, 0 ); TaskComplete(); break; case TASK_AWAIT_CUE: if( PollAssaultCue() ) { TaskComplete(); } else { // Don't do anything if we've been told to crouch if ( IsForcingCrouch() ) break; else if( m_hRallyPoint->m_RallySequenceName != NULL_STRING ) { // The cue hasn't been given yet, so set to the rally sequence. int sequence = GetOuter()->LookupSequence( STRING( m_hRallyPoint->m_RallySequenceName ) ); if( sequence != -1 ) { GetOuter()->ResetSequence( sequence ); GetOuter()->SetIdealActivity( ACT_DO_NOT_DISTURB ); } } else { // Only chain this task if I'm not playing a custom animation if( GetOuter()->GetEnemy() ) { ChainStartTask( TASK_FACE_ENEMY, 0 ); } } } break; default: BaseClass::StartTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: // Input : *pTask - //----------------------------------------------------------------------------- void CAI_AssaultBehavior::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_WAIT_ASSAULT_DELAY: case TASK_AWAIT_ASSAULT_TIMEOUT: if ( m_hAssaultPoint ) { if ( m_hAssaultPoint->m_bInputForcedClear || (m_hAssaultPoint->m_bClearOnContact && HasCondition( COND_SEE_ENEMY )) ) { // If we're on an assault that should clear on contact, clear when we see an enemy TaskComplete(); } } if( GetOuter()->IsWaitFinished() && ( pTask->iTask == TASK_WAIT_ASSAULT_DELAY || !m_hAssaultPoint->m_bNeverTimeout ) ) { TaskComplete(); } break; case TASK_FACE_RALLY_POINT: case TASK_FACE_ASSAULT_POINT: GetMotor()->UpdateYaw(); if( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) { // Out early if the NPC can attack. TaskComplete(); } if ( GetOuter()->FacingIdeal() ) { TaskComplete(); } break; case TASK_AWAIT_CUE: // If we've lost our rally point, abort if ( !m_hRallyPoint ) { TaskFail("No rally point."); break; } if( PollAssaultCue() ) { TaskComplete(); } if ( IsForcingCrouch() ) break; if( GetOuter()->GetEnemy() && m_hRallyPoint->m_RallySequenceName == NULL_STRING && !HasCondition(COND_ENEMY_OCCLUDED) ) { // I have an enemy and I'm NOT playing a custom animation. ChainRunTask( TASK_FACE_ENEMY, 0 ); } break; case TASK_WAIT_FOR_MOVEMENT: if ( ai_debug_assault.GetBool() ) { if ( IsCurSchedule( SCHED_MOVE_TO_ASSAULT_POINT ) ) { NDebugOverlay::Line( WorldSpaceCenter(), GetNavigator()->GetGoalPos(), 255,0,0, true,0.1); NDebugOverlay::Box( GetNavigator()->GetGoalPos(), -Vector(10,10,10), Vector(10,10,10), 255,0,0, 8, 0.1 ); } else if ( IsCurSchedule( SCHED_MOVE_TO_RALLY_POINT ) ) { NDebugOverlay::Line( WorldSpaceCenter(), GetNavigator()->GetGoalPos(), 0,255,0, true,0.1); NDebugOverlay::Box( GetNavigator()->GetGoalPos(), -Vector(10,10,10), Vector(10,10,10), 0,255,0, 8, 0.1 ); } } if ( m_hAssaultPoint && (m_hAssaultPoint->m_bInputForcedClear || (m_hAssaultPoint->m_bClearOnContact && HasCondition( COND_SEE_ENEMY ))) ) { DevMsg( "Assault Cleared due to Contact or Input!\n" ); ClearAssaultPoint(); TaskComplete(); return; } if ( ( ( !GetOuter()->DidChooseEnemy() && gpGlobals->curtime - GetOuter()->GetTimeEnemyAcquired() > 1 ) || !GetOuter()->GetEnemy() ) ) { CBaseEntity *pNewEnemy = GetOuter()->BestEnemy(); if( pNewEnemy != NULL && pNewEnemy != GetOuter()->GetEnemy() ) { GetOuter()->SetEnemy( pNewEnemy ); GetOuter()->SetState( NPC_STATE_COMBAT ); } } BaseClass::RunTask( pTask ); break; default: BaseClass::RunTask( pTask ); break; } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- CRallyPoint *CAI_AssaultBehavior::FindBestRallyPointInRadius( const Vector &vecCenter, float flRadius ) { VPROF_BUDGET( "CAI_AssaultBehavior::FindBestRallyPointInRadius", VPROF_BUDGETGROUP_NPCS ); const int RALLY_SEARCH_ENTS = 30; CBaseEntity *pEntities[RALLY_SEARCH_ENTS]; int iNumEntities = UTIL_EntitiesInSphere( pEntities, RALLY_SEARCH_ENTS, vecCenter, flRadius, 0 ); CRallyPoint *pBest = NULL; int iBestPriority = -1; for ( int i = 0; i < iNumEntities; i++ ) { CRallyPoint *pRallyEnt = dynamic_cast(pEntities[i]); if( pRallyEnt ) { if( !pRallyEnt->IsLocked() ) { // Consider this point. if( pRallyEnt->m_iPriority > iBestPriority ) { pBest = pRallyEnt; iBestPriority = pRallyEnt->m_iPriority; } } } } return pBest; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::IsValidShootPosition( const Vector &vLocation, CAI_Node *pNode, CAI_Hint const *pHint ) { CBaseEntity *pCuePoint = NULL; float flTolerance = 0.0f; if( m_bHitRallyPoint && !m_bHitAssaultPoint && !AssaultHasBegun() ) { if( m_hRallyPoint != NULL ) { pCuePoint = m_hRallyPoint; flTolerance = CUE_POINT_TOLERANCE; } } else if( m_bHitAssaultPoint ) { if( m_hAssaultPoint != NULL ) { pCuePoint = m_hAssaultPoint; flTolerance = m_hAssaultPoint->m_flAssaultPointTolerance; } } if ( pCuePoint && (vLocation - pCuePoint->GetAbsOrigin()).Length2DSqr() > Square( flTolerance - 0.1 ) ) return false; return BaseClass::IsValidShootPosition( vLocation, pNode, pHint ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- float CAI_AssaultBehavior::GetMaxTacticalLateralMovement( void ) { return CUE_POINT_TOLERANCE - 0.1; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_AssaultBehavior::UpdateOnRemove() { // Ignore exclusivity. Our NPC just died. UnlockRallyPoint(); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::OnStrictAssault( void ) { return (m_hAssaultPoint && m_hAssaultPoint->m_iStrictness); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::UpdateForceCrouch( void ) { if ( IsForcingCrouch() ) { // Only force crouch when we're near the point we're supposed to crouch at float flDistanceToTargetSqr = GetOuter()->GetAbsOrigin().DistToSqr( AssaultHasBegun() ? m_hAssaultPoint->GetAbsOrigin() : m_hRallyPoint->GetAbsOrigin() ); if ( flDistanceToTargetSqr < (64*64) ) { GetOuter()->ForceCrouch(); } else { GetOuter()->ClearForceCrouch(); } return true; } return false; } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::IsForcingCrouch( void ) { if ( AssaultHasBegun() ) return (m_hAssaultPoint && m_hAssaultPoint->m_bForceCrouch); return (m_hRallyPoint && m_hRallyPoint->m_bForceCrouch); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::IsUrgent( void ) { if ( AssaultHasBegun() ) return (m_hAssaultPoint && m_hAssaultPoint->m_bIsUrgent); return (m_hRallyPoint && m_hRallyPoint->m_bIsUrgent); } //----------------------------------------------------------------------------- // Purpose: Unlock any rally points the behavior is currently locking //----------------------------------------------------------------------------- void CAI_AssaultBehavior::UnlockRallyPoint( void ) { CAI_AssaultBehavior *pBehavior; if ( GetOuter()->GetBehavior( &pBehavior ) ) { if( pBehavior->m_hRallyPoint ) { pBehavior->m_hRallyPoint->Unlock( GetOuter() ); } } } //----------------------------------------------------------------------------- // Purpose: // Input : *pRallyPoint - // assaultcue - //----------------------------------------------------------------------------- void CAI_AssaultBehavior::SetParameters( CBaseEntity *pRallyEnt, AssaultCue_t assaultcue ) { VPROF_BUDGET( "CAI_AssaultBehavior::SetParameters", VPROF_BUDGETGROUP_NPCS ); // Clean up any soon to be dangling rally points UnlockRallyPoint(); // Firstly, find a rally point. CRallyPoint *pRallyPoint = dynamic_cast(pRallyEnt); if( pRallyPoint ) { if( !pRallyPoint->IsLocked() ) { // Claim it. m_hRallyPoint = pRallyPoint; m_hRallyPoint->Lock( GetOuter() ); m_AssaultCue = assaultcue; InitializeBehavior(); return; } else { DevMsg("**ERROR: Specified a rally point that is LOCKED!\n" ); } } else { DevMsg("**ERROR: Bad RallyPoint in SetParameters\n" ); // Bomb out of assault behavior. m_AssaultCue = CUE_NO_ASSAULT; ClearSchedule( "Bad rally point" ); } } //----------------------------------------------------------------------------- // Purpose: // Input : rallypointname - // assaultcue - //----------------------------------------------------------------------------- void CAI_AssaultBehavior::SetParameters( string_t rallypointname, AssaultCue_t assaultcue, int rallySelectMethod ) { VPROF_BUDGET( "CAI_AssaultBehavior::SetParameters", VPROF_BUDGETGROUP_NPCS ); // Clean up any soon to be dangling rally points UnlockRallyPoint(); // Firstly, find a rally point. CRallyPoint *pRallyEnt = dynamic_cast(gEntList.FindEntityByName( NULL, rallypointname ) ); CRallyPoint *pBest = NULL; int iBestPriority = -1; switch( rallySelectMethod ) { case RALLY_POINT_SELECT_CLOSEST: case RALLY_POINT_SELECT_FURTHEST: { while( pRallyEnt ) { if( !pRallyEnt->IsLocked() ) { // Consider this point. if( pRallyEnt->m_iPriority > iBestPriority ) { // This point is higher priority. I must take it. pBest = pRallyEnt; iBestPriority = pRallyEnt->m_iPriority; } else if ( pRallyEnt->m_iPriority == iBestPriority ) { // This point is the same priority as my current best. // I must take it if it is closer. Vector vecStart = GetOuter()->GetAbsOrigin(); float flNewDist, flBestDist; flNewDist = ( pRallyEnt->GetAbsOrigin() - vecStart ).LengthSqr(); flBestDist = ( pBest->GetAbsOrigin() - vecStart ).LengthSqr(); if( ( rallySelectMethod == RALLY_POINT_SELECT_CLOSEST && flNewDist < flBestDist ) || ( rallySelectMethod == RALLY_POINT_SELECT_FURTHEST && flNewDist > flBestDist ) ) { // Priority is already identical. Just take this point. pBest = pRallyEnt; } } } pRallyEnt = dynamic_cast(gEntList.FindEntityByName( pRallyEnt, rallypointname, NULL ) ); } } break; case RALLY_POINT_SELECT_RANDOM: { // Gather all available points into a utilvector, then pick one at random. CUtlVector rallyPoints; // List of rally points that are available to choose from. while( pRallyEnt ) { if( !pRallyEnt->IsLocked() ) { rallyPoints.AddToTail( pRallyEnt ); } pRallyEnt = dynamic_cast(gEntList.FindEntityByName( pRallyEnt, rallypointname ) ); } if( rallyPoints.Count() > 0 ) { pBest = rallyPoints[ random->RandomInt(0, rallyPoints.Count()- 1) ]; } } break; default: DevMsg( "ERROR: INVALID RALLY POINT SELECTION METHOD. Assault will not function.\n"); break; } if( !pBest ) { DevMsg("%s Didn't find a best rally point!\n", GetOuter()->GetEntityName().ToCStr() ); return; } if ( pBest->ShouldBeLocked() ) { pBest->Lock( GetOuter() ); } m_hRallyPoint = pBest; if( !m_hRallyPoint ) { DevMsg("**ERROR: Can't find a rally point named '%s'\n", STRING( rallypointname )); // Bomb out of assault behavior. m_AssaultCue = CUE_NO_ASSAULT; ClearSchedule( "Can't find rally point" ); return; } m_AssaultCue = assaultcue; InitializeBehavior(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::InitializeBehavior() { // initialize the variables that track whether the NPC has reached (hit) // his rally and assault points already. Be advised, having hit the point // only means you have been to it at some point. Doesn't mean you're standing // there still. Mainly used to understand which 'phase' of the assault an NPC // is in. m_bHitRallyPoint = false; m_bHitAssaultPoint = false; m_hAssaultPoint = false; m_bDiverting = false; m_flLastSawAnEnemyAt = 0; // Also reset the status of externally received assault cues m_ReceivedAssaultCue = CUE_NO_ASSAULT; CAssaultPoint *pAssaultEnt = FindAssaultPoint( m_hRallyPoint->m_AssaultPointName ); if( pAssaultEnt ) { SetAssaultPoint(pAssaultEnt); } else { DevMsg("**ERROR: Can't find any assault points named: %s\n", STRING( m_hRallyPoint->m_AssaultPointName )); // Bomb out of assault behavior. m_AssaultCue = CUE_NO_ASSAULT; ClearSchedule( "Can't find assault point" ); return; } // Slam the NPC's schedule so that he starts picking Assault schedules right now. ClearSchedule( "Initializing assault behavior" ); } //----------------------------------------------------------------------------- // Purpose: Check conditions and see if the cue to being an assault has come. // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::PollAssaultCue( void ) { // right now, always go when the commander says. if( m_ReceivedAssaultCue == CUE_COMMANDER ) { return true; } switch( m_AssaultCue ) { case CUE_NO_ASSAULT: // NO_ASSAULT never ever triggers. return false; break; case CUE_ENTITY_INPUT: return m_ReceivedAssaultCue == CUE_ENTITY_INPUT; break; case CUE_PLAYER_GUNFIRE: // Any gunfire will trigger this right now (sjb) if( HasCondition( COND_HEAR_COMBAT ) ) { return true; } break; case CUE_DONT_WAIT: // Just keep going! m_ReceivedAssaultCue = CUE_DONT_WAIT; return true; break; case CUE_COMMANDER: // Player told me to go, so go! return m_ReceivedAssaultCue == CUE_COMMANDER; break; } return false; } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CAI_AssaultBehavior::OnRestore() { if ( !m_hAssaultPoint || !m_hRallyPoint ) { Disable(); NotifyChangeBehaviorStatus(); } } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::CanSelectSchedule() { if ( !GetOuter()->IsInterruptable() ) return false; if ( GetOuter()->HasCondition( COND_RECEIVED_ORDERS ) ) return false; // We're letting other AI run for a little while because the assault AI failed recently. if ( m_flTimeDeferScheduleSelection > gpGlobals->curtime ) return false; // No schedule selection if no assault is being conducted. if( m_AssaultCue == CUE_NO_ASSAULT ) return false; if ( !m_hAssaultPoint || !m_hRallyPoint ) { Disable(); return false; } // Remember when we last saw an enemy if ( GetEnemy() ) { m_flLastSawAnEnemyAt = gpGlobals->curtime; } // If we've seen an enemy in the last few seconds, and we're allowed to divert, // let the base AI decide what I should do. if ( IsAllowedToDivert() ) { // Return true, but remember that we're actually allowing them to divert // This is done because we don't want the assault behaviour to think it's finished with the assault. m_bDiverting = true; } else if ( m_bDiverting ) { // If we were diverting, provoke us to make a new schedule selection SetCondition( COND_PROVOKED ); m_bDiverting = false; } // If we're diverting, let the base AI decide everything if ( m_bDiverting ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::BeginScheduleSelection() { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::EndScheduleSelection() { m_bHitAssaultPoint = false; if( m_hRallyPoint != NULL ) { if( !m_hRallyPoint->IsExclusive() ) m_bHitRallyPoint = false; if( !hl2_episodic.GetBool() || !m_hRallyPoint->IsExclusive() || !GetOuter()->IsAlive() ) { // Here we unlock the rally point if it is NOT EXCLUSIVE // -OR- the Outer is DEAD. (This gives us a head-start on // preparing the point to take new NPCs right away. Otherwise // we have to wait two seconds until the behavior is destroyed.) // NOTICE that the legacy (non-episodic) support calls UnlockRallyPoint // unconditionally on EndScheduleSelection() UnlockRallyPoint(); } } GetOuter()->ClearForceCrouch(); } //----------------------------------------------------------------------------- // Purpose: // Input : scheduleType - // Output : int //----------------------------------------------------------------------------- int CAI_AssaultBehavior::TranslateSchedule( int scheduleType ) { switch( scheduleType ) { case SCHED_ESTABLISH_LINE_OF_FIRE_FALLBACK: // This nasty schedule can allow the NPC to violate their position near // the assault point. Translate it away to something stationary. (sjb) return SCHED_COMBAT_FACE; break; case SCHED_RANGE_ATTACK1: if ( GetOuter()->GetShotRegulator()->IsInRestInterval() ) { if ( GetOuter()->HasStrategySlotRange( SQUAD_SLOT_ATTACK1, SQUAD_SLOT_ATTACK2 ) ) GetOuter()->VacateStrategySlot(); return SCHED_COMBAT_FACE; // @TODO (toml 07-02-03): Should do something more tactically sensible } break; case SCHED_MOVE_TO_WEAPON_RANGE: case SCHED_CHASE_ENEMY: if( m_bHitAssaultPoint ) { return SCHED_WAIT_AND_CLEAR; } else { return SCHED_MOVE_TO_ASSAULT_POINT; } break; case SCHED_HOLD_RALLY_POINT: if( HasCondition(COND_NO_PRIMARY_AMMO) | HasCondition(COND_LOW_PRIMARY_AMMO) ) { return SCHED_RELOAD; } break; case SCHED_MOVE_TO_ASSAULT_POINT: { float flDist = ( m_hAssaultPoint->GetAbsOrigin() - GetAbsOrigin() ).Length(); if ( flDist <= 12.0f ) return SCHED_AT_ASSAULT_POINT; } break; } return BaseClass::TranslateSchedule( scheduleType ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::OnStartSchedule( int scheduleType ) { if ( scheduleType == SCHED_HIDE_AND_RELOAD ) //!!!HACKHACK { // Dirty the assault point flag so that we return to assault point m_bHitAssaultPoint = false; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::ClearSchedule( const char *szReason ) { // HACKHACK: In reality, we shouldn't be clearing the schedule ever if the assault // behavior isn't actually in charge of the NPC. Fix after ship. For now, hacking // a fix to Grigori failing to make it over the fence of the graveyard in d1_town_02a if ( GetOuter()->ClassMatches( "npc_monk" ) && GetOuter()->GetState() == NPC_STATE_SCRIPT ) return; // Don't allow it if we're in a vehicle if ( GetOuter()->IsInAVehicle() ) return; GetOuter()->ClearSchedule( szReason ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_AssaultBehavior::IsAllowedToDivert( void ) { if ( m_hAssaultPoint && m_hAssaultPoint->m_bAllowDiversion ) { if ( m_hAssaultPoint->m_flAllowDiversionRadius == 0.0f || (m_bHitAssaultPoint && GetEnemy() != NULL && GetEnemy()->GetAbsOrigin().DistToSqr(m_hAssaultPoint->GetAbsOrigin()) <= Square(m_hAssaultPoint->m_flAllowDiversionRadius)) ) { if ( m_flLastSawAnEnemyAt && ((gpGlobals->curtime - m_flLastSawAnEnemyAt) < ASSAULT_DIVERSION_TIME) ) return true; } } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultBehavior::BuildScheduleTestBits() { BaseClass::BuildScheduleTestBits(); // If we're allowed to divert, add the appropriate interrupts to our movement schedules if ( IsAllowedToDivert() ) { if ( IsCurSchedule( SCHED_MOVE_TO_ASSAULT_POINT ) || IsCurSchedule( SCHED_MOVE_TO_RALLY_POINT ) || IsCurSchedule( SCHED_HOLD_RALLY_POINT ) ) { GetOuter()->SetCustomInterruptCondition( COND_NEW_ENEMY ); GetOuter()->SetCustomInterruptCondition( COND_SEE_ENEMY ); } } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_AssaultBehavior::OnScheduleChange() { if( IsCurSchedule(SCHED_WAIT_AND_CLEAR, false) ) { if( m_hAssaultPoint && m_hAssaultPoint->m_bClearOnContact ) { if( HasCondition(COND_SEE_ENEMY) ) { ClearAssaultPoint(); } } } BaseClass::OnScheduleChange(); } //----------------------------------------------------------------------------- // Purpose: // Output : int //----------------------------------------------------------------------------- int CAI_AssaultBehavior::SelectSchedule() { if ( !OnStrictAssault() ) { if( HasCondition( COND_PLAYER_PUSHING ) ) return SCHED_ASSAULT_MOVE_AWAY; if( HasCondition( COND_HEAR_DANGER ) ) return SCHED_TAKE_COVER_FROM_BEST_SOUND; } if( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) return SCHED_MELEE_ATTACK1; // If you're empty, reload before trying to carry out any assault functions. if( HasCondition( COND_NO_PRIMARY_AMMO ) ) return SCHED_RELOAD; if( m_bHitRallyPoint && !m_bHitAssaultPoint && !AssaultHasBegun() ) { // If I have hit my rally point, but I haven't hit my assault point yet, // Make sure I'm still on my rally point, cause another behavior may have moved me. // 2D check to be within 32 units of my rallypoint. Vector vecDiff = GetAbsOrigin() - m_hRallyPoint->GetAbsOrigin(); vecDiff.z = 0.0; if( vecDiff.LengthSqr() > Square(CUE_POINT_TOLERANCE) ) { // Someone moved me away. Get back to rally point. m_bHitRallyPoint = false; return SCHED_MOVE_TO_RALLY_POINT; } } else if( m_bHitAssaultPoint ) { // Likewise. If I have hit my assault point, make sure I'm still there. Another // behavior (hide and reload) may have moved me away. Vector vecDiff = GetAbsOrigin() - m_hAssaultPoint->GetAbsOrigin(); vecDiff.z = 0.0; if( vecDiff.LengthSqr() > Square(CUE_POINT_TOLERANCE) ) { // Someone moved me away. m_bHitAssaultPoint = false; } } // Go to my rally point, unless the assault's begun. if( !m_bHitRallyPoint && !AssaultHasBegun() ) { GetOuter()->SpeakSentence( ASSAULT_SENTENCE_SQUAD_ADVANCE_TO_RALLY ); return SCHED_MOVE_TO_RALLY_POINT; } if( !m_bHitAssaultPoint ) { if( m_ReceivedAssaultCue == m_AssaultCue || m_ReceivedAssaultCue == CUE_COMMANDER || m_AssaultCue == CUE_DONT_WAIT ) { GetOuter()->SpeakSentence( ASSAULT_SENTENCE_SQUAD_ADVANCE_TO_ASSAULT ); if ( m_hRallyPoint && !m_hRallyPoint->IsExclusive() ) { // If this assault chain is not exclusive, then free up the rallypoint so that others can follow me // Otherwise, we do not unlock this rally point until we are FINISHED or DEAD. It's exclusively our chain of assault UnlockRallyPoint();// Here we go! Free up the rally point since I'm moving to assault. } if ( !UpdateForceCrouch() ) { GetOuter()->ClearForceCrouch(); } return SCHED_MOVE_TO_ASSAULT_POINT; } else if( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) { return SCHED_RANGE_ATTACK1; } else if( HasCondition( COND_NO_PRIMARY_AMMO ) ) { // Don't run off to reload. return SCHED_RELOAD; } else if( HasCondition( COND_LIGHT_DAMAGE ) || HasCondition( COND_HEAVY_DAMAGE ) ) { GetOuter()->SpeakSentence( ASSAULT_SENTENCE_UNDER_ATTACK ); return SCHED_ALERT_FACE; } else if( GetOuter()->GetEnemy() && !HasCondition( COND_CAN_RANGE_ATTACK1 ) && !HasCondition( COND_CAN_RANGE_ATTACK2) && !HasCondition(COND_ENEMY_OCCLUDED) ) { return SCHED_COMBAT_FACE; } else { UpdateForceCrouch(); return SCHED_HOLD_RALLY_POINT; } } if( HasCondition( COND_NO_PRIMARY_AMMO ) ) { GetOuter()->SpeakSentence( ASSAULT_SENTENCE_COVER_NO_AMMO ); return SCHED_HIDE_AND_RELOAD; } if( m_hAssaultPoint->HasSpawnFlags( SF_ASSAULTPOINT_CLEARONARRIVAL ) ) { return SCHED_CLEAR_ASSAULT_POINT; } if ( (!GetEnemy() || HasCondition(COND_ENEMY_OCCLUDED)) && !GetOuter()->HasConditionsToInterruptSchedule( SCHED_WAIT_AND_CLEAR ) ) { // Don't have an enemy. Just keep an eye on things. return SCHED_WAIT_AND_CLEAR; } if ( OnStrictAssault() ) { // Don't allow the base class to select a schedule cause it will probably move the NPC. if( !HasCondition(COND_CAN_RANGE_ATTACK1) && !HasCondition(COND_CAN_RANGE_ATTACK2) && !HasCondition(COND_CAN_MELEE_ATTACK1) && !HasCondition(COND_CAN_MELEE_ATTACK2) && !HasCondition(COND_TOO_CLOSE_TO_ATTACK) && !HasCondition(COND_NOT_FACING_ATTACK) ) { return SCHED_WAIT_AND_CLEAR; } } #ifdef HL2_EPISODIC // This ugly patch fixes a bug where Combine Soldiers on an assault would not shoot through glass, because of the way // that shooting through glass is implemented in their AI. (sjb) if( HasCondition(COND_SEE_ENEMY) && HasCondition(COND_WEAPON_SIGHT_OCCLUDED) && !HasCondition(COND_LOW_PRIMARY_AMMO) ) { // If they are hiding behind something that we can destroy, start shooting at it. CBaseEntity *pBlocker = GetOuter()->GetEnemyOccluder(); if ( pBlocker && pBlocker->GetHealth() > 0 ) { if( GetOuter()->Classify() == CLASS_COMBINE && FClassnameIs(GetOuter(), "npc_combine_s") ) { return SCHED_SHOOT_ENEMY_COVER; } } } #endif//HL2_EPISODIC return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // // CAI_AssaultGoal // // Purpose: // // //----------------------------------------------------------------------------- BEGIN_DATADESC( CAI_AssaultGoal ) DEFINE_KEYFIELD( m_RallyPoint, FIELD_STRING, "rallypoint" ), DEFINE_KEYFIELD( m_AssaultCue, FIELD_INTEGER, "AssaultCue" ), DEFINE_KEYFIELD( m_RallySelectMethod, FIELD_INTEGER, "RallySelectMethod" ), DEFINE_KEYFIELD( m_BranchMethod, FIELD_INTEGER, "BranchMethod" ), DEFINE_INPUTFUNC( FIELD_VOID, "BeginAssault", InputBeginAssault ), END_DATADESC(); //------------------------------------- LINK_ENTITY_TO_CLASS( ai_goal_assault, CAI_AssaultGoal ); //------------------------------------- //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultGoal::EnableGoal( CAI_BaseNPC *pAI ) { CAI_AssaultBehavior *pBehavior; if ( !pAI->GetBehavior( &pBehavior ) ) return; pBehavior->SetGoal( this ); pBehavior->SetParameters( m_RallyPoint, (AssaultCue_t)m_AssaultCue, m_RallySelectMethod ); // Duplicate the output } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_AssaultGoal::DisableGoal( CAI_BaseNPC *pAI ) { CAI_AssaultBehavior *pBehavior; if ( pAI->GetBehavior( &pBehavior ) ) { pBehavior->Disable(); // Don't leave any hanging rally points locked. pBehavior->UnlockRallyPoint(); pBehavior->ClearSchedule( "Assault goal disabled" ); pBehavior->SetGoal( NULL ); } } //----------------------------------------------------------------------------- // Purpose: ENTITY I/O method for telling the assault behavior to cue assault // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_AssaultGoal::InputBeginAssault( inputdata_t &inputdata ) { int i; for( i = 0 ; i < NumActors() ; i++ ) { CAI_BaseNPC *pActor = GetActor( i ); if( pActor ) { // Now use this actor to lookup the Behavior CAI_AssaultBehavior *pBehavior; if( pActor->GetBehavior( &pBehavior ) ) { // GOT IT! Now tell the behavior that entity i/o wants to cue the assault. pBehavior->ReceiveAssaultCue( CUE_ENTITY_INPUT ); } } } } AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER(CAI_AssaultBehavior) DECLARE_TASK(TASK_GET_PATH_TO_RALLY_POINT) DECLARE_TASK(TASK_FACE_RALLY_POINT) DECLARE_TASK(TASK_GET_PATH_TO_ASSAULT_POINT) DECLARE_TASK(TASK_FACE_ASSAULT_POINT) DECLARE_TASK(TASK_AWAIT_CUE) DECLARE_TASK(TASK_AWAIT_ASSAULT_TIMEOUT) DECLARE_TASK(TASK_ANNOUNCE_CLEAR) DECLARE_TASK(TASK_WAIT_ASSAULT_DELAY) DECLARE_TASK(TASK_HIT_ASSAULT_POINT) DECLARE_TASK(TASK_HIT_RALLY_POINT) DECLARE_TASK(TASK_ASSAULT_DEFER_SCHEDULE_SELECTION) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_MOVE_TO_RALLY_POINT, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSAULT_FAILED_TO_MOVE" " TASK_GET_PATH_TO_RALLY_POINT 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_FACE_RALLY_POINT 0" " TASK_HIT_RALLY_POINT 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_HOLD_RALLY_POINT" " " " Interrupts" " COND_HEAR_DANGER" " COND_PROVOKED" " COND_NO_PRIMARY_AMMO" " COND_PLAYER_PUSHING" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_ASSAULT_FAILED_TO_MOVE, " Tasks" " TASK_ASSAULT_DEFER_SCHEDULE_SELECTION 1" " " " Interrupts" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_FAIL_MOVE_TO_RALLY_POINT, " Tasks" " TASK_WAIT 1" " " " Interrupts" " COND_HEAR_DANGER" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" ) #ifdef HL2_EPISODIC //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_HOLD_RALLY_POINT, " Tasks" " TASK_FACE_RALLY_POINT 0" " TASK_AWAIT_CUE 0" " TASK_WAIT_ASSAULT_DELAY 0" " " " Interrupts" //" COND_NEW_ENEMY" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_PLAYER_PUSHING" " COND_HEAR_DANGER" " COND_HEAR_BULLET_IMPACT" " COND_NO_PRIMARY_AMMO" ) #else //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_HOLD_RALLY_POINT, " Tasks" " TASK_FACE_RALLY_POINT 0" " TASK_AWAIT_CUE 0" " TASK_WAIT_ASSAULT_DELAY 0" " " " Interrupts" " COND_NEW_ENEMY" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_PLAYER_PUSHING" " COND_HEAR_DANGER" " COND_HEAR_BULLET_IMPACT" " COND_NO_PRIMARY_AMMO" " COND_TOO_CLOSE_TO_ATTACK" ) #endif//HL2_EPISODIC //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_HOLD_ASSAULT_POINT, " Tasks" " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" " TASK_WAIT 3" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_TOO_CLOSE_TO_ATTACK" " COND_LOST_ENEMY" " COND_HEAR_DANGER" " COND_HEAR_BULLET_IMPACT" " COND_NO_PRIMARY_AMMO" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_MOVE_TO_ASSAULT_POINT, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ASSAULT_FAILED_TO_MOVE" " TASK_GATHER_CONDITIONS 0" " TASK_GET_PATH_TO_ASSAULT_POINT 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_FACE_ASSAULT_POINT 0" " TASK_HIT_ASSAULT_POINT 0" " " " Interrupts" " COND_NO_PRIMARY_AMMO" " COND_HEAR_DANGER" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_AT_ASSAULT_POINT, " Tasks" " TASK_FACE_ASSAULT_POINT 0" " TASK_HIT_ASSAULT_POINT 0" " " " Interrupts" " COND_NO_PRIMARY_AMMO" " COND_HEAR_DANGER" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_WAIT_AND_CLEAR, " Tasks" " TASK_FACE_ASSAULT_POINT 0" " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" " TASK_AWAIT_ASSAULT_TIMEOUT 0" " TASK_ANNOUNCE_CLEAR 0" " " " Interrupts" " COND_NEW_ENEMY" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_CAN_RANGE_ATTACK1" " COND_CAN_MELEE_ATTACK1" " COND_CAN_RANGE_ATTACK2" " COND_CAN_MELEE_ATTACK2" " COND_HEAR_DANGER" " COND_HEAR_BULLET_IMPACT" " COND_TOO_CLOSE_TO_ATTACK" " COND_NOT_FACING_ATTACK" " COND_PLAYER_PUSHING" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_CLEAR_ASSAULT_POINT, " Tasks" " TASK_ANNOUNCE_CLEAR 0" " " " Interrupts" ) //========================================================= //========================================================= DEFINE_SCHEDULE ( SCHED_ASSAULT_MOVE_AWAY, " Tasks" " TASK_MOVE_AWAY_PATH 120" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" " " " Interrupts" ) AI_END_CUSTOM_SCHEDULE_PROVIDER()