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

1108 lines
32 KiB

//========= Copyright Valve Corporation, All rights reserved. ============//
// tf_bot_heal.cpp
// Heal a teammate
// Michael Booth, February 2009
#include "cbase.h"
#include "team.h"
#include "tf_player.h"
#include "tf_gamerules.h"
#include "tf_weapon_medigun.h"
#include "bot/tf_bot.h"
#include "bot/behavior/medic/tf_bot_medic_heal.h"
#include "bot/behavior/medic/tf_bot_medic_retreat.h"
#include "bot/behavior/tf_bot_use_teleporter.h"
#include "bot/behavior/scenario/capture_the_flag/tf_bot_fetch_flag.h"
#include "nav_mesh.h"
#include "tier0/vprof.h"
extern ConVar tf_bot_path_lookahead_range;
ConVar tf_bot_medic_stop_follow_range( "tf_bot_medic_stop_follow_range", "75", FCVAR_CHEAT ); // 100
ConVar tf_bot_medic_start_follow_range( "tf_bot_medic_start_follow_range", "250", FCVAR_CHEAT ); // 300
ConVar tf_bot_medic_max_heal_range( "tf_bot_medic_max_heal_range", "600", FCVAR_CHEAT );
ConVar tf_bot_medic_debug( "tf_bot_medic_debug", "0", FCVAR_CHEAT );
ConVar tf_bot_medic_max_call_response_range( "tf_bot_medic_max_call_response_range", "1000", FCVAR_CHEAT );
//---------------------------------------------------------------------------------------------
ActionResult< CTFBot > CTFBotMedicHeal::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
{
m_chasePath.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );
m_patient = NULL;
m_coverArea = NULL;
m_patientAnchorPos = vec3_origin;
m_isPatientRunningTimer.Invalidate();
return Continue();
}
//---------------------------------------------------------------------------------------------
/**
* Choose a player as our "primary" patient. The guy we're going to tether ourselves to
* and keep alive as long as we can.
*/
class CSelectPrimaryPatient : public IVision::IForEachKnownEntity
{
public:
CSelectPrimaryPatient( CTFBot *me, CTFPlayer *currentPatient )
{
m_me = me;
m_medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
m_selected = currentPatient;
}
CTFPlayer *SelectPreferred( CTFPlayer *current, CTFPlayer *contender )
{
// in order of preference
static int preferredClass[] =
{
TF_CLASS_HEAVYWEAPONS,
TF_CLASS_SOLDIER,
TF_CLASS_PYRO,
TF_CLASS_DEMOMAN,
// TF_CLASS_SCOUT,
// TF_CLASS_ENGINEER,
// TF_CLASS_SNIPER,
// TF_CLASS_SPY,
// TF_CLASS_MEDIC,
TF_CLASS_UNDEFINED
};
int i;
if ( TFGameRules()->IsInTraining() )
{
// in training mode, stay on the human trainee
if ( !current || current->IsBot() )
return contender;
return current;
}
if ( !current )
{
return contender;
}
else if ( !contender )
{
return current;
}
// if we are in a squad, always heal the squad leader
if ( m_me->IsInASquad() && m_me->GetSquad()->GetLeader() )
{
if ( m_me->GetSquad()->GetLeader()->entindex() == current->entindex() )
{
return current;
}
if ( m_me->GetSquad()->GetLeader()->entindex() == contender->entindex() )
{
return contender;
}
}
// if current already has another medic (not a dispenser) on him, select contender
int numHealers = current->m_Shared.GetNumHealers();
for ( i=0; i<numHealers; ++i )
{
CBaseEntity *medic = current->m_Shared.GetHealerByIndex(i);
if ( medic && medic->IsPlayer() && !m_me->IsSelf( medic ) )
return contender;
}
// if contender already has another medic (not a dispenser) on him, ignore him
numHealers = contender->m_Shared.GetNumHealers();
for ( i=0; i<numHealers; ++i )
{
CBaseEntity *medic = contender->m_Shared.GetHealerByIndex(i);
if ( medic && medic->IsPlayer() && !m_me->IsSelf( medic ) )
return current;
}
// respond to calls for help
// NOTE: For now, only attend to HUMAN calls for help
CTFPlayer *currentCaller = NULL;
CTFPlayer *contenderCaller = NULL;
CTFBotPathCost cost( m_me, FASTEST_ROUTE );
if ( !current->IsBot() && current->IsCallingForMedic() && m_me->IsRangeLessThan( current, tf_bot_medic_max_call_response_range.GetFloat() ) )
{
// check actual travel range
if ( NavAreaTravelDistance( m_me->GetLastKnownArea(), current->GetLastKnownArea(), cost, 1.5f * tf_bot_medic_max_call_response_range.GetFloat() ) >= 0.0 )
{
currentCaller = current;
}
}
if ( !contender->IsBot() && contender->IsCallingForMedic() && m_me->IsRangeLessThan( contender, tf_bot_medic_max_call_response_range.GetFloat() ) )
{
// check actual travel range
if ( NavAreaTravelDistance( m_me->GetLastKnownArea(), contender->GetLastKnownArea(), cost, 1.5f * tf_bot_medic_max_call_response_range.GetFloat() ) >= 0.0 )
{
contenderCaller = contender;
}
}
if ( currentCaller )
{
if ( contenderCaller )
{
// both are calling for me, and in range - choose most recent caller
if ( currentCaller->GetTimeSinceCalledForMedic() < contender->GetTimeSinceCalledForMedic() )
{
return current;
}
else
{
return contender;
}
}
else
{
return current;
}
}
else if ( contenderCaller )
{
return contender;
}
int currentRank = 999, contenderRank = 999;
for( i=0; preferredClass[i] != TF_CLASS_UNDEFINED; ++i )
{
// for now, heavy, solider, and pyro are equivalent choices
if ( current->GetPlayerClass()->GetClassIndex() == preferredClass[i] )
currentRank = (i < 3) ? 0 : i;
if ( contender->GetPlayerClass()->GetClassIndex() == preferredClass[i] )
contenderRank = (i < 3) ? 0 : i;
}
if ( currentRank == contenderRank )
{
// unless contender is much closer, keep current guy
const float tolerance = 300.0f;
return ( m_me->GetDistanceBetween( current ) - m_me->GetDistanceBetween( contender ) > tolerance ) ? contender : current;
}
if ( currentRank > contenderRank )
{
// switch to contender unless he's far away
const float nearbyRange = 750.0f;
if ( m_me->GetDistanceBetween( contender ) < nearbyRange )
{
return contender;
}
}
return current;
}
bool Inspect( const CKnownEntity &known )
{
if ( !known.GetEntity() || !known.GetEntity()->IsPlayer() || !known.GetEntity()->IsAlive() || !m_me->IsFriend( known.GetEntity() ) )
return true;
CTFPlayer *player = dynamic_cast< CTFPlayer * >( known.GetEntity() );
if ( player == NULL )
return true;
if ( m_me->IsSelf( player ) )
return true;
// always heal the flag carrier, regardless of class
// squads always heal the leader
if ( !player->HasTheFlag() && !m_me->IsInASquad() )
{
if ( player->IsPlayerClass( TF_CLASS_MEDIC ) ||
player->IsPlayerClass( TF_CLASS_SNIPER ) ||
player->IsPlayerClass( TF_CLASS_ENGINEER ) ||
player->IsPlayerClass( TF_CLASS_SPY ) )
{
// these classes can't be our primary heal target (although they will get opportunistic healing
return true;
}
}
// select primary patient for long-term healing
m_selected = SelectPreferred( m_selected, player );
return true;
}
CTFBot *m_me;
CWeaponMedigun *m_medigun;
CTFPlayer *m_selected;
};
//---------------------------------------------------------------------------------------------
CTFPlayer *CTFBotMedicHeal::SelectPatient( CTFBot *me, CTFPlayer *current )
{
CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
if ( medigun )
{
if ( current == NULL || !current->IsAlive() )
{
current = ToTFPlayer( medigun->GetHealTarget() );
}
if ( medigun->IsReleasingCharge() )
{
// don't change targets when using uber
return current;
}
if ( IsReadyToDeployUber( medigun ) && current && IsGoodUberTarget( current ) )
{
// don't change targets if we're ready to uber and we have a good target
return current;
}
}
CSelectPrimaryPatient choose( me, current );
if ( TFGameRules()->IsPVEModeActive() )
{
// assume perfect knowledge
CUtlVector< CTFPlayer * > livePlayerVector;
CollectPlayers( &livePlayerVector, me->GetTeamNumber(), COLLECT_ONLY_LIVING_PLAYERS );
for( int i=0; i<livePlayerVector.Count(); ++i )
{
CKnownEntity known( livePlayerVector[i] );
known.UpdatePosition();
choose.Inspect( known );
}
}
else
{
me->GetVisionInterface()->ForEachKnownEntity( choose );
}
return choose.m_selected;
}
//---------------------------------------------------------------------------------------------
/**
* Return true if the given patient is healthy and safe for now
*/
bool CTFBotMedicHeal::IsStable( CTFPlayer *patient ) const
{
const float safeTime = 3.0f;
// if they are in combat, they are not stable
if ( patient->GetTimeSinceLastInjury( GetEnemyTeam( patient->GetTeamNumber() ) ) < safeTime )
return false;
const float healthyRatio = 1.0f; // can be buffed higher
if ( ( (float)patient->GetHealth() / (float)patient->GetMaxHealth() ) < healthyRatio )
return false;
if ( patient->m_Shared.InCond( TF_COND_BURNING ) )
return false;
if ( patient->m_Shared.InCond( TF_COND_BLEEDING ) )
return false;
return true;
}
//---------------------------------------------------------------------------------------------
class CFindMostInjuredNeighbor : public IVision::IForEachKnownEntity
{
public:
CFindMostInjuredNeighbor( CTFBot *me, float maxRange, bool isInCombat )
{
m_me = me;
m_mostInjured = NULL;
m_injuredHealthRatio = 1.0f;
m_isOnFire = false;
m_maxRange = maxRange;
m_isInCombat = isInCombat;
}
bool Inspect( const CKnownEntity &known )
{
if ( known.GetEntity()->IsPlayer() )
{
CTFPlayer *player = ToTFPlayer( known.GetEntity() );
if ( m_me->IsRangeGreaterThan( player, m_maxRange ) )
return true;
if ( !m_me->IsLineOfFireClear( player->EyePosition() ) )
return true;
if ( !m_me->IsSelf( player ) && player->IsAlive() && player->InSameTeam( m_me ) )
{
// if we're not in combat, opportunistically overheal
float maxHealth = m_isInCombat ? player->GetMaxHealth() : player->m_Shared.GetMaxBuffedHealth();
float healthRatio = (float)player->GetHealth() / maxHealth;
if ( m_isOnFire )
{
// only others on fire who have less health can trump
if ( player->m_Shared.InCond( TF_COND_BURNING ) && healthRatio < m_injuredHealthRatio )
{
m_mostInjured = player;
m_injuredHealthRatio = healthRatio;
}
}
else
{
if ( player->m_Shared.InCond( TF_COND_BURNING ) )
{
// fire trumps
m_mostInjured = player;
m_injuredHealthRatio = healthRatio;
m_isOnFire = true;
}
else
{
if ( healthRatio < m_injuredHealthRatio )
{
m_mostInjured = player;
m_injuredHealthRatio = healthRatio;
}
}
}
}
}
return true;
}
CTFBot *m_me;
CTFPlayer *m_mostInjured;
float m_injuredHealthRatio;
bool m_isOnFire;
float m_maxRange;
bool m_isInCombat;
};
//---------------------------------------------------------------------------------------------
bool CTFBotMedicHeal::CanDeployUber( CTFBot *me, const CWeaponMedigun* pMedigun ) const
{
#ifdef STAGING_ONLY
if ( TFGameRules()->IsMannVsMachineMode() &&
me && me->HasAttribute( CTFBot::PROJECTILE_SHIELD ) &&
pMedigun && ( pMedigun->GetMedigunShield() != NULL ) && pMedigun->HasPermanentShield() && ( ( pMedigun->GetMedigunType() == MEDIGUN_STANDARD ) || ( pMedigun->GetMedigunType() == MEDIGUN_UBER ) ) )
{
return false;
}
#endif
return true;
}
//---------------------------------------------------------------------------------------------
//
// Return true if we our charge is full, and it is an appropriate time to release uber.
// Don't use uber in setup.
// We don't pay attention to our patient here, because we might need to pop uber to save ourselves.
//
bool CTFBotMedicHeal::IsReadyToDeployUber( const CWeaponMedigun* pMedigun ) const
{
if( !pMedigun )
return false;
if ( pMedigun->GetChargeLevel() < pMedigun->GetMinChargeAmount() )
return false;
if ( TFGameRules()->InSetup() )
return false;
return true;
}
//---------------------------------------------------------------------------------------------
bool CTFBotMedicHeal::IsGoodUberTarget( CTFPlayer *who ) const
{
if ( who->IsPlayerClass( TF_CLASS_MEDIC ) ||
who->IsPlayerClass( TF_CLASS_SNIPER ) ||
who->IsPlayerClass( TF_CLASS_ENGINEER ) ||
who->IsPlayerClass( TF_CLASS_SCOUT ) ||
who->IsPlayerClass( TF_CLASS_SPY ) )
{
return false;
}
return false;
}
//---------------------------------------------------------------------------------------------
ActionResult< CTFBot > CTFBotMedicHeal::Update( CTFBot *me, float interval )
{
// if we're in a squad, and the only other members are medics, disband the squad
if ( me->IsInASquad() )
{
CTFBotSquad *squad = me->GetSquad();
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && squad->IsLeader( me ) )
{
return ChangeTo( new CTFBotFetchFlag, "I'm now a squad leader! Going for the flag!" );
}
if ( !squad->ShouldPreserveSquad() )
{
CUtlVector< CTFBot * > memberVector;
squad->CollectMembers( &memberVector );
int i;
for( i=0; i<memberVector.Count(); ++i )
{
if ( !memberVector[i]->IsPlayerClass( TF_CLASS_MEDIC ) )
{
break;
}
}
if ( i == memberVector.Count() )
{
// squad is obsolete
for( i=0; i<memberVector.Count(); ++i )
{
memberVector[i]->LeaveSquad();
}
}
}
}
else
{
// not in a squad - for now, assume whatever mission I was on is over
me->SetMission( CTFBot::NO_MISSION, MISSION_DOESNT_RESET_BEHAVIOR_SYSTEM );
}
m_patient = SelectPatient( me, m_patient );
// prevent a group of medic healing each other in a loop. always heal the top guy in the chain
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && m_patient != NULL && m_patient->IsPlayerClass( TF_CLASS_MEDIC ) )
{
CUtlVector< CBaseEntity* > seenPatients;
seenPatients.AddToTail( m_patient );
while ( CBaseEntity* pTestPatient = m_patient->MedicGetHealTarget() )
{
if ( !pTestPatient->IsPlayer() || seenPatients.Find( pTestPatient ) != seenPatients.InvalidIndex() )
{
break;
}
seenPatients.AddToTail( pTestPatient );
m_patient = ToTFPlayer( pTestPatient );
}
}
if ( m_patient == NULL )
{
// no patients
if ( TFGameRules()->IsMannVsMachineMode() )
{
// no-one is left to heal - get the flag!
return ChangeTo( new CTFBotFetchFlag, "Everyone is gone! Going for the flag" );
}
if ( TFGameRules()->IsPVEModeActive() )
{
// don't retreat, just wait
return Continue();
}
// no patients - retreat to spawn to find another one
return SuspendFor( new CTFBotMedicRetreat, "Retreating to find another patient to heal" );
}
const float anchorRadius = 200.0f;
if ( ( m_patient->GetAbsOrigin() - m_patientAnchorPos ).IsLengthGreaterThan( anchorRadius ) )
{
// our patient is on the move
m_patientAnchorPos = m_patient->GetAbsOrigin();
m_isPatientRunningTimer.Start( 3.0f );
}
// if our patient is teleporting away - follow them!
if ( m_patient->m_Shared.InCond( TF_COND_SELECTED_TO_TELEPORT ) )
{
// find closest teleporter entrance to patient's location
CObjectTeleporter *closeTeleporter = NULL;
float closeRangeSq = FLT_MAX;
CUtlVector< CBaseObject * > objVector;
TheTFNavMesh()->CollectBuiltObjects( &objVector, me->GetTeamNumber() );
for( int i=0; i<objVector.Count(); ++i )
{
if ( objVector[i]->GetType() == OBJ_TELEPORTER )
{
CObjectTeleporter *teleporter = (CObjectTeleporter *)objVector[i];
if ( teleporter->IsEntrance() && teleporter->IsReady() )
{
float rangeSq = ( teleporter->GetAbsOrigin() - m_patient->GetAbsOrigin() ).LengthSqr();
if ( rangeSq < closeRangeSq )
{
closeRangeSq = rangeSq;
closeTeleporter = teleporter;
}
}
}
}
if ( closeTeleporter )
{
return SuspendFor( new CTFBotUseTeleporter( closeTeleporter, CTFBotUseTeleporter::ALWAYS_USE ), "Following my patient through a teleporter" );
}
}
CTFPlayer *actualHealTarget = m_patient;
bool isHealTargetBlocked = true;
bool isActivelyHealing = false;
bool isUsingProjectileShield = false;
const CKnownEntity *knownThreat = me->GetVisionInterface()->GetPrimaryKnownThreat();
CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
if ( medigun )
{
if( medigun->GetMedigunType() == MEDIGUN_RESIST )
{
// If I'm a Vaccinnator medic and am told to prefer a certain type of resist, then cycle to that resist
while( ( me->HasAttribute( CTFBot::PREFER_VACCINATOR_BULLETS ) && medigun->GetResistType() != MEDIGUN_BULLET_RESIST )
|| ( me->HasAttribute( CTFBot::PREFER_VACCINATOR_BLAST ) && medigun->GetResistType() != MEDIGUN_BLAST_RESIST )
|| ( me->HasAttribute( CTFBot::PREFER_VACCINATOR_FIRE ) && medigun->GetResistType() != MEDIGUN_FIRE_RESIST ) )
{
medigun->CycleResistType();
}
}
// if our primary patient is healthy and safe, heal others in our immediate vicinity who need it
// No opportunistic healing in training - focus on the trainee
// No opportunistic healing if I'm in a squad - stay on the leader
if ( !medigun->IsReleasingCharge() && IsStable( m_patient ) && !TFGameRules()->IsInTraining() && !me->IsInASquad() )
{
bool isInCombat = actualHealTarget ? actualHealTarget->GetTimeSinceWeaponFired() < 1.0f : false;
CFindMostInjuredNeighbor neighbor( me, 0.9f * medigun->GetTargetRange(), isInCombat );
me->GetVisionInterface()->ForEachKnownEntity( neighbor );
float hurtRatio = isInCombat ? 0.5f : 1.0f;
if ( neighbor.m_mostInjured && neighbor.m_injuredHealthRatio < hurtRatio )
{
actualHealTarget = neighbor.m_mostInjured;
}
}
// juice 'em
me->GetBodyInterface()->AimHeadTowards( actualHealTarget, IBody::CRITICAL, 1.0f, NULL, "Aiming at my patient" );
if ( medigun->GetHealTarget() == NULL || medigun->GetHealTarget() == actualHealTarget )
{
// only hold fire button if we're healing who we think we're healing
me->PressFireButton();
isHealTargetBlocked = false;
isActivelyHealing = ( medigun->GetHealTarget() != NULL );
}
else
{
// we're not healing who we want to, but we don't want to spam the medigun on/off so much
if ( m_changePatientTimer.IsElapsed() )
{
// stop pressing fire for a moment to allow the medigun to select a new target
m_changePatientTimer.Start( RandomFloat( 1.0f, 2.0f ) );
}
else
{
// keep building uber on wrong patient at least
me->PressFireButton();
}
}
// use uber if we've got it and we're under threat, or our patient was just hurt
bool useUber = false;
if ( IsReadyToDeployUber( medigun ) && CanDeployUber( me, medigun ) )
{
if( medigun->GetMedigunType() == MEDIGUN_RESIST )
{
// uber if I'm getting low and have recently taken damage
if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f )
{
useUber = true;
}
if( m_patient->GetTimeSinceLastInjury( GetEnemyTeam( m_patient->GetTeamNumber() ) ) < 1.0f )
{
useUber = true;
}
}
else
{
// use uber if our patient's health is getting low
const float healthyRatio = 0.5f;
useUber = ( ( (float)m_patient->GetHealth() / (float)m_patient->GetMaxHealth() ) < healthyRatio );
// don't uber our patient if he's already uber from some other source
if ( m_patient->m_Shared.InCond( TF_COND_INVULNERABLE ) || m_patient->m_Shared.InCond( TF_COND_MEGAHEAL ) )
{
useUber = false;
}
// uber if I'm getting low and have recently taken damage
if ( me->GetHealth() < me->GetUberHealthThreshold() )
{
if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f || TFGameRules()->IsMannVsMachineMode() )
{
useUber = true;
}
}
// also uber if I'm about to die!
if ( me->GetHealth() < 25 )
{
useUber = true;
}
// special case for bots in mvm spawn zones
if ( TFGameRules()->IsMannVsMachineMode() )
{
if ( m_patient->m_Shared.InCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED ) &&
me->m_Shared.InCond( TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED ) )
{
useUber = false;
}
}
}
if ( useUber )
{
if ( !m_delayUberTimer.HasStarted() )
{
m_delayUberTimer.Start( me->GetUberDeployDelayDuration() );
}
if ( m_delayUberTimer.IsElapsed() )
{
m_delayUberTimer.Invalidate();
// start the uber
me->PressAltFireButton();
}
}
}
#ifdef STAGING_ONLY
// try to activate shield when I'm not using uber so I don't waste it
if ( TFGameRules()->IsMannVsMachineMode() && me->HasAttribute( CTFBot::PROJECTILE_SHIELD ) && medigun->GetMedigunShield() == NULL )
{
// activate shield ASAP for permanent shield medigun
if ( medigun->HasPermanentShield() )
{
me->PressSpecialFireButton();
isUsingProjectileShield = true;
}
else
{
isUsingProjectileShield = me->m_Shared.IsRageDraining();
// when the rage is ready to deploy and we're not using uber
if ( me->m_Shared.GetRageMeter() >= 100.f && !isUsingProjectileShield && !useUber )
{
// use shield if me or my patient is getting attacked
if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f || m_patient->GetTimeSinceLastInjury( GetEnemyTeam( m_patient->GetTeamNumber() ) ) < 1.0f )
{
me->PressSpecialFireButton();
isUsingProjectileShield = true;
}
}
}
}
#else // remove this when we ship medic shield MVM update
// try to activate shield when I'm not using uber so I don't waste it
if ( TFGameRules()->IsMannVsMachineMode() && me->HasAttribute( CTFBot::PROJECTILE_SHIELD ) )
{
isUsingProjectileShield = me->m_Shared.IsRageDraining();
// when the rage is ready to deploy and we're not using uber
if ( me->m_Shared.GetRageMeter() >= 100.f && !isUsingProjectileShield && !useUber )
{
// use shield if me or my patient is getting attacked
if ( me->GetTimeSinceLastInjury( GetEnemyTeam( me->GetTeamNumber() ) ) < 1.0f || m_patient->GetTimeSinceLastInjury( GetEnemyTeam( m_patient->GetTeamNumber() ) ) < 1.0f )
{
me->PressSpecialFireButton();
isUsingProjectileShield = true;
}
}
}
#endif
}
bool isThreatened = false;
if ( knownThreat && knownThreat->IsVisibleRecently() && knownThreat->GetEntity() )
{
if ( actualHealTarget )
{
float patientRangeSq = me->GetRangeSquaredTo( actualHealTarget );
float threatRangeSq = me->GetRangeSquaredTo( knownThreat->GetEntity() );
isThreatened = threatRangeSq < patientRangeSq;
}
else
{
isThreatened = true;
}
}
bool outOfHealRange = me->IsRangeGreaterThan( actualHealTarget, 1.1f * tf_bot_medic_max_heal_range.GetFloat() );
bool isPatientObscured = actualHealTarget ? !me->IsLineOfFireClear( actualHealTarget->EyePosition() ) : true;
if ( !IsReadyToDeployUber( medigun ) && !me->m_Shared.InCond( TF_COND_INVULNERABLE ) && !isActivelyHealing && !isUsingProjectileShield && ( isThreatened || outOfHealRange || isPatientObscured ) )
{
// patient is too far to heal or obscured, equip combat weapon and defend ourselves while we move into position
me->EquipBestWeaponForThreat( knownThreat );
if ( knownThreat && knownThreat->GetEntity() )
{
me->GetBodyInterface()->AimHeadTowards( knownThreat->GetEntity(), IBody::IMPORTANT, 1.0f, NULL, "Aiming at an enemy" );
}
}
else
{
// equip the medigun and prepare to heal
CBaseCombatWeapon *gun = me->Weapon_GetSlot( TF_WPN_TYPE_SECONDARY );
if ( gun )
{
me->Weapon_Switch( gun );
}
}
// if we are ubering or are ready to uber (or lost our beam lock), stay close and locked on
if ( me->m_Shared.InCond( TF_COND_INVULNERABLE ) || IsReadyToDeployUber( medigun ) || isHealTargetBlocked )
{
// if we're not close or can't see our patient, move closer, otherwise we're good where we are
if ( me->IsRangeGreaterThan( m_patient, tf_bot_medic_stop_follow_range.GetFloat() ) || !me->IsAbleToSee( m_patient, CBaseCombatCharacter::DISREGARD_FOV ) )
{
CTFBotPathCost cost( me, FASTEST_ROUTE );
m_chasePath.Update( me, m_patient, cost );
}
}
else
{
// follow my patient (not my momentary heal target) and stay in cover
if ( m_coverTimer.IsElapsed() || IsVisibleToEnemy( me, me->EyePosition() ) )
{
m_coverTimer.Start( RandomFloat( 0.5f, 1.0f ) );
ComputeFollowPosition( me );
CTFBotPathCost cost( me, FASTEST_ROUTE );
m_coverPath.Compute( me, m_followGoal, cost );
}
m_coverPath.Update( me );
}
return Continue();
}
//---------------------------------------------------------------------------------------------
ActionResult< CTFBot > CTFBotMedicHeal::OnResume( CTFBot *me, Action< CTFBot > *interruptingAction )
{
m_chasePath.Invalidate();
return Continue();
}
//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotMedicHeal::OnStuck( CTFBot *me )
{
return TryContinue();
}
//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotMedicHeal::OnMoveToSuccess( CTFBot *me, const Path *path )
{
return TryContinue();
}
//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotMedicHeal::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
{
return TryContinue();
}
//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotMedicHeal::OnActorEmoted( CTFBot *me, CBaseCombatCharacter *emoter, int emote )
{
if ( !emoter->IsPlayer() )
return TryContinue();
CTFPlayer *emotingPlayer = ToTFPlayer( emoter );
switch( emote )
{
case MP_CONCEPT_PLAYER_MEDIC:
// emoter is calling to be healed by a Medic
// this is handled in SelectPatient()
break;
case MP_CONCEPT_PLAYER_GO:
case MP_CONCEPT_PLAYER_ACTIVATECHARGE:
// if our patient said this, and we have charge, deploy it!
if ( m_patient && emotingPlayer && m_patient->entindex() == emotingPlayer->entindex() )
{
CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
if ( IsReadyToDeployUber( medigun ) && CanDeployUber( me, medigun ) )
{
// start the uber
me->PressAltFireButton();
}
}
break;
}
return TryContinue();
}
//---------------------------------------------------------------------------------------------
QueryResultType CTFBotMedicHeal::ShouldHurry( const INextBot *me ) const
{
// never abandon our patient
return ANSWER_YES;
}
//---------------------------------------------------------------------------------------------
QueryResultType CTFBotMedicHeal::ShouldAttack( const INextBot *bot, const CKnownEntity *them ) const
{
CTFBot *me = (CTFBot *)bot->GetEntity();
// only attack if we're not wielding the medigun
return me->IsCombatWeapon( MY_CURRENT_GUN ) ? ANSWER_YES : ANSWER_NO;
}
//---------------------------------------------------------------------------------------------
QueryResultType CTFBotMedicHeal::ShouldRetreat( const INextBot *bot ) const
{
CTFBot *me = (CTFBot *)bot->GetEntity();
// retreat if stunned
if ( me->m_Shared.IsControlStunned() || me->m_Shared.IsLoserStateStunned() )
return ANSWER_YES;
// never abandon our patient
return ANSWER_NO;
}
//---------------------------------------------------------------------------------------------
class CKnownCollector: public IVision::IForEachKnownEntity
{
public:
virtual bool Inspect( const CKnownEntity &known )
{
m_vector.AddToTail( &known );
return true;
}
CUtlVector< const CKnownEntity * > m_vector;
};
//---------------------------------------------------------------------------------------------
ConVar tf_bot_medic_cover_test_resolution( "tf_bot_medic_cover_test_resolution", "8", FCVAR_CHEAT );
void CTFBotMedicHeal::ComputeFollowPosition( CTFBot *me )
{
VPROF_BUDGET( "CTFBotMedicHeal::ComputeFollowPosition", "NextBot" );
m_followGoal = me->GetAbsOrigin();
if ( m_patient == NULL )
{
return;
}
bool isExposed;
if ( TFGameRules()->IsMannVsMachineMode() && me->GetTeamNumber() == TF_TEAM_PVE_INVADERS )
{
// robot medics in MvM don't care if the enemy sees them
isExposed = false;
}
else
{
isExposed = IsVisibleToEnemy( me, me->EyePosition() );
}
Vector patientForward;
m_patient->EyeVectors( &patientForward );
patientForward.z = 0.0f;
patientForward.NormalizeInPlace();
bool isNearPatient = me->IsRangeLessThan( m_patient, tf_bot_medic_start_follow_range.GetFloat() ) && me->IsAbleToSee( m_patient, CBaseCombatCharacter::DISREGARD_FOV );
if ( !isExposed )
{
// we're not currently visible to any enemies - try to stay that way
if ( isNearPatient )
{
// if we haven't been in combat for awhile, move behind our patient if we're in front of him
Vector toPatient = m_patient->GetAbsOrigin() - me->GetAbsOrigin();
if ( !TFGameRules()->InSetup() && m_patient->GetTimeSinceWeaponFired() > 5.0f && DotProduct( patientForward, toPatient ) < 0.0f )
{
m_followGoal = m_patient->GetAbsOrigin() - tf_bot_medic_stop_follow_range.GetFloat() * patientForward;
}
else
{
// we're good where we are
m_followGoal = me->GetAbsOrigin();
}
}
else
{
// get closer to our patient
m_followGoal = m_patient->GetAbsOrigin();
}
return;
}
// we are visible to one or more enemies - try to move to nearby cover while remaining close enough to heal
Vector closeSafety = me->GetAbsOrigin();
float closeSafetyRangeSq = FLT_MAX;
trace_t trace;
NextBotTraceFilterIgnoreActors traceFilter( NULL, COLLISION_GROUP_NONE );
float angle;
float inc = M_PI / tf_bot_medic_cover_test_resolution.GetFloat();
float radius;
float radiusInc = 100.0f;
float maxRadius = tf_bot_medic_max_heal_range.GetFloat();
CWeaponMedigun *medigun = dynamic_cast< CWeaponMedigun * >( me->m_Shared.GetActiveTFWeapon() );
if ( IsPatientRunning() || IsReadyToDeployUber( medigun ) )
{
// stay close if our patient is on the move, or we have an uber ready
maxRadius = tf_bot_medic_start_follow_range.GetFloat();
}
for( radius = tf_bot_medic_stop_follow_range.GetFloat() + RandomFloat( 0.0f, radiusInc );
radius <= maxRadius;
radius += radiusInc )
{
Vector offset = vec3_origin;
for( angle = 0.0f; angle <= 2.0f * M_PI; angle += inc )
{
SinCos( angle, &offset.y, &offset.x );
Vector pos = m_patient->WorldSpaceCenter() + radius * offset;
// find cover in this direction
UTIL_TraceLine( m_patient->WorldSpaceCenter(), pos, MASK_OPAQUE | CONTENTS_IGNORE_NODRAW_OPAQUE | CONTENTS_MONSTER, &traceFilter, &trace );
Vector actualPos = trace.endpos;
if ( trace.DidHit() )
{
// back up a bit if we hit something, so there is room for the medic to stand
actualPos -= 0.5f * me->GetBodyInterface()->GetHullWidth() * offset;
}
TheNavMesh->GetSimpleGroundHeight( actualPos, &actualPos.z );
// skip spots that are too low
if ( m_patient->GetAbsOrigin().z - actualPos.z > me->GetLocomotionInterface()->GetStepHeight() )
{
if ( tf_bot_medic_debug.GetBool() )
{
NDebugOverlay::Cross3D( actualPos, 5.0f, 255, 100, 0, true, 1.0f );
NDebugOverlay::Line( m_patient->WorldSpaceCenter(), actualPos, 255, 100, 0, true, 1.0f );
}
continue;
}
actualPos.z += HumanEyeHeight;
if ( IsVisibleToEnemy( me, actualPos ) )
{
// this spot is visible to a threat
if ( tf_bot_medic_debug.GetBool() )
{
//NDebugOverlay::Circle( actualPos, 5.0f, 255, 0, 0, 255, true, 1.0f );
NDebugOverlay::Cross3D( actualPos, 5.0f, 255, 0, 0, true, 1.0f );
NDebugOverlay::Line( m_patient->WorldSpaceCenter(), actualPos, 255, 0, 0, true, 1.0f );
}
}
else
{
// no threat can see this spot
// keep the closest safe position to our current position to minimize exposure
float rangeSq = ( me->EyePosition() - actualPos ).LengthSqr();
if ( rangeSq < closeSafetyRangeSq )
{
closeSafetyRangeSq = rangeSq;
closeSafety = actualPos;
}
if ( tf_bot_medic_debug.GetBool() )
{
//NDebugOverlay::Circle( actualPos, 5.0f, 0, 255, 0, 255, true, 1.0f );
NDebugOverlay::Cross3D( actualPos, 5.0f, 0, 255, 0, true, 1.0f );
NDebugOverlay::Line( m_patient->WorldSpaceCenter(), actualPos, 0, 255, 0, true, 1.0f );
}
}
}
}
m_followGoal = closeSafety;
}
//---------------------------------------------------------------------------------------------
bool CTFBotMedicHeal::IsVisibleToEnemy( CTFBot *me, const Vector &where ) const
{
CKnownCollector known;
me->GetVisionInterface()->ForEachKnownEntity( known );
trace_t trace;
for( int i=0; i<known.m_vector.Count(); ++i )
{
CBaseCombatCharacter *threat = known.m_vector[i]->GetEntity()->MyCombatCharacterPointer();
if ( threat && me->IsEnemy( threat ) )
{
if ( threat->IsLineOfSightClear( where, CBaseCombatCharacter::IGNORE_ACTORS ) )
{
return true;
}
}
}
return false;
}