//========= Copyright Valve Corporation, All rights reserved. ============//
// bot_npc.cpp
// A NextBot non-player derived actor
// Michael Booth, November 2010

#include "cbase.h"

#ifdef OBSOLETE_USE_BOSS_ALPHA

#ifdef TF_RAID_MODE

#include "tf_player.h"
#include "tf_gamerules.h"
#include "tf_team.h"
#include "tf_projectile_arrow.h"
#include "tf_projectile_rocket.h"
#include "tf_weapon_grenade_pipebomb.h"
#include "tf_ammo_pack.h"
#include "tf_obj_sentrygun.h"
#include "nav_mesh/tf_nav_area.h"
#include "bot_npc.h"
#include "NextBot/Path/NextBotChasePath.h"
#include "econ_wearable.h"
#include "team_control_point_master.h"
#include "particle_parse.h"
#include "CRagdollMagnet.h"
#include "nav_mesh/tf_path_follower.h"
#include "bot_npc_minion.h"
#include "player_vs_environment/monster_resource.h"
#include "bot/map_entities/tf_bot_generator.h"
#include "player_vs_environment/tf_population_manager.h"

//#define USE_BOSS_SENTRY


ConVar tf_bot_npc_health( "tf_bot_npc_health", "100000"/*, FCVAR_CHEAT*/ );		// 50000

