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.
3605 lines
100 KiB
3605 lines
100 KiB
5 years ago
|
//========= 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
|