//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Combat behaviors for AIs in a relatively self-preservationist mode. // Lots of cover taking and attempted shots out of cover. // //=============================================================================// #include "cbase.h" #include "ai_hint.h" #include "ai_node.h" #include "ai_navigator.h" #include "ai_tacticalservices.h" #include "ai_behavior_standoff.h" #include "ai_senses.h" #include "ai_squad.h" #include "ai_goalentity.h" #include "ndebugoverlay.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define GOAL_POSITION_INVALID Vector( FLT_MAX, FLT_MAX, FLT_MAX ) ConVar DrawBattleLines( "ai_drawbattlelines", "0", FCVAR_CHEAT ); static AI_StandoffParams_t AI_DEFAULT_STANDOFF_PARAMS = { AIHCR_MOVE_ON_COVER, true, true, 2.5, 1., 3, 25, 0, false, 0.f }; #define MAKE_ACTMAP_KEY( posture, activity ) ( (((unsigned)(posture)) << 16) | ((unsigned)(activity)) ) // #define DEBUG_STANDOFF 1 #ifdef DEBUG_STANDOFF #define StandoffMsg( msg ) DevMsg( GetOuter(), msg ) #define StandoffMsg1( msg, a ) DevMsg( GetOuter(), msg, a ) #define StandoffMsg2( msg, a, b ) DevMsg( GetOuter(), msg, a, b ) #define StandoffMsg3( msg, a, b, c ) DevMsg( GetOuter(), msg, a, b, c ) #define StandoffMsg4( msg, a, b, c, d ) DevMsg( GetOuter(), msg, a, b, c, d ) #define StandoffMsg5( msg, a, b, c, d, e ) DevMsg( GetOuter(), msg, a, b, c, d, e ) #else #define StandoffMsg( msg ) ((void)0) #define StandoffMsg1( msg, a ) ((void)0) #define StandoffMsg2( msg, a, b ) ((void)0) #define StandoffMsg3( msg, a, b, c ) ((void)0) #define StandoffMsg4( msg, a, b, c, d ) ((void)0) #define StandoffMsg5( msg, a, b, c, d, e ) ((void)0) #endif //----------------------------------------------------------------------------- // // CAI_BattleLine // //----------------------------------------------------------------------------- const float AIBL_THINK_INTERVAL = 0.3; class CAI_BattleLine : public CBaseEntity { DECLARE_CLASS( CAI_BattleLine, CBaseEntity ); public: string_t m_iszActor; bool m_fActive; bool m_fStrict; void Spawn() { if ( m_fActive ) { SetThink(&CAI_BattleLine::MovementThink); SetNextThink( gpGlobals->curtime + AIBL_THINK_INTERVAL ); m_SelfMoveMonitor.SetMark( this, 60 ); } } virtual void InputActivate( inputdata_t &inputdata ) { if ( !m_fActive ) { m_fActive = true; NotifyChangeTacticalConstraints(); SetThink(&CAI_BattleLine::MovementThink); SetNextThink( gpGlobals->curtime + AIBL_THINK_INTERVAL ); m_SelfMoveMonitor.SetMark( this, 60 ); } } virtual void InputDeactivate( inputdata_t &inputdata ) { if ( m_fActive ) { m_fActive = false; NotifyChangeTacticalConstraints(); SetThink(NULL); } } void UpdateOnRemove() { if ( m_fActive ) { m_fActive = false; NotifyChangeTacticalConstraints(); } BaseClass::UpdateOnRemove(); } bool Affects( CAI_BaseNPC *pNpc ) { const char *pszNamedActor = STRING( m_iszActor ); if ( pNpc->NameMatches( pszNamedActor ) || pNpc->ClassMatches( pszNamedActor ) || ( pNpc->GetSquad() && stricmp( pNpc->GetSquad()->GetName(), pszNamedActor ) == 0 ) ) { return true; } return false; } void MovementThink() { if ( m_SelfMoveMonitor.TargetMoved( this ) ) { NotifyChangeTacticalConstraints(); m_SelfMoveMonitor.SetMark( this, 60 ); } SetNextThink( gpGlobals->curtime + AIBL_THINK_INTERVAL ); } private: void NotifyChangeTacticalConstraints() { for ( int i = 0; i < g_AI_Manager.NumAIs(); i++ ) { CAI_BaseNPC *pNpc = (g_AI_Manager.AccessAIs())[i]; if ( Affects( pNpc ) ) { CAI_StandoffBehavior *pBehavior; if ( pNpc->GetBehavior( &pBehavior ) ) { pBehavior->OnChangeTacticalConstraints(); } } } } CAI_MoveMonitor m_SelfMoveMonitor; DECLARE_DATADESC(); }; //------------------------------------- LINK_ENTITY_TO_CLASS( ai_battle_line, CAI_BattleLine ); BEGIN_DATADESC( CAI_BattleLine ) DEFINE_KEYFIELD( m_iszActor, FIELD_STRING, "Actor" ), DEFINE_KEYFIELD( m_fActive, FIELD_BOOLEAN, "Active" ), DEFINE_KEYFIELD( m_fStrict, FIELD_BOOLEAN, "Strict" ), DEFINE_EMBEDDED( m_SelfMoveMonitor ), // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "Activate", InputActivate ), DEFINE_INPUTFUNC( FIELD_VOID, "Deactivate", InputDeactivate ), DEFINE_THINKFUNC( MovementThink ), END_DATADESC() //----------------------------------------------------------------------------- // // CAI_StandoffBehavior // //----------------------------------------------------------------------------- BEGIN_SIMPLE_DATADESC( AI_StandoffParams_t ) DEFINE_FIELD( hintChangeReaction, FIELD_INTEGER ), DEFINE_FIELD( fPlayerIsBattleline, FIELD_BOOLEAN ), DEFINE_FIELD( fCoverOnReload, FIELD_BOOLEAN ), DEFINE_FIELD( minTimeShots, FIELD_FLOAT ), DEFINE_FIELD( maxTimeShots, FIELD_FLOAT ), DEFINE_FIELD( minShots, FIELD_INTEGER ), DEFINE_FIELD( maxShots, FIELD_INTEGER ), DEFINE_FIELD( oddsCover, FIELD_INTEGER ), DEFINE_FIELD( fStayAtCover, FIELD_BOOLEAN ), DEFINE_FIELD( flAbandonTimeLimit, FIELD_FLOAT ), END_DATADESC(); BEGIN_DATADESC( CAI_StandoffBehavior ) DEFINE_FIELD( m_fActive, FIELD_BOOLEAN ), DEFINE_FIELD( m_fTestNoDamage, FIELD_BOOLEAN ), DEFINE_FIELD( m_vecStandoffGoalPosition, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_posture, FIELD_INTEGER ), DEFINE_EMBEDDED( m_params ), DEFINE_FIELD( m_hStandoffGoal, FIELD_EHANDLE ), DEFINE_FIELD( m_fTakeCover, FIELD_BOOLEAN ), DEFINE_FIELD( m_SavedDistTooFar, FIELD_FLOAT ), DEFINE_FIELD( m_fForceNewEnemy, FIELD_BOOLEAN ), DEFINE_EMBEDDED( m_PlayerMoveMonitor ), DEFINE_EMBEDDED( m_TimeForceCoverHint ), DEFINE_EMBEDDED( m_TimePreventForceNewEnemy ), DEFINE_EMBEDDED( m_RandomCoverChangeTimer ), // m_UpdateBattleLinesSemaphore (not saved, only an in-think item) // m_BattleLines (not saved, rebuilt) DEFINE_FIELD( m_fIgnoreFronts, FIELD_BOOLEAN ), // m_ActivityMap (not saved, rebuilt) // m_bHasLowCoverActivity (not saved, rebuilt) DEFINE_FIELD( m_nSavedMinShots, FIELD_INTEGER ), DEFINE_FIELD( m_nSavedMaxShots, FIELD_INTEGER ), DEFINE_FIELD( m_flSavedMinRest, FIELD_FLOAT ), DEFINE_FIELD( m_flSavedMaxRest, FIELD_FLOAT ), END_DATADESC(); //------------------------------------- CAI_StandoffBehavior::CAI_StandoffBehavior( CAI_BaseNPC *pOuter ) : CAI_MappedActivityBehavior_Temporary( pOuter ) { m_fActive = false; SetParameters( AI_DEFAULT_STANDOFF_PARAMS ); SetPosture( AIP_STANDING ); m_SavedDistTooFar = FLT_MAX; m_fForceNewEnemy = false; m_TimePreventForceNewEnemy.Set( 3.0, 6.0 ); m_fIgnoreFronts = false; m_bHasLowCoverActivity = false; } //------------------------------------- void CAI_StandoffBehavior::SetActive( bool fActive ) { if ( fActive != m_fActive ) { if ( fActive ) { GetOuter()->SpeakSentence( STANDOFF_SENTENCE_BEGIN_STANDOFF ); } else { GetOuter()->SpeakSentence( STANDOFF_SENTENCE_END_STANDOFF ); } m_fActive = fActive; NotifyChangeBehaviorStatus(); } } //------------------------------------- void CAI_StandoffBehavior::SetParameters( const AI_StandoffParams_t ¶ms, CAI_GoalEntity *pGoalEntity ) { m_params = params; m_hStandoffGoal = pGoalEntity; m_vecStandoffGoalPosition = GOAL_POSITION_INVALID; if ( GetOuter() && GetOuter()->GetShotRegulator() ) { GetOuter()->GetShotRegulator()->SetBurstShotCountRange( m_params.minShots, m_params.maxShots ); GetOuter()->GetShotRegulator()->SetRestInterval( m_params.minTimeShots, m_params.maxTimeShots ); } } //------------------------------------- bool CAI_StandoffBehavior::CanSelectSchedule() { if ( !m_bHasLowCoverActivity ) m_fActive = false; if ( !m_fActive ) return false; return ( GetNpcState() == NPC_STATE_COMBAT && GetOuter()->GetActiveWeapon() != NULL ); } //------------------------------------- void CAI_StandoffBehavior::Spawn() { BaseClass::Spawn(); UpdateTranslateActivityMap(); } //------------------------------------- void CAI_StandoffBehavior::BeginScheduleSelection() { m_fTakeCover = true; // FIXME: Improve!!! GetOuter()->GetShotRegulator()->GetBurstShotCountRange( &m_nSavedMinShots, &m_nSavedMaxShots ); GetOuter()->GetShotRegulator()->GetRestInterval( &m_flSavedMinRest, &m_flSavedMaxRest ); GetOuter()->GetShotRegulator()->SetBurstShotCountRange( m_params.minShots, m_params.maxShots ); GetOuter()->GetShotRegulator()->SetRestInterval( m_params.minTimeShots, m_params.maxTimeShots ); GetOuter()->GetShotRegulator()->Reset(); m_SavedDistTooFar = GetOuter()->m_flDistTooFar; GetOuter()->m_flDistTooFar = FLT_MAX; m_TimeForceCoverHint.Set( 8, false ); m_RandomCoverChangeTimer.Set( 8, 16, false ); UpdateTranslateActivityMap(); } void CAI_StandoffBehavior::OnUpdateShotRegulator() { GetOuter()->GetShotRegulator()->SetBurstShotCountRange( m_params.minShots, m_params.maxShots ); GetOuter()->GetShotRegulator()->SetRestInterval( m_params.minTimeShots, m_params.maxTimeShots ); } //------------------------------------- void CAI_StandoffBehavior::EndScheduleSelection() { UnlockHintNode(); m_vecStandoffGoalPosition = GOAL_POSITION_INVALID; GetOuter()->m_flDistTooFar = m_SavedDistTooFar; // FIXME: Improve!!! GetOuter()->GetShotRegulator()->SetBurstShotCountRange( m_nSavedMinShots, m_nSavedMaxShots ); GetOuter()->GetShotRegulator()->SetRestInterval( m_flSavedMinRest, m_flSavedMaxRest ); } //------------------------------------- void CAI_StandoffBehavior::PrescheduleThink() { VPROF_BUDGET( "CAI_StandoffBehavior::PrescheduleThink", VPROF_BUDGETGROUP_NPCS ); BaseClass::PrescheduleThink(); if( DrawBattleLines.GetInt() != 0 ) { CBaseEntity *pEntity = NULL; while ((pEntity = gEntList.FindEntityByClassname( pEntity, "ai_battle_line" )) != NULL) { // Visualize the battle line and its normal. CAI_BattleLine *pLine = dynamic_cast(pEntity); if( pLine->m_fActive ) { Vector normal; pLine->GetVectors( &normal, NULL, NULL ); NDebugOverlay::Line( pLine->GetAbsOrigin() - Vector( 0, 0, 64 ), pLine->GetAbsOrigin() + Vector(0,0,64), 0,255,0, false, 0.1 ); } } } } //------------------------------------- void CAI_StandoffBehavior::GatherConditions() { CBaseEntity *pLeader = GetPlayerLeader(); if ( pLeader && m_TimeForceCoverHint.Expired() ) { if ( m_PlayerMoveMonitor.IsMarkSet() ) { if (m_PlayerMoveMonitor.TargetMoved( pLeader ) ) { OnChangeTacticalConstraints(); m_PlayerMoveMonitor.ClearMark(); } } else { m_PlayerMoveMonitor.SetMark( pLeader, 60 ); } } if ( m_fForceNewEnemy ) { m_TimePreventForceNewEnemy.Reset(); GetOuter()->SetEnemy( NULL ); } BaseClass::GatherConditions(); m_fForceNewEnemy = false; ClearCondition( COND_ABANDON_TIME_EXPIRED ); bool bAbandonStandoff = false; CAI_Squad *pSquad = GetOuter()->GetSquad(); AISquadIter_t iter; if ( GetEnemy() ) { AI_EnemyInfo_t *pEnemyInfo = GetOuter()->GetEnemies()->Find( GetEnemy() ); if ( pEnemyInfo && m_params.flAbandonTimeLimit > 0 && ( ( pEnemyInfo->timeAtFirstHand != AI_INVALID_TIME && gpGlobals->curtime - pEnemyInfo->timeLastSeen > m_params.flAbandonTimeLimit ) || ( pEnemyInfo->timeAtFirstHand == AI_INVALID_TIME && gpGlobals->curtime - pEnemyInfo->timeFirstSeen > m_params.flAbandonTimeLimit * 2 ) ) ) { SetCondition( COND_ABANDON_TIME_EXPIRED ); bAbandonStandoff = true; if ( pSquad ) { for ( CAI_BaseNPC *pSquadMate = pSquad->GetFirstMember( &iter ); pSquadMate; pSquadMate = pSquad->GetNextMember( &iter ) ) { if ( pSquadMate->IsAlive() && pSquadMate != GetOuter() ) { CAI_StandoffBehavior *pSquadmateStandoff; pSquadMate->GetBehavior( &pSquadmateStandoff ); if ( pSquadmateStandoff && pSquadmateStandoff->IsActive() && pSquadmateStandoff->m_hStandoffGoal == m_hStandoffGoal && !pSquadmateStandoff->HasCondition( COND_ABANDON_TIME_EXPIRED ) ) { bAbandonStandoff = false; break; } } } } } } if ( bAbandonStandoff ) { if ( pSquad ) { for ( CAI_BaseNPC *pSquadMate = pSquad->GetFirstMember( &iter ); pSquadMate; pSquadMate = pSquad->GetNextMember( &iter ) ) { CAI_StandoffBehavior *pSquadmateStandoff; pSquadMate->GetBehavior( &pSquadmateStandoff ); if ( pSquadmateStandoff && pSquadmateStandoff->IsActive() && pSquadmateStandoff->m_hStandoffGoal == m_hStandoffGoal ) pSquadmateStandoff->SetActive( false ); } } else SetActive( false ); } else if ( GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) { if( DrawBattleLines.GetInt() != 0 ) { if ( IsBehindBattleLines( GetAbsOrigin() ) ) { NDebugOverlay::Box( GetOuter()->GetAbsOrigin(), -Vector(48,48,4), Vector(48,48,4), 255,0,0,8, 0.1 ); } else { NDebugOverlay::Box( GetOuter()->GetAbsOrigin(), -Vector(48,48,4), Vector(48,48,4), 0,255,0,8, 0.1 ); } } } } //------------------------------------- int CAI_StandoffBehavior::SelectScheduleUpdateWeapon( void ) { // Check if need to reload if ( HasCondition ( COND_NO_PRIMARY_AMMO ) || HasCondition ( COND_LOW_PRIMARY_AMMO )) { StandoffMsg( "Out of ammo, reloading\n" ); if ( m_params.fCoverOnReload ) { GetOuter()->SpeakSentence( STANDOFF_SENTENCE_OUT_OF_AMMO ); return SCHED_HIDE_AND_RELOAD; } return SCHED_RELOAD; } // Otherwise, update planned shots to fire before taking cover again if ( HasCondition( COND_LIGHT_DAMAGE ) ) { // if hurt: int iPercent = random->RandomInt(0,99); if ( iPercent <= m_params.oddsCover && GetEnemy() != NULL ) { SetReuseCurrentCover(); StandoffMsg( "Hurt, firing one more shot before cover\n" ); if ( !GetOuter()->GetShotRegulator()->IsInRestInterval() ) { GetOuter()->GetShotRegulator()->SetBurstShotsRemaining( 1 ); } } } return SCHED_NONE; } //------------------------------------- int CAI_StandoffBehavior::SelectScheduleCheckCover( void ) { if ( m_fTakeCover ) { m_fTakeCover = false; if ( GetEnemy() ) { GetOuter()->SpeakSentence( STANDOFF_SENTENCE_FORCED_TAKE_COVER ); StandoffMsg( "Taking forced cover\n" ); return SCHED_TAKE_COVER_FROM_ENEMY; } } if ( GetOuter()->GetShotRegulator()->IsInRestInterval() ) { StandoffMsg( "Regulated to not shoot\n" ); if ( GetHintType() == HINT_TACTICAL_COVER_LOW ) SetPosture( AIP_CROUCHING ); else SetPosture( AIP_STANDING ); if ( random->RandomInt(0,99) < 80 ) SetReuseCurrentCover(); return SCHED_TAKE_COVER_FROM_ENEMY; } return SCHED_NONE; } //------------------------------------- int CAI_StandoffBehavior::SelectScheduleEstablishAim( void ) { if ( HasCondition( COND_ENEMY_OCCLUDED ) ) { if ( GetPosture() == AIP_CROUCHING ) { // force a stand up, just in case GetOuter()->SpeakSentence( STANDOFF_SENTENCE_STAND_CHECK_TARGET ); StandoffMsg( "Crouching, standing up to gain LOS\n" ); SetPosture( AIP_PEEKING ); return SCHED_STANDOFF; } else if ( GetPosture() == AIP_PEEKING ) { if ( m_TimePreventForceNewEnemy.Expired() ) { // Look for a new enemy m_fForceNewEnemy = true; StandoffMsg( "Looking for enemy\n" ); } } #if 0 else { return SCHED_ESTABLISH_LINE_OF_FIRE; } #endif } return SCHED_NONE; } //------------------------------------- int CAI_StandoffBehavior::SelectScheduleAttack( void ) { if ( GetPosture() == AIP_PEEKING || GetPosture() == AIP_STANDING ) { if ( !HasCondition( COND_CAN_RANGE_ATTACK1 ) && !HasCondition( COND_CAN_MELEE_ATTACK1 ) && HasCondition( COND_TOO_FAR_TO_ATTACK ) ) { if ( GetOuter()->GetActiveWeapon() && ( GetOuter()->GetActiveWeapon()->CapabilitiesGet() & bits_CAP_WEAPON_RANGE_ATTACK1 ) ) { if ( !HasCondition( COND_ENEMY_OCCLUDED ) || random->RandomInt(0,99) < 50 ) // Don't advance, just fire anyway return SCHED_RANGE_ATTACK1; } } } return SCHED_NONE; } //------------------------------------- int CAI_StandoffBehavior::SelectSchedule( void ) { switch ( GetNpcState() ) { case NPC_STATE_COMBAT: { int schedule = SCHED_NONE; schedule = SelectScheduleUpdateWeapon(); if ( schedule != SCHED_NONE ) return schedule; schedule = SelectScheduleCheckCover(); if ( schedule != SCHED_NONE ) return schedule; schedule = SelectScheduleEstablishAim(); if ( schedule != SCHED_NONE ) return schedule; schedule = SelectScheduleAttack(); if ( schedule != SCHED_NONE ) return schedule; break; } } return BaseClass::SelectSchedule(); } //------------------------------------- int CAI_StandoffBehavior::TranslateSchedule( int schedule ) { if ( schedule == SCHED_CHASE_ENEMY ) { StandoffMsg( "trying SCHED_ESTABLISH_LINE_OF_FIRE\n" ); return SCHED_ESTABLISH_LINE_OF_FIRE; } return BaseClass::TranslateSchedule( schedule ); } //------------------------------------- void CAI_StandoffBehavior::BuildScheduleTestBits() { BaseClass::BuildScheduleTestBits(); if ( IsCurSchedule( SCHED_TAKE_COVER_FROM_ENEMY ) ) GetOuter()->ClearCustomInterruptCondition( COND_NEW_ENEMY ); } //------------------------------------- Activity CAI_MappedActivityBehavior_Temporary::GetMappedActivity( AI_Posture_t posture, Activity activity ) { if ( posture != AIP_STANDING ) { unsigned short iActivityTranslation = m_ActivityMap.Find( MAKE_ACTMAP_KEY( posture, activity ) ); if ( iActivityTranslation != m_ActivityMap.InvalidIndex() ) { Activity result = m_ActivityMap[iActivityTranslation]; return result; } } return ACT_INVALID; } //------------------------------------- Activity CAI_StandoffBehavior::NPC_TranslateActivity( Activity activity ) { Activity coverActivity = GetCoverActivity(); if ( coverActivity != ACT_INVALID ) { if ( activity == ACT_IDLE ) activity = coverActivity; if ( GetPosture() == AIP_STANDING && coverActivity == ACT_COVER_LOW ) SetPosture( AIP_CROUCHING ); } Activity result = GetMappedActivity( GetPosture(), activity ); if ( result != ACT_INVALID) return result; return BaseClass::NPC_TranslateActivity( activity ); } //----------------------------------------------------------------------------- // Purpose: // Input : &vecPos - //----------------------------------------------------------------------------- void CAI_StandoffBehavior::SetStandoffGoalPosition( const Vector &vecPos ) { m_vecStandoffGoalPosition = vecPos; UpdateBattleLines(); OnChangeTacticalConstraints(); GetOuter()->ClearSchedule( "Standoff goal position changed" ); } //----------------------------------------------------------------------------- // Purpose: // Input : &vecPos - //----------------------------------------------------------------------------- void CAI_StandoffBehavior::ClearStandoffGoalPosition() { if ( m_vecStandoffGoalPosition != GOAL_POSITION_INVALID ) { m_vecStandoffGoalPosition = GOAL_POSITION_INVALID; UpdateBattleLines(); OnChangeTacticalConstraints(); GetOuter()->ClearSchedule( "Standoff goal position cleared" ); } } //----------------------------------------------------------------------------- // Purpose: // Output : Vector //----------------------------------------------------------------------------- Vector CAI_StandoffBehavior::GetStandoffGoalPosition() { if( m_vecStandoffGoalPosition != GOAL_POSITION_INVALID ) { return m_vecStandoffGoalPosition; } else if( PlayerIsLeading() ) { return UTIL_GetLocalPlayer()->GetAbsOrigin(); } else { CAI_BattleLine *pBattleLine = NULL; for (;;) { pBattleLine = (CAI_BattleLine *)gEntList.FindEntityByClassname( pBattleLine, "ai_battle_line" ); if ( !pBattleLine ) break; if ( pBattleLine->m_fActive && pBattleLine->Affects( GetOuter() ) ) { StandoffMsg1( "Using battleline %s as goal\n", STRING( pBattleLine->GetEntityName() ) ); return pBattleLine->GetAbsOrigin(); } } } return GetAbsOrigin(); } //------------------------------------- void CAI_StandoffBehavior::UpdateBattleLines() { if ( m_UpdateBattleLinesSemaphore.EnterThink() ) { // @TODO (toml 06-19-03): This is the quick to code thing. Could use some optimization/caching to not recalc everything (up to) each think m_BattleLines.RemoveAll(); bool bHaveGoalPosition = ( m_vecStandoffGoalPosition != GOAL_POSITION_INVALID ); if ( bHaveGoalPosition ) { // If we have a valid standoff goal position, it takes precendence. const float DIST_GOAL_PLANE = 180; BattleLine_t goalLine; if ( GetDirectionOfStandoff( &goalLine.normal ) ) { goalLine.point = GetStandoffGoalPosition() + goalLine.normal * DIST_GOAL_PLANE; m_BattleLines.AddToTail( goalLine ); } } else if ( PlayerIsLeading() && GetEnemy() ) { if ( m_params.fPlayerIsBattleline ) { const float DIST_PLAYER_PLANE = 180; CBaseEntity *pPlayer = UTIL_GetLocalPlayer(); BattleLine_t playerLine; if ( GetDirectionOfStandoff( &playerLine.normal ) ) { playerLine.point = pPlayer->GetAbsOrigin() + playerLine.normal * DIST_PLAYER_PLANE; m_BattleLines.AddToTail( playerLine ); } } } CAI_BattleLine *pBattleLine = NULL; for (;;) { pBattleLine = (CAI_BattleLine *)gEntList.FindEntityByClassname( pBattleLine, "ai_battle_line" ); if ( !pBattleLine ) break; if ( pBattleLine->m_fActive && (pBattleLine->m_fStrict || !bHaveGoalPosition ) && pBattleLine->Affects( GetOuter() ) ) { BattleLine_t battleLine; battleLine.point = pBattleLine->GetAbsOrigin(); battleLine.normal = UTIL_YawToVector( pBattleLine->GetAbsAngles().y ); m_BattleLines.AddToTail( battleLine ); } } } } //------------------------------------- bool CAI_StandoffBehavior::IsBehindBattleLines( const Vector &point ) { UpdateBattleLines(); Vector vecToPoint; for ( int i = 0; i < m_BattleLines.Count(); i++ ) { vecToPoint = point - m_BattleLines[i].point; VectorNormalize( vecToPoint ); vecToPoint.z = 0; if ( DotProduct( m_BattleLines[i].normal, vecToPoint ) > 0 ) { if( DrawBattleLines.GetInt() != 0 ) { NDebugOverlay::Box( point, -Vector(48,48,4), Vector(48,48,4), 0,255,0,8, 1 ); NDebugOverlay::Line( point, GetOuter()->GetAbsOrigin(), 0,255,0,true, 1 ); } return false; } } if( DrawBattleLines.GetInt() != 0 ) { NDebugOverlay::Box( point, -Vector(48,48,4), Vector(48,48,4), 255,0,0,8, 1 ); NDebugOverlay::Line( point, GetOuter()->GetAbsOrigin(), 255,0,0,true, 1 ); } return true; } //------------------------------------- bool CAI_StandoffBehavior::IsValidCover( const Vector &vecCoverLocation, const CAI_Hint *pHint ) { if ( !BaseClass::IsValidCover( vecCoverLocation, pHint ) ) return false; if ( IsCurSchedule( SCHED_TAKE_COVER_FROM_BEST_SOUND ) ) return true; return ( m_fIgnoreFronts || IsBehindBattleLines( vecCoverLocation ) ); } //------------------------------------- bool CAI_StandoffBehavior::IsValidShootPosition( const Vector &vLocation, CAI_Node *pNode, const CAI_Hint *pHint ) { if ( !BaseClass::IsValidShootPosition( vLocation, pNode, pHint ) ) return false; return ( m_fIgnoreFronts || IsBehindBattleLines( vLocation ) ); } //------------------------------------- void CAI_StandoffBehavior::StartTask( const Task_t *pTask ) { bool fCallBase = false; switch ( pTask->iTask ) { case TASK_RANGE_ATTACK1: { fCallBase = true; break; } case TASK_FIND_COVER_FROM_ENEMY: { StandoffMsg( "TASK_FIND_COVER_FROM_ENEMY\n" ); // If within time window to force change if ( !m_params.fStayAtCover && (!m_TimeForceCoverHint.Expired() || m_RandomCoverChangeTimer.Expired()) ) { m_TimeForceCoverHint.Force(); m_RandomCoverChangeTimer.Set( 8, 16, false ); // @TODO (toml 03-24-03): clean this up be tool-izing base tasks. Right now, this is here to force to not use lateral cover search CBaseEntity *pEntity = GetEnemy(); if ( pEntity == NULL ) { // Find cover from self if no enemy available pEntity = GetOuter(); } CBaseEntity *pLeader = GetPlayerLeader(); if ( pLeader ) { m_PlayerMoveMonitor.SetMark( pLeader, 60 ); } Vector coverPos = vec3_origin; CAI_TacticalServices * pTacticalServices = GetTacticalServices(); const Vector & enemyPos = pEntity->GetAbsOrigin(); Vector enemyEyePos = pEntity->EyePosition(); float coverRadius = GetOuter()->CoverRadius(); const Vector & goalPos = GetStandoffGoalPosition(); bool bTryGoalPosFirst = true; if( pLeader && m_vecStandoffGoalPosition == GOAL_POSITION_INVALID ) { if( random->RandomInt(1, 100) <= 50 ) { // Half the time, if the player is leading, try to find a spot near them bTryGoalPosFirst = false; StandoffMsg( "Not trying goal pos\n" ); } } if( bTryGoalPosFirst ) { // Firstly, try to find cover near the goal position. pTacticalServices->FindCoverPos( goalPos, enemyPos, enemyEyePos, 0, 15*12, &coverPos ); if ( coverPos == vec3_origin ) pTacticalServices->FindCoverPos( goalPos, enemyPos, enemyEyePos, 15*12-0.1, 40*12, &coverPos ); StandoffMsg1( "Trying goal pos, %s\n", ( coverPos == vec3_origin ) ? "failed" : "succeeded" ); } if ( coverPos == vec3_origin ) { // Otherwise, find a node near to self StandoffMsg( "Looking for near cover\n" ); if ( !GetTacticalServices()->FindCoverPos( enemyPos, enemyEyePos, 0, coverRadius, &coverPos ) ) { // Try local lateral cover if ( !GetTacticalServices()->FindLateralCover( enemyEyePos, 0, &coverPos ) ) { // At this point, try again ignoring front lines. Any cover probably better than hanging out in the open m_fIgnoreFronts = true; if ( !GetTacticalServices()->FindCoverPos( enemyPos, enemyEyePos, 0, coverRadius, &coverPos ) ) { if ( !GetTacticalServices()->FindLateralCover( enemyEyePos, 0, &coverPos ) ) { Assert( coverPos == vec3_origin ); } } m_fIgnoreFronts = false; } } } if ( coverPos != vec3_origin ) { AI_NavGoal_t goal(GOALTYPE_COVER, coverPos, ACT_RUN, AIN_HULL_TOLERANCE, AIN_DEF_FLAGS); GetNavigator()->SetGoal( goal ); GetOuter()->m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData; TaskComplete(); } else TaskFail(FAIL_NO_COVER); } else { fCallBase = true; } break; } default: { fCallBase = true; } } if ( fCallBase ) BaseClass::StartTask( pTask ); } //------------------------------------- void CAI_StandoffBehavior::OnChangeHintGroup( string_t oldGroup, string_t newGroup ) { OnChangeTacticalConstraints(); } //------------------------------------- void CAI_StandoffBehavior::OnChangeTacticalConstraints() { if ( m_params.hintChangeReaction > AIHCR_DEFAULT_AI ) m_TimeForceCoverHint.Set( 8.0, false ); if ( m_params.hintChangeReaction == AIHCR_MOVE_IMMEDIATE ) m_fTakeCover = true; } //------------------------------------- bool CAI_StandoffBehavior::PlayerIsLeading() { CBaseEntity *pPlayer = AI_GetSinglePlayer(); return ( pPlayer && GetOuter()->IRelationType( pPlayer ) == D_LI ); } //------------------------------------- CBaseEntity *CAI_StandoffBehavior::GetPlayerLeader() { CBaseEntity *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && GetOuter()->IRelationType( pPlayer ) == D_LI ) return pPlayer; return NULL; } //------------------------------------- bool CAI_StandoffBehavior::GetDirectionOfStandoff( Vector *pDir ) { if ( GetEnemy() ) { *pDir = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); VectorNormalize( *pDir ); pDir->z = 0; return true; } return false; } //------------------------------------- Hint_e CAI_StandoffBehavior::GetHintType() { CAI_Hint *pHintNode = GetHintNode(); if ( pHintNode ) return pHintNode->HintType(); return HINT_NONE; } //------------------------------------- void CAI_StandoffBehavior::SetReuseCurrentCover() { CAI_Hint *pHintNode = GetHintNode(); if ( pHintNode && pHintNode->GetNode() && pHintNode->GetNode()->IsLocked() ) pHintNode->GetNode()->Unlock(); } //------------------------------------- void CAI_StandoffBehavior::UnlockHintNode() { CAI_Hint *pHintNode = GetHintNode(); if ( pHintNode ) { if ( pHintNode->IsLocked() && pHintNode->IsLockedBy( GetOuter() ) ) pHintNode->Unlock(); CAI_Node *pNode = pHintNode->GetNode(); if ( pNode && pNode->IsLocked() ) pNode->Unlock(); ClearHintNode(); } } //------------------------------------- Activity CAI_StandoffBehavior::GetCoverActivity() { CAI_Hint *pHintNode = GetHintNode(); if ( pHintNode && pHintNode->HintType() == HINT_TACTICAL_COVER_LOW ) return GetOuter()->GetCoverActivity( pHintNode ); return ACT_INVALID; } //------------------------------------- struct AI_ActivityMapping_t { AI_Posture_t posture; Activity activity; const char * pszWeapon; Activity translation; }; void CAI_MappedActivityBehavior_Temporary::UpdateTranslateActivityMap() { AI_ActivityMapping_t mappings[] = // This array cannot be static, as some activity values are set on a per-map-load basis { { AIP_CROUCHING, ACT_IDLE, NULL, ACT_COVER_LOW, }, { AIP_CROUCHING, ACT_IDLE_ANGRY, NULL, ACT_COVER_LOW, }, { AIP_CROUCHING, ACT_WALK, NULL, ACT_WALK_CROUCH, }, { AIP_CROUCHING, ACT_RUN, NULL, ACT_RUN_CROUCH, }, { AIP_CROUCHING, ACT_WALK_AIM, NULL, ACT_WALK_CROUCH_AIM, }, { AIP_CROUCHING, ACT_RUN_AIM, NULL, ACT_RUN_CROUCH_AIM, }, { AIP_CROUCHING, ACT_RELOAD, NULL, ACT_RELOAD_LOW, }, { AIP_CROUCHING, ACT_RANGE_ATTACK_SMG1, NULL, ACT_RANGE_ATTACK_SMG1_LOW, }, { AIP_CROUCHING, ACT_RANGE_ATTACK_AR2, NULL, ACT_RANGE_ATTACK_AR2_LOW, }, //---- { AIP_PEEKING, ACT_IDLE, NULL, ACT_RANGE_AIM_LOW, }, { AIP_PEEKING, ACT_IDLE_ANGRY, NULL, ACT_RANGE_AIM_LOW, }, { AIP_PEEKING, ACT_COVER_LOW, NULL, ACT_RANGE_AIM_LOW, }, { AIP_PEEKING, ACT_RANGE_ATTACK1, NULL, ACT_RANGE_ATTACK1_LOW, }, { AIP_PEEKING, ACT_RELOAD, NULL, ACT_RELOAD_LOW, }, }; m_ActivityMap.RemoveAll(); CBaseCombatWeapon *pWeapon = GetOuter()->GetActiveWeapon(); const char *pszWeaponClass = ( pWeapon ) ? pWeapon->GetClassname() : ""; for ( int i = 0; i < ARRAYSIZE(mappings); i++ ) { if ( !mappings[i].pszWeapon || stricmp( mappings[i].pszWeapon, pszWeaponClass ) == 0 ) { if ( HaveSequenceForActivity( mappings[i].translation ) || HaveSequenceForActivity( GetOuter()->Weapon_TranslateActivity( mappings[i].translation ) ) ) { Assert( m_ActivityMap.Find( MAKE_ACTMAP_KEY( mappings[i].posture, mappings[i].activity ) ) == m_ActivityMap.InvalidIndex() ); m_ActivityMap.Insert( MAKE_ACTMAP_KEY( mappings[i].posture, mappings[i].activity ), mappings[i].translation ); } } } } void CAI_StandoffBehavior::UpdateTranslateActivityMap() { BaseClass::UpdateTranslateActivityMap(); Activity lowCoverActivity = GetMappedActivity( AIP_CROUCHING, ACT_COVER_LOW ); if ( lowCoverActivity == ACT_INVALID ) lowCoverActivity = ACT_COVER_LOW; m_bHasLowCoverActivity = ( ( CapabilitiesGet() & bits_CAP_DUCK ) && (GetOuter()->TranslateActivity( lowCoverActivity ) != ACT_INVALID)); CBaseCombatWeapon *pWeapon = GetOuter()->GetActiveWeapon(); if ( pWeapon && (GetOuter()->TranslateActivity( lowCoverActivity ) == ACT_INVALID )) DevMsg( "Note: NPC class %s lacks ACT_COVER_LOW, therefore cannot participate in standoff\n", GetOuter()->GetClassname() ); } //------------------------------------- void CAI_MappedActivityBehavior_Temporary::OnChangeActiveWeapon( CBaseCombatWeapon *pOldWeapon, CBaseCombatWeapon *pNewWeapon ) { UpdateTranslateActivityMap(); } //------------------------------------- void CAI_StandoffBehavior::OnRestore() { UpdateTranslateActivityMap(); } //------------------------------------- AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER(CAI_StandoffBehavior) DECLARE_CONDITION( COND_ABANDON_TIME_EXPIRED ) AI_END_CUSTOM_SCHEDULE_PROVIDER() //----------------------------------------------------------------------------- // // CAI_StandoffGoal // // Purpose: A level tool to control the standoff behavior. Use is not required // in order to use behavior. // //----------------------------------------------------------------------------- AI_StandoffParams_t g_StandoffParamsByAgression[] = { // hintChangeReaction, fCoverOnReload, PlayerBtlLn, minTimeShots, maxTimeShots, minShots, maxShots, oddsCover flAbandonTimeLimit { AIHCR_MOVE_ON_COVER, true, true, 4.0, 8.0, 2, 4, 50, false, 30 }, // AGGR_VERY_LOW { AIHCR_MOVE_ON_COVER, true, true, 2.0, 5.0, 3, 5, 25, false, 20 }, // AGGR_LOW { AIHCR_MOVE_ON_COVER, true, true, 0.6, 2.5, 3, 6, 25, false, 10 }, // AGGR_MEDIUM { AIHCR_MOVE_ON_COVER, true, true, 0.2, 1.5, 5, 8, 10, false, 10 }, // AGGR_HIGH { AIHCR_MOVE_ON_COVER, false, true, 0, 0, 100, 100, 0, false, 5 }, // AGGR_VERY_HIGH }; //------------------------------------- class CAI_StandoffGoal : public CAI_GoalEntity { DECLARE_CLASS( CAI_StandoffGoal, CAI_GoalEntity ); public: CAI_StandoffGoal() { m_aggressiveness = AGGR_MEDIUM; m_fPlayerIsBattleline = true; m_HintChangeReaction = AIHCR_DEFAULT_AI; m_fStayAtCover = false; m_bAbandonIfEnemyHides = false; m_customParams = AI_DEFAULT_STANDOFF_PARAMS; } //--------------------------------- void EnableGoal( CAI_BaseNPC *pAI ) { CAI_StandoffBehavior *pBehavior; if ( !pAI->GetBehavior( &pBehavior ) ) return; pBehavior->SetActive( true ); SetBehaviorParams( pBehavior); } void DisableGoal( CAI_BaseNPC *pAI ) { // @TODO (toml 04-07-03): remove the no damage spawn flag once stable. The implementation isn't very good. CAI_StandoffBehavior *pBehavior; if ( !pAI->GetBehavior( &pBehavior ) ) return; pBehavior->SetActive( false ); SetBehaviorParams( pBehavior); } void InputActivate( inputdata_t &inputdata ) { ValidateAggression(); BaseClass::InputActivate( inputdata ); } void InputDeactivate( inputdata_t &inputdata ) { ValidateAggression(); BaseClass::InputDeactivate( inputdata ); } void InputSetAggressiveness( inputdata_t &inputdata ) { int newVal = inputdata.value.Int(); m_aggressiveness = (Aggressiveness_t)newVal; ValidateAggression(); UpdateActors(); const CUtlVector &actors = AccessActors(); for ( int i = 0; i < actors.Count(); i++ ) { CAI_BaseNPC *pAI = actors[i]; CAI_StandoffBehavior *pBehavior; if ( !pAI->GetBehavior( &pBehavior ) ) continue; SetBehaviorParams( pBehavior); } } void SetBehaviorParams( CAI_StandoffBehavior *pBehavior ) { AI_StandoffParams_t params; if ( m_aggressiveness != AGGR_CUSTOM ) params = g_StandoffParamsByAgression[m_aggressiveness]; else params = m_customParams; params.hintChangeReaction = m_HintChangeReaction; params.fPlayerIsBattleline = m_fPlayerIsBattleline; params.fStayAtCover = m_fStayAtCover; if ( !m_bAbandonIfEnemyHides ) params.flAbandonTimeLimit = 0; pBehavior->SetParameters( params, this ); pBehavior->OnChangeTacticalConstraints(); if ( pBehavior->IsRunning() ) pBehavior->GetOuter()->ClearSchedule( "Standoff behavior parms changed" ); } void ValidateAggression() { if ( m_aggressiveness < AGGR_VERY_LOW || m_aggressiveness > AGGR_VERY_HIGH ) { if ( m_aggressiveness != AGGR_CUSTOM ) { DevMsg( "Invalid aggressiveness value %d\n", m_aggressiveness ); if ( m_aggressiveness < AGGR_VERY_LOW ) m_aggressiveness = AGGR_VERY_LOW; else if ( m_aggressiveness > AGGR_VERY_HIGH ) m_aggressiveness = AGGR_VERY_HIGH; } } } private: //--------------------------------- DECLARE_DATADESC(); enum Aggressiveness_t { AGGR_VERY_LOW, AGGR_LOW, AGGR_MEDIUM, AGGR_HIGH, AGGR_VERY_HIGH, AGGR_CUSTOM, }; Aggressiveness_t m_aggressiveness; AI_HintChangeReaction_t m_HintChangeReaction; bool m_fPlayerIsBattleline; bool m_fStayAtCover; bool m_bAbandonIfEnemyHides; AI_StandoffParams_t m_customParams; }; //------------------------------------- LINK_ENTITY_TO_CLASS( ai_goal_standoff, CAI_StandoffGoal ); BEGIN_DATADESC( CAI_StandoffGoal ) DEFINE_KEYFIELD( m_aggressiveness, FIELD_INTEGER, "Aggressiveness" ), // m_customParams (individually) DEFINE_KEYFIELD( m_HintChangeReaction, FIELD_INTEGER, "HintGroupChangeReaction" ), DEFINE_KEYFIELD( m_fPlayerIsBattleline, FIELD_BOOLEAN, "PlayerBattleline" ), DEFINE_KEYFIELD( m_fStayAtCover, FIELD_BOOLEAN, "StayAtCover" ), DEFINE_KEYFIELD( m_bAbandonIfEnemyHides, FIELD_BOOLEAN, "AbandonIfEnemyHides" ), DEFINE_KEYFIELD( m_customParams.fCoverOnReload, FIELD_BOOLEAN, "CustomCoverOnReload" ), DEFINE_KEYFIELD( m_customParams.minTimeShots, FIELD_FLOAT, "CustomMinTimeShots" ), DEFINE_KEYFIELD( m_customParams.maxTimeShots, FIELD_FLOAT, "CustomMaxTimeShots" ), DEFINE_KEYFIELD( m_customParams.minShots, FIELD_INTEGER, "CustomMinShots" ), DEFINE_KEYFIELD( m_customParams.maxShots, FIELD_INTEGER, "CustomMaxShots" ), DEFINE_KEYFIELD( m_customParams.oddsCover, FIELD_INTEGER, "CustomOddsCover" ), // Inputs DEFINE_INPUTFUNC( FIELD_INTEGER, "SetAggressiveness", InputSetAggressiveness ), END_DATADESC() ///-----------------------------------------------------------------------------