ConVar tf_bot_npc_speed( "tf_bot_npc_speed", "300"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_attack_range( "tf_bot_npc_attack_range", "300"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_melee_damage( "tf_bot_npc_melee_damage", "150"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_threat_tolerance( "tf_bot_npc_threat_tolerance", "100"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_shoot_interval( "tf_bot_npc_shoot_interval", "15"/*, FCVAR_CHEAT*/ ); // 2
ConVar tf_bot_npc_aim_time( "tf_bot_npc_aim_time", "1"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_chase_range( "tf_bot_npc_chase_range", "300"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_grenade_launch_range( "tf_bot_npc_grenade_launch_range", "300"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_damage( "tf_bot_npc_grenade_damage", "25"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_minion_launch_count_initial( "tf_bot_npc_minion_launch_count_initial", "5"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_minion_launch_count_increase_interval( "tf_bot_npc_minion_launch_count_increase_interval", "999999999"/*, FCVAR_CHEAT*/ );	// 30
ConVar tf_bot_npc_minion_launch_initial_interval( "tf_bot_npc_minion_launch_initial_interval", "20"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_minion_launch_interval( "tf_bot_npc_minion_launch_interval", "30"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_chase_duration( "tf_bot_npc_chase_duration", "30"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_quit_range( "tf_bot_npc_quit_range", "2500"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_reaction_time( "tf_bot_npc_reaction_time", "0.5"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_charge_interval( "tf_bot_npc_charge_interval", "10"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_charge_pushaway_force( "tf_bot_npc_charge_pushaway_force", "500"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_charge_damage( "tf_bot_npc_charge_damage", "150"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_nuke_charge_time( "tf_bot_npc_nuke_charge_time", "5" );
ConVar tf_bot_npc_nuke_interval( "tf_bot_npc_nuke_interval", "20" );
ConVar tf_bot_npc_nuke_lethal_time( "tf_bot_npc_nuke_lethal_time", "999999999" );		// 300

ConVar tf_bot_npc_block_dps_react( "tf_bot_npc_block_dps_react", "150" );

ConVar tf_bot_npc_become_stunned_damage( "tf_bot_npc_become_stunned_damage", "500" );
ConVar tf_bot_npc_stunned_injury_multiplier( "tf_bot_npc_stunned_injury_multiplier", "10" );
ConVar tf_bot_npc_stunned_duration( "tf_bot_npc_stunned_duration", "5" );
ConVar tf_bot_npc_head_radius( "tf_bot_npc_head_radius", "75" );	// 50 

ConVar tf_bot_npc_stun_rocket_reflect_count( "tf_bot_npc_stun_rocket_reflect_count", "2"/*, FCVAR_CHEAT */ );
ConVar tf_bot_npc_stun_rocket_reflect_duration( "tf_bot_npc_stun_rocket_reflect_duration", "1"/*, FCVAR_CHEAT */ );

ConVar tf_bot_npc_grenade_interval( "tf_bot_npc_grenade_interval", "10" );

ConVar tf_bot_npc_hate_taunt_cooldown( "tf_bot_npc_hate_taunt_cooldown", "10"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_debug_damage( "tf_bot_npc_debug_damage", "0"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_always_stun( "tf_bot_npc_always_stun", "0"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_min_nuke_after_stun_time( "tf_bot_npc_min_nuke_after_stun_time", "5" /*, FCVAR_CHEAT */ );



//-----------------------------------------------------------------------------------------------------
// The Bot NPC
//-----------------------------------------------------------------------------------------------------
LINK_ENTITY_TO_CLASS( bot_boss, CBotNPC );

PRECACHE_REGISTER( bot_boss );

IMPLEMENT_SERVERCLASS_ST( CBotNPC, DT_BotNPC )

	SendPropEHandle( SENDINFO( m_laserTarget ) ),
	SendPropBool( SENDINFO( m_isNuking ) ),

END_SEND_TABLE()


//------------------------------------------------------------------------------
void CBotNPC::InputSpawn( inputdata_t &inputdata )
{
	DispatchSpawn( this );
}


//-----------------------------------------------------------------------------------------------------
CBotNPC::CBotNPC()
{
	m_intention = new CBotNPCIntention( this );
	m_locomotor = new CBotNPCLocomotion( this );
	m_body = new CBotNPCBody( this );
	m_vision = new CBotNPCVision( this );

	m_conditionFlags = 0;
	m_laserTarget = NULL;
	m_isNuking = false;
	m_ageTimer.Invalidate();
	m_spawner = NULL;
	ClearStunDamage();
}


//-----------------------------------------------------------------------------------------------------
CBotNPC::~CBotNPC()
{
	if ( m_intention )
		delete m_intention;

	if ( m_locomotor )
		delete m_locomotor;

	if ( m_body )
		delete m_body;

	if ( m_vision )
		delete m_vision;
}


//-----------------------------------------------------------------------------------------------------
void CBotNPC::Precache()
{
	BaseClass::Precache();

#ifdef USE_BOSS_SENTRY
	int model = PrecacheModel( "models/bots/boss_sentry/boss_sentry.mdl" );
#else
	int model = PrecacheModel( "models/bots/knight/knight.mdl" );
#endif

	PrecacheGibsForModel( model );

	PrecacheModel( "models/weapons/c_models/c_bigsword/c_bigsword.mdl" );
	PrecacheModel( "models/weapons/c_models/c_bigshield/c_bigshield.mdl" );
	PrecacheModel( "models/weapons/c_models/c_big_mean_mother_hubbard/c_big_mean.mdl" );
	
	PrecacheScriptSound( "Weapon_Sword.Swing" );
	PrecacheScriptSound( "Weapon_Sword.HitFlesh" );
	PrecacheScriptSound( "Weapon_Sword.HitWorld" );
	PrecacheScriptSound( "DemoCharge.HitWorld" );
	PrecacheScriptSound( "TFPlayer.Pain" );
	PrecacheScriptSound( "Halloween.HeadlessBossAttack" );
	PrecacheScriptSound( "RobotBoss.StunStart" );
	PrecacheScriptSound( "RobotBoss.Stunned" );
	PrecacheScriptSound( "RobotBoss.StunRecover" );
	PrecacheScriptSound( "RobotBoss.Acquire" );
	PrecacheScriptSound( "RobotBoss.Vocalize" );
	PrecacheScriptSound( "RobotBoss.Footstep" );
	PrecacheScriptSound( "RobotBoss.LaunchGrenades" );
	PrecacheScriptSound( "RobotBoss.LaunchRockets" );
	PrecacheScriptSound( "RobotBoss.Hurt" );
	PrecacheScriptSound( "RobotBoss.Vulnerable" );
	PrecacheScriptSound( "RobotBoss.ChargeUpNukeAttack" );
	PrecacheScriptSound( "RobotBoss.NukeAttack" );
	PrecacheScriptSound( "RobotBoss.Scanning" );
	PrecacheScriptSound( "RobotBoss.ReinforcementsArrived" );
	PrecacheScriptSound( "Cart.Explode" );

	PrecacheParticleSystem( "asplode_hoodoo_embers" );
	PrecacheParticleSystem( "charge_up" );

	PrecacheArmorParts();
}


//-----------------------------------------------------------------------------------------------------
void CBotNPC::PrecacheArmorParts( void )
{
	CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::TEXT_BUFFER );

	// filename is local to game dir for Steam, so we need to prepend game dir
	char gamePath[256];
	engine->GetGameDir( gamePath, 256 );

	char filename[256];
	Q_snprintf( filename, sizeof( filename ), "%s\\models\\bots\\knight\\armor_parts.txt", gamePath );

	if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) )
	{
		Warning( "Unable to read %s\n", filename );
	}
	else
	{
		while( true )
		{
			char partName[256];

			if ( fileBuffer.Scanf( "%s", partName ) <= 0 )
			{
				break;
			}

			// Make sure we have a valid string before trying to precache it.
			if ( Q_strlen( partName ) > 0 )
			{
				PrecacheModel( partName );
			}
		}
	}
}


//-----------------------------------------------------------------------------------------------------
void CBotNPC::InstallArmorParts( void )
{
	if ( IsMiniBoss() )
		return;

	CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::TEXT_BUFFER );

	// filename is local to game dir for Steam, so we need to prepend game dir
	char gamePath[256];
	engine->GetGameDir( gamePath, 256 );

	char filename[256];
	Q_snprintf( filename, sizeof( filename ), "%s\\models\\bots\\knight\\armor_parts.txt", gamePath );

	if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) )
	{
		Warning( "Unable to read %s\n", filename );
	}
	else
	{
		while( true )
		{
			char partName[256];

			if ( fileBuffer.Scanf( "%s", partName ) <= 0 )
			{
				break;
			}

			CBaseAnimating *part = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" );
			if ( part )
			{
				part->SetModel( partName );

				// bonemerge into our model
				part->FollowEntity( this, true );

				m_armorPartVector.AddToTail( part );
			}		
		}
	}
}


//-----------------------------------------------------------------------------------------------------
void CBotNPC::Spawn( void )
{
	BaseClass::Spawn();

#ifdef USE_BOSS_SENTRY
	SetModel( "models/bots/boss_sentry/boss_sentry.mdl" );
#else
	SetModel( "models/bots/knight/knight.mdl" );
#endif

	InstallArmorParts();

	ModifyMaxHealth( tf_bot_npc_health.GetInt() );

	// show Boss' health meter on HUD
	if ( g_pMonsterResource )
	{
		g_pMonsterResource->SetBossHealthPercentage( 1.0f );
	}

	m_damagePoseParameter = -1;
	m_conditionFlags = 0;

	// randomize initial check
	m_nearestVisibleEnemy = NULL;
	m_nearestVisibleEnemyTimer.Start( RandomFloat( 0.0f, tf_bot_npc_reaction_time.GetFloat() ) );

	m_homePos = GetAbsOrigin();

	m_currentDamagePerSecond = 0.0f;
	m_lastDamagePerSecond = 0.0f;

	m_attackTarget = NULL;
	m_attackTargetTimer.Invalidate();
	m_isAttackTargetLocked = false;

	m_nukeTimer.Start( tf_bot_npc_nuke_interval.GetFloat() );
	m_isNuking = false;

	m_grenadeTimer.Start( GetGrenadeInterval() );
	m_ageTimer.Start();

	ChangeTeam( TF_TEAM_RED );

	TFGameRules()->SetActiveBoss( this );
}


//-----------------------------------------------------------------------------------------------------
ConVar tf_bot_npc_dmg_mult_sniper( "tf_bot_npc_dmg_mult_sniper", "1.5"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_dmg_mult_minigun( "tf_bot_npc_dmg_mult_minigun", "0.5"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_dmg_mult_flamethrower( "tf_bot_npc_dmg_mult_flamethrower", "1"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_dmg_mult_sentrygun( "tf_bot_npc_dmg_mult_sentrygun", "0.5"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_dmg_mult_grenade( "tf_bot_npc_dmg_mult_grenade", "2"/*, FCVAR_CHEAT*/ );


float ModifyBossDamage( const CTakeDamageInfo &info )
{
	CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() );

	if ( pWeapon )
	{
		switch( pWeapon->GetWeaponID() )
		{
		case TF_WEAPON_SNIPERRIFLE:
		case TF_WEAPON_SNIPERRIFLE_DECAP:
		case TF_WEAPON_SNIPERRIFLE_CLASSIC:
		case TF_WEAPON_COMPOUND_BOW:
			return info.GetDamage() * tf_bot_npc_dmg_mult_sniper.GetFloat();

		case TF_WEAPON_MINIGUN:
			return info.GetDamage() * tf_bot_npc_dmg_mult_minigun.GetFloat();

		case TF_WEAPON_FLAMETHROWER:
			return info.GetDamage() * tf_bot_npc_dmg_mult_flamethrower.GetFloat();

		case TF_WEAPON_SENTRY_BULLET:
			return info.GetDamage() * tf_bot_npc_dmg_mult_sentrygun.GetFloat();

		case TF_WEAPON_GRENADE_DEMOMAN:
			return info.GetDamage() * tf_bot_npc_dmg_mult_grenade.GetFloat();
		}
	}

	// unmodified
	return info.GetDamage();
}


//-----------------------------------------------------------------------------------------------------
int CBotNPC::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo )
{
	CTakeDamageInfo info = rawInfo;

	// don't take damage from myself
	if ( info.GetAttacker() == this )
	{
		return 0;
	}

	if ( IsInCondition( INVULNERABLE ) )
	{
		return 0;
	}

	if ( IsInCondition( SHIELDED ) )
	{
		// no damage from the front
		CBaseEntity *inflictor = info.GetInflictor();
		if ( inflictor )
		{
			Vector myForward;
			GetVectors( &myForward, NULL, NULL );

			Vector themForward;
			inflictor->GetVectors( &themForward, NULL, NULL );

			if ( DotProduct( themForward, myForward ) < -0.7071f )
			{
				// blocked by my shield
				EmitSound( "FX_RicochetSound.Ricochet" );
				DispatchParticleEffect( "asplode_hoodoo_embers", info.GetDamagePosition(), GetAbsAngles() );

				return 0;
			}
		}
	}

	
	// weapon-specific damage modification
	info.SetDamage( ModifyBossDamage( info ) );


	if ( IsInCondition( VULNERABLE_TO_STUN ) )
	{
		// Heavies can't deal stun damage (too high DPS)
		//CTFPlayer *playerAttacker = ToTFPlayer( info.GetAttacker() );

		if ( true ) // !playerAttacker ) || !playerAttacker->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) )
		{
			// track head damage when vulnerable
			Vector headPos;
			QAngle headAngles;
			if ( GetAttachment( "head", headPos, headAngles ) )
			{
				Vector damagePos = info.GetDamagePosition();

/*
				const trace_t &pTrace = CBaseEntity::GetTouchTrace();
				damagePos = pTrace.endpos;
*/

/*
				CBaseEntity *inflictor = info.GetInflictor();
				if ( inflictor )
				{
					damagePos = inflictor->GetAbsOrigin() + 3.0f * gpGlobals->frametime * inflictor->GetAbsVelocity();
				}
*/

				if ( tf_bot_npc_debug_damage.GetBool() )
				{
					NDebugOverlay::Cross3D( headPos, 5.0f, 255, 0, 0, true, 5.0f );
					NDebugOverlay::Cross3D( damagePos, 5.0f, 0, 255, 0, true, 5.0f );
					NDebugOverlay::Line( damagePos, headPos, 255, 255, 0, true, 5.0f );
				}

				bool isHeadHit = ( damagePos - headPos ).IsLengthLessThan( tf_bot_npc_head_radius.GetFloat() );

				if ( isHeadHit )
				{
					// hit the head
					AccumulateStunDamage( info.GetDamage() );
					DispatchParticleEffect( "asplode_hoodoo_embers", info.GetDamagePosition(), GetAbsAngles() );

					if ( tf_bot_npc_debug_damage.GetBool() )
					{
						DevMsg( "Stun dmg = %f\n", GetStunDamage() );
						NDebugOverlay::Circle( headPos, tf_bot_npc_head_radius.GetFloat(), 255, 0, 0, 255, true, 5.0f );
					}
				}
				else if ( tf_bot_npc_debug_damage.GetBool() )
				{
					NDebugOverlay::Circle( headPos, tf_bot_npc_head_radius.GetFloat(), 255, 255, 0, 255, true, 5.0f );
				}
			}
		}
	}

	// take extra damage when stunned
	if ( IsInCondition( STUNNED ) )
	{
		info.SetDamage( info.GetDamage() * tf_bot_npc_stunned_injury_multiplier.GetFloat() );

		if ( m_ouchTimer.IsElapsed() )
		{
			m_ouchTimer.Start( 1.0f );
			EmitSound( "RobotBoss.Hurt" );
		}
	}
	else if ( info.GetDamageType() & DMG_CRITICAL )
	{
		// do the critical damage increase
		info.SetDamage( info.GetDamage() * TF_DAMAGE_CRIT_MULTIPLIER );
	}


	// keep a list of everyone who hurt me, and when
	if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() && !InSameTeam( info.GetAttacker() ) )
	{
		CBaseCombatCharacter *attacker = info.GetAttacker()->MyCombatCharacterPointer();

		// sentry guns are first class attackers
		if ( info.GetInflictor() )
		{
			CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
			if ( sentry )
			{
				attacker = sentry;
			}
		}

		RememberAttacker( attacker, info.GetDamage(), ( info.GetDamageType() & DMG_CRITICAL ) ? true : false );

		CTFPlayer *playerAttacker = ToTFPlayer( attacker );
		if ( playerAttacker )
		{
			for( int i=0; i<playerAttacker->m_Shared.GetNumHealers(); ++i )
			{
				CTFPlayer *medic = ToTFPlayer( playerAttacker->m_Shared.GetHealerByIndex( i ) );
				if ( medic )
				{
					// medics healing my attacker are also considered attackers
					RememberAttacker( medic, 0, 0 );
				}
			}
		}

		// if we don't have an attack target yet, we do now
		if ( !HasAttackTarget() )
		{
			SetAttackTarget( attacker );
		}
	}

	EmitSound( "TFPlayer.Pain" );

	// fire event for client combat text, beep, etc.
	IGameEvent *event = gameeventmanager->CreateEvent( "npc_hurt" );
	if ( event )
	{
		event->SetInt( "entindex", entindex() );
		event->SetInt( "health", MAX( 0, GetHealth() ) );
		event->SetInt( "damageamount", info.GetDamage() );
		event->SetBool( "crit", ( info.GetDamageType() & DMG_CRITICAL ) ? true : false );

		CTFPlayer *attackerPlayer = ToTFPlayer( info.GetAttacker() );
		if ( attackerPlayer )
		{
			event->SetInt( "attacker_player", attackerPlayer->GetUserID() );

			if ( attackerPlayer->GetActiveTFWeapon() )
			{
				event->SetInt( "weaponid", attackerPlayer->GetActiveTFWeapon()->GetWeaponID() );
			}
			else
			{
				event->SetInt( "weaponid", 0 );
			}
		}
		else
		{
			// hurt by world
			event->SetInt( "attacker_player", 0 );
			event->SetInt( "weaponid", 0 );
		}

		gameeventmanager->FireEvent( event );
	}

	int result = BaseClass::OnTakeDamage_Alive( info );

	if ( g_pMonsterResource )
	{
		g_pMonsterResource->SetBossHealthPercentage( (float)GetHealth() / (float)GetMaxHealth() );
	}

	return result;
}


//---------------------------------------------------------------------------------------------
// Returns true if we're in a condition that means we can't start another action
bool CBotNPC::IsBusy( void ) const
{
	return IsInCondition( (Condition)( CHARGING | STUNNED | VULNERABLE_TO_STUN | BUSY ) );
}


//---------------------------------------------------------------------------------------------
void CBotNPC::RememberAttacker( CBaseCombatCharacter *attacker, float damage, bool wasCritical )
{
	AttackerInfo attackerInfo;

	attackerInfo.m_attacker = attacker;
	attackerInfo.m_timestamp = gpGlobals->curtime;
	attackerInfo.m_damage = damage;
	attackerInfo.m_wasCritical = wasCritical;

	m_attackerVector.AddToHead( attackerInfo );
}


//----------------------------------------------------------------------------------
CTFPlayer *CBotNPC::GetClosestMinionPrisoner( void )
{
	CUtlVector< CBotNPCMinion * > minionVector;
	CBotNPCMinion *minion = NULL;
	while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL )
	{
		minionVector.AddToTail( minion );
	}

	CTFPlayer *closeCapture = NULL;
	float captureRangeSq = FLT_MAX;

	for( int m=0; m<minionVector.Count(); ++m )
	{
		minion = minionVector[m];

		if ( minion->HasTarget() )
		{
			CTFPlayer *victim = minion->GetTarget();
			if ( victim->m_Shared.InCond( TF_COND_STUNNED ) )
			{
				// they've got one!
				float rangeSq = GetRangeSquaredTo( victim );
				if ( rangeSq < captureRangeSq )
				{
					closeCapture = victim;
					captureRangeSq = rangeSq;
				}
			}
		}
	}

	return closeCapture;
}


//----------------------------------------------------------------------------------
bool CBotNPC::IsPrisonerOfMinion( CBaseCombatCharacter *victim )
{
	if ( !victim->IsPlayer() )
	{
		return false;
	}

	CUtlVector< CBotNPCMinion * > minionVector;
	CBotNPCMinion *minion = NULL;
	while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL )
	{
		minionVector.AddToTail( minion );
	}

	for( int m=0; m<minionVector.Count(); ++m )
	{
		minion = minionVector[m];

		if ( minion->HasTarget() && minion->GetTarget() == victim )
		{
			if ( minion->GetTarget()->m_Shared.InCond( TF_COND_STUNNED ) )
			{
				return true;
			}
		}
	}

	return false;
}


//----------------------------------------------------------------------------------
void CBotNPC::UpdateDamagePerSecond( void )
{
	m_lastDamagePerSecond = m_currentDamagePerSecond;

	m_currentDamagePerSecond = 0.0f;

	const float windowDuration = 10.0f; // 5.0f;
	int i;

	m_threatVector.RemoveAll();

	for( i=0; i<m_attackerVector.Count(); ++i )
	{
		float age = gpGlobals->curtime - m_attackerVector[i].m_timestamp;

		if ( age > windowDuration )
		{
			// too old
			break;
		}

		float decayedDamage = ( ( windowDuration - age ) / windowDuration ) * m_attackerVector[i].m_damage;

		m_currentDamagePerSecond += decayedDamage;

		CBaseCombatCharacter *attacker = m_attackerVector[i].m_attacker;

		if ( attacker && attacker->IsAlive() )
		{
			int j;
			for( j=0; j<m_threatVector.Count(); ++j )
			{
				if ( m_threatVector[j].m_who == attacker )
				{
					m_threatVector[j].m_threat += decayedDamage;
					break;
				}
			}

			if ( j >= m_threatVector.Count() )
			{
				// new threat
				ThreatInfo threat;
				threat.m_who = attacker;
				threat.m_threat = decayedDamage;
				m_threatVector.AddToTail( threat );
			}
		}
	}

// 	if ( m_currentDamagePerSecond > 0.0001f )
// 	{
// 		DevMsg( "%3.2f: dps = %3.2f\n", gpGlobals->curtime, m_currentDamagePerSecond );
// 	}
}


//----------------------------------------------------------------------------------
const CBotNPC::ThreatInfo *CBotNPC::GetMaxThreat( void ) const
{
	int maxThreatIndex = -1;

	for( int i=0; i<m_threatVector.Count(); ++i )
	{
		if ( maxThreatIndex < 0 || m_threatVector[i].m_threat > m_threatVector[ maxThreatIndex ].m_threat )
		{
			maxThreatIndex = i;
		}
	}

	if ( maxThreatIndex < 0 )
	{
		// no threat yet
		return NULL;
	}

	return &m_threatVector[ maxThreatIndex ];
}


//----------------------------------------------------------------------------------
const CBotNPC::ThreatInfo *CBotNPC::GetThreat( CBaseCombatCharacter *who ) const
{
	for( int i=0; i<m_threatVector.Count(); ++i )
	{
		if ( m_threatVector[i].m_who == who )
		{
			return &m_threatVector[i];
		}
	}

	return NULL;
}


//----------------------------------------------------------------------------------
void CBotNPC::UpdateAttackTarget( void )
{
	if ( m_isAttackTargetLocked && HasAttackTarget() )
	{
		return;
	}

	// who is most dangerous to me at the moment
	const ThreatInfo *maxThreat = GetMaxThreat();

	if ( !maxThreat )
	{
		// nobody is hurting me at the moment

		if ( HasAttackTarget() )
		{
			// stay focused on current target
			return;
		}

		// we have no current target, either

		// if my minions have captured someone, go get them
		CTFPlayer *closeCapture = GetClosestMinionPrisoner();
		if ( closeCapture )
		{
			SetAttackTarget( closeCapture );
			return;
		}

		// if we see an enemy, attack them
		CBaseCombatCharacter *visible = GetNearestVisibleEnemy();
		if ( visible )
		{
			SetAttackTarget( visible );
		}

		return;
	}

	// we are under attack, if we don't have a target, attack the highest threat
	if ( !HasAttackTarget() )
	{
		SetAttackTarget( maxThreat->m_who );
		return;
	}

	if ( IsAttackTarget( maxThreat->m_who ) )
	{
		// our current target is still dealing the most damage to us
		return;
	}

	// switch to new threat if is is more dangerous
	const ThreatInfo *attackTargetThreat = GetThreat( GetAttackTarget() );

	if ( !attackTargetThreat || maxThreat->m_threat > attackTargetThreat->m_threat + tf_bot_npc_threat_tolerance.GetFloat() )
	{
		// change threats
		SetAttackTarget( maxThreat->m_who );
	}
}


//----------------------------------------------------------------------------------
void CBotNPC::RemoveCondition( Condition c )
{
	if ( c == STUNNED )
	{
		// reset the accumulator
		ClearStunDamage();
	}

	m_conditionFlags &= ~c;
}


//----------------------------------------------------------------------------------
void CBotNPC::SwingAxe( void )
{
	if ( !IsSwingingAxe() )
	{
		AddGesture( ACT_MP_ATTACK_STAND_ITEM1 );
		m_axeSwingTimer.Start( 0.58f );
		EmitSound( "Weapon_Sword.Swing" );
	}
}


//----------------------------------------------------------------------------------
void CBotNPC::UpdateAxeSwing( void )
{
	if ( !m_axeSwingTimer.HasStarted() )
	{
		return;
	}

	// continue axe swing
	if ( !m_axeSwingTimer.IsElapsed() )
	{
		return;
	}

	// moment of impact - did axe swing hit?
	m_axeSwingTimer.Invalidate();

	CBaseCombatCharacter *victim = GetAttackTarget();

	if ( victim )
	{
		Vector forward;
		GetVectors( &forward, NULL, NULL );

		Vector toVictim = victim->WorldSpaceCenter() - WorldSpaceCenter();
		toVictim.NormalizeInPlace();

		if ( DotProduct( forward, toVictim ) > 0.7071f )
		{
			if ( IsRangeLessThan( victim, 0.9f * tf_bot_npc_attack_range.GetFloat() ) )
			{
				if ( IsLineOfSightClear( victim ) )
				{
					// CHOP!
					CTakeDamageInfo info( this, this, tf_bot_npc_melee_damage.GetFloat(), DMG_SLASH, TF_DMG_CUSTOM_NONE );
					CalculateMeleeDamageForce( &info, toVictim, WorldSpaceCenter(), 1.0f );
					victim->TakeDamage( info );
					EmitSound( "Weapon_Sword.HitFlesh" );
					return;
				}
			}
		}
	}

	EmitSound( "Weapon_Sword.HitWorld" );
}


//----------------------------------------------------------------------------------
bool CBotNPC::IsSwingingAxe( void ) const
{
	return const_cast< CBotNPC * >( this )->IsPlayingGesture( ACT_MP_ATTACK_STAND_ITEM1 );
}


//---------------------------------------------------------------------------------------------
void CBotNPC::Update( void )
{
	BaseClass::Update();

	UpdateNearestVisibleEnemy();
	UpdateAxeSwing();
	UpdateDamagePerSecond();
	UpdateAttackTarget();

	if ( m_damagePoseParameter < 0 )
	{
		m_damagePoseParameter = LookupPoseParameter( "damage" );
	}

	if ( m_damagePoseParameter >= 0 )
	{
		SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) );
	}

	// chase down players who taunt me
	if ( m_hateTauntTimer.IsElapsed() )
	{
		CUtlVector< CTFPlayer * > playerVector;
		CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );

		for( int i=0; i<playerVector.Count(); ++i )
		{
			if ( playerVector[i]->IsTaunting() )
			{
				m_hateTauntTimer.Start( tf_bot_npc_hate_taunt_cooldown.GetFloat() );

				if ( IsLineOfSightClear( playerVector[i], IGNORE_ACTORS ) )
				{
					// the taunter becomes our new attack target
					SetAttackTarget( playerVector[i], tf_bot_npc_hate_taunt_cooldown.GetFloat() );
				}	
			}
		}
	}	
}


//---------------------------------------------------------------------------------------------
bool CBotNPC::IsPotentiallyChaseable( CTFPlayer *victim )
{
	if ( !victim )
	{
		return false;
	}

	if ( !victim->IsAlive() )
	{
		// victim is dead - pick a new one
		return false;
	}

	CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea();
	if ( !victimArea || victimArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) )
	{
		// unreachable - pick a new victim
		return false;
	}

	if ( victim->GetGroundEntity() != NULL )
	{
		Vector victimAreaPos;
		victimArea->GetClosestPointOnArea( victim->GetAbsOrigin(), &victimAreaPos );
		if ( ( victim->GetAbsOrigin() - victimAreaPos ).AsVector2D().IsLengthGreaterThan( 50.0f ) )
		{
			// off the mesh and unreachable - pick a new victim
			return false;
		}
	}

	if ( victim->m_Shared.IsInvulnerable() )
	{
		// invulnerable - pick a new victim
		return false;
	}

	Vector toHome = m_homePos - victim->GetAbsOrigin();
	if ( toHome.IsLengthGreaterThan( tf_bot_npc_quit_range.GetFloat() ) )
	{
		// too far from home - pick a new victim
		return false;
	}

	return true;
}


//---------------------------------------------------------------------------------------------
bool CBotNPC::IsIgnored( CTFPlayer *player ) const
{
	if ( player->m_Shared.IsStealthed() )
	{
		if ( player->m_Shared.GetPercentInvisible() < 0.75f )
		{
			// spy is partially cloaked, and therefore attracts our attention
			return false;
		}

		if ( player->m_Shared.InCond( TF_COND_BURNING ) ||
			 player->m_Shared.InCond( TF_COND_URINE ) ||
			 player->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ||
			 player->m_Shared.InCond( TF_COND_BLEEDING ) )
		{
			// always notice players with these conditions
			return false;
		}

		// invisible!
		return true;
	}

	return false;
}


//---------------------------------------------------------------------------------------------
void CBotNPC::UpdateNearestVisibleEnemy( void )
{
	if ( !m_nearestVisibleEnemyTimer.IsElapsed() )
	{
		return;
	}

	m_nearestVisibleEnemyTimer.Start( tf_bot_npc_reaction_time.GetFloat() );

	// collect everyone
	CUtlVector< CTFPlayer * > playerVector;
	//CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
	CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );

	Vector myForward;
	GetVectors( &myForward, NULL, NULL );

	m_nearestVisibleEnemy = NULL;
	float victimRangeSq = FLT_MAX;

	for( int i=0; i<playerVector.Count(); ++i )
	{
		CTFPlayer *victim = playerVector[i];

		if ( IsIgnored( victim ) )
		{
			continue;
		}

		float rangeSq = GetRangeSquaredTo( playerVector[i] );
		if ( rangeSq < victimRangeSq )
		{
			// FOV check
			Vector to = playerVector[i]->WorldSpaceCenter() - WorldSpaceCenter();
			to.NormalizeInPlace();

			if ( DotProduct( to, myForward ) > -0.7071f )
			{
				if ( IsLineOfSightClear( playerVector[i] ) )
				{
					m_nearestVisibleEnemy = playerVector[i];
					victimRangeSq = rangeSq;
				}
			}
		}
	}
}


//---------------------------------------------------------------------------------------------
void CBotNPC::SetAttackTarget( CBaseCombatCharacter *target, float duration )
{
	if ( target && m_attackTarget != NULL && m_attackTarget->IsAlive() && m_attackTargetTimer.HasStarted() && !m_attackTargetTimer.IsElapsed() )
	{
		// can't switch away from our still valid target yet
		return;
	}

	if ( m_attackTarget != target )
	{
		if ( target )
		{
			EmitSound( "RobotBoss.Acquire" );
			AddGesture( ACT_MP_GESTURE_FLINCH_CHEST );
		}

		TFGameRules()->SetIT( m_attackTarget );

		m_attackTarget = target;
	}

	if ( duration > 0.0f )
	{
		m_attackTargetTimer.Start( duration );
	}
	else
	{
		m_attackTargetTimer.Invalidate();
	}
}


//---------------------------------------------------------------------------------------------
CBaseCombatCharacter *CBotNPC::GetAttackTarget( void ) const
{
	if ( m_attackTarget != NULL && m_attackTarget->IsAlive() )
	{
		return m_attackTarget;
	}

	return NULL;
}


//---------------------------------------------------------------------------------------------
void CBotNPC::Break( void )
{
	CPVSFilter filter( GetAbsOrigin() );
	UserMessageBegin( filter, "BreakModel" );
		WRITE_SHORT( GetModelIndex() );
		WRITE_VEC3COORD( GetAbsOrigin() );
		WRITE_ANGLES( GetAbsAngles() );
		WRITE_SHORT( GetSkin() );
	MessageEnd();
}


//---------------------------------------------------------------------------------------------
void CBotNPC::CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector )
{
	CUtlVector< CTFPlayer * > allPlayerVector;
	CollectPlayers( &allPlayerVector, TEAM_ANY, COLLECT_ONLY_LIVING_PLAYERS );

	for( int i=0; i<allPlayerVector.Count(); ++i )
	{
		CTFPlayer *player = allPlayerVector[i];

		if ( player->GetGroundEntity() == this )
		{
			playerVector->AddToTail( player );
		}
	}
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCStunned : public Action< CBotNPC >
{
public:
	CBotNPCStunned( float duration, Action< CBotNPC > *nextAction = NULL );

	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info );

	virtual const char *GetName( void ) const	{ return "Stunned"; }		// return name of this action

private:
	CountdownTimer m_timer;
	enum StunStateType
	{
		BECOMING_STUNNED,
		STUNNED,
		RECOVERING
	}
	m_state;
	int m_layerUsed;

	Action< CBotNPC > *m_nextAction;
};


//---------------------------------------------------------------------------------------------
CBotNPCStunned::CBotNPCStunned( float duration, Action< CBotNPC > *nextAction )
{
	m_timer.Start( duration );
	m_nextAction = nextAction;
}


//---------------------------------------------------------------------------------------------
ConVar tf_bot_npc_stun_ammo_count( "tf_bot_npc_stun_ammo_count", "3"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_stun_ammo_amount( "tf_bot_npc_stun_ammo_amount", "100"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_stun_ammo_velocity( "tf_bot_npc_stun_ammo_velocity", "100"/*, FCVAR_CHEAT*/ );

void TossAmmoPack( CBotNPC *me )
{
	int iPrimary = tf_bot_npc_stun_ammo_amount.GetInt();
	int iSecondary = tf_bot_npc_stun_ammo_amount.GetInt();
	int iMetal = tf_bot_npc_stun_ammo_amount.GetInt();

	// Create the ammo pack.
	CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( me->GetAbsOrigin(), me->GetAbsAngles(), NULL, "models/items/ammopack_medium.mdl" );
	if ( pAmmoPack )
	{
/*
		Vector vel;
		
		vel.x = RandomFloat( -1.0f, 1.0f ) * tf_bot_npc_stun_ammo_velocity.GetFloat();
		vel.y = RandomFloat( -1.0f, 1.0f ) * tf_bot_npc_stun_ammo_velocity.GetFloat();
		vel.z = tf_bot_npc_stun_ammo_velocity.GetFloat();

		pAmmoPack->SetInitialVelocity( vel );
*/
		pAmmoPack->m_nSkin = 0;

		// Give the ammo pack some health, so that trains can destroy it.
		pAmmoPack->SetCollisionGroup( COLLISION_GROUP_DEBRIS );
		pAmmoPack->m_takedamage = DAMAGE_YES;		
		pAmmoPack->SetHealth( 900 );

		pAmmoPack->SetBodygroup( 1, 1 );

		pAmmoPack->ApplyLocalAngularVelocityImpulse( AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ) );

		DispatchSpawn( pAmmoPack );

		// Fill up the ammo pack.
		pAmmoPack->GiveAmmo( iPrimary, TF_AMMO_PRIMARY );
		pAmmoPack->GiveAmmo( iSecondary, TF_AMMO_SECONDARY );
		pAmmoPack->GiveAmmo( iMetal, TF_AMMO_METAL );
	}
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCStunned::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	// start animation
	me->GetBodyInterface()->StartActivity( ACT_MP_STAND_MELEE );
	m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_Stun_begin" ), 0 );
	m_state = BECOMING_STUNNED;

	m_timer.Reset();

	me->AddCondition( CBotNPC::STUNNED );
	me->EmitSound( "RobotBoss.StunStart" );

	// throw out some ammo
	for( int i=0; i<tf_bot_npc_stun_ammo_count.GetInt(); ++i )
	{
		TossAmmoPack( me );
	}

	me->m_outputOnStunned.FireOutput( me, me );

	// relay the event to the map logic
	CTFSpawnerBoss *spawner = me->GetSpawner();
	if ( spawner )
	{
		spawner->OnBotStunned( me );
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCStunned::Update( CBotNPC *me, float interval )
{
	switch( m_state )
	{
	case BECOMING_STUNNED:
		if ( me->IsSequenceFinished() )
		{
			me->FastRemoveLayer( m_layerUsed );

			m_state = STUNNED;
			m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_stun_middle" ), 0 );
			me->SetLayerLooping( m_layerUsed, true );
			me->EmitSound( "RobotBoss.Stunned" );
		}
		break;

	case STUNNED:
		if ( m_timer.IsElapsed() )
		{
			me->FastRemoveLayer( m_layerUsed );

			m_state = RECOVERING;
			m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_stun_end" ), 0 );
			me->StopSound( "RobotBoss.Stunned" );
			me->EmitSound( "RobotBoss.StunRecover" );
		}
		break;

	case RECOVERING:
		if ( me->IsSequenceFinished() )
		{
			me->FastRemoveLayer( m_layerUsed );

			if ( m_nextAction )
			{
				return ChangeTo( m_nextAction, "Stun finished" );
			}

			return Done( "Stun finished" );
		}
		break;
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCStunned::OnInjured( CBotNPC *me, const CTakeDamageInfo &info )
{
	return TryToSustain( RESULT_CRITICAL );
}


//---------------------------------------------------------------------------------------------
void CBotNPCStunned::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::STUNNED );

	if ( me->HasAbility( CBotNPC::CAN_ENRAGE ) )
	{
		// being stunned makes the boss ANGRY!
		me->AddCondition( CBotNPC::ENRAGED );
	}

	// make sure the boss attacks at least once before he starts a nuke
	if ( me->GetNukeTimer()->GetRemainingTime() < tf_bot_npc_min_nuke_after_stun_time.GetFloat() )
	{
		me->GetNukeTimer()->Start( tf_bot_npc_min_nuke_after_stun_time.GetFloat() );
	}
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCBigJump : public Action< CBotNPC >
{
public:
	CBotNPCBigJump( const Vector &destination, Action< CBotNPC > *nextAction = NULL );

	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info );

	virtual const char *GetName( void ) const	{ return "Jump"; }		// return name of this action

private:
	enum StunStateType
	{
		JUMPING_UP,
		FLOATING_UP,
		FALLING_DOWN
	}
	m_state;

	CountdownTimer m_timer;
	Vector m_destination;

	Action< CBotNPC > *m_nextAction;
};


//---------------------------------------------------------------------------------------------
CBotNPCBigJump::CBotNPCBigJump( const Vector &destination, Action< CBotNPC > *nextAction )
{
	m_destination = destination;
	m_nextAction = nextAction;
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCBigJump::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	// start animation
	me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_START_MELEE );
	m_state = JUMPING_UP;
	m_timer.Start( 3.0f );

	// disconnect us from the ground
	me->GetLocomotionInterface()->Jump();

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCBigJump::Update( CBotNPC *me, float interval )
{
	// animation state
	switch( m_state )
	{
	case JUMPING_UP:
		if ( me->IsSequenceFinished() )
		{
			me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_FLOAT_MELEE );
			m_state = FLOATING_UP;
		}
		break;
	}

	// movement
	switch( m_state )
	{
	case JUMPING_UP:
	case FLOATING_UP:
		me->GetLocomotionInterface()->SetVelocity( Vector( 0, 0, 1200.0f ) );

		if ( m_timer.IsElapsed() )
		{
			m_state = FALLING_DOWN;

			// move so we fall on our destination point
			me->SetAbsOrigin( m_destination + Vector( 0, 0, 1300.0f ) );
			me->GetLocomotionInterface()->SetVelocity( vec3_origin );
		}
		break;

	case FALLING_DOWN:
		if ( me->GetLocomotionInterface()->IsOnGround() )
		{
			me->AddGesture( ACT_MP_JUMP_LAND_MELEE );

			if ( m_nextAction )
			{
				return ChangeTo( m_nextAction, "Finished jump" );
			}

			return Done( "Finished jump" );
		}
		break;
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCBigJump::OnInjured( CBotNPC *me, const CTakeDamageInfo &info )
{
	return TryToSustain( RESULT_CRITICAL );
}


//---------------------------------------------------------------------------------------------
void CBotNPCBigJump::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCLaunchMinions : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );

	// if anything interrupts this action, abort it
	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )	{ return Done(); }

	virtual const char *GetName( void ) const	{ return "LaunchMinions"; }		// return name of this action

private:
	CountdownTimer m_timer;
	int m_minionsLeft;

	bool SpawnMinion( CBotNPC *me );
};


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaunchMinions::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	// start animation
	me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY );

	me->AddGestureSequence( me->LookupSequence( "taunt01" ) );

	m_timer.Start( 4.0f );

	int bonus = (int)( me->GetAge() / tf_bot_npc_minion_launch_count_increase_interval.GetFloat() );
	m_minionsLeft = tf_bot_npc_minion_launch_count_initial.GetInt() + bonus;

	return Continue();
}


//---------------------------------------------------------------------------------------------
bool CBotNPCLaunchMinions::SpawnMinion( CBotNPC *me )
{
	Vector spawnSpot = me->WorldSpaceCenter();

	Vector headPos;
	QAngle headAngles;
	if ( me->GetAttachment( "head", headPos, headAngles ) )
	{
		spawnSpot = headPos + RandomVector( -10.0f, 10.0f );
	}

	CBaseCombatCharacter *minion = static_cast< CBaseCombatCharacter * >( CreateEntityByName( "bot_npc_minion" ) );
	if ( minion )
	{
		minion->SetAbsAngles( me->GetAbsAngles() );
		minion->SetAbsOrigin( spawnSpot );
		minion->SetOwnerEntity( me );

		DispatchSpawn( minion );

		return true;
	}

	return false;
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaunchMinions::Update( CBotNPC *me, float interval )
{
	CBaseCombatCharacter *target = me->GetAttackTarget();

	if ( target )
	{
		me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );
	}

	if ( m_timer.IsElapsed() )
	{
		while( m_minionsLeft-- )
		{
			SpawnMinion( me );
		}

		return Done();
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCNukeAttack : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info );

	virtual const char *GetName( void ) const	{ return "NukeAttack"; }		// return name of this action

private:
	CountdownTimer m_shakeTimer;
	CountdownTimer m_chargeUpTimer;
};

ConVar tf_bot_npc_nuke_damage( "tf_bot_npc_nuke_damage", "75"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_nuke_max_remaining_health( "tf_bot_npc_nuke_max_remaining_health", "60"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_nuke_afterburn_time( "tf_bot_npc_nuke_afterburn_time", "5"/*, FCVAR_CHEAT*/ );

//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCNukeAttack::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_FLOAT_LOSERSTATE );
	me->StartNukeEffect();

	me->EmitSound( "RobotBoss.ChargeUpNukeAttack" );
	me->AddCondition( CBotNPC::VULNERABLE_TO_STUN );

	m_chargeUpTimer.Start( tf_bot_npc_nuke_charge_time.GetFloat() );
	m_shakeTimer.Start( 0.25f );


	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCNukeAttack::Update( CBotNPC *me, float interval )
{
	float stunRatio = me->GetStunDamage() / me->GetBecomeStunnedDamage();

	if ( me->HasAbility( CBotNPC::CAN_BE_STUNNED ) && stunRatio >= 1.0f )
	{
		return ChangeTo( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), "They got me" );
	}

	// update the client's HUD
	if ( g_pMonsterResource )
	{
		g_pMonsterResource->SetBossStunPercentage( 1.0f - stunRatio );
	}

	if ( m_shakeTimer.IsElapsed() )
	{
		m_shakeTimer.Reset();
		UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 3000.0f, SHAKE_START );
	}

	if ( m_chargeUpTimer.IsElapsed() )
	{
		// BLAST!
		CUtlVector< CTFPlayer * > playerVector;
		CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
		CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );

		me->EmitSound( "RobotBoss.NukeAttack" );

		CUtlVector< CBaseCombatCharacter * > victimVector;

		int i;

		// players
		for ( i=0; i<playerVector.Count(); ++i )
		{
			CBasePlayer *player = playerVector[i];

			if ( player && player->IsAlive() && player->GetTeamNumber() == TF_TEAM_BLUE )
			{
				victimVector.AddToTail( player );
			}
		}

		// objects
		CTFTeam *team = GetGlobalTFTeam( TF_TEAM_BLUE );
		if ( team )
		{
			for ( i=0; i<team->GetNumObjects(); ++i )
			{
				CBaseObject *object = team->GetObject( i );
				if ( object )
				{
					victimVector.AddToTail( object );
				}
			}
		}

#ifdef SKIPME
		team = GetGlobalTFTeam( TF_TEAM_RED );
		if ( team )
		{
			for ( i=0; i<team->GetNumObjects(); ++i )
			{
				CBaseObject *object = team->GetObject( i );
				if ( object )
				{
					victimVector.AddToTail( object );
				}
			}
		}

		// non-player bots
		CUtlVector< INextBot * > botVector;
		TheNextBots().CollectAllBots( &botVector );
		for( i=0; i<botVector.Count(); ++i )
		{
			CBaseCombatCharacter *bot = botVector[i]->GetEntity();

			if ( !bot->IsPlayer() && bot->IsAlive() )
			{
				victimVector.AddToTail( bot );
			}
		}
#endif // SKIPME

		for( int i=0; i<victimVector.Count(); ++i )
		{
			CBaseCombatCharacter *victim = victimVector[i];

			if ( me->IsSelf( victim ) )
				continue;

			if ( me->IsLineOfSightClear( victim ) )
			{
				Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter();
				toVictim.NormalizeInPlace();

				float damage = tf_bot_npc_nuke_damage.GetFloat();

				if ( me->GetAge() > tf_bot_npc_nuke_lethal_time.GetFloat() )
				{
					// nuke is now lethal
					damage = 999.9f;
				}
				else if ( tf_bot_npc_nuke_max_remaining_health.GetFloat() >= 0.0f )
				{
					// nuke slams everyone's health to this
					if ( victim->GetHealth() > tf_bot_npc_nuke_max_remaining_health.GetFloat() )
					{
						damage = victim->GetHealth() - tf_bot_npc_nuke_max_remaining_health.GetFloat();
					}
				}

				CTakeDamageInfo info( me, me, damage, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE );
				CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
				victim->TakeDamage( info );

				if ( victim->IsPlayer() )
				{
					CTFPlayer *playerVictim = ToTFPlayer( victim );

					// catch them on fire (unless they are a Pyro)
					if ( !playerVictim->IsPlayerClass( TF_CLASS_PYRO ) )
					{
						playerVictim->m_Shared.Burn( me, tf_bot_npc_nuke_afterburn_time.GetFloat() );
					}

					color32 colorHit = { 255, 255, 255, 255 };
					UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN );
				}
			}
		}

		return Done();
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCNukeAttack::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::VULNERABLE_TO_STUN );
	me->StopNukeEffect();
	me->ClearStunDamage();
	me->GetNukeTimer()->Start( tf_bot_npc_nuke_interval.GetFloat() );

	if ( g_pMonsterResource )
	{
		g_pMonsterResource->HideBossStunMeter();
	}
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCNukeAttack::OnInjured( CBotNPC *me, const CTakeDamageInfo &info )
{
	return TryToSustain( RESULT_CRITICAL );
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCLaunchRockets : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	// if anything interrupts this action, abort it
	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )	{ return Done(); }

	virtual const char *GetName( void ) const	{ return "LaunchRockets"; }		// return name of this action

private:
	CountdownTimer m_timer;

	CountdownTimer m_launchTimer;
	int m_rocketsLeft;

	int m_animLayer;

	CHandle< CBaseCombatCharacter > m_target;
	Vector m_lastTargetPosition;
};


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaunchRockets::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	// start animation
	me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY );

	m_animLayer = me->AddLayeredSequence( me->LookupSequence( "taunt02" ), 0 );

	m_timer.Start( 1.0f );

	m_rocketsLeft = me->GetRocketLaunchCount();

	me->AddCondition( CBotNPC::BUSY );
	me->LockAttackTarget();

	me->EmitSound( "RobotBoss.LaunchRockets" );

	if ( me->GetAttackTarget() == NULL )
	{
		return Done( "No target" );
	}

	m_target = me->GetAttackTarget();
	m_lastTargetPosition = m_target->WorldSpaceCenter();

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaunchRockets::Update( CBotNPC *me, float interval )
{
	if ( m_target != NULL )
	{
		m_lastTargetPosition = m_target->WorldSpaceCenter();
	}

	me->GetLocomotionInterface()->FaceTowards( m_lastTargetPosition );

	if ( m_timer.IsElapsed() && m_launchTimer.IsElapsed() )
	{
		if ( !m_rocketsLeft )
		{
			return Done();
		}

		--m_rocketsLeft;
		m_launchTimer.Start( me->GetRocketInterval() );

		QAngle launchAngles = me->GetAbsAngles();

		if ( m_target == NULL )
		{
			Vector to = m_lastTargetPosition - me->WorldSpaceCenter();
			VectorAngles( to, launchAngles );
		}
		else
		{
			float range = me->GetRangeTo( m_target->EyePosition() );

			const float rocketSpeed = me->GetRocketAimError() * 1100.0f; // 2000.0f; // 1100.0f;  nerfing accuracy
			float flightTime = range / rocketSpeed;

			Vector aimSpot = m_target->EyePosition() + m_target->GetAbsVelocity() * flightTime;

			Vector to = aimSpot - me->WorldSpaceCenter();
			VectorAngles( to, launchAngles );
		}

		CTFProjectile_Rocket *pRocket = CTFProjectile_Rocket::Create( me, me->WorldSpaceCenter(), launchAngles, me, me );
		if ( pRocket )
		{
			if ( me->IsInCondition( CBotNPC::ENRAGED ) )
			{
				pRocket->SetCritical( true );
				pRocket->EmitSound( "Weapon_RPG.SingleCrit" );
			}
			else
			{
				me->EmitSound( me->GetRocketSoundEffect() );
			}

			pRocket->SetDamage( me->GetRocketDamage() );
		}
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCLaunchRockets::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::ENRAGED );
	me->RemoveCondition( CBotNPC::BUSY );
	me->FastRemoveLayer( m_animLayer );
	me->UnlockAttackTarget();
}



//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCRush : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	// if anything interrupts this action, abort it
	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )	{ return Done(); }

	virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL );

	virtual const char *GetName( void ) const	{ return "Rush"; }		// return name of this action

private:
	CountdownTimer m_timer;
	Vector m_chargeOrigin;
	float m_maxAttainedSpeed;
	float m_lastSpeed;
	bool m_didHitVictim;
};


