//========= Copyright © 1996-2005, 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, 1.5, 2.5, 1, 3, 25, 0 };

#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 &params, 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<CAI_BattleLine *>(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 );
	}
}

//-------------------------------------

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();
}

//-----------------------------------------------------------------------------
// Purpose: 
// Input  : &vecPos - 
//-----------------------------------------------------------------------------
void CAI_StandoffBehavior::ClearStandoffGoalPosition()
{
	if ( m_vecStandoffGoalPosition != GOAL_POSITION_INVALID )
	{
		m_vecStandoffGoalPosition = GOAL_POSITION_INVALID;
		UpdateBattleLines();
		OnChangeTacticalConstraints();
		GetOuter()->ClearSchedule();
	}
}

//-----------------------------------------------------------------------------
// 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 )
			return false;
	}
	
	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_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<AIHANDLE> &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();
	}
	
	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()

///-----------------------------------------------------------------------------
