summaryrefslogtreecommitdiff
path: root/game/server/tf/bot_npc
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/bot_npc
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/tf/bot_npc')
-rw-r--r--game/server/tf/bot_npc/bot_npc.cpp3604
-rw-r--r--game/server/tf/bot_npc/bot_npc.h541
-rw-r--r--game/server/tf/bot_npc/bot_npc_archer.cpp402
-rw-r--r--game/server/tf/bot_npc/bot_npc_archer.h79
-rw-r--r--game/server/tf/bot_npc/bot_npc_body.cpp153
-rw-r--r--game/server/tf/bot_npc/bot_npc_body.h64
-rw-r--r--game/server/tf/bot_npc/bot_npc_decoy.cpp246
-rw-r--r--game/server/tf/bot_npc/bot_npc_decoy.h80
-rw-r--r--game/server/tf/bot_npc/bot_npc_mini.cpp101
-rw-r--r--game/server/tf/bot_npc/bot_npc_mini.h83
-rw-r--r--game/server/tf/bot_npc/bot_npc_minion.cpp1239
-rw-r--r--game/server/tf/bot_npc/bot_npc_minion.h199
12 files changed, 6791 insertions, 0 deletions
diff --git a/game/server/tf/bot_npc/bot_npc.cpp b/game/server/tf/bot_npc/bot_npc.cpp
new file mode 100644
index 0000000..03d5bca
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc.cpp
@@ -0,0 +1,3604 @@
+//========= 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
diff --git a/game/server/tf/bot_npc/bot_npc.h b/game/server/tf/bot_npc/bot_npc.h
new file mode 100644
index 0000000..b3ad2e7
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc.h
@@ -0,0 +1,541 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc.h
+// A NextBot non-player derived actor
+// Michael Booth, November 2010
+
+#ifndef BOT_NPC_H
+#define BOT_NPC_H
+
+#ifdef OBSOLETE_USE_BOSS_ALPHA
+
+#include "NextBot.h"
+#include "NextBotBehavior.h"
+#include "NextBotGroundLocomotion.h"
+#include "Path/NextBotPathFollow.h"
+#include "bot_npc_body.h"
+#include "bot/map_entities/tf_spawner_boss.h"
+
+class CTFPlayer;
+class CBotNPC;
+
+
+//----------------------------------------------------------------------------
+class CBotNPCLocomotion : public NextBotGroundLocomotion
+{
+public:
+ CBotNPCLocomotion( INextBot *bot );
+ virtual ~CBotNPCLocomotion() { }
+
+ virtual float GetRunSpeed( void ) const; // get maximum running speed
+ virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up
+ virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump
+
+ virtual float GetMaxAcceleration( void ) const
+ {
+ return 2500.0f;
+ }
+
+private:
+ float m_runSpeed;
+};
+
+
+//----------------------------------------------------------------------------
+class CBotNPCIntention : public IIntention
+{
+public:
+ CBotNPCIntention( CBotNPC *me );
+ virtual ~CBotNPCIntention();
+
+ virtual void Reset( void );
+ virtual void Update( void );
+
+ virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be?
+
+ virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; }
+ virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; }
+
+private:
+ Behavior< CBotNPC > *m_behavior;
+};
+
+
+//----------------------------------------------------------------------------
+class CBotNPCVision : public IVision
+{
+public:
+ CBotNPCVision( INextBot *bot ) : IVision( bot )
+ {
+ }
+
+ virtual ~CBotNPCVision() { }
+
+ virtual bool IsIgnored( CBaseEntity *subject ) const; // return true to completely ignore this entity (may not be in sight when this is called)
+};
+
+
+//----------------------------------------------------------------------------
+class CBotNPCWeapon : public CBaseAnimating
+{
+public:
+ CBotNPCWeapon( CBotNPC *owner )
+ {
+ m_owner = owner;
+ }
+
+ virtual ~CBotNPCWeapon() { }
+
+ virtual void StartAttack( void ) { }
+ virtual void Update( void ) { }
+
+private:
+ CHandle< CBotNPC > m_owner;
+};
+
+
+//----------------------------------------------------------------------------
+class CBotNPCWeapon_Axe : public CBotNPCWeapon
+{
+public:
+ DECLARE_CLASS( CBotNPCWeapon_Axe, CBotNPCWeapon );
+
+ CBotNPCWeapon_Axe( CBotNPC *owner );
+ virtual ~CBotNPCWeapon_Axe() { }
+
+ virtual void StartAttack( void );
+ virtual void Update( void );
+};
+
+
+//----------------------------------------------------------------------------
+//----------------------------------------------------------------------------
+class CBotNPCGetOffMe : 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 "GetOffMe"; } // return name of this action
+
+private:
+ CountdownTimer m_timer;
+};
+
+
+
+//----------------------------------------------------------------------------
+//----------------------------------------------------------------------------
+class CBotNPC : public NextBotCombatCharacter
+{
+public:
+ DECLARE_CLASS( CBotNPC, NextBotCombatCharacter );
+ DECLARE_SERVERCLASS();
+
+ CBotNPC();
+ virtual ~CBotNPC();
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info );
+
+ // INextBot
+ virtual CBotNPCIntention *GetIntentionInterface( void ) const { return m_intention; }
+ virtual CBotNPCLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; }
+ virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; }
+ virtual CBotNPCVision *GetVisionInterface( void ) const { return m_vision; }
+
+ virtual void Update( void );
+
+ virtual bool IsPotentiallyChaseable( CTFPlayer *victim );
+
+ void SetSpawner( CTFSpawnerBoss *spawner ); // remember the spawner that created us
+ CTFSpawnerBoss *GetSpawner( void ) const; // return the spawner that created us
+
+ void Break( void ); // bust into gibs
+
+ struct AttackerInfo
+ {
+ CHandle< CBaseCombatCharacter > m_attacker;
+ float m_timestamp;
+ float m_damage;
+ bool m_wasCritical;
+ };
+ const CUtlVector< AttackerInfo > &GetAttackerVector( void ) const;
+ void RememberAttacker( CBaseCombatCharacter *attacker, float damage, bool wasCritical );
+
+ struct ThreatInfo
+ {
+ CHandle< CBaseCombatCharacter > m_who;
+ float m_threat;
+ };
+
+ const ThreatInfo *GetMaxThreat( void ) const;
+ const ThreatInfo *GetThreat( CBaseCombatCharacter *who ) const;
+
+ void SwingAxe( void );
+ void UpdateAxeSwing( void );
+ bool IsSwingingAxe( void ) const;
+
+
+ //----------------------------------
+ enum Ability
+ {
+ CAN_BE_STUNNED = 0x01,
+ CAN_NUKE = 0x02,
+ CAN_ENRAGE = 0x04,
+ CAN_FIRE_ROCKETS = 0x08,
+ CAN_LAUNCH_STICKIES = 0x10,
+ CAN_LAUNCH_MINIONS = 0x20,
+ };
+ virtual bool HasAbility( Ability ability ) const;
+
+ virtual bool IsMiniBoss( void ) const { return false; }
+
+ virtual float GetMoveSpeed( void ) const { return 300.0f; }
+
+ virtual int GetRocketLaunchCount( void ) const { return 5; }
+ virtual float GetRocketDamage( void ) const { return 25.0f; }
+ virtual float GetRocketAimError( void ) const { return 1.81f; }
+ virtual float GetRocketInterval( void ) const { return 0.3f; }
+ virtual const char *GetRocketSoundEffect( void ) const { return "Weapon_RPG.Single"; }
+
+ virtual float GetGrenadeInterval( void ) const { return 10.0f; }
+
+ virtual float GetBecomeStunnedDamage( void ) const { return 500.0f; }
+
+
+ //----------------------------------
+ enum Condition
+ {
+ SHIELDED = 0x01,
+ CHARGING = 0x02,
+ STUNNED = 0x04,
+ INVULNERABLE = 0x08,
+ VULNERABLE_TO_STUN = 0x10,
+ BUSY = 0x20,
+ ENRAGED = 0x40,
+ };
+
+ bool IsBusy( void ) const; // returns true if we're in a condition that means we can't start another action
+
+ void AddCondition( Condition c );
+ void RemoveCondition( Condition c );
+ bool IsInCondition( Condition c ) const;
+
+ bool IsAttackTarget( CBaseCombatCharacter *target ) const;
+ bool HasAttackTarget( void ) const;
+ void SetAttackTarget( CBaseCombatCharacter *target, float duration = 0.0f );
+ CBaseCombatCharacter *GetAttackTarget( void ) const;
+ void LockAttackTarget( void ); // don't allow target to change until it is unlocked or the target is destroyed
+ void UnlockAttackTarget( void );
+
+ CBaseCombatCharacter *GetNearestVisibleEnemy( void ) const;
+
+ void SetHomePosition( const Vector &pos );
+ const Vector &GetHomePosition( void ) const;
+
+ CBaseAnimating *GetWeapon( void ) const;
+ CBaseAnimating *GetShield( void ) const;
+
+ CountdownTimer *GetNukeTimer( void );
+ CountdownTimer *GetGrenadeTimer( void );
+
+ float GetReceivedDamagePerSecond( void ) const;
+ float GetReceivedDamagePerSecondDelta( void ) const;
+
+ void SetLaserTarget( CBaseEntity *target );
+ CBaseEntity *GetLaserTarget( void ) const;
+
+ void ClearStunDamage( void );
+ void AccumulateStunDamage( float damage );
+ float GetStunDamage( void ) const;
+
+ CTFPlayer *GetClosestMinionPrisoner( void );
+ bool IsPrisonerOfMinion( CBaseCombatCharacter *victim );
+
+ void StartNukeEffect( void );
+ void StopNukeEffect( void );
+
+ float GetAge( void ) const; // how long have we been alive
+
+ void CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector );
+
+ // Entity I/O
+ void InputSpawn( inputdata_t &inputdata );
+ COutputEvent m_outputOnStunned; // fired the boss becomes stunned
+
+private:
+ CBotNPCIntention *m_intention;
+ CBotNPCLocomotion *m_locomotor;
+ CBotNPCBody *m_body;
+ CBotNPCVision *m_vision;
+
+ CHandle< CTFSpawnerBoss > m_spawner;
+
+ CBaseAnimating *m_axe;
+ CBaseAnimating *m_shield;
+
+ void PrecacheArmorParts( void );
+ void InstallArmorParts( void );
+ CUtlVector< CBaseAnimating * > m_armorPartVector;
+
+ CountdownTimer m_axeSwingTimer;
+ CountdownTimer m_attackTimer;
+ CountdownTimer m_nukeTimer;
+ CountdownTimer m_grenadeTimer;
+ CountdownTimer m_ouchTimer;
+ CountdownTimer m_hateTauntTimer;
+
+ CNetworkHandle( CBaseEntity, m_laserTarget );
+ CNetworkVar( bool, m_isNuking );
+
+ CHandle< CBaseCombatCharacter > m_nearestVisibleEnemy;
+ void UpdateNearestVisibleEnemy( void );
+ CountdownTimer m_nearestVisibleEnemyTimer;
+
+ CUtlVector< AttackerInfo > m_attackerVector; // list of everyone who injured me, and when
+ CUtlVector< ThreatInfo > m_threatVector; // list of attackers and their current damage/second on me
+
+ float m_currentDamagePerSecond;
+ float m_lastDamagePerSecond;
+ void UpdateDamagePerSecond( void );
+
+ CHandle< CBaseCombatCharacter > m_attackTarget;
+ CountdownTimer m_attackTargetTimer;
+ bool m_isAttackTargetLocked;
+ void UpdateAttackTarget( void );
+
+ int m_damagePoseParameter;
+
+ bool m_isShielded;
+ Vector m_homePos;
+
+ bool IsIgnored( CTFPlayer *player ) const;
+
+ unsigned int m_conditionFlags;
+
+ float m_stunDamage;
+
+ IntervalTimer m_ageTimer;
+};
+
+
+inline bool CBotNPC::HasAbility( Ability ability ) const
+{
+ const int myAbilities = CAN_BE_STUNNED | CAN_NUKE | CAN_ENRAGE | CAN_FIRE_ROCKETS | CAN_LAUNCH_STICKIES | CAN_LAUNCH_MINIONS;
+
+ return myAbilities & ability ? true : false;
+}
+
+inline void CBotNPC::SetSpawner( CTFSpawnerBoss *spawner )
+{
+ m_spawner = spawner;
+}
+
+inline CTFSpawnerBoss *CBotNPC::GetSpawner( void ) const
+{
+ return m_spawner;
+}
+
+inline bool CBotNPC::IsAttackTarget( CBaseCombatCharacter *target ) const
+{
+ if ( HasAttackTarget() )
+ {
+ return ( m_attackTarget == target ) ? true : false;
+ }
+ return false;
+}
+
+inline bool CBotNPC::HasAttackTarget( void ) const
+{
+ return ( m_attackTarget == NULL || !m_attackTarget->IsAlive() ) ? false : true;
+}
+
+inline void CBotNPC::LockAttackTarget( void )
+{
+ m_isAttackTargetLocked = HasAttackTarget();
+}
+
+inline void CBotNPC::UnlockAttackTarget( void )
+{
+ m_isAttackTargetLocked = false;
+}
+
+inline float CBotNPC::GetAge( void ) const
+{
+ return m_ageTimer.GetElapsedTime();
+}
+
+inline void CBotNPC::StartNukeEffect( void )
+{
+ m_isNuking = true;
+}
+
+inline void CBotNPC::StopNukeEffect( void )
+{
+ m_isNuking = false;
+}
+
+inline void CBotNPC::ClearStunDamage( void )
+{
+ m_stunDamage = 0.0f;
+}
+
+inline void CBotNPC::AccumulateStunDamage( float damage )
+{
+ m_stunDamage += damage;
+}
+
+inline float CBotNPC::GetStunDamage( void ) const
+{
+ return m_stunDamage;
+}
+
+inline void CBotNPC::SetLaserTarget( CBaseEntity *target )
+{
+ m_laserTarget = target;
+}
+
+inline CBaseEntity *CBotNPC::GetLaserTarget( void ) const
+{
+ return m_laserTarget;
+}
+
+inline float CBotNPC::GetReceivedDamagePerSecond( void ) const
+{
+ return m_currentDamagePerSecond;
+}
+
+inline float CBotNPC::GetReceivedDamagePerSecondDelta( void ) const
+{
+ return m_currentDamagePerSecond - m_lastDamagePerSecond;
+}
+
+inline CountdownTimer *CBotNPC::GetNukeTimer( void )
+{
+ return &m_nukeTimer;
+}
+
+inline CountdownTimer *CBotNPC::GetGrenadeTimer( void )
+{
+ return &m_grenadeTimer;
+}
+
+inline CBaseAnimating *CBotNPC::GetWeapon( void ) const
+{
+ return m_axe;
+}
+
+inline CBaseAnimating *CBotNPC::GetShield( void ) const
+{
+ return m_shield;
+}
+
+inline void CBotNPC::SetHomePosition( const Vector &pos )
+{
+ m_homePos = pos;
+}
+
+inline const Vector &CBotNPC::GetHomePosition( void ) const
+{
+ return m_homePos;
+}
+
+inline CBaseCombatCharacter *CBotNPC::GetNearestVisibleEnemy( void ) const
+{
+ return m_nearestVisibleEnemy;
+}
+
+inline void CBotNPC::AddCondition( Condition c )
+{
+ m_conditionFlags |= c;
+}
+
+inline bool CBotNPC::IsInCondition( Condition c ) const
+{
+ return ( m_conditionFlags & c ) ? true : false;
+}
+
+inline const CUtlVector< CBotNPC::AttackerInfo > &CBotNPC::GetAttackerVector( void ) const
+{
+ return m_attackerVector;
+}
+
+
+//--------------------------------------------------------------------------------------------------------------
+class CBotNPCPathCost : public IPathCost
+{
+public:
+ CBotNPCPathCost( CBotNPC *me )
+ {
+ m_me = me;
+ }
+
+ // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed
+ virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const
+ {
+ if ( fromArea == NULL )
+ {
+ // first area in path, no cost
+ return 0.0f;
+ }
+ else
+ {
+ if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) )
+ {
+ // our locomotor says we can't move here
+ return -1.0f;
+ }
+
+ // compute distance traveled along path so far
+ float dist;
+
+ if ( ladder )
+ {
+ dist = ladder->m_length;
+ }
+ else if ( length > 0.0 )
+ {
+ // optimization to avoid recomputing length
+ dist = length;
+ }
+ else
+ {
+ dist = ( area->GetCenter() - fromArea->GetCenter() ).Length();
+ }
+
+ float cost = dist + fromArea->GetCostSoFar();
+
+ // check height change
+ float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area );
+ if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() )
+ {
+ if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() )
+ {
+ // too high to reach
+ return -1.0f;
+ }
+
+ // jumping is slower than flat ground
+ const float jumpPenalty = 5.0f;
+ cost += jumpPenalty * dist;
+ }
+ else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() )
+ {
+ // too far to drop
+ return -1.0f;
+ }
+
+ return cost;
+ }
+ }
+
+ CBotNPC *m_me;
+};
+
+
+#endif // #ifdef OBSOLETE_USE_BOSS_ALPHA
+
+#endif // BOT_NPC_H
diff --git a/game/server/tf/bot_npc/bot_npc_archer.cpp b/game/server/tf/bot_npc/bot_npc_archer.cpp
new file mode 100644
index 0000000..592def4
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_archer.cpp
@@ -0,0 +1,402 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_archer.cpp
+// A NextBot non-player derived archer
+// Michael Booth, November 2010
+
+#include "cbase.h"
+
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_team.h"
+#include "tf_projectile_arrow.h"
+#include "tf_weapon_grenade_pipebomb.h"
+#include "nav_mesh/tf_nav_area.h"
+#include "bot_npc_archer.h"
+#include "NextBot/Path/NextBotChasePath.h"
+#include "econ_wearable.h"
+#include "team_control_point_master.h"
+#include "particle_parse.h"
+#include "CRagdollMagnet.h"
+#include "NextBot/Behavior/BehaviorMoveTo.h"
+
+ConVar tf_bot_npc_archer_health( "tf_bot_npc_archer_health", "100", FCVAR_CHEAT );
+
+ConVar tf_bot_npc_archer_speed( "tf_bot_npc_archer_speed", "100", FCVAR_CHEAT );
+
+ConVar tf_bot_npc_archer_shoot_interval( "tf_bot_npc_archer_shoot_interval", "2", FCVAR_CHEAT ); // 2
+ConVar tf_bot_npc_archer_arrow_damage( "tf_bot_npc_archer_arrow_damage", "75", FCVAR_CHEAT );
+
+
+//-----------------------------------------------------------------
+// The Bot NPC
+//-----------------------------------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( bot_npc_archer, CBotNPCArcher );
+
+PRECACHE_REGISTER( bot_npc_archer );
+
+
+//-----------------------------------------------------------------------------------------------------
+CBotNPCArcher::CBotNPCArcher()
+{
+ ALLOCATE_INTENTION_INTERFACE( CBotNPCArcher );
+
+ m_locomotor = new NextBotGroundLocomotion( this );
+ m_body = new CBotNPCBody( this );
+
+ m_eyeOffset = vec3_origin;
+ m_homePos = vec3_origin;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CBotNPCArcher::~CBotNPCArcher()
+{
+ DEALLOCATE_INTENTION_INTERFACE;
+
+ if ( m_locomotor )
+ delete m_locomotor;
+
+ if ( m_body )
+ delete m_body;
+}
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCArcher::Precache()
+{
+ BaseClass::Precache();
+
+ PrecacheModel( "models/player/sniper.mdl" );
+ PrecacheModel( "models/weapons/c_models/c_bow/c_bow.mdl" );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCArcher::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetModel( "models/player/sniper.mdl" );
+
+ m_bow = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" );
+ if ( m_bow )
+ {
+ m_bow->SetModel( "models/weapons/c_models/c_bow/c_bow.mdl" );
+
+ // bonemerge into our model
+ m_bow->FollowEntity( this, true );
+ }
+
+ int health = tf_bot_npc_archer_health.GetInt();
+ SetHealth( health );
+ SetMaxHealth( health );
+
+ ChangeTeam( TF_TEAM_RED );
+
+ Vector headPos;
+ QAngle headAngles;
+ if ( GetAttachment( "head", headPos, headAngles ) )
+ {
+ m_eyeOffset = headPos - GetAbsOrigin();
+ }
+
+ m_homePos = GetAbsOrigin();
+}
+
+
+//---------------------------------------------------------------------------------------------
+unsigned int CBotNPCArcher::PhysicsSolidMaskForEntity( void ) const
+{
+ // Only collide with the other team
+ int teamContents = ( GetTeamNumber() == TF_TEAM_RED ) ? CONTENTS_BLUETEAM : CONTENTS_REDTEAM;
+
+ return BaseClass::PhysicsSolidMaskForEntity() | teamContents;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CBotNPCArcher::ShouldCollide( int collisionGroup, int contentsMask ) const
+{
+ if ( collisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT )
+ {
+ switch( GetTeamNumber() )
+ {
+ case TF_TEAM_RED:
+ if ( !( contentsMask & CONTENTS_REDTEAM ) )
+ return false;
+ break;
+
+ case TF_TEAM_BLUE:
+ if ( !( contentsMask & CONTENTS_BLUETEAM ) )
+ return false;
+ break;
+ }
+ }
+
+ return BaseClass::ShouldCollide( collisionGroup, contentsMask );
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCArcherSurrender : public Action< CBotNPCArcher >
+{
+public:
+ virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction );
+ virtual const char *GetName( void ) const { return "Surrender"; } // return name of this action
+};
+
+
+inline ActionResult< CBotNPCArcher > CBotNPCArcherSurrender::OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction )
+{
+ CBaseAnimating *bow = me->GetBow();
+ if ( bow )
+ {
+ bow->AddEffects( EF_NODRAW );
+ }
+
+ me->GetBodyInterface()->StartActivity( ACT_MP_STAND_LOSERSTATE );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCArcherShootBow : public Action< CBotNPCArcher >
+{
+public:
+ CBotNPCArcherShootBow( CTFPlayer *target )
+ {
+ m_target = target;
+ }
+
+ virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction );
+ virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval );
+
+ virtual const char *GetName( void ) const { return "ShootBow"; } // return name of this action
+
+private:
+ CHandle< CTFPlayer > m_target;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCArcher > CBotNPCArcherShootBow::OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction )
+{
+ if ( !m_target )
+ {
+ return Done( "No target" );
+ }
+
+ me->GetLocomotionInterface()->FaceTowards( m_target->WorldSpaceCenter() );
+ me->AddGesture( ACT_MP_ATTACK_STAND_ITEM2 );
+
+ // fire arrow
+ const float arrowSpeed = 2000.0f;
+ const float arrowGravity = 0.2f;
+
+ Vector muzzleOrigin;
+ QAngle muzzleAngles;
+ if ( me->GetBow()->GetAttachment( "muzzle", muzzleOrigin, muzzleAngles ) == false )
+ {
+ return Done( "No muzzle attachment!" );
+ }
+
+ // lead target
+ float range = me->GetRangeTo( m_target->EyePosition() );
+ float flightTime = range / arrowSpeed;
+
+ Vector aimSpot = m_target->EyePosition() + m_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( false );
+
+ arrow->SetDamage( tf_bot_npc_archer_arrow_damage.GetFloat() );
+
+ me->EmitSound( "Weapon_CompoundBow.Single" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCArcher > CBotNPCArcherShootBow::Update( CBotNPCArcher *me, float interval )
+{
+ if ( me->IsSequenceFinished() )
+ {
+ return Done();
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCArcherGuardSpot : public Action< CBotNPCArcher >
+{
+public:
+ virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction )
+ {
+ me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM2 );
+
+ return Continue();
+ }
+
+ CTFPlayer *GetVictim( CBotNPCArcher *me )
+ {
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
+
+ CTFPlayer *closeVictim = NULL;
+ float victimRangeSq = FLT_MAX;
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ float rangeSq = me->GetRangeSquaredTo( playerVector[i] );
+ if ( rangeSq < victimRangeSq )
+ {
+ if ( playerVector[i]->m_Shared.IsStealthed() )
+ {
+ continue;
+ }
+
+ if ( me->IsLineOfSightClear( playerVector[i] ) )
+ {
+ closeVictim = playerVector[i];
+ victimRangeSq = rangeSq;
+ }
+ }
+ }
+
+ return closeVictim;
+ }
+
+ virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval )
+ {
+ if ( TFGameRules()->GetActiveBoss() == NULL )
+ {
+ // the Boss has been defeated - give up
+ return ChangeTo( new CBotNPCArcherSurrender, "The Boss is dead! I give up!" );
+ }
+
+ CTFPlayer *victim = GetVictim( me );
+
+ if ( victim )
+ {
+ // look at visible victim out of range
+ me->GetLocomotionInterface()->FaceTowards( victim->WorldSpaceCenter() );
+
+ if ( m_shootTimer.IsElapsed() )
+ {
+ m_shootTimer.Start( tf_bot_npc_archer_shoot_interval.GetFloat() );
+
+ return SuspendFor( new CBotNPCArcherShootBow( victim ), "Fire!" );
+ }
+ }
+
+ if ( me->GetLocomotionInterface()->IsAttemptingToMove() )
+ {
+ // play running animation
+ if ( !me->GetBodyInterface()->IsActivity( ACT_MP_DEPLOYED_IDLE_ITEM2 ) )
+ {
+ me->GetBodyInterface()->StartActivity( ACT_MP_DEPLOYED_IDLE_ITEM2 );
+ }
+ }
+ else
+ {
+ // standing still
+ if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM2 ) )
+ {
+ me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM2 );
+ }
+ }
+
+ return Continue();
+ }
+
+ virtual const char *GetName( void ) const { return "GuardSpot"; } // return name of this action
+
+private:
+ CountdownTimer m_shootTimer;
+};
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCArcherMoveToMark : public Action< CBotNPCArcher >
+{
+public:
+ virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction )
+ {
+ ShortestPathCost cost;
+ m_path.Compute( me, me->GetHomePosition(), cost );
+
+ me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM2 );
+
+ return Continue();
+ }
+
+ virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval )
+ {
+ m_path.Update( me );
+
+ if ( !m_path.IsValid() )
+ {
+ return ChangeTo( new CBotNPCArcherGuardSpot, "Reached my mark" );
+ }
+
+ return Continue();
+ }
+
+ virtual const char *GetName( void ) const { return "MoveToMark"; } // return name of this action
+
+private:
+ PathFollower m_path;
+};
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCArcherBehavior : public Action< CBotNPCArcher >
+{
+public:
+ virtual Action< CBotNPCArcher > *InitialContainedAction( CBotNPCArcher *me )
+ {
+ return new CBotNPCArcherMoveToMark;
+ }
+
+ virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval )
+ {
+ return Continue();
+ }
+
+ virtual EventDesiredResult< CBotNPCArcher > OnKilled( CBotNPCArcher *me, const CTakeDamageInfo &info )
+ {
+ // 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 );
+ }
+
+ me->BecomeRagdoll( info, forceVector );
+
+ return TryDone();
+ }
+
+ virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action
+};
+
+
+IMPLEMENT_INTENTION_INTERFACE( CBotNPCArcher, CBotNPCArcherBehavior );
diff --git a/game/server/tf/bot_npc/bot_npc_archer.h b/game/server/tf/bot_npc/bot_npc_archer.h
new file mode 100644
index 0000000..6b7973b
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_archer.h
@@ -0,0 +1,79 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_archer.h
+// A NextBot non-player derived archer
+// Michael Booth, November 2010
+
+#ifndef BOT_NPC_ARCHER_H
+#define BOT_NPC_ARCHER_H
+
+#include "NextBot.h"
+#include "NextBotBehavior.h"
+#include "NextBotGroundLocomotion.h"
+#include "Path/NextBotPathFollow.h"
+#include "bot_npc.h"
+#include "bot_npc_body.h"
+
+
+class CTFPlayer;
+
+
+//----------------------------------------------------------------------------
+class CBotNPCArcher : public NextBotCombatCharacter
+{
+public:
+ DECLARE_CLASS( CBotNPCArcher, NextBotCombatCharacter );
+
+ CBotNPCArcher();
+ virtual ~CBotNPCArcher();
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ // INextBot
+ DECLARE_INTENTION_INTERFACE( CBotNPCArcher );
+ virtual NextBotGroundLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; }
+ virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; }
+
+ virtual Vector EyePosition( void );
+
+ virtual unsigned int PhysicsSolidMaskForEntity( void ) const;
+ virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const;
+
+ CBaseAnimating *GetBow( void ) const;
+
+ void SetHomePosition( const Vector &pos );
+ const Vector &GetHomePosition( void ) const;
+
+private:
+ NextBotGroundLocomotion *m_locomotor;
+ CBotNPCBody *m_body;
+
+ CBaseAnimating *m_bow;
+ Vector m_eyeOffset;
+
+ Vector m_homePos;
+};
+
+
+inline void CBotNPCArcher::SetHomePosition( const Vector &pos )
+{
+ m_homePos = pos;
+}
+
+inline const Vector &CBotNPCArcher::GetHomePosition( void ) const
+{
+ return m_homePos;
+}
+
+inline Vector CBotNPCArcher::EyePosition( void )
+{
+ return GetAbsOrigin() + m_eyeOffset;
+}
+
+
+inline CBaseAnimating *CBotNPCArcher::GetBow( void ) const
+{
+ return m_bow;
+}
+
+#endif // BOT_NPC_ARCHER_H
diff --git a/game/server/tf/bot_npc/bot_npc_body.cpp b/game/server/tf/bot_npc/bot_npc_body.cpp
new file mode 100644
index 0000000..3d6ce8e
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_body.cpp
@@ -0,0 +1,153 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+#include "cbase.h"
+
+#include "NextBot.h"
+//#include "bot_npc.h"
+#include "bot_npc_body.h"
+
+//-------------------------------------------------------------------------------------------
+CBotNPCBody::CBotNPCBody( INextBot *bot ) : IBody( bot )
+{
+ m_moveXPoseParameter = -1;
+ m_moveYPoseParameter = -1;
+ m_currentActivity = -1;
+ m_desiredAimAngles = vec3_angle;
+}
+
+
+//-------------------------------------------------------------------------------------------
+bool CBotNPCBody::StartActivity( Activity act, unsigned int flags )
+{
+ NextBotCombatCharacter *me = (NextBotCombatCharacter *)GetBot()->GetEntity();
+
+ int animSequence = ::SelectWeightedSequence( me->GetModelPtr(), act, me->GetSequence() );
+
+ if ( animSequence )
+ {
+ m_currentActivity = act;
+ me->SetSequence( animSequence );
+ me->SetPlaybackRate( 1.0f );
+ me->SetCycle( 0 );
+ me->ResetSequenceInfo();
+
+ return true;
+ }
+
+ return false;
+}
+
+
+//-------------------------------------------------------------------------------------------
+void CBotNPCBody::Update( void )
+{
+ NextBotCombatCharacter *me = (NextBotCombatCharacter *)GetBot()->GetEntity();
+
+ if ( m_moveXPoseParameter < 0 )
+ {
+ m_moveXPoseParameter = me->LookupPoseParameter( "move_x" );
+ }
+
+ if ( m_moveYPoseParameter < 0 )
+ {
+ m_moveYPoseParameter = me->LookupPoseParameter( "move_y" );
+ }
+
+
+ // Update the pose parameters
+ float speed = me->GetLocomotionInterface()->GetGroundSpeed(); // me->GetAbsVelocity().Length();
+
+ if ( speed < 0.01f )
+ {
+ // stopped
+ if ( m_moveXPoseParameter >= 0 )
+ {
+ me->SetPoseParameter( m_moveXPoseParameter, 0.0f );
+ }
+
+ if ( m_moveYPoseParameter >= 0 )
+ {
+ me->SetPoseParameter( m_moveYPoseParameter, 0.0f );
+ }
+ }
+ else
+ {
+ Vector forward, right, up;
+ me->GetVectors( &forward, &right, &up );
+
+ const Vector &motionVector = me->GetLocomotionInterface()->GetGroundMotionVector();
+
+ // move_x == 1.0 at full forward motion and -1.0 in full reverse
+ if ( m_moveXPoseParameter >= 0 )
+ {
+ float forwardVel = DotProduct( motionVector, forward );
+
+ me->SetPoseParameter( m_moveXPoseParameter, forwardVel );
+ }
+
+ if ( m_moveYPoseParameter >= 0 )
+ {
+ float sideVel = DotProduct( motionVector, right );
+
+ me->SetPoseParameter( m_moveYPoseParameter, sideVel );
+ }
+ }
+
+ // adjust animation speed to actual movement speed
+ if ( me->m_flGroundSpeed > 0.0f )
+ {
+ // Clamp playback rate to avoid datatable warnings. Anything faster would look silly, anyway.
+ float playbackRate = clamp( speed / me->m_flGroundSpeed, -4.f, 12.f );
+ me->SetPlaybackRate( playbackRate );
+ }
+
+ // move the animation ahead in time
+ me->StudioFrameAdvance();
+ me->DispatchAnimEvents( me );
+
+ // update aim angles
+ QAngle currentAngles = me->GetAbsAngles();
+
+ QAngle angles;
+ const float approachRate = GetMaxHeadAngularVelocity(); // 3000.0f;
+ angles.y = ApproachAngle( m_desiredAimAngles.y, currentAngles.y, approachRate * TICK_INTERVAL );
+ angles.x = ApproachAngle( m_desiredAimAngles.x, currentAngles.x, 0.5f * approachRate * TICK_INTERVAL );
+ angles.z = 0.0f;
+
+ angles.x = AngleNormalize( angles.x );
+ angles.y = AngleNormalize( angles.y );
+
+ me->SetAbsAngles( angles );
+}
+
+
+//---------------------------------------------------------------------------------------------
+// return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface)
+unsigned int CBotNPCBody::GetSolidMask( void ) const
+{
+ return MASK_NPCSOLID | CONTENTS_PLAYERCLIP;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason )
+{
+ CBaseCombatCharacter *me = GetBot()->GetEntity();
+
+ Vector toTarget = lookAtPos - me->WorldSpaceCenter();
+ VectorAngles( toTarget, m_desiredAimAngles );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason )
+{
+ if ( !subject )
+ return;
+
+ CBaseCombatCharacter *me = GetBot()->GetEntity();
+
+ Vector toTarget = subject->WorldSpaceCenter() - me->WorldSpaceCenter();
+
+ QAngle angles;
+ VectorAngles( toTarget, m_desiredAimAngles );
+}
diff --git a/game/server/tf/bot_npc/bot_npc_body.h b/game/server/tf/bot_npc/bot_npc_body.h
new file mode 100644
index 0000000..3d58bee
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_body.h
@@ -0,0 +1,64 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+#ifndef BOT_NPC_BODY_H
+#define BOT_NPC_BODY_H
+
+#include "animation.h"
+#include "NextBotBodyInterface.h"
+
+class INextBot;
+
+
+//----------------------------------------------------------------------------------------------------------------
+/**
+ * The interface for control and information about the bot's body state (posture, animation state, etc)
+ */
+class CBotNPCBody : public IBody
+{
+public:
+ CBotNPCBody( INextBot *bot );
+ virtual ~CBotNPCBody() { }
+
+ virtual void Update( void );
+
+ virtual bool StartActivity( Activity act, unsigned int flags = 0 );
+ virtual Activity GetActivity( void ) const; // return currently animating activity
+ virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one
+
+ virtual void AimHeadTowards( const Vector &lookAtPos,
+ LookAtPriorityType priority = BORING,
+ float duration = 0.0f,
+ INextBotReply *replyWhenAimed = NULL,
+ const char *reason = NULL ); // aim the bot's head towards the given goal
+ virtual void AimHeadTowards( CBaseEntity *subject,
+ LookAtPriorityType priority = BORING,
+ float duration = 0.0f,
+ INextBotReply *replyWhenAimed = NULL,
+ const char *reason = NULL ); // continually aim the bot's head towards the given subject
+
+ virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface)
+ virtual unsigned int GetCollisionGroup( void ) const;
+
+private:
+ int m_currentActivity;
+ int m_moveXPoseParameter;
+ int m_moveYPoseParameter;
+ QAngle m_desiredAimAngles;
+};
+
+
+inline Activity CBotNPCBody::GetActivity( void ) const
+{
+ return (Activity)m_currentActivity;
+}
+
+inline bool CBotNPCBody::IsActivity( Activity act ) const
+{
+ return act == m_currentActivity ? true : false;
+}
+
+inline unsigned int CBotNPCBody::GetCollisionGroup( void ) const
+{
+ return COLLISION_GROUP_PLAYER_MOVEMENT;
+}
+
+#endif // BOT_NPC_BODY_H
diff --git a/game/server/tf/bot_npc/bot_npc_decoy.cpp b/game/server/tf/bot_npc/bot_npc_decoy.cpp
new file mode 100644
index 0000000..757a467
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_decoy.cpp
@@ -0,0 +1,246 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_decoy.cpp
+// A NextBot non-player decoy that imitates a real player
+// Michael Booth, January 2011
+
+#include "cbase.h"
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_team.h"
+#include "nav_mesh/tf_nav_area.h"
+#include "bot_npc_decoy.h"
+#include "econ_wearable.h"
+
+LINK_ENTITY_TO_CLASS( bot_npc_decoy, CBotNPCDecoy );
+PRECACHE_REGISTER( bot_npc_decoy );
+
+ConVar tf_decoy_lifetime( "tf_decoy_lifetime", "5", FCVAR_CHEAT, "The lifetime of a decoy, in seconds" );
+
+
+//-----------------------------------------------------------------------------------------------------
+CBotNPCDecoy::CBotNPCDecoy()
+{
+ ALLOCATE_INTENTION_INTERFACE( CBotNPCDecoy );
+
+ m_locomotor = new CBotNPCDecoyLocomotion( this );
+ m_body = new CBotNPCBody( this );
+
+ m_eyeOffset = vec3_origin;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CBotNPCDecoy::~CBotNPCDecoy()
+{
+ DEALLOCATE_INTENTION_INTERFACE;
+
+ if ( m_locomotor )
+ delete m_locomotor;
+
+ if ( m_body )
+ delete m_body;
+}
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCDecoy::Precache()
+{
+ BaseClass::Precache();
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCDecoy::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetCollisionGroup( COLLISION_GROUP_NONE );
+ SetSolid( SOLID_NONE );
+ AddSolidFlags( FSOLID_NOT_SOLID );
+
+ CTFPlayer *owner = ToTFPlayer( GetOwnerEntity() );
+ if ( !owner )
+ {
+ Warning( "Decoy spawned without an owner\n" );
+ return;
+ }
+
+ int ownerClass = owner->m_Shared.InCond( TF_COND_DISGUISED ) ? owner->m_Shared.GetDisguiseClass() : owner->GetPlayerClass()->GetClassIndex();
+ int ownerTeam = owner->m_Shared.InCond( TF_COND_DISGUISED ) ? owner->m_Shared.GetDisguiseTeam() : owner->GetTeamNumber();
+
+ SetModel( GetPlayerClassData( ownerClass )->m_szModelName );
+ ChangeTeam( ownerTeam );
+
+ if ( ownerTeam == TF_TEAM_BLUE )
+ {
+ m_nSkin = 1;
+ }
+ else
+ {
+ m_nSkin = 0;
+ }
+
+ SetAbsOrigin( owner->GetAbsOrigin() );
+ SetAbsAngles( owner->GetAbsAngles() );
+ SetAbsVelocity( owner->GetAbsVelocity() );
+
+ Vector headPos;
+ QAngle headAngles;
+ if ( GetAttachment( "head", headPos, headAngles ) )
+ {
+ m_eyeOffset = headPos - GetAbsOrigin();
+ }
+
+ CTFWeaponBase *theirWeapon = owner->m_Shared.GetDisguiseWeapon();
+ if ( !theirWeapon )
+ {
+ theirWeapon = owner->GetActiveTFWeapon();
+ }
+
+ if ( theirWeapon )
+ {
+ CBaseAnimating *weapon = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" );
+ if ( weapon )
+ {
+ weapon->SetModel( theirWeapon->GetWorldModel() );
+
+ // bonemerge the weapon into our model
+ weapon->FollowEntity( this, true );
+
+ // choose the appropriate run animation for this weapon
+ switch( theirWeapon->GetTFWpnData().m_iWeaponType )
+ {
+ case TF_WPN_TYPE_PRIMARY:
+ m_runActivity = ACT_MP_RUN_PRIMARY;
+ break;
+
+ case TF_WPN_TYPE_SECONDARY:
+ m_runActivity = ACT_MP_RUN_SECONDARY;
+ break;
+
+ case TF_WPN_TYPE_MELEE:
+ default:
+ m_runActivity = ACT_MP_RUN_MELEE;
+ break;
+ }
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+unsigned int CBotNPCDecoy::PhysicsSolidMaskForEntity( void ) const
+{
+ // Only collide with the other team
+ int teamContents = ( GetTeamNumber() == TF_TEAM_RED ) ? CONTENTS_BLUETEAM : CONTENTS_REDTEAM;
+
+ return BaseClass::PhysicsSolidMaskForEntity() | teamContents;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CBotNPCDecoy::ShouldCollide( int collisionGroup, int contentsMask ) const
+{
+ if ( collisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT )
+ {
+ switch( GetTeamNumber() )
+ {
+ case TF_TEAM_RED:
+ if ( !( contentsMask & CONTENTS_REDTEAM ) )
+ return false;
+ break;
+
+ case TF_TEAM_BLUE:
+ if ( !( contentsMask & CONTENTS_BLUETEAM ) )
+ return false;
+ break;
+ }
+ }
+
+ return BaseClass::ShouldCollide( collisionGroup, contentsMask );
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+float CBotNPCDecoyLocomotion::GetRunSpeed( void ) const
+{
+ CTFPlayer *owner = ToTFPlayer( GetBot()->GetEntity()->GetOwnerEntity() );
+ if ( !owner )
+ {
+ return 0.0f;
+ }
+
+ int ownerClass = owner->m_Shared.InCond( TF_COND_DISGUISED ) ? owner->m_Shared.GetDisguiseClass() : owner->GetPlayerClass()->GetClassIndex();
+ return GetPlayerClassData( ownerClass )->m_flMaxSpeed;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// return maximum acceleration of locomotor
+float CBotNPCDecoyLocomotion::GetMaxAcceleration( void ) const
+{
+ return 1500.0f;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// return maximum deceleration of locomotor
+float CBotNPCDecoyLocomotion::GetMaxDeceleration( void ) const
+{
+ return 1500.0f;
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCDecoyBehavior : public Action< CBotNPCDecoy >
+{
+public:
+ virtual ActionResult< CBotNPCDecoy > OnStart( CBotNPCDecoy *me, Action< CBotNPCDecoy > *priorAction )
+ {
+ m_timer.Start( tf_decoy_lifetime.GetFloat() );
+
+ // play running animation
+ if ( !me->GetBodyInterface()->IsActivity( me->GetRunActivity() ) )
+ {
+ me->GetBodyInterface()->StartActivity( me->GetRunActivity() );
+ }
+
+ return Continue();
+ }
+
+ virtual ActionResult< CBotNPCDecoy > Update( CBotNPCDecoy *me, float interval )
+ {
+ if ( m_timer.IsElapsed() )
+ {
+ // we're out of time
+ UTIL_Remove( me );
+ return Done( "Lifetime expired" );
+ }
+
+ CTFPlayer *owner = ToTFPlayer( me->GetOwnerEntity() );
+ if ( !owner )
+ {
+ UTIL_Remove( me );
+ return Done( "No owner!" );
+ }
+
+ Vector forward;
+ me->GetVectors( &forward, NULL, NULL );
+
+ me->GetLocomotionInterface()->SetDesiredSpeed( FLT_MAX ); // this is just a rate limiter
+ me->GetLocomotionInterface()->Run();
+ me->GetLocomotionInterface()->Approach( me->GetAbsOrigin() + 100.0f * forward );
+
+ return Continue();
+ }
+
+ virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action
+
+private:
+ CountdownTimer m_timer;
+};
+
+
+IMPLEMENT_INTENTION_INTERFACE( CBotNPCDecoy, CBotNPCDecoyBehavior );
+
diff --git a/game/server/tf/bot_npc/bot_npc_decoy.h b/game/server/tf/bot_npc/bot_npc_decoy.h
new file mode 100644
index 0000000..6c6227f
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_decoy.h
@@ -0,0 +1,80 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_decoy.h
+// A NextBot non-player decoy that imitates a real player
+// Michael Booth, January 2011
+
+#ifndef BOT_NPC_DECOY_H
+#define BOT_NPC_DECOY_H
+
+#include "NextBot.h"
+#include "NextBotBehavior.h"
+#include "NextBotGroundLocomotion.h"
+#include "Path/NextBotPathFollow.h"
+#include "bot_npc.h"
+#include "bot_npc_body.h"
+
+
+class CTFPlayer;
+
+
+//----------------------------------------------------------------------------
+class CBotNPCDecoyLocomotion : public NextBotGroundLocomotion
+{
+public:
+ DECLARE_CLASS( CBotNPCDecoyLocomotion, NextBotGroundLocomotion );
+
+ CBotNPCDecoyLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { }
+ virtual ~CBotNPCDecoyLocomotion() { }
+
+ virtual float GetRunSpeed( void ) const; // get maximum running speed
+
+ virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor
+ virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor
+};
+
+
+//----------------------------------------------------------------------------
+class CBotNPCDecoy : public NextBotCombatCharacter
+{
+public:
+ DECLARE_CLASS( CBotNPCDecoy, NextBotCombatCharacter );
+
+ CBotNPCDecoy();
+ virtual ~CBotNPCDecoy();
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ // INextBot
+ DECLARE_INTENTION_INTERFACE( CBotNPCDecoy );
+ virtual CBotNPCDecoyLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; }
+ virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; }
+
+ virtual Vector EyePosition( void );
+
+ virtual unsigned int PhysicsSolidMaskForEntity( void ) const;
+ virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const;
+
+ Activity GetRunActivity( void ) const;
+
+private:
+ CBotNPCDecoyLocomotion *m_locomotor;
+ CBotNPCBody *m_body;
+
+ Vector m_eyeOffset;
+ Activity m_runActivity;
+};
+
+
+inline Activity CBotNPCDecoy::GetRunActivity( void ) const
+{
+ return m_runActivity;
+}
+
+
+inline Vector CBotNPCDecoy::EyePosition( void )
+{
+ return GetAbsOrigin() + m_eyeOffset;
+}
+
+#endif // BOT_NPC_DECOY_H
diff --git a/game/server/tf/bot_npc/bot_npc_mini.cpp b/game/server/tf/bot_npc/bot_npc_mini.cpp
new file mode 100644
index 0000000..a802721
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_mini.cpp
@@ -0,0 +1,101 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_mini.cpp
+// A NextBot non-player derived actor
+// Michael Booth, March 2011
+
+#include "cbase.h"
+
+#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_mini.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"
+
+
+extern ConVar tf_bot_npc_reaction_time;
+extern ConVar tf_bot_npc_grenade_interval;
+extern float ModifyBossDamage( const CTakeDamageInfo &info );
+
+
+ConVar tf_raid_mini_rocket_boss_health( "tf_raid_mini_rocket_boss_health", "5000", 0/*FCVAR_CHEAT*/ );
+
+
+//-----------------------------------------------------------------------------------------------------
+// The Bot NPC mini-boss
+//-----------------------------------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( bot_boss_mini_rockets, CBotNPCMiniRockets );
+PRECACHE_REGISTER( bot_boss_mini_rockets );
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCMiniRockets::Precache()
+{
+ BaseClass::Precache();
+
+ int model = PrecacheModel( "models/bots/knight/knight_mini.mdl" );
+ PrecacheGibsForModel( model );
+
+ PrecacheScriptSound( "RobotMiniBoss.LaunchRocket" );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCMiniRockets::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetModel( "models/bots/knight/knight_mini.mdl" );
+
+ int health = tf_raid_mini_rocket_boss_health.GetInt();
+ SetHealth( health );
+ SetMaxHealth( health );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+// The Bot NPC mini-boss
+//-----------------------------------------------------------------------------------------------------
+LINK_ENTITY_TO_CLASS( bot_boss_mini_nuker, CBotNPCMiniNuker );
+PRECACHE_REGISTER( bot_boss_mini_nuker );
+
+ConVar tf_raid_mini_nuker_boss_health( "tf_raid_mini_nuker_boss_health", "5000", 0/*FCVAR_CHEAT*/ );
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCMiniNuker::Precache()
+{
+ BaseClass::Precache();
+
+ int model = PrecacheModel( "models/bots/knight/knight_mini.mdl" );
+ PrecacheGibsForModel( model );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCMiniNuker::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetModel( "models/bots/knight/knight_mini.mdl" );
+
+ int health = tf_raid_mini_nuker_boss_health.GetInt();
+ SetHealth( health );
+ SetMaxHealth( health );
+}
+
+#endif // TF_RAID_MODE
diff --git a/game/server/tf/bot_npc/bot_npc_mini.h b/game/server/tf/bot_npc/bot_npc_mini.h
new file mode 100644
index 0000000..f2f95b8
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_mini.h
@@ -0,0 +1,83 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_mini.h
+// A NextBot non-player derived actor
+// Michael Booth, March 2011
+
+#ifndef BOT_NPC_MINI_H
+#define BOT_NPC_MINI_H
+
+#ifdef TF_RAID_MODE
+
+#include "NextBot.h"
+#include "NextBotBehavior.h"
+#include "NextBotGroundLocomotion.h"
+#include "Path/NextBotPathFollow.h"
+#include "bot_npc_body.h"
+#include "bot/map_entities/tf_spawner_boss.h"
+#include "player_vs_environment/boss_alpha/boss_alpha.h"
+
+
+//----------------------------------------------------------------------------
+class CBotNPCMiniRockets : public CBossAlpha
+{
+public:
+ DECLARE_CLASS( CBotNPCMiniRockets, CBossAlpha );
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ virtual bool HasAbility( Ability ability ) const;
+
+ virtual bool IsMiniBoss( void ) const { return true; }
+
+ virtual float GetMoveSpeed( void ) const { return 150.0f; }
+
+ virtual int GetRocketLaunchCount( void ) const { return 3; }
+ virtual float GetRocketDamage( void ) const { return 25.0f; }
+ virtual float GetRocketAimError( void ) const { return 3.0f; }
+ virtual float GetRocketInterval( void ) const { return 0.5f; }
+ virtual const char *GetRocketSoundEffect( void ) const { return "RobotMiniBoss.LaunchRocket"; }
+
+ virtual float GetBecomeStunnedDamage( void ) const { return 300.0f; }
+};
+
+inline bool CBotNPCMiniRockets::HasAbility( Ability ability ) const
+{
+ const int myAbilities = CAN_BE_STUNNED | CAN_FIRE_ROCKETS;
+
+ return myAbilities & ability ? true : false;
+}
+
+
+//----------------------------------------------------------------------------
+class CBotNPCMiniNuker : public CBossAlpha
+{
+public:
+ DECLARE_CLASS( CBotNPCMiniNuker, CBossAlpha );
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ virtual bool HasAbility( Ability ability ) const;
+
+ virtual bool IsMiniBoss( void ) const { return true; }
+
+ virtual float GetMoveSpeed( void ) const { return 150.0f; }
+
+ virtual float GetGrenadeInterval( void ) const { return 2.0f; }
+
+ virtual float GetBecomeStunnedDamage( void ) const { return 300.0f; }
+};
+
+inline bool CBotNPCMiniNuker::HasAbility( Ability ability ) const
+{
+ const int myAbilities = CAN_BE_STUNNED | CAN_NUKE | CAN_LAUNCH_STICKIES;
+
+ return myAbilities & ability ? true : false;
+}
+
+
+
+#endif // TF_RAID_MODE
+
+#endif // BOT_NPC_MINI_H
diff --git a/game/server/tf/bot_npc/bot_npc_minion.cpp b/game/server/tf/bot_npc/bot_npc_minion.cpp
new file mode 100644
index 0000000..59f04a0
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_minion.cpp
@@ -0,0 +1,1239 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_minion.cpp
+// Minions for the Boss
+// Michael Booth, November 2010
+
+#include "cbase.h"
+
+#ifdef TF_RAID_MODE
+
+#include "tf_player.h"
+#include "tf_gamerules.h"
+#include "tf_team.h"
+#include "tf_projectile_arrow.h"
+#include "tf_weapon_grenade_pipebomb.h"
+#include "tf_ammo_pack.h"
+#include "nav_mesh/tf_nav_area.h"
+#include "bot_npc_minion.h"
+#include "NextBot/Path/NextBotChasePath.h"
+#include "econ_wearable.h"
+#include "team_control_point_master.h"
+#include "particle_parse.h"
+#include "nav_mesh/tf_path_follower.h"
+#include "tf_obj_sentrygun.h"
+#include "bot/map_entities/tf_spawner.h"
+
+#define MINION_LIGHT_ON 0
+#define MINION_LIGHT_OFF 1
+
+ConVar tf_bot_npc_minion_health( "tf_bot_npc_minion_health", "60"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_speed( "tf_bot_npc_minion_speed", "250"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_acceleration( "tf_bot_npc_minion_acceleration", "500"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_horiz_damping( "tf_bot_npc_minion_horiz_damping", "2"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_vert_damping( "tf_bot_npc_minion_vert_damping", "1"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_stun_charge( "tf_bot_npc_minion_stun_charge", "0.4"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_ammo_count( "tf_bot_npc_minion_ammo_count", "100"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_debug( "tf_bot_npc_minion_debug", "0"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_notice_threat_range( "tf_bot_npc_minion_notice_threat_range", "500"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_stun_range( "tf_bot_npc_minion_stun_range", "100"/*,FCVAR_CHEAT */ );
+ConVar tf_bot_npc_minion_stun_charge_up_time( "tf_bot_npc_minion_stun_charge_up_time", "1.5"/*,FCVAR_CHEAT */ );
+ConVar tf_bot_npc_minion_stun_kill_time( "tf_bot_npc_minion_stun_kill_time", "7"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_invuln_duration( "tf_bot_npc_minion_invuln_duration", "3"/*, FCVAR_CHEAT*/ );
+
+
+
+LINK_ENTITY_TO_CLASS( bot_npc_minion, CBotNPCMinion );
+
+PRECACHE_REGISTER( bot_npc_minion );
+
+IMPLEMENT_SERVERCLASS_ST( CBotNPCMinion, DT_BotNPCMinion )
+
+ SendPropEHandle( SENDINFO( m_stunTarget ) ),
+
+END_SEND_TABLE()
+
+
+//-----------------------------------------------------------------------------------------------------
+class CTFSpawnTemplateStunDrone : public CTFSpawnTemplate
+{
+public:
+ virtual CBaseEntity *Instantiate( void ) const
+ {
+ return CreateEntityByName( "bot_npc_minion" );
+ }
+};
+
+LINK_ENTITY_TO_CLASS( tf_template_stun_drone, CTFSpawnTemplateStunDrone );
+
+
+
+
+//-----------------------------------------------------------------------------------------------------
+//-----------------------------------------------------------------------------------------------------
+CBotNPCMinion::CBotNPCMinion()
+{
+ ALLOCATE_INTENTION_INTERFACE( CBotNPCMinion );
+
+ m_locomotor = new CNextBotFlyingLocomotion( this );
+ m_body = new CBotNPCBody( this );
+ m_vision = new CDisableVision( this );
+
+ m_eyeOffset = vec3_origin;
+ m_target = NULL;
+ m_stunTarget = NULL;
+ m_isAlert = false;
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+CBotNPCMinion::~CBotNPCMinion()
+{
+ DEALLOCATE_INTENTION_INTERFACE;
+
+ if ( m_vision )
+ delete m_vision;
+
+ if ( m_locomotor )
+ delete m_locomotor;
+
+ if ( m_body )
+ delete m_body;
+}
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCMinion::Precache()
+{
+ BaseClass::Precache();
+
+ PrecacheModel( "models/props_swamp/bug_zapper.mdl" );
+// PrecacheModel( "models/combine_helicopter/helicopter_bomb01.mdl" );
+// PrecacheModel( "models/props_lights/spotlight001a.mdl" );
+
+ PrecacheScriptSound( "Minion.Ping.Roam" );
+ PrecacheScriptSound( "Minion.Ping.Acquire" );
+ PrecacheScriptSound( "Minion.Bounce" );
+// PrecacheScriptSound( "Minion.Explode" );
+ PrecacheScriptSound( "Minion.ChargeUpStun" );
+ PrecacheScriptSound( "Minion.Stun" );
+ PrecacheScriptSound( "Minion.StunKill" );
+ PrecacheScriptSound( "Minion.Building" );
+ PrecacheScriptSound( "Minion.Recognize" );
+ PrecacheScriptSound( "Minion.Notice" );
+
+ PrecacheParticleSystem( "cart_flashinglight" );
+}
+
+
+//-----------------------------------------------------------------------------------------------------
+void CBotNPCMinion::Spawn( void )
+{
+ BaseClass::Spawn();
+
+ SetModel( "models/props_swamp/bug_zapper.mdl" );
+
+ int health = tf_bot_npc_minion_health.GetInt();
+ SetHealth( health );
+ SetMaxHealth( health );
+
+ ChangeTeam( TF_TEAM_RED );
+
+ // this flag lets flamethrowers deflect me
+ AddFlag( FL_GRENADE );
+
+ m_eyeOffset = Vector( 0, 0, 20.0f );
+ m_lastKnownTargetPosition = vec3_origin;
+ m_isAlert = false;
+
+ m_invulnTimer.Start( tf_bot_npc_minion_invuln_duration.GetFloat() );
+}
+
+
+//---------------------------------------------------------------------------------------------
+ConVar tf_bot_npc_minion_dmg_mult_sentry( "tf_bot_npc_minion_dmg_mult_sentry", "0.5"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_dmg_mult_sniper( "tf_bot_npc_minion_dmg_mult_sniper", "2"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_dmg_mult_arrow( "tf_bot_npc_minion_dmg_mult_arrow", "3"/*, FCVAR_CHEAT*/ );
+
+float MinionModifyDamage( const CTakeDamageInfo &info )
+{
+ CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() );
+ CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
+
+ if ( sentry )
+ {
+ return info.GetDamage() * tf_bot_npc_minion_dmg_mult_sentry.GetFloat();
+ }
+ else if ( pWeapon )
+ {
+ switch( pWeapon->GetWeaponID() )
+ {
+ case TF_WEAPON_SNIPERRIFLE:
+ case TF_WEAPON_SNIPERRIFLE_DECAP:
+ case TF_WEAPON_SNIPERRIFLE_CLASSIC:
+ return info.GetDamage() * tf_bot_npc_minion_dmg_mult_sniper.GetFloat();
+
+ case TF_WEAPON_COMPOUND_BOW:
+ return info.GetDamage() * tf_bot_npc_minion_dmg_mult_arrow.GetFloat();
+ }
+ }
+
+ // unmodified
+ return info.GetDamage();
+}
+
+
+//---------------------------------------------------------------------------------------------
+int CBotNPCMinion::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo )
+{
+ if ( !m_invulnTimer.IsElapsed() )
+ {
+ // invulnerable for a moment after spawning
+ return 0;
+ }
+
+ CTakeDamageInfo info = rawInfo;
+
+ info.SetDamage( MinionModifyDamage( info ) );
+
+ BecomeAlert();
+
+ // 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 );
+ }
+
+ return BaseClass::OnTakeDamage_Alive( info );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinion::Deflected( CBaseEntity *pDeflectedBy, Vector &vecDir )
+{
+ BecomeAlert();
+
+ if ( pDeflectedBy )
+ {
+ GetLocomotionInterface()->Deflect( pDeflectedBy );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+unsigned int CBotNPCMinion::PhysicsSolidMaskForEntity( void ) const
+{
+ // Only collide with the other team
+ int teamContents = ( GetTeamNumber() == TF_TEAM_RED ) ? CONTENTS_BLUETEAM : CONTENTS_REDTEAM;
+
+ return BaseClass::PhysicsSolidMaskForEntity() | teamContents;
+}
+
+
+//---------------------------------------------------------------------------------------------
+bool CBotNPCMinion::ShouldCollide( int collisionGroup, int contentsMask ) const
+{
+ if ( collisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT )
+ {
+ switch( GetTeamNumber() )
+ {
+ case TF_TEAM_RED:
+ if ( !( contentsMask & CONTENTS_REDTEAM ) )
+ return false;
+ break;
+
+ case TF_TEAM_BLUE:
+ if ( !( contentsMask & CONTENTS_BLUETEAM ) )
+ return false;
+ break;
+ }
+ }
+
+ return BaseClass::ShouldCollide( collisionGroup, contentsMask );
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinion::BecomeAmmoPack( void )
+{
+ int iPrimary = tf_bot_npc_minion_ammo_count.GetInt();
+ int iSecondary = tf_bot_npc_minion_ammo_count.GetInt();
+ int iMetal = tf_bot_npc_minion_ammo_count.GetInt();
+
+ // Create the ammo pack.
+ //CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( GetAbsOrigin(), GetAbsAngles(), NULL, "models/props_swamp/bug_zapper.mdl" );
+ CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( GetAbsOrigin(), GetAbsAngles(), NULL, "models/items/ammopack_medium.mdl" );
+ if ( pAmmoPack )
+ {
+ Vector vel = GetAbsVelocity();
+ pAmmoPack->SetInitialVelocity( vel );
+ pAmmoPack->m_nSkin = MINION_LIGHT_OFF;
+
+ // 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 );
+
+ 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 );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinion::BecomeAlert( void )
+{
+ m_isAlert = true;
+ m_nSkin = MINION_LIGHT_ON;
+}
+
+
+//---------------------------------------------------------------------------------------------
+//
+// Find the closest living player not already being targeted by another minion
+//
+CTFPlayer *CBotNPCMinion::FindTarget( void )
+{
+ CUtlVector< CBotNPCMinion * > minionVector;
+ CBotNPCMinion *minion = NULL;
+ while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL )
+ {
+ minionVector.AddToTail( minion );
+ }
+
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
+
+ CTFPlayer *closeVictim = NULL;
+ float victimRangeSq = FLT_MAX;
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ if ( !playerVector[i]->IsAlive() )
+ continue;
+
+ float rangeSq = GetRangeSquaredTo( playerVector[i] );
+ if ( rangeSq < victimRangeSq )
+ {
+ if ( playerVector[i]->m_Shared.IsStealthed() )
+ {
+ continue;
+ }
+
+ // is any other minion targeting this player?
+ bool isTargetAvailable = true;
+ for( int m=0; m<minionVector.Count(); ++m )
+ {
+ CBotNPCMinion *peer = minionVector[m];
+
+ if ( peer != this && peer->HasTarget() && peer->GetTarget() == playerVector[i] )
+ {
+ isTargetAvailable = false;
+ break;
+ }
+ }
+
+ if ( isTargetAvailable )
+ {
+ closeVictim = playerVector[i];
+ victimRangeSq = rangeSq;
+ }
+ }
+ }
+
+ return closeVictim;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinion::UpdateTarget( void )
+{
+ CUtlVector< CTFPlayer * > playerVector;
+ CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
+
+ CTFPlayer *closestPlayer = NULL;
+ float closestRangeSq = IsAlert() ? FLT_MAX : ( tf_bot_npc_minion_notice_threat_range.GetFloat() * tf_bot_npc_minion_notice_threat_range.GetFloat() );
+
+ for( int i=0; i<playerVector.Count(); ++i )
+ {
+ CTFPlayer *player = playerVector[i];
+
+ float rangeSq = ( player->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr();
+ if ( rangeSq < closestRangeSq )
+ {
+ if ( IsLineOfSightClear( player, IGNORE_ACTORS ) )
+ {
+ closestPlayer = player;
+ closestRangeSq = rangeSq;
+ }
+ }
+ }
+
+ if ( closestPlayer )
+ {
+ if ( closestPlayer != GetTarget() )
+ {
+ EmitSound( "Minion.Notice" );
+ SetTarget( closestPlayer );
+ }
+
+ m_lastKnownTargetPosition = closestPlayer->GetAbsOrigin();
+ }
+ else
+ {
+ SetTarget( NULL );
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionHoldStunVictim : public Action< CBotNPCMinion >
+{
+public:
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction );
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+ virtual void OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction );
+
+ virtual const char *GetName( void ) const { return "HoldStunVictim"; } // return name of this action
+
+private:
+ CTFPathFollower m_path;
+ CountdownTimer m_restunTimer;
+ CountdownTimer m_killTimer;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionHoldStunVictim::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+{
+ CTFPlayer *target = me->GetTarget();
+
+ if ( !target )
+ {
+ return Done( "No target" );
+ }
+
+ me->StartStunEffects( target );
+
+ me->EmitSound( "Minion.Stun" );
+
+ m_restunTimer.Invalidate();
+ m_killTimer.Start( tf_bot_npc_minion_stun_kill_time.GetFloat() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionHoldStunVictim::Update( CBotNPCMinion *me, float interval )
+{
+ CTFPlayer *target = me->GetTarget();
+
+ if ( !target )
+ {
+ return Done( "No target" );
+ }
+
+ me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );
+
+ if ( me->IsRangeGreaterThan( target, tf_bot_npc_minion_stun_range.GetFloat() ) )
+ {
+ return Done( "Target out of stun range" );
+ }
+
+ // if we've held them long enough, they die
+ if ( m_killTimer.IsElapsed() )
+ {
+ me->EmitSound( "Minion.StunKill" );
+
+ CTakeDamageInfo info( me, me, 1000.0f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE );
+
+ Vector toTarget = target->WorldSpaceCenter() - me->WorldSpaceCenter();
+ toTarget.z = 0.5f;
+ toTarget.NormalizeInPlace();
+
+ CalculateMeleeDamageForce( &info, toTarget, me->WorldSpaceCenter(), 1.0f );
+ target->TakeDamage( info );
+
+ return Done( "My work here is done." );
+ }
+
+ // stun them!
+ // if they are ubered we overload and explode from the energy feedback
+ if ( target->m_Shared.InCond( TF_COND_INVULNERABLE ) )
+ {
+ CTakeDamageInfo info( target, target, me->GetMaxHealth() + 10.0f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE );
+
+ Vector fromVictim = me->WorldSpaceCenter() - target->WorldSpaceCenter();
+ fromVictim.NormalizeInPlace();
+
+ CalculateMeleeDamageForce( &info, fromVictim, me->WorldSpaceCenter(), 1.0f );
+ me->TakeDamage( info );
+
+ return Done( "Destroyed by target's Uber" );
+ }
+
+ // periodically restart the stun effect
+ if ( m_restunTimer.IsElapsed() )
+ {
+ const float stunTime = 1.0f;
+ const float speedReduction = 0.5f;
+
+ int stunFlags = TF_STUN_LOSER_STATE | TF_STUN_MOVEMENT | TF_STUN_CONTROLS;
+ target->m_Shared.StunPlayer( stunTime + 0.5f, speedReduction, stunFlags, NULL );
+ m_restunTimer.Start( stunTime );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinionHoldStunVictim::OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction )
+{
+ me->EndStunEffects();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionStartStunAttack : public Action< CBotNPCMinion >
+{
+public:
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction );
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+
+ virtual const char *GetName( void ) const { return "StartStunAttack"; } // return name of this action
+
+private:
+ CTFPathFollower m_path;
+ CountdownTimer m_stunTimer;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionStartStunAttack::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+{
+ // light on
+ me->m_nSkin = MINION_LIGHT_ON;
+
+ me->EmitSound( "Minion.ChargeUpStun" );
+ m_stunTimer.Start( tf_bot_npc_minion_stun_charge_up_time.GetFloat() );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionStartStunAttack::Update( CBotNPCMinion *me, float interval )
+{
+ CTFPlayer *target = me->GetTarget();
+
+ if ( !target )
+ {
+ return Done( "No target" );
+ }
+
+ me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );
+
+ if ( me->IsRangeGreaterThan( target, tf_bot_npc_minion_stun_range.GetFloat() ) )
+ {
+ return Done( "Target out of stun range" );
+ }
+
+ if ( m_stunTimer.IsElapsed() )
+ {
+ // stun 'em
+ return ChangeTo( new CBotNPCMinionHoldStunVictim, "Stun successful" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionReadyToStun : public Action< CBotNPCMinion >
+{
+public:
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+
+ virtual const char *GetName( void ) const { return "ReadyToStun"; } // return name of this action
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionReadyToStun::Update( CBotNPCMinion *me, float interval )
+{
+ CTFPlayer *target = me->GetTarget();
+
+ if ( target )
+ {
+ me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() );
+
+ if ( me->IsRangeLessThan( target, tf_bot_npc_minion_stun_range.GetFloat() ) )
+ {
+ return SuspendFor( new CBotNPCMinionStartStunAttack, "Charging stun..." );
+ }
+ }
+
+ return Continue();
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionApproachTarget : public Action< CBotNPCMinion >
+{
+public:
+ virtual Action< CBotNPCMinion > *InitialContainedAction( CBotNPCMinion *me );
+
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction );
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+ virtual void OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction );
+
+ virtual const char *GetName( void ) const { return "ApproachTarget"; } // return name of this action
+
+private:
+ CTFPathFollower m_path;
+ CountdownTimer m_initialStunTimer;
+ CountdownTimer m_chooseTargetTimer;
+ CountdownTimer m_pingTimer;
+};
+
+
+//---------------------------------------------------------------------------------------------
+Action< CBotNPCMinion > *CBotNPCMinionApproachTarget::InitialContainedAction( CBotNPCMinion *me )
+{
+ return new CBotNPCMinionReadyToStun;
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionApproachTarget::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+{
+ if ( !me->HasTarget() )
+ {
+ return Done( "No initial target" );
+ }
+
+ m_pingTimer.Start( 1.0f );
+
+ // light on
+ me->m_nSkin = MINION_LIGHT_ON;
+
+ me->EmitSound( "Minion.Recognize" );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionApproachTarget::Update( CBotNPCMinion *me, float interval )
+{
+ if ( !me->HasTarget() )
+ {
+ me->GetLocomotionInterface()->FaceTowards( me->GetLastKnownTargetPosition() );
+
+ if ( ( me->GetAbsOrigin() - me->GetLastKnownTargetPosition() ).AsVector2D().IsLengthLessThan( 20.0f ) )
+ {
+ // reached last know position of threat - we lost them
+ return Done( "Lost our target" );
+ }
+
+ me->EndStunEffects();
+ }
+
+ // approach
+ if ( !m_path.IsValid() || m_path.GetAge() > 1.0f )
+ {
+ ShortestPathCost cost;
+ m_path.Compute( me, me->GetLastKnownTargetPosition(), cost );
+ }
+
+ me->GetLocomotionInterface()->SetDesiredSpeed( tf_bot_npc_minion_speed.GetFloat() );
+
+ if ( me->IsLineOfSightClear( me->GetLastKnownTargetPosition() ) )
+ {
+ // move directly toward our goal
+ me->GetLocomotionInterface()->Approach( me->GetLastKnownTargetPosition() );
+ }
+ else
+ {
+ m_path.Update( me );
+ }
+
+ if ( m_pingTimer.IsElapsed() )
+ {
+ m_pingTimer.Reset();
+ me->EmitSound( "Minion.Ping.Acquire" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinionApproachTarget::OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction )
+{
+ me->EndStunEffects();
+ me->m_nSkin = MINION_LIGHT_OFF;
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionNotice : public Action< CBotNPCMinion >
+{
+public:
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction );
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+
+ virtual const char *GetName( void ) const { return "Notice"; } // return name of this action
+
+private:
+ CountdownTimer m_timer;
+};
+
+ConVar tf_bot_npc_minion_notice_duration( "tf_bot_npc_minion_notice_duration", "1"/*, FCVAR_CHEAT */ );
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionNotice::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+{
+ m_timer.Start( tf_bot_npc_minion_notice_duration.GetFloat() );
+
+ me->BecomeAlert();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionNotice::Update( CBotNPCMinion *me, float interval )
+{
+ if ( me->HasTarget() )
+ {
+ me->GetLocomotionInterface()->FaceTowards( me->GetTarget()->WorldSpaceCenter() );
+ }
+
+ if ( m_timer.IsElapsed() )
+ {
+ return ChangeTo( new CBotNPCMinionApproachTarget, "Acquiring target" );
+ }
+
+ return Continue();
+}
+
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionRoam : public Action< CBotNPCMinion >
+{
+public:
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction );
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+
+ virtual const char *GetName( void ) const { return "Roam"; } // return name of this action
+
+private:
+ CountdownTimer m_pingTimer;
+ CTFNavArea *m_goalArea;
+ CountdownTimer m_waitTimer;
+
+ void UpdateGoal( CBotNPCMinion *me );
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionRoam::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+{
+ m_goalArea = NULL;
+ m_pingTimer.Invalidate();
+ m_waitTimer.Invalidate();
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionRoam::Update( CBotNPCMinion *me, float interval )
+{
+ if ( me->HasTarget() )
+ {
+ return SuspendFor( new CBotNPCMinionNotice, "Target found..." );
+ }
+
+ // light off
+ me->m_nSkin = MINION_LIGHT_OFF;
+
+ if ( m_goalArea == NULL ||
+ me->GetLastKnownArea() == m_goalArea ||
+ m_goalArea->IsOverlapping( me->WorldSpaceCenter() ) ||
+ ( me->WorldSpaceCenter() - m_goalArea->GetCenter() ).AsVector2D().IsLengthLessThan( 20.0f ) )
+ {
+ // at goal
+ if ( m_waitTimer.HasStarted() )
+ {
+ if ( m_waitTimer.IsElapsed() )
+ {
+ // time to find a new goal
+ UpdateGoal( me );
+ m_waitTimer.Invalidate();
+ }
+ }
+ else
+ {
+ m_waitTimer.Start( RandomFloat( 3.0f, 5.0f ) );
+ }
+ }
+
+ if ( m_goalArea )
+ {
+ me->GetLocomotionInterface()->SetDesiredSpeed( tf_bot_npc_minion_speed.GetFloat() );
+ me->GetLocomotionInterface()->Approach( m_goalArea->GetCenter() );
+
+ if ( tf_bot_npc_minion_debug.GetBool() )
+ {
+ NDebugOverlay::Line( me->WorldSpaceCenter(), m_goalArea->GetCenter(), 0, 255, 0, true, 0.1f );
+ }
+ }
+
+ if ( m_pingTimer.IsElapsed() )
+ {
+ m_pingTimer.Start( RandomFloat( 0.9f, 1.1f ) * 3.0f );
+ me->EmitSound( "Minion.Ping.Roam" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CBotNPCMinionRoam::UpdateGoal( CBotNPCMinion *me )
+{
+ CNavArea *myArea = me->GetLastKnownArea();
+
+ if ( myArea )
+ {
+ CUtlVector< CNavArea * > adjVector;
+ myArea->CollectAdjacentAreas( &adjVector );
+
+ if ( adjVector.Count() > 0 )
+ {
+ m_goalArea = (CTFNavArea *)adjVector[ RandomInt( 0, adjVector.Count()-1 ) ];
+ }
+ }
+ else
+ {
+ CBaseCombatCharacter *boss = me->GetOwnerEntity() ? me->GetOwnerEntity()->MyCombatCharacterPointer() : NULL;
+ if ( boss )
+ {
+ m_goalArea = (CTFNavArea *)boss->GetLastKnownArea();
+ }
+ }
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+class CBotNPCMinionIdle : public Action< CBotNPCMinion >
+{
+public:
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction );
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval );
+
+ virtual const char *GetName( void ) const { return "Idle"; } // return name of this action
+
+private:
+ CountdownTimer m_pingTimer;
+};
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionIdle::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+{
+ m_pingTimer.Start( 3.0f );
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+ActionResult< CBotNPCMinion > CBotNPCMinionIdle::Update( CBotNPCMinion *me, float interval )
+{
+ if ( me->HasTarget() )
+ {
+ return SuspendFor( new CBotNPCMinionNotice, "Target found..." );
+ }
+
+ // light off
+ me->m_nSkin = MINION_LIGHT_OFF;
+
+ if ( m_pingTimer.IsElapsed() )
+ {
+ m_pingTimer.Reset();
+ me->EmitSound( "Minion.Ping.Roam" );
+ }
+
+ return Continue();
+}
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+ConVar tf_bot_npc_minion_init_vel( "tf_bot_npc_minion_init_vel", "250"/*, FCVAR_CHEAT*/ );
+
+
+class CBotNPCMinionBehavior : public Action< CBotNPCMinion >
+{
+public:
+ virtual Action< CBotNPCMinion > *InitialContainedAction( CBotNPCMinion *me )
+ {
+ if ( TFGameRules()->GetActiveBoss() )
+ {
+ return new CBotNPCMinionRoam;
+ }
+
+ // minions in the wild just hover in place
+ return new CBotNPCMinionIdle;
+ }
+
+ virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction )
+ {
+ Vector initVelocity;
+
+ float s, c;
+ SinCos( RandomFloat( -M_PI, M_PI ), &s, &c );
+
+ initVelocity.x = c * tf_bot_npc_minion_init_vel.GetFloat();
+ initVelocity.y = s * tf_bot_npc_minion_init_vel.GetFloat();
+ initVelocity.z = 0.0f;
+
+ static_cast< CNextBotFlyingLocomotion * >( me->GetLocomotionInterface() )->SetVelocity( initVelocity );
+
+ return Continue();
+ }
+
+ virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval )
+ {
+ me->UpdateTarget();
+
+ return Continue();
+ }
+
+ virtual EventDesiredResult< CBotNPCMinion > OnKilled( CBotNPCMinion *me, const CTakeDamageInfo &info )
+ {
+ me->BecomeAmmoPack();
+
+ DispatchParticleEffect( "asplode_hoodoo_embers", me->GetAbsOrigin(), me->GetAbsAngles() );
+ me->EmitSound( "Minion.Explode" );
+ UTIL_Remove( me );
+
+ return TryDone();
+ }
+
+ virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action
+};
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+IMPLEMENT_INTENTION_INTERFACE( CBotNPCMinion, CBotNPCMinionBehavior );
+
+
+//---------------------------------------------------------------------------------------------
+//---------------------------------------------------------------------------------------------
+CNextBotFlyingLocomotion::CNextBotFlyingLocomotion( INextBot *bot ) : ILocomotion( bot )
+{
+ Reset();
+}
+
+
+//---------------------------------------------------------------------------------------------
+CNextBotFlyingLocomotion::~CNextBotFlyingLocomotion()
+{
+}
+
+
+//---------------------------------------------------------------------------------------------
+// (EXTEND) reset to initial state
+void CNextBotFlyingLocomotion::Reset( void )
+{
+ m_velocity = vec3_origin;
+ m_acceleration = vec3_origin;
+ m_desiredSpeed = 0.0f;
+ m_currentSpeed = 0.0f;
+ m_forward = vec3_origin;
+ m_desiredAltitude = 50.0f;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CNextBotFlyingLocomotion::MaintainAltitude( void )
+{
+ CBaseCombatCharacter *me = GetBot()->GetEntity();
+
+ trace_t result;
+ //CTraceFilterSimple filter( me, COLLISION_GROUP_NONE );
+ CTraceFilterSimpleClassnameList filter( me, COLLISION_GROUP_NONE );
+ filter.AddClassnameToIgnore( "bot_npc_minion" );
+
+ // find ceiling
+ TraceHull( me->GetAbsOrigin(), me->GetAbsOrigin() + Vector( 0, 0, 1000.0f ),
+ me->WorldAlignMins(), me->WorldAlignMaxs(),
+ GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result );
+
+ float ceiling = result.endpos.z - me->GetAbsOrigin().z;
+
+ // trace wider hull to account for nearby ledges we want to float over
+ TraceHull( me->GetAbsOrigin() + Vector( 0, 0, ceiling ),
+ me->GetAbsOrigin() + Vector( 0, 0, -1000.0f ),
+ Vector( 2.0f * me->WorldAlignMins().x, 2.0f * me->WorldAlignMins().y, me->WorldAlignMins().z ),
+ Vector( 2.0f * me->WorldAlignMaxs().x, 2.0f * me->WorldAlignMaxs().y, me->WorldAlignMaxs().z ),
+ GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result );
+
+ float groundZ = result.endpos.z;
+
+ float currentAltitude = me->GetAbsOrigin().z - groundZ;
+ float error = m_desiredAltitude - currentAltitude;
+ float accelZ = clamp( error, -tf_bot_npc_minion_acceleration.GetFloat(), tf_bot_npc_minion_acceleration.GetFloat() );
+
+ m_acceleration.z += accelZ;
+}
+
+
+ConVar tf_bot_npc_minion_avoid_range( "tf_bot_npc_minion_avoid_range", "100"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_avoid_force( "tf_bot_npc_minion_avoid_force", "100"/*, FCVAR_CHEAT*/ );
+
+
+//---------------------------------------------------------------------------------------------
+// (EXTEND) update internal state
+void CNextBotFlyingLocomotion::Update( void )
+{
+ CBaseCombatCharacter *me = GetBot()->GetEntity();
+ const float deltaT = GetUpdateInterval();
+
+ Vector pos = me->GetAbsOrigin();
+
+ // always maintain altitude, even if not trying to move (ie: no Approach call)
+ MaintainAltitude();
+
+ m_forward = m_velocity;
+ m_currentSpeed = m_forward.NormalizeInPlace();
+
+ Vector damping( tf_bot_npc_minion_horiz_damping.GetFloat(), tf_bot_npc_minion_horiz_damping.GetFloat(), tf_bot_npc_minion_vert_damping.GetFloat() );
+ Vector totalAccel = m_acceleration - m_velocity * damping;
+
+ // avoid other minions
+ CBaseEntity *minion = NULL;
+ while( ( minion = gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL )
+ {
+ if ( me == minion )
+ continue;
+
+ Vector toPeer = minion->GetAbsOrigin() - me->GetAbsOrigin();
+ toPeer.z = 0.0f;
+ float range = toPeer.NormalizeInPlace();
+
+ if ( range < tf_bot_npc_minion_avoid_range.GetFloat() )
+ {
+ totalAccel += -tf_bot_npc_minion_avoid_force.GetFloat() * toPeer;
+ }
+ }
+
+ m_velocity += totalAccel * deltaT;
+ me->SetAbsVelocity( m_velocity );
+
+ pos += m_velocity * deltaT;
+
+ // check for collisions along move
+ trace_t result;
+ CTraceFilterSkipClassname filter( me, "bot_npc_minion", COLLISION_GROUP_NONE );
+ Vector from = me->GetAbsOrigin();
+ Vector to = pos;
+ Vector desiredGoal = to;
+ Vector resolvedGoal;
+ int recursionLimit = 3;
+
+ int hitCount = 0;
+ Vector surfaceNormal = vec3_origin;
+
+ bool didHitWorld = false;
+
+ while( true )
+ {
+ TraceHull( from, desiredGoal, me->WorldAlignMins(), me->WorldAlignMaxs(), GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result );
+
+ if ( !result.DidHit() )
+ {
+ resolvedGoal = pos;
+ break;
+ }
+
+ if ( result.DidHitWorld() )
+ {
+ didHitWorld = true;
+ }
+
+ ++hitCount;
+ surfaceNormal += result.plane.normal;
+
+ // If we hit really close to our target, then stop
+ if ( !result.startsolid && desiredGoal.DistToSqr( result.endpos ) < 1.0f )
+ {
+ resolvedGoal = result.endpos;
+ break;
+ }
+
+ if ( result.startsolid )
+ {
+ // stuck inside solid; don't move
+ resolvedGoal = me->GetAbsOrigin();
+ break;
+ }
+
+ if ( --recursionLimit <= 0 )
+ {
+ // reached recursion limit, no more adjusting allowed
+ resolvedGoal = result.endpos;
+ break;
+ }
+
+ // slide off of surface we hit
+ Vector fullMove = desiredGoal - from;
+ Vector leftToMove = fullMove * ( 1.0f - result.fraction );
+
+ float blocked = DotProduct( result.plane.normal, leftToMove );
+
+ Vector unconstrained = fullMove - blocked * result.plane.normal;
+
+ // check for collisions along remainder of move
+ // But don't bother if we're not going to deflect much
+ Vector remainingMove = from + unconstrained;
+ if ( remainingMove.DistToSqr( result.endpos ) < 1.0f )
+ {
+ resolvedGoal = result.endpos;
+ break;
+ }
+
+ desiredGoal = remainingMove;
+ }
+
+ if ( hitCount > 0 )
+ {
+ surfaceNormal.NormalizeInPlace();
+
+ // bounce
+ m_velocity = m_velocity - 2.0f * DotProduct( m_velocity, surfaceNormal ) * surfaceNormal;
+
+ if ( didHitWorld )
+ {
+ me->EmitSound( "Minion.Bounce" );
+ }
+ }
+
+ GetBot()->GetEntity()->SetAbsOrigin( result.endpos );
+
+ m_acceleration = vec3_origin;
+}
+
+
+//---------------------------------------------------------------------------------------------
+// (EXTEND) move directly towards the given position
+void CNextBotFlyingLocomotion::Approach( const Vector &goalPos, float goalWeight )
+{
+ Vector flyGoal = goalPos;
+ flyGoal.z += m_desiredAltitude;
+
+ Vector toGoal = flyGoal - GetBot()->GetEntity()->GetAbsOrigin();
+ // altitude is handled in Update()
+ toGoal.z = 0.0f;
+ toGoal.NormalizeInPlace();
+
+ m_acceleration += tf_bot_npc_minion_acceleration.GetFloat() * toGoal;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CNextBotFlyingLocomotion::SetDesiredSpeed( float speed )
+{
+ m_desiredSpeed = speed;
+}
+
+
+//---------------------------------------------------------------------------------------------
+float CNextBotFlyingLocomotion::GetDesiredSpeed( void ) const
+{
+ return m_desiredSpeed;
+}
+
+
+//---------------------------------------------------------------------------------------------
+void CNextBotFlyingLocomotion::SetDesiredAltitude( float height )
+{
+ m_desiredAltitude = height;
+}
+
+
+//---------------------------------------------------------------------------------------------
+float CNextBotFlyingLocomotion::GetDesiredAltitude( void ) const
+{
+ return m_desiredAltitude;
+}
+
+
+ConVar tf_bot_npc_minion_deflect_range( "tf_bot_npc_minion_deflect_range", "300"/*, FCVAR_CHEAT*/ );
+ConVar tf_bot_npc_minion_deflect_force( "tf_bot_npc_minion_deflect_force", "2000"/*, FCVAR_CHEAT*/ );
+
+
+//---------------------------------------------------------------------------------------------
+void CNextBotFlyingLocomotion::Deflect( CBaseEntity *deflector )
+{
+ if ( deflector )
+ {
+ Vector fromDeflector = GetBot()->GetEntity()->WorldSpaceCenter() - deflector->EyePosition();
+ float range = fromDeflector.NormalizeInPlace();
+
+ if ( range < tf_bot_npc_minion_deflect_range.GetFloat() )
+ {
+ m_velocity += ( 1.0f - ( range / tf_bot_npc_minion_deflect_range.GetFloat() ) ) * tf_bot_npc_minion_deflect_force.GetFloat() * fromDeflector;
+ }
+ }
+}
+
+#endif // TF_RAID_MODE
diff --git a/game/server/tf/bot_npc/bot_npc_minion.h b/game/server/tf/bot_npc/bot_npc_minion.h
new file mode 100644
index 0000000..6d2deee
--- /dev/null
+++ b/game/server/tf/bot_npc/bot_npc_minion.h
@@ -0,0 +1,199 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+// bot_npc_minion.h
+// Minions for the Boss
+// Michael Booth, November 2010
+
+#ifndef BOT_NPC_MINION_H
+#define BOT_NPC_MINION_H
+
+#ifdef TF_RAID_MODE
+
+#include "NextBot.h"
+#include "NextBotBehavior.h"
+#include "Path/NextBotPathFollow.h"
+#include "bot_npc.h"
+#include "bot_npc_body.h"
+
+
+//----------------------------------------------------------------------------
+// Bypass vision system
+class CDisableVision : public IVision
+{
+public:
+ CDisableVision( INextBot *bot ) : IVision( bot ) { }
+ virtual ~CDisableVision() { }
+
+ virtual void Reset( void ) { }
+ virtual void Update( void ) { }
+};
+
+
+//----------------------------------------------------------------------------
+class CNextBotFlyingLocomotion : public ILocomotion
+{
+public:
+ CNextBotFlyingLocomotion( INextBot *bot );
+ virtual ~CNextBotFlyingLocomotion();
+
+ virtual void Reset( void ); // (EXTEND) reset to initial state
+ virtual void Update( void ); // (EXTEND) update internal state
+
+ virtual void Approach( const Vector &goalPos, float goalWeight = 1.0f ); // (EXTEND) move directly towards the given position
+
+ virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement
+ virtual float GetDesiredSpeed( void ) const; // returns the current desired speed
+
+ virtual void SetDesiredAltitude( float height ); // how high above our Approach goal do we float?
+ virtual float GetDesiredAltitude( void ) const;
+
+ virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with
+
+ virtual const Vector &GetVelocity( void ) const; // return current world space velocity
+ void SetVelocity( const Vector &velocity );
+
+ void Deflect( CBaseEntity *deflector );
+
+protected:
+ float m_desiredSpeed;
+ float m_currentSpeed;
+ Vector m_forward;
+
+ float m_desiredAltitude;
+ void MaintainAltitude( void );
+
+ Vector m_velocity;
+ Vector m_acceleration;
+};
+
+inline const Vector &CNextBotFlyingLocomotion::GetGroundNormal( void ) const
+{
+ static Vector up( 0, 0, 1.0f );
+
+ return up;
+}
+
+inline const Vector &CNextBotFlyingLocomotion::GetVelocity( void ) const
+{
+ return m_velocity;
+}
+
+inline void CNextBotFlyingLocomotion::SetVelocity( const Vector &velocity )
+{
+ m_velocity = velocity;
+}
+
+
+//----------------------------------------------------------------------------
+class CBotNPCMinion : public NextBotCombatCharacter
+{
+public:
+ DECLARE_CLASS( CBotNPCMinion, NextBotCombatCharacter );
+ DECLARE_SERVERCLASS();
+
+ CBotNPCMinion();
+ virtual ~CBotNPCMinion();
+
+ virtual void Precache();
+ virtual void Spawn( void );
+
+ virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info );
+
+ virtual bool IsDeflectable() { return true; } // for flamethrower
+ virtual void Deflected( CBaseEntity *pDeflectedBy, Vector &vecDir );
+
+ // INextBot
+ DECLARE_INTENTION_INTERFACE( CBotNPCMinion );
+ virtual CNextBotFlyingLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; }
+ virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; }
+ virtual IVision *GetVisionInterface( void ) const { return m_vision; }
+
+ virtual Vector EyePosition( void );
+
+ virtual unsigned int PhysicsSolidMaskForEntity( void ) const;
+ virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const;
+
+ void BecomeAmmoPack( void );
+
+ CTFPlayer *FindTarget( void ); // Find the closest living player not already being targeted by another minion
+
+ void UpdateTarget( void );
+
+ void SetTarget( CTFPlayer *target );
+ CTFPlayer *GetTarget( void ) const;
+ bool HasTarget( void ) const;
+ bool IsTarget( CTFPlayer *target ) const;
+
+ const Vector &GetLastKnownTargetPosition( void ) const;
+
+ void StartStunEffects( CTFPlayer *victim );
+ void EndStunEffects( void );
+
+ bool IsAlert( void ) const;
+ void BecomeAlert( void );
+
+private:
+ CNextBotFlyingLocomotion *m_locomotor;
+ CBotNPCBody *m_body;
+ CDisableVision *m_vision;
+
+ Vector m_eyeOffset;
+
+ CTFPlayer *m_target;
+ Vector m_lastKnownTargetPosition;
+
+ CountdownTimer m_invulnTimer;
+
+ CNetworkHandle( CBaseEntity, m_stunTarget );
+
+ bool m_isAlert;
+};
+
+inline bool CBotNPCMinion::IsAlert( void ) const
+{
+ return m_isAlert;
+}
+
+inline Vector CBotNPCMinion::EyePosition( void )
+{
+ return GetAbsOrigin() + m_eyeOffset;
+}
+
+inline bool CBotNPCMinion::HasTarget( void ) const
+{
+ return m_target == NULL ? false : true;
+}
+
+inline bool CBotNPCMinion::IsTarget( CTFPlayer *target ) const
+{
+ return ( m_target == target ) ? true : false;
+}
+
+inline void CBotNPCMinion::SetTarget( CTFPlayer *target )
+{
+ m_target = target;
+}
+
+inline CTFPlayer *CBotNPCMinion::GetTarget( void ) const
+{
+ return m_target;
+}
+
+inline const Vector &CBotNPCMinion::GetLastKnownTargetPosition( void ) const
+{
+ return m_lastKnownTargetPosition;
+}
+
+inline void CBotNPCMinion::StartStunEffects( CTFPlayer *victim )
+{
+ m_stunTarget = victim;
+}
+
+inline void CBotNPCMinion::EndStunEffects( void )
+{
+ m_stunTarget = NULL;
+}
+
+
+#endif // TF_RAID_MODE
+
+#endif // BOT_NPC_MINION_H