//---------------------------------------------------------------------------------------------
void PushawayPlayer( CTFPlayer *victim, const Vector &pushOrigin, float pushForce )
{
	if ( !victim )
		return;

	if ( victim->GetFlags() & FL_ONGROUND )
	{
		// launching into the air
		victim->SetAbsVelocity( vec3_origin );

		const float stunTime = 0.5f;
		victim->m_Shared.StunPlayer( stunTime, 1.0, TF_STUN_MOVEMENT );

		victim->ApplyPunchImpulseX( RandomInt( 10, 15 ) );
		victim->SpeakConceptIfAllowed( MP_CONCEPT_DEFLECTED, "projectile:0,victim:1" );
	}

	victim->RemoveFlag( FL_ONGROUND );

	Vector toVictim = victim->WorldSpaceCenter() - pushOrigin;
	toVictim.z = 0.0f;
	toVictim.NormalizeInPlace();
	toVictim.z = 1.0f;

	victim->ApplyAbsVelocityImpulse( pushForce * toVictim );
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCRush::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	m_timer.Start( 1.5f );
	m_chargeOrigin = me->GetAbsOrigin();
	m_maxAttainedSpeed = 0.0f;
	m_lastSpeed = 0.0f;
	m_didHitVictim = false;

	me->AddCondition( CBotNPC::CHARGING );
	me->AddCondition( CBotNPC::SHIELDED );

	me->EmitSound( "Halloween.HeadlessBossAttack" );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCRush::Update( CBotNPC *me, float interval )
{
	// pushaway/hit nearby players
	CUtlVector< CTFPlayer * > playerVector;
	CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
	CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );

	Vector chargeVector = me->GetAbsOrigin() - m_chargeOrigin;
	chargeVector.NormalizeInPlace();

	const float chargeRadius = 150.0f;

	for( int i=0; i<playerVector.Count(); ++i )
	{
		CTFPlayer *victim = playerVector[i];

		if ( me->IsRangeGreaterThan( victim, chargeRadius ) )
			continue;

		Vector closestPointOnChargePath;
		CalcClosestPointOnLine( victim->GetAbsOrigin(), m_chargeOrigin, me->GetAbsOrigin(), closestPointOnChargePath );

		Vector fromChargePath = victim->GetAbsOrigin() - closestPointOnChargePath;
		float range = fromChargePath.NormalizeInPlace();

		if ( range >= chargeRadius )
			continue;

		if ( !me->IsLineOfSightClear( victim ) )
			continue;

		float nearness = 1.0f - ( range / chargeRadius );

		// push 'em
		float pushForce = tf_bot_npc_charge_pushaway_force.GetFloat() * nearness;
		PushawayPlayer( victim, closestPointOnChargePath, pushForce );

		// crunch 'em
		CTakeDamageInfo info( me, me, tf_bot_npc_charge_damage.GetFloat() * nearness, DMG_CRUSH, TF_DMG_CUSTOM_NONE );

		CalculateMeleeDamageForce( &info, fromChargePath, closestPointOnChargePath, 1.0f );

		victim->TakeDamage( info );

		color32 color = { 255, 0, 0, 255 };
		UTIL_ScreenFade( victim, color, 0.5f, 0.1f, FFADE_IN );

		if ( nearness > 0.5f )
		{
			m_didHitVictim = true;
		}
	}

	float speed = me->GetLocomotionInterface()->GetVelocity().Length();
	m_maxAttainedSpeed = MAX( m_maxAttainedSpeed, speed );

	if ( m_timer.IsElapsed() )
	{
		return ChangeTo( new CBotNPCLaunchRockets, "Finished charge" );
	}
	else
	{
		// chaaarge!
		me->GetLocomotionInterface()->Run();

		Vector forward;
		me->GetVectors( &forward, NULL, NULL );
		me->GetLocomotionInterface()->Approach( 100.0f * forward + me->GetLocomotionInterface()->GetFeet() );

		if ( !m_didHitVictim && m_maxAttainedSpeed > 350.0f && speed - m_lastSpeed < -200.0f )
		{
			// abrupt slowdown = bonk!
			return ChangeTo( new CBotNPCStunned( 3.0f, new CBotNPCLaunchRockets ), "Smacked into the world" );
		}
	}

	// animation
	if ( !me->GetBodyInterface()->IsActivity( ACT_MP_CROUCHWALK_PRIMARY ) )
	{
		me->GetBodyInterface()->StartActivity( ACT_MP_CROUCHWALK_PRIMARY );
	}

	m_lastSpeed = speed;

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCRush::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::SHIELDED );
	me->RemoveCondition( CBotNPC::CHARGING );
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCRush::OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result )
{
	return TryContinue();
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCBlock : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction );

	virtual const char *GetName( void ) const	{ return "Block"; }		// return name of this action

