You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1650 lines
48 KiB
1650 lines
48 KiB
4 years ago
|
//========= Copyright Valve Corporation, All rights reserved. ============//
|
||
|
// tf_bot_behavior.cpp
|
||
|
// Team Fortress NextBot
|
||
|
// Michael Booth, February 2009
|
||
|
|
||
|
#include "cbase.h"
|
||
|
#include "fmtstr.h"
|
||
|
|
||
|
#include "nav_mesh.h"
|
||
|
#include "tf_player.h"
|
||
|
#include "tf_gamerules.h"
|
||
|
#include "tf_projectile_rocket.h"
|
||
|
#include "tf_weaponbase_grenadeproj.h"
|
||
|
#include "tf_obj.h"
|
||
|
#include "tf_obj_sentrygun.h"
|
||
|
#include "tf_weapon_flamethrower.h"
|
||
|
#include "tf_weapon_sniperrifle.h"
|
||
|
#include "tf_weapon_compound_bow.h"
|
||
|
#include "bot/tf_bot.h"
|
||
|
#include "bot/tf_bot_manager.h"
|
||
|
#include "bot/behavior/tf_bot_behavior.h"
|
||
|
#include "bot/behavior/tf_bot_dead.h"
|
||
|
#include "NextBot/NavMeshEntities/func_nav_prerequisite.h"
|
||
|
#include "bot/behavior/nav_entities/tf_bot_nav_ent_destroy_entity.h"
|
||
|
#include "bot/behavior/nav_entities/tf_bot_nav_ent_move_to.h"
|
||
|
#include "bot/behavior/nav_entities/tf_bot_nav_ent_wait.h"
|
||
|
#include "bot/behavior/tf_bot_tactical_monitor.h"
|
||
|
#include "bot/behavior/tf_bot_taunt.h"
|
||
|
#include "bot/behavior/scenario/creep_wave/tf_bot_creep_wave.h"
|
||
|
#include "player_vs_environment/tf_population_manager.h"
|
||
|
|
||
|
|
||
|
extern ConVar tf_bot_health_ok_ratio;
|
||
|
|
||
|
ConVar tf_bot_path_lookahead_range( "tf_bot_path_lookahead_range", "300" );
|
||
|
ConVar tf_bot_sniper_aim_error( "tf_bot_sniper_aim_error", "0.01", FCVAR_CHEAT );
|
||
|
ConVar tf_bot_sniper_aim_steady_rate( "tf_bot_sniper_aim_steady_rate", "10", FCVAR_CHEAT );
|
||
|
ConVar tf_bot_debug_sniper( "tf_bot_debug_sniper", "0", FCVAR_CHEAT );
|
||
|
ConVar tf_bot_fire_weapon_min_time( "tf_bot_fire_weapon_min_time", "1", FCVAR_CHEAT );
|
||
|
ConVar tf_bot_taunt_victim_chance( "tf_bot_taunt_victim_chance", "20" ); // community requested this not be a cheat cvar
|
||
|
|
||
|
ConVar tf_bot_notice_backstab_chance( "tf_bot_notice_backstab_chance", "25", FCVAR_CHEAT );
|
||
|
ConVar tf_bot_notice_backstab_min_range( "tf_bot_notice_backstab_min_range", "100", FCVAR_CHEAT );
|
||
|
ConVar tf_bot_notice_backstab_max_range( "tf_bot_notice_backstab_max_range", "750", FCVAR_CHEAT );
|
||
|
|
||
|
ConVar tf_bot_arrow_elevation_rate( "tf_bot_arrow_elevation_rate", "0.0001", FCVAR_CHEAT, "When firing arrows at far away targets, this is the degree/range slope to raise our aim" );
|
||
|
ConVar tf_bot_ballistic_elevation_rate( "tf_bot_ballistic_elevation_rate", "0.01", FCVAR_CHEAT, "When lobbing grenades at far away targets, this is the degree/range slope to raise our aim" );
|
||
|
|
||
|
ConVar tf_bot_hitscan_range_limit( "tf_bot_hitscan_range_limit", "1800", FCVAR_CHEAT );
|
||
|
|
||
|
ConVar tf_bot_always_full_reload( "tf_bot_always_full_reload", "0", FCVAR_CHEAT );
|
||
|
|
||
|
ConVar tf_bot_fire_weapon_allowed( "tf_bot_fire_weapon_allowed", "1", FCVAR_CHEAT, "If zero, TFBots will not pull the trigger of their weapons (but will act like they did)" );
|
||
|
|
||
|
#ifdef STAGING_ONLY
|
||
|
ConVar tf_bot_use_items( "tf_bot_use_items", "0", FCVAR_CHEAT, "0-100: Chance bot will use random item." );
|
||
|
#endif
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
Action< CTFBot > *CTFBotMainAction::InitialContainedAction( CTFBot *me )
|
||
|
{
|
||
|
return new CTFBotTacticalMonitor;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
ActionResult< CTFBot > CTFBotMainAction::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
|
||
|
{
|
||
|
m_lastTouch = NULL;
|
||
|
m_lastTouchTime = 0.0f;
|
||
|
m_aimErrorRadius = 0.0f;
|
||
|
m_aimErrorAngle = 0.0f;
|
||
|
m_nextDisguise = TF_CLASS_UNDEFINED;
|
||
|
|
||
|
m_yawRate = 0.0f;
|
||
|
m_priorYaw = 0.0f;
|
||
|
|
||
|
m_isWaitingForFullReload = false;
|
||
|
|
||
|
// if bot is already dead at this point, make sure it's dead
|
||
|
// check for !IsAlive because bot could be DYING
|
||
|
if ( !me->IsAlive() )
|
||
|
{
|
||
|
return ChangeTo( new CTFBotDead, "I'm actually dead" );
|
||
|
}
|
||
|
|
||
|
#ifdef TF_CREEP_MODE
|
||
|
if ( TFGameRules()->IsCreepWaveMode() )
|
||
|
{
|
||
|
return ChangeTo( new CTFBotCreepWave, "I'm a creep" );
|
||
|
}
|
||
|
#endif // TF_CREEP_MODE
|
||
|
|
||
|
|
||
|
#ifdef STAGING_ONLY
|
||
|
if ( tf_bot_use_items.GetInt() && ( RandomInt(0, 100) <= tf_bot_use_items.GetInt() ) )
|
||
|
{
|
||
|
me->GiveRandomItem( LOADOUT_POSITION_PRIMARY );
|
||
|
me->GiveRandomItem( LOADOUT_POSITION_SECONDARY );
|
||
|
me->GiveRandomItem( LOADOUT_POSITION_MELEE );
|
||
|
|
||
|
me->GiveRandomItem( LOADOUT_POSITION_HEAD );
|
||
|
me->GiveRandomItem( LOADOUT_POSITION_MISC );
|
||
|
me->GiveRandomItem( LOADOUT_POSITION_MISC2 );
|
||
|
}
|
||
|
#endif // STAGING_ONLY
|
||
|
|
||
|
return Continue();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
ActionResult< CTFBot > CTFBotMainAction::Update( CTFBot *me, float interval )
|
||
|
{
|
||
|
VPROF_BUDGET( "CTFBotMainAction::Update", "NextBot" );
|
||
|
|
||
|
if ( me->GetTeamNumber() != TF_TEAM_BLUE && me->GetTeamNumber() != TF_TEAM_RED )
|
||
|
{
|
||
|
// not on a team - do nothing
|
||
|
return Done( "Not on a playing team" );
|
||
|
}
|
||
|
|
||
|
// Should I accept taunt from my partner?
|
||
|
if ( me->FindPartnerTauntInitiator() )
|
||
|
{
|
||
|
return SuspendFor( new CTFBotTaunt, "Responding to teammate partner taunt" );
|
||
|
}
|
||
|
|
||
|
// make sure our vision FOV matches the player's
|
||
|
me->GetVisionInterface()->SetFieldOfView( me->GetFOV() );
|
||
|
|
||
|
// teammates in training have infinite ammo
|
||
|
if ( TFGameRules()->IsInTraining() && me->GetTeamNumber() == TF_TEAM_BLUE )
|
||
|
{
|
||
|
me->GiveAmmo( 1000, TF_AMMO_METAL, true );
|
||
|
}
|
||
|
|
||
|
// track aim velocity ourselves, since body aim "steady" is too loose
|
||
|
float deltaYaw = me->EyeAngles().y - m_priorYaw;
|
||
|
m_yawRate = fabs( deltaYaw / ( interval + 0.0001f ) );
|
||
|
m_priorYaw = me->EyeAngles().y;
|
||
|
|
||
|
if ( m_yawRate < tf_bot_sniper_aim_steady_rate.GetFloat() )
|
||
|
{
|
||
|
if ( !m_steadyTimer.HasStarted() )
|
||
|
m_steadyTimer.Start();
|
||
|
|
||
|
// if ( tf_bot_debug_sniper.GetBool() )
|
||
|
// {
|
||
|
// DevMsg( "%3.2f: STEADY\n", gpGlobals->curtime );
|
||
|
// }
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
m_steadyTimer.Invalidate();
|
||
|
|
||
|
// if ( tf_bot_debug_sniper.GetBool() )
|
||
|
// {
|
||
|
// DevMsg( "%3.2f: Yaw rate = %3.2f\n", gpGlobals->curtime, m_yawRate );
|
||
|
// }
|
||
|
}
|
||
|
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() && me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
|
||
|
{
|
||
|
// infinite ammo
|
||
|
// me->GiveAmmo( 100, TF_AMMO_PRIMARY, true );
|
||
|
// me->GiveAmmo( 100, TF_AMMO_SECONDARY, true );
|
||
|
// This resets the Sandman
|
||
|
//me->GiveAmmo( 100, TF_AMMO_GRENADES1, true );
|
||
|
// This resets the Bonk drink meter...
|
||
|
//me->GiveAmmo( 100, TF_AMMO_GRENADES2, true );
|
||
|
me->GiveAmmo( 100, TF_AMMO_METAL, true );
|
||
|
|
||
|
me->m_Shared.AddToSpyCloakMeter( 100.0f );
|
||
|
|
||
|
CTFNavArea *myArea = me->GetLastKnownArea();
|
||
|
int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
|
||
|
|
||
|
if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
|
||
|
{
|
||
|
// invading bots get uber while they leave their spawn so they don't drop their cash where players can't pick it up
|
||
|
me->m_Shared.AddCond( TF_COND_INVULNERABLE, 0.5f );
|
||
|
me->m_Shared.AddCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED, 0.5f );
|
||
|
me->m_Shared.AddCond( TF_COND_INVULNERABLE_WEARINGOFF, 0.5f );
|
||
|
}
|
||
|
|
||
|
// watch for bots that have fallen through the ground
|
||
|
if ( myArea && myArea->GetZ( me->GetAbsOrigin() ) - me->GetAbsOrigin().z > 100.0f )
|
||
|
{
|
||
|
if ( !m_undergroundTimer.HasStarted() )
|
||
|
{
|
||
|
m_undergroundTimer.Start();
|
||
|
}
|
||
|
else if ( m_undergroundTimer.IsGreaterThen( 3.0f ) )
|
||
|
{
|
||
|
UTIL_LogPrintf( "\"%s<%i><%s><%s>\" underground (position \"%3.2f %3.2f %3.2f\")\n",
|
||
|
me->GetPlayerName(),
|
||
|
me->GetUserID(),
|
||
|
me->GetNetworkIDString(),
|
||
|
me->GetTeam()->GetName(),
|
||
|
me->GetAbsOrigin().x, me->GetAbsOrigin().y, me->GetAbsOrigin().z );
|
||
|
|
||
|
// teleport bot to a reasonable place
|
||
|
me->SetAbsOrigin( myArea->GetCenter() );
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
m_undergroundTimer.Invalidate();
|
||
|
}
|
||
|
|
||
|
if ( me->ShouldAutoJump() )
|
||
|
{
|
||
|
me->GetLocomotionInterface()->Jump();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// spies always want to be disguised
|
||
|
if ( !me->IsFiringWeapon() && !me->m_Shared.InCond( TF_COND_DISGUISED ) && !me->m_Shared.InCond( TF_COND_DISGUISING ) )
|
||
|
{
|
||
|
if ( me->CanDisguise() )
|
||
|
{
|
||
|
if ( m_nextDisguise == TF_CLASS_UNDEFINED )
|
||
|
{
|
||
|
if ( me->IsDifficulty( CTFBot::EASY ) || me->IsDifficulty( CTFBot::NORMAL ) )
|
||
|
{
|
||
|
// disguise as a random class
|
||
|
me->m_Shared.Disguise( GetEnemyTeam( me->GetTeamNumber() ), RandomInt( TF_FIRST_NORMAL_CLASS, TF_LAST_NORMAL_CLASS-1 ) );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
me->DisguiseAsMemberOfEnemyTeam();
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// disguise as the class we just killed
|
||
|
me->m_Shared.Disguise( GetEnemyTeam( me->GetTeamNumber() ), m_nextDisguise );
|
||
|
m_nextDisguise = TF_CLASS_UNDEFINED;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
me->EquipRequiredWeapon();
|
||
|
|
||
|
me->UpdateLookingAroundForEnemies();
|
||
|
FireWeaponAtEnemy( me );
|
||
|
Dodge( me );
|
||
|
|
||
|
if ( me->IsPlayerClass( TF_CLASS_DEMOMAN ) )
|
||
|
{
|
||
|
// dont auto reload, so we fire stickies fast
|
||
|
me->SetAutoReload( false );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// reload weapons
|
||
|
me->SetAutoReload( true );
|
||
|
}
|
||
|
|
||
|
return Continue();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
EventDesiredResult<CTFBot> CTFBotMainAction::OnKilled( CTFBot *me, const CTakeDamageInfo& info )
|
||
|
{
|
||
|
return TryChangeTo( new CTFBotDead, RESULT_CRITICAL, "I died!" );
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
EventDesiredResult< CTFBot > CTFBotMainAction::OnInjured( CTFBot *me, const CTakeDamageInfo &info )
|
||
|
{
|
||
|
CBaseObject *obj = dynamic_cast< CBaseObject * >( info.GetInflictor() );
|
||
|
|
||
|
// if an object hurt me, it must be a sentry
|
||
|
CBaseEntity *subject = obj ? obj : info.GetAttacker();
|
||
|
|
||
|
// notice the gunfire - needed for sentry guns, which don't go through the player OnWeaponFired() system
|
||
|
me->GetVisionInterface()->AddKnownEntity( subject );
|
||
|
|
||
|
if ( info.GetInflictor() && info.GetInflictor()->GetTeamNumber() != me->GetTeamNumber() )
|
||
|
{
|
||
|
CObjectSentrygun *sentrygun = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
|
||
|
|
||
|
if ( sentrygun )
|
||
|
{
|
||
|
// we were injured by an enemy sentry - remember it
|
||
|
me->RememberEnemySentry( sentrygun, me->GetAbsOrigin() );
|
||
|
}
|
||
|
|
||
|
if ( info.GetDamageCustom() == TF_DMG_CUSTOM_BACKSTAB )
|
||
|
{
|
||
|
// backstabs that don't kill me make me mad
|
||
|
me->DelayedThreatNotice( info.GetInflictor(), 0.5f );
|
||
|
|
||
|
// chance of nearby friends noticing the backstab
|
||
|
CUtlVector< CTFPlayer * > playerVector;
|
||
|
CollectPlayers( &playerVector, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
|
||
|
|
||
|
float minRange = tf_bot_notice_backstab_min_range.GetFloat();
|
||
|
float maxRange = tf_bot_notice_backstab_max_range.GetFloat();
|
||
|
float deltaRange = maxRange - minRange;
|
||
|
|
||
|
for( int i=0; i<playerVector.Count(); ++i )
|
||
|
{
|
||
|
CTFBot *bot = ToTFBot( playerVector[i] );
|
||
|
if ( bot )
|
||
|
{
|
||
|
if ( !me->IsSelf( bot ) )
|
||
|
{
|
||
|
float range = me->GetRangeTo( bot );
|
||
|
|
||
|
if ( range > maxRange )
|
||
|
{
|
||
|
// too far away to notice
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
int noticeChance = tf_bot_notice_backstab_chance.GetInt();
|
||
|
|
||
|
if ( range > minRange )
|
||
|
{
|
||
|
// scale notice chance down to zero at max range
|
||
|
noticeChance *= ( range - minRange ) / deltaRange;
|
||
|
}
|
||
|
|
||
|
if ( RandomInt( 0, 100 ) < noticeChance )
|
||
|
{
|
||
|
bot->DelayedThreatNotice( info.GetInflictor(), 0.5f );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if ( info.GetAttacker() && ( info.GetDamageType() & DMG_CRITICAL ) && ( info.GetDamageType() & DMG_BURN ) )
|
||
|
{
|
||
|
// Notice anyone nearby hitting us with crit fire (i.e. Backburner)
|
||
|
if ( me->GetRangeTo( info.GetAttacker() ) < tf_bot_notice_backstab_max_range.GetFloat() )
|
||
|
{
|
||
|
me->DelayedThreatNotice( info.GetAttacker(), 0.5f );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
#ifdef UNNEEDED // known entity/listening to gunfire handles this without insta-turn
|
||
|
|
||
|
if ( false && !me->IsSelf( info.GetAttacker() ) )
|
||
|
{
|
||
|
// hack to stop engineers from looking away from healing their sentry
|
||
|
if ( !me->IsPlayerClass( TF_CLASS_ENGINEER ) && !me->IsPlayerClass( TF_CLASS_MEDIC ) )
|
||
|
{
|
||
|
CBaseObject *obj = dynamic_cast< CBaseObject * >( info.GetInflictor() );
|
||
|
|
||
|
// if an object hurt me, it must be a sentry
|
||
|
CBaseEntity *subject = obj ? obj : info.GetAttacker();
|
||
|
|
||
|
if ( !me->GetVisionInterface()->IsInFieldOfView( subject ) )
|
||
|
{
|
||
|
// something out of my field of view hurt me - look around for it
|
||
|
// turn right or left, since player's damage indicators tell them which way
|
||
|
Vector forward, right;
|
||
|
me->EyeVectors( &forward, &right );
|
||
|
|
||
|
Vector toAttacker = subject->EyePosition() - me->EyePosition();
|
||
|
Vector newForward;
|
||
|
float error = 1.0f; RandomFloat( -1.0f, 1.0f );
|
||
|
|
||
|
if ( DotProduct( right, toAttacker ) > 0.0f )
|
||
|
{
|
||
|
newForward = error * forward + right;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
newForward = error * forward - right;
|
||
|
}
|
||
|
|
||
|
me->GetBodyInterface()->AimHeadTowards( me->EyePosition() + 100.0f * newForward, IBody::IMPORTANT, RandomFloat( 0.5f, 1.0f ), NULL, "Something hurt me!" );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endif // _DEBUG
|
||
|
|
||
|
return TryContinue();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
EventDesiredResult< CTFBot > CTFBotMainAction::OnContact( CTFBot *me, CBaseEntity *other, CGameTrace *result )
|
||
|
{
|
||
|
if ( other && !other->IsSolidFlagSet( FSOLID_NOT_SOLID ) && !other->IsWorld() && !other->IsPlayer() )
|
||
|
{
|
||
|
m_lastTouch = other;
|
||
|
m_lastTouchTime = gpGlobals->curtime;
|
||
|
|
||
|
// Mini-bosses destroy non-Sentrygun objects they bump into (ie: Dispensers)
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() && me->IsMiniBoss() )
|
||
|
{
|
||
|
if ( other->IsBaseObject() )
|
||
|
{
|
||
|
CBaseObject *pObject = assert_cast< CBaseObject* >( other );
|
||
|
if ( pObject->GetType() != OBJ_SENTRYGUN || pObject->IsMiniBuilding() )
|
||
|
{
|
||
|
int damage = MAX( other->GetMaxHealth(), other->GetHealth() );
|
||
|
|
||
|
Vector toVictim = other->WorldSpaceCenter() - me->WorldSpaceCenter();
|
||
|
|
||
|
CTakeDamageInfo info( me, me, 4 * damage, DMG_BLAST, TF_DMG_CUSTOM_NONE );
|
||
|
CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
|
||
|
other->TakeDamage( info );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return TryContinue();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
class BlockOverlappingAreaScan
|
||
|
{
|
||
|
public:
|
||
|
BlockOverlappingAreaScan( int teamID, CBaseEntity *blocker )
|
||
|
{
|
||
|
m_teamID = teamID;
|
||
|
m_blocker = blocker;
|
||
|
}
|
||
|
|
||
|
bool operator() ( CNavArea *baseArea )
|
||
|
{
|
||
|
CTFNavArea *area = static_cast< CTFNavArea * >( baseArea );
|
||
|
|
||
|
area->SetAttributeTF( TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE );
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int m_teamID;
|
||
|
CBaseEntity *m_blocker;
|
||
|
};
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
EventDesiredResult< CTFBot > CTFBotMainAction::OnStuck( CTFBot *me )
|
||
|
{
|
||
|
/*
|
||
|
// if we are touching a func_door while stuck, assume the door is locked and block
|
||
|
// the nav areas underneath it until the next stage of the scenario
|
||
|
if ( m_lastTouch != NULL && gpGlobals->curtime - m_lastTouchTime < 2.0f )
|
||
|
{
|
||
|
if ( FClassnameIs( m_lastTouch, "func_door*" ) || FClassnameIs( m_lastTouch, "prop_door*" ) || FClassnameIs( m_lastTouch, "func_brush" ) )
|
||
|
{
|
||
|
Extent extent;
|
||
|
extent.Init( m_lastTouch );
|
||
|
|
||
|
BlockOverlappingAreaScan block( me->GetTeamNumber(), m_lastTouch );
|
||
|
TheNavMesh->ForAllAreasOverlappingExtent( block, extent );
|
||
|
}
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() )
|
||
|
{
|
||
|
if ( me->m_Shared.InCond( TF_COND_MVM_BOT_STUN_RADIOWAVE ) )
|
||
|
{
|
||
|
// bot is stunned, not stuck
|
||
|
return TryContinue();
|
||
|
}
|
||
|
|
||
|
if ( m_lastTouch != NULL && gpGlobals->curtime - m_lastTouchTime < 2.0f )
|
||
|
{
|
||
|
if ( m_lastTouch->IsBaseObject() && dynamic_cast< CObjectSentrygun * >( m_lastTouch.Get() ) == NULL )
|
||
|
{
|
||
|
// we are stuck on a teleporter or dispenser - destroy it!
|
||
|
int damage = MAX( m_lastTouch->GetMaxHealth(), m_lastTouch->GetHealth() );
|
||
|
|
||
|
Vector toVictim = m_lastTouch->WorldSpaceCenter() - me->WorldSpaceCenter();
|
||
|
|
||
|
CTakeDamageInfo info( me, me, 4 * damage, DMG_BLAST, TF_DMG_CUSTOM_NONE );
|
||
|
CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f );
|
||
|
m_lastTouch->TakeDamage( info );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
UTIL_LogPrintf( "\"%s<%i><%s><%s>\" stuck (position \"%3.2f %3.2f %3.2f\") (duration \"%3.2f\") ",
|
||
|
me->GetPlayerName(),
|
||
|
me->GetUserID(),
|
||
|
me->GetNetworkIDString(),
|
||
|
me->GetTeam()->GetName(),
|
||
|
me->GetAbsOrigin().x, me->GetAbsOrigin().y, me->GetAbsOrigin().z,
|
||
|
me->GetLocomotionInterface()->GetStuckDuration() );
|
||
|
|
||
|
const PathFollower *path = me->GetCurrentPath();
|
||
|
if ( path && path->GetCurrentGoal() )
|
||
|
{
|
||
|
UTIL_LogPrintf( " path_goal ( \"%3.2f %3.2f %3.2f\" )\n",
|
||
|
path->GetCurrentGoal()->pos.x,
|
||
|
path->GetCurrentGoal()->pos.y,
|
||
|
path->GetCurrentGoal()->pos.z );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
UTIL_LogPrintf( " path_goal ( \"NULL\" )\n" );
|
||
|
}
|
||
|
|
||
|
me->GetLocomotionInterface()->Jump();
|
||
|
|
||
|
if ( RandomInt( 0, 100 ) < 50 )
|
||
|
{
|
||
|
me->PressLeftButton();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
me->PressRightButton();
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
if ( me->GetLocomotionInterface()->GetStuckDuration() > 3.0f )
|
||
|
{
|
||
|
// stuck for too long, do something drastic
|
||
|
// warp to the our next path goal
|
||
|
if ( me->GetCurrentPath() && me->GetCurrentPath()->GetCurrentGoal() )
|
||
|
{
|
||
|
me->SetAbsOrigin( me->GetCurrentPath()->GetCurrentGoal()->pos + Vector( 0, 0, StepHeight ) );
|
||
|
|
||
|
UTIL_LogPrintf( "%3.2f: TFBot '%s' stuck for too long - slammed to goal position. Entindex = %d.\n", gpGlobals->curtime, me->GetPlayerName(), me->entindex() );
|
||
|
}
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
return TryContinue();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
EventDesiredResult< CTFBot > CTFBotMainAction::OnOtherKilled( CTFBot *me, CBaseCombatCharacter *victim, const CTakeDamageInfo &info )
|
||
|
{
|
||
|
// make sure we forget about this guy
|
||
|
me->GetVisionInterface()->ForgetEntity( victim );
|
||
|
|
||
|
bool do_taunt = victim && victim->IsPlayer();
|
||
|
|
||
|
#ifdef STAGING_ONLY
|
||
|
if ( !do_taunt )
|
||
|
{
|
||
|
// If bots are using items, go ahead and let bots taunt other bots.
|
||
|
do_taunt = victim && tf_bot_use_items.GetBool();
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
if ( do_taunt )
|
||
|
{
|
||
|
CTFPlayer *playerVictim = ToTFPlayer( victim );
|
||
|
|
||
|
me->ForgetSpy( playerVictim );
|
||
|
|
||
|
if ( me->IsSelf( info.GetAttacker() ) && me->IsPlayerClass( TF_CLASS_SPY ) )
|
||
|
{
|
||
|
// disguise as our victim
|
||
|
m_nextDisguise = playerVictim->GetPlayerClass()->GetClassIndex();
|
||
|
}
|
||
|
|
||
|
if ( !ToTFPlayer( victim )->IsBot() && me->IsEnemy( victim ) && me->IsSelf( info.GetAttacker() ) )
|
||
|
{
|
||
|
bool isTaunting = !me->HasTheFlag() && RandomFloat( 0.0f, 100.0f ) <= tf_bot_taunt_victim_chance.GetFloat();
|
||
|
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() && me->IsMiniBoss() )
|
||
|
{
|
||
|
// Bosses don't taunt puny humans
|
||
|
isTaunting = false;
|
||
|
}
|
||
|
|
||
|
if ( isTaunting )
|
||
|
{
|
||
|
// we just killed a human - taunt!
|
||
|
return TrySuspendFor( new CTFBotTaunt, RESULT_IMPORTANT, "Taunting our victim" );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if we saw a friend killed by a sentry, kill the sentry
|
||
|
if ( victim && victim->IsPlayer() && me->IsFriend( victim ) && info.GetInflictor() && me->IsEnemy( info.GetInflictor() ) && me->IsLineOfSightClear( victim->WorldSpaceCenter() ) )
|
||
|
{
|
||
|
CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
|
||
|
|
||
|
if ( sentry && !me->GetEnemySentry() )
|
||
|
{
|
||
|
me->RememberEnemySentry( sentry, victim->GetAbsOrigin() );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return TryContinue();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
/**
|
||
|
* Given a subject, return the world space position we should aim at
|
||
|
*/
|
||
|
Vector CTFBotMainAction::SelectTargetPoint( const INextBot *meBot, const CBaseCombatCharacter *subject ) const
|
||
|
{
|
||
|
CTFBot *me = (CTFBot *)meBot->GetEntity();
|
||
|
|
||
|
if ( subject )
|
||
|
{
|
||
|
// if our subject is a sentry gun, aim at it's "eye position", which is updated based on the sentry's level
|
||
|
if ( subject->IsBaseObject() )
|
||
|
{
|
||
|
CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( const_cast< CBaseCombatCharacter * >( subject ) );
|
||
|
if ( sentry )
|
||
|
{
|
||
|
// Aim a bit lower than eye height to ensure we hit the body of the sentry
|
||
|
return sentry->GetAbsOrigin() + 0.5f * sentry->GetViewOffset();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
|
||
|
if ( myWeapon )
|
||
|
{
|
||
|
// lead our target and aim for the feet with the rocket launcher
|
||
|
if ( !me->IsDifficulty( CTFBot::EASY ) )
|
||
|
{
|
||
|
if ( myWeapon->GetWeaponID() == TF_WEAPON_ROCKETLAUNCHER )
|
||
|
{
|
||
|
// if they are above us, don't aim for the feet
|
||
|
const float aboveTolerance = 30.0f;
|
||
|
if ( subject->GetAbsOrigin().z - aboveTolerance > me->GetAbsOrigin().z )
|
||
|
{
|
||
|
if ( me->GetVisionInterface()->IsAbleToSee( subject->GetAbsOrigin(), IVision::DISREGARD_FOV ) )
|
||
|
return subject->GetAbsOrigin();
|
||
|
|
||
|
if ( me->GetVisionInterface()->IsAbleToSee( subject->WorldSpaceCenter(), IVision::DISREGARD_FOV ) )
|
||
|
return subject->WorldSpaceCenter();
|
||
|
|
||
|
return subject->EyePosition();
|
||
|
}
|
||
|
|
||
|
// aim at the ground under the subject
|
||
|
if ( subject->GetGroundEntity() == NULL )
|
||
|
{
|
||
|
// they are airborne, find the ground underneath them, if they aren't too high
|
||
|
trace_t result;
|
||
|
UTIL_TraceLine( subject->GetAbsOrigin(), subject->GetAbsOrigin() + Vector( 0, 0, -200 ), MASK_SOLID, subject, COLLISION_GROUP_NONE, &result );
|
||
|
if ( result.DidHit() )
|
||
|
{
|
||
|
return result.endpos;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// aim at their feet
|
||
|
|
||
|
// lead our target
|
||
|
const float missileSpeed = 1100.0f;
|
||
|
float rangeBetween = me->GetRangeTo( subject->GetAbsOrigin() );
|
||
|
|
||
|
const float veryCloseRange = 150.0f;
|
||
|
if ( rangeBetween > veryCloseRange )
|
||
|
{
|
||
|
float timeToTravel = rangeBetween / missileSpeed;
|
||
|
|
||
|
Vector targetPos = subject->GetAbsOrigin() + timeToTravel * subject->GetAbsVelocity();
|
||
|
|
||
|
if ( me->GetVisionInterface()->IsAbleToSee( targetPos, IVision::DISREGARD_FOV ) )
|
||
|
return targetPos;
|
||
|
|
||
|
// try their head and hope
|
||
|
return subject->EyePosition() + timeToTravel * subject->GetAbsVelocity();
|
||
|
}
|
||
|
|
||
|
return subject->EyePosition();
|
||
|
}
|
||
|
else if ( myWeapon->GetWeaponID() == TF_WEAPON_COMPOUND_BOW )
|
||
|
{
|
||
|
// lead our target
|
||
|
const float missileSpeed = ( (CTFCompoundBow *)myWeapon )->GetProjectileSpeed();
|
||
|
float rangeBetween = me->GetRangeTo( subject->GetAbsOrigin() );
|
||
|
|
||
|
const float veryCloseRange = 150.0f;
|
||
|
if ( rangeBetween > veryCloseRange )
|
||
|
{
|
||
|
float timeToTravel = rangeBetween / missileSpeed;
|
||
|
|
||
|
Vector targetSpot = me->IsDifficulty( CTFBot::NORMAL ) ? subject->WorldSpaceCenter() : subject->EyePosition();
|
||
|
|
||
|
Vector leadTargetSpot = targetSpot + timeToTravel * subject->GetAbsVelocity();
|
||
|
|
||
|
// elevate our aim based on range
|
||
|
float elevationAngle = rangeBetween * tf_bot_arrow_elevation_rate.GetFloat();
|
||
|
|
||
|
if ( elevationAngle > 45.0f )
|
||
|
{
|
||
|
// ballistic range maximum at 45 degrees - aiming higher would decrease the range
|
||
|
elevationAngle = 45.0f;
|
||
|
}
|
||
|
|
||
|
float s, c;
|
||
|
FastSinCos( elevationAngle * M_PI / 180.0f, &s, &c );
|
||
|
|
||
|
if ( c > 0.0f )
|
||
|
{
|
||
|
float elevation = rangeBetween * s / c;
|
||
|
return leadTargetSpot + Vector( 0, 0, elevation );
|
||
|
}
|
||
|
|
||
|
return leadTargetSpot;
|
||
|
}
|
||
|
|
||
|
return subject->EyePosition();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
|
||
|
{
|
||
|
if ( m_aimAdjustTimer.IsElapsed() )
|
||
|
{
|
||
|
m_aimAdjustTimer.Start( RandomFloat( 0.5f, 1.5f ) );
|
||
|
|
||
|
m_aimErrorAngle = RandomFloat( -M_PI, M_PI );
|
||
|
m_aimErrorRadius = RandomFloat( 0.0f, tf_bot_sniper_aim_error.GetFloat() );
|
||
|
}
|
||
|
|
||
|
Vector toThreat = subject->GetAbsOrigin() - me->GetAbsOrigin();
|
||
|
float threatRange = toThreat.NormalizeInPlace();
|
||
|
|
||
|
float s1, c1;
|
||
|
FastSinCos( m_aimErrorRadius, &s1, &c1 );
|
||
|
|
||
|
float error = threatRange * s1;
|
||
|
|
||
|
Vector up( 0, 0, 1 );
|
||
|
Vector side;
|
||
|
CrossProduct( toThreat, up, side );
|
||
|
|
||
|
float s, c;
|
||
|
FastSinCos( m_aimErrorAngle, &s, &c );
|
||
|
|
||
|
// aim a bit lower than the head - the imperfections may yet give us a headshot
|
||
|
Vector desiredAimSpot;
|
||
|
|
||
|
switch( me->GetDifficulty() )
|
||
|
{
|
||
|
case CTFBot::EXPERT:
|
||
|
case CTFBot::HARD:
|
||
|
// aim for the head - reaction times will differentiate the skill levels
|
||
|
desiredAimSpot = subject->EyePosition();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
Assert(0);
|
||
|
case CTFBot::NORMAL:
|
||
|
desiredAimSpot = ( subject->EyePosition() + subject->EyePosition() + subject->WorldSpaceCenter() ) / 3.0f;
|
||
|
break;
|
||
|
|
||
|
case CTFBot::EASY:
|
||
|
desiredAimSpot = subject->WorldSpaceCenter();
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
Vector imperfectAimSpot = desiredAimSpot + error * s * up + error * c * side;
|
||
|
|
||
|
return imperfectAimSpot;
|
||
|
}
|
||
|
|
||
|
if ( myWeapon->IsWeapon( TF_WEAPON_GRENADELAUNCHER ) ||
|
||
|
myWeapon->IsWeapon( TF_WEAPON_PIPEBOMBLAUNCHER ) )
|
||
|
{
|
||
|
Vector toThreat = subject->GetAbsOrigin() - me->GetAbsOrigin();
|
||
|
float threatRange = toThreat.NormalizeInPlace();
|
||
|
float elevationAngle = threatRange * tf_bot_ballistic_elevation_rate.GetFloat();
|
||
|
|
||
|
if ( elevationAngle > 45.0f )
|
||
|
{
|
||
|
// ballistic range maximum at 45 degrees - aiming higher would decrease the range
|
||
|
elevationAngle = 45.0f;
|
||
|
}
|
||
|
|
||
|
float s, c;
|
||
|
FastSinCos( elevationAngle * M_PI / 180.0f, &s, &c );
|
||
|
|
||
|
if ( c > 0.0f )
|
||
|
{
|
||
|
float elevation = threatRange * s / c;
|
||
|
return subject->WorldSpaceCenter() + Vector( 0, 0, elevation );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// aim for the center of the object (ie: sentry gun)
|
||
|
return subject->WorldSpaceCenter();
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
/**
|
||
|
* Allow bot to approve of positions game movement tries to put him into.
|
||
|
* This is most useful for bots derived from CBasePlayer that go through
|
||
|
* the player movement system.
|
||
|
*/
|
||
|
QueryResultType CTFBotMainAction::IsPositionAllowed( const INextBot *me, const Vector &pos ) const
|
||
|
{
|
||
|
return ANSWER_YES;
|
||
|
|
||
|
// This is causing bots to get hung up on drop-downs, particularly in MvM. MSB 6/11/2012
|
||
|
/*
|
||
|
if ( me->GetLocomotionInterface()->IsScrambling() )
|
||
|
{
|
||
|
// anything goes when we're in the air/etc
|
||
|
return ANSWER_YES;
|
||
|
}
|
||
|
|
||
|
// if we are at a DROP_DOWN segment of our path, allow us to drop
|
||
|
const PathFollower *path = me->GetCurrentPath();
|
||
|
if ( path && path->IsValid() )
|
||
|
{
|
||
|
const Path::Segment *goal = path->GetCurrentGoal();
|
||
|
if ( goal )
|
||
|
{
|
||
|
if ( goal->type == Path::DROP_DOWN || me->GetLocomotionInterface()->GetFeet().z - goal->pos.z >= me->GetLocomotionInterface()->GetMaxJumpHeight() )
|
||
|
{
|
||
|
// our goal requires us to drop down
|
||
|
return ANSWER_YES;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// do not fall off someplace we can't get back up from!
|
||
|
trace_t result;
|
||
|
NextBotTraceFilterIgnoreActors filter( me->GetEntity(), COLLISION_GROUP_PLAYER_MOVEMENT );
|
||
|
ILocomotion *mover = me->GetLocomotionInterface();
|
||
|
IBody *body = me->GetBodyInterface();
|
||
|
|
||
|
// slightly smaller to allow skirting the edge
|
||
|
float halfWidth = 0.4f * body->GetHullWidth();
|
||
|
|
||
|
mover->TraceHull( pos + Vector( 0, 0, mover->GetStepHeight() ), // start up a bit to handle rough terrain
|
||
|
pos + Vector( 0, 0, -mover->GetMaxJumpHeight() ),
|
||
|
Vector( -halfWidth, -halfWidth, 0 ),
|
||
|
Vector( halfWidth, halfWidth, body->GetHullHeight() ),
|
||
|
body->GetSolidMask(),
|
||
|
&filter,
|
||
|
&result );
|
||
|
|
||
|
if ( result.DidHit() )
|
||
|
{
|
||
|
// there is ground safe beneath us
|
||
|
return ANSWER_YES;
|
||
|
}
|
||
|
|
||
|
return ANSWER_NO;
|
||
|
*/
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
bool CTFBotMainAction::IsImmediateThreat( const CBaseCombatCharacter *subject, const CKnownEntity *threat ) const
|
||
|
{
|
||
|
CTFBot *me = GetActor();
|
||
|
|
||
|
// the TFBot code assumes the subject is always "me"
|
||
|
if ( !me || !me->IsSelf( subject ) )
|
||
|
return false;
|
||
|
|
||
|
if ( me->InSameTeam( threat->GetEntity() ) )
|
||
|
return false;
|
||
|
|
||
|
if ( !threat->GetEntity()->IsAlive() )
|
||
|
return false;
|
||
|
|
||
|
if ( !threat->IsVisibleRecently() )
|
||
|
return false;
|
||
|
|
||
|
// if they can't hurt me, they aren't an immediate threat
|
||
|
if ( !me->IsLineOfFireClear( threat->GetEntity() ) )
|
||
|
return false;
|
||
|
|
||
|
CTFPlayer *threatPlayer = ToTFPlayer( threat->GetEntity() );
|
||
|
|
||
|
Vector to = me->GetAbsOrigin() - threat->GetLastKnownPosition();
|
||
|
float threatRange = to.NormalizeInPlace();
|
||
|
|
||
|
const float nearbyRange = 500.0f;
|
||
|
if ( threatRange < nearbyRange )
|
||
|
{
|
||
|
// very near threats are always immediately dangerous
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// mid-to-far away threats
|
||
|
|
||
|
if ( me->IsThreatFiringAtMe( threat->GetEntity() ) )
|
||
|
{
|
||
|
// distant threat firing on me - an immediate threat whether in my FOV or not
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if ( threatPlayer == NULL )
|
||
|
{
|
||
|
// non-player threat - sentry guns
|
||
|
|
||
|
CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( threat->GetEntity() );
|
||
|
if ( sentry && !sentry->HasSapper() && !sentry->IsPlasmaDisabled() && !sentry->IsPlacing() )
|
||
|
{
|
||
|
// are we in range? (or will be very soon)
|
||
|
if ( threatRange < 1.5f * SENTRY_MAX_RANGE )
|
||
|
{
|
||
|
// is it pointing at us?
|
||
|
Vector sentryForward;
|
||
|
AngleVectors( sentry->GetTurretAngles(), &sentryForward );
|
||
|
|
||
|
if ( DotProduct( to, sentryForward ) > 0.8f )
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// does a sniper have a shot on me?
|
||
|
if ( threatPlayer->IsPlayerClass( TF_CLASS_SNIPER ) )
|
||
|
{
|
||
|
Vector sniperForward;
|
||
|
threatPlayer->EyeVectors( &sniperForward );
|
||
|
|
||
|
if ( DotProduct( to, sniperForward ) > 0.0f )
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if ( me->GetDifficulty() > CTFBot::NORMAL && threatPlayer->IsPlayerClass( TF_CLASS_MEDIC ) )
|
||
|
{
|
||
|
// always try to kill these guys first
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if ( me->GetDifficulty() > CTFBot::NORMAL && threatPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) )
|
||
|
{
|
||
|
// take out engineers to let the team kill their sentry nests
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
const CKnownEntity *CTFBotMainAction::SelectCloserThreat( CTFBot *me, const CKnownEntity *threat1, const CKnownEntity *threat2 ) const
|
||
|
{
|
||
|
float rangeSq1 = me->GetRangeSquaredTo( threat1->GetEntity() );
|
||
|
float rangeSq2 = me->GetRangeSquaredTo( threat2->GetEntity() );
|
||
|
|
||
|
if ( rangeSq1 < rangeSq2 )
|
||
|
return threat1;
|
||
|
|
||
|
return threat2;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
// If the given threat is being healed by a Medic, return the Medic, otherwise just
|
||
|
// return the threat.
|
||
|
const CKnownEntity *CTFBotMainAction::GetHealerOfThreat( const CKnownEntity *threat ) const
|
||
|
{
|
||
|
if ( !threat || !threat->GetEntity() )
|
||
|
return NULL;
|
||
|
|
||
|
CTFPlayer *playerThreat = ToTFPlayer( threat->GetEntity() );
|
||
|
if ( playerThreat )
|
||
|
{
|
||
|
for( int i=0; i<playerThreat->m_Shared.GetNumHealers(); ++i )
|
||
|
{
|
||
|
CBaseEntity *healer = playerThreat->m_Shared.GetHealerByIndex( i );
|
||
|
CTFPlayer *playerHealer = ToTFPlayer( healer );
|
||
|
|
||
|
if ( playerHealer )
|
||
|
{
|
||
|
const CKnownEntity *knownHealer = GetActor()->GetVisionInterface()->GetKnown( playerHealer );
|
||
|
|
||
|
if ( knownHealer && knownHealer->IsVisibleInFOVNow() )
|
||
|
{
|
||
|
return knownHealer;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return threat;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
// return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
|
||
|
const CKnownEntity *CTFBotMainAction::SelectMoreDangerousThreat( const INextBot *meBot,
|
||
|
const CBaseCombatCharacter *subject,
|
||
|
const CKnownEntity *threat1,
|
||
|
const CKnownEntity *threat2 ) const
|
||
|
{
|
||
|
CTFBot *me = ToTFBot( meBot->GetEntity() );
|
||
|
|
||
|
// determine the actual threat
|
||
|
const CKnownEntity *threat = SelectMoreDangerousThreatInternal( me, subject, threat1, threat2 );
|
||
|
|
||
|
if ( me->IsDifficulty( CTFBot::EASY ) )
|
||
|
{
|
||
|
return threat;
|
||
|
}
|
||
|
|
||
|
if ( me->IsDifficulty( CTFBot::NORMAL ) && me->TransientlyConsistentRandomValue() < 0.5f )
|
||
|
{
|
||
|
return threat;
|
||
|
}
|
||
|
|
||
|
// smarter bots first aim at the Medic healing our dangerous target
|
||
|
return GetHealerOfThreat( threat );
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
// Given a pair of enemy players, return the closest Spy of those two, or NULL if neither is a Spy
|
||
|
const CKnownEntity *SelectClosestSpyToMe( CTFBot *me, const CKnownEntity *threat1, const CKnownEntity *threat2 )
|
||
|
{
|
||
|
CTFPlayer *playerThreat1 = ToTFPlayer( threat1->GetEntity() );
|
||
|
CTFPlayer *playerThreat2 = ToTFPlayer( threat2->GetEntity() );
|
||
|
|
||
|
if ( playerThreat1 && playerThreat1->IsPlayerClass( TF_CLASS_SPY ) )
|
||
|
{
|
||
|
if ( playerThreat2 && playerThreat2->IsPlayerClass( TF_CLASS_SPY ) )
|
||
|
{
|
||
|
if ( me->GetRangeSquaredTo( playerThreat1 ) < me->GetRangeSquaredTo( playerThreat2 ) )
|
||
|
return threat1;
|
||
|
|
||
|
return threat2;
|
||
|
}
|
||
|
|
||
|
return threat1;
|
||
|
}
|
||
|
else if ( playerThreat2 && playerThreat2->IsPlayerClass( TF_CLASS_SPY ) )
|
||
|
{
|
||
|
return threat2;
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
// Return the more dangerous of the two threats to 'subject', or NULL if we have no opinion
|
||
|
const CKnownEntity *CTFBotMainAction::SelectMoreDangerousThreatInternal( const INextBot *meBot,
|
||
|
const CBaseCombatCharacter *subject,
|
||
|
const CKnownEntity *threat1,
|
||
|
const CKnownEntity *threat2 ) const
|
||
|
{
|
||
|
CTFBot *me = ToTFBot( meBot->GetEntity() );
|
||
|
const CKnownEntity *closerThreat = SelectCloserThreat( me, threat1, threat2 );
|
||
|
|
||
|
if ( me->HasWeaponRestriction( CTFBot::MELEE_ONLY ) )
|
||
|
{
|
||
|
// melee only bots just use closest threat
|
||
|
return closerThreat;
|
||
|
}
|
||
|
|
||
|
// close range sentries are the most dangerous of all
|
||
|
bool shouldFearSentryGuns = true;
|
||
|
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() )
|
||
|
{
|
||
|
// MvM bots are not afraid of sentry guns and treat them like other enemy players
|
||
|
shouldFearSentryGuns = false;
|
||
|
}
|
||
|
|
||
|
if ( shouldFearSentryGuns )
|
||
|
{
|
||
|
CObjectSentrygun *sentry1 = NULL;
|
||
|
if ( threat1->IsVisibleRecently() && !threat1->GetEntity()->IsPlayer() )
|
||
|
{
|
||
|
sentry1 = dynamic_cast< CObjectSentrygun * >( threat1->GetEntity() );
|
||
|
}
|
||
|
|
||
|
CObjectSentrygun *sentry2 = NULL;
|
||
|
if ( threat2->IsVisibleRecently() && !threat2->GetEntity()->IsPlayer() )
|
||
|
{
|
||
|
sentry2 = dynamic_cast< CObjectSentrygun * >( threat2->GetEntity() );
|
||
|
}
|
||
|
|
||
|
if ( sentry1 && me->IsRangeLessThan( sentry1, SENTRY_MAX_RANGE ) && !sentry1->HasSapper() && !sentry1->IsPlasmaDisabled() && !sentry1->IsPlacing() )
|
||
|
{
|
||
|
// in range of a visible sentry!
|
||
|
if ( sentry2 && me->IsRangeLessThan( sentry2, SENTRY_MAX_RANGE ) && !sentry2->HasSapper() && !sentry2->IsPlasmaDisabled() && !sentry2->IsPlacing() )
|
||
|
{
|
||
|
// in range of two visible sentries! we're probably dead meat at this point.
|
||
|
// default is choose closest
|
||
|
return closerThreat;
|
||
|
}
|
||
|
|
||
|
return threat1;
|
||
|
}
|
||
|
|
||
|
if ( sentry2 && me->IsRangeLessThan( sentry2, SENTRY_MAX_RANGE ) && !sentry2->HasSapper() && !sentry2->IsPlasmaDisabled() && !sentry2->IsPlacing() )
|
||
|
{
|
||
|
// in range of a visible sentry!
|
||
|
return threat2;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// enforce Spy hatred in MvM mode
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() )
|
||
|
{
|
||
|
const float spyHateRadius = 1000.0f;
|
||
|
|
||
|
const CKnownEntity *spyThreat = SelectClosestSpyToMe( me, threat1, threat2 );
|
||
|
if ( spyThreat && me->IsRangeLessThan( spyThreat->GetEntity(), spyHateRadius ) )
|
||
|
{
|
||
|
return spyThreat;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
bool isImmediateThreat1 = IsImmediateThreat( subject, threat1 );
|
||
|
bool isImmediateThreat2 = IsImmediateThreat( subject, threat2 );
|
||
|
|
||
|
if ( isImmediateThreat1 && !isImmediateThreat2 )
|
||
|
{
|
||
|
return threat1;
|
||
|
}
|
||
|
else if ( !isImmediateThreat1 && isImmediateThreat2 )
|
||
|
{
|
||
|
return threat2;
|
||
|
}
|
||
|
else if ( !isImmediateThreat1 && !isImmediateThreat2 )
|
||
|
{
|
||
|
// neither threat is immediately dangerous - use closest
|
||
|
return closerThreat;
|
||
|
}
|
||
|
|
||
|
// both threats are immediately dangerous!
|
||
|
// check if any are extremely dangerous
|
||
|
|
||
|
const CKnownEntity *spyThreat = SelectClosestSpyToMe( me, threat1, threat2 );
|
||
|
if ( spyThreat )
|
||
|
{
|
||
|
return spyThreat;
|
||
|
}
|
||
|
|
||
|
// choose most recent attacker (assume an enemy firing their weapon at us has attacked us)
|
||
|
if ( me->IsThreatFiringAtMe( threat1->GetEntity() ) )
|
||
|
{
|
||
|
if ( me->IsThreatFiringAtMe( threat2->GetEntity() ) )
|
||
|
{
|
||
|
// choose closest
|
||
|
return closerThreat;
|
||
|
}
|
||
|
|
||
|
return threat1;
|
||
|
}
|
||
|
else if ( me->IsThreatFiringAtMe( threat2->GetEntity() ) )
|
||
|
{
|
||
|
return threat2;
|
||
|
}
|
||
|
|
||
|
// choose closest
|
||
|
return closerThreat;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
QueryResultType CTFBotMainAction::ShouldAttack( const INextBot *meBot, const CKnownEntity *them ) const
|
||
|
{
|
||
|
if ( g_pPopulationManager )
|
||
|
{
|
||
|
// if I'm in my spawn room, obey the population manager's attack restrictions
|
||
|
CTFBot *me = ToTFBot( meBot->GetEntity() );
|
||
|
CTFNavArea *myArea = me->GetLastKnownArea();
|
||
|
int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
|
||
|
|
||
|
if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
|
||
|
{
|
||
|
return g_pPopulationManager->CanBotsAttackWhileInSpawnRoom() ? ANSWER_YES : ANSWER_NO;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ANSWER_YES;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
QueryResultType CTFBotMainAction::ShouldHurry( const INextBot *meBot ) const
|
||
|
{
|
||
|
if ( g_pPopulationManager )
|
||
|
{
|
||
|
// if I'm in my spawn room, obey the population manager's attack restrictions
|
||
|
CTFBot *me = ToTFBot( meBot->GetEntity() );
|
||
|
CTFNavArea *myArea = me->GetLastKnownArea();
|
||
|
int spawnRoomFlag = me->GetTeamNumber() == TF_TEAM_RED ? TF_NAV_SPAWN_ROOM_RED : TF_NAV_SPAWN_ROOM_BLUE;
|
||
|
|
||
|
if ( myArea && myArea->HasAttributeTF( spawnRoomFlag ) )
|
||
|
{
|
||
|
if ( !g_pPopulationManager->CanBotsAttackWhileInSpawnRoom() )
|
||
|
{
|
||
|
// hurry to leave the spawn
|
||
|
return ANSWER_YES;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ANSWER_UNDEFINED;
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
void CTFBotMainAction::FireWeaponAtEnemy( CTFBot *me )
|
||
|
{
|
||
|
if ( !me->IsAlive() )
|
||
|
return;
|
||
|
|
||
|
if ( me->HasAttribute( CTFBot::SUPPRESS_FIRE ) )
|
||
|
return;
|
||
|
|
||
|
if ( me->HasAttribute( CTFBot::IGNORE_ENEMIES ) )
|
||
|
return;
|
||
|
|
||
|
if ( me->m_Shared.InCond( TF_COND_TAUNTING ) )
|
||
|
return;
|
||
|
|
||
|
if ( !tf_bot_fire_weapon_allowed.GetBool() )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CTFWeaponBase *myWeapon = me->m_Shared.GetActiveTFWeapon();
|
||
|
if ( !myWeapon )
|
||
|
return;
|
||
|
|
||
|
if ( me->IsBarrageAndReloadWeapon( myWeapon ) )
|
||
|
{
|
||
|
if ( me->HasAttribute( CTFBot::HOLD_FIRE_UNTIL_FULL_RELOAD ) || tf_bot_always_full_reload.GetBool() )
|
||
|
{
|
||
|
if ( myWeapon->Clip1() <= 0 )
|
||
|
{
|
||
|
m_isWaitingForFullReload = true;
|
||
|
}
|
||
|
|
||
|
if ( m_isWaitingForFullReload )
|
||
|
{
|
||
|
if ( myWeapon->Clip1() < myWeapon->GetMaxClip1() )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// we are fully reloaded
|
||
|
m_isWaitingForFullReload = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( me->HasAttribute( CTFBot::ALWAYS_FIRE_WEAPON ) )
|
||
|
{
|
||
|
me->PressFireButton();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ( me->IsPlayerClass( TF_CLASS_MEDIC ) )
|
||
|
{
|
||
|
if ( myWeapon && myWeapon->IsWeapon( TF_WEAPON_MEDIGUN ) )
|
||
|
{
|
||
|
// don't interfere with medic healing behaviors
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if we're a heavy and just saw a bad guy, keep the barrel spinning (unless we're in a hurry)
|
||
|
if ( me->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && !me->IsAmmoLow() && me->GetIntentionInterface()->ShouldHurry( me ) != ANSWER_YES )
|
||
|
{
|
||
|
const float spinTime = 3.0f;
|
||
|
if ( me->GetVisionInterface()->GetTimeSinceVisible( GetEnemyTeam( me->GetTeamNumber() ) ) < spinTime )
|
||
|
{
|
||
|
me->PressAltFireButton();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// shoot at bad guys
|
||
|
const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
|
||
|
|
||
|
// ignore non-visible threats here so we don't force a premature weapon switch if we're doing something else
|
||
|
if ( threat == NULL || !threat->GetEntity() || !threat->IsVisibleRecently() )
|
||
|
return;
|
||
|
|
||
|
// don't shoot through windows/etc
|
||
|
if ( !me->IsLineOfFireClear( threat->GetEntity()->EyePosition() ) )
|
||
|
{
|
||
|
if ( !me->IsLineOfFireClear( threat->GetEntity()->WorldSpaceCenter() ) )
|
||
|
{
|
||
|
if ( !me->IsLineOfFireClear( threat->GetEntity()->GetAbsOrigin() ) )
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if our target is uber'd, most weapons are useless - unless we're in MvM, where invuln tanking is valuable
|
||
|
if ( TFGameRules() && !TFGameRules()->IsMannVsMachineMode() )
|
||
|
{
|
||
|
CTFPlayer *playerThreat = ToTFPlayer( threat->GetEntity() );
|
||
|
if ( playerThreat && playerThreat->m_Shared.IsInvulnerable() )
|
||
|
{
|
||
|
if ( !myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER ) &&
|
||
|
!myWeapon->IsWeapon( TF_WEAPON_GRENADELAUNCHER ) &&
|
||
|
!myWeapon->IsWeapon( TF_WEAPON_PIPEBOMBLAUNCHER ) &
|
||
|
!myWeapon->IsWeapon( TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT ) )
|
||
|
{
|
||
|
// firing would just waste ammo, so don't
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( me->GetIntentionInterface()->ShouldAttack( me, threat ) == ANSWER_NO )
|
||
|
return;
|
||
|
|
||
|
if ( TFGameRules()->InSetup() )
|
||
|
{
|
||
|
// wait until the gates open
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ( myWeapon->IsMeleeWeapon() )
|
||
|
{
|
||
|
if ( me->IsRangeLessThan( threat->GetEntity(), 250.0f ) )
|
||
|
{
|
||
|
me->PressFireButton();
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// limit range of hitscan weapon fire in MvM
|
||
|
if ( TFGameRules()->IsMannVsMachineMode() && !me->IsPlayerClass( TF_CLASS_SNIPER ) && me->IsHitScanWeapon( myWeapon ) )
|
||
|
{
|
||
|
if ( me->IsRangeGreaterThan( threat->GetEntity(), tf_bot_hitscan_range_limit.GetFloat() ) )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( myWeapon->IsWeapon( TF_WEAPON_FLAMETHROWER ) )
|
||
|
{
|
||
|
CTFFlameThrower *pFlamethrower = assert_cast< CTFFlameThrower* >( myWeapon );
|
||
|
// watch for enemy projectiles heading our way
|
||
|
if ( pFlamethrower->CanAirBlast() && me->ShouldFireCompressionBlast() )
|
||
|
{
|
||
|
// bounce missiles with compression blast
|
||
|
me->PressAltFireButton();
|
||
|
}
|
||
|
else if ( threat->GetTimeSinceLastSeen() < 1.0f &&
|
||
|
me->IsDistanceBetweenLessThan( threat->GetEntity(), me->GetMaxAttackRange() ) )
|
||
|
{
|
||
|
me->PressFireButton( tf_bot_fire_weapon_min_time.GetFloat() );
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
float threatRange = ( threat->GetEntity()->GetAbsOrigin() - me->GetAbsOrigin() ).Length();
|
||
|
|
||
|
// actual head aiming is handled elsewhere, just check if we're on target
|
||
|
if ( me->GetBodyInterface()->IsHeadAimingOnTarget() && threatRange < me->GetMaxAttackRange() )
|
||
|
{
|
||
|
if ( myWeapon->IsWeapon( TF_WEAPON_COMPOUND_BOW ) )
|
||
|
{
|
||
|
CTFCompoundBow *myBow = (CTFCompoundBow *)myWeapon;
|
||
|
|
||
|
if ( myBow->GetCurrentCharge() < 0.95f || !me->IsLineOfFireClear( threat->GetEntity() ) )
|
||
|
{
|
||
|
me->PressFireButton();
|
||
|
}
|
||
|
}
|
||
|
else if ( WeaponID_IsSniperRifle( myWeapon->GetWeaponID() ) )
|
||
|
{
|
||
|
// only fire if zoomed in
|
||
|
if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
|
||
|
{
|
||
|
const float reactionTime = TFGameRules()->IsMannVsMachineMode() ? 0.5f : 0.1f; // just a moment to stop headshots when obviously panning too fast to see
|
||
|
if ( m_steadyTimer.HasStarted() && m_steadyTimer.IsGreaterThen( reactionTime ) )
|
||
|
{
|
||
|
trace_t trace;
|
||
|
|
||
|
Vector forward;
|
||
|
me->EyeVectors( &forward );
|
||
|
|
||
|
// allow bot to see through projectile shield
|
||
|
CTraceFilterIgnoreFriendlyCombatItems filter( me, COLLISION_GROUP_NONE, me->GetTeamNumber() );
|
||
|
UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 9000.0f * forward, MASK_SHOT, &filter, &trace );
|
||
|
|
||
|
if ( trace.m_pEnt == threat->GetEntity() )
|
||
|
{
|
||
|
// we're on target - fire!
|
||
|
me->PressFireButton();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if ( me->IsCombatWeapon( MY_CURRENT_GUN ) )
|
||
|
{
|
||
|
if ( me->IsContinuousFireWeapon( MY_CURRENT_GUN ) )
|
||
|
{
|
||
|
// spray for a bit
|
||
|
me->PressFireButton( tf_bot_fire_weapon_min_time.GetFloat() );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if ( me->IsExplosiveProjectileWeapon( MY_CURRENT_GUN ) )
|
||
|
{
|
||
|
// don't fire if we're going to hit a nearby wall
|
||
|
trace_t trace;
|
||
|
|
||
|
Vector forward;
|
||
|
me->EyeVectors( &forward );
|
||
|
|
||
|
// allow bot to see through projectile shield
|
||
|
CTraceFilterIgnoreFriendlyCombatItems filter( me, COLLISION_GROUP_NONE, me->GetTeamNumber() );
|
||
|
UTIL_TraceLine( me->EyePosition(), me->EyePosition() + 1.1f * threatRange * forward, MASK_SHOT, &filter, &trace );
|
||
|
|
||
|
float hitRange = trace.fraction * 1.1f * threatRange;
|
||
|
|
||
|
if ( hitRange < TF_ROCKET_RADIUS )
|
||
|
{
|
||
|
// shot will impact very near us
|
||
|
if ( !trace.m_pEnt || ( trace.m_pEnt && !trace.m_pEnt->MyCombatCharacterPointer() ) )
|
||
|
{
|
||
|
// don't fire, we'd only hit the world or a non-player or non-sentry
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
me->PressFireButton();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
/**
|
||
|
* Compute nearby friends influence and visible enemy influence
|
||
|
*/
|
||
|
class CCompareFriendFoeInfluence : public IVision::IForEachKnownEntity
|
||
|
{
|
||
|
public:
|
||
|
CCompareFriendFoeInfluence( CTFBot *me )
|
||
|
{
|
||
|
m_me = me;
|
||
|
m_friendScore = 0;
|
||
|
m_foeScore = 0;
|
||
|
}
|
||
|
|
||
|
virtual bool Inspect( const CKnownEntity &known )
|
||
|
{
|
||
|
if ( known.GetEntity()->IsAlive() )
|
||
|
{
|
||
|
const float nearRange = 750.0f;
|
||
|
if ( m_me->IsRangeLessThan( known.GetEntity(), nearRange ) )
|
||
|
{
|
||
|
if ( m_me->IsFriend( known.GetEntity() ) )
|
||
|
{
|
||
|
m_friendScore += m_me->GetThreatDanger( known.GetEntity()->MyCombatCharacterPointer() );
|
||
|
}
|
||
|
else if ( known.WasEverVisible() && known.GetTimeSinceLastSeen() < 3.0f && m_me->IsEnemy( known.GetEntity() ) )
|
||
|
{
|
||
|
// ignore disguised spies, etc
|
||
|
if ( m_me->GetVisionInterface()->IsIgnored( known.GetEntity() ) )
|
||
|
return true;
|
||
|
|
||
|
// only count them if they are facing me
|
||
|
if ( UTIL_IsFacingWithinTolerance( known.GetEntity(), m_me->EyePosition(), 0.5f ) )
|
||
|
{
|
||
|
m_foeScore += m_me->GetThreatDanger( known.GetEntity()->MyCombatCharacterPointer() );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
CTFBot *m_me;
|
||
|
float m_friendScore;
|
||
|
float m_foeScore;
|
||
|
};
|
||
|
|
||
|
|
||
|
//---------------------------------------------------------------------------------------------
|
||
|
/**
|
||
|
* If we're outnumbered, retreat and wait for backup - unless we're ubered!
|
||
|
*/
|
||
|
QueryResultType CTFBotMainAction::ShouldRetreat( const INextBot *bot ) const
|
||
|
{
|
||
|
CTFBot *me = (CTFBot *)bot->GetEntity();
|
||
|
|
||
|
// don't retreat if we're in "melee only" mode
|
||
|
if ( TheTFBots().IsMeleeOnly() )
|
||
|
return ANSWER_NO;
|
||
|
|
||
|
// don't retreat if ubered
|
||
|
if ( me->m_Shared.IsInvulnerable() )
|
||
|
return ANSWER_NO;
|
||
|
|
||
|
// don't retreat if we're ignoring enemies
|
||
|
if ( me->HasAttribute( CTFBot::IGNORE_ENEMIES ) )
|
||
|
return ANSWER_NO;
|
||
|
|
||
|
// retreat if stunned
|
||
|
if ( me->m_Shared.IsControlStunned() || me->m_Shared.IsLoserStateStunned() )
|
||
|
return ANSWER_YES;
|
||
|
|
||
|
// don't retreat during setup time, since we're always safe
|
||
|
if ( TFGameRules()->InSetup() )
|
||
|
return ANSWER_NO;
|
||
|
|
||
|
// if we're an undercover spy, don't blow our cover
|
||
|
if ( me->IsPlayerClass( TF_CLASS_SPY ) )
|
||
|
{
|
||
|
if ( me->m_Shared.InCond( TF_COND_DISGUISED ) ||
|
||
|
me->m_Shared.InCond( TF_COND_DISGUISING ) ||
|
||
|
me->m_Shared.IsStealthed() )
|
||
|
{
|
||
|
return ANSWER_NO;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
CCompareFriendFoeInfluence compare( me );
|
||
|
me->GetVisionInterface()->ForEachKnownEntity( compare );
|
||
|
|
||
|
if ( compare.m_friendScore < compare.m_foeScore )
|
||
|
{
|
||
|
return ANSWER_YES;
|
||
|
}
|
||
|
|
||
|
return ANSWER_NO;
|
||
|
}
|
||
|
|
||
|
|
||
|
//-----------------------------------------------------------------------------------------
|
||
|
void CTFBotMainAction::Dodge( CTFBot *me )
|
||
|
{
|
||
|
// low-skill bots don't dodge
|
||
|
if ( me->IsDifficulty( CTFBot::EASY ) )
|
||
|
return;
|
||
|
|
||
|
// no need to dodge if we're invulnerable
|
||
|
if ( me->m_Shared.IsInvulnerable() )
|
||
|
return;
|
||
|
|
||
|
// don't dodge if we're trying to snipe
|
||
|
if ( me->m_Shared.InCond( TF_COND_ZOOMED ) )
|
||
|
return;
|
||
|
|
||
|
// don't dodge if we are taunting
|
||
|
if ( me->m_Shared.InCond( TF_COND_TAUNTING ) )
|
||
|
return;
|
||
|
|
||
|
// don't dodge if that ability is "turned off"
|
||
|
if ( me->HasAttribute( CTFBot::DISABLE_DODGE ) )
|
||
|
return;
|
||
|
|
||
|
// don't dodge if we're not trying to fight back
|
||
|
if ( !me->IsCombatWeapon( MY_CURRENT_GUN ) )
|
||
|
return;
|
||
|
|
||
|
// don't waste time doding if we're in a hurry
|
||
|
if ( me->GetIntentionInterface()->ShouldHurry( me ) == ANSWER_YES )
|
||
|
return;
|
||
|
|
||
|
// for now, engies don't dodge
|
||
|
if ( me->IsPlayerClass( TF_CLASS_ENGINEER ) )
|
||
|
return;
|
||
|
|
||
|
// disguised/cloaked spies don't dodge
|
||
|
if ( me->m_Shared.InCond( TF_COND_DISGUISED ) ||
|
||
|
me->m_Shared.InCond( TF_COND_DISGUISING ) ||
|
||
|
me->m_Shared.IsStealthed() )
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
#ifdef TF_RAID_MODE
|
||
|
if ( TFGameRules()->IsRaidMode() )
|
||
|
return;
|
||
|
#endif // TF_RAID_MODE
|
||
|
|
||
|
const CKnownEntity *threat = me->GetVisionInterface()->GetPrimaryKnownThreat();
|
||
|
if ( threat && threat->IsVisibleRecently() )
|
||
|
{
|
||
|
bool isShotClear = true;
|
||
|
|
||
|
CTFWeaponBase *myGun = (CTFWeaponBase *)me->Weapon_GetSlot( TF_WPN_TYPE_PRIMARY );
|
||
|
if ( myGun && myGun->IsWeapon( TF_WEAPON_COMPOUND_BOW ) )
|
||
|
{
|
||
|
CTFCompoundBow *myBow = (CTFCompoundBow *)myGun;
|
||
|
if ( myBow->GetCurrentCharge() > 0.0f )
|
||
|
{
|
||
|
// we're drawing back our bow - hold still
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// if we don't have a clear shot, dodge around until we do
|
||
|
isShotClear = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
isShotClear = me->IsLineOfFireClear( threat->GetLastKnownPosition() );
|
||
|
}
|
||
|
|
||
|
// don't dodge if they can't hit us
|
||
|
if ( !isShotClear )
|
||
|
return;
|
||
|
|
||
|
Vector forward;
|
||
|
me->EyeVectors( &forward );
|
||
|
Vector left( -forward.y, forward.x, 0.0f );
|
||
|
left.NormalizeInPlace();
|
||
|
|
||
|
const float sideStepSize = 25.0f;
|
||
|
|
||
|
int rnd = RandomInt( 0, 100 );
|
||
|
if ( rnd < 33 )
|
||
|
{
|
||
|
if ( !me->GetLocomotionInterface()->HasPotentialGap( me->GetAbsOrigin(), me->GetAbsOrigin() + sideStepSize * left ) )
|
||
|
{
|
||
|
me->PressLeftButton();
|
||
|
}
|
||
|
}
|
||
|
else if ( rnd > 66 )
|
||
|
{
|
||
|
if ( !me->GetLocomotionInterface()->HasPotentialGap( me->GetAbsOrigin(), me->GetAbsOrigin() - sideStepSize * left ) )
|
||
|
{
|
||
|
me->PressRightButton();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|