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