private:
	CountdownTimer m_timer;
};


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCBlock::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	// start animation
	me->SetSequence( me->LookupSequence( "marketing_pose_001" ) );
	me->SetPlaybackRate( 1.0f );
	me->SetCycle( 0 );
	me->ResetSequenceInfo();

	m_timer.Start( 3.0f );

	me->AddCondition( CBotNPC::SHIELDED );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCBlock::Update( CBotNPC *me, float interval )
{
	if ( m_timer.IsElapsed() )
	{
		return Done();
	}

	if ( me->GetAttackTarget() )
	{
		me->GetLocomotionInterface()->FaceTowards( me->GetAttackTarget()->WorldSpaceCenter() );
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCBlock::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::SHIELDED );
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCBlock::OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )
{ 
	return Done(); 
}



//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCLaunchGrenades : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	// if anything interrupts this action, abort it
	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )	{ return Done(); }

	virtual const char *GetName( void ) const	{ return "LaunchGrenades"; }		// return name of this action

private:
	CountdownTimer m_timer;
	CountdownTimer m_detonateTimer;
	CUtlVector< CHandle< CTFGrenadePipebombProjectile > > m_grenadeVector;
	void LaunchGrenade( CBotNPC *me, const Vector &launchVel, CTFWeaponInfo *weaponInfo );
	void LaunchGrenadeRings( CBotNPC *me );
	void LaunchGrenadeSpokes( CBotNPC *me );
	int m_animLayer;
};

