summaryrefslogtreecommitdiff
path: root/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp')
-rw-r--r--game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp1108
1 files changed, 1108 insertions, 0 deletions
diff --git a/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp
new file mode 100644
index 0000000..74ac1d6
--- /dev/null
+++ b/game/server/tf/bot/behavior/medic/tf_bot_medic_heal.cpp
@@ -0,0 +1,1108 @@
+//========= 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;
+}