Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

3604 lines
100 KiB

//========= 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