ConVar tf_bot_npc_grenade_ring_min_horiz_vel( "tf_bot_npc_grenade_ring_min_horiz_vel", "100"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_ring_max_horiz_vel( "tf_bot_npc_grenade_ring_max_horiz_vel", "350"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_vert_vel( "tf_bot_npc_grenade_vert_vel", "750"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_det_time( "tf_bot_npc_grenade_det_time", "3"/*, FCVAR_CHEAT*/ );


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaunchGrenades::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY );
	m_animLayer = me->AddLayeredSequence( me->LookupSequence( "gesture_melee_cheer" ), 0 );

	m_timer.Start( 1.0f );
	m_detonateTimer.Invalidate();
	me->AddCondition( CBotNPC::BUSY );
	me->GetGrenadeTimer()->Start( me->GetGrenadeInterval() );

	me->EmitSound( "RobotBoss.LaunchGrenades" );

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCLaunchGrenades::LaunchGrenade( CBotNPC *me, const Vector &launchVel, CTFWeaponInfo *weaponInfo )
{
	CTFGrenadePipebombProjectile *pProjectile = CTFGrenadePipebombProjectile::Create( me->WorldSpaceCenter(), vec3_angle, launchVel, 
																					  AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ), 
																					  me, *weaponInfo, TF_PROJECTILE_PIPEBOMB_REMOTE, 1 );
	if ( pProjectile )
	{
		pProjectile->SetLauncher( me );
		pProjectile->SetDamage( tf_bot_npc_grenade_damage.GetFloat() );

		if ( me->IsInCondition( CBotNPC::ENRAGED ) )
		{
			pProjectile->SetCritical( true );
		}

		m_grenadeVector.AddToTail( pProjectile );
	}
}


//---------------------------------------------------------------------------------------------
void CBotNPCLaunchGrenades::LaunchGrenadeRings( CBotNPC *me )
{
	const char *weaponAlias = WeaponIdToAlias( TF_WEAPON_GRENADELAUNCHER );
	if ( !weaponAlias )
		return;

	WEAPON_FILE_INFO_HANDLE	weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias );
	if ( weaponInfoHandle == GetInvalidWeaponInfoHandle() )
		return;

	CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) );

	QAngle myAngles = me->EyeAngles();

	// create rings of stickies
	float deltaVel = tf_bot_npc_grenade_ring_max_horiz_vel.GetFloat() - tf_bot_npc_grenade_ring_min_horiz_vel.GetFloat();
	const int ringCount = 2;
	for( int r=0; r<ringCount; ++r )
	{
		float u = (float)r/(float)(ringCount-1);

		float horizVel = tf_bot_npc_grenade_ring_min_horiz_vel.GetFloat() + u * deltaVel;

		float angleDelta = 10.0f + 20.0f * ( 1.0f - u );

		for( float angle=0.0f; angle<360.0f; angle += angleDelta )
		{
			Vector forward;
			AngleVectors( myAngles, &forward );

			Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, tf_bot_npc_grenade_vert_vel.GetFloat() );

			LaunchGrenade( me, vecVelocity, weaponInfo );

			myAngles.y += angleDelta;
		}
	}
}


ConVar tf_bot_npc_grenade_spoke_angle( "tf_bot_npc_grenade_spoke_angle", "45"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_spoke_count( "tf_bot_npc_grenade_spoke_count", "15"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_spoke_min_horiz_vel( "tf_bot_npc_grenade_spoke_min_horiz_vel", "100"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_grenade_spoke_max_horiz_vel( "tf_bot_npc_grenade_spoke_max_horiz_vel", "750"/*, FCVAR_CHEAT*/ );


//---------------------------------------------------------------------------------------------
void CBotNPCLaunchGrenades::LaunchGrenadeSpokes( CBotNPC *me )
{
	const char *weaponAlias = WeaponIdToAlias( TF_WEAPON_GRENADELAUNCHER );
	if ( !weaponAlias )
		return;

	WEAPON_FILE_INFO_HANDLE	weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias );
	if ( weaponInfoHandle == GetInvalidWeaponInfoHandle() )
		return;

	CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) );

	// create spokes of stickies
	float deltaVel = tf_bot_npc_grenade_spoke_max_horiz_vel.GetFloat() - tf_bot_npc_grenade_spoke_min_horiz_vel.GetFloat();
	float angleDelta = tf_bot_npc_grenade_spoke_angle.GetFloat();
	QAngle myAngles = me->EyeAngles();

	for( float angle=0.0f; angle<360.0f; angle += angleDelta )
	{
		Vector forward;
		AngleVectors( myAngles, &forward );

		int spokeCount = tf_bot_npc_grenade_spoke_count.GetInt();

		for( int i=0; i<spokeCount; ++i )
		{
			float u = (float)i/(float)(spokeCount-1);

			float horizVel = tf_bot_npc_grenade_spoke_min_horiz_vel.GetFloat() + u * deltaVel;

			Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, tf_bot_npc_grenade_vert_vel.GetFloat() );

			LaunchGrenade( me, vecVelocity, weaponInfo );
		}

		myAngles.y += angleDelta;
	}
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaunchGrenades::Update( CBotNPC *me, float interval )
{
	QAngle myAngles = me->EyeAngles();

	if ( m_timer.HasStarted() && m_timer.IsElapsed() )
	{
		m_timer.Invalidate();

		if ( RandomInt( 0, 100 ) < 50 )
		{
			LaunchGrenadeRings( me );
		}
		else
		{
			LaunchGrenadeSpokes( me );
		}

		me->EmitSound( "Weapon_Grenade_Normal.Single" );

		m_detonateTimer.Start( tf_bot_npc_grenade_det_time.GetFloat() );
	}

	if ( m_detonateTimer.HasStarted() && m_detonateTimer.IsElapsed() )
	{
		// detonate the stickies
		for( int i=0; i<m_grenadeVector.Count(); ++i )
		{
			if ( m_grenadeVector[i] )
			{
				m_grenadeVector[i]->Detonate();
			}
		}

		return Done();
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCLaunchGrenades::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	// fizzle any outstanding stickies
	for( int i=0; i<m_grenadeVector.Count(); ++i )
	{
		if ( m_grenadeVector[i] )
		{
			m_grenadeVector[i]->Fizzle();
			m_grenadeVector[i]->Detonate();
		}
	}

	me->RemoveCondition( CBotNPC::ENRAGED );
	me->RemoveCondition( CBotNPC::BUSY );
	me->FastRemoveLayer( m_animLayer );
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCShootCrossbow : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );

	// if anything interrupts this action, abort it
	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )	{ return Done(); }

	virtual const char *GetName( void ) const	{ return "ShootCrossbow"; }		// return name of this action

private:
	CountdownTimer m_timer;
};


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCShootCrossbow::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY );
	m_timer.Start( tf_bot_npc_aim_time.GetFloat() );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCShootCrossbow::Update( CBotNPC *me, float interval )
{
	CBaseCombatCharacter *target = me->GetAttackTarget();

	if ( !target )
	{
		return Done( "No target" );
	}

	me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );

	if ( m_timer.IsElapsed() )
	{
		// fire bolt
		const float arrowSpeed = 4000.0f;
		const float arrowGravity = 0.0f;		// railgun

		Vector muzzleOrigin;
		QAngle muzzleAngles;
		if ( me->GetWeapon()->GetAttachment( "muzzle", muzzleOrigin, muzzleAngles ) == false )
		{
			return Done( "No muzzle attachment!" );
		}

		// lead target
		float range = me->GetRangeTo( target->EyePosition() );
		float flightTime = range / arrowSpeed;

		Vector aimSpot = target->EyePosition() + target->GetAbsVelocity() * flightTime;

		Vector to = aimSpot - muzzleOrigin;
		VectorAngles( to, muzzleAngles );

		CTFProjectile_Arrow *arrow = CTFProjectile_Arrow::Create( muzzleOrigin, muzzleAngles, arrowSpeed, arrowGravity, TF_PROJECTILE_ARROW, me, me );
		if ( arrow )
		{
			arrow->SetLauncher( me );
			arrow->SetCritical( true );

			// set damage to 5 points more than our target's max health so a Medic can save us
			// arrow->SetDamage( ( target->GetMaxHealth() + 5.0f ) / TF_DAMAGE_CRIT_MULTIPLIER );
			arrow->SetDamage( 200.0f );

			me->EmitSound( "Weapon_CompoundBow.Single" );
		}

		return Done();
	}

	return Continue();
}



//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCLostVictim : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual const char *GetName( void ) const	{ return "LostVictim"; }		// return name of this action

private:
	CountdownTimer m_timer;
	float m_headTurn;
	int m_headYawPoseParameter;
};


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLostVictim::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	m_headTurn = 0.0f;
	m_headYawPoseParameter = me->LookupPoseParameter( "body_yaw" );

	m_timer.Start( RandomFloat( 3.0f, 5.0f ) );

	me->EmitSound( "RobotBoss.Scanning" );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLostVictim::Update( CBotNPC *me, float interval )
{
	if ( m_timer.IsElapsed() )
	{
		return Done( "Giving up" );
	}

	CBaseCombatCharacter *target = me->GetAttackTarget();
	if ( target )
	{
		if ( me->IsLineOfSightClear( target ) || me->IsPrisonerOfMinion( target ) )
		{
			me->EmitSound( "RobotBoss.Acquire" );
			me->AddGesture( ACT_MP_GESTURE_FLINCH_CHEST );
			return Done( "Ah hah!" );
		}
	}

	const float rate = M_PI / 3.0f;
	m_headTurn += rate * interval;

	float s, c;
	SinCos( m_headTurn, &s, &c );

	me->SetPoseParameter( m_headYawPoseParameter, 40.0f * s );

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCLostVictim::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->SetPoseParameter( m_headYawPoseParameter, 0 );
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCChaseVictim : public Action< CBotNPC >
{
public:
	CBotNPCChaseVictim( CBaseCombatCharacter *chaseTarget );

	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual EventDesiredResult< CBotNPC > OnStuck( CBotNPC *me );
	virtual EventDesiredResult< CBotNPC > OnMoveToSuccess( CBotNPC *me, const Path *path );
	virtual EventDesiredResult< CBotNPC > OnMoveToFailure( CBotNPC *me, const Path *path, MoveToFailureType reason );

	virtual const char *GetName( void ) const	{ return "ChaseVictim"; }		// return name of this action

private:
	CTFPathFollower m_path;
	IntervalTimer m_visibleTimer;
	CHandle< CBaseCombatCharacter > m_lastTarget;

	CHandle< CBaseCombatCharacter > m_chaseTarget;
	Vector m_lastKnownTargetSpot;
};


//---------------------------------------------------------------------------------------------
CBotNPCChaseVictim::CBotNPCChaseVictim( CBaseCombatCharacter *chaseTarget )
{
	m_chaseTarget = chaseTarget;
	m_lastKnownTargetSpot = chaseTarget->GetAbsOrigin();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCChaseVictim::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	if ( m_chaseTarget == NULL )
	{
		return Done( "Target is NULL" );
	}

	m_lastKnownTargetSpot = m_chaseTarget->GetAbsOrigin();

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCChaseVictim::Update( CBotNPC *me, float interval )
{
	if ( m_chaseTarget == NULL || !m_chaseTarget->IsAlive() )
	{
		return ChangeTo( new CBotNPCLostVictim, "No victim" );
	}

	if ( m_chaseTarget != me->GetAttackTarget() )
	{
		return Done( "Changing targets" );
	}

	Vector moveGoal = m_chaseTarget->GetAbsOrigin();

	if ( me->IsLineOfSightClear( m_chaseTarget ) )
	{
		if ( !m_visibleTimer.HasStarted() )
		{
			m_visibleTimer.Start();
		}

		if ( me->HasAbility( CBotNPC::CAN_NUKE ) && me->GetNukeTimer()->IsElapsed() )
		{
			return SuspendFor( new CBotNPCNukeAttack, "Nuking!" );
		}

		m_lastKnownTargetSpot = m_chaseTarget->GetAbsOrigin();

		if ( me->HasAbility( CBotNPC::CAN_LAUNCH_STICKIES ) )
		{
			if ( ( me->GetGrenadeTimer()->IsElapsed() && me->IsRangeLessThan( m_chaseTarget, tf_bot_npc_grenade_launch_range.GetFloat() ) ) ||
				   me->IsInCondition( CBotNPC::ENRAGED ) )
			{
				return SuspendFor( new CBotNPCLaunchGrenades, "Target is close (or I am enraged) - grenades!" );
			}
		}

		// chase into line of sight a bit so they can't immediately get behind cover again
		if ( me->HasAbility( CBotNPC::CAN_FIRE_ROCKETS ) )
		{
			if ( m_visibleTimer.IsGreaterThen( 1.0f ) ||
				 me->IsRangeLessThan( m_chaseTarget, tf_bot_npc_chase_range.GetFloat() ) )
			{
				return SuspendFor( new CBotNPCLaunchRockets, "Fire!" );
			}
		}

		if ( me->IsRangeLessThan( m_chaseTarget, 150.0f ) )
		{
			// too close - stand still
			if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_MELEE ) )
			{
				me->GetBodyInterface()->StartActivity( ACT_MP_STAND_MELEE );
			}

			return Continue();
		}
	}
	else
	{
		m_visibleTimer.Invalidate();

		// move to where we last saw our target
		moveGoal = m_lastKnownTargetSpot;

		if ( me->IsRangeLessThan( m_lastKnownTargetSpot, 20.0f ) )
		{
			// reached spot where we last saw our victim - give up
			me->SetAttackTarget( NULL );

			return ChangeTo( new CBotNPCLostVictim, "I lost my chase victim" );
		}
	}


	// move into sight of target
	if ( m_path.GetAge() > 1.0f )
	{
		CBotNPCPathCost cost( me );
		m_path.Compute( me, moveGoal, cost );
	}

	me->GetLocomotionInterface()->Run();
	m_path.Update( me );

	// play running animation
	if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) )
	{
		me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE );
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCChaseVictim::OnMoveToSuccess( CBotNPC *me, const Path *path )
{
	return TryDone( RESULT_CRITICAL, "Reached move goal" );
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCChaseVictim::OnMoveToFailure( CBotNPC *me, const Path *path, MoveToFailureType reason )
{
	return TryDone( RESULT_CRITICAL, "Path follow failed" );
}


//---------------------------------------------------------------------------------------------
void CBotNPCChaseVictim::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCChaseVictim::OnStuck( CBotNPC *me )
{
	// we're stuck - just warp to the our next path goal
	if ( m_path.GetCurrentGoal() )
	{
		me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) );
	}

	return TryContinue();
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCLaserBlast : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual ActionResult< CBotNPC >	OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction );

	virtual EventDesiredResult< CBotNPC > OnStuck( CBotNPC *me );

	virtual const char *GetName( void ) const	{ return "LaserBlast"; }		// return name of this action

private:
	CTFPathFollower m_path;
	CountdownTimer m_laserTimer;
	IntervalTimer m_visibleTimer;
	CHandle< CBaseCombatCharacter > m_lastTarget;
};

ConVar tf_bot_npc_laser_damage_rate( "tf_bot_npc_laser_damage_rate", "40"/*, FCVAR_CHEAT*/ );				// 20
ConVar tf_bot_npc_laser_damage_gain_rate( "tf_bot_npc_laser_damage_gain_rate", "0"/*, FCVAR_CHEAT*/ );		// 0
ConVar tf_bot_npc_laser_damage_ignite_threshold( "tf_bot_npc_laser_damage_ignite_threshold", "999"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_laser_damage_ignite_time( "tf_bot_npc_laser_damage_ignite_time", "3"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_laser_afterburn_time( "tf_bot_npc_laser_afterburn_time", "10"/*, FCVAR_CHEAT*/ );
ConVar tf_bot_npc_laser_damage_building_multiplier( "tf_bot_npc_laser_damage_building_multiplier", "4"/*, FCVAR_CHEAT*/ );

ConVar tf_bot_npc_laser_duration( "tf_bot_npc_laser_duration", "8"/*, FCVAR_CHEAT*/ );


//----------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaserBlast::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	m_laserTimer.Start( tf_bot_npc_laser_duration.GetFloat() );
	m_visibleTimer.Invalidate();
	m_lastTarget = NULL;

	return Continue();
}


//----------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaserBlast::Update( CBotNPC *me, float interval )
{
	CBaseCombatCharacter *target = me->GetAttackTarget();

	if ( !target )
	{
		return Done( "No victim" );
	}

	if ( me->HasAbility( CBotNPC::CAN_NUKE ) && me->GetNukeTimer()->IsElapsed() )
	{
		return ChangeTo( new CBotNPCNukeAttack, "Nuking!" );
	}

	if ( target != m_lastTarget )
	{
		// new target, reset laser
		m_laserTimer.Reset();
		m_lastTarget = target;
	}

	if ( me->HasAbility( CBotNPC::CAN_FIRE_ROCKETS ) && m_laserTimer.IsElapsed() )
	{
		// laser not effective - try rockets!
		return ChangeTo( new CBotNPCLaunchRockets, "Launching Rockets!" );
	}

	if ( me->IsLineOfSightClear( target ) )
	{
		if ( !m_visibleTimer.HasStarted() )
		{
			m_visibleTimer.Start();
		}

		me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );

		// blast 'em
		me->SetLaserTarget( target );

		float damage = tf_bot_npc_laser_damage_rate.GetFloat() + m_laserTimer.GetElapsedTime() * tf_bot_npc_laser_damage_gain_rate.GetFloat();

		// lasers do extra damage to buildings
		if ( target->IsBaseObject() )
		{
			damage *= tf_bot_npc_laser_damage_building_multiplier.GetFloat();
		}

		CTakeDamageInfo info( me, me, damage * interval, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE );

		Vector toVictim = target->WorldSpaceCenter() - me->EyePosition();
		toVictim.NormalizeInPlace();

		CalculateMeleeDamageForce( &info, toVictim, me->EyePosition(), 1.0f );
		target->TakeDamage( info );

		if ( target->IsPlayer() && damage > tf_bot_npc_laser_damage_ignite_threshold.GetFloat() )
		{
			ToTFPlayer( target )->m_Shared.Burn( me, tf_bot_npc_laser_afterburn_time.GetFloat() );
		}

		if ( target->IsPlayer() && m_laserTimer.GetElapsedTime() > tf_bot_npc_laser_damage_ignite_time.GetFloat() )
		{
			ToTFPlayer( target )->m_Shared.Burn( me, tf_bot_npc_laser_afterburn_time.GetFloat() );
		}

		// me->EmitSound( "Weapon_Sword.HitFlesh" );

		if ( !me->IsPlayingGesture( ACT_MP_GESTURE_FLINCH_CHEST ) )
		{
			me->AddGesture( ACT_MP_GESTURE_FLINCH_CHEST );
		}
	}
	else
	{
		me->SetLaserTarget( NULL );
		m_laserTimer.Reset();
		m_visibleTimer.Invalidate();
	}

	// chase into line of sight a bit so they can't immediately get behind cover again
	if ( !m_visibleTimer.HasStarted() || m_visibleTimer.IsLessThen( 1.0f ) )
	{
		// don't get too close to avoid penetration/stuck issues
		if ( me->IsRangeGreaterThan( target, 100.0f ) )
		{
			// move into sight of target
			if ( m_path.GetAge() > 1.0f )
			{
				CBotNPCPathCost cost( me );
				m_path.Compute( me, target, cost );
			}

			me->GetLocomotionInterface()->Run();
			m_path.Update( me );
		}
	}

	if ( me->GetLocomotionInterface()->IsAttemptingToMove() )
	{
		// play running animation
		if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) )
		{
			me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE );
		}
	}
	else
	{
		// standing still
		if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) )
		{
			me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 );
		}
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCLaserBlast::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->SetLaserTarget( NULL );
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCLaserBlast::OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction )
{
	return Done();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCLaserBlast::OnStuck( CBotNPC *me )
{
	// we're stuck - just warp to the our next path goal
	if ( m_path.GetCurrentGoal() )
	{
		me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) );
	}

	return TryContinue( RESULT_TRY );
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCAttack : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );

	virtual ActionResult< CBotNPC >	OnResume( CBotNPC *me, Action< CBotNPC > *interruptingAction );

	virtual EventDesiredResult< CBotNPC > OnStuck( CBotNPC *me );
	virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL );

	virtual const char *GetName( void ) const	{ return "Attack"; }		// return name of this action

private:
	CTFPathFollower m_path;

	CountdownTimer m_chargeTimer;

	CHandle< CTFPlayer > m_closestVisible;
	CountdownTimer m_attackThrottleTimer;

	void ValidateChaseVictim( CBotNPC *me );

	CountdownTimer m_attackTargetFocusTimer;
};


//----------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCAttack::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	m_attackThrottleTimer.Invalidate();

	m_closestVisible = NULL;

	m_attackTargetFocusTimer.Invalidate();

	m_chargeTimer.Invalidate();

	return Continue();
}


//----------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCAttack::Update( CBotNPC *me, float interval )
{
	if ( !me->IsAlive() )
	{
		return Done();
	}

	CBaseCombatCharacter *target = me->GetAttackTarget();

	if ( !target )
	{
		return Done( "No victim" );
	}

	me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );

	// swing our axe at our attack target if they are in range
	if ( !me->IsSwingingAxe() )
	{
		if ( me->IsRangeLessThan( target, tf_bot_npc_attack_range.GetFloat() ) )
		{
			me->SwingAxe();
		}
	}

	if ( !me->IsSwingingAxe() )
	{
		if ( m_chargeTimer.IsElapsed() && me->IsLookingTowards( target->WorldSpaceCenter(), 0.9f ) )
		{
			m_chargeTimer.Start( tf_bot_npc_charge_interval.GetFloat() );
			return SuspendFor( new CBotNPCRush, "Chaaarge!" );
		}

		if ( me->GetReceivedDamagePerSecond() > tf_bot_npc_block_dps_react.GetFloat() &&
			 target->IsPlayer() && 
			 ToTFPlayer( target )->GetTimeSinceWeaponFired() < 1.0f )
		{
			return SuspendFor( new CBotNPCBlock, "Blocking" );
		}
	}

	// chase after our victim
	const float standAndSwingRange = 0.5f * tf_bot_npc_attack_range.GetFloat();

	if ( me->IsRangeGreaterThan( target, standAndSwingRange ) || !me->IsLineOfSightClear( target ) )
	{
		if ( m_path.GetAge() > 1.0f )
		{
			CBotNPCPathCost cost( me );
			m_path.Compute( me, target, cost );
		}

		m_path.Update( me );
	}

	if ( me->GetLocomotionInterface()->IsAttemptingToMove() )
	{
		// play running animation
		if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) )
		{
			me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE );
		}
	}
	else
	{
		// standing still
		if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) )
		{
			me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 );
		}
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCAttack::OnResume( CBotNPC *me, Action< CBotNPC > *interruptingAction )
{
	me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE );
	return Continue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCAttack::OnStuck( CBotNPC *me )
{
	// we're stuck - just warp to the our next path goal
	if ( m_path.GetCurrentGoal() )
	{
		me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) );
	}

	return TryContinue( RESULT_TRY );
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCAttack::OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result )
{
	return TryContinue( RESULT_TRY );
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCGuardSpot : public Action< CBotNPC >
{
public:
	//-----------------------------------------------------------------------------------------------------
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
	{
		m_path.SetMinLookAheadDistance( 300.0f );

		me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 );
		me->SetHomePosition( me->GetAbsOrigin() );

		m_lookAtSpot = vec3_origin;

		return Continue();
	}

	//-----------------------------------------------------------------------------------------------------
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval )
	{
		CBaseCombatCharacter *target = me->GetAttackTarget();
		if ( target )
		{
			if ( me->IsLineOfSightClear( target ) || me->IsPrisonerOfMinion( target ) )
			{
				return SuspendFor( new CBotNPCChaseVictim( me->GetAttackTarget() ), "Get 'em!" );
			}
		}

		CBaseCombatCharacter *visible = me->GetNearestVisibleEnemy();
		if ( visible )
		{
			// look at visible victim out of range
			me->GetLocomotionInterface()->FaceTowards( visible->WorldSpaceCenter() );
		}

		const float atHomeRange = 50.0f;
		if ( me->IsRangeGreaterThan( me->GetHomePosition(), atHomeRange ) )
		{
			if ( m_path.GetAge() > 3.0f )
			{
				CBotNPCPathCost cost( me );
				if ( m_path.Compute( me, me->GetHomePosition(), cost ) == false )
				{
					// can't reach guard post - just jump there for now
					me->Teleport( &me->GetHomePosition(), NULL, NULL );
				}
			}

			m_path.Update( me );
		}
		else
		{
			// on guard spot - look around
			if ( m_lookTimer.IsElapsed() )
			{
				m_lookTimer.Start( RandomFloat( 1.0f, 2.0f ) );

				CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea();
				if ( myArea )
				{
					const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( TF_TEAM_RED );

					if ( invasionAreaVector.Count() > 0 )
					{
						// try to not look directly at walls
						const float minGazeRange = 300.0f;
						const int retryCount = 20.0f;
						for( int r=0; r<retryCount; ++r )
						{
							int which = RandomInt( 0, invasionAreaVector.Count()-1 );
							Vector gazeSpot = invasionAreaVector[ which ]->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight );

							if ( me->IsRangeGreaterThan( gazeSpot, minGazeRange ) && me->GetVisionInterface()->IsLineOfSightClear( gazeSpot ) )
							{
								// use maxLookInterval so these looks override body aiming from path following
								m_lookAtSpot = gazeSpot;
								break;
							}
						}
					}
				}
			}

			me->GetLocomotionInterface()->FaceTowards( m_lookAtSpot );
		}

		if ( me->GetLocomotionInterface()->IsAttemptingToMove() )
		{
			// play running animation
			if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) )
			{
				me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE );
			}
		}
		else
		{
			// standing still
			if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) )
			{
				me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 );
			}
		}

		return Continue();
	}

	//-----------------------------------------------------------------------------------------------------
	virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info )
	{
		CTFPlayer *attacker = ToTFPlayer( info.GetAttacker() );

		if ( me->HasAbility( CBotNPC::CAN_BE_STUNNED ) && attacker )
		{
			if ( tf_bot_npc_always_stun.GetBool() )
			{
				return TrySuspendFor( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), RESULT_CRITICAL, "CVar force stunned" );
			}


			bool isDeflectedRocket = false;
			CTFBaseRocket *pBaseRocket = dynamic_cast< CTFBaseRocket * >( info.GetInflictor() );
			if ( pBaseRocket && pBaseRocket->GetDeflected() )
			{
				isDeflectedRocket = true;
			}

			const float hardHit = 50.0f;
			bool isPotentialStunHit = info.GetDamage() > hardHit || isDeflectedRocket;
			
			if ( m_headStunTimer.IsElapsed() && isPotentialStunHit )
			{
				Vector headPos;
				QAngle headAngles;
				if ( me->GetAttachment( "head", headPos, headAngles ) )
				{
					if ( ( info.GetDamagePosition() - headPos ).IsLengthLessThan( tf_bot_npc_head_radius.GetFloat() ) )
					{
						// hit head

						// deflecting consecutive Boss' rockets into his head == stun
						if ( isDeflectedRocket )
						{
							if ( !m_consecutiveRocketTimer.HasStarted() ||	// first rocket hit
								 m_consecutiveRocketTimer.IsElapsed() )		// too much time between hits - treat as first hit
							{
								m_consecutiveRocketTimer.Start( tf_bot_npc_stun_rocket_reflect_duration.GetFloat() );
								m_consecutiveRockets = 1;
							}
							else
							{
								// successive rocket hit
								if ( ++m_consecutiveRockets >= tf_bot_npc_stun_rocket_reflect_count.GetInt() )
								{
									return TrySuspendFor( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), RESULT_CRITICAL, "My own rockets reflected into my head!" );								
								}
							}

							me->EmitSound( "RobotBoss.Vulnerable" );
						}

						// look for hard hits from above
						Vector toAttacker = attacker->EyePosition() - headPos;
						toAttacker.NormalizeInPlace();

						if ( toAttacker.z > 0.9f )
						{
							// just got hit in the head from an attacker above me - stun
							m_headStunTimer.Start( 20.0f );

							return TrySuspendFor( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), RESULT_CRITICAL, "Hard head hit from above!" );
						}
					}
				}
			}
		}

		return TryContinue();
	}

	//-----------------------------------------------------------------------------------------------------
	virtual const char *GetName( void ) const	{ return "GuardSpot"; }		// return name of this action

private:
	CTFPathFollower m_path;
	CountdownTimer m_lookTimer;
	Vector m_lookAtSpot;
	CountdownTimer m_headStunTimer;

	CountdownTimer m_consecutiveRocketTimer;
	int m_consecutiveRockets;
};


//---------------------------------------------------------------------------------------------
ConVar tf_bot_npc_get_off_me_duration( "tf_bot_npc_get_off_me_duration", "3"/*, FCVAR_CHEAT */ );

ActionResult< CBotNPC >	CBotNPCGetOffMe::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	me->AddGestureSequence( me->LookupSequence( "gesture_melee_help" ) );
	m_timer.Start( 0.5f );

	me->AddCondition( CBotNPC::BUSY );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCGetOffMe::Update( CBotNPC *me, float interval )
{
	if ( m_timer.IsElapsed() )
	{
		// blast players off of my head
		CUtlVector< CTFPlayer * > onMeVector;
		me->CollectPlayersStandingOnMe( &onMeVector );

		Vector headPos;
		QAngle headAngles;
		if ( me->GetAttachment( "head", headPos, headAngles ) )
		{
			for( int i=0; i<onMeVector.Count(); ++i )
			{
				// push 'em off
				PushawayPlayer( onMeVector[i], headPos, tf_bot_npc_charge_pushaway_force.GetFloat() );
			}
		}

		me->EmitSound( "Weapon_FlameThrower.AirBurstAttack" );

		return Done();
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCGetOffMe::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::BUSY );
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCWaitForPlayers : public Action< CBotNPC >
{
public:
	virtual ActionResult< CBotNPC >	OnStart( CBotNPC *me, Action< CBotNPC > *priorAction );
	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval );
	virtual void					OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction );

	virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info );
	virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL );

	virtual const char *GetName( void ) const	{ return "WaitForPlayers"; }		// return name of this action
};


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCWaitForPlayers::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
{
	me->AddCondition( CBotNPC::BUSY );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CBotNPC >	CBotNPCWaitForPlayers::Update( CBotNPC *me, float interval )
{
	CBaseCombatCharacter *target = me->GetAttackTarget();
	if ( target )
	{
		return ChangeTo( new CBotNPCGuardSpot, "I see you..." );
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
void CBotNPCWaitForPlayers::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction )
{
	me->RemoveCondition( CBotNPC::BUSY );

	me->GetNukeTimer()->Start( tf_bot_npc_nuke_interval.GetFloat() );
	me->GetGrenadeTimer()->Reset();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCWaitForPlayers::OnInjured( CBotNPC *me, const CTakeDamageInfo &info )
{
	return TryChangeTo( new CBotNPCGuardSpot, RESULT_CRITICAL, "Ouch!" );
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CBotNPC > CBotNPCWaitForPlayers::OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result )
{
	if ( other && other->IsPlayer() )
	{
		return TryChangeTo( new CBotNPCGuardSpot, RESULT_CRITICAL, "Don't touch me" );
	}

	return TryContinue();
}


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCTacticalMonitor : public Action< CBotNPC >
{
public:
	virtual Action< CBotNPC > *InitialContainedAction( CBotNPC *me )	
	{
		if ( TFGameRules()->IsBossBattleMode() )
		{
			return new CBotNPCWaitForPlayers;
		}

		return NULL;
	}

	virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction )
	{ 
		m_getOffMeTimer.Invalidate();

		return Continue(); 
	}

	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval )
	{
		// HACK: If we fell off the ledge, jump back
/*
		if ( me->GetLocomotionInterface()->IsOnGround() && 
			 me->GetAbsOrigin().z < me->GetHomePosition().z - 200.0f )
		{
			return SuspendFor( new CBotNPCBigJump( me->GetHomePosition(), new CBotNPCLaunchRockets ), "Jumping home" );
		}
*/

		if ( !m_getOffMeTimer.HasStarted() )
		{
			CUtlVector< CTFPlayer * > onMeVector;
			me->CollectPlayersStandingOnMe( &onMeVector );

			if ( onMeVector.Count() )
			{
				// someone is standing on me - push them off soon
				m_getOffMeTimer.Start( tf_bot_npc_get_off_me_duration.GetFloat() );
			}
		}
		else if ( m_getOffMeTimer.IsElapsed() )
		{
			if ( !me->IsBusy() )
			{
				m_getOffMeTimer.Invalidate();

				// if someone is still on me, push them off
				CUtlVector< CTFPlayer * > onMeVector;
				me->CollectPlayersStandingOnMe( &onMeVector );
				if ( onMeVector.Count() )
				{
					return SuspendFor( new CBotNPCGetOffMe, "Get offa me!" );
				}
			}
		}

		return Continue();
	}

	virtual const char *GetName( void ) const	{ return "TacticalMonitor"; }		// return name of this action

private:
	CountdownTimer m_backOffCooldownTimer;

	CountdownTimer m_getOffMeTimer;
};


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
class CBotNPCBehavior : public Action< CBotNPC >
{
public:
	virtual Action< CBotNPC > *InitialContainedAction( CBotNPC *me )	
	{
		return new CBotNPCTacticalMonitor;
	}

	virtual ActionResult< CBotNPC >	Update( CBotNPC *me, float interval )
	{
		if ( m_vocalTimer.IsElapsed() )
		{
			m_vocalTimer.Start( RandomFloat( 3.0f, 5.0f ) );

			if ( !me->IsBusy() )
			{
				me->EmitSound( "RobotBoss.Vocalize" );
			}
		}

		return Continue();
	}

	virtual EventDesiredResult< CBotNPC > OnKilled( CBotNPC *me, const CTakeDamageInfo &info )
	{
		// relay the event to the map logic
		CTFSpawnerBoss *spawner = me->GetSpawner();
		if ( spawner )
		{
			spawner->OnBotKilled( me );
		}

		// Calculate death force
		Vector forceVector = me->CalcDamageForceVector( info );

		// See if there's a ragdoll magnet that should influence our force.
		CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( me );
		if ( magnet )
		{
			forceVector += magnet->GetForceVector( me );
		}

		if ( me->IsMiniBoss() )
		{
			me->EmitSound( "Cart.Explode" );
			me->BecomeRagdoll( info, forceVector );

			if ( g_pMonsterResource )
			{
				g_pMonsterResource->HideBossHealthMeter();
			}
		}
		else
		{
			// full end-of-game boss
			UTIL_Remove( me );

			if ( TFGameRules()->IsBossBattleMode() )
			{
				// check that ALL bosses are dead
				bool isBossBattleWon = true;

				CBotNPC *boss = NULL;
				while( ( boss = (CBotNPC *)gEntList.FindEntityByClassname( boss, "bot_boss" ) ) != NULL )
				{
					if ( !me->IsSelf( boss ) && boss->IsAlive() && !boss->IsMiniBoss() )
					{
						isBossBattleWon = false;
					}
				}

				if ( isBossBattleWon )
				{
					TFGameRules()->SetWinningTeam( TF_TEAM_BLUE, WINREASON_OPPONENTS_DEAD );

					if ( g_pMonsterResource )
					{
						g_pMonsterResource->HideBossHealthMeter();
					}
				}
			}
		}

		return TryDone();
	}

	virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL )
	{
		return TryContinue();
	}

	virtual const char *GetName( void ) const	{ return "Behavior"; }		// return name of this action

private:
	CountdownTimer m_vocalTimer;
};


//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
CBotNPCIntention::CBotNPCIntention( CBotNPC *me ) : IIntention( me )
{ 
	m_behavior = new Behavior< CBotNPC >( new CBotNPCBehavior ); 
}

CBotNPCIntention::~CBotNPCIntention()
{
	delete m_behavior;
}

void CBotNPCIntention::Reset( void )
{ 
	delete m_behavior; 
	m_behavior = new Behavior< CBotNPC >( new CBotNPCBehavior );
}

void CBotNPCIntention::Update( void )
{
	m_behavior->Update( static_cast< CBotNPC * >( GetBot() ), GetUpdateInterval() ); 
}

// is the a place we can be?
QueryResultType CBotNPCIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const
{
	return ANSWER_YES;
}



//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
CBotNPCLocomotion::CBotNPCLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) 
{ 
	CBotNPC *me = (CBotNPC *)GetBot()->GetEntity();

	m_runSpeed = me->GetMoveSpeed();
}


//---------------------------------------------------------------------------------------------
float CBotNPCLocomotion::GetRunSpeed( void ) const
{
	CBotNPC *me = (CBotNPC *)GetBot()->GetEntity();

	return me->IsInCondition( CBotNPC::CHARGING ) ? 1000.0f : m_runSpeed;
}


//---------------------------------------------------------------------------------------------
// if delta Z is greater than this, we have to jump to get up
float CBotNPCLocomotion::GetStepHeight( void ) const
{
	return 18.0f;
}


//---------------------------------------------------------------------------------------------
// return maximum height of a jump
float CBotNPCLocomotion::GetMaxJumpHeight( void ) const
{
	return 18.0f;
}


//---------------------------------------------------------------------------------------------
// Return true to completely ignore this entity (may not be in sight when this is called)
bool CBotNPCVision::IsIgnored( CBaseEntity *subject ) const
{
	if ( subject->IsPlayer() )
	{
		CTFPlayer *enemy = static_cast< CTFPlayer * >( subject );

		if ( enemy->m_Shared.InCond( TF_COND_BURNING ) ||
			 enemy->m_Shared.InCond( TF_COND_URINE ) ||
			 enemy->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) ||
			 enemy->m_Shared.InCond( TF_COND_BLEEDING ) )
		{
			// always notice players with these conditions
			return false;
		}

		if ( enemy->m_Shared.IsStealthed() )
		{
			if ( enemy->m_Shared.GetPercentInvisible() < 0.75f )
			{
				// spy is partially cloaked, and therefore attracts our attention
				return false;
			}

			// invisible!
			return true;
		}

		if ( enemy->IsPlacingSapper() )
		{
			return false;
		}

		if ( enemy->m_Shared.InCond( TF_COND_DISGUISING ) )
		{
			return false;
		}
		
		if ( enemy->m_Shared.InCond( TF_COND_DISGUISED ) && enemy->m_Shared.GetDisguiseTeam() == GetBot()->GetEntity()->GetTeamNumber() )
		{
			// spy is disguised as a member of my team
			return true;
		}
	}

	return false;
}

#endif // TF_RAID_MODE

#endif // OBSOLETE_USE_BOSS_ALPHA